7d40cf95bf
- FastAPI Identify API (port 8081) - Neo4j propertygraf (64 noder, 80 kanter) - Ingest-pipeline (Python) - Byggscript + integrationstester - Docker Compose setup
112 lines
5.6 KiB
Python
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.")
|