fix(#1906): require node-test clean-fixture causation control (#2001)

* fix(#1906): require node-test clean-fixture causation control

The node-test fail-first proof accepted a deceptive content-independent
negative test — one that reds merely because GSD_PROHIB_SUBJECT is set,
ignoring the subject's content — whenever no cleanFixture was supplied,
because #1346's causation control was opt-in. The proof's observed signal
(RED) thus diverged from its target (RED caused by content) by default.

Make the causation control mandatory for the node-test kind: a descriptor
that omits cleanFixture is un-provable (fail-closed), never accepted under
the weaker violation-only proof. When a clean fixture is present, fail-first
is proven exactly as before (RED on violation AND non-vacuous GREEN on clean).
The lint-rule kind is unchanged (its subject IS the linted file; no
GSD_PROHIB_SUBJECT indirection).

Breaking (Hyrum): a previously-green node-test prohibition with no clean
fixture now hard-gates — blast radius is zero in-tree (no node-test
prohibition ships today; only the lint-rule local/no-source-grep dogfood).

Supersedes ADR-1606 Decision 4 / ADR-550 #1346 addendum's opt-in.

Closes #1906

Claude-Session: https://claude.ai/code/session_017vYn26e3nkDNxcpty1ciPJ

* docs(#1906): supersede the #1346 opt-in causation control (mandatory for node-test)

Record the node-test mandatory-causation-control supersede across the
governing surfaces:

- ADR-1606 (the enforcement decision-of-record): addendum + Decision 4
  annotated + the "Mandatory causation control — REJECTED" alternative
  flipped to accepted (premise no longer holds: zero in-tree node-test
  consumers).
- ADR-550: the 2026-06-21 #1346 "Why opt-in, not required" paragraph
  marked SUPERSEDED, pointing at ADR-1606.
- spec-phase.md: check_clean_fixture is now REQUIRED for node-test
  (was "optional").
- CONTEXT.md: PROHIB.enforce.causation predicate updated.

Regenerated the shipped-artifact cascade from the spec-phase.md edit
(+149 B, well under the 40960 cap): 16 golden-install-parity fixtures
and the workflow size baseline.

Refs #1906

Claude-Session: https://claude.ai/code/session_017vYn26e3nkDNxcpty1ciPJ
This commit is contained in:
Rezolv
2026-07-03 23:21:55 -04:00
committed by GitHub
parent 5ae4ea4c84
commit 55604e9124
24 changed files with 194 additions and 64 deletions

View File

@@ -99,11 +99,13 @@ to be made that are not derivable from the contract alone:
optional `cleanFixture` runs the same negative test a second time with
`GSD_PROHIB_SUBJECT=<cleanFixture>` and requires **non-vacuous GREEN**
(`isNonVacuousNodeTestPass`) — so fail-first is proven only when the check is **RED on the
violation AND GREEN on the clean subject** (content-dependent red). Opt-in, not mandatory:
violation AND GREEN on the clean subject** (content-dependent red). ~~Opt-in, not mandatory:
absent `cleanFixture`, behaviour matches the pre-#1346 zero-authoring compose path and the
"reds because the env var is set" case stays a documented residual for that one author's
check. The lint-rule kind needs no analog (its subject *is* the linted file; no env-var
indirection).
check.~~ **MANDATORY for the node-test kind as of #1906 (2026-07-03) — absent `cleanFixture`
the node-test is un-provable (fail-closed), never proven on the violation alone; see the #1906
addendum below.** The lint-rule kind needs no analog (its subject *is* the linted file; no
env-var indirection).
5. **Deterministic locate via five flat scalars — never a nested object.** The wired-check
descriptor is authored at spec-phase and projected onto the `must_haves.prohibitions`
@@ -174,7 +176,46 @@ belong to the spec-phase contract and are recorded in ADR-550's "Alternatives co
- **Mandatory causation control — REJECTED in favour of opt-in.** Requiring every node-test
prohibition to ship a `cleanFixture` would regress the zero-authoring compose path and
hard-gate every existing descriptor without one; the control is opt-in (Decision 4), leaving
one documented residual rather than breaking working checks.
one documented residual rather than breaking working checks. **↳ SUPERSEDED 2026-07-03 (#1906)
— see the addendum below.** The blast-radius premise no longer holds: there is **no in-tree
`node-test` consumer** (Consequences, "Open conventions"), so making the control mandatory
hard-gates *zero* existing checks. Decision 4 is now mandatory for the node-test kind.
## Addendum (2026-07-03, #1906) — node-test causation control is now MANDATORY (supersedes Decision 4's opt-in)
Decision 4 made the `cleanFixture` causation control **opt-in** to avoid regressing the #1314
zero-authoring compose path. That trade-off left a Goodhart hole open **by default**: a node-test
that omits `cleanFixture` is proven fail-first on the violation alone, so a deceptive
content-independent negative test — one that reds merely *because* `GSD_PROHIB_SUBJECT` is set,
ignoring the subject's CONTENT (`assert.ok(!process.env.GSD_PROHIB_SUBJECT)`) — passes the proof.
The proof's observed signal (RED) thus diverges from its target (RED *caused by content*), and the
divergence is the **default**, not an edge case an author opts into.
**Decision (owner ruling on #1906, 2026-07-03):** for the `node-test` kind the causation control is
**required**. A node-test descriptor that omits `cleanFixture` is treated as **un-provable**
(fail-closed) — never accepted under the weaker violation-only proof. When a clean fixture is
present, fail-first is proven exactly as before (RED on the violation subject **AND** non-vacuous
GREEN on the clean subject); a deceptive content-independent test reds on the clean subject too, so
the control fails and the proof does not pass.
- **Why the opt-in's rationale no longer applies.** Decision 4 / the rejected-alternative above kept
the control opt-in to avoid hard-gating "every existing descriptor without one." Per this ADR's own
Consequences ("Open conventions … no in-tree `node-test` consumer"), **there are none** — the only
live dogfood is the lint-rule `local/no-source-grep`; node-test fail-first is exercised only by
synthetic temp fixtures. So the mandatory control hard-gates **zero** real checks today; its cost is
paid only by future node-test authors, who now must supply a clean control subject to earn a green.
- **Scope — node-test only.** The `lint-rule` kind is unchanged (byte-identical): its subject *is* the
linted file, with no `GSD_PROHIB_SUBJECT` indirection, so the "reds because the env var is set" gap
cannot exist there. Net effect on D4's disposition is unchanged — every miss/fail/**un-provable**
still hard-gates; this only *tightens* what counts as proven.
- **Breaking change (Hyrum).** A previously-green node-test prohibition with no clean fixture now
hard-gates. Disclosed as breaking with the ~zero in-tree blast radius noted above. `cleanFixture`
stays optional at the *type* level (it rides both kinds; the lint-rule kind never uses it) but is
*required by the node-test prover*.
- **Mechanism.** `defaultProveFailFirst`'s node-test branch in `src/prohibition-enforcement.cts`:
`const clean = check.cleanFixture; if (!clean) return { provenFailFirst: false, … }` before the
existence + non-vacuous-GREEN control. Compiled by `build:lib`. ADR-550's 2026-06-21 (#1346)
addendum "Why opt-in, not required" is superseded to match.
## Cross-references
@@ -186,4 +227,4 @@ belong to the spec-phase contract and are recorded in ADR-550's "Alternatives co
- **`gsd-core/references/prohibition-probe.md`** — the portable runtime reference.
- **`docs/how-to/resolve-prohibition-findings.md`** — user-facing resolution guide.
- Code: `src/prohibition-enforcement.cts`, `src/probe-core.cts` (`projectProhibitions`),
`gsd-core/workflows/verify-phase.md`. Issues: #644, #1259, #1278, #1279, #1346.
`gsd-core/workflows/verify-phase.md`. Issues: #644, #1259, #1278, #1279, #1346, #1906.

View File

@@ -127,6 +127,8 @@ This addendum ratifies one contract point:
**Why opt-in, not required:** making the control mandatory would regress the #1314 zero-authoring compose path — every existing node-test prohibition (which carries no clean fixture) would suddenly hard-gate. So **absent `cleanFixture` → no control runs and behavior is byte-identical to post-#1314**; the residual remains a documented permanent constraint *only* for checks whose author did not supply a clean control. An author opts into the stronger machine guarantee by supplying one. The lint-rule kind needs no analog: its "subject" *is* the linted file (no `GSD_PROHIB_SUBJECT` indirection), so the "reds because the env var is set" gap does not exist there. Net effect on D4 is unchanged — every miss/fail/un-provable still hard-gates; this only *tightens* what counts as proven. The mechanism lives in `src/prohibition-enforcement.cts` (`defaultProveFailFirst` node-test branch + the `runNodeTestWithSubject` helper) and `src/probe-core.cts` (`projectProhibitions`), compiled by `build:lib`.
> **SUPERSEDED 2026-07-03 (#1906) — the node-test causation control is now MANDATORY, not opt-in.** The "why opt-in" rationale above rested on avoiding a regression of "every existing node-test prohibition." That premise no longer holds: there is **no in-tree `node-test` consumer** (ADR-1606 Consequences, "Open conventions"), so requiring the control hard-gates *zero* existing checks — while leaving the Goodhart hole open by default let a deceptive content-independent test pass the proof. Per the owner ruling on #1906, a `node-test` descriptor that omits `cleanFixture` is now **un-provable (fail-closed)**, never proven on the violation alone; when a clean fixture is present, fail-first is proven exactly as before (RED on violation AND non-vacuous GREEN on clean). The `lint-rule` kind is unchanged. Disclosed as breaking (Hyrum) with the ~zero blast radius noted. The decision-of-record lives in **ADR-1606's 2026-07-03 (#1906) addendum**; this note supersedes the "Why opt-in, not required" paragraph immediately above.
## Addendum (2026-06-15): optional `check` descriptor on the prohibition item — D3 shape extension (#1278)
This ratifies the **deterministic SOURCE** for the test-tier `CheckDescriptor` that #1259 (PR #1273) left caller/verifier-supplied. #1259 shipped the PRODUCER (`check prohibition-enforcement`) that *runs* a wired check given a `{kind, target, rule?}` descriptor, but the descriptor itself was invented by the verify-phase LLM each run (the "locate" half). #1278 makes that locate half **deterministic**: an optional `check` descriptor is authored at spec-phase on the resolved `test`-tier prohibition, projected by `projectProhibitions`, and read back by verify-phase — so a wired, passing test closes the gap with **zero manual authoring**. This extends the **Decision 3 prohibition-item shape** (it adds optional keys to that item), so it is ratified here rather than rewriting D3 in place.