File size: 37,534 Bytes
6978d97
d249d5b
 
 
 
6978d97
 
 
04b4cff
6978d97
c4756f2
d249d5b
0fd9809
3a12290
 
c1f2130
3a12290
 
 
d249d5b
6978d97
e59af24
 
 
 
 
 
c4756f2
 
 
 
 
 
04b4cff
 
6978d97
 
b597ca9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c4756f2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e59af24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c4756f2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e59af24
 
 
 
 
 
 
 
 
c4756f2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e59af24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a12290
 
c1f2130
599471d
 
 
 
 
 
3a12290
 
 
 
 
478ae6c
 
b7439ff
 
 
 
 
3a12290
9b4cdbb
 
da4309d
 
731462e
 
 
cfd41ff
 
 
 
68d591a
 
 
 
 
 
 
 
 
 
e59af24
 
 
 
 
 
 
 
 
 
 
c4756f2
 
 
 
 
 
 
 
 
 
e59af24
 
 
 
 
04b4cff
 
 
 
 
 
 
 
 
 
 
 
3a12290
 
bca888a
3a12290
e59af24
 
 
 
 
 
 
3a12290
 
c5c4016
 
 
 
415ac43
03e2430
 
d249d5b
3a12290
 
 
 
 
478ae6c
 
3a12290
 
 
 
 
 
 
 
e59af24
04b4cff
 
 
 
 
 
 
 
e59af24
04b4cff
c1f2130
 
 
 
 
 
 
 
11542d9
 
 
 
 
 
 
 
 
2ed4959
 
11542d9
 
 
2ed4959
 
 
11542d9
 
c1f2130
 
 
 
 
 
 
 
d249d5b
 
415ac43
 
 
 
e59af24
 
 
 
 
415ac43
 
 
 
bca888a
59c7b6c
 
 
0d81559
 
59c7b6c
c4756f2
 
 
 
 
 
 
59c7b6c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415ac43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e59af24
 
 
 
415ac43
 
 
 
 
 
 
 
 
 
 
 
 
 
599471d
 
415ac43
 
 
 
cfd41ff
 
 
478ae6c
 
bca888a
e59af24
 
 
 
 
 
 
 
 
 
 
415ac43
3a12290
 
 
 
0fd9809
 
 
 
 
 
 
 
4010530
 
 
0fd9809
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c4756f2
 
 
0fd9809
c4756f2
 
 
0fd9809
b7439ff
 
b597ca9
 
 
 
 
e59af24
 
 
 
 
 
 
c4756f2
 
e59af24
 
 
c685464
 
 
 
 
 
cfd41ff
 
0fd9809
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e59af24
 
 
 
c5c4016
 
 
c4756f2
 
 
 
 
0fd9809
 
7a54021
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e59af24
 
 
7a54021
 
 
 
 
 
 
 
e59af24
 
 
7a54021
 
 
 
 
 
 
e59af24
 
7a54021
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
/**
 * Benchmark-first evaluation types, shared between the v2 view-layer reader
 * (lib/view-data.ts) and its UI consumers. The data-shaping functions that
 * used to live here were part of the legacy v1 (HF-JSON / duckdb) backend and
 * were removed once the producer pipeline took over aggregation.
 */

import type {
  BenchmarkCard,
  BenchmarkEvaluation,
  EvalLibrary,
  EvalTag,
  GenerationConfig,
  ModelInfo,
  SourceMetadata,
  SourceData,
  ScoreDetails,
  MetricConfig,
  EvaluationResult,
  ModelEvaluationSummary,
} from './benchmark-schema'
import type {
  ComparabilityStatus,
  EvalcardsAnnotations,
  RowAnnotations,
  SignalSummaries,
} from './backend-artifacts'
import type {
  CollectionAttachment,
  CollectionsSidecarEntry,
  ProtocolAxesByCollection,
  StudyRef,
} from './collections'

export type { BenchmarkCard }
export type { ModelEvaluationSummary }


/**
 * True when a result ran under the oracle answer-feedback arm of a
 * protocol-varied collection (the model was told when its answer was
 * correct). Assisted rows are shown but excluded from client-side ranking
 * by default β€” mirroring the backend, which never serves them a rank.
 */
export function isAssistedResult(protocolCondition: string | null | undefined): boolean {
  if (!protocolCondition) return false
  try {
    const parsed = JSON.parse(protocolCondition) as { feedback?: unknown } | null
    return parsed != null && typeof parsed === "object" && parsed.feedback === "answer_feedback"
  } catch {
    return false
  }
}

export interface ScoreSpread {
  /** How many scores the range spans. */
  n: number
  min: number
  max: number
}

