---
phase: 01-crossover-tid-utility
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - python/opendeviationbar/crossover.py
  - python/opendeviationbar/sidecar/midnight.py
  - scripts/heartbeat/checks/yesterday.py
  - tests/test_crossover.py
autonomous: true
requirements:
  - XOVER-01
  - XOVER-02

must_haves:
  truths:
    - "Any module can call get_crossover_tid(symbol, date) without importing sidecar internals"
    - "The shared utility uses the same 2-tier lookup (CH-seed + API-max) and self-validation as the sidecar"
    - "Sidecar midnight preflight works identically after extraction (no behavioral change)"
    - "Heartbeat yesterday.py uses the shared utility instead of its own reimplementation"
  artifacts:
    - path: "python/opendeviationbar/crossover.py"
      provides: "Shared crossover TID lookup with date parameter"
      exports: ["get_crossover_tid"]
      min_lines: 80
    - path: "tests/test_crossover.py"
      provides: "Unit tests for crossover module"
      min_lines: 30
  key_links:
    - from: "python/opendeviationbar/crossover.py"
      to: "opendeviationbar._core.fetch_aggtrades_by_id"
      via: "PyO3 binding for REST validation"
      pattern: "fetch_aggtrades_by_id"
    - from: "python/opendeviationbar/crossover.py"
      to: "opendeviationbar._core.fetch_aggtrades_rest"
      via: "PyO3 binding for API-max tier"
      pattern: "fetch_aggtrades_rest"
    - from: "python/opendeviationbar/sidecar/midnight.py"
      to: "python/opendeviationbar/crossover.py"
      via: "import get_crossover_tid"
      pattern: "from opendeviationbar\\.crossover import"
    - from: "scripts/heartbeat/checks/yesterday.py"
      to: "python/opendeviationbar/crossover.py"
      via: "import get_crossover_tid"
      pattern: "from opendeviationbar\\.crossover import"
---

<objective>
Extract crossover TID lookup from sidecar/midnight.py into a shared module so seeder, kintsugi, and heartbeat can look up crossover TIDs without importing sidecar internals.

Purpose: Phase 1 foundation -- Phases 2-4 all depend on this shared utility for day-boundary completeness checks.
Output: `python/opendeviationbar/crossover.py` with `get_crossover_tid(symbol, date)`, sidecar and heartbeat rewired to use it.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md

@python/opendeviationbar/sidecar/midnight.py
@scripts/heartbeat/checks/yesterday.py
</context>

<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->

From python/opendeviationbar/sidecar/midnight.py (functions to extract):

```python
def _get_midnight_crossover_tid(symbol: str) -> int | None:
    """2-tier crossover lookup: CH-seed + API-max post-midnight."""

def _self_validate_crossover(symbol: str, crossover_tid: int, midnight_ms: int, *, source: str) -> None:
    """Self-validate via 3 properties: predecessor < midnight, crossover >= midnight, consecutive TIDs."""

def _ch_yesterday_last_tid(symbol: str) -> int | None:
    """Query CH for yesterday's MAX(last_agg_trade_id)."""

def _verify_crossover_from_seed(symbol: str, seed_tid: int, midnight_ms: int) -> int | None:
    """Verify CH-seeded TID by fetching fromId=seed+1 from REST."""

def _api_max_post_midnight(symbol: str, midnight_ms: int) -> int | None:
    """Query first trade after midnight using API's 1-hour max range."""
```

From opendeviationbar.\_core (PyO3 bindings used by the above):

```python
def fetch_aggtrades_by_id(symbol: str, from_id: int, limit: int) -> list[dict]: ...
def fetch_aggtrades_rest(symbol: str, start_time_ms: int, end_time_ms: int) -> list[dict]: ...
```

</interfaces>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Create shared crossover module with date parameter</name>
  <files>python/opendeviationbar/crossover.py, tests/test_crossover.py</files>
  <behavior>
    - Test 1: get_crossover_tid(symbol, date) computes correct midnight_ms from a given date (e.g., date(2026, 3, 25) -> midnight_ms = 1742860800000)
    - Test 2: get_crossover_tid(symbol) with no date defaults to today (datetime.now(UTC).date())
    - Test 3: _ch_date_last_tid queries the correct date (not hardcoded yesterday())
    - Test 4: _verify_crossover_from_seed returns None when seed trade is before midnight
    - Test 5: _self_validate_crossover logs WARNING on property violations (not raises)
  </behavior>
  <action>
    Create `python/opendeviationbar/crossover.py` by extracting and generalizing from `sidecar/midnight.py:17-229`.

    The new public API:
    ```python
    def get_crossover_tid(symbol: str, date: datetime.date | None = None) -> int | None:
    ```

    Key changes from the sidecar version:
    1. Accept `date` parameter instead of hardcoding `datetime.now(UTC)`. Default to today if None.
    2. `midnight_ms = int(datetime(date.year, date.month, date.day, tzinfo=UTC).timestamp() * 1000)`
    3. Rename `_ch_yesterday_last_tid(symbol)` to `_ch_date_last_tid(symbol, date)` -- query CH for MAX(last_agg_trade_id) on the day BEFORE the given date (i.e., `toDate(close_time_us / 1000000) = toDate('{date}') - 1`). Use parameterized query with the actual date instead of `yesterday()`.
    4. Keep ALL helper functions private (underscore-prefixed): `_self_validate_crossover`, `_ch_date_last_tid`, `_verify_crossover_from_seed`, `_api_max_post_midnight`.
    5. Keep the exact same 2-tier logic: Tier 1 = CH seed + REST verify, Tier 2 = API-max post-midnight.
    6. Keep the exact same self-validation (3 properties, never raises, logs WARNING).
    7. Use the same PyO3 bindings: `fetch_aggtrades_by_id`, `fetch_aggtrades_rest` (Rust-first maintained).

    Create `tests/test_crossover.py` with unit tests that mock the PyO3 bindings and CH queries. Use `unittest.mock.patch` for `opendeviationbar._core.fetch_aggtrades_by_id` and `opendeviationbar._core.fetch_aggtrades_rest`.

  </action>
  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && uv run --python 3.13 pytest tests/test_crossover.py -x -v</automated>
  </verify>
  <done>
    - `python/opendeviationbar/crossover.py` exists with public `get_crossover_tid(symbol, date=None)` function
    - All private helpers extracted with date parameterization
    - Tests pass covering midnight_ms computation, default date behavior, CH query parameterization, and validation logic
  </done>
