@trazum/cli 1.50.7 → 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/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { open, readdir, readFile, stat, writeFile } from 'node:fs/promises';
4
4
  import { dirname, join, resolve as resolvePath } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { gunzipSync } from 'node:zlib';
7
- import { applyRewrites, BASELINE_FILENAME, BASELINE_VERSION, breaches, cacheableMinimum, analyzeCachePrefix, billLevers, bucketedCacheEconomics, bucketedProfile, buildHistory, buildPlan, connectorFor, CONNECTORS, normalizeAnthropicUsage, normalizeOpenAIUsage, bucketsFromRecords, evaluateWatch, firedKey, pruneRecords, recordsFromBuckets, storeInventory, storedReportFrom, verifyPlan, cacheEconomics, cacheHitRate, contextPressure, comparePrompts, compareToBaseline, computeSavings, countTokensAnthropic, DEFAULT_USAGE, budgetPositions, conform, BREAK_EVEN_BAND, runExperiment, ladderPosition, validateLadder, outcomeReport, rankPerOutcome, FAILURE_POLICIES, detectFromSource, matchLocale, parsePlanDocument, waiverDay, waiverHistory, proposeInit, MIN_RATE_DAYS, parseConfig, coverageDrift, driversBetween, explainGateFailure, assignSources, fleetRollup, labelCoverage, measuredUsage, gateMargin, GATE_MARGIN_TIGHT, estimateTokens, evaluate, extractPrompts, findExamples, formatBaseline, formatSignedUsd, formatUsd, getMessages, getModel, hasMarker, LOCALES, MAX_BASELINE_BYTES, moneyIsComparable, mostSpecificMatch, nearestName, optimize, parseBaseline, PHRASE_LANGUAGES, plannedCalls, profilePrompt, profileToCsv, profileUsage, promptId, providerFromEnv, pruneExamples, refineWithLlm, rejectionText, reorderForCache, repriceProfile, reviewAgeDays, reviewExamples, RULES, sharedPrefixes, sharesOf, SOURCE_EXTENSIONS, suggestRewrites, toOtlpMetrics, toPromptfoo, TTL_1H_MS, UNLABELLED, withExactTokenCounts, } from '@trazum/core';
7
+ import { applyRewrites, BASELINE_FILENAME, BASELINE_VERSION, breaches, cacheableMinimum, analyzeCachePrefix, billLevers, bucketedCacheEconomics, bucketedProfile, buildHistory, buildPlan, connectorFor, CONNECTORS, normalizeAnthropicUsage, normalizeOpenAIUsage, bucketsFromRecords, evaluateWatch, firedKey, pruneRecords, recordsFromBuckets, storeInventory, storedReportFrom, verifyPlan, cacheEconomics, cacheHitRate, contextPressure, comparePrompts, compareToBaseline, computeSavings, countTokensAnthropic, DEFAULT_USAGE, budgetPositions, conform, BREAK_EVEN_BAND, runExperiment, qualityGate, semanticPassCost, verifySemanticProposals, SEMANTIC_SYSTEM_PROMPT, ladderPosition, validateLadder, outcomeReport, rankPerOutcome, FAILURE_POLICIES, detectFromSource, matchLocale, parsePlanDocument, waiverDay, waiverHistory, proposeInit, MIN_RATE_DAYS, parseConfig, coverageDrift, driversBetween, explainGateFailure, assignSources, fleetRollup, labelCoverage, measuredUsage, gateMargin, GATE_MARGIN_TIGHT, estimateTokens, evaluate, extractPrompts, findExamples, formatBaseline, formatSignedUsd, formatUsd, getMessages, getModel, hasMarker, LOCALES, MAX_BASELINE_BYTES, moneyIsComparable, mostSpecificMatch, nearestName, optimize, parseBaseline, PHRASE_LANGUAGES, plannedCalls, profilePrompt, profileToCsv, profileUsage, promptId, providerFromEnv, pruneExamples, refineWithLlm, rejectionText, reorderForCache, repriceProfile, reviewAgeDays, reviewExamples, RULES, sharedPrefixes, sharesOf, SOURCE_EXTENSIONS, suggestRewrites, toOtlpMetrics, toPromptfoo, TTL_1H_MS, UNLABELLED, withExactTokenCounts, } from '@trazum/core';
8
8
  import { cacheDir, cacheStats, cachingProvider, clearCache } from './suggest-cache.js';
9
9
  import { dayOf, formatGap, median, spanDays } from './time.js';
10
10
  // Everything that reads the filesystem, on its own entry point so the web
@@ -33,6 +33,7 @@ const c = {
33
33
  };