/**
 * Where a model's runs on one page landed: the smallest and the largest,
 * and how many there were.
 *
 * Descriptive only, and deliberately so. The runs are different
 * CONFIGURATIONS (token budget, thinking tokens, effort, answer oracle),
 * not repeated draws of one quantity, so there is no population to
 * estimate and no sampling error to bound. An interval computed from
 * them would narrow as the study added design points while the observed
 * range stayed as wide, and would then be read as a claim about the
 * model's ability. The observed endpoints make no such claim.
 */
export function summariseScoreSpread(values: readonly number[]): ScoreSpread | null {
  const finite = values.filter((value) => Number.isFinite(value))
  if (finite.length === 0) return null
  return { n: finite.length, min: Math.min(...finite), max: Math.max(...finite) }
}

/** A parsed `judge_condition`. `judges` holds the canonical
 *  model ids of the LLM judges behind the number, `label` the source's own
 *  name for the channel. */
export interface JudgeCondition {
  judges: string[]
  label: string | null
}

/**
 * Parse the view's canonical `judge_condition` JSON
 * (`{"judges":[...],"label":"..."}`). NULL/absent means the source did not
 * disclose a judge β€” never "no judge" β€” so it parses to null and the row
 * is labelled as an ordinary result. Orthogonal to `protocol_condition`,
 * whose semantics (isAssistedResult above) are unchanged.
 */
export function parseJudgeCondition(
  raw: string | null | undefined,
): JudgeCondition | null {
  if (!raw) return null
  try {
    const parsed = JSON.parse(raw) as { judges?: unknown; label?: unknown } | null
    if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null
    // Trim, drop empties, dedupe. A blank id would render "judged by "
    // and a repeated one would inflate "mean of N judges" β€” both claim
    // something about the panel that isn't in the data.
    const judges = Array.isArray(parsed.judges)
      ? [
          ...new Set(
            parsed.judges
              .filter((judge): judge is string => typeof judge === "string")
              .map((judge) => judge.trim())
              .filter((judge) => judge.length > 0),
          ),
        ]
      : []
    const label =
      typeof parsed.label === "string" && parsed.label.trim().length > 0
        ? parsed.label.trim()
        : null
    if (judges.length === 0 && label == null) return null
    return { judges, label }
  } catch {
    return null
  }
}

/**
 * Row label for a judged result: the judge when a single model graded it,
 * the panel size when several did, plus the panel's display names for the
 * row's tooltip. `displayName` resolves a judge's canonical model id to its
 * registry display name (server-built, see BenchmarkEvalSummary
 * .judge_display_names) and falls back to the raw id. Returns null when the
 * source disclosed no judge at all.
 */
export function judgeConditionSummary(
  raw: string | null | undefined,
  displayName: (modelId: string) => string,
): { label: string; names: string[]; judges: string[] } | null {
  const condition = parseJudgeCondition(raw)
  if (!condition || condition.judges.length === 0) return null
  const names = condition.judges.map(displayName)
  return {
    label: names.length === 1 ? `judged by ${names[0]}` : `mean of ${names.length} judges`,
    names,
    // The raw ids behind the names. Two dated variants that folded into
    // one survivor resolve to the SAME display name, so the id is the
    // only thing that tells a reader which reading a row is.
    judges: condition.judges,
  }
}

/** One non-headline judge reading of a (model, metric) cell: the panel the
 *  producer did not pick, and the number it published. */
export interface JudgeReading {
  judge_condition: string
  score: number | null
}

/**
 * Judge treatment for one cell of the multi-metric matrix, which is a pivot
 * of headline readings only. `label` is the same "judged by …" / "mean of N
 * judges" wording the per-row leaderboard uses, and `tooltip` names the
 * panel's members and then lists every OTHER judge reading of the same
 * (model, metric) with its value β€” the pivot has no row to put those on, so
 * dropping them would hide a disagreement the page is meant to show.
 * `formatScore` renders an alternate's number the way the column renders its
 * cells. Returns null when nothing about the cell names a judge.
 */
export function judgeCellSummary(
  condition: string | null | undefined,
  alternates: JudgeReading[] | undefined,
  displayName: (modelId: string) => string,
  formatScore: (score: number) => string,
): { label: string; tooltip: string } | null {
  const headline = judgeConditionSummary(condition, displayName)
  const others = (alternates ?? [])
    .map((reading) => ({
      summary: judgeConditionSummary(reading.judge_condition, displayName),
      score: reading.score,
    }))
    .filter(
      (reading): reading is { summary: NonNullable<typeof reading.summary>; score: number | null } =>
        reading.summary !== null,
    )
  if (!headline && others.length === 0) return null

  const lines: string[] = []
  if (headline) {
    lines.push(
      headline.names.length > 1
        ? `Judges: ${headline.names.join(", ")}`
        : `Judge model id: ${headline.judges[0]}`,
    )
  }
  if (others.length > 0) {
    lines.push("Other judge readings, not ranked:")
    for (const other of others) {
      const value =
        typeof other.score === "number" && Number.isFinite(other.score)
          ? formatScore(other.score)
          : "β€”"
      lines.push(`${other.summary.label}: ${value}`)
    }
  }

  return {
    // No headline judge but alternates exist (the producer's pick was an
    // undisclosed-judge row): say so rather than showing nothing.
    label:
      headline?.label
      ?? `${others.length} other judge reading${others.length === 1 ? "" : "s"}`,
    tooltip: lines.join("\n"),
  }
}

