reasoning: require structural progress for supported meaning
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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