reasoning: use structured semantic fidelity contract

This commit is contained in:
2026-08-11 16:45:00 +01:00
parent b6a232ff6f
commit 7d06cd3c47
7 changed files with 630 additions and 97 deletions
+427 -51
View File
@@ -1161,8 +1161,8 @@ describe("applyValidatedProposal", () => {
"Risk is of greater relative importance than growth.",
possibleInference:
"This may imply caution, but does not establish whether risk is a hard constraint.",
supportCategory: "relative_priority_only",
resolutionGuidance: "must_remain_unresolved",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -1204,8 +1204,8 @@ describe("applyValidatedProposal", () => {
"Avoiding additional risk is a preference/trade-off rather than a hard constraint.",
possibleInference:
"The user prioritizes risk mitigation over aggressive growth strategies.",
supportCategory: "relative_priority_only",
resolutionGuidance: "must_remain_unresolved",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -1253,8 +1253,8 @@ describe("applyValidatedProposal", () => {
"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",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -1264,7 +1264,7 @@ describe("applyValidatedProposal", () => {
expect(result.errors.join(" ")).toContain("conditional qualification");
});
it("accepts the Experiment 56A supportCategory wording variant and still applies the existing Regression B guard", () => {
it("rejects the Experiment 56A supportCategory wording variant at the schema boundary", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
@@ -1305,10 +1305,10 @@ describe("applyValidatedProposal", () => {
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain("conditional qualification");
expect(result.errors.join(" ")).toContain("Invalid enum value");
});
it("accepts the Experiment 56B live wording variants and still applies the existing Regression B guard", () => {
it("rejects the Experiment 56B live wording variants at the schema boundary", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
@@ -1350,10 +1350,10 @@ describe("applyValidatedProposal", () => {
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain("conditional qualification");
expect(result.errors.join(" ")).toContain("Invalid enum value");
});
it("B live-variant 1: negated hard-constraint mention stays conditional rather than explicit hard constraint", () => {
it("B live-variant 1: invalid live wording is rejected before semantic validation", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
@@ -1395,13 +1395,13 @@ describe("applyValidatedProposal", () => {
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain("conditional qualification");
expect(result.errors.join(" ")).toContain("Invalid enum value");
expect(result.errors.join(" ")).not.toContain(
"explicitly stated hard constraint",
);
});
it("B live-variant 2: default preference plus override stays conditional rather than other", () => {
it("B live-variant 2: invalid free-text structured hints are rejected before semantic validation", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
@@ -1441,10 +1441,7 @@ describe("applyValidatedProposal", () => {
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",
);
expect(result.errors.join(" ")).toContain("Invalid enum value");
});
it("Regression B: preserves conditional trade-off when userSupportedMeaning stays within the raw answer", () => {
@@ -1555,8 +1552,8 @@ describe("applyValidatedProposal", () => {
"Avoiding additional risk is a preference rather than a hard constraint.",
possibleInference:
"The user may be signaling caution and a willingness to trade off growth for lower risk.",
supportCategory: "other",
resolutionGuidance: "may_resolve",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -1717,7 +1714,7 @@ describe("applyValidatedProposal", () => {
"There is proven broad market demand for a product that delivers these savings.",
possibleInference:
"The savings target could imply broader applicability if others share similar overhead pressures.",
supportCategory: "other",
supportCategory: null,
resolutionGuidance: null,
},
},
@@ -1761,8 +1758,8 @@ describe("applyValidatedProposal", () => {
userSupportedMeaning:
"The user is not sure whether avoiding additional risk is a hard constraint or a preference/trade-off.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "must_remain_unresolved",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -1803,8 +1800,8 @@ describe("applyValidatedProposal", () => {
userSupportedMeaning:
"Avoiding additional risk is a hard constraint and the user does not want any increase in risk.",
possibleInference: null,
supportCategory: "explicit_hard_constraint",
resolutionGuidance: "must_resolve",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -1905,7 +1902,7 @@ describe("applyValidatedProposal", () => {
);
});
it("fails safely when answerMeaning does not clearly establish one of the protected categories", () => {
it("fails at the schema boundary when answerMeaning uses unsupported structured values", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
@@ -1944,9 +1941,7 @@ describe("applyValidatedProposal", () => {
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain(
"unsupported stronger meaning than answerMeaning.userSupportedMeaning establishes",
);
expect(result.errors.join(" ")).toContain("Invalid enum value");
});
it("active unknown matches selected question node", () => {
@@ -2566,7 +2561,8 @@ describe("applyValidatedProposal", () => {
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "The user needs evidence for projected office savings realism.",
newValue:
"The user needs evidence for projected office savings realism.",
reason: "The answer directs focus to savings realism evidence.",
},
],
@@ -2583,8 +2579,8 @@ describe("applyValidatedProposal", () => {
userSupportedMeaning:
"I need evidence that projected office savings are realistic.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "may_resolve",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -2601,7 +2597,8 @@ describe("applyValidatedProposal", () => {
// 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.",
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: {
@@ -2617,7 +2614,8 @@ describe("applyValidatedProposal", () => {
}),
makeNode({
id: "n-retention-impact",
label: "Whether the move materially increases loss of key engineers",
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",
@@ -2648,8 +2646,8 @@ describe("applyValidatedProposal", () => {
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",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -2664,7 +2662,8 @@ describe("applyValidatedProposal", () => {
const result = applyValidatedProposal({
situationGraph: graph,
answer: "We are looking at this mainly for cost reduction — roughly £2M annual savings on office overhead.",
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: {
@@ -2695,7 +2694,8 @@ describe("applyValidatedProposal", () => {
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-office-paint-colour",
question: "What evidence supports the office paint colour hypothesis?",
question:
"What evidence supports the office paint colour hypothesis?",
reason: "Unrelated unknown — should be rejected.",
},
answerMeaning: {
@@ -2730,7 +2730,8 @@ describe("applyValidatedProposal", () => {
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.",
description:
"Need to check whether workforce stability matters, because that could impact scheduling.",
kind: "unknown",
status: "unknown",
confidence: "medium",
@@ -2747,8 +2748,10 @@ describe("applyValidatedProposal", () => {
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.",
userSupportedMeaning:
"The project timeline is critical to the decision.",
possibleInference:
"Workforce stability may matter for scheduling and continuity.",
supportCategory: null,
resolutionGuidance: null,
},
@@ -2802,7 +2805,8 @@ describe("applyValidatedProposal", () => {
toNodeId: "n-savings-realism-5",
relationship: "depends_on",
confidence: "medium",
description: "Savings realism is a dependency of commercial justification.",
description:
"Savings realism is a dependency of commercial justification.",
}),
],
removedEdgeIds: [],
@@ -2817,8 +2821,8 @@ describe("applyValidatedProposal", () => {
userSupportedMeaning:
"We need evidence that projected office savings are realistic.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "may_resolve",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -2884,6 +2888,376 @@ describe("applyValidatedProposal", () => {
expect(result.success).toBe(true);
});
it("Structured fidelity: raw unsure + structured uncertain does not false-reject on populated structured path", () => {
const graph = makeCommercialUpdateFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I am unsure whether the projected office savings from the relocation are realistic.",
proposal: {
...makeMeaningfulNoOpProposal(),
addedNodes: [
makeNode({
id: "n-savings-anchor",
label: "Savings realism investigation context",
description:
"Answer-derived context for savings realism because the commercial decision depends on it.",
kind: "state",
status: "known",
confidence: "medium",
}),
makeNode({
id: "n-savings-realism",
label: "Whether the projected office savings are realistic",
description:
"Need to know whether the projected office savings are realistic because that matters to whether continuing development is commercially justified.",
kind: "unknown",
status: "unknown",
confidence: "medium",
}),
],
addedEdges: [
makeEdge({
id: "e-anchor-savings-realism",
fromNodeId: "n-savings-anchor",
toNodeId: "n-savings-realism",
relationship: "depends_on",
confidence: "medium",
description:
"The answer-derived savings context depends on savings realism.",
}),
],
selectedQuestion: {
nodeId: "n-savings-realism",
question:
"What evidence would clarify whether the projected office savings are realistic?",
reason:
"The structured uncertainty remains unresolved and is the next consequential unknown.",
},
answerMeaning: {
userSupportedMeaning:
"The user is currently uncertain whether the projected office savings from the relocation are realistic.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "must_remain_unresolved",
},
},
});
expect(result.success).toBe(true);
});
it("Structured fidelity: equivalent paraphrase wording does not change acceptance when structured category is populated", () => {
const graph = makeCommercialUpdateFixture();
const baseProposal = {
...makeMeaningfulNoOpProposal(),
addedNodes: [
makeNode({
id: "n-savings-anchor",
label: "Savings realism investigation context",
description:
"Answer-derived context for savings realism because the commercial decision depends on it.",
kind: "state",
status: "known",
confidence: "medium",
}),
makeNode({
id: "n-savings-realism",
label: "Whether the projected office savings are realistic",
description:
"Need to know whether the projected office savings are realistic because that matters to whether continuing development is commercially justified.",
kind: "unknown",
status: "unknown",
confidence: "medium",
}),
],
addedEdges: [
makeEdge({
id: "e-anchor-savings-realism",
fromNodeId: "n-savings-anchor",
toNodeId: "n-savings-realism",
relationship: "depends_on",
confidence: "medium",
description:
"The answer-derived savings context depends on savings realism.",
}),
],
selectedQuestion: {
nodeId: "n-savings-realism",
question:
"What evidence would clarify whether the projected office savings are realistic?",
reason:
"The structured uncertainty remains unresolved and is the next consequential unknown.",
},
};
const answers = [
"The user is currently uncertain whether the projected office savings from the relocation are realistic.",
"The user remains unclear whether the projected office savings from the relocation are realistic.",
];
for (const userSupportedMeaning of answers) {
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I am unsure whether the projected office savings from the relocation are realistic.",
proposal: {
...baseProposal,
answerMeaning: {
userSupportedMeaning,
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "must_remain_unresolved",
},
},
});
expect(result.success).toBe(true);
}
});
it("Structured fidelity: must_remain_unresolved rejects relevant resolution mutation", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I am not sure whether avoiding additional risk is a hard constraint.",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Avoiding additional risk is a hard constraint.",
reason:
"Incorrectly resolves an uncertainty that should remain unresolved.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"The user is not sure whether avoiding additional risk is a hard constraint or a preference/trade-off.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "must_remain_unresolved",
},
},
});
expect(result.success).toBe(false);
expect(result.errors.join(" ")).toContain(
"answerMeaning.resolutionGuidance is must_remain_unresolved",
);
});
it("Structured fidelity: null structured fields retain existing lexical fallback", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I am unsure whether avoiding additional risk is a hard constraint.",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Avoiding additional risk is a hard constraint.",
reason:
"Incorrectly resolves an uncertainty that should remain unresolved.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"The user is not sure whether avoiding additional risk is a hard constraint or a preference/trade-off.",
possibleInference: null,
supportCategory: null,
resolutionGuidance: null,
},
},
});
expect(result.success).toBe(false);
expect(result.errors.join(" ")).toContain("must remain unresolved");
});
it("Structured fidelity: populated conditional_tradeoff uses structured path without lexical verification", () => {
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.",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Avoiding additional risk is flexible under certain conditions.",
reason: "The answer provides a conditional trade-off.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"Avoiding additional risk is flexible under certain conditions.",
possibleInference: null,
supportCategory: "conditional_tradeoff",
resolutionGuidance: "may_resolve",
},
},
});
expect(result.success).toBe(true);
});
it("Structured fidelity: populated explicit_hard_constraint uses structured path without lexical verification", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer: "It's a hard constraint. I don't want any increase in risk.",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Avoiding additional risk is non-negotiable.",
reason: "The answer establishes a hard constraint.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning: "Avoiding additional risk is non-negotiable.",
possibleInference: null,
supportCategory: "explicit_hard_constraint",
resolutionGuidance: "must_resolve",
},
},
});
expect(result.success).toBe(true);
});
it("Structured fidelity: possibleInference remains non-authoritative", () => {
const graph = makeCommercialUpdateFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer: "The project timeline is critical to the decision.",
proposal: {
...makeMeaningfulNoOpProposal(),
addedNodes: [
makeNode({
id: "n-workforce-stability",
label: "Workforce stability impact",
description:
"Need to know workforce stability impact because that could affect delivery confidence.",
kind: "unknown",
status: "unknown",
confidence: "low",
}),
],
addedEdges: [
makeEdge({
id: "e-parent-workforce-stability",
fromNodeId: "n-commercial-parent",
toNodeId: "n-workforce-stability",
relationship: "depends_on",
confidence: "low",
description:
"Commercial justification depends on workforce stability.",
}),
],
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.errors.join(" ")).toContain(
"explicitly related to an answer-derived node",
);
});
it("Structured fidelity: no new synonym or keyword logic was added to lexical fallback", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I am unsure whether avoiding additional risk is 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 hard constraint.",
reason:
"Incorrectly resolves an uncertainty that should remain unresolved.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"The user is currently uncertain whether avoiding additional risk is a hard constraint or a preference/trade-off.",
possibleInference: null,
supportCategory: null,
resolutionGuidance: null,
},
},
});
expect(result.success).toBe(false);
expect(result.errors.join(" ")).toContain(
"overstates a raw answer that remains uncertain",
);
});
// ── Case 7 — no support and no linkage ──
it("Case 7: unknown with neither user support nor structural linkage is rejected", () => {
const graph = makeCommercialUpdateFixture();
@@ -2996,8 +3370,8 @@ describe("applyValidatedProposal", () => {
userSupportedMeaning:
"We need evidence that cost reduction is achievable through office overhead and savings.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "may_resolve",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -3059,8 +3433,8 @@ describe("applyValidatedProposal", () => {
userSupportedMeaning:
"We need cost reduction through office overhead savings.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "may_resolve",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -3180,7 +3554,8 @@ describe("applyValidatedProposal", () => {
toNodeId: "n-boundary-c",
relationship: "depends_on",
confidence: "medium",
description: "Parent depends on commercial justification verification.",
description:
"Parent depends on commercial justification verification.",
}),
],
removedEdgeIds: [],
@@ -3192,10 +3567,11 @@ describe("applyValidatedProposal", () => {
reason: "Consequential unknown verified by user-supported meaning.",
},
answerMeaning: {
userSupportedMeaning: "We are considering relocation for cost savings.",
userSupportedMeaning:
"We are considering relocation for cost savings.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "may_resolve",
supportCategory: null,
resolutionGuidance: null,
},
},
});