eval-tab¶
The dashboard eval tab — a node's measurement timeline (verdict + expected + live freshness) with an evidence GALLERY (N images + a video/transcript) on expand, plus the spec-cli read API behind it.
raw source¶
The eval/loss engine (spec-eval, built by eval-core) records readings; this is the surface that
reads them back. Realize the founding "Evidence — one timeline, two sources" contract's first source: a
node's eval tab lists its measurements chronologically, each carrying its verdict, the scenario's
expected, and the freshness signal spex eval lint reports, with the captured evidence — a gallery of the
reading's whole evidence list (N images plus a video clip that plays inline, and/or a transcript) —
expanding inline. LOCAL readings only for now — the forge issue-events source is a later
sibling; leave a clean seam for it.
expanded spec¶
Two halves behind one tab. The read engine (spec-cli, in evaltab.ts) computes what only a live
read knows. A node's measurement timeline is every reading from its evals.ndjson sidecar (scenario,
the read's codeSha, an evidence LIST — each {hash, kind} resolved to its live blob state — verdict, ts) joined with the scenario's expected
(from the live eval.md — what zero loss looks like) and a freshness flag, derived live from git by the
same freshness machinery the lint uses (eval-core's scenario-freshness derivation): a reading is current until its governed code or its scenario
moved past the sha it was taken at, otherwise stale (and which axis moved); a code-stale
reading also carries the code axis's per-file drift detail (codeDrift — which governed files moved, +how many
commits) so the event-detail stale readout can name it, never re-deriving git in the browser;
newest-first.
The board carries this timeline as a summary (graph-lean): buildBoard folds the latest reading per
scenario onto the node's evals and the declared set slim ({name, tags}), so every overview surface — the
eval-score-badge tile count, the focus-panel — counts the WHOLE set off the one /api/graph poll
(a never-measured scenario still counts as loss). The FULL timeline — each scenario's
expected and per-scenario code included — is served by /api/specs/:id/evals, lazy-loaded when the tab
opens. The board attach stays cheap by reusing the board's specs + driftIndex and one shared eval-file walk.
Bytes are never folded anywhere: /api/evidence serves each evidence entry by its content hash from the
shared cache, fetched lazily on expand, with a per-entry miss original file signal when the bytes are gone, MIME
sniffed from the content.
The eval tab (spec-dashboard) is a fourth face on the node popup beside spec/history/issues, on the
same panesFor registry. It fetches its timeline (readings + declared detail) per node on open, cache keyed
by the summary's newest reading so a fresh filing refetches; a failed fetch degrades to the board's
summary. It is a thin consumer of the chronological-timeline
scaffold the history tab uses (see work-pane): newest expanded, older reveal on the down gesture, an
expand-all door for find-in-page, and — on a long timeline — the shared sticky scenario-name filter
(blind-spot rows and dangling tracks filter with the readings, so a filtered view stays one coherent set). Each
row's header names its scenario, the verdict badge (✓ pass / ✗ fail, optional note
beside; legacy for a pre-verdict or note-only reading), and the per-reading score circle
(eval-score-badge), then its codeSha and time.
Its evidence is the scenario's expected over the captured proof — a gallery mapping the reading's
evidence list (N screenshots, a video, a transcript), each entry showing miss original file when its blob
was pruned.
The tab surfaces the whole declared set in one list, not only the readings. A declared scenario
with no reading leads that list as a blind-spot row — the empty score ring over its name, its
expected, and the files it tracks. The ring is the only distinction (no fenced-off band, no second
scrollbar): an unmeasured scenario is outstanding loss and belongs where the attention is, so a node's
intent is legible inside the popup before a reading lands (the only place it shows once the popup
covers the focus-panel). No reading at all → those rows under a hint; some measured, some not → those
rows lead the timeline. The one presence-distinct empty state survives: a node with no scenarios
(no eval.md → no evals field) shows nothing.
The seam / out of scope: the forge issue-events half of the timeline — each tracked issue appearing twice (open, close) and linking out to its forge-hosted image rather than a local blob — arrives with the needs-eval forge node; the tab joins it at read time then. Backend and computer-use measuring hands, and the cache cleanup surface, stay with their own nodes.