reasoning: require structural progress for supported meaning
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user