← Files Public Equity InvestingARCHIVED FILE

skills/earnings-deep-dive/scripts/run_plan.py

26 KB · Oct 2, 2026 · 00:03 UTC

↓ Download file

#!/usr/bin/env python3
"""Run the Earnings Deep Dive & Summary pipeline.

This script intentionally focuses on deterministic steps:
- Validate inputs
- Compute beat/miss and guidance deltas
- Render markdown outputs (ExecutiveOverview, TearSheet, DeepDive)
- Optionally update a model workbook via driver registry and create a changelog + diff

It does **not** invent narrative. If drivers bullets, watch list, or narrative
are not provided, generated reports use precise limitation labels and fail QA
if authoring placeholders remain.
"""

from __future__ import annotations

import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any

SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

if __name__ == "__main__" and any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
    print("Usage: python scripts/run_plan.py plan.json")
    print("Run the deterministic earnings deep-dive pipeline.")
    raise SystemExit(0)

import pandas as pd

try:
    from .utils.io_utils import ensure_dir, read_json, write_text
    from .utils.math_utils import compute_delta
    from .utils.validation_utils import as_float_or_none, is_missing
    from .validate_plan import validate_plan
except ImportError:
    from utils.io_utils import ensure_dir, read_json, write_text
    from utils.math_utils import compute_delta
    from utils.validation_utils import as_float_or_none, is_missing
    from validate_plan import validate_plan


def _read_csv(path: str) -> pd.DataFrame:
    return pd.read_csv(path, dtype=str, keep_default_na=False)


def _format_number(val: float | None, units: str) -> str:
    if val is None:
        return "not provided"
    # Heuristic formatting
    if units.strip() in {"USD", "EPS"}:
        return f"{val:.2f}"
    if units.endswith("%"):
        return f"{val:.2f}%"
    # default
    if abs(val) >= 1000:
        return f"{val:,.0f}"
    return f"{val:,.2f}"


def _format_pct(val: float | None) -> str:
    if val is None:
        return "not provided"
    return f"{val * 100:.1f}%"


def _display(value: Any, fallback: str = "not provided") -> str:
    return fallback if is_missing(value) else str(value)


PLACEHOLDER_RE = re.compile(
    r"(\{[A-Z0-9_]+\}|\bTODO\b|MISSING:\s*(add|explain|map|summarize|write|list|populate|decompose))",
    re.IGNORECASE,
)


def unresolved_placeholder_errors(outputs: dict[str, str]) -> list[str]:
    errors: list[str] = []
    for name, text in outputs.items():
        matches = sorted({m.group(0) for m in PLACEHOLDER_RE.finditer(text)})
        if matches:
            errors.append(f"{name}: unresolved authoring placeholder(s): {', '.join(matches[:6])}")
    return errors


@dataclass
class BeatMissRow:
    metric: str
    period: str
    units: str
    reported: float | None
    consensus: float | None
    internal: float | None
    delta_cons: float | None
    surprise_cons: float | None
    delta_int: float | None
    surprise_int: float | None
    source_tag: str
    gaap_flag: str = ""
    comparable_gaap_metric: str = ""
    reconciliation_source_tag: str = ""
    notes: str = ""


@dataclass
class GuidanceRow:
    metric: str
    period: str
    units: str
    low: float | None
    high: float | None
    midpoint: float | None
    consensus: float | None
    internal: float | None
    delta_cons_mid: float | None
    delta_int_mid: float | None
    source_tag: str


def _pick_first(df: pd.DataFrame, metric: str, period: str, est_type: str) -> float | None:
    sub = df[
        (df["MetricName"] == metric) & (df["Period"] == period) & (df["EstimateType"] == est_type)
    ]
    if len(sub) == 0:
        return None
    return as_float_or_none(sub.iloc[0]["Value"])


