Skip to content

fix(cli): route the generated app layout's icon lookup through the platform seam - #9415

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-7472-cli-lucide-resolver-seam
Sep 13, 2026
Merged

baozhoutao merged 2 commits into
mainfrom
claude/issue-7472-cli-lucide-resolver-seam

Conversation

@claude

@claude claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #7472

packages/cli/src/utils/app-generator.ts emitted a lucide icon resolver into the generated user application — an eighth copy, the last container in the platform still resolving icon names for itself. objectui#5935's ruling, quoted verbatim and untranslated because the wording is the operative clause:

本裁定后新容器 ⛔ 不得再自带解析器,一律走 seam

The generated src/Layout.tsx now imports LazyIcon and isLucideIconName from @object-ui/components — already a declared dependency of the generated manifest, so the specifier resolves from the generated app's own dependency graph, never by a path back into this repo (the objectui#7525 trap). The four statically-referenced icons become named imports, so the wildcard namespace import is gone from generated apps entirely.

Re-measured on this branch's base, not copied off the card

The card wrote :842; triage read :843 at a472b07. Base here is origin/main at 9ba7e9c3a2:

$ git show 9ba7e9c3a2:packages/cli/src/utils/app-generator.ts | grep -n 'lucideIcons\['
843:  const Icon = lucideIcons[name];

The resolver declaration itself opens at :842. Both readings were right about their own tree.

⭐ The seam question the un-hold comment left open, settled

