← Files Investment BankingARCHIVED FILE
skills/cim-teardown/scripts/run_plan.py
14.8 KB · Oct 2, 2026 · 00:27 UTC
#!/usr/bin/env python3
"""Scaffold a CIM teardown workspace and output templates from plan.json.
This script does NOT parse the CIM. It creates the required output files
with correct headers so an analyst/agent can fill them reliably.
Usage:
python scripts/run_plan.py plan.json
Creates (by default) in output.output_dir:
- claims_ledger.csv
- evidence_checklist.csv
- diligence_questions.csv
- red_flag_register.csv
- workplan.csv
- deal_package.json (optional skeleton)
- cim_teardown_report.html (standalone decision-grade reader report skeleton)
- manifest.json (run metadata)
"""
from __future__ import annotations
import csv
import hashlib
import json
import sys
from datetime import datetime, timezone
from html import escape
from pathlib import Path
PLUGIN_ROOT = Path(__file__).resolve().parents[3]
if str(PLUGIN_ROOT) not in sys.path:
sys.path.insert(0, str(PLUGIN_ROOT))
from shared.artifacts import ( # noqa: E402
artifact_item,
build_minimal_handoff_payload,
handoff_artifact_item,
handoffs_dir,
support_dir,
write_artifact_manifest,
write_handoff_payload,
)
CLAIMS_HEADERS = [
"Claim ID",
"Claim Text (Verbatim)",
"Category",
"Subcategory",
"Claim Type",
"Metric(s)",
"Period / As-of",
"Scope (Segment/Geo/Cohort)",
"Qualifiers",
"CIM Citation",
"Supporting Doc Citations",
"Confidence (1-5)",
"Materiality (H/M/L)",
"Implied Assumptions",
"Required Evidence (Minimum)",
"Verification Method",
"Linked Question IDs",
"Linked Red-Flag IDs",
"Status",
"Notes",
]
EVIDENCE_HEADERS = [
"Evidence ID",
"Related Claim ID(s)",
"Evidence Name",
"Evidence Type (Export/Doc/Report)",
"System of Record",
"Owner (Seller)",
"Granularity Required",
"Period Covered",
"Format Required",
"Purpose (What it proves)",
"Acceptance Criteria",
"CIM Citation",
"Received? (Y/N)",
"Received Date",
"Location (Data room path)",
"Issues / Gaps",
"Status",
]
QUESTIONS_HEADERS = [
"Question ID",
"Related Claim ID(s)",
"Priority Score",
"Impact (1-5)",
"Uncertainty (1-5)",
"Downside Risk (1-5)",
"Time Urgency (1-5)",
"Category",
"Question (Falsification-first)",
"Why it matters",
"Evidence Request (Exact)",
"Tie-out / Analysis Plan",
"Owner (Your team)",
"Owner (Seller)",
"Meeting Type (Email/Call/Data room)",
"Due Date",
"Status",
"CIM Citation",
"Follow-ups / Branching",
"Notes",
]
REDFLAG_HEADERS = [
"Red-Flag ID",
"Severity (1-5)",
"Related Claim ID(s)",
"Related Question ID(s)",
"Red Flag Description",
"Detection Method / Rule",
"Evidence Needed",
"What resolves it",
"Potential Impact",
"Status",
"Owner",
"CIM Citation",
"Notes",
]
WORKPLAN_HEADERS = [
"Workstream",
"Task ID",
"Task Description",
"Related Claim ID(s)",
"Evidence ID(s)",
"Question ID(s)",
"Owner (Your team)",
"Seller Owner",
"Dependencies",
"Start Date",
"Due Date",
"Status",
"CIM Citation",
"Output Artifact",
"Notes",
]
REQUIRED_FILES = {
"claims_ledger.csv": CLAIMS_HEADERS,
"evidence_checklist.csv": EVIDENCE_HEADERS,
"diligence_questions.csv": QUESTIONS_HEADERS,
"red_flag_register.csv": REDFLAG_HEADERS,
"workplan.csv": WORKPLAN_HEADERS,
}
def _sha256_text(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def _write_csv(path: Path, headers: list[str]) -> None:
with path.open("w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(headers)
def _write_html_report(path: Path, title: str, plan: dict[str, object]) -> None:
deal = escape(str(plan.get("deal_name", "Deal")))
persona = escape(str(plan.get("persona", "TBD")))
stage = escape(str(plan.get("deal_stage", "TBD")))
as_of = escape(str(plan.get("as_of", "TBD")))
html = f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{escape(title)}</title>
<style>
:root {{ --ink: #14202c; --sub: #586577; --teal: #1f625f; --teal-soft: #e9f3f1; --line: #d7dee5; --bg: #f5f6f4; --warn: #a65d18; }}
* {{ box-sizing: border-box; }}
body {{ margin: 0; font-family: Arial, Helvetica, sans-serif; background: var(--bg); color: var(--ink); line-height: 1.45; }}
header {{ background: #fff; border-bottom: 1px solid var(--line); padding: 34px 40px 30px; }}
.eyebrow {{ color: var(--teal); font-size: 0.75rem; font-weight: 700; letter-spacing: 0.13em; text-transform: uppercase; }}
h1 {{ margin: 10px 0 8px; font-size: clamp(1.8rem, 3.2vw, 2.55rem); letter-spacing: -0.04em; }}
.meta {{ color: var(--sub); font-size: 0.93rem; }}
main {{ max-width: 1140px; margin: 0 auto; padding: 26px 32px 44px; }}
.posture {{ display: grid; grid-template-columns: 1fr 280px; gap: 18px; background: #fff; border: 1px solid var(--line); border-left: 5px solid var(--teal); border-radius: 8px; padding: 20px 22px; }}
.posture h2 {{ margin: 4px 0 9px; font-size: 1.3rem; }}
.posture p {{ margin: 0; color: var(--sub); }}
.stamp {{ background: var(--teal-soft); border-radius: 6px; padding: 13px; font-size: 0.85rem; color: var(--teal); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; }}
.cards {{ display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin: 18px 0 25px; }}
.card {{ background: #fff; border: 1px solid var(--line); border-radius: 7px; padding: 14px; }}
.card label {{ display: block; color: var(--sub); font-size: 0.74rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }}
.card strong {{ display: block; margin-top: 7px; font-size: 1.24rem; }}
section {{ background: #fff; border: 1px solid var(--line); border-radius: 8px; margin-top: 15px; padding: 20px 22px; }}
section h2 {{ margin: 0 0 13px; font-size: 1.18rem; }}
.note {{ color: var(--sub); font-size: 0.92rem; }}
table {{ border-collapse: collapse; width: 100%; margin-top: 9px; font-size: 0.9rem; }}
th {{ color: var(--sub); font-size: 0.73rem; letter-spacing: 0.07em; text-transform: uppercase; background: #f7f9f9; }}
th, td {{ border-bottom: 1px solid var(--line); padding: 10px 9px; text-align: left; vertical-align: top; }}
td:first-child {{ font-weight: 700; color: var(--teal); white-space: nowrap; }}
.placeholder {{ color: #788493; font-style: italic; }}
.gate {{ color: var(--warn); font-weight: 700; }}
@media (max-width: 800px) {{ header {{ padding: 24px 18px; }} main {{ padding: 18px; }} .posture, .cards {{ grid-template-columns: 1fr; }} table {{ display: block; overflow-x: auto; }} }}
@media print {{ body {{ background: #fff; }} main {{ max-width: none; padding: 0; }} section, .posture, .card {{ break-inside: avoid; }} }}
</style>
</head>
<body>
<header>
<div class="eyebrow">Initial IC Discussion | CIM Teardown</div>
<h1>{escape(title)}</h1>
<div class="meta">{deal} | Persona: {persona} | Stage: {stage} | As of: {as_of}</div>
</header>
<main>
<article class="posture">
<div>
<div class="eyebrow">Initial IC Recommendation</div>
<h2>Populate proceed, pause, or pass posture from tested seller claims.</h2>
<p>This standalone scaffold is incomplete until claims, evidence gaps, red flags, and first-wave seller requests are populated from the source materials.</p>
</div>
<div class="stamp">Draft Scaffold<br>Not Decision Ready</div>
</article>
<div class="cards">
<div class="card"><label>Headline Price</label><strong class="placeholder">TBD</strong></div>
<div class="card"><label>Reported Metric</label><strong class="placeholder">TBD</strong></div>
<div class="card"><label>Preliminary Entry View</label><strong class="placeholder">TBD</strong></div>
<div class="card"><label>Evidence Posture</label><strong class="placeholder">TBD</strong></div>
</div>
<section>
<h2>Claims That Matter Most</h2>
<table><thead><tr><th>Claim</th><th>Why It Matters</th><th>Proof Status</th><th>Required Validation</th></tr></thead>
<tbody><tr><td>C-0001</td><td class="placeholder">Material seller claim</td><td class="placeholder">Needs proof</td><td>E-0001</td></tr></tbody></table>
</section>
<section>
<h2>Red Flags And Kill Tests</h2>
<table><thead><tr><th>Red Flag</th><th>Impact</th><th>What Resolves It</th><th>Pause / Reprice / Pass Test</th></tr></thead>
<tbody><tr><td>RF-0001</td><td class="placeholder">Decision impact</td><td class="placeholder">Required evidence</td><td class="gate">TBD</td></tr></tbody></table>
</section>
<section>
<h2>First-Wave Seller Data Request</h2>
<p class="note">Limit this section to evidence needed before deciding whether to proceed.</p>
<table><thead><tr><th>ID</th><th>Gate</th><th>Requested Evidence</th><th>Acceptance Criteria</th></tr></thead>
<tbody><tr><td>E-0001</td><td class="placeholder">First-wave gate</td><td class="placeholder">Exact seller request</td><td class="placeholder">TBD</td></tr></tbody></table>
</section>
<section>
<h2>Quick Underwriting Implications</h2>
<p class="note">Show preliminary math only when price and relevant reported cash flow or earnings are available; distinguish seller-reported figures from normalized underwriting.</p>
</section>
<section>
<h2>Evidence, Limitations And Appendices</h2>
<p class="note">Retain full linked claims, evidence, questions, red flags, and workplan schedules as support exports when needed for downstream diligence or modeling.</p>
</section>
</main>
</body>
</html>
"""
path.write_text(html, encoding="utf-8")
def main() -> int:
if len(sys.argv) != 2:
print("Usage: python scripts/run_plan.py plan.json")
return 1
plan_path = Path(sys.argv[1])
if not plan_path.exists():
print(f"ERROR: plan file not found: {plan_path}")
return 1
plan_text = plan_path.read_text(encoding="utf-8")
try:
plan = json.loads(plan_text)
except Exception as e:
print(f"ERROR: failed to parse JSON: {e}")
return 1
output = plan.get("output") or {}
output_dir = Path(output.get("output_dir", "outputs")).resolve()
output_dir.mkdir(parents=True, exist_ok=True)
csv_dir = support_dir(output_dir)
handoff_dir = handoffs_dir(output_dir)
# Create required CSV templates
for fname, headers in REQUIRED_FILES.items():
fpath = csv_dir / fname
if fpath.exists():
print(f"[SKIP] {fpath} already exists")
else:
_write_csv(fpath, headers)
print(f"[OK] wrote {fpath}")
# Skeleton deal package JSON
deal_pkg_path = handoff_dir / "deal_package.json"
if not deal_pkg_path.exists():
deal_pkg = {
"deal": {
"name": plan.get("deal_name", ""),
"stage": plan.get("deal_stage", ""),
"persona": plan.get("persona", ""),
"as_of": plan.get("as_of", ""),
},
"documents": plan.get("inputs", {}),
"claims": [],
"evidence": [],
"questions": [],
"red_flags": [],
"workplan": [],
"links": [],
}
deal_pkg_path.write_text(json.dumps(deal_pkg, indent=2), encoding="utf-8")
print(f"[OK] wrote {deal_pkg_path}")
# Standalone report scaffold; structured schedules remain available as support artifacts.
summary_path = output_dir / "cim_teardown_report.html"
_write_html_report(summary_path, "CIM Teardown Report", plan)
print(f"[OK] wrote {summary_path}")
handoff_results = []
handoff_overrides = {
"transaction_context": plan.get("deal_name", "CIM teardown"),
"deal_name": plan.get("deal_name", ""),
"asset_type": "not_provided",
"as_of_date": plan.get("as_of", ""),
"source_scope": "CIM teardown scaffold",
"circulation_caveats": [
{
"caveat": "Scaffold only until populated from extracted CIM evidence.",
"impact": "not client-ready",
"owner": "VP",
}
],
}
for contract_name, consumer in [
("cim_teardown_to_memo_builder", "memo-builder"),
("cim_teardown_to_model_builder", "model-builders"),
]:
handoff_results.append(
write_handoff_payload(
output_dir,
contract_name,
build_minimal_handoff_payload(contract_name, handoff_overrides),
consumer_skill=consumer,
)
)
# Manifest
support_artifacts = [
artifact_item(
csv_dir / fname,
"support_artifact",
"csv",
f"{fname} diligence scaffold table.",
False,
False,
"CSV scaffold supports extraction and diligence tracking; it is not the report the banker should open first.",
)
for fname in REQUIRED_FILES
]
support_artifacts.extend(
[
artifact_item(
deal_pkg_path,
"support_artifact",
"json",
"Legacy deal package support JSON.",
False,
False,
"Legacy package JSON is machine-readable support; validated cross-skill handoffs live under handoffs/ with explicit contract metadata.",
),
*[handoff_artifact_item(result) for result in handoff_results],
]
)
write_artifact_manifest(
output_dir,
"cim-teardown",
"html_report",
summary_path,
support_artifacts=support_artifacts,
blocked_or_partial_status={
"status": "partial",
"reason": "This is a scaffold until claim extraction and evidence review are populated.",
"missing_inputs": [
"Claim ledger",
"Evidence checklist",
"Red flag register",
"Diligence workplan",
],
},
extra={
"generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"plan_path": str(plan_path),
"plan_sha256": _sha256_text(plan_text),
"handoffs": [
{
"handoff_contract_name": result["handoff_contract_name"],
"path": result["path"],
"schema_path": result["schema_path"],
"validator_status": result["validator_status"],
"validated_at": result["validated_at"],
"consumer_skill": result["consumer_skill"],
}
for result in handoff_results
],
},
)
print(f"[OK] wrote {output_dir / 'manifest.json'}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: ac8f167347d85e2318bb9c8d48c3718f01ac9b46ab86c65f368d3890d3fa7354