Eval instrument v2 β design
Date: 2026-07-02 Β· Status: approved (brainstorm with Alex) Context: ADR-035 Β· Phase-0 noise characterization Β· PRs #3752 (merged), #3757 (open, this stacks on it)
Problemβ
Phase-0 characterized the domain-eval harness and left four structural weaknesses that patching (K-vote) mitigates but doesn't fix:
- The judge grades the raw agent JSON envelope (
{reply, confidence, β¦}), not the reply a customer would read β a suspected contributor to the "intrinsic" noise floor we measured. - Our doctrine is "trust paired deltas," but there is no tool that computes them β back-tests are eyeballed aggregates.
- The judge's holistic
correctnessfloat is the least reliable unit of LLM judgment; per-item binary checks are the reliable unit, but a pure item-fraction KPI would be too coarse (2-item rubrics β {0, Β½, 1}). - 10 of 16 "ecommerce" cases actually test role-agnostic behavior (clarify-vs-proceed, no-fabrication, escalation) β conflating policy with domain knowledge and bloating every future role file.
Plus one open review finding on #3757: with transient judge failures, majority is computed over surviving votes, so one noisy vote can fail a case ("reduced quorum becomes majority").
Scopeβ
| Step | What | Lands in |
|---|---|---|
| 0 | Quorum guard | push to #3757 (open review response) |
| A | Judge the extracted reply, not the envelope; file the eco-010/016 bug | PR-1 |
| C | Judge protocol v2: item-level binary gates + graded alignment KPI (hybrid) | PR-1 |
| B | compare.py β paired-delta back-test tool | PR-2 |
| D | Policy/role suite split | PR-2 |
| V | Validation: re-baseline 3Γ K=5 on both roles + policy suite | after PR-1+2 merge |
| Docs | Post-validation docs & guides finalization | small docs PR after V |
Grouping rationale: PR-1 contains everything that changes score semantics β one re-baseline. PR-2 is tooling/content with no semantic impact; compare.py consumes the v2 report schema so it follows PR-1.
Non-goals (deferred): adaptive K-vote (cost optimization β matters at 50+ cases); linkage retrospective (judge scores vs editRatio β separate project, still the deepest untested assumption); KB-slot prompt fidelity (mirror production KB formatting β belongs to the ADR-035 sandbox work); RAGAS integration.
Designβ
0. Quorum guard (#3757)β
More than half of the configured _JUDGE_VOTES must return a valid dict, else the case becomes an explicit error (error="judge quorum not met (1/3)"). Run continues (fail-open preserved). Prevents a single surviving vote from acting as a majority.
A. Envelope extractionβ
Before judging: try json.loads(candidate); if the result is a dict with a "reply" key, the judge receives str(reply). The raw envelope is kept in CaseResult.candidate for diagnostics. Any parse failure β judge the raw string (today's behavior, fail-open).
Bug filing: GitHub issue for the over-clarification behavior (eco-010/eco-016 fail 12/12 runs at 0.0β0.5 correctness; agent interrogates fully-specified briefs; suspect clarify-first policy prompt; evidence = noise-characterization doc).
C. Judge protocol v2 (hybrid β approved with the design)β
One judge call per vote returns structured item-level output:
{
"items": {"offer to check the order": true, "explain processing": false},
"violations": {"a fabricated tracking number": false},
"alignment": 0.85,
"rationale": "one sentence"
}
K-vote aggregation (over surviving votes, quorum enforced):
must_includeitem covered iff a majority of votes saystrue;must_not_includeviolated iff majority saystrue.alignmentβ median β becomescorrectness(the continuous KPI; K-averaging stays smooth).- verdict = all items covered β§ no violations β§ median alignment β₯
_PASS_THRESHOLD.
Report schema is a superset of v1: missing_required / forbidden_present keep their names (derived from item majorities); a new items block carries the per-item detail. Cost unchanged β same call count, bigger response.
Why hybrid: binary per-item questions are the reliable unit of LLM judgment; the graded alignment float preserves the averaging-friendly continuous KPI Phase-0 proved out. Pass/fail authority moves entirely onto the reliable binary gates.
Consequence: v2 correctness (alignment) is not comparable with Phase-0 numbers β re-baseline required (step V); old numbers marked non-comparable in the findings doc.
B. compare.pyβ
python -m evals.domain.compare baseline.json candidate.json [--role R] [--fail-on-regression]
- Pairs cases by
(role, id); intersection only; warns on set mismatch. - Per-case Ξcorrectness table; counts improved/regressed/tied.
- Bootstrap 95% CI on the mean paired Ξ (10k resamples, fixed seed, stdlib-only).
- Gate-flips (passβfail / failβpass) listed; a new rubric-violation failure is a hard blocker regardless of the mean.
- Verdict per ADR-035 Β§6:
IMPROVED/REGRESSED/INCONCLUSIVE(CI vs 0). - Reads v1 reports too (no
itemsfield β v1 mode), so historical artifacts stay mutually comparable. - Output: markdown to stdout;
--fail-on-regressionβ exit 1 onREGRESSEDor gate regression (CI-gateable later).
D. Policy/role suite splitβ
golden/policy_behaviors.jsonwith"suite": "policy": the role-agnostic behavior cases move here βeco-007β¦016βpol-001β¦010, plusmkt-002(proceed-on-complete-brief).- Role files keep domain cases only:
ecommerce_ops.jsonβeco-001β¦006;marketing_content.jsonβmkt-001(compliance) +mkt-003(grounded). - Runner expands the policy suite against every role discovered in
golden/(a behavior is a property each persona must satisfy; catches role-prompt regressions), reported aspolicy@<role>.--rolesflag restricts for cheap runs. Decision: all-roles default (approved with the design). - Runbook gains a "where does my case go?" rule: behavior β policy suite; facts/red-lines β role file. Analyst sessions (e.g. Veena/campaign) then only author facts + red lines.
V. Validationβ
After PR-1 and PR-2 merge: 3 Γ domain-eval runs at DOMAIN_EVAL_JUDGE_VOTES=5 covering both roles + policy suite. Success = spread of run-level mean correctness β€ Phase-0's v1 K-vote band (0.031) and no case erroring on quorum. These numbers become the v2 baseline.
Docs & guides finalization (post-validation β explicit user requirement)β
Ships as a small docs PR once V's numbers exist:
- noise-characterization doc: "protocol v2 baseline" section with the new numbers; v1 tables marked non-comparable (holistic-judge era).
- harness README: v2 judge schema, policy-suite semantics,
compare.pyusage, updated env-knob table. - golden-set runbook: "where does my case go?" decision rule; item-writing guidance strengthened (each item must be independently judgeable yes/no); compare-based back-test workflow (baseline β change β compare β verdict).
- Non-number-dependent doc edits ship inside PR-1/PR-2 themselves; only the baseline-numbers section waits for V.
Testing (all tokenless, mocked)β
- Envelope extraction: JSON-with-reply / plain prose / malformed JSON.
- v2 aggregation: item majorities, violation majorities, median alignment, quorum failure β case error, even-K ties (split item flag β majority).
compare.py: synthetic report fixtures β IMPROVED / REGRESSED / INCONCLUSIVE / gate-flip blocker / v1-schema tolerance / disjoint case sets.- Runner policy expansion: policy suite Γ discovered roles;
--rolesfilter. - Existing mocked tests migrated to the v2 judge return shape.
Error handlingβ
- Malformed judge JSON in one vote β vote dropped, counts against quorum.
- All votes failed β case error, run continues (fail-open).
- Compare with disjoint sets β warn + intersect; empty intersection β exit with message.
- Policy suite with zero role files β clear error.
Sequencing & branchesβ
- Now: quorum guard β
feat/domain-eval-context-grounding(#3757). - PR-1
feat/domain-eval-judge-v2(this branch; stacked on #3757, rebases onto dev when #3757 merges): A + C + tests + non-number doc edits. - PR-2
feat/domain-eval-compare-policy(stacked on PR-1): B + D + tests + non-number doc edits. - V via
workflow_dispatchΓ3; docs PR with v2 baseline.