refactor(components,app-shell,plugin-detail,plugin-list,plugin-view): one lucide icon-name seam, seven resolvers down to one - #7491

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-5935-one-icon-resolver-seam
Sep 3, 2026
Merged

refactor(components,app-shell,plugin-detail,plugin-list,plugin-view): one lucide icon-name seam, seven resolvers down to one#7491
os-project-manager merged 2 commits into
mainfrom
claude/issue-5935-one-icon-resolver-seam

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#5935

Seven modules resolved authored lucide icon names, each with its own copy of the logic. There is now one, resolveIcon, and the other six call it.

Both rulings on the card execute here as written:

  • 2026-08-31 (comment 5472612351, 维护者, verbatim 「同意」, option B) — one tokeniser, one iconNameMap, one seam; and point 4, 「本裁定后新容器 ⛔ 不得再自带解析器,一律走 seam」.
  • 2026-09-03 (comment 5523286738, maintainer, verbatim 「同意你的建议」, option C) — the seam does name -> component and nothing else. No onUnresolvable parameter. Each call site keeps its own fallback.

The census, re-measured before starting

Bound declared first, because this card has been handed a wrong number four times: all 4,101 git-tracked .ts/.tsx files, whole repo, no root subset — wider than the gate's own SCAN_ROOTS (3,945 of them). Re-run of the gate's own AST discovery over that wider set.

readingruled figure (at c1fe272ad)measured at my base e307c9896drift
record-reading resolvers77none
Home -> House alias4 of 74 of 7none
dynamic-surface readers44none
RECORD_READING_TYPES14 keys, 9 non-action14 keys, 9 non-actionnone
tokenisersT1 on 5, T2 on 1, T3 on 1T1 on 5, T2 on 1, T3 on 1none

Zero drift. The only difference from the ruling's measurement is the scanned-source count (3,945 vs 3,943 — two files added to the tree since).

What changed at each of the seven sites

sitehadnow
renderers/action/resolve-icon.tssplit('-') + aliasthe seam: split(/[-_\s]+/) + alias
renderers/basic/icon.tsxown toPascalCase + own map + own record indexcalls the seam; SquareDashed placeholder + warning byte-identical
app-shellpreviews/ActionPreview.tsxsplit(/[-_\s]+/) + inline ternarycalls the seam; keeps its 3-character name chip
plugin-detail/RelatedList.tsxsplit(/[-_\s]/), no aliasresolveIcon(name) ?? Inbox
plugin-list/ListView.tsxinline split('-'), no aliasresolveIcon(iconName) ?? Inbox
plugin-list/components/TabBar.tsxsplit('-'), no aliascalls the seam; keeps null
plugin-view/ViewSwitcher.tsxown toPascalCase + own mapcalls the seam; keeps null for views and its own `

resolveIcon is exported from @object-ui/components, the one package all five consumers already depend on — no new dependency edge.

The tokeniser was not a choice made in this PR

split(/[-_\s]+/) with the rename universal, established by the pre-dispatch enumeration the 2026-08-31 ruling required (comment 5522254814): regression set empty against the authored population, against an every-name-by-every-surface cross-product, and against a bound-free differential over 8,298 spellings derived from all 1,767 live record keys. split('-') regresses 4,748 name-surface pairs in that last reading and is not adoptable.

Widening only, and structurally so: no record key contains _, whitespace or - (0 of 1,767, with a firing control), so whenever the narrow tokeniser produced a live key the wide one produces the same key. The old resolving sets are strict subsets. Sixteen name-surface pairs start resolving; none stops. Stated in words in the changeset.

⭐ The load-bearing claim: zero user-visible change on all four unresolvable behaviours

Each of the four is pinned, and each pin is proven non-vacuous two ways — a control in the same run that asserts the opposite state, and a red-then-green ablation on the committed baseline 675571f8f.

behavioursurfacespinned in
nullseam, TabBar, ViewSwitcher (+ its own `
SquareDashed + warnbasic/icon.tsxthe existing #5631 pin, untouched, 13/13 green
Inbox glyphRelatedList (header and toolbar), ListView empty stateRelatedList.iconSeam-5935.test.tsx, icon-seam-fallbacks-5935.test.tsx
3-character name chipActionPreviewActionPreview.iconSeam-5935.test.tsx

The ablation, on the committed baseline, restore proven BY STATE (git diff HEAD empty and blob equality against the HEAD blob; trap ... EXIT INT TERM with absolute paths; restore pinned to git checkout HEAD -- path, never the bare form that reads back from the index). Each mutation was proven on disk in both directions — removed text gone, injected text present — before its run was read:

mutationrows that fired
seam tokeniser back to split('-')8 across all 5 new suites
the Home -> House entry disabled2, incl. the #5631 pin's own rename row
RelatedList: ?? Inbox dropped4
ListView: ?? Inbox dropped2
ActionPreview: chip emptied1
icon.tsx: placeholder glyph swapped out1, the #5631 placeholder row

⚠️The trap named on this card, caught in the act. Dropping RelatedList's ?? Inbox also turned its CONTROL row red — the component threw and drew nothing, exactly the "a test that passes because a component failed to render" failure this card warned about. That is why every fallback describe opens with a row proving the surface rendered and asserts its fallback positively, rather than only asserting an absence.

Two earlier ablation attempts were refused by the harness rather than reported, and are named so the numbers above are readable: injecting SquareDashedNOPE left the removed text present as a substring (both-directions probe unsatisfiable), and injecting the empty string made the injected-text probe degenerate. Both were re-run with clean anchors.

The gate, simplified in the same PR

scripts/check-lucide-icon-record-names.mjs: DECLARED_RECORD_READERS goes 7 to 1, and its normalisation stops being a widest-common approximation. Its verdict line, before and after:

before: OK lucide icon names: 182 authored/declared names reaching 7 record-reading resolvers are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).
after: OK lucide icon names: 182 authored/declared names reaching 1 record-reading resolver are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).

Every part-2 counter is identical across the two runs (148 judged, 22 descendant-judged, 239 declined, 34 anchored) — the census moved, what is judged did not. The under-reporting disclosed at #5932 is closed, not merely bounded: with one resolver there is nothing left to approximate.

Ruling point 4 ships as enforcement within a stated bound. Discovery is re-run from source every run and disagreement fails in both directions. ⚠️ Corrected here after the contract review measured the earlier wording as an over-claim: the predicate holds one syntactic shape — a named icons import from lucide-react (a rename counts) plus an element access on that binding, seen through parens, as casts and !, with any argument expression. Measured red on that shape; measured green, i.e. not caught, on a namespace import, a binding re-bound through another const, property access, destructuring, require()/dynamic import(), and anything inside a string or template literal. #7472 is the known outside-predicate resolver and is outside three ways at once. So a new container that hand-rolls a lookup in the conforming shape reds the commit that adds it; one written another way does not, and the ruling still binds it by review. The bound is now stated at DECLARED_RECORD_READERS itself. ⛔ The discovery predicate is not widened — that is a separate design question.

The two-copies pin is behavioural, not textual. The gate's .mjs normalisation and the resolver's TypeScript one are held together by importing describeIconLookup and asserting toRecordKey(s) equals describeIconLookup(s).key over a corpus derived from the runtime record — all 1,767 live keys re-spelled six ways, plus a per-character separator sweep — with a control that the corpus can in fact separate the two. The sweep is load-bearing: no record key contains a separator at all, so re-spellings alone cannot see a character-class widening. The source-text rows are kept; they catch a different thing. Measured need: widening only the gate to split(/[-_.\s]+/) left the suite green 40/40 before this, and reds 2 of 42 after it, with the gate script itself still exiting 0 under that mutation — which is exactly why the pin, not the gate, has to hold this.

Verification

  • New suites: 5 files, 40 tests. Union re-run at 089e35043 (the amendment commit): 18 files, 184 tests, all passing — the 5 new plus every pre-existing icon suite (ui:icon reads the SDUI identity key name as its lucide icon name, so any icon node that authors name renders nothing at all #5631's 13-test pin, finding(components): ui:button inlines its own copy of resolveIcon instead of importing it, so any future fix to the shared resolver silently misses it #5993's button dedupe pin, breadcrumb/command/context-menu/dropdown-menu item icons, the gate's own suite, now 42 tests, TabBar.i18nLabel, ListView.emptyStateFilteredCopy, ViewSwitcher, ActionPreview.locations). That is the 675571f8f reading of 182 plus the 2 rows the behavioural pin adds, and it is quoted from a run on the current head rather than carried forward.
  • pnpm exec turbo run type-check --filter='...@object-ui/components'66 tasks, all successful, over the 45-package DOWNSTREAM CONSUMER closure (prefix filter = consumers, the direction a contract change travels). The five new test files were confirmed present in each package's tsconfig.test.json program via --listFiles, so "type-check is green" actually covers them.
  • 13 derived gates green at 675571f8f: icon-record-names, control-bytes, vi-mock-specifiers, vi-mock-inherit, readme-exports, phantom-deps, self-import, entry-guard, side-effects-array, sdui-registration-pins, eager-closure, dist-completeness, docs-route-closure.
  • ESLint over the 15 changed files: 0 errors (3 introduced by the first draft were fixed: an unused LucideIcon import, an unused React import, and a react-hooks/static-components error on the ActionPreview call site, which now carries the same annotation the other five call sites do). 250 warnings, all pre-existing any / react-hooks noise on untouched lines of two 4,000-line files.
  • Changeset present, minor on the five packages whose source moved.

Scope

Clause-②: yes — and this is a deliberate re-derivation against the ruling's stated expectation of no. The 2026-09-03 ruling recorded «Clause-②: no expected (renderer-internal; re-declare from the diff)», which holds for the half it was about: no contract accept/reject behaviour moves, no schema, validator or refusal changes, nothing an author may write changes. But the diff widens a published surfaceresolveIcon is a new export from @object-ui/components's public entry, which the ruling's "renderer-internal" reading did not anticipate and which is unavoidable because the five other call sites live in five other packages. Declaring no here would have been an auditable false declaration, so it is yes with the reason attached.

#7472 stays out.packages/cli/src/utils/app-generator.ts:842 emits an eighth resolver inside a template literal, against lucide's named-export namespace, with zero normalisation. It is correctly absent from DECLARED_RECORD_READERS (it reads neither censused vocabulary), and the gate's discovery predicate is not widened to see it.

Nothing under content/docs/releases/ is touched. No gate threshold is raised or weakened.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… one lucide icon-name seam, seven resolvers down to one
Seven modules resolved authored icon names against lucide's runtime `icons`
record, each with its own copy: three tokenisers (`split('-')` on five,
`split(/[-_\s]/)` on one, `split(/[-_\s]+/)` on one) and the `Home` -> `House`
rename on four of seven. The same authored name therefore rendered on one
surface and not another. `resolveIcon` is now exported from
`@object-ui/components` and the other six call it.
The tokeniser is `split(/[-_\s]+/)` with the rename universal — measured, not
chosen: its regression set is empty against the authored population, against an
every-name-by-every-surface cross-product, and against a bound-free differential
over 8,298 spellings derived from all 1,767 live keys.
The seam answers `name -> component | null` and decides nothing else. Every call
site keeps its own fallback, at the call site: `ui:icon` its `SquareDashed`
placeholder and warning (objectui#5631, untouched), `RelatedList` and `ListView`
their `Inbox`, `ActionPreview` its name chip, the rest `null`.
`scripts/check-lucide-icon-record-names.mjs` is simplified in the same change:
its census goes from seven sites to one, and its normalisation stops being a
widest-common approximation of three disagreeing resolvers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.8 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-C0j--fB6.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.31KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.69KB21.03KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Seat bookkeeping + the Clause ② call. ⛔ NOT a contract review.

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Head 675571f8f.

⚖️ Open question RULED: A — the declaration stands as Clause ② YES.

The card recorded "Clause ②: no expected (renderer-internal; re-declare from the diff)". The dev re-derived it against the actual diff and declared yes, because resolveIcon is now on @object-ui/components' public entry. That is correct and it stands.

The criterion has two limbs — changes accept/reject behaviourorwidens the public surface. The second is met: a symbol that was package-internal is published, into a 45-package consumer closure. ⇒ needs:contract-review applied, PR stays draft, ⛔ not enqueued, and the project director seat lands it.

⭐ The reason I am not taking the softer reading: the card's own instruction was "re-declare from the diff", not "confirm the expectation". A no that any reviewer can falsify by reading one line of index.ts costs more than one extra gate pass — it devalues every other no in the ledger. The recorded expectation was a prediction, and predictions are what re-derivation exists to overturn.

⚠️ For the reviewing seat, the honest narrowing: no accept/reject behaviour moves. Nothing an author may write changes; no schema, validator or refusal is touched. The export exists solely so five sibling packages can stop duplicating a private function, and the diagnostics helper describeIconLookup was deliberately kept off the public entry to hold the widening at exactly one symbol — verified, see below.

Verified on the branch by content, ⛔ not from the report

probeorigin/mainbranch
onUnresolvable in the seam0 — the only hit is prose explaining it was deliberately not added
resolveIcon on the public entry01
describeIconLookup on the public entry0
⛔ FENCE — SquareDashed in basic/icon.tsx (#5631)33, unchanged
gate census entries82

⚠️ That last row is my probe's count, not the report's: the dev states 7 → 1 and my pattern matches one extra non-entry line on each side. The delta is identical (−6), and I am stating both rather than silently adopting either.

Call-site fallbacks intact and local, exactly as option C requires: RelatedList.tsx:222resolveIcon(name) ?? Inbox, ListView.tsx:3881resolveIcon(iconName) ?? Inbox, icon.tsx:121 through the seam into its own placeholder branch.

⭐ The trap this card named, caught in the act

Dropping RelatedList's ?? Inbox in the ablation turned its control row red too — the component threw and rendered nothing. That is precisely "a test that passes because a component failed to render", which the dispatch flagged because this repo produced exactly that failure earlier today. It is why every fallback block here opens with a render-proof row and asserts its fallback positively.

⭐ And two ablation attempts were refused by the harness rather than reported: injecting SquareDashedNOPE left the removed text present as a substring, so the both-directions probe was unsatisfiable; injecting the empty string made grep -cF '' match every line. Both were re-run with clean anchors and neither produced a reading that was used. A harness that declines to emit a number it cannot justify is worth more than the six ablation rows it did produce.

Two more worth the reviewer's eye

The census was re-derived over a WIDER bound than the gate's own. All 4,101 tracked .ts/.tsx files, against the gate's 3,945 SCAN_ROOTSzero drift from every ruled figure, with the detector control firing in the same run. A census is only as wide as its pathspec, and this one declared its bound and then exceeded the incumbent.

Ruling point 4 ships as mechanical enforcement, not a note. 「本裁定后新容器 ⛔ 不得再自带解析器」 is now held by the gate itself: it rediscovers record readers from source every run and fails in both directions, so a container that hand-rolls a lookup reds the commit that adds it.

Out-of-scope #7492 filed (the gate's data-table census entry names a module that reads no record); ⛔ deliberately not fixed in place despite the bounded-in-place conditions arguably holding — this card carries three scope fences and a drive-by edit to an unrelated table entry is what they exist to stop. #7472 confirmed still present, still correctly outside the census, and the discovery predicate ⛔ not widened to see it.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Clause-② in-seat contract review — PASS WITH REQUIRED AMENDMENTS. needs:contract-review stays on.

Commissioned by the domain:ui execution seat (PM session session_01EMrWaQw3XS5DxTHxp4yRyC) per the in-seat review route (维护者 2026-08-31 裁定: 交付后复核由派发席在席内完成,达档 PM 自审或派 fable 复核子任务皆可). This seat runs opus and is not at tier, so it commissioned an isolated reviewer rather than self-reviewing.

Independence: the reviewer was given only the card, the rulings and the PR — ⛔ not the dispatch order and not this seat's opinion. It discloses below that it encountered a seat bookkeeping comment carrying a Clause-② opinion while reading the PR thread, and did not use it. Recording that rather than hiding it.

Tier verification (维护者 2026-08-27 裁定 — a commissioned review is legitimate only if verified): every harness-stamped model field in the reviewer's transcript was read before adopting — 97 stamps, all claude-fable-5-1, zero other values, zero fallback/overload markers. CONTRACT_REVIEW_TIER is claude-fable-5 (objectstack/scripts/pm/dispatch-gates.mjs:8091); claude-fable-5-1 is at or above that floor — the same floor reading recorded on PR #7471. ⇒ Verified, so the verdict is adopted verbatim. ⛔ Nothing below is rewritten, abridged or polished by this seat; the only legitimate alternatives were verbatim adoption or wholesale voiding.


Contract review — objectui PR #7491 (head 675571f8f) against card #5935

Inputs read in full: the PR body; the complete diff (1,590 lines, all 16 files); issue #5935 body; comments 5472612351 (ruling 2026-08-31), 5523286738 (ruling 2026-09-03), 5522254814 (pre-dispatch enumeration), plus 5523075605 (director record) and 5522309927 (the blocker that produced ruling 2); all 32 check runs; issue #7472. Disclosure for independence: the PR's comment thread contains a seat bookkeeping comment (5523950630) that states a Clause-② opinion; I encountered it while reading the PR's comments and did not use it — every judgment below is derived from the diff and my own runs.

Own verification (detached worktree /home/user/objectui-review-7491 at 675571f8f, main checkout untouched, no stash): the 5 new suites + the #5631 pin + the gate suite = 7 files / 94 tests green; node scripts/check-lucide-icon-record-names.mjs exits 0 with "1 record-reading resolver"; the same on a local merge with current origin/main (7 commits past the branch point); an independent 70,974-spelling differential; two fallback ablations; one pin-hole probe; three enforcement-shape probes. Details under each item.

① Derived judgments — every accept-set / published-surface change the diff causes

Published surface

  1. @object-ui/components public entry (packages/components/src/index.ts) gains resolveIcon(name: string | undefined): LucideIcon | null. New symbol: package.jsonexports has only . and ./style.css, so renderers/action/resolve-icon.ts was unreachable by deep import before — the "new export" claim is correct. Right. Five sibling packages need it; duplicating it is the defect the card exists to end; precedent is hasDeclaredVisibilityGate on the same entry. Shape: return LucideIcon | null is the correct published shape under option C (caller must decide the fallback). One note, not a defect: the parameter rejects null although JSON-sourced icon: null is realistic; widening to string | null | undefined later is non-breaking.
  2. describeIconLookup is exported from the module but not from the package entry, and cannot be deep-imported. Right — the widening is held to exactly one symbol.
  3. Module-private resolveIcon/toPascalCase/iconNameMap removed from TabBar.tsx, ViewSwitcher.tsx, icon.tsx. Never exported from any package index (checked on origin/main). No surface change.
  4. Gate exports: DECLARED_RECORD_READERS 7 → 1; RECORD_READING_TYPES.icon.resolver and .view-switcher.resolver become "…resolve-icon.ts (via …)". The resolver string is consumed only in diagnostics (lines 667/693/784/846/960 of the .mjs), and six entries already used that form. Right.

Accept set (which authored names resolve), per surface — derived from the diff and measured

  1. Shared resolver (and every type routed through it: action:*, dropdown-menu, context-menu, breadcrumb, command, header-bar, button): tokeniser split('-')split(/[-_\s]+/). Widening only, right, and ruled (2026-08-31 「tokeniser 与 iconNameMap 全量收敛到一份」; the tokeniser itself fixed by the enumeration, "the implementer does not pick this").
  2. ui:icon (basic/icon.tsx): same widening; alias unchanged; placeholder + warn unchanged (ui:icon reads the SDUI identity key name as its lucide icon name, so any icon node that authors name renders nothing at all #5631 pin 13/13). One diagnostic-only consequence the PR does not mention: for a name that fails under both rules and contains _/space, the warning's lookup: "X" half now prints the wide-tokeniser Pascal form (FooBar where it printed Foo_bar). Console text only, not rendered. Acceptable.
  3. ActionPreview: was already split(/[-_\s]+/) + alias → no accept-set change, routing only (the test file says so honestly).
  4. RelatedList: split(/[-_\s]/) + no alias → wide + alias. Gains home/Home (and separator variants) → draws House where it drew Inbox. Behaviour change, user-visible, sanctioned: ruling 1 converges iconNameMap to one; enumeration §5 "the alias must be universal"; ruling 2 "Home→House applied universally"; card scope point 2 asks it be stated — it is, in words, in the changeset.
  5. ListView empty state: split('-') + no alias → wide + alias. Gains snake/space spellings and home. Same judgment.
  6. TabBar: split('-') + no alias → wide + alias. Gains snake/space and home (a glyph appears where none did). Same judgment.
  7. ViewSwitcher: split('-') + alias → wide + alias. Gains snake/space. Right.

The tokeniser attack (⚠ item). I transcribed all seven pre-PR resolvers verbatim and the new seam, and ran them over 70,974 spellings: every record key re-spelled as kebab/snake/spaced/Pascal/lower/UPPER, with leading, trailing, doubled and mixed separators of -, _, space, tab and NBSP, plus alias-specific hostiles (-home, home_, ho-me, home_house, …) and controls. Result: 212,163 name-surface pairs resolved before; 0 stop resolving; 0 resolve to a different component; 262,717 widenings; controls fired (building_2 disagreed across old sites exactly as the enumeration recorded; home likewise). Why no counter-example can exist: an empty token capitalises to '' and contributes nothing to the join, so split('-') and split(/-+/) are join-identical (this also makes .filter(Boolean) inert); and since no live key contains -, _ or whitespace (0 of 1,767, re-measured), any live key the narrow rule produced came from hyphen-only tokens, which the wide rule splits identically. The only alias is Home→House and Home is not a live key, so the alias can only add. The "widening only / strict subset" claim is correct.

Home → House at the three previously alias-free sites. It is a behaviour change, it is sanctioned by both rulings (see 8), it can change what a user sees (icon: "home" on a related-list header/toolbar button or a grid empty state: Inbox → House; on a tab: nothing → House), and it is stated as such in the changeset. Right.

The four unresolvable behaviours, per site — verified by reading the head files, not the description:

  • null: seam ?? null; TabBar.tsx:146/164, 212/225, 232/241 guard TabIcon && …; ViewSwitcher.tsxgetViewIcon returns resolveIcon(view.icon) when an icon is authored (authored-but-dead → no glyph, not the type default) and actions use resolveIcon(action.icon) || DEFAULT_VIEW_ACTION_ICONS[type] (:258). Old ViewSwitcher used || null; identical for component values. Preserved.
  • SquareDashed + warn: icon.tsx:121requested ? resolveIcon(requested) : undefined then if (!Icon); old value was undefined, new is null; nothing else reads Icon. Placeholder branch byte-identical. Preserved.
  • Inbox: RelatedList.tsx:222resolveIcon(name) ?? Inbox feeding both the header (:1322) and toolbar (:299, which still gates absent icons with action.icon ? … : null — a third behaviour in that file, pinned); ListView.tsx:3881resolveIcon(iconName) ?? Inbox. Preserved.
  • 3-character chip: ActionPreview.tsxif (Glyph) … else <span>{name.slice(0,3)}</span> unchanged. Preserved.

② Semver

  • Changeset: minor on components, app-shell, plugin-detail, plugin-list, plugin-view.
  • My level: @object-ui/componentsminor — a new public export, nothing removed. The four consumers: additive behaviour widening at their surfaces, no API change — minor is defensible, patch would be too; the distinction is moot because all five (and 35 others) are in the single fixed group in .changeset/config.json, so the family bumps minor on components' account regardless. Major: no — no name stops resolving (measured), no export removed; and this repo's policy (check-changeset-no-major.mjs) reserves major for the objectstack-synchronised bump, shipping objectui breaks as minor with the break spelled out — there is no break to spell.
  • Matches. The changeset body also carries the alias widening at the three sites in words, which the card demanded.

③ Boundary flags

(a) Clause-②: yes against the ruling's expected no. Correct re-derivation. The criterion (pm-dispatch SKILL.md line 547) is two-limbed — "改变契约接受/拒绝行为 扩大公开面". Limb 1 is not met (no schema, validator or refusal moves; the gate's judged/declined counters are unchanged). Limb 2 is met: a package-internal symbol is now on the public entry of a package with a 45-package downstream closure. The director's line was explicitly "re-declare from the diff"; a no here would be a falsifiable false declaration. Stands. One carrier problem, though: the checker that landing precondition ② runs (objectstack/scripts/pm/check-clause2-carriers.mjs --pair) reads the card side from claim-marked comments in the fixed spelling; I ran its parser — the PR body's **Clause-②: yes** — … line reads declared: yes, but no comment on #5935 carries a Clause-②: line at line start (the only occurrences are mid-line prose and inside the os-dev-report JSON), and the seat's claim comments do not match CLAIM_COMMENT_MARKER (/^\s*>?\s*Claim(?:ed)?\s*:/) either. The card limb therefore reads absent, which that tool classifies as adverse (exit 4) — "⛔ 不作干净". See amendment 3.

(b) #7472 held out. Defensible. Ruling 2 says so in terms ("⛔ Out of scope here; its absence from DECLARED_RECORD_READERS is correct, not an omission"); ruling point 4 governs new containers, and the generator's DynamicIcon predates it, is emitted into a different tree, and reads a third vocabulary (named exports, zero normalisation) — routing it through the seam would change what generated apps accept and needs its own ruling. Not widening the discovery predicate is consistent with #7472's own text. But the PR over-claims the enforcement. I added a probe file in three shapes and ran the gate: named icons import + direct element access → red ("UNDECLARED record-reading resolver", exit 1); import * as L …; L.icons[k] in executed code → green; named import re-bound through a second const and indexed → green. So "a container that hand-rolls a lookup turns the gate red on the commit that adds it" (.mjsDECLARED_RECORD_READERS comment, PR body) holds for one syntactic shape. The seam's docblock states the predicate accurately ("named-imports lucide's icons record and indexes it"); the gate comment does not. See amendment 2.

(c) The two-copies pin has a hole. The new gate-test row "normalises exactly as the ONE resolver does, read from its source" asserts (i) the resolver's text contains .split(/[-_\s]+/) and not .split('-'), (ii) the map entries extracted from the text equal {Home:'House'}, and (iii) the gate's own toRecordKey on six samples. It never executes the resolver. Probe: I widened only the gate's tokeniser to split(/[-_.\s]+/)all 40 gate tests stayed green, including that row. So the claim in the .mjs Normalisation comment, "a change to either side that is not made to the other fails loudly", is false in the gate-wider direction — which is exactly the under-reporting class the card closes (the gate would judge a name live that the resolver misses). The unit vitest project already imports TypeScript straight from packages/ (scripts/__tests__/known-schema-types-derivation-5115.test.tspackages/cli/src/utils/known-schema-types.js), so a behavioural pin is feasible without a build. See amendment 1.

Pins are not vacuous — confirmed

  • Each fallback describe opens with a CONTROL asserting a positive render (getByText('Line Items'), getByTestId('view-tab-open'), getAllByText('New Task')), plus a CONTROL that a resolvable name draws that glyph and not the fallback; every fallback row asserts svg.lucide-inbox / the chip text positively, not merely an absence.
  • Ablation reproduced in my worktree: drop ?? Inbox in RelatedList.tsx → 4 red including "CONTROL — the section renders, with its title" (the readers render <SectionIcon/> unconditionally, so a null throws; a component that fails to render cannot pass these); drop it in ListView.tsx → 2 red. Tree verified clean after each restore.
  • Seam suite discriminates (file-text non-null, not-a-real-icon null, edit null against the retired named export) and re-implements the narrow tokeniser in-test with a carried > 1000 guard against a silently empty loop. The ActionPreview spy row asserts toHaveBeenCalled() before reading mock.results (the blind-instrument trap avoided).

CI, by name

29 success: Changeset Bump Policy, Changeset Overwrite Report, Changeset Declaration, Changeset Fixed Group Check, Lint (5 min, real), Type Check (9 min, real), Test shard 1/4, 2/4, 3/4, 4/4 (10–14 min each, real; the vitest include globs cover the five new packages/**/*.test.tsx and scripts/**/*.test.ts, and I reran them locally), Build & E2E (log read: 14 Playwright passed / 23 skipped, 69 s explained by a pnpm cache hit — not a path-skip), Bundle Analysis (eager closure 3178.8 / 3191.4 KB — passes with 12.6 KB headroom; note only), README Export Check, Skill Example Check, Skill Guide Path Check, Skill Eval Token Check, Control Byte Scan, Shell Escape Residue Scan, Inert vi.mock Specifier Check, Pre-Install Import Graph Check, Governed Surface Queue Guard, Docs Route Eager Closure Check, Doc Component Type Check, Doc Snippet Type Check, Doc Fence Language Check, Build Docs, Internal Docs Link Check, Live E2E (informational), label. 3 skipped by design: Test (coverage) and Test (coverage shard) run on push only; dependabot.

Base behind main:base.sha c4f541df9; branch point e307c9896; origin/main is 7 commits past the branch point (3 past base). Overlap of those commits' files with the PR's 16: none; no new icons import from lucide-react anywhere on main since the branch point; and I ran the gate and all 7 suites on an actual local merge of head + origin/main — gate OK (1 resolver), 94/94. No reading is invalidated.

Verdict

PASS WITH REQUIRED AMENDMENTS

  1. Make the two-copies pin behavioural (scripts/__tests__/check-lucide-icon-record-names.test.ts). Import describeIconLookup (or resolveIcon) from packages/components/src/renderers/action/resolve-icon.ts and assert toRecordKey(s) === describeIconLookup(s).key for every record key under the enumeration's re-spellings (kebab/snake/spaced/Pascal/lower/doubled), plus a negative control; keep or drop the source-text rows as you like. Then either make the .mjs Normalisation comment's "either side … fails loudly" true or reword it. Proof it is needed: gate-only widening to /[-_.\s]+/ passed 40/40.
  2. State the enforcement bound at DECLARED_RECORD_READERS in scripts/check-lucide-icon-record-names.mjs. Replace "a new container that hand-rolls a lookup turns this gate red" with the predicate it actually holds (named icons import from 'lucide-react' + direct element access on that binding; namespace-import, re-bound and template-literal forms are outside it), and cite finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472 as the known outside-predicate resolver. Widening discovery is a separate design question and is not asked for here.
  3. Card-side Clause-② carrier (process, no code). Post on Consolidate the eight lucide record-reading icon resolvers into one seam — three tokenisers and a 5-of-8 alias map mean the same authored name renders on one surface and not another #5935 a claim-shaped comment (first line matching Claim:) carrying Clause-②: yes in the fixed spelling so check-clause2-carriers.mjs --pair 7491 reads both limbs consistent; today the card limb reads absent, and landing precondition ② cannot pass on it.

Not required, for the record: resolveIcon's parameter could accept null (non-breaking later); the not.toMatch(/\.split\('-'\)/) text row is fragile to a future comment; eager-closure headroom is 12.6 KB.


Seat disposition

Not landing.needs:contract-review stays on this PR until all three amendments are in and this seat verifies them by content. Amendments 1 and 2 are code and are being dispatched. Amendment 3 is a PM action on card #5935 and is this seat's own to discharge.

⭐ Amendment 3 is worth calling out: it is a machine-checkable landing precondition that this seat could not have caught itselfcheck-clause2-carriers.mjs cannot run in this session at all (exit 2, no GitHub token; MCP-only access), so the seat had fallen back to reading labels by hand, and a hand check cannot see a missing claim-line spelling. The reviewer ran the tool's parser directly and found the card limb reads absent. That is exactly the kind of finding an isolated reviewer at tier exists to produce.


Generated by Claude Code

…state the discovery bound (objectui#5935)
Both required amendments from the Clause-② contract review of PR #7491
(comment 5524970713). The seam, the call sites, the alias and the five new
suites are untouched.
Amendment 1 — the two-copies pin is now BEHAVIOURAL.
`scripts/__tests__/check-lucide-icon-record-names.test.ts` asserted only the
resolver's SOURCE TEXT, so it was blind to the gate moving on its own: the
resolver's text is then unchanged and every source-text row still passes. The
review measured it — widening ONLY `toRecordKey` to `split(/[-_.\s]+/)` left
the file green 40/40 — and that is the gate-wider direction, exactly the
under-reporting class this card exists to close.
The file now imports `describeIconLookup` from the resolver and asserts
`toRecordKey(s) === describeIconLookup(s).key` over a corpus derived from the
runtime record: every one of the 1,767 live keys re-spelled six ways, plus a
per-character separator sweep. The sweep is not decoration — no key of the
record contains a separator at all, so re-spellings alone cannot see a
character-class widening, which is why the measured hole survived them. A
second row is the control that the corpus can in fact separate the two,
modelling the drifted gate by substitution on the shipped `toRecordKey` rather
than transcribing the rule a third time. The source-text rows are kept; they
catch a different thing.
TypeScript is imported straight out of `packages/` the way
`known-schema-types-derivation-5115.test.ts` already does, so the pin costs no
build step, and by a RELATIVE path so `describeIconLookup` stays off the
package entry and the published widening stays at one symbol.
Amendment 2 — the enforcement bound is stated instead of over-claimed.
The `DECLARED_RECORD_READERS` comment said a new container that hand-rolls a
lookup turns the gate red. It holds for one syntactic shape. Measured against
this gate: named `icons` import (rename included) plus element access on that
binding, through parens/`as`/`!`, any argument expression — red. Namespace
import, a binding re-bound through another const, property access,
destructuring, `require()`/dynamic `import()`, and anything inside a string or
template literal — green. objectui#7472 is cited as the known outside-predicate
resolver and is outside three ways at once. The discovery predicate is ⛔ NOT
widened; that is a separate design question.
The Normalisation comment's "either side ... fails loudly" is likewise made
true rather than reworded away: it now names the behavioural row that carries
it and records that the source-text rows do not.
Verification: gate exits 0 at 1 record-reading resolver; the 7 relevant suites
96 green (94 before, +2 rows); ablation red/green with the restore proven by
state; `type-check:scripts`, `lint:root` (0 errors on changed files) and
`check-control-bytes` green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3180.9 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DpQmpEDV.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.31KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.69KB21.03KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Amendments 1 and 2 applied — 089e35043. ⛔ Still draft, needs:contract-review untouched.

Against the contract review's PASS WITH REQUIRED AMENDMENTS (comment 5524970713). Amendment 3 is the PM seat's and was not touched. The mechanism — tokeniser, seam, Home to House alias, seven call sites, five new suites — is byte-unchanged; the commit touches two files, both under scripts/.

Amendment 1 — the two-copies pin is behavioural

scripts/__tests__/check-lucide-icon-record-names.test.ts now imports describeIconLookup from the resolver by a relative path (so it stays off the package entry and the published widening stays at one symbol) and asserts toRecordKey(s) equals describeIconLookup(s).key over a corpus derived from the runtime record: all 1,767 live keys re-spelled six ways, plus a per-character separator sweep. The source-text rows are kept.

The sweep is load-bearing, and the review's own prescription would not have worked without it. The amendment text lists the re-spellings (kebab/snake/spaced/Pascal/lower/doubled) as the corpus. Those alone cannot catch the measured hole: no key of the record contains a separator at all (0 of 1,767, control — 95 contain a digit), so widening the class by . changes nothing about any of them and the pin would have stayed green under the very mutation it was commissioned to catch. Measured before writing the row: 0 of 10,602 re-spellings differ under /[-_.\s]+/; 19 corpus members do once hostile separators are in. So the corpus sweeps the character class, not the one character.

The ablation, reproducing the review's probe. Implementation committed first, so the restore leg has a real reference point; mutation and restore both proven by state, never by exit code.

legreading
pre-mutation, on diskshipped .split(/[-_\s]+/) present 1, mutated form present 0
post-mutation, on diskshipped 0, mutated 1; blob 4985efe76 vs HEAD e1600563f; git diff HEAD numstat 1 1
gate script under the mutationexit 0, still "1 record-reading resolver" — the drift is invisible to the gate's own run
pin suite under the mutation2 failed, 40 passed (42) — the two new rows, and only those
restoregit checkout HEAD -- ABSOLUTE_PATH under trap ... EXIT INT TERM; git diff HEAD empty and blob back to e1600563f
green leg on the restored tree42 passed (42)

The 40 pre-existing rows staying green under the mutation is the review's finding reproduced exactly. The .mjs Normalisation comment is made true rather than reworded away: it now names the behavioural row that carries "either side fails loudly" and records that the source-text rows do not.

Amendment 2 — the enforcement bound is stated

Replaced at DECLARED_RECORD_READERS, and the same over-claim corrected in the PR body. Every line of the new comment is measured against discoverResolvers, not reasoned about — 19 syntactic shapes through the real predicate:

Inside (red): named icons import; a renamed named import ({ icons as reg }); (icons as Record)[k]; and any argument expression, a template literal included — the predicate never looks at the argument.
Outside (green, not caught): namespace import; a binding re-bound through another const; property access (icons.House); destructuring; require(); dynamic import(); and anything inside a string or template literal.

⚠️One correction to the amendment text, which I would rather state than quietly obey. It says "namespace-import, re-bound and template-literal forms are outside it". Element access with a template-literal argument is inside — measured red. What is outside is code emitted inside a template literal, which is #7472's first of three reasons. Both readings are defensible from the phrase, so the new comment spells out which is which rather than repeating the ambiguity. #7472 is cited, and it is outside three ways at once: app-generator.ts emits, inside a template literal, import * as LucideIcons from 'lucide-react' re-bound to a lucideIcons const and then indexed. ⛔ The discovery predicate is not widened.

Verification, on the pushed head

  • 7 relevant suites: 7 files, 96 tests passed (the review's 94, plus the 2 rows). This file alone 42, was 40.
  • Icon union re-run at 089e35043: 18 files, 184 tests passed — 182 plus the same 2. Re-run because a push after review invalidates a sha-stamped union reading.
  • Every other suite naming the edited gate script — entry-guard wiring, scripts type-check parity, breadcrumb / command / context-menu item icons, icon-renderer-declared-default, ViewSwitcher: 7 files, 62 tests passed.
  • node scripts/check-lucide-icon-record-names.mjsexit 0, OK lucide icon names: 182 authored/declared names reaching 1 record-reading resolver are live icons keys.
  • pnpm type-check:scripts — VERDICT command-exit 0. pnpm lint:root (the scope that owns scripts/) — 0 errors, 32 warnings, 0 of them in either changed file; all 32 are pre-existing no-explicit-any in e2e specs, two unrelated scripts/__tests__ files and two vitest setup files.
  • node scripts/check-control-bytes.mjs — OK, 6,158 tracked text files.

Exit codes were captured by redirecting first (cmd > log 2>&1; EXIT=$?), never after a pipe, and each verdict is quoted from the line the tool printed.

⚠️ Byte note: the separator sweep needs NBSP and the ideographic space. Both are written as / escapes — a literal one is a character no reviewer can see and no grep can be aimed at. Verified: 0 invisible or control characters in either changed file.

The changeset was checked and not edited: it makes no claim about the discovery predicate, and its "the under-reporting is closed rather than merely bounded" is what amendment 1 makes true.

⛔ Not done, deliberately: PR not flipped ready, no auto-merge, needs:contract-review left on. The package: components / plugin / tests labels appeared from the labeler, not from me.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

✅ Amendments verified — needs:contract-review cleared, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Provenance: 「PASS ⇒ 同席剥标、ready、auto-merge」 (维护者 2026-08-31 裁定). The tier review returned PASS WITH REQUIRED AMENDMENTS (5524970713); all three are now in.

Verified by content at 089e35043, not from the report

  • Scope: parent is the reviewed head 675571f8f; 2 files, both under scripts/. The seam, the seven call sites, the alias and the five new suites are byte-unchanged — confirmed by filtering the commit's file list for anything outside scripts/ and getting an empty set.
  • Amendment 1 — the pin is behavioural and cannot silently go vacuous. It imports describeIconLookup from the resolver and asserts toRecordKey(s) === describeIconLookup(s).key over 8,000+ spellings. ⭐ More than was asked: it carries its own non-vacuity control — it models a dot-splitting gate and asserts the corpus can separate the two, naming arrow.right and building.2. So a future corpus edit that quietly drops the hostile separators turns the suite red rather than passing in silence. That is strictly better than the one-off ablation the amendment prescribed, because it keeps holding after today.
  • Amendment 2 — the bound is stated, the predicate is NOT widened. The DECLARED_RECORD_READERS comment now names what is inside (named icons import, rename included, plus element access through parens / as / !, any argument expression) and what is outside (namespace import, re-bound const, property access, destructuring, require()/dynamic import(), code inside a string or template literal), and cites finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472. The PR body's matching sentence is corrected too.
  • Amendment 3 was discharged by this seat on the card (5524981959).
  • CI: 32/32 at 089e35043 — 29 success, 3 skipped by design. Test (shard 1/4) 11:52:22 · 2/4 11:52:16 · 3/4 11:54:14 · 4/4 11:54:17 · Type Check 11:50:22 · Lint 11:47:10 · Bundle Analysis, README Export Check, Governed Surface Queue Guard each read individually.

⭐ The implementer corrected the review, and it is the most valuable thing in this round

Amendment 1's prescribed corpus would not have reproduced its own proof. The review said to assert over the enumeration's re-spellings (kebab/snake/spaced/Pascal/lower/doubled). Measured: no key of the record contains a separator at all (0 of 1,767; control "contains a digit" fires at 95), so 0 of 10,602 re-spellings change under /[-_.\s]+/. A pin built to the amendment's letter would have stayed green under the exact mutation it was commissioned to catch.

The implementer added a per-character separator sweep instead — that is what makes it fire — and said so plainly rather than reporting compliance. ⇒ I ratify the deviation. Amendment 1's requirement was a pin that is behavioural and proven to fire; the letter of its corpus was insufficient for its own purpose, and following the letter would have produced a pin that certified nothing. Measured after: 2 of 42 red under that mutation, with the gate script itself still exiting 0 — which is exactly why the pin, not the gate, has to hold this.

Three further corrections from the same round, recorded because two of them are mine:

  1. ⚠️My brief said "template-literal forms are outside" the predicate — wrong as written. An element access whose argument is a template literal (icons[`${a}${b}`]) is inside; the predicate reads the base identifier and never the argument. What is outside is code emitted inside a template literal. The new comment spells out which is which instead of repeating the ambiguity. The implementer also measured that a renamed named import is inside — which the review did not mention.
  2. finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472's own stated third reason is misdescribed (in that issue, not here): it says the base is "a property access", where the generated code actually re-binds a namespace import through a const. Both are outside the predicate so the conclusion stands; only the reason is wrong. Left alone as out of scope.
  3. The changeset needed no edit — it makes no claim about the discovery predicate, and its "under-reporting closed rather than merely bounded" line is precisely what amendment 1 makes true.

An accepted, named gap — not an oversight

The seam's own docblock (packages/components/src/renderers/action/resolve-icon.ts) still ends its enforcement paragraph with the same unqualified sentence amendment 2 corrected in the .mjs. It is left as-is: the tier reviewer examined that docblock and judged it accurate in context (it is preceded by an accurate predicate sentence), and my dispatch scoped the seam out so the mechanism under review stayed byte-stable. ⇒ A one-sentence follow-up available any time; recorded here so it is a chosen gap rather than one nobody noticed.

Carriers

needs:contract-review removed from this PR (remaining: package: components, plugin, tests). Card #5935 never carried it — its labels are pm:dispatched, domain:ui. ⚠️ That is the same dual-carrier gap the reviewer found on the declaration side, from the other direction: the gate label was hung on one carrier only. I also set #5935's assignee, which was empty while this PR was open — a half-state that invites a second agent onto claimed work.

⚠️ Both readings are manual: scripts/pm/check-clause2-carriers.mjs exits 2 in this session (no GitHub token; MCP-only access), and its own output says that is not a clearance. A hand label-check covers strictly less than the tool — naming the instrument, as on every landing this shift.


Generated by Claude Code

os-sam pushed a commit that referenced this pull request Sep 4, 2026
…ecord the third zero
objectui#5936 asked for the consumer of a component registration's `icon` meta
before extending the icon gate to it. Three first-party populations were
measured and none has one (objectui, objectstack, cloud), so the gate is not
extended — adjudicated 2026-09-04.
Records that where the next reader hits it (the gate's own "Deliberate
boundaries"), with the three readings, their controls, the adjudication, and
the caveat that three zeros are not a proof of absence.
Retires the membership half of the `ui:icon` local pin, which was kept at
objectui#5633 on the premise that the palette lives outside this repo. It
guarded 1 of the 45 registrations declaring an `icon` meta while the gate is
adjudicated not to cover the other 44, and its hand-copied tokeniser had
already gone stale against the seam objectui#5935 / PR #7491 created. The
coupling half stays: it needs no external vocabulary and cannot drift.
The published `ComponentMeta.icon` key is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

refactor(components,app-shell,plugin-detail,plugin-list,plugin-view): one lucide icon-name seam, seven resolvers down to one - #7491

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-5935-one-icon-resolver-seam
Sep 3, 2026
Merged

refactor(components,app-shell,plugin-detail,plugin-list,plugin-view): one lucide icon-name seam, seven resolvers down to one#7491
os-project-manager merged 2 commits into
mainfrom
claude/issue-5935-one-icon-resolver-seam

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#5935

Seven modules resolved authored lucide icon names, each with its own copy of the logic. There is now one, resolveIcon, and the other six call it.

Both rulings on the card execute here as written:

  • 2026-08-31 (comment 5472612351, 维护者, verbatim 「同意」, option B) — one tokeniser, one iconNameMap, one seam; and point 4, 「本裁定后新容器 ⛔ 不得再自带解析器,一律走 seam」.
  • 2026-09-03 (comment 5523286738, maintainer, verbatim 「同意你的建议」, option C) — the seam does name -> component and nothing else. No onUnresolvable parameter. Each call site keeps its own fallback.

The census, re-measured before starting

Bound declared first, because this card has been handed a wrong number four times: all 4,101 git-tracked .ts/.tsx files, whole repo, no root subset — wider than the gate's own SCAN_ROOTS (3,945 of them). Re-run of the gate's own AST discovery over that wider set.

readingruled figure (at c1fe272ad)measured at my base e307c9896drift
record-reading resolvers77none
Home -> House alias4 of 74 of 7none
dynamic-surface readers44none
RECORD_READING_TYPES14 keys, 9 non-action14 keys, 9 non-actionnone
tokenisersT1 on 5, T2 on 1, T3 on 1T1 on 5, T2 on 1, T3 on 1none

Zero drift. The only difference from the ruling's measurement is the scanned-source count (3,945 vs 3,943 — two files added to the tree since).

What changed at each of the seven sites

sitehadnow
renderers/action/resolve-icon.tssplit('-') + aliasthe seam: split(/[-_\s]+/) + alias
renderers/basic/icon.tsxown toPascalCase + own map + own record indexcalls the seam; SquareDashed placeholder + warning byte-identical
app-shellpreviews/ActionPreview.tsxsplit(/[-_\s]+/) + inline ternarycalls the seam; keeps its 3-character name chip
plugin-detail/RelatedList.tsxsplit(/[-_\s]/), no aliasresolveIcon(name) ?? Inbox
plugin-list/ListView.tsxinline split('-'), no aliasresolveIcon(iconName) ?? Inbox
plugin-list/components/TabBar.tsxsplit('-'), no aliascalls the seam; keeps null
plugin-view/ViewSwitcher.tsxown toPascalCase + own mapcalls the seam; keeps null for views and its own `

resolveIcon is exported from @object-ui/components, the one package all five consumers already depend on — no new dependency edge.

The tokeniser was not a choice made in this PR

split(/[-_\s]+/) with the rename universal, established by the pre-dispatch enumeration the 2026-08-31 ruling required (comment 5522254814): regression set empty against the authored population, against an every-name-by-every-surface cross-product, and against a bound-free differential over 8,298 spellings derived from all 1,767 live record keys. split('-') regresses 4,748 name-surface pairs in that last reading and is not adoptable.

Widening only, and structurally so: no record key contains _, whitespace or - (0 of 1,767, with a firing control), so whenever the narrow tokeniser produced a live key the wide one produces the same key. The old resolving sets are strict subsets. Sixteen name-surface pairs start resolving; none stops. Stated in words in the changeset.

⭐ The load-bearing claim: zero user-visible change on all four unresolvable behaviours

Each of the four is pinned, and each pin is proven non-vacuous two ways — a control in the same run that asserts the opposite state, and a red-then-green ablation on the committed baseline 675571f8f.

behavioursurfacespinned in
nullseam, TabBar, ViewSwitcher (+ its own `
SquareDashed + warnbasic/icon.tsxthe existing #5631 pin, untouched, 13/13 green
Inbox glyphRelatedList (header and toolbar), ListView empty stateRelatedList.iconSeam-5935.test.tsx, icon-seam-fallbacks-5935.test.tsx
3-character name chipActionPreviewActionPreview.iconSeam-5935.test.tsx

The ablation, on the committed baseline, restore proven BY STATE (git diff HEAD empty and blob equality against the HEAD blob; trap ... EXIT INT TERM with absolute paths; restore pinned to git checkout HEAD -- path, never the bare form that reads back from the index). Each mutation was proven on disk in both directions — removed text gone, injected text present — before its run was read:

mutationrows that fired
seam tokeniser back to split('-')8 across all 5 new suites
the Home -> House entry disabled2, incl. the #5631 pin's own rename row
RelatedList: ?? Inbox dropped4
ListView: ?? Inbox dropped2
ActionPreview: chip emptied1
icon.tsx: placeholder glyph swapped out1, the #5631 placeholder row

⚠️The trap named on this card, caught in the act. Dropping RelatedList's ?? Inbox also turned its CONTROL row red — the component threw and drew nothing, exactly the "a test that passes because a component failed to render" failure this card warned about. That is why every fallback describe opens with a row proving the surface rendered and asserts its fallback positively, rather than only asserting an absence.

Two earlier ablation attempts were refused by the harness rather than reported, and are named so the numbers above are readable: injecting SquareDashedNOPE left the removed text present as a substring (both-directions probe unsatisfiable), and injecting the empty string made the injected-text probe degenerate. Both were re-run with clean anchors.

The gate, simplified in the same PR

scripts/check-lucide-icon-record-names.mjs: DECLARED_RECORD_READERS goes 7 to 1, and its normalisation stops being a widest-common approximation. Its verdict line, before and after:

before: OK lucide icon names: 182 authored/declared names reaching 7 record-reading resolvers are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).
after: OK lucide icon names: 182 authored/declared names reaching 1 record-reading resolver are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).

Every part-2 counter is identical across the two runs (148 judged, 22 descendant-judged, 239 declined, 34 anchored) — the census moved, what is judged did not. The under-reporting disclosed at #5932 is closed, not merely bounded: with one resolver there is nothing left to approximate.

Ruling point 4 ships as enforcement within a stated bound. Discovery is re-run from source every run and disagreement fails in both directions. ⚠️ Corrected here after the contract review measured the earlier wording as an over-claim: the predicate holds one syntactic shape — a named icons import from lucide-react (a rename counts) plus an element access on that binding, seen through parens, as casts and !, with any argument expression. Measured red on that shape; measured green, i.e. not caught, on a namespace import, a binding re-bound through another const, property access, destructuring, require()/dynamic import(), and anything inside a string or template literal. #7472 is the known outside-predicate resolver and is outside three ways at once. So a new container that hand-rolls a lookup in the conforming shape reds the commit that adds it; one written another way does not, and the ruling still binds it by review. The bound is now stated at DECLARED_RECORD_READERS itself. ⛔ The discovery predicate is not widened — that is a separate design question.

The two-copies pin is behavioural, not textual. The gate's .mjs normalisation and the resolver's TypeScript one are held together by importing describeIconLookup and asserting toRecordKey(s) equals describeIconLookup(s).key over a corpus derived from the runtime record — all 1,767 live keys re-spelled six ways, plus a per-character separator sweep — with a control that the corpus can in fact separate the two. The sweep is load-bearing: no record key contains a separator at all, so re-spellings alone cannot see a character-class widening. The source-text rows are kept; they catch a different thing. Measured need: widening only the gate to split(/[-_.\s]+/) left the suite green 40/40 before this, and reds 2 of 42 after it, with the gate script itself still exiting 0 under that mutation — which is exactly why the pin, not the gate, has to hold this.

Verification

  • New suites: 5 files, 40 tests. Union re-run at 089e35043 (the amendment commit): 18 files, 184 tests, all passing — the 5 new plus every pre-existing icon suite (ui:icon reads the SDUI identity key name as its lucide icon name, so any icon node that authors name renders nothing at all #5631's 13-test pin, finding(components): ui:button inlines its own copy of resolveIcon instead of importing it, so any future fix to the shared resolver silently misses it #5993's button dedupe pin, breadcrumb/command/context-menu/dropdown-menu item icons, the gate's own suite, now 42 tests, TabBar.i18nLabel, ListView.emptyStateFilteredCopy, ViewSwitcher, ActionPreview.locations). That is the 675571f8f reading of 182 plus the 2 rows the behavioural pin adds, and it is quoted from a run on the current head rather than carried forward.
  • pnpm exec turbo run type-check --filter='...@object-ui/components'66 tasks, all successful, over the 45-package DOWNSTREAM CONSUMER closure (prefix filter = consumers, the direction a contract change travels). The five new test files were confirmed present in each package's tsconfig.test.json program via --listFiles, so "type-check is green" actually covers them.
  • 13 derived gates green at 675571f8f: icon-record-names, control-bytes, vi-mock-specifiers, vi-mock-inherit, readme-exports, phantom-deps, self-import, entry-guard, side-effects-array, sdui-registration-pins, eager-closure, dist-completeness, docs-route-closure.
  • ESLint over the 15 changed files: 0 errors (3 introduced by the first draft were fixed: an unused LucideIcon import, an unused React import, and a react-hooks/static-components error on the ActionPreview call site, which now carries the same annotation the other five call sites do). 250 warnings, all pre-existing any / react-hooks noise on untouched lines of two 4,000-line files.
  • Changeset present, minor on the five packages whose source moved.

Scope

Clause-②: yes — and this is a deliberate re-derivation against the ruling's stated expectation of no. The 2026-09-03 ruling recorded «Clause-②: no expected (renderer-internal; re-declare from the diff)», which holds for the half it was about: no contract accept/reject behaviour moves, no schema, validator or refusal changes, nothing an author may write changes. But the diff widens a published surfaceresolveIcon is a new export from @object-ui/components's public entry, which the ruling's "renderer-internal" reading did not anticipate and which is unavoidable because the five other call sites live in five other packages. Declaring no here would have been an auditable false declaration, so it is yes with the reason attached.

#7472 stays out.packages/cli/src/utils/app-generator.ts:842 emits an eighth resolver inside a template literal, against lucide's named-export namespace, with zero normalisation. It is correctly absent from DECLARED_RECORD_READERS (it reads neither censused vocabulary), and the gate's discovery predicate is not widened to see it.

Nothing under content/docs/releases/ is touched. No gate threshold is raised or weakened.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… one lucide icon-name seam, seven resolvers down to one
Seven modules resolved authored icon names against lucide's runtime `icons`
record, each with its own copy: three tokenisers (`split('-')` on five,
`split(/[-_\s]/)` on one, `split(/[-_\s]+/)` on one) and the `Home` -> `House`
rename on four of seven. The same authored name therefore rendered on one
surface and not another. `resolveIcon` is now exported from
`@object-ui/components` and the other six call it.
The tokeniser is `split(/[-_\s]+/)` with the rename universal — measured, not
chosen: its regression set is empty against the authored population, against an
every-name-by-every-surface cross-product, and against a bound-free differential
over 8,298 spellings derived from all 1,767 live keys.
The seam answers `name -> component | null` and decides nothing else. Every call
site keeps its own fallback, at the call site: `ui:icon` its `SquareDashed`
placeholder and warning (objectui#5631, untouched), `RelatedList` and `ListView`
their `Inbox`, `ActionPreview` its name chip, the rest `null`.
`scripts/check-lucide-icon-record-names.mjs` is simplified in the same change:
its census goes from seven sites to one, and its normalisation stops being a
widest-common approximation of three disagreeing resolvers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.8 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-C0j--fB6.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.31KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.69KB21.03KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Seat bookkeeping + the Clause ② call. ⛔ NOT a contract review.

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Head 675571f8f.

⚖️ Open question RULED: A — the declaration stands as Clause ② YES.

The card recorded "Clause ②: no expected (renderer-internal; re-declare from the diff)". The dev re-derived it against the actual diff and declared yes, because resolveIcon is now on @object-ui/components' public entry. That is correct and it stands.

The criterion has two limbs — changes accept/reject behaviourorwidens the public surface. The second is met: a symbol that was package-internal is published, into a 45-package consumer closure. ⇒ needs:contract-review applied, PR stays draft, ⛔ not enqueued, and the project director seat lands it.

⭐ The reason I am not taking the softer reading: the card's own instruction was "re-declare from the diff", not "confirm the expectation". A no that any reviewer can falsify by reading one line of index.ts costs more than one extra gate pass — it devalues every other no in the ledger. The recorded expectation was a prediction, and predictions are what re-derivation exists to overturn.

⚠️ For the reviewing seat, the honest narrowing: no accept/reject behaviour moves. Nothing an author may write changes; no schema, validator or refusal is touched. The export exists solely so five sibling packages can stop duplicating a private function, and the diagnostics helper describeIconLookup was deliberately kept off the public entry to hold the widening at exactly one symbol — verified, see below.

Verified on the branch by content, ⛔ not from the report

probeorigin/mainbranch
onUnresolvable in the seam0 — the only hit is prose explaining it was deliberately not added
resolveIcon on the public entry01
describeIconLookup on the public entry0
⛔ FENCE — SquareDashed in basic/icon.tsx (#5631)33, unchanged
gate census entries82

⚠️ That last row is my probe's count, not the report's: the dev states 7 → 1 and my pattern matches one extra non-entry line on each side. The delta is identical (−6), and I am stating both rather than silently adopting either.

Call-site fallbacks intact and local, exactly as option C requires: RelatedList.tsx:222resolveIcon(name) ?? Inbox, ListView.tsx:3881resolveIcon(iconName) ?? Inbox, icon.tsx:121 through the seam into its own placeholder branch.

⭐ The trap this card named, caught in the act

Dropping RelatedList's ?? Inbox in the ablation turned its control row red too — the component threw and rendered nothing. That is precisely "a test that passes because a component failed to render", which the dispatch flagged because this repo produced exactly that failure earlier today. It is why every fallback block here opens with a render-proof row and asserts its fallback positively.

⭐ And two ablation attempts were refused by the harness rather than reported: injecting SquareDashedNOPE left the removed text present as a substring, so the both-directions probe was unsatisfiable; injecting the empty string made grep -cF '' match every line. Both were re-run with clean anchors and neither produced a reading that was used. A harness that declines to emit a number it cannot justify is worth more than the six ablation rows it did produce.

Two more worth the reviewer's eye

The census was re-derived over a WIDER bound than the gate's own. All 4,101 tracked .ts/.tsx files, against the gate's 3,945 SCAN_ROOTSzero drift from every ruled figure, with the detector control firing in the same run. A census is only as wide as its pathspec, and this one declared its bound and then exceeded the incumbent.

Ruling point 4 ships as mechanical enforcement, not a note. 「本裁定后新容器 ⛔ 不得再自带解析器」 is now held by the gate itself: it rediscovers record readers from source every run and fails in both directions, so a container that hand-rolls a lookup reds the commit that adds it.

Out-of-scope #7492 filed (the gate's data-table census entry names a module that reads no record); ⛔ deliberately not fixed in place despite the bounded-in-place conditions arguably holding — this card carries three scope fences and a drive-by edit to an unrelated table entry is what they exist to stop. #7472 confirmed still present, still correctly outside the census, and the discovery predicate ⛔ not widened to see it.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Clause-② in-seat contract review — PASS WITH REQUIRED AMENDMENTS. needs:contract-review stays on.

Commissioned by the domain:ui execution seat (PM session session_01EMrWaQw3XS5DxTHxp4yRyC) per the in-seat review route (维护者 2026-08-31 裁定: 交付后复核由派发席在席内完成,达档 PM 自审或派 fable 复核子任务皆可). This seat runs opus and is not at tier, so it commissioned an isolated reviewer rather than self-reviewing.

Independence: the reviewer was given only the card, the rulings and the PR — ⛔ not the dispatch order and not this seat's opinion. It discloses below that it encountered a seat bookkeeping comment carrying a Clause-② opinion while reading the PR thread, and did not use it. Recording that rather than hiding it.

Tier verification (维护者 2026-08-27 裁定 — a commissioned review is legitimate only if verified): every harness-stamped model field in the reviewer's transcript was read before adopting — 97 stamps, all claude-fable-5-1, zero other values, zero fallback/overload markers. CONTRACT_REVIEW_TIER is claude-fable-5 (objectstack/scripts/pm/dispatch-gates.mjs:8091); claude-fable-5-1 is at or above that floor — the same floor reading recorded on PR #7471. ⇒ Verified, so the verdict is adopted verbatim. ⛔ Nothing below is rewritten, abridged or polished by this seat; the only legitimate alternatives were verbatim adoption or wholesale voiding.


Contract review — objectui PR #7491 (head 675571f8f) against card #5935

Inputs read in full: the PR body; the complete diff (1,590 lines, all 16 files); issue #5935 body; comments 5472612351 (ruling 2026-08-31), 5523286738 (ruling 2026-09-03), 5522254814 (pre-dispatch enumeration), plus 5523075605 (director record) and 5522309927 (the blocker that produced ruling 2); all 32 check runs; issue #7472. Disclosure for independence: the PR's comment thread contains a seat bookkeeping comment (5523950630) that states a Clause-② opinion; I encountered it while reading the PR's comments and did not use it — every judgment below is derived from the diff and my own runs.

Own verification (detached worktree /home/user/objectui-review-7491 at 675571f8f, main checkout untouched, no stash): the 5 new suites + the #5631 pin + the gate suite = 7 files / 94 tests green; node scripts/check-lucide-icon-record-names.mjs exits 0 with "1 record-reading resolver"; the same on a local merge with current origin/main (7 commits past the branch point); an independent 70,974-spelling differential; two fallback ablations; one pin-hole probe; three enforcement-shape probes. Details under each item.

① Derived judgments — every accept-set / published-surface change the diff causes

Published surface

  1. @object-ui/components public entry (packages/components/src/index.ts) gains resolveIcon(name: string | undefined): LucideIcon | null. New symbol: package.jsonexports has only . and ./style.css, so renderers/action/resolve-icon.ts was unreachable by deep import before — the "new export" claim is correct. Right. Five sibling packages need it; duplicating it is the defect the card exists to end; precedent is hasDeclaredVisibilityGate on the same entry. Shape: return LucideIcon | null is the correct published shape under option C (caller must decide the fallback). One note, not a defect: the parameter rejects null although JSON-sourced icon: null is realistic; widening to string | null | undefined later is non-breaking.
  2. describeIconLookup is exported from the module but not from the package entry, and cannot be deep-imported. Right — the widening is held to exactly one symbol.
  3. Module-private resolveIcon/toPascalCase/iconNameMap removed from TabBar.tsx, ViewSwitcher.tsx, icon.tsx. Never exported from any package index (checked on origin/main). No surface change.
  4. Gate exports: DECLARED_RECORD_READERS 7 → 1; RECORD_READING_TYPES.icon.resolver and .view-switcher.resolver become "…resolve-icon.ts (via …)". The resolver string is consumed only in diagnostics (lines 667/693/784/846/960 of the .mjs), and six entries already used that form. Right.

Accept set (which authored names resolve), per surface — derived from the diff and measured

  1. Shared resolver (and every type routed through it: action:*, dropdown-menu, context-menu, breadcrumb, command, header-bar, button): tokeniser split('-')split(/[-_\s]+/). Widening only, right, and ruled (2026-08-31 「tokeniser 与 iconNameMap 全量收敛到一份」; the tokeniser itself fixed by the enumeration, "the implementer does not pick this").
  2. ui:icon (basic/icon.tsx): same widening; alias unchanged; placeholder + warn unchanged (ui:icon reads the SDUI identity key name as its lucide icon name, so any icon node that authors name renders nothing at all #5631 pin 13/13). One diagnostic-only consequence the PR does not mention: for a name that fails under both rules and contains _/space, the warning's lookup: "X" half now prints the wide-tokeniser Pascal form (FooBar where it printed Foo_bar). Console text only, not rendered. Acceptable.
  3. ActionPreview: was already split(/[-_\s]+/) + alias → no accept-set change, routing only (the test file says so honestly).
  4. RelatedList: split(/[-_\s]/) + no alias → wide + alias. Gains home/Home (and separator variants) → draws House where it drew Inbox. Behaviour change, user-visible, sanctioned: ruling 1 converges iconNameMap to one; enumeration §5 "the alias must be universal"; ruling 2 "Home→House applied universally"; card scope point 2 asks it be stated — it is, in words, in the changeset.
  5. ListView empty state: split('-') + no alias → wide + alias. Gains snake/space spellings and home. Same judgment.
  6. TabBar: split('-') + no alias → wide + alias. Gains snake/space and home (a glyph appears where none did). Same judgment.
  7. ViewSwitcher: split('-') + alias → wide + alias. Gains snake/space. Right.

The tokeniser attack (⚠ item). I transcribed all seven pre-PR resolvers verbatim and the new seam, and ran them over 70,974 spellings: every record key re-spelled as kebab/snake/spaced/Pascal/lower/UPPER, with leading, trailing, doubled and mixed separators of -, _, space, tab and NBSP, plus alias-specific hostiles (-home, home_, ho-me, home_house, …) and controls. Result: 212,163 name-surface pairs resolved before; 0 stop resolving; 0 resolve to a different component; 262,717 widenings; controls fired (building_2 disagreed across old sites exactly as the enumeration recorded; home likewise). Why no counter-example can exist: an empty token capitalises to '' and contributes nothing to the join, so split('-') and split(/-+/) are join-identical (this also makes .filter(Boolean) inert); and since no live key contains -, _ or whitespace (0 of 1,767, re-measured), any live key the narrow rule produced came from hyphen-only tokens, which the wide rule splits identically. The only alias is Home→House and Home is not a live key, so the alias can only add. The "widening only / strict subset" claim is correct.

Home → House at the three previously alias-free sites. It is a behaviour change, it is sanctioned by both rulings (see 8), it can change what a user sees (icon: "home" on a related-list header/toolbar button or a grid empty state: Inbox → House; on a tab: nothing → House), and it is stated as such in the changeset. Right.

The four unresolvable behaviours, per site — verified by reading the head files, not the description:

  • null: seam ?? null; TabBar.tsx:146/164, 212/225, 232/241 guard TabIcon && …; ViewSwitcher.tsxgetViewIcon returns resolveIcon(view.icon) when an icon is authored (authored-but-dead → no glyph, not the type default) and actions use resolveIcon(action.icon) || DEFAULT_VIEW_ACTION_ICONS[type] (:258). Old ViewSwitcher used || null; identical for component values. Preserved.
  • SquareDashed + warn: icon.tsx:121requested ? resolveIcon(requested) : undefined then if (!Icon); old value was undefined, new is null; nothing else reads Icon. Placeholder branch byte-identical. Preserved.
  • Inbox: RelatedList.tsx:222resolveIcon(name) ?? Inbox feeding both the header (:1322) and toolbar (:299, which still gates absent icons with action.icon ? … : null — a third behaviour in that file, pinned); ListView.tsx:3881resolveIcon(iconName) ?? Inbox. Preserved.
  • 3-character chip: ActionPreview.tsxif (Glyph) … else <span>{name.slice(0,3)}</span> unchanged. Preserved.

② Semver

  • Changeset: minor on components, app-shell, plugin-detail, plugin-list, plugin-view.
  • My level: @object-ui/componentsminor — a new public export, nothing removed. The four consumers: additive behaviour widening at their surfaces, no API change — minor is defensible, patch would be too; the distinction is moot because all five (and 35 others) are in the single fixed group in .changeset/config.json, so the family bumps minor on components' account regardless. Major: no — no name stops resolving (measured), no export removed; and this repo's policy (check-changeset-no-major.mjs) reserves major for the objectstack-synchronised bump, shipping objectui breaks as minor with the break spelled out — there is no break to spell.
  • Matches. The changeset body also carries the alias widening at the three sites in words, which the card demanded.

③ Boundary flags

(a) Clause-②: yes against the ruling's expected no. Correct re-derivation. The criterion (pm-dispatch SKILL.md line 547) is two-limbed — "改变契约接受/拒绝行为 扩大公开面". Limb 1 is not met (no schema, validator or refusal moves; the gate's judged/declined counters are unchanged). Limb 2 is met: a package-internal symbol is now on the public entry of a package with a 45-package downstream closure. The director's line was explicitly "re-declare from the diff"; a no here would be a falsifiable false declaration. Stands. One carrier problem, though: the checker that landing precondition ② runs (objectstack/scripts/pm/check-clause2-carriers.mjs --pair) reads the card side from claim-marked comments in the fixed spelling; I ran its parser — the PR body's **Clause-②: yes** — … line reads declared: yes, but no comment on #5935 carries a Clause-②: line at line start (the only occurrences are mid-line prose and inside the os-dev-report JSON), and the seat's claim comments do not match CLAIM_COMMENT_MARKER (/^\s*>?\s*Claim(?:ed)?\s*:/) either. The card limb therefore reads absent, which that tool classifies as adverse (exit 4) — "⛔ 不作干净". See amendment 3.

(b) #7472 held out. Defensible. Ruling 2 says so in terms ("⛔ Out of scope here; its absence from DECLARED_RECORD_READERS is correct, not an omission"); ruling point 4 governs new containers, and the generator's DynamicIcon predates it, is emitted into a different tree, and reads a third vocabulary (named exports, zero normalisation) — routing it through the seam would change what generated apps accept and needs its own ruling. Not widening the discovery predicate is consistent with #7472's own text. But the PR over-claims the enforcement. I added a probe file in three shapes and ran the gate: named icons import + direct element access → red ("UNDECLARED record-reading resolver", exit 1); import * as L …; L.icons[k] in executed code → green; named import re-bound through a second const and indexed → green. So "a container that hand-rolls a lookup turns the gate red on the commit that adds it" (.mjsDECLARED_RECORD_READERS comment, PR body) holds for one syntactic shape. The seam's docblock states the predicate accurately ("named-imports lucide's icons record and indexes it"); the gate comment does not. See amendment 2.

(c) The two-copies pin has a hole. The new gate-test row "normalises exactly as the ONE resolver does, read from its source" asserts (i) the resolver's text contains .split(/[-_\s]+/) and not .split('-'), (ii) the map entries extracted from the text equal {Home:'House'}, and (iii) the gate's own toRecordKey on six samples. It never executes the resolver. Probe: I widened only the gate's tokeniser to split(/[-_.\s]+/)all 40 gate tests stayed green, including that row. So the claim in the .mjs Normalisation comment, "a change to either side that is not made to the other fails loudly", is false in the gate-wider direction — which is exactly the under-reporting class the card closes (the gate would judge a name live that the resolver misses). The unit vitest project already imports TypeScript straight from packages/ (scripts/__tests__/known-schema-types-derivation-5115.test.tspackages/cli/src/utils/known-schema-types.js), so a behavioural pin is feasible without a build. See amendment 1.

Pins are not vacuous — confirmed

  • Each fallback describe opens with a CONTROL asserting a positive render (getByText('Line Items'), getByTestId('view-tab-open'), getAllByText('New Task')), plus a CONTROL that a resolvable name draws that glyph and not the fallback; every fallback row asserts svg.lucide-inbox / the chip text positively, not merely an absence.
  • Ablation reproduced in my worktree: drop ?? Inbox in RelatedList.tsx → 4 red including "CONTROL — the section renders, with its title" (the readers render <SectionIcon/> unconditionally, so a null throws; a component that fails to render cannot pass these); drop it in ListView.tsx → 2 red. Tree verified clean after each restore.
  • Seam suite discriminates (file-text non-null, not-a-real-icon null, edit null against the retired named export) and re-implements the narrow tokeniser in-test with a carried > 1000 guard against a silently empty loop. The ActionPreview spy row asserts toHaveBeenCalled() before reading mock.results (the blind-instrument trap avoided).

CI, by name

29 success: Changeset Bump Policy, Changeset Overwrite Report, Changeset Declaration, Changeset Fixed Group Check, Lint (5 min, real), Type Check (9 min, real), Test shard 1/4, 2/4, 3/4, 4/4 (10–14 min each, real; the vitest include globs cover the five new packages/**/*.test.tsx and scripts/**/*.test.ts, and I reran them locally), Build & E2E (log read: 14 Playwright passed / 23 skipped, 69 s explained by a pnpm cache hit — not a path-skip), Bundle Analysis (eager closure 3178.8 / 3191.4 KB — passes with 12.6 KB headroom; note only), README Export Check, Skill Example Check, Skill Guide Path Check, Skill Eval Token Check, Control Byte Scan, Shell Escape Residue Scan, Inert vi.mock Specifier Check, Pre-Install Import Graph Check, Governed Surface Queue Guard, Docs Route Eager Closure Check, Doc Component Type Check, Doc Snippet Type Check, Doc Fence Language Check, Build Docs, Internal Docs Link Check, Live E2E (informational), label. 3 skipped by design: Test (coverage) and Test (coverage shard) run on push only; dependabot.

Base behind main:base.sha c4f541df9; branch point e307c9896; origin/main is 7 commits past the branch point (3 past base). Overlap of those commits' files with the PR's 16: none; no new icons import from lucide-react anywhere on main since the branch point; and I ran the gate and all 7 suites on an actual local merge of head + origin/main — gate OK (1 resolver), 94/94. No reading is invalidated.

Verdict

PASS WITH REQUIRED AMENDMENTS

  1. Make the two-copies pin behavioural (scripts/__tests__/check-lucide-icon-record-names.test.ts). Import describeIconLookup (or resolveIcon) from packages/components/src/renderers/action/resolve-icon.ts and assert toRecordKey(s) === describeIconLookup(s).key for every record key under the enumeration's re-spellings (kebab/snake/spaced/Pascal/lower/doubled), plus a negative control; keep or drop the source-text rows as you like. Then either make the .mjs Normalisation comment's "either side … fails loudly" true or reword it. Proof it is needed: gate-only widening to /[-_.\s]+/ passed 40/40.
  2. State the enforcement bound at DECLARED_RECORD_READERS in scripts/check-lucide-icon-record-names.mjs. Replace "a new container that hand-rolls a lookup turns this gate red" with the predicate it actually holds (named icons import from 'lucide-react' + direct element access on that binding; namespace-import, re-bound and template-literal forms are outside it), and cite finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472 as the known outside-predicate resolver. Widening discovery is a separate design question and is not asked for here.
  3. Card-side Clause-② carrier (process, no code). Post on Consolidate the eight lucide record-reading icon resolvers into one seam — three tokenisers and a 5-of-8 alias map mean the same authored name renders on one surface and not another #5935 a claim-shaped comment (first line matching Claim:) carrying Clause-②: yes in the fixed spelling so check-clause2-carriers.mjs --pair 7491 reads both limbs consistent; today the card limb reads absent, and landing precondition ② cannot pass on it.

Not required, for the record: resolveIcon's parameter could accept null (non-breaking later); the not.toMatch(/\.split\('-'\)/) text row is fragile to a future comment; eager-closure headroom is 12.6 KB.


Seat disposition

Not landing.needs:contract-review stays on this PR until all three amendments are in and this seat verifies them by content. Amendments 1 and 2 are code and are being dispatched. Amendment 3 is a PM action on card #5935 and is this seat's own to discharge.

⭐ Amendment 3 is worth calling out: it is a machine-checkable landing precondition that this seat could not have caught itselfcheck-clause2-carriers.mjs cannot run in this session at all (exit 2, no GitHub token; MCP-only access), so the seat had fallen back to reading labels by hand, and a hand check cannot see a missing claim-line spelling. The reviewer ran the tool's parser directly and found the card limb reads absent. That is exactly the kind of finding an isolated reviewer at tier exists to produce.


Generated by Claude Code

…state the discovery bound (objectui#5935)
Both required amendments from the Clause-② contract review of PR #7491
(comment 5524970713). The seam, the call sites, the alias and the five new
suites are untouched.
Amendment 1 — the two-copies pin is now BEHAVIOURAL.
`scripts/__tests__/check-lucide-icon-record-names.test.ts` asserted only the
resolver's SOURCE TEXT, so it was blind to the gate moving on its own: the
resolver's text is then unchanged and every source-text row still passes. The
review measured it — widening ONLY `toRecordKey` to `split(/[-_.\s]+/)` left
the file green 40/40 — and that is the gate-wider direction, exactly the
under-reporting class this card exists to close.
The file now imports `describeIconLookup` from the resolver and asserts
`toRecordKey(s) === describeIconLookup(s).key` over a corpus derived from the
runtime record: every one of the 1,767 live keys re-spelled six ways, plus a
per-character separator sweep. The sweep is not decoration — no key of the
record contains a separator at all, so re-spellings alone cannot see a
character-class widening, which is why the measured hole survived them. A
second row is the control that the corpus can in fact separate the two,
modelling the drifted gate by substitution on the shipped `toRecordKey` rather
than transcribing the rule a third time. The source-text rows are kept; they
catch a different thing.
TypeScript is imported straight out of `packages/` the way
`known-schema-types-derivation-5115.test.ts` already does, so the pin costs no
build step, and by a RELATIVE path so `describeIconLookup` stays off the
package entry and the published widening stays at one symbol.
Amendment 2 — the enforcement bound is stated instead of over-claimed.
The `DECLARED_RECORD_READERS` comment said a new container that hand-rolls a
lookup turns the gate red. It holds for one syntactic shape. Measured against
this gate: named `icons` import (rename included) plus element access on that
binding, through parens/`as`/`!`, any argument expression — red. Namespace
import, a binding re-bound through another const, property access,
destructuring, `require()`/dynamic `import()`, and anything inside a string or
template literal — green. objectui#7472 is cited as the known outside-predicate
resolver and is outside three ways at once. The discovery predicate is ⛔ NOT
widened; that is a separate design question.
The Normalisation comment's "either side ... fails loudly" is likewise made
true rather than reworded away: it now names the behavioural row that carries
it and records that the source-text rows do not.
Verification: gate exits 0 at 1 record-reading resolver; the 7 relevant suites
96 green (94 before, +2 rows); ablation red/green with the restore proven by
state; `type-check:scripts`, `lint:root` (0 errors on changed files) and
`check-control-bytes` green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3180.9 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DpQmpEDV.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.31KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.69KB21.03KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Amendments 1 and 2 applied — 089e35043. ⛔ Still draft, needs:contract-review untouched.

Against the contract review's PASS WITH REQUIRED AMENDMENTS (comment 5524970713). Amendment 3 is the PM seat's and was not touched. The mechanism — tokeniser, seam, Home to House alias, seven call sites, five new suites — is byte-unchanged; the commit touches two files, both under scripts/.

Amendment 1 — the two-copies pin is behavioural

scripts/__tests__/check-lucide-icon-record-names.test.ts now imports describeIconLookup from the resolver by a relative path (so it stays off the package entry and the published widening stays at one symbol) and asserts toRecordKey(s) equals describeIconLookup(s).key over a corpus derived from the runtime record: all 1,767 live keys re-spelled six ways, plus a per-character separator sweep. The source-text rows are kept.

The sweep is load-bearing, and the review's own prescription would not have worked without it. The amendment text lists the re-spellings (kebab/snake/spaced/Pascal/lower/doubled) as the corpus. Those alone cannot catch the measured hole: no key of the record contains a separator at all (0 of 1,767, control — 95 contain a digit), so widening the class by . changes nothing about any of them and the pin would have stayed green under the very mutation it was commissioned to catch. Measured before writing the row: 0 of 10,602 re-spellings differ under /[-_.\s]+/; 19 corpus members do once hostile separators are in. So the corpus sweeps the character class, not the one character.

The ablation, reproducing the review's probe. Implementation committed first, so the restore leg has a real reference point; mutation and restore both proven by state, never by exit code.

legreading
pre-mutation, on diskshipped .split(/[-_\s]+/) present 1, mutated form present 0
post-mutation, on diskshipped 0, mutated 1; blob 4985efe76 vs HEAD e1600563f; git diff HEAD numstat 1 1
gate script under the mutationexit 0, still "1 record-reading resolver" — the drift is invisible to the gate's own run
pin suite under the mutation2 failed, 40 passed (42) — the two new rows, and only those
restoregit checkout HEAD -- ABSOLUTE_PATH under trap ... EXIT INT TERM; git diff HEAD empty and blob back to e1600563f
green leg on the restored tree42 passed (42)

The 40 pre-existing rows staying green under the mutation is the review's finding reproduced exactly. The .mjs Normalisation comment is made true rather than reworded away: it now names the behavioural row that carries "either side fails loudly" and records that the source-text rows do not.

Amendment 2 — the enforcement bound is stated

Replaced at DECLARED_RECORD_READERS, and the same over-claim corrected in the PR body. Every line of the new comment is measured against discoverResolvers, not reasoned about — 19 syntactic shapes through the real predicate:

Inside (red): named icons import; a renamed named import ({ icons as reg }); (icons as Record)[k]; and any argument expression, a template literal included — the predicate never looks at the argument.
Outside (green, not caught): namespace import; a binding re-bound through another const; property access (icons.House); destructuring; require(); dynamic import(); and anything inside a string or template literal.

⚠️One correction to the amendment text, which I would rather state than quietly obey. It says "namespace-import, re-bound and template-literal forms are outside it". Element access with a template-literal argument is inside — measured red. What is outside is code emitted inside a template literal, which is #7472's first of three reasons. Both readings are defensible from the phrase, so the new comment spells out which is which rather than repeating the ambiguity. #7472 is cited, and it is outside three ways at once: app-generator.ts emits, inside a template literal, import * as LucideIcons from 'lucide-react' re-bound to a lucideIcons const and then indexed. ⛔ The discovery predicate is not widened.

Verification, on the pushed head

  • 7 relevant suites: 7 files, 96 tests passed (the review's 94, plus the 2 rows). This file alone 42, was 40.
  • Icon union re-run at 089e35043: 18 files, 184 tests passed — 182 plus the same 2. Re-run because a push after review invalidates a sha-stamped union reading.
  • Every other suite naming the edited gate script — entry-guard wiring, scripts type-check parity, breadcrumb / command / context-menu item icons, icon-renderer-declared-default, ViewSwitcher: 7 files, 62 tests passed.
  • node scripts/check-lucide-icon-record-names.mjsexit 0, OK lucide icon names: 182 authored/declared names reaching 1 record-reading resolver are live icons keys.
  • pnpm type-check:scripts — VERDICT command-exit 0. pnpm lint:root (the scope that owns scripts/) — 0 errors, 32 warnings, 0 of them in either changed file; all 32 are pre-existing no-explicit-any in e2e specs, two unrelated scripts/__tests__ files and two vitest setup files.
  • node scripts/check-control-bytes.mjs — OK, 6,158 tracked text files.

Exit codes were captured by redirecting first (cmd > log 2>&1; EXIT=$?), never after a pipe, and each verdict is quoted from the line the tool printed.

⚠️ Byte note: the separator sweep needs NBSP and the ideographic space. Both are written as / escapes — a literal one is a character no reviewer can see and no grep can be aimed at. Verified: 0 invisible or control characters in either changed file.

The changeset was checked and not edited: it makes no claim about the discovery predicate, and its "the under-reporting is closed rather than merely bounded" is what amendment 1 makes true.

⛔ Not done, deliberately: PR not flipped ready, no auto-merge, needs:contract-review left on. The package: components / plugin / tests labels appeared from the labeler, not from me.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

✅ Amendments verified — needs:contract-review cleared, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Provenance: 「PASS ⇒ 同席剥标、ready、auto-merge」 (维护者 2026-08-31 裁定). The tier review returned PASS WITH REQUIRED AMENDMENTS (5524970713); all three are now in.

Verified by content at 089e35043, not from the report

  • Scope: parent is the reviewed head 675571f8f; 2 files, both under scripts/. The seam, the seven call sites, the alias and the five new suites are byte-unchanged — confirmed by filtering the commit's file list for anything outside scripts/ and getting an empty set.
  • Amendment 1 — the pin is behavioural and cannot silently go vacuous. It imports describeIconLookup from the resolver and asserts toRecordKey(s) === describeIconLookup(s).key over 8,000+ spellings. ⭐ More than was asked: it carries its own non-vacuity control — it models a dot-splitting gate and asserts the corpus can separate the two, naming arrow.right and building.2. So a future corpus edit that quietly drops the hostile separators turns the suite red rather than passing in silence. That is strictly better than the one-off ablation the amendment prescribed, because it keeps holding after today.
  • Amendment 2 — the bound is stated, the predicate is NOT widened. The DECLARED_RECORD_READERS comment now names what is inside (named icons import, rename included, plus element access through parens / as / !, any argument expression) and what is outside (namespace import, re-bound const, property access, destructuring, require()/dynamic import(), code inside a string or template literal), and cites finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472. The PR body's matching sentence is corrected too.
  • Amendment 3 was discharged by this seat on the card (5524981959).
  • CI: 32/32 at 089e35043 — 29 success, 3 skipped by design. Test (shard 1/4) 11:52:22 · 2/4 11:52:16 · 3/4 11:54:14 · 4/4 11:54:17 · Type Check 11:50:22 · Lint 11:47:10 · Bundle Analysis, README Export Check, Governed Surface Queue Guard each read individually.

⭐ The implementer corrected the review, and it is the most valuable thing in this round

Amendment 1's prescribed corpus would not have reproduced its own proof. The review said to assert over the enumeration's re-spellings (kebab/snake/spaced/Pascal/lower/doubled). Measured: no key of the record contains a separator at all (0 of 1,767; control "contains a digit" fires at 95), so 0 of 10,602 re-spellings change under /[-_.\s]+/. A pin built to the amendment's letter would have stayed green under the exact mutation it was commissioned to catch.

The implementer added a per-character separator sweep instead — that is what makes it fire — and said so plainly rather than reporting compliance. ⇒ I ratify the deviation. Amendment 1's requirement was a pin that is behavioural and proven to fire; the letter of its corpus was insufficient for its own purpose, and following the letter would have produced a pin that certified nothing. Measured after: 2 of 42 red under that mutation, with the gate script itself still exiting 0 — which is exactly why the pin, not the gate, has to hold this.

Three further corrections from the same round, recorded because two of them are mine:

  1. ⚠️My brief said "template-literal forms are outside" the predicate — wrong as written. An element access whose argument is a template literal (icons[`${a}${b}`]) is inside; the predicate reads the base identifier and never the argument. What is outside is code emitted inside a template literal. The new comment spells out which is which instead of repeating the ambiguity. The implementer also measured that a renamed named import is inside — which the review did not mention.
  2. finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472's own stated third reason is misdescribed (in that issue, not here): it says the base is "a property access", where the generated code actually re-binds a namespace import through a const. Both are outside the predicate so the conclusion stands; only the reason is wrong. Left alone as out of scope.
  3. The changeset needed no edit — it makes no claim about the discovery predicate, and its "under-reporting closed rather than merely bounded" line is precisely what amendment 1 makes true.

An accepted, named gap — not an oversight

The seam's own docblock (packages/components/src/renderers/action/resolve-icon.ts) still ends its enforcement paragraph with the same unqualified sentence amendment 2 corrected in the .mjs. It is left as-is: the tier reviewer examined that docblock and judged it accurate in context (it is preceded by an accurate predicate sentence), and my dispatch scoped the seam out so the mechanism under review stayed byte-stable. ⇒ A one-sentence follow-up available any time; recorded here so it is a chosen gap rather than one nobody noticed.

Carriers

needs:contract-review removed from this PR (remaining: package: components, plugin, tests). Card #5935 never carried it — its labels are pm:dispatched, domain:ui. ⚠️ That is the same dual-carrier gap the reviewer found on the declaration side, from the other direction: the gate label was hung on one carrier only. I also set #5935's assignee, which was empty while this PR was open — a half-state that invites a second agent onto claimed work.

⚠️ Both readings are manual: scripts/pm/check-clause2-carriers.mjs exits 2 in this session (no GitHub token; MCP-only access), and its own output says that is not a clearance. A hand label-check covers strictly less than the tool — naming the instrument, as on every landing this shift.


Generated by Claude Code

os-sam pushed a commit that referenced this pull request Sep 4, 2026
…ecord the third zero
objectui#5936 asked for the consumer of a component registration's `icon` meta
before extending the icon gate to it. Three first-party populations were
measured and none has one (objectui, objectstack, cloud), so the gate is not
extended — adjudicated 2026-09-04.
Records that where the next reader hits it (the gate's own "Deliberate
boundaries"), with the three readings, their controls, the adjudication, and
the caveat that three zeros are not a proof of absence.
Retires the membership half of the `ui:icon` local pin, which was kept at
objectui#5633 on the premise that the palette lives outside this repo. It
guarded 1 of the 45 registrations declaring an `icon` meta while the gate is
adjudicated not to cover the other 44, and its hand-copied tokeniser had
already gone stale against the seam objectui#5935 / PR #7491 created. The
coupling half stays: it needs no external vocabulary and cannot drift.
The published `ComponentMeta.icon` key is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(components,app-shell,plugin-detail,plugin-list,plugin-view): one lucide icon-name seam, seven resolvers down to one - #7491

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-5935-one-icon-resolver-seam
Sep 3, 2026
Merged

refactor(components,app-shell,plugin-detail,plugin-list,plugin-view): one lucide icon-name seam, seven resolvers down to one#7491
os-project-manager merged 2 commits into
mainfrom
claude/issue-5935-one-icon-resolver-seam

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#5935

Seven modules resolved authored lucide icon names, each with its own copy of the logic. There is now one, resolveIcon, and the other six call it.

Both rulings on the card execute here as written:

  • 2026-08-31 (comment 5472612351, 维护者, verbatim 「同意」, option B) — one tokeniser, one iconNameMap, one seam; and point 4, 「本裁定后新容器 ⛔ 不得再自带解析器,一律走 seam」.
  • 2026-09-03 (comment 5523286738, maintainer, verbatim 「同意你的建议」, option C) — the seam does name -> component and nothing else. No onUnresolvable parameter. Each call site keeps its own fallback.

The census, re-measured before starting

Bound declared first, because this card has been handed a wrong number four times: all 4,101 git-tracked .ts/.tsx files, whole repo, no root subset — wider than the gate's own SCAN_ROOTS (3,945 of them). Re-run of the gate's own AST discovery over that wider set.

readingruled figure (at c1fe272ad)measured at my base e307c9896drift
record-reading resolvers77none
Home -> House alias4 of 74 of 7none
dynamic-surface readers44none
RECORD_READING_TYPES14 keys, 9 non-action14 keys, 9 non-actionnone
tokenisersT1 on 5, T2 on 1, T3 on 1T1 on 5, T2 on 1, T3 on 1none

Zero drift. The only difference from the ruling's measurement is the scanned-source count (3,945 vs 3,943 — two files added to the tree since).

What changed at each of the seven sites

sitehadnow
renderers/action/resolve-icon.tssplit('-') + aliasthe seam: split(/[-_\s]+/) + alias
renderers/basic/icon.tsxown toPascalCase + own map + own record indexcalls the seam; SquareDashed placeholder + warning byte-identical
app-shellpreviews/ActionPreview.tsxsplit(/[-_\s]+/) + inline ternarycalls the seam; keeps its 3-character name chip
plugin-detail/RelatedList.tsxsplit(/[-_\s]/), no aliasresolveIcon(name) ?? Inbox
plugin-list/ListView.tsxinline split('-'), no aliasresolveIcon(iconName) ?? Inbox
plugin-list/components/TabBar.tsxsplit('-'), no aliascalls the seam; keeps null
plugin-view/ViewSwitcher.tsxown toPascalCase + own mapcalls the seam; keeps null for views and its own `

resolveIcon is exported from @object-ui/components, the one package all five consumers already depend on — no new dependency edge.

The tokeniser was not a choice made in this PR

split(/[-_\s]+/) with the rename universal, established by the pre-dispatch enumeration the 2026-08-31 ruling required (comment 5522254814): regression set empty against the authored population, against an every-name-by-every-surface cross-product, and against a bound-free differential over 8,298 spellings derived from all 1,767 live record keys. split('-') regresses 4,748 name-surface pairs in that last reading and is not adoptable.

Widening only, and structurally so: no record key contains _, whitespace or - (0 of 1,767, with a firing control), so whenever the narrow tokeniser produced a live key the wide one produces the same key. The old resolving sets are strict subsets. Sixteen name-surface pairs start resolving; none stops. Stated in words in the changeset.

⭐ The load-bearing claim: zero user-visible change on all four unresolvable behaviours

Each of the four is pinned, and each pin is proven non-vacuous two ways — a control in the same run that asserts the opposite state, and a red-then-green ablation on the committed baseline 675571f8f.

behavioursurfacespinned in
nullseam, TabBar, ViewSwitcher (+ its own `
SquareDashed + warnbasic/icon.tsxthe existing #5631 pin, untouched, 13/13 green
Inbox glyphRelatedList (header and toolbar), ListView empty stateRelatedList.iconSeam-5935.test.tsx, icon-seam-fallbacks-5935.test.tsx
3-character name chipActionPreviewActionPreview.iconSeam-5935.test.tsx

The ablation, on the committed baseline, restore proven BY STATE (git diff HEAD empty and blob equality against the HEAD blob; trap ... EXIT INT TERM with absolute paths; restore pinned to git checkout HEAD -- path, never the bare form that reads back from the index). Each mutation was proven on disk in both directions — removed text gone, injected text present — before its run was read:

mutationrows that fired
seam tokeniser back to split('-')8 across all 5 new suites
the Home -> House entry disabled2, incl. the #5631 pin's own rename row
RelatedList: ?? Inbox dropped4
ListView: ?? Inbox dropped2
ActionPreview: chip emptied1
icon.tsx: placeholder glyph swapped out1, the #5631 placeholder row

⚠️The trap named on this card, caught in the act. Dropping RelatedList's ?? Inbox also turned its CONTROL row red — the component threw and drew nothing, exactly the "a test that passes because a component failed to render" failure this card warned about. That is why every fallback describe opens with a row proving the surface rendered and asserts its fallback positively, rather than only asserting an absence.

Two earlier ablation attempts were refused by the harness rather than reported, and are named so the numbers above are readable: injecting SquareDashedNOPE left the removed text present as a substring (both-directions probe unsatisfiable), and injecting the empty string made the injected-text probe degenerate. Both were re-run with clean anchors.

The gate, simplified in the same PR

scripts/check-lucide-icon-record-names.mjs: DECLARED_RECORD_READERS goes 7 to 1, and its normalisation stops being a widest-common approximation. Its verdict line, before and after:

before: OK lucide icon names: 182 authored/declared names reaching 7 record-reading resolvers are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).
after: OK lucide icon names: 182 authored/declared names reaching 1 record-reading resolver are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).

Every part-2 counter is identical across the two runs (148 judged, 22 descendant-judged, 239 declined, 34 anchored) — the census moved, what is judged did not. The under-reporting disclosed at #5932 is closed, not merely bounded: with one resolver there is nothing left to approximate.

Ruling point 4 ships as enforcement within a stated bound. Discovery is re-run from source every run and disagreement fails in both directions. ⚠️ Corrected here after the contract review measured the earlier wording as an over-claim: the predicate holds one syntactic shape — a named icons import from lucide-react (a rename counts) plus an element access on that binding, seen through parens, as casts and !, with any argument expression. Measured red on that shape; measured green, i.e. not caught, on a namespace import, a binding re-bound through another const, property access, destructuring, require()/dynamic import(), and anything inside a string or template literal. #7472 is the known outside-predicate resolver and is outside three ways at once. So a new container that hand-rolls a lookup in the conforming shape reds the commit that adds it; one written another way does not, and the ruling still binds it by review. The bound is now stated at DECLARED_RECORD_READERS itself. ⛔ The discovery predicate is not widened — that is a separate design question.

The two-copies pin is behavioural, not textual. The gate's .mjs normalisation and the resolver's TypeScript one are held together by importing describeIconLookup and asserting toRecordKey(s) equals describeIconLookup(s).key over a corpus derived from the runtime record — all 1,767 live keys re-spelled six ways, plus a per-character separator sweep — with a control that the corpus can in fact separate the two. The sweep is load-bearing: no record key contains a separator at all, so re-spellings alone cannot see a character-class widening. The source-text rows are kept; they catch a different thing. Measured need: widening only the gate to split(/[-_.\s]+/) left the suite green 40/40 before this, and reds 2 of 42 after it, with the gate script itself still exiting 0 under that mutation — which is exactly why the pin, not the gate, has to hold this.

Verification

  • New suites: 5 files, 40 tests. Union re-run at 089e35043 (the amendment commit): 18 files, 184 tests, all passing — the 5 new plus every pre-existing icon suite (ui:icon reads the SDUI identity key name as its lucide icon name, so any icon node that authors name renders nothing at all #5631's 13-test pin, finding(components): ui:button inlines its own copy of resolveIcon instead of importing it, so any future fix to the shared resolver silently misses it #5993's button dedupe pin, breadcrumb/command/context-menu/dropdown-menu item icons, the gate's own suite, now 42 tests, TabBar.i18nLabel, ListView.emptyStateFilteredCopy, ViewSwitcher, ActionPreview.locations). That is the 675571f8f reading of 182 plus the 2 rows the behavioural pin adds, and it is quoted from a run on the current head rather than carried forward.
  • pnpm exec turbo run type-check --filter='...@object-ui/components'66 tasks, all successful, over the 45-package DOWNSTREAM CONSUMER closure (prefix filter = consumers, the direction a contract change travels). The five new test files were confirmed present in each package's tsconfig.test.json program via --listFiles, so "type-check is green" actually covers them.
  • 13 derived gates green at 675571f8f: icon-record-names, control-bytes, vi-mock-specifiers, vi-mock-inherit, readme-exports, phantom-deps, self-import, entry-guard, side-effects-array, sdui-registration-pins, eager-closure, dist-completeness, docs-route-closure.
  • ESLint over the 15 changed files: 0 errors (3 introduced by the first draft were fixed: an unused LucideIcon import, an unused React import, and a react-hooks/static-components error on the ActionPreview call site, which now carries the same annotation the other five call sites do). 250 warnings, all pre-existing any / react-hooks noise on untouched lines of two 4,000-line files.
  • Changeset present, minor on the five packages whose source moved.

Scope

Clause-②: yes — and this is a deliberate re-derivation against the ruling's stated expectation of no. The 2026-09-03 ruling recorded «Clause-②: no expected (renderer-internal; re-declare from the diff)», which holds for the half it was about: no contract accept/reject behaviour moves, no schema, validator or refusal changes, nothing an author may write changes. But the diff widens a published surfaceresolveIcon is a new export from @object-ui/components's public entry, which the ruling's "renderer-internal" reading did not anticipate and which is unavoidable because the five other call sites live in five other packages. Declaring no here would have been an auditable false declaration, so it is yes with the reason attached.

#7472 stays out.packages/cli/src/utils/app-generator.ts:842 emits an eighth resolver inside a template literal, against lucide's named-export namespace, with zero normalisation. It is correctly absent from DECLARED_RECORD_READERS (it reads neither censused vocabulary), and the gate's discovery predicate is not widened to see it.

Nothing under content/docs/releases/ is touched. No gate threshold is raised or weakened.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… one lucide icon-name seam, seven resolvers down to one
Seven modules resolved authored icon names against lucide's runtime `icons`
record, each with its own copy: three tokenisers (`split('-')` on five,
`split(/[-_\s]/)` on one, `split(/[-_\s]+/)` on one) and the `Home` -> `House`
rename on four of seven. The same authored name therefore rendered on one
surface and not another. `resolveIcon` is now exported from
`@object-ui/components` and the other six call it.
The tokeniser is `split(/[-_\s]+/)` with the rename universal — measured, not
chosen: its regression set is empty against the authored population, against an
every-name-by-every-surface cross-product, and against a bound-free differential
over 8,298 spellings derived from all 1,767 live keys.
The seam answers `name -> component | null` and decides nothing else. Every call
site keeps its own fallback, at the call site: `ui:icon` its `SquareDashed`
placeholder and warning (objectui#5631, untouched), `RelatedList` and `ListView`
their `Inbox`, `ActionPreview` its name chip, the rest `null`.
`scripts/check-lucide-icon-record-names.mjs` is simplified in the same change:
its census goes from seven sites to one, and its normalisation stops being a
widest-common approximation of three disagreeing resolvers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.8 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-C0j--fB6.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.31KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.69KB21.03KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Seat bookkeeping + the Clause ② call. ⛔ NOT a contract review.

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Head 675571f8f.

⚖️ Open question RULED: A — the declaration stands as Clause ② YES.

The card recorded "Clause ②: no expected (renderer-internal; re-declare from the diff)". The dev re-derived it against the actual diff and declared yes, because resolveIcon is now on @object-ui/components' public entry. That is correct and it stands.

The criterion has two limbs — changes accept/reject behaviourorwidens the public surface. The second is met: a symbol that was package-internal is published, into a 45-package consumer closure. ⇒ needs:contract-review applied, PR stays draft, ⛔ not enqueued, and the project director seat lands it.

⭐ The reason I am not taking the softer reading: the card's own instruction was "re-declare from the diff", not "confirm the expectation". A no that any reviewer can falsify by reading one line of index.ts costs more than one extra gate pass — it devalues every other no in the ledger. The recorded expectation was a prediction, and predictions are what re-derivation exists to overturn.

⚠️ For the reviewing seat, the honest narrowing: no accept/reject behaviour moves. Nothing an author may write changes; no schema, validator or refusal is touched. The export exists solely so five sibling packages can stop duplicating a private function, and the diagnostics helper describeIconLookup was deliberately kept off the public entry to hold the widening at exactly one symbol — verified, see below.

Verified on the branch by content, ⛔ not from the report

probeorigin/mainbranch
onUnresolvable in the seam0 — the only hit is prose explaining it was deliberately not added
resolveIcon on the public entry01
describeIconLookup on the public entry0
⛔ FENCE — SquareDashed in basic/icon.tsx (#5631)33, unchanged
gate census entries82

⚠️ That last row is my probe's count, not the report's: the dev states 7 → 1 and my pattern matches one extra non-entry line on each side. The delta is identical (−6), and I am stating both rather than silently adopting either.

Call-site fallbacks intact and local, exactly as option C requires: RelatedList.tsx:222resolveIcon(name) ?? Inbox, ListView.tsx:3881resolveIcon(iconName) ?? Inbox, icon.tsx:121 through the seam into its own placeholder branch.

⭐ The trap this card named, caught in the act

Dropping RelatedList's ?? Inbox in the ablation turned its control row red too — the component threw and rendered nothing. That is precisely "a test that passes because a component failed to render", which the dispatch flagged because this repo produced exactly that failure earlier today. It is why every fallback block here opens with a render-proof row and asserts its fallback positively.

⭐ And two ablation attempts were refused by the harness rather than reported: injecting SquareDashedNOPE left the removed text present as a substring, so the both-directions probe was unsatisfiable; injecting the empty string made grep -cF '' match every line. Both were re-run with clean anchors and neither produced a reading that was used. A harness that declines to emit a number it cannot justify is worth more than the six ablation rows it did produce.

Two more worth the reviewer's eye

The census was re-derived over a WIDER bound than the gate's own. All 4,101 tracked .ts/.tsx files, against the gate's 3,945 SCAN_ROOTSzero drift from every ruled figure, with the detector control firing in the same run. A census is only as wide as its pathspec, and this one declared its bound and then exceeded the incumbent.

Ruling point 4 ships as mechanical enforcement, not a note. 「本裁定后新容器 ⛔ 不得再自带解析器」 is now held by the gate itself: it rediscovers record readers from source every run and fails in both directions, so a container that hand-rolls a lookup reds the commit that adds it.

Out-of-scope #7492 filed (the gate's data-table census entry names a module that reads no record); ⛔ deliberately not fixed in place despite the bounded-in-place conditions arguably holding — this card carries three scope fences and a drive-by edit to an unrelated table entry is what they exist to stop. #7472 confirmed still present, still correctly outside the census, and the discovery predicate ⛔ not widened to see it.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Clause-② in-seat contract review — PASS WITH REQUIRED AMENDMENTS. needs:contract-review stays on.

Commissioned by the domain:ui execution seat (PM session session_01EMrWaQw3XS5DxTHxp4yRyC) per the in-seat review route (维护者 2026-08-31 裁定: 交付后复核由派发席在席内完成,达档 PM 自审或派 fable 复核子任务皆可). This seat runs opus and is not at tier, so it commissioned an isolated reviewer rather than self-reviewing.

Independence: the reviewer was given only the card, the rulings and the PR — ⛔ not the dispatch order and not this seat's opinion. It discloses below that it encountered a seat bookkeeping comment carrying a Clause-② opinion while reading the PR thread, and did not use it. Recording that rather than hiding it.

Tier verification (维护者 2026-08-27 裁定 — a commissioned review is legitimate only if verified): every harness-stamped model field in the reviewer's transcript was read before adopting — 97 stamps, all claude-fable-5-1, zero other values, zero fallback/overload markers. CONTRACT_REVIEW_TIER is claude-fable-5 (objectstack/scripts/pm/dispatch-gates.mjs:8091); claude-fable-5-1 is at or above that floor — the same floor reading recorded on PR #7471. ⇒ Verified, so the verdict is adopted verbatim. ⛔ Nothing below is rewritten, abridged or polished by this seat; the only legitimate alternatives were verbatim adoption or wholesale voiding.


Contract review — objectui PR #7491 (head 675571f8f) against card #5935

Inputs read in full: the PR body; the complete diff (1,590 lines, all 16 files); issue #5935 body; comments 5472612351 (ruling 2026-08-31), 5523286738 (ruling 2026-09-03), 5522254814 (pre-dispatch enumeration), plus 5523075605 (director record) and 5522309927 (the blocker that produced ruling 2); all 32 check runs; issue #7472. Disclosure for independence: the PR's comment thread contains a seat bookkeeping comment (5523950630) that states a Clause-② opinion; I encountered it while reading the PR's comments and did not use it — every judgment below is derived from the diff and my own runs.

Own verification (detached worktree /home/user/objectui-review-7491 at 675571f8f, main checkout untouched, no stash): the 5 new suites + the #5631 pin + the gate suite = 7 files / 94 tests green; node scripts/check-lucide-icon-record-names.mjs exits 0 with "1 record-reading resolver"; the same on a local merge with current origin/main (7 commits past the branch point); an independent 70,974-spelling differential; two fallback ablations; one pin-hole probe; three enforcement-shape probes. Details under each item.

① Derived judgments — every accept-set / published-surface change the diff causes

Published surface

  1. @object-ui/components public entry (packages/components/src/index.ts) gains resolveIcon(name: string | undefined): LucideIcon | null. New symbol: package.jsonexports has only . and ./style.css, so renderers/action/resolve-icon.ts was unreachable by deep import before — the "new export" claim is correct. Right. Five sibling packages need it; duplicating it is the defect the card exists to end; precedent is hasDeclaredVisibilityGate on the same entry. Shape: return LucideIcon | null is the correct published shape under option C (caller must decide the fallback). One note, not a defect: the parameter rejects null although JSON-sourced icon: null is realistic; widening to string | null | undefined later is non-breaking.
  2. describeIconLookup is exported from the module but not from the package entry, and cannot be deep-imported. Right — the widening is held to exactly one symbol.
  3. Module-private resolveIcon/toPascalCase/iconNameMap removed from TabBar.tsx, ViewSwitcher.tsx, icon.tsx. Never exported from any package index (checked on origin/main). No surface change.
  4. Gate exports: DECLARED_RECORD_READERS 7 → 1; RECORD_READING_TYPES.icon.resolver and .view-switcher.resolver become "…resolve-icon.ts (via …)". The resolver string is consumed only in diagnostics (lines 667/693/784/846/960 of the .mjs), and six entries already used that form. Right.

Accept set (which authored names resolve), per surface — derived from the diff and measured

  1. Shared resolver (and every type routed through it: action:*, dropdown-menu, context-menu, breadcrumb, command, header-bar, button): tokeniser split('-')split(/[-_\s]+/). Widening only, right, and ruled (2026-08-31 「tokeniser 与 iconNameMap 全量收敛到一份」; the tokeniser itself fixed by the enumeration, "the implementer does not pick this").
  2. ui:icon (basic/icon.tsx): same widening; alias unchanged; placeholder + warn unchanged (ui:icon reads the SDUI identity key name as its lucide icon name, so any icon node that authors name renders nothing at all #5631 pin 13/13). One diagnostic-only consequence the PR does not mention: for a name that fails under both rules and contains _/space, the warning's lookup: "X" half now prints the wide-tokeniser Pascal form (FooBar where it printed Foo_bar). Console text only, not rendered. Acceptable.
  3. ActionPreview: was already split(/[-_\s]+/) + alias → no accept-set change, routing only (the test file says so honestly).
  4. RelatedList: split(/[-_\s]/) + no alias → wide + alias. Gains home/Home (and separator variants) → draws House where it drew Inbox. Behaviour change, user-visible, sanctioned: ruling 1 converges iconNameMap to one; enumeration §5 "the alias must be universal"; ruling 2 "Home→House applied universally"; card scope point 2 asks it be stated — it is, in words, in the changeset.
  5. ListView empty state: split('-') + no alias → wide + alias. Gains snake/space spellings and home. Same judgment.
  6. TabBar: split('-') + no alias → wide + alias. Gains snake/space and home (a glyph appears where none did). Same judgment.
  7. ViewSwitcher: split('-') + alias → wide + alias. Gains snake/space. Right.

The tokeniser attack (⚠ item). I transcribed all seven pre-PR resolvers verbatim and the new seam, and ran them over 70,974 spellings: every record key re-spelled as kebab/snake/spaced/Pascal/lower/UPPER, with leading, trailing, doubled and mixed separators of -, _, space, tab and NBSP, plus alias-specific hostiles (-home, home_, ho-me, home_house, …) and controls. Result: 212,163 name-surface pairs resolved before; 0 stop resolving; 0 resolve to a different component; 262,717 widenings; controls fired (building_2 disagreed across old sites exactly as the enumeration recorded; home likewise). Why no counter-example can exist: an empty token capitalises to '' and contributes nothing to the join, so split('-') and split(/-+/) are join-identical (this also makes .filter(Boolean) inert); and since no live key contains -, _ or whitespace (0 of 1,767, re-measured), any live key the narrow rule produced came from hyphen-only tokens, which the wide rule splits identically. The only alias is Home→House and Home is not a live key, so the alias can only add. The "widening only / strict subset" claim is correct.

Home → House at the three previously alias-free sites. It is a behaviour change, it is sanctioned by both rulings (see 8), it can change what a user sees (icon: "home" on a related-list header/toolbar button or a grid empty state: Inbox → House; on a tab: nothing → House), and it is stated as such in the changeset. Right.

The four unresolvable behaviours, per site — verified by reading the head files, not the description:

  • null: seam ?? null; TabBar.tsx:146/164, 212/225, 232/241 guard TabIcon && …; ViewSwitcher.tsxgetViewIcon returns resolveIcon(view.icon) when an icon is authored (authored-but-dead → no glyph, not the type default) and actions use resolveIcon(action.icon) || DEFAULT_VIEW_ACTION_ICONS[type] (:258). Old ViewSwitcher used || null; identical for component values. Preserved.
  • SquareDashed + warn: icon.tsx:121requested ? resolveIcon(requested) : undefined then if (!Icon); old value was undefined, new is null; nothing else reads Icon. Placeholder branch byte-identical. Preserved.
  • Inbox: RelatedList.tsx:222resolveIcon(name) ?? Inbox feeding both the header (:1322) and toolbar (:299, which still gates absent icons with action.icon ? … : null — a third behaviour in that file, pinned); ListView.tsx:3881resolveIcon(iconName) ?? Inbox. Preserved.
  • 3-character chip: ActionPreview.tsxif (Glyph) … else <span>{name.slice(0,3)}</span> unchanged. Preserved.

② Semver

  • Changeset: minor on components, app-shell, plugin-detail, plugin-list, plugin-view.
  • My level: @object-ui/componentsminor — a new public export, nothing removed. The four consumers: additive behaviour widening at their surfaces, no API change — minor is defensible, patch would be too; the distinction is moot because all five (and 35 others) are in the single fixed group in .changeset/config.json, so the family bumps minor on components' account regardless. Major: no — no name stops resolving (measured), no export removed; and this repo's policy (check-changeset-no-major.mjs) reserves major for the objectstack-synchronised bump, shipping objectui breaks as minor with the break spelled out — there is no break to spell.
  • Matches. The changeset body also carries the alias widening at the three sites in words, which the card demanded.

③ Boundary flags

(a) Clause-②: yes against the ruling's expected no. Correct re-derivation. The criterion (pm-dispatch SKILL.md line 547) is two-limbed — "改变契约接受/拒绝行为 扩大公开面". Limb 1 is not met (no schema, validator or refusal moves; the gate's judged/declined counters are unchanged). Limb 2 is met: a package-internal symbol is now on the public entry of a package with a 45-package downstream closure. The director's line was explicitly "re-declare from the diff"; a no here would be a falsifiable false declaration. Stands. One carrier problem, though: the checker that landing precondition ② runs (objectstack/scripts/pm/check-clause2-carriers.mjs --pair) reads the card side from claim-marked comments in the fixed spelling; I ran its parser — the PR body's **Clause-②: yes** — … line reads declared: yes, but no comment on #5935 carries a Clause-②: line at line start (the only occurrences are mid-line prose and inside the os-dev-report JSON), and the seat's claim comments do not match CLAIM_COMMENT_MARKER (/^\s*>?\s*Claim(?:ed)?\s*:/) either. The card limb therefore reads absent, which that tool classifies as adverse (exit 4) — "⛔ 不作干净". See amendment 3.

(b) #7472 held out. Defensible. Ruling 2 says so in terms ("⛔ Out of scope here; its absence from DECLARED_RECORD_READERS is correct, not an omission"); ruling point 4 governs new containers, and the generator's DynamicIcon predates it, is emitted into a different tree, and reads a third vocabulary (named exports, zero normalisation) — routing it through the seam would change what generated apps accept and needs its own ruling. Not widening the discovery predicate is consistent with #7472's own text. But the PR over-claims the enforcement. I added a probe file in three shapes and ran the gate: named icons import + direct element access → red ("UNDECLARED record-reading resolver", exit 1); import * as L …; L.icons[k] in executed code → green; named import re-bound through a second const and indexed → green. So "a container that hand-rolls a lookup turns the gate red on the commit that adds it" (.mjsDECLARED_RECORD_READERS comment, PR body) holds for one syntactic shape. The seam's docblock states the predicate accurately ("named-imports lucide's icons record and indexes it"); the gate comment does not. See amendment 2.

(c) The two-copies pin has a hole. The new gate-test row "normalises exactly as the ONE resolver does, read from its source" asserts (i) the resolver's text contains .split(/[-_\s]+/) and not .split('-'), (ii) the map entries extracted from the text equal {Home:'House'}, and (iii) the gate's own toRecordKey on six samples. It never executes the resolver. Probe: I widened only the gate's tokeniser to split(/[-_.\s]+/)all 40 gate tests stayed green, including that row. So the claim in the .mjs Normalisation comment, "a change to either side that is not made to the other fails loudly", is false in the gate-wider direction — which is exactly the under-reporting class the card closes (the gate would judge a name live that the resolver misses). The unit vitest project already imports TypeScript straight from packages/ (scripts/__tests__/known-schema-types-derivation-5115.test.tspackages/cli/src/utils/known-schema-types.js), so a behavioural pin is feasible without a build. See amendment 1.

Pins are not vacuous — confirmed

  • Each fallback describe opens with a CONTROL asserting a positive render (getByText('Line Items'), getByTestId('view-tab-open'), getAllByText('New Task')), plus a CONTROL that a resolvable name draws that glyph and not the fallback; every fallback row asserts svg.lucide-inbox / the chip text positively, not merely an absence.
  • Ablation reproduced in my worktree: drop ?? Inbox in RelatedList.tsx → 4 red including "CONTROL — the section renders, with its title" (the readers render <SectionIcon/> unconditionally, so a null throws; a component that fails to render cannot pass these); drop it in ListView.tsx → 2 red. Tree verified clean after each restore.
  • Seam suite discriminates (file-text non-null, not-a-real-icon null, edit null against the retired named export) and re-implements the narrow tokeniser in-test with a carried > 1000 guard against a silently empty loop. The ActionPreview spy row asserts toHaveBeenCalled() before reading mock.results (the blind-instrument trap avoided).

CI, by name

29 success: Changeset Bump Policy, Changeset Overwrite Report, Changeset Declaration, Changeset Fixed Group Check, Lint (5 min, real), Type Check (9 min, real), Test shard 1/4, 2/4, 3/4, 4/4 (10–14 min each, real; the vitest include globs cover the five new packages/**/*.test.tsx and scripts/**/*.test.ts, and I reran them locally), Build & E2E (log read: 14 Playwright passed / 23 skipped, 69 s explained by a pnpm cache hit — not a path-skip), Bundle Analysis (eager closure 3178.8 / 3191.4 KB — passes with 12.6 KB headroom; note only), README Export Check, Skill Example Check, Skill Guide Path Check, Skill Eval Token Check, Control Byte Scan, Shell Escape Residue Scan, Inert vi.mock Specifier Check, Pre-Install Import Graph Check, Governed Surface Queue Guard, Docs Route Eager Closure Check, Doc Component Type Check, Doc Snippet Type Check, Doc Fence Language Check, Build Docs, Internal Docs Link Check, Live E2E (informational), label. 3 skipped by design: Test (coverage) and Test (coverage shard) run on push only; dependabot.

Base behind main:base.sha c4f541df9; branch point e307c9896; origin/main is 7 commits past the branch point (3 past base). Overlap of those commits' files with the PR's 16: none; no new icons import from lucide-react anywhere on main since the branch point; and I ran the gate and all 7 suites on an actual local merge of head + origin/main — gate OK (1 resolver), 94/94. No reading is invalidated.

Verdict

PASS WITH REQUIRED AMENDMENTS

  1. Make the two-copies pin behavioural (scripts/__tests__/check-lucide-icon-record-names.test.ts). Import describeIconLookup (or resolveIcon) from packages/components/src/renderers/action/resolve-icon.ts and assert toRecordKey(s) === describeIconLookup(s).key for every record key under the enumeration's re-spellings (kebab/snake/spaced/Pascal/lower/doubled), plus a negative control; keep or drop the source-text rows as you like. Then either make the .mjs Normalisation comment's "either side … fails loudly" true or reword it. Proof it is needed: gate-only widening to /[-_.\s]+/ passed 40/40.
  2. State the enforcement bound at DECLARED_RECORD_READERS in scripts/check-lucide-icon-record-names.mjs. Replace "a new container that hand-rolls a lookup turns this gate red" with the predicate it actually holds (named icons import from 'lucide-react' + direct element access on that binding; namespace-import, re-bound and template-literal forms are outside it), and cite finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472 as the known outside-predicate resolver. Widening discovery is a separate design question and is not asked for here.
  3. Card-side Clause-② carrier (process, no code). Post on Consolidate the eight lucide record-reading icon resolvers into one seam — three tokenisers and a 5-of-8 alias map mean the same authored name renders on one surface and not another #5935 a claim-shaped comment (first line matching Claim:) carrying Clause-②: yes in the fixed spelling so check-clause2-carriers.mjs --pair 7491 reads both limbs consistent; today the card limb reads absent, and landing precondition ② cannot pass on it.

Not required, for the record: resolveIcon's parameter could accept null (non-breaking later); the not.toMatch(/\.split\('-'\)/) text row is fragile to a future comment; eager-closure headroom is 12.6 KB.


Seat disposition

Not landing.needs:contract-review stays on this PR until all three amendments are in and this seat verifies them by content. Amendments 1 and 2 are code and are being dispatched. Amendment 3 is a PM action on card #5935 and is this seat's own to discharge.

⭐ Amendment 3 is worth calling out: it is a machine-checkable landing precondition that this seat could not have caught itselfcheck-clause2-carriers.mjs cannot run in this session at all (exit 2, no GitHub token; MCP-only access), so the seat had fallen back to reading labels by hand, and a hand check cannot see a missing claim-line spelling. The reviewer ran the tool's parser directly and found the card limb reads absent. That is exactly the kind of finding an isolated reviewer at tier exists to produce.


Generated by Claude Code

…state the discovery bound (objectui#5935)
Both required amendments from the Clause-② contract review of PR #7491
(comment 5524970713). The seam, the call sites, the alias and the five new
suites are untouched.
Amendment 1 — the two-copies pin is now BEHAVIOURAL.
`scripts/__tests__/check-lucide-icon-record-names.test.ts` asserted only the
resolver's SOURCE TEXT, so it was blind to the gate moving on its own: the
resolver's text is then unchanged and every source-text row still passes. The
review measured it — widening ONLY `toRecordKey` to `split(/[-_.\s]+/)` left
the file green 40/40 — and that is the gate-wider direction, exactly the
under-reporting class this card exists to close.
The file now imports `describeIconLookup` from the resolver and asserts
`toRecordKey(s) === describeIconLookup(s).key` over a corpus derived from the
runtime record: every one of the 1,767 live keys re-spelled six ways, plus a
per-character separator sweep. The sweep is not decoration — no key of the
record contains a separator at all, so re-spellings alone cannot see a
character-class widening, which is why the measured hole survived them. A
second row is the control that the corpus can in fact separate the two,
modelling the drifted gate by substitution on the shipped `toRecordKey` rather
than transcribing the rule a third time. The source-text rows are kept; they
catch a different thing.
TypeScript is imported straight out of `packages/` the way
`known-schema-types-derivation-5115.test.ts` already does, so the pin costs no
build step, and by a RELATIVE path so `describeIconLookup` stays off the
package entry and the published widening stays at one symbol.
Amendment 2 — the enforcement bound is stated instead of over-claimed.
The `DECLARED_RECORD_READERS` comment said a new container that hand-rolls a
lookup turns the gate red. It holds for one syntactic shape. Measured against
this gate: named `icons` import (rename included) plus element access on that
binding, through parens/`as`/`!`, any argument expression — red. Namespace
import, a binding re-bound through another const, property access,
destructuring, `require()`/dynamic `import()`, and anything inside a string or
template literal — green. objectui#7472 is cited as the known outside-predicate
resolver and is outside three ways at once. The discovery predicate is ⛔ NOT
widened; that is a separate design question.
The Normalisation comment's "either side ... fails loudly" is likewise made
true rather than reworded away: it now names the behavioural row that carries
it and records that the source-text rows do not.
Verification: gate exits 0 at 1 record-reading resolver; the 7 relevant suites
96 green (94 before, +2 rows); ablation red/green with the restore proven by
state; `type-check:scripts`, `lint:root` (0 errors on changed files) and
`check-control-bytes` green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3180.9 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DpQmpEDV.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.31KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.69KB21.03KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Amendments 1 and 2 applied — 089e35043. ⛔ Still draft, needs:contract-review untouched.

Against the contract review's PASS WITH REQUIRED AMENDMENTS (comment 5524970713). Amendment 3 is the PM seat's and was not touched. The mechanism — tokeniser, seam, Home to House alias, seven call sites, five new suites — is byte-unchanged; the commit touches two files, both under scripts/.

Amendment 1 — the two-copies pin is behavioural

scripts/__tests__/check-lucide-icon-record-names.test.ts now imports describeIconLookup from the resolver by a relative path (so it stays off the package entry and the published widening stays at one symbol) and asserts toRecordKey(s) equals describeIconLookup(s).key over a corpus derived from the runtime record: all 1,767 live keys re-spelled six ways, plus a per-character separator sweep. The source-text rows are kept.

The sweep is load-bearing, and the review's own prescription would not have worked without it. The amendment text lists the re-spellings (kebab/snake/spaced/Pascal/lower/doubled) as the corpus. Those alone cannot catch the measured hole: no key of the record contains a separator at all (0 of 1,767, control — 95 contain a digit), so widening the class by . changes nothing about any of them and the pin would have stayed green under the very mutation it was commissioned to catch. Measured before writing the row: 0 of 10,602 re-spellings differ under /[-_.\s]+/; 19 corpus members do once hostile separators are in. So the corpus sweeps the character class, not the one character.

The ablation, reproducing the review's probe. Implementation committed first, so the restore leg has a real reference point; mutation and restore both proven by state, never by exit code.

legreading
pre-mutation, on diskshipped .split(/[-_\s]+/) present 1, mutated form present 0
post-mutation, on diskshipped 0, mutated 1; blob 4985efe76 vs HEAD e1600563f; git diff HEAD numstat 1 1
gate script under the mutationexit 0, still "1 record-reading resolver" — the drift is invisible to the gate's own run
pin suite under the mutation2 failed, 40 passed (42) — the two new rows, and only those
restoregit checkout HEAD -- ABSOLUTE_PATH under trap ... EXIT INT TERM; git diff HEAD empty and blob back to e1600563f
green leg on the restored tree42 passed (42)

The 40 pre-existing rows staying green under the mutation is the review's finding reproduced exactly. The .mjs Normalisation comment is made true rather than reworded away: it now names the behavioural row that carries "either side fails loudly" and records that the source-text rows do not.

Amendment 2 — the enforcement bound is stated

Replaced at DECLARED_RECORD_READERS, and the same over-claim corrected in the PR body. Every line of the new comment is measured against discoverResolvers, not reasoned about — 19 syntactic shapes through the real predicate:

Inside (red): named icons import; a renamed named import ({ icons as reg }); (icons as Record)[k]; and any argument expression, a template literal included — the predicate never looks at the argument.
Outside (green, not caught): namespace import; a binding re-bound through another const; property access (icons.House); destructuring; require(); dynamic import(); and anything inside a string or template literal.

⚠️One correction to the amendment text, which I would rather state than quietly obey. It says "namespace-import, re-bound and template-literal forms are outside it". Element access with a template-literal argument is inside — measured red. What is outside is code emitted inside a template literal, which is #7472's first of three reasons. Both readings are defensible from the phrase, so the new comment spells out which is which rather than repeating the ambiguity. #7472 is cited, and it is outside three ways at once: app-generator.ts emits, inside a template literal, import * as LucideIcons from 'lucide-react' re-bound to a lucideIcons const and then indexed. ⛔ The discovery predicate is not widened.

Verification, on the pushed head

  • 7 relevant suites: 7 files, 96 tests passed (the review's 94, plus the 2 rows). This file alone 42, was 40.
  • Icon union re-run at 089e35043: 18 files, 184 tests passed — 182 plus the same 2. Re-run because a push after review invalidates a sha-stamped union reading.
  • Every other suite naming the edited gate script — entry-guard wiring, scripts type-check parity, breadcrumb / command / context-menu item icons, icon-renderer-declared-default, ViewSwitcher: 7 files, 62 tests passed.
  • node scripts/check-lucide-icon-record-names.mjsexit 0, OK lucide icon names: 182 authored/declared names reaching 1 record-reading resolver are live icons keys.
  • pnpm type-check:scripts — VERDICT command-exit 0. pnpm lint:root (the scope that owns scripts/) — 0 errors, 32 warnings, 0 of them in either changed file; all 32 are pre-existing no-explicit-any in e2e specs, two unrelated scripts/__tests__ files and two vitest setup files.
  • node scripts/check-control-bytes.mjs — OK, 6,158 tracked text files.

Exit codes were captured by redirecting first (cmd > log 2>&1; EXIT=$?), never after a pipe, and each verdict is quoted from the line the tool printed.

⚠️ Byte note: the separator sweep needs NBSP and the ideographic space. Both are written as / escapes — a literal one is a character no reviewer can see and no grep can be aimed at. Verified: 0 invisible or control characters in either changed file.

The changeset was checked and not edited: it makes no claim about the discovery predicate, and its "the under-reporting is closed rather than merely bounded" is what amendment 1 makes true.

⛔ Not done, deliberately: PR not flipped ready, no auto-merge, needs:contract-review left on. The package: components / plugin / tests labels appeared from the labeler, not from me.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

✅ Amendments verified — needs:contract-review cleared, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Provenance: 「PASS ⇒ 同席剥标、ready、auto-merge」 (维护者 2026-08-31 裁定). The tier review returned PASS WITH REQUIRED AMENDMENTS (5524970713); all three are now in.

Verified by content at 089e35043, not from the report

  • Scope: parent is the reviewed head 675571f8f; 2 files, both under scripts/. The seam, the seven call sites, the alias and the five new suites are byte-unchanged — confirmed by filtering the commit's file list for anything outside scripts/ and getting an empty set.
  • Amendment 1 — the pin is behavioural and cannot silently go vacuous. It imports describeIconLookup from the resolver and asserts toRecordKey(s) === describeIconLookup(s).key over 8,000+ spellings. ⭐ More than was asked: it carries its own non-vacuity control — it models a dot-splitting gate and asserts the corpus can separate the two, naming arrow.right and building.2. So a future corpus edit that quietly drops the hostile separators turns the suite red rather than passing in silence. That is strictly better than the one-off ablation the amendment prescribed, because it keeps holding after today.
  • Amendment 2 — the bound is stated, the predicate is NOT widened. The DECLARED_RECORD_READERS comment now names what is inside (named icons import, rename included, plus element access through parens / as / !, any argument expression) and what is outside (namespace import, re-bound const, property access, destructuring, require()/dynamic import(), code inside a string or template literal), and cites finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472. The PR body's matching sentence is corrected too.
  • Amendment 3 was discharged by this seat on the card (5524981959).
  • CI: 32/32 at 089e35043 — 29 success, 3 skipped by design. Test (shard 1/4) 11:52:22 · 2/4 11:52:16 · 3/4 11:54:14 · 4/4 11:54:17 · Type Check 11:50:22 · Lint 11:47:10 · Bundle Analysis, README Export Check, Governed Surface Queue Guard each read individually.

⭐ The implementer corrected the review, and it is the most valuable thing in this round

Amendment 1's prescribed corpus would not have reproduced its own proof. The review said to assert over the enumeration's re-spellings (kebab/snake/spaced/Pascal/lower/doubled). Measured: no key of the record contains a separator at all (0 of 1,767; control "contains a digit" fires at 95), so 0 of 10,602 re-spellings change under /[-_.\s]+/. A pin built to the amendment's letter would have stayed green under the exact mutation it was commissioned to catch.

The implementer added a per-character separator sweep instead — that is what makes it fire — and said so plainly rather than reporting compliance. ⇒ I ratify the deviation. Amendment 1's requirement was a pin that is behavioural and proven to fire; the letter of its corpus was insufficient for its own purpose, and following the letter would have produced a pin that certified nothing. Measured after: 2 of 42 red under that mutation, with the gate script itself still exiting 0 — which is exactly why the pin, not the gate, has to hold this.

Three further corrections from the same round, recorded because two of them are mine:

  1. ⚠️My brief said "template-literal forms are outside" the predicate — wrong as written. An element access whose argument is a template literal (icons[`${a}${b}`]) is inside; the predicate reads the base identifier and never the argument. What is outside is code emitted inside a template literal. The new comment spells out which is which instead of repeating the ambiguity. The implementer also measured that a renamed named import is inside — which the review did not mention.
  2. finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472's own stated third reason is misdescribed (in that issue, not here): it says the base is "a property access", where the generated code actually re-binds a namespace import through a const. Both are outside the predicate so the conclusion stands; only the reason is wrong. Left alone as out of scope.
  3. The changeset needed no edit — it makes no claim about the discovery predicate, and its "under-reporting closed rather than merely bounded" line is precisely what amendment 1 makes true.

An accepted, named gap — not an oversight

The seam's own docblock (packages/components/src/renderers/action/resolve-icon.ts) still ends its enforcement paragraph with the same unqualified sentence amendment 2 corrected in the .mjs. It is left as-is: the tier reviewer examined that docblock and judged it accurate in context (it is preceded by an accurate predicate sentence), and my dispatch scoped the seam out so the mechanism under review stayed byte-stable. ⇒ A one-sentence follow-up available any time; recorded here so it is a chosen gap rather than one nobody noticed.

Carriers

needs:contract-review removed from this PR (remaining: package: components, plugin, tests). Card #5935 never carried it — its labels are pm:dispatched, domain:ui. ⚠️ That is the same dual-carrier gap the reviewer found on the declaration side, from the other direction: the gate label was hung on one carrier only. I also set #5935's assignee, which was empty while this PR was open — a half-state that invites a second agent onto claimed work.

⚠️ Both readings are manual: scripts/pm/check-clause2-carriers.mjs exits 2 in this session (no GitHub token; MCP-only access), and its own output says that is not a clearance. A hand label-check covers strictly less than the tool — naming the instrument, as on every landing this shift.


Generated by Claude Code

os-sam pushed a commit that referenced this pull request Sep 4, 2026
…ecord the third zero
objectui#5936 asked for the consumer of a component registration's `icon` meta
before extending the icon gate to it. Three first-party populations were
measured and none has one (objectui, objectstack, cloud), so the gate is not
extended — adjudicated 2026-09-04.
Records that where the next reader hits it (the gate's own "Deliberate
boundaries"), with the three readings, their controls, the adjudication, and
the caveat that three zeros are not a proof of absence.
Retires the membership half of the `ui:icon` local pin, which was kept at
objectui#5633 on the premise that the palette lives outside this repo. It
guarded 1 of the 45 registrations declaring an `icon` meta while the gate is
adjudicated not to cover the other 44, and its hand-copied tokeniser had
already gone stale against the seam objectui#5935 / PR #7491 created. The
coupling half stays: it needs no external vocabulary and cannot drift.
The published `ComponentMeta.icon` key is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(components,app-shell,plugin-detail,plugin-list,plugin-view): one lucide icon-name seam, seven resolvers down to one - #7491

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-5935-one-icon-resolver-seam
Sep 3, 2026
Merged

refactor(components,app-shell,plugin-detail,plugin-list,plugin-view): one lucide icon-name seam, seven resolvers down to one#7491
os-project-manager merged 2 commits into
mainfrom
claude/issue-5935-one-icon-resolver-seam

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#5935

Seven modules resolved authored lucide icon names, each with its own copy of the logic. There is now one, resolveIcon, and the other six call it.

Both rulings on the card execute here as written:

  • 2026-08-31 (comment 5472612351, 维护者, verbatim 「同意」, option B) — one tokeniser, one iconNameMap, one seam; and point 4, 「本裁定后新容器 ⛔ 不得再自带解析器,一律走 seam」.
  • 2026-09-03 (comment 5523286738, maintainer, verbatim 「同意你的建议」, option C) — the seam does name -> component and nothing else. No onUnresolvable parameter. Each call site keeps its own fallback.

The census, re-measured before starting

Bound declared first, because this card has been handed a wrong number four times: all 4,101 git-tracked .ts/.tsx files, whole repo, no root subset — wider than the gate's own SCAN_ROOTS (3,945 of them). Re-run of the gate's own AST discovery over that wider set.

readingruled figure (at c1fe272ad)measured at my base e307c9896drift
record-reading resolvers77none
Home -> House alias4 of 74 of 7none
dynamic-surface readers44none
RECORD_READING_TYPES14 keys, 9 non-action14 keys, 9 non-actionnone
tokenisersT1 on 5, T2 on 1, T3 on 1T1 on 5, T2 on 1, T3 on 1none

Zero drift. The only difference from the ruling's measurement is the scanned-source count (3,945 vs 3,943 — two files added to the tree since).

What changed at each of the seven sites

sitehadnow
renderers/action/resolve-icon.tssplit('-') + aliasthe seam: split(/[-_\s]+/) + alias
renderers/basic/icon.tsxown toPascalCase + own map + own record indexcalls the seam; SquareDashed placeholder + warning byte-identical
app-shellpreviews/ActionPreview.tsxsplit(/[-_\s]+/) + inline ternarycalls the seam; keeps its 3-character name chip
plugin-detail/RelatedList.tsxsplit(/[-_\s]/), no aliasresolveIcon(name) ?? Inbox
plugin-list/ListView.tsxinline split('-'), no aliasresolveIcon(iconName) ?? Inbox
plugin-list/components/TabBar.tsxsplit('-'), no aliascalls the seam; keeps null
plugin-view/ViewSwitcher.tsxown toPascalCase + own mapcalls the seam; keeps null for views and its own `

resolveIcon is exported from @object-ui/components, the one package all five consumers already depend on — no new dependency edge.

The tokeniser was not a choice made in this PR

split(/[-_\s]+/) with the rename universal, established by the pre-dispatch enumeration the 2026-08-31 ruling required (comment 5522254814): regression set empty against the authored population, against an every-name-by-every-surface cross-product, and against a bound-free differential over 8,298 spellings derived from all 1,767 live record keys. split('-') regresses 4,748 name-surface pairs in that last reading and is not adoptable.

Widening only, and structurally so: no record key contains _, whitespace or - (0 of 1,767, with a firing control), so whenever the narrow tokeniser produced a live key the wide one produces the same key. The old resolving sets are strict subsets. Sixteen name-surface pairs start resolving; none stops. Stated in words in the changeset.

⭐ The load-bearing claim: zero user-visible change on all four unresolvable behaviours

Each of the four is pinned, and each pin is proven non-vacuous two ways — a control in the same run that asserts the opposite state, and a red-then-green ablation on the committed baseline 675571f8f.

behavioursurfacespinned in
nullseam, TabBar, ViewSwitcher (+ its own `
SquareDashed + warnbasic/icon.tsxthe existing #5631 pin, untouched, 13/13 green
Inbox glyphRelatedList (header and toolbar), ListView empty stateRelatedList.iconSeam-5935.test.tsx, icon-seam-fallbacks-5935.test.tsx
3-character name chipActionPreviewActionPreview.iconSeam-5935.test.tsx

The ablation, on the committed baseline, restore proven BY STATE (git diff HEAD empty and blob equality against the HEAD blob; trap ... EXIT INT TERM with absolute paths; restore pinned to git checkout HEAD -- path, never the bare form that reads back from the index). Each mutation was proven on disk in both directions — removed text gone, injected text present — before its run was read:

mutationrows that fired
seam tokeniser back to split('-')8 across all 5 new suites
the Home -> House entry disabled2, incl. the #5631 pin's own rename row
RelatedList: ?? Inbox dropped4
ListView: ?? Inbox dropped2
ActionPreview: chip emptied1
icon.tsx: placeholder glyph swapped out1, the #5631 placeholder row

⚠️The trap named on this card, caught in the act. Dropping RelatedList's ?? Inbox also turned its CONTROL row red — the component threw and drew nothing, exactly the "a test that passes because a component failed to render" failure this card warned about. That is why every fallback describe opens with a row proving the surface rendered and asserts its fallback positively, rather than only asserting an absence.

Two earlier ablation attempts were refused by the harness rather than reported, and are named so the numbers above are readable: injecting SquareDashedNOPE left the removed text present as a substring (both-directions probe unsatisfiable), and injecting the empty string made the injected-text probe degenerate. Both were re-run with clean anchors.

The gate, simplified in the same PR

scripts/check-lucide-icon-record-names.mjs: DECLARED_RECORD_READERS goes 7 to 1, and its normalisation stops being a widest-common approximation. Its verdict line, before and after:

before: OK lucide icon names: 182 authored/declared names reaching 7 record-reading resolvers are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).
after: OK lucide icon names: 182 authored/declared names reaching 1 record-reading resolver are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).

Every part-2 counter is identical across the two runs (148 judged, 22 descendant-judged, 239 declined, 34 anchored) — the census moved, what is judged did not. The under-reporting disclosed at #5932 is closed, not merely bounded: with one resolver there is nothing left to approximate.

Ruling point 4 ships as enforcement within a stated bound. Discovery is re-run from source every run and disagreement fails in both directions. ⚠️ Corrected here after the contract review measured the earlier wording as an over-claim: the predicate holds one syntactic shape — a named icons import from lucide-react (a rename counts) plus an element access on that binding, seen through parens, as casts and !, with any argument expression. Measured red on that shape; measured green, i.e. not caught, on a namespace import, a binding re-bound through another const, property access, destructuring, require()/dynamic import(), and anything inside a string or template literal. #7472 is the known outside-predicate resolver and is outside three ways at once. So a new container that hand-rolls a lookup in the conforming shape reds the commit that adds it; one written another way does not, and the ruling still binds it by review. The bound is now stated at DECLARED_RECORD_READERS itself. ⛔ The discovery predicate is not widened — that is a separate design question.

The two-copies pin is behavioural, not textual. The gate's .mjs normalisation and the resolver's TypeScript one are held together by importing describeIconLookup and asserting toRecordKey(s) equals describeIconLookup(s).key over a corpus derived from the runtime record — all 1,767 live keys re-spelled six ways, plus a per-character separator sweep — with a control that the corpus can in fact separate the two. The sweep is load-bearing: no record key contains a separator at all, so re-spellings alone cannot see a character-class widening. The source-text rows are kept; they catch a different thing. Measured need: widening only the gate to split(/[-_.\s]+/) left the suite green 40/40 before this, and reds 2 of 42 after it, with the gate script itself still exiting 0 under that mutation — which is exactly why the pin, not the gate, has to hold this.

Verification

  • New suites: 5 files, 40 tests. Union re-run at 089e35043 (the amendment commit): 18 files, 184 tests, all passing — the 5 new plus every pre-existing icon suite (ui:icon reads the SDUI identity key name as its lucide icon name, so any icon node that authors name renders nothing at all #5631's 13-test pin, finding(components): ui:button inlines its own copy of resolveIcon instead of importing it, so any future fix to the shared resolver silently misses it #5993's button dedupe pin, breadcrumb/command/context-menu/dropdown-menu item icons, the gate's own suite, now 42 tests, TabBar.i18nLabel, ListView.emptyStateFilteredCopy, ViewSwitcher, ActionPreview.locations). That is the 675571f8f reading of 182 plus the 2 rows the behavioural pin adds, and it is quoted from a run on the current head rather than carried forward.
  • pnpm exec turbo run type-check --filter='...@object-ui/components'66 tasks, all successful, over the 45-package DOWNSTREAM CONSUMER closure (prefix filter = consumers, the direction a contract change travels). The five new test files were confirmed present in each package's tsconfig.test.json program via --listFiles, so "type-check is green" actually covers them.
  • 13 derived gates green at 675571f8f: icon-record-names, control-bytes, vi-mock-specifiers, vi-mock-inherit, readme-exports, phantom-deps, self-import, entry-guard, side-effects-array, sdui-registration-pins, eager-closure, dist-completeness, docs-route-closure.
  • ESLint over the 15 changed files: 0 errors (3 introduced by the first draft were fixed: an unused LucideIcon import, an unused React import, and a react-hooks/static-components error on the ActionPreview call site, which now carries the same annotation the other five call sites do). 250 warnings, all pre-existing any / react-hooks noise on untouched lines of two 4,000-line files.
  • Changeset present, minor on the five packages whose source moved.

Scope

Clause-②: yes — and this is a deliberate re-derivation against the ruling's stated expectation of no. The 2026-09-03 ruling recorded «Clause-②: no expected (renderer-internal; re-declare from the diff)», which holds for the half it was about: no contract accept/reject behaviour moves, no schema, validator or refusal changes, nothing an author may write changes. But the diff widens a published surfaceresolveIcon is a new export from @object-ui/components's public entry, which the ruling's "renderer-internal" reading did not anticipate and which is unavoidable because the five other call sites live in five other packages. Declaring no here would have been an auditable false declaration, so it is yes with the reason attached.

#7472 stays out.packages/cli/src/utils/app-generator.ts:842 emits an eighth resolver inside a template literal, against lucide's named-export namespace, with zero normalisation. It is correctly absent from DECLARED_RECORD_READERS (it reads neither censused vocabulary), and the gate's discovery predicate is not widened to see it.

Nothing under content/docs/releases/ is touched. No gate threshold is raised or weakened.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… one lucide icon-name seam, seven resolvers down to one
Seven modules resolved authored icon names against lucide's runtime `icons`
record, each with its own copy: three tokenisers (`split('-')` on five,
`split(/[-_\s]/)` on one, `split(/[-_\s]+/)` on one) and the `Home` -> `House`
rename on four of seven. The same authored name therefore rendered on one
surface and not another. `resolveIcon` is now exported from
`@object-ui/components` and the other six call it.
The tokeniser is `split(/[-_\s]+/)` with the rename universal — measured, not
chosen: its regression set is empty against the authored population, against an
every-name-by-every-surface cross-product, and against a bound-free differential
over 8,298 spellings derived from all 1,767 live keys.
The seam answers `name -> component | null` and decides nothing else. Every call
site keeps its own fallback, at the call site: `ui:icon` its `SquareDashed`
placeholder and warning (objectui#5631, untouched), `RelatedList` and `ListView`
their `Inbox`, `ActionPreview` its name chip, the rest `null`.
`scripts/check-lucide-icon-record-names.mjs` is simplified in the same change:
its census goes from seven sites to one, and its normalisation stops being a
widest-common approximation of three disagreeing resolvers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.8 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-C0j--fB6.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.31KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.69KB21.03KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Seat bookkeeping + the Clause ② call. ⛔ NOT a contract review.

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Head 675571f8f.

⚖️ Open question RULED: A — the declaration stands as Clause ② YES.

The card recorded "Clause ②: no expected (renderer-internal; re-declare from the diff)". The dev re-derived it against the actual diff and declared yes, because resolveIcon is now on @object-ui/components' public entry. That is correct and it stands.

The criterion has two limbs — changes accept/reject behaviourorwidens the public surface. The second is met: a symbol that was package-internal is published, into a 45-package consumer closure. ⇒ needs:contract-review applied, PR stays draft, ⛔ not enqueued, and the project director seat lands it.

⭐ The reason I am not taking the softer reading: the card's own instruction was "re-declare from the diff", not "confirm the expectation". A no that any reviewer can falsify by reading one line of index.ts costs more than one extra gate pass — it devalues every other no in the ledger. The recorded expectation was a prediction, and predictions are what re-derivation exists to overturn.

⚠️ For the reviewing seat, the honest narrowing: no accept/reject behaviour moves. Nothing an author may write changes; no schema, validator or refusal is touched. The export exists solely so five sibling packages can stop duplicating a private function, and the diagnostics helper describeIconLookup was deliberately kept off the public entry to hold the widening at exactly one symbol — verified, see below.

Verified on the branch by content, ⛔ not from the report

probeorigin/mainbranch
onUnresolvable in the seam0 — the only hit is prose explaining it was deliberately not added
resolveIcon on the public entry01
describeIconLookup on the public entry0
⛔ FENCE — SquareDashed in basic/icon.tsx (#5631)33, unchanged
gate census entries82

⚠️ That last row is my probe's count, not the report's: the dev states 7 → 1 and my pattern matches one extra non-entry line on each side. The delta is identical (−6), and I am stating both rather than silently adopting either.

Call-site fallbacks intact and local, exactly as option C requires: RelatedList.tsx:222resolveIcon(name) ?? Inbox, ListView.tsx:3881resolveIcon(iconName) ?? Inbox, icon.tsx:121 through the seam into its own placeholder branch.

⭐ The trap this card named, caught in the act

Dropping RelatedList's ?? Inbox in the ablation turned its control row red too — the component threw and rendered nothing. That is precisely "a test that passes because a component failed to render", which the dispatch flagged because this repo produced exactly that failure earlier today. It is why every fallback block here opens with a render-proof row and asserts its fallback positively.

⭐ And two ablation attempts were refused by the harness rather than reported: injecting SquareDashedNOPE left the removed text present as a substring, so the both-directions probe was unsatisfiable; injecting the empty string made grep -cF '' match every line. Both were re-run with clean anchors and neither produced a reading that was used. A harness that declines to emit a number it cannot justify is worth more than the six ablation rows it did produce.

Two more worth the reviewer's eye

The census was re-derived over a WIDER bound than the gate's own. All 4,101 tracked .ts/.tsx files, against the gate's 3,945 SCAN_ROOTSzero drift from every ruled figure, with the detector control firing in the same run. A census is only as wide as its pathspec, and this one declared its bound and then exceeded the incumbent.

Ruling point 4 ships as mechanical enforcement, not a note. 「本裁定后新容器 ⛔ 不得再自带解析器」 is now held by the gate itself: it rediscovers record readers from source every run and fails in both directions, so a container that hand-rolls a lookup reds the commit that adds it.

Out-of-scope #7492 filed (the gate's data-table census entry names a module that reads no record); ⛔ deliberately not fixed in place despite the bounded-in-place conditions arguably holding — this card carries three scope fences and a drive-by edit to an unrelated table entry is what they exist to stop. #7472 confirmed still present, still correctly outside the census, and the discovery predicate ⛔ not widened to see it.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Clause-② in-seat contract review — PASS WITH REQUIRED AMENDMENTS. needs:contract-review stays on.

Commissioned by the domain:ui execution seat (PM session session_01EMrWaQw3XS5DxTHxp4yRyC) per the in-seat review route (维护者 2026-08-31 裁定: 交付后复核由派发席在席内完成,达档 PM 自审或派 fable 复核子任务皆可). This seat runs opus and is not at tier, so it commissioned an isolated reviewer rather than self-reviewing.

Independence: the reviewer was given only the card, the rulings and the PR — ⛔ not the dispatch order and not this seat's opinion. It discloses below that it encountered a seat bookkeeping comment carrying a Clause-② opinion while reading the PR thread, and did not use it. Recording that rather than hiding it.

Tier verification (维护者 2026-08-27 裁定 — a commissioned review is legitimate only if verified): every harness-stamped model field in the reviewer's transcript was read before adopting — 97 stamps, all claude-fable-5-1, zero other values, zero fallback/overload markers. CONTRACT_REVIEW_TIER is claude-fable-5 (objectstack/scripts/pm/dispatch-gates.mjs:8091); claude-fable-5-1 is at or above that floor — the same floor reading recorded on PR #7471. ⇒ Verified, so the verdict is adopted verbatim. ⛔ Nothing below is rewritten, abridged or polished by this seat; the only legitimate alternatives were verbatim adoption or wholesale voiding.


Contract review — objectui PR #7491 (head 675571f8f) against card #5935

Inputs read in full: the PR body; the complete diff (1,590 lines, all 16 files); issue #5935 body; comments 5472612351 (ruling 2026-08-31), 5523286738 (ruling 2026-09-03), 5522254814 (pre-dispatch enumeration), plus 5523075605 (director record) and 5522309927 (the blocker that produced ruling 2); all 32 check runs; issue #7472. Disclosure for independence: the PR's comment thread contains a seat bookkeeping comment (5523950630) that states a Clause-② opinion; I encountered it while reading the PR's comments and did not use it — every judgment below is derived from the diff and my own runs.

Own verification (detached worktree /home/user/objectui-review-7491 at 675571f8f, main checkout untouched, no stash): the 5 new suites + the #5631 pin + the gate suite = 7 files / 94 tests green; node scripts/check-lucide-icon-record-names.mjs exits 0 with "1 record-reading resolver"; the same on a local merge with current origin/main (7 commits past the branch point); an independent 70,974-spelling differential; two fallback ablations; one pin-hole probe; three enforcement-shape probes. Details under each item.

① Derived judgments — every accept-set / published-surface change the diff causes

Published surface

  1. @object-ui/components public entry (packages/components/src/index.ts) gains resolveIcon(name: string | undefined): LucideIcon | null. New symbol: package.jsonexports has only . and ./style.css, so renderers/action/resolve-icon.ts was unreachable by deep import before — the "new export" claim is correct. Right. Five sibling packages need it; duplicating it is the defect the card exists to end; precedent is hasDeclaredVisibilityGate on the same entry. Shape: return LucideIcon | null is the correct published shape under option C (caller must decide the fallback). One note, not a defect: the parameter rejects null although JSON-sourced icon: null is realistic; widening to string | null | undefined later is non-breaking.
  2. describeIconLookup is exported from the module but not from the package entry, and cannot be deep-imported. Right — the widening is held to exactly one symbol.
  3. Module-private resolveIcon/toPascalCase/iconNameMap removed from TabBar.tsx, ViewSwitcher.tsx, icon.tsx. Never exported from any package index (checked on origin/main). No surface change.
  4. Gate exports: DECLARED_RECORD_READERS 7 → 1; RECORD_READING_TYPES.icon.resolver and .view-switcher.resolver become "…resolve-icon.ts (via …)". The resolver string is consumed only in diagnostics (lines 667/693/784/846/960 of the .mjs), and six entries already used that form. Right.

Accept set (which authored names resolve), per surface — derived from the diff and measured

  1. Shared resolver (and every type routed through it: action:*, dropdown-menu, context-menu, breadcrumb, command, header-bar, button): tokeniser split('-')split(/[-_\s]+/). Widening only, right, and ruled (2026-08-31 「tokeniser 与 iconNameMap 全量收敛到一份」; the tokeniser itself fixed by the enumeration, "the implementer does not pick this").
  2. ui:icon (basic/icon.tsx): same widening; alias unchanged; placeholder + warn unchanged (ui:icon reads the SDUI identity key name as its lucide icon name, so any icon node that authors name renders nothing at all #5631 pin 13/13). One diagnostic-only consequence the PR does not mention: for a name that fails under both rules and contains _/space, the warning's lookup: "X" half now prints the wide-tokeniser Pascal form (FooBar where it printed Foo_bar). Console text only, not rendered. Acceptable.
  3. ActionPreview: was already split(/[-_\s]+/) + alias → no accept-set change, routing only (the test file says so honestly).
  4. RelatedList: split(/[-_\s]/) + no alias → wide + alias. Gains home/Home (and separator variants) → draws House where it drew Inbox. Behaviour change, user-visible, sanctioned: ruling 1 converges iconNameMap to one; enumeration §5 "the alias must be universal"; ruling 2 "Home→House applied universally"; card scope point 2 asks it be stated — it is, in words, in the changeset.
  5. ListView empty state: split('-') + no alias → wide + alias. Gains snake/space spellings and home. Same judgment.
  6. TabBar: split('-') + no alias → wide + alias. Gains snake/space and home (a glyph appears where none did). Same judgment.
  7. ViewSwitcher: split('-') + alias → wide + alias. Gains snake/space. Right.

The tokeniser attack (⚠ item). I transcribed all seven pre-PR resolvers verbatim and the new seam, and ran them over 70,974 spellings: every record key re-spelled as kebab/snake/spaced/Pascal/lower/UPPER, with leading, trailing, doubled and mixed separators of -, _, space, tab and NBSP, plus alias-specific hostiles (-home, home_, ho-me, home_house, …) and controls. Result: 212,163 name-surface pairs resolved before; 0 stop resolving; 0 resolve to a different component; 262,717 widenings; controls fired (building_2 disagreed across old sites exactly as the enumeration recorded; home likewise). Why no counter-example can exist: an empty token capitalises to '' and contributes nothing to the join, so split('-') and split(/-+/) are join-identical (this also makes .filter(Boolean) inert); and since no live key contains -, _ or whitespace (0 of 1,767, re-measured), any live key the narrow rule produced came from hyphen-only tokens, which the wide rule splits identically. The only alias is Home→House and Home is not a live key, so the alias can only add. The "widening only / strict subset" claim is correct.

Home → House at the three previously alias-free sites. It is a behaviour change, it is sanctioned by both rulings (see 8), it can change what a user sees (icon: "home" on a related-list header/toolbar button or a grid empty state: Inbox → House; on a tab: nothing → House), and it is stated as such in the changeset. Right.

The four unresolvable behaviours, per site — verified by reading the head files, not the description:

  • null: seam ?? null; TabBar.tsx:146/164, 212/225, 232/241 guard TabIcon && …; ViewSwitcher.tsxgetViewIcon returns resolveIcon(view.icon) when an icon is authored (authored-but-dead → no glyph, not the type default) and actions use resolveIcon(action.icon) || DEFAULT_VIEW_ACTION_ICONS[type] (:258). Old ViewSwitcher used || null; identical for component values. Preserved.
  • SquareDashed + warn: icon.tsx:121requested ? resolveIcon(requested) : undefined then if (!Icon); old value was undefined, new is null; nothing else reads Icon. Placeholder branch byte-identical. Preserved.
  • Inbox: RelatedList.tsx:222resolveIcon(name) ?? Inbox feeding both the header (:1322) and toolbar (:299, which still gates absent icons with action.icon ? … : null — a third behaviour in that file, pinned); ListView.tsx:3881resolveIcon(iconName) ?? Inbox. Preserved.
  • 3-character chip: ActionPreview.tsxif (Glyph) … else <span>{name.slice(0,3)}</span> unchanged. Preserved.

② Semver

  • Changeset: minor on components, app-shell, plugin-detail, plugin-list, plugin-view.
  • My level: @object-ui/componentsminor — a new public export, nothing removed. The four consumers: additive behaviour widening at their surfaces, no API change — minor is defensible, patch would be too; the distinction is moot because all five (and 35 others) are in the single fixed group in .changeset/config.json, so the family bumps minor on components' account regardless. Major: no — no name stops resolving (measured), no export removed; and this repo's policy (check-changeset-no-major.mjs) reserves major for the objectstack-synchronised bump, shipping objectui breaks as minor with the break spelled out — there is no break to spell.
  • Matches. The changeset body also carries the alias widening at the three sites in words, which the card demanded.

③ Boundary flags

(a) Clause-②: yes against the ruling's expected no. Correct re-derivation. The criterion (pm-dispatch SKILL.md line 547) is two-limbed — "改变契约接受/拒绝行为 扩大公开面". Limb 1 is not met (no schema, validator or refusal moves; the gate's judged/declined counters are unchanged). Limb 2 is met: a package-internal symbol is now on the public entry of a package with a 45-package downstream closure. The director's line was explicitly "re-declare from the diff"; a no here would be a falsifiable false declaration. Stands. One carrier problem, though: the checker that landing precondition ② runs (objectstack/scripts/pm/check-clause2-carriers.mjs --pair) reads the card side from claim-marked comments in the fixed spelling; I ran its parser — the PR body's **Clause-②: yes** — … line reads declared: yes, but no comment on #5935 carries a Clause-②: line at line start (the only occurrences are mid-line prose and inside the os-dev-report JSON), and the seat's claim comments do not match CLAIM_COMMENT_MARKER (/^\s*>?\s*Claim(?:ed)?\s*:/) either. The card limb therefore reads absent, which that tool classifies as adverse (exit 4) — "⛔ 不作干净". See amendment 3.

(b) #7472 held out. Defensible. Ruling 2 says so in terms ("⛔ Out of scope here; its absence from DECLARED_RECORD_READERS is correct, not an omission"); ruling point 4 governs new containers, and the generator's DynamicIcon predates it, is emitted into a different tree, and reads a third vocabulary (named exports, zero normalisation) — routing it through the seam would change what generated apps accept and needs its own ruling. Not widening the discovery predicate is consistent with #7472's own text. But the PR over-claims the enforcement. I added a probe file in three shapes and ran the gate: named icons import + direct element access → red ("UNDECLARED record-reading resolver", exit 1); import * as L …; L.icons[k] in executed code → green; named import re-bound through a second const and indexed → green. So "a container that hand-rolls a lookup turns the gate red on the commit that adds it" (.mjsDECLARED_RECORD_READERS comment, PR body) holds for one syntactic shape. The seam's docblock states the predicate accurately ("named-imports lucide's icons record and indexes it"); the gate comment does not. See amendment 2.

(c) The two-copies pin has a hole. The new gate-test row "normalises exactly as the ONE resolver does, read from its source" asserts (i) the resolver's text contains .split(/[-_\s]+/) and not .split('-'), (ii) the map entries extracted from the text equal {Home:'House'}, and (iii) the gate's own toRecordKey on six samples. It never executes the resolver. Probe: I widened only the gate's tokeniser to split(/[-_.\s]+/)all 40 gate tests stayed green, including that row. So the claim in the .mjs Normalisation comment, "a change to either side that is not made to the other fails loudly", is false in the gate-wider direction — which is exactly the under-reporting class the card closes (the gate would judge a name live that the resolver misses). The unit vitest project already imports TypeScript straight from packages/ (scripts/__tests__/known-schema-types-derivation-5115.test.tspackages/cli/src/utils/known-schema-types.js), so a behavioural pin is feasible without a build. See amendment 1.

Pins are not vacuous — confirmed

  • Each fallback describe opens with a CONTROL asserting a positive render (getByText('Line Items'), getByTestId('view-tab-open'), getAllByText('New Task')), plus a CONTROL that a resolvable name draws that glyph and not the fallback; every fallback row asserts svg.lucide-inbox / the chip text positively, not merely an absence.
  • Ablation reproduced in my worktree: drop ?? Inbox in RelatedList.tsx → 4 red including "CONTROL — the section renders, with its title" (the readers render <SectionIcon/> unconditionally, so a null throws; a component that fails to render cannot pass these); drop it in ListView.tsx → 2 red. Tree verified clean after each restore.
  • Seam suite discriminates (file-text non-null, not-a-real-icon null, edit null against the retired named export) and re-implements the narrow tokeniser in-test with a carried > 1000 guard against a silently empty loop. The ActionPreview spy row asserts toHaveBeenCalled() before reading mock.results (the blind-instrument trap avoided).

CI, by name

29 success: Changeset Bump Policy, Changeset Overwrite Report, Changeset Declaration, Changeset Fixed Group Check, Lint (5 min, real), Type Check (9 min, real), Test shard 1/4, 2/4, 3/4, 4/4 (10–14 min each, real; the vitest include globs cover the five new packages/**/*.test.tsx and scripts/**/*.test.ts, and I reran them locally), Build & E2E (log read: 14 Playwright passed / 23 skipped, 69 s explained by a pnpm cache hit — not a path-skip), Bundle Analysis (eager closure 3178.8 / 3191.4 KB — passes with 12.6 KB headroom; note only), README Export Check, Skill Example Check, Skill Guide Path Check, Skill Eval Token Check, Control Byte Scan, Shell Escape Residue Scan, Inert vi.mock Specifier Check, Pre-Install Import Graph Check, Governed Surface Queue Guard, Docs Route Eager Closure Check, Doc Component Type Check, Doc Snippet Type Check, Doc Fence Language Check, Build Docs, Internal Docs Link Check, Live E2E (informational), label. 3 skipped by design: Test (coverage) and Test (coverage shard) run on push only; dependabot.

Base behind main:base.sha c4f541df9; branch point e307c9896; origin/main is 7 commits past the branch point (3 past base). Overlap of those commits' files with the PR's 16: none; no new icons import from lucide-react anywhere on main since the branch point; and I ran the gate and all 7 suites on an actual local merge of head + origin/main — gate OK (1 resolver), 94/94. No reading is invalidated.

Verdict

PASS WITH REQUIRED AMENDMENTS

  1. Make the two-copies pin behavioural (scripts/__tests__/check-lucide-icon-record-names.test.ts). Import describeIconLookup (or resolveIcon) from packages/components/src/renderers/action/resolve-icon.ts and assert toRecordKey(s) === describeIconLookup(s).key for every record key under the enumeration's re-spellings (kebab/snake/spaced/Pascal/lower/doubled), plus a negative control; keep or drop the source-text rows as you like. Then either make the .mjs Normalisation comment's "either side … fails loudly" true or reword it. Proof it is needed: gate-only widening to /[-_.\s]+/ passed 40/40.
  2. State the enforcement bound at DECLARED_RECORD_READERS in scripts/check-lucide-icon-record-names.mjs. Replace "a new container that hand-rolls a lookup turns this gate red" with the predicate it actually holds (named icons import from 'lucide-react' + direct element access on that binding; namespace-import, re-bound and template-literal forms are outside it), and cite finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472 as the known outside-predicate resolver. Widening discovery is a separate design question and is not asked for here.
  3. Card-side Clause-② carrier (process, no code). Post on Consolidate the eight lucide record-reading icon resolvers into one seam — three tokenisers and a 5-of-8 alias map mean the same authored name renders on one surface and not another #5935 a claim-shaped comment (first line matching Claim:) carrying Clause-②: yes in the fixed spelling so check-clause2-carriers.mjs --pair 7491 reads both limbs consistent; today the card limb reads absent, and landing precondition ② cannot pass on it.

Not required, for the record: resolveIcon's parameter could accept null (non-breaking later); the not.toMatch(/\.split\('-'\)/) text row is fragile to a future comment; eager-closure headroom is 12.6 KB.


Seat disposition

Not landing.needs:contract-review stays on this PR until all three amendments are in and this seat verifies them by content. Amendments 1 and 2 are code and are being dispatched. Amendment 3 is a PM action on card #5935 and is this seat's own to discharge.

⭐ Amendment 3 is worth calling out: it is a machine-checkable landing precondition that this seat could not have caught itselfcheck-clause2-carriers.mjs cannot run in this session at all (exit 2, no GitHub token; MCP-only access), so the seat had fallen back to reading labels by hand, and a hand check cannot see a missing claim-line spelling. The reviewer ran the tool's parser directly and found the card limb reads absent. That is exactly the kind of finding an isolated reviewer at tier exists to produce.


Generated by Claude Code

…state the discovery bound (objectui#5935)
Both required amendments from the Clause-② contract review of PR #7491
(comment 5524970713). The seam, the call sites, the alias and the five new
suites are untouched.
Amendment 1 — the two-copies pin is now BEHAVIOURAL.
`scripts/__tests__/check-lucide-icon-record-names.test.ts` asserted only the
resolver's SOURCE TEXT, so it was blind to the gate moving on its own: the
resolver's text is then unchanged and every source-text row still passes. The
review measured it — widening ONLY `toRecordKey` to `split(/[-_.\s]+/)` left
the file green 40/40 — and that is the gate-wider direction, exactly the
under-reporting class this card exists to close.
The file now imports `describeIconLookup` from the resolver and asserts
`toRecordKey(s) === describeIconLookup(s).key` over a corpus derived from the
runtime record: every one of the 1,767 live keys re-spelled six ways, plus a
per-character separator sweep. The sweep is not decoration — no key of the
record contains a separator at all, so re-spellings alone cannot see a
character-class widening, which is why the measured hole survived them. A
second row is the control that the corpus can in fact separate the two,
modelling the drifted gate by substitution on the shipped `toRecordKey` rather
than transcribing the rule a third time. The source-text rows are kept; they
catch a different thing.
TypeScript is imported straight out of `packages/` the way
`known-schema-types-derivation-5115.test.ts` already does, so the pin costs no
build step, and by a RELATIVE path so `describeIconLookup` stays off the
package entry and the published widening stays at one symbol.
Amendment 2 — the enforcement bound is stated instead of over-claimed.
The `DECLARED_RECORD_READERS` comment said a new container that hand-rolls a
lookup turns the gate red. It holds for one syntactic shape. Measured against
this gate: named `icons` import (rename included) plus element access on that
binding, through parens/`as`/`!`, any argument expression — red. Namespace
import, a binding re-bound through another const, property access,
destructuring, `require()`/dynamic `import()`, and anything inside a string or
template literal — green. objectui#7472 is cited as the known outside-predicate
resolver and is outside three ways at once. The discovery predicate is ⛔ NOT
widened; that is a separate design question.
The Normalisation comment's "either side ... fails loudly" is likewise made
true rather than reworded away: it now names the behavioural row that carries
it and records that the source-text rows do not.
Verification: gate exits 0 at 1 record-reading resolver; the 7 relevant suites
96 green (94 before, +2 rows); ablation red/green with the restore proven by
state; `type-check:scripts`, `lint:root` (0 errors on changed files) and
`check-control-bytes` green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3180.9 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DpQmpEDV.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.31KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.69KB21.03KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Amendments 1 and 2 applied — 089e35043. ⛔ Still draft, needs:contract-review untouched.

Against the contract review's PASS WITH REQUIRED AMENDMENTS (comment 5524970713). Amendment 3 is the PM seat's and was not touched. The mechanism — tokeniser, seam, Home to House alias, seven call sites, five new suites — is byte-unchanged; the commit touches two files, both under scripts/.

Amendment 1 — the two-copies pin is behavioural

scripts/__tests__/check-lucide-icon-record-names.test.ts now imports describeIconLookup from the resolver by a relative path (so it stays off the package entry and the published widening stays at one symbol) and asserts toRecordKey(s) equals describeIconLookup(s).key over a corpus derived from the runtime record: all 1,767 live keys re-spelled six ways, plus a per-character separator sweep. The source-text rows are kept.

The sweep is load-bearing, and the review's own prescription would not have worked without it. The amendment text lists the re-spellings (kebab/snake/spaced/Pascal/lower/doubled) as the corpus. Those alone cannot catch the measured hole: no key of the record contains a separator at all (0 of 1,767, control — 95 contain a digit), so widening the class by . changes nothing about any of them and the pin would have stayed green under the very mutation it was commissioned to catch. Measured before writing the row: 0 of 10,602 re-spellings differ under /[-_.\s]+/; 19 corpus members do once hostile separators are in. So the corpus sweeps the character class, not the one character.

The ablation, reproducing the review's probe. Implementation committed first, so the restore leg has a real reference point; mutation and restore both proven by state, never by exit code.

legreading
pre-mutation, on diskshipped .split(/[-_\s]+/) present 1, mutated form present 0
post-mutation, on diskshipped 0, mutated 1; blob 4985efe76 vs HEAD e1600563f; git diff HEAD numstat 1 1
gate script under the mutationexit 0, still "1 record-reading resolver" — the drift is invisible to the gate's own run
pin suite under the mutation2 failed, 40 passed (42) — the two new rows, and only those
restoregit checkout HEAD -- ABSOLUTE_PATH under trap ... EXIT INT TERM; git diff HEAD empty and blob back to e1600563f
green leg on the restored tree42 passed (42)

The 40 pre-existing rows staying green under the mutation is the review's finding reproduced exactly. The .mjs Normalisation comment is made true rather than reworded away: it now names the behavioural row that carries "either side fails loudly" and records that the source-text rows do not.

Amendment 2 — the enforcement bound is stated

Replaced at DECLARED_RECORD_READERS, and the same over-claim corrected in the PR body. Every line of the new comment is measured against discoverResolvers, not reasoned about — 19 syntactic shapes through the real predicate:

Inside (red): named icons import; a renamed named import ({ icons as reg }); (icons as Record)[k]; and any argument expression, a template literal included — the predicate never looks at the argument.
Outside (green, not caught): namespace import; a binding re-bound through another const; property access (icons.House); destructuring; require(); dynamic import(); and anything inside a string or template literal.

⚠️One correction to the amendment text, which I would rather state than quietly obey. It says "namespace-import, re-bound and template-literal forms are outside it". Element access with a template-literal argument is inside — measured red. What is outside is code emitted inside a template literal, which is #7472's first of three reasons. Both readings are defensible from the phrase, so the new comment spells out which is which rather than repeating the ambiguity. #7472 is cited, and it is outside three ways at once: app-generator.ts emits, inside a template literal, import * as LucideIcons from 'lucide-react' re-bound to a lucideIcons const and then indexed. ⛔ The discovery predicate is not widened.

Verification, on the pushed head

  • 7 relevant suites: 7 files, 96 tests passed (the review's 94, plus the 2 rows). This file alone 42, was 40.
  • Icon union re-run at 089e35043: 18 files, 184 tests passed — 182 plus the same 2. Re-run because a push after review invalidates a sha-stamped union reading.
  • Every other suite naming the edited gate script — entry-guard wiring, scripts type-check parity, breadcrumb / command / context-menu item icons, icon-renderer-declared-default, ViewSwitcher: 7 files, 62 tests passed.
  • node scripts/check-lucide-icon-record-names.mjsexit 0, OK lucide icon names: 182 authored/declared names reaching 1 record-reading resolver are live icons keys.
  • pnpm type-check:scripts — VERDICT command-exit 0. pnpm lint:root (the scope that owns scripts/) — 0 errors, 32 warnings, 0 of them in either changed file; all 32 are pre-existing no-explicit-any in e2e specs, two unrelated scripts/__tests__ files and two vitest setup files.
  • node scripts/check-control-bytes.mjs — OK, 6,158 tracked text files.

Exit codes were captured by redirecting first (cmd > log 2>&1; EXIT=$?), never after a pipe, and each verdict is quoted from the line the tool printed.

⚠️ Byte note: the separator sweep needs NBSP and the ideographic space. Both are written as / escapes — a literal one is a character no reviewer can see and no grep can be aimed at. Verified: 0 invisible or control characters in either changed file.

The changeset was checked and not edited: it makes no claim about the discovery predicate, and its "the under-reporting is closed rather than merely bounded" is what amendment 1 makes true.

⛔ Not done, deliberately: PR not flipped ready, no auto-merge, needs:contract-review left on. The package: components / plugin / tests labels appeared from the labeler, not from me.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

✅ Amendments verified — needs:contract-review cleared, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Provenance: 「PASS ⇒ 同席剥标、ready、auto-merge」 (维护者 2026-08-31 裁定). The tier review returned PASS WITH REQUIRED AMENDMENTS (5524970713); all three are now in.

Verified by content at 089e35043, not from the report

  • Scope: parent is the reviewed head 675571f8f; 2 files, both under scripts/. The seam, the seven call sites, the alias and the five new suites are byte-unchanged — confirmed by filtering the commit's file list for anything outside scripts/ and getting an empty set.
  • Amendment 1 — the pin is behavioural and cannot silently go vacuous. It imports describeIconLookup from the resolver and asserts toRecordKey(s) === describeIconLookup(s).key over 8,000+ spellings. ⭐ More than was asked: it carries its own non-vacuity control — it models a dot-splitting gate and asserts the corpus can separate the two, naming arrow.right and building.2. So a future corpus edit that quietly drops the hostile separators turns the suite red rather than passing in silence. That is strictly better than the one-off ablation the amendment prescribed, because it keeps holding after today.
  • Amendment 2 — the bound is stated, the predicate is NOT widened. The DECLARED_RECORD_READERS comment now names what is inside (named icons import, rename included, plus element access through parens / as / !, any argument expression) and what is outside (namespace import, re-bound const, property access, destructuring, require()/dynamic import(), code inside a string or template literal), and cites finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472. The PR body's matching sentence is corrected too.
  • Amendment 3 was discharged by this seat on the card (5524981959).
  • CI: 32/32 at 089e35043 — 29 success, 3 skipped by design. Test (shard 1/4) 11:52:22 · 2/4 11:52:16 · 3/4 11:54:14 · 4/4 11:54:17 · Type Check 11:50:22 · Lint 11:47:10 · Bundle Analysis, README Export Check, Governed Surface Queue Guard each read individually.

⭐ The implementer corrected the review, and it is the most valuable thing in this round

Amendment 1's prescribed corpus would not have reproduced its own proof. The review said to assert over the enumeration's re-spellings (kebab/snake/spaced/Pascal/lower/doubled). Measured: no key of the record contains a separator at all (0 of 1,767; control "contains a digit" fires at 95), so 0 of 10,602 re-spellings change under /[-_.\s]+/. A pin built to the amendment's letter would have stayed green under the exact mutation it was commissioned to catch.

The implementer added a per-character separator sweep instead — that is what makes it fire — and said so plainly rather than reporting compliance. ⇒ I ratify the deviation. Amendment 1's requirement was a pin that is behavioural and proven to fire; the letter of its corpus was insufficient for its own purpose, and following the letter would have produced a pin that certified nothing. Measured after: 2 of 42 red under that mutation, with the gate script itself still exiting 0 — which is exactly why the pin, not the gate, has to hold this.

Three further corrections from the same round, recorded because two of them are mine:

  1. ⚠️My brief said "template-literal forms are outside" the predicate — wrong as written. An element access whose argument is a template literal (icons[`${a}${b}`]) is inside; the predicate reads the base identifier and never the argument. What is outside is code emitted inside a template literal. The new comment spells out which is which instead of repeating the ambiguity. The implementer also measured that a renamed named import is inside — which the review did not mention.
  2. finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472's own stated third reason is misdescribed (in that issue, not here): it says the base is "a property access", where the generated code actually re-binds a namespace import through a const. Both are outside the predicate so the conclusion stands; only the reason is wrong. Left alone as out of scope.
  3. The changeset needed no edit — it makes no claim about the discovery predicate, and its "under-reporting closed rather than merely bounded" line is precisely what amendment 1 makes true.

An accepted, named gap — not an oversight

The seam's own docblock (packages/components/src/renderers/action/resolve-icon.ts) still ends its enforcement paragraph with the same unqualified sentence amendment 2 corrected in the .mjs. It is left as-is: the tier reviewer examined that docblock and judged it accurate in context (it is preceded by an accurate predicate sentence), and my dispatch scoped the seam out so the mechanism under review stayed byte-stable. ⇒ A one-sentence follow-up available any time; recorded here so it is a chosen gap rather than one nobody noticed.

Carriers

needs:contract-review removed from this PR (remaining: package: components, plugin, tests). Card #5935 never carried it — its labels are pm:dispatched, domain:ui. ⚠️ That is the same dual-carrier gap the reviewer found on the declaration side, from the other direction: the gate label was hung on one carrier only. I also set #5935's assignee, which was empty while this PR was open — a half-state that invites a second agent onto claimed work.

⚠️ Both readings are manual: scripts/pm/check-clause2-carriers.mjs exits 2 in this session (no GitHub token; MCP-only access), and its own output says that is not a clearance. A hand label-check covers strictly less than the tool — naming the instrument, as on every landing this shift.


Generated by Claude Code

os-sam pushed a commit that referenced this pull request Sep 4, 2026
…ecord the third zero
objectui#5936 asked for the consumer of a component registration's `icon` meta
before extending the icon gate to it. Three first-party populations were
measured and none has one (objectui, objectstack, cloud), so the gate is not
extended — adjudicated 2026-09-04.
Records that where the next reader hits it (the gate's own "Deliberate
boundaries"), with the three readings, their controls, the adjudication, and
the caveat that three zeros are not a proof of absence.
Retires the membership half of the `ui:icon` local pin, which was kept at
objectui#5633 on the premise that the palette lives outside this repo. It
guarded 1 of the 45 registrations declaring an `icon` meta while the gate is
adjudicated not to cover the other 44, and its hand-copied tokeniser had
already gone stale against the seam objectui#5935 / PR #7491 created. The
coupling half stays: it needs no external vocabulary and cannot drift.
The published `ComponentMeta.icon` key is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

refactor(components,app-shell,plugin-detail,plugin-list,plugin-view): one lucide icon-name seam, seven resolvers down to one - #7491

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-5935-one-icon-resolver-seam
Sep 3, 2026
Merged

refactor(components,app-shell,plugin-detail,plugin-list,plugin-view): one lucide icon-name seam, seven resolvers down to one#7491
os-project-manager merged 2 commits into
mainfrom
claude/issue-5935-one-icon-resolver-seam

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#5935

Seven modules resolved authored lucide icon names, each with its own copy of the logic. There is now one, resolveIcon, and the other six call it.

Both rulings on the card execute here as written:

  • 2026-08-31 (comment 5472612351, 维护者, verbatim 「同意」, option B) — one tokeniser, one iconNameMap, one seam; and point 4, 「本裁定后新容器 ⛔ 不得再自带解析器,一律走 seam」.
  • 2026-09-03 (comment 5523286738, maintainer, verbatim 「同意你的建议」, option C) — the seam does name -> component and nothing else. No onUnresolvable parameter. Each call site keeps its own fallback.

The census, re-measured before starting

Bound declared first, because this card has been handed a wrong number four times: all 4,101 git-tracked .ts/.tsx files, whole repo, no root subset — wider than the gate's own SCAN_ROOTS (3,945 of them). Re-run of the gate's own AST discovery over that wider set.

readingruled figure (at c1fe272ad)measured at my base e307c9896drift
record-reading resolvers77none
Home -> House alias4 of 74 of 7none
dynamic-surface readers44none
RECORD_READING_TYPES14 keys, 9 non-action14 keys, 9 non-actionnone
tokenisersT1 on 5, T2 on 1, T3 on 1T1 on 5, T2 on 1, T3 on 1none

Zero drift. The only difference from the ruling's measurement is the scanned-source count (3,945 vs 3,943 — two files added to the tree since).

What changed at each of the seven sites

sitehadnow
renderers/action/resolve-icon.tssplit('-') + aliasthe seam: split(/[-_\s]+/) + alias
renderers/basic/icon.tsxown toPascalCase + own map + own record indexcalls the seam; SquareDashed placeholder + warning byte-identical
app-shellpreviews/ActionPreview.tsxsplit(/[-_\s]+/) + inline ternarycalls the seam; keeps its 3-character name chip
plugin-detail/RelatedList.tsxsplit(/[-_\s]/), no aliasresolveIcon(name) ?? Inbox
plugin-list/ListView.tsxinline split('-'), no aliasresolveIcon(iconName) ?? Inbox
plugin-list/components/TabBar.tsxsplit('-'), no aliascalls the seam; keeps null
plugin-view/ViewSwitcher.tsxown toPascalCase + own mapcalls the seam; keeps null for views and its own `

resolveIcon is exported from @object-ui/components, the one package all five consumers already depend on — no new dependency edge.

The tokeniser was not a choice made in this PR

split(/[-_\s]+/) with the rename universal, established by the pre-dispatch enumeration the 2026-08-31 ruling required (comment 5522254814): regression set empty against the authored population, against an every-name-by-every-surface cross-product, and against a bound-free differential over 8,298 spellings derived from all 1,767 live record keys. split('-') regresses 4,748 name-surface pairs in that last reading and is not adoptable.

Widening only, and structurally so: no record key contains _, whitespace or - (0 of 1,767, with a firing control), so whenever the narrow tokeniser produced a live key the wide one produces the same key. The old resolving sets are strict subsets. Sixteen name-surface pairs start resolving; none stops. Stated in words in the changeset.

⭐ The load-bearing claim: zero user-visible change on all four unresolvable behaviours

Each of the four is pinned, and each pin is proven non-vacuous two ways — a control in the same run that asserts the opposite state, and a red-then-green ablation on the committed baseline 675571f8f.

behavioursurfacespinned in
nullseam, TabBar, ViewSwitcher (+ its own `
SquareDashed + warnbasic/icon.tsxthe existing #5631 pin, untouched, 13/13 green
Inbox glyphRelatedList (header and toolbar), ListView empty stateRelatedList.iconSeam-5935.test.tsx, icon-seam-fallbacks-5935.test.tsx
3-character name chipActionPreviewActionPreview.iconSeam-5935.test.tsx

The ablation, on the committed baseline, restore proven BY STATE (git diff HEAD empty and blob equality against the HEAD blob; trap ... EXIT INT TERM with absolute paths; restore pinned to git checkout HEAD -- path, never the bare form that reads back from the index). Each mutation was proven on disk in both directions — removed text gone, injected text present — before its run was read:

mutationrows that fired
seam tokeniser back to split('-')8 across all 5 new suites
the Home -> House entry disabled2, incl. the #5631 pin's own rename row
RelatedList: ?? Inbox dropped4
ListView: ?? Inbox dropped2
ActionPreview: chip emptied1
icon.tsx: placeholder glyph swapped out1, the #5631 placeholder row

⚠️The trap named on this card, caught in the act. Dropping RelatedList's ?? Inbox also turned its CONTROL row red — the component threw and drew nothing, exactly the "a test that passes because a component failed to render" failure this card warned about. That is why every fallback describe opens with a row proving the surface rendered and asserts its fallback positively, rather than only asserting an absence.

Two earlier ablation attempts were refused by the harness rather than reported, and are named so the numbers above are readable: injecting SquareDashedNOPE left the removed text present as a substring (both-directions probe unsatisfiable), and injecting the empty string made the injected-text probe degenerate. Both were re-run with clean anchors.

The gate, simplified in the same PR

scripts/check-lucide-icon-record-names.mjs: DECLARED_RECORD_READERS goes 7 to 1, and its normalisation stops being a widest-common approximation. Its verdict line, before and after:

before: OK lucide icon names: 182 authored/declared names reaching 7 record-reading resolvers are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).
after: OK lucide icon names: 182 authored/declared names reaching 1 record-reading resolver are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).

Every part-2 counter is identical across the two runs (148 judged, 22 descendant-judged, 239 declined, 34 anchored) — the census moved, what is judged did not. The under-reporting disclosed at #5932 is closed, not merely bounded: with one resolver there is nothing left to approximate.

Ruling point 4 ships as enforcement within a stated bound. Discovery is re-run from source every run and disagreement fails in both directions. ⚠️ Corrected here after the contract review measured the earlier wording as an over-claim: the predicate holds one syntactic shape — a named icons import from lucide-react (a rename counts) plus an element access on that binding, seen through parens, as casts and !, with any argument expression. Measured red on that shape; measured green, i.e. not caught, on a namespace import, a binding re-bound through another const, property access, destructuring, require()/dynamic import(), and anything inside a string or template literal. #7472 is the known outside-predicate resolver and is outside three ways at once. So a new container that hand-rolls a lookup in the conforming shape reds the commit that adds it; one written another way does not, and the ruling still binds it by review. The bound is now stated at DECLARED_RECORD_READERS itself. ⛔ The discovery predicate is not widened — that is a separate design question.

The two-copies pin is behavioural, not textual. The gate's .mjs normalisation and the resolver's TypeScript one are held together by importing describeIconLookup and asserting toRecordKey(s) equals describeIconLookup(s).key over a corpus derived from the runtime record — all 1,767 live keys re-spelled six ways, plus a per-character separator sweep — with a control that the corpus can in fact separate the two. The sweep is load-bearing: no record key contains a separator at all, so re-spellings alone cannot see a character-class widening. The source-text rows are kept; they catch a different thing. Measured need: widening only the gate to split(/[-_.\s]+/) left the suite green 40/40 before this, and reds 2 of 42 after it, with the gate script itself still exiting 0 under that mutation — which is exactly why the pin, not the gate, has to hold this.

Verification

  • New suites: 5 files, 40 tests. Union re-run at 089e35043 (the amendment commit): 18 files, 184 tests, all passing — the 5 new plus every pre-existing icon suite (ui:icon reads the SDUI identity key name as its lucide icon name, so any icon node that authors name renders nothing at all #5631's 13-test pin, finding(components): ui:button inlines its own copy of resolveIcon instead of importing it, so any future fix to the shared resolver silently misses it #5993's button dedupe pin, breadcrumb/command/context-menu/dropdown-menu item icons, the gate's own suite, now 42 tests, TabBar.i18nLabel, ListView.emptyStateFilteredCopy, ViewSwitcher, ActionPreview.locations). That is the 675571f8f reading of 182 plus the 2 rows the behavioural pin adds, and it is quoted from a run on the current head rather than carried forward.
  • pnpm exec turbo run type-check --filter='...@object-ui/components'66 tasks, all successful, over the 45-package DOWNSTREAM CONSUMER closure (prefix filter = consumers, the direction a contract change travels). The five new test files were confirmed present in each package's tsconfig.test.json program via --listFiles, so "type-check is green" actually covers them.
  • 13 derived gates green at 675571f8f: icon-record-names, control-bytes, vi-mock-specifiers, vi-mock-inherit, readme-exports, phantom-deps, self-import, entry-guard, side-effects-array, sdui-registration-pins, eager-closure, dist-completeness, docs-route-closure.
  • ESLint over the 15 changed files: 0 errors (3 introduced by the first draft were fixed: an unused LucideIcon import, an unused React import, and a react-hooks/static-components error on the ActionPreview call site, which now carries the same annotation the other five call sites do). 250 warnings, all pre-existing any / react-hooks noise on untouched lines of two 4,000-line files.
  • Changeset present, minor on the five packages whose source moved.

Scope

Clause-②: yes — and this is a deliberate re-derivation against the ruling's stated expectation of no. The 2026-09-03 ruling recorded «Clause-②: no expected (renderer-internal; re-declare from the diff)», which holds for the half it was about: no contract accept/reject behaviour moves, no schema, validator or refusal changes, nothing an author may write changes. But the diff widens a published surfaceresolveIcon is a new export from @object-ui/components's public entry, which the ruling's "renderer-internal" reading did not anticipate and which is unavoidable because the five other call sites live in five other packages. Declaring no here would have been an auditable false declaration, so it is yes with the reason attached.

#7472 stays out.packages/cli/src/utils/app-generator.ts:842 emits an eighth resolver inside a template literal, against lucide's named-export namespace, with zero normalisation. It is correctly absent from DECLARED_RECORD_READERS (it reads neither censused vocabulary), and the gate's discovery predicate is not widened to see it.

Nothing under content/docs/releases/ is touched. No gate threshold is raised or weakened.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… one lucide icon-name seam, seven resolvers down to one
Seven modules resolved authored icon names against lucide's runtime `icons`
record, each with its own copy: three tokenisers (`split('-')` on five,
`split(/[-_\s]/)` on one, `split(/[-_\s]+/)` on one) and the `Home` -> `House`
rename on four of seven. The same authored name therefore rendered on one
surface and not another. `resolveIcon` is now exported from
`@object-ui/components` and the other six call it.
The tokeniser is `split(/[-_\s]+/)` with the rename universal — measured, not
chosen: its regression set is empty against the authored population, against an
every-name-by-every-surface cross-product, and against a bound-free differential
over 8,298 spellings derived from all 1,767 live keys.
The seam answers `name -> component | null` and decides nothing else. Every call
site keeps its own fallback, at the call site: `ui:icon` its `SquareDashed`
placeholder and warning (objectui#5631, untouched), `RelatedList` and `ListView`
their `Inbox`, `ActionPreview` its name chip, the rest `null`.
`scripts/check-lucide-icon-record-names.mjs` is simplified in the same change:
its census goes from seven sites to one, and its normalisation stops being a
widest-common approximation of three disagreeing resolvers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.8 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-C0j--fB6.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.31KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.69KB21.03KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Seat bookkeeping + the Clause ② call. ⛔ NOT a contract review.

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Head 675571f8f.

⚖️ Open question RULED: A — the declaration stands as Clause ② YES.

The card recorded "Clause ②: no expected (renderer-internal; re-declare from the diff)". The dev re-derived it against the actual diff and declared yes, because resolveIcon is now on @object-ui/components' public entry. That is correct and it stands.

The criterion has two limbs — changes accept/reject behaviourorwidens the public surface. The second is met: a symbol that was package-internal is published, into a 45-package consumer closure. ⇒ needs:contract-review applied, PR stays draft, ⛔ not enqueued, and the project director seat lands it.

⭐ The reason I am not taking the softer reading: the card's own instruction was "re-declare from the diff", not "confirm the expectation". A no that any reviewer can falsify by reading one line of index.ts costs more than one extra gate pass — it devalues every other no in the ledger. The recorded expectation was a prediction, and predictions are what re-derivation exists to overturn.

⚠️ For the reviewing seat, the honest narrowing: no accept/reject behaviour moves. Nothing an author may write changes; no schema, validator or refusal is touched. The export exists solely so five sibling packages can stop duplicating a private function, and the diagnostics helper describeIconLookup was deliberately kept off the public entry to hold the widening at exactly one symbol — verified, see below.

Verified on the branch by content, ⛔ not from the report

probeorigin/mainbranch
onUnresolvable in the seam0 — the only hit is prose explaining it was deliberately not added
resolveIcon on the public entry01
describeIconLookup on the public entry0
⛔ FENCE — SquareDashed in basic/icon.tsx (#5631)33, unchanged
gate census entries82

⚠️ That last row is my probe's count, not the report's: the dev states 7 → 1 and my pattern matches one extra non-entry line on each side. The delta is identical (−6), and I am stating both rather than silently adopting either.

Call-site fallbacks intact and local, exactly as option C requires: RelatedList.tsx:222resolveIcon(name) ?? Inbox, ListView.tsx:3881resolveIcon(iconName) ?? Inbox, icon.tsx:121 through the seam into its own placeholder branch.

⭐ The trap this card named, caught in the act

Dropping RelatedList's ?? Inbox in the ablation turned its control row red too — the component threw and rendered nothing. That is precisely "a test that passes because a component failed to render", which the dispatch flagged because this repo produced exactly that failure earlier today. It is why every fallback block here opens with a render-proof row and asserts its fallback positively.

⭐ And two ablation attempts were refused by the harness rather than reported: injecting SquareDashedNOPE left the removed text present as a substring, so the both-directions probe was unsatisfiable; injecting the empty string made grep -cF '' match every line. Both were re-run with clean anchors and neither produced a reading that was used. A harness that declines to emit a number it cannot justify is worth more than the six ablation rows it did produce.

Two more worth the reviewer's eye

The census was re-derived over a WIDER bound than the gate's own. All 4,101 tracked .ts/.tsx files, against the gate's 3,945 SCAN_ROOTSzero drift from every ruled figure, with the detector control firing in the same run. A census is only as wide as its pathspec, and this one declared its bound and then exceeded the incumbent.

Ruling point 4 ships as mechanical enforcement, not a note. 「本裁定后新容器 ⛔ 不得再自带解析器」 is now held by the gate itself: it rediscovers record readers from source every run and fails in both directions, so a container that hand-rolls a lookup reds the commit that adds it.

Out-of-scope #7492 filed (the gate's data-table census entry names a module that reads no record); ⛔ deliberately not fixed in place despite the bounded-in-place conditions arguably holding — this card carries three scope fences and a drive-by edit to an unrelated table entry is what they exist to stop. #7472 confirmed still present, still correctly outside the census, and the discovery predicate ⛔ not widened to see it.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Clause-② in-seat contract review — PASS WITH REQUIRED AMENDMENTS. needs:contract-review stays on.

Commissioned by the domain:ui execution seat (PM session session_01EMrWaQw3XS5DxTHxp4yRyC) per the in-seat review route (维护者 2026-08-31 裁定: 交付后复核由派发席在席内完成,达档 PM 自审或派 fable 复核子任务皆可). This seat runs opus and is not at tier, so it commissioned an isolated reviewer rather than self-reviewing.

Independence: the reviewer was given only the card, the rulings and the PR — ⛔ not the dispatch order and not this seat's opinion. It discloses below that it encountered a seat bookkeeping comment carrying a Clause-② opinion while reading the PR thread, and did not use it. Recording that rather than hiding it.

Tier verification (维护者 2026-08-27 裁定 — a commissioned review is legitimate only if verified): every harness-stamped model field in the reviewer's transcript was read before adopting — 97 stamps, all claude-fable-5-1, zero other values, zero fallback/overload markers. CONTRACT_REVIEW_TIER is claude-fable-5 (objectstack/scripts/pm/dispatch-gates.mjs:8091); claude-fable-5-1 is at or above that floor — the same floor reading recorded on PR #7471. ⇒ Verified, so the verdict is adopted verbatim. ⛔ Nothing below is rewritten, abridged or polished by this seat; the only legitimate alternatives were verbatim adoption or wholesale voiding.


Contract review — objectui PR #7491 (head 675571f8f) against card #5935

Inputs read in full: the PR body; the complete diff (1,590 lines, all 16 files); issue #5935 body; comments 5472612351 (ruling 2026-08-31), 5523286738 (ruling 2026-09-03), 5522254814 (pre-dispatch enumeration), plus 5523075605 (director record) and 5522309927 (the blocker that produced ruling 2); all 32 check runs; issue #7472. Disclosure for independence: the PR's comment thread contains a seat bookkeeping comment (5523950630) that states a Clause-② opinion; I encountered it while reading the PR's comments and did not use it — every judgment below is derived from the diff and my own runs.

Own verification (detached worktree /home/user/objectui-review-7491 at 675571f8f, main checkout untouched, no stash): the 5 new suites + the #5631 pin + the gate suite = 7 files / 94 tests green; node scripts/check-lucide-icon-record-names.mjs exits 0 with "1 record-reading resolver"; the same on a local merge with current origin/main (7 commits past the branch point); an independent 70,974-spelling differential; two fallback ablations; one pin-hole probe; three enforcement-shape probes. Details under each item.

① Derived judgments — every accept-set / published-surface change the diff causes

Published surface

  1. @object-ui/components public entry (packages/components/src/index.ts) gains resolveIcon(name: string | undefined): LucideIcon | null. New symbol: package.jsonexports has only . and ./style.css, so renderers/action/resolve-icon.ts was unreachable by deep import before — the "new export" claim is correct. Right. Five sibling packages need it; duplicating it is the defect the card exists to end; precedent is hasDeclaredVisibilityGate on the same entry. Shape: return LucideIcon | null is the correct published shape under option C (caller must decide the fallback). One note, not a defect: the parameter rejects null although JSON-sourced icon: null is realistic; widening to string | null | undefined later is non-breaking.
  2. describeIconLookup is exported from the module but not from the package entry, and cannot be deep-imported. Right — the widening is held to exactly one symbol.
  3. Module-private resolveIcon/toPascalCase/iconNameMap removed from TabBar.tsx, ViewSwitcher.tsx, icon.tsx. Never exported from any package index (checked on origin/main). No surface change.
  4. Gate exports: DECLARED_RECORD_READERS 7 → 1; RECORD_READING_TYPES.icon.resolver and .view-switcher.resolver become "…resolve-icon.ts (via …)". The resolver string is consumed only in diagnostics (lines 667/693/784/846/960 of the .mjs), and six entries already used that form. Right.

Accept set (which authored names resolve), per surface — derived from the diff and measured

  1. Shared resolver (and every type routed through it: action:*, dropdown-menu, context-menu, breadcrumb, command, header-bar, button): tokeniser split('-')split(/[-_\s]+/). Widening only, right, and ruled (2026-08-31 「tokeniser 与 iconNameMap 全量收敛到一份」; the tokeniser itself fixed by the enumeration, "the implementer does not pick this").
  2. ui:icon (basic/icon.tsx): same widening; alias unchanged; placeholder + warn unchanged (ui:icon reads the SDUI identity key name as its lucide icon name, so any icon node that authors name renders nothing at all #5631 pin 13/13). One diagnostic-only consequence the PR does not mention: for a name that fails under both rules and contains _/space, the warning's lookup: "X" half now prints the wide-tokeniser Pascal form (FooBar where it printed Foo_bar). Console text only, not rendered. Acceptable.
  3. ActionPreview: was already split(/[-_\s]+/) + alias → no accept-set change, routing only (the test file says so honestly).
  4. RelatedList: split(/[-_\s]/) + no alias → wide + alias. Gains home/Home (and separator variants) → draws House where it drew Inbox. Behaviour change, user-visible, sanctioned: ruling 1 converges iconNameMap to one; enumeration §5 "the alias must be universal"; ruling 2 "Home→House applied universally"; card scope point 2 asks it be stated — it is, in words, in the changeset.
  5. ListView empty state: split('-') + no alias → wide + alias. Gains snake/space spellings and home. Same judgment.
  6. TabBar: split('-') + no alias → wide + alias. Gains snake/space and home (a glyph appears where none did). Same judgment.
  7. ViewSwitcher: split('-') + alias → wide + alias. Gains snake/space. Right.

The tokeniser attack (⚠ item). I transcribed all seven pre-PR resolvers verbatim and the new seam, and ran them over 70,974 spellings: every record key re-spelled as kebab/snake/spaced/Pascal/lower/UPPER, with leading, trailing, doubled and mixed separators of -, _, space, tab and NBSP, plus alias-specific hostiles (-home, home_, ho-me, home_house, …) and controls. Result: 212,163 name-surface pairs resolved before; 0 stop resolving; 0 resolve to a different component; 262,717 widenings; controls fired (building_2 disagreed across old sites exactly as the enumeration recorded; home likewise). Why no counter-example can exist: an empty token capitalises to '' and contributes nothing to the join, so split('-') and split(/-+/) are join-identical (this also makes .filter(Boolean) inert); and since no live key contains -, _ or whitespace (0 of 1,767, re-measured), any live key the narrow rule produced came from hyphen-only tokens, which the wide rule splits identically. The only alias is Home→House and Home is not a live key, so the alias can only add. The "widening only / strict subset" claim is correct.

Home → House at the three previously alias-free sites. It is a behaviour change, it is sanctioned by both rulings (see 8), it can change what a user sees (icon: "home" on a related-list header/toolbar button or a grid empty state: Inbox → House; on a tab: nothing → House), and it is stated as such in the changeset. Right.

The four unresolvable behaviours, per site — verified by reading the head files, not the description:

  • null: seam ?? null; TabBar.tsx:146/164, 212/225, 232/241 guard TabIcon && …; ViewSwitcher.tsxgetViewIcon returns resolveIcon(view.icon) when an icon is authored (authored-but-dead → no glyph, not the type default) and actions use resolveIcon(action.icon) || DEFAULT_VIEW_ACTION_ICONS[type] (:258). Old ViewSwitcher used || null; identical for component values. Preserved.
  • SquareDashed + warn: icon.tsx:121requested ? resolveIcon(requested) : undefined then if (!Icon); old value was undefined, new is null; nothing else reads Icon. Placeholder branch byte-identical. Preserved.
  • Inbox: RelatedList.tsx:222resolveIcon(name) ?? Inbox feeding both the header (:1322) and toolbar (:299, which still gates absent icons with action.icon ? … : null — a third behaviour in that file, pinned); ListView.tsx:3881resolveIcon(iconName) ?? Inbox. Preserved.
  • 3-character chip: ActionPreview.tsxif (Glyph) … else <span>{name.slice(0,3)}</span> unchanged. Preserved.

② Semver

  • Changeset: minor on components, app-shell, plugin-detail, plugin-list, plugin-view.
  • My level: @object-ui/componentsminor — a new public export, nothing removed. The four consumers: additive behaviour widening at their surfaces, no API change — minor is defensible, patch would be too; the distinction is moot because all five (and 35 others) are in the single fixed group in .changeset/config.json, so the family bumps minor on components' account regardless. Major: no — no name stops resolving (measured), no export removed; and this repo's policy (check-changeset-no-major.mjs) reserves major for the objectstack-synchronised bump, shipping objectui breaks as minor with the break spelled out — there is no break to spell.
  • Matches. The changeset body also carries the alias widening at the three sites in words, which the card demanded.

③ Boundary flags

(a) Clause-②: yes against the ruling's expected no. Correct re-derivation. The criterion (pm-dispatch SKILL.md line 547) is two-limbed — "改变契约接受/拒绝行为 扩大公开面". Limb 1 is not met (no schema, validator or refusal moves; the gate's judged/declined counters are unchanged). Limb 2 is met: a package-internal symbol is now on the public entry of a package with a 45-package downstream closure. The director's line was explicitly "re-declare from the diff"; a no here would be a falsifiable false declaration. Stands. One carrier problem, though: the checker that landing precondition ② runs (objectstack/scripts/pm/check-clause2-carriers.mjs --pair) reads the card side from claim-marked comments in the fixed spelling; I ran its parser — the PR body's **Clause-②: yes** — … line reads declared: yes, but no comment on #5935 carries a Clause-②: line at line start (the only occurrences are mid-line prose and inside the os-dev-report JSON), and the seat's claim comments do not match CLAIM_COMMENT_MARKER (/^\s*>?\s*Claim(?:ed)?\s*:/) either. The card limb therefore reads absent, which that tool classifies as adverse (exit 4) — "⛔ 不作干净". See amendment 3.

(b) #7472 held out. Defensible. Ruling 2 says so in terms ("⛔ Out of scope here; its absence from DECLARED_RECORD_READERS is correct, not an omission"); ruling point 4 governs new containers, and the generator's DynamicIcon predates it, is emitted into a different tree, and reads a third vocabulary (named exports, zero normalisation) — routing it through the seam would change what generated apps accept and needs its own ruling. Not widening the discovery predicate is consistent with #7472's own text. But the PR over-claims the enforcement. I added a probe file in three shapes and ran the gate: named icons import + direct element access → red ("UNDECLARED record-reading resolver", exit 1); import * as L …; L.icons[k] in executed code → green; named import re-bound through a second const and indexed → green. So "a container that hand-rolls a lookup turns the gate red on the commit that adds it" (.mjsDECLARED_RECORD_READERS comment, PR body) holds for one syntactic shape. The seam's docblock states the predicate accurately ("named-imports lucide's icons record and indexes it"); the gate comment does not. See amendment 2.

(c) The two-copies pin has a hole. The new gate-test row "normalises exactly as the ONE resolver does, read from its source" asserts (i) the resolver's text contains .split(/[-_\s]+/) and not .split('-'), (ii) the map entries extracted from the text equal {Home:'House'}, and (iii) the gate's own toRecordKey on six samples. It never executes the resolver. Probe: I widened only the gate's tokeniser to split(/[-_.\s]+/)all 40 gate tests stayed green, including that row. So the claim in the .mjs Normalisation comment, "a change to either side that is not made to the other fails loudly", is false in the gate-wider direction — which is exactly the under-reporting class the card closes (the gate would judge a name live that the resolver misses). The unit vitest project already imports TypeScript straight from packages/ (scripts/__tests__/known-schema-types-derivation-5115.test.tspackages/cli/src/utils/known-schema-types.js), so a behavioural pin is feasible without a build. See amendment 1.

Pins are not vacuous — confirmed

  • Each fallback describe opens with a CONTROL asserting a positive render (getByText('Line Items'), getByTestId('view-tab-open'), getAllByText('New Task')), plus a CONTROL that a resolvable name draws that glyph and not the fallback; every fallback row asserts svg.lucide-inbox / the chip text positively, not merely an absence.
  • Ablation reproduced in my worktree: drop ?? Inbox in RelatedList.tsx → 4 red including "CONTROL — the section renders, with its title" (the readers render <SectionIcon/> unconditionally, so a null throws; a component that fails to render cannot pass these); drop it in ListView.tsx → 2 red. Tree verified clean after each restore.
  • Seam suite discriminates (file-text non-null, not-a-real-icon null, edit null against the retired named export) and re-implements the narrow tokeniser in-test with a carried > 1000 guard against a silently empty loop. The ActionPreview spy row asserts toHaveBeenCalled() before reading mock.results (the blind-instrument trap avoided).

CI, by name

29 success: Changeset Bump Policy, Changeset Overwrite Report, Changeset Declaration, Changeset Fixed Group Check, Lint (5 min, real), Type Check (9 min, real), Test shard 1/4, 2/4, 3/4, 4/4 (10–14 min each, real; the vitest include globs cover the five new packages/**/*.test.tsx and scripts/**/*.test.ts, and I reran them locally), Build & E2E (log read: 14 Playwright passed / 23 skipped, 69 s explained by a pnpm cache hit — not a path-skip), Bundle Analysis (eager closure 3178.8 / 3191.4 KB — passes with 12.6 KB headroom; note only), README Export Check, Skill Example Check, Skill Guide Path Check, Skill Eval Token Check, Control Byte Scan, Shell Escape Residue Scan, Inert vi.mock Specifier Check, Pre-Install Import Graph Check, Governed Surface Queue Guard, Docs Route Eager Closure Check, Doc Component Type Check, Doc Snippet Type Check, Doc Fence Language Check, Build Docs, Internal Docs Link Check, Live E2E (informational), label. 3 skipped by design: Test (coverage) and Test (coverage shard) run on push only; dependabot.

Base behind main:base.sha c4f541df9; branch point e307c9896; origin/main is 7 commits past the branch point (3 past base). Overlap of those commits' files with the PR's 16: none; no new icons import from lucide-react anywhere on main since the branch point; and I ran the gate and all 7 suites on an actual local merge of head + origin/main — gate OK (1 resolver), 94/94. No reading is invalidated.

Verdict

PASS WITH REQUIRED AMENDMENTS

  1. Make the two-copies pin behavioural (scripts/__tests__/check-lucide-icon-record-names.test.ts). Import describeIconLookup (or resolveIcon) from packages/components/src/renderers/action/resolve-icon.ts and assert toRecordKey(s) === describeIconLookup(s).key for every record key under the enumeration's re-spellings (kebab/snake/spaced/Pascal/lower/doubled), plus a negative control; keep or drop the source-text rows as you like. Then either make the .mjs Normalisation comment's "either side … fails loudly" true or reword it. Proof it is needed: gate-only widening to /[-_.\s]+/ passed 40/40.
  2. State the enforcement bound at DECLARED_RECORD_READERS in scripts/check-lucide-icon-record-names.mjs. Replace "a new container that hand-rolls a lookup turns this gate red" with the predicate it actually holds (named icons import from 'lucide-react' + direct element access on that binding; namespace-import, re-bound and template-literal forms are outside it), and cite finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472 as the known outside-predicate resolver. Widening discovery is a separate design question and is not asked for here.
  3. Card-side Clause-② carrier (process, no code). Post on Consolidate the eight lucide record-reading icon resolvers into one seam — three tokenisers and a 5-of-8 alias map mean the same authored name renders on one surface and not another #5935 a claim-shaped comment (first line matching Claim:) carrying Clause-②: yes in the fixed spelling so check-clause2-carriers.mjs --pair 7491 reads both limbs consistent; today the card limb reads absent, and landing precondition ② cannot pass on it.

Not required, for the record: resolveIcon's parameter could accept null (non-breaking later); the not.toMatch(/\.split\('-'\)/) text row is fragile to a future comment; eager-closure headroom is 12.6 KB.


Seat disposition

Not landing.needs:contract-review stays on this PR until all three amendments are in and this seat verifies them by content. Amendments 1 and 2 are code and are being dispatched. Amendment 3 is a PM action on card #5935 and is this seat's own to discharge.

⭐ Amendment 3 is worth calling out: it is a machine-checkable landing precondition that this seat could not have caught itselfcheck-clause2-carriers.mjs cannot run in this session at all (exit 2, no GitHub token; MCP-only access), so the seat had fallen back to reading labels by hand, and a hand check cannot see a missing claim-line spelling. The reviewer ran the tool's parser directly and found the card limb reads absent. That is exactly the kind of finding an isolated reviewer at tier exists to produce.


Generated by Claude Code

…state the discovery bound (objectui#5935)
Both required amendments from the Clause-② contract review of PR #7491
(comment 5524970713). The seam, the call sites, the alias and the five new
suites are untouched.
Amendment 1 — the two-copies pin is now BEHAVIOURAL.
`scripts/__tests__/check-lucide-icon-record-names.test.ts` asserted only the
resolver's SOURCE TEXT, so it was blind to the gate moving on its own: the
resolver's text is then unchanged and every source-text row still passes. The
review measured it — widening ONLY `toRecordKey` to `split(/[-_.\s]+/)` left
the file green 40/40 — and that is the gate-wider direction, exactly the
under-reporting class this card exists to close.
The file now imports `describeIconLookup` from the resolver and asserts
`toRecordKey(s) === describeIconLookup(s).key` over a corpus derived from the
runtime record: every one of the 1,767 live keys re-spelled six ways, plus a
per-character separator sweep. The sweep is not decoration — no key of the
record contains a separator at all, so re-spellings alone cannot see a
character-class widening, which is why the measured hole survived them. A
second row is the control that the corpus can in fact separate the two,
modelling the drifted gate by substitution on the shipped `toRecordKey` rather
than transcribing the rule a third time. The source-text rows are kept; they
catch a different thing.
TypeScript is imported straight out of `packages/` the way
`known-schema-types-derivation-5115.test.ts` already does, so the pin costs no
build step, and by a RELATIVE path so `describeIconLookup` stays off the
package entry and the published widening stays at one symbol.
Amendment 2 — the enforcement bound is stated instead of over-claimed.
The `DECLARED_RECORD_READERS` comment said a new container that hand-rolls a
lookup turns the gate red. It holds for one syntactic shape. Measured against
this gate: named `icons` import (rename included) plus element access on that
binding, through parens/`as`/`!`, any argument expression — red. Namespace
import, a binding re-bound through another const, property access,
destructuring, `require()`/dynamic `import()`, and anything inside a string or
template literal — green. objectui#7472 is cited as the known outside-predicate
resolver and is outside three ways at once. The discovery predicate is ⛔ NOT
widened; that is a separate design question.
The Normalisation comment's "either side ... fails loudly" is likewise made
true rather than reworded away: it now names the behavioural row that carries
it and records that the source-text rows do not.
Verification: gate exits 0 at 1 record-reading resolver; the 7 relevant suites
96 green (94 before, +2 rows); ablation red/green with the restore proven by
state; `type-check:scripts`, `lint:root` (0 errors on changed files) and
`check-control-bytes` green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3180.9 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DpQmpEDV.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.31KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.69KB21.03KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Amendments 1 and 2 applied — 089e35043. ⛔ Still draft, needs:contract-review untouched.

Against the contract review's PASS WITH REQUIRED AMENDMENTS (comment 5524970713). Amendment 3 is the PM seat's and was not touched. The mechanism — tokeniser, seam, Home to House alias, seven call sites, five new suites — is byte-unchanged; the commit touches two files, both under scripts/.

Amendment 1 — the two-copies pin is behavioural

scripts/__tests__/check-lucide-icon-record-names.test.ts now imports describeIconLookup from the resolver by a relative path (so it stays off the package entry and the published widening stays at one symbol) and asserts toRecordKey(s) equals describeIconLookup(s).key over a corpus derived from the runtime record: all 1,767 live keys re-spelled six ways, plus a per-character separator sweep. The source-text rows are kept.

The sweep is load-bearing, and the review's own prescription would not have worked without it. The amendment text lists the re-spellings (kebab/snake/spaced/Pascal/lower/doubled) as the corpus. Those alone cannot catch the measured hole: no key of the record contains a separator at all (0 of 1,767, control — 95 contain a digit), so widening the class by . changes nothing about any of them and the pin would have stayed green under the very mutation it was commissioned to catch. Measured before writing the row: 0 of 10,602 re-spellings differ under /[-_.\s]+/; 19 corpus members do once hostile separators are in. So the corpus sweeps the character class, not the one character.

The ablation, reproducing the review's probe. Implementation committed first, so the restore leg has a real reference point; mutation and restore both proven by state, never by exit code.

legreading
pre-mutation, on diskshipped .split(/[-_\s]+/) present 1, mutated form present 0
post-mutation, on diskshipped 0, mutated 1; blob 4985efe76 vs HEAD e1600563f; git diff HEAD numstat 1 1
gate script under the mutationexit 0, still "1 record-reading resolver" — the drift is invisible to the gate's own run
pin suite under the mutation2 failed, 40 passed (42) — the two new rows, and only those
restoregit checkout HEAD -- ABSOLUTE_PATH under trap ... EXIT INT TERM; git diff HEAD empty and blob back to e1600563f
green leg on the restored tree42 passed (42)

The 40 pre-existing rows staying green under the mutation is the review's finding reproduced exactly. The .mjs Normalisation comment is made true rather than reworded away: it now names the behavioural row that carries "either side fails loudly" and records that the source-text rows do not.

Amendment 2 — the enforcement bound is stated

Replaced at DECLARED_RECORD_READERS, and the same over-claim corrected in the PR body. Every line of the new comment is measured against discoverResolvers, not reasoned about — 19 syntactic shapes through the real predicate:

Inside (red): named icons import; a renamed named import ({ icons as reg }); (icons as Record)[k]; and any argument expression, a template literal included — the predicate never looks at the argument.
Outside (green, not caught): namespace import; a binding re-bound through another const; property access (icons.House); destructuring; require(); dynamic import(); and anything inside a string or template literal.

⚠️One correction to the amendment text, which I would rather state than quietly obey. It says "namespace-import, re-bound and template-literal forms are outside it". Element access with a template-literal argument is inside — measured red. What is outside is code emitted inside a template literal, which is #7472's first of three reasons. Both readings are defensible from the phrase, so the new comment spells out which is which rather than repeating the ambiguity. #7472 is cited, and it is outside three ways at once: app-generator.ts emits, inside a template literal, import * as LucideIcons from 'lucide-react' re-bound to a lucideIcons const and then indexed. ⛔ The discovery predicate is not widened.

Verification, on the pushed head

  • 7 relevant suites: 7 files, 96 tests passed (the review's 94, plus the 2 rows). This file alone 42, was 40.
  • Icon union re-run at 089e35043: 18 files, 184 tests passed — 182 plus the same 2. Re-run because a push after review invalidates a sha-stamped union reading.
  • Every other suite naming the edited gate script — entry-guard wiring, scripts type-check parity, breadcrumb / command / context-menu item icons, icon-renderer-declared-default, ViewSwitcher: 7 files, 62 tests passed.
  • node scripts/check-lucide-icon-record-names.mjsexit 0, OK lucide icon names: 182 authored/declared names reaching 1 record-reading resolver are live icons keys.
  • pnpm type-check:scripts — VERDICT command-exit 0. pnpm lint:root (the scope that owns scripts/) — 0 errors, 32 warnings, 0 of them in either changed file; all 32 are pre-existing no-explicit-any in e2e specs, two unrelated scripts/__tests__ files and two vitest setup files.
  • node scripts/check-control-bytes.mjs — OK, 6,158 tracked text files.

Exit codes were captured by redirecting first (cmd > log 2>&1; EXIT=$?), never after a pipe, and each verdict is quoted from the line the tool printed.

⚠️ Byte note: the separator sweep needs NBSP and the ideographic space. Both are written as / escapes — a literal one is a character no reviewer can see and no grep can be aimed at. Verified: 0 invisible or control characters in either changed file.

The changeset was checked and not edited: it makes no claim about the discovery predicate, and its "the under-reporting is closed rather than merely bounded" is what amendment 1 makes true.

⛔ Not done, deliberately: PR not flipped ready, no auto-merge, needs:contract-review left on. The package: components / plugin / tests labels appeared from the labeler, not from me.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

✅ Amendments verified — needs:contract-review cleared, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Provenance: 「PASS ⇒ 同席剥标、ready、auto-merge」 (维护者 2026-08-31 裁定). The tier review returned PASS WITH REQUIRED AMENDMENTS (5524970713); all three are now in.

Verified by content at 089e35043, not from the report

  • Scope: parent is the reviewed head 675571f8f; 2 files, both under scripts/. The seam, the seven call sites, the alias and the five new suites are byte-unchanged — confirmed by filtering the commit's file list for anything outside scripts/ and getting an empty set.
  • Amendment 1 — the pin is behavioural and cannot silently go vacuous. It imports describeIconLookup from the resolver and asserts toRecordKey(s) === describeIconLookup(s).key over 8,000+ spellings. ⭐ More than was asked: it carries its own non-vacuity control — it models a dot-splitting gate and asserts the corpus can separate the two, naming arrow.right and building.2. So a future corpus edit that quietly drops the hostile separators turns the suite red rather than passing in silence. That is strictly better than the one-off ablation the amendment prescribed, because it keeps holding after today.
  • Amendment 2 — the bound is stated, the predicate is NOT widened. The DECLARED_RECORD_READERS comment now names what is inside (named icons import, rename included, plus element access through parens / as / !, any argument expression) and what is outside (namespace import, re-bound const, property access, destructuring, require()/dynamic import(), code inside a string or template literal), and cites finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472. The PR body's matching sentence is corrected too.
  • Amendment 3 was discharged by this seat on the card (5524981959).
  • CI: 32/32 at 089e35043 — 29 success, 3 skipped by design. Test (shard 1/4) 11:52:22 · 2/4 11:52:16 · 3/4 11:54:14 · 4/4 11:54:17 · Type Check 11:50:22 · Lint 11:47:10 · Bundle Analysis, README Export Check, Governed Surface Queue Guard each read individually.

⭐ The implementer corrected the review, and it is the most valuable thing in this round

Amendment 1's prescribed corpus would not have reproduced its own proof. The review said to assert over the enumeration's re-spellings (kebab/snake/spaced/Pascal/lower/doubled). Measured: no key of the record contains a separator at all (0 of 1,767; control "contains a digit" fires at 95), so 0 of 10,602 re-spellings change under /[-_.\s]+/. A pin built to the amendment's letter would have stayed green under the exact mutation it was commissioned to catch.

The implementer added a per-character separator sweep instead — that is what makes it fire — and said so plainly rather than reporting compliance. ⇒ I ratify the deviation. Amendment 1's requirement was a pin that is behavioural and proven to fire; the letter of its corpus was insufficient for its own purpose, and following the letter would have produced a pin that certified nothing. Measured after: 2 of 42 red under that mutation, with the gate script itself still exiting 0 — which is exactly why the pin, not the gate, has to hold this.

Three further corrections from the same round, recorded because two of them are mine:

  1. ⚠️My brief said "template-literal forms are outside" the predicate — wrong as written. An element access whose argument is a template literal (icons[`${a}${b}`]) is inside; the predicate reads the base identifier and never the argument. What is outside is code emitted inside a template literal. The new comment spells out which is which instead of repeating the ambiguity. The implementer also measured that a renamed named import is inside — which the review did not mention.
  2. finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472's own stated third reason is misdescribed (in that issue, not here): it says the base is "a property access", where the generated code actually re-binds a namespace import through a const. Both are outside the predicate so the conclusion stands; only the reason is wrong. Left alone as out of scope.
  3. The changeset needed no edit — it makes no claim about the discovery predicate, and its "under-reporting closed rather than merely bounded" line is precisely what amendment 1 makes true.

An accepted, named gap — not an oversight

The seam's own docblock (packages/components/src/renderers/action/resolve-icon.ts) still ends its enforcement paragraph with the same unqualified sentence amendment 2 corrected in the .mjs. It is left as-is: the tier reviewer examined that docblock and judged it accurate in context (it is preceded by an accurate predicate sentence), and my dispatch scoped the seam out so the mechanism under review stayed byte-stable. ⇒ A one-sentence follow-up available any time; recorded here so it is a chosen gap rather than one nobody noticed.

Carriers

needs:contract-review removed from this PR (remaining: package: components, plugin, tests). Card #5935 never carried it — its labels are pm:dispatched, domain:ui. ⚠️ That is the same dual-carrier gap the reviewer found on the declaration side, from the other direction: the gate label was hung on one carrier only. I also set #5935's assignee, which was empty while this PR was open — a half-state that invites a second agent onto claimed work.

⚠️ Both readings are manual: scripts/pm/check-clause2-carriers.mjs exits 2 in this session (no GitHub token; MCP-only access), and its own output says that is not a clearance. A hand label-check covers strictly less than the tool — naming the instrument, as on every landing this shift.


Generated by Claude Code

os-sam pushed a commit that referenced this pull request Sep 4, 2026
…ecord the third zero
objectui#5936 asked for the consumer of a component registration's `icon` meta
before extending the icon gate to it. Three first-party populations were
measured and none has one (objectui, objectstack, cloud), so the gate is not
extended — adjudicated 2026-09-04.
Records that where the next reader hits it (the gate's own "Deliberate
boundaries"), with the three readings, their controls, the adjudication, and
the caveat that three zeros are not a proof of absence.
Retires the membership half of the `ui:icon` local pin, which was kept at
objectui#5633 on the premise that the palette lives outside this repo. It
guarded 1 of the 45 registrations declaring an `icon` meta while the gate is
adjudicated not to cover the other 44, and its hand-copied tokeniser had
already gone stale against the seam objectui#5935 / PR #7491 created. The
coupling half stays: it needs no external vocabulary and cannot drift.
The published `ComponentMeta.icon` key is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(components,app-shell,plugin-detail,plugin-list,plugin-view): one lucide icon-name seam, seven resolvers down to one - #7491

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-5935-one-icon-resolver-seam
Sep 3, 2026
Merged

refactor(components,app-shell,plugin-detail,plugin-list,plugin-view): one lucide icon-name seam, seven resolvers down to one#7491
os-project-manager merged 2 commits into
mainfrom
claude/issue-5935-one-icon-resolver-seam

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#5935

Seven modules resolved authored lucide icon names, each with its own copy of the logic. There is now one, resolveIcon, and the other six call it.

Both rulings on the card execute here as written:

  • 2026-08-31 (comment 5472612351, 维护者, verbatim 「同意」, option B) — one tokeniser, one iconNameMap, one seam; and point 4, 「本裁定后新容器 ⛔ 不得再自带解析器,一律走 seam」.
  • 2026-09-03 (comment 5523286738, maintainer, verbatim 「同意你的建议」, option C) — the seam does name -> component and nothing else. No onUnresolvable parameter. Each call site keeps its own fallback.

The census, re-measured before starting

Bound declared first, because this card has been handed a wrong number four times: all 4,101 git-tracked .ts/.tsx files, whole repo, no root subset — wider than the gate's own SCAN_ROOTS (3,945 of them). Re-run of the gate's own AST discovery over that wider set.

readingruled figure (at c1fe272ad)measured at my base e307c9896drift
record-reading resolvers77none
Home -> House alias4 of 74 of 7none
dynamic-surface readers44none
RECORD_READING_TYPES14 keys, 9 non-action14 keys, 9 non-actionnone
tokenisersT1 on 5, T2 on 1, T3 on 1T1 on 5, T2 on 1, T3 on 1none

Zero drift. The only difference from the ruling's measurement is the scanned-source count (3,945 vs 3,943 — two files added to the tree since).

What changed at each of the seven sites

sitehadnow
renderers/action/resolve-icon.tssplit('-') + aliasthe seam: split(/[-_\s]+/) + alias
renderers/basic/icon.tsxown toPascalCase + own map + own record indexcalls the seam; SquareDashed placeholder + warning byte-identical
app-shellpreviews/ActionPreview.tsxsplit(/[-_\s]+/) + inline ternarycalls the seam; keeps its 3-character name chip
plugin-detail/RelatedList.tsxsplit(/[-_\s]/), no aliasresolveIcon(name) ?? Inbox
plugin-list/ListView.tsxinline split('-'), no aliasresolveIcon(iconName) ?? Inbox
plugin-list/components/TabBar.tsxsplit('-'), no aliascalls the seam; keeps null
plugin-view/ViewSwitcher.tsxown toPascalCase + own mapcalls the seam; keeps null for views and its own `

resolveIcon is exported from @object-ui/components, the one package all five consumers already depend on — no new dependency edge.

The tokeniser was not a choice made in this PR

split(/[-_\s]+/) with the rename universal, established by the pre-dispatch enumeration the 2026-08-31 ruling required (comment 5522254814): regression set empty against the authored population, against an every-name-by-every-surface cross-product, and against a bound-free differential over 8,298 spellings derived from all 1,767 live record keys. split('-') regresses 4,748 name-surface pairs in that last reading and is not adoptable.

Widening only, and structurally so: no record key contains _, whitespace or - (0 of 1,767, with a firing control), so whenever the narrow tokeniser produced a live key the wide one produces the same key. The old resolving sets are strict subsets. Sixteen name-surface pairs start resolving; none stops. Stated in words in the changeset.

⭐ The load-bearing claim: zero user-visible change on all four unresolvable behaviours

Each of the four is pinned, and each pin is proven non-vacuous two ways — a control in the same run that asserts the opposite state, and a red-then-green ablation on the committed baseline 675571f8f.

behavioursurfacespinned in
nullseam, TabBar, ViewSwitcher (+ its own `
SquareDashed + warnbasic/icon.tsxthe existing #5631 pin, untouched, 13/13 green
Inbox glyphRelatedList (header and toolbar), ListView empty stateRelatedList.iconSeam-5935.test.tsx, icon-seam-fallbacks-5935.test.tsx
3-character name chipActionPreviewActionPreview.iconSeam-5935.test.tsx

The ablation, on the committed baseline, restore proven BY STATE (git diff HEAD empty and blob equality against the HEAD blob; trap ... EXIT INT TERM with absolute paths; restore pinned to git checkout HEAD -- path, never the bare form that reads back from the index). Each mutation was proven on disk in both directions — removed text gone, injected text present — before its run was read:

mutationrows that fired
seam tokeniser back to split('-')8 across all 5 new suites
the Home -> House entry disabled2, incl. the #5631 pin's own rename row
RelatedList: ?? Inbox dropped4
ListView: ?? Inbox dropped2
ActionPreview: chip emptied1
icon.tsx: placeholder glyph swapped out1, the #5631 placeholder row

⚠️The trap named on this card, caught in the act. Dropping RelatedList's ?? Inbox also turned its CONTROL row red — the component threw and drew nothing, exactly the "a test that passes because a component failed to render" failure this card warned about. That is why every fallback describe opens with a row proving the surface rendered and asserts its fallback positively, rather than only asserting an absence.

Two earlier ablation attempts were refused by the harness rather than reported, and are named so the numbers above are readable: injecting SquareDashedNOPE left the removed text present as a substring (both-directions probe unsatisfiable), and injecting the empty string made the injected-text probe degenerate. Both were re-run with clean anchors.

The gate, simplified in the same PR

scripts/check-lucide-icon-record-names.mjs: DECLARED_RECORD_READERS goes 7 to 1, and its normalisation stops being a widest-common approximation. Its verdict line, before and after:

before: OK lucide icon names: 182 authored/declared names reaching 7 record-reading resolvers are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).
after: OK lucide icon names: 182 authored/declared names reaching 1 record-reading resolver are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).

Every part-2 counter is identical across the two runs (148 judged, 22 descendant-judged, 239 declined, 34 anchored) — the census moved, what is judged did not. The under-reporting disclosed at #5932 is closed, not merely bounded: with one resolver there is nothing left to approximate.

Ruling point 4 ships as enforcement within a stated bound. Discovery is re-run from source every run and disagreement fails in both directions. ⚠️ Corrected here after the contract review measured the earlier wording as an over-claim: the predicate holds one syntactic shape — a named icons import from lucide-react (a rename counts) plus an element access on that binding, seen through parens, as casts and !, with any argument expression. Measured red on that shape; measured green, i.e. not caught, on a namespace import, a binding re-bound through another const, property access, destructuring, require()/dynamic import(), and anything inside a string or template literal. #7472 is the known outside-predicate resolver and is outside three ways at once. So a new container that hand-rolls a lookup in the conforming shape reds the commit that adds it; one written another way does not, and the ruling still binds it by review. The bound is now stated at DECLARED_RECORD_READERS itself. ⛔ The discovery predicate is not widened — that is a separate design question.

The two-copies pin is behavioural, not textual. The gate's .mjs normalisation and the resolver's TypeScript one are held together by importing describeIconLookup and asserting toRecordKey(s) equals describeIconLookup(s).key over a corpus derived from the runtime record — all 1,767 live keys re-spelled six ways, plus a per-character separator sweep — with a control that the corpus can in fact separate the two. The sweep is load-bearing: no record key contains a separator at all, so re-spellings alone cannot see a character-class widening. The source-text rows are kept; they catch a different thing. Measured need: widening only the gate to split(/[-_.\s]+/) left the suite green 40/40 before this, and reds 2 of 42 after it, with the gate script itself still exiting 0 under that mutation — which is exactly why the pin, not the gate, has to hold this.

Verification

  • New suites: 5 files, 40 tests. Union re-run at 089e35043 (the amendment commit): 18 files, 184 tests, all passing — the 5 new plus every pre-existing icon suite (ui:icon reads the SDUI identity key name as its lucide icon name, so any icon node that authors name renders nothing at all #5631's 13-test pin, finding(components): ui:button inlines its own copy of resolveIcon instead of importing it, so any future fix to the shared resolver silently misses it #5993's button dedupe pin, breadcrumb/command/context-menu/dropdown-menu item icons, the gate's own suite, now 42 tests, TabBar.i18nLabel, ListView.emptyStateFilteredCopy, ViewSwitcher, ActionPreview.locations). That is the 675571f8f reading of 182 plus the 2 rows the behavioural pin adds, and it is quoted from a run on the current head rather than carried forward.
  • pnpm exec turbo run type-check --filter='...@object-ui/components'66 tasks, all successful, over the 45-package DOWNSTREAM CONSUMER closure (prefix filter = consumers, the direction a contract change travels). The five new test files were confirmed present in each package's tsconfig.test.json program via --listFiles, so "type-check is green" actually covers them.
  • 13 derived gates green at 675571f8f: icon-record-names, control-bytes, vi-mock-specifiers, vi-mock-inherit, readme-exports, phantom-deps, self-import, entry-guard, side-effects-array, sdui-registration-pins, eager-closure, dist-completeness, docs-route-closure.
  • ESLint over the 15 changed files: 0 errors (3 introduced by the first draft were fixed: an unused LucideIcon import, an unused React import, and a react-hooks/static-components error on the ActionPreview call site, which now carries the same annotation the other five call sites do). 250 warnings, all pre-existing any / react-hooks noise on untouched lines of two 4,000-line files.
  • Changeset present, minor on the five packages whose source moved.

Scope

Clause-②: yes — and this is a deliberate re-derivation against the ruling's stated expectation of no. The 2026-09-03 ruling recorded «Clause-②: no expected (renderer-internal; re-declare from the diff)», which holds for the half it was about: no contract accept/reject behaviour moves, no schema, validator or refusal changes, nothing an author may write changes. But the diff widens a published surfaceresolveIcon is a new export from @object-ui/components's public entry, which the ruling's "renderer-internal" reading did not anticipate and which is unavoidable because the five other call sites live in five other packages. Declaring no here would have been an auditable false declaration, so it is yes with the reason attached.

#7472 stays out.packages/cli/src/utils/app-generator.ts:842 emits an eighth resolver inside a template literal, against lucide's named-export namespace, with zero normalisation. It is correctly absent from DECLARED_RECORD_READERS (it reads neither censused vocabulary), and the gate's discovery predicate is not widened to see it.

Nothing under content/docs/releases/ is touched. No gate threshold is raised or weakened.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… one lucide icon-name seam, seven resolvers down to one
Seven modules resolved authored icon names against lucide's runtime `icons`
record, each with its own copy: three tokenisers (`split('-')` on five,
`split(/[-_\s]/)` on one, `split(/[-_\s]+/)` on one) and the `Home` -> `House`
rename on four of seven. The same authored name therefore rendered on one
surface and not another. `resolveIcon` is now exported from
`@object-ui/components` and the other six call it.
The tokeniser is `split(/[-_\s]+/)` with the rename universal — measured, not
chosen: its regression set is empty against the authored population, against an
every-name-by-every-surface cross-product, and against a bound-free differential
over 8,298 spellings derived from all 1,767 live keys.
The seam answers `name -> component | null` and decides nothing else. Every call
site keeps its own fallback, at the call site: `ui:icon` its `SquareDashed`
placeholder and warning (objectui#5631, untouched), `RelatedList` and `ListView`
their `Inbox`, `ActionPreview` its name chip, the rest `null`.
`scripts/check-lucide-icon-record-names.mjs` is simplified in the same change:
its census goes from seven sites to one, and its normalisation stops being a
widest-common approximation of three disagreeing resolvers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.8 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-C0j--fB6.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.31KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.69KB21.03KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Seat bookkeeping + the Clause ② call. ⛔ NOT a contract review.

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Head 675571f8f.

⚖️ Open question RULED: A — the declaration stands as Clause ② YES.

The card recorded "Clause ②: no expected (renderer-internal; re-declare from the diff)". The dev re-derived it against the actual diff and declared yes, because resolveIcon is now on @object-ui/components' public entry. That is correct and it stands.

The criterion has two limbs — changes accept/reject behaviourorwidens the public surface. The second is met: a symbol that was package-internal is published, into a 45-package consumer closure. ⇒ needs:contract-review applied, PR stays draft, ⛔ not enqueued, and the project director seat lands it.

⭐ The reason I am not taking the softer reading: the card's own instruction was "re-declare from the diff", not "confirm the expectation". A no that any reviewer can falsify by reading one line of index.ts costs more than one extra gate pass — it devalues every other no in the ledger. The recorded expectation was a prediction, and predictions are what re-derivation exists to overturn.

⚠️ For the reviewing seat, the honest narrowing: no accept/reject behaviour moves. Nothing an author may write changes; no schema, validator or refusal is touched. The export exists solely so five sibling packages can stop duplicating a private function, and the diagnostics helper describeIconLookup was deliberately kept off the public entry to hold the widening at exactly one symbol — verified, see below.

Verified on the branch by content, ⛔ not from the report

probeorigin/mainbranch
onUnresolvable in the seam0 — the only hit is prose explaining it was deliberately not added
resolveIcon on the public entry01
describeIconLookup on the public entry0
⛔ FENCE — SquareDashed in basic/icon.tsx (#5631)33, unchanged
gate census entries82

⚠️ That last row is my probe's count, not the report's: the dev states 7 → 1 and my pattern matches one extra non-entry line on each side. The delta is identical (−6), and I am stating both rather than silently adopting either.

Call-site fallbacks intact and local, exactly as option C requires: RelatedList.tsx:222resolveIcon(name) ?? Inbox, ListView.tsx:3881resolveIcon(iconName) ?? Inbox, icon.tsx:121 through the seam into its own placeholder branch.

⭐ The trap this card named, caught in the act

Dropping RelatedList's ?? Inbox in the ablation turned its control row red too — the component threw and rendered nothing. That is precisely "a test that passes because a component failed to render", which the dispatch flagged because this repo produced exactly that failure earlier today. It is why every fallback block here opens with a render-proof row and asserts its fallback positively.

⭐ And two ablation attempts were refused by the harness rather than reported: injecting SquareDashedNOPE left the removed text present as a substring, so the both-directions probe was unsatisfiable; injecting the empty string made grep -cF '' match every line. Both were re-run with clean anchors and neither produced a reading that was used. A harness that declines to emit a number it cannot justify is worth more than the six ablation rows it did produce.

Two more worth the reviewer's eye

The census was re-derived over a WIDER bound than the gate's own. All 4,101 tracked .ts/.tsx files, against the gate's 3,945 SCAN_ROOTSzero drift from every ruled figure, with the detector control firing in the same run. A census is only as wide as its pathspec, and this one declared its bound and then exceeded the incumbent.

Ruling point 4 ships as mechanical enforcement, not a note. 「本裁定后新容器 ⛔ 不得再自带解析器」 is now held by the gate itself: it rediscovers record readers from source every run and fails in both directions, so a container that hand-rolls a lookup reds the commit that adds it.

Out-of-scope #7492 filed (the gate's data-table census entry names a module that reads no record); ⛔ deliberately not fixed in place despite the bounded-in-place conditions arguably holding — this card carries three scope fences and a drive-by edit to an unrelated table entry is what they exist to stop. #7472 confirmed still present, still correctly outside the census, and the discovery predicate ⛔ not widened to see it.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Clause-② in-seat contract review — PASS WITH REQUIRED AMENDMENTS. needs:contract-review stays on.

Commissioned by the domain:ui execution seat (PM session session_01EMrWaQw3XS5DxTHxp4yRyC) per the in-seat review route (维护者 2026-08-31 裁定: 交付后复核由派发席在席内完成,达档 PM 自审或派 fable 复核子任务皆可). This seat runs opus and is not at tier, so it commissioned an isolated reviewer rather than self-reviewing.

Independence: the reviewer was given only the card, the rulings and the PR — ⛔ not the dispatch order and not this seat's opinion. It discloses below that it encountered a seat bookkeeping comment carrying a Clause-② opinion while reading the PR thread, and did not use it. Recording that rather than hiding it.

Tier verification (维护者 2026-08-27 裁定 — a commissioned review is legitimate only if verified): every harness-stamped model field in the reviewer's transcript was read before adopting — 97 stamps, all claude-fable-5-1, zero other values, zero fallback/overload markers. CONTRACT_REVIEW_TIER is claude-fable-5 (objectstack/scripts/pm/dispatch-gates.mjs:8091); claude-fable-5-1 is at or above that floor — the same floor reading recorded on PR #7471. ⇒ Verified, so the verdict is adopted verbatim. ⛔ Nothing below is rewritten, abridged or polished by this seat; the only legitimate alternatives were verbatim adoption or wholesale voiding.


Contract review — objectui PR #7491 (head 675571f8f) against card #5935

Inputs read in full: the PR body; the complete diff (1,590 lines, all 16 files); issue #5935 body; comments 5472612351 (ruling 2026-08-31), 5523286738 (ruling 2026-09-03), 5522254814 (pre-dispatch enumeration), plus 5523075605 (director record) and 5522309927 (the blocker that produced ruling 2); all 32 check runs; issue #7472. Disclosure for independence: the PR's comment thread contains a seat bookkeeping comment (5523950630) that states a Clause-② opinion; I encountered it while reading the PR's comments and did not use it — every judgment below is derived from the diff and my own runs.

Own verification (detached worktree /home/user/objectui-review-7491 at 675571f8f, main checkout untouched, no stash): the 5 new suites + the #5631 pin + the gate suite = 7 files / 94 tests green; node scripts/check-lucide-icon-record-names.mjs exits 0 with "1 record-reading resolver"; the same on a local merge with current origin/main (7 commits past the branch point); an independent 70,974-spelling differential; two fallback ablations; one pin-hole probe; three enforcement-shape probes. Details under each item.

① Derived judgments — every accept-set / published-surface change the diff causes

Published surface

  1. @object-ui/components public entry (packages/components/src/index.ts) gains resolveIcon(name: string | undefined): LucideIcon | null. New symbol: package.jsonexports has only . and ./style.css, so renderers/action/resolve-icon.ts was unreachable by deep import before — the "new export" claim is correct. Right. Five sibling packages need it; duplicating it is the defect the card exists to end; precedent is hasDeclaredVisibilityGate on the same entry. Shape: return LucideIcon | null is the correct published shape under option C (caller must decide the fallback). One note, not a defect: the parameter rejects null although JSON-sourced icon: null is realistic; widening to string | null | undefined later is non-breaking.
  2. describeIconLookup is exported from the module but not from the package entry, and cannot be deep-imported. Right — the widening is held to exactly one symbol.
  3. Module-private resolveIcon/toPascalCase/iconNameMap removed from TabBar.tsx, ViewSwitcher.tsx, icon.tsx. Never exported from any package index (checked on origin/main). No surface change.
  4. Gate exports: DECLARED_RECORD_READERS 7 → 1; RECORD_READING_TYPES.icon.resolver and .view-switcher.resolver become "…resolve-icon.ts (via …)". The resolver string is consumed only in diagnostics (lines 667/693/784/846/960 of the .mjs), and six entries already used that form. Right.

Accept set (which authored names resolve), per surface — derived from the diff and measured

  1. Shared resolver (and every type routed through it: action:*, dropdown-menu, context-menu, breadcrumb, command, header-bar, button): tokeniser split('-')split(/[-_\s]+/). Widening only, right, and ruled (2026-08-31 「tokeniser 与 iconNameMap 全量收敛到一份」; the tokeniser itself fixed by the enumeration, "the implementer does not pick this").
  2. ui:icon (basic/icon.tsx): same widening; alias unchanged; placeholder + warn unchanged (ui:icon reads the SDUI identity key name as its lucide icon name, so any icon node that authors name renders nothing at all #5631 pin 13/13). One diagnostic-only consequence the PR does not mention: for a name that fails under both rules and contains _/space, the warning's lookup: "X" half now prints the wide-tokeniser Pascal form (FooBar where it printed Foo_bar). Console text only, not rendered. Acceptable.
  3. ActionPreview: was already split(/[-_\s]+/) + alias → no accept-set change, routing only (the test file says so honestly).
  4. RelatedList: split(/[-_\s]/) + no alias → wide + alias. Gains home/Home (and separator variants) → draws House where it drew Inbox. Behaviour change, user-visible, sanctioned: ruling 1 converges iconNameMap to one; enumeration §5 "the alias must be universal"; ruling 2 "Home→House applied universally"; card scope point 2 asks it be stated — it is, in words, in the changeset.
  5. ListView empty state: split('-') + no alias → wide + alias. Gains snake/space spellings and home. Same judgment.
  6. TabBar: split('-') + no alias → wide + alias. Gains snake/space and home (a glyph appears where none did). Same judgment.
  7. ViewSwitcher: split('-') + alias → wide + alias. Gains snake/space. Right.

The tokeniser attack (⚠ item). I transcribed all seven pre-PR resolvers verbatim and the new seam, and ran them over 70,974 spellings: every record key re-spelled as kebab/snake/spaced/Pascal/lower/UPPER, with leading, trailing, doubled and mixed separators of -, _, space, tab and NBSP, plus alias-specific hostiles (-home, home_, ho-me, home_house, …) and controls. Result: 212,163 name-surface pairs resolved before; 0 stop resolving; 0 resolve to a different component; 262,717 widenings; controls fired (building_2 disagreed across old sites exactly as the enumeration recorded; home likewise). Why no counter-example can exist: an empty token capitalises to '' and contributes nothing to the join, so split('-') and split(/-+/) are join-identical (this also makes .filter(Boolean) inert); and since no live key contains -, _ or whitespace (0 of 1,767, re-measured), any live key the narrow rule produced came from hyphen-only tokens, which the wide rule splits identically. The only alias is Home→House and Home is not a live key, so the alias can only add. The "widening only / strict subset" claim is correct.

Home → House at the three previously alias-free sites. It is a behaviour change, it is sanctioned by both rulings (see 8), it can change what a user sees (icon: "home" on a related-list header/toolbar button or a grid empty state: Inbox → House; on a tab: nothing → House), and it is stated as such in the changeset. Right.

The four unresolvable behaviours, per site — verified by reading the head files, not the description:

  • null: seam ?? null; TabBar.tsx:146/164, 212/225, 232/241 guard TabIcon && …; ViewSwitcher.tsxgetViewIcon returns resolveIcon(view.icon) when an icon is authored (authored-but-dead → no glyph, not the type default) and actions use resolveIcon(action.icon) || DEFAULT_VIEW_ACTION_ICONS[type] (:258). Old ViewSwitcher used || null; identical for component values. Preserved.
  • SquareDashed + warn: icon.tsx:121requested ? resolveIcon(requested) : undefined then if (!Icon); old value was undefined, new is null; nothing else reads Icon. Placeholder branch byte-identical. Preserved.
  • Inbox: RelatedList.tsx:222resolveIcon(name) ?? Inbox feeding both the header (:1322) and toolbar (:299, which still gates absent icons with action.icon ? … : null — a third behaviour in that file, pinned); ListView.tsx:3881resolveIcon(iconName) ?? Inbox. Preserved.
  • 3-character chip: ActionPreview.tsxif (Glyph) … else <span>{name.slice(0,3)}</span> unchanged. Preserved.

② Semver

  • Changeset: minor on components, app-shell, plugin-detail, plugin-list, plugin-view.
  • My level: @object-ui/componentsminor — a new public export, nothing removed. The four consumers: additive behaviour widening at their surfaces, no API change — minor is defensible, patch would be too; the distinction is moot because all five (and 35 others) are in the single fixed group in .changeset/config.json, so the family bumps minor on components' account regardless. Major: no — no name stops resolving (measured), no export removed; and this repo's policy (check-changeset-no-major.mjs) reserves major for the objectstack-synchronised bump, shipping objectui breaks as minor with the break spelled out — there is no break to spell.
  • Matches. The changeset body also carries the alias widening at the three sites in words, which the card demanded.

③ Boundary flags

(a) Clause-②: yes against the ruling's expected no. Correct re-derivation. The criterion (pm-dispatch SKILL.md line 547) is two-limbed — "改变契约接受/拒绝行为 扩大公开面". Limb 1 is not met (no schema, validator or refusal moves; the gate's judged/declined counters are unchanged). Limb 2 is met: a package-internal symbol is now on the public entry of a package with a 45-package downstream closure. The director's line was explicitly "re-declare from the diff"; a no here would be a falsifiable false declaration. Stands. One carrier problem, though: the checker that landing precondition ② runs (objectstack/scripts/pm/check-clause2-carriers.mjs --pair) reads the card side from claim-marked comments in the fixed spelling; I ran its parser — the PR body's **Clause-②: yes** — … line reads declared: yes, but no comment on #5935 carries a Clause-②: line at line start (the only occurrences are mid-line prose and inside the os-dev-report JSON), and the seat's claim comments do not match CLAIM_COMMENT_MARKER (/^\s*>?\s*Claim(?:ed)?\s*:/) either. The card limb therefore reads absent, which that tool classifies as adverse (exit 4) — "⛔ 不作干净". See amendment 3.

(b) #7472 held out. Defensible. Ruling 2 says so in terms ("⛔ Out of scope here; its absence from DECLARED_RECORD_READERS is correct, not an omission"); ruling point 4 governs new containers, and the generator's DynamicIcon predates it, is emitted into a different tree, and reads a third vocabulary (named exports, zero normalisation) — routing it through the seam would change what generated apps accept and needs its own ruling. Not widening the discovery predicate is consistent with #7472's own text. But the PR over-claims the enforcement. I added a probe file in three shapes and ran the gate: named icons import + direct element access → red ("UNDECLARED record-reading resolver", exit 1); import * as L …; L.icons[k] in executed code → green; named import re-bound through a second const and indexed → green. So "a container that hand-rolls a lookup turns the gate red on the commit that adds it" (.mjsDECLARED_RECORD_READERS comment, PR body) holds for one syntactic shape. The seam's docblock states the predicate accurately ("named-imports lucide's icons record and indexes it"); the gate comment does not. See amendment 2.

(c) The two-copies pin has a hole. The new gate-test row "normalises exactly as the ONE resolver does, read from its source" asserts (i) the resolver's text contains .split(/[-_\s]+/) and not .split('-'), (ii) the map entries extracted from the text equal {Home:'House'}, and (iii) the gate's own toRecordKey on six samples. It never executes the resolver. Probe: I widened only the gate's tokeniser to split(/[-_.\s]+/)all 40 gate tests stayed green, including that row. So the claim in the .mjs Normalisation comment, "a change to either side that is not made to the other fails loudly", is false in the gate-wider direction — which is exactly the under-reporting class the card closes (the gate would judge a name live that the resolver misses). The unit vitest project already imports TypeScript straight from packages/ (scripts/__tests__/known-schema-types-derivation-5115.test.tspackages/cli/src/utils/known-schema-types.js), so a behavioural pin is feasible without a build. See amendment 1.

Pins are not vacuous — confirmed

  • Each fallback describe opens with a CONTROL asserting a positive render (getByText('Line Items'), getByTestId('view-tab-open'), getAllByText('New Task')), plus a CONTROL that a resolvable name draws that glyph and not the fallback; every fallback row asserts svg.lucide-inbox / the chip text positively, not merely an absence.
  • Ablation reproduced in my worktree: drop ?? Inbox in RelatedList.tsx → 4 red including "CONTROL — the section renders, with its title" (the readers render <SectionIcon/> unconditionally, so a null throws; a component that fails to render cannot pass these); drop it in ListView.tsx → 2 red. Tree verified clean after each restore.
  • Seam suite discriminates (file-text non-null, not-a-real-icon null, edit null against the retired named export) and re-implements the narrow tokeniser in-test with a carried > 1000 guard against a silently empty loop. The ActionPreview spy row asserts toHaveBeenCalled() before reading mock.results (the blind-instrument trap avoided).

CI, by name

29 success: Changeset Bump Policy, Changeset Overwrite Report, Changeset Declaration, Changeset Fixed Group Check, Lint (5 min, real), Type Check (9 min, real), Test shard 1/4, 2/4, 3/4, 4/4 (10–14 min each, real; the vitest include globs cover the five new packages/**/*.test.tsx and scripts/**/*.test.ts, and I reran them locally), Build & E2E (log read: 14 Playwright passed / 23 skipped, 69 s explained by a pnpm cache hit — not a path-skip), Bundle Analysis (eager closure 3178.8 / 3191.4 KB — passes with 12.6 KB headroom; note only), README Export Check, Skill Example Check, Skill Guide Path Check, Skill Eval Token Check, Control Byte Scan, Shell Escape Residue Scan, Inert vi.mock Specifier Check, Pre-Install Import Graph Check, Governed Surface Queue Guard, Docs Route Eager Closure Check, Doc Component Type Check, Doc Snippet Type Check, Doc Fence Language Check, Build Docs, Internal Docs Link Check, Live E2E (informational), label. 3 skipped by design: Test (coverage) and Test (coverage shard) run on push only; dependabot.

Base behind main:base.sha c4f541df9; branch point e307c9896; origin/main is 7 commits past the branch point (3 past base). Overlap of those commits' files with the PR's 16: none; no new icons import from lucide-react anywhere on main since the branch point; and I ran the gate and all 7 suites on an actual local merge of head + origin/main — gate OK (1 resolver), 94/94. No reading is invalidated.

Verdict

PASS WITH REQUIRED AMENDMENTS

  1. Make the two-copies pin behavioural (scripts/__tests__/check-lucide-icon-record-names.test.ts). Import describeIconLookup (or resolveIcon) from packages/components/src/renderers/action/resolve-icon.ts and assert toRecordKey(s) === describeIconLookup(s).key for every record key under the enumeration's re-spellings (kebab/snake/spaced/Pascal/lower/doubled), plus a negative control; keep or drop the source-text rows as you like. Then either make the .mjs Normalisation comment's "either side … fails loudly" true or reword it. Proof it is needed: gate-only widening to /[-_.\s]+/ passed 40/40.
  2. State the enforcement bound at DECLARED_RECORD_READERS in scripts/check-lucide-icon-record-names.mjs. Replace "a new container that hand-rolls a lookup turns this gate red" with the predicate it actually holds (named icons import from 'lucide-react' + direct element access on that binding; namespace-import, re-bound and template-literal forms are outside it), and cite finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472 as the known outside-predicate resolver. Widening discovery is a separate design question and is not asked for here.
  3. Card-side Clause-② carrier (process, no code). Post on Consolidate the eight lucide record-reading icon resolvers into one seam — three tokenisers and a 5-of-8 alias map mean the same authored name renders on one surface and not another #5935 a claim-shaped comment (first line matching Claim:) carrying Clause-②: yes in the fixed spelling so check-clause2-carriers.mjs --pair 7491 reads both limbs consistent; today the card limb reads absent, and landing precondition ② cannot pass on it.

Not required, for the record: resolveIcon's parameter could accept null (non-breaking later); the not.toMatch(/\.split\('-'\)/) text row is fragile to a future comment; eager-closure headroom is 12.6 KB.


Seat disposition

Not landing.needs:contract-review stays on this PR until all three amendments are in and this seat verifies them by content. Amendments 1 and 2 are code and are being dispatched. Amendment 3 is a PM action on card #5935 and is this seat's own to discharge.

⭐ Amendment 3 is worth calling out: it is a machine-checkable landing precondition that this seat could not have caught itselfcheck-clause2-carriers.mjs cannot run in this session at all (exit 2, no GitHub token; MCP-only access), so the seat had fallen back to reading labels by hand, and a hand check cannot see a missing claim-line spelling. The reviewer ran the tool's parser directly and found the card limb reads absent. That is exactly the kind of finding an isolated reviewer at tier exists to produce.


Generated by Claude Code

…state the discovery bound (objectui#5935)
Both required amendments from the Clause-② contract review of PR #7491
(comment 5524970713). The seam, the call sites, the alias and the five new
suites are untouched.
Amendment 1 — the two-copies pin is now BEHAVIOURAL.
`scripts/__tests__/check-lucide-icon-record-names.test.ts` asserted only the
resolver's SOURCE TEXT, so it was blind to the gate moving on its own: the
resolver's text is then unchanged and every source-text row still passes. The
review measured it — widening ONLY `toRecordKey` to `split(/[-_.\s]+/)` left
the file green 40/40 — and that is the gate-wider direction, exactly the
under-reporting class this card exists to close.
The file now imports `describeIconLookup` from the resolver and asserts
`toRecordKey(s) === describeIconLookup(s).key` over a corpus derived from the
runtime record: every one of the 1,767 live keys re-spelled six ways, plus a
per-character separator sweep. The sweep is not decoration — no key of the
record contains a separator at all, so re-spellings alone cannot see a
character-class widening, which is why the measured hole survived them. A
second row is the control that the corpus can in fact separate the two,
modelling the drifted gate by substitution on the shipped `toRecordKey` rather
than transcribing the rule a third time. The source-text rows are kept; they
catch a different thing.
TypeScript is imported straight out of `packages/` the way
`known-schema-types-derivation-5115.test.ts` already does, so the pin costs no
build step, and by a RELATIVE path so `describeIconLookup` stays off the
package entry and the published widening stays at one symbol.
Amendment 2 — the enforcement bound is stated instead of over-claimed.
The `DECLARED_RECORD_READERS` comment said a new container that hand-rolls a
lookup turns the gate red. It holds for one syntactic shape. Measured against
this gate: named `icons` import (rename included) plus element access on that
binding, through parens/`as`/`!`, any argument expression — red. Namespace
import, a binding re-bound through another const, property access,
destructuring, `require()`/dynamic `import()`, and anything inside a string or
template literal — green. objectui#7472 is cited as the known outside-predicate
resolver and is outside three ways at once. The discovery predicate is ⛔ NOT
widened; that is a separate design question.
The Normalisation comment's "either side ... fails loudly" is likewise made
true rather than reworded away: it now names the behavioural row that carries
it and records that the source-text rows do not.
Verification: gate exits 0 at 1 record-reading resolver; the 7 relevant suites
96 green (94 before, +2 rows); ablation red/green with the restore proven by
state; `type-check:scripts`, `lint:root` (0 errors on changed files) and
`check-control-bytes` green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3180.9 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DpQmpEDV.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.31KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.69KB21.03KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Amendments 1 and 2 applied — 089e35043. ⛔ Still draft, needs:contract-review untouched.

Against the contract review's PASS WITH REQUIRED AMENDMENTS (comment 5524970713). Amendment 3 is the PM seat's and was not touched. The mechanism — tokeniser, seam, Home to House alias, seven call sites, five new suites — is byte-unchanged; the commit touches two files, both under scripts/.

Amendment 1 — the two-copies pin is behavioural

scripts/__tests__/check-lucide-icon-record-names.test.ts now imports describeIconLookup from the resolver by a relative path (so it stays off the package entry and the published widening stays at one symbol) and asserts toRecordKey(s) equals describeIconLookup(s).key over a corpus derived from the runtime record: all 1,767 live keys re-spelled six ways, plus a per-character separator sweep. The source-text rows are kept.

The sweep is load-bearing, and the review's own prescription would not have worked without it. The amendment text lists the re-spellings (kebab/snake/spaced/Pascal/lower/doubled) as the corpus. Those alone cannot catch the measured hole: no key of the record contains a separator at all (0 of 1,767, control — 95 contain a digit), so widening the class by . changes nothing about any of them and the pin would have stayed green under the very mutation it was commissioned to catch. Measured before writing the row: 0 of 10,602 re-spellings differ under /[-_.\s]+/; 19 corpus members do once hostile separators are in. So the corpus sweeps the character class, not the one character.

The ablation, reproducing the review's probe. Implementation committed first, so the restore leg has a real reference point; mutation and restore both proven by state, never by exit code.

legreading
pre-mutation, on diskshipped .split(/[-_\s]+/) present 1, mutated form present 0
post-mutation, on diskshipped 0, mutated 1; blob 4985efe76 vs HEAD e1600563f; git diff HEAD numstat 1 1
gate script under the mutationexit 0, still "1 record-reading resolver" — the drift is invisible to the gate's own run
pin suite under the mutation2 failed, 40 passed (42) — the two new rows, and only those
restoregit checkout HEAD -- ABSOLUTE_PATH under trap ... EXIT INT TERM; git diff HEAD empty and blob back to e1600563f
green leg on the restored tree42 passed (42)

The 40 pre-existing rows staying green under the mutation is the review's finding reproduced exactly. The .mjs Normalisation comment is made true rather than reworded away: it now names the behavioural row that carries "either side fails loudly" and records that the source-text rows do not.

Amendment 2 — the enforcement bound is stated

Replaced at DECLARED_RECORD_READERS, and the same over-claim corrected in the PR body. Every line of the new comment is measured against discoverResolvers, not reasoned about — 19 syntactic shapes through the real predicate:

Inside (red): named icons import; a renamed named import ({ icons as reg }); (icons as Record)[k]; and any argument expression, a template literal included — the predicate never looks at the argument.
Outside (green, not caught): namespace import; a binding re-bound through another const; property access (icons.House); destructuring; require(); dynamic import(); and anything inside a string or template literal.

⚠️One correction to the amendment text, which I would rather state than quietly obey. It says "namespace-import, re-bound and template-literal forms are outside it". Element access with a template-literal argument is inside — measured red. What is outside is code emitted inside a template literal, which is #7472's first of three reasons. Both readings are defensible from the phrase, so the new comment spells out which is which rather than repeating the ambiguity. #7472 is cited, and it is outside three ways at once: app-generator.ts emits, inside a template literal, import * as LucideIcons from 'lucide-react' re-bound to a lucideIcons const and then indexed. ⛔ The discovery predicate is not widened.

Verification, on the pushed head

  • 7 relevant suites: 7 files, 96 tests passed (the review's 94, plus the 2 rows). This file alone 42, was 40.
  • Icon union re-run at 089e35043: 18 files, 184 tests passed — 182 plus the same 2. Re-run because a push after review invalidates a sha-stamped union reading.
  • Every other suite naming the edited gate script — entry-guard wiring, scripts type-check parity, breadcrumb / command / context-menu item icons, icon-renderer-declared-default, ViewSwitcher: 7 files, 62 tests passed.
  • node scripts/check-lucide-icon-record-names.mjsexit 0, OK lucide icon names: 182 authored/declared names reaching 1 record-reading resolver are live icons keys.
  • pnpm type-check:scripts — VERDICT command-exit 0. pnpm lint:root (the scope that owns scripts/) — 0 errors, 32 warnings, 0 of them in either changed file; all 32 are pre-existing no-explicit-any in e2e specs, two unrelated scripts/__tests__ files and two vitest setup files.
  • node scripts/check-control-bytes.mjs — OK, 6,158 tracked text files.

Exit codes were captured by redirecting first (cmd > log 2>&1; EXIT=$?), never after a pipe, and each verdict is quoted from the line the tool printed.

⚠️ Byte note: the separator sweep needs NBSP and the ideographic space. Both are written as / escapes — a literal one is a character no reviewer can see and no grep can be aimed at. Verified: 0 invisible or control characters in either changed file.

The changeset was checked and not edited: it makes no claim about the discovery predicate, and its "the under-reporting is closed rather than merely bounded" is what amendment 1 makes true.

⛔ Not done, deliberately: PR not flipped ready, no auto-merge, needs:contract-review left on. The package: components / plugin / tests labels appeared from the labeler, not from me.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

✅ Amendments verified — needs:contract-review cleared, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Provenance: 「PASS ⇒ 同席剥标、ready、auto-merge」 (维护者 2026-08-31 裁定). The tier review returned PASS WITH REQUIRED AMENDMENTS (5524970713); all three are now in.

Verified by content at 089e35043, not from the report

  • Scope: parent is the reviewed head 675571f8f; 2 files, both under scripts/. The seam, the seven call sites, the alias and the five new suites are byte-unchanged — confirmed by filtering the commit's file list for anything outside scripts/ and getting an empty set.
  • Amendment 1 — the pin is behavioural and cannot silently go vacuous. It imports describeIconLookup from the resolver and asserts toRecordKey(s) === describeIconLookup(s).key over 8,000+ spellings. ⭐ More than was asked: it carries its own non-vacuity control — it models a dot-splitting gate and asserts the corpus can separate the two, naming arrow.right and building.2. So a future corpus edit that quietly drops the hostile separators turns the suite red rather than passing in silence. That is strictly better than the one-off ablation the amendment prescribed, because it keeps holding after today.
  • Amendment 2 — the bound is stated, the predicate is NOT widened. The DECLARED_RECORD_READERS comment now names what is inside (named icons import, rename included, plus element access through parens / as / !, any argument expression) and what is outside (namespace import, re-bound const, property access, destructuring, require()/dynamic import(), code inside a string or template literal), and cites finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472. The PR body's matching sentence is corrected too.
  • Amendment 3 was discharged by this seat on the card (5524981959).
  • CI: 32/32 at 089e35043 — 29 success, 3 skipped by design. Test (shard 1/4) 11:52:22 · 2/4 11:52:16 · 3/4 11:54:14 · 4/4 11:54:17 · Type Check 11:50:22 · Lint 11:47:10 · Bundle Analysis, README Export Check, Governed Surface Queue Guard each read individually.

⭐ The implementer corrected the review, and it is the most valuable thing in this round

Amendment 1's prescribed corpus would not have reproduced its own proof. The review said to assert over the enumeration's re-spellings (kebab/snake/spaced/Pascal/lower/doubled). Measured: no key of the record contains a separator at all (0 of 1,767; control "contains a digit" fires at 95), so 0 of 10,602 re-spellings change under /[-_.\s]+/. A pin built to the amendment's letter would have stayed green under the exact mutation it was commissioned to catch.

The implementer added a per-character separator sweep instead — that is what makes it fire — and said so plainly rather than reporting compliance. ⇒ I ratify the deviation. Amendment 1's requirement was a pin that is behavioural and proven to fire; the letter of its corpus was insufficient for its own purpose, and following the letter would have produced a pin that certified nothing. Measured after: 2 of 42 red under that mutation, with the gate script itself still exiting 0 — which is exactly why the pin, not the gate, has to hold this.

Three further corrections from the same round, recorded because two of them are mine:

  1. ⚠️My brief said "template-literal forms are outside" the predicate — wrong as written. An element access whose argument is a template literal (icons[`${a}${b}`]) is inside; the predicate reads the base identifier and never the argument. What is outside is code emitted inside a template literal. The new comment spells out which is which instead of repeating the ambiguity. The implementer also measured that a renamed named import is inside — which the review did not mention.
  2. finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472's own stated third reason is misdescribed (in that issue, not here): it says the base is "a property access", where the generated code actually re-binds a namespace import through a const. Both are outside the predicate so the conclusion stands; only the reason is wrong. Left alone as out of scope.
  3. The changeset needed no edit — it makes no claim about the discovery predicate, and its "under-reporting closed rather than merely bounded" line is precisely what amendment 1 makes true.

An accepted, named gap — not an oversight

The seam's own docblock (packages/components/src/renderers/action/resolve-icon.ts) still ends its enforcement paragraph with the same unqualified sentence amendment 2 corrected in the .mjs. It is left as-is: the tier reviewer examined that docblock and judged it accurate in context (it is preceded by an accurate predicate sentence), and my dispatch scoped the seam out so the mechanism under review stayed byte-stable. ⇒ A one-sentence follow-up available any time; recorded here so it is a chosen gap rather than one nobody noticed.

Carriers

needs:contract-review removed from this PR (remaining: package: components, plugin, tests). Card #5935 never carried it — its labels are pm:dispatched, domain:ui. ⚠️ That is the same dual-carrier gap the reviewer found on the declaration side, from the other direction: the gate label was hung on one carrier only. I also set #5935's assignee, which was empty while this PR was open — a half-state that invites a second agent onto claimed work.

⚠️ Both readings are manual: scripts/pm/check-clause2-carriers.mjs exits 2 in this session (no GitHub token; MCP-only access), and its own output says that is not a clearance. A hand label-check covers strictly less than the tool — naming the instrument, as on every landing this shift.


Generated by Claude Code

os-sam pushed a commit that referenced this pull request Sep 4, 2026
…ecord the third zero
objectui#5936 asked for the consumer of a component registration's `icon` meta
before extending the icon gate to it. Three first-party populations were
measured and none has one (objectui, objectstack, cloud), so the gate is not
extended — adjudicated 2026-09-04.
Records that where the next reader hits it (the gate's own "Deliberate
boundaries"), with the three readings, their controls, the adjudication, and
the caveat that three zeros are not a proof of absence.
Retires the membership half of the `ui:icon` local pin, which was kept at
objectui#5633 on the premise that the palette lives outside this repo. It
guarded 1 of the 45 registrations declaring an `icon` meta while the gate is
adjudicated not to cover the other 44, and its hand-copied tokeniser had
already gone stale against the seam objectui#5935 / PR #7491 created. The
coupling half stays: it needs no external vocabulary and cannot drift.
The published `ComponentMeta.icon` key is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(components,app-shell,plugin-detail,plugin-list,plugin-view): one lucide icon-name seam, seven resolvers down to one - #7491

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-5935-one-icon-resolver-seam
Sep 3, 2026
Merged

refactor(components,app-shell,plugin-detail,plugin-list,plugin-view): one lucide icon-name seam, seven resolvers down to one#7491
os-project-manager merged 2 commits into
mainfrom
claude/issue-5935-one-icon-resolver-seam

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#5935

Seven modules resolved authored lucide icon names, each with its own copy of the logic. There is now one, resolveIcon, and the other six call it.

Both rulings on the card execute here as written:

  • 2026-08-31 (comment 5472612351, 维护者, verbatim 「同意」, option B) — one tokeniser, one iconNameMap, one seam; and point 4, 「本裁定后新容器 ⛔ 不得再自带解析器,一律走 seam」.
  • 2026-09-03 (comment 5523286738, maintainer, verbatim 「同意你的建议」, option C) — the seam does name -> component and nothing else. No onUnresolvable parameter. Each call site keeps its own fallback.

The census, re-measured before starting

Bound declared first, because this card has been handed a wrong number four times: all 4,101 git-tracked .ts/.tsx files, whole repo, no root subset — wider than the gate's own SCAN_ROOTS (3,945 of them). Re-run of the gate's own AST discovery over that wider set.

readingruled figure (at c1fe272ad)measured at my base e307c9896drift
record-reading resolvers77none
Home -> House alias4 of 74 of 7none
dynamic-surface readers44none
RECORD_READING_TYPES14 keys, 9 non-action14 keys, 9 non-actionnone
tokenisersT1 on 5, T2 on 1, T3 on 1T1 on 5, T2 on 1, T3 on 1none

Zero drift. The only difference from the ruling's measurement is the scanned-source count (3,945 vs 3,943 — two files added to the tree since).

What changed at each of the seven sites

sitehadnow
renderers/action/resolve-icon.tssplit('-') + aliasthe seam: split(/[-_\s]+/) + alias
renderers/basic/icon.tsxown toPascalCase + own map + own record indexcalls the seam; SquareDashed placeholder + warning byte-identical
app-shellpreviews/ActionPreview.tsxsplit(/[-_\s]+/) + inline ternarycalls the seam; keeps its 3-character name chip
plugin-detail/RelatedList.tsxsplit(/[-_\s]/), no aliasresolveIcon(name) ?? Inbox
plugin-list/ListView.tsxinline split('-'), no aliasresolveIcon(iconName) ?? Inbox
plugin-list/components/TabBar.tsxsplit('-'), no aliascalls the seam; keeps null
plugin-view/ViewSwitcher.tsxown toPascalCase + own mapcalls the seam; keeps null for views and its own `

resolveIcon is exported from @object-ui/components, the one package all five consumers already depend on — no new dependency edge.

The tokeniser was not a choice made in this PR

split(/[-_\s]+/) with the rename universal, established by the pre-dispatch enumeration the 2026-08-31 ruling required (comment 5522254814): regression set empty against the authored population, against an every-name-by-every-surface cross-product, and against a bound-free differential over 8,298 spellings derived from all 1,767 live record keys. split('-') regresses 4,748 name-surface pairs in that last reading and is not adoptable.

Widening only, and structurally so: no record key contains _, whitespace or - (0 of 1,767, with a firing control), so whenever the narrow tokeniser produced a live key the wide one produces the same key. The old resolving sets are strict subsets. Sixteen name-surface pairs start resolving; none stops. Stated in words in the changeset.

⭐ The load-bearing claim: zero user-visible change on all four unresolvable behaviours

Each of the four is pinned, and each pin is proven non-vacuous two ways — a control in the same run that asserts the opposite state, and a red-then-green ablation on the committed baseline 675571f8f.

behavioursurfacespinned in
nullseam, TabBar, ViewSwitcher (+ its own `
SquareDashed + warnbasic/icon.tsxthe existing #5631 pin, untouched, 13/13 green
Inbox glyphRelatedList (header and toolbar), ListView empty stateRelatedList.iconSeam-5935.test.tsx, icon-seam-fallbacks-5935.test.tsx
3-character name chipActionPreviewActionPreview.iconSeam-5935.test.tsx

The ablation, on the committed baseline, restore proven BY STATE (git diff HEAD empty and blob equality against the HEAD blob; trap ... EXIT INT TERM with absolute paths; restore pinned to git checkout HEAD -- path, never the bare form that reads back from the index). Each mutation was proven on disk in both directions — removed text gone, injected text present — before its run was read:

mutationrows that fired
seam tokeniser back to split('-')8 across all 5 new suites
the Home -> House entry disabled2, incl. the #5631 pin's own rename row
RelatedList: ?? Inbox dropped4
ListView: ?? Inbox dropped2
ActionPreview: chip emptied1
icon.tsx: placeholder glyph swapped out1, the #5631 placeholder row

⚠️The trap named on this card, caught in the act. Dropping RelatedList's ?? Inbox also turned its CONTROL row red — the component threw and drew nothing, exactly the "a test that passes because a component failed to render" failure this card warned about. That is why every fallback describe opens with a row proving the surface rendered and asserts its fallback positively, rather than only asserting an absence.

Two earlier ablation attempts were refused by the harness rather than reported, and are named so the numbers above are readable: injecting SquareDashedNOPE left the removed text present as a substring (both-directions probe unsatisfiable), and injecting the empty string made the injected-text probe degenerate. Both were re-run with clean anchors.

The gate, simplified in the same PR

scripts/check-lucide-icon-record-names.mjs: DECLARED_RECORD_READERS goes 7 to 1, and its normalisation stops being a widest-common approximation. Its verdict line, before and after:

before: OK lucide icon names: 182 authored/declared names reaching 7 record-reading resolvers are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).
after: OK lucide icon names: 182 authored/declared names reaching 1 record-reading resolver are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).

Every part-2 counter is identical across the two runs (148 judged, 22 descendant-judged, 239 declined, 34 anchored) — the census moved, what is judged did not. The under-reporting disclosed at #5932 is closed, not merely bounded: with one resolver there is nothing left to approximate.

Ruling point 4 ships as enforcement within a stated bound. Discovery is re-run from source every run and disagreement fails in both directions. ⚠️ Corrected here after the contract review measured the earlier wording as an over-claim: the predicate holds one syntactic shape — a named icons import from lucide-react (a rename counts) plus an element access on that binding, seen through parens, as casts and !, with any argument expression. Measured red on that shape; measured green, i.e. not caught, on a namespace import, a binding re-bound through another const, property access, destructuring, require()/dynamic import(), and anything inside a string or template literal. #7472 is the known outside-predicate resolver and is outside three ways at once. So a new container that hand-rolls a lookup in the conforming shape reds the commit that adds it; one written another way does not, and the ruling still binds it by review. The bound is now stated at DECLARED_RECORD_READERS itself. ⛔ The discovery predicate is not widened — that is a separate design question.

The two-copies pin is behavioural, not textual. The gate's .mjs normalisation and the resolver's TypeScript one are held together by importing describeIconLookup and asserting toRecordKey(s) equals describeIconLookup(s).key over a corpus derived from the runtime record — all 1,767 live keys re-spelled six ways, plus a per-character separator sweep — with a control that the corpus can in fact separate the two. The sweep is load-bearing: no record key contains a separator at all, so re-spellings alone cannot see a character-class widening. The source-text rows are kept; they catch a different thing. Measured need: widening only the gate to split(/[-_.\s]+/) left the suite green 40/40 before this, and reds 2 of 42 after it, with the gate script itself still exiting 0 under that mutation — which is exactly why the pin, not the gate, has to hold this.

Verification

  • New suites: 5 files, 40 tests. Union re-run at 089e35043 (the amendment commit): 18 files, 184 tests, all passing — the 5 new plus every pre-existing icon suite (ui:icon reads the SDUI identity key name as its lucide icon name, so any icon node that authors name renders nothing at all #5631's 13-test pin, finding(components): ui:button inlines its own copy of resolveIcon instead of importing it, so any future fix to the shared resolver silently misses it #5993's button dedupe pin, breadcrumb/command/context-menu/dropdown-menu item icons, the gate's own suite, now 42 tests, TabBar.i18nLabel, ListView.emptyStateFilteredCopy, ViewSwitcher, ActionPreview.locations). That is the 675571f8f reading of 182 plus the 2 rows the behavioural pin adds, and it is quoted from a run on the current head rather than carried forward.
  • pnpm exec turbo run type-check --filter='...@object-ui/components'66 tasks, all successful, over the 45-package DOWNSTREAM CONSUMER closure (prefix filter = consumers, the direction a contract change travels). The five new test files were confirmed present in each package's tsconfig.test.json program via --listFiles, so "type-check is green" actually covers them.
  • 13 derived gates green at 675571f8f: icon-record-names, control-bytes, vi-mock-specifiers, vi-mock-inherit, readme-exports, phantom-deps, self-import, entry-guard, side-effects-array, sdui-registration-pins, eager-closure, dist-completeness, docs-route-closure.
  • ESLint over the 15 changed files: 0 errors (3 introduced by the first draft were fixed: an unused LucideIcon import, an unused React import, and a react-hooks/static-components error on the ActionPreview call site, which now carries the same annotation the other five call sites do). 250 warnings, all pre-existing any / react-hooks noise on untouched lines of two 4,000-line files.
  • Changeset present, minor on the five packages whose source moved.

Scope

Clause-②: yes — and this is a deliberate re-derivation against the ruling's stated expectation of no. The 2026-09-03 ruling recorded «Clause-②: no expected (renderer-internal; re-declare from the diff)», which holds for the half it was about: no contract accept/reject behaviour moves, no schema, validator or refusal changes, nothing an author may write changes. But the diff widens a published surfaceresolveIcon is a new export from @object-ui/components's public entry, which the ruling's "renderer-internal" reading did not anticipate and which is unavoidable because the five other call sites live in five other packages. Declaring no here would have been an auditable false declaration, so it is yes with the reason attached.

#7472 stays out.packages/cli/src/utils/app-generator.ts:842 emits an eighth resolver inside a template literal, against lucide's named-export namespace, with zero normalisation. It is correctly absent from DECLARED_RECORD_READERS (it reads neither censused vocabulary), and the gate's discovery predicate is not widened to see it.

Nothing under content/docs/releases/ is touched. No gate threshold is raised or weakened.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… one lucide icon-name seam, seven resolvers down to one
Seven modules resolved authored icon names against lucide's runtime `icons`
record, each with its own copy: three tokenisers (`split('-')` on five,
`split(/[-_\s]/)` on one, `split(/[-_\s]+/)` on one) and the `Home` -> `House`
rename on four of seven. The same authored name therefore rendered on one
surface and not another. `resolveIcon` is now exported from
`@object-ui/components` and the other six call it.
The tokeniser is `split(/[-_\s]+/)` with the rename universal — measured, not
chosen: its regression set is empty against the authored population, against an
every-name-by-every-surface cross-product, and against a bound-free differential
over 8,298 spellings derived from all 1,767 live keys.
The seam answers `name -> component | null` and decides nothing else. Every call
site keeps its own fallback, at the call site: `ui:icon` its `SquareDashed`
placeholder and warning (objectui#5631, untouched), `RelatedList` and `ListView`
their `Inbox`, `ActionPreview` its name chip, the rest `null`.
`scripts/check-lucide-icon-record-names.mjs` is simplified in the same change:
its census goes from seven sites to one, and its normalisation stops being a
widest-common approximation of three disagreeing resolvers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.8 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-C0j--fB6.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.31KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.69KB21.03KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Seat bookkeeping + the Clause ② call. ⛔ NOT a contract review.

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Head 675571f8f.

⚖️ Open question RULED: A — the declaration stands as Clause ② YES.

The card recorded "Clause ②: no expected (renderer-internal; re-declare from the diff)". The dev re-derived it against the actual diff and declared yes, because resolveIcon is now on @object-ui/components' public entry. That is correct and it stands.

The criterion has two limbs — changes accept/reject behaviourorwidens the public surface. The second is met: a symbol that was package-internal is published, into a 45-package consumer closure. ⇒ needs:contract-review applied, PR stays draft, ⛔ not enqueued, and the project director seat lands it.

⭐ The reason I am not taking the softer reading: the card's own instruction was "re-declare from the diff", not "confirm the expectation". A no that any reviewer can falsify by reading one line of index.ts costs more than one extra gate pass — it devalues every other no in the ledger. The recorded expectation was a prediction, and predictions are what re-derivation exists to overturn.

⚠️ For the reviewing seat, the honest narrowing: no accept/reject behaviour moves. Nothing an author may write changes; no schema, validator or refusal is touched. The export exists solely so five sibling packages can stop duplicating a private function, and the diagnostics helper describeIconLookup was deliberately kept off the public entry to hold the widening at exactly one symbol — verified, see below.

Verified on the branch by content, ⛔ not from the report

probeorigin/mainbranch
onUnresolvable in the seam0 — the only hit is prose explaining it was deliberately not added
resolveIcon on the public entry01
describeIconLookup on the public entry0
⛔ FENCE — SquareDashed in basic/icon.tsx (#5631)33, unchanged
gate census entries82

⚠️ That last row is my probe's count, not the report's: the dev states 7 → 1 and my pattern matches one extra non-entry line on each side. The delta is identical (−6), and I am stating both rather than silently adopting either.

Call-site fallbacks intact and local, exactly as option C requires: RelatedList.tsx:222resolveIcon(name) ?? Inbox, ListView.tsx:3881resolveIcon(iconName) ?? Inbox, icon.tsx:121 through the seam into its own placeholder branch.

⭐ The trap this card named, caught in the act

Dropping RelatedList's ?? Inbox in the ablation turned its control row red too — the component threw and rendered nothing. That is precisely "a test that passes because a component failed to render", which the dispatch flagged because this repo produced exactly that failure earlier today. It is why every fallback block here opens with a render-proof row and asserts its fallback positively.

⭐ And two ablation attempts were refused by the harness rather than reported: injecting SquareDashedNOPE left the removed text present as a substring, so the both-directions probe was unsatisfiable; injecting the empty string made grep -cF '' match every line. Both were re-run with clean anchors and neither produced a reading that was used. A harness that declines to emit a number it cannot justify is worth more than the six ablation rows it did produce.

Two more worth the reviewer's eye

The census was re-derived over a WIDER bound than the gate's own. All 4,101 tracked .ts/.tsx files, against the gate's 3,945 SCAN_ROOTSzero drift from every ruled figure, with the detector control firing in the same run. A census is only as wide as its pathspec, and this one declared its bound and then exceeded the incumbent.

Ruling point 4 ships as mechanical enforcement, not a note. 「本裁定后新容器 ⛔ 不得再自带解析器」 is now held by the gate itself: it rediscovers record readers from source every run and fails in both directions, so a container that hand-rolls a lookup reds the commit that adds it.

Out-of-scope #7492 filed (the gate's data-table census entry names a module that reads no record); ⛔ deliberately not fixed in place despite the bounded-in-place conditions arguably holding — this card carries three scope fences and a drive-by edit to an unrelated table entry is what they exist to stop. #7472 confirmed still present, still correctly outside the census, and the discovery predicate ⛔ not widened to see it.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Clause-② in-seat contract review — PASS WITH REQUIRED AMENDMENTS. needs:contract-review stays on.

Commissioned by the domain:ui execution seat (PM session session_01EMrWaQw3XS5DxTHxp4yRyC) per the in-seat review route (维护者 2026-08-31 裁定: 交付后复核由派发席在席内完成,达档 PM 自审或派 fable 复核子任务皆可). This seat runs opus and is not at tier, so it commissioned an isolated reviewer rather than self-reviewing.

Independence: the reviewer was given only the card, the rulings and the PR — ⛔ not the dispatch order and not this seat's opinion. It discloses below that it encountered a seat bookkeeping comment carrying a Clause-② opinion while reading the PR thread, and did not use it. Recording that rather than hiding it.

Tier verification (维护者 2026-08-27 裁定 — a commissioned review is legitimate only if verified): every harness-stamped model field in the reviewer's transcript was read before adopting — 97 stamps, all claude-fable-5-1, zero other values, zero fallback/overload markers. CONTRACT_REVIEW_TIER is claude-fable-5 (objectstack/scripts/pm/dispatch-gates.mjs:8091); claude-fable-5-1 is at or above that floor — the same floor reading recorded on PR #7471. ⇒ Verified, so the verdict is adopted verbatim. ⛔ Nothing below is rewritten, abridged or polished by this seat; the only legitimate alternatives were verbatim adoption or wholesale voiding.


Contract review — objectui PR #7491 (head 675571f8f) against card #5935

Inputs read in full: the PR body; the complete diff (1,590 lines, all 16 files); issue #5935 body; comments 5472612351 (ruling 2026-08-31), 5523286738 (ruling 2026-09-03), 5522254814 (pre-dispatch enumeration), plus 5523075605 (director record) and 5522309927 (the blocker that produced ruling 2); all 32 check runs; issue #7472. Disclosure for independence: the PR's comment thread contains a seat bookkeeping comment (5523950630) that states a Clause-② opinion; I encountered it while reading the PR's comments and did not use it — every judgment below is derived from the diff and my own runs.

Own verification (detached worktree /home/user/objectui-review-7491 at 675571f8f, main checkout untouched, no stash): the 5 new suites + the #5631 pin + the gate suite = 7 files / 94 tests green; node scripts/check-lucide-icon-record-names.mjs exits 0 with "1 record-reading resolver"; the same on a local merge with current origin/main (7 commits past the branch point); an independent 70,974-spelling differential; two fallback ablations; one pin-hole probe; three enforcement-shape probes. Details under each item.

① Derived judgments — every accept-set / published-surface change the diff causes

Published surface

  1. @object-ui/components public entry (packages/components/src/index.ts) gains resolveIcon(name: string | undefined): LucideIcon | null. New symbol: package.jsonexports has only . and ./style.css, so renderers/action/resolve-icon.ts was unreachable by deep import before — the "new export" claim is correct. Right. Five sibling packages need it; duplicating it is the defect the card exists to end; precedent is hasDeclaredVisibilityGate on the same entry. Shape: return LucideIcon | null is the correct published shape under option C (caller must decide the fallback). One note, not a defect: the parameter rejects null although JSON-sourced icon: null is realistic; widening to string | null | undefined later is non-breaking.
  2. describeIconLookup is exported from the module but not from the package entry, and cannot be deep-imported. Right — the widening is held to exactly one symbol.
  3. Module-private resolveIcon/toPascalCase/iconNameMap removed from TabBar.tsx, ViewSwitcher.tsx, icon.tsx. Never exported from any package index (checked on origin/main). No surface change.
  4. Gate exports: DECLARED_RECORD_READERS 7 → 1; RECORD_READING_TYPES.icon.resolver and .view-switcher.resolver become "…resolve-icon.ts (via …)". The resolver string is consumed only in diagnostics (lines 667/693/784/846/960 of the .mjs), and six entries already used that form. Right.

Accept set (which authored names resolve), per surface — derived from the diff and measured

  1. Shared resolver (and every type routed through it: action:*, dropdown-menu, context-menu, breadcrumb, command, header-bar, button): tokeniser split('-')split(/[-_\s]+/). Widening only, right, and ruled (2026-08-31 「tokeniser 与 iconNameMap 全量收敛到一份」; the tokeniser itself fixed by the enumeration, "the implementer does not pick this").
  2. ui:icon (basic/icon.tsx): same widening; alias unchanged; placeholder + warn unchanged (ui:icon reads the SDUI identity key name as its lucide icon name, so any icon node that authors name renders nothing at all #5631 pin 13/13). One diagnostic-only consequence the PR does not mention: for a name that fails under both rules and contains _/space, the warning's lookup: "X" half now prints the wide-tokeniser Pascal form (FooBar where it printed Foo_bar). Console text only, not rendered. Acceptable.
  3. ActionPreview: was already split(/[-_\s]+/) + alias → no accept-set change, routing only (the test file says so honestly).
  4. RelatedList: split(/[-_\s]/) + no alias → wide + alias. Gains home/Home (and separator variants) → draws House where it drew Inbox. Behaviour change, user-visible, sanctioned: ruling 1 converges iconNameMap to one; enumeration §5 "the alias must be universal"; ruling 2 "Home→House applied universally"; card scope point 2 asks it be stated — it is, in words, in the changeset.
  5. ListView empty state: split('-') + no alias → wide + alias. Gains snake/space spellings and home. Same judgment.
  6. TabBar: split('-') + no alias → wide + alias. Gains snake/space and home (a glyph appears where none did). Same judgment.
  7. ViewSwitcher: split('-') + alias → wide + alias. Gains snake/space. Right.

The tokeniser attack (⚠ item). I transcribed all seven pre-PR resolvers verbatim and the new seam, and ran them over 70,974 spellings: every record key re-spelled as kebab/snake/spaced/Pascal/lower/UPPER, with leading, trailing, doubled and mixed separators of -, _, space, tab and NBSP, plus alias-specific hostiles (-home, home_, ho-me, home_house, …) and controls. Result: 212,163 name-surface pairs resolved before; 0 stop resolving; 0 resolve to a different component; 262,717 widenings; controls fired (building_2 disagreed across old sites exactly as the enumeration recorded; home likewise). Why no counter-example can exist: an empty token capitalises to '' and contributes nothing to the join, so split('-') and split(/-+/) are join-identical (this also makes .filter(Boolean) inert); and since no live key contains -, _ or whitespace (0 of 1,767, re-measured), any live key the narrow rule produced came from hyphen-only tokens, which the wide rule splits identically. The only alias is Home→House and Home is not a live key, so the alias can only add. The "widening only / strict subset" claim is correct.

Home → House at the three previously alias-free sites. It is a behaviour change, it is sanctioned by both rulings (see 8), it can change what a user sees (icon: "home" on a related-list header/toolbar button or a grid empty state: Inbox → House; on a tab: nothing → House), and it is stated as such in the changeset. Right.

The four unresolvable behaviours, per site — verified by reading the head files, not the description:

  • null: seam ?? null; TabBar.tsx:146/164, 212/225, 232/241 guard TabIcon && …; ViewSwitcher.tsxgetViewIcon returns resolveIcon(view.icon) when an icon is authored (authored-but-dead → no glyph, not the type default) and actions use resolveIcon(action.icon) || DEFAULT_VIEW_ACTION_ICONS[type] (:258). Old ViewSwitcher used || null; identical for component values. Preserved.
  • SquareDashed + warn: icon.tsx:121requested ? resolveIcon(requested) : undefined then if (!Icon); old value was undefined, new is null; nothing else reads Icon. Placeholder branch byte-identical. Preserved.
  • Inbox: RelatedList.tsx:222resolveIcon(name) ?? Inbox feeding both the header (:1322) and toolbar (:299, which still gates absent icons with action.icon ? … : null — a third behaviour in that file, pinned); ListView.tsx:3881resolveIcon(iconName) ?? Inbox. Preserved.
  • 3-character chip: ActionPreview.tsxif (Glyph) … else <span>{name.slice(0,3)}</span> unchanged. Preserved.

② Semver

  • Changeset: minor on components, app-shell, plugin-detail, plugin-list, plugin-view.
  • My level: @object-ui/componentsminor — a new public export, nothing removed. The four consumers: additive behaviour widening at their surfaces, no API change — minor is defensible, patch would be too; the distinction is moot because all five (and 35 others) are in the single fixed group in .changeset/config.json, so the family bumps minor on components' account regardless. Major: no — no name stops resolving (measured), no export removed; and this repo's policy (check-changeset-no-major.mjs) reserves major for the objectstack-synchronised bump, shipping objectui breaks as minor with the break spelled out — there is no break to spell.
  • Matches. The changeset body also carries the alias widening at the three sites in words, which the card demanded.

③ Boundary flags

(a) Clause-②: yes against the ruling's expected no. Correct re-derivation. The criterion (pm-dispatch SKILL.md line 547) is two-limbed — "改变契约接受/拒绝行为 扩大公开面". Limb 1 is not met (no schema, validator or refusal moves; the gate's judged/declined counters are unchanged). Limb 2 is met: a package-internal symbol is now on the public entry of a package with a 45-package downstream closure. The director's line was explicitly "re-declare from the diff"; a no here would be a falsifiable false declaration. Stands. One carrier problem, though: the checker that landing precondition ② runs (objectstack/scripts/pm/check-clause2-carriers.mjs --pair) reads the card side from claim-marked comments in the fixed spelling; I ran its parser — the PR body's **Clause-②: yes** — … line reads declared: yes, but no comment on #5935 carries a Clause-②: line at line start (the only occurrences are mid-line prose and inside the os-dev-report JSON), and the seat's claim comments do not match CLAIM_COMMENT_MARKER (/^\s*>?\s*Claim(?:ed)?\s*:/) either. The card limb therefore reads absent, which that tool classifies as adverse (exit 4) — "⛔ 不作干净". See amendment 3.

(b) #7472 held out. Defensible. Ruling 2 says so in terms ("⛔ Out of scope here; its absence from DECLARED_RECORD_READERS is correct, not an omission"); ruling point 4 governs new containers, and the generator's DynamicIcon predates it, is emitted into a different tree, and reads a third vocabulary (named exports, zero normalisation) — routing it through the seam would change what generated apps accept and needs its own ruling. Not widening the discovery predicate is consistent with #7472's own text. But the PR over-claims the enforcement. I added a probe file in three shapes and ran the gate: named icons import + direct element access → red ("UNDECLARED record-reading resolver", exit 1); import * as L …; L.icons[k] in executed code → green; named import re-bound through a second const and indexed → green. So "a container that hand-rolls a lookup turns the gate red on the commit that adds it" (.mjsDECLARED_RECORD_READERS comment, PR body) holds for one syntactic shape. The seam's docblock states the predicate accurately ("named-imports lucide's icons record and indexes it"); the gate comment does not. See amendment 2.

(c) The two-copies pin has a hole. The new gate-test row "normalises exactly as the ONE resolver does, read from its source" asserts (i) the resolver's text contains .split(/[-_\s]+/) and not .split('-'), (ii) the map entries extracted from the text equal {Home:'House'}, and (iii) the gate's own toRecordKey on six samples. It never executes the resolver. Probe: I widened only the gate's tokeniser to split(/[-_.\s]+/)all 40 gate tests stayed green, including that row. So the claim in the .mjs Normalisation comment, "a change to either side that is not made to the other fails loudly", is false in the gate-wider direction — which is exactly the under-reporting class the card closes (the gate would judge a name live that the resolver misses). The unit vitest project already imports TypeScript straight from packages/ (scripts/__tests__/known-schema-types-derivation-5115.test.tspackages/cli/src/utils/known-schema-types.js), so a behavioural pin is feasible without a build. See amendment 1.

Pins are not vacuous — confirmed

  • Each fallback describe opens with a CONTROL asserting a positive render (getByText('Line Items'), getByTestId('view-tab-open'), getAllByText('New Task')), plus a CONTROL that a resolvable name draws that glyph and not the fallback; every fallback row asserts svg.lucide-inbox / the chip text positively, not merely an absence.
  • Ablation reproduced in my worktree: drop ?? Inbox in RelatedList.tsx → 4 red including "CONTROL — the section renders, with its title" (the readers render <SectionIcon/> unconditionally, so a null throws; a component that fails to render cannot pass these); drop it in ListView.tsx → 2 red. Tree verified clean after each restore.
  • Seam suite discriminates (file-text non-null, not-a-real-icon null, edit null against the retired named export) and re-implements the narrow tokeniser in-test with a carried > 1000 guard against a silently empty loop. The ActionPreview spy row asserts toHaveBeenCalled() before reading mock.results (the blind-instrument trap avoided).

CI, by name

29 success: Changeset Bump Policy, Changeset Overwrite Report, Changeset Declaration, Changeset Fixed Group Check, Lint (5 min, real), Type Check (9 min, real), Test shard 1/4, 2/4, 3/4, 4/4 (10–14 min each, real; the vitest include globs cover the five new packages/**/*.test.tsx and scripts/**/*.test.ts, and I reran them locally), Build & E2E (log read: 14 Playwright passed / 23 skipped, 69 s explained by a pnpm cache hit — not a path-skip), Bundle Analysis (eager closure 3178.8 / 3191.4 KB — passes with 12.6 KB headroom; note only), README Export Check, Skill Example Check, Skill Guide Path Check, Skill Eval Token Check, Control Byte Scan, Shell Escape Residue Scan, Inert vi.mock Specifier Check, Pre-Install Import Graph Check, Governed Surface Queue Guard, Docs Route Eager Closure Check, Doc Component Type Check, Doc Snippet Type Check, Doc Fence Language Check, Build Docs, Internal Docs Link Check, Live E2E (informational), label. 3 skipped by design: Test (coverage) and Test (coverage shard) run on push only; dependabot.

Base behind main:base.sha c4f541df9; branch point e307c9896; origin/main is 7 commits past the branch point (3 past base). Overlap of those commits' files with the PR's 16: none; no new icons import from lucide-react anywhere on main since the branch point; and I ran the gate and all 7 suites on an actual local merge of head + origin/main — gate OK (1 resolver), 94/94. No reading is invalidated.

Verdict

PASS WITH REQUIRED AMENDMENTS

  1. Make the two-copies pin behavioural (scripts/__tests__/check-lucide-icon-record-names.test.ts). Import describeIconLookup (or resolveIcon) from packages/components/src/renderers/action/resolve-icon.ts and assert toRecordKey(s) === describeIconLookup(s).key for every record key under the enumeration's re-spellings (kebab/snake/spaced/Pascal/lower/doubled), plus a negative control; keep or drop the source-text rows as you like. Then either make the .mjs Normalisation comment's "either side … fails loudly" true or reword it. Proof it is needed: gate-only widening to /[-_.\s]+/ passed 40/40.
  2. State the enforcement bound at DECLARED_RECORD_READERS in scripts/check-lucide-icon-record-names.mjs. Replace "a new container that hand-rolls a lookup turns this gate red" with the predicate it actually holds (named icons import from 'lucide-react' + direct element access on that binding; namespace-import, re-bound and template-literal forms are outside it), and cite finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472 as the known outside-predicate resolver. Widening discovery is a separate design question and is not asked for here.
  3. Card-side Clause-② carrier (process, no code). Post on Consolidate the eight lucide record-reading icon resolvers into one seam — three tokenisers and a 5-of-8 alias map mean the same authored name renders on one surface and not another #5935 a claim-shaped comment (first line matching Claim:) carrying Clause-②: yes in the fixed spelling so check-clause2-carriers.mjs --pair 7491 reads both limbs consistent; today the card limb reads absent, and landing precondition ② cannot pass on it.

Not required, for the record: resolveIcon's parameter could accept null (non-breaking later); the not.toMatch(/\.split\('-'\)/) text row is fragile to a future comment; eager-closure headroom is 12.6 KB.


Seat disposition

Not landing.needs:contract-review stays on this PR until all three amendments are in and this seat verifies them by content. Amendments 1 and 2 are code and are being dispatched. Amendment 3 is a PM action on card #5935 and is this seat's own to discharge.

⭐ Amendment 3 is worth calling out: it is a machine-checkable landing precondition that this seat could not have caught itselfcheck-clause2-carriers.mjs cannot run in this session at all (exit 2, no GitHub token; MCP-only access), so the seat had fallen back to reading labels by hand, and a hand check cannot see a missing claim-line spelling. The reviewer ran the tool's parser directly and found the card limb reads absent. That is exactly the kind of finding an isolated reviewer at tier exists to produce.


Generated by Claude Code

…state the discovery bound (objectui#5935)
Both required amendments from the Clause-② contract review of PR #7491
(comment 5524970713). The seam, the call sites, the alias and the five new
suites are untouched.
Amendment 1 — the two-copies pin is now BEHAVIOURAL.
`scripts/__tests__/check-lucide-icon-record-names.test.ts` asserted only the
resolver's SOURCE TEXT, so it was blind to the gate moving on its own: the
resolver's text is then unchanged and every source-text row still passes. The
review measured it — widening ONLY `toRecordKey` to `split(/[-_.\s]+/)` left
the file green 40/40 — and that is the gate-wider direction, exactly the
under-reporting class this card exists to close.
The file now imports `describeIconLookup` from the resolver and asserts
`toRecordKey(s) === describeIconLookup(s).key` over a corpus derived from the
runtime record: every one of the 1,767 live keys re-spelled six ways, plus a
per-character separator sweep. The sweep is not decoration — no key of the
record contains a separator at all, so re-spellings alone cannot see a
character-class widening, which is why the measured hole survived them. A
second row is the control that the corpus can in fact separate the two,
modelling the drifted gate by substitution on the shipped `toRecordKey` rather
than transcribing the rule a third time. The source-text rows are kept; they
catch a different thing.
TypeScript is imported straight out of `packages/` the way
`known-schema-types-derivation-5115.test.ts` already does, so the pin costs no
build step, and by a RELATIVE path so `describeIconLookup` stays off the
package entry and the published widening stays at one symbol.
Amendment 2 — the enforcement bound is stated instead of over-claimed.
The `DECLARED_RECORD_READERS` comment said a new container that hand-rolls a
lookup turns the gate red. It holds for one syntactic shape. Measured against
this gate: named `icons` import (rename included) plus element access on that
binding, through parens/`as`/`!`, any argument expression — red. Namespace
import, a binding re-bound through another const, property access,
destructuring, `require()`/dynamic `import()`, and anything inside a string or
template literal — green. objectui#7472 is cited as the known outside-predicate
resolver and is outside three ways at once. The discovery predicate is ⛔ NOT
widened; that is a separate design question.
The Normalisation comment's "either side ... fails loudly" is likewise made
true rather than reworded away: it now names the behavioural row that carries
it and records that the source-text rows do not.
Verification: gate exits 0 at 1 record-reading resolver; the 7 relevant suites
96 green (94 before, +2 rows); ablation red/green with the restore proven by
state; `type-check:scripts`, `lint:root` (0 errors on changed files) and
`check-control-bytes` green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3180.9 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DpQmpEDV.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.31KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.69KB21.03KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Amendments 1 and 2 applied — 089e35043. ⛔ Still draft, needs:contract-review untouched.

Against the contract review's PASS WITH REQUIRED AMENDMENTS (comment 5524970713). Amendment 3 is the PM seat's and was not touched. The mechanism — tokeniser, seam, Home to House alias, seven call sites, five new suites — is byte-unchanged; the commit touches two files, both under scripts/.

Amendment 1 — the two-copies pin is behavioural

scripts/__tests__/check-lucide-icon-record-names.test.ts now imports describeIconLookup from the resolver by a relative path (so it stays off the package entry and the published widening stays at one symbol) and asserts toRecordKey(s) equals describeIconLookup(s).key over a corpus derived from the runtime record: all 1,767 live keys re-spelled six ways, plus a per-character separator sweep. The source-text rows are kept.

The sweep is load-bearing, and the review's own prescription would not have worked without it. The amendment text lists the re-spellings (kebab/snake/spaced/Pascal/lower/doubled) as the corpus. Those alone cannot catch the measured hole: no key of the record contains a separator at all (0 of 1,767, control — 95 contain a digit), so widening the class by . changes nothing about any of them and the pin would have stayed green under the very mutation it was commissioned to catch. Measured before writing the row: 0 of 10,602 re-spellings differ under /[-_.\s]+/; 19 corpus members do once hostile separators are in. So the corpus sweeps the character class, not the one character.

The ablation, reproducing the review's probe. Implementation committed first, so the restore leg has a real reference point; mutation and restore both proven by state, never by exit code.

legreading
pre-mutation, on diskshipped .split(/[-_\s]+/) present 1, mutated form present 0
post-mutation, on diskshipped 0, mutated 1; blob 4985efe76 vs HEAD e1600563f; git diff HEAD numstat 1 1
gate script under the mutationexit 0, still "1 record-reading resolver" — the drift is invisible to the gate's own run
pin suite under the mutation2 failed, 40 passed (42) — the two new rows, and only those
restoregit checkout HEAD -- ABSOLUTE_PATH under trap ... EXIT INT TERM; git diff HEAD empty and blob back to e1600563f
green leg on the restored tree42 passed (42)

The 40 pre-existing rows staying green under the mutation is the review's finding reproduced exactly. The .mjs Normalisation comment is made true rather than reworded away: it now names the behavioural row that carries "either side fails loudly" and records that the source-text rows do not.

Amendment 2 — the enforcement bound is stated

Replaced at DECLARED_RECORD_READERS, and the same over-claim corrected in the PR body. Every line of the new comment is measured against discoverResolvers, not reasoned about — 19 syntactic shapes through the real predicate:

Inside (red): named icons import; a renamed named import ({ icons as reg }); (icons as Record)[k]; and any argument expression, a template literal included — the predicate never looks at the argument.
Outside (green, not caught): namespace import; a binding re-bound through another const; property access (icons.House); destructuring; require(); dynamic import(); and anything inside a string or template literal.

⚠️One correction to the amendment text, which I would rather state than quietly obey. It says "namespace-import, re-bound and template-literal forms are outside it". Element access with a template-literal argument is inside — measured red. What is outside is code emitted inside a template literal, which is #7472's first of three reasons. Both readings are defensible from the phrase, so the new comment spells out which is which rather than repeating the ambiguity. #7472 is cited, and it is outside three ways at once: app-generator.ts emits, inside a template literal, import * as LucideIcons from 'lucide-react' re-bound to a lucideIcons const and then indexed. ⛔ The discovery predicate is not widened.

Verification, on the pushed head

  • 7 relevant suites: 7 files, 96 tests passed (the review's 94, plus the 2 rows). This file alone 42, was 40.
  • Icon union re-run at 089e35043: 18 files, 184 tests passed — 182 plus the same 2. Re-run because a push after review invalidates a sha-stamped union reading.
  • Every other suite naming the edited gate script — entry-guard wiring, scripts type-check parity, breadcrumb / command / context-menu item icons, icon-renderer-declared-default, ViewSwitcher: 7 files, 62 tests passed.
  • node scripts/check-lucide-icon-record-names.mjsexit 0, OK lucide icon names: 182 authored/declared names reaching 1 record-reading resolver are live icons keys.
  • pnpm type-check:scripts — VERDICT command-exit 0. pnpm lint:root (the scope that owns scripts/) — 0 errors, 32 warnings, 0 of them in either changed file; all 32 are pre-existing no-explicit-any in e2e specs, two unrelated scripts/__tests__ files and two vitest setup files.
  • node scripts/check-control-bytes.mjs — OK, 6,158 tracked text files.

Exit codes were captured by redirecting first (cmd > log 2>&1; EXIT=$?), never after a pipe, and each verdict is quoted from the line the tool printed.

⚠️ Byte note: the separator sweep needs NBSP and the ideographic space. Both are written as / escapes — a literal one is a character no reviewer can see and no grep can be aimed at. Verified: 0 invisible or control characters in either changed file.

The changeset was checked and not edited: it makes no claim about the discovery predicate, and its "the under-reporting is closed rather than merely bounded" is what amendment 1 makes true.

⛔ Not done, deliberately: PR not flipped ready, no auto-merge, needs:contract-review left on. The package: components / plugin / tests labels appeared from the labeler, not from me.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

✅ Amendments verified — needs:contract-review cleared, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Provenance: 「PASS ⇒ 同席剥标、ready、auto-merge」 (维护者 2026-08-31 裁定). The tier review returned PASS WITH REQUIRED AMENDMENTS (5524970713); all three are now in.

Verified by content at 089e35043, not from the report

  • Scope: parent is the reviewed head 675571f8f; 2 files, both under scripts/. The seam, the seven call sites, the alias and the five new suites are byte-unchanged — confirmed by filtering the commit's file list for anything outside scripts/ and getting an empty set.
  • Amendment 1 — the pin is behavioural and cannot silently go vacuous. It imports describeIconLookup from the resolver and asserts toRecordKey(s) === describeIconLookup(s).key over 8,000+ spellings. ⭐ More than was asked: it carries its own non-vacuity control — it models a dot-splitting gate and asserts the corpus can separate the two, naming arrow.right and building.2. So a future corpus edit that quietly drops the hostile separators turns the suite red rather than passing in silence. That is strictly better than the one-off ablation the amendment prescribed, because it keeps holding after today.
  • Amendment 2 — the bound is stated, the predicate is NOT widened. The DECLARED_RECORD_READERS comment now names what is inside (named icons import, rename included, plus element access through parens / as / !, any argument expression) and what is outside (namespace import, re-bound const, property access, destructuring, require()/dynamic import(), code inside a string or template literal), and cites finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472. The PR body's matching sentence is corrected too.
  • Amendment 3 was discharged by this seat on the card (5524981959).
  • CI: 32/32 at 089e35043 — 29 success, 3 skipped by design. Test (shard 1/4) 11:52:22 · 2/4 11:52:16 · 3/4 11:54:14 · 4/4 11:54:17 · Type Check 11:50:22 · Lint 11:47:10 · Bundle Analysis, README Export Check, Governed Surface Queue Guard each read individually.

⭐ The implementer corrected the review, and it is the most valuable thing in this round

Amendment 1's prescribed corpus would not have reproduced its own proof. The review said to assert over the enumeration's re-spellings (kebab/snake/spaced/Pascal/lower/doubled). Measured: no key of the record contains a separator at all (0 of 1,767; control "contains a digit" fires at 95), so 0 of 10,602 re-spellings change under /[-_.\s]+/. A pin built to the amendment's letter would have stayed green under the exact mutation it was commissioned to catch.

The implementer added a per-character separator sweep instead — that is what makes it fire — and said so plainly rather than reporting compliance. ⇒ I ratify the deviation. Amendment 1's requirement was a pin that is behavioural and proven to fire; the letter of its corpus was insufficient for its own purpose, and following the letter would have produced a pin that certified nothing. Measured after: 2 of 42 red under that mutation, with the gate script itself still exiting 0 — which is exactly why the pin, not the gate, has to hold this.

Three further corrections from the same round, recorded because two of them are mine:

  1. ⚠️My brief said "template-literal forms are outside" the predicate — wrong as written. An element access whose argument is a template literal (icons[`${a}${b}`]) is inside; the predicate reads the base identifier and never the argument. What is outside is code emitted inside a template literal. The new comment spells out which is which instead of repeating the ambiguity. The implementer also measured that a renamed named import is inside — which the review did not mention.
  2. finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472's own stated third reason is misdescribed (in that issue, not here): it says the base is "a property access", where the generated code actually re-binds a namespace import through a const. Both are outside the predicate so the conclusion stands; only the reason is wrong. Left alone as out of scope.
  3. The changeset needed no edit — it makes no claim about the discovery predicate, and its "under-reporting closed rather than merely bounded" line is precisely what amendment 1 makes true.

An accepted, named gap — not an oversight

The seam's own docblock (packages/components/src/renderers/action/resolve-icon.ts) still ends its enforcement paragraph with the same unqualified sentence amendment 2 corrected in the .mjs. It is left as-is: the tier reviewer examined that docblock and judged it accurate in context (it is preceded by an accurate predicate sentence), and my dispatch scoped the seam out so the mechanism under review stayed byte-stable. ⇒ A one-sentence follow-up available any time; recorded here so it is a chosen gap rather than one nobody noticed.

Carriers

needs:contract-review removed from this PR (remaining: package: components, plugin, tests). Card #5935 never carried it — its labels are pm:dispatched, domain:ui. ⚠️ That is the same dual-carrier gap the reviewer found on the declaration side, from the other direction: the gate label was hung on one carrier only. I also set #5935's assignee, which was empty while this PR was open — a half-state that invites a second agent onto claimed work.

⚠️ Both readings are manual: scripts/pm/check-clause2-carriers.mjs exits 2 in this session (no GitHub token; MCP-only access), and its own output says that is not a clearance. A hand label-check covers strictly less than the tool — naming the instrument, as on every landing this shift.


Generated by Claude Code

os-sam pushed a commit that referenced this pull request Sep 4, 2026
…ecord the third zero
objectui#5936 asked for the consumer of a component registration's `icon` meta
before extending the icon gate to it. Three first-party populations were
measured and none has one (objectui, objectstack, cloud), so the gate is not
extended — adjudicated 2026-09-04.
Records that where the next reader hits it (the gate's own "Deliberate
boundaries"), with the three readings, their controls, the adjudication, and
the caveat that three zeros are not a proof of absence.
Retires the membership half of the `ui:icon` local pin, which was kept at
objectui#5633 on the premise that the palette lives outside this repo. It
guarded 1 of the 45 registrations declaring an `icon` meta while the gate is
adjudicated not to cover the other 44, and its hand-copied tokeniser had
already gone stale against the seam objectui#5935 / PR #7491 created. The
coupling half stays: it needs no external vocabulary and cannot drift.
The published `ComponentMeta.icon` key is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

refactor(components,app-shell,plugin-detail,plugin-list,plugin-view): one lucide icon-name seam, seven resolvers down to one - #7491

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-5935-one-icon-resolver-seam
Sep 3, 2026
Merged

refactor(components,app-shell,plugin-detail,plugin-list,plugin-view): one lucide icon-name seam, seven resolvers down to one#7491
os-project-manager merged 2 commits into
mainfrom
claude/issue-5935-one-icon-resolver-seam

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#5935

Seven modules resolved authored lucide icon names, each with its own copy of the logic. There is now one, resolveIcon, and the other six call it.

Both rulings on the card execute here as written:

  • 2026-08-31 (comment 5472612351, 维护者, verbatim 「同意」, option B) — one tokeniser, one iconNameMap, one seam; and point 4, 「本裁定后新容器 ⛔ 不得再自带解析器,一律走 seam」.
  • 2026-09-03 (comment 5523286738, maintainer, verbatim 「同意你的建议」, option C) — the seam does name -> component and nothing else. No onUnresolvable parameter. Each call site keeps its own fallback.

The census, re-measured before starting

Bound declared first, because this card has been handed a wrong number four times: all 4,101 git-tracked .ts/.tsx files, whole repo, no root subset — wider than the gate's own SCAN_ROOTS (3,945 of them). Re-run of the gate's own AST discovery over that wider set.

readingruled figure (at c1fe272ad)measured at my base e307c9896drift
record-reading resolvers77none
Home -> House alias4 of 74 of 7none
dynamic-surface readers44none
RECORD_READING_TYPES14 keys, 9 non-action14 keys, 9 non-actionnone
tokenisersT1 on 5, T2 on 1, T3 on 1T1 on 5, T2 on 1, T3 on 1none

Zero drift. The only difference from the ruling's measurement is the scanned-source count (3,945 vs 3,943 — two files added to the tree since).

What changed at each of the seven sites

sitehadnow
renderers/action/resolve-icon.tssplit('-') + aliasthe seam: split(/[-_\s]+/) + alias
renderers/basic/icon.tsxown toPascalCase + own map + own record indexcalls the seam; SquareDashed placeholder + warning byte-identical
app-shellpreviews/ActionPreview.tsxsplit(/[-_\s]+/) + inline ternarycalls the seam; keeps its 3-character name chip
plugin-detail/RelatedList.tsxsplit(/[-_\s]/), no aliasresolveIcon(name) ?? Inbox
plugin-list/ListView.tsxinline split('-'), no aliasresolveIcon(iconName) ?? Inbox
plugin-list/components/TabBar.tsxsplit('-'), no aliascalls the seam; keeps null
plugin-view/ViewSwitcher.tsxown toPascalCase + own mapcalls the seam; keeps null for views and its own `

resolveIcon is exported from @object-ui/components, the one package all five consumers already depend on — no new dependency edge.

The tokeniser was not a choice made in this PR

split(/[-_\s]+/) with the rename universal, established by the pre-dispatch enumeration the 2026-08-31 ruling required (comment 5522254814): regression set empty against the authored population, against an every-name-by-every-surface cross-product, and against a bound-free differential over 8,298 spellings derived from all 1,767 live record keys. split('-') regresses 4,748 name-surface pairs in that last reading and is not adoptable.

Widening only, and structurally so: no record key contains _, whitespace or - (0 of 1,767, with a firing control), so whenever the narrow tokeniser produced a live key the wide one produces the same key. The old resolving sets are strict subsets. Sixteen name-surface pairs start resolving; none stops. Stated in words in the changeset.

⭐ The load-bearing claim: zero user-visible change on all four unresolvable behaviours

Each of the four is pinned, and each pin is proven non-vacuous two ways — a control in the same run that asserts the opposite state, and a red-then-green ablation on the committed baseline 675571f8f.

behavioursurfacespinned in
nullseam, TabBar, ViewSwitcher (+ its own `
SquareDashed + warnbasic/icon.tsxthe existing #5631 pin, untouched, 13/13 green
Inbox glyphRelatedList (header and toolbar), ListView empty stateRelatedList.iconSeam-5935.test.tsx, icon-seam-fallbacks-5935.test.tsx
3-character name chipActionPreviewActionPreview.iconSeam-5935.test.tsx

The ablation, on the committed baseline, restore proven BY STATE (git diff HEAD empty and blob equality against the HEAD blob; trap ... EXIT INT TERM with absolute paths; restore pinned to git checkout HEAD -- path, never the bare form that reads back from the index). Each mutation was proven on disk in both directions — removed text gone, injected text present — before its run was read:

mutationrows that fired
seam tokeniser back to split('-')8 across all 5 new suites
the Home -> House entry disabled2, incl. the #5631 pin's own rename row
RelatedList: ?? Inbox dropped4
ListView: ?? Inbox dropped2
ActionPreview: chip emptied1
icon.tsx: placeholder glyph swapped out1, the #5631 placeholder row

⚠️The trap named on this card, caught in the act. Dropping RelatedList's ?? Inbox also turned its CONTROL row red — the component threw and drew nothing, exactly the "a test that passes because a component failed to render" failure this card warned about. That is why every fallback describe opens with a row proving the surface rendered and asserts its fallback positively, rather than only asserting an absence.

Two earlier ablation attempts were refused by the harness rather than reported, and are named so the numbers above are readable: injecting SquareDashedNOPE left the removed text present as a substring (both-directions probe unsatisfiable), and injecting the empty string made the injected-text probe degenerate. Both were re-run with clean anchors.

The gate, simplified in the same PR

scripts/check-lucide-icon-record-names.mjs: DECLARED_RECORD_READERS goes 7 to 1, and its normalisation stops being a widest-common approximation. Its verdict line, before and after:

before: OK lucide icon names: 182 authored/declared names reaching 7 record-reading resolvers are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).
after: OK lucide icon names: 182 authored/declared names reaching 1 record-reading resolver are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).

Every part-2 counter is identical across the two runs (148 judged, 22 descendant-judged, 239 declined, 34 anchored) — the census moved, what is judged did not. The under-reporting disclosed at #5932 is closed, not merely bounded: with one resolver there is nothing left to approximate.

Ruling point 4 ships as enforcement within a stated bound. Discovery is re-run from source every run and disagreement fails in both directions. ⚠️ Corrected here after the contract review measured the earlier wording as an over-claim: the predicate holds one syntactic shape — a named icons import from lucide-react (a rename counts) plus an element access on that binding, seen through parens, as casts and !, with any argument expression. Measured red on that shape; measured green, i.e. not caught, on a namespace import, a binding re-bound through another const, property access, destructuring, require()/dynamic import(), and anything inside a string or template literal. #7472 is the known outside-predicate resolver and is outside three ways at once. So a new container that hand-rolls a lookup in the conforming shape reds the commit that adds it; one written another way does not, and the ruling still binds it by review. The bound is now stated at DECLARED_RECORD_READERS itself. ⛔ The discovery predicate is not widened — that is a separate design question.

The two-copies pin is behavioural, not textual. The gate's .mjs normalisation and the resolver's TypeScript one are held together by importing describeIconLookup and asserting toRecordKey(s) equals describeIconLookup(s).key over a corpus derived from the runtime record — all 1,767 live keys re-spelled six ways, plus a per-character separator sweep — with a control that the corpus can in fact separate the two. The sweep is load-bearing: no record key contains a separator at all, so re-spellings alone cannot see a character-class widening. The source-text rows are kept; they catch a different thing. Measured need: widening only the gate to split(/[-_.\s]+/) left the suite green 40/40 before this, and reds 2 of 42 after it, with the gate script itself still exiting 0 under that mutation — which is exactly why the pin, not the gate, has to hold this.

Verification

  • New suites: 5 files, 40 tests. Union re-run at 089e35043 (the amendment commit): 18 files, 184 tests, all passing — the 5 new plus every pre-existing icon suite (ui:icon reads the SDUI identity key name as its lucide icon name, so any icon node that authors name renders nothing at all #5631's 13-test pin, finding(components): ui:button inlines its own copy of resolveIcon instead of importing it, so any future fix to the shared resolver silently misses it #5993's button dedupe pin, breadcrumb/command/context-menu/dropdown-menu item icons, the gate's own suite, now 42 tests, TabBar.i18nLabel, ListView.emptyStateFilteredCopy, ViewSwitcher, ActionPreview.locations). That is the 675571f8f reading of 182 plus the 2 rows the behavioural pin adds, and it is quoted from a run on the current head rather than carried forward.
  • pnpm exec turbo run type-check --filter='...@object-ui/components'66 tasks, all successful, over the 45-package DOWNSTREAM CONSUMER closure (prefix filter = consumers, the direction a contract change travels). The five new test files were confirmed present in each package's tsconfig.test.json program via --listFiles, so "type-check is green" actually covers them.
  • 13 derived gates green at 675571f8f: icon-record-names, control-bytes, vi-mock-specifiers, vi-mock-inherit, readme-exports, phantom-deps, self-import, entry-guard, side-effects-array, sdui-registration-pins, eager-closure, dist-completeness, docs-route-closure.
  • ESLint over the 15 changed files: 0 errors (3 introduced by the first draft were fixed: an unused LucideIcon import, an unused React import, and a react-hooks/static-components error on the ActionPreview call site, which now carries the same annotation the other five call sites do). 250 warnings, all pre-existing any / react-hooks noise on untouched lines of two 4,000-line files.
  • Changeset present, minor on the five packages whose source moved.

Scope

Clause-②: yes — and this is a deliberate re-derivation against the ruling's stated expectation of no. The 2026-09-03 ruling recorded «Clause-②: no expected (renderer-internal; re-declare from the diff)», which holds for the half it was about: no contract accept/reject behaviour moves, no schema, validator or refusal changes, nothing an author may write changes. But the diff widens a published surfaceresolveIcon is a new export from @object-ui/components's public entry, which the ruling's "renderer-internal" reading did not anticipate and which is unavoidable because the five other call sites live in five other packages. Declaring no here would have been an auditable false declaration, so it is yes with the reason attached.

#7472 stays out.packages/cli/src/utils/app-generator.ts:842 emits an eighth resolver inside a template literal, against lucide's named-export namespace, with zero normalisation. It is correctly absent from DECLARED_RECORD_READERS (it reads neither censused vocabulary), and the gate's discovery predicate is not widened to see it.

Nothing under content/docs/releases/ is touched. No gate threshold is raised or weakened.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… one lucide icon-name seam, seven resolvers down to one
Seven modules resolved authored icon names against lucide's runtime `icons`
record, each with its own copy: three tokenisers (`split('-')` on five,
`split(/[-_\s]/)` on one, `split(/[-_\s]+/)` on one) and the `Home` -> `House`
rename on four of seven. The same authored name therefore rendered on one
surface and not another. `resolveIcon` is now exported from
`@object-ui/components` and the other six call it.
The tokeniser is `split(/[-_\s]+/)` with the rename universal — measured, not
chosen: its regression set is empty against the authored population, against an
every-name-by-every-surface cross-product, and against a bound-free differential
over 8,298 spellings derived from all 1,767 live keys.
The seam answers `name -> component | null` and decides nothing else. Every call
site keeps its own fallback, at the call site: `ui:icon` its `SquareDashed`
placeholder and warning (objectui#5631, untouched), `RelatedList` and `ListView`
their `Inbox`, `ActionPreview` its name chip, the rest `null`.
`scripts/check-lucide-icon-record-names.mjs` is simplified in the same change:
its census goes from seven sites to one, and its normalisation stops being a
widest-common approximation of three disagreeing resolvers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.8 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-C0j--fB6.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.31KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.69KB21.03KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Seat bookkeeping + the Clause ② call. ⛔ NOT a contract review.

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Head 675571f8f.

⚖️ Open question RULED: A — the declaration stands as Clause ② YES.

The card recorded "Clause ②: no expected (renderer-internal; re-declare from the diff)". The dev re-derived it against the actual diff and declared yes, because resolveIcon is now on @object-ui/components' public entry. That is correct and it stands.

The criterion has two limbs — changes accept/reject behaviourorwidens the public surface. The second is met: a symbol that was package-internal is published, into a 45-package consumer closure. ⇒ needs:contract-review applied, PR stays draft, ⛔ not enqueued, and the project director seat lands it.

⭐ The reason I am not taking the softer reading: the card's own instruction was "re-declare from the diff", not "confirm the expectation". A no that any reviewer can falsify by reading one line of index.ts costs more than one extra gate pass — it devalues every other no in the ledger. The recorded expectation was a prediction, and predictions are what re-derivation exists to overturn.

⚠️ For the reviewing seat, the honest narrowing: no accept/reject behaviour moves. Nothing an author may write changes; no schema, validator or refusal is touched. The export exists solely so five sibling packages can stop duplicating a private function, and the diagnostics helper describeIconLookup was deliberately kept off the public entry to hold the widening at exactly one symbol — verified, see below.

Verified on the branch by content, ⛔ not from the report

probeorigin/mainbranch
onUnresolvable in the seam0 — the only hit is prose explaining it was deliberately not added
resolveIcon on the public entry01
describeIconLookup on the public entry0
⛔ FENCE — SquareDashed in basic/icon.tsx (#5631)33, unchanged
gate census entries82

⚠️ That last row is my probe's count, not the report's: the dev states 7 → 1 and my pattern matches one extra non-entry line on each side. The delta is identical (−6), and I am stating both rather than silently adopting either.

Call-site fallbacks intact and local, exactly as option C requires: RelatedList.tsx:222resolveIcon(name) ?? Inbox, ListView.tsx:3881resolveIcon(iconName) ?? Inbox, icon.tsx:121 through the seam into its own placeholder branch.

⭐ The trap this card named, caught in the act

Dropping RelatedList's ?? Inbox in the ablation turned its control row red too — the component threw and rendered nothing. That is precisely "a test that passes because a component failed to render", which the dispatch flagged because this repo produced exactly that failure earlier today. It is why every fallback block here opens with a render-proof row and asserts its fallback positively.

⭐ And two ablation attempts were refused by the harness rather than reported: injecting SquareDashedNOPE left the removed text present as a substring, so the both-directions probe was unsatisfiable; injecting the empty string made grep -cF '' match every line. Both were re-run with clean anchors and neither produced a reading that was used. A harness that declines to emit a number it cannot justify is worth more than the six ablation rows it did produce.

Two more worth the reviewer's eye

The census was re-derived over a WIDER bound than the gate's own. All 4,101 tracked .ts/.tsx files, against the gate's 3,945 SCAN_ROOTSzero drift from every ruled figure, with the detector control firing in the same run. A census is only as wide as its pathspec, and this one declared its bound and then exceeded the incumbent.

Ruling point 4 ships as mechanical enforcement, not a note. 「本裁定后新容器 ⛔ 不得再自带解析器」 is now held by the gate itself: it rediscovers record readers from source every run and fails in both directions, so a container that hand-rolls a lookup reds the commit that adds it.

Out-of-scope #7492 filed (the gate's data-table census entry names a module that reads no record); ⛔ deliberately not fixed in place despite the bounded-in-place conditions arguably holding — this card carries three scope fences and a drive-by edit to an unrelated table entry is what they exist to stop. #7472 confirmed still present, still correctly outside the census, and the discovery predicate ⛔ not widened to see it.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Clause-② in-seat contract review — PASS WITH REQUIRED AMENDMENTS. needs:contract-review stays on.

Commissioned by the domain:ui execution seat (PM session session_01EMrWaQw3XS5DxTHxp4yRyC) per the in-seat review route (维护者 2026-08-31 裁定: 交付后复核由派发席在席内完成,达档 PM 自审或派 fable 复核子任务皆可). This seat runs opus and is not at tier, so it commissioned an isolated reviewer rather than self-reviewing.

Independence: the reviewer was given only the card, the rulings and the PR — ⛔ not the dispatch order and not this seat's opinion. It discloses below that it encountered a seat bookkeeping comment carrying a Clause-② opinion while reading the PR thread, and did not use it. Recording that rather than hiding it.

Tier verification (维护者 2026-08-27 裁定 — a commissioned review is legitimate only if verified): every harness-stamped model field in the reviewer's transcript was read before adopting — 97 stamps, all claude-fable-5-1, zero other values, zero fallback/overload markers. CONTRACT_REVIEW_TIER is claude-fable-5 (objectstack/scripts/pm/dispatch-gates.mjs:8091); claude-fable-5-1 is at or above that floor — the same floor reading recorded on PR #7471. ⇒ Verified, so the verdict is adopted verbatim. ⛔ Nothing below is rewritten, abridged or polished by this seat; the only legitimate alternatives were verbatim adoption or wholesale voiding.


Contract review — objectui PR #7491 (head 675571f8f) against card #5935

Inputs read in full: the PR body; the complete diff (1,590 lines, all 16 files); issue #5935 body; comments 5472612351 (ruling 2026-08-31), 5523286738 (ruling 2026-09-03), 5522254814 (pre-dispatch enumeration), plus 5523075605 (director record) and 5522309927 (the blocker that produced ruling 2); all 32 check runs; issue #7472. Disclosure for independence: the PR's comment thread contains a seat bookkeeping comment (5523950630) that states a Clause-② opinion; I encountered it while reading the PR's comments and did not use it — every judgment below is derived from the diff and my own runs.

Own verification (detached worktree /home/user/objectui-review-7491 at 675571f8f, main checkout untouched, no stash): the 5 new suites + the #5631 pin + the gate suite = 7 files / 94 tests green; node scripts/check-lucide-icon-record-names.mjs exits 0 with "1 record-reading resolver"; the same on a local merge with current origin/main (7 commits past the branch point); an independent 70,974-spelling differential; two fallback ablations; one pin-hole probe; three enforcement-shape probes. Details under each item.

① Derived judgments — every accept-set / published-surface change the diff causes

Published surface

  1. @object-ui/components public entry (packages/components/src/index.ts) gains resolveIcon(name: string | undefined): LucideIcon | null. New symbol: package.jsonexports has only . and ./style.css, so renderers/action/resolve-icon.ts was unreachable by deep import before — the "new export" claim is correct. Right. Five sibling packages need it; duplicating it is the defect the card exists to end; precedent is hasDeclaredVisibilityGate on the same entry. Shape: return LucideIcon | null is the correct published shape under option C (caller must decide the fallback). One note, not a defect: the parameter rejects null although JSON-sourced icon: null is realistic; widening to string | null | undefined later is non-breaking.
  2. describeIconLookup is exported from the module but not from the package entry, and cannot be deep-imported. Right — the widening is held to exactly one symbol.
  3. Module-private resolveIcon/toPascalCase/iconNameMap removed from TabBar.tsx, ViewSwitcher.tsx, icon.tsx. Never exported from any package index (checked on origin/main). No surface change.
  4. Gate exports: DECLARED_RECORD_READERS 7 → 1; RECORD_READING_TYPES.icon.resolver and .view-switcher.resolver become "…resolve-icon.ts (via …)". The resolver string is consumed only in diagnostics (lines 667/693/784/846/960 of the .mjs), and six entries already used that form. Right.

Accept set (which authored names resolve), per surface — derived from the diff and measured

  1. Shared resolver (and every type routed through it: action:*, dropdown-menu, context-menu, breadcrumb, command, header-bar, button): tokeniser split('-')split(/[-_\s]+/). Widening only, right, and ruled (2026-08-31 「tokeniser 与 iconNameMap 全量收敛到一份」; the tokeniser itself fixed by the enumeration, "the implementer does not pick this").
  2. ui:icon (basic/icon.tsx): same widening; alias unchanged; placeholder + warn unchanged (ui:icon reads the SDUI identity key name as its lucide icon name, so any icon node that authors name renders nothing at all #5631 pin 13/13). One diagnostic-only consequence the PR does not mention: for a name that fails under both rules and contains _/space, the warning's lookup: "X" half now prints the wide-tokeniser Pascal form (FooBar where it printed Foo_bar). Console text only, not rendered. Acceptable.
  3. ActionPreview: was already split(/[-_\s]+/) + alias → no accept-set change, routing only (the test file says so honestly).
  4. RelatedList: split(/[-_\s]/) + no alias → wide + alias. Gains home/Home (and separator variants) → draws House where it drew Inbox. Behaviour change, user-visible, sanctioned: ruling 1 converges iconNameMap to one; enumeration §5 "the alias must be universal"; ruling 2 "Home→House applied universally"; card scope point 2 asks it be stated — it is, in words, in the changeset.
  5. ListView empty state: split('-') + no alias → wide + alias. Gains snake/space spellings and home. Same judgment.
  6. TabBar: split('-') + no alias → wide + alias. Gains snake/space and home (a glyph appears where none did). Same judgment.
  7. ViewSwitcher: split('-') + alias → wide + alias. Gains snake/space. Right.

The tokeniser attack (⚠ item). I transcribed all seven pre-PR resolvers verbatim and the new seam, and ran them over 70,974 spellings: every record key re-spelled as kebab/snake/spaced/Pascal/lower/UPPER, with leading, trailing, doubled and mixed separators of -, _, space, tab and NBSP, plus alias-specific hostiles (-home, home_, ho-me, home_house, …) and controls. Result: 212,163 name-surface pairs resolved before; 0 stop resolving; 0 resolve to a different component; 262,717 widenings; controls fired (building_2 disagreed across old sites exactly as the enumeration recorded; home likewise). Why no counter-example can exist: an empty token capitalises to '' and contributes nothing to the join, so split('-') and split(/-+/) are join-identical (this also makes .filter(Boolean) inert); and since no live key contains -, _ or whitespace (0 of 1,767, re-measured), any live key the narrow rule produced came from hyphen-only tokens, which the wide rule splits identically. The only alias is Home→House and Home is not a live key, so the alias can only add. The "widening only / strict subset" claim is correct.

Home → House at the three previously alias-free sites. It is a behaviour change, it is sanctioned by both rulings (see 8), it can change what a user sees (icon: "home" on a related-list header/toolbar button or a grid empty state: Inbox → House; on a tab: nothing → House), and it is stated as such in the changeset. Right.

The four unresolvable behaviours, per site — verified by reading the head files, not the description:

  • null: seam ?? null; TabBar.tsx:146/164, 212/225, 232/241 guard TabIcon && …; ViewSwitcher.tsxgetViewIcon returns resolveIcon(view.icon) when an icon is authored (authored-but-dead → no glyph, not the type default) and actions use resolveIcon(action.icon) || DEFAULT_VIEW_ACTION_ICONS[type] (:258). Old ViewSwitcher used || null; identical for component values. Preserved.
  • SquareDashed + warn: icon.tsx:121requested ? resolveIcon(requested) : undefined then if (!Icon); old value was undefined, new is null; nothing else reads Icon. Placeholder branch byte-identical. Preserved.
  • Inbox: RelatedList.tsx:222resolveIcon(name) ?? Inbox feeding both the header (:1322) and toolbar (:299, which still gates absent icons with action.icon ? … : null — a third behaviour in that file, pinned); ListView.tsx:3881resolveIcon(iconName) ?? Inbox. Preserved.
  • 3-character chip: ActionPreview.tsxif (Glyph) … else <span>{name.slice(0,3)}</span> unchanged. Preserved.

② Semver

  • Changeset: minor on components, app-shell, plugin-detail, plugin-list, plugin-view.
  • My level: @object-ui/componentsminor — a new public export, nothing removed. The four consumers: additive behaviour widening at their surfaces, no API change — minor is defensible, patch would be too; the distinction is moot because all five (and 35 others) are in the single fixed group in .changeset/config.json, so the family bumps minor on components' account regardless. Major: no — no name stops resolving (measured), no export removed; and this repo's policy (check-changeset-no-major.mjs) reserves major for the objectstack-synchronised bump, shipping objectui breaks as minor with the break spelled out — there is no break to spell.
  • Matches. The changeset body also carries the alias widening at the three sites in words, which the card demanded.

③ Boundary flags

(a) Clause-②: yes against the ruling's expected no. Correct re-derivation. The criterion (pm-dispatch SKILL.md line 547) is two-limbed — "改变契约接受/拒绝行为 扩大公开面". Limb 1 is not met (no schema, validator or refusal moves; the gate's judged/declined counters are unchanged). Limb 2 is met: a package-internal symbol is now on the public entry of a package with a 45-package downstream closure. The director's line was explicitly "re-declare from the diff"; a no here would be a falsifiable false declaration. Stands. One carrier problem, though: the checker that landing precondition ② runs (objectstack/scripts/pm/check-clause2-carriers.mjs --pair) reads the card side from claim-marked comments in the fixed spelling; I ran its parser — the PR body's **Clause-②: yes** — … line reads declared: yes, but no comment on #5935 carries a Clause-②: line at line start (the only occurrences are mid-line prose and inside the os-dev-report JSON), and the seat's claim comments do not match CLAIM_COMMENT_MARKER (/^\s*>?\s*Claim(?:ed)?\s*:/) either. The card limb therefore reads absent, which that tool classifies as adverse (exit 4) — "⛔ 不作干净". See amendment 3.

(b) #7472 held out. Defensible. Ruling 2 says so in terms ("⛔ Out of scope here; its absence from DECLARED_RECORD_READERS is correct, not an omission"); ruling point 4 governs new containers, and the generator's DynamicIcon predates it, is emitted into a different tree, and reads a third vocabulary (named exports, zero normalisation) — routing it through the seam would change what generated apps accept and needs its own ruling. Not widening the discovery predicate is consistent with #7472's own text. But the PR over-claims the enforcement. I added a probe file in three shapes and ran the gate: named icons import + direct element access → red ("UNDECLARED record-reading resolver", exit 1); import * as L …; L.icons[k] in executed code → green; named import re-bound through a second const and indexed → green. So "a container that hand-rolls a lookup turns the gate red on the commit that adds it" (.mjsDECLARED_RECORD_READERS comment, PR body) holds for one syntactic shape. The seam's docblock states the predicate accurately ("named-imports lucide's icons record and indexes it"); the gate comment does not. See amendment 2.

(c) The two-copies pin has a hole. The new gate-test row "normalises exactly as the ONE resolver does, read from its source" asserts (i) the resolver's text contains .split(/[-_\s]+/) and not .split('-'), (ii) the map entries extracted from the text equal {Home:'House'}, and (iii) the gate's own toRecordKey on six samples. It never executes the resolver. Probe: I widened only the gate's tokeniser to split(/[-_.\s]+/)all 40 gate tests stayed green, including that row. So the claim in the .mjs Normalisation comment, "a change to either side that is not made to the other fails loudly", is false in the gate-wider direction — which is exactly the under-reporting class the card closes (the gate would judge a name live that the resolver misses). The unit vitest project already imports TypeScript straight from packages/ (scripts/__tests__/known-schema-types-derivation-5115.test.tspackages/cli/src/utils/known-schema-types.js), so a behavioural pin is feasible without a build. See amendment 1.

Pins are not vacuous — confirmed

  • Each fallback describe opens with a CONTROL asserting a positive render (getByText('Line Items'), getByTestId('view-tab-open'), getAllByText('New Task')), plus a CONTROL that a resolvable name draws that glyph and not the fallback; every fallback row asserts svg.lucide-inbox / the chip text positively, not merely an absence.
  • Ablation reproduced in my worktree: drop ?? Inbox in RelatedList.tsx → 4 red including "CONTROL — the section renders, with its title" (the readers render <SectionIcon/> unconditionally, so a null throws; a component that fails to render cannot pass these); drop it in ListView.tsx → 2 red. Tree verified clean after each restore.
  • Seam suite discriminates (file-text non-null, not-a-real-icon null, edit null against the retired named export) and re-implements the narrow tokeniser in-test with a carried > 1000 guard against a silently empty loop. The ActionPreview spy row asserts toHaveBeenCalled() before reading mock.results (the blind-instrument trap avoided).

CI, by name

29 success: Changeset Bump Policy, Changeset Overwrite Report, Changeset Declaration, Changeset Fixed Group Check, Lint (5 min, real), Type Check (9 min, real), Test shard 1/4, 2/4, 3/4, 4/4 (10–14 min each, real; the vitest include globs cover the five new packages/**/*.test.tsx and scripts/**/*.test.ts, and I reran them locally), Build & E2E (log read: 14 Playwright passed / 23 skipped, 69 s explained by a pnpm cache hit — not a path-skip), Bundle Analysis (eager closure 3178.8 / 3191.4 KB — passes with 12.6 KB headroom; note only), README Export Check, Skill Example Check, Skill Guide Path Check, Skill Eval Token Check, Control Byte Scan, Shell Escape Residue Scan, Inert vi.mock Specifier Check, Pre-Install Import Graph Check, Governed Surface Queue Guard, Docs Route Eager Closure Check, Doc Component Type Check, Doc Snippet Type Check, Doc Fence Language Check, Build Docs, Internal Docs Link Check, Live E2E (informational), label. 3 skipped by design: Test (coverage) and Test (coverage shard) run on push only; dependabot.

Base behind main:base.sha c4f541df9; branch point e307c9896; origin/main is 7 commits past the branch point (3 past base). Overlap of those commits' files with the PR's 16: none; no new icons import from lucide-react anywhere on main since the branch point; and I ran the gate and all 7 suites on an actual local merge of head + origin/main — gate OK (1 resolver), 94/94. No reading is invalidated.

Verdict

PASS WITH REQUIRED AMENDMENTS

  1. Make the two-copies pin behavioural (scripts/__tests__/check-lucide-icon-record-names.test.ts). Import describeIconLookup (or resolveIcon) from packages/components/src/renderers/action/resolve-icon.ts and assert toRecordKey(s) === describeIconLookup(s).key for every record key under the enumeration's re-spellings (kebab/snake/spaced/Pascal/lower/doubled), plus a negative control; keep or drop the source-text rows as you like. Then either make the .mjs Normalisation comment's "either side … fails loudly" true or reword it. Proof it is needed: gate-only widening to /[-_.\s]+/ passed 40/40.
  2. State the enforcement bound at DECLARED_RECORD_READERS in scripts/check-lucide-icon-record-names.mjs. Replace "a new container that hand-rolls a lookup turns this gate red" with the predicate it actually holds (named icons import from 'lucide-react' + direct element access on that binding; namespace-import, re-bound and template-literal forms are outside it), and cite finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472 as the known outside-predicate resolver. Widening discovery is a separate design question and is not asked for here.
  3. Card-side Clause-② carrier (process, no code). Post on Consolidate the eight lucide record-reading icon resolvers into one seam — three tokenisers and a 5-of-8 alias map mean the same authored name renders on one surface and not another #5935 a claim-shaped comment (first line matching Claim:) carrying Clause-②: yes in the fixed spelling so check-clause2-carriers.mjs --pair 7491 reads both limbs consistent; today the card limb reads absent, and landing precondition ② cannot pass on it.

Not required, for the record: resolveIcon's parameter could accept null (non-breaking later); the not.toMatch(/\.split\('-'\)/) text row is fragile to a future comment; eager-closure headroom is 12.6 KB.


Seat disposition

Not landing.needs:contract-review stays on this PR until all three amendments are in and this seat verifies them by content. Amendments 1 and 2 are code and are being dispatched. Amendment 3 is a PM action on card #5935 and is this seat's own to discharge.

⭐ Amendment 3 is worth calling out: it is a machine-checkable landing precondition that this seat could not have caught itselfcheck-clause2-carriers.mjs cannot run in this session at all (exit 2, no GitHub token; MCP-only access), so the seat had fallen back to reading labels by hand, and a hand check cannot see a missing claim-line spelling. The reviewer ran the tool's parser directly and found the card limb reads absent. That is exactly the kind of finding an isolated reviewer at tier exists to produce.


Generated by Claude Code

…state the discovery bound (objectui#5935)
Both required amendments from the Clause-② contract review of PR #7491
(comment 5524970713). The seam, the call sites, the alias and the five new
suites are untouched.
Amendment 1 — the two-copies pin is now BEHAVIOURAL.
`scripts/__tests__/check-lucide-icon-record-names.test.ts` asserted only the
resolver's SOURCE TEXT, so it was blind to the gate moving on its own: the
resolver's text is then unchanged and every source-text row still passes. The
review measured it — widening ONLY `toRecordKey` to `split(/[-_.\s]+/)` left
the file green 40/40 — and that is the gate-wider direction, exactly the
under-reporting class this card exists to close.
The file now imports `describeIconLookup` from the resolver and asserts
`toRecordKey(s) === describeIconLookup(s).key` over a corpus derived from the
runtime record: every one of the 1,767 live keys re-spelled six ways, plus a
per-character separator sweep. The sweep is not decoration — no key of the
record contains a separator at all, so re-spellings alone cannot see a
character-class widening, which is why the measured hole survived them. A
second row is the control that the corpus can in fact separate the two,
modelling the drifted gate by substitution on the shipped `toRecordKey` rather
than transcribing the rule a third time. The source-text rows are kept; they
catch a different thing.
TypeScript is imported straight out of `packages/` the way
`known-schema-types-derivation-5115.test.ts` already does, so the pin costs no
build step, and by a RELATIVE path so `describeIconLookup` stays off the
package entry and the published widening stays at one symbol.
Amendment 2 — the enforcement bound is stated instead of over-claimed.
The `DECLARED_RECORD_READERS` comment said a new container that hand-rolls a
lookup turns the gate red. It holds for one syntactic shape. Measured against
this gate: named `icons` import (rename included) plus element access on that
binding, through parens/`as`/`!`, any argument expression — red. Namespace
import, a binding re-bound through another const, property access,
destructuring, `require()`/dynamic `import()`, and anything inside a string or
template literal — green. objectui#7472 is cited as the known outside-predicate
resolver and is outside three ways at once. The discovery predicate is ⛔ NOT
widened; that is a separate design question.
The Normalisation comment's "either side ... fails loudly" is likewise made
true rather than reworded away: it now names the behavioural row that carries
it and records that the source-text rows do not.
Verification: gate exits 0 at 1 record-reading resolver; the 7 relevant suites
96 green (94 before, +2 rows); ablation red/green with the restore proven by
state; `type-check:scripts`, `lint:root` (0 errors on changed files) and
`check-control-bytes` green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3180.9 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DpQmpEDV.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.31KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.69KB21.03KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Amendments 1 and 2 applied — 089e35043. ⛔ Still draft, needs:contract-review untouched.

Against the contract review's PASS WITH REQUIRED AMENDMENTS (comment 5524970713). Amendment 3 is the PM seat's and was not touched. The mechanism — tokeniser, seam, Home to House alias, seven call sites, five new suites — is byte-unchanged; the commit touches two files, both under scripts/.

Amendment 1 — the two-copies pin is behavioural

scripts/__tests__/check-lucide-icon-record-names.test.ts now imports describeIconLookup from the resolver by a relative path (so it stays off the package entry and the published widening stays at one symbol) and asserts toRecordKey(s) equals describeIconLookup(s).key over a corpus derived from the runtime record: all 1,767 live keys re-spelled six ways, plus a per-character separator sweep. The source-text rows are kept.

The sweep is load-bearing, and the review's own prescription would not have worked without it. The amendment text lists the re-spellings (kebab/snake/spaced/Pascal/lower/doubled) as the corpus. Those alone cannot catch the measured hole: no key of the record contains a separator at all (0 of 1,767, control — 95 contain a digit), so widening the class by . changes nothing about any of them and the pin would have stayed green under the very mutation it was commissioned to catch. Measured before writing the row: 0 of 10,602 re-spellings differ under /[-_.\s]+/; 19 corpus members do once hostile separators are in. So the corpus sweeps the character class, not the one character.

The ablation, reproducing the review's probe. Implementation committed first, so the restore leg has a real reference point; mutation and restore both proven by state, never by exit code.

legreading
pre-mutation, on diskshipped .split(/[-_\s]+/) present 1, mutated form present 0
post-mutation, on diskshipped 0, mutated 1; blob 4985efe76 vs HEAD e1600563f; git diff HEAD numstat 1 1
gate script under the mutationexit 0, still "1 record-reading resolver" — the drift is invisible to the gate's own run
pin suite under the mutation2 failed, 40 passed (42) — the two new rows, and only those
restoregit checkout HEAD -- ABSOLUTE_PATH under trap ... EXIT INT TERM; git diff HEAD empty and blob back to e1600563f
green leg on the restored tree42 passed (42)

The 40 pre-existing rows staying green under the mutation is the review's finding reproduced exactly. The .mjs Normalisation comment is made true rather than reworded away: it now names the behavioural row that carries "either side fails loudly" and records that the source-text rows do not.

Amendment 2 — the enforcement bound is stated

Replaced at DECLARED_RECORD_READERS, and the same over-claim corrected in the PR body. Every line of the new comment is measured against discoverResolvers, not reasoned about — 19 syntactic shapes through the real predicate:

Inside (red): named icons import; a renamed named import ({ icons as reg }); (icons as Record)[k]; and any argument expression, a template literal included — the predicate never looks at the argument.
Outside (green, not caught): namespace import; a binding re-bound through another const; property access (icons.House); destructuring; require(); dynamic import(); and anything inside a string or template literal.

⚠️One correction to the amendment text, which I would rather state than quietly obey. It says "namespace-import, re-bound and template-literal forms are outside it". Element access with a template-literal argument is inside — measured red. What is outside is code emitted inside a template literal, which is #7472's first of three reasons. Both readings are defensible from the phrase, so the new comment spells out which is which rather than repeating the ambiguity. #7472 is cited, and it is outside three ways at once: app-generator.ts emits, inside a template literal, import * as LucideIcons from 'lucide-react' re-bound to a lucideIcons const and then indexed. ⛔ The discovery predicate is not widened.

Verification, on the pushed head

  • 7 relevant suites: 7 files, 96 tests passed (the review's 94, plus the 2 rows). This file alone 42, was 40.
  • Icon union re-run at 089e35043: 18 files, 184 tests passed — 182 plus the same 2. Re-run because a push after review invalidates a sha-stamped union reading.
  • Every other suite naming the edited gate script — entry-guard wiring, scripts type-check parity, breadcrumb / command / context-menu item icons, icon-renderer-declared-default, ViewSwitcher: 7 files, 62 tests passed.
  • node scripts/check-lucide-icon-record-names.mjsexit 0, OK lucide icon names: 182 authored/declared names reaching 1 record-reading resolver are live icons keys.
  • pnpm type-check:scripts — VERDICT command-exit 0. pnpm lint:root (the scope that owns scripts/) — 0 errors, 32 warnings, 0 of them in either changed file; all 32 are pre-existing no-explicit-any in e2e specs, two unrelated scripts/__tests__ files and two vitest setup files.
  • node scripts/check-control-bytes.mjs — OK, 6,158 tracked text files.

Exit codes were captured by redirecting first (cmd > log 2>&1; EXIT=$?), never after a pipe, and each verdict is quoted from the line the tool printed.

⚠️ Byte note: the separator sweep needs NBSP and the ideographic space. Both are written as / escapes — a literal one is a character no reviewer can see and no grep can be aimed at. Verified: 0 invisible or control characters in either changed file.

The changeset was checked and not edited: it makes no claim about the discovery predicate, and its "the under-reporting is closed rather than merely bounded" is what amendment 1 makes true.

⛔ Not done, deliberately: PR not flipped ready, no auto-merge, needs:contract-review left on. The package: components / plugin / tests labels appeared from the labeler, not from me.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

✅ Amendments verified — needs:contract-review cleared, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Provenance: 「PASS ⇒ 同席剥标、ready、auto-merge」 (维护者 2026-08-31 裁定). The tier review returned PASS WITH REQUIRED AMENDMENTS (5524970713); all three are now in.

Verified by content at 089e35043, not from the report

  • Scope: parent is the reviewed head 675571f8f; 2 files, both under scripts/. The seam, the seven call sites, the alias and the five new suites are byte-unchanged — confirmed by filtering the commit's file list for anything outside scripts/ and getting an empty set.
  • Amendment 1 — the pin is behavioural and cannot silently go vacuous. It imports describeIconLookup from the resolver and asserts toRecordKey(s) === describeIconLookup(s).key over 8,000+ spellings. ⭐ More than was asked: it carries its own non-vacuity control — it models a dot-splitting gate and asserts the corpus can separate the two, naming arrow.right and building.2. So a future corpus edit that quietly drops the hostile separators turns the suite red rather than passing in silence. That is strictly better than the one-off ablation the amendment prescribed, because it keeps holding after today.
  • Amendment 2 — the bound is stated, the predicate is NOT widened. The DECLARED_RECORD_READERS comment now names what is inside (named icons import, rename included, plus element access through parens / as / !, any argument expression) and what is outside (namespace import, re-bound const, property access, destructuring, require()/dynamic import(), code inside a string or template literal), and cites finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472. The PR body's matching sentence is corrected too.
  • Amendment 3 was discharged by this seat on the card (5524981959).
  • CI: 32/32 at 089e35043 — 29 success, 3 skipped by design. Test (shard 1/4) 11:52:22 · 2/4 11:52:16 · 3/4 11:54:14 · 4/4 11:54:17 · Type Check 11:50:22 · Lint 11:47:10 · Bundle Analysis, README Export Check, Governed Surface Queue Guard each read individually.

⭐ The implementer corrected the review, and it is the most valuable thing in this round

Amendment 1's prescribed corpus would not have reproduced its own proof. The review said to assert over the enumeration's re-spellings (kebab/snake/spaced/Pascal/lower/doubled). Measured: no key of the record contains a separator at all (0 of 1,767; control "contains a digit" fires at 95), so 0 of 10,602 re-spellings change under /[-_.\s]+/. A pin built to the amendment's letter would have stayed green under the exact mutation it was commissioned to catch.

The implementer added a per-character separator sweep instead — that is what makes it fire — and said so plainly rather than reporting compliance. ⇒ I ratify the deviation. Amendment 1's requirement was a pin that is behavioural and proven to fire; the letter of its corpus was insufficient for its own purpose, and following the letter would have produced a pin that certified nothing. Measured after: 2 of 42 red under that mutation, with the gate script itself still exiting 0 — which is exactly why the pin, not the gate, has to hold this.

Three further corrections from the same round, recorded because two of them are mine:

  1. ⚠️My brief said "template-literal forms are outside" the predicate — wrong as written. An element access whose argument is a template literal (icons[`${a}${b}`]) is inside; the predicate reads the base identifier and never the argument. What is outside is code emitted inside a template literal. The new comment spells out which is which instead of repeating the ambiguity. The implementer also measured that a renamed named import is inside — which the review did not mention.
  2. finding(cli): an EIGHTH lucide icon-name resolver is emitted inside a template literal in app-generator.ts, with zero normalisation — structurally invisible to the record-reading census three ways over #7472's own stated third reason is misdescribed (in that issue, not here): it says the base is "a property access", where the generated code actually re-binds a namespace import through a const. Both are outside the predicate so the conclusion stands; only the reason is wrong. Left alone as out of scope.
  3. The changeset needed no edit — it makes no claim about the discovery predicate, and its "under-reporting closed rather than merely bounded" line is precisely what amendment 1 makes true.

An accepted, named gap — not an oversight

The seam's own docblock (packages/components/src/renderers/action/resolve-icon.ts) still ends its enforcement paragraph with the same unqualified sentence amendment 2 corrected in the .mjs. It is left as-is: the tier reviewer examined that docblock and judged it accurate in context (it is preceded by an accurate predicate sentence), and my dispatch scoped the seam out so the mechanism under review stayed byte-stable. ⇒ A one-sentence follow-up available any time; recorded here so it is a chosen gap rather than one nobody noticed.

Carriers

needs:contract-review removed from this PR (remaining: package: components, plugin, tests). Card #5935 never carried it — its labels are pm:dispatched, domain:ui. ⚠️ That is the same dual-carrier gap the reviewer found on the declaration side, from the other direction: the gate label was hung on one carrier only. I also set #5935's assignee, which was empty while this PR was open — a half-state that invites a second agent onto claimed work.

⚠️ Both readings are manual: scripts/pm/check-clause2-carriers.mjs exits 2 in this session (no GitHub token; MCP-only access), and its own output says that is not a clearance. A hand label-check covers strictly less than the tool — naming the instrument, as on every landing this shift.


Generated by Claude Code

os-sam pushed a commit that referenced this pull request Sep 4, 2026
…ecord the third zero
objectui#5936 asked for the consumer of a component registration's `icon` meta
before extending the icon gate to it. Three first-party populations were
measured and none has one (objectui, objectstack, cloud), so the gate is not
extended — adjudicated 2026-09-04.
Records that where the next reader hits it (the gate's own "Deliberate
boundaries"), with the three readings, their controls, the adjudication, and
the caveat that three zeros are not a proof of absence.
Retires the membership half of the `ui:icon` local pin, which was kept at
objectui#5633 on the premise that the palette lives outside this repo. It
guarded 1 of the 45 registrations declaring an `icon` meta while the gate is
adjudicated not to cover the other 44, and its hand-copied tokeniser had
already gone stale against the seam objectui#5935 / PR #7491 created. The
coupling half stays: it needs no external vocabulary and cannot drift.
The published `ComponentMeta.icon` key is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-project-manager@claude