Skip to content

feat(capture): bounded capture suites (captures.max_age / captures.max_cases) - #57

Open
babaliauskas wants to merge 26 commits into
mainfrom
feat/bounded-capture-suites
Open

babaliauskas wants to merge 26 commits into
mainfrom
feat/bounded-capture-suites

Conversation

@babaliauskas

Copy link
Copy Markdown
Collaborator

What this changes

Adds two optional settings under captures: in evalshift.yaml, max_age (e.g. 14d) and max_cases (e.g. 200). Together they keep each captured suite a capped, route-balanced sample of recent production traffic on defaults.source_model.

  • capture sync filters. It promotes only captures recorded within max_age and on the source model. Model ids match on bare names: provider prefix, case and snapshot date are ignored. A per-suite guardrail turns model filtering off, with a warning, when no case or capture was recorded on defaults.source_model, so a typo or alias can never empty or freeze a suite.
  • capture sync plans and retires. With max_cases set, the pure plan_suite fills free slots route-balanced, in sha256 order. Conversations enter whole. Stale cases (too old, or recorded on another model) leave only to make room, or when a fresh copy of the same question is added. Over-cap suites are trimmed from the largest route, oldest first. Cases marked provenance: reviewed (in the case file) are never retired. A retirement deletes the exact file the case was read from. The same captures give the same suite on any machine, and a re-run with nothing new touches no file.
  • Fetch window. max_age bounds what sync downloads from captures.store. --since still overrides it.
  • Provenance. Case files record captured_at / source_model, and sync backfills them on existing cases whose capture is still local.
  • Freshness checks. doctor gains a suite freshness row per captured suite and a captures limits nudge (store set without max_cases). compare prints the same findings above the verdict, including for the auto-selected suite, so a CI-only team sees its suite went stale.
  • init and the example. init scaffolds max_age: 14d, max_cases: 200. examples/capture-first was regenerated with this CLI.
  • Opt-in. With neither limit set, sync keeps and promotes exactly what it did before. defaults.source_model alone changes nothing.

Why

The SDK's 200-per-suite cap applies to local disk only, and the bucket keeps everything. A team that captures thousands of records a week, deletes its local mirror and runs capture sync got every record ever captured, old models included, and suites only grew. This bounds suite size and keeps the suites fresh.

Design spec: docs/superpowers/specs/2026-10-16-bounded-capture-suites-design.md. The plan's "Execution notes" section lists where the shipped behaviour deviates from the plan: docs/superpowers/plans/2026-10-16-bounded-capture-suites.md.

Compatibility. Older CLIs reject the new config keys and the new case-file fields. The first capture sync on this CLI writes the fields even without limits, so everyone running capture commands, including a pinned CI action's evalshift-version, should upgrade together. CI's compare reads only golden.jsonl, whose format is unchanged. CHANGELOG has the details.

Test plan

  • pytest passes: make ci shows 3267 passed, 1 skipped (the existing evalshift-server skip), coverage 95.15%.
  • ruff check, ruff format --check clean; pre-commit run --all-files clean.
  • mypy --strict src/evalshift_cli clean.
  • CHANGELOG.md updated under ## [Unreleased].
  • Docs updated: DOCS.md, llms-full.txt, docs/configuration.md (new "Bounded suites" section), getting-started, sdk, conversations, agents, and examples/capture-first.
  • Integration test tests/integration/test_capture_lifecycle.py: weeks of bucket captures stay one bounded, fresh suite; the fresh-checkout fetch is pinned; production model migration; the wrong-source-model guardrail on new and established suites.
  • Offline smoke on a scratch copy of a real project (3 suites, 31 cases):
    • First sync with 14d/200: backfill only.
    • Re-sync: nothing changed.
    • max_cases: 5: each suite trimmed to 5.
    • Re-sync after the trim: byte-identical.
    • doctor: ok / warn freshness rows as expected.
  • Online fetch against a real bucket (not run: the AWS session had expired). The fake-bucket integration test covers it.

Screenshots

✓ promoted 0 capture(s) into 3 suite(s), skipped 31 already-promoted (use --force to overwrite).
  converter: 5 cases — kept 5, retired 6 (6 over max_cases), added 0 of 0 eligible
  planner: 5 cases — kept 5, retired 6 (6 over max_cases), added 0 of 0 eligible
  triage: 5 cases — kept 5, retired 4 (4 over max_cases), added 0 of 0 eligible
 ✓  suite freshness     converter: 5 cases, captured 1d–1d ago
 ✗  suite freshness     planner: newest case captured 1d ago (captures.max_age 1h); refresh with evalshift capture sync

🤖 Generated with Claude Code

babaliauskas and others added 26 commits October 16, 2026 21:53
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te once

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cap

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…limits per spec

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stablished suite

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e room

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A huge max_age ("999999d", a plausible way to write "no limit") passed
validation, then overflowed the cutoff capture sync, doctor and compare
compute from it. The validator now caps it at 100 years and says to omit
max_age for no age limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fter it

capture sync planned and deleted bounded-suite cases by their "name"
field. A copied case file that kept its name redirected a retirement to
the original (deleting a reviewed case), and a renamed file was never
deleted while every sync still reported it retired.

- iter_promoted_case_files returns each case with its path; sync plans by
  file stem and retire_promoted_case(path) deletes exactly that file.
  A deletion that fails is reported and not counted in the suite line.
- Backfilled provenance is rewritten in the file it was read from.
- A capture whose case file name is already taken is never planned; a
  planned write that fails is not counted as added, and sync re-plans
  around what was written so its stale twin stays.
- Dedup seeds the content of every case that stays, so a replaceable case
  that shares content with a reviewed one cannot let a fresh copy in.
- A suite synced only because its directory holds case files is rebuilt
  but wired only when it already has a suites: entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A case recorded on another model is replaced only with max_cases set;
  without it the finding says to set captures.max_cases.
- An over-cap suite is trimmed only down to its reviewed cases; the
  finding says so when they alone exceed max_cases.
- capture sync --help describes bounded suites instead of "promote every
  capture".
- The doctor row comment names the real trigger (store without max_cases).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Captured suites take provenance: reviewed in the case file
  (.evalshift/suites/<suite>/<case>.json), not golden.jsonl, which sync
  regenerates; hand-written suites keep the golden-row guidance.
- captures.max_age is at most 36500d; omit it for no age limit.
- Retirement targets the case file; a failed delete is reported; a suite
  synced only from its directory is not wired back in; dedup keeps the
  claim of every case that stays.
- Fetch window: a conversation straddling it loses earlier turns on a
  fresh checkout; pass the full messages list to capture.model_call.
- Freshness messages quote the conditional wording.
- CHANGELOG: the first sync writes captured_at / source_model even without
  limits, so everyone running capture commands upgrades together; a suite
  never shrinks because its cases went stale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…res: block

A fresh init now scaffolds captures.max_age / max_cases, and the
example's demo captures (recorded 2026-09-08 on a placeholder model)
are all older than max_age, so following the README promoted nothing.

The README now has the reader delete the captures: block init writes,
says why, and shows what sync prints with it left in. The config and
promoted case files are regenerated by replaying the walkthrough with
this CLI (case files gain captured_at / source_model; the config picks
up init's max_invariant_violations line it had drifted from). The drift
test now also checks the config outside the managed region against
init's output minus CAPTURES_SCAFFOLD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant