#!/usr/bin/env python3 """Regenerate all Overton window reports in correct dependency order. Usage: uv run python analysis/right_wing/build_all_reports.py uv run python analysis/right_wing/build_all_reports.py --skip-llm """ from __future__ import annotations import argparse import logging import subprocess import sys import time from pathlib import Path ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from analysis.right_wing.common import REPORTS_DIR logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S", ) logger = logging.getLogger("build_all_reports") SCRIPT_DIR = ROOT / "analysis" / "right_wing" PHASE_1_SCRIPTS = [ "overton_breakpoint_analysis.py", "temporal_trajectory.py", "causal_timing.py", "party_differentiation.py", "voting_margin.py", "left_wing_response.py", "success_correlation.py", "overton_svd_drift.py", "svd_trajectory_viz.py", ] PHASE_1_OUTPUTS = [ "breakpoint_analysis.md", "breakpoint_figure_1.png", "breakpoint_figure_2.png", "breakpoint_figure_3.png", "breakpoint_figure_4.png", "temporal_trajectory.md", "temporal_trajectory_figure.png", "causal_timing.md", "causal_timing_figure.png", "party_differentiation.md", "party_differentiation_figure.png", "voting_margin.md", "voting_margin_figure.png", "left_wing_response.md", "left_wing_response_figure.png", "success_correlation.md", "svd_drift_chart.png", "svd_stability_report.md", "svd_trajectory_figure.png", ] PHASE_2_SCRIPTS = [ "extremity_2d_temporal.py", "predictive_model.py", "mechanism_classification.py", ] PHASE_2_OUTPUTS = [ "extremity_2d_temporal.md", "extremity_2d_temporal_figure.png", "predictive_model.md", "predictive_model_figure.png", "mechanism_classification.md", ] PHASE_3_SCRIPTS = [ "derive_categories.py", ] def _script_path(name: str) -> str: return str(SCRIPT_DIR / name) def _run_script(name: str) -> bool: """Run a single script via subprocess. Returns True on success.""" logger.info("Running %s ...", name) t0 = time.perf_counter() try: subprocess.run( [sys.executable, _script_path(name)], cwd=str(ROOT), check=True, capture_output=True, text=True, ) elapsed = time.perf_counter() - t0 logger.info("Finished %s (%.1fs)", name, elapsed) return True except subprocess.CalledProcessError as exc: elapsed = time.perf_counter() - t0 logger.error("Script %s failed after %.1fs (rc=%d)", name, elapsed, exc.returncode) if exc.stdout: for line in exc.stdout.strip().splitlines(): logger.error(" stdout: %s", line) if exc.stderr: for line in exc.stderr.strip().splitlines(): logger.error(" stderr: %s", line) return False def _verify_outputs(files: list[str]) -> list[str]: """Return list of expected output files that are missing.""" missing = [] for f in files: if not (REPORTS_DIR / f).exists(): missing.append(f) return missing def _run_phase( phase_label: str, scripts: list[str], expected_outputs: list[str] ) -> tuple[list[str], list[str]]: """Run a list of scripts and verify outputs. Returns (succeeded, failed).""" logger.info("=" * 50) logger.info("Phase %s", phase_label) logger.info("=" * 50) succeeded = [] failed = [] for script in scripts: ok = _run_script(script) if ok: succeeded.append(script) else: failed.append(script) missing = _verify_outputs(expected_outputs) if missing: logger.warning( "Phase %s: %d expected output(s) missing after run:\n %s", phase_label, len(missing), "\n ".join(missing), ) else: logger.info("Phase %s: all expected outputs present.", phase_label) return succeeded, failed def main() -> int: parser = argparse.ArgumentParser( description="Regenerate all Overton window reports in dependency order." ) parser.add_argument( "--skip-llm", action="store_true", help="Skip LLM-dependent phase (derive_categories.py)", ) args = parser.parse_args() REPORTS_DIR.mkdir(parents=True, exist_ok=True) all_succeeded: list[str] = [] all_failed: list[str] = [] t_start = time.perf_counter() # Phase 1: database-dependent (no LLM) s, f = _run_phase("1 — database-dependent", PHASE_1_SCRIPTS, PHASE_1_OUTPUTS) all_succeeded.extend(s) all_failed.extend(f) # Phase 2: 2D extremity-dependent (no LLM) s, f = _run_phase("2 — 2D extremity-dependent", PHASE_2_SCRIPTS, PHASE_2_OUTPUTS) all_succeeded.extend(s) all_failed.extend(f) # Phase 3: LLM-dependent if not args.skip_llm: s, f = _run_phase("3 — LLM-dependent", PHASE_3_SCRIPTS, []) all_succeeded.extend(s) all_failed.extend(f) else: logger.info("Skipping LLM-dependent phase (--skip-llm).") # Phase 4: Synthesis reminder (manual) print("\n" + "=" * 50) print("PHASE 4 — MANUAL STEP REQUIRED") print("=" * 50) print(" After all scripts complete, manually update:") print(" - reports/overton_window/overton_window_synthesis.md") print(" - reports/overton_window/overton_window.qmd (then: quarto render)") print(" - reports/overton_window/overton_report.html") print(" These narrative files require human judgment to integrate") print(" new data into the existing analysis framework.") print("=" * 50) total_elapsed = time.perf_counter() - t_start # Summary sep = "=" * 50 print(f"\n{sep}") print("BUILD SUMMARY") print(sep) print(f" Total time: {total_elapsed:.1f}s") print(f" Succeeded: {len(all_succeeded)}/{len(all_succeeded) + len(all_failed)}") if all_succeeded: print(" Scripts OK:") for name in all_succeeded: print(f" ✓ {name}") if all_failed: print(" Scripts FAILED:") for name in all_failed: print(f" ✗ {name}") print(sep) return 1 if all_failed else 0 if __name__ == "__main__": raise SystemExit(main())