Repository navigation
feat(spec)!: retire an email template's category and isSystem — the grouping and the built-in flag nothing read (#22876) - #22990
Conversation
…ources) Tombstones on EmailTemplateDefinitionSchema, the orphan category enum removed, the form inputs cut, the D2 conversion, the D2/D3 registry entries, the ledger rows re-noted, the built-in auth templates, showcase, fixtures, lint pins, docs and the retirement pin. Generated artifacts follow. Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
…oves authorable-surface (two [RETIRED] rows), authorable-defaults (two defaults leave with their keys), json-schema.manifest (the category enum, deleted on purpose), api-surface, export-origins, declaration-map, the reference pages, and the four metadata-forms translation bundles (the two cut form inputs). Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
…de a template interpolation Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
…retired category enum Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
…667 with the two retired email-template form labels Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
…ad-key-retirement-batch # Conflicts: # packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts
…ad-key-retirement-batch # Conflicts: # packages/spec/liveness/README.md
…rged tree The merge took main's side of the generated page (it carries the header sentence the registry rename brought); regenerating from the merged sources puts the two retired-key rows back beside it. Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 24 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 140 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 31ef4a31a254366458dafed20f364bfeb0192269 && git checkout 31ef4a31a254366458dafed20f364bfeb0192269
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1959aacd1572fc8e403f8bd9c0ddbfe981321351 1406d0ee78d3bcab5649fc11c7dcfc83ef6b4c50 && git checkout -B drift-repro 1959aacd1572fc8e403f8bd9c0ddbfe981321351 && git merge --no-ff 1406d0ee78d3bcab5649fc11c7dcfc83ef6b4c50
node scripts/docs-audit/affected-docs.mjs --json 1959aacd1572fc8e403f8bd9c0ddbfe981321351
|
|
CI note · On
|
…ad-key-retirement-batch # Conflicts: # packages/spec/src/type-alias-convention.pin.test.ts
The merge took main's side of the generated reference index and the assembled package-api page; regenerating from the merged sources puts this branch's email-template rows back beside main's changes. Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
…ad-key-retirement-batch
…ps authoring the retired category and isSystem The #22994 fixture modelled a 17.x-emitted email template with `category: 'custom'` and `isSystem: false`. This branch retires both keys, and its tree-scoped absence pin refuses an email-template literal that authors either one. The test's assertions do not depend on the two keys (its dashboard legs carry the conversion proof); the header and fixture notes are corrected to say why the template carries neither. Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Reviewed at 2026-10-12T14:38Z by an isolated subagent of ① Derived judgmentsAccept set,
Public surface,
Authorable-surface artifacts: two rows become ADR-0087 registration, three obligations:
The seams the safety claim leans on (the dependence the brief names):
Form and bundles: Liveness ledger: both rows STAY Writers inside the radius: the six built-in auth templates (12 lines), the showcase template, the ADR-0054 dogfood fixture, the downstream-contract fixture, two plugin-email test fixtures and three docs examples drop the keys; the typed ones were forced by Lint pins: Docs and census: Scope and class: exactly the two seed keys; no other Merge-round deltas in the net diff: the type-alias pin resolved to 775 (main's Check-runs on this head as read: 48 runs — 41 success, 7 skipped, 0 failure, 0 in progress (the second ② Semver level
③ Boundary flagsStage-0 report
Build report
Seat orders Merge-round report
PR acceptance notes: bound kept — verified on the file list; the stale checklist prose — accepted; the #22874 / #22943 overlap merged in — the diff carries their lines; the out-of-repo population NOT MEASURED — stated in the changeset; the companion merged and the pin at or past it — verified on the head and on The card's "Done when", against the diff and the measurements on the card: authoring refuses Escalations: none. Implemented-by: VERDICT: PASS |
Fixes #22876
Clause-②: no (narrowing)
Parked — do not land yet
Unlock condition: this PR lands with
.objectui-shaat or past the objectui companion's merge, objectstack-ai/objectui#12193 (triage ruling on the card: the editor change first, then the spec tombstones, in the same landing). The pin bump is its own card, filed when the companion is accepted; it queues behind the pin bump that landed with #22874. This PR stays draft, and the owning seat lands it.Expected red: one, and it is not this diff's.
pnpm check:platform-checklistexits 1 with 8 findings: 7 absent#symbolanchors indocs/qa/platform-checklist/areas/*.jsonthat namemetadata-protocolandservice-storagesymbols, plus the symbol-anchor floor ofattachments-storage.json(27 resolved, floor 28). The same command onorigin/main246edf5b3, in a clean detached worktree, exits 1 with the identical 8 lines (diffof the two finding lists: identical). This diff touches none of those files.What this does
Retires
categoryandisSystemfrom the email template (EmailTemplateDefinitionSchema, theemail_templatemetadata type), per ADR-0049 enforce-or-remove. This is the batch the seat bounded in claim6117767270: exactly these two keys and no otherdeadrow.Neither key has had a reader since ADR-0131 D6/D10 retired the projection of declared templates into
sys_email_templaterows (categoryfed a Setup column;isSystemfed the built-in seeder's stand-down). Both carried a schema default, so every parsed template carried them, and the dead-property advisory fired on every template with no way for an author to clear it.The retirement kit (stage 0's table, surface by surface)
retiredKey()on both keys in thestrictObject(packages/spec/src/system/email-template.zod.ts). Each prescription opens with the fully qualified key and closes with the houseos migrate meta --from 17sentence. The orphan enumEmailTemplateDefinitionCategorySchemaand its type leave with the key: its only other consumer was the type-alias pin.email-template.form.tsdrops thecategoryselect and theisSystemcheckbox. This is the form Studio renders.email-template-category-is-system-removed. Protocol 18,retiredFromLoadPath,retiredAfter: '17.7.0', order 62. It inserts betweenelementTextVariantHeadingLevelsandfieldColumnListsCanonicalizedby identifier, and is defined directly above the latter. It strips both keys from everyemailTemplates[]entry, one notice per key, and names the template in the path. Its fixture has 4 notices: the built-in shape (both keys authored), a 17.x-emitted shape (both defaults materialized) and an untouched canonical control.RETIRED_KEYS_BY_MAJOR[18]gainssystem/EmailTemplateDefinition:categoryandsystem/EmailTemplateDefinition:isSystem.RETIRED_DEFS_BY_MAJOR[18]gainssystem/EmailTemplateDefinitionCategory, and itsjson-schema.manifest/system.jsonkey is deleted on purpose: the build named it as a disappeared schema. The D3 entryemail-template-category-is-system-retiredcomes with itsSTEP18_RATIONALEfragment (order 94).deadand are re-noted as tombstones (verifiedAt2026-10-12), with the_noteand the README row updated. The dead count is unchanged, so the state-counts shard does not move (check:livenessgreen).authorable-surface(two[RETIRED]rows),authorable-defaults(the two defaults leave with their keys),api-surface,export-origins,declaration-map, three reference pages, and the fourmetadata-forms.generated.tsbundles (a pure deletion of the two form labels).plugin-email's six built-in auth templates dropcategory: 'auth'andisSystem: true(12 lines). The same goes for the showcase template, the ADR-0054 dogfood fixture, the downstream-contract fixture and twoplugin-emailtest fixtures.lint-liveness-properties.test.tsnow pins that a parsed template carries neither key and lints clean, and that the parse refuses an authored one.runtime-gate.inert-type-writes.test.tsreturns to a silentemail_templatewrite and asserts the door's schema refusescategory. The showcase control is silent again.automation/email-templates.mdx(two examples and thecategoryparagraph) andpermissions/system-context.mdxwith its census row prose: theEmailTemplate.isSystemdeclaration is now a tombstone.@objectstack/specminorwith the BREAKING banner, the FROM → TO table, the one-line fix and one ADR-0087 disposition (registered). It also names@objectstack/plugin-emailand@objectstack/platform-objectsaspatch, and carries theClause-②line.Deviations from stage 0's kit and the claimed file surface (named, measured)
packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts(the metadata-form catalog loses the two labels; it reads 668 after merging feat(spec): an email field declares itself a correspondent address for inbound mail (FieldSchema.mailAssociation) #22943's +1) andtype-alias-convention.pin.test.ts(775 → 774, the orphan enum's isomorphic pin and its module import).acceptRetiredDefaultResidue) is NOT adopted. Stage 0 did not raise it, and the decision is recorded in both retired-key entry files. Both keys were defaulted, so a 17.x parse materialized them into every template it emitted. I followed theai/Agent:memory.longTerm.storeprecedent: the replayed D2 conversion heals built artifacts (the artifact door) and stored rows (convertStoredItem, which every served and execution read runs), and the card's "Done when" requires the parse to refuse both keys. Residue would accept the default values at parse. The two doors that skipped the replay were measured on [finding] email_template.category and email_template.isSystem have no consumer once templates resolve from the registry: retire both in the next enforce-or-remove batch #22876 (seat option C) and are fixed onmainby ADR-0087 forward conversion misses two artifact doors: a ^18 artifact built on main gets no D2 replay and fails to boot; AppPlugin registers raw non-security collections, so /meta lists and the email loader read the unconverted copy #22994 and ADR-0087 window: a^18artifact built during the v18 prerelease gets no D2 replay of an entry retired after it was built, so it fails to boot (door 1, split from #22994) #23001; this head carries both.Studio save, measured at the door Studio calls
Booted own backend:
pnpm dev -- --fresh -p 38876(showcase, built-in templates registered byEmailServicePlugin), signed in as the seeded admin, then killed by recorded PID and port.GET /api/v1/meta/email_templatereturned 25 templates (the showcase's and the 6 built-ins × 4 locales). None carriescategoryorisSystem.GET /api/v1/meta/types: the servedemail_templateschema has 19 properties and neither key. The served form's fields arename,label,locale,description,subject,bodyHtml,bodyText,variables,fromOverride,replyToandactive.PUT /api/v1/meta/email_template/auth.password_reset?mode=draftis the doorclient.meta.saveItemuses for Studio's draft save. Each body was the served built-in minus the_-prefixed read decorations, with a new subject:success: true, "Saved customization overlay (env-wide, state=draft)".category: 'auth'→ 422,code: INVALID_METADATA, one issue atcategory(invalid_type) carrying the prescription.isSystem: true→ 422,INVALID_METADATA, one issue atisSystemwith its prescription.category: 'custom',isSystem: false→ 422 with both issues. With no residue stage, a client body carrying the materialized defaults is refused too.Merge round (head
1406d0ee7)scripts/pm/os-regen-merge.sh.b71a2d817mergedorigin/main0ede0f8ab, which carries door 1 (fix(metadata-core): a ^18 artifact built during the v18 prerelease boots on a later runtime — the door replays step 18 for a floor of 18.0.0 #23023,58f5c9099). Its one hand conflict waspackages/spec/src/type-alias-convention.pin.test.ts, the isomorphic-pin count:mainaddedInstallModeSchema(775 → 776) and this branch removes the category enum's pin (776 → 775). The pin's own counter (theexport type Iso…declarations it matches at line start) counts 775 on the merged file, and both history notes are kept. The driver tookmain's side ofcontent/docs/references/index.mdxandcontent/docs/references/api/package-api-assembled.mdx;432845a82regenerates both from the merged tree (gen:schema, thengen:docs).c116f7607mergedorigin/main4267484fe, door 2 (fix(runtime): AppPlugin registers the forward-converted copy of every collection #23024), with no conflict.1406d0ee7. Door 2's newpackages/runtime/src/app-plugin-converted-collections.test.tsmodelled a 17.x-emitted email template withcategory: 'custom'andisSystem: false, and this PR's tree-scoped absence pin refused it (CITest Core (1/6)onc116f7607). The fixture now authors neither key. As merged, with both keys present, that file passed 8 of 8, so door 2 plus this PR's D2 conversion strip both keys on the artifact boot. Its assertions do not depend on the keys; its dashboard legs carry the conversion proof. The pin's exclusions are unchanged.git diff BASE HEAD | git patch-id --stablegives2db65c91c0before (base246edf5b3, heade4e96505e) and0372033243after (base4267484fe, head1406d0ee7). Excluding the type-alias pin, the runtime fixture and the generated reference pages, it is74063f68a2on both sides, and all 35 of those files have byte-identical per-file patch-ids. The diff is now 1,283 changed lines over 40 files.1406d0ee7. All exit 0 unless noted:check:generated(all 14 artifacts current) andcheck-i18n-bundles(in sync).@objectstack/speclocal project in two shards: 653 files, 19,500 tests (1 todo). Repo project: 56 files, 909 tests, including the 24-test retirement pin.@objectstack/lint137 / 6,569;@objectstack/platform-objects70 / 1,096;@objectstack/plugin-email33 / 515; the runtime fixture file 8 / 8.typecheckfor runtime and spec.dispatch-gates --commandsderived 137; 136 exit 0.pnpm check:platform-checklistexits 1 with the 8 findings it also gives on4267484fe(identical lists).--ran: 137 derived, 137 run, 0 NOT-MEASURED.1406d0ee7: 41 check runs, 36 success, 5 skipped,Test Coreand all six shards green.Evidence (final head
e4e96505e, after mergingorigin/maintwice:e5687cb6bwith #22943, then246edf5b3with #22874)Both merges went through
scripts/pm/os-regen-merge.sh. Conflicts were settled by stacking both intents: the catalog count pin (#22943 +1, this −2) and the adjacentjobrow inliveness/README.md.references/system/email-template.mdxwas regenerated over the merged tree withgen:schema+gen:docs, so it carries #22874's header sentence and the two retired rows. The four bundles were regenerated withnode scripts/check-i18n-bundles.mjs --writeafter the first merge (no diff against the text merge).pnpm --filter @objectstack/spec build, thencheck:generated: all 14 artifacts up to date, exit 0.@objectstack/speclocal project, in two shards: 650 files, 19,416 tests passed (1 todo), exit 0.@objectstack/specrepo project: 55 of 56 files, 907 tests passed, exit 0. NOT MEASURED:scripts/build-schemas-check-mode.test.ts, which alone exceeds the foreground cap (the precedent's reading); the gate whose mode it tests,check:authorable-surface, is green.src/system/email-template-category-is-system-retirement.test.ts(repo project; listed invitest.repo-tests.json): 24 tests.@objectstack/lint137 files / 6,529 tests;@objectstack/platform-objects69 / 1,083;@objectstack/plugin-email33 / 515;@objectstack/example-showcase33 / 409;@objectstack/downstream-contract3 / 31;@objectstack/dogfood(the three email-template files plusexpression-conformance) 4 / 16.typecheck, all exit 0: spec (includingcheck:test-typecheck, which compiles the pin's two@ts-expect-errorlines), lint, platform-objects, plugin-email, example-showcase, dogfood, downstream-contract.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) derived 137 commands at this head. 136 exited 0. One exited 1:pnpm check:platform-checklist, the pre-existing red named under "Expected red".origin/mainhas since moved three commits (a2240c4dc), and none of them touches a file in this diff; the seat re-merges at landing.--ran(each line recorded ascommand :: exit code): 137 derived, 137 run, 0 NOT-MEASURED, 0 UNRUN.pnpm lint. ① Population:eslint.config.mjslints**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}minusNEVER_LINTED. ②--no-inline-config --format jsonover the 23 changed lintable files ate4e96505e: 23 files, 0 errors, 0 warnings, none ignored. ③ Invariance: the config never enables type-aware linting (noparserOptions.project, noprojectService), and the diff touches neither the config nor a baseline, so no verdict on an untouched file can move.Ablations (one-shot, through
scripts/ablation-replace.mjs, each wrapped in a trap and restored by blob equal to HEAD with an emptygit diff HEAD; spec-source imports, nodist/involved)category: retiredKey(EMAIL_TEMPLATE_CATEGORY_REMOVED)replaced bycategory: z.string().optional()(anchor 1 → 0): 10 of the pin's 24 tests went red (the refusal, every-value, both-keys, write-door,defineEmailTemplateDefinition,defineStackenvelope, did-you-mean, tsc-channel, boot-door and tree-walk rows), and 14 stayed green.category: 'auth'planted onAUTH_PASSWORD_RESET_TEMPLATE(anchor 1 → 0): exactly the tree-scoped walk went red, namingpackages/plugins/plugin-email/src/templates/auth-templates.ts: category beside bodyHtml. A first attempt whose replacement still contained the anchor was refused by the tool as a no-op and re-run with a landing replacement.New pin
packages/spec/src/system/email-template-category-is-system-retirement.test.ts:email_templateregistry binding,defineEmailTemplateDefinition, anddefineStack, which refuses withSTACK_SCHEMA_INVALID/ 422. Every value is refused, the old defaults included. Also pinned: the did-you-mean, the walked shape, the tsc channel, and the enum gone from@objectstack/spec/system.@objectstack/spec#testalready declares (check:cross-package-test-inputsgreen). It judges either key at the top level of an object literal that also carriesbodyHtml, after masking strings and comments, plus YAML mappings and MD/MDX fences by language.Acceptance notes
deadrow, notrealtime_subscription, not thesys_email_templateobject's owncategory/is_systemcolumns, not.objectui-sha, noskills/**,.claude/**ordocs/adr/**.docs/qa/platform-checklist/areas/integration-system.jsondescribes the showcase fixture as "category workflow". This is a one-line prose fix for whoever touches that file next.Generated by Claude Code