---
phase: 01-dedup-floor-fix
plan: 02
type: execute
wave: 2
depends_on: [01-01]
files_modified:
  - crates/opendeviationbar-streaming/src/live_engine.rs
  - src/core_bindings.rs
  - src/helpers.rs
autonomous: true
requirements: [RUST-01, RUST-02]

must_haves:
  truths:
    - "committed_floors initialization uses processor.last_completed_bar_tid() instead of processor.last_agg_trade_id()"
    - "After fill_from_rest completes and WS trades resume, no bars are suppressed by the dedup floor at the junction"
    - "PyO3 binding exposes last_completed_bar_tid() to Python"
    - "Checkpoint dict round-trip preserves last_completed_bar_tid through Python"
    - "All cargo nextest and pytest suites pass with zero failures"
  artifacts:
    - path: "crates/opendeviationbar-streaming/src/live_engine.rs"
      provides: "Fixed committed_floors initialization"
      contains: "last_completed_bar_tid"
    - path: "src/core_bindings.rs"
      provides: "PyO3 accessor for last_completed_bar_tid"
      contains: "last_completed_bar_tid"
    - path: "src/helpers.rs"
      provides: "Checkpoint dict serialization for last_completed_bar_tid"
      contains: "last_completed_bar_tid"
  key_links:
    - from: "live_engine.rs symbol_task()"
      to: "processor.last_completed_bar_tid()"
      via: "committed_floors initialization"
      pattern: "last_completed_bar_tid"
    - from: "core_bindings.rs PyOpenDeviationBarProcessor"
      to: "processor.last_completed_bar_tid()"
      via: "PyO3 method delegation"
      pattern: "fn last_completed_bar_tid"
    - from: "helpers.rs checkpoint_to_dict()"
      to: "checkpoint.last_completed_bar_tid"
      via: "dict serialization"
      pattern: "last_completed_bar_tid"
    - from: "helpers.rs dict_to_checkpoint()"
      to: "checkpoint.last_completed_bar_tid"
      via: "dict deserialization"
      pattern: "last_completed_bar_tid"
---

<objective>
Wire `last_completed_bar_tid()` into `committed_floors` initialization in live_engine.rs, add PyO3 bindings, and verify zero regression across the full test suite.

Purpose: This completes RUST-01 (floor reads from completed bar, not forming tail) and provides the integration evidence for RUST-02 (no bars suppressed at junction). The live_engine change is the ONE line that fixes the root cause.

Output: Fixed live_engine.rs, PyO3 bindings, checkpoint Python serialization, full test suite green.
</objective>

<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-dedup-floor-fix/01-CONTEXT.md
@.planning/phases/01-dedup-floor-fix/01-01-SUMMARY.md

<interfaces>
<!-- Key types and contracts from Plan 01 output + existing code -->

From Plan 01 (new accessor in processor.rs):

```rust
/// Get the last COMPLETED bar's aggregate trade ID.
pub fn last_completed_bar_tid(&self) -> Option<i64> {
    self.last_completed_bar_tid
}
```

From crates/opendeviationbar-streaming/src/live_engine.rs (lines 1373-1386, THE BUG):

```rust
// Rust-side dedup: per-threshold committed floor.
let mut committed_floors: HashMap<u32, i64> = HashMap::new();
for (threshold, processor) in processors.iter() {
    let proc_tid = processor.last_agg_trade_id().unwrap_or(-1);  // BUG: includes forming bar tail
    let ch_tid = ch_seed_tid.unwrap_or(-1);
    let floor = proc_tid.max(ch_tid);
    if floor > 0 {
        committed_floors.insert(*threshold, floor);
        tracing::info!(%symbol, threshold, floor, proc_tid, ch_tid, "rust-dedup: floor initialized");
    }
}
```

From src/core_bindings.rs (lines 615-619, existing accessor pattern):

```rust
/// Get last processed aggregate trade ID (for gap detection on reconnect).
fn last_agg_trade_id(&self) -> Option<i64> {
    self.processor.last_agg_trade_id()
}
```

From src/helpers.rs checkpoint_to_dict() (line 344-345, existing pattern):

```rust
// Issue #46: Persist defer_open state for cross-session continuity
dict.set_item("defer_open", checkpoint.defer_open)?;
```

From src/helpers.rs dict_to_checkpoint() (line 449-453, existing pattern):

```rust
// Issue #46: Extract defer_open flag from checkpoint dict
let defer_open: bool = dict
    .get_item("defer_open")?
    .and_then(|v| v.extract().ok())
    .unwrap_or(false);
```

From src/helpers.rs dict_to_checkpoint() return (line 461-473):

```rust
Ok(Checkpoint {
    version,
    symbol,
    threshold_decimal_bps,
    incomplete_bar,
    thresholds,
    last_timestamp_us,
    last_trade_id,
    price_hash,
    anomaly_summary,
    prevent_same_timestamp_close,
    defer_open,
})
```

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Wire last_completed_bar_tid into committed_floors and add bindings</name>
  <files>
    crates/opendeviationbar-streaming/src/live_engine.rs,
    src/core_bindings.rs,
    src/helpers.rs
  </files>
  <read_first>
    crates/opendeviationbar-streaming/src/live_engine.rs,
    src/core_bindings.rs,
    src/helpers.rs,
    crates/opendeviationbar-core/src/processor.rs
  </read_first>
  <action>
    **In live_engine.rs (THE FIX):**

    1. Change line 1379 in `symbol_task()` from:
       ```rust
       let proc_tid = processor.last_agg_trade_id().unwrap_or(-1);
       ```
       to:
       ```rust
       let proc_tid = processor.last_completed_bar_tid().unwrap_or(-1);
       ```

    2. Update the comment block above (lines 1373-1376) from:
       ```rust
       // Rust-side dedup: per-threshold committed floor.
       // Bars with last_agg_trade_id <= floor are already in ClickHouse — skip emission.
       // Floor is initialized from max(processor.last_agg_trade_id, ch_seed_tid) per threshold.
       // After each bar emission, floor is updated to the emitted bar's last_agg_trade_id.
       ```
       to:
       ```rust
       // Rust-side dedup: per-threshold committed floor.
       // Bars with last_agg_trade_id <= floor are already in ClickHouse — skip emission.
       // Floor is initialized from max(processor.last_completed_bar_tid, ch_seed_tid) per threshold.
       // v1.4: Uses last_completed_bar_tid (not last_agg_trade_id) to avoid suppressing
       // junction bars whose trade IDs overlap with the forming bar tail.
       // After each bar emission, floor is updated to the emitted bar's last_agg_trade_id.
       ```

    3. Update the tracing::info! on line 1384 from:
       ```rust
       tracing::info!(%symbol, threshold, floor, proc_tid, ch_tid, "rust-dedup: floor initialized");
       ```
       to:
       ```rust
       tracing::info!(%symbol, threshold, floor, completed_tid = proc_tid, ch_tid, "rust-dedup: floor initialized (v1.4: completed bar tid)");
       ```

    **In core_bindings.rs (PyO3 binding):**

    4. Add new method to `PyOpenDeviationBarProcessor` impl block, after the `last_agg_trade_id()` method (line ~619):
       ```rust
       /// Get last COMPLETED bar's aggregate trade ID.
       ///
       /// Unlike last_agg_trade_id() which includes the forming bar tail,
       /// this returns the trade ID from the most recently completed or orphaned bar.
       /// Used by committed_floors to avoid suppressing junction bars.
       ///
       /// Returns None if no bar has been completed yet.
       fn last_completed_bar_tid(&self) -> Option<i64> {
           self.processor.last_completed_bar_tid()
       }
       ```

    **In helpers.rs (checkpoint Python serialization):**

    5. In `checkpoint_to_dict()`, after the `defer_open` line (line ~345):
       ```rust
       // v1.4: Last completed bar trade ID for dedup floor
       dict.set_item("last_completed_bar_tid", checkpoint.last_completed_bar_tid)?;
       ```

    6. In `dict_to_checkpoint()`, after the `defer_open` extraction (line ~453):
       ```rust
       // v1.4: Extract last_completed_bar_tid (defaults to None for old checkpoints)
       let last_completed_bar_tid: Option<i64> = dict
           .get_item("last_completed_bar_tid")?
           .and_then(|v| if v.is_none() { None } else { v.extract().ok() });
       ```

    7. In `dict_to_checkpoint()` return struct (line ~461), after `defer_open,`:
       ```rust
       last_completed_bar_tid,
       ```

  </action>
  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && cargo nextest run --workspace --exclude opendeviationbar-py --no-fail-fast 2>&1 | tail -20</automated>
  </verify>
  <acceptance_criteria>
    - live_engine.rs line 1379 contains `processor.last_completed_bar_tid()` NOT `processor.last_agg_trade_id()`
    - live_engine.rs comment mentions "last_completed_bar_tid" and "v1.4"
    - core_bindings.rs contains `fn last_completed_bar_tid(&self) -> Option<i64>`
    - helpers.rs checkpoint_to_dict() contains `dict.set_item("last_completed_bar_tid", checkpoint.last_completed_bar_tid)`
    - helpers.rs dict_to_checkpoint() extracts `last_completed_bar_tid` from dict
    - helpers.rs dict_to_checkpoint() return struct includes `last_completed_bar_tid,`
    - `cargo nextest run --workspace --exclude opendeviationbar-py` exits 0
  </acceptance_criteria>
  <done>
    committed_floors initialization uses last_completed_bar_tid() instead of last_agg_trade_id().
    PyO3 binding exposes last_completed_bar_tid to Python.
    Checkpoint Python serialization handles last_completed_bar_tid.
    All Rust workspace tests pass.
  </done>
