Skip to content

Commit 7553be6

Browse files
committed
refactor(eval): move trend_log + dashboard into eval/dashboard/
Groups the trend-log writer/reader and the dashboard renderer (plus their data/output files) under one package, a sibling of eval/reports/, instead of loose at the eval/ top level. Fixes dashboard.py's reports_dir default (was HERE / "reports", silently wrong once dashboard.py no longer sits beside eval/reports/) and updates every import, path, doc reference, and .gitignore entry accordingly. Also commits the real trend_log.jsonl row from the live agent_delegation/unreachable_resources run (eval/reports/report_ 06_10_11_21_16.md). Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Amitfre15 <amitfrework@gmail.com>
1 parent 8837884 commit 7553be6

16 files changed

Lines changed: 65 additions & 60 deletions

‎.gitignore‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ test/integration/rego_out/
1818
eval/rego_out/
1919
eval/reports/
2020

21-
# eval results dashboard (eval/dashboard.py, issue #2542) — regenerated per invocation from
22-
# eval/trend_log.jsonl (committed) + eval/reports/ (gitignored, above), not source of truth itself.
23-
eval/dashboard.html
21+
# eval results dashboard (eval/dashboard/dashboard.py, issue #2542) — regenerated per invocation
22+
# from eval/dashboard/trend_log.jsonl (committed) + eval/reports/ (gitignored, above), not source
23+
# of truth itself.
24+
eval/dashboard/dashboard.html
2425

2526
# uc1-onboarding demo's captured .rego snapshots — regenerated per run, not source of truth.
2627
demo/use-cases/uc1-onboarding/generated/

