feat(judges): add reference equivalence scoring#358
Conversation
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — 401c90f7
This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: drewstone_author · 2026-07-13T08:14:48Z
tangletools
left a comment
There was a problem hiding this comment.
🟡 Value Audit — sound-with-nits
| Verdict | sound-with-nits |
| Concerns | 3 (3 weak-concern) |
| Heuristic | 0.0s |
| Duplication | 0.0s |
| Interrogation | 744.6s (2 bridge agents) |
| Total | 744.6s |
💰 Value — sound-with-nits
Adds a standalone, transport-injected judge that scores a free-text agent output against a free-text reference answer — fills a real gap (no existing judge does free-text answer equivalence) and reuses the right primitives; ship.
- What it does: A new
runReferenceEquivalenceJudge(input, {chat, model, signal})makes one ChatClient call, parses a strict{score, rationale}JSON object, validates score∈[0,1], checks finishReason==='stop', and returns{kind:'reference-equivalence', version, score, rationale, usage, costUsd, model, durationMs}. It hard-fails withJudgeParseError(and propagates transport errors) rather than emitting a s - Goals it achieves: (1) Give products a correct way to compare free-text expected answers with agent outputs — the PR body notes the current alternatives (ad-hoc prompts, or misusing the concept-coverage judge) lose semantic equivalence, contradiction detection, usage, and cost. (2) Isolate untrusted input: the three text fields travel as a JSON data object in the user message, never interpolated into the system inst
- Assessment: Sound. It fills a genuine gap: every existing LLM judge judges a BUILT ARTIFACT (source files / served HTML) — intent-match (src/intent-match-judge.ts:30-40), semantic-concept (src/semantic-concept-judge.ts:69-81), keyword-coverage (src/keyword-coverage-judge.ts:25-38). None compares free-text candidate-vs-reference answers. Transport choice (ChatClient) follows the codebase's stated forward direc
- Better / existing approach: Considered whether llmJudge (src/llm-judge.ts) is a better base — it is NOT. llmJudge is a factory that returns a campaign-shaped JudgeConfig whose score() yields {dimensions, composite, notes}; the new judge is a standalone one-shot returning {kind, version, score, rationale, usage, costUsd, model, durationMs}, matching the runIntentMatchJudge/runSemanticConceptJudge family for direct-call consum
- Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 2
- Bridge warning: opencode/kimi-for-coding/k2p7: bridge stream ended without value-audit content
🎯 Usefulness — sound-with-nits
A coherent, specialized reference-answer semantic-equivalence judge filling a real gap, built on the codebase's preferred ChatClient seam with strong adversarial error/injection handling; only nit is discoverability.
- Integration: Exported from both public entry points (src/index.ts:1163, src/contract/index.ts:138) and verified by an explicit test (reference-equivalence-judge.test.ts:236). No in-repo caller exists yet, but the consumption paths are concrete and established: direct call by product agents, an analyst adapter analogous to createSemanticConceptJudgeAdapter (src/analyst/adapters.ts:281), or alongside llmJudge. T
- Fit with existing patterns: Fits the grain. Uses ChatClient, which chat-client.ts:1-20 designates as the preferred seam for new LLM-calling code ('New analyst code uses ChatClient'), matching llmJudge (llm-judge.ts:27). The standalone result shape (kind/version/score/durationMs/costUsd) mirrors runSemanticConceptJudge and runIntentMatchJudge. No existing equivalent found — completion-verifier scores artifact presence (a diff
- Real-world viability: Holds up well past the happy path. Tested adversarially for prompt-injection isolation (untrusted input kept as JSON data, kept out of system instructions — reference-equivalence-judge.test.ts:84-130), malformed/truncated JSON rejection (:144-188), out-of-range score rejection (:171-188), non-'stop' finishReason rejection (:177-188), abort signal pass-through (:208-235), network-error propagation
- Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 1
💰 Value Audit
🟡 index.ts export has no section header and sits adjacent to the unrelated 'Reference replay' block [maintenance] ``
In src/contract/index.ts:129 the export gets its own '// ── Reference-answer judge ──' header, but in src/index.ts:1156-1164 it is dumped with no header immediately before '// ── Reference replay ──'. A reader scanning the root barrel could conflate reference-equivalence (answer scoring) with reference-replay (run replay/steering). Trivial: add a one-line section comment matching the contract barrel. Does not gate shipping.
🟡 Standalone judges are now split across two LLM transports [maintenance] ``
runIntentMatchJudge/runSemanticConceptJudge take LlmClientOptions+callLlmJson (src/intent-match-judge.ts:26, src/semantic-concept-judge.ts:22) and soft-fail to available:false; the new judge and llmJudge (src/llm-judge.ts:27,41) take injected ChatClient and hard-fail. This is the codebase's acknowledged forward direction (chat-client.ts:16-19), not against the grain, but it does leave two judge families with different error semantics (soft available:false vs thrown JudgeParseError) until the old
🎯 Usefulness Audit
🟡 New judge not added to the agent-eval SKILL.md judge menu, so skill-driven adopters won't discover it [ergonomics] ``
The /agent-eval skill is the canonical discovery surface: .claude/skills/agent-eval/SKILL.md:103 lists runSemanticConceptJudge + runKeywordCoverageJudge as the 'did the artifact implement the concepts?' judges. runReferenceEquivalenceJudge is correctly exported from src/index.ts:1163 and src/contract/index.ts:138, but is absent from the SKILL.md judge table, so LLM agents building integrations via the skill will not find it when choosing a free-text-answer comparison judge — the exact use case i
What this audit checks
It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.
| Pass | What it asks |
|---|---|
| Heuristic | Vague title? Whitespace-only or cruft-bearing diff? (content signals only) |
| Duplication | Do added function/class names already exist elsewhere in the repo? |
| Value Audit | What does it do? What goal does it achieve? Is it good? Better architecture or already-exists? |
| Usefulness Audit | Does it integrate and fit? Will it hold up in real use and actually get used? |
Findings are concerns, not blocks — the human reviewer decides what to do with them.
✅ No Blockers —
|
tangletools
left a comment
There was a problem hiding this comment.
✅ Approved — 6 non-blocking findings — 401c90f7
Full multi-shot audit completed 4/4 planned shots over 4 changed files. Global verifier still owns final merge decision.
Full immutable report for this review: trace
Summary comment for this run: full summary
tangletools · 2026-07-13T09:49:54Z · immutable trace
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — 7a58c62e
This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: drewstone_author · 2026-07-13T10:11:18Z
tangletools
left a comment
There was a problem hiding this comment.
🟡 Value Audit — sound-with-nits
| Verdict | sound-with-nits |
| Concerns | 1 (1 weak-concern) |
| Heuristic | 0.0s |
| Duplication | 0.0s |
| Interrogation | 248.3s (2 bridge agents) |
| Total | 248.3s |
💰 Value — sound
Adds a canonical semantic reference-answer equivalence judge composed over the existing llmJudge bridge, plus a correctly-scoped upgrade that makes paid judge calls visible in campaign cost accounting — no duplication, follows the grain.
- What it does: Adds two exports: createReferenceEquivalenceJudge (a campaign-native JudgeConfig factory) and runReferenceEquivalenceJudge (a direct-call product adapter). Both compose the existing llmJudge bridge (src/llm-judge.ts) with a single 'equivalence' dimension, a Zod strict response schema, transport-injected ChatClient, input bounding (8k/32k/32k), and prompt-injection isolation (untrusted values ride
- Goals it achieves: (1) Give products a canonical expected-answer equivalence judge so they stop rolling ad-hoc prompts or misusing concept-coverage judges for free-text comparison. (2) Make paid judge calls first-class cost citizens in campaigns — previously judge cost was invisible to totalCostUsd, costCeiling, and the aggregates dump, which is a real accounting bug for any campaign with LLM judges. (3) Make strict
- Assessment: Strong, coherent, in-grain. The reference judge composes llmJudge instead of reimplementing the call+parse loop (src/llm-judge.ts:85), so the new module is ~150 lines of prompt + schema + bounding, not a parallel transport. The responseSchema option is generic — any future strict-schema judge gets it free. The cost-accounting refactor is independently correct: totalCellCost (src/campaign/run-campa
- Better / existing approach: none — this is the right approach. Verified the codebase has no existing reference-answer equivalence judge: intent-match-judge.ts scores holistic app-correctness (source+HTML, soft-fail), semantic-concept-judge.ts scores per-concept presence in built artifacts, agreement-judge.ts is a pure no-LLM injected comparator for distillation, keyword-coverage-judge.ts is deterministic substring matching.
- Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 2
- Bridge warning: opencode/kimi-for-coding/k2p7: bridge stream ended without value-audit content
🎯 Usefulness — sound-with-nits
A well-integrated reference-answer equivalence judge built on the existing llmJudge/ChatClient seam, filling a real gap with cohesive supporting improvements to cost accounting, structured output, and cancellation.
- Integration: Fully reachable. Exported from src/index.ts, src/contract/index.ts, and src/campaign/index.ts. Plugs into runCampaign as a JudgeConfig (createReferenceEquivalenceJudge) and has a direct product-call adapter (runReferenceEquivalenceJudge). Signal, cost, cache, and trace paths all wired. As a substrate package, its job is to export for consumers — no internal caller is expected, and none is needed.
- Fit with existing patterns: Builds directly on llmJudge (llm-judge.ts:85) + ChatClient, the documented transport-agnostic seam. The supporting changes (responseSchema option, LlmCallMetadata on JudgeScore/JudgeParseError, campaign totalCellCost/failedJudgeCall, enforceDispatchUsage relocation) are cohesive and reusable — any future llmJudge-based judge inherits structured output, cost tracking, and fail-loud parsing for free
- Real-world viability: Holds up across error paths. Prompt injection: system prompt explicitly marks userRequest/expectedAnswer/candidateOutput as untrusted data; test confirms adversarial values stay in the user message. Truncation: parseResponse (llm-judge.ts:238) rejects non-stop finishReason. Parse failure: JudgeParseError carries llmCall so the paid call is still accounted (campaign stores it in failedJudgeCall). C
- Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 1
🎯 Usefulness Audit
🟡 Contract subpath exports the judge but not createChatClient [ergonomics] ``
src/contract/index.ts exports runReferenceEquivalenceJudge and createReferenceEquivalenceJudge, whose ReferenceEquivalenceJudgeOptions requires a ChatClient — but createChatClient is only exported from the root entry (src/index.ts:62) and src/analyst/index.ts, NOT from /contract. The contract header (contract/index.ts:80-85) tells consumers that reaching for any non-contract subpath is 'using internals.' A contract-only consumer therefore cannot construct the required transport without leaving t
What this audit checks
It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.
| Pass | What it asks |
|---|---|
| Heuristic | Vague title? Whitespace-only or cruft-bearing diff? (content signals only) |
| Duplication | Do added function/class names already exist elsewhere in the repo? |
| Value Audit | What does it do? What goal does it achieve? Is it good? Better architecture or already-exists? |
| Usefulness Audit | Does it integrate and fit? Will it hold up in real use and actually get used? |
Findings are concerns, not blocks — the human reviewer decides what to do with them.
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — 92889e76
This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: drewstone_author · 2026-07-13T10:23:19Z
tangletools
left a comment
There was a problem hiding this comment.
🟡 Value Audit — sound-with-nits
| Verdict | sound-with-nits |
| Concerns | 1 (1 weak-concern) |
| Heuristic | 0.0s |
| Duplication | 0.0s |
| Interrogation | 527.8s (2 bridge agents) |
| Total | 527.8s |
💰 Value — sound-with-nits
Adds a campaign-native reference-answer equivalence judge built on the existing llmJudge bridge, plus threads cost/usage metadata through the campaign so paid judge calls are never silently dropped — coherent and in-grain.
- What it does: Adds one new judge,
createReferenceEquivalenceJudge/runReferenceEquivalenceJudge(src/reference-equivalence-judge.ts:65-134), that scores a free-text candidate against an expected answer by semantic meaning on a 0–1 scale, with untrusted-input isolation, strict zod response schema, input-size bounds, cancellation, and a versioned result carrying usage/cost/model/duration. It is built as a th - Goals it achieves: (1) Give products a transport-injected, campaign-native judge for expected-answer scoring so they stop comparing free-text with ad-hoc prompts or misusing concept-coverage judges. (2) Make judge cost/usage fully visible in campaign accounting — no paid judge call (including ones that fail to parse) is silently dropped, and the stub-detection guard no longer misfires on judge tokens. Both are visib
- Assessment: Sound and in the codebase's grain. The judge reuses
llmJudge(the documented single-call bridge, src/llm-judge.ts:85) rather than reimplementing the LLM call, injects transport viaChatClient(the substrate's transport-agnostic seam), fails loud (JudgeParseError+failedJudgeCall, matching the repo's no-silent-zero / no-fake-zero doctrine in CLAUDE.md), and exports from root +/contract - Better / existing approach: none — this is the right approach. Considered the three existing purpose-built judges (semantic-concept-judge.ts, intent-match-judge.ts, keyword-coverage-judge.ts); none does reference-answer equivalence, so no reuse opportunity was missed. The reference judge is correctly layered on
llmJudgerather than standalonecallLlmJson, which is the higher-reuse path. One maintenance note (not a redire - Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 2
- Bridge warning: opencode/kimi-for-coding/k2p7: bridge stream ended without value-audit content
🎯 Usefulness — sound
A new reference-equivalence judge fills a real gap (free-text semantic equivalence, not covered by any existing judge) by composing the established llmJudge + injected ChatClient pattern, with correct cost-accounting fixes for judge spend that previously leaked out of campaign totals.
- Integration: Properly exported from three entry points — src/index.ts:1163, src/contract/index.ts:142, src/campaign/index.ts:44 — with the companion createChatClient export added to contract (src/contract/index.ts:131) so product callers can build the transport the judge requires. The judge returns a standard JudgeConfig (src/reference-equivalence-judge.ts:67) so it slots directly into runCampaign({ judges: [.
- Fit with existing patterns: Composes llmJudge (src/llm-judge.ts:85) — the canonical single-call judge factory — rather than reinventing it, and adds a generic responseSchema option (src/llm-judge.ts:65) to llmJudge that any future judge benefits from. Uses the injected ChatClient seam rather than the older LlmClientOptions shape used by intent-match-judge.ts:55 and semantic-concept-judge.ts:113; this divergence is intentiona
- Real-world viability: Handles the realistic load: untrusted-input isolation via JSON-data user message + explicit anti-injection system prompt (src/reference-equivalence-judge.ts:51-62), per-field length bounds enforced before the paid call (boundedField, line 136), provider json_schema→json_object degrade tested at reference-equivalence-judge.test.ts:163, AbortSignal propagation to the underlying fetch verified at tes
- Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 1
💰 Value Audit
🟡 Third judge-construction pattern introduced without consolidating [maintenance] ``
src/reference-equivalence-judge.ts composes
llmJudgeand exposes arunX/createXdirect adapter, a third shape alongside standalone-callLlmJson+soft-fail (src/semantic-concept-judge.ts:235, src/intent-match-judge.ts:132) and rawllmJudge(src/llm-judge.ts:85). It's the best of the three and the right choice here, butsemantic-conceptandintent-matchcould now be expressed asllmJudgeconfigs to collapse toward one pattern. Not a gate — flagging for a future consolidation, not thi
What this audit checks
It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.
| Pass | What it asks |
|---|---|
| Heuristic | Vague title? Whitespace-only or cruft-bearing diff? (content signals only) |
| Duplication | Do added function/class names already exist elsewhere in the repo? |
| Value Audit | What does it do? What goal does it achieve? Is it good? Better architecture or already-exists? |
| Usefulness Audit | Does it integrate and fit? Will it hold up in real use and actually get used? |
Findings are concerns, not blocks — the human reviewer decides what to do with them.
✅ No Blockers —
|
tangletools
left a comment
There was a problem hiding this comment.
✅ Approved — 16 non-blocking findings — 92889e76
Full multi-shot audit completed 8/8 planned shots over 11 changed files. Global verifier still owns final merge decision.
Full immutable report for this review: trace
Summary comment for this run: full summary
tangletools · 2026-07-13T11:13:22Z · immutable trace
Problem
Products compare free-text expected answers with ad hoc prompts or misuse concept-coverage judges. That loses semantic equivalence, contradiction detection, usage, and cost.
Change
Add one transport-injected
runReferenceEquivalenceJudgewith structured output, untrusted-input isolation, strict parsing, cancellation, usage, cost, model, and duration. Export it from the root and contract entry points.Proof
pnpm test: 278 files, 2,947 passed, 2 skippedpnpm typecheck: passedpnpm lint: passed with four existing warningspnpm build: passedpnpm verify:package: passedorigin/main: clean