Skip to content

[finding] The same "self-test runs nowhere" class in root scripts/**: 6 unwired, including ts-parse.mjs — whose unrun pin is what check:parse-guard's design argument rests on #10608

Description

@os-zhuang

Filed from the PR that closes #10574 / #10575, which is itself instance N of this class one tree over: scripts/check-parse-guard.mjs landed in #10573 with a 13-case --self-test and nothing ran it, and that took a separately filed card plus a dispatch round for a human to notice.

Sub-issue of #10196, which measured this class in scripts/pm/** and proposed the detector. This card is the same detector pointed at root scripts/**, with the measurement done.

The measurement — main at d5e7b9f5a1

Derived by resolving pnpm check:* aliases rather than grepping workflows for filenames, exactly as #10196 argues: 87 root scripts/*.mjs ship a --self-test, and 6 are not reachable from any workflow, directly or through an alias a workflow runs.

scriptroot alias existsreached by a workflow
scripts/ts-parse.mjsnono
scripts/js-comment-mask.mjsnono
scripts/check-optional-error-sink-contract.mjsyes (check:optional-error-sink)no
scripts/setup-git-hooks.mjsyes (setup:git-hooks)no
scripts/ablation-dist-preflight.mjsnono
scripts/measure-test-shard-timings.mjsnono

Three of those are worth separating out; the bottom three are developer tools where "nobody runs the self-test in CI" may well be the right answer, and are listed for completeness rather than as a claim.

1. check:optional-error-sink is #10574's defect, second instance, live now

The alias is in root package.json. No workflow invokes it. The gate landed 2026-08-21 in PR #10555 (for #9754) and has never run in CI — the same shape as the card this was found from, five hours apart. It is a working gate (pnpm check:optional-error-sink is green locally: 13 self-test cases, 36 sink types, 15 baselined) that enforces nothing.

Not fixed in the closing PR: its own file surface was already claimed there for a different reason (two raw ts.createSourceFile calls, converted), and wiring a second gate into lint.yml is a different acceptance with a different blast radius.

2. ts-parse.mjs --self-test runs nowhere — and it is the pin the parse-guard's DESIGN rests on

This is the one that is more than bookkeeping. check-parse-guard.mjs is a spelling gate, and its header argues explicitly why that is enough:

The behavioural evidence lives once, at the module: ts-parse.mjs --self-test spawns real children and pins that every measured wreck refuses, names its file, and cannot be swallowed by a caller's try/catch. Pinning the refusal once and enforcing that everyone routes through it covers the same ground for fifteen callers.

That argument is sound and the rejected alternative (a gate that RUNS every entry point) is correctly rejected. But the pin it delegates to runs in no workflow. So CI enforces "everybody calls parseSourceFile" and never once checks that parseSourceFile still refuses. A regression inside ts-parse.mjs — someone downgrading process.exit to a throw, or dropping the parseDiagnostics read — is caught by nothing, and its symptom is the same green line this whole family exists to distrust.

It is now 28 cases (grown in the closing PR to cover createProgramChecked and transpileChecked), spawns real child processes, and takes ~4s.

3. js-comment-mask.mjs --self-test runs nowhere

The masker both check:parse-guard and check:entry-guard depend on to tell code from prose. Its own header records that it exists because two private stripComments families drifted apart in two different directions. A mask that silently stops masking makes both gates fabricate findings out of their own documentation — loud — but a mask that starts over-masking makes both go quietly green, which is the direction that hides.

Shape

#10196's proposal already covers it: one gate asserting that every scripts/** tool shipping a --self-test is reachable from some workflow, directly or through a package.json alias, with a reasoned opt-out list for the deliberate exclusions (the bottom three rows above are candidates). The alias-resolution detector is the ~25 lines used to produce the table here.

⚠️ Worth measuring before building, same caveat as #10196: CI affordability per self-test. ts-parse.mjs --self-test spawns ~20 node children; that is a real cost, though small next to the ~70 steps already in Lint & Repo Gates.

Refs: #10196 (the parent, scripts/pm/** half) · #10574 (the instance this was found from) · #10573 / #10133 (the family) · #8110, #8170, #8162, #8528, #9348 (the five prior one-at-a-time fixes #10196 counts)

Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions