Skip to content

Commit dabe771

Browse files
feat(formula,plugin-email,lint)!: a mail template hole takes one formatter and a literal currency code, refused at authoring and at render (#26799)
Fixes #26673 Part of #26660 (item c, the silent USD; item b stays open on the card, see Acceptance notes) Clause-②: no (narrowing) Metadata-protocol: narrowed Skills-docs: none — the published skill (`skills/objectstack-formula/SKILL.md`, the `template` row) already spells a hole as one optional formatter, `{{ path | formatter[:arg] }}`; the refusals name both rules at every door, and `content/docs/automation/email-templates.mdx` is updated here. ## 维护者速读 - 改了什么:邮件模板占位符只允许一个格式化器,`currency` 的参数只能是三位大写 ISO 4217 字面代码;两条规则在 `os validate` / `os build` / `os lint` 与元数据发布门(`422 INVALID_METADATA`)拒收,渲染时同样拒收(`TEMPLATE_INVALID`),应用内模板方言共用同一判定。 - 为什么改:`{{ x | default:0 | number:2 }}` 会把 `0 | number:2` 原样发给收件人,`{{ x | currency:currency_code }}` 会静默按美元显示金额,所有门禁都放行(hotclm 实测)。 - 风险与回滚:收紧(BREAKING),写了链式格式化器或路径/小写币种参数的模板会被拒;本仓库零命中。已存储的旧模板在下次发布前发送会失败而不是发错。回滚即还原本 PR。 - 先看:`packages/formula/src/template-engine.ts`(`judgeHoleFormatter` 与 `currency` 格式化器)、`packages/plugins/plugin-email/src/template-engine.ts`(渲染时抛 `TEMPLATE_INVALID`)、`packages/lint/src/validate-email-template-hole-grammar.ts`。 - 两分钟验证:`pnpm --filter @objectstack/plugin-email exec vitest run --maxWorkers=2 src/template-hole-grammar.render.test.ts`(先 build formula),预期 10 passed:链式与路径参数都以 `TEMPLATE_INVALID` 拒收,`currency:EUR` 渲染为 €1,234.50。 - 需要你做的:无,席位落地。 ## What changes One hole grammar, one judgement, every reader: - **`@objectstack/formula`** - `judgeHoleFormatter(hole, formatterPart, formatter, arg)` (in `template-engine.ts`, beside `FORMATTERS`) is the one judgement of a hole's formatter part, used by both template dialects. Two rules: **one formatter per hole** (a second `|` outside a closed quoted argument is refused), and **the `currency` argument is a literal ISO 4217 code in three capitals** (a path, a lowercase or an empty argument is refused). No argument still formats in USD. - The in-app dialect: `templateEngine.compile` / `evaluate` answer a `parse` error naming the hole and the rule, so `validateExpression` reports `invalid-template` at `os validate` and the publish gate. - The `currency` formatter no longer catches the `Intl` refusal and substitutes USD: a non-code argument throws a `RangeError` before the value is read. - The mail hole grammar now lives once, in `mail-hole-grammar.ts` (`mailHolePattern`, `parseMailHole`, `judgeMailHoleGrammar`). `judgeMailHtmlHoles` drops its copy and imports it; so does the renderer below. A chained hole is recognised as a hole even when its first formatter alone would not parse (`{{ x | upper | lower }}` used to be left on the page verbatim). - **`@objectstack/lint`**: a new gating rule, `email-template-hole-grammar` (`validateEmailTemplateHoleGrammar`), judges `subject`, `bodyText` and `bodyHtml` of every template on all three commands and on the runtime publish surface for `email_template` writes, refused there with the existing `422 INVALID_METADATA`. The four CLI transcripts that print the rule count (54 to 55) are updated. - **`@objectstack/plugin-email`**: the renderer parses with the shared grammar; a refused hole throws `TEMPLATE_INVALID`, and `resolveAndRenderTemplate` names the template and locale in front of it. It is reached only by a template stored before the rule existed, until its next publish. It is now the one way a render throws. - **`@objectstack/service-messaging`** (patch round 1): the email and inbox channels' `classifyError` grade `TEMPLATE_INVALID` permanent, beside `TEMPLATE_NOT_FOUND`, `TEMPLATE_INACTIVE`, `MISSING_VARIABLES` and `TEMPLATE_UNSUPPORTED`. A pre-rule stored template that fails this way now goes dead at once with the code on its delivery row, instead of spending its retry ladder. Patch changeset `26660-mail-hole-grammar-messaging.md` (`Clause-②: no`, `Metadata-protocol: none`). **Landing outside the named files, and why.** The save-time verdict's producer is the authoring-rule registry in `packages/lint` (the `email_template` publish door is that registry's runtime surface), not `email-template-door.ts`, which is the closed `sys_email_template` row door and judges no holes. The door pin lives in `packages/metadata-protocol` (test only), beside the placement rule's. **Same-day churn.** Both symptoms re-measured on `origin/main` `a485b5a3` through the surviving by-name render path, `EmailService.renderTemplate` (the module-level body renderer is retired from the entry). `currency:currency_code` with `eur` rendered `US$1,234.50` (zh-CN), `$1,234.50` (en-US), `1 234,50 $US` (fr-FR); the chained hole rendered `0 | number:2` for null and `71100` otherwise, in subject, text and html. **Sweep (one pass, all trees: `packages`, `examples`, `skills`, `content`, `apps`, `ui`, `docs`).** Chained holes: zero hits (the one regex hit is a JSX `style` object). `currency:` arguments: all are uppercase literals (`EUR`, `USD`, one quoted `'USD'`), all still admitted. No in-repo template needed a change. ## Tests - `packages/formula/src/template-hole-grammar.test.ts` (new, 24): both rules through `judgeHoleFormatter`, the in-app compile / evaluate / `validateExpression`, the formatter backstop, and the mail grammar's parse and per-face judge. - `packages/plugins/plugin-email/src/template-hole-grammar.render.test.ts` (new, 10): `renderTemplate` and `sendTemplate` refuse both shapes in each face with `TEMPLATE_INVALID` naming template, locale, hole and rule; nothing is sent; one formatter, a quoted `|` and `currency:EUR` still render. - `packages/lint/src/validate-email-template-hole-grammar.test.ts` (new, 15): the rule per face, the registry entry (gating, all commands, `email_template` runtime surface), each `os` command at `error`, the runtime gate. - `packages/metadata-protocol/src/runtime-authoring-gate.email-template-writes.test.ts` (+2): each refusal at the publish door asserts `code` `INVALID_METADATA` + `status` 422 + the issue's rule and path, with a non-vacuous admit. <details><summary>Full suites and typecheck at 9fd3fde</summary> - `@objectstack/formula`: Test Files 55 passed (55), Tests 1476 passed (1476); typecheck exit 0. - `@objectstack/plugin-email`: Test Files 40 passed (40), Tests 712 passed (712); typecheck exit 0. - `@objectstack/lint`: Test Files 169 passed (169), Tests 7445 passed (7445); typecheck exit 0. - `@objectstack/metadata-protocol`: Test Files 269 passed, 3 skipped (272), Tests 21618 passed, 19 skipped; typecheck exit 0. - `pnpm exec eslint` on the 13 changed TypeScript files: exit 0. - `9d428aae` changes four docs transcripts only (no source); the suites above were not re-run there, the transcript gate was. </details> <details><summary>Ablations (each through scripts/ablation-replace.mjs, WRAP mode; dist legs proven with ablation-dist-preflight)</summary> - **A, currency rule off** (`formatter === 'currency'` to a marker in `judgeHoleFormatter`; formula + lint rebuilt, marker present in 2 dist files): formula 11 failed, plugin-email 4 failed, lint 9 failed, metadata-protocol 1 failed — exactly the currency pins; every chain pin stayed green. A first attempt was refused by the tool before running anything (the replacement contained the anchor, so the count could not drop); re-anchored. - **B, one-formatter rule off** (marker on the `chainsASecondFormatter` branch; rebuilt, marker in dist): formula 6, plugin-email 4, lint 6, metadata-protocol 1 failed — exactly the chain pins. - **C, renderer guard off** (plugin-email source): plugin-email 8 failed, others green; the chained template rendered `Paid 0 | number:2`, the original defect. - **D, the formatter's USD substitution put back** (formula source): 1 failed, the formatter-throws pin. - Every leg restored to the HEAD blob with an empty `git diff HEAD`; after the last dist leg formula and lint were rebuilt and both markers proven absent from `dist/`, tree clean. </details> - `packages/services/service-messaging/src/email-channel.test.ts` and `inbox-channel.test.ts` (+1 each): a `TEMPLATE_INVALID: …` failure from `sendTemplate` / `renderTemplate` reaches the delivery row with the code at its front and is graded `permanent`. Ablated per channel (the word removed from the regex): each went red on exactly that pin, `expected 'retryable' to be 'permanent'`, and was restored to the HEAD blob. ## Acceptance notes - **Remaining half of #26660 (item b): format an amount in the record's own currency.** Not built. Reading the code from the data (`currency:record.currency_code`, or a per-hole value the formatter knows the code of) widens the template grammar, which triage sent back to triage. Until then the refusal says to format the amount in the caller and pass the text. - **`currency` with no argument still formats in USD.** Unchanged by these cards (a present non-code argument is what is refused). It is the same reader-sees-a-currency-nobody-chose shape for a record in another currency; noted for the item-b decision, not changed here. - **The in-app template dialect narrows too.** The two rules are judged once for both dialects because the formatter is shared: a non-code `currency` argument can no longer render, so the in-app compile must refuse it rather than let `evaluate` throw. Same defect class, same file family; the in-app chain (`default:0 | number:2` printed `0 | number:2` there as well) is refused by the same function. - **`TEMPLATE_INVALID` is a new render failure code.** The messaging email and inbox channels grade it permanent (patch round 1). The `IEmailService` contract's error lists in `packages/spec/src/contracts/email-service.ts` still name three codes; that documentation line is the spec lane's: filed by the seat as #26803. - **Other formatter arguments are still unjudged** (`number:two` silently formats without fixed digits). Not measured at a door here; noted only. - **Serial constraint.** `origin/main` merged at `0f3d068c`; #25805 (also on `packages/lint`) has not landed. It adds an authoring rule too, so whichever lands second moves the four transcript counts again (`check:docs-transcript-drift` names them). ## Verification <details><summary>Gate families (derived with dispatch-gates --commands, reconciled with --ran: 91 derived, 91 run)</summary> - 86 derived commands exit 0 at `9fd3fde3`, plus `pnpm check:error-code-casing` and `node scripts/check-platform-checklist.mjs --changed-from origin/main` (both exit 0). - `pnpm check:docs-transcript-drift`: exit 1 at `9fd3fde3` (four transcripts printed 54 rules, the registry derives 55) — fixed in `9d428aae`, exit 0. - `pnpm check:cli-examples-parity`: newly derived at `9d428aae`, exit 0. - `pnpm --filter @objectstack/spec run check:skill-examples` and `pnpm check:dual-build-cjs-loads`: exit 3 (prerequisite: unbuilt packages) at first; exit 0 at `9d428aae` once the dogfood closure was built. - `node scripts/check-plugin-teardown-shape.mjs --self-test`: exit 3, NOT MEASURED — its positive control is pinned to a commit this shallow checkout cannot reach; checker health only, CI runs it. </details> <details><summary>Dogfood battery at 9d428aa</summary> `pnpm --filter @objectstack/dogfood test` under the verify lock (closure built first): `VERDICT command-exit 0`, Test Files 302 passed, 3 skipped (305), Tests 2507 passed, 20 skipped (2527). </details> <details><summary>Patch round 1 at dfe8026 (origin/main merged at 1cd4b73, the PR was 15 behind)</summary> - Full suites and typecheck exit 0: `@objectstack/service-messaging` Test Files 61 passed (61), Tests 731 passed; `@objectstack/formula` 56 files, 1484 tests; `@objectstack/plugin-email` 40 files, 712 tests; `@objectstack/lint` 169 files, 7445 tests; `@objectstack/metadata-protocol` 269 passed and 3 skipped files, 21618 passed and 19 skipped tests. `pnpm exec eslint` on the 17 changed TypeScript files: exit 0. - Gates re-derived with dispatch-gates --commands (no paths) at dfe8026: 93 commands (adds `pnpm check:i18n` and `pnpm check:i18n-stale-fill`). 92 exit 0 on the first run; `pnpm check:dual-build-cjs-loads` exit 3 (six packages unbuilt in the fresh worktree), exit 0 once they were built. Extras `pnpm check:error-code-casing` and `node scripts/check-platform-checklist.mjs --changed-from origin/main`: exit 0. `--ran` reconciliation with exit codes recorded: 93 derived, 93 run, 0 NOT-MEASURED. - Dogfood battery at dfe8026: `VERDICT command-exit 0`, Test Files 313 passed, 3 skipped (316), Tests 2588 passed, 20 skipped (2608). </details> Commits: one per member (`9e59d3de` the currency literal, `44ccaf92` one formatter per hole), merges of `origin/main` (`9fd3fde3`, `1cd4b739`), `9d428aae` the transcript counts, and patch round 1 (`d6a898e3` the messaging grade, `dfe80262` its changeset). Session: session_01QSeKYmQXM3rHJcPnrEPk4B --- _Generated by [Claude Code](https://claude.ai/code/session_01QSeKYmQXM3rHJcPnrEPk4B)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 95f818d commit dabe771

26 files changed

Lines changed: 962 additions & 104 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/formula': minor
3+
---
4+
5+
feat(formula)!: a template hole takes one formatter, and the `currency` formatter's argument is a literal ISO 4217 code — both refused at compile, never rendered as formatter syntax or in US dollars
6+
7+
Clause-②: no (narrowing) — the in-app template dialect's compile (and so `os validate` and the publish gate for every template slot) refuses a hole with a second formatter (`{{ x | default:0 | number:2 }}`) and a `{{ x | currency:ARG }}` hole whose `ARG` is not three capital letters, both of which it accepted before; `formatValue('currency', value, ARG)` now throws a `RangeError` for such an `ARG`: a path or another argument `Intl` refuses, and an empty one, used to fall back to US dollars, and a lowercase code (`eur`), which `Intl` formatted in that currency, is now refused too. No accept set is widened; the additions are exports only: `judgeHoleFormatter`, `mailHolePattern`, `parseMailHole`, `judgeMailHoleGrammar`, and the types `TemplateHoleGrammarRefusal`, `TemplateHoleGrammarRule`, `ParsedMailHole` and `MailHoleGrammarFinding`.
8+
Metadata-protocol: narrowed
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped and no export is removed: a chained formatter and a currency argument that never rendered as written are now refused where the template is compiled. The refusal names the hole and the rule it breaks, so there is nothing for a migration to rewrite. -->
11+
12+
**BREAKING** accept-set narrowing in the template dialect, and a new shared mail hole grammar.
13+
14+
- **Before, a chained hole.** `{{ record.n | default:0 | number:2 }}` compiled as `default` with the argument `0 | number:2`, and rendered that text.
15+
- **Before, a currency path.** `{{ record.amount | currency:currency_code }}` compiled; `Intl` refused the argument and the formatter caught that and used US dollars, so the amount printed in a currency nobody wrote.
16+
- **Now.** `templateEngine.compile` / `evaluate` answer a `parse` error naming the hole and the rule. `formatValue('currency', …)` throws a `RangeError` for an argument that is not a code, before it reads the value. A lowercase code (`currency:eur`), which formatted in euros before, is refused the same way. No argument still formats in US dollars.
17+
- **Shared grammar.** `judgeHoleFormatter` is the judgement both template dialects use. The mail template grammar is exported once, `mailHolePattern`, `parseMailHole` and `judgeMailHoleGrammar`, and is what `@objectstack/plugin-email`'s renderer, `judgeMailHtmlHoles` and `@objectstack/lint` read; `judgeMailHtmlHoles` no longer keeps a copy of it.
18+
- **Who sees it.** A template slot that chained formatters, or wrote a path or a lowercase code as the currency argument. No template shipped in this repository does.
19+
20+
**The remedy.** Keep one formatter per hole and decide anything more where the data is assembled. Write the currency code itself (`currency:EUR`), or format the amount where the data is assembled and pass the formatted text.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/service-messaging': patch
3+
---
4+
5+
fix(service-messaging): the email and inbox channels grade a `TEMPLATE_INVALID` template failure permanent, like the other template failures
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`@objectstack/plugin-email`'s `sendTemplate` and `renderTemplate` now fail with `TEMPLATE_INVALID` for a stored template whose hole the mail grammar refuses (a second formatter, or a `currency` argument that is not a literal ISO 4217 code). Retrying cannot succeed until the template is republished, so `classifyError` on both channels grades it `permanent`, as it already grades `TEMPLATE_NOT_FOUND`, `TEMPLATE_INACTIVE`, `MISSING_VARIABLES` and `TEMPLATE_UNSUPPORTED`: the delivery goes dead at once with the code on its row, instead of spending the whole retry schedule first. Every other failure keeps its grade.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/plugin-email': minor
3+
---
4+
5+
feat(plugin-email)!: a mail template hole that chains a second formatter, or whose `currency` argument is not a literal ISO 4217 code, fails the render with `TEMPLATE_INVALID` instead of printing formatter syntax or US dollars
6+
7+
Clause-②: no (narrowing) — `sendTemplate` and `renderTemplate` refuse a stored template whose `subject`, `body_text` or `body_html` writes a hole with a second formatter, or `{{ x | currency:ARG }}` with an `ARG` that is not three capital letters, both of which rendered before. No accept set is widened, and the package adds no export.
8+
Metadata-protocol: narrowed
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped and no export changes: the renderer refuses a hole the authoring rule refuses, for a template that reached it without being judged. There is nothing for a migration to rewrite. -->
11+
12+
**BREAKING** a render that printed a wrong currency now fails.
13+
14+
- **Before, a chained hole.** `{{ actual_amount | default:0 | number:2 }}` rendered `0 | number:2` for an empty value and `71100` otherwise, in every face.
15+
- **Before, a currency path.** `{{ planned_amount | currency:currency_code }}` with `currency_code = 'eur'` rendered `US$1,234.50` (zh-CN), `$1,234.50` (en-US) and `1 234,50 $US` (fr-FR), in every face.
16+
- **Now.** The render throws `TEMPLATE_INVALID: email template NAME (locale=LOCALE): …`, naming the hole and the rule it breaks (one formatter per hole, or the literal-only currency code); `sendTemplate` sends nothing. The authoring doors refuse the same hole first (`email-template-hole-grammar`), so this is reached only by a template stored before that rule, until its next publish.
17+
- **The grammar.** The renderer parses holes with `@objectstack/formula`'s `parseMailHole`, the grammar the authoring rule and the placement judge read; it keeps no copy of its own.
18+
19+
**The remedy.** Republish the template with one formatter per hole and a literal currency code (`currency:EUR`), or format the amount in the caller and pass the formatted text as its own variable.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/lint': minor
3+
---
4+
5+
feat(lint)!: a new gating authoring rule, `email-template-hole-grammar`, refuses an email template hole that chains a second formatter or whose `currency` argument is not a literal ISO 4217 code
6+
7+
Clause-②: no (narrowing) — `os validate`, `os build`, `os lint` and the metadata publish door (`422 INVALID_METADATA`) refuse an `email_template` whose `subject`, `bodyText` or `bodyHtml` writes a hole with a second formatter (`{{ x | default:0 | number:2 }}`) or `{{ x | currency:ARG }}` with an `ARG` that is not three capital letters, both of which they accepted before. No accept set is widened; the additions are exports only: `validateEmailTemplateHoleGrammar`, `EMAIL_TEMPLATE_HOLE_GRAMMAR`, the type `EmailTemplateHoleGrammarFinding`, and the rule id `email-template-hole-grammar`.
8+
Metadata-protocol: narrowed
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped: a hole that never rendered as written (the renderer printed formatter syntax, or the amount in US dollars) is now refused where it is authored. The refusal names the hole and the rule it breaks, so there is no key for a migration to rewrite and no tombstone. -->
11+
12+
**BREAKING** accept-set narrowing at every authoring door for `email_template`.
13+
14+
- **Before, a chained hole.** `{{ actual_amount | default:0 | number:2 }}` passed every gate. The second `|` was read as part of `default`'s argument, so the hole printed the text `0 | number:2` for an empty value and the bare `71100` otherwise; `{{ x | upper | lower }}` stayed on the page as written.
15+
- **Before, a currency path.** `{{ planned_amount | currency:currency_code }}` passed every gate. The argument is not read as a path, so the amount rendered in US dollars whatever the record's currency (`$1,234.50` for a record in euros).
16+
- **Now.** The rule refuses both at `error`, naming the hole, the face and the template. A hole takes one formatter; anything more is decided in the caller that supplies the data. The currency argument is a literal code in three capital letters (`currency:EUR`), never a path. A lowercase or empty argument is refused the same way. `currency` with no argument is unchanged (US dollars).
17+
- **Which holes.** The grammar is `@objectstack/formula`'s `judgeMailHoleGrammar`, the one the mail renderer parses with; all three faces are judged.
18+
- **Who sees it.** A template that chained formatters, or wrote a path or a lowercase code as the currency argument. No template shipped in this repository does.
19+
20+
**The remedy.** Keep one formatter per hole and pass any further decided value (a default, a second format) as its own variable. Write the currency code itself (`currency:EUR`). To show each record's own currency, format the amount in the caller that supplies the data and pass the formatted text as its own variable.

‎content/docs/automation/email-templates.mdx‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,21 @@ never needs triple braces.
8282
same way they do in-app. `datetime` honours the reference timezone the caller
8383
passes; calendar-day `date` values are timezone-naive.
8484

85+
**One formatter per hole.** `{{ amount | default:0 | number:2 }}` is refused by
86+
`os validate`, `os build`, `os lint` and the metadata publish door
87+
(`email-template-hole-grammar`): a hole takes one formatter, and there is no
88+
chaining. Decide the value in the caller that supplies the data — a default, a
89+
second format — and pass the finished value as its own variable.
90+
91+
**The `currency` argument is a literal code.** `currency:EUR` formats in euros,
92+
and `currency` with no argument formats in US dollars. The argument is an ISO
93+
4217 code in three capital letters, never a path: `currency:currency_code` does
94+
not read the record's `currency_code`. `os validate`, `os build`, `os lint` and
95+
the metadata publish door refuse any other argument (`email-template-hole-grammar`,
96+
a `422 INVALID_METADATA` at the publish door), naming the hole and the rule. To
97+
show an amount in each record's own currency, format it in the caller that
98+
supplies the data and pass the formatted text as its own variable.
99+
85100
**Where a hole may sit in `bodyHtml`.** The renderer's escaping is right for
86101
element text and for a quoted attribute value that holds plain text, so those
87102
are the supported placements for `{{path}}`. A hole anywhere else — inside a
@@ -94,10 +109,14 @@ the `href` shape above. The refusal (`email-template-hole-placement`, a
94109
supported ones. A template already stored keeps sending; it is judged the next
95110
time it is published.
96111

97-
Two properties of the renderer to author around:
112+
Three properties of the renderer to author around:
98113

99-
- **A missing placeholder renders as an empty string.** Rendering never throws.
100-
Declare a variable `required` (below) if absence should be an error instead.
114+
- **A missing placeholder renders as an empty string.** Declare a variable
115+
`required` (below) if absence should be an error instead.
116+
- **A hole the authoring rules refuse is refused at render too.** A template
117+
stored before a rule existed is judged at its next publish; until then
118+
`sendTemplate` and `renderTemplate` fail with `TEMPLATE_INVALID`, naming the
119+
template and the hole, rather than send a wrong value.
101120
- **There are no loops, conditionals or partials.** A template is a data-only
102121
rendering by design; branching belongs in the caller, which passes in the
103122
already-decided values.

‎content/docs/deployment/cli.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -758,7 +758,7 @@ os compile --json # JSON output for CI pipelines
758758
→ Normalizing stack definition...
759759
→ Lowering inline handlers...
760760
→ Validating protocol compliance...
761-
→ Running author-time rules (54)...
761+
→ Running author-time rules (55)...
762762
→ Checking that every required capability has a provider installable in this edition...
763763
→ Collecting package docs (ADR-0046)... 0 collected
764764
→ Writing artifact...

‎content/docs/deployment/validating-metadata.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -752,7 +752,7 @@ A clean run walks the registry and reports timing:
752752
Config: /path/to/support-desk/objectstack.config.ts
753753
Load time: 21ms
754754
→ Validating against ObjectStack Protocol...
755-
→ Running author-time rules (54)...
755+
→ Running author-time rules (55)...
756756
→ Checking that every required capability has a provider installable in this edition...
757757
→ Checking package docs (ADR-0046)...
758758

‎content/docs/getting-started/build-with-claude-code.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ visible: 'status != "resolved"'
305305
◆ Validate
306306
────────────────────────────────────────
307307
→ Validating against ObjectStack Protocol...
308-
→ Running author-time rules (54)...
308+
→ Running author-time rules (55)...
309309
310310
✗ Author-time rules failed (1 issue)
311311
• stack · action 'resolve_ticket' visible: bare reference `status` — a

‎content/docs/ui/react-pages.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ objectstack validate
397397
────────────────────────────────────────
398398
→ Loading configuration...
399399
→ Validating against ObjectStack Protocol...
400-
→ Running author-time rules (54)...
400+
→ Running author-time rules (55)...
401401
→ Checking that every required capability has a provider installable in this edition...
402402
→ Checking package docs (ADR-0046)...
403403

‎packages/formula/src/index.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,12 @@ export type {
6363
ParseCelToAstOptions,
6464
} from './cel-engine';
6565
export { cronEngine } from './cron-engine';
66-
export { templateEngine, TEMPLATE_FORMATTERS, formatValue } from './template-engine';
66+
export { templateEngine, TEMPLATE_FORMATTERS, formatValue, judgeHoleFormatter } from './template-engine';
67+
export type { TemplateHoleGrammarRefusal, TemplateHoleGrammarRule } from './template-engine';
68+
// The mail template hole grammar — one copy, read by `@objectstack/plugin-email`'s
69+
// renderer, the placement judge below and `@objectstack/lint`'s grammar rule.
70+
export { mailHolePattern, parseMailHole, judgeMailHoleGrammar } from './mail-hole-grammar';
71+
export type { ParsedMailHole, MailHoleGrammarFinding } from './mail-hole-grammar';
6772
// #25189 — where a hole may sit in a mail template's HTML face (ruling
6873
// 6335348115 on #25082, F1 A2 / F3 C2). Here because both readers already
6974
// depend on this package and on nothing else they share: `@objectstack/lint`'s

0 commit comments

Comments
 (0)