reasoning: add proposal-level answer meaning guard

- Add answerMeaning schema with supportCategory and resolutionGuidance enums
- Add pre-mutation guard that validates proposal alignment with answerMeaning
- Update prompt builder to instruct the model on answerMeaning contract
- Add tests for schema, guard logic, parsing defaults, and regression cases A-D
This commit is contained in:
2026-08-09 07:07:21 +01:00
parent 8c1036ecd0
commit 0d7ad5775c
7 changed files with 470 additions and 0 deletions
+134
View File
@@ -2682,6 +2682,137 @@ function answerConfirmsComparability(answer) {
);
}
function normaliseSemanticText(value) {
return String(value || "")
.toLowerCase()
.replace(/\s+/g, " ")
.trim();
}
function hasConditionalQualification(text) {
const value = normaliseSemanticText(text);
return (
value.includes("might") ||
value.includes("normally") ||
value.includes("for the right opportunity") ||
value.includes("depends") ||
value.includes("conditional") ||
value.includes("under specific")
);
}
function containsConstraintBoundaryLanguage(text) {
const value = normaliseSemanticText(text);
return (
value.includes("hard constraint") ||
value.includes("constraint") ||
value.includes("non negotiable") ||
value.includes("non-negotiable") ||
value.includes("preference") ||
value.includes("trade off") ||
value.includes("trade-off")
);
}
function containsWeakeningOfHardConstraint(text) {
const value = normaliseSemanticText(text);
return (
value.includes("preference") ||
value.includes("trade off") ||
value.includes("trade-off") ||
value.includes("not a hard constraint") ||
value.includes("rather than a hard constraint")
);
}
function proposalResolutionSummary(proposal) {
const resolved =
proposal.resolvedUnknownNodeIds.length > 0 ||
proposal.updatedNodes.some((update) => update.newStatus === "resolved");
const proposalText = normaliseSemanticText(
[
...(proposal.updatedNodes || []).flatMap((update) => [
update.reason,
update.newValue,
]),
proposal.selectedQuestion?.question,
proposal.selectedQuestion?.reason,
]
.filter(Boolean)
.join(" "),
);
return { resolved, proposalText };
}
function validateAnswerMeaningAlignment(proposal) {
if (!proposal.answerMeaning) return [];
const errors = [];
const { userSupportedMeaning, supportCategory, resolutionGuidance } =
proposal.answerMeaning;
const meaningText = normaliseSemanticText(userSupportedMeaning);
const { resolved, proposalText } = proposalResolutionSummary(proposal);
if (resolutionGuidance === "must_remain_unresolved" && resolved) {
errors.push(
"Proposal resolves an unknown even though answerMeaning says the user's answer must remain unresolved.",
);
}
if (supportCategory === "relative_priority_only") {
if (containsConstraintBoundaryLanguage(meaningText)) {
errors.push(
"answerMeaning.userSupportedMeaning for relative_priority_only must not introduce a constraint or preference/trade-off judgement.",
);
}
if (containsConstraintBoundaryLanguage(proposalText)) {
errors.push(
"Proposal introduces unsupported constraint-boundary interpretation from a relative-priority answer.",
);
}
}
if (supportCategory === "conditional_tradeoff") {
if (!hasConditionalQualification(meaningText)) {
errors.push(
"answerMeaning.userSupportedMeaning for conditional_tradeoff must preserve the user's qualification or condition.",
);
}
if (resolved && !hasConditionalQualification(proposalText)) {
errors.push(
"Proposal resolves a conditional trade-off without preserving its conditional qualification in the proposed change.",
);
}
}
if (supportCategory === "uncertain") {
if (containsConstraintBoundaryLanguage(proposalText)) {
errors.push(
"Proposal introduces a stronger interpretation even though answerMeaning marks the answer as uncertain.",
);
}
}
if (supportCategory === "explicit_hard_constraint") {
if (!resolved && resolutionGuidance === "must_resolve") {
errors.push(
"Proposal leaves an explicitly stated hard constraint unresolved.",
);
}
if (containsWeakeningOfHardConstraint(proposalText)) {
errors.push(
"Proposal weakens an explicitly stated hard constraint into a preference or trade-off.",
);
}
}
return errors;
}
function deriveReasoningStateOverride({
graph,
previousQuestion,
@@ -2853,6 +2984,9 @@ export function applyValidatedProposal({
validatedProposal,
);
proposalCompatibilityErrors.push(...selectedQuestionValidation.errors);
proposalCompatibilityErrors.push(
...validateAnswerMeaningAlignment(validatedProposal),
);
proposalCompatibilityErrors.push(
...validateQuestionSelectionRequirement(situationGraph, validatedProposal),
);