@trazum/cli 1.50.3 → 1.50.4

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.50.3",
3
+ "version": "1.50.4",
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.50.3"
40
+ "@trazum/core": "1.50.4"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.2.0",
package/src/i18n/en.ts CHANGED
@@ -597,6 +597,13 @@ ${bold('CONFIG FILE')}
597
597
  spend { "maxUsd": 200, "byLabel": { "chat": 40 } } — money budgets for
598
598
  "trazum profile", in dollars. A budgeted label with no calls in
599
599
  the log is reported as not measured, never as a pass
600
+ outcomes { "values": ["resolved", "escalated"], "success": ["resolved"] } —
601
+ your own vocabulary for what happened, and which of it counts as
602
+ a win. Both required: which words mean success is a judgement
603
+ about your product rather than your bill, and this tool has no
604
+ standing to make it. Use [] if none of them are successes. A
605
+ value in a log that "values" never declares is named as
606
+ undeclared, never counted as a failure
600
607
  waive [{ "gate": "maxUsd", "reason": "August migration", "until":
601
608
  "2026-09-15" }] — a gate failure decided about, on the record.
602
609
  All three fields required: a waiver with no end date is a
@@ -1631,6 +1638,25 @@ ${bold('EXAMPLES')}
1631
1638
  coverageHeading: () => 'What this log cannot answer yet',
1632
1639
  needsLabel: (seen) =>
1633
1640
  `"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.`,
1641
+ needsOutcome: (seen) =>
1642
+ `an "outcome" — ${seen}. The one field that changes what every other figure here means: without it this tool can say a workload got 40% cheaper and cannot say whether it stopped working. Record your own word for what happened and declare the vocabulary under "outcomes".`,
1643
+ dryRunOutcomes: (share) =>
1644
+ `cost per outcome and a success rate (${share} of records carry an "outcome")`,
1645
+ outcomeHeading: () => 'Outcomes',
1646
+ outcomeRate: (rate, ofUsd) =>
1647
+ `${rate} of ${ofUsd} in declared outcomes succeeded \u2014 by spend rather than by call, because the two diverge exactly when the expensive half is the half that fails.`,
1648
+ outcomeNoRate: (why) =>
1649
+ why === 'nothing-recorded'
1650
+ ? 'No success rate: nothing in this log recorded an outcome. That is not a rate of zero \u2014 a rate of zero is a real and terrible measurement, and this is nobody having told us.'
1651
+ : 'No success rate: "outcomes.success" declares no values, so nothing here counts as one. A legitimate thing to declare, and it means the rate is not this tool\u2019s to compute.',
1652
+ outcomeUnrecorded: (share, usd) =>
1653
+ `${share} of the bill (${usd}) carried no outcome, and is in neither half of the rate above.`,
1654
+ outcomeUndeclared: (values) =>
1655
+ `Not declared in "outcomes.values": ${values}. Named rather than counted as failures \u2014 a typo in an exporter should look like a typo, not like a product regression.`,
1656
+ outcomeColumns: { outcome: 'outcome', calls: 'calls', spend: 'spend' },
1657
+ verdictSuccess: () => 'success',
1658
+ verdictOther: () => '\u2014',
1659
+ verdictUndeclared: () => 'undeclared',
1634
1660
  needsSession: (seen) =>
1635
1661
  `"session" on ${seen} records: without it there is no conversation growth, no per-conversation cost, and no cache-TTL fit. It is grouped by and never printed.`,
1636
1662
  needsTs: (seen) =>
package/src/i18n/es.ts CHANGED
@@ -617,6 +617,13 @@ ${bold('FICHERO DE CONFIGURACIÓN')}
617
617
  spend { "maxUsd": 200, "byLabel": { "chat": 40 } } — presupuestos en
618
618
  dólares para "trazum profile". Una etiqueta con presupuesto y sin
619
619
  llamadas se informa como no medida, nunca como aprobada
620
+ outcomes { "values": ["resolved", "escalated"], "success": ["resolved"] } —
621
+ tu propio vocabulario para lo que pasó, y cuál de él cuenta como
622
+ acierto. Los dos son obligatorios: qué palabras significan éxito
623
+ es un juicio sobre tu producto y no sobre tu factura, y esta
624
+ herramienta no tiene potestad para hacerlo. Usa [] si ninguna lo
625
+ es. Un valor en un log que "values" no declare se nombra como
626
+ sin declarar, nunca se cuenta como fallo
620
627
  waive [{ "gate": "maxUsd", "reason": "migración de agosto", "until":
621
628
  "2026-09-15" }] — un fallo de gate sobre el que se ha decidido,
622
629
  registrado. Los tres campos son obligatorios: un waiver sin
@@ -1664,6 +1671,25 @@ ${bold('EJEMPLOS')}
1664
1671
  coverageHeading: () => 'Lo que este registro todavía no puede responder',
1665
1672
  needsLabel: (seen) =>
1666
1673
  `"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.`,
