Repository navigation
Commit ed03834
feat(cli,core): the organization-ownership ceremony promotes customized email templates and moves the settings global rung (C7b, PR 3 of 3) (#23329)
Fixes #22937
Clause-②: yes (widening)
PR 3 of 3 for the C7b card: the template promotion and the settings
global-rung move, per the maintainer's ruling 6138591940 (Q3 「本卡补做提升」,
Q4 「本卡单开 PR 3」) and dispatch 6151180611. PR 2 (#23034) landed as
`2808069121`. **Predecessor:** the closed stacked draft #23195. Its
branch carries this work, and it now merges `main` (`f36a3a34`,
`34989ec7`, `db208864`), so this diff reads off `main` alone. The
contract review went FAIL first (6154293191). The patch round
(6154321875) fixed the template delete on resumed runs and brought in
the walled promotion. #15211 remains open for the boot refusal, which
lands with C8.
## What it adds (9 files, +1,025 / −47; 1,072 changed lines, none
generated)
1. **`emailTemplateDefinitionFromRow`**, in
`packages/core/src/security/` beside the two catalog mappers. It is a
pure mapper whose one import is the spec's
`EMAIL_TEMPLATE_FLOOR_LOCALE`, the fallback locale. Its pin holds the
body to `EmailTemplateDefinitionSchema`: no undeclared key, and a full
`safeParse` passes.
2. **`template-promotion`**, a ceremony step (ADR-0131 §6 Q1, ruling C),
which runs under every posture.
- **What it promotes:** exactly the rows PR 2's mirror deletion keeps
for it (B1). That is every customized row, every row whose `customized`
is unknown, and every row no seeder wrote (`managed_by` NULL or outside
package / platform / system), with or without an organization.
- **How:** grouped by `(name, locale)`. A single candidate, or identical
candidates (judged on the mapped body), becomes one environment
`email_template` item through the real metadata door.
- **What it does not promote:** seeded, uncustomized copies,
organization-stamped or not. They stay mirrors (the seat's answer
6140351542).
- **Conflicts:** differing bodies for one `(name, locale)` are listed in
the plan and the `--apply` report (`templateConflicts`), each candidate
with its row id and organization. They count as remaining and block
`verified_at`, so nothing is guessed and no candidate is deleted.
- The operator chooses with the new repeatable flag **`--template-choice
ROW_ID`** on `--apply`. The chosen body is promoted, and the group's
rows are deleted by id only after that promotion is verified.
- A choice that names no candidate, or two candidates of one conflict,
is refused before any write (`template-choice-refused`).
- `--template-choice` without `--apply` is refused.
- **It also stops, keeping every row,** on:
- a customized or authored row with no name, which is named;
- an environment template that already exists and differs, because the
environment's edit is never overwritten;
- a door refusal.
3. **`settings-move`**, a ceremony step that executes the inventory's
`global-rung-move` departure (#15207's ruling 6043540291).
- Each `sys_setting` row with `scope = 'global'` and no organization is
copied into `sys_platform_setting` by `INSERT … SELECT`, so `value` and
the `value_enc` handle move verbatim.
- The destination row is read back equal before the source row leaves
`sys_setting`. A mismatch throws mid-write, and the checkpoint holds, so
a re-run resumes.
- **It stops when** a key `sys_platform_setting` already serves has a
different value, or when source rows differ.
- After it runs, B2's count for this departure reads 0. The
`declared-rule-mirrors` departure still counts until D2.
4. **Step order:** attribution → authored-conversion →
**template-promotion** → **settings-move** → rewrite → mirror-deletion →
column-drops.
5. **The report** gains `movedOut` / `movedIn` per table, and the human
face prints the moves and the template conflicts. Counts reconcile as
before − deleted − moved out + moved in.
## Merge with PR 2's landed form
`organization-ownership-apply.ts`, its test and the command conflicted
with `2808069121`. PR 2's behaviour is kept exactly:
- B1's provenance keep;
- a claim that evaluates NULL keeps its row;
- B2's departure count in `remainingFor`, and departures on the report;
- the fate-2 gate's re-read before the first DELETE.
PR 3 is layered on top. For `sys_email_template`, in every posture,
mirror deletion first re-runs the idempotent promotion after the fate-2
gate; if that pass is not done, the step is blocked and nothing is
deleted. Then the selector deletes only the complement of the promotion
population, which means the seeded uncustomized mirrors. Promoted rows
leave by id, in chunks with a count read-back, and only once this run
has verified their environment copy.
## Pins (`organization-ownership-apply.test.ts`, 17 cases)
- **One `--apply`, complete and verified.** The acceptance fixture holds
four templates to promote (customized, admin-authored with an
organization, admin-authored without one, unknown provenance), a seeded
mirror, an organization's seeded copy, and the global rung with a secret
handle. One run ends `{ complete: true, blocking: 0 }`. All four are
promoted, every template row is gone, the rung is in
`sys_platform_setting`, and effective access is identical (the PR 2 pin,
unchanged). The DELETE allow-list pins the settings selector with
`organization_id IS NULL`.
- **A blocked promotion certifies nothing:** with a differing
environment template, the ceremony stops at the promotion, ends
`complete: false`, `verified_at` stays null, and the blocker names the
template.
- **Between runs:** killed at a fate-2 DELETE with `template-promotion`
checkpointed, then a customized row inserted. The resume promotes it and
ends complete; no row is deleted unpromoted.
- **A nameless customized row** blocks the promotion, stays, and is
named.
- **Walled, no conflict:** complete, `blocking: 0`; the template is
promoted and its rows are deleted.
- **Walled, a conflict:** blocked, with the blocker naming both
candidates with their organizations and `--template-choice`. Both rows
are kept and nothing is promoted.
- **A chosen conflict:** the chosen body is promoted and verified, then
both rows are deleted.
- **A bad choice:** an unknown row id and an ambiguous pair are each
refused with a located message, and nothing is written.
- **The declared-rule mirror departure** still blocks `verified_at` and
is named; the moved rung reads 0.
- **Settings conflict:** nothing is moved or deleted for that key.
**Ablations** through `scripts/ablation-replace.mjs`, each restored to
the HEAD blob with `git diff HEAD` empty:
- **A1:** the pre-delete promotion re-run disabled. The between-runs pin
goes red.
- **A2:** the nameless blocker disabled. The nameless pin goes red.
- **A3:** the walled-promotion wall restored. Four walled cases go red.
- **A4:** the conflict detection disabled. The conflict and chosen pins
go red.
- **A5:** the chosen body replaced by the first candidate. The chosen
pin goes red.
- **A6:** the choice validation disabled. The refusal pin goes red.
- **A7:** the delete selector's template keep replaced by a no-op claim.
Still green. The measured cause: the re-run promotion verifies every
population row whenever it completes, so the id-only delete never meets
an unverified row that the keep would hold. The keep is a second line of
defence behind A1's gate, which is red under its own ablation.
## Gates
- **At `1bede353`** (the patch round, on `main` `cc14657e`):
- `@objectstack/core` and `@objectstack/cli` typecheck, both exit 0;
- core `src/security/`: 39 files, 887 tests passed;
- the full `@objectstack/cli` suite, CI-sharded: unit 2/2 (287 files,
4,292 tests) and integration 8/8 (121 files, 1,067 passed, 2 skipped:
the live-Postgres cells, which skip without `OS_TEST_POSTGRES_URL`, as
in CI);
- `dispatch-gates --commands` derived 67, all exit 0 on the first run.
`--ran`: "67 derived, 67 run, 0 NOT-MEASURED (a DERIVED zero)".
- **At `db208864`,** after merging `main` (six commits, none of this
PR's files), a scoped re-check: closure rebuilt; core and cli typecheck
exit 0; core `src/security/` 887 passed; the ceremony suites, 4 files
and 39 tests, passed; the derivation is the same 67.
- **Dogfood:** no dogfood suite exercises the ceremony. NOT MEASURED,
because none exists.
- **Contract review:** FAIL 6154293191 at `34989ec7`, then the delta
review at `db208864` (on the PR).
## Not in this PR
- The `sys_sharing_rule` declared-rule mirrors ride D2 (6140351542).
- The `sys_metadata`-family column drop is a precondition of certifying
the ceremony (6150126995, #15211).
- The `sys_user_permission_set.permission_set_id` column and its two
indexes are dropped by #15211, not by this PR (pointer 6154334043).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DjKf52bcyXa3fbGW723phR)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 3490bf5 commit ed03834
9 files changed
Lines changed: 1025 additions & 47 deletions
File tree
- .changeset
- packages
- cli/src
- commands/migrate
- utils
- core/src/security
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| 32 | + | |
32 | 33 | | |
33 | 34 | | |
34 | 35 | | |
| |||
84 | 85 | | |
85 | 86 | | |
86 | 87 | | |
87 | | - | |
88 | | - | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
89 | 91 | | |
90 | 92 | | |
91 | 93 | | |
| |||
106 | 108 | | |
107 | 109 | | |
108 | 110 | | |
109 | | - | |
| 111 | + | |
| 112 | + | |
110 | 113 | | |
111 | 114 | | |
112 | 115 | | |
| |||
140 | 143 | | |
141 | 144 | | |
142 | 145 | | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
143 | 153 | | |
144 | 154 | | |
145 | 155 | | |
| |||
172 | 182 | | |
173 | 183 | | |
174 | 184 | | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
175 | 189 | | |
176 | 190 | | |
177 | 191 | | |
| |||
261 | 275 | | |
262 | 276 | | |
263 | 277 | | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
264 | 287 | | |
265 | 288 | | |
266 | 289 | | |
| |||
276 | 299 | | |
277 | 300 | | |
278 | 301 | | |
279 | | - | |
| 302 | + | |
280 | 303 | | |
281 | 304 | | |
282 | 305 | | |
| |||
342 | 365 | | |
343 | 366 | | |
344 | 367 | | |
| 368 | + | |
345 | 369 | | |
346 | 370 | | |
347 | 371 | | |
| |||
371 | 395 | | |
372 | 396 | | |
373 | 397 | | |
| 398 | + | |
374 | 399 | | |
375 | 400 | | |
376 | 401 | | |
| |||
382 | 407 | | |
383 | 408 | | |
384 | 409 | | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
385 | 433 | | |
386 | 434 | | |
387 | 435 | | |
| |||
411 | 459 | | |
412 | 460 | | |
413 | 461 | | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
414 | 466 | | |
415 | 467 | | |
416 | 468 | | |
| |||
454 | 506 | | |
455 | 507 | | |
456 | 508 | | |
457 | | - | |
| 509 | + | |
458 | 510 | | |
459 | 511 | | |
460 | | - | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
461 | 515 | | |
462 | 516 | | |
463 | 517 | | |
464 | 518 | | |
465 | 519 | | |
466 | 520 | | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
467 | 528 | | |
468 | 529 | | |
469 | 530 | | |
| |||
0 commit comments