Skip to content

Commit 8684079

Browse files
fix(metadata-protocol,metadata): the legacy-organization read seam's loose ends: paged reads, the loader's count, the rollback's named refusal (ADR-0131 D6/D7) (#23778)
Fixes #23609 Clause-②: no Six of the card's seven items are closed here, one commit each, measured on `main` first. Item 2 is measured but stopped: its fix needs files outside the claimed surface (below). Every path matters only on an unmigrated (v17-shape) database, which `os serve` refuses to boot until `os migrate organization-ownership` has verified. Each fix acts only where legacy organization rows are present, and each pin runs beside a migrated CONTROL whose answer does not change. The items come from the PASS record on PR #23469 (#23429, C7b S2 + S5); neither is closed by this PR. ## Items 1. **Done here** (`0546039d`). `findServedOverlayRow`'s legacy filter gets its own pin: the `_lock` case in `meta-stored-legacy-organization.integration.test.ts` now asserts that the served `item` is not the organization's body. - H1 held on `main` `2906cd8`. With only that filter removed (the raw `findOne`, rebuilt, marker verified in `dist/`), the file stayed 7/7 green. - With the pin, the same ablation turns only the new assertion red (`expected 'Org A locked' not to be 'Org A locked'`, 1 failed / 6 passed). 2. **Measured, NOT done: stopped at the claim's file surface.** H2 holds. - On a v17-shape SQLite file, `protocol.publishPackageDrafts` for a package with one draft had not answered after a **25 s** bound. - CONTROL: the same file with the column dropped published in **97 ms** (`outcome: published`). - ObjectQL exposes no ambient-transaction accessor to a raw read. `txStore` and `privilegedReadDriverOptions` are private, and `engine.execute` does not thread the ambient handle. - So the narrow route (take the ambient transaction inside `legacyOrganizationRowsOf`) needs a public ObjectQL accessor in `packages/objectql`, outside the claim. - Threading `trxCtx` instead goes through `promoteDraftForPublish`, its lock read and the repository's `get` / `promoteDraft` draft reads. `repo.get` alone keeps the hang, because the repository's own legacy read has the same gap. Those are outside the claimed surface, and next to the promote door's D9.9 call. - The route and its trade-offs are in the report's `open_questions`. - **Seat ruling 6206299338: C.** Left latent. The seam is transitional code for a database `os serve` refuses, and no caller reaches the path. A future reader of the family inside an ambient transaction off the served boot carries the fix (route A) in its own card. So this PR closes the card. _(Body edit by the `domain:engine` seat, 2026-10-16T01:58Z.)_ 3. **Done here** (`a6f23cd9`). A paged family read leaves the legacy rows out before the page is cut. This covers `auditMetaItem`, `listCommits`, `revertStoredPackage`'s `limit: 1` probe (it answered 409 "never been published") and the repository's `list({ limit })`. - When the page holds a legacy row, or an `offset` could have counted one, the read is repeated with `id $nin` of them in the query. Otherwise the single read is the answer, unchanged. - Ablations: (a) send every paged read down the old post-filter path, and the 4 v17-shape protocol cases go red (audit, commits, revert, offset); (b) the repository's `list`, and its 1 v17-shape case goes red. The control cases stay green in both. 4. **Done here** (`7a52179d`). `DatabaseLoader._count` counts what `_find` serves. Before, `exists()` answered true where `load()` answered null, and `queryHistory`'s `total` counted legacy rows. - It counts first. Only a non-zero count over a table holding legacy rows is re-counted with them excluded, so the unprovisioned-table discrimination in `exists()` is untouched. - Ablation: both v17-shape cases red, controls green. 5. **Done here** (`c39c8c8e`). `rollbackToPackageCommit` answers a legacy commit with `revertCommit`'s named refusal (`NOT_OVERRIDABLE`, 403, the same sentence) instead of `COMMIT_NOT_FOUND` (404). - Both doors read the commit through one helper, `readRevertableCommit`. `revertCommit`'s behaviour is unchanged: the same read, refusal and text, moved. - Ablation: put the rollback's read back through `findEnvironmentRow` with its old 404, and only the v17-shape "one answer from both doors" case goes red. 6. **Done here** (`8052436a`). The inert plumbing is removed: `mergePackageAwareOverlay`'s `rowsRead`, `ServedOverlayRowCandidate.scope` / `organizationId`, `StoredRowPlace.organizationId`, and the always-null `orgId` / `itemOrgId` locals. - Correction to the card: `rowsRead` was a presence flag, not dead. It is dropped because every caller that passed it also sends each record's `stored` place, and every caller that omitted it sends none. `servedStoredRow` returns `undefined` over records with no place, so the merge's answer is identical. - Proof: the full `@objectstack/metadata-protocol` suite gives **237 files passed / 3 skipped, 20593 tests passed / 19 skipped**, both with this commit and with `protocol.ts` restored to its parent (blob `ea9fa58d`). - Left in place: `StoredOverlayEntry.organizationId` and the `organizationId` parameters of `restoreArtifactRegistryView` / `hydrateOverlayIntoRegistry`. They are the #6780 registry-scope API and outside the card's list. 7. **Done here** (`6bdaa0e0`; changeset `042ba4c5`). Prose only. - The four `18.sys-metadata{,-audit,-commit,-history}-organization-column-retired` entries and `metadata-lifecycle.mdx` name the ceremony's reviewed apply: `--out PLAN`, then `--apply --plan-file PLAN`. - `os migrate apply`'s E1 sentence now reads "a drop the organization-ownership ceremony owns, and the ceremony has not verified on this database". Its `--json` payload is unchanged. - No entry id, category, version or engine-read field changes. `pnpm --filter @objectstack/spec check:generated` reports all 14 artifacts up to date, so nothing checked in derives from the entries' prose. ## Verification (head `81aa285d`) - `@objectstack/metadata-protocol`: full suite 237 files passed / 3 skipped, 20593 tests passed / 19 skipped; `typecheck` exit 0. The new file `protocol.legacy-organization-read-seam.test.ts` passes 16/16. - `@objectstack/metadata`: full suite 63 files, 892/892 passed; `typecheck` exit 0. The new file `database-loader.legacy-organization-count.test.ts` passes 4/4. - `@objectstack/cli`: - `--project unit`: 297 files, 4377/4377 passed; - `--project integration`: `meta-stored-legacy-organization` 7/7, and `apply-organization-ownership-gate` + `organization-ownership-platform-global-column-drop` 11/11 (on a rebuilt `dist/`); - `typecheck` exit 0. - `@objectstack/spec`: `build` + `check:generated` exit 0 (14/14 up to date); `typecheck` exit 0. - Derived gates (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, no paths, after the last commit): 123 derived; `--ran` gives **119 run, 4 NOT MEASURED, 0 unrun** (exit 0). - The first run went over the tree at `042ba4c5`. It found one real failure, `check:engine-double-contract`: the loader pin's fake `findOne` skipped `assertEngineFindOnePredicate`, and the ledger lacked the new doubles. `81aa285d` fixes both. - At `81aa285d`, every family reading the two files that commit changed was re-run, plus the 8 the new `scripts/` path adds: 13/13 exit 0. - NOT MEASURED (each exited 3, PREREQUISITE NOT MET, on packages this diff does not touch; CI runs them): `check-plugin-teardown-shape --self-test` (needs unshallow history), `spec check:skill-examples` (`client-react` unbuilt), `check:dual-build-cjs-loads` (whole-workspace build) and `check:i18n-coverage` (`connector-slack` unbuilt). - Every ablation ran through `scripts/ablation-replace.mjs`: anchor hit, blob moved, restore proved blob == HEAD and `git diff HEAD` empty. The CLI ablation (item 1) also passed `ablation-dist-preflight` on both legs. ## Acceptance notes - `DatabaseLoader.queryHistory`'s page read (`_find` with `limit` + `offset`) still filters after the cut, the item 3 shape on the loader. It is not in the claimed surface (`_count` only), and it is latent like every item here. Carrier: none. - `listCommits` passes `limit` with no `orderBy` and sorts afterwards, so its `limit` picks store-order rows, not the newest. This predates the card and is unrelated to the legacy filter. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01TekAoE8QbLrxFVJsdNuRgg --- _Generated by [Claude Code](https://claude.ai/code/session_01TekAoE8QbLrxFVJsdNuRgg)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d3df3ba commit 8684079

