diff --git a/lib/graph/apply-proposal.js b/lib/graph/apply-proposal.js index c2e565d..2235811 100644 --- a/lib/graph/apply-proposal.js +++ b/lib/graph/apply-proposal.js @@ -2745,6 +2745,46 @@ function proposalResolutionSummary(proposal) { return { resolved, proposalText }; } +function mentionsHardConstraint(text) { + return ( + text.includes("hard constraint") || + text.includes("non-negotiable") || + text.includes("dont want any increase in risk") || + text.includes("do not want any increase in risk") + ); +} + +function mentionsNegatedHardConstraint(text) { + return ( + text.includes("rather than a hard constraint") || + text.includes("not a hard constraint") || + text.includes("not an absolute constraint") || + text.includes("preference rather than a hard constraint") + ); +} + +function hasDefaultPreferenceSignal(text) { + return ( + text.includes("preference") || + text.includes("normally") || + text.includes("default preference") || + text.includes("would usually") || + text.includes("tend to") + ); +} + +function hasExceptionOrOverrideSignal(text) { + return ( + text.includes(" but ") || + text.includes(" if ") || + text.includes("override") || + text.includes("overridden") || + text.includes("willing to accept") || + text.includes("willingness to accept") || + text.includes("accept some risk") + ); +} + function deriveAnswerMeaningProfile(userSupportedMeaning) { const meaningText = normaliseSemanticText(userSupportedMeaning); @@ -2761,25 +2801,29 @@ function deriveAnswerMeaningProfile(userSupportedMeaning) { }; } - if ( - meaningText.includes("hard constraint") || - meaningText.includes("non-negotiable") || - meaningText.includes("dont want any increase in risk") || - meaningText.includes("do not want any increase in risk") - ) { - return { - category: "explicit_hard_constraint", - resolutionGuidance: "must_resolve", - }; - } + const negatedHardConstraint = mentionsNegatedHardConstraint(meaningText); + const affirmativeHardConstraint = + mentionsHardConstraint(meaningText) && !negatedHardConstraint; + const conditionalPreferenceStructure = + (hasDefaultPreferenceSignal(meaningText) && + hasExceptionOrOverrideSignal(meaningText)) || + (negatedHardConstraint && hasExceptionOrOverrideSignal(meaningText)) || + hasConditionalQualification(meaningText); - if (hasConditionalQualification(meaningText)) { + if (conditionalPreferenceStructure) { return { category: "conditional_tradeoff", resolutionGuidance: "may_resolve", }; } + if (affirmativeHardConstraint) { + return { + category: "explicit_hard_constraint", + resolutionGuidance: "must_resolve", + }; + } + if ( meaningText.includes("matters more") || meaningText.includes("more important") || diff --git a/tests/graph/apply-proposal.test.js b/tests/graph/apply-proposal.test.js index a05d55e..00d5981 100644 --- a/tests/graph/apply-proposal.test.js +++ b/tests/graph/apply-proposal.test.js @@ -1212,6 +1212,100 @@ describe("applyValidatedProposal", () => { expect(result.errors.join(" ")).toContain("conditional qualification"); }); + it("B live-variant 1: negated hard-constraint mention stays conditional rather than explicit hard constraint", () => { + 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: + "Avoiding additional risk is a strong preference rather than a hard constraint, with willingness to accept some risk if the opportunity is sufficiently compelling.", + possibleInference: + "The exact threshold for a sufficiently compelling opportunity remains undefined.", + supportCategory: "conditional_preference", + resolutionGuidance: + "Identify and quantify the threshold conditions that trigger risk acceptance.", + }, + }, + }); + + expect(result.success).toBe(false); + expect(result.stage).toBe("proposal_compatibility"); + expect(result.errors.join(" ")).toContain("conditional qualification"); + expect(result.errors.join(" ")).not.toContain( + "explicitly stated hard constraint", + ); + }); + + it("B live-variant 2: default preference plus override stays conditional rather than other", () => { + 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: + "Avoiding additional risk is a default preference, but it can be overridden for sufficiently compelling opportunities.", + possibleInference: "The exact override threshold remains undefined.", + supportCategory: "conditional_preference", + resolutionGuidance: "needs more nuance", + }, + }, + }); + + expect(result.success).toBe(false); + expect(result.stage).toBe("proposal_compatibility"); + expect(result.errors.join(" ")).toContain("conditional qualification"); + expect(result.errors.join(" ")).not.toContain( + "does not clearly establish one of the protected reasoning categories", + ); + }); + it("Regression C: rejects unresolved uncertainty being treated as resolved", () => { const { graph, riskUnknownId } = makeRiskClarificationFixture(); @@ -1341,6 +1435,52 @@ describe("applyValidatedProposal", () => { ); }); + it("negation safety: mentioning hard constraint in a negated comparison does not become affirmative hard constraint", () => { + 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: + "Avoiding additional risk is a strong preference rather than a hard constraint.", + possibleInference: + "This indicates flexibility rather than an absolute prohibition.", + supportCategory: "conditional_preference", + resolutionGuidance: "needs more nuance", + }, + }, + }); + + expect(result.success).toBe(false); + expect(result.stage).toBe("proposal_compatibility"); + expect(result.errors.join(" ")).not.toContain( + "weakens an explicitly stated hard constraint", + ); + }); + it("fails safely when answerMeaning does not clearly establish one of the protected categories", () => { const { graph, riskUnknownId } = makeRiskClarificationFixture();