diff --git a/apps/web/app/benchmarks/[id]/page.tsx b/apps/web/app/benchmarks/[id]/page.tsx
index cf3ab1e..f79dd98 100644
--- a/apps/web/app/benchmarks/[id]/page.tsx
+++ b/apps/web/app/benchmarks/[id]/page.tsx
@@ -1,7 +1,11 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
-import { loadBenchmarkById, loadBenchmarkPairsById } from "../../../lib/data";
+import {
+ loadBenchmarkById,
+ loadBenchmarkPairsById,
+ loadAnalysisIndex,
+} from "../../../lib/data";
import {
buildBenchmarkRunRoster,
sortBenchmarkRunRoster,
@@ -14,6 +18,11 @@ import { ResponsiveTable } from "../../../components/ResponsiveTable";
import { TruncateText } from "../../../components/ResponsiveTable";
import { findMostSimilarPeerRunId } from "../../../lib/benchmarkPeers";
import { inferModeLabel } from "../../../lib/modeLabeler";
+import {
+ buildBenchmarkIndexLookup,
+ resolveIndexedModeLabel,
+} from "../../../lib/benchmarkIndexLookup";
+import { buildBenchmarkOutlierLookup } from "../../../lib/outlierLookup";
import { questionHubHref } from "../../../lib/questionGroups";
import {
extractBenchmarkSummaryDisplay,
@@ -46,12 +55,21 @@ export default async function BenchmarkDetailPage({
params: Promise<{ id: string }>;
}) {
const { id } = await params;
- const benchmark = await loadBenchmarkById(id);
+ const [benchmark, index] = await Promise.all([
+ loadBenchmarkById(id),
+ loadAnalysisIndex(),
+ ]);
if (!benchmark) {
notFound();
}
+ const benchmarkIndex = index
+ ? buildBenchmarkIndexLookup(index).get(benchmark.id)
+ : null;
+ const outlierLookup = buildBenchmarkOutlierLookup(index, benchmark.id);
+ const outlierCount = outlierLookup.size;
+
const runIds = benchmark.payload.runIds ?? [];
const pairsData = await loadBenchmarkPairsById(id);
const pairs =
@@ -78,6 +96,7 @@ export default async function BenchmarkDetailPage({
peerCompareHref: peerRunId
? `/runs/compare?left=${row.runId}&right=${peerRunId}`
: null,
+ outlier: outlierLookup.get(row.runId) ?? null,
};
});
const thresholdCounts = [
@@ -140,6 +159,20 @@ export default async function BenchmarkDetailPage({
>
Compare as right
+
+ Pairwise explorer
+
+ {outlierCount > 0 ? (
+
+ Outlier runs ({outlierCount})
+
+ ) : null}
@@ -351,6 +384,7 @@ export default async function BenchmarkDetailPage({
thresholdCounts={thresholdCounts}
similarityPairs={pairs}
runs={benchmark.payload.runs}
+ runIds={rosterRunIds}
/>
@@ -405,6 +439,29 @@ export default async function BenchmarkDetailPage({
: value.toFixed(3);
},
},
+ {
+ key: "outlier",
+ label: "Outlier",
+ helpKey: "zScore",
+ hideOnMobile: true,
+ render: (row) => {
+ const outlier = (
+ row as {
+ outlier: {
+ zScore: number;
+ } | null;
+ }
+ ).outlier;
+ if (!outlier) return "—";
+ return (
+
+ Yes
+
+ );
+ },
+ },
{
key: "open",
label: "Open",
@@ -567,7 +624,11 @@ export default async function BenchmarkDetailPage({
]}
data={modes.map((mode, idx) => ({
modeIndex: idx,
- label: inferModeLabel(mode.exemplarPreview),
+ label: resolveIndexedModeLabel(
+ benchmarkIndex?.modeLabels ?? [],
+ idx,
+ inferModeLabel(mode.exemplarPreview),
+ ),
size: mode.size,
exemplarPreview: mode.exemplarPreview,
memberRunIds: mode.members
diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css
index 671ca84..d2d7506 100644
--- a/apps/web/app/globals.css
+++ b/apps/web/app/globals.css
@@ -2013,6 +2013,21 @@ pre {
transform: scale(1.05);
}
+.benchmark-heatmap-run-link,
+.benchmark-heatmap-cell-link {
+ color: inherit;
+ text-decoration: none;
+}
+
+.benchmark-heatmap-cell-link {
+ display: block;
+}
+
+.benchmark-heatmap-run-link:hover,
+.benchmark-heatmap-cell-link:hover {
+ text-decoration: underline;
+}
+
@media (max-width: 600px) {
.trace-timeline::before {
left: 0.95rem;
diff --git a/apps/web/app/questions/view/page.tsx b/apps/web/app/questions/view/page.tsx
index bb58dbf..343efb7 100644
--- a/apps/web/app/questions/view/page.tsx
+++ b/apps/web/app/questions/view/page.tsx
@@ -18,7 +18,10 @@ import {
loadRunsByQuestion,
} from "../../../lib/data";
import { buildQueryString } from "../../../lib/listPagination";
-import { questionHubHref } from "../../../lib/questionGroups";
+import {
+ questionHubHref,
+ listQuestionInsightLinks,
+} from "../../../lib/questionGroups";
import {
buildQuestionExperimentMatrix,
lookupMatrixCell,
@@ -410,7 +413,28 @@ export default async function QuestionHubPage({
critique rollups for this question.
- ) : null}
+ ) : (
+
+
Research insights
+
+ Open filtered insight explorers scoped to this question.
+
+
+ {listQuestionInsightLinks(question).map((link) => (
+
+ {link.label}
+
+ ))}
+
+
+ )}
{experimentMatrix.models.length > 0 &&
experimentMatrix.presets.length > 0 ? (
@@ -495,24 +519,6 @@ export default async function QuestionHubPage({
- {indexMetrics?.runsWithQualityScores ? (
-
-
- Quality insights for this question
-
-
- ) : null}
) : null}
diff --git a/apps/web/components/charts/BenchmarkDetailCharts.tsx b/apps/web/components/charts/BenchmarkDetailCharts.tsx
index 61d3c38..abd55ac 100644
--- a/apps/web/components/charts/BenchmarkDetailCharts.tsx
+++ b/apps/web/components/charts/BenchmarkDetailCharts.tsx
@@ -1,5 +1,6 @@
"use client";
+import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import {
Bar,
@@ -18,8 +19,13 @@ type BenchmarkDetailChartsProps = {
thresholdCounts: Array<{ threshold: string; modeCount: number }>;
similarityPairs?: Array<{ i: number; j: number; similarity: number }>;
runs: number;
+ runIds?: string[];
};
+function buildPairCompareHref(runIdA: string, runIdB: string): string {
+ return `/runs/compare?left=${runIdA}&right=${runIdB}`;
+}
+
function similarityColor(value: number) {
if (value >= 0.95) return "rgba(34, 197, 94, 0.5)";
if (value >= 0.9) return "rgba(34, 197, 94, 0.4)";
@@ -35,6 +41,7 @@ export function BenchmarkDetailCharts({
thresholdCounts,
similarityPairs = [],
runs,
+ runIds = [],
}: BenchmarkDetailChartsProps) {
const [pairs, setPairs] = useState(similarityPairs);
const [pairsSource, setPairsSource] = useState<"artifact" | "chunk">(
@@ -176,41 +183,93 @@ export function BenchmarkDetailCharts({
- source: {pairsSource} ({pairs.length} pairwise entries)
+ source: {pairsSource} ({pairs.length} pairwise entries).
+ Click off-diagonal cells to compare runs.
|
- {Array.from({ length: runs }, (_, idx) => (
- r{idx} |
- ))}
+ {Array.from({ length: runs }, (_, idx) => {
+ const runId = runIds[idx];
+ const label = `r${idx}`;
+ return (
+
+ {runId ? (
+
+ {label}
+
+ ) : (
+ label
+ )}
+ |
+ );
+ })}
{matrix.map((row, i) => (
- | r{i} |
- {row.map((value, j) => (
-
- {value.toFixed(3)}
- |
- ))}
+
+ {runIds[i] ? (
+
+ r{i}
+
+ ) : (
+ `r${i}`
+ )}
+ |
+ {row.map((value, j) => {
+ const runIdA = runIds[i];
+ const runIdB = runIds[j];
+ const compareHref =
+ i !== j && runIdA && runIdB
+ ? buildPairCompareHref(
+ runIdA,
+ runIdB,
+ )
+ : null;
+ const cellContent = value.toFixed(3);
+ const cellStyle = {
+ background:
+ i === j
+ ? "var(--color-bg-elevated)"
+ : similarityColor(value),
+ color: "var(--color-text-primary)",
+ minWidth: 44,
+ };
+ const title = `r${i} vs r${j}: ${value.toFixed(3)}`;
+
+ return (
+
+ {compareHref ? (
+
+ {cellContent}
+
+ ) : (
+ cellContent
+ )}
+ |
+ );
+ })}
))}
diff --git a/apps/web/lib/benchmarkIndexLookup.test.ts b/apps/web/lib/benchmarkIndexLookup.test.ts
index e479fb8..adc71d1 100644
--- a/apps/web/lib/benchmarkIndexLookup.test.ts
+++ b/apps/web/lib/benchmarkIndexLookup.test.ts
@@ -3,6 +3,7 @@ import type { AnalysisIndex } from "./data";
import {
buildBenchmarkIndexLookup,
formatTopModeLabel,
+ resolveIndexedModeLabel,
} from "./benchmarkIndexLookup";
function sampleIndex(): AnalysisIndex {
@@ -65,4 +66,13 @@ describe("benchmarkIndexLookup", () => {
const labels = lookup.get("bench-1")!.modeLabels;
expect(formatTopModeLabel(labels)).toBe("Yes with safeguards");
});
+
+ it("prefers indexed mode labels over fallback text", () => {
+ const lookup = buildBenchmarkIndexLookup(sampleIndex());
+ const labels = lookup.get("bench-1")!.modeLabels;
+ expect(resolveIndexedModeLabel(labels, 0, "fallback")).toBe(
+ "Yes with safeguards",
+ );
+ expect(resolveIndexedModeLabel(labels, 9, "fallback")).toBe("fallback");
+ });
});
diff --git a/apps/web/lib/benchmarkIndexLookup.ts b/apps/web/lib/benchmarkIndexLookup.ts
index 4fbccfd..2cb6c01 100644
--- a/apps/web/lib/benchmarkIndexLookup.ts
+++ b/apps/web/lib/benchmarkIndexLookup.ts
@@ -20,6 +20,17 @@ export function buildBenchmarkIndexLookup(
return lookup;
}
+export function resolveIndexedModeLabel(
+ modeLabels: BenchmarkIndexSnapshot["modeLabels"],
+ modeIndex: number,
+ fallbackLabel: string,
+): string {
+ const indexed = modeLabels.find((row) => row.modeIndex === modeIndex);
+ const label = indexed?.label?.trim();
+ if (label) return label;
+ return fallbackLabel;
+}
+
export function formatTopModeLabel(
modeLabels: BenchmarkIndexSnapshot["modeLabels"],
maxLength = 48,
diff --git a/apps/web/lib/outlierLookup.test.ts b/apps/web/lib/outlierLookup.test.ts
index 61f4341..bf1f499 100644
--- a/apps/web/lib/outlierLookup.test.ts
+++ b/apps/web/lib/outlierLookup.test.ts
@@ -2,8 +2,10 @@ import { describe, expect, it, vi } from "vitest";
import type { AnalysisIndex } from "./data";
import {
buildOutlierRunIdSet,
+ buildBenchmarkOutlierLookup,
buildRunOutlierContext,
findRunOutlierEntry,
+ listOutliersForBenchmark,
} from "./outlierLookup";
vi.mock("./data", async (importOriginal) => {
@@ -40,6 +42,12 @@ function sampleIndex(): AnalysisIndex {
avgSimilarity: 0.42,
zScore: -2.1,
},
+ {
+ benchmarkId: "bench_2",
+ runId: "run_other",
+ avgSimilarity: 0.5,
+ zScore: -1.8,
+ },
],
},
skipped: [],
@@ -77,4 +85,21 @@ describe("outlierLookup", () => {
peerCompareHref: "/runs/compare?left=run_outlier&right=run_b",
});
});
+
+ it("lists outliers for a benchmark", () => {
+ const rows = listOutliersForBenchmark(sampleIndex(), "bench_1");
+ expect(rows).toEqual([
+ {
+ runId: "run_outlier",
+ avgSimilarity: 0.42,
+ zScore: -2.1,
+ },
+ ]);
+ });
+
+ it("builds a benchmark outlier lookup map", () => {
+ const lookup = buildBenchmarkOutlierLookup(sampleIndex(), "bench_1");
+ expect(lookup.get("run_outlier")?.zScore).toBe(-2.1);
+ expect(lookup.has("run_other")).toBe(false);
+ });
});
diff --git a/apps/web/lib/outlierLookup.ts b/apps/web/lib/outlierLookup.ts
index 2e93de3..574fc69 100644
--- a/apps/web/lib/outlierLookup.ts
+++ b/apps/web/lib/outlierLookup.ts
@@ -30,6 +30,39 @@ export function buildOutlierRunIdSet(index: AnalysisIndex | null): Set {
return new Set(index.aggregates.outlierRuns?.map((row) => row.runId) ?? []);
}
+export type BenchmarkOutlierEntry = {
+ runId: string;
+ avgSimilarity: number;
+ zScore: number;
+};
+
+export function listOutliersForBenchmark(
+ index: AnalysisIndex | null,
+ benchmarkId: string,
+): BenchmarkOutlierEntry[] {
+ if (!index) return [];
+ return (
+ index.aggregates.outlierRuns
+ ?.filter((row) => row.benchmarkId === benchmarkId)
+ .map(({ runId, avgSimilarity, zScore }) => ({
+ runId,
+ avgSimilarity,
+ zScore,
+ })) ?? []
+ );
+}
+
+export function buildBenchmarkOutlierLookup(
+ index: AnalysisIndex | null,
+ benchmarkId: string,
+): Map {
+ const lookup = new Map();
+ for (const entry of listOutliersForBenchmark(index, benchmarkId)) {
+ lookup.set(entry.runId, entry);
+ }
+ return lookup;
+}
+
export async function buildRunOutlierContext(
index: AnalysisIndex | null,
runId: string,
diff --git a/apps/web/lib/questionGroups.test.ts b/apps/web/lib/questionGroups.test.ts
index bc90f7e..c1963b5 100644
--- a/apps/web/lib/questionGroups.test.ts
+++ b/apps/web/lib/questionGroups.test.ts
@@ -3,7 +3,9 @@ import type { BenchmarkArtifact, RunArtifact } from "./data";
import {
filterArtifactsForQuestionGroups,
groupArtifactsByQuestion,
+ listQuestionInsightLinks,
questionHubHref,
+ questionInsightHref,
} from "./questionGroups";
function makeRun(id: string, question: string, createdAt: string): RunArtifact {
@@ -58,6 +60,30 @@ describe("questionHubHref", () => {
});
});
+describe("questionInsightHref", () => {
+ it("builds filtered insight explorer links", () => {
+ expect(questionInsightHref("Is AI safe?", "drift")).toBe(
+ "/drift?q=Is+AI+safe%3F",
+ );
+ expect(questionInsightHref("Is AI safe?", "outliers")).toBe(
+ "/outliers?q=Is+AI+safe%3F",
+ );
+ });
+
+ it("lists all insight shortcuts for a question", () => {
+ const links = listQuestionInsightLinks("Q?");
+ expect(links.map((link) => link.page)).toEqual([
+ "quality",
+ "drift",
+ "evidence",
+ "counterfactual",
+ "issues",
+ "outliers",
+ ]);
+ expect(links[0]?.href).toBe("/quality?q=Q%3F");
+ });
+});
+
describe("groupArtifactsByQuestion", () => {
it("groups runs and benchmarks by question", () => {
const groups = groupArtifactsByQuestion(
diff --git a/apps/web/lib/questionGroups.ts b/apps/web/lib/questionGroups.ts
index 3d6fc5a..67ce289 100644
--- a/apps/web/lib/questionGroups.ts
+++ b/apps/web/lib/questionGroups.ts
@@ -24,6 +24,41 @@ export function questionHubHref(question: string): string {
return `/questions/view?${new URLSearchParams({ question }).toString()}`;
}
+export type QuestionInsightPage =
+ | "quality"
+ | "drift"
+ | "evidence"
+ | "counterfactual"
+ | "issues"
+ | "outliers";
+
+const QUESTION_INSIGHT_PAGES: Array<{
+ page: QuestionInsightPage;
+ label: string;
+}> = [
+ { page: "quality", label: "Quality insights" },
+ { page: "drift", label: "Confidence drift" },
+ { page: "evidence", label: "Evidence planning" },
+ { page: "counterfactual", label: "Counterfactual modes" },
+ { page: "issues", label: "Critique issues" },
+ { page: "outliers", label: "Outlier runs" },
+];
+
+export function questionInsightHref(
+ question: string,
+ page: QuestionInsightPage,
+): string {
+ return `/${page}?${new URLSearchParams({ q: question }).toString()}`;
+}
+
+export function listQuestionInsightLinks(question: string) {
+ return QUESTION_INSIGHT_PAGES.map(({ page, label }) => ({
+ page,
+ label,
+ href: questionInsightHref(question, page),
+ }));
+}
+
export function filterArtifactsForQuestionGroups(
runs: RunArtifact[],
benchmarks: BenchmarkArtifact[],