Releases: CodeReclaimers/benchstone
Releases · CodeReclaimers/benchstone
Release list
v0.3.1 — adversarial-review iter-1 closure (sdist fix)
[0.3.1] — 2026-05-22
Fixed
- sdist no longer includes maintainer-local working files. Hatchling's
default sdist target picked up any non-gitignored files in the project
root; pre-0.3.1 the.idea/,RESEARCH.md, andadversarial-loop/
directories were all bundled into the published tarball. Adds an explicit
[tool.hatch.build.targets.sdist].excludelist. The wheel was always
clean (onlybenchstone/source under[tool.hatch.build.targets.wheel]),
so this affects sdist consumers only. Tagging the v0.3.0 commit early
meant the fix landed in v0.3.1; v0.3.0 was never published to PyPI.
[0.3.0] — 2026-05-22
This release closes the six CONFIRMED findings from the first adversarial-review
iteration against benchstone's integrity-claim cluster, plus three deferred
quick-wins from the same iteration and the structural keystone fix that
subsumes three more. Several are breaking changes; the minor version bump
reflects that.
Added
STALEverdict kind and a tier-orthogonal freshness guard on
bench evaluate/api.evaluate. The guard checks, in order:
the working tree'sgit status --porcelain(dirty?), the manifest's
corpus_hashagainst the on-disk corpus, and a newworking_tree_hash
against the most recent candidate run at the current SHA. Mismatch
on any check returnsVerdict(kind="STALE", category="freshness")
with exit code5. Pass--allow-dirty(CLI) /allow_dirty=True
(API) to bypass. Skipped for correctness-tier benchmarks.bench evaluate --allow-dirtyflag, mirroringbench run --allow-dirty.project.tracked_pathsmanifest field (default["bench/"]).
Declares the project-relative paths whose byte content is part of the
benchmark's input surface. Hashed by the runner on each dispatch,
re-hashed by the freshness guard at evaluate time. Projects that
import fromsrc/(or anywhere else) should extend the list.benchstone.tree_hashmodule withworking_tree_hash(project_path, tracked_paths) -> "sha256:...". Deterministic recursive hash;
excludes well-known byproducts (__pycache__/,*.pyc,*.pyo,
.pytest_cache/,.mypy_cache/,.ruff_cache/,.DS_Store) and
symlinks.benchstone.api.check_freshnessas a public helper for external
callers (custom dashboards, alternate CLIs).runs.working_tree_hashcolumn. Nullable; pre-existing rows are
treated as "no hash to compare against" by the freshness guard.baselines.run_idscolumn (JSON array of run IDs). The baseline
pointer now records the exact set of runs that constitute its
snapshot, not a query specification that future inserts can expand.
See "Changed" → baseline snapshot semantics.Store.fetch_runs_by_ids(run_ids)method — primary read path for
baseline snapshots.
Changed
- Baseline pointer records a run-ID snapshot (the v0.3.0 keystone).
Baseline.run_idsis the authoritative selector for the baseline
distribution.compute_verdictreads runs by ID, not by
(git_sha, meta_seed)query. Closes CONFIRMED-5 structurally
(subsequent inserts at the same SHA cannot expand the baseline),
closes F-PROMO-FLOOR (promoted-group size disconnect from manifest's
baseline_seeds), closes F-MANIFEST-EVOLVE (editing the manifest's
baseline_seedsno longer invalidates historical baselines), and
closes F-META-RERUN (re-running with the same meta_seed inserts new
rows but the snapshot stays fixed). Legacy baselines (pre-0.3.0,
run_ids=NULL) fall back to the query-based lookup unchanged. - Gate baseline floor is now hardcoded 2 (the math minimum for a
sample variance), down frommax(len(benchmark.baseline_seeds), 2).
Sample-size intent is recorded in the snapshot at establish/promote
time; the gate doesn't re-derive it from the live manifest. Strictly
more permissive than the old behavior for any sample with ≥ 2 ok
rows. Candidate floor unchanged (max(repetitions, 2)). - Append
AND git_dirty=0to both branches of
Store.fetch_baseline_runs. Dirty rows remain in the timeline
(fetch_runsis unchanged) — they no longer count as baseline. This
closes the INV-05 baseline-poisoning vector where an agent with
--allow-dirtyaccess could inject additionalmeta_seed=NULLrows
at the baseline SHA and shift the gate's view of baseline. With the
keystone snapshot, this filter is also redundant for non-legacy
baselines (the snapshot doesn't include the injected rows in the
first place), but it remains as defense-in-depth and as the active
protection for the legacy-fallback path. api.compute_verdictgains optionalcandidate_meta_seed: int | None = None. Default behavior unchanged (still "latest meta_seed
group at this SHA"). Non-None pins the candidate fetch to that group.api.promoteandbench promoteevaluate the gate against the
chosen meta_seed group before moving the baseline pointer, then
record the chosen group's run IDs as the new baseline snapshot.
Previously,promote --meta-seed Nvalidated only that group N
existed and moved the pointer using whichever group was latest at
the SHA — letting an explicit--meta-seedride on a different
group's PROMOTE verdict.api.check_freshnesssignature changed to takeproject: Project
(new first arg, fortracked_paths) and an optionalstore: Store | None = Nonekwarg. The function was added in the same release cycle
so no realistic external caller exists yet.references.freeze/references.replaceordering inverted —
history event is appended before the reference JSON is atomically
written. If_writefails after the history append,get()walks
history newest-first on every read and raisesReferenceErrorwhen
the most-recent install event'scontent_hashdisagrees with the
live JSON's. Catches the crash-mid-write half-state.
Fixed
- CONFIRMED-1:
bench evaluate/api.evaluate/compute_verdict
now refuse withSTALEwhen the working tree is dirty, the corpus
has drifted fromcorpus_hash, or thetracked_pathsbyte content
has changed since the most recent candidate run. Previously emitted
the gate's verdict against stored runs without checking the on-disk
state. - CONFIRMED-2: gitignored modifications under
tracked_pathsno
longer slip past the freshness check. The legacygit status --porcelainview (which omits gitignored entries) is now
complemented by the byte-levelworking_tree_hashcomparison, so
payloads written to.venv/,__pycache__/, or any
project-gitignored path undertracked_pathsproduceSTALE. - CONFIRMED-3:
promote --meta-seed Nnow re-evaluates the gate
against group N before moving the baseline pointer. Previously a
REJECT group could become baseline via the verdict of a separate
PROMOTE group inserted later at the same SHA. - CONFIRMED-4:
bench run --seed-set baselineandbench baseline establishare refused for correctness-tier benchmarks (the
invocations produced rows withmeta_seed=NULLthat
fetch_candidate_runscouldn't see, leaving the artifact invisible
toevaluateand the defaultfreeze-referenceresolution). - CONFIRMED-5: baseline poisoning via
--seed-set baseline --allow-dirtyat the baseline SHA. Closed structurally by the
run-ID snapshot (the gate doesn't read additional rows beyond the
snapshot) and defense-in-depth by the dirty-row filter. - CONFIRMED-6: untracked-only dirty trees no longer have
effectively-no audit content. Theworking_tree_hashis itself the
audit primitive for "did the relevant surface change since the
run?"; the existinggit diff HEADcapture remains unchanged for
the human-inspection use case. - F-NONFINITE:
ProjectResult.from_jsonrejectsNaN/
±Infinitymetrics at the protocol boundary. The runner's existing
ProtocolErrorhandler converts the violation into a loud
status='error'row instead of silently shrinking the sample (NaN
→ SQL NULL whilestatus='ok') or producing nan/inf statistics
(Infinity → mean_se → directed_z). - F-REF-ATOMIC:
references.freezeandreferences.replaceuse
history-first ordering (see "Changed"), andreferences.getraises
ReferenceErrorwhen the live reference'scontent_hashdoesn't
match the most recent history event's recorded hash. - F-REF-REVALIDATE:
references._require_artifactrehashes the
archived bytes and compares torun.artifact_hashbefore freeze /
replace. Catches out-of-band corruption (bit rot, manual tamper,
cleanup script) of the content-addressed store. - F-PROMO-FLOOR / F-MANIFEST-EVOLVE / F-META-RERUN: subsumed by
the run-ID snapshot keystone (see "Changed"). The baseline's
sample-size is determined at establish/promote time, not re-derived
from abaseline_seedsquery at evaluate time.
Breaking changes
- New exit code
5forSTALE. Consumer scripts that previously
only branched on 0/1/2/3/4 should add a 5 case (refuse, ask the
user, re-run, whatever the appropriate response is for the
consumer). Defaultbench evaluatebehavior on a clean tree is
unchanged. bench run --seed-set baselineon correctness-tier benchmarks
now raisesRunnerError. No callers in the test suite used this
combination; external callers (if any) need to switch to
--seed-set fresh(the default).bench baseline establishon correctness-tier benchmarks now
raisesRunnerErrorfor the same reason. Usebench run+bench freeze-referenceinstead.promote --meta-seed Nnow refuses when group N's standalone
verdict isn'tPROMOTE. Previously succeeded silently using a
different group's verdict. Pass--forceto bypass the gate (same
as before, just now actually applied to the chosen group).api.check_freshnesssignature changed (see "Changed"). Added
in the same re...