From 804afaa66e7b114866fa5df978404e28778a21bc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 21:30:27 +0000 Subject: [PATCH] refactor(tooling): build the lucide gate's two lookup maps lazily, not at import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `check-lucide-icon-record-names.mjs` built `keyByComponent` and `kebabByKey` in two top-level `for` loops, so importing the module for its exports ran them. That was the sole entry in `check-entry-guard.mjs`'s `KNOWN_IMPORT_UNSAFE`. The obvious remedy — move the loops behind the entry guard — was measured and rejected: it leaves the maps empty for importers, so `describeName('BarChart3')` prints a WRONG diagnosis for a real violation from a gate that still exits 1. They are now built on first read inside `liveSpellingFor`, memoised. Nothing runs at module top level, every importer gets what it got before, and the CLI output is byte-identical. `KNOWN_IMPORT_UNSAFE` goes to zero in the same commit, because the baseline is shrink-only in both directions. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe --- scripts/check-entry-guard.mjs | 26 ++++++++++++----- scripts/check-lucide-icon-record-names.mjs | 34 +++++++++++++++++++--- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/scripts/check-entry-guard.mjs b/scripts/check-entry-guard.mjs index 02351c3cd2..85a87d7eb0 100644 --- a/scripts/check-entry-guard.mjs +++ b/scripts/check-entry-guard.mjs @@ -623,14 +623,26 @@ export function importUnsafeStatements(source) { * never a line in here. An entry whose file has since been fixed fails as STALE * and names itself, which is what stops this from rotting into an allowlist. * - * 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. + * EMPTY. It stays as an empty set rather than being deleted: an empty debt list + * is the state this rule exists to reach, and the set still has to be here for + * a re-added line to be reconciled — FRESH and STALE are both pinned by the + * self-test in exactly that state. Nothing appears here because this rule + * recognises a hand-typed guard AS a guard (see `guardAliases`), so a + * badly-spelled guard is rule 1's business, not this list's. + * + * Its one entry, `check-lucide-icon-record-names.mjs`, is gone because that + * file's two lookup maps are now built LAZILY on first read inside + * `liveSpellingFor` instead of in top-level `for` loops (objectui#6147). + * ⚠️ WHICH remedy emptied it is the load-bearing part. Moving those loops + * behind that file's ENTRY GUARD empties this list too, and was measured and + * REJECTED: it leaves the maps empty for importers, so the gate prints a WRONG + * diagnosis for a real violation while still exiting 1. So an empty list is not + * by itself evidence anything improved — the remedy has to preserve what every + * importer already got. That file's own comment carries the measurement. + * + * @type {Set} */ -const KNOWN_IMPORT_UNSAFE = new Set(['scripts/check-lucide-icon-record-names.mjs']); +const KNOWN_IMPORT_UNSAFE = new Set(); /** Every exporting file, with the statements that would run on import. */ function importSafetyCensus(files) { diff --git a/scripts/check-lucide-icon-record-names.mjs b/scripts/check-lucide-icon-record-names.mjs index ca7988bd3f..55c1a9a5b0 100644 --- a/scripts/check-lucide-icon-record-names.mjs +++ b/scripts/check-lucide-icon-record-names.mjs @@ -238,13 +238,39 @@ export const isLiveKey = (name) => Object.prototype.hasOwnProperty.call(icons, t // Derive the live spelling of a retired name BY IDENTITY, never from a list: // lucide keeps the retired export pointing at the same object as its live key. -const keyByComponent = new Map(); -for (const [key, component] of Object.entries(icons)) if (!keyByComponent.has(component)) keyByComponent.set(component, key); -const kebabByKey = new Map(); -for (const kebab of iconNames) kebabByKey.set(toRecordKey(kebab), kebab); +// +// Built LAZILY on first read and memoised, never at module top level. This file +// exports bindings, so whatever its top level ran would run inside every +// importer — the rule `check-entry-guard.mjs` calls import-safety (objectui#6133). +// +// ⛔ Do NOT "fix" this by moving the build behind the entry guard instead. That +// was measured, not reasoned about, and rejected (objectui#6092 PR 2's ablation, +// recorded on objectui#6147): the maps stay EMPTY for importers, the suite fails +// 5 of 25, and `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. A green baseline bought +// with a lying error message is worse than an honest debt line. Lazy satisfies +// both sides: nothing runs on import, and every reader gets the built maps. +// +// ⛔ This does not make the module cheap to import, and is not trying to: +// `icons`/`iconNames` are top-level `await import('lucide-react')` above. That +// cost is a separate question (objectui#6147) and is deliberately left alone. +let memoisedLookups = null; + +/** The two identity maps, built ONCE on the first read and memoised after. */ +function glyphLookups() { + if (memoisedLookups) return memoisedLookups; + const keyByComponent = new Map(); + for (const [key, component] of Object.entries(icons)) if (!keyByComponent.has(component)) keyByComponent.set(component, key); + const kebabByKey = new Map(); + for (const kebab of iconNames) kebabByKey.set(toRecordKey(kebab), kebab); + memoisedLookups = { keyByComponent, kebabByKey }; + return memoisedLookups; +} /** `{ key, kebab }` of the live spelling naming the SAME glyph, or null. */ export function liveSpellingFor(name) { + const { keyByComponent, kebabByKey } = glyphLookups(); const retiredExport = lucide[toRecordKey(name)]; if (!retiredExport) return null; const liveKey = keyByComponent.get(retiredExport);