@trazum/cli 1.39.0 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,6 +25,8 @@ npm install -g @trazum/cli
25
25
  | `trazum route <log.jsonl>` | is the cheaper model good enough? — measured, and it asks before spending |
26
26
  | `trazum plan <log\|dir>` | the findings as a ranked plan: what to do first, route+batch combined never summed, projections and money already spent totalled apart, every assumption named. `-o` saves it dated for a later verify; `--min-usd` names what it drops |
27
27
  | `trazum verify <plan.json> --against <log>` | did the plan's savings arrive? Three outcomes, never two — arrived, did not, cannot be told — with the world's movement named beside each verdict. `--gate` fails CI on broken promises |
28
+ | `trazum history <dir>` | the long run: series from stored `--json` reports, the climbs and decays no pairwise comparison finds, and plans nobody is executing. Shapes named, nothing forecast |
29
+ | `trazum connect <provider>` | your bill read from the provider's usage API — no export by hand. The credential is borrowed from the environment and never stored; a connected report is restricted on purpose and names the findings a sum cannot support |
28
30
  | `trazum doctor [dir]` | the whole workspace: what nothing is watching, and what fixing would be worth |
29
31
  | `trazum rank <dir>` | of these forty prompts, which is worth an afternoon |
30
32
  | `trazum prune <file> --cases <file>` | which few-shot examples earn their tokens — measured, and it asks before spending |
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The fetch half of the connector: credentials, pagination, and what went
3
+ * missing.
4
+ *
5
+ * The transformation lives in `@trazum/core`, where it is testable without a
6
+ * network. This module does the part that touches the outside world, and it
7
+ * is written under three rules the rest of the product does not need:
8
+ *
9
+ * **A credential is borrowed, never held.** Keys are read from the environment
10
+ * at the moment of the call and never written to a config, a cache, a report
11
+ * or an error message. `redact` runs over everything that can reach a terminal
12
+ * — a key pasted into a CI log by an error handler is a key that has to be
13
+ * rotated, and the tool that leaked it is the tool that promised to save money.
14
+ *
15
+ * **The endpoint is not user-supplied.** Each provider has one fixed base URL
16
+ * compiled in. Trazum's SSRF story has been, since 1.14, that a request body
17
+ * must never *name* a host — it selects one. A usage connector that accepted
18
+ * `--base-url` would hand that property back for the convenience of a
19
+ * self-hosted proxy nobody has asked for yet.
20
+ *
21
+ * **A partial pull is a partial pull, out loud.** Rate limits, page caps and
22
+ * windows the provider has aged out all return what was gathered, with the
23
+ * gap named. A bill quietly short by an unknown amount is the failure this
24
+ * repository refuses everywhere it can occur, and a paginated API is exactly
25
+ * where it occurs.
26
+ */
27
+ import type { ConnectorDescriptor, ConnectorPull } from '@trazum/core';
28
+ export interface CredentialSource {
29
+ /** The environment variable the key came from — the *name*, never the value. */
30
+ variable: string;
31
+ }
32
+ /**
33
+ * Finds the credential without ever returning it to a caller that might print
34
+ * it: the key stays inside this module, and the caller gets the variable name.
35
+ */
36
+ export declare function findCredential(descriptor: ConnectorDescriptor, env: Record<string, string | undefined>): {
37
+ key: string;
38
+ source: CredentialSource;
39
+ } | null;
40
+ /**
41
+ * Removes credential material from anything on its way to a terminal.
42
+ *
43
+ * Two layers on purpose. The exact key is redacted because we hold it; the
44
+ * shapes are redacted because an error body may quote a *different* key —
45
+ * the one the caller mistyped, a key from a proxy's log line — and a leak
46
+ * through somebody else's error message is still a leak through Trazum's
47
+ * output.
48
+ */
49
+ export declare function redact(text: string, key?: string): string;
50
+ export interface FetchUsageOptions {
51
+ descriptor: ConnectorDescriptor;
52
+ fromMs: number;
53
+ toMs: number;
54
+ env: Record<string, string | undefined>;
55
+ /** Injected so the whole path is testable without a network. */
56
+ fetchImpl?: typeof fetch;
57
+ }
58
+ export interface FetchUsageResult {
59
+ pull: ConnectorPull;
60
+ source: CredentialSource;
61
+ pages: number;
62
+ }
63
+ /**
64
+ * Pulls a window of usage, page by page, and reports what it could not get.
65
+ *
66
+ * Returns whatever was gathered when a page fails partway through: half a
67
+ * month with the gap named beats an exception that throws away the half that
68
+ * arrived, and beats a total that silently describes less traffic than the
69
+ * caller asked about.
70
+ */
71
+ export declare function fetchProviderUsage(options: FetchUsageOptions): Promise<FetchUsageResult>;
72
+ //# sourceMappingURL=connect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAW,MAAM,cAAc,CAAC;AAqBhF,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,mBAAmB,EAC/B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACtC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,GAAG,IAAI,CAQlD;AAED;;;;;;;;GAQG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CASzD;AA2BD,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,mBAAmB,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACxC,gEAAgE;IAChE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAwG9F"}
@@ -0,0 +1,204 @@
1
+ /**
2
+ * The fetch half of the connector: credentials, pagination, and what went
3
+ * missing.
4
+ *
5
+ * The transformation lives in `@trazum/core`, where it is testable without a
6
+ * network. This module does the part that touches the outside world, and it
7
+ * is written under three rules the rest of the product does not need:
8
+ *
9
+ * **A credential is borrowed, never held.** Keys are read from the environment
10
+ * at the moment of the call and never written to a config, a cache, a report
11
+ * or an error message. `redact` runs over everything that can reach a terminal
12
+ * — a key pasted into a CI log by an error handler is a key that has to be
13
+ * rotated, and the tool that leaked it is the tool that promised to save money.
14
+ *
15
+ * **The endpoint is not user-supplied.** Each provider has one fixed base URL
16
+ * compiled in. Trazum's SSRF story has been, since 1.14, that a request body
17
+ * must never *name* a host — it selects one. A usage connector that accepted
18
+ * `--base-url` would hand that property back for the convenience of a
19
+ * self-hosted proxy nobody has asked for yet.
20
+ *
21
+ * **A partial pull is a partial pull, out loud.** Rate limits, page caps and
22
+ * windows the provider has aged out all return what was gathered, with the
23
+ * gap named. A bill quietly short by an unknown amount is the failure this
24
+ * repository refuses everywhere it can occur, and a paginated API is exactly
25
+ * where it occurs.
26
+ */
27
+ import { SAFE_FETCH_INIT } from '@trazum/core/node';
28
+ import { normalizeAnthropicUsage, normalizeOpenAIUsage } from '@trazum/core';
29
+ /** Fixed, compiled in, never taken from the caller. See the module note. */
30
+ const ENDPOINTS = {
31
+ anthropic: 'https://api.anthropic.com/v1/organizations/usage_report/messages',
32
+ openai: 'https://api.openai.com/v1/organizations/usage/completions',
33
+ };
34
+ /**
35
+ * How many pages a single pull will walk before it stops and says so.
36
+ *
37
+ * A cap rather than an unbounded loop: a wrong window against a busy
38
+ * organisation is otherwise a request storm against somebody's rate limit,
39
+ * paid for by them. Reaching it is reported as a gap, never as a complete
40
+ * bill.
41
+ */
42
+ const MAX_PAGES = 50;
43
+ /** Requests in flight is always one: usage endpoints are strictly rate limited. */
44
+ const REQUEST_TIMEOUT_MS = 30_000;
45
+ /**
46
+ * Finds the credential without ever returning it to a caller that might print
47
+ * it: the key stays inside this module, and the caller gets the variable name.
48
+ */
49
+ export function findCredential(descriptor, env) {
50
+ for (const variable of descriptor.credentialEnv) {
51
+ const value = env[variable];
52
+ if (typeof value === 'string' && value.trim() !== '') {
53
+ return { key: value.trim(), source: { variable } };
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+ /**
59
+ * Removes credential material from anything on its way to a terminal.
60
+ *
61
+ * Two layers on purpose. The exact key is redacted because we hold it; the
62
+ * shapes are redacted because an error body may quote a *different* key —
63
+ * the one the caller mistyped, a key from a proxy's log line — and a leak
64
+ * through somebody else's error message is still a leak through Trazum's
65
+ * output.
66
+ */
67
+ export function redact(text, key) {
68
+ let out = text;
69
+ if (key !== undefined && key.length >= 8) {
70
+ out = out.split(key).join('[redacted]');
71
+ }
72
+ return out
73
+ .replace(/sk-ant-[A-Za-z0-9_-]{8,}/g, '[redacted]')
74
+ .replace(/sk-[A-Za-z0-9_-]{16,}/g, '[redacted]')
75
+ .replace(/\bBearer\s+[A-Za-z0-9._-]{8,}/gi, 'Bearer [redacted]');
76
+ }
77
+ function headersFor(provider, key) {
78
+ if (provider === 'anthropic') {
79
+ return { 'x-api-key': key, 'anthropic-version': '2023-06-01', accept: 'application/json' };
80
+ }
81
+ return { authorization: `Bearer ${key}`, accept: 'application/json' };
82
+ }
83
+ function urlFor(provider, fromMs, toMs, page) {
84
+ const url = new URL(ENDPOINTS[provider]);
85
+ if (provider === 'anthropic') {
86
+ url.searchParams.set('starting_at', new Date(fromMs).toISOString());
87
+ url.searchParams.set('ending_at', new Date(toMs).toISOString());
88
+ url.searchParams.set('bucket_width', '1d');
89
+ url.searchParams.append('group_by[]', 'model');
90
+ }
91
+ else {
92
+ url.searchParams.set('start_time', String(Math.floor(fromMs / 1000)));
93
+ url.searchParams.set('end_time', String(Math.floor(toMs / 1000)));
94
+ url.searchParams.set('bucket_width', '1d');
95
+ url.searchParams.append('group_by[]', 'model');
96
+ url.searchParams.set('limit', '31');
97
+ }
98
+ if (page !== null)
99
+ url.searchParams.set('page', page);
100
+ return url.toString();
101
+ }
102
+ /**
103
+ * Pulls a window of usage, page by page, and reports what it could not get.
104
+ *
105
+ * Returns whatever was gathered when a page fails partway through: half a
106
+ * month with the gap named beats an exception that throws away the half that
107
+ * arrived, and beats a total that silently describes less traffic than the
108
+ * caller asked about.
109
+ */
110
+ export async function fetchProviderUsage(options) {
111
+ const { descriptor, fromMs, toMs, env, fetchImpl = fetch } = options;
112
+ const found = findCredential(descriptor, env);
113
+ if (found === null) {
114
+ throw new Error(`No credential for ${descriptor.displayName}. Trazum reads it from the environment and never stores it — set ${descriptor.credentialEnv.join(' or ')} to ${descriptor.keyKind}. See ${descriptor.docs}.`);
115
+ }
116
+ const gaps = [];
117
+ const payloads = [];
118
+ let page = null;
119
+ let pages = 0;
120
+ while (pages < MAX_PAGES) {
121
+ const url = urlFor(descriptor.id, fromMs, toMs, page);
122
+ let response;
123
+ try {
124
+ response = await fetchImpl(url, {
125
+ ...SAFE_FETCH_INIT,
126
+ method: 'GET',
127
+ headers: headersFor(descriptor.id, found.key),
128
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
129
+ });
130
+ }
131
+ catch (error) {
132
+ const message = error instanceof Error ? error.message : String(error);
133
+ gaps.push({
134
+ kind: 'rate-limited',
135
+ detail: `the request for page ${pages + 1} did not complete (${redact(message, found.key)}), so everything after it is missing from this window`,
136
+ });
137
+ break;
138
+ }
139
+ pages += 1;
140
+ if (response.status === 429) {
141
+ gaps.push({
142
+ kind: 'rate-limited',
143
+ detail: `the provider rate-limited page ${pages}, so this window stops early and the rest of it was not measured`,
144
+ });
145
+ break;
146
+ }
147
+ if (response.status === 401 || response.status === 403) {
148
+ throw new Error(`${descriptor.displayName} refused the credential in ${found.source.variable} (HTTP ${response.status}). This endpoint needs ${descriptor.keyKind}; an ordinary API key cannot read the usage report.`);
149
+ }
150
+ if (!response.ok) {
151
+ const body = await response.text().catch(() => '');
152
+ throw new Error(`${descriptor.displayName} returned HTTP ${response.status}: ${redact(body.slice(0, 400), found.key) || '(no body)'}`);
153
+ }
154
+ let payload;
155
+ try {
156
+ payload = await response.json();
157
+ }
158
+ catch {
159
+ gaps.push({
160
+ kind: 'unreadable-entry',
161
+ detail: `page ${pages} was not readable JSON, so its buckets are missing from this window`,
162
+ });
163
+ break;
164
+ }
165
+ payloads.push(payload);
166
+ const more = payload.has_more === true;
167
+ const next = payload.next_page;
168
+ if (!more)
169
+ break;
170
+ if (typeof next !== 'string' || next === '') {
171
+ gaps.push({
172
+ kind: 'cursor-expired',
173
+ detail: 'the provider said there was more and served no cursor to reach it, so this window is short by an unknown amount',
174
+ });
175
+ break;
176
+ }
177
+ page = next;
178
+ }
179
+ if (pages >= MAX_PAGES) {
180
+ gaps.push({
181
+ kind: 'page-limit',
182
+ detail: `the pull stopped at ${MAX_PAGES} pages, so this window is incomplete — narrow it with --since and --until`,
183
+ });
184
+ }
185
+ const normalize = descriptor.id === 'anthropic' ? normalizeAnthropicUsage : normalizeOpenAIUsage;
186
+ const pulls = payloads.map((payload) => normalize(payload));
187
+ const pull = {
188
+ provider: descriptor.id,
189
+ granularity: descriptor.granularity,
190
+ buckets: pulls.flatMap((p) => p.buckets),
191
+ window: pulls.length === 0
192
+ ? null
193
+ : {
194
+ fromMs: Math.min(...pulls.filter((p) => p.window).map((p) => p.window.fromMs), Infinity),
195
+ toMs: Math.max(...pulls.filter((p) => p.window).map((p) => p.window.toMs), -Infinity),
196
+ },
197
+ gaps: [...pulls.flatMap((p) => p.gaps), ...gaps],
198
+ unavailable: descriptor.unavailable,
199
+ };
200
+ if (pull.window !== null && !Number.isFinite(pull.window.fromMs))
201
+ pull.window = null;
202
+ return { pull, source: found.source, pages };
203
+ }
204
+ //# sourceMappingURL=connect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.js","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAqB9C;;;;;;GAMG;AACH,eAAO,MAAM,EAAE,EAAE,WAy+ChB,CAAC"}
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAqB9C;;;;;;GAMG;AACH,eAAO,MAAM,EAAE,EAAE,WAslDhB,CAAC"}
package/dist/i18n/en.js CHANGED
@@ -32,6 +32,8 @@ ${bold('USAGE')}
32
32
  trazum route <log.jsonl> --prompt-file <file> --cases <file> --yes
33
33
  trazum plan <log.jsonl|dir> [options]
34
34
  trazum verify <plan.json> --against <newer.jsonl|dir> [options]
35
+ trazum history <dir-of-stored-reports> [options]
36
+ trazum connect <anthropic|openai> [options]
35
37
  trazum diff <before> <after> [options]
36
38
  trazum diff --all <dir> <dir> [options]
37
39
  trazum rank <dir> [options]
@@ -309,6 +311,49 @@ ${bold('OPTIONS FOR plan')}
309
311
  a plan that hides its assumptions is advice pretending to be arithmetic.
310
312
  Projected savings and money already spent are separate totals throughout.
311
313
 
314
+ ${bold('OPTIONS FOR connect')}
315
+ --since <when> The window to pull. A UTC day, an ISO timestamp,
316
+ --until <when> a relative window (7d, 24h) or "now". Defaults to
317
+ the last 30 days.
318
+ --dry-run Say what would be called and which environment
319
+ variable the key would come from. Sends nothing
320
+ and needs no credential.
321
+ --payload <file> Price a usage payload you already have, instead of
322
+ pulling one. No credential, no network — the same
323
+ arithmetic on the same shape.
324
+ -o, --out <file> Save the priced report as JSON.
325
+ --markdown-out <file> Also write it as Markdown, for a CI job summary.
326
+ --json The report as data.
327
+
328
+ Reads your bill from the provider's usage API, so nothing has to be exported
329
+ by hand. The credential is read from the environment at the moment of the
330
+ call and never stored, never printed and never written to a config: set
331
+ TRAZUM_ANTHROPIC_ADMIN_KEY or TRAZUM_OPENAI_ADMIN_KEY. Each provider needs
332
+ the narrowest key that can read a usage report, and an ordinary API key
333
+ cannot.
334
+
335
+ These APIs serve sums over a window, not one row per call, so a connected
336
+ report is a restricted one and says so: the totals, the model split, the day
337
+ series and the cache verdict are all available, and the per-call findings —
338
+ input shapes, truncation retries, conversations, context pressure — are
339
+ listed as unavailable with what would unlock them. A rate limit, a page cap
340
+ or an expired cursor returns what arrived with the gap named, never a total
341
+ that quietly describes less traffic than you asked about.
342
+
343
+ ${bold('OPTIONS FOR history')}
344
+ --markdown-out <file> Also write the series as Markdown, for a CI job
345
+ summary or a pull request comment.
346
+ --json The history as data.
347
+
348
+ Takes a directory of stored reports — the --json documents profile already
349
+ writes — plus any saved plans beside them, and builds the series no
350
+ pairwise comparison can see: a workload climbing a little every period, a
351
+ model share rising since a date, a cache share decaying slowly enough that
352
+ no single week's report called it a finding, and the same action planned
353
+ again and again with nothing executing it. Derived from stored reports,
354
+ never re-parsed logs, so a year of JSON is enough and the raw logs can be
355
+ thrown away. Shapes are named; nothing is forecast.
356
+
312
357
  ${bold('OPTIONS FOR verify')}
313
358
  --against <log|dir> The newer usage log the plan is held to. Required.
314
359
  --gate Exit 1 when an action did not produce what the
@@ -1121,6 +1166,50 @@ ${bold('EXAMPLES')}
1121
1166
  footer: () => 'Ranked by money, projected or already spent alike. The assumptions are yours to answer: this plan is arithmetic over the log, not knowledge of your product.',
1122
1167
  wrote: (path) => `Plan written to ${path}, dated. Keep it: a prediction nobody wrote down is a prediction nobody can be held to.`,
1123
1168
  },
1169
+ connect: {
1170
+ noTarget: (providers) => `Name a provider to read your bill from: trazum connect anthropic. Available: ${providers}. The credential comes from the environment and is never stored — add --dry-run to see exactly what would be called and which variable it would be read from.`,
1171
+ unknownProvider: (id, providers) => `There is no connector for "${id}". The ones that exist are: ${providers}.`,
1172
+ dryRun: (provider, from, to, envVars, keyKind) => `Would read ${provider} usage from ${from} to ${to}, using ${keyKind} taken from ${envVars}. Nothing was sent and no credential was needed to print this.`,
1173
+ heading: (provider, from, to, usd, calls) => calls === null
1174
+ ? `${provider} · ${from} → ${to} · ${usd}`
1175
+ : `${provider} · ${from} → ${to} · ${usd} · ${calls} calls`,
1176
+ modelRow: (model, usd, share, calls) => calls === null ? `${model} ${usd} ${share}` : `${model} ${usd} ${share} · ${calls} calls`,
1177
+ nothingBilled: () => 'The provider billed nothing in this window. That is a measurement, not an error — widen it with --since if you expected traffic.',
1178
+ cachePaid: (saved) => `Caching paid for itself: ${saved} less than these tokens would have cost as ordinary input.`,
1179
+ cacheLost: (added) => `Caching added ${added} to this bill against what the same tokens would have cost as ordinary input.`,
1180
+ cacheUnsettled: () => 'This source did not say which TTL the cache writes used, so the cheaper rate was assumed and the verdict moves under the other one. Unsettled, not settled in your favour.',
1181
+ noCallCount: (provider) => `${provider}'s usage report serves token sums and no request count, so there is no call count here and no per-call average. A zero would read as "no traffic", so nothing is printed instead.`,
1182
+ unpriced: (model, tokens) => `${model} is not in the price catalogue, so its ${tokens} tokens are counted and its money is not. Add it with --pricing rather than reading the total as complete.`,
1183
+ gap: (detail) => `This window is incomplete: ${detail}.`,
1184
+ unavailable: (findings) => `Findings this source cannot support: ${findings}. They need one row per call, and a sum has lost the rows — a per-call log still answers them.`,
1185
+ wrote: (path) => `Report written to ${path}.`,
1186
+ footer: () => 'Every figure here is the provider\u2019s own billed token count at the catalogue\u2019s rates. Nothing was estimated, and nothing the provider did not serve was filled in.',
1187
+ },
1188
+ history: {
1189
+ noTarget: () => 'Point this at a directory of stored reports: trazum history reports/. It reads the --json documents "trazum profile" writes (and any saved plans beside them) and builds the series no pairwise comparison can see.',
1190
+ needsThree: (count) => `A series needs at least three dated reports, and this directory has ${count}. Two reports is a comparison, and "trazum profile --against" already does that better.`,
1191
+ heading: (periods, from, to) => `The long run: ${periods} periods, ${from} → ${to}`,
1192
+ periodRow: (name, usd, calls, days) => `${name} ${usd} · ${calls} calls · ${days} days`,
1193
+ runLabel: (label, periods, sinceName, from, to) => `${label} has climbed for ${periods} consecutive periods since ${sinceName}: ${from} → ${to}. A shape, not a forecast.`,
1194
+ runModel: (model, periods, sinceName, from, to) => `${model}'s share of the bill has climbed for ${periods} consecutive periods since ${sinceName}: ${from} → ${to}. The totals can look flat while the mix moves under them.`,
1195
+ runCache: (periods, sinceName, from, to) => `The cache share has decayed for ${periods} consecutive periods since ${sinceName}: ${from} → ${to} — slowly enough that no single report called it a finding, which is exactly why a series exists.`,
1196
+ repeated: (kind, label, model, appearances, first, last) => {
1197
+ const what = kind === 'route'
1198
+ ? `Routing ${label} (${model})`
1199
+ : kind === 'batch'
1200
+ ? `Batching ${label} (${model})`
1201
+ : kind === 'route+batch'
1202
+ ? `Routing and batching ${label} (${model})`
1203
+ : kind === 'fix-truncation'
1204
+ ? `Fixing the truncation retries on ${label} (${model})`
1205
+ : `Fixing the cache on ${label} (${model})`;
1206
+ const span = first !== null && last !== null ? ` (${first} → ${last})` : '';
1207
+ return `${what} has been planned ${appearances} times${span} and is still in the newest plan — a decision nobody is revisiting.`;
1208
+ },
1209
+ undated: (name) => `${name} carries no span, so it is on no timeline above — named, never silently absorbed.`,
1210
+ unrecognized: (name) => `${name} is neither a stored report nor a saved plan, so it is in no series above.`,
1211
+ footer: () => 'A series names shapes, not futures. Twenty points make a trend visible; they do not make next month knowable — where these lines go next is yours to judge.',
1212
+ },
1124
1213
  verify: {
1125
1214
  noTarget: () => 'Point this at a saved plan and a newer log: trazum verify plan.json --against usage.jsonl. It says, per action, whether the change arrived, did not arrive, or cannot be told — and never fewer than those three.',
1126
1215
  needsAgainst: () => '--against <newer.jsonl|dir> is required. A plan can only be verified against a log that came after it; without one there is nothing to hold the prediction to.',