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

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2001
---
**Node-test prohibition proofs now require a clean-fixture causation control** — a `node-test` prohibition's fail-first proof no longer accepts a deceptive content-independent negative test (one that reds merely because `GSD_PROHIB_SUBJECT` is *set*, ignoring the subject's content). The `check_clean_fixture` control is now **mandatory** for the `node-test` kind: a descriptor that omits it is un-provable and hard-gates, rather than greening on the violation alone. **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). The `lint-rule` kind is unchanged (its subject IS the linted file, no `GSD_PROHIB_SUBJECT` indirection). (#1906)

View File

@@ -387,7 +387,7 @@ The prompt-level data/instruction isolation seam for untrusted web/document ingr
`PROHIB.enforce.green-rule=passed iff provenFailFirst===true && run.passed===true (runProhibitionEnforcement); every miss/fail/un-provable HARD-GATES both modes via dispositionForProhibition's fail-closed default`
`PROHIB.enforce.kinds=node-test (non-vacuous red via isNonVacuousNodeTestRed; pass-side vacuity via isNonVacuousNodeTestPass) | lint-rule (eslint --format json filtered by ruleId)`
`PROHIB.enforce.failfirst=MACHINE-PROVEN against an author-supplied violation fixture (#1279); caller failFirst attestation DEMOTED to a non-authoritative hint (FF-08)`
`PROHIB.enforce.causation=opt-in clean-fixture control proves the red is content-caused not env-var-set (#1346); absent=documented residual`
`PROHIB.enforce.causation=clean-fixture control proves the red is content-caused not env-var-set; MANDATORY for node-test (#1906 supersedes #1346 opt-in) — absent clean-fixture ⇒ node-test un-provable/fail-closed; lint-rule needs none (its subject IS the linted file)`
`PROHIB.descriptor.shape=5 FLAT scalars (check_kind,check_target,check_rule,check_violation_fixture,check_clean_fixture) — NEVER a nested check:{} (parseMustHavesBlock is a flat parser, src/frontmatter.cts)`
`PROHIB.rail=core verify rail, non-toggleable (ADR-857 verification-substrate boundary / decision #6); the verifier<->predicate contract is NOT an off-by-default capability`
`PROHIB.judgment-tier=never-silent / never-hard-halt soft gate; autonomous emits "unverified-prohibition — human review recommended" (exogenous grading, ADR-550 D4)`

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.

View File

@@ -371,11 +371,13 @@ For each Requirement gathered so far, run the two-stage recall→precision pass:
against to **machine-prove fail-first**; rides BOTH kinds. Capture it to let the item green
end-to-end with zero hand-authoring at verify time; for `node-test` the negative test should
read its subject from the `GSD_PROHIB_SUBJECT` env var so the prover can inject this fixture.
- `check_clean_fixture` (#1346) — **optional** path to a KNOWN-CLEAN control subject. When
captured, the `node-test` prover also runs the check against it and requires GREEN — proving
the violation's RED is caused by the subject's *content*, not by `GSD_PROHIB_SUBJECT` merely
being set. Capture it for a stronger guarantee; omit it and the check still proves fail-first
on the violation alone (the content-causation residual stays documented for that case).
- `check_clean_fixture` (#1346; **REQUIRED for `node-test` as of #1906**) — path to a
KNOWN-CLEAN control subject. The `node-test` prover runs the check against it and requires
GREEN — proving the violation's RED is caused by the subject's *content*, not by
`GSD_PROHIB_SUBJECT` merely being set. For a `node-test` this is **mandatory**: omit it and
the check is un-provable (fail-closed), never proven on the violation alone — so a deceptive
content-independent test cannot pass. (`lint-rule` needs no clean fixture: its subject IS the
linted file, no `GSD_PROHIB_SUBJECT` indirection.)
This is a **SOFT capture (CHK-04): a `test`-tier prohibition WITHOUT a descriptor is still
allowed** — if the author cannot yet name the wired check, leave the descriptor empty and
proceed. It is NOT a hard authoring block; the item simply stays fail-closed/flagged

View File

@@ -77,12 +77,15 @@ export interface CheckDescriptor {
*/
violationFixture?: string;
/**
* OPTIONAL author-supplied path to a KNOWN-CLEAN control subject (#1346). When present, the prover
* runs the check against it as a CAUSATION CONTROL and requires it to stay GREEN — proof that the
* RED on `violationFixture` was caused by the subject's CONTENT, not merely by `GSD_PROHIB_SUBJECT`
* being set. A deceptive content-independent check reds on the clean subject too → control fails →
* not proven. ABSENT → no control runs (the documented residual remains; backward-compatible with
* the #1314 zero-authoring compose path). A supplied-but-missing path fails closed.
* Author-supplied path to a KNOWN-CLEAN control subject (#1346; MANDATORY for the node-test kind as
* of #1906). The prover runs the check against it as a CAUSATION CONTROL and requires it to stay
* GREEN — proof that the RED on `violationFixture` was caused by the subject's CONTENT, not merely by
* `GSD_PROHIB_SUBJECT` being set. A deceptive content-independent check reds on the clean subject too
* → control fails → not proven. For the `node-test` kind this is now REQUIRED: ABSENT → the node-test
* is UN-PROVABLE (fail-closed), never accepted under the weaker violation-only proof (#1906 supersedes
* #1346's opt-in; ADR-1606 Decision 4). A supplied-but-missing path fails closed. The field stays
* optional at the type level because 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 can't exist).
*/
cleanFixture?: string;
}
@@ -595,12 +598,12 @@ function defaultProveFailFirst(check: CheckDescriptor, cwd: string, timeoutMs?:
// a setup crash, not from the prohibition firing. Requiring the fixture to exist before spawning
// closes the realistic typo/stale-path case (#1279 review, Major 1).
//
// CAUSATION (#1346): existence + a non-vacuous red is necessary but not sufficient — a deceptive
// negative test that reds merely BECAUSE `GSD_PROHIB_SUBJECT` is set (rather than because the
// subject's CONTENT violates the must-NOT) would otherwise be accepted. The OPTIONAL `cleanFixture`
// control below proves content-dependence when supplied (red on bad AND green on clean). When NO
// clean fixture is authored the control cannot run, so the residual remains a documented constraint
// for that case (an author opts into the stronger proof by supplying a known-clean control subject).
// CAUSATION (#1346; MANDATORY as of #1906): existence + a non-vacuous red is necessary but not
// sufficient — a deceptive negative test that reds merely BECAUSE `GSD_PROHIB_SUBJECT` is set
// (rather than because the subject's CONTENT violates the must-NOT) would otherwise be accepted.
// The `cleanFixture` control below proves content-dependence (red on bad AND green on clean) and is
// now REQUIRED for the node-test kind: absent it, the check is un-provable (fail-closed), not
// accepted under the weaker violation-only proof (#1906 supersedes #1346's opt-in; ADR-1606 D4).
// Resolve the fixture against `cwd` (NOT the verify process's cwd): the spawned test reads
// `GSD_PROHIB_SUBJECT` and resolves a relative subject against `cwd`, so the existence check must
// use the SAME base or it could pass here yet ENOENT in the child (re-opening the fail-open hole).
@@ -608,18 +611,19 @@ function defaultProveFailFirst(check: CheckDescriptor, cwd: string, timeoutMs?:
// Run the negative test against the KNOWN-BAD subject and require a NON-VACUOUS red.
const redOut = runNodeTestWithSubject(check, cwd, fixture, timeoutMs);
if (!isNonVacuousNodeTestRed(redOut, check.target)) return { provenFailFirst: false, method: 'violation-fixture' };
// #1346 CAUSATION CONTROL (optional): if a clean control subject is supplied, run the SAME test
// against it and require it to stay GREEN. This proves the red above was caused by the subject's
// CONTENT — a deceptive test that reds merely because GSD_PROHIB_SUBJECT is SET reds here too →
// not content-dependent → not proven. Absent → no control (documented residual; backward-compat).
// #1906 CAUSATION CONTROL (MANDATORY for node-test — supersedes #1346's opt-in, ADR-1606 D4): the
// clean control subject is REQUIRED. Run the SAME test against it and require it to stay GREEN,
// proving the red above was caused by the subject's CONTENT — a deceptive test that reds merely
// because GSD_PROHIB_SUBJECT is SET reds here too → not content-dependent → not proven. ABSENT →
// the control cannot run → un-provable → fail-closed (NOT accepted under the weaker violation-only
// proof). This is the one behavior change vs #1346: absent `cleanFixture` was previously proven.
const clean = check.cleanFixture;
if (clean) {
// A supplied-but-missing/typo'd control path can't run the control → fail-closed, symmetric
// with the violation-fixture existence guard (resolve against the SAME `cwd` as the child).
if (!fs.existsSync(path.resolve(cwd, clean))) return { provenFailFirst: false, method: 'violation-fixture' };
const cleanOut = runNodeTestWithSubject(check, cwd, clean, timeoutMs);
if (!isNonVacuousNodeTestPass(cleanOut, check.target)) return { provenFailFirst: false, method: 'violation-fixture' };
}
if (!clean) return { provenFailFirst: false, method: 'violation-fixture' };
// A supplied-but-missing/typo'd control path can't run the control → fail-closed, symmetric
// with the violation-fixture existence guard (resolve against the SAME `cwd` as the child).
if (!fs.existsSync(path.resolve(cwd, clean))) return { provenFailFirst: false, method: 'violation-fixture' };
const cleanOut = runNodeTestWithSubject(check, cwd, clean, timeoutMs);
if (!isNonVacuousNodeTestPass(cleanOut, check.target)) return { provenFailFirst: false, method: 'violation-fixture' };
return { provenFailFirst: true, method: 'violation-fixture' };
}
// Unknown kind — defensive; the LOCATE guard already rejects it.

View File

@@ -289,7 +289,7 @@
"gsd-core/workflows/sketch-wrap-up.md": "ab9fbb7371ef36bf",
"gsd-core/workflows/sketch.md": "114ca2455c2eb6fc",
"gsd-core/workflows/smart-entry.md": "0f468aa4e70bcc23",
"gsd-core/workflows/spec-phase.md": "5d6f0c480c0b251d",
"gsd-core/workflows/spec-phase.md": "7191bc8f3696b315",
"gsd-core/workflows/spike-wrap-up.md": "e2f251b7c8bfa2b2",
"gsd-core/workflows/spike.md": "0f9a81bcf4573195",
"gsd-core/workflows/stats.md": "a20eb078d2ab11be",

View File

@@ -359,7 +359,7 @@
"gsd-core/workflows/sketch-wrap-up.md": "c03f64e834b69540",
"gsd-core/workflows/sketch.md": "04e0758c1a58881e",
"gsd-core/workflows/smart-entry.md": "62456d35c69d7437",
"gsd-core/workflows/spec-phase.md": "2aa0353147153776",
"gsd-core/workflows/spec-phase.md": "065cfb6c3bda023f",
"gsd-core/workflows/spike-wrap-up.md": "c86e04a0feb220a5",
"gsd-core/workflows/spike.md": "53127654e77256bf",
"gsd-core/workflows/stats.md": "01c24349370a0e6d",

View File

@@ -288,7 +288,7 @@
"gsd-core/workflows/sketch-wrap-up.md": "fcef0ef795f8bfc5",
"gsd-core/workflows/sketch.md": "dc9645b2ee28559d",
"gsd-core/workflows/smart-entry.md": "9d35a3094fb64ea0",
"gsd-core/workflows/spec-phase.md": "bab99d00772a5cd0",
"gsd-core/workflows/spec-phase.md": "f5c7b0545d4507b1",
"gsd-core/workflows/spike-wrap-up.md": "89a8d7fbc74ce8f2",
"gsd-core/workflows/spike.md": "aae8bcad15642645",
"gsd-core/workflows/stats.md": "17b4f2059f4b4ef2",

View File

@@ -292,7 +292,7 @@
"gsd-core/workflows/sketch-wrap-up.md": "91f40fc816293e3c",
"gsd-core/workflows/sketch.md": "92a8bab2605469fb",
"gsd-core/workflows/smart-entry.md": "4ca661156b9c274f",
"gsd-core/workflows/spec-phase.md": "e06346c9c626a2d2",
"gsd-core/workflows/spec-phase.md": "d6a260dd86a52660",
"gsd-core/workflows/spike-wrap-up.md": "693949a4e10e66bd",
"gsd-core/workflows/spike.md": "204e742c846ee0d9",
"gsd-core/workflows/stats.md": "5cfea82b894eee3c",

View File

@@ -359,7 +359,7 @@
"gsd-core/workflows/sketch-wrap-up.md": "7d52aa95ee69d47a",
"gsd-core/workflows/sketch.md": "55cee885b4add548",
"gsd-core/workflows/smart-entry.md": "62456d35c69d7437",
"gsd-core/workflows/spec-phase.md": "2aa0353147153776",
"gsd-core/workflows/spec-phase.md": "065cfb6c3bda023f",
"gsd-core/workflows/spike-wrap-up.md": "2fde13092ba1af57",
"gsd-core/workflows/spike.md": "0051a7e2193a7522",
"gsd-core/workflows/stats.md": "01c24349370a0e6d",

View File

@@ -324,7 +324,7 @@
"gsd-core/workflows/sketch-wrap-up.md": "7709bdd7a70f736c",
"gsd-core/workflows/sketch.md": "bfe5a781d6da3ce8",
"gsd-core/workflows/smart-entry.md": "72d698957173fd33",
"gsd-core/workflows/spec-phase.md": "47cea2b5efffa6b0",
"gsd-core/workflows/spec-phase.md": "e6c8641ff024e9f2",
"gsd-core/workflows/spike-wrap-up.md": "53a2c51a8d9e6b08",
"gsd-core/workflows/spike.md": "c2f115f0d3251654",
"gsd-core/workflows/stats.md": "0d7449acf349feec",

View File

@@ -290,7 +290,7 @@
"gsd-core/workflows/sketch-wrap-up.md": "804bb69e6c3590a0",
"gsd-core/workflows/sketch.md": "b4d17647e0fb9e5d",
"gsd-core/workflows/smart-entry.md": "d4c882ebeec21f34",
"gsd-core/workflows/spec-phase.md": "117abebab62d6df7",
"gsd-core/workflows/spec-phase.md": "6eb9b883d1f41112",
"gsd-core/workflows/spike-wrap-up.md": "e203ed8e057f654c",
"gsd-core/workflows/spike.md": "716d74cdb2e39a3e",
"gsd-core/workflows/stats.md": "49085df6d4793df3",

View File

@@ -359,7 +359,7 @@
"gsd-core/workflows/sketch-wrap-up.md": "6780757b2db165ac",
"gsd-core/workflows/sketch.md": "4221996fa3d87f7c",
"gsd-core/workflows/smart-entry.md": "f8c615009ab00a22",
"gsd-core/workflows/spec-phase.md": "fca5397bf040156d",
"gsd-core/workflows/spec-phase.md": "9cebb93bb6b09841",
"gsd-core/workflows/spike-wrap-up.md": "ec64f0f7ab03ef9b",
"gsd-core/workflows/spike.md": "9e37f8067adf87b7",
"gsd-core/workflows/stats.md": "17b4f2059f4b4ef2",

View File

@@ -359,7 +359,7 @@
"gsd-core/workflows/sketch-wrap-up.md": "067e2654f3fdaba2",
"gsd-core/workflows/sketch.md": "e1a544c83bb6e5be",
"gsd-core/workflows/smart-entry.md": "aead1f97e32cc345",
"gsd-core/workflows/spec-phase.md": "0d67fa7de33933c0",
"gsd-core/workflows/spec-phase.md": "72ba501da5edbfee",
"gsd-core/workflows/spike-wrap-up.md": "99a9e23d1c6cf66e",
"gsd-core/workflows/spike.md": "c9482514e665bcac",
"gsd-core/workflows/stats.md": "01c24349370a0e6d",

View File

@@ -289,7 +289,7 @@
"gsd-core/workflows/sketch-wrap-up.md": "eb5f0849f07479aa",
"gsd-core/workflows/sketch.md": "9a712af64fc2fff8",
"gsd-core/workflows/smart-entry.md": "bf38fd489b27231e",
"gsd-core/workflows/spec-phase.md": "79f136ab71edf595",
"gsd-core/workflows/spec-phase.md": "59ab40cd1eb44d50",
"gsd-core/workflows/spike-wrap-up.md": "79e23a81bb74bc95",
"gsd-core/workflows/spike.md": "80844a3c05339fdb",
"gsd-core/workflows/stats.md": "01289f444b66913d",

View File

@@ -359,7 +359,7 @@
"gsd-core/workflows/sketch-wrap-up.md": "7673797d65af2377",
"gsd-core/workflows/sketch.md": "201772f9c5c02e97",
"gsd-core/workflows/smart-entry.md": "8b865b82ff6d44d8",
"gsd-core/workflows/spec-phase.md": "2c67506aeb25a7e1",
"gsd-core/workflows/spec-phase.md": "cfdd2f45d58d6943",
"gsd-core/workflows/spike-wrap-up.md": "b7ee3d961b5792c6",
"gsd-core/workflows/spike.md": "a782dd863771fe0a",
"gsd-core/workflows/stats.md": "17b4f2059f4b4ef2",

View File

@@ -325,7 +325,7 @@
"gsd-core/workflows/sketch-wrap-up.md": "19046704ae337d73",
"gsd-core/workflows/sketch.md": "e42a914206c152ef",
"gsd-core/workflows/smart-entry.md": "62456d35c69d7437",
"gsd-core/workflows/spec-phase.md": "2aa0353147153776",
"gsd-core/workflows/spec-phase.md": "065cfb6c3bda023f",
"gsd-core/workflows/spike-wrap-up.md": "3f3e7f0018284059",
"gsd-core/workflows/spike.md": "be2295fe2b32956f",
"gsd-core/workflows/stats.md": "01c24349370a0e6d",

View File

@@ -359,7 +359,7 @@
"gsd-core/workflows/sketch-wrap-up.md": "1a9f5b8e01a43506",
"gsd-core/workflows/sketch.md": "89acaae8de9bf8c9",
"gsd-core/workflows/smart-entry.md": "0660f42e2d646f7e",
"gsd-core/workflows/spec-phase.md": "eb94ceb386b2a328",
"gsd-core/workflows/spec-phase.md": "15311227e3f7f69e",
"gsd-core/workflows/spike-wrap-up.md": "3609a57dbd8b5ac0",
"gsd-core/workflows/spike.md": "df52b9f31a72d204",
"gsd-core/workflows/stats.md": "598b1bb510ed0451",

View File

@@ -289,7 +289,7 @@
"gsd-core/workflows/sketch-wrap-up.md": "7630ee69ab22462d",
"gsd-core/workflows/sketch.md": "68c337d92c79dc9b",
"gsd-core/workflows/smart-entry.md": "c90d5ee83befd3ea",
"gsd-core/workflows/spec-phase.md": "dbe5f223d7bc026c",
"gsd-core/workflows/spec-phase.md": "f9481389c9e0b64a",
"gsd-core/workflows/spike-wrap-up.md": "96b8244988d651fd",
"gsd-core/workflows/spike.md": "52fcd95f7b343232",
"gsd-core/workflows/stats.md": "7ffa072290ebcae3",

View File

@@ -289,7 +289,7 @@
"gsd-core/workflows/sketch-wrap-up.md": "4894dac77fc42655",
"gsd-core/workflows/sketch.md": "52f4a04f511e205e",
"gsd-core/workflows/smart-entry.md": "0687dcb3cf5b3699",
"gsd-core/workflows/spec-phase.md": "56cecc44b2cc0bc4",
"gsd-core/workflows/spec-phase.md": "0b5c278c2d7a4bbc",
"gsd-core/workflows/spike-wrap-up.md": "1f57905138a648ac",
"gsd-core/workflows/spike.md": "48df8b626cbd378d",
"gsd-core/workflows/stats.md": "18089135bc41f1b4",

View File

@@ -289,7 +289,7 @@
"gsd-core/workflows/sketch-wrap-up.md": "67e90c60062d0d5f",
"gsd-core/workflows/sketch.md": "7418628ba45dd6c0",
"gsd-core/workflows/smart-entry.md": "963c0a363fb0cd6a",
"gsd-core/workflows/spec-phase.md": "e8ed811cd67076b7",
"gsd-core/workflows/spec-phase.md": "1eacc6cc8d146d2f",
"gsd-core/workflows/spike-wrap-up.md": "f8e39782c6e42ef7",
"gsd-core/workflows/spike.md": "aa5934044317ba7a",
"gsd-core/workflows/stats.md": "c49d3de15375d04b",

View File

@@ -520,7 +520,9 @@ describe('prohibition-enforcement REAL runner end-to-end (#1259)', () => {
fs.writeFileSync(badFixture, 'this subject contains FORBIDDEN content\n');
const result = enforce.runProhibitionEnforcement(
TEST_TIER,
{ kind: 'node-test', target: tf, failFirst: true, violationFixture: badFixture },
// #1906: the node-test causation control is mandatory — supply the clean control subject the
// test already writes so the honest content-dependent path proves fail-first and greens.
{ kind: 'node-test', target: tf, failFirst: true, violationFixture: badFixture, cleanFixture: cleanSubject },
{ cwd: dir, runCheck: () => ({ passed: true }) },
);
assert.equal(result.status, 'green', 'a real negative test proven fail-first + clean pass must green');
@@ -616,6 +618,67 @@ describe('prohibition-enforcement REAL runner end-to-end (#1259)', () => {
'a supplied clean fixture that does not exist cannot run the control -> fail-closed');
});
// ─── #1906: the causation control is now MANDATORY for the node-test kind ───
// Supersedes #1346's opt-in (ADR-1606 Decision 4; ADR-550 2026-06-21 addendum). Without a clean
// fixture the causation control cannot run, so a node-test that reds merely BECAUSE
// GSD_PROHIB_SUBJECT is set (ignoring the subject's CONTENT) would pass the fail-first proof. A
// node-test descriptor that omits `cleanFixture` is therefore treated as UN-PROVABLE (fail-closed),
// never accepted under the weaker proof. The lint-rule kind is unchanged (its subject IS the linted
// file; no GSD_PROHIB_SUBJECT indirection, so the "reds because the env var is set" gap can't exist).
test('a DECEPTIVE content-independent red is NOT proven fail-first when NO clean fixture is supplied (#1906 regression)', (t) => {
const enforce = require(ENFORCEMENT_LIB);
const dir = createTempDir('prohib-deceptive-noclean-');
t.after(() => cleanup(dir));
// Deceptive: reds whenever a subject is PRESENT, regardless of its content. Goes RED against the
// bad fixture (looks fail-first) — pre-#1906 this GREENED because no clean control ran. Now the
// mandatory control's absence makes the check un-provable -> fail-closed.
const tf = path.join(dir, 'neg.test.cjs');
fs.writeFileSync(tf,
"const { test } = require('node:test');\n" +
"const assert = require('node:assert');\n" +
"test('reds whenever a subject is present (deceptive, content-independent)', () => {\n" +
" assert.ok(!process.env.GSD_PROHIB_SUBJECT, 'fails whenever a subject is set');\n" +
"});\n");
const badFixture = path.join(dir, 'bad-subject.txt');
fs.writeFileSync(badFixture, 'this subject contains FORBIDDEN content\n');
const result = enforce.runProhibitionEnforcement(
TEST_TIER,
// NO cleanFixture — pre-#1906 the causation control was skipped and this greened.
{ kind: 'node-test', target: tf, failFirst: true, violationFixture: badFixture },
{ cwd: dir, runCheck: () => ({ passed: true }) },
);
assert.notEqual(result.status, 'green',
'a node-test that omits the mandatory clean control is un-provable -> fail-closed (#1906)');
assert.equal(result.located, true, 'the check is still located; it just cannot be proven fail-first');
});
test('even an HONEST content-dependent node-test hard-gates when NO clean fixture is supplied (#1906 mandatory)', (t) => {
const enforce = require(ENFORCEMENT_LIB);
const dir = createTempDir('prohib-honest-noclean-');
t.after(() => cleanup(dir));
// Honest: reds ONLY when the subject's CONTENT contains FORBIDDEN. Pre-#1906 this greened on the
// violation alone. Post-#1906 the control is required, so an honest test with no clean fixture is
// un-provable — the author must supply a clean control subject to earn the green.
const tf = path.join(dir, 'neg.test.cjs');
fs.writeFileSync(tf,
"const { test } = require('node:test');\n" +
"const assert = require('node:assert');\n" +
"const fs = require('node:fs');\n" +
"test('rejects the forbidden content (content-dependent)', () => {\n" +
" const subject = fs.readFileSync(process.env.GSD_PROHIB_SUBJECT, 'utf-8');\n" +
" assert.ok(!subject.includes('FORBIDDEN'), 'subject must not contain FORBIDDEN');\n" +
"});\n");
const badFixture = path.join(dir, 'bad-subject.txt');
fs.writeFileSync(badFixture, 'this subject contains FORBIDDEN content\n');
const result = enforce.runProhibitionEnforcement(
TEST_TIER,
{ kind: 'node-test', target: tf, failFirst: true, violationFixture: badFixture },
{ cwd: dir, runCheck: () => ({ passed: true }) },
);
assert.notEqual(result.status, 'green',
'the node-test causation control is mandatory (#1906); no clean fixture -> un-provable -> fail-closed');
});
test('a HANGING node-test fails closed via the bounded timeout (B2: no unbounded subprocess)', (t) => {
const enforce = require(ENFORCEMENT_LIB);
const dir = createTempDir('prohib-hang-');
@@ -779,7 +842,9 @@ describe('prohibition-enforcement REAL runner end-to-end (#1259)', () => {
fs.writeFileSync(badFixture, 'this subject contains FORBIDDEN content\n');
const result = enforce.runProhibitionEnforcement(
TEST_TIER,
{ kind: 'node-test', target: negTest, violationFixture: badFixture },
// #1906: the node-test causation control is mandatory — supply the clean control subject the
// test already writes so the honest content-dependent path greens.
{ kind: 'node-test', target: negTest, violationFixture: badFixture, cleanFixture: path.join(dir, 'clean-subject.txt') },
{ cwd: dir },
);
assert.equal(result.status, 'green', 'real node-test proven RED on the bad subject + clean pass must green');
@@ -819,14 +884,16 @@ describe('prohibition-enforcement REAL runner end-to-end (#1259)', () => {
assert.equal(result.evidence.length, 0, 'no enforcement evidence on a hard-gate');
});
test('COMPOSE (#1346): a prohibition projected WITH check_violation_fixture greens end-to-end through the DEFAULT prover+runner (zero hand-authoring)', (t) => {
test('COMPOSE (#1906): a node-test prohibition projected WITHOUT check_clean_fixture hard-gates — the mandatory causation control is absent', (t) => {
const enforce = require(ENFORCEMENT_LIB);
const pc = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'probe-core.cjs'));
const dir = createTempDir('prohib-compose-1346-');
const dir = createTempDir('prohib-compose-1906-');
t.after(() => cleanup(dir));
// The #1278 deterministic-locate path + the #1279 machine-proof now COMPOSE: a prohibition item
// authored with the four flat scalars projects -> reads back into a descriptor that ALREADY carries
// violationFixture -> the default prover greens it with NO hand-supplied fixture in the request.
// #1906: the #1278 locate + #1279 proof compose, but for the node-test kind the causation control
// (#1346's clean fixture) is now MANDATORY. A prohibition authored with only the four scalars
// (violation fixture, no clean fixture) round-trips a descriptor that carries violationFixture but
// NO cleanFixture -> the default prover cannot run the required control -> hard-gate. The five-scalar
// (WITH check_clean_fixture) green path is covered by the sibling COMPOSE (#1346 clean) test below.
const negTest = path.join(dir, 'neg.test.cjs');
fs.writeFileSync(negTest,
"const { test } = require('node:test');\n" +
@@ -847,11 +914,14 @@ describe('prohibition-enforcement REAL runner end-to-end (#1259)', () => {
])[0];
const descriptor = enforce.descriptorFromProjection(projected);
assert.equal(descriptor.violationFixture, 'bad-subject.txt', 'the projected fixture survived the round-trip');
// NO failFirst, NO hand-supplied violationFixture beyond what the projection carried.
assert.equal(descriptor.cleanFixture, undefined, 'no clean fixture was authored -> none round-trips');
// NO failFirst, NO hand-supplied fixture beyond what the projection carried.
const result = enforce.runProhibitionEnforcement(projected, descriptor, { cwd: dir });
assert.equal(result.status, 'green',
'the fully-projected prohibition greens through the default prover+runner — #1278 + #1279 compose');
assert.equal(result.evidence[0].failFirstProof, 'violation-fixture', 'green carries the machine-proof method');
assert.notEqual(result.status, 'green',
'a node-test projected without the mandatory clean control hard-gates through the default prover (#1906)');
assert.equal(result.flagged, true, 'the un-provable node-test miss is flagged');
assert.equal(result.located, true, 'the descriptor was located; it just could not be proven fail-first');
assert.equal(result.evidence.length, 0, 'no enforcement evidence on a hard-gate');
});
test('COMPOSE (#1346 clean): a prohibition projected WITH check_clean_fixture proves content-dependence end-to-end (deceptive vs honest)', (t) => {
@@ -977,8 +1047,12 @@ describe('prohibition-enforcement defaultProveFailFirst REAL prover (#1279)', ()
"});\n");
const badFixture = path.join(dir, 'bad-subject.txt');
fs.writeFileSync(badFixture, 'this subject contains FORBIDDEN content\n');
// #1906: the causation control is mandatory for node-test — supply a KNOWN-CLEAN control subject
// so the honest content-dependent test proves fail-first (RED on bad AND non-vacuous GREEN on clean).
const cleanFixture = path.join(dir, 'clean-subject.txt');
fs.writeFileSync(cleanFixture, 'this subject is clean\n');
const proof = enforce.defaultProveFailFirst(
{ kind: 'node-test', target: negTest, violationFixture: badFixture },
{ kind: 'node-test', target: negTest, violationFixture: badFixture, cleanFixture },
dir,
);
assert.equal(proof.provenFailFirst, true,
@@ -1071,8 +1145,10 @@ describe('prohibition-enforcement defaultProveFailFirst REAL prover (#1279)', ()
" assert.ok(!subject.includes('FORBIDDEN'), 'subject is clean');\n" +
"});\n");
fs.writeFileSync(path.join(dir, 'bad-subject.txt'), 'this subject contains FORBIDDEN content\n');
// #1906: mandatory clean control, also named RELATIVELY so both runs exercise the cwd-relative guard.
fs.writeFileSync(path.join(dir, 'clean-subject.txt'), 'this subject is clean\n');
const proof = enforce.defaultProveFailFirst(
{ kind: 'node-test', target: negTest, violationFixture: 'bad-subject.txt' }, // RELATIVE to cwd
{ kind: 'node-test', target: negTest, violationFixture: 'bad-subject.txt', cleanFixture: 'clean-subject.txt' }, // RELATIVE to cwd
dir,
);
assert.equal(proof.provenFailFirst, true,

View File

@@ -74,7 +74,7 @@
"sketch-wrap-up.md": 14223,
"sketch.md": 19960,
"smart-entry.md": 11080,
"spec-phase.md": 31752,
"spec-phase.md": 31901,
"spike-wrap-up.md": 15092,
"spike.md": 24517,
"stats.md": 6718,