File size: 5,590 Bytes
f073e7a
 
 
 
 
 
 
 
 
 
 
06313c1
d249d5b
06313c1
 
 
 
 
bc08b3b
 
 
 
 
f073e7a
 
 
 
 
bc08b3b
06313c1
 
f073e7a
 
 
 
 
 
 
 
 
 
 
06313c1
 
d249d5b
06313c1
 
 
 
 
 
 
d249d5b
06313c1
 
f073e7a
 
06313c1
bc08b3b
 
 
06313c1
 
 
bc08b3b
 
 
 
 
 
d249d5b
06313c1
 
 
 
 
bc08b3b
 
 
 
 
f073e7a
 
 
 
 
 
 
 
 
d249d5b
f073e7a
06313c1
f073e7a
06313c1
 
 
 
 
 
 
f073e7a
 
 
 
 
 
 
 
 
 
d249d5b
f073e7a
d249d5b
f073e7a
 
d249d5b
f073e7a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bc08b3b
 
 
 
 
 
 
 
f073e7a
 
 
 
 
06313c1
 
bc08b3b
f073e7a
 
 
 
 
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
import type {
  EvalHierarchy,
  HierarchyComposite,
  HierarchyFamily,
} from "@/lib/backend-artifacts"

export interface HierarchyEvalLocation {
  familyKey: string
  familyDisplayName: string
  compositeKey?: string
  compositeDisplayName?: string
  /** Resolved leaf benchmark — set when `eval_summary_id` lives inside a
   *  hierarchy benchmark's `constituent_evaluation_ids`. Lets the model-detail
   *  plotbox builder bucket every eval row that resolves to the same
   *  benchmark together (so a standalone like Fibble Arena with N
   *  per-split eval rows renders as one plotbox, not N). */
  benchmarkKey?: string
  benchmarkDisplayName?: string
  /** Curated category tags (data/benchmarks/categories.json vocabulary)
   *  for the leaf benchmark this eval belongs to, falling back to its
   *  composite/family. Decorated by `decorateHierarchyDerivedTags` at
   *  hydration time. */
  tags?: string[]
}

interface FamilyAppearance {
  family: HierarchyFamily
  composite?: HierarchyComposite
  benchmarkTags?: string[]
  benchmarkKey?: string
  benchmarkDisplayName?: string
}

function findComposite(
  family: HierarchyFamily,
  evalSummaryId: string,
): HierarchyComposite | undefined {
  const composites = family.composites
  if (!composites?.length) {
    return undefined
  }
  const sourcePrefix = evalSummaryId.split("%2F")[0]
  const byPrefix = composites.find((composite) => composite.key === sourcePrefix)
  if (byPrefix) return byPrefix
  // Fallback: scan benchmarks' `constituent_evaluation_ids`. The clean-hierarchy
  // post-processor synthesises composites for split families (Fibble
  // Arena's per-N-lies splits, CapArena-Auto, AgentHarm) whose
  // children carry mixed source prefixes (`fibble1-arena%2F…`,
  // `fibble2-arena%2F…`, …) that wouldn't match the synthetic
  // composite's key by prefix alone.
  return composites.find((composite) =>
    composite.benchmarks?.some((bench) =>
      bench.constituent_evaluation_ids?.includes(evalSummaryId),
    ),
  )
}

function findBenchmark(
  family: HierarchyFamily,
  composite: HierarchyComposite | undefined,
  evalSummaryId: string,
):
  | { key: string; displayName: string; tags?: string[] }
  | undefined {
  const benchmarks = [
    ...(composite?.benchmarks ?? []),
    ...(family.standalone_benchmarks ?? []),
    ...(family.benchmarks ?? []),
  ]
  for (const benchmark of benchmarks) {
    if (benchmark.constituent_evaluation_ids?.includes(evalSummaryId)) {
      return {
        key: benchmark.key,
        displayName: benchmark.display_name,
        tags: benchmark.derivedTags,
      }
    }
  }
  return undefined
}

function buildAppearancesIndex(
  hierarchy: EvalHierarchy | null | undefined,
): Map<string, FamilyAppearance[]> {
  const index = new Map<string, FamilyAppearance[]>()
  if (!hierarchy?.families) {
    return index
  }

  for (const family of hierarchy.families) {
    for (const evalSummaryId of family.constituent_evaluation_ids ?? []) {
      const composite = findComposite(family, evalSummaryId)
      const bench = findBenchmark(family, composite, evalSummaryId)
      const list = index.get(evalSummaryId) ?? []
      list.push({
        family,
        composite,
        benchmarkTags: bench?.tags,
        benchmarkKey: bench?.key,
        benchmarkDisplayName: bench?.displayName,
      })
      index.set(evalSummaryId, list)
    }
  }

  return index
}

/**
 * Build a lookup that maps each `eval_summary_id` to the family / composite
 * that contains it in `hierarchy.json`. The model-detail benchmark grouping
 * needs this because the eval row's own `family_id` is null for some evals
 * (e.g. CySE2 composites) and points at the leaf instead of the parent for
 * singleton families. The hierarchy is the only source that captures
 * curated family→composite→benchmark grouping.
 *
 * Some constituent_evaluation_ids appear in multiple families. The optional
 * `preferFamilyKey(evalSummaryId)` callback lets the caller pick the canonical
 * family — typically by passing in the eval row's own `family_id`. When no
 * preference is given, the first family encountered wins.
 */
export function buildHierarchyEvalIndex(
  hierarchy: EvalHierarchy | null | undefined,
  preferFamilyKey?: (evalSummaryId: string) => string | null | undefined,
): Map<string, HierarchyEvalLocation> {
  const appearances = buildAppearancesIndex(hierarchy)
  const index = new Map<string, HierarchyEvalLocation>()

  for (const [evalSummaryId, candidates] of appearances) {
    let chosen = candidates[0]
    if (candidates.length > 1) {
      const preferredKey = preferFamilyKey?.(evalSummaryId)?.toString().trim()
      if (preferredKey) {
        const match = candidates.find((c) => c.family.key === preferredKey)
        if (match) {
          chosen = match
        }
      }
    }

    // Tag preference order for the leaf: benchmark > composite > family.
    // We want the most specific tags available so the model-view bucketing
    // groups by leaf semantics, not by the family-level union.
    const tags =
      chosen.benchmarkTags && chosen.benchmarkTags.length > 0
        ? chosen.benchmarkTags
        : chosen.composite?.derivedTags ?? chosen.family.derivedTags ?? []

    index.set(evalSummaryId, {
      familyKey: chosen.family.key,
      familyDisplayName: chosen.family.display_name,
      compositeKey: chosen.composite?.key,
      compositeDisplayName: chosen.composite?.display_name,
      benchmarkKey: chosen.benchmarkKey,
      benchmarkDisplayName: chosen.benchmarkDisplayName,
      tags,
    })
  }

  return index
}