# Phase 1: Mutation Safety - Context

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

<domain>
## Phase Boundary

Kintsugi DELETE operations complete reliably under 24-worker concurrency without mutation timeouts. This phase audits every `ALTER TABLE DELETE` call site, migrates all to `DELETE IN PARTITION` where partition key is known, and replaces the global `_replace_lock` with per-partition locks to enable concurrent mutations on different partitions.

</domain>

<decisions>
## Implementation Decisions

### D-01: Full audit of all DELETE call sites

Migrate every `ALTER TABLE DELETE` to `DELETE IN PARTITION` where the partition key `(symbol, threshold_decimal_bps)` is known. This includes non-hot-path sites: sidecar pre-fill clean-slate, `rectify.py`, `cache_clear.py`, `validate_dedup_hardening.py`, and `kintsugi_log` cleanup. Belt-and-suspenders — no unscoped DELETEs anywhere.

### D-02: Per-partition lock replaces global `_replace_lock`

Replace the module-level `threading.Lock()` in `maintenance.py` with a dict of locks keyed by `(symbol, threshold_decimal_bps)`. Different partitions can mutate concurrently; same partition stays serialized. This matches ClickHouse behavior — mutations on different partitions don't conflict.

### D-03: MUT-01 already mostly satisfied

`_do_replace()` in `maintenance.py` already uses `DELETE IN PARTITION (symbol, threshold_decimal_bps)` since the partition key migration (v13.50.0). The kintsugi hot path is already partition-scoped. This phase is primarily about completeness (D-01) and concurrency (D-02).

### D-04: Production verification on bigblack

MUT-02 verified by running a normal kintsugi catchup on bigblack with 24 workers, then querying `system.mutations` to confirm max mutation duration stays well under 300s. No synthetic load test needed.

### Claude's Discretion

- Lock implementation details (e.g., `defaultdict(threading.Lock)` vs explicit dict with `setdefault`)
- Whether to add telemetry logging for per-partition lock contention
- Order of migration across call sites

</decisions>

<canonical_refs>

## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Mutation safety & ClickHouse DELETE strategies

- `CLAUDE.md` §7 "ClickHouse DELETE: Three Strategies" — full strategy table, anti-patterns, `DELETE IN PARTITION` syntax
- `python/opendeviationbar/clickhouse/CLAUDE.md` §"Overlap Prevention" — overlap strategy semantics, `_do_replace()` behavior

### Kintsugi repair pipeline

- `python/opendeviationbar/kintsugi/CLAUDE.md` — gap reconciliation architecture, repair flow
- `scripts/CLAUDE.md` §"Writer Ownership Model" — sidecar vs kintsugi boundary

### Current implementation

- `python/opendeviationbar/clickhouse/maintenance.py` — `_replace_lock` (line 29), `_do_replace()` (line 295)
- `python/opendeviationbar/kintsugi/repair.py` — kintsugi → `store_bars_batch(overlap_strategy="replace")` call sites
- `python/opendeviationbar/kintsugi/discovery.py:443` — `kintsugi_log` ALTER TABLE DELETE
- `python/opendeviationbar/sidecar/__init__.py:329` — sidecar pre-fill compound DELETE
- `python/opendeviationbar/rectify.py:234` — single-bar correction DELETE
- `scripts/cache_clear.py:48` — admin utility DELETE
- `scripts/validate_dedup_hardening.py:110` — test script DELETE

</canonical_refs>

<code_context>

## Existing Code Insights

### Reusable Assets

- `_do_replace()` in `maintenance.py` already has the correct `DELETE IN PARTITION` pattern — can use as template for other sites
- Partition key `(symbol, threshold_decimal_bps)` is always known at every DELETE call site

### Established Patterns

- `mutations_sync=1` on all DELETE operations (required, no exceptions)
- `_skip_lock=True` parameter prevents deadlock when `store_bars_replacing()` calls `store_bars_batch()` internally
- Dead code guard (`if False:`) in `_do_replace()` preserves the old fallback path for reference — can be removed

### Integration Points

- `store_bars_batch(overlap_strategy="replace")` → `store_bars_replacing()` → `_do_replace()` — the main routing chain
- Kintsugi workers call `store_bars_batch()` from `repair.py` (lines 436, 570) and `dead_letter.py` (line 48)
- `_replace_lock` is the single serialization point — changing it to per-partition affects all callers automatically

</code_context>

<specifics>
## Specific Ideas

No specific requirements — standard migration with clear patterns already established in the codebase.

</specifics>

<deferred>
## Deferred Ideas

None — discussion stayed within phase scope.

</deferred>

---

_Phase: 01-mutation-safety_
_Context gathered: 2026-03-24_
