diff --git a/research/joint-source-ancestry/BENCHMARK.json b/research/joint-source-ancestry/BENCHMARK.json new file mode 100644 index 0000000000..6ecb9125a6 --- /dev/null +++ b/research/joint-source-ancestry/BENCHMARK.json @@ -0,0 +1,181 @@ +{ + "base_commit": "721c03822c421b7f7fcbb2598993b947c4d88419", + "baseline_source_sha256": "63026ec3805f2a3236cc071fc78cf1c54bc6528c43e2f3d82548dc68bf346bce", + "changed_source_sha256": "31a5d71c96e2b2e5bd1a81cc2210604410ad23477cb2f2d58c729530bf39dbf9", + "failures": 0, + "fixture": "research/e8-d0-root-swaps", + "git": "git version 2.54.0 (Apple Git-157)", + "load_average": [ + 3.5625, + 3.7978515625, + 3.55908203125 + ], + "logical_cpus": 18, + "pairs": 7, + "platform": "macOS-27.0.1-arm64-arm-64bit", + "python": "3.12.15 (main, Oct 14 2026, 00:55:37) [Clang 23.1.3 ]", + "receipt": { + "commit": "721c03822c421b7f7fcbb2598993b947c4d88419", + "files": 14, + "package": "research/e8-d0-root-swaps", + "source_history_commits": [ + "08e134b4a0c7f04869dae703f2a2e688e53c44f0", + "18f7d4da808edb82d32e2081f869b8c993eca891", + "41826461eaceb25b77c026148b50b884e3c9780e", + "4c32ac3b88d63ad06d2b30e526aa07a12929db36", + "997e6a9ca7b4f95d50e366f61412fd640d72a88a" + ] + }, + "receipt_sha256": "92528be17fc5781fc54e65c294e9a607aa19fe9c6eddf5249ce07cf71906f796", + "samples": [ + { + "pair": 0, + "returncode": 0, + "seconds": 0.24568341695703566, + "side": "baseline" + }, + { + "pair": 0, + "returncode": 0, + "seconds": 0.15555229200981557, + "side": "changed" + }, + { + "pair": 1, + "returncode": 0, + "seconds": 0.17523508286103606, + "side": "changed" + }, + { + "pair": 1, + "returncode": 0, + "seconds": 0.23902325006201863, + "side": "baseline" + }, + { + "pair": 2, + "returncode": 0, + "seconds": 0.24392245803028345, + "side": "baseline" + }, + { + "pair": 2, + "returncode": 0, + "seconds": 0.1694880418945104, + "side": "changed" + }, + { + "pair": 3, + "returncode": 0, + "seconds": 0.20432179211638868, + "side": "changed" + }, + { + "pair": 3, + "returncode": 0, + "seconds": 0.24606208316981792, + "side": "baseline" + }, + { + "pair": 4, + "returncode": 0, + "seconds": 0.2425744580104947, + "side": "baseline" + }, + { + "pair": 4, + "returncode": 0, + "seconds": 0.1596787499729544, + "side": "changed" + }, + { + "pair": 5, + "returncode": 0, + "seconds": 0.1580816670320928, + "side": "changed" + }, + { + "pair": 5, + "returncode": 0, + "seconds": 0.2403820410836488, + "side": "baseline" + }, + { + "pair": 6, + "returncode": 0, + "seconds": 0.23771945806220174, + "side": "baseline" + }, + { + "pair": 6, + "returncode": 0, + "seconds": 0.15462500019930303, + "side": "changed" + } + ], + "schema_version": 1, + "shared_preflight_sha256": "63026ec3805f2a3236cc071fc78cf1c54bc6528c43e2f3d82548dc68bf346bce", + "shared_source_reader_sha256": "84a45190a6a96c20993a0d4a1045fbf24de660d2e6fef8f8a447bb3d1aa78db6", + "summary": { + "baseline": { + "maximum_seconds": 0.24606208316981792, + "median_seconds": 0.2425744580104947, + "minimum_seconds": 0.23771945806220174 + }, + "changed": { + "maximum_seconds": 0.20432179211638868, + "median_seconds": 0.1596787499729544, + "minimum_seconds": 0.15462500019930303 + } + }, + "untimed_diagnostic_profiles": { + "baseline": { + "check_research_commit.py:check": { + "calls": 1, + "cumulative_seconds": 0.22709825 + }, + "check_research_commit.py:history_git": { + "calls": 13, + "cumulative_seconds": 0.14736337500000002 + }, + "check_research_commit.py:main": { + "calls": 1, + "cumulative_seconds": 0.227757958 + }, + "check_research_commit.py:source_history": { + "calls": 1, + "cumulative_seconds": 0.148056417 + }, + "subprocess.py:run": { + "calls": 16, + "cumulative_seconds": 0.20623616700000003 + } + }, + "changed": { + "check_research_commit.py:check": { + "calls": 1, + "cumulative_seconds": 0.07309779200000001 + }, + "preflight.py:check": { + "calls": 1, + "cumulative_seconds": 0.12573045800000002 + }, + "preflight.py:history_git": { + "calls": 5, + "cumulative_seconds": 0.052028084 + }, + "preflight.py:main": { + "calls": 1, + "cumulative_seconds": 0.12632416700000001 + }, + "preflight.py:source_history": { + "calls": 1, + "cumulative_seconds": 0.052600209 + }, + "subprocess.py:run": { + "calls": 8, + "cumulative_seconds": 0.106398667 + } + } + } +} diff --git a/research/joint-source-ancestry/README.md b/research/joint-source-ancestry/README.md new file mode 100644 index 0000000000..c99183292c --- /dev/null +++ b/research/joint-source-ancestry/README.md @@ -0,0 +1,147 @@ +# Joint publication source ancestry + +The complete publication byte/history preflight takes **34.2% less time** on the +unchanged `research/e8-d0-root-swaps` fixture: median **0.2425745 s to +0.1596787 s**, with the same complete receipt. This is a verifier-engineering +improvement with a scoped equivalence proof and empirical timing certificate. +The command is opt-in; it shares the original byte/mode checker and supplies a +new source-history implementation under this package. +It changes no mathematical research result, supplier or kappa. + +The fixture has 14 files and five distinct source commits. Seven alternating +fresh-process pairs all pass. Baseline times range from 0.2377195 to 0.2460621 s; +changed times range from 0.1546250 to 0.2043218 s. The generated +[BENCHMARK.json](BENCHMARK.json) binds both implementation hashes, the complete +receipt, every sample and the environment. Timings include interpreter startup, +argument parsing, file inventory, byte comparison and source-history checking. +They exclude fixture preparation and separate diagnostic profiling. This is +warm local filesystem evidence on a shared macOS arm64 host, not a universal +runtime guarantee or a multiplier speedup. + +## What changes + +The existing `--check-source-history` preflight resolves every distinct pin and +runs a separate `merge-base --is-ancestor` query for it. For multiple source +commits, the changed preflight first checks every exact object type with one +`cat-file --batch-check` request. It then evaluates: + +```text +git rev-list --max-count=1 --not +``` + +If the result is empty, all pins are ancestors. Otherwise it runs the original +per-pin checks for the existing first-failure diagnostic. Ordinary missing or +noncommit objects also take that original path, so a later invalid object does +not hide an earlier ancestry error. Malformed protocol responses, Git errors +and timeouts fail closed. The single-source path remains the original path. + +On this fixture, source-history Git calls fall from 13 to 5. Separate profiles +in the generated evidence identify subprocess waiting as the dominant baseline +cost. The source-history portion occupies about 65% of the profiled complete +check; removing eight processes is consistent with the measured end-to-end gain. +The profiles are diagnostic observations, not timed samples. + +## Scoped equivalence proof + +Let `A(c)` include commit `c` and all its ancestors in the stored Git graph. +For exact commit pins `p_1,...,p_k` and publication `h`, the revision expression +above enumerates + +```text +(A(p_1) union ... union A(p_k)) minus A(h). +``` + +This set is empty exactly when every `p_i` belongs to `A(h)`. In one direction, +each pin belongs to its own ancestor set. In the other direction, ancestry is +transitive, so every ancestor of a pin that belongs to `A(h)` also belongs to +`A(h)`. Limiting enumeration to one commit preserves emptiness. The type batch +requires the returned object ID to equal the literal pin and its type to be +`commit`; an annotated tag cannot silently stand in for its peeled commit. +This equivalence assumes a stable graph and completed, successful Git queries; +resource failures still reject. + +Only that positive result bypasses the old loop. Ordinary negative answers run +the unchanged loop in sorted order. Both paths use committed metadata, reject +shallow history, disable replacement objects and grafts, and retain the existing +Git environment isolation and 30-second per-command timeout. No new ref, index +or repository mutation is involved. Corrupt Git graphs and hostile Git binaries +are outside the original and changed contracts. + +The written argument concerns acceptance in the stated finite Git graph; the +tests exercise concrete positive and failure cases. It is not a formal-kernel +proof or a re-verification of the fixture's mathematics. + +## Verification and reproduction + +Run the optimized command from the repository root, using CPython 3.12: + +```sh +/Users/nick/git/integer-mult-bounds/.venv/bin/python3.12 -B research/joint-source-ancestry/preflight.py --package research/e8-d0-root-swaps --check-source-history +``` + +Verify and publish this package with: + +```sh +/Users/nick/git/integer-mult-bounds/.venv/bin/python3.12 -B scripts/research_package.py --package research/joint-source-ancestry +/Users/nick/git/integer-mult-bounds/.venv/bin/python3.12 -B research/joint-source-ancestry/verify.py +/Users/nick/git/integer-mult-bounds/.venv/bin/python3.12 -B scripts/check_research_commit.py --package research/joint-source-ancestry --check-source-history +``` + +The registered normal and optimized commands compare the full real receipt and +run all 18 source-history tests through the adapter. The thirteen inherited +contract tests remain the original source; five new tests cover the joint path. They cover merged incomparable sources, duplicate +pins, foreign cached commits, missing commits, tags/trees/blobs, the first-error +ordering, committed metadata versus working drift, shallow history, replacements, +grafts, bounds, malformed responses, Git failures, timeout propagation and the +read-only CLI. They perform no scientific replay of the benchmark fixture. + +To repeat the optional timing experiment, create a scratch directory outside the +checkout and pass its path: + +```sh +/Users/nick/git/integer-mult-bounds/.venv/bin/python3.12 -B research/joint-source-ancestry/benchmark.py --scratch /absolute/external/scratch +``` + +The benchmark authenticates the baseline script against its provenance pin, +replays that exact module with its original import/root context, and compares +complete receipts from both CLIs. It needs full Git history and the materialized +14-file fixture. It writes temporary files only under the supplied directory and +removes them. The registered verifier does not enforce host-dependent timings. +The generic replay and publication preflight themselves require Git; this package +does not promise a Git-free export. No Lean, Docker, selected cold supplier replay +or complete multiplication theorem was run or claimed. + +## Resource bounds and inspection + +The existing limits remain 100,000 metadata rows, 256 distinct source commits +and 16 MiB of source metadata. New loops traverse at most 256 pins and four +object kinds. A batch request contains at most 16,640 bytes for 64-digit IDs; +normal replies contain at most 18,688 bytes. Joint reachability emits at most one +commit ID, with a 30-second timeout. Rejection can add two Git commands before +the original loop and can therefore be slower. A large-history joint walk might +also be slower or time out when individual commit-graph checks would pass. + +New functions remain under 60 physical lines and use structured, nonrecursive +control flow. Python allocation is a managed-runtime deviation from fixed-memory +operation; pin counts, request/reply sizes and benchmark sample counts bound the +new retained collections. The benchmark's dynamic execution is limited to the +exact hash-authenticated baseline module; it preserves that module's CLI context. +Normal and optimized tests run with warnings treated as errors. + +## Open next questions + +Does shared traversal still help on real publications with 32 to 256 distinct +source commits and much larger histories? Alternate the complete CLI paths on +such a publication, keep exact receipts and rejection controls, and profile the +remaining work. A representative publication and a separate measurement budget +are needed. No broad performance claim follows from the five-pin fixture. + +## Credit + +This contribution was produced by Nick Petalas. The publication preflight, +source-history policy and immutable Git reader retain their existing attribution, +including ywz65, Nick Petalas, roger-sato, Arham Tahir and Douglas Colkitt. +Conorholds's root-swap package and all its predecessor mathematics remain their +authors' work; only its existing file/history inventory is used as a timing +fixture. New work is Apache-2.0. Independent human and formal proof-kernel review +are unclaimed. diff --git a/research/joint-source-ancestry/SOURCES.json b/research/joint-source-ancestry/SOURCES.json new file mode 100644 index 0000000000..a381176db9 --- /dev/null +++ b/research/joint-source-ancestry/SOURCES.json @@ -0,0 +1,36 @@ +{ + "schema_version": 1, + "base_commit": "721c03822c421b7f7fcbb2598993b947c4d88419", + "files": [ + { + "path": "certificates/selected-result.json", + "usage": "provenance", + "git_blob": "d59b5035e7b4cbaa9d688a1d32dd0d6eb79c466a", + "sha256": "9b8d0eea95b3bf0dfcb7938a732a6439b87a2685dec2a5ab9e525b3b34debbfa" + }, + { + "path": "docs/research/agent-task-catalog.json", + "usage": "provenance", + "git_blob": "3404fbdd5e7529a0674eef26114b5071e4586cec", + "sha256": "ead6cdc9d0af6d58e8a49744d8ec8c7c15e13d2ba93c276c434bcf9b51b93a81" + }, + { + "path": "scripts/check_research_commit.py", + "usage": "provenance", + "git_blob": "dc0be6c743f79b3131e674d92c81b75b069812c9", + "sha256": "63026ec3805f2a3236cc071fc78cf1c54bc6528c43e2f3d82548dc68bf346bce" + }, + { + "path": "scripts/research_package.py", + "usage": "provenance", + "git_blob": "48fe9feb4699c09e95505d95c327ea998ac8b3a7", + "sha256": "84a45190a6a96c20993a0d4a1045fbf24de660d2e6fef8f8a447bb3d1aa78db6" + }, + { + "path": "tests/test_research_source_history.py", + "usage": "provenance", + "git_blob": "aaaa47c607ce1ee317f0c281586016920ff40eac", + "sha256": "0175206fa52dff514fea4d625a072bb66735eb5214be9396dc42b874f0918454" + } + ] +} diff --git a/research/joint-source-ancestry/TASK.json b/research/joint-source-ancestry/TASK.json new file mode 100644 index 0000000000..041e9f9dac --- /dev/null +++ b/research/joint-source-ancestry/TASK.json @@ -0,0 +1,61 @@ +{ + "schema_version": 1, + "task_id": "replay-runtime", + "priority_revision": 8, + "base_commit": "721c03822c421b7f7fcbb2598993b947c4d88419", + "original_card": { + "id": "replay-runtime", + "question": "Can one measured replay bottleneck be sped up without dropping coverage?", + "starting_points": [ + "scripts/run_isolated_tests.py", + "scripts/verify_replay_coverage.py", + "certificates/selected-result.json" + ], + "methods": [ + "Profile one isolated replay and optimize the measured bottleneck.", + "Design source-bound reuse with explicit dependency invalidation and negative controls." + ], + "acceptance_gate": "Report before/after wall time on the same fixture, retained checks and invalidation behavior.", + "first_step": { + "command": [ + "python3", + "-B", + "scripts/research_selected_baseline.py" + ], + "expected": "PASS_RESEARCH_BASELINE_BINDINGS_ONLY", + "inspect": "Resolve the current selected review, separate complex/ordinary BIT/admitted BIT/outer budgets, and compare the card against live handoffs before starting. Historical ceilings require exact source/model equivalence.", + "scope": "Source-binding orientation only; not a scientific replay or all-size proof. Materialize selected inputs if this checkout is sparse." + }, + "first_experiment": "Measure one unchanged fixture, preserve check coverage, and test one optimization with invalidation controls.", + "bottleneck_link": "Faster trustworthy replays increase review throughput and let mechanism candidates be checked sooner.", + "allowed_variations": [ + "Profile one stage or design one source-bound cache boundary." + ], + "scope_change_triggers": [ + "Skipping coverage or comparing different fixtures to claim speedup." + ], + "overlap_terms": [ + "replay runtime", + "cache invalidation", + "isolated tests" + ] + }, + "scope_status": "changed", + "actual_scope": "An opt-in publication preflight uses bounded joint reachability while preserving the original byte/mode check, source-history acceptance, ordinary diagnostics and source isolation.", + "scope_change_reason": "The publication script is a frozen live input elsewhere, so the same measured optimization is delivered as an opt-in adapter. Existing shared scripts and tests remain byte-identical to the base.", + "bottleneck_link": "This verifier-engineering result reduces the latency of source-bound publication checks; it does not change either supplier or kappa.", + "budget_minutes": 60, + "stop_rule": "Ship the first complete-preflight gain with equal receipts and passing rejection controls; do not expand the algorithm after that.", + "outcome": "completed", + "evidence": "BENCHMARK.json contains seven alternating fresh-process pairs, complete equal receipts, implementation hashes and separate diagnostic profiles. The registered checks run all 18 source-history tests.", + "author": "Nick Petalas", + "ai_assistance": "Independent human and formal proof-kernel review are unclaimed.", + "open_questions": [ + { + "question": "How does the joint walk behave on much larger histories with 32 to 256 distinct ancestor pins?", + "why_it_matters": "Shared traversal can have different costs from separate commit-graph queries; no universal speedup is established.", + "first_step": "Use a real publication with those pins and alternate complete CLI runs, retaining identical receipts and independent rejection controls.", + "blocked_by": "A representative large-pin publication fixture and a separate measurement budget." + } + ] +} diff --git a/research/joint-source-ancestry/VERIFY.json b/research/joint-source-ancestry/VERIFY.json new file mode 100644 index 0000000000..0eed2d64db --- /dev/null +++ b/research/joint-source-ancestry/VERIFY.json @@ -0,0 +1,50 @@ +{ + "schema_version": 1, + "status": "ready", + "python": "3.12", + "requirements": [], + "inputs": [ + "research/e8-d0-root-swaps/EXTENSION.md", + "research/e8-d0-root-swaps/PROOF.md", + "research/e8-d0-root-swaps/README.md", + "research/e8-d0-root-swaps/RESULT.json", + "research/e8-d0-root-swaps/SOURCES.json", + "research/e8-d0-root-swaps/TASK.json", + "research/e8-d0-root-swaps/VERIFY.json", + "research/e8-d0-root-swaps/algebra_check.py", + "research/e8-d0-root-swaps/extension_check.py", + "research/e8-d0-root-swaps/formal/RootSwapConnectedCover.lean", + "research/e8-d0-root-swaps/formal/RootSwapOneExceptional.lean", + "research/e8-d0-root-swaps/formal/RootSwapQuartet.lean", + "research/e8-d0-root-swaps/geometry_audit.py", + "research/e8-d0-root-swaps/verify.py", + "scripts/check_research_commit.py", + "scripts/research_package.py", + "tests/test_research_source_history.py" + ], + "commands": [ + { + "argv": [ + "{python}", + "-W", + "error", + "-B", + "{package}/verify.py" + ], + "timeout_seconds": 120, + "checks": "Complete real 14-file/five-source publication receipt, measured implementation binding and all 18 source-history tests, including multi-parent, object-type, mixed-invalid, graft/replacement, protocol/error and timeout controls." + }, + { + "argv": [ + "{python}", + "-O", + "-W", + "error", + "-B", + "{package}/verify.py" + ], + "timeout_seconds": 120, + "checks": "Same complete checks with optimization enabled, including optimized child rejection controls." + } + ] +} diff --git a/research/joint-source-ancestry/benchmark.py b/research/joint-source-ancestry/benchmark.py new file mode 100644 index 0000000000..ed186dc1e2 --- /dev/null +++ b/research/joint-source-ancestry/benchmark.py @@ -0,0 +1,120 @@ +"""Time complete publication preflights; write temporary files only outside Git.""" +import argparse +import hashlib +import json +import os +import platform +import pstats +import statistics +import subprocess +import sys +import tempfile +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +PACKAGE = Path(__file__).resolve().parent +FIXTURE = 'research/e8-d0-root-swaps' +sys.path.insert(0, str(ROOT / 'scripts')) +from research_package import git_source_env, require + + +def run(argv): + result = subprocess.run(argv, cwd=ROOT, capture_output=True, timeout=60, + env=git_source_env(), check=True) + return result.stdout + + +def baseline(directory, base): + raw = run(['git', 'show', base + ':scripts/check_research_commit.py']) + pins = json.loads((PACKAGE / 'SOURCES.json').read_text())['files'] + expected = next(row['sha256'] for row in pins + if row['path'] == 'scripts/check_research_commit.py') + require(hashlib.sha256(raw).hexdigest() == expected, 'baseline source differs') + source = directory / 'baseline-source.py' + source.write_bytes(raw) + wrapper = directory / 'baseline-cli.py' + # Preserve the original CLI's ROOT and sibling imports while freezing only + # the changed module. Both sides use the same current source-reader module. + wrapper.write_text('import sys\nfrom pathlib import Path\n' + f'sys.path.insert(0, {str(ROOT / "scripts")!r})\n' + f'exec(compile(Path({str(source)!r}).read_bytes(), ' + f'{str(ROOT / "scripts/check_research_commit.py")!r}, "exec"), ' + f'{{"__name__": "__main__", "__file__": ' + f'{str(ROOT / "scripts/check_research_commit.py")!r}}})\n') + return wrapper, hashlib.sha256(raw).hexdigest() + + +def profile(directory, scripts, base): + summaries = {} + for side, script in scripts.items(): + destination = directory / (side + '.profile') + run([sys.executable, '-B', '-m', 'cProfile', '-o', str(destination), + str(script), '--package', FIXTURE, '--ref', base, '--check-source-history']) + summaries[side] = {} + for (filename, _, name), (calls, _, _, cumulative, _) in pstats.Stats(str(destination)).stats.items(): + selected = (((filename.endswith('check_research_commit.py') + or filename.endswith('preflight.py')) + and name in ('main', 'check', 'source_history', 'history_git')) + or (filename.endswith('subprocess.py') and name == 'run')) + if selected: + key = Path(filename).name + ':' + name + summaries[side][key] = {'calls': calls, 'cumulative_seconds': cumulative} + return summaries + + +def measure(directory, pairs, base): + old, old_hash = baseline(directory, base) + changed = PACKAGE / 'preflight.py' + scripts = {'baseline': old, 'changed': changed} + profiles = profile(directory, scripts, base) + rows, expected = [], None + for pair in range(pairs): + order = ('baseline', 'changed') if pair % 2 == 0 else ('changed', 'baseline') + for side in order: + start = time.perf_counter() + output = run([sys.executable, '-B', str(scripts[side]), '--package', + FIXTURE, '--ref', base, '--check-source-history']) + elapsed = time.perf_counter() - start + expected = output if expected is None else expected + require(output == expected, 'complete preflight receipts differ') + rows.append({'pair': pair, 'side': side, 'seconds': elapsed, 'returncode': 0}) + summary = {} + for side in scripts: + times = [row['seconds'] for row in rows if row['side'] == side] + summary[side] = {'median_seconds': statistics.median(times), + 'minimum_seconds': min(times), 'maximum_seconds': max(times)} + return {'schema_version': 1, 'base_commit': base, 'fixture': FIXTURE, + 'pairs': pairs, 'samples': rows, 'summary': summary, 'failures': 0, + 'untimed_diagnostic_profiles': profiles, + 'receipt': json.loads(expected.decode().split(' ', 1)[1]), + 'receipt_sha256': hashlib.sha256(expected).hexdigest(), + 'baseline_source_sha256': old_hash, + 'changed_source_sha256': hashlib.sha256(changed.read_bytes()).hexdigest(), + 'shared_preflight_sha256': hashlib.sha256( + (ROOT / 'scripts/check_research_commit.py').read_bytes()).hexdigest(), + 'shared_source_reader_sha256': hashlib.sha256( + (ROOT / 'scripts/research_package.py').read_bytes()).hexdigest(), + 'python': sys.version, 'platform': platform.platform(), + 'logical_cpus': os.cpu_count(), 'load_average': os.getloadavg() + if hasattr(os, 'getloadavg') else None, + 'git': run(['git', '--version']).decode().strip()} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--scratch', type=Path, required=True) + parser.add_argument('--pairs', type=int, default=7) + args = parser.parse_args() + require(5 <= args.pairs <= 20, 'use between five and twenty timing pairs') + scratch = args.scratch.resolve() + require(scratch.is_dir() and not scratch.is_relative_to(ROOT), + 'scratch must be an existing directory outside the checkout') + base = json.loads((PACKAGE / 'SOURCES.json').read_text())['base_commit'] + with tempfile.TemporaryDirectory(dir=scratch) as directory: + result = measure(Path(directory), args.pairs, base) + print(json.dumps(result, indent=2, sort_keys=True)) + + +if __name__ == '__main__': + main() diff --git a/research/joint-source-ancestry/preflight.py b/research/joint-source-ancestry/preflight.py new file mode 100644 index 0000000000..39b60358df --- /dev/null +++ b/research/joint-source-ancestry/preflight.py @@ -0,0 +1,132 @@ +"""Opt-in publication preflight with bounded joint source ancestry. + +The byte/mode preflight retains ywz65's implementation. Source-history policy +follows Nick Petalas's existing preflight and the retained immutable-reader +contributors. New batching and adapter by Nick Petalas. Apache-2.0. +""" +import argparse +import json +import os +import re +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / 'scripts')) +from check_research_commit import check as byte_check +from check_research_commit import MAX_SOURCE_COMMITS, MAX_SOURCE_METADATA_BYTES, MAX_SOURCE_ROWS +from research_package import canonical_relative_path, git_source_env, require, source_pin + + +def history_git(root, *args, input=None): + env = git_source_env() + env['GIT_GRAFT_FILE'] = os.devnull + env.pop('GIT_SHALLOW_FILE', None) + return subprocess.run(['git', '--no-replace-objects', *args], cwd=root, env=env, input=input, + capture_output=True, check=False, timeout=30) + + +def joint_ancestors(root, pins, commit): + """Check exact commit types and share one walk across bounded source pins. + + A nonempty difference takes the original per-pin diagnostic path. Errors + and malformed batch responses fail closed, rather than becoming a hit. + """ + payload = ''.join(pin + '\n' for pin in pins).encode('ascii') + types = history_git(root, 'cat-file', '--batch-check=%(objectname) %(objecttype)', + input=payload) + require(types.returncode == 0, 'Git source commit check failed: ' + + types.stderr.decode('utf-8', errors='replace').strip()) + rows = types.stdout.splitlines() + require(len(rows) == len(pins), 'incomplete Git source commit response') + for pin, row in zip(pins, rows): + if row != (pin + ' commit').encode('ascii'): + require(row in [(pin + ' ' + kind).encode('ascii') + for kind in ('missing', 'blob', 'tree', 'tag')], + 'invalid Git source commit response') + return False + difference = history_git(root, 'rev-list', '--max-count=1', *pins, '--not', commit) + require(difference.returncode == 0, 'Git ancestry check failed: ' + + difference.stderr.decode('utf-8', errors='replace').strip()) + require(not difference.stdout or re.fullmatch(rb'[0-9a-f]{40,64}\n', difference.stdout), + 'invalid Git ancestry response') + return not difference.stdout + + +def publication_sources(root, package, commit): + """Read bounded, immutable metadata without local replacements or grafts.""" + spec = commit + ':' + package + '/SOURCES.json' + size = history_git(root, 'cat-file', '-s', spec) + require(size.returncode == 0 and size.stdout.strip().isdigit(), 'missing publication SOURCES.json') + require(int(size.stdout) <= MAX_SOURCE_METADATA_BYTES, 'source metadata byte limit exceeded') + payload = history_git(root, 'cat-file', 'blob', spec) + require(payload.returncode == 0 and len(payload.stdout) == int(size.stdout), + 'incomplete publication SOURCES.json') + return json.loads(payload.stdout) + + +def source_history(root, package, commit): + """Check committed pins against raw publication ancestry, not local refs. + + This is declared history availability, not full SOURCES.json validation + or discovery of imports, archive references or runtime dependencies. + """ + sources = publication_sources(root, package, commit) + require(isinstance(sources, dict), 'SOURCES.json must be an object') + base = sources.get('base_commit') + require(isinstance(base, str) and re.fullmatch(r'[0-9a-f]{40,64}', base), + 'SOURCES.json base_commit must be an exact commit ID') + rows = sources.get('files') + require(isinstance(rows, list), 'SOURCES.json files must be a list') + require(len(rows) <= MAX_SOURCE_ROWS, 'source row limit exceeded') + commits = {base} + for row in rows: + require(isinstance(row, dict) and isinstance(row.get('path'), str), + 'SOURCES.json files rows need a string path') + canonical_relative_path(row['path']) + commits.add(source_pin(row, base).split(':', 1)[0]) + require(len(commits) <= MAX_SOURCE_COMMITS, 'source commit limit exceeded') + shallow = history_git(root, 'rev-parse', '--is-shallow-repository') + require(shallow.returncode == 0, 'cannot inspect Git history') + require(shallow.stdout.strip() == b'false', 'source-history check requires full Git history') + pins = sorted(commits) + if len(pins) > 1 and joint_ancestors(root, pins, commit): + return pins + for pin in pins: + resolved = history_git(root, 'rev-parse', '--verify', '--end-of-options', pin + '^{commit}') + require(resolved.returncode == 0 and resolved.stdout.strip() == pin.encode('ascii'), + 'unavailable source commit: ' + pin) + ancestor = history_git(root, 'merge-base', '--is-ancestor', pin, commit) + require(ancestor.returncode in (0, 1), + 'Git ancestry check failed: ' + ancestor.stderr.decode('utf-8', errors='replace').strip()) + require(ancestor.returncode == 0, + 'source pin is not an ancestor of publication ' + commit + ': ' + pin + + '; a separate cached branch/object is not publication history') + return pins + + + +def check(root, package, ref='HEAD', check_source_history=False, check_modes=False): + result = byte_check(root, package, ref, check_source_history=False, check_modes=check_modes) + if check_source_history: + result['source_history_commits'] = source_history(root, package, result['commit']) + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--package', required=True) + parser.add_argument('--ref', default='HEAD') + parser.add_argument('--check-source-history', action='store_true') + parser.add_argument('--check-modes', action='store_true') + args = parser.parse_args() + try: + result = check(ROOT, args.package, args.ref, args.check_source_history, args.check_modes) + except (OSError, ValueError, TypeError, subprocess.SubprocessError) as exc: + parser.exit(1, str(exc) + '\n') + print('PASS_RESEARCH_COMMIT_BYTES ' + json.dumps(result)) + + +if __name__ == '__main__': + main() diff --git a/research/joint-source-ancestry/test_history.py b/research/joint-source-ancestry/test_history.py new file mode 100644 index 0000000000..c04f7b16e4 --- /dev/null +++ b/research/joint-source-ancestry/test_history.py @@ -0,0 +1,117 @@ +"""The original source-history contract exercised through the opt-in adapter.""" +import subprocess +import sys +import unittest +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / 'tests')) +import test_research_source_history as inherited +import preflight as checker + +# The inherited fixture methods resolve this module variable at call time. +# The original test/source files remain untouched, including their CLI controls. +inherited.checker = checker + + +class JointSourceHistoryTests(inherited.SourceHistoryTests): + def test_real_cli_is_read_only_and_reports_no_pass_on_history_failure(self): + super().test_real_cli_is_read_only_and_reports_no_pass_on_history_failure() + code = ('import sys; from pathlib import Path; ' + f'sys.path.insert(0, {str(Path(__file__).resolve().parent)!r}); ' + f'import preflight; preflight.ROOT = Path({str(self.root)!r}); ' + 'preflight.main()') + command = [sys.executable, '-W', 'error', '-B'] + if sys.flags.optimize: + command.append('-O') + command += ['-c', code, '--package', self.package, '--check-source-history'] + before = self.git('rev-parse', 'HEAD'), self.git('ls-files', '--stage', '-z') + bad = subprocess.run(command, cwd=self.root, capture_output=True, timeout=30) + self.assertEqual(bad.returncode, 1, bad.stderr) + self.assertIn(b'not an ancestor', bad.stderr) + self.assertNotIn(b'PASS_', bad.stdout) + self.assertEqual(before, (self.git('rev-parse', 'HEAD'), + self.git('ls-files', '--stage', '-z'))) + self.publish([self.row()]) + good = subprocess.run(command, cwd=self.root, capture_output=True, timeout=30) + self.assertEqual(good.returncode, 0, good.stderr) + self.assertIn(b'PASS_RESEARCH_COMMIT_BYTES', good.stdout) + + def test_joint_walk_accepts_merged_incomparable_sources(self): + tree = self.git('rev-parse', self.base + '^{tree}').strip() + left = self.git('commit-tree', tree, '-p', self.base, data='left\n').strip() + right = self.git('commit-tree', tree, '-p', self.base, data='right\n').strip() + head = self.publish([self.row(left), self.row(right), self.row(left)]) + tree = self.git('rev-parse', head + '^{tree}').strip() + merged = self.git('commit-tree', tree, '-p', head, '-p', left, '-p', right, + data='merged sources\n').strip() + with patch.object(checker, 'history_git', wraps=checker.history_git) as calls: + result = self.check(ref=merged) + self.assertEqual(result['source_history_commits'], sorted([self.base, left, right])) + commands = [call.args[1] for call in calls.call_args_list] + self.assertEqual(commands.count('rev-list'), 1) + self.assertNotIn('merge-base', commands) + self.assertNotIn('rev-parse', commands[3:]) + self.assertEqual(len(commands), 5) + + def test_joint_walk_preserves_first_foreign_pin_diagnostic(self): + tree = self.git('rev-parse', self.base + '^{tree}').strip() + foreign = [self.git('commit-tree', tree, '-p', self.base, + data=f'foreign {index}\n').strip() for index in range(3)] + self.publish([self.row(pin) for pin in foreign]) + with self.assertRaisesRegex(ValueError, 'not an ancestor.*' + min(foreign)): + self.check() + + def test_joint_type_fallback_preserves_earlier_ancestry_error(self): + tree = self.git('rev-parse', self.base + '^{tree}').strip() + foreign = self.git('commit-tree', tree, '-p', self.base, data='foreign\n').strip() + self.publish([self.row(foreign), self.row('f' * 40)]) + with self.assertRaisesRegex(ValueError, 'not an ancestor.*' + foreign): + self.check() + + def test_joint_batch_rejects_tags_trees_blobs_and_missing_objects(self): + tree = self.git('rev-parse', self.base + '^{tree}').strip() + blob = self.git('rev-parse', self.base + ':research/fixture/input.txt').strip() + tag = self.git('mktag', data=f'object {self.base}\ntype commit\ntag source\n' + 'tagger Fixture 1 +0000\n\ntag\n').strip() + for pin in (tag, tree, blob, '1' * 40): + with self.subTest(pin=pin): + self.publish([self.row(pin)]) + with self.assertRaisesRegex(ValueError, 'unavailable source commit.*' + pin): + self.check() + + def test_joint_git_failures_and_malformed_responses_fail_closed(self): + self.git('commit', '-qm', 'second source', '--allow-empty') + second = self.git('rev-parse', 'HEAD').strip() + self.publish([self.row(second)]) + original = checker.history_git + for phase, code, output in ( + ('batch', 128, b''), ('batch', 0, b''), + ('batch', 0, b'forged commit\n' * 2), + ('walk', 128, b''), ('walk', 0, b'not a commit\n')): + def broken(root, *args, input=None): + selected = (args[0] == 'rev-list' if phase == 'walk' + else args[0] == 'cat-file' and args[1].startswith('--batch-check')) + if selected: + return subprocess.CompletedProcess(args, code, output, b'fatal: injected failure') + return original(root, *args, input=input) + + with self.subTest(phase=phase, code=code, output=output): + with patch.object(checker, 'history_git', side_effect=broken): + with self.assertRaises(ValueError): + self.check() + + def timeout(root, *args, input=None): + if args[0] == 'rev-list': + raise subprocess.TimeoutExpired('git rev-list', 30) + return original(root, *args, input=input) + + with patch.object(checker, 'history_git', side_effect=timeout): + with self.assertRaises(subprocess.TimeoutExpired): + self.check() + + + +if __name__ == '__main__': + unittest.main() diff --git a/research/joint-source-ancestry/verify.py b/research/joint-source-ancestry/verify.py new file mode 100644 index 0000000000..091ea30e84 --- /dev/null +++ b/research/joint-source-ancestry/verify.py @@ -0,0 +1,40 @@ +"""Check the complete receipt, source-bound measurements and rejection controls.""" +import hashlib +import json +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +PACKAGE = Path(__file__).resolve().parent +sys.path.insert(0, str(ROOT / 'scripts')) +import preflight as checker +from research_package import require + + +def main(): + evidence = json.loads((PACKAGE / 'BENCHMARK.json').read_text()) + source = PACKAGE / 'preflight.py' + require(hashlib.sha256(source.read_bytes()).hexdigest() + == evidence['changed_source_sha256'], 'measured implementation changed') + samples = evidence['samples'] + require(evidence['failures'] == 0 and evidence['pairs'] >= 5, + 'measurement must include at least five successful pairs') + require(len(samples) == 2 * evidence['pairs'] + and all(row['returncode'] == 0 for row in samples), 'incomplete measurements') + actual = checker.check(ROOT, evidence['fixture'], evidence['base_commit'], True) + require(actual == evidence['receipt'], 'complete publication receipt changed') + require(actual['files'] == 14 and len(actual['source_history_commits']) == 5, + 'real fixture coverage changed') + command = [sys.executable, '-W', 'error', '-B'] + if sys.flags.optimize: + command.append('-O') + subprocess.run([*command, str(PACKAGE / 'test_history.py'), '-v'], + cwd=ROOT, timeout=120, check=True) + print('PASS_JOINT_SOURCE_ANCESTRY ' + json.dumps( + {'fixture_files': actual['files'], 'source_commits': 5, 'tests': 18, + 'claim_type': 'scoped algorithmic equivalence and empirical timing'}, sort_keys=True)) + + +if __name__ == '__main__': + main()