Repository navigation
feat(plugin-email,plugin-auth)!: close the sys_email_template organization door; retire the provenance stamp and the auth SMS seed (ADR-0131 C4, stage 1) - #22087
Conversation
…retire the provenance stamp An organization's create and update of a sys_email_template row are refused with PERMISSION_DENIED / 403 naming the closed door (ADR-0131 D6, ruling C on section 6 Q1). System-context writes pass: the seeds, the boot sweep, the live projector of a Studio save. The provenance stamp, which marked such an edit customized, retires with its exports. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…render per locale rung A fresh boot with phone sign-in on writes no sys_notification_template row. Each locale rung renders an operator's active row, or the built-in text where no row exists at that locale: byte for byte what the seeded store rendered. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…rganization door Cases 1 and 2 (a Studio save reaches the mail and survives a cold boot) are unchanged. Case 3 pinned the retired behaviour, a data-door edit stamped customized; it now pins the refusal of an edit and a create, and that the metadata-door wording survives the next cold boot. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…door on the docs pages The dated note names ruling C (decision card 22005, record 6020178017) and the two symbols the C4 retirement list gains; the ADR text is otherwise unchanged. The isSystem census row 58 anchors the closed sys_email_template door, the email-templates page says where a template is edited now, and the 14744 audit probe drops its retired plugin-email subject. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…rganizations may edit a row The organization door on sys_email_template is closed (ADR-0131 D6, ruling C), so the is_system help's 'tenants may edit' and the customized help's 'set when an admin edits' became false. Both say what holds now, in the four bundled locales; the en bundle is regenerated by check-i18n-bundles --write. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…form-objects patch Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…gine The ablation that put the seed back stayed green: AuthManager reads its engine from the 'data' service, which the harness did not answer, so the restored seed had no engine to write through. The harness now answers both. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…write site retired node scripts/tenant-audit-census.mjs --write regenerated the census tables (232 write call sites, one fewer: the retired seedPhoneSmsTemplates insert), and the page's hand-written figures follow the census. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…where-matcher contracts The boot harness routes findOne and update through ObjectQL's own dispatch predicates (ledger recorded by check-engine-double-contract --write), and both row stores refuse a where combinator instead of reading it as a field name. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 42 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 20 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9ac213cfa741e0ab1dd1b52c732dece00477ab3d && git checkout 9ac213cfa741e0ab1dd1b52c732dece00477ab3d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7ef50a4fbbf9c819a550b2943d2ac74a7bd39770 f439a1e3e8b159750a1f1896fe9d7ffcf9a512f1 && git checkout -B drift-repro 7ef50a4fbbf9c819a550b2943d2ac74a7bd39770 && git merge --no-ff f439a1e3e8b159750a1f1896fe9d7ffcf9a512f1
node scripts/docs-audit/affected-docs.mjs --json 7ef50a4fbbf9c819a550b2943d2ac74a7bd39770
|
维护者速读(终稿)· PR #22087(C4 第一段,#15205)
Generated by Claude Code |
…e names, ruling dates and foreign example ids (objectstack-ai#22125) Part of objectstack-ai#22093 Clause-②: no Wording only: no schema, key, type, export or error-code change. Patch changeset for `@objectstack/spec`, `@objectstack/service-automation` and `@objectstack/platform-objects`. objectui#11785 is the Studio half (splitting an author message from the detail); this PR changes the server-side strings only. The card keeps two named remainders, listed under "Remainder of the card and its route" below. ## What changed Form help and refusals that an author reads no longer carry service-interface names (`IXxxService.method()`), ruling dates ("maintainer ruling DATE", "ruled DATE") or another product's example ids. The rationale they carried moves into the code comment beside each string. Each describe and refusal still states the rule, why it exists and the repair. ## How "author-facing" was measured (instrument and radius) Studio reaches these strings through five routes. I measured each one at `a543e244f`: | Route | What Studio renders | Instrument | | :--- | :--- | :--- | | R1 | `/meta/types` `schema`: every `DEFAULT_METADATA_TYPE_REGISTRY` type, derived as `metadata-protocol` derives it (`z.toJSONSchema`, `unrepresentable: 'any'`, with the `io: 'input'` retry) | Every `description` in all 27 type schemas | | R2 | `/meta/types` `form`: all 17 `METADATA_FORM_REGISTRY` layouts | Every `description` / `helpText` / `label` / `placeholder` / `hint` | | R3 | The package dialog, which derives its form from `ManifestSchema` client-side (objectui `package-schema.ts`) | Every `description` | | R4 | The flow inspector, which reads node `configSchema` from `GET /api/v1/automation/actions` | An AST string scan of every file that declares a `configSchema` (13 files) | | R5 | Refusals read at a door | An AST scan of every string literal in `packages/spec/src` (1284 files, 80190 string parts, comments excluded) | The class patterns were: interface `\bI[A-Z]\w*(Service\|Driver\|Engine\|…)\b`; ruling `\bruling\b\|\bruled\b\|\b20\d\d-\d\d-\d\d\b`; foreign `steedos\|superset\|salesforce\|apache\|airtable\|…`. **Positive control: both strings the filer measured are found.** - The notify Template help is found by R4 at `service-automation/src/builtin/notify-node.ts:203`, as both an interface hit and a ruling hit. The spec copy is found by R5 at `io-node-config.zod.ts:250`. - `com.steedos.crm` is found by R5 at `manifest.zod.ts:283`. R1 to R3 found neither string (needle count 0). So the Studio-rendered Template help is produced by the service-automation descriptor, not by the spec describe. That is why the producer side is changed too. **After the change**, R1 to R3 hold no interface, ruling or foreign-id hit. The one exception is the "Airtable parity" wording (see Acceptance notes). The needles `II18nService`, `IEmailService`, `com.steedos.crm` and `maintainer ruling` count 0. The R5 hit count drops by 22. ## Every string changed Lines are at `a543e244f`. Before is the internal reference that was removed; After is what replaced it. No changed string carried a tracker number, so no tracker number moved. | # | Where | Reach | Before | After | Rationale now lives in | | :-- | :--- | :--- | :--- | :--- | :--- | | 1 | `services/service-automation/src/builtin/notify-node.ts:203`, notify `configSchema.properties.template.description` | R4: Studio notify Template help (filer-measured) | "else the deployment default (II18nService.getDefaultLocale()) … (maintainer ruling 2026-09-01). A producer-set payload.locale is not consulted." | "else the deployment default locale … The node's payload.locale is not consulted." | Comment above `template:` in the descriptor | | 2 | `spec/src/automation/io-node-config.zod.ts:250`, `NotifyConfigSchema.template` describe | Reference docs and published JSON Schema; the contract row 1 mirrors | Same as row 1, with backticks | Same as row 1 | TSDoc above `template` (it already named both; one sentence added) | | 3 | `spec/src/kernel/manifest.zod.ts:283`, `MANIFEST_ID_EXAMPLES` | R5: every package-id refusal (filer-measured on `POST /api/v1/packages`) | `'com.steedos.crm', 'org.apache.superset'` | `'com.acme.crm', 'org.example.help-desk'` | Doc comment on the constant | | 4 | `manifest.zod.ts:305`, `manifestIdRefusal` | R5: same doors | "Invalid package id 'VALUE' on `manifest.id`. Expected reverse-domain notation ('com.steedos.crm', 'org.apache.superset') — …" | "Invalid package id 'VALUE'. A package id (`manifest.id`) is written in reverse-domain notation, like 'com.acme.crm' or 'org.example.help-desk' — …", with the rule and suggestion arm unchanged | TSDoc on `manifestIdRefusal` | | 5 | `manifest.zod.ts:360-361`, `@example` on `id` | TSDoc; held equal to row 3 | `com.steedos.crm`, `org.apache.superset` | `com.acme.crm`, `org.example.help-desk` | (none) | | 6 | `spec/src/data/field.zod.ts:1142`, `required` describe | R1: object, field | "(maintainer ruling 2026-08-18)" | Removed | TSDoc above `required` | | 7 | `field.zod.ts:1170`, `multiple` describe | R1: object, field | "(maintainer ruling 2026-09-13), and on `radio` by the narrower 2026-08-22 ruling … (maintainer ruling 2026-08-18)" | "on any other type, `radio` included, is REFUSED at parse …" | TSDoc above `multiple` (new Declarability paragraph) | | 8 | `spec/src/ui/view.zod.ts:1175` / `:1221`, `GROUPING_FIELD_RULING` in the padded grouping-field refusal | R5: view save | "… (ruled 2026-09-10.)" | Removed, constant deleted | Comment where the constant was | | 9 | `view.zod.ts:3444`, form-view field `visibleWhen` describe | R1: view | "refused at parse (ruled 2026-08-27):" | "refused at parse:" | TSDoc above | | 10 | `view.zod.ts:3655`, section `collapsible` describe | R1: view | "(ruled 2026-09-18)" | Removed | The collapse-pair TSDoc (already records the ruling) | | 11 | `view.zod.ts:3663`, section `collapsed` describe | R1: view | "(ruled 2026-09-18; refusing the combination at the declaration, and warning on it, were both rejected — nobody can depend on a section that cannot be opened)" | ", so writing `collapsed: true` alone is a correct way to say \"collapsed by default\"" | The collapse-pair TSDoc (already records the rejected letters) | | 12 | `view.zod.ts:3715`, form-view section `visibleWhen` describe | R1: view | "(ruled 2026-08-27)" | Removed | TSDoc above | | 13 | `view.zod.ts:3959`, `SUBMIT_REDIRECT_RULING` in seven `submitBehavior.url` refusals (`:3988`, `:3993`, `:4003`, `:4010`, `:4018`, `:4029`, `:4039`) | R5: form-view save | "(ruled 2026-08-11)" | Removed, constant deleted | Comment where the constant was | | 14 | `view.zod.ts:4097` / `:4123`, `FORM_VIEW_FEATURES_RULING` in the `features.*` predicate refusal | R5: form-view save | "scope root (ruled 2026-08-27)." | "scope root." | Comment where the constant was | | 15 | `view.zod.ts:4444`, redirect-arm `url` describe | R1: view | "…successful submit. Ruled 2026-08-11: (1) …" | "…successful submit. (1) …" | The `checkSubmitRedirectUrl` comment | | 16 | `spec/src/ui/page.zod.ts:941` (`kind`) and `:990` (`source`) describes | R1: page | "(ADR-0065; ADR-0080 amendment 2026-06-30)" | "(ADR-0065; ADR-0080)" | TSDoc above `kind` (new) and `source` (already) | | 17 | `spec/src/system/email-template.form.ts:19`, Identity section help. Its three translated values in `platform-objects/src/apps/translations/{zh-CN,ja-JP,es-ES}.metadata-forms.generated.ts:2183` are hand-written, and `en` is regenerated by `pnpm i18n:extract` | R2: email_template form | "Template identifier resolved by IEmailService.sendTemplate({ template: name, locale, ... })." | "Senders address this template by its name; the locale selects which language version of it is sent." | Comment above `description:` | | 18 | `spec/src/data/filter.zod.ts:475`, null ordering comparand refusal | R5: filter validation | "Ruled 2026-09-01: a null ordering comparand is refused at the validation entrance." | Sentence removed | Builder TSDoc | | 19 | `filter.zod.ts:564`, `{ $field }` in a list position refusal | R5 | "Ruled 2026-08-11: declared = enforced (ADR-0049)." | Sentence removed | Comment inside the builder | | 20 | `filter.zod.ts:589`, null list member refusal | R5 | "Ruled 2026-08-31: a null list member is refused at the validation entrance." | Sentence removed | Builder TSDoc | | 21 | `filter.zod.ts:801`, blank `$between` bound refusal | R5 | "Ruled 2026-09-17: a blank $between bound is refused at the validation entrance." | Sentence removed | Builder TSDoc | | 22 | `spec/src/ui/action.zod.ts:985`, action `description` describe (added in patch round 1) | R1: action, in the authoring (`io: 'input'`) derivation; the action Description help objectui#11785 measured | "(one dialog, not two —)": the dash an earlier strip of the cited ruling left dangling | "(one dialog, not two)" | Docblock above `description` (already cites the ruling; one sentence added) | The regenerated `content/docs/references/**` files follow from rows 2, 6, 7, 9 to 12, 15, 16 and 22. ## Remainder of the card and its route This PR delivers the card minus two named rows. It is therefore `Part of objectstack-ai#22093`, and the card stays open for both. 1. **The governed `submitBehavior` row.** - What: `view.zod.ts:4478`, the top-level `submitBehavior` describe, keeps "(ruled 2026-08-11)". - Why it is not here: `gen:react-blocks` projects this describe into `skills/objectstack-ui/references/react-blocks.md`, which is a governed Tier H surface, so changing it here would make this whole sweep a Tier H landing. Measured: the regenerated skill file differs in exactly that one row. - Route: a separate Tier H draft PR carrying the one describe edit, the regenerated `react-blocks.md` row and its reference-docs row, landed on the maintainer's approval. - A comment beside the describe records the deferral. 2. **The `sys_email` field help naming `IEmailService.send`.** - What: `packages/platform-objects`, `en.objects.generated.ts:2500` and `:2508`. The source is in the `sys_email` object definitions. - Why it is not here: open PR objectstack-ai#22087 regenerates the same four `*.objects.generated.ts` bundles, so folding it here would put two PRs on those bundles at once. - Route: the same class of edit, made after that PR lands. ## Pins - **Refusal `code` is asserted with the sentence:** - `manifest.test.ts` asserts `invalid_format`. - `view-form-features-root.test.ts` asserts `custom`. - **Negative pins on what the author reads.** No interface name, ruling date or foreign id appears in: - `io-node-config.test.ts` - `notify-node.test.ts`, on the Studio-served descriptor text - `manifest.test.ts`. Its headline sentence carries no `manifest.id`, the key is still named as a locator, and there is no steedos/superset/apache. - `filter.test.ts` - `view-form-features-root.test.ts` - `view-submit-redirect-url.test.ts` - **Patch round 1: `action-description.test.ts`.** It derives `ActionSchema` the way `/meta/types` serves it (`io: 'input'`) and asserts three things about the `description` help: - it is present; - no dash is left dangling before a close paren; - it has no ruling date, service interface or tracker id. - **Pins that changed direction.** These earlier pins asserted that the date was present: - `view-submit-redirect-url.test.ts`, 7 assertions: "cites the ruling so the refusal is traceable" - `view-form-features-root.test.ts:68` - `filter.test.ts:644` - `io-node-config.test.ts:347` and its `2026-09-01` pin They are now negative pins. `filter.test.ts:661` used the date as its discriminator between two messages. It now discriminates on the blank-bound headline. - No whole-prose pin was added. ## Where the ruling dates in refusals come from (lineage) This PR reverses no ruling. - The 2026-08-29 adjudication carried out objectstack-ai#12522's charter (comment 5425845726): strip tracker ids, and keep ADR ids and migration commands. That charter does not mention ruling dates. - "ADR ids, protocol versions, error codes and ruling dates stay" is PR objectstack-ai#13298's own commit note (fd289be), not ruled text. The code comments that grew from it, saying the date is "what a refused author can act on", are rewritten here. - Triage 6041940817 on this card directs that "rationale (interface names, dates, tracker ids) moves into code comments". This PR follows that direction for describes and refusals alike, and the seat confirmed it in patch round 1. ADR ids stay. ## Tests and gates **Patch round 1, on the merged head `026204631`.** - **Merge:** origin/main was merged at `54ace18c6` with `bash scripts/pm/os-regen-merge.sh`. - Merge commit `75cec8cd1`, parents `34bf72933` and `54ace18c6`. There were no conflicts. - Step 2 kept the branch's bytes of the 7 reference-docs files main had not moved. - The regeneration ran only after the merge commit (never in MERGE state). It reproduced main's side byte for byte; the only regenerated delta is the action row, in 3 docs files. - Main's landed migration entries are still present on the merged head. Three were checked by name: `flow-edge-unresolved-or-repeated-refused`, `sys-presence-organization-column-retired` and `sys-job-organization-column-retired`. - The delta against main is exactly this branch's 30 paths. - **Builds and regeneration, all under the verify lock:** - `pnpm --filter @objectstack/spec build` passed. - `gen:schema` and `gen:docs` passed. - The `service-automation^...` closure build (spec excluded) passed. - **spec tests:** `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2` over 26 targeted files (round 0's 25 plus `action-description.test.ts`): **26 files, 2475 tests passed.** This is a declared narrowing; CI `Test Core` runs the full suite. - **service-automation:** `notify-node.test.ts`, **16 passed.** - **platform-objects:** `src/apps/translations/*.test.ts`, **24 files, 430 passed.** - **Typecheck:** `@objectstack/spec` `typecheck` passed. - **Generated artifacts:** `pnpm --filter @objectstack/spec check:generated` reports **all 15 generated artifacts up to date** (react-blocks and api-surface included). - **Reverse verification, patch round 1 (one-off):** - `action.zod.ts` went back to the `54ace18c6` bytes; landing confirmed by `grep -c` = 1. - **1 of 16 tests in `action-description.test.ts` went red:** "carries no residue of a stripped reference". - The file was restored from HEAD, hash-identical to its HEAD blob, and the suite was green again (16 of 16). - **Reverse verification, round 0 (at `34bf72933`):** - The 5 touched sources went back to the `a543e244f` bytes. - **39 of 347 spec tests went red across all 5 changed spec test files, and 1 of 16 in `notify-node.test.ts`.** Every red is a pin this PR added or flipped. - Restored byte-identical to HEAD. - **Lint:** `pnpm exec eslint --no-inline-config --format json` over the 20 changed lintable files at `026204631`: **20 files, 0 errors, 0 warnings.** - Population: `eslint.config.mjs` matches `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`. - Invariance: the config never enables type-aware linting (no `parserOptions.project`, see its note near line 328), so this diff cannot move the verdict on any file it does not touch. - **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `026204631` derived 113 families (30 paths against merge base `54ace18c6`). - **109 run, all exit 0**, each exit code captured before any pipe. - **4 NOT MEASURED, recorded as reasoned claims and read from CI on the landing head:** `check:skill-examples`, `check:dual-build-cjs-loads`, `check:i18n` and `check:type-check-debt`. In round 0 each exited 3 (PREREQUISITE NOT MET). `check:type-check-debt`'s re-measure is a 45-task workspace build and is not run outside the verify lock. - `--ran` reconciliation: **113 accounted, 109 run, 4 NOT MEASURED (claimed), 0 unrun.** ## Acceptance notes - **Measured but not changed, as outside the three classes or outside reach:** - `stack.zod.ts:458-459` (`IJobService`, `IEmailService.sendTemplate`): `defineStack` collection describes. R1 to R3 do not serve them, and they are not refusals. - Strings whose reader is a plugin, driver or API developer, for whom the interface is the contract they implement or call. None is in R1 to R3: - `data-engine.zod.ts:165,190` (`IDataEngine.find()`) - `hook.zod.ts:1269` (`IScopedContext`) - `query.zod.ts:429` - the `driver.zod.ts:287-388` refusals (`IDataDriver`) - `automation-api.zod.ts:452` - `execution-context.zod.ts:441` ("the 2026-09-03 ruling"): `preserveAudit` is server-constructed and never authored. - `plugin-rest-api.zod.ts:138` ("ruling record, 2026-09-01"): a tombstone on `RestApiEndpointSchema`. Its own comment records that nothing parses that schema outside its unit tests, so no door reaches it. - `component.zod.ts:365` ("Since the console release of 2026-08-21 (`c86185eb5`)"): a release date and a commit sha, not a ruling. - Undated ruling words. These carry no date and no "maintainer ruling", so they fall outside the class as the seat scoped it: - `action.zod.ts:1139` and `:2157` - `page.zod.ts:93` - `view-grouping-query.ts:433` - `context.zod.ts:50` - `protocol.zod.ts:1681` - `endpoint-publish-gate.ts:368,416` - the `error-code-ledger.zod.ts` notes - "Airtable parity" wording is the only R1 to R3 hit left: `page.zod.ts:911`, `page.form.ts:113`, `view.form.ts:164`, plus `view.zod.ts:1706`, `page.zod.ts:647` and `component.zod.ts:1766`. It is a design note naming a competitor, not an example id. - Salesforce mentions in `export.zod.ts:292` and `object.zod.ts:1528` are comparisons, not ids. - `src/migrations/**`, about 1,240 dated or "ruling" hits: upgrade-guide records, not a form or a door. - `manifest.zod.ts:546`, TSDoc `@example` on `dependencies` (`@steedos/plugin-auth`): TSDoc only. `packages/core/src/artifact-packages.ts:114` cites that exact example, so changing it moves a comment in core. - The `sys_email` field help naming `IEmailService.send` is the second named remainder; see "Remainder of the card and its route". - **objectui:** it has no copy of any changed string at `9990f9e` (grep for `II18nService`, `IEmailService`, `steedos.crm`, `Invalid package id`). --- _Generated by [Claude Code](https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #15205
Clause-②: no (narrowing)
Stage 1 of ADR-0131 card C4 (#15205), under the maintainer's ruling C on ADR-0131 §6 Q1 (decision card #22005): email templates are not overridden per organization. What stops being accepted: an organization's create and update of a
sys_email_templaterow are refused with403 PERMISSION_DENIED, and the message names the closed door. System-context writes still pass: the built-in seed, the declared-template boot sweep, the live projection of a Studioemail_templatesave into its sending row, and later the v18 migration ceremony's promotion (#15211). The template provenance stamp retires, and with it three published exports of@objectstack/plugin-email:bindEmailTemplateProvenanceStamp,unbindEmailTemplateProvenanceStampandEMAIL_TEMPLATE_PROVENANCE_PACKAGE. A boot with phone sign-in on no longer seeds the built-in auth SMS texts intosys_notification_template. What renders unchanged: every email template (the loader still readssys_email_template, and a Studio edit still reaches the mail at once and survives a restart); every built-in auth SMS text, byte for byte, at every recipient locale; and anysys_notification_templaterow an operator already has, which still wins.This PR carries a Tier H ADR edit (a dated note under ADR-0131 §6 Q1), so it needs the maintainer's approval to merge.
The loader half of the card (stage 2: the registry loader,
seedTemplates, the boot sweepbootstrapEffectiveEmailTemplates/EffectiveEmailTemplateSources, the live projector) waits on #15206 (C5). Undersingle, a Studio save of anemail_templatelands organization-scoped today, and no registry read that omits the organization sees it, live or after a cold boot (measured ate67ba80049; the stop report is on #15205). References: #15194 (the ADR-0131 execution tree), #21785 and #22062 (the current shape of the boot sweep, unchanged here).What changes
plugin-email: the door. New modulesrc/email-template-door.ts(not exported) registersbeforeInsert/beforeUpdatehooks onsys_email_template, at priority 10. A write is refused when its hook session names a caller and is not system-elevated. A write with no execution context at all (no caller) passes, the same scope ADR-0123 D2 draws. Delete is not part of this door.EmailServicePluginbinds the door where it bound the stamp, and unbinds it indestroy().plugin-email: the stamp retires.src/email-template-provenance.ts, its per-row test and its three exports are deleted. With the door closed, no non-system update reaches the engine's write, so nothing marks a rowcustomizedany more. Rows already marked keep their mark, and the boot seeders still skip them.plugin-auth: the SMS seed retires.seedPhoneSmsTemplatesand itskernel:readycall are gone.resolvePhoneSmsTemplateBodywalks the locale chain one rung at a time. At each rung it uses an active, non-blank row at that locale; otherwise the built-in text at that locale, when no row exists there at all; otherwise it moves to the next rung. The built-in walk is the floor, as it is on a failed read. That is exactly what the seeded store rendered: the seed inserted the built-in text wherever no row existed, and a deactivated or blank row passed its rung on. Nothing in plugin-auth's package entry changes.bootstrapEffectiveEmailTemplates/EffectiveEmailTemplateSources. The ADR text is otherwise unchanged.content/docs/permissions/system-context.mdxrow 58 now anchors the door (email-template-door.ts#isOrganizationWrite) in place of the retired stamp.content/docs/automation/email-templates.mdxsays a template is changed in Studio and the rows are closed to organization writes.scripts/audits/14744-before-update-per-row-value-probe.mjs: this audit script imported the retired module. Its plugin-email subject and that import are dropped, which is all it needed. The probe still runs (probe: 7 subjects). The dated audit recorddocs/audits/2026-09-multi-update-per-row-value-census.mdstays as measured.platform-objects(cross-lane, its own commitbffc546129): thesys_email_templatefield help foris_system("tenants may edit") andcustomized("set when an admin edits") became false with this change. Both are corrected in en, zh-CN, ja-JP and es-ES.node scripts/check-i18n-bundles.mjs --writeregenerated the en bundle. See Acceptance notes.@objectstack/plugin-emailand@objectstack/plugin-authtakeminorwith the BREAKING banner, and@objectstack/platform-objectstakespatch. The ADR-0087 disposition isnot-required (no-migration-prescription), and each retired export is named.Pins, each negative one ablated and restored by blob
The SMS legs were re-run at head
a4ba9d5070and the door legs at1c2a3ad64b(unit) anda93579f453(dogfood, through dist); neither the door's source nor its tests have changed since. Every leg usedscripts/ablation-replace.mjs, which proved that the mutation landed on disk and that the restore matched the HEAD blob with an emptygit diff HEAD. Every driver carries an EXIT/INT/TERM trap that restores from HEAD.PERMISSION_DENIED, 403, door named); nothing reaches the driverplugin-email/src/email-template-door.test.tsPATCHandPOST /api/v1/data/sys_email_templateas the org admin)email-template-overlay-survives-boot.dogfood.test.ts, case 3ablation-dist-preflightfound the marker in 2 dist filesPATCHanswered 200), cases 1 and 2 green; after restore and rebuild,--absentpassed, the tree was clean, and all 3 were greencustomizedemail-template-door.test.ts§3sys_notification_templaterowplugin-auth/src/phone-sms-seed-retired.test.tskernel:readydataservice AuthManager writes through; fixed ina93579f453, then re-ablatedNot ablated, positive pins: a
singleStudio save still reaches the mail and survives a cold boot. That is dogfood cases 1 and 2, unchanged and green.Tests
Every result below is at head
a4ba9d5070. Each exit code was captured before any pipe.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives on this change. Reconciled with--ran(exit codes recorded):123 derived, 123 run, 0 NOT-MEASURED, 0 UNRUN.check-tenant-audit-census(and its self-test): the retired seed's write site leaves the census at 232.node scripts/tenant-audit-census.mjs --writeregenerated the census page and the counts file, and the page's hand-written figures follow it.check:engine-double-contractandcheck:where-matcher: the new SMS test doubles now conform. The ledgerscripts/engine-double-contract.pinned.jsonwas updated bynode scripts/check-engine-double-contract.mjs --write.check:dual-build-cjs-loadsandspec check:skill-examplesfirst answered PREREQUISITE NOT MET (exit 3).@objectstack/plugin-emailtest: 535 passed. typecheck: OK.@objectstack/plugin-authtest: 2616 passed, 10 skipped. typecheck: OK.@objectstack/platform-objectstest: 1006 passed. typecheck: OK.turbo build --filter=@objectstack/dogfood^..., 63/63).email-template-overlay-survives-boot.dogfood.test.ts,email-template-materialization.dogfood.test.tsandemail-template-boot-sweep.test.tspassed 8 of 8. These are the dogfood files that bootplugin-email; none sends SMS.examples/app-showcase/test/email-template-locale.test.tspassed 6 of 6.npx eslint --no-inline-config --format json. None was reported as ignored by the config.eslint.config.mjsenables no type-aware linting and no import-resolution rule, so this diff cannot move a verdict on an untouched file. The repo-widepnpm lintis CI's.维护者速读(草稿)
sys_email_template),数据接口会返回 403 并说明这扇门已关;平台自己的写入(内置模板、声明模板、Studio 保存后的同步)照常。随之退役"已自定义"标记钩子及其三个导出。手机短信验证码/邀请的内置文案不再在启动时写成sys_notification_template行,而是按语言逐级取:有运营自建的行就用行,没有就用内置文案,发出去的短信逐字节不变。ADR-0131 §6 Q1 下补一条日期注记,记录你 10 月 6 日的裁决 C。sys_metadatafamily goes tenant-less; the per-organization overlay axis retires; managed content is sealed (ADR-0131 D6/D7/D13) #15206),否则单租户下 Studio 改模板会失效(已实测)。platform-objects那一处字段说明的跨车道修正可以随本 PR 一起落地。Acceptance notes
email-template-overlay-survives-boot.dogfood.test.tsstay green and unchanged. Its first two cases (thesingleStudio edit path) are byte-unchanged and green. Its third case, "a data-door edit is stamped customized and survives the next cold boot", pinned exactly the behaviour this PR retires. Measured unchanged on this branch, it went red with the new refusal (expected 403 to be 200). Per the original order's rule ("a dogfood file that pins the retired behaviour is updated in this PR"), case 3 now pins the closed door: an edit and a create are refused with 403PERMISSION_DENIEDand the door named, nothing is markedcustomized, and the metadata-door wording survives the next cold boot.platform-objectshelp-text fix sits indomain:engine's package, outside the claim's file surface. It is made because this change made the two texts false. It lives in one separate commit (bffc546129), so the seat can declare it or drop it.bootstrap-declared-email-templates.tsstill names the retired stamp in a@link. That is a comment only, but that file is outside stage 1.packages/spec/liveness/email_template.json's_notenarrates the stamp; it is narrative, not an evidence anchor,check:livenessdoes not read it, andpackages/specis untouched here.Generated by Claude Code