125 lines
6.4 KiB
Python
125 lines
6.4 KiB
Python
"""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)
|