All notable changes to this project will be documented in this file.
The format is loosely based on Keep a Changelog, and this project adheres to Semantic Versioning.
Pre-0.3.0 changes are not enumerated here; consult git log for that history.
- 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.
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.
STALEverdict kind and a tier-orthogonal freshness guard onbench evaluate/api.evaluate. The guard checks, in order: the working tree'sgit status --porcelain(dirty?), the manifest'scorpus_hashagainst the on-disk corpus, and a newworking_tree_hashagainst 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.
- 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'sbaseline_seeds), closes F-MANIFEST-EVOLVE (editing the manifest'sbaseline_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 from
max(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 ofStore.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.
- CONFIRMED-1:
bench evaluate/api.evaluate/compute_verdictnow 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=NULLthatfetch_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. The
working_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 existingProtocolErrorhandler converts the violation into a loudstatus='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.getraisesReferenceErrorwhen 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 a
baseline_seedsquery at evaluate time.
- 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 release cycle; no production callers expected yet.- Schema migration: two new columns —
runs.working_tree_hash TEXTandbaselines.run_ids TEXT. The migration runs automatically on firstStore.__init__against an existing database. Pre-existing rows get NULL; both code paths have documented fallback behavior for that. - Gate baseline floor dropped from
max(len(baseline_seeds), 2)to a hardcoded2. Strictly more permissive than before; consumer scripts that branched onNEEDS_MORE_DATAfrom baseline-undersize may see PROMOTE/REJECT where they previously saw NEEDS_MORE_DATA. Intentional — the prior floor was a footgun (manifest evolution would brick gating). - Manifest schema:
project.tracked_pathsfield added. Backward-compatible (default["bench/"]); manifests that omit it behave identically to before for projects with abench/directory. - References JSON write ordering inverted (history first, then reference JSON). Old half-state recovery scripts (if any) that assumed write-first ordering need to flip their assumption.