From b4da0a2c109983da8e70f9c0f693fe2f01820d95 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 01:10:13 +0800 Subject: [PATCH 01/15] archive: add temporal Run history and public Run relations --- .../persistence/src/archive-repository.ts | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/packages/persistence/src/archive-repository.ts b/packages/persistence/src/archive-repository.ts index 2878a37..832a447 100644 --- a/packages/persistence/src/archive-repository.ts +++ b/packages/persistence/src/archive-repository.ts @@ -101,6 +101,20 @@ export interface ArchiveRunEvidenceView { } | null; } +export interface ArchiveRunRelationEdgeView { + readonly fromRunId: string; + readonly toRunId: string; + readonly relationType: string; + readonly createdAt: string; +} + +export interface ArchiveRunRelationView { + readonly direction: "outgoing" | "incoming"; + readonly relationType: string; + readonly relatedRunId: string; + readonly createdAt: string; +} + export interface ArchiveRunDetailView extends ArchiveRunView { readonly config: Readonly> | null; readonly requestBlob: ArchiveBlobView | null; @@ -109,6 +123,7 @@ export interface ArchiveRunDetailView extends ArchiveRunView { readonly usage: Readonly> | null; readonly timing: Readonly> | null; readonly evidence: readonly ArchiveRunEvidenceView[]; + readonly relations: readonly ArchiveRunRelationView[]; } export interface ArchiveModelSnapshotView { @@ -221,6 +236,14 @@ export interface ArchiveComparisonView { }[]; } +export interface ArchiveRunHistoryView { + readonly model: ArchiveModelView; + readonly test: ArchiveTestView; + readonly runs: readonly ArchiveRunView[]; + readonly relations: readonly ArchiveRunRelationEdgeView[]; +} + + interface ArchiveRunRow { id: string; status: string; @@ -527,6 +550,67 @@ export class PgArchiveRepository { return result.rows.map(runView); } + async getRunHistory(input: { + readonly modelId: string; + readonly testCaseId: string; + readonly limit?: number; + }): Promise { + const limit = input.limit ?? 50; + if (!Number.isInteger(limit) || limit < 1 || limit > 100) { + throw new Error("Archive history limit must be an integer between 1 and 100"); + } + + const [models, tests, newestRuns] = await Promise.all([ + this.listModels(), + this.listTests(), + this.listRuns({ + modelId: input.modelId, + testCaseId: input.testCaseId, + limit, + }), + ]); + + const model = models.find((candidate) => candidate.id === input.modelId); + const test = tests.find( + (candidate) => candidate.testCaseId === input.testCaseId, + ); + if (!model || !test) return null; + + const runs = [...newestRuns].reverse(); + const runIds = runs.map((run) => run.id); + + let relations: ArchiveRunRelationEdgeView[] = []; + if (runIds.length > 1) { + const relationResult = await this.pool.query<{ + from_run_id: string; + to_run_id: string; + relation_type: string; + created_at: Date; + }>( + `SELECT from_run_id, to_run_id, relation_type, created_at + FROM modelapse.run_relations + WHERE from_run_id = ANY($1::uuid[]) + AND to_run_id = ANY($1::uuid[]) + ORDER BY created_at ASC, from_run_id, to_run_id, relation_type`, + [runIds], + ); + + relations = relationResult.rows.map((relation) => ({ + fromRunId: relation.from_run_id, + toRunId: relation.to_run_id, + relationType: relation.relation_type, + createdAt: relation.created_at.toISOString(), + })); + } + + return { + model, + test, + runs, + relations, + }; + } + async getModel(modelId: string): Promise { const modelResult = await this.pool.query<{ id: string; @@ -1068,6 +1152,37 @@ export class PgArchiveRepository { const summaryRow = summaryResult.rows[0]; if (!summaryRow) return null; + const relationResult = await this.pool.query<{ + direction: "outgoing" | "incoming"; + relation_type: string; + related_run_id: string; + created_at: Date; + }>( + `SELECT + CASE + WHEN rr.from_run_id = $1 THEN 'outgoing' + ELSE 'incoming' + END AS direction, + rr.relation_type, + CASE + WHEN rr.from_run_id = $1 THEN rr.to_run_id + ELSE rr.from_run_id + END AS related_run_id, + rr.created_at + FROM modelapse.run_relations rr + JOIN modelapse.runs related + ON related.id = CASE + WHEN rr.from_run_id = $1 THEN rr.to_run_id + ELSE rr.from_run_id + END + JOIN modelapse.test_cases related_tc ON related_tc.id = related.test_case_id + WHERE (rr.from_run_id = $1 OR rr.to_run_id = $1) + AND related.sealed_at IS NOT NULL + AND related_tc.visibility = 'public' + ORDER BY rr.created_at ASC, rr.relation_type`, + [runId], + ); + const detailResult = await this.pool.query<{ config: Readonly> | null; request_sha256: string | null; @@ -1191,6 +1306,12 @@ export class PgArchiveRepository { usage: detail.usage, timing: detail.timing, evidence: detail.evidence, + relations: relationResult.rows.map((relation) => ({ + direction: relation.direction, + relationType: relation.relation_type, + relatedRunId: relation.related_run_id, + createdAt: relation.created_at.toISOString(), + })), }; } } From 228a240c01bd1d996d7bcd68ea77561c70c85d36 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 01:10:24 +0800 Subject: [PATCH 02/15] api: expose public same-Test temporal Run history --- apps/api/src/app.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 7340d85..1750075 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -33,6 +33,7 @@ type ArchiveRepository = Pick< | "getModel" | "getTest" | "compareLatest" + | "getRunHistory" >; export interface AppDependencies { @@ -199,6 +200,42 @@ export function createApp(deps: AppDependencies) { return c.json({ test }); }); + app.get("/v1/archive/history", async (c) => { + if (!deps.archive) { + return c.json({ error: "archive_unavailable" }, 503); + } + + const modelId = c.req.query("modelId"); + const testCaseId = c.req.query("testCaseId"); + const rawLimit = c.req.query("limit"); + + if (!modelId || !UUID_RE.test(modelId)) { + return c.json({ error: "invalid_model_id" }, 400); + } + if (!testCaseId || !UUID_RE.test(testCaseId)) { + return c.json({ error: "invalid_test_case_id" }, 400); + } + + const limit = rawLimit === undefined ? undefined : Number(rawLimit); + if ( + limit !== undefined && + (!Number.isInteger(limit) || limit < 1 || limit > 100) + ) { + return c.json({ error: "invalid_limit" }, 400); + } + + const history = await deps.archive.getRunHistory({ + modelId, + testCaseId, + ...(limit !== undefined ? { limit } : {}), + }); + if (!history) { + return c.json({ error: "archive_history_not_found" }, 404); + } + + return c.json({ history }); + }); + app.get("/v1/archive/compare", async (c) => { if (!deps.archive) { return c.json({ error: "archive_unavailable" }, 503); From ede240daeaa8561e9b6c259e381cdb3b9a290a84 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 01:10:54 +0800 Subject: [PATCH 03/15] api: cover public repeat-run history endpoint --- apps/api/test/archive-api.test.ts | 81 +++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/apps/api/test/archive-api.test.ts b/apps/api/test/archive-api.test.ts index 1881384..c989f93 100644 --- a/apps/api/test/archive-api.test.ts +++ b/apps/api/test/archive-api.test.ts @@ -4,6 +4,7 @@ import { createApp } from "../src/app.js"; const RUN_ID = "00000000-0000-4000-8000-000000000021"; const MODEL_ID = "00000000-0000-4000-8000-000000000022"; const TEST_CASE_ID = "00000000-0000-4000-8000-000000000023"; +const PREVIOUS_RUN_ID = "00000000-0000-4000-8000-000000000037"; function baseRuns() { return { @@ -103,6 +104,62 @@ const archiveRun = { }, }, ], + relations: [ + { + direction: "outgoing", + relationType: "repeat_of", + relatedRunId: PREVIOUS_RUN_ID, + createdAt: "2026-09-24T08:00:01.200Z", + }, + ], +} as const; + +const archiveRunHistory = { + model: { + id: MODEL_ID, + provider: archiveRun.provider, + canonicalSlug: "deepseek-flash", + marketingName: "DeepSeek Flash", + status: "active", + runCount: 2, + latestRunAt: archiveRun.completedAt, + }, + 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: 2, + }, + runs: [ + { + ...archiveRun, + id: PREVIOUS_RUN_ID, + createdAt: "2026-09-23T08:00:00.000Z", + completedAt: "2026-09-23T08:00:01.000Z", + sealedAt: "2026-09-23T08:00:01.100Z", + relations: [], + }, + archiveRun, + ], + relations: [ + { + fromRunId: RUN_ID, + toRunId: PREVIOUS_RUN_ID, + relationType: "repeat_of", + createdAt: "2026-09-24T08:00:01.200Z", + }, + ], } as const; const archiveModelDetail = { @@ -306,6 +363,7 @@ describe("Archive read API", () => { getTest: async (testCaseId) => testCaseId === TEST_CASE_ID ? archiveTestDetail : null, compareLatest: async () => archiveComparison, + getRunHistory: async () => archiveRunHistory, }, }); @@ -353,6 +411,14 @@ describe("Archive read API", () => { comparison: archiveComparison, }); + const history = await app.request( + `/v1/archive/history?modelId=${MODEL_ID}&testCaseId=${TEST_CASE_ID}&limit=20`, + ); + expect(history.status).toBe(200); + await expect(history.json()).resolves.toEqual({ + history: archiveRunHistory, + }); + const runs = await app.request( `/v1/archive/runs?modelId=${MODEL_ID}&testCaseId=${TEST_CASE_ID}&limit=10`, ); @@ -376,6 +442,7 @@ describe("Archive read API", () => { getModel: async () => null, getTest: async () => null, compareLatest: async () => null, + getRunHistory: async () => null, }, }); @@ -398,6 +465,20 @@ describe("Archive read API", () => { ) ).status, ).toBe(400); + expect( + ( + await app.request( + `/v1/archive/history?modelId=nope&testCaseId=${TEST_CASE_ID}`, + ) + ).status, + ).toBe(400); + expect( + ( + await app.request( + `/v1/archive/history?modelId=${MODEL_ID}&testCaseId=${TEST_CASE_ID}&limit=101`, + ) + ).status, + ).toBe(400); }); }); From a86f0ec479a64a77c28fa0ee40f1c076b6ed64fd Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 01:11:22 +0800 Subject: [PATCH 04/15] archive: integration-test repeat history and Run lineage --- packages/persistence/test/integration.test.ts | 70 +++++++++++++++++-- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/packages/persistence/test/integration.test.ts b/packages/persistence/test/integration.test.ts index 22e5bf8..3479250 100644 --- a/packages/persistence/test/integration.test.ts +++ b/packages/persistence/test/integration.test.ts @@ -276,6 +276,39 @@ describe("PostgreSQL Run persistence", () => { routedProviderName: "Fake Upstream", }); + const repeat = await executePersistedProviderRun({ + repository, + blobStore, + adapter, + transport, + credentials: { resolve: async () => "unused" }, + signer: { keyId: "integration-key", privateKey }, + attestationPublicKeyPem: publicKey + .export({ type: "spki", format: "pem" }) + .toString(), + request: { + model: "fake-model", + messages: [ + { role: "user", content: [{ type: "text", text: "hello" }] }, + ], + }, + run: { + testCaseId, + modelId, + snapshotId, + providerId, + runnerBuild: "integration-test-repeat", + }, + collector: "modelapse-integration-test", + }); + + await seedPool.query( + `INSERT INTO modelapse.run_relations + (from_run_id, to_run_id, relation_type) + VALUES ($1, $2, 'repeat_of')`, + [repeat.run.id, result.run.id], + ); + const archived = await archive.getRun(result.run.id); expect(archived).not.toBeNull(); expect(archived?.requestBlob).toMatchObject({ @@ -301,15 +334,40 @@ describe("PostgreSQL Run persistence", () => { algorithm: "Ed25519", }, }); + expect(archived?.relations).toMatchObject([ + { + direction: "incoming", + relationType: "repeat_of", + relatedRunId: repeat.run.id, + }, + ]); + + const repeatHistory = await archive.getRunHistory({ + modelId, + testCaseId, + limit: 20, + }); + expect(repeatHistory).not.toBeNull(); + expect(repeatHistory?.runs.map((run) => run.id)).toEqual([ + result.run.id, + repeat.run.id, + ]); + expect(repeatHistory?.relations).toMatchObject([ + { + fromRunId: repeat.run.id, + toRunId: result.run.id, + relationType: "repeat_of", + }, + ]); const archivedModel = await archive.getModel(modelId); expect(archivedModel).toMatchObject({ id: modelId, family: { displayName: "Integration Models" }, track: { displayName: "Main" }, - runCount: 1, + runCount: 2, snapshots: [{ id: snapshotId }], - testCoverage: [{ testCaseId, runCount: 1 }], + testCoverage: [{ testCaseId, runCount: 2 }], }); expect( archivedModel?.relations.some( @@ -328,10 +386,10 @@ describe("PostgreSQL Run persistence", () => { expect(archivedTest).toMatchObject({ testCaseId, origin: "modelapse", - runCount: 1, - modelCoverage: [{ modelId, runCount: 1 }], + runCount: 2, + modelCoverage: [{ modelId, runCount: 2 }], }); - expect(archivedTest?.recentRuns[0]?.id).toBe(result.run.id); + expect(archivedTest?.recentRuns[0]?.id).toBe(repeat.run.id); const comparison = await archive.compareLatest({ modelIds: [modelId, relatedModelId], @@ -341,7 +399,7 @@ describe("PostgreSQL Run persistence", () => { expect(comparison?.rows).toHaveLength(2); expect(comparison?.rows[0]).toMatchObject({ model: { id: modelId }, - latestRun: { id: result.run.id }, + latestRun: { id: repeat.run.id }, }); expect(comparison?.rows[1]).toMatchObject({ model: { id: relatedModelId }, From eac4ecf46a6ea9ef63aa5cc60fa3e65d98b553df Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 01:11:34 +0800 Subject: [PATCH 05/15] database: index temporal Archive and incoming Run relations --- .../migrations/0006_archive_temporal_indexes.sql | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 packages/database/migrations/0006_archive_temporal_indexes.sql diff --git a/packages/database/migrations/0006_archive_temporal_indexes.sql b/packages/database/migrations/0006_archive_temporal_indexes.sql new file mode 100644 index 0000000..4652d41 --- /dev/null +++ b/packages/database/migrations/0006_archive_temporal_indexes.sql @@ -0,0 +1,12 @@ +BEGIN; + +SET search_path TO modelapse, public; + +CREATE INDEX runs_model_test_sealed_completed_idx + ON runs (model_id, test_case_id, completed_at DESC, created_at DESC) + WHERE sealed_at IS NOT NULL; + +CREATE INDEX run_relations_to_created_idx + ON run_relations (to_run_id, created_at DESC); + +COMMIT; From a1fd6dac088a64fc91365d39b0d8a5c8e37ee07e Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 01:12:11 +0800 Subject: [PATCH 06/15] web: add repeat history and temporal comparison loaders --- apps/web/src/modelapse.ts | 134 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/apps/web/src/modelapse.ts b/apps/web/src/modelapse.ts index 438cb68..06bd81d 100644 --- a/apps/web/src/modelapse.ts +++ b/apps/web/src/modelapse.ts @@ -251,6 +251,20 @@ export interface ArchiveRunEvidence { } | null; } +export interface ArchiveRunRelationEdge { + readonly fromRunId: string; + readonly toRunId: string; + readonly relationType: string; + readonly createdAt: string; +} + +export interface ArchiveRunRelation { + readonly direction: "outgoing" | "incoming"; + readonly relationType: string; + readonly relatedRunId: string; + readonly createdAt: string; +} + export interface ArchiveRunDetail extends ArchiveRun { readonly configJson: string | null; readonly requestBlob: ArchiveBlob | null; @@ -259,6 +273,7 @@ export interface ArchiveRunDetail extends ArchiveRun { readonly usageJson: string | null; readonly timingJson: string | null; readonly evidence: readonly ArchiveRunEvidence[]; + readonly relations: readonly ArchiveRunRelation[]; } interface ArchiveRunDetailWire extends ArchiveRun { @@ -269,6 +284,23 @@ interface ArchiveRunDetailWire extends ArchiveRun { readonly usage: unknown; readonly timing: unknown; readonly evidence: readonly ArchiveRunEvidence[]; + readonly relations: readonly ArchiveRunRelation[]; +} + +export interface ArchiveRunHistory { + readonly model: ArchiveModel; + readonly test: ArchiveTest; + readonly runs: readonly ArchiveRun[]; + readonly relations: readonly ArchiveRunRelationEdge[]; +} + +export interface ArchiveTemporalComparison { + readonly test: ArchiveTest; + readonly rows: readonly { + readonly model: ArchiveModel; + readonly runs: readonly ArchiveRun[]; + readonly relations: readonly ArchiveRunRelationEdge[]; + }[]; } export interface ControlJob { @@ -326,6 +358,13 @@ interface CompareArchiveInput { readonly testCaseId: string; } +interface ReadArchiveHistoryInput { + readonly modelId: string; + readonly testCaseId: string; + readonly limit?: number; +} + + interface ApiOptions { readonly method?: "GET" | "POST"; readonly control?: boolean; @@ -536,6 +575,36 @@ function parseArchiveTestInput(value: unknown): ReadArchiveTestInput { return { testCaseId }; } +function parseArchiveHistoryInput(value: unknown): ReadArchiveHistoryInput { + if (!isRecord(value)) throw new Error("Archive history request must be an object"); + + const modelId = value.modelId; + const testCaseId = value.testCaseId; + const limit = value.limit; + + if (typeof modelId !== "string" || !UUID_RE.test(modelId)) { + throw new Error("modelId must be a UUID"); + } + if (typeof testCaseId !== "string" || !UUID_RE.test(testCaseId)) { + throw new Error("testCaseId must be a UUID"); + } + if ( + limit !== undefined && + (typeof limit !== "number" || + !Number.isInteger(limit) || + limit < 1 || + limit > 100) + ) { + throw new Error("limit must be an integer between 1 and 100"); + } + + return { + modelId, + testCaseId, + ...(limit === undefined ? {} : { limit }), + }; +} + function parseArchiveComparisonInput(value: unknown): CompareArchiveInput { if (!isRecord(value)) throw new Error("Archive comparison request must be an object"); @@ -669,6 +738,71 @@ export const getArchiveTest = createServerFn({ method: "POST" }) } }); +export const getArchiveRunHistory = createServerFn({ method: "POST" }) + .validator(parseArchiveHistoryInput) + .handler(async ({ data }): Promise => { + const params = new URLSearchParams({ + modelId: data.modelId, + testCaseId: data.testCaseId, + limit: String(data.limit ?? 50), + }); + try { + const result = await requestJson<{ history: ArchiveRunHistory }>( + `/v1/archive/history?${params.toString()}`, + ); + return result.history; + } catch (error) { + if (error instanceof ApiRequestError && error.status === 404) { + return null; + } + throw error; + } + }); + +export const compareArchiveHistory = createServerFn({ method: "POST" }) + .validator(parseArchiveComparisonInput) + .handler(async ({ data }): Promise => { + const histories = await Promise.all( + data.modelIds.map(async (modelId) => { + const params = new URLSearchParams({ + modelId, + testCaseId: data.testCaseId, + limit: "20", + }); + try { + const result = await requestJson<{ history: ArchiveRunHistory }>( + `/v1/archive/history?${params.toString()}`, + ); + return result.history; + } catch (error) { + if (error instanceof ApiRequestError && error.status === 404) { + return null; + } + throw error; + } + }), + ); + + if (histories.some((history) => history === null)) { + return null; + } + + const complete = histories.filter( + (history): history is ArchiveRunHistory => history !== null, + ); + const first = complete[0]; + if (!first) return null; + + return { + test: first.test, + rows: complete.map((history) => ({ + model: history.model, + runs: history.runs, + relations: history.relations, + })), + }; + }); + export const compareArchive = createServerFn({ method: "POST" }) .validator(parseArchiveComparisonInput) .handler(async ({ data }): Promise => { From ec84a62d693c477aa25b523fe0ae1291437fdbe4 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 01:12:53 +0800 Subject: [PATCH 07/15] web: add same-Test repeat Run history page --- .../routes/history.$modelId.$testCaseId.tsx | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 apps/web/src/routes/history.$modelId.$testCaseId.tsx diff --git a/apps/web/src/routes/history.$modelId.$testCaseId.tsx b/apps/web/src/routes/history.$modelId.$testCaseId.tsx new file mode 100644 index 0000000..b6c7ff7 --- /dev/null +++ b/apps/web/src/routes/history.$modelId.$testCaseId.tsx @@ -0,0 +1,210 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { getArchiveRunHistory, type ArchiveRun } from "../modelapse"; + +export const Route = createFileRoute("/history/$modelId/$testCaseId")({ + loader: ({ params }) => + getArchiveRunHistory({ + data: { + modelId: params.modelId, + testCaseId: params.testCaseId, + limit: 100, + }, + }), + component: ArchiveRunHistoryPage, +}); + +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 ArchiveRunHistoryPage() { + const history = Route.useLoaderData(); + + if (!history) { + return ( +
+
+ + M + + Modelapse + AI Model Test & Evolution Archive + + +
+
+

RUN HISTORY

+

History not found

+

The requested model × Test Case pair is not present in the public Archive.

+ ← Back to Archive +
+
+ ); + } + + const firstRun = history.runs[0] ?? null; + const latestRun = history.runs[history.runs.length - 1] ?? null; + + return ( +
+
+ + M + + Modelapse + AI Model Test & Evolution Archive + + + +
+ +
+ + +
+
+

REPEAT-RUN HISTORY

+

{history.model.marketingName}

+

+ {history.test.familyName} · {history.test.caseSlug} · v{history.test.version} +

+
+
+ {history.runs.length} sealed run(s) + {history.relations.length} explicit relation(s) +
+
+ +
+
+
First captured
+
{formatTimestamp(firstRun?.completedAt ?? firstRun?.sealedAt ?? null)}
+
+
+
Latest captured
+
{formatTimestamp(latestRun?.completedAt ?? latestRun?.sealedAt ?? null)}
+
+
+
Provider
+
{history.model.provider.name}
+
+
+
Evaluator
+
+ {history.test.evaluator + ? `${history.test.evaluator.slug}@${history.test.evaluator.version}` + : "—"} +
+
+
+
+ +
+
+
+

TEMPORAL RECORD

+

Same model × same Test

+
+

+ Runs are ordered by capture time. Changes are shown as records; no causal + explanation or quality trend is inferred. +

+
+ +
+ {history.runs.map((run, index) => ( +
+
{String(index + 1).padStart(2, "0")}
+
+ {formatTimestamp(run.completedAt ?? run.sealedAt)} + {run.runnerBuild} +
+
+ {evaluationLabel(run)} + evidence {run.evidenceLevel ?? "—"} +
+
+ {run.returnedModel ?? run.requestedModel} + {run.executionPath} +
+ + Run {run.id.slice(0, 8)} → + +
+ ))} + {history.runs.length === 0 ? ( +
No sealed public Runs exist for this pair yet.
+ ) : null} +
+
+ +
+
+
+

RUN LINEAGE

+

Explicit repeat / retry / reproduction links

+
+ {history.relations.length} edge(s) +
+ +
+ {history.relations.map((relation) => ( + + ))} + {history.relations.length === 0 ? ( +
+ No explicit Run relations are archived. Repeated Runs can still appear above + because they share the same canonical model and exact Test Case. +
+ ) : null} +
+
+ + +
+ ); +} From 53eca8cbba8df62ca21ba961bae0047e5130798a Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 01:13:17 +0800 Subject: [PATCH 08/15] web: register temporal Run history route --- apps/web/src/routeTree.gen.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index c4f8310..b0f1e05 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -7,6 +7,7 @@ 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 HistoryModelIdTestCaseIdRouteImport } from "./routes/history.$modelId.$testCaseId"; import { Route as CompareRouteImport } from "./routes/compare"; import { Route as IndexRouteImport } from "./routes/index"; @@ -28,6 +29,12 @@ const ModelsModelIdRoute = ModelsModelIdRouteImport.update({ getParentRoute: () => rootRouteImport, } as any); +const HistoryModelIdTestCaseIdRoute = HistoryModelIdTestCaseIdRouteImport.update({ + id: "/history/$modelId/$testCaseId", + path: "/history/$modelId/$testCaseId", + getParentRoute: () => rootRouteImport, +} as any); + const CompareRoute = CompareRouteImport.update({ id: "/compare", path: "/compare", @@ -43,6 +50,7 @@ const IndexRoute = IndexRouteImport.update({ export interface FileRoutesByFullPath { "/": typeof IndexRoute; "/compare": typeof CompareRoute; + "/history/$modelId/$testCaseId": typeof HistoryModelIdTestCaseIdRoute; "/models/$modelId": typeof ModelsModelIdRoute; "/runs/$runId": typeof RunsRunIdRoute; "/tests/$testCaseId": typeof TestsTestCaseIdRoute; @@ -51,6 +59,7 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { "/": typeof IndexRoute; "/compare": typeof CompareRoute; + "/history/$modelId/$testCaseId": typeof HistoryModelIdTestCaseIdRoute; "/models/$modelId": typeof ModelsModelIdRoute; "/runs/$runId": typeof RunsRunIdRoute; "/tests/$testCaseId": typeof TestsTestCaseIdRoute; @@ -60,6 +69,7 @@ export interface FileRoutesById { __root__: typeof rootRouteImport; "/": typeof IndexRoute; "/compare": typeof CompareRoute; + "/history/$modelId/$testCaseId": typeof HistoryModelIdTestCaseIdRoute; "/models/$modelId": typeof ModelsModelIdRoute; "/runs/$runId": typeof RunsRunIdRoute; "/tests/$testCaseId": typeof TestsTestCaseIdRoute; @@ -70,6 +80,7 @@ export interface FileRouteTypes { fullPaths: | "/" | "/compare" + | "/history/$modelId/$testCaseId" | "/models/$modelId" | "/runs/$runId" | "/tests/$testCaseId"; @@ -77,6 +88,7 @@ export interface FileRouteTypes { to: | "/" | "/compare" + | "/history/$modelId/$testCaseId" | "/models/$modelId" | "/runs/$runId" | "/tests/$testCaseId"; @@ -84,6 +96,7 @@ export interface FileRouteTypes { | "__root__" | "/" | "/compare" + | "/history/$modelId/$testCaseId" | "/models/$modelId" | "/runs/$runId" | "/tests/$testCaseId"; @@ -106,6 +119,13 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof CompareRouteImport; parentRoute: typeof rootRouteImport; }; + "/history/$modelId/$testCaseId": { + id: "/history/$modelId/$testCaseId"; + path: "/history/$modelId/$testCaseId"; + fullPath: "/history/$modelId/$testCaseId"; + preLoaderRoute: typeof HistoryModelIdTestCaseIdRouteImport; + parentRoute: typeof rootRouteImport; + }; "/models/$modelId": { id: "/models/$modelId"; path: "/models/$modelId"; @@ -133,6 +153,7 @@ declare module "@tanstack/react-router" { const rootRouteChildren = { IndexRoute, CompareRoute, + HistoryModelIdTestCaseIdRoute, ModelsModelIdRoute, RunsRunIdRoute, TestsTestCaseIdRoute, From 973949c3e13061050fde7afb77d00ef765d6b493 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 01:13:45 +0800 Subject: [PATCH 09/15] web: visualize direct model relations and link repeat histories --- apps/web/src/routes/models.$modelId.tsx | 66 ++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/apps/web/src/routes/models.$modelId.tsx b/apps/web/src/routes/models.$modelId.tsx index 31b8e51..0e88857 100644 --- a/apps/web/src/routes/models.$modelId.tsx +++ b/apps/web/src/routes/models.$modelId.tsx @@ -174,13 +174,13 @@ function ArchiveModelPage() {
{model.testCoverage.map((coverage) => ( - + ))} {model.testCoverage.length === 0 ? (
No sealed public Test Runs for this model yet.
@@ -239,6 +250,47 @@ function ArchiveModelPage() { {model.relations.length === 0 ?

No model relations archived.

: null}
+ +
+
+ DIRECT RELATION GRAPH + Stored edge direction is preserved exactly as archived. +
+ {model.relations.map((relation) => { + const currentNode = ( +
+ {model.marketingName} + {model.canonicalSlug} +
+ ); + const relatedNode = ( + + {relation.relatedModel.marketingName} + + {relation.relatedModel.providerSlug} · {relation.relatedModel.canonicalSlug} + + + ); + + return ( +
+ {relation.direction === "incoming" ? relatedNode : currentNode} +
+ + {relation.relationType.replaceAll("_", " ")} + confidence {relation.confidence} +
+ {relation.direction === "incoming" ? currentNode : relatedNode} +
+ ); + })} + {model.relations.length === 0 ? ( +
No direct model relation edges are archived.
+ ) : null} +
From 8b59f39c699ad25529d5a536257da74a32fac062 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 01:14:02 +0800 Subject: [PATCH 10/15] web: link Test coverage into temporal Run histories --- apps/web/src/routes/tests.$testCaseId.tsx | 25 ++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/apps/web/src/routes/tests.$testCaseId.tsx b/apps/web/src/routes/tests.$testCaseId.tsx index a6d6b96..d59ffe7 100644 --- a/apps/web/src/routes/tests.$testCaseId.tsx +++ b/apps/web/src/routes/tests.$testCaseId.tsx @@ -187,13 +187,13 @@ function ArchiveTestPage() {
{test.modelCoverage.map((coverage) => ( - + ))} {test.modelCoverage.length === 0 ? (
No model has a sealed public Run for this Test yet.
From 34c40f8ac8a34dfe0d8bee590d4ce00d91193a6a Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 25 Sep 2026 01:14:27 +0800 Subject: [PATCH 11/15] web: add same-Test temporal comparison lanes --- apps/web/src/routes/compare.tsx | 116 ++++++++++++++++++++++++++++---- 1 file changed, 103 insertions(+), 13 deletions(-) diff --git a/apps/web/src/routes/compare.tsx b/apps/web/src/routes/compare.tsx index 73f3baa..7502820 100644 --- a/apps/web/src/routes/compare.tsx +++ b/apps/web/src/routes/compare.tsx @@ -2,9 +2,11 @@ import { createFileRoute } from "@tanstack/react-router"; import { useMemo, useState } from "react"; import { compareArchive, + compareArchiveHistory, getArchiveCatalog, type ArchiveComparison, type ArchiveRun, + type ArchiveTemporalComparison, } from "../modelapse"; export const Route = createFileRoute("/compare")({ @@ -37,6 +39,7 @@ function ArchiveComparePage() { const [modelIds, setModelIds] = useState(defaultModelIds); const [testCaseId, setTestCaseId] = useState(catalog.tests[0]?.testCaseId ?? ""); const [comparison, setComparison] = useState(null); + const [temporal, setTemporal] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); @@ -47,6 +50,7 @@ function ArchiveComparePage() { function toggleModel(modelId: string): void { setComparison(null); + setTemporal(null); setError(null); setModelIds((current) => { if (current.includes(modelId)) { @@ -63,18 +67,28 @@ function ArchiveComparePage() { setBusy(true); setError(null); try { - const result = await compareArchive({ - data: { - modelIds, - testCaseId, - }, - }); + const [result, history] = await Promise.all([ + compareArchive({ + data: { + modelIds, + testCaseId, + }, + }), + compareArchiveHistory({ + data: { + modelIds, + testCaseId, + }, + }), + ]); setComparison(result); - if (!result) { + setTemporal(history); + if (!result || !history) { setError("No public Archive comparison exists for this selection."); } } catch (caught) { setComparison(null); + setTemporal(null); setError(caught instanceof Error ? caught.message : "Comparison failed"); } finally { setBusy(false); @@ -106,11 +120,11 @@ function ArchiveComparePage() {
-

COMPARISON FOUNDATION

-

Same Test. Latest sealed Run.

+

TEMPORAL COMPARISON

+

Same Test. Latest view + history.

- Compare 2–4 canonical models against one public Test Case without - turning the Archive into a ranking table. + Compare 2–4 canonical models against one public Test Case, then inspect + each model’s sealed Run history without turning the Archive into a ranking table.

@@ -125,6 +139,7 @@ function ArchiveComparePage() { onChange={(event) => { setTestCaseId(event.target.value); setComparison(null); + setTemporal(null); setError(null); }} > @@ -135,7 +150,7 @@ function ArchiveComparePage() { ))} - Comparison uses the latest sealed public Run for this exact Test Case. + Latest comparison and temporal lanes use this exact public Test Case. @@ -177,7 +192,7 @@ function ArchiveComparePage() { disabled={busy || modelIds.length < 2 || !testCaseId} onClick={() => void handleCompare()} > - {busy ? "Comparing…" : "Compare latest sealed Runs"} + {busy ? "Comparing…" : "Compare latest + history"}
@@ -280,6 +295,81 @@ function ArchiveComparePage() { )}
+
+
+
+

SAME-TEST HISTORY

+

+ {temporal + ? `${temporal.test.caseSlug} across capture time` + : "Temporal lanes appear after comparison"} +

+
+

+ Every lane is ordered by capture time. Result changes are displayed as observations, + not as an inferred trend, winner, or cause. +

+
+ + {temporal ? ( +
+ {temporal.rows.map((row) => ( +
+
+
+ {row.model.provider.slug} + {row.model.marketingName} + {row.model.canonicalSlug} +
+ + Full history → + +
+ +
+ {row.runs.map((run, index) => ( + + + {String(index + 1).padStart(2, "0")} + + {formatTimestamp(run.completedAt ?? run.sealedAt)} + {evaluationLabel(run)} + + {run.evidenceLevel ?? "—"} · {run.returnedModel ?? run.requestedModel} + + + ))} + {row.runs.length === 0 ? ( +
+ No sealed public Run for this model × Test Case pair. +
+ ) : null} +
+ +
+ {row.runs.length} Run(s) + {row.relations.length} explicit relation(s) +
+
+ ))} +
+ ) : ( +
+

+ Select one Test and 2–4 models to inspect both the latest sealed record + and the sequence of earlier sealed Runs for the same exact Test. +

+
+ )} +
+