ADR-107: Batch verification — amortise the per-PR Gate A env-load across K proofs

Field Value
Decision ID ADR-107
Initiative throughput / verification capacity (D1b)
Proposed By unsorry maintainers
Date 2026-06-26
Status Proposed

Context

A 2026-06-26 scaling audit (#6751, building on the throughput roadmap #5678) found that unsorry is no longer verification-compute-bound — Gate A runs with slack (~8 in-flight vs the ~20 governor cap) — yet a 344-deep queue persists, ~336 of it a single solver’s (@ohdearquant) homogeneous template-ring-cofactor flood. The lever to drain it is not more gate capacity but spending the existing budget more efficiently.

Gate A’s cost is dominated by a fixed, per-PR environment load: restoring the ~12–20 GB mathlib cache and building the existing library (gate_a_prepare). That cost is paid once per PR, independent of how many new proofs the PR adds — each additional proof is only a cheap incremental lake build compile, a seconds-long nanoda leaf check (ADR-097), and its replay slice (ADR-063). So today the swarm pays the dominant fixed cost once per proof (one PR per proof), when it could pay it once per K proofs.

This is item D1b on the roadmap (issue #5683; the issue’s reserved “ADR-092” was lost to the ADR-number race — that number is now ADR-092-Segregated-Benchmark-Track — so this lands as ADR-107). It is the queue-drainage lever called out in #6751 §5.

Two facts make batching cheap and safe to add:

Decision

Add an opt-in dispatcher mode that combines up to K independent queued queued/prove/* branches into ONE batch/prove/<hash> PR, so Gate A pays its env-load once and verifies all K together. Controlled by UNSORRY_BATCH_SIZE (default 1 = today’s one-PR-per-proof behaviour; the batch code never runs until an operator raises it).

  1. Selection (pure, tools/dispatch/batch.py). From the queued branches already ordered by ADR-075/106 (fair_dispatch_order), pick up to UNSORRY_BATCH_SIZE whose changed-file sets are pairwise disjoint and whose goals are not already proved / in an open PR (singleton or batch) / picked this pass. Disjointness is checked defensively even though distinct goals guarantee it; any colliding branch is dropped back to singleton dispatch. The batch branch name is a deterministic sha-256 of the sorted goals (idempotent re-runs; distinct sets never collide).

  2. Assembly (swarm/agent.sh). Branch batch/prove/<hash> from origin/main, cherry-pick each constituent branch’s tip prove commit (disjoint ADD-only ⇒ never conflicts; preserves the prove(<goal>): subject + author), Gate-B-validate the combined tree, push, open ONE PR.

  3. Merge with a MERGE COMMIT, not squash. A batch is enrolled with gh pr merge --auto --merge (vs --squash for singletons). This keeps each constituent’s original prove(<goal>): commit in main’s reachable history, so the leaderboard’s merge_times (git log --no-merges) and git_add_authors (git log --diff-filter=A -- library/index, both without --first-parent) resolve every batched proof’s merge-hour and author with no leaderboard change. A squash would collapse the K subjects into one prove-batch(…) and erase per-proof merge-time/author resolution. (Branch protection permits this: required_linear_history=false.)

  4. Governor accounting. A batch is one admission: submission_governor_allows is consulted once, and a batch consumes one gate-a run (the binding UNSORRY_MAX_GATE_A_IN_FLIGHT meter) for K proofs — exactly the amortisation. Its prove-batch( title is deliberately distinct from prove(, so the open-prove-PR count and ADR-105’s same-repo arm/rerun janitor treat it as its own kind.

  5. Dedup. While a batch PR is open, its goals are published in a Batch-Goals: manifest line in the PR body; the dispatcher adds those to its dedup set (dispatch_open_batch_goals) so it never opens a redundant singleton for an already-batched goal.

  6. Failure handling (batch-recovery-janitor.yml). A batch is all-or-nothing per PR, so one bad proof reddens all K. Recovery (a janitor mirroring ADR-105, with the pure decision in batch.recover_action) classifies each open batch PR’s latest gate-a:

Consequences

Alternatives considered

References

ADR-058 (governor caps / in-flight meter — a batch = one admission), ADR-075/106 (dispatch ordering the batch fills from), ADR-064/071 (dispatch dedup — extended to batch PRs), ADR-005 (autonomous merge — batches enrol with --merge), ADR-105 (finalization recovery — recovery pattern + the gate_failure_is_cancellation classifier reused here; its arm/rerun janitor skips prove-batch( titles, so batch recovery is owned by the new janitor), ADR-049 (p=1, kernel sole oracle — unchanged), ADR-097/063 (nanoda leaf check + sharded replay — the cheap per-proof terms), ADR-018 (goal-statement immutability — batching adds files only). Roadmap #5683 (D1b), #5678, audit #6751. SPEC-107-A specifies the module, the assembly, and the recovery janitor.