Skip to content

k4bench.regression.render

k4bench.regression.render

Shared rendering primitives for the nightly regression report and the JSON artifact the dashboard reads back (_reports/{date}/report.json).

This module owns two things:

  • the human-label helpers the dashboard and the email both draw with — severity badges, metric/sample/platform prettifiers, the dashboard deep-link builder;
  • the to_json/from_json round-trip for the EOS artifact.

The e-group email body (subject, "Needs attention", ranked candidate cards and the bounded detailed report) is rendered by :mod:k4bench.regression.email, which imports the helpers below. Keeping the JSON renderer and the dashboard's imports here — stable — while the email evolves separately is deliberate: the email is redesigned often, the on-disk report schema almost never.

Ordering vocabulary shared everywhere: hard failures first, then confirmed regressions (any direction — "regression" here means any confirmed step beyond the baseline, not just a bad one; none of this is judged good or bad), then a short WATCH/OK summary — never a row per OK metric.

window_token

window_token(base_release: str | None, onset_release: str | None) -> str

A change window as a compact, URL-safe ?window= value — 2026-06-25..2026-06-27, or ..2026-06-27 when the older end is open.

Shared by the email (which emits the link) and the dashboard's Regressions tab (which reads it back), so a deep link always selects the window whose metrics and candidate PRs the mail was talking about. .. rather than the displayed keeps the value readable in a URL bar instead of percent-encoded.

Source code in k4bench/regression/render.py
def window_token(base_release: str | None, onset_release: str | None) -> str:
    """A change window as a compact, URL-safe ``?window=`` value —
    ``2026-06-25..2026-06-27``, or ``..2026-06-27`` when the older end is open.

    Shared by the email (which emits the link) and the dashboard's Regressions
    tab (which reads it back), so a deep link always selects the window whose
    metrics and candidate PRs the mail was talking about. ``..`` rather than the
    displayed ``→`` keeps the value readable in a URL bar instead of
    percent-encoded.
    """
    return f"{base_release or ''}..{onset_release or ''}"

window_href

window_href(dashboard_url: str | None, *, detector: str, platform: str, sample: str, base_release: str | None, onset_release: str | None, stack: str | None = None, report_night: str = '') -> str | None

The Regressions view scoped to one change window — the link that lands the reader on exactly the metrics and candidate PRs that window is about, rather than on whichever window the tab would open by default.

Shared by every renderer that points at a window (the nightly email, the pull-request comments in :mod:k4bench.blame.comment), so one link shape is defined once beside the ?window= vocabulary it uses.

Source code in k4bench/regression/render.py
def window_href(
    dashboard_url: str | None,
    *,
    detector: str,
    platform: str,
    sample: str,
    base_release: str | None,
    onset_release: str | None,
    stack: str | None = None,
    report_night: str = "",
) -> str | None:
    """The Regressions view scoped to one change window — the link that lands
    the reader on exactly the metrics and candidate PRs that window is about,
    rather than on whichever window the tab would open by default.

    Shared by every renderer that points at a window (the nightly email, the
    pull-request comments in :mod:`k4bench.blame.comment`), so one link shape is
    defined once beside the ``?window=`` vocabulary it uses.
    """
    if not dashboard_url or not onset_release:
        return None
    params = dict(
        detector=detector, platform=platform, sample=sample,
        window=window_token(base_release, onset_release),
    )
    if stack:
        params["stack"] = stack
    if report_night:
        params["report"] = report_night
    return _dashboard_link(dashboard_url, tab="Regressions", **params)

stack_changes_href

stack_changes_href(dashboard_url: str | None, *, detector: str, platform: str, sample: str, base_release: str | None, onset_release: str | None) -> str | None

The Stack Changes view for one exact release window — the package diff behind a change window, where a reader goes to see what actually moved.

The param names to/from must match what the dashboard's Stack Changes tab reads back (PARAM_TO/PARAM_FROM in dashboard/tabs/stack_changes.py) — a literal mismatch here silently breaks the deep link instead of raising.

Source code in k4bench/regression/render.py
def stack_changes_href(
    dashboard_url: str | None,
    *,
    detector: str,
    platform: str,
    sample: str,
    base_release: str | None,
    onset_release: str | None,
) -> str | None:
    """The Stack Changes view for one exact release window — the package diff
    behind a change window, where a reader goes to see what actually moved.

    The param names ``to``/``from`` must match what the dashboard's Stack
    Changes tab reads back (``PARAM_TO``/``PARAM_FROM`` in
    dashboard/tabs/stack_changes.py) — a literal mismatch here silently
    breaks the deep link instead of raising.
    """
    if not dashboard_url or not onset_release:
        return None
    params = {
        "tab": "Stack Changes",
        "detector": detector,
        "platform": platform,
        "sample": sample,
        "to": onset_release,
    }
    if base_release:
        params["from"] = base_release
    return _dashboard_link(dashboard_url, **params)

regression_href

regression_href(dashboard_url: str | None, *, verdict: MetricVerdict, base_release: str | None, onset_release: str | None) -> str | None

The Stack Changes view for one exact regression — the package diff for its window with that metric already selected below it, which renders its trend, its onset and the ranked candidates in the same view.

This is the one destination that answers "did my change do this?" without a second click, so it is what the pull-request comments point every row at. Where :func:stack_changes_href opens the range and leaves the reader to find their metric, this pins it: the reg_* params name a verdict exactly (see _query_verdict in dashboard/tabs/stack_changes.py), and a verdict the tab cannot match falls back to the same unpinned range.

