@trazum/cli 1.49.0 → 1.50.1

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.49.0",
3
+ "version": "1.50.1",
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.49.0"
40
+ "@trazum/core": "1.50.1"
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) =>
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) =>
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;
package/src/index.ts CHANGED
@@ -36,6 +36,7 @@ import {
36
36
  countTokensAnthropic,
37
37
  DEFAULT_USAGE,
38
38
  budgetPositions,
39
+ conform,
39
40
  detectFromSource,
40
41
  matchLocale,
41
42
  parsePlanDocument,
@@ -129,6 +130,7 @@ import type {
129
130
  } from '@trazum/core';
130
131
  import type {
131
132
  BudgetReport,
133
+ ContractName,
132
134
  UsageProfileReport,
133
135
  WaiverUse,
134
136
  InitDecline,
@@ -228,6 +230,7 @@ interface Args {
228
230
 
229
231
  const VALUE_FLAGS = new Set([
230
232
  'against',
233
+ 'contract',
231
234
  'from-log',
232
235
  'min-usd',
233
236
  'payload',
@@ -550,6 +553,7 @@ const COMMAND_FLAGS: Record<string, string[]> = {
550
553
  models: [],
551
554
  rank: ['level', 'model', 'calls', 'output-tokens', 'batch', 'disable', 'prompt', 'markdown-out'],
552
555
  init: ['dry-run', 'yes', 'json', 'pricing', 'pricing-live'],
556
+ conform: ['contract', 'json'],
553
557
  where: [],
554
558
  rules: [],
555
559
  blame: ['limit', 'model', 'calls', 'output-tokens', 'batch', 'prompt', 'markdown-out'],
@@ -1472,6 +1476,20 @@ const INIT_LOG_CANDIDATES = [
1472
1476
  * nobody has vouched for.
1473
1477
  */
1474
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
+
1475
1493
  /** How many source files, and how large each may be. Both reported when they bite. */
1476
1494
  const INIT_MAX_SOURCE_FILES = 400;
1477
1495
  const INIT_MAX_SOURCE_BYTES = 256 * 1024;
@@ -1850,6 +1868,87 @@ async function commandInit(
1850
1868
  console.log();
1851
1869
  }
1852
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
+
1853
1952
  function commandModels(t: CliMessages, pricing: PricingCatalogue): void {
1854
1953
  const n = (value: number): string => value.toLocaleString(t.numberLocale);
1855
1954
  const col = t.models.columns;
@@ -7843,6 +7942,9 @@ async function main(): Promise<void> {
7843
7942
  case 'models':
7844
7943
  commandModels(t, pricing);
7845
7944
  break;
7945
+ case 'conform':
7946
+ await commandConform(args, t);
7947
+ break;
7846
7948
  case 'init':
7847
7949
  await commandInit(args, config, pricing, t);
7848
7950
  break;