Skip to content

k4bench.regression.email

k4bench.regression.email

Render the nightly regression report as the e-group email — subject, hidden preheader, and the two MIME bodies (HTML and plain-text/Markdown).

The email reads at two levels, top to bottom:

  1. Header + compact primary actions (Open dashboard / CI run).
  2. A status summary — separate Failure / New / Reconfirmed / Watch / coverage counts, never colour alone.
  3. Needs attention — one compact card per (detector, platform, sample) run group that has a failure, a new confirmation, or a same-release reconfirmation, worst category first, with only the largest few changes and the ranked candidate PRs for that change window.
  4. Detailed report — reference — the full detector → run-group hierarchy, bounded so a pathological night can't produce a multi-hundred-kilobyte mail (every failure always shown; confirmed rows capped per group and globally with honest "Showing X of Y" omitted counts).
  5. Coverage / data-quality summary and footer.

Vocabulary is release-scoped and mandatory (see :class:~k4bench.regression.models.MetricVerdict): New is the first confirmation for the release being measured; Reconfirmed is a later night of the same release re-confirming it. The engine has already made that call — this renderer only reads is_new_confirmation / is_reconfirmed and never re-infers persistence across releases.

Blame (model-ranked candidate PRs) is best-effort: a missing, malformed, or mismatched sidecar silently degrades to no ranking and never blocks the mail. Everything read from blame.json — including URLs — is escaped on the way into markup; it is file content and model output, not something this process fetched itself.

EmailSummary dataclass

EmailSummary(night: str, n_failures: int, n_new: int, n_reconfirmed: int, n_watch: int, groups_total: int, groups_judged: int, groups_reliable: int, groups_unreliable: int, groups_unknown: int)

The night's headline counts, shared by the subject, the summary cells and the coverage roster so all three agree.

coverage_text property

coverage_text: str

7/7 groups judged — groups with at least one non-UNKNOWN metric.

This is intentionally derived from verdicts, not the host-reliability tri-state: a reliable first run may still lack enough history to judge, while legacy reports can carry judged metrics without a reliability field.

RankingCard dataclass

RankingCard(detector: str, platform: str, sample: str, base_release: str | None, onset_release: str, n_signals: int, total_window_signals: int, n_new: int, total_window_new: int, n_reconfirmed: int, total_window_reconfirmed: int, candidates: list[CandidatePR], total_ranked: int, compare_links: list[tuple[str, str]], total_compares: int, reused_from: str | None, assessment: StepAssessment | None = None)

One deduplicated ranking for a rank group (detector, platform, sample, base_release, onset_release) — rendered once no matter how many metrics share it.

WindowSection dataclass

WindowSection(detector: str, platform: str, sample: str, k4h_release: str, base_release: str | None, onset_release: str | None, verdicts: list[MetricVerdict], card: RankingCard | None)

One change window: the confirmed metrics whose change entered in it, and the ranked PRs for that window when the sidecar has them.

Sections come from the verdicts, not from the blame sidecar, so a window with no ranking still lists its metrics instead of vanishing — the window is what the reader has to act on, the ranking only helps.

same_release property

same_release: bool

Both ends in one release: nothing upstream moved, so the cause is benchmark-side or noise.

subject

subject(report: NightlyReport) -> str

State-aware, grammatically correct subject.

Priority: any failure or New confirmation → [ACTION]; else any Reconfirmed → [RECONFIRMED]; else any Watch → [WATCH]; then missing or unreliable coverage → [NO DATA] / [INCOMPLETE]; else [OK]. Failure counts lead the regression counts when present.

Source code in k4bench/regression/email.py
def subject(report: NightlyReport) -> str:
    """State-aware, grammatically correct subject.

    Priority: any failure or New confirmation → ``[ACTION]``; else any
    Reconfirmed → ``[RECONFIRMED]``; else any Watch → ``[WATCH]``; then missing
    or unreliable coverage → ``[NO DATA]`` / ``[INCOMPLETE]``; else ``[OK]``.
    Failure counts lead the regression counts when present.
    """
    s = EmailSummary.of(report)
    if s.n_failures or s.n_new:
        tag, parts = "ACTION", []
        if s.n_failures:
            parts.append(_plural(s.n_failures, "failure"))
        if s.n_new:
            parts.append(
                f"{s.n_new} new" if s.n_reconfirmed
                else f"{s.n_new} new {'regression' if s.n_new == 1 else 'regressions'}"
            )
        if s.n_reconfirmed:
            parts.append(f"{s.n_reconfirmed} reconfirmed")
        desc = ", ".join(parts)
    elif s.n_reconfirmed:
        tag = "RECONFIRMED"
        desc = f"{s.n_reconfirmed} reconfirmed on the same release"
    elif s.n_watch:
        tag = "WATCH"
        desc = f"{_plural(s.n_watch, 'signal')} awaiting confirmation"
    elif not s.groups_total:
        tag = "NO DATA"
        desc = "no run groups reported"
    elif s.groups_judged < s.groups_total:
        tag = "INCOMPLETE"
        desc = f"{s.groups_judged}/{s.groups_total} run groups judged"
    else:
        tag = "OK"
        desc = "all judged metrics within baseline"
    return f"[k4Bench][{tag}] {s.night}{desc}"

