chore: sync working tree changes

This commit is contained in:
root
2026-08-08 13:06:45 -04:00
parent df9fca8631
commit 0bc671b965
3 changed files with 784 additions and 0 deletions
+200
View File
@@ -0,0 +1,200 @@
#!/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 '<span class="score-bar score-na" style="width:80px"></span> 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'<span class="score-bar {cls}" style="width:{width}px"></span> {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'<span class="flaw-label {cls}">{severity}</span>')
def build_score_rows(scores):
rows = ""
for dim in scores:
rows += clean(f'<tr><td class="dim">{dim["name"]}</td><td>{score_bar(dim["score"])}</td><td style="font-size:8.5pt;color:#555">{dim["note"]}</td></tr>\n')
return rows
def build_flaws(flaws):
html = ""
for i, f in enumerate(flaws, 1):
html += clean(f'''<div class="flaw-box">
<span class="flaw-num">#{i}</span> <strong>{f["title"]}</strong> {flaw_label(f.get("severity", "Structural"))}
<p style="margin-top:4px;font-size:9pt;color:#555;">{f["detail"]}</p>
</div>\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 = '<span class="judge-verdict upheld">UPHOLDS</span>'
else:
verdict_html = clean(f'<span class="judge-verdict overruled">{j.get("verdict", "DISAGREES")}</span>')
confidence = j.get("confidence", "")
conf_html = f'<span class="judge-confidence">{confidence} confidence</span>' if confidence else ""
html += clean(f'''<div class="judge-card">
<div class="judge-name">{j["name"]}</div>
{verdict_html}
{conf_html}
<p class="judge-note">{j.get("note", "")}</p>
</div>\n''')
return html
def build_strengths(items):
return "\n".join(clean(f"<li>{s}</li>") 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'<tr><td class="priority {cls}">P{p}</td><td><strong>{a["action"]}</strong></td><td style="font-size:8.5pt;color:#555">{a["why"]}</td><td style="font-size:8.5pt;color:#999">{a["effort"]}</td></tr>\n')
return html
def build_research(data):
html = ""
for r in data:
html += clean(f'''<div class="finding">
<div class="finding-label">{r["label"]}</div>
<div class="finding-text">{r["text"]}</div>
<div class="finding-source">Source: {r.get("source", "Research sweep")}</div>
</div>\n''')
return html
def build_competitor_rows(competitors):
html = ""
for c in competitors:
html += clean(f'<tr><td><strong>{c["name"]}</strong></td><td style="font-size:8.5pt">{c["strengths"]}</td><td style="font-size:8.5pt;color:#cc0000">{c["gaps"]}</td><td style="font-size:8pt;color:#999">{c["threat"]}</td></tr>\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'''<div style="margin-bottom:12px">
<strong>{j["name"]}:</strong> {j["dissent"]}
</div>\n''')
else:
html += clean(f'''<div style="margin-bottom:12px">
<strong>{j["name"]}:</strong> No dissent. Agreed with findings and scoring without material deviation.
</div>\n''')
return html
def build_citations(citations):
html = ""
for i, c in enumerate(citations, 1):
url_html = f' <a href="{c["url"]}">{c["url"]}</a>' if c.get("url") else ""
html += clean(f'<div class="citation">[{i}] {c["text"]}{url_html}</div>\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}")