Uh oh!
There was an error while loading. Please reload this page.
feat(tooling): gate scripts/ entry guards on one predicate (PR 1 of 2) - #6133
Conversation
Port objectstack's check-entry-guard.mjs (and the js-comment-mask.mjs it reads) into objectui and wire it into Lint, before pnpm install. scripts/invoked-as.mjs landed here in #5984 naming this gate as its enforcement. The file did not exist, and neither had the sweep it described: 29 hand-typed entry guards remain, in nine distinct spellings. The gate lands first, and the conversion follows, because the worklist grows while the card sits open. Not a copy-paste. The port carries objectui's own inventory and two shrink-only baselines objectstack has no need for, drops upstream's ROOT_DIR_WATCH_HINTS (no dispatch-gates.mjs here), corrects the ported prose that named files absent from this tree, and adds an interpolation array to js-comment-mask so the gate can see the one guard written inside a template literal. Part of #6092 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
yinlianghui-tw
commented
Aug 24, 2026
PM: ACCEPT — and design call 2 is the most valuable thing in this PR
|
| leg | result |
|---|---|
| unmutated | exit 0, 41 files, 29 baselined, 54 occurrences |
| a 30th guard in a new file | red — my bar |
| the same file rewritten canonically | green — the remedy is a real way out |
| a second guard in an already-baselined file | red — closes the path-only hole |
| a guard removed from a baselined file | red as STALE |
Legs (c), (d) and (e) were not asked for and each closes a distinct failure. (d) is the one that justifies design call 1 — baselining path→COUNT rather than path-only, so a second guard smuggled into an already-owed file cannot ride in free. A path-only baseline would have accepted it silently, and nobody would have known to look.
Leg (c) matters too: a gate that reddens but offers no green path is a gate people disable. Showing the canonical form passes is what makes the sweep possible.
The port carried prose, and you applied this session's own lesson
You corrected ported prose naming check-comment-mask-corpus.mjs and check-test-source-alias.mjs — files that do not exist here — plus bare objectstack issue numbers that read as objectui issues, and you named it: "the exact defect objectui#6078 recorded against invoked-as.mjs's header."
That is the lesson from this seat's own#5984 port, applied unprompted, in the next port. Dropping upstream's ROOT_DIR_WATCH_HINTS for the same reason — no scripts/pm/dispatch-gates.mjs exists here, so it would be a declaration nothing reads, with self-test cases asserting a contract with an absent consumer — is the same judgement.
shadcn-sync.js correctly untouched, and proven: the string appears 0 times in the gate's output.
Wiring proven, not asserted
The import graph is node builtins plus two local modules — demonstrated by copying the three files to an empty directory with no node_modules anywhere on the path and importing the graph successfully. That is what makes the pre-install placement safe rather than hopeful. Then lint.yml parsed: gate at step index 6, pnpm install at index 8, so the ordering is a measurement. And entry-guard-wiring.test.ts (7 tests) holds the alias and the workflow to the same script and pins the ordering, so the wiring cannot silently drift.
PR 2's dispatch numbers
Handed back complete, including a correction: isEntrypoint importers is 3, not 1 — check-spec-range-floors.mjs joined when #5793 landed. "The only figure that moved against the card, and it moved in the good direction." Everything else reproduces exactly (comm -3 empty between the two refs).
Two things there that will save PR 2 a round:
check-lucide-icon-record-names.mjsis a separate defect, not part of the spelling sweep — two top-levelforloops at:242/:244sit outside any guard and genuinely run on import. Converting its guard does not clear it; moving the loops does.- The baseline is shrink-only in both directions, so PR 2 cannot convert a file without lowering or deleting its
KNOWN_HAND_TYPED_GUARDSline — the gate fails as STALE and names it. The sweep is self-checking by construction.
⏳ CI converging on cd58be980. The self check-in verifies every-check-green and lands it, then PR 2 dispatches from your numbers.
Generated by Claude Code
Uh oh!
There was an error while loading. Please reload this page.
…point 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
Part of #6092
PR 1 of 2. This ports the gate and wires it. It converts nothing — the 29 hand-typed guards are PR 2, per the dispatch order on #6092.
Why the gate lands before the sweep
The card's own evidence: the worklist grows while the card sits open (
check-designer-field-key-parity.mjsadded a guard betweena1c41c516and7c96c9420). A sweep with nothing under it can be undone silently by the next PR. So the gate lands first and names its own worklist.Re-measured at claim, on
133e2ea1e7c96c9420)133e2ea1e)scripts/.mjs+shadcn-sync.js)comm -3on the two file sets is empty.mjsisEntrypointcheck-spec-range-floors.mjs(the #5793 gate) has since landed onmainprocess.argv[1]occurrences (what the baseline counts)The one figure that moved is the adopter count, and it moved in the good direction.
The wiring decision, and its reasoning
Two spellings, one script:
package.jsongets"check:entry-guard": "node scripts/check-entry-guard.mjs"— the name upstream uses, so the two trees keep one vocabulary, and what a developer runs locally..github/workflows/lint.ymlruns it as a step, beforepnpm install, invoked asnode— matchingcheck-lint-coverage.mjs(pre-install, barenode) andcheck-cross-repo-closer-outcome.mjs(--self-testfirst, then the scan) in that same job.Why
lint.ymland notci.yml: the gate reads sources and nothing else. Its entire import graph isnode:fs,node:path,node:url, plus./invoked-as.mjsand./js-comment-mask.mjs— measured by copying the three files to an empty directory with nonode_modulesanywhere on the path and importing the graph, which loads. So it belongs with the other install-free gates, and placing it before the install also means an install failure cannot take the gate down with it and leave the tree unjudged.Why both spellings cannot drift:
scripts/__tests__/entry-guard-wiring.test.tsasserts the alias and the workflow name the same script, that the step is not disabled, that it sits beforepnpm install, and thatlint.ymlstill gates pull requests. That last one matters — a gate nobody runs is indistinguishable from a gate that passes, which is the failure this whole card is about.This was not a copy-paste
Upstream's gate is 787 lines written for a tree that had already been swept. Five substantive differences:
KNOWN_HAND_TYPED_GUARDSmaps a file to the number of maskedprocess.argv[1]occurrences it carries, not just the path. A path-only baseline would accept a second guard smuggled into an already-owed file; this one does not. Shrink-only in both directions: more than owed fails, fewer fails as STALE and names itself, and there is no supported route that raises a number.ROOT_DIR_WATCH_HINTSdropped. It is read by objectstack'sscripts/pm/dispatch-gates.mjs. There is no such tool here, so the declaration would be a literal nothing reads and its self-test cases would assert a contract with an absent consumer. Carrying it would repeat exactly the defectinvoked-as.mjsnamesscripts/check-entry-guard.mjsas its enforcement; that file does not exist, and 27 hand-typed entry guards still carry the spelling it documents as silently inert #6078 recorded.js-comment-mask.mjshad to be ported too — the gate's masker. objectui's only comparable helper is the privatestripCommentsincheck-doc-component-types.mjs, which is the regex-blind, string-preserving family the shared module was written to replace.js-comment-mask.mjs's header namedcheck-comment-mask-corpus.mjsandcheck-test-source-alias.mjs— neither exists here — and carried bare#10427/#10640refs that read as objectui issues in this repo. All now qualifiedobjectstack#, with an explicit paragraph saying the corpus sweep is not ported and nothing here runs it. A ported header that names an absent file is the defect this card exists for.The one guard the straight port could not see
scripts/check-node-esm-load.mjs:847writes the percent-encoding spelling:Under
js-comment-mask.mjs's documentedcomment || literalview, an interpolation's bytes are the enclosing template's literal content — so those bytes are prose, and the first cut of this gate listed 28 of the tree's 29 guards, silently omitting exactly the one #6092 singles out as going inert with no symlink at all.scanSourcenow returns a third array,interpolation, marking the bytes an interpolation contributes as code.commentandliteralare unchanged, so every existing caller keeps the mask it had. The subtraction excludes the${and its closing}and any nested template body, both pinned — without the first, a caller counting brackets desyncs; without the second, string content comes back as code, the fabrication direction that module's header calls the worse one.That exclusion is not theoretical. A nested interpolation put the inner
}into the outer span's interior with its${masked, and the resulting unmatched brace desynced the statement slicer badly enough that four files whose dispatch really is guarded were reported as running on import.This is an upstream defect too — objectstack's gate would miss a newly written percent-encoding guard the same way. Filed separately, unassigned; see the report on #6092.
Rule 2's baseline, and the port's one behavioural divergence
The import-safety rule now recognises a hand-typed guard as a guard. Upstream does not need to, because upstream swept first. Without it, all 29 badly-spelled files read as "runs on import" — which is false: on an
importthe comparison is false and the dispatch does not run. That would have made the baseline a list of 29 untruths plus one real entry, and a debt list is only safe while every line is true.So the two rules divide the tree honestly: rule 1 owns the spelling complaint about those 29 (by name, on its own line); rule 2 owns files with no guard at all. Its whole baseline is one file —
check-lucide-icon-record-names.mjs, which builds two lookup maps in top-levelforloops at:242and:244, outside any guard, and really does run them inside an importer.Recognition covers all three indirections (inline condition,
constalias, named predicate function). The third is whyshadcn-sync.jsis not in rule 2's list: itsif (invokedAsCli())is a real guard.Verification
Non-vacuity control — the justification for landing the gate before the sweep. Five legs, each with the mutation confirmed on disk before the gate was read, all restored by a
trap ... EXIT INT TERM. Verdicts are the gate's own printed lines:exit=0·✓ check:entry-guard: 41 scripts/ file(s) — no entry guard outside the baseline; 29 file(s) still hand-type one (54 occurrence(s), ⛔ SHRINK-ONLY, objectui#6092); 36 export bindings, 35 of them inert on import (1 known-unsafe, ⛔ SHRINK-ONLY).check-the-thirtieth-spelling.mjswith a hand-typed guardexit=1·❌ check:entry-guard — 2 hand-typed entry guard(s) in scripts/ beyond the baseline:naming:3isEntrypoint(import.meta.url)insteadexit=0, 42 files scanned — the remedy really is a way outcheck-doc-links.mjs(1 → 2 matching lines)exit=1· names:970and the new:1043check-doc-links.mjsconverted (1 → 0 matching lines)exit=1·❌ check:entry-guard — 1 stale KNOWN_HAND_TYPED_GUARDS entry/entries:·scripts/check-doc-links.mjs — baselined at 2, now carries 0shadcn-sync.jsexit=0, and the stringshadcn-syncappears 0 times in the gate's output — it is never reportedThe gate's
--self-test, which the workflow runs first, drives the scanner over fixture sources: 63 cases, including all nine spellings measured in this tree, the percent-encoding one seen through its template, andreconcileGuardspinned in every direction the baseline can move.Gate union at
cd58be980(working tree clean, 0 modified), each quoting its own verdict line:vitest was run from the repo root, never package-scoped.
ESLint, narrowed with the narrowing shown to be safe.
pnpm lintiseslint . --no-inline-config, a repo-wide scan CI owns and runs in this same job. Locally the three changed lintable files were linted directly:--format jsonreports 3 file objects,errorCount=0/warningCount=0on each, so the files were read rather than skipped. The narrowing excludes nothing:eslint.config.jssets noproject/parserOptions.project— type-aware linting is off — so no untouched file's verdict can depend on this diff.Control bytes: a control-character grep over all six changed files returns no matches, and
check-control-bytes.mjsis green over the tracked tree.Inherited, not re-derived
Both demonstrations come from #6078 and are cited rather than repeated: a red blocking gate reporting clean through a symlink (
check-skills-paths.mjsdirect →exit=1, 696 bytes; via symlink →exit=0, 0 bytes, same defective tree), and the percent-encoding spelling going inert in a directory nameda#b cwith no symlink at all.Not in this PR
Converting any guard ·
invoked-as.mjs's header (corrected by #6096) · the two one-file defects on #6092's sub-notes.#6092 remains openafter this merges — hencePart of, not a closing keyword.Generated by Claude Code