Skip to content

feat(scripts,components): gate unreferenced source files, and delete the one standing orphan - #7670

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-7515-unreferenced-source-gate
Sep 4, 2026
Merged

feat(scripts,components): gate unreferenced source files, and delete the one standing orphan#7670
os-sam merged 1 commit into
mainfrom
claude/issue-7515-unreferenced-source-gate

Conversation

@os-sam

@os-sam os-sam commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Fixes #7515

None of the repo's 42 scripts/check-*.mjs gates could see a source file that
nothing reaches. The nearest neighbours each answer a different question —
check-dist-completeness asks whether dist/ holds what tsc emits,
check-readme-exports compares documented exports against shipped ones,
check-i18n-dead-keys covers message keys — so a .tsx that is in the published
tarball while being reachable from nothing is outside all three. Both instances
found in one week (objectui#7319, objectui#7397) were found by a human reading
unrelated code. The hazard is not the bytes: the file objectui#7319 removed carried
the same export name as a live engine one package over and evaluated no predicate,
so name-completion alone could have wired a silently wrong renderer into a
published package.

The card's measurement, reproduced first — and reconciled, not adjusted

The card measured 210 non-test sources reaching 207 on 746f6f818. This branch
measures 209 reaching 206 on f7cf7e8a, and the delta is fully explained
rather than tuned away: 7bf244be ("remove the duplicate chart primitives",
objectui#7397) deleted packages/components/src/ui/chart.tsx on 2026-09-04, after
the card was written. 210 − 1 = 209, 207 − 1 = 206, and the same three unreached
files remain. Two independent walkers agree on the population (a throwaway script,
and the gate reusing listSourceFiles + TOOLING_FILE from
check-phantom-dependencies.mjs).

One smaller correction to the card's framing, not affecting its conclusion:
"42 check:* scripts" is 42 scripts/check-*.mjs files, of which 35 are wired
as root check:* npm scripts.

The alias leg is the whole difficulty

scripts/check-unreferenced-sources.mjs walks from two kinds of root:

  1. the package's declared entry (build.lib.entry), and
  2. every build-config alias whose replacement is a file inside the package.

Nothing in packages/components imports either use-sync-external-store shim;
both are alive only through vite.config.ts resolve.alias entries whose importer
is a bundled dependency no source file names. An alias whose replacement is a
directory ({ find: '@', replacement: resolve(__dirname, './src') }) is not a
root but a resolution rule — and the filesystem decides which is which, not the
spelling.

Design choices worth review:

  • An alias expression the reader cannot evaluate is a FINDING, never a skip.
    Skipping one would make the gate accuse whatever file that alias points at, so
    the safe direction is to fail loudly on the config.
  • Scope is declared per package in COVERED_PACKAGES (packages/components
    only), and the uncovered remainder is printed as a count derived from the
    workspace
    on every run — currently 40 packages with a src/ tree — so it
    cannot rot into a stale claim. Per the dispatch ruling: a gate that covers one
    package correctly beats one that covers forty with false positives.
  • tsconfig paths is audited, not assumed. packages/components declares
    its @ alias twice (vite for the bundler, tsconfig for the type program); the
    gate reads only the first, so it re-derives on every run that the second says
    nothing the first does not.
  • It reuses moduleSpecifiers() rather than growing a second parser.

Non-vacuity, proved in both directions on the real tree

Predictions were written before each leg; one prediction missed and is reported as
a miss.

Leg Predicted Observed
Gate on unmodified tree 1 finding (ui/toast.tsx) 2 — plus a false alarm from my own auditAliasMechanisms MISS
Drop the 3 shim aliases from vite.config.ts 3 findings (2 shims + toast) 3; reached 208 → 206, alias targets 3 → 0 HIT
Add export * from './toast' to ui/index.ts 0 findings, exit 0 0 findings, exit 0; 209/209 reached HIT

The miss was real and is fixed: the alias find was stored as raw source text and
then read with JSON.parse, which cannot parse a single-quoted TypeScript string,
so the gate reported packages/components' own @ alias as unmodelled. find is
now a structured value (readFind), and scripts/__tests__/check-unreferenced-sources.test.ts
pins that exact case.

Both mutation legs ran under an EXIT INT TERM trap with absolute paths from
git rev-parse --show-toplevel, were proved on disk by anchored grep counts and
git hash-object movement off the HEAD blob, and were restored via
git checkout HEAD -- ABSOLUTE_PATH, verified by empty git diff HEAD and
blob-hash equality.

The deletion, proved per file

packages/components/src/ui/toast.tsx is the only genuine orphan; the sweep found
no others inside packages/components. Unreferenced and not on the public
surface were checked as two separate questions, each with a lit control:

Question Reading Control (same shape)
Any importer? 0 sonner: 16 referencing files
In the JS bundle? ToastPrimitives/ToastViewport absent from dist/index.js Toaster: 3
On the type surface? dist/ui/index.d.ts never names ./toast sonner: 2

A first attempt at the type-surface question grepped dist/index.d.ts for Toast*
and read 0 — but its control read 0 too. That was a dark instrument; the reading
was discarded and rebuilt against the barrel that actually carries ui/*.

It did ship: a before/after full build shows the published dist/ losing exactly
one file
, dist/ui/toast.d.ts (212 → 211), and nothing else — which is why this
carries a patch changeset rather than none.

@radix-ui/react-toast is dropped in the same change: the removed file was its only
importer anywhere in the repository (control: @radix-ui/react-dialog, 6 files).
This is not an unrelated tidy-up — it is debris this change itself creates.

On the No-Touch zone, and my own clause-② determination

ui/toast.tsx sits in packages/components/src/ui/**, AGENTS.md commandment #7.
That rule forbids modifying the logic or styles of Shadcn-synced primitives,
and its stated reason is that the sync script overwrites such edits. Deletion is
not that act: getLocalComponents() in scripts/shadcn-sync.js derives its tracked
set from readdir(COMPONENTS_DIR), so a deleted file simply leaves the tracked set
and nothing restores it. Precedent one day old: 7bf244be deleted
packages/components/src/ui/chart.tsx from the same zone and is on main.

Clause-② determination: no, agreeing with the dispatch ruling and reached
independently. scripts/ is not a governed surface here; the gate moves no schema
key and no export. The deletion removes nothing reachable — proved above per file
rather than assumed, and the published export surface is unchanged.

Verification

All commands captured with the exit code redirected before any pipe. Gate union
re-run after the final commit, at 7fdeff55:

  • 19 gates, all exit 0, each quoting its own verdict line: check:control-bytes,
    check:phantom-deps, check:self-import, check:published-tsconfig-exclude,
    check:side-effects-array, check:esm-specifiers, check:dist-completeness,
    check:readme-exports, check:doc-fences, check:doc-types, check:doc-snippets,
    check:pre-install-import-graph, check-doc-links, check-lint-coverage,
    check-unreferenced-sources (OK Every shipped source file in every covered package is reachable.),
    and the four changeset gates.
  • packages/components: 232 test files, 2159 tests, all passing; type-check
    exit 0 (the script name echoed back, so this was not a zero-match silent pass —
    this package spells it type-check, hyphenated).
  • pnpm type-check:scripts exit 0. It caught real JSDoc type errors in the gate
    first; the typedefs now match the structured alias shape.
  • ESLint over the whole repo (eslint . --no-inline-config, 4260 files): both new
    files 0 errors, 0 warnings, and both were in the linted population.
    pnpm --filter @object-ui/components lint exit 0.
  • The ci-cd-pipeline-doc gate caught the missing doc row for the new step and is
    now green; content/docs/guide/ci-cd-pipeline.md documents it in run order.

Two failures were investigated and are pre-existing, already-filed, and unrelated
to this diff (my changed paths appear nowhere in either gate or test; control lit):

  • objectui#6893 — check-sdui-registration-pins.test.ts fails on any tree where
    packages/app-shell/dist exists. Reproduced and isolated: removing that dist
    makes it pass, restoring it makes it fail.
  • objectui#7460 — check-readme-exports.test.ts reds on a half-built tree.

With those two set aside, scripts/__tests__/ runs 101 files / 3003 tests.

PRECONDITION NOT MET was recorded twice and treated as not measured rather than
as a pass or a red: check:readme-exports and check:doc-snippets both need a full
build (the first says so itself — "the packages were never built"). Both were
satisfied with turbo run build --filter='./packages/*' and re-run green.

Heavy runs went through the shared verify lock, slot issue-7515.

What is deliberately NOT claimed

  • It does not prove an alias is used. An alias pointing at a file nobody imports
    makes that file reachable here, because the build config names it. Deciding whether
    a bundled dependency still imports use-sync-external-store/shim means walking
    node_modules — a different gate on a different input.
  • "Reachable from the entry" is not "referenced by anything." A helper used only
    by tests is unreachable from the published entry and is reported; the finding says
    referenced only by test files so the two are distinguishable. packages/components
    has no such file today.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3


Generated by Claude Code

…the one standing orphan (objectui#7515)

None of the repo's 42 `scripts/check-*.mjs` gates could see a source file that
nothing reaches. `check-dist-completeness` asks whether `dist/` holds what `tsc`
emits, `check-readme-exports` compares documented exports against shipped ones,
`check-i18n-dead-keys` covers message keys — a `.tsx` that is in the published
tarball while being reachable from nothing is outside all three. Both instances
found this week were found by a human reading unrelated code.

`scripts/check-unreferenced-sources.mjs` walks the import graph from a covered
package's declared entry AND from every build-config alias whose replacement is
a file inside the package. That second leg is the whole difficulty: nothing in
`packages/components` imports either `use-sync-external-store` shim, and both are
alive only through `vite.config.ts` `resolve.alias` entries whose importer is a
bundled dependency. A walk that skips it reports exactly those two live files as
dead on its first run.

Scope is DECLARED per package in `COVERED_PACKAGES` and the uncovered remainder
is printed as a count derived from the workspace on every run. An alias
expression the reader cannot evaluate is a FINDING, never a skip: skipping one
would make the gate accuse whatever file that alias points at.

`packages/components/src/ui/toast.tsx` is removed. It had no importer anywhere
under `packages/components/src` and `ui/index.ts` never carried it, so the
barrel's `export * from './ui'` did not reach it either; it contributed nothing
to `dist/index.js` and nothing to the export surface, while shipping as
`dist/ui/toast.d.ts`. `@radix-ui/react-toast` goes with it — the removed file was
its only importer in the repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3180.1 KB 3191.4 KB
Main entry chunk (gzip) 143.2 KB 350 KB
Entry file index-CDonrGw6.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 510.70KB 116.21KB
core (index.js) 6.70KB 2.68KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 180.00KB 50.20KB
fields (index.js) 242.27KB 61.22KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.98KB 10.98KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 48.05KB 13.34KB
plugin-charts (index.js) 70.92KB 19.75KB
plugin-chatbot (index.js) 196.19KB 46.43KB
plugin-dashboard (index.js) 132.86KB 34.68KB
plugin-designer (index.js) 212.86KB 43.19KB
plugin-detail (index.js) 250.55KB 64.06KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.87KB 32.66KB
plugin-gantt (index.js) 167.41KB 41.04KB
plugin-grid (index.js) 209.40KB 56.80KB
plugin-kanban (index.js) 52.71KB 14.55KB
plugin-list (index.js) 113.28KB 27.59KB
plugin-map (index.js) 20.57KB 6.82KB
plugin-markdown (index.js) 13.84KB 4.77KB
plugin-report (index.js) 43.57KB 11.96KB
plugin-timeline (index.js) 30.84KB 8.85KB
plugin-tree (index.js) 9.35KB 3.23KB
plugin-view (index.js) 85.24KB 20.94KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.58KB 2.23KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(components): no gate detects an unreferenced source file — two were found by hand this week, and ui/toast.tsx is the one still standing

2 participants