---
phase: 14-rust-core-gap-fields-conditional-reset
verified: 2026-03-27T07:00:00Z
status: gaps_found
score: 4/5 success criteria verified
gaps:
  - truth: "A bar constructed from trades with a TID gap has has_gap=true, gap_trade_count>0, max_gap_duration_us>0"
    status: partial
    reason: "max_gap_duration_us is always 0 due to ordering bug: close_time is updated to trade.timestamp BEFORE the gap duration calculation, so trade.timestamp - self.close_time = 0"
    artifacts:
      - path: "crates/opendeviationbar-core/src/types.rs"
        issue: "update_with_trade() updates self.close_time at line 517-519 before computing this_gap_duration = trade.timestamp - self.close_time at line 531. Duration is always 0."
    missing:
      - "Capture old close_time before updating it: let prev_close_time = self.close_time; then use prev_close_time in the gap duration calculation"
human_verification:
  - test: "Run process_trades with two AggTrades having a TID gap and distinct timestamps"
    expected: "bar.has_gap=true, bar.gap_trade_count > 0, bar.max_gap_duration_us > 0"
    why_human: "No existing unit test exercises this exact path with assertions on all three gap fields"
---

# Phase 14: Rust Core -- Gap Fields + Conditional Reset Verification Report

**Phase Goal:** The Rust processor produces bars with gap awareness metadata and conditionally skips midnight reset for Aion-mode symbols
**Verified:** 2026-03-27T07:00:00Z
**Status:** gaps_found
**Re-verification:** No — initial verification

## Goal Achievement

### Observable Truths

| #   | Truth                                                                                                     | Status   | Evidence                                                                                                      |
| --- | --------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------- |
| 1   | A bar with TID gap has has_gap=true, gap_trade_count>0, max_gap_duration_us>0                             | PARTIAL  | has_gap and gap_trade_count are correctly set; max_gap_duration_us is always 0 due to close_time ordering bug |
| 2   | A bar from continuous trades has has_gap=false, gap_trade_count=0, max_gap_duration_us=0                  | VERIFIED | Default initialization in new() (lines 410-413 types.rs) and gap detection guard (last_agg_trade_id > 0)      |
| 3   | is_exchange_gap defaults to false and is never set by Rust processor                                      | VERIFIED | Grep across all Rust source confirms is_exchange_gap is never assigned true in any Rust code                  |
| 4   | Arrow export includes 4 new gap fields in schema and data                                                 | VERIFIED | arrow_export.rs lines 431-434 (schema), 492-495 (builders), 535-538 (append), 576-579 (finish)                |
| 5   | When in Aion mode, maybe_reset_at_midnight returns None (no midnight reset, no orphan bar)                | VERIFIED | trade_dispatch.rs lines 142-148: Aion early-returns None, Day branch unchanged                                |
| 6   | When in Day mode, maybe_reset_at_midnight behaves identically to current production (backward compatible) | VERIFIED | trade_dispatch.rs lines 150-158: identical to pre-phase code; 35/35 streaming tests pass                      |
| 7   | PyO3 dict export includes 4 new gap fields                                                                | VERIFIED | helpers.rs lines 143-146; arrow_bindings.rs lines 200-204 (dict_to_opendeviationbar_full round-trip)          |

**Score:** 4/5 ROADMAP success criteria verified (SC1 partial due to max_gap_duration_us=0 bug)

### Required Artifacts

