@@ -750,20 +750,25 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
750750 throw new Error ( 'driver has neither raw nor execute' ) ;
751751 }
752752 } ;
753- // ADR-0005 (revised 2026-05): per-env DBs replace the old
753+ // ADR-0005 (revised 2026-05) + ADR-0048 : per-env DBs replace the old
754754 // "per-project" isolation, so `environment_id` is no longer a
755755 // discriminator. Overlay uniqueness is `(type, name,
756- // organization_id)` filtered to active rows. Drop the legacy
757- // composite index first so the new partial UNIQUE can claim
758- // the same name — DROP INDEX IF EXISTS is idempotent.
756+ // organization_id, COALESCE(package_id,''))` filtered to active
757+ // rows — `package_id` is in the key so two installed packages
758+ // shipping the same name each get their own overlay, while
759+ // `COALESCE(...,'')` keeps the package-less (global) rows unique
760+ // among themselves (a plain unique index would treat NULLs as
761+ // distinct and allow duplicate globals). Drop the legacy composite
762+ // index first so the new partial UNIQUE can claim the same name —
763+ // DROP INDEX IF EXISTS is idempotent.
759764 try { await exec ( "DROP INDEX IF EXISTS idx_sys_metadata_overlay_active" ) ; } catch { /* best-effort */ }
760765 const partialSql =
761766 "CREATE UNIQUE INDEX IF NOT EXISTS idx_sys_metadata_overlay_active " +
762- "ON sys_metadata (type, name, organization_id) " +
767+ "ON sys_metadata (type, name, organization_id, COALESCE(package_id, '') ) " +
763768 "WHERE state = 'active'" ;
764769 const fallbackSql =
765770 "CREATE INDEX IF NOT EXISTS idx_sys_metadata_overlay_active " +
766- "ON sys_metadata (type, name, organization_id)" ;
771+ "ON sys_metadata (type, name, organization_id, package_id )" ;
767772 try {
768773 await exec ( partialSql ) ;
769774 } catch ( err : any ) {
@@ -779,12 +784,14 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
779784 }
780785 // Mirror the same partial-UNIQUE for draft rows so a second
781786 // simultaneous draft cannot be inserted for the same
782- // (type,name,org). The unique-active index above already
787+ // (type,name,org,package ). The unique-active index above already
783788 // guards published rows; the two never collide because the
784- // `state` predicate disambiguates them.
789+ // `state` predicate disambiguates them. DROP first so an existing
790+ // legacy 3-column draft index is replaced in-place (ADR-0048).
791+ try { await exec ( "DROP INDEX IF EXISTS idx_sys_metadata_overlay_draft" ) ; } catch { /* best-effort */ }
785792 const draftPartialSql =
786793 "CREATE UNIQUE INDEX IF NOT EXISTS idx_sys_metadata_overlay_draft " +
787- "ON sys_metadata (type, name, organization_id) " +
794+ "ON sys_metadata (type, name, organization_id, COALESCE(package_id, '') ) " +
788795 "WHERE state = 'draft'" ;
789796 try {
790797 await exec ( draftPartialSql ) ;
@@ -794,7 +801,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
794801 try {
795802 await exec (
796803 "CREATE INDEX IF NOT EXISTS idx_sys_metadata_overlay_draft " +
797- "ON sys_metadata (type, name, organization_id)" ,
804+ "ON sys_metadata (type, name, organization_id, package_id )" ,
798805 ) ;
799806 } catch {
800807 // ignore — best effort
@@ -1428,6 +1435,11 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
14281435 where : { ...base , package_id : request . packageId } ,
14291436 } ) ;
14301437 if ( scoped ) return scoped ;
1438+ // ADR-0048 — global (package-less) draft only, never
1439+ // another package's draft.
1440+ return await this . engine . findOne ( 'sys_metadata' , {
1441+ where : { ...base , package_id : null } ,
1442+ } ) ;
14311443 }
14321444 return await this . engine . findOne ( 'sys_metadata' , { where : base } ) ;
14331445 } ;
@@ -1476,7 +1488,15 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
14761488 where : { ...base , package_id : request . packageId } ,
14771489 } ) ;
14781490 if ( scoped ) return scoped ;
1491+ // ADR-0048 — no package-owned overlay; fall back to the
1492+ // GLOBAL (package-less) overlay only. Must NOT match a
1493+ // different package's row, or a collision would serve
1494+ // package B's customization for a package A read.
1495+ return await this . engine . findOne ( 'sys_metadata' , {
1496+ where : { ...base , package_id : null } ,
1497+ } ) ;
14791498 }
1499+ // No package context (legacy/runtime reader) — match any.
14801500 return await this . engine . findOne ( 'sys_metadata' , { where : base } ) ;
14811501 } ;
14821502 const rec = await lookup ( request . type ) ;
@@ -1748,6 +1768,11 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
17481768 where : { ...base , package_id : request . packageId } ,
17491769 } ) ;
17501770 if ( scoped ) return scoped ;
1771+ // ADR-0048 — fall back to the GLOBAL (package-less)
1772+ // overlay only, never another package's row.
1773+ return await this . engine . findOne ( 'sys_metadata' , {
1774+ where : { ...base , package_id : null } ,
1775+ } ) ;
17511776 }
17521777 return await this . engine . findOne ( 'sys_metadata' , { where : base } ) ;
17531778 } ;
@@ -3722,8 +3747,13 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
37223747 // Parent is scoped to the lifecycle we're about to write:
37233748 // a draft's parent is the current draft hash (or null
37243749 // for the first draft); a publish's parent is the
3725- // current published hash.
3726- const current = await repo . get ( ref , { state : mode === 'draft' ? 'draft' : 'active' } ) ;
3750+ // current published hash. ADR-0048 — scope to the same
3751+ // package the upsert targets so a collision's other-package
3752+ // row is never read as this item's parent.
3753+ const current = await repo . get ( ref , {
3754+ state : mode === 'draft' ? 'draft' : 'active' ,
3755+ packageId : request . packageId ?? null ,
3756+ } ) ;
37273757 parentVersion = current ?. hash ?? null ;
37283758 }
37293759 try {
0 commit comments