---
wave: 1
depends_on: []
files_modified:
  - crates/opendeviationbar-streaming/src/live_engine.rs
requirements:
  - JUNC-01
  - JUNC-02
  - JUNC-03
  - JUNC-04
autonomous: true
---

# Plan 01: Junction Fill Implementation

## Goal

Insert a junction fill phase in `symbol_task()` between the gap detector seeding (line ~1277) and the main `loop { select! { ... } }` (line ~1285). This eliminates forming-bar abandonment at the REST-to-WS transition for all 18 symbols x 4 thresholds.

## Tasks

<task id="01-01">
<name>Add junction fill between gap detector seed and main loop</name>
<read_first>
- crates/opendeviationbar-streaming/src/live_engine.rs (lines 1214-1450, full symbol_task function)
- crates/opendeviationbar-streaming/src/gap/detector.rs (seed, check, update_last_tid API)
</read_first>
<action>
In `symbol_task()` in `crates/opendeviationbar-streaming/src/live_engine.rs`, insert a junction fill block AFTER the `let mut last_days` initialization (line 1282) and BEFORE the `loop {` on line 1285. The block must appear between these two lines because `process_trade_through_processors` requires `last_days`, and the block must run before the main select loop.

The junction fill block does the following in order:

1. Receive the first WS trade from `trade_rx` (blocking `recv().await`). If `None`, log and return early (channel closed before any trade).

2. Compute junction gap: get `last_rest_tid` from `processors.first().and_then(|(_, p)| p.last_agg_trade_id())`. If `last_rest_tid` is `Some` AND `first_ws_trade.agg_trade_id > last_rest_tid + 1`, there is a junction gap to fill.

3. When junction gap exists: call `puck_fill()` with the same processor vec (JUNC-02: single-processor continuity). Arguments: `&symbol`, `last_rest_tid` (as `last_known_id`), `first_ws_trade.agg_trade_id` (as `first_ws_id`), `&mut processors`, `&bar_buffer`, `&metrics`, `&shutdown`, `rate_limiter.clone()`, `&forming_tx_map`. Log the junction gap size before calling.

4. After junction fill (whether gap existed or not): re-seed the gap detector with `first_ws_trade.agg_trade_id` via `gap_detector.seed(first_ws_trade.agg_trade_id)` (JUNC-03: re-seed past junction so Puck won't fire on this range in the main loop).

5. MONOTONICITY GUARD (JUNC-04): Before processing the first WS trade, check if `first_ws_trade.agg_trade_id <= processors.first().and_then(|(_, p)| p.last_agg_trade_id()).unwrap_or(-1)`. If so, silently drop it (log at debug level: "dropping overlapping WS trade at junction") and do NOT process. Otherwise, process the first WS trade via `process_trade_through_processors()` and `gap_detector.update_last_tid()`.

The concrete code to insert (approximately 30 lines):

```rust
// --- JUNCTION FILL: Bridge REST→WS gap (Issue #XXX) ---
// Peek first WS trade to determine if a junction gap exists.
// This runs ONCE at startup, before the main select loop.
let first_ws_trade = match trade_rx.recv().await {
    Some(t) => t,
    None => {
        tracing::warn!(%symbol, "WS channel closed before first trade, exiting symbol_task");
        return;
    }
};

// JUNC-01/JUNC-02: Fill junction gap using the SAME processors (preserves forming bar)
let last_rest_tid = processors.first().and_then(|(_, p)| p.last_agg_trade_id());
if let Some(rest_tid) = last_rest_tid {
    let junction_gap = first_ws_trade.agg_trade_id - rest_tid - 1;
    if junction_gap > 0 {
        tracing::info!(
            %symbol,
            junction_gap,
            rest_last_tid = rest_tid,
            ws_first_tid = first_ws_trade.agg_trade_id,
            "junction fill: bridging REST→WS gap"
        );
        let _recovered = puck_fill(
            &symbol,
            rest_tid,
            first_ws_trade.agg_trade_id,
            &mut processors,
            &bar_buffer,
            &metrics,
            &shutdown,
            rate_limiter.clone(),
            &forming_tx_map,
        )
        .await;
    }
}

// JUNC-03: Re-seed gap detector past junction — Puck won't fire on this range
gap_detector.seed(first_ws_trade.agg_trade_id);

// JUNC-04: Monotonicity guard — drop WS trade if already processed during junction fill
let current_last_tid = processors.first().and_then(|(_, p)| p.last_agg_trade_id()).unwrap_or(-1);
if first_ws_trade.agg_trade_id > current_last_tid {
    process_trade_through_processors(
        &first_ws_trade,
        &mut processors,
        &mut last_days,
        &bar_buffer,
        &metrics,
        &symbol_arc,
        &symbol,
        &forming_tx_map,
    );
    gap_detector.update_last_tid(first_ws_trade.agg_trade_id);
} else {
    tracing::debug!(
        %symbol,
        ws_tid = first_ws_trade.agg_trade_id,
        current_last_tid,
        "dropping overlapping WS trade at junction"
    );
}
// --- END JUNCTION FILL ---
```

Note: The `let mut last_days` initialization (line 1282) must remain BEFORE this junction fill block since `process_trade_through_processors` uses `last_days`. The block is inserted between `let mut last_days` and the `// Process trades → bars` / `loop {` block.
</action>
<acceptance_criteria>

- `grep -n "junction fill: bridging REST" crates/opendeviationbar-streaming/src/live_engine.rs` returns exactly 1 match
- `grep -n "JUNC-03: Re-seed gap detector past junction" crates/opendeviationbar-streaming/src/live_engine.rs` returns exactly 1 match
- `grep -n "JUNC-04: Monotonicity guard" crates/opendeviationbar-streaming/src/live_engine.rs` returns exactly 1 match
- `grep -n "dropping overlapping WS trade at junction" crates/opendeviationbar-streaming/src/live_engine.rs` returns exactly 1 match
- `grep -c "puck_fill" crates/opendeviationbar-streaming/src/live_engine.rs` returns 4 (definition, original in main loop, on-demand gap-fill, and junction fill)
- `grep -n "gap_detector.seed(first_ws_trade.agg_trade_id)" crates/opendeviationbar-streaming/src/live_engine.rs` returns exactly 1 match
- `grep -n "let mut last_days\|JUNCTION FILL\|loop {" crates/opendeviationbar-streaming/src/live_engine.rs` shows `let mut last_days` line < `JUNCTION FILL` line < `loop {` line (verifies correct insertion order)
- `cargo check -p opendeviationbar-streaming` succeeds with zero errors
  </acceptance_criteria>
  </task>

## Verification

```bash
# Build check (must pass)
cargo check -p opendeviationbar-streaming

# Verify junction fill is placed correctly (between last_days init and main loop)
grep -n "junction fill\|JUNC-0[1234]\|puck_fill\|gap_detector.seed" \
  crates/opendeviationbar-streaming/src/live_engine.rs
```

## must_haves

- [ ] Junction fill calls `puck_fill()` with the SAME `&mut processors` (single-processor continuity — JUNC-02)
- [ ] Gap detector re-seeded with `first_ws_trade.agg_trade_id` AFTER junction fill, BEFORE main loop (JUNC-03)
- [ ] Overlapping WS trades silently dropped via trade-ID comparison (JUNC-04)
- [ ] Junction fill runs exactly ONCE at startup, before the main `loop { select! {} }` (JUNC-01)
- [ ] `cargo check -p opendeviationbar-streaming` passes
