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,18 @@
|
||||
import json, hashlib
|
||||
from pathlib import Path
|
||||
ROT = Path("/mnt/user-data/outputs/landvex-produktionspaket-v1")
|
||||
ROLL = {"00-styrning":"styrning","10-data":"data (enda sanningskälla)","20-automation":"automation","30-produkt":"produkt","40-strategi":"strategi & rapporter"}
|
||||
filer = []
|
||||
for p in sorted(ROT.rglob("*")):
|
||||
if p.is_file() and p.name != "MANIFEST.json":
|
||||
rel = p.relative_to(ROT).as_posix()
|
||||
filer.append({"sokvag": rel, "roll": ROLL[rel.split("/")[0]],
|
||||
"bytes": p.stat().st_size,
|
||||
"sha256": hashlib.sha256(p.read_bytes()).hexdigest()})
|
||||
man = {"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":len(filer),"filer":filer}
|
||||
(ROT/"00-styrning/MANIFEST.json").write_text(json.dumps(man,ensure_ascii=False,indent=2),encoding="utf-8")
|
||||
print(f"MANIFEST: {len(filer)} filer checksummerade")
|
||||
@@ -0,0 +1,328 @@
|
||||
const pptxgen = require("pptxgenjs");
|
||||
const React = require("react");
|
||||
const ReactDOMServer = require("react-dom/server");
|
||||
const sharp = require("sharp");
|
||||
const {
|
||||
FaSearch, FaClipboardList, FaExclamationTriangle, FaLayerGroup, FaTree, FaIndustry,
|
||||
FaTint, FaQuestionCircle, FaBullseye, FaCamera, FaCheckCircle, FaSyncAlt, FaShieldAlt,
|
||||
FaExchangeAlt, FaHistory, FaRocket, FaChartLine, FaBuilding, FaLink, FaDatabase
|
||||
} = require("react-icons/fa");
|
||||
|
||||
// Palett: asfalt + galvstål + signalgul (vägmarkering)
|
||||
const DARK = "23272B", CARD_D = "2E343B", INK = "2A3138", BODY = "3A4149";
|
||||
const MUTED = "6B7480", MUTED_L = "AEB6BF", LIGHT = "F3F5F7", WHITE = "FFFFFF";
|
||||
const YEL = "F5B301", YEL_TINT = "FFF6DC", LINE = "D9DEE4";
|
||||
const H = "Arial", B = "Calibri", MONO = "Courier New";
|
||||
|
||||
const mkShadow = () => ({ type: "outer", color: "000000", blur: 7, offset: 2, angle: 45, opacity: 0.14 });
|
||||
|
||||
async function icon(Comp, color, size = 256) {
|
||||
const svg = ReactDOMServer.renderToStaticMarkup(React.createElement(Comp, { color, size: String(size) }));
|
||||
const png = await sharp(Buffer.from(svg)).png().toBuffer();
|
||||
return "image/png;base64," + png.toString("base64");
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const P = {};
|
||||
P.search = await icon(FaSearch, "#" + INK);
|
||||
P.clip = await icon(FaClipboardList, "#" + INK);
|
||||
P.warn = await icon(FaExclamationTriangle, "#" + INK);
|
||||
P.layer = await icon(FaLayerGroup, "#" + INK);
|
||||
P.tree = await icon(FaTree, "#" + INK);
|
||||
P.ind = await icon(FaIndustry, "#" + INK);
|
||||
P.tint = await icon(FaTint, "#" + INK);
|
||||
P.q = await icon(FaQuestionCircle, "#" + WHITE);
|
||||
P.bull = await icon(FaBullseye, "#" + WHITE);
|
||||
P.cam = await icon(FaCamera, "#" + WHITE);
|
||||
P.check = await icon(FaCheckCircle, "#" + WHITE);
|
||||
P.sync = await icon(FaSyncAlt, "#" + INK);
|
||||
P.shield = await icon(FaShieldAlt, "#" + INK);
|
||||
P.exch = await icon(FaExchangeAlt, "#" + INK);
|
||||
P.hist = await icon(FaHistory, "#" + INK);
|
||||
P.rocket = await icon(FaRocket, "#" + INK);
|
||||
P.chart = await icon(FaChartLine, "#" + INK);
|
||||
P.build = await icon(FaBuilding, "#" + INK);
|
||||
P.link = await icon(FaLink, "#" + INK);
|
||||
P.db = await icon(FaDatabase, "#" + INK);
|
||||
P.clipY = await icon(FaClipboardList, "#" + INK);
|
||||
P.warnY = await icon(FaExclamationTriangle, "#" + INK);
|
||||
P.checkY = await icon(FaCheckCircle, "#" + INK);
|
||||
|
||||
const pres = new pptxgen();
|
||||
pres.layout = "LAYOUT_16x9";
|
||||
pres.author = "Landvex";
|
||||
pres.title = "Landvex — Infrastructure Object Library, pilotpitch";
|
||||
|
||||
const title = (s, t, dark = false) =>
|
||||
s.addText(t, { x: 0.6, y: 0.42, w: 8.8, h: 0.7, fontFace: H, fontSize: 36, bold: true, color: dark ? WHITE : INK, margin: 0 });
|
||||
|
||||
const circleIcon = (s, data, cx, cy, d, fill) => {
|
||||
s.addShape(pres.shapes.OVAL, { x: cx, y: cy, w: d, h: d, fill: { color: fill } });
|
||||
const pad = d * 0.26;
|
||||
s.addImage({ data, x: cx + pad, y: cy + pad, w: d - 2 * pad, h: d - 2 * pad });
|
||||
};
|
||||
|
||||
// ---------- S1 · Titel (mörk) ----------
|
||||
let s = pres.addSlide();
|
||||
s.background = { color: DARK };
|
||||
s.addText("LANDVEX × QUIXZOOM", { x: 0.6, y: 0.55, w: 5.8, h: 0.35, fontFace: H, fontSize: 13, bold: true, color: YEL, charSpacing: 4, margin: 0 });
|
||||
s.addText([
|
||||
{ text: "Infrastructure", options: { breakLine: true } },
|
||||
{ text: "Object Library" }
|
||||
], { x: 0.6, y: 1.05, w: 5.9, h: 1.8, fontFace: H, fontSize: 42, bold: true, color: WHITE, margin: 0, lineSpacing: 46 });
|
||||
s.addText("Vet vad som står där ute — varje objekt i gaturummet, med källa på varje faktum.", { x: 0.6, y: 2.95, w: 5.5, h: 0.75, fontFace: B, fontSize: 15, color: MUTED_L, margin: 0 });
|
||||
s.addText("37 klasser · 26 modeller · 167 grafkanter · 0 valideringsfel", { x: 0.6, y: 3.85, w: 5.7, h: 0.35, fontFace: H, fontSize: 13, bold: true, color: YEL, margin: 0 });
|
||||
s.addText("Pilotpitch · juli 2026", { x: 0.6, y: 5.05, w: 4, h: 0.3, fontFace: B, fontSize: 11, color: MUTED, margin: 0 });
|
||||
const chips = [
|
||||
["LVX-TRP-0102", "Vägbelysningsarmatur"],
|
||||
["LVX-P-0000026", "Tesla Supercharger V4"],
|
||||
["LVX-VAT-0103", "Brandpost / hydrant"],
|
||||
["LVX-ELN-0101", "Kabelskåp lågspänning"]
|
||||
];
|
||||
chips.forEach((c, i) => {
|
||||
const y = 1.05 + i * 0.82;
|
||||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 6.35, y, w: 3.05, h: 0.62, rectRadius: 0.06, fill: { color: CARD_D } });
|
||||
s.addText([
|
||||
{ text: c[0], options: { fontFace: MONO, fontSize: 11.5, bold: true, color: YEL, breakLine: true } },
|
||||
{ text: c[1], options: { fontFace: B, fontSize: 10.5, color: WHITE } }
|
||||
], { x: 6.55, y: y + 0.05, w: 2.75, h: 0.52, margin: 0, lineSpacing: 13 });
|
||||
});
|
||||
s.addNotes("Landvex bygger referensbiblioteket för fysisk infrastruktur. QUIXZOOM är fältarmen. Siffrorna är dagens bibliotek — validerat, inte vision.");
|
||||
|
||||
// ---------- S2 · Problemet ----------
|
||||
s = pres.addSlide();
|
||||
s.background = { color: WHITE };
|
||||
title(s, "Gatan saknar identitet");
|
||||
const rows2 = [
|
||||
[P.clip, "Inventering är manuell och dyr", "Beståndsdata byggs om från noll vid varje upphandling och ägarbyte."],
|
||||
[P.warn, "Utgångna modeller upptäcks för sent", "Reservdelsrisken syns först när armaturen slocknat eller locket spruckit."],
|
||||
[P.search, "\u201dVad är detta?\u201d saknar svar i fält", "Tekniker, system och AI gissar — det finns ingen gemensam referens."]
|
||||
];
|
||||
rows2.forEach((r, i) => {
|
||||
const y = 1.4 + i * 1.15;
|
||||
circleIcon(s, r[0], 0.6, y, 0.52, YEL);
|
||||
s.addText(r[1], { x: 1.35, y: y - 0.06, w: 4.15, h: 0.35, fontFace: H, fontSize: 15, bold: true, color: INK, margin: 0 });
|
||||
s.addText(r[2], { x: 1.35, y: y + 0.3, w: 4.15, h: 0.62, fontFace: B, fontSize: 12, color: MUTED, margin: 0 });
|
||||
});
|
||||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 5.85, y: 1.35, w: 3.55, h: 3.4, rectRadius: 0.09, fill: { color: WHITE }, line: { color: LINE, width: 1 }, shadow: mkShadow() });
|
||||
s.addText("60M+", { x: 6.1, y: 1.6, w: 3.05, h: 0.95, fontFace: H, fontSize: 58, bold: true, color: INK, margin: 0 });
|
||||
s.addText("elstolpar enbart i USA — de flesta bär identiteten inbränd i godset.", { x: 6.1, y: 2.65, w: 3.05, h: 0.85, fontFace: B, fontSize: 12.5, color: MUTED, margin: 0 });
|
||||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 6.1, y: 3.7, w: 3.05, h: 0.62, rectRadius: 0.06, fill: { color: YEL_TINT } });
|
||||
s.addText("birthmark: år · klass · impregnering", { x: 6.28, y: 3.78, w: 2.75, h: 0.46, fontFace: MONO, fontSize: 10.5, bold: true, color: INK, margin: 0, valign: "middle" });
|
||||
s.addNotes("Problemet är inte brist på data — det är brist på identitet. Ingen gemensam nyckel mellan fält, system och leverantörer.");
|
||||
|
||||
// ---------- S3 · Lösningen: fem nivåer ----------
|
||||
s = pres.addSlide();
|
||||
s.background = { color: WHITE };
|
||||
title(s, "Ett bibliotek — fem nivåer");
|
||||
const steps = [
|
||||
["Domän", "TRP · VAT · ELN…"],
|
||||
["Objektklass", "LVX-TRP-0102"],
|
||||
["Typ", "Cobrahead-LED"],
|
||||
["Produktmodell", "LVX-P-0000003"],
|
||||
["Individ", "LVX-I-… (pilot)"]
|
||||
];
|
||||
steps.forEach((st, i) => {
|
||||
const x = 0.6 + i * 1.9;
|
||||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x, y: 1.35, w: 1.55, h: 0.95, rectRadius: 0.08, fill: { color: LIGHT } });
|
||||
s.addText([
|
||||
{ text: st[0], options: { fontFace: H, fontSize: 12.5, bold: true, color: INK, breakLine: true } },
|
||||
{ text: st[1], options: { fontFace: MONO, fontSize: 9.5, color: MUTED } }
|
||||
], { x: x + 0.12, y: 1.47, w: 1.31, h: 0.75, margin: 0, lineSpacing: 14 });
|
||||
if (i < 4) s.addText("\u203A", { x: x + 1.55, y: 1.55, w: 0.35, h: 0.5, fontFace: H, fontSize: 20, color: MUTED, align: "center", margin: 0 });
|
||||
});
|
||||
const cards3 = [
|
||||
[P.link, "Permanenta LVX-ID", "Identiteten överlever ägarbyten, versioner och systembyten — referensen alla kan peka på."],
|
||||
[P.db, "Källa på varje faktum", "Problem utan källa publiceras aldrig. PLATSHÅLLARE-regeln är hård — juridik by design."],
|
||||
[P.hist, "Bitemporal fakta", "Vad som gällde där ute — och när vi visste det. Ersättningskedjor blir spårbara."]
|
||||
];
|
||||
cards3.forEach((c, i) => {
|
||||
const x = 0.6 + i * 3.07;
|
||||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x, y: 2.85, w: 2.87, h: 2.15, rectRadius: 0.09, fill: { color: LIGHT } });
|
||||
circleIcon(s, c[0], x + 0.22, y3(i), 0.46, YEL);
|
||||
s.addText(c[1], { x: x + 0.85, y: 3.05, w: 1.95, h: 0.6, fontFace: H, fontSize: 13.5, bold: true, color: INK, margin: 0 });
|
||||
s.addText(c[2], { x: x + 0.22, y: 3.75, w: 2.45, h: 1.1, fontFace: B, fontSize: 11.5, color: BODY, margin: 0 });
|
||||
});
|
||||
function y3() { return 3.05; }
|
||||
s.addNotes("Fem nivåer: domän ned till individ. Piloten fyller individlagret för kundens geografi. ID:na är permanenta — det är vallgraven.");
|
||||
|
||||
// ---------- S4 · Bevis ----------
|
||||
s = pres.addSlide();
|
||||
s.background = { color: WHITE };
|
||||
title(s, "Byggt — och validerat");
|
||||
const stats = [
|
||||
["37", "objektklasser — 31 källbelagda"],
|
||||
["26", "produktmodeller — alla källbelagda"],
|
||||
["167", "grafkanter i konsolidering v2"],
|
||||
["0", "valideringsfel mot schemat"]
|
||||
];
|
||||
stats.forEach((st, i) => {
|
||||
const x = 0.6 + i * 2.25;
|
||||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x, y: 1.3, w: 2.05, h: 1.45, rectRadius: 0.09, fill: { color: WHITE }, line: { color: LINE, width: 1 }, shadow: mkShadow() });
|
||||
s.addText(st[0], { x: x + 0.18, y: 1.42, w: 1.7, h: 0.7, fontFace: H, fontSize: 40, bold: true, color: i === 3 ? YEL : INK, margin: 0 });
|
||||
s.addText(st[1], { x: x + 0.18, y: 2.14, w: 1.72, h: 0.55, fontFace: B, fontSize: 10.5, color: MUTED, margin: 0 });
|
||||
});
|
||||
s.addText("Verifieringstrappan — varje fältfoto lyfter posten ett steg", { x: 0.6, y: 3.12, w: 8.8, h: 0.3, fontFace: B, fontSize: 12.5, italic: true, color: MUTED, margin: 0 });
|
||||
const stair = ["Obekräftad", "Källbelagd", "Fältverifierad", "Tillverkarbekräftad"];
|
||||
stair.forEach((t, i) => {
|
||||
const h = 0.55 + i * 0.28, x = 0.6 + i * 2.25;
|
||||
s.addShape(pres.shapes.RECTANGLE, { x, y: 5.05 - h, w: 2.05, h, fill: { color: i === 3 ? YEL : ["E7EAEE", "D5DAE0", "BFC7CF"][i] } });
|
||||
s.addText(t, { x: x + 0.1, y: 5.05 - h + 0.08, w: 1.85, h: 0.35, fontFace: H, fontSize: 11.5, bold: true, color: INK, margin: 0 });
|
||||
});
|
||||
s.addNotes("Detta är inte en idé — biblioteket finns, är källbelagt och maskinvaliderat. Trappan är kvalitetsmodellen: piloten flyttar poster till fältverifierad.");
|
||||
|
||||
// ---------- S5 · OCR-guldet ----------
|
||||
s = pres.addSlide();
|
||||
s.background = { color: WHITE };
|
||||
title(s, "Identiteten sitter i godset");
|
||||
const cards5 = [
|
||||
[P.tree, "Stolpens birthmark", "Inbränd märkning i ögonhöjd: år, längd, klass och impregneringsmedel.", "år · klass · CCA/penta"],
|
||||
[P.ind, "Gjutmärket", "Tillverkare, funktion och lastklass gjuts in i lock och ram.", "ULEFOS · EN124-2 · D400"],
|
||||
[P.tint, "Hydrantens kropp", "AWWA kräver gjuten märkning — tillverkningsår och öppningsriktning.", "AMERICAN-DARLING · 1998"]
|
||||
];
|
||||
cards5.forEach((c, i) => {
|
||||
const x = 0.6 + i * 3.07;
|
||||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x, y: 1.3, w: 2.87, h: 3.15, rectRadius: 0.09, fill: { color: LIGHT } });
|
||||
circleIcon(s, c[0], x + 0.22, 1.55, 0.5, YEL);
|
||||
s.addText(c[1], { x: x + 0.88, y: 1.6, w: 1.95, h: 0.6, fontFace: H, fontSize: 14, bold: true, color: INK, margin: 0 });
|
||||
s.addText(c[2], { x: x + 0.22, y: 2.35, w: 2.45, h: 1.05, fontFace: B, fontSize: 11.5, color: BODY, margin: 0 });
|
||||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: x + 0.22, y: 3.6, w: 2.43, h: 0.55, rectRadius: 0.06, fill: { color: WHITE }, line: { color: LINE, width: 1 } });
|
||||
s.addText(c[3], { x: x + 0.36, y: 3.66, w: 2.2, h: 0.43, fontFace: MONO, fontSize: 10, bold: true, color: INK, margin: 0, valign: "middle" });
|
||||
});
|
||||
s.addText("Ingen databas krävs på plats — objektet bär sin egen identitet. OCR-först är protokollets kärna.", { x: 0.6, y: 4.75, w: 8.8, h: 0.35, fontFace: B, fontSize: 13, italic: true, color: MUTED, margin: 0 });
|
||||
s.addNotes("Läsbarheten är affärsidén: birthmarks, gjutmärken och typskyltar gör varje foto till en databaspost.");
|
||||
|
||||
// ---------- S6 · Flyhjulet ----------
|
||||
s = pres.addSlide();
|
||||
s.background = { color: WHITE };
|
||||
title(s, "Flyhjulet: QUIXZOOM");
|
||||
const nodes = [
|
||||
[2.3, 1.12, P.q, "Kundfråga", "identify → okänd"],
|
||||
[3.85, 2.38, P.bull, "Bounty", "skapas automatiskt"],
|
||||
[2.3, 3.6, P.cam, "Zoomer-foto", "OCR + position"],
|
||||
[0.75, 2.38, P.check, "Fältverifierad", "post + individ"]
|
||||
];
|
||||
nodes.forEach(n => {
|
||||
circleIcon(s, n[2], n[0], n[1], 0.95, DARK);
|
||||
s.addText(n[3], { x: n[0] - 0.45, y: n[1] + 0.92, w: 1.85, h: 0.28, fontFace: H, fontSize: 11.5, bold: true, color: INK, align: "center", margin: 0 });
|
||||
s.addText(n[4], { x: n[0] - 0.45, y: n[1] + 1.16, w: 1.85, h: 0.24, fontFace: B, fontSize: 9.5, color: MUTED, align: "center", margin: 0 });
|
||||
});
|
||||
circleIcon(s, P.sync, 2.5, 2.6, 0.55, YEL);
|
||||
s.addText("\u2198", { x: 3.5, y: 2.0, w: 0.4, h: 0.4, fontFace: H, fontSize: 16, color: MUTED, margin: 0 });
|
||||
s.addText("\u2199", { x: 3.5, y: 3.3, w: 0.4, h: 0.4, fontFace: H, fontSize: 16, color: MUTED, margin: 0 });
|
||||
s.addText("\u2196", { x: 1.85, y: 3.3, w: 0.4, h: 0.4, fontFace: H, fontSize: 16, color: MUTED, margin: 0 });
|
||||
s.addText("\u2197", { x: 1.85, y: 2.0, w: 0.4, h: 0.4, fontFace: H, fontSize: 16, color: MUTED, margin: 0 });
|
||||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 5.85, y: 1.3, w: 3.55, h: 3.2, rectRadius: 0.09, fill: { color: LIGHT } });
|
||||
s.addText("Bounty-motorn", { x: 6.1, y: 1.5, w: 3.05, h: 0.35, fontFace: H, fontSize: 14, bold: true, color: INK, margin: 0 });
|
||||
s.addText([
|
||||
{ text: "16 aktiva bounties ur posternas OCR-fält", options: { bullet: true, breakLine: true } },
|
||||
{ text: "Poäng: bas · först-på-individ ×2 · full OCR +50 %", options: { bullet: true, breakLine: true } },
|
||||
{ text: "Hårda säkerhetsregler — allt fotas från gångyta", options: { bullet: true, breakLine: true } },
|
||||
{ text: "Varje \u201dobekräftad\u201d i API:t skapar en bounty", options: { bullet: true } }
|
||||
], { x: 6.1, y: 1.95, w: 3.1, h: 2.4, fontFace: B, fontSize: 11.5, color: BODY, paraSpaceAfter: 8, margin: 0 });
|
||||
s.addText("Kundens fråga finansierar datan som gör nästa svar bättre.", { x: 0.6, y: 5.12, w: 8.8, h: 0.35, fontFace: B, fontSize: 13, italic: true, color: MUTED, margin: 0 });
|
||||
s.addNotes("Flyhjulet: obesvarade kundfrågor blir bounties, Zoomers fotograferar, posten fältverifieras, svaret blir bättre. Andelen kunddrivna bounties är hälsotalet.");
|
||||
|
||||
// ---------- S7 · API:t ----------
|
||||
s = pres.addSlide();
|
||||
s.background = { color: WHITE };
|
||||
title(s, "Produkten: Object API v0");
|
||||
const eps = [
|
||||
["POST", "/v0/identify", "bild in → modell ut"],
|
||||
["GET", "/v0/objects/{lvx_id}", "hela posten + källor"],
|
||||
["GET", "/v0/objects/{id}/succession", "ersättningskedjan"],
|
||||
["GET", "/v0/search", "fritext + filter"],
|
||||
["GET", "/v0/standards/{beteckning}", "objekt per standard"],
|
||||
["POST", "/v0/feedback", "fel/okänt → bounty"]
|
||||
];
|
||||
eps.forEach((e, i) => {
|
||||
const y = 1.3 + i * 0.58;
|
||||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 0.6, y, w: 0.62, h: 0.32, rectRadius: 0.05, fill: { color: e[0] === "POST" ? YEL : CARD_D } });
|
||||
s.addText(e[0], { x: 0.6, y, w: 0.62, h: 0.32, fontFace: H, fontSize: 9, bold: true, color: e[0] === "POST" ? INK : WHITE, align: "center", valign: "middle", margin: 0 });
|
||||
s.addText(e[1], { x: 1.35, y: y + 0.01, w: 2.62, h: 0.32, fontFace: MONO, fontSize: 10.5, bold: true, color: INK, margin: 0, valign: "middle" });
|
||||
s.addText(e[2], { x: 4.02, y: y + 0.01, w: 1.72, h: 0.32, fontFace: B, fontSize: 9.5, color: MUTED, margin: 0, valign: "middle" });
|
||||
});
|
||||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 5.85, y: 1.3, w: 3.55, h: 3.45, rectRadius: 0.09, fill: { color: WHITE }, line: { color: LINE, width: 1 }, shadow: mkShadow() });
|
||||
circleIcon(s, P.shield, 6.1, 1.52, 0.46, YEL);
|
||||
s.addText("Svarskontraktet", { x: 6.72, y: 1.58, w: 2.6, h: 0.35, fontFace: H, fontSize: 14, bold: true, color: INK, margin: 0 });
|
||||
s.addText([
|
||||
{ text: "verifieringsnivå — alltid med", options: { bullet: true, breakLine: true } },
|
||||
{ text: "konfidens 0–1 — aldrig dold", options: { bullet: true, breakLine: true } },
|
||||
{ text: "källor[] på varje påstående", options: { bullet: true, breakLine: true } },
|
||||
{ text: "PLATSHÅLLARE filtreras bort vid serialisering", options: { bullet: true, breakLine: true } },
|
||||
{ text: "svagt svar ⇒ bounty-ID följer med", options: { bullet: true } }
|
||||
], { x: 6.1, y: 2.15, w: 3.1, h: 2.4, fontFace: B, fontSize: 11.5, color: BODY, paraSpaceAfter: 8, margin: 0 });
|
||||
s.addText("Juridiken sitter i kontraktet — referenser och beteckningar, aldrig standardtext.", { x: 0.6, y: 5.05, w: 8.8, h: 0.35, fontFace: B, fontSize: 13, italic: true, color: MUTED, margin: 0 });
|
||||
s.addNotes("Sex endpoints räcker för piloten. Kontraktet är differentieringen: ärlighet och proveniens är obligatoriska fält, inte fotnoter.");
|
||||
|
||||
// ---------- S8 · Golden cases ----------
|
||||
s = pres.addSlide();
|
||||
s.background = { color: WHITE };
|
||||
title(s, "Tre svar som säljer");
|
||||
const g = [
|
||||
[P.exch, "Ersättningskedjan", "GE ERS identifieras → status: ersatt → ersättare: ERL-serien. Reservdelsrisken syns innan lagret tar slut.", "identify → succession"],
|
||||
[P.hist, "Årsmodell ur godset", "American-Darling B-84-B, gjutet 1998, öppnar vänster — individdata utan register.", "konfidens 0,97 · källbelagd"],
|
||||
[P.bull2 || P.search, "Ärlig när den inte vet", "Okänt UK-skåp → bästa klass, konfidens 0,71 — och en bounty skapas automatiskt.", "svar + BNTY-A0001"]
|
||||
];
|
||||
g.forEach((c, i) => {
|
||||
const x = 0.6 + i * 3.07;
|
||||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x, y: 1.3, w: 2.87, h: 3.15, rectRadius: 0.09, fill: { color: LIGHT } });
|
||||
circleIcon(s, c[0], x + 0.22, 1.55, 0.5, YEL);
|
||||
s.addText(c[1], { x: x + 0.88, y: 1.6, w: 1.95, h: 0.6, fontFace: H, fontSize: 14, bold: true, color: INK, margin: 0 });
|
||||
s.addText(c[2], { x: x + 0.22, y: 2.35, w: 2.45, h: 1.15, fontFace: B, fontSize: 11.5, color: BODY, margin: 0 });
|
||||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: x + 0.22, y: 3.6, w: 2.43, h: 0.55, rectRadius: 0.06, fill: { color: WHITE }, line: { color: LINE, width: 1 } });
|
||||
s.addText(c[3], { x: x + 0.36, y: 3.66, w: 2.2, h: 0.43, fontFace: MONO, fontSize: 10, bold: true, color: INK, margin: 0, valign: "middle" });
|
||||
});
|
||||
s.addText("Alla tre körs på riktiga poster — demodatat ingår i pilotpaketet.", { x: 0.6, y: 4.75, w: 8.8, h: 0.35, fontFace: B, fontSize: 13, italic: true, color: MUTED, margin: 0 });
|
||||
s.addNotes("Tre golden cases som demoas live: succession, individdata ur gjutgods, och ärlighet + bounty vid okänt objekt.");
|
||||
|
||||
// ---------- S9 · Paketering ----------
|
||||
s = pres.addSlide();
|
||||
s.background = { color: WHITE };
|
||||
title(s, "Paketering");
|
||||
const pk = [
|
||||
[P.rocket, "Pilot", "1–3 designpartners", ["Fast månadsavgift, generös kvot", "Gemensam backlog styr utvecklingen", "Betalar även med fältaccess"], LIGHT],
|
||||
[P.chart, "Pro", "entreprenad · besiktning · försäkring", ["Usage-based per identify", "Billiga uppslag driver LVX-ID-inlåsning", "Volymtrappa"], YEL_TINT],
|
||||
[P.build, "Enterprise", "nätbolag · förvaltare · tillverkare", ["SLA + eget individlager (LVX-I)", "Bulkexport och integrationer", "Riktade bounties i egen geografi"], LIGHT]
|
||||
];
|
||||
pk.forEach((c, i) => {
|
||||
const x = 0.6 + i * 3.07;
|
||||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x, y: 1.3, w: 2.87, h: 3.1, rectRadius: 0.09, fill: { color: c[4] }, shadow: i === 1 ? mkShadow() : undefined });
|
||||
circleIcon(s, c[0], x + 0.22, 1.52, 0.46, WHITE === c[4] ? LIGHT : WHITE);
|
||||
s.addText(c[1], { x: x + 0.82, y: 1.55, w: 1.95, h: 0.35, fontFace: H, fontSize: 16, bold: true, color: INK, margin: 0 });
|
||||
s.addText(c[2], { x: x + 0.22, y: 2.12, w: 2.45, h: 0.3, fontFace: B, fontSize: 10.5, italic: true, color: MUTED, margin: 0 });
|
||||
s.addText(c[3].map((t, j) => ({ text: t, options: { bullet: true, breakLine: j < 2 } })), { x: x + 0.22, y: 2.5, w: 2.5, h: 1.75, fontFace: B, fontSize: 11, color: BODY, paraSpaceAfter: 7, margin: 0 });
|
||||
});
|
||||
s.addText("Identifieringen är värdet · uppslaget är räckvidden · bounty-krediter köper fältverifiering där kunden behöver den.", { x: 0.6, y: 4.75, w: 8.8, h: 0.35, fontFace: B, fontSize: 12.5, italic: true, color: MUTED, margin: 0 });
|
||||
s.addNotes("SaaS-logiken: identify prissätts per värdefullt svar, uppslag hålls billiga för att göra LVX-ID till branschreferens, Enterprise får eget individlager.");
|
||||
|
||||
// ---------- S10 · Piloten (mörk) ----------
|
||||
s = pres.addSlide();
|
||||
s.background = { color: DARK };
|
||||
title(s, "Piloten: 90 dagar till bevis", true);
|
||||
const uc = [
|
||||
[P.clipY, "Inventering", "Fota beståndet → klass, modell och status per objekt."],
|
||||
[P.warnY, "Utgånget-listan", "Allt med status ersatt/utgången — med ersättare och källa."],
|
||||
[P.checkY, "Problemflagg", "Källbelagda klassproblem matchade mot ert bestånd."]
|
||||
];
|
||||
uc.forEach((r, i) => {
|
||||
const y = 1.45 + i * 1.1;
|
||||
circleIcon(s, r[0], 0.6, y, 0.52, YEL);
|
||||
s.addText(r[1], { x: 1.35, y: y - 0.05, w: 4.1, h: 0.35, fontFace: H, fontSize: 15, bold: true, color: WHITE, margin: 0 });
|
||||
s.addText(r[2], { x: 1.35, y: y + 0.31, w: 4.1, h: 0.6, fontFace: B, fontSize: 12, color: MUTED_L, margin: 0 });
|
||||
});
|
||||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 5.85, y: 1.35, w: 3.55, h: 3.35, rectRadius: 0.09, fill: { color: CARD_D } });
|
||||
s.addText("Erbjudandet", { x: 6.1, y: 1.55, w: 3.05, h: 0.35, fontFace: H, fontSize: 14, bold: true, color: WHITE, margin: 0 });
|
||||
s.addText([
|
||||
{ text: "Avgränsat område, 90 dagar", options: { bullet: true, breakLine: true } },
|
||||
{ text: "Fast pilotavgift — kvittas mot årsavtal", options: { bullet: true, breakLine: true } },
|
||||
{ text: "Gemensam backlog styr bounties", options: { bullet: true, breakLine: true } },
|
||||
{ text: "Ni behåller export av ert individlager", options: { bullet: true } }
|
||||
], { x: 6.1, y: 2.0, w: 3.1, h: 1.85, fontFace: B, fontSize: 11.5, color: MUTED_L, paraSpaceAfter: 8, margin: 0 });
|
||||
s.addText("Boka pilotworkshop \u2192", { x: 6.1, y: 4.05, w: 3.05, h: 0.45, fontFace: H, fontSize: 17, bold: true, color: YEL, margin: 0 });
|
||||
s.addText("LANDVEX × QUIXZOOM · juli 2026", { x: 0.6, y: 5.08, w: 5, h: 0.3, fontFace: B, fontSize: 11, color: MUTED, charSpacing: 2, margin: 0 });
|
||||
s.addNotes("Stäng med erbjudandet: 90 dagar, avgränsat område, tre användningsfall, fast avgift som kvittas. Nästa steg är en workshop där området och backloggen väljs.");
|
||||
|
||||
await pres.writeFile({ fileName: "/home/claude/landvex-pilotpitch.pptx" });
|
||||
console.log("deck skriven");
|
||||
})().catch(e => { console.error(e); process.exit(1); });
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/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}")
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
# Landvex IOL — våg 5: konsolidering v2 (Tier 1 + Tier 2), validering, grafexport
|
||||
import json, re
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path("/mnt/user-data/outputs")
|
||||
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}$")
|
||||
|
||||
def load(name):
|
||||
with open(OUT / name, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
poster = {}
|
||||
for fn in ["lvx-klassposter-master-v1.json","lvx-klassposter-tier2-trp.json","lvx-klassposter-tier2-ovriga.json"]:
|
||||
for p in load(fn)["poster"]:
|
||||
poster[p["lvx_id"]] = p
|
||||
|
||||
def multi_lvx(mal):
|
||||
return " / " in mal and all(ID_RE.match(x.strip()) for x in mal.split(" / "))
|
||||
|
||||
olosta = {}
|
||||
for p in poster.values():
|
||||
for rel in p.get("relationer", []):
|
||||
mal = rel.get("mal","")
|
||||
if rel.get("predikat") == "foljer_standard" or multi_lvx(mal) or ID_RE.match(mal):
|
||||
continue
|
||||
olosta[mal] = olosta.get(mal, 0) + 1
|
||||
|
||||
modeller = {}
|
||||
for fn in ["lvx-produktmodeller-vag2.json","lvx-produktmodeller-vag3.json"]:
|
||||
for m in load(fn)["poster"]:
|
||||
modeller[m["lvx_id"]] = m
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
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_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")
|
||||
verif_count = {}
|
||||
for p in poster.values():
|
||||
v = p["proveniens"]["verifieringsniva"]
|
||||
verif_count[v] = verif_count.get(v,0)+1
|
||||
|
||||
master = {
|
||||
"beskrivning": "Landvex IOL — konsoliderade klassposter v2: Tier 1 + Tier 2 (37 klasser). Ersätter master-v1 och de två tier2-filerna som arbetskopia.",
|
||||
"genererad": "2026-07-04",
|
||||
"antal_poster": len(poster),
|
||||
"verifieringsfordelning": verif_count,
|
||||
"poster": [poster[k] for k in sorted(poster)]
|
||||
}
|
||||
with open(OUT/"lvx-klassposter-master-v2.json","w",encoding="utf-8") as f:
|
||||
json.dump(master,f,ensure_ascii=False,indent=2)
|
||||
|
||||
graf = {
|
||||
"beskrivning": "Landvex IOL — grafexport v2. Tier 1 + Tier 2-klasser samt produktmodeller. objekt_typ: lvx=upplöst nod, standard=standardbeteckning, text=olöst platshållare (framtida klass).",
|
||||
"genererad": "2026-07-04",
|
||||
"noder": {"objektklasser": len(poster), "produktmodeller": len(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
|
||||
}
|
||||
with open(OUT/"lvx-graf-kanter-v2.json","w",encoding="utf-8") as f:
|
||||
json.dump(graf,f,ensure_ascii=False,indent=2)
|
||||
|
||||
print(f"Klassposter: {len(poster)} | fördelning: {verif_count}")
|
||||
print(f"Produktmodeller: {len(modeller)}")
|
||||
print(f"Grafkanter: {len(kanter)} totalt | {lvx_k} LVX-upplösta | {std_k} 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}")
|
||||
Reference in New Issue
Block a user