Skip to content

[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

@hotlong

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

Shapes worth weighing (not a decision)

  1. 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.
  2. Keep the gap, make it visible: a component-aware prescription in the refusal ("action:button declares a type input that the html tier cannot carry — use the react tier or …") plus a sentence in content/docs/ui/react-pages.mdx. Smallest change; leaves a declared input that one tier cannot author.
  3. 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:

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. ⚠️ Shape 1 does not land here: renaming the input at source is an 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 type attribute 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 authored type attribute 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

  1. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    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_write body-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

  2. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Correction to my R+97 triage note above: PR #14274 has now merged.

    The R+98 governed-merge audit picked it up — commit 0e68ed25c on origin/main at 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 on main today". 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-tier action:button / action:icon now gets forbidden-attr with the prescription that has no answer for these two components — the card's second bullet is present-tense on main, not prospective.
    • ⛔ Nothing else in the grading changes. The two components still declare a type input (re-verified this round: 57 components, exactly two, control non-empty), the decision is still shape 1 vs shape 2, and needs-user-decision still holds — the direction was never gated on fix(sdui-parser): refuse an authored type attribute 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

  3. os-project-manager commented on Sep 2, 2026

    @os-project-manager
    Collaborator

    Blocked-by: objectstack-ai/objectui#7415

    Maintainer ruling recorded — A: rename the type input of action:button / action:icon at its source; objectui leads (objectui#7415), the manifest pin here follows

    Director 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 type on the only two manifest components that declare one (action:button, action:icon) is renamed (working name actionType) in objectui's component configuration; the pinned sdui.manifest.json in 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 in content/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:ui lane 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 is pm:blocked until #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:p3 retained. Ledger: objectstack director seat post #12708, summon #10.


    Generated by Claude Code

  4. claude commented on Sep 6, 2026

    @claude
    Contributor

    Unlock scan — pm:blocked → pm:queue. Seat domain:devx @ objectstack (#6023), session session_01AhooRxUmvwYwcnQ5LATTB7, 2026-09-06T02:30Z.

    Trigger: half-state patrol H19 row on anchor #9857 (swept 01:58Z) — this card is pm:blocked on objectstack-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 in 5518063113 is in source: packages/components/src/renderers/action/action-button.tsx:302 and packages/components/src/renderers/action/action-icon.tsx:233 both declare an input name: 'actionType'; no non-test input declaration named type remains 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 pinned sdui.manifest.json still declares type on action:button / action:icon and no actionType anywhere (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), regenerate sdui.manifest.json through its script, and confirm the html-tier refusal then needs no component-aware special case. No open PR touches .objectui-sha or sdui.manifest.json (all 34 open PRs' file lists read at 02:27Z).

    Labels: pm:blocked replaced by pm:queue in one write; domain:devx, priority:p3 unchanged.


    Generated by Claude Code

  5. 5 remaining items

  6. claude commented on Sep 6, 2026

    @claude
    Contributor

    ACCEPT (premise falsified, no PR) — pm:dispatched → pm:on-hold, assignee cleared

    domain:devx seat, session session_01AhooRxUmvwYwcnQ5LATTB7, 2026-09-06T02:46Z. Reviewed against GitHub, git and npm, ⛔ not the report's self-description (report: the os-dev-report comment above, this dispatch's subagent).

    The correction, owned by this seat

    My unlock comment 5556347687 said "the pin half here has not landed". Wrong. Verified independently now: .objectui-sha on origin/main @ 1f2a02ba0 is a472b07167a3…, and git merge-base --is-ancestor f0bb9fa3 a472b071 exits 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:233 at the pinned tree declare actionType. 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/components is 17.6.0 (2026-08-24), its dist declares name:type ×2 / name:actionType ×0 (control name:variant ×17), and .changeset/7415-action-type-input-rename.md is still unconsumed upstream ⇒ next release 17.7.0. node scripts/check-sdui-manifest.mjs is 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 delivery
    governed 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 half

    Open questions — answered by the seat (non-escalation class: verification strategy / recording)

    1. 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).
    2. 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)" != 0 exits 0 — a published @object-ui/components newer than 17.6.0 whose dist declares the renamed input on both components, checked by content (the variant grep is the instrument control).
    Restart-touch: .objectui-sha
    Restart-touch: scripts/sdui-manifest.record.json

    Dispatch shape on restart (pre-written): move .objectui-sha with pnpm objectui:bump to an objectui main commit whose packages/core/package.json carries V; node scripts/gen-sdui-manifest-node.mjs --objectui-version V (after pnpm --filter @objectstack/sdui-parser build); confirm the regenerated manifest declares actionType and no type on action:button / action:icon (positive control 57 components / 2 hits, negative control old spelling absent); then the html-tier fixture: actionType= validates, type= still forbidden-attr.


    Generated by Claude Code

  7. hotlong commented on Sep 18, 2026

    @hotlong
    ContributorAuthor

    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) carries actionType on action:button / action:icon where the tracked artefact carries type — 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-hold and the Restart-when: line stand until then.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    Contributor

    Hold released: pm:on-hold → pm:queue. The card's own Restart-when: command exits 0 on @object-ui/components 17.7.0

    Triage 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 version answers 17.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

  9. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 59
    Session: session_01VDtqoecgES7ScQYGbFVDRv
    Branch: claude/issue-14490-action-type-remeasure (cut from origin/main 18c2ddc1ec), only if a change is owed
    Worktree: objectstack-issue-14490
    Domain: domain:devx
    Seat: domain:devx#1
    File surface: the hold release 5985936223.

    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 (pin 9dfaca6543). No open PR touches packages/sdui-parser or sdui.manifest.json.

    Priority rule 3 reading: domain:devx has no open P0/P1. This is a p3 finding whose hold condition was met (@object-ui/components 17.7.0).


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    Contributor

    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:

    • (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.

  11. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    Contributor

    Closed: resolved at the current pin. Seat domain:devx#1, read at 2026-10-05T03:59Z. Measurement: dev report 5987841815. No PR was owed.

    • The premise is gone. At origin/main 18c2ddc1ec (.objectui-sha 9dfaca6543, via chore(objectui): bump the console pin past objectui c096f03 — 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.json declares actionType (enum script|url|modal|flow|api) and no type on both action:button and action:icon. objectui#7436 shipped in @object-ui/components 17.7.0.
    • The html-tier compile, run from @objectstack/sdui-parser source against that manifest:
      • <action:button actionType="url" …/> and <action:icon actionType="modal" …/>: ok, 0 diagnostics, and the node carries actionType. The input is authorable on the html tier.
      • type="…" on either: ok=false, one error forbidden-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". type is 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.ts plus 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:dispatched removed; closed as completed.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions