Changelog¶
Every release of the harness, newest first: what it shipped, and what a project pinning it should know. These are the release notes. The same section is the text of the release's GitHub Release, and the harness site shows them at evals.humancopilot.eu/releases.html.
The version lives in scripts/_version.py; a release is the git tag v<version>, and the
release workflow refuses a tag that disagrees with that file or has no section here. A change
that can move a result is never a patch release without saying so under Changed.
0.6.2 — 2026-11-03¶
Faster answers from evh and its MCP server. No result moves.
Added¶
evh mcp --write-answers FILEandevh mcp --public --answers FILE: a release's answers written ahead. Each call to the MCP server runs a freshevhprocess, several seconds of CPU; a deployment that limits the server's CPU pays it in tens of seconds (the evals server's firstgoalsafter each start, and after each hour, took 33 s at half a core).--write-answerswrites the answer of every tool that takes no argument and does not depend on the date (notovervieworactivity); a public server started with--answersserves them without running the command, for as long as it runs. A line another harness version wrote is not read.
Fixed¶
- A view and a catalog build read each arm's scores once.
evh goalsread 1,054 arms' scores for 686 arms; that reading was 79% of its CPU. The answers are byte for byte the same;goals,armsandmodelstake about a quarter to a third less CPU.
0.6.1 — 2026-11-03¶
Fixes to 0.6.0, from two reviews of what it shows. Six can move a number or a verdict a project sees (Changed): the sheet's win rates and verdicts, a goal's recommendation and the places in its field, and the catalog's run and spend figures. One keeps a hidden dataset's labels and notes off public surfaces (Security). The rest restore what 0.6.0 documented or make the catalog say what a number means beside it (Fixed).
Changed¶
- A goal recommends from the group it decides on, once per dataset. A judged group (a grade, a pick, a pairwise pass) or another measure of a scorer is shown with its leader, never recommended: 0.6.0's CNN goal page printed four "Recommended:" verdicts, three of them from judging passes the goal does not decide on.
- A goal's field places one arm per config, without the arms a round retired (a re-timing, a second run, a
paired side run). A config run twice took two places: AG News
anthropic_lread "#8 of 33" in the catalog where the site reads 5th of 30, and the Models page put Gemma 4 31B first on SummEval through its paired side run. - Runs re-scored or re-parsed in place count in every period figure, dated when they were measured: 0.6.0 left Few-NERD's and SciFact's 54 runs and $5.58 out of the activity, the datasets page and the home tiles. A copied re-score (judged, re-scored, silver, adjudicated) keeps its run's id too and is still never counted.
-
The sheet's verdict compares only the arms scored on the metric: an arm with no score on it reads "no
in this run", where 0.6.0 named the other arm the leader and recommended. -
The sheet's win rate on a pick or pairwise verdict is each arm's share of what the judge gave over the rows it judged (its
judge_won), so with every arm shown it is the judging pass's own win rate. 0.6.0 gave a shown arm a whole row when it held the largest share among the arms shown, however small: on the CNN pick passanthropic_lread 0.646 in the sheet against the pass's 0.402. - The sheet's win rate on a metric is over the rows each arm was scored on, "—" for an arm scored on none. 0.6.0 divided by every row with a winner, so an arm with no value on a row counted as beaten there, and one never scored read 0.000.
Security¶
- A hidden dataset's labels and notes stay off public surfaces (RFC-019 §4: per-item labels, adjudication notes).
0.6.0 showed its label sets' confusion matrices, whose rows and columns are the label values, on the catalog's
dataset page and in a public server's
labels; listed a person's sheet notes on its items in a public server'slabels; and copied those notes into a deployment's data. The counts, Cohen's kappa and Krippendorff's alpha stay.
Fixed¶
- The installed
evhreads the project's.env. 0.6.0'sevh, as pip installs it, loaded none (env-check: "loaded 0 var(s)"), so a key or a cost cap put there was not in force;python -m evaluation_harnessand a checkout were unaffected. Found by the newcomer scenario. reference-promoteadopting a hand-made folder keeps its files' bytes. It added a newline to each, so the reference digest every run scored against the folder had recorded no longer matched (rescorewarned the reference had changed), where UPGRADING said no run's reference moves.- Fetching the examples' public data falls back to the Hub's parquet files when the Hugging Face
datasets-server's
/rowskeeps failing (its outage of 2026-11-02 answered HTTP 503 for every dataset): the same rows and row numbers, read withpyarrow, now in each example's environment; every item is still checked against the frozen dataset. The installed harness does not depend on it. inspect --sheet --wrong-against NAMErefuses a name two datasets' sets share on this dataset's items (labels follow an item into every dataset holding it), naming each as DATASET/SET; it read whichever it found first.- A round's best is read as its goal decides: on the deciding reference, never the arm that wrote it (CNN round
r1's best read
anthropic_l1.000, the silver's author scored against itself, beside a conclusion namingbart_l). - Gold reference sets are dated by when their labels came to be: the dataset's freeze, or the adjudication. The 0.4.0 migration had stamped every one with its own time, so the references page listed sets a month old as new. No run's reference digest moves (the digest leaves the date out).
evh deploy-sheetand the goal page say a key production never recorded "cannot be checked", where they read "prompt_sha 0 match; not recorded: 233".- The catalog says each thing once, and what a number means beside it. A goal page names a re-score's scorer
once, counts a long list of contenders, folds a conclusion its answer already shows, and explains its groups and
strategy once. Dataset pages open on the deciding measure, give one inspect button per judging pass, name the
machine in one form ("served on", "run on"), say how much of a period's spend was judging, and show a rise past
doubling as a multiple. The answer column no longer scrolls off at 1280 px; chart names never overlap; a cost every
arm shares draws no axis; a metric's strip leaves out the arm scored against what it wrote; ids break between
words at phone width; the sheet's rows stay short and name arms by what sets them apart; probes read yes or no.
The triage datasets and
fleet_dedup_v1have their profiles (evh dataset-profile). - Site: SciFact leads with the compared field (the best reranker +0.025, not significant) and names the extra
+0.07 run with its caveats; the method page describes fingerprint v5 with 0.6.0's own
run-compareoutput; the use page counts its steps; the XSum control's caveat and the judging spend's passes are carried where they are quoted. - Docs: tutorial 5 and the RUNBOOK named the old fixed ports (8765, 8766) where 0.6.0 gives each project its own; the home page's MCP section names what 0.6.0 added and that the public server never answers with an item's text.
0.6.0 — 2026-11-02¶
What you get: datasets made from what production did (capture, scrub, split, dataset-from-capture); labels that say
who decided what is right, and reference sets promoted from them; production's own decisions scored as an arm;
models onboarded before they are run; scoring that evolves without re-running an arm; judges that grade, pick the
best or compare two; and one sheet of every item and arm, filled in Excel or another tool and read back. Upgrading
moves no committed score. Two formats (fingerprint v5, model records) and the old references are migrated by a
command, and a few names change to the field's words with no alias (evh run, significant, contenders):
UPGRADING has the steps.
Added¶
- Labels, and the reference sets promoted from them (RFC-023, PRD-002, ADR-0022). A label says what is right
for one item and who said it: a
person, amodelor arule. Labels come in sets, never edited, underdata/labels/<dataset>/<set>/, and follow an item by its sha256 into any dataset holding the same bytes. evh label-from-run: a finished run's outputs as a model's labels, no calls, the refusals a reference's provenance needs (a dirty or unknown build, an unfinished or derived run, an empty output, one cut off atmax_tokensunless--allow-cut-off).--not-redistributedkeeps the texts out of git (a.gitignorebeside them) and records the command that rebuilds them, checked against the set's checksums.evh label-sheetwrites a sheet of the items to label (unlabelled, all, where runs disagree, or chosen), each run's decision and a proposal beside an empty label;evh label-importreads it back, or a folder of<item_id>.txt, as a person's set (yestakes the proposal; notes kept). The sheet is CSV that Excel opens and saves (UTF-8 with its mark,,or;);--choicesputs the allowed answers in the label column's header and the import refuses any other; the local inspector downloads it.evh label-from-captureturns a capture's recorded outcomes into aruleset by the rulesreference-from-outcomesread.evh labelslists the sets, what each covers and how two agree: the same answers, sharing one, Cohen's kappa, Krippendorff's alpha and (--confusion) the confusion matrix; free-text labels are not compared as text (an MCP tool). V19 checks every set.- A label can accept more than one answer, when several are right for an item:
{"accept": ["escalate", "file"]}, sorted and without repeats, made by the sheet'salso rightcolumn, a rule'slabel: [file, escalate]or a folder's text, shown "escalate or file". A decision is wrong against a label when it is none of them; two labels agree only when they accept the same answers. An adapter'sscorereads a reference withanswers(reference)(evaluation_harness.adapter); the bundled one keeps the best overlap. The triage gold's 23file-or-escalatelabels (and their reference) are written so now, bylabels_from_verdicts.pybyte for byte: every one of the 18,460 item scores of the 284 runs against it is the same replayed old and new. The outcome rules give the same two answers, so gold and the rules accept the same answers on 34 of the 34 items both label (kappa 1.00). evh reference-promotemakes a reference set of a label set, at the tier you choose, with a reason and who promoted it; the manifest records where it came from (for a model's set, the run and model that wrote it). No rule ties the tier to the source. Every existing reference set records how it was promoted (20 here; 6 copied byte for byte into label sets, 209 labels), every run's fingerprint unchanged.- Scoring against a label set without promoting it:
evh rescore --labels SET,evh run --labels SET. The run recordsreference_id: labels/<dataset>/<set>and lands indata/runs-labels-<dataset>-<set>/, an arm of its own; a goal groups such arms under the set, the arm that made a model's labels flagged. - The catalog: a tier is a pill in its own colour, a label set a source chip, wherever either is named; a Labels page; a dataset's label sets and how often two agree; where each reference set was promoted from.
- Evaluating what production did (RFC-023, PRD-014, ADR-0023). A capture's decisions are evaluated against labels; the governed datasets grow only by a version you make:
evh arm-from-capturemakes production's recorded decisions an arm (provider: recorded): each output the capture's record of the same id, read by the adapter'srecorded_output(record)(else--field), scored, tested and compared like any arm, cost 0. The catalog badges itrecorded, its cost and speed "recorded, no call".- Agreement: the adapter's
same(a, b)says when two outputs are one decision (default: the same text; the triage adapter compares dispositions).evh inspect --disagreeand the inspector's "only where they disagree" keep the items where the arms decided differently;label-sheet --items disagreereads the same hook. - The sheet,
evh inspect --sheet --dataset-id D --runs A,B,…and the inspector's 05 sheet (the dataset page's "Open its sheet"): the items down, a column per arm across with what it wrote and every score it has (its run's metrics, and a judging pass's where one covered the run;--metric MorSCORER/Mchooses the one it is read on;--scores-onlyleaves the outputs out), every label set and reference set, the winner per item (on that metric, and per pick or pairwise judge), the signals wrong (--wrong-against SET), disagree, split, new (--new-vs D: a field value it never had), unlabelled, each a filter beside--whereand--by, and a footer per arm: its mean, its win rate and its verdict (the goal's decision, else the paired test against the leader). On a pick or pairwise verdict only an arm the judge preferred wins a row; a row it gave to an arm not shown is nobody's win. The item column stays in place as the arms scroll; the rows you tick give theevh pickandevh label-sheetcommands.--csv FILEwrites it (UTF-8 with the mark Excel needs), and the inspector downloads the same (/api/sheet,&format=csv). Local only: a public server refuses it. - A sheet to fill, and
evh sheet-import(make sheet-import):--csv FILE --fill labeladdslabel,also rightandnote(--choices);--fill pickorscoreswrites the outputs as blind columns A, B, C… shuffled per row (--seed), apickor ascore: Xcolumn per output (--scale LO..HI), the rubric as the first row (#rubric;--rubric FILE) and its key beside the sheet (sheet.key.json).evh sheet-import FILE --dataset-id D --by WHOreads back what was filled: labels as a label set (--set,--kind person|model), picks or scores as judged runs underdata/runs-judged/<scorer>/(--scorer),judge_won_unvalidatedorjudge_score_unvalidatedper arm per item, through the key. A row or text the key does not have, a letter not on its row, a score off the scale and a name already used are refused, with nothing written. - Picks:
evh pickmarks a production item for the next version of one or more governed datasets (targets, tags, a note) indata/picks.jsonl, append-only;evh pickslists what waits (an MCP tool), and the target dataset's page shows them with a box per row.evh dataset-extend V1 --dataset-id V2 --picks(or--pick) makes V2 of V1's items and the picks chosen, each checked against the bytes picked,extended_fromrecorded; labels follow the items; nothing is promoted. evh deploy-sheet --config ARM [--capture C]: what production must match to run what you promoted (the model, its record and deployment, every setting sent and a record's setting that was not, what each item reported, the adapter), and with a capture, whether each record says production ran it (the adapter'sproduction_ran(record)); exit 1 unless every record matches. On the promoted arm's page, and folded beside a goal's baseline. The triage importer keeps the ledger'sprompt_verandprompt_sha.- Several projects on one machine (ADR-0024):
catalog-serveandmcp --httptake a port of the project's own (18000–18999, 19000–19999), the same every time, the next free one if taken; a local catalog names its project; every command notes a strayEVAL_HOME; a model record's lock is keyed by the record. - Datasets from production: the capture lane (RFC-019, PRD-014, ADR-0019). A project keeps what a production system did as a capture, then makes datasets and labels from it:
evh capturereads records through an importer (--importer jsonl|tsv|github|langfuse, or a project's ownpath.py:Class) intodata/captures/<id>/: each record's id, body, fields, time, source and outcome, hashed one by one (capture.json);--since-capturecontinues from an earlier capture.evh importer-checktests an importer against the contract, including that it keeps tosinceanduntil;evh captureslists them, and the catalog's Captures page shows them with the datasets made from each. A capture keeps the window it records: a record outside it is named and nothing is written (--latekeeps the ones older thansincethat the earlier capture never saw). Newer outcomes of the same records are a fresh capture, labelled withlabel-from-capture --capture(tutorial 6 §5).evh capture-scrubruns a project's own scrubber over a capture's records and re-hashes the ones it changed, keeping their earlier hashes.evh capture --sample-rate R --seed Skeeps a share of the records, by a hash of each id, as they are read: the same rule draws the same records again, an importer may sample at the source, and a continued capture keeps the rule.evh dataset-from-capturefreezes a selection (--where,--dedup,--one-per), typed fields from a field set, and--tag K=Vlabels; only the body becomes the item.- What happened afterwards becomes labels by a small rule set (
when,unless,exists,absent,range,min_age_days):evh label-from-capture, above. evh dataset-splitmakes<D>_validationand<D>_testby hash (stable across versions, a group kept whole with--key) or by time.- V17 checks every capture's records against their hashes; a capture whose records a project keeps out of git (fetched from where it publishes them) is named as not fetched, not failed.
text: hiddenon a dataset: none of its text, nor anything derived from it, on any public surface: the catalog's pages, the inspector andevh mcpin public mode, and a deployment's copy of the data (_release.py copy-data, which also refuses a release carrying reference texts or run outputs git does not track).dataset-from-capture --hide-textsets it.- Dataset tags on the views: the catalog's datasets page groups by each tag key and shows where a
dataset came from (a capture and its selection);
evh datasets --tag K=Vfilters the same way. - Two examples from a production fleet:
examples/triage-fleet/(an agent's real triage decisions, replayed through its own code; the operator's verdicts as gold; the model's own decision scored beside the fleet's, before and after its gates) andexamples/signal-fleet-dedup/(a regression guard for the fleet's de-duplication functions; no model). Their data is published on Hugging Face and fetched by a pinned revision. - A share card on every catalog page: its title and its opening line as
description,og:*andtwitter:card, and withevh catalog --card URLthe image a shared link shows. The pages staynoindex. - Onboarding models, once, for every eval (RFC-020). A project keeps a model record per model,
data/models/<model_id>.yaml: its aliases, weights and licence, what it can do, the settings that hold whatever the task (reasoning_effort; device, precision, batch size for an ML model) with why, its history, and its deployments (each place and build it runs: machine or host, server and version, digest, revision or hash, the route for a hosted one), each onboarded on its own. evh model-onboard <id> --deployment <d>probes an LLM through the deployment's endpoint: the digest the server reports, whether it reasons at the default and with the record's settings, JSON, determinism and speed (the deployment's, not the record's); a different build than the record's is flagged and kept in its history. An ML model or a baseline through an arm (--config).ONBOARD PASSorONBOARD CHECK.evh arm-smoke --config <arm>runs any arm on a few items before its full run and writes no run: runs, valid output for the scorer, a projection of the full run's time and cost, build pinned, determinism; for an LLM every model call (reasoning, slow, long, cut off, no answer, from a provider's_metaor an adapter'smeta["calls"]; a reply is long past half the arm's reply cap, or with no cap against the arm's own median, so neither a summarising arm nor an agent's short probe calls beside its answers make a long reply).SMOKE PASSorSMOKE CHECK, written into the record.- An arm names a record and a deployment (
model_record:,deployment:): the record's settings go under its params, which add the task's own (its reply cap, its call limit: a record refuses them, since they would reach every other eval's arms); the run records both. V18 checks the records, and the arm configs indata/configsand those validate.json'sconfigsglobs name;validatenotes an arm never smoked, a reasoning model with nomax_tokens, a tag that is a different build on two deployments. - The catalog's Models page and a page per model, a smoke badge on the arms page, a models tile;
evh modelsfor the same (and overevh mcp). A model is vetted when an arm that names it passed its smoke; its onboarding alone makes it onboarded. - Scoring that evolves (RFC-022). A round records the scorer it is judged by, and a scoring change is a step of its own:
evh score-preview --goal G [--round rN]re-scores the goal's arms (or the round's) with the scorer in the working tree, in a temporary directory, and prints each arm before and after and the decision before and after; it writes nothing and spends nothing.evh rescore --goal G --round rN --why "…"re-scores a round's arms; the round records the change (scoring: when, the scorer, why, where).evh round-open --from rN --keep all --scoring "…"opens the next round judged by the current scorer: every carried arm another scorer judged is re-scored first ($0), each one's measure printed before and after.- Re-scores land in one place the tool designates,
data/runs-rescored/<scorer>/<source>/, read by the catalog, the goals, the rounds andvalidate;--outstill names any other. Rounds read an arm from its re-score as the goals do, andevh roundsand the goal page show a round's scorer and scoring changes. evh round-open --adoptrecords a goal's first round from the arms already run for it.- A goal axis's
limit: baseline: an arm is out when the paired test finds it worse than the group's baseline arm on that measure. validatenotes a round whose arms one adapter scored in more than one version.evh init's.gitignoreignores the throwawaydata/runs-smoke*/anddata/runs-tune*/.call_timeout_s, an arm's call limit, for the built-in providers: the SDK client's timeout (its own default is 10 minutes). The RUNBOOK told arms to set a call limit only the triage adapter honoured.- The triage example's adapter takes
reasoning_effortandmax_tokensper arm, reports every model call forarm-smoke, and records a call that fails the same way twice at temperature 0 as the fleet's own "no terminal decision" (no_decision,deferred). Withreply_in_tool_call: <name>it reads an answer a server returns as a tool call to that name, with no text: Ollama 0.40 does that to every gpt-oss answer, and the fleet, which reads only the text, got nothing. - No key for a local server: the
openaiprovider asks for noOPENAI_API_KEYwhen its address is on this machine (localhost, 127.x, ::1) or on Ollama's port, 11434; Ollama takes any key. Any other address still needs one, and a key that is set is used. - Tutorial 7, set up a model before you test it: a model record, onboarding it where it runs, an arm that names it, a smoke before the full run; on your own machine with Ollama.
- Judging modes (RFC-005, PRD-003 FR5): a scorer's
modesays what a judge is asked:grade(the default, as before),pick(the best of an item's outputs, in one call:outputs_per_callmust hold every arm) orpairwise(the better of two, every pair of arms;outputs_per_call2). Pick and pairwise projectwonper arm per item (judged runs' primaryjudge_won) and report each arm's win rate and Bradley-Terry strength with a 95% interval from resampling the items, and the position and noise checks as the share of verdicts that held. A judge is validated in its mode (validation.modemust bemode). The SummEval example validatesgemma_landqwen_sin both (mode: pick, scoredtop1besidetop1_chance;mode: pairwise, scoredpair_agreementwithn_pairs; arandomfloor each): picks 0.364 and 0.263 against a random 0.070, pairwise 0.825 and 0.813 against 0.473; the prompts ask for the JSON first and nothing after it.judge-summ-pick-v1andjudge-summ-pairwise-v1are the summarisation eval's judges in those modes; the pick pass is run (29 arms in one pick: the order moved about half the picks, see the SummEval README), the pairwise one not yet. - An adapter's optional
primary_metric(params): it names an arm's primary metric from the arm's params, besidePRIMARY_METRIC(its fallback);runandrescorerecord it. The SummEval adapter's istau,top1orpair_agreementby mode.
Changed¶
evh experiment-runisevh run, and an experiment is an eval: industry words (OpenAI evals, inspect_ai, Weave, Langfuse); what Braintrust and LangSmith call an experiment is a run here. The site's Experiments page is Evals (evals.html); the old address redirects.- Dataset roles are validation and test (were dev and heldout):
evh dataset-split --test SHARE(was--heldout) writes<D>_validationand<D>_test, and a split dataset recordssplit.testandpartvalidation or test. A goal'srole:says validation, test or pilot. The reports' dev slice is the pilot slice, a small slice inside the test slice (not a validation split), and their measurement slice the test slice.evh holdoutkeeps its name. - The paired test's verdict is significant (was separated):
significant: better,significant: worse,not significant, everywhere they are printed or shown (evhoutput, the catalog, the inspector, the site, the reports);family_testandbootstrap_testprintSIGNIFICANT,pair-testSIGNIFICANTorNOT-SIGNIFICANT. A project reading the old verdict strings reads the new ones. - A goal's arms that may win the tie-break are its contenders (were tied): the decision's key is
contenders(wastied),evh arms --quality contender|below(wasties|below), the arms page's facet "a contender", and the why-lines say "Contenders beside it" and "Still undecided after the tie-break". A tie is still what it was in a paired test: an item where two arms score the same. - A promoted reference is a reference set (the governed collection under
data/references/<tier>/<name>/); a reference is the expected answer for one item. The catalog's References page is Reference sets. Commands, paths and keys (reference-promote,reference_id,reference_tier) keep their names. REFERENCE.md maps every term to what inspect_ai, lm-evaluation-harness, HELM, OpenAI evals, Braintrust, LangSmith, Langfuse and Hugging Face call it. reference-createandreference-from-outcomesare removed (ADR-0022): a reference set is made only by promoting labels. A run's outputs arelabel-from-run's (with the same refusals and--not-redistributed), outcomeslabel-from-capture's (the same rules). A reference set made from outcomes has no rule of its own any more: promoted, it is a silver like any (with gold beside it, gold still decides; ADR-0020 is superseded). The one reference set here that namedreference-createas its rebuild says where its texts come from instead.validatefails a reference set with no manifest (V16), where it noted one: texts copied intodata/references/by hand are a reference set nobody promoted. Import them as labels where they are and promote them:reference-promoteadopts the folder when its texts are the labels' (writing its manifest; a folder whose texts differ is still never overwritten). Tutorial 1, the newcomer scenario,evh self-testand the tests make gold that way.evh reference-manifestis a promotion: it takes--labelled-by,--reasonand--by(and--kind), and records them aspromoted_fromand the reason, so every reference set says where it came from; the reference schema requires both. This repository's reference sets are aligned: the caveats name where their labels came from (no "human-authored" or "model-generated" tier wording), the two that named a removed command as their rebuild name the 0.6.0 ones, andfrom_outcomesandcost_usd, which only the removed commands wrote, are gone. No run's fingerprint moves (none of these is hashed).- The sheet shows each arm's decision, the signals first: an adapter's optional
decision(output)(the triage example's: its disposition) is what an arm's column shows, the output itself otherwise; with nosame(a, b), two outputs agree when their decisions do. The browser's columns follow the terminal's (UXS-002). An adapter's optionaltitle(item)names each item (the triage example's: the signal's alert name, not the probes that open it). evh inspect --itemsis gone, and so is--rows; the sheet replaces both (no aliases):evh inspect --sheet --dataset-id D --runs …shows every item's scores per arm and the rows view's signals in one table. The inspector's endpoints/api/rowsand/api/label-sheetare gone;/api/sheetserves the sheet, and its&format=csvthe download. The MCP toolinspectoffers the sheet's options but not the ones that write a file (--csv,--filland theirs).- The triage example's
write_gold.pyislabels_from_verdicts.py: a source's labels fromverdicts.jsonl, one file per item, forevh label-import; the tier is the promotion's. Its owner labels rebuild the committed gold set byte for byte. catalog-serveserves on the project's own port, not 8765 (--portoverrides;--publickeeps 8765), andmcp --httpon another; a bookmark of127.0.0.1:8765moves.- The release leaves the pick list out: picks are working notes that name production items, so a published catalog shows none waiting.
- The tutorial kit's desk export records where the desk's router sent each ticket (
routed): tutorial 6 scores that decision against what happened, and picks the misrouted tickets for a dataset it governs. - The goal page explains, then answers, then shows how it got there: what it is for first; then the answer so far, led by the latest round's conclusion when one closed after the baseline was promoted (the promotion's reason folded under it); then its rounds, one folded section each, headed by its row (round and status, the conclusion, what it set out to learn, arms and best), and inside it a heading per part (what changed, arms with retirements grouped by reason and the carried ones folded, scoring, evidence, suggested next, notes). The rounds table is gone.
- The Models page has one line per cell: the first build and "+k", the deployments' ids, "N goals" with the first named, the single best placing with "+k more"; vetted records first. A model's page has one folded section per deployment, holding its build and its own onboarding with the flags it accepts and why; its arms table names each arm's deployment and reads a re-scored arm as the goals do.
-
The datasets table shows each tag as a chip.
-
Fingerprint v5 (RFC-021): what each part identifies.
instrument(the harness and the adapter's code),data,deployment(the system under test as it executed: from the arm's model record and deployment, the adapter's hook, or the params; the weights, the server, where it ran; in process only when the arm calls nothing elsewhere) andsettings(how it was asked: the params that change behaviour, and what a hook declares about its prompt), each hashed, andhashover the four. The machineevhran on isrunner, recorded outside the hash: the same deployment run from two machines is the same measurement.evh run-comparenames the part that moved; the inspector's speed warning tells a deployment that differs from a runner that does. Every committed run is migrated byevh fingerprint-migrate(idempotent,--dry-run); v4 fingerprints are not kept. -
data/models.yamlis replaced by the model records (data/models/): its 41 entries are migrated one record each, and the arms page's open / proprietary filter reads the records. -
CI runs once per merge, not twice: a squash onto main whose tree is its pull request's head, tested green and up to date with main, skips the workflows that already passed on it; the deploy no longer waits ~13 minutes for a second run of the same files. Anything else runs them again.
- An item id may contain
:,@,+and=(a production record's id, e.g. a fingerprint and its time), on the inspector's item view too; still never a path. - A deployment copies its data through
_release.py copy-data, not a list inrender.sh: the same directories, plus each capture'scapture.json(never its records), with thetext: hiddenrules applied and the untracked-text refusal checked before anything is written.
Fixed¶
- Every
evhcommand is amaketarget again, as the READMEs and the RUNBOOK say: the 22 added in 0.5.0 and 0.6.0 had none (make labels,make reference-promote ARGS="…", …). - The home page's "recorded" feed shows the period's events. Its period filter read the date from the table cell, not the span inside it, so it hid every event and said "Nothing recorded in this period" (since 0.4.2).
- Coverage counts an arm scored against a label set as
labels, on the home page and inevh overview; it counted asnone. - The home page's glance reads a goal's baseline from its re-score (RFC-022), as the goal page does: a baseline promoted before a re-score showed no value and no test.
- A frontier chart's names stay inside the chart: a long name on a dot in the middle ran past the right edge; it goes to the side it fits, shortened when neither does, never over the y axis.
- The label-set and agreement tables keep their promoted and agreement columns on screen on a desktop: long set ids may break there (a phone still scrolls the table sideways).
evh deploy-sheetsays "against no reference" for a run scored against none, notNone (None).- The leaderboard's silver note no longer says "model-generated": a silver reference set can be a person's or a rule's labels.
- A retired arm read through its re-score is out of the goal's contenders. A round's retirements name the arm's
config, and an arm read from
runs-rescored/<scorer>/kept the prefix, so it was never matched as retired and stayed among the contenders (the triage goal's: 76 arms, 51 once read right). - Sorting a page's later table sorts it by the column clicked. Its header's index was counted over every sortable header on the page, so a second table sorted by a column it does not have and threw.
evh mcp --publicsees only public fields ininspect. The tool runs the command in its own process, where the inspector's public rules were never set, so a--whereor--byon a field not marked public was accepted and its values could be read through the counts (0.5.0). A public server now runs its commands as public (EVH_PUBLIC), and such a filter is refused by name. This repository's own deployment declares no field sets, so it exposed nothing.- A command's own refusal through
evh mcp(exit 1, itsERROR:line, no traceback, such as a dataset id that is a path) is told to--hooksasrefused: "command", not as an error: a deployment's metrics count it as refused and its analytics send no event for it. - The site's links to its home page point at
/, neverindex.html(analytics counted the two apart, search engines saw two addresses for one page), and each of its pages names its own address as<link rel="canonical">andog:url, checked bysite/build.py --check. validatechecks every git-tracked runs directory, thosecommittedin validate.json with their floors and every other one: a directory re-allowed in.gitignorebut not added to the list was never checked (a set of 65 re-scored triage runs, before RFC-022 gave re-scores one place).- The local catalog reads what is committed. It read every
data/runs*directory on disk, so a smoke pass in an ignoredruns-smoke*showed as a result locally and was gone from a clone and the published catalog. Directories git ignores are no longer read (outside a git checkout everything is). A measured arm the goals read from its re-score says so on its page, on the arms page and inevh arms. - Runs written where git does not keep them are named.
EVAL_RUNS_DIRpointed at anydata/runs-*directory that.gitignoredoes not re-allow wrote runs that were never committed and that no catalog showed, without a word: 35 self-hosted triage runs sat indata/runs-localthat way.runandrescorenow print aWARNINGwhen they write to such a directory (throwawayruns-smoke*andruns-tune*stay silent), or outside the project's data, where nothing reads them, andevh validatenotes one that holds finished runs. The RUNBOOK's scratch example isdata/runs-smoke, the throwaway name git already ignores. - One row per arm when a scorer changed. A goal listed an arm twice when its runs had been
re-scored (
evh rescore) into a derived set, once per directory, and dropped the measured runs whose older score lacked the goal's measure: 37 triage arms were listed twice, 15 only from the re-score. A re-score against the same reference now stands in for the run it re-read on the goal pages, inevh goalsand in the recommendation, which says how many arms it read that way.evh goals --goalalso prints a dataset's notes, as its page did. - A deployment the adapter's hook declares keeps the server it was called on. Its
serverwas always null, so a self-hosted run whose adapter has afingerprint()hook lost its Ollama or vLLM URL. The 130 triage runs are backfilled with the fleet they replayed (tools/backfill_triage_fleet.py: the fleet's code hash and commitf07efa9, the prompt's hash, each checked against what the runs' own items recorded) and re-sealed. - Onboarding a model on several machines at once keeps every result.
model-onboardandarm-smokeread the record when they started and wrote all of it back when they finished, so the slower erased the other machine's result. They re-read it under a lock and write only their own deployment or smoke. - A LiteLLM address given with
/v1is not given a second one (http://host:4000/v1became/v1/v1, a 404), and a reasoning setting sent through LiteLLM is named inallowed_openai_params: most hosted model groups refusedreasoning_effortwithout it (UnsupportedParamsError). - A record can accept a flag as the model's nature (
accepted: {reasoning: "<why>"}): onboarding and arm smokes report it as accepted and pass (gpt-oss reasons whatever it is asked). A deployment accepts kinds of its own beside the record's: a hosted gateway's build cannot be pinned, and its repeats at temperature 0 vary. - A round with more than 40 arms is compared. Its evidence asked the inspector for one comparison of every arm, which refuses past 40, and the refusal left the rows empty without a word (the triage round, 52 arms). Past the inspector's limit the round runs the same paired test on the scores directly; a refusal becomes a note.
- A project module edited within the second is what runs. Adapters and hooks were imported through
Python's bytecode cache, which trusts a
.pycwhile the source's modification time (whole seconds) and size match: a scorer edited to a value of the same length (BONUS = 0.0to0.5) and re-scored at once ran the old code while the re-score recorded the new code's hash. They are compiled from the source every time. - A system that ran in the harness's own process, with no hook, is in process. A deployment taken
from the params was always
in_process: false, so a local baseline (Lead-3, a keyword rule) kept its libraries on the runner instead of the deployment.evh fingerprint-migratebrings existing v5 fingerprints to the rule when it has the run's params (11 runs here, one baseline's hash). - A run stops when its server cannot be reached. An adapter that turns a failed model call into an
answer (the triage replay records it as the fleet's "no terminal decision") wrote a complete-looking run
when a tunnel dropped: 54 and 65 refused items scored as the model's own failures. When every call an
item reports (
meta["calls"]) failed reaching its server (connection refused, no route, a name that does not resolve),runstops there withSTOPPED, that item unwritten, and--resumecalls it again. A model's own failure is still a measurement. evh pair-test --roundreads each arm as the round does: from its latest re-score when one stands in for its runs. It readdata/runsalone, so a measure an arm's re-score added (production's triage run ontriage_cost) found "only 0 shared item(s)" while the round's own evidence tested it. "(its leader)" is said only when the test chose the leader.
0.5.0 — 2026-10-29¶
Two things to know before upgrading: the package now needs Python 3.10 or later, and it depends on the
official MCP SDK (mcp>=2.3,<3). On 3.9, stay on v0.4.4. What you get: evh mcp, the project's read
commands as tools for any agent (stdio on your machine, or --http for a team or the public), and a dataset page
that shows what is inside a dataset without republishing its text. UPGRADING has the steps.
Added¶
evh mcp: the project's read commands as MCP tools, for an agent (stdio, the official SDK, RFC-018). Thirteen tools:overview,datasets,dataset-items,arms,goals,references,metrics,activity,search,inspect,rounds,runs-list,validate. Each call runs the command as a child process, so a tool answers what the command prints; each is annotated read-only; no tool writes project data, spends or runs a model (the one file a call writes is the p cache in the project's ignored.cache/, below). For Claude Code, in the project:claude mcp add evh -- evh mcp.evh mcp --http: the same server for a team, or for anyone. Streamable HTTP, stateless, JSON responses: clients on every protocol revision since 2025-03-26 call tools without a session. The Host is checked against DNS rebinding (--allowed-hostis required to bind beyond the loopback);--publicleaves out the options that answer with an item's text, and thevalidatetool;--rate,--concurrentand--timeoutbound the load; a missing required argument is refused before anything runs;--hookstakes a deployment's error reporting, tracing, metrics and analytics, in the inspector's shape. Each call is logged as one JSON line: the tool, its arguments' names (never their values), how it ended, its time; never the caller's address or user agent.--ratecounts calls a minute per caller (an IPv6 caller by its /64),--rate-totalfor all callers together; over HTTP a caller has one call running at a time.EVH_P_CACHE: the goals' paired tests kept on disk. Everyevhcommand is a fresh process, so a view that shows the goals drew the same tests again each time (about 50 s on this repository's runs). With the variable naming a file, each estimated p is stored and read back exactly;evh mcpsets it to the project's ignored.cache/.goals,overviewandarmsanswer in under two seconds after the first.- This repository's MCP server,
https://evals.humancopilot.eu/mcp(the deployment, not the package): the six worked examples for any agent, read-only, without sign-in. -
--jsonon the catalog's view commands (overview,datasets,arms,goals,references,metrics,activity,search,inspect): one object,{"command", "version", "result": {"blocks": [...]}}, each table as columns and rows of values, every other line as text. It is collected from the same calls that print the text, so the two cannot disagree. 0.7.0 gives each command its own result schema and retires this generic form: do not build onblocks. -
What is inside a dataset, on its page and as a command.
evh dataset-profile --dataset-id D, run where the dataset is materialized, writesdata/profiles/D.json(schemas/profile.schema.json, checked by validate V1): the number of items and their words, the shape of each gold reference (labels and how many of each, entries per item, a record's fields, or words per reference), and per item its length, its gold and, when known, where it can be read at its source. A gold of labels (a few short values that repeat) is listed per item; any other gold is summarised, never listed. It holds no item text, which a dataset's licence may not let you redistribute, unless written with--with-text, which is refused unless git has committed every item's file (a project's own synthetic data). Commit the profile and the dataset page shows "what's inside" wherever the catalog is built;evh dataset-items --dataset-id D [--item ID]prints the same, and an item's text when it is materialized here. - An item's link to its source comes from the dataset: an item's
source_url, or adata/sources/<dataset_id>.rows.jsonthat a fetch or import step writes ({"source", "item_url", "rows": {item_id: {"row", "sha256"}}}), matched by the item's bytes, so an id that recurs in another dataset never takes its link. This repository's Hugging Face fetchers write one, so each item opens at its row in the dataset viewer. inspect.html?…&item=IDopens that item once the comparison has loaded; a dataset page's inspect link uses it.- Adapter:
dataset_profile(dataset), optional (RFC-003). - Every catalog page names its command, in its footer (
evh goals --goal G, the inspector's from the arms picked).evh catalog --mcp URLnames the same as an MCP tool call, with the server and how to connect (--mcp-howto); this repository's catalog passes its/mcp, and its header links to the site's MCP section. evh dataset-profile --jsonandevh dataset-items --jsonprint the profile inside the same envelope as the views,{"command", "version", "result": <the profile>}; the Makefile targetsdataset-profileanddataset-items.
Changed¶
- Python 3.10 or later. The official MCP SDK needs it, and every feature comes with one install, so the
package's floor moves from 3.9 to 3.10 (CI tests 3.10, 3.11 and 3.14). On 3.9, pin
v0.4.4. - A new dependency:
mcp>=2.3,<3, which bringsanyio,httpx2,mcp-types,opentelemetry-api,pydantic>=2.12,pyjwt[crypto],python-multipart,sse-starlette,starlette,typing-extensions,typing-inspectionanduvicorn. No result moves. - A dataset id is a name, never a path. Every command that reads or writes
data/…/<id>.jsonrefuses an id that is not letters, digits,.,_and-(or that holds..); before,../xread or wrote outsidedata/. - A param shown on a catalog page or by
evh arms --armhides a URL's user and password and a nested key named like a credential (headers: {Authorization: …}), as it already hid a top-levelapi_key.
Security¶
From two independent reviews before this release, each reproduced before it was fixed:
- The inspector served any
.txtunder its data tree through/api/item?item=../../…(an item id was never checked; onmainsince 0.4.0). An item id is now a file's name, refused otherwise, in the server and the adapter. - The inspector's answers were cached by count, not size: a hundred distinct 40-arm questions took a 256 MiB
container past its limit, and
--warmalone did on this repository's data. The cache is bounded by the bytes it keeps (32 MB), keeps no refusals, takes no question over 4 KB, and--warmstops when it is three quarters full; the deployment's inspector has 384 MB (it peaked at 228 of 256 under load). Its rate key is an address, never a header's text. evh mcp --http: a few parallel 4 MiB bodies killed the server; held GETs and ping floods cost memory no rate saw. Now--max-body,--max-connections, 8 open requests per caller, a 10 s deadline on a request's body (eight callers holding slow bodies took every connection),--request-rate, GET is 405, refusals do not count toward--rate-total, and--publicanswers are kept and their errors carry no paths. Values with NUL bytes or over 512 characters are refused.- The deployment: Caddy refuses a body over 64 KB on
/mcp(and itshandleline now adapts); nginx limits each visitor (20 requests a second, a burst of 200, 32 connections); the deploy checks the inspector is up and under 80% of its memory once--warmis done, and rolls back otherwise; the MCP hooks no longer export the SDK's own spans; the containers'/tmpcaps files as well as bytes;render.sh's check for reference texts in a release could pass with 1,657 of them (a pipe's SIGPIPE) and refused the five tracked ones: it now compares against what git tracks; a deploy keeps two releases (the live one and the rollback), prunes at its start as well as its end, and refuses to start short of 2 GB or 300,000 inodes; the upload hard-links unchanged files again (no file ever linked: each release took ~69k new inodes).
Fixed¶
- The deployment's release ships
data/models.yaml(render.sh, and the item fields and profiles with it): from 0.4.2 it was never copied, so the deployed arms page called every model's weights unknown.tests/test_render_data.pyfails when a part ofdata/the adapter reads is not copied. - The CNN/DailyMail and SummEval fetchers deleted the committed gold
manifest.jsonon every refetch; they now clear only the item files. --hooksfiles with a module-level@dataclassfailed to load (the module was not registered before it ran); they load as any other module now.- A page links only to
http(s)URLs: asource_urlor rows map that heldjavascript:…was written into anhref. - The p cache reads an unreadable or non-UTF-8 file as no cache instead of failing the command, starts a new line after a torn one, and keys each p by how it was computed.
evh dataset-profile --with-textcounts a file as tracked only once it is committed, not when it is merely staged.
0.4.4 — 2026-10-28¶
The catalog says what it is and links out: --about puts a deployment's own paragraph on the
overview, a goal's links are shown, and --repo turns project paths into links. The arms page
now carries the deployment's options like every other page. No result moves.
Added¶
evh catalog --about FILE: a deployment's own paragraph on the overview, under its description. It says which project the catalog shows and where its findings are. None by default.- A goal's
linkson its page and inevh goals --goal G. The field was in the goal schema but shown nowhere. A web address opens; a path in the project is named as text, because the static pages cannot know where the project is hosted. evh catalog --repo URL: where the project's files can be read (e.g. a GitHubblob/main/address). A project path in a goal's links, or at the start of a guardrail's source, then links there. Without it, the path stays text.
Changed¶
- The overview says what the catalog is: "pages the harness builds from this project's committed runs", and that the inspector tests any arms against a baseline, item by item.
Fixed¶
evh catalogbuilt the arms page without the deployment's options. Run as a command, the builder is__main__, and the arms page was rendered by a second copy of it with the defaults. Soarms.htmlsaid "eval catalog" whatever--brandwas, and had no--linkswitcher, no--headand the default--api. Every page now carries them.
0.4.3 — 2026-10-27¶
A security fix to the inspector: evh catalog-serve no longer serves files through /api/.
Upgrade if you run it where anyone else can reach it. No result moves.
Fixed¶
- Nothing is read through
/api/. A path under/api/skipped the server's path checks and fell through to the file server, so/api/../.env, encoded or not, returned files from the project root, keys included. This happened in local and in--publicmode. Now a..anywhere in a path is refused first, and only the API's three endpoints (/api/health,/api/compare,/api/item) answer under/api/. Any other/api/path is 404. In--publicmode no path outside the API serves a file. --hookssee one route for every unknown/api/path. It is named/api/other, so someone asking for many made-up paths cannot create a metric series for each.
0.4.2 — 2026-10-27¶
A catalog that says what changed, and where: every count on the home page has a trend and opens a page that shows the rows behind it, for the same period. An activity page and an arms and configs page. No result moves.
Added¶
- A period on every page, counted from today. Four chips (14 days, 30 days, this month, all
time) and a list with the rest (7 and 90 days, this and last week, last month, this year); the
browser works it out from the viewer's date, so a catalog built days ago still shows this week.
A trend compares with the same days of the period before. The period travels in the link
(
datasets.html#p30d). On the goals, datasets, arms, references and metrics pages a row made in the period has a new badge, chips keep only the new or the active rows, and in-period columns give the runs and spend with their trend (▲ 12%, ▼ 31%, ▲ from 0) and a line of runs per day. The goals page lists what happened to each goal (baselines, rounds). - The activity page (
activity.html,evh activity): the home page's charts at full width, and the period's runs, spend and tokens by dataset, by arm and by kind of call, the busiest first. The measured runs, billed and tokens tiles open it. - The arms and configs page (
arms.html): every arm, saying how it was measured (measured, rescored against a reference, judged by a scorer, re-parsed, run again), grouped by dataset, config, model, provider or what it is scored against, filtered to local or hosted arms or a price tier, with its role in each goal's decision; tick arms on one dataset to open them in the inspector. Every config across the datasets it ran on, with its rank on each goal. What changed for arms in the period, and each goal's trade-off charts. The arms tile opens it; the configs tile opens it grouped by config. --period Pon every list:evh datasets,goals,references,metrics,armsprint the page's period columns.evh armslists the arms page's rows (--by,--where,--tier) and--configsthe configs. Each page's period numbers come from one builder the page and the command both read.- Adapter:
dataset_created(dataset), optional: the day a dataset counts as new (else its first run).spend()events may name a measured run'sarm, which the arms and activity pages bill it to;HarnessAdapterdoes. - Grouping on the datasets page (by goal) and the metrics page (by role: decides, weighs,
describes), as the arms page groups; one script (
group.js) groups and filters all three. - The arms page filters on quality and on weights: ties the leader or below it (the arm's place
in a goal's decision), and open weights or proprietary.
evh arms --quality ties|belowand--weights open|proprietarylist the same. data/models.yaml(optional,schemas/models.schema.json, validate V1): each model's weights, open or proprietary, and licence, keyed by the model a run records. The adapter's optionalmodel_facts()reads it.- A derived arm says how far it moved from the config's measured arm on the same dataset
(rescored, re-parsed, run again: "0.712 → 0.745 (+0.033) vs measured", in
evh armstoo). - An arm a goal leaves out for a limit it breaks is badged "over a limit", not "not eligible".
evh catalog-serve --hooks FILE.py|MODULE: a deployment's error reporting, tracing or metrics, on a generic hook point (start,request,error, extraroutessuch as/metrics). The harness ships no tracker and depends on none; without--hooksnothing changes. This repository's deployment uses it (infra/inspector/hooks.py).
Changed¶
- The home page's tiles carry trends. Measured runs, billed and tokens show the period's total, ▲/▼ % against the period before and a line per day; goals, datasets, configs, arms, references and metrics show the total and how many are new. The tiles are grouped and coloured by subject (goals; data; experiments; work and spend), with a legend; never red or green for a direction. The charts and the feed follow the period.
- The menu is grouped by the same subjects: Overview, Goals, Data (Datasets, References), Experiments (Arms, Configs, Metrics), Activity, Inspector; a phone's menu lists every page.
evh overview --period Pprints the tiles and trends;--allis gone (--period all).- The activity page and
evh activitylist what moved most first against the period before (up or down), then the busiest; for all time, the busiest. - "What changed in the period" on the arms page folds three or more arms of one dataset
re-measured the same way on one day into one line ("12 arms on
<dataset>"). - A period select never makes a page wider than a phone, however long its options.
Fixed¶
evh bootstrap-testand this repository's report tools refuse when the gold references are not on the machine (the repository commits their checksums only): they name the fetch step. The extraction and retrieval reports printed tables of zeros instead, as if they were results, and the classification report died with a traceback.
0.4.1 — 2026-10-27¶
A catalog that works on a phone and sits inside a deployment's own site, and an inspector that no longer runs out of memory. No result moves.
Added¶
evh catalog --head FILE: HTML added to every page's<head>, for a deployment's own analytics or tags. The harness adds none of its own.evh catalog --api PATH: where the inspector's API answers, for a catalog served under a path (default/api/; e.g./catalog/api/).evh catalog --brand NAME: the product's name beside the logo (default "eval catalog").
Changed¶
--link LABEL=URLis a switcher between the parts of a publication, not extra nav entries: the links head every page in order, the catalog marked (LABEL=.places it; without one it comes first as Catalog), and the logo opens the first. The catalog's own pages stay in its nav (a ☰ menu at the left of the bar on phones), and its front page is called Overview. The search box reads "Search ( / )", and on a phone it is a round search button in the bar that opens it below; a long count on the overview shows in short form on a phone (19.5M). The bar's buttons are SVG icons.- Tables on a phone: a cell breaks between words, never inside a name, so a narrow screen scrolls a wide table sideways; running text keeps a readable width; a long item id is shortened in the inspector's per-item table (the whole id is its title); no catalog or inspector page is wider than the screen.
- No inline styles: the charts' legend colours are classes, so a deployment's content
security policy can forbid
styleattributes.
Fixed¶
- The inspector no longer runs out of memory. The paired test kept the signs it draws for
every item count it met (~31 MB each), so
--warmon a large catalog grew past a 256 MiB container limit and the server was killed every few minutes. At most two sign tables are kept now, stored as bytes, and at most 4096 estimates; a warmed inspector settles at ~150 MB. The p-values are the same, bit for bit.
0.4.0 — 2026-10-25¶
See your runs, not only tables of them. evh catalog builds static pages over a project's
runs, and evh catalog-serve runs an inspector that tests chosen arms against a baseline and
shows one item's outputs beside its reference. A project on the harness's layout needs no code;
one with its own layout writes one adapter. Goals record what an evaluation is for; items carry
the typed fields a project declares, and can live at a pinned URL. The paired test now lives in
one place, evaluation_harness.stats, exact at 30 items or fewer.
Upgrading can move a result only where pair-test or family-test ran on 30 items or fewer,
in a wins count or an order on a lower-is-better metric, or where --exclude-dataset named a
dataset whose items are not .txt files under data/sources/<id>/ (all below), and adds no
dependency. Design:
ADR-0002,
ADR-0003;
how to upgrade: UPGRADING.
This repository now deploys the harness on its own example experiments:
- the catalog and run inspector are public at
evals.humancopilot.eu/catalog;
- the public site, the harness and its findings, moves from GitHub Pages to the same server;
- the docs move to evals.humancopilot.eu/docs, and old
/docs/ links redirect there.
The deployment files are in infra/ (runbook: infra/README.md). They are not part of the
package.
The example experiments keep one record per arm, the latest: the rescored and re-parsed runs
are folded into data/runs as derived runs, and DailyMail's bart_s is its fp32 run
(coverage 0.4048; the fp16 run read 0.4039). CNN's bart_s stays the fp16 run, the one the
judge's verdicts were made on. The reports state the current figures only; their correction
logs are gone (git keeps them). Two published statements were stale and are restated:
SciFact's qwen_s e5-first against the e5-first leader is +0.0178, p 0.10 (it read +0.0092,
p 0.36, from the pre-fix run), and DBpedia's 56-item dev slice, over today's 28 arms, puts
bert_base_fy alone at the top rather than 13 arms in a tie.
Added¶
- The rest of the inspector and catalog in the terminal. Every page and view now has a command:
evh search TEXTis the catalog's search box, ranked the same way, with the command that shows each hit;evh inspect --itemsis the per-item table (each arm's score and its Δ against the baseline);evh inspect --analysis [--x METRIC]gives the analysis charts as numbers: spread per run, the difference from the baseline, and each arm's mean against a second metric.pair-test --round rNtests within a round. A defaults to the round's leader, B to the baseline the round started from, and the metric to the goal's measure. An arm that is not in play is refused.- On the goal page:
- each round has an inspect button that opens its arms against its leader (
evh inspect --round); - every suggested command has a copy button;
- a retired arm is greyed out.
- Experiment rounds, optional (ADR-0004).
Experiments go round on round: try some arms, keep what wins, drop what lost, change something,
compare again.
evh round-open --goal G --dataset D [--from rN]records what a round sets out to learn, what changed, the arms carried in unchanged (--keep) and those dropped with the reason (--retire ... --reason); arms join by naming it in their config (round: rN, recorded on the run).evh rounds --goal Greads the evidence the way the inspector does — the round's arms against its leader, its best against the previous round's best and against the baseline it started from, its spend againstbudget_usd— and suggests the next decision, each as the command that makes it:evh round-retire,evh rank-stability,evh round-close --conclusion ... --promote <run>(which promotes the baseline with the conclusion as its reason). A closed round refuses new runs; validate (V14) checks the records;leaderboardandfamily-testtake--round. The goal page shows the rounds, newest first, with the open round's suggestions. The catalog stays read-only: decisions are commands. A project that never opens a round sees none of it. - A goal page leads with its answer. First the promoted baseline on each of its datasets and
why (or "no decision yet", with the command that makes one), then what the goal is for, then on
each dataset its references and its arms in groups by what they are scored against: gold,
silver, a judge. Arms are ranked only within a group, and a group's leader is never an arm graded
against its own outputs. A dataset page shows its baseline and reason, and the baseline's row is
tagged.
evh goals --goalandevh datasets --dataset-idsay the same. - Everything the catalog and the inspector show is a command.
evh datasets(one dataset: its versions, references and arms),evh references(one: who wrote it, from which run, what is scored against it),evh metrics,evh goals(one: its arms per dataset) andevh inspect --runs …(the inspector's verdicts, conditions,--where/--byon fields,--itemfor one item's outputs beside the reference). Same adapter, same computation: the numbers are the pages'. Each is also amaketarget. - The catalog says what every number is measured against. A metrics page (what each metric
measures, which way is better, where it is recorded, which goals decide on it) and a page per
reference (who wrote it, from which run, every arm scored against it); every metric and
reference on the pages links there. Each arm's row says what it is scored against, and rows
scored against silver, by a judge, or against the reference the arm wrote itself are tinted; a
judged arm reads "judge
", no longer "gold". A goal page flags the arm that wrote the reference it is scored against, so it is never the leader. The inspector's charts section is "analysis", and its verdict rows carry the same tints. Actions are compact and tinted. - Long tables page. Every catalog table and the inspector's per-item table show 10 rows, with "show 10 more" and "show all"; the filter and the sort work over every row, and a link to a row opens the table at it. The verdict tables stay whole: their Holm thresholds are read together.
- Dataset versions keep their lineage.
evh dataset-create --dataset-id x_v2 --from x_v1records where a version came from and the item ids it adds, removes and changes (derived_fromin the dataset file);validateV2 fails a version whose parent is gone. The catalog links versions both ways with what changed, and a version no arm has run on yet gets a page when it is linked to one that has. - The inspector says whether the compared runs ran under the same conditions, from their fingerprints: the machine, library versions, the harness, the scorer (adapter code), the items and the reference's bytes. One line when all match; otherwise the fields that differ, arm by arm, with a warning when the scorer or the reference bytes differ (different measurements) or latency is compared across machines. A dataset page names each arm's machine when its arms ran on more than one.
METRIC_DESCRIPTIONSin an adapter, one sentence per metric, is recorded on every run (metric_descriptionsin metrics.json) and refreshed byevh rescore; the catalog reads the adapter's current descriptions over the recorded ones, so describing a metric later needs no new run. The harness describes its own metrics (cost, tokens, latency, judge scores).- A run becomes a reference:
evh reference-create --from-run <run_id>(make reference-create RUN=<run_id>). The usual way to get references is to run the expensive model as an arm, read its results, and score the cheaper arms against what it wrote. Until now that took a second paid pass (--config), whose outputs could differ from the run you read, and the reference did not say which run it came from.--from-runcopies a finished run's outputs and calls nothing; the manifest records the run, its config, fingerprint, build and cost. It refuses whatrun-promoterefuses (a dirty tree, an unknown build), an unfinished, derived or other-dataset run, and--tier gold(gold is what a person wrote or checked). With--not-redistributedonly the manifest is committed: it records the command that rebuilds the texts from the run, and that command, on a checkout with the manifest alone, writes them again and checks them against its checksums without touching it. reference-createrefuses an empty output, and outputs that stopped atmax_tokens, by either path (--allow-cut-offfor outputs that end at the cap by design). A frontier arm atmax_tokens: 80wrote 19 of 20 summaries cut off mid-sentence and one empty reply, and they were frozen as the reference with no word.- The leaderboard marks the arm that wrote the reference it is scored against (
arm *, and a note): that row is graded against its own outputs. reference-createsays when gold exists (new runs keep scoring against gold; how to rescore against the new silver), and to commit the reference before the next run: an uncommitted reference makes every later run dirty.- A newcomer scenario for every release (SCENARIO): a fresh project, the published docs only, a summariser and a classifier with small CPU models, cheap hosted LLMs, a frontier reference and a judge, browsed in the catalog, under $1. RELEASING runs it before the merge.
-
A goal weighs cost, quality and speed, and recommends (ADR-0006). Cost (USD per item, tokens beside it) and speed (latency p50 and p95) are built in;
success_criteria.axesadds limits, another measure,role: ignore, or the project's own per-item metric in one line. Astrategysays who is tied with the quality leader (balanced, the default: not separated by the paired test and within a margin, 2% of the leader unless the goal sets one;quality_first;frugal) andtie_break(default cost, then speed) picks among them. Limits come first; flagged and retired arms are never recommended, nor is an arm measured on fewer items than the rest of its group; identical scores are always tied; a missing number is "not recorded", and an axis a tied arm does not record cannot break the tie; speed is not compared across machines the runs recorded. Some groups show their leader and never recommend: a dataset markeddecides: false, a silver group beside a gold one, and a group measured by unvalidated judges ("Shown, not recommended", with the reason). The helpers are public asevaluation_harness.axes. The goal page shows the decision with its reasons, an axis column per arm and quality against each axis as charts; the home page's glance a recommended column with cost and speed against the baseline; the dataset and arm pages cost and speed; the inspector's scatter cost and tokens.evh goals --goalandevh overviewprint the same. Guardrails are shown on the goal page and in the CLI. When the recommendation is not the promoted baseline, the goal page andevh goals --goalsay by how much and whether the test separates them; with no eligible arm, they count the reasons. The goals page andevh goalscount configs filed and show each goal's recommendation. Cost and speed metrics are their own kinds, with the goals that weigh them, and costs print to six places; chart names no longer overlap. -
Reference manifests and V16. Every reference carries a
manifest.jsonwith the checksum of each item's text (schemas/reference.schema.json);evh reference-manifestwrites it for gold a script builds,reference-createfor the references it makes. It is committed even when the texts are not redistributed:evh validate(V16) checks a rebuilt copy against it, and the catalog lists such a reference as fetched. The manifest names the dataset (dataset_id), so a second gold for one dataset can have a name of its own (reference-manifest --dataset-id, e.g. gold/ag_news_200_adj1 for ag_news_200). This repository's gold references now have one. round-retire --runs-dirretires one copy of a config (its runs in another runs directory, such as a re-timing on another machine) and leaves the arm in the project's runs directory in play.evaluation_harness.stats. Standard library only, and the one place the paired test lives:exact_pandsign_flip_p;holm, with a declared family larger than the p-values passed;exact_ci, an interval for the mean difference that agrees with the test: none at 5 items or fewer, where no shift can be rejected;compare, every arm against a baseline with verdicts in words;compare_by, the same test within each group of items, Holm-corrected over every group and arm together;ranks,pearsonandspearman(average ranks for ties; a constant input returnsconstant, NaN unless you pass another), which the judging, rank-stability and report-check scripts share instead of keeping their own copies.
It has its own test suite, scripts/test_stats.py, run by make test.
- The exact count is ~4x faster (0.03 s at 30 items, was 0.11 s): the sign-pattern sums are
built by doubling a list instead of enumerating each pattern. The same sums, so the same p.
The exact interval bisects to ~1e-9 of its span, not ~1e-14: 1.4 s at 30 items, was 11 s.
It is shown to 3 decimals.
- evh catalog and evh catalog-serve (make catalog, make catalog-serve).
- The catalog: static pages over a project's runs, by dataset, built into
.build/catalog/. Each dataset page lists every arm with its headline number, and links
to the inspector for one arm, or for all the arms on one metric scored against one
reference.
- The run inspector: a server on 127.0.0.1 that tests any arms on one dataset against
a baseline with stats.py, so it gives the same p as pair-test on the same pair. It
shows the verdicts, charts of the per-item scores and differences, and, for one item,
each arm's output beside the reference.
- The same rules as everywhere else:
- Arms on different dataset_ids are refused.
- On a metric scored against a reference, an arm scored against another reference than
the baseline's (silver against gold, say) is named and not tested against it; cost,
latency, tokens and length compare across references.
- The static pages carry counts and means only, never an item's text.
- The server answers only to a 127.0.0.1 Host. It serves only the catalog and the
data tree, and no dot-folder in either.
- An adapter is the only project code needed. catalog_adapter.Adapter lists the
questions both tools ask. The built-in HarnessAdapter reads the harness's own layout
(runs*/, runs-judged/, references/): an arm is one config on one dataset, its
repeats averaged per item. A project with another layout names its own adapter in
config.yaml (catalog: {adapter: path/to/file.py:Class}) or passes --adapter.
-
A public mode (
evh catalog-serve --public HOST), for a deployment behind a reverse proxy that serves the static pages:- it answers only the named host;
- each client gets
--raterequests a minute; - at most
--max-armsarms in one comparison; - two computations at once, every answer cached;
- errors never show the server's internals;
--warmcomputes, at start, every comparison the built catalog links to.
evh catalog --link LABEL=URLadds the deployment's other sites to the nav. - CSP-clean: no inline script or style. The theme script is a file, and the legend colours are set from JS.
Tests: tests/test_catalog.py (54 assertions), run by make test.
- Goals: what an evaluation is for (data/goals/<goal_id>.yaml, schemas/goal.schema.json).
- What a goal says: who uses the output, the decision it informs, the measure that
decides (and which direction is better), the output formats it accepts, its datasets
and their roles, and optionally min_items, the fewest items a winner may rest on.
- Configs name their goal (goal:) and may declare the format they ask for.
- experiment-run refuses an unknown goal before it runs.
- The run records both, outside the fingerprint: they change what a run answers, not
what it measured.
- run-promote carries both onto the baseline.
- validate V14 fails:
- a goal that breaks its schema, or whose goal_id is not its file name;
- a config, run or baseline naming a goal that does not exist, or a format the goal
does not accept.
A dataset smaller than a goal's `min_items` gets a note.
- Optional until a project opts in. Set
required_from: YYYY-MM-DDindata/goals/policy.yaml, and every run created from then on must name a goal. A project upgrading changes nothing until it sets the date. - In the catalog: a goals page, and a page per goal. On each of the goal's datasets
it ranks the arms that record the goal's measure. An arm under
min_itemsor in a format the goal does not accept is flagged and never the leader. Each arm links to the inspector, tested against the leader. - This repository's six experiments have goals (
data/goals/), and so does the tutorial (tutorial/goals/tickets_summary.yaml). No date is set here, so nothing already committed is held to one.
Tests: tests/test_goals.py (14 assertions), run by make test.
- Item fields (data/fields/<fieldset_id>.yaml, schemas/fieldset.schema.json, ADR-0003).
The harness defines no field: a project declares its own in a field set, and any number of
its datasets name it.
- Types: text, number, integer, boolean, date, duration (seconds; h:mm:ss accepted),
url, and choice with its values; a field can be required and public.
- dataset-create --fieldset ID --fields FILE attaches values from a CSV or JSONL, one
row per item_id, and refuses an undeclared column, a mistyped value, a value outside a
choice, an unknown item and an empty required field. --update-fields corrects values on
an existing dataset.
- Not the identity. A run's fingerprint hashes item ids and bytes only, so a corrected
field invalidates no run; the dataset records fields_sha256 beside them.
- validate V15 fails a field set that breaks its schema, fields with no field set or an
unknown one, an undeclared or mistyped value, a missing required value, and a
fields_sha256 that does not match (a hand edit).
- In the catalog: each field with its coverage, and a summary of its values only when it
is public. In the inspector: an item's fields beside its outputs; a filter on any
field; a breakdown by a choice or yes/no field, tested per group with stats.compare_by.
A public deployment answers with public fields only.
- The adapter protocol gains an optional item_fields(dataset).
- Items fetched from a pinned URL. dataset-create --manifest FILE takes one row per item:
item_id, source_url and source_sha256 (empty: fetched and pinned now), and field
columns. dataset-materialize downloads each into .cache/items/ and refuses bytes that
hash to anything else, as it refuses a changed file. A transient failure is retried like a
provider call; a host that needs a header gets it from EVAL_FETCH_HEADERS (that host only,
never on a redirect elsewhere, never written into a dataset). With --update-fields, an
empty cell clears a value.
Tests: tests/test_fields.py (47 assertions), run by make test; the catalog's in
test_catalog.py.
- evh init [DIR] [--tutorial] starts a project: data/sources, data/configs,
data/goals, data/fields and a .gitignore for what the harness derives. --tutorial
copies the GUIDE's kit (tickets, their fields, gold, arms, a goal, a judge scorer and its
prompt, the judge panel's config) into tutorial/; the kit ships inside the package. It never overwrites a file.
- make guide-check (part of make check) runs docs/GUIDE.md in a fresh project, as a
reader would, and fails when a quoted output no longer matches what the command prints.
Lines that the unseeded lead2_noisy arm moves are marked in the GUIDE and not compared.
- The tests that run evh install the real wheel (tools/_wheel.py), built from
pyproject.toml, instead of copying folders. A file the package data leaves out now fails
them, where a copy would have included it.
- make browser-check drives the catalog and the inspector in headless Chromium: a
tutorial project with fields, the main paths and the field controls, the phone-width layout,
and no console error. A development extra (requirements-browser.txt, make
browser-install), never a dependency of the package; CI runs it as its own job. The root
make browser-check also drives the public site, served with production's headers.
- tests/test_installed_commands.py (in make test) runs every evh command in a
project that pins the package; a command without a case fails it.
- tests/check_commands.py (in make test) checks every make target, evh command
and flag shown in the current docs still exists: 274 of them across 27 docs today.
- The deploy's health check also asks the inspector the question readers send most, a
group of arms on one metric (infra/deploy/probe_link.sh), and rolls back without verdict
rows.
- Code-quality gates, make quality at the repository root and a CI job of the same name:
ruff (lint and format), mypy, bandit, pip-audit, vulture, interrogate, codespell, shellcheck
and actionlint, with radon's complexity report. Tools pinned in requirements-dev.txt
(make quality-install), never a dependency of the package. What they found, and how ruff
compares with the black, isort and flake8 that podcast_scraper runs:
research/internal/QUALITY_TOOLING_EXPERIMENT.md. Every public function, class and method
has a docstring (interrogate at 100%, the bar podcast_scraper sets). Logic kept in two or
more copies (Spearman, Holm, the sign-flip test, module loading, the test scripts' check(),
cost sums, file hashing) now lives in one place each, and every report claim re-derives
unchanged; a test script that runs no check now fails.
- This repository's worked examples run on patched libraries (Linux): transformers 5.20.2,
sentence-transformers 6.2.0, pillow 13.0.0, aiohttp 3.14.5, nltk 3.10.4, certifi 2026.10.19,
clearing 58 published advisories in their locks; few-nerd stays on transformers 4.55.4,
because span-marker has no transformers 5 release. make audit-locks audits every lock per
platform. Every local arm was re-run against the committed record: every published finding
holds, and all but four arms reproduce item for item (why, per arm, in the record).
Not part of the package.
Changed¶
- A config with a key written into its params is refused (
api_key,access_token,secret, ...): it would be copied into every run record and onto the arm page. Name the variable withapi_key_envinstead; the catalog shows any such param as "(hidden)". success_criteria.secondaryis removed: its entries areaxes(UPGRADING); a goal that still has it fails V14.- Documentation never moves a fingerprint (fingerprint version 4). Code is hashed by what it
does: its syntax tree without docstrings, comments, formatting or
METRIC_DESCRIPTIONS(assigned, or imported from a shared module). That holds for the adapter, the harness's measuring modules, judge_score's own hash and the examples'code_digest. A judge scorer YAML is hashed by its parsed content, and a reference manifest'scaveatis outside the reference digest. Adding metric descriptions to an adapter used to make run-compare and the inspector call every earlier run "not the same conditions". The tree is serialised without empty fields, so one file hashes the same on Python 3.11, 3.12 and 3.14 (ast.dumpdoes not). v3 and v4 digests are not comparable. The committed v3 runs here were migrated: each recorded hash was matched to the code or reference state it came from (git history, unreachable objects included) and recomputed. No score changed. - import-run hashes the scoring code itself:
--scorer-filereplaces--scorer-sha256. Give the file (or several, repeated in a fixed order) and the harness hashes it by the same rule, so a docstring in your scorer is not a new scorer. The hash is now the run'sinstrument.adapter.sha256, inside the fingerprint hash: before, it sat only inimported, outside the hash, so two imports scored by different versions of the code looked like the same conditions. - One install calls hosted models.
openaiandanthropicare core dependencies; the[llm]extra is gone. A newcomer's first hosted arm failed on the import, and only one RUNBOOK section named the extra. - The core times every call.
latency_msis recorded for any adapter that does not report its own (the attempt that answered, not the retries' backoff); only the bundled adapter used to time itself, so a project's adapter recorded no speed. This can move a result: a project's runs gain a latency column, and the leaderboard its cost/speed frontier. - experiment-run warns when outputs stop at
max_tokens, by the provider'sfinish_reason(the bundled OpenAI-compatible and Anthropic providers now record it) or tokens_out reaching the cap, naming the items: they are scored as if complete. - A run that stops mid-pass says what to do: which run, after how many items, the exact
--resumecommand, or delete it; V7's failure says the same. - validate V5 no longer fails two different arms that are both perfect. On a saturated binary metric their per-item scores agree too; different outputs now tell two measurements from a copy.
- The inspector's item view shows the item itself (what every arm read), folded when long; the
local inspector only.
evh inspect --itemprints it. - Bookkeeping fields (
resumed, the judge'sself_family_*) stay in the record but are listed as metrics nowhere (leaderboard, runs-list, the spread, the inspector). - The leaderboard's version note fires only for one arm at two versions, not for different arms at different versions (judge-score's too); "P(top 1)" is no longer printed beside "P(1st)" when they are the same; the rounding note names the scorer as what rounds.
evh init's.gitignoreignores__pycache__/; experiment-run ends "wrote N run(s) to …".- Exact p-values at 30 items or fewer.
pair-testandfamily-testnow count the two-sided sign-flip p over all 2^n sign patterns when a pair shares 30 items or fewer, instead of estimating it from 20,000 seeded permutations. Above 30 items the estimate is unchanged, bit for bit, includingfamily-test's single stream across opponents. Near a threshold the estimate could land on either side of it; the count cannot. This can move a result: - in this repository's reports: none moved. All 200 report claims re-derive unchanged, because every published pair and family test is on more than 30 items;
- in the tutorial (6 items):
pair-testreads 0.0312 (was 0.0330), and the family test 0.0938 againstlead1and 0.3125 againstauthor(lead2_noisy's is unseeded and moves with every run). The verdicts are the same (getting started step 7, tutorial 3 step 4); - in your project: re-run
pair-test/family-teston any pair with 30 or fewer shared items. Whether a verdict near its threshold moves depends on your data. pair-testsays which it did:(exact, all 2^n sign patterns)or(20000 permutations).family-test's cache key includesstats.py, so a cached answer from 0.3.0 is recomputed.validateV4 no longer passes what it did not check. A materialized copy that has lost an item file now fails; it used to be skipped. A dataset with no materialized copy on the machine (materialized data is not committed, so a CI job has none) is named as not checked, and the ok line says how many were checked, e.g. "3 of 14 datasets materialized here". A project whose materialized copy is incomplete goes red where it was green.- The bundled adapter names its primary metric (
PRIMARY_METRIC = "overlap_f1"). New runs on it recordprimary_metric: overlap_f1. The leaderboard already ranked them by it, first by name; it no longer prints that the metric was "chosen by sort order", and the catalog and the inspector open on it. Runs made before keep what they recorded. docs/GUIDE.mdis getting started, in your own project: install the package,evh init --tutorial, then the loop withevh(a dataset, arms, a reference from a run, a comparison, a choice). It used to run inside a checkout of this repository, withmake, and to hold every step. The rest is in five tutorials (docs/tutorials/): a local model against a baseline, a hosted LLM with your own key (which variable each provider reads), a frontier model's run as the reference with gold and judging, experiment rounds and dataset versions, and the catalog and the inspector with the command behind each view.make guide-checkruns getting started and the three offline tutorials, each from where getting started ends. The README and the RUNBOOK lead withevh.- The default metric is the one your runs recorded.
pair-test,family-test,holdoutandrank-stabilitydefaulted tocoverage, the summarisation example's metric: in any project that does not score it they found no runs, or crashed. Without--metricthey now read the primary metric the runs on the dataset recorded, and refuse, naming the recorded metrics, when none did. evh self-testin a project checks the installed harness: the paired test's own suite, then the tutorial end to end in a scratch folder (dataset with fields, runs, pair-test, rescore, validate, catalog). It used to run this repository's development suite, which checks this repository's reports and failed 52 checks in every project; in a checkout it still does.- A metric's direction is inferred, not remembered by a flag, by one rule for every command
and the inspector: a flag, else the goal the runs name, else the adapter's
METRIC_DIRECTIONS(new, recorded on the run) orimport-run --asc(a field nothing read until now), else the name (cost, latency and token metrics go down); V14 fails a goal that contradicts its runs. So lower is better without--ascforpair-test's "A wins",family-test's wins and "ahead", the leaderboard's order and significance block,holdoutandrank-stability.leaderboard --sort latency_msranked the slowest arm first. p-values do not change (the tests are two-sided);--ascand--descoverride. This can move a wins or "ahead" count, and an order, on a lower-is-better metric; no published claim is on one (200/200 re-derive). validatechecks the project's own run directories. V7, V8 and V9 read this repository's six directory names, so a project'sdata/runs-adjudicatedwas never checked; V10's scorer sources were this repository's example paths. They are project policy in<data>/validate.json(committed,floors,scorer_sources,duplicate_ok); without it every git-trackedruns*directory is checked (validate_floors.jsonis gone).- The package holds the harness, not this repository's tests. The wheel shipped ~14,500
lines of tests and report checkers; they are in
harness/tests/andharness/tools/in the checkout.self-test's own suite (test_stats.py) still ships. A run'scode_sha256hashes the code that can move a number (the run-writing commands and what they import, read from the source), so editing a test no longer shows inrun-compareas a confound; 0.4.0 runs record a different value from 0.3.0 runs. The deploy installs the inspector from the wheel too. - Refusals and arguments. Every command refuses with
ERROR: …on stderr and exit 1 (four printed to stdout); a command that takes no arguments refuses one with exit 2 (validate,self-testand five checkers ignored them). RUNBOOK: exit codes and every environment variable. - The bundled adapter refuses an unknown
provider:(a typo fell back toechoand recorded the echoed input as the model's output), and records what an OpenAI-compatible provider billed when the response carries it. One price rule for it and the judges. - One run record, one format; a derived run says what it was derived from.
rescore,judge-scoreand the SciFact re-parse writederived: {kind, from, at, with}, their owncreated_at, andsource_created_at.rescored_from,rescored_with,judged_from,judged_with,judge_label,reparsed_*andprimary_metric_ascare gone, and nothing reads them: a 0.3.0 derived run is read as a measured one. Every field the harness writes is declared inmetrics.schema.json, and V1 fails a run record with any other (0.3.0'simport-run --ascwroteprimary_metric_asc, which nothing read; 0.4.0 writesmetric_directions, which every command reads).EVAL_GOALS_DIRreads goals from outside the data root;latency_sis a speed metric, lower is better, aslatency_msis. A goal may name no dataset (one checked by a gate) and may setdirection: non_inferior, a guard that ranks nothing (no significant drop against a baseline).rescoreno longer records a metric it kept as dropped (rescore_dropped_metricsnamed whichever key a loop ended on). --resume-unverifiedis gone.experiment-run --resumerefuses rows written before 0.4.0 (no_extra); experiment-run writes_extraon every row, so its own rows resume.- The integration seam is
adapter.pyat the project root. An arm whose config names noadapter:uses it, recorded asadapter.py, and the bundled adapter only when there is none. The docs lead with the pin andevh initrather than a copiedharness/. - Importing the package changes nothing.
.envis read when a harness command starts, not on import, and the project's paths resolve on first use, so an adapter's tests or a notebook can setEVAL_HOMEafter the import.evaluation_harness.catalog_adapterreads no path at import, and itsload_adapterisload_catalog_adapter. - Configs, judge scorers, judging sets and verdicts have schemas, and
validateV1 checks them. A config key other thanconfig_id,dataset_id,params,adapter,goalandformatfails (a mistyped key was read by nothing and passed). - A baseline carries what its number means:
run-promoterecords the run's reference tier, adapter and fingerprint hash,runs-list --baselinesprints the tier and adapter, and V6 fails a baseline that no longer matches its run. A 0.3.0 baseline fails V6 until it is promoted again. Promoting replaces the baseline: the copy archived todata/baselines/superseded/and thesupersedesfield are gone, and git keeps the old one. - A command run from a project's subfolder finds the project: without
EVAL_HOME, the root is the nearest directory up with adata/folder (it was the current directory, soevh validatein a subfolder read an empty project). - The judge panel is a command,
evh judge(it sat beside the package and could not run from a pin); the tutorial kit shipstutorial/judge_panel.yaml.
Fixed¶
- V9 fails when git cannot list the committed runs. A
git ls-filesthat refused (a committed directory behind a link that leaves the repository) listed nothing, and V9 read that as nothing missing: a run with no rows passed. It now fails and quotes git's error; a project that is not a git repository has nothing committed, and V9 says it was not checked. silver-calibrateno longer prints a ceiling it did not measure. With one set of runs serving as both references and arms, it correlated a ranking with itself and printed "rho = +1.000 … from two different sets of runs". The ceiling now counts only arms whose two sides are different runs, and says "not measured" when fewer than three are.- From an external review of the core and the UI surfaces (0.4.0 candidate):
- A run scored against references for only some items says so. experiment-run took a
reference directory as covering the dataset, so gold for 20 of 200 items gave a 20-item mean
recorded as a 200-item gold run. It now counts coverage before any call. With no file for any
item it refuses. With some, it warns, records
reference_coverageon the run (schema), and the leaderboard names the run. This can move a result: a run that was refused now stops. - A pair opened from a goal page's group is held to the group's family. "test vs leader"
passes
family=<group size − 1>, so it is not tested as if it were the only pair. The inspector refuses afamilysmaller than the arms tested, and athresholdabove alpha or below what the family allows, instead of obeying a URL. - rescore and judge-score keep a project adapter's call-time extras (named in the row's
_extra) andresumed; they were dropped and reported as metrics the scorer dropped. - The adapter contract:
- INTEGRATION gives
lower_is_better(metric, dataset), as the code calls it; catalog_adapterre-exportsload_fieldsetandfieldset_problems, so an adapter needs no private_fields;- two new optional members:
arm_of(dataset, config_id)(rounds,inspect --round; it followsEVAL_RUNS_DIR) andreference_free(metric)(from the metric's kinds, not its name); - the structured view kinds (
insights,nodes,entities) are documented; evh roundsreads the project's catalog adapter;- the inspector's default baseline is the promoted baseline's arm.
- INTEGRATION gives
- A public inspector publishes text only when told.
catalog-serve --publicserved every arm's output and the reference text for any item, while INTEGRATION and the adapter contract said that text never leaves the machine. It now serves numbers, adds outputs or reference text only for what--publish outputs,referencesnames, and never serves the item itself; the item view says what it withholds. This can change a deployment: one that relied on the old behaviour adds--publish. This repository's own publishes outputs (already public in the repository), not references. - The catalog opens on the whole project, and every name is a link.
- Home is a dashboard: counts (goals, datasets, configs, arms, references, metrics, runs,
USD billed, tokens), each goal's answer at a glance (its baseline, the leader of the gold
group and the paired test between them), activity and spend side by side (spend in USD or
in tokens, so a free local model's use is tracked too), what each dataset covers, and
everything recorded; each for the last 14 days of activity or all time.
evh overviewprints the same. - New pages: Datasets (what the home page listed) and References in the menu; a page per
arm (its metrics, runs, config without credentials, conditions, the same config scored other
ways, its rounds;
evh arms --arm) and per metric (what it measures and how, which way is better, its kind, the goals it decides, every arm recording it with a strip chart;evh metrics --metric). - Links everywhere: arms, metrics, datasets, references and goals link to their pages from every page and table, a reference's author to its arm, a round's configs to theirs; in the inspector, arm names and the metric open their pages.
- Metric explanations: the harness explains its own metrics at length; a project's
METRIC_DESCRIPTIONSvalue may be{"summary": ..., "details": ...}for its metric pages. - The catalog adapter gains optional
arm_runs,arm_configandspend. - From the second newcomer run: the inspector warns when the baseline wrote the reference the arms are scored against; a judged arm's item shows each judge's score beside the output; the goal page says what "filed" means.
- Home is a dashboard: counts (goals, datasets, configs, arms, references, metrics, runs,
USD billed, tokens), each goal's answer at a glance (its baseline, the leader of the gold
group and the paired test between them), activity and spend side by side (spend in USD or
in tokens, so a free local model's use is tracked too), what each dataset covers, and
everything recorded; each for the last 14 days of activity or all time.
- One provider layer for the systems under test and the judges, keys by role. The bundled
adapter, judge-score's judges and the judge panel call hosted models through one module,
_providers:openai(OpenAI or any compatible endpoint),anthropic,openrouterandlitellm, each takingmodel,base_url,api_key_env,temperatureandmax_tokens.- A judge goes where its
provider:says. Judges ignored it and went to a LiteLLM proxy ifLITELLM_BASE_URLwas set, else to OpenRouter with an alias looked up inmodels.json, while the record said the declared provider. The scorer schema now requiresprovideron every judge. This can change a setup: re-running the committed scorers (provider: litellm) needs a LiteLLM proxy. - Judging can have its own keys. A judge reads every variable as
EVAL_JUDGE_<NAME>first, then<NAME>, so it runs on separate keys, budgets and spend logs, or shares the experiments' with no setting.evh env-checkshows what each role reads. - A key is not identity.
api_key_envis left out of a run's fingerprint and a judge's; no committed hash moved (all 2,800 committed verdicts keep theirjudge_fp). - judge-score names each judge's route and key, dry run included, and stops before any call when a key or address is missing.
- The judge panel's judges are declared like judge-score's (
id,provider,model, prices). It called only Anthropic, priced Sonnet in code, and ran the fake judge whenANTHROPIC_API_KEYwas missing; the fake judge now runs only with--use-fake-judge. - Anthropic takes
base_urlandapi_key_envlike the others. Arms passreasoning,reasoning_effortandprovider_routingto the request as judges did, and recordreasoning_tokens(descriptive, as every committed run declares it), the model and the host that answered.
- A judge goes where its
- The harness imports itself as a package. Its modules imported each other as bare
top-level names (
import stats) with their own directory onsys.path, so a project's ownstats.pyfirst on the path replaced the harness's paired test insideevaluation_harness.judging. Underevh, the harness'sadapter,statsorroundscould also hide a project's module of that name. They now importevaluation_harness.*, and nothing of the harness goes onsys.path. In a checkoutharness/evaluation_harnesslinks toscripts/, andpython scripts/x.pystill runs. A project script that relied on the harness's directory being on the path writesfrom evaluation_harness.adapter import Result, as INTEGRATION documents. - The public server trusts no client header unless told (
--client-header NAME); a spoofedCF-Connecting-IPno longer buys a fresh rate bucket. The deployment names Cloudflare's header (origin lock). - The rest of the page features have commands:
evh inspect --no-rescore(the inspector's box), its "Average only" table, and the dataset page's fields block inevh datasets --dataset-id. - Statistics, consistently:
family-testmarks an estimated p (~) when a family mixes exact and estimated rows;- pair-test separates at
p <= threshold, as Holm and the inspector do; - the bootstrap ties ranks with the same tolerance as the within-item ranks;
- runs that disagree on a direction or a primary metric are named;
- rescore refreshes
metric_directions; - the inspector and
evh inspectshow each row's 95% interval of Δ (exact up to 30 items, a t interval above, as its docstring now says).
- Accessibility: a sortable column's header works from the keyboard and states
aria-sort; every chart mark with a tooltip carries an SVG<title>. - Messages: rescore's missing-reference message no longer sends a project to
examples/; RFC-002 tells_extra(adapter extras) from_meta(provider metadata). - From the second newcomer run (
/work/evh-scenario-040; SCENARIO, 0.4.0 candidate): - A baseline promoted from one of an arm's repeats records every repeat and their mean
under
repeats, andrun-promoteprints the two side by side. The catalog, the inspector and the tests read the mean;scoresstays the run's own (V6). - A judged run keeps its source's cost and speed kinds, so the judged leaderboard has its
frontier. That leaderboard also prints the judging pass's agreement and its position and
noise checks, from
summary.json. - The arm that wrote a silver reference is left out of the ordering test and the frontier, and the leaderboard says so (its row measures agreement with itself).
- A judge reply that is not committed (not bare JSON, or repeating the source) keeps its
text in a git-ignored
.withheld/beside its record, and the log names the file. An unparsed reply's cause can then be read; the record keeps only the hash (V12). - The dataset page and
evh datasetssort arms by what they are scored against (gold, silver, a judge, nothing) before the mean. A silver and a gold mean no longer interleave. - The goal page and
evh goals --goalshow a judge's group, read on its own score.evh goals --goalalso lists the rounds and marks retired arms. - One p format everywhere (
stats.fmt_p: four decimals,<0.0001below that).family-testsays whether p is exact or a seeded estimate. Above 30 items it draws one random stream for the family, sopair-testand the inspector, which estimate one pair at a time, can differ in the third decimal. Its wins column is labelledA wins. - Round messages:
round-open --keepsays carried arms need no change and no new run;evh roundsdoes not suggest promoting the run that is already the baseline;leaderboard --rounddoes not warn about two versions of an arm in a round that compares them.
evh cost-reportsees the bundled adapter's bills. A provider's bill (OpenRouter, or a LiteLLM proxy passing it through) was the run's cost but was not kept where cost-report reads it, so every run read as "price table only". It is now kept in the item's_meta.usage.cost.evh inspect --itemprints which run each output is from and, for a repeated arm, that it is the latest of N repeats beside their mean (as the page does).evh datasetscounts configs.- Smaller items:
experiment-runprints what the command spent, and its repeat spread calls cost and speed what they are, not "deterministic";judge-scoresays its spend is the provider's bill where one is reported, else the price table;silver-calibrateexplains rho and sibling lift;- the catalog counts the configs behind its arms;
- "filed" reads "filed under this goal";
- the item view says it shows the latest of N repeats beside their mean;
- judged arms keep their version in the inspector;
- a heading scrolled to is not hidden under the header;
env-checknames the example arms only in a checkout;- the tutorial scorer points at tutorial 3.
- One reference per table and per arm. The leaderboard averaged an arm's runs whatever they
were scored against: a silver 0.90 next to a gold 0.24, in one table labelled silver. It now
prints one table per reference, gold first, each with its own tests. The catalog, the
inspector and
evh roundsread an arm against the reference of its latest run (bycreated_at), and they name the earlier runs they leave out. A judged run is grouped by its judge, not by its source run's reference.evh roundsreads a round against the reference most of its arms use and names any arm scored against another. This repository's data, the trial and the podcast project have no such arm. provider: openrouteruses OpenRouter. It readOPENAI_API_KEYand called OpenAI's address unless the arm set bothbase_urlandapi_key_env. It now readsOPENROUTER_API_KEYand callshttps://openrouter.ai/api/v1, and an arm's own values still win.evh inspect --roundreads the round asevh roundsdoes: on the goal's measure, against the round's leader. It defaulted to the first arm, so an arm with no score became the baseline and the metric fell back tocost_usd. Arms with no score are named and left out.- The bundled adapter stores
overlap_f1as computed. It rounded the value to 6 decimals, which made ties that are not there (the leaderboard warned about its own example). This can move a result only where two arms tied at the sixth decimal. - Hints and messages:
- with no reference,
experiment-runnamesreference-create --from-runfirst; evh --helpdescribesreference-createas "from a run or with a config";env-checkand thelitellmprovider no longer tell a pinned project to copy a.env.exampleit does not have;run-promoteno longer printsresumedandself_family_*among the scores (the baseline file still keeps them);- inspector notes are no longer all labelled "Re-scoring";
- the bundled adapter and
dataset-create --helpdescribe what the code does now, not its history. - A judge scorer must state its position and noise checks. ADR-0001 makes them part of the
pass, not optional, but a scorer that left
protocol.position_check_fractionornoise_check_fractionout ran with them off. Both are now required. The noise check must be above 0, and so must the position check whenever a call sees more than one output (a pointwise scorer has no order to check).judge-scoreand V1 refuse a scorer that breaks this. - A round is read against the arm that wrote its silver reference.
evh roundsand the goal page now compare the leader with the previous round's best, the baseline and, new, the frontier arm whose outputs became a silver reference of the dataset. Cost per item is shown for each of those arms. That arm is the yardstick, so it is never suggested for retirement. - The inspector reads only arms and references the catalog lists. It asked the
adapter's
arms_ofabout any path a request named; an adapter that maps a path to itself (the podcast repo's did) letcompareread outside the data tree (runs=../../.git). A request may name an arm, or a path that selects arms (a run directory), but only arms the adapter'sarms()reach, and a referencereferences()lists. - A catalog nav entry may carry a CSS class as a fourth element (
hide-smhides it on a phone), and the inspector page names the command that serves it (serve). evh rescoreandevh silver-calibratein a project that pins the package. A run made with noadapter:uses the bundled adapter and records it asscripts/adapter.py, a path that exists only in a checkout, so both failed with "adapter not found" in every pinned project on the default adapter (0.2.0 and 0.3.0 alike). They now score with the package's own copy, the one that made the run, through one shared lookup. Found by following the GUIDE in a brand-new project.- The "next, run this" hints name a command you have. Every one said
make …, a target only a checkout of this repository has; throughevhthey now sayevh …(the first was GUIDE step 1's "Next: make dataset-materialize"). Two were wrong in a checkout too: an empty baselines list suggestedmake promote(the target isrun-promote), and an empty runs list suggestedmake demo, which a project without the bundled sample data cannot run. evh catalog-serveon a port another program holds says so and suggests--port, instead of a Python traceback.rescoreonto another reference no longer claims the references changed. Moving runs from silver to gold (--reference-id) compared the gold files' hash with the silver files' and, as they always differ, wrotereferences_changed_since_measurement: trueinto every re-scored run and printed "the reference files have CHANGED". The move is recorded as before (reference_id_at_measurement); the flag and the warning are kept for the same reference's files changing. No committed run in this repository carries the false flag.- Commands that assumed a checkout, each found by running every
evhcommand in a project that pins the package: rank-stabilitycrashed (set.intersection()) instead of saying no arm had the metric; on a lower-is-better metric it named the worst arm the leader; with too few items for two halves it now says so.--exclude-dataset(pair-test,family-test,holdout,bootstrap-test) readdata/sources/<id>/*.txtinstead of the dataset file: wrong for any item that is not a .txt, any dataset from a manifest, any project keeping sources elsewhere, and "nothing excluded" was then reported as true.- The leaderboard's metric kinds,
run-compare's, and the classification report's label parser looked for a run's adapter in the checkout only; they now find the bundled one in the package, asrescoredoes. bootstrap-teston a gold set of free text (summaries) treated every summary as a class and would have reported a macro-F1; it now refuses, and its "too few items" refusal names the threshold and the opponents.catalog-serveprinted its address without flushing, so under a log file or a process supervisor the line did not appear;--build-diroutside the project was a traceback, now a refusal;self-test --helpran the whole suite instead of printing help.- The catalog counted a dataset's items from the harness's own path, not the adapter's,
so a project with its own layout got "the most items any arm scored". It now asks the
adapter (
dataset_items, optional). - The bundled adapter's
anthropicprovider failed every call with the anthropic SDK 1.x, which dropped thetemperature=keyword: a TypeError before the request was sent. The temperature now travels in the request body, so the request is the one every SDK version sends. A self-test checks the call against the installed SDK's own signature. Found by mypy. - A manifest URL could read a local file.
dataset-create --manifestfetched any schemeurlopenaccepts, so afile:///…row was copied into the item cache and pinned, although a dataset's schema allows only http(s). Every fetch now refuses other schemes before reading anything. Found by bandit. - The judge panel judged placeholder text. Every finalist, real runs included, was scored
on "Synthetic item i for
". It now reads each finalist's stored outputs, finds runs where every command does ( EVAL_RUNS_DIR, the project), ranks by their primary metric, and refuses a lower-is-better one rather than promote the worst. rescorecould record the bundled adapter's metric kinds in a project's run: two registries named loaded adapters differently, and the lookup fell back to the bundled one. One loader (_adapter.py), keyed by the resolved file.import evaluation_harness.judgingfailed in an installed package (No module named 'stats'), and importingcatalog_adapterbeside it loaded_commontwice.holdout's significance block had no direction, so on a lower-is-better metric it ranked the worst arm first.- Checks that could not fail, or did not run:
self-test's cost-cap control turned any failure into a skip;check_terminologywas failing and nothing ran it (it is inmake testnow);make -C harness docswas a no-op (eight targets were not.PHONY).
0.3.0 — 2026-10-21¶
Re-measure what you already have. Stored outputs can now be scored again by LLM judges, as an
optional pass that never touches a committed run: evh judge-score, for any text task you
declare (what an item is, what the outputs are, the dimensions, the scale). The harness refuses
a judge until the scorer names, for each judge, a committed run that measured it against human
ratings; it runs at least two judges from different vendors blind, measures their position bias
and noise, and stores every verdict so a rerun costs $0. The tutorial has an offline, $0
judging step (GUIDE step 10). Upgrading from 0.2.0 changes no committed run or score and adds
no dependency. Design: ADR-0001;
what it found here: the judging page.
Added¶
evh judge-score(andmake judge-score). Re-measure an experiment's frozen outputs with a validated LLM judge, billed, cost-capped and resumable. It runs from the installed package in a project that has only its own data directory, scorer, prompt and runs: the prompt builder, reply parser, tau-b and Spearman, and an OpenAI-compatible judge client (LiteLLM proxy, OpenRouter direct, per-judgebase_url/api_key_env, billed cost read from the response, retries) are inevaluation_harness.judging, with no dependency onexamples/.- The judge scorer artifact (
data/scorers/<id>.yaml): two or more judges from different vendor families, the task, the prompt (with its sha256), the protocol, avalidationblock and a cost cap. - Any text task, declared in the scorer. An optional
taskblock names what each item is (source_label), what the arms wrote (output_label), thedimensionsthe judge scores and thescale; a prompt writes{n}where the number of outputs per call goes. Without ataskblock a scorer judges SummEval's news-summary task (ARTICLE, SUMMARIES, four dimensions, 1-5), byte for byte as before. A declared task is hashed into the judging set. - A judging step in the tutorial (
docs/GUIDE.mdstep 10):harness/tutorial/scorers/tickets-judge-v1.yamlre-measures the tutorial's arms with two fake judges, offline and $0, through the refusal, the dry run, the pass and the leaderboard. - A validation gate. A scorer runs only if its
validationblock names a committed run for each judge whose metric equals the stated value (and is at leastmin_metric, when the scorer sets one) and whose model, temperature, reasoning, provider, max tokens, prompt file and prompt sha256 are the judge's. The gate checks that a validation exists and is current, not that it transfers to your outputs. Otherwise--unvalidatedruns it and labels every number "unvalidated judge".provider: fakejudges (offline, free, for testing a scorer) can never pass the gate. - Judging sets and stored verdicts. One item is the article plus every arm's output, arm
identities hidden, order shuffled by a seed; the set is hashed into its id and frozen in
data/judging-sets/. Every reply is stored per call indata/judge-verdicts/, with its scores, so a rerun resumes and re-reading costs $0; a reply that is not bare JSON scores, or repeats 8 consecutive words of the item or an output, is kept only as its hash, since a judge that reasons aloud can copy the source. A different arm set is a new set, never an append. A block retried with--retry-unparsedkeeps the earlier calls' cost in its record. - Controls. A position check (a second shuffle that re-forms the blocks) and a noise check
(a repeat on a subset), plus per-judge agreement and a self-family flag, in
summary.json. - Adjudicated gold re-measurement (repository tooling,
examples/_shared/adjudicate.py, not in the package): a committed adjudication file overwrites only the disputed items of a local gold reference set to make a new reference version, andrescorere-scores every run against it for $0, the old reference kept beside it. - Judge validation on published human ratings (repository tooling, not in the package):
validate_judge_goyal.py(Goyal, Li & Durrett 2022) andvalidate_judge_zhang.py(Zhang et al. 2024, crowd ratings of 19 LLM-era summarisers) check a scorer's judges against people. The ratings are downloaded when the check runs, pinned by sha256, and never committed; the committed records hold article ids, the judges' replies and scores, and hashes; a reply that is not bare JSON scores, or repeats 8 consecutive words of the source, is kept only as its hash. validateV11-V13. V11: every judging set's stored hash is the hash of its content. V12: every stored verdict names a real set, item, pass and block, and itsnis that block's size, and every committed judge reply (verdicts and validation records) is bare JSON or kept only as its hash. V13: every judge scorer loads, is structurally valid (itstaskincluded), and its prompt file resolves and renders. They read the configured data directory.judge-scorejudges every arm on the dataset by default. A scorer'sarms_match,--matchand--armsnarrow it, and it warns when the arm set mixes config versions (_v1and_v2of one arm are judged as two arms).- A scorer's
prompt.fileresolves against the project root, then the scorer file's own directory, then (in a checkout of this repository only) the repository root. - Judged runs carry no
reference_tier, since a judge reads no reference: the source run's tier is kept asjudged_with.source_reference_tier, andleaderboardsays the scores come from a judge, with its label. - A refused judge call stops the pass cleanly. A call refused, or still failing after its retries (a provider's own budget guardrail answering 403, for example), stops the pass the way the cost cap does: every stored verdict is kept, the partial result is projected and labelled, and the same command resumes.
- Scorer checks.
task.scalemust be whole numbers, and a prompt with both{n}and SummEval's fixed count phrases is refused (the phrases would keep saying 16). V13 renders each prompt at 2 outputs, so a prompt that cannot be rendered fails there, not at run time. - Experiment-level results this enables are reported in the repository's reports (the summarisation experiment re-measured with two pairs of SummEval-validated judges, not confirmed by people on those outputs; AG News and DBpedia against adjudicated gold); none of them is in the package.
Changed¶
leaderboard's precision note no longer fires on values that are exact small-integer fractions (a mean of 1-5 ratings over two judges and four dimensions is in eighths); scores rounded on the way to disk still get it.- Nothing that scores a run changed: committed runs and their recorded hashes are untouched, and the committed summarisation judging set rebuilds from its stored verdicts with the same id.
0.2.0 — 2026-10-20¶
The first version meant to be installed rather than copied: pin a release tag, run evh, and
your project's data stays in your project, never in the installed package. Runs now record
which harness produced them, other tools' scores can be imported as runs, and the paired tests
handle lower-is-better metrics.
Added¶
- Installable package.
pip install "evaluation-harness @ git+https://github.com/chipi/eval-harness@v0.2.0#subdirectory=harness"gives theevhcommand: every Makefile target, running the same scripts. EVAL_HOME/EVAL_DATA_DIR. The project's data lives in the project, never in the installed package; the data root can be nested (for exampledata/eval/stats).- The harness identifies itself in every run.
fingerprint.instrument.harnessgainsversion,code_sha256andinstalled. evh import-run. Per-item scores produced by another tool become a run the significance tests read, with the source, the scorer and the sha256 of the imported bytes on the record.--ascforpair-testandfamily-test. For lower-is-better metrics, "wins" and "ahead" count the lower score. The p-values were already two-sided and do not change.
Changed¶
validate's V9 evidence floors are read from<data>/validate_floors.json(project policy) instead of being hardcoded for this repository's run directories.- The
family-testcache lives under the project root, not beside the code.
0.1.0 — 2026-10-19¶
The harness as first published with its six worked experiments, before it was packaged: a copy
you take into your project rather than install. Frozen, hash-checked datasets; one adapter
function for your system; runs that record their data, prompt, code, model and build; each
arm's own noise; paired tests with Holm correction that say separated or tied; billed cost
beside quality; re-scoring from stored outputs for $0; validation that fails CI; baselines
promoted only with a reason. Tagged afterwards, at the commit the site was deployed from on
2026-10-19 (9454f19), so a project that took a copy that week can name what it took. What
changed between this copy and 0.2.0, including fixes that can move results:
UPGRADING.