Repository navigation
feat(spec)!: an action's description, param helpText and outcomeMessages are {{ }} template slots; ${…} refused at publish - #28463
Conversation
…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
📓 Docs Drift CheckThis PR changes 5 package(s): 31 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 180 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
…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
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract review of head
|
…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
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: ① Derived judgments
② Rulings cited
③ Narrowing / widening
④ Behaviour / flags
Implemented-by: |
|
domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D 维护者速读
席位核对 · ACCEPT ·
|
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 forconfirmText/successMessage/errorMessageonly; adddescription, paramhelpTextandoutcomeMessages)维护者速读
description(参数对话框说明)、每个参数的helpText、每条outcomeMessages现在和confirmText/successMessage一样是{{ }}模板槽:裸字符串解析成{ dialect: 'template', source },objectstack validate/compile/ 发布闸(422)拒绝其中的${…},并给出{{ }}写法。平台自带文案(邀请、重发邀请、改角色)和四种语言翻译已改成{{ }}。{{ }};第 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。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。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:dialogCopyOfhands the param-collection handler the action and params withdescription/ eachhelpTextreduced to their text (actionCopyText, new inactionCopyTemplate.ts); a locale map is handed on for the host to localize.composeSuccessMessagereads an outcome envelope'ssource.ui/packages/react/src/hooks/useActionTextLocalizer.ts: anoutcomeMessagesenvelope keeps its shape (a bundle entry replaces itssource) instead of collapsing to the word "template".packages/runtime/src/action-execution.tssummarizeActionParams: the MCP param description readshelpText's text, never "[object Object]".packages/cli/src/utils/i18n-extract.ts:description, paramhelpTextandoutcomeMessagesgo throughactionCopy(templateSlotSource), so the envelope seeds exactly as the bare string.ui/packages/app-shell/src/views/metadata-admin/previews/ActionPreview.tsx:localizereads the envelope'ssourceandhelpTextgoes 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:descriptionand eachoutcomeMessagesvalue areactionCopy(...)slots with the inline locale map;ActionParamSchema.helpTextisactionParamHelpText(), the same slot without a map (it was a plain string). Describes teach{{ }}.packages/spec/src/ui/action-copy-template.ts:ActionCopyKeywidened to the three keys, each with its own refusal wording and example;ActionRowCopyKeykeeps 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 ondescription, eachoutcomeMessages.OUTCOMEand eachparams[N].helpText; an inlineproperties.actionon its params'helpText. Roots:actionConfirmTemplateRoots(action)for the dialog copy,ACTION_SUCCESS_TEMPLATE_ROOTSfor an outcome. Every finding is an error, so the publish gate answers 422.packages/spec/src/system/i18n-resolver.ts:ActionLiketypes the envelope on the three keys.template-action-dialog-copyandtemplate-action-outcome-message.liveness/action.jsonoutcomeMessages evidence names{{ }}. Reference docs regenerated.templateSlotSource(copyAsWritten), as theirconfirmTextreads already did.3. Migration and producers (
a8f9f1c7).action-dialog-outcome-copy-template-holes. No conversion: there is no copy-hole conversion family inos migrate meta, and a${…}token held an expression (${record.user_id?.name ?? 'this member'}), which has no mechanical{{ }}spelling.invite-outcome-messages.ts(sent/link_only, invite and resend),sys_invitation.resend_invitationandsys_member.update_member_roledescriptions ({{ record.user_id.name | default:'this member' }}), bothpermission_set_grantedoutcomes, the fourobjects.generatedbundles (en regenerates identically;check-i18n-bundlesin sync), the action form'soutcomeMessageshelp (action.form.ts+ fourmetadata-forms.generatedbundles), andcontent/docs/ui/actions.mdx.27948-action-copy-template-slots.md(@objectstack/specand@objectstack/lintminor; platform-objects, runtime, cli patch; ADR-0087registered, FROM → TO list) and27948-action-copy-envelope-readers.md(@object-ui/core,@object-ui/react,@object-ui/fieldspatch).4. Fields reader (
408466db).ui/packages/fields/src/resolveActionParams.tsreadshelpTextthroughactionCopySource; pinui/packages/app-shell/src/utils/resolveActionParams.helpTextEnvelope-27948.test.ts(3 tests; reverse 2 failed, the type pin passed).Tests
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{{ }}).action-description.test.tsandaction-outcome-messages.test.tsexpect the envelope from a parsed bare string.Acceptance notes
@object-ui/reactuseActionTextLocalizer; runtimesummarizeActionParams; clii18n-extract; app-shellActionPreview.tsx;@object-ui/fieldsresolveActionParams(commit 4,408466db, named by CI "Type Check UI":RawActionParam.helpTextisunknownand read throughactionCopySource; a field-backed param whose envelope has no text falls back to the field's help). Plus core's ownActionRunner(dialogCopyOf,composeSuccessMessage). App-shell'suseConsoleActionRuntime/RecordDetailView/ActionParamDialogreaddescription/helpTextfrom what the runner hands the handler, so they get strings and do not move.action-icon.tsxtooltip (ui/packages/components/src/renderers/action/action-icon.tsxlines 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:useActionTextLocalizeralways fillslabel(falling back to the name), a page row'sdescriptionis not a slot, andUIActionSchemastill typesdescriptionas a string. Seat ruling: leave it.408466dbover non-test sources inui/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) forwarddescriptionandoutcomeMessagesintoexecute(), and the runner reduces them.src/service/actions/knowledge_article.actions.tslines 121-123 and 144-146 (sixoutcomeMessagesentries with${result.helpful}/${result.notHelpful}), andsrc/sales/translations/{en,es-ES,ja-JP,zh-CN}/objects.service.ts. Roots areresult, so each converts to{{ result.helpful }}/{{ result.notHelpful }}.defaultvs??.{{ 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.out/27948-step2-surface-stop.mdlisted: thelink_onlyoutcomes and bothpermission_set_grantedoutcomes (${result.grantedPermissionSet.label}). All onrecord/result, all converted.{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.packages/plugins/plugin-auth/src/role-change-granted-set.tsline 29 still names${result.grantedPermissionSet.label}in a comment (outside the surface). The ui console pinActionParamDialog.invitationScreens-27521.test.tsxcarries its own${…}literal, which the console still reads.ActionLike/ActionCopyKeywidening is a type-level change for TypeScript readers of a parsedActionSchema; the changeset namestemplateSlotSourceas the reader.Verification
Readings at head
a8f9f1c7(merge base776c1d87).Tests
pnpm --filter @objectstack/spec test: Test Files 773 passed, 1 skipped; Tests 22871 passed.packages/lintvitest: Test Files 183/183, Tests 7699/7699.packages/platform-objectsvitest: Test Files 104/104, Tests 1474/1474.packages/clivitesttest/i18n: 44/44 files, 419 tests.packages/runtimevitestsrc/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.tsc --noEmit, runtime and clitypecheck,@object-ui/coreand@object-ui/reacttype-check: clean.node scripts/check-i18n-bundles.mjs: all bundles in sync.Gates
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 --commandsata8f9f1c7: 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.🤖 Generated with Claude Code
Generated by Claude Code