Skip to content

fix(spec)!: an inline action refuses confirmText beside a non-empty params, as a registered action does - #29829

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

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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

Requested by Yibo Wang · project thread

Fixes #29383
Ruling-ref: 6595808462

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — no skills/** file teaches an inline action carrying confirmText beside 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 generated content/docs/references/** tables that render the confirmText describe are regenerated here.

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

维护者速读

  • 改了什么:页面按钮上内联声明的动作(element:button 的 properties.action)同时写 confirmText 和非空 params 时,现在和已注册动作一样被拒收,提示把这句话挪到 description、删掉 confirmText。
  • 为什么改:您在 spec(ui): an inline action accepts confirmText beside params, the pair ActionSchema refuses as two dialogs; once #29369 gives the inline pick description, decide whether the inline door refuses it too #29383 裁定 A(「同意」)。内联动作和注册动作走同一个执行器,先弹确认框、再弹参数框,一个决定两个对话框;description 已能写在内联动作上,作者有地方挪这句话。
  • 风险与回滚:仓内普查 0 处已发运的内联动作带这对组合,只翻转 spec 自己的一条测试 pin;不带 params 或 params: [] 的 confirmText 照常通过,批量动作不受影响。已存页面照常加载,下次保存才被拒。回滚即 revert 本 PR。
  • 先看:packages/spec/src/ui/action.zod.ts(新的共用判定 declaresConfirmBesideParams 与 CONFIRM_BESIDE_PARAMS_REFUSAL,InlineActionSchema 链上新增的那条 .refine),packages/spec/src/ui/action-confirm-params-guard.test.ts 末尾新的 describe 组,.changeset/29383-inline-action-confirm-beside-params-refused.md。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/ui/action-confirm-params-guard.test.ts,预期 16 个测试全过,其中内联组 5 条(2 条拒收、3 条对照)。
  • 需要你做的:无,席位落地。

What changes

  • One predicate, one refusal, two doors. packages/spec/src/ui/action.zod.ts lifts the ActionSchema pair rule out of its refine body into a module-level predicate, declaresConfirmBesideParams(data) (confirmText != null and params a non-empty array, unchanged), and a module-level refusal, CONFIRM_BESIDE_PARAMS_REFUSAL (the existing message, byte for byte, at path ['confirmText']). ActionSchema's refinement now reads return !declaresConfirmBesideParams(data) with that refusal; InlineActionSchema gains one .refine with the same two, between its target-required rule and its url-target rule. No second copy of either.
  • The comment at the refinement no longer says the inline pair is "a separate narrowing question, not decided here". It states that InlineActionSchema carries the same refusal on its own chain, by ruling record 6595808462 letter A, and why (same runner, so the same two dialogs; its pick carries description, so the remedy has a slot).
  • The confirmText describe dropped its "On a registered action," qualifier ("pairing this with a non-empty params is refused …"), since both doors that render this describe now refuse the pair; a view's bulkActionDefs declares its own confirmText. The NB comment above the field says so. content/docs/references/{data/object,kernel/metadata-plugin,ui/action,ui/component}.mdx are regenerated from it (check:generated --fix, which proved only check:docs stale).
  • Changeset .changeset/29383-inline-action-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 at the inline door, on the census case

The census case is the one inline carrier the census found, the pin in action-confirm-params-guard.test.ts:

InlineActionSchema.safeParse({
  type: 'url', target: '/approvals?reject=1', label: 'Reject',
  confirmText: 'Reject this request?',
  params: [{ name: 'reason', label: 'Reason', type: 'textarea' }],
});
  • Before (origin/main b3d14150): success: true, pinned by the test "InlineActionSchema is out of reach too — it picks fields, not this refine chain".
  • After (this head): success: false, one issue at ['confirmText'], code: 'custom', and its message equals ActionSchema's own issue for the registered pair (asserted with toBe). With description in place of confirmText, with no params, and with params: [], the same action parses.
  • At the authoring door. The page component-props gate (validateComponentProps, @objectstack/lint src against this head's spec dist), on a page whose element:button carries { type: 'api', target: '/api/v1/approvals/reject', params: [reason], confirmText: 'Reject this request?' }: one finding, component-props-invalid, severity error, at pages[0].regions[0].components[0].properties.action.confirmText, message opening "action.confirmText: An action that declares confirmText beside a non-empty params shows the user TWO dialogs for one decision". The remedy body (description, no confirmText), the param-less body and the params: [] body: no finding.
  • defineStack does not judge it, before or after: a component's properties is an open bag that the stack parse does not dispatch (measured: an inline cancelText, an inline url action with no target, and the pair all pass defineStack on this head). The changeset names the props gate as the door, not defineStack.

ADR-0087 disposition

The changeset body carries the ADR-0087 marker comment with the disposition adr-0087: not-required (no-migration-prescription), the form the repo's narrowing changesets use (223 of the .changeset/*.md markers on origin/main are not-required (no-migration-prescription); the closest precedents are 26413-now-anchor-extra-key-refused.md and 26462-diff-missing-version-refused.md). No docs/adr/** path is touched. The reasoning: 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, re-run at the base

At origin/main b3d14150 (report 6580676225 counted at a6441636): every file under examples/, packages/**, ui/**, skills/**, templates, apps/ carrying confirmText (excluding CHANGELOG.md), each confirmText line read with a ±25-line window that holds an inline marker (element:button, InlineAction, properties:, action: {) and a non-empty params: [. 16 windows in 5 files, each read by hand:

  • packages/spec/src/ui/action-confirm-params-guard.test.ts — the inline pin above (the one carrier; flipped here) and ActionSchema / BulkActionDefSchema fixtures.
  • packages/lint/src/validate-action-copy-templates.test.ts L111–137 — registered actions and action:button rows, not inline actions.
  • packages/spec/src/ui/action-cancel-text.test.ts L29, action-copy-template.test.ts L158, action-row-update.test.ts L291–295 — registered actions, a roots helper, and a registered action.

The files changed between a6441636 and b3d14150 that carry both keys are registered-action metadata (platform-objects identity objects, plugin-approvals), generated translation bundles, and ui/spec tests none of which parse an inline action with the pair. Cloud fixtures are not in this repository and were not read. Disposition unchanged: 0 shipped carriers, 1 spec test pin.

Tests

  • action-confirm-params-guard.test.ts: the "out of reach" inline pin is removed from the boundary block (renamed "the pair guard is scoped by SCHEMA BOUNDARY — the bulk def keeps the pair") and replaced by a new block, "confirmText + non-empty params is refused on InlineActionSchema too": the refusal (path, code, message equal to ActionSchema's), the locale-map form refused, and three accept controls: description with no confirmText, confirmText with no params, confirmText with params: []. The file header names the inline door.
Readings (head `65665759`, under os-verify-lock)
  • Targeted: vitest run src/ui/action-confirm-params-guard.test.ts src/ui/inline-action.test.ts src/ui/action-cancel-text.test.ts src/ui/action-row-update.test.ts src/ui/component.test.ts: Test Files 5 passed (5), Tests 459 passed (459).
  • pnpm --filter @objectstack/spec test && pnpm --filter @objectstack/spec typecheck: Test Files 804 passed (804), Tests 23884 passed | 1 todo (23885); VERDICT command-exit 0.
  • Ablation, through scripts/ablation-replace.mjs (anchor hit 1 → 0, blob c1a9cf11 → 2b9084b3): the inline refine's predicate replaced by true || …; action-confirm-params-guard.test.ts → Tests 2 failed | 14 passed (16) (the two refusal tests); restored, blob equals HEAD, git diff HEAD empty. No dist in that path (the test imports ./action.zod).
  • Lint consumers: pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-action-copy-templates.test.ts src/validate-component-props.test.ts src/validate-component-props-url-target.test.ts src/validate-component-props-row-on-success.test.ts (after the dogfood closure build): Test Files 4 passed (4), Tests 207 passed (207); VERDICT command-exit 0. The copy-template rule (validate-action-copy-templates, its INLINE_KEYS) needs no change: it lists copy keys, and this refusal adds none.
  • node ui/scripts/check-action-forward-parity.mjs: exit 0 (element:button owes 13, forwards 18; no spec key moved, so nothing new is owed).

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

Acceptance notes

  • content/docs/protocol/objectui/actions.mdx L444 (hand-written) still says "On a registered action, pairing it with a non-empty params is refused". It stays true but no longer names the inline door; outside this claim's file surface, so left for the docs owner.
  • skills/objectstack-ui/rules/actions.md L22 has the same "a registered action" phrasing. True, and it names the remedy; a one-word widening ("A registered or inline action") would be a skills/** (Tier H, token-ratcheted) edit, kept out of this Tier S PR.
  • The lint copy-template rule's INLINE_KEYS (validate-action-copy-templates.ts) lists copy keys, not pair rules, so it does not move.

Verification

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 102 families on this tree (7 paths vs merge base b3d14150).

102 derived gates: 102 exit 0
  • Serial pass on 65665759: 100 exit 0, 2 exit 3 (built-output prerequisites: spec check:skill-examples needs the client SDK dist, check:dual-build-cjs-loads needs every package's dist). check-plugin-teardown-shape --self-test needed its pinned fixture commit on the shallow clone (git fetch origin 621a4876); fetched before the pass, exit 0.
  • After the full pnpm turbo run build --concurrency=2 (Tasks: 118 successful, 118 total, VERDICT command-exit 0), the 2 were rerun: both exit 0 (283 prose examples type-check across 3 surface(s); the CJS load probe at floors 90/58/520/1).
  • check:generated, check:authorable-surface, check:api-surface, check:docs, check:liveness: exit 0. No authorable-surface, api-surface, liveness or dropped-refinements row moves: the refusal adds no key and no export, and InlineActionSchema already carried a dropped refinement at every position it is reached from.
  • dispatch-gates --ran: "102 derived famil(ies) accounted for — 102 run, 0 NOT-MEASURED".

origin/main gained 19 commits after the base (b0ef25f5 at open); none touches action.zod.ts or the guard test, and two regenerate other lines of content/docs/references/data/object.mdx. A local merge-tree of head and origin/main is clean; not merged here, so the dogfood reading stays pinned to this head.

check-changeset-no-major with this body as the event: exit 0 ("LEVEL AXIS: this PR declares clause-② no (narrowing) and metadata-protocol narrowed, and no package whose published source … it moves is graded patch"). check-closing-keyword-parity: the only closing target is Fixes #29383.

CI families are read from this PR's check-runs.


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzfHVbsfKWji5VLrLbG

… params, as ActionSchema does

An inline page action runs through the registered action's runner, so the
pair opens a confirm dialog and then the param dialog for one decision.
Both doors now share one predicate and one refusal; the remedy is the
inline pick's description.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzfHVbsfKWji5VLrLbG
@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 6 documentable anchor(s).

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via confirmText (literal, a string literal in ActionSchema; a string literal in CONFIRM_BESIDE_PARAMS_REFUSAL; a string literal in actionObject))
  • content/docs/ai/connect-mcp.mdx (via confirmText (literal, a string literal in ActionSchema; a string literal in CONFIRM_BESIDE_PARAMS_REFUSAL; a string literal in actionObject))
  • content/docs/deployment/troubleshooting.mdx (via ActionSchema (symbol, a top-level const))
  • content/docs/frontend/architecture-overview.mdx (via ActionSchema (symbol, a top-level const))
  • content/docs/frontend/layout.mdx (via confirmText (literal, a string literal in ActionSchema; a string literal in CONFIRM_BESIDE_PARAMS_REFUSAL; a string literal in actionObject))
  • content/docs/frontend/troubleshooting.mdx (via ActionSchema (symbol, a top-level const))
  • content/docs/protocol/kernel/i18n-standard.mdx (via confirmText (literal, a string literal in ActionSchema; a string literal in CONFIRM_BESIDE_PARAMS_REFUSAL; a string literal in actionObject))
  • content/docs/protocol/objectui/actions.mdx (via ActionSchema (symbol, a top-level const), confirmText (literal, a string literal in ActionSchema; a string literal in CONFIRM_BESIDE_PARAMS_REFUSAL; a string literal in actionObject))
  • content/docs/protocol/objectui/record-alert.mdx (via confirmText (literal, a string literal in ActionSchema; a string literal in CONFIRM_BESIDE_PARAMS_REFUSAL; a string literal in actionObject))
  • content/docs/ui/actions.mdx (via confirmText (literal, a string literal in ActionSchema; a string literal in CONFIRM_BESIDE_PARAMS_REFUSAL; a string literal in actionObject))
  • content/docs/ui/translations.mdx (via confirmText (literal, a string literal in ActionSchema; a string literal in CONFIRM_BESIDE_PARAMS_REFUSAL; a string literal in actionObject))

⛔ 5 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-1.mdx (via ActionSchema (symbol, a top-level const), confirmText (literal, a string literal in ActionSchema; a string literal in CONFIRM_BESIDE_PARAMS_REFUSAL; a string literal in actionObject))
  • content/docs/releases/v17/17-3.mdx (via ActionSchema (symbol, a top-level const))
  • content/docs/releases/v17/index.mdx (via ActionSchema (symbol, a top-level const))
  • content/docs/releases/v18/18-1.mdx (via confirmText (literal, a string literal in ActionSchema; a string literal in CONFIRM_BESIDE_PARAMS_REFUSAL; a string literal in actionObject))
  • content/docs/releases/v18/18-2.mdx (via ActionSchema (symbol, a top-level const), confirmText (literal, a string literal in ActionSchema; a string literal in CONFIRM_BESIDE_PARAMS_REFUSAL; a string literal in actionObject))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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 b0ef25f524b870433424b8ad797ef3801374f745 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d8d242457431d817d58b445fbe0e63cc2f1f17cd — the merge of head 656657595b803b3c9ee684e5ece3b8a42addc4f9 into base b0ef25f524b870433424b8ad797ef3801374f745, 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 d8d242457431d817d58b445fbe0e63cc2f1f17cd && git checkout d8d242457431d817d58b445fbe0e63cc2f1f17cd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b0ef25f524b870433424b8ad797ef3801374f745 656657595b803b3c9ee684e5ece3b8a42addc4f9 && git checkout -B drift-repro b0ef25f524b870433424b8ad797ef3801374f745 && git merge --no-ff 656657595b803b3c9ee684e5ece3b8a42addc4f9

node scripts/docs-audit/affected-docs.mjs --json b0ef25f524b870433424b8ad797ef3801374f745

⚠️ 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 b0ef25f524b870433424b8ad797ef3801374f745 → 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 6566575 · 2026-11-03T06:34Z

@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: 656657595b803b3c9ee684e5ece3b8a42addc4f9
Local-runs: none
Scope: packages/spec/src/ui/action.zod.ts + packages/spec/src/ui/action-confirm-params-guard.test.ts, .changeset/29383-inline-action-confirm-beside-params-refused.md, content/docs/references/data/object.mdx, content/docs/references/kernel/metadata-plugin.mdx, content/docs/references/ui/action.mdx, content/docs/references/ui/component.mdx, packages/spec/src/ui/component.zod.ts, packages/spec/src/ui/page.zod.ts, packages/lint/src/validate-component-props.ts, packages/lint/src/authoring-rules.ts, packages/cli/src/commands/compile.ts, ui/packages/components/src/renderers/basic/elements.tsx, ui/packages/core/src/actions/ActionRunner.ts, scripts/check-adr-0087-registration.mjs, scripts/check-changeset-no-major.mjs, docs/adr/0087-metadata-protocol-upgrade-contract.md, AGENTS.md

Inputs read: card #29383 body and all nine comments (triage 6580249013, claim 6580616231, measurement report 6580676225, decision card 6580700185, hand-off 6589420791, ruling 6595808462 letter A, seat claims 6595853086 and 6595887076, os-dev-report 6597115604); PR #29829 body, its seven-file list, and the net diff of 65665759 against merge base b3d14150 with origin/main (19 commits ahead); the 42 check-runs on 65665759; and the cited code at that head: action.zod.ts (actionObject, declaresConfirmBesideParams, CONFIRM_BESIDE_PARAMS_REFUSAL, ActionSchema, normalizeInlineAction, InlineActionSchema), the guard test with its base / oneParam / guardIssue helpers, component.zod.ts (ElementButtonPropsSchema.action, ActionButtonPropsSchema, ComponentPropsMap), page.zod.ts:662 (the component properties record) and its door comment at 2264, validate-component-props.ts, authoring-rules.ts:176,471-476,1285-1312, compile.ts:541,577, elements.tsx:345-420, ActionRunner.ts:1650-1740,2011, the ADR-0087 checker (its CATEGORIES, findMigrationPrescription, the no-migration-prescription fact check), the level checker's level-axis docblock, ADR-0087's marker grammar, AGENTS.md § Touched packages/spec, Prime Directive #14 and rule 3 of the finishing list, and the two precedent changesets 26413-now-anchor-extra-key-refused.md and 26462-diff-missing-version-refused.md on origin/main. The face: --paths on the seven files returns hits = packages/spec/src/ui/action.zod.ts (a published schema); 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. InlineActionSchema gains one .refine(...) whose predicate is the negation of declaresConfirmBesideParams(data) and whose issue is CONFIRM_BESIDE_PARAMS_REFUSAL, between its target-required rule and the url-target superRefine. The predicate is: confirmText is neither null nor undefined, and params is an array of length at least one; byte for byte the condition ActionSchema carried inline on main; the refusal object is the ActionSchema message moved to module level, unchanged, with path: ['confirmText']. ActionSchema's refinement now reads return !declaresConfirmBesideParams(data) with the same object. One predicate, one message, two doors: right, and the test pins the two messages equal with toBe.
  2. Nothing else in the accept set moves. No key is added to or removed from the pick; normalizeInlineAction (the preprocess) rewrites only navigation to url and to to target, so it cannot manufacture or hide a confirmText / params pair before the refine. confirmText alone, confirmText beside params: [], and description beside params all still parse (three accept controls in the test). The locale-map form of confirmText is refused beside params because the predicate reads != null, which is the registered door's reading too. BulkActionDefSchema is untouched and its accept pin stays in the renamed boundary block. Right.
  3. The describe change is true on every door that renders it. The confirmText describe on the shared field factory drops "On a registered action,". That describe renders on ActionSchema (object actions in data/object.mdx, kernel/metadata-plugin.mdx, ui/action.mdx) and on InlineActionSchema (ui/action.mdx second table, and the ElementButtonProps.action nested table in ui/component.mdx at L237, which I located under the ## ElementButtonProps heading; the hunk's ActionButtonPropsSchema.parse context line is git's heuristic, not the table). Both doors now refuse the pair, so the unqualified sentence is true at every render site. action:button's own confirmText uses ACTION_ROW_COPY_DESCRIBE.confirmText, a different describe, and is not touched. Right.
  4. The four regenerated reference pages carry only that phrase. I diffed the five removed table rows against the five added rows after substituting "On a registered action, pairing this" with "Pairing this": identical. No other describe or type moved in the regen, which matches the PR's claim that check:generated found only check:docs stale. Right.
  5. The measurement that grounds the ruling holds in the cited code. elements.tsx forwards confirmText as is and an array params as actionParams to execute(...) from useAction(); ActionRunner.execute computes confirmMessage(action) (returns text whenever confirmText is declared, with no params branch) and awaits confirmHandler before it reads actionParams and awaits paramCollectionHandler. Two dialogs, in that order, for an inline action, exactly as 6580676225 read it. Right.
  6. The refinement comment moves from "not decided here" to the ruling. The ActionSchema comment now names spec(ui): an inline action accepts confirmText beside params, the pair ActionSchema refuses as two dialogs; once #29369 gives the inline pick description, decide whether the inline door refuses it too #29383 and record 6595808462 letter A, and the inline refine carries the same reference. Right.
  7. Test shape. The old inline acceptance pin ("out of reach too") is removed from the boundary block and the block is renamed to say what it still pins (the bulk def keeps the pair); a new five-test block pins the refusal (path, code: 'custom', message equal to ActionSchema's and containing "TWO dialogs" and "description"), the locale-map refusal, and the three accept controls. The helpers it uses (base, oneParam, guardIssue) are defined at the file head. Right.

② Semver level

  • .changeset/29383-inline-action-confirm-beside-params-refused.md: '@objectstack/spec': minor, Clause-②: no (narrowing), Metadata-protocol: narrowed (...), the **BREAKING** banner, exactly one adr-0087: marker. The PR body carries the same two declaration lines. The only package the diff moves is @objectstack/spec (the lint, cli and ui files I read are cited, not changed). Under the level checker's level axis (narrowed ⇒ at least one moved package minor+; the (narrowing) arm is the breaking signal and ships minor during the launch window), minor on @objectstack/spec is the level the rule prescribes, and the cross-checks that would contradict (narrowed without a (narrowing) arm; added under no) are not tripped. The Check Changeset run on the head (which runs check-adr-0087-registration.mjs --base and check-changeset-no-major.mjs --base) concluded success. Level and declarations match the diff.
  • Metadata-protocol: narrowed is the right kind: "a value refused ... a requirement tightened" is the AGENTS.md definition, and an author may no longer write confirmText beside a non-empty params on a page button's inline action.
  • No skip-changeset, no major, no second package. Correct.

③ Boundary flags

  1. Dev flag: the ADR-0087 disposition as a marker, no docs/adr/** path. Judged against AGENTS.md rule 3 ("A breaking changeset must also state its ADR-0087 disposition, in writing — exactly one marker in the changeset body"), ADR-0087's marker grammar (not-required (no-migration-prescription) why is one of the seven published forms), the checker's CATEGORIES (it is a member), and the checker's fact check for this category (refused only when the body carries a migration prescription; the Check Changeset run passed, so the detector found none in "The remedy" paragraph). The ruling 6595808462 asks for "an ADR-0087 disposition"; the earlier hand-off's word "entry" is the dev report's guess, and the ruling is what binds. The closest precedent, 26413-now-anchor-extra-key-refused.md on main, is a spec minor with Clause-②: no (narrowing), Metadata-protocol: narrowed and the same not-required (no-migration-prescription) marker, and it touched no docs/adr/** path. A docs/adr/** edit would also be a Tier H governed-surface edit (Prime Directive feat: Comprehensive CRM example demonstrating all ObjectStack protocol features #14) that this Tier S PR could not land on its own. With census 0 shipped carriers, no key added, removed or renamed, and a remedy that needs the author to read the sentence, this is the repo's accepted disposition for the case. Answered: holds.
  2. Dev finding: defineStack does not judge the pair; the door is lint validateComponentProps. The page component's properties is z.record(z.string(), z.unknown()) (page.zod.ts:662), so the stack parse never dispatches a component's props to ComponentPropsMap; page.zod.ts:2264 says in its own words that the bag is judged by @objectstack/lint's validateComponentProps at the CLI doors and the runtime save door. The rule's registration in authoring-rules.ts is tier: 'gating', commands: ALL (validate, build, lint), surfaces: CLI_AND_RUNTIME (cli, runtime-publish), runtimeTypes: ['page'], and the gate dispatches element:button to ElementButtonPropsSchema, whose action is InlineActionSchema.optional(). os compile runs runAuthoringRules('build', ...) (compile.ts:577), so it is the build command the gate names. The claimed door is real on every command and on the runtime page write; the changeset's "a stored page keeps loading; its next save is refused" matches the registration comment ("the gate judges writes, never the read path"). Other doors: defineStack / definePage accept the pair silently, by design of the open bag, for every props defect and not this one; a stored page row is not replayed. Boundary flag, not a must-fix, and the changeset already names it. Answered.
  3. Sibling door observation, outside the ruling. ActionButtonPropsSchema (action:button, a different component) carries its own confirmText and a params: z.unknown() whose array form the renderers forward to the same runner as actionParams (the ui tests under renderers/action/__tests__ say so); it carries no pair rule, so an action:button with confirmText beside an array params opens the same two dialogs and is accepted. Ruling A binds InlineActionSchema only, the PR claims nothing about action:button, and widening the refusal to a third door is a separate narrowing decision. Escalated to the seat as a candidate card, not a finding on this PR.
  4. Deviation: 19 commits behind main, not merged. Of the 19, two touch a PR file: 138dacf6ce (a relatedList describe) and eeb3117a5e (object extension field keys) regenerate content/docs/references/data/object.mdx at L254, L577 and L625; the PR's hunk is at L495. Neither touches action.zod.ts, the guard test, the changeset, or the other three reference pages. The merge queue rebuilds on main, check:docs reruns there, and the hunks are disjoint, so the deviation does not bear on landing. The dev's reason (keeping the dogfood reading pinned to the reviewed head) is sound. Answered: no action.
  5. open_questions on 6597115604: none. The report's two out_of_scope_findings (the hand-written content/docs/protocol/objectui/actions.mdx L444 and skills/objectstack-ui/rules/actions.md L22, both still true but naming only the registered door) are outside the claim surface; the skills line is a Tier H edit. Both stay true sentences, so leaving them is correct for this PR; the seat may file a docs follow-up. Answered.
  6. Check-runs on 65665759 at review time: 42 runs. Concluded success: Auto Label, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected, Governed Surface Queue Guard, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, PR-body guards, Plan Test Core shards, Spec property liveness, filter. skipped: 18.x harness, Dogfood shard matrix, Dogfood Verify CLI, Packed-tarball smoke, Schedule gate, Temporal Conformance, UI Published Artifacts. Still in_progress with no conclusion: Build Core, Build Docs, Build UI Deps, Lint · CI/release/engine gates, Lint · ESLint, Lint · docs and code guards, Lint · repo and tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup and corpus, Live E2E UI, Test Core 1-8, Type Check · consumer gates, debt ledger, source gates, workspace. The check:docs / check:api-surface / check:authorable-surface verdicts therefore sit in a Type Check run not yet concluded; the queue guard reads them before landing. Reported as seen, not waited for. The PR body's local readings (spec 804 files / 23884 tests, lint consumers 207 tests, dogfood 3326 tests, 102 derived gates) are the dev's account and are not adopted here.
  7. Governed surfaces: none of the seven 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.

④ Design judgment

packages/spec/src/ui/action.zod.ts — holds. Judged against the ruling it implements (6595808462 letter A), the published spec on main, and its first consumers (ElementButtonPropsSchema.action through lint validateComponentProps; the element:button renderer through ActionRunner).

  • Does it make the spec's own refusal real at the consumer? Yes. The ruling binds "the same predicate and message family ActionSchema uses ... Issue path confirmText"; the diff uses the same function and the same object, so the two doors cannot drift apart in wording, and the test pins the messages equal. The refusal reaches the author at pages[N].regions[N].components[N].properties.action.confirmText through the gating rule at every CLI command and the runtime page write.
  • Does it take a declared capability away beyond the ruling? No. The only pair refused is confirmText beside a non-empty params array; params on the inline pick is the ActionParam[] definition only, so Array.isArray is the complete reading, and params: [], no params, and the description remedy all parse. The remedy slot exists on the inline pick (description: true, since feat(spec): InlineActionSchema picks description, so an inline action's param dialog can say what it does #29369), and the message names it.
  • Attempts to break it. (a) The preprocess cannot reshape the pair (it only renames navigation / to). (b) Refine order: zod 4 runs every .refine on the chain unless abort is set, so the url-target superRefine still reports on the same body; the test reads the issue by path, not by index. (c) A blank confirmText ('') is != null and refused beside params on both doors alike, consistent with main. (d) The locale-map arm is refused, as on ActionSchema. (e) BulkActionDefSchema is a separate strictObject with its own refinements; nothing in this diff can reach it, and its pin stays green by construction. (f) No export is added or removed (declaresConfirmBesideParams and CONFIRM_BESIDE_PARAMS_REFUSAL are module-private), so check:api-surface has nothing to move; no key is added, so check:authorable-surface has nothing to move.
  • Must-fix: none.
  • Should-fix: none on the hits file. For the seat's follow-up list only (not this PR): the action:button sibling in ③.3, and the two hand-written prose sites in ③.5.

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

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

维护者速读

  • 改了什么:行内动作(InlineActionSchema,页面按钮的 properties.action)同时写 confirmText 和非空 params 时拒收,判定和报错文字与注册动作(ActionSchema)完全同一份;只写 confirmText、或配 params: []、或把问句放进 description,都照常通过。
  • 为什么改:执行 spec(ui): an inline action accepts confirmText beside params, the pair ActionSchema refuses as two dialogs; once #29369 gives the inline pick description, decide whether the inline door refuses it too #29383 裁决 A(记录 6595808462):这对组合在控制台会先弹确认框、再弹参数框,两个对话框问同一件事;注册动作早已拒收,行内动作是漏网的那扇门。
  • 风险与回滚:一处收窄(Clause-②: no (narrowing)),仓库内已发元数据里带这对组合的行内动作为 0,只翻转了 spec 自己的一条测试 pin;作者在 os validate 和运行时页面保存时看到拒收与改法。回滚即撤回本 PR。
  • 先看:packages/spec/src/ui/action.zod.ts 的 declaresConfirmBesideParams 与 InlineActionSchema 的新 refine,再看 action-confirm-params-guard.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/ui/action-confirm-params-guard.test.ts,两扇门的拒收文字相同,三个通过对照全绿。
  • 需要你做的:无,席位落地。

席位意见:同一谓词、同一文案、两扇门,契约复审在收窄档 PASS,全量 dogfood 在本 head 0 失败;同意落地。复审另看到 action:button 的 ActionButtonPropsSchema 也能带这对组合且无拒收,属裁决 A 之外的新问题,已转到裁决讨论线程由你定。

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

  • Shape: one refine on InlineActionSchema reusing ActionSchema's predicate and refusal (moved to module level as declaresConfirmBesideParams / CONFIRM_BESIDE_PARAMS_REFUSAL, unchanged), the action.zod.ts comment moved from "not decided here" to the ruling, the guard test's pin flipped plus three accept controls, four regenerated content/docs/references/** pages, one changeset. 7 files, +146 −42; no governed path.
  • Premise measured: ruling A (6595808462); measurement 6580676225 found two dialogs on the inline path; census at b3d14150 unchanged: 0 shipped carriers, 1 spec pin.
  • Gates (check-runs at this head): 65 total, 57 success, 8 skipped, 0 red; mergeable_state: clean. Dev readings at this head: full dogfood 404 files / 3326 tests, 0 failures; lint consumers 207 green; check-action-forward-parity exit 0; ablation reds both refusal pins.
  • Contract review: record 6597260383, 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 disposition: the changeset's not-required (no-migration-prescription) marker, no docs/adr/** path (precedent 26413-now-anchor-extra-key-refused.md). Skills-docs: none with its reason: accepted.
  • Accepted deviations: the branch is 19 commits behind main, unmerged to keep dogfood pinned to this head; the only overlap is data/object.mdx in disjoint hunks, and the merge queue rebuilds on main.
  • Boundary note: the door an author meets is lint validateComponentProps (os validate, build, runtime page save); defineStack / definePage accept the open properties bag by design. Named in the changeset.
  • Carried: ActionButtonPropsSchema (action:button) accepts confirmText beside array params with no pair rule. Outside ruling A; sent to the maintainer's ruling thread as a candidate decision.
  • Landing: ready + auto-merge.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

The third door found by contract review 6597260383 (action:button, ActionButtonPropsSchema, accepts confirmText beside an array params) is ruled A and filed as #29844 (ruling 6597621758). It ships as its own PR after #29829 lands; #29829 is not widened.

Merged via the queue into main with commit 2c881fb Nov 3, 2026
66 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-glsdvz branch November 3, 2026 07:31
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

1 participant