Files
poimen/tasks/T6.6-held-out-split.md

106 lines
4.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# T6.6 — Held-out split
| Field | Value |
|---|---|
| Phase | P6 — Learning loop |
| Size | M — 1 to 3 days |
| Status | Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | — |
## Goal
Partition tasks into selection and held-out sets. Promote on selection, report
held-out without optimizing against it.
## Facts (inlined — no spec read needed)
- Selecting on a fixed set of recorded tasks overfits to those tasks, **silently**:
shadow scores improve while live performance does not.
- **No promotion gate takes held-out as an entry criterion.** A gate that reads
held-out has converted it into a second selection set and left nothing
measuring generalization.
- **The proposer reads the selection set only.** If the slow loop consumes
held-out failures to generate candidates, the held-out set is contaminated
through the generator instead of the selector. **This leak is easy to introduce
and invisible once present** — which is why the audit test is the control, not
a nicety.
- A widening selection-versus-held-out gap is the overfitting alarm.
- Under one-state convergence there is no competing variant whose divergence
would reveal a stalled loop, so **the held-out report is also the only stall
detector there is**. It must be produced and surfaced even when no promotion is
pending.
- Held-out catches overfitting to *episodes*. It does not catch drift in the
*task mix* — that is T6.7.
## Steps
1. Assign each `TaskId` to selection or held-out by a deterministic hash of the
id, so the partition is stable and needs no stored membership list.
2. Expose two separate query surfaces — `selection_tasks()` and
`heldout_tasks()` — rather than one with a flag. A flag defaults wrong.
3. Give the proposer access to the selection surface only, at the type level if
possible.
4. Compute the held-out report on a **schedule**, independent of promotion
activity, and surface the selection-versus-held-out gap as a metric.
5. Write the audit test: enumerate every proposer input and every gate
evaluation, assert no held-out `TaskId` appears in either.
6. Keep the audit test running in CI permanently. It is the only thing that will
notice the leak.
## Acceptance
- An audit test asserting **no held-out `TaskId` appears in proposer input**, and
none in any promotion-gate evaluation. This leak is invisible once present, so
the test is the control.
- The held-out report is **emitted on a schedule, not only at a gate** — asserted,
since under one-state convergence it is the only stall detector there is.
## Verify
**Harness:** an interception layer recording **every** `TaskId` that reaches the
proposer and every `TaskId` read during a gate evaluation. The audit is the
deliverable — it is the only thing that will ever notice this leak.
**Integration test**`tests/it_heldout_audit.rs`:
1. Partition a corpus of 1000 `TaskId`s. Assert the split is deterministic:
recompute it in a second process and assert identical membership.
2. Run a full generate-and-promote cycle with interception on.
3. Assert **no held-out `TaskId`** appears in the recorded proposer input set —
intersection with the held-out set is empty.
4. Assert **no held-out `TaskId`** appears in any gate evaluation.
5. Assert the two query surfaces are distinct types or distinct functions —
`trybuild` a call that tries to pass held-out tasks into the proposer.
6. **Schedule:** advance simulated time with **no promotion pending**; assert the
held-out report is still emitted. Under one-state convergence it is the only
stall detector there is.
7. Assert the selection-versus-held-out gap is emitted as a metric.
8. Negative control: deliberately wire a held-out task into the proposer in a
test-only build; assert the audit **fails**. An audit that never fails is not
a control.
**Command:** `cargo test -p loop heldout_audit`
**False pass:**
- Steps 34 passing because the interception layer only sees one of several code
paths into the proposer. Assert the interception is at the single chokepoint,
or the audit measures nothing.
- Step 8 omitted. This is the most important one: an audit test that has never
been observed failing may simply be looking in the wrong place, and the leak it
guards is invisible once present.
- Step 6 omitted, so the report is computed only at gates — exactly when a
stalled loop produces none, which is when it is needed.
## Traps
- A single task query with an `include_heldout: bool`. Someone passes `true`.
- Computing the report only when a promotion is pending, which is exactly when a
stalled loop produces none.
---
Background (not required to do this task):
[rust-agentic-sys.md](../../../rust-agentic-sys.md) §12.1, §12.3, §12.5, §15 ·
[rust-agentic-task.md](../../../rust-agentic-task.md)