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