# 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 `structuralActionRequired` contract 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: ```javascript 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): ```javascript 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 silent - `null` / `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) 1. `true + zero mutation + populated meaning` → exactly one contract error, no legacy duplicate ✓ 2. `false + zero mutation + populated meaning` → pass (intentional no-op) ✓ 3. `true + meaningful mutation` → pass ✓ 4. `false + meaningful mutation` → exactly one contradiction error ✓ 5. `null + populated meaning` → transition rule rejection preserved ✓ 6. `null + no meaning + zero mutation` → legacy no-op rejection preserved ✓ 7. `hasMeaningfulChange` definition unchanged (status change = meaningful) ✓ 8. Schema shape unchanged (makeNode/makeEdge resolve correctly) ✓ 9. Validator-level fix does not affect apply path ✓ 10. 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 `structuralActionRequired` was previously absent) - Tests 4-8: Added `structuralActionRequired: true` where 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 ## Ollama calls: 0 ## Live API calls: 0 ## Schema changed: NO ## Prompt changed: NO