</task>

<task type="auto">
  <name>Task 2: Full regression -- cargo nextest + pytest</name>
  <files>
    (no files modified -- verification only)
  </files>
  <read_first>
    pyproject.toml
  </read_first>
  <action>
    Run the full quality gate to verify zero regression:

    1. Build the Python wheel with the Rust changes:
       ```bash
       cd /Users/terryli/eon/opendeviationbar-py
       maturin develop --release 2>&1
       ```

    2. Run the full Rust test suite (all workspace crates):
       ```bash
       cargo nextest run --workspace --exclude opendeviationbar-py --no-fail-fast 2>&1
       ```

    3. Run the Python test suite:
       ```bash
       uv run --python 3.13 pytest tests/ -x -q 2>&1
       ```

    4. Verify the fix is logically correct by checking the key invariant:
       - `last_completed_bar_tid()` is strictly less than or equal to `last_agg_trade_id()` at all times
       - The forming bar tail (trades processed into an incomplete bar) only affects `last_agg_trade_id()`, not `last_completed_bar_tid()`
       - Therefore `committed_floors` using `last_completed_bar_tid()` produces a LOWER floor, which is PERMISSIVE (allows more bars through) rather than restrictive

    If any test fails, diagnose and fix. The fix should be confined to the files already modified in Task 1 and Plan 01.

  </action>
  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && maturin develop --release 2>&1 | tail -5 && cargo nextest run --workspace --exclude opendeviationbar-py --no-fail-fast 2>&1 | tail -5 && uv run --python 3.13 pytest tests/ -x -q 2>&1 | tail -10</automated>
  </verify>
  <acceptance_criteria>
    - `maturin develop --release` exits 0
    - `cargo nextest run --workspace --exclude opendeviationbar-py` exits 0 with all tests passing
    - `uv run --python 3.13 pytest tests/ -x -q` exits 0 with all tests passing
    - No test failures, no compilation warnings related to the changes
  </acceptance_criteria>
  <done>
    Full test suite (Rust + Python) passes with zero failures.
    The dedup floor fix is verified to not regress any existing functionality.
    RUST-01 and RUST-02 requirements are satisfied.
  </done>
</task>

</tasks>

<verification>
```bash
# THE FIX: committed_floors uses completed bar tid
grep -n "last_completed_bar_tid" crates/opendeviationbar-streaming/src/live_engine.rs

# PyO3 binding exists

grep -n "last_completed_bar_tid" src/core_bindings.rs

# Python checkpoint serialization

grep -n "last_completed_bar_tid" src/helpers.rs

# Full Rust tests pass

cargo nextest run --workspace --exclude opendeviationbar-py --no-fail-fast

# Full Python tests pass

uv run --python 3.13 pytest tests/ -x -q

# The old pattern is gone (should NOT match)

grep "processor.last_agg_trade_id()" crates/opendeviationbar-streaming/src/live_engine.rs | grep "committed_floors\|proc_tid"

```
</verification>

<success_criteria>
- `committed_floors` initialization in `symbol_task()` uses `processor.last_completed_bar_tid()` -- the ONE line fix
- `processor.last_agg_trade_id()` is NOT used for floor initialization (old pattern removed)
- PyO3 binding `last_completed_bar_tid()` is accessible from Python
- Checkpoint Python dict includes `last_completed_bar_tid` key
- All Rust workspace tests pass (cargo nextest)
- All Python tests pass (pytest)
- No compilation errors or warnings related to the changes
</success_criteria>

<output>
After completion, create `.planning/phases/01-dedup-floor-fix/01-02-SUMMARY.md`
</output>
```
