Skip to content

fix(spec)!: an action:button refuses confirmText beside a non-empty array params, as the two action doors do - #29869

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-bg56gs
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-bg56gs

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

domain:spec#3 · os-support-ai · session_01WGufziDSrjZDDBZDrXMGAQ

Requested by Yibo Wang · project thread

Fixes #29844
Ruling-ref: 6597621758

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — no skills/** file teaches an action:button carrying confirmText beside an array params. The one rule line about the pair (skills/objectstack-ui/rules/actions.md L22, "A registered action is refused it beside a non-empty params (put the question on description)") stays true and names the remedy this refusal prescribes; the skills/objectui page-builder eval that asks for a confirmText button (evals/page-builder.json id 4) is param-less, so it stays correct. The generated content/docs/references/ui/component.mdx row that renders the button's confirmText describe is regenerated here.

BREAKING accept-set narrowing at ActionButtonPropsSchema (@objectstack/spec), served where a page's action:button properties are judged: the component-props gate (component-props-invalid, @objectstack/lint) at every CLI command and at the runtime page save door.

维护者速读

  • 改了什么:页面上的 action:button 同时写 confirmText 和非空数组 params 时,现在和已注册动作、内联动作一样被拒收,提示把这句话挪到 description、删掉 confirmText。
  • 为什么改:您在 spec(ui): an action:button component accepts confirmText beside an array params, the two-dialog pair ActionSchema and InlineActionSchema refuse #29844 裁定 A(「同意」)。这个按钮把两键都交给同一个执行器,先弹确认框、再弹参数框,一个决定两个对话框;三道门现在用同一条判据、同一句提示。
  • 风险与回滚:仓内普查 0 处已发运的 action:button 带这对组合;不带 params、params: []、对象形 params(静态值,不弹参数框)都照常通过。已存页面照常加载,下次保存才被拒。回滚即 revert 本 PR。
  • 先看:packages/spec/src/ui/component.zod.ts(ActionButtonPropsSchema 链尾新增的 .refine),packages/spec/src/ui/action-confirm-beside-params.ts(判据与提示从 action.zod.ts 原样移入),packages/spec/src/ui/action-confirm-params-guard.test.ts 末尾新的 describe 组。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/ui/action-confirm-params-guard.test.ts,预期 21 个测试全过,其中按钮组 5 条(1 条拒收、4 条对照)。
  • 需要你做的:无,席位落地。

What changes

  • One predicate, one refusal, three doors. declaresConfirmBesideParams(data) (confirmText != null and params a non-empty ARRAY) and CONFIRM_BESIDE_PARAMS_REFUSAL (path ['confirmText']) move, byte for byte, from module level in packages/spec/src/ui/action.zod.ts into a sibling module, packages/spec/src/ui/action-confirm-beside-params.ts, which action.zod.ts and component.zod.ts both import. The move is so component.zod.ts can read them without exporting them from action.zod.ts, which ui/index.ts re-exports with export * and would publish (the action-on-success-executor.ts / action-declarative-update.ts pattern; the new module is not re-exported). ActionSchema and InlineActionSchema read them unchanged.
  • The shared predicate already fits the ruling. Its Array.isArray(params) && params.length > 0 read refuses a non-empty array and leaves an object-form params (an action:button's static values) and params: [] alone, so no narrower predicate was needed.
  • ActionButtonPropsSchema gains one .refine((data) => !declaresConfirmBesideParams(data), CONFIRM_BESIDE_PARAMS_REFUSAL) at the end of its chain. action:icon and the action:group / action:menu member rows are not touched (see Acceptance notes).
  • The button's confirmText describe now appends one sentence: refused beside a non-empty array params, correct with no params, beside params: [] and beside an object params. Only the action:button row's describe changes; the shared ACTION_ROW_COPY_DESCRIBE.confirmText that action:icon and the group/menu members also render is untouched, since those rows do not refuse the pair. content/docs/references/ui/component.mdx is regenerated from it (check:generated --fix, which proved only check:docs stale).
  • Comments that counted two doors now name three: the predicate's docblock (in its new module), the NB comment above ActionSchema's confirmText field, the #7428 refinement comment in ActionSchema's chain, and the guard test's file header.
  • Changeset .changeset/29844-action-button-confirm-beside-params-refused.md: @objectstack/spec minor, both declaration lines, the ADR-0087 disposition marker, the narrowing banner, before / now / unchanged / who, and the remedy.

Before / after on the census case

There is no shipped carrier, so the census case is the card's own body:

ActionButtonPropsSchema.safeParse({
  label: 'Reject', actionType: 'api', target: '/api/v1/approvals/reject',
  confirmText: 'Reject this request?',
  params: [{ name: 'reason', label: 'Reason', type: 'textarea' }],
});
  • Before (origin/main bb2c04e3; component.zod.ts is byte-identical at 06513bc4, the src the probe read): success: true.
  • After (this head): success: false, one issue at ['confirmText'], code: 'custom', and its message equals both ActionSchema's and InlineActionSchema's issue for their pair (asserted with toBe).
  • At the authoring door. validateComponentProps (@objectstack/lint src against this head's spec dist), on a page whose action:button carries the body above: one finding, component-props-invalid, severity error, at pages[0].regions[0].components[0].properties.confirmText, message opening "confirmText: An action that declares confirmText beside a non-empty params shows the user TWO dialogs for". The remedy (description, no confirmText), the param-less button, params: [] and an object params: 0 findings each.

ADR-0087 disposition

The changeset body carries the ADR-0087 marker comment with the disposition adr-0087: not-required (no-migration-prescription), as #29829's changeset does. No docs/adr/** path is touched. No key is added, removed or renamed; the census found 0 shipped carriers; and the one rewrite there could be (move confirmText into description) needs the author to read the sentence, so the refusal names it and nothing converts it.

Census at the base

At origin/main bb2c04e3: every file under examples/, packages/, ui/, skills/, apps/, content/, docs/, templates that names action:button (206 files, CHANGELOG.md excluded), each action:button occurrence read with a 45-line window cut at the next component type, kept when the window holds confirmText and a non-empty params: [. 3 windows, each read by hand:

  • packages/lint/src/validate-action-copy-templates.test.ts L125–126: a test fixture action:button with confirmText beside params: [{ name: 'tier', … }]. It feeds validateStackExpressions (the copy-template rule), which does not parse ActionButtonPropsSchema, so it is unaffected; the lint suite below runs it green.
  • skills/objectui/rules/protocol.md L266: a false positive; the window spans prose about variant: "danger" and a registered action's params example further down.

Cloud fixtures are not in this repository and were not read. Disposition: 0 shipped carriers, 1 test fixture outside this door.

Tests

  • action-confirm-params-guard.test.ts gains the block "confirmText + non-empty ARRAY params is refused on the action:button row too": the refusal (path, code, message equal to ActionSchema's and InlineActionSchema's) and four accept controls: description beside an array params, confirmText with no params, confirmText beside params: [], confirmText beside an object params. The header names the third door.
  • There is no locale-map case for the button, unlike the two action doors: the row's confirmText is a template slot that refuses a locale map for its own reason (invalid_union, measured), so such a test would pass with the refusal removed. The first ablation run found exactly that and the case was dropped.
Readings (under os-verify-lock)
  • Targeted (head d77dce5b): vitest run src/ui/action-confirm-params-guard.test.ts src/ui/inline-action.test.ts src/ui/component.test.ts src/ui/component-action-element-rows-20371.test.ts: Test Files 4 passed (4), Tests 458 passed (458).
  • pnpm --filter @objectstack/spec test && pnpm --filter @objectstack/spec typecheck (head bb8c665b): Test Files 806 passed | 1 skipped (807), Tests 24050 passed | 1 skipped | 1 todo (24052); VERDICT command-exit 0.
  • Ablation, through scripts/ablation-replace.mjs (head bb8c665b; anchor hit 1 → 0, blob c9c80b8e → f1896925): the button refine's predicate replaced by true || …; action-confirm-params-guard.test.ts → Tests 1 failed | 20 passed (21) (the button refusal test); restored, blob equals HEAD, git diff HEAD empty. No dist in that path (the test imports ./component.zod).
  • Lint door probe: validateComponentProps against this head's spec dist, five cases, results above.
  • node ui/scripts/check-action-forward-parity.mjs: exit 0 (action:button owes 31, forwards 28; no key moved).
  • ui files that read ActionButtonPropsSchema: cd ui && vitest run packages/components/src/__tests__/spec-symbol-action-renderer-props-11073.test.ts packages/types/src/__tests__/imported-defaults-8317.test.ts: Test Files 2 passed (2), Tests 278 passed (278). runner-variant.ts and public-blocks.zod.ts read the schema's type, which a .refine does not change.
  • Lint consumers (head bb8c665b, after the dogfood closure build): pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-action-copy-templates.test.ts src/validate-component-props-list-view-22444.test.ts src/validate-component-props-row-on-success.test.ts src/validate-component-props-url-target.test.ts src/validate-component-props.test.ts: Test Files 5 passed (5), Tests 215 passed (215); VERDICT command-exit 0. The copy-template rule needs no change: this refusal adds no copy key.

Full dogfood battery (narrowing arm): on head bb8c665b, one locked command: pnpm --workspace-concurrency=2 --filter '@objectstack/dogfood^...' build && pnpm --filter @objectstack/dogfood test (no OS_TEST_SHARD, no path): Test Files 407 passed | 3 skipped (410), Tests 3359 passed | 28 skipped (3387), 0 failures; VERDICT command-exit 0 (33m58s held).

Acceptance notes

  • What else moved, measured. authorable-surface: no key added or removed, check:authorable-surface exit 0. dropped-refinements.baseline.json: unchanged; ui/ActionButtonProps already lists the root site "" (its superRefines), and the build accepted the new refine at that site with no ledger move. Generated reference pages: content/docs/references/ui/component.mdx (one row). api-surface/: unchanged (no new export). Lint consumers: validate-component-props judges the row through ComponentPropsMap and needed no change.
  • Same family, not in this ruling. action:icon (ActionIconPropsSchema) and the action:group / action:menu member rows declare confirmText and an array-or-object params too, and their renderers forward both to the same runner (action-icon.tsx L155/L199, action-group.tsx L351/L380, action-menu.tsx L274/L312). At the lint door the pair passes on all three (0 findings, measured on this head). Ruling 6597621758 names ActionButtonPropsSchema only, so they are left for the seat.

Verification

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at bb8c665b: 102 derived families. Run serially: 89 exit 0 on the first pass; 13 answered exit 3 (PREREQUISITE NOT MET: a dist older than src, or the teardown self-test's pinned fixture outside the shallow clone), and after the closure build and git fetch --unshallow origin main all 13 reran exit 0. --ran reconciliation: 102 derived famil(ies) accounted for — 102 run, 0 NOT-MEASURED.
  • Main has moved 10 commits since the base; git merge-tree against origin/main is clean, so this head is not merged with it. CI's check-runs on this PR are the verdict for every family CI runs.

Generated by Claude Code

…ty array params, as the action doors do

The predicate and refusal move to ui/action-confirm-beside-params.ts so
component.zod.ts reads the same ones without publishing them.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufziDSrjZDDBZDrXMGAQ
…ion:button pair refusal

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufziDSrjZDDBZDrXMGAQ
…te slot refuses a locale map for its own reason

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufziDSrjZDDBZDrXMGAQ
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ui tests tooling labels Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 7 documentable anchor(s).

16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 4b71a82b76531059ad45ba224899aaadaa219ebc.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 168 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4b71a82b76531059ad45ba224899aaadaa219ebc → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3fb90bc253bd04a93b642aecd72244baec7f523d — the merge of head bb8c665baa276edfac440fcd1630700e19c8e2e8 into base 4b71a82b76531059ad45ba224899aaadaa219ebc, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3fb90bc253bd04a93b642aecd72244baec7f523d && git checkout 3fb90bc253bd04a93b642aecd72244baec7f523d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4b71a82b76531059ad45ba224899aaadaa219ebc bb8c665baa276edfac440fcd1630700e19c8e2e8 && git checkout -B drift-repro 4b71a82b76531059ad45ba224899aaadaa219ebc && git merge --no-ff bb8c665baa276edfac440fcd1630700e19c8e2e8

node scripts/docs-audit/affected-docs.mjs --json 4b71a82b76531059ad45ba224899aaadaa219ebc

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4b71a82b76531059ad45ba224899aaadaa219ebc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head bb8c665 · 2026-11-03T08:53Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: bb8c665baa276edfac440fcd1630700e19c8e2e8
Local-runs: none
Scope: packages/spec/src/ui/action-confirm-beside-params.ts, packages/spec/src/ui/action.zod.ts, packages/spec/src/ui/component.zod.ts + packages/spec/src/ui/action-confirm-params-guard.test.ts, .changeset/29844-action-button-confirm-beside-params-refused.md, content/docs/references/ui/component.mdx, packages/spec/src/ui/index.ts, packages/spec/api-surface/ui.json, packages/spec/dropped-refinements.baseline.json, packages/spec/package.json, packages/lint/src/validate-component-props.ts, packages/lint/src/validate-action-copy-templates.test.ts, ui/packages/components/src/renderers/action/action-button.tsx, ui/packages/components/src/renderers/action/action-icon.tsx, ui/packages/components/src/renderers/action/action-group.tsx, ui/packages/components/src/renderers/action/action-menu.tsx, ui/packages/core/src/actions/ActionRunner.ts, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs, .github/workflows/pr-automation.yml, .github/workflows/lint.yml, skills/objectstack-ui/rules/actions.md, AGENTS.md

Inputs read: card #29844 body and all four comments (ruling 6597621758 letter A, the seat's claims 6597681847 and 6598028624, the os-dev-report 6599345445); PR #29869 body, its six-file list, and the net diff of bb8c665b against merge base bb2c04e3 with origin/main (git diff bb2c04e3 bb8c665b); the check-runs on bb8c665b, read twice (42, then 63 runs as the UI matrix fanned out); the cited code at that head: component.zod.ts (ActionButtonPropsSchema L3948-4010, ActionIconPropsSchema L4055-4120, actionContainerMemberShape L4210-4262, ACTION_ROW_COPY_DESCRIBE L3435, ComponentPropsMap L8694-8696), action.zod.ts (the import at L58, the ActionSchema refine at L2305-2313, the InlineActionSchema refine at L2621-2629), the new action-confirm-beside-params.ts whole, the guard test's new block, ui/index.ts (no re-export of the new module, nor of its three sibling internal modules), api-surface/ui.json (neither name present; git diff --stat of api-surface/ base..head empty), dropped-refinements.baseline.json (ui/ActionButtonProps sites "" and onSuccess, unchanged), validate-component-props.ts L146-198 and L395-445 (ComponentPropsMap dispatch, safeParse, issue path joined under properties), validate-action-copy-templates.test.ts L125-131, the four action renderers' paramsPayload and confirmText forwards (action-button.tsx L256-258/L333, action-icon.tsx L154-156/L199, action-group.tsx L350-352/L380, action-menu.tsx L273-275/L312), ActionRunner.ts L1657-1680 (confirm awaited, then actionParams read and the param dialog opened), check-changeset-no-major.mjs header and level axis, check-adr-0087-registration.mjs (CATEGORIES L554-557, findMigrationPrescription L1900, the signal readers L710-718), pr-automation.yml L1001-1002 and L1152 (the Check Changeset job runs both checkers), lint.yml (which Type Check lane runs check:docs / check:authorable-surface and which runs check:api-surface), skills/objectstack-ui/rules/actions.md L22, AGENTS.md § Touched packages/spec, Prime Directive #14, Post-Task Checklist rule 3, and the precedent review 6597260383 on PR #29829. A read-only census of the head tree: every file under examples/, packages/, ui/, skills/, apps/, content/, docs/, templates naming action:button (CHANGELOG excluded), 45-line window after each occurrence, kept when it holds confirmText and params: [. The face: --paths on the six files returns hits = action-confirm-beside-params.ts, action.zod.ts, component.zod.ts (published schema sources); the PR body and changeset both carry Clause-②: no (narrowing), so the narrowing face applies and the tier is CONTRACT_REVIEW_TIER either way.

① Derived judgments

  1. The accept-set change the diff implies is exactly one refusal, on one door, for one shape. ActionButtonPropsSchema gains one .refine(...) at the end of its chain, after its four superRefines, whose predicate is the negation of declaresConfirmBesideParams(data) and whose issue is CONFIRM_BESIDE_PARAMS_REFUSAL. The predicate reads: confirmText is neither null nor undefined, and params is an array whose length is at least one. On this row params is z.unknown(), so the three shapes the ruling distinguishes resolve as: no params (undefined, not an array) accepted; params: [] (length 0) accepted; object-form params (not an array) accepted; an array of length at least one beside a declared confirmText refused at path ['confirmText'], code: 'custom'. That is the ruling's scope exactly, with no narrower predicate needed. Right.
  2. Same message, same path, as the other two doors. The refusal object is the one ActionSchema and InlineActionSchema read; the test pins the button issue's message equal to both with toBe. One predicate, one wording, three doors. Right.
  3. ActionSchema and InlineActionSchema behave exactly as on main. The non-comment code diff of action.zod.ts is one added import line and the deletion of the module-private function and object; both refine call sites (ActionSchema L2312-2313, InlineActionSchema L2627-2628) are untouched and now bind the same names from ./action-confirm-beside-params. The function body and the message string in the new module are byte-identical to the deleted ones (compared in the diff). The new module imports nothing, so no cycle is introduced. Right.
  4. The module stays internal. No index.ts under packages/spec/src names action-confirm-beside-params; ui/index.ts re-exports action.zod with export *, which is exactly why the names could not be exported from there without publishing them. The three sibling internal modules the PR cites as the pattern (action-on-success-executor.ts, action-declarative-update.ts, list-view-export-options.ts) are likewise absent from every barrel and from api-surface/ui.json, as are declaresConfirmBesideParams and CONFIRM_BESIDE_PARAMS_REFUSAL. git diff --stat bb2c04e3 bb8c665b -- packages/spec/api-surface is empty. The npm tarball's files carries src/**/*.zod.ts, which the new .ts module is not in; it ships compiled inside dist through the importers, as its three siblings do. The public surface does not move. Right.
  5. The deviation is the right placement. The card wrote "reuse CONFIRM_BESIDE_PARAMS_REFUSAL / declaresConfirmBesideParams from action.zod.ts after fix(spec)!: an inline action refuses confirmText beside a non-empty params, as a registered action does #29829, or its equivalent". Exporting them from action.zod.ts would have published two helpers as package API through the export * barrel, moved api-surface/ui.json, and made the refusal's wording a public contract for no consumer; an internal sibling module reaches the same reuse with none of that cost and follows a pattern already in the tree. A deviation in placement, declared in the dev report and the PR body, not in behaviour. Right.
  6. The describe change is true where it renders, and only there. The button row's own confirmText describe appends the refusal sentence; ACTION_ROW_COPY_DESCRIBE.confirmText, shared with action:icon and the group/menu members, is untouched, which is correct because those rows do not refuse the pair. ActionButtonPropsSchema renders on one reference page (content/docs/references/ui/component.mdx); the regenerated row carries the new sentence verbatim and nothing else moved in that file. Type Check · source gates, the lane that runs check:docs and check:authorable-surface, concluded success. Right.
  7. No authorable key moves; no ledger moves. No key is added, removed or renamed; authorable-surface/ is untouched. dropped-refinements.baseline.json already lists the root site "" for ui/ActionButtonProps, so a further root-level .refine needs no ledger entry, and the file is unchanged in the diff. Right.
  8. The measurement grounding the ruling holds in the cited code. action-button.tsx L256-258 forwards an array params as actionParams and an object params only as static params (read through readStaticParamValues), and L333 forwards confirmText; ActionRunner.execute L1657-1663 awaits the confirm, then L1675-1680 reads actionParams (or an array params) and opens the param dialog when its length is positive. Two dialogs, in that order, for exactly the shape the predicate refuses; an object params reaches the runner as values and opens none. Right.
  9. The door is real. validate-component-props.ts dispatches each page component's properties to ComponentPropsMap[type].safeParse (L198, L427) and reports each issue at ${path}.properties.${issue.path} under component-props-invalid, so the refusal lands at pages[N].regions[N].components[N].properties.confirmText on every CLI command and the runtime page save. Right.
  10. Test shape. Five tests: the refusal (path, code, message equal to both other doors) and four accept controls (description beside an array, no params, params: [], object params). The dev dropped a locale-map case after its first ablation showed it vacuous (the row's template slot refuses a locale map for its own reason), which is the honest shape. Right.

② Semver level

  • .changeset/29844-action-button-confirm-beside-params-refused.md: '@objectstack/spec': minor, Clause-②: no (narrowing), Metadata-protocol: narrowed (...), the **BREAKING** banner, the fix(spec)!: summary, exactly one adr-0087: not-required (no-migration-prescription) ... marker. The PR body carries the same two declaration lines. The only package the diff moves is @objectstack/spec. Under the level checker's level axis (narrowed ⇒ at least one moved package minor or above; the (narrowing) arm beside narrowed is the required pairing; breaking ships minor during the launch window), minor on @objectstack/spec is the level the rule prescribes. The Check Changeset run on the head, which runs check-adr-0087-registration.mjs --base and check-changeset-no-major.mjs --base, concluded success, so the marker parsed, the category is in CATEGORIES, and findMigrationPrescription found no prescription in the body's "The remedy" paragraph.
  • Metadata-protocol: narrowed is the right kind: "a value refused ... a requirement tightened" is AGENTS.md's definition, and a page action:button may no longer carry confirmText beside a non-empty array params.
  • ADR-0087 disposition not-required (no-migration-prescription): no key added, removed or renamed; the census (mine, on the head tree, agrees with the dev's at the base) finds 0 shipped carriers and one lint test fixture (validate-action-copy-templates.test.ts L131) that feeds the copy-template rule, which never parses ActionButtonPropsSchema; the one rewrite there could be needs the author to read the sentence. The sibling precedent fix(spec)!: an inline action refuses confirmText beside a non-empty params, as a registered action does #29829 carries the same marker for the same case. No docs/adr/** path is touched, which is also the only Tier S shape this PR could land in. Correct.

③ Boundary flags

  1. Dev deviation: predicate and refusal relocated to an internal module instead of exported from action.zod.ts. Judged in ① 4-5: the module is not re-exported from any barrel, api-surface/ is unchanged, both action doors are byte-for-byte the same behaviour, and the placement follows three siblings already in the tree. Answered: holds, and it is the better placement.
  2. Dev finding, same family outside the ruling: action:icon and the action:group / action:menu members accept the pair. Confirmed true in the cited code. ActionIconPropsSchema (L4093) and actionContainerMemberShape (L4243) both declare confirmText through actionRowCopy and params: z.unknown(), and their chains end at refuseRowOnSuccessBesideExecutor with no pair rule. Their renderers forward exactly as the button does: action-icon.tsx L154-156 (Array.isArray(schema.params) ? { actionParams: schema.params, params: staticValues } : ...) and L199 (confirmText); action-group.tsx L350-352 and L380; action-menu.tsx L273-275 and L312; all three hand the result to the same ActionRunner, so the same two dialogs open. On a group/menu member a non-array params is already refused on every non-api type, so the only pair left there is precisely the two-dialog one. Ruling 6597621758 names ActionButtonPropsSchema only, and the PR claims nothing about these rows (the shared describe is deliberately unchanged, the changeset says so). Escalated to the seat as a candidate decision card (one A/B question covering the three rows), not a must-fix on this PR.
  3. Dev deviations "harness assignee/reviewer step skipped" and "commit trailers model-free". Both are standing rules (os-dev rule 2; AGENTS.md's model-free trailer pair), not deviations from this card. Answered: no action.
  4. open_questions on 6599345445: none. Answered.
  5. The branch is behind main, merge-tree clean, not merged. At review time 14 commits behind (the dev reported 10; four more landed since). Of the 14, none touches any of the six PR paths; under packages/spec only api-surface/data.json and api-surface/system.json moved, neither in this diff. A local merge-tree --write-tree against origin/main writes a tree with no conflict; GitHub reports mergeable: true (mergeable_state: blocked, the draft and the running checks). The queue rebuilds on main and reruns the spec gates there. AGENTS.md § Multi-agent 10 asks for a main merge only on a conflict, a main-caused red or a queue removal, so not merging is the rule, not a deviation. Answered: does not matter.
  6. Check-runs on bb8c665b at review time (63 runs on the second read). Concluded success: Auto Label, Build Docs, Build UI Deps, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint · CI/release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, PR-body guards, Plan Test Core shards, Spec property liveness, Type Check · source gates (carries check:docs and check:authorable-surface), filter. skipped: 18.x upgrade harness, Dogfood shard matrix, Dogfood Verify CLI, Lint UI, Packed-tarball smoke, Schedule gate, Temporal Conformance, UI Published Artifacts. Still in_progress with no conclusion: Build Core, Build & E2E UI, Live E2E UI, Test Core 1-8, Test UI 1-16, Type Check UI, Type Check · consumer gates (carries check:api-surface), Type Check · debt ledger, Type Check · workspace. Reported as seen, not waited for; the queue guard reads the required set before landing. The PR body's local readings (spec 806 files / 24050 tests, lint 215, ui 278, dogfood 3359, 102 derived gate families) are the dev's account and are not adopted here.
  7. Governed surfaces: none of the six paths is under docs/adr/**, skills/**, .claude/**, AGENTS.md, CLAUDE.md or .github/workflows/**; the Governed Surface Queue Guard concluded success. Tier S landing on this record.
  8. Prose that names fewer doors than now refuse. skills/objectstack-ui/rules/actions.md L22 ("A registered action is refused it beside a non-empty params") stays a true sentence and names the remedy; it is a Tier H path and outside this PR. The seat may fold it into the follow-up in 2. Answered.

④ Design judgment

packages/spec/src/ui/action-confirm-beside-params.ts — holds. Judged against the two doors it was lifted from on main and the third it now serves. The function body and the message are byte-identical to the module-private originals; the module has no imports, so it cannot cycle with action.zod.ts or component.zod.ts; it is exported from no barrel and absent from api-surface/. Its docblock names the three doors, the runner order it rests on, and why an empty or object params is not the pair. Attempts to break it: (a) a consumer importing it by deep path is not possible through package.json exports, which list only the subpath barrels; (b) the files glob src/**/*.zod.ts excludes it from the shipped source, as it excludes its three siblings, and the compiled dist carries it through its importers. Must-fix: none. Should-fix: none.

packages/spec/src/ui/action.zod.ts — holds. Judged against the published spec on main: the diff is one import and the deletion of the two module-private definitions, plus comment text; both refine call sites are unchanged and bind the same values. ActionSchema and InlineActionSchema accept and refuse exactly what they did on main, and the existing sixteen guard-test cases on those doors still pin that. No export added or removed. Must-fix: none. Should-fix: none.

packages/spec/src/ui/component.zod.ts — holds. Judged against ruling 6597621758 letter A and its first consumers (lint validateComponentProps at the CLI and runtime page doors; the action:button renderer through ActionRunner).

  • Does it make the spec's own refusal real at the consumer? Yes. The gate parses the row through ComponentPropsMap, and the issue lands at properties.confirmText with the action doors' message naming the description remedy, which the row declares and the renderer forwards to the param dialog.
  • Does it take a declared capability away beyond the ruling? No. With params: z.unknown(), the Array.isArray test joined with a positive-length test is the complete reading of "non-empty array": an object (static values, no dialog), an empty array and an absent params all parse, as the four controls pin. A blank confirmText ('') is != null and is refused beside an array, which is how both other doors read it on main; a locale-map confirmText is refused by the row's template slot before the predicate matters.
  • Attempts to break it. (a) The strictObject alias layer (ACTION_NODE_ALIASES) renames endpoint and friends, never params or confirmText, so the predicate sees the authored pair. (b) Zod 4 runs every .refine on the chain, so the four superRefines still report on the same body; the test reads the issue by path, not index. (c) The refine adds no key, so check:authorable-surface has nothing to move (its lane concluded success); it changes the schema's type not at all, so ActionButtonProps / ActionButtonPropsParsed and their ui readers (runner-variant.ts, public-blocks.zod.ts) are unaffected. (d) action:bar spreads a registered action onto an action:button node; that member is an ActionSchema, which already refuses the pair, so no previously-valid composition is newly refused there. (e) The describe change touches only this row's confirmText, so the shared describe on action:icon and the members stays true for rows that do not refuse the pair.
  • Must-fix: none.
  • Should-fix: none on the hits files. For the seat's follow-up list only: the action:icon / action:group / action:menu pair in ③ 2, and the skills prose line in ③ 8.

Implemented-by: claude/project-thread-bg56gs
Reviewed-by: session_01WGufzfgvGexKn9vLHsy89K

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

维护者速读

席位意见:裁决范围逐条落地,收窄档契约复审 PASS,全量 dogfood 在本 head 0 失败;同意落地。复审确认 action:icon 和 action:group / action:menu 的成员行也能带这对组合,已作为新待裁项转到裁决讨论线程。

席位核对 — ACCEPT · domain:spec#3 · head bb8c665baa276edfac440fcd1630700e19c8e2e8

  • Shape: one refine on ActionButtonPropsSchema refusing confirmText beside a non-empty array params; the shared predicate and refusal moved byte for byte into the internal module ui/action-confirm-beside-params.ts (no barrel re-export; ActionSchema and InlineActionSchema call sites unchanged); the guard test gains the refusal pin and four accept controls; one regenerated reference page; one changeset. 6 files, +169 −34; no governed path.
  • Premise measured: ruling A (6597621758); census at this head: 0 shipped action:button carriers with both keys.
  • Gates (check-runs at this head): 65 total, 57 success, 8 skipped, 0 red; mergeable_state: clean. Dev readings at this head: full dogfood 407 files / 3359 tests, 0 failures; lint suite 215/215; ablation reds the refusal pin.
  • Contract review: record 6599507683, Served-tier: CONTRACT_REVIEW_TIER, face narrowing, this head, VERDICT PASS, no must-fix, no should-fix.
  • Declarations: Clause-②: no (narrowing), Metadata-protocol: narrowed, @objectstack/spec minor with the ! banner; ADR-0087 not-required (no-migration-prescription) marker. api-surface/ does not move.
  • Accepted deviation: the shared refusal lives in a new internal module rather than as an export of action.zod.ts, which keeps api-surface/ unchanged; the review judged it right.
  • Carried: action:icon (ActionIconPropsSchema) and the action:group / action:menu member row forward an array params and confirmText to the same runner with no pair rule. That is outside ruling 6597621758, so it goes to the maintainer as a candidate decision.
  • Landing: ready + auto-merge.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 3, 2026 09:24
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 3, 2026 09:24
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 2bf71ce Nov 3, 2026
66 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-bg56gs branch November 3, 2026 09:52
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Cross-reference: the two doors that the PR #29869 review (record 6599507683) found outside this card's ruling, action:icon and the action:group / action:menu member rows, were ruled A by the maintainer and filed as #30017 (ruling comment 6624208841). They are refused with the same predicate and scope limits as here.

Posted 2026-11-04T06:26Z.

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

Labels

documentation Improvements or additions to documentation protocol:ui size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(ui): an action:button component accepts confirmText beside an array params, the two-dialog pair ActionSchema and InlineActionSchema refuse

1 participant