Skip to content

fix(components): split the letter-to-digit boundary in the icon tokeniser - #9442

Merged
os-tesla merged 1 commit into
mainfrom
claude/issue-9414-lazy-icon-digit-boundary
Sep 14, 2026
Merged

fix(components): split the letter-to-digit boundary in the icon tokeniser#9442
os-tesla merged 1 commit into
mainfrom
claude/issue-9414-lazy-icon-digit-boundary

Conversation

@os-tesla

@os-tesla os-tesla commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Fixes #9414

Session: session_011QreXiyMEqKLN4U5daMPVa — in prose because a footer link does not
survive an edit of this description.

What was wrong

packages/components/src/lib/lazy-icon.tsx's toKebabIconName split two boundaries —
lower-or-digit -> Upper and acronym-run -> Word — and never letter -> digit. So
Building2, the PascalCase component lucide-react exports and the spelling lucide's own
site shows an author, tokenised to building2 while the canonical key is building-2. The
name matched nothing and getLazyIcon degraded it to the Database glyph with no error,
no warning and no log
— the author saw an icon with no signal that it was not theirs.

Re-measured at this branch's own base — the card's numbers were NOT inherited

Base 63d9ca6, lucide-react@1.35.0 as resolved from packages/components, the card's
reproducer run from that directory so the specifiers resolve:

namespace keys 6110 | canonical names 2039
non-alias keys the tokeniser drops 155
recovered by a digit-boundary split 147
CONTROL, must be true: true true

The CONTROL line is alive in that same run, so this is a reading rather than a void one.
PR #8941 (lucide-react 1.43.0) had not landed at this base, so the population is still
the one the card measured against.

Two differences from the card, both reported rather than smoothed over:

  • 155, not 154. The extra member is default, the namespace's own default-export key.
    It is not an icon and stays correctly rejected. The card's residual list of seven was
    otherwise exact.
  • The residual after the fix is two, not seven or eight. The rule below reaches 153 of
    the 155, leaving default and useLucideContext — a default export and a hook. Neither
    is an icon, and both are still rejected.

