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
This commit is contained in:
Bernt
2026-07-05 05:55:48 +00:00
parent 8dcca96185
commit 7d40cf95bf
61 changed files with 22330 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
# AGENTARMÉ — implementeringsorder v1
## Landvex produktionspaket · 58 agenter · 11 skvadroner · 4 faser
Detta dokument är arbetsordern. Paketets filer är enda sanningskällan. Ingenting implementeras som inte går att spåra hit.
## Hårda regler — gäller varje agent, utan undantag
1. **Enda sanningskälla:** `10-data/` + `lvx-objekt-schema.json`. Ingen agent uppfinner fakta, fält, klasser, standarder eller ID:n.
2. **Aldrig mock i kundnära väg.** Saknad förmåga levereras som ärligt svar med konfidens + automatisk bounty (mönstret i `30-produkt/lvx-api-demosvar.json`, case 3) — aldrig som låtsad träff, aldrig som stubbe i miljö en kund kan nå.
3. **PLATSHÅLLARE serialiseras aldrig** ut ur något publikt gränssnitt. Filtret sitter i serialiseringskoden och testas.
4. **Källa på varje påstående.** Publika svar bär `verifieringsniva`, `konfidens` och `kallor[]` — obligatoriska fält enligt API-kontraktet.
5. **Standarder refereras** med organ + beteckning + roll. Standardtext återges aldrig (juridikregeln).
6. **ID:n mintas endast** via registertjänsten (`lvx-id-register.json`-logiken i `ingest.py`). ID:n återanvänds aldrig, av ingen.
7. **Verifieringstrappan är enkelriktad:** obekräftad → källbelagd (kräver källor) → fältverifierad (endast fotoflödet) → tillverkarbekräftad (endast portalflödet). Ingen kod får sätta nivå utanför sitt flöde.
8. **Grind före merge/deploy:** `00-styrning/verifiera.py` grönt + checksummor mot `MANIFEST.json`. Rött stoppar allt.
9. **Ontologi-/schemaändring** endast via Ändringsrådet (skvadron A) med versionslyft och migrering.
10. **Osäkerhet uttrycks, döljs aldrig.** Konfidens är produktyta, inte intern metadata.
## Skvadroner (58 agenter)
**A · Programkontor & arkitektur (3)** — äger detta dokument, MANIFEST, gränssnittskontrakt och Ändringsrådet. Godkänner fasgrindar. *Input:* allt. *Output:* beslut, versionslyft.
**B · Plattform & SRE (6)** — Fas 0: dev/stage/prod, IaC, hemligheter, observabilitet, larm, backup/DR. Driftsätter samtliga tjänster. *DoD:* verifiera.py körs grönt i CI på varje miljö; återställningstest genomfört.
**C · Datakärna & graf (6)** — laddar `lvx-klassposter-master-v3.json` + modellfilerna till dokument-+graflager; grafen härleds alltid ur posterna (aldrig handredigerad); ID-registertjänst med atomär mintning. *DoD:* laddad databas reproducerar exakt grafstatistiken i `lvx-graf-kanter-v3.json` (176 kanter · 80 LVX · 82 standard).
**D · Ingest-automation (7)** — produktifierar `20-automation/ingest.py` till tjänst: tre köer (schemalagd / kunddriven / fält), skördare med källvitlista och referatregel, extraktor-API bakom `lvx-extraktionsprompt.md`, granskningskö-UI för konflikter, körningsrapporter. *DoD:* våg 7-batchen i `intag/` replayad genom tjänsten ger identiskt resultat (4 nivålyft, 1 mint, 0 fel).
**E · Object API (7)** — implementerar `30-produkt/lvx-pilot-api-spec.md` exakt: 6 endpoints, svarskontraktet, PLATSHÅLLARE-filter, feedback→bounty, nycklar, rate limits och **usage-mätning per identify för debitering** (SaaS-kärnan). *DoD:* kontraktstester gröna; de tre golden-casen reproduceras via API:t mot riktig data.
**F · Vision & identify (6)** — utvärderings- och träningsflöde från bountydata; identify-modell bakom API:t. Tills modellen når beslutad tröskel svarar identify med klasskandidat + ärlig konfidens — regel 2 i praktiken. *DoD:* golden-case-regressionssvit; konfidenskalibrering rapporterad.
**G · QUIXZOOM fältflöde (6)** — bounty-motorn (`lvx-zoomer-bounties-vag1.json` + auto-förslagen): poäng, först-på-individ ×2, dubblettkontroll hash+geo, hårdkodade säkerhets- och integritetsregler (auto-blurr, inga registreringsskyltar), foto→OCR→**fältverifierad**-lyft, LVX-I-individregister. *DoD:* ett komplett e2e-lyft källbelagd→fältverifierad i stage med riktigt foto.
**H · Tillverkarportal (4)** — flödet som sätter **tillverkarbekräftad**: claims kopplade till modellposter, spårbara godkännanden, aldrig standardtext. *DoD:* en modellpost lyft e2e i stage.
**I · Säkerhet, juridik & compliance (4)** — källpolicy (referat, aldrig kopiering), licensregister per källa, GDPR för positionsdata, åtkomstmodell, pentest. *DoD:* godkänd granskning av serialisering + datalager.
**J · QA & verifiering (5)** — äger och utökar `verifiera.py`; kontraktstester mot API-spec; datagrind 0 valideringsfel; golden-case-replay i CI; last- och kaostester. *DoD:* grindarna blockerar på riktigt (bevisat med avsiktligt rött bygge).
**K · Kund, pilot & SaaS-paketering (4)** — Pilot/Pro/Enterprise enligt API-specens paketering, mätarbaserad debitering på identify, onboarding, pilotworkshop med `40-strategi/landvex-pilotpitch.pptx`. *DoD:* första designpartner tecknad till pilotstart.
## Faser & grindar
**Fas 0 — Grund (B, C, J):** miljöer uppe, data laddad, verifiera.py grönt i CI.
*Grind 0:* checksummor + 0 valideringsfel + grafstatistik reproducerad.
**Fas 1 — Produkt (D, E, F, I):** ingest-tjänst och Object API live mot riktig data.
*Grind 1:* golden cases via API:t; våg 7-replay identisk; pentest av API-ytan.
**Fas 2 — Flyhjul (G, F):** fältflödet live; feedback→bounty→foto→fältverifierad sluter loopen.
*Grind 2:* första e2e-nivålyftet; kunddrivna kön mätbar.
**Fas 3 — Skala (H, K, D):** tillverkarportal, fler skördebatchar genom pipelinen, pilotkund onboardad.
*Grind 3:* tillverkarbekräftad e2e; kostnad per verifierat faktum rapporteras sjunkande.
Beroenden: C före D/E · E före F/G-exponering · G före H:s fältdata · J tvärs igenom alla faser.
## Eskalering
Datakonflikt → granskningskö (D) → Ändringsrådet (A). Schemabehov → A med versionslyft. Säkerhets-/juridikfynd → I stoppar berörd leverans omedelbart. Oklarhet om fakta → svaret är alltid: gå till källfilen; finns det inte där, finns det inte.
+210
View File
@@ -0,0 +1,210 @@
{
"paket": "landvex-produktionspaket",
"version": "1.0",
"genererad": "2026-07-04",
"statistik": {
"objektklasser": 37,
"kallbelagda_klasser": 35,
"produktmodeller": 27,
"grafkanter": 176,
"aktiva_bounties": 16,
"bounty_forslag": 17,
"valideringsfel": 0
},
"kanonisk_not": "10-data/ ersätter alla tidigare versioner (master v1/v2, delfiler). Arkiv kvar i arbetsytan utanför paketet.",
"antal_filer": 32,
"filer": [
{
"sokvag": "00-styrning/AGENTARME.md",
"roll": "styrning",
"bytes": 6037,
"sha256": "dcacfff60fa3354729fe758b0baf2a0478faa011acd679686709de813dac85c1"
},
{
"sokvag": "00-styrning/OVERSIKT.md",
"roll": "styrning",
"bytes": 2497,
"sha256": "6005162be3fd4921255bf39eed7d6ec8f8ecbffa058f3e32d367410375fe0880"
},
{
"sokvag": "00-styrning/verifiera.py",
"roll": "styrning",
"bytes": 5745,
"sha256": "1723e94897d87f3a83a22995ae034aca88302b70611ad3b12dfb51888503ec64"
},
{
"sokvag": "10-data/lvx-bounty-kandidater-vag7.json",
"roll": "data (enda sanningskälla)",
"bytes": 4798,
"sha256": "d6f3e8a192c769a85fcc7c5e0043128838ccbb6074c38857943f8b558cd9d682"
},
{
"sokvag": "10-data/lvx-exempel-brunnsbetackning.json",
"roll": "data (enda sanningskälla)",
"bytes": 4700,
"sha256": "cc7f09f629512829330f91232523d4f5d33bef92983e4c2a24631064d703454b"
},
{
"sokvag": "10-data/lvx-graf-kanter-v3.json",
"roll": "data (enda sanningskälla)",
"bytes": 40573,
"sha256": "2a920c49767e812574a2d4d83cd6d04c2231d3005ff09ebb54c364a5dfe47527"
},
{
"sokvag": "10-data/lvx-id-register.json",
"roll": "data (enda sanningskälla)",
"bytes": 593,
"sha256": "939ce9b0305724e7cdf3fb00a44610f6e122489ccdec652a7188cc747f6ff54e"
},
{
"sokvag": "10-data/lvx-klassposter-master-v3.json",
"roll": "data (enda sanningskälla)",
"bytes": 152207,
"sha256": "b568ee83d4f6b941dcf07b0f5631cd38431cd727436687bb0f3e52cf9d5078f8"
},
{
"sokvag": "10-data/lvx-objekt-schema.json",
"roll": "data (enda sanningskälla)",
"bytes": 8023,
"sha256": "8abb092517947b5313c3a7fd23c835c9126b9054d688f50c1b5fa968a17f05f5"
},
{
"sokvag": "10-data/lvx-produktmodeller-auto.json",
"roll": "data (enda sanningskälla)",
"bytes": 1592,
"sha256": "59e1fdc6af7ec282664fadd7c9943bdd1742e6d97a14a5fe24fdb7fa8b4ccfd7"
},
{
"sokvag": "10-data/lvx-produktmodeller-vag2.json",
"roll": "data (enda sanningskälla)",
"bytes": 24976,
"sha256": "25d8618e172971982fab827ae45982c9e161f59050deb3d44e1c1813e2f7f2cb"
},
{
"sokvag": "10-data/lvx-produktmodeller-vag3.json",
"roll": "data (enda sanningskälla)",
"bytes": 15428,
"sha256": "490242c8f798c09932451226d1fcadb255bcc808c258d81e583346417d911c50"
},
{
"sokvag": "10-data/lvx-taxonomi-tier1.json",
"roll": "data (enda sanningskälla)",
"bytes": 6316,
"sha256": "7ef057a11d6d0cc5ba6da6fac40790f85f9a713716c7eb20b8c27993cd616464"
},
{
"sokvag": "10-data/lvx-taxonomi-tier2.json",
"roll": "data (enda sanningskälla)",
"bytes": 4419,
"sha256": "4701f03f5dbed92d48dae5d40a9d5ad5ed427340b9f9bdc6b04bcb7776787515"
},
{
"sokvag": "10-data/lvx-zoomer-bounties-vag1.json",
"roll": "data (enda sanningskälla)",
"bytes": 5651,
"sha256": "fc314b0641e92d3ee663a08a423826b04e53a9361d474f35520acafb7466081d"
},
{
"sokvag": "20-automation/ingest.py",
"roll": "automation",
"bytes": 12071,
"sha256": "93c41f1eebe2564454e1743d7791eb8ed3f02916bede017f648d556bfb729ea5"
},
{
"sokvag": "20-automation/intag/kallpaket-vag7.json",
"roll": "automation",
"bytes": 4242,
"sha256": "0df98d5bb0843f563ea39a8716e5ed9e450a2cee06689734afab39199df840db"
},
{
"sokvag": "20-automation/intag/kandidatposter-vag7.json",
"roll": "automation",
"bytes": 7126,
"sha256": "c1816b9077232ecf1752ffd659d0385eae6f0edac3f5bf90b4b91e1ab2f701d3"
},
{
"sokvag": "20-automation/lvx-extraktionsprompt.md",
"roll": "automation",
"bytes": 2171,
"sha256": "ae9bceb372fa64745bee75979ff7907643e50133bfae120f2f50321266255a30"
},
{
"sokvag": "20-automation/lvx-ingest-spec.md",
"roll": "automation",
"bytes": 3879,
"sha256": "073a67d8921fe005f89dd4a4a5647a0c4abfc920022e65612ffa2e63d4b20ce4"
},
{
"sokvag": "30-produkt/lvx-api-demosvar.json",
"roll": "produkt",
"bytes": 4936,
"sha256": "f9a7eb102084b1d0add635260d1fe623b7467c3b57fc50be1a762a56773348be"
},
{
"sokvag": "30-produkt/lvx-pilot-api-spec.md",
"roll": "produkt",
"bytes": 3766,
"sha256": "596325020e8f87009639491036c7d65f23a2eb321b2524f1d5f1e294927794d7"
},
{
"sokvag": "30-produkt/lvx-zoomer-bountyprogram.md",
"roll": "produkt",
"bytes": 3887,
"sha256": "05f70da6891b34901ede9b4f1249375b051f6927035cb46830005bdeb70a6c04"
},
{
"sokvag": "40-strategi/iol-grundspecifikation.md",
"roll": "strategi & rapporter",
"bytes": 9721,
"sha256": "4c7fbed1d30407467d17b2ff97ea14692028b5632e9d6a522381c82c1836f01a"
},
{
"sokvag": "40-strategi/landvex-genome-strategi.md",
"roll": "strategi & rapporter",
"bytes": 8777,
"sha256": "99b6a5030f70a6abf2c3f4a65d2869e4dfde598d135c83ecb54f01204726b28a"
},
{
"sokvag": "40-strategi/landvex-pilotpitch.pptx",
"roll": "strategi & rapporter",
"bytes": 170939,
"sha256": "bbcbec18e3701a73e27e987b873abac886fc70ef96bc93fc315cbe9518dd113a"
},
{
"sokvag": "40-strategi/rapporter/lvx-leveransrapport-tier1.md",
"roll": "strategi & rapporter",
"bytes": 3613,
"sha256": "a5b3d57dc00f3d56df91c27664f186a237eaa783344988db31504e7151983140"
},
{
"sokvag": "40-strategi/rapporter/lvx-leveransrapport-vag2.md",
"roll": "strategi & rapporter",
"bytes": 3186,
"sha256": "f63016785654b7bc0d9face400fc8ac4401aba52c45aea03cc25f61b1fb4c1d4"
},
{
"sokvag": "40-strategi/rapporter/lvx-leveransrapport-vag3.md",
"roll": "strategi & rapporter",
"bytes": 2804,
"sha256": "3276174b8f65b28a88a6bfd911d5773e1c4f5e1fa15ee002269c6e7b3f765058"
},
{
"sokvag": "40-strategi/rapporter/lvx-leveransrapport-vag4.md",
"roll": "strategi & rapporter",
"bytes": 2698,
"sha256": "19a62e5d4c7e9d8422ec46baae85576dec2b4d769a1cc227669ecc93b6ccefc4"
},
{
"sokvag": "40-strategi/rapporter/lvx-leveransrapport-vag5.md",
"roll": "strategi & rapporter",
"bytes": 2745,
"sha256": "8a019cc71ede7683026f60401c46f96bdde0117c025808225871c7c657a56652"
},
{
"sokvag": "40-strategi/rapporter/lvx-leveransrapport-vag7.md",
"roll": "strategi & rapporter",
"bytes": 2887,
"sha256": "a8e9f287988a44eca60c13c05015903e4918d04e4db5b36e6971977533ff9644"
}
]
}
+37
View File
@@ -0,0 +1,37 @@
# LANDVEX PRODUKTIONSPAKET v1
## Översikt · 2026-07-04
Allt material och all automation från utvecklingstråden, paketerat för implementering av agentarmén. Arbetsordern ligger i `00-styrning/AGENTARME.md`. Integriteten ligger i `00-styrning/MANIFEST.json` (SHA-256 per fil) och grinden i `00-styrning/verifiera.py`.
## Innehållskarta
| Mapp | Innehåll | Kanonisk status |
|---|---|---|
| `00-styrning/` | AGENTARMÉ (arbetsorder, 58 agenter), MANIFEST (checksummor), verifiera.py (grind), denna översikt | Styr allt |
| `10-data/` | Biblioteket: master v3 (37 klasser, 35 källbelagda), graf v3 (176 kanter), schema, taxonomier, 27 modeller, ID-register, 16 bounties + 17 auto-förslag, exempelpost | **Enda sanningskälla.** Ersätter alla äldre versioner (v1/v2 och delfiler arkiveras utanför paketet) |
| `20-automation/` | `ingest.py` (pipelinens kärna), extraktionsprompten (LLM-kontraktet), `intag/` med våg 7-batchen (källpaket + kandidatposter) för replay-test | Körbar, demonstrerad: 4 nivålyft · 1 ID mintat · 0 fel |
| `30-produkt/` | Object API-spec v0 (6 endpoints, svarskontrakt, SaaS-paketering), tre golden demo-svar, Zoomer-bountyprogrammet | Implementeras exakt av skvadron E/G |
| `40-strategi/` | Genome-strategin, IOL-grundspecifikationen, pilotpitch-deck (pptx), samtliga leveransrapporter | Kontext & säljmaterial |
## Snabbstart (varje agent, dag 1)
1. Läs `AGENTARME.md` — särskilt de tio hårda reglerna.
2. Kör `python3 00-styrning/verifiera.py` från paketroten — ska sluta **GRÖNT**.
3. Hitta din skvadron, dina inputfiler och din Definition of Done.
4. Fakta finns bara i `10-data/`. Finns det inte där, finns det inte.
## Nyckeltal vid paketering
37 objektklasser (35 källbelagda, 2 ärligt obekräftade) · 27 produktmodeller (alla källbelagda) · 176 grafkanter (80 LVX-upplösta, 82 standardkanter) · 0 valideringsfel · ID-register: nästa P = 0000028 · Verifieringstrappa: obekräftad → källbelagd → fältverifierad → tillverkarbekräftad.
## Ärlighetsnot om "deployat"
Paketet är körbart och maskinverifierat i byggmiljön (verifiera.py grönt vid paketering). Driftsättning i er infrastruktur är per definition **Fas 0 i arbetsordern** — miljöer, dataladdning och CI-grindar sätts av skvadron B/C/J innan något exponeras. Att påstå något annat vore att bryta paketets egen regel 2. Inga mockar, inga låtsade tjänster: det som inte är driftsatt hos er kallas inte driftsatt.
+111
View File
@@ -0,0 +1,111 @@
#!/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.")