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:
@@ -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 |
|
||||
Reference in New Issue
Block a user