import { describe, expect, it } from "vitest"; import { applyValidatedProposal, assessReasoningPatternCompatibility, assessStructuralContextAdmission, } from "@/lib/graph/apply-proposal.js"; import { selectReasoningPattern } from "@/lib/graph/question-formulator.js"; import { makeGraph, makeNode } from "@/lib/graph/schema.js"; // ── Helpers ────────────────────────────────────────────── function buildDecisionContext() { const decision = makeNode({ id: "n_relocation_decision", label: "Whether continuing development is commercially justified", description: "Need to know whether continuing development is commercially justified before committing resources.", kind: "unknown", status: "unknown", confidence: "medium", }); return makeGraph({ centralStatement: "Relocation decision context for structural embedding tests.", nodes: [decision], edges: [], activeUnknownNodeId: decision.id, resolvedNodeIds: [], currentSummary: "Decision fixture", }); } function addOption(graph, optionId) { const option = makeNode({ id: optionId, label: "Relocate to new office", description: "Move operations to a new location.", kind: "option", status: "provisional", }); graph.nodes.push(option); graph.edges.push({ id: `${optionId}_contained_in`, fromNodeId: optionId, toNodeId: graph.activeUnknownNodeId, relationship: "contained_in", confidence: "high", description: "Option belongs to this decision.", }); } function resolveActivePattern(graph) { const activeNode = graph.nodes.find( (n) => n.id === graph.activeUnknownNodeId, ); if (!activeNode) return null; return selectReasoningPattern({ node: activeNode, graph }).pattern; } function assessBoundedAdmission(graph, node, overrides = {}) { return assessStructuralContextAdmission({ node, graph, activePattern: overrides.activePattern ?? resolveActivePattern(graph), activeNodeId: overrides.activeNodeId ?? graph.activeUnknownNodeId, }); } function buildApplyProposalDecisionFixture() { const decision = makeNode({ id: "n_active_decision", label: "Whether relocating is commercially justified", description: "Need to evaluate whether relocating is commercially justified.", kind: "unknown", status: "unknown", confidence: "medium", }); return makeGraph({ centralStatement: "Relocation follow-up fixture.", nodes: [decision], edges: [], activeUnknownNodeId: decision.id, resolvedNodeIds: [], currentSummary: "Decision apply-proposal fixture", }); } // ── Tests ──────────────────────────────────────────────── describe("reasoning-context compatibility — bounded structural admission (60B.19)", () => { it("Test 1 - Route A: newly-added decision unknown admitted via parent/ancestor chain", () => { const decision = makeNode({ id: "n_decision_a", label: "Investment context A", description: "A parent context for the option under review.", kind: "unknown", status: "unknown", confidence: "medium", }); const parentOption = makeNode({ id: "opt_parent_a", label: "Parent option A", description: "An option for the decision.", kind: "option", status: "provisional", parentId: "n_decision_a", }); const childUnknown = makeNode({ id: "n_child_embedded", label: "What practical issue is blocking progress", description: "Need to identify the specific blocking issue before continuing this line of work.", kind: "unknown", status: "unknown", confidence: "medium", parentId: "opt_parent_a", }); const graph = makeGraph({ centralStatement: "Market entry decision.", nodes: [decision, parentOption], edges: [], activeUnknownNodeId: "n_decision_a", resolvedNodeIds: [], currentSummary: "Route A fixture", }); // childUnknown is not yet in graph.nodes; add it so we can build the chain const compat = assessBoundedAdmission( { ...graph, nodes: [...graph.nodes, childUnknown] }, childUnknown, { activePattern: "decision", activeNodeId: "n_decision_a" }, ); expect(compat.admitted).toBe(true); expect(compat.structuralEmbedding).toBe(true); expect(compat.routeA).toBe(true); }); it("Test 2 - Route B may_cause admitted", () => { const graph = buildDecisionContext(); addOption(graph, "opt_relocate"); const newUnknown = makeNode({ id: "n_client_retention_uncertainty", label: "What practical risk affects this option", description: "Need to identify the concrete risk factor affecting this option.", kind: "unknown", status: "unknown", confidence: "medium", }); graph.nodes.push(newUnknown); graph.edges.push({ id: "n_client_retention_uncertainty_may_cause_opt_relocate", fromNodeId: "n_client_retention_uncertainty", toNodeId: "opt_relocate", relationship: "may_cause", confidence: "medium", description: "May cause option consequence.", }); const compat = assessBoundedAdmission(graph, newUnknown); expect(compat.admitted).toBe(true); expect(compat.structuralEmbedding).toBe(true); expect(compat.routeB).toBe(true); }); it("Test 3 - Route B causes admitted", () => { const graph = buildDecisionContext(); addOption(graph, "opt_relocate"); // Add the node and edge into the graph for traversal. const newUnknown = makeNode({ id: "n_client_retention_uncertainty", label: "What practical risk affects this option", description: "Need to identify the concrete risk factor affecting this option.", kind: "unknown", status: "unknown", confidence: "medium", }); graph.nodes.push(newUnknown); graph.edges.push({ id: "n_client_retention_uncertainty_causes_opt_relocate", fromNodeId: "n_client_retention_uncertainty", toNodeId: "opt_relocate", relationship: "causes", confidence: "medium", description: "Causal link.", }); const compat = assessBoundedAdmission(graph, newUnknown); expect(compat.admitted).toBe(true); expect(compat.structuralEmbedding).toBe(true); expect(compat.routeB).toBe(true); }); it("Test 4 - Route B affects admitted", () => { const graph = buildDecisionContext(); addOption(graph, "opt_relocate"); const newUnknown = makeNode({ id: "n_client_retention_uncertainty", label: "What practical risk affects this option", description: "Need to identify the concrete risk factor affecting this option.", kind: "unknown", status: "unknown", confidence: "medium", }); graph.nodes.push(newUnknown); graph.edges.push({ id: "n_client_retention_uncertainty_affects_opt_relocate", fromNodeId: "n_client_retention_uncertainty", toNodeId: "opt_relocate", relationship: "affects", confidence: "medium", description: "Impact link.", }); const compat = assessBoundedAdmission(graph, newUnknown); expect(compat.admitted).toBe(true); expect(compat.structuralEmbedding).toBe(true); expect(compat.routeB).toBe(true); }); it("Test 5 - supports does NOT qualify as structural embedding", () => { const graph = buildDecisionContext(); addOption(graph, "opt_relocate"); const newUnknown = makeNode({ id: "n_supporting_factor", label: "What causes the revenue discrepancy?", description: "Need to understand root cause of divergence.", kind: "unknown", status: "unknown", confidence: "medium", }); graph.nodes.push(newUnknown); graph.edges.push({ id: "n_supporting_factor_supports_opt_relocate", fromNodeId: "n_supporting_factor", toNodeId: "opt_relocate", relationship: "supports", confidence: "medium", description: "Support link.", }); const compat = assessBoundedAdmission(graph, newUnknown); expect(compat.admitted).toBe(false); expect(compat.structuralEmbedding).toBe(false); }); it("Test 6 - measures does NOT qualify as structural embedding", () => { const graph = buildDecisionContext(); addOption(graph, "opt_relocate"); const newUnknown = makeNode({ id: "n_measuring_node", label: "What causes the revenue discrepancy?", description: "Need to understand root cause of divergence.", kind: "unknown", status: "unknown", confidence: "medium", }); graph.nodes.push(newUnknown); graph.edges.push({ id: "n_measuring_node_measures_opt_relocate", fromNodeId: "n_measuring_node", toNodeId: "opt_relocate", relationship: "measures", confidence: "medium", description: "Measurement link.", }); const compat = assessBoundedAdmission(graph, newUnknown); expect(compat.admitted).toBe(false); expect(compat.structuralEmbedding).toBe(false); }); it("Test 7 - depends_on does NOT qualify as structural embedding", () => { const graph = buildDecisionContext(); addOption(graph, "opt_relocate"); const newUnknown = makeNode({ id: "n_depends_node", label: "What causes the revenue discrepancy?", description: "Need to understand root cause of divergence.", kind: "unknown", status: "unknown", confidence: "medium", }); graph.nodes.push(newUnknown); graph.edges.push({ id: "n_depends_node_depends_on_opt_relocate", fromNodeId: "n_depends_node", toNodeId: "opt_relocate", relationship: "depends_on", confidence: "medium", description: "Dependency link.", }); const compat = assessBoundedAdmission(graph, newUnknown); expect(compat.admitted).toBe(false); expect(compat.structuralEmbedding).toBe(false); }); it("Test 8 - arbitrary graph connectivity does NOT produce compatibility", () => { const graph = buildDecisionContext(); addOption(graph, "opt_relocate"); const intermediateNode = makeNode({ id: "n_intermediate", label: "Some unrelated factor", description: "Not relevant.", kind: "unknown", status: "unknown", confidence: "medium", }); const newUnknown = makeNode({ id: "n_arbitrary_path", label: "What causes the revenue discrepancy?", description: "Need to understand root cause of divergence.", kind: "unknown", status: "unknown", confidence: "medium", }); graph.nodes.push(intermediateNode, newUnknown); graph.edges.push({ id: "n_arbitrary_path_may_cause_intermediate", fromNodeId: "n_arbitrary_path", toNodeId: "n_intermediate", relationship: "may_cause", confidence: "medium", description: "Connects to intermediate, not an option.", }); const compat = assessBoundedAdmission(graph, newUnknown); expect(compat.admitted).toBe(false); expect(compat.structuralEmbedding).toBe(false); }); it("Test 9 - wrong decision context rejects", () => { const otherDecision = makeNode({ id: "n_other_decision", label: "Should we launch product B?", description: "Different decision.", kind: "unknown", status: "unknown", confidence: "medium", }); const baseGraph = makeGraph({ centralStatement: "Testing wrong decision context.", nodes: [buildDecisionContext().nodes[0], otherDecision], edges: [], activeUnknownNodeId: "n_relocation_decision", resolvedNodeIds: [], currentSummary: "Wrong context fixture", }); const wrongOption = makeNode({ id: "opt_wrong_context", label: "Launch product B option", description: "Belongs to other decision.", kind: "option", status: "provisional", }); baseGraph.nodes.push(wrongOption); baseGraph.edges.push({ id: "opt_wrong_context_contained_in_other", fromNodeId: "opt_wrong_context", toNodeId: "n_other_decision", relationship: "contained_in", confidence: "high", description: "Wrong decision membership.", }); const newUnknown = makeNode({ id: "n_wrong_context_unknown", label: "What practical risk affects this option", description: "Need to identify the concrete risk factor affecting this option.", kind: "unknown", status: "unknown", confidence: "medium", }); baseGraph.nodes.push(newUnknown); baseGraph.edges.push({ id: "n_wrong_context_unknown_may_cause_opt", fromNodeId: "n_wrong_context_unknown", toNodeId: "opt_wrong_context", relationship: "may_cause", confidence: "medium", description: "May cause wrong option.", }); const compat = assessBoundedAdmission(baseGraph, newUnknown); expect(compat.admitted).toBe(false); expect(compat.structuralEmbedding).toBe(false); }); it("Test 10 - already-compatible pattern remains unchanged", () => { const decision = makeNode({ id: "n_decision_10", label: "Whether proceeding is commercially justified", description: "Need to evaluate whether proceeding is commercially justified.", kind: "unknown", status: "unknown", confidence: "medium", }); const definitionNode = makeNode({ id: "n_definition_node", label: "Definition of commercially justified", description: "Define what commercially justified means. Define the term.", kind: "unknown", status: "unknown", confidence: "medium", }); const graph = makeGraph({ centralStatement: "Decision context.", nodes: [decision, definitionNode], edges: [], activeUnknownNodeId: "n_decision_10", resolvedNodeIds: [], currentSummary: "Already compatible fixture", }); const compat = assessBoundedAdmission(graph, definitionNode); expect(compat.intrinsicCompatible).toBe(true); expect(compat.admitted).toBe(false); expect(compat.structuralEmbedding).toBe(false); }); it("Test 11 - genuine incompatible child under explanation is rejected", () => { const explanationUnknown = makeNode({ id: "n_explanation", label: "Explanation for why revenue increased by 18%", description: "Need to understand what change or event could explain these observations.", kind: "unknown", status: "unknown", confidence: "medium", }); const graph = makeGraph({ centralStatement: "Revenue increased by 18%, but cash in the bank fell over the same period.", nodes: [ explanationUnknown, makeNode({ id: "n_revenue_obs", label: "Revenue increased by 18%.", description: "Revenue increased by 18%.", kind: "observation", status: "supported", confidence: "high", }), makeNode({ id: "n_cash_obs", label: "Cash in the bank fell over the same period.", description: "Cash in the bank fell.", kind: "observation", status: "supported", confidence: "high", }), ], edges: [], activeUnknownNodeId: "n_explanation", resolvedNodeIds: [], currentSummary: "Explanation fixture", }); const diagnosisChild = makeNode({ id: "n_diagnosis_child", label: "What practical issue is blocking progress", description: "Need to identify the specific blocking issue before continuing this line of work.", kind: "unknown", status: "unknown", confidence: "medium", parentId: "n_explanation", }); graph.nodes.push(diagnosisChild); const compat = assessBoundedAdmission(graph, diagnosisChild); const compatibility = assessReasoningPatternCompatibility({ node: diagnosisChild, graph, activePattern: resolveActivePattern(graph), activeNodeId: graph.activeUnknownNodeId, }); expect(compat.admitted).toBe(false); expect(compat.nodePattern).toBe("diagnosis"); expect(compat.structuralEmbedding).toBe(false); expect(compatibility.compatible).toBe(false); }); it("Test 12 - pre-existing unknown does not enter fallback", () => { const graph = buildApplyProposalDecisionFixture(); const preExisting = makeNode({ id: "n_preexisting_diagnosis", label: "What practical risk affects this option", description: "Need to identify the concrete risk factor affecting this option.", kind: "unknown", status: "unknown", confidence: "medium", parentId: graph.activeUnknownNodeId, }); graph.nodes.push(preExisting); const result = applyValidatedProposal({ situationGraph: graph, proposal: { addedNodes: [], updatedNodes: [ { nodeId: graph.activeUnknownNodeId, previousStatus: "unknown", newStatus: "known", previousValue: null, newValue: "Decision anchor updated for selection.", reason: "Makes the parent answer-derived for the test.", }, ], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: [], affectedNodeIds: [], selectedQuestion: { nodeId: "n_preexisting_diagnosis", question: "What practical risk affects this option?", reason: "Pre-existing node should not gain fallback admission.", }, }, }); expect(result.success).toBe(true); }); it("Test 13 - admitted node survives later result validation", () => { const graph = buildApplyProposalDecisionFixture(); const result = applyValidatedProposal({ situationGraph: graph, answer: "We need to identify the practical risk affecting this option.", previousQuestion: "Should we relocate?", proposal: { addedNodes: [ makeNode({ id: "n_added_route_a", label: "What practical risk affects this option", description: "Need to identify the concrete risk factor affecting this option because it matters to the decision.", kind: "unknown", status: "unknown", confidence: "medium", parentId: graph.activeUnknownNodeId, }), ], updatedNodes: [ { nodeId: graph.activeUnknownNodeId, previousStatus: "unknown", newStatus: "provisional", previousValue: null, newValue: "Decision remains open pending factor clarification.", reason: "The answer introduces a concrete follow-up factor.", }, ], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: [], affectedNodeIds: [], selectedQuestion: { nodeId: "n_added_route_a", question: "What practical risk affects this option?", reason: "New factor introduced this turn.", }, answerMeaning: { userSupportedMeaning: "We need to identify the practical risk affecting this option.", possibleInference: null, supportCategory: null, resolutionGuidance: null, }, structuralActionRequired: true, }, }); expect(result.success).toBe(true); expect(result.selectedQuestion?.nodeId).toBe("n_added_route_a"); }); it("Test 14 - non-pattern validations still apply", () => { const graph = buildApplyProposalDecisionFixture(); const result = applyValidatedProposal({ situationGraph: graph, answer: "We need to identify the practical risk affecting this option.", previousQuestion: "Should we relocate?", proposal: { addedNodes: [ makeNode({ id: "n_added_invalid", label: "What practical risk affects this option", description: "Need to identify the concrete risk factor affecting this option because it matters to the decision.", kind: "unknown", status: "unknown", confidence: "medium", parentId: graph.activeUnknownNodeId, }), ], updatedNodes: [ { nodeId: graph.activeUnknownNodeId, previousStatus: "unknown", newStatus: "provisional", previousValue: null, newValue: "Decision remains open pending factor clarification.", reason: "The answer introduces a concrete follow-up factor.", }, ], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: [], affectedNodeIds: [], selectedQuestion: { nodeId: "n_added_invalid", question: "What practical risk affects this option, and how severe is it?", reason: "Compound question should still fail.", }, answerMeaning: { userSupportedMeaning: "We need to identify the practical risk affecting this option.", possibleInference: null, supportCategory: null, resolutionGuidance: null, }, structuralActionRequired: true, }, }); expect(result.success).toBe(false); expect(result.errors.join(" ")).toContain( "selectedQuestion must be a single non-compound question", ); }); });