Skip to content

Commit ecf44b1

Browse files
claude[bot]claude
andauthored
fix(tooling): derive the prerequisite gate name by stripping the module extension, not only .mjs (#16670)
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. Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 67ccfe8 commit ecf44b1

1 file changed

Lines changed: 78 additions & 1 deletion

File tree

‎scripts/import-prerequisite.mjs‎

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -875,12 +875,61 @@ export function reportPrerequisiteNotMet(importerUrl, verdict, measures) {
875875
process.exit(EXIT_PREREQUISITE_NOT_MET);
876876
}
877877

878+
/**
879+
* The module extensions a gate in this repo is written in. The corpus is
880+
* exactly these two, and `check-ratchet-remedy-authority.mjs` already declares
881+
* the same pair for the same reason. Declared as a list rather than inlined as
882+
* a character class so the rule below reads as a rule, and so widening it is an
883+
* edit someone has to mean.
884+
*/
885+
const GATE_MODULE_EXTENSIONS = ['.mjs', '.mts'];
886+
887+
/**
888+
* The gate's NAME: its basename with the module extension removed, whichever of
889+
* the two it is.
890+
*
891+
* The identifier in the headline and the command the reader runs are two
892+
* different strings -- the block below says so, and this is the half that is a
893+
* NAME. The extension comes off for `.mts` as it always has for `.mjs`, and the
894+
* test that settles it is what the reader actually has to type:
895+
*
896+
* - The reader never types this identifier. The runnable string is `command`,
897+
* built by `importerCommandPath`, which keeps the real path WITH its real
898+
* extension (`node scripts/check-exported-any-returns.mts`) and is already
899+
* correct for both spellings.
900+
* - What the reader types to RE-RUN the gate is its package script --
901+
* `pnpm --filter @objectstack/client check:exported-any-returns` -- which
902+
* carries no extension at all. A `.mts` gate runs through `tsx`, not `node`,
903+
* so its file extension is not the reader's entry point either.
904+
*
905+
* So an extension here is not information the reader needs; it only appeared
906+
* because a strip written when every gate was `.mjs` stopped matching the file
907+
* family that arrived later. The rule is stated here rather than left as a
908+
* regex because the other defensible reading -- keep the extension and call the
909+
* `.mjs` gates the inconsistent ones -- would change what EVERY importer prints,
910+
* which is a different change from this one.
911+
*
912+
* ⛔ Not a path: a basename, always. `prerequisiteNotMetText`'s own comment and
913+
* the self-test below both hold that boundary, and it is unchanged here.
914+
*
915+
* The `X.mjs` + `X.d.mts` pairs in this tree are NOT a name collision waiting
916+
* to happen: a `.d.mts` is a type declaration beside its implementation, holds
917+
* no runtime code and is never executed, so it never imports this module and
918+
* never reaches this function. ⛔ Do not add a `.d` case for it — pinning an
919+
* input that cannot occur is the phantom check AGENTS.md warns about.
920+
*/
921+
function gateNameOf(importerUrl) {
922+
const base = fileURLToPath(importerUrl).split('/').pop();
923+
const ext = GATE_MODULE_EXTENSIONS.find((e) => base.endsWith(e));
924+
return ext ? base.slice(0, -ext.length) : base;
925+
}
926+
878927
/**
879928
* The text `reportPrerequisiteNotMet` prints, as a value — so the self-test can
880929
* assert on the advisory without spawning a process or stubbing `process.exit`.
881930
*/
882931
function prerequisiteNotMetText(importerUrl, verdict, measures) {
883-
const gate = fileURLToPath(importerUrl).split('/').pop().replace(/\.mjs$/, '');
932+
const gate = gateNameOf(importerUrl);
884933
// The path to RUN and the name to CALL IT BY are two different strings, and
885934
// only the first moves. ⛔ The `/tmp/${gate}.log` sink below keeps the
886935
// BASENAME on purpose: a repo-relative path there would spell
@@ -1197,6 +1246,34 @@ export function selfTest() {
11971246
advisoryFor(lintGate).includes('> /tmp/check-doc-formula-expressions.log 2>&1')
11981247
&& !advisoryFor(lintGate).includes('/tmp/packages/lint'));
11991248

1249+
// (g) The module extension comes off for `.mts` exactly as it always did
1250+
// for `.mjs`. Pinned as a PAIR on the same fixture — one `.mts` gate and
1251+
// one `.mjs` gate differing only in extension — because a single
1252+
// observation cannot tell "the extension was stripped" from "there was
1253+
// never one to strip", which is the discrimination this card turned on.
1254+
const mtsGate = join(wt, 'scripts', 'check-fixture-any-returns.mts');
1255+
const mjsSibling = join(wt, 'scripts', 'check-fixture-any-returns.mjs');
1256+
t('a `.mts` importer is named WITHOUT its extension',
1257+
advisoryFor(mtsGate).includes('\ncheck-fixture-any-returns: PREREQUISITE NOT MET')
1258+
&& !advisoryFor(mtsGate).includes('check-fixture-any-returns.mts: PREREQUISITE'),
1259+
advisoryFor(mtsGate));
1260+
t('CONTROL: its `.mjs` sibling prints the SAME name, so the identifier is the gate\'s, not the file\'s',
1261+
advisoryFor(mjsSibling).includes('\ncheck-fixture-any-returns: PREREQUISITE NOT MET')
1262+
&& !advisoryFor(mjsSibling).includes('check-fixture-any-returns.mjs: PREREQUISITE'),
1263+
advisoryFor(mjsSibling));
1264+
1265+
// ⛔ The half that must NOT move with it. The command is a PATH: strip its
1266+
// extension and the reader is handed a file that does not exist — the same
1267+
// defect this module already fixed once, relocated one token to the right.
1268+
t('⛔ but the COMMAND still carries the real `.mts` extension — a path, not a name',
1269+
advisoryFor(mtsGate).includes('`node scripts/check-fixture-any-returns.mts > '),
1270+
advisoryFor(mtsGate));
1271+
t('and the `.mjs` sibling\'s command keeps ITS real extension too',
1272+
advisoryFor(mjsSibling).includes('`node scripts/check-fixture-any-returns.mjs > '));
1273+
t('the /tmp log sink follows the NAME, so it carries no extension either',
1274+
advisoryFor(mtsGate).includes('> /tmp/check-fixture-any-returns.log 2>&1'),
1275+
advisoryFor(mtsGate));
1276+
12001277
// ── the CLOSURE: every declared prerequisite, in one command ────────────
12011278
//
12021279
// The acceptance this card was ruled on: what is pinned is the printed

0 commit comments

Comments
 (0)