Files
boc/projects/landvex/00-styrning/verifiera.py
T
Bernt 7d40cf95bf landvex: IOL v3 + Identify API + Neo4j-graf + byggpipeline
- FastAPI Identify API (port 8081)
- Neo4j propertygraf (64 noder, 80 kanter)
- Ingest-pipeline (Python)
- Byggscript + integrationstester
- Docker Compose setup
2026-07-05 05:55:48 +00:00

112 lines
5.6 KiB
Python

#!/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.")