diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dac53fa4..9a244bd7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,6 +75,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Check repository release policy + run: cargo run -p xtask --locked -- release-gate - name: Verify crates.io package run: cargo publish --dry-run --locked - name: Enforce crates.io package size limit diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index 330237b3..a633567e 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -1173,6 +1173,12 @@ fn check_workflow_policy( ); } let ci_publish = yaml_path(ci, &["jobs", "publish-dry-run"]).unwrap_or(&YamlValue::Null); + if !yaml_contains_string(ci_publish, "cargo run -p xtask --locked -- release-gate") { + issues.push( + "FAIL: repository-release-gate-missing: CI must check repository release policy." + .to_string(), + ); + } if !yaml_contains_string(ci_publish, "cargo package --locked --no-verify") || !yaml_contains_string(ci_publish, "verify-crate-size") { @@ -2837,6 +2843,31 @@ mod tests { ); } + #[test] + fn workflow_policy_rejects_missing_repository_release_gate() { + let mut ci = valid_ci_workflow(); + let steps = ci["jobs"]["publish-dry-run"]["steps"] + .as_sequence_mut() + .unwrap(); + steps.retain(|step| { + step.get("run").and_then(YamlValue::as_str) + != Some("cargo run -p xtask --locked -- release-gate") + }); + let mut issues = Vec::new(); + check_workflow_policy( + &ci, + &valid_native_workflow(), + &yaml_serde::from_str(&valid_release_workflow_text()).unwrap(), + &mut issues, + ); + assert!( + issues + .iter() + .any(|issue| issue.contains("repository-release-gate-missing")), + "{issues:?}" + ); + } + #[test] fn workflow_policy_rejects_missing_crate_size_gates() { let broken_release = valid_release_workflow_text() diff --git a/docs/release.md b/docs/release.md index 9bd55258..6e7d1008 100644 --- a/docs/release.md +++ b/docs/release.md @@ -37,6 +37,7 @@ Pull requests targeting `main` and pushes to `main` run: - platform-aligned workspace tests on macOS ARM64 and Windows x86_64 - `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings` - Rust advisory scanning with `cargo audit` +- `cargo run -p xtask --locked -- release-gate`, before the package dry-run, to reject repository-policy violations before release. - `cargo publish --dry-run --locked` - An exact-byte check that the generated `.crate` does not exceed crates.io's 10 MiB upload limit. - `cargo package -p k-wiki --locked --no-verify` and the isolated Knowledge Wiki smoke. The diff --git a/docs/testing/graph-disk-cleanup-evidence/README.md b/docs/testing/graph-disk-cleanup-evidence/README.md new file mode 100644 index 00000000..f2a49ec0 --- /dev/null +++ b/docs/testing/graph-disk-cleanup-evidence/README.md @@ -0,0 +1,58 @@ +# Historical Python evidence helpers + +[COD-99](https://linear.app/codebasegraph/issue/COD-99) archives the four Python +helpers from commit `7038e66ff8271ffddc511f879cf8d28353a09b2d`. The user approved +archiving them on 2026-10-27. They are retained historical evidence, not maintained +implementation or supported verification tooling. The Rust-only repository policy +is unchanged. + +`historical-python-helpers.tar.gz` preserves each file's original bytes and repository-relative +path. [The manifest](./historical-python-helpers.json) records the source commit, +Git blob IDs, byte lengths, SHA-256 digests and exact archive creation command. +The archive SHA-256 is `032ac4ef9cfddf40dc0540a913f71c34de4901a4a7d61d035141cb96832e7519`. Two independent `git archive` +invocations produced identical bytes; each regular member matched its original +file byte-for-byte before removal. + +Historical reports, source manifests, test outputs and their hashes are unchanged. +Their `.py` paths still identify their original inputs, now stored in this archive. +The archive does not turn earlier partial evidence into current qualification. +The canonical DC-S1–DC-S5 contract remains +`knowledge:architecture/graph-disk-cleanup-contract`. + +## Reconstruct historical inputs + +Use a separate temporary checkout of the revision named by the report you are +reproducing. Do not extract into the current checkout: loose Python source would +correctly fail the repository gate again. The archive contains only the four +helpers, not their historical environment or every report prerequisite. + +From this checkout, inspect and verify the archive before extracting: + +```bash +shasum -a 256 docs/testing/graph-disk-cleanup-evidence/historical-python-helpers.tar.gz +tar -tzf docs/testing/graph-disk-cleanup-evidence/historical-python-helpers.tar.gz +``` + +For an unused temporary directory, reconstruct the archive's source revision: + +```bash +git worktree add --detach /private/tmp/codebasegraph-historical-7038e66 \ + 7038e66ff8271ffddc511f879cf8d28353a09b2d +tar -xzf docs/testing/graph-disk-cleanup-evidence/historical-python-helpers.tar.gz \ + -C /private/tmp/codebasegraph-historical-7038e66 +``` + +All file members are under `docs/testing/graph-disk-cleanup-evidence/`, so sibling +imports and original relative paths are preserved. Verify their hashes against +the manifest. Follow the selected historical report's commands and prerequisites; +this archival change does not claim that historical helper tests were rerun or +that all inputs can be reproduced from the archive alone. + +## Release limitation + +The original [failed job](https://github.com/rabii-chaarani/codebaseGraph/actions/runs/40453997235/job/122023180345) +checks immutable source `7038e66`. A fix on a later commit cannot make that source +pass by rerunning it. Corrected publication needs a trusted replacement release +PR and its own successful exact-SHA CI under [the release process](../../release.md). +Version selection, replacement of PR #156, tag/label changes and publication are +not approved by the archival decision. diff --git a/docs/testing/graph-disk-cleanup-evidence/audit.py b/docs/testing/graph-disk-cleanup-evidence/audit.py deleted file mode 100644 index 18a90517..00000000 --- a/docs/testing/graph-disk-cleanup-evidence/audit.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Audit archived executions without treating them as aggregate acceptance. - -Run with Python 3.8+: python3 docs/testing/graph-disk-cleanup-evidence/audit.py -This checks evidence integrity, not implementation correctness or platform gates. -""" - -import hashlib -import gzip -import json -from pathlib import Path -import xml.etree.ElementTree as ET - - -def sha256(path): - return hashlib.sha256(path.read_bytes()).hexdigest() - - -def passed(case): - return not any(case.find(tag) is not None for tag in ("failure", "error", "skipped")) - - -def audit(root=None): - root = Path(root) if root is not None else Path(__file__).resolve().parent - index = json.loads((root.parent / "graph-disk-cleanup-evidence.json").read_text()) - reports = json.loads((root / "foundation-reports.json").read_text())["reports"] - report_ids = set() - executions = 0 - cases_by_report = {} - for report in reports: - report_id = report["id"] - assert report_id not in report_ids, report_id - report_ids.add(report_id) - path = root / report["report"] - assert sha256(path) == report["sha256"], path - xml = ET.parse(path).getroot() - cases = xml.findall(".//testcase") - cases_by_report[report_id] = cases - actual = { - "cases": len(cases), - "failed": sum(bool(c.findall("failure") + c.findall("error")) for c in cases), - "skipped": sum(c.find("skipped") is not None for c in cases), - } - assert actual == report["execution_counts"], (report_id, actual) - names = [ - {"suite": suite.get("name"), "name": case.get("name"), "classname": case.get("classname")} - for suite in xml.findall(".//testsuite") - for case in suite.findall("testcase") - ] - assert names == report["case_names"], report_id - if "source_manifest" in report: - assert sha256(root / report["source_manifest"]) == report["source_manifest_sha256"] - executions += actual["cases"] - - ids = [row["id"] for row in index["traceability"]] - assert len(ids) == len(set(ids)), "duplicate work identity" - expected = { - "DC-S%d-%s%d" % (step, kind, number) - for step, requirements, acceptance in [(1, 8, 4), (2, 8, 4), (3, 8, 5), (4, 8, 4), (5, 9, 4)] - for kind, count in [("R", requirements), ("A", acceptance)] - for number in range(1, count + 1) - } - assert set(ids) == expected, (expected - set(ids), set(ids) - expected) - by_id = {report["id"]: report for report in reports} - for row in index["traceability"]: - for evidence in row["executed_evidence"]: - report = by_id[evidence["report_id"]] - names = {case["name"] for case in report["case_names"]} - assert evidence["tests"] and set(evidence["tests"]) <= names, row["id"] - if evidence["result"] == "passed": - for name in evidence["tests"]: - referenced = [case for case in cases_by_report[evidence["report_id"]] if case.get("name") == name] - assert referenced and all(passed(case) for case in referenced), (row["id"], name) - msrv = json.loads((root / "msrv-readiness.json").read_text()) - for attempt in msrv["attempts"]: - assert sha256(root / attempt["stderr"]) == attempt["stderr_sha256"] - assert attempt["actual_exit_code"] == 101 - assert msrv["baseline_manifest_lock_diff"] == "", "dependency baseline changed" - reviews = json.loads((root / "review-outcomes.json").read_text()) - for artifact in reviews["s3_expanded_partial"]["executed_failure_artifacts"]: - assert sha256(root / artifact["path"]) == artifact["sha256"] - if artifact.get("encoding") == "gzip": - decoded = gzip.decompress((root / artifact["path"]).read_bytes()) - assert hashlib.sha256(decoded).hexdigest() == artifact["decoded_sha256"] - else: - assert artifact.get("encoding") is None, "unsupported evidence encoding" - scheduler = reviews["s4_scheduler_primitive"]["actual_junit"] - assert sha256(root / scheduler["path"]) == scheduler["sha256"] - external = json.loads((root.parent / index["s5_external_execution"]).read_text()) - assert sha256(root / external["junit"]) == external["junit_sha256"] - external_xml = ET.parse(root / external["junit"]).getroot() - external_cases = external_xml.findall(".//testcase") - assert external_xml.get("uuid") == external["run_id"], "external run identity mismatch" - actual_names = sorted((case.get("name"), case.get("classname")) for case in external_cases) - expected_names = sorted((case["name"], case["classname"]) for case in external["expected_test_cases"]) - assert expected_names == sorted((name, "codebase-graph::storage_maintenance") for name in ( - "health_preserves_legacy_fields_and_marks_unobserved_maintenance_unknown", - "nonboolean_quiescence_confirmation_is_rejected_before_repository_effects", - )), "unexpected external test contract" - assert actual_names == expected_names, "external case identity mismatch" - actual_results = { - "passed": sum(passed(case) for case in external_cases), - "failed": sum(bool(case.findall("failure") + case.findall("error")) for case in external_cases), - "skipped": sum(case.find("skipped") is not None for case in external_cases), - } - assert actual_results == external["actual_result"], "external result mismatch" - assert actual_results == {"passed": 2, "failed": 0, "skipped": 0} - assert "src/api/normalization.rs" in external["source_sha256"] - for path, expected_hash in external["capture_files_sha256"].items(): - assert sha256(root / path) == expected_hash, path - capture = root / "s5-fresh-capture" - source_before = json.loads((capture / "source-before.json").read_text()) - source_after_build = json.loads((capture / "source-after-build.json").read_text()) - source_after_execution = json.loads((capture / "source-after-execution.json").read_text()) - assert source_before == source_after_build == source_after_execution == external["source_sha256"] - binary_before = json.loads((capture / "binary-before.json").read_text()) - binary_after = json.loads((capture / "binary-after.json").read_text()) - assert binary_before == binary_after == external["binary_sha256"] - print("Evidence integrity passed: %d traceability rows, %d reports, %d executions." % (len(ids), len(reports), executions)) - print("Execution counts are not unique scenarios. This audit does not establish aggregate acceptance.") - print("Separate narrow S5 health/request report: 2 passed; final integration remains pending.") - - -if __name__ == "__main__": - audit() diff --git a/docs/testing/graph-disk-cleanup-evidence/historical-python-helpers.json b/docs/testing/graph-disk-cleanup-evidence/historical-python-helpers.json new file mode 100644 index 00000000..0f7717a1 --- /dev/null +++ b/docs/testing/graph-disk-cleanup-evidence/historical-python-helpers.json @@ -0,0 +1,48 @@ +{ + "schema_version": 1, + "work_item": "COD-99", + "classification": "historical evidence only; not maintained implementation or supported tooling", + "source_commit": "7038e66ff8271ffddc511f879cf8d28353a09b2d", + "approval": { + "date": "2026-10-27", + "user_decision": "i approve archiving the python helpers", + "scope": "Lossless historical archival of the four Python helpers; no policy exemption or release publication approval." + }, + "archive": { + "path": "docs/testing/graph-disk-cleanup-evidence/historical-python-helpers.tar.gz", + "sha256": "032ac4ef9cfddf40dc0540a913f71c34de4901a4a7d61d035141cb96832e7519", + "size_bytes": 15810, + "member_paths_relative_to": "repository root" + }, + "creation_command": "git archive --format=tar.gz --output=docs/testing/graph-disk-cleanup-evidence/historical-python-helpers.tar.gz 7038e66ff8271ffddc511f879cf8d28353a09b2d -- docs/testing/graph-disk-cleanup-evidence/audit.py docs/testing/graph-disk-cleanup-evidence/qualification.py docs/testing/graph-disk-cleanup-evidence/test_audit.py docs/testing/graph-disk-cleanup-evidence/test_qualification.py", + "files": [ + { + "path": "docs/testing/graph-disk-cleanup-evidence/audit.py", + "git_blob_sha1": "18a90517c8f2954dea0d57df412faf119186f266", + "sha256": "316630c38e1638370b8c1873174d4311cc32ae4d0d9bcbbf0045e1083ccfb518", + "size_bytes": 6707, + "git_mode": "100644" + }, + { + "path": "docs/testing/graph-disk-cleanup-evidence/qualification.py", + "git_blob_sha1": "1384a3298cedeb0f3d0022938e885b50875cce04", + "sha256": "264753ae10581691d0dc1c7591acab9601b29ff7c9f95df8d04ed56b56509e3b", + "size_bytes": 26627, + "git_mode": "100644" + }, + { + "path": "docs/testing/graph-disk-cleanup-evidence/test_audit.py", + "git_blob_sha1": "f934f32d3a7f5d583c038bf4c431a48a674573e9", + "sha256": "b317ac2a5f4ab7ec3c52e98169e37090d65c9a7a92d397bd898962516f6f545c", + "size_bytes": 5632, + "git_mode": "100644" + }, + { + "path": "docs/testing/graph-disk-cleanup-evidence/test_qualification.py", + "git_blob_sha1": "4e20628898f3e019e7e45bb956d1669b960a1e53", + "sha256": "4fa5adbdff51ec53085ae307444ca4800a557726b95e18cacfc42091c0c57ef3", + "size_bytes": 24274, + "git_mode": "100644" + } + ] +} diff --git a/docs/testing/graph-disk-cleanup-evidence/historical-python-helpers.tar.gz b/docs/testing/graph-disk-cleanup-evidence/historical-python-helpers.tar.gz new file mode 100644 index 00000000..05ec406e Binary files /dev/null and b/docs/testing/graph-disk-cleanup-evidence/historical-python-helpers.tar.gz differ diff --git a/docs/testing/graph-disk-cleanup-evidence/qualification.py b/docs/testing/graph-disk-cleanup-evidence/qualification.py deleted file mode 100644 index 1384a329..00000000 --- a/docs/testing/graph-disk-cleanup-evidence/qualification.py +++ /dev/null @@ -1,449 +0,0 @@ -"""Fresh DC-S5 suite execution capture; never promotes aggregate/platform gates. - -Requires Python 3.9+. Run `python3 qualification.py capture --repository REPO --output NEW_DIRECTORY` -only while holding the coordinator's Cargo target lease. `verify DIRECTORY` -checks the archived capture without rebuilding or reading the original checkout. -""" - -import argparse -import hashlib -import json -import os -from pathlib import Path -import platform -import subprocess -import xml.etree.ElementTree as ET - -SCHEMA = 3 -BINARY = "codebase-graph::storage_maintenance" -EXPECTED = { - "health_preserves_legacy_fields_and_marks_unobserved_maintenance_unknown", - "nonboolean_quiescence_confirmation_is_rejected_before_repository_effects", - "repeated_health_entries_preserve_source_and_foreign_sentinels", -} -LEGACY_LIMITATIONS = [ - "Narrow external storage_maintenance execution only; no collector qualification.", - "Native Windows/Linux, MSRV, full suite and aggregate closure remain pending.", - "Source identity covers repository build inputs; dependency provenance uses Cargo.lock and metadata.", -] -LIMITATIONS = [ - "Captured storage_maintenance suite execution only; acceptance remains criterion-specific.", - "Native Windows/Linux, MSRV, workspace full suite and aggregate closure remain pending.", - "Source identity covers repository build inputs; dependency provenance uses Cargo.lock and metadata.", -] -WORKLOAD_CASES = { - "twenty_public_builds_match_clean_control_and_preserve_unchanged_artifact", - "fixture::workload::tests::schedule_covers_twenty_updates_and_each_required_interruption_boundary", - "fixture::workload::tests::independent_control_matches_twenty_source_updates_without_rewriting_stable_input", - "fixture::workload::tests::search_oracle_omits_only_incarnation_ids_and_keeps_rank_spans_and_content", - "fixture::workload::tests::oracle_rejects_missing_response_fields_and_preserves_context_multiplicity", -} -PROCESS_CASE = "native_phase_owner_death_preserves_prior_graph_and_retries_without_source_edit" - - - -def require(condition, message): - if not condition: - raise ValueError(message) - - -def sha256(path): - digest = hashlib.sha256() - with Path(path).open("rb") as stream: - for block in iter(lambda: stream.read(1024 * 1024), b""): - digest.update(block) - return digest.hexdigest() - - -def write_json(path, value): - Path(path).write_text(json.dumps(value, sort_keys=True, indent=2) + "\n") - - -def read_json(path): - return json.loads(Path(path).read_text()) - - -def source_manifest(repository): - repository = Path(repository).resolve() - # Include untracked new inputs and detect additions/deletions between phases. - names = subprocess.check_output([ - "git", "ls-files", "-z", "--cached", "--others", "--exclude-standard", - ], cwd=repository).decode().split("\0") - roots = {"src", "tests", "crates", "vendor", "assets", ".cargo", ".config"} - singles = {"Cargo.toml", "Cargo.lock", "build.rs", "rust-toolchain", "rust-toolchain.toml"} - result = {} - for name in sorted(set(names) - {""}): - if name.split("/")[0] not in roots and name not in singles: - continue - path = repository / name - require(not path.is_symlink() and path.resolve().is_relative_to(repository), - "build input is a symlink or escapes repository: " + name) - result[name] = sha256(path) # Missing tracked input fails closed. - require({"Cargo.toml", "Cargo.lock", "build.rs"} <= result.keys(), "missing core build inputs") - return result - - -def run(command, repository, output, label, env=None): - with (output / (label + ".stdout")).open("wb") as stdout: - with (output / (label + ".stderr")).open("wb") as stderr: - result = subprocess.run(command, cwd=repository, stdout=stdout, stderr=stderr, check=False, env=env) - return {"argv": command, "exit_code": result.returncode} - - -def binary_paths(metadata): - binaries = metadata["rust-binaries"] - require(set(binaries) == {BINARY}, "capture must select exactly the storage_maintenance binary") - paths = {str(Path(row["binary-path"]).resolve()) for row in binaries.values()} - # nextest can launch non-test helper executables too: freeze those as inputs. - build = metadata["rust-build-meta"] - for rows in build.get("non-test-binaries", {}).values(): - for row in rows: - paths.add(str((Path(build["target-directory"]) / row["path"]).resolve())) - return sorted(paths) - - -def junit_result(path, case_names=None): - xml = ET.parse(path).getroot() - cases = xml.findall(".//testcase") - identities = [(case.get("classname"), case.get("name")) for case in cases] - expected = {(BINARY, name) for name in (EXPECTED if case_names is None else case_names)} - require(len(identities) == len(expected) and set(identities) == expected, - "unexpected, missing or duplicate external testcase") - require(all(not any(case.find(tag) is not None for tag in ("failure", "error", "skipped", "rerunFailure", "rerunError")) - for case in cases), "external testcase did not pass on its first execution") - return {"run_id": xml.get("uuid"), "passed": len(cases), "failed": 0, "skipped": 0} - - -def verify(output): - output = Path(output) - record = read_json(output / "capture.json") - schema = record["schema"] - require(schema in (1, 2, SCHEMA), "unsupported capture schema") - require(record["scope"] == ("storage_maintenance_suite" if schema == SCHEMA else "narrow_external_contract"), "capture scope cannot claim qualification") - require(record["qualification"] == {"aggregate": "pending", "windows": "pending", "linux": "pending"}, - "narrow capture cannot promote qualification gates") - for name, digest in record["artifacts"].items(): - path = Path(name) - require(not path.is_absolute() and ".." not in path.parts, "capture path escapes archive") - require(not (output / path).is_symlink() and (output / path).resolve().is_relative_to(output.resolve()), - "capture artifact escapes archive") - require(sha256(output / path) == digest, "artifact changed: " + name) - required = {"source-before.json", "source-after-build.json", "source-after-execution.json", - "binary-before.json", "binary-after.json", "binary-list.stdout", "binary-list.stderr", - "cargo-metadata.stdout", "cargo-metadata.stderr", "execution.stdout", "execution.stderr", - "junit.xml", "nextest.toml", "capture-driver.py", "rustc.stdout", "rustc.stderr", "cargo.stdout", "cargo.stderr"} - if schema >= 2: - required.update({"root-build.stdout", "root-build.stderr", "wrapper-build.stdout", "wrapper-build.stderr", "root-wrapper.rs", "root-wrapper.exe", "compile-proof.json", "build-environment.json", "root-rustc.stdout", "root-rustc.stderr"}) - if schema == SCHEMA: - required.update({"inventory.stdout", "inventory.stderr"}) - require(set(record["artifacts"]) == required, "incomplete capture artifact set") - sources = [read_json(output / name) for name in ( - "source-before.json", "source-after-build.json", "source-after-execution.json")] - require(sources[0] and sources[0] == sources[1] == sources[2], "source changed during capture") - require({"Cargo.toml", "Cargo.lock", "build.rs"} <= sources[0].keys(), "missing core build inputs") - before, after = [read_json(output / name) for name in ("binary-before.json", "binary-after.json")] - metadata = read_json(output / "binary-list.stdout") - require(before and before == after and set(before) == set(binary_paths(metadata)), "binary identity mismatch") - require(metadata["rust-build-meta"]["base-output-directories"] == ["debug"], "capture did not use debug build") - commands = record["commands"] - expected_commands = {"rustc", "cargo", "cargo-metadata", "binary-list", "execution"} - if schema >= 2: - expected_commands.update({"wrapper-build", "root-build", "root-rustc"}) - if schema == SCHEMA: - expected_commands.add("inventory") - require(set(commands) == expected_commands, "missing command provenance") - require(all(command["exit_code"] == 0 for command in commands.values()), "captured command failed") - for label, argv in [("rustc", ["rustc", "-vV"]), ("cargo", ["cargo", "-V"]), - ("cargo-metadata", ["cargo", "metadata", "--offline", "--locked", "--format-version", "1"])]: - require(commands[label]["argv"] == argv, "unexpected " + label + " command") - require(commands["binary-list"]["argv"] == ["cargo", "nextest", "list", "--offline", "--locked", "--test", "storage_maintenance", "--list-type", "binaries-only", "--message-format", "json"], "unexpected compile command") - require(commands["execution"]["argv"] == execution_command(Path(record["output"])), "unexpected execution command") - if schema >= 2: - validate_compile_proof(record, output) - if schema == SCHEMA: - require(commands["inventory"]["argv"] == inventory_command(Path(record["output"])), "unexpected inventory command") - names = inventory_cases(read_json(output / "inventory.stdout"), metadata, sources[0]) - require(record["expected_case_names"] == sorted(names), "recorded inventory identity mismatch") - else: - names = EXPECTED - result = junit_result(output / "junit.xml", names) - require(result["run_id"] and result == record["result"], "JUnit result/run identity mismatch") - require(record["limitations"] == (LIMITATIONS if schema == SCHEMA else LEGACY_LIMITATIONS), "qualification limitations changed") - return record - - -def execution_command(output): - return ["cargo", "nextest", "run", "--binaries-metadata", str(output / "binary-list.stdout"), - "--cargo-metadata", str(output / "cargo-metadata.stdout"), "--config-file", str(output / "nextest.toml"), - "--profile", "cleanup-s5-capture", "--test-threads", "1"] - - - -def inventory_command(output): - return ["cargo", "nextest", "list", "--binaries-metadata", str(output / "binary-list.stdout"), - "--cargo-metadata", str(output / "cargo-metadata.stdout"), "--config-file", str(output / "nextest.toml"), - "--profile", "cleanup-s5-capture", "--message-format", "json"] - - -def inventory_cases(inventory, binaries, sources): - require(inventory["rust-build-meta"] == binaries["rust-build-meta"], "inventory build identity mismatch") - suites = inventory["rust-suites"] - require(set(suites) == {BINARY}, "inventory selected unexpected test suites") - suite = suites[BINARY] - binary = binaries["rust-binaries"][BINARY] - require(suite["binary-path"] == binary["binary-path"] and suite["package-id"] == binary["package-id"], "inventory binary/package identity mismatch") - cases = suite["testcases"] - require(inventory["test-count"] == len(cases), "inventory count does not match named cases") - require(all(case["ignored"] is False and case["filter-match"]["status"] == "matches" for case in cases.values()), "ignored or filtered required test in inventory") - required = set(EXPECTED) - if "tests/support/storage_maintenance/workload.rs" in sources: - required.update(WORKLOAD_CASES) - if "tests/support/storage_maintenance/process.rs" in sources: - required.add(PROCESS_CASE) - require(required <= cases.keys(), "required fixture test cases were not registered") - return set(cases) - - -def capture(repository, output): - repository, output = Path(repository).resolve(), Path(output).resolve() - require(not output.is_relative_to(repository), "capture output must be outside the repository") - output.mkdir(parents=False, exist_ok=False) # Never overwrite a historical execution. - (output / "capture-driver.py").write_bytes(Path(__file__).read_bytes()) - head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repository).decode().strip() - write_json(output / "source-before.json", source_manifest(repository)) - # An absolute path avoids shared target directories overwriting other JUnit runs. - (output / "nextest.toml").write_text('[profile.cleanup-s5-capture]\nfail-fast = false\nretries = 0\n' - '[profile.cleanup-s5-capture.junit]\npath = ' + json.dumps(str(output / "junit.xml")) + '\n') - commands = {} - environment = build_environment(output) - write_json(output / "build-environment.json", {name: environment[name] for name in BUILD_ENV}) - (output / "root-wrapper.rs").write_text(WRAPPER_SOURCE) - (output / "rustc-events").mkdir() - commands["wrapper-build"] = run(["rustc", "--edition=2021", str(output / "root-wrapper.rs"), "-o", str(output / "root-wrapper.exe")], repository, output, "wrapper-build", environment) - require(commands["wrapper-build"]["exit_code"] == 0, "wrapper compilation failed") - for label, argv in [ - ("rustc", ["rustc", "-vV"]), ("cargo", ["cargo", "-V"]), - ("cargo-metadata", ["cargo", "metadata", "--offline", "--locked", "--format-version", "1"]), - ]: - commands[label] = run(argv, repository, output, label, environment) - require(commands[label]["exit_code"] == 0, label + " failed; raw output retained") - commands["root-build"] = run(root_build_command(), repository, output, "root-build", environment) - require(commands["root-build"]["exit_code"] == 0, "root build failed; raw output retained") - for label, argv in [ - ("binary-list", ["cargo", "nextest", "list", "--offline", "--locked", "--test", "storage_maintenance", "--list-type", "binaries-only", "--message-format", "json"]), - ]: - commands[label] = run(argv, repository, output, label, environment) - require(commands[label]["exit_code"] == 0, label + " failed; raw output retained") - write_json(output / "source-after-build.json", source_manifest(repository)) - require(read_json(output / "source-before.json") == read_json(output / "source-after-build.json"), "source changed during build") - paths = binary_paths(read_json(output / "binary-list.stdout")) - proof = compile_proof(repository, output) - write_json(output / "compile-proof.json", proof) - compilers = {event["compiler"] for event in proof["events"] - if event["manifest"] == str(repository) and "--crate-name" in event["args"]} - require(len(compilers) == 1, "root package used ambiguous compilers") - commands["root-rustc"] = run([next(iter(compilers)), "-vV"], repository, output, "root-rustc", environment) - require(commands["root-rustc"]["exit_code"] == 0, "actual root rustc identity failed") - write_json(output / "binary-before.json", {path: sha256(path) for path in paths}) - commands["inventory"] = run(inventory_command(output), repository, output, "inventory", environment) - require(commands["inventory"]["exit_code"] == 0, "test inventory failed; raw output retained") - names = inventory_cases(read_json(output / "inventory.stdout"), read_json(output / "binary-list.stdout"), read_json(output / "source-before.json")) - commands["execution"] = run(execution_command(output), repository, output, "execution", environment) - write_json(output / "binary-after.json", {path: sha256(path) for path in paths}) - write_json(output / "source-after-execution.json", source_manifest(repository)) - require(commands["execution"]["exit_code"] == 0, "execution failed; raw output retained") - record = { - "schema": SCHEMA, "scope": "storage_maintenance_suite", "head": head, - "repository": str(repository), "output": str(output), "host": platform.platform(), - "qualification": {"aggregate": "pending", "windows": "pending", "linux": "pending"}, - "commands": commands, "compile_proof": proof, "expected_case_names": sorted(names), "result": junit_result(output / "junit.xml", names), "limitations": LIMITATIONS, - "artifacts": {path.name: sha256(path) for path in sorted(output.iterdir()) if path.is_file()}, - } - write_json(output / "capture.json", record) - verify(output) - return record - - -# A unique workspace-wrapper path is part of Cargo's crate hash. Each capture -# therefore recompiles workspace code, retaining non-workspace dependency caches. -# The wrapper records actual compiler execution and applies assertions LAST, -# after flags supplied by Cargo configuration or profiles. -WRAPPER_SOURCE = r''' -use std::{env, ffi::OsString, fs, process::{Command, exit}}; -fn hex(value: &str) -> String { - value.as_bytes().iter().map(|b| format!("{b:02x}")).collect() -} -fn main() { - let mut args: Vec = env::args_os().skip(1).collect(); - let compiler = args.remove(0); - if args.iter().any(|arg| arg == "--crate-name") { - args.push("-C".into()); - args.push("debug-assertions=on".into()); - } - let mut evidence = format!("compiler={}\nmanifest={}\ncwd={}\n", - hex(compiler.to_str().expect("non-UTF8 compiler path")), - hex(&env::var("CARGO_MANIFEST_DIR").unwrap_or_default()), - hex(env::current_dir().unwrap().to_str().expect("non-UTF8 cwd"))); - for arg in &args { evidence.push_str(&format!("arg={}\n", hex(arg.to_str().expect("non-UTF8 compiler arg")))); } - let status = Command::new(&compiler).args(&args).status().expect("launch rustc"); - let code = status.code().unwrap_or(1); - evidence.push_str(&format!("exit={code}\n")); - let dir = env::var_os("CGRAPH_QUALIFICATION_EVENTS").expect("event directory"); - fs::write(std::path::PathBuf::from(dir).join(format!("{}.txt", std::process::id())), evidence).unwrap(); - exit(code); -} -''' -BUILD_ENV = ("RUSTC_WORKSPACE_WRAPPER", "CGRAPH_QUALIFICATION_EVENTS", - "CARGO_PROFILE_TEST_DEBUG_ASSERTIONS", "CARGO_PROFILE_DEV_DEBUG_ASSERTIONS") - - -def build_environment(output): - environment = os.environ.copy() - for name in ("RUSTC_WRAPPER", "RUSTC_WORKSPACE_WRAPPER", "RUSTFLAGS", "CARGO_ENCODED_RUSTFLAGS", "RUSTC"): - require(not environment.get(name), "unsupported ambient compiler override: " + name) - environment.update({ - "RUSTC_WORKSPACE_WRAPPER": str(output / "root-wrapper.exe"), - "CGRAPH_QUALIFICATION_EVENTS": str(output / "rustc-events"), - "CARGO_PROFILE_TEST_DEBUG_ASSERTIONS": "true", - "CARGO_PROFILE_DEV_DEBUG_ASSERTIONS": "true", - }) - return environment - - -def root_build_command(): - return ["cargo", "test", "--offline", "--locked", "--package", "codebase-graph", - "--test", "storage_maintenance", "--no-run", "--message-format=json", "-vv"] - - -def compile_proof(repository, output): - events = [] - for path in sorted((output / "rustc-events").glob("*.txt")): - event = {"args": []} - for line in path.read_text().splitlines(): - key, value = line.split("=", 1) - if key == "exit": - event[key] = int(value) - elif key == "arg": - event["args"].append(bytes.fromhex(value).decode()) - else: - event[key] = bytes.fromhex(value).decode() - events.append(event) - artifacts = [json.loads(line) for line in (output / "root-build.stdout").read_text().splitlines() - if line.startswith("{")] - outputs = {} - for artifact in artifacts: - if artifact.get("reason") != "compiler-artifact" or "custom-build" in artifact["target"]["kind"]: - continue - name = artifact["target"]["name"].replace("-", "_") - for event in events: - args = event["args"] - if event["manifest"] != str(repository) or "--crate-name" not in args or args[args.index("--crate-name") + 1] != name: - continue - actual_sources = [str(Path(event["cwd"]) / arg) if not Path(arg).is_absolute() else arg - for arg in args if arg.endswith(".rs")] - if artifact["target"]["src_path"] not in actual_sources: - continue - for filename in artifact["filenames"]: - emitted = compiler_output(event, artifact, filename) - require(sha256(emitted) == sha256(filename), "Cargo artifact differs from actual compiler output") - outputs[filename] = {"compiler_output": str(emitted), "sha256": sha256(emitted)} - return {"events": events, "cargo_messages": artifacts, "outputs": outputs} - - -def compiler_output(event, artifact, filename): - args = event["args"] - out_dir = Path(args[args.index("--out-dir") + 1]) - path = Path(filename) - if path.parent == out_dir: - return path - # Cargo copies normal bin targets from deps to the profile directory. - require("bin" in artifact["target"]["kind"] and path.parent == out_dir.parent, - "root output path mismatch") - extra = [arg.split("=", 1)[1] for arg in args if arg.startswith("extra-filename=")] - require(len(extra) == 1, "missing compiler output suffix") - return out_dir / (artifact["target"]["name"].replace("-", "_") + extra[0] + (".exe" if path.suffix == ".exe" else "")) - - -def validate_compile_proof(record, output): - repository = Path(record["repository"]) - original_output = Path(record["output"]) - require(record["commands"]["root-build"]["argv"] == root_build_command(), "unexpected root build command") - require(record["commands"]["wrapper-build"]["argv"] == ["rustc", "--edition=2021", str(original_output / "root-wrapper.rs"), "-o", str(original_output / "root-wrapper.exe")], "unexpected wrapper build command") - environment = read_json(output / "build-environment.json") - require(environment == { - "RUSTC_WORKSPACE_WRAPPER": str(original_output / "root-wrapper.exe"), - "CGRAPH_QUALIFICATION_EVENTS": str(original_output / "rustc-events"), - "CARGO_PROFILE_TEST_DEBUG_ASSERTIONS": "true", "CARGO_PROFILE_DEV_DEBUG_ASSERTIONS": "true", - }, "unsupported build environment") - require((output / "root-wrapper.rs").read_text() == WRAPPER_SOURCE, "compiler wrapper changed") - metadata = read_json(output / "cargo-metadata.stdout") - require(metadata["workspace_root"] == str(repository), "wrong Cargo workspace") - packages = [p for p in metadata["packages"] if p["manifest_path"] == str(repository / "Cargo.toml")] - require(len(packages) == 1 and packages[0]["name"] == "codebase-graph", "wrong root Cargo package") - package = packages[0] - require(package["id"] in metadata["workspace_members"], "root package is not a workspace member") - proof = read_json(output / "compile-proof.json") - require(proof == record["compile_proof"], "compile proof mismatch") - raw_messages = [json.loads(line) for line in (output / "root-build.stdout").read_text().splitlines() if line.startswith("{")] - require(proof["cargo_messages"] == raw_messages, "Cargo compile messages mismatch") - artifacts = [message for message in raw_messages if message.get("reason") == "compiler-artifact" - and message["package_id"] == package["id"] and "custom-build" not in message["target"]["kind"]] - require(artifacts, "missing root compile artifacts") - selected = read_json(output / "binary-list.stdout") - require(all(row["package-id"] == package["id"] for row in selected["rust-binaries"].values()), "nextest selected wrong Cargo package") - require(set(selected["rust-build-meta"].get("non-test-binaries", {})) <= {package["id"]}, "wrong helper package") - compilers = {event["compiler"] for event in proof["events"] - if event["manifest"] == str(repository) and "--crate-name" in event["args"]} - require(len(compilers) == 1 and record["commands"]["root-rustc"]["argv"] == [next(iter(compilers)), "-vV"], "actual root compiler identity mismatch") - compiled_paths = set() - saw_library = False - sources = read_json(output / "source-before.json") - for artifact in artifacts: - require(artifact["fresh"] is False, "root artifact reused from cache") - require(artifact["profile"]["debug_assertions"] is True, "root profile disables debug assertions") - source = Path(artifact["target"]["src_path"]) - require(source.is_relative_to(repository) and str(source.relative_to(repository)) in sources, "unrecorded root source") - name = artifact["target"]["name"].replace("-", "_") - matching = [] - for event in proof["events"]: - args = event["args"] - if event["manifest"] != str(repository) or event["exit"] != 0 or "--crate-name" not in args: - continue - if args[args.index("--crate-name") + 1] != name: - continue - actual_sources = [str(Path(event["cwd"]) / arg) if not Path(arg).is_absolute() else arg - for arg in args if arg.endswith(".rs")] - if str(source) in actual_sources: - matching.append(event) - require(matching, "no actual rustc invocation for root artifact") - for event in matching: - require(event["args"][-2:] == ["-C", "debug-assertions=on"], "actual root compiler assertions are not enabled") - for path in artifact["filenames"]: - emitted = compiler_output(event, artifact, path) - require(proof["outputs"][path]["compiler_output"] == str(emitted), "compiler output identity mismatch") - if path in read_json(output / "binary-before.json"): - require(proof["outputs"][path]["sha256"] == read_json(output / "binary-before.json")[path], "selected binary differs from compiler output") - compiled_paths.update(artifact["filenames"]) - saw_library |= "lib" in artifact["target"]["kind"] - require(saw_library, "root library was not freshly compiled") - require(set(binary_paths(selected)) <= compiled_paths, "selected binary/helper did not come from root compilation") - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - commands = parser.add_subparsers(dest="action", required=True) - command = commands.add_parser("capture") - command.add_argument("--repository", required=True) - command.add_argument("--output", required=True) - command = commands.add_parser("verify") - command.add_argument("output") - args = parser.parse_args() - record = capture(args.repository, args.output) if args.action == "capture" else verify(args.output) - if record["schema"] < SCHEMA: - print("Historical %d-case capture integrity verified for its recorded scope; no new suite qualification." % record["result"]["passed"]) - else: - print("Verified all %d registered storage_maintenance cases; aggregate/platform gates remain pending." % record["result"]["passed"]) - - -if __name__ == "__main__": - main() diff --git a/docs/testing/graph-disk-cleanup-evidence/test_audit.py b/docs/testing/graph-disk-cleanup-evidence/test_audit.py deleted file mode 100644 index f934f32d..00000000 --- a/docs/testing/graph-disk-cleanup-evidence/test_audit.py +++ /dev/null @@ -1,133 +0,0 @@ -"""Mutation regressions use isolated copies; original execution reports stay intact.""" - -import contextlib -import hashlib -import gzip -import importlib.util -import io -import json -from pathlib import Path -import shutil -import tempfile -import unittest -import xml.etree.ElementTree as ET - -ROOT = Path(__file__).resolve().parent -SPEC = importlib.util.spec_from_file_location("evidence_audit", ROOT / "audit.py") -AUDIT = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(AUDIT) - - -class EvidenceMutationTests(unittest.TestCase): - def setUp(self): - self.temporary = tempfile.TemporaryDirectory(prefix="cgraph-s5-audit-regression-") - self.addCleanup(self.temporary.cleanup) - base = Path(self.temporary.name) - self.root = base / ROOT.name - shutil.copytree(ROOT, self.root, ignore=shutil.ignore_patterns("__pycache__")) - shutil.copy2(ROOT.parent / "graph-disk-cleanup-evidence.json", base) - - def read(self, name): - return json.loads((self.root / name).read_text()) - - def write(self, name, data): - (self.root / name).write_text(json.dumps(data)) - - def rewrite_report(self, record, mutate, path_key="junit", hash_key="junit_sha256"): - path = self.root / record[path_key] - tree = ET.parse(path) - mutate(tree.getroot()) - tree.write(path, encoding="utf-8", xml_declaration=True) - record[hash_key] = hashlib.sha256(path.read_bytes()).hexdigest() - - def external(self): - index = json.loads((self.root.parent / "graph-disk-cleanup-evidence.json").read_text()) - return Path(index["s5_external_execution"]).name - - def rejected(self): - with contextlib.redirect_stdout(io.StringIO()), self.assertRaises(AssertionError): - AUDIT.audit(self.root) - - def test_original_actual_reports_pass(self): - with contextlib.redirect_stdout(io.StringIO()): - AUDIT.audit(self.root) - - def test_compressed_artifact_matches_historical_decoded_bytes(self): - data = self.read("review-outcomes.json") - artifact = next(a for a in data["s3_expanded_partial"]["executed_failure_artifacts"] if a.get("encoding") == "gzip") - decoded = gzip.decompress((self.root / artifact["path"]).read_bytes()) - historical = self.read("s5-narrow-initial-checkpoint.json") - historical_hash = historical["owned_files_sha256"][artifact["historical_repository_path"]] - self.assertEqual(hashlib.sha256(decoded).hexdigest(), historical_hash) - self.assertEqual(artifact["decoded_sha256"], historical_hash) - self.assertEqual(artifact["historical_commit"], "9eaa8dedf4f626ebf0974f327f81d0a3c0315e57") - - def test_compressed_payload_tamper_rejected_even_with_matching_encoded_hash(self): - data = self.read("review-outcomes.json") - artifact = next(a for a in data["s3_expanded_partial"]["executed_failure_artifacts"] if a.get("encoding") == "gzip") - encoded = gzip.compress(b"changed observation output\n", mtime=0) - (self.root / artifact["path"]).write_bytes(encoded) - artifact["sha256"] = hashlib.sha256(encoded).hexdigest() - self.write("review-outcomes.json", data) - self.rejected() - - def test_named_foundation_skip_rejected_even_with_matching_hash_and_counts(self): - data = self.read("foundation-reports.json") - record = next(r for r in data["reports"] if r["id"] == "s1-post-main") - def skip(xml): - case = next(c for c in xml.findall(".//testcase") if "dc_s1_a1_" in c.get("name", "")) - ET.SubElement(case, "skipped") - self.rewrite_report(record, skip, "report", "sha256") - record["execution_counts"]["skipped"] += 1 - self.write("foundation-reports.json", data) - self.rejected() - - def test_external_skip_rejected_with_original_claimed_counts(self): - name = self.external() - data = self.read(name) - self.rewrite_report(data, lambda xml: ET.SubElement(xml.findall(".//testcase")[0], "skipped")) - self.write(name, data) - self.rejected() - - def test_external_skip_rejected_even_with_matching_manifest_counts(self): - name = self.external() - data = self.read(name) - self.rewrite_report(data, lambda xml: ET.SubElement(xml.findall(".//testcase")[0], "skipped")) - data["actual_result"] = {"passed": 1, "failed": 0, "skipped": 1} - self.write(name, data) - self.rejected() - - def test_external_claimed_failure_count_mismatch_rejected(self): - name = self.external() - data = self.read(name) - data["actual_result"]["failed"] = 1 - self.write(name, data) - self.rejected() - - def test_external_case_name_mismatch_rejected_despite_valid_hash(self): - name = self.external() - data = self.read(name) - self.rewrite_report(data, lambda xml: xml.findall(".//testcase")[0].set("name", "different_case")) - self.write(name, data) - self.rejected() - - def test_external_run_identity_mismatch_rejected_despite_valid_hash(self): - name = self.external() - data = self.read(name) - self.rewrite_report(data, lambda xml: xml.set("uuid", "different-run")) - self.write(name, data) - self.rejected() - - def test_external_missing_case_rejected_despite_valid_hash(self): - name = self.external() - data = self.read(name) - def remove(xml): - suite = xml.find("testsuite") - suite.remove(suite.find("testcase")) - self.rewrite_report(data, remove) - self.write(name, data) - self.rejected() - - -if __name__ == "__main__": - unittest.main() diff --git a/docs/testing/graph-disk-cleanup-evidence/test_qualification.py b/docs/testing/graph-disk-cleanup-evidence/test_qualification.py deleted file mode 100644 index 4e206288..00000000 --- a/docs/testing/graph-disk-cleanup-evidence/test_qualification.py +++ /dev/null @@ -1,405 +0,0 @@ -"""Protocol regressions. Synthetic archives here never qualify production behavior.""" - -import importlib.util -import json -import os -import subprocess -from pathlib import Path -import sys -import tempfile -import unittest -from unittest.mock import patch - -ROOT = Path(__file__).resolve().parent -SPEC = importlib.util.spec_from_file_location("qualification", ROOT / "qualification.py") -Q = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(Q) - - -class QualificationProtocolTests(unittest.TestCase): - def setUp(self): - self.temporary = tempfile.TemporaryDirectory(prefix="cgraph-s5-qualification-protocol-") - self.addCleanup(self.temporary.cleanup) - self.output = Path(self.temporary.name) - source = {name: "a" * 64 for name in ["Cargo.toml", "Cargo.lock", "build.rs", "src/lib.rs", "src/main.rs", "tests/storage_maintenance.rs"]} - for name in ["source-before.json", "source-after-build.json", "source-after-execution.json"]: - Q.write_json(self.output / name, source) - binaries = {"/synthetic/target/debug/deps/storage_maintenance-test": "b" * 64, - "/synthetic/target/debug/codebase-graph": "c" * 64} - for name in ["binary-before.json", "binary-after.json"]: - Q.write_json(self.output / name, binaries) - metadata = {"rust-build-meta": { - "target-directory": "/synthetic/target", "base-output-directories": ["debug"], - "non-test-binaries": {"synthetic-root": [{"path": "debug/codebase-graph"}]}, - }, "rust-binaries": {Q.BINARY: {"package-id": "synthetic-root", "binary-path": "/synthetic/target/debug/deps/storage_maintenance-test"}}} - Q.write_json(self.output / "binary-list.stdout", metadata) - self.xml = '' + ''.join( - '' % (Q.BINARY, name) for name in sorted(Q.EXPECTED) - ) + '' - (self.output / "junit.xml").write_text(self.xml) - for name in ["binary-list.stderr", "cargo-metadata.stdout", "cargo-metadata.stderr", - "execution.stdout", "execution.stderr", "nextest.toml", "capture-driver.py", "rustc.stdout", - "rustc.stderr", "cargo.stdout", "cargo.stderr"]: - (self.output / name).write_text("synthetic protocol fixture\n") - repository = Path("/synthetic/repo") - package = {"id": "synthetic-root", "name": "codebase-graph", "manifest_path": str(repository / "Cargo.toml")} - Q.write_json(self.output / "cargo-metadata.stdout", {"workspace_root": str(repository), "workspace_members": [package["id"]], "packages": [package]}) - messages, events = [], [] - for name, kind, source, filename in [ - ("codebase_graph", "lib", "src/lib.rs", "/synthetic/target/debug/deps/libcodebase_graph.rlib"), - ("codebase-graph", "bin", "src/main.rs", "/synthetic/target/debug/codebase-graph"), - ("storage_maintenance", "test", "tests/storage_maintenance.rs", "/synthetic/target/debug/deps/storage_maintenance-test"), - ]: - messages.append({"reason": "compiler-artifact", "package_id": package["id"], "fresh": False, - "profile": {"debug_assertions": True}, "target": {"name": name, "kind": [kind], "src_path": str(repository / source)}, "filenames": [filename]}) - events.append({"compiler": "/synthetic/rustc", "manifest": str(repository), "cwd": str(repository), "exit": 0, - "args": ["--crate-name", name.replace("-", "_"), source, "--out-dir", str(Path(filename).parent), "-C", "debug-assertions=on"]}) - (self.output / "root-build.stdout").write_text("".join(json.dumps(m) + "\n" for m in messages)) - for name in ["root-build.stderr", "wrapper-build.stdout", "wrapper-build.stderr", "root-wrapper.exe", "root-rustc.stdout", "root-rustc.stderr"]: - (self.output / name).write_text("synthetic protocol fixture\n") - (self.output / "root-wrapper.rs").write_text(Q.WRAPPER_SOURCE) - outputs = {m["filenames"][0]: {"compiler_output": m["filenames"][0], "sha256": binaries.get(m["filenames"][0], "d" * 64)} for m in messages} - Q.write_json(self.output / "compile-proof.json", {"events": events, "cargo_messages": messages, "outputs": outputs}) - Q.write_json(self.output / "build-environment.json", { - "RUSTC_WORKSPACE_WRAPPER": str(self.output / "root-wrapper.exe"), "CGRAPH_QUALIFICATION_EVENTS": str(self.output / "rustc-events"), - "CARGO_PROFILE_TEST_DEBUG_ASSERTIONS": "true", "CARGO_PROFILE_DEV_DEBUG_ASSERTIONS": "true"}) - Q.write_json(self.output / "inventory.stdout", { - "rust-build-meta": metadata["rust-build-meta"], "test-count": len(Q.EXPECTED), - "rust-suites": {Q.BINARY: {"binary-path": "/synthetic/target/debug/deps/storage_maintenance-test", "package-id": "synthetic-root", - "testcases": {name: {"ignored": False, "filter-match": {"status": "matches"}} for name in Q.EXPECTED}}}}) - (self.output / "inventory.stderr").write_text("synthetic protocol inventory\n") - self.record = { - "schema": Q.SCHEMA, "scope": "storage_maintenance_suite", "output": str(self.output), "repository": str(repository), - "compile_proof": {"events": events, "cargo_messages": messages, "outputs": outputs}, - "expected_case_names": sorted(Q.EXPECTED), - "qualification": {"aggregate": "pending", "windows": "pending", "linux": "pending"}, - "limitations": Q.LIMITATIONS, - "commands": {label: {"argv": argv, "exit_code": 0} for label, argv in [ - ("rustc", ["rustc", "-vV"]), ("cargo", ["cargo", "-V"]), - ("cargo-metadata", ["cargo", "metadata", "--offline", "--locked", "--format-version", "1"]), - ("binary-list", ["cargo", "nextest", "list", "--offline", "--locked", "--test", "storage_maintenance", "--list-type", "binaries-only", "--message-format", "json"]), - ("wrapper-build", ["rustc", "--edition=2021", str(self.output / "root-wrapper.rs"), "-o", str(self.output / "root-wrapper.exe")]), - ("root-build", Q.root_build_command()), - ("root-rustc", ["/synthetic/rustc", "-vV"]), - ("inventory", Q.inventory_command(self.output)), - ("execution", Q.execution_command(self.output)), - ]}, - "result": Q.junit_result(self.output / "junit.xml"), - } - self.seal() - - def seal(self): - self.record["artifacts"] = {p.name: Q.sha256(p) for p in self.output.iterdir() if p.name != "capture.json"} - Q.write_json(self.output / "capture.json", self.record) - - def rejected(self, reason): - with self.assertRaisesRegex(ValueError, reason): - Q.verify(self.output) - - def test_protocol_fixture_is_valid_without_production_qualification(self): - self.assertEqual(Q.verify(self.output)["qualification"]["aggregate"], "pending") - - def test_missing_and_changed_artifacts_rejected(self): - (self.output / "execution.stderr").write_text("changed") - self.rejected("artifact changed") - - def test_path_traversal_rejected(self): - self.record["artifacts"]["../outside"] = "a" * 64 - Q.write_json(self.output / "capture.json", self.record) - self.rejected("escapes archive") - - def test_source_change_even_with_rehashed_artifact_rejected(self): - Q.write_json(self.output / "source-after-build.json", {"Cargo.toml": "d" * 64}) - self.seal() - self.rejected("source changed") - - def test_source_addition_and_deletion_invalidate_snapshot(self): - for name in ["Cargo.toml", "Cargo.lock", "build.rs"]: - (self.output / name).write_text("fixture") - with patch.object(Q.subprocess, "check_output", return_value=b"Cargo.toml\0Cargo.lock\0build.rs\0"): - original = Q.source_manifest(self.output) - (self.output / "new.rs").write_text("new") - with patch.object(Q.subprocess, "check_output", return_value=b"Cargo.toml\0Cargo.lock\0build.rs\0src/new.rs\0"): - (self.output / "src").mkdir() - (self.output / "src/new.rs").write_text("new") - self.assertNotEqual(Q.source_manifest(self.output), original) - (self.output / "Cargo.lock").unlink() - with patch.object(Q.subprocess, "check_output", return_value=b"Cargo.toml\0Cargo.lock\0build.rs\0"): - with self.assertRaises(FileNotFoundError): - Q.source_manifest(self.output) - - def test_changed_binary_rejected_even_with_rehashed_artifact(self): - Q.write_json(self.output / "binary-after.json", {"/synthetic/other": "d" * 64}) - self.seal() - self.rejected("binary identity") - - def test_unrecorded_helper_binary_rejected(self): - for name in ["binary-before.json", "binary-after.json"]: - data = Q.read_json(self.output / name) - del data["/synthetic/target/debug/codebase-graph"] - Q.write_json(self.output / name, data) - self.seal() - self.rejected("binary identity") - - def test_release_build_cannot_qualify_debug_capture(self): - metadata = Q.read_json(self.output / "binary-list.stdout") - metadata["rust-build-meta"]["base-output-directories"] = ["release"] - Q.write_json(self.output / "binary-list.stdout", metadata) - self.seal() - self.rejected("debug build") - - def test_failed_build_or_execution_rejected(self): - for label in ["binary-list", "execution"]: - self.record["commands"][label]["exit_code"] = 101 - self.seal() - self.rejected("command failed") - self.record["commands"][label]["exit_code"] = 0 - - def test_filtered_or_rebuilt_execution_rejected(self): - self.record["commands"]["execution"]["argv"] += ["-E", "test(nothing)"] - self.seal() - self.rejected("execution command") - - def test_skip_failure_error_and_retry_are_not_passes(self): - for tag in ["skipped", "failure", "error", "rerunFailure", "rerunError"]: - (self.output / "junit.xml").write_text(self.xml.replace('/>', '><%s/>' % tag, 1)) - self.seal() - self.rejected("first execution") - - def test_missing_extra_and_duplicate_cases_rejected(self): - for change in [self.xml.replace('name="health_preserves', 'name="different_health_preserves'), - self.xml.replace('', ''), - self.xml.replace('', '' % (Q.BINARY, sorted(Q.EXPECTED)[0]))]: - (self.output / "junit.xml").write_text(change) - self.seal() - self.rejected("testcase") - - def test_result_and_run_identity_mismatch_rejected(self): - self.record["result"]["run_id"] = "another-run" - self.seal() - self.rejected("identity mismatch") - - def test_platform_or_aggregate_promotion_rejected(self): - for gate in ["aggregate", "windows", "linux"]: - self.record["qualification"][gate] = "passed" - self.seal() - self.rejected("qualification gates") - self.record["qualification"][gate] = "pending" - - def test_capture_refuses_existing_output_before_any_build(self): - with self.assertRaises(FileExistsError): - Q.capture(ROOT, self.output) - - def mutate_proof(self, mutate): - proof = Q.read_json(self.output / "compile-proof.json") - mutate(proof) - Q.write_json(self.output / "compile-proof.json", proof) - self.record["compile_proof"] = proof - (self.output / "root-build.stdout").write_text("".join(json.dumps(m) + "\n" for m in proof["cargo_messages"])) - self.seal() - - def test_cached_root_artifact_is_rejected(self): - self.mutate_proof(lambda proof: proof["cargo_messages"][0].update(fresh=True)) - self.rejected("reused from cache") - - def test_assertion_disabled_root_profile_is_rejected(self): - self.mutate_proof(lambda proof: proof["cargo_messages"][0]["profile"].update(debug_assertions=False)) - self.rejected("profile disables") - - def test_missing_actual_root_compiler_invocation_is_rejected(self): - self.mutate_proof(lambda proof: proof.update(events=[])) - self.rejected("actual root compiler identity|actual rustc invocation") - - def test_assertion_disabled_actual_compiler_is_rejected(self): - self.mutate_proof(lambda proof: proof["events"][0]["args"].__setitem__(-1, "debug-assertions=off")) - self.rejected("compiler assertions") - - def test_wrong_workspace_and_package_are_rejected(self): - data = Q.read_json(self.output / "cargo-metadata.stdout") - data["workspace_root"] = "/another/repository" - Q.write_json(self.output / "cargo-metadata.stdout", data) - self.seal() - self.rejected("wrong Cargo workspace") - - def test_ambient_rustflags_are_rejected_before_compilation(self): - with patch.dict(os.environ, {"RUSTFLAGS": "-C debug-assertions=off"}): - with self.assertRaisesRegex(ValueError, "unsupported ambient compiler override"): - Q.build_environment(self.output) - - def test_current_suite_inventory_cannot_hide_filtered_or_ignored_cases(self): - original = Q.read_json(self.output / "inventory.stdout") - for field in ["ignored", "filter-match"]: - inventory = json.loads(json.dumps(original)) - case = next(iter(inventory["rust-suites"][Q.BINARY]["testcases"].values())) - case[field] = True if field == "ignored" else {"status": "mismatch"} - Q.write_json(self.output / "inventory.stdout", inventory) - self.seal() - self.rejected("ignored or filtered") - - def test_added_registered_case_must_execute_in_junit(self): - inventory = Q.read_json(self.output / "inventory.stdout") - inventory["test-count"] += 1 - inventory["rust-suites"][Q.BINARY]["testcases"]["new_real_case"] = {"ignored": False, "filter-match": {"status": "matches"}} - Q.write_json(self.output / "inventory.stdout", inventory) - self.record["expected_case_names"].append("new_real_case") - self.record["expected_case_names"].sort() - self.seal() - self.rejected("testcase") - - def test_source_workload_cannot_pass_only_the_three_historical_cases(self): - for name in ["source-before.json", "source-after-build.json", "source-after-execution.json"]: - source = Q.read_json(self.output / name) - source["tests/support/storage_maintenance/workload.rs"] = "e" * 64 - Q.write_json(self.output / name, source) - self.seal() - self.rejected("required fixture test cases") - - def test_inventory_binary_package_and_named_identity_must_match(self): - inventory = Q.read_json(self.output / "inventory.stdout") - inventory["rust-suites"][Q.BINARY]["package-id"] = "foreign" - Q.write_json(self.output / "inventory.stdout", inventory) - self.seal() - self.rejected("binary/package identity") - - def test_source_bound_eight_and_nine_case_inventories_are_accepted_exactly(self): - names = set(Q.EXPECTED) | Q.WORKLOAD_CASES - for include_process in [False, True]: - if include_process: - names.add(Q.PROCESS_CASE) - for name in ["source-before.json", "source-after-build.json", "source-after-execution.json"]: - source = Q.read_json(self.output / name) - source["tests/support/storage_maintenance/workload.rs"] = "e" * 64 - if include_process: - source["tests/support/storage_maintenance/process.rs"] = "f" * 64 - Q.write_json(self.output / name, source) - inventory = Q.read_json(self.output / "inventory.stdout") - inventory["test-count"] = len(names) - inventory["rust-suites"][Q.BINARY]["testcases"] = {name: {"ignored": False, "filter-match": {"status": "matches"}} for name in names} - Q.write_json(self.output / "inventory.stdout", inventory) - xml = '' + ''.join('' % (Q.BINARY, name) for name in sorted(names)) + '' - (self.output / "junit.xml").write_text(xml) - self.record["expected_case_names"] = sorted(names) - self.record["result"] = Q.junit_result(self.output / "junit.xml", names) - self.seal() - self.assertEqual(Q.verify(self.output)["result"]["passed"], 9 if include_process else 8) - - def test_historical_schema_two_keeps_its_exact_three_case_scope(self): - self.record["schema"] = 2 - self.record["scope"] = "narrow_external_contract" - self.record["limitations"] = Q.LEGACY_LIMITATIONS - del self.record["commands"]["inventory"] - for name in ["inventory.stdout", "inventory.stderr"]: - (self.output / name).unlink() - self.seal() - historical = Q.verify(self.output) - self.assertEqual(historical["result"]["passed"], 3) - self.assertEqual(historical["schema"], 2) - self.assertEqual(historical["scope"], "narrow_external_contract") - - def test_real_subprocess_exit_and_output_are_captured(self): - result = Q.run([sys.executable, "-c", "import sys; print('actual output'); sys.stderr.write('actual error'); sys.exit(7)"], self.output, self.output, "probe") - self.assertEqual(result["exit_code"], 7) - self.assertEqual((self.output / "probe.stdout").read_text(), "actual output\n") - self.assertEqual((self.output / "probe.stderr").read_text(), "actual error") - - -class QualificationRuntimeTests(unittest.TestCase): - """Real Cargo/nextest counterexamples, using only a tiny fixture-owned target. - - These run normally with this module. --protocol-only is available while the - coordinator's Cargo lease belongs to another slice; it is not qualification. - """ - def setUp(self): - evidence_root = os.environ.get("CGRAPH_QUALIFICATION_RUNTIME_OUTPUT") - if evidence_root: - self.base = Path(tempfile.mkdtemp(prefix=self._testMethodName + "-", dir=evidence_root)).resolve() - print("Real provenance regression archive: " + str(self.base), file=sys.stderr) - else: - self.temporary = tempfile.TemporaryDirectory(prefix="cgraph-s5-real-provenance-") - self.addCleanup(self.temporary.cleanup) - self.base = Path(self.temporary.name).resolve() - self.repository = self.base / "repo" - self.repository.mkdir() - (self.repository / "src").mkdir() - (self.repository / "tests").mkdir() - (self.repository / "Cargo.toml").write_text('[package]\nname="codebase-graph"\nversion="0.0.0"\nedition="2021"\n') - (self.repository / "build.rs").write_text("fn main() {}\n") - (self.repository / "src/lib.rs").write_text("pub fn assertions_enabled() -> bool { cfg!(debug_assertions) }\n") - (self.repository / "src/main.rs").write_text("fn main() { assert!(cfg!(debug_assertions)); }\n") - self.test_source = self.repository / "tests/storage_maintenance.rs" - self.write_assertions("assert!(true)") - environment = os.environ.copy() - environment["CARGO_TARGET_DIR"] = str(self.base / "target") - for name in ["RUSTFLAGS", "CARGO_ENCODED_RUSTFLAGS", "RUSTC", "RUSTC_WRAPPER", "RUSTC_WORKSPACE_WRAPPER"]: - environment.pop(name, None) - self.environment = environment - for command in [ - ["git", "init", "--quiet"], ["cargo", "generate-lockfile", "--offline"], - ["git", "add", "."], ["git", "-c", "user.name=Qualification fixture", "-c", "user.email=fixture@example.invalid", "commit", "--quiet", "-m", "test: isolated fixture"], - ]: - subprocess.run(command, cwd=self.repository, env=environment, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - def write_assertions(self, assertion): - self.test_source.write_text("".join( - '#[test]\nfn %s() { %s; assert!(codebase_graph::assertions_enabled()); assert!(std::process::Command::new(env!("CARGO_BIN_EXE_codebase-graph")).status().unwrap().success()); }\n' % (name, assertion) - for name in sorted(Q.EXPECTED))) - - def capture(self, name): - with patch.dict(os.environ, self.environment, clear=True): - return Q.capture(self.repository, self.base / name) - - def test_mtime_preserved_failing_source_is_recompiled_and_fails(self): - original = self.capture("before") - old_stat = self.test_source.stat() - self.write_assertions("assert!(false)") - os.utime(self.test_source, ns=(old_stat.st_atime_ns, old_stat.st_mtime_ns)) - failing_hash = Q.sha256(self.test_source) - with self.assertRaisesRegex(ValueError, "execution failed"): - self.capture("after") - self.assertEqual(Q.sha256(self.test_source), failing_hash) - self.assertEqual(self.test_source.stat().st_mtime_ns, old_stat.st_mtime_ns) - proof = Q.read_json(self.base / "after/compile-proof.json") - root = [m for m in proof["cargo_messages"] if m.get("reason") == "compiler-artifact" and "custom-build" not in m["target"]["kind"]] - self.assertTrue(root and all(m["fresh"] is False for m in root)) - self.assertNotEqual(Q.read_json(self.base / "before/source-before.json"), Q.read_json(self.base / "after/source-before.json")) - self.assertEqual(original["result"]["passed"], 3) - Q.write_json(self.base / "runtime-result.json", { - "scenario": "mtime-preserved changed assertion", "baseline_result": original["result"], - "failing_source_sha256": failing_hash, "mtime_before_ns": old_stat.st_mtime_ns, - "mtime_after_ns": self.test_source.stat().st_mtime_ns, - "changed_source_was_recompiled": all(m["fresh"] is False for m in root), - "changed_source_capture_rejected": True, - "actual_failed_junit_sha256": Q.sha256(self.base / "after/junit.xml"), - }) - - def test_assertion_disabled_environment_and_config_cannot_pass_disabled_tests(self): - self.environment["CARGO_PROFILE_TEST_DEBUG_ASSERTIONS"] = "false" - self.environment["CARGO_PROFILE_DEV_DEBUG_ASSERTIONS"] = "false" - config = self.repository / ".cargo" - config.mkdir() - (config / "config.toml").write_text('[build]\nrustflags = ["-C", "debug-assertions=off"]\n') - # Enabled-sensitive library, helper and all tests pass despite ambient off. - enabled = self.capture("enabled") - self.assertEqual(enabled["result"]["passed"], 3) - self.write_assertions("assert!(!cfg!(debug_assertions))") - with self.assertRaisesRegex(ValueError, "execution failed"): - self.capture("disabled-counterexample") - proof = Q.read_json(self.base / "enabled/compile-proof.json") - self.assertTrue(any("debug-assertions=off" in e["args"] for e in proof["events"])) - root_events = [e for e in proof["events"] if "--crate-name" in e["args"]] - self.assertTrue(root_events and all(e["args"][-2:] == ["-C", "debug-assertions=on"] for e in root_events)) - Q.write_json(self.base / "runtime-result.json", { - "scenario": "assertion-disabled environment and Cargo config", "enabled_result": enabled["result"], - "ambient_test_debug_assertions": "false", "ambient_dev_debug_assertions": "false", - "config_rustflags": ["-C", "debug-assertions=off"], "actual_root_compilers_end_assertions_on": True, - "assertion_disabled_test_capture_rejected": True, - "actual_failed_junit_sha256": Q.sha256(self.base / "disabled-counterexample/junit.xml"), - }) - - -if __name__ == "__main__": - if "--protocol-only" in sys.argv: - sys.argv.remove("--protocol-only") - suite = unittest.defaultTestLoader.loadTestsFromTestCase(QualificationProtocolTests) - sys.exit(not unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful()) - unittest.main() diff --git a/docs/testing/release-gate-40453997235/local-checks.log b/docs/testing/release-gate-40453997235/local-checks.log new file mode 100644 index 00000000..e27772fc --- /dev/null +++ b/docs/testing/release-gate-40453997235/local-checks.log @@ -0,0 +1,95 @@ + Compiling xtask v4.0.0 (/private/tmp/codebasegraph-fix-release-python-40453997235/crates/xtask) + Finished `test` profile [unoptimized + debuginfo] target(s) in 0.87s + Running unittests src/main.rs (target/debug/deps/xtask-3ae50130c6c7ac02) + +running 43 tests +test native_tests::shards::tests::empty_shard_never_runs_an_unfiltered_suite ... ok +test native_tests::shards::tests::selected_exact_filters_execute_the_expected_real_harness_cases ... ok +test native_tests::shards::tests::shards_cover_every_listed_case_once_and_bound_windows_arguments ... ok +test native_tests::tests::captured_cli_survives_cargo_replacing_its_output ... ok +test native_tests::tests::partitions_every_test_target_and_keeps_new_integration_targets ... ok +test native_tests::tests::windows_regressions_precede_the_complete_unfiltered_suite ... ok +test release::tests::metadata_rejects_inconsistent_versions ... ok +test release::tests::notes_only_include_the_exact_release ... ok +test release::tests::notes_reject_missing_duplicate_and_empty_sections ... ok +test release::workflow_tests::ci_run_recovery_dry_run_validates_without_authorizing_publication ... ok +test release::workflow_tests::ordinary_commit_cannot_authorize_release_and_tagged_retry_still_can ... ok +test release::workflow_tests::proposal_reports_pending_release_then_proceeds_after_finalization ... ok +test release::workflow_tests::publisher_rejects_conflicting_tags_and_does_not_regress_latest ... ok +test release::workflow_tests::publisher_targets_original_sha_and_is_idempotent ... ok +test release::workflow_tests::recovery_requires_one_target_and_forbids_ci_run_rebuilds ... ok +test release::workflow_tests::verified_release_rejects_failed_ci_and_untrusted_run_identity ... ok +test release::workflow_tests::verified_release_rejects_untrusted_prs_and_removed_history ... ok +test release::workflow_tests::verified_release_survives_main_advancing_and_ignores_other_pending_releases ... ok +test tests::archive_contract_rejects_unexpected_entries ... ok +test tests::complete_artifact_set_rejects_mixed_provenance ... ok +test tests::crate_size_validation_enforces_the_registry_limit ... ok +test tests::installer_rejects_removed_hosts_before_reading_archives_or_writing_destinations ... ok +test tests::native_capture_requires_unique_source_bound_normal_cli_artifact ... ok +test tests::native_capture_reuses_the_exact_test_profile_and_features ... ok +test tests::native_target_contract_maps_names_and_binaries ... ok +test tests::native_test_failure_cannot_authorize_artifact_production ... ok +test tests::release_identity_validation_is_strict ... ok +test tests::windows_native_tests_forward_bundled_extensions ... ok +test tests::workflow_policy_rejects_automatic_rebuild_recovery ... ok +test tests::workflow_policy_rejects_direct_release_push_trigger ... ok +test tests::workflow_policy_rejects_github_sha_for_release_identity ... ok +test tests::workflow_policy_rejects_missing_ci_success_guard ... ok +test tests::workflow_policy_rejects_missing_crate_publish_retry ... ok +test tests::workflow_policy_rejects_missing_crate_size_gates ... ok +test tests::workflow_policy_rejects_missing_repository_release_gate ... ok +test tests::workflow_policy_rejects_missing_triggering_run_binding ... ok +test tests::workflow_policy_rejects_release_queue_replacement ... ok +test tests::workflow_policy_rejects_unconditional_release_publication ... ok +test tests::workflow_policy_rejects_unvalidated_publishers ... ok +test tests::workflow_policy_requires_ci_completion_release_trigger ... ok +test tests::workflow_policy_requires_publication_revalidation ... ok +test tests::workflow_yaml_validation_rejects_unindented_heredoc_body ... ok +test tests::workflows_reject_removed_platform_targets ... ok + +test result: ok. 43 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.02s + + Checking typenum v1.20.1 + Checking cfg-if v1.0.4 + Checking libc v0.2.189 + Checking bitflags v2.13.0 + Checking serde_core v1.0.228 + Checking itoa v1.0.18 + Checking equivalent v1.0.2 + Checking hashbrown v0.15.5 + Checking adler2 v2.0.1 + Checking simd-adler32 v0.3.10 + Checking crc32fast v1.5.0 + Checking zmij v1.0.21 + Checking libyaml-rs v0.3.0 + Checking ryu v1.0.23 + Checking memchr v2.8.2 + Checking miniz_oxide v0.9.1 + Checking flate2 v1.1.10 + Checking errno v0.3.14 + Checking filetime v0.2.29 + Checking cpufeatures v0.2.17 + Checking indexmap v2.7.1 + Checking rustix v1.1.4 + Checking generic-array v0.14.7 + Checking block-buffer v0.10.4 + Checking crypto-common v0.1.7 + Checking digest v0.10.7 + Checking sha2 v0.10.9 + Checking xattr v1.6.1 + Checking tar v0.4.46 + Checking serde v1.0.228 + Checking serde_json v1.0.150 + Checking yaml_serde v0.10.7 + Checking xtask v4.0.0 (/private/tmp/codebasegraph-fix-release-python-40453997235/crates/xtask) + Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.50s + Compiling xtask v4.0.0 (/private/tmp/codebasegraph-fix-release-python-40453997235/crates/xtask) + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.68s + Running `target/debug/xtask check-workflows` +workflow policy passed + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s + Running `target/debug/xtask release-gate` +release gate passed + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s + Running `target/debug/xtask release-gate --production --confirm release-environment --confirm private-vulnerability-reporting` +release gate passed diff --git a/docs/testing/release-gate-40453997235/nextest.toml b/docs/testing/release-gate-40453997235/nextest.toml new file mode 100644 index 00000000..dfda1d6a --- /dev/null +++ b/docs/testing/release-gate-40453997235/nextest.toml @@ -0,0 +1,2 @@ +[profile.cod99] +junit.path = "cod99-junit.xml" diff --git a/docs/testing/release-gate-40453997235/policy-failure-probes.log b/docs/testing/release-gate-40453997235/policy-failure-probes.log new file mode 100644 index 00000000..b4d7c08a --- /dev/null +++ b/docs/testing/release-gate-40453997235/policy-failure-probes.log @@ -0,0 +1,8 @@ +Python source probe (expected exit 1): +FAIL: python-file-present: docs/testing/graph-disk-cleanup-evidence/cod99-policy-probe.py must not be maintained source. +release gate failed + +Missing production confirmations (expected exit 1): +FAIL: external-confirmation-missing: production release requires --confirm release-environment. +FAIL: external-confirmation-missing: production release requires --confirm private-vulnerability-reporting. +release gate failed diff --git a/docs/testing/release-gate-40453997235/review.md b/docs/testing/release-gate-40453997235/review.md new file mode 100644 index 00000000..64f69583 --- /dev/null +++ b/docs/testing/release-gate-40453997235/review.md @@ -0,0 +1,54 @@ +# COD-99 read-only review + +**Result: no confirmed defects in the reviewed staged change.** + +- Reviewer: `openai-codex/gpt-6.1-sol`, `thinking="max"` (parent-confirmed actual spawn settings); fresh child `archival-ci-review`, `sub-4f7bb63f`. +- Ordinary small-CI/evidence-layout review; not a claimed large-implementation or adversarial-skill gate. +- Worktree: `/private/tmp/codebasegraph-fix-release-python-40453997235`, branch `fix/release-python-evidence-gate`. +- Baseline/HEAD: `7038e66ff8271ffddc511f879cf8d28353a09b2d`. +- Reviewed all 14 staged paths. No unstaged changes or reviewer implementation edits. No commits, integration mutations, external posts, dispatches or publication actions. + +## Confirmed scope and behavior + +1. The archive contains exactly four regular helper files plus their three parent directories. Paths are relative and confined to the original documentation subtree; no traversal, absolute paths, links, duplicate members or special files. Each member is byte-for-byte identical to a fresh `git archive` of the baseline and matches the manifest's original Git blob ID, SHA-256 and size. Historical Python bodies were not read or executed. +2. `historical-python-helpers.tar.gz` is staged despite the generic ignore rule: mode `100644`, blob `05ec406ef509a1bfd58673f67333a434f92c1ecc`, 15,810 bytes. All ten staged nondeleted file contents match their working files and the hashes below. Four loose `.py` files are deleted as intended. +3. `check_rust_only_files` is unchanged from the baseline. The new CI step runs the existing non-production gate before `cargo publish --dry-run --locked`, remains mandatory through the existing `required` dependency, and needs no production confirmation. The policy check and removal regression detect a missing gate command. Runtime, package manifests, release workflow, version, labels and release identity contracts are unchanged. +4. Existing historical reports, source manifests, test results/hashes and cleanup qualification claims are unchanged. The README correctly describes archival only, retains the canonical DC-S1–DC-S5 contract, and does not upgrade partial historical evidence to present qualification. +5. The original Release job remains bound to immutable source `7038e66`; rerunning it cannot apply this later fix. Replacement release/version selection, PR #156 replacement, tags/labels and publication remain outside the archival approval. + +## Executed and inspected checks + +Independently executed, all successful unless noted: + +- `cargo fmt --check`. +- `cargo test -p xtask --locked`: **43 passed, 0 failed, 0 ignored**. +- `cargo clippy -p xtask --all-targets --locked -- -D warnings`: passed without warnings. +- `cargo run -p xtask --locked -- check-workflows`. +- `cargo run -p xtask --locked -- release-gate`. +- `target/debug/xtask release-gate --production`: expected exit 1, rejecting both missing production confirmations. +- `git diff --cached --check`; index-vs-working-content verification; binary archive-vs-Git comparison and member safety checks. + +Inspected coordinator evidence under `docs/testing/release-gate-40453997235/`: both positive gate modes and Python reintroduction rejection, plus nextest JUnit run `caa02f03-1b42-4b8f-94c0-6b9fb63dedcf`. Parsed JUnit confirms **43 test cases, 0 failures/errors/skips**, and its digest matches `verification.json`. The baseline failure and source reintroduction probe were not independently reenacted by this read-only reviewer. + +## Exact reviewed SHA-256 snapshot + +Core files match `/private/tmp/codebasegraph-cod99-verification/review-snapshot.json`: + +- `.github/workflows/ci.yml`: `a2cc1e8eb18dee65eaa72f9fdfbfd7a9df23f3a071a4faa935e124a888c747bb` +- `crates/xtask/src/main.rs`: `70507a624361a97a7bcdb28412b31112258fcb023fa8e80dccd943b113b335ad` +- `docs/release.md`: `8c813f005f68e62e8dfeca0d4cdf8054a3802efcf9311ca865f9bdbbab508ecf` +- `docs/testing/graph-disk-cleanup-evidence/README.md`: `1c7212d00b8793b00a7672482bb77fee508fac8bbe80f0ee305eec12f4c3a7bc` +- `docs/testing/graph-disk-cleanup-evidence/historical-python-helpers.json`: `b9b750c6f8500064b9942d9d4fa413c949f4ca8075bb0e1564fd9aab2ecdfcbb` +- `docs/testing/graph-disk-cleanup-evidence/historical-python-helpers.tar.gz`: `032ac4ef9cfddf40dc0540a913f71c34de4901a4a7d61d035141cb96832e7519` +- `docs/testing/release-gate-40453997235/local-checks.log`: `a214c81efa0a33b1afa8cc75198b36ef9f7e2614b74209768b00e2566210105d` +- `docs/testing/release-gate-40453997235/policy-failure-probes.log`: `da54c62abec905f698741e1f82fed59a8106a0ed18ee24e47b4cc5782af4159e` +- `docs/testing/release-gate-40453997235/verification.json`: `aeb56c95afa98dd77c744cb285938bab6fecda0a7cb1d6a637c8d95985744889` +- `docs/testing/release-gate-40453997235/xtask.junit.xml`: `551ec04ad88fcf4885fa2b375a7a2c4eab8eba74dab28032b579dcab68ec4c18` + +## Limits and remaining gates + +- Local Darwin/aarch64 xtask scope only. Full product/native-platform matrix and package dry-run were not run locally; PR CI remains pending. Historical helper tests were not rerun, and the archive does not contain every historical environment prerequisite. +- Production flags reproduce the failed job's arguments; they do not independently verify owner-controlled external environment settings or authorize publishing. +- Graph and Scryer were consulted read-only against the older original checkout for orientation. The shared Scryer model is not configured for this implementation worktree. No JUnit ingestion, source/model drift reconciliation, seeding or copying was performed against mismatched source. Existing `resp-14`/`resp-15` responsibilities remain unchanged; model evidence synchronization is not claimed. +- Approval assumption: the parent's quoted user decision, “i approve archiving the python helpers”, governs this lossless archive; the requested CI failure repair covers the preventive existing-gate invocation. No broader release action is inferred. +- No constants changed. No new reusable memory candidate: this review confirms existing immutable-release guidance and directly rediscoverable snapshot facts, not a new durable procedure. diff --git a/docs/testing/release-gate-40453997235/verification.json b/docs/testing/release-gate-40453997235/verification.json new file mode 100644 index 00000000..054bd6e8 --- /dev/null +++ b/docs/testing/release-gate-40453997235/verification.json @@ -0,0 +1,116 @@ +{ + "work_item": "COD-99", + "baseline_commit": "7038e66ff8271ffddc511f879cf8d28353a09b2d", + "approval": { + "date": "2026-10-27", + "user_decision": "i approve archiving the python helpers", + "scope": "Archive four historical Python helpers losslessly. No release replacement or publication approval." + }, + "scope": "Historical evidence archival and required CI repository policy prevention; no architecture, source-policy exemption, runtime or package-payload changes.", + "environment": { + "platform": "Darwin aarch64", + "rustc": "1.96.0 (ac68faa20 2026-05-25)", + "cargo": "1.96.0 (30a34c682 2026-05-25)", + "cargo_nextest": "0.9.140" + }, + "snapshot_sha256": { + ".github/workflows/ci.yml": "a2cc1e8eb18dee65eaa72f9fdfbfd7a9df23f3a071a4faa935e124a888c747bb", + "crates/xtask/src/main.rs": "70507a624361a97a7bcdb28412b31112258fcb023fa8e80dccd943b113b335ad", + "docs/release.md": "8c813f005f68e62e8dfeca0d4cdf8054a3802efcf9311ca865f9bdbbab508ecf", + "docs/testing/graph-disk-cleanup-evidence/README.md": "1c7212d00b8793b00a7672482bb77fee508fac8bbe80f0ee305eec12f4c3a7bc", + "docs/testing/graph-disk-cleanup-evidence/historical-python-helpers.json": "b9b750c6f8500064b9942d9d4fa413c949f4ca8075bb0e1564fd9aab2ecdfcbb", + "docs/testing/graph-disk-cleanup-evidence/historical-python-helpers.tar.gz": "032ac4ef9cfddf40dc0540a913f71c34de4901a4a7d61d035141cb96832e7519" + }, + "archive_integrity": { + "sha256": "032ac4ef9cfddf40dc0540a913f71c34de4901a4a7d61d035141cb96832e7519", + "repeated_archive_byte_identical": true, + "files_checked": 4, + "source_bytes_checked_before_removal": true, + "no_extra_regular_members": true, + "safe_paths_and_types": true, + "extracted_hashes": [ + { + "path": "docs/testing/graph-disk-cleanup-evidence/audit.py", + "sha256": "316630c38e1638370b8c1873174d4311cc32ae4d0d9bcbbf0045e1083ccfb518", + "matched": true + }, + { + "path": "docs/testing/graph-disk-cleanup-evidence/qualification.py", + "sha256": "264753ae10581691d0dc1c7591acab9601b29ff7c9f95df8d04ed56b56509e3b", + "matched": true + }, + { + "path": "docs/testing/graph-disk-cleanup-evidence/test_audit.py", + "sha256": "b317ac2a5f4ab7ec3c52e98169e37090d65c9a7a92d397bd898962516f6f545c", + "matched": true + }, + { + "path": "docs/testing/graph-disk-cleanup-evidence/test_qualification.py", + "sha256": "4fa5adbdff51ec53085ae307444ca4800a557726b95e18cacfc42091c0c57ef3", + "matched": true + } + ] + }, + "checks": [ + { + "command": "cargo fmt --check", + "result": "passed" + }, + { + "command": "cargo test -p xtask --locked", + "result": "passed", + "tests": 43, + "failed": 0, + "ignored": 0 + }, + { + "command": "cargo clippy -p xtask --all-targets --locked -- -D warnings", + "result": "passed" + }, + { + "command": "cargo run -p xtask --locked -- check-workflows", + "result": "passed" + }, + { + "command": "cargo run -p xtask --locked -- release-gate", + "result": "passed" + }, + { + "command": "cargo run -p xtask --locked -- release-gate --production --confirm release-environment --confirm private-vulnerability-reporting", + "result": "passed", + "note": "Reproduces failed-job confirmation arguments; not new owner-controlled environment verification." + }, + { + "command": "cargo nextest run -p xtask --locked --config-file /private/tmp/codebasegraph-cod99-verification/nextest.toml --profile cod99", + "result": "passed", + "run_id": "caa02f03-1b42-4b8f-94c0-6b9fb63dedcf", + "tests": 43, + "failed": 0, + "skipped": 0, + "junit_path": "xtask.junit.xml", + "junit_sha256": "551ec04ad88fcf4885fa2b375a7a2c4eab8eba74dab28032b579dcab68ec4c18" + }, + { + "method": "Run target/debug/xtask release-gate with an empty loose Python source probe in the same evidence directory, then remove the probe", + "result": "expected failure: python-file-present", + "exit_code": 1 + }, + { + "command": "target/debug/xtask release-gate --production", + "result": "expected failure: both production confirmations missing", + "exit_code": 1 + } + ], + "limits": [ + "Historical helper tests were not rerun; exact bytes and extraction verified.", + "Full product/native platform matrix and package dry-run not executed locally; pending PR CI.", + "Existing failed job remains bound to immutable 7038e66. Corrected release replacement/version/publication require a separate decision.", + "Shared Scryer model is configured for the unrelated original worktree; no whole-project drift reconciliation or verdict activation against mismatched source." + ], + "scryer_change": "chg-cxz5az", + "scryer_existing_claims": [ + "resp-14", + "resp-15" + ], + "architecture_claims_changed": false +} diff --git a/docs/testing/release-gate-40453997235/xtask.junit.xml b/docs/testing/release-gate-40453997235/xtask.junit.xml new file mode 100644 index 00000000..ade658cf --- /dev/null +++ b/docs/testing/release-gate-40453997235/xtask.junit.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +