Skip to content

A Class-Wide Invariant Gate Must Exempt the Triggering Spec's Own New Artifact

Problem

SPEC-143 froze a T4 bit-identity gate: no existing plan.md across the repo (139 legacy files) may be modified when the plan template changes. SPEC-146's whole job was to extend the plan.md contract — which means it ships its own new specs/146-.../plan.md. A repo-wide gate that globs specs/*/plan.md with a default git diff sees that new file as an Added entry and flags the spec that introduced it. The invariant ("don't rewrite legacy plan.md") is still correct; the gate's scope is wrong — it never distinguished "a legacy file changed" from "this spec added a brand-new file."

The second trap is duplication: if the real gate and its test guards each hand-roll the "did a legacy plan.md get touched?" check, they diverge, and the test stops guarding the behavior the gate actually runs.

Solution

  1. Narrow the diff to --diff-filter=MD (Modified + Deleted only — excludes Added). The new artifact the spec introduces is A, so it drops out; a genuine rewrite or deletion of a legacy file is still M/D and still caught.
git diff --name-only --diff-filter=MD "$merge_base"..HEAD \
  -- 'specs/*/plan.md' ':!specs/143-plan-md-template-diet/**'
  1. Share ONE predicate (t4_touched()) between the real gate and every test guard. No test-double reimplementation → no drift.

  2. Pair the guards — one must-pass (an added plan.md is ignored) and one must-fail (a modified legacy plan.md is still caught), both hermetic (mktemp -d sandbox, core.hooksPath=/dev/null, cleanup on exit). The pair proves the exemption is exactly as wide as intended — no wider, no narrower.

Prevention

Whenever a spec adds a new member of a file class that an earlier spec froze with a class-wide invariant gate, ask: does this gate's diff distinguish "legacy file changed" from "this spec added a new file"? If not, filter Added out (--diff-filter=MD) and prove the boundary with a must-pass and a must-fail guard that both call the gate's own predicate.

Sibling pattern: suppression-mirrors-detection.md — there a suppressor must be as anchored as the detection it cancels; here a gate's scope must exempt exactly the triggering artifact and nothing more. Both fail the same way: an over-broad relaxation silently flips the gate's verdict.

Source: SPEC-146 (T4 gate correction), review pass 2026-07-23 (empirically verified: the narrowed predicate returns empty for the added specs/146-.../plan.md while still catching legacy modifications).