The ops2 to i3 warehouse event stream · put-away, pick, pack, move, bin adjust, cycle count · with the idempotency key, the replay control and the per-event-type migration state that makes the Phase 5 cutover impossible to double count
2 bridge events failed HMAC verification in the last hour. Both were stored with hmac_verified=false and status=rejected, never discarded, so the incident is reconstructable. Likely a shared-secret rotation overlap; the previous key is accepted for 60 minutes.
13 of 24 event types are still dual-write. ops2 owns their ledger, i3 shadows. Replay is refused for those types and the cutover counter is at day 4 of 7. GATE 5 needs every type in i3_owns or ops2_retired.Location register →
3 events selected·replay re-runs the classifier with the current ruleset; ledger writes are unique on (source_system, idempotency_key) so a replay adds nothing·approval required above 10 events
Migration state per event type
The Phase 5 cutover plan, held as data in iwms_event_types · exactly one of ops2 and i3 writes the ledger for any type at any instant, enforced by a check constraint, not by convention
8 i3 owns13 dual-write2 shadow1 not started
Event type
Family
Posts ledger
From type
To type
State
ops2 writes
i3 writes
Shadow since
Days clean
Today
Note
carton.created
carton
registry only
·
·
i3_owns
no
yes
21-06
38
93
GRN owns the stock recognition
carton.moved
carton
conditional
any
same
i3_owns
no
yes
21-06
38
62
ledger pair only when the location changes
carton.split
carton
conditional
any
same
i3_owns
no
yes
21-06
38
11
same-location split writes nothing
carton.opened
picking
yes · pair
STOCK
STOCK
i3_owns
no
yes
21-06
38
6
debit carton lines, credit picking
zone.moved
zone
conditional
any
any
i3_owns
no
yes
21-06
38
62
writes only on a stock-type change
qc.queued
qc
registry only
·
·
i3_owns
no
yes
28-06
31
11
soft flag for the QC queue
count.scan
count
registry only
·
·
i3_owns
no
yes
28-06
31
18
module 10 owns the variance
count.disturbance
count
registry only
·
·
i3_owns
no
yes
28-06
31
2
flags a count as unreliable
picking.replenish_out
picking
yes · pair
STOCK
STOCK
dual_write
yes
no
15-07
4
24
needs 7 clean days
picking.adjust
picking
yes
STOCK
varies
dual_write
yes
no
15-07
4
3
damage, correction, found, moved out
qc.accept
qc
yes
RNI
STOCK
dual_write
yes
no
15-07
4
7
-
qc.reject_local
qc
yes
RNI
QUARANTINE
dual_write
yes
no
15-07
4
3
ops2 bug: bypassed the backbone from 19-04
qc.reject_supplier
qc
yes
RNI
RTS_PENDING
dual_write
yes
no
15-07
4
1
-
qc.repair_local
qc
yes
RNI
QUARANTINE
dual_write
yes
no
15-07
4
2
ops2 sent REPAIRABLE, backbone rejects it
qc.repair_supplier
qc
yes
RNI
RTS_PENDING
dual_write
yes
no
15-07
4
0
-
transfer.stage
transfer
registry only
·
·
dual_write
yes
no
18-07
4
12
module 07 owns the state machine
transfer.ship
transfer
yes · 2N
STOCK
IN_TRANSIT_RETURN
dual_write
yes
no
18-07
4
22
posts at the TRANSIT location, not the destination
transfer.deliver
transfer
yes · 2N
IN_TRANSIT_RETURN
STOCK
dual_write
yes
no
18-07
4
16
-
rts.stage
rts
yes
STOCK
RTS_PENDING
dual_write
yes
no
20-07
4
6
-
rts.ship
rts
yes
RTS_PENDING
RTS_PENDING
dual_write
yes
no
20-07
4
4
moves to SUPPLIER_HOLD location
rts.resolve
rts
yes
RTS_PENDING
STOCK or WO
dual_write
yes
no
20-07
4
3
credit or write-off, finance approves
repack.session
repack
recipe_transform
STOCK
STOCK
shadow
yes
no
26-07
3
9
bulk to retail composition plus spoilage
picking.remove_to_picking
picking
yes · pair
STOCK
STOCK
shadow
never
no
27-07
0
12
ops2 never wrote a ledger row for this at all, see below
label.reprint
label
registry only
·
·
not_started
yes
no
-
·
0
no events in 24h, harmless
packmat.consumed
repack
registry only
·
·
ops2_retired
no
yes
02-06
57
24
packaging material, not stock
24 registered event types · ops2 writes and i3 writes can never both be yes on one row, because ck_evt_single_writer makes that state unrepresentable. That is the whole double-count defence, and it lives in the schema rather than in a runbook.
Cutover burn-down
Event types by migration state, week by week · a type only moves right after 7 consecutive clean shadow days · GATE 5 closes when the two left bands are empty
13 still owned by ops2
02-0609-0616-0623-0630-0607-0714-07today
Not startedShadowDual write, ops2 owns the ledgeri3 ownsops2 retiredProgress stalled at 7 owned for three weeks: the QC, transfer and RTS families all entered dual-write together and share one 7-day clock
Topic health
12 topics subscribed, all HMAC verifiedhealthy stale idle failing
carton/created
1m ago93
carton/split
4m ago11
carton/opened
1h ago6
location/moved
1m ago62
picking/replenish
12m ago24
picking/adjust
2h ago3
qc/outcome
6m ago13
transfer/confirmed
30s ago28
transfer/bridge
2m ago22
rts/created
12m ago13
repack/session
8m ago9
count/scan
3m ago18
packmat/consumed
14m ago24
awb/printed
3h ago4
label/reprint
over 24h0
state/reset
over 24h0
inflow/count
1h ago31
carton/merge
5h ago2
Events per minute, last 60 minutes
Two-minute buckets · dispatch batches and put-away waves are visible as spikes
7 per minute
13:4414:1414:42
Latency by family
p5098ms
p95210ms
p99388ms
carton
p95 142ms
zone
p95 96ms
picking
p95 188ms
qc
p95 204ms
transfer
p95 246ms
repack
p95 388ms
Repack is the slow family because one session fans out into a component transform group. SLA is 500ms for the receiver acknowledgement, which is measured separately from classification.
Bridge control
State
running
Ack cursor
ops2 outbox #884,217
Last ack
14:41:08 · 0 lag
HMAC key
key_2026_07 · rotated 12-07
Overlap window
60 min for the previous key
Retention
ops2 holds until acked
Pausing stops the ack cursor advancing. ops2 retains outbox rows until acked, so the backfill on resume is real rather than a promise. The old subscriber-list emit dropped any event whose subscriber was not registered at that moment.
Event stream
Newest first · the idempotency key column is what makes replay and dedup safe · a registry-only event correctly produces zero ledger rows
418 today1 unmatched2 rejected
Time
Topic
Reference
Location · zone
Idempotency key
Ledger
Rule
HMAC
State
ms
14:41:12
carton/created
CTN-2048
HQ_319 · RECEIVING
iwms:carton_created:CTN-2048
0
carton_single_sku · 0.99
ok
processed
142
⋯
14:38:04
transfer/confirmed
TRF-4412
HQ_319 → MCC
ops2:transfer:TRF-4412:ship
4
transfer_confirm_qr · 0.97
ok
dual_write · i3 shadow
188
⋯
14:36:41
location/moved
MC-4470
319_A3 → 319_A1
iwms:carton_moved:884211
0
zone_move_same_type · 0.99
ok
processed · registry only
96
⋯
14:32:08
picking/replenish
REPL-0441
319_PICKING → PACKOUT
iwms:replen:0441
2
pick_replenish_out · 0.94
ok
dual_write · i3 shadow
188
⋯
14:28:55
carton/opened
MC-4471
319_A3 → 319_PICKING
iwms:opened:MC-4471
2
carton_open_debit · 0.99
ok
processed
164
⋯
14:22:19
qc/outcome
CTN-2041
HQ_319 · QC_HOLD
iwms:qc:S-1188:CTN-2041
2
qc_outcome_accept · 0.98
ok
dual_write · i3 shadow
204
⋯
14:14:00
carton/split
CTN-2044
319_A2 · C04
iwms:split:CTN-2044:0003
0
unmatched · needs classification
ok
drift audit opened
388
⋯
14:08:32
rts/created
RTS-8821
HQ_319 · RTS_STAGING
iwms:rts:8821:stage
2
rts_auto_create · 0.89
ok
dual_write · i3 shadow
204
⋯
14:02:11
carton/created
CTN-2043
HQ_319 · RECEIVING
iwms:carton_created:CTN-2043
0
not classified
failed · key_2026_06
rejected · stored
12
⋯
13:48:55
repack/session
RPK-318
HQ_319 · 319_PICKING
iwms:repack:318
6
repack_spoilage · 0.86
ok
shadow only
388
⋯
13:41:02
count/scan
CC-1108
HQ_319 · all zones
iwms:count:1108:scan:4411
0
count_scan · 0.99
ok
processed · registry only
88
⋯
13:32:44
location/moved
MC-4462
319_A2 · C04
iwms:carton_moved:884198
0
zone_move_same_type · 0.99
ok
suppressed duplicate
8
⋯
13:22:11
carton/split
SPL-0212
319_A2 → 319_A3
iwms:split:MC-4462:0007
0
split_same_location · 0.99
ok
processed · registry only
118
⋯
13:05:38
picking/adjust
PADJ-0119
319_PICKING
iwms:pickadj:0119
1
picking_adjust_damage · 0.91
ok
dual_write · i3 shadow
174
⋯
12:58:04
packmat/consumed
PKM-8842
HQ_319 · PACKOUT
iwms:packmat:8842
0
packmat_consumed · 0.99
ok
i3 owns · ops2 retired
72
⋯
09:40:21
carton/merge
MRG-0044
319_A1 · A08
iwms:merge:MC-4401:MC-4402
0
carton_merge · 0.95
ok
dual_write · i3 shadow
156
⋯
168 of today's 418 events posted a ledger row; the other 250 were registry-only, which is correct. Under the ops2 bridge every one of the 418 would have written an inventory_ledger row, including 250 with quantity = 0 and the self-cancelling split pairs.
Why replay is safe, and when it is refused
Every ledger write carries a composite unique index on (source_system, idempotency_key). A replay re-runs the classifier with the current ruleset and attempts the same inserts; the duplicates are rejected by the database, not by an application-level check-then-act.
That distinction matters. ops2's bridge guards idempotency with a SELECT ... WHERE reference_type='iwms' AND reference_id=? inside the same transaction as the insert, which is a race under concurrency and a silent double-post when it loses.
Replay is refused for any event type in dual_write or not_started, because ops2 still owns that type's ledger and a replay would post into a ledger i3 does not own. The refusal is a 409 with the event type's current state in the body, not a silent no-op.
Bulk replay above 10 events writes an approval row naming the approver before anything runs. Today's two replays produced 0 new ledger rows, which is the acceptance test for GATE 5 criterion 9: replaying the last 24 hours must change nothing.
The event type ops2 never wrote a ledger row for
picking.remove_to_picking is highlighted in the migration table with never in the ops2 column, and that is not a typo.
In ops2, partial removal to the picking area calls record_movement(event_type="remove_to_picking"). That string is absent from the valid-event frozenset, so the call raises immediately. The caller catches it with a bare handler whose comment reads "movement logging is audit-only; swallow so the stock move itself is the authoritative record".
The carton is decremented. The picking row is incremented. No movement row is created, so the ledger bridge never runs, so no ledger row exists. Every partial replenishment since that feature shipped is invisible to the quantity ledger. The swallow was written to protect an audit write and it is eating a stock write.
i3's fix is structural rather than a patch: carton_events.event_type is a foreign key to this registry, so an unregistered type is a constraint violation at insert time, and the carton event plus its ledger row are written in the same transaction or neither is. There is no audit-only path to swallow.
The type sits in shadow with 0 clean days because there is nothing on the ops2 side to compare against. It goes straight to i3_owns once the historical gap is quantified by the reconciler in T-04.16.