14 files changed

Lines changed: 564 additions & 126 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
'@objectstack/metadata': patch
4+
'@objectstack/cli': patch
5+
'@objectstack/spec': patch
6+
---
7+
8+
fix(metadata-protocol,metadata): on a database the organization-ownership ceremony has not migrated, the paged family reads, `DatabaseLoader`'s count and the package rollback agree with the environment-only reads; the ceremony's apply is named with `--plan-file` (ADR-0131 D6/D7)
9+
10+
Clause-②: no
11+
12+
Every change below matters only where legacy organization rows still sit in the `sys_metadata` family, the database `os serve` refuses to start on until `os migrate organization-ownership` has verified. On a migrated or fresh database every answer is unchanged.
13+
14+
- **`@objectstack/metadata-protocol`: a paged read leaves the legacy rows out before the page is cut.** `auditMetaItem` (`limit`), `listCommits` (`limit`), `revertStoredPackage`'s first-active-row probe and `SysMetadataRepository.list({ limit })` used to filter a legacy organization's rows out after the limit. A page they sat in came back short, and `revertStoredPackage` answered `RESOURCE_CONFLICT` (409) "has never been published" for a package the environment had published when its first active row was an organization's. When a page holds a legacy row, the read is now repeated with those rows excluded in the query.
15+
- **`@objectstack/metadata-protocol`: `rollbackToPackageCommit` refuses a legacy organization's commit by name.** It answered `COMMIT_NOT_FOUND` (404) for a commit that is there. It now gives `revertCommit`'s refusal: `NOT_OVERRIDABLE` (403), the same sentence, naming the commit and its organization.
16+
- **`@objectstack/metadata`: `DatabaseLoader` counts what it reads.** `exists()` answered true for an address only a legacy organization row held, while `load()` answered null, and `queryHistory`'s `total` counted legacy history rows no page serves. Neither counts them now.
17+
- **`@objectstack/metadata-protocol`: internal cleanup.** The organization plumbing that only ever carried the environment (`null`) is removed from the list merge and the served-row resolution. No export changes.
18+
- **`@objectstack/spec`: wording.** The four ADR-0087 entries `18.sys-metadata{,-audit,-commit,-history}-organization-column-retired` now name the ceremony's reviewed apply, `os migrate organization-ownership --out PLAN` and then `--apply --plan-file PLAN`, which `--plan-file`'s own entry requires. No id, category, version or field the migration engine reads changes.
19+
- **`@objectstack/cli`: wording.** `os migrate apply --allow-destructive`'s refusal on an unmigrated database now reads "a drop the organization-ownership ceremony owns, and the ceremony has not verified on this database". Its `--json` payload is unchanged.

