Successor to the delivered M4 umbrella #138. These are its residual review follow-ups plus two re-homed issues.
Follow-up epic for M4, opened after the eleven M4 cars (#462-#474) and #562 merged into dev/m1 at aec462a. Target: L3 cow-venue / composable-cow / modules, plus one L1 nexum-sdk primitive.
Seven items survive verification against the tree. Six issues were delivered by the cars and close without work, and two are re-homed out of the milestone.
The M5 three-repo split is now redone after these follow-ups land, so no item here carries a pre-cleave urgency argument. The forcing functions that remain are real: one live duplicate-order defect, one wire-format change that must happen before anything is released, and a file-ownership window on the module strategies that closes the moment the next car opens.
Scope
#538 keeper: make journal dedup durable across a post-submit record fault. Reserve/commit/release on the existing local-store verbs, no WIT change. Three sites, not two: the L2 sweep at crates/videre-sdk/src/keeper.rs:105-111, the L3 CoW sweep at crates/composable-cow/src/sweep.rs:133-157, and modules/examples/stop-loss/src/strategy.rs:80-111, which hand-rolls submitted:{id} against host.get/host.set and therefore inherits nothing from a Journal fix. Port stop-loss onto Journal in the same pass. The doc-tightening bullet is already landed by #473 and drops from scope.
#560 cow: make the already-held interpretation explicit at the type level. Split refusal into refusal_for_submit -> Refusal and refusal_for_read -> VenueError so a read path cannot hold an unresolved already-held meaning. Standalone PR against dev/m1; the #473 file lock is gone.
#559 cow: reconcile the submit receipt against the orderbook's stored order. Narrowed to the cheap option: assert the client-side derivation against the server UID on the Posted::Accepted branch at crates/cow-venue/src/adapter.rs:171/:184, plus the rustdoc caveat. Do not widen the status_with signature, and do not implement the review's original guard, which the body already retracts.
#545 cow-venue: reconsider the OrderBuilder typestate. Decided: OrderBuilder::new(sell, buy, valid_to) plus optional setters, dropping PhantomData, the four state markers and the per-state impls. Not the keep-and-document option, and not a Result-returning build.
#546 cow-venue: use alloy primitives instead of byte-array aliases. Supersede the body's ruint-direct plan with alloy-primitives = { features = ["borsh"] }, as #557 proved. The body's "regenerate the goldens if the encoding moves" is not conditional: ruint serializes Uint little-endian (as_le_slice), the current U256 = [u8; 32] fields are big-endian, so sell_amount/buy_amount/fee_amount flip. Both crates/cow-venue/tests/vectors/cow-intent-body.json and cow-header-goldens.json regenerate, and intent_id changes with them, so any live submitted: journal key rotates. Settle the optional = true and no_std questions in the same pass.
#398 (M4 sliver only) cow: model Verdict::Post::next_poll_timestamp as Option<u64> instead of the 0 sentinel. The rest of #398, the wire swap and the LegacyRevertAdapter deletion, is gated on a third-party deployment and re-homes to M8.
#522 examples: adopt the keeper vernacular for the submitter modules. Rescoped to two modules, twap-monitor and stop-loss. ethflow-watcher is now observe-only (modules/ethflow-watcher/src/lib.rs:8, sole venue verb venue.observe at strategy.rs:174), so by the issue's own criterion it joins balance-tracker and price-alert in the exclude column. The body's claim that twap-monitor's header already reads "keeper module" is false; it reads "Pure strategy logic for the twap-monitor module".
Order
Wave 1: #538 and #560 in parallel. Disjoint crates.
Wave 2: #559 and #398-sliver in parallel. #559 follows #560 in adapter.rs; the sliver follows #538 in composable-cow.
Wave 3: #545 alone.
Wave 4: #546 alone, after #545 by maintainer decision so order.rs is reworked once.
Wave 5: #522 alone, last. It renames the two files every earlier wave edits.
Re-homed
nullislabs/videre-nexum-module#12 venue: chain-qualify the venue id, to M6. The body's premise is wrong: crates/videre-host/src/registry.rs:355 already returns DuplicateVenue and nexum-runtime/src/supervisor.rs:933 fails boot on it, so the fallback acceptance criterion is met. The host keys on the adapter's manifest name, not on CowVenue::ID, so two chains can already register distinct ids by manifest. What remains is a Venue::ID and typed-client change, and the second venue is what proves whether it is needed.
#398 (the wire swap), to M8. Blocked on the nullisLabs fork being deployed somewhere.
Closed on verification
#399, #402, #121, #75, #48 are delivered on the tree. #138 closes once #398 is detached and re-milestoned.
Binding decisions, 2026-07-23
These supersede the plan above where they differ.
stop-loss is deleted, not ported. It is subsumed by the generic ComposableCoW monitor, tracked as #568, which lands first so no later wave touches a file about to be removed. Two consequences: #538 drops its stop-loss Journal port and covers only the L2 keeper sweep and the L3 CoW sweep; #522 rescopes to twap-monitor alone, since ethflow-watcher is observe-only and stop-loss is gone. Deleting the module removes the five-touch collision seam the M4 and M3 waves shared.
The #398 sliver is #567 and rides in wave 2 with the #538 pass. The rest of #398, the wire swap and the LegacyRevertAdapter deletion, stays in M4 labelled blocked rather than moving to M8, so the milestone shows the full picture; it cannot close until the fork is deployed. This corrects the plan's re-homing of that arm to M8.
Two unmilestoned issues fold in here on the same adapter seam: #491 (restore or document the dropped empty-receipt validation) beside #559 and #560, and #496 (type the retry classifier boundary with OrderbookApiErrorType) before wave 3, since it is a fifth writer on crates/cow-venue/src/adapter.rs.
#546 keeps the accepted wire break. The encoding moves, both vector files regenerate, intent_id rotates and any in-flight order resubmits once on the first tick after deploy. #538 lands first so that resubmit is absorbed by the reserve/commit path rather than by luck, and nothing may be deployed when it lands.
Revised order
Wave 0: #568, delete stop-loss.
Wave 1: #538 and #560 in parallel.
Wave 2: #559, #491 and #567 (the sliver). #559 and #491 follow #560 in adapter.rs; the sliver follows #538 in composable-cow.
Wave 3: #496 then #545, both on cow-venue. #496 types the classifier boundary, #545 replaces the OrderBuilder typestate.
Wave 4: #546 alone, the wire break.
Wave 5: #522 alone, last, the twap-monitor rename.
Seven items survive verification against the tree. Six issues were delivered by the cars and close without work, and two are re-homed out of the milestone.
The M5 three-repo split is now redone after these follow-ups land, so no item here carries a pre-cleave urgency argument. The forcing functions that remain are real: one live duplicate-order defect, one wire-format change that must happen before anything is released, and a file-ownership window on the module strategies that closes the moment the next car opens.
Scope
#538 keeper: make journal dedup durable across a post-submit record fault. Reserve/commit/release on the existing local-store verbs, no WIT change. Three sites, not two: the L2 sweep at
crates/videre-sdk/src/keeper.rs:105-111, the L3 CoW sweep atcrates/composable-cow/src/sweep.rs:133-157, andmodules/examples/stop-loss/src/strategy.rs:80-111, which hand-rollssubmitted:{id}againsthost.get/host.setand therefore inherits nothing from aJournalfix. Port stop-loss ontoJournalin the same pass. The doc-tightening bullet is already landed by #473 and drops from scope.#560 cow: make the already-held interpretation explicit at the type level. Split
refusalintorefusal_for_submit -> Refusalandrefusal_for_read -> VenueErrorso a read path cannot hold an unresolved already-held meaning. Standalone PR againstdev/m1; the #473 file lock is gone.#559 cow: reconcile the submit receipt against the orderbook's stored order. Narrowed to the cheap option: assert the client-side derivation against the server UID on the
Posted::Acceptedbranch atcrates/cow-venue/src/adapter.rs:171/:184, plus the rustdoc caveat. Do not widen thestatus_withsignature, and do not implement the review's original guard, which the body already retracts.#545 cow-venue: reconsider the OrderBuilder typestate. Decided:
OrderBuilder::new(sell, buy, valid_to)plus optional setters, droppingPhantomData, the four state markers and the per-state impls. Not the keep-and-document option, and not aResult-returning build.#546 cow-venue: use alloy primitives instead of byte-array aliases. Supersede the body's
ruint-direct plan withalloy-primitives = { features = ["borsh"] }, as #557 proved. The body's "regenerate the goldens if the encoding moves" is not conditional: ruint serializesUintlittle-endian (as_le_slice), the currentU256 = [u8; 32]fields are big-endian, sosell_amount/buy_amount/fee_amountflip. Bothcrates/cow-venue/tests/vectors/cow-intent-body.jsonandcow-header-goldens.jsonregenerate, andintent_idchanges with them, so any livesubmitted:journal key rotates. Settle theoptional = trueandno_stdquestions in the same pass.#398 (M4 sliver only) cow: model
Verdict::Post::next_poll_timestampasOption<u64>instead of the0sentinel. The rest of #398, the wire swap and theLegacyRevertAdapterdeletion, is gated on a third-party deployment and re-homes to M8.#522 examples: adopt the keeper vernacular for the submitter modules. Rescoped to two modules,
twap-monitorandstop-loss.ethflow-watcheris now observe-only (modules/ethflow-watcher/src/lib.rs:8, sole venue verbvenue.observeatstrategy.rs:174), so by the issue's own criterion it joinsbalance-trackerandprice-alertin the exclude column. The body's claim that twap-monitor's header already reads "keeper module" is false; it reads "Pure strategy logic for the twap-monitor module".Order
Wave 1: #538 and #560 in parallel. Disjoint crates.
Wave 2: #559 and #398-sliver in parallel. #559 follows #560 in
adapter.rs; the sliver follows #538 incomposable-cow.Wave 3: #545 alone.
Wave 4: #546 alone, after #545 by maintainer decision so
order.rsis reworked once.Wave 5: #522 alone, last. It renames the two files every earlier wave edits.
Re-homed
nullislabs/videre-nexum-module#12 venue: chain-qualify the venue id, to M6. The body's premise is wrong:
crates/videre-host/src/registry.rs:355already returnsDuplicateVenueandnexum-runtime/src/supervisor.rs:933fails boot on it, so the fallback acceptance criterion is met. The host keys on the adapter's manifest name, not onCowVenue::ID, so two chains can already register distinct ids by manifest. What remains is aVenue::IDand typed-client change, and the second venue is what proves whether it is needed.#398 (the wire swap), to M8. Blocked on the nullisLabs fork being deployed somewhere.
Closed on verification
#399, #402, #121, #75, #48 are delivered on the tree. #138 closes once #398 is detached and re-milestoned.
Binding decisions, 2026-07-23
These supersede the plan above where they differ.
stop-loss is deleted, not ported. It is subsumed by the generic ComposableCoW monitor, tracked as #568, which lands first so no later wave touches a file about to be removed. Two consequences: #538 drops its stop-loss
Journalport and covers only the L2 keeper sweep and the L3 CoW sweep; #522 rescopes totwap-monitoralone, sinceethflow-watcheris observe-only and stop-loss is gone. Deleting the module removes the five-touch collision seam the M4 and M3 waves shared.The #398 sliver is #567 and rides in wave 2 with the #538 pass. The rest of #398, the wire swap and the
LegacyRevertAdapterdeletion, stays in M4 labelled blocked rather than moving to M8, so the milestone shows the full picture; it cannot close until the fork is deployed. This corrects the plan's re-homing of that arm to M8.Two unmilestoned issues fold in here on the same adapter seam: #491 (restore or document the dropped empty-receipt validation) beside #559 and #560, and #496 (type the retry classifier boundary with
OrderbookApiErrorType) before wave 3, since it is a fifth writer oncrates/cow-venue/src/adapter.rs.#546 keeps the accepted wire break. The encoding moves, both vector files regenerate,
intent_idrotates and any in-flight order resubmits once on the first tick after deploy. #538 lands first so that resubmit is absorbed by the reserve/commit path rather than by luck, and nothing may be deployed when it lands.Revised order
Wave 0: #568, delete stop-loss.
Wave 1: #538 and #560 in parallel.
Wave 2: #559, #491 and #567 (the sliver). #559 and #491 follow #560 in
adapter.rs; the sliver follows #538 incomposable-cow.Wave 3: #496 then #545, both on
cow-venue. #496 types the classifier boundary, #545 replaces the OrderBuilder typestate.Wave 4: #546 alone, the wire break.
Wave 5: #522 alone, last, the twap-monitor rename.