Found while removing the retired allowRestore / allowPurge columns in objectui#6595. Out of that card's scope (a different defect class — these three keys are live and enforced), so recording only, deliberately unassigned.
What happens
bulkSetObject in packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx REPLACES the object's permission row instead of merging into it:
constnext: ObjectPerm=action==='none' ? {}
: action==='all' ? Object.fromEntries(OBJECT_ACTIONS.map((a)=>[a.key,true]))asObjectPerm
: action==='crud'? {allowCreate: true,allowRead: true,allowEdit: true,allowDelete: true}
: {allowRead: true};return{ ...prev,objects: { ...prev.objects,[objectName]: next}};OBJECT_ACTIONS is the matrix's own column list, and the local ObjectPerm interface in permission-slice.ts is a hand-written SUBSET of the spec's ObjectPermission. Three keys the spec declares are modelled by neither:
allowExportreadScope and writeScope — the ADR-0057 access-depth axis
So one click on any of the four bulk buttons in a row drops whatever those three held. The per-checkbox path is unaffected: updateObjectPerm spreads ({ ...cur, [action]: value }) and preserves them.
Why it persists rather than being a display-only glitch
Both save doors carry the truncated row to storage. At environment scope the whole record is written. At package scope mergePermissionSlice takes in-scope rows entirely from edited by design (ADR-0086 P0), so base cannot restore them.
Why this is worth a card rather than a note
readScope / writeScope are an access-depth axis, so dropping them is not cosmetic — the row silently falls back to default depth. The sharpest shape is the button labelled All: an admin clicking what reads as "grant everything" can WIDEN effective read access by deleting a readScope: own narrowing, with no diff shown and no error. The preview renders all three keys (objectstack#4115 added the Export column and the scope cell precisely so a reviewer could see them), so the reviewing surface shows a value the authoring surface deletes on the next bulk click.
Same class as objectui#4302 — a facet the editor cannot model being silently reverted on save — one level down: that card was about record-level facets, this is about keys inside an object row.
Suggested fix
Merge rather than replace: start bulk results from { ...cur } and set only the keys the matrix actually authors (OBJECT_ACTIONS), so unmodelled keys ride through the way updateObjectPerm already lets them. none needs the same treatment — clearing the grants the matrix owns, not the row. Worth pinning with a test that round-trips a row carrying all three keys through each of the four buttons.
Note for whoever picks this up: allowRestore / allowPurge are NOT part of this — they are retired and were removed by objectui#6595.
Found while removing the retired
allowRestore/allowPurgecolumns in objectui#6595. Out of that card's scope (a different defect class — these three keys are live and enforced), so recording only, deliberately unassigned.What happens
bulkSetObjectinpackages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsxREPLACES the object's permission row instead of merging into it:OBJECT_ACTIONSis the matrix's own column list, and the localObjectPerminterface inpermission-slice.tsis a hand-written SUBSET of the spec'sObjectPermission. Three keys the spec declares are modelled by neither:allowExportreadScopeandwriteScope— the ADR-0057 access-depth axisSo one click on any of the four bulk buttons in a row drops whatever those three held. The per-checkbox path is unaffected:
updateObjectPermspreads ({ ...cur, [action]: value }) and preserves them.Why it persists rather than being a display-only glitch
Both save doors carry the truncated row to storage. At environment scope the whole record is written. At package scope
mergePermissionSlicetakes in-scope rows entirely fromeditedby design (ADR-0086 P0), sobasecannot restore them.Why this is worth a card rather than a note
readScope/writeScopeare an access-depth axis, so dropping them is not cosmetic — the row silently falls back to default depth. The sharpest shape is the button labelled All: an admin clicking what reads as "grant everything" can WIDEN effective read access by deleting areadScope: ownnarrowing, with no diff shown and no error. The preview renders all three keys (objectstack#4115 added the Export column and the scope cell precisely so a reviewer could see them), so the reviewing surface shows a value the authoring surface deletes on the next bulk click.Same class as objectui#4302 — a facet the editor cannot model being silently reverted on save — one level down: that card was about record-level facets, this is about keys inside an object row.
Suggested fix
Merge rather than replace: start bulk results from
{ ...cur }and set only the keys the matrix actually authors (OBJECT_ACTIONS), so unmodelled keys ride through the wayupdateObjectPermalready lets them.noneneeds the same treatment — clearing the grants the matrix owns, not the row. Worth pinning with a test that round-trips a row carrying all three keys through each of the four buttons.Note for whoever picks this up:
allowRestore/allowPurgeare NOT part of this — they are retired and were removed by objectui#6595.