reasoning: use structured semantic fidelity contract
This commit is contained in:
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -122,12 +122,36 @@ describe("buildGraphUpdatePrompt", () => {
|
||||
"Put any stronger interpretation in answerMeaning.possibleInference",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"supportCategory and resolutionGuidance are optional descriptive hints only",
|
||||
"populate supportCategory with one of the allowed values whenever the user's meaning fits an existing category",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"leave them null rather than inventing rigid category labels",
|
||||
"Do not leave supportCategory null merely because the wording is uncertain",
|
||||
);
|
||||
});
|
||||
|
||||
it("lists allowed structured semantic enum values", () => {
|
||||
const prompt = buildGraphUpdatePrompt(makeContext());
|
||||
|
||||
expect(prompt).toContain("## Allowed answerMeaning.supportCategory Values");
|
||||
expect(prompt).toContain(
|
||||
"relative_priority_only | conditional_tradeoff | uncertain | explicit_hard_constraint | other",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"must_remain_unresolved | may_resolve | must_resolve",
|
||||
);
|
||||
});
|
||||
|
||||
it("strengthens resolutionGuidance population guidance without provider-specific wording", () => {
|
||||
const prompt = buildGraphUpdatePrompt(makeContext());
|
||||
|
||||
expect(prompt).toContain(
|
||||
"Populate resolutionGuidance when the user's meaning genuinely implies must_remain_unresolved, may_resolve, or must_resolve",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"Keep it null only when no existing resolution state actually applies",
|
||||
);
|
||||
expect(prompt).not.toContain("qwen");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Semantic-to-mutation contract (57J.39) ──────────────
|
||||
@@ -135,7 +159,9 @@ describe("buildGraphUpdatePrompt", () => {
|
||||
describe("buildGraphUpdatePrompt — semantic-to-mutation MUST rule", () => {
|
||||
it("contains the explicit structural-materialization MUST rule", () => {
|
||||
const prompt = buildGraphUpdatePrompt(makeContext());
|
||||
expect(prompt).toContain("MUST express its effect through structural mutation");
|
||||
expect(prompt).toContain(
|
||||
"MUST express its effect through structural mutation",
|
||||
);
|
||||
});
|
||||
|
||||
it("rule permits update/refine of existing structure", () => {
|
||||
@@ -162,9 +188,7 @@ describe("buildGraphUpdatePrompt — semantic-to-mutation MUST rule", () => {
|
||||
const prompt = buildGraphUpdatePrompt(makeContext());
|
||||
// The rule should be silent about forcing new nodes — this is preserved by existing rule #7.
|
||||
// Verify the MUST rule exists but doesn't contain "must add a new node" or similar.
|
||||
const mustRuleMatch = prompt.match(
|
||||
/6\..*?(?=\n7\.)/s,
|
||||
);
|
||||
const mustRuleMatch = prompt.match(/6\..*?(?=\n7\.)/s);
|
||||
expect(mustRuleMatch).not.toBe(null);
|
||||
expect(mustRuleMatch[0]).not.toContain("must add a new node");
|
||||
});
|
||||
@@ -172,9 +196,7 @@ describe("buildGraphUpdatePrompt — semantic-to-mutation MUST rule", () => {
|
||||
it("does not imply possibleInference alone triggers mutation", () => {
|
||||
const prompt = buildGraphUpdatePrompt(makeContext());
|
||||
// The rule must reference userSupportedMeaning specifically, not possibleInference as a trigger.
|
||||
const mustRuleMatch = prompt.match(
|
||||
/6\..*?(?=\n7\.)/s,
|
||||
);
|
||||
const mustRuleMatch = prompt.match(/6\..*?(?=\n7\.)/s);
|
||||
expect(mustRuleMatch[0]).toContain("userSupportedMeaning");
|
||||
});
|
||||
|
||||
@@ -234,7 +256,9 @@ describe("buildGraphUpdatePrompt — semantic-to-mutation MUST rule", () => {
|
||||
expect(prompt).toContain("duplicate unknowns");
|
||||
// Additional Guidance preference for update over add:
|
||||
const additionalGuidance = prompt.split("## Additional Guidance")[1];
|
||||
expect(additionalGuidance).toContain("prefer updatedNodes and resolvedUnknownNodeIds over creating duplicate nodes");
|
||||
expect(additionalGuidance).toContain(
|
||||
"prefer updatedNodes and resolvedUnknownNodeIds over creating duplicate nodes",
|
||||
);
|
||||
});
|
||||
|
||||
it("PASS: update/refine route preserved — no new mandatory-add requirement", () => {
|
||||
@@ -244,8 +268,12 @@ describe("buildGraphUpdatePrompt — semantic-to-mutation MUST rule", () => {
|
||||
expect(prompt).toContain("update/refinement of existing structure");
|
||||
// Additional guidance still encourages preferring updates:
|
||||
const additionalGuidance = prompt.split("## Additional Guidance")[1];
|
||||
expect(additionalGuidance).toContain("prefer updatedNodes and resolvedUnknownNodeIds over creating duplicate nodes");
|
||||
expect(additionalGuidance).toContain("update that node rather than creating only a parallel observation");
|
||||
expect(additionalGuidance).toContain(
|
||||
"prefer updatedNodes and resolvedUnknownNodeIds over creating duplicate nodes",
|
||||
);
|
||||
expect(additionalGuidance).toContain(
|
||||
"update that node rather than creating only a parallel observation",
|
||||
);
|
||||
});
|
||||
|
||||
it("PASS: possibleInference separation preserved — not converted to mandatory mutation", () => {
|
||||
@@ -263,7 +291,9 @@ describe("buildGraphUpdatePrompt — semantic-to-mutation MUST rule", () => {
|
||||
expect(additionalGuidance).not.toContain("possibleInference");
|
||||
|
||||
// Rule #27 exists in the prompt (separation preserved):
|
||||
expect(fullPrompt).toContain("answerMeaning.possibleInference, not in userSupportedMeaning");
|
||||
expect(fullPrompt).toContain(
|
||||
"answerMeaning.possibleInference, not in userSupportedMeaning",
|
||||
);
|
||||
});
|
||||
|
||||
it("PASS: no action-selection machinery added — no keyword routing or node-kind decision table", () => {
|
||||
@@ -293,7 +323,9 @@ describe("buildGraphUpdatePrompt — 57J.46 existing-first uncertainty fallback"
|
||||
it("test 1: assembled prompt explicitly says to check for an equivalent unresolved unknown first", () => {
|
||||
const prompt = buildGraphUpdatePrompt(makeContext());
|
||||
const guidance = getAdditionalGuidance(prompt);
|
||||
expect(guidance).toContain("first check whether an existing unresolved node");
|
||||
expect(guidance).toContain(
|
||||
"first check whether an existing unresolved node",
|
||||
);
|
||||
expect(guidance).toContain("represents the same uncertainty");
|
||||
});
|
||||
|
||||
@@ -310,7 +342,9 @@ describe("buildGraphUpdatePrompt — 57J.46 existing-first uncertainty fallback"
|
||||
const prompt = buildGraphUpdatePrompt(makeContext());
|
||||
const guidance = getAdditionalGuidance(prompt);
|
||||
expect(guidance).toContain("if no such node exists");
|
||||
expect(guidance).toContain("add a new unknown that directly represents the unresolved uncertainty");
|
||||
expect(guidance).toContain(
|
||||
"add a new unknown that directly represents the unresolved uncertainty",
|
||||
);
|
||||
});
|
||||
|
||||
// Test 4 — ordered fallback means: existing first, otherwise add
|
||||
@@ -361,9 +395,13 @@ describe("buildGraphUpdatePrompt — 57J.46 existing-first uncertainty fallback"
|
||||
expect(prompt).toContain(
|
||||
"include that existing node ID in resolvedUnknownNodeIds",
|
||||
);
|
||||
expect(prompt).toContain("update that node rather than creating only a parallel observation");
|
||||
expect(prompt).toContain(
|
||||
"update that node rather than creating only a parallel observation",
|
||||
);
|
||||
// Rule #5 (resolve answered unknown first) must still exist:
|
||||
expect(prompt).toContain("Resolve the answered unknown first when the answer supports it");
|
||||
expect(prompt).toContain(
|
||||
"Resolve the answered unknown first when the answer supports it",
|
||||
);
|
||||
});
|
||||
|
||||
// Test 9 — duplicate validator/contract preserved
|
||||
@@ -372,7 +410,9 @@ describe("buildGraphUpdatePrompt — 57J.46 existing-first uncertainty fallback"
|
||||
expect(prompt).toContain("Do not add duplicate unknowns");
|
||||
expect(prompt).toContain("genuinely new concepts");
|
||||
const guidance = getAdditionalGuidance(prompt);
|
||||
expect(guidance).toContain("prefer updatedNodes and resolvedUnknownNodeIds over creating duplicate nodes");
|
||||
expect(guidance).toContain(
|
||||
"prefer updatedNodes and resolvedUnknownNodeIds over creating duplicate nodes",
|
||||
);
|
||||
});
|
||||
|
||||
// Test 10 — scope remains uncertainty-only (not universal to all categories)
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
SituationStatus,
|
||||
ConfidenceLevel,
|
||||
SituationRelationship,
|
||||
answerResolutionGuidance,
|
||||
answerSupportCategory,
|
||||
situationNodeSchema,
|
||||
situationEdgeSchema,
|
||||
situationGraphSchema,
|
||||
@@ -212,13 +214,39 @@ describe("graphUpdateSchema", () => {
|
||||
userSupportedMeaning: "Risk matters more to me.",
|
||||
possibleInference:
|
||||
"This may imply caution, but does not establish a hard constraint.",
|
||||
supportCategory: "relative priority only",
|
||||
resolutionGuidance: "leave unresolved",
|
||||
supportCategory: answerSupportCategory.relative_priority_only,
|
||||
resolutionGuidance: answerResolutionGuidance.must_remain_unresolved,
|
||||
},
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects invalid supportCategory values", () => {
|
||||
const result = graphUpdateSchema.safeParse({
|
||||
answerMeaning: {
|
||||
userSupportedMeaning: "Risk matters more to me.",
|
||||
possibleInference: null,
|
||||
supportCategory: "relative priority only",
|
||||
resolutionGuidance: answerResolutionGuidance.must_remain_unresolved,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects invalid resolutionGuidance values", () => {
|
||||
const result = graphUpdateSchema.safeParse({
|
||||
answerMeaning: {
|
||||
userSupportedMeaning: "Risk matters more to me.",
|
||||
possibleInference: null,
|
||||
supportCategory: answerSupportCategory.relative_priority_only,
|
||||
resolutionGuidance: "leave unresolved",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects update with invalid node kind in addedNodes", () => {
|
||||
const invalid = graphUpdateSchema.safeParse({
|
||||
addedNodes: [
|
||||
|
||||
@@ -104,7 +104,7 @@ describe("parseGraphUpdateProposal", () => {
|
||||
expect(result.proposal.addedNodes[0].id).toBe("n-new");
|
||||
});
|
||||
|
||||
it("accepts live-style free-text answerMeaning hints without requiring enum tokens", () => {
|
||||
it("rejects supportCategory values outside the existing enum", () => {
|
||||
const result = parseGraphUpdateProposal({
|
||||
...makeValidProposal(),
|
||||
answerMeaning: {
|
||||
@@ -118,15 +118,10 @@ describe("parseGraphUpdateProposal", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.proposal.answerMeaning).toMatchObject({
|
||||
supportCategory: "conditional_preference",
|
||||
resolutionGuidance:
|
||||
"Identify and quantify the threshold conditions that trigger risk acceptance.",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts the earlier live Regression B wording variant without special aliasing", () => {
|
||||
it("rejects the earlier live Regression B wording variant when it is outside the existing enum", () => {
|
||||
const result = parseGraphUpdateProposal({
|
||||
...makeValidProposal(),
|
||||
answerMeaning: {
|
||||
@@ -139,10 +134,26 @@ describe("parseGraphUpdateProposal", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts valid structured answerMeaning enum values", () => {
|
||||
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_tradeoff",
|
||||
resolutionGuidance: "may_resolve",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.proposal.answerMeaning).toMatchObject({
|
||||
supportCategory: "conditional_qualification",
|
||||
resolutionGuidance: "may resolve once the condition is clarified",
|
||||
supportCategory: "conditional_tradeoff",
|
||||
resolutionGuidance: "may_resolve",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -289,7 +300,7 @@ describe("parseGraphUpdateProposal", () => {
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("does not reject unsupported free-text answerMeaning labels at parse time", () => {
|
||||
it("rejects unsupported answerMeaning enum labels at parse time", () => {
|
||||
const result = parseGraphUpdateProposal({
|
||||
...makeValidProposal(),
|
||||
answerMeaning: {
|
||||
@@ -300,11 +311,7 @@ describe("parseGraphUpdateProposal", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.proposal.answerMeaning).toMatchObject({
|
||||
supportCategory: "constraint_preference_mix",
|
||||
resolutionGuidance: "needs more nuance",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults missing selectedQuestion to null", () => {
|
||||
|
||||
Reference in New Issue
Block a user