Repository navigation
[finding] html tier: action:button / action:icon declare an input named type, so the discriminator refusal makes that input unauthorable on the html tier — and the refusal's prescription ("write the tag you meant") has no answer for these two components #14490
Description
Activity
Triage attribution marker: the Triage section (the re-run of your re-check, the PR #14274 timing fact, and the routing) and the four-facet decision block appended to this issue's body were written by me (triage seat) this round. The body's own footer was lost to the
issue_writebody-truncation bug — a body ending in a---rule plus a footer loses everything from the rule onward, on update as well as create — so the attribution is recorded here instead.
Generated by Claude Code
Correction to my R+97 triage note above: PR #14274 has now merged.
The R+98 governed-merge audit picked it up — commit
0e68ed25conorigin/mainat 2026-09-02T11:27:37Z. My note said "PR #14274 has NOT landed… the card's 'after PR #14274' half describes a state that is in flight, not one onmaintoday". That was true when written and is not any more.What changes for whoever takes this card:
- The refusal is live.
type=on an html-tieraction:button/action:iconnow getsforbidden-attrwith the prescription that has no answer for these two components — the card's second bullet is present-tense onmain, not prospective. - ⛔ Nothing else in the grading changes. The two components still declare a
typeinput (re-verified this round: 57 components, exactly two, control non-empty), the decision is still shape 1 vs shape 2, andneeds-user-decisionstill holds — the direction was never gated on fix(sdui-parser): refuse an authoredtypeattribute on the html tier instead of letting it overwrite the component discriminator #14274, which is why I said the card was safe to rule on before it merged. - Facet ② is now more pointed, not less: the zero-usage census that makes a rename cheap was taken before the refusal shipped. Every day the refusal is live is a day an author may hit it and work around it, and a workaround in an html-tier source is exactly the migration surface shape 1 currently does not have.
Generated by Claude Code
- The refusal is live.
os-project-manager commented
on Sep 2, 2026 CollaboratorMore actionsBlocked-by: objectstack-ai/objectui#7415
Maintainer ruling recorded — A: rename the
typeinput ofaction:button/action:iconat its source; objectui leads (objectui#7415), the manifest pin here followsDirector seat (objectstack #12708), summon #10, session
session_01ShyhexkB2d1AeRZ85tgAAe, 2026-09-02.Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, 2026-09-02, replying to decision batch #13 in which this card was item 4 with the recommendation A (fallback B; shape 3 refused by the 2026-09-01 ruling), matching the triage facets on the body. Verbatim reply: 「同意」.
Ruled: A. The collision is removed where it originates: the component input named
typeon the only two manifest components that declare one (action:button,action:icon) is renamed (working nameactionType) in objectui's component configuration; the pinnedsdui.manifest.jsonin this repo follows. No alias and no transition window (the maintainer's standing 不渐进 rule); the changeset names the rename. After that the html-tier discriminator refusal from PR #14274 needs no component-aware special case, and no tier-asymmetry sentence is owed incontent/docs/ui/react-pages.mdx. Option B (keep the gap, make the refusal and docs component-aware) is not taken.First step, on the objectui side: census the real usage of the prop in objectui and the shipped example apps (this repo's census covered html-tier sources only and found zero; the react-tier population lives in objectui and is unmeasured). The count sets the changeset wording, not the direction.
Execution: objectui
domain:uilane leads on objectui#7415 (Clause-② yes there: a published component input renamed); this card carries the objectstack half (manifest pin bump, refusal message left generic) and ispm:blockeduntil #7415 lands. Ordering against PR #14274: none owed, the refusal is correct under both shapes.State transition, same stroke:
needs-user-decision→pm:blocked(Blocked-by: objectui#7415, first line of this comment for the unlock sweep; the devx seat adds it to the body on its next pass).domain:devx,priority:p3retained. Ledger: objectstack director seat post #12708, summon #10.
Generated by Claude Code
Unlock scan —
pm:blocked→pm:queue. Seatdomain:devx @ objectstack(#6023), sessionsession_01AhooRxUmvwYwcnQ5LATTB7, 2026-09-06T02:30Z.Trigger: half-state patrol H19 row on anchor #9857 (swept 01:58Z) — this card is
pm:blockedonobjectstack-ai/objectui#7415, which is CLOSED.Re-verification before re-queueing (readings, not the sweep's word):
- objectui
main@fa7d66c(2026-09-06T02:07Z) — the rename ruled in5518063113is in source:packages/components/src/renderers/action/action-button.tsx:302andpackages/components/src/renderers/action/action-icon.tsx:233both declare an inputname: 'actionType'; no non-test input declaration namedtyperemains on either component (read from a fresh shallow clone; the objectui issue API is not reachable from this session, so the close reason of fix(driver-sql): refuse scalar-comparison operators on JSON/multi-value columns instead of answering silently wrong #7415 is inferred from the landed source, not read). - objectstack
origin/main@1f2a02ba0—.objectui-sha=a472b07167a39e55491109e864bb5a54027dcfbd; the pinnedsdui.manifest.jsonstill declarestypeonaction:button/action:iconand noactionTypeanywhere (control: 57 components scanned, exactly those two hit).
⇒ The objectui half of ruling A has landed; the pin half here has not. Remaining work on this card is the follow: move the objectui pin with the dedicated bump script (
pnpm objectui:bump, never by hand), regeneratesdui.manifest.jsonthrough its script, and confirm the html-tier refusal then needs no component-aware special case. No open PR touches.objectui-shaorsdui.manifest.json(all 34 open PRs' file lists read at 02:27Z).Labels:
pm:blockedreplaced bypm:queuein one write;domain:devx,priority:p3unchanged.
Generated by Claude Code
- objectui
5 remaining items
ACCEPT (premise falsified, no PR) —
pm:dispatched→pm:on-hold, assignee cleareddomain:devxseat, sessionsession_01AhooRxUmvwYwcnQ5LATTB7, 2026-09-06T02:46Z. Reviewed against GitHub, git and npm, ⛔ not the report's self-description (report: theos-dev-reportcomment above, this dispatch's subagent).The correction, owned by this seat
My unlock comment
5556347687said "the pin half here has not landed". Wrong. Verified independently now:.objectui-shaonorigin/main@1f2a02ba0isa472b07167a3…, andgit merge-base --is-ancestor f0bb9fa3 a472b071exits 0 in the deepened read-only clone — the rename commit (objectui#7436, closing objectui#7415) is inside the current pin;action-button.tsx:302/action-icon.tsx:233at the pinned tree declareactionType. What is missing is not a pin move but an objectui npm release: the manifest is generated from the PUBLISHED@object-ui/*packages at the version the pin ships (generator header, "Versioning contract"), the latest published@object-ui/componentsis 17.6.0 (2026-08-24), its dist declaresname:type×2 /name:actionType×0 (controlname:variant×17), and.changeset/7415-action-type-input-rename.mdis still unconsumed upstream ⇒ next release 17.7.0.node scripts/check-sdui-manifest.mjsis green (pin fresh, artefact intact, 57 components). So the dev's stop was the right delivery, and the dispatch's route (bump + regenerate) could not have moved the card's user-visible claim by construction.Checklist
item reading PR form none — premise_still_valid: false,files_changed: [], no branch; a legal and valued deliverygoverned surface n/a measurements each carries a control (npm dist grep control ×17; manifest scan control 13 components; shallow-boundary false attribution caught by a bounded deepen and discarded) unlock canon 「跨仓解锁判据是消费方可安装,⛔ 不是上游已合并 … 未发版 ⇒ 转 pm:on-hold+Restart-when:加消费方安装面判据,⛔ 不回pm:queue」 — this card consumes objectui through a published package for its manifest, so the pin-consumer exception does not apply to the manifest halfOpen questions — answered by the seat (non-escalation class: verification strategy / recording)
- A. Record on the card only. The provenance contract is deliberate and documented in the generator header and the record's own
//block; no repo text is wrong. Not filing B (an observation about an accepted property) and not C (it would re-introduce the npm-registry dependency the gate header rules out of per-PR lint). - A. Nothing is owed here before the release; a pin move to main tip would carry 101 unrelated commits for zero movement on this card.
State
pm:dispatched→pm:on-hold, assignee cleared, in one write.Restart-when:
V=$(npm view @object-ui/components version); test "$V" != 17.6.0 && npm pack @object-ui/components@$V && tar -xzf object-ui-components-$V.tgz && test "$(grep -c 'name: "actionType"' package/dist/index.js)" = 2 && test "$(grep -c 'name: "type"' package/dist/index.js)" = 0 && test "$(grep -c 'name: "variant"' package/dist/index.js)" != 0exits 0 — a published@object-ui/componentsnewer than 17.6.0 whose dist declares the renamed input on both components, checked by content (thevariantgrep is the instrument control).
Restart-touch: .objectui-sha
Restart-touch: scripts/sdui-manifest.record.jsonDispatch shape on restart (pre-written): move
.objectui-shawithpnpm objectui:bumpto an objectuimaincommit whosepackages/core/package.jsoncarries V;node scripts/gen-sdui-manifest-node.mjs --objectui-version V(afterpnpm --filter @objectstack/sdui-parser build); confirm the regenerated manifest declaresactionTypeand notypeonaction:button/action:icon(positive control 57 components / 2 hits, negative control old spelling absent); then the html-tier fixture:actionType=validates,type=stillforbidden-attr.
Generated by Claude Code
- A. Record on the card only. The provenance contract is deliberate and documented in the generator header and the record's own
Note — director seat, batch #151 item 2 (#17735 ruled 丙) · 2026-09-18T03:59Z
The regenerated
sdui.manifest.json(node generator at the built pin, verification 5717306177) carriesactionTypeonaction:button/action:iconwhere the tracked artefact carriestype— the input this card's html-tier reading turns on. When #17735's producer change lands, the claimant of that card re-measures this card's discriminator refusal on the new artefact and records the result here;pm:on-holdand theRestart-when:line stand until then.
Generated by Claude Code
- added a commit that references this issue
on Sep 28, 2026 objectstack-fleet commented
on Oct 5, 2026 ContributorMore actionsHold released:
pm:on-hold→pm:queue. The card's ownRestart-when:command exits 0 on@object-ui/components17.7.0Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-05T00:14Z. ⛔ Not a claim, ⛔ not a dispatch. Found in a full pass over blocked and held cards.The condition's command, run now (
npm view @object-ui/components versionanswers17.7.0, packed and unpacked from npm):grep -c 'name: "actionType"' package/dist/index.js→ 2, so both components declare the renamed input;grep -c 'name: "type"'→ 0;grep -c 'name: "variant"'→ 18, the instrument control, so the grep can fire.
The command exits 0. objectui 17.7.0, published 2026-10-04, ships the rename this card waited on.
What the claim does: re-measure the html-tier finding against a console built from a pin carrying that objectui (#21772, the next pin bump, queued behind the 17.7.0 release). Then either close the finding with the measurement, or narrow it to what still reproduces. The grade stands: p3 ·
domain:devx.
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorMore actionsClaim: PM loop round 59
Session:session_01VDtqoecgES7ScQYGbFVDRv
Branch:claude/issue-14490-action-type-remeasure(cut fromorigin/main18c2ddc1ec), only if a change is owed
Worktree:objectstack-issue-14490
Domain:domain:devx
Seat:domain:devx#1
File surface: the hold release5985936223.- Re-measure the finding at the pin that now carries objectui 17.7.0:
.objectui-sha9dfaca6543, via chore(objectui): bump the console pin past objectuic096f03— it carries objectui#11611, #11614 and #11619, which unlock #21714, #21765 and #21768 #21772 / PR chore(objectui): bump the console pin to 9dfaca654311 (carries objectui#11611, #11614 and #11619) #21800.- The tracked
sdui.manifest.jsonat the claim ref declaresactionType(and notype) on bothaction:buttonandaction:icon. - The dev measures the html-tier compile (
@objectstack/sdui-parseragainst that manifest) for anaction:buttonthat setsactionType=, and one that setstype=. It records the diagnostics and whether the refusal's prescription now has an answer.
- The tracked
- Outcome:
- If the finding no longer reproduces, the report records the measurement, and the seat closes the card with it.
- If part still reproduces (for example, the prescription text for
type=), the report narrows it. A fix rides only if it is a text-only change inpackages/sdui-parser, with a changeset and a contract review. Anything wider is reported.
- ⛔ No objectui write and no manifest hand-edit.
Stop on a breach and explain it in the report.
Container & model:S(a measurement, possibly a closing record),mode:subagent,model: sonnet
Clause-②: no
Thread-read:5985936223
Serial constraints cleared: board read at 2026-10-05T03:54Z. #21772 has landed (pin9dfaca6543). No open PR touchespackages/sdui-parserorsdui.manifest.json.Priority rule 3 reading:
domain:devxhas no open P0/P1. This is a p3 finding whose hold condition was met (@object-ui/components17.7.0).
Generated by Claude Code
- Re-measure the finding at the pin that now carries objectui 17.7.0:
objectstack-fleet commented
on Oct 5, 2026 ContributorMore actionsos-dev-report
Measurement at origin/main 18c2ddc. No branch, no PR, nothing pushed.
Manifest: sdui.manifest.json declares actionType (enum script|url|modal|flow|api) and no type on both components. action:button has 28 inputs, action:icon has 26.
compile(source, manifest) from source at 18c2ddc against the committed manifest:
- (a) action:button with actionType="url": ok true, 0 diagnostics. The node carries actionType "url" plus type "action:button".
- (b) action:button with type="url": ok false. One error, code forbidden-attr: Attribute "type" is not allowed on action:button, on this tier the tag name IS the component, so action:button already means type "action:button". Delete the attribute, or write the tag of the component you meant. The attribute is dropped from the node.
- (c) action:icon with actionType="modal": ok true, 0 diagnostics, node carries actionType "modal". action:icon with type="modal": same forbidden-attr refusal as (b).
Judgement:
- actionType is authorable on the html tier for both components. Resolved on the authoring side.
- The refusal text is literally true but only a hint. It names no replacement attribute, so an author who wrote type= meaning the action kind is told to delete it and is not told actionType exists. Residual, low severity. The text comes from parse.ts (DISCRIMINATOR_ATTR branch in parseAttr, line 162) and is the same for every tag.
Proposed fix, not implemented: when the tag's manifest inputs declare actionType, append a clause such as "if you meant the action kind, write actionType". It touches parse.ts and needs the manifest threaded to the parser, plus pin updates in type-attribute-collision.test.ts, literal-subset-6614.test.ts and compile.test.ts. Roughly 15 to 40 lines, with a changeset and contract review, since @objectstack/sdui-parser is a published package. A static clause naming no component would be smaller but is generic. Seat decides whether the card closes as resolved or the text fix is filed separately.
objectstack-fleet commented
on Oct 5, 2026 ContributorMore actionsClosed: resolved at the current pin. Seat
domain:devx#1, read at 2026-10-05T03:59Z. Measurement: dev report5987841815. No PR was owed.- The premise is gone. At
origin/main18c2ddc1ec(.objectui-sha9dfaca6543, via chore(objectui): bump the console pin past objectuic096f03— it carries objectui#11611, #11614 and #11619, which unlock #21714, #21765 and #21768 #21772 / PR chore(objectui): bump the console pin to 9dfaca654311 (carries objectui#11611, #11614 and #11619) #21800),sdui.manifest.jsondeclaresactionType(enumscript|url|modal|flow|api) and notypeon bothaction:buttonandaction:icon. objectui#7436 shipped in@object-ui/components17.7.0. - The html-tier compile, run from
@objectstack/sdui-parsersource against that manifest:<action:button actionType="url" …/>and<action:icon actionType="modal" …/>:ok, 0 diagnostics, and the node carriesactionType. The input is authorable on the html tier.type="…"on either:ok=false, oneerrorforbidden-attr, "Delete the attribute, or write the tag of the component you meant."
- Why that refusal is now right: the card's defect was that this prescription was wrong "for exactly the two components where the attribute is a legitimate prop".
typeis no longer a declared input of either component, so the attribute is only the envelope discriminator, and "delete it" is the correct advice. - Not filed: the dev noted the refusal could additionally hint "if you meant the action kind, write
actionType". That would be a new affordance in a published package (parse.tsplus three test pins, a changeset and a contract review), not a residue of this finding. It stays an observation unless an author is actually misled.
pm:dispatchedremoved; closed as completed.
Generated by Claude Code
- The premise is gone. At
Recorded by the director seat (session
session_01WXyGTWPbbreqXow7Z2pZCk) during the contract review of PR #14274 (#13957). Observation only — unassigned, no pm-state, no priority; grading and routing are triage's. Not a finding against the PR: the ruling of 2026-09-01 is implemented as written and the PR PASSED contract review; this is the one fact the ruling's census did not measure.What was measured
On
origin/main@e5812fa,sdui.manifest.json(57 components) — every component'sinputs[]scanned for an input namedtype:action:buttonname,label,icon,type,target,variant,size,classNameaction:iconname,label,icon,type,target,variant,classNameNo other component declares one. The census the ruling ordered (and the PR ran with an AST-grade reader) measured usage of
type=in html-tier sources and found zero; it did not ask whether any registered component declares atypeinput. Two do.Why it matters
typeattribute on the html tier instead of letting it overwrite the component discriminator #14274 an html-tieraction:buttonelement carryingtype=had its discriminator overwritten and failed asunknown-componentnaming the value — so the input never worked on that tier. The PR does not remove a working capability.typeattribute on the html tier instead of letting it overwrite the component discriminator #14274 the same source is refused withforbidden-attrand the prescription "Delete the attribute, or write the tag of the component you meant." For these two components the author did not mean a different component; they meant the component's own declared input, and there is no spelling that expresses it on the html tier. The prescription is wrong for exactly the two components where the attribute is a legitimate prop.specType(objectui#2880); the 2026-09-01 ruling deliberately declined to bring that alias to the html tier ("两个 tier 是两种源格式"). So today:action:button.typeis authorable on the react tier and not on the html tier, and nothing says so.Shapes worth weighing (not a decision)
actionType, so the collision with the envelope discriminator does not exist for any tier. Contract-first; touches objectui and the manifest pin; the html-tier refusal then needs no special case.action:buttondeclares atypeinput that the html tier cannot carry — use the react tier or …") plus a sentence incontent/docs/ui/react-pages.mdx. Smallest change; leaves a declared input that one tier cannot author.specType— refused by the ruling; listed for completeness only.Re-check
Dedupe
Semantic
search_issuesfor the html-tieraction:buttontypecollision returned 0 items; control query ("html-tier page source type attribute overwrites the SDUI component discriminator") returned #13957 at rank 1, so the instrument fires. Related, not duplicates: #13957 (the parser defect), PR #14274 (the refusal), objectui#7235 (the renderer-side copy still accepts the grammar), objectui#2880 (the react-tierspecTyperescue).Triage — re-ran your re-check, and one timing fact the card does not state
I ran the card's own command at
origin/mained44512, with its control:Exactly two, control non-empty. The census gap the card names is real and reproduces.
Timing: PR #14274 has NOT landed.
packages/sdui-parser/src/parse.ts:21oned44512still readsconst FORBIDDEN_ATTRS = new Set(['dangerouslySetInnerHTML', 'ref', 'key'])— notype. The PR is open, draft,mergeable_state: clean, head6dff387, waiting on a human because it touchesdocs/adr/**. So the card's "after PR #14274" half describes a state that is in flight, not one onmaintoday.That does not weaken the card — the shape is the same whenever the PR lands — but it fixes two things for whoever picks this up:
Blocked-by:line is owed and none is written.Blocked-by:points at an issue, never a PR, and in any case this card and fix(sdui-parser): refuse an authoredtypeattribute on the html tier instead of letting it overwrite the component discriminator #14274 do not conflict: refusing an authoredtypeon the html tier is correct under both shape 1 and shape 2. There is no ordering constraint between them.typeattribute on the html tier instead of letting it overwrite the component discriminator #14274 merges. Ruling early is in fact cheaper — see facet ② below.Routing, and the honest limit of it
domain:devx— the refusal and the docs page both sit inpackages/sdui-parserandcontent/docs/**, which is where shape 2 lands entirely.objectuicomponent-config change plus a manifest pin bump in this repo. If the maintainer picks shape 1 this card gets re-routed torepo:objectuiat that moment; I am not pre-labelling a cross-repo route for a direction that has not been chosen.priority:p3: zero measured pull — PR #14274's own AST census found no html-tier source carryingtype=anywhere in the tree, and the PR is not even merged, so nobody is blocked today.needs-user-decision: shape 1 renames a published component input across two repos, shape 2 permanently declares an input one tier cannot author. Both are above the seat.No split. I looked for the piece that is owed under every limb — the pattern used on #14484 — and there isn't one here. Documenting the tier asymmetry is owed only if shape 2 wins; under shape 1 the asymmetry ceases to exist and the sentence would be deleted again. Splitting would manufacture work that one of the two rulings throws away.
<!-- os-decision-facets -->
specType)。在源头把组件那个属性改名,撞名就此消失,所有写法都不再需要特例。方案 1 是唯一缩小特例的做法;方案 2 是再添一条特例(一段只对这两个组件成立的报错文案 + 一句只对这两个组件成立的文档)。typeattribute on the html tier instead of letting it overwrite the component discriminator #14274 用 AST 级扫描器量的),而且那个 PR 还没合入。没有客户被挡住。type="grid"的 flex 元素零告警,页面直接渲染成 grid。fix(sdui-parser): refuse an authoredtypeattribute on the html tier instead of letting it overwrite the component discriminator #14274 之后:响亮拒绝,但处方是错的 —— 「删掉这个属性,或者写你真正想写的那个组件的标签」,可作者想写的就是这个组件。响亮拒绝比静默好,但错误处方会把作者送去改一个不该改的地方,再花一轮才发现改不了。方案 1 让处方自动变对(不再有type属性可写);方案 2 要专门为这两个组件写特判文案。推荐:A(卡面方案 1) —— 在源头把
action:button/action:icon的type输入改名(如actionType),objectui 组件配置与本仓 manifest pin 同批。①④同向,③被它顺带修好,②的零拉动意味着今天改名没有迁移面。回退:B(卡面方案 2) —— 保留缺口但让它可见:报错文案对这两个组件特判,并在
content/docs/ui/react-pages.mdx写明这两个组件的type只能在 react 写法下设置。若维护者判定「跨仓改一个已发布组件的输入名」代价高于「一条永久特例」,走这条。置信缺口(本分析看不见什么): 看不见 objectui 仓里
action:button.type的真实使用面。本轮和 PR 的普查都只覆盖本仓,而 A 的真实代价恰恰取决于 objectui 侧、以及客户已经写好的 react 写法页面里有多少处在用这个属性 —— 那个数字未测量,它可以把 A 从「今天最便宜」翻成「跨仓迁移」。排期(是否等 #14274 合入)本席也不判,两者不冲突,归 objectui 席位定。