Skip to content

Commit 707b3cd

Browse files
feat(cli,spec): a host declares the ADR-0131 D10 fates of its own objects through os migrate organization-ownership --fate-supplement (#23348)
Fixes #23280 Clause-②: yes (a host-declared fate supplement becomes a published input of `os migrate organization-ownership`) ## What this does The v18 organization-ownership ceremony (ADR-0131 D10) refuses every platform-prefixed table that carries `organization_id` and has no fate in its inventory. That stays right for the platform's own objects. But no inventory row covers a host's own objects (cloud's control-plane and tenant tables, for example), so the plan refused every host database. This PR adds one declared channel through which a host states those fates, the inventory's C10 stage: - **The input is declared in the spec.** `packages/spec/src/system/fate-supplement.zod.ts` exports `OrganizationOwnershipSupplementSchema`, a JSON array of `OrganizationOwnershipSupplementEntrySchema` rows. It also exports the row's building blocks: `OrganizationOwnershipFateSchema`, `...RowConditionSchema`, `...AnchorSchema`, `...CategorySchema` and `...DepartureSchema`. - A row carries `object`, `fate`, `citation`, `categories?`, `note?`, plus only the fields its fate reads: `mirror?` on fate 2; `anchors` (required) and `departures?` on fate 3; `departures?` and `notNullExempt?` on fate 4. - Every shape is strict. A key written on the wrong fate is refused by name, and the platform-only markers (`census`, `cloudCarried`, `external`) are not authorable. - **One flag, read in both modes:** `os migrate organization-ownership --fate-supplement FILE`. - The command reads the file before the database is opened. It passes the merged inventory as `inventory` to `buildOrganizationOwnershipPlan` (plan mode) and to `applyOrganizationOwnership` (`--apply`). - The apply path's semantics are unchanged beyond receiving `inventory`. - **The merge joins host rows on the platform's terms** (`mergeHostFateSupplement` in `organization-ownership-inventory.ts`). Each host row takes census `host-supplement` and is planned and applied exactly as a platform row with that fate would be. Per `domain:spec#2`'s answer 3: - a row naming an object the platform inventory already gives a fate is refused; - a row whose table this database lacks is listed `absent`, so one supplement serves both the control-plane and the tenant database; - an undeclared table that holds data is still refused by name. - **Refusals, never a half-read:** - The file may be missing, not JSON, not an array, hold a row the schema refuses, declare one object twice, or name a platform object. - Each case is a `HostFateSupplementRefusal` with a reason code, mapped through `refuse()` inside the `--json` envelope (`plan_refused` / `apply_refused`). - The envelope names `file`, plus `row` (zero-based index) and `object` for a row refusal. - Reason codes: `fate-supplement-unreadable`, `-unparsable`, `-invalid`, `-duplicate-object`, `-platform-object`. - **The undeclared-table refusal gains one remedy clause** (answer 4). It now ends: "…owes it a row, or, when the host provides the object, the host declares its fate in a fate supplement passed with --fate-supplement FILE." Its condition is unchanged, and this is the only edit in `organization-ownership-plan.ts`. - **The `CLOUD` premise sentence** (`organization-ownership-inventory.ts`, formerly `:177-179`) no longer says "Cloud stays on v17 (ruling record 6094175435)". It now cites cloud ruling record `6146394952` (objectstack-ai/cloud#2866 A: cloud's tenant databases run this ceremony), and says a host fate supplement replaces the row. - The citation is spelled as a record id rather than a tracker number, because it is a runtime string (`check:doc-authoring`). - The header comment now says the same. - ⛔ No fate is guessed inside objectstack for any cloud table. The 8 hard-coded `cloud(...)` placeholder rows are kept, unchanged and fateless, until a supplement replaces one. - ⛔ No ambient input: no environment variable and no conventional path. The flag declares no `env:`. ### Why `--fate-supplement` "Supplement" is ADR-0131 D10's own word for this input ("cloud's supplement for cloud-declared ones"), and "fate" names what it supplements. A bare `--fates` could be read as a filter over the plan's fates, and `--supplement` alone does not say what it supplements. The spec module is named after the flag (`fate-supplement.zod.ts`). The fuller "organization-ownership-supplement" made its generated reference page title 51 characters, outside the docs title band (36 to 46), and no ladder rung fits it. Renaming the module was the in-scope fix; widening the docs tooling was not. ### One shape, not two hand-kept copies (H1) The CLI already depends on `@objectstack/spec` and imports its schemas (`ObjectStackDefinitionSchema.safeParse` in `validate`, `ManifestSchema` in `plugin build`). - **Derived:** the CLI's row vocabulary now comes from the spec. `OrganizationOwnershipFate`, `RowCondition`, `Anchor`, `InventoryCategory` and `InventoryDeparture` are aliases of the spec types, and `ORGANIZATION_OWNERSHIP_FATES` is `OrganizationOwnershipFateSchema.options`. - **Pinned equal:** only the top-level row key list stays in the CLI (`InventoryEntry` also carries `census`, `cloudCarried` and `external`). It is held equal to the spec three ways: - compile-time spec→CLI: `mergeHostFateSupplement` assigns a parsed spec row into `InventoryEntry`; - compile-time key equality: `KEYS_AGREE` in `organization-ownership-inventory.test.ts`. Reverse-verified below; - runtime CLI→spec: every platform inventory row, minus `census` / `cloudCarried` / `external`, parses under `OrganizationOwnershipSupplementEntrySchema`. The platform rows use all 5 condition forms, all 3 anchor kinds, both category forms and both departure forms, so this check covers the whole vocabulary in use today. ## Measurements (dispatch Zone 2) - **H1 (row shape):** measured as stated. A row is the inventory entry minus `census` (the merge stamps `host-supplement`), minus the platform markers `cloudCarried` / `external`. These are output markers only: the plan copies them onto `TablePlan` and nothing branches on them. - **H2 (format, flag):** JSON only. No migrate sibling reads a YAML input, so YAML was not added. - **H3 (`cloudCarried`):** the plan copies it to the table plan (`organization-ownership-plan.ts`, `planPresentTable` / `absentTable`), and apply reads it nowhere (`git grep cloudCarried packages/cli/src`: plan, inventory and the inventory test only). - So the 8 `cloud(...)` rows are fate-4 placeholders with a marker. As the PM read it, a supplement row naming one of them is not "a platform object": it replaces the placeholder in place. - Without a supplement those rows are byte-identical in shape to before. Their citation text changed, per the claim. - **H4 (digest):** `inventoryDigest` hashes the merged list, so a plan's digest names its supplement (pinned in the integration test). - `--apply` run with a different supplement (or none) than the checkpoint it resumes is **not refused**. `applyOrganizationOwnership` compares the stored checkpoint's digest with the new plan's, discards a mismatched checkpoint, prints "the inventory changed since the recorded checkpoint, so every step runs again (each is idempotent)", and re-runs every step. - The kept plan file is not an input of `--apply`, so planning with supplement A and applying with supplement B is not refused either. - With no supplement, `--apply` refuses at its own plan stage, because the host tables are undeclared. No new mechanism was built; see Acceptance notes. - **H5 (duplicates):** two rows naming one object are refused (`fate-supplement-duplicate-object`), naming both row indexes. - The check lives in the CLI merge, not the spec. A uniqueness `.refine` would be a dropped refinement in the published JSON Schema, and `dropped-refinements.baseline.json` is shrink-only. - **H6 (fixtures):** reused the existing plan harness, a `better-sqlite3` file database behind a `PlanReader`, as in `organization-ownership-plan.integration.test.ts`. ## Pins (each with a negative control) | # | Pin | Where | Negative control | |---|---|---|---| | 1 | A host-declared fate is planned like a platform row: a host table with `sys_approval_action`'s fate and anchor plans to `sys_approval_action`'s exact `TablePlan`, except `object` / `citation` / `census` / `tables` | `organization-ownership-host-supplement.integration.test.ts` | Same database without the supplement: refused at that table | | 1 (command) | `--fate-supplement` reaches the plan (real SQLite) and `--apply` (the merged inventory reaches the apply step) | `organization-ownership.fate-supplement.integration.test.ts` | Without the flag: refused (plan), and no `inventory` passed (apply) | | 2 | An undeclared host table holding data is still refused, by name, and the refusal names `--fate-supplement FILE` | both integration files | The full supplement plans the same database | | 3 | A row naming a platform object is refused (`fate-supplement-platform-object`, with row and object) | inventory unit test, command test (`--apply`) | The same row naming an uninventoried object joins; a cloud placeholder is replaced | | 4 | A row whose table this database lacks is listed `absent` | host-supplement integration test | The same rows on the database that holds them are `present` | | 5 | The plan names every table on a control-plane-shaped and a tenant-shaped fixture | host-supplement integration test | The platform inventory alone refuses each fixture, naming a host table | Fixtures use the card's 7 control-database and 3 tenant-database tables, plus labelled placeholders (`sys_billing_fixture_1..7`, `ai_fixture_1..8`), per answer 2. The real names take fate 4. Their citation says "test data, not any host's declaration". ## Ablations and reverse verification All three ran through `scripts/ablation-replace.mjs` in wrap mode, under `os-verify-lock`. Each also had an EXIT/INT/TERM trap restoring from `HEAD` by absolute path, and each restore was proven as blob == `HEAD` with `git diff HEAD` empty. 1. **The command does not pass `inventory`.** Both `...(inventory ? { inventory } : {})` sites were replaced (anchor 2→0, blob `b5aa7463` → `901af2d0`). - Red: "with it, the host-declared fate is planned like a platform row" and "with --fate-supplement, the apply step receives the host row" (2 failed / 27 passed). - Restored to `b5aa7463` == HEAD. 2. **No platform-object refusal.** The guard was set to `if (false …)` (anchor 1→0, blob `c9791d01` → `e0e81f8c`). - Red: "a row naming a platform object is refused…", "the merge refusals reach the caller from the file too", and the command's "a row naming a platform object (--apply)" (3 failed / 26 passed). - Restored to `c9791d01` == HEAD. 3. **Reverse verification of `KEYS_AGREE`:** a key `reverseProbe?: string` was added to `InventoryEntry`, then `tsc --noEmit` was run on `packages/cli`. - Exactly one error: `organization-ownership-inventory.test.ts(161,7): error TS2322: Type 'true' is not assignable to type 'false'`. Restored to `c9791d01` == HEAD. - The first attempt was a no-op: the tool refused because the replacement contained its anchor, so tsc never ran. It was re-anchored and re-run. ## Verification The final head is `a0cbcf9cf`. `origin/main` was merged twice through `scripts/pm/os-regen-merge.sh`: at `cc14657ed` (`2da5e78d6`), then at `bbb975771` (`a133014ac`), which resolved one conflict with PR #23329 in the command's flags (both flags kept byte-identical). One type-only commit (`a0cbcf9cf`) keeps the derived row condition's lists read-only, so the merged `SEEDED_BY` typechecks. Against `bbb975771`: 25 files, +1861 / -75. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` gives 114 commands at `a0cbcf9cf`. - All 114 ran on that head, with each exit code captured before any pipe. All exited 0, none exited 3. - Reconciled with `--ran`: "114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)". - `check:dts-closure` and `check:dual-build-cjs-loads` first failed on one partial `@objectstack/organizations` dist. My own stray build had left it, and the files were not in this diff. Both were re-run green after a turbo rebuild under the lock. - `check:type-check-debt` ran under the lock, because it builds the workspace first. - **Spec:** - `pnpm --filter @objectstack/spec check:generated`: all 14 artifacts up to date on the merged tree. - `pnpm --filter @objectstack/spec test` (local): 670 files, 20284 passed, at `11764ed1f`, before the merge. On the merged head, `fate-supplement.test.ts` (8) and `type-alias-convention.pin.test.ts` (3) re-ran green. - `test:repo`, in 3 shards on the merged head: 58 of 59 files fully green (shard 1: 20 files / 307; shard 2: 20 files / 280; shard 3: 18 complete files green). - **Declared narrowing:** `scripts/build-schemas-check-mode.test.ts` ran 20 of its 88 cases, all green. Each case re-runs `build-schemas` (13–28 s), so the file alone is about 30 minutes of serial lock time on this shared box. The remaining 68 cases are NOT MEASURED locally and left to CI's repo project. The diff does not touch `build-schemas.ts` or `authorable-surface.base.json`, and `check:authorable-surface` is green. - **CLI:** - The six ceremony files: `organization-ownership-inventory` 22, `-plan.integration` 11, `-apply` 9, `-host-supplement.integration` 7, `organization-ownership.apply-flags` 3, `.fate-supplement.integration` 7 — 59 passed on the merged head. - `pnpm --filter @objectstack/cli typecheck`: green (tsc, and `check:test-typecheck` OK). - The whole `unit` layer in 4 shards: 287 files, 4306 tests passed. - The integration tier was run for the touched files only (above); the rest is declared to CI. - **Lint:** `eslint --no-inline-config` over the 10 touched TypeScript files: exit 0. - **Not caused by this diff:** `packages/cli/test/hook-timeout-override-refusal.test.ts` timed out once in each of its two CONTROL legs, on two runs out of three, under load (`load average` 4 to 9). Each leg spawns `vitest list` against the 5 s default timeout and measured 3.9–5.1 s. The third run passed 4 of 4, and the file is not in this diff. ## Acceptance notes - **The apply checkpoint across supplements (H4):** a changed supplement discards the recorded checkpoint, and every step re-runs idempotently. It is never refused, and the kept plan file is not read by `--apply`. That is the existing #23034 behaviour; this PR only feeds the supplement into the digest. Noted, not filed: it is neither a defect nor a contract violation. - **The default inventory's digest moves once.** The corrected `CLOUD` citation is part of the hashed rows, so an apply checkpoint recorded on a pre-merge build re-runs its steps once (each is idempotent). v18 is unreleased (pre mode), so no released checkpoint is affected. - **No hand-written docs page** documents the ceremony's flags. Measured: no file under `content/docs` names `--fate-supplement` or `--backup-taken`. So no docs page was edited, and `variant-docs.json` exempts the two new unions as `generated-reference-only`, stating that gap. - **Spec-gate registrations a new exported schema requires**, beyond the generated projections: - `llms.txt`'s inventory count (`check:llms-txt`); - two `scope` rows in `empty-state-registry.mts` (`check:empty-state`); - two `variant-docs.json` rows (`check:variant-docs`); - seven isomorphic pins in `type-alias-convention.pin.test.ts` (`check:spec-parsed-alias`, ADR-0122, instead of seven `XParsed` synonyms). - At landing, the seat records the mechanism (a host supplement read through the command's flag) on #15211, as its release `6148865508` says. --- _Generated by [Claude Code](https://claude.ai/code/session_012uFegdfR6K6uqvLzt7XsdZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e790cfc commit 707b3cd

25 files changed

Lines changed: 1861 additions & 75 deletions
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/cli': minor
3+
'@objectstack/spec': minor
4+
---
5+
6+
feat(cli,spec): `os migrate organization-ownership --fate-supplement FILE`. A host declares the ADR-0131 D10 fates of the objects it provides, and the ceremony plans and applies them as it does the platform's own rows
7+
8+
Clause-②: yes (widening)
9+
10+
The v18 organization-ownership ceremony refuses any platform-prefixed table that carries `organization_id` when its inventory gives that table no fate. That is right for the platform's own objects, but a host's objects have no row in the inventory, so every database a host runs was refused. The host now declares those fates itself, in one JSON file, and passes the file to the command. **Adoption: a host passes its supplement through the flag; with no flag, the ceremony behaves as before.**
11+
12+
- **The file is declared in the spec.** `OrganizationOwnershipSupplementSchema` (`@objectstack/spec/system`) is a JSON array of `OrganizationOwnershipSupplementEntrySchema` rows. Each row gives `object`, `fate` (`column-drop`, `mirror-deletion`, `attribution` or `report`), `citation`, and only the fields its fate reads: `anchors` (required for `attribution`), `mirror`, `departures`, `categories`, `notNullExempt` and `note`. Every shape refuses an unknown key, and a key written on the wrong fate is refused rather than ignored.
13+
- **Both modes read it.** The plan plans each row on the same terms as a platform row, and `--apply` performs it the same way. A row whose table is not on the database being planned is listed `absent`, so one supplement serves both a control-plane database and a tenant database.
14+
- **It is refused, never half-read.** The command refuses a missing file, a file that is not JSON, a row the schema refuses, two rows for one object, or a row naming an object the platform inventory already gives a fate. The refusal comes before the database is opened, inside the `--json` envelope (`plan_refused` / `apply_refused`), and names the file, the row index and the object. A row naming one of the cloud-provided objects that the inventory lists with no fate (`cloudCarried`) replaces that placeholder row.
15+
- **Nothing is read without the flag:** no environment variable and no conventional path. A host table with no declared fate is still refused by name, and the refusal now names `--fate-supplement FILE` as the remedy.
16+
- The supplement's rows are part of the plan's `inventoryDigest`, so the apply's checkpoint is keyed on the supplement as well.
17+
18+
```bash
19+
os migrate organization-ownership --fate-supplement host-fates.json --out plan.json
20+
os migrate organization-ownership --fate-supplement host-fates.json --apply --backup-taken
21+
```

‎content/docs/getting-started/quick-reference.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ Plugin architecture, manifests, and kernel runtime.
8585
| **[Metadata Loader](/docs/references/kernel/metadata-loader)** | `metadata-loader.zod.ts` | MetadataLoaderContract | Metadata loading |
8686
| **[Package Registry](/docs/references/kernel/package-registry)** | `package-registry.zod.ts` | InstalledPackage, InstallPackageRequest | Package resolution |
8787

88-
## System Protocol (16 of 35 schemas)
88+
## System Protocol (16 of 36 schemas)
8989

9090
Runtime environment, logging, jobs, caching, and observability.
9191

‎content/docs/references/index.mdx‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Protocol reference — every schema by module
33
navTitle: Protocol Reference
4-
description: Every schema published by @objectstack/spec — 1524 schemas across 14 protocol modules
4+
description: Every schema published by @objectstack/spec — 1531 schemas across 14 protocol modules
55
---
66

77
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -32,9 +32,9 @@ counts are sums of the rows they head. Regenerate with
3232
| [Security Protocol](/docs/references/security) | 5 | 30 | Permission sets, row-level security, sharing rules, tenancy posture. |
3333
| [Shared Protocol](/docs/references/shared) | 10 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. |
3434
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
35-
| [System Protocol](/docs/references/system) | 35 | 277 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
35+
| [System Protocol](/docs/references/system) | 36 | 284 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
3636
| [UI Protocol](/docs/references/ui) | 16 | 167 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
37-
| **Total** | **196** | **1524** | 14 protocol modules |
37+
| **Total** | **197** | **1531** | 14 protocol modules |
3838

3939
---
4040

@@ -317,7 +317,7 @@ Studio designer metadata — the authoring surfaces for the protocols above.
317317

318318
## System Protocol
319319

320-
**Source:** `packages/spec/src/system/` · **Import:** `@objectstack/spec/system` · **35 pages, 277 schemas**
320+
**Source:** `packages/spec/src/system/` · **Import:** `@objectstack/spec/system` · **36 pages, 284 schemas**
321321

322322
The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance.
323323

@@ -337,6 +337,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a
337337
| [`email-template.zod.ts`](/docs/references/system/email-template) | `EmailTemplateDefinition`, `EmailTemplateDefinitionVariable` |
338338
| [`encryption.zod.ts`](/docs/references/system/encryption) | `EncryptionAlgorithm`, `EncryptionConfig`, `FieldEncryption`, `KeyManagementProvider`, `KeyRotationPolicy` |
339339
| [`environment-artifact.zod.ts`](/docs/references/system/environment-artifact) | `Sha256Digest` |
340+
| [`fate-supplement.zod.ts`](/docs/references/system/fate-supplement) | `OrganizationOwnershipAnchor`, `OrganizationOwnershipCategory`, `OrganizationOwnershipDeparture`, `OrganizationOwnershipFate`, `OrganizationOwnershipRowCondition`, `OrganizationOwnershipSupplement`, `OrganizationOwnershipSupplementEntry` |
340341
| [`http-server.zod.ts`](/docs/references/system/http-server) | `MiddlewareConfig`, `MiddlewareType`, `RouteHandlerMetadata` |
341342
| [`job.zod.ts`](/docs/references/system/job) | `CronSchedule`, `IntervalSchedule`, `Job`, `JobExecution`, `JobExecutionStatus`, `OnceSchedule`, `RetryPolicy`, `Schedule` |
342343
| [`license.zod.ts`](/docs/references/system/license) | `Feature`, `License`, `LicenseMetricType`, `Plan` |

0 commit comments

Comments
 (0)