def compute_beat_miss(metrics_df: pd.DataFrame, estimates_df: pd.DataFrame) -> list[BeatMissRow]:
    rows: list[BeatMissRow] = []
    m = metrics_df.copy()
    m["IsTearSheet"] = m["IsTearSheet"].astype(str)
    m = m[m["IsTearSheet"].str.upper().isin(["Y", "YES", "TRUE", "1"])].copy()

    def _order(x: str) -> int:
        try:
            return int(float(x))
        except Exception:
            return 999

    m["_order"] = m["DisplayOrder"].apply(_order)
    m = m.sort_values("_order")

    for _, r in m.iterrows():
        metric = r["MetricName"]
        period = r["Period"]
        units = r["Units"]
        reported = as_float_or_none(r["Value"])
        source_tag = _display(r.get("SourceTag"), "source not provided")

        cons = _pick_first(estimates_df, metric, period, "Consensus")
        internal = _pick_first(estimates_df, metric, period, "Internal")

        dcons, scons = compute_delta(reported, cons)
        dint, sint = compute_delta(reported, internal)

        rows.append(
            BeatMissRow(
                metric=metric,
                period=period,
                units=units,
                reported=reported,
                consensus=cons,
                internal=internal,
                delta_cons=dcons,
                surprise_cons=scons,
                delta_int=dint,
                surprise_int=sint,
                source_tag=source_tag,
                gaap_flag=str(r.get("GAAP_Flag") or ""),
                comparable_gaap_metric=str(r.get("ComparableGAAPMetricName") or ""),
                reconciliation_source_tag=str(r.get("ReconciliationSourceTag") or ""),
                notes=str(r.get("Notes") or ""),
            )
        )

    return rows


def compute_guidance(guidance_df: pd.DataFrame, estimates_df: pd.DataFrame) -> list[GuidanceRow]:
    rows: list[GuidanceRow] = []
    for _, r in guidance_df.iterrows():
        metric = r["MetricName"]
        period = r["Period"]
        units = r["Units"]
        low = as_float_or_none(r["Low"])
        high = as_float_or_none(r["High"])
        source_tag = _display(r.get("SourceTag"), "source not provided")

        midpoint: float | None = None
        if low is not None and high is not None:
            midpoint = (low + high) / 2.0

        cons = _pick_first(estimates_df, metric, period, "Consensus")
        internal = _pick_first(estimates_df, metric, period, "Internal")

        dcons, _ = compute_delta(midpoint, cons)
        dint, _ = compute_delta(midpoint, internal)

        rows.append(
            GuidanceRow(
                metric=metric,
                period=period,
                units=units,
                low=low,
                high=high,
                midpoint=midpoint,
                consensus=cons,
                internal=internal,
                delta_cons_mid=dcons,
                delta_int_mid=dint,
                source_tag=source_tag,
            )
        )
    return rows


def _md_table_beatmiss(rows: list[BeatMissRow]) -> str:
    lines: list[str] = []
    lines.append(
        "| Metric | Reported | Consensus | Δ | Surprise % | Internal | Δ | Surprise % | SourceTag |"
    )
    lines.append("|---|---:|---:|---:|---:|---:|---:|---:|---|")
    for r in rows:
        lines.append(
            "| {m} | {rep} | {c} | {dc} | {sc} | {i} | {di} | {si} | {src} |".format(
                m=r.metric,
                rep=_format_number(r.reported, r.units),
                c=_format_number(r.consensus, r.units),
                dc=_format_number(r.delta_cons, r.units),
                sc=_format_pct(r.surprise_cons),
                i=_format_number(r.internal, r.units),
                di=_format_number(r.delta_int, r.units),
                si=_format_pct(r.surprise_int),
                src=r.source_tag,
            )
        )
    return "\n".join(lines)


def _md_table_guidance(rows: list[GuidanceRow]) -> str:
    lines: list[str] = []
    lines.append(
        "| Metric | Period | Low | High | Mid | Consensus | Δ(mid) | Internal | Δ(mid) | SourceTag |"
    )
    lines.append("|---|---|---:|---:|---:|---:|---:|---:|---:|---|")
    for r in rows:
        lines.append(
            "| {m} | {p} | {lo} | {hi} | {mid} | {c} | {dc} | {i} | {di} | {src} |".format(
                m=r.metric,
                p=r.period,
                lo=_format_number(r.low, r.units),
                hi=_format_number(r.high, r.units),
                mid=_format_number(r.midpoint, r.units),
                c=_format_number(r.consensus, r.units),
                dc=_format_number(r.delta_cons_mid, r.units),
                i=_format_number(r.internal, r.units),
                di=_format_number(r.delta_int_mid, r.units),
                src=r.source_tag,
            )
        )
    return "\n".join(lines)


EPS_DISTORTION_TERMS = (
    "asset sale",
    "below-the-line",
    "equity investment",
    "fair value",
    "fx",
    "gain",
    "impairment",
    "litigation",
    "loss",
    "mark-to-market",
    "non-operating",
    "non-recurring",
    "one-time",
    "pension",
    "restructuring",
    "share count",
    "tax",
)


