#!/usr/bin/env python3 """API-only GWAS extension-stage benchmark runner. This runner deliberately avoids archetype_spec monkey patches. Codex may use it to submit and record experiments, but API conversion/repair must remain the only source of spec changes during benchmark runs. """ from __future__ import annotations import argparse import concurrent.futures import csv import json import os import shutil import sys import time import traceback from dataclasses import dataclass from datetime import datetime from pathlib import Path from typing import Any PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) DEFAULT_BASELINE_ROOT = PROJECT_ROOT / "项目记录" / "GWAS拓展分析阶段_发表级完整实验_20260622" DEFAULT_OUTPUT_ROOT = PROJECT_ROOT / "项目记录" / "GWAS拓展分析阶段_API20_Benchmark_20260622" @dataclass(frozen=True) class ApiCase: dir_name: str case_id: str archetype_id: str script_name: str CASES: tuple[ApiCase, ...] = ( ApiCase("01_gwas_trait_vcf_qc", "gwas_trait_vcf_qc", "gwas_trait_vcf_qc", "run_gwas_trait_vcf_qc.sh"), ApiCase("02_gwas_vcf_filter_index", "gwas_vcf_filter_index", "gwas_vcf_filter_index", "run_gwas_vcf_filter_index.sh"), ApiCase("03_gwas_trait_prepare_match", "gwas_trait_prepare_match", "gwas_trait_prepare_match", "run_gwas_trait_prepare_match.sh"), ApiCase("04_gwas_plink_pca", "gwas_plink_pca", "gwas_plink_pca", "run_gwas_plink_pca.sh"), ApiCase("05_gwas_trait_blup_format", "gwas_trait_blup_format", "gwas_trait_blup_format", "run_gwas_trait_blup_format.sh"), ApiCase("06_gwas_plink_impute", "gwas_plink_impute", "gwas_plink_impute", "run_gwas_plink_impute.sh"), ApiCase("07_gwas_tassel_compatible", "gwas_tassel_compatible", "gwas_tassel_compatible", "run_gwas_tassel_compatible.sh"), ApiCase("08_gwas_gemma_assoc", "gwas_gemma_assoc", "gwas_gemma_assoc", "run_gwas_gemma_assoc.sh"), ) def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--baseline-root", type=Path, default=DEFAULT_BASELINE_ROOT) parser.add_argument("--output-root", type=Path, default=DEFAULT_OUTPUT_ROOT) parser.add_argument("--runtime-env", default="ASAW") parser.add_argument("--runs", type=int, default=20) parser.add_argument("--start-run", type=int, default=1, help="First run index to execute; use for resuming an interrupted benchmark.") parser.add_argument("--jobs", type=int, default=1, help="Number of independent run_id/case tasks to execute concurrently.") parser.add_argument("--case", action="append", dest="case_ids", help="Run only selected case_id; repeatable.") parser.add_argument("--freeze-baseline", action="store_true") parser.add_argument("--init-fixture", action="store_true") parser.add_argument("--dry-run", action="store_true") parser.add_argument("--run", action="store_true") parser.add_argument("--report", action="store_true") args = parser.parse_args() if args.freeze_baseline: freeze_baseline(args.baseline_root) if args.init_fixture: init_fixture(args.baseline_root, args.output_root) if args.dry_run: dry_run(args.output_root, selected_cases(args.case_ids)) if args.run: run_api20(args.output_root, selected_cases(args.case_ids), runs=args.runs, runtime_env=args.runtime_env, start_run=args.start_run, jobs=args.jobs) if args.report: write_report(args.output_root) if not any((args.freeze_baseline, args.init_fixture, args.dry_run, args.run, args.report)): parser.print_help() return 0 def freeze_baseline(baseline_root: Path) -> None: baseline_root.mkdir(parents=True, exist_ok=True) code_backup = baseline_root / "code_backup" code_backup.mkdir(parents=True, exist_ok=True) src = PROJECT_ROOT / "scripts" / "run_gwas_publication_experiments.py" if src.exists(): shutil.copy2(src, code_backup / "run_gwas_publication_experiments.codex_adjusted.py") classification = baseline_root / "EXPERIMENT_CLASSIFICATION.md" classification.write_text( "\n".join( [ "# GWAS Extension Experiment Classification", "", f"- frozen_at: `{now()}`", "- classification: `Codex-adjusted deterministic baseline`", "- benchmark_status: `not_api20_benchmark`", "- template_eligibility: `not_eligible`", "- reason: this directory contains archetype_spec/deterministic baseline runs and Codex-adjusted harness logic.", "- allowed_use: fixture design, tool preflight evidence, raw step sanity checks, GWAS archetype design reference.", "- disallowed_use: API autonomous success, API20 success-rate denominator/numerator, direct template admission evidence.", "", "Formal API-only benchmark results must be written to a separate `GWAS拓展分析阶段_API20_Benchmark_*` directory.", "", ] ), encoding="utf-8", ) write_json( baseline_root / "experiment_classification.json", { "schema_version": "gwas_experiment_classification_v1", "created_at": now(), "classification": "codex_adjusted_deterministic_baseline", "benchmark_status": "not_api20_benchmark", "template_eligibility": "not_eligible", "code_backup": str(code_backup / "run_gwas_publication_experiments.codex_adjusted.py"), }, ) def init_fixture(baseline_root: Path, output_root: Path) -> None: fixture_root = output_root / "fixtures" / "packages" fixture_root.mkdir(parents=True, exist_ok=True) rows = [] for case in CASES: src = baseline_root / "packages" / case.dir_name dst = fixture_root / case.dir_name if not src.exists(): rows.append({"case_id": case.case_id, "status": "missing_source", "source": str(src), "target": str(dst)}) continue if not dst.exists(): shutil.copytree(src, dst) rows.append( { "case_id": case.case_id, "archetype_id": case.archetype_id, "status": "ready" if dst.exists() else "failed", "source": str(src), "target": str(dst), "script": case.script_name, "raw_code_files": inventory(dst / "raw_code"), "raw_test_data_files": inventory(dst / "raw_test_data"), "expected_output_files": inventory(dst / "expected_outputs"), } ) write_json( output_root / "fixture_manifest.json", { "schema_version": "gwas_api20_fixture_manifest_v1", "created_at": now(), "source_baseline_root": str(baseline_root), "fixture_root": str(fixture_root), "codex_prepared_fixture": True, "fixture_policy": "frozen_before_api20; do not modify during API benchmark runs", "cases": rows, }, ) (output_root / "README.md").write_text( "\n".join( [ "# GWAS API20 Benchmark", "", "This directory is reserved for pure API extension-stage benchmark runs.", "", "- Codex submits runs and collects metrics only.", "- API conversion/repair is the only source of spec changes.", "- Deterministic archetype_spec results are not counted here.", "- Fixture files are copied from the frozen Codex-adjusted baseline and must not be modified during API20.", "", ] ), encoding="utf-8", ) def dry_run(output_root: Path, cases: tuple[ApiCase, ...]) -> None: from gfa_core.extension_stage.template_gate import evaluate_template_gate fixture_root = output_root / "fixtures" / "packages" rows = [] for case in cases: root = fixture_root / case.dir_name raw_code = root / "raw_code" / case.script_name raw_data = root / "raw_test_data" expected = root / "expected_outputs" extension = { "extension_id": f"dry_run_{case.case_id}", "archetype_id": case.archetype_id, "analysis_type": "generic_cli", "tool_family": "generic", "input_roles": {"input_file": {"role": "generic_input_file", "required": True}}, "output_contract": output_contract_from_expected(expected), "golden_comparison": {"status": "passed"}, } gate = evaluate_template_gate(extension) rows.append( { "case_id": case.case_id, "fixture_exists": root.exists(), "raw_code_exists": raw_code.exists(), "raw_data_file_count": len(inventory(raw_data)), "expected_output_count": len(inventory(expected)), "template_gate_status": gate.get("status", ""), "template_gate_errors": "; ".join(gate.get("errors") or []), } ) write_json(output_root / "dry_run.json", {"schema_version": "gwas_api20_dry_run_v1", "created_at": now(), "rows": rows}) write_csv(output_root / "dry_run.csv", rows) def run_api20(output_root: Path, cases: tuple[ApiCase, ...], *, runs: int, runtime_env: str, start_run: int = 1, jobs: int = 1) -> None: from gfa_core.extension_stage import ExtensionStageAgent, ExtensionStageManager os.environ["BBAV5_EXTENSION_CONVERTER"] = "deepseek_api" jobs = max(1, int(jobs or 1)) if jobs > 1: run_api20_parallel(output_root, cases, runs=runs, runtime_env=runtime_env, start_run=start_run, jobs=jobs) return rows = existing_result_rows(output_root) for run_index in range(start_run, runs + 1): run_id = f"api_run_{run_index:03d}" for case in cases: run_case_root = output_root / "runs" / run_id / case.case_id existing_row = run_case_root / "result_row.json" if existing_row.exists(): continue run_case_root.mkdir(parents=True, exist_ok=True) manager = ExtensionStageManager( PROJECT_ROOT, store_root=run_case_root / "extension_stages", workspace_root=run_case_root / "workspace", input_root=run_case_root / "input", ) row = run_one_case(ExtensionStageAgent(manager), output_root, run_case_root, run_id, case, runtime_env) rows.append(row) write_json(run_case_root / "result_row.json", row) rows = existing_result_rows(output_root) write_partial_summary(output_root, rows) write_summary(output_root, existing_result_rows(output_root)) def run_api20_parallel(output_root: Path, cases: tuple[ApiCase, ...], *, runs: int, runtime_env: str, start_run: int, jobs: int) -> None: os.environ["BBAV5_EXTENSION_CONVERTER"] = "deepseek_api" tasks: list[tuple[str, ApiCase]] = [] for run_index in range(start_run, runs + 1): run_id = f"api_run_{run_index:03d}" for case in cases: run_case_root = output_root / "runs" / run_id / case.case_id if (run_case_root / "result_row.json").exists(): continue tasks.append((run_id, case)) if not tasks: write_summary(output_root, existing_result_rows(output_root)) return with concurrent.futures.ProcessPoolExecutor(max_workers=jobs) as pool: future_to_task = { pool.submit(run_one_case_worker, output_root, run_id, case, runtime_env): (run_id, case) for run_id, case in tasks } for future in concurrent.futures.as_completed(future_to_task): run_id, case = future_to_task[future] run_case_root = output_root / "runs" / run_id / case.case_id try: row = future.result() except Exception as exc: row = base_row(run_id, case, runtime_env) row["error"] = str(exc) row["first_failure_phase"] = infer_failure_phase(row) row = finalize_row(row, time.time()) run_case_root.mkdir(parents=True, exist_ok=True) write_json(run_case_root / "result_row.json", row) write_partial_summary(output_root, existing_result_rows(output_root)) write_summary(output_root, existing_result_rows(output_root)) def run_one_case_worker(output_root: Path, run_id: str, case: ApiCase, runtime_env: str) -> dict[str, Any]: from gfa_core.extension_stage import ExtensionStageAgent, ExtensionStageManager os.environ["BBAV5_EXTENSION_CONVERTER"] = "deepseek_api" run_case_root = output_root / "runs" / run_id / case.case_id existing_row = run_case_root / "result_row.json" if existing_row.exists(): return read_json(existing_row) run_case_root.mkdir(parents=True, exist_ok=True) manager = ExtensionStageManager( PROJECT_ROOT, store_root=run_case_root / "extension_stages", workspace_root=run_case_root / "workspace", input_root=run_case_root / "input", ) row = run_one_case(ExtensionStageAgent(manager), output_root, run_case_root, run_id, case, runtime_env) write_json(run_case_root / "result_row.json", row) return row def run_one_case(agent: Any, output_root: Path, run_case_root: Path, run_id: str, case: ApiCase, runtime_env: str) -> dict[str, Any]: fixture_case = output_root / "fixtures" / "packages" / case.dir_name row = base_row(run_id, case, runtime_env) started = time.time() try: build = agent.create_from_uploads( display_name=f"gwas_api20_{case.case_id}_{run_id}", command=f"bash {case.script_name}", runtime_env=runtime_env, code_files=upload_files(fixture_case / "raw_code"), data_files=upload_files(fixture_case / "raw_test_data"), description=f"API-only GWAS benchmark case {case.case_id}", ) write_json(run_case_root / "build_response.json", build) ext = build.get("extension") or {} eid = str(ext.get("extension_id") or "") row["extension_id"] = eid row["build_success"] = bool(build.get("success") and eid) row["sanitize_success"] = row["build_success"] row["spec_validate_success"] = row["build_success"] row["wrapper_success"] = row["build_success"] row["build_api_attempt_count"] = len(ext.get("build_attempts") or []) row["sanitizer_action_count"] = len(ext.get("sanitizer_actions") or []) if not row["build_success"]: row["first_failure_phase"] = "build" return finalize_row(row, started) trial = agent.trial_run(eid) write_json(run_case_root / "trial_run.json", trial) if not trial.get("success"): repair = agent.repair_from_context(eid, phase="trial", run_context=trial.get("run") or {}, max_attempts=20, auto_publish=False) write_json(run_case_root / "trial_repair_response.json", repair) row["trial_repair_attempt_count"] = int(repair.get("attempts") or 0) if repair.get("success"): trial = {"success": True, "extension": repair.get("extension") or {}, "run": repair.get("run") or {}} else: row["first_failure_phase"] = "trial" row["last_error"] = str(repair.get("last_error") or "") return finalize_row(row, started) row["trial_success"] = bool(trial.get("success")) row["output_count"] = len((trial.get("run") or {}).get("outputs") or []) outputs = (trial.get("extension") or {}).get("outputs_detected") or (trial.get("run") or {}).get("outputs") or [] confirm = agent.confirm(eid, outputs=outputs, nl_triggers=[case.case_id, case.archetype_id, "gwas"]) write_json(run_case_root / "confirm_response.json", confirm) row["confirm_success"] = bool(confirm.get("success")) if not row["confirm_success"]: row["first_failure_phase"] = "confirm" return finalize_row(row, started) val = agent.validate(eid) write_json(run_case_root / "validate_response.json", val) run = val.get("run") or {} row.update(validation_status(val)) if not val.get("success"): repair = agent.repair_from_context(eid, phase="validation", run_context=run, max_attempts=20, auto_publish=False) write_json(run_case_root / "validation_repair_response.json", repair) row["validation_repair_attempt_count"] = int(repair.get("attempts") or 0) if repair.get("success"): val = {"success": True, "extension": repair.get("extension") or {}, "run": repair.get("run") or {}} row.update(validation_status(val)) else: row["first_failure_phase"] = "validation" row["last_error"] = str(repair.get("last_error") or "") return finalize_row(row, started) try: pub = agent.publish(eid) write_json(run_case_root / "publish_response.json", pub) row["publish_gate_status"] = "passed" row["publish_success"] = bool(pub.get("success")) row["final_publish_status"] = str((pub.get("extension") or {}).get("status") or "") except Exception as pub_exc: publish_context = { "repair_phase": "publish_gate", "status": "failed", "error": str(pub_exc), "publish_checks": (agent.load(eid).get("publish_checks") or {}) if hasattr(agent, "load") else {}, "last_validation": (val.get("run") or {}), "golden_comparison": (val.get("run") or {}).get("golden_comparison") or {}, "template_gate": (val.get("run") or {}).get("template_gate") or {}, } row["publish_gate_status"] = "failed" repair = agent.repair_from_context(eid, phase="publish_gate", run_context=publish_context, max_attempts=20, auto_publish=True) write_json(run_case_root / "publish_repair_response.json", repair) row["publish_gate_repair_attempt_count"] = int(repair.get("attempts") or 0) if repair.get("success") and repair.get("published"): row["publish_gate_status"] = "passed" row["publish_success"] = True row["final_publish_status"] = "published" else: row["first_failure_phase"] = "publish_gate" row["last_error"] = str(repair.get("last_error") or str(pub_exc)) return finalize_row(row, started) if row["publish_success"]: job_id = f"{run_id}_{case.case_id}_published" materialize_job_inputs(agent, eid, job_id, fixture_case / "raw_test_data") published = agent.run(extension_id=eid, job_id=job_id) write_json(run_case_root / "published_run_response.json", published) row["published_run_success"] = bool(published.get("success")) if not row["published_run_success"]: row["first_failure_phase"] = "published_run" row["last_error"] = str((published.get("run") or {}).get("error") or "") except Exception as exc: row["error"] = str(exc) row["traceback"] = traceback.format_exc() if not row["first_failure_phase"]: row["first_failure_phase"] = infer_failure_phase(row) write_json(run_case_root / "error.json", {"error": str(exc), "traceback": row["traceback"], "row": row}) return finalize_row(row, started) def materialize_job_inputs(agent: Any, extension_id: str, job_id: str, raw_data: Path) -> None: extension = agent.load(extension_id) root = Path(str((extension.get("paths") or {}).get("root") or "")) manager = agent.manager input_dir = manager.input_root / job_id input_dir.mkdir(parents=True, exist_ok=True) copy_tree_contents(raw_data, input_dir) def validation_status(val: dict[str, Any]) -> dict[str, Any]: run = val.get("run") or {} golden = run.get("golden_comparison") or {} gate = run.get("template_gate") or {} return { "validate_success": bool(val.get("success")), "golden_status": str(golden.get("status") or ""), "template_gate_status": str(gate.get("status") or ""), "final_validation_status": str(run.get("status") or ""), } def base_row(run_id: str, case: ApiCase, runtime_env: str) -> dict[str, Any]: return { "schema_version": "gwas_api20_case_result_v1", "created_at": now(), "run_id": run_id, "case_id": case.case_id, "archetype_id": case.archetype_id, "converter": "deepseek_api", "runtime_env": runtime_env, "codex_manual_spec_edit": False, "extension_id": "", "raw_code_ok": True, "build_success": False, "sanitize_success": False, "spec_validate_success": False, "wrapper_success": False, "trial_success": False, "confirm_success": False, "validate_success": False, "golden_status": "", "template_gate_status": "", "publish_gate_status": "", "publish_success": False, "published_run_success": False, "api_autonomous_success": False, "published_runnable": False, "template_eligible": False, "api_case_success_after_framework_fix": False, "codex_framework_fix_required": True, "environment_failed": False, "repair_stalled": False, "stall_error_signature": "", "output_count": 0, "build_api_attempt_count": 0, "trial_repair_attempt_count": 0, "validation_repair_attempt_count": 0, "publish_gate_repair_attempt_count": 0, "total_api_attempt_count": 0, "sanitizer_action_count": 0, "final_validation_status": "", "final_publish_status": "", "first_failure_phase": "", "last_error": "", "error": "", "traceback": "", "wall_sec": 0, } def finalize_row(row: dict[str, Any], started: float) -> dict[str, Any]: row["total_api_attempt_count"] = ( int(row.get("build_api_attempt_count") or 0) + int(row.get("trial_repair_attempt_count") or 0) + int(row.get("validation_repair_attempt_count") or 0) + int(row.get("publish_gate_repair_attempt_count") or 0) ) row["published_runnable"] = all( [ row.get("build_success"), row.get("sanitize_success"), row.get("spec_validate_success"), row.get("wrapper_success"), row.get("trial_success"), row.get("confirm_success"), row.get("validate_success"), row.get("golden_status") == "passed", row.get("publish_gate_status") == "passed", row.get("publish_success"), row.get("published_run_success"), ] ) row["template_eligible"] = bool(row.get("published_runnable") and row.get("template_gate_status") == "passed") row["api_autonomous_success"] = bool(row.get("published_runnable") and not row.get("codex_manual_spec_edit")) row["api_case_success_after_framework_fix"] = bool(row.get("api_autonomous_success") and row.get("codex_framework_fix_required")) if not row.get("first_failure_phase") and not row["api_autonomous_success"]: row["first_failure_phase"] = infer_failure_phase(row) row["wall_sec"] = round(time.time() - started, 3) return row def infer_failure_phase(row: dict[str, Any]) -> str: checks = [ ("build", "build_success"), ("sanitize", "sanitize_success"), ("spec_validate", "spec_validate_success"), ("wrapper", "wrapper_success"), ("trial", "trial_success"), ("confirm", "confirm_success"), ("validate", "validate_success"), ("golden", "golden_status"), ("publish_gate", "publish_gate_status"), ("publish", "publish_success"), ("published_run", "published_run_success"), ] for phase, key in checks: value = row.get(key) if key.endswith("_status"): if value != "passed": return phase elif not value: return phase return "" def output_contract_from_expected(expected: Path) -> list[dict[str, Any]]: contract = [] for path in sorted(expected.glob("*")): if not path.is_file(): continue contract.append({"name_pattern": path.name, "semantic_role": semantic_role(path.name), "required": required_output(path.name)}) return contract def semantic_role(name: str) -> str: mapping = { "gwas_metrics.json": "gwas_metrics", "sample_match_report.tsv": "gwas_sample_match_report", "trait.clean.tsv": "gwas_clean_trait_table", "trait.prepared.tsv": "gwas_prepared_trait_table", "filtered.vcf.gz": "gwas_filtered_vcf_gz", "filtered.vcf.gz.tbi": "gwas_filtered_vcf_index", "plink_pca.eigenvec": "gwas_plink_pca_eigenvec", "plink_pca.eigenval": "gwas_plink_pca_eigenval", "heading_days_BLUP_value.tsv": "gwas_blup_trait_table", "heading_days_BLUP_value.tassel.tsv": "gwas_tassel_trait_table", "all.impute.vcf.gz": "gwas_imputed_vcf_gz", "all.impute.vcf.gz.tbi": "gwas_imputed_vcf_index", "imputed_bed.bed": "gwas_imputed_plink_bed", "imputed_bed.bim": "gwas_imputed_plink_bim", "imputed_bed.fam": "gwas_imputed_plink_fam", "hapmap.hmp.txt": "gwas_tassel_hapmap", "tassel_trait.tsv": "gwas_tassel_trait_table", "glm_pvalue.txt": "gwas_tassel_glm_pvalue_table", "tassel_probe.json": "gwas_tassel_probe_record", "lm.assoc.txt": "gwas_gemma_assoc_table", } return mapping.get(name, "gwas_auxiliary_output") def required_output(name: str) -> bool: return name in { "gwas_metrics.json", "sample_match_report.tsv", "trait.clean.tsv", "trait.prepared.tsv", "filtered.vcf.gz", "filtered.vcf.gz.tbi", "plink_pca.eigenvec", "plink_pca.eigenval", "heading_days_BLUP_value.tsv", "heading_days_BLUP_value.tassel.tsv", "all.impute.vcf.gz", "all.impute.vcf.gz.tbi", "imputed_bed.bed", "imputed_bed.bim", "imputed_bed.fam", "hapmap.hmp.txt", "tassel_trait.tsv", "glm_pvalue.txt", "tassel_probe.json", "lm.assoc.txt", } def selected_cases(case_ids: list[str] | None) -> tuple[ApiCase, ...]: if not case_ids: return CASES wanted = set(case_ids) return tuple(case for case in CASES if case.case_id in wanted or case.archetype_id in wanted) def write_partial_summary(output_root: Path, rows: list[dict[str, Any]]) -> None: write_json(output_root / "partial_summary.json", summarize(rows)) write_csv(output_root / "partial_summary.csv", rows) def write_summary(output_root: Path, rows: list[dict[str, Any]]) -> None: summary = summarize(rows) write_json(output_root / "summary.json", summary) write_csv(output_root / "summary.csv", rows) write_json(output_root / "template_eligibility.json", {"schema_version": "gwas_api20_template_eligibility_v1", "created_at": now(), "eligible": [row for row in rows if row.get("template_eligible")]}) write_failure_matrix(output_root, rows) def write_report(output_root: Path) -> None: rows = existing_result_rows(output_root) write_summary(output_root, rows) write_json( output_root / "publication_metrics.json", { "schema_version": "gwas_api20_publication_metrics_v1", "created_at": now(), "benchmark_type": "api_only", "codex_manual_spec_edit": False, "summary": summarize(rows), }, ) def existing_result_rows(output_root: Path) -> list[dict[str, Any]]: rows = [] for path in sorted((output_root / "runs").glob("api_run_*/*/result_row.json")): rows.append(read_json(path)) return rows def summarize(rows: list[dict[str, Any]]) -> dict[str, Any]: return { "schema_version": "gwas_api20_summary_v1", "created_at": now(), "benchmark_type": "api_only", "case_result_count": len(rows), "api_autonomous_success": sum(1 for row in rows if row.get("api_autonomous_success")), "published_runnable": sum(1 for row in rows if row.get("published_runnable")), "template_eligible": sum(1 for row in rows if row.get("template_eligible")), "failed": sum(1 for row in rows if not row.get("api_autonomous_success")), "rows": rows, } def write_failure_matrix(output_root: Path, rows: list[dict[str, Any]]) -> None: counts: dict[tuple[str, str], int] = {} for row in rows: key = (str(row.get("case_id") or ""), str(row.get("first_failure_phase") or "none")) counts[key] = counts.get(key, 0) + 1 matrix = [{"case_id": case_id, "first_failure_phase": phase, "count": count} for (case_id, phase), count in sorted(counts.items())] write_csv(output_root / "failure_matrix.csv", matrix) def upload_files(root: Path) -> list[tuple[str, bytes]]: return [(path.name, path.read_bytes()) for path in sorted(root.iterdir()) if path.is_file()] def inventory(root: Path) -> list[dict[str, Any]]: if not root.exists(): return [] rows = [] for path in sorted(p for p in root.rglob("*") if p.is_file()): rows.append({"path": path.relative_to(root).as_posix(), "size": path.stat().st_size}) return rows def copy_tree_contents(src: Path, dst: Path) -> None: for item in sorted(src.iterdir()): target = dst / item.name if item.is_dir(): if target.exists(): shutil.rmtree(target) shutil.copytree(item, target) else: shutil.copy2(item, target) def now() -> str: return datetime.now().replace(microsecond=0).isoformat() def write_json(path: Path, data: Any) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") def read_json(path: Path) -> Any: return json.loads(path.read_text(encoding="utf-8")) def write_csv(path: Path, rows: list[dict[str, Any]]) -> None: path.parent.mkdir(parents=True, exist_ok=True) keys = sorted({key for row in rows for key in row}) with path.open("w", encoding="utf-8", newline="") as handle: writer = csv.DictWriter(handle, fieldnames=keys) writer.writeheader() for row in rows: writer.writerow({key: json.dumps(value, ensure_ascii=False) if isinstance(value, (dict, list)) else value for key, value in row.items()}) if __name__ == "__main__": raise SystemExit(main())