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
|
// turnInFlight lets a returning client re-attach to a running turn
|
||||||
// (chat-handler write-ahead + registry — see src/server/turn-liveness.ts).
|
// (chat-handler write-ahead + registry — see src/server/turn-liveness.ts).
|
||||||
// coverage (extraction-UX redesign, 2026-09-24): the chat view's ring
|
// coverage (extraction-UX redesign, 2026-09-24): the chat view's ring
|
||||||
// meter — one session's read of the same derivation /api/graph/coverage
|
// meter + sidebar block — THE coverage surface (the all-sessions GET
|
||||||
// serves for all sessions. getChat already 404'd, so the block exists.
|
// 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) });
|
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
|
/** One session's coverage read (extraction-UX redesign, owner dispatch
|
||||||
* 2026-09-24): the chat view's ring meter + popover data — the same
|
* 2026-09-24): the chat view's ring meter + popover data, scoped to one
|
||||||
* derivation the all-sessions list serves (/api/graph/coverage), scoped
|
* chat (the all-sessions GET died with this redesign — nothing consumed
|
||||||
* to one chat. "Uncovered" stays DERIVED per request (fingerprint the
|
* it once the sidebar and ring read here; the Extract POST stays).
|
||||||
* transcript, diff the coverage ledger — never dual-tracked).
|
* 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
|
* lastFoldedAt: the newest coverage row's fold timestamp — a cheap single
|
||||||
* pass over the chat's ledger rows (the ring popover's "last extraction"
|
* 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 assert from "node:assert/strict";
|
||||||
import { describe, it } from "node:test";
|
import { describe, it } from "node:test";
|
||||||
|
import * as fs from "node:fs";
|
||||||
import type { UIMessage } from "ai";
|
import type { UIMessage } from "ai";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -156,4 +157,16 @@ describe("batchTag — deterministic, ref-stable, mutation-sensitive", () => {
|
|||||||
const mutated = coverageSlots([user("m1", "aaaa!"), user("m2", "bbbb")]);
|
const mutated = coverageSlots([user("m1", "aaaa!"), user("m2", "bbbb")]);
|
||||||
assert.notEqual(batchTag(slots), batchTag(mutated));
|
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