@trazum/cli 1.32.0 → 1.33.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.32.0",
3
+ "version": "1.33.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.32.0"
40
+ "@trazum/core": "1.33.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.2.0",
package/src/i18n/en.ts CHANGED
@@ -1150,6 +1150,28 @@ ${bold('EXAMPLES')}
1150
1150
  `--what-if does not know "${value}". Priced models: ${available}. Add it with --pricing if you have its rates.`,
1151
1151
  badGzip: (file, detail) =>
1152
1152
  `${file} is gzipped and would not decompress: ${detail}. Reading the rest and saying nothing would report a bill missing whatever that file held, so this stops instead. Check the file, or move it out of the directory.`,
1153
+ dryRunHeading: () => 'What this log can answer — no bill was produced',
1154
+ dryRunParsed: (parsed, skipped) =>
1155
+ `${parsed} records parse; ${skipped} lines could not be read. A dry run prices nothing: the point is what the gates you are about to wire would stand on.`,
1156
+ dryRunUnpriced: (models) =>
1157
+ `Models the price table does not know: ${models}. Their tokens parse; their dollars need a --pricing overlay.`,
1158
+ dryRunTotals: () => 'The bill itself: totals, per-model split, cache economics, the levers.',
1159
+ dryRunLabels: (share) =>
1160
+ `Per-workload findings and label budgets — "label" on ${share} of records.`,
1161
+ dryRunClock: (share) =>
1162
+ `The period, per-day and per-hour shape, --max-day-usd, --since/--until — a timestamp on ${share} of records.`,
1163
+ dryRunSessions: (share) =>
1164
+ `Conversation growth, per-conversation cost, --max-session-usd — a session on ${share} of records. Grouped by, never printed.`,
1165
+ dryRunStopReason: (share) =>
1166
+ `Truncated answers and their retry bill — a stop reason on ${share} of records.`,
1167
+ dryRunCacheTtl: (ttl, writes) =>
1168
+ `Settled cache verdicts — the "cache_creation" split on ${ttl} of ${writes} cache-writing records.`,
1169
+ dryRunNoCacheTraffic: () =>
1170
+ 'Cache verdicts: nothing wrote to the cache in this log, so there is nothing to settle — not a missing field.',
1171
+ dryRunFooter: () =>
1172
+ 'A ✗ is not a defect in the log; it is a finding this log cannot support yet. The README\'s recording recipe carries every field above.',
1173
+ dryRunNoGates: () =>
1174
+ '--dry-run produces no bill, so a gate beside it would exit green having judged nothing. Run the gates without --dry-run.',
1153
1175
  coverageHeading: () => 'What this log cannot answer yet',
1154
1176
  needsLabel: (seen) =>
1155
1177
  `"label" on ${seen} records: without it every workload is one row, so no per-workload spend, no drill-down, and the levers describe a mixture rather than a decision.`,
package/src/i18n/es.ts CHANGED
@@ -1165,6 +1165,28 @@ ${bold('EJEMPLOS')}
1165
1165
  `--what-if no conoce "${value}". Modelos con precio: ${available}. Añádelo con --pricing si tienes sus tarifas.`,
1166
1166
  badGzip: (file, detail) =>
1167
1167
  `${file} está comprimido con gzip y no se ha podido descomprimir: ${detail}. Leer el resto y no decir nada daría una factura a la que le falta lo que hubiera en ese fichero, así que se para aquí. Revisa el fichero, o sácalo del directorio.`,
1168
+ dryRunHeading: () => 'Qué puede responder este registro — no se ha producido factura',
1169
+ dryRunParsed: (parsed, skipped) =>
1170
+ `${parsed} registros se leen; ${skipped} líneas no se pudieron leer. Un dry run no valora nada: el punto es sobre qué se apoyarían los gates que vas a cablear.`,
1171
+ dryRunUnpriced: (models) =>
1172
+ `Modelos que la tabla de precios no conoce: ${models}. Sus tokens se leen; sus dólares necesitan un overlay --pricing.`,
1173
+ dryRunTotals: () => 'La factura en sí: totales, desglose por modelo, economía de caché, las palancas.',
1174
+ dryRunLabels: (share) =>
1175
+ `Hallazgos por carga y presupuestos por etiqueta — "label" en el ${share} de los registros.`,
1176
+ dryRunClock: (share) =>
1177
+ `El periodo, la forma por día y por hora, --max-day-usd, --since/--until — marca de tiempo en el ${share} de los registros.`,
1178
+ dryRunSessions: (share) =>
1179
+ `Crecimiento de conversación, coste por conversación, --max-session-usd — sesión en el ${share} de los registros. Se agrupa por ella, nunca se imprime.`,
1180
+ dryRunStopReason: (share) =>
1181
+ `Respuestas truncadas y su factura de reintentos — razón de parada en el ${share} de los registros.`,
1182
+ dryRunCacheTtl: (ttl, writes) =>
1183
+ `Veredictos de caché resueltos — el desglose "cache_creation" en ${ttl} de ${writes} registros que escriben caché.`,
1184
+ dryRunNoCacheTraffic: () =>
1185
+ 'Veredictos de caché: nada escribió en la caché en este registro, así que no hay nada que resolver — no es un campo ausente.',
1186
+ dryRunFooter: () =>
1187
+ 'Una ✗ no es un defecto del registro; es un hallazgo que este registro aún no puede sostener. La receta de registro del README lleva todos los campos de arriba.',
1188
+ dryRunNoGates: () =>
1189
+ '--dry-run no produce factura, así que un gate a su lado saldría en verde sin haber juzgado nada. Corre los gates sin --dry-run.',
1168
1190
  coverageHeading: () => 'Lo que este registro todavía no puede responder',
1169
1191
  needsLabel: (seen) =>
1170
1192
  `"label" en ${seen} registros: sin él todas las cargas son una sola fila, así que no hay gasto por carga, ni zoom, y las palancas describen una mezcla en vez de una decisión.`,
package/src/i18n/types.ts CHANGED
@@ -916,6 +916,23 @@ export interface CliMessages {
916
916
  * a skip: a total quietly missing a day is the failure this refuses.
917
917
  */
918
918
  badGzip(file: string, detail: string): string;
919
+ /**
920
+ * `--dry-run`: readiness per capability, no bill. The question somebody
921
+ * has before wiring CI, answered without making them read a report to
922
+ * discover a missing field.
923
+ */
924
+ dryRunHeading(): string;
925
+ dryRunParsed(parsed: string, skipped: string): string;
926
+ dryRunUnpriced(models: string): string;
927
+ dryRunTotals(): string;
928
+ dryRunLabels(share: string): string;
929
+ dryRunClock(share: string): string;
930
+ dryRunSessions(share: string): string;
931
+ dryRunStopReason(share: string): string;
932
+ dryRunCacheTtl(ttl: string, writes: string): string;
933
+ dryRunNoCacheTraffic(): string;
934
+ dryRunFooter(): string;
935
+ dryRunNoGates(): string;
919
936
  coverageHeading(): string;
920
937
  needsLabel(seen: string): string;
921
938
  needsSession(seen: string): string;
package/src/index.ts CHANGED
@@ -440,7 +440,7 @@ const COMMAND_FLAGS: Record<string, string[]> = {
440
440
  ],
441
441
  check: ['max-tokens', 'level', 'exact-tokens', 'markdown-out', 'baseline'],
442
442
  baseline: ['model', 'calls', 'output-tokens', 'cache-hit-rate', 'batch', 'exact-tokens', 'out', 'o'],
443
- 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'],
443
+ 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'],
444
444
  route: ['prompt-file', 'cases', 'label', 'concurrency', 'json', 'yes', 'pricing', 'pricing-live'],
445
445
  eval: ['cases', 'level', 'concurrency', 'export', 'out', 'o', 'model'],
446
446
  prune: ['cases', 'concurrency', 'json', 'yes'],
@@ -2146,6 +2146,52 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2146
2146
  const n = (value: number): string => value.toLocaleString(t.numberLocale);
2147
2147
  const pct = (share: number): string => `${(share * 100).toFixed(1)}%`;
2148
2148
 
2149
+ /**
2150
+ * `--dry-run`: what this log could and could not answer, and no bill.
2151
+ *
2152
+ * The question somebody has *before* wiring Trazum into CI is not "what did
2153
+ * we spend" but "will this log support the gates I want" — and answering it
2154
+ * with a full report makes them read a bill to find out a field is missing.
2155
+ * This path states readiness per capability and produces no dollar figure
2156
+ * at all, so nothing here can be mistaken for spend. It also refuses to
2157
+ * coexist with the gates: a gate over a report that was never produced
2158
+ * would exit green having judged nothing.
2159
+ */
2160
+ if (boolFlag(args, 'dry-run')) {
2161
+ const gateFlags = ['max-usd', 'max-growth-usd', 'max-cache-loss-usd', 'max-day-usd', 'max-session-usd'];
2162
+ if (gateFlags.some((flag) => typeof args.flags.get(flag) === 'string')) {
2163
+ throw new Error(t.profile.dryRunNoGates());
2164
+ }
2165
+ const cov = report.fieldCoverage;
2166
+ const parsed = cov.parsed;
2167
+ console.log(c.bold(t.profile.dryRunHeading()));
2168
+ console.log(` ${wrap(t.profile.dryRunParsed(n(parsed), n(report.skippedLines.length)), 74, ' ')}`);
2169
+ if (report.unpricedModels.length > 0) {
2170
+ console.log(` ${c.yellow('!')} ${wrap(t.profile.dryRunUnpriced(report.unpricedModels.join(', ')), 74, ' ')}`);
2171
+ }
2172
+ console.log();
2173
+ const can = (ok: boolean, line: string): void => {
2174
+ console.log(` ${ok ? c.green('✓') : c.yellow('✗')} ${wrap(line, 74, ' ')}`);
2175
+ };
2176
+ const share = (count: number): string => (parsed > 0 ? pct(count / parsed) : '0%');
2177
+ can(parsed > 0, t.profile.dryRunTotals());
2178
+ can(cov.label > 0, t.profile.dryRunLabels(share(cov.label)));
2179
+ can(cov.ts > 0, t.profile.dryRunClock(share(cov.ts)));
2180
+ can(cov.session > 0, t.profile.dryRunSessions(share(cov.session)));
2181
+ can(cov.stopReason > 0, t.profile.dryRunStopReason(share(cov.stopReason)));
2182
+ // "No cache traffic" is not a missing field: the split can only exist on
2183
+ // records that wrote, and a log that never wrote has nothing to record.
2184
+ if (cov.cacheWrites > 0) {
2185
+ can(cov.cacheTtl > 0, t.profile.dryRunCacheTtl(n(cov.cacheTtl), n(cov.cacheWrites)));
2186
+ } else {
2187
+ console.log(` ${c.dim('·')} ${wrap(t.profile.dryRunNoCacheTraffic(), 74, ' ')}`);
2188
+ }
2189
+ console.log();
2190
+ console.log(` ${c.dim(wrap(t.profile.dryRunFooter(), 74, ' '))}`);
2191
+ if (parsed === 0) process.exitCode = 1;
2192
+ return;
2193
+ }
2194
+
2149
2195
  /**
2150
2196
  * The previous log, loaded before the output paths split so the growth gate
2151
2197
  * exists under `--json` too — a CI step reads the JSON and trusts the exit