from __future__ import annotations
import html
from pathlib import Path
from typing import Any
def _escape(value: object) -> str:
return html.escape(str(value), quote=True)
def _indicator_rows(audit: dict[str, Any]) -> str:
indicators = audit.get("review", {}).get("indicators", [])
rows: list[str] = []
if isinstance(indicators, list):
for indicator in indicators:
if not isinstance(indicator, dict):
continue
rows.append(
"
"
f"| {_escape(indicator.get('line', ''))} | "
f"{_escape(indicator.get('kind', ''))} | "
f"{_escape(indicator.get('name', ''))} | "
f"{_escape(indicator.get('reason', ''))} | "
"
"
)
return "\n".join(rows) or '| No review indicators found. |
'
def _option_rows(options: dict[str, object]) -> str:
return "\n".join(
f"| {_escape(key)} | {_escape(value)} |
"
for key, value in sorted(options.items())
)
def render_report(*, audit: dict[str, Any], options: dict[str, object], stats: dict[str, object] | None = None) -> str:
review = audit.get("review", {})
stats = stats or {}
return f"""
Patchwork Build Report
Patchwork Build Report
Source Audit
Path: {_escape(audit.get('path'))}
SHA-256: {_escape(audit.get('sha256'))}
Lines: {_escape(audit.get('lines'))}
AST nodes: {_escape(audit.get('ast_nodes'))}
Review indicators: {_escape(review.get('indicator_count', 0))}
Output Stats
Output SHA-256: {_escape(stats.get('output_sha256', 'not generated'))}
Input bytes: {_escape(stats.get('input_bytes', 'not generated'))}
Output bytes: {_escape(stats.get('output_bytes', 'not generated'))}
Expansion ratio: {_escape(stats.get('ratio', 'not generated'))}
Options
| Option | Value |
{_option_rows(options)}
Review Indicators
| Line | Kind | Name | Reason |
{_indicator_rows(audit)}
"""
def write_report(path: str | Path, *, audit: dict[str, Any], options: dict[str, object], stats: dict[str, object] | None = None) -> Path:
output_path = Path(path)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(render_report(audit=audit, options=options, stats=stats), encoding="utf-8")
return output_path