"""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".*?"): body = re.sub(pat, "", body) nums = set(re.findall(r"(?=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])