From 36faf70a087e172c0e169ade5311b0acde068605 Mon Sep 17 00:00:00 2001 From: robbond Date: Sun, 9 Aug 2026 08:06:38 +0100 Subject: [PATCH] refine answerMeaning support category normalisation --- lib/graph/update-proposal.js | 18 +++++++++++ tests/graph/apply-proposal.test.js | 44 ++++++++++++++++++++++++++ tests/graph/update-proposal.test.js | 48 +++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) diff --git a/lib/graph/update-proposal.js b/lib/graph/update-proposal.js index 8e462ea..91a5f27 100644 --- a/lib/graph/update-proposal.js +++ b/lib/graph/update-proposal.js @@ -11,6 +11,10 @@ const TOP_LEVEL_ARRAY_FIELDS = [ const TOP_LEVEL_NULLABLE_FIELDS = ["selectedQuestion"]; +const ANSWER_SUPPORT_CATEGORY_ALIASES = { + conditional_qualification: "conditional_tradeoff", +}; + function cloneJsonSafe(value) { if (value == null) return value; return JSON.parse(JSON.stringify(value)); @@ -62,6 +66,20 @@ function applyKnownEnumAliases(proposal, normalisationsApplied) { }); } + const supportCategory = proposal.answerMeaning?.supportCategory; + const canonicalSupportCategory = + ANSWER_SUPPORT_CATEGORY_ALIASES[supportCategory]; + if (canonicalSupportCategory) { + normalisationsApplied.push({ + path: ["answerMeaning", "supportCategory"], + change: `Converted ${supportCategory} to ${canonicalSupportCategory}`, + }); + proposal.answerMeaning = { + ...proposal.answerMeaning, + supportCategory: canonicalSupportCategory, + }; + } + return proposal; } diff --git a/tests/graph/apply-proposal.test.js b/tests/graph/apply-proposal.test.js index 0ffe901..d535d65 100644 --- a/tests/graph/apply-proposal.test.js +++ b/tests/graph/apply-proposal.test.js @@ -1123,6 +1123,50 @@ describe("applyValidatedProposal", () => { expect(result.errors.join(" ")).toContain("conditional qualification"); }); + it("accepts equivalent conditional category labels and still applies the existing Regression B guard", () => { + const { graph, riskUnknownId } = makeRiskClarificationFixture(); + + const result = applyValidatedProposal({ + situationGraph: graph, + answer: + "I'd normally avoid more risk, but for the right opportunity I might accept some.", + previousQuestion: + "Is avoiding additional risk a hard constraint or a preference/trade-off?", + proposal: { + addedNodes: [], + updatedNodes: [ + { + nodeId: riskUnknownId, + previousStatus: "unknown", + newStatus: "resolved", + previousValue: null, + newValue: + "Avoiding additional risk is a preference or trade-off rather than a hard constraint.", + reason: + "The answer shows a preference or trade-off rather than a hard constraint.", + }, + ], + addedEdges: [], + removedEdgeIds: [], + resolvedUnknownNodeIds: [riskUnknownId], + affectedNodeIds: [], + selectedQuestion: null, + answerMeaning: { + userSupportedMeaning: + "The user would normally avoid more risk, but for the right opportunity might accept some.", + possibleInference: + "This may support eventual clarification, but the qualifying condition remains material.", + supportCategory: "conditional_tradeoff", + resolutionGuidance: "may_resolve", + }, + }, + }); + + expect(result.success).toBe(false); + expect(result.stage).toBe("proposal_compatibility"); + expect(result.errors.join(" ")).toContain("conditional qualification"); + }); + it("Regression C: rejects unresolved uncertainty being treated as resolved", () => { const { graph, riskUnknownId } = makeRiskClarificationFixture(); diff --git a/tests/graph/update-proposal.test.js b/tests/graph/update-proposal.test.js index a526a44..82d2242 100644 --- a/tests/graph/update-proposal.test.js +++ b/tests/graph/update-proposal.test.js @@ -101,6 +101,33 @@ describe("parseGraphUpdateProposal", () => { expect(result.proposal.addedNodes[0].id).toBe("n-new"); }); + it("normalises equivalent answerMeaning supportCategory labels to the canonical contract", () => { + const result = parseGraphUpdateProposal({ + ...makeValidProposal(), + answerMeaning: { + userSupportedMeaning: + "I'd normally avoid more risk, but for the right opportunity I might accept some.", + possibleInference: + "This may support later clarification, but the condition remains material.", + supportCategory: "conditional_qualification", + resolutionGuidance: "may_resolve", + }, + }); + + expect(result.success).toBe(true); + expect(result.proposal.answerMeaning.supportCategory).toBe( + "conditional_tradeoff", + ); + expect(result.normalisationsApplied).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + path: ["answerMeaning", "supportCategory"], + change: "Converted conditional_qualification to conditional_tradeoff", + }), + ]), + ); + }); + it("unknown enum values still fail", () => { const result = parseGraphUpdateProposal({ ...makeValidProposal(), @@ -125,6 +152,27 @@ describe("parseGraphUpdateProposal", () => { expect(result.success).toBe(false); }); + it("still rejects unsupported answerMeaning supportCategory labels", () => { + const result = parseGraphUpdateProposal({ + ...makeValidProposal(), + answerMeaning: { + userSupportedMeaning: "Risk matters more to me.", + possibleInference: null, + supportCategory: "constraint_preference_mix", + resolutionGuidance: "must_remain_unresolved", + }, + }); + + expect(result.success).toBe(false); + expect(result.errors).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + path: ["answerMeaning", "supportCategory"], + }), + ]), + ); + }); + it("defaults missing selectedQuestion to null", () => { const result = parseGraphUpdateProposal({ addedNodes: [],