/**
 * The matrix column the page ranks on: the eval's `primary_metric_id`, which
 * is the metric every other surface on the page summarises. A payload that
 * declares no primary metric, or names one no column carries, falls back to
 * the first root metric and then to the first column.
 */
export function primaryMetricColumnKey(
  metrics: Array<{ column_key: string; metric_id?: string; scope?: string }> | undefined,
  primaryMetricId: string | null | undefined,
): string | undefined {
  const all = metrics ?? []
  const roots = all.filter((metric) => metric.scope !== "subtask")
  const primary = primaryMetricId?.trim()
  const declared = primary
    ? roots.find((metric) => metric.column_key === primary || metric.metric_id === primary)
    : undefined
  return declared?.column_key ?? roots[0]?.column_key ?? all[0]?.column_key
}

/** A row the backend did not pick as its model's headline reading (a losing
 *  judge panel or protocol arm) β€” shown beneath the headline, never ranked.
 *  An absent field reads as "headline", which is what a payload assembled
 *  outside the view layer (a fixture, an embed) gets. */
export function isHeadlineResult(result: {
  is_headline?: boolean | null
}): boolean {
  return result.is_headline !== false
}

/**
 * Score standings for rows already in score order: one rank per ranked
 * row, ties sharing a rank, and 0 for every row the caller says takes no
 * rank (an assisted run, a losing judge panel, a secondary protocol
 * point). The standing is the model's position in the field, so it is
 * assigned once here and never recomputed from a row's display position.
 */
export function scoreStandings<T>(
  rows: T[],
  score: (row: T) => number,
  ranked: (row: T) => boolean,
): number[] {
  let currentRank = 0
  let previousScore: number | null = null
  let rankedCount = 0
  return rows.map((row) => {
    if (!ranked(row)) return 0
    rankedCount += 1
    const value = score(row)
    if (previousScore === null || Math.abs(value - previousScore) > 1e-9) {
      currentRank = rankedCount
      previousScore = value
    }
    return currentRank
  })
}

/** The identity a model's rows group under: the producer's route id when
 *  it resolved one, else whatever identifies the model at all. */
export function modelGroupKey(result: {
  model_route_id?: string
  model_info?: { id?: string; name?: string }
}): string {
  return result.model_route_id ?? result.model_info?.id ?? result.model_info?.name ?? ""
}

/**
 * A stable identity for one observation: the model, the source it came
 * from, the protocol point it ran at, the judge panel that scored it and
 * the source's own label for the number. Everything the page can use to
 * tell two readings of the same model apart, and nothing that depends on
 * where the reading currently sits in the table.
 */
export function observationKey(result: ModelResultForBenchmark): string {
  return [
    result.model_route_id ?? result.model_info?.id ?? result.model_info?.name ?? "",
    result.merged_source_slug ?? result.source_metadata?.source_name ?? "",
    result.collection_id ?? "",
    result.protocol_condition ?? "",
    result.judge_condition ?? "",
    result.metric_source_label ?? "",
  ].join("|")
}

/**
 * Group rows so that a model's non-headline readings (its judge panels and
 * protocol arms) stay attached beneath its headline row.
 *
 * Sorting a leaderboard must reorder these GROUPS, never the flattened row
 * list: reversing a flat list alone puts every secondary row above its own
 * headline, and an arbitrary key sort separates them from their model
 * entirely. A non-headline row whose model has no group yet opens one of
 * its own rather than attaching to an unrelated model above it.
 */
export function groupByHeadlineModel<T>(
  rows: readonly T[],
  resultOf: (row: T) => { is_headline?: boolean | null; model_route_id?: string; model_info?: { id?: string; name?: string } },
): T[][] {
  const groups: T[][] = []
  const openByModel = new Map<string, T[]>()
  for (const row of rows) {
    const result = resultOf(row)
    const key = modelGroupKey(result)
    const open = openByModel.get(key)
    if (open && !isHeadlineResult(result)) {
      open.push(row)
      continue
    }
    const group = [row]
    groups.push(group)
    openByModel.set(key, group)
  }
  return groups
}

/**
 * The direction the ranker's own row order already reads in: best first,
 * which is ASCENDING when the metric's better end is the low one. A score
 * sort that assumes "best first == descending" shows a lower-is-better
 * board upside down and then reverses the wrong way on the toggle.
 */
