#!/usr/bin/env python3
"""
VerdictTank PDF Generator - fills template with verdict data, renders to PDF via WeasyPrint.
Usage: python3 generate-pdf.py [--data data.json] [--output verdict-report.pdf]
"""
import json, os, sys, re
from datetime import datetime
from weasyprint import HTML
TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), "pdf-template.html")
OUTPUT_DIR = "/var/www/verdicttank/reports"
EM_DASH_PATTERN = re.compile(r'\u2014|\u2013') # em dash and en dash
def clean(text):
"""Strip all em/en dashes - replace with hyphen."""
return EM_DASH_PATTERN.sub('-', str(text))
def score_bar(score):
if score is None:
return ' N/A'
width = score * 8
if score <= 3:
cls, label = "score-critical", "Critical"
elif score <= 5:
cls, label = "score-warning", "Weak"
elif score <= 7:
cls, label = "score-ok", "Adequate"
else:
cls, label = "score-good", "Strong"
return clean(f' {score}/10 - {label}')
def flaw_label(severity):
mapping = {
"Existential": "flaw-existential",
"Structural": "flaw-structural",
"Financial": "flaw-financial",
"Operational": "flaw-operational",
"Brand-fatal": "flaw-existential",
}
cls = mapping.get(severity, "flaw-structural")
return clean(f'{severity}')
def build_score_rows(scores):
rows = ""
for dim in scores:
rows += clean(f'
| {dim["name"]} | {score_bar(dim["score"])} | {dim["note"]} |
\n')
return rows
def build_flaws(flaws):
html = ""
for i, f in enumerate(flaws, 1):
html += clean(f'''
#{i} {f["title"]} {flaw_label(f.get("severity", "Structural"))}
{f["detail"]}
\n''')
return html
def build_judge_cards(judges):
"""Build judge cards - uses data-provided names (expected: Judge 1, Judge 2, Judge 3)."""
html = ""
for i, j in enumerate(judges):
verdict_html = ""
if j.get("verdict") == "upheld":
verdict_html = 'UPHOLDS'
else:
verdict_html = clean(f'{j.get("verdict", "DISAGREES")}')
confidence = j.get("confidence", "")
conf_html = f'{confidence} confidence' if confidence else ""
html += clean(f'''
{j["name"]}
{verdict_html}
{conf_html}
{j.get("note", "")}
\n''')
return html
def build_strengths(items):
return "\n".join(clean(f"{s}") for s in items)
def build_action_rows(actions):
html = ""
for a in actions:
p = a["priority"]
cls = f"priority-p{p}" if p in ("0", "1", "2") else ""
html += clean(f'| P{p} | {a["action"]} | {a["why"]} | {a["effort"]} |
\n')
return html
def build_research(data):
html = ""
for r in data:
html += clean(f'''
{r["label"]}
{r["text"]}
Source: {r.get("source", "Research sweep")}
\n''')
return html
def build_competitor_rows(competitors):
html = ""
for c in competitors:
html += clean(f'| {c["name"]} | {c["strengths"]} | {c["gaps"]} | {c["threat"]} |
\n')
return html
def build_judge_notes(judges):
"""Judge notes - uses data-provided judge names, no model identifiers."""
html = ""
for i, j in enumerate(judges):
if j.get("dissent"):
html += clean(f'''
{j["name"]}: {j["dissent"]}
\n''')
else:
html += clean(f'''
{j["name"]}: No dissent. Agreed with findings and scoring without material deviation.
\n''')
return html
def build_citations(citations):
html = ""
for i, c in enumerate(citations, 1):
url_html = f' {c["url"]}' if c.get("url") else ""
html += clean(f'[{i}] {c["text"]}{url_html}
\n')
return html
def fill_template(data):
with open(TEMPLATE_PATH, "r") as f:
template = f.read()
judges = data.get("judges", [])
replacements = {
"{{PROPOSAL_NAME}}": clean(data.get("proposal_name", "Untitled Proposal")),
"{{SUBMITTER_NAME}}": clean(data.get("submitter_name", "Unknown")),
"{{REVIEW_DATE}}": clean(data.get("review_date", datetime.now().strftime("%B %d, %Y"))),
"{{REVIEW_ID}}": clean(data.get("review_id", "VT-" + datetime.now().strftime("%Y%m%d-%H%M"))),
"{{EXECUTIVE_SUMMARY}}": clean(data.get("executive_summary", "")),
"{{SCORE_ROWS}}": build_score_rows(data.get("scores", [])),
"{{FATAL_FLAWS}}": build_flaws(data.get("fatal_flaws", [])),
"{{JUDGE_CARDS}}": build_judge_cards(judges),
"{{JUDGE_MAJORITY_VERDICT}}": clean(data.get("judge_majority_verdict", "NO CONSENSUS")),
"{{JUDGE_AGREEMENT}}": str(data.get("judge_agreement", 0)),
"{{JUDGE_TOTAL}}": str(len(judges)),
"{{STRENGTHS}}": build_strengths(data.get("strengths", [])),
"{{RESEARCH_FINDINGS}}": build_research(data.get("research", [])),
"{{COMPETITOR_ROWS}}": build_competitor_rows(data.get("competitors", [])),
"{{ACTION_ROWS}}": build_action_rows(data.get("action_plan", [])),
"{{JUDGE_NOTES}}": build_judge_notes(judges),
"{{CITATIONS}}": build_citations(data.get("citations", [])),
}
for placeholder, value in replacements.items():
template = template.replace(placeholder, value)
# Kill any remaining unreplaced placeholders
template = re.sub(r'\{\{[A-Z_]+\}\}', 'N/A', template)
return template
def generate(data_path, output_path=None):
with open(data_path, "r") as f:
data = json.load(f)
html = fill_template(data)
if not output_path:
os.makedirs(OUTPUT_DIR, exist_ok=True)
client = data.get("submitter_name", "Client")
safe_client = re.sub(r'[^a-zA-Z0-9]', '', client)
if not safe_client:
safe_client = "Client"
output_path = os.path.join(OUTPUT_DIR, f"{safe_client}-VerdictTank-Report.pdf")
HTML(string=html).write_pdf(output_path)
return output_path
if __name__ == "__main__":
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("--data", required=True, help="JSON data file")
ap.add_argument("--output", help="Output PDF path")
args = ap.parse_args()
path = generate(args.data, args.output)
print(f"PDF generated: {path}")