Repository navigation
[Decision] A greyed-out record action can only say 对此记录不可用 — re-ask for an authored disabled reason now that a second app needs it (re-file per #24034 B′) #26619
Description
Ruled: 6442737825 · letter A · 2026-10-27T13:13Z
Filing gate: ② a decision only the maintainer can make — a spec widening that ruling B′ on #24034 closed as not planned (#24018), with the re-file condition it named: "Re-file after GA when a second page needs it, on a measured ask." 18.0.0 went GA on 2026-10-20; the measured ask is below.
⚠️ Drafted on the session's default model because the Fable quota was exhausted (HTTP 429, 2026-10-27 ~10:20Z); the maintainer chose 「现在用当前模型起草」 in the hotclm project thread. Pending Fable re-review before ruling (maintainer rule 2026-10-25 「决策卡应该使用fable」).
Reader who acts: the maintainer rules; on A, domain:spec then the console (ui/).
维护者速读
- 一句话问题: 按钮因为条件不满足被置灰后,用户只能看到「对此记录不可用」,看不出缺的是什么(比如「需要一条通过的法务审查」)。上次([Decision] cloud#2910 (the organisation page): which of four framework widenings to build — an invite action on sys_organization, an action disabled-reason key, an "only owner" predicate root, a record-scoped leave confirm? #24034 B′)因为只有一个使用方没做,说好 GA 后有第二个使用方、有实测需求再提。现在两个条件都满足了。
- 谁撞上: hotclm(合同管理)的法务和档案岗。hotclm#169 要把「送审 / 发起签署 / 生效」在条件不满足时置灰,现在这三个按钮是点了才报错(实测:送审 →
422,提示 "An approved legal review is required before the contract enters approval.")。置灰之后,如果不能写原因,用户只看到「对此记录不可用」。第一个使用方仍是 cloud#2910 的「唯一所有者不能退出组织」。 - 选项:
| 选项 | 做什么 | 用户看到 | 工作量 |
|---|---|---|---|
| A 加原因键(推荐) | ActionSchema 在 disabled 旁加一个可翻译的原因键(I18nLabel),声明即生效;控制台的悬停提示和无障碍描述读它,缺省时仍是「对此记录不可用」;i18n extract 生成翻译键 |
「需要一条通过的法务审查后才能送审」 | spec 1 个 PR + 控制台 1 个 PR。先例:#25643 给流程加 requiredPermissions(spec + 运行时各个入口 + lint),+753 −74,34 个文件。这次只是一个展示用的键,预计明显更小 |
| B 维持 B′ | 不加键 | 「对此记录不可用」;用户要靠猜,或者去问法务 | 0 |
- 业务含义: A 相当于 Salesforce 的按钮置灰时附带说明(Dynamic Actions 的提示);B 是灰按钮不给理由。
- 推荐 A。 有实测需求,而且来自两个不同的应用(cloud、hotclm)。这个键只是展示文案,不参与权限判断,所以扩散面小。
- 请回:
A / B。
选项细节
- A 的契约: 原因键只负责展示,不改变
disabled的求值结果,也不进入服务端判断(动作被拒的依据仍是 hook / 权限);空字符串会被拒绝。翻译位放在objects.<object>._actions.<action>下,跟confirmText/successMessage同一组。 - 为什么不在应用里绕开: hotclm 可以把原因写进按钮的
description,或者在页面上加一个「下一步缺什么」的字段,但这等于在应用里复刻平台的置灰机制;pm-dispatch 应用仓推荐序规定不荐绕行。
四维
- 长远合理性: 「按钮为什么不能用」是所有业务平台的标配(Salesforce、ServiceNow 的 UI Action 都有条件加说明)。两年后平台一定会有这个能力,现在加的是终态形状,不是过渡。
- 业务拉动: 两个真实使用方,都有实测(cloud#2910 的唯一所有者;hotclm#169 的三个流程按钮)。
- 防 AI 犯错: 原因写错时,用户看到一句错的提示,但服务端照样拒绝,不会漏数据。会有人误以为原因键能控制权限吗?契约写明它只用于展示,lint 也不把它当作判断条件。
- 不扩散: 新增一个永久的 spec 键。只用于展示、有翻译位、旁边已有
disabled,维护面小。
只看①选 A;②③④ 是否翻转:否。
os-decision-facets
- ① 项目长远合理性:缩小特例。灰按钮必须给理由是终态形状;没有这个键,每个应用都会各自想办法绕开。
- ② 实际业务拉动:两个使用方都有实测(cloud#2910、hotclm#169),今天就会撞上。
- ③ 防 AI 犯错:原因只用于展示,不参与判断;拒绝仍由服务端响亮地给出,写错原因不会造成静默泄露。
- ④ 创业阶段不扩散:新增一个永久键(I18nLabel),这是代价;它跟
disabled成对、语义闭合。
Prior rulings read: disabledReason, "disabled reason", 24018, 24034 → 2 hits (#24018 not_planned, #24034 Ruled B′ 6228318638); ADR none; thread: none.
裁后执行
- A: reopen spec: ActionSchema declares
disabledbut no authored reason, so a header action cannot say why it is unavailable (e.g. "You are the only owner; transfer ownership first") #24018,或由本卡转成执行卡:domain:spec,p2;控制台跟进卡;hotclm#169 先只用disabled落地,原因键发布后再补一行文案。 - B: 关闭本卡,
not_planned,引用本次裁决;hotclm#169 照常只做置灰。
Duplicate search: objectstack issues, all states, "action disabled predicate reason tooltip why button is disabled" → 2 hits (#24018 closed not_planned, #8990 unrelated).
Activity
objectstack-fleet commentedon Oct 27, 2026
Triage: decision card, intake labels · triage seat ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-27T11:03Z⛔ Not a claim.
priority:p2·domain:spec·area:records·pm:awaiting-maintaineradded. It is a spec widening (an authored disabled reason on a record action), re-filed under #24034 B′'s re-file condition. The card says it is pending a Fable re-review before ruling (it was drafted while the Fable quota was exhausted). That re-review is the filer's to run; triage does not grade the letter.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Brief-claimed-by: session_01KrKXQVvgYk2AJ4VkqMobyb · batch #413 · 2026-10-27T12:28Z
Director seat, summon #51 continuation (ledger objectstack#12708), served on
claude-fable-5-1— this reading is the Fable re-review the card's header says it is pending. Presented to the maintainer in batch #413 as item 1 of 3. Seat increment (re-verified onorigin/mainfe53769a; the card is not restated):leave_organizationdeclaresdisabledover a roll-up; no new spec key) and named the re-file condition.packages/spec/src/ui/action.zod.tscarriesdisabled(:89 carry-over, :859 the CEL shorthand) and theconfirmText/successMessagetemplate slots (:881–882), and no reason key (grep disabledReason packages/spec/src→ 0). The second consumer is real: hotclm#169 greys 送审 / 发起签署 / 生效 on conditions that today surface only as a 422 after the click; the first is cloud#2910 (closed, the sole-owner action). Precedent for the workload column holds: PR feat(automation): flow-level requiredPermissions, checked at every flow start door #25643 (requiredPermissionson flows) +753/−74, 34 files; this key is display-only and smaller.needs-user-decisionandpm:awaiting-maintainertogether; the second leaves in the ruling's stroke.descriptionor a field; ② two apps, both measured; ③ display-only — a wrong reason never changes the server's refusal; ④ one permanent I18nLabel key, paired withdisabled, translation slot besideconfirmText.disabledReason: an I18nLabel besidedisabled, display only, empty string refused, translation key underobjects.<object>._actions.<action>, the console's hover and accessible description read it, 「对此记录不可用」 stays the default). Not B (the re-file condition B′ itself set is met; refusing now makes the condition meaningless). Workload: spec · 1 PR (key + i18n extract + lint not treating it as a predicate) + ui · 1 PR (hover/aria) · S each;Clause-②: yes (widening),Metadata-protocol: added→ p1 under the standing yardstick (6435014712) once queued; theSkills-docs:line owed.Prior rulings read: [Decision] cloud#2910 (the organisation page): which of four framework widenings to build — an invite action on sys_organization, an action disabled-reason key, an "only owner" predicate root, a record-scoped leave confirm? #24034 B′ (6228318638), spec: ActionSchema declares
disabledbut no authored reason, so a header action cannot say why it is unavailable (e.g. "You are the only owner; transfer ownership first") #24018 (not_planned under it), 「协议新增定 p1」; ADR-0032 (template slots); thread: none.objectstack-fleet commentedon Oct 27, 2026
Ruling: batch #413 item 1 · letter A · maintainer 「同意」 2026-10-27T13:10Z
Recorded by the director seat (summon #51 continuation,
session_01KrKXQVvgYk2AJ4VkqMobyb, served onclaude-fable-5-1— the Fable re-review the card's header asked for is the brief 6441887048). The maintainer answered batch #413 with 「同意」 — every seat letter. Letter A:ActionSchemagains a display-only, translatable disabled reason besidedisabled— an I18nLabel key (the spec lane names it; the card's candidate isdisabledReason), empty string refused, translation slot underobjects.<object>._actions.<action>withconfirmText/successMessage, read by the console's hover title and accessible description of a greyed action, 「对此记录不可用」 as the default when absent. It never enters the server's admission (the refusal stays the hook's / the permission's) and the lint does not read it as a predicate. This re-rules #24034 B′ (6228318638) under the re-file condition B′ itself set — GA shipped (18.0.0, 2026-10-20) and a second consumer measured (hotclm#169, beside cloud#2910). Not B.Workload (card column): spec · 1 PR (key, i18n extract, lint, docs) + ui · 1 PR (hover / aria) · S each; precedent PR #25643 (+753/−74, 34 files) is larger.
Clause-②: yes (widening),Metadata-protocol: added— a protocol addition, so p1 under 「协议新增定 p1」 (6435014712); theSkills-docs:line is owed on the spec PR (6431004737). hotclm#169 lands its greying ondisablednow and adds the reason line after the release that carries the key; #24018 stays closed, this card is the execution carrier.Execution:
needs-user-decision→pm:queuein this act (domain:spec,priority:p2→priority:p1,area:records);pm:awaiting-maintainerremoved in the same stroke.Ruling-ref:names this comment.objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 1 · 2026-10-27T14:20Z
Session:
session_01WGufzfhobWEgUUrjz4rrbhAccount:
os-support-aiBranch:
claude/project-thread-yxepsj(card threadsession_01WGufzfHuAwrecfjUDtkwcm. The platform assigns the thread's branch and the thread can push only that one, so the branch is written as measured instead ofclaude/issue-26619-<slug>.)Worktree:
objectstack-issue-26619Domain:
domain:specSeat:
domain:spec#1File surface: two PRs, in order. PR 1 (
Part of #26619):packages/spec/src/ui/action.zod.ts(an optional, display-only I18nLabel disabled reason besidedisabled, the key named by the dev, the empty string refused), the i18n extract slot underobjects.<object>._actions.<action>besideconfirmText/successMessage, the spec tests and generated artifacts, the docs and skills lines owed under #26369 B, and changesets. PR 2 (Fixes #26619, after PR 1 merges): the console readers of a greyed action's hover title and accessible description, as the dev measures them (at leastui/packages/components/src/renderers/layout/containers.tsxdisabledReasonFor,renderers/action/disabled-reason.tsx,plugin-detailrecord-quick-actions.tsxandapp-shellDeclaredActionsBar.tsx), with their tests and a changeset. "Not available for this record" stays the default when the key is absent. The key never enters server admission, and the lint never reads it as a predicate. Stop on a breach and explain it in the report.Container & model:
M,mode:thread,model: TIER_DEFAULT,effort: highClause-②: yes (widening)
Metadata-protocol: added
Responsibility: n/a. A decision card, ruled A (re-rules #24034 B′ under its own re-file condition).
Thread-read: 6442737825
Premise: holds (read at
origin/main1685a06d:ActionSchemacarriesdisabledand no authored reason, and the console'sdisabledReasonForreturns only the generic sentence)Serial constraints cleared: yes. No open PR or claim names
action.zod.tsdisabled.Ruling-ref: 6442737825
objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{ "issue": 26619, "status": "done", "branch": "claude/project-thread-yxepsj", "pr": "https://github.com/objectstack-ai/objectstack/pull/26786", "session": "session_01WGufzfHuAwrecfjUDtkwcm", "premise_still_valid": true, "summary": "PR 1 of 2 (Part of #26619, the spec half). ActionSchema gains `disabledReason`, an optional display-only I18nLabel beside `disabled`: the string arm refuses '' and whitespace through the shared NON_BLANK_STRING, so the published JSON Schema carries the refusal (minLength 1, pattern \\\\S) and the dropped-refinement ledger gains no row; the map arm is InlineLocaleMapSchema unchanged. Name kept as the card's candidate: it pairs with `disabled`, names the content not one of its three render surfaces, and is the console's own word already (useDisabledReason / disabledReasonFor); disabledMessage / disabledTooltip / disabledHint / disabledText rename onto it. The translation slot sits at objects.OBJECT._actions.ACTION and globalActions.ACTION beside confirmText / successMessage, translateAction overlays it, os i18n extract emits it (pushOptional, nothing seeded when absent), the Studio action form gains a row (4 locales), liveness row `planned` until the console PR; never read by the runtime or the lint. Skills-docs line: follow-up owed (skills/** is Tier H and kept out of the spec half). PR 2 (console, Fixes #26619) waits for this one to merge.", "tests": "On aa552ced/1d2b0252 under os-verify-lock: spec test Test Files 733 passed (733), Tests 21985 passed 1 todo; spec typecheck exit 0; lint test Test Files 167 passed, Tests 7423 passed (first run red on validate-translation-references.test.ts coverage table, fixed by adding the disabledReason leaf-copy row); lint typecheck exit 0; cli unit i18n-extract + i18n-coverage Test Files 10 passed, Tests 100 passed; cli typecheck exit 0 after fixing 4 TS18048 in the new test; pnpm check:i18n OK; eslint over the 10 changed files exit 0; spec check:generated 2 of 14 stale then --fix (docs, liveness counts). No ablation: the new behaviour is pinned by new tests, not a changed one.", "gates": "dispatch-gates --commands on c81d8630e (after merging origin/main): 109 families, 106 exit 0, 3 exit 3 PREREQUISITE NOT MET (spec check:skill-examples, check:dual-build-cjs-loads, check:i18n-coverage; no full build) = NOT MEASURED locally. --ran: 109 accounted for, 106 run, 3 NOT-MEASURED. CI check-runs: in_progress at report time.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "6 — mcp__github__issue_read x6 (reads only; no MCP write tool used)", "api_writes": "2 relay strokes — fleet-write pr_create (POST /repos/objectstack-ai/objectstack/pulls + POST /issues/26786/assignees), then pr_request_reviewers (POST /pulls/26786/requested_reviewers) with this os-dev-report comment (POST /issues/26619/comments); git push is not a REST write", "open_questions": [], "out_of_scope_findings": [ "carrier: seat 1 · Skills-docs follow-up owed: skills/objectstack-ui/rules/actions.md, skills/objectstack-ui/evals/views-apps-actions-pages.json (both at 0 token headroom), skills/objectstack-i18n/SKILL.md (_actions leaf list) · noted, not filed", "carrier: none · a blank value inside an inline locale map is not refused (InlineLocaleMapSchema as every I18nLabel takes it; refusing it would need a refinement outside the projection's closed list) · noted in Acceptance notes" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Landed ·
domain:specseat 1 (#6017) · sessionsession_01WGufzfhobWEgUUrjz4rrbh· 2026-10-27T18:09ZPR #26786 (PR 1 of 2 for this card) merged at
13dc413f.13dc413fis an ancestor oforigin/main. On 24 of the 27 paths the PR touched, the merge tree matches the accepted headc9d6d356byte for byte. The other 3 differ only bymain's own hunks:content/docs/references/data/object.mdxandpackages/lint/src/lint-liveness-properties.test.tscarry theexternalIdregrade from chore(ui-merge): clear the objectui leftovers after the ui/ merge (#25753) #26598.packages/spec/src/system/translation.zod.tscarries the 2–3 letter locale subtag from feat(spec)!: a single-word language name (Chinese, English, Japanese) is refused as a locale #26770. None of this PR's 6disabledReasonlines in that file moved.An action can now say why it is greyed:
disabledReasonsits besidedisabledinActionSchema. It is translatable (i18n-extractemits the key, the translation bundle carries the slot, and the reference validator checks it) and documented inactions.mdx. The liveness row staysplannedwithauthorWarnuntil the console reads it.Still open on this card: PR 2 (the console renders the reason,
Fixes #26619) flips the liveness row tolive, dropsauthorWarn/authorHint, and turns the lint block into a silence pin. The card stayspm:dispatcheduntil PR 2 lands. The skills row is #26820.objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{ "issue": 26619, "status": "done", "branch": "claude/project-thread-yxepsj", "pr": "https://github.com/objectstack-ai/objectstack/pull/26869", "session": "session_01WGufzfHuAwrecfjUDtkwcm", "premise_still_valid": true, "summary": "PR 2 of 2 (Fixes #26619, the console half; the spec half #26786 merged at 13dc413f). While an action's DECLARED disabled predicate is TRUE, the record header (inline and overflow, containers.tsx#disabledReasonFor), the record section bar (record-quick-actions.tsx), the declared actions bar (DeclaredActionsBar.tsx) and the four action:* renderers (disabled-reason.tsx#useDisabledReason) show its declared disabledReason as hover title and accessible description (a menu item's second line). 'Not available for this record' stays the default when none or a blank one is declared. Translation: new useObjectLabel.actionDisabledReason (the actionSuffixes pattern, same addresses translateAction and i18n extract use) and useActionTextLocalizer resolve it only when declared: the bundle's sentence, else the authored I18nLabel collapsed to the active language. The action:* renderers read the action as authored and honour the inline map. UIActionSchema.disabledReason is derived from the spec. The liveness row flips to live with the readers as evidence, drops authorWarn/authorHint, and the lint block becomes a silence pin. The describe() texts and the actions page say the console shows it. Changeset: patch on every moved package, Clause-② no, Metadata-protocol none.", "tests": "Under os-verify-lock on f6a479a30/d96cc5bbb: spec test 735/735 files (22041 tests); lint test 168/168 (7432); spec check:liveness OK; check:generated 2 of 14 stale then --fix (docs, liveness counts); check-doc-authoring exit 0; ui type-check of types, i18n, react, components, plugin-detail and app-shell exit 0 after building @object-ui/app-shell^...; ui vitest over packages/types, i18n, react, components, plugin-detail and app-shell/src/views/__tests__: 1442 files passed, 1 failed (useObjectLabel-identity-5564 member count 27 -> 28, a pin that moves with the new resolver, fixed in d96cc5bbb, 5/5); the five new suites plus the -11811/-11839 siblings 93 tests green; eslint over the 16 changed ui files 0 errors. Ablation: each new suite's declared-reason and bundle cases assert text the pre-change readers never produced.", "gates": "Derived dispatch-gates not run for this PR; the per-package suites and type-checks above stand in. Known red on main: ui/packages/core actionKeys.pin.test.ts (SPEC_ACTION_KEYS lacks disabledReason since #26786), owned by seat 1's hotfix #26859; this branch takes it by a main merge once it lands and does not add the row. CI check-runs: queued at report time.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "6 — mcp__github__pull_request_read x3, mcp__github__get_job_logs x2 (CI on #26786), subscribe_pr_activity x1; reads and subscriptions only, no MCP write tool used", "api_writes": "2 relay strokes — fleet-write pr_create (POST /repos/objectstack-ai/objectstack/pulls + POST /issues/26869/assignees), then pr_request_reviewers (POST /pulls/26869/requested_reviewers) with this os-dev-report comment (POST /issues/26619/comments); git push is not a REST write", "open_questions": [], "out_of_scope_findings": [ "carrier: none · the four action:* renderers render `label` and now `disabledReason` as authored (inline locale map only, no bundle lookup); a bundle lookup there needs a key object they do not carry · noted in Acceptance notes" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Landed ·
domain:specseat 1 (#6017) · sessionsession_01WGufzfhobWEgUUrjz4rrbh· 2026-10-27T20:36ZPR #26869 (PR 2 of 2, the console half) merged at
eab4bbaf, which is an ancestor oforigin/main. All 28 paths the PR touched match the accepted head4b5b87d2byte for byte in the merge tree.With both halves in, a greyed action says why it is greyed.
disabledReasonsits besidedisabledon an action, as a string or an inline locale map, and is translatable atobjects.OBJECT._actions.ACTION.disabledReason/globalActions.ACTION.disabledReason.The console shows it wherever an action is greyed: the record header and its overflow menu, record quick actions, the declared actions bar, and the four
action:*renderers. A button carries it as a hover title and an accessible description. A menu item shows it as a second line. An action that declares no reason shows "Not available for this record".The liveness row is
live, and the planned-property warning is gone.The card closed through
Fixes #26619, and this seat removespm:dispatched. The skills row is #26790. Along the way, #26859 restored theactionKeysinventory row that PR 1 left red onmain.