Release scope

August 27, 2026 · View on GitHub

Grounded in git diff v4.0.0..<candidate> -- src/ (41 files, +1130/−146) and the commit log, not a recollection — the 4.0.0 Nachtrag lesson: a scope list that omits what actually ships is the defect it exists to prevent. Honest limit: the categories below are read from the commit subjects + the file-level diff; re-confirm the src diff against the final merged HEAD before the tag.

In

Headline feature — reported-version status (the MAJOR-carrying user-facing change)

  • Every provenance field carrying a harness-reported version (harness_version, task_version, promptfoo_version) now also carries <field>_status ∈ {reported, not_reported, not_bound} with a <field>_status_reason mandatory when the status is not reported. The absence of a version field stops meaning two things at once (66e72f4). Backwards compatible: the version field itself is unchanged, the status is never derived, not_reported never folds to PASS. Verifier version_status_issues() rejects an unknown literal / missing mandatory reason / status-field contradiction; conformance vectors conformance/provenance/version-status-*. Touches outcome.py, verification_summary.py, the provenance verifier, and the harness adapters (promptfoo binds harness_version — the class, not the instance, 529cd20; inspect_hook.py/pytest_plugin.py/hf_evals.py).

Never-raise / fail-closed hardening across the verify surfaces (the makellose-500 work)

A sweep closing "a public verify surface raises a raw exception on a malformed producer argument instead of ruling" as a CLASS, not per-instance — each fix is fail-closed (typed refusal, never a false accept), landed with property/mutation tests:

  • renewal.py (+233, the largest): non-int .time / giant-int magnitude / malformed signed fields reach a fail-closed refusal instead of a raw crash; a _never_raise_verdict decorator makes verify-sequence never-raise by construction (6aef259, iter9 L2; earlier 38a672a/880ca55).
  • merkle.py (+43): verify_consistency binds second_size (sn==0 not fn==0, 681bfe6).
  • kbjwt.py (+41), sdjwt.py/sdjwt_issue.py/sdjwt_vc.py, tlogproof.py (+27), statuslist.py, relation.py/relation_statement.py, persample.py (+39), policy_profiles.py, run_ledger.py, public_transparency.py: the same never-raise / type-confusion class closed on each (26a42be sdjwt-vc P0 false-accept + rp_headers TypeError; f403360/58403ef/2da2644 anchors isinstance(Mapping) proves no usable .get; 7581bd7 the three surviving never-raise findings).
  • Membership guard (_membership.py): 27 membership tests hashed attacker data — fixed as the class, not the 27 lines (fd84e1d); and the guard whose contract is "never raises" could raise — closed (8ef363f).
  • Wire-form (d478882): a signed receipt has again EXACTLY one accepted wire form.

Pre-tag ceremony + release infrastructure shipped in this tag (not library API — recorded for completeness)

  • Pre-tag receipt ceremony option C: subject_tree_digest binds the HEAD top-level tree minus audit_artifacts/ — binds src + pyproject (deps) + scripts, keeps the receipt committable, closes a post-signing dependency-injection supply-chain hole (deep-gate refuted the src-only option B).
  • Two-half keyless receipt mode (pre_tag_receipt.py --emit-payload / --assemble) for the Mac key-custody handshake — the private release key never reaches the build host.
  • P1-A: the pre-tag gate no longer crashes on import proofbundle when run bare (the CI condition), fail-closed on any verify exception.
  • Gate-qualification harness hardening (cc-classes + AST-count meta-tests; option-C dep-injection regression).
  • audit_candidate_matrix version_pin: re-baselined to 5.0.0 at the vor-tag (P3-3). The deep-gate L6-01 fix is the BINDING (version_pin_binding detects a pin/package drift and withholds readiness with a named reason + nonzero exit) — and because that binding is now in place, pulling the literal to the shipping version is the correct release step, not the recreated defect the docstring warned about (that warning was against bumping the literal without the binding). The drift the binding catches is now exercised SYNTHETICALLY (test_drift_withholds_readiness monkeypatches an older pin) instead of relying on the live 3.6.0-vs-5.0.0 mismatch; a 5.0.0 release_evidence_slots entry is added. C12.1 (the signed pre-tag receipt) stays FAIL until the Mac key-custody handshake at tag time — expected, not a defect.

Out (explicitly deferred, no semantic change withheld from a caller who needs it)

  • Zenodo academic Technical-Note deposit — a separate scholarly artifact with its own DOI track (10.5281/zenodo.21230466), owner-gated, NOT part of this software release (Owner G1 = A, 2026-08-27). The software concept DOI (10.5281/zenodo.21110642) IS minted automatically with the release, as for every release of this repo.

Why MAJOR (the SemVer question, answered) — TWO independent triggers, both recorded

  1. Exit-code contract change. An input class that previously exited 2 (malformed/usage) now exits 1 (crypto failure/verdict). SPEC.md is normative (RFC 2119); a declared exit-code contract is public API → SemVer MAJOR (Owner decision 2026-08-18).
  2. A threshold is now required for a verdict. The Inspect lifecycle hook and the pytest plugin previously defaulted PROOFBUNDLE_THRESHOLD to "0" (passed for any non-negative score). Both now require it and skip emission when unset — a caller that relied on getting a receipt gets none. Migration: export PROOFBUNDLE_THRESHOLD=0.

No verdict flips: nothing that verified before stops verifying, nothing that failed starts passing. The never-raise sweep only turns raw crashes into typed refusals — no input that verified now fails. The 4.x line ends here (no maintenance branch, per SECURITY.md).