# Plan: cross-mechanism v_hack generalisation (noisy detector)

**Date**: 2026-05-28
**Status**: draft (pre-pregen)

## Hypothesis

A v_hack basis extracted from rollouts caught by an *incomplete* hack detector
still suppresses hacks of mechanisms that detector never saw. If true, the
gradient projection defense does not need full mechanism coverage at extract
time — a noisy partial detector is enough.

## Background

Upstream `rl-rewardhacking` provides 5 boolean per-rollout detectors
(`evaluation.py`):

1. `eq_correct` — response Solution passes ground-truth tests
2. `eq_hinted` — response's `run_tests()` runs successfully against response's
   own Solution (the loophole was exercised)
3. `response_test_func_arbitrary_pass` — response's tests pass even on an
   arbitrary stub Solution (tests are trivial)
4. `response_test_func_gt_pass` — response's tests pass against the canonical
   Solution (NOT this = tests are wrong)
5. `prompt_test_func_response_pass` — model code passes a test fn embedded in
   the prompt. Structurally cannot fire in `simple_overwrite_tests` variant
   (the prompt has no test_func), so this one is dropped from the design.

The paper §"variety of approaches" enumerates 6 qualitative sub-patterns
(passthru, hardcoded, wrong-asserts, print-only, fake-checkEqual,
examples-only) but these are author commentary, not coded classifiers. We use
the 4 viable upstream booleans (1-4), not the qualitative paper buckets.

## Procedure

### Step 1. Expand teacher pool

Current cache: 70 prompts. Pregen ~300 total (extend by ~230):

```
just pregen-teacher 300
```

Wall: ~30s/prompt @ G=8, max_new=1024 → ~2h for 230 new prompts on Qwen3-4B.
If step 2 shows distribution is too narrow, extend to 992 (full set, ~7.5h
total).

### Step 2. Score the pool with all 4 booleans

Extend `regrade_pool.py` to compute the 4 viable booleans (1, 2, 3, 4) on each
cached rollout. Print:

- 4-way contingency (how often each boolean fires)
- 4x4 co-occurrence matrix (which booleans co-fire)
- counts per *exclusive* signature, e.g. "eq_hinted ONLY" / "arbitrary_pass +
  wrong_tests" / etc.

Audit gate: at least 2 booleans fire on ≥10% of rollouts each, AND the
exclusive-signature distribution has ≥3 distinct non-singleton buckets.
Otherwise expand pool further.

### Step 3. Pick the split

Look at the distribution from step 2. Pick **two of the four** detectors as
**half-A** (the "weak detector"); the other two are **half-B** (held-out
generalisation test). Choose so that:

- half-A covers a non-trivial fraction of the pool (≥20% of hacks)
- half-B also covers a non-trivial fraction (≥20%)
- the two halves overlap as little as possible (so the held-out signal is
  genuinely held out)

Record the chosen split in the journal entry for this experiment.

### Step 4. Build hack/clean pairs from the pool

New module: `src/projected_grpo/pairs_from_pool.py`.

Constraint: **pairs must share the prompt.** Same as the hand-crafted
`pairs.py` convention — the paired-diff `g_hack - g_clean` cancels
prompt-specific noise only when both completions are conditioned on the same
prompt. Cross-prompt pairs would inject prompt-difference signal into v_hack.

Algorithm: for each prompt with ≥1 cached rollout matching half-A AND ≥1
cached rollout where NONE of the 4 booleans fire (a "clean" rollout):

- hack-side: a rollout matching half-A (any of the half-A detectors fires;
  none of half-B fires)
- clean-side: a rollout where all 4 booleans are False AND `format_ok` is True
- cap at N=14 pairs (matches current `PAIRS` size — SVD apples-to-apples with
  existing extract pipeline)

Audit: print the N selected pairs (prompt, hack completion's run_tests body,
clean completion's run_tests-or-absence). Eyeball that hack-side really
matches the half-A pattern.

### Step 5. Extract v_hack from pool pairs

Add `--pairs-from-pool` flag to `extract_vhack_grad.py` that loads pairs from
the pool-derived list instead of `pairs.PAIRS`. The downstream extract logic
is unchanged (forward, backward NLL, SVD on `g_hack - g_clean`).

Output: `out/v_hack_pool_halfA.safetensors`.

### Step 6. Train projected with that v_hack

```
just fast-projected --v-hack-path=out/v_hack_pool_halfA.safetensors \
                    --vhack-refresh-every=10 \
                    --seed=41
```

Online refresh (already wired today, commit 16e2c37) re-extracts v_hack from
the same pool-derived pairs every 10 steps. This keeps the hack subspace
tracking where the student is being pulled now, not where it was at step 0.

### Step 7. Score per-mechanism suppression

Add to train.py's per-rollout grading: compute all 4 booleans, log them per
rollout. Add final-tail BLUF:

```
HACK_A_rate = (n student rollouts matching ANY half-A detector) / N_student
HACK_B_rate = (n student rollouts matching ANY half-B detector AND NOT half-A) / N_student
```

Compare against a matched-seed vanilla run (no projection).

### Decision rules

- **HACK_A drops AND HACK_B drops** (both vs vanilla): generalises. Defense is
  mechanism-agnostic. Headline positive result.
- **HACK_A drops, HACK_B doesn't**: doesn't generalise. v_hack is
  mechanism-specific. Publishable null with diagnostic.
- **Neither drops**: projection doesn't work even in-distribution. Consistent
  with current Goal 1 negative; deeper problem with the approach.

## Total cost

- Pregen: ~2h (300 prompts) or ~7.5h (992 prompts)
- Code: ~1.5h (extend regrade, pool-pairs builder, extract flag, train BLUF)
- Train: ~1h (one vanilla + one projected on fast preset, same seed)

## Files touched

- `src/projected_grpo/regrade_pool.py` — extend to 4 booleans + co-occurrence
- `src/projected_grpo/pairs_from_pool.py` — NEW, builds pairs from cached pool
- `src/projected_grpo/extract_vhack_grad.py` — `--pairs-from-pool` flag
- `src/projected_grpo/train.py` — per-rollout 4-boolean log, final BLUF lines
- `src/projected_grpo/rewards.py` — already has C and D; add E (eq_hinted) and
  the surface for the upstream `eq_correct` (already computable from gt_pass)

## Out of scope

- Paper's qualitative 6-bucket detectors (passthru/print-only/etc.) — would be
  extending the paper, not using it. Defer until upstream's 4 booleans prove
  insufficient.
- vLLM for faster pregen — defer.
- Mixed-variant prompts (overwrite_tests + modify_tests + ...) — would give
  more mechanism diversity but requires re-training the teacher LoRA, much
  bigger scope.
