# CLAUDE.md — Phase 8: Simulation and What-If Engine

## Scope of This File

This file instructs you (Claude Code) to implement **Phase 8 only** of the Seedbox Command Center.

**Do not implement phases 9 through 15.** Stay in scope.

## Authoritative Specification

`PROJECT.md` §8.3 (Simulation and What-If Engine) is authoritative. Read it. Also read:
- `PROGRESS.md` and `DECISIONS.md`.
- Phase 7's intelligence modules — Phase 8 replays them against historical data with proposed rules instead of current rules.

## Phase 8 Mission

Replay proposed rule changes against historical snapshots in a sandboxed evaluation context. The operator can propose tightening an autobrr filter, enabling a new tqm rule, or adjusting a notification rule, and see exactly which torrents would have been deleted, which notifications would have fired, and how ratios would have moved — without touching production state.

Simulation isolates writes with a `simulation_id`. Real state is never mutated; a separate set of simulation-tagged rows accumulates and can be inspected, compared, and (rarely) promoted back to production.

## Deliverables Checklist

Phase 8 is complete when every item below is true:

- [ ] SQLite migration `009_phase8_simulation.sql` adds the `simulation_runs` table per Appendix A.
- [ ] `ratio_snapshots.simulation_id` and `torrent_snapshots.simulation_id` columns (which Phase 0 created in the DuckDB schema and Phase 1/Phase 3 created in the SQLite snapshots) are now actively used: simulation writes go to the same tables with a non-NULL simulation_id; production reads always filter `WHERE simulation_id IS NULL`. Audit existing queries to ensure they apply this filter.
- [ ] `internal/simulation` provides:
    - `Engine.Run(ctx, run *SimulationRun) (Summary, error)`: takes a proposed config and a time window, replays the snapshot stream through the intelligence + rules + notification modules with the proposed config in effect, writes resulting decisions and snapshots tagged with the simulation_id, returns a summary.
    - Sandboxed evaluation: Phase 7's intelligence modules are refactored (if not already) to accept a `Context` carrying the simulation_id and rule overrides, so the same code paths run identically against real and simulated state.
    - `Engine.Promote(ctx, runID int64) error`: moves the proposed config from the run's `proposed_config_json` into the relevant production config files (with audit-log entries; never silently). The operator can also reject promotion via the UI.
- [ ] API endpoints (auth required, under `/api/simulation`):
    - `POST /api/simulation/runs` — create a new run with a proposed_config_json + window; returns the run id; runs asynchronously.
    - `GET /api/simulation/runs` — list runs.
    - `GET /api/simulation/runs/:id` — one run's status and summary.
    - `GET /api/simulation/runs/:id/results` — full breakdown: which torrents would have been affected, which notifications would have fired, ratio impact.
    - `DELETE /api/simulation/runs/:id` — clean up simulation rows.
    - `POST /api/simulation/runs/:id/promote` — apply.
- [ ] Frontend:
    - `/simulation` route: list of runs with status, window, summary stats.
    - `/simulation/new`: a "rule wizard" UI that lets the operator load an existing rule (filter, notification rule, tqm rule) and propose modifications via a typed form. The window picker offers presets (last 7 / 30 / 90 / 365 days) and a custom range.
    - `/simulation/:id`: the results view — split-pane diff between current-config-projected and proposed-config-projected outcomes over the same window. Charts overlay; tables annotate.
    - Promotion confirms with a checklist of what will change.
- [ ] Tests: simulation against a frozen historical fixture yields deterministic results. Promotion is a no-op when applied twice. Simulation cleanup (`DELETE /api/simulation/runs/:id`) removes only the simulation_id-tagged rows.
- [ ] `PROGRESS.md` and `DECISIONS.md` updated. `DECISIONS.md` records: how simulation handles non-idempotent operations (filter mutations that would have triggered grabs that wouldn't have completed by window end), the policy on partial-window torrents, the cleanup TTL for stale simulations (default 30 days).

## Phase 8 Database Scope

Migration `009_phase8_simulation.sql` creates:
- `simulation_runs`

`ratio_snapshots.simulation_id` and `torrent_snapshots.simulation_id` already exist. No structural changes.

## Repository Layout

```
internal/
  simulation/
    engine.go                # Run, Promote, Cleanup
    context.go               # the sandboxed evaluation context
    proposals.go             # typed schemas for proposed config diffs
    summary.go               # the result aggregator
    *_test.go
```

Files modified in existing packages (audit only — no architectural changes):
- `internal/intelligence/*` — accept the simulation context where snapshot queries are issued.
- `internal/rules/engine.go` — same.

Frontend additions:
- `web/src/pages/Simulation.tsx`
- `web/src/pages/SimulationDetail.tsx`
- `web/src/pages/SimulationNew.tsx`
- `web/src/components/SimulationDiff.tsx`

## Working Rules

**Replay is bit-for-bit reproducible.** Same window + same proposed config + same input snapshots = same output. If your simulation depends on time-of-day or randomness, seed it. The disk forecast Monte Carlo uses a seeded RNG when invoked in simulation mode.

**Real state is sacred.** Simulation MUST NOT issue any `UPDATE` against production rows. The only writes are inserts tagged with `simulation_id`. Code review for simulation paths must verify this.

**Production reads always filter NULL.** Audit every `SELECT` against `ratio_snapshots`, `torrent_snapshots`, `decision_log` (if you tag those too — TBD), and add `WHERE simulation_id IS NULL` where missing. Tests cover this.

**Promotion is explicit and audited.** No "auto-promote if results look good" pathway. The operator clicks promote; the system writes the new config files and audits the action.

**Simulation cleanup is the operator's call.** A simulation accumulates real rows in the snapshot tables (just tagged). Don't auto-delete; surface storage cost in the UI. Provide a one-click delete with confirmation.

## What "Phase 8 Complete" Looks Like

After Phase 8 ships, the operator can:

1. Open Simulation → New, choose "Filter F", set a tighter size limit, choose "last 90 days".
2. Submit. The engine runs the replay in the background; the run shows up in the list with status `running`, then `completed`.
3. Open the results. See: "Would have skipped 47 grabs. Of those, 12 produced ≥ 5 GB upload (would have been a loss). The other 35 produced ≤ 100 MB each (would have been a win)."
4. Decide: maybe skip the tighter limit, or pair it with a different filter. Either way, no production change happened.
5. After several simulation iterations, find a config the operator is happy with. Click Promote. The new filter parameters are written to `config/filters.yaml` (or routed to autobrr's API) with an audit-log entry.
6. Delete the older simulations to free storage.

## Begin

Read `PROJECT.md §8.3`. Start by auditing Phase 7's intelligence modules for snapshot reads; add the simulation context plumbing. Then build the engine, then the promotion path, then the UI. Stop at the end of Phase 8.
