Skip to content

feat(spec)!: an action's description, param helpText and outcomeMessages are {{ }} template slots; ${…} refused at publish - #28463

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-2br7jq
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-2br7jq

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D

Requested by Tesla · project thread

Fixes #27948

Clause-②: yes (narrowing)
Metadata-protocol: narrowed
Skills-docs: follow-up #28464 (skills/objectstack-ui/rules/actions.md line 23 lists the {{ }} copy holes for confirmText / successMessage / errorMessage only; add description, param helpText and outcomeMessages)

维护者速读

  • 改了什么:动作的 description(参数对话框说明)、每个参数的 helpText、每条 outcomeMessages 现在和 confirmText / successMessage 一样是 {{ }} 模板槽:裸字符串解析成 { dialect: 'template', source },objectstack validate / compile / 发布闸(422)拒绝其中的 ${…},并给出 {{ }} 写法。平台自带文案(邀请、重发邀请、改角色)和四种语言翻译已改成 {{ }}。
  • 为什么改:裁决 A(「同意」)定五个文案键统一用 {{ }};第 1 步(feat(core): an action's param dialog description, helpText and outcomeMessages fill {{ }} template holes, and still fill ${…} #28436)让控制台先能读 {{ }},这是第 2 步:规范收口 + 发布拒绝 + 迁移。
  • 风险与回滚:收窄(破坏性):写 ${…} 的这三个键在保存/发布时被拒;已存的仍能加载,控制台对没有 {{ }} 的句子继续按 ${…} 填,直到下次保存。解析后的值变成信封对象,所以第一个提交先让所有读取方(core、react、runtime、cli、Studio 预览)接受信封。回滚即 revert 本 PR。
  • 先看:packages/spec/src/ui/action.zod.ts(三个槽的类型)、packages/lint/src/validate-action-copy-templates.ts(发布闸)、ui/packages/core/src/actions/ActionRunner.ts(dialogCopyOf)、迁移条目 packages/spec/src/migrations/entries/semantic/18.action-dialog-outcome-copy-template-holes.ts。
  • 两分钟验证:pnpm --filter @objectstack/lint exec vitest run src/validate-action-copy-templates.test.ts,预期 23/23 通过;cd ui && pnpm exec vitest run packages/core/src/actions/__tests__/ActionRunner.copyEnvelope-27948.test.ts,预期 4/4。
  • 需要你做的:无,契约复审后由席位落地;HotCRM 的同类文案另开为 Knowledge article vote outcomeMessages: ${result.*} holes become {{ result.* }} with the platform pin that refuses ${…} hotcrm#2384。

What changes

Step 2 of #27948, ruling 6545229148, letter A. Four commits.

1. Readers first (fcd2e654). Typing the three keys makes a parsed action carry { dialect: 'template', source } where it carried a string, so every reader that took a string now takes the envelope (claim 6546254270, amendments 6546313079 and 6546830184):

  • ui/packages/core/src/actions/ActionRunner.ts: dialogCopyOf hands the param-collection handler the action and params with description / each helpText reduced to their text (actionCopyText, new in actionCopyTemplate.ts); a locale map is handed on for the host to localize. composeSuccessMessage reads an outcome envelope's source.
  • ui/packages/react/src/hooks/useActionTextLocalizer.ts: an outcomeMessages envelope keeps its shape (a bundle entry replaces its source) instead of collapsing to the word "template".
  • packages/runtime/src/action-execution.ts summarizeActionParams: the MCP param description reads helpText's text, never "[object Object]".
  • packages/cli/src/utils/i18n-extract.ts: description, param helpText and outcomeMessages go through actionCopy (templateSlotSource), so the envelope seeds exactly as the bare string.
  • ui/packages/app-shell/src/views/metadata-admin/previews/ActionPreview.tsx: localize reads the envelope's source and helpText goes through it (it was drawn raw, an object as a React child).

2. Spec and the publish refusal (45d5e284).

  • packages/spec/src/ui/action.zod.ts: description and each outcomeMessages value are actionCopy(...) slots with the inline locale map; ActionParamSchema.helpText is actionParamHelpText(), the same slot without a map (it was a plain string). Describes teach {{ }}.
  • packages/spec/src/ui/action-copy-template.ts: ActionCopyKey widened to the three keys, each with its own refusal wording and example; ActionRowCopyKey keeps the page rows at their three; three describe constants; module notes on the roots and the ${…} transition.
  • packages/lint/src/validate-action-copy-templates.ts: a registered action is judged on description, each outcomeMessages.OUTCOME and each params[N].helpText; an inline properties.action on its params' helpText. Roots: actionConfirmTemplateRoots(action) for the dialog copy, ACTION_SUCCESS_TEMPLATE_ROOTS for an outcome. Every finding is an error, so the publish gate answers 422.
  • packages/spec/src/system/i18n-resolver.ts: ActionLike types the envelope on the three keys.
  • Expression-conformance ledger: rows template-action-dialog-copy and template-action-outcome-message. liveness/action.json outcomeMessages evidence names {{ }}. Reference docs regenerated.
  • Eight platform-objects identity tests read the parsed copy through templateSlotSource (copyAsWritten), as their confirmText reads already did.

3. Migration and producers (a8f9f1c7).

  • Semantic entry action-dialog-outcome-copy-template-holes. No conversion: there is no copy-hole conversion family in os migrate meta, and a ${…} token held an expression (${record.user_id?.name ?? 'this member'}), which has no mechanical {{ }} spelling.
  • Producers: invite-outcome-messages.ts (sent / link_only, invite and resend), sys_invitation.resend_invitation and sys_member.update_member_role descriptions ({{ record.user_id.name | default:'this member' }}), both permission_set_granted outcomes, the four objects.generated bundles (en regenerates identically; check-i18n-bundles in sync), the action form's outcomeMessages help (action.form.ts + four metadata-forms.generated bundles), and content/docs/ui/actions.mdx.
  • Changesets: 27948-action-copy-template-slots.md (@objectstack/spec and @objectstack/lint minor; platform-objects, runtime, cli patch; ADR-0087 registered, FROM → TO list) and 27948-action-copy-envelope-readers.md (@object-ui/core, @object-ui/react, @object-ui/fields patch).

4. Fields reader (408466db). ui/packages/fields/src/resolveActionParams.ts reads helpText through actionCopySource; pin ui/packages/app-shell/src/utils/resolveActionParams.helpTextEnvelope-27948.test.ts (3 tests; reverse 2 failed, the type pin passed).

Tests

  • New pins, each reverse-checked (source restored from the merge base, test kept, then restored):
    • ActionRunner.copyEnvelope-27948.test.ts (core): 4; reverse 2 failed, 2 passed (the passing two are controls).
    • useActionTextLocalizer.outcomeEnvelope-27948.test.tsx (react): 3; reverse 2 failed, 1 control passed.
    • action-param-help-envelope-27948.test.ts (runtime): 2; reverse 2 failed.
    • i18n-extract-action-copy-envelope-27948.test.ts (cli): 2; reverse 1 failed, 1 control passed.
    • ActionPreview.copyEnvelope-27948.test.tsx (app-shell): 2; reverse 1 failed, 1 control passed.
    • validate-action-copy-templates.test.ts (lint): 6 new; reverse all 5 refusal pins failed, the accept pin passed.
    • action-copy-template.test.ts (spec): 7 new (parse arms, locale map per slot, refusal names, describes teach {{ }}).
  • Updated: action-description.test.ts and action-outcome-messages.test.ts expect the envelope from a parsed bare string.

Acceptance notes

  • Readers, all five groups (seat ruling A, amendments 6546313079, 6546830184 and 6547728288): @object-ui/react useActionTextLocalizer; runtime summarizeActionParams; cli i18n-extract; app-shell ActionPreview.tsx; @object-ui/fields resolveActionParams (commit 4, 408466db, named by CI "Type Check UI": RawActionParam.helpText is unknown and read through actionCopySource; a field-backed param whose envelope has no text falls back to the field's help). Plus core's own ActionRunner (dialogCopyOf, composeSuccessMessage). App-shell's useConsoleActionRuntime / RecordDetailView / ActionParamDialog read description / helpText from what the runner hands the handler, so they get strings and do not move.
  • Left as is: action-icon.tsx tooltip (ui/packages/components/src/renderers/action/action-icon.tsx lines 319 and 332, schema.label || schema.description). It would draw an envelope only with an empty label, and no parsed slot reaches it that way: useActionTextLocalizer always fills label (falling back to the name), a page row's description is not a slot, and UIActionSchema still types description as a string. Seat ruling: leave it.
  • Reader sweep at 408466db over non-test sources in ui/packages/*/src, packages/*/src, plugins, services, mcp and cli: no other reader of the three keys takes the parsed value as a string. The renderers (action:button / icon / group / menu, element:button) forward description and outcomeMessages into execute(), and the runner reduces them.
  • HotCRM producers (carried by Knowledge article vote outcomeMessages: ${result.*} holes become {{ result.* }} with the platform pin that refuses ${…} hotcrm#2384): src/service/actions/knowledge_article.actions.ts lines 121-123 and 144-146 (six outcomeMessages entries with ${result.helpful} / ${result.notHelpful}), and src/sales/translations/{en,es-ES,ja-JP,zh-CN}/objects.service.ts. Roots are result, so each converts to {{ result.helpful }} / {{ result.notHelpful }}.
  • default vs ??. {{ record.user_id.name | default:'this member' }} also fires on an empty name, where ?? fired only on null/undefined. For a member name that is the intended reading.
  • Census beyond the stop report. Three more platform holes than out/27948-step2-surface-stop.md listed: the link_only outcomes and both permission_set_granted outcomes (${result.grantedPermissionSet.label}). All on record / result, all converted.
  • The family's other refusals now reach these keys: a blank sentence, a single-brace {x}, a hole over an unbound root, logic or an unknown formatter. The in-repo census found none; full dogfood is the check on the platform's own copy.
  • Not changed. packages/plugins/plugin-auth/src/role-change-granted-set.ts line 29 still names ${result.grantedPermissionSet.label} in a comment (outside the surface). The ui console pin ActionParamDialog.invitationScreens-27521.test.tsx carries its own ${…} literal, which the console still reads.
  • ActionLike / ActionCopyKey widening is a type-level change for TypeScript readers of a parsed ActionSchema; the changeset names templateSlotSource as the reader.

Verification

Readings at head a8f9f1c7 (merge base 776c1d87).

Tests
  • pnpm --filter @objectstack/spec test: Test Files 773 passed, 1 skipped; Tests 22871 passed.
  • packages/lint vitest: Test Files 183/183, Tests 7699/7699.
  • packages/platform-objects vitest: Test Files 104/104, Tests 1474/1474.
  • packages/cli vitest test/i18n: 44/44 files, 419 tests. packages/runtime vitest src/action*: 29/29 files, 521 tests.
  • cd ui && pnpm exec vitest run packages/core/src/actions packages/react/src/hooks packages/react/src/context: 108/108 files, 1263 tests. packages/app-shell/src/views/metadata-admin/previews: 157/157 files, 1713 tests.
  • Type-check: spec tsc --noEmit, runtime and cli typecheck, @object-ui/core and @object-ui/react type-check: clean.
  • node scripts/check-i18n-bundles.mjs: all bundles in sync.
  • Full dogfood: see the os-dev-report on the card.
Gates
  • Build: env -u OS_SKIP_DTS bash scripts/pm/os-verify-lock.sh -c "pnpm exec turbo run build --filter='./packages/**' --filter='@objectstack/dogfood^...'", exit 0.
  • dispatch-gates --commands at a8f9f1c7: 116 families, stdin from /dev/null, exit codes recorded before any pipe; all 116 exit 0. --ran: 116 derived, 116 run, 0 NOT-MEASURED. Record: /mnt/project-files/spec-seat-2/r2026-10-30/out/27948-step2-gates-ran-a8f9f1c7.list.
  • The full CI farm is CI's.

🤖 Generated with Claude Code

Generated by Claude Code

…an action's description, helpText and outcomeMessages

Readers first (#27948, ruling A, step 2): the next commit types these three
keys as `template` slots, so a parsed action carries
`{ dialect: 'template', source }` where it carried a string. Each reader
that took a string now takes the envelope, pinned and reverse-checked:

- core ActionRunner hands the param-collection handler the action and
  params with `description` / each `helpText` reduced to their text
  (`actionCopyText`), and composeSuccessMessage reads an outcome
  envelope's source;
- react useActionTextLocalizer keeps an outcomeMessages envelope's shape
  instead of collapsing it to the word "template";
- runtime summarizeActionParams reads helpText's text for the MCP
  param description, never "[object Object]";
- cli i18n-extract seeds the envelope's text exactly as the bare string's
  on description, helpText and outcomeMessages;
- app-shell ActionPreview (Studio) draws helpText, confirmText and
  successMessage from the envelope's source; it drew helpText raw, an
  object as a React child.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibwQ8tWwY7TiHQPMCHR
… and each outcomeMessages entry as template slots; refuse ${…} there

#27948, ruling A, step 2. The five action copy keys speak one hole syntax:

- spec: `ActionSchema.description` and each `outcomeMessages` entry become
  `actionCopyTemplateSlot`s with the inline locale map, and
  `ActionParamSchema.helpText` one without it (it was a plain string). A
  bare string parses to `{ dialect: 'template', source }`; a blank or
  `${…}` sentence still parses, so a stored action loads. Describes teach
  `{{ }}`; refusals name each slot (`ActionCopyKey` widened, page rows keep
  `ActionRowCopyKey`). `ActionLike` (i18n-resolver) types the envelope.
- lint: the action copy door judges the three per path
  (`description`, `params[N].helpText`, `outcomeMessages.OUTCOME`) through
  `closedScopeTemplateRefusal` over `confirmText`'s roots for the dialog
  copy and `successMessage`'s for an outcome; every finding an error, so
  validate / compile fail and the publish gate answers 422. An inline
  `properties.action` is judged on its params' helpText.
- ledgers: expression-conformance rows for the two new surfaces; the
  outcomeMessages liveness evidence names `{{ }}`.
- platform-objects tests read the parsed copy as written.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibwQ8tWwY7TiHQPMCHR
…}; semantic migration entry

#27948, ruling A, step 2:

- migration: semantic entry `action-dialog-outcome-copy-template-holes`
  (no conversion: a `${…}` token held an expression, a hole holds a path
  and formatters only; `os migrate meta` lists it).
- producers: the invite actions' `sent` / `link_only` outcomes,
  `resend_invitation` and `update_member_role` descriptions (the `??`
  fallback becomes `| default:'this member'`), the role change's
  `permission_set_granted` outcomes, and their en / zh-CN / ja-JP / es-ES
  bundles (en regenerates identically); the action form's outcomeMessages
  help and the actions guide teach `{{ }}`.
- changeset: Clause-② yes (narrowing), Metadata-protocol narrowed,
  ADR-0087 registered.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibwQ8tWwY7TiHQPMCHR
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/cli, @objectstack/lint, @objectstack/platform-objects, @objectstack/runtime, @objectstack/spec, touching 42 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (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.

31 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 80a9f99222a62c336659bef6e8dcabd7ffb7b1b3.

⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/liveness/action.json) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: sys_user (symbol, 48 pages)
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 212 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 — 180 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 80a9f99222a62c336659bef6e8dcabd7ffb7b1b3 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 80a9f99222a62c336659bef6e8dcabd7ffb7b1b3

⚠️ 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 80a9f99222a62c336659bef6e8dcabd7ffb7b1b3 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…27948)

ActionParam.helpText is a template slot since step 2, so the spec's
ActionParam was no longer assignable to fields' RawActionParam (Type Check
UI, resolveActionParams.test.ts). RawActionParam.helpText is now unknown
and every emit reads it through actionCopySource; a field-backed param
whose envelope carries no text falls back to the field's own help.

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

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 408466dbf15142e15b12faed75b61d213717187e · 2026-10-31T19:14Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review of head 408466dbf15142e15b12faed75b61d213717187e is read and waiting on a green head · 2026-10-31T19:20Z

  • Two Test Core legs are red on this head, both stale string pins on copy that now parses to the template envelope, in test files the reader sweep (non-test sources) did not cover:
    • Test Core (5/8): packages/cli/test/i18n-extract-action-description.test.ts L173 — parsed.data.description expected the bare string, received { dialect: 'template', source }.
    • Test Core (6/8): packages/plugins/plugin-approvals pnpm run test exited 1 (annotation at log line 27704; the tail the lane can read ends after it). The one pin in that package that reads a parsed action's description as a string is src/translations/approve-decision-question.test.ts L50 (typeof description is 'string'); the seat confirms from its own run.
  • Neither is a contract matter: the spec, lint, migration and reader hunks raise nothing against ruling A as read so far. One should-fix for the same push: the outcomeMessages TSDoc in action.zod.ts (the "Interpolation" paragraph) says os migrate meta rewrites stored copy to {{ }}, while the registered semantic entry action-dialog-outcome-copy-template-holes deliberately registers no conversion; the changeset and the entry say "no conversion", so the docblock should too.
  • The lane does not PASS a red head. The claim above stays; on the next head the lane re-claims and records the delta.

…emplate envelope; docblocks name the semantic entry (#27948)

Test Core 5/8 and 6/8 pinned a parsed action's `description` as a bare
string. It now parses to `{ dialect: 'template', source }`, so the cli
extractor pin expects the envelope and the two plugin-approvals pins read
the sentence through templateSlotSource.

The outcomeMessages docblock (action.zod.ts) and the action-copy-template
module note said `os migrate meta` rewrites stored copy. The semantic entry
action-dialog-outcome-copy-template-holes registers no conversion, so both
now say the rewrite is by hand and name the entry.

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

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head e68dae860ca61761f550856546d6af37ddd3ccd4 · 2026-10-31T20:15Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e68dae860ca61761f550856546d6af37ddd3ccd4
Local-runs: none
Scope: packages/spec: ui/action.zod.ts (description and each outcomeMessages value through actionCopy(...) with the locale map; ActionParamSchema.helpText through actionParamHelpText() without one; describes), ui/action-copy-template.ts (ActionCopyKey widened to six, ActionRowCopyKey keeps the page rows at three, per-key SLOT_NAME / EXAMPLE, three describe constants), system/i18n-resolver.ts (ActionLike / ActionParamLike type the envelope), ui/component.zod.ts (row keys typed ActionRowCopyKey), ui/action.form.ts help, semantic migration entry action-dialog-outcome-copy-template-holes (no conversion), liveness outcomeMessages evidence; packages/lint validate-action-copy-templates.ts (REGISTERED_KEYS / INLINE_KEYS, valuesOf paths outcomeMessages.OUTCOME and params[N].helpText, roots per key); readers: @object-ui/core ActionRunner (dialogCopyOf, composeSuccessMessage via actionCopySource) + actionCopyText, @object-ui/react useActionTextLocalizer, @object-ui/fields resolveActionParams, app-shell ActionPreview.tsx, runtime summarizeActionParams, cli i18n-extract; platform producers (invite-outcome-messages.ts, sys-invitation / sys-member descriptions and permission_set_granted outcomes, four objects.generated + four metadata-forms.generated bundles); dogfood ledger rows template-action-dialog-copy / template-action-outcome-message; docs; two changesets (@objectstack/spec + @objectstack/lint minor, platform-objects / runtime / cli patch, ADR-0087 registered; @object-ui/core / react / fields patch). 57 files, +1360/−211 against merge-base 776c1d8, merge-tree clean. Fixes #27948 step 2 (ruling A 6545229148; claim 6546254270 + amendments 6546313079 / 6546830184 / 6547728288; step 1 = #28436, lane PASS 6546030702). Not governed (no skills/**, no docs/adr/**; skills follow-up #28464).
Inputs: card #27948 thread (ruling, both claims and the three amendments, the step-1 report with its two step-2 findings), the PR body, the PR diff at this head (every non-test, non-generated hunk read in full; tests by case; translation bundles by count), git grep on origin/main (actionCopySource / fillTransitionalCopy in actionCopyTemplate.ts and templateSlotSource in flow-text-slot-template.ts exist from step 1), the hold note 6548021215 on the red head 408466db and the delta to this head (one commit e68dae86, 5 files, +26/−10: the three stale string pins read the parsed description as the template envelope — cli i18n-extract-action-description.test.ts L173 toEqual({ dialect: 'template', source }), plugin-approvals sys-approval-request.object.test.ts and translations/approve-decision-question.test.ts through templateSlotSource — and the two TSDoc sentences in action.zod.ts / action-copy-template.ts now say the ${…} → {{ }} rewrite is by hand and the semantic entry registers no conversion; no producer or reader moved), check-runs (read 2026-10-31T20:27Z: all 65 latest-per-name check-runs completed, 58 success + 7 skipped, none failed, none running) — 65 checks. Nothing built or run here; the seat's numbers (spec 773 files / 22871, lint 183 / 7699, platform-objects 104 / 1474, 116 derived gates, the reverse checks per pin) are the PR body's.

① Derived judgments

  • Ruling A's step 2, as ordered. Step 1 (feat(core): an action's param dialog description, helpText and outcomeMessages fill {{ }} template holes, and still fill ${…} #28436) taught the console {{ }} on the three keys while ${…} kept reading; this step closes the spec (the three keys typed as template slots with the describes teaching {{ }}), adds the publish refusal (the lint pass judges description, each outcomeMessages.OUTCOME and each params[N].helpText through the same judge as confirmText, every finding an error so the gate answers 422) and registers the migration. All five keys now speak one hole syntax, which is what A decided; B (lint only) is not what shipped.
  • Roots follow the key each slot stands beside. description / helpText take actionConfirmTemplateRoots(action) (the dialog opens before the run, over record; a registered action's params binds nothing), an outcomeMessages entry takes ACTION_SUCCESS_TEMPLATE_ROOTS (result, record). That matches what step 1's runner fills (dialogCopyFiller over record + static params; composeSuccessMessage over result + record), so lint and renderer agree on the closed scope. Step 1's second finding (the ${…} path read the whole runner context, e.g. user) is answered: a hole over any other root is refused by name, and the migration entry says such logic moves into the handler's answer.
  • The parse keeps loading stored rows. Each slot keeps the blank-string arm and a ${…} sentence still parses; refusal is at the doors only, and the console keeps the transitional ${…} reading for a sentence with no {{ }} hole. So a stored action loads and reads as before until its next save, which is judged. Metadata-protocol: narrowed + Clause-②: yes (narrowing) + the ! title + ADR-0087 registered (semantic entry, no conversion because a ${…} token held an expression) are the right declarations; the FROM → TO list in the changeset is the migration prose.
  • Every reader the typing breaks is in the first commit (step 1's first finding, objectui#13367's trap): core dialogCopyOf hands the dialog text not envelopes; composeSuccessMessage reads an outcome envelope's source; react keeps an outcome envelope's shape for the bundle; fields resolveActionParams reads helpText through actionCopySource (the fifth reader, forced by Type Check UI, amendment 6547728288); app-shell ActionPreview localizes the envelope; runtime summarizeActionParams reads helpText's text; cli i18n-extract seeds the envelope as the string. action-icon.tsx's tooltip is left with the stated reason (an envelope reaches it only with an empty label, which useActionTextLocalizer never leaves empty).
  • Platform copy converted on the right roots (result.email, result.grantedPermissionSet.label, record.email, record.user_id.name | default:'this member'), with the default-vs-?? difference named; the four bundles regenerate identically (check-i18n-bundles in sync). HotCRM's six ${result.*} outcomes are carried by hotcrm#2384; skills/objectstack-ui/rules/actions.md's hole list by Skills: action copy hole list names the three new template slots (description, param helpText, outcomeMessages) #28464 — both open.
  • Should-fix, recorded: fixed on this head. The step-1 action.zod.ts outcomeMessages TSDoc claimed os migrate meta rewrites stored copy while the semantic entry 18.action-dialog-outcome-copy-template-holes.ts registers no conversion; the docblocks now state the entry names a hand edit and registers none, which matches the entry.
  • Tier S by path; check-governed-merges not governed; seat lands after this record and green CI.

② Rulings cited

  • 6545229148 (spec seat 2 recording the maintainer's 「同意」 on the 2026-10-31 list item 4, 2026-10-31T15:13Z): letter A, two steps, renderer first. Step 2 implemented as ruled; no open ruling.

③ Narrowing / widening

  • Narrowing at the doors: a ${…} token, a single-brace {x} token, a hole over an unbound root, logic or an unknown formatter in a hole, or a blank sentence on description, params[N].helpText or an outcomeMessages entry is refused at objectstack validate / compile and at the publish gate (422). Stored rows still parse; the console's transitional read keeps them working.

④ Behaviour / flags

Implemented-by: claude/project-thread-2br7jq
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz
VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D

维护者速读

席位核对 · ACCEPT · domain:spec seat 2 (#18549) · 2026-10-31T20:30Z

Head e68dae860ca61761f550856546d6af37ddd3ccd4. Card #27948 step 2 of 2, claim 6546254270 + amendments 6546313079, 6546830184, 6547728288 (five envelope readers: @object-ui/react text localizer, runtime summarizeActionParams, cli i18n-extract, Studio ActionPreview, @object-ui/fields resolveActionParams); ruling 6545229148, letter A (maintainer 「同意」 2026-10-31T15:10Z). Step 1 was #28436.

  • Shape: draft to main, made by the relay; Fixes #27948; Clause-②: yes (narrowing), Metadata-protocol: narrowed at line start; Skills-docs: follow-up #28464. Title carries !; changeset minor with the ADR-0087 marker; semantic migration entry action-dialog-outcome-copy-template-holes (no conversion; the TSDoc says so).
  • Contract review: PASS by the lane review thread, record 6548733114 on this head (delta after 408466d: three re-pinned tests and two TSDoc sentences; the should-fix is recorded fixed), reviewer not the drafter. No must-fix.
  • Diff read by the seat: 57 files, +1360/−211. Readers first (commit 1, plus the forced fields commit), then the spec typing and the door refusal, then the producers and bundles. Every in-repo hole has a record or result root (census in the body). The console keeps step 1's ${…} read for stored rows. action-icon.tsx's label || description tooltip is left (not reachable from a parsed slot), noted in Acceptance notes.
  • Follow-ups filed: Skills: action copy hole list names the three new template slots (description, param helpText, outcomeMessages) #28464 (skills hole list, Tier H), Knowledge article vote outcomeMessages: ${result.*} holes become {{ result.* }} with the platform pin that refuses ${…} hotcrm#2384 (knowledge article vote outcomes; converts in the pin bump that carries this PR; Blocked-by: #27948).
  • Gates: check-runs on this head 58 success, 9 skipped, 0 red, 1 in progress (started by the label edit); auto-merge waits for it. Full dogfood exit 0 locally. check-governed-merges --branch: NOT governed, 57 paths, +1360/−211.
  • Authority: maintainer standing go-ahead 「按 pm-dispatch 审过的pr你可以自主落地」 (2026-10-30T14:20Z, project thread).
  • Landing: pr_ready + automerge_enable in this act.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 31, 2026 20:31
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 31, 2026 20:31
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit 7e3822c Oct 31, 2026
69 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-2br7jq branch October 31, 2026 20:58
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.

Action copy takes two hole syntaxes: description, param helpText and outcomeMessages read ${…}, confirmText and successMessage read {{ … }}

1 participant