#!/usr/bin/env python3 # /// script # requires-python = ">=3.10" # /// """Render the analysis report deterministically from findings JSON. Injects a validated findings JSON object into the report shell's report-data island and writes the self-contained HTML atomically. With --md, also writes a markdown rendering of the same data as the archival artifact. Refuses (non-zero exit, message on stderr) when the JSON does not parse, fails shape validation, or still carries the shell's placeholder subject — a refused render means fix the findings file and re-run, never hand-edit the HTML. Usage: uv run render_report.py --shell \ -o [--md ] On success prints one JSON line: output paths, grade, and severity counts derived from the findings array. """ from __future__ import annotations import argparse import json import os import re import sys import tempfile from pathlib import Path SEVERITIES = ("critical", "high", "medium", "low") GRADES = ("excellent", "good", "fair", "poor") PLACEHOLDER_SUBJECT = "__PLACEHOLDER__" ISLAND_RE = re.compile( r'(]*\bid="report-data"[^>]*>)(.*?)()', re.DOTALL ) def fail(message: str) -> None: print(f"render_report: {message}", file=sys.stderr) sys.exit(1) def validate(data: object) -> list[str]: """Return a list of shape errors; empty list means valid.""" if not isinstance(data, dict): return ["top level must be a JSON object"] errors: list[str] = [] subject = data.get("subject") if not isinstance(subject, str) or not subject.strip(): errors.append('"subject" must be a non-empty string') elif PLACEHOLDER_SUBJECT in subject: errors.append( f'"subject" still carries the placeholder {PLACEHOLDER_SUBJECT}; ' "this is the unfilled shell sample, not real findings" ) findings = data.get("findings") if not isinstance(findings, list): errors.append('"findings" must be an array (use [] for a clean pass)') else: for i, finding in enumerate(findings): if not isinstance(finding, dict): errors.append(f"findings[{i}] must be an object") grade = data.get("grade") if grade is not None and grade not in GRADES: errors.append(f'"grade" must be one of: {", ".join(GRADES)}') for key in ("themes", "recommendations"): value = data.get(key) if value is not None and ( not isinstance(value, list) or any(not isinstance(item, dict) for item in value) ): errors.append(f'"{key}" must be an array of objects') strengths = data.get("strengths") if strengths is not None and ( not isinstance(strengths, list) or any(not isinstance(item, str) for item in strengths) ): errors.append('"strengths" must be an array of strings') return errors def severity_counts(findings: list[dict]) -> dict[str, int]: counts = {sev: 0 for sev in SEVERITIES} for finding in findings: sev = finding.get("severity") counts[sev if sev in counts else "low"] += 1 return counts def inject(shell_html: str, data: dict) -> str: payload = json.dumps(data, ensure_ascii=False, indent=2) # A " str: return match.group(1) + "\n" + payload + "\n" + match.group(3) new_html, count = ISLAND_RE.subn(replace, shell_html, count=1) if count != 1: fail('shell has no