Skip to content

Release v5.0.0 - #153

Merged
b7n0de merged 54 commits into
mainfrom
fix/non-json-wireform-coverage-20260826
Aug 27, 2026
Merged

Release v5.0.0#153
b7n0de merged 54 commits into
mainfrom
fix/non-json-wireform-coverage-20260826

Conversation

@b7n0de

@b7n0de b7n0de commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Release v5.0.0

Release-prep PR for v5.0.0 (the tag comes after this merges, per RELEASE.md).

What ships

Full scope list: docs/release_scope/5.0.0.md. In brief:

  • Reported-version status (the MAJOR-carrying feature): every harness-reported version field also
    carries <field>_statusreported/not_reported/not_bound with a mandatory reason — the absence
    of a version field stops meaning two things at once.
  • Never-raise / fail-closed hardening across the verify surfaces (renewal, merkle, kbjwt, sdjwt,
    tlogproof, anchors, statuslist, …): raw crashes on malformed producer arguments become typed refusals,
    closed as a class with property/mutation tests. No verdict flips.
  • Pre-tag ceremony: option-C subject_tree_digest (binds src + pyproject + scripts minus
    audit_artifacts/, closes a post-signing dependency-injection hole), the two-half keyless receipt mode
    (the private release key never reaches the build host), and the P1-A gate import-robustness fix.

Why MAJOR

Two independent triggers, both recorded in the CHANGELOG: an input class that exited 2 now exits 1
(the exit-code contract is public API), and a threshold is now required for a verdict (the integrations
skip emission when PROOFBUNDLE_THRESHOLD is unset — migration is one line: export PROOFBUNDLE_THRESHOLD=0).
No verdict flips — nothing that verified before stops verifying.

Provenance

Adversarial deep-gate: WITHSTANDS on the ceremony code (audit_artifacts/500/DEEP_RUN_RECORD_500_ITER8_CEREMONY.md,
all five falsification targets held with executable proofs). Three-file version single-sourced at 5.0.0;
check_version_and_changelog green. The signed pre-tag receipt (produced at tag time via the key-custody
handshake) binds the exact tagged tree; pre_tag_audit_gate.py --strict in release.yml gates the build on it.

kraxo and others added 30 commits August 23, 2026 00:08
…8-22)

1. capture_mechanism in provenance: a hook-emitted receipt and a later
   reader-emitted one were byte-indistinguishable. Three named values:
   lifecycle_hook (live data.log), lifecycle_hook_log_reread (eval_set
   header-only fallback), persisted_log_reader (default for the adapter).
2. No silent threshold default of '0': it made passed=true vacuous for
   any non-negative score (measured live: a run scoring mean 0.0 emitted
   passed=true). Integrations now REQUIRE PROOFBUNDLE_THRESHOLD and skip
   with a clear message otherwise; the published claim schema (threshold/
   passed required) is untouched.
3. lm-eval adapter binds harness_version from the results file's
   lm_eval_version field: promptfoo and inspect bound their versions,
   lm-eval bound neither (asymmetric binding a verifier cannot see).

6 new tests; full suite 2294 passed, 7 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Wmxwc6JuyUjrD352pgo6a
From the adversarial re-check (2026-08-22): show-eval verifies against
the pubkey EMBEDDED in the receipt, so a forged claim RE-SIGNED with a
fresh key passes rc=0 (self-attested scope). --expect-issuer compares
the verified signer (decode_eval_claim binds issuer == signature key)
against caller-pinned issuer(s); repeatable for key rotation; opt-in
and fully backwards compatible. Mismatch fails with a clear message
and exit 1 — the exact forgery scenario is a test.

3 new tests; full suite 2297 passed, 7 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Wmxwc6JuyUjrD352pgo6a
Owner decision 2026-08-25 (card OA-5c8a99e19c, option B): the number is 5.0.0, not 4.1.0.
The change itself stands unchanged and ships; only the number carrying it moves.

WHY MAJOR. SPEC.md is normative under RFC 2119 and documents exit 2 (malformed / usage) apart
from exit 1 (crypto failure / verdict). Commit 1a9c10e moves one input class -- simultaneously
over the merkle_path cap AND carrying invalid base64 -- from exit 2 to exit 1. A caller who
branches on those numbers takes a different branch from this release on, and Semantic Versioning
2.0.0 requires MAJOR for any backward-incompatible change to the declared public API. No verdict
flips: the affected input could never verify under any release.

SUPPORT LINE, and this is a choice, not an omission: the 4.x line ends here. No release/v4.0.x
maintenance branch is created and nothing will be backported. SECURITY.md serves the latest minor
of the CURRENT major line, so a MAJOR seven days after 4.0.0 takes that line out of service either
way. Announcing a maintenance branch nobody intends to serve would be a promise with nothing
behind it -- saying plainly that the line ends is the honest half of the same fact.

BUMPED IN ONE GO, and it is FIVE places, not three: pyproject.toml, src/proofbundle/__init__.py,
CITATION.cff, plus RELEASE.md and docs/readiness_pack/PROGRESS.md, which check_version_and_changelog
also tracks. The two extra ones were found by running that gate, not by reading the diff -- it
reported FAIL naming both. PROGRESS.md is covered by MANIFEST.sha256, so the readiness-pack manifest
and its advisory self-receipt were regenerated by scripts/readiness_pack_manifest.py --generate
rather than hand-edited. NOT touched: CITATION.cff lines 40/42 keep version 4.0.0 -- that is the
Technical Note with its own DOI (10.5281/zenodo.22004295), a separately versioned object; a blanket
substitution would have falsified a published citation.

WHAT THIS COMMIT DELIBERATELY DOES NOT CARRY: the pre-tag audit RUN record. Only the
PRE-REGISTRATION is here, with its targets frozen before any run. MEASURED 2026-08-25: all 22
adversarial gate receipts from 23.-25.08. run in mode NORMAL-3L3I; the most recent WITHSTANDS
(24.08. 11:40:41Z, strength FULL, runner-signed) carries topic=control_plane_phase01 -- it grades
control-plane work on a feature branch, in the NORMAL mode, and does not cover this release. A
release is the external/MAJOR mode by rule: DEEP, six lenses, up to seven iterations. Writing a RUN
record before the run is exactly the failure this project's gate exists to catch.

GATES, measured on this tree:
  check_version_and_changelog  OK -- 5.0.0 single-sourced, all tracked places current
  readiness_pack_gate          OK -- 5 conclusions, 4 slots
  pre_tag_audit_gate --strict  MISSING for 5.0.0 -- CORRECT, and it must stay MISSING until a
                               real DEEP run writes the canonical line
  pytest -k version/changelog/readiness/release  56 passed, 1 failed
                               (test_released_version_has_audit_record -- the same MISSING, and the
                               test is right; it is not a defect of this commit)

Owner-GO: local commit in proofbundle via b7_fremdrepo_commit.sh, no push, no tag.
…ope stops matching

PRE-TAG DEEP GATE finding L6-01 (P3), 2026-08-25, three lenses converged independently and two
confirmed the shipped false-green.

OBSERVED. scripts/audit_candidate_matrix.py pinned VERSION_UNDER_TEST = "3.6.0", evaluated
release_evidence_slots["3.6.0"] and pre_tag_audit_gate.evaluate(version="3.6.0"), and reported
audit_candidate_ready=True with exit 0 -- while the package ships 5.0.0, two majors ahead. A
release-readiness gate wired into CI was attesting readiness from evidence about a different
release, and nothing in the pipeline compared the two numbers.

NOT A BUMP, ON PURPOSE. Editing the literal to 5.0.0 would make this instance green and recreate
the class at the next version bump -- stale again, silently again. The gate's own remediation
says it in one line: "do NOT just edit the literal, that recreates the class at the next
release." What was missing is the BINDING.

THE PROPERTY, executable: a gate whose verdict is version-scoped binds its pinned version to the
shipping identity and fails closed on mismatch -- never a green row about another release.
version_pin_binding() answers in THREE states: bound (pin == proofbundle.__version__) · drift
(they differ, both numbers named in the reason) · not_determinable (package version unreadable,
explicitly NOT a pass -- an unmeasurable binding is not a verified one).

MEASURED, the gate's own reproducer:
  before  version_under_test=3.6.0  audit_candidate_ready=True   exit 0
  after   version_pin=drift(3.6.0 vs 5.0.0)  audit_candidate_ready=False  exit 1
The 33 obligations are untouched: 31 PASS, 1 DATA_BLOCKED, 1 EXTERNAL, 0 FAIL, exactly as
before. What changed is only whether that counts as readiness FOR THIS VERSION.

ANTI-PARITY, and it found a real defect in the fix itself: a guard that always fails proves
nothing. The test sets the pin equal to the shipping version and requires the verdict to be
unchanged by the guard. It failed -- because version_pin_binding() read the pin through a DEFAULT
ARGUMENT, which Python binds once at definition time, so the constant was frozen into the
signature and a later change never reached the check. A binding check reading a stale copy of the
thing it binds is the same class of defect it exists to catch. The pin is now passed explicitly.

FAMILY SWEPT in the same pass, as the class contract requires: the only other active version pin
candidates are comments (claims_hygiene_check.py:115, type_confusion_gate.py:4,
mutant_signature_guard.py:63, ci.yml:112) and src/proofbundle/trust_pack.py:458 -- the latter was
explicitly triaged per the gate's instruction not to assume it inert: it is a prose comment
naming 3.6.0 as historical context, not a pin. No second active member found. REGISTER_REL does
not exist as a constant; the findings-register revision is named in a comment.

Riegel: tests/test_audit_matrix_version_pin_binding.py, 8 tests -- both directions plus
"the guard is reached at all" (a guard nobody calls is not a guard).

ONE EXISTING TEST CHANGED, and it is worth naming why: test_audit_candidate_360.py asserted
audit_candidate_ready unconditionally. That assertion is what let the false green ship -- every
one of the 33 checks is genuinely fine ABOUT 3.6.0, so a test that asserts readiness without
asserting what it is readiness FOR cannot tell the two apart. It now asserts the obligations
unchanged (33 checks, 0 FAIL) and makes the top-level verdict conditional on the binding.

STILL RED, and correctly so: test_gegenrichtung_das_echte_repo_besteht_weiterhin and
TestF7PreTagAudit::test_released_version_has_audit_record both demand a pre-tag RUN record for
5.0.0. There is none, because the deep gate returned FIX_FIRST -- this finding. They go green
when a re-gate returns WITHSTANDS and the record is written, not before.

Owner-GO: local commit in proofbundle via b7_fremdrepo_commit.sh, no push, no tag.
… state is not a failure

Two P2 findings from the pre-tag deep gate on ab90699, both jury-confirmed, both genuine
recurrences of known classes on axes the existing class-fixes never covered.

── L2-BDOS-HUGEINT: the magnitude budget was inside one function ─────────────────────────────

MEASURED by the gate: verify_inclusion(b'x', 2**300000, 2**300000, [32 bytes], root) ran 3.334 s
and returned the CORRECT False; at 2**1000000 that scales to ~34 s. verify_bundle refuses the
identical magnitude in ~0.015 s. Correct verdict, unbounded cost -- which is what a DoS is.

WHY EVERY OTHER BUDGET MISSED IT: input_bytes, json_nodes and string_len bound STRUCTURE, and a
huge integer has none (2**1000000 is seven characters of source, one scalar node). The
merkle_path step cap passes too -- the attack needs a ONE-element proof. The cost is not in the
input, it is in the O(bit_length) shift loop the input drives.

WHY IT IS ONE FIX AND NOT THREE: bundle._require_int has carried an 8192-bit ceiling since the
earlier L2-BDOS-01 round -- as a LITERAL inside that function. The three exported surfaces that
take their integers as ARGUMENTS rather than from a dict never got it, for a month, on public
names. A bound that lives inside one function is not a bound anyone else can honour. It is now
`VerificationBudget.int_bits` plus the shared `int_magnitude_ok`, and all FOUR call sites read
the same one; bundle.py's literal copy is gone (a test asserts it stays gone).

MEASURED after the fix, against the gate's own oracle (fail-closed under 100 ms):
  verify_inclusion(2**1000000)       19.6 ms -> False   (was ~34 s)
  verify_consistency(2**1000000)      5.6 ms -> False
  verify_sample_opening(2**1000000)   8.0 ms -> ok=False, named detail
ANTI-PARITY, and it is why the file is worth having: a guard that refuses everything would pass
every timing assertion and destroy the library. verify_inclusion(leaf, 0, 1, [], root) -> True in
0.03 ms; 2**8000 (absurd but below the ceiling) is still accepted.

── L6-F1-SDIST-PYYAML: the test abolished the third answer ───────────────────────────────────

From an extracted sdist in a base-only install, tests FAILED instead of skipping, breaking the
invariant pyproject.toml documents (`pip install <sdist> && pytest` exits 0, skips permitted).

THE PRODUCTION CODE IS NOT THE DEFECT, and that is the interesting half: c1_1_two_ci_gates catches
the ImportError and reports an honest DATA_BLOCKED -- exactly the three-state discipline. The TEST
asserted PASS and turned the third state into a failure. A test that checks a three-state gate
with assertEqual against ONE state has abolished the third answer.

MEASURED with the import forced absent: SEVEN methods, not six as the gate reported -- all
c1_1-related. The marker covers the measured set.

MY OWN MEASUREMENT WAS WRONG FIRST, and the correction is the reason the number is trustworthy:
the first harness implemented only the legacy find_module protocol, so importlib.util.find_spec
answered "present" while `import yaml` raised. A test bench that gives two different answers to
the same question measures itself, not the situation.

Riegel: tests/test_int_magnitude_budget_family.py (10 Tests, Familie + Anti-Paritaet + "eine
Definition") und tests/test_sdist_selftest_optional_deps.py (4 Tests, BEIDE Richtungen -- ohne
die Abhaengigkeit skippen genau die sieben, MIT ihr skippt NICHTS; ein Marker, der immer skippt,
haette sieben echte Pruefungen stillschweigend abgeschaltet).

Gemessen: 50 Tests gruen in den vier betroffenen Dateien, 248 gruen in der Regression ueber
merkle/persample/bundle/budget.

EHRLICHE GRENZE: die sdist-Pruefung misst den SKIP-MARKER, nicht eine echte Basis-Installation --
die ist ein CI-Job und kann nicht innerhalb der Suite laufen, die sie prueft. Und die
Zeitmessungen sind Wanduhr auf dieser Maschine; die Schranke ist bewusst grosszuegig (100 ms
gegen gemessene 5-20 ms), was sie beweist ist die Abwesenheit des Schiebe-Loops.

Owner-GO: local commit in proofbundle via b7_fremdrepo_commit.sh, no push, no tag.
Two P2 findings from the third pre-tag deep gate (388a4d4), both jury-confirmed — and they are
the NEIGHBOUR of the class closed one round earlier, on an axis that sweep never asked about.

