/** * Sprint 12 Task 2 §2.1 — A3 failure taxonomy namespace split coverage. * * Decision doc: decisions/2026-04-23-jsonl-record-taxonomy-split-locked.md * * Covers the additive namespace split across four surfaces: * 1. JudgePayload carries `a3_failure_code` + `a3_rationale` alongside the * legacy `judge_failure_mode` + `judge_rationale` fields (Sprint 9 * read-only preservation contract). * 2. `buildAggregate` emits `failure_distribution` when records carry the * `a3_failure_code` column (legacy-only runs stay `undefined`). * 3. `a3_failure_code` / `a3_rationale` grep-compatibility — A3 rows are * greppable under the `a3_` prefix per Opcija C audit trail rationale. * 4. Mapping from Sprint 9 5-value FailureMode into A3 § 6 8-value * FailureCode preserves semantic equivalence (F1..F5 pass through, * null → null, F6 / F_other remain unused until rubric upgrade). */ import { describe, expect, it } from 'vitest'; import { runJudge, type JudgeConfig, type JudgeTriple } from '../src/judge-runner.js'; import type { LlmClient } from '../src/judge-types.js'; import { buildAggregate } from '../src/metrics.js'; import type { FailureMode, JsonlRecord, RunConfig } from '../src/types.js'; function stubClient( verdict: 'correct' | 'incorrect', failureMode: FailureMode | null, ): LlmClient { return { async complete(_prompt: string) { return JSON.stringify({ verdict, failure_mode: failureMode, rationale: `stub: ${verdict}/${failureMode ?? 'null'}`, }); }, }; } const TRIPLE: JudgeTriple = { question: 'What is the capital of France?', groundTruth: 'Paris', contextExcerpt: 'France is a country in Europe. Its capital is Paris.', modelAnswer: 'Paris', }; describe('Sprint 12 Task 2 §2.1 — JudgePayload carries a3_* fields', () => { it('single-judge correct verdict → a3_failure_code=null, a3_rationale=null, legacy preserved', async () => { const client = stubClient('correct', null); const payload = await runJudge(TRIPLE, { kind: 'single', model: 'claude-opus-4-7', client }); // A3 namespace columns. expect(payload.a3_failure_code).toBeNull(); expect(payload.a3_rationale).toBeNull(); // Sprint 9 legacy preserved verbatim (contract: read-only, not erased). expect(payload.judge_failure_mode).toBeNull(); expect(payload.judge_rationale).toContain('correct'); }); it('single-judge incorrect/F3 → a3_failure_code mirrors legacy 5-value code', async () => { const client = stubClient('incorrect', 'F3'); const payload = await runJudge(TRIPLE, { kind: 'single', model: 'claude-opus-4-7', client }); expect(payload.a3_failure_code).toBe('F3'); expect(payload.a3_rationale).toBeNull(); expect(payload.judge_failure_mode).toBe('F3'); }); it('3-primary ensemble 2-1 majority → a3_failure_code mirrors majority code', async () => { const opus = stubClient('incorrect', 'F1'); const gpt = stubClient('incorrect', 'F1'); const gemini = stubClient('correct', null); const clients = new Map(); clients.set('claude-opus-4-7', opus); clients.set('gpt-5.4', gpt); clients.set('gemini-3.1', gemini); const config: JudgeConfig = { kind: 'ensemble', models: ['claude-opus-4-7', 'gpt-5.4', 'gemini-3.1'], clients, }; const payload = await runJudge(TRIPLE, config); expect(payload.judge_failure_mode).toBe('F1'); expect(payload.a3_failure_code).toBe('F1'); expect(payload.a3_rationale).toBeNull(); }); it('3-primary + quadri-vendor tie-break → a3_failure_code mirrors resolved code', async () => { const opus = stubClient('correct', null); const gpt = stubClient('incorrect', 'F3'); const gemini = stubClient('incorrect', 'F4'); const grok = stubClient('correct', null); const clients = new Map(); clients.set('claude-opus-4-7', opus); clients.set('gpt-5.4', gpt); clients.set('gemini-3.1', gemini); const config: JudgeConfig = { kind: 'ensemble', models: ['claude-opus-4-7', 'gpt-5.4', 'gemini-3.1'], clients, tieBreakerModel: 'xai/grok-4.20', tieBreakerClient: grok, }; const payload = await runJudge(TRIPLE, config); expect(payload.tie_break_path).toBe('quadri-vendor'); expect(payload.judge_verdict).toBe('correct'); expect(payload.judge_failure_mode).toBeNull(); expect(payload.a3_failure_code).toBeNull(); expect(payload.a3_rationale).toBeNull(); }); it('3-primary pm-escalation 1-1-1-1 → a3_failure_code undefined (skipped-judge semantics)', async () => { const opus = stubClient('correct', null); const gpt = stubClient('incorrect', 'F1'); const gemini = stubClient('incorrect', 'F2'); const grok = stubClient('incorrect', 'F3'); // fourth bucket → 1-1-1-1 const clients = new Map(); clients.set('claude-opus-4-7', opus); clients.set('gpt-5.4', gpt); clients.set('gemini-3.1', gemini); const config: JudgeConfig = { kind: 'ensemble', models: ['claude-opus-4-7', 'gpt-5.4', 'gemini-3.1'], clients, tieBreakerModel: 'xai/grok-4.20', tieBreakerClient: grok, }; const payload = await runJudge(TRIPLE, config); expect(payload.tie_break_path).toBe('pm-escalation'); expect(payload.judge_error).toBe('PM_ESCALATION'); // skipped-judge semantics: no A3 code assigned, aggregator will exclude. expect(payload.a3_failure_code).toBeUndefined(); expect(payload.a3_rationale).toBeNull(); }); }); describe('Sprint 12 Task 2 §2.1 — buildAggregate failure_distribution', () => { function makeConfig(): RunConfig { return { run: { kind: 'cell', name: 'raw' }, dataset: { id: 'synthetic', displayName: 'Synthetic', dataPath: 'synthetic', source: 'synthetic' }, model: { id: 'qwen3.6-35b-a3b', displayName: 'Qwen', provider: 'alibaba', litellmModel: 'dashscope/qwen3.6-35b-a3b', pricePerMillionInput: 0.2, pricePerMillionOutput: 0.8, contextWindow: 262144, }, limit: 10, seed: 42, budgetUsd: Infinity, outputPath: '/tmp/test.jsonl', dryRun: true, litellmUrl: 'http://localhost:4000', litellmApiKey: 'sk-test', }; } function makeRecord(overrides: Partial = {}): JsonlRecord { return { turnId: 't-1', cell: 'raw', instance_id: 'inst-1', model: 'qwen', seed: 42, accuracy: 1, p50_latency_ms: 10, p95_latency_ms: 20, usd_per_query: 0.001, failure_mode: null, ...overrides, }; } it('emits failure_distribution when records carry a3_failure_code', () => { const records: JsonlRecord[] = [ makeRecord({ instance_id: 'i1', a3_failure_code: null }), makeRecord({ instance_id: 'i2', a3_failure_code: null }), makeRecord({ instance_id: 'i3', a3_failure_code: 'F1' }), makeRecord({ instance_id: 'i4', a3_failure_code: 'F_other', a3_rationale: 'model hallucinated an unrelated entity and drifted off topic entirely forever', }), ]; const summary = buildAggregate( makeConfig(), records, '2026-04-23T00:00:00.000Z', '2026-04-23T00:00:01.000Z', null, ); expect(summary.failure_distribution).toBeDefined(); expect(summary.failure_distribution!.total).toBe(4); expect(summary.failure_distribution!.counts.null).toBe(2); expect(summary.failure_distribution!.counts.F1).toBe(1); expect(summary.failure_distribution!.counts.F_other).toBe(1); expect(summary.failure_distribution!.f_other_rate).toBeCloseTo(0.25, 10); expect(summary.failure_distribution!.f_other_review_flag).toBe(true); // 25% > 10% expect(summary.failure_distribution!.f_other_rationales_sample).toHaveLength(1); }); it('leaves failure_distribution undefined when no records carry a3_failure_code', () => { const records: JsonlRecord[] = [ makeRecord({ instance_id: 'i1', judge_failure_mode: 'F1' }), // legacy only makeRecord({ instance_id: 'i2' }), // no judge at all ]; const summary = buildAggregate( makeConfig(), records, '2026-04-23T00:00:00.000Z', '2026-04-23T00:00:01.000Z', null, ); expect(summary.failure_distribution).toBeUndefined(); }); it('excludes rows with undefined a3_failure_code (PM_ESCALATION skipped-judge semantics)', () => { const records: JsonlRecord[] = [ makeRecord({ instance_id: 'i1', a3_failure_code: null }), makeRecord({ instance_id: 'i2', a3_failure_code: 'F1' }), // PM escalated → a3 undefined, should be excluded from distribution. makeRecord({ instance_id: 'i3' }), ]; const summary = buildAggregate( makeConfig(), records, '2026-04-23T00:00:00.000Z', '2026-04-23T00:00:01.000Z', null, ); expect(summary.failure_distribution).toBeDefined(); expect(summary.failure_distribution!.total).toBe(2); }); }); describe('Sprint 12 Task 2 §2.1 — JsonlRecord grep compatibility', () => { it('a3_failure_code + a3_rationale accepted at the type level as optional fields', () => { // Compile-time contract check: the fields exist and accept the // FailureCode union. The test body only asserts that the object // structural-types correctly against JsonlRecord. const rec: JsonlRecord = { turnId: 't-1', cell: 'raw', instance_id: 'inst-1', model: 'qwen', seed: 42, accuracy: 1, p50_latency_ms: 10, p95_latency_ms: 20, usd_per_query: 0.001, failure_mode: null, a3_failure_code: 'F_other', a3_rationale: 'ten or more token rationale satisfying the A3 LOCK validator invariant', }; expect(rec.a3_failure_code).toBe('F_other'); expect(rec.a3_rationale?.split(/\s+/).filter(Boolean).length).toBeGreaterThanOrEqual(10); }); });