Skip to content

v6.4.0

Latest

Choose a tag to compare

@github-actions github-actions released this 28 Oct 08:59
· 312 commits to main since this release

A feature release following 6.3.1, with an experimental SCITT reader, a TRACE verifier and a stricter JSON reader. It adds proofbundle.scitt_ccf, an experimental reader for SCITT Transparent Statements with CCF receipts under the narrow profile scitt-ccf/v1, and proofbundle verify-trace for TRACE v0.2 Trust Records. The strict JSON reader now refuses non-finite numbers, integer literals outside the inclusive range -(2^53 - 1) to 2^53 - 1, and JSON bytes with a UTF-8 byte order mark or encoded as UTF-16 or UTF-32. Review the inputs your code hands a verifier when upgrading.

python -m pip install --upgrade proofbundle==6.4.0

Highlights

  • An experimental reader for SCITT Transparent Statements with CCF receipts. proofbundle.scitt_ccf reads statements under the narrow profile scitt-ccf/v1, which follows draft -06 of the CCF profile, and needs the [scitt] extra. It is reached only by calling it; no verify path of the package reaches it. The Rust cross verifier pb_verify_rs, which is not part of the Python package, gives the same results for statement signatures, Transparent Statements and consistency receipts over shared vectors.
  • proofbundle verify-trace verifies TRACE v0.2 Trust Records. It checks a record against keys, a time window and expectations that the relying party supplies, in the compact JWS form and in the embedded Ed25519 form. A pass is evidence for the relying party's policy, and safeForAutomation is false in every verdict.
  • The strict JSON reader rejects non-finite numbers and applies explicit integer and encoding limits. loads_strict refuses NaN, Infinity, -Infinity and every number that becomes infinite, integer literals outside the inclusive range -(2^53 - 1) to 2^53 - 1, and JSON bytes with a UTF-8 byte order mark or in UTF-16 or UTF-32, with BundleFormatError. With an sd_jwt_vc whose issuer signature verifies, a signed JSON payload refused for a duplicate member or one of these limits now demands and fails sd-jwt-bundle-binding. A payload that is not JSON at all remains outside the scope of that binding. Review code that hands a verifier such input.
  • The release run compares its files with the signed candidate. Before any upload, the tag run compares its sdist and wheel with the pair signed in the candidate block of the short soak and of the Rust differential, and stops when the bytes differ. The build tools are selected at fixed versions through .github/build-constraints.txt; the constraints name versions, not archive hashes.

Verify this release yourself

Use the checksums and build provenance to check the downloaded files. To check the signed pre-tag audit receipt, use a clone at the source commit identified by the build attestation. The commands, prerequisites and limits are in Verifying a published release.

What a receipt still does not prove

A valid signature binds the signed content to the signing key. It does not make a reported result true.

Measured on the reference machine

The budget cost axis was measured on the reference machine on 2026-10-27 using the code at e29a268. The record identifies the run as a reference measurement (ist_referenzmessung=true). All 12 expected axes and all 7 expected combinations passed, and no axis was skipped. The recorded calibration factor maschinenfaktor_schnellstes_ende is 1.0, and the timing-limit setting is latte_aus_der_klammer=schnellstes_ende. These results apply to the listed axes and combinations, not to every reader or input. The measurement record lists every axis and combination.

Details

All 30 entries, one line each, as in the CHANGELOG

