@@ -110,21 +110,30 @@ function makeQl() {
110110 * Mock metadata protocol over the ql's sys_metadata table: env-scope active
111111 * overlays, layered read (overlay-wins over `declared`), and the ADR-0094
112112 * awaited mutation-projector seam.
113+ *
114+ * [#21861] The package dimension of a row is modelled, because the defect it
115+ * hid lives there. A save targets the ONE row keyed by its `packageId` (absent
116+ * = the package-less row), as `SysMetadataRepository.put` does, so a save that
117+ * names no package beside a package-bound row mints a second row instead of
118+ * quietly overwriting the only one. The reads serve the first env-wide row of
119+ * the name in any package (`findServedOverlayRow` with no package), and the
120+ * single-item read states that row's `package_id` on the item as `_packageId`,
121+ * as `getMetaItem` does.
113122 */
114123function makeProtocol ( ql : any , declared : Record < string , any > = { } ) {
115124 let projector : ( ( evt : any ) => Promise < void > ) | null = null ;
116- const overlayFor = ( name : string ) =>
117- ql . metaRows . find (
118- ( r : any ) =>
119- r . type === 'permission' && r . name === name && r . state === 'active' && ( r . organization_id ?? null ) === null ,
120- ) ;
125+ const isActiveEnvRow = ( r : any , name : string ) =>
126+ r . type === 'permission' && r . name === name && r . state === 'active' && ( r . organization_id ?? null ) === null ;
127+ const overlayFor = ( name : string ) => ql . metaRows . find ( ( r : any ) => isActiveEnvRow ( r , name ) ) ;
128+ const rowAt = ( name : string , packageId : string | null ) =>
129+ ql . metaRows . find ( ( r : any ) => isActiveEnvRow ( r , name ) && ( r . package_id ?? null ) === packageId ) ;
121130 const protocol = {
122131 saves : [ ] as any [ ] ,
123132 deletes : [ ] as any [ ] ,
124133 registerMutationProjector ( _type : string , fn : ( evt : any ) => Promise < void > ) {
125134 projector = fn ;
126135 } ,
127- async saveMetaItem ( req : { type : string ; name : string ; item : any ; actor ?: string } ) {
136+ async saveMetaItem ( req : { type : string ; name : string ; item : any ; actor ?: string ; packageId ?: string | null } ) {
128137 // [#6858 / ADR-0094 D5-R] ADR-0005's tier gate, first — ahead of schema
129138 // validation, exactly as `protocol.ts` orders it. `declared` IS this
130139 // stub's artifact registry, so `declared[name] !== undefined` is the
@@ -165,12 +174,15 @@ function makeProtocol(ql: any, declared: Record<string, any> = {}) {
165174 err . status = 422 ;
166175 throw err ;
167176 }
168- const existing = overlayFor ( req . name ) ;
177+ const packageId = req . packageId ?? null ;
178+ const existing = rowAt ( req . name , packageId ) ;
169179 if ( existing ) existing . metadata = JSON . stringify ( req . item ) ;
170180 else {
171181 ql . metaRows . push ( {
172- id : `meta_${ req . name } ` , type : 'permission' , name : req . name , state : 'active' ,
173- organization_id : null , metadata : JSON . stringify ( req . item ) ,
182+ id : packageId === null ? `meta_${ req . name } ` : `meta_${ req . name } @${ packageId } ` ,
183+ type : 'permission' , name : req . name , state : 'active' ,
184+ organization_id : null , ...( packageId === null ? { } : { package_id : packageId } ) ,
185+ metadata : JSON . stringify ( req . item ) ,
174186 } ) ;
175187 }
176188 protocol . saves . push ( { ...req } ) ;
@@ -195,6 +207,20 @@ function makeProtocol(ql: any, declared: Record<string, any> = {}) {
195207 overlayScope : overlay ? 'env' : null , effective : overlay ?? code ,
196208 } ;
197209 } ,
210+ getMetaItemCalls : 0 ,
211+ async getMetaItem ( req : { type : string ; name : string } ) {
212+ protocol . getMetaItemCalls += 1 ;
213+ const o = overlayFor ( req . name ) ;
214+ const item = o
215+ ? { ...JSON . parse ( o . metadata ) , ...( o . package_id ? { _packageId : o . package_id } : { } ) }
216+ : declared [ req . name ] ;
217+ if ( item === undefined ) {
218+ const err : any = new Error ( `permission/${ req . name } not found` ) ;
219+ err . status = 404 ;
220+ throw err ;
221+ }
222+ return { type : 'permission' , name : req . name , item } ;
223+ } ,
198224 } ;
199225 return protocol ;
200226}
@@ -1071,6 +1097,111 @@ describe('createPermissionSetWriteThrough (data door → metadata store)', () =>
10711097 } ) ;
10721098} ) ;
10731099
1100+ // ── [#21861] The update leg writes back into the row it edits ──────────────
1101+
1102+ describe ( '[#21861] a data-door UPDATE writes back into the stored row it edits — no package-less fork' , ( ) => {
1103+ const userCtx = { userId : 'usr_admin' } ;
1104+ const PKG = 'com.acme.runtime' ;
1105+
1106+ /** Every active stored row of `name`, as its scope, its binding and the description its body carries. */
1107+ const activeRows = ( ql : any , name : string ) =>
1108+ ql . metaRows
1109+ . filter ( ( r : any ) => r . name === name && r . state === 'active' )
1110+ . map ( ( r : any ) => ( {
1111+ organization_id : r . organization_id ?? null ,
1112+ package_id : r . package_id ?? null ,
1113+ description : JSON . parse ( r . metadata ) . description ,
1114+ } ) ) ;
1115+
1116+ /** A stored set (one row, optionally package-bound) with its projected record. */
1117+ async function storedSet ( packageId ?: string ) {
1118+ const ql = makeQl ( ) ;
1119+ const protocol = makeProtocol ( ql ) ;
1120+ registerPermissionSetProjection ( protocol , { ql } ) ;
1121+ await protocol . saveMetaItem ( {
1122+ type : 'permission' , name : 'support_agent' , item : envBody ( { name : 'support_agent' } ) ,
1123+ ...( packageId ? { packageId } : { } ) ,
1124+ } ) ;
1125+ protocol . saves . length = 0 ;
1126+ return { ql, protocol, rowId : ql . permRows [ 0 ] . id as string } ;
1127+ }
1128+
1129+ it ( 'a set stored in a writable runtime package: the edit lands on that row, which stays bound' , async ( ) => {
1130+ const { ql, protocol, rowId } = await storedSet ( PKG ) ;
1131+ expect ( activeRows ( ql , 'support_agent' ) , 'before' ) . toEqual ( [ { organization_id : null , package_id : PKG , description : undefined } ] ) ;
1132+
1133+ const opCtx : any = {
1134+ object : 'sys_permission_set' , operation : 'update' , context : userCtx ,
1135+ data : { id : rowId , description : 'edited at the data door' } ,
1136+ } ;
1137+ expect ( await run ( makeMiddleware ( ql , protocol ) , opCtx ) ) . toBe ( false ) ;
1138+
1139+ expect ( activeRows ( ql , 'support_agent' ) , 'after: still one row, same binding, carrying the edit' )
1140+ . toEqual ( [ { organization_id : null , package_id : PKG , description : 'edited at the data door' } ] ) ;
1141+ expect ( protocol . saves . map ( ( s : any ) => s . packageId ) , 'the save names the row\'s own package' ) . toEqual ( [ PKG ] ) ;
1142+ expect ( opCtx . result ?. description , 'the projected record follows' ) . toBe ( 'edited at the data door' ) ;
1143+ } ) ;
1144+
1145+ it ( 'a package-less set: the save still names no package, and its one row carries the edit' , async ( ) => {
1146+ const { ql, protocol, rowId } = await storedSet ( ) ;
1147+ expect ( activeRows ( ql , 'support_agent' ) , 'before' ) . toEqual ( [ { organization_id : null , package_id : null , description : undefined } ] ) ;
1148+
1149+ await run ( makeMiddleware ( ql , protocol ) , {
1150+ object : 'sys_permission_set' , operation : 'update' , context : userCtx ,
1151+ data : { id : rowId , description : 'edited' } ,
1152+ } ) ;
1153+
1154+ expect ( protocol . saves ) . toHaveLength ( 1 ) ;
1155+ expect ( protocol . saves [ 0 ] , 'no binding is invented for a set that has none' ) . not . toHaveProperty ( 'packageId' ) ;
1156+ expect ( activeRows ( ql , 'support_agent' ) , 'after' )
1157+ . toEqual ( [ { organization_id : null , package_id : null , description : 'edited' } ] ) ;
1158+ } ) ;
1159+
1160+ it ( 'a filtered edit spanning a package-bound and a package-less set writes each back into its own row' , async ( ) => {
1161+ const ql = makeQl ( ) ;
1162+ const protocol = makeProtocol ( ql ) ;
1163+ registerPermissionSetProjection ( protocol , { ql } ) ;
1164+ await protocol . saveMetaItem ( { type : 'permission' , name : 'pkg_set' , item : envBody ( { name : 'pkg_set' } ) , packageId : PKG } ) ;
1165+ await protocol . saveMetaItem ( { type : 'permission' , name : 'org_set' , item : envBody ( { name : 'org_set' } ) } ) ;
1166+ protocol . saves . length = 0 ;
1167+ const before = { pkg : activeRows ( ql , 'pkg_set' ) , org : activeRows ( ql , 'org_set' ) } ;
1168+ expect ( before ) . toEqual ( {
1169+ pkg : [ { organization_id : null , package_id : PKG , description : undefined } ] ,
1170+ org : [ { organization_id : null , package_id : null , description : undefined } ] ,
1171+ } ) ;
1172+
1173+ await run ( makeMiddleware ( ql , protocol ) , {
1174+ object : 'sys_permission_set' , operation : 'update' , context : userCtx ,
1175+ data : { description : 'bulk' } , options : { where : { name : { $in : [ 'pkg_set' , 'org_set' ] } } } ,
1176+ } ) ;
1177+
1178+ expect ( { pkg : activeRows ( ql , 'pkg_set' ) , org : activeRows ( ql , 'org_set' ) } , 'after' ) . toEqual ( {
1179+ pkg : [ { organization_id : null , package_id : PKG , description : 'bulk' } ] ,
1180+ org : [ { organization_id : null , package_id : null , description : 'bulk' } ] ,
1181+ } ) ;
1182+ expect ( protocol . saves . map ( ( s : any ) => [ s . name , s . packageId ?? null ] ) )
1183+ . toEqual ( [ [ 'pkg_set' , PKG ] , [ 'org_set' , null ] ] ) ;
1184+ } ) ;
1185+
1186+ it ( 'a set a code package ships is still refused by the lock (403 NOT_OVERRIDABLE) before any binding read or save' , async ( ) => {
1187+ const ql = makeQl ( ) ;
1188+ const declaredBody = { ...envBody ( { name : 'crm_rep' } ) , _packageId : 'com.example.crm' } ;
1189+ ( ql as any ) . registry = { listItems : ( t : string ) => ( t === 'permission' ? [ declaredBody ] : [ ] ) } ;
1190+ const protocol = makeProtocol ( ql , { crm_rep : declaredBody } ) ;
1191+ ql . permRows . push ( { id : 'ps_pkg' , name : 'crm_rep' , managed_by : 'package' , package_id : 'com.example.crm' } ) ;
1192+ const before = activeRows ( ql , 'crm_rep' ) ;
1193+
1194+ await expect ( run ( makeMiddleware ( ql , protocol ) , {
1195+ object : 'sys_permission_set' , operation : 'update' , context : userCtx ,
1196+ data : { id : 'ps_pkg' , description : 'customized' } ,
1197+ } ) ) . rejects . toMatchObject ( { code : 'NOT_OVERRIDABLE' , status : 403 } ) ;
1198+
1199+ expect ( activeRows ( ql , 'crm_rep' ) , 'the refused edit stored nothing' ) . toEqual ( before ) ;
1200+ expect ( protocol . saves ) . toHaveLength ( 0 ) ;
1201+ expect ( protocol . getMetaItemCalls , 'the binding is never read for a refused edit' ) . toBe ( 0 ) ;
1202+ } ) ;
1203+ } ) ;
1204+
10741205// ── Boot reconciliation + one-time backfill (ADR-0094 D4) ───────────────────
10751206
10761207describe ( 'reconcilePermissionSetProjection' , ( ) => {
0 commit comments