orgstate / delivery /reports /render.py
Legal-i's picture
Initial OrgState deploy via Stage 150 free-tier stack
d2d1903 verified
"""
delivery.reports.render — render a CustomerReport to Markdown / HTML.
The findings block delegates to the Stage 4a evidence renderers, so card
formatting, escaping, and severity badges stay consistent between the
per-issue view and the full report.
"""
from __future__ import annotations
import html as _html
from typing import List
from ..evidence import EvidenceView
from ..evidence.render import (
_card_html as _evidence_card_html,
)
from ..evidence.render import (
_card_markdown as _evidence_card_markdown,
)
from ..evidence.render import (
_esc,
_fmt_pct,
_fmt_score,
_severity_summary_line,
)
from .customer import CustomerReport, ReportDecision
_REPORT_TITLE = "Operational Drift Report"
# --- diff section (Stage 21) ---------------------------------------------
def _entity_pill(item: dict, *, with_from: bool = False) -> str:
"""One pill (entity id + severity badge) used in the diff lists."""
eid = _esc(item.get("entity_id", ""))
sev = item.get("severity", "")
if with_from and item.get("from_severity"):
from_sev = _esc(item["from_severity"])
return (
f'<li><code>{eid}</code> '
f'<span class="sev sev-{_esc(from_sev)}">{from_sev}</span>'
f' &rarr; <span class="sev sev-{_esc(sev)}">{_esc(sev)}</span></li>'
)
return (
f'<li><code>{eid}</code> '
f'<span class="sev sev-{_esc(sev)}">{_esc(sev)}</span></li>'
)
def _diff_section_html(diff: dict) -> str:
"""Render the 'Since previous run' callout. Suppressed entirely when
every counter is zero — an empty diff is just visual noise."""
n_new = diff.get("n_new", 0)
n_resolved = diff.get("n_resolved", 0)
n_severity_changed = diff.get("n_severity_changed", 0)
n_unchanged = diff.get("n_unchanged", 0)
against = diff.get("against_run_id")
if not any((n_new, n_resolved, n_severity_changed)):
# nothing changed — surface that fact briefly, since "no news"
# IS the headline some days
return (
'<div class="diff-callout diff-quiet">'
'<strong>Since previous run:</strong> no changes '
f'({n_unchanged} unchanged).'
'</div>'
)
sections = []
if against is None:
sections.append(
'<p class="meta">This is the first run on this tenant — '
'every issue below is new.</p>'
)
def _list_block(title, items, with_from=False):
if not items:
return ""
pills = "".join(_entity_pill(it, with_from=with_from)
for it in items[:20])
more = "" if len(items) <= 20 else (
f' <span class="meta">+{len(items) - 20} more</span>'
)
return (
f'<div class="diff-bucket"><strong>{_esc(title)} '
f'({len(items)})</strong>'
f'<ul class="diff-pills">{pills}</ul>{more}</div>'
)
sections.append(_list_block("New", diff.get("new", [])))
sections.append(_list_block("Severity changed",
diff.get("severity_changed", []),
with_from=True))
sections.append(_list_block("Resolved", diff.get("resolved", [])))
return (
'<div class="diff-callout">'
'<h2>Since previous run</h2>'
'<p class="counts">'
f'<strong>{n_new}</strong> new &middot; '
f'<strong>{n_severity_changed}</strong> severity changed &middot; '
f'<strong>{n_resolved}</strong> resolved &middot; '
f'<strong>{n_unchanged}</strong> unchanged'
'</p>'
+ "".join(sections)
+ '</div>'
)
# --- markdown -------------------------------------------------------------
def _summary_markdown(report: CustomerReport) -> List[str]:
s = report.summary
return [
"## Executive Summary",
"",
s.headline,
"",
f"- **Entities analysed:** {s.n_entities_analyzed}",
f"- **Drift issues:** {s.n_issues} ({_severity_summary_line(report.evidence)})",
f"- **Decisions raised:** {s.n_decisions}",
"",
]
def _findings_markdown(view: EvidenceView) -> List[str]:
if not view.cards:
return [
"## Findings",
"",
"No entities crossed a drift threshold on this run.",
"",
]
lines = ["## Findings", ""]
for card in view.cards:
lines += _evidence_card_markdown(card)
return lines
def _decisions_markdown(decisions: List[ReportDecision]) -> List[str]:
if not decisions:
return [
"## Decision Queue",
"",
"No decisions raised on this run.",
"",
]
lines = [
"## Decision Queue",
"",
"Ranked by urgency. Each decision links back to the issue that triggered it.",
"",
"| Urgency | Entity | Recommendation | Confidence | Issue |",
"|---:|---|---|---:|---|",
]
for d in decisions:
lines.append(
f"| {_fmt_score(d.urgency)} | `{d.entity_id}` | {d.recommendation} | "
f"{_fmt_pct(d.confidence)} | `{d.issue_id}` |"
)
lines.append("")
return lines
def _methodology_markdown() -> List[str]:
return [
"## Methodology",
"",
"Drift is detected by a generic, config-driven pipeline over a "
"per-tenant calibration derived from the tenant's own history (MAD-robust). "
"Each issue is scored from five signals (Δ, ψ, ξ, γ, κ) and mapped to a "
"severity. Decisions are recommendations grounded in the evidence of the "
"issue that triggered them — they are operational hypotheses to validate, "
"not guaranteed outcomes.",
"",
]
def render_markdown(report: CustomerReport) -> str:
lines = [
f"# {_REPORT_TITLE} — `{report.tenant_id}`",
"",
(
f"- **Run:** `{report.run_id}` "
f"\n- **Entity type:** {report.entity_type} "
f"\n- **Finished at:** {report.finished_at}"
),
"",
]
lines += _summary_markdown(report)
lines += _findings_markdown(report.evidence)
lines += _decisions_markdown(report.decisions)
lines += _methodology_markdown()
return "\n".join(lines).rstrip() + "\n"
# --- html -----------------------------------------------------------------
_HTML_HEAD = (
"<!doctype html><html><head><meta charset=\"utf-8\">"
"<title>Operational Drift Report</title>"
"<style>"
"body{font-family:-apple-system,Segoe UI,Arial,sans-serif;"
"max-width:960px;margin:32px auto;padding:0 16px;line-height:1.5;color:#222}"
"h1{font-size:1.7em;margin-bottom:0.1em}"
"h2{margin-top:1.8em;border-bottom:1px solid #ddd;padding-bottom:0.2em}"
"h3{margin-top:1.3em}"
"table{border-collapse:collapse;width:100%;margin:0.6em 0}"
"th,td{border:1px solid #ddd;padding:6px 8px;text-align:left;font-size:0.95em}"
"th{background:#f4f4f4}"
".sev{display:inline-block;padding:1px 8px;border-radius:3px;"
"font-size:0.85em;font-weight:600;color:#fff}"
".sev-critical{background:#a00}.sev-high{background:#d35400}"
".sev-medium{background:#b8860b}.sev-low{background:#777}"
".meta{color:#555;font-size:0.93em}"
".headline{font-size:1.05em;margin:0.4em 0 1em 0}"
"code{background:#f4f4f4;padding:1px 4px;border-radius:3px}"
".diff-callout{padding:14px 18px;background:#eef6fb;"
"border:1px solid #b8d8e8;border-radius:6px;margin:1em 0 2em}"
".diff-callout h2{margin-top:0;border:none;color:#1f4f6f;font-size:1.2em}"
".diff-quiet{padding:8px 14px;color:#555;background:#f5f5f5;"
"border:1px solid #ddd;border-radius:4px;margin:1em 0 2em;font-size:0.95em}"
".diff-bucket{margin-top:0.5em}"
".diff-pills{list-style:none;padding:0;margin:0.4em 0;display:flex;"
"flex-wrap:wrap;gap:6px}"
".diff-pills li{padding:2px 8px;background:#fff;border:1px solid #cdd9e1;"
"border-radius:12px;font-size:0.9em}"
".counts{margin:0.4em 0;font-size:0.96em}"
"</style></head><body>"
)
_HTML_FOOT = "</body></html>"
def _summary_html(report: CustomerReport) -> str:
s = report.summary
parts = [
"<h2>Executive Summary</h2>",
f'<p class="headline">{_esc(s.headline)}</p>',
"<ul>",
f"<li><strong>Entities analysed:</strong> {s.n_entities_analyzed}</li>",
(
"<li><strong>Drift issues:</strong> "
f"{s.n_issues} ({_esc(_severity_summary_line(report.evidence))})</li>"
),
f"<li><strong>Decisions raised:</strong> {s.n_decisions}</li>",
"</ul>",
]
return "".join(parts)
def _findings_html(view: EvidenceView) -> str:
if not view.cards:
return (
"<h2>Findings</h2>"
"<p>No entities crossed a drift threshold on this run.</p>"
)
parts = ["<h2>Findings</h2>"]
for card in view.cards:
parts.append(_evidence_card_html(card))
return "".join(parts)
def _decisions_html(decisions: List[ReportDecision]) -> str:
if not decisions:
return "<h2>Decision Queue</h2><p>No decisions raised on this run.</p>"
parts = [
"<h2>Decision Queue</h2>",
'<p class="meta">Ranked by urgency. Each decision links back to '
"the issue that triggered it.</p>",
"<table><thead><tr>"
"<th>Urgency</th><th>Entity</th><th>Recommendation</th>"
"<th>Confidence</th><th>Issue</th>"
"</tr></thead><tbody>",
]
for d in decisions:
parts.append(
"<tr>"
f"<td>{_esc(_fmt_score(d.urgency))}</td>"
f"<td><code>{_esc(d.entity_id)}</code></td>"
f"<td>{_esc(d.recommendation)}</td>"
f"<td>{_esc(_fmt_pct(d.confidence))}</td>"
f"<td><code>{_esc(d.issue_id)}</code></td>"
"</tr>"
)
parts.append("</tbody></table>")
return "".join(parts)
def _methodology_html() -> str:
return (
"<h2>Methodology</h2>"
"<p>Drift is detected by a generic, config-driven pipeline over a "
"per-tenant calibration derived from the tenant's own history "
"(MAD-robust). Each issue is scored from five signals (Δ, ψ, ξ, γ, κ) "
"and mapped to a severity. Decisions are recommendations grounded in "
"the evidence of the issue that triggered them &mdash; they are "
"operational hypotheses to validate, not guaranteed outcomes.</p>"
)
def render_html(report: CustomerReport) -> str:
body = [
_HTML_HEAD,
f"<h1>{_html.escape(_REPORT_TITLE)} &mdash; "
f"<code>{_esc(report.tenant_id)}</code></h1>",
'<p class="meta">',
f"Run <code>{_esc(report.run_id)}</code>",
f" &middot; entity type {_esc(report.entity_type)}",
f" &middot; finished {_esc(report.finished_at)}",
"</p>",
# Stage 21: "Since previous run" callout, above the summary so it
# is the first thing an operator opening the daily report sees
_diff_section_html(report.diff) if report.diff is not None else "",
_summary_html(report),
_findings_html(report.evidence),
_decisions_html(report.decisions),
_methodology_html(),
_HTML_FOOT,
]
return "".join(body)