1674
+ needsOutcome: (seen) =>
1675
+ `un "outcome" \u2014 ${seen}. El \u00fanico campo que cambia lo que significa cualquier otra cifra de aqu\u00ed: sin \u00e9l esta herramienta puede decir que una carga baj\u00f3 un 40% y no puede decir si dej\u00f3 de funcionar. Registra tu propia palabra para lo que pas\u00f3 y declara el vocabulario en "outcomes".`,
1676
+ dryRunOutcomes: (share) =>
1677
+ `coste por resultado y una tasa de \u00e9xito (${share} de los registros llevan "outcome")`,
1678
+ outcomeHeading: () => 'Resultados',
1679
+ outcomeRate: (rate, ofUsd) =>
1680
+ `${rate} de ${ofUsd} en resultados declarados tuvo \u00e9xito \u2014 por gasto y no por llamada, porque las dos cifras divergen justo cuando la mitad cara es la que falla.`,
1681
+ outcomeNoRate: (why) =>
1682
+ why === 'nothing-recorded'
1683
+ ? 'Sin tasa de \u00e9xito: nada en este log registr\u00f3 un resultado. Eso no es una tasa de cero \u2014 una tasa de cero es una medici\u00f3n real y p\u00e9sima, y esto es que nadie nos lo ha dicho.'
1684
+ : 'Sin tasa de \u00e9xito: "outcomes.success" no declara ning\u00fan valor, as\u00ed que aqu\u00ed nada cuenta como \u00e9xito. Es leg\u00edtimo declararlo as\u00ed, y significa que la tasa no le corresponde calcularla a esta herramienta.',
1685
+ outcomeUnrecorded: (share, usd) =>
1686
+ `${share} de la factura (${usd}) no llev\u00f3 resultado, y no est\u00e1 en ninguna de las dos mitades de la tasa de arriba.`,
1687
+ outcomeUndeclared: (values) =>
1688
+ `No declarados en "outcomes.values": ${values}. Nombrados en vez de contados como fallos \u2014 una errata en un exportador debe parecer una errata, no una regresi\u00f3n del producto.`,
1689
+ outcomeColumns: { outcome: 'resultado', calls: 'llamadas', spend: 'gasto' },
1690
+ verdictSuccess: () => '\u00e9xito',
1691
+ verdictOther: () => '\u2014',
1692
+ verdictUndeclared: () => 'sin declarar',
1667
1693
  needsSession: (seen) =>
1668
1694
  `"session" en ${seen} registros: sin él no hay crecimiento de conversación, ni coste por conversación, ni encaje del TTL de caché. Se agrupa por él y nunca se imprime.`,
1669
1695
  needsTs: (seen) =>
