IMigrationRegistryV1 orders migrations within one writer's namespace: applyMigration(expectedHead, migration) refuses unless the namespace is at expectedHead, so a skipped predecessor is a revert. There is no way to say that a migration may only be applied once a migration in another writer's namespace has been applied.
That is the shape of every ordering problem in S01-Issuer/st0x.deploy's current rollout, and each one is enforced today by a script re-deriving the other script's post-state, or by a runbook:
- the orchestrator cutover bundle (Safe writer) must not apply until the fleet upgrade of the receipt and receipt-vault beacons (Safe writer, different script, different day) has applied; the script reads the beacons'
implementation() and reverts FleetNotUpgraded;
- the direct-role retirement must not apply until the cutover has applied and burned in; the script reads four
hasRoles;
- the governance-timelock migration (Safe writer) must not apply until the orchestrator beacon's owner migration (deploy-EOA writer) has applied; nothing enforces it, so the timelock bundle either skips or refuses on the beacon.
Reading the other migration's state is the "bookkeeping vs pins" layering the interface already describes; what is missing is the index half for the cross-writer case, so the applying script can name its prerequisite as a fact the registry checks instead of re-implementing the other script's post-state read.
Ask
An opt-in prerequisite on apply. Something like:
struct Prerequisite { address writer; bytes32 migration; }
/// Reverts `PrerequisiteNotApplied(writer, migration)` for the first entry
/// whose `applied(writer, migration)` is zero. Otherwise identical to
/// `applyMigration`.
function applyMigrationAfter(bytes32 expectedHead, bytes32 migration, Prerequisite[] calldata prerequisites) external;
and the same for applyMigrationHistory. The check is a precondition on the write, not a record: nothing new is stored, in keeping with "MUST NOT record anything about the state a migration produced". Emitting the prerequisites in Migrated (or a sibling event) would let an indexer reconstruct the cross-namespace order without a storage footprint.
A prerequisite is still an index, not proof: it says the other writer recorded its migration, not that the state it produced holds. Consumers keep their pins. What it removes is each dependent script carrying a hand-written copy of its prerequisite's post-state.
IMigrationRegistryV1orders migrations within one writer's namespace:applyMigration(expectedHead, migration)refuses unless the namespace is atexpectedHead, so a skipped predecessor is a revert. There is no way to say that a migration may only be applied once a migration in another writer's namespace has been applied.That is the shape of every ordering problem in S01-Issuer/st0x.deploy's current rollout, and each one is enforced today by a script re-deriving the other script's post-state, or by a runbook:
implementation()and revertsFleetNotUpgraded;hasRoles;Reading the other migration's state is the "bookkeeping vs pins" layering the interface already describes; what is missing is the index half for the cross-writer case, so the applying script can name its prerequisite as a fact the registry checks instead of re-implementing the other script's post-state read.
Ask
An opt-in prerequisite on apply. Something like:
and the same for
applyMigrationHistory. The check is a precondition on the write, not a record: nothing new is stored, in keeping with "MUST NOT record anything about the state a migration produced". Emitting the prerequisites inMigrated(or a sibling event) would let an indexer reconstruct the cross-namespace order without a storage footprint.A prerequisite is still an index, not proof: it says the other writer recorded its migration, not that the state it produced holds. Consumers keep their pins. What it removes is each dependent script carrying a hand-written copy of its prerequisite's post-state.