| Artifact                                                              | Expected                                      | Status   | Details                                                                       |
| --------------------------------------------------------------------- | --------------------------------------------- | -------- | ----------------------------------------------------------------------------- |
| `crates/opendeviationbar-core/src/types.rs`                           | Gap fields on OpenDeviationBar struct         | VERIFIED | Tier 3b section at line 104; gap detection in update_with_trade lines 523-537 |
| `crates/opendeviationbar-core/src/arrow_export.rs`                    | Arrow schema and builder for gap fields       | VERIFIED | Schema lines 431-434, builders 492-495, append 535-538, finish 576-579        |
| `src/helpers.rs`                                                      | PyO3 dict export for gap fields               | VERIFIED | Lines 143-146: all 4 fields exported                                          |
| `src/arrow_bindings.rs`                                               | Round-trip dict_to_opendeviationbar_full      | VERIFIED | Lines 200-204: all 4 fields extracted on import                               |
| `crates/opendeviationbar-streaming/src/live_engine/types.rs`          | OuroborosMode enum with Day and Aion variants | VERIFIED | Lines 19-28: enum with #[default] Day, Aion variant                           |
| `crates/opendeviationbar-streaming/src/live_engine/trade_dispatch.rs` | Conditional midnight reset                    | VERIFIED | Lines 140-148: ouroboros_mode param, Aion early-return None                   |

### Key Link Verification

| From                        | To                                  | Via                                                | Status   | Details                                                                            |
| --------------------------- | ----------------------------------- | -------------------------------------------------- | -------- | ---------------------------------------------------------------------------------- |
| `processor.rs` (batch path) | `types.rs update_with_trade()`      | update_with_trade called for all subsequent trades | VERIFIED | Gap detection fires in update_with_trade, called by both streaming and batch paths |
| `arrow_export.rs`           | `types.rs OpenDeviationBar`         | Arrow schema includes gap fields                   | VERIFIED | All 4 gap fields in schema and builders                                            |
| `trade_dispatch.rs`         | `processor.rs reset_at_ouroboros()` | Aion mode bypasses the call                        | VERIFIED | `if ouroboros_mode == OuroborosMode::Aion { return None; }` at line 143            |
| `helpers.rs`                | `types.rs OpenDeviationBar`         | dict.set_item for gap fields                       | VERIFIED | 4 set_item calls at lines 143-146                                                  |

### Data-Flow Trace (Level 4)

Not applicable — this phase modifies Rust structs and algorithms, not data-rendering components.

### Behavioral Spot-Checks

| Behavior                          | Check                                                                  | Result                                  | Status |
| --------------------------------- | ---------------------------------------------------------------------- | --------------------------------------- | ------ |
| opendeviationbar-core compiles    | `cargo check -p opendeviationbar-core`                                 | Clean compilation                       | PASS   |
| Core with Arrow feature compiles  | `cargo check -p opendeviationbar-core --features arrow`                | Clean compilation                       | PASS   |
| Full PyO3 crate compiles          | `cargo check -p opendeviationbar-py`                                   | Clean compilation                       | PASS   |
| Streaming crate compiles          | `cargo check -p opendeviationbar-streaming`                            | Clean compilation                       | PASS   |
| Streaming tests pass (35 tests)   | `cargo nextest run -p opendeviationbar-streaming --lib`                | 35/35 passed                            | PASS   |
| Aion mode returns None            | Code inspection: lines 142-148 trade_dispatch.rs                       | Confirmed early-return None             | PASS   |
| max_gap_duration_us set correctly | Code inspection: types.rs line 531 `trade.timestamp - self.close_time` | Bug: close_time updated first, always 0 | FAIL   |

### Requirements Coverage

| Requirement | Source Plan | Description                                                                                         | Status    | Evidence                                                                                    |
| ----------- | ----------- | --------------------------------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------- |
| CORE-01     | 14-02       | Processor skips reset_at_ouroboros() when symbol's ouroboros_mode is Aion                           | SATISFIED | trade_dispatch.rs lines 143-148: Aion early-returns None before reset_at_ouroboros() call   |
| CORE-02     | 14-01       | OpenDeviationBar has has_gap, gap_trade_count, max_gap_duration_us set during construction when gap | PARTIAL   | has_gap and gap_trade_count set correctly; max_gap_duration_us always 0 due to ordering bug |
| CORE-03     | 14-01       | OpenDeviationBar has is_exchange_gap for unhealable gaps (set by Python, not Rust)                  | SATISFIED | Field exists (types.rs line 123); no Rust-side setter confirmed by grep of all Rust source  |
| CORE-04     | 14-02       | maybe_reset_at_midnight() in trade_dispatch.rs reads mode and skips reset for Aion                  | SATISFIED | Lines 136-158: accepts ouroboros_mode param, Aion early-returns None                        |

