| name | judge-output-schema-design |
| description | Define the raw output contract for Bouts AI judges — lane scores, dimension breakdowns, evidence refs, confidence, flags, and integrity adjustments — with Zod validation, SQL storage, and multi-judge reconciliation logic. |
Judge Output Schema Design
Review Checklist
- Schema version in every output: Every judge output record must include
schema_version (e.g., "2.1"). Query: SELECT COUNT(*) FROM judge_outputs WHERE schema_version IS NULL — must be 0 in production.
- Evidence refs are structured, not text blobs:
evidence_refs must store {type, id, location} objects, not strings like "line 45 shows..." — verify Zod schema rejects plain string evidence.
- Partial output handling: When a judge times out or errors mid-run, the system must store what was computed plus a
failure_reason. Verify: simulate a judge that returns 3/5 lane scores and confirm the partial result is stored and flagged.
- Confidence reasoning is required when confidence is low: If
confidence === 'low', confidence_reasoning must be non-empty. Add .refine() on the Zod schema enforcing this.
- Multi-judge reconciliation stores all raw outputs: The reconciled/aggregated score must never overwrite raw judge outputs. Verify:
SELECT COUNT(*) FROM judge_outputs WHERE evaluation_run_id = $1 returns N rows for N judges.
- Integrity adjustments are bounded: Each
integrity_adjustment.deduction must be between -50 and 0. Unbounded deductions allow a judge bug to wipe a score to negative. Add constraint.
- Lane score vs computed weighted score consistency check: After loading a judge output, recompute
sum(dimension_score * dimension_weight) and verify it's within 1 point of lane_score. Log a warning if divergent.
- Telemetry summaries are present for all evaluation runs:
tool_call_count, step_count, and error_rate must be non-null for any run that had telemetry available. Verify: runs without telemetry store explicit null with a telemetry_unavailable_reason field.
- Flags have machine-readable codes, not just prose: Each flag must include a
flag_code (e.g., COPY_PASTE_DETECTED) so downstream logic can filter/gate on it. Prose-only flags can't be processed programmatically.
- Failed judge doesn't block display: When one judge fails, the UI must show results from the N-1 judges with a "One judge unavailable" notice. Verify this path in the reconciliation logic.
- Schema migration is additive-only: When adding fields to the judge output schema, new fields must be optional with defaults. Verify: loading a v1.0 record with a v2.0 schema returns a valid object, not a parse error.
- positive_signal and primary_weakness are not duplicates of lane score text: These fields must contain specific observations (a concrete thing the submission did), not summaries of scores. Add a minimum-specificity check at storage time.
Complete Zod Schema: Full Judge Output Contract
import { z } from 'zod';
const EvidenceRefSchema = z.object({
type: z.enum([
'transcript_line',
'tool_call',
'diff_hunk',
'test_result',
'code_artifact',
'error_event',
]),
id: z.string().min(1),
location: z.string().optional(),
excerpt: z.string().max(300).optional(),
});
const IntegrityAdjustmentSchema = z.object({
flag_code: z.string().regex(/^[A-Z_]+$/, 'flag_code must be SCREAMING_SNAKE_CASE'),
description: z.string().min(10),
deduction: z.number().min(-50).max(0),
evidence_refs: z.array(EvidenceRefSchema).min(1, 'Integrity adjustment must cite evidence'),
});
const FlagSchema = z.object({
flag_code: z.string().regex(/^[A-Z_]+$/),
severity: z.enum(['info', 'warning', 'critical']),
description: z.string().min(5),
evidence_refs: z.array(EvidenceRefSchema).optional(),
auto_disqualify: z.boolean().default(false),
});
const DimensionScoreSchema = z.object({
dimension_key: z.string().regex(/^[a-z_]+$/),
score: z.number().int().min(0).max(100),
reasoning: z.string().min(20, 'Dimension reasoning must be substantive (>20 chars)'),
evidence_refs: z.array(EvidenceRefSchema).min(0).max(10),
band_label: z.string(),
});
const LaneScoreSchema = z.object({
lane_key: z.string().regex(/^[a-z_]+$/),
lane_score: z.number().int().min(0).max(100),
dimension_scores: z.array(DimensionScoreSchema).min(1),
}).refine(
(lane) => {
return lane.lane_score >= 0 && lane.lane_score <= 100;
}
);
const TelemetrySummarySchema = z.object({
tool_call_count: z.number().int().min(0).nullable(),
step_count: z.number().int().min(0).nullable(),
error_count: z.number().int().min(0).nullable(),
error_rate: z.number().min(0).max(1).nullable(),
total_tokens_used: z.number().int().min(0).nullable(),
wall_time_seconds: z.number().min(0).nullable(),
telemetry_unavailable_reason: z.string().nullable(),
});
export const JudgeOutputSchema = z.object({
schema_version: z.string().regex(/^\d+\.\d+$/, 'Must be semver minor: "2.1"'),
judge_model: z.string().min(3),
judge_run_id: z.string().uuid(),
evaluation_run_id: z.string().uuid(),
submission_id: z.string().uuid(),
rubric_version_id: z.string().uuid(),
overall_score: z.number().int().min(0).max(100),
lane_scores: z.array(LaneScoreSchema).min(1),
positive_signal: z.string().min(20).max(500),
primary_weakness: z.string().min(20).max(500),
confidence: z.enum(['high', 'medium', 'low']),
confidence_reasoning: z.string(),
flags: z.array(FlagSchema).default([]),
integrity_adjustments: z.array(IntegrityAdjustmentSchema).default([]),
integrity_adjusted_score: z.number().int().min(0).max(100),
telemetry_summary: TelemetrySummarySchema,
status: z.enum(['complete', 'partial', 'failed']),
failure_reason: z.string().nullable(),
completed_lane_keys: z.array(z.string()),
judge_started_at: z.string().datetime(),
judge_completed_at: z.string().datetime().nullable(),
}).refine(
(output) => {
if (output.confidence === 'low' && output.confidence_reasoning.length < 20) {
return false;
}
return true;
},
{ message: 'confidence_reasoning must be substantive when confidence is low', path: ['confidence_reasoning'] }
).refine(
(output) => {
return output.integrity_adjusted_score <= output.overall_score;
},
{ message: 'integrity_adjusted_score cannot exceed overall_score', path: ['integrity_adjusted_score'] }
).refine(
(output) => {
if (output.status === 'failed' && !output.failure_reason) return false;
return true;
},
{ message: 'failed outputs must include failure_reason', path: ['failure_reason'] }
);
export type JudgeOutput = z.infer<typeof JudgeOutputSchema>;
export type LaneScore = z.infer<typeof LaneScoreSchema>;
export type DimensionScore = z.infer<typeof DimensionScoreSchema>;
export type EvidenceRef = z.infer<typeof EvidenceRefSchema>;
export type IntegrityAdjustment = z.infer<typeof IntegrityAdjustmentSchema>;
export type TelemetrySummary = z.infer<typeof TelemetrySummarySchema>;
SQL Migration: Judge Output Storage
CREATE TABLE judge_outputs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
evaluation_run_id UUID NOT NULL REFERENCES evaluation_runs(id),
submission_id UUID NOT NULL REFERENCES submissions(id),
rubric_version_id UUID NOT NULL REFERENCES rubrics(id),
judge_run_id UUID NOT NULL UNIQUE,
judge_model TEXT NOT NULL,
schema_version TEXT NOT NULL,
overall_score INTEGER CHECK (overall_score BETWEEN 0 AND 100),
integrity_adjusted_score INTEGER CHECK (integrity_adjusted_score BETWEEN 0 AND 100),
positive_signal TEXT,
primary_weakness TEXT,
confidence TEXT CHECK (confidence IN ('high', 'medium', 'low')),
confidence_reasoning TEXT,
status TEXT NOT NULL CHECK (status IN ('complete', 'partial', 'failed')),
failure_reason TEXT,
completed_lane_keys TEXT[] NOT NULL DEFAULT '{}',
judge_started_at TIMESTAMPTZ NOT NULL,
judge_completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT adjusted_lte_overall CHECK (integrity_adjusted_score <= overall_score)
);
CREATE TABLE judge_lane_scores (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
judge_output_id UUID NOT NULL REFERENCES judge_outputs(id) ON DELETE CASCADE,
lane_key TEXT NOT NULL,
lane_score INTEGER NOT NULL CHECK (lane_score BETWEEN 0 AND 100),
UNIQUE (judge_output_id, lane_key)
);
CREATE TABLE judge_dimension_scores (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
lane_score_id UUID NOT NULL REFERENCES judge_lane_scores(id) ON DELETE CASCADE,
dimension_key TEXT NOT NULL,
score INTEGER NOT NULL CHECK (score BETWEEN 0 AND 100),
reasoning TEXT NOT NULL,
band_label TEXT NOT NULL,
UNIQUE (lane_score_id, dimension_key)
);
CREATE TABLE judge_evidence_refs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
judge_output_id UUID NOT NULL REFERENCES judge_outputs(id) ON DELETE CASCADE,
parent_type TEXT NOT NULL CHECK (parent_type IN ('dimension_score', 'integrity_adjustment', 'flag')),
parent_id UUID NOT NULL,
ref_type TEXT NOT NULL,
ref_id TEXT NOT NULL,
ref_location TEXT,
excerpt TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE judge_flags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
judge_output_id UUID NOT NULL REFERENCES judge_outputs(id) ON DELETE CASCADE,
flag_code TEXT NOT NULL,
severity TEXT NOT NULL CHECK (severity IN ('info', 'warning', 'critical')),
description TEXT NOT NULL,
auto_disqualify BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE judge_integrity_adjustments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
judge_output_id UUID NOT NULL REFERENCES judge_outputs(id) ON DELETE CASCADE,
flag_code TEXT NOT NULL,
description TEXT NOT NULL,
deduction INTEGER NOT NULL CHECK (deduction BETWEEN -50 AND 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE judge_telemetry (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
judge_output_id UUID NOT NULL UNIQUE REFERENCES judge_outputs(id) ON DELETE CASCADE,
tool_call_count INTEGER,
step_count INTEGER,
error_count INTEGER,
error_rate NUMERIC(5,4),
total_tokens_used INTEGER,
wall_time_seconds NUMERIC(10,2),
telemetry_unavailable_reason TEXT
);
CREATE TABLE evaluation_reconciled_scores (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
evaluation_run_id UUID NOT NULL UNIQUE REFERENCES evaluation_runs(id),
submission_id UUID NOT NULL REFERENCES submissions(id),
judge_count INTEGER NOT NULL,
successful_judge_count INTEGER NOT NULL,
final_overall_score NUMERIC(5,2),
final_adjusted_score NUMERIC(5,2),
reconciliation_method TEXT NOT NULL,
has_contradictions BOOLEAN NOT NULL DEFAULT false,
contradiction_details JSONB,
finalized_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_judge_outputs_eval_run ON judge_outputs(evaluation_run_id);
CREATE INDEX idx_judge_outputs_submission ON judge_outputs(submission_id);
CREATE INDEX idx_judge_outputs_status ON judge_outputs(status);
CREATE INDEX idx_judge_lane_scores_output ON judge_lane_scores(judge_output_id);
CREATE INDEX idx_judge_dimension_scores_lane ON judge_dimension_scores(lane_score_id);
CREATE INDEX idx_judge_evidence_refs_parent ON judge_evidence_refs(parent_id, parent_type);
CREATE INDEX idx_judge_flags_output ON judge_flags(judge_output_id);
CREATE INDEX idx_judge_flags_code ON judge_flags(flag_code);
CREATE INDEX idx_reconciled_scores_submission ON evaluation_reconciled_scores(submission_id);
TypeScript: Judge Runner Types and Multi-Judge Reconciliation
import { z } from 'zod';
import { JudgeOutputSchema, type JudgeOutput } from './judge-output-schema';
import { createClient } from '@/lib/supabase/server';
export interface JudgeRunConfig {
judgeModel: 'claude-opus-4-6' | 'gpt-4o' | 'gemini-1.5-pro';
evaluationRunId: string;
submissionId: string;
rubricVersionId: string;
submissionTranscript: string;
telemetryTrace: TelemetryTrace | null;
timeoutMs: number;
}
export interface TelemetryTrace {
toolCalls: Array<{ id: string; name: string; timestamp: number; success: boolean }>;
steps: Array<{ stepNumber: number; type: string; timestamp: number }>;
errors: Array<{ type: string; message: string; timestamp: number }>;
totalTokens: number;
wallTimeSeconds: number;
}
export async function storeJudgeOutput(output: JudgeOutput): Promise<string> {
const supabase = createClient();
const { data: outputRow, error: outputError } = await supabase
.from('judge_outputs')
.insert({
evaluation_run_id: output.evaluation_run_id,
submission_id: output.submission_id,
rubric_version_id: output.rubric_version_id,
judge_run_id: output.judge_run_id,
judge_model: output.judge_model,
schema_version: output.schema_version,
overall_score: output.overall_score,
integrity_adjusted_score: output.integrity_adjusted_score,
positive_signal: output.positive_signal,
primary_weakness: output.primary_weakness,
confidence: output.confidence,
confidence_reasoning: output.confidence_reasoning,
status: output.status,
failure_reason: output.failure_reason,
completed_lane_keys: output.completed_lane_keys,
judge_started_at: output.judge_started_at,
judge_completed_at: output.judge_completed_at,
})
.select('id')
.single();
if (outputError || !outputRow) {
throw new Error(`Failed to store judge output: ${outputError?.message}`);
}
const judgeOutputId = outputRow.id;
for (const laneScore of output.lane_scores) {
const { data: laneRow } = await supabase
.from('judge_lane_scores')
.insert({
judge_output_id: judgeOutputId,
lane_key: laneScore.lane_key,
lane_score: laneScore.lane_score,
})
.select('id')
.single();
if (!laneRow) continue;
for (const dim of laneScore.dimension_scores) {
const { data: dimRow } = await supabase
.from('judge_dimension_scores')
.insert({
lane_score_id: laneRow.id,
dimension_key: dim.dimension_key,
score: dim.score,
reasoning: dim.reasoning,
band_label: dim.band_label,
})
.select('id')
.single();
if (!dimRow) continue;
if (dim.evidence_refs.length > 0) {
await supabase.from('judge_evidence_refs').insert(
dim.evidence_refs.map((ref) => ({
judge_output_id: judgeOutputId,
parent_type: 'dimension_score',
parent_id: dimRow.id,
ref_type: ref.type,
ref_id: ref.id,
ref_location: ref.location ?? null,
excerpt: ref.excerpt ?? null,
}))
);
}
}
}
if (output.flags.length > 0) {
await supabase.from('judge_flags').insert(
output.flags.map((flag) => ({
judge_output_id: judgeOutputId,
flag_code: flag.flag_code,
severity: flag.severity,
description: flag.description,
auto_disqualify: flag.auto_disqualify,
}))
);
}
if (output.integrity_adjustments.length > 0) {
await supabase.from('judge_integrity_adjustments').insert(
output.integrity_adjustments.map((adj) => ({
judge_output_id: judgeOutputId,
flag_code: adj.flag_code,
description: adj.description,
deduction: adj.deduction,
}))
);
}
await supabase.from('judge_telemetry').insert({
judge_output_id: judgeOutputId,
tool_call_count: output.telemetry_summary.tool_call_count,
step_count: output.telemetry_summary.step_count,
error_count: output.telemetry_summary.error_count,
error_rate: output.telemetry_summary.error_rate,
total_tokens_used: output.telemetry_summary.total_tokens_used,
wall_time_seconds: output.telemetry_summary.wall_time_seconds,
telemetry_unavailable_reason: output.telemetry_summary.telemetry_unavailable_reason,
});
return judgeOutputId;
}
export interface ReconciliationResult {
finalOverallScore: number;
finalAdjustedScore: number;
judgeCount: number;
successfulJudgeCount: number;
hasContradictions: boolean;
contradictionDetails: ContradictionDetail[];
laneScoresByJudge: Record<string, Record<string, number>>;
reconciledLaneScores: Record<string, number>;
method: 'mean' | 'median' | 'weighted_judge_confidence';
}
export interface ContradictionDetail {
laneKey: string;
scores: Array<{ judgeModel: string; score: number }>;
spread: number;
}
export function reconcileJudgeOutputs(
outputs: JudgeOutput[],
method: 'mean' | 'median' | 'weighted_judge_confidence' = 'mean'
): ReconciliationResult {
const successfulOutputs = outputs.filter((o) => o.status !== 'failed');
if (successfulOutputs.length === 0) {
throw new Error('Cannot reconcile: no successful judge outputs');
}
const laneScoresByJudge: Record<string, Record<string, number>> = {};
for (const output of successfulOutputs) {
laneScoresByJudge[output.judge_model] = {};
for (const lane of output.lane_scores) {
laneScoresByJudge[output.judge_model][lane.lane_key] = lane.lane_score;
}
}
const allLaneKeys = Array.from(
new Set(successfulOutputs.flatMap((o) => o.lane_scores.map((l) => l.lane_key)))
);
const contradictionDetails: ContradictionDetail[] = [];
const reconciledLaneScores: Record<string, number> = {};
for (const laneKey of allLaneKeys) {
const scores = successfulOutputs
.filter((o) => laneScoresByJudge[o.judge_model][laneKey] !== undefined)
.map((o) => ({
judgeModel: o.judge_model,
score: laneScoresByJudge[o.judge_model][laneKey],
}));
if (scores.length === 0) continue;
const scoreValues = scores.map((s) => s.score);
const spread = Math.max(...scoreValues) - Math.min(...scoreValues);
if (spread > 25) {
contradictionDetails.push({ laneKey, scores, spread });
}
if (method === 'mean') {
reconciledLaneScores[laneKey] = Math.round(
scoreValues.reduce((a, b) => a + b, 0) / scoreValues.length
);
} else if (method === 'median') {
const sorted = [...scoreValues].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
reconciledLaneScores[laneKey] = sorted.length % 2 !== 0
? sorted[mid]
: Math.round((sorted[mid - 1] + sorted[mid]) / 2);
} else {
const confidenceWeights: Record<string, number> = { high: 1.0, medium: 0.7, low: 0.4 };
const judgeConfidence: Record<string, number> = {};
for (const o of successfulOutputs) {
judgeConfidence[o.judge_model] = confidenceWeights[o.confidence] ?? 0.7;
}
let weightedSum = 0;
let totalWeight = 0;
for (const { judgeModel, score } of scores) {
const w = judgeConfidence[judgeModel] ?? 0.7;
weightedSum += score * w;
totalWeight += w;
}
reconciledLaneScores[laneKey] = Math.round(weightedSum / totalWeight);
}
}
const laneValues = Object.values(reconciledLaneScores);
const finalOverallScore = Math.round(
laneValues.reduce((a, b) => a + b, 0) / laneValues.length
);
const worstTotalDeduction = successfulOutputs.reduce((worst, o) => {
const totalDeduction = o.integrity_adjustments.reduce((sum, adj) => sum + adj.deduction, 0);
return Math.min(worst, totalDeduction);
}, 0);
const finalAdjustedScore = Math.max(0, finalOverallScore + worstTotalDeduction);
return {
finalOverallScore,
finalAdjustedScore,
judgeCount: outputs.length,
successfulJudgeCount: successfulOutputs.length,
hasContradictions: contradictionDetails.length > 0,
contradictionDetails,
laneScoresByJudge,
reconciledLaneScores,
method,
};
}
export async function loadAndValidateJudgeOutput(
judgeRunId: string
): Promise<{ output: JudgeOutput; warnings: string[] }> {
const supabase = createClient();
const warnings: string[] = [];
const { data: row, error } = await supabase
.from('judge_outputs')
.select(`
*,
lane_scores:judge_lane_scores(
*,
dimension_scores:judge_dimension_scores(*),
evidence_refs:judge_evidence_refs(*)
),
flags:judge_flags(*),
integrity_adjustments:judge_integrity_adjustments(*),
telemetry:judge_telemetry(*)
`)
.eq('judge_run_id', judgeRunId)
.single();
if (error || !row) {
throw new Error(`Judge output not found: ${judgeRunId}`);
}
const raw = {
schema_version: row.schema_version,
judge_model: row.judge_model,
judge_run_id: row.judge_run_id,
evaluation_run_id: row.evaluation_run_id,
submission_id: row.submission_id,
rubric_version_id: row.rubric_version_id,
overall_score: row.overall_score,
integrity_adjusted_score: row.integrity_adjusted_score,
positive_signal: row.positive_signal,
primary_weakness: row.primary_weakness,
confidence: row.confidence,
confidence_reasoning: row.confidence_reasoning,
status: row.status,
failure_reason: row.failure_reason,
completed_lane_keys: row.completed_lane_keys,
judge_started_at: row.judge_started_at,
judge_completed_at: row.judge_completed_at,
lane_scores: (row.lane_scores ?? []).map((lane: any) => ({
lane_key: lane.lane_key,
lane_score: lane.lane_score,
dimension_scores: (lane.dimension_scores ?? []).map((dim: any) => ({
dimension_key: dim.dimension_key,
score: dim.score,
reasoning: dim.reasoning,
band_label: dim.band_label,
evidence_refs: (lane.evidence_refs ?? [])
.filter((ref: any) => ref.parent_id === dim.id)
.map((ref: any) => ({
type: ref.ref_type,
id: ref.ref_id,
location: ref.ref_location,
excerpt: ref.excerpt,
})),
})),
})),
flags: (row.flags ?? []).map((f: any) => ({
flag_code: f.flag_code,
severity: f.severity,
description: f.description,
auto_disqualify: f.auto_disqualify,
})),
integrity_adjustments: (row.integrity_adjustments ?? []).map((adj: any) => ({
flag_code: adj.flag_code,
description: adj.description,
deduction: adj.deduction,
evidence_refs: [],
})),
telemetry_summary: row.telemetry
? {
tool_call_count: row.telemetry.tool_call_count,
step_count: row.telemetry.step_count,
error_count: row.telemetry.error_count,
error_rate: row.telemetry.error_rate,
total_tokens_used: row.telemetry.total_tokens_used,
wall_time_seconds: row.telemetry.wall_time_seconds,
telemetry_unavailable_reason: row.telemetry.telemetry_unavailable_reason,
}
: {
tool_call_count: null, step_count: null, error_count: null,
error_rate: null, total_tokens_used: null, wall_time_seconds: null,
telemetry_unavailable_reason: 'Telemetry record missing',
},
};
const result = JudgeOutputSchema.safeParse(raw);
if (!result.success) {
throw new Error(`Judge output validation failed: ${JSON.stringify(result.error.issues)}`);
}
for (const lane of result.data.lane_scores) {
if (lane.dimension_scores.length > 1) {
const dimAvg = lane.dimension_scores.reduce((s, d) => s + d.score, 0) / lane.dimension_scores.length;
if (Math.abs(dimAvg - lane.lane_score) > 10) {
warnings.push(
`Lane ${lane.lane_key}: lane_score (${lane.lane_score}) diverges >10pts from dimension average (${Math.round(dimAvg)})`
);
}
}
}
return { output: result.data, warnings };
}
Anti-Patterns
Anti-Pattern 1: Storing evidence as text blobs
const badOutput = {
lane_scores: [{
lane_key: 'correctness',
evidence: 'Line 45 shows the solution fails to handle null inputs. Also the tool call at step 3 crashed.',
}],
};
const goodOutput = {
lane_scores: [{
lane_key: 'correctness',
dimension_scores: [{
dimension_key: 'edge_case_handling',
score: 35,
reasoning: 'Solution crashes on null input at line 45 and does not recover from the tool error at step 3',
evidence_refs: [
{ type: 'transcript_line', id: '45', excerpt: 'TypeError: Cannot read property of null' },
{ type: 'tool_call', id: 'tc_abc123', location: 'step:3', excerpt: 'Error: connection timeout' },
],
}],
}],
};
Anti-Pattern 2: Overwriting raw judge outputs with reconciled data
async function saveReconciled(evalRunId: string, reconciledScore: number) {
await supabase
.from('judge_outputs')
.update({ overall_score: reconciledScore })
.eq('evaluation_run_id', evalRunId);
}
async function saveReconciled(evalRunId: string, result: ReconciliationResult) {
await supabase.from('evaluation_reconciled_scores').insert({
evaluation_run_id: evalRunId,
judge_count: result.judgeCount,
successful_judge_count: result.successfulJudgeCount,
final_overall_score: result.finalOverallScore,
final_adjusted_score: result.finalAdjustedScore,
reconciliation_method: result.method,
has_contradictions: result.hasContradictions,
contradiction_details: result.contradictionDetails,
finalized_at: new Date().toISOString(),
});
}
Anti-Pattern 3: Failing fast when one judge fails
async function runAllJudges(configs: JudgeRunConfig[]) {
const outputs = await Promise.all(configs.map(runSingleJudge));
return reconcileJudgeOutputs(outputs);
}
async function runAllJudges(configs: JudgeRunConfig[]) {
const results = await Promise.allSettled(configs.map(runSingleJudge));
const outputs: JudgeOutput[] = results.map((r, i) => {
if (r.status === 'fulfilled') return r.value;
return {
schema_version: '2.1',
judge_model: configs[i].judgeModel,
judge_run_id: crypto.randomUUID(),
evaluation_run_id: configs[i].evaluationRunId,
submission_id: configs[i].submissionId,
rubric_version_id: configs[i].rubricVersionId,
overall_score: 0,
lane_scores: [],
positive_signal: 'N/A — judge failed',
primary_weakness: 'N/A — judge failed',
confidence: 'low' as const,
confidence_reasoning: `Judge failed: ${r.reason?.message ?? 'unknown error'}`,
flags: [],
integrity_adjustments: [],
integrity_adjusted_score: 0,
telemetry_summary: {
tool_call_count: null, step_count: null, error_count: null,
error_rate: null, total_tokens_used: null, wall_time_seconds: null,
telemetry_unavailable_reason: r.reason?.message ?? 'judge_failed',
},
status: 'failed' as const,
failure_reason: r.reason?.message ?? 'unknown',
completed_lane_keys: [],
judge_started_at: new Date().toISOString(),
judge_completed_at: new Date().toISOString(),
};
});
await Promise.all(outputs.map(storeJudgeOutput));
return reconcileJudgeOutputs(outputs);
}
Common Failures to Catch in Review
| Failure | Symptom | Fix |
|---|
schema_version not pinned at evaluation time | Judge prompt changes silently; old records can't be interpreted correctly against current schema | Store schema_version in the judge output row; load the correct validator version for display |
| Evidence refs are text, not structured objects | Can't render "click to line 45" links; can't filter by evidence type; can't build evidence panels | Enforce EvidenceRefSchema via Zod; reject plain string evidence in the judge prompt's output format |
integrity_adjusted_score > overall_score | A judge applies a positive "adjustment" (bug in prompt); downstream score shows higher than raw | Add DB constraint integrity_adjusted_score <= overall_score; add Zod refine check |
| No lane scores stored for partial/failed outputs | completed_lane_keys is empty but not explained; UI can't show which lanes were completed | Store completed_lane_keys explicitly; insert lane rows for completed lanes even when status is 'partial' |
| Multi-judge reconciliation deletes raw outputs | Audit log shows reconciled score but no individual judge breakdown; can't investigate contradictions | Use evaluation_reconciled_scores table; never UPDATE judge_outputs rows |
| Confidence is 'low' but reasoning is empty | User sees "low confidence" with no explanation; trust is lost with no recovery | Add Zod .refine(): confidence === 'low' requires confidence_reasoning.length >= 20 |
positive_signal is generic ("Shows good understanding") | Premium feedback reads like AI-generated filler; users feel cheated | Add minimum-specificity check in synthesis layer (see anti-generic skill); store the raw signal without synthesis |
| Judge timeout leaves evaluation_run in 'running' state forever | User never sees results; no error surface | Set explicit judge timeout; always write a 'failed' stub on timeout; add heartbeat check for stale evaluation_runs |
deduction field is positive (typo in judge prompt) | Integrity adjustment increases score instead of decreasing it | DB constraint deduction BETWEEN -50 AND 0; Zod .max(0) on the deduction field |
| Lane keys in judge output don't match rubric lane keys | Reconciliation can't match lanes; score is silently dropped | Validate completed_lane_keys against rubric.lanes.map(l => l.lane_key) before storing |
Changelog
- 2026-03-31: Created for Bouts premium feedback system build