Like :func:stack_changes_href, the param names are literals the dashboard reads back (PARAM_REG_*); a mismatch here loses the selection silently. Returns None when the verdict carries no onset identity — the tab needs one to tell two onsets of the same release apart, and a link that would quietly select the wrong step is worse than the unpinned one.

Source code in k4bench/regression/render.py
def regression_href(
    dashboard_url: str | None,
    *,
    verdict: MetricVerdict,
    base_release: str | None,
    onset_release: str | None,
) -> str | None:
    """The Stack Changes view for one exact regression — the package diff for
    its window with *that* metric already selected below it, which renders its
    trend, its onset and the ranked candidates in the same view.

    This is the one destination that answers "did my change do this?" without a
    second click, so it is what the pull-request comments point every row at.
    Where :func:`stack_changes_href` opens the range and leaves the reader to
    find their metric, this pins it: the ``reg_*`` params name a verdict
    exactly (see ``_query_verdict`` in dashboard/tabs/stack_changes.py), and a
    verdict the tab cannot match falls back to the same unpinned range.

    Like :func:`stack_changes_href`, the param names are literals the dashboard
    reads back (``PARAM_REG_*``); a mismatch here loses the selection silently.
    Returns ``None`` when the verdict carries no onset identity — the tab needs
    one to tell two onsets of the same release apart, and a link that would
    quietly select the wrong step is worse than the unpinned one.
    """
    base = stack_changes_href(
        dashboard_url,
        detector=verdict.detector, platform=verdict.platform,
        sample=verdict.sample,
        base_release=base_release, onset_release=onset_release,
    )
    onset = verdict.onset_run_id or verdict.onset_run_date
    if not base or not onset:
        return None
    params = {
        "reg_detector": verdict.detector,
        "reg_sample": verdict.sample,
        "reg_config": verdict.label,
        "reg_metric": verdict.metric,
        "reg_onset": onset,
    }
    if verdict.sub_detector:
        params["reg_region"] = verdict.sub_detector
    return _dashboard_link(base, **params)

to_json

to_json(report: NightlyReport) -> dict

Plain-dict form of the report for json.dump.

The summary block duplicates the convenience properties so the CI workflow (and the dashboard banner) can gate on the report without re-deriving anything.

Source code in k4bench/regression/render.py
def to_json(report: NightlyReport) -> dict:
    """Plain-dict form of the report for ``json.dump``.

    The ``summary`` block duplicates the convenience properties so the CI
    workflow (and the dashboard banner) can gate on the report without
    re-deriving anything.
    """
    data = _sanitize(dataclasses.asdict(report))
    data["summary"] = {
        "report_night":     report.report_night,
        "n_detectors":      len(report.by_detector()),
        "n_regressions":    len(report.regressions),
        "n_new":            len(report.new_regressions),
        "n_reconfirmed":    len(report.reconfirmed_regressions),
        "n_watches":        len(report.watches),
        "n_failures":       len(report.failures) + len(report.job_failures),
        "has_alertable":    report.has_alertable,
    }
    return data

from_json

from_json(data: dict) -> NightlyReport

Rebuild a :class:NightlyReport from :func:to_json output (used by the dashboard when reading _reports/{date}/report.json off EOS).

Source code in k4bench/regression/render.py
def from_json(data: dict) -> NightlyReport:
    """Rebuild a :class:`NightlyReport` from :func:`to_json` output (used by
    the dashboard when reading ``_reports/{date}/report.json`` off EOS)."""
    groups = []
    for g in data.get("groups", []):
        verdicts = [
            MetricVerdict(**{
                **{k: val for k, val in v.items() if k in _VERDICT_FIELDS},
                "severity": Severity(v["severity"]),
                "direction": Direction(v["direction"]),
                "history": _history(v.get("history")),
                "region_deltas": _region_deltas(v.get("region_deltas")),
            })
            for v in g.get("verdicts", [])
        ]
        groups.append(RunGroupReport(
            detector=g["detector"], platform=g["platform"], sample=g["sample"],
            k4h_release=g.get("k4h_release", ""), run_date=g.get("run_date", ""),
            run_id=g.get("run_id", ""), verdicts=verdicts,
            job_failures=list(g.get("job_failures", [])),
            notes=list(g.get("notes", [])),
            reliable=g.get("reliable"),
            github_run_url=g.get("github_run_url"),
            geometry_path=str(g.get("geometry_path") or ""),
        ))
    return NightlyReport(generated_at=data.get("generated_at", ""), groups=groups)

to_html

to_html(report: NightlyReport, **kwargs) -> str

Render the HTML email via :mod:k4bench.regression.email.

New code should import from k4bench.regression.email directly; this wrapper keeps existing integrations working after the renderer split.

Source code in k4bench/regression/render.py
def to_html(report: NightlyReport, **kwargs) -> str:
    """Render the HTML email via :mod:`k4bench.regression.email`.

    New code should import from ``k4bench.regression.email`` directly; this
    wrapper keeps existing integrations working after the renderer split.
    """
    from k4bench.regression.email import to_html as render_email_html

    return render_email_html(report, **kwargs)

to_markdown

to_markdown(report: NightlyReport, **kwargs) -> str

Render the text alternative via :mod:k4bench.regression.email.

See :func:to_html for why this compatibility wrapper is lazy.

Source code in k4bench/regression/render.py
def to_markdown(report: NightlyReport, **kwargs) -> str:
    """Render the text alternative via :mod:`k4bench.regression.email`.

    See :func:`to_html` for why this compatibility wrapper is lazy.
    """
    from k4bench.regression.email import to_markdown as render_email_markdown

    return render_email_markdown(report, **kwargs)