@trazum/cli 1.50.8 → 1.50.9

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.8",
3
+ "version": "1.50.9",
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.8"
40
+ "@trazum/core": "1.50.9"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.2.0",
package/src/i18n/en.ts CHANGED
@@ -60,6 +60,7 @@ ${bold('USAGE')}
60
60
  trazum gateway <anthropic|openai> --on-cannot-tell <fail-open|fail-closed>
61
61
  trazum experiment <log> --a <label> --b <label> --min-outcomes <n>
62
62
  trazum quality <log> --label <name> --at <iso> [--gate]
63
+ trazum semantic <prompt> [--yes]
63
64
  trazum ladder <log>
64
65
  trazum feedback
65
66
  trazum --version
@@ -147,6 +148,17 @@ ${bold('OPTIONS FOR quality')}
147
148
  whenever the model mix, the call volume or the outcome coverage moved across
148
149
  the boundary — the prompt is not the only variable and it says so.
149
150
 
151
+ ${bold('OPTIONS FOR semantic')}
152
+ --yes Required to send anything. Without it the command
153
+ prints what the call would cost and stops.
154
+ --model <id> Which model to ask. Priced from the catalogue.
155
+
156
+ Finds what a dictionary cannot: the same rule taught twice in different
157
+ words, an instruction restated far away, a policy a later clause contradicts.
158
+ Every quoted passage is checked character for character against the prompt,
159
+ pairs the rules engine already catches are dropped, and every token figure is
160
+ counted rather than believed. Optional, and always will be.
161
+
150
162
  ${bold('OPTIONS FOR prune')}
151
163
  --cases <file> One input per line, or a JSON array. Required.
152
164
  --yes Actually spend the calls. Without it the estimate is
@@ -992,6 +1004,36 @@ ${bold('EXAMPLES')}
992
1004
  `${path} exists and could not be parsed, so nothing was written over it. Fix or move it first.`,
993
1005
  },
994
1006
 
1007
+ semantic: {
1008
+ heading: (path) => `Semantic pass on ${path}`,
1009
+ willCost: (usd, input, output, model) =>
1010
+ `This will send the prompt to ${model}: about ${input} tokens in and ${output} out, roughly ${usd}. Estimated, not measured \u2014 a tool that spends your money to tell you how to spend less should be the first thing audited by its own arithmetic. Pass --yes to run it.`,
1011
+ needsYes: () => 'Nothing was sent. Add --yes once you have read the price above.',
1012
+ finding: (kind, because) =>
1013
+ `${kind === 'contradiction' ? 'Contradiction' : kind === 'restated-instruction' ? 'Restated' : 'Same thing, different words'}: ${because}`,
1014
+ span: (line, text) => `line ${line}: ${text}`,
1015
+ ceiling: (tokens) =>
1016
+ `At most ${tokens} tokens \u2014 a ceiling, not a saving. Merging these two means writing one passage that does the work of both, and nobody knows yet how long that is.`,
1017
+ noCeiling: () =>
1018
+ 'No tokens attached. A contradiction is worth fixing because the prompt is wrong, not because it is long, and putting a figure on it would sell the wrong reason.',
1019
+ nothingFound: () => 'Nothing survived checking. That is a real answer.',
1020
+ rejected: (count) => `${count} proposals did not survive checking against the prompt.`,
1021
+ rejectedLine: (reason, span) =>
1022
+ reason === 'span-not-found'
1023
+ ? `paraphrased its own evidence: ${span}`
1024
+ : reason === 'already-detected'
1025
+ ? `already found without a model: ${span}`
1026
+ : reason === 'contradiction-of-a-copy'
1027
+ ? `called a near-copy a contradiction: ${span}`
1028
+ : reason === 'spans-identical' || reason === 'spans-overlap'
1029
+ ? `quoted the same passage twice: ${span}`
1030
+ : `covers ground an accepted finding covers: ${span}`,
1031
+ disposes: () =>
1032
+ 'The model proposes and the deterministic layer disposes: every quoted passage is checked character for character against the prompt, pairs the rules engine already catches are dropped, and every token figure is counted here rather than believed.',
1033
+ optIn: () =>
1034
+ 'This pass is optional and always will be. Trazum works with no key, no network and no model \u2014 that has been true since 0.1.0 and this does not change it.',
1035
+ },
1036
+
995
1037
  quality: {
996
1038
  heading: (label) => `Quality across the change: ${label}`,
997
1039
  needsLabel: () => 'Name the workload with --label: this compares one label before and after a change, and a mixture of workloads would average a regression away.',
package/src/i18n/es.ts CHANGED
@@ -47,6 +47,7 @@ ${bold('USO')}
47
47
  trazum gateway <anthropic|openai> --on-cannot-tell <fail-open|fail-closed>
48
48
  trazum experiment <log> --a <label> --b <label> --min-outcomes <n>
49
49
  trazum quality <log> --label <name> --at <iso> [--gate]
50
+ trazum semantic <prompt> [--yes]
50
51
  trazum ladder <log>
51
52
  trazum feedback
52
53
  trazum --version
@@ -137,6 +138,17 @@ ${bold('OPCIONES DE quality')}
137
138
  siempre que la mezcla de modelos, el volumen o la cobertura se movieran a
138
139
  través de la frontera — el prompt no es la única variable y lo dice.
139
140
 
141
+ ${bold('OPCIONES DE semantic')}
142
+ --yes Obligatorio para enviar nada. Sin él, el comando
143
+ imprime lo que costaría la llamada y para.
144
+ --model <id> Qué modelo preguntar. Se tarifa del catálogo.
145
+
146
+ Encuentra lo que un diccionario no puede: la misma regla enseñada dos veces
147
+ con otras palabras, una instrucción repetida lejos, una política que una
148
+ cláusula posterior contradice. Cada pasaje citado se comprueba carácter a
149
+ carácter contra el prompt, los pares que el motor de reglas ya caza se
150
+ descartan, y toda cifra de tokens se cuenta en vez de creérsela.
151
+
140
152
  ${bold('OPCIONES DE prune')}
141
153
  --cases <fichero> Una entrada por línea, o un array JSON. Obligatorio.
142
154
  --yes Gasta las llamadas de verdad. Sin él se imprime la
@@ -1028,6 +1040,36 @@ ${bold('EJEMPLOS')}
1028
1040
  `${path} existe y no se pudo interpretar, así que no se escribió nada encima. Arréglalo o muévelo primero.`,
1029
1041
  },
