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:
@@ -298,6 +298,8 @@ function fmtRunCost(dollars: number | null): string {
|
|||||||
* titles / file names — so the core's naming fold (and its historical
|
* titles / file names — so the core's naming fold (and its historical
|
||||||
* spend) is deliberately not shown anywhere: the pill and the panel must
|
* spend) is deliberately not shown anywhere: the pill and the panel must
|
||||||
* agree, and both read THIS, never GraphCostSummary.totalDollars.
|
* 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<GraphCostSummary, "stages">): {
|
export function displayCostTotals(costs: Pick<GraphCostSummary, "stages">): {
|
||||||
totalDollars: number;
|
totalDollars: number;
|
||||||
@@ -393,9 +395,7 @@ function GraphCostPanel({ costs }: { costs: GraphCostSummary }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex flex-col gap-0.5 border-t pt-2">
|
<div className="mt-2 flex flex-col gap-0.5 border-t pt-2">
|
||||||
{costs.runs
|
{costs.runs.map((run) => (
|
||||||
.filter((run) => run.kind !== "naming")
|
|
||||||
.map((run) => (
|
|
||||||
<div key={run.startedAt} className="flex items-baseline justify-between gap-2 text-[11px]">
|
<div key={run.startedAt} className="flex items-baseline justify-between gap-2 text-[11px]">
|
||||||
<span className="min-w-0 flex-1 truncate text-muted-foreground">
|
<span className="min-w-0 flex-1 truncate text-muted-foreground">
|
||||||
<span className="text-foreground/70">{timeAgo(run.startedAt)}</span>{" "}
|
<span className="text-foreground/70">{timeAgo(run.startedAt)}</span>{" "}
|
||||||
|
|||||||
@@ -73,9 +73,6 @@ export const LEDGER_FILES = {
|
|||||||
* the checkpoint's replacement for chat extractions — notes keep
|
* the checkpoint's replacement for chat extractions — notes keep
|
||||||
* sessions.jsonl, whole-file as they are). */
|
* sessions.jsonl, whole-file as they are). */
|
||||||
coverage: path.join(PATHS.graph, "coverage.jsonl"),
|
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;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -79,27 +79,6 @@ export interface NamingRunStats {
|
|||||||
appended: number;
|
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 {
|
export interface GraphNode {
|
||||||
/** Opaque stable id, store-assigned: "n_" + nanoid-like. */
|
/** 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
|
* 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
|
* fold behind the graph view's cost pill. Two usage channels in: chat
|
||||||
* extraction runs (ChatRecord.extractions), notes-source runs
|
* extraction runs (ChatRecord.extractions) and notes-source runs
|
||||||
* (NotesSource.extractions), and cluster-naming runs (naming-runs.jsonl).
|
* (NotesSource.extractions). Out: per-stage totals (extraction / audit)
|
||||||
* Out: per-stage totals (extraction / audit / cluster-naming) + a capped
|
* + a capped recent-runs list. (The cluster-naming channel died with the
|
||||||
* recent-runs list.
|
* 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
|
* MONEY CHANNEL LAW: dollars are INFERRED here at read time via
|
||||||
* core/prices.ts — never stored anywhere. An unpriced model contributes
|
* core/prices.ts — never stored anywhere. An unpriced model contributes
|
||||||
@@ -15,7 +17,7 @@
|
|||||||
* barrel so the pill/panel components carry no cost math.
|
* 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";
|
import { callCost, sumSteps } from "./prices.ts";
|
||||||
|
|
||||||
/** One pipeline stage's cumulative cost across every run. */
|
/** One pipeline stage's cumulative cost across every run. */
|
||||||
@@ -37,7 +39,7 @@ export interface RecentRun {
|
|||||||
at: string;
|
at: string;
|
||||||
durationMs: number;
|
durationMs: number;
|
||||||
/** "extraction" covers both front doors (chat + notes). */
|
/** "extraction" covers both front doors (chat + notes). */
|
||||||
kind: "extraction" | "naming";
|
kind: "extraction";
|
||||||
/** Chat title / source file path / fixed label for naming runs. */
|
/** Chat title / source file path / fixed label for naming runs. */
|
||||||
title: string;
|
title: string;
|
||||||
/** Whole-run inferred USD; null only when EVERY stage is unpriced. */
|
/** Whole-run inferred USD; null only when EVERY stage is unpriced. */
|
||||||
@@ -45,8 +47,6 @@ export interface RecentRun {
|
|||||||
tokens: CallUsage;
|
tokens: CallUsage;
|
||||||
/** What the run merged into the graph; absent on failed runs. */
|
/** What the run merged into the graph; absent on failed runs. */
|
||||||
merged?: EmissionStats;
|
merged?: EmissionStats;
|
||||||
/** Naming runs: labeling records appended (extraction runs omit it). */
|
|
||||||
labels?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The cost pill's whole payload: stage totals + recent runs. */
|
/** 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). */
|
/** Runs where EVERY stage was unpriced (totalDollars excludes them). */
|
||||||
unpricedRuns: number;
|
unpricedRuns: number;
|
||||||
/** The billable pipeline stages. (Wave 7, owner ruling 2026-09-23:
|
/** The billable pipeline stages. (Wave 7, owner ruling 2026-09-23:
|
||||||
* back to three — the embedding stage is gone with the provider
|
* the embedding stage is gone with the provider embedder; the
|
||||||
* embedder; historical records' stages.embedding spend is simply not
|
* 2026-09-24 close-out culled cluster naming the same way —
|
||||||
* displayed.) */
|
* historical spend on either is simply not displayed.) */
|
||||||
stages: {
|
stages: {
|
||||||
extraction: StageCost;
|
extraction: StageCost;
|
||||||
audit: StageCost;
|
audit: StageCost;
|
||||||
naming: StageCost;
|
|
||||||
};
|
};
|
||||||
/** Newest first, capped at `recentCap` (default 10). */
|
/** Newest first, capped at `recentCap` (default 10). */
|
||||||
runs: RecentRun[];
|
runs: RecentRun[];
|
||||||
@@ -74,8 +73,6 @@ export interface CostSummaryInput {
|
|||||||
chats: { title: string; extractions: ExtractionRunUsage[] }[];
|
chats: { title: string; extractions: ExtractionRunUsage[] }[];
|
||||||
/** Notes-front-door runs: imported file path + the source's usage log. */
|
/** Notes-front-door runs: imported file path + the source's usage log. */
|
||||||
sources: { filePath: string; extractions: ExtractionRunUsage[] }[];
|
sources: { filePath: string; extractions: ExtractionRunUsage[] }[];
|
||||||
/** Cluster-naming runs (graph-global ledger). */
|
|
||||||
naming: NamingRunUsage[];
|
|
||||||
recentCap?: number;
|
recentCap?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +113,6 @@ function mergeTokens(into: CallUsage, steps: readonly CallUsage[]): void {
|
|||||||
export function graphCostSummary(input: CostSummaryInput): GraphCostSummary {
|
export function graphCostSummary(input: CostSummaryInput): GraphCostSummary {
|
||||||
const extraction = emptyStage();
|
const extraction = emptyStage();
|
||||||
const audit = emptyStage();
|
const audit = emptyStage();
|
||||||
const naming = emptyStage();
|
|
||||||
const runs: RecentRun[] = [];
|
const runs: RecentRun[] = [];
|
||||||
|
|
||||||
const foldExtractionRun = (run: ExtractionRunUsage, title: string) => {
|
const foldExtractionRun = (run: ExtractionRunUsage, title: string) => {
|
||||||
@@ -158,23 +154,6 @@ export function graphCostSummary(input: CostSummaryInput): GraphCostSummary {
|
|||||||
for (const source of input.sources) {
|
for (const source of input.sources) {
|
||||||
for (const run of source.extractions) foldExtractionRun(run, source.filePath);
|
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);
|
runs.sort((a, b) => b.startedAt - a.startedAt);
|
||||||
const capped = runs.slice(0, input.recentCap ?? 10);
|
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.
|
// A run is "unpriced" when it billed nothing countable at all.
|
||||||
const unpricedRuns = runs.filter((r) => r.dollars === null).length;
|
const unpricedRuns = runs.filter((r) => r.dollars === null).length;
|
||||||
return {
|
return {
|
||||||
totalDollars: extraction.dollars + audit.dollars + naming.dollars,
|
totalDollars: extraction.dollars + audit.dollars,
|
||||||
unpricedRuns,
|
unpricedRuns,
|
||||||
stages: { extraction, audit, naming },
|
stages: { extraction, audit },
|
||||||
runs: capped,
|
runs: capped,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import type {
|
|||||||
SessionCheckpoint,
|
SessionCheckpoint,
|
||||||
TurnUsage,
|
TurnUsage,
|
||||||
NotesSource,
|
NotesSource,
|
||||||
NamingRunUsage,
|
|
||||||
} from "./contracts.ts";
|
} from "./contracts.ts";
|
||||||
import type { UserSettings } from "./settings.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) —
|
/** Append one cluster-naming run's usage (graph-global money channel) —
|
||||||
* idempotent per startedAt. */
|
* idempotent per startedAt. */
|
||||||
appendNamingUsage(entry: NamingRunUsage): void;
|
|
||||||
readNamingRuns(): NamingRunUsage[];
|
|
||||||
|
|
||||||
/** Upsert one imported-notes source record (config.SOURCES_FILE — a
|
/** Upsert one imported-notes source record (config.SOURCES_FILE — a
|
||||||
* JSON map, not a ledger; an upsert keeps the existing usage log). */
|
* JSON map, not a ledger; an upsert keeps the existing usage log). */
|
||||||
|
|||||||
+5
-6
@@ -42,14 +42,14 @@ export function graphCosts(): GraphCostSummary {
|
|||||||
.readSources()
|
.readSources()
|
||||||
.filter((s) => s.extractions?.length)
|
.filter((s) => s.extractions?.length)
|
||||||
.map((s) => ({ filePath: s.filePath, extractions: s.extractions! }));
|
.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
|
/** The graph's version stamp (session-deletion wave 6): the max mtime
|
||||||
* across every graph data file — ledgers, positions, embeddings. Moves
|
* across every graph data file — ledgers, positions. Moves on ANY
|
||||||
* on ANY graph write (merge append, settle, embed, or a deletion's
|
* graph write (merge append, settle, or a deletion's atomic rewrite),
|
||||||
* atomic rewrite), never moves on reads, and needs no bookkeeping of
|
* never moves on reads, and needs no bookkeeping of its own: the fs
|
||||||
* its own: the fs IS the counter. Consumers poll it (the activity feed
|
* IS the counter. Consumers poll it (the activity feed
|
||||||
* carries it; the DELETE response returns it) to know clusters/layout
|
* carries it; the DELETE response returns it) to know clusters/layout
|
||||||
* must refetch — the "graph changed underneath me" signal. */
|
* must refetch — the "graph changed underneath me" signal. */
|
||||||
export function graphRevision(): number {
|
export function graphRevision(): number {
|
||||||
@@ -61,7 +61,6 @@ export function graphRevision(): number {
|
|||||||
LEDGER_FILES.mappings,
|
LEDGER_FILES.mappings,
|
||||||
LEDGER_FILES.sessions,
|
LEDGER_FILES.sessions,
|
||||||
LEDGER_FILES.coverage,
|
LEDGER_FILES.coverage,
|
||||||
LEDGER_FILES.namingRuns,
|
|
||||||
POSITIONS_FILE,
|
POSITIONS_FILE,
|
||||||
]) {
|
]) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ import type {
|
|||||||
ProvenancePoint,
|
ProvenancePoint,
|
||||||
CoverageEntry,
|
CoverageEntry,
|
||||||
SessionCheckpoint,
|
SessionCheckpoint,
|
||||||
NamingRunUsage,
|
|
||||||
} from "../core/contracts.ts";
|
} from "../core/contracts.ts";
|
||||||
import type { GraphStore } from "../core/ports.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 =>
|
const isProvenancePoint = (v: unknown): v is ProvenancePoint =>
|
||||||
isRecord(v) && isString(v.chatId) && isString(v.messageId);
|
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 =>
|
const isGraphNode = (v: unknown): v is GraphNode =>
|
||||||
isRecord(v) &&
|
isRecord(v) &&
|
||||||
@@ -190,18 +177,6 @@ export const graphStore: GraphStore = {
|
|||||||
return readValid(LEDGER_FILES.coverage, isCoverageEntry);
|
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) ──
|
// ── imported-notes bookkeeping (config.SOURCES_FILE — a JSON map) ──
|
||||||
|
|
||||||
/** Upsert: keeps the existing record's usage log when re-importing. */
|
/** Upsert: keeps the existing record's usage log when re-importing. */
|
||||||
|
|||||||
+10
-24
@@ -12,7 +12,7 @@ import {
|
|||||||
type CostSummaryInput,
|
type CostSummaryInput,
|
||||||
type GraphCostSummary,
|
type GraphCostSummary,
|
||||||
} from "../src/core/cost-summary.ts";
|
} 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";
|
import { PRICES, callCost } from "../src/core/prices.ts";
|
||||||
|
|
||||||
/** A model with full published pricing (from the generated registry). */
|
/** A model with full published pricing (from the generated registry). */
|
||||||
@@ -43,40 +43,30 @@ const extractionRun = (
|
|||||||
...(stats ? { stats } : {}),
|
...(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)", () => {
|
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 = {
|
const input: CostSummaryInput = {
|
||||||
chats: [{ title: "Testing session", extractions: [extractionRun(100, PRICED, PRICED)] }],
|
chats: [{ title: "Testing session", extractions: [extractionRun(100, PRICED, PRICED)] }],
|
||||||
sources: [{ filePath: "/vault/modal.md", extractions: [extractionRun(200, PRICED, PRICED)] }],
|
sources: [{ filePath: "/vault/modal.md", extractions: [extractionRun(200, PRICED, PRICED)] }],
|
||||||
naming: [namingRun(300, PRICED, 2)],
|
|
||||||
};
|
};
|
||||||
const s = graphCostSummary(input);
|
const s = graphCostSummary(input);
|
||||||
const extStep = callCost(step(1000, 200), PRICED)!;
|
const extStep = callCost(step(1000, 200), PRICED)!;
|
||||||
const audStep = callCost(step(2000, 100), PRICED)!;
|
const audStep = callCost(step(2000, 100), PRICED)!;
|
||||||
assert.equal(s.stages.extraction.dollars, extStep * 2);
|
assert.equal(s.stages.extraction.dollars, extStep * 2);
|
||||||
assert.equal(s.stages.audit.dollars, audStep * 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.extraction.runs, 2);
|
||||||
assert.equal(s.stages.naming.runs, 1);
|
|
||||||
assert.equal(s.stages.extraction.tokens.inputTokens, 2000);
|
assert.equal(s.stages.extraction.tokens.inputTokens, 2000);
|
||||||
assert.equal(s.stages.audit.tokens.outputTokens, 200);
|
assert.equal(s.stages.audit.tokens.outputTokens, 200);
|
||||||
assert.equal(s.totalDollars, extStep * 2 + audStep * 2 + callCost(step(500, 50), PRICED)!);
|
assert.equal(s.totalDollars, extStep * 2 + audStep * 2);
|
||||||
// All three runs, newest first, titles joined per front door.
|
// Both runs, newest first, titles joined per front door — every
|
||||||
|
// run is kind "extraction" now.
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
s.runs.map((r) => r.title),
|
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.deepEqual(s.runs.map((r) => r.kind), ["extraction", "extraction"]);
|
||||||
assert.equal(s.runs[0]!.kind, "naming");
|
|
||||||
assert.equal(s.unpricedRuns, 0);
|
assert.equal(s.unpricedRuns, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,7 +74,6 @@ describe("graphCostSummary (the cost pill's fold)", () => {
|
|||||||
const s: GraphCostSummary = graphCostSummary({
|
const s: GraphCostSummary = graphCostSummary({
|
||||||
chats: [{ title: "partial", extractions: [extractionRun(100, PRICED, "unknown-model")] }],
|
chats: [{ title: "partial", extractions: [extractionRun(100, PRICED, "unknown-model")] }],
|
||||||
sources: [{ filePath: "/vault/x.md", extractions: [extractionRun(200, "unknown-model", "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.unpricedRuns, 2);
|
||||||
assert.equal(s.stages.audit.dollars, 0); // never a silent zero: unpricedRuns says it
|
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({
|
const s = graphCostSummary({
|
||||||
chats: [{ title: "chat", extractions }],
|
chats: [{ title: "chat", extractions }],
|
||||||
sources: [],
|
sources: [],
|
||||||
naming: [],
|
|
||||||
recentCap: 2,
|
recentCap: 2,
|
||||||
});
|
});
|
||||||
assert.equal(s.runs.length, 2);
|
assert.equal(s.runs.length, 2);
|
||||||
@@ -114,13 +102,11 @@ describe("graphCostSummary (the cost pill's fold)", () => {
|
|||||||
const s = graphCostSummary({
|
const s = graphCostSummary({
|
||||||
chats: [{ title: "failed", extractions: [extractionRun(100, PRICED, PRICED)] }],
|
chats: [{ title: "failed", extractions: [extractionRun(100, PRICED, PRICED)] }],
|
||||||
sources: [],
|
sources: [],
|
||||||
naming: [],
|
|
||||||
});
|
});
|
||||||
assert.equal(s.runs[0]!.merged, undefined);
|
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.totalDollars, 0);
|
||||||
assert.equal(empty.runs.length, 0);
|
assert.equal(empty.runs.length, 0);
|
||||||
assert.equal(empty.stages.naming.runs, 0);
|
|
||||||
assert.equal(empty.stages.extraction.tokens.inputTokens, undefined);
|
assert.equal(empty.stages.extraction.tokens.inputTokens, undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import type {
|
|||||||
GraphEdge,
|
GraphEdge,
|
||||||
GraphNode,
|
GraphNode,
|
||||||
LedgerEvent,
|
LedgerEvent,
|
||||||
NamingRunUsage,
|
|
||||||
NodeMapping,
|
NodeMapping,
|
||||||
NotesSource,
|
NotesSource,
|
||||||
SessionCheckpoint,
|
SessionCheckpoint,
|
||||||
@@ -68,7 +67,6 @@ function memoryLedgers() {
|
|||||||
const checkpoints: SessionCheckpoint[] = [];
|
const checkpoints: SessionCheckpoint[] = [];
|
||||||
const coverage: CoverageEntry[] = [];
|
const coverage: CoverageEntry[] = [];
|
||||||
const sources = new Map<string, NotesSource>();
|
const sources = new Map<string, NotesSource>();
|
||||||
const namingRuns: NamingRunUsage[] = [];
|
|
||||||
const positions = new Map<string, { x: number; y: number }>();
|
const positions = new Map<string, { x: number; y: number }>();
|
||||||
return {
|
return {
|
||||||
appendNode: (n: GraphNode) => void nodes.push(n),
|
appendNode: (n: GraphNode) => void nodes.push(n),
|
||||||
@@ -99,11 +97,6 @@ function memoryLedgers() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Cluster labelings — mirrors the real store: append dedupes by id.
|
// 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.
|
// The layout ledger — settleGraphLayout writes it after each merge.
|
||||||
readPositions: () => Object.fromEntries([...positions].map(([id, p]) => [id, { ...p }])),
|
readPositions: () => Object.fromEntries([...positions].map(([id, p]) => [id, { ...p }])),
|
||||||
writePositions: (next) => {
|
writePositions: (next) => {
|
||||||
@@ -422,7 +415,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
|
|||||||
// Wave 7 (950ddfd): birth-session clusters carry session-name labels,
|
// Wave 7 (950ddfd): birth-session clusters carry session-name labels,
|
||||||
// so the post-merge naming garnish is gone — no model spend, no
|
// so the post-merge naming garnish is gone — no model spend, no
|
||||||
// naming-run records, no label writes.
|
// naming-run records, no label writes.
|
||||||
assert.equal(ledgers.readNamingRuns().length, 0, "no naming runs minted");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ function fakeStores(
|
|||||||
readClusterLabels: () => [],
|
readClusterLabels: () => [],
|
||||||
appendClusterLabel: () => {},
|
appendClusterLabel: () => {},
|
||||||
appendNamingRun: () => {},
|
appendNamingRun: () => {},
|
||||||
readNamingRuns: () => [],
|
|
||||||
} as unknown as GraphStore;
|
} as unknown as GraphStore;
|
||||||
const chatStore: ChatStore = {
|
const chatStore: ChatStore = {
|
||||||
listChats: () =>
|
listChats: () =>
|
||||||
|
|||||||
+1
-23
@@ -18,7 +18,7 @@ import assert from "node:assert/strict";
|
|||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import { after, describe, it } from "node:test";
|
import { after, describe, it } from "node:test";
|
||||||
import type { UIMessage } from "ai";
|
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 type { QuestionAnswerData } from "../src/core/question-core.ts";
|
||||||
import { getGraph, graphSnapshot, mergeFindings, type GraphDeps } from "../src/core/graph.ts";
|
import { getGraph, graphSnapshot, mergeFindings, type GraphDeps } from "../src/core/graph.ts";
|
||||||
import { coverageSlots } from "../src/core/coverage.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" }]);
|
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", () => {
|
it("returns RAW node lines — repeated ids (link-appends) are each their own line; getGraph merges", () => {
|
||||||
const line = (msgId: string) =>
|
const line = (msgId: string) =>
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ function fakeStores(
|
|||||||
(over.sources ?? []).map((s) => ({ ...s, extractions: [] }) as unknown as NotesSource),
|
(over.sources ?? []).map((s) => ({ ...s, extractions: [] }) as unknown as NotesSource),
|
||||||
readPositions: () => ({}),
|
readPositions: () => ({}),
|
||||||
writePositions: () => {},
|
writePositions: () => {},
|
||||||
readNamingRuns: () => [],
|
|
||||||
} as unknown as GraphStore;
|
} as unknown as GraphStore;
|
||||||
const chatStore: ChatStore = {
|
const chatStore: ChatStore = {
|
||||||
listChats: () =>
|
listChats: () =>
|
||||||
|
|||||||
Reference in New Issue
Block a user