From 5ef0c826a4114486b9d1e98cce1716a37a824a57 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 21:04:48 +0000 Subject: [PATCH 1/3] refactor(tooling): route all 29 scripts/ entry guards through isEntrypoint The second half of objectui#6092. PR #6133 landed the gate and the shrink-only baseline it measured; this converts every site the baseline named and empties it. Nine distinct hand-typed spellings across 28 .mjs files became one: import { isEntrypoint } from './invoked-as.mjs'; if (isEntrypoint(import.meta.url)) { ... } Twenty-eight of those spellings were WRONG, in the direction nothing in CI can see: node resolves symlinks for the module graph but leaves process.argv[1] as the caller typed it, so a gate reached through a symlink compared two different paths, answered false, and did nothing -- exit 0, no output, which a wrapper holding result.status reads as a pass. One spelling (check-node-esm-load.mjs) went inert with no symlink at all, percent-encoding apart from argv[1] in any directory whose name needs encoding. scripts/shadcn-sync.js is the twenty-ninth and is different in kind: its invokedAsCli() already compared through realpathSync, so it was already correct. Its conversion is a SIMPLIFICATION, not a fix, and the call site now says so. KNOWN_HAND_TYPED_GUARDS is empty. It stays, because the reconciliation it feeds is the live rule: with no lines left, any scripts/ file that hand-types a guard fails as FRESH and names itself. The gate's header is rewritten to the swept state rather than left describing a tree that no longer exists -- the ported-prose defect objectui#6078 recorded. check-doc-component-types.test.ts pinned "needs no install" by requiring every import in that gate to start with `node:`. A relative import of a builtins-only local module keeps that claim true but not that spelling, so the assertion now walks the whole static import graph and requires every leaf to be a builtin. That is the stronger form of the same claim, not a loosened one. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe --- .../check-doc-component-types.test.ts | 32 ++- scripts/check-action-forward-parity.mjs | 3 +- scripts/check-changeset-fixed.mjs | 3 +- scripts/check-changeset-no-major.mjs | 3 +- scripts/check-changeset-presence.mjs | 3 +- scripts/check-control-bytes.mjs | 3 +- scripts/check-cross-repo-closer-outcome.mjs | 4 +- scripts/check-designer-field-key-parity.mjs | 3 +- scripts/check-doc-component-types.mjs | 3 +- scripts/check-doc-links.mjs | 3 +- scripts/check-doc-snippet-types.mjs | 3 +- scripts/check-eager-closure-budget.mjs | 4 +- scripts/check-entry-guard.mjs | 190 +++++++++--------- scripts/check-i18n-call-site-keys.mjs | 3 +- scripts/check-i18n-dead-keys.mjs | 3 +- scripts/check-i18n-en-drift.mjs | 3 +- scripts/check-lucide-icon-record-names.mjs | 3 +- scripts/check-node-esm-load.mjs | 3 +- scripts/check-package-self-import.mjs | 3 +- scripts/check-phantom-dependencies.mjs | 3 +- scripts/check-published-dist-tooling.mjs | 3 +- scripts/check-skills-paths.mjs | 3 +- scripts/check-spec-symbol-derivation.mjs | 3 +- scripts/check-type-check-coverage.mjs | 3 +- scripts/dependabot-merge-gate.mjs | 4 +- scripts/extract-mdx-demos.mjs | 4 +- scripts/regenerate-known-schema-types.mjs | 3 +- scripts/render-budget-comment.mjs | 4 +- scripts/shadcn-check-report.mjs | 5 +- scripts/shadcn-sync.js | 31 +-- scripts/sync-quick-reference-release.mjs | 3 +- 31 files changed, 185 insertions(+), 159 deletions(-) diff --git a/scripts/__tests__/check-doc-component-types.test.ts b/scripts/__tests__/check-doc-component-types.test.ts index 542a456399..d49a312f94 100644 --- a/scripts/__tests__/check-doc-component-types.test.ts +++ b/scripts/__tests__/check-doc-component-types.test.ts @@ -546,9 +546,35 @@ describe('wiring — the gate is reachable and a docs-only PR starts it', () => const yaml = yamlOf('doc-component-types.yml'); expect(yaml).not.toContain('pnpm install'); expect(yaml).not.toContain('corepack'); - const gate = fs.readFileSync(path.join(repoRoot, SCRIPT), 'utf8'); - const imports = [...gate.matchAll(/^import .* from '([^']+)';$/gm)].map((m) => m[1]); - expect(imports.every((spec) => spec.startsWith('node:')), `non-builtin import in the gate: ${imports}`).toBe(true); + + // Walk the WHOLE static import graph, not just the gate's own first line. + // objectui#6092 converted this gate's entry guard to `./invoked-as.mjs`, a + // relative import — install-free, but not spelled `node:`. Asserting on the + // gate's own imports alone would have had to be loosened to let that + // through, and a loosened one-file assertion is how a relative import that + // DOES pull a package in later lands unnoticed. Following the graph keeps + // the original claim ("this needs no node_modules") literally true, and + // makes it true of every module the gate reaches. + const seen = new Set(); + const external: string[] = []; + const walk = (abs: string) => { + if (seen.has(abs)) return; + seen.add(abs); + const source = fs.readFileSync(abs, 'utf8'); + for (const m of source.matchAll(/^import .* from '([^']+)';$/gm)) { + const spec = m[1]; + if (spec.startsWith('node:')) continue; + if (!spec.startsWith('.')) { + external.push(`${path.relative(repoRoot, abs)} -> ${spec}`); + continue; + } + walk(path.resolve(path.dirname(abs), spec)); + } + }; + walk(path.join(repoRoot, SCRIPT)); + + expect(seen.size, 'the import walk read only the gate itself — it followed nothing').toBeGreaterThan(1); + expect(external, `the gate's import graph reaches a package, so it needs an install: ${external}`).toEqual([]); }); }); diff --git a/scripts/check-action-forward-parity.mjs b/scripts/check-action-forward-parity.mjs index c7377ec1fe..10a16881a5 100644 --- a/scripts/check-action-forward-parity.mjs +++ b/scripts/check-action-forward-parity.mjs @@ -127,6 +127,7 @@ import { createRequire } from "module"; import { readFileSync, existsSync } from "fs"; import { resolve, dirname } from "path"; import { fileURLToPath } from "url"; +import { isEntrypoint } from "./invoked-as.mjs"; const HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(HERE, ".."); @@ -1058,7 +1059,7 @@ export function analyze(root = REPO_ROOT, options = {}) { } // ── CLI ────────────────────────────────────────────────────────────────────── -const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url)); +const invokedDirectly = isEntrypoint(import.meta.url); if (invokedDirectly) { let result; diff --git a/scripts/check-changeset-fixed.mjs b/scripts/check-changeset-fixed.mjs index 26b1c69314..fc2d74218d 100644 --- a/scripts/check-changeset-fixed.mjs +++ b/scripts/check-changeset-fixed.mjs @@ -62,6 +62,7 @@ import { readFileSync, readdirSync, statSync } from "fs"; import { resolve, dirname, join } from "path"; import { fileURLToPath } from "url"; +import { isEntrypoint } from "./invoked-as.mjs"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); @@ -229,6 +230,6 @@ function main() { } // Only run when invoked as a script — the tests import the helpers above. -if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { +if (isEntrypoint(import.meta.url)) { process.exit(main()); } diff --git a/scripts/check-changeset-no-major.mjs b/scripts/check-changeset-no-major.mjs index c8d33d6bb2..662e37c189 100644 --- a/scripts/check-changeset-no-major.mjs +++ b/scripts/check-changeset-no-major.mjs @@ -44,6 +44,7 @@ import { readFileSync, readdirSync } from 'node:fs'; import { resolve, dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const changesetDir = resolve(root, '.changeset'); @@ -158,6 +159,6 @@ release sets OBJECTUI_ALLOW_MAJOR=1. } // Only run when invoked as a script — the tests import the parser above. -if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { +if (isEntrypoint(import.meta.url)) { process.exit(main()); } diff --git a/scripts/check-changeset-presence.mjs b/scripts/check-changeset-presence.mjs index acd7077547..986670ed76 100644 --- a/scripts/check-changeset-presence.mjs +++ b/scripts/check-changeset-presence.mjs @@ -247,6 +247,7 @@ import { execFileSync } from 'node:child_process'; import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; const scriptDir = dirname(fileURLToPath(import.meta.url)); @@ -722,7 +723,7 @@ export function verdict(analysis) { // -- CLI ---------------------------------------------------------------------- -const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url)); +const invokedDirectly = isEntrypoint(import.meta.url); if (invokedDirectly) { const argOf = (name) => { diff --git a/scripts/check-control-bytes.mjs b/scripts/check-control-bytes.mjs index 0534bf32cc..3f8628a187 100644 --- a/scripts/check-control-bytes.mjs +++ b/scripts/check-control-bytes.mjs @@ -88,6 +88,7 @@ import { execFileSync } from 'node:child_process'; import { lstatSync, readFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; /** * Control characters that are never legitimate in a text file: the C0 range @@ -356,7 +357,7 @@ nobody removes is how a baseline turns into a permanent skip-list.`); // Run only when invoked directly — the test suite imports `scan`/`classify` // from here and must not trigger a repo scan (or a process.exit) on import. // Same guard shape as scripts/check-changeset-no-major.mjs. -const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url); +const invokedDirectly = isEntrypoint(import.meta.url); if (invokedDirectly) { if (process.argv.includes('--list')) { diff --git a/scripts/check-cross-repo-closer-outcome.mjs b/scripts/check-cross-repo-closer-outcome.mjs index e4918ef977..50e844647b 100644 --- a/scripts/check-cross-repo-closer-outcome.mjs +++ b/scripts/check-cross-repo-closer-outcome.mjs @@ -129,8 +129,8 @@ import { execFileSync } from 'node:child_process'; import { createRequire } from 'node:module'; import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; -import { pathToFileURL } from 'node:url'; import { isMap, isSeq, parseDocument } from 'yaml'; +import { isEntrypoint } from './invoked-as.mjs'; const WORKFLOW = '.github/workflows/cross-repo-issue-closer.yml'; const JOB = 'close-foreign-issues'; @@ -1170,7 +1170,7 @@ async function selfTest() { // reverse-verification route needs `extractScript` / `judge` pointed at another // tree (a pre-fix checkout), and a module that runs its gate on import would // silently judge THIS repo instead and print a pass about the wrong subject. -if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { +if (isEntrypoint(import.meta.url)) { if (process.argv.includes('--self-test')) await selfTest(); else if (process.argv.includes('--list')) list(); else await main(); diff --git a/scripts/check-designer-field-key-parity.mjs b/scripts/check-designer-field-key-parity.mjs index 47b1df136b..254a987a2d 100644 --- a/scripts/check-designer-field-key-parity.mjs +++ b/scripts/check-designer-field-key-parity.mjs @@ -125,6 +125,7 @@ import { createRequire } from "module"; import { readFileSync, existsSync } from "fs"; import { resolve, dirname } from "path"; import { fileURLToPath } from "url"; +import { isEntrypoint } from "./invoked-as.mjs"; const HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(HERE, ".."); @@ -459,6 +460,6 @@ async function main() { console.log("\ndesigner-field-key-parity: OK"); } -if (resolve(process.argv[1] ?? "") === resolve(fileURLToPath(import.meta.url))) { +if (isEntrypoint(import.meta.url)) { await main(); } diff --git a/scripts/check-doc-component-types.mjs b/scripts/check-doc-component-types.mjs index 4e9bccde12..a1e7015f7f 100644 --- a/scripts/check-doc-component-types.mjs +++ b/scripts/check-doc-component-types.mjs @@ -137,6 +137,7 @@ import { readFileSync, readdirSync, statSync } from 'node:fs'; import { dirname, join, relative, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; const scriptDir = dirname(fileURLToPath(import.meta.url)); @@ -995,7 +996,7 @@ const HINTS = { 'A doc file has an unclosed ``` fence. The scan cannot separate code from prose past that point.', }; -const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url)); +const invokedDirectly = isEntrypoint(import.meta.url); if (invokedDirectly) { const argOf = (name) => { diff --git a/scripts/check-doc-links.mjs b/scripts/check-doc-links.mjs index 74d9d0c9b6..80d16c7d64 100644 --- a/scripts/check-doc-links.mjs +++ b/scripts/check-doc-links.mjs @@ -460,6 +460,7 @@ import { readdirSync, readFileSync, statSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; const DOCS_ROUTE_PREFIX = '/docs'; const MARKDOWN_LINK_RE = /\[[^\]]+\]\(([^)]+)\)/g; @@ -967,7 +968,7 @@ export function collectBrokenLinks(repoRoot) { // Run only when invoked directly — the test suite imports the helpers above and // must not trigger a repo scan (or a `process.exit`) on import. Same guard shape // as scripts/check-control-bytes.mjs. -const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); +const invokedDirectly = isEntrypoint(import.meta.url); const HINTS = { relative: diff --git a/scripts/check-doc-snippet-types.mjs b/scripts/check-doc-snippet-types.mjs index e574f02d1f..09b94d634d 100644 --- a/scripts/check-doc-snippet-types.mjs +++ b/scripts/check-doc-snippet-types.mjs @@ -259,6 +259,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; import { dirname, join, relative, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; import ts from 'typescript'; +import { isEntrypoint } from './invoked-as.mjs'; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); @@ -1162,7 +1163,7 @@ function main() { return 0; } -if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) { +if (isEntrypoint(import.meta.url)) { process.exit(main()); } diff --git a/scripts/check-eager-closure-budget.mjs b/scripts/check-eager-closure-budget.mjs index 4e40ad84a1..26e1378888 100644 --- a/scripts/check-eager-closure-budget.mjs +++ b/scripts/check-eager-closure-budget.mjs @@ -93,7 +93,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { pathToFileURL } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; /** * Ceiling for the console eager closure, in gzipped bytes. See the header for @@ -340,6 +340,6 @@ export function main(argv = process.argv.slice(2)) { return result.status === 'fail' ? 1 : 2; } -if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { +if (isEntrypoint(import.meta.url)) { process.exit(main()); } diff --git a/scripts/check-entry-guard.mjs b/scripts/check-entry-guard.mjs index 02351c3cd2..25af2f3d25 100644 --- a/scripts/check-entry-guard.mjs +++ b/scripts/check-entry-guard.mjs @@ -9,10 +9,13 @@ * * Ported from objectstack's gate of the same name (objectstack#10784 and the * cards above it), NOT copied: objectstack swept its tree BEFORE landing the - * gate, so its spelling rule admits no exceptions at all. This repository has - * not been swept. The port therefore carries a baseline that objectstack has - * no need for, and the whole design question of this file is how that baseline - * is shaped so it cannot become an allowlist. See "The baseline" below. + * gate, so its spelling rule admits no exceptions at all. This repository was + * swept the other way round -- gate first, then the sweep -- so the port landed + * carrying a baseline of 29 owed files that objectstack has no need for. That + * baseline is now EMPTY: objectui#6092's second half converted all 29 and + * deleted every line. The design question the shape answers -- how a baseline + * is kept from becoming an allowlist -- is what got it to empty, and is still + * what keeps a new line from being added. See "The baseline" below. * * Two rules live here. The first is about the SPELLING of a guard that exists; * the second, further down, is about a guard that is MISSING from a file that @@ -22,10 +25,12 @@ * ## What this gate is for * * A CLI script has to answer "did node run me, or did something import me?" - * before it does anything. Hand-typed answers to that question have drifted + * before it does anything. Hand-typed answers to that question HAD drifted * into NINE distinct spellings across 28 `.mjs` files in `scripts/` here -- - * measured on `133e2ea1e`, not estimated -- and every one of them is wrong in - * the same way. The dominant failure: + * measured on `133e2ea1e`, not estimated -- and every one of them was wrong in + * the same way. All 29 sites (those 28 plus `shadcn-sync.js`) now go through + * the predicate; what this gate is for is the TENTH spelling, which nothing + * else in CI can see. The dominant failure: * * node resolves symlinks for the module graph but leaves `process.argv[1]` * as the caller typed it @@ -34,16 +39,18 @@ * `false`, and does nothing -- **exit 0, no output**. The CI wrappers spawn * these tools and hold `result.status` only, so an inert child is a green gate. * - * That is measured in THIS tree, on a real blocking gate (objectui#6078): + * That was measured in THIS tree, on a real blocking gate (objectui#6078), + * against the spelling `check-skills-paths.mjs` carried before the sweep: * * node scripts/check-skills-paths.mjs direct : exit=1, 696 bytes * node /…/link/check-skills-paths.mjs (symlink) symlink : exit=0, 0 bytes * - * Same tree, same defect, same gate. A second spelling here goes inert with no - * symlink at all: `check-node-esm-load.mjs:847` writes + * Same tree, same defect, same gate. A second spelling went inert with no + * symlink at all: `check-node-esm-load.mjs` wrote * ``import.meta.url === `file://${process.argv[1]}` ``, which percent-encodes * apart from `argv[1]` in any directory whose name needs encoding (measured in - * a directory named `a#b c`). + * a directory named `a#b c`). Both files now use the predicate; the two + * measurements are kept because they are why the rule exists, not a to-do. * * `scripts/invoked-as.mjs` is the only place allowed to read `process.argv[1]`; * everywhere else spells the guard @@ -52,45 +59,51 @@ * * which has no comparison in it to get wrong. * - * ## Why the gate lands BEFORE the sweep + * ## Why the gate landed BEFORE the sweep * * The sweep on its own is worth little: nothing would stop a TENTH spelling * from being typed the next time someone adds a script, and the next one would * be just as invisible. That is not hypothetical here -- objectui#6092 measured * the worklist growing while the card sat open: `check-designer-field-key-parity.mjs` * added a guard between `a1c41c516` and `7c96c9420`. A sweep with no gate under - * it can be silently undone by the next pull request. So the gate lands first - * and names its own worklist; the conversion of the 29 existing call sites is - * the second half of objectui#6092 and is deliberately NOT in this file's PR. + * it can be silently undone by the next pull request. So the gate landed first + * and named its own worklist, and the conversion of those 29 call sites landed + * against it -- each conversion had to lower or delete its own baseline line or + * this gate failed STALE and named the file, which is what made the sweep + * self-checking rather than a claim. * * ## The baseline, and why it is not an allowlist * * ⛔ SHRINK-ONLY, and shaped so the difference is mechanical rather than a * promise. `KNOWN_HAND_TYPED_GUARDS` maps a file to the NUMBER of masked - * `process.argv[1]` occurrences it carried when this gate landed. Three - * consequences, and the third is the one that makes the whole thing worth - * landing: - * - * • a file NOT in the map that carries a guard fails -- a 30th spelling - * cannot land; - * • a file IN the map that carries MORE than its number fails -- a second - * guard cannot be smuggled into an already-owed file, which a + * `process.argv[1]` occurrences it carried when this gate landed. It is EMPTY + * now; the shape is what emptied it, in three consequences: + * + * • a file NOT in the map that carries a guard fails -- with the map empty + * that is every file, so this is the whole live rule today and a 30th + * spelling cannot land; + * • a file IN the map that carries MORE than its number failed -- a second + * guard could not be smuggled into an already-owed file, which a * path-only baseline would have accepted silently; - * • a file that carries FEWER fails as STALE and names itself, with the - * remedy being to lower or delete the line. There is no supported route - * that raises a number. That is what stops the map from rotting into a - * list nobody re-reads. - * - * Every entry has the same one-line remedy -- `isEntrypoint(import.meta.url)` - * -- so no entry records a judgement anyone has to re-make. That is the - * property that makes a debt list safe, and it is the only reason one is here. - * - * ONE entry is different in kind and is labelled as such rather than left to be - * re-derived: `scripts/shadcn-sync.js` hand-types the CORRECT two-leg shape - * (`realpathSync(resolved) === __filename`, objectui#6092). It is not a defect - * and this gate never reports it as one. It is in the map because the rule is - * "one predicate", not "one predicate or a second correct implementation" -- - * converting it is a simplification, not a fix. + * • a file that carried FEWER failed as STALE and named itself, with the + * remedy being to lower or delete the line. There was no supported route + * that raised a number, and there is none that adds one back. + * + * The last two are unreachable while the map is empty, and they stay in + * `reconcileGuards` (and pinned by the self-test) precisely because that is the + * state a re-added line would have to pass through: a baseline that only ever + * shrank cannot be re-opened as an allowlist. + * + * Every entry had the same one-line remedy -- `isEntrypoint(import.meta.url)` + * -- so no entry recorded a judgement anyone had to re-make. That is the + * property that made the debt list safe, and it is the only reason one was here. + * + * ONE of the 29 was different in kind: `scripts/shadcn-sync.js` hand-typed the + * CORRECT two-leg shape (`realpathSync(resolved) === __filename`), so this gate + * never reported it as a defect. It was in the map because the rule is "one + * predicate", not "one predicate or a second correct implementation" -- its + * conversion was a simplification, not a fix, and that distinction is now + * recorded at the call site in `shadcn-sync.js` rather than here. * * ## Why a spelling gate rather than a behavioural sweep * @@ -114,9 +127,13 @@ * Comments AND string/template/regex literals are masked before the scan * (`js-comment-mask.mjs`), because a `process.argv[1]` inside a string payload * for a spawned child is not an entry guard, and neither is one inside a - * docblock -- `shadcn-sync.js:1046` really does write one in prose, and an - * allowlist to excuse it would be a hole the next such file falls through - * silently. + * docblock. That masking is load-bearing, not defensive: after the sweep the + * only `scripts/` files that still contain the string at all are this gate + * (skipped -- it quotes the idioms it bans), `invoked-as.mjs` (the one module + * allowed to read it), and `js-comment-mask.mjs`, whose own corpus carries 8 + * occurrences in literals. Unmasked, that last file would read as a 30th + * hand-typed guard, and an allowlist to excuse it would be a hole the next such + * file falls through silently. * * ## What was deliberately NOT ported * @@ -252,58 +269,22 @@ export function scanFile(rel, source, { isPredicateHome = false } = {}) { } /** - * ⛔ SHRINK-ONLY. The hand-typed entry guards this tree carried when the gate - * landed, as `path -> number of masked process.argv[1] occurrences`. The - * rationale, and why a COUNT rather than a bare path, is in the header. Two - * shapes appear because a guard written - * `const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === …` - * reads `argv[1]` twice. - * - * The remedy for every line is the same: + * ⛔ SHRINK-ONLY, and now EMPTY. This carried the 29 hand-typed entry guards + * the tree had when the gate landed, as `path -> number of masked + * process.argv[1] occurrences`; objectui#6092's second half converted all 29 + * and deleted every line. The rationale, and why a COUNT rather than a bare + * path, is in the header. + * + * It stays here, empty, because the reconciliation it feeds is the live rule: + * a `scripts/` file that hand-types a guard is now a file the map does not + * carry, so it fails as FRESH and names itself. There is no supported route + * that adds a line back — the remedy for a new hand-typed guard is the same + * one-liner every deleted line had: * * import { isEntrypoint } from './invoked-as.mjs'; * if (isEntrypoint(import.meta.url)) { … } - * - * then delete the line from here. objectui#6092's second half does this sweep. - */ -const KNOWN_HAND_TYPED_GUARDS = new Map([ - ['scripts/check-action-forward-parity.mjs', 2], - ['scripts/check-changeset-fixed.mjs', 2], - ['scripts/check-changeset-no-major.mjs', 2], - ['scripts/check-changeset-presence.mjs', 2], - ['scripts/check-control-bytes.mjs', 2], - ['scripts/check-cross-repo-closer-outcome.mjs', 1], - ['scripts/check-designer-field-key-parity.mjs', 1], - ['scripts/check-doc-component-types.mjs', 2], - ['scripts/check-doc-links.mjs', 2], - ['scripts/check-doc-snippet-types.mjs', 2], - ['scripts/check-eager-closure-budget.mjs', 2], - ['scripts/check-i18n-call-site-keys.mjs', 2], - ['scripts/check-i18n-dead-keys.mjs', 2], - ['scripts/check-i18n-en-drift.mjs', 2], - ['scripts/check-lucide-icon-record-names.mjs', 2], - ['scripts/check-node-esm-load.mjs', 1], - ['scripts/check-package-self-import.mjs', 2], - ['scripts/check-phantom-dependencies.mjs', 2], - ['scripts/check-published-dist-tooling.mjs', 2], - ['scripts/check-skills-paths.mjs', 2], - ['scripts/check-spec-symbol-derivation.mjs', 2], - ['scripts/check-type-check-coverage.mjs', 2], - ['scripts/dependabot-merge-gate.mjs', 2], - ['scripts/extract-mdx-demos.mjs', 2], - ['scripts/regenerate-known-schema-types.mjs', 2], - ['scripts/render-budget-comment.mjs', 2], - ['scripts/shadcn-check-report.mjs', 2], - ['scripts/shadcn-sync.js', 1], - ['scripts/sync-quick-reference-release.mjs', 2], -]); - -/** - * The one entry above that is NOT a defect: it hand-types the CORRECT two-leg - * shape already. Named here so a reader of the map is not left to re-derive - * which of the entries is which, and so nothing later reports it as broken. */ -const CORRECT_SHAPE_BUT_HAND_TYPED = new Set(['scripts/shadcn-sync.js']); +const KNOWN_HAND_TYPED_GUARDS = new Map([]); /** * Reconcile what the tree carries against the shrink-only baseline. Pure, and @@ -626,9 +607,18 @@ export function importUnsafeStatements(source) { * ONE entry, and that is the point: this rule recognises a hand-typed guard AS * a guard (see `guardAliases`), so the 29 badly-spelled files are rule 1's * business and do not appear here. `check-lucide-icon-record-names.mjs` builds - * two lookup maps in top-level `for` loops at :242 and :244, outside any guard, - * and really does run them inside an importer. That is a true sentence with one - * remedy, which is the only kind of line a debt list may carry. + * two lookup maps in top-level `for` loops at :243 and :245, outside any guard, + * and really does run them inside an importer. + * + * ⚠️ Its remedy is NOT "move the loops behind the guard": those maps are read by + * the exported `liveSpellingFor` / `describeName`, which importers really call + * (`scripts/__tests__/check-lucide-icon-record-names.test.ts`), so guarding them + * turns a working import into a broken one. Measured on this branch, not + * reasoned: with the loops moved behind the guard that suite fails. objectui#6092 + * ruled the restructuring out of scope rather than trade an import for a + * baseline; the entry stays, and its remedy is a judgement someone still has to + * make -- which is exactly what the rest of this comment says a debt line must + * not be. It is the one line here that owes a card, not a one-liner. */ const KNOWN_IMPORT_UNSAFE = new Set(['scripts/check-lucide-icon-record-names.mjs']); @@ -760,7 +750,6 @@ function list() { const rel = relative(REPO_ROOT, abs); const found = scanFile(rel, readFileSync(abs, 'utf8'), { isPredicateHome: abs === PREDICATE_HOME }); if (!found.length) continue; - const note = CORRECT_SHAPE_BUT_HAND_TYPED.has(rel) ? ' [correct shape, hand-typed]' : ''; console.log(` ${String(found.length).padStart(2)} ${rel}${note}`); for (const f of found) console.log(` :${f.line} ${f.what}`); } @@ -967,12 +956,15 @@ export function selfTest() { t('else continues the statement before it', topLevelStatements(codeOnly('if (a) { x(); } else { y(); }\n')).length === 1); t('catch continues the statement before it', topLevelStatements(codeOnly('try { x(); } catch (e) { y(); }\n')).length === 1); - // ── the labelled entry is really in the baseline it is labelled against ── - // A label naming a file the map does not carry is a sentence about nothing, - // which is the failure mode this whole card exists to stop. - for (const rel of CORRECT_SHAPE_BUT_HAND_TYPED) { - t(`${rel} is labelled AND baselined, not just labelled`, KNOWN_HAND_TYPED_GUARDS.has(rel), rel); - } + // ── the baseline is empty, and that is an assertion, not a description ── + // With no lines left, the ONLY thing the map can still do is redden on a file + // that hand-types a guard. A test that read the map's size would pass on an + // empty map that had also stopped being consulted, so this asserts the + // reconciliation instead: an empty baseline must call a real guard FRESH. + t( + 'an EMPTY baseline still reddens on a hand-typed guard', + reconcileGuards(new Map([['scripts/anything.mjs', 1]]), new Map([])).fresh.length === 1, + ); const failed = cases.filter((c) => !c.ok); for (const c of failed) console.error(` ✗ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`); diff --git a/scripts/check-i18n-call-site-keys.mjs b/scripts/check-i18n-call-site-keys.mjs index 1e57f51a57..cbd4297018 100644 --- a/scripts/check-i18n-call-site-keys.mjs +++ b/scripts/check-i18n-call-site-keys.mjs @@ -332,6 +332,7 @@ import ts from 'typescript'; import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'; import { resolve, dirname, join, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; const scriptDir = dirname(fileURLToPath(import.meta.url)); @@ -1908,7 +1909,7 @@ const HINTS = { const quote = (text) => JSON.stringify(text).replace(/[^\x20-\x7e]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`); -const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url)); +const invokedDirectly = isEntrypoint(import.meta.url); if (invokedDirectly) { const root = resolve(scriptDir, '..'); diff --git a/scripts/check-i18n-dead-keys.mjs b/scripts/check-i18n-dead-keys.mjs index 2dd6d6a6d6..5e88c7aed4 100644 --- a/scripts/check-i18n-dead-keys.mjs +++ b/scripts/check-i18n-dead-keys.mjs @@ -112,6 +112,7 @@ import { dirname, join, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { analyze, collectEnKeys } from './check-i18n-call-site-keys.mjs'; +import { isEntrypoint } from './invoked-as.mjs'; const scriptDir = dirname(fileURLToPath(import.meta.url)); @@ -269,7 +270,7 @@ export function sweep(root) { // ── CLI ────────────────────────────────────────────────────────────────────── -const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url)); +const invokedDirectly = isEntrypoint(import.meta.url); if (invokedDirectly) { const root = resolve(scriptDir, '..'); diff --git a/scripts/check-i18n-en-drift.mjs b/scripts/check-i18n-en-drift.mjs index 093610b015..e02383b93f 100644 --- a/scripts/check-i18n-en-drift.mjs +++ b/scripts/check-i18n-en-drift.mjs @@ -145,6 +145,7 @@ import { execFileSync } from 'node:child_process'; import { existsSync, readFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; const scriptDir = dirname(fileURLToPath(import.meta.url)); @@ -550,7 +551,7 @@ export function analyze(root, { base, head = null }) { return { before, after, findings, counters }; } -const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url)); +const invokedDirectly = isEntrypoint(import.meta.url); if (invokedDirectly) { const argOf = (name) => { diff --git a/scripts/check-lucide-icon-record-names.mjs b/scripts/check-lucide-icon-record-names.mjs index ca7988bd3f..620c684913 100644 --- a/scripts/check-lucide-icon-record-names.mjs +++ b/scripts/check-lucide-icon-record-names.mjs @@ -88,6 +88,7 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join, relative, resolve, sep } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; /** This gate's OWN repo — where lucide and typescript are resolved from. */ const gateRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); @@ -573,7 +574,7 @@ export function analyze(root, { } // ── CLI ────────────────────────────────────────────────────────────────────── -const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url)); +const invokedDirectly = isEntrypoint(import.meta.url); if (invokedDirectly) { const result = analyze(gateRoot); const { counters, discovered, errors, violations } = result; diff --git a/scripts/check-node-esm-load.mjs b/scripts/check-node-esm-load.mjs index 5f4b119e6c..9996160706 100644 --- a/scripts/check-node-esm-load.mjs +++ b/scripts/check-node-esm-load.mjs @@ -101,6 +101,7 @@ import { fileURLToPath } from 'node:url'; import ts from 'typescript'; import { SKIP_DIRS, TOOLING_FILE, discoverPackages, moduleSpecifiers } from './check-phantom-dependencies.mjs'; +import { isEntrypoint } from './invoked-as.mjs'; const scriptDir = dirname(fileURLToPath(import.meta.url)); @@ -844,7 +845,7 @@ function main(argv) { return failed ? 1 : 0; } -if (import.meta.url === `file://${process.argv[1]}`) { +if (isEntrypoint(import.meta.url)) { process.exit(main(process.argv.slice(2))); } diff --git a/scripts/check-package-self-import.mjs b/scripts/check-package-self-import.mjs index 587dfc312d..2a9d432c1e 100644 --- a/scripts/check-package-self-import.mjs +++ b/scripts/check-package-self-import.mjs @@ -123,6 +123,7 @@ import { moduleSpecifiers, packageNameOf, } from './check-phantom-dependencies.mjs'; +import { isEntrypoint } from './invoked-as.mjs'; const scriptDir = dirname(fileURLToPath(import.meta.url)); @@ -443,7 +444,7 @@ const HINTS = { 'site has gone silently widens the hole for the next file that lands there.', }; -const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url)); +const invokedDirectly = isEntrypoint(import.meta.url); if (invokedDirectly) { const argOf = (name) => { diff --git a/scripts/check-phantom-dependencies.mjs b/scripts/check-phantom-dependencies.mjs index 6537de62ca..3298563188 100644 --- a/scripts/check-phantom-dependencies.mjs +++ b/scripts/check-phantom-dependencies.mjs @@ -200,6 +200,7 @@ import { builtinModules } from 'node:module'; import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; import { dirname, join, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; const scriptDir = dirname(fileURLToPath(import.meta.url)); @@ -703,7 +704,7 @@ const HINTS = { 'gone covers a package that may now be shipping undeclared imports.', }; -const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url)); +const invokedDirectly = isEntrypoint(import.meta.url); if (invokedDirectly) { const argOf = (name) => { diff --git a/scripts/check-published-dist-tooling.mjs b/scripts/check-published-dist-tooling.mjs index b8bd7d0fb2..281fb2ff0b 100644 --- a/scripts/check-published-dist-tooling.mjs +++ b/scripts/check-published-dist-tooling.mjs @@ -118,6 +118,7 @@ import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { PACKAGE_ROOTS, TOOLING_FILE, readReleaseGroup } from './check-phantom-dependencies.mjs'; +import { isEntrypoint } from './invoked-as.mjs'; const scriptDir = dirname(fileURLToPath(import.meta.url)); @@ -397,7 +398,7 @@ const HINTS = { 'unexaminable package must not read as a clean one (objectui#4846).', }; -const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url)); +const invokedDirectly = isEntrypoint(import.meta.url); if (invokedDirectly) { const argOf = (name) => { diff --git a/scripts/check-skills-paths.mjs b/scripts/check-skills-paths.mjs index 4fbef99cca..bfe61a904c 100644 --- a/scripts/check-skills-paths.mjs +++ b/scripts/check-skills-paths.mjs @@ -99,6 +99,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; /** The scan surface, relative to the repo root: markdown under this directory. */ export const SCAN_ROOT = 'skills'; @@ -351,7 +352,7 @@ exemption nobody removes is how a baseline turns into a permanent skip-list.`); // Run only when invoked directly — the test suite imports `scan()` and the // extractor from here and must not trigger a repo scan (or a `process.exit`) on // import. Same guard shape as `scripts/check-control-bytes.mjs`. -const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); +const invokedDirectly = isEntrypoint(import.meta.url); if (invokedDirectly) { if (process.argv.includes('--list')) { diff --git a/scripts/check-spec-symbol-derivation.mjs b/scripts/check-spec-symbol-derivation.mjs index a1dd66695f..931f86a3a1 100644 --- a/scripts/check-spec-symbol-derivation.mjs +++ b/scripts/check-spec-symbol-derivation.mjs @@ -152,6 +152,7 @@ import { createRequire } from "module"; import { readFileSync, readdirSync, statSync } from "fs"; import { resolve, dirname, join, relative } from "path"; import { fileURLToPath } from "url"; +import { isEntrypoint } from "./invoked-as.mjs"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); @@ -1176,5 +1177,5 @@ process.exit(1); // `scanFileForClaims` / `normalizeDoc` from here and must not trigger a repo // scan (or a process.exit) on import. Same guard shape as // scripts/check-control-bytes.mjs. -const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url); +const invokedDirectly = isEntrypoint(import.meta.url); if (invokedDirectly) main(); diff --git a/scripts/check-type-check-coverage.mjs b/scripts/check-type-check-coverage.mjs index 9a87ff6d40..f6747eddb0 100644 --- a/scripts/check-type-check-coverage.mjs +++ b/scripts/check-type-check-coverage.mjs @@ -64,6 +64,7 @@ import { readFileSync, readdirSync, statSync } from "fs"; import { resolve, dirname, join } from "path"; import { fileURLToPath } from "url"; +import { isEntrypoint } from "./invoked-as.mjs"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); @@ -802,6 +803,6 @@ export function main() { // imports `collect()` / `auditPackages()` from here and must not trigger a repo // scan (or a `process.exit`) on import. Same guard shape as // `scripts/check-skills-paths.mjs` and `scripts/check-control-bytes.mjs`. -const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url)); +const invokedDirectly = isEntrypoint(import.meta.url); if (invokedDirectly) main(); diff --git a/scripts/dependabot-merge-gate.mjs b/scripts/dependabot-merge-gate.mjs index a85a31a714..542e67b78b 100644 --- a/scripts/dependabot-merge-gate.mjs +++ b/scripts/dependabot-merge-gate.mjs @@ -109,7 +109,7 @@ */ import fs from 'node:fs'; -import { pathToFileURL } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; /** * Blocking checks whose workflow subscribes `pull_request` with NO trigger-level @@ -499,7 +499,7 @@ export async function main({ api, env = process.env } = {}) { return result; } -const invokedDirectly = process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url; +const invokedDirectly = isEntrypoint(import.meta.url); if (invokedDirectly) { await main(); } diff --git a/scripts/extract-mdx-demos.mjs b/scripts/extract-mdx-demos.mjs index 4ffa615403..d71c68518f 100644 --- a/scripts/extract-mdx-demos.mjs +++ b/scripts/extract-mdx-demos.mjs @@ -29,6 +29,7 @@ import fs from 'node:fs'; import path from 'node:path'; import vm from 'node:vm'; import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, '..'); @@ -413,6 +414,5 @@ export { DEMO_MODULE, findDemoImports, rewriteDemoImports, stripCode }; // Only run the CLI when executed directly — the import rewriter above is // imported by scripts/__tests__/extract-mdx-demos.test.ts. -const invokedDirectly = - process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); +const invokedDirectly = isEntrypoint(import.meta.url); if (invokedDirectly) main(); diff --git a/scripts/regenerate-known-schema-types.mjs b/scripts/regenerate-known-schema-types.mjs index 2bf6a8bb4c..d3b1725dd5 100644 --- a/scripts/regenerate-known-schema-types.mjs +++ b/scripts/regenerate-known-schema-types.mjs @@ -75,6 +75,7 @@ import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { deriveRegistryKeys } from './check-doc-component-types.mjs'; +import { isEntrypoint } from './invoked-as.mjs'; const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); export const TARGET = 'packages/cli/src/utils/known-schema-types.ts'; @@ -184,6 +185,6 @@ function main() { console.log(`wrote ${TARGET}`); } -if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { +if (isEntrypoint(import.meta.url)) { main(); } diff --git a/scripts/render-budget-comment.mjs b/scripts/render-budget-comment.mjs index 9cacaeddeb..a80554435b 100644 --- a/scripts/render-budget-comment.mjs +++ b/scripts/render-budget-comment.mjs @@ -27,7 +27,7 @@ */ import fs from 'node:fs'; -import { pathToFileURL } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; /** * The only two statuses that carry a measurement. Kept as an explicit @@ -191,7 +191,7 @@ export function renderFromEnv(env = process.env, sizeReportPath = 'size-report.m } // CLI: write the comment body to stdout for the workflow to capture. -if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { +if (isEntrypoint(import.meta.url)) { const { kind, body } = renderFromEnv(); process.stderr.write(`Rendered performance budget comment (kind: ${kind})\n`); process.stdout.write(body); diff --git a/scripts/shadcn-check-report.mjs b/scripts/shadcn-check-report.mjs index 0013b1efbe..f00702b61f 100644 --- a/scripts/shadcn-check-report.mjs +++ b/scripts/shadcn-check-report.mjs @@ -58,7 +58,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { pathToFileURL } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; /** * How many consecutive unreachable runs escalate into the issue channel. @@ -605,8 +605,7 @@ export async function main({ api } = {}) { return { alarm, reasons, checkClass, analyzeClass, registryState, streak }; } -const invokedDirectly = - process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url; +const invokedDirectly = isEntrypoint(import.meta.url); if (invokedDirectly) { await main(); } diff --git a/scripts/shadcn-sync.js b/scripts/shadcn-sync.js index 9c6bccbba4..24ab51de99 100755 --- a/scripts/shadcn-sync.js +++ b/scripts/shadcn-sync.js @@ -22,9 +22,9 @@ */ import fs from 'fs/promises'; -import { realpathSync } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; +import { isEntrypoint } from './invoked-as.mjs'; import https from 'https'; import { spawnSync } from 'child_process'; import crypto from 'crypto'; @@ -1036,27 +1036,14 @@ async function main() { } } -/** - * Is this file the process entry point? - * - * The CLI must keep running exactly as before when invoked as - * `node scripts/shadcn-sync.js …`, while an `import` of this module (the tests - * in `scripts/__tests__/shadcn-sync-fetch-cache.test.ts`) must NOT execute it. - * Compared through `realpathSync` because ESM resolves `import.meta.url` to the - * real path while `process.argv[1]` may still carry a symlink. - */ -function invokedAsCli() { - const entry = process.argv[1]; - if (!entry) return false; - const resolved = path.resolve(entry); - try { - return realpathSync(resolved) === __filename; - } catch { - return resolved === __filename; - } -} - -if (invokedAsCli()) { +// This file is the ONE of objectui#6092's 29 hand-typed guards that was not +// wrong: the deleted `invokedAsCli()` compared through `realpathSync`, so it +// already answered correctly through a symlink. Replacing it with the shared +// predicate is therefore a SIMPLIFICATION, not a fix — one spelling fewer for a +// reader to re-derive, and one fewer place for the realpath leg to be dropped +// by a later edit. `scripts/invoked-as.mjs` carries the rationale, and adds the +// `node ` case this hand-typed copy never had. +if (isEntrypoint(import.meta.url)) { main().catch(error => { log(`Fatal error: ${error.message}`, 'red'); console.error(error); diff --git a/scripts/sync-quick-reference-release.mjs b/scripts/sync-quick-reference-release.mjs index 9b187fe560..d4eb8bc844 100644 --- a/scripts/sync-quick-reference-release.mjs +++ b/scripts/sync-quick-reference-release.mjs @@ -62,6 +62,7 @@ import { readFileSync, writeFileSync, readdirSync, existsSync } from 'node:fs'; import { resolve, dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); @@ -405,6 +406,6 @@ export function main(argv = process.argv.slice(2), root = repoRoot) { } // Only run when invoked as a script — the tests import the functions above. -if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { +if (isEntrypoint(import.meta.url)) { process.exit(main()); } From 9b61f435f66a5c33cc1d6f62cd3b4d036180a81d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 21:08:05 +0000 Subject: [PATCH 2/3] docs(tooling): record the measured lucide import-safety ruling in the gate The KNOWN_IMPORT_UNSAFE comment said the entry had "one remedy". It does not: moving check-lucide-icon-record-names.mjs's two top-level loops behind the guard turns rule 2 green and breaks the module's importers. Measured, not reasoned -- 5 of 25 cases in the importing suite fail, and describeName() starts printing a WRONG diagnosis for a real violation rather than merely failing. So the line stays and the comment now says why, including the lazy-build shape a future card should consider. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe --- scripts/check-entry-guard.mjs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/scripts/check-entry-guard.mjs b/scripts/check-entry-guard.mjs index 25af2f3d25..0b4e19154a 100644 --- a/scripts/check-entry-guard.mjs +++ b/scripts/check-entry-guard.mjs @@ -610,15 +610,23 @@ export function importUnsafeStatements(source) { * two lookup maps in top-level `for` loops at :243 and :245, outside any guard, * and really does run them inside an importer. * - * ⚠️ Its remedy is NOT "move the loops behind the guard": those maps are read by - * the exported `liveSpellingFor` / `describeName`, which importers really call - * (`scripts/__tests__/check-lucide-icon-record-names.test.ts`), so guarding them - * turns a working import into a broken one. Measured on this branch, not - * reasoned: with the loops moved behind the guard that suite fails. objectui#6092 - * ruled the restructuring out of scope rather than trade an import for a - * baseline; the entry stays, and its remedy is a judgement someone still has to - * make -- which is exactly what the rest of this comment says a debt line must - * not be. It is the one line here that owes a card, not a one-liner. + * ⚠️ Its remedy is NOT "move the loops behind the guard". Those maps are read by + * the exported `liveSpellingFor` / `describeName`, which importers really call, + * so guarding them empties the maps for every importer. Measured on objectui#6092's + * branch rather than reasoned: with the two loops moved inside the guard, rule 2 + * goes green and reports the entry as STALE -- and + * `scripts/__tests__/check-lucide-icon-record-names.test.ts` fails 5 of 25. + * The failure is not merely a red test. `describeName('BarChart3')` stops saying + * "write `chart-column`" and says "no live key names the same glyph" instead: + * a WRONG diagnosis for a real violation, printed by a gate that still exits 1, + * which is a worse outcome than the unguarded loops. + * + * So objectui#6092 ruled the restructuring out of scope rather than trade a + * working import for a baseline line. The entry stays, and its remedy is a + * judgement someone still has to make -- which is exactly what the rest of this + * comment says a debt line must not be. It is the one line here that owes a + * card, not a one-liner. A lazy build of the two maps inside `liveSpellingFor` + * would satisfy both, and is the shape that card should consider. */ const KNOWN_IMPORT_UNSAFE = new Set(['scripts/check-lucide-icon-record-names.mjs']); From 06ad4dff00e20bbf60a489c1aa15813c741c859f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 21:08:46 +0000 Subject: [PATCH 3/3] chore(changeset): declare the entry-guard sweep as tooling only Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe --- .changeset/6092-entry-guard-sweep.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changeset/6092-entry-guard-sweep.md diff --git a/.changeset/6092-entry-guard-sweep.md b/.changeset/6092-entry-guard-sweep.md new file mode 100644 index 0000000000..e63819ad6d --- /dev/null +++ b/.changeset/6092-entry-guard-sweep.md @@ -0,0 +1,4 @@ +--- +--- + +Tooling only, no package released: every `scripts/**` entry guard now goes through one predicate. The 29 hand-typed guards `check-entry-guard.mjs` baselined (nine distinct spellings across 28 `.mjs` files, plus `shadcn-sync.js`) are converted to `isEntrypoint(import.meta.url)` and `KNOWN_HAND_TYPED_GUARDS` is empty. Twenty-eight of them were silently wrong: reached through a symlink they compared two different paths, answered `false`, and did nothing — exit 0 with no output, which a CI wrapper holding `result.status` reads as a pass (objectui#6092).