34
34
  const VALUE_FLAGS = new Set([
35
35
  'a',
36
+ 'at',
36
37
  'b',
37
38
  'min-outcomes',
38
39
  'against',
@@ -324,6 +325,8 @@ const COMMAND_FLAGS = {
324
325
  gateway: ['on-cannot-tell', 'port', 'socket', 'pricing', 'pricing-live'],
325
326
  ladder: ['pricing', 'pricing-live', 'since', 'until', 'label'],
326
327
  experiment: ['a', 'b', 'min-outcomes', 'pricing', 'pricing-live'],
328
+ quality: ['label', 'at', 'gate', 'pricing', 'pricing-live'],
329
+ semantic: ['yes', 'model', 'pricing', 'pricing-live'],
327
330
  where: [],
328
331
  rules: [],
329
332
  blame: ['limit', 'model', 'calls', 'output-tokens', 'batch', 'prompt', 'markdown-out'],
@@ -1854,6 +1857,205 @@ async function commandExperiment(args, config, pricing, t) {
1854
1857
  console.log(` ${c.dim(wrap(t.experiment.neverPromotes(), 74, ' '))}`);
1855
1858
  console.log();
1856
1859
  }
1860
+ /**
1861
+ * `trazum quality <log> --label <name> --at <iso> [--gate]`
1862
+ *
1863
+ * The failure that actually matters: a prompt edit that quietly made the
1864
+ * product worse. CI has been able to fail a build for tokens since 1.4 and for
1865
+ * dollars since 1.21, and this has never been gateable — so every saving this
1866
+ * tool has ever recommended went into a repository with its most important
1867
+ * consequence unmeasured.
1868
+ *
1869
+ * **Named `quality` rather than `check --against-outcomes`, which is what the
1870
+ * plan called for.** `check` reads *prompt files* and gates on tokens; it has
1871
+ * never opened a usage log, and a command that takes either a prompt or a log
1872
+ * depending on a flag is two commands wearing one name. The split-by-time this
1873
+ * needs is also not a `check` idea — there is nothing in a prompt file with a
1874
+ * timestamp on it.
1875
+ */
1876
+ async function commandQuality(args, config, pricing, t) {
1877
+ const path = args.positional[0];
1878
+ if (path === undefined)
1879
+ throw new Error(t.errors.missingInputFile());
1880
+ const label = stringFlag(args, 'label');
1881
+ if (label === undefined)
1882
+ throw new Error(t.quality.needsLabel());
1883
+ const atRaw = stringFlag(args, 'at');
1884
+ const atMs = atRaw === undefined ? Number.NaN : Date.parse(atRaw);
1885
+ if (!Number.isFinite(atMs))
1886
+ throw new Error(t.quality.needsAt());
1887
+ /**
1888
+ * Two profiles over the same file, split at the boundary — rather than one
1889
+ * profile the caller has to slice.
1890
+ *
1891
+ * The alternative is asking somebody for two logs, which invites the mistake
1892
+ * this whole module exists to avoid: two files gathered under conditions
1893
+ * nobody wrote down.
1894
+ */
1895
+ const raw = await readUsageLog(path, t);
1896
+ const sideOf = (since, until) => {
1897
+ const report = profileUsage(raw, { catalogue: pricing, label, sinceMs: since, untilMs: until });
1898
+ const slice = report.outcomeTallyByLabel.find((entry) => entry.label === label);
1899
+ return {
1900
+ arm: {
1901
+ name: label,
1902
+ totalUsd: report.total.totalUsd,
1903
+ tally: slice?.tally ?? { byValue: [], recorded: 0, parsed: 0, unrecordedUsd: 0 },
1904
+ },
1905
+ calls: report.total.calls,
1906
+ usdByModel: report.byModel.map((entry) => ({ model: entry.model, usd: entry.breakdown.totalUsd })),
1907
+ };
1908
+ };
1909
+ const result = qualityGate(sideOf(undefined, atMs), sideOf(atMs, undefined), config.outcomes ?? null);
1910
+ const pct = (value) => `${(value * 100).toFixed(1)}%`;
1911
+ const n = (value) => value.toLocaleString(t.numberLocale);
1912
+ console.log();
1913
+ console.log(c.bold(t.quality.heading(label)));
1914
+ console.log(` ${c.dim(wrap(t.quality.notRandomised(), 74, ' '))}`);
1915
+ console.log();
1916
+ console.log(` ${t.quality.sides(result.before.rate === null ? '—' : pct(result.before.rate), result.after.rate === null ? '—' : pct(result.after.rate), n(result.outcomes.before), n(result.outcomes.after))}`);
1917
+ console.log();
1918
+ if (result.verdict === 'dropped') {
1919
+ const cost = result.cost === null
1920
+ ? ''
1921
+ : result.cost.deltaUsdPerCall < 0
1922
+ ? `saves ${formatUsd(-result.cost.deltaUsdPerCall)} a call`
1923
+ : `costs ${formatUsd(result.cost.deltaUsdPerCall)} a call more`;
1924
+ console.log(` ${c.red('✗')} ${wrap(t.quality.dropped(pct(result.before.rate ?? 0), pct(result.after.rate ?? 0), n(result.outcomes.before + result.outcomes.after), cost), 74, ' ')}`);
1925
+ }
1926
+ else if (result.verdict === 'held') {
1927
+ console.log(` ${c.green('✓')} ${wrap(t.quality.held(pct(result.before.rate ?? 0), pct(result.after.rate ?? 0), n(result.outcomes.before + result.outcomes.after)), 74, ' ')}`);
1928
+ }
1929
+ else {
1930
+ const need = result.unknown === 'too-few-before' ? n(result.outcomes.before) : n(result.outcomes.after);
1931
+ console.log(` ${c.yellow('?')} ${wrap(t.quality.cannotTell(result.unknown ?? '', need), 74, ' ')}`);
1932
+ }
1933
+ /**
1934
+ * Confounders print on **every** verdict, not only on `cannot-tell`.
1935
+ *
1936
+ * A rate that held while the model changed underneath is not evidence that
1937
+ * the prompt is fine either, and hiding the confounder on a green result is
1938
+ * how a gate teaches people to trust it in exactly the case it should not be
1939
+ * trusted.
1940
+ */
1941
+ if (result.confounders.length > 0) {
1942
+ console.log();
1943
+ console.log(` ${c.bold(t.quality.confoundersHeading())}`);
1944
+ for (const confounder of result.confounders) {
1945
+ const detail = confounder.kind === 'model-mix-moved'
1946
+ ? `${pct(confounder.drift)} (${confounder.model})`
1947
+ : confounder.kind === 'volume-moved'
1948
+ ? `${n(confounder.beforeCalls)} → ${n(confounder.afterCalls)} calls`
1949
+ : `${pct(confounder.before)} → ${pct(confounder.after)}`;
1950
+ console.log(` ${c.yellow('!')} ${wrap(t.quality.confounder(confounder.kind, detail), 70, ' ')}`);
1951
+ }
1952
+ }
1953
+ console.log();
1954
+ console.log(` ${c.dim(wrap(t.quality.cannotSee(), 74, ' '))}`);
1955
+ if (boolFlag(args, 'gate')) {
1956
+ console.log();
1957
+ if (result.verdict === 'dropped') {
1958
+ console.log(` ${c.red(t.quality.gateFailed())}`);
1959
+ process.exitCode = 1;
1960
+ }
1961
+ else if (result.verdict === 'cannot-tell') {
1962
+ // Three outcomes, never two. `cannot tell` holds the claim open rather
1963
+ // than exiting green, the posture `verify --gate` has had since 1.39.
1964
+ console.log(` ${c.yellow(t.quality.gateHeldOpen())}`);
1965
+ process.exitCode = 2;
1966
+ }
1967
+ }
1968
+ console.log();
1969
+ }
1970
+ /**
1971
+ * `trazum semantic <prompt> [--yes]` — the findings a dictionary cannot see.
1972
+ *
1973
+ * The rules engine has deferred these since 0.1.0 for one honest reason: a
1974
+ * dictionary cannot see meaning, and a model that hallucinates a finding is
1975
+ * worse than a rule that misses one.
1976
+ *
1977
+ * **The price is printed before anything is sent, and `--yes` is required.** A
1978
+ * tool that spends somebody's money to tell them how to spend less has to be
1979
+ * the first thing audited by its own arithmetic, and it has to ask.
1980
+ */
1981
+ async function commandSemantic(args, config, pricing, t) {
1982
+ const prompt = await readInput(args.positional[0], t);
1983
+ const modelId = stringFlag(args, 'model') ?? config.usage?.model ?? DEFAULT_USAGE.model;
1984
+ const model = pricing.byId.get(modelId) ?? getModel(DEFAULT_USAGE.model);
1985
+ const rates = { inputPerMTok: model.inputPerMTok, outputPerMTok: model.outputPerMTok };
1986
+ const cost = semanticPassCost(prompt, rates);
1987
+ const n = (value) => value.toLocaleString(t.numberLocale);
1988
+ console.log();
1989
+ console.log(c.bold(t.semantic.heading(args.positional[0] ?? '-')));
1990
+ console.log();
1991
+ console.log(` ${wrap(t.semantic.willCost(formatUsd(cost.usd), n(cost.inputTokens), n(cost.outputTokens), model.displayName), 74, ' ')}`);
1992
+ if (!boolFlag(args, 'yes')) {
1993
+ // Nothing has been sent at this point, and nothing will be. The price
1994
+ // above is the whole output of a run without --yes.
1995
+ console.log();
1996
+ console.log(` ${c.dim(t.semantic.needsYes())}`);
1997
+ console.log();
1998
+ return;
1999
+ }
2000
+ const provider = providerFromEnv();
2001
+ if (!provider)
2002
+ throw new Error(t.errors.llmNotConfigured());
2003
+ const answer = await provider.complete({ system: SEMANTIC_SYSTEM_PROMPT, user: prompt });
2004
+ let proposals = [];
2005
+ try {
2006
+ const parsed = JSON.parse(/^(?:```|~~~)[a-zA-Z]*\n([\s\S]*?)\n?(?:```|~~~)$/.exec(answer.trim())?.[1] ?? answer.trim());
2007
+ /**
2008
+ * A response that is not the shape asked for is **no proposals**, never a
2009
+ * crash and never a partial read. The model was told exactly what to
2010
+ * return; anything else is a response this layer cannot check, and an
2011
+ * unchecked finding is the one thing this whole module exists to prevent.
2012
+ */
2013
+ if (Array.isArray(parsed)) {
2014
+ proposals = parsed.filter((entry) => typeof entry === 'object' &&
2015
+ entry !== null &&
2016
+ Array.isArray(entry.spans) &&
2017
+ entry.spans.length === 2 &&
2018
+ entry.spans.every((span) => typeof span === 'string'));
2019
+ }
2020
+ }
2021
+ catch {
2022
+ proposals = [];
2023
+ }
2024
+ const result = verifySemanticProposals(prompt, proposals);
2025
+ const lineOf = (offset) => prompt.slice(0, offset).split('\n').length;
2026
+ console.log();
2027
+ if (result.findings.length === 0) {
2028
+ console.log(` ${c.dim(t.semantic.nothingFound())}`);
2029
+ }
2030
+ for (const finding of result.findings) {
2031
+ console.log(` ${c.bold(t.semantic.finding(finding.kind, finding.because))}`);
2032
+ finding.spans.forEach((span, index) => {
2033
+ const shown = span.length > 90 ? `${span.slice(0, 87)}…` : span;
2034
+ console.log(` ${c.dim(t.semantic.span(String(lineOf(finding.offsets[index] ?? 0)), shown))}`);
2035
+ });
2036
+ console.log(` ${c.dim(wrap(finding.ceilingTokens > 0 ? t.semantic.ceiling(n(finding.ceilingTokens)) : t.semantic.noCeiling(), 70, ' '))}`);
2037
+ console.log();
2038
+ }
2039
+ /**
2040
+ * What did **not** survive, counted and reasoned.
2041
+ *
2042
+ * A pass that showed only its accepted findings would hide its own hit
2043
+ * rate, and the hit rate is the most useful thing a reader can know about
2044
+ * whether to run it again.
2045
+ */
2046
+ if (result.rejected.length > 0) {
2047
+ console.log(` ${c.dim(t.semantic.rejected(n(result.rejected.length)))}`);
2048
+ for (const { proposal, reason } of result.rejected.slice(0, 5)) {
2049
+ const span = proposal.spans[0];
2050
+ const shown = span.length > 50 ? `${span.slice(0, 47)}…` : span;
2051
+ console.log(` ${c.dim(t.semantic.rejectedLine(reason, shown))}`);
2052
+ }
2053
+ console.log();
2054
+ }
2055
+ console.log(` ${c.dim(wrap(t.semantic.disposes(), 74, ' '))}`);
2056
+ console.log(` ${c.dim(wrap(t.semantic.optIn(), 74, ' '))}`);
2057
+ console.log();
2058
+ }
1857
2059
  function commandModels(t, pricing) {
1858
2060
  const n = (value) => value.toLocaleString(t.numberLocale);
1859
2061
  const col = t.models.columns;
@@ -6901,6 +7103,12 @@ async function main() {
6901
7103
  case 'models':
6902
7104
  commandModels(t, pricing);
6903
7105
  break;
7106
+ case 'semantic':
7107
+ await commandSemantic(args, config, pricing, t);
7108
+ break;
7109
+ case 'quality':
7110
+ await commandQuality(args, config, pricing, t);
7111
+ break;
6904
7112
  case 'experiment':
6905
7113
  await commandExperiment(args, config, pricing, t);
6906
7114
  break;