Repository navigation
Commit 869a381
feat(cli): the organization-ownership ceremony carries the sys_metadata organization layer into the environment, and a choice deletes only the rows the reviewed plan listed (ADR-0131 D10, C7b S1) (#23452)
Part of #15211 (stage S1 of C7b; the other stages are this card's S4,
#23437, and the split cards #23429 and #23430)
Fixes #23398
Clause-②: yes (widening)
## What
`os migrate organization-ownership --apply` carries the `sys_metadata`
organization layer into the environment (ADR-0131 D10; rulings A on
#22011 and #22008; ruling 6163114098, Q1 B and Q6 A; seat answer Q2 A;
review rulings in 6165835673).
- **The plan gains `metadataConflicts`, and `--apply` gains
`--metadata-choice ROW_ID`.** That is the widening, so cli is `minor`.
Nothing is removed. This PR changes only `@objectstack/cli`.
- **A new step, `metadata-promotion`, after the authored conversion:**
- **Form withdrawals are carried first, under every posture, and
fail-closed.** Every view of that name the environment serves still open
takes an organization's withdrawal, stored or code-served, in every
package. It is saved through the metadata door into the view's own
package, and any organization's withdrawal wins.
- **Under `single`, an organization customization whose key no other row
holds is promoted in place,** with its history. This covers `view`,
`dashboard`, `report`, `translation` and `email_template`; the key is
type, name, package and bundle member.
- **A key held by more than one row is a conflict.** Under `single` it
blocks until `--metadata-choice ROW_ID` names one row. The key then ends
with **exactly one environment row** defining the chosen body:
- where the key has an environment row, that row takes the body in
place, by one UPDATE. Its version is the chosen row's after its lineage
is folded, so it names that body's snapshot. Not through the door: the
door writes a packaged item's customization into the package's slot,
never onto a package-less row of the key (measured; see the dogfood);
- where it has none, the chosen organization row is promoted in place.
Any withdrawal another row of the key holds is kept on that body, and
also saved through the door (its history, and the package's slot). The
row is **read back**, the choice is **recorded in the checkpoint**, and
only then are the key's organization rows deleted, in **one** statement,
by id. A rerun after a crash anywhere in this accepts the same
`--metadata-choice` and finishes.
- **Under a wall:** nothing is promoted unless named, and **nothing is
ever deleted** (ruling 6163114098, Q6 A's text, applied by the seat). A
conflict nobody chose for is reported, not blocking, and its rows hold
the column.
- **History (Q2 A): `event_seq` and every `(type, name, version)` unique
environment-wide.** The door numbers a save from history in its own
scope, so:
- **A promoted row takes its history with it.** Its organization's
lineage of the item moves above the environment's numbers (wholly above
its own range too, because 17.x keeps unique indexes on both counters)
and becomes environment history. Every later environment save, the
ceremony's or the runtime's, numbers above it, and the row's version
still names its own snapshot.
- **A new step, `history-renumber`, runs just before `column-drops`, on
every apply, even after a checkpoint.** It lays out every lineage still
tagged to an organization above the environment's: event ranges by one
offset each, with their commit ranges; version lineages with the
organization's rows.
- Under `single`, that history then becomes the environment's too.
- Under a wall it stays its organization's: private, renumbered before
the column can go.
- **A choice deletes only rows the reviewed plan listed.** One mechanism
for both flags, on S4's `ReviewedPlan`. The plan file's
`metadataConflicts` and `templateConflicts` are read into the reviewed
plan. Before anything is written, each chosen conflict must hold exactly
the rows the file listed for it, or the apply refuses with
`plan-conflict-mismatch`, naming the conflict, the rows added and the
rows gone.
- **17.x databases plan.** `sys_metadata.bundle_member` first shipped in
18.0.0-next.0, and the ceremony boots with schema changes held. So
`environment-overlay-duplicates` groups by `bundle_member` only where
the column exists; on a 17.x table the member reads as NULL, the overlay
key's folded part.
- **The anonymous form doors keep their organization read.** By the
seat's ruling, its deletion moves to S2 (#23429), which lands with S5's
boot refusal, so no release serves an organization-only withdrawal open.
`rest` and `metadata-protocol` are back to `main`.
## Merge round (review 6173411667 PASS at `d02b699d65`; head
`82bd7e0b80`, base: `main` `222293f60`, S3 merged)
- **Merge.** `main`, now carrying S3 (#23503), is merged through
`os-regen-merge.sh`.
- Two conflicts:
- `organization-ownership-plan.ts`: `TablePlan` gains both
`metadataConflicts` and S3's `grants`, and both types are kept;
- `organization-ownership-apply.test.ts`: both sides' new describes are
kept.
- The script's rerun found no generated path to restore.
- **The uniqueness gate stands first in S3's rewritten
`runColumnDrops`.** The gate pin and the two column-drop doubles now use
S3's `{ table, column }` port shape.
- The cli dependency closure was rebuilt on the merged tree: S3's
plugin-security pin reads its dist.
- **Should-fix (S1).** The door's lock verdict is asked once, before the
history fold, the promotion and the in-place write, so a refusal leaves
nothing written.
- Pin: ⭐ `the lock is asked before the history fold and the promotion:
under a wall, a refusal leaves history and rows as they were`, with a
history table and `isolated`.
- Ablation: removing the early check reddens it and the locked-item pin.
- **Nit (S2).** The door save after the in-place write sends the row as
written, with its stored lock family.
- Pin: `the door save after the in-place write sends the row as written,
its stored lock family kept`.
- Ablation: sending `body` reddens it.
- **`history-renumber` keeps blocking on a duplicate**, by the seat's
call. Its refusal names the table, each item holding the duplicated
number (by row) and the remedy. Both messages are pinned: the
`event_seq` one verbatim, the version one by its parts.
- **Checks on `82bd7e0b80`:**
- cli typecheck exit 0;
- the ceremony suites (9 files, S3's included): 146/146;
- narrowed lint of the 8 changed `.ts` files: 0 errors, 0 warnings;
- `main` merges clean again.
- The integration verdict is CI's Test Core on this head.
## Delta round 2 (review 6172666364; head `d02b699d65`)
The seat's rulings, each fixed and pinned, each ablation red with the
restore verified (blob == HEAD):
| Item | Fix | Pin | Ablation → red |
|---|---|---|---|
| **N1** the fold landed on the organization's own events | each fold's
offset (`event_seq` and versions) is based on `max(environment, the same
organization's other items)`; excluding the lineage itself keeps a rerun
idempotent | ⭐ `N1: folding one item of an organization never lands on
that organization's other events, on a rerun too` (the review's smallest
case, 17.x's unique indexes, a crash after the first fold's move, then
the rerun) | base on the environment alone → the N1 pin |
| **N2** checksum | the in-place UPDATE restamps `checksum` with
`hashSpec` | ⭐ `restamps checksum from the written body` | restamp
removed → that pin |
| **N2** stored `_lock` keys | the row's stored `_lock` family is kept,
whatever the chosen body carries | ⭐ `keeps the lock family the
environment row stores` | carry removed → that pin |
| **N2** the door's lock check | a new port asks the item read's
`editable` (the save door's own verdict) before the write; a locked item
blocks the conflict, nothing written or deleted | ⭐ `a locked item stays
locked` | check removed → that pin |
| Wall with an environment row (ruled as built) | each organization row
left is a report line: "Remaining: … blocks column-drops on sys_metadata
… Remedy: …" | ⭐ the wall pin asserts the line | |
| Uniqueness under a wall (ruled, on condition) | `history-renumber`
verifies `event_seq` and `(type, name, version)` uniqueness
environment-wide on every apply and blocks naming any duplicate;
`column-drops` refuses unless that verification passed in the same apply
| ⭐ `column-drops never runs unless this apply's history-renumber
verified uniqueness` (two environment events sharing a number) |
verification removed → that pin |
| Bundle member in `validateMetadataChoices` | the conflict key carries
the member | `two conflicts of one item that differ only by bundle
member are two conflicts, each chosen for` | member removed → that pin |
| Title and citation | retitled (no rest, metadata-protocol or form
doors); the fold fix is `917e0b8cc` | | |
Checks on `d02b699d65`:
- cli typecheck exit 0. The ceremony suites (`organization-ownership*`,
8 files): 135/135. Narrowed lint of the 8 changed `.ts` files: 0 errors,
0 warnings.
- Main merges clean (`git merge-tree`), so it is not merged.
- **The integration verdict is CI's Test Core on this head.** The local
tier was not re-run.
- **The previous head's local runs.** On `468b74a09d`, CI's Test Core
passed (all 6 shards). I stopped the local sweep there for this round:
58 of 65 gates had run, all exit 0.
## Verification of the previous round (head `468b74a09d`, base: `main`
`f9b421716d`, S4 merged)
**Merge.** `main`, carrying S4, is merged through
`scripts/pm/os-regen-merge.sh`.
- The two textual conflicts (the command's flags, refusals and
description, and the test imports and helper) stack both intents.
- The script's rerun found no generated path to restore.
- `main`'s `served-catalog` test gains the `views` port, which serves no
view.
- The #23446 pin's resumed-step list gains this PR's two steps.
**The binding** (seat option A):
| Pin | Ablation → red |
|---|---|
| ⭐ `--metadata-choice: a row that joined the conflict since the review
refuses the apply, naming it; nothing is written`; `a reviewed row gone
since refuses too, and a plan file that lists no such conflict`; and the
unchanged group resolves | metadata leg removed → both refusal pins |
| ⭐ `--template-choice: a row that joined the conflict since the review
refuses the apply, naming it; nothing is written`; and the unchanged
group resolves | template leg removed → the template pin |
| `reviewedPlanFromFile` reads both lists, keyed and sorted | |
**The review round (6165835673)** keeps its pins and ablations:
| Finding | Pin | Ablation → red |
|---|---|---|
| F1 17.x refused | `plans a 17.x sys_metadata with no bundle_member
column…` | filter removed → the pin and 7 more plan pins |
| F2 versions collide | ⭐ organization-only history; ⭐ under a wall; ⭐
two organizations with no environment history | fold removed → 2 ⭐;
`single` fold removed → the two-organization pin; seq offset not wholly
above → the organization-history pin |
| F3 two environment rows / rerun | ⭐ org-over-environment choice (one
UPDATE, no organization row cleared); ⭐ the packaged-item pin (below); ⭐
crash, then the same choice; `a write that does not land deletes
nothing` | in-place write sent through the door instead → both ⭐;
read-back removed → the read-back pin; applied-choice acceptance removed
→ the crash pin |
| Wall ruling | ⭐ a chosen row defines the environment row, nothing
deleted; ⭐ two organizations, the other stays | posture branch removed →
both |
Checks:
- cli typecheck: exit 0. The ceremony suites (`organization-ownership*`,
8 files): 129/129. Narrowed lint of the 8 changed `.ts` files: 0 errors,
0 warnings.
- **The whole cli integration tier:** CI's Test Core passed on that head
(all 6 shards).
- **`dispatch-gates`:** the local sweep on that head was stopped at 58
of 65, all exit 0, for the next round. The interim head `b0093a3337` ran
65/65 at exit 0.
- **Dogfood as an operator,** with the real `os` binary built from this
head. One `objectstack dev --seed-admin` boot of the showcase, then
stopped. I made the database a 17.x shape (`bundle_member` dropped;
history keeps 17.x's unique `(organization_id, event_seq)` and
`(organization_id, type, name, version)` indexes), and planted a
conflict on `showcase_inquiry.contact`'s package-less key: the
organization's withdrawn copy (with only an organization history, 1–2)
beside an open environment copy.
1. The plan exits 0 and lists the conflict (`env_copy`,
`legacy_org_withdrawal`).
2. A row joined the conflict after the plan. `--apply --plan-file
plan.json --metadata-choice legacy_org_withdrawal` exits 1,
`plan-conflict-mismatch`: reviewed `env_copy,legacy_org_withdrawal`,
current with `joined_later`. The database file is byte-identical, with
no ceremony row.
3. That row removed, the same invocation resolved the conflict.
`env_copy` took the chosen body in place, withdrawal kept; its version
is 4, the folded `corg2` snapshot. The organization row is deleted, and
the package's served copy carries the withdrawal. After the `rewrite`
remedy, the resume ran `history-renumber`: no history is left tagged to
an organization, and no duplicate exists. `column-drops` blocks on
`sys_metadata`'s declared column, which is S2's.
4. On a reboot, both form doors answer 404, nothing landed, and a
runtime save took `event_seq` and `version` 5, above everything, with no
duplicate.
- **The dogfood found two defects this round, both fixed and pinned:**
- The fold's `event_seq` offset was smaller than a lineage's own span.
17.x's unique index refused it row by row. Fixed in `917e0b8cc`, and its
base corrected in this round's N1.
- The door writes a packaged item's customization into the package's
slot, so the conflict's package-less environment row never took the
body. The read-back caught it and nothing was deleted. Fixed in
`0162d47cd`.
## Acceptance notes
- **Deviations for the seat to ratify:**
1. Under `single`, when an organization row is chosen over the key's
environment row, the environment row keeps its record and takes the
chosen body in place, by one UPDATE, doing to the row what the door does
(N2). Its version is the chosen row's after its lineage is folded. The
chosen organization row is then deleted with the key's other
organization rows; its body survives in the environment row. This is the
"replace in place" the ruling offered: it keeps one environment row at
every point, and any other order leaves zero or two.
2. A promoted row's history is re-tagged to the environment (folded) at
promotion. Under `single`, the history left tagged to an organization is
folded by the late step. History rows keep their bodies and their order;
only `organization_id` and the numbers change.
3. Under a wall, a conflict nobody chose for no longer blocks the step:
it is reported in the step's notes, and its rows hold the column.
Otherwise a wall's conflicts, whose rows are never deleted, would block
every rerun.
- **`--apply` shipped in `@objectstack/cli@18.0.0-next.1`.** This PR
adds to it (a plan key, a flag, two steps), hence `minor` and `Clause-②:
yes (widening)`. It refuses no invocation that release could finish. A
conflict now blocks the new step until it is chosen, but on such a
database `18.0.0-next.1`'s `--apply` already stopped at `column-drops`,
which never drops `sys_metadata`'s still-declared column. The changeset
says so.
- **The choice binding (seat item 7) is in this head,** for both flags;
see above. It closes the `--template-choice` gap S4 left (6167752569).
The changeset's example carries `--plan-file`.
- **No MongoDB path** (maintainer ruling 6171293009).
- Closing-target claim: 6172270710 on #23398 (branch
`claude/issue-15211-c7b-promotions`).
- **Not changed, from the review's non-blocking list:**
- the conflict key does not read `state`;
- identical-body pairs are conflicts;
- the two conflict lists in one plan;
- the carry skip under `single` for a key an organization row holds.
With the doors' organization read kept, no form is open meanwhile.
- **An observation, not caused by this PR.** On a freshly seeded
showcase database, `rewrite` blocks on the `--seed-admin` grants
`admin_full_access` and `organization_admin_no_bypass` (6163356782;
escalated in 6165835673). The dogfood removed them as the blocker's
remedy says.
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01USccGXR13sWyTeEwe8QFdH
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 47d569e commit 869a381
9 files changed
Lines changed: 2352 additions & 32 deletions
File tree
- .changeset
- packages/cli/src
- commands/migrate
- utils
| 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 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| 36 | + | |
36 | 37 | | |
37 | 38 | | |
38 | 39 | | |
| |||
95 | 96 | | |
96 | 97 | | |
97 | 98 | | |
| 99 | + | |
98 | 100 | | |
99 | 101 | | |
100 | 102 | | |
| |||
143 | 145 | | |
144 | 146 | | |
145 | 147 | | |
146 | | - | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
147 | 151 | | |
148 | 152 | | |
149 | 153 | | |
| |||
192 | 196 | | |
193 | 197 | | |
194 | 198 | | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
195 | 208 | | |
196 | 209 | | |
197 | 210 | | |
| |||
256 | 269 | | |
257 | 270 | | |
258 | 271 | | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
259 | 276 | | |
260 | 277 | | |
261 | 278 | | |
| |||
358 | 375 | | |
359 | 376 | | |
360 | 377 | | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
361 | 385 | | |
362 | 386 | | |
363 | 387 | | |
| |||
380 | 404 | | |
381 | 405 | | |
382 | 406 | | |
| 407 | + | |
383 | 408 | | |
384 | 409 | | |
385 | 410 | | |
| |||
468 | 493 | | |
469 | 494 | | |
470 | 495 | | |
| 496 | + | |
471 | 497 | | |
| 498 | + | |
472 | 499 | | |
473 | 500 | | |
474 | 501 | | |
| |||
499 | 526 | | |
500 | 527 | | |
501 | 528 | | |
| 529 | + | |
502 | 530 | | |
503 | 531 | | |
504 | 532 | | |
505 | 533 | | |
506 | | - | |
| 534 | + | |
507 | 535 | | |
508 | 536 | | |
509 | 537 | | |
510 | | - | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
511 | 573 | | |
512 | 574 | | |
513 | 575 | | |
| |||
570 | 632 | | |
571 | 633 | | |
572 | 634 | | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
573 | 639 | | |
574 | 640 | | |
575 | 641 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
130 | 130 | | |
131 | 131 | | |
132 | 132 | | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
133 | 136 | | |
134 | 137 | | |
135 | 138 | | |
| |||
0 commit comments