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; }