fix(#1279): node-test prover fail-closes on missing violationFixture + honest projection docs

Addresses the #1314 maintainer review (trek-e):

- Major 1 (fail-OPEN): defaultProveFailFirst's node-test branch only guarded
  `if (!fixture)`. A missing/typo'd/stale violationFixture made GSD_PROHIB_SUBJECT
  point at a missing file; an honest negative test threw ENOENT *inside its
  callback* (a failing test named distinctly from the file), which
  isNonVacuousNodeTestRed accepted as proof -> a green forged from a setup crash.
  Now requires fs.existsSync(path.resolve(cwd, fixture)) before spawning, symmetric
  with the lint-rule path's file-result guard. Regression test pins it (RED without
  the guard); a second test pins cwd-relative fixture resolution.

- Major 2 (misleading prose): the #1278 projection carries no violationFixture, so
  the deterministic-locate path always hard-gates (fail-closed) until a
  check_violation_fixture scalar is threaded through. verify-phase.md and
  prohibition-probe.md no longer read as if the projected path produces greens;
  the ADR-550 addendum records both items. Tracked as follow-up #1346.

- Documented residual: existence is necessary but not sufficient (a red caused by
  the env being set vs the subject's content); recorded as a constraint, in #1346.

- Nit: stale 'NOT attested fail-first' comment -> 'NOT machine-proven fail-first'.

64 tests pass; eslint + tsc clean; changeset valid.
This commit is contained in:
Dave
2026-06-16 13:05:20 -04:00
parent ee9ef7a8e4
commit 33b6ee6f0c
6 changed files with 91 additions and 9 deletions

View File

@@ -539,7 +539,23 @@ function defaultProveFailFirst(check: CheckDescriptor, cwd: string, timeoutMs?:
}
if (check.kind === 'node-test') {
const fixture = check.violationFixture;
if (!fixture) return { provenFailFirst: false }; // no fixture -> hard-gate (NEVER attestation)
// Fail-CLOSED on a missing/typo'd/stale fixture path, SYMMETRIC with the lint-rule path's
// `eslintFileResultCount >= 1` guard. Without the existence check, a non-existent fixture makes
// `GSD_PROHIB_SUBJECT` point at a missing file; an honest negative test reading that subject
// throws ENOENT *inside its callback* — a failing test named DISTINCTLY from the file, which
// `isNonVacuousNodeTestRed` would accept as proof. That is fail-OPEN: a typo forges a green from
// 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).
//
// KNOWN RESIDUAL (documented, fail-open direction, tracked follow-up #1346): existence is
// necessary but not sufficient — a deliberately deceptive negative test that reds merely BECAUSE
// `GSD_PROHIB_SUBJECT` is set (rather than because the subject's CONTENT violates the must-NOT)
// is still accepted. Proving "the red was CAUSED BY the violation" cannot be done generically for
// an arbitrary author-supplied test, so it is recorded as a constraint, not silently implied-solved.
// 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).
if (!fixture || !fs.existsSync(path.resolve(cwd, fixture))) return { provenFailFirst: false };
let out = '';
try {
out = execFileSync(process.execPath, buildNodeTestArgs(check), {
@@ -632,7 +648,7 @@ export function runProhibitionEnforcement(
const passed = proof.provenFailFirst === true && run.passed === true;
if (!passed) {
// NOT attested fail-first OR did not genuinely pass -> fail-closed, located: true (an actual
// NOT machine-proven fail-first OR did not genuinely pass -> fail-closed, located: true (an actual
// located miss/fail). Hard-gate applies in BOTH modes; the disposition stays non-green / flagged.
const disposition = dispositionForProhibition(prohibition, { enforcementEvidence: [] });
return {