Found while implementing #6563. Recording only, per Prime Directive #10 — a different cause (a missing call) on a different line than the write intent #6563 fixes.
Evidence
rollbackMetaItem — the per-item revert — ends its restore with a registry write-through, and #4521 states why in that file:
// #4521 — a rollback is a live write like any other: the restored
// body must be the one the runtime dispatches on immediately, not
// after someone lists the type.
this.applyRegistryWriteThrough({
type: singularType, name: request.name,
item: result.item.body, packageId: rollbackPackageId,
});
revertCommit — the batch revert over the same repository call — has no equivalent. Its restore limb awaits repo.restoreVersion(...), pushes { action: 'restored' } and moves on; nothing re-registers the restored body.
Measured on #6563's branch (where an object can reach the restore at all — on origin/main @ e39dd66 it fails earlier with NOT_OVERRIDABLE), real SysMetadataRepository over the #6215 in-memory double: an object saved twice through saveMetaItem (v2 adds a due_date field), then reverted through revertCommit:
revertCommit -> { success: true, revertedCount: 1, failed: [] }
stored sys_metadata row fields -> ["name","amount"] # reverted
SchemaRegistry.getObject(...) fields -> [... ,"name","amount","due_date"] # NOT reverted
Ownership survives (getObjectOwner(...).packageId === 'app.myapp', _provenance: 'org'), so this is purely the body going stale.
This is type-agnostic and pre-dates #6563: an overlay-allowed type (view, page, dashboard, …) has always been able to reach this limb and has always come back with the same split — #6215's pins assert the stored row and are silent on the registry. object merely makes it loud, because the registry copy is what CRUD dispatches on.
Why it matters
revertCommit answers success: true while the runtime keeps serving the body the operator just reverted away — a revert that looks done and is not, healing only at the next restart or re-hydration. That is exactly the failure mode #4521 named for the single-item path, one seam over. rollbackToPackageCommit reverts through the same loop, so a whole package rollback can report success and change nothing the running process can see.
Suggested direction (not a ruling)
Mirror rollbackMetaItem inside revertCommit's loop: use the restoreVersion result's body plus the row's own ownership key (resolveOverlayPackageBinding, per item, read the way #4636 requires so the re-register does not clash with already owned by package ...). The soft-remove limb needs the symmetric decision made deliberately rather than by omission — whether an unregister belongs there is a real question, not an obvious yes.
Worth deciding together with #6602 (which gates write-through on environmentId and never on org): a batch revert of org-scoped rows would inherit whatever that issue rules.
Related: #6563, #4521, #4636, #6215, #6602, ADR-0067.
Found while implementing #6563. Recording only, per Prime Directive #10 — a different cause (a missing call) on a different line than the write intent #6563 fixes.
Evidence
rollbackMetaItem— the per-item revert — ends its restore with a registry write-through, and #4521 states why in that file:revertCommit— the batch revert over the same repository call — has no equivalent. Its restore limb awaitsrepo.restoreVersion(...), pushes{ action: 'restored' }and moves on; nothing re-registers the restored body.Measured on #6563's branch (where an
objectcan reach the restore at all — onorigin/main@ e39dd66 it fails earlier withNOT_OVERRIDABLE), realSysMetadataRepositoryover the #6215 in-memory double: an object saved twice throughsaveMetaItem(v2 adds adue_datefield), then reverted throughrevertCommit:Ownership survives (
getObjectOwner(...).packageId === 'app.myapp',_provenance: 'org'), so this is purely the body going stale.This is type-agnostic and pre-dates #6563: an overlay-allowed type (
view,page,dashboard, …) has always been able to reach this limb and has always come back with the same split — #6215's pins assert the stored row and are silent on the registry.objectmerely makes it loud, because the registry copy is what CRUD dispatches on.Why it matters
revertCommitanswerssuccess: truewhile the runtime keeps serving the body the operator just reverted away — a revert that looks done and is not, healing only at the next restart or re-hydration. That is exactly the failure mode #4521 named for the single-item path, one seam over.rollbackToPackageCommitreverts through the same loop, so a whole package rollback can report success and change nothing the running process can see.Suggested direction (not a ruling)
Mirror
rollbackMetaIteminsiderevertCommit's loop: use therestoreVersionresult's body plus the row's own ownership key (resolveOverlayPackageBinding, per item, read the way #4636 requires so the re-register does not clash withalready owned by package ...). The soft-remove limb needs the symmetric decision made deliberately rather than by omission — whether an unregister belongs there is a real question, not an obvious yes.Worth deciding together with #6602 (which gates write-through on
environmentIdand never on org): a batch revert of org-scoped rows would inherit whatever that issue rules.Related: #6563, #4521, #4636, #6215, #6602, ADR-0067.