Uh oh!
There was an error while loading. Please reload this page.
refactor(components)!: rename the type input of action:button / action:icon to actionType (#7415) - #7436
Conversation
…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-manager
commented
Sep 3, 2026
Handoff to the project director seat — ⛔ NOT reviewed or accepted by this seatPM note from the Clause ② is engaged (a published component input is renamed) and the card states Verified independently by this seat, from |
| claim | reading |
|---|---|
actionType is already this repo's spelling for this value | ActionRunner.ts:972 — const actionType = action.type || action.actionType || action.name || ''; |
both renderers already read actionTypefirst | action-button.tsx:148 and action-icon.tsx:113 — both type: schema.actionType || schema.type, |
| the census figure is corroborated by a second instrument | SchemaRenderer.tsx:1126 — the repo's own corpus census reads "action:button (5 nodes)", matching the PR's independent md/mdx scan |
the page:tabs.type → tabStyle rename precedent exists | 5 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.
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.typefallback 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 omittingactionTypeforwardsundefined, soActionRunnerfalls through to its ownaction.nameleg instead of being handed'action:icon'— the objectui#6306 shape. - Bytes:
frameworkmoves zero. The change is entirely insidepackages/components, which lands inui-components, not inframework(packages/core|react|types). Measuredframework523,823 → 523,823;ui-componentsgzip 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] Theframeworkper-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/propertiesbag, no JSXtype=attribute).
Generated by Claude Code
✅ Console Performance Budget
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
Size Limits
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#7415
Implements objectstack#14490 ruling A (maintainer, 2026-09-02, decision batch #13 item 4, verbatim 「同意」): the collision between the component input named
typeonaction:button/action:iconand the SDUI envelope discriminatortypeis removed at the source. The input is renamed toactionType. No alias, no transition window (the standing 不渐进 rule). Landing this unblocks objectstack#14490, which ispm:blockedon 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
typeonaction:buttonoraction:icon? Every zero below is reported with a control on the same query that fires non-empty.typeisaction:button/action:icontypekey; 127type: "button"nodestype: "button"nodes in the same blockstype,label,icon,action(×2) andtype,label,action(×3): the discriminator plus theactionchannelprops/propertiesbag carryingtypeon those two componentsActionButton/ActionIconJSX tag carrying atype=attributeresolveIconbindings or prose, verified individuallycontent/docstypeinput of either componentcontent/docs/core/enhanced-actions.mdxdocuments the legacyactionTypedialect — 30 hits, so the term firesapps/consolepublic-contract.test.tslists both ids, so the term firespackages/**non-test sourceaction:bar's own compositionschema.typeread as an action typeaction-button.tsx,action-icon.tsx)inputs+defaultPropsScanned 4,797 files across
examples/,apps/console/,content/docs/andpackages/.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, thevariantscan, stays non-empty: both components still declarevariant, 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:action:barrenames the declared type as it spreads a member onto its child (type: componentType, actionType: action.type),ActionRunner.executeresolvesaction.type || action.actionType || action.name,ActionDefdeclaresactionType, and both renderers already readschema.actionTypefirst. The rename does not introduce a word; it makes the one spelling that already worked the declared one.page:tabs.typehit the identical carrier collision (a node is the object{ type: 'page:tabs', … }, andvalidate.tsliststypeinBASE_PROPS). Upstream retired it in favour oftabStyle(objectstack#6776);apps/console/src/__tests__/registry-inputs-spec-parity.test.tscarries that history in itsHARVESTEDtable.3. Why the old name could not be authored on any tier
type="api"parse.tscomposes{ type: tag, ...props }— props spread last — so the value replaced the component discriminator and the node stopped resolving to a component.validate.tscannot report it either:typeis inBASE_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.type: taglast and parks the author's value underspecType(objectui#2880) — which neither action renderer reads.SchemaRenderertypeis 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: registryinputsentrytype→actionType;defaultPropsfollows; the forward readsschema.actionTypealone.|| schema.typefallback 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 omittingactionTypeforwardsundefined, soActionRunnerfalls through to its ownaction.nameleg 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:menuare unaffected: their members carry the specActionSchema.typeinside anactionsarray, a different surface. Theaction-onSuccess-forwardfixture now separates the two spellings explicitly rather than conflating them.@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.jsonpin bump, the html-tier refusal's component-aware special case, thereact-pages.mdxsentence.5. Acceptance
An html-tier page setting the renamed input parses and renders —
packages/components/src/renderers/action/__tests__/action-type-input-html-tier.test.tsx, four rows through the realkind:'html'page path:action:buttontag written withlabel="Mark done" actionType="api" target="/api/v1/tasks/mark_done"compiles with no error panel, renders a real button, and on click handsActionRunnertype: 'api'with the authoredtarget.apihandler 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.type="api"is still the discriminator: the node's type becomes the bare stringapi, which no registry entry claims, so the page reports the compile error and renders no button.getJsxManifest()makes):actionTypeproduces nounknown-propdiagnostic on either component, while a bogus prop does — and the registryinputscontainactionTypeand no longer containtype.Rows 1-3 are green on
origin/maintoo, and that is stated rather than glossed: the renderer already readactionTypefirst, 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:
name: 'actionType'→name: 'type', both files)HEAD1 failed | 3 passed,AssertionError: expected [ Array(1) ] to deeply equal []|| schema.typeback onaction:icon)action-icon.tsxMUTATED,action-button.tsxUNCHANGED1 failed | 5 passed,expected "vi.fn()" to not be called at all, but actually been called 1 times— the component-id trap firesThe 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_PATHunder anEXIT INT TERMtrap, andgit diff HEADis empty afterwards. (The first attempt at this ran the mutation script and the test asablate.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-closureon each, byte-exact fromapps/console/dist/eager-closure.json. Before =6411def25(this branch's base) in a dedicated comparison worktree; after =09562aa26.framework(gzip)ui-components(gzip)ui-components(raw)vendor-objectstack(gzip)Which chunk this lands in was the question, and the answer is
ui-components, notframework.frameworkispackages/core|react|typesand had 177 B (0.2 KB) of headroom with two PRs already parked on it (#7399); this change is entirely insidepackages/components, soframeworkmoves zero bytes — the +8 raw bytes land inui-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-closurepasses 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.packages/components(app-shellwidget/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 spelledtype-checkhere; 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).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 wasPRECONDITION 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 lintis 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_TIERapplies. The shape is pinned by the ruling and implemented mechanically. This PR carriesneeds:contract-reviewfor 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