def _metric_has(row: BeatMissRow, *needles: str) -> bool:
    metric = row.metric.lower()
    return all(needle.lower() in metric for needle in needles)


def _first_row(rows: list[BeatMissRow], *needles: str, period: str = "") -> BeatMissRow | None:
    for row in rows:
        if period and row.period != period:
            continue
        if _metric_has(row, *needles):
            return row
    return None


def _distortion_text(row: BeatMissRow) -> str:
    text = " ".join(
        [
            row.metric,
            row.gaap_flag,
            row.comparable_gaap_metric,
            row.reconciliation_source_tag,
            row.notes,
        ]
    ).lower()
    found = [term for term in EPS_DISTORTION_TERMS if term in text]
    return ", ".join(found)


def _surprise_gap_flag(eps_row: BeatMissRow, peer_row: BeatMissRow | None, label: str) -> str:
    if eps_row.surprise_cons is None or peer_row is None or peer_row.surprise_cons is None:
        return ""
    eps_surprise = abs(eps_row.surprise_cons)
    peer_surprise = abs(peer_row.surprise_cons)
    if eps_surprise >= 0.15 and eps_surprise > max(peer_surprise * 2.0, peer_surprise + 0.10):
        return f"GAAP EPS surprise materially outpaced {label} surprise"
    return ""


def render_eps_quality_screen(rows: list[BeatMissRow]) -> str:
    gaap_eps_rows = [
        row
        for row in rows
        if "eps" in row.metric.lower()
        and ("gaap" in row.metric.lower() or row.gaap_flag.lower() == "gaap")
    ]
    if not gaap_eps_rows:
        return (
            "EPS quality screen not run: GAAP diluted EPS row was not provided in "
            "normalized metrics. Add GAAP EPS, adjusted/operating EPS if available, "
            "and below-the-line notes before using EPS surprise as a recurring "
            "earnings signal."
        )

    lines: list[str] = []
    lines.append("| Check | Status | Read-through | Source |")
    lines.append("|---|---|---|---|")

    for eps_row in gaap_eps_rows:
        revenue_row = _first_row(rows, "revenue", period=eps_row.period)
        operating_row = _first_row(rows, "operating", period=eps_row.period)
        adjusted_eps_row = _first_row(rows, "adjusted", "eps", period=eps_row.period)
        flags = [
            flag
            for flag in [
                _distortion_text(eps_row),
                _surprise_gap_flag(eps_row, revenue_row, "revenue"),
                _surprise_gap_flag(eps_row, operating_row, "operating income"),
            ]
            if flag
        ]

        if flags:
            status = "expanded bridge required"
            read_through = (
                "Do not capitalize the full GAAP EPS beat as recurring until "
                + "; ".join(flags)
                + " is isolated."
            )
        else:
            status = "no material trigger in normalized inputs"
            read_through = (
                "GAAP EPS still needs filing/release tie-out, but no explicit "
                "distortion flag was supplied."
            )

        lines.append(
            f"| GAAP EPS surprise quality ({eps_row.period}) | {status} | "
            f"{read_through} | {eps_row.source_tag} |"
        )

        if adjusted_eps_row:
            lines.append(
                "| Adjusted / operating EPS comparison | provided | "
                f"Compare {_format_number(eps_row.reported, eps_row.units)} GAAP EPS "
                f"with {_format_number(adjusted_eps_row.reported, adjusted_eps_row.units)} "
                "adjusted/operating EPS before updating forward EPS. | "
                f"{adjusted_eps_row.source_tag} |"
            )
        else:
            lines.append(
                "| Adjusted / operating EPS comparison | source not provided | "
                "Request company reconciliation or calculate a clearly labeled "
                "analyst-derived recurring EPS bridge if the EPS beat matters. | "
                "source not provided |"
            )

    return "\n".join(lines)


def _read_optional_lines(path: Path, max_items: int) -> list[str]:
    if not path.exists():
        return []
    txt = path.read_text(encoding="utf-8").splitlines()
    items: list[str] = []
    for line in txt:
        line = line.strip()
        if not line:
            continue
        items.append(line)
        if len(items) >= max_items:
            break
    return items


