j-chim commited on
Commit
2355bb3
Β·
1 Parent(s): 478ae6c

format fixes - validated evals

Browse files
app/evaluators/[...id]/page.tsx CHANGED
@@ -4,15 +4,14 @@ import { Suspense, useCallback, useEffect, useMemo, useState } from "react"
4
  import { useParams, useRouter, useSearchParams } from "next/navigation"
5
  import { ArrowLeft, Search } from "lucide-react"
6
 
7
- import { EvalCard } from "@/components/eval-card"
8
- import { InfiniteScrollSentinel } from "@/components/infinite-scroll"
9
  import { Navigation } from "@/components/navigation"
10
  import { VerifiedBadge } from "@/components/signals/verified-badge"
11
- import { fetchEvalList } from "@/lib/dashboard-data-client"
 
 
12
  import type { BenchmarkEvalListItem } from "@/lib/eval-processing"
13
- import { getEvalsForEvaluator } from "@/lib/evaluators"
14
-
15
- const PAGE_SIZE = 24
16
 
17
  function EvaluatorDetailInner() {
18
  const params = useParams()
@@ -29,70 +28,81 @@ function EvaluatorDetailInner() {
29
  }, [params.id])
30
 
31
  const [allEvals, setAllEvals] = useState<BenchmarkEvalListItem[]>([])
 
 
32
  const [loading, setLoading] = useState(true)
33
  const [error, setError] = useState<string | null>(null)
34
  const [searchQuery, setSearchQuery] = useState("")
35
- const [visibleCount, setVisibleCount] = useState(PAGE_SIZE)
 
36
 
37
  useEffect(() => {
38
- fetchEvalList()
39
  .then((list) => setAllEvals(list.evals))
40
  .catch((err) => {
41
  console.error(err)
42
  setError("Failed to load evaluations")
43
  })
44
- .finally(() => setLoading(false))
 
 
 
 
 
 
 
 
45
  }, [])
46
 
 
 
 
 
 
 
 
 
 
47
  const { name, isVerified, evals } = useMemo(
48
  () => getEvalsForEvaluator(allEvals, slug, { verifiedOnly }),
49
  [allEvals, slug, verifiedOnly],
50
  )
51
 
52
- const filteredEvals = useMemo(() => {
53
- const query = searchQuery.trim().toLowerCase()
54
- const list = query
55
- ? evals.filter((ev) => {
56
- const haystacks = [
57
- ev.evaluation_name,
58
- ev.family_display_name,
59
- ev.composite_benchmark_name,
60
- ]
61
- return haystacks.some((v) => v?.toLowerCase().includes(query))
62
- })
63
- : evals
64
- return list.slice().sort((a, b) => a.evaluation_name.localeCompare(b.evaluation_name))
65
- }, [evals, searchQuery])
66
-
67
- // Quantified facts for the header, derived from the org's owned evals.
68
- // familyCount = distinct benchmark families covered; verifiedCount = evals
69
- // where this org is a verified evaluator.
 
 
 
 
70
  const { familyCount, verifiedCount } = useMemo(() => {
71
- const families = new Set<string>()
72
  let verified = 0
73
  for (const ev of evals) {
74
- const fam = ev.family_display_name?.trim()
75
- if (fam) families.add(fam)
76
  if (name && (ev.verified_evaluator_names ?? []).includes(name)) verified += 1
77
  }
78
- return {
79
- familyCount: families.size,
80
- verifiedCount: verified,
 
 
81
  }
82
- }, [evals, name])
83
-
84
- useEffect(() => {
85
- setVisibleCount(PAGE_SIZE)
86
- }, [searchQuery, slug, verifiedOnly])
87
-
88
- const visibleEvals = useMemo(
89
- () => filteredEvals.slice(0, visibleCount),
90
- [filteredEvals, visibleCount],
91
- )
92
- const hasMore = visibleCount < filteredEvals.length
93
- const handleLoadMore = useCallback(() => {
94
- setVisibleCount((current) => Math.min(current + PAGE_SIZE, filteredEvals.length))
95
- }, [filteredEvals.length])
96
 
97
  const handleBack = useCallback(() => {
98
  router.push(verifiedOnly ? "/evals?groupBy=evaluator&verified=1" : "/evals?groupBy=evaluator")
@@ -164,8 +174,8 @@ function EvaluatorDetailInner() {
164
  <span>{verifiedCount} verified</span>
165
  </div>
166
  <p className="ec-page-lede">
167
- Reported <strong>{filteredEvals.length.toLocaleString()}</strong>{" "}
168
- {filteredEvals.length === 1 ? "evaluation" : "evaluations"} across{" "}
169
  <strong>{familyCount.toLocaleString()}</strong>{" "}
170
  {familyCount === 1 ? "benchmark family" : "benchmark families"}
171
  {verifiedCount > 0 && (
@@ -179,7 +189,7 @@ function EvaluatorDetailInner() {
179
  <div className="ec-page-meta mt-2">
180
  <div className="ec-page-meta-item">
181
  <span className="ec-page-meta-item-l">Evaluations</span>
182
- <span className="ec-page-meta-item-v">{filteredEvals.length.toLocaleString()}</span>
183
  </div>
184
  <div className="ec-page-meta-item">
185
  <span className="ec-page-meta-item-l">Verified</span>
@@ -191,14 +201,14 @@ function EvaluatorDetailInner() {
191
  </div>
192
  </div>
193
 
194
- {/* META + FILTER BAR --------------------------------------- */}
195
  <div className="mb-6 flex flex-wrap items-center gap-x-8 gap-y-3 border-y border-[color:var(--border-soft)] py-4">
196
  <div className="flex shrink-0 flex-wrap items-baseline gap-x-4 gap-y-1 font-mono text-[11px] tracking-[0.1em] uppercase text-[color:var(--fg-subtle)]">
197
  <span>
198
  <span className="text-[color:var(--fg)] tabular-nums font-semibold mr-1">
199
- {filteredEvals.length.toLocaleString()}
200
  </span>
201
- {filteredEvals.length === 1 ? "evaluation" : "evaluations"}
202
  </span>
203
  </div>
204
 
@@ -210,30 +220,29 @@ function EvaluatorDetailInner() {
210
  className="ec-input pl-9"
211
  value={searchQuery}
212
  onChange={(event) => setSearchQuery(event.target.value)}
213
- placeholder="Search evaluations…"
214
  />
215
  </div>
216
  </div>
217
 
218
- {/* EVAL CARDS ---------------------------------------------- */}
219
- {filteredEvals.length === 0 ? (
220
  <div className="border border-dashed border-[color:var(--border-soft)] bg-[color:var(--bg-warm)] py-12 text-center font-mono text-[11px] uppercase tracking-[0.2em] text-[color:var(--fg-subtle)]">
221
  No evaluations match the current filters
222
  </div>
223
  ) : (
224
- <div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
225
- {visibleEvals.map((ev, i) => (
226
- <EvalCard key={ev.evaluation_id} summary={ev} delayMs={Math.min(i, 8) * 40} />
227
- ))}
228
- </div>
 
 
 
 
 
 
229
  )}
230
-
231
- <InfiniteScrollSentinel
232
- hasMore={hasMore}
233
- onLoadMore={handleLoadMore}
234
- loadingLabel="Loading more…"
235
- endLabel={`Showing ${Math.min(visibleCount, filteredEvals.length).toLocaleString()} of ${filteredEvals.length.toLocaleString()} evaluations`}
236
- />
237
  </main>
238
  </div>
239
  )
 
4
  import { useParams, useRouter, useSearchParams } from "next/navigation"
5
  import { ArrowLeft, Search } from "lucide-react"
6
 
7
+ import { FamilyTable, type FamilySortCol } from "@/components/family-table"
 
8
  import { Navigation } from "@/components/navigation"
9
  import { VerifiedBadge } from "@/components/signals/verified-badge"
10
+ import type { EvalHierarchy } from "@/lib/backend-artifacts"
11
+ import type { BenchmarkCard } from "@/lib/benchmark-schema"
12
+ import { fetchBenchmarkMetadata, fetchEvalHierarchy, fetchEvalList } from "@/lib/dashboard-data-client"
13
  import type { BenchmarkEvalListItem } from "@/lib/eval-processing"
14
+ import { getEvalsForEvaluator, verifiedEvalIds } from "@/lib/evaluators"
 
 
15
 
16
  function EvaluatorDetailInner() {
17
  const params = useParams()
 
28
  }, [params.id])
29
 
30
  const [allEvals, setAllEvals] = useState<BenchmarkEvalListItem[]>([])
31
+ const [hierarchy, setHierarchy] = useState<EvalHierarchy | null>(null)
32
+ const [benchmarkCards, setBenchmarkCards] = useState<Record<string, BenchmarkCard>>({})
33
  const [loading, setLoading] = useState(true)
34
  const [error, setError] = useState<string | null>(null)
35
  const [searchQuery, setSearchQuery] = useState("")
36
+ const [sortCol, setSortCol] = useState<FamilySortCol>("name")
37
+ const [sortDir, setSortDir] = useState<"asc" | "desc">("asc")
38
 
39
  useEffect(() => {
40
+ const evalListRequest = fetchEvalList()
41
  .then((list) => setAllEvals(list.evals))
42
  .catch((err) => {
43
  console.error(err)
44
  setError("Failed to load evaluations")
45
  })
46
+ const hierarchyRequest = fetchEvalHierarchy()
47
+ .then((h) => setHierarchy(h))
48
+ .catch(console.error)
49
+ const metadataRequest = fetchBenchmarkMetadata()
50
+ .then((metadata) => setBenchmarkCards(metadata))
51
+ .catch(console.error)
52
+ Promise.allSettled([evalListRequest, hierarchyRequest, metadataRequest]).finally(() =>
53
+ setLoading(false),
54
+ )
55
  }, [])
56
 
57
+ const handleSort = useCallback((col: FamilySortCol) => {
58
+ if (sortCol === col) {
59
+ setSortDir((d) => (d === "asc" ? "desc" : "asc"))
60
+ } else {
61
+ setSortCol(col)
62
+ setSortDir("asc")
63
+ }
64
+ }, [sortCol])
65
+
66
  const { name, isVerified, evals } = useMemo(
67
  () => getEvalsForEvaluator(allEvals, slug, { verifiedOnly }),
68
  [allEvals, slug, verifiedOnly],
69
  )
70
 
71
+ // Eval-id universe owned by this evaluator β€” restricts the family tree to
72
+ // this org's evaluations (slices already excluded by getEvalsForEvaluator).
73
+ const restrictEvalIds = useMemo(() => {
74
+ const set = new Set<string>()
75
+ for (const ev of evals) set.add(ev.evaluation_id)
76
+ return set
77
+ }, [evals])
78
+
79
+ const verifiedIds = useMemo(() => verifiedEvalIds(allEvals), [allEvals])
80
+
81
+ const evalItems = useMemo(() => {
82
+ const map = new Map<string, BenchmarkEvalListItem>()
83
+ for (const ev of allEvals) map.set(ev.evaluation_id, ev)
84
+ return map
85
+ }, [allEvals])
86
+
87
+ const families = hierarchy?.families ?? []
88
+
89
+ // Quantified facts for the header. familyCount counts the top-level families
90
+ // the table actually renders for this org (those whose constituent evals
91
+ // intersect this evaluator's set), so the header agrees with the accordion
92
+ // below it rather than the finer family_display_name grouping.
93
  const { familyCount, verifiedCount } = useMemo(() => {
 
94
  let verified = 0
95
  for (const ev of evals) {
 
 
96
  if (name && (ev.verified_evaluator_names ?? []).includes(name)) verified += 1
97
  }
98
+ let familyCount = 0
99
+ for (const fam of families) {
100
+ if ((fam.constituent_evaluation_ids ?? []).some((id) => restrictEvalIds.has(id))) {
101
+ familyCount += 1
102
+ }
103
  }
104
+ return { familyCount, verifiedCount: verified }
105
+ }, [evals, name, families, restrictEvalIds])
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
  const handleBack = useCallback(() => {
108
  router.push(verifiedOnly ? "/evals?groupBy=evaluator&verified=1" : "/evals?groupBy=evaluator")
 
174
  <span>{verifiedCount} verified</span>
175
  </div>
176
  <p className="ec-page-lede">
177
+ Reported <strong>{evals.length.toLocaleString()}</strong>{" "}
178
+ {evals.length === 1 ? "evaluation" : "evaluations"} across{" "}
179
  <strong>{familyCount.toLocaleString()}</strong>{" "}
180
  {familyCount === 1 ? "benchmark family" : "benchmark families"}
181
  {verifiedCount > 0 && (
 
189
  <div className="ec-page-meta mt-2">
190
  <div className="ec-page-meta-item">
191
  <span className="ec-page-meta-item-l">Evaluations</span>
192
+ <span className="ec-page-meta-item-v">{evals.length.toLocaleString()}</span>
193
  </div>
194
  <div className="ec-page-meta-item">
195
  <span className="ec-page-meta-item-l">Verified</span>
 
201
  </div>
202
  </div>
203
 
204
+ {/* FILTER BAR ---------------------------------------------- */}
205
  <div className="mb-6 flex flex-wrap items-center gap-x-8 gap-y-3 border-y border-[color:var(--border-soft)] py-4">
206
  <div className="flex shrink-0 flex-wrap items-baseline gap-x-4 gap-y-1 font-mono text-[11px] tracking-[0.1em] uppercase text-[color:var(--fg-subtle)]">
207
  <span>
208
  <span className="text-[color:var(--fg)] tabular-nums font-semibold mr-1">
209
+ {evals.length.toLocaleString()}
210
  </span>
211
+ {evals.length === 1 ? "evaluation" : "evaluations"}
212
  </span>
213
  </div>
214
 
 
220
  className="ec-input pl-9"
221
  value={searchQuery}
222
  onChange={(event) => setSearchQuery(event.target.value)}
223
+ placeholder="Search benchmarks…"
224
  />
225
  </div>
226
  </div>
227
 
228
+ {/* FAMILY TABLE β€” scoped to this evaluator's evaluations ---- */}
229
+ {restrictEvalIds.size === 0 ? (
230
  <div className="border border-dashed border-[color:var(--border-soft)] bg-[color:var(--bg-warm)] py-12 text-center font-mono text-[11px] uppercase tracking-[0.2em] text-[color:var(--fg-subtle)]">
231
  No evaluations match the current filters
232
  </div>
233
  ) : (
234
+ <FamilyTable
235
+ families={families}
236
+ evalItems={evalItems}
237
+ benchmarkCards={benchmarkCards}
238
+ searchQuery={searchQuery}
239
+ verifiedEvalIds={verifiedOnly ? verifiedIds : null}
240
+ restrictEvalIds={restrictEvalIds}
241
+ sortCol={sortCol}
242
+ sortDir={sortDir}
243
+ onSort={handleSort}
244
+ />
245
  )}
 
 
 
 
 
 
 
246
  </main>
247
  </div>
248
  )
components/benchmark-detail.tsx CHANGED
@@ -1856,7 +1856,10 @@ export function BenchmarkDetail({
1856
  // families. "overlaps" = cross-family duplicates only, rendered as a
1857
  // table (no plotbox/list toggle) with mean and 95% CI for the model's
1858
  // score across each canonical's appearances.
1859
- const [groupingMode, setGroupingMode] = useState<"source" | "category" | "overlaps">("source")
 
 
 
1860
  const [expandedFamilies, setExpandedFamilies] = useState<Set<string>>(new Set())
1861
  const toggleFamily = (key: string) =>
1862
  setExpandedFamilies((prev) => {
@@ -2257,6 +2260,7 @@ export function BenchmarkDetail({
2257
  return buildModelPolicySummary({
2258
  summary,
2259
  thirdPartyEvaluations: reportingStats.thirdPartyEvaluations,
 
2260
  organizationCount: reportingStats.organizationCount,
2261
  organizationNames: reportingStats.organizationNames,
2262
  benchmarkCount,
@@ -2264,6 +2268,7 @@ export function BenchmarkDetail({
2264
  })
2265
  }, [
2266
  allCategoryResults,
 
2267
  reportingStats.thirdPartyEvaluations,
2268
  reportingStats.organizationCount,
2269
  reportingStats.organizationNames,
@@ -2961,6 +2966,11 @@ export function BenchmarkDetail({
2961
  currentModelIdentityKeys,
2962
  ])
2963
 
 
 
 
 
 
2964
  // Per-(eval, metric) leaderboards sourced from comparison-index.json.
2965
  const benchmarkHistograms = useMemo<Map<string, BenchmarkHistogram>>(() => {
2966
  const result = new Map<string, BenchmarkHistogram>()
@@ -5130,6 +5140,16 @@ export function BenchmarkDetail({
5130
  </div>
5131
  </div>
5132
 
 
 
 
 
 
 
 
 
 
 
5133
  {/* SUMMARY VIEW β€” text-only list grouped by family, ranked
5134
  bestβ†’worst, with the category pill bar so non-technical
5135
  readers can filter without diving into Researcher view. */}
@@ -5354,14 +5374,15 @@ export function BenchmarkDetail({
5354
  </div>
5355
  <div className="flex flex-wrap gap-1">
5356
  {row.appearances.map((app) => (
5357
- <span
5358
  key={`${row.canonicalKey}::${app.familyKey}::${app.evalSummaryId}`}
5359
- className="ec-tag"
 
5360
  style={{ fontSize: 10 }}
5361
- title={`${app.familyName} Β· ${app.metricName}`}
5362
  >
5363
  {app.familyName} Β· {fmt(app.score)}
5364
- </span>
5365
  ))}
5366
  </div>
5367
  </div>
 
1856
  // families. "overlaps" = cross-family duplicates only, rendered as a
1857
  // table (no plotbox/list toggle) with mean and 95% CI for the model's
1858
  // score across each canonical's appearances.
1859
+ // `null` until the user explicitly picks a view; the effective default is
1860
+ // derived from the data (overlaps when this model has any cross-suite
1861
+ // overlaps, else source) β€” see `groupingMode` just after `overlapsRows`.
1862
+ const [pickedGroupingMode, setGroupingMode] = useState<"source" | "category" | "overlaps" | null>(null)
1863
  const [expandedFamilies, setExpandedFamilies] = useState<Set<string>>(new Set())
1864
  const toggleFamily = (key: string) =>
1865
  setExpandedFamilies((prev) => {
 
2260
  return buildModelPolicySummary({
2261
  summary,
2262
  thirdPartyEvaluations: reportingStats.thirdPartyEvaluations,
2263
+ reportedEvaluationCount: allEvaluations.length,
2264
  organizationCount: reportingStats.organizationCount,
2265
  organizationNames: reportingStats.organizationNames,
2266
  benchmarkCount,
 
2268
  })
2269
  }, [
2270
  allCategoryResults,
2271
+ allEvaluations.length,
2272
  reportingStats.thirdPartyEvaluations,
2273
  reportingStats.organizationCount,
2274
  reportingStats.organizationNames,
 
2966
  currentModelIdentityKeys,
2967
  ])
2968
 
2969
+ // Effective view: honour the user's explicit pick; otherwise default to
2970
+ // overlaps when this model has cross-suite overlaps, falling back to source
2971
+ // when it has none (so models without overlaps don't open on an empty view).
2972
+ const groupingMode = pickedGroupingMode ?? (overlapsRows.length > 0 ? "overlaps" : "source")
2973
+
2974
  // Per-(eval, metric) leaderboards sourced from comparison-index.json.
2975
  const benchmarkHistograms = useMemo<Map<string, BenchmarkHistogram>>(() => {
2976
  const result = new Map<string, BenchmarkHistogram>()
 
5140
  </div>
5141
  </div>
5142
 
5143
+ {isResearchView && !embedReportedMetricsOnly && (
5144
+ <p className="text-[14px] leading-[1.7] text-[color:var(--fg-muted)] max-w-[64rem] mb-6">
5145
+ {groupingMode === "overlaps"
5146
+ ? "Cross-suite overlaps β€” benchmarks this model reports under more than one suite, with the mean and 95% CI across appearances. Each source links through to its eval. Switch to Source or Category for the full result set."
5147
+ : groupingMode === "category"
5148
+ ? "Every reported result, regrouped under curated category tags so similar benchmarks cluster across families."
5149
+ : "Every reported result in the warehouse's natural shape β€” family-rooted plots and accordions, with no cross-family collapse."}
5150
+ </p>
5151
+ )}
5152
+
5153
  {/* SUMMARY VIEW β€” text-only list grouped by family, ranked
5154
  bestβ†’worst, with the category pill bar so non-technical
5155
  readers can filter without diving into Researcher view. */}
 
5374
  </div>
5375
  <div className="flex flex-wrap gap-1">
5376
  {row.appearances.map((app) => (
5377
+ <Link
5378
  key={`${row.canonicalKey}::${app.familyKey}::${app.evalSummaryId}`}
5379
+ href={`/evals/${routeIdToPath(app.evalSummaryId)}?from=${encodeURIComponent(currentDetailHref)}`}
5380
+ className="ec-tag outline hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors"
5381
  style={{ fontSize: 10 }}
5382
+ title={`${app.familyName} Β· ${app.metricName} β€” view eval`}
5383
  >
5384
  {app.familyName} Β· {fmt(app.score)}
5385
+ </Link>
5386
  ))}
5387
  </div>
5388
  </div>
components/family-table.tsx CHANGED
@@ -29,6 +29,12 @@ interface FamilyTableProps {
29
  * mode β€” the set is the verified-eval id universe). Null/undefined =
30
  * no restriction. */
31
  verifiedEvalIds?: Set<string> | null
 
 
 
 
 
 
32
  sortCol?: FamilySortCol
33
  sortDir?: "asc" | "desc"
34
  onSort?: (col: FamilySortCol) => void
@@ -214,6 +220,7 @@ export function FamilyTable({
214
  categoryFilter,
215
  searchQuery,
216
  verifiedEvalIds,
 
217
  sortCol,
218
  sortDir,
219
  onSort,
@@ -224,9 +231,10 @@ export function FamilyTable({
224
  const domainFilterActive = Boolean(domainFilter && domainFilter.size > 0)
225
  const categoryFilterActive = Boolean(categoryFilter && categoryFilter.size > 0)
226
  const verifiedFilterActive = Boolean(verifiedEvalIds)
 
227
  const normalizedQuery = (searchQuery ?? "").trim().toLowerCase()
228
  const searchActive = normalizedQuery.length > 0
229
- const filterActive = domainFilterActive || categoryFilterActive || searchActive || verifiedFilterActive
230
 
231
  function leafMatchesDomain(leaf: LeafEntry): boolean {
232
  if (!domainFilterActive || !domainFilter) return true
@@ -252,10 +260,16 @@ export function FamilyTable({
252
  return leaf.evalIds.some((id) => verifiedEvalIds.has(id))
253
  }
254
 
 
 
 
 
 
255
  function leafMatchesFilter(leaf: LeafEntry, opts?: { skipQuery?: boolean }): boolean {
256
  if (!leafMatchesDomain(leaf)) return false
257
  if (!leafMatchesCategory(leaf)) return false
258
  if (!leafMatchesVerified(leaf)) return false
 
259
  if (!opts?.skipQuery && !leafMatchesQuery(leaf)) return false
260
  return true
261
  }
@@ -279,12 +293,12 @@ export function FamilyTable({
279
  if (leafEntries.some((leaf) => leafMatchesFilter(leaf))) return true
280
  // Family-level search match keeps the row even if no leaf survives
281
  // the leaf-query filter (the row will fall back to showing all
282
- // leaves). But the verified filter is a hard leaf-level gate: never
283
- // resurrect a family that has zero verified leaves.
284
  if (
285
  searchActive &&
286
  familyMatchedAtFamilyLevel(fam) &&
287
- leafEntries.some((leaf) => leafMatchesVerified(leaf))
288
  )
289
  return true
290
  if (categoryFilterActive && categoryFilter) {
@@ -402,7 +416,7 @@ export function FamilyTable({
402
  }
403
  return out
404
  // eslint-disable-next-line react-hooks/exhaustive-deps
405
- }, [families, evalItems, benchmarkCards, domainFilter, categoryFilter, searchQuery])
406
 
407
  function SortIcon({ col }: { col: FamilySortCol }) {
408
  if (!onSort) return null
 
29
  * mode β€” the set is the verified-eval id universe). Null/undefined =
30
  * no restriction. */
31
  verifiedEvalIds?: Set<string> | null
32
+ /** When provided, restrict leaves to those mapping to one of these
33
+ * evaluation_ids (drives the /evaluators/<slug> detail page β€” the set is
34
+ * the eval-id universe owned by one reporting org). Composes with
35
+ * verifiedEvalIds: when both are present a leaf must intersect both.
36
+ * Null/undefined = no restriction. */
37
+ restrictEvalIds?: Set<string> | null
38
  sortCol?: FamilySortCol
39
  sortDir?: "asc" | "desc"
40
  onSort?: (col: FamilySortCol) => void
 
220
  categoryFilter,
221
  searchQuery,
222
  verifiedEvalIds,
223
+ restrictEvalIds,
224
  sortCol,
225
  sortDir,
226
  onSort,
 
231
  const domainFilterActive = Boolean(domainFilter && domainFilter.size > 0)
232
  const categoryFilterActive = Boolean(categoryFilter && categoryFilter.size > 0)
233
  const verifiedFilterActive = Boolean(verifiedEvalIds)
234
+ const restrictFilterActive = Boolean(restrictEvalIds)
235
  const normalizedQuery = (searchQuery ?? "").trim().toLowerCase()
236
  const searchActive = normalizedQuery.length > 0
237
+ const filterActive = domainFilterActive || categoryFilterActive || searchActive || verifiedFilterActive || restrictFilterActive
238
 
239
  function leafMatchesDomain(leaf: LeafEntry): boolean {
240
  if (!domainFilterActive || !domainFilter) return true
 
260
  return leaf.evalIds.some((id) => verifiedEvalIds.has(id))
261
  }
262
 
263
+ function leafMatchesRestrict(leaf: LeafEntry): boolean {
264
+ if (!restrictFilterActive || !restrictEvalIds) return true
265
+ return leaf.evalIds.some((id) => restrictEvalIds.has(id))
266
+ }
267
+
268
  function leafMatchesFilter(leaf: LeafEntry, opts?: { skipQuery?: boolean }): boolean {
269
  if (!leafMatchesDomain(leaf)) return false
270
  if (!leafMatchesCategory(leaf)) return false
271
  if (!leafMatchesVerified(leaf)) return false
272
+ if (!leafMatchesRestrict(leaf)) return false
273
  if (!opts?.skipQuery && !leafMatchesQuery(leaf)) return false
274
  return true
275
  }
 
293
  if (leafEntries.some((leaf) => leafMatchesFilter(leaf))) return true
294
  // Family-level search match keeps the row even if no leaf survives
295
  // the leaf-query filter (the row will fall back to showing all
296
+ // leaves). But the verified and restrict filters are hard leaf-level
297
+ // gates: never resurrect a family that has zero surviving leaves.
298
  if (
299
  searchActive &&
300
  familyMatchedAtFamilyLevel(fam) &&
301
+ leafEntries.some((leaf) => leafMatchesVerified(leaf) && leafMatchesRestrict(leaf))
302
  )
303
  return true
304
  if (categoryFilterActive && categoryFilter) {
 
416
  }
417
  return out
418
  // eslint-disable-next-line react-hooks/exhaustive-deps
419
+ }, [families, evalItems, benchmarkCards, domainFilter, categoryFilter, searchQuery, verifiedEvalIds, restrictEvalIds])
420
 
421
  function SortIcon({ col }: { col: FamilySortCol }) {
422
  if (!onSort) return null
lib/policy-summaries.ts CHANGED
@@ -127,6 +127,11 @@ interface ModelPolicyInputs {
127
  /** Pre-computed third-party tally from caller (cheap to compute, but
128
  * caller already has it in benchmark-detail). */
129
  thirdPartyEvaluations: number
 
 
 
 
 
130
  organizationCount: number
131
  organizationNames: string[]
132
  /** Distinct benchmark count derived from group reduction. */
@@ -138,12 +143,17 @@ interface ModelPolicyInputs {
138
  export function buildModelPolicySummary({
139
  summary,
140
  thirdPartyEvaluations,
 
141
  organizationCount,
142
  organizationNames,
143
  benchmarkCount,
144
  reportedCategories,
145
  }: ModelPolicyInputs): ModelPolicySummary {
146
  const totalEvals = summary.total_evaluations
 
 
 
 
147
  const repro = summary.reproducibility_summary
148
  const reproGap = repro?.has_reproducibility_gap_count ?? 0
149
  const reproTotal = repro?.results_total ?? totalEvals
@@ -191,8 +201,8 @@ export function buildModelPolicySummary({
191
  provenance?.first_party_only_groups != null && provenance.total_groups > 0
192
  ? provenance.first_party_only_groups === provenance.total_groups
193
  : null
194
- const allThirdParty = totalEvals > 0 && thirdPartyEvaluations === totalEvals
195
- const noThirdParty = thirdPartyEvaluations === 0 && totalEvals > 0
196
  const lead = organizationNames[0]
197
 
198
  let reportingSentence: string
@@ -253,11 +263,11 @@ export function buildModelPolicySummary({
253
 
254
  // ── 6. Verification headline ─────────────────────────────────────────
255
  let verificationLabel: string | null = null
256
- if (allThirdParty && totalEvals > 0) {
257
- verificationLabel = "Independently verified"
258
- } else if (thirdPartyEvaluations > 0 && totalEvals > 0) {
259
- const pct = Math.round((thirdPartyEvaluations / totalEvals) * 100)
260
- verificationLabel = `${pct}% independently verified`
261
  } else if (noThirdParty) {
262
  verificationLabel = "Developer-reported only"
263
  }
 
127
  /** Pre-computed third-party tally from caller (cheap to compute, but
128
  * caller already has it in benchmark-detail). */
129
  thirdPartyEvaluations: number
130
+ /** Denominator for the third-party share. MUST be counted from the same
131
+ * population as `thirdPartyEvaluations` (the caller's flattened evaluation
132
+ * list), not the warehouse's distinct `total_evaluations` β€” those have
133
+ * different grains, which made the share exceed 100%. */
134
+ reportedEvaluationCount: number
135
  organizationCount: number
136
  organizationNames: string[]
137
  /** Distinct benchmark count derived from group reduction. */
 
143
  export function buildModelPolicySummary({
144
  summary,
145
  thirdPartyEvaluations,
146
+ reportedEvaluationCount,
147
  organizationCount,
148
  organizationNames,
149
  benchmarkCount,
150
  reportedCategories,
151
  }: ModelPolicyInputs): ModelPolicySummary {
152
  const totalEvals = summary.total_evaluations
153
+ // Denominator for the third-party share, counted from the same population as
154
+ // the numerator so the ratio stays within 0–100%. Falls back to totalEvals
155
+ // only if the caller passed nothing.
156
+ const thirdPartyBase = reportedEvaluationCount > 0 ? reportedEvaluationCount : totalEvals
157
  const repro = summary.reproducibility_summary
158
  const reproGap = repro?.has_reproducibility_gap_count ?? 0
159
  const reproTotal = repro?.results_total ?? totalEvals
 
201
  provenance?.first_party_only_groups != null && provenance.total_groups > 0
202
  ? provenance.first_party_only_groups === provenance.total_groups
203
  : null
204
+ const allThirdParty = thirdPartyBase > 0 && thirdPartyEvaluations === thirdPartyBase
205
+ const noThirdParty = thirdPartyEvaluations === 0 && thirdPartyBase > 0
206
  const lead = organizationNames[0]
207
 
208
  let reportingSentence: string
 
263
 
264
  // ── 6. Verification headline ─────────────────────────────────────────
265
  let verificationLabel: string | null = null
266
+ if (allThirdParty && thirdPartyBase > 0) {
267
+ verificationLabel = "100% third party"
268
+ } else if (thirdPartyEvaluations > 0 && thirdPartyBase > 0) {
269
+ const pct = Math.min(100, Math.round((thirdPartyEvaluations / thirdPartyBase) * 100))
270
+ verificationLabel = `${pct}% third party`
271
  } else if (noThirdParty) {
272
  verificationLabel = "Developer-reported only"
273
  }