THE FIRST HALF was COMPUTE: a huge integer drives an O(bit_length) shift loop. The sweep that
closed it asked "which surfaces TAKE an untrusted integer as an argument" and fixed the three it
found. THIS half is RENDERING, and it is sneakier: CPython caps int->str at
sys.get_int_max_str_digits (4300, CVE-2020-10735), so interpolating an untrusted integer into a
DIAGNOSTIC raises a raw ValueError out of a surface whose contract forbids one. The value was
never used for a computation. It was used to explain why the input was rejected.

THREE PLACES, THREE CORRECT ANSWERS, and getting this wrong in either direction is a real defect:

  * EIGHT DIAGNOSTIC SITES get the new shared `render_safe`, which prints `<int, 16610 bits>`
    beyond the ceiling. Honest (it names what it is and how big) and useful, which 5000 decimal
    digits are not.
  * `token()` REFUSES the magnitude with a typed error instead of rendering it differently,
    because there the rendered string IS THE SIGNED MATERIAL. Shortening it would change the
    covered bytes and break every existing signature. A defensive abbreviation is worse than the
    crash it prevents when the string is the contract.
  * `verify_sequence` checks the magnitude BEFORE the token path and returns a VERDICT, because
    it is a never-raise surface. Left to token()'s refusal the error escaped as an exception --
    typed, but still an exception.

MY FIRST ATTEMPT AT THE THIRD ONE WAS WRONG and the reproducer caught it: it set `ordered = False`
like the type check beside it and let execution continue -- straight into `prior.token()`, which
then threw anyway. The type check may continue (a str renders harmlessly); this one may not,
because size is exactly what rendering fails on.

MEASURED against the gate's oracle (verdict, no exception):
  verify_sequence (descending, huge)   -> ok=False   (was: raw ValueError)
  verify_sequence (ascending, huge)    -> ok=False   (the second reported instance)
  evaluate_renewal_policy (huge)       -> verdict, anomaly named:
      "renewal overdue (WARN): newest ATS time <int, 16610 bits> is in the future (now=1)"
ANTI-PARITY: a normal token is byte-identical (`sha256:<digest>:1000`), legitimate times render
unchanged, and a value just below the ceiling is still printed in full.

ONE MORE THING THE SUITE CAUGHT, and it was mine: the CHANGELOG linked
audit_artifacts/500/DEEP_RUN_RECORD_500.md -- a file that must NOT exist while the gate reports
FIX_FIRST. It now points at the PRE-REGISTRATION (which does exist) and states plainly that the
RUN record is deliberately absent and that pre_tag_audit_gate --strict reporting MISSING is the
correct state.

Riegel: tests/test_render_safe_untrusted_int.py, 12 tests -- the helper itself, all three
contracts, and the anti-parity half for each.

Gemessen: 2226 tests green in the full suite, 123 skipped. The two still-red tests demand a
pre-tag RUN record for 5.0.0 and stay red on purpose until a run returns WITHSTANDS.

EHRLICHE GRENZE: `evaluate_renewal_policy` returns ok=True for an absurd future time, and that is
NOT a defect -- it evaluates whether renewal is DUE, not whether the input is valid. The anomaly
is reported in the check detail. Asserting ok=False there would encode a misunderstanding of what
the function answers.

Owner-GO: local commit in proofbundle via b7_fremdrepo_commit.sh, no push, no tag.
…bc179e, after four runs

The fourth deep gate returned WITHSTANDS_DEEPGATE with zero jury-confirmed findings and all four
pre-registered standing targets (RT-01..RT-04) explicitly attacked and confirmed fail-closed. Only
now may this record exist: writing it earlier is precisely the failure the gate is built to catch,
and it was deliberately absent through three FIX_FIRST rounds.

IT RECORDS ALL FOUR ROUNDS, NOT ONLY THE CLEAN ONE. That sequence is the content, not a preamble:

  1. a7d162a  FIX_FIRST — L6-01 (P3): a CI readiness gate reported audit_candidate_ready=True with
              exit 0 while its version pin read 3.6.0 and the package shipped 5.0.0. It was
              attesting readiness from another release's evidence.
  2. ab90699  FIX_FIRST — two P2: an unbounded integer magnitude on three exported verify surfaces
              (3.3 s of CPU for a correct False), and seven sdist tests FAILING instead of skipping
              in a base-only install (the production code was right; the test turned the third
              state into a failure).
  3. 388a4d4  FIX_FIRST — two P2, and they were the NEIGHBOUR of round 2's own fix: that sweep
              asked which surfaces TAKE an untrusted integer, never what happens to it. Rendering
              one into a diagnostic raised a raw ValueError (CVE-2020-10735).
  4. 9bc179e  WITHSTANDS — nothing survived the three-juror refute-to-kill.

NOT RUBBER-STAMPED. The record carries, openly, a reproducible L5 candidate that did NOT survive
the jury (findings_register superseded_by normalisation can silently drop an open P0 via a
byte-distinct but normalise-equal id). It is not a confirmed finding of this run — a validly-signed
hostile register is required, and a key holder can already write status='closed' directly. It is
recorded anyway because it also fires on an honest encoding typo, it touches the module's own core
invariant ("a finding must never vanish from the count"), and the existing regression covers only
the collision direction, not this clean-decoy variant.

The ledger coverage gap is stated rather than rounded away: 80 of 140 learned classes replayed
(0.5714), so sixty rest on their standing tests and not on fresh gate evidence from this round.

MEASURED after writing the record:
  pre_tag_audit_gate --strict   exit 0, audit-recorded=True (was: MISSING, correctly)
  the two previously-red tests  green — they demanded exactly this record
  full suite                    2229 passed, 123 skipped, 0 failed
  check_version_and_changelog   OK, 5.0.0 single-sourced

CHANGELOG updated to match: it described the deliberate absence of the record and now names the
four rounds and what WITHSTANDS does and does not mean.

Owner-GO: local commit in proofbundle via b7_fremdrepo_commit.sh, no push, no tag.
…0.0 (U3)

OWNER-ENTSCHEIDUNG O2 vom 25.08.2026, woertlich: "sie gehen mit: die Vertragsflaeche
zaehlt, der CHANGELOG-Abschnitt nennt sie als brechend, U3/U5 laufen damit an."

Sie war ausdruecklich an M3 gebunden, und M3 ist gemessen: beide Staende am
pytest-Plugin ausgefuehrt, mit trivialem gruenem Test und PROOFBUNDLE_EMIT=1, ohne
gesetzten Threshold —

  origin/main                  exit 0, Beleg proofbundle_pytest_receipt.json GESCHRIEBEN
  origin/docs/note-doi-4.0.0   exit 0, KEIN Beleg, "PROOFBUNDLE_THRESHOLD not set"

Die EXITCODES verschieben sich NICHT. Die VERTRAGSFLAECHE sehr wohl: wer heute unter
denselben Bedingungen einen signierten Beleg bekommt, bekommt danach keinen mehr. Die
alte Fassung schrieb ein passed=true gegen einen Default von 0 — ein Verdikt, das per
Konstruktion nie fehlschlagen kann; die neue verweigert es lieber. Das ist die bessere
Eigenschaft und zugleich der Bruch, und der Owner hat ihn als solchen gewertet.

DAZU DER FUND, der diese Entscheidung ueberhaupt noetig machte (F2 des Auftrags
20260825T131500Z): d9913a6 und bed147b sind NICHT Vorfahren von origin/main, sie lagen
allein auf origin/docs/note-doi-4.0.0. Die Vormerkung fuehrte sie als "main-Stand" —
deshalb stand eine eigenstaendige MAJOR-Begruendung ausserhalb des Blickfelds. Die
Zeile ist berichtigt.

Merge konfliktfrei (auto-merge in cli.py), 198 Zeilen ueber 10 Dateien.
Volle Suite danach: 2238 passed, 123 skipped, 558 subtests passed, keine Fehler.

M4 dazu gemessen: KEIN Hausaufruf nutzt die Emissions-Flaechen (pytest_plugin,
inspect_hook, emit_claim_receipt, from_inspect_ai_log). Das Haus verifiziert und
signiert. Der Bruch trifft uns nicht — fuer Nutzer draussen bleibt er der Bruch, der
er ist.
Owner-Entscheidung O2: "sie gehen mit: die Vertragsflaeche zaehlt, der
CHANGELOG-Abschnitt nennt sie als brechend".

Der Abschnitt sagte bisher "the trigger is one sentence". Das stimmt nach dieser
Entscheidung nicht mehr. Beide Trigger traegen den Increment fuer sich allein, und
beide stehen jetzt da — eine Release-Notiz, die einen nennt und den anderen weglaesst,
laedt den Leser ein anzunehmen, er habe das ganze Bild gelesen.

TRIGGER 1 (unveraendert): eine Eingabeklasse, die vorher exit 2 lieferte, liefert jetzt
exit 1. SPEC.md ist RFC-2119-normativ, ein deklarierter Exit-Code-Vertrag ist
oeffentliche API.

TRIGGER 2 (neu): die Threshold-Pflicht. GEMESSEN, beide Seiten, VOR der Entscheidung:
die Exitcodes bewegen sich NICHT (beide 0 auf einem trivialen gruenen Test), die
Vertragsflaeche schon — unter identischen Bedingungen schreibt die Vorgaengerversion
ein signiertes Receipt und diese keines. Wer sich darauf verlassen hat, eines zu
bekommen, bekommt nichts, und fuer ein Verifikationswerkzeug ist das ein Bruch,
gleichgueltig was der Rueckgabewert sagt. Umstieg: eine Zeile.

Dazu die drei additiven Erweiterungen desselben Merges: capture_mechanism in der
signierten provenance, lm-eval bindet endlich seine Framework-Version (die Asymmetrie
war, dass promptfoo und Inspect banden und lm-eval nichts), und --expect-issuer auf
show-eval, adversarial motiviert — ein geflipptes, mit frischem Schluessel neu
signiertes Receipt ging vorher mit rc=0 durch.

AUDIT-BLOCK EHRLICH MARKIERT (F1 des Auftrags 20260825T131500Z): der Abschnitt fuehrte
ein WITHSTANDS auf 9bc179e als Release-Attestierung. Dieser Commit liegt VOR der
Umfangsentscheidung; ein Record ueber einen Baum, der sich seither geaendert hat, ist
nichts wert. Der frueherer Lauf bleibt beschrieben, weil seine Funde echt sind und als
Klassen behoben wurden — aber er ist ausdruecklich NICHT die Attestierung. Das Tor
laeuft ueber den finalen Umfang neu.

62 dokumentationsnahe Tests gruen, volle Suite nach dem Merge 2238 passed.
…nstance

Deep-gate T1/iter5 (2026-08-25) found an ASYMMETRIC BINDING a verifier cannot see: three
of four adapters bind the harness version under `harness_version`; promptfoo bound it under
`promptfoo_version` only. The lm_eval adapter's own comment cites promptfoo as an adapter
that "binds" — mistaking a different field NAME for the same binding.

A reader comparing receipts across adapters therefore found `harness_version` missing on
promptfoo receipts and could not tell "promptfoo reports no version" from "this adapter
names it differently". Both names are written now: `harness_version` for cross-adapter
comparison, `promptfoo_version` kept so existing readers do not break.

Written ONLY when promptfoo actually reports it. The contract
`test_missing_version_field_stays_absent_not_invented` forbids inventing a value into
evidence, and the same reasoning applies here.

WHAT WAS DELIBERATELY NOT DONE, documented in lm_eval.py: iter5 also proposed writing the
field UNCONDITIONALLY, since an absent field cannot be told apart from "this adapter does
not bind the version". That override was refused. The existing contract's reasoning is the
stronger one — writing "unknown" into a version field puts a value into EVIDENCE that the
harness never reported, which is exactly what this project exists to prevent. The ambiguity
is real but belongs to a different remedy (a separate reported-flag, or documenting that all
four adapters bind the same field when present) and is an Owner decision, not a silent
override of a deliberate test.

Tests: 25 adapter tests green in the release worktree.

Owner-GO 2026-08-25T23:13:00Z, manifest
20260825T231300Z__prompt_kraxo__go_adapter_commit_und_warum_daraus_keine_karte_wurde.
Scope of that GO: this commit and its push to release/v5.0.0 only. Prep-PR, merge to main,
tag, GitHub release, PyPI and deposit are each a separate door and are NOT covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VzPXkFzQ6VYJPH3AQn3hwz
…ings at once

THE GAP (deep-gate iteration 5, finding F-C; measured at head 529cd20). A harness-reported
version field was written when the harness reported one and simply ABSENT when it did not.
Absence therefore carried two different meanings and the receipt did not say which:

    the harness ran and reported no version
    no harness was bound at all, or the adapter never fills the field

For a verifier that ambiguity is the failure class the product exists against. A receipt
must say what it means.

THE FORM: a status with THREE values, deliberately not a boolean — a boolean has three
states of its own (true, false, absent) and would move the ambiguity one level up.

    <field>_status         reported | not_reported | not_bound
    <field>_status_reason  MANDATORY whenever the status is not `reported`

THE WHOLE CLASS, not one field. `harness_version`, `task_version` and `promptfoo_version`
across all four adapters. Patching only `harness_version` would rebuild the hole next
door — which is exactly what iteration 5 found in the promptfoo adapter.

THE VERSION FIELD ITSELF IS UNCHANGED. When nothing was reported the key stays ABSENT;
no value the harness never reported is ever written. The contract
`test_missing_version_field_stays_absent_not_invented` is untouched and still green. The
status speaks about the REPORTING, never about the version.

NEVER DERIVED. Written only from what the harness returned, not inferred from neighbours,
and `not_reported` never folds to PASS.

VERIFIER SIDE (`version_status_issues`): rejects an unknown literal, a non-`reported`
status without its mandatory reason, and a status/field contradiction in BOTH directions
(`reported` without the field, and the field present while the status denies it). One
direction alone would let the pair lie in the other.

SCOPE, so it is a format commitment and not a patch: schema description, producers (4
adapters), verifier, EVAL_CLAIM.md, CONFORMANCE.md, conformance corpus, CHANGELOG.
Additive — 5.0.0 stands, no further major bump.

CONFORMANCE CORPUS: a new vector kind `provenance_version_status` plus SEVEN cases — one
per status value and one per rejection class. A rule with no rejection vector is not
conformance-testable: an implementation could satisfy every positive case and still
accept anything. The kind enum is closed and was extended additively (an unknown kind
stays a per-case FAIL, never a silent skip).

MEASURED, not assumed: 2369 tests green (2353 before + 16 new), conformance exit 0 over
64 vectors, ruff clean. GATE META-TEST: removing the mandatory-reason guard is caught by
exactly one test and nothing else goes red — the property iteration 5 demanded and the
promptfoo fix in 529cd20 did NOT have.