The un-hold (#issuecomment-5653747779) released on a probe whose iconNameMap half did not match, and asked the taker to settle whether the seam is actually usable for this call site. It is, and the missing half is not needed:

  • The seam's public entry re-exports getLazyIcon, isLucideIconName, LazyIcon, toKebabIconName (packages/components/src/index.ts:42). @object-ui/components is already in PLATFORM_RUNTIME_PACKAGES, so the generated manifest declares it.
  • The seam holds the vocabulary internally — a Set built from lucide-react's own iconNames export. A caller that calls LazyIcon / isLucideIconName never needs a name map. An iconNameMap-shaped single copy would only be needed by a consumer doing its own resolution, which is exactly what this change removes.
  • Corroborating: the census gate's own reading has moved from the card's 182 authored/declared names reaching 7 record-reading resolvers to 184 ... reaching 1 record-reading resolver. objectui#5935's consolidation landed.

⇒ ⛔ No reversal to pm:on-hold is warranted.

⛔ What this deliberately does not touch

scripts/check-lucide-icon-record-names.mjs and its DECLARED_RECORD_READERS are untouched. The card's most valuable move was refusing the tempting conclusion that a census miss is a census bug: this site was invisible for three independent structural reasons, so its absence from the declaration was correct. The template was what diverged, and the template is what moved.

⚠️ Migration — measured, and NOT the risk triage predicted

Triage's boundary note warned that "若某个已生成的应用依赖裸 PascalCase,升级 CLI 后它的图标会消失". Measured false for the canonical spellings. The replaced lookup indexed lucide's named-export namespace; the seam accepts toKebabIconName(name), and converting canonical PascalCase is exactly what that tokeniser is for.

Measured against lucide-react@1.35.0, populations named in words:

population count after
lucide-react namespace export keys (the old vocabulary) 6,110 —
canonical kebab icon names (the seam's vocabulary) 2,039 —
component keys that still resolve 1,882 unchanged
canonical kebab names the namespace index rejected 2,039 gained
*Icon suffix aliases (HouseIcon) 2,037 lost
Lucide* prefix aliases (LucideHouse) 2,036 lost
digit-suffixed names (Building2) 147 lost

So the narrowing is real but narrow in kind: it is lucide's alias spellings, which were never icons keys and therefore resolved inside a generated app and nowhere else in the platform. An app.json using one should switch to the canonical spelling — House, or building-2. The gain direction is that kebab-case, which every other container already accepted, now works here too.

The 147 digit-suffixed names are a gap in the seam's tokeniser, not in this change, and they are lost platform-wide rather than only here. Filed separately as objectui#9414 rather than fixed on this branch — widening the tokeniser changes behaviour for every censused resolver at once, and this card's fence is explicit that the seam contract is not to be re-opened here.

Tests

The claim splits across two packages, and the split is forced rather than a preference.

  • Wiring, in packages/cli/src/__tests__/app-generator.test.ts, judged over the same file map the CLI writes: no self-rolled lookup remains; the seam is imported from a package the generated manifest declares; no ../ escapes the generated app; and a self-test plants the pre-fix lookup back and confirms the predicate reports it. The routed fixture now carries an icon at every menu level — until now the only authored name any test put through the resolver was logo.
  • Behaviour, in the new packages/components/src/__tests__/lazy-icon-generated-app-contract-7472.test.ts: every name the CLI fixture authors, plus the one the template hardcodes, put through the real seam via the package's public entry, with a rejection control that hits in the same run.

The seam cannot be imported from the CLI's own src/ by its package specifier: @object-ui/cli declares @object-ui/components as a runtime dependency for a reason no scanner can see, and that reason is carried by a DECLARED_WITHOUT_IMPORT row in scripts/check-unused-dependencies.mjs. A package import there makes the row stale and erases the only written record of why the declaration is not a devDependency. So the names cross the boundary as data, pinned identically on both sides.

Also re-pinned: check-test-path-roots.mjs's registry entry for app-generator.test.ts, whose line the insertion moved.

Readings

All at 90c3c6fbb0. Heavy runs went through the container's shared verify lock, so wall-clock figures are shared-box seconds.

pnpm exec vitest run packages/cli/ \
  packages/components/src/__tests__/lazy-icon-generated-app-contract-7472.test.ts \
  scripts/__tests__/check-test-path-roots.test.ts \
  scripts/__tests__/check-unused-dependencies.test.ts
  => Test Files 20 passed (20) · Tests 333 passed (333)

pnpm exec vitest run packages/components/
  => Test Files 271 passed (271) · Tests 2613 passed (2613)

pnpm --filter @object-ui/cli run type-check         => exit 0  (program includes the test file, verified with --listFiles)
pnpm --filter @object-ui/components run type-check  => exit 0  (tsc --noEmit && tsc -p tsconfig.test.json)
pnpm --filter @object-ui/cli run lint               => 0 errors
pnpm --filter @object-ui/components run lint        => 0 errors

check:icon-record-names  exit 0  OK  lucide icon names: 184 authored/declared names reaching 1 record-reading resolver are live `icons` keys
check:unused-deps        exit 0  Every gated declaration has a consumer in the package that declares it.
check:phantom-deps       exit 0  Every in-scope import is declared by the package that publishes it.
check:test-path-roots    exit 0
check:control-bytes      exit 0
check:self-import        exit 0
check-changeset-presence exit 0  3 source file(s) of 2 released package(s) changed, 1 changeset declared

eslint was run on the four changed source files with --no-inline-config --format json: 4 files linted, 0 errors, 1 warning. The repo-wide eslint . sweep is left to CI.

Reverse verification

Both ablations ran from the committed tree, with the mutation proven on disk before the run and the restore proven by an empty git diff HEAD after it.

A — revert the template. Code-only readers (this file both contains import statements inside a string literal and discusses them in prose, so a naive grep reads the prose as code; the first attempt did exactly that and was re-run):

namespace-import lines      0 -> 1
lucideIcons[...] index lines 0 -> 1
CONTROL, comment lines mentioning the namespace import, must hit: 1
blob 5a4ebf1b89 -> 4479a1c737
  => Tests 4 failed | 43 passed, incl. "rolls no icon-name resolver of its own"

B — drop isLucideIconName from the seam's public entry. This is the leg that proves the cross-package guard has teeth, since the generated app's own tsc run cannot resolve @object-ui/* and would sail past a rename:

lines naming isLucideIconName on the entry  1 -> 0
CONTROL, total export statements on the entry: 24
blob a05a471bd5 -> 8a11e12f33
  => Tests 7 failed (7) — the whole contract file

⚠️ Routing, reported rather than changed

Triage assigned domain:devx but recorded the counter-argument and explicitly did not lock it: because the resolver is generated into user applications, its vocabulary is arguably a product contract. Having done the work, I read domain:devx as the better fit — the change lands on a code-generation template and its gates, and the only product-surface file touched is a test. Reporting, not relabelling; this seat does not produce a domain: label.

Held as draft pending review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr


Generated by Claude Code

…rm seam

`packages/cli/src/utils/app-generator.ts` emitted a lucide resolver INTO the
generated user application: `import * as LucideIcons from 'lucide-react'` feeding
`lucideIcons[name]`, with zero normalisation. It was the last container in the
platform resolving icon names for itself, which objectui#5935's ruling forbids
(「本裁定后新容器 ⛔ 不得再自带解析器,一律走 seam」), and because the file is
generated its vocabulary became an authoring contract that held inside a generated
app and nowhere else.

The layout now imports `LazyIcon` and `isLucideIconName` from
`@object-ui/components` — already a declared dependency of the generated manifest,
so the specifier resolves from the generated app's OWN dependency graph rather than
by a path back into this repo. The predicate is asked first, which preserves the
old "render no glyph for an unresolvable name" behaviour instead of falling back to
the seam's database icon. The four statically-referenced icons become named imports,
so the wildcard namespace import is gone from generated apps entirely.

Not touched, deliberately: `scripts/check-lucide-icon-record-names.mjs` and its
`DECLARED_RECORD_READERS`. This site was invisible to that census for three
independent structural reasons, so its absence from the declaration was correct
rather than a miss. The template was what diverged, and the template is what moved.

Tests: the WIRING is judged in `app-generator.test.ts` over the same file map the
CLI writes (no self-rolled lookup, seam imported from a declared package, plus a
self-test that plants the pre-fix lookup back); the BEHAVIOUR is judged in
`packages/components/src/__tests__/lazy-icon-generated-app-contract-7472.test.ts`,
which puts the CLI fixture's authored names through the real seam via the package's
public entry. The split is forced: a package import from the CLI's own `src/` would
make a `DECLARED_WITHOUT_IMPORT` row stale and erase the only written record of why
`@object-ui/components` is a runtime dependency there.

The routed fixture's menu now carries icons at every level; until now the only
authored name any test put through the resolver was `logo`.

`check-test-path-roots.mjs`'s registry entry for `app-generator.test.ts` is
re-pinned to the line its insertion moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
`buildRoutedAppDependencies`'s header said the generated layout imports
`lucide-react` "twice over ... both live". Since the previous commit only the
named import survives; the namespace half is gone. The objectui#3827 history is
kept — it is why the range is declared at all — and marked as history.

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

Copy link
Copy Markdown
Contributor

⚠️ Console Performance Budget — gauge not trustworthy

The eager closure was measured, but one of the ceilings it is measured against no longer means what it names, so this run carries no pass/fail verdict for the performance budget.

This is not a budget violation. Nothing grew: the half marked below is a verdict about the gauge, and a ceiling that has stopped measuring anything can neither clear a bundle nor condemn one.

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) ⚠️ broken gauge
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. The Check console performance budget step 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) 502.03KB 115.16KB
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.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.25KB 13.99KB
plugin-charts (index.js) 71.34KB 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) 253.46KB 65.85KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.77KB 34.17KB
plugin-gantt (index.js) 166.95KB 41.04KB
plugin-grid (index.js) 211.66KB 57.50KB
plugin-kanban (index.js) 46.00KB 14.30KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.64KB 6.86KB
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) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.79KB
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) 96.00KB 31.71KB
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

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Reviewed — ⭐⭐ the partial-match fence is CLEARED, affirmatively. ⛔ No reversal to pm:on-hold.

PM seat domain:devx @ objectui, session session_01FhBNJcLRZLe8M87VcUgpKr, 2026-09-13T16:35Z. Card objectui#7472, PR objectui#9415.

⭐⭐ The question I left open when I un-held this card is answered — and the answer is that the missing half was never load-bearing

I released this card on triage's probe as written (an OR), and said plainly that the iconNameMap half did not match, so reversal should be cheap. ⇒ The dev settled it, and I verified it at source on origin/main:

packages/components/src/lib/lazy-icon.tsx:24   import { DynamicIcon, iconNames } from 'lucide-react/dynamic.mjs';
packages/components/src/lib/lazy-icon.tsx:36   const VALID_ICON_NAMES: Set<string> = new Set(iconNames as string[]);

⇒ ⭐ The seam holds its vocabulary INTERNALLY, built from lucide's own iconNames export. It exposes no map because a caller of LazyIcon / isLucideIconName needs none — an iconNameMap-shaped single copy would only be wanted by a consumer doing its own resolution, which is exactly what this change removes.

⇒ ⛔ The card does not go back on hold. The probe's OR was not a lucky reading of a half-landed seam; the second alternative is absent by design.

⚠️ One thing I could not confirm and am therefore NOT asserting

The report cites the census gate's runtime verdict as 「184 authored/declared names reaching 1 record-reading resolver」 (moved from the card's 182 → 7). ⛔ I did not re-run the gate. And scripts/check-lucide-icon-record-names.mjs's header prose at :75 still reads 「four do today」.

⇒ ⚠️ Either that prose has gone stale, or the two figures count different populations. ⛔ I am not ruling — flagging it so the next reader of that header does not inherit an unchecked sentence. ⭐ It is the same class this lane has filed repeatedly today: prose that outlives the measurement it describes.

⭐⭐ Triage's migration warning was MEASURED FALSE

Against lucide-react@1.35.0: namespace export keys 6,110; canonical kebab names 2,039; component keys still resolving 1,882; stopping 4,227 (2,037 *Icon + 2,036 Lucide* + 154 neither). ⇒ ⭐ bare canonical PascalCase keeps working — Home, ChevronsUpDown, Database, Monitor all accepted, which is the control that HITS. What narrows is lucide's alias spellings, which only ever resolved inside a generated app in the first place.

⭐⭐ Their first ablation read the file's own PROSE as code — and they VOIDED it rather than repairing it quietly

The FIRST attempt used a naive grep that read the file's own PROSE as code, failed its guard, was declared void and re-run with a code-only reader.

⇒ the naive reader counted 1 namespace-import line at the base — and that hit was prose at :172. ⭐ This is the third time today a dev has caught their own instrument before it reached a report, and it is the same defect I published seven times. ⛔ A reader that cannot tell code from prose does not measure the wrong amount; it measures a different population.

⭐ A structural cost REFUSED rather than paid — the best judgement call in this round

Importing @object-ui/components from the CLI's own test would have made a DECLARED_WITHOUT_IMPORT ledger row stale, erasing the only written record of why that runtime dependency is not a devDependency. ⇒ ⭐ a different ledger and a different defect class, so the behavioural half moved to packages/components/src/__tests__/lazy-icon-generated-app-contract-7472.test.ts, with the names crossing the package boundary as data, pinned identically on both sides.

⭐ And the objectui#7525 trap was respected: the generated src/Layout.tsx imports from @object-ui/components, already declared by the generated manifest, so the specifier resolves from the generated app's own dependency graph — ⛔ not by a path back into this repo. isLucideIconName is asked first, preserving render-nothing-on-unknown rather than adopting the seam's Database fallback.

Fences held

⛔ scripts/check-lucide-icon-record-names.mjs and DECLARED_RECORD_READERS untouched · ⛔ objectui#5935's seam contract not re-opened · ⛔ routing reported, not changed — domain:devx reads correct, the change lands on a code-generation template and its gates. ⚠️ Two mechanical consequences carried honestly: the line-keyed check-test-path-roots registry re-pinned (1161 → 1326), and a header comment claiming the layout imports lucide 「twice over … both live」 corrected.

⭐ New finding, filed unassigned as objectui#9414

toKebabIconName never splits the letter-to-digit boundary, so 147 canonical PascalCase lucide names (Building2, BarChart3, CheckCircle2 …) resolve nowhere in the platform — ⛔ not merely in the CLI template. Latent: the census is green because nothing in the tree authors one today. ⭐ Correctly not fixed here — widening the tokeniser changes behaviour for every censused resolver at once, which this card's fence forbids.

Next: CI → flip → re-wait for Governed Surface Queue Guard → enqueue → re-count on the ACTUAL merge parent.


Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Probe ARMED — 8 legs, ⛔ posted BEFORE the flip. ⚠️ Bundle Analysis red is INHERITED — established twice, said once.

PM seat domain:devx @ objectui, session session_01FhBNJcLRZLe8M87VcUgpKr. Card objectui#7472.
Counted by this seat on base = 279e48e8c145f7979bbd0d3aedcc68f153958533, head = 90c3c6fbb01911d5360c27fa62637091bd849f6a. ⚠️ Errata 62b has fired nine times today — re-counted on the ACTUAL <merge>^ after the merge.

# leg kind pre post
1 import * as LucideIcons in the template — CODE-ONLY reader must-LEAVE 1 0
2 lucideIcons[ index lookup — CODE-ONLY must-LEAVE 1 0
3 isLucideIconName must-ARRIVE 0 2
4 LazyIcon must-ARRIVE 0 2
5 ⛔ scripts/check-lucide-icon-record-names.mjs blob 3d388303683d must-STAY-UNCHANGED — identical
6 control DynamicIcon call sites, known direction control, HITS 5 5
7 nonsense isLucideIconNaem control 0 0
8 ⭐ the naive reader on leg 1's anchor, without the code-only filter contrast 2 0

⭐⭐ Leg 8 exists because the dev's first ablation died on exactly this

The naive reader counts 2 at the base where the code-only reader counts 1 — the extra hit is prose. ⇒ their first ablation used that naive grep, failed its own guard, and was declared VOID rather than repaired quietly. Leg 8 keeps the contrast on the record so the next reader of this file does not re-derive it the hard way.

⚠️ ⛔ A reader that cannot tell code from prose does not measure the wrong amount — it measures a different population. That is the same defect I published seven times today.

⭐ Leg 5 is the card's central fence, measured

The card's most valuable move was refusing a tempting conclusion: 「Its absence from DECLARED_RECORD_READERS is correct, not an omission … This is not a gate defect to file against the gate; it is a surface outside the census by construction.」 ⇒ leg 5 proves the census gate and its ledger are byte-identical across this diff. The template was fixed, ⛔ never the instrument.

⚠️ Bundle Analysis is RED, and it is INHERITED — ⛔ nothing here for this PR to fix

establishment reading
red on this PR's own base Bundle Analysis at 279e48e8c1 = completed/failure — and likewise at 0e62707603 and 852437297b, i.e. every main commit where the paths filter matched
diff ∩ cause = NONE the three packages/** files are two test files and the CLI code generator. ⛔ None enters the console's eager closure.
⭐ live control on the filter itself PR objectui#9413, in flight beside this one, triggers no Bundle Analysis at all — it touches no packages/**. ⇒ the paths filter discriminates, so 「it ran here」 is a reading.

⇒ Same standing debt established on PR objectui#9388 this morning: the ui-components row at 1.9 KB headroom, owned by objectui#7848 and blocked on objectui#9204 (priority:p1, pm:dispatched, domain:ui — the lucide 1,767-entry map in that exact chunk). ⛔ Both greening edits are forbidden by the gate and by this seat's limits. ⛔ Nothing ported, raised or nagged. Said once, here.

⭐ And the coincidence is worth one line: this PR removes a lucide resolver, while the chunk that is over budget is over budget because of lucide — ⛔ but they are different artefacts (a generated app's template vs the console's own bundle), and ⛔ this diff does not move the console's closure.

Next: flip (⚠️ adds Governed Surface Queue Guard, 11/11 this shift) → re-wait → enqueue → confirm added_to_merge_queue → re-count all eight on the real parent.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 13, 2026 17:05
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit fb4ec65 Sep 13, 2026
37 of 38 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-7472-cli-lucide-resolver-seam branch September 13, 2026 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants