Skip to content

feat(core): an action's param dialog description, helpText and outcomeMessages fill {{ }} template holes, and still fill ${…} - #28436

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

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Part of #27948

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:控制台的动作文案里,参数对话框的 description、每个参数的 helpText、以及 outcomeMessages 现在也会填 {{ record.x }} / {{ result.x }} 这种模板洞(含 {{ amount | currency:USD }} 格式化),和 confirmText / successMessage 用同一个模板引擎;原来的 ${…} 写法照旧能填。
  • 为什么改:规范要求带参数的动作把确认问句从 confirmText 挪到 description,可挪过去后 {{ record.x }} 原样显示成花括号;裁决 A(「同意」)定五个文案键统一成 {{ }},渲染端先行。这是第 1 步。
  • 风险与回滚:只改 @object-ui/core 的动作运行器;一句话里有 {{ }} 就只走模板引擎(同句里的 ${…} 当普通文字),没有就走旧的 ${…},现有文案行为不变;回滚即 revert 本 PR。
  • 先看:ui/packages/core/src/actions/ActionRunner.ts(dialogCopyFiller、composeSuccessMessage 第一档)、ui/packages/core/src/actions/actionCopyTemplate.ts(fillTransitionalCopy)、ui/packages/core/src/actions/tests/ActionRunner.transitionalCopy-27948.test.ts。
  • 两分钟验证:cd ui && pnpm exec vitest run packages/core/src/actions/__tests__/ActionRunner.transitionalCopy-27948.test.ts,预期 15/15 通过。
  • 需要你做的:无,席位落地。

What changes