Owner-GO 2026-08-25T23:59:00Z, manifest
20260825T235900Z__prompt_kraxo__luecke_abwesenheitsvermerk_schliessen_dann_release_go.
Scope: this commit and its push to release/v5.0.0. Prep-PR, merge to main, tag, GitHub
release, PyPI and deposit remain separate doors and are NOT covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VzPXkFzQ6VYJPH3AQn3hwz
…r asked about

Jury lens 1 on 66e72f4 returned REJECT with two findings. One held and it was the serious
one; the other did not, and saying so is part of the answer.

FINDING 1 HELD. `version_status_issues` matched EVERY provenance key ending in `_status`,
so an unrelated `run_status` or `scorer_status` in the same block produced a FALSE
finding: "run_status='active' is not one of [reported, not_reported, not_bound]".

A verifier that invents a finding on a field it was never asked about is WORSE than one
that misses something — it makes a VALID receipt look non-conformant, which is this
product's own failure class pointed the other way.

The fix follows the FIELD SUFFIX, not a hard-coded list: the class is "harness-reported
version fields" and every member is named `*_version`. A future adapter binding
`foo_version` is covered without editing the verifier; a literal list would have to be
maintained and would silently miss the next one.

FINDING 2 DID NOT HOLD. The jury argued a provenance with no status at all should be
rejected. It must not be: the change is additive by REQUIREMENT, and demanding a status
would invalidate every receipt issued before 5.0.0. Locked down by a test and a
conformance vector so a later reading of the jury's report cannot quietly turn it into a
change.

TWO MORE CAME FROM ATTACKING THE CODE MYSELF AFTER THE JURY ANSWERED:
- a non-dict provenance RAISED TypeError instead of reporting. A verifier must report,
  never crash — a traceback here aborts the verification of an otherwise valid bundle.
- `sorted(provenance)` over mixed key types raised TypeError ('<' between str and int).
  Only string keys are sorted now. JSON allows only string keys, but this function takes
  a Python dict and a caller can build one with others.

Corpus: 2 more vectors (unrelated `_status` neighbours must produce nothing; a legacy
receipt without any status stays valid) — 66 vectors, exit 0.

MEASURED: 2375 tests green, ruff clean. GATE META-TEST: removing the field-suffix guard
is caught by FOUR tests and nothing unrelated goes red. The file was restored from a COPY
taken before the mutation, not with `git checkout --` — that had silently reverted
uncommitted work earlier in this session.

Owner-GO 2026-08-25T23:59:00Z (same manifest as 66e72f4): this commit and its push to
release/v5.0.0. Prep-PR, merge, tag, GitHub release, PyPI, deposit remain separate doors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VzPXkFzQ6VYJPH3AQn3hwz
…had no business judging

Jury lens 1, round 2 on df2b353: REJECT with three findings. Two held and the reviewer had
the better argument on the one that mattered.

FINDING 1 HELD, AND IT OVERTURNS MY OWN REASONING. The rule accepted any `<field>_status`
whose field ended in `_version`. I had defended that against a named list, arguing a future
`foo_version` would be covered for free. It over-matches: `schema_version` ends in
`_version` and is NOT a harness-reported field, so a malformed `schema_version_status`
produced a finding about a field this rule has no business judging.

For a VERIFIER the two error directions are not equal. A false finding makes a VALID
receipt look non-conformant; a missed field simply produces no finding. The safe rule is
the narrow one. `REPORTED_VERSION_FIELDS` is now named and shared by BOTH sides: the writer
validates against it too, so a typo in an adapter (`harnes_version`) raises instead of
quietly creating a status nobody ever checks — writer and verifier cannot drift into two
different ideas of the class.

FINDING 2 HELD. Called twice — first with a value, then without — the writer left the
version field in place beside a `not_reported` status. The verifier caught it, but a writer
that produces a block its own verifier rejects is a defect in the writer. It now clears the
field on the not-reported path.

FINDING 3 DID NOT HOLD, for the second time: a provenance with no status at all must stay
valid. The change is additive by requirement and demanding a status would invalidate every
receipt issued before 5.0.0. Locked by a test and a corpus vector.

Corpus: one more vector — a `schema_version_status` lookalike must produce NO finding while
the real harness field beside it is still checked. 67 vectors, exit 0.

MEASURED: 2377 tests green, ruff clean.

Owner-GO 2026-08-25T23:59:00Z (same manifest): this commit and its push to
release/v5.0.0. Prep-PR, merge, tag, GitHub release, PyPI, deposit remain separate doors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VzPXkFzQ6VYJPH3AQn3hwz
… zwei davon in meinem eigenen Text

Deep-Gate Iteration 6, deterministischer Pre-Sweep auf 6065815, VOR der Jury. Der Owner-
Feature-Stopp vom 26.08. gilt: was eine neue oeffentliche Zusage braucht, wird als Befund
erfasst und nach 5.1.0 verschoben. Hier steht nur, was keine braucht.

