Skip to content

feat: show the per-finding "Why the gate failed" explanation in the diff index.html - #96

Merged
noteflowai merged 1 commit into
mainfrom
automation/feature-2026-11-02-evalarc-134438647132
Nov 2, 2026
Merged

noteflowai merged 1 commit into
mainfrom
automation/feature-2026-11-02-evalarc-134438647132

Conversation

@noteflowai

Copy link
Copy Markdown
Owner

EA-03 (Now) requires reviewers to see the smallest reproducible example, source provenance, threshold and uncertainty for each blocking finding. evalarc diff already writes this into summary.md, but index.html does not show it.

Read status of the planning inputs:

  • src/evalarc/results_diff.py: read in part. Lines 297-430 cover render_html. Lines 1336-1449 cover _cell, _clip, _attempt_text, _first_attempt, the whole of _failing_attempt, _provenance_line and the whole of _blocking_markdown.
  • src/evalarc/report.py: read in part. Lines 100-135 cover TONES, _tone, _verdict and _table. Lines 298-345 cover _esc, _card and _page.
  • docs/ci-gate.md: read in part, lines 16-196.
  • tests/test_results_diff.py: searched only. It has no match for 'Why the gate failed'.
  • docs/ci-gate.zh-CN.md: not read.
  • render_markdown, _value, _fraction, STYLE, CSP and report_enhance.js: not read.

Measured gap: render_html writes a Changed checks table. Its 'Current evidence' cell joins every evidence string from current_detail, or from baseline_detail when current_detail is absent, and cuts the result to 400 characters. The page shows no numeric threshold. It does not pick out the first failing attempt, and it does not label removed-row evidence as coming from the baseline. docs/ci-gate.md says the summary.md section 'never changes ... index.html'. A reviewer who opens the 'Report:' file that the CLI prints therefore cannot see which attempt failed or which threshold applied.

The issue list is empty, and none of the research items bears on this. The choice rests on the code gap and the EA-03 milestone.

Entry point (unchanged): evalarc diff BASELINE CURRENT --output DIR writes diff.json, summary.md, index.html and copies of both inputs.

New behavior: when result['changes'] contains at least one dict row whose kind is in BLOCKING, index.html gets an h2 'Why the gate failed'. The section goes directly after the Changed checks table and before the generalization, usage, suites, case-attributes and Inputs sections.

Shared data keeps the two renderers from drifting. A new private helper _blocking_rows(result) returns the rows that _blocking_markdown selects today, with the same filter and order. For each row it returns structured, unescaped parts:

  • finding: the kind with underscores replaced by spaces, plus ' (within noise)' when within_sampling_noise is True.
  • case_id and check.
  • baseline and current, passed through unchanged for _fraction.
  • attempt: a record {prefix, value, evidence, text}.

_clip is applied once to the raw value and once to the raw evidence. The attempt record follows today's rules:

  • regressed, less_reliable and unassessed rows: prefix None, using the first current attempt that did not pass.
  • removed rows: prefix 'not recorded in the current run; baseline: ', using the first baseline attempt.
  • less_covered rows: text 'assessed N of M baseline attempts'.
  • No usable value or evidence: text 'no attempt detail recorded'. For a removed row, the prefix is kept before this text.

A second helper returns the threshold display string (_value, or '—', exactly as now). A third returns the provenance parts, (label, name, digest[:12]) for each input, or None under the same conditions as _provenance_line.

Markdown: _attempt_text, _failing_attempt, _provenance_line and _blocking_markdown are rebuilt from these parts. Each value, evidence and name goes through _cell. The parts are joined with the same literal 'value ', 'evidence ', ' · ' and prefix strings, outside the code spans, as today. summary.md stays byte-identical, and the row limit argument and overflow line stay.

HTML: every part goes through report._esc. The threshold sentence is a p with the same wording as in summary.md (html.escape turns the apostrophe into '). The table is built with report._table('Why the gate failed', headers, rows), which render_html adds to its import from evalarc.report. In the source read, _table emits a div.scroll with tabindex="0", role="region" and aria-label="Why the gate failed", plus th scope="col". The _page regex only matches a div whose class attribute is followed directly by '>', and th replacement only touches a bare '', so _page does not duplicate these attributes.

The headers are Finding, Case / check, Baseline, Current and Failing attempt. Each cell is built as follows:

  • Finding: _tone(finding, 'bad'), which renders ✗ followed by the text, so the finding is never shown by color alone.
  • Case / check: case / check.
  • Baseline and Current: the escaped _fraction.
  • Failing attempt: [prefix] value V · evidence E, or the escaped text.

The HTML lists every blocking row with no row limit, as the Changed checks table already does. When the provenance parts exist, a p.metadata reads 'Inputs: baseline name sha256 prefix, current …'. The full Inputs section stays.

Absent state: if no row blocks, the section is not written and index.html is otherwise byte-identical. This covers a passing gate, an incomplete run with no changes, regression-suite-only failures and f2p-only failures.

Errors: input problems still exit 2 before any report is written. Inside the new helpers, a missing or malformed current_detail or baseline_detail produces the fallback text and never raises. The existing Changed checks loop is unchanged and still expects rows with case_id, check, baseline and current, which compare always produces.

No new CSS, JS or dependency is added. The verdict, cards, Changed checks and Inputs sections are unchanged.

Acceptance:

  • Run evalarc diff examples/results-diff/inspect/baseline.json examples/results-diff/inspect/current.json --output DIR. The command exits 1. In DIR/index.html, the 'Why the gate failed' h2 comes after the Changed checks table and before the Inputs h2. The section contains 'Numeric scores pass at threshold 1' and rows containing 'refund-duplicate / includes' and 'refund-duplicate / match', each with a regressed finding. One row contains 'cancel-pending / match', '✗ less reliable (within noise)' and 'value I · evidence cancel:1006:queued'.
  • For the same run, DIR/summary.md contains exactly the documented row '| less reliable (within noise) | cancel-pending / match | 2/2 | 1/2 | value I · evidence cancel:1006:queued |' and the unchanged threshold sentence. diff.json has gate_passed false and blocking_changes 3. The existing pytest suite passes unmodified.
  • Comparing current.json with itself exits 0, and its index.html contains no 'Why the gate failed' text.
  • results_diff.render_html is called on the example's loaded diff.json with synthetic rows appended. Each synthetic row copies baseline and current from an existing row, has case_id and check, and uses only dict detail entries. One is a regressed row whose check is '<script>x</script>', whose evidence contains '&', and whose value is longer than 120 characters. One is a removed row with baseline_detail. One is an unassessed row with no detail. The output contains '<script>x' and '&' and does not contain the raw string '<script>x</script>'. The long value appears clipped to 120 characters followed by '…'. The page contains 'not recorded in the current run; baseline:' and 'no attempt detail recorded'.
  • In the example's index.html, the new table sits inside a div with class="scroll", tabindex="0", role="region" and aria-label="Why the gate failed". That div appears exactly once. Its five th elements carry scope="col", and each Finding cell is a span with class "tone bad" whose text starts with '✗ '.

One specialist feature. Behavioral acceptance fails on the base and passes on the implementation; project checks and independent model review passed. GPU results are included only when actually executed. Automatically developed and reviewed; no human review is claimed.

@noteflowai
noteflowai merged commit b15bdb6 into main Nov 2, 2026
15 checks passed
@noteflowai
noteflowai deleted the automation/feature-2026-11-02-evalarc-134438647132 branch November 2, 2026 14:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant