@trazum/cli 1.50.4 → 1.50.5

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.4",
3
+ "version": "1.50.5",
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.4"
40
+ "@trazum/core": "1.50.5"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.2.0",
package/src/i18n/en.ts CHANGED
@@ -1653,6 +1653,30 @@ ${bold('EXAMPLES')}
1653
1653
  `${share} of the bill (${usd}) carried no outcome, and is in neither half of the rate above.`,
1654
1654
  outcomeUndeclared: (values) =>
1655
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
+ perOutcomeHeading: () => 'What an outcome costs',
1657
+ perOutcomeRow: (key, perCall, perOutcome, coverage) => `${key} ${perCall} ${perOutcome} ${coverage}`,
1658
+ perOutcomeColumns: {
1659
+ workload: 'workload',
1660
+ perCall: 'per call',
1661
+ perOutcome: 'per success',
1662
+ recorded: 'recorded',
1663
+ },
1664
+ perOutcomeWithheld: (why, successes, coverage) =>
1665
+ why === 'too-few-outcomes'
1666
+ ? `${successes} so far`
1667
+ : why === 'too-little-coverage'
1668
+ ? `${coverage} covered`
1669
+ : why === 'no-successes-recorded'
1670
+ ? 'none succeeded'
1671
+ : why === 'nothing-recorded'
1672
+ ? 'not recorded'
1673
+ : 'no vocabulary',
1674
+ perOutcomeNumerator: () =>
1675
+ 'Per success divides the spend on calls that recorded an outcome, never the whole bill \u2014 dividing everything would charge your uninstrumented traffic to your measured successes and report a figure too high by exactly the uncovered share, silently, in the direction that gets a working feature killed. "recorded" is what share of each workload\u2019s spend the figure covers.',
1676
+ perOutcomeDisagreement: (key, callRank, outcomeRank) =>
1677
+ `${key} is #${callRank} by cost per call and #${outcomeRank} by cost per success.`,
1678
+ perOutcomeBothOrders: () =>
1679
+ 'Cheapest per call and cheapest per success are different orders, and both are printed rather than one being picked. A workload can move up one while moving down the other, and somebody optimising on the first number would be moving the wrong one.',
1656
1680
  outcomeColumns: { outcome: 'outcome', calls: 'calls', spend: 'spend' },
1657
1681
  verdictSuccess: () => 'success',
1658
1682
  verdictOther: () => '\u2014',
package/src/i18n/es.ts CHANGED
@@ -1686,6 +1686,30 @@ ${bold('EJEMPLOS')}
1686
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
1687
  outcomeUndeclared: (values) =>
1688
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
+ perOutcomeHeading: () => 'Lo que cuesta un resultado',
1690
+ perOutcomeRow: (key, perCall, perOutcome, coverage) => `${key} ${perCall} ${perOutcome} ${coverage}`,
1691
+ perOutcomeColumns: {
1692
+ workload: 'carga',
1693
+ perCall: 'por llamada',
1694
+ perOutcome: 'por acierto',
1695
+ recorded: 'cubierto',
1696
+ },
1697
+ perOutcomeWithheld: (why, successes, coverage) =>
1698
+ why === 'too-few-outcomes'
1699
+ ? `${successes} hasta ahora`
1700
+ : why === 'too-little-coverage'
1701
+ ? `${coverage} cubierto`
1702
+ : why === 'no-successes-recorded'
1703
+ ? 'ninguno acert\u00f3'
1704
+ : why === 'nothing-recorded'
1705
+ ? 'sin registrar'
1706
+ : 'sin vocabulario',
1707
+ perOutcomeNumerator: () =>
1708
+ 'Por acierto divide el gasto de las llamadas que registraron un resultado, nunca la factura entera \u2014 dividirlo todo cargar\u00eda tu tr\u00e1fico sin instrumentar a tus aciertos medidos y dar\u00eda una cifra alta por exactamente la parte no cubierta, en silencio, en la direcci\u00f3n que mata una funci\u00f3n que s\u00ed funciona. "cubierto" es qu\u00e9 parte del gasto de cada carga cubre la cifra.',
1709
+ perOutcomeDisagreement: (key, callRank, outcomeRank) =>
1710
+ `${key} es el #${callRank} por coste por llamada y el #${outcomeRank} por coste por acierto.`,
1711
+ perOutcomeBothOrders: () =>
1712
+ 'M\u00e1s barato por llamada y m\u00e1s barato por acierto son \u00f3rdenes distintos, y se imprimen los dos en vez de elegir uno. Una carga puede subir en uno mientras baja en el otro, y quien optimice por el primer n\u00famero estar\u00eda moviendo el equivocado.',
1689
1713
  outcomeColumns: { outcome: 'resultado', calls: 'llamadas', spend: 'gasto' },
1690
1714
  verdictSuccess: () => '\u00e9xito',
1691
1715
  verdictOther: () => '\u2014',
package/src/i18n/types.ts CHANGED
@@ -1128,6 +1128,13 @@ export interface CliMessages {
1128
1128
  outcomeNoRate(why: string): string;
1129
1129
  outcomeUnrecorded(share: string, usd: string): string;
1130
1130
  outcomeUndeclared(values: string): string;
1131
+ perOutcomeHeading(): string;
1132
+ perOutcomeRow(key: string, perCall: string, perOutcome: string, coverage: string): string;
1133
+ perOutcomeColumns: { workload: string; perCall: string; perOutcome: string; recorded: string };
1134
+ perOutcomeWithheld(why: string, successes: string, coverage: string): string;
1135
+ perOutcomeNumerator(): string;
1136
+ perOutcomeDisagreement(key: string, callRank: string, outcomeRank: string): string;
1137
+ perOutcomeBothOrders(): string;
1131
1138
  outcomeColumns: { outcome: string; calls: string; spend: string };
1132
1139
  verdictSuccess(): string;
1133
1140
  verdictOther(): string;
package/src/index.ts CHANGED
@@ -40,6 +40,7 @@ import {
40
40
  budgetPositions,
41
41
  conform,
42
42
  outcomeReport,
43
+ rankPerOutcome,
43
44
  FAILURE_POLICIES,
44
45
  detectFromSource,
45
46
  matchLocale,
@@ -6497,6 +6498,76 @@ async function commandProfile(
6497
6498
  )}`,
6498
6499
  );
6499
6500
  }
6501
+ /**
6502
+ * What an outcome costs, per workload — the finding a total cannot make.
6503
+ *
6504
+ * Printed only when at least one workload has enough recorded outcomes
6505
+ * to say something, and every row that cannot state a figure says which
6506
+ * of the five reasons applies rather than showing a blank.
6507
+ */
6508
+ const ranking = rankPerOutcome(
6509
+ report.outcomeTallyByLabel.map((entry) => ({
6510
+ key: entry.label,
6511
+ calls: entry.calls,
6512
+ totalUsd: entry.totalUsd,
6513
+ tally: entry.tally,
6514
+ })),
6515
+ config.outcomes ?? null,
6516
+ );
6517
+ if (ranking.byCall.length > 0) {
6518
+ console.log();
6519
+ console.log(c.bold(t.profile.perOutcomeHeading()));
6520
+ const pcol = t.profile.perOutcomeColumns;
6521
+ const prows = ranking.byCall.map((slice) => ({
6522
+ key: slice.key,
6523
+ perCall: formatUsd(slice.usdPerCall),
6524
+ perOutcome:
6525
+ slice.per.usdPerSuccess !== null
6526
+ ? formatUsd(slice.per.usdPerSuccess)
6527
+ : t.profile.perOutcomeWithheld(
6528
+ slice.per.withheld ?? 'no-vocabulary',
6529
+ n(slice.per.successes),
6530
+ pct(slice.per.coverage),
6531
+ ),
6532
+ recorded: pct(slice.per.coverage),
6533
+ }));
6534
+ const pw = {
6535
+ key: Math.max(...prows.map((r) => r.key.length), pcol.workload.length),
6536
+ perCall: Math.max(...prows.map((r) => r.perCall.length), pcol.perCall.length),
6537
+ perOutcome: Math.max(...prows.map((r) => r.perOutcome.length), pcol.perOutcome.length),
6538
+ recorded: Math.max(...prows.map((r) => r.recorded.length), pcol.recorded.length),
6539
+ };
6540
+ console.log(
6541
+ c.dim(
6542
+ ` ${pcol.workload.padEnd(pw.key)} ${pcol.perCall.padStart(pw.perCall)} ` +
6543
+ `${pcol.perOutcome.padStart(pw.perOutcome)} ${pcol.recorded.padStart(pw.recorded)}`,
6544
+ ),
6545
+ );
6546
+ for (const row of prows) {
6547
+ console.log(
6548
+ ` ${row.key.padEnd(pw.key)} ${row.perCall.padStart(pw.perCall)} ` +
6549
+ `${row.perOutcome.padStart(pw.perOutcome)} ${c.dim(row.recorded.padStart(pw.recorded))}`,
6550
+ );
6551
+ }
6552
+ console.log();
6553
+ console.log(` ${c.dim(wrap(t.profile.perOutcomeNumerator(), 74, ' '))}`);
6554
+
6555
+ // The disagreement between the two orders, which is itself the finding.
6556
+ if (ranking.disagreements.length > 0) {
6557
+ console.log();
6558
+ console.log(` ${c.dim(wrap(t.profile.perOutcomeBothOrders(), 74, ' '))}`);
6559
+ for (const d of ranking.disagreements) {
6560
+ console.log(
6561
+ ` ${c.yellow('→')} ${wrap(
6562
+ t.profile.perOutcomeDisagreement(d.slice.key, n(d.callRank + 1), n(d.outcomeRank + 1)),
6563
+ 74,
6564
+ ' ',
6565
+ )}`,
6566
+ );
6567
+ }
6568
+ }
6569
+ }
6570
+
6500
6571
  if (outcomes.undeclared.length > 0) {
6501
6572
  console.log(
6502
6573
  ` ${c.yellow('!')} ${wrap(