You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
motief/analysis/right_wing/build_all_reports.py

228 lines
6.3 KiB

#!/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())