diff --git a/tests/fixtures/dependency_source_bindings.json b/tests/fixtures/dependency_source_bindings.json new file mode 100644 index 000000000..1d9c1beb4 --- /dev/null +++ b/tests/fixtures/dependency_source_bindings.json @@ -0,0 +1,260 @@ +{ + "directory_sources": { + "eng.hard-gate": { + "files": { + "frontiers/downstream_gate_20260925/D0_CI_UNBLOCK.md": "397ba511a4cd4e1bfb97b64017d711847a468cc0", + "frontiers/downstream_gate_20260925/GRAPH.json": "220b56867f24c5ef6f55a3cce70490e9777cf3af", + "frontiers/downstream_gate_20260925/README.md": "6ff49b966ab41dc4db771d48e547cc60da11764f", + "frontiers/downstream_gate_20260925/RESULTS.json": "657594914d07f34b03513ffea42bd071fa1ba59f", + "frontiers/downstream_gate_20260925/SCOPE.md": "ed8c56a88c483cdda3138a99e217ab3518c6f5d3", + "frontiers/downstream_gate_20260925/SELECTOR_REGION.json": "ebffdb0969c7d3484f8fd1f4352ea290346f0318", + "frontiers/downstream_gate_20260925/SOURCE_FILES.json": "1fa1e59e4ced1c3aa45f0e1ca2fb25df68267247", + "frontiers/downstream_gate_20260925/git_transition_audit.py": "bdc43cd508a7c9a0a49fe4a06754025502c17dfd", + "frontiers/downstream_gate_20260925/hard_gate.py": "3a334f11e4bf1792cf843474aa6c3013c51893a2", + "frontiers/downstream_gate_20260925/patches/d0_packet_allowlist.patch": "f09b248b10d6a8f229bd21bb3a53a97ca16525f2", + "frontiers/downstream_gate_20260925/run_validation.py": "58c2b5e855272d11df627d472fd09274ff14fcc6", + "frontiers/downstream_gate_20260925/test_hard_gate.py": "0c8263e960ad18fe095c52de4a7c8c9e2094bd59", + "frontiers/downstream_gate_20260925/test_transition_contract.py": "fca3ee2584c4abead767a35221cf3deb428e4de4" + }, + "source": "frontiers/downstream_gate_20260925/", + "tree": "eee9cdcb4c4b20542f96f6d7a60159aee9852664" + }, + "math.d1-component.reconciliation-record": { + "files": { + "reviews/d1_chain_reconciliation_20260928/LEDGER.json": "f6c329fb0f21a6cc2cc690240e86aa0a86114d4e", + "reviews/d1_chain_reconciliation_20260928/RECONCILIATION.md": "75da2597971510f843f8d90c743950cb8c177342", + "reviews/d1_chain_reconciliation_20260928/RESULTS.json": "e2de6c41e87ce64196dfbe5077ead2f09d87b367", + "reviews/d1_chain_reconciliation_20260928/SOURCE_FILES.json": "0ade05ab9218684955ad27a8c5c97f758dbc8e23", + "reviews/d1_chain_reconciliation_20260928/reconciliation_check.py": "c2f413e47feef42d57034685b1f504dd48ed2ed3" + }, + "source": "reviews/d1_chain_reconciliation_20260928/", + "tree": "39734d39af1220069d3a29405106e2bc03c45b40" + } + }, + "edge_count": 55, + "file_nodes": { + "eng.d0-packet-allowlist-fix": { + "blob": "f09b248b10d6a8f229bd21bb3a53a97ca16525f2", + "bytes": 3096, + "fingerprint": "84b7ad724e4da1c5b4b396c90ae03d4ddedc37771a6e45e1082a45fd80d7ff4a", + "kind": "sha256", + "sha256": "84b7ad724e4da1c5b4b396c90ae03d4ddedc37771a6e45e1082a45fd80d7ff4a", + "source": "frontiers/downstream_gate_20260925/patches/d0_packet_allowlist.patch" + }, + "math.d1-component.congruence-erratum": { + "blob": "213594d6ca6a86fb938110f4d166d9ce275a02d0", + "bytes": 1782, + "fingerprint": "bad7ef609c4ad8c41ad6af562c1b6807921e19a9d556ed793ad1a0db6e202028", + "kind": "sha256", + "sha256": "bad7ef609c4ad8c41ad6af562c1b6807921e19a9d556ed793ad1a0db6e202028", + "source": "imports/lifetime_parent_20260925/ERRATUM_CONGRUENCE.md" + }, + "math.d1-component.marked-cylinder-cap": { + "blob": "0633aca3c2a2882b0de4399da0a75d64c2e6b2e1", + "bytes": 15160, + "fingerprint": "0bf922b9203c29088b12388807aa0e2ecd020485eb0f6e919679841b5b2636fc", + "kind": "sha256", + "sha256": "0bf922b9203c29088b12388807aa0e2ecd020485eb0f6e919679841b5b2636fc", + "source": "imports/lifetime_parent_20260925/MARKED_CYLINDER_CAP_PROOF.md" + }, + "math.d1-component.section9-replacement-v1_1": { + "blob": "fe9b9ce4999908bb3814b500ee2d0ceb0c6f704a", + "bytes": 9062, + "fingerprint": "845abf9f9c99d672c2a10a887b5a2e7206a3d2de3d876f35f75ff6e2dc13e62f", + "kind": "sha256", + "sha256": "845abf9f9c99d672c2a10a887b5a2e7206a3d2de3d876f35f75ff6e2dc13e62f", + "source": "reviews/d1_section9_borel_repair_20260925/REPAIR.md" + }, + "math.d5-component.annulus-proof": { + "blob": "6f317515b3d417661f86e2fed09bc7d950899c2b", + "bytes": 16948, + "fingerprint": "d55e2c03bb17e7977ff94130cc1ff21e54840cd1e20dc4e41ea9ad52228beb05", + "kind": "sha256", + "sha256": "d55e2c03bb17e7977ff94130cc1ff21e54840cd1e20dc4e41ea9ad52228beb05", + "source": "frontiers/rn_annulus_bridge_20260925/PROOF.md" + }, + "math.d5-component.annulus-review": { + "blob": "559d72242fdb7e4e3ac4a10d10641ae1da84f40e", + "bytes": 15925, + "fingerprint": "69ea7a479a9e8a5150cbe07b373772ab22413a1d81194d5a6a78adf6ada5f03f", + "kind": "sha256", + "sha256": "69ea7a479a9e8a5150cbe07b373772ab22413a1d81194d5a6a78adf6ada5f03f", + "source": "reviews/pr28_annulus_bridge_nonauthor_20260925/REVIEW.md" + }, + "math.d5-component.collar-proof": { + "blob": "b5647907a8a589635855b59cc4246b23efce2801", + "bytes": 20318, + "fingerprint": "794babe0fd039c3a93c401c0b8978316331af009fad45f953e63fbe7e1e31aab", + "kind": "sha256", + "sha256": "794babe0fd039c3a93c401c0b8978316331af009fad45f953e63fbe7e1e31aab", + "source": "reviews/d5_local_collar_20260928/COLLAR_PROOF.md" + }, + "math.d5-component.collar-review": { + "blob": "6b1350fa88b3e217f9aac3c6bde7499639ebebe7", + "bytes": 1791, + "fingerprint": "8c8c30cc3b19601a7421341d5da8cb6c982fbc1fcc5ae4791acafa11cc81ee4e", + "kind": "sha256", + "sha256": "8c8c30cc3b19601a7421341d5da8cb6c982fbc1fcc5ae4791acafa11cc81ee4e", + "source": "reviews/d5_collar_count_20260928/REVIEW.md" + }, + "math.d5-component.i5-review-claude": { + "blob": "100ce8ecc120fcf4e3e4e6b9b039c08632e2711c", + "bytes": 14710, + "fingerprint": "fb3207802bf2d92ad138bc338a36f39bcd3a94440669b9aba9558e566f767976", + "kind": "sha256", + "sha256": "fb3207802bf2d92ad138bc338a36f39bcd3a94440669b9aba9558e566f767976", + "source": "reviews/d5_intermediate_window_claude_20260928/REVIEW.md" + }, + "math.d5-component.i5-review-xai": { + "blob": "031c5364d1377e0f85ed62caadd8406fbbedc0ab", + "bytes": 3552, + "fingerprint": "527d66e72b079ef7c854987f7e9d44f2252b81aa0e8d9e7ef3b45deb02388659", + "kind": "sha256", + "sha256": "527d66e72b079ef7c854987f7e9d44f2252b81aa0e8d9e7ef3b45deb02388659", + "source": "reviews/d5_i5_planar_f_20260928/REVIEW.md" + }, + "math.d5-component.intermediate-window-proof": { + "blob": "f53a527ce0204fda271f24730b62b4223a5e43ec", + "bytes": 27775, + "fingerprint": "b3eb9456d7058b5b78149cfca4e072679beda85cd1a0293d209664a542db66cf", + "kind": "sha256", + "sha256": "b3eb9456d7058b5b78149cfca4e072679beda85cd1a0293d209664a542db66cf", + "source": "frontiers/intermediate_window_20260928/PROOF.md" + }, + "math.d5-component.offpin-second-moment-review": { + "blob": "62697c471d4299590d807ab64b03b0ebb0d4dd43", + "bytes": 1473, + "fingerprint": "43beb4ef7da5bf71e15164d5c4bc1862db387d83f906aebdf9831625b1ca6369", + "kind": "sha256", + "sha256": "43beb4ef7da5bf71e15164d5c4bc1862db387d83f906aebdf9831625b1ca6369", + "source": "reviews/d5_offpin_second_moment_20260928/REVIEW.md" + }, + "math.d5-component.punctured-pin-continuum-check": { + "blob": "b0d6321f7e480d7ea855a2017532588d241f5d34", + "bytes": 10012, + "fingerprint": "57bc87d11271b47ebccec6ea73abf823fa90e3a8c3c7d7cd2165bdc6f7e4175c", + "kind": "sha256", + "sha256": "57bc87d11271b47ebccec6ea73abf823fa90e3a8c3c7d7cd2165bdc6f7e4175c", + "source": "reviews/d5_punctured_pin_continuum_claude_20260929/pp_continuum_check.py" + }, + "math.d5-component.punctured-pin-continuum-review": { + "blob": "4904e3d929d6dc7bc16b49eee85d7c3a21b6b484", + "bytes": 10501, + "fingerprint": "124c472cc064ba24021cb4bb8da1002836a4ce26c8df506228f5c20609f6575a", + "kind": "sha256", + "sha256": "124c472cc064ba24021cb4bb8da1002836a4ce26c8df506228f5c20609f6575a", + "source": "reviews/d5_punctured_pin_continuum_claude_20260929/REVIEW.md" + }, + "math.d5-component.punctured-pin-proof": { + "blob": "d8acf0bcb9025b1562c03c077c8e0094ae282060", + "bytes": 23771, + "fingerprint": "f972e50bc07674ebce9d971d1a1bd4dba2f8036dde37ecb57d8a44a35650f76a", + "kind": "sha256", + "sha256": "f972e50bc07674ebce9d971d1a1bd4dba2f8036dde37ecb57d8a44a35650f76a", + "source": "reviews/d5_local_collar_20260928/PUNCTURED_PIN_PROOF.md" + }, + "math.d5-component.punctured-pin-review-111": { + "blob": "c98cf3eefbf61f2ee3bbc1c8ef88f2218dd8b47a", + "bytes": 5534, + "fingerprint": "668d74825f156c929177bc1a3481cf421de4dd7874e7b25b2aece6c10aa7dc0e", + "kind": "sha256", + "sha256": "668d74825f156c929177bc1a3481cf421de4dd7874e7b25b2aece6c10aa7dc0e", + "source": "reviews/d5_punctured_pin_nonauthor_20260928/REVIEW.md" + }, + "math.d5-component.remote-window-proof": { + "blob": "b383bfcc88ec4ad497dff01fb6640e429ba24a84", + "bytes": 18355, + "fingerprint": "a332bae9bdc0106ce17047f7e0409cc3d94eb610a0c7b74ba5ba2d01a1620cb7", + "kind": "sha256", + "sha256": "a332bae9bdc0106ce17047f7e0409cc3d94eb610a0c7b74ba5ba2d01a1620cb7", + "source": "frontiers/remote_window_20260924/PROOF.md" + }, + "math.lifetime-remainder": { + "blob": "247b3ecf80bfbe896948d5d489b2d5842a81c481", + "bytes": 17734, + "fingerprint": "380b7d0abdb0fe2de5a6564565af9560d3f1b1ce22fd5538927db0c52f4f3a4a", + "kind": "sha256", + "sha256": "380b7d0abdb0fe2de5a6564565af9560d3f1b1ce22fd5538927db0c52f4f3a4a", + "source": "frontiers/three_fronts_20260924/LIFETIME_REMAINDER.md" + }, + "math.p15-price-boundary": { + "blob": "1d6946658e00e2446d68e8a4884425d4bcec62fb", + "bytes": 2266, + "fingerprint": "498af650ef7139c39d2630561dbfd0822c495ce6308a6cac8b767aefe0c2a40b", + "kind": "sha256", + "sha256": "498af650ef7139c39d2630561dbfd0822c495ce6308a6cac8b767aefe0c2a40b", + "source": "frontiers/three_fronts_20260924/P15_PRICE_BOUNDARY.md" + }, + "math.rn-count-interface": { + "blob": "371fd6d17920f2eb3b1c5ec30297acf6daf1d385", + "bytes": 8938, + "fingerprint": "aa993f5217bd70e6d1060402d121ab659f6582a098e29042c67585a345a8e3ab", + "kind": "sha256", + "sha256": "aa993f5217bd70e6d1060402d121ab659f6582a098e29042c67585a345a8e3ab", + "source": "frontiers/three_fronts_20260924/RN_COUNT_INTERFACE.md" + }, + "math.rn-fixed-annulus-window": { + "blob": "081abc13c5e66342c13df2af2bd6b114f320486f", + "bytes": 17646, + "fingerprint": "081abc13c5e66342c13df2af2bd6b114f320486f", + "kind": "blob40", + "sha256": "1fd9fe7141e464fd1c09ebf0318d8a701729c9c61ea24f73f7e20a9cf10c552b", + "source": "frontiers/rn_thin_tube_20260925/FIXED_ANNULUS_CANDIDATE.md" + }, + "math.rn-fixed-remote-window": { + "blob": "b383bfcc88ec4ad497dff01fb6640e429ba24a84", + "bytes": 18355, + "fingerprint": "a332bae9bdc0106ce17047f7e0409cc3d94eb610a0c7b74ba5ba2d01a1620cb7", + "kind": "sha256", + "sha256": "a332bae9bdc0106ce17047f7e0409cc3d94eb610a0c7b74ba5ba2d01a1620cb7", + "source": "frontiers/remote_window_20260924/PROOF.md" + }, + "math.side24-coefficient": { + "blob": "44b66f04f89fcd87383b3603fa69f1feb64cdddd", + "bytes": 10272, + "fingerprint": "c06daccc4ba4b9168522b9888b76a7d599934fc3b91bd753ee5d492262917769", + "kind": "sha256", + "sha256": "c06daccc4ba4b9168522b9888b76a7d599934fc3b91bd753ee5d492262917769", + "source": "coefficients/side24_v1/PROOF.md" + }, + "math.uniform-matrix-cap-lifetime": { + "blob": "dfed3b8d318a3ab1950957f393307733a4bef3f2", + "bytes": 40261, + "fingerprint": "9350ad6eaba6626b93c3dedeef9e2ff816e5cdf1c8318e85fb27499141c84bc7", + "kind": "sha256", + "sha256": "9350ad6eaba6626b93c3dedeef9e2ff816e5cdf1c8318e85fb27499141c84bc7", + "source": "imports/lifetime_parent_20260925/UNIFORM_MATRIX_CAP_AND_LIFETIME.md" + } + }, + "fingerprint_census": { + "ABSENT": 3, + "blob40": 1, + "no_field": 2, + "sha256": 23, + "symbolic": 20 + }, + "graph_bytes": 38753, + "graph_sha256": "8822e9618678321a342d69cd0b8ae6552de1b5d578c331de5072b2892ee9dd09", + "meaning": "Pinned path/blob/bytes/sha256 per digest-fingerprinted atlas node at the PROVENANCE commit, plus the files under each directory-bound source. Byte identity only; not proof validity, review independence or scientific acceptance.", + "node_count": 49, + "object": "DEPENDENCY-SOURCE-BINDINGS-v1", + "pinned_commit": "7858329974e28be79f29b22644370084ff43da4f", + "record_only_nodes": [ + "hist.ALLCELL-FDZ-Q4", + "hist.CH-LIFT", + "hist.ENV-RESCOV", + "hist.LOGQ-TAIL", + "hist.OBL-H5-JETMOD", + "hist.Piece-2-annulus", + "hist.SYM-Fw-jet", + "hist.lemma_closed", + "math.rn-region.fixed-annulus-window", + "math.rn-region.fixed-remote", + "math.rn-region.intermediate-r-to-rho", + "math.rn-region.mesoscopic-scaled-annulus", + "math.rn-region.pin-collision", + "math.rn-region.witness-collision", + "regional.fixed-annulus.high-jet-route" + ], + "scientific_effect": "NONE" +} diff --git a/tests/test_dependency_source_binding.py b/tests/test_dependency_source_binding.py new file mode 100644 index 000000000..126930b0e --- /dev/null +++ b/tests/test_dependency_source_binding.py @@ -0,0 +1,149 @@ +"""Offline readback of the dependency atlas's node fingerprints against pinned sources. + +Scientific effect: NONE. These controls check byte identity and graph structure. +A digest match is not proof validity, review independence or scientific acceptance; +the page's own "binding unresolved" wording is not changed by anything here. + +Inputs, all committed: +- docs/site/dependency-source/GRAPH.json and PROVENANCE.json (the 3 October snapshot); +- docs/site/current.json (the committed inventory observation); +- tests/fixtures/dependency_source_bindings.json, written by + tools/dependency_source_bindings.py from a Math- checkout at the pinned commit. +""" +from __future__ import annotations + +import hashlib +import json +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "tools")) +from dependency_source_bindings import fingerprint_kind, is_repository_path # noqa: E402 + +SITE = ROOT / "docs" / "site" +GRAPH_PATH = SITE / "dependency-source" / "GRAPH.json" +PROVENANCE_PATH = SITE / "dependency-source" / "PROVENANCE.json" +CURRENT_PATH = SITE / "current.json" +FIXTURE_PATH = ROOT / "tests" / "fixtures" / "dependency_source_bindings.json" +MATH_REPOSITORY = "d6g8k5htny-coder/Math-" + +EXPECTED_NODES = 49 +EXPECTED_EDGES = 55 +EXPECTED_REQUIRED_EDGES = 42 +EXPECTED_CENSUS = {"ABSENT": 3, "blob40": 1, "no_field": 2, "sha256": 23, "symbolic": 20} +EXPECTED_RECORD_ONLY = 15 + + +def load_json(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +class DependencySourceBinding(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.graph_bytes = GRAPH_PATH.read_bytes() + cls.graph = json.loads(cls.graph_bytes) + cls.provenance = load_json(PROVENANCE_PATH) + cls.current = load_json(CURRENT_PATH) + cls.fixture = load_json(FIXTURE_PATH) + cls.inventory = {(row["repository"], row["path"]): row for row in cls.current["sources"]} + + def test_graph_bytes_match_provenance_and_fixture(self): + digest = hashlib.sha256(self.graph_bytes).hexdigest() + self.assertEqual(digest, self.provenance["files"]["GRAPH.json"]["sha256"]) + self.assertEqual(len(self.graph_bytes), self.provenance["files"]["GRAPH.json"]["bytes"]) + self.assertEqual(digest, self.fixture["graph_sha256"]) + self.assertEqual(self.fixture["graph_bytes"], len(self.graph_bytes)) + self.assertEqual(self.fixture["pinned_commit"], self.provenance["commit"]) + identity = self.current["graph"]["identity"] + self.assertEqual(identity["sha256"], digest) + self.assertEqual(identity["bytes"], len(self.graph_bytes)) + # The inventory's blob id is Git's object name for these same bytes. + git_blob = hashlib.sha1(b"blob %d\0" % len(self.graph_bytes) + self.graph_bytes).hexdigest() + self.assertEqual(identity["blob"], git_blob) + + def test_node_and_edge_structure(self): + nodes = self.graph["nodes"] + edges = self.graph["edges"] + self.assertEqual(len(nodes), EXPECTED_NODES) + self.assertEqual(len(edges), EXPECTED_EDGES) + self.assertEqual(self.fixture["node_count"], EXPECTED_NODES) + self.assertEqual(self.fixture["edge_count"], EXPECTED_EDGES) + pairs = set() + required = 0 + for edge in edges: + self.assertIn(edge["from"], nodes, edge) + self.assertIn(edge["to"], nodes, edge) + pair = (edge["from"], edge["to"]) + self.assertNotIn(pair, pairs, "duplicate edge") + pairs.add(pair) + if edge.get("required") is True: + required += 1 + self.assertEqual(required, EXPECTED_REQUIRED_EDGES) + self.assertEqual(len(edges) - required, EXPECTED_EDGES - EXPECTED_REQUIRED_EDGES) + + def test_fingerprint_census_is_pinned(self): + census: dict[str, int] = {} + for node in self.graph["nodes"].values(): + kind = fingerprint_kind(node.get("fingerprint")) + census[kind] = census.get(kind, 0) + 1 + self.assertEqual(census, EXPECTED_CENSUS) + self.assertEqual(self.fixture["fingerprint_census"], EXPECTED_CENSUS) + + def test_absent_carriers_are_fail_closed(self): + for node_id, node in self.graph["nodes"].items(): + if node.get("fingerprint") == "ABSENT": + self.assertEqual(node.get("classification"), "BLOCKED_ABSENT", node_id) + self.assertFalse(node.get("controlling"), node_id) + + def test_every_digest_node_binds_to_its_pinned_bytes(self): + file_nodes = self.fixture["file_nodes"] + digest_nodes = {node_id for node_id, node in self.graph["nodes"].items() + if fingerprint_kind(node.get("fingerprint")) in ("sha256", "blob40")} + self.assertEqual(set(file_nodes), digest_nodes) + for node_id, pinned in file_nodes.items(): + node = self.graph["nodes"][node_id] + with self.subTest(node=node_id): + self.assertEqual(node["source"], pinned["source"]) + self.assertTrue(is_repository_path(pinned["source"])) + expected = pinned["sha256"] if pinned["kind"] == "sha256" else pinned["blob"] + self.assertEqual(node["fingerprint"], expected, "fingerprint is not the pinned identity") + row = self.inventory.get((MATH_REPOSITORY, pinned["source"])) + self.assertIsNotNone(row, "committed inventory lacks the pinned path") + self.assertEqual(row["blob"], pinned["blob"], "inventory blob differs from the pinned blob") + self.assertEqual(row["bytes"], pinned["bytes"], "inventory byte count differs from the pinned bytes") + + def test_record_only_nodes_are_pinned(self): + # Same rule as the generator: a symbolic fingerprint and no source value + # (the key absent or null) makes a node record-only. + record_only = sorted(node_id for node_id, node in self.graph["nodes"].items() + if fingerprint_kind(node.get("fingerprint")) == "symbolic" and node.get("source") is None) + self.assertEqual(record_only, self.fixture["record_only_nodes"]) + self.assertEqual(len(record_only), EXPECTED_RECORD_ONLY) + + def test_directory_sources_report_inventory_drift_without_failing(self): + # The graph binds two nodes to whole directories without a digest. The pinned + # file list is compared with the committed inventory and the differences are + # reported; a difference is information for a reviewer, not a failure here. + sources = self.fixture["directory_sources"] + self.assertTrue(sources, "fixture lists no directory source") + for node_id, pinned in sources.items(): + self.assertTrue(pinned["files"], node_id) + prefix = pinned["source"] + observed = {path: row["blob"] for (repo, path), row in self.inventory.items() + if repo == MATH_REPOSITORY and path.startswith(prefix)} + added = sorted(set(observed) - set(pinned["files"])) + removed = sorted(set(pinned["files"]) - set(observed)) + changed = sorted(path for path in set(observed) & set(pinned["files"]) + if observed[path] != pinned["files"][path]) + report = {"node": node_id, "source": prefix, "pinned_commit": self.fixture["pinned_commit"], + "observation_commit": self.current["graph"]["identity"]["commit"], + "added": added, "removed": removed, "changed": changed, + "meaning": "inventory rows under a directory-bound source compared with the pinned file list; byte identity only"} + print("DIRECTORY_SOURCE_REPORT " + json.dumps(report, sort_keys=True)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/dependency_source_bindings.py b/tools/dependency_source_bindings.py new file mode 100644 index 000000000..3b3a74e25 --- /dev/null +++ b/tools/dependency_source_bindings.py @@ -0,0 +1,156 @@ +"""Record the pinned source identities behind the dependency atlas's node fingerprints. + +Scientific effect: NONE. A digest match establishes byte identity only, never proof +validity, review independence or scientific acceptance. + +The public atlas (docs/site/dependencies.html) renders the downstream hard-gate graph +captured in docs/site/dependency-source/GRAPH.json from the Math- commit named in +docs/site/dependency-source/PROVENANCE.json. Some graph nodes carry a fingerprint +that is the SHA-256 of a Math- file (one node carries the file's Git blob id) and a +``source`` path. This tool reads those files from a Math- checkout at the pinned +commit and writes a fixture that tests/test_dependency_source_binding.py compares +with GRAPH.json and with the committed inventory in docs/site/current.json. + +Usage: + python3 tools/dependency_source_bindings.py --math /path/to/Math- \ + [--output tests/fixtures/dependency_source_bindings.json] + +The Math- checkout must contain the pinned commit (a shallow clone may not). Only +Git plumbing commands that read objects are run; nothing is written to Math-. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +GRAPH_PATH = ROOT / "docs" / "site" / "dependency-source" / "GRAPH.json" +PROVENANCE_PATH = ROOT / "docs" / "site" / "dependency-source" / "PROVENANCE.json" +DEFAULT_OUTPUT = ROOT / "tests" / "fixtures" / "dependency_source_bindings.json" + +SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +BLOB_RE = re.compile(r"^[0-9a-f]{40}$") + + +def fingerprint_kind(fingerprint: object) -> str: + """Classify a node fingerprint: sha256, blob40, ABSENT, symbolic or no_field.""" + if fingerprint is None: + return "no_field" + if not isinstance(fingerprint, str): + return "symbolic" + if fingerprint == "ABSENT": + return "ABSENT" + if SHA256_RE.match(fingerprint): + return "sha256" + if BLOB_RE.match(fingerprint): + return "blob40" + return "symbolic" + + +def is_repository_path(source: object) -> bool: + """A source that names a path inside the Math- repository (not a URL or label).""" + return isinstance(source, str) and bool(source) and "://" not in source and ":" not in source + + +def _git(math_root: Path, *args: str) -> bytes: + proc = subprocess.run( + ["git", "--no-replace-objects", "-C", str(math_root), *args], + capture_output=True, timeout=60, check=False) + if proc.returncode: + raise SystemExit("REFUSED: git failed: " + proc.stderr.decode(errors="replace").strip()) + return proc.stdout + + +def build_fixture(math_root: Path, graph: dict, provenance: dict, graph_bytes: bytes) -> dict: + commit = provenance["commit"] + _git(math_root, "cat-file", "-e", commit + "^{commit}") + nodes = graph["nodes"] + census: dict[str, int] = {} + file_nodes: dict[str, dict] = {} + directory_sources: dict[str, dict] = {} + record_only: list[str] = [] + for node_id in sorted(nodes): + node = nodes[node_id] + kind = fingerprint_kind(node.get("fingerprint")) + census[kind] = census.get(kind, 0) + 1 + source = node.get("source") + if kind in ("sha256", "blob40"): + if not is_repository_path(source): + raise SystemExit("REFUSED: digest node without a repository path: " + node_id) + blob = _git(math_root, "rev-parse", f"{commit}:{source}").decode().strip() + data = _git(math_root, "cat-file", "blob", blob) + file_nodes[node_id] = { + "source": source, + "kind": kind, + "fingerprint": node["fingerprint"], + "blob": blob, + "bytes": len(data), + "sha256": hashlib.sha256(data).hexdigest(), + } + elif kind == "symbolic" and source is None: + record_only.append(node_id) + if is_repository_path(source) and source.endswith("/"): + listing = _git(math_root, "ls-tree", "-r", "-z", commit, "--", source).decode() + files = {} + for entry in listing.split("\0"): + if not entry: + continue + meta, _, path = entry.partition("\t") + _mode, _kind, blob = meta.split(" ", 2) + files[path] = blob + tree = _git(math_root, "rev-parse", f"{commit}:{source.rstrip('/')}").decode().strip() + directory_sources[node_id] = {"source": source, "tree": tree, "files": files} + return { + "object": "DEPENDENCY-SOURCE-BINDINGS-v1", + "scientific_effect": "NONE", + "meaning": ("Pinned path/blob/bytes/sha256 per digest-fingerprinted atlas node at the PROVENANCE commit, " + "plus the files under each directory-bound source. Byte identity only; not proof validity, " + "review independence or scientific acceptance."), + "pinned_commit": commit, + "graph_sha256": hashlib.sha256(graph_bytes).hexdigest(), + "graph_bytes": len(graph_bytes), + "node_count": len(nodes), + "edge_count": len(graph["edges"]), + "fingerprint_census": dict(sorted(census.items())), + "file_nodes": file_nodes, + "directory_sources": directory_sources, + "record_only_nodes": record_only, + } + + +def render(fixture: dict) -> str: + return json.dumps(fixture, indent=1, sort_keys=True) + "\n" + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + parser.add_argument("--math", required=True, type=Path, help="Math- checkout containing the pinned commit") + parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) + parser.add_argument("--check", action="store_true", help="compare with the existing fixture instead of writing") + args = parser.parse_args(argv) + graph_bytes = GRAPH_PATH.read_bytes() + fixture = build_fixture(args.math, json.loads(graph_bytes), json.loads(PROVENANCE_PATH.read_text(encoding="utf-8")), graph_bytes) + text = render(fixture) + if args.check: + existing = args.output.read_text(encoding="utf-8") if args.output.exists() else "" + if existing != text: + print("DEPENDENCY_SOURCE_BINDINGS_DIFFER: " + str(args.output), file=sys.stderr) + return 1 + print("DEPENDENCY_SOURCE_BINDINGS_MATCH: " + str(args.output)) + return 0 + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(text, encoding="utf-8") + print(json.dumps({"written": str(args.output), "file_nodes": len(fixture["file_nodes"]), + "directory_sources": len(fixture["directory_sources"]), + "record_only_nodes": len(fixture["record_only_nodes"]), + "pinned_commit": fixture["pinned_commit"]}, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())