‎docs/evaluation/README.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ run.
105105
Every run of the six suites above writes a Markdown report to the gitignored `eval/reports/`
106106
(`report_<DD_MM_HH_MM_SS>.md`) — see `eval/conftest.py`. The two Correctness suites, the
107107
Robustness suite's mechanical tier, and the Scale suite (one row per dimension/level) additionally
108-
append one row each to the **committed** `eval/trend_log.jsonl` (spec:
108+
append one row each to the **committed** `eval/dashboard/trend_log.jsonl` (spec:
109109
[eval-framework.md §9](eval-framework.md#9-reporting-and-trend-persistence)) — see
110110
[policy-eval-correctness-prb.md § Trend log](policy-eval-correctness-prb.md#trend-log),
111111
[policy-eval-robustness-consistency.md § Trend log](policy-eval-robustness-consistency.md#trend-log),
@@ -114,16 +114,16 @@ log](policy-eval-scale.md#test-report-and-trend-log).
114114

115115
## Dashboard
116116

117-
`eval/dashboard.py` renders a static, self-contained HTML view (inline SVG, no external
117+
`eval/dashboard/dashboard.py` renders a static, self-contained HTML view (inline SVG, no external
118118
dependency, no CI wiring) from the two artifacts above — no new data source. Regenerate it after a
119119
run:
120120

121121
```bash
122-
.venv/bin/python -m eval.dashboard
122+
.venv/bin/python -m eval.dashboard.dashboard
123123
```
124124

125-
Writes `eval/dashboard.html` (gitignored, regenerated each time) with one historical trend chart
126-
per suite present in `eval/trend_log.jsonl`, plus a collapsible scenario drill-down section per
125+
Writes `eval/dashboard/dashboard.html` (gitignored, regenerated each time) with one historical
126+
trend chart per suite present in `eval/dashboard/trend_log.jsonl`, plus a collapsible scenario drill-down section per
127127
report found under `eval/reports/`. A chart point links to its matching report's section when one
128128
is found on disk within a few hours of that trend-log row's timestamp; otherwise it still shows its
129129
exact values via a hover tooltip, just without a link.

‎docs/evaluation/policy-eval-correctness-e2e.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,9 @@ render behavior itself.
197197

198198
Same mechanism as
199199
[policy-eval-correctness-prb.md § Trend log](policy-eval-correctness-prb.md#trend-log) (#2091,
200-
`eval/trend_log.py`) — `test_e2e_correctness` `record_property`s the same `true_positives`/
200+
`eval/dashboard/trend_log.py`) — `test_e2e_correctness` `record_property`s the same `true_positives`/
201201
`denied_total` counts, and `eval/conftest.py` pools them into a `suite="correctness_e2e"` row in
202-
the same committed `eval/trend_log.jsonl`, distinct from the PRB-level suite's
202+
the same committed `eval/dashboard/trend_log.jsonl`, distinct from the PRB-level suite's
203203
`"correctness_prb"` row.
204204

205205
## Relationship to other integration tests

‎docs/evaluation/policy-eval-correctness-prb.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,12 +284,12 @@ This is **one** integration-test spec among several indexed by the master PRD
284284

285285
## Trend log
286286

287-
`eval/trend_log.py`'s shared `append_row`/`pool_correctness_metrics` (#2091) pools this suite's
287+
`eval/dashboard/trend_log.py`'s shared `append_row`/`pool_correctness_metrics` (#2091) pools this suite's
288288
`true_positives`/`denied_total` counts (and `over_grants`/`under_grants`/`incorrectly_denied` pair
289289
dicts) — `record_property`'d by `test_prb_correctness` alongside the existing
290290
precision/recall/denial-precision floats — across every scenario that reached `score_scenario` in
291291
a run, and appends one row (`suite="correctness_prb"`) to the committed, append-only
292-
`eval/trend_log.jsonl` from `eval/conftest.py`'s `pytest_sessionfinish`. Pooled by summed count,
292+
`eval/dashboard/trend_log.jsonl` from `eval/conftest.py`'s `pytest_sessionfinish`. Pooled by summed count,
293293
not averaged per-scenario — same union-not-average philosophy as `score_scenario`'s own
294294
cross-gate aggregation. See `docs/evaluation/eval-framework.md` §9.
295295

‎docs/evaluation/policy-eval-robustness-consistency.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,14 +390,14 @@ is also read back by `_write_trend_log` (see [Trend log](#trend-log)).
390390
`record_property("invariant"/"sensitive", bool)` values, via nodeid-substring matching
391391
(`_ROBUSTNESS_TEST_MARKERS`, since the single flat `eval` marker — shared by every suite under
392392
`eval/` — spans four test functions across two families × two tiers that must stay unblended per
393-
spec §4), and appends **four** rows to the committed, append-only `eval/trend_log.jsonl` from
393+
spec §4), and appends **four** rows to the committed, append-only `eval/dashboard/trend_log.jsonl` from
394394
`pytest_sessionfinish` — one per family/tier combination, never combined: `suite=
395395
"robustness_mechanical_invariance"`, `"robustness_semantic_invariance"` (#2467),
396396
`"robustness_mechanical_sensitivity"`, and `"robustness_semantic_sensitivity"` (#2467). Each row
397397
carries that row's own pass/fail rate (`invariance_rate`/`sensitivity_rate`, mean of that row's
398398
booleans) *and* that row's own `precision`/`recall`/`denial_precision`, pooled from the same
399399
`true_positives`/`denied_total`/`over_grants`/`under_grants`/`incorrectly_denied` shape
400-
`_record_scoring` records (`eval/trend_log.py`'s `pool_correctness_metrics` — the same pooling
400+
`_record_scoring` records (`eval/dashboard/trend_log.py`'s `pool_correctness_metrics` — the same pooling
401401
function the two Correctness suites use, reused as-is here) — so each row's chart is directly
402402
comparable in shape to a Correctness chart: two measuring the PRB against *original* inputs
403403
(invariance, mechanical vs. semantic tier), two against *deliberately edited/reworded* inputs
@@ -406,7 +406,7 @@ filter that only exercises one test function) simply gets no row at all, rather
406406
mislabeling one row's count against another's. See `docs/evaluation/eval-framework.md` §9.
407407

408408
**Consistency (#2468)** gets its own single row, `suite="consistency"`, pooled by
409-
`eval/trend_log.py`'s `pool_consistency_metrics` — not `pool_correctness_metrics`, since there's no
409+
`eval/dashboard/trend_log.py`'s `pool_consistency_metrics` — not `pool_correctness_metrics`, since there's no
410410
truth table here (Consistency compares PRB runs to each other, not to ground truth), just each
411411
scenario's own `inconsistent` boolean rolled up into an `agreement_rate` (the fraction of the
412412
corpus's scenarios that agreed across all of their `PRB_CONSISTENCY_REPEATS` repeats). Framed as

‎docs/evaluation/policy-eval-scale.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ Reuses `eval.correctness_scorer.score_scenario`/`score_gate` (the same scorer
172172
"gates" the way a real scenario's three gates are — each candidate becomes a `(role_name,
173173
scope_name)` pair against the fixed focal entity (subject-side pairs for the scope direction,
174174
resource-side pairs for the role direction), so `score_scenario`'s existing per-gate aggregation,
175-
`eval/conftest.py`'s report rendering, and `eval/trend_log.py`'s pooling all apply with **zero**
175+
`eval/conftest.py`'s report rendering, and `eval/dashboard/trend_log.py`'s pooling all apply with **zero**
176176
special-casing.
177177

178178
Same zero-tolerance-on-over-grants gate every sibling suite uses; under-grants and incorrect
@@ -187,7 +187,7 @@ dimension/level are checked by separate test functions but land on one row
187187
(`scale_total_corpus_prb`/`scale_total_corpus_e2e`/`scale_per_decision_prb`/`scale_per_decision_e2e`
188188
— total-corpus and per-decision, and PRB and e2e, always kept on **separate** rows, per spec §5's
189189
explicit warning that a system can pass one dimension/level while silently failing the other).
190-
`eval.trend_log.pool_scale_metrics` pools the structural half (`structural_pass`/
190+
`eval.dashboard.trend_log.pool_scale_metrics` pools the structural half (`structural_pass`/
191191
`structural_issue_count`/`wall_clock_seconds`/`total_tokens`/`token_coverage`, common fields both
192192
dimensions record regardless of their own concrete-check taxonomy) into `structural_pass_rate`/
193193
`structural_issue_count`/`total_tokens`/`mean_wall_clock_seconds`/`mean_token_coverage`; the
@@ -261,6 +261,6 @@ placement (Nightly, not per-PR).
261261

262262
## Blocked-by
263263

264-
\#2091 (the committed trend log — already shipped; `eval.trend_log.append_row`/
264+
\#2091 (the committed trend log — already shipped; `eval.dashboard.trend_log.append_row`/
265265
`pool_correctness_metrics` reused here unmodified, `pool_scale_metrics` added alongside them). Same
266266
PRB/e2e prerequisites as every other suite in this family — no new production dependency introduced.

‎eval/conftest.py‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
``true_positives`` and ``denied_total`` — not rendered in the Markdown report (the floats and pair
6464
breakdowns already cover a human reader) but read back here by ``_write_trend_log`` to pool a
6565
run's precision/recall/denial_precision into one committed, append-only trend-log row per suite
66-
(``eval/trend_log.py``, spec: ``docs/evaluation/eval-framework.md`` §9) — pooled by summed count
66+
(``eval/dashboard/trend_log.py``, spec: ``docs/evaluation/eval-framework.md`` §9) — pooled by summed count
6767
across every scenario that reached ``score_scenario`` in the run, not averaged per-scenario, so a
6868
run with an uneven pair count per scenario isn't skewed by weighting every scenario equally. A
6969
scenario whose own setup failed never recorded these, so it contributes nothing to the pooled row
@@ -80,7 +80,7 @@
8080
committed trend-log row per family/tier combination -- ``suite="robustness_mechanical_invariance"``/
8181
``"robustness_semantic_invariance"``/``"robustness_mechanical_sensitivity"``/
8282
``"robustness_semantic_sensitivity"`` -- each carrying that row's own precision/recall/
83-
denial_precision (``eval/trend_log.py``'s ``pool_correctness_metrics``, the same pooling the two
83+
denial_precision (``eval/dashboard/trend_log.py``'s ``pool_correctness_metrics``, the same pooling the two
8484
Correctness suites use, so the resulting chart is directly comparable to theirs: two measuring
8585
performance against the *original* inputs, two against *deliberately edited/reworded* inputs) plus
8686
that row's own pass/fail rate (``invariance_rate``/``sensitivity_rate``).
@@ -91,7 +91,7 @@
9191
mismatch detail when ``True``) via ``_render_consistency_block``, on every entry, pass or fail,
9292
unlike every other test in this suite besides the correctness/robustness metrics blocks above -- and
9393
pooled by nodeid substring (``_CONSISTENCY_TEST_MARKERS``) into its own committed trend-log row,
94-
``suite="consistency"``, carrying an ``agreement_rate`` (``eval/trend_log.py``'s
94+
``suite="consistency"``, carrying an ``agreement_rate`` (``eval/dashboard/trend_log.py``'s
9595
``pool_consistency_metrics`` -- its own smaller pooling function, since there's no truth table here
9696
to produce a ``pool_correctness_metrics``-shaped precision/recall).
9797
@@ -107,7 +107,7 @@
107107
pooled by nodeid substring (``_SCALE_TEST_MARKERS``, mapping to ``(suite, "structural" |
108108
"correctness")`` rather than a bare suite name) and merged into one row per dimension/level --
109109
``scale_total_corpus_prb``/``scale_per_decision_prb`` (and their ``_e2e`` counterparts) -- via
110-
``eval/trend_log.py``'s ``pool_scale_metrics`` for the structural half and the existing
110+
``eval/dashboard/trend_log.py``'s ``pool_scale_metrics`` for the structural half and the existing
111111
``pool_correctness_metrics`` for the correctness half.
112112
113113
Below the per-category drill-down, ``pytest_sessionfinish`` appends an **"Improvement
@@ -139,8 +139,8 @@
139139
import pytest
140140
from dotenv import load_dotenv
141141

142+
from eval.dashboard.trend_log import append_row, pool_consistency_metrics, pool_correctness_metrics, pool_scale_metrics
142143
from eval.recommendations import build_recommendations
143-
from eval.trend_log import append_row, pool_consistency_metrics, pool_correctness_metrics, pool_scale_metrics
144144

145145
HERE = Path(__file__).resolve().parent
146146
REPORTS_DIR = HERE / "reports"
@@ -292,7 +292,7 @@ def _format_pairs_dict(pairs_by_gate: dict) -> str:
292292
"::test_prb_sensitive_to_semantic_perturbation[",
293293
)
294294

295-
# Nodeid substring -> trend-log suite name (eval/trend_log.py). With the five former ``eval_*``
295+
# Nodeid substring -> trend-log suite name (eval/dashboard/trend_log.py). With the five former ``eval_*``
296296
# markers collapsed to one flat ``eval`` marker, every suite under `eval/` is disambiguated by its
297297
# own test function's nodeid rather than by marker — the same ``::test_prb_correctness[``/
298298
# ``::test_e2e_correctness[`` substrings ``_CORRECTNESS_TEST_MARKERS`` already uses. Deliberately
@@ -347,7 +347,7 @@ def _format_pairs_dict(pairs_by_gate: dict) -> str:
347347
# invariance_rate/sensitivity_rate, which _write_trend_log injects on top of pool_correctness_
348348
# metrics' output separately). Kept as its own dict (not folded into _TREND_LOG_SUITES) since its
349349
# entries are pooled by pool_consistency_metrics, not pool_correctness_metrics -- see
350-
# eval/trend_log.py.
350+
# eval/dashboard/trend_log.py.
351351
_CONSISTENCY_TEST_MARKERS = {
352352
"::test_prb_consistent_across_repeats[": "consistency",
353353
}
@@ -356,7 +356,7 @@ def _format_pairs_dict(pairs_by_gate: dict) -> str:
356356
# docs/evaluation/policy-eval-scale.md). Unlike every dict above, each Scale trend-log row
357357
# (e.g. "scale_total_corpus_prb") is built from *two* separate test functions -- a structural test
358358
# (record_property("structural_pass"/"structural_issue_count"/...), pooled by
359-
# eval.trend_log.pool_scale_metrics) and a correctness test (the familiar precision/recall shape,
359+
# eval.dashboard.trend_log.pool_scale_metrics) and a correctness test (the familiar precision/recall shape,
360360
# pooled by the existing pool_correctness_metrics) -- so this dict's value is a
361361
# (suite, "structural" | "correctness") pair rather than a bare suite name; _write_trend_log pools
362362
# each half separately then merges them into one row. Neither test function is parametrized (each

‎eval/dashboard/__init__.py‎

Whitespace-only changes.
Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
"""Static eval results dashboard — scenario drill-down + historical trend charts (issue #2542).
22
3-
Renders entirely from the artifacts ``eval/trend_log.py``/``eval/conftest.py`` (#2091) already
4-
produce — the committed, append-only ``eval/trend_log.jsonl`` and the gitignored per-run
5-
``eval/reports/report_<timestamp>.md`` — no new data source, no CI wiring, no server. A single
6-
self-contained HTML file (inline SVG, inline CSS, no external requests) a developer regenerates
7-
locally after an eval run.
3+
Renders entirely from the artifacts ``eval/dashboard/trend_log.py``/``eval/conftest.py`` (#2091)
4+
already produce — the committed, append-only ``eval/dashboard/trend_log.jsonl`` and the
5+
gitignored per-run ``eval/reports/report_<timestamp>.md`` — no new data source, no CI wiring, no
6+
server. A single self-contained HTML file (inline SVG, inline CSS, no external requests) a
7+
developer regenerates locally after an eval run.
88
99
Two artifacts, two read paths: ``load_trend_log`` reads the committed trend log; ``parse_report``/
1010
``parse_reports`` re-derive structured scenario data from the Markdown report(s) ``eval/conftest.py``
@@ -24,9 +24,10 @@
2424
from pathlib import Path
2525
from typing import Any
2626

27-
from eval.trend_log import DEFAULT_PATH as TREND_LOG_DEFAULT_PATH
27+
from eval.dashboard.trend_log import DEFAULT_PATH as TREND_LOG_DEFAULT_PATH
2828

29-
HERE = Path(__file__).resolve().parent
29+
HERE = Path(__file__).resolve().parent # eval/dashboard/
30+
EVAL_DIR = HERE.parent # eval/ -- eval/reports/ lives here, a sibling of this package, not under it
3031

3132

3233
def load_trend_log(path: Path = TREND_LOG_DEFAULT_PATH) -> list[dict[str, Any]]:
@@ -301,7 +302,7 @@ def _report_anchor(report: ParsedReport) -> str:
301302
return "run-" + report.run_at.strftime("%Y%m%dT%H%M%SZ")
302303

303304

304-
# Bookkeeping keys every trend-log row carries (eval/trend_log.py's `append_row`, plus
305+
# Bookkeeping keys every trend-log row carries (eval/dashboard/trend_log.py's `append_row`, plus
305306
# `scenarios_scored` that every pooling function adds) that are never themselves a plottable
306307
# metric -- everything else on a row *used* to be one, whatever the suite, back when every metric
307308
# any pooling function produced was naturally 0-1-bounded (a rate or a score). This chart hardcodes
@@ -543,7 +544,7 @@ def render_dashboard(trend_rows: list[dict[str, Any]], reports: list[ParsedRepor
543544
)
544545
trends_body = f'<div class="trends-grid">{chart_sections}</div>'
545546
else:
546-
trends_body = "<p>No trend-log rows yet — run an eval suite to populate eval/trend_log.jsonl.</p>"
547+
trends_body = "<p>No trend-log rows yet — run an eval suite to populate eval/dashboard/trend_log.jsonl.</p>"
547548

548549
tables = "".join(render_scenario_table(report) for report in reports)
549550
drilldown_sections = (
@@ -565,7 +566,7 @@ def render_dashboard(trend_rows: list[dict[str, Any]], reports: list[ParsedRepor
565566

566567
def build_dashboard(
567568
trend_log_path: Path = TREND_LOG_DEFAULT_PATH,
568-
reports_dir: Path = HERE / "reports",
569+
reports_dir: Path = EVAL_DIR / "reports",
569570
output_path: Path = HERE / "dashboard.html",
570571
) -> Path:
571572
"""Load the trend log + every per-run report, render the dashboard, write it to
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@
3131
{"timestamp": "2026-09-28T14:23:10+00:00", "suite": "scale_per_decision_prb", "run_type": "regression", "model": "Azure/gpt-5-mini-2025-08-07", "structural_pass_rate": 1.0, "structural_issue_count": 0, "total_tokens": 14435, "mean_wall_clock_seconds": 31.791, "mean_token_coverage": 1.0, "scenarios_scored": 1, "precision": 1.0, "recall": 1.0, "denial_precision": 1.0}
3232
{"timestamp": "2026-09-28T14:23:10+00:00", "suite": "scale_total_corpus_e2e", "run_type": "regression", "model": "Azure/gpt-5-mini-2025-08-07", "structural_pass_rate": 1.0, "structural_issue_count": 0, "total_tokens": 9231887, "mean_wall_clock_seconds": 118.671, "mean_token_coverage": 1.0, "scenarios_scored": 1, "precision": 1.0, "recall": 0.991, "denial_precision": 1.0}
3333
{"timestamp": "2026-09-28T14:23:10+00:00", "suite": "scale_total_corpus_prb", "run_type": "regression", "model": "Azure/gpt-5-mini-2025-08-07", "structural_pass_rate": 1.0, "structural_issue_count": 0, "total_tokens": 9109596, "mean_wall_clock_seconds": 291.003, "mean_token_coverage": 1.0, "scenarios_scored": 1, "precision": 1.0, "recall": 0.99, "denial_precision": 1.0}
34+
{"timestamp": "2026-10-06T11:21:16+00:00", "suite": "correctness_e2e", "run_type": "regression", "model": "Azure/gpt-5-mini-2025-08-07", "scenarios_scored": 8, "precision": 0.969, "recall": 1.0, "denial_precision": 0.75}

0 commit comments

Comments
 (0)