reasoning: require structural progress for supported meaning

This commit is contained in:
2026-08-11 12:32:03 +01:00
parent 3b868b266e
commit 6adcd817e1
4 changed files with 244 additions and 2 deletions
+185
View File
@@ -1155,3 +1155,188 @@ describe("update lifecycle integration", () => {
expect(graph.nodes[0].status).toBe("unknown"); // unchanged
});
});
// ── Semantic-to-mutation contract (57J.39) ──────────────
describe("semantic-to-mutation contract", () => {
const baseUpdate = {
addedNodes: [],
updatedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
};
// Test 1 — semantic-only no-op
it("REJECTS with specific semantic-only structural-progress error when userSupportedMeaning populated and zero structural mutation", () => {
const graph = makeTestGraph();
const update = {
...baseUpdate,
answerMeaning: {
userSupportedMeaning: "The user states that cost reduction is a primary driver.",
possibleInference: null,
},
};
const result = validateGraphUpdate(graph, update);
expect(result.valid).toBe(false);
expect(
result.errors.some((e) => e.includes("userSupportedMeaning") && e.includes("mutation")),
).toBe(true);
// Should NOT contain only the generic no-op message without the semantic-specific variant
expect(result.errors.some((e) => e === "Update contains no meaningful change")).toBe(false);
});
// Test 2 — ordinary no-op (answerMeaning null)
it("REJECTS with existing 'no meaningful change' when answerMeaning is null and zero structural mutation", () => {
const graph = makeTestGraph();
const update = {
...baseUpdate,
answerMeaning: null,
};
const result = validateGraphUpdate(graph, update);
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.includes("no meaningful"))).toBe(true);
});
// Test 3 — possibleInference only
it("does NOT trigger the new userSupportedMeaning-specific error when only possibleInference is populated", () => {
const graph = makeTestGraph();
const update = {
...baseUpdate,
answerMeaning: {
userSupportedMeaning: null,
possibleInference: "Cost reduction could be achieved through staff consolidation.",
},
};
const result = validateGraphUpdate(graph, update);
expect(result.valid).toBe(false);
// Must NOT have the semantic-specific error (userSupportedMeaning is not populated)
expect(
result.errors.some((e) => e.includes("userSupportedMeaning") && e.includes("mutation")),
).toBe(false);
// Generic no-op still applies
expect(result.errors.some((e) => e.includes("no meaningful"))).toBe(true);
});
// Test 4 — update existing structure (valid structural progress)
it("ACCEPTS past new guard when userSupportedMeaning populated AND valid existing-node status change", () => {
const graph = makeTestGraph();
const update = {
...baseUpdate,
answerMeaning: {
userSupportedMeaning: "The user confirms risk is a hard constraint.",
possibleInference: null,
},
updatedNodes: [
{
nodeId: "n4",
previousStatus: "unknown",
newStatus: "known",
reason: "Confirmed by user answer",
},
],
};
const result = validateGraphUpdate(graph, update);
expect(result.valid).toBe(true);
expect(result.errors.length).toBe(0);
});
// Test 5 — resolve existing unknown (counts as structural progress)
it("counts as structural progress when userSupportedMeaning populated AND valid resolution of existing unknown", () => {
const graph = makeTestGraph();
const update = {
...baseUpdate,
answerMeaning: {
userSupportedMeaning: "The user provides criteria for acceptable opportunity.",
possibleInference: null,
},
updatedNodes: [
{
nodeId: "n4",
previousStatus: "unknown",
newStatus: "resolved",
reason: "Threshold defined by user",
},
],
resolvedUnknownNodeIds: ["n4"],
};
const result = validateGraphUpdate(graph, update);
expect(result.valid).toBe(true);
expect(result.errors.length).toBe(0);
});
// Test 6 — add new structure (counts as structural progress)
it("counts as structural progress when userSupportedMeaning populated AND valid added unknown", () => {
const graph = makeTestGraph();
const update = {
...baseUpdate,
answerMeaning: {
userSupportedMeaning: "The user needs evidence for both savings realism and retention impact.",
possibleInference: null,
},
addedNodes: [makeNode({ id: "n-new-unknown", label: "New unknown" })],
};
const result = validateGraphUpdate(graph, update);
expect(result.valid).toBe(true);
expect(result.errors.length).toBe(0);
});
// Test 7 — duplicate avoidance preserved
it("still rejects duplicate node IDs even with populated userSupportedMeaning", () => {
const graph = makeTestGraph();
const existingNode = graph.nodes[0];
const update = {
...baseUpdate,
answerMeaning: {
userSupportedMeaning: "There is a new constraint the user identified.",
possibleInference: null,
},
addedNodes: [existingNode], // Duplicate ID — should still be rejected
};
const result = validateGraphUpdate(graph, update);
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.includes("duplicate ID"))).toBe(true);
});
// Test 8 — userSupportedMeaning with meaningful value change (no status change)
it("counts as structural progress when userSupportedMeaning populated AND valid existing-node value change", () => {
const graph = makeTestGraph();
const update = {
...baseUpdate,
answerMeaning: {
userSupportedMeaning: "The user clarified the constraint is absolute.",
possibleInference: null,
},
updatedNodes: [
{
nodeId: "n4",
previousStatus: null,
newStatus: null,
previousValue: null,
newValue: "absolute_constraint",
reason: "Clarified by user answer",
},
],
};
const result = validateGraphUpdate(graph, update);
expect(result.valid).toBe(true);
expect(result.errors.length).toBe(0);
});
});