Cost channel cull: the cluster-naming fold dies — two stages, two channels
Off UI's d651f93 flag (displayCostTotals already isolates the display to extraction + audit; the naming stage row and naming runs were filtered out client-side). This culls the server side of the same ruling. THE STANDING RULING, REVERSED BY ITS OWN PREMISE: NamingRunUsage / readNamingRuns / appendNamingUsage were kept at the wave-7 namer kill ONLY because cost-summary's HISTORICAL naming stage read them. With the fold culled, that premise dies — so the whole ledger read path goes too, following the wave-7 embeddings precedent chain: consumer-less ledger seams are deleted, and the on-disk naming-runs.jsonl stays INERT (real trees keep their historical spend bytes; they are simply not displayed anywhere — the same treatment as historical stages.embedding spend). - core/cost-summary.ts: two channels in (chat + notes front doors), two stages out; RecentRun.kind collapses to "extraction" (labels field gone); CostSummaryInput.naming gone; totalDollars = extraction + audit. - ports/store/contracts/config: appendNamingUsage + readNamingRuns ports, store impls + guards, the NamingRunUsage interface, and LEDGER_FILES.namingRuns die. graphRevision no longer counts the file (a smaller revision value is harmless — reads never compare). - components/graph-view.tsx (mechanical, flagged): the defensive .filter(run.kind !== "naming") guards a kind that no longer exists. - Tests: cost-summary's both-channels fold pins; the naming fixtures and the appendNamingUsage store pin die with the seams.
This commit is contained in:
@@ -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. */
|
||||
|
||||
+14
-35
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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). */
|
||||
|
||||
+5
-6
@@ -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 {
|
||||
|
||||
@@ -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<T>(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. */
|
||||
|
||||
Reference in New Issue
Block a user