@trazum/cli 1.33.0 → 1.34.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.33.0",
3
+ "version": "1.34.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.33.0"
40
+ "@trazum/core": "1.34.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.2.0",
package/src/i18n/en.ts CHANGED
@@ -345,6 +345,11 @@ ${bold('CONFIG FILE')}
345
345
  spend { "maxUsd": 200, "byLabel": { "chat": 40 } } — money budgets for
346
346
  "trazum profile", in dollars. A budgeted label with no calls in
347
347
  the log is reported as not measured, never as a pass
348
+ waive [{ "gate": "maxUsd", "reason": "August migration", "until":
349
+ "2026-09-15" }] — a gate failure decided about, on the record.
350
+ All three fields required: a waiver with no end date is a
351
+ finding deleted with extra steps. Waived failures still print,
352
+ and the day the waiver expires the gate fails again
348
353
 
349
354
  Flags beat the config; the config beats the defaults. Budgets resolve to the
350
355
  most specific matching pattern — most literal characters wins. A boolean the
@@ -1199,6 +1204,10 @@ ${bold('EXAMPLES')}
1199
1204
  `The 95th percentile is ${ratio}x the median there: most conversations are cheap and a few are not, which is a tail a quota can catch. Where median and p95 sit close together the workload is simply expensive and there is no tail to hunt.`,
1200
1205
  sessionSpendOnly: (sessions, max) =>
1201
1206
  `${sessions} ${sessions === '1' ? 'conversation' : 'conversations'} in this log; the most expensive cost ${max}. Too few per workload for a percentile — a maximum is a fact at any count, and it is the figure --max-session-usd judges.`,
1207
+ waiveActive: (gate, reason, until, daysLeft) =>
1208
+ `WAIVED — the ${gate} failure above is on the record and silenced until ${until} (${daysLeft} days left): "${reason}". The bill still counts it; only the exit code is quiet, and the day the waiver expires this gate fails again.`,
1209
+ waiveExpired: (gate, until, reason) =>
1210
+ `The waiver on ${gate} expired on ${until} and no longer silences anything. It was written for: "${reason}". Renew it with a new date and a current reason, or fix what it was covering — an expired waiver left in place is a finding deleted with extra steps.`,
1202
1211
  gateLargest: (label, model, usd, share) =>
1203
1212
  `Most of it is ${label} on ${model}: ${usd}, ${share} of the bill. That is where the money is, not necessarily where the fix is.`,
1204
1213
  gateLever: (label, action, saving, overage, covers) =>
package/src/i18n/es.ts CHANGED
@@ -348,6 +348,12 @@ ${bold('FICHERO DE CONFIGURACIÓN')}
348
348
  spend { "maxUsd": 200, "byLabel": { "chat": 40 } } — presupuestos en
349
349
  dólares para "trazum profile". Una etiqueta con presupuesto y sin
350
350
  llamadas se informa como no medida, nunca como aprobada
351
+ waive [{ "gate": "maxUsd", "reason": "migración de agosto", "until":
352
+ "2026-09-15" }] — un fallo de gate sobre el que se ha decidido,
353
+ registrado. Los tres campos son obligatorios: un waiver sin
354
+ fecha de fin es un hallazgo borrado con pasos extra. Los fallos
355
+ waived se siguen imprimiendo, y el día que caduca el gate
356
+ vuelve a fallar
351
357
 
352
358
  Las opciones ganan al config; el config gana a los valores por defecto. Los
353
359
  presupuestos se resuelven con el patrón más específico que encaje — gana el
@@ -1214,6 +1220,10 @@ ${bold('EJEMPLOS')}
1214
1220
  `El percentil 95 es ${ratio}x la mediana ahí: casi todas las conversaciones son baratas y unas pocas no, y esa es una cola que una cuota puede cazar. Cuando mediana y p95 quedan cerca, la carga es cara sin más y no hay cola que perseguir.`,
1215
1221
  sessionSpendOnly: (sessions, max) =>
1216
1222
  `${sessions} ${sessions === '1' ? 'conversación' : 'conversaciones'} en este registro; la más cara costó ${max}. Demasiado pocas por carga para un percentil — un máximo es un hecho con cualquier recuento, y es la cifra que juzga --max-session-usd.`,
1223
+ waiveActive: (gate, reason, until, daysLeft) =>
1224
+ `WAIVED — el fallo de ${gate} de arriba queda registrado y silenciado hasta ${until} (${daysLeft} días restantes): "${reason}". La factura lo sigue contando; solo el código de salida calla, y el día que caduque el waiver este gate vuelve a fallar.`,
1225
+ waiveExpired: (gate, until, reason) =>
1226
+ `El waiver de ${gate} caducó el ${until} y ya no silencia nada. Se escribió por: "${reason}". Renuévalo con fecha nueva y razón vigente, o arregla lo que cubría — un waiver caducado dejado ahí es un hallazgo borrado con pasos extra.`,
1217
1227
  gateLargest: (label, model, usd, share) =>
1218
1228
  `La mayor parte es ${label} en ${model}: ${usd}, el ${share} de la factura. Ahí está el dinero, no necesariamente el arreglo.`,
1219
1229
  gateLever: (label, action, saving, overage, covers) =>
package/src/i18n/types.ts CHANGED
@@ -784,6 +784,13 @@ export interface CliMessages {
784
784
  * computed. Nothing here recommends: whether the cheaper model can do the
785
785
  * work is the reader's to judge, and the copy says so.
786
786
  */
787
+ /**
788
+ * Findings as policy. An active waiver silences one gate's exit code, on
789
+ * the record, with the reason and the days left; an expired one silences
790
+ * nothing and says so beside the failure it used to cover.
791
+ */
792
+ waiveActive(gate: string, reason: string, until: string, daysLeft: string): string;
793
+ waiveExpired(gate: string, until: string, reason: string): string;
787
794
  gateLargest(label: string, model: string, usd: string, share: string): string;
788
795
  gateLever(label: string, action: string, saving: string, overage: string, covers: boolean): string;
789
796
  /**
package/src/index.ts CHANGED
@@ -2312,6 +2312,46 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2312
2312
  */
2313
2313
  const gateVerdicts: string[] = [];
2314
2314
  let gateFailed = false;
2315
+ /**
2316
+ * Findings as policy. A waiver silences one gate's exit code for a bounded
2317
+ * time, on the record: the failure still prints (waived is shown as
2318
+ * waived, never hidden — the bill still counts it), the reason and the
2319
+ * days left print beside it, and the day the waiver expires the gate
2320
+ * fails again louder, naming the date and the reason somebody wrote.
2321
+ * That expiry is the entire mechanism by which a waiver stays a decision
2322
+ * instead of becoming a habit.
2323
+ */
2324
+ const waiverFor = (gate: string): { entry: { gate: string; reason: string; until: string }; expired: boolean } | null => {
2325
+ const entry = (config.waive ?? []).find((w) => w.gate === gate);
2326
+ if (entry === undefined) return null;
2327
+ // The waiver covers its named day whole: expiry begins the next UTC day.
2328
+ const expiresMs = Date.parse(`${entry.until}T00:00:00Z`) + 86_400_000;
2329
+ return { entry, expired: Date.now() >= expiresMs };
2330
+ };
2331
+ /**
2332
+ * Applies a waiver to one failing gate. Returns true when the failure is
2333
+ * silenced — the caller skips its exitCode — and prints the record either
2334
+ * way, because a waived failure that vanished from the output would be a
2335
+ * finding deleted with extra steps.
2336
+ */
2337
+ const waived = (gate: string): boolean => {
2338
+ const found = waiverFor(gate);
2339
+ if (found === null) return false;
2340
+ if (found.expired) {
2341
+ console.error(
2342
+ c.red(t.profile.waiveExpired(gate, found.entry.until, found.entry.reason)),
2343
+ );
2344
+ return false;
2345
+ }
2346
+ const daysLeft = Math.max(
2347
+ 0,
2348
+ Math.ceil((Date.parse(`${found.entry.until}T00:00:00Z`) + 86_400_000 - Date.now()) / 86_400_000),
2349
+ );
2350
+ console.error(
2351
+ c.yellow(t.profile.waiveActive(gate, found.entry.reason, found.entry.until, String(daysLeft))),
2352
+ );
2353
+ return true;
2354
+ };
2315
2355
  const applyGates = (): void => {
2316
2356
  /**
2317
2357
  * Before any verdict: whether the gated figure is the whole bill. A gate
@@ -2358,7 +2398,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2358
2398
  }
2359
2399
  if (usd > limit) {
2360
2400
  console.error(c.red(t.profile.labelBudgetFailed(label, formatUsd(usd), formatUsd(limit))));
2361
- process.exitCode = 1;
2401
+ if (!waived(`byLabel:${label}`)) process.exitCode = 1;
2362
2402
  } else {
2363
2403
  console.error(c.dim(t.profile.labelBudgetOk(label, formatUsd(usd), formatUsd(limit))));
2364
2404
  }
@@ -2431,7 +2471,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2431
2471
  * copy says so.
2432
2472
  */
2433
2473
  explainFailure(report.total.totalUsd - maxUsd);
2434
- process.exitCode = 1;
2474
+ if (!waived('maxUsd')) process.exitCode = 1;
2435
2475
  } else {
2436
2476
  console.error(c.dim(t.profile.maxUsdOk(formatUsd(report.total.totalUsd), formatUsd(maxUsd))));
2437
2477
  explainMargin(report.total.totalUsd, maxUsd);
@@ -2465,10 +2505,13 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2465
2505
  ),
2466
2506
  ),
2467
2507
  );
2508
+ // Deliberately unwaivable: this failure is "the comparison cannot
2509
+ // be made", and a waiver on unmeasurability would be a decision to
2510
+ // stop measuring — not a budget decision with an end date.
2468
2511
  process.exitCode = 1;
2469
2512
  } else if (againstDelta > maxGrowth) {
2470
2513
  console.error(c.red(t.profile.maxGrowthUsdFailed(formatSignedUsd(againstDelta), formatUsd(maxGrowth))));
2471
- process.exitCode = 1;
2514
+ if (!waived('maxGrowthUsd')) process.exitCode = 1;
2472
2515
  }
2473
2516
  }
2474
2517
  /**
@@ -2486,7 +2529,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2486
2529
  console.error(
2487
2530
  c.red(t.profile.maxCacheLossFailed(formatUsd(gateCache.deltaUsd), formatUsd(maxLoss))),
2488
2531
  );
2489
- process.exitCode = 1;
2532
+ if (!waived('maxCacheLossUsd')) process.exitCode = 1;
2490
2533
  } else if (gateCache.worstCaseDeltaUsd > maxLoss) {
2491
2534
  console.error(
2492
2535
  c.red(
@@ -2497,7 +2540,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2497
2540
  ),
2498
2541
  ),
2499
2542
  );
2500
- process.exitCode = 1;
2543
+ if (!waived('maxCacheLossUsd')) process.exitCode = 1;
2501
2544
  } else {
2502
2545
  console.error(
2503
2546
  c.dim(t.profile.maxCacheLossOk(formatUsd(Math.max(0, gateCache.worstCaseDeltaUsd)), formatUsd(maxLoss))),
@@ -2546,7 +2589,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2546
2589
  c.red(`${t.profile.maxDayFailed(worst.day, formatUsd(worst.usd), formatUsd(maxDay))}${suspect}`),
2547
2590
  );
2548
2591
  explainFailure(worst.usd - maxDay, { namesLargest: true });
2549
- process.exitCode = 1;
2592
+ if (!waived('maxDayUsd')) process.exitCode = 1;
2550
2593
  } else {
2551
2594
  console.error(c.dim(t.profile.maxDayOk(worst.day, formatUsd(worst.usd), formatUsd(maxDay))));
2552
2595
  explainMargin(worst.usd, maxDay);
@@ -2588,7 +2631,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2588
2631
  c.red(t.profile.maxSessionFailed(formatUsd(report.sessionSpend.maxUsd), formatUsd(maxSession), n(report.sessionSpend.sessions))),
2589
2632
  );
2590
2633
  explainFailure(report.sessionSpend.maxUsd - maxSession);
2591
- process.exitCode = 1;
2634
+ if (!waived('maxSessionUsd')) process.exitCode = 1;
2592
2635
  } else {
2593
2636
  console.error(
2594
2637
  c.dim(t.profile.maxSessionOk(formatUsd(report.sessionSpend.maxUsd), formatUsd(maxSession), n(report.sessionSpend.sessions))),