Skip to content

fix(tooling): derive the prerequisite gate name by stripping the module extension, not only .mjs - #16670

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-15864-prerequisite-gate-name-extension
Sep 7, 2026
Merged

fix(tooling): derive the prerequisite gate name by stripping the module extension, not only .mjs#16670
baozhoutao merged 1 commit into
mainfrom
claude/issue-15864-prerequisite-gate-name-extension

Conversation

@claude

@claude claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #15864

Re-derived on origin/main at 0a61db1f5d: the strip is at scripts/import-prerequisite.mjs:883 (the dispatch's reading of 883 holds), and the TypeScript consumer population is onescripts/check-exported-any-returns.mts. My own re-derivation counts 61 real importers at that tip (60 .mjs + 1 .mts), not the 62 the dispatch carried; the one-TypeScript-consumer premise is unaffected.

The decision, and why — triage asked for this explicitly

Decision: the module extension comes off, for .mts exactly as it always did for .mjs. Not reached by widening the regex to /\.m[jt]s$/ — reached by applying triage's own test, what does the reader actually have to type, which turns out to point away from the extension in both directions:

  1. The reader never types this identifier. The refusal already prints two different strings, and the code says so in a comment right below the strip: the identifier is the name, and command (from importerCommandPath) is the path. The path half already carries the real extension — node scripts/check-exported-any-returns.mts — and is already correct. The reader copies that line, not the headline.
  2. What the reader types to re-run the gate has no extension at all. The .mts gate's real invocation is pnpm --filter @objectstack/client check:exported-any-returns, which runs it through tsx, not node — so the file extension is not even the reader's entry point.

So the alternative reading — keep the extension, and call the .mjs gates the inconsistent ones — fails its own test, and it would also change what every importer prints. That is the 61-consumer output change the card's stop condition names as a different card, and it is not what landed here.

Per triage's "make the rule explicit rather than extend the strip", the rule is now stated rather than encoded in a regex: GATE_MODULE_EXTENSIONS declares the two module extensions this repo's gate corpus uses (matching the pair check-ratchet-remedy-authority.mjs already declares for the same reason), and gateNameOf removes whichever one the file has.

Does anything read the derived name programmatically? — swept, answer is no

Triage forbade assuming this away. Swept five reader classes; one real reader exists and it does not key on the name:

  • scripts/check-regen-pending.mjs:474gateCouldNotRun matches the refusal with (\S+): PREREQUISITE NOT MET and does capture the identifier. But \S+ matches both spellings, and the captured value is only re-printed in a detail line; nothing branches on it. Measured rather than read: feeding it both spellings returns byte-identical kind, headline and fix, differing only in the display string. Cosmetic stays cosmetic.
  • The log-file convention/tmp/NAME.log is a command handed to a human; nothing in the tree reads such a file. Both spellings are writable paths.
  • CI greps — none on this identifier.
  • Test expectations — nothing asserts the .mts consumer's identifier.
  • check-ratchet-remedy-authority.mjs, whose table has a check-exported-any-returns.mts key, keys on script filenames (CORPUS_EXTENSIONS = ['.mjs', '.mts']), not on this derived name.

On the dispatch's lead about independent copies of the frame: check-dts-closure.mjs, check-i18n-bundles.mjs, check-i18n-coverage.mjs and check-published-readme-exports.mjs each spell their own prerequisiteNotMetText, but every one takes a message and hard-codes its own name literal — none derives a name from a path. This PR fixes the helper, which is the only place the derivation exists; "everywhere it is spelled" is not a different outcome here because the other spellings have no derivation to fix.

Before / after — paired, same tree, same run, no node_modules

Exit code captured before any pipe in every reading. Both are exit 3 before and after; the refusal still refuses.

Before (at 0a61db1f5d):

$ node scripts/check-exported-any-returns.mts   # EXIT=3
check-exported-any-returns.mts: PREREQUISITE NOT MET — the dependency `typescript` is not installed
$ node scripts/check-comment-mask-corpus.mjs    # EXIT=3   (control)
check-comment-mask-corpus: PREREQUISITE NOT MET — the dependency `@typescript-eslint/parser` is not installed

After:

$ node scripts/check-exported-any-returns.mts   # EXIT=3
check-exported-any-returns: PREREQUISITE NOT MET — the dependency `typescript` is not installed
$ node scripts/check-comment-mask-corpus.mjs    # EXIT=3   (control, unchanged)
check-comment-mask-corpus: PREREQUISITE NOT MET — the dependency `@typescript-eslint/parser` is not installed

The control is what makes it a discrimination: the .mjs sibling is byte-identical before and after, so the .mts line moved because of the rule, not because of the run.

The half that deliberately did not move — the runnable path keeps its real extension:

  `node scripts/check-exported-any-returns.mts > /tmp/check-exported-any-returns.log 2>&1; echo "EXIT=$?"`

(the log sink follows the name and drops the extension; both spellings are writable, and no .mjs/.mts basename collision exists in the gate corpus — the X.mjs + X.d.mts pairs in the tree are type declarations, which hold no runtime code and never import this module).

Tests

Five new self-test cases, pinned as a pair on one fixture (a .mts gate and a .mjs sibling differing only in extension), because a single observation cannot tell "the extension was stripped" from "there was never one to strip". They cover both halves: the name loses the extension, and the command keeps it.

  • node scripts/import-prerequisite.mjs --self-test -> exit 0, 76 cases pass (71 before).
  • Ablation (restoring the .mjs-only list, mutation proven on disk by before/after grep -c, restored under a trap and verified by blob hash ef5f4df360… plus an empty git diff HEAD): exit 1, 2 of 76 fail — precisely the two name-reading cases, while the two command cases and the .mjs control stayed green. Direction predicted before running and matched.
  • Dispatch-gates reconciliation: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. Two initially returned exit 3 (PREREQUISITE NOT MET — the very frame this card is about, read as NOT MEASURED, never a pass); cleared by building the @objectstack/lint / @objectstack/formula closures through scripts/pm/os-verify-lock.sh and re-run to exit 0.
  • Repo-wide eslint . --no-inline-config: 6309 files received by eslint, 0 errors, 0 warnings, exit 0 — run at final commit, so no narrowing needed and none claimed.

Changeset

skip-changesetscripts/import-prerequisite.mjs is repo-internal tooling at the repo root; no published package ships it (verified against every non-private manifest's files), so nothing is released by this change. Label applied at PR-open time.


🤖 Generated with Claude Code

https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8


Generated by Claude Code

…le extension, not only `.mjs`

The refusal frame prints two different strings: the gate's NAME in the headline
and the PATH the reader runs. The name was derived with a `.mjs`-only strip
written when every importer was `.mjs`, so the first TypeScript importer
(`scripts/check-exported-any-returns.mts`) printed its extension in the one
message a reader is meant to quote back.

The extension comes off, and the rule is now stated rather than left as a
regex: `GATE_MODULE_EXTENSIONS` declares the two module extensions this repo's
gate corpus uses, and `gateNameOf` removes whichever one the file has. The test
that settles the direction is what the reader has to type, and it does not
point at the extension either way -- the runnable string is `command`, which
keeps the real path with its real extension, and the way a reader re-runs the
gate is its package script (`check:exported-any-returns`), which has no
extension at all.

The PATH half is deliberately unmoved: stripping it would hand the reader a
file that does not exist. Five self-test cases pin the pair on one fixture --
a `.mts` gate and a `.mjs` sibling differing only in extension -- so neither
half can drift without a red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 7, 2026
@github-actions github-actions Bot added the size/s label Sep 7, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 7, 2026 17:52
@baozhoutao
baozhoutao enabled auto-merge September 7, 2026 17:52
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit ecf44b1 Sep 7, 2026
36 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-15864-prerequisite-gate-name-extension branch September 7, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: the prerequisite refusal strips only .mjs when deriving the gate name, so the first .mts consumer prints its extension

2 participants