Files
learnharness/app/api/graph/coverage/route.ts
T

40 lines
1.6 KiB
TypeScript
Raw Normal View History

/**
* GET /api/graph/coverage — the exact extraction coverage of every session
* (vision-extraction-coverage.md, settled 2026-09-15): which sessions have
* unextracted content, how much (tokens + message counts), and which runs
* are in flight. "Uncovered" is DERIVED here per request — fingerprint the
* current transcript, diff against the coverage ledger — never tracked as
* a second ledger. This is the data behind the graph view's coverage panel
* and its per-session "extract what's missing" buttons.
*/
import type { UIMessage } from "ai";
import { chatStore, extractionStatuses, stores } from "@/src/server";
import { coverageOf } from "@/src/core";
export const dynamic = "force-dynamic";
export async function GET() {
const running = new Set(
extractionStatuses().filter((run) => run.kind === "chat").map((run) => run.chatId),
);
const entries = stores.graphStore.readCoverage();
const chats = chatStore.listChats().map((summary) => {
const record = chatStore.getChat(summary.id);
const messages = (record?.messages ?? []) as UIMessage[];
const cov = coverageOf(messages, entries, summary.id);
return {
chatId: summary.id,
title: summary.title,
updatedAt: summary.updatedAt,
totalMessages: cov.slots.length,
coveredMessages: cov.coveredCount,
uncoveredMessages: cov.uncoveredCount,
coveredTokens: cov.coveredTokens,
uncoveredTokens: cov.uncoveredTokens,
running: running.has(summary.id),
};
});
// Most uncovered first — the actionable sessions lead the panel.
chats.sort((a, b) => b.uncoveredTokens - a.uncoveredTokens || (a.updatedAt < b.updatedAt ? 1 : -1));
return Response.json({ chats });
}