---
phase: 30-parquet-first-gap-fill-junction-telemetry
plan: 02
type: tdd
wave: 2
depends_on: ["30-01"]
files_modified:
  - python/opendeviationbar/sidecar/junction_telemetry.py
  - python/opendeviationbar/sidecar/__init__.py
  - tests/test_sidecar_junction_telemetry.py
autonomous: true
requirements: [RESUME-03]

must_haves:
  truths:
    - "Sidecar logs junction_gap_tid_delta at REST-to-WS handoff per symbol"
    - "Sidecar logs ws_buffer_depth at handoff per symbol"
    - "Sidecar logs skipped_trades count at handoff per symbol"
    - "Junction telemetry is visible in /status endpoint and local telemetry log"
  artifacts:
    - path: "python/opendeviationbar/sidecar/junction_telemetry.py"
      provides: "_log_junction_telemetry() function"
      exports: ["_log_junction_telemetry"]
    - path: "tests/test_sidecar_junction_telemetry.py"
      provides: "Unit tests for junction telemetry"
      min_lines: 40
  key_links:
    - from: "python/opendeviationbar/sidecar/__init__.py"
      to: "python/opendeviationbar/sidecar/junction_telemetry.py"
      via: "_log_junction_telemetry() called after sync_flush and before engine.start()"
      pattern: "_log_junction_telemetry"
    - from: "python/opendeviationbar/sidecar/junction_telemetry.py"
      to: "python/opendeviationbar/sidecar/http_server.py"
      via: "_junction_state dict enrichment with telemetry fields"
      pattern: "_junction_state"
---

<objective>
Junction telemetry logging at REST-to-WS handoff point in sidecar startup.

Purpose: After fill_from_rest and sync_flush complete, before engine.start() begins WS streaming, we need to measure the quality of the handoff: how large is the TID gap between REST-filled bars and the first WS trade? How deep is the WS buffer that accumulated during startup? How many trades were skipped? This data is critical for diagnosing zero-gap guarantee violations.

Output: `junction_telemetry.py` module with `_log_junction_telemetry()` that computes and logs per-symbol handoff quality metrics, enriches \_junction_state for /status endpoint, and writes to local telemetry log.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/30-parquet-first-gap-fill-junction-telemetry/30-01-SUMMARY.md

<interfaces>
<!-- Key types and contracts the executor needs -->

From python/opendeviationbar/sidecar/http_server.py:

```python
# Junction telemetry state populated by _sync_flush_rest_bars() for /status endpoint.
_junction_state: dict[str, dict] = {}
# After sync_flush, _junction_state[symbol] contains:
#   rest_first_tid: int  -- first TID in REST-filled bars
#   rest_last_tid: int   -- last TID in REST-filled bars
#   ws_first_tid: int    -- first WS bar TID (populated later in main loop)
```

From python/opendeviationbar/sidecar/**init**.py (main loop, lines 1277-1284):

```python
# Record first WS bar TID per symbol for junction telemetry (STREAM-02 gap closure)
if (
    symbol in _junction_state
    and "ws_first_tid" not in _junction_state[symbol]
):
    ws_tid = burst_bar.get("first_agg_trade_id", 0)
    if ws_tid > 0:
        _junction_state[symbol]["ws_first_tid"] = ws_tid
```

From python/opendeviationbar/sidecar/**init**.py (after sync_flush, line 543):

```python
# sync_flush complete, engine.start() next at line 584
logger.info("sync_flush: drained and wrote %d bars to CH", flushed)
# >>> NEW: Junction telemetry goes here <<<
```

StreamManager.get_metrics() returns dict with:

- trades_received: total trades buffered from WS
- dropped_bars: bars lost to ring buffer overflow
- reconnections: WS reconnection count
  </interfaces>

</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Junction telemetry logging with TDD</name>
  <files>python/opendeviationbar/sidecar/junction_telemetry.py, python/opendeviationbar/sidecar/__init__.py, tests/test_sidecar_junction_telemetry.py</files>
  <read_first>
    - python/opendeviationbar/sidecar/__init__.py (lines 508-584 for fill_from_rest + sync_flush + engine.start region)
    - python/opendeviationbar/sidecar/__init__.py (lines 1270-1290 for ws_first_tid tracking)
    - python/opendeviationbar/sidecar/http_server.py (lines 39-41 for _junction_state, lines 346-379 for /status endpoint)
    - python/opendeviationbar/sidecar/delete_strategy.py (module structure pattern)
  </read_first>
  <behavior>
    - Test 1: _log_junction_telemetry computes junction_gap_tid_delta = ws_buffer_min_tid - rest_last_tid - 1 per symbol
    - Test 2: When WS buffer has trades, ws_buffer_depth is the count of buffered trades from engine.get_metrics()
    - Test 3: When no REST bars exist for a symbol, junction is marked "no_rest_data"
    - Test 4: _junction_state is enriched with telemetry fields (junction_gap_tid_delta, ws_buffer_depth, logged_at)
    - Test 5: Telemetry is logged as structured NDJSON to local telemetry file
  </behavior>
  <action>
    **RED phase**: Create tests/test_sidecar_junction_telemetry.py with 5 tests. Mock engine, _junction_state.

    **GREEN phase**: Create python/opendeviationbar/sidecar/junction_telemetry.py:

    ```python
    def _log_junction_telemetry(
        engine,              # StreamManager with get_metrics()
        aion_symbols,        # list[str] -- symbols to report on
        resume_tids,         # dict[str, int | None] -- original resume TIDs before Parquet prefill
    ) -> None:
    ```

    Implementation:
    1. Import _junction_state from .http_server
    2. Get engine metrics via engine.get_metrics() for ws_buffer_depth (trades_received count)
    3. For each aion_symbol:
       a. rest_last_tid = _junction_state.get(symbol, {}).get("rest_last_tid")
       b. If rest_last_tid is None: log "no_rest_data" for symbol, continue
       c. ws_buffer_depth = engine.get_metrics().get("trades_received", 0) (total WS trades buffered during startup)
       d. resume_tid = resume_tids.get(symbol) -- original resume point
       e. skipped_trades = 0 (will be computed when first WS bar arrives -- for now log the pre-handoff state)
       f. Compute parquet_coverage: True if Parquet prefill advanced TID beyond original resume_tid
    4. Log per-symbol telemetry at INFO level:
       ```
       junction_telemetry: {symbol} rest_last_tid={N} ws_buffer_depth={N} resume_tid={N} parquet_advanced={bool}
       ```
    5. Enrich _junction_state[symbol] with: junction_logged_at (ISO timestamp), ws_buffer_depth, parquet_coverage
    6. Write NDJSON entry to local telemetry log (same pattern as other telemetry):
       ```python
       from opendeviationbar.logging import write_telemetry_event
       write_telemetry_event("junction_telemetry", {...per-symbol data...})
       ```
       If write_telemetry_event doesn't exist, use direct NDJSON append to logs/events.jsonl
    7. Entire function is non-fatal (try/except at top level, log warning on failure)

    **Wire into __init__.py**: After sync_flush (line ~543) and before engine.start() (line ~584):
    ```python
    # Step 5.5: Junction telemetry (RESUME-03)
    try:
        from .junction_telemetry import _log_junction_telemetry
        _log_junction_telemetry(engine, aion_symbols, resume_tids)
    except Exception:
        logger.warning("junction telemetry failed (non-fatal)", exc_info=True)
    ```

    Add import to __init__.py's import block:
    ```python
    from .junction_telemetry import (
        _log_junction_telemetry as _log_junction_telemetry,
    )
    ```

    Note on skipped_trades: The exact count of skipped (duplicate) trades at the junction
    can only be known after the first WS bar arrives. The junction_telemetry log captures
    the pre-handoff state; the ws_first_tid tracking in the main loop (already exists at
    line 1277) captures the post-handoff state. The junction_gap_tid_delta = ws_first_tid -
    rest_last_tid - 1 is computed lazily when ws_first_tid becomes available (already logged
    via _junction_state and exposed via /status endpoint). This plan adds the pre-handoff
    metrics; the post-handoff delta is already wired.

  </action>
  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && uv run --python 3.13 pytest tests/test_sidecar_junction_telemetry.py -x -v</automated>
  </verify>
  <acceptance_criteria>
    - grep -q "def _log_junction_telemetry" python/opendeviationbar/sidecar/junction_telemetry.py
    - grep -q "_log_junction_telemetry" python/opendeviationbar/sidecar/__init__.py
    - grep -q "rest_last_tid" python/opendeviationbar/sidecar/junction_telemetry.py
    - grep -q "ws_buffer_depth" python/opendeviationbar/sidecar/junction_telemetry.py
    - grep -q "_junction_state" python/opendeviationbar/sidecar/junction_telemetry.py
    - grep -q "def test_" tests/test_sidecar_junction_telemetry.py
    - uv run --python 3.13 pytest tests/test_sidecar_junction_telemetry.py -x passes
  </acceptance_criteria>
  <done>
    _log_junction_telemetry() logs per-symbol handoff quality metrics (rest_last_tid, ws_buffer_depth,
    parquet_coverage) at INFO level, enriches _junction_state for /status endpoint, and writes
    NDJSON telemetry. Call site in __init__.py fires after sync_flush and before engine.start().
    All tests pass.
  </done>
</task>

</tasks>

<verification>
- tests/test_sidecar_junction_telemetry.py passes with all 5+ tests
- grep confirms _log_junction_telemetry is called in __init__.py after sync_flush
- grep confirms _junction_state enrichment in junction_telemetry.py
- grep confirms ws_buffer_depth and rest_last_tid metrics
- ruff check passes on new files
</verification>

<success_criteria>

1. New junction_telemetry.py module with_log_junction_telemetry() function
2. Call site in **init**.py after sync_flush, before engine.start()
3. Per-symbol metrics logged: rest_last_tid, ws_buffer_depth, parquet_coverage
4. \_junction_state enriched for /status endpoint consumption
5. NDJSON telemetry written to local log
6. Non-fatal: any failure does not block sidecar startup
7. All unit tests pass
   </success_criteria>

<output>
After completion, create `.planning/phases/30-parquet-first-gap-fill-junction-telemetry/30-02-SUMMARY.md`
</output>