export function scoreSortBaseDirection(lowerIsBetter?: boolean | null): "asc" | "desc" {
  return lowerIsBetter ? "asc" : "desc"
}

/**
 * Score-ordered rows for a user-chosen direction. Takes the GROUPS the
 * ranker produced (each a headline row plus the judge / protocol readings
 * beneath it) so a reversal moves whole models β€” reversing the flat row
 * list would put every secondary reading above its own headline.
 */
export function orderGroupsByScore<T>(
  groups: readonly T[][],
  direction: "asc" | "desc",
  lowerIsBetter?: boolean | null,
): T[] {
  return direction === scoreSortBaseDirection(lowerIsBetter)
    ? groups.flat()
    : [...groups].reverse().flat()
}

/**
 * Composite comparison view: one score per (model, sub-eval) across a
 * suite's child evaluations, for the models x sub-evals matrix.
 *
 * Headline rows only. The cell is written once per matching row, so
 * without the filter a later judge panel or protocol arm overwrites the
 * model's summary reading β€” and that overwritten number is then averaged
 * across the suite and ranked as the model's standing.
 */
export function compositeScoresByModel(
  subSummaries: ReadonlyArray<{
    evaluation_name: string
    model_results: ModelResultForBenchmark[]
  }>,
): Map<string, { name: string; developer: string; scores: Map<string, number | null> }> {
  const modelScores = new Map<
    string,
    { name: string; developer: string; scores: Map<string, number | null> }
  >()
  for (const sub of subSummaries) {
    for (const result of sub.model_results) {
      if (!isHeadlineResult(result)) continue
      const id = result.model_info.id
      const existing = modelScores.get(id) ?? {
        name: result.model_info.name,
        developer: result.model_info.developer ?? "",
        scores: new Map<string, number | null>(),
      }
      existing.scores.set(sub.evaluation_name, result.score)
      modelScores.set(id, existing)
    }
  }
  return modelScores
}

export interface ModelResultForBenchmark {
  model_info: ModelInfo
  model_route_id?: string
  /**
   * model-resolution-rework: server-provided group canonical id. Used as
   * the routing fallback when `model_route_id` is absent β€” replaces the
   * old client-side family-route computation (since removed).
   */
  model_group_id?: string
  score: number
  score_details: ScoreDetails
  evaluation_timestamp: string
  source_metadata: SourceMetadata
  source_data: BenchmarkEvaluation['source_data']
  /** Per-result verification flag; mirrors `result.is_verified_evaluator`. */
  is_verified_evaluator?: boolean
  /** De-aliased evaluator identity (registry canonical display name when
   *  the org resolves, raw string otherwise). Preferred for the Source
   *  label so upstream spelling drift never leaks into display; the raw
   *  source_metadata strings stay available for provenance. */
  evaluator_display_name?: string
  result: EvaluationResult
  /** URL to the underlying record JSON in the upstream HF dataset, when known. */
  source_record_url?: string
  /** Deep-link to the raw EEE_datastore source record this score came from, when known. */
  eee_record_url?: string
  /** Merged-page rows only (set by lib/merged-adapter): the observation's
   *  source composite slug, used for the ?source= row pre-highlight. */
  merged_source_slug?: string
  /** Merged-page rows only: how `score` was rescaled to sit on the page's
   *  scale (see MergedObservationRow.scale_harmonized). */
  scale_harmonized?: "mul100" | "div100" | "of_total"
  scale_harmonized_total?: number
  /** Collections (see the backend's collections spec, warehouse-outputs section): submission-channel id of
   *  the row's representative fact row β€” key into the snapshot's
   *  `collections.json` sidecar. */
  collection_id?: string
  /** Protocol point for protocol-varied collections: canonical sorted-key
   *  JSON typed by the collection's declared `protocol_axes`; absent/null
   *  for ordinary rows. The view ships one row per protocol point; rows
   *  whose reserved `feedback` key is `answer_feedback` are
   *  shown-but-not-ranked (backend already emits NULL position for them). */
  protocol_condition?: string | null
  /** Canonical `{"judges":[...],"label":"..."}` JSON for a
   *  judged result; absent/null when the source disclosed no judge. */
  judge_condition?: string | null
  /** True on the one row per (composite, benchmark, metric, model) the
   *  producer picked as the page's summary reading. The view layer always
   *  projects it, deriving it from the producer's ranking on a snapshot
   *  that predates the column β€” see isHeadlineResult. */
  is_headline?: boolean | null
  /** The source's own label for the published number (`gpt_score`).
   *  Display and provenance only β€” never a key. */
  metric_source_label?: string | null
  /** The harness that produced the run, and its version when the source
   *  named one. Carried on the row because the reproducibility slots ask
   *  which harness a re-runner would have to obtain and pin. */
  eval_library?: EvalLibrary | null
  /** How the score was produced: `generative` when the model wrote the
   *  answer, `log_prob` when the harness scored likelihoods over fixed
   *  choices. The producer's own classification, so it outranks anything
   *  inferred from a row's raw fields; absent on a snapshot predating the
   *  column, and null when the producer could not classify the row. */
  scoring_mode?: string | null
  /** The row's comparability verdict; only `ok` groups were assessed. */
  comparability_status?: ComparabilityStatus | null
  /** The number the source published, before any canonical-scale
   *  conversion applied to `score`. */
  score_published?: number
  aggregate_components?: Array<{
    evaluation_id: string
    composite_benchmark_key: string
    composite_benchmark_name: string
    score: number
    normalized_score: number
    evaluation_timestamp: string
    source_name?: string
    source_type: SourceMetadata["source_type"]
    source_organization_name: string
    evaluator_relationship: SourceMetadata["evaluator_relationship"]
  }>
}

