Skip to content

eval-score-badge

Provenance

  • Source: .spec/spexcode/spec-eval/eval-score-badge/spec.md
  • Source SHA-256: e7ab6ea0ad0dd009b1fe44cceb53a4908cbfb6a41b4b737734730e7f86ca3318

The board carries every node's eval readings AND its declared scenarios ([[eval-tab]] folds both onto /api/graph). This node spends that data on a glance: a small count on the node tile — ✓ satisfied of total — that says, without opening anything, how many of a node's scenarios are measured-and-passing and how many are still outstanding (failing, stale, or never measured). A score is execution, like an issue count — so it rides beside the node, never as node state: the git-derived status dot keeps its own authority, and the count is drawn deliberately UNLIKE it.

raw source

Put each node's eval score on its tile as a per-scenario count — ✓ satisfied / total — not one fuzzy collapsed verdict. satisfied is the scenarios that are a fresh pass; the gap up to total is the outstanding loss (a fresh fail, a stale reading, or a scenario never measured), so the number itself says how far the node is from zero loss. The count's COLOUR carries the worst-first state — green when every scenario is a fresh pass, red when any is a fresh fail, grey when the rest is only stale or blind — so the loudest problem still reads at a glance. NO badge at all when the node declares no scenarios (no eval.md). The eval tab keeps the per-reading circle (a ring whose colour is freshness, whose ✓/✗ is the verdict), and the count reuses that same colour vocabulary, so tile and tab still speak ONE language.

expanded spec

One vocabulary, two surfaces. The scoring lives once in score.jsx: readingScore maps ONE reading to a circle state; scenarioStates joins the node's DECLARED scenarios (the folded node.scenarios) to their latest reading so a never-measured scenario is still seen — a unit of loss, not an absence; aggregateState folds those per-scenario states to one worst-first colour; ScenarioCount renders the tile/stat-bar count; ScoreBadge delegates the per-reading icon + label + tone to [[review-chrome]]'s ONE ReviewState mapping, so graph/eval-tab glances and the routed Evals list/detail/A-B cannot disagree or fall back to Unicode. The node tile ([[node-graph]]) and the node-info stat bar render ScenarioCount; the eval tab ([[eval-tab]]) renders the per-reading circle.

Tags are the second at-a-glance adornment. Beside the satisfaction count, a scenario carries classification [[eval-core]] tags; score.jsx exports the one shared TagChips element that renders them as a compact wrapping row of chips. It is the SAME element wherever a scenario surfaces — the search palette ([[session-search]]) and the eval tab's declared-scenario row — so a tag looks identical everywhere and reads off the same .tag-chip vocabulary the other chips use. scenarioStates already threads each scenario's tags through (it spreads the scenario), so the consumers need no extra wiring. This node owns TagChips + its .tag-chip style slice; it does NOT own the tag vocabulary or its validation — that is [[eval-core]]'s schema. Count says how satisfied, tags say what kind — two orthogonal glances on one scenario.

The glance is an entry point — a real anchor, never a dead pill. Wherever the score vocabulary surfaces as an affordance, it is a REAL <a href> into the routed Evals pages ([[evals-view]]), so middle-click, copy-link, and keyboard Enter come free and browser history — never an in-page fake Back — is the return path. A concrete reading (an eval-tab reading row) links to the canonical full-page detail #/evals/<node>/<scenario> (path only — the detail hash carries no list filters). An aggregate count with no single scenario behind it — the node-info stat bar's ScenarioCount — links to the Evals LIST filtered to the node, and every such href is minted by [[address-routing]]'s ONE scenario-less evalAddress(nodeId) form, so the list-filter grammar has exactly one swap point when it evolves. The graph TILE's count is the one deliberate exception: it stays a passive glyph, because a tile's click belongs to the board ([[node-graph]]) — ScenarioCount renders the anchor only when handed an href. evalNav.test.mjs pins this contract at the source level.

The aggregate is a worst-first fold over the per-scenario states: any fresh fail makes it red ✗ (the loudest current signal); else any stale scenario makes it grey (fail-flavoured if any stale scenario last-failed, else pass-flavoured — the node remembers its last verdict but admits it's out of date); else any scenario with no current score — never measured (its own state, counted toward the total), or only a note/legacy reading — makes it the empty blind-spot colour the scoreboard exists to surface; else every declared scenario is a fresh pass and it is green ✓. Because the fold now ranges over DECLARED scenarios, not just the readings that happen to exist, a node with an unmeasured scenario reads as the blind spot it is.

Freshness is the same live signal the tab and spex eval lint use ([[eval-core]]'s scenario-freshness derivation): it arrives on each reading's fresh flag — this node never recomputes it. A scenario's freshness can be scoped to its own code files ([[eval-core]]); this node just reads the resulting per-reading fresh. A note and a legacy pre-verdict reading carry no ✓/✗, so they read as a blind spot here while their textual verdict badge still names them in the tab.

This node owns only its score slice of the shared node tile (SpecNode.jsx) and of the shared stylesheet (its .score-badge + .scenario-count rules, sanctioned by [[node-graph]]'s shared-stylesheet contract) — exactly as [[dashboard-issues]] owns only its issue badge there — so a co-owner's churn in those files is that feature, not this node's drift. Out of scope: what a score MEANS or how it is measured (that is [[spec-eval]] / [[eval-core]]); and the deep per-reading timeline, which is the eval tab's job.