# Phase 1: Dedup Floor Fix - Context

**Gathered:** 2026-03-20
**Status:** Ready for planning

<domain>
## Phase Boundary

Fix `committed_floors` initialization in `live_engine.rs` to use the last COMPLETED bar's trade ID instead of `processor.last_agg_trade_id()` (which includes the forming bar tail). Eliminates guaranteed orphan bar suppression at midnight reset and multi-threshold floor inflation.

</domain>

<decisions>
## Implementation Decisions

### Floor Initialization Source

- Add `last_completed_bar_tid: Option<i64>` field to `OpenDeviationBarProcessor` — updated only on bar completion (`process_single_trade` returns `Some(bar)`) and orphan emission (`reset_at_ouroboros`), NOT on every trade
- `committed_floors` initialization changes from `processor.last_agg_trade_id()` to `processor.last_completed_bar_tid()` in `symbol_task()` line 1379
- Keep `ch_seed_tid` in the max calculation: `max(completed_bar_tid, ch_seed_tid)` — covers cold start and checkpoint-less restart
- No floor entry when both are absent (-1 equivalent) — don't suppress anything until first bar is emitted

### Checkpoint Compatibility

- Include `last_completed_bar_tid` in `Checkpoint` struct with `#[serde(default)]` — same pattern as `defer_open` and `prevent_same_timestamp_close`
- Old checkpoints deserialize fine (field defaults to `None`)
- No checkpoint version bump needed

### puck_fill Integration

- puck_fill's bypass of committed_floors CHECK is correct — no change needed
- puck_fill's omission of committed_floors UPDATE is harmless (floor is permissive/too-low, not restrictive)
- No refactoring of puck_fill code paths

### Claude's Discretion

- Exact placement of field update in `process_single_trade()` and `process_agg_trade_records_with_options()`
- Whether `reset_at_ouroboros()` sets `last_completed_bar_tid` to orphan's trade ID or resets to `None`
- PyO3 binding details in `core_bindings.rs` and `helpers.rs`
- Test design for verifying the fix
- Log message formatting

</decisions>

<code_context>

## Existing Code Insights

### Reusable Assets

- `crates/opendeviationbar-core/src/processor.rs:567` — existing `last_agg_trade_id()` accessor pattern to mirror
- `crates/opendeviationbar-core/src/checkpoint.rs:53-114` — `Checkpoint` struct with `#[serde(default)]` pattern for new fields
- `src/core_bindings.rs:618-619` — PyO3 binding pattern for `last_agg_trade_id()`
- `src/helpers.rs:262-415` — `checkpoint_to_dict()` / `dict_to_checkpoint()` for checkpoint serialization

### Established Patterns

- Processor fields: `last_trade_id` updated at line 433 on every trade, `defer_open` updated only on bar completion
- Checkpoint serde: `#[serde(default)]` for backward compat, no version bump
- PyO3 accessors: thin wrappers that delegate to processor methods
- Floor check: `bar.last_agg_trade_id <= floor` at lines 1070 and 1096

### Integration Points

- `live_engine.rs:1379` — the ONE line that changes: `processor.last_agg_trade_id()` → `processor.last_completed_bar_tid()`
- `processor.rs:528-534` — bar completion path in `process_single_trade()`
- `processor.rs:772` — batch path in `process_agg_trade_records_with_options()`
- `processor.rs:1101-1120` — `reset_at_ouroboros()` orphan emission
- `checkpoint.rs:852-874` — `create_checkpoint()`
- `checkpoint.rs:893-953` — `from_checkpoint()`

</code_context>

<specifics>
## Specific Ideas

### Root Cause (from 4-agent deep dive)

- **Primary**: Midnight orphan suppression — orphan bar's `last_agg_trade_id` ALWAYS equals the floor because both derive from the same forming bar tail. The `<=` check guarantees suppression.
- **Secondary**: Multi-threshold `ch_seed_tid` inflation — ch_seed_tid is per-symbol MAX across all thresholds, inflating the floor for high-frequency thresholds.
- **Production evidence**: WLDUSDT@250 bar [113843149, 113843152] suppressed, floor 113843341 from forming bar tail.

### Not Bugs (confirmed by research)

- puck_fill bypassing committed_floors: correct behavior, floor is a lower bound
- Stale floor after puck_fill: permissive (too low), not restrictive — no data loss risk
- ReplacingMergeTree + min_tid_guard + INSERT dedup tokens: defense-in-depth backstop

</specifics>

<deferred>
## Deferred Ideas

- Multi-threshold ch_seed_tid inflation: `fill_from_rest` uses per-symbol MAX trade ID, potentially skipping trades for lower thresholds. Separate concern from committed_floors, may warrant its own fix.

</deferred>