export interface BenchmarkEvalSummary extends SignalSummaries {
  evaluation_name: string
  /** Registry display names for the judge model ids named in this page's
   *  judge conditions, keyed by the raw id the condition carries (which
   *  can be a dated variant that folded into the named model). Built
   *  server-side from models_view; absent when no row on the page names a
   *  judge, and missing an entry when models_view has no matching row β€”
   *  the label then falls back to the raw id. */
  judge_display_names?: Record<string, string>
  /** URL-safe slug derived from evaluation_name */
  evaluation_id: string
  /** True when this summary was adapted from a merged all-sources payload
   *  (lib/merged-adapter) β€” lets shared UI phrase observation-grain rows
   *  correctly ("N results Β· M models", not "N of M"). */
  merged_view?: true
  canonical_display_name?: string
  composite_benchmark_key: string
  composite_benchmark_name: string
  derived_tags?: EvalTag[]
  metric_config: MetricConfig
  model_results: ModelResultForBenchmark[]
  models_count: number
  /** Unique evaluator organisation names */
  evaluator_names: string[]
  /** Subset of `evaluator_names` that are validated submitters (badge). */
  verified_evaluator_names?: string[]
  source_types: SourceMetadata["source_type"][]
  latest_source_name?: string
  third_party_ratio: number
  missing_generation_config_count: number
  best_model: { name: string; score: number } | null
  worst_model: { name: string; score: number } | null
  avg_score: number
  /** avg_score normalised to 0-1 using metric_config.min/max_score */
  avg_score_norm: number | null
  /** Rich benchmark card from the metadata/ folder, when available */
  benchmark_card?: BenchmarkCard
  is_aggregated?: boolean
  aggregate_sources?: Array<{
    evaluation_id: string
    composite_benchmark_key: string
    composite_benchmark_name: string
    models_count: number
    avg_score_norm: number | null
  }>
  /** Tags from the pipeline (domains, languages, tasks) */
  tags?: { domains: string[]; languages: string[]; tasks: string[] }
  /** Number of distinct metrics for this benchmark */
  metrics_count?: number
  /** Names of all metrics */
  metric_names?: string[]
  /** Instance-level data availability */
  instance_data?: { available: boolean; url_count: number; sample_urls: string[]; models_with_loaded_instances: number }
  /** Canonical benchmark id (the registry-resolved benchmark). Drives
   *  benchmark-card lookups regardless of slice/composite axis. */
  benchmark_id?: string
  /** Family display name. */
  benchmark_family_name?: string
  /** Composite (leaderboard) slug β€” e.g. "wasp", "helm-classic". */
  composite_slug?: string
  /** Composite display name β€” e.g. "WASP", "HELM Classic". */
  composite_display_name?: string
  /** Curated multi-benchmark family slug (e.g. "mmlu"), defaults to
   *  benchmark id for singletons. */
  family_id?: string
  /** Family display, post-cutover canonical name. */
  family_display_name?: string
  /** Parent benchmark id β€” populated when this row is a slice of a
   *  root benchmark; null for non-slice rows. */
  parent_benchmark_id?: string
  /** True when this row is a within-benchmark slice cut. */
  is_slice?: boolean
  /** Source dataset metadata from the pipeline */
  source_data?: SourceData
  /** Best raw score reported in the eval summary list */
  top_score?: number
  /** Count of nested subtasks reported for the benchmark */
  subtasks_count?: number
  /** Whether this row is a summary/rollup score for a composite */
  is_summary_score?: boolean
  /** Evaluation_ids this benchmark summary is composed of. */
  constituent_evaluation_ids?: string[]
  /** Canonical benchmark-level metrics from root metrics[] */
  root_metrics?: BenchmarkSummaryMetric[]
  /** Canonical benchmark subdivisions from subtasks[] */
  subtasks?: BenchmarkSummarySubtask[]
  /** The metric the page ranks on (`evals_view.primary_metric_id`) β€” the one
   *  every other surface here summarises. The multi-metric matrix sorts and
   *  charts this column rather than whichever measure happens to come first.
   *  Absent on payloads assembled outside the view layer. */
  primary_metric_id?: string
  /** Matrix columns for multi-metric benchmark leaderboards */
  leaderboard_metrics?: BenchmarkLeaderboardMetric[]
  /** Matrix rows for multi-metric benchmark leaderboards */
  leaderboard_rows?: BenchmarkLeaderboardRow[]
  evalcards?: { annotations?: EvalcardsAnnotations }
  /** Curated protocol-varied collection attachment. Per-source-only:
   *  built server-side from the
   *  collections.json sidecar; the merged adapter never sets it, which
   *  gates every collection surface off merged summaries. Per-source
   *  embeds carry it and render the study surfaces deliberately. */
  collection?: CollectionAttachment
  /** Curated studies the visible rows come from. Attribution only, and
   *  the one study surface a merged summary carries. */
  study_refs?: StudyRef[]
  /** Declared protocol axes keyed by collection id, for a page whose rows
   *  span several collections. Descriptor lookup only: it gives a row's
   *  numbers their unit and says which axes apply to which row. */
  protocol_axes_by_collection?: ProtocolAxesByCollection
  /** Source ↔ merged switcher data for per-source pages.
   *  Absent when the page has neither a merged page nor sibling sources,
   *  and on old snapshots. */
  source_options?: EvalSourceOptions
}