The stronger reading, which is what the pin actually asserts: every one of the 2,039
canonical icon names is now reachable from its own exported PascalCase spelling
, up from
1,882. ⛔ That number is not written into any source file — the test re-derives it from the
installed lucide on every run (AGENTS.md #9).

The fix — one rule, and deliberately not an unconditional one

A third .replace(...) on the same chain: match a letter immediately followed by a
digit and insert a hyphen between them, under a negative lookbehind that refuses the match
when the letter is itself preceded by a digit.

⚠ Spelled in prose rather than pasted, and the reason is on-topic: the literal regex
opens with a question mark, a less-than sign and an exclamation mark, and GitHub's body
sanitizer eats that sequence as an HTML comment opener — including inside a code
fence
. The first revision of this description was stored with the ! silently removed,
turning the regex into a different one. The exact text lives in
packages/components/src/lib/lazy-icon.tsx.

The transform is the inverse of lucide's own toPascalCase, which builds each exported
component name by dropping the hyphens of a canonical name and upper-casing what followed.
The job is to put a boundary back exactly where toPascalCase removed one — and the
negative lookbehind is what stops it going further. When the letter is itself preceded by a
digit, it sits inside a segment rather than starting one: Grid2x2 is the Pascal form of
grid-2x2, and an unconditional rule yields grid-2x-2, which is not a name.

Design call 1 — a tokeniser rule, not an alias table

The card framed this as smaller vs. auditable, and the measurement inverts that framing.
A table would need 153 hand-listed rows that state a fact derived once and never again —
exactly the shape AGENTS.md #9 rules against — and would rot at every lucide bump with
nothing to notice. The rule is re-derived from its input on every call, and its acceptance
test enumerates the whole canonical population from the installed lucide, so it is
auditable by re-derivation rather than by reading a list. A table would also be a second
de-facto vocabulary standing beside the rule, which is the "N dialects" outcome
commandment #0.1 exists to refuse.

Design call 2 — the Grid2x2 family is IN

The card and triage both fenced this as the taker's call, so: in, and reached by the
same single rule rather than a second one.

The second rule the card anticipated would be a digit -> letter split. Measured, it
recovers five of the six by landing on lucide's alias spelling grid-2-x-2, and still
misses Grid3x2 — for which lucide ships no grid-3-x-2 alias at all. That leaves an
asymmetric hole that is more confusing than a clean stated fence. The lookbehind
refinement of rule 3 lands on lucide's primary grid-2x2 instead, and reaches all six.
⇒ including them costs one lookbehind assertion, not a second rule, so leaving them out
would have been a fence with nothing behind it.

The no-loss leg — ⛔ a count that only goes up is not a reading

Triage asked for proof that the 1,882 keys which already survived still survive
byte-identically. Measured over the whole population and pinned as two cases that run on
every CI run:

Clause-②: no

Contract-text: the seam's published .d.tspackages/components publishes
files: ["dist", "README.md", "CHANGELOG.md", "LICENSE"], and tsc emits these doc
comments verbatim into dist/lib/lazy-icon.d.ts, where they have shipped since before this
card:

Whether name (kebab-case or PascalCase) resolves to a real Lucide icon.

Resolve a Lucide icon by name (kebab-case or PascalCase). ... Falls back to the Database
icon when no name is provided or when the requested name is not a valid Lucide icon

and the published notifications guide, linked from that same published README:

Every surface ... resolves icon through the same rule: a declared Lucide name
(kebab-case or PascalCase) replaces the severity icon; anything else falls back to it.

A name Lucide doesn't have costs the author their override and nothing more

⇒ the declared accept set is real Lucide icon names, spelled kebab-case or PascalCase.
Building2 is precisely that: the PascalCase spelling lucide-react exports for the live
canonical key building-2. Not one of the names this change admits sits outside the
declared set, and the reject rule — a name Lucide does not have still degrades rather than
throwing — is untouched. So the accept set does not move; the implementation stops
under-delivering against it. That is the case the rules exempt from Clause ② in as many
words, which is why the claim's conservative yes is overturned here rather than carried.

⛔ The mechanical half is neutral, not supporting, and is reported as such:
check-widening-tells.mjs --declaration no exits 0 on this diff while printing
5 NOT MEASURED ... no declared surface covers it. An exit 0 that examined nothing is not
evidence, so the declaration rests on the contract text above.

The pin, before and after

The gate is correctly green on the defect: check:icon-record-names censuses what IS
authored, and nothing in this tree authors a digit-suffixed name. So the test pins the
capability instead, and it was proved to fail before the fix — mutation proved on disk,
restore proved by state.

Mutation: rule 3 deleted from lazy-icon.tsx, i.e. the pre-#9414 tokeniser body restored.
Proved on disk by anchor count and by blob hash, ⛔ not by an editor's exit code:

MUTATION ON DISK: anchor 1 -> 0 ; blob e25f2199eefd3db3586df9d49bdee456b07acfbc -> a5c3ee15fd6cd13f46bcff3bc058da8f07ef2f41

With the fix reverted:

Test Files  3 failed (3)
     Tests  18 failed | 20 passed (38)
AssertionError: expected [ 'arrow-down-0-1', …(156) ] to deeply equal []
AssertionError: expected { kebab: 'building2', live: true } to deeply equal { kebab: 'building-2', live: true }
AssertionError: expected { kebab: 'grid2x2', live: true } to deeply equal { kebab: 'grid-2x2', live: true }

157 canonical names unreachable, which is exactly 2039 - 1882. The two no-loss cases pass
under the mutation, which is the correct shape — they assert that nothing moved.

Restore proved by state, ⛔ not by re-running the test:

git diff HEAD: EMPTY
working blob : e25f2199eefd3db3586df9d49bdee456b07acfbc
HEAD blob    : e25f2199eefd3db3586df9d49bdee456b07acfbc

No build was involved in either leg and none was needed: the root vitest.config.mts maps
@object-ui/components to packages/components/src, so the mutated source is the subject
under test directly rather than a stale dist.

Two existing pins recorded the defect as intended behaviour, and both move

  • lazy-icon-generated-app-contract-7472.test.ts listed Building2 among lucide's alias
    spellings that stopped resolving, beside HouseIcon and LucideHouse. It never was one.
    The two alias shapes are correctly described and are untouched here; Building2 is
    canonical, and only this tokeniser could not reach it. The file now asserts the opposite,
    with the correction stated in place.
  • record-alert.severityIcons.test.ts armed a tripwire reading "Should lucide ever add
    check-circle2 to the dynamic surface, this line goes red — deliberately, because that
    would be worth a look." It fired, for a cause the note did not anticipate: lucide added
    nothing, and the seam learned the boundary. CheckCircle2 moves from DARK to LIT; the
    DARK half moves to LucideCheckCircle2, a prefix alias that must stay unreachable, so the
    control still stands on the same footing.

Interaction with PR #8941, stated rather than left to be discovered

The worked-example cases assert that each expected canonical name is a live member of
lucide's dynamic surface, so a lucide release that retires one reds here by design instead
of leaving the assertion restating a dead name. #8941 moves lucide to 1.43.0. If it lands
first, this branch should be merged with origin/main and the suite re-run; a red in those
cases is the pin doing its job and names the retired spelling directly. The universal leg
needs no such care — it is derived from whatever lucide is installed.

What ran

leg result
new pin lazy-icon-digit-boundary-9414.test.ts 20 passed
packages/components/ + packages/plugin-detail/ full suites 4,266 tests — 2 failed on the first pass, both the stale pins above; green after they moved
every test file touching the seam or authoring a digit-bearing name (28 files, derived by grep) 28 files, 309 tests passed
turbo run type-check for both packages 14 tasks successful
turbo run lint for both packages 3 tasks successful, 0 errors
check:icon-record-names OK ... 184 authored/declared names reaching 1 record-reading resolver are live — unchanged, and ⛔ the gate was NOT widened
check:control-bytes · check:changeset-presence · changeset:check · check:changeset-claims · check:changeset-overwrite · check:new-line-citations · check:test-path-roots · check:comment-mask-corpus all green
check:readme-exports NOT MEASURED — it needs every package built and reports the population COLLAPSED, this run proves nothing otherwise. No export surface moved in this diff, so it is outside what this change owes; CI runs it.
repo-wide sweeps (pnpm test, pnpm lint, full type-check) CI's run, not duplicated here

Acceptance notes

  • The pending changeset for objectui#7472 lists "digit-suffixed spellings such as
    Building2" among lucide's alias forms that stopped resolving. The two alias shapes it
    names are correct; Building2 never belonged with them. ⛔ That changeset was NOT edited —
    it belongs to another PR and check-changeset-overwrite exists for exactly that reason.
    The correction is carried in this PR's own changeset, which lands in the same CHANGELOG
    section, so a reader meets both together. Noted, not filed.
  • packages/components/src/renderers/action/__tests__/resolve-icon-seam.test.ts pins
    resolveIcon('building_2') against the icons record — a different seam on the same
    subject, touched by PR perf(components): take lucide's icons record off the eager path #9399 and not by this one. It is unaffected and passes here.
    Noted, not filed.
  • The icons-record resolver (resolve-icon.ts) has its own tokeniser and is out of this
    card's fence. Whether the two seams should agree is a live question that this change does
    not answer and does not make worse. Noted, not filed.
  • DECLARED_RECORD_READERS and the gate's discovery predicate were not widened, per the
    fence objectui#7472 set and this card inherited.

🤖 Generated with Claude Code

https://claude.ai/code/session_011QreXiyMEqKLN4U5daMPVa


Generated by Claude Code

…iser

`toKebabIconName` split `lower-or-digit -> Upper` and `acronym-run -> Word`,
and never `letter -> digit`. `Building2` — the PascalCase component
`lucide-react` exports and the spelling lucide's own site shows an author —
tokenised to `building2` while the canonical key is `building-2`, so the name
matched nothing and `getLazyIcon` degraded it to the `Database` glyph with no
error, no warning and no log.

The new rule is deliberately not unconditional: a negative lookbehind holds the
split off when the letter is itself preceded by a digit, which is lucide's grid
spelling (`Grid2x2` is the Pascal form of `grid-2x2`, where the `x` sits inside
a segment rather than starting one). That is what lands the same single rule on
lucide's primary `grid-2x2` rather than its `grid-2-x-2` alias, and it is why
`Grid3x2` — for which lucide ships no `-3-x-2` alias — is reached too.

The change is a strict superset: every name the previous tokeniser accepted
still tokenises byte-identically, and every canonical kebab spelling is still
returned untouched. Both legs and the capability itself are re-derived from the
installed lucide on every run rather than written down.

Two existing pins move with the seam, both of which had recorded the defect as
intended behaviour:

- the CLI-seam contract test listed `Building2` among lucide's ALIAS spellings.
  It never was one; it is canonical, and only the tokeniser could not reach it.
- `record-alert`'s severity-icon controls armed a tripwire for the day
  `check-circle-2` became reachable. It fired — for the tokeniser, not for a
  lucide release — so `CheckCircle2` moves from DARK to LIT and the DARK half
  moves to `LucideCheckCircle2`, a prefix alias that must stay unreachable.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3061.0 KB 3104.5 KB
Main entry chunk (gzip) 145.6 KB 350 KB
Entry file index-DzQM6URy.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) 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) 544.89KB 130.48KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 213.54KB 59.33KB
fields (index.js) 247.89KB 62.50KB
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.83KB 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.92KB 14.22KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.27KB 65.55KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.55KB 57.83KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.68KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.36KB 20.78KB
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) 99.04KB 32.62KB
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.04KB 5.36KB
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

