Files
confidence-engine/lib/graph/builder.js
T

339 lines
8.8 KiB
JavaScript

/**
* 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",
));
}
}
// 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(" | ");
}