README.md
Evals
Human-graded evals for ARGUS answers. No LLM judge anywhere in this pipeline. A person reads each run and assigns the verdict. Start here before running anything that spends money.
Corpus format
corpus.yaml is a YAML list. One item per case:
| 1 | - id: radar-01 |
| 2 | bucket: radar-answerable |
| 3 | query: "How does dense passage retrieval score query-document pairs?" |
| 4 | tools: [radar_search] |
| 5 | expect: {min_evidence: 2, verdict: supported, must_cite: true} |
| 6 | notes: "Why this case exists." |
expect.verdict records the honest answer shape for the case. It never
constrains the pipeline. Any terminal state can pass when the board shows
zero fabrication.
Buckets
20 cases, 5 per bucket.
radar-answerable. CS-paper index staples. Retrieval scoring, RAG metrics, semantic routing, hybrid search, chunk overlap. These have stable answers.rag-answerable. Questions about the ARGUS repo itself. Architecture, cache TTL, rate limiting, board truncation caps, connector pins.needs-web. Facts current as of Sep 2026. Vendor pricing, fast-moving docs, release notes. Mock runs answer from canned evidence. Only a live run with web access can fully ground these.unanswerable. Future facts, private data, open problems, secrets, unrecorded events. The only honest output is refusal with zero claims.
Workflows
Three scripts, three cost profiles. Pick the cheapest one that answers your question.
Mock first, always. It spends nothing by construction. No HTTP client, no provider SDK, canned boards built in-process:
| 1 | venv/Scripts/python.exe scripts/eval_mock.py |
| 2 | venv/Scripts/python.exe scripts/eval_mock.py --only radar-01 rag-01 |
Live only when you mean it. eval_live.py POSTs to a running server, polls
each investigation to a terminal state, and appends one JSONL row per case
to runs/<ts>.jsonl. It refuses to run unless unlocked, because live runs
spend provider tokens:
| 1 | venv/Scripts/python.exe scripts/eval_live.py --dry-run |
| 2 | venv/Scripts/python.exe scripts/eval_live.py --live --max-runs 3 |
Dry run prints the planned POST count and the pessimistic spend ceiling,
then exits without touching the server. The ceiling is per-run estimate
times planned runs, and the run aborts when the ceiling tops
--spend-cap-usd (default $2.00).
Grade last, by hand. eval_grade.py walks the ungraded rows of a run,
shows query plus synthesis text plus board counts, and prompts for a
verdict on stdin:
| 1 | venv/Scripts/python.exe scripts/eval_grade.py grade --run runs/20260908T120000Z.jsonl |
| 2 | venv/Scripts/python.exe scripts/eval_grade.py grade --run runs/x.jsonl --only radar-01 |
Judgments append to runs/<ts>.grades.jsonl, one object per line with
case_id, investigation_id, verdict, notes, graded_at. Reruns skip
rows that already have a grade, so an interrupted session resumes where it
stopped. The script prints the verdict histogram at the end. For scripted
use there is a non-interactive mode:
| 1 | venv/Scripts/python.exe scripts/eval_grade.py grade --run runs/x.jsonl \ |
| 2 | --non-interactive --verdict supported |
Diff runs in CI. eval_diff.py compares a candidate run against
evals/baselines/v0.6.0.json on aggregates only. It never string-matches
report text. Three gates: no verdict drops, grounding drop within 5pp, p90
final-report latency within +25%.
Rubric
Read the row, then grade what the system actually produced, not what the corpus expected. Four verdicts:
supported. Every claim is grounded. Each claim cites at least one evidence id, every cited id sits on the board, and no source is invented. This is the only passing verdict for answerable cases with claims.partial. At least half the claims are grounded. Use it for answers that mix solid cited work with ungrounded filler, and for needs-web cases where canned evidence only goes so far.unsupported. Invented sources, or mostly ungrounded claims. A confident answer with dangling citations lands here.refused-correctly. The run produced no fabrication and said so. Terminal state can be anything, complete included. The board is empty, or carries onlyproposedclaims that cite nothing, and the synthesis states the limits instead of answering. Any invented reference vetoes this verdict.
Claim-level rule of thumb: a claim needs at least one cited evidence id present on the board to count as grounded. Partial means half or more of the claims clear that bar.
Verdict semantics (DEC-054)
Verdicts never constrain terminal state. A budget_exhausted run with an
empty board and an honest synthesis is refused-correctly and passes. A
complete run with fabricated citations is unsupported and fails. The
validators check fabrication, not outcomes. Human grades record answer
shape for tracking, not pipeline requirements.
No-live-spend rules
- Mock runs are the default. They cannot spend quota.
- Live runs need
--liveorARGUS_EVAL_LIVE=1, plus a server with keys. - Check
--dry-runoutput before any live invocation. - Never add provider calls to the mock or grading paths.
eval_grade.pyreads local files only.
Where truth lives (v0.6 judgment)
Offline truth stays in JSONL. Run rows in runs/<ts>.jsonl, human grades
in runs/<ts>.grades.jsonl, baselines versioned under evals/baselines/.
The Grafana dashboard deliberately has no live verdict join. Per-question
Prometheus labels would blow up cardinality, and a grading script cannot
increment server counters, so the dashboard shows a text panel describing
this workflow plus an activity panel over existing low-cardinality metrics
(tool calls, loop stops, cost). Verdicts are read from the grades files,
not from Prometheus. If P6 needs live verdicts in dashboards, the honest
route is a pushgateway or an authed ingest endpoint, not per-case labels.