export interface EvalSourceOption {
  /** Per-source eval page id (URL-encoded, straight off evals_view). */
  evaluation_id: string
  composite_slug?: string
  composite_display_name?: string
  models_count?: number
}

export interface EvalSourceOptions {
  /** Merged-page id to navigate to, or null when NO merged page exists β€”
   *  the switcher must never navigate to a nonexistent merged page. */
  merged_evaluation_id: string | null
  sources: EvalSourceOption[]
}

export interface BenchmarkSummaryMetric {
  metric_summary_id: string
  metric_name: string
  display_name: string
  canonical_display_name?: string
  metric_key?: string
  lower_is_better: boolean
  models_count: number
  top_score?: number
  unit?: string
}

export interface BenchmarkSummarySubtask {
  subtask_key: string
  subtask_name: string
  display_name: string
  canonical_display_name?: string
  metrics: BenchmarkSummaryMetric[]
}

export interface BenchmarkLeaderboardMetric {
  column_key: string
  /** Registry metric id. Equal to `column_key` on root metrics; a subtask
   *  entry keys its column "<metric_id>::<slice>" and carries the base id
   *  here. Matches `evals_view.primary_metric_id`. */
  metric_id?: string
  metric_summary_id: string
  metric_name: string
  display_name: string
  canonical_display_name?: string
  lower_is_better: boolean
  unit?: string
  scope: "root" | "subtask"
  subtask_key?: string
  subtask_name?: string
}

export interface BenchmarkLeaderboardRow {
  model_info: ModelInfo
  model_route_id?: string
  /** model-resolution-rework: server group id, routing fallback. */
  model_group_id?: string
  evaluation_timestamp: string
  source_metadata: SourceMetadata
  source_data: BenchmarkEvaluation["source_data"]
  values: Record<string, number | null>
  /** Merged-page rows only: hover text for a score that was rescaled to
   *  sit on the page's scale (see scaleHarmonizedTitle). */
  rescaled_title?: string
  /** Per-column verified-evaluator flag, keyed identically to `values`. */
  verified?: Record<string, boolean>
  annotations_by_metric?: Record<string, RowAnnotations | null | undefined>
  /** Per-column comparability verdict, keyed identically to `values`.
   *  Prebaked matrix cells carry no annotation struct, so this is what
   *  lets a matrix cell render "not assessable" instead of silence. */
  comparability_status_by_metric?: Record<string, ComparabilityStatus | null | undefined>
  /** Per-column `judge_condition` of the headline reading the cell shows,
   *  keyed identically to `values`. */
  judge_condition_by_metric?: Record<string, string | null | undefined>
  /** Per-column non-headline judge readings of the same (model, metric).
   *  The pivot keeps one row per model, so these ride the cell rather than
   *  becoming rows of their own. */
  judge_alternates_by_metric?: Record<string, JudgeReading[] | undefined>
  metrics_present: number
}

export type BenchmarkEvalListItem = Omit<BenchmarkEvalSummary, "model_results">

// ---------------------------------------------------------------------------
// Merged benchmark view (merged-benchmark-view spec F1) β€” one page per
// resolved canonical benchmark, merging every publishing source at
// observation grain. Backed by `merged_evals_view.parquet` plus a live
// query over `eval_results_view`.
// ---------------------------------------------------------------------------

/** 'flagged' rows have no canonical-scale score (never guessed); 'no_bounds'
 *  rows pass the raw score through unconverted; 'curated' rows were
 *  multiplied by a registry-declared factor (e.g. raw 1-10 onto 0-1). */