Added

  • proofbundle.scitt_ccf reads a SCITT Transparent Statement with CCF receipts under the narrow profile scitt-ccf/v1 of ADR 0009 (PR278, pull request 406). It is experimental and needs the [scitt] extra (cbor2==6.1.4); without it every entry point refuses with no_lib, and nothing falls back to a weaker reader. Its own structural pre-scan, proofbundle._cbor_prescan, decides before cbor2 sees a byte: definite lengths, shortest heads, no floats, unique map keys, tag 18 only at the top and tag 1 only around an integer CWT time claim; cbor2, held in proofbundle._scitt_second_reader, must then read the same values. The three verifiers verify_statement_signature, verify_transparent_statement and verify_consistency_receipt are reachable only by calling them: the anchor type is not registered, and no verify path reaches it. The statement key is selected by the protected x5chain and never trusted from it, a key is built only by its own type, and consistency receipts have a closed status set of their own. A Receipt meets the CWT Claims rule of a Signed Statement, a text iss and sub in the protected header (RFC 9943 section 6), and a label whose value is null is present, not absent. tools/scitt_ccf_external/ holds the measuring tools and their recorded runs: the offline recomputation of third-party receipts, the comparison with microsoft/scitt-verifier, the measurement of section 4 for the working group in SECTION4_WGLC.md, and two differential corpus rounds registered on a local scitt-ccf-ledger.
  • pb_verify_rs verify-scitt-statement-signature gives the status and verdict of proofbundle.scitt_ccf.verify_statement_signature (PR290, pull request 408). It exits 0 for confirmed, 1 for statement_signature_invalid and 3 for a status that is no verdict about the signature, and a file over its input budget is malformed, exit 3, as Python reads the same bytes. src/scitt.rs shares no reading code with the Python module: its own CBOR reader, the COSE_Sign1 shape and the statement header types of the CDDL pass, and ES256, ES384, PS256 and PS384 through the RustCrypto crates p256, p384 and rsa, with a signature s >= n refused before the rsa crate would reduce it. The crate carries RUSTSEC-2023-0071, a timing leak of a private key; the verifier links only its public-key verification. The 91 vectors of tests/fixtures/scitt_statement_signature/vectors.json, written by tools/scitt_ccf_external/statement_signature_vectors.py and real statements of the two corpus rounds among them, hold both verifiers to the built verdict in tests/test_scitt_statement_signature_parity.py and crosscheck.py, and the parity register row is COVERED.
  • pb_verify_rs verify-scitt-transparent-statement gives the result of proofbundle.scitt_ccf.verify_transparent_statement (PR292, pull request 409). It prints one JSON object with the fields of its to_dict() but the prose, and exits 0 for confirmed, 1 for a status that says a check over the evidence failed and 3 for a status that is no verdict about it; the relying party's trust is a JSON file with scitt_statement_keys and scitt_ccf_services by issuer. src/scitt_transparent.rs shares no code with the Python module: it runs the CDDL pass of each receipt and of every proof family, computes the roots, holds each receipt to the rules of the profile and verifies its signature over the root. The vectors of tests/fixtures/scitt_transparent_statement/vectors.json, written by tools/scitt_ccf_external/transparent_statement_vectors.py, hold both verifiers to the built verdict, the Transparent Statements a local scitt-ccf-ledger served in the two corpus rounds among them, and the parity register row is COVERED.
  • pb_verify_rs verify-scitt-consistency-receipt gives the result of proofbundle.scitt_ccf.verify_consistency_receipt (PR297, pull request 410). It prints one JSON object with the fields of its to_dict() but detail, and exits 0 for confirmed, 1 for a status that says a check over the evidence failed and 3 for a status that is no verdict about it. src/scitt_consistency.rs shares no code with the Python module and runs section 4 in the order of CONSISTENCY_STATUS_ORDER, the receipt signature over the newer root under the relying party's service keys by issuer and kid. The vectors of tests/fixtures/scitt_consistency_receipt/vectors.json, written by tools/scitt_ccf_external/consistency_receipt_vectors.py, hold both verifiers to the built verdict, vectors over three signed states of a local scitt-ccf-ledger among them, where the service's own signature over the newer root stands with a proof computed from the ledger's leaves, and the parity register row is COVERED.
  • Section 4 consistency receipts get fifteen vectors, an independent checker and a Go reader (PR355, pull request 411). The consistency reader qualifies a success without both tree sizes as confirmed_without_tree_sizes, and pb_verify_rs verify-scitt-consistency-receipt gives that status, exit 0, with older_size, newer_size and newer_size_source null, as the Python call without sizes gives them: no size comes from the receipt, and so none from its ccf.v1 txid (-06 section 7.3). tools/scitt_ccf_external/section4_vectors/ holds the receipt-level vectors S4-01 to S4-15 with their generator, a checker that imports nothing from proofbundle and a manifest; they record two readings of section 4 and leave to the working group which one the draft means. The directory carries one key file, TEST_ONLY_es384_private_key.pem, an ES384 key made for these vectors alone and published on purpose, the key of no service, release or person. tools/scitt_ccf_external/section4_reader_go/ is a Go reader of section 4 written from the draft text.
  • A decision proof for the Rust verifier pb_verify_rs, and two workflows that run it. formal/z381_pruefentscheidung proves over the domains D1 to D6 that verify-bundle gives every input exactly one verdict of the closed list, without a panic and the same verdict twice, built from the bytes of the shipped verifier source; paritaet_python.py measures the Rust and the Python verdict over the same argv and file bytes; Kani harnesses with decidable bounds carry one planted counter-check per property. The workflow decision-proof runs the proof with the real binary and five mutants that must turn it red on every pull request, advisory until its first runs are measured. The workflow kani-nightly runs k5 and k6 every night from a Kani 0.67.0 whose crate and bundle are pinned by sha256.
  • proofbundle verify-trace verifies TRACE v0.2 Trust Records. proofbundle.adapters.trace_record.verify_trace_record checks a record against keys, a time window and expectations that the relying party supplies, in the compact JWS form (ES256, ES384, EdDSA) and in the embedded Ed25519 form of section 3.2.2, and cnf.jwk must name the trusted key that verified. The window values are bounded like every integer of the record. It was written from the specification alone and imports nothing of the TRACE reference; interop/trace/ compares it case by case with agentrust-trace 0.11.0. A pass is evidence for the relying party's policy, and safeForAutomation is false in every verdict. signature.verify_ecdsa_p384 is the ES384 primitive it uses, and the ECDSA inventory allows the curve name SECP384R1 for that verifier. docs/TRACE_VERIFIER.md describes the verifier and names tool_transcript as a gap between section 3.1 of the specification and its schema.
  • Repository tools, outside the installed package. An advisory language check for the title, the body and the commit messages of a pull request, scripts/pr_language_check.py, runs as the job pr-language in .github/workflows/pr-form.yml, also after an edit of the title or the body. It reads the repository's one word list in tests/_deutsche_prosa.py, and an event payload without its two commits, or a commit git does not have, is NOT MEASURABLE with exit 2 (tests/test_pr_language_check.py). A line of the body or of a commit message counts as German at two different words of the list, the title at one, and a word written in capitals, such as the acronym DER, counts as none there. It is not a required context of the ruleset protect-main and blocks no merge.
  • Repository tools, as a follow-up to pull request 377. The language check reads a commit message whole from its object, past a NUL byte and in the encoding its commit declares. It says NOT MEASURABLE with exit 2 instead of clean when no source is named or the range holds no commit, and instead of a traceback with exit 1 when the payload or a message cannot be read. It reads the text with every line boundary as a line feed, then after NFKC and without format characters, and it knows the capital eszett. A quotation in straight double quotes, also over a line break, or in an HTML <blockquote>, a finding identifier in capitals and a trailer that names a person are no prose, and a quote line and a trailer are read in linear time. Over the 820 commits on main from 2026-09-14 (UTC) to 84fc4a3c, four messages are red instead of thirteen, each with German prose. The check runs the copy of the pull request; that is named as the limit of an advisory check (scripts/pr_language_check.py, tests/test_pr_language_check.py).

