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.
228 lines
6.3 KiB
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())
|
|
|