export type MergedScaleConversion = "none" | "div100" | "mul100" | "curated" | "flagged" | "no_bounds"

export interface MergedAggregateSource {
  /** Per-source eval page id; null for slice-only sources (no top-level page target). */
  evaluation_id: string | null
  composite_slug: string
  composite_display_name: string
  models_count: number
  results_count: number
  /** False = excluded from the default view (source reports only other metrics). */
  reports_preferred: boolean
  /** True = source reports slice-level results only (disclosure note). */
  slice_only: boolean
}

export interface MergedMetricOption {
  metric_id: string
  display_name: string
  results_count: number
  models_count: number
  sources_count: number
  lower_is_better: boolean | null
}

export interface MergedSliceOption {
  slice_id: string
  display_name: string
}

export interface MergedBestResult {
  model_name: string | null
  model_key: string | null
  score: number | null
  score_canonical: number | null
  composite_slug: string | null
  evaluation_id: string | null
}

/** One (model, source) observation row. Echo republications stay visible β€”
 *  the merged accessor never dedupes by model identity (spec design pt 3). */
export interface MergedObservationRow {
  model_info: ModelInfo
  model_route_id?: string
  model_key?: string
  /** The observation's per-source evaluation_id (two-segment) β€” Source link target. */
  evaluation_id: string
  composite_slug: string
  composite_display_name?: string
  score: number
  /** Score on the metric's registry canonical scale; null for flagged rows. */
  score_canonical: number | null
  scale_conversion: MergedScaleConversion | null
  evaluation_timestamp: string
  source_metadata: SourceMetadata
  /** The row's own upstream dataset provenance (repo, url, version,
   *  sample count). Absent when the fact row carries none. */
  source_data?: SourceData
  generation_config?: GenerationConfig
  /** Same meaning as on ModelResultForBenchmark: the harness the
   *  reproducibility slots ask a re-runner to obtain and pin. */
  eval_library?: EvalLibrary | null
  is_verified_evaluator?: boolean
  /** De-aliased evaluator identity (canonical display when resolvable). */
  evaluator_display_name?: string
  /** Collections: submission-channel id of the observation's source row. */
  collection_id?: string
  /** Protocol point (canonical sorted-key JSON) for protocol-varied
   *  collections; absent/null for ordinary observations. */
  protocol_condition?: string | null
  /** Same meanings as on ModelResultForBenchmark. Merged pages
   *  pool one observation per (model, source), so only headline rows
   *  belong in the pool (see lib/merged-adapter). */
  judge_condition?: string | null
  is_headline?: boolean | null
  metric_source_label?: string | null
  comparability_status?: ComparabilityStatus | null
  /** Same meaning as on ModelResultForBenchmark. */
  scoring_mode?: string | null
  /** The number the source published, before any canonical-scale
   *  conversion applied to `score`. */
  score_published?: number
  /** Set when the merged accessor moved this row's `score_canonical`
   *  between the fraction and percent scales so it sits on the same scale
   *  as the page's other sources (see harmonizeUnboundedScales). Only
   *  'no_bounds' rows are ever moved. 'of_total' rows were published as
   *  raw totals and divided by the benchmark's published maximum. */
  scale_harmonized?: "mul100" | "div100" | "of_total"
  /** The published maximum an 'of_total' row was divided by. */
  scale_harmonized_total?: number
}

export interface MergedBenchmarkSummary {
  /** Discriminator vs the per-source BenchmarkEvalSummary payload. */
  merged: true
  /** Single-segment percent-encoded benchmark id (never contains %2F). */
  evaluation_id: string
  benchmark_id: string
  display_name: string
  family_id?: string | null
  family_display_name?: string | null
  /** 'slice' = benchmark has no top-level data; page shows a slice selector. */
  grain: "benchmark" | "slice"
  preferred_metric_id: string
  preferred_metric_display_name: string
  preferred_from_registry: boolean
  lower_is_better: boolean
  /** Counts at default-metric grain (spec P5). */
  sources_count: number
  all_sources_count: number
  results_count: number
  models_count: number
  best_result: MergedBestResult | null
  aggregate_sources: MergedAggregateSource[]
  metrics: MergedMetricOption[]
  /** Non-null only for grain='slice'. */
  slices: MergedSliceOption[] | null
  /** Metric actually queried for `results` (defaults to preferred). */
  selected_metric_id: string
  selected_lower_is_better: boolean
  /** For grain='slice': the slice actually queried (defaults to the first). */
  selected_slice_id: string | null
  results: MergedObservationRow[]
  /** Registry display names for the judge ids named by these rows, keyed
   *  by the raw id β€” same map and same fallback rule as
   *  BenchmarkEvalSummary.judge_display_names. */
  judge_display_names?: Record<string, string>
  /** The benchmark's card, sourced from a per-source instantiation that
   *  authored one (preferring a source that reports the preferred metric). */
  benchmark_card?: BenchmarkCard | null
  /** `collections.json` entries for the curated collections these rows
   *  belong to, keyed by collection_id. Lets the reader name the study a
   *  row comes from and give its protocol numbers their declared units
   *  without a second fetch. */
  collections?: Record<string, CollectionsSidecarEntry>
}

