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

Closed

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. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Sep 2, 2026
  2. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:devxDomain lane — seat card indexed by label:pm:seat
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Sep 2, 2026
  3. 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

  4. 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

  5. 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

  6. added
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Sep 2, 2026
  7. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Sep 6, 2026
  8. 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

  9. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 6, 2026
  10. 5 remaining items

  11. 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

  12. 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

  13. 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

  14. added
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:on-holdDecision made, deliberately deferred — no dispatch, no nag; restart condition in the hold comment
    pm:queueReady for the PM dispatch loop
    on Oct 5, 2026
  15. 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

  16. 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.

  17. 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

Labels

domain:devxDomain lane — seat card indexed by label:pm:seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions