4.1 KiB
Experiment 57J.70 — structuralActionRequired as Authoritative No-Op Contract
Branch: feature/semantic-action-contract-v0.23
Starting HEAD: bd3c7d5 (fix(graph): make structural action contract authoritative)
Objective
Answer and fix exactly:
Can the validator emit only the authoritative
structuralActionRequiredcontract diagnostic on the new-contract path, while preserving the old no-op behaviour only for legacy proposals that do not use the new field?
Defect (from 57J.69)
When structuralActionRequired=true + zero-mutation proposal:
- structuralActionRequired is true but proposal contains no graph mutation
- answerMeaning.userSupportedMeaning is populated, but the proposal contains no graph mutation. answerMeaning alone does not constitute graph progress.
Both errors fired for the same proposal. Under the v0.23 design, only the first (new contract) error should fire when structuralActionRequired is present.
Root Cause
The legacy semantic-only no-op guard at line 905 of lib/graph/utils.js used the condition:
if (!hasMeaningfulChange && update.structuralActionRequired !== false) {
This meant the guard still fired when structuralActionRequired === true, because true !== false. The guard then checked meaningPopulated (which was true) and added a second, duplicate error message about userSupportedMeaning.
Fix
Changed the guard condition to only fire when structuralActionRequired is absent (null/undefined):
const fieldAbsent =
update.structuralActionRequired === null ||
update.structuralActionRequired === undefined;
if (!hasMeaningfulChange && fieldAbsent) {
if (meaningPopulated) {
// structuralActionRequired was missing while userSupportedMeaning exists.
// Missing-field rejection already added above; skip semantic-only guard.
} else if (!meaningPopulated) {
errors.push("Update contains no meaningful change");
}
}
This ensures:
true/false→ new contract owns no-op/mutation consistency; legacy guard is silentnull/undefined→ transition rule fires first (missing-field rejection), then legacy no-op for meaning-less proposals
Contract Matrix After Fix
| structuralActionRequired | meaningful mutation | Result | Errors |
|---|---|---|---|
| true | absent | REJECT | 1: "structuralActionRequired is true but proposal contains no graph mutation" |
| true | present | PASS | 0 |
| false | absent | PASS (intentional no-op) | 0 |
| false | present | REJECT | 1: "structuralActionRequired is false but proposal contains meaningful mutations" |
| null/missing | populated meaning | REJECT | 1: "structuralActionRequired must be present when userSupportedMeaning is populated" |
| null/missing | no meaning + zero mutation | REJECT | 1: "Update contains no meaningful change" |
Tests Added (structural-action-contract-v0.23 block)
true + zero mutation + populated meaning→ exactly one contract error, no legacy duplicate ✓false + zero mutation + populated meaning→ pass (intentional no-op) ✓true + meaningful mutation→ pass ✓false + meaningful mutation→ exactly one contradiction error ✓null + populated meaning→ transition rule rejection preserved ✓null + no meaning + zero mutation→ legacy no-op rejection preserved ✓hasMeaningfulChangedefinition unchanged (status change = meaningful) ✓- Schema shape unchanged (makeNode/makeEdge resolve correctly) ✓
- Validator-level fix does not affect apply path ✓
- Pure boolean + structural gating, no semantic keyword logic added ✓
Tests Updated in Existing "semantic-to-mutation contract" Block
- Test 1: Changed to verify missing-field rejection fires (since
structuralActionRequiredwas previously absent) - Tests 4-8: Added
structuralActionRequired: truewhere meaningful mutation is present (required by v0.23 transition rule)
Files Changed
lib/graph/utils.js— guard condition (line ~905)tests/graph/utils.test.js— updated 6 existing tests, added 10 new regression tests