profileShare

rasmusjy / roundtable

Read-only snapshot

No repository description.

main default branch 181 files Expires Sep 13, 2026, 9:06 AM
scoring.test.ts 6,315 bytes
1 import { describe, expect, it } from 'vitest';
2 import {
3 buildPredictionCells,
4 buildScoreMatrix,
5 classifyPrediction,
6 predictionCalibration,
7 type PredictionCell,
8 } from './scoring';
9 import { emptyUsage, type CritiqueRecord, type Participant } from './types';
10
11 const participants: Participant[] = [
12 { id: 'p0', model: 'openai/gpt-4o', displayName: 'GPT 4o' },
13 { id: 'p1', model: 'anthropic/claude-3.5', displayName: 'Claude 3.5' },
14 { id: 'p2', model: 'google/gemini-pro', displayName: 'Gemini Pro' },
15 ];
16
17 function critique(reviewer: string, scores: Record<string, number>): CritiqueRecord {
18 return {
19 round: 1,
20 reviewerParticipantId: reviewer,
21 reviewerModel: 'x/y',
22 reviews: Object.entries(scores).map(([targetParticipantId, score]) => ({
23 label: 'A',
24 targetParticipantId,
25 weaknesses: [],
26 strengths: [],
27 score,
28 justification: '',
29 })),
30 usage: emptyUsage(),
31 latencyMs: 0,
32 };
33 }
34
35 describe('buildScoreMatrix', () => {
36 it('places scores in reviewer×target cells with self-cells null', () => {
37 const critiques = [
38 critique('p0', { p1: 8, p2: 6 }),
39 critique('p1', { p0: 7, p2: 5 }),
40 critique('p2', { p0: 9, p1: 4 }),
41 ];
42 const m = buildScoreMatrix(participants, critiques);
43
44 expect(m.order).toEqual(['p0', 'p1', 'p2']);
45 expect(m.cells.p0!.p1).toBe(8);
46 expect(m.cells.p0!.p2).toBe(6);
47 expect(m.cells.p0!.p0).toBeNull(); // no self-review
48 expect(m.cells.p2!.p1).toBe(4);
49 });
50
51 it('computes averages received and given', () => {
52 const critiques = [
53 critique('p0', { p1: 8, p2: 6 }),
54 critique('p1', { p0: 7, p2: 5 }),
55 critique('p2', { p0: 9, p1: 4 }),
56 ];
57 const m = buildScoreMatrix(participants, critiques);
58
59 // p0 received 7 (from p1) and 9 (from p2) -> 8
60 expect(m.averagesReceived.p0).toBe(8);
61 // p1 received 8 (p0) and 4 (p2) -> 6
62 expect(m.averagesReceived.p1).toBe(6);
63 // p0 gave 8 and 6 -> 7
64 expect(m.averagesGiven.p0).toBe(7);
65 });
66
67 it('returns null averages for a participant nobody scored', () => {
68 const m = buildScoreMatrix(participants, [critique('p0', { p1: 8 })]);
69 expect(m.averagesReceived.p2).toBeNull();
70 expect(m.averagesGiven.p1).toBeNull();
71 });
72
73 it('ignores critiques from unknown reviewers defensively', () => {
74 const stray = critique('ghost', { p0: 10 });
75 const m = buildScoreMatrix(participants, [stray]);
76 expect(m.averagesReceived.p0).toBeNull();
77 });
78 });
79
80 function critiqueWithPredictions(
81 reviewer: string,
82 reviews: Record<string, { score: number; predicted?: number }>,
83 ): CritiqueRecord {
84 return {
85 round: 1,
86 reviewerParticipantId: reviewer,
87 reviewerModel: 'x/y',
88 reviews: Object.entries(reviews).map(([targetParticipantId, r]) => ({
89 label: 'A',
90 targetParticipantId,
91 weaknesses: [],
92 strengths: [],
93 score: r.score,
94 justification: '',
95 ...(r.predicted !== undefined ? { predictedPeerMean: r.predicted } : {}),
96 })),
97 usage: emptyUsage(),
98 latencyMs: 0,
99 };
100 }
101
102 describe('buildPredictionCells', () => {
103 it('pairs each prediction with the mean of the other reviewers scores', () => {
104 const critiques = [
105 critiqueWithPredictions('p0', { p2: { score: 3, predicted: 7 } }),
106 critiqueWithPredictions('p1', { p2: { score: 8 } }),
107 critiqueWithPredictions('p2', { p0: { score: 6 } }),
108 ];
109 const cells = buildPredictionCells(participants, critiques);
110 expect(cells).toHaveLength(1);
111 // p0 predicted 7 for p2; the only other reviewer of p2 (p1) gave 8.
112 expect(cells[0]).toMatchObject({ reviewerId: 'p0', targetId: 'p2', ownScore: 3, predicted: 7, actualPeerMean: 8 });
113 });
114
115 it('yields a null actual mean when no other reviewer scored the target', () => {
116 const critiques = [critiqueWithPredictions('p0', { p1: { score: 5, predicted: 6 } })];
117 const cells = buildPredictionCells(participants, critiques);
118 expect(cells[0]!.actualPeerMean).toBeNull();
119 });
120
121 it('skips reviews without predictions', () => {
122 const critiques = [critiqueWithPredictions('p0', { p1: { score: 5 } })];
123 expect(buildPredictionCells(participants, critiques)).toHaveLength(0);
124 });
125
126 it('compares predictions only against scores from the same round', () => {
127 const round1 = critiqueWithPredictions('p0', { p2: { score: 3, predicted: 7 } });
128 const round1Peer = critiqueWithPredictions('p1', { p2: { score: 8 } });
129 const round2Peer = { ...critiqueWithPredictions('p1', { p2: { score: 4 } }), round: 2 };
130 const cells = buildPredictionCells(participants, [round1, round1Peer, round2Peer]);
131 // The round-1 prediction must see round-1's 8, not round-2's 4.
132 expect(cells[0]!.actualPeerMean).toBe(8);
133 });
134 });
135
136 describe('classifyPrediction', () => {
137 const cell = (ownScore: number, predicted: number, actualPeerMean: number | null): PredictionCell => ({
138 reviewerId: 'p0',
139 targetId: 'p1',
140 ownScore,
141 predicted,
142 actualPeerMean,
143 });
144
145 it('is aligned when the reviewer roughly agrees with the council', () => {
146 expect(classifyPrediction(cell(7, 7, 7.5))).toBe('aligned');
147 });
148
149 it('is an informed contrarian when disagreeing but predicting the council accurately', () => {
150 expect(classifyPrediction(cell(3, 7.5, 8))).toBe('informed_contrarian');
151 });
152
153 it('is miscalibrated when disagreeing and mispredicting the council', () => {
154 expect(classifyPrediction(cell(3, 3.5, 8))).toBe('miscalibrated');
155 });
156
157 it('is null when there is no actual peer mean to compare against', () => {
158 expect(classifyPrediction(cell(3, 7, null))).toBeNull();
159 });
160 });
161
162 describe('predictionCalibration', () => {
163 it('averages absolute prediction error per reviewer and nulls the rest', () => {
164 const cells: PredictionCell[] = [
165 { reviewerId: 'p0', targetId: 'p1', ownScore: 5, predicted: 6, actualPeerMean: 8 },
166 { reviewerId: 'p0', targetId: 'p2', ownScore: 5, predicted: 7, actualPeerMean: 6 },
167 { reviewerId: 'p1', targetId: 'p2', ownScore: 5, predicted: 5, actualPeerMean: null },
168 ];
169 const calib = predictionCalibration(participants, cells);
170 expect(calib.p0).toBeCloseTo(1.5); // |6-8|=2, |7-6|=1
171 expect(calib.p1).toBeNull(); // its only cell has no actual mean
172 expect(calib.p2).toBeNull();
173 });
174 });
175