Files
confidence-engine/docs/experiment-57j70.md
T

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 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:

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 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