Repository navigation
Commit cc305a3
feat(cli): os migrate organization-ownership — the ADR-0131 D10 inventory and the read-only ceremony plan (C7a) (#22643)
Fixes #22617
Part of #15211
Clause-②: yes (widening)
The Clause-② widening is a new read-only command surface. No existing
command or verdict changes.
## What this adds (C7a of the ADR-0131 D10 ceremony)
1. **The inventory**:
`packages/cli/src/utils/organization-ownership-inventory.ts`. It gives
each object one D10 fate (column drop, mirror deletion, attribution
through a parent anchor, or report) with its citation. 128 objects:
| source | objects | column-drop | mirror-deletion | attribution |
report |
|---|---|---|---|---|---|
| platform, first census (5479883784, at `00d8f6541b`) | 84 | 37 | 5 |
38 | 4 |
| platform, added since (gated census on `main`) | 3 | 1 | 0 | 2 | 0 |
| examples (the census's "28" = 28 files declaring 33 objects) | 33 | 0
| 0 | 31 | 2 |
| cloud supplement (cloud-carried) | 8 | 0 | 0 | 0 | 8 |
| **total** | **128** | 38 | 5 | 71 | 14 |
- **#14570** (`sys_business_unit_member`) is read in: attribution
through `business_unit_id` → `sys_business_unit`, citing 5536484221 and
6067123924. **#15086** (the business unit seeded with a NULL
organization) is read in: attribution on `sys_business_unit` (D3;
pointer 5536478573).
- The ruled categories are counted per table by name: the `sys_metadata`
presentational promotion with its conflict list (6020279837, records
6020163868 and 6020151485), the overlay duplicates (6071418113), the
email-template promotion (6020178017), and the `sys_setting` global rung
moving out (6051723395).
- The `sys_metadata` family's schema change (6068052798) is fate 1 on
all four objects.
- A non-platform table that carries `organization_id` and has no
inventory row takes the application-object default: attribution, which
is the Default Organization under `single` and a report otherwise.
2. **`os migrate organization-ownership`**: the read-only plan (D10
ceremony item 1). For each table it gives:
- the fate and its citation;
- the row counts (total, NULL organization, stamped) and what the fate
touches;
- the ruled categories;
- the derivable rows per anchor;
- the rows whose owner cannot be derived, listed by id with the reason;
- `notNull: { willReceive, reason }`.
It writes the plan to a file (`--out`, default
`organization-ownership-plan-TIMESTAMP.json`) and never overwrites one.
`--json` also prints it.
3. **The completion-marker design**: below. It is design only, with no
code.
### Why `os migrate organization-ownership` and not `os migrate --plan`
- Bare `os migrate` is the schema-drift plan (#2186), and this PR leaves
it unchanged.
- Every data step in the family (`summary-nulls`, `files-to-references`,
`security-catalog-overlays`, …) is a named subcommand. Its bare run is
the read-only preview and `--apply` is its only writing mode. The ADR's
`--plan` names this mode.
- C7b adds `--apply` and the post-check to the same command. Until then
it has no writing mode at all.
### Read-only, and refusal
- **Read-only boot.** It uses the family's read-only boot
(`deferSchemaDdl` + `readOnlyProbe`). It reads the physical database
through the planned driver's raw seam, with SELECT statements only.
- **Refusals.** It refuses the whole plan, names the table, and exits 1
with no file written when:
- the dialect has no catalog statement;
- the catalog or a table read fails;
- a table lacks a column its fate reads;
- a platform-prefixed table carries the column but has no inventory row;
- a table name cannot be quoted;
- the database holds none of the inventoried platform tables (the wrong
`--database-url`).
- **Absent tables.** An inventoried object with no table on this
database is listed as `physical: 'absent'` with `rows: null`. It is
never reported as a table of zero rows.
- **Rotation shards.** `sys_activity` shards are folded onto the base
object.
- **Measured on a real database.** The family pin's served database (an
`os serve` boot, then the next release's artifact) plans 20 tables: 9
column-drop, 11 attribution, and `sys_activity` folded from its shard.
### Where it lives, measured
The inventory and the planner sit under `packages/cli/src/utils/`.
oclif's `pattern` strategy turns every module under `src/commands/**`
into a command, so a data module there would become one. The command is
`src/commands/migrate/organization-ownership.ts`. The inventory is a
typed TypeScript table rather than a JSON file, so the CLI ships it in
`dist` and the planner and the pin read one spelling.
## The completion marker — design only (C7b builds it)
⛔ This PR contains no code that writes or reads the marker. This section
is the design that C7b's last step writes and the v18 boot refusal
reads.
**Where it lives.** One row in `sys_migration`, the deployment-level
migration-flag table (`platform-objects/src/system/migration-flag.ts`,
#3617). The row's primary key is a new spec constant,
`ORGANIZATION_OWNERSHIP_MIGRATION_ID =
'adr-0131-organization-ownership'`, which sits beside
`FILE_REFERENCES_MIGRATION_ID` and `VALUE_SHAPES_MIGRATION_ID`. No new
table. `sys_migration` is itself a D7 column-drop object. Its drop runs
inside the ceremony before the marker is written, so the marker is
always written to the table's final, tenant-less shape.
**What it records.** It reuses the existing columns:
- `applied_at`: when the apply finished.
- `verified_at`: when the post-check passed.
- `blocking`: the number of tables whose post-check did not complete.
This is not the count of reported rows. Reported rows are D10 fate 4:
they are named at boot and do not block it.
- `details`: one JSON document:
- `ceremonyVersion`: the integer `1`, the same `ceremonyVersion` this
plan prints;
- `inventoryDigest`: the sha256 the plan prints, which ties the marker
to the inventory it executed;
- `posture`;
- `tables`: per table, `{ object, fate, remainingNull, notNull:
'applied' | 'withheld', reason }`.
`verified_at` is set only by a post-check that re-ran the plan and found
zero tables left mid-fate.
**When it is written.** Only as the ceremony's last statement, after the
post-check. An interrupted apply leaves no marker; its checkpoint lives
in the ADR-0119 journal (`os migrate resume`). A fresh v18 database gets
the marker when it is created, the way `attestFreshDatastore` attests
the creation-attested migration ids. A database born on v18 has nothing
to migrate.
**How a v18 boot reads it.** This has the same fail-fast shape as
ADR-0093 D5, with no escape hatch.
- **When:** before schema sync and before any plugin `start()`. Additive
sync could otherwise create tables on a database the boot is about to
refuse.
- **How:** a primary-key read of that one row, through the raw read
seam, in the same pre-boot gate as the tenancy-posture refusal.
- **What it decides:**
- A database with no ObjectStack tables is fresh, and is attested.
- A database that holds `sys_organization` but no marker row is refused,
and the refusal names `os migrate organization-ownership --apply`.
- A marker that is unreadable, malformed, at a `ceremonyVersion` below
the runtime's required version, missing `verified_at`, or with `blocking
> 0` is refused.
- **Reads fail toward refused:** the same asymmetry as
`readDataMigrationFlag`.
- **The refusal text** says what was found, that the server is refusing
to start, the command to run, and that a 17.x runtime is the way to keep
17.x semantics.
- ⛔ There is no `OS_ALLOW_*` / `OS_SKIP_*` variable and no flag that
skips the read (ADR-0131 D10 item 5). A marker with tables still
reporting NULL rows boots. Those tables are listed at boot with counts
and the remedy (fate 4), and they stay unconstrained.
## Verification (head `a3f7ab26`)
- **Unit tier.** `pnpm --filter @objectstack/cli typecheck && pnpm
--filter @objectstack/cli exec vitest run --project unit` → `Test Files
278 passed (278)`, `Tests 4115 passed (4115)`, VERDICT command-exit 0.
This includes the new `organization-ownership-inventory.test.ts`, the
enumeration pin:
- the first census's 59 + 25 platform objects and 33 example objects,
frozen as counted;
- the live `PLATFORM_OBJECTS_BY_PACKAGE` registry, which reds by name
for an object with no row;
- `CLOUD_PROVIDED_OBJECT_NAMES`, each pinned as fate 4 and
`cloudCarried`;
- #14570 and #15086 each read in.
- **Integration tier, the files this diff touches.** `vitest run
--project integration
src/utils/organization-ownership-plan.integration.test.ts
src/utils/schema-migrate.one-shot-family.integration.test.ts -t
'organization-ownership|ADR-0131|missing from CALLERS'` → 14 passed.
That covers:
- the per-table plan against a fixture carrying each fate, under
`isolated` and under `single`;
- six refusals, each naming its table (uninventoried platform table,
failing read, missing column, unsupported driver, not an ObjectStack
database, unquotable name);
- **the control:** across two runs the database file's sha256 and a full
schema-and-row dump are byte-equal, and every statement issued starts
with `SELECT`;
- the family pin's no-write cases, now including this command (the
database stays byte-identical and no missing SQLite file is created).
- **Ablation.** Through `scripts/ablation-replace.mjs`, `if
(hasPlatformObjectPrefix(base)) {` was changed to `… && false) {`. The
mutation landed (anchor 1→0) and the uninventoried-table pin went red (1
failed, 9 passed). The restore was verified: blob equals HEAD and `git
diff HEAD` is empty.
- **Gates.** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derives 65 commands. All 65 exit
0 on `a3f7ab26`, and `--ran` reconciles them: 65 derived, 65 run, 0
NOT-MEASURED, all with exit codes.
- Before the fix commit, two were real findings on this diff and are
fixed: `check:doc-authoring` (tracker numbers in citation strings, now
ADR sections and ruling-record ids) and
`check:dispatcher-error-vocabulary` (an unregistered `PLAN_REFUSED`
code, dropped because the refusal carries its `reason`).
- Three were unmet prerequisites (a shallow clone, unbuilt packages);
after the clone was deepened and the packages built, all three ran
green.
- **Lint.** `eslint --no-inline-config` on the changed files exits 0.
The repo-wide lint is CI's.
## Acceptance notes
- **The second census.** #13564's 2026-09-02 ledger (5507087600) answers
404, and the 2026-09-03 cloud-side supplement lives in `cloud`, which
this session cannot reach. Both are NOT MEASURED. The cloud rows are the
names this repository records as cloud-provided. Cloud stays on v17 by
ruling 6094175435, so they carry fate 4 and `cloudCarried`, and C10
assigns them their fates.
- **Parent chains are one level.** A child whose parent is NULL now but
attributable in the same apply is listed as unattributable, with the
reason `parent row … has no organization`. C7b's apply runs attribution
parents-first and its post-check re-plans. Under `single`, the Default
Organization covers those rows anyway.
- **`sys_notification_template` stays on attribution.** It is the
conservative fate: no column tells a seeded row from an authored one, so
no row is selected as a mirror on a guess.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01AB6Nvx7Ue6yzJypw7VTvkj
---
_Generated by [Claude
Code](https://claude.ai/code/session_01AB6Nvx7Ue6yzJypw7VTvkj)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d1d42ec commit cc305a3
7 files changed
Lines changed: 2011 additions & 0 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 | + | |
Lines changed: 202 additions & 0 deletions
| 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 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
Lines changed: 137 additions & 0 deletions
| 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 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
0 commit comments