docs: record structural action guard cleanup
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user