From 26b4d15e5bc74558f91c00e799537cb22681384e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 22:41:38 +0000 Subject: [PATCH 1/4] fix(metadata-protocol, objectql, core): platform store reads and writes carry the explicit system opt-in The metadata protocol's overlay reads, list reads, authoring-gate fold, audit/commit trail writes and package verbs, SysMetadataRepository's store methods, ObjectQLPlugin's authored action/hook reads and the authored-translation read reached the data engine with no principal and no isSystem, so they rode plugin-security's principal-less hand-off (ADR-0096 E1). Each engine call now passes context { isSystem: true } (inside a repository transaction, { ...ctx, isSystem: true }, keeping the handle). None of the gates the hand-off runs before next() is scoped to the sys_metadata family, so nothing accepted or refused moves. Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN Co-authored-by: Claude --- .../fallbacks/authored-translation-sync.ts | 5 ++ packages/metadata-protocol/src/protocol.ts | 47 ++++++++++++++----- .../src/sys-metadata-repository.ts | 36 +++++++++----- packages/objectql/src/plugin.ts | 11 +++++ 4 files changed, 76 insertions(+), 23 deletions(-) diff --git a/packages/core/src/fallbacks/authored-translation-sync.ts b/packages/core/src/fallbacks/authored-translation-sync.ts index 22cc679d7d1..05e37c6b9a3 100644 --- a/packages/core/src/fallbacks/authored-translation-sync.ts +++ b/packages/core/src/fallbacks/authored-translation-sync.ts @@ -119,13 +119,18 @@ export async function readAuthoredTranslationLayer( ): Promise> | null> { let rows: any[]; try { + // [#21911, ADR-0096] The explicit system opt-in: a boot / resync read of + // the platform store with no caller behind it, never a principal-less + // engine context. rows = (await engine.find('sys_metadata', { where: { type: 'translation', state: 'active' }, + context: { isSystem: true }, })) ?? []; if (rows.length === 0) { // Legacy plural rows — mirrors the protocol's singular/plural fallback. rows = (await engine.find('sys_metadata', { where: { type: 'translations', state: 'active' }, + context: { isSystem: true }, })) ?? []; } } catch (err: any) { diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index 0b35ebb2763..bf80cf2d84d 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -6329,8 +6329,10 @@ export class ObjectStackProtocolImplementation implements try { const scopes: (string | null)[] = organizationId ? [null, organizationId] : [null]; const read = async (type: string, oid: string | null): Promise[]> => { + // [#21911] The explicit system opt-in — see findServedOverlayRow. const rs = await this.engine.find('sys_metadata', { where: { type, state: 'active', organization_id: oid }, + context: { isSystem: true }, }); return (rs ?? []) as Record[]; }; @@ -9125,13 +9127,14 @@ export class ObjectStackProtocolImplementation implements const queryDrafts = async (oid: string | null, pkg: string | undefined): Promise => { const whereClause: Record = { type: request.type, state: 'draft', organization_id: oid }; if (pkg) whereClause.package_id = pkg; - let rs = await this.engine.find('sys_metadata', { where: whereClause }); + // [#21911] The explicit system opt-in — see findServedOverlayRow. + let rs = await this.engine.find('sys_metadata', { where: whereClause, context: { isSystem: true } }); if (!rs || rs.length === 0) { const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type]; if (alt) { const altWhere: Record = { type: alt, state: 'draft', organization_id: oid }; if (pkg) altWhere.package_id = pkg; - rs = await this.engine.find('sys_metadata', { where: altWhere }); + rs = await this.engine.find('sys_metadata', { where: altWhere, context: { isSystem: true } }); } } return rs ?? []; @@ -9449,13 +9452,14 @@ export class ObjectStackProtocolImplementation implements organization_id: oid, }; if (packageId) whereClause.package_id = packageId; - let rs = await this.engine.find('sys_metadata', { where: whereClause }); + // [#21911] The explicit system opt-in — see findServedOverlayRow. + let rs = await this.engine.find('sys_metadata', { where: whereClause, context: { isSystem: true } }); if ((!rs || rs.length === 0)) { const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type]; if (alt) { const altWhere: Record = { type: alt, state: 'active', organization_id: oid }; if (packageId) altWhere.package_id = packageId; - rs = await this.engine.find('sys_metadata', { where: altWhere }); + rs = await this.engine.find('sys_metadata', { where: altWhere, context: { isSystem: true } }); } } return rs ?? []; @@ -9742,7 +9746,12 @@ export class ObjectStackProtocolImplementation implements type, name: args.name, state: args.state, organization_id: candidate.organizationId, }; if (candidate.packageId !== undefined) where.package_id = candidate.packageId; - const row = await this.engine.findOne('sys_metadata', { where }); + // [#21911, ADR-0096] The explicit system opt-in: a platform store + // read, which no caller's grants scope — the door that asked + // already authorized the caller, and the protocol scopes the row + // itself (`organization_id` above). It no longer reaches the data + // engine as a principal-less context. + const row = await this.engine.findOne('sys_metadata', { where, context: { isSystem: true } }); if (row) return { row, scope: candidate.scope }; } return undefined; @@ -9777,8 +9786,10 @@ export class ObjectStackProtocolImplementation implements return resolveOverlayLockLayer(address, async (organizationId, spelling) => { const type = spelling === 'canonical' ? address.type : other; if (type === undefined) return []; + // [#21911] The explicit system opt-in — see findServedOverlayRow. const rows: StoredOverlayRow[] | null | undefined = await this.engine.find('sys_metadata', { where: { type, name: address.name, state: 'active', organization_id: organizationId }, + context: { isSystem: true }, }); return rows ?? []; }, options); @@ -16485,8 +16496,10 @@ export class ObjectStackProtocolImplementation implements */ private async storedFlowBindingAgrees(name: string, packageId: string): Promise { try { + // [#21911] The explicit system opt-in — see findServedOverlayRow. const row = await this.engine.findOne('sys_metadata', { where: { type: 'flow', name, package_id: packageId }, + context: { isSystem: true }, }); return row != null; } catch (error) { @@ -17152,7 +17165,11 @@ export class ObjectStackProtocolImplementation implements lock_overridden: entry.lockOverridden ?? false, request_id: entry.requestId ?? null, note: entry.note ?? null, - } as any); + } as any, { + // [#21911] The explicit system opt-in: the platform writes its + // own trail row; the `actor` column above names who acted. + context: { isSystem: true }, + }); } catch (err: any) { // Don't promote audit-table failures to API errors. Log so // operators can spot a misconfigured deployment. @@ -18645,6 +18662,7 @@ export class ObjectStackProtocolImplementation implements name: string, organizationId: string | null, ): Promise { + // [#21911] The explicit system opt-in — see findServedOverlayRow. const row = await this.engine.findOne('sys_metadata', { where: { type, @@ -18652,6 +18670,7 @@ export class ObjectStackProtocolImplementation implements organization_id: organizationId, state: 'active', }, + context: { isSystem: true }, }); return (row as { package_id?: string | null } | null)?.package_id ?? null; } @@ -22303,8 +22322,10 @@ export class ObjectStackProtocolImplementation implements // (env-wide drafts have env-wide active rows). Using the // request's active org here would miss an env-wide edit and // mis-record it as a create in the revert plan (#3115). + // [#21911] The explicit system opt-in — see findServedOverlayRow. const activeRow = (await this.engine.findOne('sys_metadata', { where: { organization_id: d.organizationId ?? null, type: d.type, name: d.name, state: 'active' }, + context: { isSystem: true }, })) as { version?: number } | null; commitItems.push({ type: d.type, @@ -23086,7 +23107,8 @@ export class ObjectStackProtocolImplementation implements // changes. let rows: any[]; try { - rows = (await this.engine.find('sys_metadata', { where })) as any[]; + // [#21911] The explicit system opt-in — see findServedOverlayRow. + rows = (await this.engine.find('sys_metadata', { where, context: { isSystem: true } })) as any[]; } catch (e) { // [#12536] …through the classifier: a marked application refusal // is not a store fault. See {@link metadataReadFailureError}. @@ -23418,7 +23440,8 @@ export class ObjectStackProtocolImplementation implements { organization_id: null }, ]; } - const scanned = (await this.engine.find('sys_metadata', { where })) as any[]; + // [#21911] The explicit system opt-in — see findServedOverlayRow. + const scanned = (await this.engine.find('sys_metadata', { where, context: { isSystem: true } })) as any[]; // [#7819 tier 2] ADR-0005 overlay precedence — the caller's OWN org // shadows env-wide ({@link resolveMetaItemOrgScope} states the same rule @@ -23763,7 +23786,8 @@ export class ObjectStackProtocolImplementation implements { organization_id: null }, ]; } - const rows = (await this.engine.find('sys_metadata', { where })) as any[]; + // [#21911] The explicit system opt-in — see findServedOverlayRow. + const rows = (await this.engine.find('sys_metadata', { where, context: { isSystem: true } })) as any[]; const orphans = rows.filter( (r) => r?.package_id == null || r.package_id === '' || r.package_id === 'sys_metadata', ); @@ -23796,7 +23820,7 @@ export class ObjectStackProtocolImplementation implements await this.engine.update( 'sys_metadata', { package_id: request.targetPackageId }, - { where: { id: row.id } }, + { where: { id: row.id }, context: { isSystem: true } }, ); reassigned.push({ type: row.type, name: row.name }); } catch (e: any) { @@ -23874,7 +23898,8 @@ export class ObjectStackProtocolImplementation implements * again — which is precisely how the seam this repairs was born. */ private async persistPackageCommitRow(row: Record): Promise { - await this.engine.insert('sys_metadata_commit', row); + // [#21911] The explicit system opt-in — see recordMetadataAudit. + await this.engine.insert('sys_metadata_commit', row, { context: { isSystem: true } }); } /** diff --git a/packages/metadata-protocol/src/sys-metadata-repository.ts b/packages/metadata-protocol/src/sys-metadata-repository.ts index 013ffb59ed3..9150c06cf50 100644 --- a/packages/metadata-protocol/src/sys-metadata-repository.ts +++ b/packages/metadata-protocol/src/sys-metadata-repository.ts @@ -467,8 +467,17 @@ export class SysMetadataRepository implements MetadataRepository { // ADR-0048 — when a package scope is supplied, resolve the row owned by // that package (used by saveMetaItem to read the correct parent-version // lineage before an upsert). Omitted → legacy "any package" match. + // + // [#21911, ADR-0096] This read, and every store call of `put`, `delete`, + // `promoteDraft`, `restoreVersion`, `listDrafts` and the two lineage + // counters, carries the explicit system opt-in (`{ ...ctx, isSystem: true }` + // inside a transaction, so the handle rides along): the repository is + // platform plumbing under a door that already authorized the caller, and + // it scopes its own rows by organization. None of them reaches the data + // engine as a principal-less context. const row = await this.engine.findOne('sys_metadata', { where: this.whereFor(ref, state, opts && 'packageId' in opts ? (opts.packageId ?? null) : undefined), + context: { isSystem: true }, }); if (!row) return null; return this.rowToItem(ref, row); @@ -584,7 +593,7 @@ export class SysMetadataRepository implements MetadataRepository { } else { activeWhere.organization_id = null; } - const activeRow = await this.engine.findOne('sys_metadata', { where: activeWhere }); + const activeRow = await this.engine.findOne('sys_metadata', { where: activeWhere, context: { isSystem: true } }); const activePkg = (activeRow as { package_id?: string | null } | null)?.package_id ?? null; if (activePkg) targetPackageId = activePkg; } @@ -597,7 +606,7 @@ export class SysMetadataRepository implements MetadataRepository { // overlay. A package-less save (packageId null) targets the global row. let existing = await this.engine.findOne('sys_metadata', { where: this.whereFor(ref, state, targetPackageId), - context: ctx, + context: { ...ctx, isSystem: true }, }); // [#11087] Orphan-draft adoption: when the package binding above was // INHERITED (caller named none), a pre-fix draft for the same @@ -608,7 +617,7 @@ export class SysMetadataRepository implements MetadataRepository { if (!existing && state === 'draft' && opts.packageId == null && targetPackageId !== null) { existing = await this.engine.findOne('sys_metadata', { where: this.whereFor(ref, state, null), - context: ctx, + context: { ...ctx, isSystem: true }, }); } const existingHash: string | null = existing?.checksum ?? null; @@ -672,11 +681,11 @@ export class SysMetadataRepository implements MetadataRepository { } await this.engine.update('sys_metadata', parentRowData, { where: { id: existingId }, - context: ctx, + context: { ...ctx, isSystem: true }, }); } else { parentRowData.created_at = now; - await this.engine.insert('sys_metadata', parentRowData, { context: ctx }); + await this.engine.insert('sys_metadata', parentRowData, { context: { ...ctx, isSystem: true } }); } // Durable history append — same transaction, so the parent write @@ -702,7 +711,7 @@ export class SysMetadataRepository implements MetadataRepository { recorded_by: opts.actor ?? null, recorded_at: now, }, - { context: ctx }, + { context: { ...ctx, isSystem: true } }, ); const item: MetadataItem = { @@ -787,7 +796,7 @@ export class SysMetadataRepository implements MetadataRepository { state, 'packageId' in opts ? (opts.packageId ?? null) : undefined, ), - context: ctx, + context: { ...ctx, isSystem: true }, }); if (!existing) { throw new ConflictError(this.fullRef(ref), opts.parentVersion, null); @@ -817,7 +826,7 @@ export class SysMetadataRepository implements MetadataRepository { await this.engine.delete('sys_metadata', { where: { id: existingId }, - context: ctx, + context: { ...ctx, isSystem: true }, }); if (state === 'active') { @@ -843,7 +852,7 @@ export class SysMetadataRepository implements MetadataRepository { recorded_by: opts.actor ?? null, recorded_at: now, }, - { context: ctx }, + { context: { ...ctx, isSystem: true } }, ); } @@ -963,6 +972,7 @@ export class SysMetadataRepository implements MetadataRepository { 'draft', 'packageId' in opts ? (opts.packageId ?? null) : undefined, ), + context: { isSystem: true }, }); if (!draftRow) { const err: any = new Error( @@ -1062,6 +1072,7 @@ export class SysMetadataRepository implements MetadataRepository { name: full.name, version: targetVersion, }, + context: { isSystem: true }, }); if (!row) { const err: any = new Error( @@ -1102,6 +1113,7 @@ export class SysMetadataRepository implements MetadataRepository { // carries no `package_id` column, so a vanished binding is not recoverable. const activeRow = await this.engine.findOne('sys_metadata', { where: this.whereFor(ref, 'active'), + context: { isSystem: true }, }); const activePackageId = (activeRow as { package_id?: string | null } | null)?.package_id ?? null; @@ -1188,7 +1200,7 @@ export class SysMetadataRepository implements MetadataRepository { } if (filter?.type) where.type = filter.type; if (filter?.packageId) where.package_id = filter.packageId; - const rows = await this.engine.find('sys_metadata', { where }); + const rows = await this.engine.find('sys_metadata', { where, context: { isSystem: true } }); return (rows as any[]).map((row) => ({ type: row.type, name: row.name, @@ -2057,7 +2069,7 @@ export class SysMetadataRepository implements MetadataRepository { try { const rows = await this.engine.find(this.historyTable, { where: { organization_id: this.organizationId }, - context: ctx, + context: { ...ctx, isSystem: true }, }); let max = 0; for (const row of rows as Array<{ event_seq?: number | null }>) { @@ -2101,7 +2113,7 @@ export class SysMetadataRepository implements MetadataRepository { type: ref.type, name: ref.name, }, - context: ctx, + context: { ...ctx, isSystem: true }, }); let max = 0; for (const row of rows as Array<{ version?: number | null }>) { diff --git a/packages/objectql/src/plugin.ts b/packages/objectql/src/plugin.ts index 7615e1f45de..eee182aa0cc 100644 --- a/packages/objectql/src/plugin.ts +++ b/packages/objectql/src/plugin.ts @@ -2216,13 +2216,19 @@ export class ObjectQLPlugin implements Plugin { // only ever holds its own rows (saveMetaItem no longer stamps // environment_id). Rows across ALL organizations are taken — engine // hooks fire process-wide, matching flow-trigger semantics. + // + // [#21911, ADR-0096] The explicit system opt-in: a boot / resync read + // of the platform store with no caller behind it, never a principal-less + // engine context. let rows: any[] = (await this.ql.find('sys_metadata', { where: { type: 'hook', state: 'active' }, + context: { isSystem: true }, })) ?? []; if (rows.length === 0) { // Legacy plural rows — mirrors getMetaItems' singular/plural fallback. rows = (await this.ql.find('sys_metadata', { where: { type: 'hooks', state: 'active' }, + context: { isSystem: true }, })) ?? []; } const hooks: any[] = []; @@ -2399,12 +2405,16 @@ export class ObjectQLPlugin implements Plugin { } }; try { + // [#21911, ADR-0096] The explicit system opt-in on all three reads — see + // readAuthoredHookRows. let rows: any[] = (await this.ql.find('sys_metadata', { where: { type: 'action', state: 'active' }, + context: { isSystem: true }, })) ?? []; if (rows.length === 0) { rows = (await this.ql.find('sys_metadata', { where: { type: 'actions', state: 'active' }, + context: { isSystem: true }, })) ?? []; } const actions: any[] = []; @@ -2418,6 +2428,7 @@ export class ObjectQLPlugin implements Plugin { // the chain's action walker covers `objects[].actions[]`. const objectRows: any[] = (await this.ql.find('sys_metadata', { where: { type: 'object', state: 'active' }, + context: { isSystem: true }, })) ?? []; for (const row of objectRows) { const obj = parseRow(row, 'object'); From 08becc80e2361883d188508550750e07f45d6181 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 23:15:23 +0000 Subject: [PATCH 2/4] test(metadata-protocol, objectql, core): pin the explicit system opt-in on the platform store calls One pin per package: the engine double records the context each call arrives with, and every step asserts it reached the store and that every call carried isSystem: true. metadata-protocol drives the repository write path (draft save, publish, active save, rollback, delete), the overlay reads and the package verbs; objectql and core pin the authored action/hook and translation reads. The new pinned double is recorded in the engine-double-contract coverage ledger (--write). Changeset added. Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN Co-authored-by: Claude --- .changeset/21911-principal-less-producers.md | 19 ++ .../authored-translation-sync.test.ts | 19 ++ ...tocol.platform-store-system-opt-in.test.ts | 294 ++++++++++++++++++ .../src/plugin-authored-actions.test.ts | 15 + .../src/plugin-authored-hooks.test.ts | 13 + scripts/engine-double-contract.pinned.json | 15 + 6 files changed, 375 insertions(+) create mode 100644 .changeset/21911-principal-less-producers.md create mode 100644 packages/metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts diff --git a/.changeset/21911-principal-less-producers.md b/.changeset/21911-principal-less-producers.md new file mode 100644 index 00000000000..e72e179df1d --- /dev/null +++ b/.changeset/21911-principal-less-producers.md @@ -0,0 +1,19 @@ +--- +"@objectstack/metadata-protocol": patch +"@objectstack/objectql": patch +"@objectstack/core": patch +--- + +The platform's own `sys_metadata` reads and writes now carry the explicit system opt-in (`isSystem: true`) instead of reaching the data engine with no principal at all + +Clause-②: no + +- **What moved.** Each engine call in these functions now passes `context: { isSystem: true }`. Inside a repository transaction it passes `{ ...ctx, isSystem: true }`, so the transaction handle still rides along. + - `@objectstack/metadata-protocol`: the overlay reads (`findServedOverlayRow`, `overlayLockLayerAt`), the list read (`readActiveOverlayRows`, and `readFlattenedMetaItems`' draft preview), the authoring gate's stored-collection fold (`foldStoredCollection`), the audit and commit trail writes (`recordMetadataAudit`, `persistPackageCommitRow`), and the package verbs' store calls (`publishPackageDrafts`, `resolveOverlayPackageBinding`, `storedFlowBindingAgrees`, `deletePackage`, `duplicatePackage`, `reassignOrphanedMetadata`). + - `SysMetadataRepository`: `get`, `put`, `delete`, `promoteDraft`, `restoreVersion`, `listDrafts` and the two lineage counters. + - `@objectstack/objectql`: `ObjectQLPlugin`'s authored action and hook reads, at boot and on resync. + - `@objectstack/core`: the authored-translation read (`readAuthoredTranslationLayer`). +- **Why.** plugin-security passes an engine operation whose context has no user, no position, no permission set and no `isSystem` straight on to the next handler (ADR-0096's principal-less hand-off). These calls worked only because of that pass-through. They are platform plumbing: any door in front of them has already authorized the caller, and the protocol scopes its own rows by organization. So they now say so with the opt-in that already exists. +- **No gate verdict moves.** A system context skips the six gates the middleware still runs before that pass-through: package-managed, system-row, curated-capability, audience-anchor, engine-owned and delegated-administration. Four of them only act on other objects. The engine-owned gate never fires on a context with no user. The delegated-administration gate only acts on the RBAC link tables. So none of the six applies to the `sys_metadata` family. An instrumented run of the dogfood suite and a booted dev composition recorded no gate firing on any of these calls before the change. After the change it recorded no principal-less call from these functions. +- **One engine check also stands down under `isSystem`.** That is the referential-integrity check on a caller-supplied lookup. On these writes the only lookup it judged was `sys_metadata.organization_id`, which the repository fills from the door-derived organization. The instrumented runs recorded no refusal from it on any of these calls. +- ⛔ No new API, no export change, and no change to what any door authorizes. diff --git a/packages/core/src/fallbacks/authored-translation-sync.test.ts b/packages/core/src/fallbacks/authored-translation-sync.test.ts index 53c50e609b7..38344fff219 100644 --- a/packages/core/src/fallbacks/authored-translation-sync.test.ts +++ b/packages/core/src/fallbacks/authored-translation-sync.test.ts @@ -118,3 +118,22 @@ describe('authored-translation sync replays the conversion chain over stored row expect(warn).toHaveBeenCalledTimes(1); }); }); + +// [#21911, ADR-0096] The boot / resync read is platform plumbing with no caller +// behind it: it carries the explicit system opt-in rather than reaching the +// engine as a principal-less context. No singular rows, so the legacy plural +// fallback issues too. +describe('authored-translation read carries the explicit system opt-in (#21911)', () => { + it('passes isSystem on the singular read and on the plural fallback', async () => { + const engine = engineOf([]); + + const layer = await readAuthoredTranslationLayer(engine); + + expect(layer).toEqual({}); + expect(engine.find.mock.calls.map(([obj, q]) => [obj, q?.where?.type])).toEqual([ + ['sys_metadata', 'translation'], + ['sys_metadata', 'translations'], + ]); + for (const [, q] of engine.find.mock.calls) expect(q?.context).toEqual({ isSystem: true }); + }); +}); diff --git a/packages/metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts b/packages/metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts new file mode 100644 index 00000000000..dd6307c7beb --- /dev/null +++ b/packages/metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts @@ -0,0 +1,294 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#21911, ADR-0096] The metadata protocol's platform-store calls carry the + * EXPLICIT system opt-in, never a principal-less context. + * + * plugin-security hands an engine operation whose context carries no user, no + * position, no permission set and no `isSystem` straight to `next()` (the + * ADR-0096 E1 hand-off). The protocol's own reads and writes of the + * `sys_metadata` family reached the engine exactly that way — a `{ where }` + * with no `context`, or a repository transaction context with only its + * handle — so they worked only because the hand-off let them through. They + * now pass `context: { isSystem: true }` (inside a repository transaction, + * `{ ...ctx, isSystem: true }`, so the handle rides along), the opt-in that + * already exists; nothing about what any door authorizes moves. + * + * The observation channel is the engine double's own record of the context + * each call arrived with. Every step asserts it recorded at least one call + * (so a step that stops reaching the store reds instead of passing over + * nothing) and that every recorded call carried `isSystem: true`. + * + * The double is the pinned shape of `protocol-publish-drafts-org-scope.test.ts` + * (its write verbs and `findOne` open with the producer's own dispatch + * predicates), plus that recorder and a transaction that hands its callback a + * real handle object, as `ObjectQL.transaction` does. + */ + +import { describe, expect, it } from 'vitest'; +import { assertEngineDeleteDispatch, assertEngineUpdateDispatch, assertEngineFindOnePredicate } from '@objectstack/metadata-core'; +import { ObjectStackProtocolImplementation } from './protocol.js'; + +interface Row { + id: string; + type: string; + name: string; + organization_id: string | null; + package_id: string | null; + state: string; + metadata: string; + [k: string]: unknown; +} + +interface HistoryRow { + id: string; + organization_id: string | null; + type: string; + name: string; + version: number; + [k: string]: unknown; +} + +interface EngineCall { + verb: string; + table: string; + context: Record | undefined; +} + +const TRX = { trx: 'handle' }; + +function keyOf(w: Record) { + return `${w.type}|${w.name}|${w.organization_id ?? '__env__'}|${w.state ?? 'active'}|${w.package_id ?? '__nopkg__'}`; +} + +function matchesMetadataWhere(r: Row, where: Record): boolean { + for (const [k, v] of Object.entries(where)) { + if (k === '$or') { + const clauses = v as Array>; + if (!clauses.some((c) => matchesMetadataWhere(r, c))) return false; + continue; + } + if (k.startsWith('$')) throw new Error(`[test double] unsupported WHERE combinator '${k}'`); + if (v === undefined) continue; + if (r[k] !== v) return false; + } + return true; +} + +function makeStubEngine() { + const rows = new Map(); + const historyRows: HistoryRow[] = []; + const calls: EngineCall[] = []; + let nextId = 0; + + const record = (verb: string, table: string, context: unknown) => { + calls.push({ verb, table, context: context as Record | undefined }); + }; + + const findRow = (w: Record): { key: string; row: Row } | null => { + if (w.id !== undefined) { + for (const [k, r] of rows) if (r.id === w.id) return { key: k, row: r }; + return null; + } + for (const [k, r] of rows) if (matchesMetadataWhere(r, w)) return { key: k, row: r }; + return null; + }; + + const matchesHistory = (h: HistoryRow, w: Record): boolean => { + for (const k of ['organization_id', 'type', 'name', 'version', 'checksum'] as const) { + if (w[k] !== undefined && h[k] !== w[k]) return false; + } + return true; + }; + + const engine: any = { + async findOne(table: string, opts: { where: Record; context?: unknown }) { + assertEngineFindOnePredicate(table, opts); + record('findOne', table, opts.context); + if (table === 'sys_metadata_history') { + return historyRows.find((h) => matchesHistory(h, opts.where)) ?? null; + } + return findRow(opts.where)?.row ?? null; + }, + async find(table: string, opts: { where: Record; context?: unknown }) { + record('find', table, opts?.context); + if (table === 'sys_metadata_history') { + return historyRows.filter((h) => matchesHistory(h, opts.where ?? {})); + } + if (table !== 'sys_metadata') return []; + return Array.from(rows.values()).filter((r) => matchesMetadataWhere(r, opts.where ?? {})); + }, + async insert(table: string, data: Record, options?: { context?: unknown }) { + record('insert', table, options?.context); + nextId += 1; + if (table === 'sys_metadata_history') { + historyRows.push({ id: `h_${nextId}`, ...(data as any) }); + return { id: `h_${nextId}` }; + } + if (table !== 'sys_metadata') return { id: `${table}_${nextId}` }; + const row = { id: `r_${nextId}`, ...(data as any) } as Row; + rows.set(keyOf(data), row); + return { id: row.id }; + }, + async update(table: string, data: Record, opts: { where: Record; context?: unknown }) { + assertEngineUpdateDispatch(data, opts); + record('update', table, opts.context); + const found = findRow(opts.where); + if (!found) return { id: null }; + const merged = { ...found.row, ...(data as any) }; + rows.delete(found.key); + rows.set(keyOf(merged), merged); + return { id: found.row.id }; + }, + async delete(table: string, opts: { where: Record; context?: unknown }) { + assertEngineDeleteDispatch(opts); + record('delete', table, opts.context); + const found = findRow(opts.where); + if (!found) return { deleted: 0 }; + rows.delete(found.key); + return { deleted: 1 }; + }, + async transaction(cb: (ctx: any, info: { owned: boolean }) => Promise): Promise { + return cb({ transaction: TRX }, { owned: true }); + }, + registry: { + registerItem: () => undefined, + registerObject: () => undefined, + unregisterItem: () => undefined, + listItems: () => [], + getItem: () => undefined, + getObject: () => undefined, + getPackage: () => undefined, + getArtifactItem: () => undefined, + isPackageDisabled: () => false, + applyNavContributions: (app: unknown) => app, + }, + }; + return { engine, rows, historyRows, calls }; +} + +const viewBody = (name: string) => ({ + name, + label: 'Project Tasks', + object: 'proj_task', + viewKind: 'list', + columns: [{ field: 'title', label: 'Title' }], +}); + +/** + * Runs `step`, then asserts it reached the store and that every engine call + * it issued carried the explicit system opt-in. + */ +async function expectSystemOptIn( + calls: EngineCall[], + label: string, + step: () => Promise, +): Promise { + const from = calls.length; + await step(); + const issued = calls.slice(from); + expect(issued.length, `${label}: reached the store`).toBeGreaterThan(0); + const principalLess = issued.filter((c) => c.context?.isSystem !== true); + expect(principalLess, `${label}: every engine call carries isSystem: true`).toEqual([]); + return issued; +} + +describe('platform-store calls carry the explicit system opt-in (#21911)', () => { + it('the repository write path: draft save, publish, active save, rollback, delete', async () => { + const { engine, calls } = makeStubEngine(); + const protocol = new ObjectStackProtocolImplementation(engine); + + // SysMetadataRepository.put (create, draft) + the two lineage counters. + const draftCalls = await expectSystemOptIn(calls, 'draft save', () => protocol.saveMetaItem({ + type: 'view', name: 'proj_task_grid', item: viewBody('proj_task_grid'), + packageId: 'app.pin', mode: 'draft', + })); + // Inside the repository transaction the handle rides along with the opt-in. + const inTxn = draftCalls.filter((c) => c.context?.transaction !== undefined); + expect(inTxn.length, 'the put transaction reached the store').toBeGreaterThan(0); + for (const c of inTxn) expect(c.context).toEqual({ transaction: TRX, isSystem: true }); + + // publishPackageDrafts: listDrafts, its pre-publish active-row read, + // promoteDraft (+ put), recordMetadataAudit and persistPackageCommitRow. + const publishCalls = await expectSystemOptIn(calls, 'publish', async () => { + const res = await protocol.publishPackageDrafts({ packageId: 'app.pin' }); + expect(res).toMatchObject({ success: true, publishedCount: 1, failedCount: 0 }); + }); + expect(publishCalls.some((c) => c.table === 'sys_metadata_audit' && c.verb === 'insert')).toBe(true); + expect(publishCalls.some((c) => c.table === 'sys_metadata_commit' && c.verb === 'insert')).toBe(true); + + // put (update) + the runtime authoring gate's stored-collection fold. + await expectSystemOptIn(calls, 'active save', () => protocol.saveMetaItem({ + type: 'view', name: 'proj_task_grid', + item: { ...viewBody('proj_task_grid'), label: 'Project Tasks v2' }, + packageId: 'app.pin', mode: 'publish', force: true, + })); + + // restoreVersion (history read + active read, then put). + await expectSystemOptIn(calls, 'rollback', async () => { + await protocol.rollbackMetaItem({ type: 'view', name: 'proj_task_grid', toVersion: 1 } as any); + }); + + // SysMetadataRepository.delete (findOne, delete, tombstone insert). + const deleteCalls = await expectSystemOptIn(calls, 'delete', () => protocol.deleteMetaItem({ + type: 'view', name: 'proj_task_grid', force: true, + } as any)); + expect(deleteCalls.some((c) => c.verb === 'delete' && c.table === 'sys_metadata')).toBe(true); + }); + + it('the overlay reads: served row, lock layer, list read, draft preview', async () => { + const { engine, calls } = makeStubEngine(); + const protocol = new ObjectStackProtocolImplementation(engine) as any; + + await protocol.saveMetaItem({ + type: 'view', name: 'proj_task_grid', item: viewBody('proj_task_grid'), + packageId: 'app.pin', mode: 'draft', + }); + + // findServedOverlayRow + overlayLockLayerAt (the by-name read). + await expectSystemOptIn(calls, 'by-name read', () => protocol.getMetaItem({ type: 'view', name: 'proj_task_grid' })); + // readFlattenedMetaItems: readActiveOverlayRows' queryByOrg + the draft preview. + await expectSystemOptIn(calls, 'list read with draft preview', () => protocol.getMetaItems({ + type: 'view', previewDrafts: true, + })); + // Each private reader on its own, so no caller can mask one of them. + await expectSystemOptIn(calls, 'findServedOverlayRow', () => protocol.findServedOverlayRow({ + type: 'view', name: 'proj_task_grid', orgId: 'org_a', state: 'draft', packageId: 'app.pin', + })); + await expectSystemOptIn(calls, 'overlayLockLayerAt', () => protocol.overlayLockLayerAt( + { type: 'view', name: 'proj_task_grid', organizationId: 'org_a' }, { otherSpelling: true }, + )); + await expectSystemOptIn(calls, 'readActiveOverlayRows', () => protocol.readActiveOverlayRows({ type: 'view' }, 'org_a')); + await expectSystemOptIn(calls, 'foldStoredCollection', () => protocol.foldStoredCollection([], 'object', 'objects', 'org_a')); + await expectSystemOptIn(calls, 'resolveOverlayPackageBinding', () => protocol.resolveOverlayPackageBinding('view', 'proj_task_grid', 'org_a')); + await expectSystemOptIn(calls, 'storedFlowBindingAgrees', () => protocol.storedFlowBindingAgrees('flow_a', 'app.pin')); + await expectSystemOptIn(calls, 'recordMetadataAudit', () => protocol.recordMetadataAudit({ + type: 'view', name: 'proj_task_grid', operation: 'update', outcome: 'success', code: 'OK', + })); + await expectSystemOptIn(calls, 'persistPackageCommitRow', () => protocol.persistPackageCommitRow({ id: 'c_1' })); + }); + + it('the package verbs: reassign orphans, duplicate, uninstall', async () => { + const { engine, calls } = makeStubEngine(); + const protocol = new ObjectStackProtocolImplementation(engine); + + await protocol.saveMetaItem({ + type: 'view', name: 'orphan_grid', item: viewBody('orphan_grid'), mode: 'publish', + }); + + const reassignCalls = await expectSystemOptIn(calls, 'reassignOrphanedMetadata', async () => { + const res = await protocol.reassignOrphanedMetadata({ targetPackageId: 'app.pin' }); + expect(res.reassignedCount).toBe(1); + }); + expect(reassignCalls.map((c) => c.verb)).toEqual(['find', 'update']); + + await expectSystemOptIn(calls, 'duplicatePackage', async () => { + await protocol.duplicatePackage({ + sourcePackageId: 'app.pin', targetPackageId: 'com.example.pincopy', targetNamespace: 'pincopy', + } as any).catch(() => undefined); + }); + await expectSystemOptIn(calls, 'deletePackage', async () => { + await protocol.deletePackage({ packageId: 'com.example.pincopy', allTenants: true } as any).catch(() => undefined); + }); + }); +}); diff --git a/packages/objectql/src/plugin-authored-actions.test.ts b/packages/objectql/src/plugin-authored-actions.test.ts index 0d15f5dff35..6089963002d 100644 --- a/packages/objectql/src/plugin-authored-actions.test.ts +++ b/packages/objectql/src/plugin-authored-actions.test.ts @@ -275,6 +275,21 @@ describe('ObjectQLPlugin.resyncAuthoredActions (#2605)', () => { expect(ql.registerAction).toHaveBeenCalledTimes(1); expect(ql.registerAction.mock.calls[0][1]).toBe('good_action'); }); + + // [#21911, ADR-0096] The boot / resync read is platform plumbing with no + // caller behind it: it carries the explicit system opt-in rather than + // reaching the engine as a principal-less context. No singular rows, so all + // three reads issue — the singular one, the legacy plural fallback and the + // embedded-object read. + it('reads sys_metadata with the explicit system opt-in on every read (#21911)', async () => { + ql.find.mockResolvedValue([]); + + await (makePlugin(ql) as any).resyncAuthoredActions(makeCtx()); + + const reads = ql.find.mock.calls.filter(([obj]: [string]) => obj === 'sys_metadata'); + expect(reads.map(([, q]: [string, AnyRecord]) => q?.where?.type)).toEqual(['action', 'actions', 'object']); + for (const [, q] of reads) expect(q?.context).toEqual({ isSystem: true }); + }); }); describe('ObjectQLPlugin protocol-mutation subscription (#2605)', () => { diff --git a/packages/objectql/src/plugin-authored-hooks.test.ts b/packages/objectql/src/plugin-authored-hooks.test.ts index 9ba41abf43e..305e0b7f169 100644 --- a/packages/objectql/src/plugin-authored-hooks.test.ts +++ b/packages/objectql/src/plugin-authored-hooks.test.ts @@ -180,6 +180,19 @@ describe('ObjectQLPlugin.resyncAuthoredHooks (#2588)', () => { const [hooks] = ql.bindHooks.mock.calls[0]; expect(hooks.map((h: AnyRecord) => h.name)).toEqual(['good_hook']); }); + + // [#21911, ADR-0096] The boot / resync read carries the explicit system + // opt-in rather than reaching the engine as a principal-less context. No + // singular rows, so the legacy plural fallback issues too. + it('reads sys_metadata with the explicit system opt-in on every read (#21911)', async () => { + ql.find.mockResolvedValue([]); + + await (makePlugin(ql) as any).resyncAuthoredHooks(makeCtx()); + + const reads = ql.find.mock.calls.filter(([obj]: [string]) => obj === 'sys_metadata'); + expect(reads.map(([, q]: [string, AnyRecord]) => q?.where?.type)).toEqual(['hook', 'hooks']); + for (const [, q] of reads) expect(q?.context).toEqual({ isSystem: true }); + }); }); describe('ObjectQLPlugin protocol-mutation subscription (#2588)', () => { diff --git a/scripts/engine-double-contract.pinned.json b/scripts/engine-double-contract.pinned.json index 5d93cb509e2..01100064419 100644 --- a/scripts/engine-double-contract.pinned.json +++ b/scripts/engine-double-contract.pinned.json @@ -1111,6 +1111,21 @@ "verb": "update", "pinned": 1 }, + { + "file": "packages/metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts", + "verb": "delete", + "pinned": 1 + }, + { + "file": "packages/metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts", + "verb": "findOne", + "pinned": 1 + }, + { + "file": "packages/metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts", + "verb": "update", + "pinned": 1 + }, { "file": "packages/metadata-protocol/src/protocol.publish-commit-capture-read-failure.test.ts", "verb": "delete", From ca70934e0c36418cdd2d64949f3a2e4bfafc541b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 00:18:56 +0000 Subject: [PATCH 3/4] test(metadata-protocol): the opt-in pin's find double honours the caller's bound check:objectql-double-limit refuses a new limit-blind find double; the bound is now applied after the filter, by presence. Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN Co-authored-by: Claude --- .../protocol.platform-store-system-opt-in.test.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts b/packages/metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts index dd6307c7beb..ad0c930b461 100644 --- a/packages/metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts +++ b/packages/metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts @@ -110,13 +110,15 @@ function makeStubEngine() { } return findRow(opts.where)?.row ?? null; }, - async find(table: string, opts: { where: Record; context?: unknown }) { + async find(table: string, opts: { where: Record; limit?: number; context?: unknown }) { record('find', table, opts?.context); - if (table === 'sys_metadata_history') { - return historyRows.filter((h) => matchesHistory(h, opts.where ?? {})); - } - if (table !== 'sys_metadata') return []; - return Array.from(rows.values()).filter((r) => matchesMetadataWhere(r, opts.where ?? {})); + const hits: unknown[] = table === 'sys_metadata_history' + ? historyRows.filter((h) => matchesHistory(h, opts.where ?? {})) + : table === 'sys_metadata' + ? Array.from(rows.values()).filter((r) => matchesMetadataWhere(r, opts.where ?? {})) + : []; + // The caller's bound, applied after the filter, by presence. + return typeof opts?.limit === 'number' ? hits.slice(0, opts.limit) : hits; }, async insert(table: string, data: Record, options?: { context?: unknown }) { record('insert', table, options?.context); From 3a4835dfb7e4341e8cdd2ad44162ee9fd185b59b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 00:31:30 +0000 Subject: [PATCH 4/4] test(objectql): the protocol suites expect the explicit system opt-in on the platform store calls Seven exact-argument expectations in protocol-meta, the reassign rebind and the listDrafts WHERE now include context { isSystem: true }. The two revert/rollback conflict pins identified put's in-transaction read by a bare `context` key; every repository read now carries one, so they give the engine a transaction that hands its callback a handle (as ObjectQL.transaction does) and discriminate on the handle instead. Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN Co-authored-by: Claude --- .../src/protocol-commit-history.test.ts | 13 +++++++---- packages/objectql/src/protocol-meta.test.ts | 22 +++++++++++++++---- .../src/protocol-package-lifecycle.test.ts | 3 ++- ...rotocol-writepath-object-ownership.test.ts | 13 +++++++---- ...ys-metadata-repository-list-drafts.test.ts | 2 ++ 5 files changed, 40 insertions(+), 13 deletions(-) diff --git a/packages/objectql/src/protocol-commit-history.test.ts b/packages/objectql/src/protocol-commit-history.test.ts index 8b3cf8a9b0e..b6fe15d7d36 100644 --- a/packages/objectql/src/protocol-commit-history.test.ts +++ b/packages/objectql/src/protocol-commit-history.test.ts @@ -496,13 +496,18 @@ describe('#6215 — revertCommit restores a PACKAGE-BOUND overlay row', () => { const realFindOne = engine.findOne.bind(engine); // The concurrent publish lands between `restoreVersion`'s parent read and // `put`'s optimistic-lock read. That second read is identified by the one - // property only it has — it runs INSIDE the write transaction, so the - // engine call carries a `context` — rather than by counting reads, which - // would silently stop covering anything the day a read is added. + // property only it has — it runs INSIDE the write transaction, so its + // context carries the transaction handle — rather than by counting reads, + // which would silently stop covering anything the day a read is added. + // [#21911] Every repository read now carries the explicit system opt-in, + // so a bare `context` no longer singles it out; the handle does. The + // engine hands its callback one here, as `ObjectQL.transaction` does. + engine.transaction = async (cb: (ctx: unknown, info: { owned: boolean }) => Promise) => + cb({ transaction: 'trx' }, { owned: true }); let fired = false; engine.findOne = async (table: string, opts: Record) => { const row = await realFindOne(table, opts); - if (!fired && table === 'sys_metadata' && row && 'context' in opts && opts.where?.state === 'active') { + if (!fired && table === 'sys_metadata' && row && opts.context?.transaction !== undefined && opts.where?.state === 'active') { fired = true; row.checksum = `sha256:${'a'.repeat(64)}`; // someone else published } diff --git a/packages/objectql/src/protocol-meta.test.ts b/packages/objectql/src/protocol-meta.test.ts index fa6279481de..aa82903a782 100644 --- a/packages/objectql/src/protocol-meta.test.ts +++ b/packages/objectql/src/protocol-meta.test.ts @@ -80,6 +80,8 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => { // ADR-0048 — a package-less save scopes the upsert lookup to the // GLOBAL row (package_id IS NULL), not any package's row. where: { type: 'view', name: 'test_grid', organization_id: 'org_alpha', state: 'active', package_id: null }, + // [#21911] The platform store read carries the explicit system opt-in. + context: { isSystem: true }, }); expect(mockEngine.insert).toHaveBeenCalledWith('sys_metadata', expect.objectContaining({ organization_id: 'org_alpha', @@ -154,6 +156,8 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => { // the assertion could no longer fail. expect(mockEngine.findOne).toHaveBeenCalledWith('sys_metadata', { where: { type: 'view', name: 'test_grid', state: 'active', organization_id: 'org_alpha' }, + // [#21911] The platform store read carries the explicit system opt-in. + context: { isSystem: true }, }); }); @@ -341,7 +345,9 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => { expect(mockEngine.findOne).toHaveBeenCalledWith('sys_metadata', { // ADR-0048 — package-less save scopes the lookup to the global row. - where: { type: 'app', name: 'test_app', organization_id: null, state: 'active', package_id: null } + where: { type: 'app', name: 'test_app', organization_id: null, state: 'active', package_id: null }, + // [#21911] The platform store read carries the explicit system opt-in. + context: { isSystem: true }, }); expect(mockEngine.insert).toHaveBeenCalledWith('sys_metadata', expect.objectContaining({ name: 'test_app', @@ -368,6 +374,8 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => { expect(mockEngine.findOne).toHaveBeenCalledWith('sys_metadata', { where: { type: 'view', name: 'test_grid', organization_id: 'org_alpha', state: 'active', package_id: 'com.acme.beta' }, + // [#21911] The platform store read carries the explicit system opt-in. + context: { isSystem: true }, }); expect(mockEngine.insert).toHaveBeenCalledWith('sys_metadata', expect.objectContaining({ package_id: 'com.acme.beta', @@ -742,7 +750,9 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => { expect(result.item).toMatchObject(sampleApp); expect(mockEngine.findOne).toHaveBeenCalledWith('sys_metadata', { - where: { type: 'app', name: 'test_app', state: 'active', organization_id: null } + where: { type: 'app', name: 'test_app', state: 'active', organization_id: null }, + // [#21911] The platform store read carries the explicit system opt-in. + context: { isSystem: true }, }); }); @@ -1021,7 +1031,9 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => { // DB *is* queried (always-merge semantics) so seeded metadata // surfaces even when the registry already has unrelated items. expect(mockEngine.find).toHaveBeenCalledWith('sys_metadata', { - where: { type: 'app', state: 'active', organization_id: null } + where: { type: 'app', state: 'active', organization_id: null }, + // [#21911] The platform store read carries the explicit system opt-in. + context: { isSystem: true }, }); }); @@ -1164,7 +1176,9 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => { expect(result.items).toHaveLength(1); expect(result.items[0]).toMatchObject(sampleApp); expect(mockEngine.find).toHaveBeenCalledWith('sys_metadata', { - where: { type: 'app', state: 'active', organization_id: null } + where: { type: 'app', state: 'active', organization_id: null }, + // [#21911] The platform store read carries the explicit system opt-in. + context: { isSystem: true }, }); }); diff --git a/packages/objectql/src/protocol-package-lifecycle.test.ts b/packages/objectql/src/protocol-package-lifecycle.test.ts index 83b198b1da1..e9f297522dc 100644 --- a/packages/objectql/src/protocol-package-lifecycle.test.ts +++ b/packages/objectql/src/protocol-package-lifecycle.test.ts @@ -200,7 +200,8 @@ describe('protocol.reassignOrphanedMetadata (ADR-0070 D5)', () => { expect(res).toMatchObject({ success: true, reassignedCount: 3, targetPackageId: 'app.home' }); const movedIds = update.mock.calls.map((c: any) => c[2].where.id); expect(movedIds).toEqual(['r1', 'r2', 'r3']); // the 3 orphans, not the owned r4 - expect(update).toHaveBeenCalledWith('sys_metadata', { package_id: 'app.home' }, { where: { id: 'r1' } }); + // [#21911] The rebind carries the explicit system opt-in. + expect(update).toHaveBeenCalledWith('sys_metadata', { package_id: 'app.home' }, { where: { id: 'r1' }, context: { isSystem: true } }); expect(res.reassigned.some((x) => x.name === 'owned')).toBe(false); }); diff --git a/packages/objectql/src/protocol-writepath-object-ownership.test.ts b/packages/objectql/src/protocol-writepath-object-ownership.test.ts index 4ea52435d5d..e0cb3105fb3 100644 --- a/packages/objectql/src/protocol-writepath-object-ownership.test.ts +++ b/packages/objectql/src/protocol-writepath-object-ownership.test.ts @@ -455,13 +455,18 @@ describe('#4636 — rollback re-registers under the row\'s own package binding', // The concurrent publish lands between `restoreVersion`'s parent read // and `put`'s optimistic-lock read. That second read is identified by // the one property only it has — it runs INSIDE the write transaction, - // so the engine call carries a `context` — rather than by counting - // reads, which would silently stop covering anything the day a read is - // added to the rollback path. + // so its context carries the transaction handle — rather than by + // counting reads, which would silently stop covering anything the day + // a read is added to the rollback path. [#21911] Every repository read + // now carries the explicit system opt-in, so a bare `context` no + // longer singles it out; the handle does. The engine hands its + // callback one here, as `ObjectQL.transaction` does. + engine.transaction = async (cb: (ctx: unknown, info: { owned: boolean }) => Promise) => + cb({ transaction: 'trx' }, { owned: true }); let fired = false; engine.findOne = async (table: string, opts: Record) => { const row = await realFindOne(table, opts); - if (!fired && table === 'sys_metadata' && row && 'context' in opts && opts.where?.state === 'active') { + if (!fired && table === 'sys_metadata' && row && opts.context?.transaction !== undefined && opts.where?.state === 'active') { fired = true; (row as any).checksum = `sha256:${'a'.repeat(64)}`; // someone else published } diff --git a/packages/objectql/src/sys-metadata-repository-list-drafts.test.ts b/packages/objectql/src/sys-metadata-repository-list-drafts.test.ts index af924f31b95..73e71f09b20 100644 --- a/packages/objectql/src/sys-metadata-repository-list-drafts.test.ts +++ b/packages/objectql/src/sys-metadata-repository-list-drafts.test.ts @@ -96,6 +96,8 @@ describe('SysMetadataRepository.listDrafts (ADR-0033)', () => { await repo.listDrafts({ type: 'object', packageId: 'app.edu' }); expect(find).toHaveBeenCalledWith('sys_metadata', { where: { organization_id: null, state: 'draft', type: 'object', package_id: 'app.edu' }, + // [#21911] The platform store read carries the explicit system opt-in. + context: { isSystem: true }, }); });