Files
cyber-tesi/scripts/lint.py
T

125 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Controlli meccanici della wiki (vedi CLAUDE.md, sezione Lint). Uso: python scripts/lint.py
Stampa solo segnalazioni; non modifica nulla. Le verifiche semantiche (contraddizioni, valori) restano all'LLM."""
import json
import re
from collections import Counter
from pathlib import Path
root = Path(__file__).resolve().parent.parent
BS = "\\"
pages = [p for p in root.glob("wiki/**/*.md") if "figures" not in p.parts and p.name != "bibliografia.md"]
allp = list(root.glob("wiki/**/*.md")) + [root / "index.md"]
stems = {p.stem for p in root.glob("wiki/**/*.md")} | {p.stem for p in root.glob("raw/fonti/*.md")} | {"index", "log", "CLAUDE"}
imgs = {p.name for p in root.glob("raw/immagini/*")}
tesi = (root / "raw/tesi.md").read_text(encoding="utf8")
n_lines = len(tesi.split("\n"))
LINK = re.compile(r"(!?)\[\[([^\]]+)\]\]")
FIG = re.compile(r"\[\[(fig-[\w-]+)(?:\||\]|\\)")
REF = re.compile(r"\[\[(\d\d)-[\w-]+(?:\||\\)")
# 1 link rotti, embed mancanti, pagine orfane
bad, inbound = [], Counter()
for p in allp:
for m in LINK.finditer(p.read_text(encoding="utf8")):
tg = m.group(2).split("|")[0].split("#")[0].replace(BS, "")
if m.group(1):
if Path(tg).name not in imgs:
bad.append((p.name, tg))
elif tg not in stems:
bad.append((p.name, tg))
else:
inbound[tg] += 1
print("1 link/embed rotti:", bad or "nessuno")
print("1 pagine orfane:", [s for s in stems if not inbound[s] and s not in ("CLAUDE", "log") and not s[:2].isdigit()] or "nessuna")
for p in pages:
t = p.read_text(encoding="utf8")
m = re.match(r"---\n(.*?)\n---\n(.*)", t, re.S)
if not m:
print("2 frontmatter assente:", p.name)
continue
fm, body = m.groups()
# 2 chiavi obbligatorie
miss = [k for k in ("title", "type", "tags", "created", "updated", "tesi_loc", "status") if not re.search(rf"^{k}:", fm, re.M)]
if miss:
print("2 chiavi mancanti:", p.name, miss)
# 3 figure e riferimenti del frontmatter coerenti con il corpo
fmf, bf = set(re.findall(r'"(fig-[\w-]+)"', fm)), set(FIG.findall(body))
if re.search(r"^figures:", fm, re.M) and fmf != bf:
print("3 figure fm/corpo:", p.name, "solo fm", sorted(fmf - bf), "solo corpo", sorted(bf - fmf))
r = re.search(r"^refs: \[(.*?)\]", fm, re.M)
fr = {int(x) for x in re.findall(r"\d+", r.group(1))} if r else set()
br = {int(x) for x in REF.findall(body)}
if r and fr != br:
print("3 refs fm/corpo:", p.name, "solo fm", sorted(fr - br), "solo corpo", sorted(br - fr))
# 4 tesi_loc entro il file
loc = re.search(r'tesi_loc: "(.*?)"', fm)
for a, b in re.findall(r"r\.(\d+)(?:-(\d+))?", loc.group(1) if loc else ""):
if max(int(a), int(b or 0)) > n_lines:
print("4 riga oltre la fine di tesi.md:", p.name, a, b)
# 5 numero del riferimento coerente con il nome della scheda
for p in allp:
for m in re.finditer(r"\[\[(\d\d)-[\w-]+\\?\|(\d+)\]\]", p.read_text(encoding="utf8")):
if int(m.group(1)) != int(m.group(2)):
print("5 riferimento incoerente:", p.name, m.group(0))
# 6 tabelle senza riga vuota prima
for p in allp:
L = p.read_text(encoding="utf8").split("\n")
for i, l in enumerate(L):
if l.startswith("|") and i and L[i - 1].strip() and not L[i - 1].startswith("|"):
print("6 tabella senza riga vuota prima:", p.name, i + 1)
# 7 numeri nelle pagine che non compaiono in tesi.md (da rivedere a mano: molti sono legittimi, es. conversioni)
tn = set(re.findall(r"\d+(?:[.,]\d+)?", tesi))
for p in pages:
body = re.sub(r"^---\n.*?\n---\n", "", p.read_text(encoding="utf8"), flags=re.S)
for pat in (r"!?\[\[[^\]]*\]\]", r"\br\.\d+(?:-\d+)?(?:, ?\d+)*", r"§[\d.]+",
r"[Ff]ig(?:ura|\.)? ?\d+|Tab(?:ella|\.)? ?\d+|Eq(?:uazione|\.)? ?\d+|rif\. [\d,]+", r"<su[bp]>.*?</su[bp]>"):
body = re.sub(pat, "", body)
nums = set(re.findall(r"(?<![\w.,])\d+(?:[.,]\d+)?(?!\w)", body))
miss = sorted(n for n in nums if n not in tn and n.replace(",", ".") not in tn and n.replace(".", ",") not in tn)
if miss:
print("7 numeri non in tesi.md:", p.name, miss[:40])
# 8 slug concetto del catalogo usati da >=2 figure (candidati a pagina concetto)
cat = json.loads((root / "raw/immagini.json").read_text(encoding="utf8"))
c = Counter(x for f in cat["figures"] for x in f.get("concepts", []))
print("8 slug in >=2 figure:", [(k, v) for k, v in c.most_common() if v >= 2])
# 9 figure citate per numero ("Fig. N") senza wikilink [[fig-...]] né embed del file nella stessa pagina
# (escluse le pagine analyses/ e riassunto-tesi, che usano numerazioni di altri documenti o citano per ragioni diverse)
n2f = {f["number"]: f for f in cat["figures"] if f["number"]}
FIGLINK = re.compile(r"\[\[((?:ris-)?fig-[\w-]+)")
EMB = re.compile(r"!\[\[immagini/([0-9a-f]{64})")
nav = {"tesi", "campioni", "dati", "glossario", "overview", "tabelle-indice", "percorso-sintetico", "riassunto-tesi"}
for p in pages:
if "analyses" in p.parts or p.stem == "riassunto-tesi":
continue
body = re.sub(r"^---\n.*?\n---\n", "", p.read_text(encoding="utf8"), flags=re.S)
ids, hs = set(FIGLINK.findall(body)), set(EMB.findall(body))
miss = set()
for m in re.finditer(r"Figg?\.\s*(\d+)(?:\s*[-–]\s*(\d+))?", re.sub(r"\[\[[^\]]*\]\]", "", body)):
for n in range(int(m.group(1)), int(m.group(2) or m.group(1)) + 1):
f = n2f.get(n)
if f and f["id"] not in ids and not {x["file"][:-4] for x in f["panels"]} & hs:
miss.add(n)
if miss:
print("9 figure citate senza link né embed:", p.name, sorted(miss))
# 10 figure mai collegate da una pagina tematica (solo da navigazione o da analisi)
topical = set()
for p in pages:
if p.stem in nav or "analyses" in p.parts:
continue
t = p.read_text(encoding="utf8")
topical |= set(FIGLINK.findall(t))
topical |= {x for x in (pn["file"][:-4] for f in cat["figures"] + cat.get("riassunto", {}).get("figures", []) for pn in f["panels"]) if x in set(EMB.findall(t))}
ids_all = {f["id"] for f in cat["figures"]} | {f["id"] for f in cat.get("riassunto", {}).get("figures", [])}
file2id = {pn["file"][:-4]: f["id"] for f in cat["figures"] + cat.get("riassunto", {}).get("figures", []) for pn in f["panels"]}
topical_ids = {file2id.get(x, x) for x in topical}
orph = sorted(i for i in ids_all - topical_ids if not i.startswith("ris-fig"))
if orph:
print("10 figure della tesi senza pagina tematica:", orph)