def render_tearsheet(
    plan: dict[str, Any],
    beatmiss_rows: list[BeatMissRow],
    guidance_rows: list[GuidanceRow],
    quotes_df: pd.DataFrame,
    driver_updates_df: pd.DataFrame,
    diff_path: Path | None,
    estimate_asof: str,
    artifact_summary: str,
) -> str:
    ev = plan["event"]
    ticker = _display(ev.get("ticker"))
    period = _display(ev.get("fiscal_period"))

    est_asof = estimate_asof

    norm_dir = Path(
        plan.get("inputs", {}).get("normalized", {}).get("metrics_csv", "normalized/metrics.csv")
    ).parent
    drivers_bullets = _read_optional_lines(norm_dir / "drivers_bullets.md", 8)
    if not drivers_bullets:
        drivers_bullets = [
            "Driver bullets not provided in normalized inputs; add source-supported driver bullets for committee use."
        ]

    watch_list = _read_optional_lines(norm_dir / "watch_list.md", 5)
    if not watch_list:
        watch_list = [
            "Watch list not provided in normalized inputs; add measurable next-quarter watch items."
        ]

    # Quotes (max 8)
    quotes: list[dict[str, str]] = []
    if not quotes_df.empty:
        for _, r in quotes_df.iterrows():
            if len(quotes) >= 8:
                break
            qt = r.get("QuoteText")
            if is_missing(qt):
                continue
            quotes.append(
                {
                    "Section": _display(r.get("Section")),
                    "Speaker": _display(r.get("Speaker")),
                    "Questioner": r.get("Questioner") or "",
                    "TopicTag": _display(r.get("TopicTag")),
                    "QuoteText": qt,
                    "SourceTag": _display(r.get("SourceTag"), "source not provided"),
                }
            )

    # Model impact from driver_updates
    model_impact_lines: list[str] = []
    if not driver_updates_df.empty:
        for _, r in driver_updates_df.iterrows():
            did = _display(r.get("DriverID"))
            newv = _display(r.get("NewValue"))
            units = r.get("Units") or ""
            why = _display(r.get("Why"))
            src = _display(r.get("SourceTag"), "source not provided")
            oldv = _display(r.get("OldValue"))
            model_impact_lines.append(
                f"- {did}: {oldv} → {newv} ({units}) | Why: {why} | Source: {src}"
            )
            if len(model_impact_lines) >= 8:
                break
    else:
        model_impact_lines.append(
            "- Driver updates not provided; create a driver update packet before using this as a model-impact artifact."
        )

    diff_note = ""
    if diff_path and diff_path.exists():
        diff_note = f"\n\n**Diff:** see `{diff_path.as_posix()}`"

    lines: list[str] = []
    lines.append(f"# {ticker} — {period} Earnings Tear Sheet")
    lines.append("")
    lines.append(f"**Event:** {_display(ev.get('event_date'))} ({_display(ev.get('timezone'))})  ")
    lines.append(f"**Estimate set (as-of):** {est_asof}  ")
    lines.append(f"**Artifacts:** {artifact_summary}  ")
    lines.append(f"**Base units:** {_display(ev.get('base_currency'))}{ev.get('base_scale', '')}  ")
    lines.append("")

    lines.append("## Beat/Miss vs expectations")
    lines.append(_md_table_beatmiss(beatmiss_rows))
    lines.append("")

    lines.append("## EPS quality screen")
    lines.append(render_eps_quality_screen(beatmiss_rows))
    lines.append("")

    lines.append("## Guidance delta")
    lines.append(_md_table_guidance(guidance_rows))
    lines.append("")

    lines.append("## Drivers (5–8)")
    for b in drivers_bullets:
        if b.startswith("-"):
            lines.append(b)
        else:
            lines.append(f"- {b}")
    lines.append("")

    lines.append("## Key quotes (max 4–8)")
    if quotes:
        for q in quotes:
            qhdr = f"**[{q['TopicTag']}] {q['Speaker']} ({q['Section']})**"
            if q["Section"] == "Q&A" and q["Questioner"]:
                qhdr = f"**[{q['TopicTag']}] {q['Speaker']} (Q&A; Q: {q['Questioner']})**"
            lines.append(f"> {qhdr}: “{q['QuoteText']}”  ")
            lines.append(f"> Source: {q['SourceTag']}")
            lines.append("")
    else:
        lines.append(
            "> Quote pack not provided in normalized inputs; add high-signal quotes with speaker and SourceTag for quote mode."
        )
        lines.append("")

    lines.append("## Model impact")
    lines.append("Top driver changes applied (old → new):")
    lines += model_impact_lines
    lines.append("")
    lines.append("Key output deltas (old → new):")
    lines.append(
        "- Output deltas not computed in this run; recalc model or provide output registry to summarize top deltas."
    )
    lines.append(diff_note)
    lines.append("")

    lines.append("## Watch list (next quarter)")
    for i, w in enumerate(watch_list, start=1):
        w = w.lstrip("0123456789. ")
        lines.append(f"{i}. {w}")

    return "\n".join(lines)


