From dd524ec671ae78d90bd0ed695b46effeba1778cb Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Tue, 16 Jun 2026 13:59:36 +0530 Subject: [PATCH] Add Gate Zero evidence preflight Signed-off-by: docushell-admin --- .../scripts/gate_zero_evidence_preflight.py | 376 ++++++++++++++++++ .../test_gate_zero_evidence_preflight.py | 232 +++++++++++ .github/workflows/ci.yml | 2 + benchmarks/results/gate-zero/README.md | 17 +- docs/gate-zero-evidence-runbook.md | 16 + 5 files changed, 639 insertions(+), 4 deletions(-) create mode 100644 .github/scripts/gate_zero_evidence_preflight.py create mode 100644 .github/scripts/test_gate_zero_evidence_preflight.py diff --git a/.github/scripts/gate_zero_evidence_preflight.py b/.github/scripts/gate_zero_evidence_preflight.py new file mode 100644 index 0000000..6a07025 --- /dev/null +++ b/.github/scripts/gate_zero_evidence_preflight.py @@ -0,0 +1,376 @@ +#!/usr/bin/env python3 +"""Check the Gate Zero evidence handoff between ethos and ethos-bench. + +The checker does not generate benchmark results. It only reports whether the +source repository and sibling evidence repository are ready for a controlled run +or for the ADR-0005 decision review. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parent.parent.parent +TIMESTAMP_RE = re.compile(r"^[0-9]{8}T[0-9]{6}Z$") +REQUIRED_PLATFORMS = ("macos-arm64", "linux-x64") + + +@dataclass(frozen=True) +class ResultSpec: + platform: str + gate: str + result_path: Path + evidence_root: Path + schema_version: str + + +RESULT_SPECS = ( + ResultSpec( + platform="macos-arm64", + gate="g1", + result_path=Path("benchmarks/results/gate-zero/macos-arm64/g1.json"), + evidence_root=Path("benchmarks/results/gate-zero/macos-arm64/evidence/g1"), + schema_version="ethos-gate-zero-result-v1", + ), + ResultSpec( + platform="macos-arm64", + gate="g2", + result_path=Path("benchmarks/results/gate-zero/macos-arm64/g2.json"), + evidence_root=Path("benchmarks/results/gate-zero/macos-arm64/evidence/g2"), + schema_version="ethos-gate-zero-g2-result-v1", + ), + ResultSpec( + platform="linux-x64", + gate="g1", + result_path=Path("benchmarks/results/gate-zero/linux-x64/g1.json"), + evidence_root=Path("benchmarks/results/gate-zero/linux-x64/evidence/g1"), + schema_version="ethos-gate-zero-result-v1", + ), + ResultSpec( + platform="linux-x64", + gate="g2", + result_path=Path("benchmarks/results/gate-zero/linux-x64/g2.json"), + evidence_root=Path("benchmarks/results/gate-zero/linux-x64/evidence/g2"), + schema_version="ethos-gate-zero-g2-result-v1", + ), + ResultSpec( + platform="cross-platform", + gate="g3", + result_path=Path("benchmarks/results/gate-zero/g3.json"), + evidence_root=Path("benchmarks/results/gate-zero/cross-platform/evidence/g3"), + schema_version="ethos-gate-zero-g3-result-v1", + ), +) + + +class Gate: + def __init__(self) -> None: + self.failures: list[str] = [] + + def require(self, condition: bool, message: str) -> None: + if not condition: + self.failures.append(message) + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def load_json(path: Path) -> Any: + with path.open("r", encoding="utf-8") as handle: + return json.load(handle) + + +def rel(path: Path, root: Path) -> str: + try: + return path.relative_to(root).as_posix() + except ValueError: + return str(path) + + +def run_git(path: Path, args: list[str]) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["git", "-C", str(path), *args], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False, + ) + + +def check_no_generated_results_in_ethos(gate: Gate, repo_root: Path) -> None: + results_root = repo_root / "benchmarks" / "results" / "gate-zero" + if not results_root.exists(): + return + allowed = {(results_root / "README.md").resolve()} + generated = [ + path + for path in results_root.rglob("*") + if path.is_file() and path.resolve() not in allowed + ] + for path in sorted(generated): + gate.require( + False, + f"generated Gate Zero output must live in ethos-bench, not ethos: {rel(path, repo_root)}", + ) + + +def check_ethos_bench_checkout(gate: Gate, repo_root: Path, ethos_bench: Path) -> None: + if not ethos_bench.exists(): + gate.require(False, f"ethos-bench checkout does not exist: {ethos_bench}") + return + if not ethos_bench.is_dir(): + gate.require(False, f"ethos-bench path is not a directory: {ethos_bench}") + return + gate.require( + ethos_bench.resolve() != repo_root.resolve(), + "ethos-bench path must not point at the ethos repository", + ) + git_root = run_git(ethos_bench, ["rev-parse", "--show-toplevel"]) + gate.require( + git_root.returncode == 0, + f"ethos-bench path is not a Git checkout: {ethos_bench}", + ) + + +def check_ethos_bench_clean(gate: Gate, ethos_bench: Path) -> None: + if not ethos_bench.exists() or not ethos_bench.is_dir(): + return + status = run_git(ethos_bench, ["status", "--short"]) + if status.returncode != 0: + return + dirty = [line for line in status.stdout.splitlines() if line.strip()] + gate.require( + not dirty, + "ethos-bench checkout is not clean before controlled output generation", + ) + + +def validate_result_shape( + gate: Gate, + result: dict[str, Any], + spec: ResultSpec, + path: Path, +) -> None: + gate.require( + result.get("schema_version") == spec.schema_version, + f"{path} schema_version is not {spec.schema_version}", + ) + if spec.gate == "g1": + selected = result.get("host", {}).get("selected", {}) + platform = selected.get("platform") if isinstance(selected, dict) else None + gate.require(platform == spec.platform, f"{path} host platform is not {spec.platform}") + return + if spec.gate == "g2": + gate.require(result.get("gate") == "g2", f"{path} gate is not g2") + gate.require(result.get("platform") == spec.platform, f"{path} platform is not {spec.platform}") + return + if spec.gate == "g3": + platforms = result.get("platforms", []) + gate.require(result.get("gate") == "g3", f"{path} gate is not g3") + gate.require( + all(platform in platforms for platform in REQUIRED_PLATFORMS), + f"{path} does not include required G3 platforms: {', '.join(REQUIRED_PLATFORMS)}", + ) + + +def load_result_for_spec(gate: Gate, ethos_bench: Path, spec: ResultSpec) -> dict[str, Any] | None: + path = ethos_bench / spec.result_path + if not path.is_file(): + gate.require(False, f"missing Gate Zero {spec.gate.upper()} result: {spec.result_path}") + return None + try: + result = load_json(path) + except json.JSONDecodeError as exc: + gate.require(False, f"invalid JSON in {spec.result_path}: {exc}") + return None + if not isinstance(result, dict): + gate.require(False, f"{spec.result_path} must be a JSON object") + return None + validate_result_shape(gate, result, spec, spec.result_path) + return result + + +def parse_checksum_line(line: str, index: int, bundle_dir: Path) -> tuple[str, Path] | str: + try: + expected, relative_path = line.split(" ", 1) + except ValueError: + return f"{bundle_dir / 'SHA256SUMS'} line {index} is malformed" + if not re.fullmatch(r"[0-9a-f]{64}", expected): + return f"{bundle_dir / 'SHA256SUMS'} line {index} has invalid SHA256" + return expected, bundle_dir / relative_path + + +def verify_checksum_manifest(bundle_dir: Path) -> list[str]: + checksum_path = bundle_dir / "SHA256SUMS" + failures: list[str] = [] + if not checksum_path.is_file(): + return [f"{checksum_path} is missing"] + for index, line in enumerate(checksum_path.read_text(encoding="utf-8").splitlines(), 1): + if not line.strip(): + continue + parsed = parse_checksum_line(line, index, bundle_dir) + if isinstance(parsed, str): + failures.append(parsed) + continue + expected, path = parsed + if not path.is_file(): + failures.append(f"{path} is missing") + continue + actual = sha256_file(path) + if actual != expected: + failures.append(f"{path} sha256 mismatch: expected={expected} actual={actual}") + return failures + + +def validate_bundle( + bundle_dir: Path, + spec: ResultSpec, + result_sha256: str, +) -> list[str]: + failures: list[str] = [] + required = [ + "SUMMARY.md", + "host-attestation.json", + "evidence-manifest.json", + "reproduction-command.txt", + "reproduction-env.json", + "SHA256SUMS", + "SHA256SUMS.digest.json", + ] + for name in required: + if not (bundle_dir / name).is_file(): + failures.append(f"{bundle_dir / name} is missing") + + raw_dir = bundle_dir / "raw" + if not raw_dir.is_dir() or not list(raw_dir.glob("*.json")): + failures.append(f"{raw_dir} is missing a raw result JSON archive") + + try: + reproduction_env = load_json(bundle_dir / "reproduction-env.json") + except (FileNotFoundError, json.JSONDecodeError) as exc: + failures.append(f"{bundle_dir / 'reproduction-env.json'} is invalid: {exc}") + else: + if not isinstance(reproduction_env, dict): + failures.append(f"{bundle_dir / 'reproduction-env.json'} must be a JSON object") + elif reproduction_env.get("status") != "complete": + failures.append(f"{bundle_dir / 'reproduction-env.json'} status is not complete") + + try: + manifest = load_json(bundle_dir / "evidence-manifest.json") + except (FileNotFoundError, json.JSONDecodeError) as exc: + failures.append(f"{bundle_dir / 'evidence-manifest.json'} is invalid: {exc}") + else: + if not isinstance(manifest, dict): + failures.append(f"{bundle_dir / 'evidence-manifest.json'} must be a JSON object") + else: + if manifest.get("gate") != spec.gate: + failures.append(f"{bundle_dir / 'evidence-manifest.json'} gate is not {spec.gate}") + if manifest.get("platform") != spec.platform: + failures.append( + f"{bundle_dir / 'evidence-manifest.json'} platform is not {spec.platform}" + ) + if manifest.get("source_result_sha256") != result_sha256: + failures.append( + f"{bundle_dir / 'evidence-manifest.json'} source_result_sha256 does not match {spec.result_path}" + ) + + failures.extend(verify_checksum_manifest(bundle_dir)) + + digest_path = bundle_dir / "SHA256SUMS.digest.json" + try: + digest = load_json(digest_path) + except (FileNotFoundError, json.JSONDecodeError) as exc: + failures.append(f"{digest_path} is invalid: {exc}") + else: + if not isinstance(digest, dict): + failures.append(f"{digest_path} must be a JSON object") + elif (bundle_dir / "SHA256SUMS").is_file() and digest.get( + "payload_sha256" + ) != sha256_file(bundle_dir / "SHA256SUMS"): + failures.append(f"{digest_path} payload_sha256 does not match SHA256SUMS") + + return failures + + +def check_evidence_bundle(gate: Gate, ethos_bench: Path, spec: ResultSpec) -> None: + result_path = ethos_bench / spec.result_path + if not result_path.is_file(): + return + result_sha256 = sha256_file(result_path) + evidence_root = ethos_bench / spec.evidence_root + if not evidence_root.is_dir(): + gate.require(False, f"missing Gate Zero {spec.gate.upper()} evidence root: {spec.evidence_root}") + return + bundles = sorted( + path + for path in evidence_root.iterdir() + if path.is_dir() and TIMESTAMP_RE.fullmatch(path.name) + ) + if not bundles: + gate.require(False, f"no timestamped evidence bundle under {spec.evidence_root}") + return + valid_bundle_count = 0 + latest_failures: list[str] = [] + for bundle_dir in bundles: + failures = validate_bundle(bundle_dir, spec, result_sha256) + if failures: + latest_failures = failures + else: + valid_bundle_count += 1 + if valid_bundle_count == 0: + gate.require(False, f"no complete evidence bundle under {spec.evidence_root}") + for failure in latest_failures[:8]: + gate.require(False, failure) + + +def run(mode: str, *, repo_root: Path = ROOT, ethos_bench: Path | None = None) -> Gate: + repo_root = repo_root.resolve() + ethos_bench = (ethos_bench or repo_root.parent / "ethos-bench").resolve() + gate = Gate() + check_no_generated_results_in_ethos(gate, repo_root) + check_ethos_bench_checkout(gate, repo_root, ethos_bench) + if mode == "prepare": + check_ethos_bench_clean(gate, ethos_bench) + return gate + for spec in RESULT_SPECS: + load_result_for_spec(gate, ethos_bench, spec) + for spec in RESULT_SPECS: + check_evidence_bundle(gate, ethos_bench, spec) + return gate + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("mode", choices=["prepare", "decision"]) + parser.add_argument("--repo-root", type=Path, default=ROOT) + parser.add_argument("--ethos-bench", type=Path) + parser.add_argument("--report-only", action="store_true") + args = parser.parse_args() + + gate = run(args.mode, repo_root=args.repo_root, ethos_bench=args.ethos_bench) + label = f"gate-zero evidence {args.mode}" + if gate.failures: + print(f"{label}: BLOCKED") + for failure in gate.failures: + print(f"- {failure}") + return 0 if args.report_only else 1 + print(f"{label}: green") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/scripts/test_gate_zero_evidence_preflight.py b/.github/scripts/test_gate_zero_evidence_preflight.py new file mode 100644 index 0000000..df7a0d7 --- /dev/null +++ b/.github/scripts/test_gate_zero_evidence_preflight.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import importlib.util +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +SCRIPT = Path(__file__).resolve().with_name("gate_zero_evidence_preflight.py") +SPEC = importlib.util.spec_from_file_location("gate_zero_evidence_preflight", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +preflight = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = preflight +SPEC.loader.exec_module(preflight) + +HEX = "a" * 64 +TIMESTAMP = "20260616T120000Z" + + +def write_json(path: Path, value: object) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, sort_keys=True), encoding="utf-8") + + +def write_text(path: Path, value: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(value, encoding="utf-8") + + +def init_git(path: Path) -> None: + path.mkdir(parents=True, exist_ok=True) + subprocess.run(["git", "-C", str(path), "init"], check=True, stdout=subprocess.DEVNULL) + subprocess.run( + ["git", "-C", str(path), "config", "user.email", "test@example.com"], + check=True, + ) + subprocess.run( + ["git", "-C", str(path), "config", "user.name", "Test User"], + check=True, + ) + write_text(path / "README.md", "# ethos-bench\n") + subprocess.run(["git", "-C", str(path), "add", "README.md"], check=True) + subprocess.run( + ["git", "-C", str(path), "commit", "-m", "init"], + check=True, + stdout=subprocess.DEVNULL, + ) + + +def g1_result(platform: str) -> dict[str, object]: + return { + "schema_version": "ethos-gate-zero-result-v1", + "status": "pass", + "host": {"selected": {"platform": platform}}, + "summary": {"status": "pass"}, + } + + +def g2_result(platform: str) -> dict[str, object]: + return { + "schema_version": "ethos-gate-zero-g2-result-v1", + "gate": "g2", + "status": "pass", + "platform": platform, + "summary": {"status": "pass"}, + } + + +def g3_result() -> dict[str, object]: + return { + "schema_version": "ethos-gate-zero-g3-result-v1", + "gate": "g3", + "status": "pass", + "platforms": ["macos-arm64", "linux-x64"], + "summary": {"status": "pass"}, + } + + +def result_for(spec: preflight.ResultSpec) -> dict[str, object]: + if spec.gate == "g1": + return g1_result(spec.platform) + if spec.gate == "g2": + return g2_result(spec.platform) + return g3_result() + + +def write_complete_bundle(bench: Path, spec: preflight.ResultSpec) -> None: + result_path = bench / spec.result_path + result_sha256 = preflight.sha256_file(result_path) + bundle = bench / spec.evidence_root / TIMESTAMP + write_text(bundle / "SUMMARY.md", "summary\n") + write_text(bundle / "reproduction-command.txt", "make gate-zero\n") + write_json( + bundle / "reproduction-env.json", + { + "schema_version": "ethos-gate-zero-reproduction-env-v1", + "status": "complete", + "variables": [], + "blockers": [], + }, + ) + write_json(bundle / "host-attestation.json", {"source_result_sha256": result_sha256}) + write_json( + bundle / "evidence-manifest.json", + { + "schema_version": "ethos-gate-zero-evidence-v1", + "gate": spec.gate, + "platform": spec.platform, + "source_result_sha256": result_sha256, + }, + ) + write_json(bundle / "raw" / f"{spec.gate}-{spec.platform}-{TIMESTAMP}.json", result_for(spec)) + checksum_rows = [] + for path in sorted( + [ + bundle / "SUMMARY.md", + bundle / "reproduction-command.txt", + bundle / "reproduction-env.json", + bundle / "host-attestation.json", + bundle / "evidence-manifest.json", + bundle / "raw" / f"{spec.gate}-{spec.platform}-{TIMESTAMP}.json", + ], + key=lambda item: item.relative_to(bundle).as_posix(), + ): + checksum_rows.append( + f"{preflight.sha256_file(path)} {path.relative_to(bundle).as_posix()}\n" + ) + write_text(bundle / "SHA256SUMS", "".join(checksum_rows)) + write_json( + bundle / "SHA256SUMS.digest.json", + { + "schema_version": "ethos-gate-zero-checksum-digest-v1", + "digest_type": "sha256", + "payload": "SHA256SUMS", + "payload_sha256": preflight.sha256_file(bundle / "SHA256SUMS"), + }, + ) + + +class GateZeroEvidencePreflightTests(unittest.TestCase): + def test_prepare_blocks_generated_results_in_ethos_and_missing_bench(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) / "ethos" + write_text(root / "benchmarks/results/gate-zero/README.md", "# pointer\n") + write_json(root / "benchmarks/results/gate-zero/macos-arm64/g1.json", {}) + + gate = preflight.run("prepare", repo_root=root, ethos_bench=Path(tmp) / "ethos-bench") + + self.assertTrue( + any("generated Gate Zero output must live in ethos-bench" in f for f in gate.failures) + ) + self.assertTrue(any("ethos-bench checkout does not exist" in f for f in gate.failures)) + + def test_prepare_requires_clean_ethos_bench_checkout(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) / "ethos" + write_text(root / "benchmarks/results/gate-zero/README.md", "# pointer\n") + bench = Path(tmp) / "ethos-bench" + init_git(bench) + write_text(bench / "untracked.txt", "dirty\n") + + gate = preflight.run("prepare", repo_root=root, ethos_bench=bench) + + self.assertIn( + "ethos-bench checkout is not clean before controlled output generation", + gate.failures, + ) + + def test_decision_blocks_missing_result_files(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) / "ethos" + write_text(root / "benchmarks/results/gate-zero/README.md", "# pointer\n") + bench = Path(tmp) / "ethos-bench" + init_git(bench) + + gate = preflight.run("decision", repo_root=root, ethos_bench=bench) + + self.assertTrue(any("missing Gate Zero G1 result" in f for f in gate.failures)) + self.assertTrue(any("missing Gate Zero G2 result" in f for f in gate.failures)) + self.assertTrue(any("missing Gate Zero G3 result" in f for f in gate.failures)) + + def test_decision_green_with_complete_results_and_bundles(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) / "ethos" + write_text(root / "benchmarks/results/gate-zero/README.md", "# pointer\n") + bench = Path(tmp) / "ethos-bench" + init_git(bench) + for spec in preflight.RESULT_SPECS: + write_json(bench / spec.result_path, result_for(spec)) + write_complete_bundle(bench, spec) + + gate = preflight.run("decision", repo_root=root, ethos_bench=bench) + + self.assertEqual(gate.failures, []) + + def test_decision_blocks_incomplete_evidence_bundle(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) / "ethos" + write_text(root / "benchmarks/results/gate-zero/README.md", "# pointer\n") + bench = Path(tmp) / "ethos-bench" + init_git(bench) + for spec in preflight.RESULT_SPECS: + write_json(bench / spec.result_path, result_for(spec)) + write_complete_bundle(bench, spec) + env_path = ( + bench + / "benchmarks/results/gate-zero/macos-arm64/evidence/g1" + / TIMESTAMP + / "reproduction-env.json" + ) + write_json( + env_path, + { + "schema_version": "ethos-gate-zero-reproduction-env-v1", + "status": "incomplete", + "variables": [], + "blockers": ["missing env"], + }, + ) + + gate = preflight.run("decision", repo_root=root, ethos_bench=bench) + + self.assertTrue(any("status is not complete" in failure for failure in gate.failures)) + self.assertTrue(any("sha256 mismatch" in failure for failure in gate.failures)) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fea867a..54c3c32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,8 @@ jobs: run: python3 fixtures/validate_fixtures.py - name: readiness gate tests run: python3 .github/scripts/test_readiness_gate.py + - name: Gate Zero evidence preflight tests + run: python3 .github/scripts/test_gate_zero_evidence_preflight.py - name: Gate Zero harness tests run: python3 benchmarks/harness/test_run_gate_zero.py - name: same-platform double-parse byte-diff diff --git a/benchmarks/results/gate-zero/README.md b/benchmarks/results/gate-zero/README.md index ddf81a5..ddfe970 100644 --- a/benchmarks/results/gate-zero/README.md +++ b/benchmarks/results/gate-zero/README.md @@ -4,10 +4,17 @@ Generated Gate Zero result files and evidence bundles now belong in the sibling `ethos-bench` repository. This `ethos` directory is only a pointer kept for repository-boundary clarity. -Platform-scoped Gate Zero results are produced and stored in `ethos-bench` as: +Platform-scoped G1/G2 Gate Zero results are produced and stored in `ethos-bench` as: ```text -/{g1,g2,g3}.json +/g1.json +/g2.json +``` + +The cross-host G3 comparison is stored once at: + +```text +g3.json ``` G2/G3 result files must cite the active gate definition hash from: @@ -20,10 +27,12 @@ G3 is a cross-platform gate. Platform-local G3 evidence is not sufficient for a until the required `macos-arm64` and `linux-x64` comparisons show zero stable-payload-projection and fingerprint divergences. -Publishable evidence sidecars are generated from saved result JSON and land under: +Evidence sidecars are generated from saved result JSON and land under: ```text -/evidence/// +/evidence/g1// +/evidence/g2// +cross-platform/evidence/g3// ``` Each evidence bundle contains the raw result archive, reproduction command, reproduction diff --git a/docs/gate-zero-evidence-runbook.md b/docs/gate-zero-evidence-runbook.md index 1ea6742..cceaf5b 100644 --- a/docs/gate-zero-evidence-runbook.md +++ b/docs/gate-zero-evidence-runbook.md @@ -43,6 +43,7 @@ git switch main git pull --ff-only make verify-alpha PYTHON=/private/tmp/ethos-jsonschema-venv/bin/python python3 .github/scripts/readiness_gate.py gate-zero +python3 .github/scripts/gate_zero_evidence_preflight.py prepare --ethos-bench ../ethos-bench make -C benchmarks/harness smoke make -C benchmarks/harness test git status --short --branch @@ -50,6 +51,9 @@ git status --short --branch The `readiness_gate.py gate-zero` command only checks that frozen inputs and pins are present. It does not produce benchmark results. +The `gate_zero_evidence_preflight.py prepare` command checks that generated Gate Zero outputs are +not present in this repository and that the sibling `ethos-bench` checkout is ready to receive +controlled-run output. ## Per-Host G1 Result @@ -127,6 +131,8 @@ make -C "$ETHOS_REPO/benchmarks/harness" gate-zero-evidence \ Repeat for G2 and G3 with the matching gate/result paths. Reproduction command and environment sidecars must describe the actual controlled run; placeholders keep the bundle incomplete. +For the cross-host G3 bundle, use `GATE_ZERO_PLATFORM=cross-platform` and +`GATE_ZERO_GATE=g3`. ## Decision Step @@ -138,6 +144,16 @@ Fill `docs/decisions/ADR-0005-gate-zero-decision.md` only after: - evidence bundles exist for the source result files; - the decider has reviewed the result JSON and reproduction sidecars. +Before filling the ADR, run: + +```bash +python3 .github/scripts/gate_zero_evidence_preflight.py decision --ethos-bench ../ethos-bench +``` + +This checks the expected `ethos-bench` result paths, timestamped evidence bundle sidecars, +complete reproduction environments, and bundle checksum manifests. It does not decide whether +Gate Zero passes. + Until that ADR is filled, public language remains: ```text