From 912de96c99e34ed26a336e050d0d0e3ef1384b3c Mon Sep 17 00:00:00 2001 From: Jeremie Grodziski Date: Thu, 29 Oct 2026 22:42:57 +0000 Subject: [PATCH 1/4] test(cli): no-key test ignores the shell's keys The test runs `explain` through the real seam, which reads the process environment. With OPENROUTER_API_KEY exported in the developer's shell the run found a key, stopped at the no-terminal confirmation instead of the missing-key error, and the test failed. It now clears the four provider variables for its own duration (vi.stubEnv), as childEnv already strips NO_COLOR from a spawned CLI. No call could have gone out: without a terminal and without --yes the run stops before the first call. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w --- packages/cli/test/explain.test.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/cli/test/explain.test.ts b/packages/cli/test/explain.test.ts index bdd1e2a..c0bd2b7 100644 --- a/packages/cli/test/explain.test.ts +++ b/packages/cli/test/explain.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from "node:f import { tmpdir } from "node:os"; import { join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { loadSqlite } from "@codegraph/analyzer"; import { decodeInsights, encodeInsightsToString, insightsStorePathFor, sqliteInsightsStore, type InsightRecord, type InsightsStore } from "@codegraph/insights"; import { LlmError, fakeLlmClient, type LlmClient, type LlmRequest, type Provider } from "@codegraph/llm"; @@ -799,12 +799,18 @@ describe("explain picks its provider from the flag or the environment", () => { describe("explain usage errors exit 2 with nothing on stdout", () => { it("without an API key when calls are planned", async () => { - const io = captureIo(); - const code = await run(["explain", FIXTURE, "--src", SRC, "--out", OUT], io); - expect(code).toBe(EXIT.USAGE); - expect(io.stdout()).toBe(""); - expect(io.stderr()).toContain("OPENROUTER_API_KEY"); - expect(io.stderr()).toContain("CLOUDFLARE_API_TOKEN"); + // The real seam reads the process environment: a key in the developer's shell must not make it found. + for (const name of ["OPENROUTER_API_KEY", "CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_AI_GATEWAY_ID"]) vi.stubEnv(name, undefined); + try { + const io = captureIo(); + const code = await run(["explain", FIXTURE, "--src", SRC, "--out", OUT], io); + expect(code).toBe(EXIT.USAGE); + expect(io.stdout()).toBe(""); + expect(io.stderr()).toContain("OPENROUTER_API_KEY"); + expect(io.stderr()).toContain("CLOUDFLARE_API_TOKEN"); + } finally { + vi.unstubAllEnvs(); + } }, 20_000); it("with --src and two models", () => { From 1efdc86cdc28301b11cf1da7c75aeb13f4966ac0 Mon Sep 17 00:00:00 2001 From: Jeremie Grodziski Date: Thu, 29 Oct 2026 22:43:12 +0000 Subject: [PATCH 2/4] fix(insights): refit the completion estimate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-word and per-entry figures were PLAN §20.3's guesses (30 and 40) and the block figures came from prompt 1. A live prompt-2 run of gpt-5.6-luna over Spring PetClinic (106 calls, 96 356 completion tokens, reasoning included) measured 31% more output than --estimate said. Each call's prompt was rebuilt with the planner (contextPackFor + renderPrompts over the run's records; the rebuilt estimates sum to the run's, token for token) and its completion tokens fitted on what it asked: 466 / 534 / 527 per operation / type / module block, 121 per word, 123 per glossary entry (r² 0.81 per call, against 0.13). Rounded: 470 / 530 / 530, 120 and 125. Fitted on half the calls, the figures predict the other half's total within -7% and +15%; the old ones missed by -29% and -33%. The glossary figure rests on five module calls. The estimate line now says the figures were measured on a live run, for which model, with reasoning counted. The tests that multiplied 450/650/ 900 by hand use the exported constants, so they keep checking the formula, not restating the measurement. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w --- packages/cli/src/commands/explain.ts | 13 +++++++------ packages/cli/test/explain.test.ts | 20 +++++++++++++++----- packages/insights/src/prompt.ts | 23 ++++++++++++----------- packages/insights/test/run.test.ts | 9 +++++---- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/packages/cli/src/commands/explain.ts b/packages/cli/src/commands/explain.ts index 327f72b..5511760 100644 --- a/packages/cli/src/commands/explain.ts +++ b/packages/cli/src/commands/explain.ts @@ -741,9 +741,10 @@ export function estimatedCost(promptTokens: number, completionTokens: number, pr /** * `--estimate`: the volume a run would move, before spending anything. Input - * is the rendered prompts at ~4 chars/token; output is one measured block - * average per block asked for. Repair re-asks and rate-limit retries are not - * included, and a run that reuses records sends less than a cold one. + * is the rendered prompts at ~4 chars/token; output is a measured figure per + * block, per word and per glossary entry asked for. Repair re-asks and + * rate-limit retries are not included, and a run that reuses records sends + * less than a cold one. */ function printEstimate(plan: RunPlan, options: ExplainOptions, io: IoSink): void { const { estimates } = plan; @@ -768,7 +769,7 @@ function printEstimate(plan: RunPlan, options: ExplainOptions, io: IoSink): void ...(cost === undefined ? {} : { cost }), assumptions: { promptTokens: "characters of the rendered prompts / 4", - completionTokens: `measured block averages: ${outputAssumption()}`, + completionTokens: `measured on a live run: ${outputAssumption()}`, excluded: "repair re-asks, rate-limit retries", }, }), @@ -801,7 +802,7 @@ function estimateLines(plan: RunPlan, options: ExplainOptions): string[] { } else { lines.push(" cost: pass --price-in USD --price-out USD (per million tokens) for a figure"); } - lines.push(` input ≈ rendered prompts at 4 characters per token; output ≈ measured block averages: ${outputAssumption()}.`); + lines.push(` input ≈ rendered prompts at 4 characters per token; output ≈ measured on a live run: ${outputAssumption()}.`); lines.push(" not included: repair re-asks, rate-limit retries. Records already in the side-car are reused, not re-sent."); return lines; } @@ -809,7 +810,7 @@ function estimateLines(plan: RunPlan, options: ExplainOptions): string[] { /** What the output figure assumes, from the constants it is computed with. */ function outputAssumption(): string { const { operation, type, module } = COMPLETION_TOKENS_PER_BLOCK; - return `operation ${operation}, type ${type}, module ${module} tokens per block asked for, plus an estimated ${COMPLETION_TOKENS_PER_WORD} per word and ${COMPLETION_TOKENS_PER_GLOSSARY_ENTRY} per glossary entry asked to define`; + return `operation ${operation}, type ${type}, module ${module} tokens per block asked for, plus ${COMPLETION_TOKENS_PER_WORD} per word and ${COMPLETION_TOKENS_PER_GLOSSARY_ENTRY} per glossary entry asked to define (gpt-5.6-luna, reasoning included)`; } /** diff --git a/packages/cli/test/explain.test.ts b/packages/cli/test/explain.test.ts index c0bd2b7..5680fd5 100644 --- a/packages/cli/test/explain.test.ts +++ b/packages/cli/test/explain.test.ts @@ -4,7 +4,17 @@ import { join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it, vi } from "vitest"; import { loadSqlite } from "@codegraph/analyzer"; -import { decodeInsights, encodeInsightsToString, insightsStorePathFor, sqliteInsightsStore, type InsightRecord, type InsightsStore } from "@codegraph/insights"; +import { + COMPLETION_TOKENS_PER_BLOCK, + COMPLETION_TOKENS_PER_GLOSSARY_ENTRY, + COMPLETION_TOKENS_PER_WORD, + decodeInsights, + encodeInsightsToString, + insightsStorePathFor, + sqliteInsightsStore, + type InsightRecord, + type InsightsStore, +} from "@codegraph/insights"; import { LlmError, fakeLlmClient, type LlmClient, type LlmRequest, type Provider } from "@codegraph/llm"; import { EXIT } from "../src/exit.js"; import { captureIo } from "../src/io.js"; @@ -680,9 +690,9 @@ describe("explain --estimate", () => { const report = JSON.parse(json.stdout()) as { kind: string; calls: number; promptTokens: number; completionTokens: number; cost: number; byLevel: Record }; expect(report.kind).toBe("codegraph.explainEstimate/1"); expect(report.calls).toBe(68); - // Output = one measured block average per block asked for (a cycle call asks for several), plus - // 30 tokens per word and 40 per glossary entry its prompt asks — counted here on the prompts a run sends. - const per = { operation: 450, type: 650, module: 900 } as const; + // Output = one measured block figure per block asked for (a cycle call asks for several), plus one + // per word and one per glossary entry its prompt asks — counted here on the prompts a run sends. + const per = COMPLETION_TOKENS_PER_BLOCK; const ran = seamWith({ OPENROUTER_API_KEY: KEY }); await explainCommand(options(["--out", OUT]), captureIo(), ran.seam); const expected = { operation: 0, type: 0, module: 0 }; @@ -691,7 +701,7 @@ describe("explain --estimate", () => { const unit = /^# Unit: \w+ (\S+)/u.exec(call.user)?.[1] ?? ""; const members = /Return one entry per id: (.+)\./u.exec(call.user)?.[1]?.split(", ") ?? [unit]; const words = members.reduce((n, id) => n + wordsAsked(call.user, id).size, 0); - expected[level] += per[level] * members.length + 30 * words + 40 * glossaryAsked(call.user).length; + expected[level] += per[level] * members.length + COMPLETION_TOKENS_PER_WORD * words + COMPLETION_TOKENS_PER_GLOSSARY_ENTRY * glossaryAsked(call.user).length; } let sum = 0; for (const level of ["operation", "type", "module"] as const) { diff --git a/packages/insights/src/prompt.ts b/packages/insights/src/prompt.ts index d812832..a641bd9 100644 --- a/packages/insights/src/prompt.ts +++ b/packages/insights/src/prompt.ts @@ -35,23 +35,24 @@ export function estimateTokens(text: string): number { } /** - * Completion tokens one block costs, per level — averages measured on real - * runs (gpt-5.6-luna, Java fixture and gson: ~450 per operation block, ~650 - * per type block, ~900 per module block). A cycle prompt returns one block - * per member, so the estimate scales with the members asked for. + * Completion tokens one block costs, per level, before its words — fitted + * with the two figures below on a live prompt-2 run (PLAN.md §20.3: + * gpt-5.6-luna, Spring PetClinic, 106 calls, reasoning tokens included). A + * cycle prompt returns one block per member, so the estimate scales with the + * members asked for. */ export const COMPLETION_TOKENS_PER_BLOCK: Readonly> = { - operation: 450, - type: 650, - module: 900, + operation: 470, + type: 530, + module: 530, }; /** - * Completion tokens per word defined and per glossary entry (PLAN.md §20.3): - * ESTIMATES, to be replaced by a measurement on a real run under prompt 2. + * Completion tokens per word asked to define and per glossary entry asked to + * reconcile, from the same fit. The glossary figure rests on five module calls. */ -export const COMPLETION_TOKENS_PER_WORD = 30; -export const COMPLETION_TOKENS_PER_GLOSSARY_ENTRY = 40; +export const COMPLETION_TOKENS_PER_WORD = 120; +export const COMPLETION_TOKENS_PER_GLOSSARY_ENTRY = 125; export function estimateCompletionTokens(level: Level, blocks: number, asked: Prompt["asked"] = { words: 0, glossary: 0 }): number { return ( diff --git a/packages/insights/test/run.test.ts b/packages/insights/test/run.test.ts index 97c6c37..5d20f9b 100644 --- a/packages/insights/test/run.test.ts +++ b/packages/insights/test/run.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import type { Edge, Entity } from "@codegraph/core"; import { buildWalk } from "../src/order.js"; import { planRun, retryScope, type PlanEnv, type PlanOptions } from "../src/plan.js"; -import { COMPLETION_TOKENS_PER_GLOSSARY_ENTRY, COMPLETION_TOKENS_PER_WORD } from "../src/prompt.js"; +import { COMPLETION_TOKENS_PER_BLOCK, COMPLETION_TOKENS_PER_GLOSSARY_ENTRY, COMPLETION_TOKENS_PER_WORD } from "../src/prompt.js"; import { memoryBook, type RecordBook } from "../src/records.js"; import { executeRun, type Completer, type CompletionRequest } from "../src/run.js"; import type { Block, FailureRecord, InsightRecord, ModuleBlock, OperationBlock, TypeBlock } from "../src/schema.js"; @@ -240,8 +240,9 @@ describe("planRun", () => { expect(plan.estimates.byStatus).toEqual({ llm: 5, template: 1, reuse: 0, "skip-scope": 0, "skip-budget": 0 }); expect(plan.estimates.promptTokens).toBeGreaterThan(0); // The type defines `total` (its field), the module `java` (its name) and reconciles `total`. - expect(plan.estimates.completionTokens).toBe(3 * 450 + (650 + COMPLETION_TOKENS_PER_WORD) + (900 + COMPLETION_TOKENS_PER_WORD + COMPLETION_TOKENS_PER_GLOSSARY_ENTRY)); - expect(plan.estimates.byLevel.operation.completionTokens).toBe(3 * 450); + const { operation, type, module } = COMPLETION_TOKENS_PER_BLOCK; + expect(plan.estimates.completionTokens).toBe(3 * operation + (type + COMPLETION_TOKENS_PER_WORD) + (module + COMPLETION_TOKENS_PER_WORD + COMPLETION_TOKENS_PER_GLOSSARY_ENTRY)); + expect(plan.estimates.byLevel.operation.completionTokens).toBe(3 * operation); expect(plan.steps.find((s) => s.unit.id === m("getTotal"))?.completionTokens).toBe(0); expect(plan.steps.find((s) => s.unit.id === m("h"))?.model).toBe("leaf-model"); expect(plan.steps.find((s) => s.unit.id === T)?.model).toBe("rollup-model"); @@ -392,7 +393,7 @@ describe("executeRun", () => { const cycle = plan.steps.find((s) => s.unit.members.length === 3); expect(cycle?.calls).toBe(1); // One call, three blocks asked for: the output estimate scales with the members. - expect(cycle?.completionTokens).toBe(3 * 450); + expect(cycle?.completionTokens).toBe(3 * COMPLETION_TOKENS_PER_BLOCK.operation); for (const id of [m("f"), m("g"), m("h")]) { const rec = result.records.find((r) => r.id === id); expect(rec?.scc).toEqual([m("f"), m("g"), m("h")]); From caaf52e9837c790c2000f2b82072704c006fe963 Mon Sep 17 00:00:00 2001 From: Jeremie Grodziski Date: Thu, 29 Oct 2026 22:43:12 +0000 Subject: [PATCH 3/4] docs: the estimate's measured figures docs/insights.md (IN-6) and the explanation page state the refitted figures and where they were measured. The how-to, the tutorial and the discover page reprint the Java fixture's --estimate as it now reads: 52 900 output tokens (was 41 670), $0.0432 at $0.10/$0.60 (was $0.0365). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w --- docs/insights.md | 8 +++++--- website/content/docs/discover/meaning.md | 6 +++--- .../content/docs/explanation/explaining-bottom-up.md | 7 ++++--- website/content/docs/how-to/explain-cost.md | 12 ++++++------ website/content/docs/tutorials/explain.md | 12 ++++++------ 5 files changed, 24 insertions(+), 21 deletions(-) diff --git a/docs/insights.md b/docs/insights.md index 67d30f2..4d40a85 100644 --- a/docs/insights.md +++ b/docs/insights.md @@ -351,9 +351,11 @@ calls it will make, the estimated prompt tokens per level — and makes no call; it needs no key. `--estimate` prints only the volume: calls, input and output tokens per level, and a cost when `--price-in`/`--price-out` (USD per million tokens) are given. Input is the rendered prompts at four characters per token; -output is one measured block average per block asked for (operation 450, type -650, module 900 tokens — averages from gpt-5.6-luna on the fixture and gson), -so a cycle call counts once per member. Repair re-asks and rate-limit retries +output is a measured figure per block asked for (operation 470, type 530, +module 530 tokens), plus 120 per word and 125 per glossary entry asked to +define — fitted on a live prompt-2 run of gpt-5.6-luna over Spring PetClinic, +reasoning tokens included (PLAN.md §20.3) — so a cycle call counts once per +member. Repair re-asks and rate-limit retries are not counted, and records already in the side-car are not re-sent. A real run shows that same estimate on stderr and asks for confirmation before the first call; `--yes` skips the question, and with no terminal to ask on the run diff --git a/website/content/docs/discover/meaning.md b/website/content/docs/discover/meaning.md index ba6aeb5..962259c 100644 --- a/website/content/docs/discover/meaning.md +++ b/website/content/docs/discover/meaning.md @@ -50,7 +50,7 @@ explain plan: 77 units in 8 layers type 17 units 17 calls ~31001 prompt tokens module 3 units 3 calls ~5334 prompt tokens statuses: llm 68, template 9, reuse 0, skip-scope 0, skip-budget 0 - total: 68 calls, ~114483 prompt tokens in, ~41670 completion tokens out + total: 68 calls, ~114483 prompt tokens in, ~52900 completion tokens out ``` ```bash @@ -59,11 +59,11 @@ codegraph explain app.jsonl --src ~/src/app/src/main/java \ ``` ```text - cost at $0.1/M in, $0.6/M out: $0.0365 + cost at $0.1/M in, $0.6/M out: $0.0432 ``` Prices are dollars per million tokens for the model you intend to use; the -figure is a floor (retries are not in it). Three cents for the reference +figure is a floor (retries are not in it). Four cents for the reference fixture; a few dollars for a mid-sized service with a cheap model; tens of dollars for a very large codebase. Neither command makes a call or needs a key, so run them before every real run. diff --git a/website/content/docs/explanation/explaining-bottom-up.md b/website/content/docs/explanation/explaining-bottom-up.md index 3f433ef..1bd2de4 100644 --- a/website/content/docs/explanation/explaining-bottom-up.md +++ b/website/content/docs/explanation/explaining-bottom-up.md @@ -181,9 +181,10 @@ needs no API key at all, which means the plan is inspectable by someone who has not decided to spend anything yet. An **estimate** prints the volume: calls, input and output tokens per level, and a cost when prices per million tokens are supplied. Input is the rendered prompts at four characters per token; output is -one measured average per block asked for — 450 tokens for an operation, 650 for a -type, 900 for a module, averaged from real runs on the fixture and on gson — so a -cycle call counts once per member. Repair re-asks and rate-limit retries are not +one measured figure per block asked for — 470 tokens for an operation, 530 for a +type or a module — plus 120 per word the prompt asks to define and 125 per +glossary entry, fitted on a live run over Spring PetClinic with the model's +reasoning counted — so a cycle call counts once per member. Repair re-asks and rate-limit retries are not counted, and records already in the side-car are not re-sent. Then the caps. A **maximum call count** stops planning calls after N, and what diff --git a/website/content/docs/how-to/explain-cost.md b/website/content/docs/how-to/explain-cost.md index 80a3320..1db7572 100644 --- a/website/content/docs/how-to/explain-cost.md +++ b/website/content/docs/how-to/explain-cost.md @@ -43,13 +43,13 @@ codegraph explain model.jsonl --src fixtures/java/src \ ```text level calls input tokens output tokens - operation 48 78 148 23 550 - type 17 31 001 13 730 - module 3 5 334 4 390 - total 68 114 483 41 670 + operation 48 78 148 30 360 + type 17 31 001 15 590 + module 3 5 334 6 950 + total 68 114 483 52 900 - cost at $0.1/M in, $0.6/M out: $0.0365 - input ≈ rendered prompts at 4 characters per token; output ≈ measured block averages: operation 450, type 650, module 900 tokens per block asked for, plus an estimated 30 per word and 40 per glossary entry asked to define. + cost at $0.1/M in, $0.6/M out: $0.0432 + input ≈ rendered prompts at 4 characters per token; output ≈ measured on a live run: operation 470, type 530, module 530 tokens per block asked for, plus 120 per word and 125 per glossary entry asked to define (gpt-5.6-luna, reasoning included). not included: repair re-asks, rate-limit retries. Records already in the side-car are reused, not re-sent. ``` diff --git a/website/content/docs/tutorials/explain.md b/website/content/docs/tutorials/explain.md index 960f8b4..2108add 100644 --- a/website/content/docs/tutorials/explain.md +++ b/website/content/docs/tutorials/explain.md @@ -122,13 +122,13 @@ explain estimate: order.jsonl models: openai/gpt-5.6-luna (operations), openai/gpt-5.6-luna (types, modules); depth 1 level calls input tokens output tokens - operation 48 78 148 23 550 - type 17 31 001 13 730 - module 3 5 334 4 390 - total 68 114 483 41 670 + operation 48 78 148 30 360 + type 17 31 001 15 590 + module 3 5 334 6 950 + total 68 114 483 52 900 - cost at $0.1/M in, $0.6/M out: $0.0365 - input ≈ rendered prompts at 4 characters per token; output ≈ measured block averages: operation 450, type 650, module 900 tokens per block asked for, plus an estimated 30 per word and 40 per glossary entry asked to define. + cost at $0.1/M in, $0.6/M out: $0.0432 + input ≈ rendered prompts at 4 characters per token; output ≈ measured on a live run: operation 470, type 530, module 530 tokens per block asked for, plus 120 per word and 125 per glossary entry asked to define (gpt-5.6-luna, reasoning included). not included: repair re-asks, rate-limit retries. Records already in the side-car are reused, not re-sent. ``` From 60b323f327ca0e4bce45f6eadcb188cb034d9c9a Mon Sep 17 00:00:00 2001 From: Jeremie Grodziski Date: Thu, 29 Oct 2026 22:43:12 +0000 Subject: [PATCH 4/4] docs(plan): the PetClinic measurement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §20.3's Cost paragraph keeps its estimate and gains the measurement that replaces it: the live PetClinic run (Broadleaf's place as the test corpus, by the user's decision), the fit, its out-of-sample check, the caveat on the glossary figure, and the input gap left as it is (7.9% from explanations a cold estimate cannot quote, 6.3% from a tokenizer at 3.76 characters per token). §20.4's estimate item: the measurement is in; the live run on the Java fixture is still owed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w --- PLAN.md | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/PLAN.md b/PLAN.md index f6ff752..b5e6ba9 100644 --- a/PLAN.md +++ b/PLAN.md @@ -5420,6 +5420,27 @@ per million out; and the bump re-explains the existing side-car once (measured at $4.70 under `deepseek-v4.1-flash`), which `--estimate` prints before anyone pays. +**Measured (2026-10-29) — the estimate above is replaced.** The test corpus is +Spring PetClinic, not Broadleaf (the user's decision: small enough to read +every answer). A live prompt-2 run, gpt-5.6-luna, `--max-tokens 6000`: 106 +calls, 228 177 prompt + 96 356 completion tokens, $0.1343, no failure; +`--estimate` had said 66 570 out (−31%). Each call's prompt was rebuilt with +the planner (`contextPackFor` + `renderPrompts` over the run's own records; +their estimates sum to the run's, token for token) and its measured completion +tokens fitted on what it asked, reasoning tokens included since they are +billed: **466 / 534 / 527 per operation / type / module block, 121 per word, +123 per glossary entry** (r² 0.81 per call; the old figures 0.13), rounded +into `COMPLETION_TOKENS_PER_BLOCK` = 470 / 530 / 530 and 120 / 125. A word +entry's own JSON is ~50 tokens; the rest is the reasoning that goes with it. +Fitted on one half of the calls, the other half's total is predicted within +−7% and +15% (±6% without the module calls), against −29% and −33% before. +The glossary figure rests on five module calls (one half-sample fits 342): a +corpus with more modules should refit it. Per word, at the run's fitted +prices ($0.067/M in, $1.256/M out), ~$0.00015. Input, unchanged here: the +planned prompts were 15% under the measured input — 7.9% because a cold +estimate cannot quote explanations no run has written yet, 6.3% because this +model's tokenizer packs 3.76 characters per token, not 4. + **Layer 3 — readers, all over the store, none calling anything.** `codegraph insights --word order` — every entity whose names spell it, grouped module → type → operation, each with the identifiers it is in, its @@ -5476,7 +5497,9 @@ never looks like a fact. - **The estimate** adds 30 completion tokens per word and 40 per glossary entry, this section's figures, until a run measures them. On the Java fixture the plan goes from 102 786 to 114 483 prompt tokens (+11%) and - from 36 650 to 41 670 completion tokens (+14%). + from 36 650 to 41 670 completion tokens (+14%). *Measured since (the + Cost paragraph): 120 per word, 125 per entry, blocks 470 / 530 / 530; the + fixture's plan now says 52 900 completion tokens.* **As built — the store (2026-10-27).** `INSIGHTS_DB_VERSION = 2`. Migration 1 creates `insight_word (id, entry)`, indexed on `word` and on `id`. `entry` is @@ -5698,8 +5721,9 @@ Vocabulary tab's entries and the Navigate panel's, over `answerGlossary` in answer for a record explained under prompt `"1"`; on the fake client and live on the Java fixture; the §20.3 estimate replaced by the Broadleaf measurement. *(In: `--terms`, the estimate's per-word - figures, the fake-client run, the prompt-1 answer. Owed: the - measurement, live runs.)* + figures, the fake-client run, the prompt-1 answer; the measurement, + from a live run on Spring PetClinic in Broadleaf's place, 2026-10-29, + §20.3 "Measured". Owed: the live run on the Java fixture.)* - [x] Readers: `insights --word`, `--glossary`, `--polysemy`, `--concerns`; `vocabulary --drift-insights`; `/glossary.json`; the entries in the Vocabulary tab and in the Navigate panel as inferences.