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:
@@ -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) });
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user