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
+233
View File
@@ -0,0 +1,233 @@
#!/usr/bin/env python3
# Landvex Ingest v0 — deterministisk pipeline
# kandidatposter (extraktorns output) -> resolve -> merge -> validera -> master/graf/bounty-förslag
import json, re, difflib
from pathlib import Path
OUT = Path("/mnt/user-data/outputs")
IDAG = "2026-07-04"
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_RANK = {"obekraftad":0,"kallbelagd":1,"faltverifierad":2,"tillverkarbekraftad":3}
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(p):
with open(OUT/p, encoding="utf-8") as f: return json.load(f)
def save(p,obj):
with open(OUT/p,"w",encoding="utf-8") as f: json.dump(obj,f,ensure_ascii=False,indent=2)
def norm_bet(b):
b = b.upper().split(":")[0]
b = re.sub(r"\((KARTLÄGGS|KARTLAGGS)\)","",b)
return re.sub(r"\s+"," ",b).strip()
TOM = ("", None)
def ar_tom(v): return v in TOM or v == [] or v == {}
class Ingest:
def __init__(self):
self.poster = {p["lvx_id"]: p for p in load("lvx-klassposter-master-v2.json")["poster"]}
self.modeller = {}
for fn in ["lvx-produktmodeller-vag2.json","lvx-produktmodeller-vag3.json"]:
for m in load(fn)["poster"]: self.modeller[m["lvx_id"]] = m
self.reg = load("lvx-id-register.json")
self.nya_modeller, self.logg, self.konflikter = [], [], []
self.stat = {"uppdaterade":0,"nya":0,"nivalyft":0,"kallor":0,"standarder":0}
# ---- resolve ----
def hitta(self, kand):
rid = kand.get("lvx_id")
if rid:
if rid in self.poster: return self.poster[rid]
if rid in self.modeller: return self.modeller[rid]
raise KeyError(f"{kand['kandidat_id']}: okänt lvx_id {rid}")
# entitetsupplösning utan ID: slug, annars fuzzy på namn
post = kand.get("post",{})
alla = {**self.poster, **self.modeller}
for r in alla.values():
if r.get("slug") and r["slug"] == post.get("slug"): return r
namn = post.get("namn",{}).get("sv","")
kandnamn = {r["namn"]["sv"]: r for r in alla.values()}
tr = difflib.get_close_matches(namn, kandnamn.keys(), n=1, cutoff=0.88)
return kandnamn[tr[0]] if tr else None
# ---- merge-primitiver ----
def merge_kallor(self, rec, nya):
kl = rec.setdefault("proveniens",{}).setdefault("kallor",[])
urls = {k.get("url") for k in kl}
for k in nya:
if k.get("url") not in urls:
kl.append(k); urls.add(k.get("url"))
self.stat["kallor"] += 1
self.logg.append(f"{rec['lvx_id']}: +källa {k.get('url','?')[:60]}")
def merge_standarder(self, rec, nya):
st = rec.setdefault("standarder",[])
idx = {norm_bet(s["beteckning"]): i for i,s in enumerate(st) if s.get("beteckning")}
for s in nya:
n = norm_bet(s["beteckning"])
if n in idx:
st[idx[n]] = s
self.logg.append(f"{rec['lvx_id']}: standard uppdaterad {s['beteckning']}")
else:
st.append(s)
self.logg.append(f"{rec['lvx_id']}: +standard {s['beteckning']}")
self.stat["standarder"] += 1
def fyll(self, base, patch, rid, stig=""):
for k,v in patch.items():
here = f"{stig}.{k}" if stig else k
if k not in base or ar_tom(base.get(k)):
base[k] = v
self.logg.append(f"{rid}: fyllde {here}")
elif isinstance(base[k], dict) and isinstance(v, dict):
self.fyll(base[k], v, rid, here)
elif base[k] != v:
self.konflikter.append(f"{rid}: {here} — befintligt värde behålls (granskningskö i produktion)")
# ---- åtgärder ----
def uppdatera(self, kand):
rec = self.hitta(kand)
patch = dict(kand.get("patch",{}))
if "standarder" in patch: self.merge_standarder(rec, patch.pop("standarder"))
prov = patch.pop("proveniens", {})
if prov.get("kallor"): self.merge_kallor(rec, prov["kallor"])
if patch: self.fyll(rec, patch, rec["lvx_id"])
fs = kand.get("verifieringsniva_forslag")
if fs:
nu = rec["proveniens"].get("verifieringsniva","obekraftad")
if VERIF_RANK[fs] > VERIF_RANK[nu]:
if VERIF_RANK[fs] >= 1 and not rec["proveniens"].get("kallor"):
self.konflikter.append(f"{rec['lvx_id']}: nivålyft nekad — inga källor")
else:
rec["proveniens"]["verifieringsniva"] = fs
rec["proveniens"]["konfidens"] = max(rec["proveniens"].get("konfidens",0), 0.72)
self.stat["nivalyft"] += 1
self.logg.append(f"{rec['lvx_id']}: {nu}{fs}")
rec["proveniens"]["uppdaterad"] = IDAG
self.stat["uppdaterade"] += 1
def ny(self, kand):
if self.hitta(kand):
self.konflikter.append(f"{kand['kandidat_id']}: dublett — matchar befintlig post, konverteras ej")
return
post = kand["post"]
typ = kand.get("serie",{}).get("typ","modell" if post.get("posttyp")=="produktmodell" else "klass")
if typ == "modell":
post["lvx_id"] = f"LVX-P-{self.reg['P_nasta']:07d}"
self.reg["P_nasta"] += 1
if not post.get("parent"): raise ValueError(f"{kand['kandidat_id']}: modell utan parent")
self.modeller[post["lvx_id"]] = post
self.nya_modeller.append(post)
else:
dom, tier = post["doman"], str(kand["serie"]["tier"])
nn = self.reg["klass_nasta"].setdefault(dom,{}).setdefault(tier,1)
post["lvx_id"] = f"LVX-{dom}-0{tier}{nn:02d}"
self.reg["klass_nasta"][dom][tier] = nn + 1
self.poster[post["lvx_id"]] = post
post.setdefault("proveniens",{}).setdefault("skapad",IDAG)
post["proveniens"]["uppdaterad"] = IDAG
self.stat["nya"] += 1
self.logg.append(f"NY: {post['lvx_id']}{post['namn']['sv']}")
def kor(self, fil):
for kand in load(fil)["kandidater"]:
(self.uppdatera if kand["atgard"]=="uppdatera" else self.ny)(kand)
# ---- validering (samma regler som konsolidering v2) ----
def validera(poster, modeller):
fel = []
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_RANK: 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 {rel.get('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")
return fel
# ---- graf ----
def bygg_graf(poster, modeller):
kanter, olosta = [], {}
def multi(m): return " / " in m and all(ID_RE.match(x.strip()) for x in m.split(" / "))
def kant(s,pred,obj,niva,beskr=""):
typ = "lvx" if ID_RE.match(obj or "") else ("standard" if pred=="foljer_standard" else "text")
if typ=="text": olosta[obj] = olosta.get(obj,0)+1
kanter.append({"subjekt":s,"predikat":pred,"objekt":obj,"objekt_typ":typ,"niva":niva,
**({"beskrivning":beskr} if beskr else {})})
def emit(s,pred,m,niva,beskr=""):
if multi(m):
for x in m.split(" / "): kant(s,pred,x.strip(),niva,beskr)
else: kant(s,pred,m,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")
for e in m.get("historik",{}).get("ersatter",[]): kant(m["lvx_id"],"ersatter",e,"modell")
for e in m.get("historik",{}).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",""))
return kanter, olosta
# ---- flyhjulet: bounty-förslag ur nya/uppgraderade poster ----
def bounty_forslag(poster):
try: tackta = {b["mal_lvx"] for b in load("lvx-zoomer-bounties-vag1.json")["bounties"]}
except FileNotFoundError: tackta = set()
ut = []
for p in poster.values():
if VERIF_RANK[p["proveniens"]["verifieringsniva"]] < 1: continue
falt = p.get("ai",{}).get("ocr",{}).get("falt",[])
if falt and p["lvx_id"] not in tackta:
ut.append({"forslag_id": f"BNTY-K-{len(ut)+1:03d}", "mal_lvx": p["lvx_id"],
"titel": f"OCR-fält: {p['namn']['sv']}", "ocr_falt": falt,
"grund": "auto-genererad ur postens OCR-fält (ingest-pipelinen)"})
return ut
if __name__ == "__main__":
ing = Ingest()
ing.kor("intag/kandidatposter-vag7.json")
fel = validera(ing.poster, ing.modeller)
kanter, olosta = bygg_graf(ing.poster, ing.modeller)
vf = {}
for p in ing.poster.values():
v = p["proveniens"]["verifieringsniva"]; vf[v] = vf.get(v,0)+1
save("lvx-klassposter-master-v3.json", {
"beskrivning":"Landvex IOL — master v3 (ingest-pipelinen, våg 7). Ersätter v2.",
"genererad": IDAG, "antal_poster": len(ing.poster), "verifieringsfordelning": vf,
"poster":[ing.poster[k] for k in sorted(ing.poster)]})
if ing.nya_modeller:
save("lvx-produktmodeller-auto.json", {
"beskrivning":"Modellposter mintade av ingest-pipelinen.", "genererad": IDAG, "poster": ing.nya_modeller})
lvx_k = sum(1 for k in kanter if k["objekt_typ"]=="lvx"); std_k = sum(1 for k in kanter if k["objekt_typ"]=="standard")
save("lvx-graf-kanter-v3.json", {
"beskrivning":"Landvex IOL — grafexport v3 (efter ingest våg 7).", "genererad": IDAG,
"noder":{"objektklasser":len(ing.poster),"produktmodeller":len(ing.modeller)},
"statistik":{"kanter_totalt":len(kanter),"upplosta_lvx_kanter":lvx_k,"standardkanter":std_k,"textnoder_olosta":len(olosta)},
"olosta_platshallare":[{"text":k,"forekomster":v} for k,v in sorted(olosta.items())],
"kanter":kanter})
bf = bounty_forslag(ing.poster)
save("lvx-bounty-kandidater-vag7.json", {"beskrivning":"Auto-genererade bounty-förslag (granskas innan publicering).","genererad":IDAG,"forslag":bf})
ing.reg["uppdaterad"] = IDAG
save("lvx-id-register.json", ing.reg)
print(f"Uppdaterade poster: {ing.stat['uppdaterade']} | Nya: {ing.stat['nya']} (register P_nasta={ing.reg['P_nasta']})")
print(f"Nivålyft: {ing.stat['nivalyft']} | +källor: {ing.stat['kallor']} | standarder: {ing.stat['standarder']}")
print(f"Verifieringsfördelning: {vf}")
print(f"Modeller totalt: {len(ing.modeller)} | Kanter: {len(kanter)} ({lvx_k} lvx / {std_k} standard / {len(olosta)} textnoder)")
print(f"Bounty-förslag: {len(bf)} | Konflikter: {len(ing.konflikter)} | Valideringsfel: {len(fel)}")
for r in ing.logg: print(" ·", r)
for k in ing.konflikter: print(" !", k)
for f_ in fel: print(" !!", f_)
@@ -0,0 +1,55 @@
{
"beskrivning": "Landvex Ingest — källpaket från skördepass 2026-07-04 (våg 7). Skördare i denna miljö: Claude web_search. Utdragen är REFERAT — aldrig kopierad källtext.",
"paket": [
{
"paket_id": "KP-20260704-001",
"amne": "AASHTO LTS — strukturstandard för skylt-, belysnings- och signalstöd",
"metod": "web_search",
"hamtad": "2026-07-04",
"kallor": [
{ "titel": "AASHTO Journal — 2025 Interim Revisions till LTS-6", "url": "https://aashtojournal.transportation.org/new-interim-revisions-for-structural-support-standards/" },
{ "titel": "Accuris — AASHTO LTS-6 med interimsrevisioner 2015/2019/2020/2022/2025", "url": "https://store.accuristech.com/standards/aashto-lts-6?product_id=1859294" },
{ "titel": "Iowa DOT Bridge Design Manual 10.2 — LTS-6 (ASD) vs LRFD-LTS-1 (2015)", "url": "https://iowadot.gov/media/4676/download?inline=" }
],
"referat": "AASHTO:s LTS-serie ar strukturstandarden for stod till vagmarken, belysning och trafiksignaler. LTS-6 (6:e utgavan, 2013, ASD) galler med interimsrevisioner t.o.m. 2025; parallellt finns LRFD-LTS-1 (2015). Scope kategoriserar stodkonstruktioner som skyltstod, belysningsstod, signalstod och kombinationer — inklusive portaler (overhead bridge/cantilever).",
"mal_lvx": ["LVX-TRP-0202", "LVX-TRP-0205"]
},
{
"paket_id": "KP-20260704-002",
"amne": "EN 1917 — prefabricerade betongbrunnar",
"metod": "web_search",
"hamtad": "2026-07-04",
"kallor": [
{ "titel": "ANSI Webstore — BS EN 1917:2002 scope", "url": "https://webstore.ansi.org/standards/bsi/bsen19172002" },
{ "titel": "iTeh — SIST EN 1917:2003 (innehall, provmetoder)", "url": "https://standards.iteh.ai/catalog/standards/sist/49425637-2c7e-4408-8568-df3d9c49f270/sist-en-1917-2003" },
{ "titel": "GlobalSpec — BS 5911-3 (UK-komplement) och BS 5911-6 (road gullies)", "url": "https://standards.globalspec.com/std/1146029/BS%20EN%201917" }
],
"referat": "EN 1917:2002 staller prestandakrav och provmetoder for prefabricerade betongenheter till nedstigningsbrunnar och inspektionsbrunnar (oarmerade, stalfiber, armerade), cirkulara/rektangulara/elliptiska, upp till DN/LN 1250; inspektionsbrunnar for vattengang max 2 m djup. Omfattar fogar och tathetsprov. UK kompletterar med BS 5911-3; BS 5911-6 tacker vaggallerbrunnar (road gullies).",
"mal_lvx": ["LVX-VAT-0202", "LVX-VAT-0203"]
},
{
"paket_id": "KP-20260704-003",
"amne": "ASTM C913 — prefab betongkonstruktioner inkl. catch basins",
"metod": "web_search",
"hamtad": "2026-07-04",
"kallor": [
{ "titel": "ASTM — C913-21 scope (Note 1: catch basins, drop inlets)", "url": "https://store.astm.org/c0913-21.html" },
{ "titel": "ANSI Webstore — ASTM C913-18", "url": "https://webstore.ansi.org/standards/astm/astmc91318" }
],
"referat": "ASTM C913 tacker konstruktions- och tillverkningskrav for monolitiska/sektionerade prefab-betongkonstruktioner for vatten/avlopp. Scope-notering raknar upp bl.a. catch basins och drop inlets; det som ligger under C478 (brunnsringar m.m.) ar undantaget. Aktuell utgava C913-21.",
"mal_lvx": ["LVX-VAT-0203"]
},
{
"paket_id": "KP-20260704-004",
"amne": "Ateranvandning: EN 12767-scope tacker fundament; EJ SELFLEVEL som modell",
"metod": "intern korsreferens (vag 5/3-kallor)",
"hamtad": "2026-07-04",
"kallor": [
{ "titel": "iTeh — EN 12767:2019 scope (stolpar, konstruktionsdelar, fundament)", "url": "https://standards.iteh.ai/catalog/standards/cen/ea3ba437-12fd-4b81-a98c-009b9762e0d8/en-12767-2019" },
{ "titel": "EJ — SELFLEVEL-fallstudie (sjalvnivellerande ramsystem)", "url": "https://www.ejco.com/am/en/resource-center/case-studies/manhole-cover-case-study" }
],
"referat": "Redan skordade kallor racker for tva atgarder: (1) EN 12767-scopet omfattar aven fundament — lyfter LVX-TRP-0203 till kallbelagd. (2) EJ SELFLEVEL ar en namngiven produktfamilj (sjalvnivellerande betackningsram) — ny modellpost under LVX-VAT-0201.",
"mal_lvx": ["LVX-TRP-0203", "LVX-VAT-0201"]
}
]
}
@@ -0,0 +1,115 @@
{
"beskrivning": "Landvex Ingest — kandidatposter (extraktorns output) fran kallpaket-vag7. Patch-format: 'uppdatera' merger mot befintlig post enligt policy; 'ny' mintar ID ur registret.",
"extraktor": "Claude (manuell korning av extraktionsprompten)",
"genererad": "2026-07-04",
"kandidater": [
{
"kandidat_id": "K-20260704-01",
"atgard": "uppdatera",
"lvx_id": "LVX-TRP-0202",
"paket_ref": ["KP-20260704-001"],
"patch": {
"standarder": [
{ "organ": "AASHTO", "beteckning": "LTS-6 (2013, interim t.o.m. 2025)", "titel": "Standard Specifications for Structural Supports for Highway Signs, Luminaires, and Traffic Signals", "roll": "Strukturkrav (ASD) för signalstolpar och mastarmar", "marknad": "US" },
{ "organ": "AASHTO", "beteckning": "LRFD-LTS-1 (2015)", "titel": "LRFD Specifications for Structural Supports", "roll": "Parallell LRFD-dimensionering", "marknad": "US" }
],
"proveniens": { "kallor": [
{ "titel": "AASHTO Journal — 2025 interimsrevisioner till LTS-6", "url": "https://aashtojournal.transportation.org/new-interim-revisions-for-structural-support-standards/", "hamtad": "2026-07-04" },
{ "titel": "Iowa DOT BDM 10.2 — LTS-6/LRFD-LTS-1 i praktiken", "url": "https://iowadot.gov/media/4676/download?inline=", "hamtad": "2026-07-04" }
] }
},
"verifieringsniva_forslag": "kallbelagd",
"motivering": "Strukturstandarden identifierad och belagd; scope täcker signalstöd."
},
{
"kandidat_id": "K-20260704-02",
"atgard": "uppdatera",
"lvx_id": "LVX-TRP-0205",
"paket_ref": ["KP-20260704-001"],
"patch": {
"standarder": [
{ "organ": "AASHTO", "beteckning": "LTS-6 (2013, interim t.o.m. 2025)", "titel": "Standard Specifications for Structural Supports", "roll": "Scope kategoriserar skyltstöd inkl. portaler (overhead bridge/cantilever)", "marknad": "US" },
{ "organ": "AASHTO", "beteckning": "LRFD-LTS-1 (2015)", "titel": "LRFD Specifications for Structural Supports", "roll": "LRFD-spår; Iowa DOT tillämpar båda för skylttruss", "marknad": "US" }
],
"proveniens": { "kallor": [
{ "titel": "Accuris — AASHTO LTS-6 (utgåvor/interim)", "url": "https://store.accuristech.com/standards/aashto-lts-6?product_id=1859294", "hamtad": "2026-07-04" },
{ "titel": "Iowa DOT BDM 10.2 — portal-/utliggartruss enligt LTS", "url": "https://iowadot.gov/media/4676/download?inline=", "hamtad": "2026-07-04" }
] }
},
"verifieringsniva_forslag": "kallbelagd",
"motivering": "Portaler ingår i LTS-scope (sign support structures)."
},
{
"kandidat_id": "K-20260704-03",
"atgard": "uppdatera",
"lvx_id": "LVX-TRP-0203",
"paket_ref": ["KP-20260704-004"],
"patch": {
"standarder": [
{ "organ": "CEN", "beteckning": "EN 12767:2019 (+A1:2024)", "titel": "Passive safety of support structures", "roll": "Scope omfattar fundament i eftergivliga system", "marknad": "EU/UK" }
],
"proveniens": { "kallor": [
{ "titel": "iTeh — EN 12767:2019 scope (stolpar, konstruktionsdelar, fundament)", "url": "https://standards.iteh.ai/catalog/standards/cen/ea3ba437-12fd-4b81-a98c-009b9762e0d8/en-12767-2019", "hamtad": "2026-07-04" }
] }
},
"verifieringsniva_forslag": "kallbelagd",
"motivering": "Befintlig våg 5-källa täcker fundament — korsreferens, ingen ny skörd krävdes."
},
{
"kandidat_id": "K-20260704-04",
"atgard": "uppdatera",
"lvx_id": "LVX-VAT-0202",
"paket_ref": ["KP-20260704-002"],
"patch": {
"standarder": [
{ "organ": "CEN", "beteckning": "EN 1917:2002", "titel": "Concrete manholes and inspection chambers", "roll": "EU-krav för prefab brunnsenheter (DN/LN ≤1250; inspektionsbrunn ≤2 m), fogar och täthet", "marknad": "EU/UK" },
{ "organ": "BSI", "beteckning": "BS 5911-3", "titel": "Concrete manholes, inspection chambers and soakaways", "roll": "UK-komplement till EN 1917", "marknad": "UK" }
],
"proveniens": { "kallor": [
{ "titel": "ANSI Webstore — BS EN 1917:2002 scope", "url": "https://webstore.ansi.org/standards/bsi/bsen19172002", "hamtad": "2026-07-04" },
{ "titel": "GlobalSpec — BS 5911-3 som komplement till EN 1917", "url": "https://standards.globalspec.com/std/1146029/BS%20EN%201917", "hamtad": "2026-07-04" }
] }
},
"verifieringsniva_forslag": "kallbelagd",
"motivering": "EU-standarden nu belagd — ersätter 'kartläggs'-posten."
},
{
"kandidat_id": "K-20260704-05",
"atgard": "uppdatera",
"lvx_id": "LVX-VAT-0203",
"paket_ref": ["KP-20260704-002", "KP-20260704-003"],
"patch": {
"standarder": [
{ "organ": "ASTM", "beteckning": "ASTM C913-21", "titel": "Precast Concrete Water and Wastewater Structures", "roll": "Scope-notering omfattar catch basins och drop inlets (US)", "marknad": "US" },
{ "organ": "BSI", "beteckning": "BS 5911-6", "titel": "Road gullies and gully cover slabs", "roll": "UK-krav för vägbrunnar", "marknad": "UK" }
],
"proveniens": { "kallor": [
{ "titel": "ASTM — C913-21 (Note 1: catch basins, drop inlets)", "url": "https://store.astm.org/c0913-21.html", "hamtad": "2026-07-04" },
{ "titel": "GlobalSpec — BS 5911-6 (road gullies)", "url": "https://standards.globalspec.com/std/1146029/BS%20EN%201917", "hamtad": "2026-07-04" }
] }
},
"verifieringsniva_forslag": "kallbelagd",
"motivering": "US- och UK-spår belagda för dagvattenbrunnar."
},
{
"kandidat_id": "K-20260704-06",
"atgard": "ny",
"serie": { "typ": "modell" },
"paket_ref": ["KP-20260704-004"],
"post": {
"posttyp": "produktmodell",
"slug": "ej-selflevel",
"namn": { "sv": "EJ SELFLEVEL betäckningsram", "en": "EJ SELFLEVEL access frame" },
"doman": "VAT",
"parent": "LVX-VAT-0201",
"identitet": { "tillverkare": "EJ", "produktfamilj": "SELFLEVEL", "modell": "SELFLEVEL", "status": "aktiv" },
"teknik": { "material": ["segjärn"], "ovrigt": { "funktion": "Självnivellerande ram som vilar i beläggningen och följer vägytans rörelser — minskar sättningsgupp och omjusteringar" } },
"ai": { "kannetecken": ["Flytande ramkrans utan fast gjutfläns", "EJ-märkning i gods"], "forvaxlingsrisker": ["Konventionell fast ram"], "ocr": { "typskylt_position": "Gjuten text i ram/lock", "falt": ["EJ", "SELFLEVEL"] } },
"proveniens": { "skapad": "2026-07-04", "uppdaterad": "2026-07-04", "kallor": [
{ "titel": "EJ — SELFLEVEL-fallstudie", "url": "https://www.ejco.com/am/en/resource-center/case-studies/manhole-cover-case-study", "hamtad": "2026-07-04" }
], "konfidens": 0.72, "verifieringsniva": "kallbelagd" }
},
"motivering": "Namngiven produktfamilj i redan skördad källa — mintas som ny modell."
}
]
}
@@ -0,0 +1,40 @@
# Landvex Extraktionsprompt v1
## LLM-steget i ingest-pipelinen — kontraktet
Denna prompt körs mot varje källpaket i produktionen. I nuvarande miljö spelar Claude extraktorns roll manuellt; kontraktet är detsamma.
## Systeminstruktion (mall)
Du är Landvex extraktor. Din uppgift: omvandla ett källpaket (referat + käll-URL:er) till kandidatposter i patch-format enligt schemat nedan. Regler:
1. **Referera — kopiera aldrig.** Alla formuleringar ska vara dina egna. Standarder anges med organ, beteckning och roll — aldrig innehåll ur standardtexten.
2. **Källa på varje påstående.** Varje faktabärande fält i patchen ska kunna spåras till en URL i paketet. Kan det inte det: utelämna fältet.
3. **Hitta aldrig på.** Saknas uppgiften i källpaketet skriver du inte fältet. Problem utan källa markeras `"kalla": "PLATSHÅLLARE"` och publiceras aldrig.
4. **Patch, inte omskrivning.** Vid `atgard: "uppdatera"` skickar du endast nya/ändrade fält. Pipelinen skriver aldrig över befintliga ifyllda värden — konflikter går till granskningskö.
5. **Föreslå nivå, besluta inte.** `verifieringsniva_forslag` får högst vara `kallbelagd`. `faltverifierad` och `tillverkarbekraftad` sätts av andra flöden (foto respektive portal).
6. **Ny post kräver serie.** `atgard: "ny"` ska ange `serie` (klass: domän + tier; modell: parent-klass). ID sätter pipelinen — aldrig du.
7. **ASCII-nycklar, svenska värden.** Följ lvx-objekt-schema.json.
## Utformat (per kandidat)
```json
{
"kandidat_id": "K-YYYYMMDD-NN",
"atgard": "uppdatera | ny",
"lvx_id": "(vid uppdatera)",
"serie": { "typ": "klass|modell", "tier": 2 },
"paket_ref": ["KP-..."],
"patch": { "standarder": [], "teknik": {}, "ai": {}, "proveniens": { "kallor": [] } },
"post": { "(full post vid ny, utan lvx_id)" : "" },
"verifieringsniva_forslag": "kallbelagd",
"motivering": "en mening"
}
```
## Kvalitetsspärrar (pipelinen verkställer)
* Nivålyft till källbelagd nekas om `kallor` är tom
* Dublettkontroll: slug-träff eller namnlikhet ≥ 0,88 stoppar `ny`
* Okänt predikat, ID-format eller saknad parent ⇒ valideringsfel, batchen flaggas
@@ -0,0 +1,71 @@
# Landvex Ingest — automationsspec v0
## Datafabriken: från källa till validerad graf utan handpåläggning
## Kedjan
**Källor → Skördare → Källpaket → Extraktor (LLM) → Kandidatposter → Ingest (deterministisk) → Master + Graf + Bounty-förslag → Rapport**
Två halvor med olika natur:
* **Skörd + extraktion** är probabilistisk (sök, läs, referera). Får föreslå — aldrig besluta.
* **Ingest** är deterministisk kod (`ingest.py`). Den beslutar: upplöser entiteter, verkställer merge-policyn, mintar ID, validerar, bygger grafen. Samma indata ger alltid samma bibliotek.
## Stegen
**1. Skördare.** Hämtar källor per kö (se Drift). Output: *källpaket* — referat med URL-lista, aldrig kopierad text. Rätten till originalet stannar hos källan; biblioteket bär referat + referens.
**2. Extraktor.** LLM med `lvx-extraktionsprompt.md` som kontrakt. Output: *kandidatposter* i patch-format. Får högst föreslå nivån källbelagd.
**3. Ingest (`ingest.py`).**
* **Entitetsupplösning:** lvx_id → slug → namnlikhet ≥ 0,88 (difflib). Dublett stoppar `ny`.
* **Merge-policy:** källor unionas på URL; standarder dedupe:as på normaliserad beteckning (så att "EN 1917 (kartläggs)" ersätts av "EN 1917:2002"); tomma fält fylls; **ifyllda fält skrivs aldrig över** — konflikter loggas till granskningskö.
* **Verifieringstrappan:** lyft sker endast uppåt, och källbelagd kräver icke-tom källista. Fältverifierad/tillverkarbekräftad kan bara sättas av foto- respektive portalflödet.
* **ID-mintning:** `lvx-id-register.json` håller nästa nummer per serie; register skrivs tillbaka atomärt; ID återanvänds aldrig.
* **Validering:** samma regler som konsolideringen (ID-format, domän, namn sv/en, predikatenum, problem-källa, parent, källkrav). Fel ⇒ batchen flaggas.
* **Grafexport:** kantlista regenereras ur posterna varje körning — grafen är alltid härledd, aldrig handredigerad.
**4. Bounty-generatorn.** Varje körning skannar posterna: källbelagd + OCR-fält + ingen aktiv bounty ⇒ bounty-förslag. Flyhjulet matas automatiskt av ny data.
**5. Rapport.** Körningslogg: uppdaterade/nya, nivålyft, källor, konflikter, valideringsfel, grafstatistik.
## Drift: tre köer in i skördaren
| Kö | Trigger | Exempel |
|---|---|---|
| **Schemalagd** | Domän-/klasscykel | "Alla TRP-standarder omkontrolleras kvartalsvis" |
| **Kunddriven** | `POST /v0/feedback` + svaga identify-svar | Okänd modell i Leeds ⇒ riktad skörd + bounty |
| **Fält** | Zoomer-foton godkända | OCR-data ⇒ individposter + nivålyft till fältverifierad |
Kunddrivna kön är SaaS-motorn: betald efterfrågan bestämmer var datafabriken gräver härnäst, och kostnaden bärs av intäkten den skapar.
## Kostnadsmåttet
**Kostnad per verifierat faktum** = (skörd + extraktion + granskning) / antal nivålyft & nya källbelagda fält. Våg 7-demon: 3 sökningar + 1 korsreferens gav 4 nivålyft, 9 nya källor, 9 standardposter och 1 ny modell — korsreferenssteget (återanvänd källa, noll ny skörd) är gratis-lyften som automationen ska hitta själv i produktionen.
## Miljönot
I denna arbetsyta: Claude är skördare + extraktor (web_search/web_fetch), containern kör ingest. I produktion: crawler med källvitlista + LLM-API bakom extraktionsprompten + granskningskö-UI för konflikter. `ingest.py` är samma kod i båda miljöerna.
## Filkarta
| Fil | Roll |
|---|---|
| `intag/kallpaket-*.json` | Skördarens output |
| `intag/kandidatposter-*.json` | Extraktorns output |
| `lvx-extraktionsprompt.md` | LLM-kontraktet |
| `lvx-id-register.json` | ID-serier (mintning) |
| `ingest.py` | Pipelinens kärna |
| `lvx-klassposter-master-vN.json`, `lvx-graf-kanter-vN.json` | Beslutade utdata |
| `lvx-bounty-kandidater-*.json` | Flyhjulsförslag till granskning |