| """Bundle verification for completed run folders.""" |
| from __future__ import annotations |
|
|
| import csv |
| import hashlib |
| import json |
| import zipfile |
| from pathlib import Path |
|
|
| REQUIRED_FILES = [ |
| "RUN_CARD.json", |
| "config/resolved_config.json", |
| "logs/events.jsonl", |
| ] |
|
|
| DATA_CSVS = [ |
| "data/head_recovery.csv", |
| "data/logit_lens.csv", |
| "data/gradient_attribution.csv", |
| ] |
|
|
| OPTIONAL_CSVS = [ |
| "data/ablation_results.csv", |
| "data/source_results.csv", |
| "summaries/candidate_heads.csv", |
| "summaries/ablation_summary.csv", |
| "summaries/source_summary.csv", |
| ] |
|
|
| SUMMARY_FILES = [ |
| "summaries/run_summary.json", |
| ] |
|
|
| def verify_bundle(run_dir: Path) -> bool: |
| """Verify a completed run bundle. Prints pass/fail for each check. Returns True if all pass.""" |
| run_dir = Path(run_dir) |
| print(f"\n=== Verifying: {run_dir} ===") |
| ok = True |
|
|
| |
| if not run_dir.is_dir(): |
| print(f" [FAIL] Directory not found: {run_dir}") |
| return False |
|
|
| |
| for rel in REQUIRED_FILES: |
| path = run_dir / rel |
| if path.exists(): |
| print(f" [OK] {rel}") |
| else: |
| print(f" [FAIL] Missing: {rel}") |
| ok = False |
|
|
| |
| for rel in SUMMARY_FILES: |
| path = run_dir / rel |
| if path.exists(): |
| try: |
| json.loads(path.read_text(encoding="utf-8")) |
| print(f" [OK] {rel} (valid JSON)") |
| except Exception as exc: |
| print(f" [FAIL] {rel} - JSON parse error: {exc}") |
| ok = False |
| else: |
| print(f" [WARN] Missing (optional): {rel}") |
|
|
| |
| raw_dir = run_dir / "raw" |
| if raw_dir.is_dir(): |
| raw_jsons = list(raw_dir.glob("*.json")) |
| if raw_jsons: |
| for f in raw_jsons: |
| try: |
| json.loads(f.read_text(encoding="utf-8")) |
| print(f" [OK] raw/{f.name} (valid JSON)") |
| except Exception as exc: |
| print(f" [FAIL] raw/{f.name} - JSON parse error: {exc}") |
| ok = False |
| else: |
| print(" [WARN] raw/ directory is empty") |
| else: |
| print(" [WARN] raw/ directory not found") |
|
|
| |
| for rel in DATA_CSVS: |
| path = run_dir / rel |
| if path.exists(): |
| row_count = _count_csv_rows(path) |
| if row_count > 0: |
| print(f" [OK] {rel} ({row_count} rows)") |
| else: |
| print(f" [FAIL] {rel} is empty (0 rows)") |
| ok = False |
| else: |
| print(f" [WARN] Missing (may be absent for ablation/source-only runs): {rel}") |
|
|
| |
| for rel in OPTIONAL_CSVS: |
| path = run_dir / rel |
| if path.exists(): |
| row_count = _count_csv_rows(path) |
| print(f" [OK] {rel} ({row_count} rows)") |
|
|
| |
| integrity_path = run_dir / "integrity.json" |
| if integrity_path.exists(): |
| try: |
| integrity = json.loads(integrity_path.read_text(encoding="utf-8")) |
| files = integrity.get("files", {}) |
| mismatches = [] |
| for rel_path, file_info in files.items(): |
| actual = run_dir / rel_path |
| if actual.exists(): |
| expected_hash = file_info["sha256"] if isinstance(file_info, dict) else file_info |
| actual_hash = hashlib.sha256(actual.read_bytes()).hexdigest() |
| if actual_hash != expected_hash: |
| mismatches.append(rel_path) |
| if mismatches: |
| print(f" [FAIL] Checksum mismatches: {mismatches}") |
| ok = False |
| else: |
| print(f" [OK] integrity.json ({len(files)} checksums verified)") |
| except Exception as exc: |
| print(f" [WARN] integrity.json - parse error: {exc}") |
| else: |
| print(" [WARN] integrity.json not found") |
|
|
| |
| zip_search_roots = [run_dir.parent, run_dir.parent.parent, Path.cwd()] |
| zips = [] |
| seen_roots = set() |
| for root in zip_search_roots: |
| resolved = root.resolve() |
| if resolved in seen_roots: |
| continue |
| seen_roots.add(resolved) |
| zips.extend(root.glob(f"{run_dir.name}*.zip")) |
| if zips: |
| zip_path = zips[0] |
| try: |
| with zipfile.ZipFile(zip_path) as zf: |
| names = zf.namelist() |
| print(f" [OK] Archive: {zip_path.name} ({len(names)} files)") |
| except Exception as exc: |
| print(f" [FAIL] Archive {zip_path.name} - {exc}") |
| ok = False |
| else: |
| print(" [INFO] No run archive found") |
|
|
| status = "PASS" if ok else "FAIL" |
| print(f"\n Result: {status}\n") |
| return ok |
|
|
| def _count_csv_rows(path: Path) -> int: |
| try: |
| with path.open(newline="", encoding="utf-8") as f: |
| reader = csv.reader(f) |
| rows = sum(1 for _ in reader) |
| return max(0, rows - 1) |
| except Exception: |
| return -1 |
|
|