* 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:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user