def render_executive_overview(
    plan: dict[str, Any], beatmiss_rows: list[BeatMissRow], guidance_rows: list[GuidanceRow]
) -> str:
    ev = plan["event"]
    ticker = _display(ev.get("ticker"))
    period = _display(ev.get("fiscal_period"))

    # Heuristic ranking: guidance deltas first, then headline surprises.
    bullets: list[str] = []

    # Guidance bullets (top 5)
    for r in guidance_rows[:5]:
        if r.midpoint is None:
            continue
        bullets.append(
            f"Guidance ({r.metric}, {r.period}) midpoint vs consensus: {_format_number(r.delta_cons_mid, r.units)} ({r.units}); Source: {r.source_tag}"
        )

    # Beat/miss bullets (top 5)
    for r in beatmiss_rows[:5]:
        if r.reported is None:
            continue
        bullets.append(
            f"Reported {r.metric} vs consensus: {_format_number(r.delta_cons, r.units)} ({_format_pct(r.surprise_cons)}); Source: {r.source_tag}"
        )

    # Pad to 10
    while len(bullets) < 10:
        bullets.append(
            "Additional source-supported what-mattered point not provided in normalized inputs."
        )

    lines: list[str] = []
    lines.append(f"# Executive Overview — {ticker} {period}")
    lines.append("")
    lines.append("## What matters (ranked)")
    for i, b in enumerate(bullets[:15], start=1):
        lines.append(f"{i}. {b}")
    lines.append("")
    lines.append("## Stock move frame (vs expectations)")
    lines.append(
        "Stock move frame not provided in normalized inputs; frame the move against the estimate set before committee use."
    )
    lines.append("")
    lines.append("## EPS quality screen")
    lines.append(render_eps_quality_screen(beatmiss_rows))
    lines.append("")
    lines.append("## Bull / Base / Bear narrative delta")
    lines.append("Bull/base/bear narrative delta not provided in normalized inputs.")
    return "\n".join(lines)


def render_deep_dive(
    plan: dict[str, Any],
    beatmiss_rows: list[BeatMissRow],
    guidance_rows: list[GuidanceRow],
    quotes_df: pd.DataFrame,
) -> str:
    ev = plan["event"]
    ticker = _display(ev.get("ticker"))
    period = _display(ev.get("fiscal_period"))

    lines: list[str] = []
    lines.append(f"# Earnings Deep Dive — {ticker} {period}")
    lines.append("")
    lines.append("## 1. Headline results vs expectations")
    lines.append(_md_table_beatmiss(beatmiss_rows))
    lines.append("")
    lines.append("## 2. EPS quality screen")
    lines.append(render_eps_quality_screen(beatmiss_rows))
    lines.append("")
    lines.append("## 3. Guidance and outlook")
    lines.append(_md_table_guidance(guidance_rows))
    lines.append("")
    lines.append("## 4. Results drivers (write-up)")
    lines.append(
        "Results-driver narrative not provided in normalized inputs; add source-tagged driver decomposition before using as a full deep dive."
    )
    lines.append("")
    lines.append("## 5. Transcript themes + Q&A (high signal)")
    if not quotes_df.empty:
        for _, r in quotes_df.head(12).iterrows():
            qt = r.get("QuoteText")
            if is_missing(qt):
                continue
            lines.append(
                f"- [{_display(r.get('TopicTag'))}] {_display(r.get('Speaker'))} ({_display(r.get('Section'))}): “{qt}” (Source: {_display(r.get('SourceTag'), 'source not provided')})"
            )
    else:
        lines.append("Quote themes not provided in normalized inputs.")
    lines.append("")
    lines.append("## 6. Model changes")
    lines.append(
        "Model-change narrative not provided in normalized inputs; map surprises and guidance into driver updates before using as a model-update artifact."
    )
    return "\n".join(lines)


