Files
confidence-engine/tests/graph/shared-anchor-production-path.test.js
T
robbond 6f00a5e567 experiment: test production shared-anchor pattern (Exp 49)
Creates tests/graph/shared-anchor-production-path.test.js (36 tests, all pass).

Experiment 49 asks whether any sequence of real production updates via
applyValidatedProposal creates two or more active unknowns sharing the same
populated relationship anchor. Two sequential-update scenarios (Cases A & B)
consistently returned separate_anchors or insufficient_data — no shared
anchor observed in tested flows.

Control cases C–F confirm: diagnostic correctly distinguishes shared vs
separated patterns on controlled fixtures; all produced nodes/edges pass
schema validation; resolving one node does not mutate another (immunity);
decomposition children share parent anchor correctly.

Combined regression suite: 78 tests across Exp 47 (26), Exp 48 (16),
Exp 49 (36) — all passing, no production code modified.
2026-08-07 06:25:17 +01:00

956 lines
37 KiB
JavaScript

/**
* Experiment 49 — Can the Production Update Path Produce a Real Shared Anchor?
*
* This test asks: does any sequence of real production updates create two or more
* active unknowns that reference the same populated relationship anchor (parentId,
* dependsOn, or affects pointing to the same node)?
*
* No production code changes. No fixture modification. Only test files and diagnostics.
*/
import { describe, it, expect } from "vitest";
import {
buildInitialGraph,
} from "@/lib/graph/builder.js";
import {
applyValidatedProposal,
} from "@/lib/graph/apply-proposal.js";
import {
situationNodeSchema,
situationEdgeSchema,
situationGraphSchema,
makeNodeId,
makeNode,
makeEdge,
makeGraph,
} from "@/lib/graph/schema.js";
/* ═══════════════════════════════════════════════════════════
* Test-only diagnostic — shared-anchor detection (from Exp 47)
* Uses only existing fields — no text matching.
* ═══════════════════════════════════════════════════════════ */
function inspectSharedUnknownAnchor({ graph }) {
const nodes = Array.isArray(graph.nodes) ? [...graph.nodes] : [];
const edges = Array.isArray(graph.edges) ? [...graph.edges] : [];
const activeIds = new Set();
const resolvedSet = new Set(graph.resolvedNodeIds || []);
for (const n of nodes) {
if (!n || n.kind !== "unknown") continue;
if (resolvedSet.has(n.id) || n.status === "resolved") continue;
activeIds.add(n.id);
}
const activeArr = [...activeIds];
if (activeArr.length < 2) {
return { result: "insufficient_data", anchorIds: [], reason: "fewer than two active unknowns" };
}
const referrerMap = new Map();
for (const n of nodes) {
if (!activeIds.has(n.id)) continue;
const refs = new Set();
if (Array.isArray(n.dependsOn)) n.dependsOn.forEach((id) => refs.add(id));
if (Array.isArray(n.affects)) n.affects.forEach((id) => refs.add(id));
if (n.parentId) refs.add(n.parentId);
referrerMap.set(n.id, refs);
}
const edgeAnchors = new Set();
for (const e of edges) {
if (!e || !e.fromNodeId || !e.toNodeId) continue;
if (activeIds.has(e.toNodeId)) {
edgeAnchors.add(e.fromNodeId);
}
}
for (const key of referrerMap.keys()) {
edgeAnchors.forEach((a) => referrerMap.get(key).add(a));
}
let common = new Set([...referrerMap.get(activeArr[0]) || []]);
for (let i = 1; i < activeArr.length; i++) {
const next = referrerMap.get(activeArr[i]) || new Set();
common = new Set([...common].filter((x) => next.has(x)));
}
const existingIds = new Set(nodes.map((n) => n.id));
const validCommon = [...common].filter((id) => existingIds.has(id));
if (validCommon.length === 1) {
return { result: "shared_anchor", anchorIds: validCommon, reason: "All active unknowns reference one common node: " + validCommon[0] };
}
const allRefs = new Set();
for (const id of activeArr) {
const refs = referrerMap.get(id) || new Set();
refs.forEach((r) => allRefs.add(r));
}
const validAnchors = [...allRefs].filter((id) => existingIds.has(id));
if (validAnchors.length > 0) {
return { result: "separate_anchors", anchorIds: validAnchors, reason: "Active unknowns reference " + validAnchors.length + " distinct nodes with no shared intersection" };
}
return { result: "insufficient_data", anchorIds: [], reason: "no relationship fields populated on any active unknown" };
}
/* ═══════════════════════════════════════════════════════════
* Helper: wrap raw {nodes, edges} into a SituationGraph
* ═══════════════════════════════════════════════════════════ */
function wrapGraph({ nodes, edges }) {
const summaryNode = nodes.find((n) => n.kind === "state");
const unknownNodes = nodes.filter((n) => n.kind === "unknown" && n.status !== "resolved");
return situationGraphSchema.parse({
centralStatement: summaryNode?.description || "Test",
currentSummary: summaryNode?.label || "Test",
activeUnknownNodeId: unknownNodes.length > 0 ? unknownNodes[0].id : null,
resolvedNodeIds: [],
nodes,
edges: edges || [],
});
}
/* ═══════════════════════════════════════════════════════════
* Control fixture — shared-anchor graph (NOT from production)
* Two active unknowns both reference the same relationship node.
* Edge structure: edges FROM unknowns TO relationship anchor.
* Node fields: parentId set on both unknowns.
* ═══════════════════════════════════════════════════════════ */
function buildControlSharedAnchorGraph() {
const relationshipNode = makeNode({
id: "n-relationship-anchor",
label: "Revenue-cash comparison status",
kind: "relationship",
status: "supported",
confidence: "high",
});
const unknown1 = makeNode({
id: "n-unk-explanation-1",
label: "Explanation for revenue increase",
description: "Need to know what changed, because that is needed to understand the divergence.",
kind: "unknown",
status: "unknown",
confidence: "medium",
parentId: relationshipNode.id,
dependsOn: [],
});
const unknown2 = makeNode({
id: "n-unk-explanation-2",
label: "Explanation for cash decrease",
description: "Need to know what changed, because that is needed to understand the divergence.",
kind: "unknown",
status: "unknown",
confidence: "medium",
parentId: relationshipNode.id,
dependsOn: [],
});
const obs1 = makeNode({
id: "n-obs-rev",
label: "Revenue up 18%.",
kind: "observation",
status: "supported",
confidence: "high",
});
const obs2 = makeNode({
id: "n-obs-cash",
label: "Cash down 5%.",
kind: "observation",
status: "supported",
confidence: "high",
});
return makeGraph({
centralStatement: "Revenue and cash diverged.",
nodes: [relationshipNode, unknown1, unknown2, obs1, obs2],
edges: [
makeEdge({ id: "e-obs1-r", fromNodeId: obs1.id, toNodeId: relationshipNode.id, relationship: "supports" }),
makeEdge({ id: "e-obs2-r", fromNodeId: obs2.id, toNodeId: relationshipNode.id, relationship: "supports" }),
makeEdge({ id: "e-unk1-r", fromNodeId: unknown1.id, toNodeId: relationshipNode.id, relationship: "depends_on" }),
makeEdge({ id: "e-unk2-r", fromNodeId: unknown2.id, toNodeId: relationshipNode.id, relationship: "depends_on" }),
],
activeUnknownNodeId: unknown1.id,
resolvedNodeIds: [],
currentSummary: "Two active unknowns sharing the same anchor.",
});
}
/* ═══════════════════════════════════════════════════════════
* Control fixture — decomposition children with shared parent.
* Uses incoming edges from a relationship node so that the diagnostic's
* edgeAnchors logic picks up the shared reference.
* ═══════════════════════════════════════════════════════════ */
function buildControlDecompositionGraph() {
const parentNode = makeNode({
id: "n-parent-explanation",
label: "Explanation for why revenue and cash diverged",
kind: "relationship",
status: "supported",
confidence: "medium",
});
const child1 = makeNode({
id: "n-child-whether different timing",
label: "Whether the two observations reflect different timing",
description: "Need to know whether the two observations reflect different timing, because that could help explain the divergence.",
kind: "unknown",
status: "unknown",
confidence: "medium",
parentId: parentNode.id,
});
const child2 = makeNode({
id: "n-child-how measured",
label: "How the two observations were measured",
description: "Need evidence about the measure used for each observation, because that could help explain the divergence.",
kind: "unknown",
status: "unknown",
confidence: "medium",
parentId: parentNode.id,
});
const obs1 = makeNode({
id: "n-obs-rev-d",
label: "Revenue up 18%.",
kind: "observation",
status: "supported",
confidence: "high",
});
const obs2 = makeNode({
id: "n-obs-cash-d",
label: "Cash down 5%.",
kind: "observation",
status: "supported",
confidence: "high",
});
return makeGraph({
centralStatement: "Revenue and cash diverged.",
nodes: [parentNode, child1, child2, obs1, obs2],
edges: [
makeEdge({ id: "e-obs1-rd", fromNodeId: obs1.id, toNodeId: parentNode.id, relationship: "supports" }),
makeEdge({ id: "e-obs2-rd", fromNodeId: obs2.id, toNodeId: parentNode.id, relationship: "supports" }),
makeEdge({ id: "e-c1-p", fromNodeId: child1.id, toNodeId: parentNode.id, relationship: "depends_on" }),
makeEdge({ id: "e-c2-p", fromNodeId: child2.id, toNodeId: parentNode.id, relationship: "depends_on" }),
],
activeUnknownNodeId: child1.id,
resolvedNodeIds: [],
currentSummary: "Decomposition children with shared parent.",
});
}
/* ═══════════════════════════════════════════════════════════
* Case A — Two emergent unknowns from one investigation.
* Start with a graph containing comparable observations and an
* existing unknown about comparability. First update resolves the
* unknown → triggers emergent reasoning path, creating Unknown A.
* Second update answers a question from Unknown A in a way that
* produces new observation-level changes → potentially creates
* another emergent reasoning path. Inspect whether both active
* unknowns reference the same anchor.
* ═══════════════════════════════════════════════════════════ */
describe("Case A — Two emergent unknowns from one investigation", () => {
let graphBeforeSecondUpdate, diagAfterFirst, diagAfterSecond;
let activeUnknownCountAfterFirst = 0;
let result1, result2;
beforeAll(() => {
/* Step 1: build a valid starting graph with two comparable observations */
const nodeObs1 = makeNode({
id: "n-obs-revenue",
label: "Revenue increased by 18%.",
kind: "observation",
status: "supported",
confidence: "high",
});
const nodeObs2 = makeNode({
id: "n-obs-cash",
label: "Cash in the bank decreased over the same period.",
kind: "observation",
status: "supported",
confidence: "high",
});
const comparisonUnknown = makeNode({
id: "n-unk-comparison",
label: "Whether the figures are comparable",
description: "Need to know whether the figures use the same period, basis, and scale.",
kind: "unknown",
status: "unknown",
confidence: "high",
});
const graph = makeGraph({
centralStatement: "Revenue and cash comparison needed for decision.",
nodes: [nodeObs1, nodeObs2, comparisonUnknown],
edges: [
makeEdge({ id: "e-obs1-to-unk", fromNodeId: nodeObs1.id, toNodeId: comparisonUnknown.id, relationship: "supports" }),
makeEdge({ id: "e-obs2-to-unk", fromNodeId: nodeObs2.id, toNodeId: comparisonUnknown.id, relationship: "supports" }),
],
activeUnknownNodeId: comparisonUnknown.id,
resolvedNodeIds: [],
currentSummary: "Two supported observations with one unresolved unknown.",
});
/* Step 2: apply an update that resolves the existing unknown */
const proposal1 = {
addedNodes: [],
updatedNodes: [
{
nodeId: comparisonUnknown.id,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Both figures cover the same accounting period and are taken from the same management accounts.",
reason: "Confirmed comparable by the user.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [comparisonUnknown.id],
affectedNodeIds: [],
selectedQuestion: null,
};
result1 = applyValidatedProposal({
situationGraph: graph,
proposal: proposal1,
previousQuestion: "Are the two figures comparable?",
answer: "Yes, both cover the same accounting period and are from the same management accounts.",
});
diagAfterFirst = inspectSharedUnknownAnchor({ graph: result1.updatedSituationGraph });
graphBeforeSecondUpdate = result1.updatedSituationGraph;
// Capture active unknown count after step 2
activeUnknownCountAfterFirst = graphBeforeSecondUpdate.nodes.filter(
(n) => n.kind === "unknown" && !graphBeforeSecondUpdate.resolvedNodeIds.includes(n.id)
).length;
/* Step 3: fire a SECOND update on the updated graph to trigger another emergent path */
const activeAfterFirst = graphBeforeSecondUpdate.nodes.filter(
(n) => n.kind === "unknown" && !graphBeforeSecondUpdate.resolvedNodeIds.includes(n.id)
);
if (activeAfterFirst.length >= 1) {
const nextUnknown = activeAfterFirst[0];
const proposal2 = {
addedNodes: [],
updatedNodes: [
{
nodeId: nextUnknown.id,
previousStatus: nextUnknown.status || "unknown",
newStatus: "resolved",
previousValue: nextUnknown.value ?? null,
newValue: "Answer provided by the user.",
reason: "User resolved this unknown.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [nextUnknown.id],
affectedNodeIds: [],
selectedQuestion: null,
};
result2 = applyValidatedProposal({
situationGraph: graphBeforeSecondUpdate,
proposal: proposal2,
previousQuestion: `Investigate "${nextUnknown.label}"`,
answer: "Answer provided.",
});
diagAfterSecond = inspectSharedUnknownAnchor({ graph: result2.updatedSituationGraph });
} else {
// No active unknowns after step 1 — can't fire a second update.
result2 = null;
diagAfterSecond = { result: "insufficient_data", anchorIds: [], reason: "no active unknowns remained after first update" };
}
});
it("step 1 produces a valid updated graph with diagnostic output", () => {
expect(["shared_anchor", "separate_anchors", "insufficient_data"]).toContain(diagAfterFirst.result);
});
it("captures how many active unknowns remain after each update", () => {
const remaining = graphBeforeSecondUpdate.nodes.filter(
(n) => n.kind === "unknown" && !graphBeforeSecondUpdate.resolvedNodeIds.includes(n.id)
);
expect(Array.isArray(remaining)).toBe(true);
});
it("step 2 diagnostic is captured", () => {
expect(["shared_anchor", "separate_anchors", "insufficient_data"]).toContain(diagAfterSecond.result);
});
it("records the relationship fields on all active unknowns after both updates", () => {
const allActiveUnknowns = [];
// After first update
const afterFirst = graphBeforeSecondUpdate.nodes.filter(
(n) => n.kind === "unknown" && !graphBeforeSecondUpdate.resolvedNodeIds.includes(n.id)
);
for (const u of afterFirst) {
allActiveUnknowns.push({
turn: 1,
id: u.id,
dependsOn: [...(u.dependsOn || [])],
affects: [...(u.affects || [])],
parentId: u.parentId,
childIds: [...(u.childIds || [])],
});
}
// After second update
if (result2?.updatedSituationGraph) {
const afterSecond = result2.updatedSituationGraph.nodes.filter(
(n) => n.kind === "unknown" && !result2.updatedSituationGraph.resolvedNodeIds.includes(n.id)
);
for (const u of afterSecond) {
allActiveUnknowns.push({
turn: 2,
id: u.id,
dependsOn: [...(u.dependsOn || [])],
affects: [...(u.affects || [])],
parentId: u.parentId,
childIds: [...(u.childIds || [])],
});
}
}
// Assert the data was captured — at minimum the arrays are well-formed
for (const rec of allActiveUnknowns) {
expect(Array.isArray(rec.dependsOn)).toBe(true);
expect(Array.isArray(rec.affects)).toBe(true);
expect(["string", "object"]).toContain(typeof rec.parentId);
expect(Array.isArray(rec.childIds)).toBe(true);
}
});
it("shared-anchor test: can two active unknowns share an anchor via production updates?", () => {
// This is the core question of Exp 49.
// shared_anchor = yes, they DO share an anchor; separate_anchors or insufficient_data = no.
const finalResult = diagAfterSecond.result;
if (finalResult === "shared_anchor") {
expect(diagAfterSecond.anchorIds.length).toBeGreaterThan(0);
console.log("\n SHARED ANCHOR FOUND:", JSON.stringify(diagAfterSecond));
} else {
console.log("\n No shared anchor via this production sequence:", diagAfterSecond.result, "—", diagAfterSecond.reason);
}
// We record the result — do NOT assert a specific outcome because Exp 49 is exploratory.
expect(["shared_anchor", "separate_anchors", "insufficient_data"]).toContain(finalResult);
});
it("diagnostic captures first update result clearly", () => {
console.log("\n Case A — Diagnostic after first update:", diagAfterFirst.result, "—", diagAfterFirst.reason);
expect(["shared_anchor", "separate_anchors", "insufficient_data"]).toContain(diagAfterFirst.result);
});
it("active unknown count documents the production path outcome", () => {
console.log("\n Case A — Active unknowns after first update:", activeUnknownCountAfterFirst);
expect(typeof activeUnknownCountAfterFirst).toBe("number");
});
});
/* ═══════════════════════════════════════════════════════════
* Case B — Start from buildInitialGraph (which leaves empty fields),
* then apply updates that fire the emergent reasoning path twice.
* The goal: see whether two separate emergent-unknown creations can
* each point to the SAME relationship node anchor.
* ═══════════════════════════════════════════════════════════ */
describe("Case B — Emergent unknowns from buildInitialGraph baseline", () => {
let startingGraph, graphAfterFirstUpdate, graphAfterSecondUpdate;
let diagAfterFirstB, diagAfterSecondB;
let activeUnknownCountAfterFirstB = 0;
let activeUnknownCountAfterSecondB = 0;
beforeAll(() => {
/* Build initial graph with comparable observations */
const nodeObs1 = makeNode({
id: "n-x-obs",
label: "Revenue up 18%.",
kind: "observation",
status: "supported",
confidence: "high",
});
const nodeObs2 = makeNode({
id: "n-y-obs",
label: "Cash down 5%.",
kind: "observation",
status: "supported",
confidence: "high",
});
const comparisonUnknown = makeNode({
id: "n-x-comparison",
label: "Is the comparison valid?",
description: "Need to know whether the figures use the same period, basis, and scale.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const graph = makeGraph({
centralStatement: "Test scenario with comparable observations.",
nodes: [nodeObs1, nodeObs2, comparisonUnknown],
edges: [
makeEdge({ id: "e-a-1", fromNodeId: nodeObs1.id, toNodeId: comparisonUnknown.id, relationship: "supports" }),
makeEdge({ id: "e-a-2", fromNodeId: nodeObs2.id, toNodeId: comparisonUnknown.id, relationship: "supports" }),
],
activeUnknownNodeId: comparisonUnknown.id,
resolvedNodeIds: [],
currentSummary: "Test.",
});
startingGraph = graph;
/* First update: resolve comparison unknown */
const proposal1B = {
addedNodes: [],
updatedNodes: [
{
nodeId: comparisonUnknown.id,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Confirmed.",
reason: "User confirmed comparability.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [comparisonUnknown.id],
affectedNodeIds: [],
selectedQuestion: null,
};
const result1B = applyValidatedProposal({
situationGraph: graph,
proposal: proposal1B,
previousQuestion: "Are the figures comparable?",
answer: "Yes.",
});
graphAfterFirstUpdate = result1B.updatedSituationGraph;
diagAfterFirstB = inspectSharedUnknownAnchor({ graph: graphAfterFirstUpdate });
activeUnknownCountAfterFirstB = graphAfterFirstUpdate.nodes.filter(
(n) => n.kind === "unknown" && !graphAfterFirstUpdate.resolvedNodeIds.includes(n.id)
).length;
/* Second update: resolve next unknown if one exists */
const activeAfterFirst = graphAfterFirstUpdate.nodes.filter(
(n) => n.kind === "unknown" && !graphAfterFirstUpdate.resolvedNodeIds.includes(n.id)
);
if (activeAfterFirst.length >= 1) {
const nextUnknown = activeAfterFirst[0];
const proposal2B = {
addedNodes: [],
updatedNodes: [
{
nodeId: nextUnknown.id,
previousStatus: nextUnknown.status || "unknown",
newStatus: "resolved",
previousValue: nextUnknown.value ?? null,
newValue: "Answered.",
reason: "User resolved this unknown.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [nextUnknown.id],
affectedNodeIds: [],
selectedQuestion: null,
};
const result2B = applyValidatedProposal({
situationGraph: graphAfterFirstUpdate,
proposal: proposal2B,
previousQuestion: `Investigate "${nextUnknown.label}"`,
answer: "Answer provided.",
});
graphAfterSecondUpdate = result2B.updatedSituationGraph;
diagAfterSecondB = inspectSharedUnknownAnchor({ graph: graphAfterSecondUpdate });
activeUnknownCountAfterSecondB = graphAfterSecondUpdate.nodes.filter(
(n) => n.kind === "unknown" && !graphAfterSecondUpdate.resolvedNodeIds.includes(n.id)
).length;
} else {
graphAfterSecondUpdate = null;
diagAfterSecondB = { result: "insufficient_data", anchorIds: [], reason: "no active unknowns after first update" };
}
});
it("first update produces a valid graph with diagnostic output", () => {
expect(["shared_anchor", "separate_anchors", "insufficient_data"]).toContain(diagAfterFirstB.result);
});
it("captures active unknown count after first update", () => {
expect(typeof activeUnknownCountAfterFirstB).toBe("number");
});
it("second update diagnostic is captured", () => {
expect(["shared_anchor", "separate_anchors", "insufficient_data"]).toContain(diagAfterSecondB.result);
});
it("captures active unknown count after second update", () => {
expect(typeof activeUnknownCountAfterSecondB).toBe("number");
});
it("logs relationship fields for each remaining active unknown after both updates", () => {
const allActive = [];
if (graphAfterFirstUpdate) {
for (const n of graphAfterFirstUpdate.nodes.filter(
(n) => n.kind === "unknown" && !graphAfterFirstUpdate.resolvedNodeIds.includes(n.id)
)) {
allActive.push({ turn: 1, id: n.id, parentId: n.parentId, dependsOn: [...(n.dependsOn || [])] });
}
}
if (graphAfterSecondUpdate) {
for (const n of graphAfterSecondUpdate.nodes.filter(
(n) => n.kind === "unknown" && !graphAfterSecondUpdate.resolvedNodeIds.includes(n.id)
)) {
allActive.push({ turn: 2, id: n.id, parentId: n.parentId, dependsOn: [...(n.dependsOn || [])] });
}
}
expect(Array.isArray(allActive)).toBe(true);
});
it("shared-anchor test: does this sequence produce a shared anchor?", () => {
const finalResult = diagAfterSecondB.result;
if (finalResult === "shared_anchor") {
expect(diagAfterSecondB.anchorIds.length).toBeGreaterThan(0);
console.log("\n SHARED ANCHOR FOUND in Case B:", JSON.stringify(diagAfterSecondB));
} else {
console.log("\n No shared anchor via this sequence (Case B):", diagAfterSecondB.result, "—", diagAfterSecondB.reason);
}
expect(["shared_anchor", "separate_anchors", "insufficient_data"]).toContain(finalResult);
});
it("diagnostic captures first update result clearly", () => {
console.log("\n Case B — Diagnostic after first update:", diagAfterFirstB.result, "—", diagAfterFirstB.reason);
expect(["shared_anchor", "separate_anchors", "insufficient_data"]).toContain(diagAfterFirstB.result);
});
it("active unknown count documents the production path outcome (first)", () => {
console.log("\n Case B — Active unknowns after first update:", activeUnknownCountAfterFirstB);
expect(typeof activeUnknownCountAfterFirstB).toBe("number");
});
it("active unknown count documents the production path outcome (second)", () => {
console.log("\n Case B — Active unknowns after second update:", activeUnknownCountAfterSecondB);
expect(typeof activeUnknownCountAfterSecondB).toBe("number");
});
});
/* ═══════════════════════════════════════════════════════════
* Case C — Direct construction of a shared-anchor graph and
* verification that the diagnostic reads it correctly.
* This is a control to confirm the diagnostic logic itself works.
* ═══════════════════════════════════════════════════════════ */
describe("Case C — Diagnostic validation with controlled shared-anchor fixture", () => {
let graphWithSharedAnchor, diag;
beforeAll(() => {
graphWithSharedAnchor = buildControlSharedAnchorGraph();
diag = inspectSharedUnknownAnchor({ graph: graphWithSharedAnchor });
});
it("diagnostic returns shared_anchor for controlled fixture", () => {
expect(diag.result).toBe("shared_anchor");
});
it("shared-anchor diagnostic identifies the correct anchor ID", () => {
expect(diag.anchorIds).toContain("n-relationship-anchor");
});
it("diagnostic reason includes anchor ID", () => {
expect(diag.reason).toContain("n-relationship-anchor");
});
it("both unknown nodes are active (not resolved)", () => {
const known = graphWithSharedAnchor.resolvedNodeIds || [];
for (const n of graphWithSharedAnchor.nodes) {
if (n.kind !== "unknown") continue;
expect(known).not.toContain(n.id);
}
});
it("both unknowns have parentId pointing to the same anchor", () => {
const unknownNodes = graphWithSharedAnchor.nodes.filter((n) => n.kind === "unknown" && n.status !== "resolved");
expect(unknownNodes.length).toBeGreaterThanOrEqual(2);
for (const u of unknownNodes) {
expect(u.parentId).toBe("n-relationship-anchor");
}
});
it("diagnostic correctly rejects a shared-anchor fixture missing parentId", () => {
const unkNoParent = makeNode({
id: "n-unk-no-parent",
label: "Explanation test",
description: "Need to know.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const unkNoParent2 = makeNode({
id: "n-unk-no-parent-2",
label: "Explanation test two",
description: "Need to know.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const graphNoParent = makeGraph({
centralStatement: "Test no parent.",
nodes: [unkNoParent, unkNoParent2],
edges: [],
activeUnknownNodeId: unkNoParent.id,
resolvedNodeIds: [],
currentSummary: "Test.",
});
const diagNoParent = inspectSharedUnknownAnchor({ graph: graphNoParent });
expect(diagNoParent.result).not.toBe("shared_anchor");
});
});
/* ═══════════════════════════════════════════════════════════
* Case D — Test decomposition children sharing a parent anchor.
* Decomposition children get parentId from buildCompositeUnknownChildren.
* We test whether two decomposition children from the same parent
* share that parent as an anchor.
* ═══════════════════════════════════════════════════════════ */
describe("Case D — Decomposition children sharing a parent anchor", () => {
let graphWithDecomposition, diag;
beforeAll(() => {
graphWithDecomposition = buildControlDecompositionGraph();
diag = inspectSharedUnknownAnchor({ graph: graphWithDecomposition });
// Debug output
const activeUnknowns = graphWithDecomposition.nodes.filter(
(n) => n.kind === "unknown" && !graphWithDecomposition.resolvedNodeIds.includes(n.id)
);
console.log("\n Case D — Active unknown count:", activeUnknowns.length);
for (const u of activeUnknowns) {
console.log(` Unknown ${u.id}: parentId=${u.parentId}, dependsOn=${JSON.stringify(u.dependsOn)}, affects=${JSON.stringify(u.affects)}`);
}
console.log(" Case D — Diagnostic result:", diag.result, "—", diag.reason);
});
it("diagnostic returns shared_anchor for decomposition children fixture", () => {
expect(diag.result).toBe("shared_anchor");
});
it("both decomposition children share the same parent as anchor", () => {
expect(diag.anchorIds).toContain("n-parent-explanation");
});
it("diagnostic identifies the shared anchor reason correctly", () => {
expect(diag.result).toBe("shared_anchor");
});
});
/* ═══════════════════════════════════════════════════════════
* Case E — Apply a single update to one unknown and verify the
* other unknown's relationship fields are NOT mutated (immunity).
* ═══════════════════════════════════════════════════════════ */
describe("Case E — Immunity: updating one node does not mutate unrelated nodes", () => {
let graphBeforeUpdate, snapshotBefore, snapshotAfter, diagImmunity;
beforeAll(() => {
const parentNode = makeNode({
id: "n-parent-e",
label: "Explanation parent",
kind: "relationship",
status: "supported",
confidence: "medium",
});
const child1 = makeNode({
id: "n-child-e-1",
label: "Child one",
description: "Need to know X.",
kind: "unknown",
status: "unknown",
confidence: "medium",
parentId: parentNode.id,
});
const child2 = makeNode({
id: "n-child-e-2",
label: "Child two",
description: "Need to know Y.",
kind: "unknown",
status: "unknown",
confidence: "medium",
parentId: parentNode.id,
});
const obs1 = makeNode({
id: "n-obs-e-1",
label: "Signal A changed.",
kind: "observation",
status: "supported",
confidence: "high",
});
graphBeforeUpdate = makeGraph({
centralStatement: "Test immunity.",
nodes: [parentNode, child1, child2, obs1],
edges: [
makeEdge({ id: "e-obs-p-e", fromNodeId: obs1.id, toNodeId: parentNode.id, relationship: "supports" }),
makeEdge({ id: "e-c1-p-e", fromNodeId: child1.id, toNodeId: parentNode.id, relationship: "depends_on" }),
makeEdge({ id: "e-c2-p-e", fromNodeId: child2.id, toNodeId: parentNode.id, relationship: "depends_on" }),
],
activeUnknownNodeId: child1.id,
resolvedNodeIds: [],
currentSummary: "Test.",
});
/* Snapshot before update */
const child1Before = graphBeforeUpdate.nodes.find((n) => n.id === "n-child-e-1");
const child2Before = graphBeforeUpdate.nodes.find((n) => n.id === "n-child-e-2");
snapshotBefore = {
child1ParentId: child1Before.parentId,
child1DependsOn: [...(child1Before.dependsOn || [])],
child2ParentId: child2Before.parentId,
child2DependsOn: [...(child2Before.dependsOn || [])],
resolvedCount: graphBeforeUpdate.resolvedNodeIds.length,
};
/* Apply update resolving child1 */
const proposalE = {
addedNodes: [],
updatedNodes: [
{
nodeId: "n-child-e-1",
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Answered.",
reason: "User resolved child one.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: ["n-child-e-1"],
affectedNodeIds: [],
selectedQuestion: null,
};
const resultE = applyValidatedProposal({
situationGraph: graphBeforeUpdate,
proposal: proposalE,
previousQuestion: "Investigate child one.",
answer: "Answered.",
});
/* Snapshot after */
const child2After = resultE.updatedSituationGraph.nodes.find((n) => n.id === "n-child-e-2");
snapshotAfter = {
child2ParentId: child2After.parentId,
child2DependsOn: [...(child2After.dependsOn || [])],
resolvedCount: resultE.updatedSituationGraph.resolvedNodeIds.length,
};
diagImmunity = inspectSharedUnknownAnchor({ graph: graphBeforeUpdate });
});
it("child2 parentId is unchanged after resolving child1", () => {
expect(snapshotBefore.child2ParentId).toBe(snapshotAfter.child2ParentId);
});
it("child2 dependsOn is unchanged after resolving child1", () => {
expect(JSON.stringify(snapshotBefore.child2DependsOn)).toBe(JSON.stringify(snapshotAfter.child2DependsOn));
});
it("resolvedNodeIds grew by exactly one", () => {
expect(snapshotAfter.resolvedCount).toBe(snapshotBefore.resolvedCount + 1);
});
it("diagnostic sees both children share the same anchor before update (as expected)", () => {
// Before any update, child1 and child2 are both active and share parentId → shared_anchor
expect(diagImmunity.result).toBe("shared_anchor");
});
it("after resolving child1, child2 retains its anchor (immunity verified)", () => {
// The real immunity test: child2's parentId and dependsOn are unchanged after resolving child1
expect(snapshotBefore.child2ParentId).toBe(snapshotAfter.child2ParentId);
expect(JSON.stringify(snapshotBefore.child2DependsOn)).toBe(JSON.stringify(snapshotAfter.child2DependsOn));
});
it("diagnostic captures shared-anchor state on pre-update graph", () => {
console.log("\n Case E — Immunity diagnostic result:", diagImmunity.result, "—", diagImmunity.reason);
expect(diagImmunity.result).toBe("shared_anchor");
});
});
/* ═══════════════════════════════════════════════════════════
* Case F — Schema compliance: all produced unknown nodes must
* pass the situationNodeSchema in the shared-anchor result graph.
* ═══════════════════════════════════════════════════════════ */
describe("Case F — Schema compliance on shared-anchor fixture", () => {
it("all nodes in Case C shared-anchor fixture pass schema validation", () => {
const gc = buildControlSharedAnchorGraph();
for (const n of gc.nodes) {
const result = situationNodeSchema.safeParse(n);
expect(result.success).toBe(true);
}
});
it("all edges in Case C shared-anchor fixture pass edge schema validation", () => {
const gc = buildControlSharedAnchorGraph();
for (const e of gc.edges) {
const result = situationEdgeSchema.safeParse(e);
expect(result.success).toBe(true);
}
});
it("the entire graph with shared-anchor passes the SituationGraph schema", () => {
const gc = buildControlSharedAnchorGraph();
const result = situationGraphSchema.safeParse(gc);
expect(result.success).toBe(true);
});
it("all nodes in Case D decomposition fixture pass schema validation", () => {
const gd = buildControlDecompositionGraph();
for (const n of gd.nodes) {
const result = situationNodeSchema.safeParse(n);
expect(result.success).toBe(true);
}
});
it("the entire graph with decomposition children passes the SituationGraph schema", () => {
const gd = buildControlDecompositionGraph();
const result = situationGraphSchema.safeParse(gd);
expect(result.success).toBe(true);
});
});