Copy link
Copy Markdown
Collaborator Author

Bundle Analysis — the gate and the reason, recorded on the PR as the gate itself asks

Posted by the domain:ui PM seat. This is the record the enqueue rule requires on the pull request (⛔ not on the card) before a PR is queued carrying this red.

The gate: Bundle Analysis (.github/workflows/performance-budget.yml). It is a blocking check when it runs — scripts/dependabot-merge-gate.mjs classifies it under OPTIONAL_CONTEXTS, where "optional" is about the trigger-level path filter, ⛔ not about the check being advisory: present ⇒ must be success; absent ⇒ not waited for.

The reason it is red here, quoted from the gate's own output rather than paraphrased:

❌ chunk `ui-components`  387.9 KB measured / 389.6 KB ceiling
   (headroom 1.7 KB = 0.02x the 89.0 KB regression, under the 0.10x floor
   and held open by its declared 4289-byte allowance)
chunk `ui-components` was ALREADY declared exhausted before this run, and has
now lost a further 0.01x of a regression against the pinned figure.

and the gate's own instruction, verbatim:

⚠️ READ THIS BEFORE AUDITING YOUR OWN DIFF. This row's headroom is a standing debt that predates this change, and it moves under traffic that has nothing to do with the chunk — measured at three gzipped bytes across five unrelated merges. So this verdict is NOT an accusation that your diff spent the bytes…