def main() -> int:
    if len(sys.argv) != 2:
        print("Usage: python scripts/run_plan.py plan.json")
        return 1

    plan_path = sys.argv[1]
    plan = read_json(plan_path)

    # Plan validation (structure)
    v = validate_plan(plan)
    if v["errors"]:
        for e in v["errors"]:
            print(f"ERROR: {e}")
        return 1

    out_dir = Path(plan.get("outputs", {}).get("output_dir", "output"))
    ensure_dir(str(out_dir))
    ensure_dir(str(out_dir / "audit"))
    ensure_dir(str(out_dir / "model"))

    # Validate normalized inputs (structural + SourceTag hygiene)
    val_script = Path(__file__).resolve().parent / "validate_normalized_inputs.py"
    proc = subprocess.run([sys.executable, str(val_script), plan_path])
    if proc.returncode != 0:
        return proc.returncode

    # Read normalized tables
    norm = plan.get("inputs", {}).get("normalized", {})
    metrics_df = _read_csv(norm["metrics_csv"])
    estimates_df = _read_csv(norm["estimates_csv"])
    guidance_df = _read_csv(norm["guidance_csv"])
    quotes_df = _read_csv(norm["quotes_csv"])
    driver_updates_df = _read_csv(norm["driver_updates_csv"])

    # Estimate set AsOf (best-effort)
    estimate_asof = "not provided"
    if "AsOf" in estimates_df.columns:
        vals = [
            v
            for v in estimates_df["AsOf"].tolist()
            if str(v).strip() and str(v).strip().upper() != "MISSING"
        ]
        if vals:
            estimate_asof = str(vals[0]).strip()

    # Artifact summary (for tear sheet header)
    artifacts = plan.get("inputs", {}).get("artifacts", {})
    parts = []
    if isinstance(artifacts, dict):
        for k, v in artifacts.items():
            if isinstance(v, str) and v.strip().upper() != "MISSING":
                parts.append(f"{k}={Path(v).name}")
    artifact_summary = ", ".join(parts) if parts else "not provided"

    beatmiss_rows = compute_beat_miss(metrics_df, estimates_df)
    guidance_rows = compute_guidance(guidance_df, estimates_df)

    # Optional model update
    diff_path: Path | None = None
    if plan.get("outputs", {}).get("model_update", {}).get("enabled"):
        mode = plan.get("outputs", {}).get("model_update", {}).get("mode", "apply")
        if mode == "apply":
            try:
                try:
                    from .apply_model_updates import apply_model_updates
                except ImportError:
                    from apply_model_updates import apply_model_updates

                updated_model_path, changelog_path = apply_model_updates(plan)
                if plan.get("outputs", {}).get("model_update", {}).get("write_diff"):
                    try:
                        try:
                            from .model_diff import write_diff
                        except ImportError:
                            from model_diff import write_diff

                        diff_path = out_dir / "audit" / "WhatChanged_Diff.csv"
                        write_diff(plan, diff_path, updated_model_path)
                    except Exception as e:
                        print(f"WARN: diff generation failed: {e}")
            except Exception as e:
                print(f"WARN: model update failed: {e}")
        else:
            # packet mode
            packet_path = out_dir / "audit" / "ModelUpdatePacket.csv"
            driver_updates_df.to_csv(packet_path, index=False)

    # Render markdown outputs
    tearsheet_md = render_tearsheet(
        plan,
        beatmiss_rows,
        guidance_rows,
        quotes_df,
        driver_updates_df,
        diff_path,
        estimate_asof,
        artifact_summary,
    )
    exec_md = render_executive_overview(plan, beatmiss_rows, guidance_rows)
    deep_md = render_deep_dive(plan, beatmiss_rows, guidance_rows, quotes_df)

    write_text(exec_md, str(out_dir / "ExecutiveOverview.md"))
    write_text(tearsheet_md, str(out_dir / "TearSheet.md"))
    write_text(deep_md, str(out_dir / "DeepDive.md"))

    output_errors = unresolved_placeholder_errors(
        {
            "ExecutiveOverview.md": exec_md,
            "TearSheet.md": tearsheet_md,
            "DeepDive.md": deep_md,
        }
    )
    if output_errors:
        qa_path = out_dir / "audit" / "PlaceholderQA.md"
        write_text(
            "# Placeholder QA\n\n" + "\n".join(f"- {e}" for e in output_errors) + "\n", str(qa_path)
        )
        for e in output_errors:
            print(f"ERROR: {e}")
        return 1

    print(f"Wrote outputs to: {out_dir}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

SHA-256: bf3ade54a274418a929319c6fc711e9243ba66cbd30e331e8ebbf7e1a7519bb7