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