general-eval-card / scripts /compare-data-backends.mjs
Jenny Chim
Add DuckDB shadow-read backend with source-metadata fix
2fcae3f
#!/usr/bin/env node
const DEFAULT_BASE_URL = "http://localhost:3000"
const args = new Map()
for (let i = 2; i < process.argv.length; i += 2) {
const key = process.argv[i]
const value = process.argv[i + 1]
if (!key?.startsWith("--") || value == null) {
throw new Error("Usage: node scripts/compare-data-backends.mjs --json-base http://localhost:3000 --duckdb-base http://localhost:3001")
}
args.set(key, value)
}
const jsonBase = args.get("--json-base") ?? process.env.JSON_BASE_URL ?? DEFAULT_BASE_URL
const duckdbBase = args.get("--duckdb-base") ?? process.env.DUCKDB_BASE_URL ?? jsonBase
function endpoint(path) {
return path.startsWith("/") ? path : `/${path}`
}
async function fetchJson(baseUrl, path) {
const url = new URL(endpoint(path), baseUrl)
const response = await fetch(url)
if (!response.ok) {
throw new Error(`${url.toString()} returned ${response.status} ${response.statusText}`)
}
return response.json()
}
function stableArrayKey(item) {
if (!item || typeof item !== "object" || Array.isArray(item)) {
return null
}
const record = item
return (
record.eval_summary_id ??
record.evaluation_id ??
record.model_route_id ??
record.route_id ??
record.developer_route_id ??
record.model_id ??
record.id ??
record.column_key ??
record.metric_summary_id ??
record.metric_name ??
record.name ??
null
)
}
function normalize(value) {
if (Array.isArray(value)) {
const normalized = value.map(normalize)
if (normalized.every((item) => stableArrayKey(item) != null)) {
normalized.sort((a, b) => String(stableArrayKey(a)).localeCompare(String(stableArrayKey(b))))
}
return normalized
}
if (!value || typeof value !== "object") {
return value
}
const entries = Object.entries(value)
.map(([key, nestedValue]) => [key, normalize(nestedValue)])
.sort(([a], [b]) => a.localeCompare(b))
return Object.fromEntries(entries)
}
function diffPaths(left, right, prefix = "") {
const out = []
if (left === right) return out
if (
left == null || right == null ||
typeof left !== typeof right ||
Array.isArray(left) !== Array.isArray(right) ||
typeof left !== "object"
) {
out.push({ path: prefix || "<root>", left, right })
return out
}
if (Array.isArray(left)) {
const max = Math.max(left.length, right.length)
if (left.length !== right.length) {
out.push({ path: `${prefix}.length`, left: left.length, right: right.length })
}
for (let i = 0; i < max && out.length < 20; i++) {
out.push(...diffPaths(left[i], right[i], `${prefix}[${i}]`))
}
return out
}
const keys = new Set([...Object.keys(left), ...Object.keys(right)])
for (const key of keys) {
if (out.length >= 20) break
out.push(...diffPaths(left[key], right[key], prefix ? `${prefix}.${key}` : key))
}
return out
}
const FAIL_FAST = process.env.PARITY_FAIL_FAST !== "0"
const failures = []
function assertEqual(label, left, right) {
const normalizedLeft = normalize(left)
const normalizedRight = normalize(right)
const leftText = JSON.stringify(normalizedLeft)
const rightText = JSON.stringify(normalizedRight)
if (leftText === rightText) {
console.log(`✓ ${label}`)
return
}
console.log(`✗ ${label}`)
const diffs = diffPaths(normalizedLeft, normalizedRight)
for (const diff of diffs.slice(0, 12)) {
const left = JSON.stringify(diff.left)
const right = JSON.stringify(diff.right)
const truncate = (text) => text != null && text.length > 160 ? `${text.slice(0, 160)}…` : text
console.log(` ${diff.path}`)
console.log(` JSON : ${truncate(left)}`)
console.log(` DuckDB : ${truncate(right)}`)
}
if (diffs.length > 12) {
console.log(` …(${diffs.length - 12} more)`)
}
failures.push(label)
if (FAIL_FAST) {
throw new Error(`Mismatch for ${label}`)
}
}
async function compareEndpoint(path) {
const [jsonValue, duckdbValue] = await Promise.all([
fetchJson(jsonBase, path),
fetchJson(duckdbBase, path),
])
assertEqual(path, jsonValue, duckdbValue)
return jsonValue
}
const evalListLite = await compareEndpoint("/api/eval-list-lite")
const modelCardsLite = await compareEndpoint("/api/model-cards-lite")
await compareEndpoint("/api/eval-list")
await compareEndpoint("/api/model-cards")
const evalId = evalListLite?.evals?.[0]?.evaluation_id ?? evalListLite?.evals?.[0]?.eval_summary_id
if (evalId) {
await compareEndpoint(`/api/eval-summary?id=${encodeURIComponent(evalId)}`)
} else {
console.warn("No eval id found in /api/eval-list-lite; skipping eval summary parity")
}
const modelId = modelCardsLite?.[0]?.route_id ?? modelCardsLite?.[0]?.model_route_id ?? modelCardsLite?.[0]?.id
if (modelId) {
await compareEndpoint(`/api/model-summary?id=${encodeURIComponent(modelId)}`)
} else {
console.warn("No model id found in /api/model-cards-lite; skipping model summary parity")
}
try {
const developers = await compareEndpoint("/api/developers")
const developerId = developers?.[0]?.route_id
if (developerId) {
await compareEndpoint(`/api/developer-summary?id=${encodeURIComponent(developerId)}`)
} else {
console.warn("No developer id found in /api/developers; skipping developer summary parity")
}
} catch (error) {
console.warn(`Developer parity skipped: ${error instanceof Error ? error.message : String(error)}`)
}
console.log(`Compared JSON backend ${jsonBase} with DuckDB backend ${duckdbBase}`)
if (failures.length > 0) {
console.error(`\n${failures.length} endpoint(s) failed parity:`)
for (const label of failures) console.error(` - ${label}`)
process.exit(1)
}