Repository navigation
Commit 7de0265
feat(plugin-email)!: email templates resolve from the registry; the built-in seed, the boot sweep and the live projector retire (ADR-0131 D6/D10, C4 stage 2) (#22875)
Fixes #15205
Clause-②: yes (narrowing)
C4 stage 2, the registry loader half (ADR-0131 D6/D10, ruling C on §6
Q1, record 6020178017 on #22005). Claim 6109130514; dispatch from the
`domain:services` seat 2. One cross-domain PR under triage's routing
6108741731 (the `packages/spec` ledger and describe text ride with the
loader so `check:liveness` is never red on `main`).
## What changed
- **`@objectstack/plugin-email` reads templates from the registry.**
`createRegistryEmailTemplateLoader` (in `template-loader.ts`) resolves
`email_template` by `(name, locale)` at every send through the
protocol's layered execution list (`getMetaItemsForExecution` — the
environment's item over each code package's declaration), or the
ObjectQL registry on a host with no metadata door. The locale ladder is
the row loader's, unchanged. Within one slot any other package's item,
or a package-less environment item, wins over the built-in. A failed
read propagates (never answered from the registry's package wording); a
malformed item refuses the send with the schema's message instead of
falling through to the built-in copy.
- **The built-in auth templates are code-declared items** of the email
plugin's own package (`com.objectstack.service.email`), registered
through its manifest at `init`. They now appear in Studio's
email-template list; an app declaring the same `(name, locale)` wins; a
Studio save overrides at environment level.
- **Retired:** `seedTemplates` (the boot seed),
`bootstrap-declared-email-templates.ts` whole
(`bootstrapDeclaredEmailTemplates`, `bootstrapEffectiveEmailTemplates`,
`EffectiveEmailTemplateSources`, `upsertDeclaredEmailTemplate`,
`deactivateDeclaredEmailTemplate`, `mapTemplateToRow`,
`EMAIL_TEMPLATE_OBJECT`), the live projector
(`wireTemplateMutationProjection`, `materializeTemplateMutation`,
`readEffectiveTemplate`, the `metadata.subscribe` watcher),
`createSysEmailTemplateLoader` and its two types, and the
`seedTemplates` / `templates` plugin options. The changeset names each
with its FROM → TO line.
- **Kept:** the `sys_email_template` object, its rows and the closed
organization door (stage 1). No row is promoted or deleted: the
customized rows are C7's (#15211). `TemplateLoader` / `EmailTemplateRow`
stay as the service's render seam.
- **`packages/spec` (describe-only, no schema shape change):** the
`email_template` liveness ledger is re-anchored by hand to the loader;
the `IEmailService` contract docs, the notify node's `template` describe
and refusal texts, and the `email-template.zod.ts` /
`notification.zod.ts` prose name the registry, not rows. `gen:docs`
regenerated two reference pages.
- **`service-automation`:** the notify node's `template` help text.
**`platform-objects`:** the `sys_email_template` header comment and
provenance comment (no field help changed, so no translation churn).
## Premise re-measured first (dispatch zone 2, items 1, 2, 4)
Scratch showcase measurement (never committed), `orgContext: true`,
Default Organization bootstrapped, at `d8c7d3864`:
- A Studio `PUT /meta/email_template/showcase_task_done_email` lands
**environment-scoped** (`sys_metadata.organization_id` null).
- Organization-free reads see it **live** (no restart) and **after a
cold boot**: `SchemaRegistry.getItem`, `protocol.getMetaItem`,
`protocol.getMetaItems` and `getMetaItemsForExecution` all serve the
saved subject. `metadataService.get` / `list` answer nothing for the
type.
- **Which read:** the layered execution list. `SchemaRegistry.listItems`
hands back the environment item beside the package entry it overrides,
both wearing the package's `_packageId` / `_provenance` (measured), and
`getItem` answers one bundle member per name. The list has already
decided precedence, one item per `(package, name, locale)` slot.
- **Live without a restart:** confirmed through the registry
write-through; the projector had no job left and is gone (pinned: no
projector, listener or subscription is armed; the `PUT` response carries
no `projectionApplied`).
## Byte identity
`builtin-auth-render-identity.test.ts` renders all 24 built-in templates
(6 names x 4 locales) through `EmailService.renderTemplate` and pins
subject (clear) plus HTML and text (SHA-256). At `924da8d8a` it ran the
seeded-row path (rows via `mapTemplateToRow`, the row loader) and the
registry path side by side: identical for all 24. Since the row path's
deletion it boots `EmailServicePlugin` over a real ObjectQL and holds
the registry path to those digests.
## Pins rewritten, not deleted (dispatch zone 2, item 7)
- `bootstrap-declared-email-templates.test.ts` becomes
`template-loader.test.ts`: every pin that asked whether the authored
wording reaches the send is restated against the loader. Row-only
mechanics (seed-not-clobber, provenance adoption, the bulk read) retired
with the rows.
- `email-plugin.template-runtime-write.test.ts`: a save sends on the
very next send; no seam armed; no `sys_email_template` read or write,
pre-existing rows byte-unchanged; the `#16152` envelope pins moved to
the loader path.
- `plugin-shutdown-detaches-template-bridge.test.ts`: shutdown releases
the door (the only engine binding left).
- Dogfood: `email-template-overlay-survives-boot` (save environment-wide
and live, survives a cold boot, a legacy customized row keeps its bytes
and is not what sends, the closed data door),
`email-template-materialization` (ADR-0054 proof keeps its `@proof` tag:
no row, authored override beats the built-in, the built-in serves zh-CN,
a Studio save over it sends without a restart),
`email-template-boot-sweep` (real engine + SQL driver: zero statements
reach the table on boot and send).
- `examples/app-showcase/test/email-template-locale.test.ts` runs
through the registry loader (Q2 answer A).
- `packages/spec/src/automation/io-node-config.test.ts`: the describe
pin asserted `sys_email_template`; it now asserts the email templates
and refuses the row wording.
## Ablations (each mutated, measured red, restored by blob equal to HEAD
with an empty `git diff HEAD`)
Through `scripts/ablation-replace.mjs`, src-level at `fcd3b610a`: A1 a
failed read falls back to the registry (2 red); A2 built-in precedence
dropped (4 red); A3 malformed items skipped (3 red); A4 per-load read
cached (6 red); A5 the plugin writes a row (1 red); A6 a projector armed
(1 red); A7 the read names an organization (1 red). Dist leg: A5 rebuilt
into `dist/` (`ablation-dist-preflight` found the marker), the dogfood
statement pin went 2 red, then restore, rebuild, `--absent`, 2 green.
A8: the notify describe back to `sys_email_template` reddens the flipped
spec pin. A9: the re-pointed swallow-census resolution control reddens
with departure ONE removed (its first attempt was a no-op the tool
refused; re-run in delete mode).
## Evidence at `1cc2da623` (after merging `origin/main` `55382dc02`)
- Closure build (`--filter=@objectstack/dogfood^...
--filter=@objectstack/example-showcase...`): 63/63, VERDICT command-exit
0.
- `plugin-email`: 33 files / 515 tests pass; typecheck OK. `spec`: 648
files / 19,375 tests pass; typecheck OK. `service-automation`
notify-node 16/16 and typecheck OK; `platform-objects` typecheck OK.
- Dogfood email-template files 9/9; showcase locale test 6/6.
- Gates: `dispatch-gates --commands` derived 136; `--ran` reconciles 136
run, 0 NOT-MEASURED. 135 exit 0. `check:platform-checklist` exits 1 with
8 findings in `docs/qa/platform-checklist/areas/*.json` naming
`metadata-protocol` / `service-storage` symbols: the identical 8 on
`origin/main` `69d421805` in a scratch worktree, so pre-existing and not
from this branch.
- Narrowed lint: `eslint --no-inline-config --format json` over the 33
changed source files: 33 files, 0 errors, 0 warnings, none ignored. The
narrowing excludes nothing: `eslint.config.mjs` enables no type-aware
linting, so this diff cannot move a verdict on an untouched file.
## Needs a decision: `email_template.category` and `.isSystem`
Their only consumer was the row projection this PR retires (a Setup
column, and the built-in seeder's stand-down). Measured: no reader in
this repository, and at the `.objectui-sha` pin `0df67f23` objectui
reads both only as editor inputs. The ledger grades both `dead` (honest
measurement; `label` and `description` stay `live` on the Studio list's
default columns). Consequence: `packages/lint`'s
`runtime-gate.inert-type-writes.test.ts` has 2 red cases, because a dead
`category` now draws a `liveness-dead-property` advisory, and `category`
is schema-defaulted (`'custom'`), so every template carries it and the
advisory cannot be cleared by the author. Not resolved here: retiring
the keys is a schema shape change this dispatch fences out, and
`packages/lint` is outside the file surface. Options and the four-axis
analysis are in the report comment on #15205.
## Acceptance notes
- Gate-forced edits outside the named surface, each the minimal answer
to a gate naming retired code:
`scripts/engine-double-contract.baseline.json` and
`scripts/objectql-double-limit.baseline.json` (shrink-only rows for
files now gone or clean), `scripts/isystem-census.mjs` (a
`NON_ELEVATION_READS` row for the deleted module),
`scripts/measure-durability-swallow-family.mjs` (a resolution control
re-pointed by measurement: 21 departure-ONE sites remain, none
write-shaped), the regenerated censuses
(`content/docs/permissions/system-context.mdx`,
`tenant-audit-census.mdx` with its hand-written figures,
`docs/audits/2026-08-tenant-audit-write-call-sites.counts.md`), and
`packages/spec/liveness/README.md` plus the generated
`state-counts/email_template.md`.
- Cross-lane docs edit, its own commit `94ab10722` (droppable):
`content/docs/automation/email-templates.mdx` described the
materialization and seed-not-clobber this PR retires.
- Transitional, as ruled: a `sys_email_template` row an organization
customized is no longer what sends until C7 promotes it. The changeset
says so and names the re-save in Studio as the interim fix.
- Pre-existing defect observed (not this card's): `sys_metadata` holds
one overlay row per `(type, name)`, so a Studio save of a second locale
member of one `email_template` name replaces the first member's overlay
at rest; live reads still show both until a restart, then the first
reverts to package wording. Measured on the showcase; the row path
showed the same revert at boot. Reported for filing.
- Comment-only references to retired names left outside the surface:
`packages/objectql/src/registry.ts`, `registry-i18n-bundle-key.test.ts`,
`packages/spec/src/stack-email-template-locale-floor.test.ts`,
`packages/spec/scripts/liveness/proof-registry.mts` (class prose), and
the dogfood fixture `fixtures/email-template-materialization-fixture.ts`
(header and manifest description).
- Changed lines: 4,490 (additions 1,517 + deletions 2,973, generated
included). Over 3,000, so this lands the Tier H way. No governed surface
in the file list.
## Patch round: the seat's answer B to Q1
The seat answered Q1 with B (`6111829211`): `email_template.category`
and `.isSystem` land graded dead, and #22876 retires them in the next
enforce-or-remove batch.
- `packages/lint/src/runtime-gate.inert-type-writes.test.ts`: the
group-C case is split. `mapping` keeps its assertions and title.
`email_template` now pins exactly one advisory, rule
`liveness-dead-property` on property `category`. The shipped-corpus
control pins the same single advisory; its `isSystem: false` draws
nothing.
- `packages/lint/src/lint-liveness-properties.test.ts`: a template that
sets neither key still warns about nothing. `category`, and `isSystem:
true`, each draw one dead-property advisory. The comments cite #22876.
- The tests assert the advisory's rule id and property path, not its
wording. The lint rule, the ledger grading and the schema are untouched.
- Ablations L1 to L4 each went red (1 failed of 142) and were restored
by blob.
- Merged `origin/main` `0253192f7` (`81ec7792b`) and regenerated the
censuses (`33641fe78`).
- At `33641fe78`: lint 6,461/6,461 and typecheck OK; plugin-email
515/515; dogfood 9/9.
- Gates: 136 derived, all run, 0 NOT-MEASURED. 135 exit 0.
`check:platform-checklist` fails with the same 8 findings already
present on `origin/main` `69d421805`.
- 4,575 changed lines against `0253192f7`.
## Patch round 3: the Clause-② line carries the narrowing arm
The seat amended its claim (`6112650531`): this PR retires published
exports and plugin options of `@objectstack/plugin-email`, a BREAKING
narrowing of the package's public face that widens nothing, so the
declaration reads `Clause-②: no (narrowing)`. The bare `no` was the
seat's own error in the claim.
- One commit, `4aa656e7a` on top of `33641fe78`, changes one line of
`.changeset/15205-email-templates-resolve-registry.md`: `Clause-②: no` →
`Clause-②: no (narrowing)`. The BREAKING banner, the `minor` grade for
`@objectstack/plugin-email`, the two `patch` rows and the ADR-0087
marker `not-required (no-migration-prescription)` are unchanged. This
body's second line carries the same arm.
- `check-adr-0087-registration --base origin/main` exits 0 and reads the
changeset as `[BREAKING+bang+clause-②-narrowing]`, accepting the
unchanged disposition. `check-changeset-no-major` exits 0 in its local
form and in CI's form (`--base` the merge base, `--event` a
`pull_request` payload with this line 2): "the declared narrowing is
accounted for: `@objectstack/plugin-email: minor`".
`check-empty-changeset` exits 0.
- `dispatch-gates --commands` for the changeset path: 20 derived, all 20
run at `4aa656e7a`, all exit 0; `--ran` 0 NOT-MEASURED, 0 UNRUN. No
package build or test was owed.
- Changed lines: unchanged at 4,575.
## Patch round 4: the at-tier review FAIL `6113872462`
The seat amended its claim a second time (`6113893432`). The PR widens
`@objectstack/plugin-email`'s public face
(`createRegistryEmailTemplateLoader`, `EmailTemplateRegistrySources`)
and also narrows it (the retirements), so the declaration is `Clause-②:
yes (narrowing)`. That is on this body's second line, in the changeset
and on the claim. The seat's earlier `no` and `no (narrowing)` were both
its own error.
- **`2c11a7675` (changeset):**
- The `Clause-②` line now reads `yes (narrowing)`. The `minor` grade and
the BREAKING banner stay.
- The ADR-0087 marker keeps `not-required (no-migration-prescription)`.
Its `why` gains one sentence: the FROM → TO block above is a code change
the compiler carries, not a metadata migration for `objectstack migrate
meta`. ADR-0087's vocabulary has no category for that shape exactly, so
the maintainer is asked to rule the reading.
- `check-adr-0087-registration --base origin/main` exits 0 and prints
that sentence in its reason.
- **`0f1906217` (`packages/spec/liveness/email_template.json`):** the
cross-repo evidence is re-measured at the tree's `.objectui-sha` pin
`4997995022f8` and restated with file:line. That covers the `label` and
`description` evidence and the `category` and `isSystem` dead notes.
- `ResourceListPage.tsx#defaultColumns` (:835, rendering `label` :838
and `description` :839), and its fallback at :421.
- `default-schemas.ts`: `email_template` is registered at :336 with no
`listColumns`. `category` (:310) and `isSystem` (:324) are editor inputs
only.
- No reading changed from `0df67f23`: none of the cited files is among
the 80 files the 7 commits between the two pins touch. No grade moves.
- **Gates at `0f1906217`, for the two changed paths:** 60 derived, 59
run, 1 NOT-MEASURED, 0 UNRUN; 57 exit 0, `check:liveness` among them.
- `check:platform-checklist` exits 1 with the same 8 findings as on
`origin/main` `45e206e49`. That is pre-existing; see the note below.
- `check:dual-build-cjs-loads` is NOT MEASURED. It needs every package's
`dist/`, and this round changes no build input, so CI runs it.
- In CI mode, `check-changeset-no-major` with this line 2 reports "the
declared narrowing is accounted for: `@objectstack/plugin-email:
minor`".
- Changed lines: unchanged at 4,575.
## Seat's acceptance notes after the review
- **The `destroy()` drain-hook finding:** `EmailServicePlugin.destroy()`
leaves the `sys_email` outbox drain hook bound on the engine after
kernel shutdown. Carrier: none. It is noted here and not filed. Its only
reach is an engine that outlives its kernel, measured on a test fake,
which is below the filing gate's measured-reach bar. It is pre-existing
and not this PR's. A semantic search for an open card on the class found
none.
- **`check:platform-checklist` red on `main`:** the 8 findings are the
known red that triage closed `not_planned` twice, #22594 and #22758
(`6093524974` sets the direction). Neither card's re-entry line is met.
The gate is kept out of per-PR CI by standing decision
(`.github/workflows/checklist-status.yml`). This seat files nothing.
- **For the maintainer, who approves this Tier H landing in any case:**
ADR-0087's disposition vocabulary has no category that verifiably fits a
compiler-carried removal of runtime exports in a diff that also touches
`packages/spec`. This changeset and stage 1's
(`15205-email-template-org-door-closed.md`) both hold `not-required
(no-migration-prescription)` on that reading. The open choice: rule that
reading into the ADR's addendum, or add a seventh category. The at-tier
review `6113872462` raised it, and it does not block this PR's
substance.
## Merge round: `origin/main` `3fdb92db0`, through `os-regen-merge.sh`
The PR was conflicted against `main` (`mergeable_state: dirty`), so no
pull_request CI ran on `0f1906217`. Two commits fix that; the head is
now `78c3eed63` and the merge base `3fdb92db0`.
- **`be55432f6`** merges `origin/main` `3fdb92db0` through
`scripts/pm/os-regen-merge.sh`. The one conflict was the census figures
table in `content/docs/permissions/system-context.mdx`, a MIXED
`merge=os-regen` path, with no prose in the hunk. It was resolved to
main's figures, and the PR's row-58 prose stayed.
- **`78c3eed63`** re-runs the repo's own generators only:
`gen:system-context-census` and `tenant-audit-census.mjs --write`. Spec
`check:generated` is green, `check:docs` included.
- **Gates at `78c3eed63`:** 136 derived, 136 run, 0 NOT-MEASURED, 0
UNRUN; 135 exit 0. `check:platform-checklist`'s 8 findings are identical
at the merge base `3fdb92db0`.
- **Pure regeneration** (`Regen-provenance` `6114859685`, re-run by the
seat on the committed trees): the net diff covers the same 49 paths
before and after. Exactly 3 paths' changes differ, all generator output:
one census figure (main's base moved, the PR's −1 unchanged) and two
measured-at stamps. The at-tier PASS `6114282872` carries to this head.
- Changed lines: unchanged at 4,575.
---
_Generated by [Claude
Code](https://claude.ai/code/session_013LbZ9MhPp1iriZEtgJqioA)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9e9e6f9 commit 7de0265
49 files changed
Lines changed: 1581 additions & 2994 deletions
File tree
- .changeset
- content/docs
- automation
- permissions
- references
- automation
- system
- docs/audits
- examples/app-showcase/test
- packages
- lint/src
- platform-objects/src/audit
- plugins/plugin-email/src
- templates
- qa/dogfood/test
- services/service-automation/src/builtin
- spec
- liveness
- state-counts
- src
- automation
- contracts
- system
- scripts
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 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
168 | 168 | | |
169 | 169 | | |
170 | 170 | | |
171 | | - | |
172 | | - | |
| 171 | + | |
| 172 | + | |
173 | 173 | | |
174 | 174 | | |
175 | | - | |
| 175 | + | |
176 | 176 | | |
177 | 177 | | |
178 | 178 | | |
| |||
212 | 212 | | |
213 | 213 | | |
214 | 214 | | |
215 | | - | |
216 | | - | |
217 | | - | |
218 | | - | |
219 | | - | |
220 | | - | |
221 | | - | |
222 | | - | |
223 | | - | |
224 | | - | |
225 | | - | |
226 | | - | |
227 | | - | |
228 | | - | |
229 | | - | |
230 | | - | |
231 | | - | |
232 | | - | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
233 | 235 | | |
234 | 236 | | |
235 | 237 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
183 | 183 | | |
184 | 184 | | |
185 | 185 | | |
186 | | - | |
| 186 | + | |
187 | 187 | | |
188 | 188 | | |
189 | 189 | | |
| |||
359 | 359 | | |
360 | 360 | | |
361 | 361 | | |
362 | | - | |
| 362 | + | |
363 | 363 | | |
364 | 364 | | |
365 | | - | |
| 365 | + | |
366 | 366 | | |
367 | 367 | | |
368 | 368 | | |
| |||
0 commit comments