⛔ There is therefore nothing here for this pull request to "fix", and the two edits that would turn this green are both forbidden: ⛔ never raise the ceiling, and ⛔ never raise the allowance. Paying the row down is the open decision on the chunk, ⛔ not a task for whichever change the queue happened to weigh — take it there, and say on this pull request that you did.

⇒ Taken there: the debt is tracked at objectui#9251 / objectui#9204. ⛔ No duplicate filed, and ⛔ nothing in this PR attempts to turn the row green.

The three conditions for carrying it red into the queue, each verified structurally this round:

  1. Red by design on a pushed branch ✓ — the gate says so in its own output above: nothing here for this PR to fix, and both green-making edits forbidden.
  2. Does not run on merge_group ✓ — the workflow's on: carries only push and pull_request; the sole merge_group mentions in the file are in a comment explaining it cannot hold the queue.
  3. This comment.

⚠️ Correction of this seat's own earlier record, so the bad sentence does not propagate: this seat previously wrote that the gate is "red on origin/main's own tip". That is withdrawnmain's tip often carries no Bundle Analysis run at all, because the push: trigger is path-filtered, and a bare zero there reads as a pass while meaning nothing of the kind. The durable statement is red on 7 of the last 7 main commits that actually ran it. The entry-chunk half of the gate is green throughout (144.5 KB against a 350 KB line); only the ui-components closure row is red.


Generated by Claude Code

@os-tesla
os-tesla marked this pull request as ready for review September 13, 2026 23:52
@os-tesla
os-tesla added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit 1ef89c0 Sep 14, 2026
38 checks passed
@os-tesla
os-tesla deleted the claude/issue-9414-lazy-icon-digit-boundary branch September 14, 2026 00:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants