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:
amosblomqvist
2026-09-24 19:33:35 +03:00
parent 2cba283009
commit 73027f49bf
12 changed files with 33 additions and 153 deletions
-21
View File
@@ -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
View File
@@ -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,
};
}
-3
View File
@@ -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
View File
@@ -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 {
-25
View File
@@ -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. */