Skip to content

feat(spec): a greyed action says why — disabledReason beside disabled - #26786

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-yxepsj
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-yxepsj

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

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):

Test readings
  • 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).

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

Derived gates (`dispatch-gates.mjs --commands`, 109 families, run serially on `c81d8630e`)
  • 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.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 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).
  • 4. One issue, one branch, one pull request; main merged in at 5b5e6275d (no rebase), current with main at f7c95cd19.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzfHuAwrecfjUDtkwcm


Generated by Claude Code

…led`

ActionSchema gains an optional, display-only I18nLabel `disabledReason`
(blank refused), translated at objects.OBJECT._actions.ACTION beside
confirmText / successMessage, overlaid by translateAction and emitted by
os i18n extract. Never a server admission input, never read as a predicate.

Claude-Session: https://claude.ai/code/session_01WGufzfHuAwrecfjUDtkwcm
Co-authored-by: Claude <noreply@anthropic.com>
…ema; regenerate artifacts

Claude-Session: https://claude.ai/code/session_01WGufzfHuAwrecfjUDtkwcm
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/cli, @objectstack/lint, @objectstack/platform-objects, @objectstack/spec, touching 14 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/lint/src/validate-translation-references.ts, packages/spec/authorable-surface/ui.json, packages/spec/liveness/action.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

9 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 lookupActionField))
  • content/docs/ai/connect-mcp.mdx (via confirmText (literal, a string literal in lookupActionField))
  • content/docs/automation/flows.mdx (via successMessage (literal, a string literal in lookupActionField))
  • content/docs/getting-started/build-with-claude-code.mdx (via successMessage (literal, a string literal in lookupActionField))
  • content/docs/protocol/kernel/i18n-standard.mdx (via confirmText (literal, a string literal in lookupActionField))
  • content/docs/protocol/objectui/actions.mdx (via disabledReason (symbol, a field of interface ActionLike), confirmText (literal, a string literal in lookupActionField), disabledReason (literal, a string literal in actionObject; a string literal in actionTranslationSchema; a string literal in collectExpectedEntries; a string literal in lookupActionField; a string literal in sections; a string literal in translateAction), successMessage (literal, a string literal in lookupActionField))
  • content/docs/protocol/objectui/record-alert.mdx (via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/ui/actions.mdx (via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/ui/translations.mdx (via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))

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

  • content/docs/releases/v17/17-1.mdx (via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/releases/v17/17-7.mdx (via successMessage (literal, a string literal in lookupActionField))
  • content/docs/releases/v18/18-1.mdx (via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))
  • content/docs/releases/v18/18-2.mdx (via confirmText (literal, a string literal in lookupActionField), successMessage (literal, a string literal in lookupActionField))

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
  • 4 changed file(s) yielded no anchor (packages/lint/src/validate-translation-references.ts, packages/spec/authorable-surface/ui.json, packages/spec/liveness/action.json, …) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 55 of 207 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 154 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 8b276385e6f1e2b94a4061160c148cfa56f81588 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 8b276385e6f1e2b94a4061160c148cfa56f81588

⚠️ 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 8b276385e6f1e2b94a4061160c148cfa56f81588 → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c81d8630e4f132d4cafb2b490bb678f637377a57
Local-runs: none
Scope: packages/spec/src/system/i18n-resolver.ts, packages/spec/src/system/translation.zod.ts, packages/spec/src/ui/action.form.ts, packages/spec/src/ui/action.zod.ts + packages/spec/src/ui/i18n.zod.ts (InlineLocaleMapSchema), packages/spec/src/shared/refinement-projection.ts (NON_BLANK_STRING), packages/spec/src/ui/action-disabled-reason.test.ts, packages/spec/liveness/action.json, packages/spec/scripts/liveness/check-liveness.mts, packages/lint/src/lint-liveness-properties.ts, packages/lint/src/validate-expressions.ts (+ .test.ts), packages/lint/src/validate-translation-references.ts (+ .test.ts), packages/cli/src/utils/i18n-extract.ts, .changeset/26619-action-disabled-reason.md, content/docs/protocol/objectui/actions.mdx, ui/packages/app-shell/src/views/metadata-admin/SchemaForm.tsx, ui/packages/components/src/renderers/layout/containers.tsx, ui/packages/components/src/renderers/action/*.tsx, ui/packages/app-shell/src/views/DeclaredActionsBar.tsx, ui/packages/plugin-detail/src/renderers/record-quick-actions.tsx

Ruling executed: 6442737825 on #26619, letter A. Inputs read: the card body and all five comments (triage 6440296436, brief 6441887048, ruling 6442737825, claim 6444476085, os-dev-report 6446453447), the PR body, its 24-file list, the net diff a485b5a3..c81d8630e (24 files, +505/−11, matching the file list exactly), and the check-runs on the head.

Head provenance. c81d8630e is a merge of origin/main a485b5a3 into the branch's three commits (13e780bb, aa552ced, 1d2b0252). git diff a485b5a3..c81d8630e is byte-identical to the branch's own pre-merge diff (merge-base(1d2b0252, a485b5a3)..1d2b0252): the merge adds nothing beyond the PR's net diff, with no conflict resolution. origin/main has since advanced three commits (a9f654eb, 785a7c3b, 0f3d068c) that touch four PR paths — packages/spec/src/system/i18n-resolver.ts at SYSTEM_FIELD_LABELS (~line 3556, the ADR-0142 queue label) and the three non-English *.metadata-forms.generated.ts bundles (+8 lines each, other keys) — all disjoint from this PR's hunks. A textual merge is expected to be clean; the queue leg performs it.

Check-runs on the head at review time (two REST reads, 15:50Z and ~16:05Z): 48 runs — 22 success, 9 skipped, 17 in_progress (Test Core 1–8, Build Core, Build Docs, Type Check × 5 incl. Type Check UI, Build & E2E UI, Lint · repo & tooling gates), 0 failure. The derived gate families (check:liveness, check:generated, check:api-surface, dropped-refinements, check:skill-examples, check:i18n-coverage, typecheck, the spec/lint/cli test suites) had therefore NOT rendered their verdicts when this record was written; nothing was red. This record judges the tree; the queue guard still holds the PR until every check is green, as Tier S requires.

① Derived judgments

  1. ActionSchema.disabledReason — optional, display-only, I18nLabel: RIGHT. action.zod.ts adds disabledReason: actionDisabledReason().optional() beside disabled on actionObject(), where actionDisabledReason = z.union([z.string().refine(NON_BLANK_STRING, …), InlineLocaleMapSchema]). An action without it parses unchanged (test "is optional"); InlineActionSchema (.pick() of type/name/label/target/openIn/method/params/bodyExtra/confirmText/successMessage/errorMessage/refreshAfter/opensInNewTab) picks neither disabled nor the new key, so the inline toolbar action is correctly left out — nothing there to explain. The authorable surface gains exactly ui/Action:disabledReason.
  2. Blank string refused, and the refusal projects: RIGHT. NON_BLANK_STRING (refinement-projection.ts:263, source.trim().length > 0, declared pattern: 'non-blank-string') is the registry-declared predicate, so the published json-schema/ui/Action.json (gitignored build output, line 63 of .gitignore) carries minLength/pattern: "\\S" and dropped-refinements.baseline.json is untouched — the diff adds no ledger row. '' and ' ' are refused at path ['disabledReason'] with a message naming the default and the omit remedy (tests pin both).
  3. Blank INSIDE the inline map is NOT refused — and the changeset and the protocol doc say it is: WRONG (prose, not code). InlineLocaleMapSchema (i18n.zod.ts:216–227) is z.record(key-regex, z.string()): {} and { en: '' } both parse. The code is consistent with the ruling, which names the empty string. But .changeset/26619-action-disabled-reason.md states "A blank reason is refused at parse: '', a whitespace-only string, an empty map, or a map with a blank value", and content/docs/protocol/objectui/actions.mdx states "A blank reason ('', whitespace, or a map with a blank value) is refused at parse". The PR body's acceptance note concedes the opposite ("A blank value inside an inline locale map is not refused"). Judged under ② below.
  4. Translation slot at objects.OBJECT._actions.ACTION.disabledReason AND globalActions.ACTION.disabledReason: RIGHT, and the second address is a sound extension, not an overreach. actionTranslationSchema(surface) (translation.zod.ts:177) is ONE factory instantiated at both addresses; confirmText / successMessage — the siblings the ruling names — already live at both. actionTranslationNode (i18n-resolver.ts) reads a bound action ONLY at objects.OBJECT._actions.ACTION and an object-less action ONLY at globalActions.ACTION, with no fallback. Declaring the key at the object address alone would leave every object-less action with no slot at all, which the ruling's "beside confirmText / successMessage" does not intend. Both address lists in the docblocks (i18n-resolver.ts header, ObjectTranslationDataSchema, appTranslationDataShape) are updated; the near-miss rename map (disabledMessage / disabledTooltip / disabledHint / disabledText → disabledReason) is mirrored on the action and on the bundle.
  5. translateAction overlays it like description: RIGHT. lookupActionField gains the 'disabledReason' literal; translateAction does lookupActionField(…, 'disabledReason') ?? action.disabledReason and spreads the key only when defined (test "adds no key to an action that declares none"). ActionLike.disabledReason?: string is narrowed to string exactly as description is (ActionLike.description?: string against an I18nLabel authoring site): an authored inline map passes through untouched, the same contract description has. Tests cover the object address, the global address, the fallback, and the no-key case.
  6. i18n extract emits it: RIGHT. collectExpectedEntries adds pushOptional(out, [...root, 'disabledReason'], action.disabledReason, kind, { objectName }) at both the object-action site and the global/inline-object site, pushOptional like description (an absent reason seeds nothing), with the header address list updated at both addresses. Covered by the new cli test.
  7. Never enters server admission: RIGHT. git grep disabledReason over the head tree outside tests finds the key only in the five spec/cli files above, one lint comment, and the four metadata-forms bundles. Every ui/ hit (containers.tsx#disabledReasonFor, DeclaredActionsBar.tsx:598, record-quick-actions.tsx:464, the action-*.tsx renderers' useDisabledReason, HomePage.tsx) is a LOCAL identifier bound to the generic sentence or to a hook result — no ui/ or packages/{core,rest,runtime,metadata} path reads action.disabledReason. Server refusal stays the hook's / requiredPermissions'.
  8. Lint never reads it as a predicate: RIGHT. validate-expressions.ts reads action.disabled alone (line 3346–3347) and action.visible; the literal-member census READ_SURFACES[receiver='action'].expected stays ['disabled', 'name', 'objectName', 'visible'] (the PR adds only a comment there plus the pin "never reads disabledReason as a predicate", which feeds a bare-field source — an error on disabled — into disabledReason and expects zero issues). The parse test pins that a predicate-looking value survives verbatim while disabled is lowered to its {dialect:'cel'} envelope. validate-translation-references.ts changes a docblock only (the leaf-key list), and its test's coverage table gains disabledReason: { kind: 'leaf-copy', why: 'prose' } — the pin that reds when the action translation shape declares a key the table does not classify. Neither reads the key as a condition.
  9. Liveness row planned: the STATE is right; the row is INCOMPLETE for the window it describes — WRONG as shipped. lint-liveness-properties.ts:182–189: "planned is deliberately absent [from the verdicts that warn on their own]: a planned key is one the platform asks authors to write, so it warns only where its row opts in" — i.e. only with authorWarn: true. The new row in liveness/action.json carries status, verifiedAt, evidence, note and NO authorWarn. So the changeset's closing sentence ("an author declaring it gets the planned-property warning that says so") and the PR body's 「作者会收到 planned 提示」 are false in the tree: between this landing — and the release the ruling says carries the key before hotclm#169 adds its line — and the console PR, an author who declares the key gets no signal that nothing renders it. The precedent row in the same ledger (outcomeMessages, line 108 note) ran its own planned window with authorWarn + authorHint and dropped both on the flip to live, which is the discipline check-liveness.mts#scanAuthorWarnRows enforces (a warned live row reds the gate). planned is otherwise the correct grade: dead would prescribe "Remove it", experimental is for a declared-but-unenforced guarantee, and the gate scans a planned row's evidence pointers (EVIDENCE_SCANNED_STATUSES), which here resolve to i18n-resolver.ts#translateAction, i18n-extract.ts#collectExpectedEntries and translation.zod.ts. A planned key in a published schema is explicitly allowed (10 planned rows on the census the gate header cites; no describe-marker is required when the ledger row grades it — led?.status || markerStatus(description), line 1219).
  10. Studio form row: RIGHT. action.form.ts adds { field: 'disabledReason', helpText: … } under disabled with no widget, mirroring the errorMessage row (errorMessage: I18nLabelSchema.optional(), action.zod.ts:1466). The node is not I18nLabelSchema by identity, but the Studio derives the face from the published JSON Schema: SchemaForm.tsx picks the best-matching anyOf branch and its string-arm detector walks nested anyOf (its own docblock example is { type: 'string', minLength: 1 }), so the refined string arm keeps the I18nLabel face. The en.metadata-forms.generated.ts leaf carries the form's helpText byte-for-byte; the three other locales carry hand-written leaves, which is the file header's documented editing model ("Edit translations in place").
  11. Public type surface. ActionLike (exported from i18n-resolver.ts) gains an optional member; lookupActionField's field literal union widens (module-private). check:api-surface records export existence, not members (AGENTS.md), so no snapshot change is owed; check:generated was 14/14 after the dev's --fix and CI re-runs it.

② Semver level

Changeset .changeset/26619-action-disabled-reason.md: @objectstack/spec: minor, @objectstack/cli: minor, @objectstack/platform-objects: minor; Clause-②: yes (widening); Metadata-protocol: added. The PR body carries the same two lines.

  • Level: RIGHT. The diff adds an authorable key and a translation leaf (kind added), so every package the diff moves takes at least minor (AGENTS.md Post-Task step 3): spec (the key), cli (extract emits it), platform-objects (four bundle leaves). @objectstack/lint moves only a docblock and two test files and takes no changeset — defensible; CI's changeset-coverage gate is the authority if it disagrees.
  • Clause-②: yes (widening) — RIGHT. One optional key added on a strictObject; the four near-miss spellings were already refused as unrecognized_keys and now refuse with a rename hint; no accept-set narrows anywhere. Metadata-protocol: added — RIGHT (a new key an author can write; several-kinds precedence does not apply, nothing is narrowed or re-meant).
  • Body matches what the diff publishes: WRONG on two sentences — MUST-FIX. The changeset ships as CHANGELOG.md and promises (i) refusal of "an empty map, or a map with a blank value" that InlineLocaleMapSchema does not make (①.3), and (ii) "an author declaring it gets the planned-property warning" that the ledger row does not opt into (①.9). The same (i) sentence is in content/docs/protocol/objectui/actions.mdx. Each is cured either by making the tree say it (for (ii): authorWarn: true + an authorHint on the row, the outcomeMessages precedent; for (i): a record-value refinement the projection can publish, if the dev wants it — the acceptance note already explains why not) or by rewriting the sentence to what the parse and the lint actually do. The fix is small; the record is FAIL until the published text and the tree agree.

③ Boundary flags

  • open_questions: [] in the os-dev-report — nothing to answer.
  • Dev flag: "A blank value inside an inline locale map is not refused." Answered: correct as a statement of the tree and consistent with the ruling ("empty string refused"); the flaw is that the changeset and the docs assert the opposite. Folded into the ② must-fix. Whether to ALSO refuse blank map values is not escalated: InlineLocaleMapSchema is shared by every I18nLabel, and a bespoke second map schema here would fork the vocabulary for one key.
  • Dev flag: "Skills-docs is owed, not done here." The line Skills-docs: follow-up owed — skills/objectstack-ui/rules/actions.md, skills/objectstack-ui/evals/views-apps-actions-pages.json, skills/objectstack-i18n/SKILL.md is well-formed under the pm-dispatch grammar (follow-up owed — FILES); the ruling (6442737825) says the line "is owed on the spec PR", which this is. skills/** is Tier H, so keeping it out of the spec half is the right split, and updated here is not available: the PR teaches the key in content/docs but no eval exercises it. The seat files the owed card and rewrites owed — FILES to #N before ACCEPT; this record does not substitute for that stroke. The two objectstack-ui files are at 0 token headroom (main 0f3d068c pins two skill line ceilings), so the owed card pays under 「A:先削重复,余额按实测提高」, as the dev notes.
  • Dev flag: "The renderer half follows" / liveness planned. The direction (spec first, console second) is the ruling's own workload column. The window is acceptable; the row must warn through it (①.9) — must-fix above.
  • The dev-report's premise_still_valid: true holds at the reviewed head: disabledReasonFor in containers.tsx:2372 still returns only the generic sentence.
  • Head freshness. Three main commits since the merge touch four PR files at disjoint hunks (provenance above); not a flag for the dev, noted for the queue leg.

④ Design judgment

packages/spec/src/ui/action.zod.ts — holds. Tried to break: (a) a predicate-looking value — stays a string, disabled beside it still lowers to its CEL envelope (pinned); (b) the key/defaultValue retired map form — refused by InlineLocaleMapSchema's key regex, pinned via the label parity loop; (c) the blank refusal escaping the published schema — NON_BLANK_STRING is the registry predicate, so the JSON Schema carries minLength + \S and the ledger needs no row (a superRefine draft would have added 12); (d) leaking into InlineActionSchema — not picked, pinned; (e) an unbounded author-facing message — DISABLED_REASON_BLANK names the default and the remedy and carries no tracker number (the [#26619] marker is in the docblock only, which check:doc-authoring permits). Should-fix (non-blocking): the .describe() text says the console shows it "(hover title and accessible description)" in the present tense while the liveness row is planned; harmless once the console PR lands, but a [planned]-style caveat here would also let markerStatus agree with the ledger during the window. Not required — the ledger row governs.

packages/spec/src/system/translation.zod.ts — holds. One factory, two addresses, siblings confirmText / successMessage already at both; the bundle-side rename map mirrors the action's; z.string().optional() is the right leaf type (a bundle leaf is ONE locale's sentence — never a map). Tried to break: a bound action's bundle at globalActions — unchanged refusal path (translation-target-unknown), nothing new reaches it. The validate-translation-references coverage table now classifies the leaf (leaf-copy / prose), so the shape and the lint cannot drift apart silently.

packages/spec/src/system/i18n-resolver.ts — holds. lookupActionField reads through the single actionTranslationNode, so the new leaf cannot disagree with its siblings about the address; the ?? action.disabledReason fallback and the conditional spread match description exactly. ActionLike.disabledReason?: string is the same deliberate narrowing description carries (the docblock says so), so an inline map is answered as authored — resolveI18nLabel (form 2) is the console's job, as for every I18nLabel. No must-fix.

packages/spec/src/ui/action.form.ts — holds. Plain row under disabled in Placement, no widget, I18nLabel face derived from the JSON Schema anyOf (SchemaForm walks nested anyOf and tolerates minLength on the string arm); the en bundle leaf equals the row's helpText; check:i18n reported 10 packages in sync. The helpText quotes the console default in English and says "Display only" — the right thing to tell a Studio author.

Must-fix (one, two sentences wide, found under ②/①.3/①.9): the changeset body and content/docs/protocol/objectui/actions.mdx claim a blank-map-value refusal the parse does not make, and the changeset/PR body claim a planned-property author warning the ledger row does not opt into. Make the tree say it (authorWarn: true + authorHint on the disabledReason row in packages/spec/liveness/action.json, to be dropped on the console PR's flip to live; and either a projectable record-value refinement or the sentence corrected) or make the text say what the tree does. Re-review on the new head.

Should-fix: the present-tense console sentence in the .describe() texts of action.zod.ts / translation.zod.ts during the planned window (cosmetic). Nothing else.

Implemented-by: claude/project-thread-yxepsj
Reviewed-by: session_01WGufzfhobWEgUUrjz4rrbh

VERDICT: FAIL

…the console reads it

Contract review on the spec half:
- The changeset and the actions page said an empty locale map or a blank map
  value is refused; only a blank string is. Both now say so.
- The changeset said an author gets the planned-property warning, but the
  liveness row set no authorWarn. The row now carries authorWarn plus an
  authorHint, and a lint pin reads the warning off the shipped ledger.
- The two .describe() texts no longer say the console shows the key yet.

CI pins that move with the new key: the metadata-form catalog positive
control (686 -> 687, the action form's new row) and the /meta/types action
key set (47 -> 48, now sampled by name).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzfHuAwrecfjUDtkwcm
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: bdac02acfe23d57afe4a7441a837b2cf57f46710
Local-runs: none
Scope: packages/spec/src/system/i18n-resolver.ts, packages/spec/src/system/translation.zod.ts, packages/spec/src/ui/action.form.ts, packages/spec/src/ui/action.zod.ts + packages/spec/liveness/action.json, packages/lint/src/lint-liveness-properties.ts (+ .test.ts), packages/spec/scripts/liveness/check-liveness.mts, scripts/check-doc-authoring.mjs, packages/spec/src/ui/i18n.zod.ts (InlineLocaleMapSchema), .changeset/26619-action-disabled-reason.md, content/docs/protocol/objectui/actions.mdx, content/docs/references/ui/action.mdx, content/docs/references/data/object.mdx, content/docs/references/kernel/metadata-plugin.mdx, content/docs/references/system/translation.mdx, packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts, packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts

This is a DELTA record over the full review at c81d8630e (PR #26786 comment 6446707981, VERDICT FAIL). It judges the one commit c81d8630e..bdac02acf (fix(spec): say what disabledReason refuses and warn the author until the console reads it, 12 files, +47/−17): whether it closes that record's one must-fix (two halves) and one should-fix, whether it introduces anything new, and whether every PASS judgment of the full record still holds. Inputs read: comment 6446707981, the delta diff, the four hits files and every cited file at bdac02acf, the current PR body, the check-runs / check-suites / workflow runs on bdac02acf and on c81d8630e, and origin/main at 665059b6 against the merge-base a485b5a3.

Check-runs on bdac02acf at review time (16:18Z, pushed 16:12Z): 0 check-runs, 0 workflow runs; 5 check-suites (claude, objectstack-fleet, vercel, fly-io, cloudflare-workers-and-pages) all queued, combined status pending. Nothing has rendered a verdict on this head. On the PREVIOUS head c81d8630e (51 runs) the CI workflow since completed with failure: Test Core (2/8) and Test Core (7/8) failed at "Run this shard's tests" (job logs redirect to blob storage the review proxy refuses, so the failing tests are not named here); every other workflow on c81d8630e (Lint & Type Check, Spec Liveness Check, Docs Drift Check, Governed Surface Guard, PR-Body Guards, Check Links, PR Automation) is success. Two unit-test failures in two shards are consistent with the two pins bdac02acf moves (platform-objects 686 and metadata-protocol 47, each a count the new key grew by one), which is the PR body's own account ("contract-review and CI fixes"). The queue guard still holds the PR until every check on bdac02acf is green.

① Derived judgments

Every ①-judgment of 6446707981 (1–11) stands at bdac02acf: the delta touches no parse, no translation slot, no resolver, no extract site, no form row, no bundle leaf — only two .describe() strings, the ledger row, the changeset, one docs sentence, four regenerated reference pages and three test files. Re-read:

  1. Must-fix half (i) — the blank-map claim: CLOSED. The changeset now reads "A blank string is refused at parse: '' or whitespace only. … The locale-map arm is the InlineLocaleMapSchema every I18nLabel takes, so a map is not checked for blank values", and content/docs/protocol/objectui/actions.mdx:349 reads "A blank string ('' or whitespace only) is refused at parse." Both now say exactly what actionDisabledReason() (action.zod.ts:917–920: z.string().refine(NON_BLANK_STRING) ∪ InlineLocaleMapSchema) does. The text was corrected rather than the schema widened, which is the right side: InlineLocaleMapSchema is shared by every I18nLabel.
  2. Must-fix half (ii) — the planned-property warning: CLOSED. packages/spec/liveness/action.json:154–161 row disabledReason now carries "authorWarn": true and an "authorHint" beside status: planned, verifiedAt, evidence (unchanged) and note (extended to say why authorWarn is set and that the console PR drops it on the flip to live). Row shape checked against both readers: (a) lint-liveness-properties.ts — LedgerEntry is { status?, authorWarn?, authorHint?, note?, children? }; shouldWarn admits a planned row only via entry.authorWarn === true (line ~190, RULED_VERDICTS/VERDICTS_THAT_WARN exclude planned); describe() answers planned with rule liveness-planned-property; checkItem shows entry.authorHint ?? defaultHint, never note; action is in TYPE_COLLECTIONS (actions), and the warn-map path is the top-level key, so item['disabledReason'] (a string → isAuthored) raises one finding at "action 'NAME'". (b) check-liveness.mts#scanAuthorWarnRows (1142–1151) walks raw rows and reds only authorWarn on a live row (report.authorWarnOnLive, red condition at 1523); a planned + authorWarn row is counted in authorWarnRows and is green — the precedent outcomeMessages row ran this exact window. The authorHint carries no tracker number (the note does, which is maintainer prose the lint never shows). The changeset's closing sentence and the PR body's 「作者会收到 planned 提示」 are now true in the tree.
  3. The new lint test (lint-liveness-properties.test.ts:2318–2340): SOUND. It reads the SHIPPED ledger through lintLivenessProperties (same resolver resolveLivenessDir → @objectstack/spec/package.json → liveness/), the same posture as the #26548 report block directly above it. Positive: an action with disabledReason set draws ruleOf(findings,'disabledReason') === 'liveness-planned-property' (ruleOf matches the message sets \disabledReason`), wherecontains the action name, andhintmatches^Keep it, but nothing reads it yet: no console shows a greyed action's `disabledReason`, which is the row's authorHintverbatim prefix. Control: the same action without the key draws[]— this holds only while no OTHER warned row inaction.jsonis tripped byname/label/objectName/disabled, and none is (disabledislive). The header comment correctly says the console PR's flip to liveturns the warning half red on purpose and prescribes rewriting it as a silence pin, the discipline theoutcomeMessagesprecedent followed. Not vacuous, not tautological, and it reds if the row losesauthorWarn` or the hint is reworded.
  4. Pin 686 → 687 (object-lifecycle-panel-echo-decisions.test.ts:1229–1231): HONEST. The control counts en catalog leaves ending in .label whose value is translated (present and different) in each of zh-CN/ja-JP/es-ES. The delta adds exactly one form row (action.form.ts:168, disabledReason) with one .label leaf authored in all four bundles (*.metadata-forms.generated.ts:1846); its helpText leaf does not end in .label and is not counted. +1 is the measured consequence of the key. The comment line follows the file's ledger convention.
  5. Pin 47 → 48 (protocol.meta-types-degenerate-derivation.test.ts:256–258): HONEST. The assertion is Object.keys(properties).length + retiredTopLevelCount('action') on the served /meta/types document; disabledReason is one new top-level key on actionObject(), nothing else on the action's key set moved (and origin/main has not touched action.zod.ts or this test since the merge-base), so +1 is the arithmetic. The sample list gains 'disabledReason', so the served schema is held to carrying THIS key and not merely one more; CARD_PROPERTY_COUNTS (the thirteen controls) does not list action and is untouched. The comment cites the card id in a // comment, which the doc-authoring gate permits.
  6. Regenerated docs mdx match the .describe() texts: YES, byte-for-byte. content/docs/references/ui/action.mdx:59, data/object.mdx:502 and kernel/metadata-plugin.mdx:345 carry the new action.zod.ts:1586 text; system/translation.mdx rows 129/229/386/605/793 carry the new translation.zod.ts:198 text. authorable-surface/ui.json lists the key only (no description), so no other sharded artifact is owed by a describe-text change. (But see ②/④: the text they faithfully project is the new must-fix.)
  7. Should-fix of 6446707981 (present-tense console sentence in .describe()): ADDRESSED in substance. Both describe texts now say the reader does not exist yet ("declared ahead of its console reader: no renderer shows it yet"; "no renderer shows it yet"). markerStatus (check-liveness.mts:503, /\[planned|not yet implemented|coming soon/i) still does not match this wording, which is fine — the ledger row governs (led?.status || markerStatus). The cure, however, appended , #26619 INSIDE the two strings — judged under ② and ④ below.

② Semver level

Unchanged from 6446707981 and still RIGHT: @objectstack/spec / @objectstack/cli / @objectstack/platform-objects at minor; Clause-②: yes (widening); Metadata-protocol: added, in both the changeset and the PR body. @objectstack/lint still moves only a test file in this delta, no changeset owed.

Body matches what the diff publishes — the two sentences the full record failed are now true (①.1, ①.2). The remaining PR-body claims were re-read against the tree: "planned with authorWarn and an authorHint, so os validate warns an author" — true; "Two pins move with the new key … 686 → 687 … 47 → 48, with disabledReason added to the named sample" — true; "A blank value inside an inline locale map is not refused" — true and now consistent with the changeset and docs; "check:generated --fix regenerated check:docs only (the .describe() texts)" — consistent with the four regenerated pages being the only generated files in the delta. One body claim is stale by events rather than by the tree: Human Minimum 4 "one branch off an up-to-date main" — see ③ (the PR is now mergeable_state: dirty).

NEW — MUST-FIX: a tracker id in customer-facing .describe() prose. action.zod.ts:1586 ("… no renderer shows it yet, #26619) — …") and translation.zod.ts:198 ("… (no renderer shows it yet, #26619)") each carry #26619 inside the .describe() string. scripts/check-doc-authoring.mjs Rule 3 (pnpm check:doc-authoring, run by .github/workflows/lint.yml:1822 in Lint & Type Check) walks packages/spec/src (SPEC_SOURCE_ROOT, position-based, no exemption, test files excluded), recognises a string literal whose parent call is .describe() at argument 0 as the describe bucket (customerTextPosition, line 1417), matches it against INTERNAL_ID_SOURCE (a # not preceded by #/&, then 3–5 digits, then no letter or digit — , #26619) matches) and reds the gate (messageIdViolations.length > 0 → failed = true, line 3275–3276, process.exit(1)). The gate's own failure text names the exact reason: ".describe() prose projects into content/docs/references/** and the generated skill artifacts. Neither reader has a tracker … #NNNN is a citation-shaped token resolving to nothing." AGENTS.md states the same rule in its header ("Runtime strings — refusal prose, prescriptions, anything an author is shown — carry no tracker number"). Census: these are the ONLY two .describe() strings in packages/spec/src at bdac02acf carrying a #NNNNN token; origin/main has zero. The full record's ④ explicitly noted the marker was then "in the docblock only, which check:doc-authoring permits" — the delta moved it into the string. Cure (the gate's prescription): drop , #26619 from both strings (the card id is already in the adjacent // comment at translation.zod.ts:196 and in the action.zod.ts docblock), e.g. "(declared ahead of its console reader: no renderer shows it yet)", then check:generated --fix so the four reference pages re-project. The Lint & Type Check workflow on bdac02acf has not run yet; on c81d8630e it passed because the id was not in the string then.

③ Boundary flags

  • Head freshness / mergeability — FLAG (not a tree defect, but the body's "up-to-date main" is no longer true). GitHub reports mergeable: false, mergeable_state: dirty on bdac02acf. origin/main is 12 commits past the merge-base a485b5a3; a9f654eb (ADR-0142 phase 2, queue / assignment_rule) moved the SAME pin line this delta moves: main object-lifecycle-panel-echo-decisions.test.ts:1231 is now toBe(688) ("688 since ADR-0142: queue and assignment_rule joined the registry, each with its type-level display pair"), the PR has toBe(687) on that line — a textual conflict whose honest resolution is 689 (688 + this PR's one row label), with both ledger comment lines kept. Main also moved translation.zod.ts, i18n-resolver.ts, i18n-extract.ts, en.metadata-forms.generated.ts, references/system/translation.mdx and references/kernel/metadata-plugin.mdx (feat(spec): a joined report's block headings and a master-detail field's inline grid heading are translatable #26757 joined-report headings, ADR-0142) at hunks disjoint from this PR's, and check-liveness.mts GOVERNED gained queue, assignment_rule (no effect on the action ledger). action.zod.ts, action.json, the metadata-protocol pin and the lint files are untouched on main, so 48 and the lint test survive the merge unchanged. The dev merges main, resolves 687 → 689, and re-runs check:generated (the two overlapping reference pages). The queue leg cannot do this: GitHub runs no custom merge driver.
  • CI on the previous head — CI on c81d8630e failed in Test Core shards 2/8 and 7/8 (see header). Not retrievable through the proxy; consistent with, and by the body's account caused by, the two pins this delta moves. The run on bdac02acf is what decides it.
  • Residual, non-blocking (carried, not new): content/docs/protocol/objectui/actions.mdx:339 still says "The console shows it as the button's hover title and its accessible description" in the present tense. The PR body's acceptance note discloses it ("The docs section describes the console's reading as the contract the follow-up delivers"), it was in scope and not a finding at c81d8630e, and the .describe() texts now carry the window caveat; a parenthetical of the same kind here would make the two surfaces agree. Should-fix, the dev's call.
  • The dev's three acceptance-note flags (renderer half follows; Skills-docs owed → now Skills-docs: #26790; blank map value not refused) stand as answered in 6446707981; #26790 is the seat's filing and is not re-judged here.

④ Design judgment

packages/spec/src/ui/action.zod.ts — holds on the contract, fails the gate on one token. actionDisabledReason() is unchanged; the delta is the .describe() text only. Tried to break the new text: it states the window honestly ("declared ahead of its console reader: no renderer shows it yet"), keeps the default sentence, the translation addresses, the not-a-gate clause and "A blank reason is refused" — all true (the string arm). The one defect is the #26619 token (②): a customer reading content/docs/references/ui/action.mdx or the JSON Schema description cannot resolve it, and the gate that exists for exactly this will red the PR. Mechanical fix, no design change.

packages/spec/src/system/translation.zod.ts — same. The leaf type, factory, both addresses and the rename map are untouched; the describe text now says "no renderer shows it yet" and carries the same #26619 token (②). The card id already sits in the // comment two lines above (:196), which is where the gate's prescription says it belongs.

packages/spec/src/system/i18n-resolver.ts — untouched by the delta; holds as judged in 6446707981 (translateAction overlays disabledReason like description; ActionLike.disabledReason?: string). Note for the merge: main's hunks in this file (lookupR…, SYSTEM_FIELD_LABELS, translat… at 3833+/3874+) are disjoint from the PR's (15, 46, 222, 648, 1427–1476).

packages/spec/src/ui/action.form.ts — untouched by the delta; holds. The row is what the 687 pin counts (①.4).

packages/spec/liveness/action.json row disabledReason — holds, and is now the shape the precedent prescribes: planned + authorWarn: true + authorHint (author-facing, no tracker id) + evidence (three resolvable pointers the gate scans for a planned row) + note (maintainer prose naming the four console readers and the flip discipline). The hint names the default sentence the author will actually see, which is the one fact an author needs during the window. The console PR must drop authorWarn/authorHint on the flip to live or check-liveness reds (authorWarnOnLive), and the new lint test reds too, by design.

Must-fix (one, new, two strings wide): remove #26619 from the two .describe() strings (action.zod.ts:1586, translation.zod.ts:198) and regenerate the four reference pages; pnpm check:doc-authoring Rule 3 reds on them. Also required before the queue: merge origin/main and resolve the 686/687/688 three-way to 689. Re-review on the new head is a delta over this record.

Should-fix: the present-tense console sentence at actions.mdx:339 (③). Nothing else.

Implemented-by: claude/project-thread-yxepsj
Reviewed-by: session_01WGufzfhobWEgUUrjz4rrbh

VERDICT: FAIL

…text

The two .describe() texts carried an issue id, which check-doc-authoring
refuses in customer text; the adjacent comments keep it. The actions page
now says the console does not show the key yet.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzfHuAwrecfjUDtkwcm
…-yxepsj

# Conflicts:
#	packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-27T17:16Z

Head c9d6d356f73771753b20bde48dbef24d9120692e. Card #26619 (ruling 6442737825, letter A), claim 6444476085, dev report 6446453447. This PR is the spec half (Part of #26619). The console half follows as PR 2 (Fixes #26619).

  • Contract review: owed at CONTRACT_REVIEW_TIER for action.zod.ts, action.form.ts, translation.zod.ts and i18n-resolver.ts. The history has three steps:
    1. The full review (6446707981) at c81d8630 was a FAIL. The code executed ruling A correctly, but the published text promised a blank-map refusal and a planned-property warning that the tree lacked.
    2. The delta review (6447429917) at bdac02ac closed both halves of that finding. The cure added one new must-fix: #26619 in the two customer-facing .describe() strings, which check-doc-authoring Rule 3 refuses.
    3. Since then, 51685f13 drops that token from both strings, and the id stays in the adjacent comments. Read at this head, neither action.zod.ts nor translation.zod.ts carries #26619)'. 5b5e6275 merges main with no rebase; its one conflict was the platform-objects catalog pin, now 689 (main's 688 plus this row), and the action key-set pin stays 48. c9d6d356 regenerates the reference docs on the merged tree. The net diff against the merge base is the PR's own 27 files, +538/−14. actions.mdx now says that the console does not show the key yet. No finding is open.
  • Lane seat (rest of the diff, TIER_DEFAULT):
    • disabledReason is an optional, display-only I18nLabel with a blank string refused through NON_BLANK_STRING.
    • Its translation slots sit at objects.OBJECT._actions.ACTION and globalActions.ACTION, beside confirmText / successMessage.
    • translateAction overlays it, and os i18n extract emits it.
    • No server path reads it, and the lint never reads it as a predicate.
    • Its liveness row is planned with authorWarn and authorHint until PR 2.
  • Skills-docs: #26790.
  • Shape: Part of #26619, 27 files. @objectstack/spec, @objectstack/cli and @objectstack/platform-objects are minor. Clause-②: yes (widening) and Metadata-protocol: added. check-governed-merges --pr 26786: NOT governed, 552 changed lines.
  • Gates: check-runs on this head are 42 success, 9 skipped and 0 red. mergeable_state is clean.
  • Landing: Tier S. pr_ready + automerge_enable in this act. PR 2 (console) starts once this merges.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 17:18
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 13dc413 Oct 27, 2026
52 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-yxepsj branch October 27, 2026 18:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants