Skip to content

feat(spec,lint): an action's confirmText, successMessage and errorMessage are template slots over the roots the console runner binds - #25637

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-24814-action-copy-template-slots
Oct 24, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-24814-action-copy-template-slots

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24814

Clause-②: yes (widening)
Metadata-protocol: semantic
Ruling-ref: 6365736632

An action's person-facing copy (confirmText, successMessage, errorMessage) is now an ADR-0032 template slot over the roots the console's action runner binds. This is ruling A's second half: the console moved first (objectui #13409, in the pinned .objectui-sha 2958f17e9c6a), and this PR types the action-copy family. It uses the #24947 form: the blank arm is kept, door findings are warnings, and the judge prescribes {{ }} for a ${…} token. The refusal half is its own BREAKING card, to be filed once this lands. Nothing here refuses text that parses today.

What changes

  • The 9 row slots (action:button, action:icon, the action:group / action:menu member, × confirmText / successMessage / errorMessage, ui/component.zod.ts) and ActionSchema.confirmText / successMessage (ui/action.zod.ts, which InlineActionSchema picks) are template slots. A bare string is shorthand for { dialect: 'template', source }. The envelope is accepted, and a blank string still parses. ActionSchema keeps its inline locale map arm.
  • Roots, measured at the runner (objectui ActionRunner at 2958f17e9c6a, confirmMessage / composeSuccessMessage / handlePostExecution), and declared in @objectstack/spec/ui:
    • confirmText: record, plus params only beside an object of static values (actionConfirmTemplateRoots). The runner binds params to action.params minus _rowRecord. An input list binds nothing, because its values are collected after the confirmation.
    • successMessage: result (the handler's payload) and record (ACTION_SUCCESS_TEMPLATE_ROOTS).
    • errorMessage: none (ACTION_ERROR_TEMPLATE_ROOTS). The runner shows an authored errorMessage as written. Re-measured at the pin: it fills no hole, as the earlier report said.
  • The judge (automation/flow-text-slot-template.ts closedScopeTemplateRefusal) reads a ${…} token with its $. Delete ${record.name}? is prescribed Delete {{ record.name }}?, no longer Delete ${{ record.name }}?. A zero-root scope names every hole and token in its text as literal text. The flow text slots' judge (textSlotTemplateRefusal) is unchanged, and the bulk sentence keeps its single-brace wording (pinned).
  • The doors: a new pass, packages/lint/src/validate-action-copy-templates.ts actionCopyTemplateIssues, runs inside validateStackExpressions. That is objectstack validate, compile and the publish gate. It covers registered actions (top-level, and an object's own, deduplicated), the page action rows and a page component's inline properties.action, and judges each locale of an inline map on its own. page joins the rule's runtimeTypes, and on a page write this pass runs alone. Every finding is a warning, so it shows as an advisory at the gate and never as a 422.
  • The platform's sentences, converted: the 4 invite_user sentences the ruling names, and the 9 sys_approval_request decision toasts (see Deviations). They move from ${result.*} to {{ result.* }} in the source and in every shipped locale bundle. The en bundles were regenerated with check-i18n-bundles --write.
  • i18n: ActionLike.confirmText / successMessage in system/i18n-resolver.ts widen to the envelope. resolveActionConfirm / resolveActionSuccess answer the authored value as written when no translation applies. os i18n extract seeds an envelope like the string (actionCopy, through templateSlotSource).
  • Expression-surface ledger: the census (packages/qa/dogfood/test/expression-conformance.test.ts) scans only *.zod.ts files. So the template input is built at each declaring .zod.ts file, and all 11 positions are discovered and classified: template-action-confirm-text, template-action-success-message, template-action-error-message. A first cut built them in a plain .ts module, and the census never saw them. That is why the construction lives where it does.

Measured

  • Renderer, re-verified at the pin (.objectui-sha 2958f17e9c6a, objectui 8e90779dcb merged in): actionCopySource / fillTemplateHoles (packages/core/src/actions/actionCopyTemplate.ts) read the envelope for all three keys. useActionTextLocalizer hands the envelope on, and flowSuccessCopy reads it.
  • Producers (authored confirmText / successMessage / errorMessage in examples/** + packages/**, tests and generated bundles excluded): at e649c2d91e, 13 carried ${…} (4 platform-objects, 9 plugin-approvals) and 0 carried {{. On this branch, 0 carry ${…} and 13 carry {{. In examples/**, 0 either way.
  • Downstream shape check (the unlock's instruction): OBJECTUI_ROOT=../objectui bash scripts/build-console.sh, with objectui at 2958f17e9c6a and this branch's spec built (pre-merge head 3c1eb1e0). The console's tsc && vite build and app-shell's tsc completed. Turbo: 36/36 then 34/34 tasks successful. The canaries passed, including "✓ Console bundle carries THIS tree's @objectstack/spec, and only it". The merge brought no spec type change this check reads (only spec(ui): record:details hideEmpty: false — in read-only mode an all-empty section renders its heading plus one fold row; describe() says so #25612's hideEmpty describe).

Tests

  • packages/spec: new src/ui/action-copy-template.test.ts, plus new cases in src/automation/flow-text-slot-template.test.ts (${…}, zero-root, bulk control). Re-pinned: ui/action.test.ts, ui/component-report-items-action-members-typed.pin.test.ts (the copy keys written as envelopes stay byte-identical, and the bare-string normalisation is pinned), and ui/component-props-unknown-members.pin.test.ts (the envelopes' ast). Full suite before the restructure: 789 of 793 files passed, 3 failed, 1 skipped (the 3 were the fixtures above plus the export-origins artifact), and all 3 are fixed. After the restructure, src/ui/, src/system/ and the judge: 211/211 files, 7614 tests.
  • packages/lint: new src/validate-action-copy-templates.test.ts (13). It drives the real runRuntimeAuthoringRules on a page write and an action write, and asserts one advisory and zero errors. Re-pinned: the meta-test's import-specifier artefact (templates) and runtime-gate.page-visibility-predicate-writes.test.ts (validateStackExpressions now dispatches on page). Full suite: 159/159 files, 7250 tests.
  • packages/platform-objects: 94/94 files, 1343 tests. packages/plugins/plugin-approvals: 95/95 files, 1244 tests (token pins moved to {{ }}, and the parsed copy is read through templateSlotSource).
  • packages/cli unit tier: 327/327 files, 4812 tests.
  • packages/metadata-protocol protocol.runtime-authoring-gate.test.ts (197), packages/rest meta-view-container-i18n.test.ts (11), packages/runtime action-execution-destructive.test.ts (63), services/service-i18n authored-translations.test.ts (16): all pass.
  • packages/qa/dogfood expression-conformance.test.ts: 8/8. member-transfer-ownership-step-down.dogfood.test.ts now reads the served copy through templateSlotSource. NOT MEASURED locally: it boots the showcase. CI's battery runs it.

Gates

All runs are at head e875a564 (the merged head). The derivation is node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 119 families, against merge base 481ac6211. Each was run with its exit code captured before any pipe. 115 exited 0 on the first pass. 4 refused with exit 3 (PREREQUISITE NOT MET, nothing measured): check-plugin-teardown-shape --self-test (a shallow clone; I fetched the pinned fixture commit), spec check:skill-examples, check:dual-build-cjs-loads and check:i18n-coverage (unbuilt packages). All 4 exited 0 after turbo run build (77/77) and the fetch. --ran with cmd :: exit N reports: "119 derived famil(ies) accounted for — 119 run, 0 NOT-MEASURED (a DERIVED zero)".

Also run: pnpm --filter @objectstack/spec check:generated reports "All 14 generated artifacts are up to date". typecheck exited 0 on spec, lint, cli, platform-objects, plugin-approvals and dogfood. The local-only check:dev-prereqs and check:skill-frame-freshness both exited 0. The full lint battery and the repo-wide pnpm lint are CI's.

One ablation: isDollarToken was forced to return false through scripts/ablation-replace.mjs. The anchor went 1 to 0, and the blob went e12c4385 to 38c36986. flow-text-slot-template.test.ts then had 4 failed and 41 passed. The restore left the blob back at e12c4385 and git diff HEAD empty.

Deviations

  • plugin-approvals, 9 sentences, beyond the ruling's "4". The ruling's count came from the earlier report, which missed packages/plugins/plugin-approvals/src/sys-approval-request.object.ts (9 Approved "${result.request.record_title}"-style toasts). Left alone, the platform would ship 9 sentences its own doors warn on. This conversion meets the four conditions for a bounded in-place fix: the same defect class, a mechanical conversion of the same form, no other open PR touches the file (all open PRs' file lists were read, and only spec(ui): record:details hideEmpty: false — in read-only mode an all-empty section renders its heading plus one fold row; describe() says so #25612 overlaps, on component.zod.ts), and the same gate family. The claim's file surface needs that file and its translations added. The seat should write that addendum, not me.
  • Translated-locale values edited by hand (zh-CN / ja-JP / es-ES *.objects.generated.ts, successMessage leaves only). AGENTS.md allows this. None of those leaves has a source-hash entry.
  • packages/cli/src/commands/explain.ts: confirmText's row reads template (i18n), with the hole spelled out.

Acceptance notes


Generated by Claude Code

…mplate slots; convert the platform's ${result.*} sentences

Claude-Session: https://claude.ai/code/session_01DcqhBfbrsH8ACDsMQ62Hzq
Co-authored-by: Claude <noreply@anthropic.com>
…ing .zod.ts files; classify them in the expression-surface ledger

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/cli, @objectstack/lint, @objectstack/platform-objects, @objectstack/plugin-approvals, @objectstack/spec, touching 62 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json, packages/spec/src/ui/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

30 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 e8de8a7ece08da5875cb50186eed3b6509a367c8.

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

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json, packages/spec/src/ui/index.ts) — 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, 43 pages)
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 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 — 151 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 e8de8a7ece08da5875cb50186eed3b6509a367c8 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json e8de8a7ece08da5875cb50186eed3b6509a367c8

⚠️ 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 e8de8a7ece08da5875cb50186eed3b6509a367c8 → 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

席位核对

ACCEPT · #24814 · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-24T12:23Z · head e875a56445 · report 6383834597 · seat review at TIER_DEFAULT. Both clause-② limbs hit (path: ui/component.zod.ts, ui/action.zod.ts, ui/action-copy-template.ts, automation/flow-text-slot-template.ts, system/i18n-resolver.ts, ui/index.ts; declaration: Clause-②: yes), so a same-shape ## Contract review PASS at CONTRACT_REVIEW_TIER on the landing head is still owed. needs:contract-review is added in this stroke.

  1. Shape. Draft, base main, first line Fixes #24814, no other closing keyword. Clause-②: yes (widening), Metadata-protocol: semantic and Ruling-ref: 6365736632 are line-initial in the body. The changeset carries both declarations, at minor on spec, lint, cli, platform-objects and plugin-approvals (the feat(spec,lint): a bulk action's confirmText is a template slot over count and its params, judged at validate, compile and the publish gate #24947 precedent's form). 46 files, +1209/−179 (get_files), not governed, under 3000 lines (check-governed-merges --pr 25637). The claim surface was amended this round (6384157751) for the plugin-approvals, dogfood and cli/explain.ts files the PR declares.
  2. The ruling, measured on the diff. Letter A: the console first (met: .objectui-sha 2958f17e9c6a, unlock 6377096620), then one action-copy family in the feat(spec,lint): a bulk action's confirmText is a template slot over count and its params, judged at validate, compile and the publish gate #24947 form. The 9 row slots (component.zod.ts, actionRowCopy) and ActionSchema.confirmText / successMessage (action.zod.ts, keeping the inline locale map) are template slots; the blank arm is kept. Door findings are warnings (advisory at the publish gate, no exit-code change). The judge now prescribes {{ x }} for a ${x} token. The 4 named ${result.*} sentences are converted. Roots were measured at the pinned runner: confirmText over record (plus params only beside static values), successMessage over result then record, and errorMessage over none. emptyText is untouched (out of scope by the ruling). The parsed shape moves from string to envelope; the changeset says so, templateSlotSource reads both, and the downstream shape check the unlock asked for passed (build-console.sh with objectui at 2958f17e9c6a against this branch's spec, canary green).
  3. Gates. Check-runs on e875a56445: 36 success, 6 skipped, 0 failed (CI converged). Local, per the report: --ran 119/119 with no NOT MEASURED; spec, lint (159 files), platform-objects (94), plugin-approvals (95), cli unit (327) and the named metadata-protocol / rest / runtime / service-i18n / dogfood conformance suites all green. The dogfood boot test member-transfer-ownership-step-down is left to CI's battery (declared). Ablation on isDollarToken: 4/45 red, then restored. Landing waits for the contract-review PASS.
  4. Deviations accepted. (a) The in-place extension to the 9 sys_approval_request toasts (${result.request.record_title}) beyond the ruling's "4". Four conditions checked: same defect class, mechanical, no other claim (PR fix(plugin-approvals): an SLA reassign to a target that resolves to no one keeps the slate #25600 on plugin-approvals has merged), same gate family. The claim surface was amended the same round, and the PR body names it with the 13-producer census. (b) Files outside the original surface that the new type needs: the dogfood conformance ledger rows and boot test, and cli/src/commands/explain.ts. (c) The slot's template input is built in each declaring .zod.ts (the expression-surface census scans only *.zod.ts). (d) Model-free trailers and the role file's PR footer. mcp_calls 0; relay writes 2.
  5. Open questions and out-of-scope outcomes.
  6. Landing. Not governed, 1388 changed lines. Once the CONTRACT_REVIEW_TIER PASS is on record and every check on the then-current head is green, the seat sends pr_ready + automerge_enable through the relay. At the merge the seat files the refusal-half card.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review absent: tier quota wall · domain:spec seat 2 · 2026-10-24T12:26Z

The CONTRACT_REVIEW_TIER reviewer for this head (e875a56445) was dispatched and stopped before writing anything. The host answered HTTP 429, rate_limit: the account's quota for the model behind CONTRACT_REVIEW_TIER is used up, with no reset time given. The rule for a mandatory tier is to wait for it, not to step down: references/platform-readings.md 「强制档不因临缺授权降档…不达档起隔离达档子代理或等档」, and references/landing-operations.md 「子代理起不来 ⇒ 复核缺席,PR 带 needs:contract-review 留 draft 队列外;旁路仅维护者逐次亲审」. So this PR stays draft with needs:contract-review. The review re-runs on this head (or the then-current one) when that tier answers again. The only bypass is the maintainer's own review of this PR.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Claim: cross-account contract review of this PR at head e875a56445 · domain:server seat 1 (seat post #6367, the Sales account's project) · 2026-10-24T14:15Z
Session: session_01JrXUZYyTV4i588KLZRxSvJ
Account: os-sales

⛔ Not a claim on the card and no label change now. Under the cross-account review patrol (is:pr is:open label:needs:contract-review, PRs not signed by this account), this seat runs the CONTRACT_REVIEW_TIER review per contract-review.md as an isolated subagent; its ## Contract review record lands on this PR. On PASS the record's writer removes needs:contract-review in the same stroke; on FAIL the label stays. If another account's review is already under way, say so here and this seat yields.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e875a56445ed6ccec00d46a7edaf8d37baaa2529
Local-runs: none
Scope: packages/spec/src/automation/flow-text-slot-template.ts, packages/spec/src/system/i18n-resolver.ts, packages/spec/src/ui/action-copy-template.ts, packages/spec/src/ui/action.zod.ts, packages/spec/src/ui/component.zod.ts, packages/spec/src/ui/index.ts + packages/lint/src/validate-action-copy-templates.ts, packages/lint/src/validate-expressions.ts, packages/lint/src/authoring-rules.ts, packages/lint/src/runtime-gate.ts, packages/lint/src/page-walk.ts, packages/spec/src/shared/typed-expression-input.ts, packages/spec/src/shared/expression.zod.ts, packages/spec/src/ui/i18n.zod.ts, packages/spec/src/ui/bulk-action.zod.ts, packages/cli/src/utils/i18n-extract.ts, packages/cli/src/commands/explain.ts, packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json, packages/qa/dogfood/test/expression-conformance.ledger.ts, .changeset/24814-action-copy-template-slots.md, .objectui-sha, docs/adr/0032-unified-expression-layer.md, and the 13 converted producers with their 8 locale bundles (packages/platform-objects/src/identity/sys-invitation / sys-member / sys-organization / sys-user .object.ts, packages/plugins/plugin-approvals/src/sys-approval-request.object.ts, the en / es-ES / ja-JP / zh-CN *.objects.generated.ts under packages/platform-objects/src/apps/translations and packages/plugins/plugin-approvals/src/translations). The rest of the diff (tests and pins, the four regenerated reference pages) is the lane seat's ACCEPT at TIER_DEFAULT (6384169307).

Cross-account review: implemented under domain:spec seat 2 (sunhaolin, cloud dev session session_01DcqhBfbrsH8ACDsMQ62Hzq); reviewed for domain:server seat 1 (os-sales) per claim 6385268492, the review the quota-wall note 6384195279 left owed. Inputs: card #24814 (body and every comment, ruling 6365736632 and unlock 6377096620 included), PR #25637 (body, every comment, file list, net diff against main at this head), and this head's check-runs. Not read: the dispatch order, the dispatching seat's own conclusions, objectui.

① Derived judgments

Every accept-set and public-surface change the diff implies, each judged:

  1. The 9 page action-row slots (ActionButtonPropsSchema, ActionIconPropsSchema, actionContainerMemberShape, each × confirmText / successMessage / errorMessage, ui/component.zod.ts): z.string() becomes actionCopyTemplateSlot(templateExpressionInput(ExpressionSchema, …), key), the template input plus a blank-string arm. Read in shared/typed-expression-input.ts: a non-blank string transforms to { dialect: 'template', source }, an envelope must declare dialect: 'template' (ExpressionSchema.safeExtend), and the blank arm z.string().regex(/^\s*$/) takes exactly the strings NON_BLANK_STRING refuses. Every string that parsed before still parses; the envelope is newly accepted; a non-string was refused before and is refused now. Widening at the parse: right. The parsed shape of a non-blank string moves from string to envelope, the semantic half the changeset names. Same form as the bulk precedent (BulkConfirmTextSchema, ui/bulk-action.zod.ts:389), as the ruling orders.
  2. ActionSchema.confirmText / successMessage (ui/action.zod.ts:881, :1367, :1375): I18nLabelSchema (string or inline locale map) becomes the same slot with the locale-map arm kept. InlineLocaleMapSchema is keyed by INLINE_LOCALE_KEY (ui/i18n.zod.ts:113), which dialect and source fail, so a { dialect, source } object was refused before and the envelope is a pure addition; strings and maps keep parsing. Widening: right. InlineActionSchema picks both keys (:2294, :2295), so a component's inline properties.action widens the same way, and the regenerated InlineAction table says so. The spec: guard the confirmText + params PAIR at authoring time, once the 20 shipped sites have migrated to action.description #7428 refine (data.confirmText != null beside a non-empty params, :2048) reads the envelope exactly as it read the string: unchanged. ActionSchema.errorMessage stays I18nLabelSchema (:1424), the ruling's letter: right.
  3. The roots, declared in @objectstack/spec/ui (ui/action-copy-template.ts; six new names in api-surface/ui.json and export-origins/ui.json, additions only): confirmText over record, plus params only when action.params is a non-array object; successMessage over result then record; errorMessage over none. Internally consistent: a page row's params is z.unknown() (component.zod.ts:4061, :4163, :4311), so an object of static values is authorable there while a registered action's params array binds nothing; the describes, the lint pass and the ledger rows state the same three scopes. The runner-side half (what objectui's ActionRunner binds at .objectui-sha 2958f17e9c6a) is the PR's measurement, re-stated by the ledger rows as read at the pin and not run; it lies outside this review's inputs and agrees with the ruling (6365736632: the runner reads the envelope for the three keys and fills {{ }} holes) and with the earlier report's reading (6365644870: errorMessage interpolates nothing). Right, as a declared scope with its provenance written down.
  4. The judge (closedScopeTemplateRefusal, automation/flow-text-slot-template.ts:565 onward): a single-brace token preceded by $ is read as a ${…} token (isDollarToken), its prescription is the hole alone (closedDoubled drops the $), an unspellable token is quoted with its $, and a zero-root scope names every token and hole as literal text with no rewrite. This changes refusal prose for the bulk confirmText door too (a ${count} there is now prescribed {{ count }}, no longer ${{ count }}) and no accept set; the single-brace wording the bulk tests pin is kept (the control case). textSlotTemplateRefusal, the flow text slots' judge, is untouched. Right.
  5. The doors (lint/validate-action-copy-templates.ts, wired into runStackExpressionPasses in validate-expressions.ts; authoring-rules.ts adds page to the rule's runtimeTypes): registered actions (top-level and an object's own, de-duplicated by objectName:name), the three row surfaces, each action:group / action:menu member and a component's inline properties.action, each locale of an inline map at its own location; blank first, then closedScopeTemplateRefusal, then validateExpression('template', …). Every finding carries severity: 'warning', and runRuntimeAuthoringRules routes every non-error severity to advisories (runtime-gate.ts:1191), so no write is refused and no exit code moves. On a page write runStackExpressionPasses returns this pass alone; page writes ran no expression pass before, so nothing is lost and nothing can newly red. Right, and the ruling's "door findings as warnings" is met.
  6. system/i18n-resolver.ts: ActionLike.confirmText / successMessage widen to the envelope, and resolveActionConfirm / resolveActionSuccess now return ActionLike['confirmText'] instead of string | undefined. lookupActionField takes only a non-empty string from the bundle (:633), so an envelope is never mistaken for a locale map: a translated locale still answers a string, and untranslated the authored envelope is handed on, which the pinned console reads. The return-type widening reaches @objectstack/spec/system callers; Type Check · workspace is green, so no in-repo caller breaks. Right. PAGE_COMPONENT_COPY_KEYS (:2197) does not carry the three row keys, so page-row copy translation is unchanged.
  7. Producers: the 4 invite_user sentences the ruling names and the 9 sys_approval_request decision toasts move from ${result.*} to {{ result.* }} in source and in all four locales (en regenerated; the zh-CN / ja-JP / es-ES leaf values hand-edited, which AGENTS.md allows for translated-locale values). At this head no authored confirmText / successMessage / errorMessage outside tests and generated bundles carries ${…}; the three remaining ${…} matches (component.zod.ts:3566 to :3568) are TypeScript interpolations of the describe constants, not copy. The 9 beyond the ruling's "4" are the same defect class in the same gate family, the seat amended the claim surface for them (6384157751), and leaving them would ship platform metadata the new door warns on. Right. outcomeMessages keeps ${result.*}, as declared.
  8. Downstream shape: the parsed envelope is served where a string was. The only non-test readers of these keys in this repo are the resolver (item 6) and cli/src/utils/i18n-extract.ts (actionCopy reads the envelope through templateSlotSource and passes a locale map through), both converted; os i18n extract therefore seeds the envelope exactly like the string. The shipped console is pinned at 2958f17e9c6a (.objectui-sha at this head), which the card's unlock (6377096620) reads as carrying objectui PR feat(plugin-security): extend the packaged-permission-set lock to the restore leg of the write-through #13409's envelope reader. Right. Console Pin Gate is main's full-battery check and is skipped on the PR; the PR's own build-console.sh run is the dev's measurement, not a check-run.
  9. Generated artifacts: api-surface/ui.json, export-origins/ui.json and the four reference pages are regenerated, and TypeScript Type Check (the job that runs check:generated's gates) is green. Right. Cosmetic only: the reference type column renders the blank arm as a second string, as the bulk slot's row does.
  10. Not changed, and right to leave: emptyText ×2 (ruled out of scope), ActionSchema.errorMessage, outcomeMessages, onSuccess.navigate's ${result.*} URL scope, textSlotTemplateRefusal.

② Semver level

.changeset/24814-action-copy-template-slots.md: minor on @objectstack/spec, @objectstack/lint, @objectstack/cli, @objectstack/platform-objects and @objectstack/plugin-approvals, every published package the diff moves (@objectstack/dogfood is private: true; content/docs/references/** is not a package). Clause-②: yes (widening) and Metadata-protocol: semantic are line-initial in both the PR body and the changeset, and both match the diff: the accept set only grows (items 1 and 2), the parsed shape and the dialect of the three keys change meaning (the semantic kind outranks the added envelope arm), and no key, value or requirement that parsed before is refused. The kind takes minor; minor is declared. No ADR-0087 disposition is owed (nothing breaking). Check Changeset is green. Right.

③ Boundary flags

PR author objectstack-fleet[bot]; assignee sunhaolin; head repo is the base repo (not a fork); draft; 46 files, +1209 / −179; no governed path (the register's rows docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, docs/NORTH-STAR.md, .github/workflows/** are all absent from the file list).

Check-runs on e875a564: 47 runs, 37 success, 10 skipped, 0 failed, 0 pending. The five required contexts (Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard) are all success. Skipped by design on a PR run: Console Pin Gate, Temporal Conformance (live PG + MySQL), the dogfood shards and Dogfood Verify CLI, the two opt-in harnesses, and the second Auto Label / Check PR Size instances from the label push.

Dev flags (os-dev-report 6383834597 and the PR body's Deviations), each answered:

  • plugin-approvals ×9 beyond the ruling's "4": accepted, item 7; the claim addendum 6384157751 covers the files.
  • Files outside the claimed surface (ledger rows, the dogfood boot test, explain.ts, the approval test pins, translated-locale leaves): each is the new type's own consequence, covered by the same addendum.
  • Template inputs built in the declaring .zod.ts files rather than in action-copy-template.ts: the conformance census scans .zod.ts text (expression-conformance.test.ts:396 onward), so this is the only placement the ledger can see; right.
  • Harness attribution, no PR-activity subscription, no reverse type probe: process notes with no contract effect; the downstream console compile stands in for the type probe and is the dev's measurement.

Open questions (6383834597), each answered:

Out-of-scope findings, carried as noted: Studio's inspector and preview read the three keys as I18nLabel maps and would show an envelope as template (an objectui authoring surface, objectui #13409's note; no producer writes the envelope form today); the ledger's template-bulk-confirm-text row still says no evaluator (objectstack-ai/objectui#13183). The template-action-error-message row is experimental / unevaluated on purpose: the runner fills no hole there and no scope is ruled for the failure toast. One more of the same class, found on the consumer sweep and noted here so nobody rediscovers it: packages/metadata-protocol/src/protocol.ts:866 (HAND_CRAFTED_SCHEMAS.action) still declares confirmText / successMessage as type: 'string'; read at this head, that table is the Studio form's fallback descriptor, consulted only when the Zod derivation yields no JSON Schema and only to seed defaults and list property names (its own docblock), never to validate a write, so it has no contract effect and already lags the spec on visible the same way. Noted, not filed.

Landing notes for the owning seat: PR #25626 also edits platform-objects/src/identity/sys-user.object.ts and the four platform-objects locale bundles, so whichever lands second merges origin/main and regenerates the bundles with check-i18n-bundles --write; after the merge, main's full battery runs Console Pin Gate against the pin, the one check this PR could not carry itself.

Review mechanics: the shared checkout's FETCH_HEAD moved under this review between two reads (another fetch landed), so every cited-code read above was redone against an owned ref (refs/review/pr-25637, verified at e875a564), and the diff reviewed is byte-identical to that ref's diff against main.

Implemented-by: claude/issue-24814-action-copy-template-slots
Reviewed-by: session_01JrXUZYyTV4i588KLZRxSvJ

VERDICT: PASS


Generated by Claude Code

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

Projects

None yet

2 participants