‎content/docs/concepts/metadata-lifecycle.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ later ships. See [ADR-0070](https://github.com/objectstack-ai/objectstack/blob/m
106106

107107
**Most metadata types must not be overlaid by an environment** — overlaying a packaged item would break the physical schema or the behaviour its package ships. The whitelist lives in **one** place: `MetadataTypeRegistryEntry.allowEnvironmentOverlay` in `packages/spec/src/kernel/metadata-plugin.zod.ts` (spelled `allowOrgOverride` before protocol 18; `GET /api/v1/meta/types` serves it per entry as `allowEnvironmentOverlay`).
108108

109-
> **ADR-0131 D6 — the per-organization overlay axis is retired.** The `/meta` doors (REST and the runtime dispatcher) carry no organization into a metadata write or read: a Studio or `PUT /api/v1/meta/...` write of the five ✅ types below lands **environment-wide** (since ADR-0131 D7 the `sys_metadata` family declares no organization column) and is served to every organization, and an organization admin holding only the retired `manage_org_presentation` capability is refused (`403`). The protocol itself refuses every organization-scoped write, and every read — item, list, layers, cached, history, diff, audit, drafts and the commit timeline — is environment → code, whatever organization a caller names. An overlay row an earlier release stored under an organization — under the `single` posture, every Studio save of these types, filed under the Default Organization — is served by no read, and an 18.x `os serve` refuses to start on a database still holding one until the v18 migration ceremony (ADR-0131 C7, `os migrate organization-ownership --apply`) has carried it to the environment layer and dropped the column; nothing deletes or rewrites it before then. The anonymous form doors read the environment alone too: the ceremony carries every organization-layer public-form withdrawal onto the environment, fail-closed. The table still records which types take an overlay of a shipped item.
109+
> **ADR-0131 D6 — the per-organization overlay axis is retired.** The `/meta` doors (REST and the runtime dispatcher) carry no organization into a metadata write or read: a Studio or `PUT /api/v1/meta/...` write of the five ✅ types below lands **environment-wide** (since ADR-0131 D7 the `sys_metadata` family declares no organization column) and is served to every organization, and an organization admin holding only the retired `manage_org_presentation` capability is refused (`403`). The protocol itself refuses every organization-scoped write, and every read — item, list, layers, cached, history, diff, audit, drafts and the commit timeline — is environment → code, whatever organization a caller names. An overlay row an earlier release stored under an organization — under the `single` posture, every Studio save of these types, filed under the Default Organization — is served by no read, and an 18.x `os serve` refuses to start on a database still holding one until the v18 migration ceremony (ADR-0131 C7: `os migrate organization-ownership --out PLAN`, then `--apply --plan-file PLAN`) has carried it to the environment layer and dropped the column; nothing deletes or rewrites it before then. The anonymous form doors read the environment alone too: the ceremony carries every organization-layer public-form withdrawal onto the environment, fail-closed. The table still records which types take an overlay of a shipped item.
110110

111111
| Type | `allowEnvironmentOverlay` | Rationale |
112112
| :--- | :---: | :--- |

‎packages/cli/src/commands/migrate/apply.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -512,8 +512,8 @@ export default class MigrateApply extends Command {
512512
return;
513513
}
514514
printError(
515-
`Refusing this --allow-destructive run: it would drop organization_id from ${organizationDrops.join(', ')}, which the organization-ownership ceremony `
516-
+ `owns that drop, and it has not verified on this database. ${detail} Dropping the column first would turn `
515+
`Refusing this --allow-destructive run: it would drop organization_id from ${organizationDrops.join(', ')}, a drop the organization-ownership ceremony `
516+
+ `owns, and the ceremony has not verified on this database. ${detail} Dropping the column first would turn `
517517
+ 'every legacy organization row into an environment row without the ceremony\'s promotion or report. Run '
518518
+ '"os migrate organization-ownership --out PLAN" to write and review its plan, then '
519519
+ '"os migrate organization-ownership --apply --plan-file PLAN"; it drops these columns itself, in its '

‎packages/cli/src/commands/migrate/meta-stored-legacy-organization.integration.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,11 @@ describe('[ADR-0131 D6/D7] the ceremony boot\'s readers ignore a legacy organiza
307307
});
308308
expect((read as { lock?: unknown } | null)?.lock ?? 'none').not.toBe('full');
309309
expect((read as { editable?: unknown } | null)?.editable).not.toBe(false);
310+
// The served CONTENT is not the organization's body either. The lock is
311+
// selected apart from the content (`overlayLockLayerAt`), so the two
312+
// assertions above stay green when only the content read
313+
// (`findServedOverlayRow`) serves the organization's row; this one does not.
314+
expect((read as { item?: { subject?: unknown } } | null)?.item?.subject).not.toBe('Org A locked');
310315
// The `_lock` gate runs BEFORE the write. It did not refuse over the
311316
// organization's lock: the save reached the store, where the re-keyed
312317
// unique index refuses an environment row beside the organization's row of

0 commit comments

Comments
 (0)