@trazum/cli 1.28.0 → 1.30.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/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +18 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +18 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +36 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +99 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/i18n/en.ts +27 -0
- package/src/i18n/es.ts +27 -0
- package/src/i18n/types.ts +36 -0
- package/src/index.ts +117 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trazum/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.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.
|
|
40
|
+
"@trazum/core": "1.30.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^26.2.0",
|
package/src/i18n/en.ts
CHANGED
|
@@ -234,6 +234,10 @@ ${bold('OPTIONS FOR profile')}
|
|
|
234
234
|
afternoon a loop burned a quarter of it. A log
|
|
235
235
|
with no timestamps fails: not measured is not
|
|
236
236
|
under budget.
|
|
237
|
+
--max-session-usd <n> Fail when any single conversation in the log
|
|
238
|
+
cost more than this — the unit an agent product
|
|
239
|
+
blows up in. A log with no sessions fails: not
|
|
240
|
+
measured is not under budget.
|
|
237
241
|
--since <when> Profile only calls at or after this moment. A UTC
|
|
238
242
|
--until <when> day (2026-08-14), a full ISO 8601 timestamp, a
|
|
239
243
|
relative window (7d, 24h) or "now";
|
|
@@ -1081,6 +1085,12 @@ ${bold('EXAMPLES')}
|
|
|
1081
1085
|
`FAILED — ${day} spent ${usd}, over the --max-day-usd limit of ${max}. A total under budget can hide a single runaway day, which is what this gate exists to catch.`,
|
|
1082
1086
|
maxDayNoClock: () =>
|
|
1083
1087
|
'FAILED — --max-day-usd was asked for and no record in this log carries a timestamp, so there are no days to judge. That is not a pass: a bill nobody could measure by day is not a bill that stayed under a daily budget. Add "ts" to the record and the gate arms.',
|
|
1088
|
+
maxSessionOk: (worst, max, sessions) =>
|
|
1089
|
+
`No conversation over budget: the most expensive of ${sessions} cost ${worst}, against --max-session-usd ${max}. A conversation that started before this log is counted only for the turns recorded here, so this is a floor.`,
|
|
1090
|
+
maxSessionFailed: (worst, max, sessions) =>
|
|
1091
|
+
`FAILED — the most expensive of ${sessions} conversations cost ${worst}, over the --max-session-usd limit of ${max}. A month's budget and a day's budget both pass while one conversation loops its way through this; the per-conversation figure is the one that catches it.`,
|
|
1092
|
+
maxSessionNoSessions: () =>
|
|
1093
|
+
'FAILED — --max-session-usd was asked for and no record in this log carries a session, so there are no conversations to judge. That is not a pass. Add "session" (or "conversation_id") to the record and the gate arms; Trazum groups by it and never prints it.',
|
|
1084
1094
|
maxDayUndated: (calls) =>
|
|
1085
1095
|
`${calls} calls carry no timestamp, so they are in the bill and in none of the days above — the worst day is a floor by whatever they held. A failure would stand regardless; this pass is over the part that could be dated.`,
|
|
1086
1096
|
maxCacheLossWorstCase: (calls, worst, max) =>
|
|
@@ -1161,6 +1171,23 @@ ${bold('EXAMPLES')}
|
|
|
1161
1171
|
`No usage logs in "${directory}". Looked for files ending in ${extensions}. A directory with nothing readable in it is an error rather than an empty report, which would read as "you spent nothing".`,
|
|
1162
1172
|
sessionCostTail: (ratio) =>
|
|
1163
1173
|
`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.`,
|
|
1174
|
+
sessionSpendOnly: (sessions, max) =>
|
|
1175
|
+
`${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.`,
|
|
1176
|
+
maxGrowthCoverageLost: (fields, was, now) =>
|
|
1177
|
+
`FAILED — this log stopped recording ${fields} (${was} of records before, ${now} now), so the comparison cannot be made. That is not a pass: a bill whose growth nobody could measure is not a bill that stayed flat, and every finding that needed the field went quiet for a reason that has nothing to do with spend.`,
|
|
1178
|
+
coverageField: (field) =>
|
|
1179
|
+
({ label: 'label', session: 'session', ts: 'timestamp', stopReason: 'stop reason' })[field] ?? field,
|
|
1180
|
+
coverageSilenced: (field) =>
|
|
1181
|
+
({
|
|
1182
|
+
label: 'Gone quiet with it: per-workload spend, the drill-down, and levers that describe a decision rather than a mixture.',
|
|
1183
|
+
session: 'Gone quiet with it: conversation growth, per-conversation cost, repeated turns, truncation retries and the cache-TTL fit.',
|
|
1184
|
+
ts: 'Gone quiet with it: the period, the per-day and per-hour shape, the model mix drift, and the cache-TTL question entirely.',
|
|
1185
|
+
stopReason: 'Gone quiet with it: answers cut off at max_tokens, and the retries billed after them.',
|
|
1186
|
+
})[field] ?? '',
|
|
1187
|
+
coverageDrift: (field, was, now) =>
|
|
1188
|
+
`Coverage moved: ${field} was on ${was} of records and is now on ${now}.`,
|
|
1189
|
+
coverageDriftWhy: () =>
|
|
1190
|
+
'A field the log stopped recording is not a finding that got fixed — every finding that needed it has gone quiet for a reason that has nothing to do with the bill. Reported from a 20-point move in either direction; a field that appeared means this report can see what the previous one could not.',
|
|
1164
1191
|
againstOverlap: (from, to) =>
|
|
1165
1192
|
`These two logs both cover ${from} → ${to}, so some of the same calls sit on both sides of this subtraction and part of the change is the same money counted twice. Compare periods that do not overlap — or window both logs with --since/--until.`,
|
|
1166
1193
|
windowLine: (since, until) =>
|
package/src/i18n/es.ts
CHANGED
|
@@ -232,6 +232,10 @@ ${bold('OPCIONES DE profile')}
|
|
|
232
232
|
una cuarta parte. Un registro sin marcas de
|
|
233
233
|
tiempo falla: no medido no es dentro de
|
|
234
234
|
presupuesto.
|
|
235
|
+
--max-session-usd <n> Falla cuando una sola conversación del registro
|
|
236
|
+
costó más que esto — la unidad en la que revienta
|
|
237
|
+
un producto de agentes. Un registro sin sesiones
|
|
238
|
+
falla: no medido no es dentro de presupuesto.
|
|
235
239
|
--since <cuándo> Perfila solo llamadas desde/hasta ese momento. Un
|
|
236
240
|
--until <cuándo> día UTC (2026-08-14), una marca ISO 8601 completa,
|
|
237
241
|
una ventana relativa (7d, 24h) o "now";
|
|
@@ -1095,6 +1099,12 @@ ${bold('EJEMPLOS')}
|
|
|
1095
1099
|
`FALLO — ${day} gastó ${usd}, por encima del límite --max-day-usd de ${max}. Un total dentro de presupuesto puede esconder un solo día desbocado, que es justo lo que vigila esta puerta.`,
|
|
1096
1100
|
maxDayNoClock: () =>
|
|
1097
1101
|
'FALLO — se pidió --max-day-usd y ningún registro de este fichero lleva marca de tiempo, así que no hay días que juzgar. Eso no es un aprobado: una factura que nadie pudo medir por días no es una factura que se mantuvo bajo un presupuesto diario. Añade "ts" al registro y la puerta se arma.',
|
|
1102
|
+
maxSessionOk: (worst, max, sessions) =>
|
|
1103
|
+
`Ninguna conversación por encima del presupuesto: la más cara de ${sessions} costó ${worst}, contra --max-session-usd ${max}. Una conversación que empezó antes de este registro solo cuenta los turnos grabados aquí, así que esto es un suelo.`,
|
|
1104
|
+
maxSessionFailed: (worst, max, sessions) =>
|
|
1105
|
+
`FALLO — la más cara de ${sessions} conversaciones costó ${worst}, por encima del límite --max-session-usd de ${max}. El presupuesto del mes y el del día aprueban mientras una conversación en bucle se come esto; la cifra por conversación es la que lo caza.`,
|
|
1106
|
+
maxSessionNoSessions: () =>
|
|
1107
|
+
'FALLO — se pidió --max-session-usd y ningún registro lleva sesión, así que no hay conversaciones que juzgar. Eso no es un aprobado. Añade "session" (o "conversation_id") al registro y la puerta se arma; Trazum agrupa por ella y nunca la imprime.',
|
|
1098
1108
|
maxDayUndated: (calls) =>
|
|
1099
1109
|
`${calls} llamadas no llevan marca de tiempo, así que están en la factura y en ninguno de los días de arriba — el peor día es un suelo por lo que valieran esas llamadas. Un fallo se sostendría igual; este aprobado cubre la parte que se pudo fechar.`,
|
|
1100
1110
|
maxCacheLossWorstCase: (calls, worst, max) =>
|
|
@@ -1176,6 +1186,23 @@ ${bold('EJEMPLOS')}
|
|
|
1176
1186
|
`No hay registros de uso en "${directory}". Se buscaron ficheros terminados en ${extensions}. Un directorio sin nada legible es un error, no un informe vacío, que se leería como "no has gastado nada".`,
|
|
1177
1187
|
sessionCostTail: (ratio) =>
|
|
1178
1188
|
`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.`,
|
|
1189
|
+
sessionSpendOnly: (sessions, max) =>
|
|
1190
|
+
`${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.`,
|
|
1191
|
+
maxGrowthCoverageLost: (fields, was, now) =>
|
|
1192
|
+
`FALLÓ — este registro dejó de grabar ${fields} (${was} de los registros antes, ${now} ahora), así que la comparación no se puede hacer. Eso no es un aprobado: una factura cuyo crecimiento nadie pudo medir no es una factura que se mantuvo plana, y todo hallazgo que necesitaba ese campo se calló por un motivo que nada tiene que ver con el gasto.`,
|
|
1193
|
+
coverageField: (field) =>
|
|
1194
|
+
({ label: 'etiqueta', session: 'sesión', ts: 'marca de tiempo', stopReason: 'razón de parada' })[field] ?? field,
|
|
1195
|
+
coverageSilenced: (field) =>
|
|
1196
|
+
({
|
|
1197
|
+
label: 'Se callaron con él: el gasto por carga, el desglose y unas palancas que describan una decisión y no una mezcla.',
|
|
1198
|
+
session: 'Se callaron con él: el crecimiento de conversación, el coste por conversación, los turnos repetidos, los reintentos por truncado y el ajuste del TTL de caché.',
|
|
1199
|
+
ts: 'Se callaron con él: el periodo, la forma por día y por hora, la deriva de mezcla de modelos y la pregunta del TTL de caché por completo.',
|
|
1200
|
+
stopReason: 'Se callaron con él: las respuestas cortadas en max_tokens y los reintentos facturados después.',
|
|
1201
|
+
})[field] ?? '',
|
|
1202
|
+
coverageDrift: (field, was, now) =>
|
|
1203
|
+
`La cobertura se movió: ${field} estaba en el ${was} de los registros y ahora está en el ${now}.`,
|
|
1204
|
+
coverageDriftWhy: () =>
|
|
1205
|
+
'Un campo que el registro dejó de grabar no es un hallazgo arreglado — todo hallazgo que lo necesitaba se ha callado por un motivo que nada tiene que ver con la factura. Se informa a partir de 20 puntos de movimiento en cualquier dirección; un campo que aparece significa que este informe ve lo que el anterior no podía.',
|
|
1179
1206
|
againstOverlap: (from, to) =>
|
|
1180
1207
|
`Estos dos registros cubren ambos ${from} → ${to}, así que algunas de las mismas llamadas están a los dos lados de esta resta y parte del cambio es el mismo dinero contado dos veces. Compara periodos que no se solapen — o acota ambos registros con --since/--until.`,
|
|
1181
1208
|
windowLine: (since, until) =>
|
package/src/i18n/types.ts
CHANGED
|
@@ -730,6 +730,15 @@ export interface CliMessages {
|
|
|
730
730
|
maxDayFailed(day: string, usd: string, max: string): string;
|
|
731
731
|
maxDayNoClock(): string;
|
|
732
732
|
maxDayUndated(calls: string): string;
|
|
733
|
+
/**
|
|
734
|
+
* The per-conversation gate. The single most expensive conversation is
|
|
735
|
+
* the number a per-conversation policy judges; a log with no sessions
|
|
736
|
+
* fails, and a conversation that started before the log makes the pass
|
|
737
|
+
* a floor — said in the pass message. The session key never appears.
|
|
738
|
+
*/
|
|
739
|
+
maxSessionOk(worst: string, max: string, sessions: string): string;
|
|
740
|
+
maxSessionFailed(worst: string, max: string, sessions: string): string;
|
|
741
|
+
maxSessionNoSessions(): string;
|
|
733
742
|
/**
|
|
734
743
|
* The price table behind every dollar in the report, when it is old
|
|
735
744
|
* enough to matter. Unlike a skipped line, staleness does not name its
|
|
@@ -760,6 +769,26 @@ export interface CliMessages {
|
|
|
760
769
|
* money on both sides of the subtraction. Only decidable when both logs
|
|
761
770
|
* carry a clock; unknown stays silent rather than reassuring.
|
|
762
771
|
*/
|
|
772
|
+
/**
|
|
773
|
+
* What the comparison stopped being able to see. Dollars cannot tell a
|
|
774
|
+
* fixed finding from a field the log stopped recording; only coverage can,
|
|
775
|
+
* so the copy names the threshold and states the distinction.
|
|
776
|
+
*/
|
|
777
|
+
/**
|
|
778
|
+
* The comparison gate's refusal: the current log stopped recording a field
|
|
779
|
+
* the previous one carried, so "did not grow" is a claim nobody could
|
|
780
|
+
* check. "Not measured" is not "did not grow", as everywhere here.
|
|
781
|
+
*/
|
|
782
|
+
maxGrowthCoverageLost(fields: string, was: string, now: string): string;
|
|
783
|
+
coverageField(field: string): string;
|
|
784
|
+
/**
|
|
785
|
+
* Which findings a collapsed field took with it. Named rather than left as
|
|
786
|
+
* "some findings": a reader deciding whether to trust this report needs to
|
|
787
|
+
* know exactly which sections are now silence rather than absence.
|
|
788
|
+
*/
|
|
789
|
+
coverageSilenced(field: string): string;
|
|
790
|
+
coverageDrift(field: string, was: string, now: string): string;
|
|
791
|
+
coverageDriftWhy(): string;
|
|
763
792
|
againstOverlap(from: string, to: string): string;
|
|
764
793
|
/**
|
|
765
794
|
* What one conversation costs — median against p95, never a mean: one
|
|
@@ -777,6 +806,13 @@ export interface CliMessages {
|
|
|
777
806
|
): string;
|
|
778
807
|
/** Said only when the p95 clears ten times the median — a real tail. */
|
|
779
808
|
sessionCostTail(ratio: string): string;
|
|
809
|
+
/**
|
|
810
|
+
* The whole log's conversations when the per-slice percentiles refused:
|
|
811
|
+
* count and single worst cost, stated because a maximum is a fact at any
|
|
812
|
+
* count while a percentile over four conversations would be the largest
|
|
813
|
+
* of four wearing a percentile's name.
|
|
814
|
+
*/
|
|
815
|
+
sessionSpendOnly(sessions: string, max: string): string;
|
|
780
816
|
/**
|
|
781
817
|
* Per-workload budgets from the config. A budgeted label with no calls in
|
|
782
818
|
* the log is "not measured", never a pass: a workload that did not appear
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
countTokensAnthropic,
|
|
21
21
|
DEFAULT_USAGE,
|
|
22
22
|
detectFromSource,
|
|
23
|
+
coverageDrift,
|
|
23
24
|
driversBetween,
|
|
24
25
|
estimateTokens,
|
|
25
26
|
evaluate,
|
|
@@ -180,6 +181,7 @@ const VALUE_FLAGS = new Set([
|
|
|
180
181
|
'max-growth-usd',
|
|
181
182
|
'max-cache-loss-usd',
|
|
182
183
|
'max-day-usd',
|
|
184
|
+
'max-session-usd',
|
|
183
185
|
'csv-out',
|
|
184
186
|
'csv-shape',
|
|
185
187
|
'what-if',
|
|
@@ -435,7 +437,7 @@ const COMMAND_FLAGS: Record<string, string[]> = {
|
|
|
435
437
|
],
|
|
436
438
|
check: ['max-tokens', 'level', 'exact-tokens', 'markdown-out', 'baseline'],
|
|
437
439
|
baseline: ['model', 'calls', 'output-tokens', 'cache-hit-rate', 'batch', 'exact-tokens', 'out', 'o'],
|
|
438
|
-
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', 'label', 'since', 'until'],
|
|
440
|
+
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'],
|
|
439
441
|
route: ['prompt-file', 'cases', 'label', 'concurrency', 'json', 'yes', 'pricing', 'pricing-live'],
|
|
440
442
|
eval: ['cases', 'level', 'concurrency', 'export', 'out', 'o', 'model'],
|
|
441
443
|
prune: ['cases', 'concurrency', 'json', 'yes'],
|
|
@@ -2262,6 +2264,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
2262
2264
|
typeof args.flags.get('max-growth-usd') === 'string' ||
|
|
2263
2265
|
typeof args.flags.get('max-cache-loss-usd') === 'string' ||
|
|
2264
2266
|
typeof args.flags.get('max-day-usd') === 'string' ||
|
|
2267
|
+
typeof args.flags.get('max-session-usd') === 'string' ||
|
|
2265
2268
|
config.spend !== undefined;
|
|
2266
2269
|
if (anyGate) {
|
|
2267
2270
|
const reasons: string[] = [];
|
|
@@ -2318,7 +2321,34 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
2318
2321
|
}
|
|
2319
2322
|
if (typeof args.flags.get('max-growth-usd') === 'string' && againstDelta !== null) {
|
|
2320
2323
|
const maxGrowth = numberFlag(args, 'max-growth-usd', 0, t);
|
|
2321
|
-
|
|
2324
|
+
/**
|
|
2325
|
+
* A comparison that went blind fails before it is judged.
|
|
2326
|
+
*
|
|
2327
|
+
* The dollars can hold flat while the current log stopped recording a
|
|
2328
|
+
* field the previous one carried — and every finding that needed the
|
|
2329
|
+
* field is now silent for a reason that has nothing to do with spend.
|
|
2330
|
+
* A gate passing there would be certifying a comparison it could not
|
|
2331
|
+
* make: "not measured" is not "did not grow", the same refusal
|
|
2332
|
+
* --max-day-usd makes on a clockless log and --max-session-usd on a
|
|
2333
|
+
* sessionless one. Only a collapse fails; a field that appeared means
|
|
2334
|
+
* this side can see more, which is never a reason to refuse.
|
|
2335
|
+
*/
|
|
2336
|
+
const blinded = previous !== null
|
|
2337
|
+
? coverageDrift(previous.fieldCoverage, report.fieldCoverage).filter((d) => d.delta < 0)
|
|
2338
|
+
: [];
|
|
2339
|
+
const worst = blinded[0];
|
|
2340
|
+
if (worst !== undefined) {
|
|
2341
|
+
console.error(
|
|
2342
|
+
c.red(
|
|
2343
|
+
t.profile.maxGrowthCoverageLost(
|
|
2344
|
+
blinded.map((d) => t.profile.coverageField(d.field)).join(', '),
|
|
2345
|
+
pct(worst.was),
|
|
2346
|
+
pct(worst.now),
|
|
2347
|
+
),
|
|
2348
|
+
),
|
|
2349
|
+
);
|
|
2350
|
+
process.exitCode = 1;
|
|
2351
|
+
} else if (againstDelta > maxGrowth) {
|
|
2322
2352
|
console.error(c.red(t.profile.maxGrowthUsdFailed(formatSignedUsd(againstDelta), formatUsd(maxGrowth))));
|
|
2323
2353
|
process.exitCode = 1;
|
|
2324
2354
|
}
|
|
@@ -2412,6 +2442,38 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
2412
2442
|
}
|
|
2413
2443
|
}
|
|
2414
2444
|
}
|
|
2445
|
+
/**
|
|
2446
|
+
* The per-conversation gate — the unit an agent product actually blows
|
|
2447
|
+
* up in. A month's budget and a day's budget both pass while one
|
|
2448
|
+
* conversation loops its way through $400; the single most expensive
|
|
2449
|
+
* conversation is the number a per-conversation policy has to judge,
|
|
2450
|
+
* and the log already carries it.
|
|
2451
|
+
*
|
|
2452
|
+
* The refusals it inherits: a log with **no sessions** fails rather
|
|
2453
|
+
* than passes ("not measured" is not "under budget"), and a
|
|
2454
|
+
* conversation that started before this log is counted only for the
|
|
2455
|
+
* turns recorded here — so a pass is a floor, and the pass message says
|
|
2456
|
+
* so. The session key itself is never printed, here or anywhere.
|
|
2457
|
+
*/
|
|
2458
|
+
if (typeof args.flags.get('max-session-usd') === 'string' || config.spend?.maxSessionUsd !== undefined) {
|
|
2459
|
+
const maxSession =
|
|
2460
|
+
typeof args.flags.get('max-session-usd') === 'string'
|
|
2461
|
+
? numberFlag(args, 'max-session-usd', 0, t)
|
|
2462
|
+
: config.spend!.maxSessionUsd!;
|
|
2463
|
+
if (report.sessionSpend === null) {
|
|
2464
|
+
console.error(c.red(t.profile.maxSessionNoSessions()));
|
|
2465
|
+
process.exitCode = 1;
|
|
2466
|
+
} else if (report.sessionSpend.maxUsd > maxSession) {
|
|
2467
|
+
console.error(
|
|
2468
|
+
c.red(t.profile.maxSessionFailed(formatUsd(report.sessionSpend.maxUsd), formatUsd(maxSession), n(report.sessionSpend.sessions))),
|
|
2469
|
+
);
|
|
2470
|
+
process.exitCode = 1;
|
|
2471
|
+
} else {
|
|
2472
|
+
console.error(
|
|
2473
|
+
c.dim(t.profile.maxSessionOk(formatUsd(report.sessionSpend.maxUsd), formatUsd(maxSession), n(report.sessionSpend.sessions))),
|
|
2474
|
+
);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2415
2477
|
};
|
|
2416
2478
|
|
|
2417
2479
|
/**
|
|
@@ -3070,6 +3132,19 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
3070
3132
|
);
|
|
3071
3133
|
}
|
|
3072
3134
|
}
|
|
3135
|
+
/**
|
|
3136
|
+
* The figure that survives a small log. `sessionCosts` refuses slices too
|
|
3137
|
+
* thin for a percentile, and rightly — but a log of four conversations
|
|
3138
|
+
* still has a most expensive one, and that maximum is a fact at any count.
|
|
3139
|
+
* It is also exactly the number `--max-session-usd` judges, so the report
|
|
3140
|
+
* states it rather than going silent where the gate would speak.
|
|
3141
|
+
*/
|
|
3142
|
+
if (report.sessionCosts.length === 0 && report.sessionSpend !== null) {
|
|
3143
|
+
console.log();
|
|
3144
|
+
console.log(
|
|
3145
|
+
` ${c.dim(wrap(t.profile.sessionSpendOnly(n(report.sessionSpend.sessions), formatUsd(report.sessionSpend.maxUsd)), 74, ' '))}`,
|
|
3146
|
+
);
|
|
3147
|
+
}
|
|
3073
3148
|
|
|
3074
3149
|
/**
|
|
3075
3150
|
* A total that assumed a cache-write rate is a floor, and says so.
|
|
@@ -3596,6 +3671,46 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
3596
3671
|
console.log(` ${d.delta > 0 ? c.yellow(line) : c.dim(line)}`);
|
|
3597
3672
|
}
|
|
3598
3673
|
}
|
|
3674
|
+
|
|
3675
|
+
/**
|
|
3676
|
+
* What the comparison stopped being able to see.
|
|
3677
|
+
*
|
|
3678
|
+
* Every figure above is dollars, and dollars cannot tell a finding that
|
|
3679
|
+
* was fixed from a finding whose field the log stopped recording — both
|
|
3680
|
+
* are silence. This is the only section that can, so it is loud: a
|
|
3681
|
+
* collapse in coverage invalidates whichever findings depended on it,
|
|
3682
|
+
* and reading the drop as good news is the specific mistake it exists
|
|
3683
|
+
* to prevent.
|
|
3684
|
+
*/
|
|
3685
|
+
const drifts = coverageDrift(previous.fieldCoverage, report.fieldCoverage);
|
|
3686
|
+
if (drifts.length > 0) {
|
|
3687
|
+
console.log();
|
|
3688
|
+
for (const drift of drifts) {
|
|
3689
|
+
const line = t.profile.coverageDrift(
|
|
3690
|
+
t.profile.coverageField(drift.field),
|
|
3691
|
+
pct(drift.was),
|
|
3692
|
+
pct(drift.now),
|
|
3693
|
+
);
|
|
3694
|
+
console.log(
|
|
3695
|
+
drift.delta < 0
|
|
3696
|
+
? ` ${c.yellow('!')} ${c.bold(wrap(line, 74, ' '))}`
|
|
3697
|
+
: ` ${c.dim(wrap(line, 74, ' '))}`,
|
|
3698
|
+
);
|
|
3699
|
+
/**
|
|
3700
|
+
* Which findings went with it, named. "Some findings are silent" is
|
|
3701
|
+
* not something a reader can act on; knowing that conversation
|
|
3702
|
+
* growth and the cache-TTL fit are now silence rather than absence
|
|
3703
|
+
* tells them exactly which sections of this report to distrust.
|
|
3704
|
+
*/
|
|
3705
|
+
if (drift.delta < 0) {
|
|
3706
|
+
const silenced = t.profile.coverageSilenced(drift.field);
|
|
3707
|
+
if (silenced !== '') console.log(` ${c.dim(wrap(silenced, 72, ' '))}`);
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
if (drifts.some((d) => d.delta < 0)) {
|
|
3711
|
+
console.log(` ${c.dim(wrap(t.profile.coverageDriftWhy(), 74, ' '))}`);
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3599
3714
|
}
|
|
3600
3715
|
}
|
|
3601
3716
|
|