@trazum/cli 1.48.0 → 1.50.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.48.0",
3
+ "version": "1.50.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.48.0"
40
+ "@trazum/core": "1.50.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.2.0",
package/src/i18n/en.ts CHANGED
@@ -54,6 +54,7 @@ ${bold('USAGE')}
54
54
  trazum blame <file> [options]
55
55
  trazum prune <file> --cases <file> --yes
56
56
  trazum where [file]
57
+ trazum conform <file|-> [--contract <name>]
57
58
  trazum models
58
59
  trazum rules
59
60
 
@@ -73,6 +74,20 @@ ${bold('OPTIONS FOR init')}
73
74
  measured figure and leaves the limit to you — a generated config full of
74
75
  guessed numbers is one nobody trusts.
75
76
 
77
+ ${bold('OPTIONS FOR conform')}
78
+ --contract <name> Check against a named contract instead of
79
+ detecting one: usage-log, profile, plan,
80
+ verification, history, connected, cost-answer.
81
+ --json The report as data.
82
+
83
+ Answers two questions and keeps them apart. Does this document conform —
84
+ required fields, present and the right type, exits 1 when not. And what can a
85
+ valid document of this shape not answer, with the field that would unlock
86
+ each.
87
+
88
+ The second never gates. Choosing not to log sessions is a decision, not a
89
+ defect. See docs/format.md.
90
+
76
91
  ${bold('OPTIONS FOR prune')}
77
92
  --cases <file> One input per line, or a JSON array. Required.
78
93
  --yes Actually spend the calls. Without it the estimate is
@@ -904,6 +919,23 @@ ${bold('EXAMPLES')}
904
919
  `${path} exists and could not be parsed, so nothing was written over it. Fix or move it first.`,
905
920
  },
906
921
 
922
+ conform: {
923
+ noTarget: () =>
924
+ 'Pass a file to check — a usage log, or any document Trazum emits. Use "-" to read from stdin.',
925
+ badContract: (given, known) => `"${given}" is not a contract. Known contracts: ${known}.`,
926
+ unrecognised: (path) => `${path} does not match any contract Trazum knows.`,
927
+ heading: (path, contract) => `${path} reads as a ${contract} document`,
928
+ headingLog: (path, contract, records) =>
929
+ `${path} reads as a ${contract}: ${records} ${records === 1 ? 'record' : 'records'}`,
930
+ conforms: () => 'It conforms. Every required field is present and the right type.',
931
+ problem: (at, kind, detail) => `${at}: ${detail} (${kind})`,
932
+ moreProblems: (count) => `…and ${count} more. Fix these first; they are often the same mistake.`,
933
+ unavailableHeading: () => 'What this cannot answer, and what would unlock it',
934
+ unavailable: (finding, because, unlockedBy) => `${finding} — ${because}. Add ${unlockedBy}.`,
935
+ unavailableNeverGates: () =>
936
+ 'None of those failed anything. Choosing not to record a field is a decision, not a defect, and a gate that failed on it would be this tool telling you what to log.',
937
+ },
938
+
907
939
  where: {
908
940
  hostHeading: () => 'Running inside',
909
941
  subscription: (host) =>
@@ -1666,7 +1698,9 @@ ${bold('EXAMPLES')}
1666
1698
  nothingMeasured: (dir) =>
1667
1699
  `Nothing is measured yet (the store at ${dir} is empty), so the budget half of every answer will say so. The cost half still answers from the catalogue: offline is a mode, not a failure.`,
1668
1700
  noBudget: () =>
1669
- 'No spend.maxUsd is configured, so "is there budget left" has no subject and every answer says so rather than guessing one.',
1701
+ 'No spend.monthlyUsd is configured, so "is there budget left" has no subject and every answer says so rather than guessing one. spend.maxUsd is deliberately not read here: it gates whatever period a log covers, and reading it as a monthly limit is how two surfaces of this tool come to disagree.',
1702
+ partialCoverage: (measuredDays, elapsedDays, period) =>
1703
+ `Only ${measuredDays} of the ${elapsedDays} elapsed days of ${period} carry any measurement, so the consumed figure is a floor on the period rather than the period. Pull the missing days with trazum connect before treating what is left as headroom.`,
1670
1704
  badPort: (value) => `"${value}" is not a port. Give a whole number from 0 to 65535, or use --socket.`,
1671
1705
  },
1672
1706
 
@@ -1742,6 +1776,27 @@ ${bold('EXAMPLES')}
1742
1776
  span === null
1743
1777
  ? `Nothing was older than ${days} days. ${kept} measurements kept, and the append log compacted.`
1744
1778
  : `Deleted ${count} measurements older than ${days} days, covering ${span} and ${usd} of measured spend. ${kept} kept, and the append log compacted to what the store already resolved to.`,
1779
+ budgetHeading: (period) => `Budget for ${period}`,
1780
+ budgetStanding: (consumed, limit, share, measuredDays, periodDays) =>
1781
+ `${consumed} of ${limit} (${share}), measured over ${measuredDays} of the month's ${periodDays} days.`,
1782
+ budgetShape: (shape, elapsedPct, coverage) =>
1783
+ shape === 'ahead'
1784
+ ? `The money is going faster than the calendar: ${elapsedPct}% of the month has elapsed.`
1785
+ : shape === 'behind'
1786
+ ? `The money is going slower than the calendar: ${elapsedPct}% of the month has elapsed.`
1787
+ : shape === 'on-pace'
1788
+ ? `Tracking the calendar: ${elapsedPct}% of the month has elapsed.`
1789
+ : coverage === 'partial'
1790
+ ? 'Whether that is fast or slow for the month cannot be told from a floor: the unmeasured days spent something, and only an overrun would be unarguable.'
1791
+ : 'There is nothing to compare the spend against yet.',
1792
+ budgetNeverForecast: () =>
1793
+ 'That is a shape, not a forecast. Where this goes next depends on what you do next, and no arithmetic here knows that.',
1794
+ budgetNothingMeasured: (elapsedDays) =>
1795
+ `Nothing has been measured this month, across ${elapsedDays} elapsed ${elapsedDays === 1 ? 'day' : 'days'}. That is not a budget under control — it is a budget nobody is watching. Run trazum connect to pull what the provider has.`,
1796
+ budgetPartial: (measuredDays, elapsedDays, days) =>
1797
+ `Only ${measuredDays} of ${elapsedDays} elapsed days carry any measurement, so the figure below is a floor on the month rather than the month. Missing: ${days}.`,
1798
+ budgetScopesUnmeasured: (count) =>
1799
+ `${count} budgeted ${count === 1 ? 'scope' : 'scopes'} (per label or per service) cannot be answered from the store: a store record carries a provider and a model, not a workload label. Gate those with trazum profile against a per-call log.`,
1745
1800
  },
1746
1801
 
1747
1802
  connect: {
package/src/i18n/es.ts CHANGED
@@ -41,6 +41,7 @@ ${bold('USO')}
41
41
  trazum blame <fichero> [opciones]
42
42
  trazum prune <fichero> --cases <fichero> --yes
43
43
  trazum where [fichero]
44
+ trazum conform <fichero|-> [--contract <nombre>]
44
45
  trazum models
45
46
  trazum rules
46
47
 
@@ -60,6 +61,20 @@ ${bold('OPCIONES DE init')}
60
61
  cifra medida y te deja el límite a ti — una configuración generada llena de
61
62
  números adivinados no se la cree nadie.
62
63
 
64
+ ${bold('OPCIONES DE conform')}
65
+ --contract <nombre> Comprueba contra un contrato concreto en vez de
66
+ detectarlo: usage-log, profile, plan,
67
+ verification, history, connected, cost-answer.
68
+ --json El informe como datos.
69
+
70
+ Responde dos preguntas y las mantiene separadas. ¿Cumple este documento —
71
+ campos obligatorios, presentes y del tipo correcto; sale con 1 si no. Y qué no
72
+ puede responder un documento válido de esta forma, con el campo que lo
73
+ desbloquearía.
74
+
75
+ La segunda nunca hace fallar nada. Decidir no registrar sesiones es una
76
+ decisión, no un defecto. Ver docs/format.md.
77
+
63
78
  ${bold('OPCIONES DE prune')}
64
79
  --cases <fichero> Una entrada por línea, o un array JSON. Obligatorio.
65
80
  --yes Gasta las llamadas de verdad. Sin él se imprime la
@@ -937,6 +952,23 @@ ${bold('EJEMPLOS')}
937
952
  `${path} existe y no se pudo interpretar, así que no se escribió nada encima. Arréglalo o muévelo primero.`,
938
953
  },
939
954
 
955
+ conform: {
956
+ noTarget: () =>
957
+ 'Pasa un archivo para comprobar — un registro de consumo, o cualquier documento que emita Trazum. Usa "-" para leer de la entrada estándar.',
958
+ badContract: (given, known) => `"${given}" no es un contrato. Contratos conocidos: ${known}.`,
959
+ unrecognised: (path) => `${path} no encaja con ningún contrato que Trazum conozca.`,
960
+ heading: (path, contract) => `${path} se lee como un documento ${contract}`,
961
+ headingLog: (path, contract, records) =>
962
+ `${path} se lee como ${contract}: ${records} ${records === 1 ? 'registro' : 'registros'}`,
963
+ conforms: () => 'Cumple. Todos los campos obligatorios están y son del tipo correcto.',
964
+ problem: (at, kind, detail) => `${at}: ${detail} (${kind})`,
965
+ moreProblems: (count) => `…y ${count} más. Arregla estos primero; suelen ser el mismo error.`,
966
+ unavailableHeading: () => 'Lo que esto no puede responder, y qué lo desbloquearía',
967
+ unavailable: (finding, because, unlockedBy) => `${finding} — ${because}. Añade ${unlockedBy}.`,
968
+ unavailableNeverGates: () =>
969
+ 'Nada de eso ha hecho fallar nada. Decidir no registrar un campo es una decisión, no un defecto, y una puerta que fallara por ello sería esta herramienta diciéndote qué registrar.',
970
+ },
971
+
940
972
  where: {
941
973
  hostHeading: () => 'Ejecutándose dentro de',
942
974
  subscription: (host) =>
@@ -1696,7 +1728,9 @@ ${bold('EJEMPLOS')}
1696
1728
  nothingMeasured: (dir) =>
1697
1729
  `Todavía no hay nada medido (el almacén de ${dir} está vacío), así que la mitad de presupuesto de cada respuesta lo dirá. La mitad del coste sigue respondiendo desde el catálogo: sin conexión es un modo, no un fallo.`,
1698
1730
  noBudget: () =>
1699
- 'No hay spend.maxUsd configurado, así que "queda presupuesto" no tiene sujeto y cada respuesta lo dice en vez de inventarse uno.',
1731
+ 'No hay spend.monthlyUsd configurado, así que "queda presupuesto" no tiene sujeto y cada respuesta lo dice en vez de inventarse uno. spend.maxUsd no se lee aquí a propósito: ese controla el periodo que cubra un registro, y leerlo como límite mensual es justo como dos superficies de esta herramienta acaban en desacuerdo.',
1732
+ partialCoverage: (measuredDays, elapsedDays, period) =>
1733
+ `Solo ${measuredDays} de los ${elapsedDays} días transcurridos de ${period} tienen alguna medición, así que la cifra consumida es un suelo del periodo y no el periodo. Descarga los días que faltan con trazum connect antes de tratar lo que queda como margen.`,
1700
1734
  badPort: (value) => `"${value}" no es un puerto. Da un número entero de 0 a 65535, o usa --socket.`,
1701
1735
  },
1702
1736
 
@@ -1772,6 +1806,27 @@ ${bold('EJEMPLOS')}
1772
1806
  span === null
1773
1807
  ? `Nada era más antiguo que ${days} días. ${kept} mediciones conservadas, y el log compactado.`
1774
1808
  : `Borradas ${count} mediciones de más de ${days} días, que cubren ${span} y ${usd} de gasto medido. ${kept} conservadas, y el log compactado a lo que el almacén ya resolvía.`,
1809
+ budgetHeading: (period) => `Presupuesto de ${period}`,
1810
+ budgetStanding: (consumed, limit, share, measuredDays, periodDays) =>
1811
+ `${consumed} de ${limit} (${share}), medido sobre ${measuredDays} de los ${periodDays} días del mes.`,
1812
+ budgetShape: (shape, elapsedPct, coverage) =>
1813
+ shape === 'ahead'
1814
+ ? `El dinero va más rápido que el calendario: ha transcurrido el ${elapsedPct}% del mes.`
1815
+ : shape === 'behind'
1816
+ ? `El dinero va más lento que el calendario: ha transcurrido el ${elapsedPct}% del mes.`
1817
+ : shape === 'on-pace'
1818
+ ? `Al ritmo del calendario: ha transcurrido el ${elapsedPct}% del mes.`
1819
+ : coverage === 'partial'
1820
+ ? 'Si eso es rápido o lento para el mes no se puede saber desde un suelo: los días sin medir gastaron algo, y solo un exceso sería incontestable.'
1821
+ : 'Todavía no hay nada con lo que comparar el gasto.',
1822
+ budgetNeverForecast: () =>
1823
+ 'Eso es una forma, no un pronóstico. A dónde va esto después depende de lo que hagas después, y ninguna aritmética de aquí lo sabe.',
1824
+ budgetNothingMeasured: (elapsedDays) =>
1825
+ `No se ha medido nada este mes, en ${elapsedDays} ${elapsedDays === 1 ? 'día transcurrido' : 'días transcurridos'}. Eso no es un presupuesto bajo control — es un presupuesto que nadie está mirando. Ejecuta trazum connect para descargar lo que tenga el proveedor.`,
1826
+ budgetPartial: (measuredDays, elapsedDays, days) =>
1827
+ `Solo ${measuredDays} de ${elapsedDays} días transcurridos tienen alguna medición, así que la cifra de abajo es un suelo del mes y no el mes. Faltan: ${days}.`,
1828
+ budgetScopesUnmeasured: (count) =>
1829
+ `${count} ${count === 1 ? 'ámbito presupuestado' : 'ámbitos presupuestados'} (por etiqueta o por servicio) no se pueden responder desde el almacén: un registro del almacén lleva un proveedor y un modelo, no una etiqueta de flujo. Contrólalos con trazum profile contra un registro por llamada.`,
1775
1830
  },
1776
1831
 
1777
1832
  connect: {
package/src/i18n/types.ts CHANGED
@@ -246,6 +246,26 @@ export interface CliMessages {
246
246
  existingUnparseable(path: string): string;
247
247
  };
248
248
 
249
+ /**
250
+ * The conformance check.
251
+ *
252
+ * Two halves, and the copy keeps them apart everywhere: problems gate, gaps
253
+ * do not. Choosing not to log sessions is a decision, not a defect.
254
+ */
255
+ conform: {
256
+ noTarget(): string;
257
+ badContract(given: string, known: string): string;
258
+ unrecognised(path: string): string;
259
+ heading(path: string, contract: string): string;
260
+ headingLog(path: string, contract: string, records: number): string;
261
+ conforms(): string;
262
+ problem(at: string, kind: string, detail: string): string;
263
+ moreProblems(count: number): string;
264
+ unavailableHeading(): string;
265
+ unavailable(finding: string, because: string, unlockedBy: string): string;
266
+ unavailableNeverGates(): string;
267
+ };
268
+
249
269
  where: {
250
270
  hostHeading(): string;
251
271
  subscription(host: string): string;
@@ -1151,6 +1171,12 @@ export interface CliMessages {
1151
1171
  measuredFrom(usd: string): string;
1152
1172
  nothingMeasured(dir: string): string;
1153
1173
  noBudget(): string;
1174
+ /**
1175
+ * The period is only partly measured — said out loud, because a position
1176
+ * standing on three days out of thirty must not read as a comfortable
1177
+ * ninety per cent remaining.
1178
+ */
1179
+ partialCoverage(measuredDays: number, elapsedDays: number, period: string): string;
1154
1180
  badPort(value: string): string;
1155
1181
  };
1156
1182
 
@@ -1205,6 +1231,15 @@ export interface CliMessages {
1205
1231
  pruneNeedsPolicy(): string;
1206
1232
  pruneDryRun(count: string, days: string, span: string | null, usd: string): string;
1207
1233
  pruned(count: string, days: string, span: string | null, usd: string, kept: string): string;
1234
+ /** The live budget — the one number `serve` and the MCP guard also read. */
1235
+ budgetHeading(period: string): string;
1236
+ budgetStanding(consumed: string, limit: string, share: string, measuredDays: string, periodDays: string): string;
1237
+ /** The shape of the burn, named. Never a date — see `budgetNeverForecast`. */
1238
+ budgetShape(shape: string, elapsedPct: number, coverage: string): string;
1239
+ budgetNeverForecast(): string;
1240
+ budgetNothingMeasured(elapsedDays: number): string;
1241
+ budgetPartial(measuredDays: number, elapsedDays: number, days: string): string;
1242
+ budgetScopesUnmeasured(count: number): string;
1208
1243
  };
1209
1244
 
1210
1245
  /**
package/src/index.ts CHANGED
@@ -35,6 +35,8 @@ import {
35
35
  computeSavings,
36
36
  countTokensAnthropic,
37
37
  DEFAULT_USAGE,
38
+ budgetPositions,
39
+ conform,
38
40
  detectFromSource,
39
41
  matchLocale,
40
42
  parsePlanDocument,
@@ -127,6 +129,8 @@ import type {
127
129
  UsageProfile,
128
130
  } from '@trazum/core';
129
131
  import type {
132
+ BudgetReport,
133
+ ContractName,
130
134
  UsageProfileReport,
131
135
  WaiverUse,
132
136
  InitDecline,
@@ -226,6 +230,7 @@ interface Args {
226
230
 
227
231
  const VALUE_FLAGS = new Set([
228
232
  'against',
233
+ 'contract',
229
234
  'from-log',
230
235
  'min-usd',
231
236
  'payload',
@@ -548,6 +553,7 @@ const COMMAND_FLAGS: Record<string, string[]> = {
548
553
  models: [],
549
554
  rank: ['level', 'model', 'calls', 'output-tokens', 'batch', 'disable', 'prompt', 'markdown-out'],
550
555
  init: ['dry-run', 'yes', 'json', 'pricing', 'pricing-live'],
556
+ conform: ['contract', 'json'],
551
557
  where: [],
552
558
  rules: [],
553
559
  blame: ['limit', 'model', 'calls', 'output-tokens', 'batch', 'prompt', 'markdown-out'],
@@ -1470,6 +1476,20 @@ const INIT_LOG_CANDIDATES = [
1470
1476
  * nobody has vouched for.
1471
1477
  */
1472
1478
 
1479
+ /** Problems listed before the rest are counted. A wall of them helps nobody. */
1480
+ const MAX_CONFORM_PROBLEMS = 20;
1481
+
1482
+ /** The contracts `--contract` accepts, so a typo is refused with the list. */
1483
+ const CONTRACT_NAMES = [
1484
+ 'usage-log',
1485
+ 'profile',
1486
+ 'plan',
1487
+ 'verification',
1488
+ 'history',
1489
+ 'connected',
1490
+ 'cost-answer',
1491
+ ];
1492
+
1473
1493
  /** How many source files, and how large each may be. Both reported when they bite. */
1474
1494
  const INIT_MAX_SOURCE_FILES = 400;
1475
1495
  const INIT_MAX_SOURCE_BYTES = 256 * 1024;
@@ -1848,6 +1868,87 @@ async function commandInit(
1848
1868
  console.log();
1849
1869
  }
1850
1870
 
1871
+ /**
1872
+ * `trazum conform <file>` — does this document conform, and what will it not
1873
+ * be able to answer?
1874
+ *
1875
+ * The command that makes the five contracts something to build against rather
1876
+ * than something to read about. An emitter — a logging wrapper somebody wrote
1877
+ * this afternoon, a connector for a provider this repository has never heard
1878
+ * of, a dashboard writing profile documents of its own — points this at what
1879
+ * it produced and finds out before shipping.
1880
+ *
1881
+ * **The second half is the useful half.** "Valid" is a yes or no. "Here is
1882
+ * what a valid document of this shape cannot tell you, and the field that
1883
+ * would unlock each" is the answer somebody acts on: a usage log with no
1884
+ * `session` is perfectly conformant and simply has no conversation growth in
1885
+ * it, and an emitter that only ever hears "valid" ships it and never finds out
1886
+ * why half the report is empty.
1887
+ *
1888
+ * Exits 1 on a problem, so it gates. It never exits 1 on an *unavailable
1889
+ * finding*: choosing not to log sessions is a decision, not a defect, and a
1890
+ * gate that failed on it would be this tool telling somebody what to record.
1891
+ */
1892
+ async function commandConform(args: Args, t: CliMessages): Promise<void> {
1893
+ const target = args.positional[0];
1894
+ if (target === undefined) throw new Error(t.conform.noTarget());
1895
+
1896
+ const named = stringFlag(args, 'contract');
1897
+ if (named !== undefined && !CONTRACT_NAMES.includes(named)) {
1898
+ throw new Error(t.conform.badContract(named, CONTRACT_NAMES.join(', ')));
1899
+ }
1900
+
1901
+ const text = target === '-' ? await readInput('-', t) : await readUsageLog(target, t);
1902
+ const report = conform(text, named === undefined ? {} : { contract: named as ContractName });
1903
+
1904
+ if (boolFlag(args, 'json')) {
1905
+ console.log(JSON.stringify(report, null, 2));
1906
+ if (!report.conforms) process.exitCode = 1;
1907
+ return;
1908
+ }
1909
+
1910
+ console.log();
1911
+ if (report.contract === null) {
1912
+ console.log(c.red(t.conform.unrecognised(target)));
1913
+ console.log(` ${c.dim(wrap(report.because ?? '', 74, ' '))}`);
1914
+ console.log();
1915
+ process.exitCode = 1;
1916
+ return;
1917
+ }
1918
+
1919
+ console.log(
1920
+ c.bold(
1921
+ report.records === null
1922
+ ? t.conform.heading(target, report.contract)
1923
+ : t.conform.headingLog(target, report.contract, report.records),
1924
+ ),
1925
+ );
1926
+
1927
+ if (report.problems.length === 0) {
1928
+ console.log(` ${c.green(t.conform.conforms())}`);
1929
+ } else {
1930
+ for (const problem of report.problems.slice(0, MAX_CONFORM_PROBLEMS)) {
1931
+ console.log(` ${c.red(t.conform.problem(problem.at, problem.kind, problem.detail))}`);
1932
+ }
1933
+ if (report.problems.length > MAX_CONFORM_PROBLEMS) {
1934
+ console.log(` ${c.dim(t.conform.moreProblems(report.problems.length - MAX_CONFORM_PROBLEMS))}`);
1935
+ }
1936
+ process.exitCode = 1;
1937
+ }
1938
+
1939
+ if (report.unavailable.length > 0) {
1940
+ console.log();
1941
+ console.log(c.bold(t.conform.unavailableHeading()));
1942
+ for (const gap of report.unavailable) {
1943
+ console.log(` ${c.dim(wrap(t.conform.unavailable(gap.finding, gap.because, gap.unlockedBy), 74, ' '))}`);
1944
+ }
1945
+ // Said out loud, because the exit code says it silently and somebody
1946
+ // reading a red-and-yellow screen will assume both halves gated.
1947
+ console.log(` ${c.dim(wrap(t.conform.unavailableNeverGates(), 74, ' '))}`);
1948
+ }
1949
+ console.log();
1950
+ }
1951
+
1851
1952
  function commandModels(t: CliMessages, pricing: PricingCatalogue): void {
1852
1953
  const n = (value: number): string => value.toLocaleString(t.numberLocale);
1853
1954
  const col = t.models.columns;
@@ -2747,41 +2848,39 @@ async function commandServe(
2747
2848
  t: CliMessages,
2748
2849
  ): Promise<void> {
2749
2850
  const root = process.cwd();
2750
- const limitUsd = config.spend?.maxUsd;
2751
2851
 
2752
2852
  const { resolved } = await readStore(root);
2753
- const measured = resolved.records.length > 0;
2853
+
2754
2854
  /**
2755
- * The window the measurement covers, carried into every answer.
2855
+ * The live budget, from `budgetPositions` the same number `store` prints
2856
+ * and the same one the MCP guard consults.
2756
2857
  *
2757
- * The position is read once at start, so a caller has to be able to see how
2758
- * old it is. A null window here would let a figure from last month read as
2759
- * current, which is the staleness this endpoint is otherwise honest about.
2858
+ * **This used to read `spend.maxUsd` against the whole store**, which is a
2859
+ * per-log gate compared against however much history the store happened to
2860
+ * hold. A year of records against a monthly limit reported as a budget
2861
+ * position, with a straight face and no way for a caller to tell. Same
2862
+ * units, different denominators, and the two surfaces disagreed by exactly
2863
+ * as much history as the machine had. `spend.monthlyUsd` is the key for a
2864
+ * calendar month and nothing infers one key from the other: a repository
2865
+ * with a per-log gate and no monthly budget has no monthly position, and
2866
+ * this says so rather than picking a number that is the right shape.
2760
2867
  */
2761
- const window = measured
2762
- ? {
2763
- fromMs: Math.min(...resolved.records.map((record) => record.fromMs)),
2764
- toMs: Math.max(...resolved.records.map((record) => record.toMs)),
2765
- }
2766
- : null;
2767
- const report = bucketedProfile(
2768
- {
2769
- provider: 'store',
2770
- granularity: 'bucketed',
2771
- buckets: bucketsFromRecords(resolved.records),
2772
- window,
2773
- gaps: [],
2774
- unavailable: [],
2775
- },
2776
- { catalogue: pricing },
2777
- );
2868
+ const budget = budgetPositions(resolved.records, config.spend, { catalogue: pricing });
2869
+ const standing = budget.positions[0] ?? null;
2870
+ const limitUsd = config.spend?.monthlyUsd;
2871
+ const measured = standing !== null && standing.coverage !== 'none';
2778
2872
 
2779
2873
  const server = buildServer({
2780
2874
  catalogue: pricing,
2781
2875
  position: () => ({
2782
- consumedUsd: measured ? report.total.totalUsd : undefined,
2876
+ // Nothing measured inside the period is `undefined`, never zero: the
2877
+ // endpoint's `cannot-tell` exists for exactly this, and a $0 consumed
2878
+ // would be the healthiest-looking budget a dead store can produce.
2879
+ consumedUsd: measured ? standing.consumedUsd : undefined,
2783
2880
  limitUsd,
2784
- window: report.span,
2881
+ // The period, not the store's span. A caller judging staleness needs to
2882
+ // know which month the figure is about.
2883
+ window: standing === null ? null : { fromMs: standing.period.fromMs, toMs: standing.period.toMs },
2785
2884
  }),
2786
2885
  });
2787
2886
 
@@ -2796,8 +2895,13 @@ async function commandServe(
2796
2895
  console.log(c.bold(t.serve.listening(where)));
2797
2896
  console.log(` ${c.dim(wrap(t.serve.loopbackOnly(), 74, ' '))}`);
2798
2897
  console.log(
2799
- ` ${c.dim(wrap(measured ? t.serve.measuredFrom(formatUsd(report.total.totalUsd)) : t.serve.nothingMeasured(STORE_DIR), 74, ' '))}`,
2898
+ ` ${c.dim(wrap(measured ? t.serve.measuredFrom(formatUsd(standing.consumedUsd)) : t.serve.nothingMeasured(STORE_DIR), 74, ' '))}`,
2800
2899
  );
2900
+ if (standing !== null && standing.coverage === 'partial') {
2901
+ console.log(
2902
+ ` ${c.yellow(wrap(t.serve.partialCoverage(standing.measuredDays, standing.elapsedDays, standing.period.id), 74, ' '))}`,
2903
+ );
2904
+ }
2801
2905
  if (limitUsd === undefined) {
2802
2906
  console.log(` ${c.dim(wrap(t.serve.noBudget(), 74, ' '))}`);
2803
2907
  }
@@ -3164,6 +3268,72 @@ async function commandStore(
3164
3268
  console.log(
3165
3269
  ` ${c.dim(wrap(keepDays === undefined ? t.store.noRetention() : t.store.retention(String(keepDays)), 74, ' '))}`,
3166
3270
  );
3271
+
3272
+ /**
3273
+ * The live budget, printed here because this is where the measurement lives.
3274
+ *
3275
+ * The same call `serve` makes and the same call the MCP guard makes, so the
3276
+ * three cannot disagree about how much of the month is gone — which is the
3277
+ * whole point of the number existing in one place.
3278
+ */
3279
+ renderBudget(budgetPositions(resolved.records, config.spend, { catalogue: pricing }), t, n);
3280
+ }
3281
+
3282
+ /**
3283
+ * One budget standing, rendered.
3284
+ *
3285
+ * Coverage before the money, deliberately. A reader who sees "$61 of $100"
3286
+ * first has already formed a view by the time they reach "over three of
3287
+ * nineteen elapsed days", and the second sentence has to undo the first.
3288
+ */
3289
+ function renderBudget(report: BudgetReport, t: CliMessages, n: (value: number) => string): void {
3290
+ const standing = report.positions[0];
3291
+ if (standing === undefined) {
3292
+ if (report.unmeasuredScopes.length > 0) {
3293
+ console.log();
3294
+ console.log(
3295
+ ` ${c.dim(wrap(t.store.budgetScopesUnmeasured(report.unmeasuredScopes.length), 74, ' '))}`,
3296
+ );
3297
+ }
3298
+ return;
3299
+ }
3300
+
3301
+ console.log();
3302
+ console.log(c.bold(t.store.budgetHeading(standing.period.id)));
3303
+
3304
+ if (standing.coverage === 'none') {
3305
+ // Nothing measured is never rendered as nothing spent. A dead store and a
3306
+ // quiet month produce the same zero, and only one of them is good news.
3307
+ console.log(` ${c.red(wrap(t.store.budgetNothingMeasured(standing.elapsedDays), 74, ' '))}`);
3308
+ return;
3309
+ }
3310
+ if (standing.coverage === 'partial') {
3311
+ console.log(
3312
+ ` ${c.yellow(wrap(t.store.budgetPartial(standing.measuredDays, standing.elapsedDays, standing.unmeasuredDays.join(', ')), 74, ' '))}`,
3313
+ );
3314
+ }
3315
+
3316
+ const share = standing.burn.consumedShare;
3317
+ console.log(
3318
+ ` ${t.store.budgetStanding(
3319
+ formatUsd(standing.consumedUsd),
3320
+ formatUsd(standing.limitUsd),
3321
+ share === null ? '—' : `${Math.round(share * 100)}%`,
3322
+ n(standing.measuredDays),
3323
+ n(standing.period.days),
3324
+ )}`,
3325
+ );
3326
+ const line = t.store.budgetShape(
3327
+ standing.burn.shape,
3328
+ Math.round(standing.burn.elapsedShare * 100),
3329
+ standing.coverage,
3330
+ );
3331
+ console.log(` ${standing.verdict === 'over' ? c.red(line) : c.dim(wrap(line, 74, ' '))}`);
3332
+ // Only where there is a shape to disclaim. "That is a shape, not a forecast"
3333
+ // under "nothing to compare against" is a disclaimer about nothing.
3334
+ if (standing.burn.shape !== 'cannot-tell') {
3335
+ console.log(` ${c.dim(wrap(t.store.budgetNeverForecast(), 74, ' '))}`);
3336
+ }
3167
3337
  }
3168
3338
 
3169
3339
  /**
@@ -7772,6 +7942,9 @@ async function main(): Promise<void> {
7772
7942
  case 'models':
7773
7943
  commandModels(t, pricing);
7774
7944
  break;
7945
+ case 'conform':
7946
+ await commandConform(args, t);
7947
+ break;
7775
7948
  case 'init':
7776
7949
  await commandInit(args, config, pricing, t);
7777
7950
  break;