diff --git a/apps/web/app/api/pairs/route.ts b/apps/web/app/api/pairs/route.ts
index cf32240..01749d1 100644
--- a/apps/web/app/api/pairs/route.ts
+++ b/apps/web/app/api/pairs/route.ts
@@ -1,16 +1,33 @@
import { loadBenchmarkArtifacts } from "../../../lib/data";
+import type { ArtifactFilterParams } from "../../../lib/data";
import { pairsExplorerToCsv } from "../../../lib/listExport";
import {
buildBenchmarkPairDetails,
buildBenchmarkPairSummaries,
+ filterPairSummaries,
} from "../../../lib/pairsExplorer";
+function readPairFilters(url: URL): ArtifactFilterParams {
+ return {
+ q: url.searchParams.get("q") ?? undefined,
+ model: url.searchParams.get("model") ?? undefined,
+ preset: url.searchParams.get("preset") ?? undefined,
+ fast: url.searchParams.get("fast") ?? undefined,
+ from: url.searchParams.get("from") ?? undefined,
+ to: url.searchParams.get("to") ?? undefined,
+ };
+}
+
export async function GET(request: Request) {
const url = new URL(request.url);
const format = url.searchParams.get("format") ?? "json";
const benchmark = url.searchParams.get("benchmark") ?? undefined;
+ const filters = readPairFilters(url);
const benchmarks = await loadBenchmarkArtifacts();
- const summaries = await buildBenchmarkPairSummaries(benchmarks);
+ const summaries = filterPairSummaries(
+ await buildBenchmarkPairSummaries(benchmarks),
+ filters,
+ );
if (benchmark) {
const details = await buildBenchmarkPairDetails(benchmark, benchmarks);
diff --git a/apps/web/app/errors/page.tsx b/apps/web/app/errors/page.tsx
index 3f08bce..cfb8cc5 100644
--- a/apps/web/app/errors/page.tsx
+++ b/apps/web/app/errors/page.tsx
@@ -18,6 +18,11 @@ import {
buildPipelineErrorRows,
collectPipelineErrorAgents,
} from "../../lib/pipelineErrors";
+import {
+ buildPipelineErrorByAgent,
+ buildPipelineErrorTrendSeries,
+} from "../../lib/pipelineErrorTrends";
+import { PipelineErrorTrendCharts } from "../../components/charts/PipelineErrorTrendCharts";
export const metadata: Metadata = {
title: "Pipeline errors",
@@ -43,6 +48,8 @@ export default async function PipelineErrorsPage({
agent: params.agent,
});
const errorAgents = collectPipelineErrorAgents(filteredRuns);
+ const errorTrendSeries = buildPipelineErrorTrendSeries(rows);
+ const errorAgentSeries = buildPipelineErrorByAgent(rows);
const uniqueRuns = new Set(rows.map((row) => row.runId)).size;
if (allRuns.length === 0) {
@@ -192,6 +199,11 @@ export default async function PipelineErrorsPage({
/>
+
+
diff --git a/apps/web/app/questions/view/page.tsx b/apps/web/app/questions/view/page.tsx
index f44b661..8c05f5d 100644
--- a/apps/web/app/questions/view/page.tsx
+++ b/apps/web/app/questions/view/page.tsx
@@ -14,14 +14,18 @@ import {
filterBenchmarkArtifacts,
filterRunArtifacts,
loadAnalysisIndex,
+ loadBenchmarkArtifacts,
loadBenchmarksByQuestion,
+ loadRunArtifacts,
loadRunsByQuestion,
} from "../../../lib/data";
import { buildQueryString } from "../../../lib/listPagination";
import {
+ groupArtifactsByQuestion,
questionHubHref,
listQuestionInsightLinks,
} from "../../../lib/questionGroups";
+import { buildTopQuestions } from "../../../lib/topQuestions";
import {
buildQuestionExperimentMatrix,
buildMatrixCellRunCompareHref,
@@ -56,14 +60,115 @@ export default async function QuestionHubPage({
const params = await searchParams;
const question = (params.question ?? "").trim();
if (!question) {
+ const [runs, benchmarks] = await Promise.all([
+ loadRunArtifacts(),
+ loadBenchmarkArtifacts(),
+ ]);
+ const topQuestions = buildTopQuestions(
+ groupArtifactsByQuestion(runs, benchmarks),
+ 12,
+ );
+
return (
- Question hub
-
- Pick a question from the{" "}
- questions explorer to see all
- runs and benchmarks for that topic.
-
+
+
Question hub
+
+ Open a research question to compare every run and
+ benchmark on that topic — or browse the full{" "}
+ questions explorer.
+
+
+
+ All questions
+
+
+ Search artifacts
+
+
+
+
+ {topQuestions.length === 0 ? (
+
+
+ No questions yet. Run debate experiments locally,
+ then open the questions explorer once artifacts are
+ available.
+
+
+ ) : (
+
+
+ Popular research topics
+
+
+ Questions with the most runs and benchmarks in the
+ artifact store.
+
+
(
+
+
+
+ ),
+ },
+ { key: "runCount", label: "Runs" },
+ { key: "benchmarkCount", label: "Benchmarks" },
+ {
+ key: "totalExperiments",
+ label: "Total",
+ },
+ {
+ key: "latestCreatedAt",
+ label: "Latest",
+ hideOnMobile: true,
+ render: (row) =>
+ new Date(
+ (
+ row as {
+ latestCreatedAt: string;
+ }
+ ).latestCreatedAt,
+ ).toLocaleString(),
+ },
+ ]}
+ data={topQuestions}
+ getRowId={(row) =>
+ (row as { question: string }).question
+ }
+ renderCardActions={(row) => (
+
+ Open hub
+
+ )}
+ />
+
+ )}
);
}
diff --git a/apps/web/components/InsightFilterCard.tsx b/apps/web/components/InsightFilterCard.tsx
index 2143835..81f2487 100644
--- a/apps/web/components/InsightFilterCard.tsx
+++ b/apps/web/components/InsightFilterCard.tsx
@@ -11,6 +11,7 @@ type InsightFilterCardProps = {
totalRuns: number;
filteredRuns: number;
preserveKeys?: string[];
+ entityLabel?: string;
};
export function InsightFilterCard({
@@ -21,6 +22,7 @@ export function InsightFilterCard({
totalRuns,
filteredRuns,
preserveKeys = [],
+ entityLabel = "runs",
}: InsightFilterCardProps) {
const clearHref = (() => {
const query = new URLSearchParams();
@@ -38,7 +40,7 @@ export function InsightFilterCard({
- {filteredRuns} of {totalRuns} runs
+ {filteredRuns} of {totalRuns} {entityLabel}
>
}
>
diff --git a/apps/web/components/charts/PipelineErrorTrendCharts.tsx b/apps/web/components/charts/PipelineErrorTrendCharts.tsx
new file mode 100644
index 0000000..7eed84e
--- /dev/null
+++ b/apps/web/components/charts/PipelineErrorTrendCharts.tsx
@@ -0,0 +1,154 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import {
+ Bar,
+ BarChart,
+ CartesianGrid,
+ Legend,
+ Line,
+ LineChart,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from "recharts";
+import { InfoTooltip } from "../InfoTooltip";
+import type {
+ PipelineErrorAgentPoint,
+ PipelineErrorTrendPoint,
+} from "../../lib/pipelineErrorTrends";
+
+type PipelineErrorTrendChartsProps = {
+ trendSeries: PipelineErrorTrendPoint[];
+ agentSeries: PipelineErrorAgentPoint[];
+};
+
+export function PipelineErrorTrendCharts({
+ trendSeries,
+ agentSeries,
+}: PipelineErrorTrendChartsProps) {
+ const [mounted, setMounted] = useState(false);
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ if (trendSeries.length === 0 && agentSeries.length === 0) {
+ return null;
+ }
+
+ if (!mounted) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ Errors over time
+
+
+ {trendSeries.length === 0 ? (
+
+ No dated errors in the current filter scope.
+
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ Errors by agent
+
+
+ {agentSeries.length === 0 ? (
+
+ No agent failures in the current filter scope.
+
+ ) : (
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/apps/web/components/charts/QualityTrendCharts.tsx b/apps/web/components/charts/QualityTrendCharts.tsx
new file mode 100644
index 0000000..4fbae61
--- /dev/null
+++ b/apps/web/components/charts/QualityTrendCharts.tsx
@@ -0,0 +1,105 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import {
+ CartesianGrid,
+ Legend,
+ Line,
+ LineChart,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from "recharts";
+import { InfoTooltip } from "../InfoTooltip";
+import type { QualityTrendPoint } from "../../lib/qualityTrends";
+
+type QualityTrendChartsProps = {
+ series: QualityTrendPoint[];
+};
+
+export function QualityTrendCharts({ series }: QualityTrendChartsProps) {
+ const [mounted, setMounted] = useState(false);
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ if (series.length === 0) {
+ return null;
+ }
+
+ if (!mounted) {
+ return ;
+ }
+
+ return (
+
+
+ Judge rubric scores over time
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/lib/glossary.ts b/apps/web/lib/glossary.ts
index 376118c..68eae43 100644
--- a/apps/web/lib/glossary.ts
+++ b/apps/web/lib/glossary.ts
@@ -144,6 +144,10 @@ export const GLOSSARY: Record = {
"Compare key metrics (mode count, entropy, stability) between two benchmarks.",
pipelineTiming:
"Average and median wall-clock duration per agent step, computed from trace timestamps.",
+ pipelineErrors:
+ "Failed agent steps over time — spikes often indicate model outages, parse failures, or timeouts.",
+ qualityTrend:
+ "Judge rubric scores (1–5) across filtered runs in chronological order.",
};
export function getGlossaryEntry(key: string): string | undefined {
diff --git a/apps/web/lib/pairsExplorer.filter.test.ts b/apps/web/lib/pairsExplorer.filter.test.ts
new file mode 100644
index 0000000..65bc228
--- /dev/null
+++ b/apps/web/lib/pairsExplorer.filter.test.ts
@@ -0,0 +1,59 @@
+import { describe, expect, it } from "vitest";
+import { filterPairSummaries } from "./pairsExplorer";
+import type { BenchmarkPairSummaryRow } from "./pairsExplorer";
+
+function makeSummary(
+ overrides: Partial = {},
+): BenchmarkPairSummaryRow {
+ return {
+ benchmarkId: "bench_a",
+ question: "What is policy X?",
+ model: "gpt-test",
+ pipelinePreset: "standard",
+ fastMode: false,
+ createdAt: "2025-06-15T12:00:00.000Z",
+ runCount: 3,
+ pairCount: 2,
+ minSimilarity: 0.7,
+ maxSimilarity: 0.9,
+ avgSimilarity: 0.8,
+ ...overrides,
+ };
+}
+
+describe("filterPairSummaries", () => {
+ const rows = [
+ makeSummary(),
+ makeSummary({
+ benchmarkId: "bench_b",
+ question: "Climate impact study",
+ model: "claude-test",
+ pipelinePreset: "research_deep",
+ fastMode: true,
+ createdAt: "2025-07-01T12:00:00.000Z",
+ }),
+ ];
+
+ it("filters by question text", () => {
+ const filtered = filterPairSummaries(rows, { q: "climate" });
+ expect(filtered).toHaveLength(1);
+ expect(filtered[0].benchmarkId).toBe("bench_b");
+ });
+
+ it("filters by model and preset", () => {
+ expect(filterPairSummaries(rows, { model: "claude" })).toHaveLength(1);
+ expect(filterPairSummaries(rows, { preset: "standard" })).toHaveLength(
+ 1,
+ );
+ });
+
+ it("filters by fast mode and date range", () => {
+ expect(filterPairSummaries(rows, { fast: "true" })).toHaveLength(1);
+ expect(
+ filterPairSummaries(rows, { from: "2025-06-20T00:00:00" }),
+ ).toHaveLength(1);
+ expect(
+ filterPairSummaries(rows, { to: "2025-06-20T00:00:00" }),
+ ).toHaveLength(1);
+ });
+});
diff --git a/apps/web/lib/pairsExplorer.ts b/apps/web/lib/pairsExplorer.ts
index 346b1dc..f7b4b3d 100644
--- a/apps/web/lib/pairsExplorer.ts
+++ b/apps/web/lib/pairsExplorer.ts
@@ -1,4 +1,4 @@
-import type { BenchmarkArtifact } from "./data";
+import type { ArtifactFilterParams, BenchmarkArtifact } from "./data";
import { loadBenchmarkPairsExport } from "./data";
export type BenchmarkPairSummaryRow = {
@@ -6,6 +6,8 @@ export type BenchmarkPairSummaryRow = {
question: string;
model: string;
pipelinePreset: string;
+ fastMode: boolean;
+ createdAt: string;
runCount: number;
pairCount: number;
minSimilarity: number | null;
@@ -13,6 +15,69 @@ export type BenchmarkPairSummaryRow = {
avgSimilarity: number | null;
};
+function normalize(value: string | undefined): string {
+ return (value ?? "").trim().toLowerCase();
+}
+
+function parseDateInput(value: string | undefined): Date | undefined {
+ if (!value) return undefined;
+ const date = new Date(value);
+ if (Number.isNaN(date.getTime())) return undefined;
+ return date;
+}
+
+function parseFastFilter(value: string | undefined): boolean | undefined {
+ const normalized = normalize(value);
+ if (normalized === "true") return true;
+ if (normalized === "false") return false;
+ return undefined;
+}
+
+export function filterPairSummaries(
+ rows: BenchmarkPairSummaryRow[],
+ filters: ArtifactFilterParams,
+): BenchmarkPairSummaryRow[] {
+ const q = normalize(filters.q);
+ const model = normalize(filters.model);
+ const preset = normalize(filters.preset);
+ const fast = parseFastFilter(filters.fast);
+ const fromDate = parseDateInput(filters.from);
+ const toDate = parseDateInput(filters.to);
+
+ return rows.filter((row) => {
+ if (q) {
+ const haystack =
+ `${row.benchmarkId} ${row.question} ${row.model} ${row.pipelinePreset}`.toLowerCase();
+ if (!haystack.includes(q)) return false;
+ }
+ if (model && !row.model.toLowerCase().includes(model)) {
+ return false;
+ }
+ if (preset && row.pipelinePreset.toLowerCase() !== preset) {
+ return false;
+ }
+ if (typeof fast === "boolean" && row.fastMode !== fast) {
+ return false;
+ }
+ const createdAt = new Date(row.createdAt);
+ if (
+ fromDate &&
+ !Number.isNaN(createdAt.getTime()) &&
+ createdAt < fromDate
+ ) {
+ return false;
+ }
+ if (
+ toDate &&
+ !Number.isNaN(createdAt.getTime()) &&
+ createdAt > toDate
+ ) {
+ return false;
+ }
+ return true;
+ });
+}
+
export type BenchmarkPairDetailRow = {
runIdA: string;
runIdB: string;
@@ -67,6 +132,9 @@ export async function buildBenchmarkPairSummaries(
question: benchmark?.question ?? "(unknown question)",
model: benchmark?.metadata.model ?? "—",
pipelinePreset: benchmark?.metadata.pipelinePreset ?? "—",
+ fastMode: benchmark?.metadata.fastMode ?? false,
+ createdAt:
+ benchmark?.metadata.createdAt ?? "1970-01-01T00:00:00.000Z",
runCount: entry.runIds?.length ?? benchmark?.payload.runs ?? 0,
...stats,
});
@@ -84,6 +152,8 @@ export async function buildBenchmarkPairSummaries(
question: benchmark.question,
model: benchmark.metadata.model,
pipelinePreset: benchmark.metadata.pipelinePreset,
+ fastMode: benchmark.metadata.fastMode,
+ createdAt: benchmark.metadata.createdAt,
runCount: runIds.length || benchmark.payload.runs,
...stats,
});
@@ -121,6 +191,8 @@ export async function buildBenchmarkPairDetails(
question: benchmark.question,
model: benchmark.metadata.model,
pipelinePreset: benchmark.metadata.pipelinePreset,
+ fastMode: benchmark.metadata.fastMode,
+ createdAt: benchmark.metadata.createdAt,
runCount: runIds.length || benchmark.payload.runs,
...summarizePairs(rawPairs, runIds),
}
@@ -130,6 +202,8 @@ export async function buildBenchmarkPairDetails(
question: "(unknown question)",
model: "—",
pipelinePreset: "—",
+ fastMode: false,
+ createdAt: "1970-01-01T00:00:00.000Z",
runCount: runIds.length,
...summarizePairs(rawPairs, runIds),
}
diff --git a/apps/web/lib/pipelineErrorTrends.test.ts b/apps/web/lib/pipelineErrorTrends.test.ts
new file mode 100644
index 0000000..5db4ddf
--- /dev/null
+++ b/apps/web/lib/pipelineErrorTrends.test.ts
@@ -0,0 +1,56 @@
+import { describe, expect, it } from "vitest";
+import {
+ buildPipelineErrorByAgent,
+ buildPipelineErrorTrendSeries,
+} from "./pipelineErrorTrends";
+import type { PipelineErrorRow } from "./pipelineErrors";
+
+function makeError(
+ overrides: Partial = {},
+): PipelineErrorRow {
+ return {
+ runId: "run_a",
+ question: "Q",
+ model: "gpt-test",
+ pipelinePreset: "standard",
+ fastMode: false,
+ agentName: "SolverAgent",
+ stepIndex: 1,
+ error: "timeout",
+ createdAt: "2025-06-15T12:00:00.000Z",
+ traceHref: "/runs/run_a#step-1",
+ ...overrides,
+ };
+}
+
+describe("pipeline error trends", () => {
+ it("groups errors by day and counts affected runs", () => {
+ const series = buildPipelineErrorTrendSeries([
+ makeError(),
+ makeError({
+ runId: "run_b",
+ createdAt: "2025-06-15T18:00:00.000Z",
+ }),
+ makeError({
+ runId: "run_c",
+ createdAt: "2025-06-16T12:00:00.000Z",
+ }),
+ ]);
+
+ expect(series).toHaveLength(2);
+ expect(series[0].errorCount).toBe(2);
+ expect(series[0].runCount).toBe(2);
+ expect(series[1].errorCount).toBe(1);
+ });
+
+ it("counts errors by agent", () => {
+ const agents = buildPipelineErrorByAgent([
+ makeError(),
+ makeError({ agentName: "JudgeAgent" }),
+ makeError({ agentName: "JudgeAgent" }),
+ ]);
+
+ expect(agents[0]).toEqual({ agent: "JudgeAgent", errorCount: 2 });
+ expect(agents[1]).toEqual({ agent: "SolverAgent", errorCount: 1 });
+ });
+});
diff --git a/apps/web/lib/pipelineErrorTrends.ts b/apps/web/lib/pipelineErrorTrends.ts
new file mode 100644
index 0000000..6154ede
--- /dev/null
+++ b/apps/web/lib/pipelineErrorTrends.ts
@@ -0,0 +1,76 @@
+import type { PipelineErrorRow } from "./pipelineErrors";
+
+export type PipelineErrorTrendPoint = {
+ label: string;
+ dateKey: string;
+ errorCount: number;
+ runCount: number;
+};
+
+export type PipelineErrorAgentPoint = {
+ agent: string;
+ errorCount: number;
+};
+
+function dayKey(createdAt: string): string | null {
+ const date = new Date(createdAt);
+ if (Number.isNaN(date.getTime())) return null;
+ return date.toISOString().slice(0, 10);
+}
+
+function formatDayLabel(dateKey: string): string {
+ const date = new Date(`${dateKey}T12:00:00.000Z`);
+ if (Number.isNaN(date.getTime())) return dateKey;
+ return date.toLocaleDateString(undefined, {
+ month: "short",
+ day: "numeric",
+ });
+}
+
+export function buildPipelineErrorTrendSeries(
+ rows: PipelineErrorRow[],
+): PipelineErrorTrendPoint[] {
+ const byDay = new Map<
+ string,
+ { errorCount: number; runIds: Set }
+ >();
+
+ for (const row of rows) {
+ const key = dayKey(row.createdAt);
+ if (!key) continue;
+ const bucket = byDay.get(key) ?? {
+ errorCount: 0,
+ runIds: new Set(),
+ };
+ bucket.errorCount += 1;
+ bucket.runIds.add(row.runId);
+ byDay.set(key, bucket);
+ }
+
+ return [...byDay.entries()]
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([dateKey, bucket]) => ({
+ label: formatDayLabel(dateKey),
+ dateKey,
+ errorCount: bucket.errorCount,
+ runCount: bucket.runIds.size,
+ }));
+}
+
+export function buildPipelineErrorByAgent(
+ rows: PipelineErrorRow[],
+): PipelineErrorAgentPoint[] {
+ const counts = new Map();
+ for (const row of rows) {
+ counts.set(row.agentName, (counts.get(row.agentName) ?? 0) + 1);
+ }
+
+ return [...counts.entries()]
+ .map(([agent, errorCount]) => ({ agent, errorCount }))
+ .sort((a, b) => {
+ if (b.errorCount !== a.errorCount) {
+ return b.errorCount - a.errorCount;
+ }
+ return a.agent.localeCompare(b.agent);
+ });
+}
diff --git a/apps/web/lib/qualityTrends.test.ts b/apps/web/lib/qualityTrends.test.ts
new file mode 100644
index 0000000..721ba58
--- /dev/null
+++ b/apps/web/lib/qualityTrends.test.ts
@@ -0,0 +1,55 @@
+import { describe, expect, it } from "vitest";
+import { buildQualityTrendSeries } from "./qualityTrends";
+import type { QualityRunRow } from "./qualityInsights";
+
+describe("buildQualityTrendSeries", () => {
+ it("returns chronologically sorted runs with rubric scores", () => {
+ const rows: QualityRunRow[] = [
+ {
+ id: "run_b",
+ question: "Q",
+ model: "gpt-test",
+ preset: "research_deep",
+ createdAt: "2025-06-20T00:00:00.000Z",
+ coherence: 4,
+ completeness: null,
+ factualRisk: 2,
+ uncertaintyHandling: null,
+ issueCount: 1,
+ traceHref: "/runs/run_b",
+ },
+ {
+ id: "run_a",
+ question: "Q",
+ model: "gpt-test",
+ preset: "research_deep",
+ createdAt: "2025-06-10T00:00:00.000Z",
+ coherence: 3,
+ completeness: 4,
+ factualRisk: null,
+ uncertaintyHandling: 3,
+ issueCount: 0,
+ traceHref: "/runs/run_a",
+ },
+ {
+ id: "run_c",
+ question: "Q",
+ model: "gpt-test",
+ preset: "standard",
+ createdAt: "2025-06-30T00:00:00.000Z",
+ coherence: null,
+ completeness: null,
+ factualRisk: null,
+ uncertaintyHandling: null,
+ issueCount: 0,
+ traceHref: "/runs/run_c",
+ },
+ ];
+
+ const series = buildQualityTrendSeries(rows);
+ expect(series).toHaveLength(2);
+ expect(series[0].label).toContain("run_a");
+ expect(series[1].label).toContain("run_b");
+ expect(series[1].coherence).toBe(4);
+ });
+});
diff --git a/apps/web/lib/qualityTrends.ts b/apps/web/lib/qualityTrends.ts
new file mode 100644
index 0000000..f24e386
--- /dev/null
+++ b/apps/web/lib/qualityTrends.ts
@@ -0,0 +1,41 @@
+import type { QualityRunRow } from "./qualityInsights";
+
+export type QualityTrendPoint = {
+ label: string;
+ createdAt: string;
+ coherence: number | null;
+ completeness: number | null;
+ factualRisk: number | null;
+ uncertaintyHandling: number | null;
+};
+
+function shortLabel(id: string, createdAt: string, index: number): string {
+ const date = new Date(createdAt);
+ const stamp = Number.isNaN(date.getTime())
+ ? `run-${index + 1}`
+ : `${date.getMonth() + 1}/${date.getDate()}`;
+ return `${stamp}-${id.slice(-6)}`;
+}
+
+export function buildQualityTrendSeries(
+ rows: QualityRunRow[],
+): QualityTrendPoint[] {
+ return rows
+ .filter(
+ (row) =>
+ row.coherence != null ||
+ row.completeness != null ||
+ row.factualRisk != null ||
+ row.uncertaintyHandling != null,
+ )
+ .slice()
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt))
+ .map((row, index) => ({
+ label: shortLabel(row.id, row.createdAt, index),
+ createdAt: row.createdAt,
+ coherence: row.coherence,
+ completeness: row.completeness,
+ factualRisk: row.factualRisk,
+ uncertaintyHandling: row.uncertaintyHandling,
+ }));
+}