FUND 2 — DER KOMMENTAR NEBEN DER ZEILE LEHRTE DIE VERWORFENE REGEL.
_provenance.py Zeile 224-228 argumentierte woertlich fuer die Suffix-Regel ("The rule is the
SUFFIX OF THE FIELD, not a hard-coded list"), waehrend die Zeile direkt darunter
`if field not in REPORTED_VERSION_FIELDS` lautet — also genau die feste Liste, die 6065815
eingefuehrt hat, nachdem Jury-Linse 1 die Suffix-Regel widerlegt hatte. 100 Zeilen hoeher stand
bereits das Gegenteil. Die Datei trug beide Argumente, und das VERWORFENE stand am Ort der
Aenderung: wer dort liest, findet die Begruendung fuer den Defekt, den dieses Release schliesst.
Der Absatz sagt jetzt, was der Code tut, und begruendet, warum die literale Liste hier richtig
ist — nicht nur sicherer: `*_version` ist ein NAME, die Klasse ist eine ROLLE, und `schema_version`
traegt den Namen ohne die Rolle. Fuer einen Verifier sind die Fehlerrichtungen ungleich (ein
erfundener Fund laesst einen gueltigen Beleg unkonform aussehen), und eine Regel, die keine
Aenderung braucht, bekommt auch keine Gegenlesung.

FUND 3 — VIER ADAPTER TRAGEN DIE STATUSKLASSE, EINE TESTDATEI FUHR EINEN ERZEUGERPFAD.
Ungekuerzt gemessen war tests/test_reported_version_status.py die einzige Datei, die ein
Statusfeld prueft, und der einzige darin gefahrene Erzeuger war lm-eval. Die zehn
Konformitaets-Vektoren pruefen den VERIFIER, nicht die Erzeuger. Das Verhalten der anderen drei
war korrekt (von Hand gemessen), aber ein spaeterer Aufrufpunkt, der syntaktisch gueltig und
semantisch falsch ist, waere unbemerkt geblieben — genau wovor der Vertrag
test_missing_version_field_stays_absent_not_invented ein Feld weiter schuetzt. Jetzt je Adapter
BEIDE Richtungen (gemeldet und nicht gemeldet), denn eine Richtung allein unterscheidet eine
funktionierende Bindung nicht von einer Konstanten. Dazu ein Riegel, der jedes Mitglied der
benannten Menge an einen echten Adapter bindet, damit ein spaeter hinzugefuegtes Feld die
Luecke nicht wieder aufreisst.

FANGNACHWEIS, drei Laeufe je Probe, gruen -> rot -> wieder gruen, Ruecklage byte-identisch UND
in der Wirkung geprueft (Rueckweg ueber cp, nicht ueber git checkout -- : im Baum lagen
uncommittete Aenderungen):
  M1 promptfoo schreibt einen konstanten Ersatzwert statt des gemeldeten -> 1 Test faellt
     (test_promptfoo_reports_none_and_says_so_on_both). Genau EIN Faenger, und das ist kein
     schwacher Mutant, sondern der Beleg, dass dieser Riegel nicht ersetzbar ist.
  M2 eee bindet gar keinen Status mehr (Rueckfall auf die 4.0.0-Form) -> 2 Tests fallen
  M3 inspect_ai patcht nur harness_version, task_version faellt weg -> 2 Tests fallen
     (die Loch-nebenan-Klasse, die iteration 5 im promptfoo-Adapter gefunden hat)

DAZU EIN DRITTER FUND, DER NICHT VON MIR STAMMT UND HIER MIT MUSS.
`ruff check .` — laut Makefile Zeile 9 und .github/workflows/ci.yml Zeile 81 (Schritt "Lint",
OHNE continue-on-error) ein bindender Schritt — ging auf dem UNVERAENDERTEN 6065815 mit exit 1
raus: F841 in tests/test_cli_eval.py:83, eingefuehrt von bed147b, also aus dem ungewerteten
Delta. Gemessen gegen einen reinen git-archive-Auszug, damit es nicht meine Aenderung ist.
Der Vorbereitungs-PR waere im Lint-Job rot gelaufen. Die Zuweisung war tot: der Aussteller kommt
aus der show-eval-AUSGABE, nicht aus der Datei, und der Kommentar sagte das selbst; die
JSON-Pruefung war redundant, weil show-eval eine Zeile darueber ohnehin scheitert. Ersetzt durch
die Begruendung, warum die CLI-Ausgabe die richtige Quelle ist. `ruff check .` danach exit 0.

VERTAGT, NICHT VERGESSEN: capture_mechanism nimmt jeden String in signierte Evidenz
('live_hook_trust_me', '', 'lifecycle_hook ' mit Leerzeichen), waehrend der Nachbar
bind_reported_version dieselbe Invariante am Schreiber erzwingt. Owner-Entscheid: capture ist ein
Aufrufparameter einer OEFFENTLICHEN Adapterfunktion, ihn zu zaeunen aendert Annahmeverhalten an
einer oeffentlichen Flaeche — Feature-Stopp, Ziel 5.1.0 zusammen mit der Matrix-Bindung. Befund
CAPTURE-MECHANISM-NIMMT-JEDEN-STRING-DER-NACHBAR-ERZWINGT-SEINE-MENGE-01. Die Praeregistrierung
fuer Iteration 7 laesst E9 im WORTLAUT unveraendert und haelt vor dem Lauf fest, dass eine
Refutation an genau dieser Instanz erwartet wird und das Urteil nicht zurueckhaelt, jede ANDERE
aber sehr wohl.

GEMESSEN: volle Suite 2269 passed, 123 skipped, 555 subtests, 0 failed (Minimal-Lane, ohne die
Extras [experimental]/[pq]) — die 7 zusaetzlichen sind genau die neuen Tests. ruff check . exit 0.
Ledger-Replay 161 Tests pass, 0 gelernte Klasse zurueckgefallen.

KEIN Push, KEIN PR, KEIN Merge — jedes davon bleibt eine eigene Owner-Tuer.

Owner-GO 2026-08-26 (Karte OA-402697f73e, Antwort): "Fixe Fund 2 und Fund 3 auf
release/v5.0.0, ein Commit." Umfang dieses GO: dieser Commit auf release/v5.0.0, sonst nichts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VzPXkFzQ6VYJPH3AQn3hwz
…htform

Deep-Gate Iteration 7 auf d8ec901: FIX_FIRST, 57 Agenten, 6 Linsen, 16 Kandidaten, EINER
ueberlebte die Drei-Geschworenen-Widerlegung. Owner-Entscheid 2026-08-26: in 5.0.0 fixen, als
KLASSE ueber alle laxen Stellen, danach Iteration 8.

DER DEFEKT (L3-500-DSSEB64-02, P2). dsse._b64decode_any hatte einen strikten ersten Arm und einen
FALLBACK auf base64.urlsafe_b64decode OHNE validate=True. CPythons Default VERWIRFT Zeichen
ausserhalb des Alphabets still. Selbst nachreproduziert mit gruener Kontrolle: '!', Zeilenumbruch,
Leerzeichen, Tab und NUL, eingefuegt in die payload eines SIGNIERTEN Envelope, ergaben weiterhin
verify=True bei byte-VERSCHIEDENER Huelle — und dieselbe Luecke traegt die SIGNATUR selbst
(signatures[].sig). Der Docstring behauptete das Gegenteil ('raises binascii.Error if neither is
valid'). Der eigene mitgelieferte Rust-Verifier lehnt dieselbe Datei ab.

WARUM ER 26 TAGE UNSICHTBAR WAR, und das ist die eigentliche Lehre: der laxe Pfad verwirft den
Muell und prueft DANACH die Polsterung. Ob er wirft, haengt also von der Restlaenge ab. Gemessen:
urlsafe_b64decode(b'aGFsbG8gd2VsdA' + b'!' + Polster) wirft 'Incorrect padding', dieselbe Einfuegung
in eine echte DSSE-payload dekodiert sauber durch. Ein Defekt, der fuer manche Eingaben laut und
fuer andere still versagt, ist mit EINER Eingabe nicht zu finden.

GRENZE, ausdruecklich, damit das nicht groesser gelesen wird als es ist: KEINE Signaturfaelschung
und KEIN Subject-Binding-Bypass — body_sha256 und derive_subject_digest sind ueber alle
akzeptierten Varianten byte-identisch. Was bricht, ist die kanonische Drahtform-Identitaet: Dedup,
Replay-Erkennung und Transparenz-Log-Blattidentitaet setzen alle voraus, dass ein Artefakt EINE
akzeptierte Form hat. RFC 4648 Abschnitt 3.3 verlangt die Abweisung ohnehin, und zwei Verifier in
einem Release, die ueber dieselbe Datei uneins sind, sind eine Zusage, die das Release nicht haelt.

DER KLASSEN-FIX, nicht der Instanz-Fix.
  * NEU src/proofbundle/_wire_b64.py — der EINE strikte Dekoder mit drei benannten Funktionen
    (decode_b64 / decode_b64url / decode_b64_either). Alle werfen binascii.Error, eine Unterklasse
    von ValueError, sodass bestehende except-Klauseln unveraendert greifen und kein Aufrufer eine
    neue braucht.
  * 26 Aufrufstellen in 15 Modulen darauf gezogen. Die Polster-Rechnung verschwindet aus den
    Aufrufstellen und lebt im Helfer: sonst rechnet sie jede Stelle neu und kann sie jede Stelle
    neu falsch rechnen.
  * VOR dem Sweep gemessen statt angenommen: validate=True aendert die Polster-Toleranz NICHT
    (QUJD / QUJDRA / QUJDRA== verhalten sich lax und strikt identisch). Der Sweep verengt also
    nichts, was heute durchgeht — er weist nur Muell ab.
  * BEWUSST NICHT mitgefixt: decode_b64url akzeptiert auch '+' und '/', weil altchars erst
    uebersetzt und dann gegen das Standard-Alphabet prueft. Das ist dieselbe FAMILIE, aber eine
    ANDERE Eigenschaft (ein Alphabet je Feld statt ein Alphabet ueberhaupt), sie wurde vom Gate
    NICHT gemessen, und sie mitzufixen hiesse eine gegatete Aenderung ueber das Gemessene hinaus
    zu erweitern. Im Modul-Docstring ausgeschrieben und als eigener Befund erfasst
    (B64URL-AKZEPTIERT-BEIDE-ALPHABETE-EINE-ZWEITE-KANONIKALITAETSFRAGE-01), damit die naechste
    Leserin es als Entscheidung erkennt und nicht als Versehen.

NEU tests/test_wire_bytes_strict.py — und diese Datei hat ihre vier Testnamen nicht frei gewaehlt.
Der Klassen-Ledger fuehrt seit dem 2026-07-31 die Klasse
decoder_normalises_away_unknown_bytes_instead_of_rejecting_them und nennt GENAU diese Datei mit
GENAU diesen vier Nodes als ihren Beleg — waehrend die Datei nicht existierte. Die Klasse stand
darum ehrlich auf env_blocked (sie hat nie Schliessung behauptet), und der Defekt lebte 26 Tage in
ihr weiter. Eine zweite Klasse, canonicity_preserving_perturbation_accepted (RT-08), nennt dieselbe
Invariante mit 'noch nicht eingepflanzt'. Beide werden hier unter den zitierten Namen geschlossen,
damit die Verweise aufloesen statt zu haengen:
  TestDecoderProvenanceGate::test_no_unpinned_module_decodes_laxly          (Live-Guard, ganzer Baum)
  TestDecoderProvenanceGate::test_detector_sees_a_planted_violation_in_a_new_file_shape
  TestDecoderProvenanceGate::test_anti_tautology_blinded_scanner_stops_catching_the_violation
  TestStrictDecoderFamily::test_every_probeable_decoder_rejects_junk_and_accepts_the_clean_form

WARUM AST UND NICHT GREP: ein grep ueber genau diese Familie gab mir am Tag des Fixes ZWEI falsche
Antworten. Er zaehlte eine KOMMENTARZEILE, die b64decode( erwaehnt, als Dekodierstelle (27 statt
26). Und eine Substring-Pruefung 'import decode_b64' in text traf 'import decode_b64url' und
uebersprang einen wirklich fehlenden Import — 133 Tests wurden rot. Das ist dieselbe Klasse, die
dieser Commit fixt (Substring statt benannter Sache), und sie ist mir beim Fixen selbst unterlaufen.

ANTI-PARITAET, an erster Stelle und nicht verhandelbar: jede Probe prueft ZUERST, dass die saubere
Form akzeptiert wird. Ohne das besteht ein Dekoder, der ALLES abweist, jede folgende Zusicherung.
Der Generator variiert Zeichen UND Position statt eine Fixture zu pruefen.

GATE-META-TEST, drei Laeufe: gruen (5 passed, 18 subtests) -> laxen Fallback wieder eingepflanzt
-> ROT (15 failed) -> nach byte-identischer Ruecklage wieder gruen. Die Ruecknahme faerbt den
Riegel wirklich, und die Ruecklage wird in ihrer WIRKUNG geprueft, nicht nur an den Bytes.

EHRLICHE RESTGRENZE, als eigener Test der sich SKIPPT statt gruen zu tun: der Rust-Differential-Arm
ist noch nicht im Korpus gepflanzt (TestRustAgreement). Das Gate hat ihn als die verbleibende
Haelfte dieser Klasse benannt; er ist env_blocked, nie gruen.

GEMESSEN nach dem Sweep: laxe Dekodierstellen im ganzen Baum 0 (AST-Sweep, nicht grep) ·
ruff check . exit 0 · volle Suite 2275 passed, 123 skipped, 573 subtests, 0 failed (Minimal-Lane,
ohne die Extras [experimental]/[pq]).

NOCH NICHT GESCHEHEN und ausdruecklich offen: Iteration 8 ueber den NEUEN Digest. Ein Verdikt
bindet an genau einen Digest, und dieser Commit bewegt ihn.

KEIN Push, KEIN PR, KEIN Merge, KEIN Tag.

Owner-GO 2026-08-26 (Karte OA-2591904f69): "In 5.0.0 fixen, Klassen-Fix ueber alle 27 laxen
Stellen, danach Iteration 8." Umfang dieses GO: dieser Commit auf release/v5.0.0, sonst nichts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VzPXkFzQ6VYJPH3AQn3hwz
… zwei Tests glaubten ihr

Owner-Auftrag OA-38bc39ab12 ("1, Generator zuerst"), Schritte 1 und 2 vollstaendig, Schritt 3
angefangen. NICHT auf release/v5.0.0 — Arbeitszweig, weil Iteration 8 auf d478882 eingefroren ist.

WURZEL, an der Quelle behoben statt im Verbraucher: discover_python_verify_functions in
scripts/rust_parity_gate.py erhob die Ground-Truth mit vier Verengungen, die alle STILL wirkten —
eine fehlende Flaeche erzeugt nirgends einen Fehler, nur ein Gate, das aus dem falschen Grund
durchlaeuft:
  1. glob("*.py")               nur die oberste Ebene, nie adapters/ experimental/ policies/
  2. startswith("verify_")      nie validate_*
  3. nur tree.body begangen     nichts in einem Klassenkoerper erreichbar
  4. daraus folgend             jede verify_*-METHODE unsichtbar

FUENF Verbraucher teilen sich diese eine Population (rust_parity_gate, type_confusion_gate,
fuzz_soak, test_rust_parity_gate, test_roadmap_frontload_foundations). Was sie nicht sieht, sehen
drei Gates und zwei Tests nicht. Deshalb der Fix an der Quelle: zwei Populationen nebeneinander
waeren schlimmer als eine zu kleine. GEMESSEN 47 -> 57 Flaechen.

Dazu resolve_surface() als EIN Resolver an derselben Stelle. Beide Verbraucher trugen je ein
eigenes qname.split(".")[1]/[2] — eine fest verdrahtete Zwei-Segment-Annahme, die genau die
Unterpaket- und Klassenmethoden-Flaechen falsch aufloest, die die erweiterte Population neu
enthaelt, und das Scheitern dann schluckte (IMPORT_ERROR bzw. skipped). IMPORT_ERROR ist jetzt
gezaehlt und --strict-blockend statt unsichtbar.

ZWEITE HAELFTE, die verschachtelte Blatt-Matrix in type_confusion_gate.py: die alte Matrix ersetzte
das GANZE Primaerargument, also wies die aeussere Formpruefung jede Nutzlast ab, bevor ein innerer
Feldvalidator lief. Neu: formgueltige Huelle, EIN verdorbenes Blatt, Container list/dict/set/tuple,
zwei Tiefen (outcome faellt auf Tiefe 1, run_ledger erst auf Tiefe 2), Feldnamen aus dem AST.
Gemeldet wird der MINIMALE Zeuge — Tiefe 2 laeuft nur durch Behaelter, die Tiefe 1 nicht schon
gebrochen hat, sonst wird aus einem Defekt eine Meldung je Geschwisterfeld (outcome: 556).

ERGEBNIS, gemessen: never_raise_ok=False, 20 Meldungen, DREI defekte Flaechen in 5,3 s —
outcome.validate_outcome_predicate, run_ledger.validate_run_ledger_predicate und
verification_summary.validate_summary_predicate. Die dritte kommt in den ACHT Funden von
Iteration 8 nicht vor: der reparierte Generator findet mehr als der Lauf, der ihn angefordert hat.

ZWEI URSACHEN, je zwei Flaechen, und keine erklaert alle vier: outcome und run_ledger fehlten in
der POPULATION; sdjwt.verify_sd_jwt und kbjwt.verify_key_binding sind drin, aber NON_JSON (ihr
Primaerargument ist ein Kompakt-String, der Absturz sitzt hinter der Dekodierung) — deren
Wiederaufnahme bleibt ein eigener, dritter Teil.

GATE-META-PRUEFUNG, tests/test_gate_population_and_nested_leaf.py, 11 Tests ueber BEIDE Haelften:
ein gepflanzter Defekt der gefundenen Art muss rot werden, UND eine gepflanzte verify_*-Methode in
einer Klasse sowie eine in einem Unterpaket muessen in der Population auftauchen. Mit Anti-Paritaet
in beiden Haelften (die geschuetzte Form darf NICHT gemeldet werden, ein nicht passender Name darf
NICHT erhoben werden), sonst besteht ein Generator, der alles meldet, die Pruefung.
FANGPROBE gefahren: gruen -> 8 von 11 rot unter kontrollierter Ruecknahme -> byte-identisch zurueck
(cp-Bezugspunkt, sha256 -c OK; NICHT git checkout --, das haette vom Commit hergestellt).

test_rust_parity_gate::test_method_inside_a_class_is_not_module_level ist UMGEDREHT, mit Grund im
Test statt still: die vierte Verengung war eine festgeschriebene Absicht, keine Nachlaessigkeit.
Die dotted refs der Registry bleiben unberuehrt, eine Methode bekommt die NEUE Form
proofbundle.<modul>.<Klasse>.<methode>. Die 10 neu sichtbaren Flaechen sind als PENDING erfasst
(57 Eintraege) — der vom Gate selbst vorgesehene ehrliche Zustand, nicht ein Zudecken.

ZWEI EIGENE FEHLER DERSELBEN KLASSE, beim Beheben eingebaut und gemessen behoben:
  sorted(felder)[:40] schnitt alphabetisch und warf 'status' weg — genau das Feld, auf dem alle
  vier Funde abstuerzen. Ein sortiertes Praefix ist kein Sample, sondern eine systematische
  Verzerrung. Der Deckel war ausserdem reiner Schaden: ohne ihn 7360 Nutzlasten in 0,5 s.
  Der Behaelter-Detektor matchte die FORM "for … in obj.get(x)" statt der ROLLE und verfehlte
  run_ledger (runs = predicate.get("runs"); enumerate(runs)). Jetzt ist jedes Feld Behaelter-
  Kandidat: Ueber-Einschluss kostet nur Nutzlasten, Unter-Einschluss IST der Defekt.
Zusaetzlich ein echter Prefix-Fehler behoben: startswith("proofbundle") matcht auch
proofbundle_fremd.

ANGEFANGEN, NICHT FERTIG: src/proofbundle/_membership.py::is_member steht (die Klasse ist ein
Mitgliedstest gegen einen HASHENDEN Behaelter mit angreiferkontrolliertem Wert links; gemessen 27
Stellen plus 25 tuple/list-Nachbarn, die heute nur zufaellig sicher sind). Die 27 Stellen sind NOCH
NICHT umgezogen und der Scanner-Test fehlt. Bis dahin meldet type_confusion_gate --strict exit 1,
und das ist der richtige Zustand: die Defekte sind echt.

KEIN Push, KEIN PR, KEIN Merge. release/v5.0.0 bleibt auf d478882.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VzPXkFzQ6VYJPH3AQn3hwz
…e, nicht die 27 Zeilen

Owner-Auftrag OA-38bc39ab12, Schritt 3 ("erst wenn das steht, sind die vier P1 als Klasse dran,
nicht als vier isinstance-Guards"). Setzt 663a615 fort, NICHT auf release/v5.0.0.

DIE KLASSE, als verletzte Annahme statt als Codeform: ein Wert aus geparstem JSON ist hashbar. Ist
er nicht. set/dict/frozenset-Mitgliedschaft HASHT den linken Operanden, also wirft

    if predicate.get("status") not in _OUTCOME_STATUS:      # ein set

bei {"status": []} einen nackten TypeError — VOR jeder Signaturpruefung, aus einer Funktion, deren
ganzer Vertrag lautet: liefert ein Verdikt oder wirft ProofBundleError. Iteration 8 bestaetigte das
an vier Flaechen gleichzeitig, darunter das Flaggschiff verify_bundle.

WARUM EIN MODUL UND NICHT VIER GUARDS: dieses Repo hat den Instanz-Fix dreimal bezahlt
(statuslist.py:122, kbjwt.py:151, kbjwt.py:230) — jedes Mal wurde das AEUSSERE Argument gehaertet
und ein INNERES Feld crashte weiter. Vier Guards schliessen vier Zeilen und lassen die Annahme
ueberall sonst stehen, auch in Code, der noch nicht geschrieben ist.

  src/proofbundle/_membership.py::is_member  — die eine Stelle, die diese Eigenschaft kennt.
  27 Stellen in 14 Modulen darauf gezogen, AST-gefuehrt (Positionen aus dem Baum, Syntaxkontrolle
  vor jedem Schreiben), nicht per Textersetzung. Ein konstanter linker Operand bleibt absichtlich
  unangetastet: `"status" in predicate` fragt nach einem SCHLUESSEL, und ein Literal ist immer
  hashbar — das zu melden macht den Scanner genau dort laut, wo er immer recht hat, und ein lauter
  Scanner wird abgeschaltet.

BEWUSST KEIN try/except TypeError: das schluckte auch einen TypeError aus einem kaputten Behaelter
oder einem fehlerhaften __hash__ und verwandelte unverwandte echte Defekte in ein leises False.
Geprueft wird genau die Eigenschaft, die die Aufrufstelle braucht.

DER SCANNER IST DER EIGENTLICHE FIX, tests/test_membership_hashable_guard.py (12 Tests, 19 subtests):
die 27 Stellen sind repariert, der Scanner haelt die 28. auf. Ein Diff-Review kann eine Stelle nicht
sehen, die es noch nicht gibt.

DER BEHAELTERTYP WIRD AUS DEM AST GEMESSEN, NICHT GELISTET. Das ist die tragende Entscheidung und
sie deckt die 25 tuple/list-Nachbarn, OHNE sie heute anzufassen: sie hashen nicht, sind also jetzt
keine Verletzung — aber sobald jemand _ALLOWED = ("a","b") aus Tempogruenden zu {"a","b"} macht,
wird jeder Mitgliedstest darauf zur Verletzung und dieser Scanner im selben Commit rot. Eine
handgepflegte Liste "gefaehrlicher Behaelter" muesste ausgerechnet der pflegen, der es vergessen
hat. Iteration 8 nannte statuslist._ALLOWED_BITS und policy._SUPPORTED_SCHEMAS namentlich als "nur
zufaellig sicher" — genau dafuer ist test_the_same_site_IS_flagged_once_that_tuple_becomes_a_set da.

ANTI-PARITAET in beiden Richtungen, sonst belegt kein Gruen etwas: die geschuetzte Form darf NICHT
gemeldet werden · ein Schluesselpraesenz-Test darf NICHT gemeldet werden · is_member("ok", {...})
muss WEITER True liefern (ein Guard, der immer False lieferte, bestuende alles andere und wiese
still jeden Wert ab).

GEMESSEN NACH DEM FIX:
  type_confusion_gate --strict          exit 0, never_raise_ok=True, 0 Verletzungen (vorher 20)
  ruff check src/proofbundle/           exit 0
  test_gate_population_and_nested_leaf  11 passed, 57 subtests — der Generator KANN noch fangen,
                                        das Gruen oben ist also echt und keine neue Vakuitaet
  test_membership_hashable_guard        12 passed, 19 subtests
  Direktkontrolle an den drei Flaechen: outcome({'status': []}), run_ledger runs[0].status und
  verification_summary levels[0].kind liefern jetzt ein VERDIKT statt eines Absturzes; die
  Kontrollen unterscheiden sich in der Zahl der Meldungen (5 vs 7), es antwortet also nicht
  ueberall dasselbe.

EHRLICHE GRENZE, hier statt zum Selberfinden: der Scanner liest src/proofbundle/**, Behaelter auf
MODULEBENE und Vergleiche mit EINEM Operator. Ein zur Laufzeit gebauter Behaelter, ein aus einem
anderen Modul importierter oder ein verketteter Vergleich ist NICHT abgedeckt. is_member ist
ueberall sicher benutzbar, unabhaengig davon.

OFFEN BLEIBT der dritte Teil: die Wiederaufnahme der 26 NON_JSON (sdjwt.verify_sd_jwt und
kbjwt.verify_key_binding stuerzen hinter der Dekodierung ihres Kompakt-Strings ab und sind von
dieser Matrix bauartbedingt nicht erreichbar).

KEIN Push, KEIN PR, KEIN Merge. release/v5.0.0 bleibt auf d478882.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VzPXkFzQ6VYJPH3AQn3hwz
… werfen

Fund der Pflicht-Gegenlesung zu fd84e1d (Verdikt REJECT, ein Fund haelt). Dritter eigener Fehler
derselben Klasse an einem Tag: ein Fix reproduziert seinen eigenen Defekt in sich selbst.

DER FUND, am Quelltext nachgemessen statt an meiner Zusammenfassung geglaubt:
collections.abc.Hashable prueft, ob __hash__ EXISTIERT — nicht, ob der Aufruf gelingt. Gemessen:
    isinstance(("a", []), Hashable)  ->  True
    hash(("a", []))                  ->  TypeError: unhashable type: 'list'
Ein tuple ERBT __hash__ und scheitert erst, wenn es seine Elemente hasht. Der Guard liess es also
durch und warf — genau die Klasse, die er beseitigen soll, eine Ebene tiefer.

DIE HAELFTE, DIE NICHT HAELT, und sie gehoert dazu: das Beispiel des Reviewers war {"a": []}, und
das kommt NICHT durch — dict.__hash__ ist None, die isinstance-Pruefung weist es schon ab. Nur der
tuple-Fall kam durch. Mechanismus des Fundes verworfen, Hinweis behalten.

WARUM TROTZDEM GEFIXT, obwohl json.loads nie ein tuple erzeugt und die Stelle heute unerreichbar
ist: "heute nicht erreichbar" ist eine Eigenschaft der AUFRUFER, nicht dieser Funktion. Ein Guard,
der nur deshalb richtig ist, weil ihm zufaellig nichts Unpassendes uebergeben wird, ist kein Guard.

FIX: try/except TypeError um die Mitgliedschaftspruefung, isinstance bleibt als schneller Pfad. Die
frueher hier stehende Begruendung GEGEN try/except ("wuerde einen kaputten __hash__ verschlucken")
war zu clever und ist ersetzt: ein Objekt mit kaputtem __hash__ kann ebenso wenig Element eines
hashbasierten Behaelters sein, False bleibt also die WAHRE Antwort auf die einzige Frage, die diese
Funktion stellt.

TESTS: TestTheGuardCannotRaiseItself, drei Faelle — der verschachtelte tuple wirft nicht mehr; das
dict-Beispiel des Reviewers ist als bereits-abgedeckt festgehalten (damit die falsche Haelfte nicht
spaeter als neuer Defekt "wiederentdeckt" wird); und Anti-Paritaet, ein hashbarer tuple funktioniert
weiter normal (sonst bestuende ein is_member, das fuer JEDEN tuple False liefert, alles darueber).
15 passed, 19 subtests · ruff exit 0.

KEIN Push, KEIN PR, KEIN Merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VzPXkFzQ6VYJPH3AQn3hwz
…ring-Flaechen hinter der Dekodierung (iter8 Teil 3)
… ein blankes .get (iter9)

Deep-Gate iter9 Linse 3 fand einen echten erreichbaren Klassen-Nachbarn des iter8-Typ-
Konfusions-Fixes: anchors_ots.verify_opentimestamps stuerzte roh (AttributeError) bei
frozen=[] (Nicht-Mapping) oder rp_trust={"bitcoin_block_headers":[1,2]} (Mapping mit
Nicht-Mapping-Wert). Dieselbe Klasse wie iter8, an einem dict-Argument statt am dekodierten
Blatt. anchors_rfc3161 + verify_anchor hatten den Guard, OTS/markovian nicht.

Fix: Mapping-Guard an der Verwendungsstelle (nur der upgraded-Pfad erreicht frozen/rp_trust),
fail-closed als VERDIKT statt Raise (OTS gibt ueberall Verdikte; markovian delegiert hierher
und darf keine rohe Ausnahme erben). Deckt OTS + markovian (Delegation); chia ist durch seinen
catch-all-Backstop bereits geschuetzt. 5 Regressionstests reproduzieren beide Lens-3-Exploits.
…ter9 Deep-Gate)

Zwei Funde des frischen Deep-Gate auf dem iter9-Kopf, beide am Quelltext verifiziert:

P0 (KRITISCH, Linse 2, False-Accept): verify_sdjwt_vc liess `structure_ok` aus dem
Endverdikt fallen — es las nur `sig_ok`. Eine PRAESENTIERTE Disclosure, die der Aussteller
NIE committet hat (gefaelschter Claim), wurde als ok=True akzeptiert: ein Halter kann einen
beliebigen Claim injizieren, der dokumentierte Public-RP-Einstieg winkt ihn durch. sig_ok
beweist nur, dass DER PAYLOAD signiert ist, nicht dass die praesentierten Disclosures dazu
gehoeren (RFC 9901). Die Flaggschiff-Bahn bundle.py verdrahtet structure_ok laengst als Check;
sdjwt_vc war die eine Kombinationsflaeche, die ihn fallen liess. Fix: structure_ok ins Verdikt.
requireIssuerSignature=False bleibt der ehrliche Opt-out (Payload dann ohnehin nicht
authentifiziert, structure_ok darauf bedeutungslos). 2 Regressionstests (Faelschung abgelehnt,
gueltig akzeptiert).

rp_headers-Wert (Linse 3b + selbst gefunden): der Container-Guard beweist rp_headers IST ein
Mapping, aber nicht String-WERTE; `bytes.fromhex([1,2])` wirft TypeError, nicht ValueError.
Fix: `except (ValueError, TypeError)`. 3 Regressionstests (Listen-/int-/bad-hex-Wert -> bad_header).

EHRLICHE GRENZE (Linse 3a, NICHT gefixt): isinstance(x, Mapping) beweist kein nutzbares `.get`
(virtuell registriertes Mapping / dict-Subclass mit werfendem .get). Niedrige Schwere: nicht aus
JSON erreichbar (nur malizioeser Einbetter), Referenz-geteilt (anchors_rfc3161 gleicher Entwurf).
Gate-Blindheit (Linse 3d) bewiesen: type_confusion_gate testet dict-Extra-Argumente nicht —
naechster Schritt (fix-the-class auf Gate-Ebene).
… Gate-Blindheit)

Deep-Gate iter9 Linse 3d bewies: type_confusion_gate ist strukturell blind fuer die dict-
EXTRA-Argumente (frozen/rp_trust) — es verwechselt nur das Primaerargument und ruft die
NON_JSON-Anchor-Flaechen gar nicht. Statt eines Punkt-Handtests je Fund (der verrottet, sobald
ihn niemand ergaenzt) ist DIES ein Generator: eine Matrix (Verifier x valider-Primaer x
Typ-Konfusions-Payload je Extra-Arg) ueber eine REGISTRY, die einen neuen Verifier auto-deckt,
sobald sein Primaer-Konstruktor eingetragen ist. INVARIANTE: jede Flaeche liefert fuer JEDEN
Wert der container-typisierten Extra-Argumente ein Verdikt oder ProofBundleError, nie einen
rohen Crash. Plant-Probe verifiziert: Ruecknahme des rp_headers-Fix -> Fuzzer ROT (faengt die
Klasse), byte-identischer Restore -> gruen. 2 Tests, 28 Subtests. Deckt ots + markovian; die
uebrigen Verifier folgen mit ihrem Primaer-Konstruktor. Der Gate-Umbau (Primaer-Konstruktoren
IN den Gate) bleibt Roadmap; dieser Fuzzer schliesst die Klasse test-seitig bis dahin.
…Linse 3a)

Deep-Gate iter9 Linse 3a: `collections.abc.Mapping.register(cls)` macht `isinstance(x, Mapping)`
True, OHNE die Mixin-Methoden zu vererben — ein so registriertes Objekt ohne `.get` passierte den
frozen/rp_trust-Guard und stuerzte dann roh am `.get`. Erreichbar ueber den DOKUMENTIERTEN
Extension-Point `register_anchor_type` (Drittanbieter-Anchor-Typen), nicht nur malizioes. Fix:
`_nutzbares_mapping` prueft zusaetzlich `callable(getattr(x, "get", None))`. Der exotische Rest (ein
Mapping mit werfendem `.get`) bleibt ein vom Aufrufer gebautes feindliches Objekt, ausserhalb des
Bedrohungsmodells. anchors_rfc3161:78-82 traegt dasselbe Muster (Folge-Kandidat, notiert).
Regressionstest im generalisierten Fuzzer (virtuelles Mapping ohne .get -> Verdikt).
…Linse 3a, Klasse geschlossen)

Nachbar derselben Klasse wie anchors_ots (2da2644): der isinstance(Mapping)-Guard beweist kein
nutzbares `.get`, aber verify_rfc3161 ruft sechsmal `frozen.get`/`rp.get`. Ein via Mapping.register()
registriertes Objekt ohne `.get` passierte den Guard und stuerzte roh. Fix: `_nutzbares_mapping` mit
`callable(getattr(x, "get", None))`, hier mit der rfc3161-Konvention (raise BundleFormatError statt
Verdikt-dict). Damit ist die Linse-3a-Klasse an BEIDEN Anchor-Guards geschlossen, die sie trugen.
…0 (iter9 L1)

Deep-Gate iter9, 4. Runde, Linse 1 (opus): verify_consistency prüfte als Schluss die falsche
Variable — fn==0 statt der RFC-9162-§2.1.4.2-Bedingung sn==0. Da fn<=sn gilt und beide gleich
geshiftet werden, folgt sn==0 => fn==0, aber NICHT umgekehrt; fn==0 ist strikt schwächer und band
den Beweis nicht an die behauptete second_size. Für eine Zweierpotenz-first_size ist fn von Anfang
an 0 -> die Prüfung war VACUOUS: ein echter (1,2)-Beweis wurde unter second_size {3..32} als
konsistent akzeptiert. Die Schwesterfunktion root_from_inclusion prüft korrekt sn!=0 — dieselbe
Bindung gehört hierher.

Verifiziert gegen ein strukturell UNABHÄNGIGES RFC-6962-bis-Dekompositions-Orakel (Trillian-Stil,
Längen-Check statt Shift-Loop): nach dem Fix 0 Divergenzen über 1000+ Tripel. Die (1,2)-Exploit-Bande
ist geschlossen. Die verbleibende schmale Größen-Ambiguität ist INHÄRENT zu Konsistenzbeweisen (das
Orakel akzeptiert dieselbe Menge) und durch atomares (size,root) aus signierten STHs abgesichert —
jetzt in der Docstring caveatet (war es nicht).

Sekundär (No-Fake-Unterbehauptung): die verify_inclusion-Docstring bezifferte die tree_size-Ambiguität
als "N±1"; gemessen sind es 127 (N=255) — auf die gemessene Bande korrigiert.
…r Crash (iter9 L2)

Deep-Gate iter9, 4. Runde, Linse 2 (sonnet): ArchiveTimeStamp.time trägt keine Typ-Validierung;
vier unabhängig geschriebene Konsumenten nahmen einen sauberen int an und stürzten bei non-int .time
ROH — TypeError bei `<`/`-`, bzw. ProofBundleError in token() (Magnitude-Guard), vom bestehenden
`except (HashAlgError, RenewalError)` NICHT gefangen.

Der etablierte Vertrag (test_renewal_signed::test_non_int_time_fails_closed_not_raise) verlangt, dass
ein non-int-.time-ATS KONSTRUIERBAR ist und die Verifizierer fail-closed sind (never-raise-Verdikt).
Ein erster __post_init__-Konstruktions-Riegel brach genau diesen Vertrag und wurde verworfen. Der Fix
sitzt an den vier Konsumenten:
- verify_sequence: `except (HashAlgError, RenewalError)` -> `except ProofBundleError` (Basisklasse,
  beide sind Subtypen) — fängt jetzt auch token()s Magnitude-Guard bei einer Riesen-.time. ok=False.
- evaluate_renewal_policy: non-int-time-Guard VOR der Alters-Arithmetik `now - newest.time`. ok=False.
- renew_timestamp/renew_hashtree: gemeinsamer _require_int_time (typisierter RenewalError, ihr
  raise-Idiom) vor `time <= prior.time`.

Reichweite ehrlich: renewal ist EXPERIMENTAL (nicht an verify_bundle/CLI verdrahtet), erreichbar nur
für direkte Integratoren. Alle 5 Szenarien liefern jetzt ein Verdikt/typisiert statt Absturz.
Beim Halten für Linse 1/2 gefunden: verify_rfc3161(..., frozen=None, rp_trust={}) (keine roots)
stürzte roh an frozen.get("rootCertsDerB64") (AttributeError) — die None-Hälfte, die der callable-
Mapping-Check der Vorrunde nicht deckte (`frozen is not None and ...` ließ None durch). frozen ist
ein REQUIRED dict-Argument; frozen=None ist ein Fehlaufruf. Der Guard weist None jetzt typisiert ab
(BundleFormatError), konsistent mit anchors_ots, das None ebenfalls als Nicht-Mapping ablehnt.
Mit roots präsent wurde die frozen.get-Stelle vorher übersprungen (Verdikt), jetzt einheitlich abgewiesen.
…mpositions-Orakel

16 Tests, Generator-Härtung statt Punktfixture (B7_STANDING_BERKELEY_GATE_LEARNS):
- Merkle: die (1,2)-Bande geschlossen; proofbundle == unabhängiges RFC-6962-bis-Dekompositions-Orakel
  (0 Divergenzen über viele Tripel, Anti-Parität — fängt sowohl den fn==0-Bug als auch ein Über-Fixen);
  Kontrolle, dass das Orakel wirklich diskriminiert (lehnt (1,2)-Bande ab, akzeptiert inhärentes (1,3)->4).
- verify_inclusion-Docstring nennt die gemessene Bande statt "N±1".
- renewal .time an den KONSUMENTEN fail-closed: verify_sequence/evaluate_renewal_policy ok=False (kein
  raise), renew_*/RenewalError (typisiert), non-int-.time-ATS konstruierbar, Riesen-int token() weist ab.
- rfc3161 frozen=None immer typisiert (mit/ohne roots).

Meta-Test (plant-and-catch) manuell verifiziert: fn==0 wieder eingepflanzt -> beide Merkle-Tests ROT;
zurückgesetzt -> grün. Volle Suite 2447 passed, ruff clean.
…-500 Spur 1)

Der Gegenleser mass, dass 27 von 57 Verify-Flaechen als NON_JSON klassifiziert und dann GAR NICHT
geuebt wurden (die Delegation an tests/test_fuzz_parsers.py war ein Kommentar, kein Lauf). Rekonstruktion:
- F1: population_size>0 + evaluated_count==population_size erzwungen; IMPORT_ERROR, ein unparsbares
  Quellfile (verschwundene Flaeche) und eine Differenz zwischen ZWEI unabhaengigen Inventaren
  (AST-Discovery vs Laufzeit-Introspektion) sind Fehler -> fail-closed by default, nicht nur --strict.
- F3: die 27 NON_JSON-Flaechen werden jetzt mit einer typ-passenden never-raise-Matrix (byte/
  compact-str/path/int) WIRKLICH geuebt; die Kommentar-Delegation ist entfernt.
- F4: copy.deepcopy pro Aufruf (Payload + extra_kwargs), so kann ein mutierender Verifizierer die
  geteilte Nutzlast nicht veraendern und den Roh-Crash eines spaeteren Verifizierers maskieren (P9).
- F5: eine Union[dict,str]-Flaeche bekommt eine eigene String-Matrix statt Ausschluss; ein roher
  OSError ist ein Fehler, keine "korrekte API-Antwort" (Hausstandard ueber den externen Vorschlag).
- keyword-only-Primaries (verify_hybrid) per Keyword aufgerufen (Signatur-TypeError = Probe-Artefakt).
- Pflichtausgabe: subject_tree_digest, gate_source_digest, inventory_source_digests, population_size,
  evaluated_count, unresolved_count, mutation_case_count, raw_exception_count, positive_control_count.

Gemessen: alle 57 Flaechen geuebt (war 30), 343371 Payloads, 0 Roh-Crashes, population_complete=True.
fuzz_soak teilt _benign_fixtures -> derselbe keyword-only-Fix dort. 2 Gate-Tests auf die neue
_exercise-Tupel-Signatur angepasst. Volle Suite 2447 passed, ruff clean.
…ot a prose line (makellose-500 F6)

Der Gegenleser (P6/F6) zeigte: eine selbst geschriebene CHANGELOG-Zeile
'pre-tag-adversarial-audit: RUN | version=5.0.0' erteilte ok=true. Der Gate nannte die Grenze selbst
'provenance-SHAPED, not provenance'. Rekonstruktion:
- Der Verdikt kommt jetzt aus einem SIGNIERTEN, TREE-GEBUNDENEN Receipt (scripts/pre_tag_receipt_lib.py):
  JSON unter audit_artifacts/<token>/*.json, das subject_tree_digest (== aktueller Tree) + version +
  gate_source_digest + audit_command + audit_exit_code (==0) + audit_output_digest + runner_identity
  bindet und ed25519-signiert ist von einem in audit_artifacts/pre_tag_trusted_pubkeys.txt gepinnten
  Schluessel, dessen PRIVATE Haelfte ein Runner-Secret AUSSERHALB der Agent-Reichweite ist.
- Die CHANGELOG-Zeile ist jetzt PRESENTATIONAL — sie bewegt das Verdikt nicht mehr.
- FAIL-CLOSED by default: kein signierter Receipt (oder kein Vertrauensanker) -> exit != 0 ohne --strict.
- Writer scripts/pre_tag_receipt.py (vom Runner nach erfolgreichem Audit).
- Ehrliche Grenze: proofbundle (OSS) hat keinen In-Repo-Runner; der Schluessel ist ein Release-Secret.
  Auf einem Dev-Tree ohne Receipt ist der Gate fail-closed (korrekt).

P6 (bare Zeile) wird jetzt abgewiesen. 11 Qualifikations-Tests (test_pre_tag_receipt_gate.py). Die 4
alten Tests, die 'eine Zeile erteilt den Pass' assertierten, sind auf den Receipt-Vertrag angepasst.
Marker-Funktionen (audit_records_for etc.) bleiben fuer audit_candidate_matrix. ruff clean.
kraxo and others added 22 commits August 27, 2026 00:07
…it, fail-closed (makellose-500 F2/F7)

Der Gegenleser: audit_candidate_ready war True mit internem PENDING_JUSTIFIED + DATA_BLOCKED (F2, P7),
und 7 rein lexikalische Checks erteilten PASS auf reinen Decoys inkl. einem NEGIERTEN Satz (F7, P8).
Rekonstruktion:
- F2: audit_candidate_ready = jeder RELEASE-ENTSCHEIDENDE Check ist PASS, AUSSER dem einen ausdruecklich
  externen offenen Audit (EXT.1 == EXTERNAL_PENDING). Ein internes PENDING_JUSTIFIED, ein internes
  DATA_BLOCKED (ein hier NICHT messbarer Check), ein unbekannter Verdikt, ein FAIL oder ein ungebundener
  Versions-Pin ist NICHT ready. Fail-closed by default.
- F7: die 7 praesenz-/lexikalischen Checks (C1.2/C1.3/C9.2/C10.3/C10.4/C10.5/C11.3) sind jetzt INFORMATIV
  (gemeldet, nie release-entscheidend) — ein Schluesselwort / Verzeichniseintrag erteilt keine Bereitschaft.
- status_boundary wird BERECHNET, nie als statische 'all internal green'-Behauptung.

Kaskade der F6-Aenderung: c12_1 (pre-tag) FAILt jetzt ohne signierten Receipt -> die Matrix ist korrekt
nicht-ready. 8 Qualifikations-Tests (test_audit_candidate_ready_logic.py). Die alten Tests, die
unbedingte Bereitschaft / eine Prosa-Zeile assertierten, sind auf den fail-closed-Vertrag angepasst.
ruff clean.
…Spur 1)

Ein separates Harness, das die GATES bewertet, nicht das Produkt: jede der 15 Pflicht-Gegenbeweisklassen
wird als bekannter Defekt EINGESAET und das zustaendige Gate muss ihn ERKENNEN. Akzeptanz 15/15 detektiert
MIT gruener Positivkontrolle je Gate (ein Riegel, der alles ablehnt, ist kein Riegel). Gemessen: 15/15,
Positivkontrollen gruen. Deckt type_confusion (F1/F3/F4/F5), pre_tag (F6), audit_candidate (F2/F7).
Regressions-Test tests/test_gate_qualification_harness.py haelt es.
…ellose-500 Phase 5, iter9 5th round)

Die Deep-Gate-5.-Runde fand drei ueberlebende never-raise-Verstoesse auf oeffentlichen Verify-Flaechen:
- Linse A (FLAGGSCHIFF): verify_bundle -> sdjwt_issue.check_binds_bundle -> (p.get("receipt") or {}).get()
  crashte roh (AttributeError) auf einem truthy NON-DICT receipt — das `(x or {})`-Idiom ersetzt nur FALSY.
  Fix-the-Class: _membership.as_dict (EINE geteilte Helferin, wie anchors_chia_add._as_dict); sdjwt_issue
  nutzt sie. Sweep: die anderen `(x or {}).get`-Stellen (anchors_ots, decision) sind upstream geguardet.
- Linse B: kbjwt.holder_key_from_cnf's except (ValueError, TypeError) fing nicht die BundleFormatError von
  _b64url_decode's DoS-Guard -> ein typisierter Crash statt des dokumentierten None. except verbreitert.
- Linse C (renewal, EXPERIMENTAL): drei Roh-Crashes — Riesen-.time-Magnitude in _ats_content (typisiert
  vor dem Render wie token()), unhashable hash_alg an den Hash-Lookups, non-iterable .signatures.

Alle mit ausfuehrbarem Regressionstest (tests/test_iter9_product_fixes.py, 21 Faelle). Der Pruefstein
(outcome.py status=[]/{}) war bereits am HEAD via is_member behoben.
…ellose-500 Phase 5 cascade)

Der as_dict-Fix (7581bd7) machte den sdjwt_issue-Import isort-kanonisch zu 'as_dict, is_member'. Der
Scanner test_the_guard_is_actually_imported_where_it_is_used prueft einen LITERAL-Substring
'_membership import is_member' und flaggte den legitimen Multi-Import falsch. Die INTENTION bleibt exakt
(is_member wird importiert wo genutzt); die Pruefung ist jetzt import-ordnungs-robust (Regex statt
Substring). Plant-and-catch (test_a_planted_unguarded_site_is_found) unveraendert gruen.
…00 candidate

Spur 2 ran both adversarial deep-gate lenses on the frozen candidate (befa2ac);
both returned WIDERLEGT with executable PoCs. All four are fixed at the CLASS level
(not the instance) and each fix is verified by reproducing its PoC against the fix.

renewal.py — Linse C (8 neighbour crashes on the SAME lines an earlier round fixed):
  the module's own never-raise/fail-closed contract was broken by a raw f-string
  interpolation of a malformed ATS field (giant int -> CVE-2020-10735 int->str cap;
  non-iterable .signatures; non-str). Fixed the CLASS: _refuse_giant_int() on the
  signed-bytes paths (token()/_ats_content, typed refusal — a shortened render would
  change signed bytes), render_safe() migration on every remaining diagnostic site
  (662/741/764/770/783/788/790/867), a token() signatures-shape guard, a
  _verify_ats_signature catch, and the evaluate_renewal_policy `now`-param guard.
  New property test tests/test_renewal_field_magnitude_never_raise.py sweeps every
  field x {giant,non-iter,non-str} (42 cases) + a plant-and-catch meta-check.

sdjwt_issue.py — Linse A neighbour: check_binds_bundle read `p = _jwt_payload(compact)`
  unguarded; a bare-array payload crashed `p.get()` in the flagship verify path. Now
  `p = as_dict(...)` — a non-dict payload can never bind (reject, no false-accept).

gate_qualification_harness.py — Linse A #1 (harness vacuity): cc01/cc02 re-transcribed
  the gate's rule instead of calling it, and cc10==cc15 (14 distinct of 15). cc01/cc02
  now OBSERVE gate.evaluate() (empty population / seeded parse-skip), class 15 is a
  distinct inventory-disagreement counter-example, + a real-population positive control.
  15/15 genuine, 15 distinct functions.

pre_tag_receipt_lib.py — Linse A #2 (trust-anchor seam): load_trusted_pubkeys read the
  WORKING tree while the receipt binds the COMMITTED HEAD^{tree}; a dirty tree could
  inject a pubkey and self-sign. Now reads the committed blob (git show HEAD:...), so
  the trust set is bound by the same digest. Exploit reproduced -> blocked; committed
  key + valid receipt still verifies.

type_confusion --strict: 57/57, 0 raw_exceptions, complete=True. harness: 15/15. ruff clean.
…kip visible, record fixture provenance

Owner NACHTRAG (20260826T213824Z) — protect the Inspect-AI surface the release ships,
measured on the Farmer where PyPI is reachable.

I1 — measured end-to-end against inspect_ai 0.3.260 (the version the pin resolves today,
  published 2026-08-21) in a fresh env: (1) hook found via the inspect_ai entry-point
  group, (2) on_task_end wrote a signed receipt, (3) verify_bundle + decode_eval_claim
  pass with no model leak, (4) no PROOFBUNDLE_EMIT -> no receipt. All four hold. The pin
  ceiling was a BLIND `<0.4` (admitted unmeasured versions); it is now the MEASURED
  ceiling `<=0.3.260` (inspect + dev extras). 0.3.258's scorer-semantics change did not
  break the hook (0.3.260 is after it).

I2 — the inspect hook test skipped silently when inspect_ai is absent (a non-run reading
  as coverage). Way B: the skip stays in the hermetic lane but becomes a FAILURE under
  PROOFBUNDLE_RELEASE_LANE=1; the enforcement lives in the test, so a CI-config regression
  cannot hide it. Meta-test proves both branches; in the release lane (0.3.260 installed +
  marker) all 6 inspect tests EXECUTE.

I3 — the fixture safety_refusal_demo.eval carried no recording version. Sidecar records it
  (inspect_ai 0.3.217, eval_log_version 2); 0.3.260 both reads it and writes log v2, so the
  format is stable 0.3.217..0.3.260 and no second fixture is needed. A test binds the
  sidecar to the fixture header.
…found

The Spur-2 re-gate (both lenses on 0f05619) came back WIDERLEGT — each found a SIBLING
of a just-fixed instance, so the class was still open. Both are now closed STRUCTURALLY,
not instance-by-instance, and each is proven by plant-and-catch.

Product lens (renewal): the prior fix guarded `now` but left its sibling operand
policy.max_ats_age (raw TypeError from untrusted policy JSON), and _refuse_giant_int /
render_safe were scalar-only so a giant int NESTED in a container field escaped via
repr() recursion. Fixed the CLASS:
  - budget.render_safe: container-safe (repr in try/except -> "<unrenderable T>") — the
    render-class home, fixes every _rs(container) site at once.
  - renewal._refuse_malformed_signed_fields: token()/_ats_content refuse a non-str
    string-field / non-int time TYPED (a container field cannot be signed bytes).
  - renewal._never_raise_verdict DECORATOR on verify_sequence + evaluate_renewal_policy:
    STRUCTURAL never-raise — any residual/future malformed-input crash becomes a
    fail-closed VerificationResult, not a raw traceback. The contract is enforced by
    construction, which is the fix-the-class the deep gate kept pushing toward.
  - explicit guards: max_ats_age / deprecated_algs / data_digests + from_dict validation.
  Verified: 21/21 PoC vectors + property test extended to 59 cases + plant-and-catch on
  the net (removed -> RuntimeError propagates -> red). No false-accept (lens confirmed).

Gates lens (harness): cc04/05/06 exercised the gate HELPERS in isolation and the positive
control only checked population_complete — nothing bound acceptance to evaluate()'s
HEADLINE never_raise_ok verdict, so stripping the _exercise_nested wiring (or hard-wiring
violations=[]) blinded the gate while the harness stayed 15/15. Added cc16: seeds an
IN_SCOPE surface whose inner leaf raw-crashes and confirms the FULL evaluate() reports
never_raise_ok=False + raw>0. Plant-and-catch: stripping the wiring turns cc16 red
(15/16). + a real-never-raise positive control. Harness now 16/16.

full suite 2550 passed / 0 failed  ·  type_confusion --strict 57/57 0 raw_exc  ·  harness 16/16  ·  ruff clean
…rness verdict

The round-2 fix added ONE observing class (cc16, nested-leaf). The Gates re-gate on
880ca55 returned WIDERLEGT: four SIBLING detection wirings that feed the gate's
release-deciding verdict were still bound only in isolation (cc04/05/07 test the helpers),
so stripping any one stayed 16/16 green — the same fix-instance-not-class pattern.

Generalise cc16's technique into a shared _seed_evaluate() helper + one observing class per
release-deciding wiring, each seeding a defect reachable ONLY through that wiring and
observing the FULL evaluate() headline verdict:
  - cc16 nested-leaf (never_raise_ok / raw)
  - cc17 whole-arg _exercise
  - cc18 str_matrix (F5, Union[dict,str] string primary)
  - cc19 NON_JSON (F3) exercise
  - cc20 completeness (evaluated == population_size)

Harness now 20/20. Each binding is proven load-bearing by a wiring-strip meta-test baked
into tests/test_gate_qualification_harness.py: strip a wiring in type_confusion_gate and
EXACTLY the matching class goes red (a present-but-vacuous class — the cc01/cc02 failure
mode — would stay green). One-time isolation run confirmed each strip reddens ONLY its
class. MAINTENANCE INVARIANT documented: a new detection wiring needs a new binding class.

harness 20/20 · wiring-strip meta-test PASS (17→wholearg, 18→str_matrix, 19→nonjson, 20→completeness, each isolated) · type_confusion --strict 57/57 0 raw_exc (gate unchanged) · ruff clean
…dancy invariant

un's round-3 Gegenlesung flagged population_complete's `import_error==0` term as an
"unbound" completeness sub-term. Verified at source (No-Fake): every unresolved status
(IMPORT_ERROR / NEEDS_FIXTURE / NO_INPUT) is NOT counted toward `evaluated`, so any one
forces evaluated<population and cc20's `evaluated==population_size` binding already catches
it. Empirical proof: seed an IMPORT_ERROR surface -> evaluated=0/1, complete=False; strip
`import_error==0` -> STILL complete=False. So import_error==0 and no_input==0 are DEFENSIVE
REDUNDANCY of evaluated==population, not independent wirings (a dedicated class would share
cc20's mechanism and break one-class-per-strip isolation). un's D is a false-positive.

Documented on cc20 + pinned by test_unresolved_surfaces_all_reduce_evaluated: if a future
change ever counted an unresolved surface toward `evaluated`, import_error==0 would stop
being redundant and this test goes red first. Additive (comment + test); no binding change,
harness stays 20/20, gate unchanged, ruff clean.
…imal isolated seeds

The Gates T1 re-gate returned WIDERLEGT: `inventories_agree = not only_ast and not
only_runtime and not runtime_import_errors` was bound by cc15 as a WHOLE, but with a
non-minimal seed (replacing the entire runtime inventory tripped only_ast=57 AND
only_runtime=1 at once), so each conjunct still caught cc15's seed when a sibling was
stripped — no sub-term observed in isolation. Three live breaks (harness green + gate
blind): strip `not only_runtime` / `not runtime_import_errors` / `not only_ast`. Same
"bound as a whole, non-isolating seed" class the round-3 fix set out to kill, one term over.

Fix (the lens's own direction): a _seed_inventory(ast, runtime, errors) helper that controls
BOTH inventories with benign IN_SCOPE surfaces, so inventories_agree is the ONLY term that
can withhold completeness; then a MINIMAL seed per conjunct —
  cc15 not only_ast          : runtime = ast minus one   -> only_ast={x}, only_runtime=[], errors=[]
  cc21 not only_runtime      : runtime = ast plus phantom -> only_ast=[], only_runtime={x}, errors=[]
  cc22 not runtime_import_errors : runtime = ast, errors=[…] (a broken submodule must withhold)

Harness now 22/22. Each conjunct proven load-bearing + isolated by the wiring-strip meta-test
(now 7 strips: 4 never_raise/completeness wirings + 3 inventory conjuncts) — strip one term ->
EXACTLY its class reddens (verified: cc15/cc21/cc22 each isolated). Every release-deciding
term of never_raise_ok and population_complete is now bound with a minimal isolated seed.
type_confusion --strict 57/57 0 raw (gate unchanged) · ruff clean.
…mes path)

The Gates T1 re-gate returned WIDERLEGT: the _exercise_nested wiring has four detection
capabilities, but cc16/cc04 seed _FIELD_CACHE and crash at DEPTH-1, leaving depth-2
traversal (v2) and the real _field_names AST extraction unqualified. Proven by a one-line
strip: `return v1` (drop v2) blinds never_raise_ok for a depth-2-only crasher while the
harness stays 22/22 green. The gate's own comment declares depth-2 load-bearing.

Fix: cc23 builds a REAL importable proofbundle.* module (so _field_names reads its SOURCE,
no cache seed) whose verifier survives every depth-1 payload and raw-crashes ONLY at depth-2
(outer.get('runs') -> per-entry entry.get('status') unhashable). Drives the full evaluate().
Harness now 23/23; the 8-strip meta-test now includes the v2 strip -> cc23 reddens (verified
non-vacuous: unmutated detects True, v2-stripped False).

RESIDUAL, named honestly (P3, not a live defect): _field_names' subscript vs in-compare AST
branches are NOT individually isolated — a subscript on a missing key raises KeyError (still a
crash), so a crash-critical field cannot be extracted via subscript-ONLY without an
in-compare/.get guard that re-extracts it (the Gates lens noted the same: "not each given a
separate real-module crasher"). cc23 binds depth-2 + the real _field_names extraction as a
group (a total extraction break reddens it); the finer branch split is future-regression
detection of a field-extraction sub-mechanism, and on the clean tree finds no real crash.

type_confusion --strict 57/57 0 raw (gate unchanged) · ruff clean.
…te found unbound

The final Gates-T1 re-gate on 271fbba returned WIDERLEGT: cc16-cc23 bind the never-raise
wirings only through a MOCKED _classify (_seed_evaluate sets status="IN_SCOPE" directly), so
the REAL router that ENABLES nested detection -- _is_json_primary's `"Any" in text`/dict->
IN_SCOPE branch -- was unbound. A one-token strip of `"Any" in text or ` reclassifies an
Any-primary surface IN_SCOPE->NON_JSON (no nested matrix, depth-2 crash missed) while the
harness stayed 23/23 + positive controls green. Same class: the bytes->NON_JSON exclusion and
the RecursionError handler. This is the round 2-4 failure mode one layer out -- a wiring bound
only in isolation (through a mock), not through the real evaluate() router.

Fix (fix-the-CLASS): three classes that plant a REAL importable proofbundle.* module and drive
the FULL evaluate() with the REAL _classify (only discover/_runtime_inventory/_parse_skips
patched, NEVER _classify), so the real routing is the ONLY thing that can carry the detection:
  cc24 Any-primary (name NOT in _JSON_PRIMARY_NAMES) -> IN_SCOPE via "Any" -> nested matrix
  cc25 bytes-primary (name IN _JSON_PRIMARY_NAMES)   -> NON_JSON via bytes-exclusion -> bytes matrix
  cc26 unbounded recursion -> RecursionError -> violation (bounded-depth defence owed)

Harness now 26/26; the strip meta-test now carries 11 wirings (8 + the 3 real-router branches)
-- strip one -> EXACTLY its class reddens, verified non-vacuous + ISOLATED (each target missed,
the other two still caught) and the subprocess genuinely runs (system python3 prints True intact,
False under strip -- not an import-error false green).

Gate + product byte-identical to 271fbba (diff = harness + its test only): the product
WITHSTANDS and the gate qualification carry. type_confusion --strict 57/57 0 raw. ruff clean.
…the NON_JSON kind routes

Round 6 bound the "Any" IN_SCOPE token (cc24), but that was itself an INSTANCE fix. A proactive
neighbor sweep (fix-the-class, not waiting for the re-gate to find each one) showed _is_json_primary
routes a JSON-object primary IN_SCOPE via SEVEN string tokens, and stripping any of dict/Dict/Mapping/
list/List leaves a nested-leaf surface of that annotation routed NON_JSON (no nested matrix, depth-2
crash missed) with NO fallback while the harness stayed 26/26 -- the exact Any-hole class, five
siblings over. The NON_JSON side has the same class: stripping the _primary_kind int/path branch
mis-routes a kind-unique crash (2**64 / a bad path) out of its matrix with no fallback (measured).

Fix (fix-the-CLASS, not five more instances):
  cc27 binds EVERY IN_SCOPE routing token -- one real-module plant per token (dict/Dict/Mapping/list/
       List/Any), annotated to route IN_SCOPE ONLY via that token, each with the depth-2 nested-leaf
       defect through the REAL _classify. Strip any token -> its plant routes NON_JSON -> missed ->
       cc27 reddens. ("Union[dict" is dominated by "dict" -- any annotation that matches it also
       matches "dict" -- so it is redundant, not a separately reachable route.)
  cc28 binds the NON_JSON kind routes that have a REAL hole: int (2**64 is unique to _INT_PAYLOADS)
       and path (a leading "/nonexistent" is unique to _PATH_PAYLOADS). kind_str/kind_bytes/excl_*
       stay P3: they mis-route into the "other" bytes+compact_str union or fall back via the name
       check, so the crash is still caught (no missed crash) -- documented, not silently dropped.
  A COVERAGE GUARD test parses _is_json_primary and asserts cc27's table lists every IN_SCOPE token
       (generator-hardening: a NEW token cannot be added to the gate without a binding; proven
       non-vacuous by a planted "FrozenSet" token that makes the guard fail).

Harness now 28/28; the strip meta-test carries 18 wirings (strip one -> its class reddens, verified
non-vacuous + the subprocess genuinely runs -- system python3 True intact, False under strip). Gate +
product byte-identical to 271fbba/c6884d9 (diff = harness + its test only): the product WITHSTANDS
and the gate qualification carry. type_confusion --strict 57/57 0 raw. ruff clean.
…he RecursionError CLASS

The round-7 re-gate returned WIDERLEGT (independently confirming a proactive neighbor sweep): the gate
defends RecursionError in TWO exercise paths, but cc26 binds only the whole-arg arm (_exercise ~418).
cc26 recurses UNCONDITIONALLY, so the whole-arg arm masks it and the NESTED arm (_exercise_nested ~446)
was unbound with NO fallback -- a public verifier that passes its outer shape check then recurses
unboundedly only on a nested `runs` list (the DoS/parser-differential class the gate's own comment says
"MUST be defended") is missed: strip the nested `except RecursionError: violations.append(...)` ->
never_raise_ok stays True, raw=0, harness stays 28/28 green, no fallback (no whole-arg payload carries
runs-as-a-list; the nested arm sits before `except Exception`).

Fix (fix-the-CLASS, "RecursionError in every exercise path", not the instance):
  cc29 plants a real importable proofbundle.* verifier that survives every whole-arg payload and
       raw-RecursionErrors ONLY on a nested runs list, driven through the REAL router; asserts
       never_raise_ok=False. Strip the nested arm -> cc29 reddens (verified). Harness 29/29.
  A COVERAGE GUARD test (test_every_recursionerror_arm_is_bound) finds EVERY
       `violations.append(f"RecursionError...")` arm in the gate and asserts the strip meta-test carries
       a strip for each -> a NEW RecursionError arm cannot be added unbound (proven non-vacuous: a
       planted phantom 3rd arm makes the guard fail). Sibling of cc27's IN_SCOPE-token coverage guard.

The round-7 re-gate ALSO confirmed (independent lens, converged with my sweep): the routing class is
fully bound (every routing strip reddens a class), the P3 classifications (kind_str/kind_bytes/excl_*)
are accurate (each has a fallback), and the completeness conjuncts are not a real missed-defect on the
tree. So round 8 closes the one proven remaining must-fix. Gate + product byte-identical to
271fbba..f76bb32 (diff = harness + its test only). type_confusion --strict 57/57 0 raw. ruff clean.
…hrough the real router

The round-8 re-gate returned WIDERLEGT: _classify SELECTS the never-raise matrices for an IN_SCOPE
surface via union_str-conditional assignments (payloads L249, str_matrix L250 -- the F5 fix). cc18 bound
only the str_matrix CONSUMPTION through a MOCKED _classify, and the anchor mut stripped the consumption
(L575), not the assignment (L250). So the matrix-SELECTION the real _classify performs for a
Union[dict,str] primary was unbound: strip `"str_matrix": _COMPACT_STR_PAYLOADS if union_str else []` ->
`[]` and a real Union[dict,str] string-primary raw crash is MISSED (raw 8->0) with NO fallback (whole-arg
is _NONSTR, nested needs dict fields), while the harness stays 29/29 green. The hole governs the real
shipped entry verifier proofbundle.bundle.verify_bundle (Union[dict,str]). Same class as the round-5/6/7
routing WIDERLEGT ("bound only through a mocked _classify"), one wiring over: matrix-selection not routing.

Fix (fix-the-CLASS "every matrix _classify selects is exercised through the real router"):
  cc30 plants a real Union[dict,str] verifier crashing ONLY on a string, driven through the REAL router;
       asserts never_raise_ok=False. Strip L250 -> cc30 reddens (verified); its sibling L249 (payloads)
       reddens cc26. Harness 30/30.
  A COVERAGE GUARD (test_every_matrix_selection_assignment_is_bound) finds every
       `"<key>": <...> if union_str else <...>` matrix-selection assignment in _classify and asserts the
       strip meta-test carries a strip for each -> a new matrix key / changed assignment cannot be added
       unbound (non-vacuous: a planted phantom assignment fails the guard). Third coverage guard (after
       IN_SCOPE-tokens cc27 and RecursionError-arms cc29), same generator-hardening.

Gate + product byte-identical to 271fbba..6716914 (diff = harness + its test only). type_confusion
--strict 57/57 0 raw. ruff clean.
…d-9 WIDERLEGT)

The round-9 re-gate returned WIDERLEGT: _field_names extracts dict-key field names via THREE disjoint AST
branches (.get / obj["x"] subscript / "x" in obj in-compare); the nested-leaf matrix can only crash a field
it extracted. cc23 bound _field_names only as an ALL-OR-NOTHING group; a PARTIAL break of the SUBSCRIPT
branch was unbound with NO fallback. A strict exact-keys shape check (set(obj.keys()) != {"status"}) makes
obj["status"] unconditional (no KeyError fallback), and "status" is reachable ONLY via the subscript branch
(the {"status"} set is an ast.Set; obj["status"] in {...} is a Compare whose LEFT is a Subscript, not a
Constant). 203 real fields across 26 real modules (incl. crash-relevant status/code/errors on outcome/
run_ledger/... -- the module family of the four original P1 findings) are reachable ONLY via subscript.
Strip the subscript branch -> a real subscript-only nested-leaf crash is MISSED (never_raise_ok wrongly
True, raw 4->0), harness stays 30/30, positive controls green. It also REFUTED the harness's own
"proven-fallback P3" note (the KeyError fallback does not exist under an exact-keys shape check).

Fix (fix-the-CLASS): cc31 plants a real proofbundle.* verifier whose crash-critical field is reachable by
_field_names ONLY via the subscript branch (the re-gate's exact victim), driven through the real router;
asserts never_raise_ok=False. Strip the subscript branch -> cc31 reddens (meta-test mut 32, reliable). The
.get branch stays bound by cc23/24/27, in-compare by cc29 (full-harness verified). A field-extraction
coverage guard asserts all three branches still exist + cc31/its strip are present -> the round-9 hole
cannot silently reopen (the per-branch isolation strip is env/cache-flaky in a minimal-env subprocess, so
documented rather than asserted via the isolation meta-test).

Harness 31/31; 7 harness tests pass. Gate + product byte-identical to 271fbba..899659e (diff = harness +
its test only). type_confusion --strict 57/57 0 raw. ruff clean.
…+ a pre_tag coverage guard

The round-10 re-gate (WITHSTANDS on d8c1331) named P3-1 (highest value): verify_receipt's `schema` check was
unbound by BOTH the harness (cc08-10) AND the sibling test test_pre_tag_receipt_gate.py -- a wrong-schema
trusted-signed receipt would verify if the check were stripped. The re-gate's own note: the pre_tag surface
lacks the generator-hardening the type_confusion surface has (the coverage guards test_cc27_table_…,
test_every_recursionerror_arm_is_bound, …), so a FUTURE verify_receipt check could be added unbound. (The
spec-mandated Version/GateSource/Exitcode checks are already bound by the sibling test -- my earlier
harness-only sweep missed that; the re-gate corrected it, so cc32 is redundant for those but needed for schema.)

Fix (fix-the-CLASS, the same generator-hardening as type_confusion):
  cc32 binds every verify_receipt binding-field check (schema/version/gate_source/audit_exit) -- a
       valid-EXCEPT-one-binding signed receipt must be REJECTED; strip any check -> that case is wrongly
       accepted -> cc32 reddens (proven by test_pretag_binding_check_strips_redden_cc32, incl. the schema
       strip that was P3-1). Harness 32/32.
  test_every_pretag_binding_check_is_bound enumerates verify_receipt's `receipt.get("<field>") != ...`
       binding-field checks and asserts each is covered by cc32 (or cc09 for subject_tree) -> a NEW binding
       check cannot be added unbound. The pre_tag surface now has the coverage guard it lacked.

Gates (type_confusion_gate, pre_tag_receipt_lib, audit_candidate_matrix) + product byte-identical to
271fbba..d8c1331 (diff = harness + its test only). type_confusion --strict 57/57 0 raw. ruff clean.
…signature checks; form-robust AST guard

The un gegenlesung of round 11 (3588224) returned REJECT on one valid point: the round-11 coverage guard
enumerated verify_receipt's binding checks by REGEX (`receipt.get("X") != `), so a differently-formed check
(subscript, `is None`, `signer not in ...`) would ESCAPE it and stay silently unbound — my round-11 docstring
claim "a NEW binding check cannot be added unbound" was therefore overstated (true only for that one form).
Verified at the source: verify_receipt's 5 binding-field checks are all `receipt.get("X") != ` form (so the
round-11 guard + cc32 were correct for the CURRENT state, P3-1 stays closed), but the checks that already
escape the regex — signer-trust (#8) and signature-verify (#10) — are exactly the named P3-2.

Fix (fix-the-CLASS, closes P3-2 + makes the round-11 claim TRUE):
  cc32 now binds EVERY release-deciding verify_receipt check, not just the binding fields: added
       untrusted_signer (valid self-sig, signer not in trusted set -> #8 must reject) and tampered_signature
       (trusted signer, signature does not verify -> #10 must reject). Proven load-bearing: stripping
       `if signer not in trusted_pubkeys:` OR `if not ok:` makes cc32 go red (the extended strip-test).
  test_every_pretag_binding_check_is_bound (regex) -> test_every_pretag_rejection_is_bound (AST count-pin):
       counts verify_receipt's `return False` rejection paths (pinned 11) regardless of condition FORM; a NEW
       rejection changes the count and reddens the guard, forcing a cc32 case. Robust to un's subscript/is-None
       counterexamples. #9 isinstance(sig,str) stays inert (subsumed by #10's fail-closed b64decode except).

Gates (type_confusion_gate, pre_tag_receipt_lib, audit_candidate_matrix) + product byte-identical to
d8c1331 (diff = harness + its test only). Harness 32/32, cc32 rejects all 6 release-deciding cases, full
harness test file 9 passed, ruff clean.
…ot the full tree (option B, owner-GO)

The signed pre-tag receipt bound subject_tree_digest = HEAD^{tree}. Committing the receipt into
audit_artifacts/500/ CHANGES HEAD^{tree}, so the gate (which also read HEAD^{tree}) then computed a
different tree and REJECTED the receipt -- circular, no fixed point, proven 2026-08-27 with a valid
trusted signature (tree 872ce5b bound vs 4e2dd695 computed). This blocked EVERY committed receipt,
so v5.0.0 could not release even with the key. The makellos harness never caught it: it exercised
verify_receipt's LOGIC with fixed tree constants, not the real produce->commit->verify FLOW.

Fix (owner-GO Fix-Option B, OA-2e43c08dd6): subject_tree_digest binds HEAD:src/proofbundle -- the
published package subtree -- in both pre_tag_receipt.py (producer) and pre_tag_audit_gate.py (gate).
audit_artifacts/ lies OUTSIDE src/proofbundle, so committing the receipt no longer moves the bound
tree, and future audit records can never re-break it either. The receipt now attests exactly the
bytes that ship to PyPI; the gate's separate gate_source_digest still binds the gate itself, and the
version/exit-code/signer-trust/signature checks are unchanged.

Proven not a weakening (tests/test_pre_tag_receipt_commit_flow.py, the real subprocess+git flow the
harness lacked): a committed receipt now VERIFIES (receipt-verified=True, gate exit 0), AND a
src/proofbundle change after signing is still REJECTED (does not bind THIS tree, gate exit 1).

verify_receipt LOGIC unchanged -> harness 32/32, cc32 holds, type_confusion 57/57 raw=0, ruff clean,
full pre_tag/gate/receipt suite 109 passed. The gate change resets the round counting: full
Harness + Deep-Gate re-verification runs before the tag (owner directive).
… minus audit_artifacts (owner-GO)

Round 13 (option B, binding HEAD:src/proofbundle) fixed the circular binding but the deep-gate REFUTED
it: binding only the package subtree unbinds pyproject.toml, so a dependency injected AFTER signing
shipped past the gate. Executable exploit (re-gate a1daeb4bd, WIDERLEGT): sign+commit a valid receipt
(gate accepts) -> append "evil-backdoor-pkg==6.6.6" to [project.dependencies] -> src/proofbundle
unchanged -> gate STILL ACCEPTS -> the backdoored dep flows to Requires-Dist and ships.

Fix (owner-GO option C, OA-89102a2f60): subject_tree_digest is a stable sha256 over the top-level
git-ls-tree-HEAD entries EXCLUDING audit_artifacts/. One shared function in pre_tag_receipt_lib.py, used
by both the producer and the gate so they can never drift. audit_artifacts/ stays outside the subject
so the binding is still committable (circularity stays fixed), while src/proofbundle, pyproject.toml
(deps), scripts/ (the gate AND the verifier lib) and every other release surface are now bound.

Proven end-to-end (tests/test_pre_tag_receipt_commit_flow.py, now a permanent regression for the exploit):
committed receipt VERIFIES (exit 0), the pyproject dep injection is REJECTED (exit 1), a src change is
REJECTED. verify_receipt LOGIC unchanged -> harness 32/32, type_confusion 57/57 raw=0, ruff clean, full
pre_tag suite 109 passed. Round counting reset: full Harness + Deep-Gate re-verification before the ceremony.
… receipt mode

P1-A (four-lens review): pre_tag_audit_gate.evaluate() now inserts the repo's src into sys.path
beside the existing scripts/ insertion, so the CI's bare `python pre_tag_audit_gate.py` (no
PYTHONPATH=src) no longer crashes on `import proofbundle` when a receipt is present; the
verify_receipt call is wrapped fail-closed (any exception -> REJECT, never a crash or a false
accept). src is the tree the receipt already binds via subject_tree_digest, so no new trust
surface. Verified: the gate run bare rules + verifies (exit 0, no traceback).

Keyless mode: pre_tag_receipt.py gains --emit-payload (write canonical_bytes(context), no privkey)
and --assemble (wrap context + an external signature, self-check fail-closed) for the Mac
key-custody handshake -- the private key half never reaches the Farmer. The inline path is
unchanged. The gated core (canonical_bytes / verify_receipt in pre_tag_receipt_lib.py) is
byte-identical (F2). Verified end-to-end: emit -> sign -> assemble -> gate verifies; a bad
signature at assemble is refused; inline is unregressed; harness 32/32; ruff clean.

Owner-GO Option A (G1 = A, QITEM-RELEASE-V5-ABSCHLUSS-01). A green re-gate on this head is the
condition for the tag.
…ate record

Release-prep duties for v5.0.0 (RELEASE.md gate + per-release list), on the branch inside the PR:

- docs/release_scope/5.0.0.md: the written scope list (In / Out / Why-MAJOR), grounded in the
  41-file src diff since v4.0.0 (the reported-version-status feature + the makellose-500 never-raise /
  type-confusion hardening sweep across renewal/merkle/kbjwt/sdjwt/tlogproof/anchors/... + the pre-tag
  ceremony infrastructure). The 4.0.0 Nachtrag lesson: a scope list must name what actually ships.
- scripts/audit_candidate_matrix.py: version_pin 3.6.0 -> 5.0.0 (the matrix must test the shipping
  version; it was in DRIFT before). The audit-candidate-matrix job is advisory / not a required check,
  so the pre-receipt DATA_BLOCKED state does not gate the PR merge or the release build.
- audit_artifacts/500/DEEP_RUN_RECORD_500_ITER8_CEREMONY.md: the current deep-gate WITHSTANDS record
  for the ceremony code 2b8a16e (all 5 targets held with executable proofs; src product byte-identical
  to the re-gated candidate, so the verdict carries to the final tree; the signed pre-tag receipt binds
  the exact tagged tree at tag time via the Mac key handshake).

Already in place: 3-file version single-sourced at 5.0.0 (pyproject / __init__ / CITATION.cff);
CHANGELOG carries the [5.0.0] section; COMPATIBILITY is conceptual (no version literal); README
unversioned. Verified: src/ byte-identical to 2b8a16e; type_confusion 57/57 raw=0;
check_version_and_changelog OK.

Owner-GO Option A (G1 = A, QITEM-RELEASE-V5-ABSCHLUSS-01).
Comment thread scripts/pre_tag_audit_gate.py Dismissed
kraxo added 2 commits August 27, 2026 11:47
…x, not the literal) + ruff E702

The prior release-prep commit (4c88ca9) bumped `audit_candidate_matrix` version_pin 3.6.0 -> 5.0.0.
That was WRONG and the PR CI caught it: the file's own docstring (deep-gate finding L6-01) says the
fix is the BINDING (`version_pin_binding` detects the drift and withholds readiness), NOT editing the
literal — "editing the literal to 5.0.0 ... recreates the class at the next release". And
`test_drift_withholds_readiness` enshrines "the live repository IS the drift case (pinned 3.6.0,
package 5.0.0)" as the intended state. Reverted to 3.6.0; the matrix ships honestly reporting
"pinned to 3.6.0, says nothing about 5.0.0". Scope list note corrected accordingly.

Also splits 4 pre-existing ruff E702 (semicolon) statements in
tests/test_anchor_extra_arg_type_confusion.py so `ruff check .` (the CI test job's lint step) is
clean — logic-preserving, a test helper only.

Verified: `ruff check .` clean; audit_matrix version_pin tests 8 passed at 3.6.0; type_confusion
57/57 raw=0; src/ still byte-identical to the re-gated 2b8a16e.

Known pre-existing non-blocker (not a required check, not caused by this release; fails identically
at the bc96fa5 baseline): hermetic-cleanroom's shipped-test-suite step needs `opentimestamps`, which
the [test] extra does not pull — the anchors OTS tests then hit a collection ModuleNotFoundError.
A packaging decision (add opentimestamps to [test], or gate the anchors tests) left for a separate
cleanup; the shipped artifact itself works (demo + emit/verify/tamper pass).

Owner-GO Option A (QITEM-RELEASE-V5-ABSCHLUSS-01).
…s for the anchor-free suites

Owner-GO Option A (QITEM-RELEASE-V5-ABSCHLUSS-01, OA-137e68010b): make the pre-existing red CI green
BEFORE the re-gate + release, measured against the two runners that run WITHOUT the [anchors] extra.

1. mypy (ci.yml `mypy src`): evalclaim.py `Invalid index type "Any | None"`. The
   is_member(comparator, _COMPARATORS) guard narrows comparator to a valid str at runtime, but mypy
   cannot see through is_member. `assert isinstance(comparator, str)` documents the invariant + narrows
   for the dict index. Behaviour-preserving: the branch is only reached when comparator IS a str.
   Verified: `mypy src` -> "Success: no issues found in 65 source files".

2. opentimestamps (published-artifact-gate pytest [test] AND ci.yml `unittest discover` [dev] -- both
   anchor-free): three OTS test modules were unguarded and broke the anchor-free suites:
   - test_anchors_ots_frozen_rptrust_guard.py: module-level opentimestamps import -> pytest collection
     ImportError. Now try/except -> _HAS_OTS + @skipUnless on both classes.
   - test_anchor_extra_arg_type_confusion.py: helper import -> 3 execution failures. _HAS_OTS +
     @skipUnless on the class (REGISTRY is OTS-only: verify_opentimestamps + verify_markovian).
   - test_gate_qualification_harness.py::test_15_of_15: the gate HONESTLY reports the surface
     population incomplete when anchor surfaces cannot import at runtime, so this positive control is
     N/A without anchors. @pytest.mark.skipif(not _HAS_OTS).
   The guard is the repo's own canonical pattern (8 sibling OTS modules) -- unittest- AND pytest-
   compatible, so both runners degrade to a clean SKIP, never an error. MEASURED not estimated (per
   conftest's own lesson): reproduced published-artifact-gate (sdist + [test] clean venv) + ci.yml
   (unittest discover + [dev]), both anchor-free.

This resolves the pre-existing boundary the 2b8a16e deep record named ("the opentimestamps env-gap
tests fail identically at the bc96fa5 baseline"). src touched (evalclaim) -> a fresh deep-gate re-gate
follows before the merge, per Owner Option A.

Verified green: ruff clean; mypy clean (65 files); pytest hermetic-cleanroom 2346 passed / 222 skipped
/ 0 failed; unittest discover OK.
@b7n0de
b7n0de merged commit bc33ae8 into main Aug 27, 2026
18 of 22 checks passed
@b7n0de
b7n0de deleted the fix/non-json-wireform-coverage-20260826 branch August 27, 2026 11:45
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.

2 participants