Skip to content

[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

Closed

Description

@objectstack-fleet

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/).

维护者速读

选项 做什么 用户看到 工作量
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.

裁后执行

Duplicate search: objectstack issues, all states, "action disabled predicate reason tooltip why button is disabled" → 2 hits (#24018 closed not_planned, #8990 unrelated).

Activity

  1. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: decision card, intake labels · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-27T11:03Z

    ⛔ Not a claim. priority:p2 · domain:spec · area:records · pm:awaiting-maintainer added. 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

  2. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    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 on origin/main fe53769a; the card is not restated):

  3. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #413 item 1 · letter A · maintainer 「同意」 2026-10-27T13:10Z

    Recorded by the director seat (summon #51 continuation, session_01KrKXQVvgYk2AJ4VkqMobyb, served on claude-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: ActionSchema gains a display-only, translatable disabled reason beside disabled — an I18nLabel key (the spec lane names it; the card's candidate is disabledReason), empty string refused, translation slot under objects.<object>._actions.<action> with confirmText / 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); the Skills-docs: line is owed on the spec PR (6431004737). hotclm#169 lands its greying on disabled now 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:queue in this act (domain:spec, priority:p2 → priority:p1, area:records); pm:awaiting-maintainer removed in the same stroke. Ruling-ref: names this comment.

  4. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-27T14:20Z
    Session: session_01WGufzfhobWEgUUrjz4rrbh
    Account: os-support-ai
    Branch: claude/project-thread-yxepsj (card thread session_01WGufzfHuAwrecfjUDtkwcm. The platform assigns the thread's branch and the thread can push only that one, so the branch is written as measured instead of claude/issue-26619-<slug>.)
    Worktree: objectstack-issue-26619
    Domain: domain:spec
    Seat: domain:spec#1
    File surface: two PRs, in order. PR 1 (Part of #26619): packages/spec/src/ui/action.zod.ts (an optional, display-only I18nLabel disabled reason beside disabled, the key named by the dev, the empty string refused), the i18n extract slot under objects.<object>._actions.<action> beside confirmText / 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 least ui/packages/components/src/renderers/layout/containers.tsx disabledReasonFor, renderers/action/disabled-reason.tsx, plugin-detail record-quick-actions.tsx and app-shell DeclaredActionsBar.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: high
    Clause-②: 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/main 1685a06d: ActionSchema carries disabled and no authored reason, and the console's disabledReasonFor returns only the generic sentence)
    Serial constraints cleared: yes. No open PR or claim names action.zod.ts disabled.
    Ruling-ref: 6442737825

  5. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  6. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-27T18:09Z

    PR #26786 (PR 1 of 2 for this card) merged at 13dc413f. 13dc413f is an ancestor of origin/main. On 24 of the 27 paths the PR touched, the merge tree matches the accepted head c9d6d356 byte for byte. The other 3 differ only by main's own hunks:

    An action can now say why it is greyed: disabledReason sits beside disabled in ActionSchema. It is translatable (i18n-extract emits the key, the translation bundle carries the slot, and the reference validator checks it) and documented in actions.mdx. The liveness row stays planned with authorWarn until the console reads it.

    Still open on this card: PR 2 (the console renders the reason, Fixes #26619) flips the liveness row to live, drops authorWarn/authorHint, and turns the lint block into a silence pin. The card stays pm:dispatched until PR 2 lands. The skills row is #26820.

  7. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  8. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-27T20:36Z

    PR #26869 (PR 2 of 2, the console half) merged at eab4bbaf, which is an ancestor of origin/main. All 28 paths the PR touched match the accepted head 4b5b87d2 byte for byte in the merge tree.

    With both halves in, a greyed action says why it is greyed. disabledReason sits beside disabled on an action, as a string or an inline locale map, and is translatable at objects.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 removes pm:dispatched. The skills row is #26790. Along the way, #26859 restored the actionKeys inventory row that PR 1 left red on main.

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

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:specpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions