feat(graph): add structuralActionRequired contract (57J.67)
- Add structuralActionRequired field to graphUpdateSchema (optional boolean nullable) - Validate declaration consistency in validateGraphUpdate(): - true requires meaningful mutation (addedNodes/updatedNodes/addedEdges) - false permits intentional no-op when userSupportedMeaning populated - null/absent with meaning → reject - true/false mismatch on output shape → reject - preserve legacy no-op guard for non-contract paths - Update prompt-builder: add field to required list, insert contract section between rules and Additional Guidance with two mandatory sentences - 50 new tests: schema validation (4), prompt builder content checks (10), utils contract matrix (10), plus 26 existing suite migrations All 197 graph tests pass.
This commit is contained in:
+35
-9
@@ -865,7 +865,9 @@ export function validateGraphUpdate(graph, update) {
|
||||
}
|
||||
}
|
||||
|
||||
// Reject updates with no meaningful change
|
||||
// ── structuralActionRequired contract (57J.67) ───────────
|
||||
|
||||
const meaningPopulated = !!update.answerMeaning?.userSupportedMeaning;
|
||||
const statusChanged = update.updatedNodes.some(
|
||||
(u) => u.previousStatus !== null && u.newStatus !== u.previousStatus,
|
||||
);
|
||||
@@ -880,14 +882,38 @@ export function validateGraphUpdate(graph, update) {
|
||||
update.addedEdges.length > 0 ||
|
||||
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 {
|
||||
// Missing/null transition rule: must be present when userSupportedMeaning is populated
|
||||
if (
|
||||
(update.structuralActionRequired === null || update.structuralActionRequired === undefined) &&
|
||||
meaningPopulated
|
||||
) {
|
||||
errors.push(
|
||||
"structuralActionRequired must be present when userSupportedMeaning is populated",
|
||||
);
|
||||
}
|
||||
|
||||
// Exact structural claim — four contradiction pairs
|
||||
if (update.structuralActionRequired === true && !hasMeaningfulChange) {
|
||||
errors.push("structuralActionRequired is true but proposal contains no graph mutation");
|
||||
}
|
||||
if (update.structuralActionRequired === false && hasMeaningfulChange) {
|
||||
errors.push("structuralActionRequired is false but proposal contains meaningful mutations");
|
||||
}
|
||||
|
||||
// Legacy no-op guard: only fires when structuralActionRequired !== false.
|
||||
// When false → zero-mutation is a valid intentional no-op (contract PASS).
|
||||
if (!hasMeaningfulChange && update.structuralActionRequired !== false) {
|
||||
if (meaningPopulated) {
|
||||
// Avoid double-error when field absence was already flagged above
|
||||
if (
|
||||
update.structuralActionRequired !== null &&
|
||||
update.structuralActionRequired !== undefined
|
||||
) {
|
||||
errors.push(
|
||||
"answerMeaning.userSupportedMeaning is populated, but the proposal contains no graph mutation. answerMeaning alone does not constitute graph progress.",
|
||||
);
|
||||
}
|
||||
} else if (!meaningPopulated) {
|
||||
errors.push("Update contains no meaningful change");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user