#!/usr/bin/env python3 # Landvex produktionspaket — verifieringsgrind # Körs från paketroten: python3 00-styrning/verifiera.py # GRÖNT krävs för merge/deploy. Kontrollerar: # 1) SHA-256 mot MANIFEST 2) schemaregler (0 fel) 3) grafen reproduceras exakt # 4) ID-registret konsistent 5) referensintegritet (bounties, demosvar, relationer) import json, re, hashlib, sys from pathlib import Path ROT = Path(__file__).resolve().parent.parent PREDIKAT = {"monteras_pa","star_pa","ansluts_med","matas_fran","ingar_i","del_av", "ersatter","ersatts_av","kompatibel_med","forvaxlas_med","tillverkas_av","foljer_standard","variant_av"} VERIF = {"obekraftad","kallbelagd","faltverifierad","tillverkarbekraftad"} DOMANER = {"TRP","ELN","VAT","TEL","BYG","PRK","JVG","HMN","FLG"} ID_RE = re.compile(r"^LVX-(TRP|ELN|VAT|TEL|BYG|PRK|JVG|HMN|FLG)-[0-9]{4}$|^LVX-P-[0-9]{7}$") fel = [] def load(rel): with open(ROT/rel, encoding="utf-8") as f: return json.load(f) # ---- 1. Checksummor ---- man = load("00-styrning/MANIFEST.json") for post in man["filer"]: p = ROT/post["sokvag"] if not p.exists(): fel.append(f"MANIFEST: saknad fil {post['sokvag']}"); continue h = hashlib.sha256(p.read_bytes()).hexdigest() if h != post["sha256"]: fel.append(f"MANIFEST: checksumma avviker {post['sokvag']}") # ---- 2. Schemaregler ---- poster = {p["lvx_id"]: p for p in load("10-data/lvx-klassposter-master-v3.json")["poster"]} modeller = {} for fn in ["lvx-produktmodeller-vag2.json","lvx-produktmodeller-vag3.json","lvx-produktmodeller-auto.json"]: for m in load(f"10-data/{fn}")["poster"]: modeller[m["lvx_id"]] = m def kolla(rec, typ): rid = rec.get("lvx_id","?") if not ID_RE.match(rid): fel.append(f"{rid}: ID-format") if rec.get("doman") not in DOMANER: fel.append(f"{rid}: domän") if not (rec.get("namn",{}).get("sv") and rec["namn"].get("en")): fel.append(f"{rid}: namn") prov = rec.get("proveniens",{}) if prov.get("verifieringsniva") not in VERIF: fel.append(f"{rid}: verifieringsnivå") for pr in rec.get("problem",[]): if not pr.get("kalla"): fel.append(f"{rid}: problem utan källa") for rel in rec.get("relationer",[]): if rel.get("predikat") not in PREDIKAT: fel.append(f"{rid}: predikat") if typ=="modell" and not rec.get("parent"): fel.append(f"{rid}: parent") if prov.get("verifieringsniva")=="kallbelagd" and not prov.get("kallor"): fel.append(f"{rid}: källbelagd utan källor") for p in poster.values(): kolla(p,"klass") for m in modeller.values(): kolla(m,"modell") # ---- 3. Grafen reproduceras ---- kanter, olosta = [], set() def multi(m): return " / " in m and all(ID_RE.match(x.strip()) for x in m.split(" / ")) def kant(s,pred,obj): typ = "lvx" if ID_RE.match(obj or "") else ("standard" if pred=="foljer_standard" else "text") if typ=="text": olosta.add(obj) kanter.append(typ) def emit(s,pred,m): for x in (m.split(" / ") if multi(m) else [m]): kant(s,pred,x.strip()) for p in poster.values(): for rel in p.get("relationer",[]): emit(p["lvx_id"],rel["predikat"],rel["mal"]) for st in p.get("standarder",[]): if st.get("beteckning"): kant(p["lvx_id"],"foljer_standard",st["beteckning"]) for m in modeller.values(): kant(m["lvx_id"],"tillhor_klass",m["parent"]) for e in m.get("historik",{}).get("ersatter",[]): kant(m["lvx_id"],"ersatter",e) for e in m.get("historik",{}).get("ersatts_av",[]): kant(m["lvx_id"],"ersatts_av",e) for rel in m.get("relationer",[]): emit(m["lvx_id"],rel["predikat"],rel["mal"]) g = load("10-data/lvx-graf-kanter-v3.json")["statistik"] fakta = {"kanter_totalt": len(kanter), "upplosta_lvx_kanter": kanter.count("lvx"), "standardkanter": kanter.count("standard"), "textnoder_olosta": len(olosta)} for k,v in fakta.items(): if g.get(k) != v: fel.append(f"GRAF: {k} förväntat {g.get(k)}, fick {v}") # ---- 4. ID-registret ---- reg = load("10-data/lvx-id-register.json") max_p = max(int(m[6:]) for m in modeller) if reg["P_nasta"] <= max_p: fel.append(f"REGISTER: P_nasta {reg['P_nasta']} ≤ högsta använda {max_p}") for rid in poster: dom, num = rid[4:7], rid[8:] tier, nn = num[1], int(num[2:]) if reg["klass_nasta"].get(dom,{}).get(tier,0) <= nn: fel.append(f"REGISTER: {dom} tier {tier} nästa ≤ använda {nn}") # ---- 5. Referensintegritet ---- alla = set(poster) | set(modeller) for b in load("10-data/lvx-zoomer-bounties-vag1.json")["bounties"]: if b["mal_lvx"] not in alla: fel.append(f"BOUNTY {b['bounty_id']}: okänt mål {b['mal_lvx']}") for f_ in load("10-data/lvx-bounty-kandidater-vag7.json")["forslag"]: if f_["mal_lvx"] not in alla: fel.append(f"BOUNTYFÖRSLAG: okänt mål {f_['mal_lvx']}") demoraw = (ROT/"30-produkt/lvx-api-demosvar.json").read_text(encoding="utf-8") for rid in set(re.findall(r"LVX-(?:TRP|ELN|VAT|TEL|BYG|PRK|JVG|HMN|FLG)-[0-9]{4}|LVX-P-[0-9]{7}", demoraw)): if rid not in alla: fel.append(f"DEMOSVAR: okänt lvx_id {rid}") for p in poster.values(): for rel in p.get("relationer",[]): m = rel["mal"] for x in (m.split(" / ") if multi(m) else [m]): if ID_RE.match(x.strip()) and x.strip() not in alla: fel.append(f"{p['lvx_id']}: relation mot okänt ID {x.strip()}") # ---- Resultat ---- print(f"Filer i manifest: {len(man['filer'])} | Klasser: {len(poster)} | Modeller: {len(modeller)}") print(f"Graf: {fakta['kanter_totalt']} kanter ({fakta['upplosta_lvx_kanter']} lvx / {fakta['standardkanter']} standard / {fakta['textnoder_olosta']} textnoder)") if fel: print(f"\nRÖTT — {len(fel)} fel:") for e in fel: print(" !!", e) sys.exit(1) print("\nGRÖNT — paketet verifierat. Grind passerad.")