Files
boc/projects/landvex/arkiv/byggscript/konsolidera.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

157 lines
7.0 KiB
Python

#!/usr/bin/env python3
# Landvex IOL — våg 4: datastädning, validering och grafexport
import json, re, sys
from pathlib import Path
OUT = Path("/mnt/user-data/outputs")
PREDIKAT = {"monteras_pa","star_pa","ansluts_med","matas_fran","ingar_i","del_af" if False else "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}$")
def load(name):
with open(OUT / name, encoding="utf-8") as f:
return json.load(f)
# ---------- 1. Ladda klassposter ----------
klass_filer = ["lvx-klassposter-trp.json","lvx-klassposter-vat.json",
"lvx-klassposter-eln.json","lvx-klassposter-tel-prk.json"]
poster = {}
for fn in klass_filer:
for p in load(fn)["poster"]:
poster[p["lvx_id"]] = p
# Våg 2-uppdateringar ersätter
uppdaterade = 0
for p in load("lvx-klassposter-vag2-uppdaterade.json")["poster"]:
poster[p["lvx_id"]] = p
uppdaterade += 1
# ---------- 2. Relationskarta ----------
t2 = load("lvx-taxonomi-tier2.json")
karta = {}
for rad in t2["relationskarta"]:
nytt = rad["nytt_id"]
if "/" in nytt: # dubbelmappning: skyltstolpe/portal
karta[rad["platshallare"]] = ("LVX-TRP-0201", "portalvariant → LVX-TRP-0205")
else:
karta[rad["platshallare"]] = (nytt, None)
for txt in ["Fundament/fotplatta", "Fundament/stolpe", "Direkt i mark / fundament"]:
karta[txt] = ("LVX-TRP-0203", None)
def multi_lvx(mal):
return " / " in mal and all(ID_RE.match(x.strip()) for x in mal.split(" / "))
remappade, olosta = 0, {}
for p in poster.values():
for rel in p.get("relationer", []):
mal = rel.get("mal","")
if mal in karta:
nytt, notis = karta[mal]
rel["mal"] = nytt
if notis:
rel["beskrivning"] = (rel.get("beskrivning","") + " | " + notis).strip(" |")
remappade += 1
elif rel.get("predikat") == "foljer_standard":
pass # standardnod — hanteras i grafexporten
elif multi_lvx(mal):
pass # multipla LVX-mål — splittas i grafexporten
elif not ID_RE.match(mal):
olosta.setdefault(mal, 0)
olosta[mal] += 1
# ---------- 3. Ladda produktmodeller ----------
modeller = {}
for fn in ["lvx-produktmodeller-vag2.json","lvx-produktmodeller-vag3.json"]:
for m in load(fn)["poster"]:
modeller[m["lvx_id"]] = m
# ---------- 4. Validering ----------
fel = []
def kolla(rec, typ):
rid = rec.get("lvx_id","?")
if not ID_RE.match(rid): fel.append(f"{rid}: ogiltigt ID-format")
if rec.get("doman") not in DOMANER: fel.append(f"{rid}: ogiltig domän")
namn = rec.get("namn",{})
if not (namn.get("sv") and namn.get("en")): fel.append(f"{rid}: namn.sv/en saknas")
if rec.get("posttyp") not in {"objektklass","produktmodell"}: fel.append(f"{rid}: posttyp")
prov = rec.get("proveniens",{})
if prov.get("verifieringsniva") not in VERIF: fel.append(f"{rid}: verifieringsnivå")
if not prov.get("skapad"): fel.append(f"{rid}: proveniens.skapad saknas")
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}: okänt predikat {rel.get('predikat')}")
if typ=="modell" and not rec.get("parent"): fel.append(f"{rid}: parent saknas")
if prov.get("verifieringsniva")=="kallbelagd" and not prov.get("kallor"):
fel.append(f"{rid}: källbelagd utan kallor")
for p in poster.values(): kolla(p,"klass")
for m in modeller.values(): kolla(m,"modell")
# ---------- 5. Grafkanter ----------
kanter = []
def kant(s, pred, obj, niva, beskr=""):
typ = "lvx" if ID_RE.match(obj or "") else ("standard" if pred=="foljer_standard" else "text")
kanter.append({"subjekt": s, "predikat": pred, "objekt": obj, "objekt_typ": typ,
"niva": niva, **({"beskrivning": beskr} if beskr else {})})
def emit(s, pred, mal, niva, beskr=""):
if multi_lvx(mal):
for x in mal.split(" / "):
kant(s, pred, x.strip(), niva, beskr)
else:
kant(s, pred, mal, niva, beskr)
for p in poster.values():
for rel in p.get("relationer", []):
emit(p["lvx_id"], rel["predikat"], rel["mal"], "klass", rel.get("beskrivning",""))
for st in p.get("standarder", []):
if st.get("beteckning"):
kant(p["lvx_id"], "foljer_standard", st["beteckning"], "klass", st.get("roll",""))
for m in modeller.values():
kant(m["lvx_id"], "tillhor_klass", m["parent"], "modell") # ontologibeslut v1.1
hist = m.get("historik", {})
for e in hist.get("ersatter", []): kant(m["lvx_id"], "ersatter", e, "modell")
for e in hist.get("ersatts_av", []): kant(m["lvx_id"], "ersatts_av", e, "modell")
for rel in m.get("relationer", []):
emit(m["lvx_id"], rel["predikat"], rel["mal"], "modell", rel.get("beskrivning",""))
lvx_kanter = sum(1 for k in kanter if k["objekt_typ"]=="lvx")
std_kanter = sum(1 for k in kanter if k["objekt_typ"]=="standard")
# ---------- 6. Skriv ut ----------
master = {
"beskrivning": "Landvex IOL — konsoliderade Tier 1-klassposter v1. Våg 2-uppdateringar inarbetade, relationer ommappade enligt Tier 2-relationskartan. Ersätter de fyra våg 1-filerna + vag2-uppdaterade som arbetskopia.",
"genererad": "2026-07-04",
"antal_poster": len(poster),
"poster": [poster[k] for k in sorted(poster)]
}
with open(OUT/"lvx-klassposter-master-v1.json","w",encoding="utf-8") as f:
json.dump(master,f,ensure_ascii=False,indent=2)
graf = {
"beskrivning": "Landvex IOL — grafexport v1. Kanter ur klass- och modellposter. objekt_typ: lvx=upplöst nod, standard=standardbeteckning, text=olöst platshållare (framtida klass). Predikatet tillhor_klass (modell→klass) är ontologibeslut v1.1.",
"genererad": "2026-07-04",
"noder": {"objektklasser_tier1": len(poster), "objektklasser_tier2": len(t2["objektklasser"]), "produktmodeller": len(modeller)},
"statistik": {"kanter_totalt": len(kanter), "upplosta_lvx_kanter": lvx_kanter, "standardkanter": std_kanter,
"textnoder_olosta": len(olosta)},
"olosta_platshallare": [{"text": k, "forekomster": v} for k,v in sorted(olosta.items())],
"kanter": kanter
}
with open(OUT/"lvx-graf-kanter-v1.json","w",encoding="utf-8") as f:
json.dump(graf,f,ensure_ascii=False,indent=2)
print(f"Klassposter: {len(poster)} (varav {uppdaterade} våg2-uppdaterade inarbetade)")
print(f"Produktmodeller: {len(modeller)}")
print(f"Relationer ommappade till Tier 2-ID: {remappade}")
print(f"Grafkanter: {len(kanter)} totalt | {lvx_kanter} upplösta LVX-kanter | {std_kanter} standardkanter")
print(f"Olösta textnoder: {len(olosta)}")
for k,v in sorted(olosta.items()): print(f" - {k} ({v})")
print(f"Valideringsfel: {len(fel)}")
for e in fel: print(f" ! {e}")