Repository navigation
Commit 79ed6f7
feat(formula, plugin-email, messaging, automation, ui)!: a template
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBPgpqhXpoVLpqQEw4Pge7jf)_
Fixes #26939
Ruling-ref: 6464202770
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: updated here — content/docs/automation/email-templates.mdx,
content/docs/automation/flows.mdx and
content/docs/data-modeling/formulas.mdx state the tenant default and the
refusal; no skill under skills/** taught the dollar default (the formula
skill names the formatter grammar only), so no skills/** edit
## 维护者速读
- 改了什么:模板里不带参数的 `{{ amount | currency }}`
不再按美元格式化,改为按组织默认币种(`localization.currency`)格式化;邮件(含认证邮件)、流程文本槽、动作/批量动作文案三种模板方言全部生效。没有默认币种时,邮件发送/渲染以
TEMPLATE_INVALID 拒绝(不发送),流程节点失败,动作文案按原文显示。
- 为什么改:裁决 6464202770(#27119 Q2 = X)。原先默认美元是没人选过的默认值,非美元组织的金额被印成 `$`。
- 风险与回滚:BREAKING(语义变化,ADR-0087 登记 D3 条目
`template-currency-no-argument-tenant-default`)。没有设置默认币种的组织,带无参 currency
的邮件会进死信(permanent)。编写期不变,保存/发布照常接受。回滚即 revert。
- 先看:`packages/formula/src/template-engine.ts` 的
`currencyDefaultRefusal` 与 currency
格式化器;`packages/plugins/plugin-email/src/template-engine.ts` 的
render;`packages/services/service-automation/src/run-resume.ts`
的币种种入;`packages/services/service-messaging/src/messaging-service-plugin.ts`
的 `getTenantCurrency`。
- 两分钟验证:`pnpm --filter @objectstack/plugin-email exec vitest run
src/template-currency-tenant-default.render.test.ts`,预期全部通过(CNY 渲染为
`CN¥1,234.50`,无默认币种时 TEMPLATE_INVALID 且不发送)。
- 需要你做的:无,席位落地。#27217 已合入并已合并进本分支;设置读取故障被当作永久失败的残留问题已立卡 #27257。
## What changes
**One rule, every dialect.** A `currency` hole with no argument formats
in the organization's default currency, the `localization.currency`
setting that a currency field and a dashboard measure already display
in. With no default, the render refuses the hole by name and names the
remedy. Nothing formats in USD by default.
**One sentence.** `currencyDefaultRefusal(hole, currency, dialect)`
(`@objectstack/formula`, exported) returns `null` for a three-capital
code and otherwise the refusal. The mail dialect's remedy also names
`currency:@currency_code` (#26937, merged in here); the in-app dialect's
names the literal form only, because it refuses `@path`.
**How each host hands the default over.** Every server-side read goes
through `resolveLocalizationContext` (`@objectstack/core`), the read a
request resolves `ExecutionContext.currency` with; there is no second
resolution.
- `@objectstack/formula`: `EvalContext.currency`, a reserved channel
like `locale`, never bound as a root. `evaluate` refuses the sentence
(`kind: 'runtime'`) when a no-argument hole meets no default;
`formatValue(…, { currency })` throws a `RangeError` with the same
sentence. Compile and `validateExpression` are unchanged.
- `@objectstack/plugin-email`: `RenderOptions.currency`; `render()`
judges a no-argument hole before reading the amount and throws
`TEMPLATE_INVALID`. `EmailService` passes `input.currency` through.
- `@objectstack/spec` (contracts): `SendTemplateInput.currency`,
`RenderTemplateInput.currency`, `AutomationContext.currency`, with docs;
the `TEMPLATE_INVALID` docs on `sendTemplate` / `renderTemplate` name
the case.
- `@objectstack/service-messaging`: the email and inbox channels take
`getTenantCurrency(organizationId)` and pass `currency` on the template
arm only when the delivery's organization has one. The plugin wires it
to `resolveLocalizationContext`. A refusal is graded permanent, like
every `TEMPLATE_INVALID`.
- `@objectstack/service-automation`:
`AutomationEngine.setTenantCurrencyResolver`, wired by the plugin.
`resolveRunContext` seeds `AutomationContext.currency` once at run
setup, when the context carries none; a failed or empty read leaves it
unset. A resumed segment whose persisted context carries none (a run
parked before this change) is seeded the same way in
`resolveDecisionSegmentContext`, on a copy, so the persisted
`run.context` stays as it parked. `renderFlowTextSlot` renders the
authored text before it blames a translation: when both throw (a
no-argument hole with no default refuses both alike), the authored
refusal is the answer and no translation warning is logged. A subflow
and a map child inherit it through the context spread, and a suspended
run persists it with the context. `renderTextSlot` takes the currency,
and every text slot passes `context.currency`: notify `title` /
`message` / `actionUrl`, http `url` and headers, screen texts, end
message.
- `@objectstack/plugin-auth`: the six auth mail call sites
(verification, password reset, account and organization invitations,
magic link, notices) reach `sendTemplate` through
`withMailTenantCurrency` (`mail-tenant-currency.ts`), which the plugin
wraps once around the email service it wires into the AuthManager. A
call that names no `currency` gets the default of the organization it
names (`organizationId`) or, for identity mail, the deployment-wide
setting, read through `resolveLocalizationContext`; a call that names
one keeps it, and a failed or empty read adds nothing.
- `@object-ui/core` / `@object-ui/react` / `@object-ui/plugin-grid`:
`ActionRunner.setTenantCurrency`, installed by `ActionProvider` and
`useActionRunner` from `useLocalization().currency` (latest-ref, like
the display locale); `fillTemplateHoles` and `bulkConfirmText` pass it
to the engine. `BulkActionDialog` reads it from `useLocalization`. A
sentence the engine refuses is shown as written, and `fillTemplateHoles`
now `console.warn`s the refusal once per distinct sentence, naming the
hole and the remedy.
- Migration: semantic entry
`18.template-currency-no-argument-tenant-default` (ADR-0087 D3: intent,
not text, so no conversion).
- Docs: email-templates (a paragraph; #26937's "no argument formats in
US dollars" sentence now points at it), flows (a bullet), formulas (a
sentence).
**Census, left as they are.** These show the spelling `{{ record.amount
| currency }}` and claim nothing about USD, so they stay: the spec
describes in `flow-text-slot-template.ts`, `io-node-config.zod.ts`
(notify, http, screen), the notify and screen node config descriptions,
the semantic entry `18.flow-text-slot-single-brace-refused`, and the
formula invalid-hole message. The lint fixtures and the ui
`flowTextSlots` fixture stay valid because authoring is unchanged. No
seeded template in the repo uses a no-argument `currency` hole.
## Tests
- `packages/formula/src/template-currency-tenant-default.test.ts`: CNY
in `en-US` (`CN¥1,234.50`) and `zh-CN` (`¥1,234.50`), EUR, JPY; a
literal code ignores the default; a root named `currency` is a value,
never the default; null renders empty; refusal when the default is
unset, empty, `usd` or `dollars`, whatever the value, and for the whole
sentence when one hole of several has no argument; compile and
`validateExpression` stay ok; `formatValue`; `currencyDefaultRefusal`
per dialect.
- `packages/formula/src/template-formatters.test.ts`,
`template-hole-grammar.test.ts`: re-pinned from USD to a supplied
default.
-
`packages/plugins/plugin-email/src/template-currency-tenant-default.render.test.ts`:
`renderTemplate` / `sendTemplate` over all three faces, double- and
triple-braced; with no default or `usd`, `^TEMPLATE_INVALID: email
template contract.overdue (locale=en-US): the hole {{ planned_amount |
currency }}`, naming `localization.currency`, `currency:EUR` and
`currency:@currency_code`, and nothing sent; the face renderers throw; a
`currency:@path` hole and a template with no no-argument hole need no
default.
-
`packages/services/service-messaging/src/template-tenant-currency.test.ts`:
the email and inbox channels pass the delivery organization's default
(exact input pinned), pass no `currency` key without one, and grade the
refusal permanent.
-
`packages/services/service-automation/src/builtin/notify-currency-tenant-default.test.ts`:
the resolver is read once per run with the tenant id and the title
renders `Won CN¥1,234.50`; a context that already carries a currency
keeps it; with no resolver, no default or a throwing read, the run fails
naming the hole and nothing is emitted; `renderTextSlot` pins.
-
`packages/services/service-automation/src/builtin/notify-currency-tenant-default.test.ts`
also: a run parked with no currency resumes on a second engine with a
resolver, renders `Won CN¥1,234.50`, and the persisted context stays
without `currency` (red with the resume seeding removed).
`end-node-refusal-translation.test.ts`: a refusal both texts share fails
the run naming the hole with no translation warning (red on the old
`renderFlowTextSlot`), and with `currency: 'CNY'` the zh-CN translation
renders in it.
- `packages/plugins/plugin-auth/src/mail-tenant-currency.test.ts`: the
named organization's default is added; identity mail reads the
deployment-wide one; a named currency is kept and nothing is read; no
default or a failed read adds no key; other members delegate.
`auth-plugin.test.ts`: the wired service is the wrapper and still
reaches the transport.
- ui: `ActionRunner.copyTemplateTenantCurrency-26939`,
`ActionProvider.copyTemplateTenantCurrency-26939`,
`bulkConfirmTenantCurrency-26939`: confirm and success copy in the
tenant default; with none, the sentence is shown as written and the
refusal is logged once.
## Acceptance notes
- **Display surfaces with no default.** An action's confirm or success
sentence with a no-argument hole and no tenant default is shown as
written, the existing fallback for any sentence the engine refuses, and
the refusal is logged. A flow `screen` text refuses the node like every
other text slot. The contract review kept this split (comment
6469598248, open question answered A).
- **Mail with no default dead-letters.** A messaging delivery whose
organization has no default currency records the `TEMPLATE_INVALID` as
permanent, so it is not retried. Setting the default and re-sending is
the remedy.
- **Residual: a settings read fault is graded permanent (#27257).**
`resolveLocalizationContext` answers `currency: undefined` both when an
organization has no default and when its `sys_setting` read fails (the
failure is memoized for a short TTL), so within that window a templated
delivery with a no-argument hole dead-letters as `TEMPLATE_INVALID`.
Telling the two apart needs a core seam, filed as #27257.
- **Pending 26660 changesets.** `.changeset/26660-*.md` (from #26937,
unreleased) still say "no argument formats in US dollars".
`check-empty-changeset` refuses an edit to a changeset the merge base
carries, so this PR's changeset says it supersedes them in the same
release instead.
- **#27217 (#26948)** landed first and is merged in; its
unknown-formatter refusal and this PR's no-default refusal sit side by
side in `render()`.
## Verification
Head `9727b5111`, which carries main at `9ff8154cf` (#27217 included)
and the review's should-fixes S1, S3, S4 and S5.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on `2ba0895c2`: 124 derived families, each
run with its exit recorded; `--ran`: 124 run, 0 NOT-MEASURED, 0 UNRUN,
all exit 0. Five spec-dist families first answered exit 3 because a spec
source edit (the `9727b5111` fix below) had made `packages/spec/dist`
stale; after `@objectstack/spec` build they reran exit 0.
- Under `os-verify-lock` on `2ba0895c2`: the build of every changed
package and dogfood's dependencies; `@objectstack/spec`
`check:generated` exit 0; test and typecheck exit 0 for
`@objectstack/plugin-auth`, `@objectstack/service-automation`,
`@objectstack/plugin-email`, `@objectstack/formula`, `@object-ui/core`,
`@object-ui/react` and `@object-ui/plugin-grid`; `@objectstack/dogfood`
typecheck exit 0 and the full dogfood suite: 348 files passed, 3 skipped
(2804 tests passed).
- CI's Test Core shard 2 on `2ba0895c2` failed one spec repo pin,
`currency-mode-family-closure.pin.test.ts` rule B: the new
`RenderTemplateInput.currency` doc sat within five lines of the
`timezone` doc's ADR-0053 citation. `9727b5111` moves that member beside
`data`. Under `os-verify-lock` on `9727b5111`: `@objectstack/spec`
build, `check:generated`, `test:repo`, `test` and typecheck, all exit 0.
- Ablations: the S3 cold-resume test is red with the resume seeding
removed, and the S5 test is red on the old `renderFlowTextSlot`; both
are green restored.
- ESLint on the changed files: 0 errors.
## Human Minimum
Each item is one rule from
[CONTRIBUTING.md › Human
Minimum](https://github.com/objectstack-ai/objectstack/blob/main/CONTRIBUTING.md#human-minimum),
which links the AGENTS.md text that states it.
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [ ] 3. No formatter ran; `pnpm lint` passes. (No formatter ran. ESLint
on the changed files: 0 errors; `pnpm lint` is CI's run.)
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English. (The 维护者速读 block is in
Chinese by the repo's dispatch rule.)
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01WGufzizxYz7JyC5JLW3Gmv
---------
Co-authored-by: Claude <noreply@anthropic.com>currency hole with no argument formats in the organization's default currency, and with none the render refuses it (#27249)1 parent 6eaf404 commit 79ed6f7
48 files changed
Lines changed: 1609 additions & 59 deletions
File tree
- .changeset
- content/docs
- automation
- data-modeling
- kernel/runtime-services
- packages
- formula/src
- plugins
- plugin-auth/src
- plugin-email/src
- services
- service-automation/src
- builtin
- service-messaging/src
- spec/src
- contracts
- migrations/entries/semantic
- scripts
- ui/packages
- core/src/actions
- __tests__
- plugin-grid/src
- __tests__
- components
- react/src
- context
- __tests__
- hooks
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
111 | 111 | | |
112 | 112 | | |
113 | 113 | | |
114 | | - | |
| 114 | + | |
115 | 115 | | |
116 | 116 | | |
117 | 117 | | |
| |||
139 | 139 | | |
140 | 140 | | |
141 | 141 | | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
142 | 153 | | |
143 | 154 | | |
144 | 155 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
484 | 484 | | |
485 | 485 | | |
486 | 486 | | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
487 | 493 | | |
488 | 494 | | |
489 | 495 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
82 | 82 | | |
83 | 83 | | |
84 | 84 | | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
85 | 89 | | |
86 | 90 | | |
87 | 91 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | | - | |
| 34 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
71 | 71 | | |
72 | 72 | | |
73 | 73 | | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
74 | 77 | | |
75 | 78 | | |
76 | 79 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
0 commit comments