Skip to content

[finding] two derived gates die with a raw ERR_MODULE_NOT_FOUND stack trace in a fresh worktree instead of reporting an unmet prerequisite #11557

Description

@os-steve

Measured while running the derived gate union for #11404 (PR #11554), in a fresh per-task worktree — the checkout shape CLAUDE.md mandates. Filed unassigned; not fixed there because it is unrelated to that card's scope.

What happens

A fresh worktree has no node_modules until pnpm install runs. Most gates in scripts/ are dependency-free by design and run fine. Two do not, and neither says so:

$ node scripts/check-ci-filter-parity.mjs --self-test
node:internal/modules/package_json_reader:314
throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null);
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'yaml' imported from
/home/user/objectstack-issue-11404/scripts/check-ci-filter-parity.mjs
... 7 more frames of node internals ...
exit 1
$ node scripts/ts-parse.mjs --self-test
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'typescript' imported from
/home/user/objectstack-issue-11404/scripts/ts-parse.mjs
exit 1

Both go green immediately after pnpm install, unchanged — measured on the same tree, same commit: check-ci-filter-parity --self-test 39 assertions pass, ts-parse --self-test 28 cases pass.

Why it is worth a card

check-ci-filter-parity is a derived family: dispatch-gates.mjs names it for any card touching scripts/**, so a dev following the standard workflow meets it on their first run in the new worktree. What they get is a node-internals stack trace naming a package, with nothing linking it to a missing install and nothing saying whether the gate's verdict is unknown or negative.

The repo already has the idiom and states the rule this violates. check-i18n-coverage.mjs's reportPrerequisiteNotMet prints the condition, the command that satisfies it, and — the load-bearing half — that nothing was measured:

Nothing was measured: no config was linted and no count was compared, so this result says NOTHING about whether any declared label went untranslated.

A raw throw carries none of that. Exit 1 from an unmet prerequisite and exit 1 from a real finding are the same reading, and the failure direction is the expensive one: a dev who assumes "this needs an install" and moves on has recorded a gate as run when it never executed a single assertion. scripts/pm/dispatch-gates.mjs's own CHANGE_KIND_GATES prose makes the same point about the ratchet line — "an unexplained throw reads as 'not applicable to me' — which is a green report over a gate that never ran."

Not asserted

No claim about scope beyond the two measured. Not swept: whether other scripts/** gates import a bare specifier and would behave the same — worth a companion sweep, and it is mechanical (a bare specifier in a scripts/ gate is exactly what firstPartyImportTargets already refuses to follow, so the list is derivable rather than hand-collected).

Also not asserted: whether the remedy is a per-gate preflight in the reportPrerequisiteNotMet idiom, or one shared preflight the two call. The second is smaller if the sweep finds more than two.

Related

#11404 / PR #11554 (where this was measured) · reportPrerequisiteNotMet in scripts/check-i18n-coverage.mjs (the idiom) · checkCliBuildPrerequisite in the same file (the same shape for a build prerequisite)


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