Feature/product platform foundation v0.62 #1

Merged
robbond merged 683 commits from feature/product-platform-foundation-v0.62 into feature/emergent-unknowns-v0.5 2026-09-09 07:58:20 +01:00
3 changed files with 1016 additions and 3 deletions
Showing only changes of commit 6f00a5e567 - Show all commits
+5 -3
View File
@@ -108,10 +108,12 @@ Answer before continuing:
---
*Created by Experiment 34. Updated by Experiments 3848. Branch: `feature/user-workspace-ux-v0.7`.*
*Created by Experiment 34. Updated by Experiments 3849. Branch: `feature/user-workspace-ux-v0.7`.*
### Return-to-Work Note (Experiment 47)
Experiment 47 created a test-only diagnostic helper (`inspectSharedUnknownAnchor`) that inspects existing relationship fields (dependsOn, affects, parentId, childIds on nodes; fromNodeId/toNodeId + relationship on edges) to distinguish shared-anchor investigations from scattered ones. Three controlled fixtures (shared/none/separate anchors, all with identical structural counts of 6 nodes and 4 active unknowns) confirmed the helper correctly distinguishes all three patterns. Inspecting three real scenarios from Experiments 39-46 returned insufficient_data for all — existing data lacks populated relationship fields on unknown nodes. The assessor remains unchanged (produces identical too_broad output across all fixtures). Branch: `feature/user-workspace-ux-v0.7`. First file to inspect: `tests/investigation-state-assessor.shared-anchor.test.js` for results, then `docs/design-evolution-log.md` Experiment 47 section for full analysis.
Experiment 47 created a test-only diagnostic helper (`inspectSharedUnknownAnchor`) that inspects existing relationship fields (dependsOn, affects, parentId, childIds on nodes; fromNodeId/toNodeId + relationship on edges) to distinguish shared-anchor investigations from scattered ones. Three controlled fixtures (shared/none/separate anchors, all with identical structural counts of 6 nodes and 4 active unknowns) confirmed the helper correctly distinguishes all three patterns. Inspecting three real scenarios from Experiments 39-46 returned insufficient_data for all — existing data lacks populated relationship fields on unknown nodes. The assessor remains unchanged (produces identical too_broad output across all fixtures). Status closed.
Experiment 48 passively audited whether real graph updates populate usable unknown relationships (the signal needed for the Exp-47 diagnostic). Three production paths inspected: (1) `buildInitialGraph` — does NOT populate dependsOn/affects/parentId, only edges exist; (2) emergent reasoning via `buildEmergentReasoningUnknown` — DOES populate dependsOn and parentId with proper values; (3) decomposition children via `buildCompositeUnknownChildren` — DOES populate parentId. One test file created (`tests/graph/unknown-relationship-population.test.js`, 16 tests, all pass). Conclusion: **Insufficient Data** — shared-anchor coherence is structurally supportable through Path 2 only, requiring at least two active unknowns with shared references in dependsOn/affects arrays from emergent reasoning. The gap is not schema-level but triggering logic (initial build creates empty fields; emergent path populates correctly). Branch: `feature/user-workspace-ux-v0.7`. First file to inspect: `tests/graph/unknown-relationship-population.test.js` for detailed audit, then `docs/design-evolution-log.md` Experiment 48 section.
Experiment 48 passively audited whether real graph updates populate usable unknown relationships (the signal needed for the Exp-47 diagnostic). Three production paths inspected: (1) `buildInitialGraph` — does NOT populate dependsOn/affects/parentId, only edges exist; (2) emergent reasoning via `buildEmergentReasoningUnknown` — DOES populate dependsOn and parentId with proper values; (3) decomposition children via `buildCompositeUnknownChildren` — DOES populate parentId. One test file created (`tests/graph/unknown-relationship-population.test.js`, 16 tests, all pass). Conclusion: **Insufficient Data** — shared-anchor coherence is structurally supportable through Path 2 only, requiring at least two active unknowns with shared references in dependsOn/affects arrays from emergent reasoning. The gap is not schema-level but triggering logic (initial build creates empty fields; emergent path populates correctly). Status closed.
Experiment 49 tested whether production update sequences can produce a real shared anchor (two or more active unknowns sharing the same populated relationship node). Two sequential-update scenarios via `applyValidatedProposal` (Cases A and B in the new test file) consistently returned `separate_anchors` or `insufficient_data` — no coexisting active unknowns reference the same anchor. The structural capability exists (fields populate correctly via emergent reasoning), but the triggering logic never produces shared anchors within tested flows. Control cases (CF, 20 tests) confirmed the diagnostic works correctly on controlled fixtures and all produced nodes pass schema validation. Total: 36 new tests, all passing. Branch: `feature/user-workspace-ux-v0.7`. First file to inspect: `tests/graph/shared-anchor-production-path.test.js` for results, then `docs/design-evolution-log.md` Experiment 49 section.
+56
View File
@@ -3218,3 +3218,59 @@ Additionally, `applyGraphUpdate()` auto-creates/updates `dependsOn` and `childId
### Pending Rob's review. No production code or graph schema modified.
**Commit:** pending (experiment: audit unknown relationship population)
## Experiment 49 — Test Production Shared-Anchor Pattern (2026-08-07)
Experiment 49 asked whether any sequence of real production updates creates two or more active unknowns that reference the same populated relationship anchor. No production code changed. Only a new test file and diagnostic.
### Approach
Three production-path scenarios tested via `applyValidatedProposal`:
- **Case A**: Start with comparable observations + existing unknown → resolve it (triggers emergent reasoning) → then resolve the next active unknown → inspect for shared anchor between remaining unknowns.
- **Case B**: Identical approach from a separate fixture baseline.
- **Cases CF**: Diagnostic controls — verified shared-anchor detection works on controlled fixtures, schema compliance holds, decomposition children share parent anchor correctly, and resolving one node doesn't mutate another's fields (immunity).
### Results
**All 36 tests pass.** The production-path cases (A & B) consistently returned `separate_anchors` or `insufficient_data`, not `shared_anchor`. Key observations:
- After first update in both Cases A and B: only one active unknown typically remains — the diagnostic correctly returns `insufficient_data` (< 2 active).
- When two active unknowns do exist after emergent reasoning, they reference *different* anchor nodes (separate anchors), not the same one.
- The diagnostic correctly identifies shared anchors on controlled fixtures (Cases C & D pass as expected).
- Schema compliance: all production-created nodes and edges pass `situationNodeSchema`/`situationEdgeSchema` validation.
### Why No Shared Anchor Emerges
The production flow creates at most one emergent reasoning unknown per update, via `buildEmergentReasoningUnknown`. For two unknowns to share an anchor, they would need to independently reference the same relationship node — but each call generates a unique ID and references different source nodes. The path exists (via parentId/populated dependsOn) but the *triggering logic* in `applyValidatedProposal` never produces coexisting active unknowns that point to the same anchor in any tested scenario.
### Answering the Seven Questions
1. **Can two active unknowns share an anchor via production updates?** No — not in any tested sequence. Each emergent reasoning creates a new unique node with distinct references.
2. **Does the diagnostic distinguish shared vs scattered patterns when both exist?** Yes (Cases C, D confirm). It returns `shared_anchor` for identical parentId/dependsOn intersections and `separate_anchors` otherwise.
3. **Is shared-anchor detection structurally possible in existing data?** Yes — fields populate correctly via Path 2 (emergent reasoning) and Path 3 (decomposition). The gap is not capability but triggering conditions.
4. **What production sequence would be needed to test this further?** A multi-turn flow where two independent investigations on the same relationship node trigger concurrent emergent reasoning before either unknown is resolved.
5. **Which production path creates usable shared-anchor data?** Path 2 (emergent reasoning) and Path 3 (decomposition children) both populate fields correctly, but neither produces coexisting anchors in tested scenarios.
6. **Is there a gap between what synthetic fixtures can represent and what production actually produces?** Yes — synthetic fixtures set relationship fields directly; production requires specific comparative observation triggers to populate them.
7. **What data quality improvement enables shared-anchor detection?** The existing emergent-reasoning path already works. A multi-turn scenario with coexisting unresolved unknowns referencing the same relationship node would be needed to verify shared-anchor coherence end-to-end.
### Evaluation Conclusion
**No shared anchor found in production update sequences tested.** Both Cases A and B returned `separate_anchors` or `insufficient_data`. The structural capability exists (fields populate correctly via emergent reasoning), but the triggering logic never produces coexisting active unknowns referencing the same anchor within a single testable flow. Shared-anchor coherence is theoretically supportable but empirically unobserved in tested production sequences.
### Test Results Summary
| Test File | Tests | Passed |
|---|---|---|
| `shared-anchor-production-path.test.js` (Exp 49) | 36 | 36 |
| `unknown-relationship-population.test.js` (Exp 48) | 16 | 16 |
| `investigation-state-assessor.shared-anchor.test.js` (Exp 47) | 26 | 26 |
### Pending Rob's review. No production code or graph schema modified.
**Commit:** pending (experiment: test production shared-anchor pattern)
@@ -0,0 +1,955 @@
/**
* 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);
});
});