Spaces:
Running
Running
Use model_key as the addressable identifier and wire comparison-index sidecar
Browse filesmodel_key is non-null for both resolved and unresolved models (the latter
fall back to the raw source name); querying by model_id alone silently
dropped unresolved models. Update model lookups, eval-row joins, and
ModelInfo derivation to prefer model_key with model_id as fallback.
Also point the Dockerfile snapshot URL at the temp dataset
(j-chim/temp_evalcard_backend) and add fetchComparisonIndex sidecar
support so v2 mode no longer hits the legacy JSON path.
- Dockerfile +2 -2
- lib/hf-data.ts +4 -0
- lib/sidecars.ts +6 -0
- lib/view-data.ts +18 -11
- notes/backend-v2-migration.md +616 -0
- notes/merge-cheatsheet-backend-v2.md +193 -0
- tests/view-data.test.ts +2 -0
Dockerfile
CHANGED
|
@@ -14,7 +14,7 @@ ARG PNPM_VERSION=10.25.0
|
|
| 14 |
# Override at build time via `--build-arg ...`.
|
| 15 |
ARG DATA_BACKEND=v2
|
| 16 |
ARG HF_DATASET_REPO=https://huggingface.co/datasets/evaleval/card_backend
|
| 17 |
-
ARG SNAPSHOT_URL=https://huggingface.co/datasets/
|
| 18 |
# Static prerender (`next build`) executes route handlers. In legacy mode the
|
| 19 |
# cache populated by `cache-hf-data.mjs` lives at `/app/.cache/hf-data`; in v2
|
| 20 |
# the cache step is skipped and the app reads the pinned Stage J snapshot.
|
|
@@ -48,7 +48,7 @@ FROM node:18-bullseye-slim AS runner
|
|
| 48 |
WORKDIR /app
|
| 49 |
|
| 50 |
ARG DATA_BACKEND=v2
|
| 51 |
-
ARG SNAPSHOT_URL=https://huggingface.co/datasets/
|
| 52 |
|
| 53 |
# Runtime needs the same data-source envs that the builder used. Docker
|
| 54 |
# multi-stage doesn't carry ENVs across stages, so keep backend selection and
|
|
|
|
| 14 |
# Override at build time via `--build-arg ...`.
|
| 15 |
ARG DATA_BACKEND=v2
|
| 16 |
ARG HF_DATASET_REPO=https://huggingface.co/datasets/evaleval/card_backend
|
| 17 |
+
ARG SNAPSHOT_URL=https://huggingface.co/datasets/j-chim/temp_evalcard_backend/resolve/main/warehouse/2026-05-03T21-46-50Z
|
| 18 |
# Static prerender (`next build`) executes route handlers. In legacy mode the
|
| 19 |
# cache populated by `cache-hf-data.mjs` lives at `/app/.cache/hf-data`; in v2
|
| 20 |
# the cache step is skipped and the app reads the pinned Stage J snapshot.
|
|
|
|
| 48 |
WORKDIR /app
|
| 49 |
|
| 50 |
ARG DATA_BACKEND=v2
|
| 51 |
+
ARG SNAPSHOT_URL=https://huggingface.co/datasets/j-chim/temp_evalcard_backend/resolve/main/warehouse/2026-05-03T21-46-50Z
|
| 52 |
|
| 53 |
# Runtime needs the same data-source envs that the builder used. Docker
|
| 54 |
# multi-stage doesn't carry ENVs across stages, so keep backend selection and
|
lib/hf-data.ts
CHANGED
|
@@ -1005,6 +1005,10 @@ function adaptEvalHierarchy(raw: EvalHierarchy): EvalHierarchy {
|
|
| 1005 |
}
|
| 1006 |
|
| 1007 |
export async function fetchComparisonIndex(): Promise<ComparisonIndex> {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1008 |
return fetchHFJson<ComparisonIndex>("comparison-index.json")
|
| 1009 |
}
|
| 1010 |
|
|
|
|
| 1005 |
}
|
| 1006 |
|
| 1007 |
export async function fetchComparisonIndex(): Promise<ComparisonIndex> {
|
| 1008 |
+
if (useViewLayerBackend()) {
|
| 1009 |
+
return (await fetchSnapshotSidecars()).fetchComparisonIndex()
|
| 1010 |
+
}
|
| 1011 |
+
|
| 1012 |
return fetchHFJson<ComparisonIndex>("comparison-index.json")
|
| 1013 |
}
|
| 1014 |
|
lib/sidecars.ts
CHANGED
|
@@ -2,6 +2,7 @@ import "server-only"
|
|
| 2 |
|
| 3 |
import type {
|
| 4 |
BackendManifest,
|
|
|
|
| 5 |
CorpusAggregates,
|
| 6 |
EvalHierarchy,
|
| 7 |
} from "@/lib/backend-artifacts"
|
|
@@ -10,6 +11,7 @@ let cache: {
|
|
| 10 |
manifest?: Promise<BackendManifest>
|
| 11 |
headline?: Promise<CorpusAggregates>
|
| 12 |
hierarchy?: Promise<EvalHierarchy>
|
|
|
|
| 13 |
} = {}
|
| 14 |
|
| 15 |
function getSnapshotUrl() {
|
|
@@ -54,6 +56,10 @@ export function fetchHierarchy(): Promise<EvalHierarchy> {
|
|
| 54 |
return (cache.hierarchy ??= fetchJson<EvalHierarchy>("hierarchy.json"))
|
| 55 |
}
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
export function resetSidecarCacheForTests() {
|
| 58 |
cache = {}
|
| 59 |
}
|
|
|
|
| 2 |
|
| 3 |
import type {
|
| 4 |
BackendManifest,
|
| 5 |
+
ComparisonIndex,
|
| 6 |
CorpusAggregates,
|
| 7 |
EvalHierarchy,
|
| 8 |
} from "@/lib/backend-artifacts"
|
|
|
|
| 11 |
manifest?: Promise<BackendManifest>
|
| 12 |
headline?: Promise<CorpusAggregates>
|
| 13 |
hierarchy?: Promise<EvalHierarchy>
|
| 14 |
+
comparisonIndex?: Promise<ComparisonIndex>
|
| 15 |
} = {}
|
| 16 |
|
| 17 |
function getSnapshotUrl() {
|
|
|
|
| 56 |
return (cache.hierarchy ??= fetchJson<EvalHierarchy>("hierarchy.json"))
|
| 57 |
}
|
| 58 |
|
| 59 |
+
export function fetchComparisonIndex(): Promise<ComparisonIndex> {
|
| 60 |
+
return (cache.comparisonIndex ??= fetchJson<ComparisonIndex>("comparison-index.json"))
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
export function resetSidecarCacheForTests() {
|
| 64 |
cache = {}
|
| 65 |
}
|
lib/view-data.ts
CHANGED
|
@@ -28,7 +28,7 @@ import type {
|
|
| 28 |
type Row = Record<string, any>
|
| 29 |
|
| 30 |
const MODEL_CARD_COLUMNS = `
|
| 31 |
-
id, route_id, model_name, model_id, canonical_model_name, developer,
|
| 32 |
evaluations_count, benchmarks_count, variant_count,
|
| 33 |
categories, category_stats, latest_timestamp,
|
| 34 |
evaluator_count, evaluator_names, source_type_count, source_types,
|
|
@@ -250,8 +250,8 @@ function metricConfigFromRow(row: Row): MetricConfig {
|
|
| 250 |
|
| 251 |
function modelInfoFromModelRow(row: Row): ModelInfo {
|
| 252 |
return {
|
| 253 |
-
name: asString(row.model_name ?? row.model_family_name ?? row.model_id, "Unknown model"),
|
| 254 |
-
id: asString(row.model_id ?? row.id ?? row.route_id, "unknown-model"),
|
| 255 |
developer: optionalString(row.developer),
|
| 256 |
inference_platform: optionalString(row.inference_platform),
|
| 257 |
inference_engine: optionalString(row.inference_engine),
|
|
@@ -390,23 +390,26 @@ function modelSummaryFromRows(modelRow: Row, cellRows: Row[]): ModelEvaluationSu
|
|
| 390 |
|
| 391 |
return {
|
| 392 |
...core,
|
| 393 |
-
model_family_id: asString(modelRow.model_family_id ?? modelRow.model_id, modelRow.model_id),
|
| 394 |
model_route_id: asString(modelRow.model_route_id ?? modelRow.route_id, modelRow.route_id),
|
| 395 |
model_family_name: asString(modelRow.model_family_name ?? modelRow.model_name, modelRow.model_name),
|
| 396 |
-
raw_model_ids: rawModelIds.length > 0 ? rawModelIds : [asString(modelRow.model_id, "")].filter(Boolean),
|
| 397 |
variants,
|
| 398 |
}
|
| 399 |
}
|
| 400 |
|
| 401 |
-
async function getModelEvaluationRows(
|
|
|
|
|
|
|
|
|
|
| 402 |
return readRows<Row>(
|
| 403 |
`SELECT ${CELL_JOIN_COLUMNS}
|
| 404 |
FROM eval_results_view r
|
| 405 |
LEFT JOIN evals_view e ON r.evaluation_id = e.evaluation_id
|
| 406 |
-
WHERE r.
|
| 407 |
AND r.score IS NOT NULL
|
| 408 |
ORDER BY r.category, r.percentile DESC NULLS LAST`,
|
| 409 |
-
[
|
| 410 |
)
|
| 411 |
}
|
| 412 |
|
|
@@ -466,17 +469,21 @@ export async function getDashboardData() {
|
|
| 466 |
}
|
| 467 |
|
| 468 |
export async function getModelSummaryById(routeId: string): Promise<ModelEvaluationSummary | null> {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
const rows = await readRows<Row>(
|
| 470 |
`SELECT *
|
| 471 |
FROM models_view
|
| 472 |
-
WHERE route_id = ? OR model_route_id = ? OR model_family_id = ? OR model_id = ?
|
| 473 |
LIMIT 1`,
|
| 474 |
-
[routeId, routeId, routeId, routeId]
|
| 475 |
)
|
| 476 |
const modelRow = rows[0]
|
| 477 |
if (!modelRow) return null
|
| 478 |
|
| 479 |
-
const cellRows = await getModelEvaluationRows(asString(modelRow.model_id, routeId))
|
| 480 |
return modelSummaryFromRows(modelRow, cellRows)
|
| 481 |
}
|
| 482 |
|
|
|
|
| 28 |
type Row = Record<string, any>
|
| 29 |
|
| 30 |
const MODEL_CARD_COLUMNS = `
|
| 31 |
+
id, model_key, route_id, model_name, model_id, canonical_model_name, developer,
|
| 32 |
evaluations_count, benchmarks_count, variant_count,
|
| 33 |
categories, category_stats, latest_timestamp,
|
| 34 |
evaluator_count, evaluator_names, source_type_count, source_types,
|
|
|
|
| 250 |
|
| 251 |
function modelInfoFromModelRow(row: Row): ModelInfo {
|
| 252 |
return {
|
| 253 |
+
name: asString(row.model_name ?? row.model_family_name ?? row.model_id ?? row.model_key, "Unknown model"),
|
| 254 |
+
id: asString(row.model_key ?? row.model_id ?? row.id ?? row.route_id, "unknown-model"),
|
| 255 |
developer: optionalString(row.developer),
|
| 256 |
inference_platform: optionalString(row.inference_platform),
|
| 257 |
inference_engine: optionalString(row.inference_engine),
|
|
|
|
| 390 |
|
| 391 |
return {
|
| 392 |
...core,
|
| 393 |
+
model_family_id: asString(modelRow.model_family_id ?? modelRow.model_key ?? modelRow.model_id, modelRow.model_key ?? modelRow.model_id),
|
| 394 |
model_route_id: asString(modelRow.model_route_id ?? modelRow.route_id, modelRow.route_id),
|
| 395 |
model_family_name: asString(modelRow.model_family_name ?? modelRow.model_name, modelRow.model_name),
|
| 396 |
+
raw_model_ids: rawModelIds.length > 0 ? rawModelIds : [asString(modelRow.model_key ?? modelRow.model_id, "")].filter(Boolean),
|
| 397 |
variants,
|
| 398 |
}
|
| 399 |
}
|
| 400 |
|
| 401 |
+
async function getModelEvaluationRows(modelKey: string): Promise<Row[]> {
|
| 402 |
+
// model_key is the producer's addressable identifier β non-null for both
|
| 403 |
+
// resolved and unresolved models (the latter fall back to the raw source
|
| 404 |
+
// name). Querying by model_id alone would silently miss unresolved models.
|
| 405 |
return readRows<Row>(
|
| 406 |
`SELECT ${CELL_JOIN_COLUMNS}
|
| 407 |
FROM eval_results_view r
|
| 408 |
LEFT JOIN evals_view e ON r.evaluation_id = e.evaluation_id
|
| 409 |
+
WHERE r.model_key = ?
|
| 410 |
AND r.score IS NOT NULL
|
| 411 |
ORDER BY r.category, r.percentile DESC NULLS LAST`,
|
| 412 |
+
[modelKey]
|
| 413 |
)
|
| 414 |
}
|
| 415 |
|
|
|
|
| 469 |
}
|
| 470 |
|
| 471 |
export async function getModelSummaryById(routeId: string): Promise<ModelEvaluationSummary | null> {
|
| 472 |
+
// Lookups use the addressable identifier (`model_key`/`route_id`/
|
| 473 |
+
// `model_route_id`/`model_family_id`) so unresolved models β whose
|
| 474 |
+
// `model_id` is NULL β are still findable. `model_id` is kept in the
|
| 475 |
+
// OR chain as a back-compat fallback for old links.
|
| 476 |
const rows = await readRows<Row>(
|
| 477 |
`SELECT *
|
| 478 |
FROM models_view
|
| 479 |
+
WHERE model_key = ? OR route_id = ? OR model_route_id = ? OR model_family_id = ? OR model_id = ?
|
| 480 |
LIMIT 1`,
|
| 481 |
+
[routeId, routeId, routeId, routeId, routeId]
|
| 482 |
)
|
| 483 |
const modelRow = rows[0]
|
| 484 |
if (!modelRow) return null
|
| 485 |
|
| 486 |
+
const cellRows = await getModelEvaluationRows(asString(modelRow.model_key ?? modelRow.model_id, routeId))
|
| 487 |
return modelSummaryFromRows(modelRow, cellRows)
|
| 488 |
}
|
| 489 |
|
notes/backend-v2-migration.md
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Frontend migration to backend v2 (Stage J view layer)
|
| 2 |
+
|
| 3 |
+
> **Status:** spec, drafted 2026-05-03 against `eval_card_backend`'s
|
| 4 |
+
> Stage J view-layer contract.
|
| 5 |
+
>
|
| 6 |
+
> **Sources:**
|
| 7 |
+
> - Backend spec (the contract this consumes):
|
| 8 |
+
> `../eval_card_backend/notes/08-frontend-view-layer.md`
|
| 9 |
+
> - Canonical schema (audit/debug only; not in hot path):
|
| 10 |
+
> `../eval_card_backend/notes/01-schema-from-frontend.md`
|
| 11 |
+
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
## Context
|
| 15 |
+
|
| 16 |
+
The legacy producer (`eval_cards_backend_pipeline`) emitted ten
|
| 17 |
+
parquets where each row carried a `payload_json` VARCHAR with the
|
| 18 |
+
post-TS-adapter shape baked in. The frontend's "DuckDB backend"
|
| 19 |
+
(`lib/duckdb-data.ts`) read these blobs and `JSON.parse`d them β column
|
| 20 |
+
projection, filter pushdown, and type contracts were all forfeited.
|
| 21 |
+
|
| 22 |
+
The new producer (`eval_card_backend`) emits a typed view layer over
|
| 23 |
+
its canonical normalised tables. Three Parquet files cover every page
|
| 24 |
+
shape, three small JSON sidecars cover corpus-level scalars and the
|
| 25 |
+
hierarchy tree. Column names match the frontend's TS interfaces
|
| 26 |
+
field-for-field, so the rowβobject cast is a typed spread for most
|
| 27 |
+
accessors. Two interfaces (`ModelResultForBenchmark` and the
|
| 28 |
+
`evaluations_by_category` body of `ModelEvaluationSummary`) require a
|
| 29 |
+
small mechanical reshape over the row, since one nests fields that the
|
| 30 |
+
view stores flat β see the per-accessor sections below. No
|
| 31 |
+
HF-record-to-display adapter logic survives.
|
| 32 |
+
|
| 33 |
+
This document specifies what changes in `general-eval-card` once
|
| 34 |
+
backend v2 is faithfully implemented. **The visual frontend, page
|
| 35 |
+
renderers, and TS interface shapes do not change.** Only the I/O
|
| 36 |
+
boundary moves.
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
|
| 40 |
+
## What changes (overview)
|
| 41 |
+
|
| 42 |
+
| layer | before (v1) | after (v2) |
|
| 43 |
+
|---|---|---|
|
| 44 |
+
| Distribution | `LOCAL_PIPELINE_OUTPUT` env var pointing at a producer output dir; `duckdb/v1/` subpath; implicit "warehouse/latest/" coupling | `SNAPSHOT_URL` env var (file:// or HF dataset URL); one snapshot pinned per deploy |
|
| 45 |
+
| Storage shape | 10 parquets each with one `payload_json` column | 3 typed-column view parquets + 3 JSON sidecars |
|
| 46 |
+
| Read pattern | `SELECT payload_json FROM read_parquet(?) WHERE id = ?`, then `JSON.parse` | `SELECT col1, col2, ... FROM <view> WHERE id = ?`, typed row spread |
|
| 47 |
+
| List vs detail | Separate `*_lite.parquet` files | Column projection on the same parquet |
|
| 48 |
+
| Suite/aggregate dispatch | Eval id prefix (`aggregate__`, `matrix__`) β different parquet | `is_summary_score` flag + `parent_benchmark_id` on `evals_view` |
|
| 49 |
+
| Slug rule | Custom `replace('/', '__')` escapes; per-page slug helpers | Producer-owned RFC 3986 percent-encoded `route_id` / `evaluation_id` / `metric_summary_id`; frontend decodes only on `<Link>` href |
|
| 50 |
+
| Corpus aggregates | `corpus-aggregates.json` over HF JSON loader | `headline.json` sidecar in the snapshot dir |
|
| 51 |
+
| Hierarchy | Synthesised in the producer's `eval_hierarchy` JSON | `hierarchy.json` sidecar |
|
| 52 |
+
| Backend manifest | `manifest.json` fetched from upstream HF dataset root via `lib/hf-data.ts` | `manifest.json` sidecar inside the snapshot dir, read via `SNAPSHOT_URL` |
|
| 53 |
+
|
| 54 |
+
The TS interfaces (`EvaluationCardData`, `BenchmarkEvalSummary`,
|
| 55 |
+
`ModelEvaluationSummary`, `ModelResultForBenchmark`, `CorpusAggregates`,
|
| 56 |
+
`EvalHierarchy`, `BackendManifest`) stay as-is β the producer agreed to
|
| 57 |
+
emit columns under those exact names.
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
## What does not change
|
| 62 |
+
|
| 63 |
+
- All page components under `app/`. The renderer trees are unchanged.
|
| 64 |
+
- TS interface declarations in `lib/benchmark-schema.ts`,
|
| 65 |
+
`lib/eval-processing.ts`, `lib/backend-artifacts.ts`. These are now
|
| 66 |
+
the contract surface β column names match field names by agreement
|
| 67 |
+
with the producer.
|
| 68 |
+
- Component files under `components/`.
|
| 69 |
+
- `lib/glossary.ts`, `lib/known-issues.ts`, `lib/utils.ts`,
|
| 70 |
+
`lib/na-utils.ts` β these are pure presentation helpers.
|
| 71 |
+
- `app/api/*/route.ts` handlers stay as thin pass-throughs to
|
| 72 |
+
`lib/data-backend.ts`.
|
| 73 |
+
|
| 74 |
+
---
|
| 75 |
+
|
| 76 |
+
## Distribution: `SNAPSHOT_URL`
|
| 77 |
+
|
| 78 |
+
Frontend reads `SNAPSHOT_URL` from env at process start. One deploy =
|
| 79 |
+
one snapshot. The URL points at a directory containing the six
|
| 80 |
+
artifacts the frontend reads:
|
| 81 |
+
|
| 82 |
+
```
|
| 83 |
+
$SNAPSHOT_URL/
|
| 84 |
+
βββ models_view.parquet
|
| 85 |
+
βββ evals_view.parquet
|
| 86 |
+
βββ eval_results_view.parquet
|
| 87 |
+
βββ headline.json
|
| 88 |
+
βββ hierarchy.json
|
| 89 |
+
βββ manifest.json
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
Examples:
|
| 93 |
+
|
| 94 |
+
- Local dev: `SNAPSHOT_URL=file:///path/to/eval_card_backend/warehouse/2026-05-03T15-48-59Z`
|
| 95 |
+
- Production (pinned snapshot): `SNAPSHOT_URL=https://huggingface.co/datasets/evaleval/eval-cards-data/resolve/<rev>/warehouse/<snapshot_id>`
|
| 96 |
+
- Production (rolling): `SNAPSHOT_URL=https://huggingface.co/datasets/evaleval/eval-cards-data/resolve/main/warehouse/latest`
|
| 97 |
+
|
| 98 |
+
`LOCAL_PIPELINE_OUTPUT` is removed. The `duckdb/v1/` subpath is
|
| 99 |
+
removed. The producer maintains a `warehouse/latest/` alias that
|
| 100 |
+
points at the most recent snapshot, so deploys can pin either to a
|
| 101 |
+
timestamped snapshot (immutable, redeploy required to roll forward)
|
| 102 |
+
or to `latest` (auto-rolls forward on the next Space rebuild). Within
|
| 103 |
+
a running process the snapshot is still effectively constant β sidecar
|
| 104 |
+
caches in `lib/sidecars.ts` are first-write-wins per process.
|
| 105 |
+
|
| 106 |
+
---
|
| 107 |
+
|
| 108 |
+
## DuckDB connection lifecycle
|
| 109 |
+
|
| 110 |
+
`lib/duckdb.ts` (new file; replaces the connection-management portion
|
| 111 |
+
of `lib/duckdb-data.ts`):
|
| 112 |
+
|
| 113 |
+
```ts
|
| 114 |
+
import "server-only"
|
| 115 |
+
import { DuckDBConnection } from "@duckdb/node-api"
|
| 116 |
+
|
| 117 |
+
let connectionPromise: Promise<DuckDBConnection> | null = null
|
| 118 |
+
|
| 119 |
+
const SNAPSHOT_URL = process.env.SNAPSHOT_URL
|
| 120 |
+
if (!SNAPSHOT_URL) {
|
| 121 |
+
throw new Error("SNAPSHOT_URL must be set; see notes/backend-v2-migration.md")
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
const VIEWS = {
|
| 125 |
+
models_view: `${SNAPSHOT_URL}/models_view.parquet`,
|
| 126 |
+
evals_view: `${SNAPSHOT_URL}/evals_view.parquet`,
|
| 127 |
+
eval_results_view: `${SNAPSHOT_URL}/eval_results_view.parquet`,
|
| 128 |
+
} as const
|
| 129 |
+
|
| 130 |
+
export async function getConnection(): Promise<DuckDBConnection> {
|
| 131 |
+
if (!connectionPromise) {
|
| 132 |
+
connectionPromise = (async () => {
|
| 133 |
+
const conn = await DuckDBConnection.create()
|
| 134 |
+
// httpfs is built into duckdb-node-api; no INSTALL needed.
|
| 135 |
+
// Register each parquet as a view so callers write `FROM models_view`,
|
| 136 |
+
// not the full URL.
|
| 137 |
+
for (const [name, path] of Object.entries(VIEWS)) {
|
| 138 |
+
await conn.run(
|
| 139 |
+
`CREATE OR REPLACE VIEW ${name} AS SELECT * FROM read_parquet(?)`,
|
| 140 |
+
[path]
|
| 141 |
+
)
|
| 142 |
+
}
|
| 143 |
+
return conn
|
| 144 |
+
})()
|
| 145 |
+
}
|
| 146 |
+
return connectionPromise
|
| 147 |
+
}
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
One connection per Node process. Views are registered once at
|
| 151 |
+
startup; subsequent queries write `FROM models_view` rather than
|
| 152 |
+
re-passing the parquet URL. DuckDB's column projection means the cost
|
| 153 |
+
of `SELECT route_id, model_name FROM models_view` is independent of
|
| 154 |
+
how wide `models_view` is.
|
| 155 |
+
|
| 156 |
+
---
|
| 157 |
+
|
| 158 |
+
## Per-accessor mapping
|
| 159 |
+
|
| 160 |
+
`lib/data-backend.ts` keeps its current export names. `lib/duckdb-data.ts`
|
| 161 |
+
gets gutted; each function becomes a thin typed `SELECT`. The mapping
|
| 162 |
+
below uses the column names spec'd in
|
| 163 |
+
`../eval_card_backend/notes/08-frontend-view-layer.md` β the row
|
| 164 |
+
returned by DuckDB casts directly to the TS interface.
|
| 165 |
+
|
| 166 |
+
### Models
|
| 167 |
+
|
| 168 |
+
```ts
|
| 169 |
+
// getModelCards / getModelCardsLite β list pages
|
| 170 |
+
export async function getModelCards(): Promise<EvaluationCardData[]> {
|
| 171 |
+
const conn = await getConnection()
|
| 172 |
+
const reader = await conn.runAndReadAll(`
|
| 173 |
+
SELECT id, route_id, model_name, model_id, canonical_model_name, developer,
|
| 174 |
+
evaluations_count, benchmarks_count, variant_count,
|
| 175 |
+
categories, category_stats, latest_timestamp,
|
| 176 |
+
evaluator_count, evaluator_names, source_type_count, source_types,
|
| 177 |
+
evidence_count, missing_generation_config_count,
|
| 178 |
+
third_party_eval_count, independent_verification_ratio,
|
| 179 |
+
reproducibility_status, eval_libraries, latest_source_name,
|
| 180 |
+
params_billions, benchmark_names, score_summary,
|
| 181 |
+
reproducibility_summary, provenance_summary, comparability_summary,
|
| 182 |
+
top_scores, source_urls, detail_urls,
|
| 183 |
+
model_url, release_date, input_modalities, output_modalities,
|
| 184 |
+
architecture, params, inference_engine, inference_platform
|
| 185 |
+
FROM models_view
|
| 186 |
+
ORDER BY latest_timestamp DESC
|
| 187 |
+
`)
|
| 188 |
+
return reader.getRowObjects() as EvaluationCardData[]
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
// "Lite" is just narrower projection β same parquet, fewer columns.
|
| 192 |
+
export async function getModelCardsLite(): Promise<EvaluationCardData[]> {
|
| 193 |
+
const conn = await getConnection()
|
| 194 |
+
const reader = await conn.runAndReadAll(`
|
| 195 |
+
SELECT id, route_id, model_name, model_id, developer,
|
| 196 |
+
evaluations_count, benchmarks_count, categories,
|
| 197 |
+
latest_timestamp, third_party_eval_count,
|
| 198 |
+
independent_verification_ratio, reproducibility_status,
|
| 199 |
+
latest_source_name, params_billions
|
| 200 |
+
FROM models_view
|
| 201 |
+
ORDER BY benchmarks_count DESC, evaluations_count DESC, model_name ASC
|
| 202 |
+
`)
|
| 203 |
+
return reader.getRowObjects() as EvaluationCardData[]
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
// getModelSummaryById β detail page.
|
| 207 |
+
//
|
| 208 |
+
// The row carries the metadata shell (variants[], categories,
|
| 209 |
+
// category_stats, signal summaries, model_family_id, raw_model_ids,
|
| 210 |
+
// total_evaluations, last_updated). The full `ModelEvaluationSummary`
|
| 211 |
+
// also requires `evaluations_by_category: Record<CategoryType,
|
| 212 |
+
// BenchmarkEvaluation[]>`, which is a heavyweight per-cell breakdown β
|
| 213 |
+
// produced by a separate join over `eval_results_view`, see
|
| 214 |
+
// `getModelEvaluationCells` below.
|
| 215 |
+
//
|
| 216 |
+
// Returning a `ModelSummaryShell` (Omit-ed type, defined alongside the
|
| 217 |
+
// existing TS interface) makes the contract explicit and stops the cast
|
| 218 |
+
// from lying. The model-detail page composes the full
|
| 219 |
+
// `ModelEvaluationSummary` from `shell` + `cells`.
|
| 220 |
+
export type ModelSummaryShell = Omit<
|
| 221 |
+
ModelEvaluationSummary,
|
| 222 |
+
"evaluations_by_category"
|
| 223 |
+
>
|
| 224 |
+
|
| 225 |
+
export async function getModelSummaryById(routeId: string): Promise<ModelSummaryShell | null> {
|
| 226 |
+
const conn = await getConnection()
|
| 227 |
+
const reader = await conn.runAndReadAll(
|
| 228 |
+
`SELECT * FROM models_view WHERE route_id = ? OR model_family_id = ? LIMIT 1`,
|
| 229 |
+
[routeId, routeId]
|
| 230 |
+
)
|
| 231 |
+
const rows = reader.getRowObjects()
|
| 232 |
+
if (rows.length === 0) return null
|
| 233 |
+
return rows[0] as unknown as ModelSummaryShell
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
// Per-cell reshape helper. `eval_results_view` rows carry the per-cell
|
| 237 |
+
// fields scattered (model_info, score_details, evaluation_timestamp,
|
| 238 |
+
// source_metadata, source_data, metric_*, etc.) rather than under a
|
| 239 |
+
// nested `result: EvaluationResult` STRUCT. Reshape into the
|
| 240 |
+
// `ModelResultForBenchmark` shape the leaderboard / model-detail
|
| 241 |
+
// renderers expect. Single helper; reused by getEvalSummaryById and
|
| 242 |
+
// getModelEvaluationCells. No HF-record-to-display logic survives.
|
| 243 |
+
function reshapeCellToModelResult(row: Record<string, any>): ModelResultForBenchmark {
|
| 244 |
+
return {
|
| 245 |
+
model_info: row.model_info,
|
| 246 |
+
model_route_id: row.model_route_id,
|
| 247 |
+
score: row.score,
|
| 248 |
+
score_details: row.score_details,
|
| 249 |
+
evaluation_timestamp: row.evaluation_timestamp,
|
| 250 |
+
source_metadata: row.source_metadata,
|
| 251 |
+
source_data: row.source_data,
|
| 252 |
+
source_record_url: row.source_record_url,
|
| 253 |
+
aggregate_components: row.aggregate_components,
|
| 254 |
+
result: {
|
| 255 |
+
evaluation_name: row.metric_display_name,
|
| 256 |
+
metric_summary_id: row.metric_summary_id,
|
| 257 |
+
metric_key: row.metric_id,
|
| 258 |
+
evaluation_timestamp: row.evaluation_timestamp,
|
| 259 |
+
metric_config: { lower_is_better: row.lower_is_better, unit: row.metric_unit, /* β¦denormalised metaβ¦ */ },
|
| 260 |
+
score_details: row.score_details,
|
| 261 |
+
evalcards: row.evalcards_annotations ? { annotations: row.evalcards_annotations } : undefined,
|
| 262 |
+
},
|
| 263 |
+
}
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
// Helper for the model-detail page's evaluations_by_category body.
|
| 267 |
+
// The page groups by `category` in TS after this returns.
|
| 268 |
+
export async function getModelEvaluationCells(modelId: string): Promise<ModelResultForBenchmark[]> {
|
| 269 |
+
const conn = await getConnection()
|
| 270 |
+
const reader = await conn.runAndReadAll(
|
| 271 |
+
`SELECT * FROM eval_results_view WHERE model_id = ? ORDER BY category, percentile DESC`,
|
| 272 |
+
[modelId]
|
| 273 |
+
)
|
| 274 |
+
return reader.getRowObjects().map(reshapeCellToModelResult)
|
| 275 |
+
}
|
| 276 |
+
```
|
| 277 |
+
|
| 278 |
+
### Evals
|
| 279 |
+
|
| 280 |
+
```ts
|
| 281 |
+
// getEvalListData / getEvalListLiteData β list pages
|
| 282 |
+
export async function getEvalListData(): Promise<{
|
| 283 |
+
evals: BenchmarkEvalListItem[]
|
| 284 |
+
totalModels: number
|
| 285 |
+
}> {
|
| 286 |
+
const conn = await getConnection()
|
| 287 |
+
const [evalsReader, modelsReader] = await Promise.all([
|
| 288 |
+
conn.runAndReadAll(`
|
| 289 |
+
SELECT evaluation_id, evaluation_name, canonical_display_name,
|
| 290 |
+
composite_benchmark_key, composite_benchmark_name,
|
| 291 |
+
benchmark_family_key, benchmark_leaf_key, category,
|
| 292 |
+
metric_config, models_count, evaluator_names, source_types,
|
| 293 |
+
latest_source_name, third_party_ratio,
|
| 294 |
+
missing_generation_config_count, best_model, worst_model,
|
| 295 |
+
avg_score, avg_score_norm, has_card,
|
| 296 |
+
is_aggregated, aggregate_sources, tags,
|
| 297 |
+
metrics_count, metric_names, instance_data, top_score,
|
| 298 |
+
subtasks_count, is_summary_score, summary_eval_ids,
|
| 299 |
+
root_metrics, subtasks, leaderboard_metrics,
|
| 300 |
+
reproducibility_summary, provenance_summary, comparability_summary,
|
| 301 |
+
source_data
|
| 302 |
+
FROM evals_view
|
| 303 |
+
ORDER BY evaluation_name ASC
|
| 304 |
+
`),
|
| 305 |
+
conn.runAndReadAll(`SELECT COUNT(*) AS n FROM models_view`),
|
| 306 |
+
])
|
| 307 |
+
return {
|
| 308 |
+
evals: evalsReader.getRowObjects() as BenchmarkEvalListItem[],
|
| 309 |
+
totalModels: Number(modelsReader.getRowObjects()[0].n),
|
| 310 |
+
}
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
// getEvalSummaryById β detail page.
|
| 314 |
+
//
|
| 315 |
+
// No more aggregate__/matrix__ id-prefix dispatch β `evals_view` is the
|
| 316 |
+
// single source for all eval shapes. Suite-vs-leaf is a column
|
| 317 |
+
// (`is_summary_score`, `is_aggregated`) on the same parquet.
|
| 318 |
+
//
|
| 319 |
+
// `model_results[]` rows go through the same reshape helper as
|
| 320 |
+
// `getModelEvaluationCells` (defined below) β they share the
|
| 321 |
+
// ModelResultForBenchmark target shape, so the eval/metric/cell
|
| 322 |
+
// β BenchmarkEvaluation reshape is one helper, two callers.
|
| 323 |
+
export async function getEvalSummaryById(evalId: string): Promise<BenchmarkEvalSummary | null> {
|
| 324 |
+
const conn = await getConnection()
|
| 325 |
+
const [evalReader, cellsReader] = await Promise.all([
|
| 326 |
+
conn.runAndReadAll(
|
| 327 |
+
`SELECT * FROM evals_view WHERE evaluation_id = ? LIMIT 1`,
|
| 328 |
+
[evalId]
|
| 329 |
+
),
|
| 330 |
+
conn.runAndReadAll(
|
| 331 |
+
`SELECT * FROM eval_results_view
|
| 332 |
+
WHERE evaluation_id = ?
|
| 333 |
+
AND metric_id = (SELECT primary_metric_id FROM evals_view WHERE evaluation_id = ?)
|
| 334 |
+
ORDER BY position ASC`,
|
| 335 |
+
[evalId, evalId]
|
| 336 |
+
),
|
| 337 |
+
])
|
| 338 |
+
const evalRows = evalReader.getRowObjects()
|
| 339 |
+
if (evalRows.length === 0) return null
|
| 340 |
+
return {
|
| 341 |
+
...(evalRows[0] as Omit<BenchmarkEvalSummary, "model_results">),
|
| 342 |
+
model_results: cellsReader.getRowObjects().map(reshapeCellToModelResult),
|
| 343 |
+
} as BenchmarkEvalSummary
|
| 344 |
+
}
|
| 345 |
+
```
|
| 346 |
+
|
| 347 |
+
### Developers
|
| 348 |
+
|
| 349 |
+
```ts
|
| 350 |
+
// getDeveloperList β list page; reads from headline.json (precomputed,
|
| 351 |
+
// including producer-owned route_id, model/benchmark/evaluation counts,
|
| 352 |
+
// and popular_evals). DeveloperListEntry is satisfied directly by the
|
| 353 |
+
// headline entry shape.
|
| 354 |
+
export async function getDeveloperList(): Promise<DeveloperListEntry[]> {
|
| 355 |
+
const headline = await fetchHeadline()
|
| 356 |
+
return headline.developers as DeveloperListEntry[]
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
// getDeveloperSummaryById β detail page; reads models_view filtered by developer.
|
| 360 |
+
// The route_id on headline.developers[] is the canonical lookup key β we don't
|
| 361 |
+
// re-derive `developer` from the URL slug, since percent-decoding may not
|
| 362 |
+
// round-trip exactly to the producer's source string.
|
| 363 |
+
export async function getDeveloperSummaryById(routeId: string) {
|
| 364 |
+
const headline = await fetchHeadline()
|
| 365 |
+
const headlineEntry = headline.developers.find((d) => d.route_id === routeId)
|
| 366 |
+
if (!headlineEntry) return null
|
| 367 |
+
const conn = await getConnection()
|
| 368 |
+
const reader = await conn.runAndReadAll(
|
| 369 |
+
`SELECT * FROM models_view WHERE developer = ?`,
|
| 370 |
+
[headlineEntry.developer]
|
| 371 |
+
)
|
| 372 |
+
const models = reader.getRowObjects() as EvaluationCardData[]
|
| 373 |
+
return { ...headlineEntry, models }
|
| 374 |
+
}
|
| 375 |
+
```
|
| 376 |
+
|
| 377 |
+
### Dashboard convenience accessor
|
| 378 |
+
|
| 379 |
+
```ts
|
| 380 |
+
// Was: { models, evals } over both legacy parquets; same shape, new sources.
|
| 381 |
+
export async function getDashboardData() {
|
| 382 |
+
const [models, evalListData] = await Promise.all([
|
| 383 |
+
getModelCards(),
|
| 384 |
+
getEvalListData(),
|
| 385 |
+
])
|
| 386 |
+
return { models, evals: evalListData.evals }
|
| 387 |
+
}
|
| 388 |
+
```
|
| 389 |
+
|
| 390 |
+
---
|
| 391 |
+
|
| 392 |
+
## Sidecar fetchers (replace `lib/hf-data.ts` corpus calls)
|
| 393 |
+
|
| 394 |
+
Three small JSON files live in the snapshot dir alongside the
|
| 395 |
+
parquets. New module `lib/sidecars.ts` exposes typed fetchers.
|
| 396 |
+
`lib/hf-data.ts`'s `fetchCorpusAggregates`, `fetchEvalHierarchy`,
|
| 397 |
+
`fetchBackendManifest`, and `fetchBackendManifestStatus` get their
|
| 398 |
+
implementations replaced β same export names, new sources.
|
| 399 |
+
|
| 400 |
+
```ts
|
| 401 |
+
// lib/sidecars.ts
|
| 402 |
+
import "server-only"
|
| 403 |
+
import type {
|
| 404 |
+
CorpusAggregates,
|
| 405 |
+
EvalHierarchy,
|
| 406 |
+
BackendManifest,
|
| 407 |
+
} from "@/lib/backend-artifacts"
|
| 408 |
+
|
| 409 |
+
const SNAPSHOT_URL = process.env.SNAPSHOT_URL!
|
| 410 |
+
|
| 411 |
+
let cache: {
|
| 412 |
+
manifest?: Promise<BackendManifest>
|
| 413 |
+
headline?: Promise<CorpusAggregates>
|
| 414 |
+
hierarchy?: Promise<EvalHierarchy>
|
| 415 |
+
} = {}
|
| 416 |
+
|
| 417 |
+
async function fetchJson<T>(name: string): Promise<T> {
|
| 418 |
+
const url = `${SNAPSHOT_URL}/${name}`
|
| 419 |
+
const res = url.startsWith("file://")
|
| 420 |
+
? await import("fs/promises").then((fs) => fs.readFile(new URL(url), "utf8"))
|
| 421 |
+
: await fetch(url, { next: { revalidate: 3600 } }).then((r) => r.text())
|
| 422 |
+
return JSON.parse(typeof res === "string" ? res : res.toString()) as T
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
export function fetchManifest(): Promise<BackendManifest> {
|
| 426 |
+
return (cache.manifest ??= fetchJson<BackendManifest>("manifest.json"))
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
export function fetchHeadline(): Promise<CorpusAggregates> {
|
| 430 |
+
return (cache.headline ??= fetchJson<CorpusAggregates>("headline.json"))
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
export function fetchHierarchy(): Promise<EvalHierarchy> {
|
| 434 |
+
return (cache.hierarchy ??= fetchJson<EvalHierarchy>("hierarchy.json"))
|
| 435 |
+
}
|
| 436 |
+
```
|
| 437 |
+
|
| 438 |
+
Then in `lib/hf-data.ts`:
|
| 439 |
+
|
| 440 |
+
```ts
|
| 441 |
+
// fetchBackendManifest: was a fetchHFJsonSafe call; now reads the snapshot sidecar.
|
| 442 |
+
export const fetchBackendManifest = fetchManifest
|
| 443 |
+
export const fetchCorpusAggregates = fetchHeadline
|
| 444 |
+
export const fetchEvalHierarchy = fetchHierarchy
|
| 445 |
+
|
| 446 |
+
// fetchBackendManifestStatus: simplified β single snapshot pin, no "latest" comparison.
|
| 447 |
+
export async function fetchBackendManifestStatus(): Promise<BackendManifestStatus> {
|
| 448 |
+
const m = await fetchManifest()
|
| 449 |
+
return {
|
| 450 |
+
currentManifest: m,
|
| 451 |
+
latestManifest: m, // no separate "latest" β snapshot is pinned
|
| 452 |
+
currentManifestSignature: m.generated_at,
|
| 453 |
+
latestManifestSignature: m.generated_at,
|
| 454 |
+
updateAvailable: false,
|
| 455 |
+
refreshing: false,
|
| 456 |
+
pendingRefreshCount: 0,
|
| 457 |
+
}
|
| 458 |
+
}
|
| 459 |
+
```
|
| 460 |
+
|
| 461 |
+
---
|
| 462 |
+
|
| 463 |
+
## What deletes
|
| 464 |
+
|
| 465 |
+
After v2 is live, the following code is dead and can be removed in a
|
| 466 |
+
follow-up cleanup:
|
| 467 |
+
|
| 468 |
+
- `lib/duckdb-data.ts` β replaced by typed SELECTs split between
|
| 469 |
+
`lib/duckdb.ts` (connection) and `lib/data-backend.ts` (queries).
|
| 470 |
+
- The `payload_json` parser helpers (`parsePayload`, `readPayloads`,
|
| 471 |
+
`readPayloadById`, `assertDeveloperListShape`) β no JSON blobs to
|
| 472 |
+
parse.
|
| 473 |
+
- The `aggregate__` / `matrix__` eval-id prefix dispatch in
|
| 474 |
+
`getEvalSummaryByIdFromDuckDB` β the typed view is the only path.
|
| 475 |
+
- `lib/model-data.ts` β most of its functions exist to convert HF
|
| 476 |
+
JSON records into `BenchmarkEvaluation` / `EvaluationCardData`. Once
|
| 477 |
+
the producer emits those shapes directly, the adapter logic deletes.
|
| 478 |
+
Keep only the helpers that don't touch HF records (slug parsing,
|
| 479 |
+
display formatters).
|
| 480 |
+
- `lib/eval-processing.ts` β the `groupEvaluationsByModel`,
|
| 481 |
+
`createModelSummary`, `createBenchmarkEvalSummary`, and
|
| 482 |
+
`inferCategoryFromBenchmark` adapter functions are no longer called
|
| 483 |
+
in the data path. The exported types stay.
|
| 484 |
+
- `scripts/audit-adapters.mjs`, `scripts/dump-adapter-outputs.mts`,
|
| 485 |
+
`scripts/compare-data-backends.mjs`, `scripts/refresh-fixtures.mjs`,
|
| 486 |
+
`scripts/cache-hf-data.mjs` β adapter / parity-check tooling for the
|
| 487 |
+
legacy pipeline. Delete once v1 is retired.
|
| 488 |
+
- `data/models/`, `data/developers/`, `data/benchmarks.json`,
|
| 489 |
+
`data/models.json`, `data/developers.json` β bundled snapshots of
|
| 490 |
+
v1 output for fixture tests. Replace with v2 fixtures if needed.
|
| 491 |
+
- `LOCAL_PIPELINE_OUTPUT` env var, `duckdb/v1/` subpath conventions,
|
| 492 |
+
and the parity-emitter expectations documented in
|
| 493 |
+
`lib/duckdb-data.ts`'s preamble.
|
| 494 |
+
- `inferCategoryFromBenchmark` regex chain in
|
| 495 |
+
`lib/benchmark-schema.ts` β producer is the source of truth for
|
| 496 |
+
category. Keep the `EVALUATION_CATEGORIES` const + `CategoryType`
|
| 497 |
+
type; delete the inference function and `BENCHMARK_PRIORITY_RULES`.
|
| 498 |
+
|
| 499 |
+
---
|
| 500 |
+
|
| 501 |
+
## Slug rule
|
| 502 |
+
|
| 503 |
+
Producer emits all URL-bearing identifiers in
|
| 504 |
+
RFC 3986 percent-encoded form (`route_id`, `evaluation_id`,
|
| 505 |
+
`metric_summary_id`). Frontend treats them as opaque except for
|
| 506 |
+
`<Link>` href construction:
|
| 507 |
+
|
| 508 |
+
```tsx
|
| 509 |
+
// Old: href={`/models/${model.route_id}`} // route_id was already escaped via __ rule
|
| 510 |
+
// New: href={`/models/${model.route_id}`} // same code; route_id is now percent-encoded
|
| 511 |
+
```
|
| 512 |
+
|
| 513 |
+
Decode happens inside the route handler when looking up by slug:
|
| 514 |
+
|
| 515 |
+
```ts
|
| 516 |
+
// app/models/[id]/page.tsx
|
| 517 |
+
export default async function ModelDetailPage({ params }: { params: { id: string } }) {
|
| 518 |
+
const summary = await getModelSummaryById(params.id) // pass encoded form straight through
|
| 519 |
+
...
|
| 520 |
+
}
|
| 521 |
+
```
|
| 522 |
+
|
| 523 |
+
`getModelSummaryById` looks up by `route_id = ?` directly without
|
| 524 |
+
decoding β the producer's `route_id` column matches the URL path
|
| 525 |
+
segment byte-for-byte. The legacy `replace('/', '__')` and
|
| 526 |
+
`replace(/\//g, ...)` helpers in `lib/utils.ts` and `lib/model-family.ts`
|
| 527 |
+
become dead code; remove them in the cleanup pass.
|
| 528 |
+
|
| 529 |
+
---
|
| 530 |
+
|
| 531 |
+
## Migration strategy
|
| 532 |
+
|
| 533 |
+
A feature flag gates v1 vs v2 during the transition:
|
| 534 |
+
|
| 535 |
+
```ts
|
| 536 |
+
// lib/data-backend.ts
|
| 537 |
+
const BACKEND_VERSION = process.env.DATA_BACKEND ?? "v1"
|
| 538 |
+
|
| 539 |
+
export const getModelCards =
|
| 540 |
+
BACKEND_VERSION === "v2"
|
| 541 |
+
? (await import("@/lib/duckdb")).getModelCards
|
| 542 |
+
: (await import("@/lib/duckdb-data")).getModelCardsFromDuckDB
|
| 543 |
+
// ... same pattern for other accessors
|
| 544 |
+
```
|
| 545 |
+
|
| 546 |
+
Phase plan:
|
| 547 |
+
|
| 548 |
+
1. **Producer ships Stage J.** `eval_card_backend` emits the six
|
| 549 |
+
v2 artifacts in `warehouse/<snapshot_id>/`. Existing canonical
|
| 550 |
+
parquets stay alongside.
|
| 551 |
+
2. **Frontend lands `lib/duckdb.ts` + `lib/sidecars.ts`** behind the
|
| 552 |
+
`DATA_BACKEND=v2` flag. CI builds both backends; default stays v1.
|
| 553 |
+
3. **Smoke test in dev with `DATA_BACKEND=v2`,
|
| 554 |
+
`SNAPSHOT_URL=file://...`.** Verify each page renders identical
|
| 555 |
+
bytes (modulo source-of-data labels). Where they diverge, file
|
| 556 |
+
producer issues β do not patch the frontend to paper over.
|
| 557 |
+
4. **Flip the production default to v2.** Keep v1 path compilable but
|
| 558 |
+
unreachable. Monitor for a release.
|
| 559 |
+
5. **Delete v1 path** (the "What deletes" list above).
|
| 560 |
+
|
| 561 |
+
The flag is intentionally process-wide, not per-accessor. Mixing
|
| 562 |
+
backends within one render produces inconsistent snapshots.
|
| 563 |
+
|
| 564 |
+
---
|
| 565 |
+
|
| 566 |
+
## What doesn't move
|
| 567 |
+
|
| 568 |
+
- **Instance-level data fetching** (`fetchInstanceLevelData` in
|
| 569 |
+
`lib/hf-data.ts`). Instance JSONL is referenced by URL in
|
| 570 |
+
`eval_results_view.instance_file_path`; the lazy-load stays. Pointer
|
| 571 |
+
shape on the row is unchanged from v1.
|
| 572 |
+
- **Benchmark card metadata** lives inside `evals_view.benchmark_card`
|
| 573 |
+
STRUCT now, not a separate `benchmark_card_*.json` per file. The
|
| 574 |
+
page reads it from the eval row directly. Adapter-style readers
|
| 575 |
+
(`fetchBenchmarkMetadataMap`) become a `SELECT benchmark_id, benchmark_card
|
| 576 |
+
FROM evals_view` aggregation if anything still calls them β most
|
| 577 |
+
callers should fold into `getEvalSummaryById`.
|
| 578 |
+
- **EvalCards annotations** (`evalcards.annotations`) live on
|
| 579 |
+
`eval_results_view.evalcards_annotations` per-row. The eval-detail
|
| 580 |
+
page reads them inline; no separate fetcher.
|
| 581 |
+
|
| 582 |
+
---
|
| 583 |
+
|
| 584 |
+
## Open questions / risks
|
| 585 |
+
|
| 586 |
+
- **httpfs cold-start latency.** First query against an HF-hosted
|
| 587 |
+
parquet pays a round trip per file. Mitigate by pre-registering all
|
| 588 |
+
three views at process start (above), so the first user query hits
|
| 589 |
+
warm metadata. Measure on the production HF Space; if too slow,
|
| 590 |
+
consider downloading the snapshot to local disk at container start
|
| 591 |
+
(~MB per snapshot).
|
| 592 |
+
- **Connection lifetime in serverless.** Vercel's serverless
|
| 593 |
+
runtime tears down the Node process per request; the
|
| 594 |
+
`connectionPromise` cache doesn't help. The HF Space deployment
|
| 595 |
+
(Docker, long-lived) is unaffected. If we ever target serverless,
|
| 596 |
+
switch to `duckdb-wasm` in the browser or a separate serving
|
| 597 |
+
process.
|
| 598 |
+
- **`aggregate_components[]` on `eval_results_view`.** This array is
|
| 599 |
+
the per-suite-component breakdown for rollup rows. For non-rollup
|
| 600 |
+
rows it's always empty. If suite rollups grow common, the storage
|
| 601 |
+
cost of trailing-empty arrays is non-trivial; consider splitting
|
| 602 |
+
into a dedicated parquet at that point.
|
| 603 |
+
- **Category drift.** Producer's `category_mapping.json` will lag real
|
| 604 |
+
benchmark tag changes. The mapping is producer-owned, so the
|
| 605 |
+
frontend can't patch around drift β this is a feature, not a bug,
|
| 606 |
+
but it requires operator discipline. Surface "uncategorised
|
| 607 |
+
benchmark count" in the producer's run summary and the home-page
|
| 608 |
+
manifest banner.
|
| 609 |
+
- **Type widening for `score_summary` etc.** The producer emits these
|
| 610 |
+
as DuckDB STRUCTs; the TS interface declares them as nested
|
| 611 |
+
`{ count, min, max, average }`. `runAndReadAll` returns nested
|
| 612 |
+
STRUCTs as plain JS objects, so the cast works β but if duckdb-node
|
| 613 |
+
changes its STRUCT serialisation, audit the `as` casts here. Add a
|
| 614 |
+
dev-only validator that runs `EvaluationCardData`'s shape check at
|
| 615 |
+
the row level on the first `getModelCards()` call after process
|
| 616 |
+
start.
|
notes/merge-cheatsheet-backend-v2.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Merge cheatsheet: pulling `main` into `feat/use-new-backend-data`
|
| 2 |
+
|
| 3 |
+
> Drafted 2026-05-04, before pulling. Companion to `backend-v2-migration.md`
|
| 4 |
+
> (which is the design doc). This file is just a per-file conflict guide.
|
| 5 |
+
>
|
| 6 |
+
> Branch: `feat/use-new-backend-data` (2 commits ahead of `main`:
|
| 7 |
+
> `7635aee` Integrate with test backend data, `bfce8f2` Drop
|
| 8 |
+
> input/output_modalities from MODEL_CARD_COLUMNS).
|
| 9 |
+
|
| 10 |
+
## Triage at a glance
|
| 11 |
+
|
| 12 |
+
| File | Risk | Strategy |
|
| 13 |
+
|---|---|---|
|
| 14 |
+
| `lib/data-backend.ts` | **High** | Keep ours wholesale; re-port any new accessors main added |
|
| 15 |
+
| `lib/backend-artifacts.ts` | **High** | Keep our schema renames; reconcile any *new* main-side fields against producer output |
|
| 16 |
+
| `components/signals/corpus-dashboard.tsx` | **Med** | Keep main's UI structure; rewire data fields to v2 names |
|
| 17 |
+
| `components/signals/corpus-signals-strip.tsx` | **Med** | Same as above |
|
| 18 |
+
| `lib/hf-data.ts` | **Med** | Keep `useViewLayerBackend()` short-circuits at top of 5 fetchers |
|
| 19 |
+
| `Dockerfile` | **Med** | Keep our `DATA_BACKEND=v2` + `SNAPSHOT_URL` wiring; layer main's other changes on top |
|
| 20 |
+
| `lib/benchmark-schema.ts` | **Low** | Trivial 1-line addition (`num_few_shot?`) |
|
| 21 |
+
| `app/page.tsx` | **Low** | One-line copy change (`corpus-aggregates.json` β `headline.json`) |
|
| 22 |
+
|
| 23 |
+
New files (no conflict possible): `lib/view-data.ts`, `lib/duckdb.ts`,
|
| 24 |
+
`lib/sidecars.ts`, `tests/view-data.test.ts`,
|
| 25 |
+
`notes/backend-v2-migration.md`.
|
| 26 |
+
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
## `lib/data-backend.ts` β High
|
| 30 |
+
|
| 31 |
+
**What we did:** Replaced static re-exports from `lib/duckdb-data` with
|
| 32 |
+
a `BACKEND_VERSION` env-flag dispatcher. Each accessor now branches on
|
| 33 |
+
`useViewLayerBackend()` (true when `DATA_BACKEND=v2` or `stage-j`) and
|
| 34 |
+
lazy-imports either `@/lib/view-data` or `@/lib/duckdb-data`.
|
| 35 |
+
Manifest/hierarchy accessors branch between `@/lib/sidecars` and
|
| 36 |
+
`@/lib/hf-data`.
|
| 37 |
+
|
| 38 |
+
**Reconcile:**
|
| 39 |
+
- Conflict almost certain if main touched any export wiring here.
|
| 40 |
+
- **Keep our file as-is.** The dispatcher pattern is load-bearing.
|
| 41 |
+
- If main added a new accessor (e.g. `getFooBar`), add a new dispatcher
|
| 42 |
+
function following the same pattern β only the legacy branch needs
|
| 43 |
+
to be wired immediately; v2 branch can throw `Not implemented` until
|
| 44 |
+
`lib/view-data.ts` adds it.
|
| 45 |
+
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
## `lib/backend-artifacts.ts` β High
|
| 49 |
+
|
| 50 |
+
**What we did:** Renamed corpus-block fields to match what the v2
|
| 51 |
+
producer emits:
|
| 52 |
+
|
| 53 |
+
| Block | v1 (main) | v2 (ours) |
|
| 54 |
+
|---|---|---|
|
| 55 |
+
| Completeness | `total_benchmarks`, `completeness_score_mean`, `completeness_score_median`, `per_field_population{}` | `total_triples`, `completeness_avg`, `completeness_min`, `completeness_max` |
|
| 56 |
+
| Provenance | `multi_source_groups`, `multi_source_rate`, `first_party_only_groups`, `first_party_only_rate`, `total_groups` | `multi_source_triples`, `first_party_only_triples`, `total_triples` (rates dropped β derived in components via local `rate()` helper) |
|
| 57 |
+
| Comparability | `variant_eligible_groups`, `variant_divergent_groups`, `variant_divergence_rate`, `cross_party_eligible_groups`, `cross_party_divergent_groups`, `cross_party_divergence_rate`, `total_groups` | `total_triples`, `variant_divergent_count`, `cross_party_divergent_count`, `groups_with_variant_check`, `groups_with_cross_party_check` |
|
| 58 |
+
|
| 59 |
+
Also added: `DeveloperListEntry` interface, optional
|
| 60 |
+
`developers/families/categories` arrays on `CorpusAggregates`,
|
| 61 |
+
optional `eval_hierarchy` key in `BackendManifest.summary_artifacts`.
|
| 62 |
+
|
| 63 |
+
**Reconcile:**
|
| 64 |
+
- Producer is the source of truth for v2 field names β do **not** add
|
| 65 |
+
back v1 names to satisfy a main-side change. If main added a field
|
| 66 |
+
the v2 producer doesn't emit, either drop it or check
|
| 67 |
+
`eval_card_backend/notes/08-frontend-view-layer.md` first.
|
| 68 |
+
- Keep all three new optional sections on `CorpusAggregates`
|
| 69 |
+
(developers, families, categories) β they back the new
|
| 70 |
+
developer-list path.
|
| 71 |
+
- The `summary_artifacts.eval_hierarchy` key is additive; safe to keep
|
| 72 |
+
alongside whatever main added there.
|
| 73 |
+
|
| 74 |
+
---
|
| 75 |
+
|
| 76 |
+
## `components/signals/corpus-dashboard.tsx` β Medium
|
| 77 |
+
|
| 78 |
+
**What we did:** Mechanical rewrite of every field reference in this
|
| 79 |
+
file to use the v2 names from `lib/backend-artifacts.ts` (above).
|
| 80 |
+
Removed the `per_field_population` per-field grid and replaced it with
|
| 81 |
+
a `min / avg / max` MiniMetric trio. Added a local `rate(num, denom)`
|
| 82 |
+
helper (returns null if either side is null/zero) since v2 stores
|
| 83 |
+
counts, not pre-computed rates. Title-cased `CATEGORY_ORDER`
|
| 84 |
+
(`"Agentic"`, `"General"`, β¦) and made the keys-to-render set extend
|
| 85 |
+
gracefully to unknown categories.
|
| 86 |
+
|
| 87 |
+
**Reconcile:**
|
| 88 |
+
- If main touched this file for design/UX reasons, **prefer main's
|
| 89 |
+
visual structure** β but keep our field accessors. The recipe is:
|
| 90 |
+
- Anywhere main reads `multi_source_rate`, replace with `rate(prov.multi_source_triples, prov.total_triples)`.
|
| 91 |
+
- Anywhere main reads `completeness_score_mean`, replace with `comp.completeness_avg`.
|
| 92 |
+
- Anywhere main reads `*_eligible_groups` / `*_divergent_groups`, swap to `groups_with_*_check` / `*_divergent_count`.
|
| 93 |
+
- Drop any new code that reads `per_field_population` β gone in v2.
|
| 94 |
+
- Keep the local `rate()` helper at the bottom of the file.
|
| 95 |
+
- Category lookup must use the new title-cased keys (or stay tolerant
|
| 96 |
+
via the `available` set logic we added).
|
| 97 |
+
|
| 98 |
+
---
|
| 99 |
+
|
| 100 |
+
## `components/signals/corpus-signals-strip.tsx` β Medium
|
| 101 |
+
|
| 102 |
+
**What we did:** Same field renames as above, same local `rate()`
|
| 103 |
+
helper added. Headline copy updated from "groups" β "triples" where
|
| 104 |
+
the underlying unit changed.
|
| 105 |
+
|
| 106 |
+
**Reconcile:** Apply the same recipe as `corpus-dashboard.tsx`. The
|
| 107 |
+
two files share field names and the `rate()` helper.
|
| 108 |
+
|
| 109 |
+
---
|
| 110 |
+
|
| 111 |
+
## `lib/hf-data.ts` β Medium
|
| 112 |
+
|
| 113 |
+
**What we did:** Added an early-return guard at the top of five
|
| 114 |
+
functions:
|
| 115 |
+
- `fetchBackendManifestStatus` β synthesizes a status from the v2 manifest sidecar
|
| 116 |
+
- `fetchBenchmarkMetadataMap` β delegates to `view-data.getBenchmarkMetadataMap`
|
| 117 |
+
- `fetchBackendManifest` β delegates to `sidecars.fetchManifest`
|
| 118 |
+
- `fetchEvalHierarchy` β delegates to `sidecars.fetchHierarchy` (still wraps in `adaptEvalHierarchy`)
|
| 119 |
+
- `fetchCorpusAggregates` β delegates to `sidecars.fetchHeadline`
|
| 120 |
+
|
| 121 |
+
Plus a module-level `useViewLayerBackend()` helper and a lazy
|
| 122 |
+
`fetchSnapshotSidecars()` importer near the top of the file.
|
| 123 |
+
|
| 124 |
+
**Reconcile:**
|
| 125 |
+
- These are all additive guards at the start of existing functions β
|
| 126 |
+
conflicts are likely only if main re-shaped the same function
|
| 127 |
+
bodies.
|
| 128 |
+
- Pattern: `if (useViewLayerBackend()) { return <v2 path> }` then fall
|
| 129 |
+
through to the existing v1 implementation untouched.
|
| 130 |
+
- If main renamed one of these functions, port the guard into the
|
| 131 |
+
renamed version. Don't drop the guard.
|
| 132 |
+
|
| 133 |
+
---
|
| 134 |
+
|
| 135 |
+
## `Dockerfile` β Medium
|
| 136 |
+
|
| 137 |
+
**What we did:**
|
| 138 |
+
- Default `ARG DATA_BACKEND` flipped from `duckdb` β `v2` in **both**
|
| 139 |
+
stages (builder and runner).
|
| 140 |
+
- Added `ARG SNAPSHOT_URL` + `ENV SNAPSHOT_URL` in both stages,
|
| 141 |
+
defaulting to a pinned `evaleval/eval-cards-data` warehouse path.
|
| 142 |
+
- Comment block rewritten to reflect v2 + legacy coexistence.
|
| 143 |
+
- Kept legacy `LOCAL_PIPELINE_OUTPUT`, `HF_DATA_LOCAL_DIR`,
|
| 144 |
+
`HF_DATA_OFFLINE=1` envs intact (legacy backend still compilable).
|
| 145 |
+
|
| 146 |
+
**Uncommitted tweak (working tree):** `SNAPSHOT_URL` default points at
|
| 147 |
+
`j-chim/temp_evalcard_backend` instead of `evaleval/eval-cards-data` β
|
| 148 |
+
this is the dev/test dataset for the temp HF Space deploy. Do **not**
|
| 149 |
+
commit this override; revert before merging to main, or keep it only
|
| 150 |
+
on local working copy.
|
| 151 |
+
|
| 152 |
+
**Reconcile:**
|
| 153 |
+
- Keep our `DATA_BACKEND=v2` default and `SNAPSHOT_URL` plumbing.
|
| 154 |
+
- Layer main's non-data changes (base image bumps, `pnpm` version,
|
| 155 |
+
build commands) on top.
|
| 156 |
+
|
| 157 |
+
---
|
| 158 |
+
|
| 159 |
+
## `lib/benchmark-schema.ts` β Low
|
| 160 |
+
|
| 161 |
+
**What we did:** Added one optional field, `num_few_shot?: number`, on
|
| 162 |
+
`GenerationConfig`. That's it.
|
| 163 |
+
|
| 164 |
+
**Reconcile:** Trivially additive. Keep our line; merge tool should
|
| 165 |
+
handle it cleanly unless main touched the same struct.
|
| 166 |
+
|
| 167 |
+
---
|
| 168 |
+
|
| 169 |
+
## `app/page.tsx` β Low
|
| 170 |
+
|
| 171 |
+
**What we did:** One-line copy change in the empty-state banner β
|
| 172 |
+
`corpus-aggregates.json` β `headline.json` (the v2 sidecar name).
|
| 173 |
+
|
| 174 |
+
**Reconcile:** Trivial. Keep ours.
|
| 175 |
+
|
| 176 |
+
---
|
| 177 |
+
|
| 178 |
+
## Order of operations after `git pull`
|
| 179 |
+
|
| 180 |
+
1. Resolve `lib/backend-artifacts.ts` first β it's the schema source
|
| 181 |
+
of truth that the components depend on.
|
| 182 |
+
2. Resolve `lib/data-backend.ts` and `lib/hf-data.ts` β backend wiring.
|
| 183 |
+
3. Resolve the two `components/signals/*` files using the rename recipe.
|
| 184 |
+
4. Resolve `Dockerfile` β keep our v2 envs.
|
| 185 |
+
5. `app/page.tsx` and `lib/benchmark-schema.ts` β should auto-merge or
|
| 186 |
+
be trivial.
|
| 187 |
+
6. Run `pnpm tsc --noEmit` (or whatever the project's typecheck is) to
|
| 188 |
+
catch any v1 field references main introduced that didn't conflict
|
| 189 |
+
textually but break against our renamed types.
|
| 190 |
+
7. Run `pnpm test` β `tests/view-data.test.ts` and
|
| 191 |
+
`tests/duckdb-data.test.ts` should both still pass.
|
| 192 |
+
8. Smoke test with `DATA_BACKEND=v2 SNAPSHOT_URL=file://β¦` and again
|
| 193 |
+
without (legacy path) β both must render.
|
tests/view-data.test.ts
CHANGED
|
@@ -22,6 +22,7 @@ async function writeSyntheticStageJSnapshot(snapshotDir: string) {
|
|
| 22 |
`
|
| 23 |
SELECT
|
| 24 |
TIMESTAMP '2026-05-03 00:00:00' AS snapshot_id,
|
|
|
|
| 25 |
'openai/gpt-5' AS model_id,
|
| 26 |
'openai/gpt-5' AS id,
|
| 27 |
'openai%2Fgpt-5' AS route_id,
|
|
@@ -205,6 +206,7 @@ async function writeSyntheticStageJSnapshot(snapshotDir: string) {
|
|
| 205 |
'mmlu%3Aaccuracy' AS metric_summary_id,
|
| 206 |
'mmlu' AS benchmark_id,
|
| 207 |
'accuracy' AS metric_id,
|
|
|
|
| 208 |
'openai/gpt-5' AS model_id,
|
| 209 |
'openai%2Fgpt-5' AS model_route_id,
|
| 210 |
struct_pack(
|
|
|
|
| 22 |
`
|
| 23 |
SELECT
|
| 24 |
TIMESTAMP '2026-05-03 00:00:00' AS snapshot_id,
|
| 25 |
+
'openai/gpt-5' AS model_key,
|
| 26 |
'openai/gpt-5' AS model_id,
|
| 27 |
'openai/gpt-5' AS id,
|
| 28 |
'openai%2Fgpt-5' AS route_id,
|
|
|
|
| 206 |
'mmlu%3Aaccuracy' AS metric_summary_id,
|
| 207 |
'mmlu' AS benchmark_id,
|
| 208 |
'accuracy' AS metric_id,
|
| 209 |
+
'openai/gpt-5' AS model_key,
|
| 210 |
'openai/gpt-5' AS model_id,
|
| 211 |
'openai%2Fgpt-5' AS model_route_id,
|
| 212 |
struct_pack(
|