diff --git a/docs/current-handoff.md b/docs/current-handoff.md index 6b137d7..be6ba38 100644 --- a/docs/current-handoff.md +++ b/docs/current-handoff.md @@ -239,6 +239,12 @@ Fixed a co-occurring bug where the accepted-update block referenced `startResult --- +### Experiment 57J.70 — structuralActionRequired Authoritative Guard Cleanup + +**Classification: A — BOUNDED IMPLEMENTATION COMPLETE.** One implementation defect from 57J.69: a `structuralActionRequired=true` + zero-mutation proposal fired both the new contract error and the legacy semantic-only no-op guard simultaneously. Fixed in `lib/graph/utils.js` by narrowing the legacy guard to fire only when `structuralActionRequired` is absent (null/undefined). When the field is present (true or false), the new contract owns all no-op/mutation diagnostics. Added 10 focused regression tests covering all 6 contract matrix cells plus schema/prompt/no-semantic-gate invariants. Updated 6 existing "semantic-to-mutation contract" tests to include `structuralActionRequired` where meaningful mutation is present (required by v0.23 transition rule). All 78 tests pass. **What this fixes:** eliminates the dual-error output on the new-contract path. **What this leaves unresolved:** same prompt-enforcement gap from 57J.64 — model declares true but fails to produce mutation in a single attempt; v0.23 contract now gives clean, authoritative rejection for that case. Configured Ollama: none. No production code changed beyond validator guard ownership. Full record in `docs/experiment-57j70.md`. + +--- + ### Experiment 57J.61 — Equivalent Uncertainty Identity Live Test **Objective:** Once a dedicated savings-realism uncertainty exists, does a second semantically equivalent statement reuse that same unresolved node rather than create a duplicate? **Classification: D — UPDATE 1 FAILED.** One start + two updates. Start HTTP 200 (6 nodes). Update 1 returned HTTP 200 at update_applied but the harness crash prevented detailed proposal capture. A cold-start variant confirmed that when userSupportedMeaning is populated for savings-realism uncertainty, the model extracts meaning but proposes zero graph mutations — updatedNodes=[{nodeId: X, newValue: null}], addedNodes=[], addedEdges=[]. The gateway rejects this at proposal_compatibility with "answerMeaning.userSupportedMeaning is populated, but the proposal contains no graph mutation." Update 2 was reached (total 3 calls) and was rejected for the same reason. **Neither turn established a persistent savings-realism unknown.** The identity invariant cannot be tested when neither turn produces a valid, persistent unknown node. Configured Ollama: qwen-claude:latest at http://192.168.1.111:11434. No production code changed. Full record in `docs/experiment-57j61.md`. diff --git a/docs/experiment-57j70.md b/docs/experiment-57j70.md new file mode 100644 index 0000000..810803d --- /dev/null +++ b/docs/experiment-57j70.md @@ -0,0 +1,93 @@ +# 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