@trazum/cli 1.38.0 → 1.40.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/src/index.ts CHANGED
@@ -11,7 +11,10 @@ import {
11
11
  cacheableMinimum,
12
12
  analyzeCachePrefix,
13
13
  billLevers,
14
+ buildHistory,
14
15
  buildPlan,
16
+ storedReportFrom,
17
+ verifyPlan,
15
18
  cacheEconomics,
16
19
  cacheHitRate,
17
20
  contextPressure,
@@ -78,7 +81,11 @@ import { cacheDir, cacheStats, cachingProvider, clearCache } from './suggest-cac
78
81
  import { dayOf, formatGap, median, spanDays } from './time.js';
79
82
  import type {
80
83
  FleetSource,
84
+ HistoryRun,
81
85
  MeasuredUsage,
86
+ PlanDocument,
87
+ StoredReport,
88
+ VerifiedAction,
82
89
  BaselineBreach,
83
90
  BaselineChange,
84
91
  BaselineComparison,
@@ -475,6 +482,8 @@ const COMMAND_FLAGS: Record<string, string[]> = {
475
482
  baseline: ['model', 'calls', 'output-tokens', 'cache-hit-rate', 'batch', 'exact-tokens', 'out', 'o'],
476
483
  profile: ['json', 'pricing', 'pricing-live', 'against', 'what-if', 'markdown-out', 'csv-out', 'csv-shape', 'max-usd', 'max-growth-usd', 'max-cache-loss-usd', 'max-day-usd', 'max-session-usd', 'label', 'since', 'until', 'dry-run', 'markdown-summary', 'by-source'],
477
484
  plan: ['json', 'out', 'markdown-out', 'min-usd', 'pricing', 'pricing-live'],
485
+ verify: ['against', 'gate', 'json', 'markdown-out', 'pricing', 'pricing-live'],
486
+ history: ['json', 'markdown-out'],
478
487
  route: ['prompt-file', 'cases', 'label', 'concurrency', 'json', 'yes', 'pricing', 'pricing-live'],
479
488
  eval: ['cases', 'level', 'concurrency', 'export', 'out', 'o', 'model'],
480
489
  prune: ['cases', 'concurrency', 'json', 'yes'],
@@ -2214,6 +2223,260 @@ function isoDate(): string {
2214
2223
  * metered API calls somebody was actually billed for — the bill exists wherever
2215
2224
  * Trazum happens to be running, so the host has no bearing on it.
2216
2225
  */
2226
+ /**
2227
+ * `trazum history <dir>` — many reports over many periods, as one series.
2228
+ *
2229
+ * Derived from *stored* `--json` documents, never re-parsed logs: a team can
2230
+ * keep a year of reports and throw the raw logs away, which is what the
2231
+ * privacy story requires anyway. Shapes are named — a climb, a decay, the
2232
+ * same action planned twice — and no series, however long, becomes a
2233
+ * forecast.
2234
+ */
2235
+ async function commandHistory(args: Args, t: CliMessages): Promise<void> {
2236
+ const path = args.positional[0];
2237
+ if (path === undefined) throw new Error(t.history.noTarget());
2238
+ const target = await stat(path).catch(() => null);
2239
+ if (!target?.isDirectory()) throw new Error(t.history.noTarget());
2240
+
2241
+ const entries = await readdir(path, { withFileTypes: true });
2242
+ const files = entries
2243
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
2244
+ .map((entry) => join(path, entry.name))
2245
+ .sort((a, b) => a.localeCompare(b));
2246
+
2247
+ const reports: StoredReport[] = [];
2248
+ const plans: (PlanDocument & { createdAt?: string })[] = [];
2249
+ const unrecognized: string[] = [];
2250
+ for (const file of files) {
2251
+ let parsed: unknown;
2252
+ try {
2253
+ parsed = JSON.parse(await readFile(file, 'utf8'));
2254
+ } catch {
2255
+ unrecognized.push(file);
2256
+ continue;
2257
+ }
2258
+ const report = storedReportFrom(file, parsed);
2259
+ if (report !== null) {
2260
+ reports.push(report);
2261
+ continue;
2262
+ }
2263
+ const maybePlan = parsed as PlanDocument & { createdAt?: string };
2264
+ if (maybePlan?.schemaVersion === 1 && Array.isArray(maybePlan.actions)) {
2265
+ plans.push(maybePlan);
2266
+ continue;
2267
+ }
2268
+ unrecognized.push(file);
2269
+ }
2270
+
2271
+ const history = buildHistory(reports, plans);
2272
+ if (history.periods.length < 3) {
2273
+ throw new Error(t.history.needsThree(String(history.periods.length)));
2274
+ }
2275
+
2276
+ const stamped = { ...history, unrecognizedFiles: unrecognized };
2277
+ const n = (value: number): string => value.toLocaleString(t.numberLocale);
2278
+ const day = (ms: number): string => new Date(ms).toISOString().slice(0, 10);
2279
+ const pct = (value: number): string => `${(value * 100).toFixed(1)}%`;
2280
+
2281
+ const runLine = (run: HistoryRun): string => {
2282
+ if (run.kind === 'label-spend-climbing') {
2283
+ const name = run.subject === UNLABELLED ? t.profile.unlabelled() : run.subject;
2284
+ return t.history.runLabel(name, n(run.periods), run.sinceName, formatUsd(run.from), formatUsd(run.to));
2285
+ }
2286
+ if (run.kind === 'model-share-climbing') {
2287
+ return t.history.runModel(run.subject, n(run.periods), run.sinceName, pct(run.from), pct(run.to));
2288
+ }
2289
+ return t.history.runCache(n(run.periods), run.sinceName, pct(run.from), pct(run.to));
2290
+ };
2291
+
2292
+ const lines = (md: boolean): string[] => {
2293
+ const out: string[] = [];
2294
+ const first = history.periods[0]!;
2295
+ const last = history.periods[history.periods.length - 1]!;
2296
+ const heading = t.history.heading(n(history.periods.length), day(first.fromMs), day(last.toMs));
2297
+ out.push(md ? `## ${heading}` : heading);
2298
+ for (const period of history.periods) {
2299
+ const row = t.history.periodRow(
2300
+ period.name,
2301
+ formatUsd(period.totalUsd),
2302
+ n(period.calls),
2303
+ ((period.toMs - period.fromMs) / 86_400_000).toFixed(1),
2304
+ );
2305
+ out.push(md ? `- ${row}` : ` ${row}`);
2306
+ }
2307
+ if (history.runs.length > 0) out.push('');
2308
+ for (const run of history.runs) {
2309
+ out.push(md ? `- ${runLine(run)}` : ` ! ${runLine(run)}`);
2310
+ }
2311
+ if (history.repeatedPlanActions.length > 0) out.push('');
2312
+ for (const repeat of history.repeatedPlanActions) {
2313
+ const name = repeat.label === UNLABELLED ? t.profile.unlabelled() : repeat.label;
2314
+ const row = t.history.repeated(
2315
+ repeat.kind,
2316
+ name,
2317
+ repeat.model,
2318
+ n(repeat.appearances),
2319
+ repeat.firstPlanned?.slice(0, 10) ?? null,
2320
+ repeat.lastPlanned?.slice(0, 10) ?? null,
2321
+ );
2322
+ out.push(md ? `- ${row}` : ` ! ${row}`);
2323
+ }
2324
+ for (const name of history.undatedReports) {
2325
+ out.push(md ? `- ${t.history.undated(name)}` : ` ${t.history.undated(name)}`);
2326
+ }
2327
+ for (const name of unrecognized) {
2328
+ out.push(md ? `- ${t.history.unrecognized(name)}` : ` ${t.history.unrecognized(name)}`);
2329
+ }
2330
+ out.push('');
2331
+ out.push(md ? `_${t.history.footer()}_` : ` ${t.history.footer()}`);
2332
+ return out;
2333
+ };
2334
+
2335
+ await writeMarkdown(args, () => lines(true).join('\n'));
2336
+
2337
+ if (boolFlag(args, 'json')) {
2338
+ console.log(JSON.stringify(stamped, null, 2));
2339
+ return;
2340
+ }
2341
+ const [head, ...rest] = lines(false);
2342
+ console.log(c.bold(head!));
2343
+ for (const row of rest) console.log(row === '' ? '' : wrap(row, 76, ' '));
2344
+ }
2345
+
2346
+ /**
2347
+ * `trazum verify <plan.json> --against <newer.jsonl|dir>` — did it work?
2348
+ *
2349
+ * The plan predicted; this holds the prediction to the log that came after
2350
+ * it. Three outcomes and never two — arrived, did not arrive, cannot be told
2351
+ * — because "cannot be told" rendered as "arrived" is how every other tool
2352
+ * congratulates a team for a workload that merely vanished. With `--gate`,
2353
+ * a broken promise is a failing exit code: a different and more useful gate
2354
+ * than "spend went up".
2355
+ */
2356
+ async function commandVerify(
2357
+ args: Args,
2358
+ pricing: PricingCatalogue,
2359
+ t: CliMessages,
2360
+ ): Promise<void> {
2361
+ const planPath = args.positional[0];
2362
+ if (planPath === undefined) throw new Error(t.verify.noTarget());
2363
+ const againstPath = stringFlag(args, 'against');
2364
+ if (againstPath === undefined) throw new Error(t.verify.needsAgainst());
2365
+
2366
+ let plan: PlanDocument & { createdAt?: string };
2367
+ try {
2368
+ const parsed = JSON.parse(await readFile(planPath, 'utf8'));
2369
+ if (parsed?.schemaVersion !== 1 || !Array.isArray(parsed.actions)) {
2370
+ throw new Error(t.verify.badPlan(planPath));
2371
+ }
2372
+ plan = parsed;
2373
+ } catch (error) {
2374
+ if (error instanceof SyntaxError) throw new Error(t.verify.badPlan(planPath));
2375
+ throw error;
2376
+ }
2377
+
2378
+ const GZ = LOG_EXTENSIONS.map((ext) => `${ext}.gz`);
2379
+ const READABLE = [...LOG_EXTENSIONS, ...GZ];
2380
+ const target = await stat(againstPath).catch(() => null);
2381
+ let files: string[] = [againstPath];
2382
+ if (target?.isDirectory()) {
2383
+ const entries = await readdir(againstPath, { withFileTypes: true });
2384
+ files = entries
2385
+ .filter((entry) => entry.isFile() && READABLE.some((ext) => entry.name.endsWith(ext)))
2386
+ .map((entry) => join(againstPath, entry.name))
2387
+ .sort((a, b) => a.localeCompare(b));
2388
+ if (files.length === 0) throw new Error(t.profile.noLogsInDirectory(againstPath, READABLE.join(', ')));
2389
+ }
2390
+ const texts = await Promise.all(files.map((file) => readUsageLog(file, t)));
2391
+ const raw = texts.map((text) => (text.endsWith('\n') ? text : `${text}\n`)).join('');
2392
+ const report = profileUsage(raw, { catalogue: pricing });
2393
+
2394
+ const verification = verifyPlan(plan, report, { currentPricingLastReviewed: pricing.lastReviewed });
2395
+ const gate = boolFlag(args, 'gate');
2396
+ const n = (value: number): string => value.toLocaleString(t.numberLocale);
2397
+
2398
+ const lines = (md: boolean): string[] => {
2399
+ const out: string[] = [];
2400
+ const actionLine = (v: VerifiedAction): string[] => {
2401
+ const name = v.action.label === UNLABELLED ? t.profile.unlabelled() : v.action.label;
2402
+ const rows: string[] = [];
2403
+ rows.push(t.verify.action(v.action.kind, name, v.action.model, v.outcome));
2404
+ if (v.outcome === 'cannot-tell' && v.reason !== null) rows.push(t.verify.reason(v.reason));
2405
+ if (v.action.kind === 'route' || v.action.kind === 'route+batch') {
2406
+ if (v.outcome !== 'cannot-tell') {
2407
+ rows.push(
2408
+ t.verify.routeObserved(
2409
+ String(v.observed.dearestModel ?? ''),
2410
+ formatUsd(Number(v.observed.onTargetUsd ?? 0)),
2411
+ formatUsd(Number(v.observed.onOldModelUsd ?? 0)),
2412
+ ),
2413
+ );
2414
+ }
2415
+ if (v.action.kind === 'route+batch' && v.outcome !== 'cannot-tell') rows.push(t.verify.batchUnobservable());
2416
+ }
2417
+ if (v.action.kind === 'fix-truncation' && v.outcome === 'not-arrived') {
2418
+ rows.push(t.verify.truncationObserved(formatUsd(Number(v.observed.retryBillUsd ?? 0))));
2419
+ }
2420
+ if (v.action.kind === 'fix-caching' && v.outcome !== 'cannot-tell') {
2421
+ rows.push(t.verify.cacheObserved(formatUsd(Number(v.observed.deltaUsd ?? 0)), v.outcome));
2422
+ }
2423
+ if (v.attribution?.calls !== undefined) {
2424
+ rows.push(
2425
+ t.verify.attribution(
2426
+ n(Math.round(v.attribution.calls.before)),
2427
+ n(Math.round(v.attribution.calls.after)),
2428
+ n(Math.round(v.attribution.outputPerCallTokens?.before ?? 0)),
2429
+ n(Math.round(v.attribution.outputPerCallTokens?.after ?? 0)),
2430
+ ),
2431
+ );
2432
+ }
2433
+ return rows;
2434
+ };
2435
+
2436
+ const heading = t.verify.heading(
2437
+ n(verification.actions.length),
2438
+ verification.planCreatedAt === null ? null : verification.planCreatedAt.slice(0, 10),
2439
+ );
2440
+ out.push(md ? `## ${heading}` : heading);
2441
+ out.push(
2442
+ t.verify.counts(n(verification.arrived), n(verification.notArrived), n(verification.cannotTell)),
2443
+ );
2444
+ if (verification.pricesChanged) {
2445
+ out.push(t.verify.pricesChanged(verification.planPricing, verification.currentPricing));
2446
+ }
2447
+ for (const v of verification.actions) {
2448
+ out.push('');
2449
+ const [head, ...rest] = actionLine(v);
2450
+ out.push(md ? `### ${head}` : `→ ${head}`);
2451
+ for (const row of rest) out.push(md ? `- ${row}` : ` · ${row}`);
2452
+ }
2453
+ out.push('');
2454
+ out.push(t.verify.footer());
2455
+ return out;
2456
+ };
2457
+
2458
+ await writeMarkdown(args, () => lines(true).join('\n'));
2459
+
2460
+ if (boolFlag(args, 'json')) {
2461
+ console.log(JSON.stringify(verification, null, 2));
2462
+ } else {
2463
+ const [head, ...rest] = lines(false);
2464
+ console.log(c.bold(head!));
2465
+ for (const row of rest) {
2466
+ console.log(row === '' ? '' : ` ${wrap(row, 74, ' ')}`);
2467
+ }
2468
+ }
2469
+
2470
+ if (gate) {
2471
+ if (verification.gateFailures > 0) {
2472
+ console.error(c.red(t.verify.gateFailed(n(verification.gateFailures), n(verification.actions.length))));
2473
+ process.exitCode = 1;
2474
+ } else {
2475
+ console.log(c.green(t.verify.gateOk()));
2476
+ }
2477
+ }
2478
+ }
2479
+
2217
2480
  /**
2218
2481
  * `trazum plan <log>` — not a list of findings, a ranked plan of what to do.
2219
2482
  *
@@ -6161,6 +6424,12 @@ async function main(): Promise<void> {
6161
6424
  case 'plan':
6162
6425
  await commandPlan(args, pricing, t);
6163
6426
  break;
6427
+ case 'verify':
6428
+ await commandVerify(args, pricing, t);
6429
+ break;
6430
+ case 'history':
6431
+ await commandHistory(args, t);
6432
+ break;
6164
6433
  case 'route':
6165
6434
  await commandRoute(args, pricing, t);
6166
6435
  break;