---
phase: 22-monitor-hardening-schema
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - scripts/heartbeat/checks/yesterday.py
  - python/opendeviationbar/clickhouse/bulk_operations.py
autonomous: true
requirements: [MON-04, SCHEMA-01]

must_haves:
  truths:
    - "Aion freshness query only considers bars with ouroboros_mode='aion', preventing false-positive freshness from leftover day-mode bars"
    - "A store_bars_batch/store_bars_bulk call with ouroboros_mode=None for a crypto symbol logs a loud warning identifying the call site"
  artifacts:
    - path: "scripts/heartbeat/checks/yesterday.py"
      provides: "Aion freshness query with mode filter"
      contains: "ouroboros_mode"
    - path: "python/opendeviationbar/clickhouse/bulk_operations.py"
      provides: "Python-side ouroboros_mode validation on write"
      contains: "ouroboros_mode.*warning"
  key_links:
    - from: "scripts/heartbeat/checks/yesterday.py"
      to: "opendeviationbar_cache.open_deviation_bars"
      via: "SQL query with ouroboros_mode filter"
      pattern: "ouroboros_mode.*=.*aion"
---

<objective>
Fix Aion freshness query false-positive risk and add Python-side ouroboros_mode INSERT validation.

Purpose: MON-04 prevents stale day-mode bars from masking Aion freshness issues. SCHEMA-01 adds defense-in-depth against silent mode mismatch on writes (keeping CH DEFAULT 'day' for backward compat, but warning loudly when Python falls back to it for crypto symbols).
Output: Patched heartbeat query, defensive logging in bulk write path.
</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

@scripts/heartbeat/checks/yesterday.py
@python/opendeviationbar/clickhouse/bulk_operations.py

<interfaces>
From scripts/heartbeat/checks/yesterday.py (lines 65-109):
```python
def _check_aion_freshness(
    client: object,
    symbol: str,
    results: dict[str, bool | str],
    full_table: str,
) -> None:
    # Current query at line 77-83:
    # SELECT max(close_time_us) FROM {full_table} FINAL
    # WHERE symbol = '{symbol}' AND close_time_us > 0
    # MISSING: AND ouroboros_mode = 'aion'
```

From python/opendeviationbar/clickhouse/bulk_operations.py (lines 580-585):

```python
# Issue #126: Resolve ouroboros_mode from config if not specified
if ouroboros_mode is None:
    from opendeviationbar.ouroboros import get_operational_ouroboros_mode
    ouroboros_mode = get_operational_ouroboros_mode()
```

CH schema (schema.sql line 259):

```sql
ouroboros_mode LowCardinality(String) DEFAULT 'day',
```

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add ouroboros_mode='aion' filter to freshness query (MON-04)</name>
  <files>scripts/heartbeat/checks/yesterday.py</files>
  <read_first>scripts/heartbeat/checks/yesterday.py</read_first>
  <action>
In `_check_aion_freshness()` (line 77-83), add `AND ouroboros_mode = 'aion'` to the WHERE clause of the SQL query. The current query:

```sql
SELECT max(close_time_us)
FROM {full_table} FINAL
WHERE symbol = '{symbol}'
  AND close_time_us > 0
```

Must become:

```sql
SELECT max(close_time_us)
FROM {full_table} FINAL
WHERE symbol = '{symbol}'
  AND ouroboros_mode = 'aion'
  AND close_time_us > 0
```

This prevents false-positive freshness results from leftover day-mode bars that may still exist in ClickHouse after the v7.0 Aion recompute. Per MON-04.
</action>
<verify>
<automated>grep -n "ouroboros_mode.\*aion" scripts/heartbeat/checks/yesterday.py | grep -q "SELECT\|WHERE\|AND"</automated>
</verify>
<done>The_check_aion_freshness SQL query includes AND ouroboros_mode = 'aion' in the WHERE clause</done>
</task>

<task type="auto">
  <name>Task 2: Add Python-side ouroboros_mode validation warning on write (SCHEMA-01)</name>
  <files>python/opendeviationbar/clickhouse/bulk_operations.py</files>
  <read_first>python/opendeviationbar/clickhouse/bulk_operations.py</read_first>
  <action>
Decision: Keep CH `DEFAULT 'day'` for backward compatibility (removing it would break existing INSERT statements that don't specify the column). Instead, add Python-side defense-in-depth.

In `store_bars_batch()` (around line 582-585), where `ouroboros_mode` is resolved from `get_operational_ouroboros_mode()` when None: after resolving the mode, check if `symbol` is a crypto symbol that should be Aion. If so, log a WARNING identifying the caller.

Implementation:

1. After the existing `ouroboros_mode = get_operational_ouroboros_mode()` fallback (line 585), add a check:

```python
# SCHEMA-01: Warn when crypto symbol falls back to global mode
try:
    from opendeviationbar.symbol_registry import get_symbol_entry
    entry = get_symbol_entry(symbol)
    if entry and hasattr(entry, 'ouroboros_mode'):
        expected = entry.ouroboros_mode.value
        if expected != ouroboros_mode:
            logger.warning(
                "SCHEMA-01: ouroboros_mode mismatch for %s: "
                "caller passed None (resolved to '%s'), "
                "registry says '%s'. Fix the caller to pass explicit mode.",
                symbol, ouroboros_mode, expected,
            )
except Exception:
    pass  # Registry unavailable — allow write
```

1. Apply the same pattern to `store_bars_bulk()` — find the equivalent `ouroboros_mode is None` fallback block and add the same check.

This surfaces misconfigured call sites loudly in logs without breaking writes. The CH DEFAULT stays as-is for safety.
</action>
<verify>
<automated>grep -c "SCHEMA-01.\*ouroboros_mode mismatch" python/opendeviationbar/clickhouse/bulk_operations.py</automated>
</verify>
<done>Both store_bars_batch and store_bars_bulk log a WARNING when a crypto symbol's ouroboros_mode falls back to global default instead of explicit per-symbol mode. grep returns count >= 2.</done>
</task>

</tasks>

<verification>
1. `grep -n "ouroboros_mode.*aion" scripts/heartbeat/checks/yesterday.py` shows the filter in the query
2. `grep -n "SCHEMA-01" python/opendeviationbar/clickhouse/bulk_operations.py` shows warnings in both store methods
3. `mise run check-full` passes (formatting, linting, tests)
</verification>

<success_criteria>

- Aion freshness heartbeat query filters by ouroboros_mode='aion' (MON-04)
- Crypto symbols with wrong ouroboros_mode on write produce loud log warnings (SCHEMA-01)
- No behavioral change to existing write paths (warnings only, no errors)
  </success_criteria>

<output>
After completion, create `.planning/phases/22-monitor-hardening-schema/22-01-SUMMARY.md`
</output>