Changed

  • The scitt-ccf/v1 reader verifies a consistency receipt and a statement algorithm as draft -06 of the CCF profile does (PR412, pull request 412). With the sizes the caller gives, a consistency receipt is held to 0 < m < n and to a first sibling on the right; the reader's own anchor rules of the -05 reading, the anchor position for both sizes and the count of left siblings for the older size alone, are gone, with their three statuses and the fields anchor_rule_checked and left_siblings_checked, since -06 says verification does not establish the anchor (section 4, lines 548-549). verify_statement_signature gives outside_profile for a statement algorithm other than ES256, ES384, PS256 and PS384, before any key is tried, as the profile check of verify_transparent_statement does (ADR 0009, Decision 6). pb_verify_rs follows both: verify-scitt-statement-signature reports such an algorithm as outside_profile, exit 3, and verify-scitt-consistency-receipt writes neither anchor field.
  • The strict JSON reader rejects non-finite numbers and applies explicit integer and encoding limits. proofbundle._strict_json.loads_strict refuses NaN, Infinity, -Infinity and every number that becomes infinite, an integer literal outside plus or minus 2^53 - 1 (the integer range the Rust verifier pb_verify_rs reads), and JSON bytes with a UTF-8 byte order mark or in UTF-16 or UTF-32; text with a leading U+FEFF stays the JSONDecodeError it was. It raises BundleFormatError, the error it already raises for a duplicate member, and every verifier that reads through it refuses such an input instead of reading a value RFC 8785 cannot represent. The SD-JWT and KB-JWT checks name the reason of a refusal in their detail instead of reporting every refusal as a duplicate key.
  • Repository build tooling, outside the installed Python runtime. The distribution-build steps in .github/workflows/release.yml, .github/workflows/published-artifact-gate.yml and .github/workflows/reusable-build-attest.yml set PIP_CONSTRAINT to .github/build-constraints.txt, which fixes build 1.6.1, packaging 26.3, pyproject_hooks 1.3.3, setuptools 84.2.1 and wheel 0.48.0. Their four build jobs fetch the full history for the build epoch. The two steps that produce wheels for attestation or publication also use scripts/build_reproducible.py --expect-backend-from to compare the wheel's recorded setuptools Generator with the constraint. The constraints select versions; they do not pin dependency archive hashes or prove that a later tag build has the bytes of an earlier candidate build. Measured at 58de280 with Python 3.12.15 and build 1.6.1, setuptools 84.2.1 and 80.9.0 built different wheels from the same commit, with the same sdist. A depth-1 clone at a head that touched only audit_artifacts/ selected that head's time and produced a different sdist. tests/test_every_build_path_pins_its_backend.py checks recognized forms in workflow text and the read-back refusals; its coverage limits are recorded in RESTRISIKO_640.md.
  • Repository tools, outside the installed package. Before any upload, .github/workflows/release.yml compares the sdist and wheel of the tag run with the pair the owner signed in the candidate block of the short soak and of the Rust differential, and stops when the bytes differ, when a signature does not verify under audit_artifacts/readiness_trusted_pubkeys.txt, when a source is of another version or kind, or when the two sources name different pairs (scripts/release_dist_matches_signed_candidate.py, tests/test_release_dist_matches_the_signed_candidate.py). scripts/build_reproducible.py --check-wheel reports the digest of the sdist it built, and with --frozen-pair it fails unless that sdist and both wheels are the frozen pair (tests/test_check_wheel_is_bound_to_the_frozen_pair.py). The binding of the signed readiness evidence leaves out the pre-tag receipt as the receipt's own tree digest does, so one evidence commit after the candidate can carry the receipt and the evidence together (scripts/sign_readiness_artifact.py, scripts/audit_candidate_matrix.py, tests/test_the_readiness_binding_excludes_the_receipt_as_the_receipt_does.py).

