Skip to content

fix(icons): correct two retired lucide spellings on registration icon meta - #7590

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-5936-retired-icon-spellings
Sep 4, 2026
Merged

fix(icons): correct two retired lucide spellings on registration icon meta#7590
os-project-manager merged 1 commit into
mainfrom
claude/issue-5936-retired-icon-spellings

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Part of #5936

Clause-②: no — this diff changes two authored VALUES and reaches no declaration. No key is declared or removed, no accept set moves, no gate is extended. Determined from the diff itself: two lines, both the right-hand side of an existing icon: property inside an existing registration meta object.

What changed

Two component registrations declared lucide spellings that lucide has retired:

Site Before After
packages/components/src/renderers/layout/page.tsx (ui:page) icon: 'Layout' icon: 'panels-top-left'
packages/plugin-detail/src/index.tsx (record:alert) icon: 'AlertTriangle' icon: 'triangle-alert'

Plus one changeset. Nothing else. git diff --stat is 2 files changed, 2 insertions(+), 2 deletions(-) before the changeset.

The load-bearing measurement: behaviour-neutral BY IDENTITY

lucide retires a spelling by dropping it from the runtime icons record while keeping the deprecated named export pointing at the very same glyph object. So the repair cannot substitute one glyph for another — that is what makes it landable without a contract review, and it is measured against the installed lucide 1.31.0 rather than asserted:

>> IDENTITY  lucide.Layout        === icons["PanelsTopLeft"] : true
>> IDENTITY  lucide.Layout        === lucide.PanelsTopLeft   : true
>> IDENTITY  lucide.AlertTriangle === icons["TriangleAlert"] : true
>> IDENTITY  lucide.AlertTriangle === lucide.TriangleAlert   : true

Membership, resolved through the seam's own tokeniser (renderers/action/resolve-icon.ts):

"Layout"          -> key "Layout"        | member of RECORD icons: false | resolveIcon: null
"panels-top-left" -> key "PanelsTopLeft" | member of RECORD icons: true  | resolveIcon: component
"AlertTriangle"   -> key "AlertTriangle" | member of RECORD icons: false | resolveIcon: null
"triangle-alert"  -> key "TriangleAlert" | member of RECORD icons: true  | resolveIcon: component

LIT  CONTROL hasOwnProperty(icons, "CircleCheck")       : true   (membership instrument live)
LIT  CONTROL resolveIcon("circle-check")                : component
LIT  CONTROL iconNames.includes("circle-check")         : true   (dynamic instrument live)
DARK CONTROL hasOwnProperty(icons, "NoSuchGlyphXyzzy")  : false  (false is reachable)
DARK CONTROL resolveIcon("no-such-glyph-xyzzy")         : null

Every zero-answering query above is printed beside a control in the same query shape that fires.

Neutral in all three resolution worlds, which is why it is correct whichever way the open half of #5936 is decided:

  • through a record-reading resolver — old resolves to null, new resolves to that shared object; a repair, never a substitution;
  • through the dynamic surface (iconNames, 2025 names, retired aliases included) — both spellings already resolved, to the same glyph;
  • with no consumer at all — inert either way.

Why the kebab spellings. They are the only ones live on BOTH surfaces. Measured:

panels-top-left | RECORD: true  | DYNAMIC: true
PanelsTopLeft   | RECORD: true  | DYNAMIC: false
triangle-alert  | RECORD: true  | DYNAMIC: true
TriangleAlert   | RECORD: true  | DYNAMIC: false
Layout          | RECORD: false | DYNAMIC: false
AlertTriangle   | RECORD: false | DYNAMIC: false

iconNames is kebab-case only, so a PascalCase record key is absent from it. The card's prescribed spellings are therefore also the strictly safer ones.

The gate reading, and an honest correction

scripts/check-lucide-icon-record-names.mjs is green both before and after, with identical counters (182 judged names). It does not go red on the old spellings, because it deliberately does not judge a component registration's icon meta — which is exactly what #5936 says, and this PR does not change it.

That green is a real reading and not a dead instrument, proven two ways in the same run shape:

1. Lit control — the same retired spelling on a site the gate DOES judge. Put 'Layout' into the anchored iconMap in packages/plugin-view/src/ObjectView.tsx:

GATE exit=1
FAIL  lucide icon names
    - packages/plugin-view/src/ObjectView.tsx:1119  [iconMap (strings)]
      "Layout" -> `Layout` is not a key of the runtime `icons` record. lucide keeps it only as a
      DEPRECATED EXPORT of the same glyph — write `panels-top-left` (the spelling the record carries).

The gate derives panels-top-left by identity, independently corroborating the measurement above.

2. Ablation — the retired spellings put back at the two sites in this PR. Gate exit 0, and every counter identical to the repaired tree: authored icon names judged: 148, anchored map entries judged: 34, 182 total.

Both ablations ran under trap ... EXIT INT TERM with absolute paths. Each mutation was proven on disk before any result was read (anchored fixed-string counts plus git hash-object differing from the HEAD blob), and each restore was proven BY STATE — git diff HEAD empty AND on-disk blob equal to the HEAD blob, restored via git checkout HEAD -- ABSOLUTE_PATH. The mutated blobs round-tripped byte-exactly to the pre-repair blobs (0c14df03c, b9451e00f), so the ablation is provably the exact inverse of the repair.

