From 9e869cbcb8a73aa3725b1b715ec98299c796a5be Mon Sep 17 00:00:00 2001 From: robbond Date: Mon, 10 Aug 2026 19:27:54 +0100 Subject: [PATCH] reasoning: admit verified user-supported unknowns without provenance edges --- lib/graph/apply-proposal.js | 28 +- tests/graph/apply-proposal.test.js | 404 +++++++++++++++++++++++++++++ 2 files changed, 429 insertions(+), 3 deletions(-) diff --git a/lib/graph/apply-proposal.js b/lib/graph/apply-proposal.js index d34728d..099c88f 100644 --- a/lib/graph/apply-proposal.js +++ b/lib/graph/apply-proposal.js @@ -75,12 +75,31 @@ function isCompoundQuestion(question) { return false; } -function validateAddedUnknowns(graph, proposal) { +function validateAddedUnknowns(graph, proposal, answerMeaning) { const errors = []; const addedUnknowns = proposal.addedNodes.filter( (node) => node.kind === "unknown", ); + const userSupportedMeaningText = answerMeaning?.userSupportedMeaning; + + function hasNodeLevelUserSupport(unknownNode) { + if (!userSupportedMeaningText) return false; + + const unknownText = normaliseSemanticText( + [normaliseText(unknownNode.label), normaliseText(unknownNode.description)] + .filter(Boolean) + .join(" "), + ); + + const overlapRatio = semanticOverlapRatio(userSupportedMeaningText, unknownText); + const overlappingTokens = semanticContentTokens(userSupportedMeaningText).filter( + (token) => semanticContentTokens(unknownText).includes(token), + ).length; + + return overlapRatio >= 0.4 || overlappingTokens >= 3; + } + if (addedUnknowns.length > 3) { errors.push( `Proposal adds too many unknown nodes: ${addedUnknowns.length} (maximum 3)`, @@ -180,7 +199,10 @@ function validateAddedUnknowns(graph, proposal) { ); } - if (!hasExplicitAnswerDerivedRelationship(unknownNode)) { + if ( + !hasNodeLevelUserSupport(unknownNode) && + !hasExplicitAnswerDerivedRelationship(unknownNode) + ) { errors.push( `New unknown must be explicitly related to an answer-derived node: "${unknownNode.id}"`, ); @@ -3232,7 +3254,7 @@ export function applyValidatedProposal({ ...validateSemanticDuplicateUnknowns(situationGraph, validatedProposal), ); proposalCompatibilityErrors.push( - ...validateAddedUnknowns(situationGraph, validatedProposal), + ...validateAddedUnknowns(situationGraph, validatedProposal, validatedProposal.answerMeaning), ); const selectedQuestionValidation = validateSelectedQuestion( diff --git a/tests/graph/apply-proposal.test.js b/tests/graph/apply-proposal.test.js index d1afaf0..084d834 100644 --- a/tests/graph/apply-proposal.test.js +++ b/tests/graph/apply-proposal.test.js @@ -2537,4 +2537,408 @@ describe("applyValidatedProposal", () => { ); expect(result.selectedQuestion?.reasoningPattern).toBe("decision"); }); + + // ── Case 1 — direct user-supported uncertainty, no structural anchor ── + it("Case 1: admitted unknown passes via node-level user support without structural linkage", () => { + const graph = makeCommercialUpdateFixture(); + const parentId = graph.activeUnknownNodeId; + + const result = applyValidatedProposal({ + situationGraph: graph, + answer: "I need evidence that projected office savings are realistic.", + previousQuestion: + "What problem would this need to solve to justify continuing development?", + proposal: { + addedNodes: [ + makeNode({ + id: "n-savings-realism", + label: "Whether projected office savings are realistic", + description: + "Need to verify whether the projected office savings are realistic, because that determines commercial justification.", + kind: "unknown", + status: "unknown", + confidence: "high", + }), + ], + updatedNodes: [ + { + nodeId: parentId, + previousStatus: "unknown", + newStatus: "resolved", + previousValue: null, + newValue: "The user needs evidence for projected office savings realism.", + reason: "The answer directs focus to savings realism evidence.", + }, + ], + addedEdges: [], + removedEdgeIds: [], + resolvedUnknownNodeIds: [parentId], + affectedNodeIds: [], + selectedQuestion: { + nodeId: "n-savings-realism", + question: "What evidence supports the projected office savings?", + reason: "Consequential unknown verified by user-supported meaning.", + }, + answerMeaning: { + userSupportedMeaning: + "I need evidence that projected office savings are realistic.", + possibleInference: null, + supportCategory: "uncertain", + resolutionGuidance: "may_resolve", + }, + }, + }); + + expect(result.success).toBe(true); + }); + + // ── Case 2 — two direct user-supported unknowns ── + it("Case 2: two independently supported unknowns both pass via node-level user support (parent stays unresolved)", () => { + const graph = makeCommercialUpdateFixture(); + const parentId = graph.activeUnknownNodeId; + + // The parent remains unresolved — the answer adds two new unknowns + // without resolving the original question. Both children are independently verified. + const result = applyValidatedProposal({ + situationGraph: graph, + answer: "I need evidence the savings are realistic and evidence the move will not materially increase loss of key engineers.", + previousQuestion: + "What problem would this need to solve to justify continuing development?", + proposal: { + addedNodes: [ + makeNode({ + id: "n-savings-realism-2", + label: "Whether projected savings are realistic", + description: + "Need to verify whether the savings figures are realistic, because that is needed for commercial evaluation.", + kind: "unknown", + status: "unknown", + confidence: "high", + }), + makeNode({ + id: "n-retention-impact", + label: "Whether the move materially increases loss of key engineers", + description: + "Need to check whether the relocation increases risk of losing key engineers, because that matters for continuity.", + kind: "unknown", + status: "unknown", + confidence: "high", + }), + ], + updatedNodes: [], + addedEdges: [ + makeEdge({ + id: "e-parent-to-savings", + fromNodeId: parentId, + toNodeId: "n-savings-realism-2", + relationship: "depends_on", + confidence: "medium", + description: "Parent depends on savings verification.", + }), + ], + removedEdgeIds: [], + resolvedUnknownNodeIds: [], + affectedNodeIds: [], + selectedQuestion: { + nodeId: "n-savings-realism-2", + question: "What evidence supports the projected office savings?", + reason: "First of two independently verified consequential unknowns.", + }, + answerMeaning: { + userSupportedMeaning: + "I need evidence the savings are realistic and evidence the move will not materially increase loss of key engineers.", + possibleInference: null, + supportCategory: "uncertain", + resolutionGuidance: "may_resolve", + }, + }, + }); + + expect(result.success).toBe(true); + }); + + // ── Case 3 — unsupported hallucinated unknown ── + it("Case 3: unsupported hallucinated unknown is rejected without structural linkage", () => { + const graph = makeCommercialUpdateFixture(); + const parentId = graph.activeUnknownNodeId; + + const result = applyValidatedProposal({ + situationGraph: graph, + answer: "We are looking at this mainly for cost reduction — roughly £2M annual savings on office overhead.", + previousQuestion: + "What problem would this need to solve to justify continuing development?", + proposal: { + addedNodes: [ + makeNode({ + id: "n-office-paint-colour", + label: "Whether office paint colour affects morale", + description: + "Need to know whether paint colour in the current office affects team morale, because that is a potential factor.", + kind: "unknown", + status: "unknown", + confidence: "low", + }), + ], + updatedNodes: [ + { + nodeId: parentId, + previousStatus: "unknown", + newStatus: "resolved", + previousValue: null, + newValue: "Cost reduction is the primary driver.", + reason: "The answer identifies cost reduction as the main concern.", + }, + ], + addedEdges: [], + removedEdgeIds: [], + resolvedUnknownNodeIds: [parentId], + affectedNodeIds: [], + selectedQuestion: { + nodeId: "n-office-paint-colour", + question: "What evidence supports the office paint colour hypothesis?", + reason: "Unrelated unknown — should be rejected.", + }, + answerMeaning: { + userSupportedMeaning: + "The main reason for considering this is cost reduction, specifically about £2M in annual office-overhead savings.", + possibleInference: null, + supportCategory: "other", + resolutionGuidance: null, + }, + }, + }); + + expect(result.success).toBe(false); + expect(result.stage).toBe("proposal_compatibility"); + expect(result.errors.join(" ")).toContain( + "explicitly related to an answer-derived node", + ); + }); + + // ── Case 4 — possibleInference only ── + it("Case 4: unknown based on possibleInference only is not admitted through user-support path", () => { + const graph = makeCommercialUpdateFixture(); + + // AnswerMeaning's userSupportedMeaning shares NO tokens with the unknown. + // possibleInference does mention "workforce" but that must NOT establish support. + const result = applyValidatedProposal({ + situationGraph: graph, + answer: "The project timeline is critical.", + previousQuestion: "What problem would this need to solve?", + proposal: { + addedNodes: [ + makeNode({ + id: "n-workforce-stability", + label: "Whether workforce stability affects the decision", + description: "Need to check whether workforce stability matters, because that could impact scheduling.", + kind: "unknown", + status: "unknown", + confidence: "medium", + }), + ], + updatedNodes: [], + addedEdges: [], + removedEdgeIds: [], + resolvedUnknownNodeIds: [], + affectedNodeIds: [], + selectedQuestion: { + nodeId: "n-workforce-stability", + question: "How important is workforce stability to this decision?", + reason: "Inferred consequential unknown — should be rejected.", + }, + answerMeaning: { + userSupportedMeaning: "The project timeline is critical to the decision.", + possibleInference: "Workforce stability may matter for scheduling and continuity.", + supportCategory: null, + resolutionGuidance: null, + }, + }, + }); + + expect(result.success).toBe(false); + expect(result.stage).toBe("proposal_compatibility"); + expect(result.errors.join(" ")).toContain( + "explicitly related to an answer-derived node", + ); + }); + + // ── Case 5 — genuine structural edge ── + it("Case 5: supported unknown with genuine structural edge passes without additional provenance edge", () => { + const graph = makeCommercialUpdateFixture(); + const parentId = graph.activeUnknownNodeId; + + const result = applyValidatedProposal({ + situationGraph: graph, + answer: "We need evidence that projected office savings are realistic.", + previousQuestion: + "What problem would this need to solve to justify continuing development?", + proposal: { + addedNodes: [ + makeNode({ + id: "n-savings-realism-5", + label: "Whether projected office savings are realistic", + description: + "Need to verify whether the savings figures are realistic, because that is needed for commercial evaluation.", + kind: "unknown", + status: "unknown", + confidence: "high", + parentId, + }), + ], + updatedNodes: [ + { + nodeId: parentId, + previousStatus: "unknown", + newStatus: "resolved", + previousValue: null, + newValue: "Savings realism evidence is needed.", + reason: "The answer directs to savings verification.", + }, + ], + addedEdges: [ + makeEdge({ + id: "e-parent-savings-realism-5", + fromNodeId: parentId, + toNodeId: "n-savings-realism-5", + relationship: "depends_on", + confidence: "medium", + description: "Savings realism is a dependency of commercial justification.", + }), + ], + removedEdgeIds: [], + resolvedUnknownNodeIds: [parentId], + affectedNodeIds: [], + selectedQuestion: { + nodeId: "n-savings-realism-5", + question: "What evidence supports the projected office savings?", + reason: "Both supported and structurally linked.", + }, + answerMeaning: { + userSupportedMeaning: + "We need evidence that projected office savings are realistic.", + possibleInference: null, + supportCategory: "uncertain", + resolutionGuidance: "may_resolve", + }, + }, + }); + + expect(result.success).toBe(true); + }); + + // ── Case 6 — answerMeaning absent + valid structural linkage ── + it("Case 6: no answerMeaning but valid structural linkage passes (existing behaviour unchanged)", () => { + const graph = makeCommercialUpdateFixture(); + const parentId = graph.activeUnknownNodeId; + + const result = applyValidatedProposal({ + situationGraph: graph, + answer: "We need evidence that projected office savings are realistic.", + previousQuestion: + "What problem would this need to solve to justify continuing development?", + proposal: { + addedNodes: [ + makeNode({ + id: "n-savings-realism-6", + label: "Whether projected office savings are realistic", + description: + "Need to verify whether the savings figures are realistic, because that is needed for commercial evaluation.", + kind: "unknown", + status: "unknown", + confidence: "high", + parentId, + }), + ], + updatedNodes: [ + { + nodeId: parentId, + previousStatus: "unknown", + newStatus: "resolved", + previousValue: null, + newValue: "Savings realism evidence is needed.", + reason: "The answer directs to savings verification.", + }, + ], + addedEdges: [ + makeEdge({ + id: "e-parent-savings-realism-6", + fromNodeId: parentId, + toNodeId: "n-savings-realism-6", + relationship: "depends_on", + confidence: "medium", + description: "Savings realism is a dependency.", + }), + ], + removedEdgeIds: [], + resolvedUnknownNodeIds: [parentId], + affectedNodeIds: [], + selectedQuestion: { + nodeId: "n-savings-realism-6", + question: "What evidence supports the projected office savings?", + reason: "Structurally linked without answerMeaning.", + }, + answerMeaning: null, + }, + }); + + expect(result.success).toBe(true); + }); + + // ── Case 7 — no support and no linkage ── + it("Case 7: unknown with neither user support nor structural linkage is rejected", () => { + const graph = makeCommercialUpdateFixture(); + const parentId = graph.activeUnknownNodeId; + + const result = applyValidatedProposal({ + situationGraph: graph, + answer: "We need evidence that projected office savings are realistic.", + previousQuestion: + "What problem would this need to solve to justify continuing development?", + proposal: { + addedNodes: [ + makeNode({ + id: "n-hallucinated-unknown", + label: "Whether the kitchen lighting affects productivity", + description: + "Need to know whether lighting matters, because it could impact outcomes.", + kind: "unknown", + status: "unknown", + confidence: "low", + }), + ], + updatedNodes: [ + { + nodeId: parentId, + previousStatus: "unknown", + newStatus: "resolved", + previousValue: null, + newValue: "Savings realism is the focus.", + reason: "Answer directs to savings verification.", + }, + ], + addedEdges: [], + removedEdgeIds: [], + resolvedUnknownNodeIds: [parentId], + affectedNodeIds: [], + selectedQuestion: { + nodeId: "n-hallucinated-unknown", + question: "Does kitchen lighting affect productivity?", + reason: "No support, no linkage — should be rejected.", + }, + answerMeaning: { + userSupportedMeaning: + "We need evidence that projected office savings are realistic.", + possibleInference: null, + supportCategory: "uncertain", + resolutionGuidance: "may_resolve", + }, + }, + }); + + expect(result.success).toBe(false); + expect(result.stage).toBe("proposal_compatibility"); + expect(result.errors.join(" ")).toContain( + "explicitly related to an answer-derived node", + ); + }); });