Repository navigation
feat(metadata-core,metadata-protocol)!: the sys_metadata family declares no organization column; indexes re-keyed (ADR-0131 D6/D7, C7b S2) - #23469
Conversation
…eclares no organization column; indexes re-keyed (ADR-0131 D6/D7, C7b S2) WIP: the engine reads and writes that still name organization_id on these four tables are not yet changed. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USccGXR13sWyTeEwe8QFdH
…ization_id on the sys_metadata family (C7b S2) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USccGXR13sWyTeEwe8QFdH
…organization read, byte-identical (C7b S2) The undeclared organization column makes that read inexpressible, so S2 carries the same hunks S1 lands first; the overlap merges clean. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USccGXR13sWyTeEwe8QFdH
…y; legacy uninstall path and legacy-row report deleted (C7b S2) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USccGXR13sWyTeEwe8QFdH
…s stop naming organization_id (C7b S2) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USccGXR13sWyTeEwe8QFdH
…shape database until the ownership ceremony is verified (ADR-0131 D10 item 5, C7b S5) WIP: tests not yet run. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USccGXR13sWyTeEwe8QFdH
… unique environment-wide before column-drops (seat answer Q2 A, C7b S2) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USccGXR13sWyTeEwe8QFdH
…on claude/issue-15211-c7b-promotions) The card (#23429) places the Q2 A renumbering in #15211 S1, and S1's branch carries it. S2 holds only the family's schema and its readers. This reverts commit 5326201. Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…tadata-family # Conflicts: # packages/metadata-protocol/src/sys-metadata-repository.ts
…stop naming organization_id; the last protocol filter goes (C7b S2) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…ation-scoped stored rows (C7b S2) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…op seeding legacy organization rows (C7b S2) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…e environment's rows only (C7b S2) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…ow the organization-free family; the legacy-row boot report's pin goes with it (C7b S2) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…ng legacy organization rows (C7b S2) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…ry's retired organizationId option; flow, audit and store pins follow (C7b S2) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…pins drop their organization legs (C7b S2) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…s left unused (C7b S2) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…tion scope (C7b S2) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…on-free family (C7b S2) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…ly's changeset (C7b S2) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
… the form door reads one view layer (C7b S2) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…rified marker and a fresh database boot (ADR-0131 D10 item 5, C7b S5) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…5, C7b S5) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…-free family and the boot refusal (C7b S2) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…e docs for the boot refusal; changeset wording (C7b S5) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…hape (C7b S5) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…anization rows (C7b S2) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…ude/issue-23429-metadata-family No textual conflict on this merge. The generated reference docs are regenerated over the merged ledger in the next commit, never picked from a side. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ
) `gen:schema && gen:docs` on the merged tree: `… +333 more` (main's four API_KEY_* codes from #23541, plus ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED and LEGACY_ORGANIZATION_ROWS_UNREADABLE). check:docs green; the i18n bundles are in sync (check-i18n-bundles OK, nothing to regenerate). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ
…of every family read (#23429) S2 re-review 6181366782, blocking fix 2. On a v17-shape database (which `os serve` refuses but the ceremony's own boot reads), these still served, locked, addressed or echoed a legacy organization's row as the environment's: - protocol: every sys_metadata / _audit / _commit / _history read now goes through findEnvironmentRows / findEnvironmentRow (the physical column via readLegacyOrganizationRows, joining context.transaction). That covers findServedOverlayRow (getMetaItem, getMetaItemLayered), overlayLockLayerAt (the item lock, lockWriteRefusal, assertLockAllowsDelete, authoredLockFamily), packagelessAddress (save, delete, readVersionToken, packagelessReadAddress) and every other find the record lists. Two keep legacy rows on purpose and read the column themselves: migrateStoredMetadata and revertCommit's commit read. Stale `organization_id null` comments fixed. - plugin-security security-catalog-echo and service-automation flow-credential-migration: the dead `row.organization_id` discriminators read the physical column. - metadata DatabaseLoader: its family reads leave legacy rows out, engine- or driver-backed. - Real-engine pins (cli, v17-shape SQLite, column added raw): a package-bound organization row never becomes the package-less address; an organization row carrying `_lock: full` is neither the item's lock (lock.verdict) nor the gate's refusal; the kernel:ready echo registers the environment's permission overlay and never the organization's. Ablation: overlayLockLayerAt back on the raw read turns the lock pin red; restored. - Non-blocking: es-ES/ja-JP/zh-CN environment_id help re-translated; TursoDriver's introspectSchema JSDoc re-attached and the remote catalog read's errors go through rawStatementFault; the changeset names the ApiError.code widening and service-automation; the repository's history reads and the tenant-chokepoint gate's not-covered note are documented. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ
…odule load (check:test-source-alias) (#23429) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ
Contract reviewServed-tier: Scope. This is one re-review of the combined S2 + S5 head (ruling 6180024026). It covers:
What was read.
Gate verdicts: 49 check-runs, re-read at 04:36Z after the last one finished. 42 succeeded, 7 were skipped, none failed and none is pending.
The landing tree.
① Derived judgments1. B1: met.
2. B2: met. No reader whose The seam (
How the record's five readers are covered:
Reads outside the seam:
The other packages the brief names.
The repository (
The two deliberate exceptions: right.
The single-row form's re-read past a legacy hit: correct.
The plural form filters after
The transaction join is explicit only.
Fails closed: yes.
3. B2 pins (
4. S5 on the combined head: holds.
5. The earlier S2 items from 6171128604 still hold.
6. The dogfood census: no change.
7. Public-surface and accept-set changes, each judged against the changesets:
8. Size and tier.
② Semver levelThe levels.
BREAKING, markers and placement.
Clause-②: yes (narrowing) ③ Boundary flags6167186090.
6168648434.
6170427033 and 6172828957. The body notes are the seat's; nothing is open. 6179430370.
6181023527. The per-hunk claims check out. "No overlapping hunk" in 6182409899.
6184164699.
Seat answers.
NOT MEASURED:
Implemented-by: VERDICT: PASS Non-blocking:
Generated by Claude Code |
Fixes #23429 · Part of #15211
Clause-②: yes (narrowing)
维护者速读
改了什么:本 PR 合并了 C7b 的 S2 和 S5(裁决 6180024026:两者合为一个 PR,#23461 已关闭并入此处)。
sys_metadata、sys_metadata_audit、sys_metadata_commit、sys_metadata_history)不再声明organization_id列(systemFields.tenant: false),索引去掉组织前缀后重建。protocol.ts里的同一个接缝,读物理列把遗留的组织行排除在外。读不到这一列时直接拒绝,不会当作已迁移的库。studio.access(E2)。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 运行时在旧库跑完仪式前拒绝启动。
风险与代价(含回滚):
os serve启动会被拒绝,必须先执行os migrate organization-ownership --out PLAN,再执行--apply --plan-file PLAN --backup-taken。没有绕过开关。single姿态下,普通成员通过数据门读 audit、history、commit 三张轨迹表会被拒绝。Studio 作者和平台管理员不受影响。席位意见:建议合入(ACCEPT 6184588618)。
main无冲突。single下普通成员读轨迹表被拒)是席位裁定的(E2 → A),changeset 已写明。main的全量测试;席位会盯首轮结果,红了立即修。你要做的:回复「可以进队列」即可。席位随后通过 relay 把本 PR 转为 ready 并开启自动合并,由合并队列在全绿后合入。
What changed
S2: the metadata family holds no organization column (ADR-0131 D6/D7)
packages/metadata-core/src/objects/sys-metadata*.object.ts): noorganization_idfield;systemFields: { tenant: false }. Indexes re-keyed:idx_sys_metadata_overlay_active→ (type,name,package_id,bundle_member);event_seq) and unique (type,name,version); its other indexes lose the organization prefix;idx_sys_metadata_org_typeis dropped.metadata-protocol/src/migrations/overlay-index.ts):OVERLAY_INDEX_COLUMNSand the duplicate probe take the new key.organization_idon the four tables, because the engine refuses an undeclared field (INVALID_FIELD). TheorganizationIdoption onSysMetadataRepositoryOptionsis removed.DatabaseLoaderandhistory-cleanup.tsacceptorganizationIdand ignore it, as they doenvironmentId.protocol.tsreads the family through one seam,findEnvironmentRows/findEnvironmentRow. Each runs the engine read, then leaves out the rows thatreadLegacyOrganizationRowsfinds on the physical column, joining the write's transaction.getMetaItem,getMetaItemLayeredviafindServedOverlayRow), the item lock (overlayLockLayerAt) and the package-less address (packagelessAddress, including fix(metadata-protocol): the read naming no package serves the row the package-less save writes, beside a package-unbound row (S6) #23489'spackagelessReadAddress) go through that seam. So do the list, draft, package, commit and history reads.migrateStoredMetadatareports such a row.revertCommitrefuses a legacy commit by name.metadata'sDatabaseLoader,plugin-security's security-catalog echo andservice-automation's flow-credential migration.LegacyOrganizationRowsUnreadable(codeLEGACY_ORGANIZATION_ROWS_UNREADABLE). It is never taken for a migrated database.SqlDriver.listPhysicalColumnsis added, so the seam can ask whether the column exists.isLegacyOrganizationRowis removed.studio.accessgatessys_metadata_audit,_historyand_commitin every posture (seat answers 6171152049, 6179490127). Undersingle, a plain member's data-door read of these three tables was a bypass of what the/metatrail doors refuse. That bypass is closed. The/metatrail doors admittingsetup.accessis a separate card, rest/runtime: under a wall, the /meta history, audit and package-commits doors admit setup.access, which organization admins hold, so an organization admin reads the environment-wide authoring trail of any item #23546.getMetaItems'legacyFormOrganizationIdand its callers in the REST form doors (seat 2's pointer 6165850696).packages/cli.security-catalog-overlaysreads the physical column through the raw-SQL seam (legacyOrganizationRowIds), so an unmigrated database's organization control row is never listed or deleted.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). Atkernel:readyit asks whether a table whose object declares no organization column still carries a physicalorganization_id.sys_migrationmarkeradr-0131-organization-ownershipis not verified, it throwsOrganizationOwnershipBootRefusal(codeORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED). The refusal names the tables and the command, and stopsbootstrap()beforekernel:listening.SqlDriver.dialectName, so the defaultSqliteWasmDriveris covered.os serveonly (packages/cli/src/commands/serve.ts), besideMigrationRecoveryPlugin. It is never increateStandaloneStackordata-migration-plugins.ts. Those are the CLI data and schema boots, the ceremony's ownbootSchemaStackamong them, and a guard there would refuse the one command that clears it.os migrate applyrefusesdrop_column organization_idon any table in the ceremony's inventory (ORGANIZATION_OWNERSHIP_INVENTORY) while the marker is not verified.os migrate organization-ownership --out PLAN, then--apply --plan-file PLAN, and exits 1. Under--jsonit answersrefused_organization_ownership_unmigrated.--allow-destructiverun, as Adopt better-auth's account-issuer rollback: drop sys_account.issuer, retire the backfill, raise the family to 1.7.3 #17440 and v18 upgrade drops stored comment reactions (9963322fe8 does not migrate sys_comment.reactions) — under the maintainer's rule an upgrade keeps data #23281 do: nothing in it is created, applied or dropped. A run without--allow-destructiveis unchanged.stack.applyMigrationEntriesdirectly and is not gated.ORGANIZATION_OWNERSHIP_MIGRATION_IDmoves to@objectstack/spec/system(migration.zod.ts), so the runtime and the ceremony read one constant.Both
ERROR_CODE_LEDGER:ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED(@objectstack/runtime) andLEGACY_ORGANIZATION_ROWS_UNREADABLE. The publishedApiError.code/ErrorCodeenum widens by both, and the changeset names it.gen:schema && gen:docs).contract.mdxreads… +333 moreonmainatfa6a6085c0.minor.registered, with the four ADR-0087 ids.already-registered.minor.service-automationis bumped.Landing
contract.mdxline, so two queued PRs would eject each other. S5 (ba6d9c5de8) is merged in asad23169210, and feat(runtime,cli)!: os serve refuses a v17-shape database until the organization-ownership ceremony has verified it (ADR-0131 D10 item 5, C7b S5) #23461 is closed unmerged as superseded.mainas869a381dc9, so the ceremony promotes before it drops (6163669810).Measured choices
main. The build-ahead's copy (5326201dff) is reverted here (4ba2ea3c), not rewritten.main's S1 version, except the form-door organization read, which S2 deletes.MigrationRecoveryPluginis also composed by the CLI data-migration boot that the ceremony runs on, so a refusal inside it would refuse the ceremony itself.kernel:ready, after every plugin'sstart()and its additive schema sync, so S3'ssys_user_permission_set.permission_setalready 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--applyboot.door.saveof an item an organization holds bound to a package writes an environment row, and leaves the organization row untouched._lock: fullis not the item's lock.kernel:readysecurity-catalog echo resolves the environment's permission overlay, not the organization's.overlayLockLayerAtput back on the raw read turns the lock pin red.E2: the 14-case
studio.accesspin on the real SecurityPlugin middleware.S5's guard:
organization-ownership-boot-guard.test.ts(unit, real SQLite) andorganization-ownership-boot-guard.kernel.test.ts, which runs on a realObjectKernel. They cover:E1:
apply-organization-ownership-gate.integration.test.ts, real CLI, 5 cases:The upgrade order, end to end (
organization-ownership-boot-refusal-order.dogfood.test.ts):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;Dogfood runs on
main's full battery, not on PR CI.Acceptance notes
rowsRead,ServedOverlayRowCandidate.scope/organizationIdandorgId/itemOrgIdinprotocol.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.listPhysicalColumnswas read, not run./metatrail doors admitsetup.access;dialectOf/resolvePlannedDriverExecreadconfig.clientas a string only.Verification
Latest round, on head
e4811b67f2(the dev's report 6184164699, all in the foreground):metadata-coremetadata-protocolobjectqlrestruntimeplugin-securityservice-automationservice-datasourcedriver-sqlmetadatacliunitcliintegration (the full project)sys_metadataor the ceremony)metadata-protocol,metadata,service-automation,plugin-security,driver-tursoandcli.check:docsandspec check:generatedgreen;check-i18n-bundlesOK.node scripts/pm/dispatch-gates.mjs --commandsderived 159 families, and all 159 ran; 154 exited 0.check:test-source-aliaswas red on the new pin's in-body import and was fixed bye4811b67f2. Four are NOT MEASURED, because they refused on a local prerequisite; CI runs them:check-engine-split-ratioandcheck-plugin-teardown-shape --self-test: the shallow clone;check:skill-examplesandcheck:dual-build-cjs-loads: unbuilt packages.Earlier rounds: os-dev-reports 6168648434, 6170427033, 6179430370, 6181023527 and 6182409899 on #23429.
Contract reviews:
e4811b67f2: PASS 6184549574. Its non-blocking items are carded as metadata: after #23469, the legacy-organization read seam's loose ends (ambient transaction, filter-after-limit, an unpinned by-name filter, DatabaseLoader._count, a 404 where a named refusal belongs) and the ceremony wording #23609.Body rewritten for the one-PR form by
domain:engineseat 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.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR