@trazum/cli 1.50.3 → 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/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +40 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +40 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +27 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +121 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/i18n/en.ts +50 -0
- package/src/i18n/es.ts +50 -0
- package/src/i18n/types.ts +18 -0
- package/src/index.ts +172 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trazum/cli",
|
|
3
|
-
"version": "1.50.
|
|
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.
|
|
40
|
+
"@trazum/core": "1.50.5"
|
|
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,49 @@ ${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
|
+
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.',
|
|
1680
|
+
outcomeColumns: { outcome: 'outcome', calls: 'calls', spend: 'spend' },
|
|
1681
|
+
verdictSuccess: () => 'success',
|
|
1682
|
+
verdictOther: () => '\u2014',
|
|
1683
|
+
verdictUndeclared: () => 'undeclared',
|
|
1634
1684
|
needsSession: (seen) =>
|
|
1635
1685
|
`"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
1686
|
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,49 @@ ${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
|
+
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.',
|
|
1713
|
+
outcomeColumns: { outcome: 'resultado', calls: 'llamadas', spend: 'gasto' },
|
|
1714
|
+
verdictSuccess: () => '\u00e9xito',
|
|
1715
|
+
verdictOther: () => '\u2014',
|
|
1716
|
+
verdictUndeclared: () => 'sin declarar',
|
|
1667
1717
|
needsSession: (seen) =>
|
|
1668
1718
|
`"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
1719
|
needsTs: (seen) =>
|
package/src/i18n/types.ts
CHANGED
|
@@ -1121,6 +1121,24 @@ 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
|
+
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;
|
|
1138
|
+
outcomeColumns: { outcome: string; calls: string; spend: string };
|
|
1139
|
+
verdictSuccess(): string;
|
|
1140
|
+
verdictOther(): string;
|
|
1141
|
+
verdictUndeclared(): string;
|
|
1124
1142
|
needsTs(seen: string): string;
|
|
1125
1143
|
needsStopReason(seen: string): string;
|
|
1126
1144
|
needsCacheTtl(seen: string): string;
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,8 @@ import {
|
|
|
39
39
|
DEFAULT_USAGE,
|
|
40
40
|
budgetPositions,
|
|
41
41
|
conform,
|
|
42
|
+
outcomeReport,
|
|
43
|
+
rankPerOutcome,
|
|
42
44
|
FAILURE_POLICIES,
|
|
43
45
|
detectFromSource,
|
|
44
46
|
matchLocale,
|
|
@@ -4585,6 +4587,7 @@ async function commandProfile(
|
|
|
4585
4587
|
can(cov.label > 0, t.profile.dryRunLabels(share(cov.label)));
|
|
4586
4588
|
can(cov.ts > 0, t.profile.dryRunClock(share(cov.ts)));
|
|
4587
4589
|
can(cov.session > 0, t.profile.dryRunSessions(share(cov.session)));
|
|
4590
|
+
can(cov.outcome > 0, t.profile.dryRunOutcomes(share(cov.outcome)));
|
|
4588
4591
|
can(cov.stopReason > 0, t.profile.dryRunStopReason(share(cov.stopReason)));
|
|
4589
4592
|
// "No cache traffic" is not a missing field: the split can only exist on
|
|
4590
4593
|
// records that wrote, and a log that never wrote has nothing to record.
|
|
@@ -6416,6 +6419,167 @@ async function commandProfile(
|
|
|
6416
6419
|
* section at all, because a paragraph of things that are fine is the
|
|
6417
6420
|
* paragraph readers learn to skip.
|
|
6418
6421
|
*/
|
|
6422
|
+
/**
|
|
6423
|
+
* Outcomes — the counterpart, where somebody recorded one.
|
|
6424
|
+
*
|
|
6425
|
+
* Printed above the coverage section rather than below it, because when this
|
|
6426
|
+
* section is present it is the most valuable thing on the page: every other
|
|
6427
|
+
* figure in this report is a cost, and this is the only one that says what
|
|
6428
|
+
* the money bought.
|
|
6429
|
+
*
|
|
6430
|
+
* Silent when nothing recorded an outcome. The coverage section below
|
|
6431
|
+
* already names the missing field and what it would unlock, and printing an
|
|
6432
|
+
* empty Outcomes heading above it would be the same sentence twice.
|
|
6433
|
+
*/
|
|
6434
|
+
{
|
|
6435
|
+
const outcomes = outcomeReport(report.outcomeTally, config.outcomes ?? null);
|
|
6436
|
+
if (outcomes.coverage.recorded > 0) {
|
|
6437
|
+
console.log();
|
|
6438
|
+
console.log(c.bold(t.profile.outcomeHeading()));
|
|
6439
|
+
|
|
6440
|
+
const col = t.profile.outcomeColumns;
|
|
6441
|
+
const rows = [...outcomes.slices, ...outcomes.undeclared].map((slice) => ({
|
|
6442
|
+
value: slice.value,
|
|
6443
|
+
verdict:
|
|
6444
|
+
slice.verdict === 'success'
|
|
6445
|
+
? t.profile.verdictSuccess()
|
|
6446
|
+
: slice.verdict === 'undeclared'
|
|
6447
|
+
? t.profile.verdictUndeclared()
|
|
6448
|
+
: t.profile.verdictOther(),
|
|
6449
|
+
calls: n(slice.calls),
|
|
6450
|
+
spend: formatUsd(slice.usd),
|
|
6451
|
+
}));
|
|
6452
|
+
const w = {
|
|
6453
|
+
value: Math.max(...rows.map((r) => r.value.length), col.outcome.length),
|
|
6454
|
+
verdict: Math.max(...rows.map((r) => r.verdict.length), 0),
|
|
6455
|
+
calls: Math.max(...rows.map((r) => r.calls.length), col.calls.length),
|
|
6456
|
+
spend: Math.max(...rows.map((r) => r.spend.length), col.spend.length),
|
|
6457
|
+
};
|
|
6458
|
+
console.log(
|
|
6459
|
+
c.dim(
|
|
6460
|
+
` ${col.outcome.padEnd(w.value)} ${''.padEnd(w.verdict)} ` +
|
|
6461
|
+
`${col.calls.padStart(w.calls)} ${col.spend.padStart(w.spend)}`,
|
|
6462
|
+
),
|
|
6463
|
+
);
|
|
6464
|
+
for (const row of rows) {
|
|
6465
|
+
const tint =
|
|
6466
|
+
row.verdict === t.profile.verdictUndeclared()
|
|
6467
|
+
? c.yellow
|
|
6468
|
+
: row.verdict === t.profile.verdictSuccess()
|
|
6469
|
+
? c.green
|
|
6470
|
+
: c.dim;
|
|
6471
|
+
console.log(
|
|
6472
|
+
` ${row.value.padEnd(w.value)} ${tint(row.verdict.padEnd(w.verdict))} ` +
|
|
6473
|
+
`${row.calls.padStart(w.calls)} ${row.spend.padStart(w.spend)}`,
|
|
6474
|
+
);
|
|
6475
|
+
}
|
|
6476
|
+
|
|
6477
|
+
console.log();
|
|
6478
|
+
if (outcomes.successShareOfRecordedUsd !== null) {
|
|
6479
|
+
const declaredUsd = outcomes.slices.reduce((sum, slice) => sum + slice.usd, 0);
|
|
6480
|
+
console.log(
|
|
6481
|
+
` ${wrap(t.profile.outcomeRate(pct(outcomes.successShareOfRecordedUsd), formatUsd(declaredUsd)), 74, ' ')}`,
|
|
6482
|
+
);
|
|
6483
|
+
} else if (outcomes.noRate !== null) {
|
|
6484
|
+
console.log(` ${c.dim(wrap(t.profile.outcomeNoRate(outcomes.noRate), 74, ' '))}`);
|
|
6485
|
+
}
|
|
6486
|
+
|
|
6487
|
+
// What the rate does not cover, every time it is printed. A rate over a
|
|
6488
|
+
// twelfth of the bill is a rate about a twelfth of the bill.
|
|
6489
|
+
if (outcomes.coverage.unrecordedUsd > 0 && report.total.totalUsd > 0) {
|
|
6490
|
+
console.log(
|
|
6491
|
+
` ${c.yellow('!')} ${wrap(
|
|
6492
|
+
t.profile.outcomeUnrecorded(
|
|
6493
|
+
pct(outcomes.coverage.unrecordedUsd / report.total.totalUsd),
|
|
6494
|
+
formatUsd(outcomes.coverage.unrecordedUsd),
|
|
6495
|
+
),
|
|
6496
|
+
74,
|
|
6497
|
+
' ',
|
|
6498
|
+
)}`,
|
|
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
|
+
|
|
6571
|
+
if (outcomes.undeclared.length > 0) {
|
|
6572
|
+
console.log(
|
|
6573
|
+
` ${c.yellow('!')} ${wrap(
|
|
6574
|
+
t.profile.outcomeUndeclared(outcomes.undeclared.map((s) => s.value).join(', ')),
|
|
6575
|
+
74,
|
|
6576
|
+
' ',
|
|
6577
|
+
)}`,
|
|
6578
|
+
);
|
|
6579
|
+
}
|
|
6580
|
+
}
|
|
6581
|
+
}
|
|
6582
|
+
|
|
6419
6583
|
const coverage = report.fieldCoverage;
|
|
6420
6584
|
if (coverage.parsed > 0) {
|
|
6421
6585
|
const missing: string[] = [];
|
|
@@ -6426,6 +6590,14 @@ async function commandProfile(
|
|
|
6426
6590
|
if (coverage.session < coverage.parsed) {
|
|
6427
6591
|
missing.push(t.profile.needsSession(partial(coverage.session)));
|
|
6428
6592
|
}
|
|
6593
|
+
/**
|
|
6594
|
+
* Listed first among the missing when it is missing entirely, because it
|
|
6595
|
+
* is the one field that changes what every other figure here *means*. The
|
|
6596
|
+
* rest sharpen a cost; this one gives it a counterpart.
|
|
6597
|
+
*/
|
|
6598
|
+
if (coverage.outcome < coverage.parsed) {
|
|
6599
|
+
missing.push(t.profile.needsOutcome(partial(coverage.outcome)));
|
|
6600
|
+
}
|
|
6429
6601
|
if (coverage.ts < coverage.parsed) {
|
|
6430
6602
|
missing.push(t.profile.needsTs(partial(coverage.ts)));
|
|
6431
6603
|
}
|