@trazum/cli 1.38.0 → 1.39.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trazum/cli",
3
- "version": "1.38.0",
3
+ "version": "1.39.0",
4
4
  "description": "Trazum CLI: find where your LLM bill goes, price every finding per month, and enforce token budgets in CI.",
5
5
  "license": "MIT",
6
6
  "author": "David Mu\u00f1oz Rey",
@@ -37,7 +37,7 @@
37
37
  "prepublishOnly": "npm run build && npm test"
38
38
  },
39
39
  "dependencies": {
40
- "@trazum/core": "1.38.0"
40
+ "@trazum/core": "1.39.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.2.0",
package/src/i18n/en.ts CHANGED
@@ -40,6 +40,7 @@ ${bold('USAGE')}
40
40
  trazum eval <file> --cases <file> --export promptfoo -o suite.json
41
41
  trazum route <log.jsonl> --prompt-file <file> --cases <file> --yes
42
42
  trazum plan <log.jsonl|dir> [options]
43
+ trazum verify <plan.json> --against <newer.jsonl|dir> [options]
43
44
  trazum diff <before> <after> [options]
44
45
  trazum diff --all <dir> <dir> [options]
45
46
  trazum rank <dir> [options]
@@ -317,6 +318,25 @@ ${bold('OPTIONS FOR plan')}
317
318
  a plan that hides its assumptions is advice pretending to be arithmetic.
318
319
  Projected savings and money already spent are separate totals throughout.
319
320
 
321
+ ${bold('OPTIONS FOR verify')}
322
+ --against <log|dir> The newer usage log the plan is held to. Required.
323
+ --gate Exit 1 when an action did not produce what the
324
+ plan promised, or its fields stopped being
325
+ recorded — "not recorded" must not read as
326
+ "fixed". A vanished workload fails nothing.
327
+ --markdown-out <file> Also write the verdicts as Markdown, for a CI
328
+ job summary or a pull request comment.
329
+ --pricing <file> Local price overlay, as everywhere else.
330
+ --json The verification as data.
331
+
332
+ Holds a saved plan to the log that came after it, with three outcomes and
333
+ never two: the change arrived, it did not, or it cannot be told — because
334
+ the workload vanished, the fields the detection needs stopped being
335
+ recorded, or tokens cannot say which tier billed them. Differences carry
336
+ the world's measured movement (calls, output per call) from the plan's own
337
+ baseline, and a plan priced under a different catalogue says so rather
338
+ than blaming a team for a saving that arithmetic revoked.
339
+
320
340
  ${bold('OPTIONS FOR route')}
321
341
  --prompt-file <file> The prompt those calls send. Not --prompt, which
322
342
  names a marked prompt inside a source file.
@@ -1412,6 +1432,61 @@ ${bold('EXAMPLES')}
1412
1432
  `Plan written to ${path}, dated. Keep it: a prediction nobody wrote down is a prediction nobody can be held to.`,
1413
1433
  },
1414
1434
 
1435
+ verify: {
1436
+ noTarget: () =>
1437
+ '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.',
1438
+ needsAgainst: () =>
1439
+ '--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.',
1440
+ badPlan: (path) =>
1441
+ `${path} is not a plan document this tool can verify — expected the JSON that "trazum plan -o" writes (schemaVersion 1, with an actions array).`,
1442
+ heading: (actions, planDate) =>
1443
+ planDate === null
1444
+ ? `Did it work? ${actions} actions from an undated plan, against this log`
1445
+ : `Did it work? ${actions} actions from the plan of ${planDate}, against this log`,
1446
+ counts: (arrived, notArrived, cannotTell) =>
1447
+ `${arrived} arrived · ${notArrived} did not arrive · ${cannotTell} cannot be told. The third is not a soft version of the second: it means this log cannot answer, which is its own finding.`,
1448
+ pricesChanged: (planReviewed, nowReviewed) =>
1449
+ `Prices were reviewed ${planReviewed} when the plan was made and ${nowReviewed} now, so every dollar comparison here is two price lists, not one measurement — a team must not be blamed for a saving that arithmetic revoked.`,
1450
+ action: (kind, label, model, outcome) => {
1451
+ const what =
1452
+ kind === 'route'
1453
+ ? `Route ${label} (${model})`
1454
+ : kind === 'batch'
1455
+ ? `Batch ${label} (${model})`
1456
+ : kind === 'route+batch'
1457
+ ? `Route and batch ${label} (${model})`
1458
+ : kind === 'fix-truncation'
1459
+ ? `Fix the truncation retries on ${label} (${model})`
1460
+ : `Fix the cache on ${label} (${model})`;
1461
+ const verdict =
1462
+ outcome === 'arrived' ? 'ARRIVED' : outcome === 'not-arrived' ? 'DID NOT ARRIVE' : 'CANNOT BE TOLD';
1463
+ return `${what} — ${verdict}`;
1464
+ },
1465
+ reason: (reason) =>
1466
+ reason === 'workload-vanished'
1467
+ ? 'the label carries no priced traffic in this log — a vanished workload is not a fixed one, and not a broken one either'
1468
+ : reason === 'fields-stopped'
1469
+ ? 'the fields the detection needs are not in this log — "not recorded" must not read as "fixed", so with --gate this fails'
1470
+ : 'the log records tokens, and tokens do not say which tier billed them — the Batch API cannot be seen from here',
1471
+ routeObserved: (dearestModel, onTargetUsd, onOldUsd) =>
1472
+ `the label's dearest model is now ${dearestModel} · ${onTargetUsd} on the target, ${onOldUsd} still on the old model`,
1473
+ batchUnobservable: () =>
1474
+ 'the batch half of this action cannot be seen in token counts; the verdict above is the route half alone',
1475
+ truncationObserved: (retryBillUsd) =>
1476
+ `this log still shows ${retryBillUsd} of truncation waste and retries`,
1477
+ cacheObserved: (deltaUsd, outcome) =>
1478
+ outcome === 'arrived'
1479
+ ? `caching now pays for itself on this slice (${deltaUsd} against the no-cache bill)`
1480
+ : `caching still adds ${deltaUsd} to this slice's bill`,
1481
+ attribution: (callsBefore, callsAfter, outBefore, outAfter) =>
1482
+ `the world moved too: calls ${callsBefore} → ${callsAfter}, output/call ${outBefore} → ${outAfter} tokens — stated so the verdict is not read as the whole story`,
1483
+ gateFailed: (failures, total) =>
1484
+ `GATE FAILED — ${failures} of ${total} actions did not produce what the plan promised, or stopped being measurable by the team's own log.`,
1485
+ gateOk: () => 'Gate passed: every verifiable action arrived, and nothing became unverifiable.',
1486
+ footer: () =>
1487
+ 'Arrived and did-not-arrive are measurements; cannot-be-told is the log refusing to guess. All three are the verification working, not failing.',
1488
+ },
1489
+
1415
1490
  route: {
1416
1491
  noTarget: () =>
1417
1492
  'Point this at a usage log and a prompt: trazum route usage.jsonl --prompt-file prompts/support.txt --cases cases.txt --yes. It finds the slice worth the most, then measures whether the cheaper model still does the job. The flag is --prompt-file and not --prompt, because --prompt names a marked prompt inside a source file everywhere else in this tool.',
package/src/i18n/es.ts CHANGED
@@ -27,6 +27,7 @@ ${bold('USO')}
27
27
  trazum eval <fichero> --cases <fichero> --export promptfoo -o suite.json
28
28
  trazum route <log.jsonl> --prompt-file <fichero> --cases <fichero> --yes
29
29
  trazum plan <log.jsonl|dir> [opciones]
30
+ trazum verify <plan.json> --against <nuevo.jsonl|dir> [opciones]
30
31
  trazum diff <antes> <después> [opciones]
31
32
  trazum diff --all <dir> <dir> [opciones]
32
33
  trazum rank <dir> [opciones]
@@ -324,6 +325,27 @@ ${bold('OPCIONES DE plan')}
324
325
  consejo haciéndose pasar por aritmética. El ahorro proyectado y el dinero ya
325
326
  gastado son totales separados en todas partes.
326
327
 
328
+ ${bold('OPCIONES DE verify')}
329
+ --against <log|dir> El registro de uso posterior al que se somete el
330
+ plan. Obligatorio.
331
+ --gate Sale con 1 cuando una acción no produjo lo que el
332
+ plan prometió, o sus campos dejaron de
333
+ registrarse — "no registrado" no puede leerse
334
+ como "arreglado". Una carga de trabajo
335
+ desaparecida no suspende nada.
336
+ --markdown-out <fichero> Escribe además los veredictos como Markdown, para
337
+ un resumen de CI o un comentario de pull request.
338
+ --pricing <fichero> Tarifas locales superpuestas, como en el resto.
339
+ --json La verificación como datos.
340
+
341
+ Somete un plan guardado al registro que vino después, con tres resultados y
342
+ nunca dos: el cambio llegó, no llegó, o no se puede saber — porque la carga
343
+ desapareció, los campos que la detección necesita dejaron de registrarse, o
344
+ los tokens no dicen con qué tarifa se facturaron. Las diferencias llevan el
345
+ movimiento medido del mundo (llamadas, salida por llamada) desde la línea
346
+ base del propio plan, y un plan tasado con otro catálogo lo dice en vez de
347
+ culpar a un equipo por un ahorro que la aritmética revocó.
348
+
327
349
  ${bold('OPCIONES DE route')}
328
350
  --prompt-file <fichero> El prompt que mandan esas llamadas. No --prompt,
329
351
  que nombra un prompt marcado dentro de un fuente.
@@ -1432,6 +1454,61 @@ ${bold('EJEMPLOS')}
1432
1454
  `Plan escrito en ${path}, con fecha. Guárdalo: una predicción que nadie apuntó es una predicción que no se le puede exigir a nadie.`,
1433
1455
  },
1434
1456
 
1457
+ verify: {
1458
+ noTarget: () =>
1459
+ 'Apunta esto a un plan guardado y a un registro posterior: trazum verify plan.json --against usage.jsonl. Dice, por acción, si el cambio llegó, no llegó o no se puede saber — y nunca menos de esos tres.',
1460
+ needsAgainst: () =>
1461
+ '--against <nuevo.jsonl|dir> es obligatorio. Un plan solo puede verificarse contra un registro posterior; sin uno no hay nada a lo que someter la predicción.',
1462
+ badPlan: (path) =>
1463
+ `${path} no es un documento de plan que esta herramienta pueda verificar — se esperaba el JSON que escribe "trazum plan -o" (schemaVersion 1, con un array actions).`,
1464
+ heading: (actions, planDate) =>
1465
+ planDate === null
1466
+ ? `¿Funcionó? ${actions} acciones de un plan sin fecha, contra este registro`
1467
+ : `¿Funcionó? ${actions} acciones del plan del ${planDate}, contra este registro`,
1468
+ counts: (arrived, notArrived, cannotTell) =>
1469
+ `${arrived} llegaron · ${notArrived} no llegaron · ${cannotTell} no se puede saber. Lo tercero no es una versión suave de lo segundo: significa que este registro no puede responder, y eso es un hallazgo en sí.`,
1470
+ pricesChanged: (planReviewed, nowReviewed) =>
1471
+ `Las tarifas se revisaron el ${planReviewed} cuando se hizo el plan y el ${nowReviewed} ahora, así que cada comparación en dólares aquí son dos listas de precios, no una medición — no se puede culpar a un equipo por un ahorro que la aritmética revocó.`,
1472
+ action: (kind, label, model, outcome) => {
1473
+ const what =
1474
+ kind === 'route'
1475
+ ? `Enrutar ${label} (${model})`
1476
+ : kind === 'batch'
1477
+ ? `Agrupar en batch ${label} (${model})`
1478
+ : kind === 'route+batch'
1479
+ ? `Enrutar y agrupar ${label} (${model})`
1480
+ : kind === 'fix-truncation'
1481
+ ? `Arreglar los reintentos por truncado de ${label} (${model})`
1482
+ : `Arreglar la caché de ${label} (${model})`;
1483
+ const verdict =
1484
+ outcome === 'arrived' ? 'LLEGÓ' : outcome === 'not-arrived' ? 'NO LLEGÓ' : 'NO SE PUEDE SABER';
1485
+ return `${what} — ${verdict}`;
1486
+ },
1487
+ reason: (reason) =>
1488
+ reason === 'workload-vanished'
1489
+ ? 'el label no lleva tráfico tasado en este registro — una carga desaparecida no está arreglada, y tampoco rota'
1490
+ : reason === 'fields-stopped'
1491
+ ? 'los campos que la detección necesita no están en este registro — "no registrado" no puede leerse como "arreglado", así que con --gate esto suspende'
1492
+ : 'el registro guarda tokens, y los tokens no dicen con qué tarifa se facturaron — el Batch API no se puede ver desde aquí',
1493
+ routeObserved: (dearestModel, onTargetUsd, onOldUsd) =>
1494
+ `el modelo más caro del label ahora es ${dearestModel} · ${onTargetUsd} en el destino, ${onOldUsd} todavía en el modelo antiguo`,
1495
+ batchUnobservable: () =>
1496
+ 'la mitad batch de esta acción no se puede ver en los recuentos de tokens; el veredicto de arriba es solo la mitad de la ruta',
1497
+ truncationObserved: (retryBillUsd) =>
1498
+ `este registro todavía muestra ${retryBillUsd} de desperdicio y reintentos por truncado`,
1499
+ cacheObserved: (deltaUsd, outcome) =>
1500
+ outcome === 'arrived'
1501
+ ? `la caché ahora se paga sola en esta porción (${deltaUsd} contra la factura sin caché)`
1502
+ : `la caché todavía añade ${deltaUsd} a la factura de esta porción`,
1503
+ attribution: (callsBefore, callsAfter, outBefore, outAfter) =>
1504
+ `el mundo también se movió: llamadas ${callsBefore} → ${callsAfter}, salida/llamada ${outBefore} → ${outAfter} tokens — dicho para que el veredicto no se lea como toda la historia`,
1505
+ gateFailed: (failures, total) =>
1506
+ `GATE SUSPENDIDO — ${failures} de ${total} acciones no produjeron lo que el plan prometió, o dejaron de poder medirse por el propio registro del equipo.`,
1507
+ gateOk: () => 'Gate superado: toda acción verificable llegó, y nada se volvió inverificable.',
1508
+ footer: () =>
1509
+ 'Llegó y no-llegó son mediciones; no-se-puede-saber es el registro negándose a adivinar. Los tres son la verificación funcionando, no fallando.',
1510
+ },
1511
+
1435
1512
  route: {
1436
1513
  noTarget: () =>
1437
1514
  'Apunta esto a un registro de uso y a un prompt: trazum route usage.jsonl --prompt-file prompts/soporte.txt --cases casos.txt --yes. Busca la porción que más vale y mide si el modelo más barato sigue haciendo el trabajo. El flag es --prompt-file y no --prompt, porque en el resto de la herramienta --prompt nombra un prompt marcado dentro de un fichero fuente.',
package/src/i18n/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { EvalVerdict, Locale, PlanActionKind, PlanAssumption, RuleLevel } from '@trazum/core';
1
+ import type { CannotTellReason, EvalVerdict, Locale, PlanActionKind, PlanAssumption, RuleLevel, VerifyOutcome } from '@trazum/core';
2
2
 
3
3
  /**
4
4
  * The CLI's own message catalogue.
@@ -1072,6 +1072,38 @@ export interface CliMessages {
1072
1072
  wrote(path: string): string;
1073
1073
  };
1074
1074
 
1075
+ /**
1076
+ * `trazum verify` — the plan held to the log that came after it.
1077
+ *
1078
+ * Three outcomes and never two: arrived, did not arrive, cannot be told.
1079
+ * The third is the honest one, and the one every other tool renders as
1080
+ * the first. Differences carry the world's measured movement, and a plan
1081
+ * priced under another catalogue says so rather than blaming a team for
1082
+ * a saving that arithmetic revoked.
1083
+ */
1084
+ verify: {
1085
+ noTarget(): string;
1086
+ needsAgainst(): string;
1087
+ /** Not a plan document: wrong shape, wrong version, or not JSON at all. */
1088
+ badPlan(path: string): string;
1089
+ heading(actions: string, planDate: string | null): string;
1090
+ counts(arrived: string, notArrived: string, cannotTell: string): string;
1091
+ /** Two price lists are two measurements; every dollar line inherits this. */
1092
+ pricesChanged(planReviewed: string, nowReviewed: string): string;
1093
+ action(kind: PlanActionKind, label: string, model: string, outcome: VerifyOutcome): string;
1094
+ reason(reason: CannotTellReason): string;
1095
+ routeObserved(dearestModel: string, onTargetUsd: string, onOldUsd: string): string;
1096
+ /** Tokens do not say which tier billed them; named, never assumed arrived. */
1097
+ batchUnobservable(): string;
1098
+ truncationObserved(retryBillUsd: string): string;
1099
+ cacheObserved(deltaUsd: string, outcome: VerifyOutcome): string;
1100
+ /** The world's movement, measured: never a verdict, always both numbers. */
1101
+ attribution(callsBefore: string, callsAfter: string, outBefore: string, outAfter: string): string;
1102
+ gateFailed(failures: string, total: string): string;
1103
+ gateOk(): string;
1104
+ footer(): string;
1105
+ };
1106
+
1075
1107
  /**
1076
1108
  * `trazum route` — the loop the levers section could only point at.
1077
1109
  *
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  analyzeCachePrefix,
13
13
  billLevers,
14
14
  buildPlan,
15
+ verifyPlan,
15
16
  cacheEconomics,
16
17
  cacheHitRate,
17
18
  contextPressure,
@@ -79,6 +80,8 @@ import { dayOf, formatGap, median, spanDays } from './time.js';
79
80
  import type {
80
81
  FleetSource,
81
82
  MeasuredUsage,
83
+ PlanDocument,
84
+ VerifiedAction,
82
85
  BaselineBreach,
83
86
  BaselineChange,
84
87
  BaselineComparison,
@@ -475,6 +478,7 @@ const COMMAND_FLAGS: Record<string, string[]> = {
475
478
  baseline: ['model', 'calls', 'output-tokens', 'cache-hit-rate', 'batch', 'exact-tokens', 'out', 'o'],
476
479
  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
480
  plan: ['json', 'out', 'markdown-out', 'min-usd', 'pricing', 'pricing-live'],
481
+ verify: ['against', 'gate', 'json', 'markdown-out', 'pricing', 'pricing-live'],
478
482
  route: ['prompt-file', 'cases', 'label', 'concurrency', 'json', 'yes', 'pricing', 'pricing-live'],
479
483
  eval: ['cases', 'level', 'concurrency', 'export', 'out', 'o', 'model'],
480
484
  prune: ['cases', 'concurrency', 'json', 'yes'],
@@ -2214,6 +2218,140 @@ function isoDate(): string {
2214
2218
  * metered API calls somebody was actually billed for — the bill exists wherever
2215
2219
  * Trazum happens to be running, so the host has no bearing on it.
2216
2220
  */
2221
+ /**
2222
+ * `trazum verify <plan.json> --against <newer.jsonl|dir>` — did it work?
2223
+ *
2224
+ * The plan predicted; this holds the prediction to the log that came after
2225
+ * it. Three outcomes and never two — arrived, did not arrive, cannot be told
2226
+ * — because "cannot be told" rendered as "arrived" is how every other tool
2227
+ * congratulates a team for a workload that merely vanished. With `--gate`,
2228
+ * a broken promise is a failing exit code: a different and more useful gate
2229
+ * than "spend went up".
2230
+ */
2231
+ async function commandVerify(
2232
+ args: Args,
2233
+ pricing: PricingCatalogue,
2234
+ t: CliMessages,
2235
+ ): Promise<void> {
2236
+ const planPath = args.positional[0];
2237
+ if (planPath === undefined) throw new Error(t.verify.noTarget());
2238
+ const againstPath = stringFlag(args, 'against');
2239
+ if (againstPath === undefined) throw new Error(t.verify.needsAgainst());
2240
+
2241
+ let plan: PlanDocument & { createdAt?: string };
2242
+ try {
2243
+ const parsed = JSON.parse(await readFile(planPath, 'utf8'));
2244
+ if (parsed?.schemaVersion !== 1 || !Array.isArray(parsed.actions)) {
2245
+ throw new Error(t.verify.badPlan(planPath));
2246
+ }
2247
+ plan = parsed;
2248
+ } catch (error) {
2249
+ if (error instanceof SyntaxError) throw new Error(t.verify.badPlan(planPath));
2250
+ throw error;
2251
+ }
2252
+
2253
+ const GZ = LOG_EXTENSIONS.map((ext) => `${ext}.gz`);
2254
+ const READABLE = [...LOG_EXTENSIONS, ...GZ];
2255
+ const target = await stat(againstPath).catch(() => null);
2256
+ let files: string[] = [againstPath];
2257
+ if (target?.isDirectory()) {
2258
+ const entries = await readdir(againstPath, { withFileTypes: true });
2259
+ files = entries
2260
+ .filter((entry) => entry.isFile() && READABLE.some((ext) => entry.name.endsWith(ext)))
2261
+ .map((entry) => join(againstPath, entry.name))
2262
+ .sort((a, b) => a.localeCompare(b));
2263
+ if (files.length === 0) throw new Error(t.profile.noLogsInDirectory(againstPath, READABLE.join(', ')));
2264
+ }
2265
+ const texts = await Promise.all(files.map((file) => readUsageLog(file, t)));
2266
+ const raw = texts.map((text) => (text.endsWith('\n') ? text : `${text}\n`)).join('');
2267
+ const report = profileUsage(raw, { catalogue: pricing });
2268
+
2269
+ const verification = verifyPlan(plan, report, { currentPricingLastReviewed: pricing.lastReviewed });
2270
+ const gate = boolFlag(args, 'gate');
2271
+ const n = (value: number): string => value.toLocaleString(t.numberLocale);
2272
+
2273
+ const lines = (md: boolean): string[] => {
2274
+ const out: string[] = [];
2275
+ const actionLine = (v: VerifiedAction): string[] => {
2276
+ const name = v.action.label === UNLABELLED ? t.profile.unlabelled() : v.action.label;
2277
+ const rows: string[] = [];
2278
+ rows.push(t.verify.action(v.action.kind, name, v.action.model, v.outcome));
2279
+ if (v.outcome === 'cannot-tell' && v.reason !== null) rows.push(t.verify.reason(v.reason));
2280
+ if (v.action.kind === 'route' || v.action.kind === 'route+batch') {
2281
+ if (v.outcome !== 'cannot-tell') {
2282
+ rows.push(
2283
+ t.verify.routeObserved(
2284
+ String(v.observed.dearestModel ?? ''),
2285
+ formatUsd(Number(v.observed.onTargetUsd ?? 0)),
2286
+ formatUsd(Number(v.observed.onOldModelUsd ?? 0)),
2287
+ ),
2288
+ );
2289
+ }
2290
+ if (v.action.kind === 'route+batch' && v.outcome !== 'cannot-tell') rows.push(t.verify.batchUnobservable());
2291
+ }
2292
+ if (v.action.kind === 'fix-truncation' && v.outcome === 'not-arrived') {
2293
+ rows.push(t.verify.truncationObserved(formatUsd(Number(v.observed.retryBillUsd ?? 0))));
2294
+ }
2295
+ if (v.action.kind === 'fix-caching' && v.outcome !== 'cannot-tell') {
2296
+ rows.push(t.verify.cacheObserved(formatUsd(Number(v.observed.deltaUsd ?? 0)), v.outcome));
2297
+ }
2298
+ if (v.attribution?.calls !== undefined) {
2299
+ rows.push(
2300
+ t.verify.attribution(
2301
+ n(Math.round(v.attribution.calls.before)),
2302
+ n(Math.round(v.attribution.calls.after)),
2303
+ n(Math.round(v.attribution.outputPerCallTokens?.before ?? 0)),
2304
+ n(Math.round(v.attribution.outputPerCallTokens?.after ?? 0)),
2305
+ ),
2306
+ );
2307
+ }
2308
+ return rows;
2309
+ };
2310
+
2311
+ const heading = t.verify.heading(
2312
+ n(verification.actions.length),
2313
+ verification.planCreatedAt === null ? null : verification.planCreatedAt.slice(0, 10),
2314
+ );
2315
+ out.push(md ? `## ${heading}` : heading);
2316
+ out.push(
2317
+ t.verify.counts(n(verification.arrived), n(verification.notArrived), n(verification.cannotTell)),
2318
+ );
2319
+ if (verification.pricesChanged) {
2320
+ out.push(t.verify.pricesChanged(verification.planPricing, verification.currentPricing));
2321
+ }
2322
+ for (const v of verification.actions) {
2323
+ out.push('');
2324
+ const [head, ...rest] = actionLine(v);
2325
+ out.push(md ? `### ${head}` : `→ ${head}`);
2326
+ for (const row of rest) out.push(md ? `- ${row}` : ` · ${row}`);
2327
+ }
2328
+ out.push('');
2329
+ out.push(t.verify.footer());
2330
+ return out;
2331
+ };
2332
+
2333
+ await writeMarkdown(args, () => lines(true).join('\n'));
2334
+
2335
+ if (boolFlag(args, 'json')) {
2336
+ console.log(JSON.stringify(verification, null, 2));
2337
+ } else {
2338
+ const [head, ...rest] = lines(false);
2339
+ console.log(c.bold(head!));
2340
+ for (const row of rest) {
2341
+ console.log(row === '' ? '' : ` ${wrap(row, 74, ' ')}`);
2342
+ }
2343
+ }
2344
+
2345
+ if (gate) {
2346
+ if (verification.gateFailures > 0) {
2347
+ console.error(c.red(t.verify.gateFailed(n(verification.gateFailures), n(verification.actions.length))));
2348
+ process.exitCode = 1;
2349
+ } else {
2350
+ console.log(c.green(t.verify.gateOk()));
2351
+ }
2352
+ }
2353
+ }
2354
+
2217
2355
  /**
2218
2356
  * `trazum plan <log>` — not a list of findings, a ranked plan of what to do.
2219
2357
  *
@@ -6161,6 +6299,9 @@ async function main(): Promise<void> {
6161
6299
  case 'plan':
6162
6300
  await commandPlan(args, pricing, t);
6163
6301
  break;
6302
+ case 'verify':
6303
+ await commandVerify(args, pricing, t);
6304
+ break;
6164
6305
  case 'route':
6165
6306
  await commandRoute(args, pricing, t);
6166
6307
  break;