Verification

Gate union re-run after the final commit, at fadfd6e06, tree clean:

check-lucide-icon-record-names  exit=0  OK  lucide icon names: 182 authored/declared names reaching 1
                                            record-reading resolver are live `icons` keys
check-control-bytes             exit=0  check-control-bytes: OK (scanned 6246 tracked text file(s))
check-changeset-presence        exit=0  2 source file(s) of 2 released package(s) changed, and this
                                            change declares 1 changeset(s)
check-changeset-fixed           exit=0  All workspace packages are in the changeset fixed group.
check-changeset-no-major        exit=0  No changeset declares a `major` bump.

Every exit code captured by redirect-then-read, never through a pipe, and each verdict line above is the gate's own.

  • Testspnpm exec vitest run packages/components/ packages/plugin-detail/ from the repo root (the package-scoped form is refused by scripts/vitest-invocation-guard.mjs): 360 test files passed, 3333 tests passed, 0 failures. Both changed packages in full, not a subset.
  • Type-checkpackages/components and packages/plugin-detail, both tsc --noEmit && tsc -p tsconfig.test.json, both Done. Scope: 2 of 47 workspace projects confirms the filter matched rather than silently matching nothing.
  • Dependency closure built first, so nothing above read a stale dist.
  • Changeset — the gate ruled the bump. It demanded a declaration and offered the empty-frontmatter form; a real patch for the two packages was written instead, because the "no consumer anywhere" reading is Locate the consumer of a registration's icon meta before extending the icon gate to it — zero read points in objectui AND objectstack, so two suspect spellings stay undetermined #5936's ADR-0049 retirement PREMISE and not a proof of absence, so declaring "releases nothing" would lean on exactly the premise this slice is fenced away from.

Declared narrowing (one)

Lint. eslint was run on the two changed files rather than repo-wide: 2 files linted (count from --format json), 0 errors, 42 warnings, all pre-existing and none on the changed lines. The three things that make this a measurement rather than a skipped run:

  1. the population is read off eslint's own config (files: ['**/*.{ts,tsx}'] in eslint.config.js), not guessed — and eslint's own membership answer was exercised in both directions (--print-config resolves 117 rules for each changed file; a node_modules path comes back File ignored by default);
  2. the file count is eslint's own, from --format json;
  3. type-aware linting is not enabled — the config extends tseslint.configs.recommended, not recommendedTypeChecked, and declares no project / projectService. Every rule is therefore per-file, so a string-literal change inside these two files cannot move the verdict of any untouched file. A repo-wide run could not have caught anything this run did not.

CI runs the full farm regardless.

What is deliberately NOT here

#5936 has a second half that this PR does not touch and must not be read as having settled:

  • Nothing is retired. No declared key removed, no registration meta deleted.
  • The gate is not extended to registrations' icon meta (scope item 3) — the relayed reading found no record-reading consumer to justify it.
  • Whether the registration icon meta should itself be retired (scope item 4) is ADR-0049 territory and stays with the maintainer. The card stays open for it, which is why this is Part of #5936 and not Fixes.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

… meta

`ui:page` declared `icon: 'Layout'` and `record:alert` declared
`icon: 'AlertTriangle'`. lucide retires a spelling by dropping it from the
runtime `icons` record while keeping the deprecated named export, so both
names still import and still type-check while resolving to nothing through
any resolver that reads that record.

Behaviour-neutral by identity, measured against the installed lucide 1.31.0:
`Layout === icons['PanelsTopLeft']` and `AlertTriangle === icons['TriangleAlert']`
are both true, so the repair cannot substitute one glyph for another.

The kebab spellings are the only ones live on BOTH surfaces: `PanelsTopLeft`
and `TriangleAlert` are record keys but are absent from the dynamic
`iconNames` list, which is kebab-case only.

Nothing is retired and no gate is extended.

Part of #5936

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@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) 3182.7 KB 3191.4 KB
Main entry chunk (gzip) 143.2 KB 350 KB
Entry file index-BeAPHbHh.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) 516.22KB 117.83KB
core (index.js) 6.12KB 2.42KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 180.00KB 50.20KB
fields (index.js) 242.40KB 61.26KB
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.55KB 0.62KB
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 (useResponsiveConfig.js) 1.37KB 0.63KB
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.09KB 13.34KB
plugin-charts (index.js) 70.92KB 19.75KB
plugin-chatbot (index.js) 196.19KB 46.43KB
plugin-dashboard (index.js) 132.89KB 34.68KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 250.93KB 64.09KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.87KB 32.66KB
plugin-gantt (index.js) 167.44KB 41.05KB
plugin-grid (index.js) 210.75KB 56.95KB
plugin-kanban (index.js) 52.71KB 14.55KB
plugin-list (index.js) 113.28KB 27.59KB
plugin-map (index.js) 20.55KB 6.80KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.57KB 11.96KB
plugin-timeline (index.js) 30.84KB 8.85KB
plugin-tree (index.js) 9.38KB 3.22KB
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

@os-project-manager
os-project-manager marked this pull request as ready for review September 4, 2026 03:21
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit e30ed15 Sep 4, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-5936-retired-icon-spellings branch September 4, 2026 03:36
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.

2 participants