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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions