From 2f711e629760c2ec1b3a0179bc66b2e2708ccab0 Mon Sep 17 00:00:00 2001 From: WangEn Date: Thu, 24 Sep 2026 23:58:06 +0800 Subject: [PATCH 01/13] archive: add model, test, timeline and comparison projections --- .../persistence/src/archive-repository.ts | 637 ++++++++++++++++++ 1 file changed, 637 insertions(+) diff --git a/packages/persistence/src/archive-repository.ts b/packages/persistence/src/archive-repository.ts index 0a64a64..2878a37 100644 --- a/packages/persistence/src/archive-repository.ts +++ b/packages/persistence/src/archive-repository.ts @@ -111,6 +111,116 @@ export interface ArchiveRunDetailView extends ArchiveRunView { readonly evidence: readonly ArchiveRunEvidenceView[]; } +export interface ArchiveModelSnapshotView { + readonly id: string; + readonly providerSnapshotId: string; + readonly validFrom: string | null; + readonly validTo: string | null; + readonly sourceId: string | null; +} + +export interface ArchiveModelRelationView { + readonly id: string; + readonly direction: "outgoing" | "incoming"; + readonly relationType: string; + readonly relatedModel: { + readonly id: string; + readonly canonicalSlug: string; + readonly marketingName: string; + readonly providerSlug: string; + }; + readonly validFrom: string | null; + readonly validTo: string | null; + readonly sourceId: string | null; + readonly confidence: number; +} + +export interface ArchiveTestCoverageView { + readonly testCaseId: string; + readonly familySlug: string; + readonly familyName: string; + readonly version: string; + readonly caseSlug: string; + readonly runCount: number; + readonly latestRunAt: string | null; +} + +export interface ArchiveModelCoverageView { + readonly modelId: string; + readonly canonicalSlug: string; + readonly marketingName: string; + readonly providerSlug: string; + readonly runCount: number; + readonly latestRunAt: string | null; +} + +export interface ArchiveTimelineEventView { + readonly id: string; + readonly kind: + | "model_released" + | "model_retired" + | "snapshot_started" + | "snapshot_ended" + | "relation" + | "run"; + readonly occurredAt: string; + readonly title: string; + readonly description: string; + readonly runId: string | null; + readonly testCaseId: string | null; + readonly snapshotId: string | null; + readonly relatedModelId: string | null; +} + +export interface ArchiveModelDetailView extends ArchiveModelView { + readonly family: { + readonly id: string; + readonly slug: string; + readonly displayName: string; + } | null; + readonly track: { + readonly id: string; + readonly slug: string; + readonly displayName: string; + readonly trackType: string | null; + } | null; + readonly releasedAt: string | null; + readonly retiredAt: string | null; + readonly canonicalSourceId: string | null; + readonly snapshots: readonly ArchiveModelSnapshotView[]; + readonly relations: readonly ArchiveModelRelationView[]; + readonly testCoverage: readonly ArchiveTestCoverageView[]; + readonly recentRuns: readonly ArchiveRunView[]; + readonly timeline: readonly ArchiveTimelineEventView[]; +} + +export interface ArchiveTestDetailView extends ArchiveTestView { + readonly origin: string; + readonly canonicalSourceId: string | null; + readonly versionStatus: string; + readonly definitionSha256: string; + readonly license: string | null; + readonly publishedAt: string | null; + readonly versionCreatedAt: string; + readonly caseType: string; + readonly caseStatus: string; + readonly activeFrom: string | null; + readonly activeTo: string | null; + readonly promptSha256: string; + readonly fixtureManifestSha256: string | null; + readonly evaluatorDefinitionSha256: string | null; + readonly modelCoverage: readonly ArchiveModelCoverageView[]; + readonly recentRuns: readonly ArchiveRunView[]; +} + +export interface ArchiveComparisonView { + readonly test: ArchiveTestView; + readonly rows: readonly { + readonly model: ArchiveModelView; + readonly latestRun: ArchiveRunView | null; + }[]; +} + interface ArchiveRunRow { id: string; status: string; @@ -417,6 +527,533 @@ export class PgArchiveRepository { return result.rows.map(runView); } + async getModel(modelId: string): Promise { + const modelResult = await this.pool.query<{ + id: string; + provider_id: string; + provider_slug: string; + provider_name: string; + canonical_slug: string; + marketing_name: string; + status: string; + released_at: Date | null; + retired_at: Date | null; + canonical_source_id: string | null; + family_id: string | null; + family_slug: string | null; + family_name: string | null; + track_id: string | null; + track_slug: string | null; + track_name: string | null; + track_type: string | null; + run_count: string; + latest_run_at: Date | null; + }>( + `SELECT + m.id, + p.id AS provider_id, + p.slug AS provider_slug, + p.name AS provider_name, + m.canonical_slug, + m.marketing_name, + m.status, + m.released_at, + m.retired_at, + m.canonical_source_id, + mf.id AS family_id, + mf.slug AS family_slug, + mf.display_name AS family_name, + mt.id AS track_id, + mt.slug AS track_slug, + mt.display_name AS track_name, + mt.track_type, + ( + SELECT COUNT(*)::text + FROM modelapse.runs r + JOIN modelapse.test_cases rtc ON rtc.id = r.test_case_id + WHERE r.model_id = m.id + AND r.sealed_at IS NOT NULL + AND rtc.visibility = 'public' + ) AS run_count, + ( + SELECT MAX(r.completed_at) + FROM modelapse.runs r + JOIN modelapse.test_cases rtc ON rtc.id = r.test_case_id + WHERE r.model_id = m.id + AND r.sealed_at IS NOT NULL + AND rtc.visibility = 'public' + ) AS latest_run_at + FROM modelapse.models m + JOIN modelapse.providers p ON p.id = m.provider_id + LEFT JOIN modelapse.model_families mf ON mf.id = m.family_id + LEFT JOIN modelapse.model_tracks mt ON mt.id = m.track_id + WHERE m.id = $1 + LIMIT 1`, + [modelId], + ); + + const row = modelResult.rows[0]; + if (!row) return null; + + const [snapshotResult, relationResult, coverageResult, allRuns] = + await Promise.all([ + this.pool.query<{ + id: string; + provider_snapshot_id: string; + valid_from: Date | null; + valid_to: Date | null; + source_id: string | null; + }>( + `SELECT id, provider_snapshot_id, valid_from, valid_to, source_id + FROM modelapse.model_snapshots + WHERE model_id = $1 + ORDER BY valid_from DESC NULLS LAST, provider_snapshot_id`, + [modelId], + ), + this.pool.query<{ + id: string; + direction: "outgoing" | "incoming"; + relation_type: string; + related_model_id: string; + related_canonical_slug: string; + related_marketing_name: string; + related_provider_slug: string; + valid_from: Date | null; + valid_to: Date | null; + source_id: string | null; + confidence: string; + }>( + `SELECT + mr.id, + CASE + WHEN mr.from_model_id = $1 THEN 'outgoing' + ELSE 'incoming' + END AS direction, + mr.relation_type, + related.id AS related_model_id, + related.canonical_slug AS related_canonical_slug, + related.marketing_name AS related_marketing_name, + rp.slug AS related_provider_slug, + mr.valid_from, + mr.valid_to, + mr.source_id, + mr.confidence::text AS confidence + FROM modelapse.model_relations mr + JOIN modelapse.models related + ON related.id = CASE + WHEN mr.from_model_id = $1 THEN mr.to_model_id + ELSE mr.from_model_id + END + JOIN modelapse.providers rp ON rp.id = related.provider_id + WHERE mr.from_model_id = $1 OR mr.to_model_id = $1 + ORDER BY mr.valid_from DESC NULLS LAST, mr.relation_type`, + [modelId], + ), + this.pool.query<{ + test_case_id: string; + family_slug: string; + family_name: string; + version: string; + case_slug: string; + run_count: string; + latest_run_at: Date | null; + }>( + `SELECT + tc.id AS test_case_id, + tf.slug AS family_slug, + tf.name AS family_name, + tv.version, + tc.slug AS case_slug, + COUNT(r.id)::text AS run_count, + MAX(r.completed_at) AS latest_run_at + FROM modelapse.runs r + JOIN modelapse.test_cases tc ON tc.id = r.test_case_id + JOIN modelapse.test_versions tv ON tv.id = tc.test_version_id + JOIN modelapse.test_variants tvar ON tvar.id = tv.variant_id + JOIN modelapse.test_families tf ON tf.id = tvar.family_id + WHERE r.model_id = $1 + AND r.sealed_at IS NOT NULL + AND tc.visibility = 'public' + GROUP BY tc.id, tv.id, tf.id + ORDER BY MAX(r.completed_at) DESC NULLS LAST, tf.slug, tc.slug`, + [modelId], + ), + this.listRuns({ modelId, limit: 100 }), + ]); + + const snapshots: ArchiveModelSnapshotView[] = snapshotResult.rows.map( + (snapshot) => ({ + id: snapshot.id, + providerSnapshotId: snapshot.provider_snapshot_id, + validFrom: snapshot.valid_from?.toISOString() ?? null, + validTo: snapshot.valid_to?.toISOString() ?? null, + sourceId: snapshot.source_id, + }), + ); + + const relations: ArchiveModelRelationView[] = relationResult.rows.map( + (relation) => ({ + id: relation.id, + direction: relation.direction, + relationType: relation.relation_type, + relatedModel: { + id: relation.related_model_id, + canonicalSlug: relation.related_canonical_slug, + marketingName: relation.related_marketing_name, + providerSlug: relation.related_provider_slug, + }, + validFrom: relation.valid_from?.toISOString() ?? null, + validTo: relation.valid_to?.toISOString() ?? null, + sourceId: relation.source_id, + confidence: Number(relation.confidence), + }), + ); + + const timeline: ArchiveTimelineEventView[] = []; + + if (row.released_at) { + timeline.push({ + id: `model:${row.id}:released`, + kind: "model_released", + occurredAt: row.released_at.toISOString(), + title: "Model released", + description: row.marketing_name, + runId: null, + testCaseId: null, + snapshotId: null, + relatedModelId: null, + }); + } + if (row.retired_at) { + timeline.push({ + id: `model:${row.id}:retired`, + kind: "model_retired", + occurredAt: row.retired_at.toISOString(), + title: "Model retired", + description: row.marketing_name, + runId: null, + testCaseId: null, + snapshotId: null, + relatedModelId: null, + }); + } + + for (const snapshot of snapshots) { + if (snapshot.validFrom) { + timeline.push({ + id: `snapshot:${snapshot.id}:start`, + kind: "snapshot_started", + occurredAt: snapshot.validFrom, + title: "Snapshot observed", + description: snapshot.providerSnapshotId, + runId: null, + testCaseId: null, + snapshotId: snapshot.id, + relatedModelId: null, + }); + } + if (snapshot.validTo) { + timeline.push({ + id: `snapshot:${snapshot.id}:end`, + kind: "snapshot_ended", + occurredAt: snapshot.validTo, + title: "Snapshot validity ended", + description: snapshot.providerSnapshotId, + runId: null, + testCaseId: null, + snapshotId: snapshot.id, + relatedModelId: null, + }); + } + } + + for (const relation of relations) { + if (!relation.validFrom) continue; + timeline.push({ + id: `relation:${relation.id}`, + kind: "relation", + occurredAt: relation.validFrom, + title: relation.relationType.replaceAll("_", " "), + description: `${relation.direction} · ${relation.relatedModel.marketingName}`, + runId: null, + testCaseId: null, + snapshotId: null, + relatedModelId: relation.relatedModel.id, + }); + } + + for (const run of allRuns) { + timeline.push({ + id: `run:${run.id}`, + kind: "run", + occurredAt: run.completedAt ?? run.sealedAt ?? run.createdAt, + title: `Run · ${run.test.caseSlug}`, + description: `${run.evidenceLevel ?? "—"} · ${ + run.evaluation?.exactMatch === true + ? "exact match" + : run.evaluation?.exactMatch === false + ? "mismatch" + : run.evaluation?.status ?? "not evaluated" + }`, + runId: run.id, + testCaseId: run.test.testCaseId, + snapshotId: null, + relatedModelId: null, + }); + } + + timeline.sort( + (left, right) => + Date.parse(right.occurredAt) - Date.parse(left.occurredAt), + ); + + return { + id: row.id, + provider: { + id: row.provider_id, + slug: row.provider_slug, + name: row.provider_name, + }, + canonicalSlug: row.canonical_slug, + marketingName: row.marketing_name, + status: row.status, + runCount: Number(row.run_count), + latestRunAt: row.latest_run_at?.toISOString() ?? null, + family: + row.family_id && row.family_slug && row.family_name + ? { + id: row.family_id, + slug: row.family_slug, + displayName: row.family_name, + } + : null, + track: + row.track_id && row.track_slug && row.track_name + ? { + id: row.track_id, + slug: row.track_slug, + displayName: row.track_name, + trackType: row.track_type, + } + : null, + releasedAt: row.released_at?.toISOString() ?? null, + retiredAt: row.retired_at?.toISOString() ?? null, + canonicalSourceId: row.canonical_source_id, + snapshots, + relations, + testCoverage: coverageResult.rows.map((coverage) => ({ + testCaseId: coverage.test_case_id, + familySlug: coverage.family_slug, + familyName: coverage.family_name, + version: coverage.version, + caseSlug: coverage.case_slug, + runCount: Number(coverage.run_count), + latestRunAt: coverage.latest_run_at?.toISOString() ?? null, + })), + recentRuns: allRuns.slice(0, 20), + timeline, + }; + } + + async getTest(testCaseId: string): Promise { + const testResult = await this.pool.query<{ + test_case_id: string; + family_slug: string; + family_name: string; + origin: string; + canonical_source_id: string | null; + variant_slug: string; + variant_name: string; + category: string; + artifact_type: string; + version: string; + version_status: string; + definition_sha256: string; + license: string | null; + published_at: Date | null; + version_created_at: Date; + case_slug: string; + case_type: string; + case_status: string; + active_from: Date | null; + active_to: Date | null; + prompt_blob_sha256: string; + fixture_manifest_blob_sha256: string | null; + evaluator_slug: string | null; + evaluator_version: string | null; + evaluator_kind: string | null; + evaluator_definition_sha256: string | null; + run_count: string; + }>( + `SELECT + tc.id AS test_case_id, + tf.slug AS family_slug, + tf.name AS family_name, + tf.origin, + tf.canonical_source_id, + tvar.slug AS variant_slug, + tvar.name AS variant_name, + tvar.category, + tvar.artifact_type, + tv.version, + tv.status AS version_status, + tv.definition_sha256, + tv.license, + tv.published_at, + tv.created_at AS version_created_at, + tc.slug AS case_slug, + tc.case_type, + tc.status AS case_status, + tc.active_from, + tc.active_to, + tc.prompt_blob_sha256, + tc.fixture_manifest_blob_sha256, + evaluator.slug AS evaluator_slug, + evaluator.version AS evaluator_version, + evaluator.kind AS evaluator_kind, + evaluator.definition_sha256 AS evaluator_definition_sha256, + ( + SELECT COUNT(*)::text + FROM modelapse.runs r + WHERE r.test_case_id = tc.id + AND r.sealed_at IS NOT NULL + ) AS run_count + FROM modelapse.test_cases tc + JOIN modelapse.test_versions tv ON tv.id = tc.test_version_id + JOIN modelapse.test_variants tvar ON tvar.id = tv.variant_id + JOIN modelapse.test_families tf ON tf.id = tvar.family_id + LEFT JOIN LATERAL ( + SELECT e.slug, e.version, e.kind, e.definition_sha256 + FROM modelapse.test_version_evaluators tve + JOIN modelapse.evaluators e ON e.id = tve.evaluator_id + WHERE tve.test_version_id = tv.id + ORDER BY e.slug, e.version + LIMIT 1 + ) evaluator ON true + WHERE tc.id = $1 + AND tc.visibility = 'public' + LIMIT 1`, + [testCaseId], + ); + + const row = testResult.rows[0]; + if (!row) return null; + + const [coverageResult, recentRuns] = await Promise.all([ + this.pool.query<{ + model_id: string; + canonical_slug: string; + marketing_name: string; + provider_slug: string; + run_count: string; + latest_run_at: Date | null; + }>( + `SELECT + m.id AS model_id, + m.canonical_slug, + m.marketing_name, + p.slug AS provider_slug, + COUNT(r.id)::text AS run_count, + MAX(r.completed_at) AS latest_run_at + FROM modelapse.runs r + JOIN modelapse.models m ON m.id = r.model_id + JOIN modelapse.providers p ON p.id = m.provider_id + WHERE r.test_case_id = $1 + AND r.sealed_at IS NOT NULL + GROUP BY m.id, p.id + ORDER BY MAX(r.completed_at) DESC NULLS LAST, p.slug, m.marketing_name`, + [testCaseId], + ), + this.listRuns({ testCaseId, limit: 50 }), + ]); + + return { + testCaseId: row.test_case_id, + familySlug: row.family_slug, + familyName: row.family_name, + variantSlug: row.variant_slug, + variantName: row.variant_name, + category: row.category, + artifactType: row.artifact_type, + version: row.version, + caseSlug: row.case_slug, + evaluator: + row.evaluator_slug && row.evaluator_version && row.evaluator_kind + ? { + slug: row.evaluator_slug, + version: row.evaluator_version, + kind: row.evaluator_kind, + } + : null, + runCount: Number(row.run_count), + origin: row.origin, + canonicalSourceId: row.canonical_source_id, + versionStatus: row.version_status, + definitionSha256: row.definition_sha256, + license: row.license, + publishedAt: row.published_at?.toISOString() ?? null, + versionCreatedAt: row.version_created_at.toISOString(), + caseType: row.case_type, + caseStatus: row.case_status, + activeFrom: row.active_from?.toISOString() ?? null, + activeTo: row.active_to?.toISOString() ?? null, + promptSha256: row.prompt_blob_sha256, + fixtureManifestSha256: row.fixture_manifest_blob_sha256, + evaluatorDefinitionSha256: row.evaluator_definition_sha256, + modelCoverage: coverageResult.rows.map((coverage) => ({ + modelId: coverage.model_id, + canonicalSlug: coverage.canonical_slug, + marketingName: coverage.marketing_name, + providerSlug: coverage.provider_slug, + runCount: Number(coverage.run_count), + latestRunAt: coverage.latest_run_at?.toISOString() ?? null, + })), + recentRuns, + }; + } + + async compareLatest(input: { + readonly modelIds: readonly string[]; + readonly testCaseId: string; + }): Promise { + const modelIds = [...new Set(input.modelIds)]; + if (modelIds.length < 2 || modelIds.length > 4) { + throw new Error("Archive comparison requires between 2 and 4 models"); + } + + const [models, tests] = await Promise.all([ + this.listModels(), + this.listTests(), + ]); + const selectedModels = modelIds + .map((modelId) => models.find((model) => model.id === modelId)) + .filter((model): model is ArchiveModelView => Boolean(model)); + const test = tests.find((candidate) => candidate.testCaseId === input.testCaseId); + + if (!test || selectedModels.length !== modelIds.length) { + return null; + } + + const latestRuns = await Promise.all( + modelIds.map(async (modelId) => { + const runs = await this.listRuns({ + modelId, + testCaseId: input.testCaseId, + limit: 1, + }); + return runs[0] ?? null; + }), + ); + + return { + test, + rows: selectedModels.map((model, index) => ({ + model, + latestRun: latestRuns[index] ?? null, + })), + }; + } + async getRun(runId: string): Promise { const summaryResult = await this.pool.query( RUN_SELECT + From 1c91463f77f6017ada8a6cb16fd76a58c7fcd522 Mon Sep 17 00:00:00 2001 From: WangEn Date: Thu, 24 Sep 2026 23:58:24 +0800 Subject: [PATCH 02/13] api: expose Archive model, test and comparison reads --- apps/api/src/app.ts | 81 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index a8bb7dd..7340d85 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -25,7 +25,14 @@ type ControlPlanner = Pick< >; type ArchiveRepository = Pick< PgArchiveRepository, - "ping" | "listModels" | "listTests" | "listRuns" | "getRun" + | "ping" + | "listModels" + | "listTests" + | "listRuns" + | "getRun" + | "getModel" + | "getTest" + | "compareLatest" >; export interface AppDependencies { @@ -156,6 +163,78 @@ export function createApp(deps: AppDependencies) { return c.json({ tests: await deps.archive.listTests() }); }); + app.get("/v1/archive/models/:modelId", async (c) => { + if (!deps.archive) { + return c.json({ error: "archive_unavailable" }, 503); + } + + const modelId = c.req.param("modelId"); + if (!UUID_RE.test(modelId)) { + return c.json({ error: "invalid_model_id" }, 400); + } + + const model = await deps.archive.getModel(modelId); + if (!model) { + return c.json({ error: "archive_model_not_found" }, 404); + } + + return c.json({ model }); + }); + + app.get("/v1/archive/tests/:testCaseId", async (c) => { + if (!deps.archive) { + return c.json({ error: "archive_unavailable" }, 503); + } + + const testCaseId = c.req.param("testCaseId"); + if (!UUID_RE.test(testCaseId)) { + return c.json({ error: "invalid_test_case_id" }, 400); + } + + const test = await deps.archive.getTest(testCaseId); + if (!test) { + return c.json({ error: "archive_test_not_found" }, 404); + } + + return c.json({ test }); + }); + + app.get("/v1/archive/compare", async (c) => { + if (!deps.archive) { + return c.json({ error: "archive_unavailable" }, 503); + } + + const rawModelIds = c.req.query("modelIds"); + const testCaseId = c.req.query("testCaseId"); + const modelIds = rawModelIds + ?.split(",") + .map((value) => value.trim()) + .filter(Boolean); + + if ( + !modelIds || + modelIds.length < 2 || + modelIds.length > 4 || + new Set(modelIds).size !== modelIds.length || + modelIds.some((modelId) => !UUID_RE.test(modelId)) + ) { + return c.json({ error: "invalid_model_ids" }, 400); + } + if (!testCaseId || !UUID_RE.test(testCaseId)) { + return c.json({ error: "invalid_test_case_id" }, 400); + } + + const comparison = await deps.archive.compareLatest({ + modelIds, + testCaseId, + }); + if (!comparison) { + return c.json({ error: "archive_comparison_not_found" }, 404); + } + + return c.json({ comparison }); + }); + app.get("/v1/archive/runs", async (c) => { if (!deps.archive) { return c.json({ error: "archive_unavailable" }, 503); From 7a0897a878cbaf889a1efc827beedc46295997b0 Mon Sep 17 00:00:00 2001 From: WangEn Date: Thu, 24 Sep 2026 23:58:54 +0800 Subject: [PATCH 03/13] api: cover Archive v0.3 detail and comparison endpoints --- apps/api/test/archive-api.test.ts | 203 ++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) diff --git a/apps/api/test/archive-api.test.ts b/apps/api/test/archive-api.test.ts index cb56754..1881384 100644 --- a/apps/api/test/archive-api.test.ts +++ b/apps/api/test/archive-api.test.ts @@ -105,6 +105,160 @@ const archiveRun = { ], } as const; +const archiveModelDetail = { + id: MODEL_ID, + provider: archiveRun.provider, + canonicalSlug: "deepseek-flash", + marketingName: "DeepSeek Flash", + status: "active", + runCount: 1, + latestRunAt: archiveRun.completedAt, + family: { + id: "00000000-0000-4000-8000-000000000031", + slug: "deepseek", + displayName: "DeepSeek", + }, + track: { + id: "00000000-0000-4000-8000-000000000032", + slug: "flash", + displayName: "Flash", + trackType: "fast", + }, + releasedAt: "2026-09-01T00:00:00.000Z", + retiredAt: null, + canonicalSourceId: "00000000-0000-4000-8000-000000000033", + snapshots: [ + { + id: "00000000-0000-4000-8000-000000000034", + providerSnapshotId: "deepseek-flash-202609", + validFrom: "2026-09-01T00:00:00.000Z", + validTo: null, + sourceId: "00000000-0000-4000-8000-000000000033", + }, + ], + relations: [], + testCoverage: [ + { + testCaseId: TEST_CASE_ID, + familySlug: archiveRun.test.familySlug, + familyName: archiveRun.test.familyName, + version: archiveRun.test.version, + caseSlug: archiveRun.test.caseSlug, + runCount: 1, + latestRunAt: archiveRun.completedAt, + }, + ], + recentRuns: [archiveRun], + timeline: [ + { + id: `run:${RUN_ID}`, + kind: "run", + occurredAt: archiveRun.completedAt, + title: "Run · exact-modelapse", + description: "E4 · exact match", + runId: RUN_ID, + testCaseId: TEST_CASE_ID, + snapshotId: null, + relatedModelId: null, + }, + ], +} as const; + +const archiveTestDetail = { + testCaseId: TEST_CASE_ID, + familySlug: archiveRun.test.familySlug, + familyName: archiveRun.test.familyName, + variantSlug: archiveRun.test.variantSlug, + variantName: archiveRun.test.variantName, + category: "smoke", + artifactType: "text", + version: archiveRun.test.version, + caseSlug: archiveRun.test.caseSlug, + evaluator: { + slug: "exact-text", + version: "1.0.0", + kind: "deterministic", + }, + runCount: 1, + origin: "modelapse", + canonicalSourceId: null, + versionStatus: "published", + definitionSha256: + "1111111111111111111111111111111111111111111111111111111111111111", + license: "Apache-2.0", + publishedAt: "2026-09-01T00:00:00.000Z", + versionCreatedAt: "2026-08-31T00:00:00.000Z", + caseType: "public", + caseStatus: "active", + activeFrom: "2026-09-01T00:00:00.000Z", + activeTo: null, + promptSha256: + "2222222222222222222222222222222222222222222222222222222222222222", + fixtureManifestSha256: null, + evaluatorDefinitionSha256: archiveRun.evaluation.definitionSha256, + modelCoverage: [ + { + modelId: MODEL_ID, + canonicalSlug: "deepseek-flash", + marketingName: "DeepSeek Flash", + providerSlug: "deepseek", + runCount: 1, + latestRunAt: archiveRun.completedAt, + }, + ], + recentRuns: [archiveRun], +} as const; + +const archiveComparison = { + test: { + testCaseId: TEST_CASE_ID, + familySlug: archiveRun.test.familySlug, + familyName: archiveRun.test.familyName, + variantSlug: archiveRun.test.variantSlug, + variantName: archiveRun.test.variantName, + category: "smoke", + artifactType: "text", + version: archiveRun.test.version, + caseSlug: archiveRun.test.caseSlug, + evaluator: { + slug: "exact-text", + version: "1.0.0", + kind: "deterministic", + }, + runCount: 1, + }, + rows: [ + { + model: { + id: MODEL_ID, + provider: archiveRun.provider, + canonicalSlug: "deepseek-flash", + marketingName: "DeepSeek Flash", + status: "active", + runCount: 1, + latestRunAt: archiveRun.completedAt, + }, + latestRun: archiveRun, + }, + { + model: { + id: "00000000-0000-4000-8000-000000000035", + provider: { + id: "00000000-0000-4000-8000-000000000036", + slug: "openai", + name: "OpenAI", + }, + canonicalSlug: "gpt-test", + marketingName: "GPT Test", + status: "active", + runCount: 0, + latestRunAt: null, + }, + latestRun: null, + }, + ], +} as const; + describe("Archive read API", () => { it("exposes public model, test and evaluated Run views without control auth", async () => { const app = createApp({ @@ -147,6 +301,11 @@ describe("Archive read API", () => { ], listRuns: async () => [archiveRun], getRun: async (runId) => (runId === RUN_ID ? archiveRun : null), + getModel: async (modelId) => + modelId === MODEL_ID ? archiveModelDetail : null, + getTest: async (testCaseId) => + testCaseId === TEST_CASE_ID ? archiveTestDetail : null, + compareLatest: async () => archiveComparison, }, }); @@ -167,6 +326,33 @@ describe("Archive read API", () => { ], }); + const model = await app.request("/v1/archive/models/" + MODEL_ID); + expect(model.status).toBe(200); + await expect(model.json()).resolves.toMatchObject({ + model: { + id: MODEL_ID, + timeline: [{ kind: "run", runId: RUN_ID }], + }, + }); + + const test = await app.request("/v1/archive/tests/" + TEST_CASE_ID); + expect(test.status).toBe(200); + await expect(test.json()).resolves.toMatchObject({ + test: { + testCaseId: TEST_CASE_ID, + definitionSha256: archiveTestDetail.definitionSha256, + }, + }); + + const secondModelId = archiveComparison.rows[1].model.id; + const comparison = await app.request( + `/v1/archive/compare?modelIds=${MODEL_ID},${secondModelId}&testCaseId=${TEST_CASE_ID}`, + ); + expect(comparison.status).toBe(200); + await expect(comparison.json()).resolves.toEqual({ + comparison: archiveComparison, + }); + const runs = await app.request( `/v1/archive/runs?modelId=${MODEL_ID}&testCaseId=${TEST_CASE_ID}&limit=10`, ); @@ -187,6 +373,9 @@ describe("Archive read API", () => { listTests: async () => [], listRuns: async () => [], getRun: async () => null, + getModel: async () => null, + getTest: async () => null, + compareLatest: async () => null, }, }); @@ -196,5 +385,19 @@ describe("Archive read API", () => { expect( (await app.request("/v1/archive/runs?limit=101")).status, ).toBe(400); + expect( + (await app.request("/v1/archive/models/nope")).status, + ).toBe(400); + expect( + (await app.request("/v1/archive/tests/nope")).status, + ).toBe(400); + expect( + ( + await app.request( + `/v1/archive/compare?modelIds=${MODEL_ID}&testCaseId=${TEST_CASE_ID}`, + ) + ).status, + ).toBe(400); + }); }); From 16170a06bcbb58670165cb1912d91aba928942a2 Mon Sep 17 00:00:00 2001 From: WangEn Date: Thu, 24 Sep 2026 23:59:27 +0800 Subject: [PATCH 04/13] archive: integration-test model, test, timeline and compare projections --- packages/persistence/test/integration.test.ts | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/packages/persistence/test/integration.test.ts b/packages/persistence/test/integration.test.ts index 6d494b7..22e5bf8 100644 --- a/packages/persistence/test/integration.test.ts +++ b/packages/persistence/test/integration.test.ts @@ -26,6 +26,9 @@ describe("PostgreSQL Run persistence", () => { let root = ""; let providerId = ""; let testCaseId = ""; + let modelId = ""; + let relatedModelId = ""; + let snapshotId = ""; beforeAll(async () => { root = await mkdtemp(join(tmpdir(), "modelapse-integration-")); @@ -48,6 +51,84 @@ describe("PostgreSQL Run persistence", () => { ); providerId = provider.rows[0]!.id; + const modelFamily = await seedPool.query<{ id: string }>( + `INSERT INTO modelapse.model_families + (provider_id, slug, display_name) + VALUES ($1, $2, 'Integration Models') + RETURNING id`, + [providerId, `integration-models-${suffix}`], + ); + + const modelTrack = await seedPool.query<{ id: string }>( + `INSERT INTO modelapse.model_tracks + (family_id, slug, display_name, track_type) + VALUES ($1, 'main', 'Main', 'integration') + RETURNING id`, + [modelFamily.rows[0]!.id], + ); + + const model = await seedPool.query<{ id: string }>( + `INSERT INTO modelapse.models + ( + provider_id, + family_id, + track_id, + canonical_slug, + marketing_name, + released_at, + status + ) + VALUES ($1, $2, $3, $4, 'Integration Model', $5, 'active') + RETURNING id`, + [ + providerId, + modelFamily.rows[0]!.id, + modelTrack.rows[0]!.id, + `integration-model-${suffix}`, + "2026-09-01T00:00:00.000Z", + ], + ); + modelId = model.rows[0]!.id; + + const relatedModel = await seedPool.query<{ id: string }>( + `INSERT INTO modelapse.models + ( + provider_id, + family_id, + track_id, + canonical_slug, + marketing_name, + released_at, + status + ) + VALUES ($1, $2, $3, $4, 'Integration Model Next', $5, 'preview') + RETURNING id`, + [ + providerId, + modelFamily.rows[0]!.id, + modelTrack.rows[0]!.id, + `integration-model-next-${suffix}`, + "2026-09-20T00:00:00.000Z", + ], + ); + relatedModelId = relatedModel.rows[0]!.id; + + const snapshot = await seedPool.query<{ id: string }>( + `INSERT INTO modelapse.model_snapshots + (model_id, provider_snapshot_id, valid_from) + VALUES ($1, $2, $3) + RETURNING id`, + [modelId, `integration-snapshot-${suffix}`, "2026-09-01T00:00:00.000Z"], + ); + snapshotId = snapshot.rows[0]!.id; + + await seedPool.query( + `INSERT INTO modelapse.model_relations + (from_model_id, to_model_id, relation_type, valid_from, confidence) + VALUES ($1, $2, 'successor_of', $3, 1.0)`, + [relatedModelId, modelId, "2026-09-20T00:00:00.000Z"], + ); + const family = await seedPool.query<{ id: string }>( `INSERT INTO modelapse.test_families (slug, name, origin) VALUES ($1, $2, 'modelapse') @@ -168,6 +249,8 @@ describe("PostgreSQL Run persistence", () => { }, run: { testCaseId, + modelId, + snapshotId, providerId, runnerBuild: "integration-test", }, @@ -219,6 +302,52 @@ describe("PostgreSQL Run persistence", () => { }, }); + const archivedModel = await archive.getModel(modelId); + expect(archivedModel).toMatchObject({ + id: modelId, + family: { displayName: "Integration Models" }, + track: { displayName: "Main" }, + runCount: 1, + snapshots: [{ id: snapshotId }], + testCoverage: [{ testCaseId, runCount: 1 }], + }); + expect( + archivedModel?.relations.some( + (relation) => + relation.relatedModel.id === relatedModelId && + relation.relationType === "successor_of", + ), + ).toBe(true); + expect( + archivedModel?.timeline.some( + (event) => event.kind === "run" && event.runId === result.run.id, + ), + ).toBe(true); + + const archivedTest = await archive.getTest(testCaseId); + expect(archivedTest).toMatchObject({ + testCaseId, + origin: "modelapse", + runCount: 1, + modelCoverage: [{ modelId, runCount: 1 }], + }); + expect(archivedTest?.recentRuns[0]?.id).toBe(result.run.id); + + const comparison = await archive.compareLatest({ + modelIds: [modelId, relatedModelId], + testCaseId, + }); + expect(comparison).not.toBeNull(); + expect(comparison?.rows).toHaveLength(2); + expect(comparison?.rows[0]).toMatchObject({ + model: { id: modelId }, + latestRun: { id: result.run.id }, + }); + expect(comparison?.rows[1]).toMatchObject({ + model: { id: relatedModelId }, + latestRun: null, + }); + await expect( repository.markStatus(result.run.id, "executing"), ).rejects.toThrow(/sealed run/); From ca8412664efa731d2063c6766497bd0a6423d0d9 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 00:00:03 +0800 Subject: [PATCH 05/13] web: add Archive v0.3 model, test and comparison loaders --- apps/web/src/modelapse.ts | 233 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) diff --git a/apps/web/src/modelapse.ts b/apps/web/src/modelapse.ts index d1793a8..438cb68 100644 --- a/apps/web/src/modelapse.ts +++ b/apps/web/src/modelapse.ts @@ -71,6 +71,117 @@ export interface ArchiveTest { readonly runCount: number; } +export interface ArchiveModelSnapshot { + readonly id: string; + readonly providerSnapshotId: string; + readonly validFrom: string | null; + readonly validTo: string | null; + readonly sourceId: string | null; +} + +export interface ArchiveModelRelation { + readonly id: string; + readonly direction: "outgoing" | "incoming"; + readonly relationType: string; + readonly relatedModel: { + readonly id: string; + readonly canonicalSlug: string; + readonly marketingName: string; + readonly providerSlug: string; + }; + readonly validFrom: string | null; + readonly validTo: string | null; + readonly sourceId: string | null; + readonly confidence: number; +} + +export interface ArchiveTimelineEvent { + readonly id: string; + readonly kind: + | "model_released" + | "model_retired" + | "snapshot_started" + | "snapshot_ended" + | "relation" + | "run"; + readonly occurredAt: string; + readonly title: string; + readonly description: string; + readonly runId: string | null; + readonly testCaseId: string | null; + readonly snapshotId: string | null; + readonly relatedModelId: string | null; +} + +export interface ArchiveModelDetail extends ArchiveModel { + readonly family: { + readonly id: string; + readonly slug: string; + readonly displayName: string; + } | null; + readonly track: { + readonly id: string; + readonly slug: string; + readonly displayName: string; + readonly trackType: string | null; + } | null; + readonly releasedAt: string | null; + readonly retiredAt: string | null; + readonly canonicalSourceId: string | null; + readonly snapshots: readonly ArchiveModelSnapshot[]; + readonly relations: readonly ArchiveModelRelation[]; + readonly testCoverage: readonly { + readonly testCaseId: string; + readonly familySlug: string; + readonly familyName: string; + readonly version: string; + readonly caseSlug: string; + readonly runCount: number; + readonly latestRunAt: string | null; + }[]; + readonly recentRuns: readonly ArchiveRun[]; + readonly timeline: readonly ArchiveTimelineEvent[]; +} + +export interface ArchiveTestDetail extends ArchiveTest { + readonly origin: string; + readonly canonicalSourceId: string | null; + readonly versionStatus: string; + readonly definitionSha256: string; + readonly license: string | null; + readonly publishedAt: string | null; + readonly versionCreatedAt: string; + readonly caseType: string; + readonly caseStatus: string; + readonly activeFrom: string | null; + readonly activeTo: string | null; + readonly promptSha256: string; + readonly fixtureManifestSha256: string | null; + readonly evaluatorDefinitionSha256: string | null; + readonly modelCoverage: readonly { + readonly modelId: string; + readonly canonicalSlug: string; + readonly marketingName: string; + readonly providerSlug: string; + readonly runCount: number; + readonly latestRunAt: string | null; + }[]; + readonly recentRuns: readonly ArchiveRun[]; +} + +export interface ArchiveComparison { + readonly test: ArchiveTest; + readonly rows: readonly { + readonly model: ArchiveModel; + readonly latestRun: ArchiveRun | null; + }[]; +} + +export interface ArchiveCatalog { + readonly models: readonly ArchiveModel[]; + readonly tests: readonly ArchiveTest[]; +} + export interface ArchiveRun { readonly id: string; readonly status: string; @@ -202,6 +313,19 @@ interface ReadArchiveRunInput { readonly runId: string; } +interface ReadArchiveModelInput { + readonly modelId: string; +} + +interface ReadArchiveTestInput { + readonly testCaseId: string; +} + +interface CompareArchiveInput { + readonly modelIds: readonly string[]; + readonly testCaseId: string; +} + interface ApiOptions { readonly method?: "GET" | "POST"; readonly control?: boolean; @@ -390,6 +514,53 @@ function parseArchiveRunInput(value: unknown): ReadArchiveRunInput { return { runId }; } +function parseArchiveModelInput(value: unknown): ReadArchiveModelInput { + if (!isRecord(value)) throw new Error("Archive Model request must be an object"); + + const modelId = value.modelId; + if (typeof modelId !== "string" || !UUID_RE.test(modelId)) { + throw new Error("modelId must be a UUID"); + } + + return { modelId }; +} + +function parseArchiveTestInput(value: unknown): ReadArchiveTestInput { + if (!isRecord(value)) throw new Error("Archive Test request must be an object"); + + const testCaseId = value.testCaseId; + if (typeof testCaseId !== "string" || !UUID_RE.test(testCaseId)) { + throw new Error("testCaseId must be a UUID"); + } + + return { testCaseId }; +} + +function parseArchiveComparisonInput(value: unknown): CompareArchiveInput { + if (!isRecord(value)) throw new Error("Archive comparison request must be an object"); + + const modelIds = value.modelIds; + const testCaseId = value.testCaseId; + + if ( + !Array.isArray(modelIds) || + modelIds.length < 2 || + modelIds.length > 4 || + modelIds.some((modelId) => typeof modelId !== "string" || !UUID_RE.test(modelId)) || + new Set(modelIds).size !== modelIds.length + ) { + throw new Error("modelIds must contain between 2 and 4 unique UUIDs"); + } + if (typeof testCaseId !== "string" || !UUID_RE.test(testCaseId)) { + throw new Error("testCaseId must be a UUID"); + } + + return { + modelIds: modelIds as string[], + testCaseId, + }; +} + function requireOperator(candidate: string): void { const expected = process.env.MODELAPSE_WEB_OPERATOR_TOKEN; if (!expected) { @@ -453,6 +624,68 @@ export const getArchiveRun = createServerFn({ method: "POST" }) } }); +export const getArchiveCatalog = createServerFn({ method: "GET" }).handler( + async (): Promise => { + const [models, tests] = await Promise.all([ + requestJson<{ models: readonly ArchiveModel[] }>("/v1/archive/models"), + requestJson<{ tests: readonly ArchiveTest[] }>("/v1/archive/tests"), + ]); + return { + models: models.models, + tests: tests.tests, + }; + }, +); + +export const getArchiveModel = createServerFn({ method: "POST" }) + .validator(parseArchiveModelInput) + .handler(async ({ data }): Promise => { + try { + const result = await requestJson<{ model: ArchiveModelDetail }>( + `/v1/archive/models/${data.modelId}`, + ); + return result.model; + } catch (error) { + if (error instanceof ApiRequestError && error.status === 404) { + return null; + } + throw error; + } + }); + +export const getArchiveTest = createServerFn({ method: "POST" }) + .validator(parseArchiveTestInput) + .handler(async ({ data }): Promise => { + try { + const result = await requestJson<{ test: ArchiveTestDetail }>( + `/v1/archive/tests/${data.testCaseId}`, + ); + return result.test; + } catch (error) { + if (error instanceof ApiRequestError && error.status === 404) { + return null; + } + throw error; + } + }); + +export const compareArchive = createServerFn({ method: "POST" }) + .validator(parseArchiveComparisonInput) + .handler(async ({ data }): Promise => { + const modelIds = data.modelIds.join(","); + try { + const result = await requestJson<{ comparison: ArchiveComparison }>( + `/v1/archive/compare?modelIds=${encodeURIComponent(modelIds)}&testCaseId=${encodeURIComponent(data.testCaseId)}`, + ); + return result.comparison; + } catch (error) { + if (error instanceof ApiRequestError && error.status === 404) { + return null; + } + throw error; + } + }); + export const getControlCatalog = createServerFn({ method: "POST" }) .validator(parseOperatorInput) .handler(async ({ data }): Promise => { From b78c90aa1aed8e8bd6d97b5b5c75c5e59aec93ff Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 00:00:52 +0800 Subject: [PATCH 06/13] web: add Archive model detail and evolution timeline --- apps/web/src/routes/models.$modelId.tsx | 285 ++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 apps/web/src/routes/models.$modelId.tsx diff --git a/apps/web/src/routes/models.$modelId.tsx b/apps/web/src/routes/models.$modelId.tsx new file mode 100644 index 0000000..31b8e51 --- /dev/null +++ b/apps/web/src/routes/models.$modelId.tsx @@ -0,0 +1,285 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { + getArchiveModel, + type ArchiveRun, + type ArchiveTimelineEvent, +} from "../modelapse"; + +export const Route = createFileRoute("/models/$modelId")({ + loader: ({ params }) => + getArchiveModel({ + data: { + modelId: params.modelId, + }, + }), + component: ArchiveModelPage, +}); + +function formatTimestamp(value: string | null): string { + if (!value) return "—"; + return new Date(value).toISOString().replace("T", " ").replace(".000Z", "Z"); +} + +function evaluationLabel(run: ArchiveRun): string { + if (!run.evaluation) return "not evaluated"; + if (run.evaluation.exactMatch === true) return "exact match"; + if (run.evaluation.exactMatch === false) return "mismatch"; + return run.evaluation.status; +} + +function evaluationClass(run: ArchiveRun): string { + if (run.evaluation?.exactMatch === true) return "badge badge-pass"; + if (run.evaluation?.exactMatch === false) return "badge badge-fail"; + return "badge"; +} + +function timelineHref(event: ArchiveTimelineEvent): string | null { + if (event.runId) return `/runs/${event.runId}`; + if (event.relatedModelId) return `/models/${event.relatedModelId}`; + if (event.testCaseId) return `/tests/${event.testCaseId}`; + return null; +} + +function ArchiveModelPage() { + const model = Route.useLoaderData(); + + if (!model) { + return ( +
+
+ + M + + Modelapse + AI Model Test & Evolution Archive + + +
+
+

MODEL ARCHIVE

+

Model not found

+

The requested canonical model is not present in the Archive catalog.

+ ← Back to Archive +
+
+ ); + } + + return ( +
+
+ + M + + Modelapse + AI Model Test & Evolution Archive + + + +
+ +
+
+ Archive + / + Models + / + {model.canonicalSlug} +
+ +
+
+

MODEL ARCHIVE

+

{model.marketingName}

+

+ {model.provider.name} · {model.canonicalSlug} +

+
+
+ {model.status} + {model.track ? {model.track.displayName} : null} + {model.runCount} sealed run(s) +
+
+ +
+
+
Released
+
{formatTimestamp(model.releasedAt)}
+
+
+
Latest public Run
+
{formatTimestamp(model.latestRunAt)}
+
+
+
Family
+
{model.family?.displayName ?? "—"}
+
+
+
Track
+
{model.track?.trackType ?? model.track?.displayName ?? "—"}
+
+
+
+ +
+
+
+

EVOLUTION TIMELINE

+

Catalog changes + verified Runs

+
+

+ Timeline events are descriptive Archive facts. Run evaluations remain + derived views and are not converted into a model ranking. +

+
+ +
+ {model.timeline.map((event) => { + const href = timelineHref(event); + return ( +
+
+ ); + })} + {model.timeline.length === 0 ? ( +
No dated evolution events are archived yet.
+ ) : null} +
+
+ +
+
+
+

TEST COVERAGE

+

Public Test history

+
+ Open comparison → +
+ +
+ {model.testCoverage.map((coverage) => ( + + {coverage.familyName} + {coverage.caseSlug} + v{coverage.version} +
+
+
Runs
+
{coverage.runCount}
+
+
+
Latest
+
{formatTimestamp(coverage.latestRunAt)}
+
+
+
+ ))} + {model.testCoverage.length === 0 ? ( +
No sealed public Test Runs for this model yet.
+ ) : null} +
+
+ +
+
+
+

LINEAGE

+

Snapshots and relations

+
+
+ +
+
+

Provider snapshots

+ {model.snapshots.map((snapshot) => ( +
+ {snapshot.providerSnapshotId} + + {formatTimestamp(snapshot.validFrom)} → {formatTimestamp(snapshot.validTo)} + + source {snapshot.sourceId ?? "—"} +
+ ))} + {model.snapshots.length === 0 ?

No snapshots archived.

: null} +
+ +
+

Model relations

+ {model.relations.map((relation) => ( +
+ {relation.relationType.replaceAll("_", " ")} + + {relation.relatedModel.marketingName} + + + {relation.direction} · confidence {relation.confidence} + +
+ ))} + {model.relations.length === 0 ?

No model relations archived.

: null} +
+
+
+ +
+
+
+

RECENT RUNS

+

Latest sealed evidence

+
+
+ + +
+ + +
+ ); +} From 03d1e541e45252b5f9b7e2ec40b58fd30cca7727 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 00:01:18 +0800 Subject: [PATCH 07/13] web: add Archive Test detail and model coverage --- apps/web/src/routes/tests.$testCaseId.tsx | 257 ++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 apps/web/src/routes/tests.$testCaseId.tsx diff --git a/apps/web/src/routes/tests.$testCaseId.tsx b/apps/web/src/routes/tests.$testCaseId.tsx new file mode 100644 index 0000000..a6d6b96 --- /dev/null +++ b/apps/web/src/routes/tests.$testCaseId.tsx @@ -0,0 +1,257 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { getArchiveTest, type ArchiveRun } from "../modelapse"; + +export const Route = createFileRoute("/tests/$testCaseId")({ + loader: ({ params }) => + getArchiveTest({ + data: { + testCaseId: params.testCaseId, + }, + }), + component: ArchiveTestPage, +}); + +function formatTimestamp(value: string | null): string { + if (!value) return "—"; + return new Date(value).toISOString().replace("T", " ").replace(".000Z", "Z"); +} + +function shortHash(value: string | null): string { + if (!value) return "—"; + if (value.length <= 30) return value; + return `${value.slice(0, 16)}…${value.slice(-10)}`; +} + +function evaluationLabel(run: ArchiveRun): string { + if (!run.evaluation) return "not evaluated"; + if (run.evaluation.exactMatch === true) return "exact match"; + if (run.evaluation.exactMatch === false) return "mismatch"; + return run.evaluation.status; +} + +function evaluationClass(run: ArchiveRun): string { + if (run.evaluation?.exactMatch === true) return "badge badge-pass"; + if (run.evaluation?.exactMatch === false) return "badge badge-fail"; + return "badge"; +} + +function ArchiveTestPage() { + const test = Route.useLoaderData(); + + if (!test) { + return ( +
+
+ + M + + Modelapse + AI Model Test & Evolution Archive + + +
+
+

TEST ARCHIVE

+

Test not found

+

This Test Case is not part of the public Archive.

+ ← Back to Archive +
+
+ ); + } + + return ( +
+
+ + M + + Modelapse + AI Model Test & Evolution Archive + + + +
+ +
+
+ Archive + / + Tests + / + {test.caseSlug} +
+ +
+
+

TEST ARCHIVE

+

{test.familyName}

+

+ {test.caseSlug} · {test.variantName} · v{test.version} +

+
+
+ {test.versionStatus} + {test.category} + {test.artifactType} +
+
+ +
+
+
Origin
+
{test.origin}
+
+
+
Published
+
{formatTimestamp(test.publishedAt)}
+
+
+
Public sealed Runs
+
{test.runCount}
+
+
+
Evaluator
+
+ {test.evaluator + ? `${test.evaluator.slug}@${test.evaluator.version}` + : "—"} +
+
+
+
+ +
+
+
+

DEFINITION

+

Immutable Test identity

+
+

+ Definition and prompt hashes identify the archived Test without + exposing private blob object locations. +

+
+ +
+
+
Definition SHA-256
+
{shortHash(test.definitionSha256)}
+
+
+
Prompt SHA-256
+
{shortHash(test.promptSha256)}
+
+
+
Fixture manifest
+
+ {shortHash(test.fixtureManifestSha256)} +
+
+
+
Evaluator definition
+
+ {shortHash(test.evaluatorDefinitionSha256)} +
+
+
+
License
+
{test.license ?? "—"}
+
+
+
Case type / status
+
{test.caseType} · {test.caseStatus}
+
+
+
Active window
+
{formatTimestamp(test.activeFrom)} → {formatTimestamp(test.activeTo)}
+
+
+
Canonical source record
+
{test.canonicalSourceId ?? "—"}
+
+
+
+ +
+
+
+

MODEL COVERAGE

+

Who has a verified Run

+
+ Compare models → +
+ +
+ {test.modelCoverage.map((coverage) => ( + + {coverage.providerSlug} + {coverage.marketingName} + {coverage.canonicalSlug} +
+
+
Runs
+
{coverage.runCount}
+
+
+
Latest
+
{formatTimestamp(coverage.latestRunAt)}
+
+
+
+ ))} + {test.modelCoverage.length === 0 ? ( +
No model has a sealed public Run for this Test yet.
+ ) : null} +
+
+ +
+
+
+

RUN HISTORY

+

Recent sealed evidence

+
+
+ + +
+ + +
+ ); +} From 31b27dd93e43b3e46176f97fc95e0f801d3322e2 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 00:01:51 +0800 Subject: [PATCH 08/13] web: add latest sealed Run comparison foundation --- apps/web/src/routes/compare.tsx | 289 ++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 apps/web/src/routes/compare.tsx diff --git a/apps/web/src/routes/compare.tsx b/apps/web/src/routes/compare.tsx new file mode 100644 index 0000000..73f3baa --- /dev/null +++ b/apps/web/src/routes/compare.tsx @@ -0,0 +1,289 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { useMemo, useState } from "react"; +import { + compareArchive, + getArchiveCatalog, + type ArchiveComparison, + type ArchiveRun, +} from "../modelapse"; + +export const Route = createFileRoute("/compare")({ + loader: () => getArchiveCatalog(), + component: ArchiveComparePage, +}); + +function formatTimestamp(value: string | null): string { + if (!value) return "—"; + return new Date(value).toISOString().replace("T", " ").replace(".000Z", "Z"); +} + +function evaluationLabel(run: ArchiveRun | null): string { + if (!run) return "no Run"; + if (!run.evaluation) return "not evaluated"; + if (run.evaluation.exactMatch === true) return "exact match"; + if (run.evaluation.exactMatch === false) return "mismatch"; + return run.evaluation.status; +} + +function evaluationClass(run: ArchiveRun | null): string { + if (run?.evaluation?.exactMatch === true) return "badge badge-pass"; + if (run?.evaluation?.exactMatch === false) return "badge badge-fail"; + return "badge"; +} + +function ArchiveComparePage() { + const catalog = Route.useLoaderData(); + const defaultModelIds = catalog.models.slice(0, 2).map((model) => model.id); + const [modelIds, setModelIds] = useState(defaultModelIds); + const [testCaseId, setTestCaseId] = useState(catalog.tests[0]?.testCaseId ?? ""); + const [comparison, setComparison] = useState(null); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + const selectedModels = useMemo( + () => catalog.models.filter((model) => modelIds.includes(model.id)), + [catalog.models, modelIds], + ); + + function toggleModel(modelId: string): void { + setComparison(null); + setError(null); + setModelIds((current) => { + if (current.includes(modelId)) { + return current.filter((id) => id !== modelId); + } + if (current.length >= 4) return current; + return [...current, modelId]; + }); + } + + async function handleCompare(): Promise { + if (modelIds.length < 2 || !testCaseId) return; + + setBusy(true); + setError(null); + try { + const result = await compareArchive({ + data: { + modelIds, + testCaseId, + }, + }); + setComparison(result); + if (!result) { + setError("No public Archive comparison exists for this selection."); + } + } catch (caught) { + setComparison(null); + setError(caught instanceof Error ? caught.message : "Comparison failed"); + } finally { + setBusy(false); + } + } + + return ( +
+
+ + M + + Modelapse + AI Model Test & Evolution Archive + + + +
+ +
+
+ Archive + / + Compare +
+ +
+
+

COMPARISON FOUNDATION

+

Same Test. Latest sealed Run.

+

+ Compare 2–4 canonical models against one public Test Case without + turning the Archive into a ranking table. +

+
+
+
+ +
+
+ + +
+ 02 / Models +
+ {catalog.models.map((model) => { + const selected = modelIds.includes(model.id); + return ( + + ); + })} +
+ {modelIds.length} / 4 selected · minimum 2 +
+
+ +
+
+ + {selectedModels.map((model) => model.marketingName).join(" ↔ ") || "Select models"} + + No winner, score aggregation, or ordering is computed. +
+ +
+ + {error ?
{error}
: null} +
+ +
+
+
+

SIDE BY SIDE

+

+ {comparison + ? `${comparison.test.familyName} · ${comparison.test.caseSlug}` + : "Select models and run comparison"} +

+
+ {comparison ? ( + + Inspect Test → + + ) : null} +
+ + {comparison ? ( +
+ {comparison.rows.map((row) => { + const run = row.latestRun; + return ( +
+
+ {row.model.provider.slug} + + {row.model.marketingName} + + {row.model.canonicalSlug} +
+ + {run ? ( + <> +
+ + {evaluationLabel(run)} + + evidence {run.evidenceLevel ?? "—"} +
+
+ +
+
Completed
+
{formatTimestamp(run.completedAt)}
+
+
+
Requested
+
{run.requestedModel}
+
+
+
Returned
+
{run.returnedModel ?? "—"}
+
+
+
Execution
+
{run.executionPath}
+
+
+
Evaluator
+
+ {run.evaluation + ? `${run.evaluation.evaluatorSlug}@${run.evaluation.evaluatorVersion}` + : "—"} +
+
+
+ + ) : ( +
+ No sealed public Run exists for this model × Test Case pair. +
+ )} +
+ ); + })} +
+ ) : ( +
+

+ Comparison intentionally uses raw Archive records. It does not infer + an overall model winner from heterogeneous Tests. +

+
+ )} +
+ + +
+ ); +} From 17329a91759fac8abfc937021d30dbf75362ffde Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 00:02:08 +0800 Subject: [PATCH 09/13] web: link Archive rows into Model, Test and Compare views --- apps/web/src/routes/index.tsx | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 3994257..c1fe30d 100644 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -392,7 +392,11 @@ function ModelapseHome() {

PUBLIC ARCHIVE

Recent sealed runs

- @@ -439,13 +444,22 @@ function ModelapseHome() { {run.provider.slug} - - {run.model.marketingName ?? run.requestedModel} - + {run.model.id ? ( + + {run.model.marketingName ?? run.requestedModel} + + ) : ( + {run.model.marketingName ?? run.requestedModel} + )} {run.returnedModel ?? run.requestedModel} - {run.test.caseSlug} + + {run.test.caseSlug} + {run.test.familySlug} · v{run.test.version} From 9531d1264af2e6e9acb68d1f4326b946530f556d Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 00:02:27 +0800 Subject: [PATCH 10/13] web: connect Run detail to Model, Test and Compare Archive views --- apps/web/src/routes/runs.$runId.tsx | 37 +++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/apps/web/src/routes/runs.$runId.tsx b/apps/web/src/routes/runs.$runId.tsx index 2f5df2f..7bb3815 100644 --- a/apps/web/src/routes/runs.$runId.tsx +++ b/apps/web/src/routes/runs.$runId.tsx @@ -109,9 +109,10 @@ function ArchiveRunPage() { AI Model Test & Evolution Archive - - Archive - +
@@ -124,9 +125,19 @@ function ArchiveRunPage() {

SEALED RUN

-

{run.model.marketingName ?? run.requestedModel}

+

+ {run.model.id ? ( + + {run.model.marketingName ?? run.requestedModel} + + ) : ( + run.model.marketingName ?? run.requestedModel + )} +

- {run.test.familyName} · {run.test.caseSlug} · v{run.test.version} + + {run.test.familyName} · {run.test.caseSlug} · v{run.test.version} +

@@ -216,7 +227,15 @@ function ArchiveRunPage() {
Canonical model
-
{run.model.canonicalSlug ?? "unbound"}
+
+ {run.model.id ? ( + + {run.model.canonicalSlug ?? "unbound"} + + ) : ( + run.model.canonicalSlug ?? "unbound" + )} +
Execution path
@@ -249,7 +268,11 @@ function ArchiveRunPage() {
Test Case ID
-
{run.test.testCaseId}
+
+ + {run.test.testCaseId} + +
Variant
From 99067565a2e7397678f760340e60136fa1e7a092 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 00:03:15 +0800 Subject: [PATCH 11/13] web: style Model, Test, timeline and comparison Archive views --- apps/web/src/styles.css | 574 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 573 insertions(+), 1 deletion(-) diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index c943ec4..e6b7b09 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -752,6 +752,516 @@ button:disabled { line-height: 1.6; } + +.header-nav { + display: flex; + align-items: center; + gap: 22px; +} + +.header-link-current { + color: var(--soft); + cursor: default; +} + +.archive-tools { + display: flex; + min-width: min(720px, 100%); + align-items: center; + justify-content: flex-end; + gap: 18px; +} + +.archive-compare-link { + white-space: nowrap; +} + +.archive-entity-link, +.entity-title-link, +.coverage-card, +.lineage-record a, +.comparison-card-head a { + color: var(--ink); + text-decoration: none; +} + +.archive-entity-link { + font-size: 13px; + font-weight: 700; + overflow-wrap: anywhere; +} + +.archive-entity-link:hover, +.entity-title-link:hover, +.lineage-record a:hover, +.comparison-card-head a:hover { + color: var(--accent); +} + +.entity-title-link { + color: inherit; +} + +.entity-hero { + padding: 46px clamp(20px, 4vw, 64px) 54px; + border-bottom: 1px solid var(--line); + background: + radial-gradient(circle at 84% 18%, rgba(200, 255, 99, 0.075), transparent 25rem), + rgba(255, 255, 255, 0.008); +} + +.entity-hero h1 { + margin: 0; + max-width: 980px; + font-size: clamp(44px, 6vw, 86px); + line-height: 0.96; + letter-spacing: -0.05em; + font-weight: 650; +} + +.entity-stats { + margin: 48px 0 0; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + border-top: 1px solid var(--line); + border-left: 1px solid var(--line); +} + +.entity-stats > div { + min-width: 0; + min-height: 82px; + padding: 16px; + border-right: 1px solid var(--line); + border-bottom: 1px solid var(--line); +} + +.entity-stats dt, +.coverage-card dt, +.definition-grid dt, +.comparison-facts dt { + color: var(--muted); + font: 700 10px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.entity-stats dd, +.coverage-card dd, +.definition-grid dd, +.comparison-facts dd { + margin: 7px 0 0; + overflow-wrap: anywhere; + font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.entity-section { + background: rgba(255, 255, 255, 0.003); +} + +.timeline { + position: relative; + border-top: 1px solid var(--line); +} + +.timeline-event { + display: grid; + grid-template-columns: 20px minmax(160px, 0.55fr) minmax(0, 1.45fr); + gap: 16px; + min-height: 108px; + border-bottom: 1px solid var(--line); +} + +.timeline-marker { + position: relative; +} + +.timeline-marker::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 9px; + width: 1px; + background: var(--line); +} + +.timeline-marker::after { + content: ""; + position: absolute; + top: 32px; + left: 5px; + width: 9px; + height: 9px; + border-radius: 999px; + border: 2px solid var(--accent); + background: var(--bg); +} + +.timeline-time { + padding: 27px 8px 20px 0; + color: var(--muted); + font: 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.timeline-body { + padding: 20px 0; + display: flex; + flex-wrap: wrap; + gap: 10px 14px; + align-items: center; +} + +.timeline-body strong { + font-size: 15px; +} + +.timeline-body small { + flex-basis: 100%; + color: var(--muted); + font: 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.coverage-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + border-top: 1px solid var(--line); + border-left: 1px solid var(--line); +} + +.coverage-card { + min-width: 0; + min-height: 190px; + padding: 20px; + border-right: 1px solid var(--line); + border-bottom: 1px solid var(--line); + display: flex; + flex-direction: column; + gap: 10px; + background: rgba(255, 255, 255, 0.008); +} + +.coverage-card:hover { + background: rgba(200, 255, 99, 0.035); +} + +.coverage-card > span, +.coverage-card > small { + color: var(--muted); + font: 10px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.coverage-card > strong { + font-size: 17px; + overflow-wrap: anywhere; +} + +.coverage-card dl { + margin: auto 0 0; + display: grid; + grid-template-columns: 0.55fr 1.45fr; + gap: 12px; + padding-top: 16px; + border-top: 1px solid var(--line); +} + +.lineage-grid { + display: grid; + grid-template-columns: 1fr 1fr; + border-top: 1px solid var(--line); + border-left: 1px solid var(--line); +} + +.lineage-panel { + min-width: 0; + padding: 22px; + border-right: 1px solid var(--line); + border-bottom: 1px solid var(--line); +} + +.lineage-panel h3 { + margin: 0 0 20px; + font-size: 17px; +} + +.lineage-panel > p { + color: var(--muted); + font-size: 13px; +} + +.lineage-record { + padding: 14px 0; + border-top: 1px solid var(--line); + display: flex; + flex-wrap: wrap; + gap: 8px 12px; + align-items: center; +} + +.lineage-record strong, +.lineage-record a { + font-size: 13px; + font-weight: 700; + overflow-wrap: anywhere; +} + +.lineage-record small { + flex-basis: 100%; + color: var(--muted); + font: 10px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.compact-run-list { + border-top: 1px solid var(--line); + border-left: 1px solid var(--line); +} + +.compact-run-row { + color: var(--ink); + text-decoration: none; + display: grid; + grid-template-columns: minmax(125px, 0.8fr) minmax(190px, 1.45fr) minmax(130px, 0.9fr) minmax(150px, 1fr); +} + +.compact-run-row:hover { + background: rgba(200, 255, 99, 0.025); +} + +.compact-run-row > span { + min-width: 0; + min-height: 78px; + padding: 14px; + border-right: 1px solid var(--line); + border-bottom: 1px solid var(--line); + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 7px; +} + +.compact-run-row strong { + font-size: 12px; + overflow-wrap: anywhere; +} + +.compact-run-row small { + color: var(--muted); + font: 10px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; + overflow-wrap: anywhere; +} + +.definition-grid { + margin: 0; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + border-top: 1px solid var(--line); + border-left: 1px solid var(--line); +} + +.definition-grid > div { + min-width: 0; + min-height: 92px; + padding: 17px; + border-right: 1px solid var(--line); + border-bottom: 1px solid var(--line); +} + +.compare-hero h1 { + max-width: 1120px; +} + +.compare-controls-section { + background: rgba(255, 255, 255, 0.008); +} + +.compare-controls { + display: grid; + grid-template-columns: minmax(280px, 0.7fr) minmax(0, 1.3fr); + border-top: 1px solid var(--line); + border-left: 1px solid var(--line); +} + +.compare-controls > label, +.model-picker { + min-width: 0; + padding: 22px; + border-right: 1px solid var(--line); + border-bottom: 1px solid var(--line); + display: flex; + flex-direction: column; + gap: 12px; +} + +.compare-controls > label > span, +.model-picker > span { + color: var(--muted); + font: 700 10px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.compare-controls small, +.model-picker > small { + color: var(--muted); + font: 10px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.model-picker-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.model-choice { + min-width: 0; + min-height: 74px; + padding: 12px; + border: 1px solid var(--line); + display: grid; + grid-template-columns: auto minmax(0, 1fr); + grid-template-rows: auto auto; + column-gap: 10px; + align-items: center; + background: #0d0f0f; + cursor: pointer; +} + +.model-choice-selected { + border-color: rgba(200, 255, 99, 0.55); + background: rgba(200, 255, 99, 0.04); +} + +.model-choice input { + width: auto; + min-height: 0; + margin: 0; + grid-row: 1 / 3; +} + +.model-choice strong { + min-width: 0; + overflow-wrap: anywhere; + font-size: 12px; +} + +.model-choice small { + overflow-wrap: anywhere; +} + +.compare-action { + margin-top: 20px; + padding: 18px 20px; + border: 1px solid var(--line); + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; +} + +.compare-action > div { + min-width: 0; + display: flex; + flex-direction: column; + gap: 6px; +} + +.compare-action strong { + overflow-wrap: anywhere; + font-size: 13px; +} + +.compare-action small { + color: var(--muted); + font: 10px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.comparison-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + border-top: 1px solid var(--line); + border-left: 1px solid var(--line); +} + +.comparison-card { + min-width: 0; + border-right: 1px solid var(--line); + border-bottom: 1px solid var(--line); + background: rgba(255, 255, 255, 0.006); +} + +.comparison-card-head { + min-height: 120px; + padding: 20px; + display: flex; + flex-direction: column; + justify-content: center; + gap: 8px; + border-bottom: 1px solid var(--line); +} + +.comparison-card-head > span, +.comparison-card-head > small { + color: var(--muted); + font: 10px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.comparison-card-head a { + font-size: 22px; + font-weight: 700; + letter-spacing: -0.025em; +} + +.comparison-verdict { + padding: 16px 20px; + display: flex; + flex-wrap: wrap; + gap: 8px; + border-bottom: 1px solid var(--line); +} + +.comparison-facts { + margin: 0; + display: grid; + grid-template-columns: 1fr 1fr; +} + +.comparison-facts > div { + min-width: 0; + min-height: 84px; + padding: 14px 20px; + border-right: 1px solid var(--line); + border-bottom: 1px solid var(--line); +} + +.comparison-empty { + min-height: 250px; + padding: 30px 20px; + display: grid; + place-items: center; + color: var(--muted); + text-align: center; + font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.comparison-placeholder { + min-height: 250px; + padding: 50px 24px; + border: 1px solid var(--line); + display: grid; + place-items: center; + color: var(--muted); + text-align: center; +} + +.comparison-placeholder p { + max-width: 680px; + line-height: 1.7; +} + footer { height: 110px; padding: 0 clamp(20px, 4vw, 64px); @@ -780,6 +1290,12 @@ footer { flex-direction: column; } + .archive-tools { + min-width: 100%; + align-items: stretch; + flex-direction: column; + } + .archive-filters { min-width: 100%; } @@ -807,10 +1323,22 @@ footer { grid-template-columns: 1fr 1fr; } - .detail-grid { + .detail-grid, + .lineage-grid, + .compare-controls { grid-template-columns: 1fr; } + .entity-stats, + .coverage-grid, + .comparison-grid { + grid-template-columns: 1fr 1fr; + } + + .compact-run-row { + grid-template-columns: 1fr 1fr; + } + .section-note { text-align: left; } @@ -838,6 +1366,50 @@ footer { flex-direction: column; } + .header-nav { + gap: 12px; + } + + .entity-stats, + .coverage-grid, + .definition-grid, + .comparison-grid, + .model-picker-grid, + .comparison-facts { + grid-template-columns: 1fr; + } + + .timeline-event { + grid-template-columns: 20px 1fr; + } + + .timeline-time { + grid-column: 2; + padding: 16px 0 0; + } + + .timeline-body { + grid-column: 2; + padding-top: 6px; + } + + .timeline-marker { + grid-row: 1 / 3; + } + + .compact-run-row { + grid-template-columns: 1fr; + } + + .compare-action { + align-items: stretch; + flex-direction: column; + } + + .compare-action button { + width: 100%; + } + .archive-row { grid-template-columns: 1fr; } From b54c872423ea8b4fce48a2a2cd20f9ea44596515 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 00:03:33 +0800 Subject: [PATCH 12/13] web: register Archive v0.3 routes --- apps/web/src/routeTree.gen.ts | 76 +++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index c679283..c4f8310 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -4,15 +4,36 @@ // This file is regenerated by the TanStack Start Vite plugin. import { Route as rootRouteImport } from "./routes/__root"; +import { Route as TestsTestCaseIdRouteImport } from "./routes/tests.$testCaseId"; import { Route as RunsRunIdRouteImport } from "./routes/runs.$runId"; +import { Route as ModelsModelIdRouteImport } from "./routes/models.$modelId"; +import { Route as CompareRouteImport } from "./routes/compare"; import { Route as IndexRouteImport } from "./routes/index"; +const TestsTestCaseIdRoute = TestsTestCaseIdRouteImport.update({ + id: "/tests/$testCaseId", + path: "/tests/$testCaseId", + getParentRoute: () => rootRouteImport, +} as any); + const RunsRunIdRoute = RunsRunIdRouteImport.update({ id: "/runs/$runId", path: "/runs/$runId", getParentRoute: () => rootRouteImport, } as any); +const ModelsModelIdRoute = ModelsModelIdRouteImport.update({ + id: "/models/$modelId", + path: "/models/$modelId", + getParentRoute: () => rootRouteImport, +} as any); + +const CompareRoute = CompareRouteImport.update({ + id: "/compare", + path: "/compare", + getParentRoute: () => rootRouteImport, +} as any); + const IndexRoute = IndexRouteImport.update({ id: "/", path: "/", @@ -21,26 +42,51 @@ const IndexRoute = IndexRouteImport.update({ export interface FileRoutesByFullPath { "/": typeof IndexRoute; + "/compare": typeof CompareRoute; + "/models/$modelId": typeof ModelsModelIdRoute; "/runs/$runId": typeof RunsRunIdRoute; + "/tests/$testCaseId": typeof TestsTestCaseIdRoute; } export interface FileRoutesByTo { "/": typeof IndexRoute; + "/compare": typeof CompareRoute; + "/models/$modelId": typeof ModelsModelIdRoute; "/runs/$runId": typeof RunsRunIdRoute; + "/tests/$testCaseId": typeof TestsTestCaseIdRoute; } export interface FileRoutesById { __root__: typeof rootRouteImport; "/": typeof IndexRoute; + "/compare": typeof CompareRoute; + "/models/$modelId": typeof ModelsModelIdRoute; "/runs/$runId": typeof RunsRunIdRoute; + "/tests/$testCaseId": typeof TestsTestCaseIdRoute; } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath; - fullPaths: "/" | "/runs/$runId"; + fullPaths: + | "/" + | "/compare" + | "/models/$modelId" + | "/runs/$runId" + | "/tests/$testCaseId"; fileRoutesByTo: FileRoutesByTo; - to: "/" | "/runs/$runId"; - id: "__root__" | "/" | "/runs/$runId"; + to: + | "/" + | "/compare" + | "/models/$modelId" + | "/runs/$runId" + | "/tests/$testCaseId"; + id: + | "__root__" + | "/" + | "/compare" + | "/models/$modelId" + | "/runs/$runId" + | "/tests/$testCaseId"; fileRoutesById: FileRoutesById; } @@ -53,6 +99,20 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof IndexRouteImport; parentRoute: typeof rootRouteImport; }; + "/compare": { + id: "/compare"; + path: "/compare"; + fullPath: "/compare"; + preLoaderRoute: typeof CompareRouteImport; + parentRoute: typeof rootRouteImport; + }; + "/models/$modelId": { + id: "/models/$modelId"; + path: "/models/$modelId"; + fullPath: "/models/$modelId"; + preLoaderRoute: typeof ModelsModelIdRouteImport; + parentRoute: typeof rootRouteImport; + }; "/runs/$runId": { id: "/runs/$runId"; path: "/runs/$runId"; @@ -60,12 +120,22 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof RunsRunIdRouteImport; parentRoute: typeof rootRouteImport; }; + "/tests/$testCaseId": { + id: "/tests/$testCaseId"; + path: "/tests/$testCaseId"; + fullPath: "/tests/$testCaseId"; + preLoaderRoute: typeof TestsTestCaseIdRouteImport; + parentRoute: typeof rootRouteImport; + }; } } const rootRouteChildren = { IndexRoute, + CompareRoute, + ModelsModelIdRoute, RunsRunIdRoute, + TestsTestCaseIdRoute, }; export const routeTree = rootRouteImport From d209939207f9cc209b854a01fc28ba037806e1e5 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 00:04:08 +0800 Subject: [PATCH 13/13] docs: define Archive v0.3 entity and comparison layer --- docs/web-operator-ui.md | 56 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/docs/web-operator-ui.md b/docs/web-operator-ui.md index cbc00d7..dde3858 100644 --- a/docs/web-operator-ui.md +++ b/docs/web-operator-ui.md @@ -145,6 +145,62 @@ Evaluation = derived view The Archive list and successful operator Run result both link directly to the immutable Run detail URL. +## Archive v0.3: Model / Test detail, timeline and comparison + +The public Archive now has first-class entity routes: + +```text +/models/ +/tests/ +/compare +``` + +### Model detail + +The Model page combines canonical catalog identity with public Archive evidence: + +- provider, family, track, release/retirement metadata; +- provider snapshots and explicit model relations; +- Test coverage based on sealed public Runs; +- recent sealed Runs; +- an evolution timeline composed from dated model lifecycle events, snapshot validity, sourced model relations and sealed public Runs. + +Timeline entries remain descriptive facts. Evaluation results can appear on Run events, but the timeline does not convert them into a ranking or inferred model quality trend. + +### Test detail + +The Test page exposes the immutable public Test identity: + +- family / variant / case / version metadata; +- definition, prompt, fixture and evaluator hashes; +- origin, license, publication and active-window metadata; +- model coverage based only on sealed public Runs; +- recent Run history. + +Blob object locations and raw private payloads remain excluded. + +### Comparison foundation + +`/compare` lets a reader select one public Test Case and 2–4 canonical models. The comparison uses the latest sealed public Run for each exact model × Test Case pair and presents the records side by side: + +- exact Run identity; +- completion time; +- requested / returned model identifiers; +- execution path and evidence level; +- evaluator identity and deterministic result when available. + +The comparison endpoint does not calculate an overall score, winner, ranking, or cross-Test aggregate. It is intentionally a record comparison layer that later evaluation views can build on. + +Public API additions: + +```text +GET /v1/archive/models/:modelId +GET /v1/archive/tests/:testCaseId +GET /v1/archive/compare?modelIds=<2-4 comma-separated UUIDs>&testCaseId= +``` + +All three remain outside operator authentication and read only Archive-safe projections. + ## Deliberately deferred - general user login/session management;