diff --git a/components/graph-view.tsx b/components/graph-view.tsx index 16ab806..10616fe 100644 --- a/components/graph-view.tsx +++ b/components/graph-view.tsx @@ -298,6 +298,8 @@ function fmtRunCost(dollars: number | null): string { * titles / file names — so the core's naming fold (and its historical * spend) is deliberately not shown anywhere: the pill and the panel must * agree, and both read THIS, never GraphCostSummary.totalDollars. + * (The core fold's naming stage was then culled outright — 2026-09-24 — + * so the recent-runs list carries extraction runs only.) */ export function displayCostTotals(costs: Pick): { totalDollars: number; @@ -393,9 +395,7 @@ function GraphCostPanel({ costs }: { costs: GraphCostSummary }) { ))}
- {costs.runs - .filter((run) => run.kind !== "naming") - .map((run) => ( + {costs.runs.map((run) => (
{timeAgo(run.startedAt)}{" "} diff --git a/config.ts b/config.ts index adaa46e..e971b24 100644 --- a/config.ts +++ b/config.ts @@ -73,9 +73,6 @@ export const LEDGER_FILES = { * the checkpoint's replacement for chat extractions — notes keep * sessions.jsonl, whole-file as they are). */ coverage: path.join(PATHS.graph, "coverage.jsonl"), - /** Cluster-naming run usage (contracts/graph.ts NamingRunUsage) — the - * money channel's third stage; graph-global, so its own ledger. */ - namingRuns: path.join(PATHS.graph, "naming-runs.jsonl"), } as const; /** diff --git a/src/core/contracts/graph.ts b/src/core/contracts/graph.ts index 9ded9dd..6c7b945 100644 --- a/src/core/contracts/graph.ts +++ b/src/core/contracts/graph.ts @@ -79,27 +79,6 @@ export interface NamingRunStats { appended: number; } -/** One cluster-naming run's usage record — the money channel's THIRD stage - * (graph cost metrics, 2026-09). The namer is graph-global (tied to no - * chat or source), so runs live in their own append-only ledger - * (config.LEDGER_FILES.namingRuns). Appended when a run actually called - * the model; skipped refreshes (nothing to name) never bill and never - * append. Idempotent per startedAt — same discipline as every usage - * channel. Cost is INFERRED at read time from steps + model - * (core/prices.ts) — dollars are never stored. */ -export interface NamingRunUsage { - /** ms epoch — the idempotency key. */ - startedAt: number; - /** ISO — wall-clock time of the run. */ - at: string; - durationMs: number; - /** The naming run's labeler role model id (the role died in wave 7; - * the field stays as the historical record's shape). */ - model: string; - /** Every billed call (usually ONE — the namer is single-submit). */ - steps: CallUsage[]; - stats: NamingRunStats; -} export interface GraphNode { /** Opaque stable id, store-assigned: "n_" + nanoid-like. */ diff --git a/src/core/cost-summary.ts b/src/core/cost-summary.ts index e08aa98..641acc2 100644 --- a/src/core/cost-summary.ts +++ b/src/core/cost-summary.ts @@ -1,10 +1,12 @@ /** * Graph-pipeline cost summary (2026-09 graph cost metrics wave) — the pure - * fold behind the graph view's cost pill. Three usage channels in: chat - * extraction runs (ChatRecord.extractions), notes-source runs - * (NotesSource.extractions), and cluster-naming runs (naming-runs.jsonl). - * Out: per-stage totals (extraction / audit / cluster-naming) + a capped - * recent-runs list. + * fold behind the graph view's cost pill. Two usage channels in: chat + * extraction runs (ChatRecord.extractions) and notes-source runs + * (NotesSource.extractions). Out: per-stage totals (extraction / audit) + * + a capped recent-runs list. (The cluster-naming channel died with the + * extraction-UX close-out, 2026-09-24 — UI's displayCostTotals had already + * dropped it; historical naming-runs.jsonl spend is simply not displayed, + * the same ruling as wave 7's stages.embedding.) * * MONEY CHANNEL LAW: dollars are INFERRED here at read time via * core/prices.ts — never stored anywhere. An unpriced model contributes @@ -15,7 +17,7 @@ * barrel so the pill/panel components carry no cost math. */ -import type { CallUsage, EmissionStats, ExtractionRunUsage, NamingRunUsage } from "./contracts.ts"; +import type { CallUsage, EmissionStats, ExtractionRunUsage } from "./contracts.ts"; import { callCost, sumSteps } from "./prices.ts"; /** One pipeline stage's cumulative cost across every run. */ @@ -37,7 +39,7 @@ export interface RecentRun { at: string; durationMs: number; /** "extraction" covers both front doors (chat + notes). */ - kind: "extraction" | "naming"; + kind: "extraction"; /** Chat title / source file path / fixed label for naming runs. */ title: string; /** Whole-run inferred USD; null only when EVERY stage is unpriced. */ @@ -45,8 +47,6 @@ export interface RecentRun { tokens: CallUsage; /** What the run merged into the graph; absent on failed runs. */ merged?: EmissionStats; - /** Naming runs: labeling records appended (extraction runs omit it). */ - labels?: number; } /** The cost pill's whole payload: stage totals + recent runs. */ @@ -56,13 +56,12 @@ export interface GraphCostSummary { /** Runs where EVERY stage was unpriced (totalDollars excludes them). */ unpricedRuns: number; /** The billable pipeline stages. (Wave 7, owner ruling 2026-09-23: - * back to three — the embedding stage is gone with the provider - * embedder; historical records' stages.embedding spend is simply not - * displayed.) */ + * the embedding stage is gone with the provider embedder; the + * 2026-09-24 close-out culled cluster naming the same way — + * historical spend on either is simply not displayed.) */ stages: { extraction: StageCost; audit: StageCost; - naming: StageCost; }; /** Newest first, capped at `recentCap` (default 10). */ runs: RecentRun[]; @@ -74,8 +73,6 @@ export interface CostSummaryInput { chats: { title: string; extractions: ExtractionRunUsage[] }[]; /** Notes-front-door runs: imported file path + the source's usage log. */ sources: { filePath: string; extractions: ExtractionRunUsage[] }[]; - /** Cluster-naming runs (graph-global ledger). */ - naming: NamingRunUsage[]; recentCap?: number; } @@ -116,7 +113,6 @@ function mergeTokens(into: CallUsage, steps: readonly CallUsage[]): void { export function graphCostSummary(input: CostSummaryInput): GraphCostSummary { const extraction = emptyStage(); const audit = emptyStage(); - const naming = emptyStage(); const runs: RecentRun[] = []; const foldExtractionRun = (run: ExtractionRunUsage, title: string) => { @@ -158,23 +154,6 @@ export function graphCostSummary(input: CostSummaryInput): GraphCostSummary { for (const source of input.sources) { for (const run of source.extractions) foldExtractionRun(run, source.filePath); } - for (const run of input.naming) { - naming.runs++; - mergeTokens(naming.tokens, run.steps); - const dollars = stageDollars(run.model, run.steps); - if (dollars === null) naming.unpricedRuns++; - else naming.dollars += dollars; - runs.push({ - startedAt: run.startedAt, - at: run.at, - durationMs: run.durationMs, - kind: "naming", - title: "Cluster naming", - dollars, - tokens: sumSteps(run.steps), - labels: run.stats.appended, - }); - } runs.sort((a, b) => b.startedAt - a.startedAt); const capped = runs.slice(0, input.recentCap ?? 10); @@ -182,9 +161,9 @@ export function graphCostSummary(input: CostSummaryInput): GraphCostSummary { // A run is "unpriced" when it billed nothing countable at all. const unpricedRuns = runs.filter((r) => r.dollars === null).length; return { - totalDollars: extraction.dollars + audit.dollars + naming.dollars, + totalDollars: extraction.dollars + audit.dollars, unpricedRuns, - stages: { extraction, audit, naming }, + stages: { extraction, audit }, runs: capped, }; } diff --git a/src/core/ports.ts b/src/core/ports.ts index c34b588..722d4ba 100644 --- a/src/core/ports.ts +++ b/src/core/ports.ts @@ -27,7 +27,6 @@ import type { SessionCheckpoint, TurnUsage, NotesSource, - NamingRunUsage, } from "./contracts.ts"; import type { UserSettings } from "./settings.ts"; @@ -102,8 +101,6 @@ export interface GraphStore { /** Append one cluster-naming run's usage (graph-global money channel) — * idempotent per startedAt. */ - appendNamingUsage(entry: NamingRunUsage): void; - readNamingRuns(): NamingRunUsage[]; /** Upsert one imported-notes source record (config.SOURCES_FILE — a * JSON map, not a ledger; an upsert keeps the existing usage log). */ diff --git a/src/server/graph.ts b/src/server/graph.ts index a2051e2..0ce4783 100644 --- a/src/server/graph.ts +++ b/src/server/graph.ts @@ -42,14 +42,14 @@ export function graphCosts(): GraphCostSummary { .readSources() .filter((s) => s.extractions?.length) .map((s) => ({ filePath: s.filePath, extractions: s.extractions! })); - return graphCostSummary({ chats, sources, naming: stores.graphStore.readNamingRuns() }); + return graphCostSummary({ chats, sources }); } /** The graph's version stamp (session-deletion wave 6): the max mtime - * across every graph data file — ledgers, positions, embeddings. Moves - * on ANY graph write (merge append, settle, embed, or a deletion's - * atomic rewrite), never moves on reads, and needs no bookkeeping of - * its own: the fs IS the counter. Consumers poll it (the activity feed + * across every graph data file — ledgers, positions. Moves on ANY + * graph write (merge append, settle, or a deletion's atomic rewrite), + * never moves on reads, and needs no bookkeeping of its own: the fs + * IS the counter. Consumers poll it (the activity feed * carries it; the DELETE response returns it) to know clusters/layout * must refetch — the "graph changed underneath me" signal. */ export function graphRevision(): number { @@ -61,7 +61,6 @@ export function graphRevision(): number { LEDGER_FILES.mappings, LEDGER_FILES.sessions, LEDGER_FILES.coverage, - LEDGER_FILES.namingRuns, POSITIONS_FILE, ]) { try { diff --git a/src/store/graph.ts b/src/store/graph.ts index f4f89b8..362c57f 100644 --- a/src/store/graph.ts +++ b/src/store/graph.ts @@ -42,7 +42,6 @@ import type { ProvenancePoint, CoverageEntry, SessionCheckpoint, - NamingRunUsage, } from "../core/contracts.ts"; import type { GraphStore } from "../core/ports.ts"; @@ -89,18 +88,6 @@ function readValid(file: string, isValid: (v: unknown) => v is T): T[] { const isProvenancePoint = (v: unknown): v is ProvenancePoint => isRecord(v) && isString(v.chatId) && isString(v.messageId); -const isNamingStats = (v: unknown): v is NamingRunUsage["stats"] => - isRecord(v) && - ["detected", "claimed", "named", "appended"].every((k) => typeof v[k] === "number"); - -const isNamingRun = (v: unknown): v is NamingRunUsage => - isRecord(v) && - typeof v.startedAt === "number" && - isString(v.at) && - typeof v.durationMs === "number" && - isString(v.model) && - Array.isArray(v.steps) && - isNamingStats(v.stats); const isGraphNode = (v: unknown): v is GraphNode => isRecord(v) && @@ -190,18 +177,6 @@ export const graphStore: GraphStore = { return readValid(LEDGER_FILES.coverage, isCoverageEntry); }, - // ── cluster-naming run usage (config.LEDGER_FILES.namingRuns) ── - - /** Idempotent per startedAt — the same discipline as every usage - * channel (chats.appendExtractionUsage, appendSourceUsage). */ - appendNamingUsage(entry: NamingRunUsage): void { - if (this.readNamingRuns().some((r) => r.startedAt === entry.startedAt)) return; - appendLine(LEDGER_FILES.namingRuns, entry); - }, - readNamingRuns(): NamingRunUsage[] { - return readValid(LEDGER_FILES.namingRuns, isNamingRun); - }, - // ── imported-notes bookkeeping (config.SOURCES_FILE — a JSON map) ── /** Upsert: keeps the existing record's usage log when re-importing. */ diff --git a/tests/cost-summary.test.ts b/tests/cost-summary.test.ts index feb097b..6fa98fb 100644 --- a/tests/cost-summary.test.ts +++ b/tests/cost-summary.test.ts @@ -12,7 +12,7 @@ import { type CostSummaryInput, type GraphCostSummary, } from "../src/core/cost-summary.ts"; -import type { CallUsage, ExtractionRunUsage, NamingRunUsage } from "../src/core/contracts.ts"; +import type { CallUsage, ExtractionRunUsage } from "../src/core/contracts.ts"; import { PRICES, callCost } from "../src/core/prices.ts"; /** A model with full published pricing (from the generated registry). */ @@ -43,40 +43,30 @@ const extractionRun = ( ...(stats ? { stats } : {}), }); -const namingRun = (startedAt: number, model: string, appended: number): NamingRunUsage => ({ - startedAt, - at: new Date(startedAt).toISOString(), - durationMs: 4_000, - model, - steps: [step(500, 50)], - stats: { detected: 8, claimed: 6, named: 2, appended }, -}); - describe("graphCostSummary (the cost pill's fold)", () => { - test("folds all three channels: stage totals, tokens, newest-first runs", () => { + test("folds both channels: stage totals, tokens, newest-first runs", () => { + // (The cluster-naming channel died with the extraction-UX close-out, + // 2026-09-24 — two channels: chat + notes front doors.) const input: CostSummaryInput = { chats: [{ title: "Testing session", extractions: [extractionRun(100, PRICED, PRICED)] }], sources: [{ filePath: "/vault/modal.md", extractions: [extractionRun(200, PRICED, PRICED)] }], - naming: [namingRun(300, PRICED, 2)], }; const s = graphCostSummary(input); const extStep = callCost(step(1000, 200), PRICED)!; const audStep = callCost(step(2000, 100), PRICED)!; assert.equal(s.stages.extraction.dollars, extStep * 2); assert.equal(s.stages.audit.dollars, audStep * 2); - assert.equal(s.stages.naming.dollars, callCost(step(500, 50), PRICED)); assert.equal(s.stages.extraction.runs, 2); - assert.equal(s.stages.naming.runs, 1); assert.equal(s.stages.extraction.tokens.inputTokens, 2000); assert.equal(s.stages.audit.tokens.outputTokens, 200); - assert.equal(s.totalDollars, extStep * 2 + audStep * 2 + callCost(step(500, 50), PRICED)!); - // All three runs, newest first, titles joined per front door. + assert.equal(s.totalDollars, extStep * 2 + audStep * 2); + // Both runs, newest first, titles joined per front door — every + // run is kind "extraction" now. assert.deepEqual( s.runs.map((r) => r.title), - ["Cluster naming", "/vault/modal.md", "Testing session"], + ["/vault/modal.md", "Testing session"], ); - assert.equal(s.runs[0]!.labels, 2); - assert.equal(s.runs[0]!.kind, "naming"); + assert.deepEqual(s.runs.map((r) => r.kind), ["extraction", "extraction"]); assert.equal(s.unpricedRuns, 0); }); @@ -84,7 +74,6 @@ describe("graphCostSummary (the cost pill's fold)", () => { const s: GraphCostSummary = graphCostSummary({ chats: [{ title: "partial", extractions: [extractionRun(100, PRICED, "unknown-model")] }], sources: [{ filePath: "/vault/x.md", extractions: [extractionRun(200, "unknown-model", "unknown-model")] }], - naming: [], }); assert.equal(s.stages.audit.unpricedRuns, 2); assert.equal(s.stages.audit.dollars, 0); // never a silent zero: unpricedRuns says it @@ -101,7 +90,6 @@ describe("graphCostSummary (the cost pill's fold)", () => { const s = graphCostSummary({ chats: [{ title: "chat", extractions }], sources: [], - naming: [], recentCap: 2, }); assert.equal(s.runs.length, 2); @@ -114,13 +102,11 @@ describe("graphCostSummary (the cost pill's fold)", () => { const s = graphCostSummary({ chats: [{ title: "failed", extractions: [extractionRun(100, PRICED, PRICED)] }], sources: [], - naming: [], }); assert.equal(s.runs[0]!.merged, undefined); - const empty = graphCostSummary({ chats: [], sources: [], naming: [] }); + const empty = graphCostSummary({ chats: [], sources: [] }); assert.equal(empty.totalDollars, 0); assert.equal(empty.runs.length, 0); - assert.equal(empty.stages.naming.runs, 0); assert.equal(empty.stages.extraction.tokens.inputTokens, undefined); }); }); diff --git a/tests/extraction.test.ts b/tests/extraction.test.ts index 0b8e59b..215c2ac 100644 --- a/tests/extraction.test.ts +++ b/tests/extraction.test.ts @@ -32,7 +32,6 @@ import type { GraphEdge, GraphNode, LedgerEvent, - NamingRunUsage, NodeMapping, NotesSource, SessionCheckpoint, @@ -68,7 +67,6 @@ function memoryLedgers() { const checkpoints: SessionCheckpoint[] = []; const coverage: CoverageEntry[] = []; const sources = new Map(); - const namingRuns: NamingRunUsage[] = []; const positions = new Map(); return { appendNode: (n: GraphNode) => void nodes.push(n), @@ -99,11 +97,6 @@ function memoryLedgers() { } }, // Cluster labelings — mirrors the real store: append dedupes by id. - // Cluster-naming run usage — mirrors the real store: dedupe per startedAt. - appendNamingUsage: (entry: NamingRunUsage) => { - if (!namingRuns.some((r) => r.startedAt === entry.startedAt)) namingRuns.push(entry); - }, - readNamingRuns: () => namingRuns.map((r) => ({ ...r, steps: [...r.steps], stats: { ...r.stats } })), // The layout ledger — settleGraphLayout writes it after each merge. readPositions: () => Object.fromEntries([...positions].map(([id, p]) => [id, { ...p }])), writePositions: (next) => { @@ -422,7 +415,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => { // Wave 7 (950ddfd): birth-session clusters carry session-name labels, // so the post-merge naming garnish is gone — no model spend, no // naming-run records, no label writes. - assert.equal(ledgers.readNamingRuns().length, 0, "no naming runs minted"); }); diff --git a/tests/graph-tools.test.ts b/tests/graph-tools.test.ts index a48efcd..55655f3 100644 --- a/tests/graph-tools.test.ts +++ b/tests/graph-tools.test.ts @@ -60,7 +60,6 @@ function fakeStores( readClusterLabels: () => [], appendClusterLabel: () => {}, appendNamingRun: () => {}, - readNamingRuns: () => [], } as unknown as GraphStore; const chatStore: ChatStore = { listChats: () => diff --git a/tests/graph.test.ts b/tests/graph.test.ts index 7fde5d9..3ceec3e 100644 --- a/tests/graph.test.ts +++ b/tests/graph.test.ts @@ -18,7 +18,7 @@ import assert from "node:assert/strict"; import * as fs from "node:fs"; import { after, describe, it } from "node:test"; import type { UIMessage } from "ai"; -import type { AuditResult, NamingRunUsage } from "../src/core/contracts.ts"; +import type { AuditResult } from "../src/core/contracts.ts"; import type { QuestionAnswerData } from "../src/core/question-core.ts"; import { getGraph, graphSnapshot, mergeFindings, type GraphDeps } from "../src/core/graph.ts"; import { coverageSlots } from "../src/core/coverage.ts"; @@ -573,28 +573,6 @@ describe("graph ledger store", () => { assert.deepEqual(nodes[0]!.provenance, [{ chatId: "chat_c", messageId: "m1" }]); }); - it("appendNamingUsage is idempotent per startedAt and strict-reads foreign shapes", () => { - const run: NamingRunUsage = { - startedAt: 1789800000000, - at: "2026-09-17T00:00:00.000Z", - durationMs: 1234, - model: "z-ai/glm-5.3-flash", - steps: [{ inputTokens: 10, outputTokens: 2, cacheReadTokens: 0, cacheWriteTokens: undefined, reasoningTokens: 0 }], - stats: { detected: 4, claimed: 3, named: 1, appended: 1 }, - }; - graphStore.appendNamingUsage(run); - graphStore.appendNamingUsage(run); // re-append: same startedAt — no-op - // A torn append + a foreign shape (missing stats) — skipped by the guard. - fs.appendFileSync( - LEDGER_FILES.namingRuns, - `{not json}\n${JSON.stringify({ startedAt: 1789800000001, at: "x", durationMs: 1, model: "m", steps: [] })}\n`, - "utf-8", - ); - const runs = graphStore.readNamingRuns().filter((r) => r.startedAt >= 1789800000000); - assert.equal(runs.length, 1, "one record — idempotent append, corrupt/foreign lines skipped"); - assert.deepEqual(runs[0]!.stats, { detected: 4, claimed: 3, named: 1, appended: 1 }); - assert.equal(runs[0]!.steps.length, 1); - }); it("returns RAW node lines — repeated ids (link-appends) are each their own line; getGraph merges", () => { const line = (msgId: string) => diff --git a/tests/session-clusters.test.ts b/tests/session-clusters.test.ts index b1c2e2e..50e952b 100644 --- a/tests/session-clusters.test.ts +++ b/tests/session-clusters.test.ts @@ -54,7 +54,6 @@ function fakeStores( (over.sources ?? []).map((s) => ({ ...s, extractions: [] }) as unknown as NotesSource), readPositions: () => ({}), writePositions: () => {}, - readNamingRuns: () => [], } as unknown as GraphStore; const chatStore: ChatStore = { listChats: () =>