/** * Deterministic situation graph builder — builds initial graph from scenario text. * Takes v0.2/v0.3 analysis output (from analyseScenario) and constructs a SituationGraph. */ import { situationNodeSchema, situationEdgeSchema, makeNodeId, } from "./schema.js"; /** * Build an initial situation graph from a v0.3 reconstruction result. * @param {{ reconstruction: object, evidence: object[] | undefined }} analysisData * @returns {{ nodes: import("./schema.js").SituationNode[], edges: import("./schema.js").SituationEdge[] }} */ export function buildInitialGraph(analysisData) { const { reconstruction, evidence = [] } = analysisData; if (!reconstruction || !reconstruction.summary) { return { nodes: [], edges: [] }; } const nodeMap = new Map(); // label -> node const sourceIdToNodeId = new Map(); function mapSourceId(sourceId, node) { if (sourceId) sourceIdToNodeId.set(sourceId, node.id); return node; } // ── Helper: register or get a node by label ──────────── function ensureNode( label, kind, status, description, value, unit, confidence, ) { if (nodeMap.has(label)) return nodeMap.get(label); const id = makeNodeId(label); const node = situationNodeSchema.parse({ id, label, description: description ?? label, kind, status, confidence, value: value ?? null, unit: unit ?? null, evidenceIds: [], dependsOn: [], affects: [], parentId: null, childIds: [], }); nodeMap.set(label, node); return node; } // ── Evidence lookup ──────────────────────────────────── const evidenceMap = new Map(); for (const ev of evidence) { if (ev.id) evidenceMap.set(ev.id, ev); } function addEvidenceToNode(nodeId, evidenceId) { const node = Object.values(nodeMap).find((n) => n.id === nodeId); if (node && !node.evidenceIds.includes(evidenceId)) { node.evidenceIds.push(evidenceId); } } // ── Extract observed states as nodes ──────────────────── const summaryNode = ensureNode( reconstruction.summary || "Situation Summary", "state", "provisional", "Summary of the situation from the scenario text", null, null, "medium", ); // Collect all observable quantities as metric nodes const metrics = new Map(); if (reconstruction.observedStates) { for (const obs of reconstruction.observedStates) { const node = ensureNode( obs.description || obs.label, "observation", "supported", obs.description || obs.label, null, null, obs.confidence || "medium", ); if (obs.id) { node.evidenceIds.push(obs.id); mapSourceId(obs.id, node); } } } // Actors as states/nodes if (reconstruction.actors) { for (const actor of reconstruction.actors) { mapSourceId(actor.id, ensureNode( actor.description || actor.label, "observation", "supported", actor.description || actor.label, null, null, actor.confidence || "medium", )); } } if (reconstruction.systemsOrObjects) { for (const sys of reconstruction.systemsOrObjects) { mapSourceId(sys.id, ensureNode( sys.description || sys.label, "metric", "known", sys.description || sys.label, null, null, sys.confidence || "medium", )); } } // Differences as relationship nodes if (reconstruction.differences) { for (const diff of reconstruction.differences) { const node = ensureNode( diff.description || "Difference", "relationship", "supported", diff.description || "Difference", null, null, diff.confidence || "medium", ); mapSourceId(diff.id, node); } } // Contradictions as nodes if (reconstruction.contradictions) { for (const c of reconstruction.contradictions) { const node = ensureNode( c.description || c.label, "relationship", "supported", c.description || c.label, null, null, c.confidence || "medium", ); mapSourceId(c.id, node); } } // Important unknowns as unknown nodes const unknownNodes = []; if (reconstruction.importantUnknowns) { for (const unk of reconstruction.importantUnknowns) { const node = ensureNode( unk.description || unk.label, "unknown", "unknown", unk.description || "Unknown factor in the situation", null, null, unk.confidence || "low", ); mapSourceId(unk.id, node); unknownNodes.push(node); } } // Plausible interpretations if (reconstruction.plausibleInterpretations) { for (const interp of reconstruction.plausibleInterpretations) { mapSourceId(interp.id, ensureNode( interp.description || interp.label, "assumption", "provisional", interp.description || "Plausible interpretation", null, null, interp.confidence || "low", )); } } // Unexplained transitions remain unresolved transition nodes if (reconstruction.unexplainedTransitions) { for (const trans of reconstruction.unexplainedTransitions) { const label = trans.description || `${trans.entity ?? "Unexplained transition"}: ${trans.previousState ?? "unknown"} → ${trans.currentState ?? "unknown"}`; mapSourceId(trans.id, ensureNode( label, "transition", "unknown", trans.description || label, null, null, trans.confidence || "low", )); } } // Known transitions if (reconstruction.knownTransitions) { for (const trans of reconstruction.knownTransitions) { mapSourceId(trans.id, ensureNode( `${trans.entity}: ${trans.previousState} → ${trans.currentState}`, "transition", trans.explanationStatus === "confirmed" ? "known" : "provisional", trans.description || `Transition: ${trans.entity} from ${trans.previousState} to ${trans.currentState}`, null, null, trans.confidence || "medium", )); } } // ── Build edges between nodes ──────────────────────── const nodeArr = Array.from(nodeMap.values()); const edges = []; // Link actors → observed states as measures relationships let actorNodes = []; let metricNodes = []; let unknownNodeIds = []; for (const n of nodeArr) { if (n.kind === "observation" && n.status === "supported") { // These are observations — link to summary edges.push( situationEdgeSchema.parse({ id: `e-sum-${n.id}`, fromNodeId: n.id, toNodeId: summaryNode.id, relationship: "supports", confidence: n.confidence || "medium", description: `${n.label} supports the summary`, }), ); } if (n.kind === "unknown") { unknownNodeIds.push(n.id); edges.push( situationEdgeSchema.parse({ id: `e-unk-${n.id}`, fromNodeId: n.id, toNodeId: summaryNode.id, relationship: "depends_on", confidence: n.confidence || "low", description: `${n.label} is an unresolved factor for this situation`, }), ); } } const edgeIds = new Set(edges.map((edge) => edge.id)); for (const relationship of reconstruction.relationships ?? []) { const fromNodeId = sourceIdToNodeId.get(relationship.fromId); const toNodeId = sourceIdToNodeId.get(relationship.toId); const id = `e-rel-${relationship.id}`; if (!fromNodeId || !toNodeId || edgeIds.has(id)) continue; edges.push( situationEdgeSchema.parse({ id, fromNodeId, toNodeId, relationship: relationship.relationship, confidence: relationship.confidence, description: relationship.description, }), ); edgeIds.add(id); } return { nodes: nodeArr, edges }; } /** * Build a minimal starting graph for any scenario. * Used when analysis has no reconstruction data (e.g., error state). */ export function buildMinimalGraph(scenario) { const shortLabel = scenario.slice(0, 80); return { nodes: [ situationNodeSchema.parse({ id: "n0", label: shortLabel, description: `Initial situation from: "${scenario.slice(0, 200)}"`, kind: "state", status: "provisional", confidence: "low", value: null, unit: null, evidenceIds: [], dependsOn: [], affects: [], parentId: null, childIds: [], }), ], edges: [], }; } /** * Convert graph nodes/edges to a human-readable summary for display. */ export function describeGraph(graph) { const parts = []; // Count by kind const byKind = {}; for (const n of graph.nodes) { byKind[n.kind] = (byKind[n.kind] || 0) + 1; } parts.push( `Nodes: ${Object.entries(byKind) .map(([k, v]) => `${v} ${k}`) .join(", ")}`, ); parts.push(`Edges: ${graph.edges.length} total`); parts.push( `Unknowns: ${graph.nodes.filter((n) => n.status === "unknown").length} unresolved`, ); return parts.join(" | "); }