From 1f6d29d3a678a18b0978c3910fcaf8bec1d8aca2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 08:26:09 +0900 Subject: [PATCH 1/6] fix: hash-lock psycopg binary for Python 3.14 --- CHANGELOG.md | 1 + requirements-quality.txt | 5 +++-- scripts/validate_repository.py | 3 +++ tests/test_repository_contracts.py | 11 +++++++++++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f54fc4ae..a24fc874 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] +- Extended the hash-locked PostgreSQL quality dependency set with the CPython 3.14 `psycopg-binary` wheel required by the central coverage runner; the repository contract now keeps that interpreter boundary executable. - Added the deterministic bank-reconciliation proposal engine from ADR 0054: stable provider, end-to-end, and account-servicer references take precedence; exact decimal amount and currency evidence must agree; exact-money plus bounded-date fallback is permitted only for one unique candidate; and ambiguity, conflicts, out-of-window evidence, or no candidate produce explicit abstention with an operator next action. Match proposals are read-only evidence with no automatic journal posting; any accounting adjustment must re-enter the existing journal command boundary. - Added the exact book-to-bank reconciliation bridge with exact Decimal equations, fail-closed one minor unit differences, statement-population and book-population provenance, no automatic journal posting, and ADR 0054. - Added a read-only reconciliation close-review projection over deterministic decisions and the exact bridge: controllers receive exact bank/book/reconciled/outstanding/unexplained Decimal values, immutable run and population provenance, preceding-run deltas, unresolved statement-entry references, and an explicit next action. JSON and CSV exports preserve money as decimal strings. `suitable_for_period_close_review` is evidence eligibility only; the projection cannot approve reconciliation, close a period, or post a journal. ADR 0054 records the authority boundary. diff --git a/requirements-quality.txt b/requirements-quality.txt index 5d8f8766..9d38b3c5 100644 --- a/requirements-quality.txt +++ b/requirements-quality.txt @@ -1,6 +1,6 @@ # Hash-locked quality and packaging tools for --require-hashes --only-binary=:all:. # coverage 7.15.4: every CPython 3.13 wheel plus py3-none-any. -# psycopg 3.3.4 plus every CPython 3.13 psycopg-binary wheel. +# psycopg 3.3.4 plus every CPython 3.13 and 3.14 psycopg-binary wheel. coverage==7.15.4 \ --hash=sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921 \ --hash=sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e \ @@ -37,4 +37,5 @@ psycopg-binary==3.3.4 \ --hash=sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf \ --hash=sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260 \ --hash=sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf \ - --hash=sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38 + --hash=sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38 \ + --hash=sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277 diff --git a/scripts/validate_repository.py b/scripts/validate_repository.py index 99299c4f..04b6dc08 100644 --- a/scripts/validate_repository.py +++ b/scripts/validate_repository.py @@ -129,6 +129,9 @@ COVERAGE_CP313_MANYLINUX_X86_64_WHEEL_HASH = ( "12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4" ) +PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH = ( + "8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277" +) SCHEMA_NAME_PATTERN = re.compile( r"\bCREATE\s+SCHEMA(?:\s+IF\s+NOT\s+EXISTS)?\s+([A-Za-z_][A-Za-z0-9_]*)", re.IGNORECASE, diff --git a/tests/test_repository_contracts.py b/tests/test_repository_contracts.py index 7709d66a..22505527 100644 --- a/tests/test_repository_contracts.py +++ b/tests/test_repository_contracts.py @@ -17,6 +17,7 @@ APPEND_ONLY_JOURNAL_MUTATION_ERROR, COVERAGE_CP313_MANYLINUX_X86_64_WHEEL_HASH, COVERAGE_UNIVERSAL_WHEEL_HASH, + PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH, main, find_mutable_action_references, find_placeholder_tokens, @@ -369,6 +370,16 @@ def test_quality_requirements_require_ci_coverage_wheels_and_packaging_backend( ("coverage must pin the CPython 3.13 manylinux x86_64 wheel hash",), ) + def test_quality_requirements_include_central_python_314_psycopg_wheel(self) -> None: + """The central coverage image must install the native PostgreSQL wheel.""" + quality_requirements = (ROOT / "requirements-quality.txt").read_text( + encoding="utf-8" + ) + self.assertIn( + f"--hash=sha256:{PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH}", + quality_requirements, + ) + orphan_and_unpinned = validate_quality_requirements( f"--hash=sha256:{COVERAGE_UNIVERSAL_WHEEL_HASH}\n" "not-a-pinned-requirement\n" From 7046d9932f28533f41fb1dc7d52bd28a0d7ae3dc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 09:23:23 +0900 Subject: [PATCH 2/6] fix: validate psycopg Python 3.14 hash --- scripts/validate_repository.py | 6 ++++++ tests/test_repository_contracts.py | 24 +++++++++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/scripts/validate_repository.py b/scripts/validate_repository.py index 04b6dc08..37af9bfd 100644 --- a/scripts/validate_repository.py +++ b/scripts/validate_repository.py @@ -407,6 +407,12 @@ def validate_quality_requirements(requirements_text: str) -> tuple[str, ...]: ) elif not package_hashes["psycopg-binary"]: errors.append("psycopg-binary must be hash locked") + elif PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH not in package_hashes[ + "psycopg-binary" + ]: + errors.append( + "psycopg-binary must pin the CPython 3.14 manylinux x86_64 wheel hash" + ) return tuple(errors) diff --git a/tests/test_repository_contracts.py b/tests/test_repository_contracts.py index 22505527..7be670fa 100644 --- a/tests/test_repository_contracts.py +++ b/tests/test_repository_contracts.py @@ -367,7 +367,10 @@ def test_quality_requirements_require_ci_coverage_wheels_and_packaging_backend( ) self.assertEqual( universal_only, - ("coverage must pin the CPython 3.13 manylinux x86_64 wheel hash",), + ( + "coverage must pin the CPython 3.13 manylinux x86_64 wheel hash", + "psycopg-binary must pin the CPython 3.14 manylinux x86_64 wheel hash", + ), ) def test_quality_requirements_include_central_python_314_psycopg_wheel(self) -> None: @@ -375,9 +378,23 @@ def test_quality_requirements_include_central_python_314_psycopg_wheel(self) -> quality_requirements = (ROOT / "requirements-quality.txt").read_text( encoding="utf-8" ) + psycopg_binary_stanza = re.search( + r"(?ms)^psycopg-binary==3\.3\.4 \\\n" + r"(?: --hash=sha256:[0-9a-f]{64}(?: \\\n|\n))+", + quality_requirements, + ) + self.assertIsNotNone(psycopg_binary_stanza) self.assertIn( f"--hash=sha256:{PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH}", - quality_requirements, + psycopg_binary_stanza.group(0) if psycopg_binary_stanza else "", + ) + without_cp314 = quality_requirements.replace( + f" --hash=sha256:{PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH}\n", + "", + ) + self.assertIn( + "psycopg-binary must pin the CPython 3.14 manylinux x86_64 wheel hash", + validate_quality_requirements(without_cp314), ) orphan_and_unpinned = validate_quality_requirements( @@ -416,7 +433,8 @@ def test_quality_requirements_include_central_python_314_psycopg_wheel(self) -> "psycopg==3.3.4 --hash=sha256:" "b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a\n" "psycopg-binary==3.3.4 --hash=sha256:" - "c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3\n" + "c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3 " + f"--hash=sha256:{PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH}\n" "# comment and blank lines are ignored\n\n" ) self.assertEqual(inline_valid, ()) From 3afa59fed6cb59f43f36f786d7082d90207de580 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:02:56 +0900 Subject: [PATCH 3/6] test: isolate quality requirement contracts --- requirements-quality.txt | 2 +- tests/test_repository_contracts.py | 48 +++++++++++++++--------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/requirements-quality.txt b/requirements-quality.txt index 9d38b3c5..3d35c445 100644 --- a/requirements-quality.txt +++ b/requirements-quality.txt @@ -1,6 +1,6 @@ # Hash-locked quality and packaging tools for --require-hashes --only-binary=:all:. # coverage 7.15.4: every CPython 3.13 wheel plus py3-none-any. -# psycopg 3.3.4 plus every CPython 3.13 and 3.14 psycopg-binary wheel. +# psycopg 3.3.4 plus every CPython 3.13 wheel and the CPython 3.14 manylinux2014 x86_64 wheel. coverage==7.15.4 \ --hash=sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921 \ --hash=sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e \ diff --git a/tests/test_repository_contracts.py b/tests/test_repository_contracts.py index 7be670fa..f715b214 100644 --- a/tests/test_repository_contracts.py +++ b/tests/test_repository_contracts.py @@ -373,30 +373,6 @@ def test_quality_requirements_require_ci_coverage_wheels_and_packaging_backend( ), ) - def test_quality_requirements_include_central_python_314_psycopg_wheel(self) -> None: - """The central coverage image must install the native PostgreSQL wheel.""" - quality_requirements = (ROOT / "requirements-quality.txt").read_text( - encoding="utf-8" - ) - psycopg_binary_stanza = re.search( - r"(?ms)^psycopg-binary==3\.3\.4 \\\n" - r"(?: --hash=sha256:[0-9a-f]{64}(?: \\\n|\n))+", - quality_requirements, - ) - self.assertIsNotNone(psycopg_binary_stanza) - self.assertIn( - f"--hash=sha256:{PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH}", - psycopg_binary_stanza.group(0) if psycopg_binary_stanza else "", - ) - without_cp314 = quality_requirements.replace( - f" --hash=sha256:{PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH}\n", - "", - ) - self.assertIn( - "psycopg-binary must pin the CPython 3.14 manylinux x86_64 wheel hash", - validate_quality_requirements(without_cp314), - ) - orphan_and_unpinned = validate_quality_requirements( f"--hash=sha256:{COVERAGE_UNIVERSAL_WHEEL_HASH}\n" "not-a-pinned-requirement\n" @@ -439,6 +415,30 @@ def test_quality_requirements_include_central_python_314_psycopg_wheel(self) -> ) self.assertEqual(inline_valid, ()) + def test_quality_requirements_include_central_python_314_psycopg_wheel(self) -> None: + """The central coverage image must install the native PostgreSQL wheel.""" + quality_requirements = (ROOT / "requirements-quality.txt").read_text( + encoding="utf-8" + ) + psycopg_binary_stanza = re.search( + r"(?ms)^psycopg-binary==3\.3\.4 \\\n" + r"(?: --hash=sha256:[0-9a-f]{64}(?: \\\n|\n))+", + quality_requirements, + ) + self.assertIsNotNone(psycopg_binary_stanza) + self.assertIn( + f"--hash=sha256:{PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH}", + psycopg_binary_stanza.group(0) if psycopg_binary_stanza else "", + ) + without_cp314 = quality_requirements.replace( + f" --hash=sha256:{PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH}\n", + "", + ) + self.assertIn( + "psycopg-binary must pin the CPython 3.14 manylinux x86_64 wheel hash", + validate_quality_requirements(without_cp314), + ) + def test_append_only_journal_sql_rejects_update_and_delete(self) -> None: """Migrations cannot UPDATE or DELETE posted journal tables.""" forbidden_statements = ( From 1e9f4678bf5d92629eaf423c91652a9101dde0ab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:29:30 +0900 Subject: [PATCH 4/6] fix: bind quality hashes to package versions --- scripts/validate_repository.py | 28 ++++++++++++++++++---------- tests/test_repository_contracts.py | 9 +++++++++ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/scripts/validate_repository.py b/scripts/validate_repository.py index 37af9bfd..889355e9 100644 --- a/scripts/validate_repository.py +++ b/scripts/validate_repository.py @@ -122,7 +122,9 @@ ) TWO_WORD_SNAKE_PATTERN = re.compile(r"^[a-z][a-z0-9]*_[a-z0-9_]+$") HASH_TOKEN_PATTERN = re.compile(r"--hash=sha256:([0-9a-f]{64})") -PINNED_REQUIREMENT_PATTERN = re.compile(r"^([A-Za-z0-9][A-Za-z0-9._-]*)==") +PINNED_REQUIREMENT_PATTERN = re.compile( + r"^([A-Za-z0-9][A-Za-z0-9._-]*)==([^\s\\]+)" +) COVERAGE_UNIVERSAL_WHEEL_HASH = ( "964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84" ) @@ -337,7 +339,8 @@ def validate_quality_requirements(requirements_text: str) -> tuple[str, ...]: """Require hash-locked coverage wheels and the no-build-isolation packaging backend.""" errors: list[str] = [] package_hashes: dict[str, set[str]] = {} - current_package: str | None = None + package_version_hashes: dict[tuple[str, str], set[str]] = {} + current_requirement: tuple[str, str] | None = None for raw_line in requirements_text.splitlines(): line = raw_line.strip() @@ -349,15 +352,20 @@ def validate_quality_requirements(requirements_text: str) -> tuple[str, ...]: name_match = PINNED_REQUIREMENT_PATTERN.match(requirement_line) if name_match is None: errors.append(f"unrecognized quality dependency line: {requirement_line}") - current_package = None + current_requirement = None else: - current_package = name_match.group(1).lower() - package_hashes.setdefault(current_package, set()) + current_requirement = ( + name_match.group(1).lower(), + name_match.group(2), + ) + package_hashes.setdefault(current_requirement[0], set()) + package_version_hashes.setdefault(current_requirement, set()) if hashes: - if current_package is None: + if current_requirement is None: errors.append("hash lock is not attached to a quality dependency") else: - package_hashes.setdefault(current_package, set()).update(hashes) + package_hashes[current_requirement[0]].update(hashes) + package_version_hashes[current_requirement].update(hashes) if not any(package_hashes.values()): errors.append("quality dependencies must be hash locked") @@ -407,9 +415,9 @@ def validate_quality_requirements(requirements_text: str) -> tuple[str, ...]: ) elif not package_hashes["psycopg-binary"]: errors.append("psycopg-binary must be hash locked") - elif PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH not in package_hashes[ - "psycopg-binary" - ]: + elif PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH not in package_version_hashes.get( + ("psycopg-binary", "3.3.4"), set() + ): errors.append( "psycopg-binary must pin the CPython 3.14 manylinux x86_64 wheel hash" ) diff --git a/tests/test_repository_contracts.py b/tests/test_repository_contracts.py index f715b214..b19f3cdf 100644 --- a/tests/test_repository_contracts.py +++ b/tests/test_repository_contracts.py @@ -438,6 +438,15 @@ def test_quality_requirements_include_central_python_314_psycopg_wheel(self) -> "psycopg-binary must pin the CPython 3.14 manylinux x86_64 wheel hash", validate_quality_requirements(without_cp314), ) + wrong_version = ( + without_cp314 + + "\npsycopg-binary==3.3.5 \\\n" + f" --hash=sha256:{PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH}\n" + ) + self.assertIn( + "psycopg-binary must pin the CPython 3.14 manylinux x86_64 wheel hash", + validate_quality_requirements(wrong_version), + ) def test_append_only_journal_sql_rejects_update_and_delete(self) -> None: """Migrations cannot UPDATE or DELETE posted journal tables.""" From 50fca8c763a57fa2fa4977021c88c18977f55c3f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:42:58 +0900 Subject: [PATCH 5/6] fix: reject duplicate quality dependency stanzas --- CHANGELOG.md | 1 + scripts/validate_repository.py | 7 +++++++ tests/test_repository_contracts.py | 8 ++++++++ 3 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a24fc874..c18df6dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] +- Quality dependency validation now rejects duplicate package-version stanzas so a required wheel hash cannot be satisfied by merging separate declarations. - Extended the hash-locked PostgreSQL quality dependency set with the CPython 3.14 `psycopg-binary` wheel required by the central coverage runner; the repository contract now keeps that interpreter boundary executable. - Added the deterministic bank-reconciliation proposal engine from ADR 0054: stable provider, end-to-end, and account-servicer references take precedence; exact decimal amount and currency evidence must agree; exact-money plus bounded-date fallback is permitted only for one unique candidate; and ambiguity, conflicts, out-of-window evidence, or no candidate produce explicit abstention with an operator next action. Match proposals are read-only evidence with no automatic journal posting; any accounting adjustment must re-enter the existing journal command boundary. - Added the exact book-to-bank reconciliation bridge with exact Decimal equations, fail-closed one minor unit differences, statement-population and book-population provenance, no automatic journal posting, and ADR 0054. diff --git a/scripts/validate_repository.py b/scripts/validate_repository.py index 889355e9..56015c96 100644 --- a/scripts/validate_repository.py +++ b/scripts/validate_repository.py @@ -340,6 +340,7 @@ def validate_quality_requirements(requirements_text: str) -> tuple[str, ...]: errors: list[str] = [] package_hashes: dict[str, set[str]] = {} package_version_hashes: dict[tuple[str, str], set[str]] = {} + seen_requirements: set[tuple[str, str]] = set() current_requirement: tuple[str, str] | None = None for raw_line in requirements_text.splitlines(): @@ -358,6 +359,12 @@ def validate_quality_requirements(requirements_text: str) -> tuple[str, ...]: name_match.group(1).lower(), name_match.group(2), ) + if current_requirement in seen_requirements: + errors.append( + "quality dependency stanza appears more than once: " + f"{current_requirement[0]}=={current_requirement[1]}" + ) + seen_requirements.add(current_requirement) package_hashes.setdefault(current_requirement[0], set()) package_version_hashes.setdefault(current_requirement, set()) if hashes: diff --git a/tests/test_repository_contracts.py b/tests/test_repository_contracts.py index b19f3cdf..ac520aac 100644 --- a/tests/test_repository_contracts.py +++ b/tests/test_repository_contracts.py @@ -448,6 +448,14 @@ def test_quality_requirements_include_central_python_314_psycopg_wheel(self) -> validate_quality_requirements(wrong_version), ) + duplicate_version = quality_requirements + ( + "\npsycopg-binary==3.3.4\n" + ) + self.assertIn( + "quality dependency stanza appears more than once: psycopg-binary==3.3.4", + validate_quality_requirements(duplicate_version), + ) + def test_append_only_journal_sql_rejects_update_and_delete(self) -> None: """Migrations cannot UPDATE or DELETE posted journal tables.""" forbidden_statements = ( From a093530868213ea712ffadac62965bdce997b757 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 23:33:47 +0900 Subject: [PATCH 6/6] fix: canonicalize quality dependency names --- CHANGELOG.md | 2 +- docs/adr/0048-reproducible-package-evidence.md | 4 ++-- docs/doctoring/STANDARD_TRACEABILITY.md | 2 +- scripts/validate_repository.py | 2 +- tests/test_repository_contracts.py | 9 +++++++++ 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c18df6dc..62afda18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [Unreleased] -- Quality dependency validation now rejects duplicate package-version stanzas so a required wheel hash cannot be satisfied by merging separate declarations. +- Quality dependency validation now canonicalizes package names using the packaging project identity rules and rejects duplicate package-version stanzas, so equivalent spellings cannot bypass duplicate detection or split a required wheel hash across declarations. - Extended the hash-locked PostgreSQL quality dependency set with the CPython 3.14 `psycopg-binary` wheel required by the central coverage runner; the repository contract now keeps that interpreter boundary executable. - Added the deterministic bank-reconciliation proposal engine from ADR 0054: stable provider, end-to-end, and account-servicer references take precedence; exact decimal amount and currency evidence must agree; exact-money plus bounded-date fallback is permitted only for one unique candidate; and ambiguity, conflicts, out-of-window evidence, or no candidate produce explicit abstention with an operator next action. Match proposals are read-only evidence with no automatic journal posting; any accounting adjustment must re-enter the existing journal command boundary. - Added the exact book-to-bank reconciliation bridge with exact Decimal equations, fail-closed one minor unit differences, statement-population and book-population provenance, no automatic journal posting, and ADR 0054. diff --git a/docs/adr/0048-reproducible-package-evidence.md b/docs/adr/0048-reproducible-package-evidence.md index 2fb976b6..2294f2ae 100644 --- a/docs/adr/0048-reproducible-package-evidence.md +++ b/docs/adr/0048-reproducible-package-evidence.md @@ -22,7 +22,7 @@ The Accounting Foundation CI separates **premerge exact-source evidence** from * 2. The build/test job has `contents: read` only. It does not receive OIDC, attestation, or artifact-metadata write authority while executing repository-controlled tests, validation, package build code, or smoke tests. 3. The job derives `SOURCE_DATE_EPOCH` from that verified commit timestamp and builds the wheel twice from clean build metadata. The two SHA-256 digests must be identical. A non-reproducible wheel fails the job. 4. `scripts/generate_supply_chain_evidence.py` emits deterministic `sbom.spdx.json` and `source-provenance.json`. The source-provenance manifest binds the exact verified source SHA and source timestamp to the wheel file/digest, SPDX SBOM file/digest, repository, and build definition. The generator fails closed if runtime dependencies are introduced before explicit dependency relationships are represented in the SBOM. -5. `SHA256SUMS` covers the wheel, the SPDX SBOM, and `source-provenance.json`. The workflow verifies those checksums, then installs the rebuilt wheel with `--require-hashes` from a requirements line that carries the measured `--hash=sha256:` digest (pip does not accept `--hash` as a CLI option) before the isolated smoke test. All four files are retained together in the same-head Actions artifact. +5. `SHA256SUMS` covers the wheel, the SPDX SBOM, and `source-provenance.json`. The workflow verifies those checksums, then installs the rebuilt wheel with `--require-hashes` from a requirements line that carries the measured `--hash=sha256:` digest (pip does not accept `--hash` as a CLI option) before the isolated smoke test. The repository quality validator canonicalizes package names using the packaging project identity rules before binding versions, duplicate stanzas, and wheel hashes, so equivalent spellings cannot split or bypass a lock. All four files are retained together in the same-head Actions artifact. 6. On `pull_request`, `exact-head-dependency-diff` checks out `pull_request.head.sha`, independently fetches the live base branch tip, records the live base/head identities, dependency-manifest diff and manifest SHA-256 values, and fails if the live base is not an ancestor of the exact head. It scans the complete hash-locked `requirements-quality.txt` with a digest-pinned OSV-Scanner container. A vulnerability finding, scanner failure, unavailable scanner/evidence path, wrong head identity or missing expected evidence is non-passing. Because the bootstrap base has no dependency manifests, the foundation PR records those files as additions and requires the complete exact-head dependency set to be vulnerability-free rather than treating an empty base scan as evidence. The SHA-named artifact retains the identity/diff record and OSV JSON result. 7. On `pull_request`, the deterministic source-provenance manifest is the authoritative artifact-to-exact-PR-head evidence. No job with `id-token: write`, `attestations: write`, or `artifact-metadata: write` executes for that event. 8. On `push` to `develop` or `main`, a distinct `integrated-attestations` job runs only after `accounting-foundation` succeeds. It downloads the immutable SHA-named package-evidence artifact, re-verifies `SHA256SUMS`, verifies `source-provenance.json.source_sha == github.sha`, and only then receives the OIDC/attestation permissions required by the full-SHA-pinned `actions/attest` action to create build-provenance and SPDX-SBOM attestations. Those integrated-head attestations are mandatory release evidence. @@ -52,4 +52,4 @@ Google Open Source Security Team. (2026b). *OSV-Scanner: Supported artifacts and SPDX Workgroup. (n.d.). *SPDX specifications*. https://spdx.dev/use/specifications/ -Supply-chain Levels for Software Artifacts. (2026). *SLSA specification, version 1.2*. https://slsa.dev/spec/v1.2/ \ No newline at end of file +Supply-chain Levels for Software Artifacts. (2026). *SLSA specification, version 1.2*. https://slsa.dev/spec/v1.2/ diff --git a/docs/doctoring/STANDARD_TRACEABILITY.md b/docs/doctoring/STANDARD_TRACEABILITY.md index c35fd19c..6a620329 100644 --- a/docs/doctoring/STANDARD_TRACEABILITY.md +++ b/docs/doctoring/STANDARD_TRACEABILITY.md @@ -14,7 +14,7 @@ | RFC 9112 | The standalone HTTP/1.1 command boundary deliberately does not implement transfer coding: any request carrying `Transfer-Encoding` fails closed with HTTP 400 and connection close rather than being combined with a `Content-Length` interpretation. A valid `Content-Length` is an exact octet contract; premature EOF/short reads are incomplete messages, fail with HTTP 400, and close the connection before JSON/domain processing. This prevents ambiguous message boundaries from becoming request-smuggling or valid-prefix acceptance paths | `JournalProposalHandler._read_body`, HTTP request-boundary RED/GREEN regressions, RFC 9112 §§6.2–6.3 and §8 | | RFC 9562 | New persistence identifiers use UUIDv7 | Initial migration | | CloudEvents 1.0.2 | Commit authoritative events through a transactional outbox and replay by event identity | Outbox table and architecture | -| SLSA 1.2 / SPDX 2.3 / GitHub artifact attestations | Exact-head package evidence builds the wheel twice from a source-derived `SOURCE_DATE_EPOCH`, requires byte-identical SHA-256 digests, emits a deterministic SPDX 2.3 SBOM plus `source-provenance.json`, and makes `SHA256SUMS` cover the wheel, SBOM and source-provenance manifest. After checksum verification, the rebuilt wheel is installed with `--require-hashes` from a requirements line that carries the measured `--hash=sha256:` digest. The intermediate public-API smoke test imports the source tree over `PYTHONPATH` instead of an unhashed editable install. The manifest binds the verified source SHA to the wheel digest and SBOM digest before merge. Pull-request-controlled build/test code runs with `contents: read` only; OIDC, attestation and artifact-metadata write permissions are isolated in a distinct push-only `integrated-attestations` job. That job depends on the successful foundation build, downloads the immutable SHA-named evidence bundle, re-verifies checksums and `source_sha == github.sha`, and only then creates GitHub OIDC-backed signed provenance and SBOM attestations on integrated `develop`/`main` heads. A new runtime dependency fails closed until the SBOM generator represents its dependency relationship. This is evidence readiness, not a claimed SLSA level or certification | Accounting Foundation CI, `scripts/generate_supply_chain_evidence.py`, supply-chain evidence tests, GitHub workflow-permissions/OIDC/artifact-attestation guidance, and ADR 0048 | +| SLSA 1.2 / SPDX 2.3 / GitHub artifact attestations | Exact-head package evidence builds the wheel twice from a source-derived `SOURCE_DATE_EPOCH`, requires byte-identical SHA-256 digests, emits a deterministic SPDX 2.3 SBOM plus `source-provenance.json`, and makes `SHA256SUMS` cover the wheel, SBOM and source-provenance manifest. After checksum verification, the rebuilt wheel is installed with `--require-hashes` from a requirements line that carries the measured `--hash=sha256:` digest. The quality validator canonicalizes package names using packaging project identity rules before binding versions, duplicate stanzas, and wheel hashes. The intermediate public-API smoke test imports the source tree over `PYTHONPATH` instead of an unhashed editable install. The manifest binds the verified source SHA to the wheel digest and SBOM digest before merge. Pull-request-controlled build/test code runs with `contents: read` only; OIDC, attestation and artifact-metadata write permissions are isolated in a distinct push-only `integrated-attestations` job. That job depends on the successful foundation build, downloads the immutable SHA-named evidence bundle, re-verifies checksums and `source_sha == github.sha`, and only then creates GitHub OIDC-backed signed provenance and SBOM attestations on integrated `develop`/`main` heads. A new runtime dependency fails closed until the SBOM generator represents its dependency relationship. This is evidence readiness, not a claimed SLSA level or certification | Accounting Foundation CI, `scripts/generate_supply_chain_evidence.py`, supply-chain evidence tests, GitHub workflow-permissions/OIDC/artifact-attestation guidance, and ADR 0048 | | OSV-Scanner / OSV.dev vulnerability data | Pull-request dependency evidence is tied to the immutable PR head and an independently fetched live base tip. The gate records dependency-manifest diffs and SHA-256 values, rejects stale/non-ancestor base identity, and scans the complete hash-locked exact-head Python dependency set with a digest-pinned OSV-Scanner image. A known vulnerability, scanner failure, skipped/unavailable evidence path or wrong checkout identity is non-passing; aggregate organization workflow success cannot substitute for an unexecuted dependency-review step | `exact-head-dependency-diff` CI job, `tests/test_dependency_review_contract.py`, OSV-Scanner source/lockfile guidance, and ADR 0048 | | AICPA Trust Services Criteria (SOC 2) | Auditors read an append-only history of posted, reversed, and closed facts from existing `outbox_event` rows, including already-published rows, without marking publish. Controllers also list stored `journal_reversal` lineage and durable hard-close receipts over HTTP without SQL. A HomeTax filing command fail-closes and persists a rejected receipt when the VAT register or the purpose-limited HomeTax credential is missing, and this slice never claims `transmitted` | HTTP audit-event history, HTTP journal-reversal list, HTTP period-close list, HTTP fail-closed HomeTax submission, ADR 0027, ADR 0029, ADR 0030, and ADR 0046 | | W3C PROV-O | Preserve entity, activity, agent, derivation, and attribution references across source proposal, posting, and append-only journal reversal lineage. The in-memory posting oracle scopes those identities by tenant and refuses to overwrite a posted `proposal_id`. Checked-in migrations cannot `UPDATE` or `DELETE` `general_journal` or `journal_entry_line` | Source-reference and receipt contracts, HTTP journal-reversal list, repository migration validation, ADR 0003, and ADR 0029 | diff --git a/scripts/validate_repository.py b/scripts/validate_repository.py index 56015c96..9aa84bc3 100644 --- a/scripts/validate_repository.py +++ b/scripts/validate_repository.py @@ -356,7 +356,7 @@ def validate_quality_requirements(requirements_text: str) -> tuple[str, ...]: current_requirement = None else: current_requirement = ( - name_match.group(1).lower(), + re.sub(r"[-_.]+", "-", name_match.group(1)).lower(), name_match.group(2), ) if current_requirement in seen_requirements: diff --git a/tests/test_repository_contracts.py b/tests/test_repository_contracts.py index ac520aac..49e6d24b 100644 --- a/tests/test_repository_contracts.py +++ b/tests/test_repository_contracts.py @@ -456,6 +456,15 @@ def test_quality_requirements_include_central_python_314_psycopg_wheel(self) -> validate_quality_requirements(duplicate_version), ) + for equivalent_name in ("psycopg_binary", "psycopg.binary"): + equivalent_duplicate = quality_requirements + ( + f"\n{equivalent_name}==3.3.4\n" + ) + self.assertIn( + "quality dependency stanza appears more than once: psycopg-binary==3.3.4", + validate_quality_requirements(equivalent_duplicate), + ) + def test_append_only_journal_sql_rejects_update_and_delete(self) -> None: """Migrations cannot UPDATE or DELETE posted journal tables.""" forbidden_statements = (