The all-sessions coverage GET dies: the per-session block is the coverage surface

Extraction-UX close-out interlude (master dispatch 2026-09-24, off UI's
ff1ec7a confirmation): nothing client-side consumes GET /api/graph/coverage
— the sidebar block and the chat-bar ring both read GET /api/chats/[id]'s
coverage block. The dead route (app/api/graph/coverage/route.ts) is
deleted; the manual-extract front door (POST /api/graph/coverage/extract)
stays by design.

No server helper was orphaned — the route was composition-only over
shared seams (chatStore, extractionStatuses, stores, coverageOf). The
sessionCoverage doc + the chats/[id] route comment now point at the
per-session block as THE coverage surface.

Pin (tests/coverage.test.ts): the route file's absence (prompt-overrides'
structural-pin precedent) + the Extract POST's survival — a merge cannot
resurrect the dead surface silently.
This commit is contained in:
amosblomqvist
2026-09-24 15:29:01 +03:00
parent bc1f668e2a
commit 3327309e8b
4 changed files with 22 additions and 47 deletions
+3 -2
View File
@@ -7,8 +7,9 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
// turnInFlight lets a returning client re-attach to a running turn
// (chat-handler write-ahead + registry — see src/server/turn-liveness.ts).
// coverage (extraction-UX redesign, 2026-09-24): the chat view's ring
// meter — one session's read of the same derivation /api/graph/coverage
// serves for all sessions. getChat already 404'd, so the block exists.
// meter + sidebar block — THE coverage surface (the all-sessions GET
// route died with the redesign; the Extract POST stays). getChat
// already 404'd, so the block exists.
return Response.json({ ...chat, turnInFlight: isTurnRunning(id), coverage: sessionCoverage(id) });
}
-40
View File
@@ -1,40 +0,0 @@
/**
* 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 });
}
+5 -4
View File
@@ -114,10 +114,11 @@ export async function deleteChatSession(chatId: string, mode: ChatDeleteMode): P
}
/** One session's coverage read (extraction-UX redesign, owner dispatch
* 2026-09-24): the chat view's ring meter + popover data — the same
* derivation the all-sessions list serves (/api/graph/coverage), scoped
* to one chat. "Uncovered" stays DERIVED per request (fingerprint the
* transcript, diff the coverage ledger — never dual-tracked).
* 2026-09-24): the chat view's ring meter + popover data, scoped to one
* chat (the all-sessions GET died with this redesign — nothing consumed
* it once the sidebar and ring read here; the Extract POST stays).
* Uncovered stays DERIVED per request (fingerprint the transcript,
* diff the coverage ledger — never dual-tracked).
*
* lastFoldedAt: the newest coverage row's fold timestamp — a cheap single
* pass over the chat's ledger rows (the ring popover's "last extraction"
+14 -1
View File
@@ -1,5 +1,6 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as fs from "node:fs";
import type { UIMessage } from "ai";
import {
@@ -156,4 +157,16 @@ describe("batchTag — deterministic, ref-stable, mutation-sensitive", () => {
const mutated = coverageSlots([user("m1", "aaaa!"), user("m2", "bbbb")]);
assert.notEqual(batchTag(slots), batchTag(mutated));
});
});
});
describe("the all-sessions coverage GET is dead (extraction-UX close-out, 2026-09-24)", () => {
it("the route file is gone — the only coverage endpoint is the Extract POST", () => {
// UI confirmation (ff1ec7a): nothing client-side consumes the
// all-sessions list anymore — the sidebar block and the ring both
// read GET /api/chats/[id]'s coverage block. This pin guards against
// a merge resurrecting the dead surface.
assert.equal(fs.existsSync("app/api/graph/coverage/route.ts"), false, "the all-sessions GET route is deleted");
// stays by design: the manual-extract front door (sidebar block's
// Extract affordance) lives on.
assert.equal(fs.existsSync("app/api/graph/coverage/extract/route.ts"), true, "the Extract POST survives");
});
});