Fixed

  • The scitt-ccf/v1 reader checks the CDDL first, then the status (PR279, pull request 407). One pass validates the statement, its receipts and every vdp proof family completely against the CDDL of the CCF profile and the RFC 9995 parameter types before any status logic runs, and only that pass sets readable. A label or an array that is present must have its CDDL type, or the artifact is malformed; absence, and values v1 does not take, stay with the status logic. verify_transparent_statement, verify_consistency_receipt, verify_statement_signature and recompute_data_hash all enter through the pass (ADR 0009, Decision 16). Each check is one named rule, and a test takes each away in turn and must fail. The CWT issuer and subject are text on a statement and on each receipt, as RFC 8392 gives them, and another type is malformed; before, a subject of 7 on a receipt ended outside_profile with readable true. Before, a status could come before the shape was checked in full: verify_statement_signature confirmed a statement whose unprotected header carried a kid as text, an empty crit, CWT Claims as an array or a content type of -1.
  • The scitt-ccf/v1 reader follows F1 and F2 of draft -06 of the CCF profile (PR412, pull request 412). An inclusion proof with an empty path is a tree of one leaf, whose root is the leaf hash (-06 section 3), and a consistency proof keeps one path element or more (section 4); the reader refused the empty inclusion path as malformed, and the valid sample 01-one-leaf-empty-path of the profile repository, signed again under the repository's test key, now confirms. Both tree sizes of a consistency receipt are the caller's: without a newer size from the caller the result is confirmed_without_tree_sizes at most, whatever the receipt's ccf.v1 txid says, and a newer size the caller gives must agree with that txid (consistency_newer_size_mismatch). pb_verify_rs verify-scitt-transparent-statement reads an empty inclusion path as the reader does, so both verifiers give v19-path-empty the -06 verdict, and v39-path-empty-over-the-leaf confirms a one-leaf receipt in both.
  • pb_verify_rs reads its command line as the Python CLI does under CPython 3.13. It reads argv with args_os, so a path that is not UTF-8 reaches the file read and a text argument that is not UTF-8 is a usage error (exit 2) instead of a panic (exit 101). verify-bundle requires schema proofbundle/v0.1, signature.alg ed25519 and merkle.hash_alg sha256-rfc6962 before the cryptography, refuses --allow-pending without an anchor requirement, takes only receipt, preRegistration or statement for --anchor-target, and reads --expected-tree-size as Python's int() reads it. verify-bundle and verify-relation read their argument values before the signature, an option takes the next word only when argparse takes it as an argument, and verify-bundle, verify-relation and verify-relation-statement read the words after the subcommand with argparse's rules, --pub included. These readings follow CPython 3.13 (Unicode 15.1), and 3.12 reads the measured digits the same way. Under Python 3.10 and 3.11 a digit that Unicode added later, such as a Kawi digit in --expected-tree-size, is a usage error for the Python CLI and a number for pb_verify_rs; under Python 3.14 a digit of Unicode 16.0, such as a Garay digit, is a number for the Python CLI and a usage error for pb_verify_rs. A help word and an option that only Python carries out stay different on purpose; CROSS_IMPLEMENTATION_REPORT.md states both as a contract, and names the earlier relation form without --pub, which pb_verify_rs still reads and the Python CLI refuses, as a residual.
  • The independent cap1 verifier cap1_verify_rs reads argv with args_os, so a file under a name that is not UTF-8 gets a verdict instead of a panic.
  • verify_bundle no longer reads a payload the strict reader refuses as a payload without a claim. With an sd_jwt_vc whose issuer signature verifies, such a payload skipped the sd-jwt-bundle-binding check, so an eval-claim payload with a duplicate member verified, and with the stricter reader so would one with NaN or an integer outside the reader's accepted range. The binding is now demanded and fails, as the holder-binding check and hf_evals already treat the same refusal. A payload that is not JSON at all stays out of scope for that binding. This additional binding check runs only after the issuer signature has been checked and verified; it does not replace the SD-JWT signature checks.
  • site-data.json links the audit folder of a patch release. scripts/render_site_data.py derived the audit folder as major100 + minor10, so audit_link of 6.3.1 named audit_artifacts/630/, the folder of 6.3.0. It now uses the rule of the receipt tools, the version without its dots, and only for a release version X.Y.Z, with or without a suffix that starts with a letter, so 6.4.1rc1 finds audit_artifacts/641rc1/ where its receipt is filed; any other string is a gap with its reason instead of a guess. A minor of two digits is refused as before, and a patch of two digits is refused now as well instead of naming the folder of its minor, each with the release it would collide with. A folder whose name is a non-ASCII digit no longer stops the generator.
  • Repository tools, outside the installed package. The core-layer gate no longer reads the annotation of a variable in a function body as code. Python never evaluates it (PEP 526), and x: o.getenv("X") = 1 in a function was reported as a read of the environment, at main and at the head of pull request 395 alike. The value of such an assignment stays judged, and so do the annotations that run: at module level, in a class body and on a function's parameters (scripts/core_layer_gate.py, tests/test_core_layer_gate_reads_no_local_annotation.py).
  • Repository tools, outside the installed package. The pre-commit hook (scripts/git-hooks/pre-commit) asks whether every file under scripts/ has a decision in MANIFEST.in before the commit, when a staged path lies under scripts/, deletions included. It runs the same test node CI runs, against the staged snapshot written out with git checkout-index and not against the working tree (tests/test_the_hook_asks_before_the_long_job.py). Without pytest it says so on stderr and lets the commit through; CI stays the gate.
  • Repository tools, outside the installed package. The mutation gate reads the abort banner of pytest only outside captured test output, and a captured block closes only at a separator of the width of the line that opened it, so a failing test that prints a line such as ! ordinary diagnostic ! no longer turns a finished run into NOT MEASURABLE (scripts/mutation_check.py). The text reader of the test-manifest gate reads the names of each import statement from the token stream, so it agrees with the syntax tree on import os, unittest, a trailing comment, an alias and a backslash continuation (scripts/test_manifest_gate.py, tests/test_codex_199_banner_and_import_path.py).
  • Repository tools, outside the installed package. The release-scope title gate reads the title of a follow-up, [<version> <ID>-FOLGE] type(scope): subject. On a branch of its own, which no row of the scope names, such a title is judged against the In row of its parent and needs no row of its own; before, the form did not read the suffix, and the gate was green only because the follow-up's branch stood outside the scope. A parent that is no In row with a branch leaves its follow-up outside the scope, where the parent stands, and the verdict names the parent; the branch of a row carrying the suffix is red, because the landing card counts the line only by its plain identifier and never counts a follow-up. A second bracket with the suffix counts as an identifier like any other, in a follow-up title and in the title of a row's own branch, and a branch or a title that is no text is red with its reason instead of ending the gate in an exception. Without --version, in a clone that shows no release tag, a follow-up chooses the release by the row of its parent, as the parent's own branch would; before, its branch, which no scope file names, chose the newest candidate, and a follow-up of a line of the older candidate was outside the scope with exit 0 (scripts/b7_release_scope_title_gate.py, tests/test_release_scope_title_gate.py, tests/test_release_scope_landing_card.py, tests/test_release_scope_title_gate_folge_waehlt_die_version.py).
  • Every tool that reads a path list from git reads it as git names the paths (scripts/check_version_and_changelog.py, scripts/audit_output_aufloesbar.py, scripts/verify_pre_tag_receipt.py, scripts/neue_zeilen_sind_englisch.py). The mutant guard's fix below was one instance of a class: without -z, git quotes a path that holds a byte outside ASCII, and the quoted string opens no file. Measured in throwaway repositories, each against the state before: the version gate's sweep for undeclared release claims skipped docs/prüfung.md; the digest-resolvability check said NICHT_AUFLOESBAR for a tracked record under such a name and did not count it; the third-party receipt verifier did not see a receipt under such a name; the language gate reported a German line in docs/prüfung.md under the file before it, judged green when no file came before it, and said NOT MEASURABLE for such an untracked file. The lists are read with -z and as bytes now (one name that is not UTF-8 would otherwise empty the version gate's list through its text decoder), and the language gate decodes a diff header with the mutant guard's decoder. A sweep over scripts/ and tools/ holds every git call that lists paths to -z or to a named reason; five calls keep the quoted form, among them the two tree digests, whose listing is hashed and never opened. No tracked path at this change needs quoting, so no verdict on this repository changes. A name that is not UTF-8 decodes to a lone surrogate, and each of the four tools raised on one while writing its report, with exit 1, the exit code of a finding (measured in every output form each tool has). The reports write such a name with backslash escapes now, and the JSON forms write every character outside ASCII as its JSON escape, so the name reads back as it was under any output encoding. When git does not list the tracked files, the version gate's sweep for undeclared current-version claims no longer reads that as a tree without files and prints OK over it: in a tree without git its summary names the sweep as not measured, and in a checkout git cannot list it fails.
  • The mutant guard reads a path git quotes (scripts/mutant_signature_guard.py). git writes a path with a byte outside ASCII, a double quote, a backslash or a control character in a diff header in double quotes with C escapes ("b/src/proofbundle/pr\303\274fung.py"). The guard read that text as the path, it matched no security path, and a staged if False: in such a file was reported clean with exit 0 (measured in a throwaway repository; plain names and names with a space were caught). The header path is decoded now, the self-test plants a mutant under a non-ASCII name, and the tests cover a non-ASCII name, a double quote and a backslash in both modes. No file under src/proofbundle carries such a name today. A newline in a name is decoded too, and the scope pattern now reads it with DOTALL between \A and \Z: before, its . stopped at the decoded newline, and a staged or committed if False: in src/proofbundle/a\nb.py was reported clean with exit 0. The tests plant that name in both modes, and against the guard without this change the three new cases are red. The guard names the header prefixes of the diff it reads, so diff.mnemonicPrefix in a developer's configuration no longer turns b/ into i/ and leaves the staged form reporting clean over a staged if False:; the language gate's working-tree form had the same gap with w/.
  • Within parsed Python code and the operations it recognizes, the core-layer gate checks uses against the bindings its analysis identifies. It treats TYPE_CHECKING as a trusted guard only within that analysis and when the analysis finds it unchanged (K6, tests/test_core_layer_gate_judges_every_binding.py). It recognizes additional forms of the operations it checks (tests/test_core_layer_gate_sees_every_spelling.py), including writes through getattr(module, "__dict__"), object.__getattribute__, module.__setattr__, object.__setattr__, aliases of setattr or vars, a function's __globals__, a frame's f_globals, sys.modules, and dotted names such as typing.TYPE_CHECKING. Names introduced by star imports, re-exports and match captures whose value the analysis can read are resolved in their binding context. So are a name bound under nonlocal, a name an assignment expression binds inside a comprehension, which Python binds in the scope around the comprehension, and the annotations of a function, and getattr(x, "name") and x.__getattribute__("name") with a literal name stand for x.name inside a chain. The value of a binding under global or nonlocal is read where it stands, so a lazy import (global _os, then import os and _os = os in a function) is resolved, and a chain of such reads is resolved once per link. getattr(x, "name", default) stands for x.name or the default, and a default the analysis cannot resolve stands beside the names it can as a possible unresolved result; a reader imported through another module of the package stays a reader, in the findings and in the import closure. Reading a namespace, as in print(vars(x)) or x.__dict__ is None, does not by itself invalidate every guard. An object that the analysis cannot rule out as a module is treated as a possible module, a parameter among them whatever else the function assigns to it. An object built by a class of the same module counts as no module only when the class is plain: bound once, with no decorator, no metaclass or other keyword, no __new__ bound in its body in any form that binds a name, a match capture among them, and no attribute __new__ set in its module, also by a setattr whose name is built from constants or whose arguments the gate cannot assign, and only object or such classes of the module as bases; a name computed from anything else stays out of reach. An exception for the identified write in src/proofbundle/canonical.py is recorded by name in scripts/core_layer_modules.json, with its reason, its count and the source of its line. That permission does not by itself establish the runtime effects of the write or that TYPE_CHECKING is unchanged. The analysis does not follow module attributes selected by computed names, as in getattr(module, name), treats the first parameter of a method as no module, so a method called through its class with a module as that argument is not seen, and does not cover code it does not parse. A passing result is limited to these checks and is not a proof that no other runtime binding or write exists.
  • Repository tools, as a follow-up to pull request 375. The mutant guard reads a header only outside a hunk and splits the diff at its line feeds only, so an added line ++ 1 no longer moves the lines after it out of the scope, and a form feed no longer cuts an added if False: off its +. It asks git for the quoted header form whatever core.quotePath says, keeps an octal escape above \377 as it stands instead of raising, and shows a name with a character that is not printable in git's quoted form, so a newline in a name no longer begins a line of its own in the log (scripts/mutant_signature_guard.py). The language gate reads the hunks of its diff the same way (scripts/neue_zeilen_sind_englisch.py). The JSON forms of three tools write every character outside ASCII as its JSON escape (scripts/audit_output_aufloesbar.py, scripts/neue_zeilen_sind_englisch.py, scripts/verify_pre_tag_receipt.py). The readiness tree digest pins core.quotePath=true as the receipt digest does (scripts/sign_readiness_artifact.py), the candidate matrix reads the paths changed after a bound commit with -z and trims none (scripts/audit_candidate_matrix.py), and the mutation gate and the release renderer decode a name that is not UTF-8 as Python decodes a file name (scripts/mutation_check.py, scripts/render_release.py).
  • Repository tools, as a follow-up to pull request 415. The mutant guard and the language gate ask git for their diff in a fixed form: diff.interHunkContext=0 and diff.suppressBlankEmpty=false on the command line, and --no-color, --no-ext-diff, --no-textconv and --text. With the two settings in a session's configuration, an empty context line between two changes lost the hunk count, and the guard reported clean over a staged mutant, the language gate green over a German line. An external diff program, the text conversion of a diff driver or a -diff attribute, which a tracked .gitattributes can set, made git print no hunk, and both reported clean over a diff they had not read, and color.ui=always did the same to the language gate, which asked without --no-color. A line an open hunk cannot hold now makes the guard exit 2 and the language gate report NOT MEASURABLE, instead of being passed over (scripts/mutant_signature_guard.py, scripts/neue_zeilen_sind_englisch.py). The guard's self-test plants a -diff attribute, and its error message names the git subcommand again. Both read what git writes as bytes, so a binary file under --text and a lone CR no longer end them in a traceback or move a line out of its hunk, and the guard checks every piece of an added line that Python reads as a statement of its own. Every git call of both runs in a built environment that passes the names git needs to find the repository and its index and pins the rest, so GIT_LITERAL_PATHSPECS and its two neighbours no longer make the language gate green and GIT_TRACE=/dev/stdout no longer makes the guard skip its range; replacement refs are switched off. A failed git call and a file on a security path that is not UTF-8 make the guard exit 2. Both answer in git's line space, at line feeds: a lone CR, a form feed, U+2028 and the other characters splitlines() ends a line at no longer move a return True out of the change, a German line after a fence onto the fence, or an allow marker off its line. Markdown is read in CommonMark's lines, so a closing fence or a quotation marker after a lone CR still counts, and of a git line that holds code and prose only the prose is read. A byte order mark no longer hides a mutant, a NUL and a damaged base object make the guard exit 2, a file system monitor no longer hides a change from the working-tree form, and the text report of the language gate escapes a CR or LF in a name or a line.
  • Repository tools, outside the installed package, as a follow-up to pull request 371. A captured block of the mutation gate closes only at a separator with a title, so a line of the run's width that a test prints without one no longer puts the next line back on the top level; the text fallback reads Interrupted and INTERNALERROR outside captured output as well; and the head of a captured block is read in linear time (scripts/mutation_check.py). The token reader of the test-manifest gate reads a module name after NFKC, as the syntax tree does, and counts a source with a NUL byte as importing instead of stopping with SystemError under CPython 3.12. Both readers leave out a test source that does not decode as UTF-8, and the syntax tree also one its parser refuses with ValueError (a NUL byte under 3.10), RecursionError (a long chain from 3.11 on) or MemoryError (a source that overflows the parser's stack, on every version); each of these stopped the gate with a traceback (scripts/test_manifest_gate.py, tests/test_codex_199_banner_and_import_path.py).
  • Repository tools, as a follow-up to pull request 395. The core-layer gate reads a dotted name for a patch helper also when it is built from constants, by a name, a concatenation, an f-string, a join or a name bound in another module of the package. An operator over a namespace reads it only when the other operand runs no code of the module, so W() == vars(typing) with a class of the module that defines __eq__ is a possible change. Path(...).cwd() and Path(...).home() are environment reads. A module the gate cannot read or parse, also outside the core, and a module too deep for the parser are not measurable (exit 2) instead of passed over or a traceback. A chain of aliases is followed to its end, and the reviewed change of the guard in scripts/core_layer_modules.json is bound to the source of its line as well, with the line numbers of its reason corrected. The bindings the analysis does not follow and the finite lists of operations are named in the gate. A literal attribute read is read in each spelling the gate reads an attribute call in: through __call__, as C.__getattribute__(x, "name") for any class C, and through a __getattribute__ bound to a name or imported from a module (F-B). A conditional, a boolean and an assignment expression stand for each value they may give, as the value of a binding and as a receiver (F-C). A receiver read through a call or through such an expression keeps counting as unresolved for the file and environment methods, so getattr(sys, "stdin").read() and getattr(zipfile, "ZipFile").open are file findings, which main does not report, and under from __future__ import annotations the annotations of a function are read in the module scope, where typing.get_type_hints evaluates them (F-D), and the module is read a second time with them in the scope of the function around the def, always, as main reads it, since a caller may hand an evaluator those names in more ways than the gate can tell apart; a finding of either reading counts, once. A name stands for at most 4096 qualified names, so a value that grows around a cycle of attributes no longer runs the gate out of memory, and a name bound only after that limit is not seen, and a reading that does is not measurable (exit 2) with its report. A diamond of plain classes builds no possible module: each class is judged once and its verdict kept, so a shared base reached a second time is not taken for a cycle (scripts/core_layer_gate.py, tests/test_core_layer_gate_sees_every_spelling.py). As a follow-up to pull request 420, the string a name is read as is made once per scope, name and depth, so a counter assigned eight times and then used as an index no longer takes a minute, a class counts as plain only when no __new__ is bound in its body and no attribute __new__ is set in its module, the reviewed change of the guard reads its lines as ast counts them, and a modules file nested too deep is not measurable (tests/test_core_layer_gate_counter_reading_420.py).

Full changelog · Known limitations for 6.4.0 · Earlier risk record · Release scope · proofbundle.dev

Assisted by little helpers.