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
+1 -1
View File
@@ -92,7 +92,7 @@ The JSON object must contain exactly these top-level fields:
3. Reference existing node IDs when updating an existing concept.
4. Use addedNodes only for genuinely new concepts.
5. Resolve the answered unknown first when the answer supports it.
6. Then inspect the answer for newly introduced consequential uncertainty.
6. If answerMeaning.userSupportedMeaning contains consequential information or unresolved uncertainty that is not already represented in the graph, you MUST express its effect through structural mutation. This may be an update/refinement of existing structure, resolution of an existing unknown, a genuinely new unknown, or a justified relationship. answerMeaning alone is not sufficient for a successful proposal.
7. Add new unknown nodes only when the answer introduces a new decision, claim, object, measure, dependency, or unresolved term directly relevant to the case.
8. Add at most 3 new unknown nodes.
9. Every new unknown must be directly traceable to the user's answer and its description must state why that uncertainty matters.
+8
View File
@@ -881,8 +881,16 @@ export function validateGraphUpdate(graph, update) {
update.removedEdgeIds.length > 0;
if (!hasMeaningfulChange) {
// Specific diagnostic for the semantic-only no-op case: populated userSupportedMeaning with zero structural mutation.
// The validator does NOT determine whether meaning is "new" or "consequential" — it only observes that meaning exists without structural expression.
if (update.answerMeaning?.userSupportedMeaning) {
errors.push(
"answerMeaning.userSupportedMeaning is populated, but the proposal contains no graph mutation. answerMeaning alone does not constitute graph progress.",
);
} else {
errors.push("Update contains no meaningful change");
}
}
// Reject oversized input
const totalSize = JSON.stringify(update).length;
+49
View File
@@ -129,3 +129,52 @@ describe("buildGraphUpdatePrompt", () => {
);
});
});
// ── Semantic-to-mutation contract (57J.39) ──────────────
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");
});
it("rule permits update/refine of existing structure", () => {
const prompt = buildGraphUpdatePrompt(makeContext());
expect(prompt).toContain("update/refinement of existing structure");
});
it("rule permits resolving an existing unknown", () => {
const prompt = buildGraphUpdatePrompt(makeContext());
expect(prompt).toContain("resolution of an existing unknown");
});
it("rule permits genuinely new unknown when needed", () => {
const prompt = buildGraphUpdatePrompt(makeContext());
expect(prompt).toContain("a genuinely new unknown");
});
it("rule explicitly states answerMeaning alone is not sufficient", () => {
const prompt = buildGraphUpdatePrompt(makeContext());
expect(prompt).toContain("answerMeaning alone is not sufficient");
});
it("rule does NOT force adding a new node", () => {
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,
);
expect(mustRuleMatch).not.toBe(null);
expect(mustRuleMatch[0]).not.toContain("must add a new node");
});
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,
);
expect(mustRuleMatch[0]).toContain("userSupportedMeaning");
});
});
+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);
});
});