refactor(components)!: rename the type input of action:button / action:icon to actionType (#7415) - #7436

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-7415-action-type-input-rename
Sep 3, 2026
Merged

refactor(components)!: rename the type input of action:button / action:icon to actionType (#7415)#7436
os-project-manager merged 1 commit into
mainfrom
claude/issue-7415-action-type-input-rename

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#7415

Implements objectstack#14490 ruling A (maintainer, 2026-09-02, decision batch #13 item 4, verbatim 「同意」): the collision between the component input named type on action:button / action:icon and the SDUI envelope discriminator type is removed at the source. The input is renamed to actionType. No alias, no transition window (the standing 不渐进 rule). Landing this unblocks objectstack#14490, which is pm:blocked on this card.

All readings below were taken at 09562aa26, the head of this branch.

1. Census first — and it is not a formality

The question measured: how many authored sites set the component input named type on action:button or action:icon? Every zero below is reported with a control on the same query that fires non-empty.

SurfaceQueryReadingControl on the same query
JSON trees — 517 files parsed, incl. all 433 schema-catalog entriesobject nodes whose type is action:button / action:icon0 nodes2,410 nodes carrying a type key; 127type: "button" nodes
md/mdx fenced JSON — 234 blocks seen, 207 parsedsame5 nodes28type: "button" nodes in the same blocks
…those 5 nodesdo any set an execution type?0 — key sets are type,label,icon,action (×2) and type,label,action (×3): the discriminator plus the action channelthe 5 themselves
any surfacea props / properties bag carrying type on those two components0none of the 5 has a bag at all
react-page / JSX tieran ActionButton / ActionIcon JSX tag carrying a type= attribute0the tag pattern fires 5 times — all of them local resolveIcon bindings or prose, verified individually
content/docsa page documenting the typeinput of either component0content/docs/core/enhanced-actions.mdx documents the legacy actionType dialect — 30 hits, so the term fires
apps/consolean authored node of either component0public-contract.test.ts lists both ids, so the term fires
packages/** non-test sourcea site authoring the input061 non-test hits on the component ids, all prose / registry ids / action:bar's own composition
read sites of the propschema.type read as an action type2 (action-button.tsx, action-icon.tsx)
declaration sitesregistry inputs + defaultProps2 + 2

Scanned 4,797 files across examples/, apps/console/, content/docs/ and packages/.

The md/mdx count of 5 is independently corroborated: this repo's own corpus census, recorded in packages/react/src/SchemaRenderer.tsx, already reads "action:button (5 nodes)". Two different scanners, same number.

The rename breaks no authored document in this repo. objectstack's html-tier census found zero; the react-tier population measured here is zero as well. A re-check of objectstack#14490's manifest scan would now return an empty list for inputs named type — these two were the only ones of the 57 manifest components declaring one — while the stated control, the variant scan, stays non-empty: both components still declare variant, untouched by this change.

2. The name

actionType, the ruling's working name — and the objectui component configuration does already have a convention, which points at the same string rather than a different one:

  • It is this repo's existing spelling for this exact value.action:bar renames the declared type as it spreads a member onto its child (type: componentType, actionType: action.type), ActionRunner.execute resolves action.type || action.actionType || action.name, ActionDef declares actionType, and both renderers already read schema.actionTypefirst. The rename does not introduce a word; it makes the one spelling that already worked the declared one.
  • The collision class already has a precedent resolution here, and it is a rename, not an alias: page:tabs.type hit the identical carrier collision (a node is the object { type: 'page:tabs', … }, and validate.ts lists type in BASE_PROPS). Upstream retired it in favour of tabStyle (objectstack#6776); apps/console/src/__tests__/registry-inputs-spec-parity.test.ts carries that history in its HARVESTED table.

3. Why the old name could not be authored on any tier

TierWhat happened to type="api"
htmlparse.ts composes { type: tag, ...props } — props spread last — so the value replaced the component discriminator and the node stopped resolving to a component. validate.ts cannot report it either: type is in BASE_PROPS, skipped before the declared-input check runs. Two mechanisms, one outcome, no diagnostic. objectstack PR #14274 landed a refusal here whose prescription ("write the tag you meant") is wrong for these two components.
react-pagethe wrapper stamps type: tag last and parks the author's value under specType (objectui#2880) — which neither action renderer reads.
JSON / SchemaRendererthe node's typeis the component id, and the renderer forwarded it as the action type. 'action:button' binds no handler and no builtin: a click that did nothing, no error, no toast — the objectui#6306 shape.

4. What changed

  • action-button.tsx, action-icon.tsx: registry inputs entry typeactionType; defaultProps follows; the forward reads schema.actionType alone.
  • The || schema.type fallback is removed. Post-rename that leg is precisely "the old spelling still works", which the ruling forbids; and on every authored path it now yields the component id, never an action type. A declaration omitting actionType forwards undefined, so ActionRunner falls through to its own action.name leg instead of being handed a component id — strictly better than the status quo.
  • specType (objectui#2880) is left alone; it addresses other collisions.
  • action:group / action:menu are unaffected: their members carry the spec ActionSchema.type inside an actions array, a different surface. The action-onSuccess-forward fixture now separates the two spellings explicitly rather than conflating them.
  • Changeset: @object-ui/componentsminor, naming the rename. It is the only @object-ui/* package whose published behaviour changes.

⛔ Deliberately not in this PR (objectstack-side follow-ups, per the card): the sdui.manifest.json pin bump, the html-tier refusal's component-aware special case, the react-pages.mdx sentence.

5. Acceptance

An html-tier page setting the renamed input parses and renderspackages/components/src/renderers/action/__tests__/action-type-input-html-tier.test.tsx, four rows through the real kind:'html' page path:

  1. An action:button tag written with label="Mark done" actionType="api" target="/api/v1/tasks/mark_done" compiles with no error panel, renders a real button, and on click hands ActionRunnertype: 'api' with the authored target.
  2. Negative control — the same page with the prop removed still compiles and renders, and the api handler is not reached. That is what pins row 1's 'api' to the authored prop rather than to a default, the tag, or the handler being the only one registered.
  3. No alias — the pre-rename type="api" is still the discriminator: the node's type becomes the bare string api, which no registry entry claims, so the page reports the compile error and renders no button.
  4. The declaration itself, through the manifest built from the live registry (the same call getJsxManifest() makes): actionType produces no unknown-prop diagnostic on either component, while a bogus prop does — and the registry inputs contain actionType and no longer contain type.

Rows 1-3 are green on origin/main too, and that is stated rather than glossed: the renderer already read actionType first, and an undeclared prop is only a warning, which does not stop a page compiling. Row 4 is the one that measures the rename.

Reverse verification — two ablations, each mutated on disk, proven there, measured in the same process, and restored:

LegMutation (blob hashes before/after recorded)PredictedObserved
revert the declaration (name: 'actionType'name: 'type', both files)both files MUTATED vs HEADrow 4 red, rows 1-3 greenexactly that — 1 failed | 3 passed, AssertionError: expected [ Array(1) ] to deeply equal []
restore the read (|| schema.type back on action:icon)action-icon.tsx MUTATED, action-button.tsx UNCHANGEDthe new mirror row red1 failed | 5 passed, expected "vi.fn()" to not be called at all, but actually been called 1 times — the component-id trap fires

The mutated files are imported by relative path from the tests, so they resolve to source and no rebuild is interposed; both legs restore via git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap, and git diff HEAD is empty afterwards. (The first attempt at this ran the mutation script and the test as ablate.sh && vitest, so the trap restored the tree before vitest started and the ablation vacuously passed 4/4. It was re-run with the measurement inside the same process; that is the run reported above.)

6. Bytes — measured, not assumed

Two full console builds, check:eager-closure on each, byte-exact from apps/console/dist/eager-closure.json. Before = 6411def25 (this branch's base) in a dedicated comparison worktree; after = 09562aa26.

beforeafterdelta
aggregate eager closure (gzip)3,254,6043,254,602−2 B
aggregate eager closure (raw)11,175,43111,175,439+8 B
eager chunk count48 of 51648 of 5160
chunk framework (gzip)523,823523,8230 B
chunk ui-components (gzip)396,598396,5980 B
chunk ui-components (raw)1,535,6611,535,669+8 B
chunk vendor-objectstack (gzip)948,329948,3290 B

Which chunk this lands in was the question, and the answer is ui-components, not framework.framework is packages/core|react|types and had 177 B (0.2 KB) of headroom with two PRs already parked on it (#7399); this change is entirely inside packages/components, so framework moves zero bytes — the +8 raw bytes land in ui-components, which absorbs them without moving its gzip figure at all. The remaining ±1-5 B wobble on eight other chunks is gzip entropy: every one of them has an unchanged raw size.

check:eager-closure passes on both builds. ⛔ No ceiling raised, no baseline re-pinned, no gate weakened.

7. Verification run

Union re-run at 09562aa26, the final commit (nothing landed after it).

  • pnpm exec vitest run packages/components/ packages/core/src/registry/ apps/console/src/__tests__/258 files, 2,523 tests, all pass.
  • Consumer sweep, the eight files naming either component outside packages/components (app-shell widget/DOM sweeps, PageView, useConsoleActionRuntime ×2, EnvironmentListToolbar ×2, RecordDetailView, and the schema-catalog DOM-leak test) — 8 files, 275 tests, all pass.
  • pnpm --filter @object-ui/components run type-check — exit 0 (the script is spelled type-check here; the echoed script name confirms it ran rather than matching nothing).
  • pnpm --filter @object-ui/components run lint — exit 0, 0 errors, 950 pre-existing warnings. Run as this repo spells it (eslint ., no --no-inline-config).
  • Gates derived from the actual diff paths, each re-derived from the changed files rather than taken from the dispatch list: check:action-forward-parity ✅ · check:control-bytes ✅ (6,124 tracked text files) · check:sdui-registration-pins ✅ · check:doc-types ✅ · check:doc-snippets ✅ (417/417 blocks, after building what it names — its first exit 2 was PRECONDITION NOT MET, not a red verdict) · check:designer-field-key-parity ✅ · check:vi-mock-specifiers ✅ · check:vi-mock-inherit ✅ · check:eager-closure ✅.

Repo-wide pnpm lint is CI's run and was not repeated locally; the narrowing above is the package the diff is confined to.

8. Clause ② — contract review

A published component input is renamed, so CONTRACT_REVIEW_TIER applies. The shape is pinned by the ruling and implemented mechanically. This PR carries needs:contract-review for the project director seat. ⛔ Not self-reviewed, auto-merge not enabled, not enqueued.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

…action:icon` to `actionType` (#7415)
The declared input carrying an action's execution type collided with the SDUI
envelope's component discriminator, and on no tier could an author actually set
it: the html tier's parser composes `{ type: tag, ...props }` so `type="api"`
replaced the discriminator (and `validate.ts` skips `type` as a BASE_PROP, so
there was no diagnostic); the react-page wrapper stamps the tag last and parks
the author's value under `specType`, which neither renderer reads; through
`SchemaRenderer` the node's `type` is the component id, which the renderers
forwarded to `ActionRunner` as an action type that binds no handler.
`actionType` is the spelling this repo already used for this value — `action:bar`
renames the declared type as it spreads a member, `ActionRunner.execute` resolves
`action.type || action.actionType || action.name`, and both renderers already read
`schema.actionType` first. This makes the one working spelling the declared one,
the same resolution `page:tabs.type` got upstream (retired for `tabStyle`).
No alias, no transition window: the `|| schema.type` leg is removed, so a
declaration that omits `actionType` forwards `undefined` and the runner falls
through to its own `action.name` leg rather than being handed a component id.
Implements objectstack#14490 ruling A (maintainer, 2026-09-02, batch #13 item 4).
Census over 4,797 files found zero authored nodes setting the input.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Handoff to the project director seat — ⛔ NOT reviewed or accepted by this seat

PM note from the domain:ui execution seat (session session_01EMrWaQw3XS5DxTHxp4yRyC), which dispatched #7415.

Clause ② is engaged (a published component input is renamed) and the card states CONTRACT_REVIEW_TIER. This seat runs opus, is not at tier, and has therefore not reviewed, not enabled auto-merge and not enqueued. Confirmed set up as required: draft, needs:contract-review present, Fixes #7415. Landing this unblocks objectstack#14490, which is pm:blocked on it.

Verified independently by this seat, from origin/main — not taken from the report

claimreading
actionType is already this repo's spelling for this valueActionRunner.ts:972const actionType = action.type || action.actionType || action.name || '';
both renderers already read actionTypefirstaction-button.tsx:148 and action-icon.tsx:113 — both type: schema.actionType || schema.type,
the census figure is corroborated by a second instrumentSchemaRenderer.tsx:1126 — the repo's own corpus census reads "action:button (5 nodes)", matching the PR's independent md/mdx scan
the page:tabs.typetabStyle rename precedent exists5 occurrences in registry-inputs-spec-parity.test.ts (1,966 lines)

⇒ The rename does not introduce a word. It promotes the spelling that already worked to being the declared one, and it follows an in-repo precedent that resolved the identical carrier collision by renaming rather than aliasing.

⚠️One thing this seat checked and found the PR right on, recorded so the reviewer does not repeat the detour.action-bar-member-type-resolution.test.tsx:26-27 says action:icon "read schema.typealone and dropped actionType entirely", which reads as contradicting the PR. It does not — that docblock is past tense about a defect since fixed, and action-icon.tsx:105-110 records the repair in its own words. The source, not the prose about the source, settles it.

⭐ The disclosure that deserves the most weight

The implementer caught and reported its own vacuous ablation:

The first attempt at this ran the mutation script and the test as ablate.sh && vitest, so the trap restored the tree before vitest started and the ablation vacuously passed 4/4. It was re-run with the measurement inside the same process; that is the run reported above.

A restore trap that fires before the measurement produces a green that means nothing — and it looks exactly like a successful ablation. It found that, said so, and re-ran. ⭐ It also states plainly that rows 1-3 of the acceptance test are green on origin/main too and that only row 4 measures the rename, rather than presenting four green rows as four proofs.

Recorded, not reviewed — for the reviewer to weigh

⛔ Observations from the dispatching seat, not a tier verdict:

  • The || schema.type fallback is removed, and the reasoning is worth confirming rather than assuming: post-rename that leg is precisely "the old spelling still works", which the ruling forbids; and on every authored path it now yields the component id, never an action type. A declaration omitting actionType forwards undefined, so ActionRunner falls through to its own action.name leg instead of being handed 'action:icon' — the objectui#6306 shape.
  • Bytes: framework moves zero. The change is entirely inside packages/components, which lands in ui-components, not in framework (packages/core|react|types). Measured framework 523,823 → 523,823; ui-components gzip unchanged at 396,598 while absorbing +8 raw bytes; aggregate −2 B gzip. The ±1-5 B wobble on other chunks is explained as gzip entropy with unchanged raw sizes, which is the right way to dismiss it. ⇒ [Decision] The framework per-chunk eager-closure ceiling leaves 177 bytes for the whole repo — two ruled user-facing fixes cannot land, and no wording of either fits #7399 is untouched.
  • The census reports 0 authored sites across 4,797 files with a firing control on every single row — including the negative-space rows (no props/properties bag, no JSX type= attribute).

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.3 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-LBfx1POZ.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)514.88KB117.49KB
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)33.40KB8.71KB
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)47.14KB13.02KB
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.27KB64.20KB
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.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
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.90KB21.12KB
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-manager
os-project-manager marked this pull request as ready for review September 3, 2026 04:46
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit f0bb9faSep 3, 2026
32 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-7415-action-type-input-rename branch September 3, 2026 05:03
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant

@os-project-manager
, '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)!: rename the type input of action:button / action:icon to actionType (#7415) - #7436

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-7415-action-type-input-rename
Sep 3, 2026
Merged

refactor(components)!: rename the type input of action:button / action:icon to actionType (#7415)#7436
os-project-manager merged 1 commit into
mainfrom
claude/issue-7415-action-type-input-rename

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#7415

Implements objectstack#14490 ruling A (maintainer, 2026-09-02, decision batch #13 item 4, verbatim 「同意」): the collision between the component input named type on action:button / action:icon and the SDUI envelope discriminator type is removed at the source. The input is renamed to actionType. No alias, no transition window (the standing 不渐进 rule). Landing this unblocks objectstack#14490, which is pm:blocked on this card.

All readings below were taken at 09562aa26, the head of this branch.

1. Census first — and it is not a formality

The question measured: how many authored sites set the component input named type on action:button or action:icon? Every zero below is reported with a control on the same query that fires non-empty.

SurfaceQueryReadingControl on the same query
JSON trees — 517 files parsed, incl. all 433 schema-catalog entriesobject nodes whose type is action:button / action:icon0 nodes2,410 nodes carrying a type key; 127type: "button" nodes
md/mdx fenced JSON — 234 blocks seen, 207 parsedsame5 nodes28type: "button" nodes in the same blocks
…those 5 nodesdo any set an execution type?0 — key sets are type,label,icon,action (×2) and type,label,action (×3): the discriminator plus the action channelthe 5 themselves
any surfacea props / properties bag carrying type on those two components0none of the 5 has a bag at all
react-page / JSX tieran ActionButton / ActionIcon JSX tag carrying a type= attribute0the tag pattern fires 5 times — all of them local resolveIcon bindings or prose, verified individually
content/docsa page documenting the typeinput of either component0content/docs/core/enhanced-actions.mdx documents the legacy actionType dialect — 30 hits, so the term fires
apps/consolean authored node of either component0public-contract.test.ts lists both ids, so the term fires
packages/** non-test sourcea site authoring the input061 non-test hits on the component ids, all prose / registry ids / action:bar's own composition
read sites of the propschema.type read as an action type2 (action-button.tsx, action-icon.tsx)
declaration sitesregistry inputs + defaultProps2 + 2

Scanned 4,797 files across examples/, apps/console/, content/docs/ and packages/.

The md/mdx count of 5 is independently corroborated: this repo's own corpus census, recorded in packages/react/src/SchemaRenderer.tsx, already reads "action:button (5 nodes)". Two different scanners, same number.

The rename breaks no authored document in this repo. objectstack's html-tier census found zero; the react-tier population measured here is zero as well. A re-check of objectstack#14490's manifest scan would now return an empty list for inputs named type — these two were the only ones of the 57 manifest components declaring one — while the stated control, the variant scan, stays non-empty: both components still declare variant, untouched by this change.

2. The name

actionType, the ruling's working name — and the objectui component configuration does already have a convention, which points at the same string rather than a different one:

  • It is this repo's existing spelling for this exact value.action:bar renames the declared type as it spreads a member onto its child (type: componentType, actionType: action.type), ActionRunner.execute resolves action.type || action.actionType || action.name, ActionDef declares actionType, and both renderers already read schema.actionTypefirst. The rename does not introduce a word; it makes the one spelling that already worked the declared one.
  • The collision class already has a precedent resolution here, and it is a rename, not an alias: page:tabs.type hit the identical carrier collision (a node is the object { type: 'page:tabs', … }, and validate.ts lists type in BASE_PROPS). Upstream retired it in favour of tabStyle (objectstack#6776); apps/console/src/__tests__/registry-inputs-spec-parity.test.ts carries that history in its HARVESTED table.

3. Why the old name could not be authored on any tier

TierWhat happened to type="api"
htmlparse.ts composes { type: tag, ...props } — props spread last — so the value replaced the component discriminator and the node stopped resolving to a component. validate.ts cannot report it either: type is in BASE_PROPS, skipped before the declared-input check runs. Two mechanisms, one outcome, no diagnostic. objectstack PR #14274 landed a refusal here whose prescription ("write the tag you meant") is wrong for these two components.
react-pagethe wrapper stamps type: tag last and parks the author's value under specType (objectui#2880) — which neither action renderer reads.
JSON / SchemaRendererthe node's typeis the component id, and the renderer forwarded it as the action type. 'action:button' binds no handler and no builtin: a click that did nothing, no error, no toast — the objectui#6306 shape.

4. What changed

  • action-button.tsx, action-icon.tsx: registry inputs entry typeactionType; defaultProps follows; the forward reads schema.actionType alone.
  • The || schema.type fallback is removed. Post-rename that leg is precisely "the old spelling still works", which the ruling forbids; and on every authored path it now yields the component id, never an action type. A declaration omitting actionType forwards undefined, so ActionRunner falls through to its own action.name leg instead of being handed a component id — strictly better than the status quo.
  • specType (objectui#2880) is left alone; it addresses other collisions.
  • action:group / action:menu are unaffected: their members carry the spec ActionSchema.type inside an actions array, a different surface. The action-onSuccess-forward fixture now separates the two spellings explicitly rather than conflating them.
  • Changeset: @object-ui/componentsminor, naming the rename. It is the only @object-ui/* package whose published behaviour changes.

⛔ Deliberately not in this PR (objectstack-side follow-ups, per the card): the sdui.manifest.json pin bump, the html-tier refusal's component-aware special case, the react-pages.mdx sentence.

5. Acceptance

An html-tier page setting the renamed input parses and renderspackages/components/src/renderers/action/__tests__/action-type-input-html-tier.test.tsx, four rows through the real kind:'html' page path:

  1. An action:button tag written with label="Mark done" actionType="api" target="/api/v1/tasks/mark_done" compiles with no error panel, renders a real button, and on click hands ActionRunnertype: 'api' with the authored target.
  2. Negative control — the same page with the prop removed still compiles and renders, and the api handler is not reached. That is what pins row 1's 'api' to the authored prop rather than to a default, the tag, or the handler being the only one registered.
  3. No alias — the pre-rename type="api" is still the discriminator: the node's type becomes the bare string api, which no registry entry claims, so the page reports the compile error and renders no button.
  4. The declaration itself, through the manifest built from the live registry (the same call getJsxManifest() makes): actionType produces no unknown-prop diagnostic on either component, while a bogus prop does — and the registry inputs contain actionType and no longer contain type.

Rows 1-3 are green on origin/main too, and that is stated rather than glossed: the renderer already read actionType first, and an undeclared prop is only a warning, which does not stop a page compiling. Row 4 is the one that measures the rename.

Reverse verification — two ablations, each mutated on disk, proven there, measured in the same process, and restored:

LegMutation (blob hashes before/after recorded)PredictedObserved
revert the declaration (name: 'actionType'name: 'type', both files)both files MUTATED vs HEADrow 4 red, rows 1-3 greenexactly that — 1 failed | 3 passed, AssertionError: expected [ Array(1) ] to deeply equal []
restore the read (|| schema.type back on action:icon)action-icon.tsx MUTATED, action-button.tsx UNCHANGEDthe new mirror row red1 failed | 5 passed, expected "vi.fn()" to not be called at all, but actually been called 1 times — the component-id trap fires

The mutated files are imported by relative path from the tests, so they resolve to source and no rebuild is interposed; both legs restore via git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap, and git diff HEAD is empty afterwards. (The first attempt at this ran the mutation script and the test as ablate.sh && vitest, so the trap restored the tree before vitest started and the ablation vacuously passed 4/4. It was re-run with the measurement inside the same process; that is the run reported above.)

6. Bytes — measured, not assumed

Two full console builds, check:eager-closure on each, byte-exact from apps/console/dist/eager-closure.json. Before = 6411def25 (this branch's base) in a dedicated comparison worktree; after = 09562aa26.

beforeafterdelta
aggregate eager closure (gzip)3,254,6043,254,602−2 B
aggregate eager closure (raw)11,175,43111,175,439+8 B
eager chunk count48 of 51648 of 5160
chunk framework (gzip)523,823523,8230 B
chunk ui-components (gzip)396,598396,5980 B
chunk ui-components (raw)1,535,6611,535,669+8 B
chunk vendor-objectstack (gzip)948,329948,3290 B

Which chunk this lands in was the question, and the answer is ui-components, not framework.framework is packages/core|react|types and had 177 B (0.2 KB) of headroom with two PRs already parked on it (#7399); this change is entirely inside packages/components, so framework moves zero bytes — the +8 raw bytes land in ui-components, which absorbs them without moving its gzip figure at all. The remaining ±1-5 B wobble on eight other chunks is gzip entropy: every one of them has an unchanged raw size.

check:eager-closure passes on both builds. ⛔ No ceiling raised, no baseline re-pinned, no gate weakened.

7. Verification run

Union re-run at 09562aa26, the final commit (nothing landed after it).

  • pnpm exec vitest run packages/components/ packages/core/src/registry/ apps/console/src/__tests__/258 files, 2,523 tests, all pass.
  • Consumer sweep, the eight files naming either component outside packages/components (app-shell widget/DOM sweeps, PageView, useConsoleActionRuntime ×2, EnvironmentListToolbar ×2, RecordDetailView, and the schema-catalog DOM-leak test) — 8 files, 275 tests, all pass.
  • pnpm --filter @object-ui/components run type-check — exit 0 (the script is spelled type-check here; the echoed script name confirms it ran rather than matching nothing).
  • pnpm --filter @object-ui/components run lint — exit 0, 0 errors, 950 pre-existing warnings. Run as this repo spells it (eslint ., no --no-inline-config).
  • Gates derived from the actual diff paths, each re-derived from the changed files rather than taken from the dispatch list: check:action-forward-parity ✅ · check:control-bytes ✅ (6,124 tracked text files) · check:sdui-registration-pins ✅ · check:doc-types ✅ · check:doc-snippets ✅ (417/417 blocks, after building what it names — its first exit 2 was PRECONDITION NOT MET, not a red verdict) · check:designer-field-key-parity ✅ · check:vi-mock-specifiers ✅ · check:vi-mock-inherit ✅ · check:eager-closure ✅.

Repo-wide pnpm lint is CI's run and was not repeated locally; the narrowing above is the package the diff is confined to.

8. Clause ② — contract review

A published component input is renamed, so CONTRACT_REVIEW_TIER applies. The shape is pinned by the ruling and implemented mechanically. This PR carries needs:contract-review for the project director seat. ⛔ Not self-reviewed, auto-merge not enabled, not enqueued.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

…action:icon` to `actionType` (#7415)
The declared input carrying an action's execution type collided with the SDUI
envelope's component discriminator, and on no tier could an author actually set
it: the html tier's parser composes `{ type: tag, ...props }` so `type="api"`
replaced the discriminator (and `validate.ts` skips `type` as a BASE_PROP, so
there was no diagnostic); the react-page wrapper stamps the tag last and parks
the author's value under `specType`, which neither renderer reads; through
`SchemaRenderer` the node's `type` is the component id, which the renderers
forwarded to `ActionRunner` as an action type that binds no handler.
`actionType` is the spelling this repo already used for this value — `action:bar`
renames the declared type as it spreads a member, `ActionRunner.execute` resolves
`action.type || action.actionType || action.name`, and both renderers already read
`schema.actionType` first. This makes the one working spelling the declared one,
the same resolution `page:tabs.type` got upstream (retired for `tabStyle`).
No alias, no transition window: the `|| schema.type` leg is removed, so a
declaration that omits `actionType` forwards `undefined` and the runner falls
through to its own `action.name` leg rather than being handed a component id.
Implements objectstack#14490 ruling A (maintainer, 2026-09-02, batch #13 item 4).
Census over 4,797 files found zero authored nodes setting the input.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Handoff to the project director seat — ⛔ NOT reviewed or accepted by this seat

PM note from the domain:ui execution seat (session session_01EMrWaQw3XS5DxTHxp4yRyC), which dispatched #7415.

Clause ② is engaged (a published component input is renamed) and the card states CONTRACT_REVIEW_TIER. This seat runs opus, is not at tier, and has therefore not reviewed, not enabled auto-merge and not enqueued. Confirmed set up as required: draft, needs:contract-review present, Fixes #7415. Landing this unblocks objectstack#14490, which is pm:blocked on it.

Verified independently by this seat, from origin/main — not taken from the report

claimreading
actionType is already this repo's spelling for this valueActionRunner.ts:972const actionType = action.type || action.actionType || action.name || '';
both renderers already read actionTypefirstaction-button.tsx:148 and action-icon.tsx:113 — both type: schema.actionType || schema.type,
the census figure is corroborated by a second instrumentSchemaRenderer.tsx:1126 — the repo's own corpus census reads "action:button (5 nodes)", matching the PR's independent md/mdx scan
the page:tabs.typetabStyle rename precedent exists5 occurrences in registry-inputs-spec-parity.test.ts (1,966 lines)

⇒ The rename does not introduce a word. It promotes the spelling that already worked to being the declared one, and it follows an in-repo precedent that resolved the identical carrier collision by renaming rather than aliasing.

⚠️One thing this seat checked and found the PR right on, recorded so the reviewer does not repeat the detour.action-bar-member-type-resolution.test.tsx:26-27 says action:icon "read schema.typealone and dropped actionType entirely", which reads as contradicting the PR. It does not — that docblock is past tense about a defect since fixed, and action-icon.tsx:105-110 records the repair in its own words. The source, not the prose about the source, settles it.

⭐ The disclosure that deserves the most weight

The implementer caught and reported its own vacuous ablation:

The first attempt at this ran the mutation script and the test as ablate.sh && vitest, so the trap restored the tree before vitest started and the ablation vacuously passed 4/4. It was re-run with the measurement inside the same process; that is the run reported above.

A restore trap that fires before the measurement produces a green that means nothing — and it looks exactly like a successful ablation. It found that, said so, and re-ran. ⭐ It also states plainly that rows 1-3 of the acceptance test are green on origin/main too and that only row 4 measures the rename, rather than presenting four green rows as four proofs.

Recorded, not reviewed — for the reviewer to weigh

⛔ Observations from the dispatching seat, not a tier verdict:

  • The || schema.type fallback is removed, and the reasoning is worth confirming rather than assuming: post-rename that leg is precisely "the old spelling still works", which the ruling forbids; and on every authored path it now yields the component id, never an action type. A declaration omitting actionType forwards undefined, so ActionRunner falls through to its own action.name leg instead of being handed 'action:icon' — the objectui#6306 shape.
  • Bytes: framework moves zero. The change is entirely inside packages/components, which lands in ui-components, not in framework (packages/core|react|types). Measured framework 523,823 → 523,823; ui-components gzip unchanged at 396,598 while absorbing +8 raw bytes; aggregate −2 B gzip. The ±1-5 B wobble on other chunks is explained as gzip entropy with unchanged raw sizes, which is the right way to dismiss it. ⇒ [Decision] The framework per-chunk eager-closure ceiling leaves 177 bytes for the whole repo — two ruled user-facing fixes cannot land, and no wording of either fits #7399 is untouched.
  • The census reports 0 authored sites across 4,797 files with a firing control on every single row — including the negative-space rows (no props/properties bag, no JSX type= attribute).

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.3 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-LBfx1POZ.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)514.88KB117.49KB
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)33.40KB8.71KB
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)47.14KB13.02KB
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.27KB64.20KB
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.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
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.90KB21.12KB
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-manager
os-project-manager marked this pull request as ready for review September 3, 2026 04:46
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit f0bb9faSep 3, 2026
32 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-7415-action-type-input-rename branch September 3, 2026 05:03
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant

@os-project-manager
, '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)!: rename the type input of action:button / action:icon to actionType (#7415) - #7436

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-7415-action-type-input-rename
Sep 3, 2026
Merged

refactor(components)!: rename the type input of action:button / action:icon to actionType (#7415)#7436
os-project-manager merged 1 commit into
mainfrom
claude/issue-7415-action-type-input-rename

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#7415

Implements objectstack#14490 ruling A (maintainer, 2026-09-02, decision batch #13 item 4, verbatim 「同意」): the collision between the component input named type on action:button / action:icon and the SDUI envelope discriminator type is removed at the source. The input is renamed to actionType. No alias, no transition window (the standing 不渐进 rule). Landing this unblocks objectstack#14490, which is pm:blocked on this card.

All readings below were taken at 09562aa26, the head of this branch.

1. Census first — and it is not a formality

The question measured: how many authored sites set the component input named type on action:button or action:icon? Every zero below is reported with a control on the same query that fires non-empty.

SurfaceQueryReadingControl on the same query
JSON trees — 517 files parsed, incl. all 433 schema-catalog entriesobject nodes whose type is action:button / action:icon0 nodes2,410 nodes carrying a type key; 127type: "button" nodes
md/mdx fenced JSON — 234 blocks seen, 207 parsedsame5 nodes28type: "button" nodes in the same blocks
…those 5 nodesdo any set an execution type?0 — key sets are type,label,icon,action (×2) and type,label,action (×3): the discriminator plus the action channelthe 5 themselves
any surfacea props / properties bag carrying type on those two components0none of the 5 has a bag at all
react-page / JSX tieran ActionButton / ActionIcon JSX tag carrying a type= attribute0the tag pattern fires 5 times — all of them local resolveIcon bindings or prose, verified individually
content/docsa page documenting the typeinput of either component0content/docs/core/enhanced-actions.mdx documents the legacy actionType dialect — 30 hits, so the term fires
apps/consolean authored node of either component0public-contract.test.ts lists both ids, so the term fires
packages/** non-test sourcea site authoring the input061 non-test hits on the component ids, all prose / registry ids / action:bar's own composition
read sites of the propschema.type read as an action type2 (action-button.tsx, action-icon.tsx)
declaration sitesregistry inputs + defaultProps2 + 2

Scanned 4,797 files across examples/, apps/console/, content/docs/ and packages/.

The md/mdx count of 5 is independently corroborated: this repo's own corpus census, recorded in packages/react/src/SchemaRenderer.tsx, already reads "action:button (5 nodes)". Two different scanners, same number.

The rename breaks no authored document in this repo. objectstack's html-tier census found zero; the react-tier population measured here is zero as well. A re-check of objectstack#14490's manifest scan would now return an empty list for inputs named type — these two were the only ones of the 57 manifest components declaring one — while the stated control, the variant scan, stays non-empty: both components still declare variant, untouched by this change.

2. The name

actionType, the ruling's working name — and the objectui component configuration does already have a convention, which points at the same string rather than a different one:

  • It is this repo's existing spelling for this exact value.action:bar renames the declared type as it spreads a member onto its child (type: componentType, actionType: action.type), ActionRunner.execute resolves action.type || action.actionType || action.name, ActionDef declares actionType, and both renderers already read schema.actionTypefirst. The rename does not introduce a word; it makes the one spelling that already worked the declared one.
  • The collision class already has a precedent resolution here, and it is a rename, not an alias: page:tabs.type hit the identical carrier collision (a node is the object { type: 'page:tabs', … }, and validate.ts lists type in BASE_PROPS). Upstream retired it in favour of tabStyle (objectstack#6776); apps/console/src/__tests__/registry-inputs-spec-parity.test.ts carries that history in its HARVESTED table.

3. Why the old name could not be authored on any tier

TierWhat happened to type="api"
htmlparse.ts composes { type: tag, ...props } — props spread last — so the value replaced the component discriminator and the node stopped resolving to a component. validate.ts cannot report it either: type is in BASE_PROPS, skipped before the declared-input check runs. Two mechanisms, one outcome, no diagnostic. objectstack PR #14274 landed a refusal here whose prescription ("write the tag you meant") is wrong for these two components.
react-pagethe wrapper stamps type: tag last and parks the author's value under specType (objectui#2880) — which neither action renderer reads.
JSON / SchemaRendererthe node's typeis the component id, and the renderer forwarded it as the action type. 'action:button' binds no handler and no builtin: a click that did nothing, no error, no toast — the objectui#6306 shape.

4. What changed

  • action-button.tsx, action-icon.tsx: registry inputs entry typeactionType; defaultProps follows; the forward reads schema.actionType alone.
  • The || schema.type fallback is removed. Post-rename that leg is precisely "the old spelling still works", which the ruling forbids; and on every authored path it now yields the component id, never an action type. A declaration omitting actionType forwards undefined, so ActionRunner falls through to its own action.name leg instead of being handed a component id — strictly better than the status quo.
  • specType (objectui#2880) is left alone; it addresses other collisions.
  • action:group / action:menu are unaffected: their members carry the spec ActionSchema.type inside an actions array, a different surface. The action-onSuccess-forward fixture now separates the two spellings explicitly rather than conflating them.
  • Changeset: @object-ui/componentsminor, naming the rename. It is the only @object-ui/* package whose published behaviour changes.

⛔ Deliberately not in this PR (objectstack-side follow-ups, per the card): the sdui.manifest.json pin bump, the html-tier refusal's component-aware special case, the react-pages.mdx sentence.

5. Acceptance

An html-tier page setting the renamed input parses and renderspackages/components/src/renderers/action/__tests__/action-type-input-html-tier.test.tsx, four rows through the real kind:'html' page path:

  1. An action:button tag written with label="Mark done" actionType="api" target="/api/v1/tasks/mark_done" compiles with no error panel, renders a real button, and on click hands ActionRunnertype: 'api' with the authored target.
  2. Negative control — the same page with the prop removed still compiles and renders, and the api handler is not reached. That is what pins row 1's 'api' to the authored prop rather than to a default, the tag, or the handler being the only one registered.
  3. No alias — the pre-rename type="api" is still the discriminator: the node's type becomes the bare string api, which no registry entry claims, so the page reports the compile error and renders no button.
  4. The declaration itself, through the manifest built from the live registry (the same call getJsxManifest() makes): actionType produces no unknown-prop diagnostic on either component, while a bogus prop does — and the registry inputs contain actionType and no longer contain type.

Rows 1-3 are green on origin/main too, and that is stated rather than glossed: the renderer already read actionType first, and an undeclared prop is only a warning, which does not stop a page compiling. Row 4 is the one that measures the rename.

Reverse verification — two ablations, each mutated on disk, proven there, measured in the same process, and restored:

LegMutation (blob hashes before/after recorded)PredictedObserved
revert the declaration (name: 'actionType'name: 'type', both files)both files MUTATED vs HEADrow 4 red, rows 1-3 greenexactly that — 1 failed | 3 passed, AssertionError: expected [ Array(1) ] to deeply equal []
restore the read (|| schema.type back on action:icon)action-icon.tsx MUTATED, action-button.tsx UNCHANGEDthe new mirror row red1 failed | 5 passed, expected "vi.fn()" to not be called at all, but actually been called 1 times — the component-id trap fires

The mutated files are imported by relative path from the tests, so they resolve to source and no rebuild is interposed; both legs restore via git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap, and git diff HEAD is empty afterwards. (The first attempt at this ran the mutation script and the test as ablate.sh && vitest, so the trap restored the tree before vitest started and the ablation vacuously passed 4/4. It was re-run with the measurement inside the same process; that is the run reported above.)

6. Bytes — measured, not assumed

Two full console builds, check:eager-closure on each, byte-exact from apps/console/dist/eager-closure.json. Before = 6411def25 (this branch's base) in a dedicated comparison worktree; after = 09562aa26.

beforeafterdelta
aggregate eager closure (gzip)3,254,6043,254,602−2 B
aggregate eager closure (raw)11,175,43111,175,439+8 B
eager chunk count48 of 51648 of 5160
chunk framework (gzip)523,823523,8230 B
chunk ui-components (gzip)396,598396,5980 B
chunk ui-components (raw)1,535,6611,535,669+8 B
chunk vendor-objectstack (gzip)948,329948,3290 B

Which chunk this lands in was the question, and the answer is ui-components, not framework.framework is packages/core|react|types and had 177 B (0.2 KB) of headroom with two PRs already parked on it (#7399); this change is entirely inside packages/components, so framework moves zero bytes — the +8 raw bytes land in ui-components, which absorbs them without moving its gzip figure at all. The remaining ±1-5 B wobble on eight other chunks is gzip entropy: every one of them has an unchanged raw size.

check:eager-closure passes on both builds. ⛔ No ceiling raised, no baseline re-pinned, no gate weakened.

7. Verification run

Union re-run at 09562aa26, the final commit (nothing landed after it).

  • pnpm exec vitest run packages/components/ packages/core/src/registry/ apps/console/src/__tests__/258 files, 2,523 tests, all pass.
  • Consumer sweep, the eight files naming either component outside packages/components (app-shell widget/DOM sweeps, PageView, useConsoleActionRuntime ×2, EnvironmentListToolbar ×2, RecordDetailView, and the schema-catalog DOM-leak test) — 8 files, 275 tests, all pass.
  • pnpm --filter @object-ui/components run type-check — exit 0 (the script is spelled type-check here; the echoed script name confirms it ran rather than matching nothing).
  • pnpm --filter @object-ui/components run lint — exit 0, 0 errors, 950 pre-existing warnings. Run as this repo spells it (eslint ., no --no-inline-config).
  • Gates derived from the actual diff paths, each re-derived from the changed files rather than taken from the dispatch list: check:action-forward-parity ✅ · check:control-bytes ✅ (6,124 tracked text files) · check:sdui-registration-pins ✅ · check:doc-types ✅ · check:doc-snippets ✅ (417/417 blocks, after building what it names — its first exit 2 was PRECONDITION NOT MET, not a red verdict) · check:designer-field-key-parity ✅ · check:vi-mock-specifiers ✅ · check:vi-mock-inherit ✅ · check:eager-closure ✅.

Repo-wide pnpm lint is CI's run and was not repeated locally; the narrowing above is the package the diff is confined to.

8. Clause ② — contract review

A published component input is renamed, so CONTRACT_REVIEW_TIER applies. The shape is pinned by the ruling and implemented mechanically. This PR carries needs:contract-review for the project director seat. ⛔ Not self-reviewed, auto-merge not enabled, not enqueued.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

…action:icon` to `actionType` (#7415)
The declared input carrying an action's execution type collided with the SDUI
envelope's component discriminator, and on no tier could an author actually set
it: the html tier's parser composes `{ type: tag, ...props }` so `type="api"`
replaced the discriminator (and `validate.ts` skips `type` as a BASE_PROP, so
there was no diagnostic); the react-page wrapper stamps the tag last and parks
the author's value under `specType`, which neither renderer reads; through
`SchemaRenderer` the node's `type` is the component id, which the renderers
forwarded to `ActionRunner` as an action type that binds no handler.
`actionType` is the spelling this repo already used for this value — `action:bar`
renames the declared type as it spreads a member, `ActionRunner.execute` resolves
`action.type || action.actionType || action.name`, and both renderers already read
`schema.actionType` first. This makes the one working spelling the declared one,
the same resolution `page:tabs.type` got upstream (retired for `tabStyle`).
No alias, no transition window: the `|| schema.type` leg is removed, so a
declaration that omits `actionType` forwards `undefined` and the runner falls
through to its own `action.name` leg rather than being handed a component id.
Implements objectstack#14490 ruling A (maintainer, 2026-09-02, batch #13 item 4).
Census over 4,797 files found zero authored nodes setting the input.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Handoff to the project director seat — ⛔ NOT reviewed or accepted by this seat

PM note from the domain:ui execution seat (session session_01EMrWaQw3XS5DxTHxp4yRyC), which dispatched #7415.

Clause ② is engaged (a published component input is renamed) and the card states CONTRACT_REVIEW_TIER. This seat runs opus, is not at tier, and has therefore not reviewed, not enabled auto-merge and not enqueued. Confirmed set up as required: draft, needs:contract-review present, Fixes #7415. Landing this unblocks objectstack#14490, which is pm:blocked on it.

Verified independently by this seat, from origin/main — not taken from the report

claimreading
actionType is already this repo's spelling for this valueActionRunner.ts:972const actionType = action.type || action.actionType || action.name || '';
both renderers already read actionTypefirstaction-button.tsx:148 and action-icon.tsx:113 — both type: schema.actionType || schema.type,
the census figure is corroborated by a second instrumentSchemaRenderer.tsx:1126 — the repo's own corpus census reads "action:button (5 nodes)", matching the PR's independent md/mdx scan
the page:tabs.typetabStyle rename precedent exists5 occurrences in registry-inputs-spec-parity.test.ts (1,966 lines)

⇒ The rename does not introduce a word. It promotes the spelling that already worked to being the declared one, and it follows an in-repo precedent that resolved the identical carrier collision by renaming rather than aliasing.

⚠️One thing this seat checked and found the PR right on, recorded so the reviewer does not repeat the detour.action-bar-member-type-resolution.test.tsx:26-27 says action:icon "read schema.typealone and dropped actionType entirely", which reads as contradicting the PR. It does not — that docblock is past tense about a defect since fixed, and action-icon.tsx:105-110 records the repair in its own words. The source, not the prose about the source, settles it.

⭐ The disclosure that deserves the most weight

The implementer caught and reported its own vacuous ablation:

The first attempt at this ran the mutation script and the test as ablate.sh && vitest, so the trap restored the tree before vitest started and the ablation vacuously passed 4/4. It was re-run with the measurement inside the same process; that is the run reported above.

A restore trap that fires before the measurement produces a green that means nothing — and it looks exactly like a successful ablation. It found that, said so, and re-ran. ⭐ It also states plainly that rows 1-3 of the acceptance test are green on origin/main too and that only row 4 measures the rename, rather than presenting four green rows as four proofs.

Recorded, not reviewed — for the reviewer to weigh

⛔ Observations from the dispatching seat, not a tier verdict:

  • The || schema.type fallback is removed, and the reasoning is worth confirming rather than assuming: post-rename that leg is precisely "the old spelling still works", which the ruling forbids; and on every authored path it now yields the component id, never an action type. A declaration omitting actionType forwards undefined, so ActionRunner falls through to its own action.name leg instead of being handed 'action:icon' — the objectui#6306 shape.
  • Bytes: framework moves zero. The change is entirely inside packages/components, which lands in ui-components, not in framework (packages/core|react|types). Measured framework 523,823 → 523,823; ui-components gzip unchanged at 396,598 while absorbing +8 raw bytes; aggregate −2 B gzip. The ±1-5 B wobble on other chunks is explained as gzip entropy with unchanged raw sizes, which is the right way to dismiss it. ⇒ [Decision] The framework per-chunk eager-closure ceiling leaves 177 bytes for the whole repo — two ruled user-facing fixes cannot land, and no wording of either fits #7399 is untouched.
  • The census reports 0 authored sites across 4,797 files with a firing control on every single row — including the negative-space rows (no props/properties bag, no JSX type= attribute).

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.3 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-LBfx1POZ.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)514.88KB117.49KB
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)33.40KB8.71KB
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)47.14KB13.02KB
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.27KB64.20KB
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.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
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.90KB21.12KB
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-manager
os-project-manager marked this pull request as ready for review September 3, 2026 04:46
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit f0bb9faSep 3, 2026
32 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-7415-action-type-input-rename branch September 3, 2026 05:03
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant

@os-project-manager
, '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)!: rename the type input of action:button / action:icon to actionType (#7415) - #7436

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-7415-action-type-input-rename
Sep 3, 2026
Merged

refactor(components)!: rename the type input of action:button / action:icon to actionType (#7415)#7436
os-project-manager merged 1 commit into
mainfrom
claude/issue-7415-action-type-input-rename

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#7415

Implements objectstack#14490 ruling A (maintainer, 2026-09-02, decision batch #13 item 4, verbatim 「同意」): the collision between the component input named type on action:button / action:icon and the SDUI envelope discriminator type is removed at the source. The input is renamed to actionType. No alias, no transition window (the standing 不渐进 rule). Landing this unblocks objectstack#14490, which is pm:blocked on this card.

All readings below were taken at 09562aa26, the head of this branch.

1. Census first — and it is not a formality

The question measured: how many authored sites set the component input named type on action:button or action:icon? Every zero below is reported with a control on the same query that fires non-empty.

SurfaceQueryReadingControl on the same query
JSON trees — 517 files parsed, incl. all 433 schema-catalog entriesobject nodes whose type is action:button / action:icon0 nodes2,410 nodes carrying a type key; 127type: "button" nodes
md/mdx fenced JSON — 234 blocks seen, 207 parsedsame5 nodes28type: "button" nodes in the same blocks
…those 5 nodesdo any set an execution type?0 — key sets are type,label,icon,action (×2) and type,label,action (×3): the discriminator plus the action channelthe 5 themselves
any surfacea props / properties bag carrying type on those two components0none of the 5 has a bag at all
react-page / JSX tieran ActionButton / ActionIcon JSX tag carrying a type= attribute0the tag pattern fires 5 times — all of them local resolveIcon bindings or prose, verified individually
content/docsa page documenting the typeinput of either component0content/docs/core/enhanced-actions.mdx documents the legacy actionType dialect — 30 hits, so the term fires
apps/consolean authored node of either component0public-contract.test.ts lists both ids, so the term fires
packages/** non-test sourcea site authoring the input061 non-test hits on the component ids, all prose / registry ids / action:bar's own composition
read sites of the propschema.type read as an action type2 (action-button.tsx, action-icon.tsx)
declaration sitesregistry inputs + defaultProps2 + 2

Scanned 4,797 files across examples/, apps/console/, content/docs/ and packages/.

The md/mdx count of 5 is independently corroborated: this repo's own corpus census, recorded in packages/react/src/SchemaRenderer.tsx, already reads "action:button (5 nodes)". Two different scanners, same number.

The rename breaks no authored document in this repo. objectstack's html-tier census found zero; the react-tier population measured here is zero as well. A re-check of objectstack#14490's manifest scan would now return an empty list for inputs named type — these two were the only ones of the 57 manifest components declaring one — while the stated control, the variant scan, stays non-empty: both components still declare variant, untouched by this change.

2. The name

actionType, the ruling's working name — and the objectui component configuration does already have a convention, which points at the same string rather than a different one:

  • It is this repo's existing spelling for this exact value.action:bar renames the declared type as it spreads a member onto its child (type: componentType, actionType: action.type), ActionRunner.execute resolves action.type || action.actionType || action.name, ActionDef declares actionType, and both renderers already read schema.actionTypefirst. The rename does not introduce a word; it makes the one spelling that already worked the declared one.
  • The collision class already has a precedent resolution here, and it is a rename, not an alias: page:tabs.type hit the identical carrier collision (a node is the object { type: 'page:tabs', … }, and validate.ts lists type in BASE_PROPS). Upstream retired it in favour of tabStyle (objectstack#6776); apps/console/src/__tests__/registry-inputs-spec-parity.test.ts carries that history in its HARVESTED table.

3. Why the old name could not be authored on any tier

TierWhat happened to type="api"
htmlparse.ts composes { type: tag, ...props } — props spread last — so the value replaced the component discriminator and the node stopped resolving to a component. validate.ts cannot report it either: type is in BASE_PROPS, skipped before the declared-input check runs. Two mechanisms, one outcome, no diagnostic. objectstack PR #14274 landed a refusal here whose prescription ("write the tag you meant") is wrong for these two components.
react-pagethe wrapper stamps type: tag last and parks the author's value under specType (objectui#2880) — which neither action renderer reads.
JSON / SchemaRendererthe node's typeis the component id, and the renderer forwarded it as the action type. 'action:button' binds no handler and no builtin: a click that did nothing, no error, no toast — the objectui#6306 shape.

4. What changed

  • action-button.tsx, action-icon.tsx: registry inputs entry typeactionType; defaultProps follows; the forward reads schema.actionType alone.
  • The || schema.type fallback is removed. Post-rename that leg is precisely "the old spelling still works", which the ruling forbids; and on every authored path it now yields the component id, never an action type. A declaration omitting actionType forwards undefined, so ActionRunner falls through to its own action.name leg instead of being handed a component id — strictly better than the status quo.
  • specType (objectui#2880) is left alone; it addresses other collisions.
  • action:group / action:menu are unaffected: their members carry the spec ActionSchema.type inside an actions array, a different surface. The action-onSuccess-forward fixture now separates the two spellings explicitly rather than conflating them.
  • Changeset: @object-ui/componentsminor, naming the rename. It is the only @object-ui/* package whose published behaviour changes.

⛔ Deliberately not in this PR (objectstack-side follow-ups, per the card): the sdui.manifest.json pin bump, the html-tier refusal's component-aware special case, the react-pages.mdx sentence.

5. Acceptance

An html-tier page setting the renamed input parses and renderspackages/components/src/renderers/action/__tests__/action-type-input-html-tier.test.tsx, four rows through the real kind:'html' page path:

  1. An action:button tag written with label="Mark done" actionType="api" target="/api/v1/tasks/mark_done" compiles with no error panel, renders a real button, and on click hands ActionRunnertype: 'api' with the authored target.
  2. Negative control — the same page with the prop removed still compiles and renders, and the api handler is not reached. That is what pins row 1's 'api' to the authored prop rather than to a default, the tag, or the handler being the only one registered.
  3. No alias — the pre-rename type="api" is still the discriminator: the node's type becomes the bare string api, which no registry entry claims, so the page reports the compile error and renders no button.
  4. The declaration itself, through the manifest built from the live registry (the same call getJsxManifest() makes): actionType produces no unknown-prop diagnostic on either component, while a bogus prop does — and the registry inputs contain actionType and no longer contain type.

Rows 1-3 are green on origin/main too, and that is stated rather than glossed: the renderer already read actionType first, and an undeclared prop is only a warning, which does not stop a page compiling. Row 4 is the one that measures the rename.

Reverse verification — two ablations, each mutated on disk, proven there, measured in the same process, and restored:

LegMutation (blob hashes before/after recorded)PredictedObserved
revert the declaration (name: 'actionType'name: 'type', both files)both files MUTATED vs HEADrow 4 red, rows 1-3 greenexactly that — 1 failed | 3 passed, AssertionError: expected [ Array(1) ] to deeply equal []
restore the read (|| schema.type back on action:icon)action-icon.tsx MUTATED, action-button.tsx UNCHANGEDthe new mirror row red1 failed | 5 passed, expected "vi.fn()" to not be called at all, but actually been called 1 times — the component-id trap fires

The mutated files are imported by relative path from the tests, so they resolve to source and no rebuild is interposed; both legs restore via git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap, and git diff HEAD is empty afterwards. (The first attempt at this ran the mutation script and the test as ablate.sh && vitest, so the trap restored the tree before vitest started and the ablation vacuously passed 4/4. It was re-run with the measurement inside the same process; that is the run reported above.)

6. Bytes — measured, not assumed

Two full console builds, check:eager-closure on each, byte-exact from apps/console/dist/eager-closure.json. Before = 6411def25 (this branch's base) in a dedicated comparison worktree; after = 09562aa26.

beforeafterdelta
aggregate eager closure (gzip)3,254,6043,254,602−2 B
aggregate eager closure (raw)11,175,43111,175,439+8 B
eager chunk count48 of 51648 of 5160
chunk framework (gzip)523,823523,8230 B
chunk ui-components (gzip)396,598396,5980 B
chunk ui-components (raw)1,535,6611,535,669+8 B
chunk vendor-objectstack (gzip)948,329948,3290 B

Which chunk this lands in was the question, and the answer is ui-components, not framework.framework is packages/core|react|types and had 177 B (0.2 KB) of headroom with two PRs already parked on it (#7399); this change is entirely inside packages/components, so framework moves zero bytes — the +8 raw bytes land in ui-components, which absorbs them without moving its gzip figure at all. The remaining ±1-5 B wobble on eight other chunks is gzip entropy: every one of them has an unchanged raw size.

check:eager-closure passes on both builds. ⛔ No ceiling raised, no baseline re-pinned, no gate weakened.

7. Verification run

Union re-run at 09562aa26, the final commit (nothing landed after it).

  • pnpm exec vitest run packages/components/ packages/core/src/registry/ apps/console/src/__tests__/258 files, 2,523 tests, all pass.
  • Consumer sweep, the eight files naming either component outside packages/components (app-shell widget/DOM sweeps, PageView, useConsoleActionRuntime ×2, EnvironmentListToolbar ×2, RecordDetailView, and the schema-catalog DOM-leak test) — 8 files, 275 tests, all pass.
  • pnpm --filter @object-ui/components run type-check — exit 0 (the script is spelled type-check here; the echoed script name confirms it ran rather than matching nothing).
  • pnpm --filter @object-ui/components run lint — exit 0, 0 errors, 950 pre-existing warnings. Run as this repo spells it (eslint ., no --no-inline-config).
  • Gates derived from the actual diff paths, each re-derived from the changed files rather than taken from the dispatch list: check:action-forward-parity ✅ · check:control-bytes ✅ (6,124 tracked text files) · check:sdui-registration-pins ✅ · check:doc-types ✅ · check:doc-snippets ✅ (417/417 blocks, after building what it names — its first exit 2 was PRECONDITION NOT MET, not a red verdict) · check:designer-field-key-parity ✅ · check:vi-mock-specifiers ✅ · check:vi-mock-inherit ✅ · check:eager-closure ✅.

Repo-wide pnpm lint is CI's run and was not repeated locally; the narrowing above is the package the diff is confined to.

8. Clause ② — contract review

A published component input is renamed, so CONTRACT_REVIEW_TIER applies. The shape is pinned by the ruling and implemented mechanically. This PR carries needs:contract-review for the project director seat. ⛔ Not self-reviewed, auto-merge not enabled, not enqueued.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

…action:icon` to `actionType` (#7415)
The declared input carrying an action's execution type collided with the SDUI
envelope's component discriminator, and on no tier could an author actually set
it: the html tier's parser composes `{ type: tag, ...props }` so `type="api"`
replaced the discriminator (and `validate.ts` skips `type` as a BASE_PROP, so
there was no diagnostic); the react-page wrapper stamps the tag last and parks
the author's value under `specType`, which neither renderer reads; through
`SchemaRenderer` the node's `type` is the component id, which the renderers
forwarded to `ActionRunner` as an action type that binds no handler.
`actionType` is the spelling this repo already used for this value — `action:bar`
renames the declared type as it spreads a member, `ActionRunner.execute` resolves
`action.type || action.actionType || action.name`, and both renderers already read
`schema.actionType` first. This makes the one working spelling the declared one,
the same resolution `page:tabs.type` got upstream (retired for `tabStyle`).
No alias, no transition window: the `|| schema.type` leg is removed, so a
declaration that omits `actionType` forwards `undefined` and the runner falls
through to its own `action.name` leg rather than being handed a component id.
Implements objectstack#14490 ruling A (maintainer, 2026-09-02, batch #13 item 4).
Census over 4,797 files found zero authored nodes setting the input.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Handoff to the project director seat — ⛔ NOT reviewed or accepted by this seat

PM note from the domain:ui execution seat (session session_01EMrWaQw3XS5DxTHxp4yRyC), which dispatched #7415.

Clause ② is engaged (a published component input is renamed) and the card states CONTRACT_REVIEW_TIER. This seat runs opus, is not at tier, and has therefore not reviewed, not enabled auto-merge and not enqueued. Confirmed set up as required: draft, needs:contract-review present, Fixes #7415. Landing this unblocks objectstack#14490, which is pm:blocked on it.

Verified independently by this seat, from origin/main — not taken from the report

claimreading
actionType is already this repo's spelling for this valueActionRunner.ts:972const actionType = action.type || action.actionType || action.name || '';
both renderers already read actionTypefirstaction-button.tsx:148 and action-icon.tsx:113 — both type: schema.actionType || schema.type,
the census figure is corroborated by a second instrumentSchemaRenderer.tsx:1126 — the repo's own corpus census reads "action:button (5 nodes)", matching the PR's independent md/mdx scan
the page:tabs.typetabStyle rename precedent exists5 occurrences in registry-inputs-spec-parity.test.ts (1,966 lines)

⇒ The rename does not introduce a word. It promotes the spelling that already worked to being the declared one, and it follows an in-repo precedent that resolved the identical carrier collision by renaming rather than aliasing.

⚠️One thing this seat checked and found the PR right on, recorded so the reviewer does not repeat the detour.action-bar-member-type-resolution.test.tsx:26-27 says action:icon "read schema.typealone and dropped actionType entirely", which reads as contradicting the PR. It does not — that docblock is past tense about a defect since fixed, and action-icon.tsx:105-110 records the repair in its own words. The source, not the prose about the source, settles it.

⭐ The disclosure that deserves the most weight

The implementer caught and reported its own vacuous ablation:

The first attempt at this ran the mutation script and the test as ablate.sh && vitest, so the trap restored the tree before vitest started and the ablation vacuously passed 4/4. It was re-run with the measurement inside the same process; that is the run reported above.

A restore trap that fires before the measurement produces a green that means nothing — and it looks exactly like a successful ablation. It found that, said so, and re-ran. ⭐ It also states plainly that rows 1-3 of the acceptance test are green on origin/main too and that only row 4 measures the rename, rather than presenting four green rows as four proofs.

Recorded, not reviewed — for the reviewer to weigh

⛔ Observations from the dispatching seat, not a tier verdict:

  • The || schema.type fallback is removed, and the reasoning is worth confirming rather than assuming: post-rename that leg is precisely "the old spelling still works", which the ruling forbids; and on every authored path it now yields the component id, never an action type. A declaration omitting actionType forwards undefined, so ActionRunner falls through to its own action.name leg instead of being handed 'action:icon' — the objectui#6306 shape.
  • Bytes: framework moves zero. The change is entirely inside packages/components, which lands in ui-components, not in framework (packages/core|react|types). Measured framework 523,823 → 523,823; ui-components gzip unchanged at 396,598 while absorbing +8 raw bytes; aggregate −2 B gzip. The ±1-5 B wobble on other chunks is explained as gzip entropy with unchanged raw sizes, which is the right way to dismiss it. ⇒ [Decision] The framework per-chunk eager-closure ceiling leaves 177 bytes for the whole repo — two ruled user-facing fixes cannot land, and no wording of either fits #7399 is untouched.
  • The census reports 0 authored sites across 4,797 files with a firing control on every single row — including the negative-space rows (no props/properties bag, no JSX type= attribute).

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.3 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-LBfx1POZ.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)514.88KB117.49KB
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)33.40KB8.71KB
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)47.14KB13.02KB
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.27KB64.20KB
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.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
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.90KB21.12KB
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-manager
os-project-manager marked this pull request as ready for review September 3, 2026 04:46
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit f0bb9faSep 3, 2026
32 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-7415-action-type-input-rename branch September 3, 2026 05:03
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant

@os-project-manager
, '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)!: rename the type input of action:button / action:icon to actionType (#7415) - #7436

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-7415-action-type-input-rename
Sep 3, 2026
Merged

refactor(components)!: rename the type input of action:button / action:icon to actionType (#7415)#7436
os-project-manager merged 1 commit into
mainfrom
claude/issue-7415-action-type-input-rename

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#7415

Implements objectstack#14490 ruling A (maintainer, 2026-09-02, decision batch #13 item 4, verbatim 「同意」): the collision between the component input named type on action:button / action:icon and the SDUI envelope discriminator type is removed at the source. The input is renamed to actionType. No alias, no transition window (the standing 不渐进 rule). Landing this unblocks objectstack#14490, which is pm:blocked on this card.

All readings below were taken at 09562aa26, the head of this branch.

1. Census first — and it is not a formality

The question measured: how many authored sites set the component input named type on action:button or action:icon? Every zero below is reported with a control on the same query that fires non-empty.

SurfaceQueryReadingControl on the same query
JSON trees — 517 files parsed, incl. all 433 schema-catalog entriesobject nodes whose type is action:button / action:icon0 nodes2,410 nodes carrying a type key; 127type: "button" nodes
md/mdx fenced JSON — 234 blocks seen, 207 parsedsame5 nodes28type: "button" nodes in the same blocks
…those 5 nodesdo any set an execution type?0 — key sets are type,label,icon,action (×2) and type,label,action (×3): the discriminator plus the action channelthe 5 themselves
any surfacea props / properties bag carrying type on those two components0none of the 5 has a bag at all
react-page / JSX tieran ActionButton / ActionIcon JSX tag carrying a type= attribute0the tag pattern fires 5 times — all of them local resolveIcon bindings or prose, verified individually
content/docsa page documenting the typeinput of either component0content/docs/core/enhanced-actions.mdx documents the legacy actionType dialect — 30 hits, so the term fires
apps/consolean authored node of either component0public-contract.test.ts lists both ids, so the term fires
packages/** non-test sourcea site authoring the input061 non-test hits on the component ids, all prose / registry ids / action:bar's own composition
read sites of the propschema.type read as an action type2 (action-button.tsx, action-icon.tsx)
declaration sitesregistry inputs + defaultProps2 + 2

Scanned 4,797 files across examples/, apps/console/, content/docs/ and packages/.

The md/mdx count of 5 is independently corroborated: this repo's own corpus census, recorded in packages/react/src/SchemaRenderer.tsx, already reads "action:button (5 nodes)". Two different scanners, same number.

The rename breaks no authored document in this repo. objectstack's html-tier census found zero; the react-tier population measured here is zero as well. A re-check of objectstack#14490's manifest scan would now return an empty list for inputs named type — these two were the only ones of the 57 manifest components declaring one — while the stated control, the variant scan, stays non-empty: both components still declare variant, untouched by this change.

2. The name

actionType, the ruling's working name — and the objectui component configuration does already have a convention, which points at the same string rather than a different one:

  • It is this repo's existing spelling for this exact value.action:bar renames the declared type as it spreads a member onto its child (type: componentType, actionType: action.type), ActionRunner.execute resolves action.type || action.actionType || action.name, ActionDef declares actionType, and both renderers already read schema.actionTypefirst. The rename does not introduce a word; it makes the one spelling that already worked the declared one.
  • The collision class already has a precedent resolution here, and it is a rename, not an alias: page:tabs.type hit the identical carrier collision (a node is the object { type: 'page:tabs', … }, and validate.ts lists type in BASE_PROPS). Upstream retired it in favour of tabStyle (objectstack#6776); apps/console/src/__tests__/registry-inputs-spec-parity.test.ts carries that history in its HARVESTED table.

3. Why the old name could not be authored on any tier

TierWhat happened to type="api"
htmlparse.ts composes { type: tag, ...props } — props spread last — so the value replaced the component discriminator and the node stopped resolving to a component. validate.ts cannot report it either: type is in BASE_PROPS, skipped before the declared-input check runs. Two mechanisms, one outcome, no diagnostic. objectstack PR #14274 landed a refusal here whose prescription ("write the tag you meant") is wrong for these two components.
react-pagethe wrapper stamps type: tag last and parks the author's value under specType (objectui#2880) — which neither action renderer reads.
JSON / SchemaRendererthe node's typeis the component id, and the renderer forwarded it as the action type. 'action:button' binds no handler and no builtin: a click that did nothing, no error, no toast — the objectui#6306 shape.

4. What changed

  • action-button.tsx, action-icon.tsx: registry inputs entry typeactionType; defaultProps follows; the forward reads schema.actionType alone.
  • The || schema.type fallback is removed. Post-rename that leg is precisely "the old spelling still works", which the ruling forbids; and on every authored path it now yields the component id, never an action type. A declaration omitting actionType forwards undefined, so ActionRunner falls through to its own action.name leg instead of being handed a component id — strictly better than the status quo.
  • specType (objectui#2880) is left alone; it addresses other collisions.
  • action:group / action:menu are unaffected: their members carry the spec ActionSchema.type inside an actions array, a different surface. The action-onSuccess-forward fixture now separates the two spellings explicitly rather than conflating them.
  • Changeset: @object-ui/componentsminor, naming the rename. It is the only @object-ui/* package whose published behaviour changes.

⛔ Deliberately not in this PR (objectstack-side follow-ups, per the card): the sdui.manifest.json pin bump, the html-tier refusal's component-aware special case, the react-pages.mdx sentence.

5. Acceptance

An html-tier page setting the renamed input parses and renderspackages/components/src/renderers/action/__tests__/action-type-input-html-tier.test.tsx, four rows through the real kind:'html' page path:

  1. An action:button tag written with label="Mark done" actionType="api" target="/api/v1/tasks/mark_done" compiles with no error panel, renders a real button, and on click hands ActionRunnertype: 'api' with the authored target.
  2. Negative control — the same page with the prop removed still compiles and renders, and the api handler is not reached. That is what pins row 1's 'api' to the authored prop rather than to a default, the tag, or the handler being the only one registered.
  3. No alias — the pre-rename type="api" is still the discriminator: the node's type becomes the bare string api, which no registry entry claims, so the page reports the compile error and renders no button.
  4. The declaration itself, through the manifest built from the live registry (the same call getJsxManifest() makes): actionType produces no unknown-prop diagnostic on either component, while a bogus prop does — and the registry inputs contain actionType and no longer contain type.

Rows 1-3 are green on origin/main too, and that is stated rather than glossed: the renderer already read actionType first, and an undeclared prop is only a warning, which does not stop a page compiling. Row 4 is the one that measures the rename.

Reverse verification — two ablations, each mutated on disk, proven there, measured in the same process, and restored:

LegMutation (blob hashes before/after recorded)PredictedObserved
revert the declaration (name: 'actionType'name: 'type', both files)both files MUTATED vs HEADrow 4 red, rows 1-3 greenexactly that — 1 failed | 3 passed, AssertionError: expected [ Array(1) ] to deeply equal []
restore the read (|| schema.type back on action:icon)action-icon.tsx MUTATED, action-button.tsx UNCHANGEDthe new mirror row red1 failed | 5 passed, expected "vi.fn()" to not be called at all, but actually been called 1 times — the component-id trap fires

The mutated files are imported by relative path from the tests, so they resolve to source and no rebuild is interposed; both legs restore via git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap, and git diff HEAD is empty afterwards. (The first attempt at this ran the mutation script and the test as ablate.sh && vitest, so the trap restored the tree before vitest started and the ablation vacuously passed 4/4. It was re-run with the measurement inside the same process; that is the run reported above.)

6. Bytes — measured, not assumed

Two full console builds, check:eager-closure on each, byte-exact from apps/console/dist/eager-closure.json. Before = 6411def25 (this branch's base) in a dedicated comparison worktree; after = 09562aa26.

beforeafterdelta
aggregate eager closure (gzip)3,254,6043,254,602−2 B
aggregate eager closure (raw)11,175,43111,175,439+8 B
eager chunk count48 of 51648 of 5160
chunk framework (gzip)523,823523,8230 B
chunk ui-components (gzip)396,598396,5980 B
chunk ui-components (raw)1,535,6611,535,669+8 B
chunk vendor-objectstack (gzip)948,329948,3290 B

Which chunk this lands in was the question, and the answer is ui-components, not framework.framework is packages/core|react|types and had 177 B (0.2 KB) of headroom with two PRs already parked on it (#7399); this change is entirely inside packages/components, so framework moves zero bytes — the +8 raw bytes land in ui-components, which absorbs them without moving its gzip figure at all. The remaining ±1-5 B wobble on eight other chunks is gzip entropy: every one of them has an unchanged raw size.

check:eager-closure passes on both builds. ⛔ No ceiling raised, no baseline re-pinned, no gate weakened.

7. Verification run

Union re-run at 09562aa26, the final commit (nothing landed after it).

  • pnpm exec vitest run packages/components/ packages/core/src/registry/ apps/console/src/__tests__/258 files, 2,523 tests, all pass.
  • Consumer sweep, the eight files naming either component outside packages/components (app-shell widget/DOM sweeps, PageView, useConsoleActionRuntime ×2, EnvironmentListToolbar ×2, RecordDetailView, and the schema-catalog DOM-leak test) — 8 files, 275 tests, all pass.
  • pnpm --filter @object-ui/components run type-check — exit 0 (the script is spelled type-check here; the echoed script name confirms it ran rather than matching nothing).
  • pnpm --filter @object-ui/components run lint — exit 0, 0 errors, 950 pre-existing warnings. Run as this repo spells it (eslint ., no --no-inline-config).
  • Gates derived from the actual diff paths, each re-derived from the changed files rather than taken from the dispatch list: check:action-forward-parity ✅ · check:control-bytes ✅ (6,124 tracked text files) · check:sdui-registration-pins ✅ · check:doc-types ✅ · check:doc-snippets ✅ (417/417 blocks, after building what it names — its first exit 2 was PRECONDITION NOT MET, not a red verdict) · check:designer-field-key-parity ✅ · check:vi-mock-specifiers ✅ · check:vi-mock-inherit ✅ · check:eager-closure ✅.

Repo-wide pnpm lint is CI's run and was not repeated locally; the narrowing above is the package the diff is confined to.

8. Clause ② — contract review

A published component input is renamed, so CONTRACT_REVIEW_TIER applies. The shape is pinned by the ruling and implemented mechanically. This PR carries needs:contract-review for the project director seat. ⛔ Not self-reviewed, auto-merge not enabled, not enqueued.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

…action:icon` to `actionType` (#7415)
The declared input carrying an action's execution type collided with the SDUI
envelope's component discriminator, and on no tier could an author actually set
it: the html tier's parser composes `{ type: tag, ...props }` so `type="api"`
replaced the discriminator (and `validate.ts` skips `type` as a BASE_PROP, so
there was no diagnostic); the react-page wrapper stamps the tag last and parks
the author's value under `specType`, which neither renderer reads; through
`SchemaRenderer` the node's `type` is the component id, which the renderers
forwarded to `ActionRunner` as an action type that binds no handler.
`actionType` is the spelling this repo already used for this value — `action:bar`
renames the declared type as it spreads a member, `ActionRunner.execute` resolves
`action.type || action.actionType || action.name`, and both renderers already read
`schema.actionType` first. This makes the one working spelling the declared one,
the same resolution `page:tabs.type` got upstream (retired for `tabStyle`).
No alias, no transition window: the `|| schema.type` leg is removed, so a
declaration that omits `actionType` forwards `undefined` and the runner falls
through to its own `action.name` leg rather than being handed a component id.
Implements objectstack#14490 ruling A (maintainer, 2026-09-02, batch #13 item 4).
Census over 4,797 files found zero authored nodes setting the input.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Handoff to the project director seat — ⛔ NOT reviewed or accepted by this seat

PM note from the domain:ui execution seat (session session_01EMrWaQw3XS5DxTHxp4yRyC), which dispatched #7415.

Clause ② is engaged (a published component input is renamed) and the card states CONTRACT_REVIEW_TIER. This seat runs opus, is not at tier, and has therefore not reviewed, not enabled auto-merge and not enqueued. Confirmed set up as required: draft, needs:contract-review present, Fixes #7415. Landing this unblocks objectstack#14490, which is pm:blocked on it.

Verified independently by this seat, from origin/main — not taken from the report

claimreading
actionType is already this repo's spelling for this valueActionRunner.ts:972const actionType = action.type || action.actionType || action.name || '';
both renderers already read actionTypefirstaction-button.tsx:148 and action-icon.tsx:113 — both type: schema.actionType || schema.type,
the census figure is corroborated by a second instrumentSchemaRenderer.tsx:1126 — the repo's own corpus census reads "action:button (5 nodes)", matching the PR's independent md/mdx scan
the page:tabs.typetabStyle rename precedent exists5 occurrences in registry-inputs-spec-parity.test.ts (1,966 lines)

⇒ The rename does not introduce a word. It promotes the spelling that already worked to being the declared one, and it follows an in-repo precedent that resolved the identical carrier collision by renaming rather than aliasing.

⚠️One thing this seat checked and found the PR right on, recorded so the reviewer does not repeat the detour.action-bar-member-type-resolution.test.tsx:26-27 says action:icon "read schema.typealone and dropped actionType entirely", which reads as contradicting the PR. It does not — that docblock is past tense about a defect since fixed, and action-icon.tsx:105-110 records the repair in its own words. The source, not the prose about the source, settles it.

⭐ The disclosure that deserves the most weight

The implementer caught and reported its own vacuous ablation:

The first attempt at this ran the mutation script and the test as ablate.sh && vitest, so the trap restored the tree before vitest started and the ablation vacuously passed 4/4. It was re-run with the measurement inside the same process; that is the run reported above.

A restore trap that fires before the measurement produces a green that means nothing — and it looks exactly like a successful ablation. It found that, said so, and re-ran. ⭐ It also states plainly that rows 1-3 of the acceptance test are green on origin/main too and that only row 4 measures the rename, rather than presenting four green rows as four proofs.

Recorded, not reviewed — for the reviewer to weigh

⛔ Observations from the dispatching seat, not a tier verdict:

  • The || schema.type fallback is removed, and the reasoning is worth confirming rather than assuming: post-rename that leg is precisely "the old spelling still works", which the ruling forbids; and on every authored path it now yields the component id, never an action type. A declaration omitting actionType forwards undefined, so ActionRunner falls through to its own action.name leg instead of being handed 'action:icon' — the objectui#6306 shape.
  • Bytes: framework moves zero. The change is entirely inside packages/components, which lands in ui-components, not in framework (packages/core|react|types). Measured framework 523,823 → 523,823; ui-components gzip unchanged at 396,598 while absorbing +8 raw bytes; aggregate −2 B gzip. The ±1-5 B wobble on other chunks is explained as gzip entropy with unchanged raw sizes, which is the right way to dismiss it. ⇒ [Decision] The framework per-chunk eager-closure ceiling leaves 177 bytes for the whole repo — two ruled user-facing fixes cannot land, and no wording of either fits #7399 is untouched.
  • The census reports 0 authored sites across 4,797 files with a firing control on every single row — including the negative-space rows (no props/properties bag, no JSX type= attribute).

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.3 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-LBfx1POZ.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)514.88KB117.49KB
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)33.40KB8.71KB
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)47.14KB13.02KB
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.27KB64.20KB
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.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
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.90KB21.12KB
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-manager
os-project-manager marked this pull request as ready for review September 3, 2026 04:46
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit f0bb9faSep 3, 2026
32 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-7415-action-type-input-rename branch September 3, 2026 05:03
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant

@os-project-manager
, '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)!: rename the type input of action:button / action:icon to actionType (#7415) - #7436

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-7415-action-type-input-rename
Sep 3, 2026
Merged

refactor(components)!: rename the type input of action:button / action:icon to actionType (#7415)#7436
os-project-manager merged 1 commit into
mainfrom
claude/issue-7415-action-type-input-rename

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#7415

Implements objectstack#14490 ruling A (maintainer, 2026-09-02, decision batch #13 item 4, verbatim 「同意」): the collision between the component input named type on action:button / action:icon and the SDUI envelope discriminator type is removed at the source. The input is renamed to actionType. No alias, no transition window (the standing 不渐进 rule). Landing this unblocks objectstack#14490, which is pm:blocked on this card.

All readings below were taken at 09562aa26, the head of this branch.

1. Census first — and it is not a formality

The question measured: how many authored sites set the component input named type on action:button or action:icon? Every zero below is reported with a control on the same query that fires non-empty.

SurfaceQueryReadingControl on the same query
JSON trees — 517 files parsed, incl. all 433 schema-catalog entriesobject nodes whose type is action:button / action:icon0 nodes2,410 nodes carrying a type key; 127type: "button" nodes
md/mdx fenced JSON — 234 blocks seen, 207 parsedsame5 nodes28type: "button" nodes in the same blocks
…those 5 nodesdo any set an execution type?0 — key sets are type,label,icon,action (×2) and type,label,action (×3): the discriminator plus the action channelthe 5 themselves
any surfacea props / properties bag carrying type on those two components0none of the 5 has a bag at all
react-page / JSX tieran ActionButton / ActionIcon JSX tag carrying a type= attribute0the tag pattern fires 5 times — all of them local resolveIcon bindings or prose, verified individually
content/docsa page documenting the typeinput of either component0content/docs/core/enhanced-actions.mdx documents the legacy actionType dialect — 30 hits, so the term fires
apps/consolean authored node of either component0public-contract.test.ts lists both ids, so the term fires
packages/** non-test sourcea site authoring the input061 non-test hits on the component ids, all prose / registry ids / action:bar's own composition
read sites of the propschema.type read as an action type2 (action-button.tsx, action-icon.tsx)
declaration sitesregistry inputs + defaultProps2 + 2

Scanned 4,797 files across examples/, apps/console/, content/docs/ and packages/.

The md/mdx count of 5 is independently corroborated: this repo's own corpus census, recorded in packages/react/src/SchemaRenderer.tsx, already reads "action:button (5 nodes)". Two different scanners, same number.

The rename breaks no authored document in this repo. objectstack's html-tier census found zero; the react-tier population measured here is zero as well. A re-check of objectstack#14490's manifest scan would now return an empty list for inputs named type — these two were the only ones of the 57 manifest components declaring one — while the stated control, the variant scan, stays non-empty: both components still declare variant, untouched by this change.

2. The name

actionType, the ruling's working name — and the objectui component configuration does already have a convention, which points at the same string rather than a different one:

  • It is this repo's existing spelling for this exact value.action:bar renames the declared type as it spreads a member onto its child (type: componentType, actionType: action.type), ActionRunner.execute resolves action.type || action.actionType || action.name, ActionDef declares actionType, and both renderers already read schema.actionTypefirst. The rename does not introduce a word; it makes the one spelling that already worked the declared one.
  • The collision class already has a precedent resolution here, and it is a rename, not an alias: page:tabs.type hit the identical carrier collision (a node is the object { type: 'page:tabs', … }, and validate.ts lists type in BASE_PROPS). Upstream retired it in favour of tabStyle (objectstack#6776); apps/console/src/__tests__/registry-inputs-spec-parity.test.ts carries that history in its HARVESTED table.

3. Why the old name could not be authored on any tier

TierWhat happened to type="api"
htmlparse.ts composes { type: tag, ...props } — props spread last — so the value replaced the component discriminator and the node stopped resolving to a component. validate.ts cannot report it either: type is in BASE_PROPS, skipped before the declared-input check runs. Two mechanisms, one outcome, no diagnostic. objectstack PR #14274 landed a refusal here whose prescription ("write the tag you meant") is wrong for these two components.
react-pagethe wrapper stamps type: tag last and parks the author's value under specType (objectui#2880) — which neither action renderer reads.
JSON / SchemaRendererthe node's typeis the component id, and the renderer forwarded it as the action type. 'action:button' binds no handler and no builtin: a click that did nothing, no error, no toast — the objectui#6306 shape.

4. What changed

  • action-button.tsx, action-icon.tsx: registry inputs entry typeactionType; defaultProps follows; the forward reads schema.actionType alone.
  • The || schema.type fallback is removed. Post-rename that leg is precisely "the old spelling still works", which the ruling forbids; and on every authored path it now yields the component id, never an action type. A declaration omitting actionType forwards undefined, so ActionRunner falls through to its own action.name leg instead of being handed a component id — strictly better than the status quo.
  • specType (objectui#2880) is left alone; it addresses other collisions.
  • action:group / action:menu are unaffected: their members carry the spec ActionSchema.type inside an actions array, a different surface. The action-onSuccess-forward fixture now separates the two spellings explicitly rather than conflating them.
  • Changeset: @object-ui/componentsminor, naming the rename. It is the only @object-ui/* package whose published behaviour changes.

⛔ Deliberately not in this PR (objectstack-side follow-ups, per the card): the sdui.manifest.json pin bump, the html-tier refusal's component-aware special case, the react-pages.mdx sentence.

5. Acceptance

An html-tier page setting the renamed input parses and renderspackages/components/src/renderers/action/__tests__/action-type-input-html-tier.test.tsx, four rows through the real kind:'html' page path:

  1. An action:button tag written with label="Mark done" actionType="api" target="/api/v1/tasks/mark_done" compiles with no error panel, renders a real button, and on click hands ActionRunnertype: 'api' with the authored target.
  2. Negative control — the same page with the prop removed still compiles and renders, and the api handler is not reached. That is what pins row 1's 'api' to the authored prop rather than to a default, the tag, or the handler being the only one registered.
  3. No alias — the pre-rename type="api" is still the discriminator: the node's type becomes the bare string api, which no registry entry claims, so the page reports the compile error and renders no button.
  4. The declaration itself, through the manifest built from the live registry (the same call getJsxManifest() makes): actionType produces no unknown-prop diagnostic on either component, while a bogus prop does — and the registry inputs contain actionType and no longer contain type.

Rows 1-3 are green on origin/main too, and that is stated rather than glossed: the renderer already read actionType first, and an undeclared prop is only a warning, which does not stop a page compiling. Row 4 is the one that measures the rename.

Reverse verification — two ablations, each mutated on disk, proven there, measured in the same process, and restored:

LegMutation (blob hashes before/after recorded)PredictedObserved
revert the declaration (name: 'actionType'name: 'type', both files)both files MUTATED vs HEADrow 4 red, rows 1-3 greenexactly that — 1 failed | 3 passed, AssertionError: expected [ Array(1) ] to deeply equal []
restore the read (|| schema.type back on action:icon)action-icon.tsx MUTATED, action-button.tsx UNCHANGEDthe new mirror row red1 failed | 5 passed, expected "vi.fn()" to not be called at all, but actually been called 1 times — the component-id trap fires

The mutated files are imported by relative path from the tests, so they resolve to source and no rebuild is interposed; both legs restore via git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap, and git diff HEAD is empty afterwards. (The first attempt at this ran the mutation script and the test as ablate.sh && vitest, so the trap restored the tree before vitest started and the ablation vacuously passed 4/4. It was re-run with the measurement inside the same process; that is the run reported above.)

6. Bytes — measured, not assumed

Two full console builds, check:eager-closure on each, byte-exact from apps/console/dist/eager-closure.json. Before = 6411def25 (this branch's base) in a dedicated comparison worktree; after = 09562aa26.

beforeafterdelta
aggregate eager closure (gzip)3,254,6043,254,602−2 B
aggregate eager closure (raw)11,175,43111,175,439+8 B
eager chunk count48 of 51648 of 5160
chunk framework (gzip)523,823523,8230 B
chunk ui-components (gzip)396,598396,5980 B
chunk ui-components (raw)1,535,6611,535,669+8 B
chunk vendor-objectstack (gzip)948,329948,3290 B

Which chunk this lands in was the question, and the answer is ui-components, not framework.framework is packages/core|react|types and had 177 B (0.2 KB) of headroom with two PRs already parked on it (#7399); this change is entirely inside packages/components, so framework moves zero bytes — the +8 raw bytes land in ui-components, which absorbs them without moving its gzip figure at all. The remaining ±1-5 B wobble on eight other chunks is gzip entropy: every one of them has an unchanged raw size.

check:eager-closure passes on both builds. ⛔ No ceiling raised, no baseline re-pinned, no gate weakened.

7. Verification run

Union re-run at 09562aa26, the final commit (nothing landed after it).

  • pnpm exec vitest run packages/components/ packages/core/src/registry/ apps/console/src/__tests__/258 files, 2,523 tests, all pass.
  • Consumer sweep, the eight files naming either component outside packages/components (app-shell widget/DOM sweeps, PageView, useConsoleActionRuntime ×2, EnvironmentListToolbar ×2, RecordDetailView, and the schema-catalog DOM-leak test) — 8 files, 275 tests, all pass.
  • pnpm --filter @object-ui/components run type-check — exit 0 (the script is spelled type-check here; the echoed script name confirms it ran rather than matching nothing).
  • pnpm --filter @object-ui/components run lint — exit 0, 0 errors, 950 pre-existing warnings. Run as this repo spells it (eslint ., no --no-inline-config).
  • Gates derived from the actual diff paths, each re-derived from the changed files rather than taken from the dispatch list: check:action-forward-parity ✅ · check:control-bytes ✅ (6,124 tracked text files) · check:sdui-registration-pins ✅ · check:doc-types ✅ · check:doc-snippets ✅ (417/417 blocks, after building what it names — its first exit 2 was PRECONDITION NOT MET, not a red verdict) · check:designer-field-key-parity ✅ · check:vi-mock-specifiers ✅ · check:vi-mock-inherit ✅ · check:eager-closure ✅.

Repo-wide pnpm lint is CI's run and was not repeated locally; the narrowing above is the package the diff is confined to.

8. Clause ② — contract review

A published component input is renamed, so CONTRACT_REVIEW_TIER applies. The shape is pinned by the ruling and implemented mechanically. This PR carries needs:contract-review for the project director seat. ⛔ Not self-reviewed, auto-merge not enabled, not enqueued.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

…action:icon` to `actionType` (#7415)
The declared input carrying an action's execution type collided with the SDUI
envelope's component discriminator, and on no tier could an author actually set
it: the html tier's parser composes `{ type: tag, ...props }` so `type="api"`
replaced the discriminator (and `validate.ts` skips `type` as a BASE_PROP, so
there was no diagnostic); the react-page wrapper stamps the tag last and parks
the author's value under `specType`, which neither renderer reads; through
`SchemaRenderer` the node's `type` is the component id, which the renderers
forwarded to `ActionRunner` as an action type that binds no handler.
`actionType` is the spelling this repo already used for this value — `action:bar`
renames the declared type as it spreads a member, `ActionRunner.execute` resolves
`action.type || action.actionType || action.name`, and both renderers already read
`schema.actionType` first. This makes the one working spelling the declared one,
the same resolution `page:tabs.type` got upstream (retired for `tabStyle`).
No alias, no transition window: the `|| schema.type` leg is removed, so a
declaration that omits `actionType` forwards `undefined` and the runner falls
through to its own `action.name` leg rather than being handed a component id.
Implements objectstack#14490 ruling A (maintainer, 2026-09-02, batch #13 item 4).
Census over 4,797 files found zero authored nodes setting the input.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Handoff to the project director seat — ⛔ NOT reviewed or accepted by this seat

PM note from the domain:ui execution seat (session session_01EMrWaQw3XS5DxTHxp4yRyC), which dispatched #7415.

Clause ② is engaged (a published component input is renamed) and the card states CONTRACT_REVIEW_TIER. This seat runs opus, is not at tier, and has therefore not reviewed, not enabled auto-merge and not enqueued. Confirmed set up as required: draft, needs:contract-review present, Fixes #7415. Landing this unblocks objectstack#14490, which is pm:blocked on it.

Verified independently by this seat, from origin/main — not taken from the report

claimreading
actionType is already this repo's spelling for this valueActionRunner.ts:972const actionType = action.type || action.actionType || action.name || '';
both renderers already read actionTypefirstaction-button.tsx:148 and action-icon.tsx:113 — both type: schema.actionType || schema.type,
the census figure is corroborated by a second instrumentSchemaRenderer.tsx:1126 — the repo's own corpus census reads "action:button (5 nodes)", matching the PR's independent md/mdx scan
the page:tabs.typetabStyle rename precedent exists5 occurrences in registry-inputs-spec-parity.test.ts (1,966 lines)

⇒ The rename does not introduce a word. It promotes the spelling that already worked to being the declared one, and it follows an in-repo precedent that resolved the identical carrier collision by renaming rather than aliasing.

⚠️One thing this seat checked and found the PR right on, recorded so the reviewer does not repeat the detour.action-bar-member-type-resolution.test.tsx:26-27 says action:icon "read schema.typealone and dropped actionType entirely", which reads as contradicting the PR. It does not — that docblock is past tense about a defect since fixed, and action-icon.tsx:105-110 records the repair in its own words. The source, not the prose about the source, settles it.

⭐ The disclosure that deserves the most weight

The implementer caught and reported its own vacuous ablation:

The first attempt at this ran the mutation script and the test as ablate.sh && vitest, so the trap restored the tree before vitest started and the ablation vacuously passed 4/4. It was re-run with the measurement inside the same process; that is the run reported above.

A restore trap that fires before the measurement produces a green that means nothing — and it looks exactly like a successful ablation. It found that, said so, and re-ran. ⭐ It also states plainly that rows 1-3 of the acceptance test are green on origin/main too and that only row 4 measures the rename, rather than presenting four green rows as four proofs.

Recorded, not reviewed — for the reviewer to weigh

⛔ Observations from the dispatching seat, not a tier verdict:

  • The || schema.type fallback is removed, and the reasoning is worth confirming rather than assuming: post-rename that leg is precisely "the old spelling still works", which the ruling forbids; and on every authored path it now yields the component id, never an action type. A declaration omitting actionType forwards undefined, so ActionRunner falls through to its own action.name leg instead of being handed 'action:icon' — the objectui#6306 shape.
  • Bytes: framework moves zero. The change is entirely inside packages/components, which lands in ui-components, not in framework (packages/core|react|types). Measured framework 523,823 → 523,823; ui-components gzip unchanged at 396,598 while absorbing +8 raw bytes; aggregate −2 B gzip. The ±1-5 B wobble on other chunks is explained as gzip entropy with unchanged raw sizes, which is the right way to dismiss it. ⇒ [Decision] The framework per-chunk eager-closure ceiling leaves 177 bytes for the whole repo — two ruled user-facing fixes cannot land, and no wording of either fits #7399 is untouched.
  • The census reports 0 authored sites across 4,797 files with a firing control on every single row — including the negative-space rows (no props/properties bag, no JSX type= attribute).

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.3 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-LBfx1POZ.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)514.88KB117.49KB
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)33.40KB8.71KB
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)47.14KB13.02KB
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.27KB64.20KB
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.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
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.90KB21.12KB
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-manager
os-project-manager marked this pull request as ready for review September 3, 2026 04:46
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit f0bb9faSep 3, 2026
32 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-7415-action-type-input-rename branch September 3, 2026 05:03
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant

@os-project-manager
, '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)!: rename the type input of action:button / action:icon to actionType (#7415) - #7436

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-7415-action-type-input-rename
Sep 3, 2026
Merged

refactor(components)!: rename the type input of action:button / action:icon to actionType (#7415)#7436
os-project-manager merged 1 commit into
mainfrom
claude/issue-7415-action-type-input-rename

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#7415

Implements objectstack#14490 ruling A (maintainer, 2026-09-02, decision batch #13 item 4, verbatim 「同意」): the collision between the component input named type on action:button / action:icon and the SDUI envelope discriminator type is removed at the source. The input is renamed to actionType. No alias, no transition window (the standing 不渐进 rule). Landing this unblocks objectstack#14490, which is pm:blocked on this card.

All readings below were taken at 09562aa26, the head of this branch.

1. Census first — and it is not a formality

The question measured: how many authored sites set the component input named type on action:button or action:icon? Every zero below is reported with a control on the same query that fires non-empty.

SurfaceQueryReadingControl on the same query
JSON trees — 517 files parsed, incl. all 433 schema-catalog entriesobject nodes whose type is action:button / action:icon0 nodes2,410 nodes carrying a type key; 127type: "button" nodes
md/mdx fenced JSON — 234 blocks seen, 207 parsedsame5 nodes28type: "button" nodes in the same blocks
…those 5 nodesdo any set an execution type?0 — key sets are type,label,icon,action (×2) and type,label,action (×3): the discriminator plus the action channelthe 5 themselves
any surfacea props / properties bag carrying type on those two components0none of the 5 has a bag at all
react-page / JSX tieran ActionButton / ActionIcon JSX tag carrying a type= attribute0the tag pattern fires 5 times — all of them local resolveIcon bindings or prose, verified individually
content/docsa page documenting the typeinput of either component0content/docs/core/enhanced-actions.mdx documents the legacy actionType dialect — 30 hits, so the term fires
apps/consolean authored node of either component0public-contract.test.ts lists both ids, so the term fires
packages/** non-test sourcea site authoring the input061 non-test hits on the component ids, all prose / registry ids / action:bar's own composition
read sites of the propschema.type read as an action type2 (action-button.tsx, action-icon.tsx)
declaration sitesregistry inputs + defaultProps2 + 2

Scanned 4,797 files across examples/, apps/console/, content/docs/ and packages/.

The md/mdx count of 5 is independently corroborated: this repo's own corpus census, recorded in packages/react/src/SchemaRenderer.tsx, already reads "action:button (5 nodes)". Two different scanners, same number.

The rename breaks no authored document in this repo. objectstack's html-tier census found zero; the react-tier population measured here is zero as well. A re-check of objectstack#14490's manifest scan would now return an empty list for inputs named type — these two were the only ones of the 57 manifest components declaring one — while the stated control, the variant scan, stays non-empty: both components still declare variant, untouched by this change.

2. The name

actionType, the ruling's working name — and the objectui component configuration does already have a convention, which points at the same string rather than a different one:

  • It is this repo's existing spelling for this exact value.action:bar renames the declared type as it spreads a member onto its child (type: componentType, actionType: action.type), ActionRunner.execute resolves action.type || action.actionType || action.name, ActionDef declares actionType, and both renderers already read schema.actionTypefirst. The rename does not introduce a word; it makes the one spelling that already worked the declared one.
  • The collision class already has a precedent resolution here, and it is a rename, not an alias: page:tabs.type hit the identical carrier collision (a node is the object { type: 'page:tabs', … }, and validate.ts lists type in BASE_PROPS). Upstream retired it in favour of tabStyle (objectstack#6776); apps/console/src/__tests__/registry-inputs-spec-parity.test.ts carries that history in its HARVESTED table.

3. Why the old name could not be authored on any tier

TierWhat happened to type="api"
htmlparse.ts composes { type: tag, ...props } — props spread last — so the value replaced the component discriminator and the node stopped resolving to a component. validate.ts cannot report it either: type is in BASE_PROPS, skipped before the declared-input check runs. Two mechanisms, one outcome, no diagnostic. objectstack PR #14274 landed a refusal here whose prescription ("write the tag you meant") is wrong for these two components.
react-pagethe wrapper stamps type: tag last and parks the author's value under specType (objectui#2880) — which neither action renderer reads.
JSON / SchemaRendererthe node's typeis the component id, and the renderer forwarded it as the action type. 'action:button' binds no handler and no builtin: a click that did nothing, no error, no toast — the objectui#6306 shape.

4. What changed

  • action-button.tsx, action-icon.tsx: registry inputs entry typeactionType; defaultProps follows; the forward reads schema.actionType alone.
  • The || schema.type fallback is removed. Post-rename that leg is precisely "the old spelling still works", which the ruling forbids; and on every authored path it now yields the component id, never an action type. A declaration omitting actionType forwards undefined, so ActionRunner falls through to its own action.name leg instead of being handed a component id — strictly better than the status quo.
  • specType (objectui#2880) is left alone; it addresses other collisions.
  • action:group / action:menu are unaffected: their members carry the spec ActionSchema.type inside an actions array, a different surface. The action-onSuccess-forward fixture now separates the two spellings explicitly rather than conflating them.
  • Changeset: @object-ui/componentsminor, naming the rename. It is the only @object-ui/* package whose published behaviour changes.

⛔ Deliberately not in this PR (objectstack-side follow-ups, per the card): the sdui.manifest.json pin bump, the html-tier refusal's component-aware special case, the react-pages.mdx sentence.

5. Acceptance

An html-tier page setting the renamed input parses and renderspackages/components/src/renderers/action/__tests__/action-type-input-html-tier.test.tsx, four rows through the real kind:'html' page path:

  1. An action:button tag written with label="Mark done" actionType="api" target="/api/v1/tasks/mark_done" compiles with no error panel, renders a real button, and on click hands ActionRunnertype: 'api' with the authored target.
  2. Negative control — the same page with the prop removed still compiles and renders, and the api handler is not reached. That is what pins row 1's 'api' to the authored prop rather than to a default, the tag, or the handler being the only one registered.
  3. No alias — the pre-rename type="api" is still the discriminator: the node's type becomes the bare string api, which no registry entry claims, so the page reports the compile error and renders no button.
  4. The declaration itself, through the manifest built from the live registry (the same call getJsxManifest() makes): actionType produces no unknown-prop diagnostic on either component, while a bogus prop does — and the registry inputs contain actionType and no longer contain type.

Rows 1-3 are green on origin/main too, and that is stated rather than glossed: the renderer already read actionType first, and an undeclared prop is only a warning, which does not stop a page compiling. Row 4 is the one that measures the rename.

Reverse verification — two ablations, each mutated on disk, proven there, measured in the same process, and restored:

LegMutation (blob hashes before/after recorded)PredictedObserved
revert the declaration (name: 'actionType'name: 'type', both files)both files MUTATED vs HEADrow 4 red, rows 1-3 greenexactly that — 1 failed | 3 passed, AssertionError: expected [ Array(1) ] to deeply equal []
restore the read (|| schema.type back on action:icon)action-icon.tsx MUTATED, action-button.tsx UNCHANGEDthe new mirror row red1 failed | 5 passed, expected "vi.fn()" to not be called at all, but actually been called 1 times — the component-id trap fires

The mutated files are imported by relative path from the tests, so they resolve to source and no rebuild is interposed; both legs restore via git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap, and git diff HEAD is empty afterwards. (The first attempt at this ran the mutation script and the test as ablate.sh && vitest, so the trap restored the tree before vitest started and the ablation vacuously passed 4/4. It was re-run with the measurement inside the same process; that is the run reported above.)

6. Bytes — measured, not assumed

Two full console builds, check:eager-closure on each, byte-exact from apps/console/dist/eager-closure.json. Before = 6411def25 (this branch's base) in a dedicated comparison worktree; after = 09562aa26.

beforeafterdelta
aggregate eager closure (gzip)3,254,6043,254,602−2 B
aggregate eager closure (raw)11,175,43111,175,439+8 B
eager chunk count48 of 51648 of 5160
chunk framework (gzip)523,823523,8230 B
chunk ui-components (gzip)396,598396,5980 B
chunk ui-components (raw)1,535,6611,535,669+8 B
chunk vendor-objectstack (gzip)948,329948,3290 B

Which chunk this lands in was the question, and the answer is ui-components, not framework.framework is packages/core|react|types and had 177 B (0.2 KB) of headroom with two PRs already parked on it (#7399); this change is entirely inside packages/components, so framework moves zero bytes — the +8 raw bytes land in ui-components, which absorbs them without moving its gzip figure at all. The remaining ±1-5 B wobble on eight other chunks is gzip entropy: every one of them has an unchanged raw size.

check:eager-closure passes on both builds. ⛔ No ceiling raised, no baseline re-pinned, no gate weakened.

7. Verification run

Union re-run at 09562aa26, the final commit (nothing landed after it).

  • pnpm exec vitest run packages/components/ packages/core/src/registry/ apps/console/src/__tests__/258 files, 2,523 tests, all pass.
  • Consumer sweep, the eight files naming either component outside packages/components (app-shell widget/DOM sweeps, PageView, useConsoleActionRuntime ×2, EnvironmentListToolbar ×2, RecordDetailView, and the schema-catalog DOM-leak test) — 8 files, 275 tests, all pass.
  • pnpm --filter @object-ui/components run type-check — exit 0 (the script is spelled type-check here; the echoed script name confirms it ran rather than matching nothing).
  • pnpm --filter @object-ui/components run lint — exit 0, 0 errors, 950 pre-existing warnings. Run as this repo spells it (eslint ., no --no-inline-config).
  • Gates derived from the actual diff paths, each re-derived from the changed files rather than taken from the dispatch list: check:action-forward-parity ✅ · check:control-bytes ✅ (6,124 tracked text files) · check:sdui-registration-pins ✅ · check:doc-types ✅ · check:doc-snippets ✅ (417/417 blocks, after building what it names — its first exit 2 was PRECONDITION NOT MET, not a red verdict) · check:designer-field-key-parity ✅ · check:vi-mock-specifiers ✅ · check:vi-mock-inherit ✅ · check:eager-closure ✅.

Repo-wide pnpm lint is CI's run and was not repeated locally; the narrowing above is the package the diff is confined to.

8. Clause ② — contract review

A published component input is renamed, so CONTRACT_REVIEW_TIER applies. The shape is pinned by the ruling and implemented mechanically. This PR carries needs:contract-review for the project director seat. ⛔ Not self-reviewed, auto-merge not enabled, not enqueued.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

…action:icon` to `actionType` (#7415)
The declared input carrying an action's execution type collided with the SDUI
envelope's component discriminator, and on no tier could an author actually set
it: the html tier's parser composes `{ type: tag, ...props }` so `type="api"`
replaced the discriminator (and `validate.ts` skips `type` as a BASE_PROP, so
there was no diagnostic); the react-page wrapper stamps the tag last and parks
the author's value under `specType`, which neither renderer reads; through
`SchemaRenderer` the node's `type` is the component id, which the renderers
forwarded to `ActionRunner` as an action type that binds no handler.
`actionType` is the spelling this repo already used for this value — `action:bar`
renames the declared type as it spreads a member, `ActionRunner.execute` resolves
`action.type || action.actionType || action.name`, and both renderers already read
`schema.actionType` first. This makes the one working spelling the declared one,
the same resolution `page:tabs.type` got upstream (retired for `tabStyle`).
No alias, no transition window: the `|| schema.type` leg is removed, so a
declaration that omits `actionType` forwards `undefined` and the runner falls
through to its own `action.name` leg rather than being handed a component id.
Implements objectstack#14490 ruling A (maintainer, 2026-09-02, batch #13 item 4).
Census over 4,797 files found zero authored nodes setting the input.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Handoff to the project director seat — ⛔ NOT reviewed or accepted by this seat

PM note from the domain:ui execution seat (session session_01EMrWaQw3XS5DxTHxp4yRyC), which dispatched #7415.

Clause ② is engaged (a published component input is renamed) and the card states CONTRACT_REVIEW_TIER. This seat runs opus, is not at tier, and has therefore not reviewed, not enabled auto-merge and not enqueued. Confirmed set up as required: draft, needs:contract-review present, Fixes #7415. Landing this unblocks objectstack#14490, which is pm:blocked on it.

Verified independently by this seat, from origin/main — not taken from the report

claimreading
actionType is already this repo's spelling for this valueActionRunner.ts:972const actionType = action.type || action.actionType || action.name || '';
both renderers already read actionTypefirstaction-button.tsx:148 and action-icon.tsx:113 — both type: schema.actionType || schema.type,
the census figure is corroborated by a second instrumentSchemaRenderer.tsx:1126 — the repo's own corpus census reads "action:button (5 nodes)", matching the PR's independent md/mdx scan
the page:tabs.typetabStyle rename precedent exists5 occurrences in registry-inputs-spec-parity.test.ts (1,966 lines)

⇒ The rename does not introduce a word. It promotes the spelling that already worked to being the declared one, and it follows an in-repo precedent that resolved the identical carrier collision by renaming rather than aliasing.

⚠️One thing this seat checked and found the PR right on, recorded so the reviewer does not repeat the detour.action-bar-member-type-resolution.test.tsx:26-27 says action:icon "read schema.typealone and dropped actionType entirely", which reads as contradicting the PR. It does not — that docblock is past tense about a defect since fixed, and action-icon.tsx:105-110 records the repair in its own words. The source, not the prose about the source, settles it.

⭐ The disclosure that deserves the most weight

The implementer caught and reported its own vacuous ablation:

The first attempt at this ran the mutation script and the test as ablate.sh && vitest, so the trap restored the tree before vitest started and the ablation vacuously passed 4/4. It was re-run with the measurement inside the same process; that is the run reported above.

A restore trap that fires before the measurement produces a green that means nothing — and it looks exactly like a successful ablation. It found that, said so, and re-ran. ⭐ It also states plainly that rows 1-3 of the acceptance test are green on origin/main too and that only row 4 measures the rename, rather than presenting four green rows as four proofs.

Recorded, not reviewed — for the reviewer to weigh

⛔ Observations from the dispatching seat, not a tier verdict:

  • The || schema.type fallback is removed, and the reasoning is worth confirming rather than assuming: post-rename that leg is precisely "the old spelling still works", which the ruling forbids; and on every authored path it now yields the component id, never an action type. A declaration omitting actionType forwards undefined, so ActionRunner falls through to its own action.name leg instead of being handed 'action:icon' — the objectui#6306 shape.
  • Bytes: framework moves zero. The change is entirely inside packages/components, which lands in ui-components, not in framework (packages/core|react|types). Measured framework 523,823 → 523,823; ui-components gzip unchanged at 396,598 while absorbing +8 raw bytes; aggregate −2 B gzip. The ±1-5 B wobble on other chunks is explained as gzip entropy with unchanged raw sizes, which is the right way to dismiss it. ⇒ [Decision] The framework per-chunk eager-closure ceiling leaves 177 bytes for the whole repo — two ruled user-facing fixes cannot land, and no wording of either fits #7399 is untouched.
  • The census reports 0 authored sites across 4,797 files with a firing control on every single row — including the negative-space rows (no props/properties bag, no JSX type= attribute).

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.3 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-LBfx1POZ.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)514.88KB117.49KB
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)33.40KB8.71KB
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)47.14KB13.02KB
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.27KB64.20KB
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.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
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.90KB21.12KB
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-manager
os-project-manager marked this pull request as ready for review September 3, 2026 04:46
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit f0bb9faSep 3, 2026
32 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-7415-action-type-input-rename branch September 3, 2026 05:03
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant

@os-project-manager
, '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)!: rename the type input of action:button / action:icon to actionType (#7415) - #7436

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-7415-action-type-input-rename
Sep 3, 2026
Merged

refactor(components)!: rename the type input of action:button / action:icon to actionType (#7415)#7436
os-project-manager merged 1 commit into
mainfrom
claude/issue-7415-action-type-input-rename

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#7415

Implements objectstack#14490 ruling A (maintainer, 2026-09-02, decision batch #13 item 4, verbatim 「同意」): the collision between the component input named type on action:button / action:icon and the SDUI envelope discriminator type is removed at the source. The input is renamed to actionType. No alias, no transition window (the standing 不渐进 rule). Landing this unblocks objectstack#14490, which is pm:blocked on this card.

All readings below were taken at 09562aa26, the head of this branch.

1. Census first — and it is not a formality

The question measured: how many authored sites set the component input named type on action:button or action:icon? Every zero below is reported with a control on the same query that fires non-empty.

SurfaceQueryReadingControl on the same query
JSON trees — 517 files parsed, incl. all 433 schema-catalog entriesobject nodes whose type is action:button / action:icon0 nodes2,410 nodes carrying a type key; 127type: "button" nodes
md/mdx fenced JSON — 234 blocks seen, 207 parsedsame5 nodes28type: "button" nodes in the same blocks
…those 5 nodesdo any set an execution type?0 — key sets are type,label,icon,action (×2) and type,label,action (×3): the discriminator plus the action channelthe 5 themselves
any surfacea props / properties bag carrying type on those two components0none of the 5 has a bag at all
react-page / JSX tieran ActionButton / ActionIcon JSX tag carrying a type= attribute0the tag pattern fires 5 times — all of them local resolveIcon bindings or prose, verified individually
content/docsa page documenting the typeinput of either component0content/docs/core/enhanced-actions.mdx documents the legacy actionType dialect — 30 hits, so the term fires
apps/consolean authored node of either component0public-contract.test.ts lists both ids, so the term fires
packages/** non-test sourcea site authoring the input061 non-test hits on the component ids, all prose / registry ids / action:bar's own composition
read sites of the propschema.type read as an action type2 (action-button.tsx, action-icon.tsx)
declaration sitesregistry inputs + defaultProps2 + 2

Scanned 4,797 files across examples/, apps/console/, content/docs/ and packages/.

The md/mdx count of 5 is independently corroborated: this repo's own corpus census, recorded in packages/react/src/SchemaRenderer.tsx, already reads "action:button (5 nodes)". Two different scanners, same number.

The rename breaks no authored document in this repo. objectstack's html-tier census found zero; the react-tier population measured here is zero as well. A re-check of objectstack#14490's manifest scan would now return an empty list for inputs named type — these two were the only ones of the 57 manifest components declaring one — while the stated control, the variant scan, stays non-empty: both components still declare variant, untouched by this change.

2. The name

actionType, the ruling's working name — and the objectui component configuration does already have a convention, which points at the same string rather than a different one:

  • It is this repo's existing spelling for this exact value.action:bar renames the declared type as it spreads a member onto its child (type: componentType, actionType: action.type), ActionRunner.execute resolves action.type || action.actionType || action.name, ActionDef declares actionType, and both renderers already read schema.actionTypefirst. The rename does not introduce a word; it makes the one spelling that already worked the declared one.
  • The collision class already has a precedent resolution here, and it is a rename, not an alias: page:tabs.type hit the identical carrier collision (a node is the object { type: 'page:tabs', … }, and validate.ts lists type in BASE_PROPS). Upstream retired it in favour of tabStyle (objectstack#6776); apps/console/src/__tests__/registry-inputs-spec-parity.test.ts carries that history in its HARVESTED table.

3. Why the old name could not be authored on any tier

TierWhat happened to type="api"
htmlparse.ts composes { type: tag, ...props } — props spread last — so the value replaced the component discriminator and the node stopped resolving to a component. validate.ts cannot report it either: type is in BASE_PROPS, skipped before the declared-input check runs. Two mechanisms, one outcome, no diagnostic. objectstack PR #14274 landed a refusal here whose prescription ("write the tag you meant") is wrong for these two components.
react-pagethe wrapper stamps type: tag last and parks the author's value under specType (objectui#2880) — which neither action renderer reads.
JSON / SchemaRendererthe node's typeis the component id, and the renderer forwarded it as the action type. 'action:button' binds no handler and no builtin: a click that did nothing, no error, no toast — the objectui#6306 shape.

4. What changed

  • action-button.tsx, action-icon.tsx: registry inputs entry typeactionType; defaultProps follows; the forward reads schema.actionType alone.
  • The || schema.type fallback is removed. Post-rename that leg is precisely "the old spelling still works", which the ruling forbids; and on every authored path it now yields the component id, never an action type. A declaration omitting actionType forwards undefined, so ActionRunner falls through to its own action.name leg instead of being handed a component id — strictly better than the status quo.
  • specType (objectui#2880) is left alone; it addresses other collisions.
  • action:group / action:menu are unaffected: their members carry the spec ActionSchema.type inside an actions array, a different surface. The action-onSuccess-forward fixture now separates the two spellings explicitly rather than conflating them.
  • Changeset: @object-ui/componentsminor, naming the rename. It is the only @object-ui/* package whose published behaviour changes.

⛔ Deliberately not in this PR (objectstack-side follow-ups, per the card): the sdui.manifest.json pin bump, the html-tier refusal's component-aware special case, the react-pages.mdx sentence.

5. Acceptance

An html-tier page setting the renamed input parses and renderspackages/components/src/renderers/action/__tests__/action-type-input-html-tier.test.tsx, four rows through the real kind:'html' page path:

  1. An action:button tag written with label="Mark done" actionType="api" target="/api/v1/tasks/mark_done" compiles with no error panel, renders a real button, and on click hands ActionRunnertype: 'api' with the authored target.
  2. Negative control — the same page with the prop removed still compiles and renders, and the api handler is not reached. That is what pins row 1's 'api' to the authored prop rather than to a default, the tag, or the handler being the only one registered.
  3. No alias — the pre-rename type="api" is still the discriminator: the node's type becomes the bare string api, which no registry entry claims, so the page reports the compile error and renders no button.
  4. The declaration itself, through the manifest built from the live registry (the same call getJsxManifest() makes): actionType produces no unknown-prop diagnostic on either component, while a bogus prop does — and the registry inputs contain actionType and no longer contain type.

Rows 1-3 are green on origin/main too, and that is stated rather than glossed: the renderer already read actionType first, and an undeclared prop is only a warning, which does not stop a page compiling. Row 4 is the one that measures the rename.

Reverse verification — two ablations, each mutated on disk, proven there, measured in the same process, and restored:

LegMutation (blob hashes before/after recorded)PredictedObserved
revert the declaration (name: 'actionType'name: 'type', both files)both files MUTATED vs HEADrow 4 red, rows 1-3 greenexactly that — 1 failed | 3 passed, AssertionError: expected [ Array(1) ] to deeply equal []
restore the read (|| schema.type back on action:icon)action-icon.tsx MUTATED, action-button.tsx UNCHANGEDthe new mirror row red1 failed | 5 passed, expected "vi.fn()" to not be called at all, but actually been called 1 times — the component-id trap fires

The mutated files are imported by relative path from the tests, so they resolve to source and no rebuild is interposed; both legs restore via git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap, and git diff HEAD is empty afterwards. (The first attempt at this ran the mutation script and the test as ablate.sh && vitest, so the trap restored the tree before vitest started and the ablation vacuously passed 4/4. It was re-run with the measurement inside the same process; that is the run reported above.)

6. Bytes — measured, not assumed

Two full console builds, check:eager-closure on each, byte-exact from apps/console/dist/eager-closure.json. Before = 6411def25 (this branch's base) in a dedicated comparison worktree; after = 09562aa26.

beforeafterdelta
aggregate eager closure (gzip)3,254,6043,254,602−2 B
aggregate eager closure (raw)11,175,43111,175,439+8 B
eager chunk count48 of 51648 of 5160
chunk framework (gzip)523,823523,8230 B
chunk ui-components (gzip)396,598396,5980 B
chunk ui-components (raw)1,535,6611,535,669+8 B
chunk vendor-objectstack (gzip)948,329948,3290 B

Which chunk this lands in was the question, and the answer is ui-components, not framework.framework is packages/core|react|types and had 177 B (0.2 KB) of headroom with two PRs already parked on it (#7399); this change is entirely inside packages/components, so framework moves zero bytes — the +8 raw bytes land in ui-components, which absorbs them without moving its gzip figure at all. The remaining ±1-5 B wobble on eight other chunks is gzip entropy: every one of them has an unchanged raw size.

check:eager-closure passes on both builds. ⛔ No ceiling raised, no baseline re-pinned, no gate weakened.

7. Verification run

Union re-run at 09562aa26, the final commit (nothing landed after it).

  • pnpm exec vitest run packages/components/ packages/core/src/registry/ apps/console/src/__tests__/258 files, 2,523 tests, all pass.
  • Consumer sweep, the eight files naming either component outside packages/components (app-shell widget/DOM sweeps, PageView, useConsoleActionRuntime ×2, EnvironmentListToolbar ×2, RecordDetailView, and the schema-catalog DOM-leak test) — 8 files, 275 tests, all pass.
  • pnpm --filter @object-ui/components run type-check — exit 0 (the script is spelled type-check here; the echoed script name confirms it ran rather than matching nothing).
  • pnpm --filter @object-ui/components run lint — exit 0, 0 errors, 950 pre-existing warnings. Run as this repo spells it (eslint ., no --no-inline-config).
  • Gates derived from the actual diff paths, each re-derived from the changed files rather than taken from the dispatch list: check:action-forward-parity ✅ · check:control-bytes ✅ (6,124 tracked text files) · check:sdui-registration-pins ✅ · check:doc-types ✅ · check:doc-snippets ✅ (417/417 blocks, after building what it names — its first exit 2 was PRECONDITION NOT MET, not a red verdict) · check:designer-field-key-parity ✅ · check:vi-mock-specifiers ✅ · check:vi-mock-inherit ✅ · check:eager-closure ✅.

Repo-wide pnpm lint is CI's run and was not repeated locally; the narrowing above is the package the diff is confined to.

8. Clause ② — contract review

A published component input is renamed, so CONTRACT_REVIEW_TIER applies. The shape is pinned by the ruling and implemented mechanically. This PR carries needs:contract-review for the project director seat. ⛔ Not self-reviewed, auto-merge not enabled, not enqueued.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

…action:icon` to `actionType` (#7415)
The declared input carrying an action's execution type collided with the SDUI
envelope's component discriminator, and on no tier could an author actually set
it: the html tier's parser composes `{ type: tag, ...props }` so `type="api"`
replaced the discriminator (and `validate.ts` skips `type` as a BASE_PROP, so
there was no diagnostic); the react-page wrapper stamps the tag last and parks
the author's value under `specType`, which neither renderer reads; through
`SchemaRenderer` the node's `type` is the component id, which the renderers
forwarded to `ActionRunner` as an action type that binds no handler.
`actionType` is the spelling this repo already used for this value — `action:bar`
renames the declared type as it spreads a member, `ActionRunner.execute` resolves
`action.type || action.actionType || action.name`, and both renderers already read
`schema.actionType` first. This makes the one working spelling the declared one,
the same resolution `page:tabs.type` got upstream (retired for `tabStyle`).
No alias, no transition window: the `|| schema.type` leg is removed, so a
declaration that omits `actionType` forwards `undefined` and the runner falls
through to its own `action.name` leg rather than being handed a component id.
Implements objectstack#14490 ruling A (maintainer, 2026-09-02, batch #13 item 4).
Census over 4,797 files found zero authored nodes setting the input.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@os-project-managerClaude

Copy link
Copy Markdown
Collaborator

Handoff to the project director seat — ⛔ NOT reviewed or accepted by this seat

PM note from the domain:ui execution seat (session session_01EMrWaQw3XS5DxTHxp4yRyC), which dispatched #7415.

Clause ② is engaged (a published component input is renamed) and the card states CONTRACT_REVIEW_TIER. This seat runs opus, is not at tier, and has therefore not reviewed, not enabled auto-merge and not enqueued. Confirmed set up as required: draft, needs:contract-review present, Fixes #7415. Landing this unblocks objectstack#14490, which is pm:blocked on it.

Verified independently by this seat, from origin/main — not taken from the report

claimreading
actionType is already this repo's spelling for this valueActionRunner.ts:972const actionType = action.type || action.actionType || action.name || '';
both renderers already read actionTypefirstaction-button.tsx:148 and action-icon.tsx:113 — both type: schema.actionType || schema.type,
the census figure is corroborated by a second instrumentSchemaRenderer.tsx:1126 — the repo's own corpus census reads "action:button (5 nodes)", matching the PR's independent md/mdx scan
the page:tabs.typetabStyle rename precedent exists5 occurrences in registry-inputs-spec-parity.test.ts (1,966 lines)

⇒ The rename does not introduce a word. It promotes the spelling that already worked to being the declared one, and it follows an in-repo precedent that resolved the identical carrier collision by renaming rather than aliasing.

⚠️One thing this seat checked and found the PR right on, recorded so the reviewer does not repeat the detour.action-bar-member-type-resolution.test.tsx:26-27 says action:icon "read schema.typealone and dropped actionType entirely", which reads as contradicting the PR. It does not — that docblock is past tense about a defect since fixed, and action-icon.tsx:105-110 records the repair in its own words. The source, not the prose about the source, settles it.

⭐ The disclosure that deserves the most weight

The implementer caught and reported its own vacuous ablation:

The first attempt at this ran the mutation script and the test as ablate.sh && vitest, so the trap restored the tree before vitest started and the ablation vacuously passed 4/4. It was re-run with the measurement inside the same process; that is the run reported above.

A restore trap that fires before the measurement produces a green that means nothing — and it looks exactly like a successful ablation. It found that, said so, and re-ran. ⭐ It also states plainly that rows 1-3 of the acceptance test are green on origin/main too and that only row 4 measures the rename, rather than presenting four green rows as four proofs.

Recorded, not reviewed — for the reviewer to weigh

⛔ Observations from the dispatching seat, not a tier verdict:

  • The || schema.type fallback is removed, and the reasoning is worth confirming rather than assuming: post-rename that leg is precisely "the old spelling still works", which the ruling forbids; and on every authored path it now yields the component id, never an action type. A declaration omitting actionType forwards undefined, so ActionRunner falls through to its own action.name leg instead of being handed 'action:icon' — the objectui#6306 shape.
  • Bytes: framework moves zero. The change is entirely inside packages/components, which lands in ui-components, not in framework (packages/core|react|types). Measured framework 523,823 → 523,823; ui-components gzip unchanged at 396,598 while absorbing +8 raw bytes; aggregate −2 B gzip. The ±1-5 B wobble on other chunks is explained as gzip entropy with unchanged raw sizes, which is the right way to dismiss it. ⇒ [Decision] The framework per-chunk eager-closure ceiling leaves 177 bytes for the whole repo — two ruled user-facing fixes cannot land, and no wording of either fits #7399 is untouched.
  • The census reports 0 authored sites across 4,797 files with a firing control on every single row — including the negative-space rows (no props/properties bag, no JSX type= attribute).

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.3 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-LBfx1POZ.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)514.88KB117.49KB
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)33.40KB8.71KB
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)47.14KB13.02KB
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.27KB64.20KB
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.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
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.90KB21.12KB
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-manager
os-project-manager marked this pull request as ready for review September 3, 2026 04:46
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit f0bb9faSep 3, 2026
32 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-7415-action-type-input-rename branch September 3, 2026 05:03
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant

@os-project-manager