perf(components,app-shell,console): answer icon-name membership from lucide's icons record — MEASURED −8,515 B, ui-components back to green - #9250
Conversation
lucide derives `iconNames` as `Object.keys(dynamicIconImports)`, so a static import of either name drags the 1,767-entry dynamic-import map into the importer's chunk. Four modules imported it, and the map rode the console's eager `ui-components` chunk on every page load. - `packages/components/src/lib/lucide-icon-names.ts` — the names as DATA, generated from the installed lucide by `scripts/gen-lucide-icon-names.mjs` and re-derived from that same install by a test, so the mirror cannot age silently. - `lazy-icon.tsx` answers the synchronous `isLucideIconName` from that mirror and reaches the map through `import()` on the first icon that renders. - The two transcriptions of `getLazyIcon` (`app-shell`, `apps/console`) become delegations, so one resolver reads one vocabulary. - `check-lucide-icon-record-names.mjs` learns the two spellings it could not see (`import()`, the catalogue binding) and gains an EMPTY `DECLARED_EAGER_DYNAMIC_IMPORTERS`, so a static import is named on the commit that adds it. Part of #9204 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
…1facae The card's -45,749 B ablation does not reproduce. Three console builds in one container put the map at 8,253 B gzipped of the eager `ui-components` chunk and the icon-name catalogue that has to replace it at 9,176 B in the same chunk, so deferring the map while `isLucideIconName` stays a synchronous exact-membership predicate is net +923 B. Say so in the files that carry the claim, and add the changeset the diff owes. Part of #9204 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
|
| Step | Outcome |
|---|---|
| Build packages | success |
| Check console performance budget | failure |
Which half objected:
| Eager-closure half | Verdict |
|---|---|
| Aggregate closure ceiling | ✅ pass |
| Per-chunk ceilings | ✅ pass |
| Ceiling sensitivity (headroom) | |
| Ceiling freshness (checkout vs. base branch) | ✅ pass |
⚠️ A broken gauge half is a verdict about the ceiling, not about the bundle: that line has drifted out of range of the regression it exists to catch, or the report behind it cannot be trusted. It does not say anything grew. TheCheck console performance budgetstep log carries the ceiling and the number it was compared against.
Reason: The entry chunk measured 144.3 KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either.
See the workflow run for details.
📦 Bundle Size Report
| Package | Size | Gzipped |
|---|---|---|
| app-shell (consoleActionDispatch.js) | 0.20KB | 0.19KB |
| app-shell (index.js) | 16.69KB | 6.21KB |
| 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) | 11.08KB | 4.58KB |
| 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) | 527.91KB | 123.65KB |
| core (index.js) | 8.52KB | 3.41KB |
| create-plugin (index.js) | 27.94KB | 9.51KB |
| data-objectstack (index.js) | 211.58KB | 58.68KB |
| fields (index.js) | 247.88KB | 62.48KB |
| 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) | 8.87KB | 3.64KB |
| i18n (index.js) | 5.21KB | 2.26KB |
| i18n (pickLocalized.js) | 7.62KB | 3.26KB |
| i18n (provider.js) | 32.15KB | 10.49KB |
| 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.84KB | 10.95KB |
| 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.39KB | 1.66KB |
| mobile (useTouchTarget.js) | 1.01KB | 0.54KB |
| permissions (MePermissionsProvider.js) | 13.52KB | 4.88KB |
| 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) | 8.39KB | 3.10KB |
| 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) | 14.81KB | 3.63KB |
| plugin-calendar (index.js) | 49.04KB | 13.93KB |
| plugin-charts (index.js) | 71.52KB | 19.98KB |
| plugin-chatbot (index.js) | 195.35KB | 46.52KB |
| plugin-dashboard (index.js) | 131.27KB | 34.62KB |
| plugin-designer (index.js) | 215.82KB | 44.33KB |
| plugin-detail (index.js) | 253.38KB | 65.87KB |
| plugin-editor (index.js) | 2.23KB | 1.05KB |
| plugin-form (index.js) | 136.79KB | 34.19KB |
| plugin-gantt (index.js) | 166.95KB | 41.04KB |
| plugin-grid (index.js) | 211.62KB | 57.50KB |
| plugin-kanban (index.js) | 46.10KB | 14.33KB |
| plugin-list (index.js) | 112.59KB | 27.66KB |
| plugin-map (index.js) | 20.43KB | 6.81KB |
| plugin-markdown (index.js) | 13.88KB | 4.80KB |
| plugin-report (index.js) | 43.42KB | 11.92KB |
| plugin-timeline (index.js) | 30.10KB | 8.74KB |
| plugin-tree (index.js) | 9.55KB | 3.32KB |
| plugin-view (index.js) | 84.42KB | 20.80KB |
| 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) | 94.03KB | 31.02KB |
| react (data-invalidation.js) | 5.05KB | 2.08KB |
| react (index.js) | 4.63KB | 2.18KB |
| react (schema-input.js) | 4.25KB | 2.04KB |
| react (spec-input.js) | 0.20KB | 0.18KB |
| sdui-parser (codegen.js) | 6.58KB | 2.74KB |
| sdui-parser (dashboard-widget-options.js) | 3.08KB | 1.30KB |
| sdui-parser (index.js) | 5.66KB | 2.50KB |
| sdui-parser (input-type.js) | 2.84KB | 1.40KB |
| sdui-parser (kanban-quick-add.js) | 3.89KB | 1.87KB |
| sdui-parser (parse.js) | 25.28KB | 7.80KB |
| sdui-parser (provenance.js) | 3.66KB | 1.82KB |
| sdui-parser (types.js) | 0.28KB | 0.23KB |
| sdui-parser (validate.js) | 14.82KB | 4.99KB |
| 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.93KB | 1.49KB |
| 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 (expression.js) | 0.20KB | 0.18KB |
| 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.73KB | 2.28KB |
| 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 (strict-authoring-face.js) | 14.27KB | 5.47KB |
| 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
Bring the branch up to date with `main` (64 commits behind) so CI runs against the current tree. No source change of this branch's own is touched. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
|
| Step | Outcome |
|---|---|
| Build packages | success |
| Check console performance budget | failure |
Which half objected:
| Eager-closure half | Verdict |
|---|---|
| Aggregate closure ceiling | ✅ pass |
| Per-chunk ceilings | ✅ pass |
| Ceiling sensitivity (headroom) | |
| Ceiling freshness (checkout vs. base branch) | ✅ pass |
⚠️ A broken gauge half is a verdict about the ceiling, not about the bundle: that line has drifted out of range of the regression it exists to catch, or the report behind it cannot be trusted. It does not say anything grew. TheCheck console performance budgetstep log carries the ceiling and the number it was compared against.
Reason: The entry chunk measured 144.4 KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either.
See the workflow run for details.
📦 Bundle Size Report
| Package | Size | Gzipped |
|---|---|---|
| app-shell (consoleActionDispatch.js) | 0.20KB | 0.19KB |
| app-shell (index.js) | 16.69KB | 6.21KB |
| 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) | 11.08KB | 4.58KB |
| 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) | 529.13KB | 123.99KB |
| core (index.js) | 8.52KB | 3.41KB |
| create-plugin (index.js) | 27.94KB | 9.51KB |
| data-objectstack (index.js) | 211.58KB | 58.68KB |
| fields (index.js) | 247.91KB | 62.51KB |
| 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) | 8.87KB | 3.64KB |
| i18n (index.js) | 5.22KB | 2.26KB |
| i18n (pickLocalized.js) | 9.86KB | 3.95KB |
| i18n (provider.js) | 32.15KB | 10.49KB |
| 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.84KB | 10.95KB |
| 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.39KB | 1.66KB |
| mobile (useTouchTarget.js) | 1.01KB | 0.54KB |
| permissions (MePermissionsProvider.js) | 13.52KB | 4.88KB |
| 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) | 8.39KB | 3.10KB |
| 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) | 14.81KB | 3.63KB |
| plugin-calendar (index.js) | 49.04KB | 13.93KB |
| plugin-charts (index.js) | 71.52KB | 19.98KB |
| plugin-chatbot (index.js) | 195.35KB | 46.52KB |
| plugin-dashboard (index.js) | 131.24KB | 34.61KB |
| plugin-designer (index.js) | 215.95KB | 44.33KB |
| plugin-detail (index.js) | 253.51KB | 65.88KB |
| plugin-editor (index.js) | 2.23KB | 1.05KB |
| plugin-form (index.js) | 136.79KB | 34.19KB |
| plugin-gantt (index.js) | 166.95KB | 41.04KB |
| plugin-grid (index.js) | 211.58KB | 57.48KB |
| plugin-kanban (index.js) | 46.00KB | 14.30KB |
| plugin-list (index.js) | 112.59KB | 27.66KB |
| plugin-map (index.js) | 20.43KB | 6.81KB |
| plugin-markdown (index.js) | 13.88KB | 4.80KB |
| plugin-report (index.js) | 43.42KB | 11.93KB |
| plugin-timeline (index.js) | 30.07KB | 8.74KB |
| plugin-tree (index.js) | 9.55KB | 3.32KB |
| plugin-view (index.js) | 84.43KB | 20.80KB |
| 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) | 94.03KB | 31.02KB |
| react (data-invalidation.js) | 5.05KB | 2.08KB |
| react (index.js) | 4.63KB | 2.18KB |
| react (schema-input.js) | 4.25KB | 2.04KB |
| react (spec-input.js) | 0.20KB | 0.18KB |
| sdui-parser (codegen.js) | 6.58KB | 2.74KB |
| sdui-parser (dashboard-widget-options.js) | 3.08KB | 1.30KB |
| sdui-parser (index.js) | 5.66KB | 2.50KB |
| sdui-parser (input-type.js) | 2.84KB | 1.40KB |
| sdui-parser (kanban-quick-add.js) | 3.89KB | 1.87KB |
| sdui-parser (parse.js) | 25.28KB | 7.80KB |
| sdui-parser (provenance.js) | 3.66KB | 1.82KB |
| sdui-parser (types.js) | 0.28KB | 0.23KB |
| sdui-parser (validate.js) | 14.82KB | 4.99KB |
| 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.93KB | 1.49KB |
| 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 (expression.js) | 0.20KB | 0.18KB |
| 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.73KB | 2.28KB |
| 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 (strict-authoring-face.js) | 14.27KB | 5.47KB |
| 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
…er-lucide-icon-map
…lucide's `icons` record objectui#9204, maintainer decision batch #125 item 3 (2026-09-13), option A. lucide derives `iconNames` as `Object.keys(dynamicIconImports)`, so importing the icon NAMES imports the dynamic-import map with them. Four modules did, and the map sat in the console's eager `ui-components` chunk on every page load — 8,253 B gzipped, the row this card exists to pay down. The names leave the eager path by being answered from the `icons` RECORD, which `renderers/action/resolve-icon.ts` already puts in that same chunk. Zero new eager bytes, one vocabulary instead of two. A generated mirror of the names was measured first and rejected: the map's keys ARE the names, so a bare catalogue of them costs 9,176 B gz against the 8,253 B map it replaces. MEASURED, two console builds in one container: ui-components 397,090 -> 388,494 gz (-8,596) aggregate 3,180,420 -> 3,171,783 (-8,637) The two deltas agree within 41 bytes, so these are bytes LEAVING the page load, not bytes moving between columns. `check:eager-closure` goes exit 2 -> exit 0 and the row's headroom goes 1,910 B (0.02x) -> 10,506 B (0.12x).⚠️ Behaviour change, which is the cost the ruling took: membership narrows from lucide's DYNAMIC vocabulary to its RECORD, so 254 retired spellings (`smile`, `edit`, `filter`, `alert-triangle`, `sort-desc`, …) stop resolving. Ruling item 2 makes that loud rather than silent — a refused spelling now names itself, lucide's current name for it, and the spelling to write instead, all derived from the installed lucide at runtime rather than from a list that would age. Gate constants: `EXHAUSTED_HEADROOM_ALLOWANCES` loses its `ui-components` entry — REMOVED, not lowered, because the row now clears the 0.10x floor on its own, which is the only way out of that table and the end state its own docblock names. `PER_CHUNK_BASELINE['ui-components']` moves with it in this commit, since the gate's unit fixtures are built from it. ⛔ No ceiling moved. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
✅ Console Performance Budget
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
Size Limits
|
…ng icons" objectui#9204 follow-through. The picker's vocabulary became asynchronous when the icon names left the eager path, so the grid is empty for one frame after the dialog opens. The existing empty state said "No matching icons." — a false answer to a query nobody had run yet. It now distinguishes the two states with literal i18n keys on both arms, reusing `engine.form.loadingOptions` rather than adding a key that would owe every locale a translation. Its tests follow the same split: the trigger stays synchronous (`isLucideIconName` reads the `icons` record and needs nothing loaded) so it is asserted synchronously; the grid is awaited. `lucide-react/dynamic.mjs` is imported at module scope so the fetch is paid for in the import phase rather than out of a `findBy` budget under a saturated transform pipeline. Adds one row the old file could not have: the picker offers `list-filter` and NOT `filter`, so the grid and `getLazyIcon` are pinned to one vocabulary. A pickable name the renderer then refuses would be the worst shape this card could ship. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
…ured build objectui#9204. The picker's loading state moved the row 81 bytes after the previous pin, so the constant, the changeset and the gate test's comment all name the build that actually produced them: `d43f51ee1`, 388,575 gzipped. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
icons record — MEASURED −8,515 B, ui-components back to green
…er-lucide-icon-map
✅ Console Performance Budget
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
Size Limits
|
✅ Console Performance Budget
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
Size Limits
|
… the declaration
objectui#9204. `pnpm type-check:scripts` (`tsc -p tsconfig.scripts.json`) was red
on two files in this branch's diff. Both errors landed on a TEST, and in both
cases the thing that was actually wrong was a DECLARATION in a `.mjs` that this
project reads for inference but does not check (`checkJs: false`) — so the error
surfaced at the one place the defect was not.
1. `check-lucide-icon-record-names.mjs` — `AnalyzeOptions` did not list
`declaredEagerDynamicImporters`, which `analyze()` destructures, defaults and
uses. The census exists to be substituted, exactly like the two siblings the
typedef does list: the unit test's fixture row for the eager-import rule has
to declare a fixture importer. ⇒ the PRODUCTION signature carries it; the
test was right to pass it. Dropping it from the test instead would have left
one census non-substitutable and made that row assert against production
state rather than a fixture.
2. `check-eager-closure-budget.mjs` — `EXHAUSTED_HEADROOM_ALLOWANCES` is empty
since this branch paid `ui-components` off, and `Object.freeze({})` infers
`Readonly<{}>`, so `Object.values` of it went from `number[]` to `unknown[]`.
A reader doing arithmetic on an allowance stopped compiling because the table
was EMPTY, not because anything about it was wrong. The element type is a
property of what the table holds, not of how many rows it holds today, so it
is annotated. `DECLARED_EAGER_DYNAMIC_IMPORTERS` gets the same treatment for
the same reason — its emptiness IS its assertion, and `never[]` would erase
the element type of a list that is declared empty on purpose.
⛔ No `as any`, no `@ts-ignore`, no `@ts-expect-error`, and no behaviour changed:
both edits are JSDoc on declarations. `pnpm type-check:scripts` exit 2 -> exit 0.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
…er-lucide-icon-map
Repair:
|
| check | result |
|---|---|
pnpm type-check:scripts |
exit 2 before, exit 0 after |
vitest run the two changed test files plus scripts-type-check.test.ts |
3 files, 212 passed |
check:icon-record-names |
exit 0 — 184 authored names, 1 record resolver, 1 dynamic site, 0 static importers |
check:changeset-presence |
exit 0 |
check:control-bytes plus a grep -naP self-scan of both edited files |
exit 0, no hits |
⭐ The headline measurement is unmoved. Full package build then console build on the merged tree, through the shared verify lock, VERDICT command-exit 0:
MERGED @10e156066 ui-components gz = 388,575 aggregate = 3,172,046
BUDGET_EXIT=0
✅ chunk `ui-components` 379.5 KB measured / 389.6 KB ceiling
(headroom 10.2 KB = 0.11x the 89.0 KB regression)
388,575 is byte-for-byte the figure pinned in PER_CHUNK_BASELINE, and it is now the third build to read it — before the merge, after the first merge, and after this one. A type-only repair should not move the row, and it did not.
The premise this falsified, stated plainly
My round's verification scoped "typecheck" to the affected packages (turbo run type-check). scripts/ is not a workspace package, so turbo run type-check structurally cannot reach it — tsconfig.scripts.json's own header says exactly that, and objectui#3494 exists because of it. Editing anything under scripts/ owes the separate root script pnpm type-check:scripts, and my narrowing missed it. Recording it here rather than only in the report, because the next reader of that directory needs it more than I do.
Generated by Claude Code
|
✅ Console Performance Budget
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
Size Limits
|
Contract reviewReviewed head: Scope of this record: the diff, not the grading. Card objectui#9204 declares ① Derived judgments1. ⛔ This head regresses a shipped renderer, and CI's own
2. Ruling item 1 — the literal "kebab-cased
Controls that fired: the specimens ( 3. Ruling item 3 — the two constant moves both TIGHTEN, and no ceiling moved. 4. Byte claims — measured here: two console builds, one container, serialised through
Row and aggregate agree within 29 bytes and one eager chunk left the closure — bytes leaving the page load, not moving between columns. The head figure is byte-identical to the pinned baseline and to the seat's merged-tree reading, and the emitted chunk ( 5. Gates and suites re-run on this head (exit codes captured by redirect before any pipe): 6. The ② Semver grading against the changeset
③ Boundary flags and open_questionsPublished behaviour this diff changes (for the record):
Not measured here, stated rather than passed through: the full
Verdict rests on ①.1 alone: the byte paydown, both ruling deviations and the gate constants all verify, but the head as pushed breaks a shipped renderer and CI's test lane is red for this diff's own reason. A paydown that ships a regression is worse than the debt. FAIL Generated by Claude Code |
ADOPTED — the FAIL at
|
⛔ Correction to my own record — the repair was NOT dispatched when I said it was
My adoption comment
That was not true when it was written, and it is still not true now. No
Why this matters beyond bookkeepingA reader of this PR — including whoever picks it up next — would have concluded that a repair was already under way and waited for it. The correction is here rather than in a chat reply because the false sentence is on this PR, and that is where anyone misled by it will be looking. The actual state, and what happens nextThe repair is owed and queued, ⛔ not in progress:
It is queued behind the concurrency cap of 3 that the maintainer reaffirmed, and goes out the moment a slot frees. Generated by Claude Code |
|
Closing — the ruling on objectui#9251 refuses this PR's membership source by namePosted by the This branch is objectui#9204 rescue A: it derives the legal icon-name set from
⇒ Closed as ⛔ This does not say the work was wrong to do, and the previous ruling A ( Also retired with the branch: the repair this seat still owedThe contract review adopted at
The open question this seat had raised — whether refusing never-lucide names loudly was intended, Where the work goesobjectui#9204 stays open, moves to Generated by Claude Code |
⛔ Correction to my own record — the
|
Fixes #9204
Implements the maintainer's ruling of 2026-09-13 (decision batch #125 item 3, comment 5650203001), option A: one icon vocabulary — the
iconsrecord that actually ships. This pull request was previously the DO-NOT-MERGE measurement of record for Door 1; ruling item 1 turns it into the A implementation, and the refuted Door-1 measurement is kept below because it is the reason A is the shape.mainwas RED onBundle Analysisfor this row and every other lane inherited it. It is green here.The measurement — control and result, two console builds, one container
Instrument each time:
pnpm turbo run build --filter='./packages/*' --concurrency=2thenpnpm --filter @object-ui/console build, thenapps/console/dist/eager-closure.jsonread directly. Both runs serialised through the shared verify lock, bothVERDICT command-exit 0.ui-componentsgzmainat852437297d43f51ee1⭐ The two deltas agree within 5 bytes. That is the reading triage said it was relying on: these are bytes leaving the page load, not bytes moving between columns — the failure mode the console config forbids in its own words ("This moves NO MODULE BYTES … Nothing here may be read as headroom that was earned").
The control reproduces CI exactly: 397,090 B is 387.8 KB, the figure the
Bundle Analysisjob printed formainat85243729.check:eager-closure— exit 2 before, exit 0 afterEvery other row unchanged and green: aggregate 0.42x,
vendor-objectstack0.15x,i18n-locale-en0.10x,framework0.61x.Gate constants: one retired, one re-pinned, ⛔ no ceiling moved
PER_CHUNK_GZIP_CEILINGS['ui-components']is still 399,000, unchanged since objectui#5490. The maintainer's raise authorisation for this card is unspent — the row pays for itself, so there is no residue to raise for.EXHAUSTED_HEADROOM_ALLOWANCESloses itsui-componentsentry — REMOVED, not lowered, which is the one way out of that table and the end state its own docblock names fori18n-locales: "the debt PAID, in the only currency this table takes: the row cleared the floor on its own."PER_CHUNK_BASELINE['ui-components']moves 391,095 → 388,575 in the same commit as the removal, and it had to: the gate's unit fixtures are built from that constant, so leaving it would have made the gate's own tests assert a row under a floor the live build clears.What changed, and why the bytes were there
lucide derives
iconNamesasObject.keys(dynamicIconImports), so importing the icon names imports the dynamic-import map with them. Four modules did, and the map sat in the console's eagerui-componentschunk on every page load.The names leave that path by being answered from the
iconsrecord, whichrenderers/action/resolve-icon.tsalready puts in the same chunk — so membership costs zero new eager bytes.packages/components/src/lib/lazy-icon.tsx—isLucideIconName/getLazyIcon/LazyIconjudge names through the seam (resolveIcon), not throughiconNames. The map is still reached, throughimport(), and only to RENDER.renderers/action/resolve-icon.tsgains one diagnostics-only export,liveIconNameOf.loadLucideIconNames()— the renderable vocabulary,Promise-returning, for a picker that needs the whole list to search.getLazyIcon(app-shell,apps/console) become delegations: one resolver, one vocabulary, one memo.check-lucide-icon-record-names.mjskeeps the census objectui#9204 gave it:DECLARED_EAGER_DYNAMIC_IMPORTERSis empty, and that emptiness is the assertion.DECLARED_DYNAMIC_READERSis down from four modules to one.Membership narrows from lucide's DYNAMIC vocabulary (2,039 names) to its RECORD (1,781 keys). Measured against the installed lucide, 254 spellings lucide has retired —
smile,edit,filter,alert-triangle,sort-desc— stop resolving.Ruling item 2 makes that loud. A refused spelling used to become the
Databaseglyph, or a notification's severity icon, with nothing logged — a page that still rendered and a glyph that looked deliberate. The console now carries, once per spelling:⭐ Both halves are derived from the installed lucide at runtime; there is no list of retired names in this repo to go stale. That is the gate's own rule applied one level in — "a hand-kept vocabulary is the same defect one level up: it ages the moment lucide retires the next name, and it ages SILENTLY" — and a table of 254 names would also have put roughly 5 KB gzipped back on the path this card is emptying. The replacement is found by identity in the record, with lucide's own
displayNameas the fallback when the two halves came from different module graphs (measured: under this repo's vitest,===is false for two components that are the same icon, while both carry the samedisplayName).Grid2x2kebabs togrid2x2while lucide spells itgrid-2x2— so a diagnostic that echoed only the key would send an author to a name that passes membership and then fails to load. Pinned over the whole retired population, not one specimen.⛔ What was refuted first — Door 1 as the card ranked it
Kept because ruling item 1 asks for it: the card's headline −45,749 B for Door 1 does not reproduce. Measured on
91facaef6, four console builds:ui-componentsgzTwo mechanisms, both measured on the emitted chunk:
iconsrecord keeps all icon modules eager, so the now-lazy map chunk imports them back across a chunk boundary: the export statement grows 4,457 B / 492 bindings to 22,108 B / 2,257 bindings. The card's ablation stubbed the component itself, so that edge never existed in its number.⇒ the recoverable maximum was 8,253 B and only by taking the NAMES off the eager path too. This branch takes 8,515 B, because the delegations removed a small eager chunk with them.
Verification
Exit codes captured by redirect before any pipe. Test paths are repo-root relative with no
--separator.vitest run packages/components/ scripts/vitest run packages/app-shell/src/views/metadata-admin/ apps/console/vitest run packages/app-shell/(full, at the previous commit)type-check— components, app-shell, consolecheck:eager-closurecheck:icon-record-namescheck:changeset-presence/check:changeset-no-majorcheck:readme-exports,check:control-bytes,check:self-import,check:phantom-deps,check:side-effects-array,check:dist-completeness,check:lint-rule-coverageeslintover the 12 changed source filesmain(37 and 7), and the two delegating files dropped from 2 warnings each to 0check-governed-queue-guard --testover the diffReverse validation — run from the committed state,
trap ... EXIT INT TERMrestore with absolute paths, each leg proved on disk (HEAD blob hash printed first and non-empty, mutated hash after,git diff --numstatnon-empty, grep anchor counts before and after) and each restore proved by the blob hash returning to HEAD's with an emptygit diff HEAD:icon-name-vocabulary-9204.test.tsgo red;isLucideIconName→ 2 of 10 go red.⛔ Not measured here, left to CI: the full
check:*farm and repo-widepnpm lint.Acceptance notes
needs:contract-reviewlabel was applied by the previous round under itsClause-②: yes. The ruling restatesClause-②: no, so this round did not apply it — and did not remove it either, because it is another actor's state. Flagged for the PM rather than reverted.iconsrecord puts about 1.4 MB raw of icon modules in this chunk, and half the chunk is lucide. It comes back as its own decision card if the row trips again.getLazyIconnow re-fetches, through a per-icon chunk, a component the page already holds. Rendering straight from the record would remove the deferred map entirely and take more bytes than this change did. ⛔ Not done here: it would change what@object-ui/componentspublishes for a consumer that importsgetLazyIconwithout any record reader, and the record is Door 2's subject. Successor: whoever takes the Door 2 decision card.Building2) do they kebab to a name lucide does not carry. Those 95 answeredfalsebefore this change too and still draw the fallback glyph; what is new is that lucide logs about them. A digit-aware tokeniser would change what resolves for names nobody has asked for, andaxis-3dagainst the retiredaxis-3-dshows the two spellings can both exist with only one live. Successor: whoever revisits the icon-name tokeniser.vendor-icons-*advancedChunksgroup for lucide's per-icon modules would drop this row under its ceiling without removing one byte from the page load. Recorded again so the next reader meets it as a refusal rather than an idea.packages/fieldsbarrel, and any re-chunk ofreact-day-picker/react-hook-form/vaul/date-fns/embla-carousel/react-resizable-panels.resolveIconseam ruling, which this change does not reopen —resolveIcon's signature and contract are untouched).Drafted by the
domain:specat objectui seat under the maintainer's takeover instruction; the session behind this revision issession_01L5xpA5q533BgTTNADibEFt.🤖 Generated with Claude Code
https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt