@lythos/skill-arena 0.15.7 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -12
- package/package.json +7 -7
- package/src/arena-toml.test.ts +19 -19
- package/src/cli.test.ts +118 -0
- package/src/cli.ts +121 -98
- package/src/comparative-judge.test.ts +31 -31
- package/src/player.test.ts +11 -11
- package/src/preflight.test.ts +56 -56
- package/src/runner.test.ts +269 -10
- package/src/runner.ts +88 -35
- package/src/stats.test.ts +10 -10
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { describe,
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
2
|
import { computePareto, buildComparativePrompt, toScoreMatrix, normalizeComparativeOutput } from './comparative-judge'
|
|
3
3
|
import { ArenaManifest, CriterionDef, ComparativeReport } from '@lythos/test-utils/schema'
|
|
4
4
|
import type { ArenaManifest as ArenaManifestType } from '@lythos/test-utils/schema'
|
|
5
5
|
|
|
6
6
|
describe('computePareto', () => {
|
|
7
|
-
|
|
7
|
+
it('single participant is always non-dominated', () => {
|
|
8
8
|
const result = computePareto([
|
|
9
9
|
{ participant_id: 'run-01', scores: { a: 5, b: 3 } },
|
|
10
10
|
])
|
|
@@ -13,7 +13,7 @@ describe('computePareto', () => {
|
|
|
13
13
|
expect(result[0].dominated_by).toEqual([])
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
it('clear dominance: run-01 dominates run-02 on all criteria', () => {
|
|
17
17
|
const result = computePareto([
|
|
18
18
|
{ participant_id: 'run-01', scores: { coverage: 5, relevance: 5 } },
|
|
19
19
|
{ participant_id: 'run-02', scores: { coverage: 3, relevance: 2 } },
|
|
@@ -23,7 +23,7 @@ describe('computePareto', () => {
|
|
|
23
23
|
expect(result[1].dominated_by).toEqual(['run-01'])
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
it('equal scores: no one dominates', () => {
|
|
27
27
|
const result = computePareto([
|
|
28
28
|
{ participant_id: 'run-01', scores: { a: 4, b: 4 } },
|
|
29
29
|
{ participant_id: 'run-02', scores: { a: 4, b: 4 } },
|
|
@@ -32,7 +32,7 @@ describe('computePareto', () => {
|
|
|
32
32
|
expect(result[1].dominated).toBe(false)
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
it('cross dominance: each wins on different criteria', () => {
|
|
36
36
|
const result = computePareto([
|
|
37
37
|
{ participant_id: 'run-01', scores: { speed: 5, accuracy: 2 } },
|
|
38
38
|
{ participant_id: 'run-02', scores: { speed: 2, accuracy: 5 } },
|
|
@@ -42,7 +42,7 @@ describe('computePareto', () => {
|
|
|
42
42
|
expect(result[1].dominated).toBe(false)
|
|
43
43
|
})
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
it('multi-participant: transitive dominance chain', () => {
|
|
46
46
|
const result = computePareto([
|
|
47
47
|
{ participant_id: 'best', scores: { a: 5, b: 5, c: 5 } },
|
|
48
48
|
{ participant_id: 'mid', scores: { a: 4, b: 4, c: 4 } },
|
|
@@ -56,7 +56,7 @@ describe('computePareto', () => {
|
|
|
56
56
|
expect(result[2].dominated_by.sort()).toEqual(['best', 'mid'].sort())
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
it('Pareto frontier from playground BDD-research: run-01 dominates run-02', () => {
|
|
60
60
|
// From playground/arena-bdd-research/report.md:
|
|
61
61
|
// Run-01: coverage=5, relevance=5, actionability=5, depth=5
|
|
62
62
|
// Run-02: coverage=3, relevance=2, actionability=2, depth=1
|
|
@@ -69,7 +69,7 @@ describe('computePareto', () => {
|
|
|
69
69
|
expect(result[1].dominated_by).toEqual(['run-01'])
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
it('empty scores object', () => {
|
|
73
73
|
const result = computePareto([
|
|
74
74
|
{ participant_id: 'a', scores: {} },
|
|
75
75
|
{ participant_id: 'b', scores: {} },
|
|
@@ -79,7 +79,7 @@ describe('computePareto', () => {
|
|
|
79
79
|
expect(result[1].dominated).toBe(false)
|
|
80
80
|
})
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
it('partial criteria overlap', () => {
|
|
83
83
|
const result = computePareto([
|
|
84
84
|
{ participant_id: 'run-01', scores: { a: 5, b: 3 } },
|
|
85
85
|
{ participant_id: 'run-02', scores: { a: 3, c: 5 } },
|
|
@@ -103,7 +103,7 @@ const manifestFixture: ArenaManifest = {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
describe('buildComparativePrompt', () => {
|
|
106
|
-
|
|
106
|
+
it('includes task description', () => {
|
|
107
107
|
const prompt = buildComparativePrompt({
|
|
108
108
|
manifest: manifestFixture,
|
|
109
109
|
verdicts: [],
|
|
@@ -111,7 +111,7 @@ describe('buildComparativePrompt', () => {
|
|
|
111
111
|
expect(prompt).toContain('Write a function that adds two numbers')
|
|
112
112
|
})
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
it('includes all participants', () => {
|
|
115
115
|
const prompt = buildComparativePrompt({
|
|
116
116
|
manifest: manifestFixture,
|
|
117
117
|
verdicts: [],
|
|
@@ -122,7 +122,7 @@ describe('buildComparativePrompt', () => {
|
|
|
122
122
|
expect(prompt).toContain('Full test discipline')
|
|
123
123
|
})
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
it('includes criteria list', () => {
|
|
126
126
|
const prompt = buildComparativePrompt({
|
|
127
127
|
manifest: manifestFixture,
|
|
128
128
|
verdicts: [],
|
|
@@ -131,7 +131,7 @@ describe('buildComparativePrompt', () => {
|
|
|
131
131
|
expect(prompt).toContain('efficiency')
|
|
132
132
|
})
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
it('includes Zod schema in output spec', () => {
|
|
135
135
|
const prompt = buildComparativePrompt({
|
|
136
136
|
manifest: manifestFixture,
|
|
137
137
|
verdicts: [],
|
|
@@ -145,7 +145,7 @@ describe('buildComparativePrompt', () => {
|
|
|
145
145
|
// ── toScoreMatrix (pure Zod validation wrapper) ──────────────────────
|
|
146
146
|
|
|
147
147
|
describe('toScoreMatrix', () => {
|
|
148
|
-
|
|
148
|
+
it('passes through valid score cells', () => {
|
|
149
149
|
const result = toScoreMatrix(manifestFixture, [
|
|
150
150
|
{ participant_id: 'bare', criterion: 'correctness', weight: 0.5, score: 4, rationale: 'works' },
|
|
151
151
|
{ participant_id: 'bare', criterion: 'efficiency', weight: 0.5, score: 3, rationale: 'ok' },
|
|
@@ -166,7 +166,7 @@ const sampleScoreMatrix = [
|
|
|
166
166
|
]
|
|
167
167
|
|
|
168
168
|
describe('normalizeComparativeOutput', () => {
|
|
169
|
-
|
|
169
|
+
it('passes through already-correct format', () => {
|
|
170
170
|
const input = {
|
|
171
171
|
score_matrix: sampleScoreMatrix,
|
|
172
172
|
key_findings: ['TDD produced cleaner code'],
|
|
@@ -177,7 +177,7 @@ describe('normalizeComparativeOutput', () => {
|
|
|
177
177
|
expect((result.score_matrix as any[])[0].participant_id).toBe('bare')
|
|
178
178
|
})
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
it('maps participantId to participant_id', () => {
|
|
181
181
|
const input: Record<string, unknown> = {
|
|
182
182
|
score_matrix: [
|
|
183
183
|
{ participantId: 'bare', criterion: 'accuracy', weight: 0.5, score: 4, rationale: 'good' },
|
|
@@ -189,7 +189,7 @@ describe('normalizeComparativeOutput', () => {
|
|
|
189
189
|
expect((result.score_matrix as any[])[0].participant_id).toBe('bare')
|
|
190
190
|
})
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
it('maps side to participant_id', () => {
|
|
193
193
|
const input: Record<string, unknown> = {
|
|
194
194
|
score_matrix: [
|
|
195
195
|
{ side: 'tdd', criterion: 'quality', weight: 0.5, score: 5, rationale: 'excellent' },
|
|
@@ -201,7 +201,7 @@ describe('normalizeComparativeOutput', () => {
|
|
|
201
201
|
expect((result.score_matrix as any[])[0].participant_id).toBe('tdd')
|
|
202
202
|
})
|
|
203
203
|
|
|
204
|
-
|
|
204
|
+
it('coerces string score to number', () => {
|
|
205
205
|
const input: Record<string, unknown> = {
|
|
206
206
|
score_matrix: [
|
|
207
207
|
{ participant_id: 'bare', criterion: 'a', weight: 0.5, score: '4', rationale: 'ok' },
|
|
@@ -213,7 +213,7 @@ describe('normalizeComparativeOutput', () => {
|
|
|
213
213
|
expect((result.score_matrix as any[])[0].score).toBe(4)
|
|
214
214
|
})
|
|
215
215
|
|
|
216
|
-
|
|
216
|
+
it('normalizes weight >1 as percentage', () => {
|
|
217
217
|
const input: Record<string, unknown> = {
|
|
218
218
|
score_matrix: [
|
|
219
219
|
{ participant_id: 'bare', criterion: 'a', weight: 50, score: 4, rationale: 'ok' },
|
|
@@ -225,7 +225,7 @@ describe('normalizeComparativeOutput', () => {
|
|
|
225
225
|
expect((result.score_matrix as any[])[0].weight).toBe(0.5)
|
|
226
226
|
})
|
|
227
227
|
|
|
228
|
-
|
|
228
|
+
it('defaults weight to 0.25 when undefined', () => {
|
|
229
229
|
const input: Record<string, unknown> = {
|
|
230
230
|
score_matrix: [
|
|
231
231
|
{ participant_id: 'bare', criterion: 'a', score: 4, rationale: 'ok' },
|
|
@@ -237,7 +237,7 @@ describe('normalizeComparativeOutput', () => {
|
|
|
237
237
|
expect((result.score_matrix as any[])[0].weight).toBe(0.25)
|
|
238
238
|
})
|
|
239
239
|
|
|
240
|
-
|
|
240
|
+
it('maps reason to rationale', () => {
|
|
241
241
|
const input: Record<string, unknown> = {
|
|
242
242
|
score_matrix: [
|
|
243
243
|
{ participant_id: 'bare', criterion: 'a', weight: 0.5, score: 4, reason: 'looks fine' },
|
|
@@ -249,7 +249,7 @@ describe('normalizeComparativeOutput', () => {
|
|
|
249
249
|
expect((result.score_matrix as any[])[0].rationale).toBe('looks fine')
|
|
250
250
|
})
|
|
251
251
|
|
|
252
|
-
|
|
252
|
+
it('maps explanation to rationale', () => {
|
|
253
253
|
const input: Record<string, unknown> = {
|
|
254
254
|
score_matrix: [
|
|
255
255
|
{ participant_id: 'bare', criterion: 'a', weight: 0.5, score: 4, explanation: 'works' },
|
|
@@ -261,7 +261,7 @@ describe('normalizeComparativeOutput', () => {
|
|
|
261
261
|
expect((result.score_matrix as any[])[0].rationale).toBe('works')
|
|
262
262
|
})
|
|
263
263
|
|
|
264
|
-
|
|
264
|
+
it('normalizes recommendations with role fallback', () => {
|
|
265
265
|
const input: Record<string, unknown> = {
|
|
266
266
|
score_matrix: [],
|
|
267
267
|
key_findings: [],
|
|
@@ -275,7 +275,7 @@ describe('normalizeComparativeOutput', () => {
|
|
|
275
275
|
expect(recs[0].recommendation).toBe('Use TDD')
|
|
276
276
|
})
|
|
277
277
|
|
|
278
|
-
|
|
278
|
+
it('normalizes recommendations with advice fallback', () => {
|
|
279
279
|
const input: Record<string, unknown> = {
|
|
280
280
|
score_matrix: [],
|
|
281
281
|
key_findings: [],
|
|
@@ -287,7 +287,7 @@ describe('normalizeComparativeOutput', () => {
|
|
|
287
287
|
expect((result.recommendations as any[])[0].recommendation).toBe('Consider refactoring')
|
|
288
288
|
})
|
|
289
289
|
|
|
290
|
-
|
|
290
|
+
it('handles empty key_findings', () => {
|
|
291
291
|
const input: Record<string, unknown> = {
|
|
292
292
|
score_matrix: [],
|
|
293
293
|
}
|
|
@@ -295,7 +295,7 @@ describe('normalizeComparativeOutput', () => {
|
|
|
295
295
|
expect(result.key_findings).toEqual([])
|
|
296
296
|
})
|
|
297
297
|
|
|
298
|
-
|
|
298
|
+
it('converts pivot-table format: { participant: { criterion: score } }', () => {
|
|
299
299
|
const input: Record<string, unknown> = {
|
|
300
300
|
bare: { correctness: 4, correctness_rationale: 'works', efficiency: 3, efficiency_rationale: 'ok' },
|
|
301
301
|
tdd: { correctness: 5, correctness_rationale: 'tests', efficiency: 4, efficiency_rationale: 'clean' },
|
|
@@ -310,7 +310,7 @@ describe('normalizeComparativeOutput', () => {
|
|
|
310
310
|
expect(bareCorrectness.weight).toBe(0.25)
|
|
311
311
|
})
|
|
312
312
|
|
|
313
|
-
|
|
313
|
+
it('clamps score to 1-5 range', () => {
|
|
314
314
|
const input: Record<string, unknown> = {
|
|
315
315
|
score_matrix: [
|
|
316
316
|
{ participant_id: 'bare', criterion: 'a', weight: 0.5, score: 0, rationale: 'terrible' },
|
|
@@ -367,7 +367,7 @@ const manifestWithRubrics: ArenaManifestType = {
|
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
describe('buildComparativePrompt with structured criteria', () => {
|
|
370
|
-
|
|
370
|
+
it('injects rubric anchors into prompt', () => {
|
|
371
371
|
const prompt = buildComparativePrompt({ manifest: manifestWithRubrics, verdicts: [] })
|
|
372
372
|
expect(prompt).toContain('信息准确性')
|
|
373
373
|
expect(prompt).toContain('Evaluator: ISTJ测试员')
|
|
@@ -377,7 +377,7 @@ describe('buildComparativePrompt with structured criteria', () => {
|
|
|
377
377
|
expect(prompt).toContain('Evaluator: INTJ架构师')
|
|
378
378
|
})
|
|
379
379
|
|
|
380
|
-
|
|
380
|
+
it('falls back to bare format for string criteria', () => {
|
|
381
381
|
const manifest: ArenaManifestType = {
|
|
382
382
|
id: 'test', created_at: '2026-01-01T00:00:00Z', task: 'test', mode: 'decks',
|
|
383
383
|
participants: [{ id: 'a', name: 'A', deck: 'd1' }, { id: 'b', name: 'B', deck: 'd2' }],
|
|
@@ -393,7 +393,7 @@ describe('buildComparativePrompt with structured criteria', () => {
|
|
|
393
393
|
// Simulate a realistic LLM judge output — the kind of JSON an actual Claude
|
|
394
394
|
// comparative judge call would produce. Verify our normalization handles it.
|
|
395
395
|
describe('full pipeline: mock LLM output → schema validation', () => {
|
|
396
|
-
|
|
396
|
+
it('clean score_matrix passes through ComparativeReport.parse', () => {
|
|
397
397
|
const cleanOutput = {
|
|
398
398
|
score_matrix: [
|
|
399
399
|
{ participant_id: 'bare', criterion: 'accuracy', weight: 0.4, score: 3, rationale: 'Correct on Bun version but missed pnpm migration detail' },
|
|
@@ -419,7 +419,7 @@ describe('full pipeline: mock LLM output → schema validation', () => {
|
|
|
419
419
|
expect(deepAccuracy!.score).toBe(5)
|
|
420
420
|
})
|
|
421
421
|
|
|
422
|
-
|
|
422
|
+
it('messy LLM output with field name variants gets normalized', () => {
|
|
423
423
|
// Simulates a messy Claude output — participantId instead of participant_id,
|
|
424
424
|
// reason instead of rationale, string score
|
|
425
425
|
const messyLLMOutput = {
|
package/src/player.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe,
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
2
|
import { resolvePlayer, resolveSides, groupBySide, totalRuns } from './player'
|
|
3
3
|
import { parseArenaToml } from './arena-toml'
|
|
4
4
|
|
|
@@ -20,29 +20,29 @@ deck = "./decks/rich.toml"
|
|
|
20
20
|
`)
|
|
21
21
|
|
|
22
22
|
describe('resolvePlayer', () => {
|
|
23
|
-
|
|
23
|
+
it('maps claude-code → claude', () => {
|
|
24
24
|
expect(resolvePlayer('claude-code')).toBe('claude-sdk')
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
it('maps Claude → claude (case insensitive)', () => {
|
|
28
28
|
expect(resolvePlayer('Claude')).toBe('claude-sdk')
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
it('maps kimi → kimi', () => {
|
|
32
32
|
expect(resolvePlayer('kimi')).toBe('kimi')
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
it('passes through unknown player names', () => {
|
|
36
36
|
expect(resolvePlayer('expert-architect')).toBe('expert-architect')
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
it('trims whitespace', () => {
|
|
40
40
|
expect(resolvePlayer(' claude-code ')).toBe('claude-sdk')
|
|
41
41
|
})
|
|
42
42
|
})
|
|
43
43
|
|
|
44
44
|
describe('resolveSides', () => {
|
|
45
|
-
|
|
45
|
+
it('resolves all sides in arena.toml', () => {
|
|
46
46
|
const sides = resolveSides(toml)
|
|
47
47
|
expect(sides).toHaveLength(2)
|
|
48
48
|
expect(sides[0].platform).toBe('claude-sdk')
|
|
@@ -50,7 +50,7 @@ describe('resolveSides', () => {
|
|
|
50
50
|
expect(sides[0].playerName).toBe('claude-code')
|
|
51
51
|
})
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
it('preserves side config', () => {
|
|
54
54
|
const sides = resolveSides(toml)
|
|
55
55
|
expect(sides[0].side.name).toBe('minimal')
|
|
56
56
|
expect(sides[0].side.deck).toBe('./decks/minimal.toml')
|
|
@@ -58,7 +58,7 @@ describe('resolveSides', () => {
|
|
|
58
58
|
})
|
|
59
59
|
|
|
60
60
|
describe('groupBySide', () => {
|
|
61
|
-
|
|
61
|
+
it('groups by side name with run count', () => {
|
|
62
62
|
const groups = groupBySide(toml)
|
|
63
63
|
expect(groups).toHaveLength(2)
|
|
64
64
|
expect(groups[0].runs).toBe(3) // runs_per_side
|
|
@@ -66,7 +66,7 @@ describe('groupBySide', () => {
|
|
|
66
66
|
expect(groups[0].platform).toBe('claude-sdk')
|
|
67
67
|
})
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
it('control flag preserved', () => {
|
|
70
70
|
const controlToml = parseArenaToml(`
|
|
71
71
|
[arena]
|
|
72
72
|
task = "x"
|
|
@@ -89,7 +89,7 @@ control = true
|
|
|
89
89
|
})
|
|
90
90
|
|
|
91
91
|
describe('totalRuns', () => {
|
|
92
|
-
|
|
92
|
+
it('calculates sides × runs_per_side', () => {
|
|
93
93
|
expect(totalRuns(toml)).toBe(6) // 2 sides × 3 runs
|
|
94
94
|
})
|
|
95
95
|
})
|