Repository navigation
fix(cli): route the generated app layout's icon lookup through the platform seam - #9415
Conversation
…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
|
| 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) | 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
Reviewed — ⭐⭐ the partial-match fence is CLEARED, affirmatively. ⛔ No reversal to
|
Probe ARMED — 8 legs, ⛔ posted BEFORE the flip.
|
| # | 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.
⭐ 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 (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
Fixes #7472
packages/cli/src/utils/app-generator.tsemitted 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:The generated
src/Layout.tsxnow importsLazyIconandisLucideIconNamefrom@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:843ata472b07. Base here isorigin/mainat9ba7e9c3a2: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 whoseiconNameMaphalf 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:getLazyIcon,isLucideIconName,LazyIcon,toKebabIconName(packages/components/src/index.ts:42).@object-ui/componentsis already inPLATFORM_RUNTIME_PACKAGES, so the generated manifest declares it.Setbuilt fromlucide-react's owniconNamesexport. A caller that callsLazyIcon/isLucideIconNamenever needs a name map. AniconNameMap-shaped single copy would only be needed by a consumer doing its own resolution, which is exactly what this change removes.182 authored/declared names reaching 7 record-reading resolversto184 ... reaching 1 record-reading resolver. objectui#5935's consolidation landed.⇒ ⛔ No reversal to
pm:on-holdis warranted.⛔ What this deliberately does not touch
scripts/check-lucide-icon-record-names.mjsand itsDECLARED_RECORD_READERSare 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.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 acceptstoKebabIconName(name), and converting canonical PascalCase is exactly what that tokeniser is for.Measured against
lucide-react@1.35.0, populations named in words:lucide-reactnamespace export keys (the old vocabulary)*Iconsuffix aliases (HouseIcon)Lucide*prefix aliases (LucideHouse)Building2)So the narrowing is real but narrow in kind: it is lucide's alias spellings, which were never
iconskeys and therefore resolved inside a generated app and nowhere else in the platform. Anapp.jsonusing one should switch to the canonical spelling —House, orbuilding-2. The gain direction is thatkebab-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.
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 aniconat every menu level — until now the only authored name any test put through the resolver waslogo.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/clideclares@object-ui/componentsas a runtime dependency for a reason no scanner can see, and that reason is carried by aDECLARED_WITHOUT_IMPORTrow inscripts/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 forapp-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.eslint was run on the four changed source files with
--no-inline-config --format json: 4 files linted, 0 errors, 1 warning. The repo-wideeslint .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 HEADafter 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):
B — drop
isLucideIconNamefrom 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:Triage assigned
domain:devxbut 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 readdomain:devxas 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 adomain:label.Held as draft pending review.
🤖 Generated with Claude Code
https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
Generated by Claude Code