1030
1042
 
1043
+ semantic: {
1044
+ heading: (path) => `Pase sem\u00e1ntico sobre ${path}`,
1045
+ willCost: (usd, input, output, model) =>
1046
+ `Esto enviar\u00e1 el prompt a ${model}: unos ${input} tokens de entrada y ${output} de salida, aproximadamente ${usd}. Estimado, no medido \u2014 una herramienta que gasta tu dinero para decirte c\u00f3mo gastar menos deber\u00eda ser lo primero que audite su propia aritm\u00e9tica. Pasa --yes para ejecutarlo.`,
1047
+ needsYes: () => 'No se envi\u00f3 nada. A\u00f1ade --yes cuando hayas le\u00eddo el precio de arriba.',
1048
+ finding: (kind, because) =>
1049
+ `${kind === 'contradiction' ? 'Contradicci\u00f3n' : kind === 'restated-instruction' ? 'Repetido' : 'Lo mismo con otras palabras'}: ${because}`,
1050
+ span: (line, text) => `l\u00ednea ${line}: ${text}`,
1051
+ ceiling: (tokens) =>
1052
+ `Como mucho ${tokens} tokens \u2014 un techo, no un ahorro. Fundir los dos significa escribir un pasaje que haga el trabajo de ambos, y nadie sabe todav\u00eda cu\u00e1nto ocupa.`,
1053
+ noCeiling: () =>
1054
+ 'Sin tokens asociados. Una contradicci\u00f3n merece arreglarse porque el prompt est\u00e1 mal, no porque sea largo, y ponerle una cifra vender\u00eda el motivo equivocado.',
1055
+ nothingFound: () => 'Nada sobrevivi\u00f3 a la comprobaci\u00f3n. Esa es una respuesta real.',
1056
+ rejected: (count) => `${count} propuestas no sobrevivieron a la comprobaci\u00f3n contra el prompt.`,
1057
+ rejectedLine: (reason, span) =>
1058
+ reason === 'span-not-found'
1059
+ ? `parafrase\u00f3 su propia evidencia: ${span}`
1060
+ : reason === 'already-detected'
1061
+ ? `ya se encuentra sin modelo: ${span}`
1062
+ : reason === 'contradiction-of-a-copy'
1063
+ ? `llam\u00f3 contradicci\u00f3n a una casi copia: ${span}`
1064
+ : reason === 'spans-identical' || reason === 'spans-overlap'
1065
+ ? `cit\u00f3 el mismo pasaje dos veces: ${span}`
1066
+ : `cubre terreno que ya cubre un hallazgo aceptado: ${span}`,
1067
+ disposes: () =>
1068
+ 'El modelo propone y la capa determinista dispone: cada pasaje citado se comprueba car\u00e1cter a car\u00e1cter contra el prompt, los pares que el motor de reglas ya caza se descartan, y toda cifra de tokens se cuenta aqu\u00ed en vez de cre\u00e9rsela.',
1069
+ optIn: () =>
1070
+ 'Este pase es opcional y siempre lo ser\u00e1. Trazum funciona sin clave, sin red y sin modelo \u2014 eso es cierto desde 0.1.0 y esto no lo cambia.',
1071
+ },
1072
+
1031
1073
  quality: {
1032
1074
  heading: (label) => `Calidad a trav\u00e9s del cambio: ${label}`,
1033
1075
  needsLabel: () => 'Nombra la carga con --label: esto compara una etiqueta antes y despu\u00e9s de un cambio, y una mezcla de cargas promediar\u00eda una regresi\u00f3n hasta hacerla desaparecer.',
package/src/i18n/types.ts CHANGED
@@ -268,6 +268,21 @@ export interface CliMessages {
268
268
  * The ladder. Every line here exists to stop somebody reading "we route to
269
269
  * the cheap model first" as a saving without the number that decides it.
270
270
  */
271
+ semantic: {
272
+ heading(path: string): string;
273
+ willCost(usd: string, input: string, output: string, model: string): string;
274
+ needsYes(): string;
275
+ finding(kind: string, because: string): string;
276
+ span(line: string, text: string): string;
277
+ ceiling(tokens: string): string;
278
+ noCeiling(): string;
279
+ nothingFound(): string;
280
+ rejected(count: string): string;
281
+ rejectedLine(reason: string, span: string): string;
282
+ disposes(): string;
283
+ optIn(): string;
284
+ };
285
+
271
286
  quality: {
272
287
  heading(label: string): string;
273
288
  needsLabel(): string;
package/src/index.ts CHANGED
@@ -42,6 +42,9 @@ import {
42
42
  BREAK_EVEN_BAND,
43
43
  runExperiment,
44
44
  qualityGate,
45
+ semanticPassCost,
46
+ verifySemanticProposals,
47
+ SEMANTIC_SYSTEM_PROMPT,
45
48
  ladderPosition,
46
49
  validateLadder,
47
50
  outcomeReport,
@@ -143,6 +146,7 @@ import type {
143
146
  ContractName,
144
147
  ExperimentArm,
145
148
  GateSide,
149
+ SemanticProposal,
146
150
  FailurePolicy,
147
151
  GatewayStanding,
148
152
  UsageProfileReport,
@@ -584,6 +588,7 @@ const COMMAND_FLAGS: Record<string, string[]> = {
584
588
  ladder: ['pricing', 'pricing-live', 'since', 'until', 'label'],
585
589
  experiment: ['a', 'b', 'min-outcomes', 'pricing', 'pricing-live'],
586
590
  quality: ['label', 'at', 'gate', 'pricing', 'pricing-live'],
591
+ semantic: ['yes', 'model', 'pricing', 'pricing-live'],
587
592
  where: [],
588
593
  rules: [],
589
594
  blame: ['limit', 'model', 'calls', 'output-tokens', 'batch', 'prompt', 'markdown-out'],
@@ -2609,6 +2614,126 @@ async function commandQuality(
2609
2614
  console.log();
2610
2615
  }
2611
2616
 
2617
+ /**
2618
+ * `trazum semantic <prompt> [--yes]` — the findings a dictionary cannot see.
2619
+ *
2620
+ * The rules engine has deferred these since 0.1.0 for one honest reason: a
2621
+ * dictionary cannot see meaning, and a model that hallucinates a finding is
2622
+ * worse than a rule that misses one.
2623
+ *
2624
+ * **The price is printed before anything is sent, and `--yes` is required.** A
2625
+ * tool that spends somebody's money to tell them how to spend less has to be
2626
+ * the first thing audited by its own arithmetic, and it has to ask.
2627
+ */
2628
+ async function commandSemantic(
2629
+ args: Args,
2630
+ config: TrazumConfig,
2631
+ pricing: PricingCatalogue,
2632
+ t: CliMessages,
2633
+ ): Promise<void> {
2634
+ const prompt = await readInput(args.positional[0], t);
2635
+ const modelId = stringFlag(args, 'model') ?? config.usage?.model ?? DEFAULT_USAGE.model;
2636
+ const model = pricing.byId.get(modelId) ?? getModel(DEFAULT_USAGE.model);
2637
+ const rates = { inputPerMTok: model.inputPerMTok, outputPerMTok: model.outputPerMTok };
2638
+ const cost = semanticPassCost(prompt, rates);
2639
+ const n = (value: number): string => value.toLocaleString(t.numberLocale);
2640
+
2641
+ console.log();
2642
+ console.log(c.bold(t.semantic.heading(args.positional[0] ?? '-')));
2643
+ console.log();
2644
+ console.log(
2645
+ ` ${wrap(
2646
+ t.semantic.willCost(formatUsd(cost.usd), n(cost.inputTokens), n(cost.outputTokens), model.displayName),
2647
+ 74,
2648
+ ' ',
2649
+ )}`,
2650
+ );
2651
+
2652
+ if (!boolFlag(args, 'yes')) {
2653
+ // Nothing has been sent at this point, and nothing will be. The price
2654
+ // above is the whole output of a run without --yes.
2655
+ console.log();
2656
+ console.log(` ${c.dim(t.semantic.needsYes())}`);
2657
+ console.log();
2658
+ return;
2659
+ }
2660
+
2661
+ const provider = providerFromEnv();
2662
+ if (!provider) throw new Error(t.errors.llmNotConfigured());
2663
+
2664
+ const answer = await provider.complete({ system: SEMANTIC_SYSTEM_PROMPT, user: prompt });
2665
+ let proposals: SemanticProposal[] = [];
2666
+ try {
2667
+ const parsed: unknown = JSON.parse(
2668
+ /^(?:```|~~~)[a-zA-Z]*\n([\s\S]*?)\n?(?:```|~~~)$/.exec(answer.trim())?.[1] ?? answer.trim(),
2669
+ );
2670
+ /**
2671
+ * A response that is not the shape asked for is **no proposals**, never a
2672
+ * crash and never a partial read. The model was told exactly what to
2673
+ * return; anything else is a response this layer cannot check, and an
2674
+ * unchecked finding is the one thing this whole module exists to prevent.
2675
+ */
2676
+ if (Array.isArray(parsed)) {
2677
+ proposals = parsed.filter(
2678
+ (entry): entry is SemanticProposal =>
2679
+ typeof entry === 'object' &&
2680
+ entry !== null &&
2681
+ Array.isArray((entry as SemanticProposal).spans) &&
2682
+ (entry as SemanticProposal).spans.length === 2 &&
2683
+ (entry as SemanticProposal).spans.every((span) => typeof span === 'string'),
2684
+ );
2685
+ }
2686
+ } catch {
2687
+ proposals = [];
2688
+ }
2689
+
2690
+ const result = verifySemanticProposals(prompt, proposals);
2691
+ const lineOf = (offset: number): number => prompt.slice(0, offset).split('\n').length;
2692
+
2693
+ console.log();
2694
+ if (result.findings.length === 0) {
2695
+ console.log(` ${c.dim(t.semantic.nothingFound())}`);
2696
+ }
2697
+ for (const finding of result.findings) {
2698
+ console.log(` ${c.bold(t.semantic.finding(finding.kind, finding.because))}`);
2699
+ finding.spans.forEach((span, index) => {
2700
+ const shown = span.length > 90 ? `${span.slice(0, 87)}…` : span;
2701
+ console.log(` ${c.dim(t.semantic.span(String(lineOf(finding.offsets[index] ?? 0)), shown))}`);
2702
+ });
2703
+ console.log(
2704
+ ` ${c.dim(
2705
+ wrap(
2706
+ finding.ceilingTokens > 0 ? t.semantic.ceiling(n(finding.ceilingTokens)) : t.semantic.noCeiling(),
2707
+ 70,
2708
+ ' ',
2709
+ ),
2710
+ )}`,
2711
+ );
2712
+ console.log();
2713
+ }
2714
+
2715
+ /**
2716
+ * What did **not** survive, counted and reasoned.
2717
+ *
2718
+ * A pass that showed only its accepted findings would hide its own hit
2719
+ * rate, and the hit rate is the most useful thing a reader can know about
2720
+ * whether to run it again.
2721
+ */
2722
+ if (result.rejected.length > 0) {
2723
+ console.log(` ${c.dim(t.semantic.rejected(n(result.rejected.length)))}`);
2724
+ for (const { proposal, reason } of result.rejected.slice(0, 5)) {
2725
+ const span = proposal.spans[0];
2726
+ const shown = span.length > 50 ? `${span.slice(0, 47)}…` : span;
2727
+ console.log(` ${c.dim(t.semantic.rejectedLine(reason, shown))}`);
2728
+ }
2729
+ console.log();
2730
+ }
2731
+
2732
+ console.log(` ${c.dim(wrap(t.semantic.disposes(), 74, ' '))}`);
2733
+ console.log(` ${c.dim(wrap(t.semantic.optIn(), 74, ' '))}`);
2734
+ console.log();
2735
+ }
2736
+
2612
2737
  function commandModels(t: CliMessages, pricing: PricingCatalogue): void {
2613
2738
  const n = (value: number): string => value.toLocaleString(t.numberLocale);
2614
2739
  const col = t.models.columns;
@@ -8809,6 +8934,9 @@ async function main(): Promise<void> {
8809
8934
  case 'models':
8810
8935
  commandModels(t, pricing);
8811
8936
  break;
8937
+ case 'semantic':
8938
+ await commandSemantic(args, config, pricing, t);
8939
+ break;
8812
8940
  case 'quality':
8813
8941
  await commandQuality(args, config, pricing, t);
8814
8942
  break;