Step 1 of #27948, ruling 6545229148, letter A (renderer first). The five action copy keys become one ADR-0032 {{ }} template slot. confirmText and successMessage already read it; the param dialog's description, each param's helpText, and outcomeMessages read only ${…}. So an author following the spec (move the confirm question of an action with params from confirmText to description) saw {{ record.x }} printed raw, from the record header and from a row.

  • ui/packages/core/src/actions/actionCopyTemplate.ts: fillTransitionalCopy(text, fillTemplate, fillLegacy), one dialect per sentence, the rule confirmText / successMessage kept through their own transition (objectui#13367 until objectui#13476): a sentence with a {{ }} hole is filled by the template engine alone, so a ${…} token in it stays text and a filled value is never read a second time; a sentence with none keeps its ${…} reading.
  • ui/packages/core/src/actions/ActionRunner.ts:
    • The interpolate handed to the param-collection handler is now dialogCopyFiller(action): {{ }} holes fill over record (the row on a row dispatch, the page's record on the header) and the action's static params, exactly the roots confirmText reads (factored out of confirmMessage as recordCopyRoots); ${…} still goes through templateEvaluatorFor (objectui#12967). Both handlers (useConsoleActionRuntime, RecordDetailView) already pass that interpolate to the dialog for description and helpText, so no app-shell code moves.
    • composeSuccessMessage rung 1 (outcomeMessages): {{ }} holes fill over result and record, the roots successMessage reads, with the viewer's locale and tenant currency; ${result.*} still goes through substituteScopeTokens.
    • Doc comments on ActionDef.outcomeMessages, ParamCollectionHandler, RUNNER_TEXT, templateEvaluatorFor and substituteScopeTokens say which spelling each reads.
  • .changeset/27948-action-copy-template-holes.md: @object-ui/core patch.

No packages/spec/** change: the describe text, the publish refusal of ${…} and the migration of existing ${…} copy are step 2, after this is on main.

Tests

ui/packages/core/src/actions/__tests__/ActionRunner.transitionalCopy-27948.test.ts, 15 pins through the real runner and @objectstack/formula's templateEngine, reading what the host is handed (the dialog's interpolate output and the toast handler's argument):

  • description + helpText, on the header door and the row door (params._rowRecord, as ObjectGrid dispatches): the card's case (the confirmText sentence Pause {{ record.display_name }}? reads the same moved to description), a ${record.x} hole still fills, an unknown {{ x }} hole reads exactly as confirmText reads it; a formatter hole; a mixed sentence ({{ }} wins, ${…} stays text); a filled value is not re-read as ${…}.
  • outcomeMessages: {{ result.x }}, {{ record.x }} from the row, a formatter hole, ${result.x} still fills, an unknown {{ x }} reads as successMessage reads it, a mixed sentence.

Reverse check against the unfixed runner (ActionRunner.ts and actionCopyTemplate.ts restored from 5da3a8ce, test file kept): 12 failed, 3 passed. The 3 that stay green are the ${…} keepers, which pin preservation and are meant to pass on both. Restore proven by an empty git diff HEAD.

Acceptance notes

  • Order inside one sentence. One dialect per sentence, as the ruling's "decide and pin the order" asks: a {{ }} hole makes the whole sentence a template; ${…} in it is text. This is the reading confirmText / successMessage had during their transition, so step 2's refusal of ${…} changes nothing for a sentence that already has a {{ }} hole.
  • Roots. The dialog copy reads record and static params (not the collected values, which do not exist yet, the same as confirmText); outcomeMessages reads result and record. The ${…} path keeps its wider scope (the whole runner context) until step 2 retires it; step 2's migration should check producers for ${user.*}-style holes, which have no {{ }} root here.
  • Envelope. These three keys are still read as strings: description / helpText are localized in app-shell before interpolate, and outcomeMessages entries are read only when they are strings. If step 2 types them as template slots that parse to the { dialect, source } envelope, the readers must accept the envelope first (the trap objectui#13367 closed for confirmText / successMessage), or a served entry silently falls back.
  • Registered producers using ${result.*} in outcomeMessages / successMessage (platform-objects' invitation copy) are untouched; ${…} keeps filling.

Verification

Readings at head 164cbe99 on objectstack-ai/objectstack (merge base 5da3a8ce).

Tests
  • cd ui && pnpm exec vitest run packages/core/src/actions/__tests__/ActionRunner.transitionalCopy-27948.test.ts: Tests 15/15 passed.
  • cd ui && pnpm exec vitest run packages/core/: Test Files 224/224, Tests 4197 passed, 27 skipped.
  • cd ui && pnpm exec vitest run packages/app-shell/src/views/ActionParamDialog packages/app-shell/src/views/ObjectView.inviteOutcome-27797 packages/app-shell/src/hooks/: Test Files 102/102, Tests 886/886.
  • pnpm --filter @object-ui/core type-check (sources and tsconfig.test.json): lock VERDICT command-exit 0.
Gates
  • Build: env -u OS_SKIP_DTS pnpm turbo run build --filter='@object-ui/app-shell^...' --filter='!@object-ui/*' (37/37 tasks) and --filter='@object-ui/core^...' (3/3), lock VERDICT command-exit 0 both.
  • dispatch-gates --commands at 164cbe99: 34 families, each run with stdin from /dev/null, exit codes recorded before any pipe; all 34 exit 0 (pnpm --dir ui exec vitest run scripts/: Test Files 169 passed, 2 skipped). --ran: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN.
  • origin/main moved to 630b4078 after branch-off; none of the incoming files is this PR's and git merge-tree against it is clean.
  • The full CI farm (test:repo, the UI lanes, lint) is CI's.

Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 31, 2026
@objectstack-fleet objectstack-fleet Bot added the needs:contract-review Marker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides label Oct 31, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 164cbe9991292ec8de9a297f70d16b5bf2d32ea6 · 2026-10-31T16:19Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 164cbe9991292ec8de9a297f70d16b5bf2d32ea6
Local-runs: none
Scope: ui/packages/core/src/actions/ActionRunner.ts (dialogCopyFiller, recordCopyRoots, composeSuccessMessage rung 1, docblocks), ui/packages/core/src/actions/actionCopyTemplate.ts (fillTransitionalCopy), the new test ui/packages/core/src/actions/tests/ActionRunner.transitionalCopy-27948.test.ts (15 pins), .changeset/27948-action-copy-template-holes.md (@object-ui/core patch). 4 files, +276/−34. Part of #27948, step 1 of ruling 6545229148 letter A (renderer first); dispatch claim 6545314289, dev report 6545766362.
Inputs: card #27948 body + ruling + claim + dev report, the PR body, the PR diff at this head, git grep on origin/main (ActionRunner.ts readers at the merge-base 5da3a8ce; main has moved to 630b407 with none of these 4 files), check-runs (read 2026-10-31T16:23Z: all 55 latest-per-name check-runs completed, 42 success + 13 skipped, none failed, none running) — 55 checks. Nothing built or run here; the seat's numbers (15/15, core 224 files / 4197 tests, app-shell 886/886, reverse check 12 failed / 3 passed against the unfixed runner, 34 derived gates) come from the dev report.

① Derived judgments

  • No spec surface moves. Clause-②: no, Metadata-protocol: none are right: no packages/spec/** path, no describe text, no publish refusal. Step 2 (spec narrowing of the five copy keys to the template slot, ${…} refusal, migration) is explicitly deferred until this is on main, as the ruling's "renderer first" orders.
  • Three readers gain the {{ }} dialect, none loses ${…}. On main the param dialog's interpolate is String(templates.evaluate(text) ?? '') (ActionRunner.ts L1708-1712) and outcomeMessages rung 1 is substituteScopeTokens(outcomeCopy, { result }) (L1951). At head both go through fillTransitionalCopy(text, fillTemplate, fillLegacy): a sentence with a {{ }} hole (hasTemplateHoles, the existing TEMPLATE_HOLE test) is filled by fillTemplateHoles alone; one with none takes the exact pre-change path. So every existing ${…} sentence reads as before — pinned by the three ${…} keepers that also pass on the unfixed runner.
  • Roots match the sibling keys. Dialog copy fills over recordCopyRoots(action) = { record: dispatchRecord(action), params: staticParams } — the same object confirmMessage built inline on main (L1986-1999, now factored out, _rowRecord still stripped); outcomeMessages fills over { result: payload, record: dispatchRecord(action) } — the same resultRoots successMessage rung 2 reads (main L1956-1961). Locale and tenant currency are passed as for confirmText/successMessage, so | currency:USD formats. The row door (params._rowRecord) and the header door (record in the runner context) are both pinned.
  • One dialect per sentence is a deliberate pin, not an accident. A ${…} token inside a {{ }} sentence stays text, and a value filled into a hole is never re-read as ${…} (test "never read again") — the reading confirmText/successMessage had during objectui#13367→[finding] after the #13279 repair, an UNRESOLVABLE data engine still answers 403 FORBIDDEN — the last surviving GRANTS-LOST disguise at the package door #13476, and the one step 2's refusal makes moot. The unknown-hole case reads identically to confirmText/successMessage (pinned by equality, not by a literal).
  • Changeset. @object-ui/core patch is the ui-package convention; the body names the three keys, the transition and the ruling.

② Rulings cited

③ Narrowing / widening

  • None at the contract: the console accepts one more hole spelling on three keys. No served document changes meaning; nothing refused that parsed before.

④ Behaviour / flags

  • Envelope trap for step 2 (carried, not blocking): description/helpText/outcomeMessages are still read as strings. If step 2 types them as template slots that parse to { dialect, source }, these readers must accept the envelope first (actionCopySource), or a served entry falls back silently — the objectui#13367 trap, named in the PR's acceptance notes.
  • Registered producers' ${result.*} copy (platform-objects invitation outcomes) keeps filling; step 2's migration must look for ${user.*}-style holes with no {{ }} root.
  • The wider ${…} scope of templateEvaluatorFor stays reachable only from hole-less sentences until step 2 retires it.

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

@objectstack-fleet objectstack-fleet Bot removed the needs:contract-review Marker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides label Oct 31, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D

维护者速读

  • 改了什么: 动作参数对话框的说明(description)、各参数的提示(helpText)和结果提示(outcomeMessages)现在能填 {{ record.x }} / {{ result.x }} 这种插值,和 confirmText、successMessage 用同一个模板引擎;原来的 ${…} 写法照常生效。
  • 为什么改: 你裁定 Action copy takes two hole syntaxes: description, param helpText and outcomeMessages read ${…}, confirmText and successMessage read {{ … }} #27948 选 A:五个动作文案键统一为 {{ }},渲染端先行。这是第一步,过渡期两种都认。
  • 风险与回滚: 只改界面渲染,spec 契约不变,已有 ${…} 文案照常填。同一句里出现 {{ }} 就按模板处理,其中的 ${…} 原样显示。回滚即 revert。
  • 先看: ui/packages/core/src/actions/ActionRunner.ts。
  • 两分钟验证: 跑 PR 正文“验证”一节列的 core 动作测试,预期全绿。
  • 需要你做的: 无。不属于受管面,契约复审已 PASS,由席位落地。第二步(spec 收口、发布拒绝 ${…}、迁移)在本卡上接着做。
  • 席位意见: ACCEPT,可落地。

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

Head 164cbe9991292ec8de9a297f70d16b5bf2d32ea6. Card #27948 step 1 of 2 (renderer first), claim 6545314289; ruling 6545229148, letter A (maintainer 「同意」 2026-10-31T15:10Z).

  • Shape: draft to main, made by the relay; Part of #27948 (the card stays open for step 2); Clause-②: no, Metadata-protocol: none at line start. Changeset @object-ui/core patch.
  • Contract review: PASS at CONTRACT_REVIEW_TIER by the lane review thread, record 6546030702 on this head, reviewer not the drafter. No must-fix. Its notes for step 2 (readers must accept the parsed envelope before the slots are typed; the migration must find ${user.*}-style holes with no {{ }} root) carry into step 2's order.
  • Diff read by the seat: 4 files, +276/−34, all under ui/packages/core/src/actions plus the changeset. One dialect per sentence: a {{ }} hole makes the sentence a template, otherwise the ${…} path runs as before, so registered producers' ${result.*} copy keeps filling. 15 new pins, reverse-checked (12 red on the unfixed code, 3 keepers green).
  • Follow-ups filed: none new; step 2 stays on Action copy takes two hole syntaxes: description, param helpText and outcomeMessages read ${…}, confirmText and successMessage read {{ … }} #27948.
  • Gates: check-runs on this head 45 success, 15 skipped, 0 red. mergeable_state clean. check-governed-merges --branch: NOT governed, 4 paths, +276/−34.
  • Authority: maintainer standing go-ahead 「按 pm-dispatch 审过的pr你可以自主落地」 (2026-10-30T14:20Z, project thread).
  • Landing: pr_ready + automerge_enable in this act.

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

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant