# Phase 40.1 Plan 01: Kintsugi 3-Bar Merge for Writer-Boundary Orphans (#339)

**One-liner:** 3-bar merge detects writer-boundary orphans via open-anchored breach check against left/right bar prices, reprocesses full TID range when cumulative deviation exceeds threshold.

## Changes

### `python/opendeviationbar/kintsugi/repair.py`

**New function: `_try_writer_boundary_merge()`** (~170 lines)

Called from the `trades_df.is_empty()` block in `_repair_aion()` when REST confirms trades exist (not an exchange gap) but no trades fall in the gap TID range. Before classifying as `forming_boundary`, checks whether the gap is a writer-boundary orphan:

1. Queries CH for left bar (ending at `anchor_last_tid`) and right bar (starting at `resume_first_tid`)
2. Computes breach check: `left_close * (1 - threshold) > right_low` (down) or `left_close * (1 + threshold) < right_high` (up)
3. If breach detected: loads trades from Parquet for range `[left.first_tid, right.last_tid]` (expanded date range +/- 1 day)
4. Processes with fresh `OpenDeviationBarProcessor`
5. DELETEs both left and right bars from CH using `DELETE IN PARTITION` with `mutations_sync=1`
6. INSERTs reprocessed bars via `store_bars_batch`

**Returns:** `int` (bars written) if merge performed, `None` if not a writer-boundary orphan.

**Modified: `_repair_aion()` empty-trades branch** (~6 lines changed)

Inserted call to `_try_writer_boundary_merge()` after exchange gap is ruled out but before `forming_boundary` classification. If merge returns a count, that value is returned immediately. If `None`, proceeds with existing `forming_boundary` logic.

**Fixed: `store_bars_batch` call** (pre-existing bug)

Changed `symbol=shard.symbol` keyword arg to positional `shard.symbol` to match the actual parameter name `symbol_or_key`.

### `pyproject.toml`

Added `PLR0911` to `repair.py` per-file-ignores (early-exit pattern in `_try_writer_boundary_merge`).

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] Fixed store_bars_batch keyword argument mismatch**

- **Found during:** implementation
- **Issue:** `symbol=shard.symbol` did not match parameter name `symbol_or_key`
- **Fix:** Changed to positional argument
- **Files modified:** `repair.py`
- **Commit:** 41d01d8

## Decisions Made

1. **Breach check uses strict inequality** (`>` / `<`): at exact threshold boundary, price hasn't actually breached -- matches the ODB algorithm's `>=` / `<=` behavior where the forming bar needs to exceed, not just touch.
2. **Date range expanded +/- 1 day** for Parquet lookup: left/right bar trades may span beyond the gap's timestamp range.
3. **`_thread_local.last_source = "merge"`**: new source label distinguishes writer-boundary merges in telemetry from standard repairs.

## Known Stubs

None.

## Commits

| Task | Description                             | Commit    |
| ---- | --------------------------------------- | --------- |
| 1    | 3-bar merge for writer-boundary orphans | `41d01d8` |

## Self-Check: PASSED

- [x] `repair.py` modified with `_try_writer_boundary_merge` function
- [x] `ruff check` passes clean
- [x] Import test passes
- [x] Breach calculation verified against known evidence values
- [x] Commit `41d01d8` exists