**Orphaned requirements check:** REQUIREMENTS.md maps CORE-01 through CORE-04 to Phase 14. All 4 are covered by the two plans. No orphaned requirements.

### Anti-Patterns Found

| File                                                                  | Line | Pattern                                              | Severity | Impact                                          |
| --------------------------------------------------------------------- | ---- | ---------------------------------------------------- | -------- | ----------------------------------------------- |
| `crates/opendeviationbar-core/src/types.rs`                           | 531  | `close_time` updated before gap duration calculation | BLOCKER  | max_gap_duration_us is always 0 when gap occurs |
| `crates/opendeviationbar-streaming/src/live_engine/trade_dispatch.rs` | 28   | TODO: wire actual mode from symbol config (Phase 16) | INFO     | Intentional — Phase 16 will wire this           |

### Human Verification Required

#### 1. Gap Duration Field Correctness

**Test:** Run process_trades with two AggTrades having a TID gap (e.g., TIDs 100 and 105) with different timestamps (e.g., 1000000 us and 1500000 us)
**Expected:** `bar.has_gap=true`, `bar.gap_trade_count=4`, `bar.max_gap_duration_us=500000`
**Why human:** No existing unit test exercises this path with assertions on all three gap fields. The code bug (close_time updated before gap duration calculation) means this will currently fail.

### Gaps Summary

**One functional bug** blocks full goal achievement: `max_gap_duration_us` is always 0 when a TID gap occurs.

**Root cause:** In `update_with_trade()` (types.rs lines 517-518), `self.close_time` is updated to `trade.timestamp` BEFORE the gap duration is computed at line 531 as `trade.timestamp - self.close_time`. Since `self.close_time` now equals `trade.timestamp`, the result is always 0.

**Fix required:** Capture `self.close_time` before updating it:

```rust
// Save previous close_time for gap duration calculation
let prev_close_time = self.close_time;

self.close = trade.price;
if trade.timestamp > self.close_time {
    self.close_time = trade.timestamp;
}
...
if trade.agg_trade_id > self.last_agg_trade_id {
    let expected_next = self.last_agg_trade_id + 1;
    if trade.agg_trade_id > expected_next && self.last_agg_trade_id > 0 {
        self.has_gap = true;
        let this_gap_count = trade.agg_trade_id - expected_next;
        self.gap_trade_count += this_gap_count;
        // Now uses pre-update close_time (time of last trade before gap)
        let this_gap_duration = trade.timestamp - prev_close_time;
        if this_gap_duration > self.max_gap_duration_us {
            self.max_gap_duration_us = this_gap_duration;
        }
    }
    self.last_agg_trade_id = trade.agg_trade_id;
}
```

**Scope of remaining gaps:**

- CORE-01, CORE-03, CORE-04: Fully satisfied
- CORE-02: Partially satisfied (has_gap and gap_trade_count correct; max_gap_duration_us is always 0)
- ROADMAP SC1: Fails the `max_gap_duration_us > 0` assertion
- All other SC (SC2, SC3, SC4, SC5): Satisfied

**Note on OuroborosMode export:** `OuroborosMode` is re-exported from `live_engine/mod.rs` (line 21) but NOT from the streaming crate root `lib.rs`. Phase 16 consumers can access it via `opendeviationbar_streaming::live_engine::OuroborosMode` (since `live_engine` is `pub mod`). This is a minor gap vs the plan's stated intent ("crate's public path") but does not block Phase 16.

---

_Verified: 2026-03-27T07:00:00Z_
_Verifier: Claude (gsd-verifier)_
