Repository navigation
Commit 13dc413
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBAQyBVaDQuUj1ZDR9GVydAG)_
Part of #26619 — the spec half. The console half (the greyed action's
hover title and accessible description read the key) is the follow-up
PR, which closes the card.
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: #26790
## 维护者速读
- **改了什么:** 动作元数据在 `disabled` 旁新增一个可选键
`disabledReason`:按钮置灰时说明原因的那句话(可翻译,字符串或内联多语言映射)。翻译位在
`objects.OBJECT._actions.ACTION.disabledReason`,`translateAction`
会套用译文,`os i18n extract` 会生成这个键。空字符串和纯空白被拒绝。Studio 动作表单加了一行,四种语言的表单文案已补。
- **为什么改:** 裁决 A(6442737825)。现在置灰的按钮只能显示「对此记录不可用」,hotclm 的送审 / 发起签署 /
生效需要说清缺什么。这是规格的一半,控制台读取在下一个 PR。
- **风险与回滚:** 只加可选键,不写这个键的动作解析和显示都不变。它只用于展示:不参与服务端准入,lint 也不把它当判断条件读。控制台
PR 落地前没有渲染方读它(存活账本记为 planned,作者会收到 planned 提示)。回滚就是 revert 本 PR。
- **先看:**
`packages/spec/src/ui/action.zod.ts`(新键与空白拒绝),`packages/spec/src/system/i18n-resolver.ts`(`translateAction`
套用译文),`packages/cli/src/utils/i18n-extract.ts`(生成翻译键)。
- **两分钟验证:** `pnpm --filter @objectstack/spec exec vitest run
src/ui/action-disabled-reason.test.ts`,预期 16 个用例全部通过,其中一条断言像 CEL
的字符串原样保留、不会被当成表达式。
- **需要你做的:** 无,席位落地。
## What changes
**`ActionSchema.disabledReason`**
(`packages/spec/src/ui/action.zod.ts`), an optional key beside
`disabled`. It is the sentence a greyed action shows to say why it is
unavailable. An action that does not declare it parses exactly as
before.
- **The name.** `disabledReason`, the card's candidate, kept for three
reasons. It pairs with the key it explains (`disabled` →
`disabledReason`), the way an author reads the two together. It names
the content (why), not one place it lands: the console shows it as a
button's hover title, as its accessible description, and as a menu
item's second line, so `disabledTooltip` would name one of three. And it
is the console's own word already (`useDisabledReason`,
`disabledReasonFor`, `DisabledReason` in
`renderers/action/disabled-reason.tsx`), so spec and renderer speak one
vocabulary. Rejected: `disabledMessage` (reads as the `successMessage` /
`errorMessage` family, copy shown after a run), `disabledTooltip`,
`disabledHint`. All four, with `disabledText`, are renamed onto the key,
on the action and in a translation bundle.
- **The shape.** An I18nLabel:
`z.union([z.string().refine(NON_BLANK_STRING), InlineLocaleMapSchema])`.
The string arm refuses `''` and whitespace-only, with a message that
names the default it would replace and says to omit the key instead. The
refinement is the shared `NON_BLANK_STRING`, which
`refinement-projection.ts` declares, so the published
`json-schema/ui/Action.json` carries it (`minLength: 1`, `pattern:
"\\S"`) and the dropped-refinement ledger gains no row. A first cut with
a `superRefine` added 12 ledger rows; this one adds none. The map arm is
`InlineLocaleMapSchema` unchanged, the I18nLabel every other copy key
takes.
- **Display only.** It never enters server admission: nothing in the
runtime reads it, and a disabled action that is called anyway is refused
by its own rules (its body or hook, `requiredPermissions`). It is not an
expression: a predicate-looking value comes out of the parse as the same
string, where `disabled` is lowered to its CEL envelope. `os validate`
reads `disabled` and not `disabledReason`, and the lint's literal-member
census (`validate-expressions.test.ts`, receiver `action`) stays
`['disabled', 'name', 'objectName', 'visible']`.
- **Translation slot.** `actionTranslationSchema`
(`packages/spec/src/system/translation.zod.ts`) declares
`disabledReason` at both addresses, `objects.OBJECT._actions.ACTION` and
`globalActions.ACTION`, beside `confirmText` / `successMessage`.
`translateAction` (`i18n-resolver.ts`) overlays it the way it overlays
`description`: the bundle's sentence for the locale chain, else the
authored value. The address lists in both files' docblocks name it.
- **Extract.** `collectExpectedEntries`
(`packages/cli/src/utils/i18n-extract.ts`) emits the key for a top-level
object action, a global action and an action declared inline on an
object. It is `pushOptional`, like `description`: an action that gives
no reason seeds nothing and is not a coverage gap, because the console
has its own default sentence.
- **Studio.** `action.form.ts` gains a `disabledReason` row under
`disabled` in the Placement section (the plain row `errorMessage` uses,
same I18nLabel node). The four `metadata-forms` bundles in
`@objectstack/platform-objects` carry its label and help (en, zh-CN,
ja-JP, es-ES).
- **Liveness.** `liveness/action.json` row `disabledReason`: `planned`
with `authorWarn` and an `authorHint`, so `os validate` warns an author
who declares it that nothing shows it yet. The framework read points are
the evidence, and the four console readers are named as the follow-up
that flips it to `live` and drops the warning.
- **Docs.** `content/docs/protocol/objectui/actions.mdx` gains the key
in the interface block and a "Saying why: `disabledReason`" section;
`content/docs/ui/translations.mdx` names disabled reasons on the
`_actions` row.
- **Generated.** `authorable-surface/ui.json`
(`ui/Action:disabledReason`), `liveness/state-counts/action.md`, and
four `content/docs/references/**` pages, by `check:generated --fix`.
- **Changeset.** `.changeset/26619-action-disabled-reason.md`:
`@objectstack/spec`, `@objectstack/cli` and
`@objectstack/platform-objects` at `minor` (AGENTS.md step 3: a kind
other than `none` takes at least `minor` on a package the diff moves).
`@objectstack/lint` changes a comment and a test only, so it publishes
nothing.
## Tests
- `packages/spec/src/ui/action-disabled-reason.test.ts` (16): the parse
(string, locale map, optional), the not-a-condition pin, the I18nLabel
refusals shared with `label`, the blank refusal (`''`, whitespace: path
and message), the four near-miss renames, the inline action still
refusing the key, the translation slot at both addresses, the
bundle-side rename, and `translateAction` (translated, global address,
fallback to authored, no key added).
- `packages/cli/test/i18n-extract-action-disabled-reason.test.ts`: the
three emit sites, nothing seeded for an action without a reason, the
declared text carried into the bundle and gated until translated, and
one fixture through schema → extract → translation schema.
- `packages/lint/src/validate-expressions.test.ts`: a bare-field source
that is an error on `disabled` produces no finding on `disabledReason`.
- `packages/lint/src/lint-liveness-properties.test.ts`: the shipped
ledger warns on `disabledReason` at the action, with the row's hint; an
action without it draws nothing. Two pins move with the new key:
`packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts`
(the metadata-form catalog, 688 → 689 after the `main` merge; main's
ADR-0142 `queue` / `assignment_rule` rows had moved it 686 → 688) and
`packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts`
(the action key set, 47 → 48, with `disabledReason` added to the named
sample).
Local runs (on `aa552ced`, before the `main` merge at `c81d8630e`, which
touched none of these packages; the lint fix and the cli typing fix came
after and were re-run on `1d2b0252`):
<details><summary>Test readings</summary>
- `pnpm --filter @objectstack/spec test` (under `os-verify-lock`): Test
Files 733 passed (733), Tests 21985 passed, 1 todo.
- `pnpm --filter @objectstack/spec typecheck`: exit 0
(check:test-typecheck OK).
- `pnpm --filter @objectstack/lint test`: Test Files 167 passed (167),
Tests 7423 passed. The first run reded one pin,
`validate-translation-references.test.ts` "classifies every key the
action translation shape declares": its coverage table needed the new
`disabledReason` row (`leaf-copy`, prose), added in `1d2b0252`.
- `pnpm --filter @objectstack/lint typecheck`: exit 0.
- `pnpm --filter @objectstack/cli exec vitest run --project unit
test/i18n-extract test/i18n-coverage`: Test Files 10 passed, Tests 100
passed; the new file 6/6 after the typing fix.
- `pnpm --filter @objectstack/cli typecheck`: exit 0 after `1d2b0252`
(the first run named 4 TS18048 in the new test file, fixed with optional
chaining rather than a debt-ledger row).
- `pnpm check:i18n`: OK, 10 packages in sync.
- `pnpm --filter @objectstack/spec check:generated`: 2 of 14 stale on
the first run (`check:docs`, `check:liveness`), regenerated with
`--fix`, committed in `aa552ced`.
- Round 2 on `bdac02acf` (contract-review and CI fixes):
`@objectstack/spec` test 733/733 files (21985 tests);
`@objectstack/lint` test 167/167 files (7425 tests) and typecheck exit
0; `@objectstack/platform-objects` test 96/96 files (1361 tests);
`@objectstack/metadata-protocol` test 269 passed, 3 skipped (21580
tests); `check:liveness` OK; `check:generated --fix` regenerated
`check:docs` only (the `.describe()` texts).
- Round 3 on `c9d6d356f` (delta-review fix `51685f13c`, `main` merged at
`5b5e6275d`, reference docs regenerated on the merged tree in
`c9d6d356f`): `@objectstack/platform-objects` test 96/96 files (1361
tests); `@objectstack/metadata-protocol` test 269 passed, 3 skipped
(21616 tests), the action key-set pin still 48 after the merge;
`@objectstack/spec` test 735/735 files (22034 tests);
`@objectstack/lint` test 168/168 files (7433 tests); `@objectstack/spec
check:generated` all current; `node scripts/check-doc-authoring.mjs`
exit 0 (no internal issue id in customer text).
</details>
## Acceptance notes
- **The renderer half follows.** Between this landing and the console PR
an author can declare the key and nothing renders it; the liveness row
is `planned` and the author-warning lint says so. The docs section says
the console does not show the key yet, names the default reason every
greyed action shows until the readers ship, and says `os validate` warns
an author who declares it. Direction of the window: spec accepts ahead
of the renderer, never the reverse.
- **Skills-docs is owed, not done here.** `skills/**` is Tier H. Putting
the skills lines in this PR would make the spec half wait for a
maintainer approval; the owed files are listed on the `Skills-docs:`
line for the seat to file. `skills/objectstack-ui/rules/actions.md` and
its eval file are at 0 token headroom, so that card pays under the
ruling 「A:先削重复,余额按实测提高」.
- **A blank value inside an inline locale map is not refused.** The map
arm is `InlineLocaleMapSchema` as every I18nLabel takes it; refusing a
blank map value would need a refinement the projection's closed list
cannot publish, or a second locale-map schema. The ruling names the
empty string, which is refused.
## Verification
<details><summary>Derived gates (`dispatch-gates.mjs --commands`, 109
families, run serially on `c81d8630e`)</summary>
- 106 exit 0.
- 3 exit 3, PREREQUISITE NOT MET (read built output of the whole repo;
no full build here): `pnpm --filter @objectstack/spec run
check:skill-examples`, `pnpm check:dual-build-cjs-loads`, `pnpm
check:i18n-coverage`. NOT MEASURED locally; CI runs them.
- `dispatch-gates --ran`: "109 derived famil(ies) accounted for — 106
run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3)."
- Dogfood: not run. No `!` in the title and no `(narrowing)` arm.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes (eslint over the 10
changed source and test files: exit 0; the repo-wide run is CI's).
- [x] 4. One issue, one branch, one pull request; `main` merged in at
`5b5e6275d` (no rebase), current with `main` at `f7c95cd19`.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01WGufzfHuAwrecfjUDtkwcm
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzfHuAwrecfjUDtkwcm)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 55b5042 commit 13dc413
27 files changed
Lines changed: 538 additions & 14 deletions
File tree
- .changeset
- content/docs
- protocol/objectui
- references
- data
- kernel
- system
- ui
- ui
- packages
- cli
- src/utils
- test
- lint/src
- metadata-protocol/src
- platform-objects/src/apps/translations
- spec
- authorable-surface
- liveness
- state-counts
- src
- system
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
245 | 245 | | |
246 | 246 | | |
247 | 247 | | |
| 248 | + | |
248 | 249 | | |
249 | 250 | | |
250 | 251 | | |
| |||
333 | 334 | | |
334 | 335 | | |
335 | 336 | | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
336 | 352 | | |
337 | 353 | | |
338 | 354 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
499 | 499 | | |
500 | 500 | | |
501 | 501 | | |
| 502 | + | |
502 | 503 | | |
503 | 504 | | |
504 | 505 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
346 | 346 | | |
347 | 347 | | |
348 | 348 | | |
| 349 | + | |
349 | 350 | | |
350 | 351 | | |
351 | 352 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
128 | 128 | | |
129 | 129 | | |
130 | 130 | | |
| 131 | + | |
131 | 132 | | |
132 | 133 | | |
133 | 134 | | |
| |||
227 | 228 | | |
228 | 229 | | |
229 | 230 | | |
| 231 | + | |
230 | 232 | | |
231 | 233 | | |
232 | 234 | | |
| |||
384 | 386 | | |
385 | 387 | | |
386 | 388 | | |
| 389 | + | |
387 | 390 | | |
388 | 391 | | |
389 | 392 | | |
| |||
603 | 606 | | |
604 | 607 | | |
605 | 608 | | |
| 609 | + | |
606 | 610 | | |
607 | 611 | | |
608 | 612 | | |
| |||
791 | 795 | | |
792 | 796 | | |
793 | 797 | | |
| 798 | + | |
794 | 799 | | |
795 | 800 | | |
796 | 801 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| 59 | + | |
59 | 60 | | |
60 | 61 | | |
61 | 62 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
75 | | - | |
| 75 | + | |
76 | 76 | | |
77 | 77 | | |
78 | 78 | | |
| |||
0 commit comments