← Files Investment BankingARCHIVED FILE

skills/pitch-deck-builder/scripts/validate_deck_plan_json.py

7.05 KB · Oct 2, 2026 · 00:27 UTC

↓ Download file

#!/usr/bin/env python3
"""Validate a user-facing investment-banking deck/page plan JSON file.

This checks the pitch-deck-builder planning contract in
references/output-schema.md: the banker-readable storyline, page sequence,
evidence, sources, assumptions, open items, and QC flags that guide a deck.
It is intentionally not a validator for the lower-level slide construction
blueprint used by presentation rendering tools.
"""

import argparse
import json
from pathlib import Path

VALID_DECK_TYPES = {
    "buyer_pitch",
    "sell_side_pitch",
    "financing_pitch",
    "strategic_alternatives",
    "company_profile",
    "market_map",
    "board_client_meeting",
}
VALID_EVIDENCE_STATUS = {"supported", "needs_source", "assumption", "placeholder"}
VALID_EVIDENCE_LABELS = {
    "fact",
    "source_derived_estimate",
    "model_derived_estimate",
    "banker_judgment",
    "client_assumption",
    "external_assumption",
    "placeholder",
    "unknown",
}
VALID_CANONICAL_EVIDENCE_CATEGORIES = {
    "verified_fact",
    "reported_fact",
    "seller_claim",
    "management_statement",
    "pro_forma_adjustment",
    "assumption",
    "inference",
    "estimate",
    "stale",
    "contradicted",
    "unknown",
}
DEFAULT_CANONICAL_BY_EVIDENCE_LABEL = {
    "source_derived_estimate": "estimate",
    "model_derived_estimate": "estimate",
    "banker_judgment": "inference",
    "client_assumption": "assumption",
    "external_assumption": "assumption",
    "placeholder": "unknown",
    "unknown": "unknown",
}
GENERIC_TITLES = {
    "market overview",
    "financial summary",
    "valuation",
    "buyer universe",
    "strategic alternatives",
    "company overview",
    "transaction overview",
    "next steps",
    "summary",
}


def load_json(path: Path) -> dict:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except json.JSONDecodeError as exc:
        raise SystemExit(f"invalid json: {exc}") from exc


def is_generic_title(title: str) -> bool:
    return title.strip().lower() in GENERIC_TITLES


def main() -> int:
    parser = argparse.ArgumentParser(
        description=(
            "Validate a user-facing investment-banking deck/page plan JSON file. "
            "This is the planning-layer contract, not the slide construction blueprint."
        ),
        epilog="For lower-level slide construction blueprints, use validate_deck_blueprint.py instead.",
    )
    parser.add_argument(
        "json_file", type=Path, help="Path to the user-facing deck/page plan JSON file."
    )
    args = parser.parse_args()

    data = load_json(args.json_file)
    errors = []
    warnings = []

    metadata = data.get("deck_metadata", {})
    deck_type = metadata.get("deck_type")
    if deck_type not in VALID_DECK_TYPES:
        errors.append(f"deck_metadata.deck_type must be one of {sorted(VALID_DECK_TYPES)}")
    for field in ["client_or_subject", "audience", "objective", "prepared_date"]:
        if not metadata.get(field):
            errors.append(f"deck_metadata.{field} is required")
    if metadata.get("non_destructive_mode") is not True:
        errors.append("deck_metadata.non_destructive_mode must be true")

    storyline = data.get("md_storyline", {})
    if not storyline.get("recommended_action"):
        errors.append("md_storyline.recommended_action is required")
    if not storyline.get("why_now"):
        warnings.append("md_storyline.why_now is empty; MD-level pitch should explain urgency")
    if len(storyline.get("proof_pillars", [])) < 2:
        warnings.append("md_storyline.proof_pillars should include at least two proof pillars")

    source_ids = {
        source.get("source_id") for source in data.get("sources", []) if source.get("source_id")
    }
    slides = data.get("slides")
    if not isinstance(slides, list) or not slides:
        errors.append("slides must be a non-empty list of planned deck pages")
    else:
        for idx, slide in enumerate(slides, start=1):
            prefix = f"slides[{idx}]"
            title = slide.get("slide_title", "")
            if not title:
                errors.append(f"{prefix}.slide_title is required")
            elif is_generic_title(title):
                warnings.append(f"{prefix}.slide_title appears generic: {title!r}")
            if not slide.get("key_message"):
                errors.append(f"{prefix}.key_message is required")
            status = slide.get("evidence_status")
            if status not in VALID_EVIDENCE_STATUS:
                errors.append(
                    f"{prefix}.evidence_status must be one of {sorted(VALID_EVIDENCE_STATUS)}"
                )
            if not slide.get("recommended_visual"):
                warnings.append(
                    f"{prefix}.recommended_visual is empty; describe the intended page visual at a planning level"
                )
            for m_idx, metric in enumerate(slide.get("metrics", []), start=1):
                label = metric.get("evidence_label")
                if label not in VALID_EVIDENCE_LABELS:
                    errors.append(f"{prefix}.metrics[{m_idx}].evidence_label is invalid")
                canonical = metric.get("canonical_evidence_category")
                if canonical and canonical not in VALID_CANONICAL_EVIDENCE_CATEGORIES:
                    errors.append(
                        f"{prefix}.metrics[{m_idx}].canonical_evidence_category must use the shared "
                        "Investment Banking evidence-label taxonomy"
                    )
                expected_canonical = DEFAULT_CANONICAL_BY_EVIDENCE_LABEL.get(label)
                if canonical and expected_canonical and canonical != expected_canonical:
                    warnings.append(
                        f"{prefix}.metrics[{m_idx}].canonical_evidence_category usually maps to "
                        f"{expected_canonical!r} for native label {label!r}; confirm source context if using {canonical!r}"
                    )
                source_id = metric.get("source_id")
                if label in {"fact", "source_derived_estimate", "model_derived_estimate"}:
                    if not source_id or (source_id not in source_ids and source_id != "assumption"):
                        errors.append(
                            f"{prefix}.metrics[{m_idx}] needs a valid source_id for label {label}"
                        )
                if not metric.get("period"):
                    warnings.append(f"{prefix}.metrics[{m_idx}] missing period")

    qa = data.get("qa", {})
    if qa.get("assumptions_labeled") is not True:
        warnings.append("qa.assumptions_labeled should be true before final delivery")
    if qa.get("template_preserved") is not True:
        warnings.append("qa.template_preserved should be true unless user asked to change style")

    if errors:
        print("INVALID user-facing deck/page plan")
        for error in errors:
            print(f"ERROR: {error}")
        for warning in warnings:
            print(f"WARNING: {warning}")
        return 1

    print("VALID user-facing deck/page plan")
    for warning in warnings:
        print(f"WARNING: {warning}")
    return 0


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

SHA-256: b6325aa139c12d1c50d543e98463dfc13263172776e2c220f40d54c92e865504