package/src/i18n/types.ts CHANGED
@@ -1121,6 +1121,17 @@ export interface CliMessages {
1121
1121
  coverageHeading(): string;
1122
1122
  needsLabel(seen: string): string;
1123
1123
  needsSession(seen: string): string;
1124
+ needsOutcome(seen: string): string;
1125
+ dryRunOutcomes(share: string): string;
1126
+ outcomeHeading(): string;
1127
+ outcomeRate(rate: string, ofUsd: string): string;
1128
+ outcomeNoRate(why: string): string;
1129
+ outcomeUnrecorded(share: string, usd: string): string;
1130
+ outcomeUndeclared(values: string): string;
1131
+ outcomeColumns: { outcome: string; calls: string; spend: string };
1132
+ verdictSuccess(): string;
1133
+ verdictOther(): string;
1134
+ verdictUndeclared(): string;
1124
1135
  needsTs(seen: string): string;
1125
1136
  needsStopReason(seen: string): string;
1126
1137
  needsCacheTtl(seen: string): string;
package/src/index.ts CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  DEFAULT_USAGE,
40
40
  budgetPositions,
41
41
  conform,
42
+ outcomeReport,
42
43
  FAILURE_POLICIES,
43
44
  detectFromSource,
44
45
  matchLocale,
@@ -4585,6 +4586,7 @@ async function commandProfile(
4585
4586
  can(cov.label > 0, t.profile.dryRunLabels(share(cov.label)));
4586
4587
  can(cov.ts > 0, t.profile.dryRunClock(share(cov.ts)));
4587
4588
  can(cov.session > 0, t.profile.dryRunSessions(share(cov.session)));
4589
+ can(cov.outcome > 0, t.profile.dryRunOutcomes(share(cov.outcome)));
4588
4590
  can(cov.stopReason > 0, t.profile.dryRunStopReason(share(cov.stopReason)));
4589
4591
  // "No cache traffic" is not a missing field: the split can only exist on
4590
4592
  // records that wrote, and a log that never wrote has nothing to record.
@@ -6416,6 +6418,97 @@ async function commandProfile(
6416
6418
  * section at all, because a paragraph of things that are fine is the
6417
6419
  * paragraph readers learn to skip.
6418
6420
  */
6421
+ /**
6422
+ * Outcomes — the counterpart, where somebody recorded one.
6423
+ *
6424
+ * Printed above the coverage section rather than below it, because when this
6425
+ * section is present it is the most valuable thing on the page: every other
6426
+ * figure in this report is a cost, and this is the only one that says what
6427
+ * the money bought.
6428
+ *
6429
+ * Silent when nothing recorded an outcome. The coverage section below
6430
+ * already names the missing field and what it would unlock, and printing an
6431
+ * empty Outcomes heading above it would be the same sentence twice.
6432
+ */
6433
+ {
6434
+ const outcomes = outcomeReport(report.outcomeTally, config.outcomes ?? null);
6435
+ if (outcomes.coverage.recorded > 0) {
6436
+ console.log();
6437
+ console.log(c.bold(t.profile.outcomeHeading()));
6438
+
6439
+ const col = t.profile.outcomeColumns;
6440
+ const rows = [...outcomes.slices, ...outcomes.undeclared].map((slice) => ({
6441
+ value: slice.value,
6442
+ verdict:
6443
+ slice.verdict === 'success'
6444
+ ? t.profile.verdictSuccess()
6445
+ : slice.verdict === 'undeclared'
6446
+ ? t.profile.verdictUndeclared()
6447
+ : t.profile.verdictOther(),
6448
+ calls: n(slice.calls),
6449
+ spend: formatUsd(slice.usd),
6450
+ }));
6451
+ const w = {
6452
+ value: Math.max(...rows.map((r) => r.value.length), col.outcome.length),
6453
+ verdict: Math.max(...rows.map((r) => r.verdict.length), 0),
6454
+ calls: Math.max(...rows.map((r) => r.calls.length), col.calls.length),
6455
+ spend: Math.max(...rows.map((r) => r.spend.length), col.spend.length),
6456
+ };
6457
+ console.log(
6458
+ c.dim(
6459
+ ` ${col.outcome.padEnd(w.value)} ${''.padEnd(w.verdict)} ` +
6460
+ `${col.calls.padStart(w.calls)} ${col.spend.padStart(w.spend)}`,
6461
+ ),
6462
+ );
6463
+ for (const row of rows) {
6464
+ const tint =
6465
+ row.verdict === t.profile.verdictUndeclared()
6466
+ ? c.yellow
6467
+ : row.verdict === t.profile.verdictSuccess()
6468
+ ? c.green
6469
+ : c.dim;
6470
+ console.log(
6471
+ ` ${row.value.padEnd(w.value)} ${tint(row.verdict.padEnd(w.verdict))} ` +
6472
+ `${row.calls.padStart(w.calls)} ${row.spend.padStart(w.spend)}`,
6473
+ );
6474
+ }
6475
+
6476
+ console.log();
6477
+ if (outcomes.successShareOfRecordedUsd !== null) {
6478
+ const declaredUsd = outcomes.slices.reduce((sum, slice) => sum + slice.usd, 0);
6479
+ console.log(
6480
+ ` ${wrap(t.profile.outcomeRate(pct(outcomes.successShareOfRecordedUsd), formatUsd(declaredUsd)), 74, ' ')}`,
6481
+ );
6482
+ } else if (outcomes.noRate !== null) {
6483
+ console.log(` ${c.dim(wrap(t.profile.outcomeNoRate(outcomes.noRate), 74, ' '))}`);
6484
+ }
6485
+
6486
+ // What the rate does not cover, every time it is printed. A rate over a
6487
+ // twelfth of the bill is a rate about a twelfth of the bill.
6488
+ if (outcomes.coverage.unrecordedUsd > 0 && report.total.totalUsd > 0) {
6489
+ console.log(
6490
+ ` ${c.yellow('!')} ${wrap(
6491
+ t.profile.outcomeUnrecorded(
6492
+ pct(outcomes.coverage.unrecordedUsd / report.total.totalUsd),
6493
+ formatUsd(outcomes.coverage.unrecordedUsd),
6494
+ ),
6495
+ 74,
6496
+ ' ',
6497
+ )}`,
6498
+ );
6499
+ }
6500
+ if (outcomes.undeclared.length > 0) {
6501
+ console.log(
6502
+ ` ${c.yellow('!')} ${wrap(
6503
+ t.profile.outcomeUndeclared(outcomes.undeclared.map((s) => s.value).join(', ')),
6504
+ 74,
6505
+ ' ',
6506
+ )}`,
6507
+ );
6508
+ }
6509
+ }
6510
+ }
6511
+
6419
6512
  const coverage = report.fieldCoverage;
6420
6513
  if (coverage.parsed > 0) {
6421
6514
  const missing: string[] = [];
@@ -6426,6 +6519,14 @@ async function commandProfile(
6426
6519
  if (coverage.session < coverage.parsed) {
6427
6520
  missing.push(t.profile.needsSession(partial(coverage.session)));
6428
6521
  }
6522
+ /**
6523
+ * Listed first among the missing when it is missing entirely, because it
6524
+ * is the one field that changes what every other figure here *means*. The
6525
+ * rest sharpen a cost; this one gives it a counterpart.
6526
+ */
6527
+ if (coverage.outcome < coverage.parsed) {
6528
+ missing.push(t.profile.needsOutcome(partial(coverage.outcome)));
6529
+ }
6429
6530
  if (coverage.ts < coverage.parsed) {
6430
6531
  missing.push(t.profile.needsTs(partial(coverage.ts)));
6431
6532
  }