From 13bd431a0b6f5e1fe41d34460619cf84f4846593 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 23 Aug 2026 08:25:20 +0800 Subject: [PATCH] fix(metadata-protocol): draft package inheritance reads the active base across scopes (#11087 layer 1b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The console's session carries an active org, so its draft lands in the org overlay scope while the active base row is env-wide (ADR-0005 overlay order). The inheritance lookup went through whereFor, which pins organization_id to the repo's own scope — so for exactly the console-session saves this inheritance exists for, it found nothing and the draft stayed package_id NULL (measured live on staging after the layer-1 deploy: an org-scoped view draft over an env-wide active row bound to app.k9qk). Resolve the base with the same $or two-scope reach listDrafts applies; a null-org repo keeps the env-wide-only equality. Co-Authored-By: Claude Opus 5 --- .changeset/draft-inherit-cross-scope-11087.md | 5 ++++ ...a-repository.draft-package-inherit.test.ts | 22 +++++++++++++++++ .../src/sys-metadata-repository.ts | 24 ++++++++++++++++--- 3 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 .changeset/draft-inherit-cross-scope-11087.md diff --git a/.changeset/draft-inherit-cross-scope-11087.md b/.changeset/draft-inherit-cross-scope-11087.md new file mode 100644 index 0000000000..610c95cd8f --- /dev/null +++ b/.changeset/draft-inherit-cross-scope-11087.md @@ -0,0 +1,5 @@ +--- +'@objectstack/metadata-protocol': patch +--- + +Draft package inheritance (#11087) resolves the overlaid active row with the same two-scope reach as `listDrafts` (caller org + env-wide) — an org-scoped console save now inherits from the env-wide active base instead of finding nothing in its own scope. diff --git a/packages/metadata-protocol/src/sys-metadata-repository.draft-package-inherit.test.ts b/packages/metadata-protocol/src/sys-metadata-repository.draft-package-inherit.test.ts index abf73c0b61..8ab02ee23b 100644 --- a/packages/metadata-protocol/src/sys-metadata-repository.draft-package-inherit.test.ts +++ b/packages/metadata-protocol/src/sys-metadata-repository.draft-package-inherit.test.ts @@ -131,6 +131,28 @@ describe('SysMetadataRepository draft-save package inheritance (#11087)', () => expect(draft.package_id ?? null).toBeNull(); }); + it('an ORG-scoped save inherits from the env-wide active row — the ADR-0005 overlay reach (#11087 layer 1b)', async () => { + // The console's session carries an active org, so its draft lands in the + // org scope while the active base row is env-wide. A same-org-only + // inheritance lookup finds nothing there (measured live), so the reach + // must mirror listDrafts' $or contract. + const engine = makeFakeEngine([ + { + type: REF.type, name: REF.name, organization_id: null, state: 'active', + package_id: 'app.k9qk', metadata: '{"label":"Member"}', checksum: 'sha-active', version: 1, + }, + ]); + const repo = new SysMetadataRepository({ + engine: engine as never, + organizationId: 'org_1', + orgLabel: 'org_1', + } as never); + await repo.put(REF, { label: 'Member v2' }, { parentVersion: null, actor: 't', state: 'draft' as const }); + const draft = engine.rows.find((r) => r.state === 'draft')!; + expect(draft.organization_id).toBe('org_1'); // org-scoped save, unchanged + expect(draft.package_id).toBe('app.k9qk'); // inherited across scopes + }); + it('adopts a pre-fix orphan draft (NULL package) instead of forking a second draft row', async () => { const engine = makeFakeEngine([ { diff --git a/packages/metadata-protocol/src/sys-metadata-repository.ts b/packages/metadata-protocol/src/sys-metadata-repository.ts index 49afe2984d..9343ca4bfa 100644 --- a/packages/metadata-protocol/src/sys-metadata-repository.ts +++ b/packages/metadata-protocol/src/sys-metadata-repository.ts @@ -464,9 +464,27 @@ export class SysMetadataRepository implements MetadataRepository { // ADR-0048), and with no active row — a brand-new item drafted first — // there is nothing to inherit and the package-less semantics stand. if (state === 'draft' && opts.packageId == null) { - const activeRow = await this.engine.findOne('sys_metadata', { - where: this.whereFor(ref, 'active'), - }); + // The overlaid base is resolved with the SAME two-scope reach the draft + // list applies (`listDrafts`' $or contract): an org-scoped caller's + // active row usually lives ENV-WIDE (`organization_id IS NULL`) — the + // ADR-0005 overlay order — so a same-org-only lookup here found nothing + // for exactly the console-session saves this inheritance exists for + // (measured live: an org-scoped view draft stayed `package_id NULL` + // over an env-wide active row bound to `app.k9qk`). + const activeWhere: Record = { + type: ref.type, + name: ref.name, + state: 'active', + }; + if (this.organizationId != null) { + activeWhere.$or = [ + { organization_id: this.organizationId }, + { organization_id: null }, + ]; + } else { + activeWhere.organization_id = null; + } + const activeRow = await this.engine.findOne('sys_metadata', { where: activeWhere }); const activePkg = (activeRow as { package_id?: string | null } | null)?.package_id ?? null; if (activePkg) targetPackageId = activePkg; }