</task>

<task type="auto">
  <name>Task 2: Rewire sidecar and heartbeat to use shared module</name>
  <files>python/opendeviationbar/sidecar/midnight.py, scripts/heartbeat/checks/yesterday.py</files>
  <read_first>
    - python/opendeviationbar/crossover.py (created in Task 1)
    - python/opendeviationbar/sidecar/midnight.py
    - python/opendeviationbar/sidecar/__init__.py (lines 130-145 for imports)
    - scripts/heartbeat/checks/yesterday.py
    - tests/test_midnight_preflight.py
    - tests/test_heartbeat_yesterday_junction.py
    - tests/test_yesterday_integrity.py
  </read_first>
  <action>
    **Sidecar midnight.py changes:**
    1. Delete the extracted functions: `_get_midnight_crossover_tid`, `_self_validate_crossover`, `_ch_yesterday_last_tid`, `_verify_crossover_from_seed`, `_api_max_post_midnight` (lines 17-229).
    2. Add import at top: `from opendeviationbar.crossover import get_crossover_tid`
    3. Add a backward-compatible alias so sidecar/__init__.py's `_get_midnight_crossover_tid as _get_midnight_crossover_tid` import still works:
       ```python
       def _get_midnight_crossover_tid(symbol: str) -> int | None:
           """Backward-compatible wrapper. Delegates to shared crossover module."""
           return get_crossover_tid(symbol)  # date=None means today
       ```
    4. Update any other callers within midnight.py (e.g., `_run_midnight_preflight` at line ~868) to use either the wrapper or `get_crossover_tid` directly.
    5. Keep ALL other functions in midnight.py unchanged: `_run_midnight_preflight`, `fill_from_rest`, `_verify_yesterday_orphan`, `_heal_day_boundary_gaps`, etc.

    **Heartbeat yesterday.py changes:**
    1. Delete the local `_get_midnight_crossover_tid` function (lines 17-60).
    2. Add import: `from opendeviationbar.crossover import get_crossover_tid`
    3. In `collect_yesterday_completion()`, replace `crossover_tid = _get_midnight_crossover_tid(symbol)` with `crossover_tid = get_crossover_tid(symbol)` (today's crossover, same as before).
    4. Remove `import requests` if no longer needed after deleting the local function.

    **Verify backward compatibility:**
    - `sidecar/__init__.py` line 141 imports `_get_midnight_crossover_tid as _get_midnight_crossover_tid` -- must still resolve (the wrapper provides this).
    - No behavioral change: sidecar still gets today's crossover, heartbeat still gets today's crossover.

  </action>
  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && uv run --python 3.13 pytest tests/test_crossover.py tests/test_midnight_preflight.py tests/test_yesterday_integrity.py tests/test_heartbeat_yesterday_junction.py -x -v 2>&1 | tail -30</automated>
  </verify>
  <done>
    - sidecar/midnight.py no longer contains crossover lookup logic -- imports from shared module
    - heartbeat/yesterday.py no longer contains its own crossover lookup -- imports from shared module
    - Backward-compatible `_get_midnight_crossover_tid` wrapper exists in midnight.py for sidecar/__init__.py import
    - All existing tests pass without modification (no behavioral change)
    - `from opendeviationbar.crossover import get_crossover_tid` works from any Python module
  </done>
</task>

</tasks>

<verification>
1. `get_crossover_tid` is importable from `opendeviationbar.crossover` without any sidecar dependency
2. Sidecar midnight preflight still works (existing tests pass)
3. Heartbeat yesterday check still works (existing tests pass)
4. New crossover tests pass
5. No circular imports: `crossover.py` imports only from `opendeviationbar._core` and `opendeviationbar.clickhouse.cache`
</verification>

<success_criteria>

- `python/opendeviationbar/crossover.py` exists with `get_crossover_tid(symbol, date=None)` public API
- Sidecar and heartbeat both delegate to the shared module
- All tests pass (new + existing)
- No behavioral change in production (sidecar and heartbeat work identically)
  </success_criteria>

<output>
After completion, create `.planning/phases/01-crossover-tid-utility/01-01-SUMMARY.md`
</output>