preheader

preheader(report: NightlyReport) -> str

Short inbox-preview text expanding the subject with coverage and watch — kept compact enough for common preview panes.

Source code in k4bench/regression/email.py
def preheader(report: NightlyReport) -> str:
    """Short inbox-preview text expanding the subject with coverage and watch —
    kept compact enough for common preview panes."""
    s = EmailSummary.of(report)
    return (
        f"{s.n_failures} failures · {s.n_new} new · {s.n_reconfirmed} reconfirmed"
        f" · {s.n_watch} watch · {s.coverage_text}"
    )

to_html

to_html(report: NightlyReport, *, dashboard_url: str | None = None, actions_url: str | None = None, blame: BlameReport | None = None, historical_blame: dict[str, BlameReport] | None = None) -> str

Self-contained HTML email body (inline styles only, no CSS/JS).

Source code in k4bench/regression/email.py
def to_html(
    report: NightlyReport, *,
    dashboard_url: str | None = None,
    actions_url: str | None = None,
    blame: BlameReport | None = None,
    historical_blame: dict[str, BlameReport] | None = None,
) -> str:
    """Self-contained HTML email body (inline styles only, no CSS/JS)."""
    index = _BlameIndex(blame, historical_blame)
    attention = _needs_attention(report)
    if attention:
        attention_html = (
            '<h2 style="font-size:18px;margin:20px 0 2px;">Needs attention</h2>'
            + "".join(
                _html_attention_card(g, report, dashboard_url, actions_url, index)
                for g in attention
            )
        )
    else:
        attention_html = (
            '<h2 style="font-size:18px;margin:20px 0 2px;">Needs attention</h2>'
            f'<p style="color:{_C_MUTED};font-size:14px;margin:4px 0;">'
            f"{_esc(_no_attention_message(report))}</p>"
        )
    parts = [
        _html_preheader(report),
        '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" '
        'style="border-collapse:collapse;width:100%;"><tr><td align="center">',
        '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" '
        'style="border-collapse:collapse;width:100%;max-width:860px;"><tr>'
        f'<td style="{_CONTAINER_STYLE}">',
        _html_header(report, dashboard_url, actions_url),
        _html_summary(report),
        attention_html,
        _html_detail(report, dashboard_url),
        _html_footer(report),
        "</td></tr></table>",
        "</td></tr></table>",
    ]
    return "\n".join(parts)

to_markdown

to_markdown(report: NightlyReport, *, dashboard_url: str | None = None, actions_url: str | None = None, blame: BlameReport | None = None, historical_blame: dict[str, BlameReport] | None = None) -> str

Plain-text/Markdown MIME alternative — the same important content as the HTML body, useful on its own in a text-only client.

Source code in k4bench/regression/email.py
def to_markdown(
    report: NightlyReport, *,
    dashboard_url: str | None = None,
    actions_url: str | None = None,
    blame: BlameReport | None = None,
    historical_blame: dict[str, BlameReport] | None = None,
) -> str:
    """Plain-text/Markdown MIME alternative — the same important content as the
    HTML body, useful on its own in a text-only client."""
    index = _BlameIndex(blame, historical_blame)
    s = EmailSummary.of(report)
    lines = [
        f"# k4Bench nightly report — {_human_date(s.night) if s.night != 'no data' else s.night}",
        "",
    ]
    release_line = _release_line(report)
    if release_line:
        lines.append(f"**{release_line}**")
        lines.append("")
    lines += [
        f"Generated {_human_datetime(report.generated_at)}.",
        "",
        f"**Status:** {s.n_failures} failures · {s.n_new} new · "
        f"{s.n_reconfirmed} reconfirmed · {s.n_watch} watch · {s.coverage_text}.",
        "",
    ]
    actions = []
    if dashboard_url:
        actions.append(f"[Open dashboard]({_dashboard_link(dashboard_url, tab='Overview')})")
    if actions_url:
        actions.append(f"[CI run]({actions_url})")
    if actions:
        lines.append(" · ".join(actions))
        lines.append("")

    lines.append("## Needs attention")
    lines.append("")
    attention = _needs_attention(report)
    if attention:
        for group in attention:
            lines.extend(
                _md_attention_card(group, report, dashboard_url, actions_url, index)
            )
    else:
        lines.append(_no_attention_message(report))
        lines.append("")

    lines.append("## Detailed report — reference")
    lines.append("")
    plan = _detail_plan(report)
    for detector, groups in report.by_detector().items():
        lines.append(f"### {_detector_badge(groups)} {detector}")
        lines.append("")
        for group in groups:
            lines.extend(_md_detail_group(group, plan[id(group)], report, dashboard_url))

    lines.append(_md_footer(report))
    return "\n".join(lines)