# CLAUDE.md — Phase 13: Multi-Armed Bandit Filter Tuning

## Scope of This File

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

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

## Authoritative Specification

`PROJECT.md` §8.7 (Multi-Armed Bandit Filter Tuning) is authoritative. Read it carefully — the "credit assignment without modifying grab behavior" constraint is subtle. Also read:
- `PROGRESS.md` and `DECISIONS.md`.
- Phase 5's filter performance scorer — Phase 13 reuses the scoring infrastructure with a variant dimension.

## Phase 13 Mission

Run parallel filter variants and learn which produces the most real upload per byte grabbed, without modifying actual grab behavior. The operator defines a base filter and one or more proposed variants (different size limits, group inclusions, tracker subsets). The system only grabs what the most permissive variant would have grabbed anyway; the bandit chooses which variant gets *credit* for each grab. Over time, the score per variant converges to its true value with confidence intervals.

This is a research-grade feature in scope, but at single-operator scale the math is tractable.

## Deliverables Checklist

Phase 13 is complete when every item below is true:

- [ ] No new SQLite tables. Phase 5's `filter_performance.variant_id` column carries the variant assignment. The bandit reads and writes that column.
- [ ] Variant definitions live in `config/filter-variants.yaml`:
    ```yaml
    bandits:
      - filter_id: "autobrr:42"
        variants:
          - id: "baseline"
            description: "Current filter, unchanged"
            predicate: "true"                      # everything matches
          - id: "tight_size"
            description: "Max 10 GB instead of 50"
            predicate: "size_bytes <= 10737418240"
          - id: "internal_only"
            description: "Only INTERNAL release groups"
            predicate: "group == 'INTERNAL'"
        strategy: "thompson_sampling"              # epsilon_greedy | thompson_sampling | ucb1
        epsilon: 0.1                               # for epsilon_greedy
        prior_alpha: 1                             # for thompson_sampling beta priors
        prior_beta: 1
        promote_at_pct_of_grabs: 50                # auto-promote if a variant exceeds N% allocation
    ```
- [ ] Bandit core (`internal/bandit`):
    - Strategies: `epsilon_greedy`, `thompson_sampling`, `ucb1`. Strategy types pluggable.
    - On every grab event from the bus, evaluate the grab against each variant's predicate. The set of "would-have-matched" variants is the candidate pool. The bandit selects one variant from that pool to receive *credit* for the grab.
    - Credit is recorded by setting `filter_performance.variant_id` on the row.
    - The grab itself is unaffected — the most permissive variant always wins on whether-to-grab. Only credit attribution varies.
- [ ] Bandit scorer (extension of Phase 5 scorer): aggregates `filter_performance` by variant_id, produces a posterior distribution per variant (depending on strategy), surfaces convergence indicators (sample size, credible interval width).
- [ ] API endpoints (auth required, under `/api/bandits`):
    - `GET /api/bandits` — list active bandits with current state.
    - `GET /api/bandits/:filter_id` — one bandit's variants and scores.
    - `POST /api/bandits/:filter_id/promote` — apply a variant's parameters to the live filter (routes through autobrr's API; audit-logged).
    - `DELETE /api/bandits/:filter_id` — stop the bandit (all subsequent grabs default to baseline credit).
- [ ] Frontend:
    - `/bandits` route: list of active bandits.
    - `/bandits/:filter_id`: variant breakdown with posteriors, sample sizes, credible intervals, convergence warnings.
    - Promote action shows a confirmation dialog summarizing the diff between current and promoted variant.
- [ ] Integration with Phase 8 (simulation): bandits can be simulated by running each variant against the historical grab stream and reporting predicted scores. The simulation engine and bandit scorer share the predicate evaluator.
- [ ] Tests: bandit selection respects strategy semantics under controlled inputs; predicate evaluator correctly applies to synthetic grabs; promotion writes the right autobrr API call (mocked); convergence indicators correctly report wide-interval state.
- [ ] `PROGRESS.md` and `DECISIONS.md` updated. `DECISIONS.md` records: chosen default strategy, the predicate language (a small expression language; document the grammar), the auto-promote policy.

## Phase 13 Database Scope

No new tables. Phase 13 uses:
- `filter_performance.variant_id` (Phase 5).

## Repository Layout

```
internal/
  bandit/
    types.go                 # typed config schema
    loader.go                # YAML → typed → in-memory
    strategies/
      epsilon_greedy.go
      thompson_sampling.go
      ucb1.go
    predicate/
      lexer.go               # tiny expression language
      parser.go
      evaluator.go
      *_test.go
    selector.go              # the per-grab selection loop
    scorer.go                # posterior aggregation
    *_test.go
config/
  filter-variants.yaml
  filter-variants.example.yaml
```

Files added under existing packages:
- `internal/server/routes_bandits.go`
- `internal/performance/scorer.go` — extended to consume variant dimension.
- `internal/simulation/engine.go` — extended to support bandit simulation.

Frontend additions:
- `web/src/pages/Bandits.tsx`
- `web/src/pages/BanditDetail.tsx`
- `web/src/components/PosteriorChart.tsx`

## Working Rules

**Credit attribution, not grab control.** The most permissive variant decides whether to grab. The bandit decides which variant to credit. Operators may dispute this; document the choice explicitly in `DECISIONS.md` and the bandit UI.

**Convergence honesty.** A variant with 3 grabs and a wide credible interval is not a winner. The UI surfaces sample size next to every score. Promotion is gated by a configurable minimum sample size (default 30 per variant).

**Don't auto-promote by default.** `promote_at_pct_of_grabs` exists in the schema but defaults to disabled. The operator promotes deliberately.

**Tracker rules still apply.** A variant's predicate must not violate any tracker policy. Phase 11's tracker rule corpus is consulted on variant definitions; conflicting predicates produce a load-time warning.

**Bandit state survives restarts.** The accumulated credit is in `filter_performance`; the bandit recovers its state from the table on boot. Don't keep state in memory only.

## What "Phase 13 Complete" Looks Like

After Phase 13 ships, the operator can:

1. Define a bandit on an active filter in `config/filter-variants.yaml` with 3 variants.
2. Wait 1–2 weeks for grabs to accumulate.
3. Open the bandit's detail page: see Thompson-sampled posteriors per variant, sample sizes, and a "winning by 87% probability" note for one variant.
4. Run the bandit in simulation against the prior 90 days (Phase 8) to corroborate.
5. Promote the winning variant. The Command Center calls autobrr's API to update the filter parameters; audit-log entry written; the bandit auto-terminates with the new baseline.
6. If a variant's predicate references a tracker the operator has no entry for in their corpus (Phase 11), the loader logs a warning at startup.

## Begin

Read `PROJECT.md §8.7`. Start with the predicate expression language — that's the foundation. Then the strategies (thompson_sampling is the most useful default). Then the selector loop. Promotion last. Stop at the end of Phase 13.
