# Heartbeat Refactor: Zero-Regression Strategy

## Core Principle

**Extract one module at a time. After each extraction, verify the Telegram message output is byte-identical.** Never extract two modules in the same commit.

## Test Harness (Build First)

Before any refactoring, create a snapshot test:

```python
# scripts/test_heartbeat_output.py
"""Compare old vs new heartbeat output for regression detection."""
import subprocess, json, sys

def run_heartbeat_dry():
    """Run heartbeat in dry-run mode, return results dict + formatted message."""
    # Import and call _run_checks() + _format_message() directly
    sys.path.insert(0, "scripts")
    from health_heartbeat import _run_checks, _format_message
    results = _run_checks()
    healthy = all(v is True for k, v in results.items() if isinstance(v, bool))
    message = _format_message(results, healthy)
    return results, message

if __name__ == "__main__":
    results, message = run_heartbeat_dry()
    # Save snapshot
    with open("/tmp/heartbeat_snapshot.json", "w") as f:
        json.dump({k: str(v) for k, v in results.items()}, f, indent=2)
    with open("/tmp/heartbeat_snapshot.txt", "w") as f:
        f.write(message)
    print(f"Results: {len(results)} keys")
    print(f"Message: {len(message)} chars")
    print("Snapshots saved to /tmp/heartbeat_snapshot.*")
```

Run this BEFORE and AFTER each extraction. Diff the outputs.

## Extraction Order (safest first)

### Phase 1: Zero-risk extractions (no logic changes)

1. **config.py** (~50 lines) — Extract constants only. Import them back. Zero logic change.
   - `_TTL_SECONDS`, `_STATE_DIR`, `_STATE_FILE`, `_STATHERA_STATE_FILE`
   - `_KNOWN_GAP_FINGERPRINTS`
   - `_ALERT_DEDUP_TTL`
   - `_HEARTBEAT_PRIORITY_SYMBOLS`

2. **state.py** (~70 lines) — Extract state persistence functions.
   - `_load_pending()`, `_save_pending()`
   - `_load_stathera_counts()`, `_save_stathera_counts()`

3. **telegram.py** (~110 lines) — Extract Telegram API functions.
   - `_send_telegram()`, `_delete_telegram_message()`, `_delete_expired()`
   - `_push_deadman_switch()`

4. **alert_dedup.py** (~45 lines) — Extract Redis dedup.
   - `_get_redis()`, `_should_alert()`

**After Phase 1**: health_heartbeat.py shrinks by ~275 lines. All functions are now imports. Run test harness — output must be identical.

### Phase 2: Check module extractions (logic preserved exactly)

1. **checks/services.py** (~140 lines)
   - `_check_oom_kills()`, `_check_systemd_overrides()`, `_get_kintsugi_daemon_pid()`
   - `_is_kintsugi_catchup_active()`, `_collect_service_stats()`

2. **checks/clickhouse.py** (~80 lines)
   - `_collect_clickhouse_stats()`

3. **checks/freshness.py** (~125 lines)
   - `_collect_symbol_freshness()`

4. **checks/kintsugi.py** (~170 lines)
   - `_KintsugiNdjsonResult`, `_parse_kintsugi_ndjson()`

5. **checks/resources.py** (~110 lines)
   - `_query_prometheus_range()`, `_smart_peak()`, `_collect_resource_peaks()`

6. **checks/version.py** (~180 lines)
   - `_detect_hotfix_files()`, `_check_version_drift()`

7. **checks/seeder.py** (~110 lines)
   - `_check_seeder()`

8. **checks/stathera.py** (~220 lines)
   - Extract the Stathera section from `_run_checks()` into `collect_stathera()`

**After Phase 2**: health_heartbeat.py is down to ~800 lines (orchestrator + formatter).

### Phase 3: Orchestrator + Formatter (most complex)

1. **checks/**init**.py** (~100 lines) — Extract `_run_checks()` orchestrator.
   - Each check module exports a `collect(results, client=None)` function
   - `run_checks()` calls them in order

2. **formatter.py** (~340 lines) — Extract `_format_message()`.

3. \***\*main**.py\*\* (~120 lines) — Extract `main()`, `_main()`.

4. **Shim**: Replace `health_heartbeat.py` with:

   ```python
   #!/usr/bin/env python3
   """Backward-compatible shim for systemd ExecStart."""
   from heartbeat.__main__ import main
   import sys
   sys.exit(main())
   ```

**After Phase 3**: Full modularization complete.

## Commit Convention

Each extraction is ONE commit:

```
refactor(heartbeat): extract config.py — constants and known fingerprints
refactor(heartbeat): extract state.py — pending messages and stathera counts
refactor(heartbeat): extract telegram.py — send, delete, dead-man switch
...
```

## Regression Detection Checklist

After EACH commit:

1. `python3 scripts/test_heartbeat_output.py` — compare with previous snapshot
2. `diff /tmp/heartbeat_snapshot_prev.txt /tmp/heartbeat_snapshot.txt` — message diff
3. `diff /tmp/heartbeat_snapshot_prev.json /tmp/heartbeat_snapshot.json` — results diff
4. Deploy to bigblack, wait for one heartbeat cycle, compare Telegram output

## Bug Fixes (Separate Commits)

Bug fixes from the analysis should be SEPARATE commits, not mixed with refactoring:

- `fix(heartbeat): emergency alert uses HTML not Markdown`
- `fix(heartbeat): explicit HEALTH_GATE_KEYS instead of all-bools`
- `fix(heartbeat): rename kintsugi_stuck to kintsugi_repairs_ok (inverted semantics)`

## New Checks (After Refactor)

Only add new checks AFTER the refactor is complete and stable:

- `feat(heartbeat): add sidecar engine metrics check`
- `feat(heartbeat): add dead letter count check`
- `feat(heartbeat): add Puck telemetry check`
