From c50c324a7f4666d8e59c29e5cff15cd4bc697546 Mon Sep 17 00:00:00 2001 From: Bernd Helm Date: Mon, 14 Sep 2026 12:21:29 +0200 Subject: [PATCH 1/2] ci: repair parity governance maintenance flow --- .github/workflows/parity-governance.yml | 10 +- Tools/PARITY_GOVERNANCE.md | 14 +++ Tools/parity_ledger.py | 10 ++ Tools/parity_ledger_baseline.json | 2 +- Tools/parity_ratchet.py | 47 +++++++- Tools/parity_twin_map.json | 18 ++-- .../test_parity_governance_acceptance.py | 101 +++++++++++++++++- 7 files changed, 181 insertions(+), 21 deletions(-) diff --git a/.github/workflows/parity-governance.yml b/.github/workflows/parity-governance.yml index e20b995ecf..027d9c2221 100644 --- a/.github/workflows/parity-governance.yml +++ b/.github/workflows/parity-governance.yml @@ -29,7 +29,7 @@ on: - 'Tools/issue_ref.py' - 'Tools/parity_*.py' - 'Tools/parity_*.json' - - 'Tools/tests/**' + - 'Tools/tests/test_parity_*.py' - '.github/workflows/parity-governance.yml' push: branches: [main] @@ -37,7 +37,7 @@ on: - 'Tools/issue_ref.py' - 'Tools/parity_*.py' - 'Tools/parity_*.json' - - 'Tools/tests/**' + - 'Tools/tests/test_parity_*.py' - '.github/workflows/parity-governance.yml' # Daily, so authority staleness is found on a schedule instead of by whoever # next edits Tools/. See the header note. @@ -64,11 +64,11 @@ jobs: - name: Run parity-governance tests run: | set -o pipefail - python3 -m unittest discover -s tests -p "test_*.py" -v 2>&1 | tee "$RUNNER_TEMP/out.txt" + python3 -m unittest discover -s tests -p "test_parity_*.py" -v 2>&1 | tee "$RUNNER_TEMP/out.txt" ran=$(grep -oE '^Ran [0-9]+ test' "$RUNNER_TEMP/out.txt" | grep -oE '[0-9]+') echo "collected ${ran:-0} tests" - if [ "${ran:-0}" -lt 103 ]; then - echo "::error::expected at least 103 parity-governance tests, collected ${ran:-0} — discovery is broken, not the suite" + if [ "${ran:-0}" -lt 108 ]; then + echo "::error::expected at least 108 parity-governance tests, collected ${ran:-0} — discovery is broken, not the suite" exit 1 fi working-directory: Tools diff --git a/Tools/PARITY_GOVERNANCE.md b/Tools/PARITY_GOVERNANCE.md index 7d57221c63..a801856874 100644 --- a/Tools/PARITY_GOVERNANCE.md +++ b/Tools/PARITY_GOVERNANCE.md @@ -101,6 +101,20 @@ edits `parity_dispositions.json`. Deleting a paired duplicate is fail-closed derived-snapshot drift that requires this guarded refresh flow by design. +If the scheduled `main` run finds that the exact base already contains stale +metadata, use the explicit repair variant: + +```sh +python3 Tools/parity_ledger.py --refresh-derived --repair-stale-base --base origin/main +``` + +This is not a force switch and does not create a disposition. The ratchet +independently scans the exact base and the current tree, then permits the repair +only when semantic authority, finding identities, counters, and typed +dispositions are unchanged between them and both regenerated snapshots exactly +match that shared state. Any source debt introduced on the repair branch still +fails closed, as does a hand-edited or partial snapshot. + For a new one-sided declaration, choose explicitly: The `add-unpaired-function` diagnostic defines "new" by absence of the diff --git a/Tools/parity_ledger.py b/Tools/parity_ledger.py index ba7f52b113..b9aa0ed5d2 100644 --- a/Tools/parity_ledger.py +++ b/Tools/parity_ledger.py @@ -2620,6 +2620,11 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--bootstrap-map", action="store_true", help="write a fresh inventory map before scanning") parser.add_argument("--write-baseline", action="store_true", help="rewrite the baseline with current findings") parser.add_argument("--refresh-derived", action="store_true", help="refresh existing derived snapshots only if the ratchet accepts the result") + parser.add_argument( + "--repair-stale-base", + action="store_true", + help="with --refresh-derived, repair metadata drift already present in the exact base", + ) parser.add_argument("--base", default="origin/main", help="exact git ref used to prove debt reductions") args = parser.parse_args(argv) @@ -2629,6 +2634,9 @@ def main(argv: list[str] | None = None) -> int: if args.bootstrap_map != args.write_baseline: print("FAIL --bootstrap-map and --write-baseline must be used together") return 2 + if args.repair_stale_base and not args.refresh_derived: + print("FAIL --repair-stale-base requires --refresh-derived") + return 2 if args.refresh_derived: if args.bootstrap_map or args.write_baseline or args.no_baseline: print("FAIL --refresh-derived cannot be combined with bootstrap, baseline, or display modes") @@ -2654,6 +2662,8 @@ def main(argv: list[str] | None = None) -> int: sys.executable, str(Path(__file__).with_name("parity_ratchet.py")), "--root", str(root), "--base", args.base, "--offline", ] + if args.repair_stale_base: + command.append("--repair-stale-base") completed = subprocess.run(command, cwd=root, text=True, capture_output=True) if completed.returncode: print("FAIL derived refresh rejected; snapshots restored") diff --git a/Tools/parity_ledger_baseline.json b/Tools/parity_ledger_baseline.json index 6adaed9e89..793b1c820a 100644 --- a/Tools/parity_ledger_baseline.json +++ b/Tools/parity_ledger_baseline.json @@ -18,7 +18,7 @@ {"rule": "test-only-callsite", "scope": "Packages/StrandAnalytics", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope Packages/StrandAnalytics; no wildcard matching.", "count": 44, "identities_sha256": "fff777acbc66e7dad92bd0a6c8b7644e5c7ab6ddc0a7106dfecc49319da6127b"}, {"rule": "test-only-callsite", "scope": "Packages/StrandImport", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope Packages/StrandImport; no wildcard matching.", "count": 13, "identities_sha256": "2ee99bb3c9fe8ab244063600cd863f41344b26780bcb73b814c13cd26aea9413"}, {"rule": "test-only-callsite", "scope": "Packages/WhoopProtocol", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope Packages/WhoopProtocol; no wildcard matching.", "count": 30, "identities_sha256": "a9dc115534e8710217db9cd39bcceaa1419741737bfd3b733007dc0215cd5af2"}, - {"rule": "test-only-callsite", "scope": "Packages/WhoopStore", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope Packages/WhoopStore; no wildcard matching.", "count": 37, "identities_sha256": "2a62c1ece1c541b662dd407f0668d9a02782bb4d21f9bb181518272ff12f0e40"}, + {"rule": "test-only-callsite", "scope": "Packages/WhoopStore", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope Packages/WhoopStore; no wildcard matching.", "count": 51, "identities_sha256": "82cb0bf1f7db514ca69dc7a1394b0de01d7ae4d7d1f948754da52c5b29e01277"}, {"rule": "test-only-callsite", "scope": "android/analytics", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope android/analytics; no wildcard matching.", "count": 66, "identities_sha256": "edae212c4a4da22b0f11432e83d6a9b8abc21d0649d16026b439686681add718"}, {"rule": "test-only-callsite", "scope": "android/data", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope android/data; no wildcard matching.", "count": 14, "identities_sha256": "9bab871f5ddbc1d2634805bf525888125c3880d160ab09b3ee7f3d7af6c487cd"}, {"rule": "test-only-callsite", "scope": "android/ingest", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope android/ingest; no wildcard matching.", "count": 12, "identities_sha256": "d4f45d66087768eab133b918af6658b06ec1d91305e38fb045f6ae1629c94678"}, diff --git a/Tools/parity_ratchet.py b/Tools/parity_ratchet.py index 0cb1157301..5129c1a5ef 100644 --- a/Tools/parity_ratchet.py +++ b/Tools/parity_ratchet.py @@ -403,6 +403,7 @@ def compare_metadata( base: str, *, offline: bool, + repair_stale_base: bool = False, warnings: list[str] | None = None, ) -> list[str]: """Compare current governance metadata with the exact requested base.""" @@ -440,12 +441,39 @@ def compare_metadata( with _base_tree(root, base) as base_root: base_sets = parity_ledger.semantic_authority(base_root) base_manifest = parity_ledger.authority_manifest(base_sets) - if old_map["authority"] != base_manifest: + base_scan_map = parity_ledger.build_compact_twin_map(base_root) + base_scan_map["exemptions"] = old_registry["dispositions"] + base_scan = parity_ledger.scan(base_root, base_scan_map) + base_authority_is_stale = old_map["authority"] != base_manifest + if base_authority_is_stale: + repair_mismatches: list[str] = [] + if current_sets != base_sets: + repair_mismatches.append("semantic authority differs from the exact base") + if ({item.identity for item in current_scan.findings} + != {item.identity for item in base_scan.findings}): + repair_mismatches.append("finding identities differ from the exact base") + if current_scan.counters != base_scan.counters: + repair_mismatches.append("counters differ from the exact base") + if current_registry != old_registry: + repair_mismatches.append("typed dispositions differ from the exact base") + if current_map["authority"] != current_manifest: + repair_mismatches.append("current authority is not exactly derived") + if current_baseline != parity_ledger.build_compact_baseline(current_scan): + repair_mismatches.append("current baseline is not exactly derived") + if not repair_stale_base: errors.append( f"{TWIN_MAP_PATH}: base authority cannot be reproduced with the current derivation; migration required" ) - base_scan_map = parity_ledger.build_compact_twin_map(base_root) - base_scan = parity_ledger.scan(base_root, base_scan_map) + elif repair_mismatches: + errors.append( + f"{TWIN_MAP_PATH}: stale-base repair rejected because " + + "; ".join(repair_mismatches) + ) + else: + warnings.append( + f"{TWIN_MAP_PATH}: repaired stale metadata already present in the exact base; " + "no current-tree governance delta accepted" + ) base_findings = {item.identity for item in base_scan.findings} current_findings = {item.identity for item in current_scan.findings} required = _required_v3_exemptions( @@ -616,13 +644,24 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--root", type=Path, default=ROOT) parser.add_argument("--base", help="base ref; defaults durably to origin/main") parser.add_argument("--offline", action="store_true", help="skip GitHub issue existence checks") + parser.add_argument( + "--repair-stale-base", + action="store_true", + help="adopt exactly derived metadata only when governed state is unchanged from an already-stale base", + ) args = parser.parse_args(argv) root = args.root.resolve() try: base = resolve_base(root, args.base) warnings: list[str] = [] errors = repository_consistency_errors(root, warnings=warnings) - errors.extend(compare_metadata(root, base, offline=args.offline, warnings=warnings)) + errors.extend(compare_metadata( + root, + base, + offline=args.offline, + repair_stale_base=args.repair_stale_base, + warnings=warnings, + )) for warning in warnings: print(f"WARNING: {warning}", file=sys.stderr) return _print_errors(errors) diff --git a/Tools/parity_twin_map.json b/Tools/parity_twin_map.json index 5e2766e7f9..07770c2da2 100644 --- a/Tools/parity_twin_map.json +++ b/Tools/parity_twin_map.json @@ -18,17 +18,17 @@ ] }, "authority": { - "files": {"count": 487, "sha256": "a90bd1677a35dae57901a6e193e4ec9b5648a004d6bcce796cb56dccc2648e66"}, - "functions": {"count": 4315, "sha256": "d2bf60097e1a84455b3ab07fcc707973829d3be465adf26686188f8e464ab2cf"}, - "properties": {"count": 448, "sha256": "e90524140aa597cab434483743950f0b78110140d1e2b1dd9ed7d72e8d25fddf"}, - "constants": {"count": 1890, "sha256": "d918e6c0739ae1d2df2f74bfdbd36f6e7f26c434ea4dc09a1afe4eb75c8f8d11"}, + "files": {"count": 490, "sha256": "554e510e220e3bf87187abc422178216f31c855311cb093434bda155e9283f96"}, + "functions": {"count": 4353, "sha256": "dc31ce701cf0ee2ec1992a21da290712ee63cfa0268078051fd71878dd2671bf"}, + "properties": {"count": 451, "sha256": "cb176d7203c8c42522f55d0db9acdcde97ed7cad5e7225cc6a41b18206495c8d"}, + "constants": {"count": 1893, "sha256": "9687e3e36f209bf54e0f78e8e0d0cbf4e21d148c47ed1e4cac3b0645c60b53ff"}, "file_pairs": {"count": 61, "sha256": "ed77a9b1fadece7a30b27b37cb1826a2e95ac1b5e81419fa5364ae6dffc22bf3"}, - "function_pairs": {"count": 152, "sha256": "a8723d1c2bd163b6c128350fb8c169a7325b3a5e3dff877362f8a2094d70f3f4"}, + "function_pairs": {"count": 153, "sha256": "a4ed8f320c9da4e8838b605095f5f82d58abd98fae3ece0e30ce4db4898d5dc6"}, "property_pairs": {"count": 147, "sha256": "781073f4b7af31db4400da247eb16910c2c8adbbb4ab5f9169b852566ed3dbc8"}, "constant_pairs": {"count": 665, "sha256": "080c97b062e80cb7d1f526bfb50b367cbbf6b0cadf43e2c5ec3f2ad046fe09cb"}, - "unpaired_files": {"count": 383, "sha256": "af7ea6779645ef49bedf7c8db1b67b95e493daa96215fd0bcfb86c7db78618f4"}, - "unpaired_functions": {"count": 4017, "sha256": "f27d4ce4d5033c98ee900c0eb60399507354320b0edd2a35fdfba96f458db5f4"}, - "unpaired_properties": {"count": 154, "sha256": "e38ff018589491eedd40569390492f97f5d9dd2a12acd8a2880a34d1d58c73e8"}, - "unpaired_constants": {"count": 560, "sha256": "7edadfead59eb823725ed319636e573e73f623e4e4ce39ac85144d2b3531f787"} + "unpaired_files": {"count": 386, "sha256": "a2edbca2181629394366a14ad4562bbaa82b2b9db0d729e43f3a8f1836f255cd"}, + "unpaired_functions": {"count": 4053, "sha256": "ee636450c185d53eb6e6754d41890a07f8d24412c07edeec859492f9cafe4c39"}, + "unpaired_properties": {"count": 157, "sha256": "bd4cab9d2ebec6da6ed6ff95599b0ab8cd3b4ca7a0c2c3e82493280940d8fda9"}, + "unpaired_constants": {"count": 563, "sha256": "0ada9824528f78eba95c65c56ce1b744d9d5bb876ebb2ca75f6d0b02874e89e8"} } } diff --git a/Tools/tests/test_parity_governance_acceptance.py b/Tools/tests/test_parity_governance_acceptance.py index e79929f1a2..5173171984 100644 --- a/Tools/tests/test_parity_governance_acceptance.py +++ b/Tools/tests/test_parity_governance_acceptance.py @@ -8,6 +8,7 @@ import sys import tempfile import unittest +from fnmatch import fnmatchcase from pathlib import Path from unittest import mock @@ -129,7 +130,7 @@ def test_core_tools_filter_covers_every_governance_tool_path(self) -> None: "Tools/issue_ref.py", "Tools/parity_*.py", "Tools/parity_*.json", - "Tools/tests/**", + "Tools/tests/test_parity_*.py", ".github/workflows/parity-governance.yml", ] * 2, governance_paths, @@ -144,7 +145,18 @@ def test_core_tools_filter_covers_every_governance_tool_path(self) -> None: ) self.assertNotIn("'Packages/**/*.swift'", governance) self.assertNotIn("'android/**/*.kt'", governance) - self.assertIn("unittest discover -s tests", governance) + self.assertNotIn("Tools/tests/**", governance) + self.assertNotIn("test_german_today_localization", governance) + self.assertIn('unittest discover -s tests -p "test_parity_*.py"', governance) + pull_request_paths = governance_paths[:5] + self.assertFalse(any( + fnmatchcase("Tools/tests/test_german_today_localization.py", pattern) + for pattern in pull_request_paths + )) + self.assertTrue(any( + fnmatchcase("Tools/tests/test_parity_ledger.py", pattern) + for pattern in pull_request_paths + )) def test_checked_in_inventory_and_baseline_match_current_sources(self) -> None: self.assertEqual([], self.result.errors) @@ -433,6 +445,91 @@ def test_refresh_restores_snapshots_when_new_debt_is_undisposed(self) -> None: self.assertEqual(before_baseline, (self.root / "Tools/parity_ledger_baseline.json").read_bytes()) self.assertEqual(before_dispositions, (self.root / "Tools/parity_dispositions.json").read_bytes()) + def test_stale_base_repair_accepts_only_governance_state_already_on_base(self) -> None: + swift = self.root / "Packages/StrandAnalytics/Sources/StrandAnalytics/Engine.swift" + kotlin = self.root / "android/app/src/main/java/com/noop/analytics/Engine.kt" + test = self.root / "Packages/StrandAnalytics/Tests/StrandAnalyticsTests/EngineTests.swift" + swift.parent.mkdir(parents=True, exist_ok=True) + kotlin.parent.mkdir(parents=True, exist_ok=True) + test.parent.mkdir(parents=True, exist_ok=True) + swift.write_text("enum Engine {}\n", encoding="utf-8") + kotlin.write_text("object Engine {}\n", encoding="utf-8") + compact = parity_ledger.build_compact_twin_map(self.root) + self.write("Tools/parity_twin_map.json", compact) + self.write( + "Tools/parity_ledger_baseline.json", + parity_ledger.build_compact_baseline(parity_ledger.scan(self.root, compact)), + ) + self.write("Tools/parity_dispositions.json", {"schema_version": 1, "dispositions": []}) + self.commit() + + swift.write_text("enum Engine { static func alreadyOnMain() {} }\n", encoding="utf-8") + test.write_text("func testOnly() { Engine.alreadyOnMain() }\n", encoding="utf-8") + base = self.commit() + refreshed = parity_ledger.build_compact_twin_map(self.root) + self.write("Tools/parity_twin_map.json", refreshed) + self.write( + "Tools/parity_ledger_baseline.json", + parity_ledger.build_compact_baseline(parity_ledger.scan(self.root, refreshed)), + ) + + ordinary = parity_ratchet.compare_metadata(self.root, base, offline=True) + repaired = parity_ratchet.compare_metadata( + self.root, base, offline=True, repair_stale_base=True + ) + + self.assertTrue(any("migration required" in error for error in ordinary), ordinary) + self.assertEqual([], repaired) + self.assertEqual( + {"schema_version": 1, "dispositions": []}, + parity_ledger._load_json(self.root / "Tools/parity_dispositions.json", {}), + ) + + def test_stale_base_repair_rejects_branch_added_governance_debt(self) -> None: + swift = self.root / "Packages/StrandAnalytics/Sources/StrandAnalytics/Engine.swift" + kotlin = self.root / "android/app/src/main/java/com/noop/analytics/Engine.kt" + swift.parent.mkdir(parents=True, exist_ok=True) + kotlin.parent.mkdir(parents=True, exist_ok=True) + swift.write_text("enum Engine {}\n", encoding="utf-8") + kotlin.write_text("object Engine {}\n", encoding="utf-8") + compact = parity_ledger.build_compact_twin_map(self.root) + self.write("Tools/parity_twin_map.json", compact) + self.write( + "Tools/parity_ledger_baseline.json", + parity_ledger.build_compact_baseline(parity_ledger.scan(self.root, compact)), + ) + self.write("Tools/parity_dispositions.json", {"schema_version": 1, "dispositions": []}) + self.commit() + + swift.write_text("enum Engine { static func alreadyOnMain() {} }\n", encoding="utf-8") + base = self.commit() + swift.write_text( + "enum Engine { static func alreadyOnMain() {}; static func addedOnBranch() {} }\n", + encoding="utf-8", + ) + refreshed = parity_ledger.build_compact_twin_map(self.root) + self.write("Tools/parity_twin_map.json", refreshed) + self.write( + "Tools/parity_ledger_baseline.json", + parity_ledger.build_compact_baseline(parity_ledger.scan(self.root, refreshed)), + ) + + errors = parity_ratchet.compare_metadata( + self.root, base, offline=True, repair_stale_base=True + ) + + self.assertTrue(any("stale-base repair rejected" in error for error in errors), errors) + self.assertTrue(any("addedOnBranch" in error for error in errors), errors) + + def test_repair_stale_base_flag_requires_guarded_refresh(self) -> None: + output = io.StringIO() + with mock.patch("sys.stdout", output): + code = parity_ledger.main([ + "--root", str(self.root), "--repair-stale-base", + ]) + self.assertEqual(2, code) + self.assertIn("requires --refresh-derived", output.getvalue()) + def test_expired_experimental_disposition_blocks(self) -> None: marker = self.root / "README" marker.write_text("base\n", encoding="utf-8") From 3f9cdc73455ecc4c8af44f31522a3818dd9a987d Mon Sep 17 00:00:00 2001 From: Bernd Helm Date: Mon, 14 Sep 2026 12:44:02 +0200 Subject: [PATCH 2/2] test: close parity maintenance review gaps --- .github/workflows/parity-governance.yml | 21 ++- .../test_parity_governance_acceptance.py | 147 ++++++++++++------ 2 files changed, 117 insertions(+), 51 deletions(-) diff --git a/.github/workflows/parity-governance.yml b/.github/workflows/parity-governance.yml index 027d9c2221..c266946f24 100644 --- a/.github/workflows/parity-governance.yml +++ b/.github/workflows/parity-governance.yml @@ -1,8 +1,11 @@ -# These are the scanner's own unit and repository-acceptance tests. +# These are the scanner's own unit and repository-acceptance tests, plus the +# explicitly named legacy R-R preservation contract. Keep that companion test +# exact rather than widening back to every Tools/tests file. # # On a PULL REQUEST, run the multi-minute suite only when its implementation, -# authority, tests or workflow change; ordinary product-source changes belong to -# the later ledger/ratchet gate, not to repeated self-testing of the scanner. +# authority, named protection tests or workflow change; ordinary product-source +# changes belong to the later ledger/ratchet gate, not to repeated self-testing +# of the scanner. # # It also runs DAILY on main. One of these tests is not about the scanner at # all: the repository acceptance asserts that the checked-in authority still @@ -30,6 +33,7 @@ on: - 'Tools/parity_*.py' - 'Tools/parity_*.json' - 'Tools/tests/test_parity_*.py' + - 'Tools/tests/test_rr_legacy_preservation_contract.py' - '.github/workflows/parity-governance.yml' push: branches: [main] @@ -38,6 +42,7 @@ on: - 'Tools/parity_*.py' - 'Tools/parity_*.json' - 'Tools/tests/test_parity_*.py' + - 'Tools/tests/test_rr_legacy_preservation_contract.py' - '.github/workflows/parity-governance.yml' # Daily, so authority staleness is found on a schedule instead of by whoever # next edits Tools/. See the header note. @@ -64,11 +69,15 @@ jobs: - name: Run parity-governance tests run: | set -o pipefail - python3 -m unittest discover -s tests -p "test_parity_*.py" -v 2>&1 | tee "$RUNNER_TEMP/out.txt" + python3 -m unittest -v \ + tests.test_parity_ledger \ + tests.test_parity_governance_acceptance \ + tests.test_rr_legacy_preservation_contract \ + 2>&1 | tee "$RUNNER_TEMP/out.txt" ran=$(grep -oE '^Ran [0-9]+ test' "$RUNNER_TEMP/out.txt" | grep -oE '[0-9]+') echo "collected ${ran:-0} tests" - if [ "${ran:-0}" -lt 108 ]; then - echo "::error::expected at least 108 parity-governance tests, collected ${ran:-0} — discovery is broken, not the suite" + if [ "${ran:-0}" -lt 113 ]; then + echo "::error::expected at least 113 parity-governance tests, collected ${ran:-0} — discovery is broken, not the suite" exit 1 fi working-directory: Tools diff --git a/Tools/tests/test_parity_governance_acceptance.py b/Tools/tests/test_parity_governance_acceptance.py index 5173171984..aa87af338c 100644 --- a/Tools/tests/test_parity_governance_acceptance.py +++ b/Tools/tests/test_parity_governance_acceptance.py @@ -131,6 +131,7 @@ def test_core_tools_filter_covers_every_governance_tool_path(self) -> None: "Tools/parity_*.py", "Tools/parity_*.json", "Tools/tests/test_parity_*.py", + "Tools/tests/test_rr_legacy_preservation_contract.py", ".github/workflows/parity-governance.yml", ] * 2, governance_paths, @@ -147,8 +148,10 @@ def test_core_tools_filter_covers_every_governance_tool_path(self) -> None: self.assertNotIn("'android/**/*.kt'", governance) self.assertNotIn("Tools/tests/**", governance) self.assertNotIn("test_german_today_localization", governance) - self.assertIn('unittest discover -s tests -p "test_parity_*.py"', governance) - pull_request_paths = governance_paths[:5] + self.assertIn("tests.test_parity_ledger", governance) + self.assertIn("tests.test_parity_governance_acceptance", governance) + self.assertIn("tests.test_rr_legacy_preservation_contract", governance) + pull_request_paths = governance_paths[:6] self.assertFalse(any( fnmatchcase("Tools/tests/test_german_today_localization.py", pattern) for pattern in pull_request_paths @@ -157,6 +160,10 @@ def test_core_tools_filter_covers_every_governance_tool_path(self) -> None: fnmatchcase("Tools/tests/test_parity_ledger.py", pattern) for pattern in pull_request_paths )) + self.assertTrue(any( + fnmatchcase("Tools/tests/test_rr_legacy_preservation_contract.py", pattern) + for pattern in pull_request_paths + )) def test_checked_in_inventory_and_baseline_match_current_sources(self) -> None: self.assertEqual([], self.result.errors) @@ -318,6 +325,35 @@ def experimental_registry(self, identity: str, issue: str = "bhelm/noop#78") -> "expires_on": "2026-12-31", }]} + def stale_base_repair_fixture(self) -> tuple[Path, str]: + swift = self.root / "Packages/StrandAnalytics/Sources/StrandAnalytics/Engine.swift" + kotlin = self.root / "android/app/src/main/java/com/noop/analytics/Engine.kt" + test = self.root / "Packages/StrandAnalytics/Tests/StrandAnalyticsTests/EngineTests.swift" + swift.parent.mkdir(parents=True, exist_ok=True) + kotlin.parent.mkdir(parents=True, exist_ok=True) + test.parent.mkdir(parents=True, exist_ok=True) + swift.write_text("enum Engine {}\n", encoding="utf-8") + kotlin.write_text("object Engine {}\n", encoding="utf-8") + compact = parity_ledger.build_compact_twin_map(self.root) + self.write("Tools/parity_twin_map.json", compact) + self.write( + "Tools/parity_ledger_baseline.json", + parity_ledger.build_compact_baseline(parity_ledger.scan(self.root, compact)), + ) + self.write("Tools/parity_dispositions.json", {"schema_version": 1, "dispositions": []}) + self.commit() + + swift.write_text("enum Engine { static func alreadyOnMain() {} }\n", encoding="utf-8") + test.write_text("func testOnly() { Engine.alreadyOnMain() }\n", encoding="utf-8") + base = self.commit() + refreshed = parity_ledger.build_compact_twin_map(self.root) + self.write("Tools/parity_twin_map.json", refreshed) + self.write( + "Tools/parity_ledger_baseline.json", + parity_ledger.build_compact_baseline(parity_ledger.scan(self.root, refreshed)), + ) + return swift, base + def test_typed_dispositions_require_explicit_platform_and_lifecycle_fields(self) -> None: experimental = { "schema_version": 1, @@ -446,32 +482,7 @@ def test_refresh_restores_snapshots_when_new_debt_is_undisposed(self) -> None: self.assertEqual(before_dispositions, (self.root / "Tools/parity_dispositions.json").read_bytes()) def test_stale_base_repair_accepts_only_governance_state_already_on_base(self) -> None: - swift = self.root / "Packages/StrandAnalytics/Sources/StrandAnalytics/Engine.swift" - kotlin = self.root / "android/app/src/main/java/com/noop/analytics/Engine.kt" - test = self.root / "Packages/StrandAnalytics/Tests/StrandAnalyticsTests/EngineTests.swift" - swift.parent.mkdir(parents=True, exist_ok=True) - kotlin.parent.mkdir(parents=True, exist_ok=True) - test.parent.mkdir(parents=True, exist_ok=True) - swift.write_text("enum Engine {}\n", encoding="utf-8") - kotlin.write_text("object Engine {}\n", encoding="utf-8") - compact = parity_ledger.build_compact_twin_map(self.root) - self.write("Tools/parity_twin_map.json", compact) - self.write( - "Tools/parity_ledger_baseline.json", - parity_ledger.build_compact_baseline(parity_ledger.scan(self.root, compact)), - ) - self.write("Tools/parity_dispositions.json", {"schema_version": 1, "dispositions": []}) - self.commit() - - swift.write_text("enum Engine { static func alreadyOnMain() {} }\n", encoding="utf-8") - test.write_text("func testOnly() { Engine.alreadyOnMain() }\n", encoding="utf-8") - base = self.commit() - refreshed = parity_ledger.build_compact_twin_map(self.root) - self.write("Tools/parity_twin_map.json", refreshed) - self.write( - "Tools/parity_ledger_baseline.json", - parity_ledger.build_compact_baseline(parity_ledger.scan(self.root, refreshed)), - ) + _swift, base = self.stale_base_repair_fixture() ordinary = parity_ratchet.compare_metadata(self.root, base, offline=True) repaired = parity_ratchet.compare_metadata( @@ -486,23 +497,7 @@ def test_stale_base_repair_accepts_only_governance_state_already_on_base(self) - ) def test_stale_base_repair_rejects_branch_added_governance_debt(self) -> None: - swift = self.root / "Packages/StrandAnalytics/Sources/StrandAnalytics/Engine.swift" - kotlin = self.root / "android/app/src/main/java/com/noop/analytics/Engine.kt" - swift.parent.mkdir(parents=True, exist_ok=True) - kotlin.parent.mkdir(parents=True, exist_ok=True) - swift.write_text("enum Engine {}\n", encoding="utf-8") - kotlin.write_text("object Engine {}\n", encoding="utf-8") - compact = parity_ledger.build_compact_twin_map(self.root) - self.write("Tools/parity_twin_map.json", compact) - self.write( - "Tools/parity_ledger_baseline.json", - parity_ledger.build_compact_baseline(parity_ledger.scan(self.root, compact)), - ) - self.write("Tools/parity_dispositions.json", {"schema_version": 1, "dispositions": []}) - self.commit() - - swift.write_text("enum Engine { static func alreadyOnMain() {} }\n", encoding="utf-8") - base = self.commit() + swift, base = self.stale_base_repair_fixture() swift.write_text( "enum Engine { static func alreadyOnMain() {}; static func addedOnBranch() {} }\n", encoding="utf-8", @@ -521,6 +516,68 @@ def test_stale_base_repair_rejects_branch_added_governance_debt(self) -> None: self.assertTrue(any("stale-base repair rejected" in error for error in errors), errors) self.assertTrue(any("addedOnBranch" in error for error in errors), errors) + def test_stale_base_repair_rejects_disposition_changes(self) -> None: + _swift, base = self.stale_base_repair_fixture() + identity = next( + item for item in parity_ledger.semantic_authority(self.root)["unpaired_functions"] + if "alreadyOnMain" in item + ) + self.write("Tools/parity_dispositions.json", { + "schema_version": 1, + "dispositions": [{ + "type": "platform_specific", + "kind": "add-unpaired-function", + "identity": identity, + "platform": "swift", + "identity_sha256": parity_ledger._canonical_sha256(identity), + "rationale": "Synthetic platform-only decision must not ride a stale-base repair.", + }], + }) + + errors = parity_ratchet.compare_metadata( + self.root, base, offline=True, repair_stale_base=True + ) + + self.assertTrue(any( + "stale-base repair rejected" in error + and "typed dispositions differ from the exact base" in error + for error in errors + ), errors) + + def test_stale_base_repair_rejects_nonexact_map(self) -> None: + _swift, base = self.stale_base_repair_fixture() + compact = parity_ledger._load_json(self.root / "Tools/parity_twin_map.json", {}) + compact["authority"]["functions"]["count"] += 1 + self.write("Tools/parity_twin_map.json", compact) + + errors = parity_ratchet.compare_metadata( + self.root, base, offline=True, repair_stale_base=True + ) + + self.assertTrue(any( + "stale-base repair rejected" in error + and "current authority is not exactly derived" in error + for error in errors + ), errors) + + def test_stale_base_repair_rejects_nonexact_baseline(self) -> None: + _swift, base = self.stale_base_repair_fixture() + baseline = parity_ledger._load_json( + self.root / "Tools/parity_ledger_baseline.json", {} + ) + baseline["accepted_findings"][0]["count"] += 1 + self.write("Tools/parity_ledger_baseline.json", baseline) + + errors = parity_ratchet.compare_metadata( + self.root, base, offline=True, repair_stale_base=True + ) + + self.assertTrue(any( + "stale-base repair rejected" in error + and "current baseline is not exactly derived" in error + for error in errors + ), errors) + def test_repair_stale_base_flag_requires_guarded_refresh(self) -> None: output = io.StringIO() with mock.patch("sys.stdout", output):