mirror of
https://github.com/Imbad0202/academic-research-skills.git
synced 2026-09-14 13:51:17 +08:00
e90a98d321
* ci(eval-harness): render PR comment as verdict + table, fold raw JSON The eval-harness workflow pasted the entire eval_report.json into every PR comment as one raw fenced block. Replace that with a unit-tested display module (scripts/render_eval_comment.py): a one-line verdict (measured passed/total + pending count), a per-task markdown table (metric, value, threshold, result), and the full JSON folded into <details>. Display layer only — run_evals, scripts._eval_threshold_gate, the gate step, and the [eval-regression-acknowledged] ack contract are untouched. The row verdict mirrors the gate's failure signal (aggregate AND per-class, #328) so the table never shows a clean pass on a run the gate blocks. Registered in scripts/_ci_pytest_manifest.toml; workflow test extended to pin the renderer so the comment can't silently regress to a raw dump. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017jcH7gEkVTQ1baMbVedSZp * fix(eval-comment): escape pipes/newlines in table cells (codex P2) task_name / metric / comparison come from eval manifests; a `|` or newline in them would break the markdown table or spoof extra rows/results. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017jcH7gEkVTQ1baMbVedSZp * refactor(eval-comment): /simplify pass — \r line boundaries, fd hygiene, gate-agreement pin - _cell now normalizes ALL line boundaries via splitlines() (codex re-review: \r could still split a table row), not just \n. - main() reads the report via a context manager (ResourceWarning hygiene). - New test pins that _task_failures agrees with _eval_threshold_gate.failed_tasks on which tasks fail, so the deliberate display-side mirror of the gate's failure rule drifts loudly (CI fail) instead of silently rendering green on a blocked run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017jcH7gEkVTQ1baMbVedSZp --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
131 lines
4.3 KiB
Python
131 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Render a ``run_evals`` report as the eval-harness PR comment markdown.
|
|
|
|
Display layer ONLY (#184 follow-up): the workflow used to paste the whole
|
|
``eval_report.json`` into the PR comment as one raw fenced block. This module
|
|
renders the same report as a one-line verdict + a per-task table, with the
|
|
full JSON folded into a ``<details>`` block. The threshold verdict itself is
|
|
computed by ``scripts._eval_threshold_gate`` — this renderer never gates.
|
|
|
|
Kept out of the workflow YAML (no inline heredoc) per the same house rule as
|
|
the gate module; see scripts/test_eval_harness_workflow.py.
|
|
|
|
CLI::
|
|
|
|
python -m scripts.render_eval_comment <report.json>
|
|
|
|
prints the comment markdown to stdout.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from typing import Any
|
|
|
|
_COMPARISON_SYMBOLS = {">=": "≥", "<=": "≤", "==": "="}
|
|
|
|
_PENDING_RESULT = "⏸️ pending"
|
|
|
|
|
|
def _fmt_value(value: Any) -> str:
|
|
if isinstance(value, (int, float)):
|
|
return f"{value:.2f}"
|
|
return str(value)
|
|
|
|
|
|
def _cell(text: Any) -> str:
|
|
"""Markdown-table-safe cell: report strings (task_name, metric, comparison)
|
|
come from eval manifests, so a `|` or any line boundary (\\n, \\r, U+2028…)
|
|
would break the table or spoof extra rows/results."""
|
|
return " ".join(str(text).splitlines()).replace("|", "\\|")
|
|
|
|
|
|
def _task_failures(task: dict[str, Any]) -> tuple[bool, bool]:
|
|
"""(aggregate_failed, per_class_failed) — mirrors the gate's failure signal."""
|
|
agg = task.get("aggregate_metric") or {}
|
|
agg_failed = agg.get("passed") is False
|
|
pc_failed = any(pc.get("passed") is False for pc in task.get("per_class", []))
|
|
return agg_failed, pc_failed
|
|
|
|
|
|
def _table_row(task: dict[str, Any]) -> str:
|
|
name = _cell(task.get("task_name", "?"))
|
|
if task.get("status", "measured") != "measured":
|
|
return f"| {name} | — | — | — | {_PENDING_RESULT} |"
|
|
agg = task.get("aggregate_metric") or {}
|
|
metric = _cell(agg.get("metric", "—"))
|
|
value = _cell(_fmt_value(agg["value"])) if "value" in agg else "—"
|
|
if "threshold_value" in agg:
|
|
comparison = agg.get("comparison", ">=")
|
|
symbol = _COMPARISON_SYMBOLS.get(comparison, comparison)
|
|
threshold = _cell(f"{symbol} {_fmt_value(agg['threshold_value'])}")
|
|
else:
|
|
threshold = "—"
|
|
agg_failed, pc_failed = _task_failures(task)
|
|
if agg_failed:
|
|
result = "❌"
|
|
elif pc_failed:
|
|
# Aggregate met its threshold but a per-class metric did not — the gate
|
|
# still blocks (#328), so the row must not show a clean pass.
|
|
result = "❌ (per-class)"
|
|
else:
|
|
result = "✅"
|
|
return f"| {name} | {metric} | {value} | {threshold} | {result} |"
|
|
|
|
|
|
def render_comment(report: dict[str, Any], raw_json: str) -> str:
|
|
tasks = report.get("per_task", [])
|
|
measured = [t for t in tasks if t.get("status", "measured") == "measured"]
|
|
pending = [t for t in tasks if t.get("status", "measured") != "measured"]
|
|
passed = [t for t in measured if not any(_task_failures(t))]
|
|
|
|
if not measured:
|
|
verdict = "⏸️ no measured tasks"
|
|
else:
|
|
emoji = "✅" if len(passed) == len(measured) else "❌"
|
|
verdict = f"{emoji} {len(passed)}/{len(measured)} measured tasks passed"
|
|
if pending:
|
|
verdict += f" · {len(pending)} pending (not wired)"
|
|
|
|
lines = [
|
|
"## Eval harness results",
|
|
"",
|
|
verdict,
|
|
"",
|
|
"| Task | Metric | Value | Threshold | Result |",
|
|
"| --- | --- | --- | --- | --- |",
|
|
]
|
|
lines.extend(_table_row(t) for t in measured + pending)
|
|
lines.extend(
|
|
[
|
|
"",
|
|
"<details>",
|
|
"<summary>Raw JSON</summary>",
|
|
"",
|
|
"```json",
|
|
raw_json.rstrip("\n"),
|
|
"```",
|
|
"",
|
|
"</details>",
|
|
"",
|
|
]
|
|
)
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = sys.argv[1:] if argv is None else argv
|
|
if len(args) != 1:
|
|
print("usage: python -m scripts.render_eval_comment <report.json>",
|
|
file=sys.stderr)
|
|
return 2
|
|
with open(args[0], encoding="utf-8") as fh:
|
|
raw_json = fh.read()
|
|
report = json.loads(raw_json)
|
|
print(render_comment(report, raw_json))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|