Repository navigation
Commit dd29bbe
feat(metadata-core,metadata-protocol)!: the sys_metadata family declares no organization column; indexes re-keyed (ADR-0131 D6/D7, C7b S2) (#23469)
Fixes #23429 · Part of #15211
Clause-②: yes (narrowing)
## 维护者速读
**改了什么**:本 PR 合并了 C7b 的 S2 和 S5(裁决 6180024026:两者合为一个 PR,#23461 已关闭并入此处)。
-
**S2**:元数据家族的四张表(`sys_metadata`、`sys_metadata_audit`、`sys_metadata_commit`、`sys_metadata_history`)不再声明
`organization_id` 列(`systemFields.tenant: false`),索引去掉组织前缀后重建。
- 引擎读这四张表的路径统一走 `protocol.ts`
里的同一个接缝,读物理列把遗留的组织行排除在外。读不到这一列时直接拒绝,不会当作已迁移的库。
- 三张轨迹表(audit、history、commit)的数据门读取一律要求 `studio.access`(E2)。
- **S5**:`os serve` 启动时,若某张已无组织列声明的表在物理上仍带 `organization_id`,且仪式标记
`adr-0131-organization-ownership` 未验证,就拒绝启动,并点名 `os migrate
organization-ownership`。
- 没有任何开关可以绕过。
- 同时 `os migrate apply` 在仪式完成前拒绝删除清单内表上的 `organization_id` 列(E1)。
**为什么改**:ADR-0131 D6/D7 规定元数据属于环境、这些表不再带组织列;D10 第 5 项要求 v18
运行时在旧库跑完仪式前拒绝启动。
**风险与代价(含回滚)**:
- **破坏性变更(BREAKING)**:没跑过 v18 仪式的 17.x 库,用 `os serve` 启动会被拒绝,必须先执行 `os
migrate organization-ownership --out PLAN`,再执行 `--apply --plan-file PLAN
--backup-taken`。没有绕过开关。
- **收紧(Clause-②)**:`single` 姿态下,普通成员通过数据门读 audit、history、commit
三张轨迹表会被拒绝。Studio 作者和平台管理员不受影响。
- **收紧**:去掉组织列后唯一性变严;旧库里新唯一索引拒收的重复行会被点名,不会被删除。
- **回滚**:revert 本 PR;代码不改写任何数据。
- **体量**:变更约 6,800 行,超过 3,000 行,属 Tier H。审批这一项已由 os-zhuang 的治理批准
5520987282 满足(#20153)。
**席位意见**:建议合入(ACCEPT 6184588618)。
- opus 合并复审 PASS 6184549574,三份 FAIL 记录里的阻塞项全部满足;CI 49 项全绿(42 通过、7
项按设计跳过),与当前 `main` 无冲突。
- 收紧(`single` 下普通成员读轨迹表被拒)是席位裁定的(E2 → A),changeset 已写明。
- 启动拒绝是 ADR-0131 D10 的硬性要求;升级顺序有端到端测试证明不会互相卡死。
- 复审列出的非阻塞项都只在未迁移的旧库上才会出现,而旧库本 PR 自己就拒绝启动,所以转成后续卡片 #23609,不阻塞合入。
- 未实测:dogfood 升级顺序测试在 PR CI 上不跑,首次运行在合并队列或 `main` 的全量测试;席位会盯首轮结果,红了立即修。
**你要做的**:回复「可以进队列」即可。席位随后通过 relay 把本 PR 转为 ready 并开启自动合并,由合并队列在全绿后合入。
## What changed
### S2: the metadata family holds no organization column (ADR-0131
D6/D7)
- **The four declarations**
(`packages/metadata-core/src/objects/sys-metadata*.object.ts`): no
`organization_id` field; `systemFields: { tenant: false }`. Indexes
re-keyed:
- `idx_sys_metadata_overlay_active` → (`type`, `name`, `package_id`,
`bundle_member`);
- history: unique (`event_seq`) and unique (`type`, `name`, `version`);
its other indexes lose the organization prefix;
- audit and commit lose the organization prefix;
- `idx_sys_metadata_org_type` is dropped.
- **The runtime overlay index**
(`metadata-protocol/src/migrations/overlay-index.ts`):
`OVERLAY_INDEX_COLUMNS` and the duplicate probe take the new key.
- **No engine write names `organization_id` on the four tables**,
because the engine refuses an undeclared field (`INVALID_FIELD`). The
`organizationId` option on `SysMetadataRepositoryOptions` is removed.
`DatabaseLoader` and `history-cleanup.ts` accept `organizationId` and
ignore it, as they do `environmentId`.
- **Legacy organization rows on an unmigrated (v17-shape) database are
left out of every family read, per the dev's report 6184164699.**
- `protocol.ts` reads the family through one seam, `findEnvironmentRows`
/ `findEnvironmentRow`. Each runs the engine read, then leaves out the
rows that `readLegacyOrganizationRows` finds on the physical column,
joining the write's transaction.
- The by-name reads (`getMetaItem`, `getMetaItemLayered` via
`findServedOverlayRow`), the item lock (`overlayLockLayerAt`) and the
package-less address (`packagelessAddress`, including #23489's
`packagelessReadAddress`) go through that seam. So do the list, draft,
package, commit and history reads.
- Two reads keep legacy rows on purpose and read the column themselves.
`migrateStoredMetadata` reports such a row. `revertCommit` refuses a
legacy commit by name.
- The same filter covers `metadata`'s `DatabaseLoader`,
`plugin-security`'s security-catalog echo and `service-automation`'s
flow-credential migration.
- A column read that fails throws `LegacyOrganizationRowsUnreadable`
(code `LEGACY_ORGANIZATION_ROWS_UNREADABLE`). It is never taken for a
migrated database.
- `SqlDriver.listPhysicalColumns` is added, so the seam can ask whether
the column exists. `isLegacyOrganizationRow` is removed.
- **E2: `studio.access` gates `sys_metadata_audit`, `_history` and
`_commit` in every posture** (seat answers 6171152049, 6179490127).
Under `single`, a plain member's data-door read of these three tables
was a bypass of what the `/meta` trail doors refuse. That bypass is
closed. The `/meta` trail doors admitting `setup.access` is a separate
card, #23546.
- **The form doors' organization read is deleted:** `getMetaItems`'
`legacyFormOrganizationId` and its callers in the REST form doors (seat
2's pointer 6165850696).
- **The ceremony's family readers in `packages/cli`.**
- The template lookup names the column only while it exists.
- The overlay-duplicates category keys the bundle member.
- `security-catalog-overlays` reads the physical column through the
raw-SQL seam (`legacyOrganizationRowIds`), so an unmigrated database's
organization control row is never listed or deleted.
- **Four ADR-0087 column-retired semantic entries**
(`18.sys-metadata{,-audit,-commit,-history}-organization-column-retired`),
and the census (`scripts/platform-object-tenancy-census.json`): the four
objects move out of reach.
### S5: the v18 boot refusal (ADR-0131 D10 item 5; ruling 6163114098, Q3
→ A)
- **`OrganizationOwnershipBootGuardPlugin`**
(`packages/runtime/src/organization-ownership-boot-guard.ts`). At
`kernel:ready` it asks whether a table whose object declares no
organization column still carries a physical `organization_id`.
- If one does, and the ceremony's `sys_migration` marker
`adr-0131-organization-ownership` is not verified, it throws
`OrganizationOwnershipBootRefusal` (code
`ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED`). The refusal names the
tables and the command, and stops `bootstrap()` before
`kernel:listening`.
- A fresh database boots. A database whose ceremony verified boots.
- A failed column read is refused.
- A driver with no physical columns (memory, MongoDB) is not checked.
- It reads `SqlDriver.dialectName`, so the default `SqliteWasmDriver` is
covered.
- ⛔ There is no escape hatch: no variable and no flag skips the check
(D10 verbatim).
- **Composed by `os serve` only**
(`packages/cli/src/commands/serve.ts`), beside
`MigrationRecoveryPlugin`. It is never in `createStandaloneStack` or
`data-migration-plugins.ts`. Those are the CLI data and schema boots,
the ceremony's own `bootSchemaStack` among them, and a guard there would
refuse the one command that clears it.
- **E1: `os migrate apply` refuses `drop_column organization_id`** on
any table in the ceremony's inventory
(`ORGANIZATION_OWNERSHIP_INVENTORY`) while the marker is not verified.
- It names `os migrate organization-ownership --out PLAN`, then `--apply
--plan-file PLAN`, and exits 1. Under `--json` it answers
`refused_organization_ownership_unmigrated`.
- The refusal holds the whole `--allow-destructive` run, as #17440 and
#23281 do: nothing in it is created, applied or dropped. A run without
`--allow-destructive` is unchanged.
- The ceremony drops the columns itself, in its column-drops step,
before it records the run verified. That step calls
`stack.applyMigrationEntries` directly and is not gated.
- **`ORGANIZATION_OWNERSHIP_MIGRATION_ID`** moves to
`@objectstack/spec/system` (`migration.zod.ts`), so the runtime and the
ceremony read one constant.
### Both
- **Two new error codes** in `ERROR_CODE_LEDGER`:
`ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED` (`@objectstack/runtime`) and
`LEGACY_ORGANIZATION_ROWS_UNREADABLE`. The published `ApiError.code` /
`ErrorCode` enum widens by both, and the changeset names it.
- **The spec reference docs are regenerated once over both codes**
(`gen:schema && gen:docs`). `contract.mdx` reads `… +333 more` on `main`
at `fa6a6085c0`.
- **Two changesets travel**, both BREAKING under the launch-window
`minor`.
- S2's is `registered`, with the four ADR-0087 ids.
- S5's is `already-registered`.
- Every package in the fixed group publishes at `minor`.
- `service-automation` is bumped.
## Landing
- **ONE PR** (ruling 6180024026). S2 and S5 each register an error code
and regenerate the same `contract.mdx` line, so two queued PRs would
eject each other. S5 (`ba6d9c5de8`) is merged in as `ad23169210`, and
#23461 is closed unmerged as superseded.
- **Landing gate met:** #15211's S1 (#23452) is on `main` as
`869a381dc9`, so the ceremony promotes before it drops (6163669810).
- **Tier H by size** (over 3,000 changed lines). The approval leg is met
by os-zhuang's governed approval 5520987282, which counts on any commit
under #20153. The seat lands it through the merge queue after ACCEPT,
and asks the maintainer first.
## Measured choices
- **The history renumbering (Q2 → A) is not here.** S1 carries it, and
S1 is on `main`. The build-ahead's copy (`5326201dff`) is reverted here
(`4ba2ea3c`), not rewritten.
- **S1's ceremony hunk copies are dropped.** Each shared hunk takes
`main`'s S1 version, except the form-door organization read, which S2
deletes.
- **Why the boot guard is its own plugin:** `MigrationRecoveryPlugin` is
also composed by the CLI data-migration boot that the ceremony runs on,
so a refusal inside it would refuse the ceremony itself.
- **The upgrade order cannot deadlock.** The refusal fires at
`kernel:ready`, after every plugin's `start()` and its additive schema
sync, so S3's `sys_user_permission_set.permission_set` already exists
when the ceremony runs. The dogfood pin below proves the order end to
end.
## Pins
- **S2's unmigrated-database pins**
(`packages/cli/test/meta-stored-legacy-organization.integration.test.ts`):
a real engine on a v17-shape SQLite database, with the column added raw,
run on the ceremony's own `--apply` boot.
- `door.save` of an item an organization holds bound to a package writes
an environment row, and leaves the organization row untouched.
- An organization row carrying `_lock: full` is not the item's lock.
- The `kernel:ready` security-catalog echo resolves the environment's
permission overlay, not the organization's.
- The dev reports one ablation: `overlayLockLayerAt` put back on the raw
read turns the lock pin red.
- **E2:** the 14-case `studio.access` pin on the real SecurityPlugin
middleware.
- **S5's guard:** `organization-ownership-boot-guard.test.ts` (unit,
real SQLite) and `organization-ownership-boot-guard.kernel.test.ts`,
which runs on a real `ObjectKernel`. They cover:
- a v17-shape database refused;
- an unverified marker refused;
- a verified marker, a migrated database and a fresh database booting;
- a failed read refused;
- no environment variable standing the refusal down.
- **E1:** `apply-organization-ownership-gate.integration.test.ts`, real
CLI, 5 cases:
- refused with no marker, or with an unverified one;
- an unrelated destructive drop on the same table proceeds;
- a gated drop beside an unrelated drop in one run refuses both;
- proceeds once the marker is verified.
- **The upgrade order, end to end**
(`organization-ownership-boot-refusal-order.dogfood.test.ts`):
- a v17-shape file database is refused, naming the command;
- the refused boot has already added
`sys_user_permission_set.permission_set`;
- `os migrate organization-ownership --out PLAN`, then `--apply
--plan-file PLAN --backup-taken`, verifies the marker with no boot in
between;
- the next boot succeeds.
Dogfood runs on `main`'s full battery, not on PR CI.
## Acceptance notes
- **Left for their own change (the dev's report 6184164699):**
`rowsRead`, `ServedOverlayRowCandidate.scope` / `organizationId` and
`orgId` / `itemOrgId` in `protocol.ts`. Every row that reaches them is
now the environment's, so they are inert. Removing them changes the list
merge's signatures across the file, a behaviour-neutral refactor.
- **Turso remote on an unmigrated database: NOT MEASURED.** The remote
arm of `listPhysicalColumns` was read, not run.
- **Out of scope, carded:**
- #23546: the `/meta` trail doors admit `setup.access`;
- #23558: the ceremony's `dialectOf` / `resolvePlannedDriverExec` read
`config.client` as a string only.
## Verification
**Latest round, on head `e4811b67f2`** (the dev's report 6184164699, all
in the foreground):
| Package | Files | Tests |
| --- | --- | --- |
| `metadata-core` | 18 | 715 |
| `metadata-protocol` | 235 (+3 skipped) | 20,533 |
| `objectql` | 422 | 8,117 |
| `rest` | 289 | 6,056 |
| `runtime` | 386 | 6,449 |
| `plugin-security` | 186 | 3,704 |
| `service-automation` | 186 | 2,486 |
| `service-datasource` | 45 | 838 |
| `driver-sql` | 219 | 3,642 |
| `metadata` | 62 | 888 |
| `cli` unit | 290 | 4,324 |
| `cli` integration (the full project) | 133 | 1,218 passed, 2 skipped |
| dogfood (every file naming `sys_metadata` or the ceremony) | 29 | 222
|
- Typecheck exit 0 in `metadata-protocol`, `metadata`,
`service-automation`, `plugin-security`, `driver-turso` and `cli`.
- `check:docs` and `spec check:generated` green; `check-i18n-bundles`
OK.
- `node scripts/pm/dispatch-gates.mjs --commands` derived 159 families,
and all 159 ran; 154 exited 0. `check:test-source-alias` was red on the
new pin's in-body import and was fixed by `e4811b67f2`. Four are NOT
MEASURED, because they refused on a local prerequisite; CI runs them:
- `check-engine-split-ratio` and `check-plugin-teardown-shape
--self-test`: the shallow clone;
- `check:skill-examples` and `check:dual-build-cjs-loads`: unbuilt
packages.
**Earlier rounds:** os-dev-reports 6168648434, 6170427033, 6179430370,
6181023527 and 6182409899 on #23429.
**Contract reviews:**
- S2: FAIL 6171128604 and FAIL 6181366782.
- S5: PASS 6170906686 and FAIL 6180007547.
- **The combined head at `e4811b67f2`: PASS 6184549574.** Its
non-blocking items are carded as #23609.
- Seat ACCEPT: 6184588618 on #23429.
_Body rewritten for the one-PR form by `domain:engine` seat 1
(`session_01Rerax7QTjKMPCUZxQUtPFR`) from the dev's notes and #23461's
body; the 维护者速读 was finalized after the PASS at 2026-10-15T04:43Z._
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 361c946 commit dd29bbe
143 files changed
Lines changed: 3823 additions & 3019 deletions
File tree
- .changeset
- content/docs
- concepts
- deployment
- kernel/contracts
- references/api
- packages
- cli
- src
- commands
- migrate
- utils
- test
- drivers
- driver-sql/src
- driver-turso/src
- metadata-core/src
- objects
- metadata-protocol/src
- migrations
- metadata/src
- loaders
- utils
- objectql
- src
- platform-objects/src/apps/translations
- plugins/plugin-security/src
- qa/dogfood
- test
- rest/src
- runtime/src
- services
- service-automation/src
- service-datasource/src
- __tests__
- spec
- api-surface
- export-origins
- src
- api
- migrations/entries/semantic
- system
- scripts
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| 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 | + | |
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
106 | 106 | | |
107 | 107 | | |
108 | 108 | | |
109 | | - | |
| 109 | + | |
110 | 110 | | |
111 | 111 | | |
112 | 112 | | |
| |||
234 | 234 | | |
235 | 235 | | |
236 | 236 | | |
237 | | - | |
| 237 | + | |
238 | 238 | | |
239 | 239 | | |
240 | 240 | | |
| |||
0 commit comments