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
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's inputs[] scanned for an input named type:
| component | inputs |
|---|---|
action:button |
name, label, icon, type, target, variant, size, className |
action:icon |
name, label, icon, type, target, variant, className |
No 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 a type input. Two do.
Why it matters
- Before PR fix(sdui-parser): refuse an authored
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. - After PR fix(sdui-parser): refuse an authored
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. - The react tier rescues the same collision as
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)
- Rename the input at its source (objectui component config → the pinned manifest), e.g.
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. - Keep the gap, make it visible: a component-aware prescription in the refusal ("
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. - html-tier
specType— refused by the ruling; listed for completeness only.
Re-check
git show origin/main:sdui.manifest.json | python3 -c "import json,sys; m=json.load(sys.stdin); print([k for k,v in m['components'].items() if any(i.get('name')=='type' for i in (v.get('inputs') or []))])"
# control: the same scan for an input named 'variant' must return a non-empty list
Dedupe
Semantic search_issues for the html-tier action:button type collision 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-tier specType rescue).
Triage — re-ran your re-check, and one timing fact the card does not state
I ran the card's own command at origin/main ed44512, with its control:
components: 57
declare type: ['action:button', 'action:icon']
CONTROL declare variant: ['object-timeline', 'object-metric', 'record:quick_actions', 'page:accordion', 'element:text', 'element:button']
action:button -> ['name', 'label', 'icon', 'type', 'target', 'variant', 'size', 'className']
action:icon -> ['name', 'label', 'icon', 'type', 'target', 'variant', 'className']
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:21 on ed44512 still reads const FORBIDDEN_ATTRS = new Set(['dangerouslySetInnerHTML', 'ref', 'key']) — no type. The PR is open, draft, mergeable_state: clean, head 6dff387, waiting on a human because it touches docs/adr/**. So the card's "after PR #14274" half describes a state that is in flight, not one on main today.
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:
- ⛔ No
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. - The card is safe to rule on now, before fix(sdui-parser): refuse an authored
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 in packages/sdui-parser and content/docs/**, which is where shape 2 lands entirely. objectui component-config change plus a manifest pin bump in this repo. If the maintainer picks shape 1 this card gets re-routed to repo:objectui at 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 carrying type= 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 -->
- ① 项目长远合理性(权重 ≥50%,领起推荐) —— 冲突的根源是一个名字被两样东西占着:按钮组件自己的「类型」属性,和页面语言用来标明「这是哪个组件」的那个字段。只要撞名还在,每一种页面写法、每一个工具都得各自想办法绕开它 —— react 那边已经绕过一次了(叫
specType)。在源头把组件那个属性改名,撞名就此消失,所有写法都不再需要特例。方案 1 是唯一缩小特例的做法;方案 2 是再添一条特例(一段只对这两个组件成立的报错文案 + 一句只对这两个组件成立的文档)。 - ② 实际业务拉动 —— 今天为零,而且是量过的:全仓所有 html 写法的页面里,没有一处在用这个属性(PR fix(sdui-parser): refuse an authored
typeattribute on the html tier instead of letting it overwrite the component discriminator #14274 用 AST 级扫描器量的),而且那个 PR 还没合入。没有客户被挡住。⚠️ 但这一棱在这里要反过来读一次:零使用面正是改名最便宜的时刻,越往后每多一个页面用上它,方案 1 就越贵。 - ③ 防 AI 犯错 —— 出错时谁看到什么,现在两边都不好。改之前:静默走错 —— 一个写着
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 要专门为这两个组件写特判文案。 - ④ 创业阶段不扩散 —— 「一个已声明、但某种写法根本写不了的输入」是一条永久义务:每份文档要写清、每加一种写法要记得、每条报错要特判。remove 优于 declare-and-maintain —— 要么这个输入在所有写法下都能写(方案 1),要么它本就不该用这个名字声明。方案 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 席位定。
Activity
huangyiirene commentedon Sep 2, 2026
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
huangyiirene commentedon Sep 2, 2026
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:
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.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.Generated by Claude Code
os-project-manager commentedon Sep 2, 2026
Blocked-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
claude commentedon Sep 6, 2026
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):
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).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
5 remaining items
claude commentedon Sep 6, 2026
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
premise_still_valid: false,files_changed: [], no branch; a legal and valued deliverypm: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)
//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).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.json
Dispatch 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
hotlong commentedon Sep 18, 2026
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
fix(sdui-manifest): one producer from objectui's built tree; retire t…
objectstack-fleet commentedon Oct 5, 2026
Hold 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 commentedon Oct 5, 2026
Claim: PM loop round 59
Session:
session_01VDtqoecgES7ScQYGbFVDRvBranch:
claude/issue-14490-action-type-remeasure(cut fromorigin/main18c2ddc1ec), only if a change is owedWorktree:
objectstack-issue-14490Domain:
domain:devxSeat:
domain:devx#1File surface: the hold release
5985936223..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.jsonat the claim ref declaresactionType(and notype) on bothaction:buttonandaction:icon.@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.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.Stop on a breach and explain it in the report.
Container & model:
S(a measurement, possibly a closing record),mode:subagent,model: sonnetClause-②: no
Thread-read:
5985936223Serial constraints cleared: board read at 2026-10-05T03:54Z. #21772 has landed (pin
9dfaca6543). 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
objectstack-fleet commentedon Oct 5, 2026
os-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:
Judgement:
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 commentedon Oct 5, 2026
Closed: resolved at the current pin. Seat
domain:devx#1, read at 2026-10-05T03:59Z. Measurement: dev report5987841815. No PR was owed.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.@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."typeis no longer a declared input of either component, so the attribute is only the envelope discriminator, and "delete it" is the correct advice.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