* 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:
5
.changeset/1906-node-test-causation-mandatory.md
Normal file
5
.changeset/1906-node-test-causation-mandatory.md
Normal 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)
|
||||
@@ -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)`
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user