Files
msd-core/.changeset/1259-test-tier-enforcement.md
Dave 6a9e6cb0ae fix(1259-01): address trek-e review — B1 (fatal/suppressed), B2 (bounded subprocess), M1/M2, minors
Maintainer CHANGES_REQUESTED (reviewed e08667e5, pre-portability-fix):
- B1: lint-rule no longer greens an unparseable target (eslintHasFatalError -> fail
  closed on any fatal/parse error) or an inline-suppressed violation (eslintJsonHasRule
  now scans suppressedMessages too). RED-first + real-runner repros.
- B2: both child spawns get a bounded timeout (30s node / 60s eslint) + 16MiB maxBuffer;
  timeout fails closed. Injectable timeoutMs (positive-only — 0/negative can't disable
  the bound) enables a fast 1.5s hang test.
- M1: scoped verify-phase.md — the check descriptor is author-supplied for now; filed
  #1278 for deterministic auto-locate of the descriptor (the locate half).
- M2: added tests/prohibition-enforcement.property.test.cjs (fast-check fail-closed
  invariants; within the <=2-file budget).
- m1: tapTestNames excludes # SKIP/# TODO; parseNodeTestSummary tracks # cancelled;
  isNonVacuousNodeTestPass requires cancelled===0.
- m2: scoped the determinism claim to the decision/parse layer (real runner is env-dependent).
- m3: filed #1279 for machine-proven fail-first (violation-fixture probe).
- n1: -- before target in both arg builders (option-injection). n2: dropped dead token.
- B3 (Windows npx) was already fixed in 2af76306 (pushed ~65s after the review).

Verified node 22 + 24; size baseline regenerated for the verify-phase note.
2026-06-15 15:19:04 -04:00

2.7 KiB

type, pr
type pr
Changed 1273

Test-tier prohibitions are now a real, provable gate instead of a permanent, unsatisfiable gaps_found — the deferred ENFORCEMENT half of ADR-550 Decision 5d (the "heavy half" that #644 / PR #1149 deferred) has landed. A new deterministic check prohibition-enforcement sub-command (authored as src/prohibition-enforcement.cts, compiled by build:lib to the gitignored gsd-core/bin/lib/prohibition-enforcement.cjs) is the missing PRODUCER: it locates the wired mechanical check, runs it for a genuine non-vacuous pass, builds enforcementEvidence, and emits the dispositionForProhibition() verdict. The previously-unreachable green branch in dispositionForProhibition() is now reachable from the live pipeline — a test-tier prohibition with a genuinely-passing wired check disposes green and can reach passed, while a missing, non-attested, or non-passing check hard-gates (flagged, never green → gaps_found) in BOTH interactive and autonomous modes (ADR-550 D4 / D3). verify-phase.md wires the consumer; the green/fail-closed policy in src/probe-core.cts is untouched. Both wired-check kinds are accepted (ADR-550 D2): a node --test negative test (requiring a real reported test — an empty file, which node --test counts as one passing "test", does NOT green) AND a lint/AST rule run as eslint --format json filtered by ruleId (so plugin rules like local/* load — bare --rule cannot), anchored on the in-tree local/no-source-grep rule (dogfooding, ADR-550 D4). This enforcement seam is the concrete instance of ADR-857 open-question §147 and lands on the core verify rail, never in capabilities/ (D6). (#1259)

Honest scope — failFirst is caller-attested, not yet machine-proven. This lands the execution + non-vacuous-pass half: the producer requires the caller to attest failFirst: true and the check to genuinely run and pass. It does NOT yet independently prove the check fails-on-violation (the literal regression-must-fail-first property) — cheap proof of that at verify time needs running the check against a known violation fixture, which is a tracked follow-up (#1279). The red-first property currently rests on caller attestation, surfaced transparently in the evidence record.

Correction to the issue body (#1259): the issue's "96 invalid/error negative-proof cases" figure is wrong. For the no-source-grep anchor specifically, the genuine regression-must-fail-first proofs are its two invalid cases (the .includes() and .match() blocks) in tests/eslint-rules.test.cjs — not 96. The anchor argument is unaffected (those two cases ARE real fail-first proofs); only the count was off.