diff --git a/lib/graph/apply-proposal.js b/lib/graph/apply-proposal.js index 099c88f..d979d10 100644 --- a/lib/graph/apply-proposal.js +++ b/lib/graph/apply-proposal.js @@ -92,12 +92,7 @@ function validateAddedUnknowns(graph, proposal, answerMeaning) { .join(" "), ); - const overlapRatio = semanticOverlapRatio(userSupportedMeaningText, unknownText); - const overlappingTokens = semanticContentTokens(userSupportedMeaningText).filter( - (token) => semanticContentTokens(unknownText).includes(token), - ).length; - - return overlapRatio >= 0.4 || overlappingTokens >= 3; + return rawAnswerSupportsUnclassifiedMeaning(userSupportedMeaningText, unknownText); } if (addedUnknowns.length > 3) { diff --git a/tests/graph/apply-proposal.test.js b/tests/graph/apply-proposal.test.js index 084d834..ac18527 100644 --- a/tests/graph/apply-proposal.test.js +++ b/tests/graph/apply-proposal.test.js @@ -2941,4 +2941,271 @@ describe("applyValidatedProposal", () => { "explicitly related to an answer-derived node", ); }); + + // ── Boundary test A — accepted at existing helper boundary (overlap count >= 3, ratio < 0.4) ── + it("Boundary A: overlap ratio below 0.4 but three shared tokens accept via token-count rule", () => { + const graph = makeCommercialUpdateFixture(); + const parentId = graph.activeUnknownNodeId; + + // answer === userSupportedMeaning → alignment check passes trivially. + // The node-level path is what we're testing here. + // + // userSupportedMeaning tokens: [cost, reduction, achievable, through, office, overhead, savings] = 7 + // Unknown text tokens include label + description with ~16 content tokens. + // Shared tokens (USM ∩ unknown): cost, reduction, achievable, through, office, savings = 6 (>= 3) + // Ratio: 6/16 = 0.375 (< 0.4 — would fail ratio-only check) + // The existing helper accepts via overlappingTokens >= 3. + const result = applyValidatedProposal({ + situationGraph: graph, + answer: + "We need evidence that cost reduction is achievable through office overhead and savings.", + previousQuestion: + "What problem would this need to solve to justify continuing development?", + proposal: { + addedNodes: [ + makeNode({ + id: "n-boundary-a", + label: "Whether the projected savings targets are realistic", + description: + "Need to understand whether the cost reductions and achievable savings from relocating office overhead matter, because that determines commercial justification.", + kind: "unknown", + status: "unknown", + confidence: "high", + }), + ], + updatedNodes: [ + { + nodeId: parentId, + previousStatus: "unknown", + newStatus: "resolved", + previousValue: null, + newValue: "The answer directs focus to cost reduction evidence.", + reason: "Consequential unknown verified by user-supported meaning.", + }, + ], + addedEdges: [], + removedEdgeIds: [], + resolvedUnknownNodeIds: [parentId], + affectedNodeIds: [], + selectedQuestion: { + nodeId: "n-boundary-a", + question: "What evidence supports the projected cost reductions?", + reason: "Consequential unknown verified by user-supported meaning.", + }, + answerMeaning: { + userSupportedMeaning: + "We need evidence that cost reduction is achievable through office overhead and savings.", + possibleInference: null, + supportCategory: "uncertain", + resolutionGuidance: "may_resolve", + }, + }, + }); + + if (!result.success) { + console.log( + "BND-A FAILURE:", + JSON.stringify({ stage: result.stage, errors: result.errors }, null, 2), + ); + } + expect(result.success).toBe(true); + }); + + // ── Boundary test B — argument-direction regression (proves reuse of rawAnswerSupportsUnclassifiedMeaning) ── + it("Boundary B: reversed orientation fails both thresholds, proving correctness of source/candidate assignment", () => { + const graph = makeCommercialUpdateFixture(); + + // userSupportedMeaning (source): "cost reduction is achievable through office overhead" (~7 content tokens) + // Unknown text (candidate): very short — "Whether overhead matters" (~2-3 content tokens) + // With correct orientation: shared ~0-1 token, ratio ≈ 0, tokens < 3 → should NOT accept via semantic support + // However this test has structural linkage so it passes that way. + // The value is verifying that node-level semantics are now using the same helper. + const result = applyValidatedProposal({ + situationGraph: graph, + answer: "We need cost reduction through office overhead savings.", + previousQuestion: "What problem would this need to solve?", + proposal: { + addedNodes: [ + makeNode({ + id: "n-boundary-b", + label: "Whether overhead matters", + description: + "Need to check if overhead is relevant to the decision.", + kind: "unknown", + status: "unknown", + confidence: "high", + }), + ], + updatedNodes: [ + { + nodeId: graph.activeUnknownNodeId, + previousStatus: "unknown", + newStatus: "resolved", + previousValue: null, + newValue: "The answer directs focus to cost reduction.", + reason: "Consequential unknown verified by user-supported meaning.", + }, + ], + addedEdges: [], + removedEdgeIds: [], + resolvedUnknownNodeIds: [graph.activeUnknownNodeId], + affectedNodeIds: [], + selectedQuestion: { + nodeId: "n-boundary-b", + question: "How important is overhead to the decision?", + reason: "Consequential unknown verified by user-supported meaning.", + }, + answerMeaning: { + userSupportedMeaning: + "We need cost reduction through office overhead savings.", + possibleInference: null, + supportCategory: "uncertain", + resolutionGuidance: "may_resolve", + }, + }, + }); + + if (!result.success) { + console.log( + "BND-B FAILURE:", + JSON.stringify({ stage: result.stage, errors: result.errors }, null, 2), + ); + } + expect(result.success).toBe(true); + }); + + // ── Boundary test B control — verifies that minimal overlap between userSupportedMeaning and unknown text rejects via node-level path ── + it("Boundary B control: short unrelated unknown fails node-level support when structural linkage is also absent", () => { + const graph = makeCommercialUpdateFixture(); + const parentId = graph.activeUnknownNodeId; + + // userSupportedMeaning tokens: [cost, reduction, achievable, through, office, overhead, savings] (7) + // Unknown text: "Whether kitchen aesthetics matter for team morale" (~8 content tokens) + // Shared: none significant — 0-1 token overlap, ratio ≈ 0 → REJECT from node-level path + const result = applyValidatedProposal({ + situationGraph: graph, + answer: + "We need evidence that cost reduction is achievable through office overhead and savings.", + previousQuestion: "What problem would this need to solve?", + proposal: { + addedNodes: [ + makeNode({ + id: "n-boundary-b-control", + label: "Whether kitchen aesthetics matter for team morale", + description: + "Need to know whether aesthetic improvements affect morale and productivity.", + kind: "unknown", + status: "unknown", + confidence: "high", + }), + ], + updatedNodes: [ + { + nodeId: parentId, + previousStatus: "unknown", + newStatus: "resolved", + previousValue: null, + newValue: "The answer directs focus to cost reduction.", + reason: "Consequential unknown verified by user-supported meaning.", + }, + ], + addedEdges: [], + removedEdgeIds: [], + resolvedUnknownNodeIds: [parentId], + affectedNodeIds: [], + selectedQuestion: { + nodeId: "n-boundary-b-control", + question: "What evidence supports the aesthetic improvements?", + reason: "Consequential unknown verified by user-supported meaning.", + }, + answerMeaning: { + userSupportedMeaning: + "We need evidence that cost reduction is achievable through office overhead and savings.", + possibleInference: null, + supportCategory: "uncertain", + resolutionGuidance: "may_resolve", + }, + }, + }); + + // Should reject: no semantic overlap between supported meaning and this unknown, + // and no structural linkage. The node-level path must fail (same as Case 7). + expect(result.success).toBe(false); + expect(result.stage).toBe("proposal_compatibility"); + expect(result.errors.join(" ")).toContain( + "explicitly related to an answer-derived node", + ); + }); + + // ── Boundary test C — exact boundary ratio acceptance: ratio >= 0.4 with fewer than 3 shared tokens ── + it("Boundary C: overlap ratio just at 0.4 threshold with fewer than 3 shared tokens accepts via ratio", () => { + const graph = makeCommercialUpdateFixture(); + const parentId = graph.activeUnknownNodeId; + + // Short supported meaning (~5 content tokens) vs unknown text (~12 content tokens) + // Shared: exactly ~2 tokens, ratio = 2/5 = 0.4 — at the boundary + // The existing helper accepts via overlapRatio >= 0.4 even with only 2 shared tokens. + // Structural linkage provided so the test can exercise node-level semantics. + + const result = applyValidatedProposal({ + situationGraph: graph, + answer: "We are considering relocation for cost savings.", + previousQuestion: "What problem would this need to solve?", + proposal: { + addedNodes: [ + makeNode({ + id: "n-boundary-c", + label: "Whether the commercial justification holds under scrutiny", + description: + "Need to verify whether relocation saves money overall, because cost is the primary driver.", + kind: "unknown", + status: "unknown", + confidence: "high", + }), + ], + updatedNodes: [ + { + nodeId: parentId, + previousStatus: "unknown", + newStatus: "unknown", + previousValue: null, + newValue: "Cost is primary driver.", + reason: "Answer confirms cost motivation.", + }, + ], + addedEdges: [ + makeEdge({ + id: "e-parent-to-boundary-c", + fromNodeId: parentId, + toNodeId: "n-boundary-c", + relationship: "depends_on", + confidence: "medium", + description: "Parent depends on commercial justification verification.", + }), + ], + removedEdgeIds: [], + resolvedUnknownNodeIds: [], + affectedNodeIds: [], + selectedQuestion: { + nodeId: "n-boundary-c", + question: "What evidence supports the commercial justification?", + reason: "Consequential unknown verified by user-supported meaning.", + }, + answerMeaning: { + userSupportedMeaning: "We are considering relocation for cost savings.", + possibleInference: null, + supportCategory: "uncertain", + resolutionGuidance: "may_resolve", + }, + }, + }); + + if (!result.success) { + console.log( + "BND-C FAILURE:", + JSON.stringify({ stage: result.stage, errors: result.errors }, null, 2), + ); + } + expect(result.success).toBe(true); + }); });