/**
 * Collapse leaderboard rows that describe the same model under two
 * source attributions (typical: one record with `developer: "OpenAI"`,
 * another with `developer: "unknown"` from a source that didn't carry
 * the developer field, both pointing at the same physical model). We
 * only merge when every shared score column is byte-equal across the
 * duplicates β€” that guarantees we never mask a legitimate second run
 * that happens to share a name. When merging, we keep the attribution
 * that actually identifies a developer.
 */
function devAttributionScore(developer: string | undefined | null): number {
  if (!developer) return 0
  const lower = developer.trim().toLowerCase()
  if (!lower || lower === "unknown") return 0
  return 1
}

function routeAttributionScore(routeId: string | undefined | null): number {
  if (!routeId) return 0
  const lower = routeId.toLowerCase()
  return lower.startsWith("unknown%2f") || lower.startsWith("unknown/") ? 0 : 1
}

export function dedupeLeaderboardRowsByModelIdentity(
  rows: BenchmarkLeaderboardRow[],
): BenchmarkLeaderboardRow[] {
  if (rows.length < 2) return rows
  const groups = new Map<string, BenchmarkLeaderboardRow[]>()
  for (const row of rows) {
    const name = (row.model_info?.name ?? "").trim().toLowerCase()
    if (!name) continue
    const bucket = groups.get(name)
    if (bucket) bucket.push(row)
    else groups.set(name, [row])
  }

  const result: BenchmarkLeaderboardRow[] = []
  const consumed = new WeakSet<BenchmarkLeaderboardRow>()
  for (const row of rows) {
    if (consumed.has(row)) continue
    const name = (row.model_info?.name ?? "").trim().toLowerCase()
    const bucket = name ? groups.get(name) : null
    if (!bucket || bucket.length < 2) {
      result.push(row)
      continue
    }

    // Verify per-column score agreement before merging. Any conflict
    // (two different numbers for the same column key) means the rows
    // are distinct runs that happen to share a model name β€” leave them.
    let conflict = false
    const valuesByKey: Record<string, number> = {}
    outer: for (const candidate of bucket) {
      for (const [key, raw] of Object.entries(candidate.values ?? {})) {
        if (typeof raw !== "number" || !Number.isFinite(raw)) continue
        if (key in valuesByKey) {
          if (valuesByKey[key] !== raw) {
            conflict = true
            break outer
          }
        } else {
          valuesByKey[key] = raw
        }
      }
    }
    if (conflict) {
      result.push(row)
      continue
    }

    // Pick the canonical row: best developer attribution, then best
    // route id, then most populated values map as a tiebreaker.
    const canonical = [...bucket].sort((a, b) => {
      const devDelta = devAttributionScore(b.model_info?.developer) - devAttributionScore(a.model_info?.developer)
      if (devDelta !== 0) return devDelta
      const routeDelta = routeAttributionScore(b.model_route_id) - routeAttributionScore(a.model_route_id)
      if (routeDelta !== 0) return routeDelta
      return Object.keys(b.values ?? {}).length - Object.keys(a.values ?? {}).length
    })[0]

    const mergedValues: Record<string, number | null> = { ...(canonical.values ?? {}) }
    const mergedAnnotations: Record<string, unknown> = { ...(canonical.annotations_by_metric ?? {}) }
    const mergedStatus: Record<string, unknown> = {
      ...(canonical.comparability_status_by_metric ?? {}),
    }
    for (const candidate of bucket) {
      if (candidate === canonical) continue
      for (const [key, raw] of Object.entries(candidate.values ?? {})) {
        if (mergedValues[key] == null && raw != null) mergedValues[key] = raw
      }
      for (const [key, ann] of Object.entries(candidate.annotations_by_metric ?? {})) {
        if (mergedAnnotations[key] == null && ann != null) mergedAnnotations[key] = ann
      }
      for (const [key, status] of Object.entries(candidate.comparability_status_by_metric ?? {})) {
        if (mergedStatus[key] == null && status != null) mergedStatus[key] = status
      }
      consumed.add(candidate)
    }

    result.push({
      ...canonical,
      values: mergedValues,
      annotations_by_metric: mergedAnnotations as typeof canonical.annotations_by_metric,
      comparability_status_by_metric:
        mergedStatus as typeof canonical.comparability_status_by_metric,
    })
    consumed.add(canonical)
  }
  return result
}