Files
bikini-patchwork/patchwork/report.py
T
2026-06-04 04:26:04 -05:00

132 lines
4.0 KiB
Python

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(
"<tr>"
f"<td>{_escape(indicator.get('line', ''))}</td>"
f"<td>{_escape(indicator.get('kind', ''))}</td>"
f"<td>{_escape(indicator.get('name', ''))}</td>"
f"<td>{_escape(indicator.get('reason', ''))}</td>"
"</tr>"
)
return "\n".join(rows) or '<tr><td colspan="4">No review indicators found.</td></tr>'
def _option_rows(options: dict[str, object]) -> str:
return "\n".join(
f"<tr><td>{_escape(key)}</td><td><code>{_escape(value)}</code></td></tr>"
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"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Patchwork Build Report</title>
<style>
:root {{
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.45;
color: #15181f;
background: #f5f7fa;
}}
body {{
margin: 0;
}}
main {{
max-width: 1080px;
margin: 0 auto;
padding: 32px 20px 48px;
}}
section {{
background: #ffffff;
border: 1px solid #dce2ea;
border-radius: 8px;
margin: 16px 0;
padding: 18px;
}}
table {{
border-collapse: collapse;
width: 100%;
background: #ffffff;
}}
th, td {{
border-bottom: 1px solid #e7ebf0;
padding: 9px;
text-align: left;
vertical-align: top;
font-size: 14px;
}}
th {{
background: #edf1f6;
}}
code {{
font-family: Consolas, "Liberation Mono", monospace;
overflow-wrap: anywhere;
}}
</style>
</head>
<body>
<main>
<h1>Patchwork Build Report</h1>
<section>
<h2>Source Audit</h2>
<p><strong>Path:</strong> {_escape(audit.get('path'))}</p>
<p><strong>SHA-256:</strong> <code>{_escape(audit.get('sha256'))}</code></p>
<p><strong>Lines:</strong> {_escape(audit.get('lines'))}</p>
<p><strong>AST nodes:</strong> {_escape(audit.get('ast_nodes'))}</p>
<p><strong>Review indicators:</strong> {_escape(review.get('indicator_count', 0))}</p>
</section>
<section>
<h2>Output Stats</h2>
<p><strong>Output SHA-256:</strong> <code>{_escape(stats.get('output_sha256', 'not generated'))}</code></p>
<p><strong>Input bytes:</strong> {_escape(stats.get('input_bytes', 'not generated'))}</p>
<p><strong>Output bytes:</strong> {_escape(stats.get('output_bytes', 'not generated'))}</p>
<p><strong>Expansion ratio:</strong> {_escape(stats.get('ratio', 'not generated'))}</p>
</section>
<section>
<h2>Options</h2>
<table>
<thead><tr><th>Option</th><th>Value</th></tr></thead>
<tbody>{_option_rows(options)}</tbody>
</table>
</section>
<section>
<h2>Review Indicators</h2>
<table>
<thead><tr><th>Line</th><th>Kind</th><th>Name</th><th>Reason</th></tr></thead>
<tbody>{_indicator_rows(audit)}</tbody>
</table>
</section>
</main>
</body>
</html>
"""
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