@trazum/cli 1.50.8 → 1.50.10

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, qualityGate, 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, allocate, 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
@@ -326,6 +326,8 @@ const COMMAND_FLAGS = {
326
326
  ladder: ['pricing', 'pricing-live', 'since', 'until', 'label'],
327
327
  experiment: ['a', 'b', 'min-outcomes', 'pricing', 'pricing-live'],
328
328
  quality: ['label', 'at', 'gate', 'pricing', 'pricing-live'],
329
+ semantic: ['yes', 'model', 'pricing', 'pricing-live'],
330
+ owners: ['pricing', 'pricing-live', 'since', 'until'],
329
331
  where: [],
330
332
  rules: [],
331
333
  blame: ['limit', 'model', 'calls', 'output-tokens', 'batch', 'prompt', 'markdown-out'],
@@ -1966,6 +1968,204 @@ async function commandQuality(args, config, pricing, t) {
1966
1968
  }
1967
1969
  console.log();
1968
1970
  }
1971
+ /**
1972
+ * `trazum semantic <prompt> [--yes]` — the findings a dictionary cannot see.
1973
+ *
1974
+ * The rules engine has deferred these since 0.1.0 for one honest reason: a
1975
+ * dictionary cannot see meaning, and a model that hallucinates a finding is
1976
+ * worse than a rule that misses one.
1977
+ *
1978
+ * **The price is printed before anything is sent, and `--yes` is required.** A
1979
+ * tool that spends somebody's money to tell them how to spend less has to be
1980
+ * the first thing audited by its own arithmetic, and it has to ask.
1981
+ */
1982
+ async function commandSemantic(args, config, pricing, t) {
1983
+ const prompt = await readInput(args.positional[0], t);
1984
+ const modelId = stringFlag(args, 'model') ?? config.usage?.model ?? DEFAULT_USAGE.model;
1985
+ const model = pricing.byId.get(modelId) ?? getModel(DEFAULT_USAGE.model);
1986
+ const rates = { inputPerMTok: model.inputPerMTok, outputPerMTok: model.outputPerMTok };
1987
+ const cost = semanticPassCost(prompt, rates);
1988
+ const n = (value) => value.toLocaleString(t.numberLocale);
1989
+ console.log();
1990
+ console.log(c.bold(t.semantic.heading(args.positional[0] ?? '-')));
1991
+ console.log();
1992
+ console.log(` ${wrap(t.semantic.willCost(formatUsd(cost.usd), n(cost.inputTokens), n(cost.outputTokens), model.displayName), 74, ' ')}`);
1993
+ if (!boolFlag(args, 'yes')) {
1994
+ // Nothing has been sent at this point, and nothing will be. The price
1995
+ // above is the whole output of a run without --yes.
1996
+ console.log();
1997
+ console.log(` ${c.dim(t.semantic.needsYes())}`);
1998
+ console.log();
1999
+ return;
2000
+ }
2001
+ const provider = providerFromEnv();
2002
+ if (!provider)
2003
+ throw new Error(t.errors.llmNotConfigured());
2004
+ const answer = await provider.complete({ system: SEMANTIC_SYSTEM_PROMPT, user: prompt });
2005
+ let proposals = [];
2006
+ try {
2007
+ const parsed = JSON.parse(/^(?:```|~~~)[a-zA-Z]*\n([\s\S]*?)\n?(?:```|~~~)$/.exec(answer.trim())?.[1] ?? answer.trim());
2008
+ /**
2009
+ * A response that is not the shape asked for is **no proposals**, never a
2010
+ * crash and never a partial read. The model was told exactly what to
2011
+ * return; anything else is a response this layer cannot check, and an
2012
+ * unchecked finding is the one thing this whole module exists to prevent.
2013
+ */
2014
+ if (Array.isArray(parsed)) {
2015
+ proposals = parsed.filter((entry) => typeof entry === 'object' &&
2016
+ entry !== null &&
2017
+ Array.isArray(entry.spans) &&
2018
+ entry.spans.length === 2 &&
2019
+ entry.spans.every((span) => typeof span === 'string'));
2020
+ }
2021
+ }
2022
+ catch {
2023
+ proposals = [];
2024
+ }
2025
+ const result = verifySemanticProposals(prompt, proposals);
2026
+ const lineOf = (offset) => prompt.slice(0, offset).split('\n').length;
2027
+ console.log();
2028
+ if (result.findings.length === 0) {
2029
+ console.log(` ${c.dim(t.semantic.nothingFound())}`);
2030
+ }
2031
+ for (const finding of result.findings) {
2032
+ console.log(` ${c.bold(t.semantic.finding(finding.kind, finding.because))}`);
2033
+ finding.spans.forEach((span, index) => {
2034
+ const shown = span.length > 90 ? `${span.slice(0, 87)}…` : span;
2035
+ console.log(` ${c.dim(t.semantic.span(String(lineOf(finding.offsets[index] ?? 0)), shown))}`);
2036
+ });
2037
+ console.log(` ${c.dim(wrap(finding.ceilingTokens > 0 ? t.semantic.ceiling(n(finding.ceilingTokens)) : t.semantic.noCeiling(), 70, ' '))}`);
2038
+ console.log();
2039
+ }
2040
+ /**
2041
+ * What did **not** survive, counted and reasoned.
2042
+ *
2043
+ * A pass that showed only its accepted findings would hide its own hit
2044
+ * rate, and the hit rate is the most useful thing a reader can know about
2045
+ * whether to run it again.
2046
+ */
2047
+ if (result.rejected.length > 0) {
2048
+ console.log(` ${c.dim(t.semantic.rejected(n(result.rejected.length)))}`);
2049
+ for (const { proposal, reason } of result.rejected.slice(0, 5)) {
2050
+ const span = proposal.spans[0];
2051
+ const shown = span.length > 50 ? `${span.slice(0, 47)}…` : span;
2052
+ console.log(` ${c.dim(t.semantic.rejectedLine(reason, shown))}`);
2053
+ }
2054
+ console.log();
2055
+ }
2056
+ console.log(` ${c.dim(wrap(t.semantic.disposes(), 74, ' '))}`);
2057
+ console.log(` ${c.dim(wrap(t.semantic.optIn(), 74, ' '))}`);
2058
+ console.log();
2059
+ }
2060
+ /**
2061
+ * `trazum owners <log>` — whose budget each workload lands on.
2062
+ *
2063
+ * The fleet answered *which service* in 1.37. This answers *whose money*,
2064
+ * which is the question that decides whether anything on the list gets done: a
2065
+ * report saying "the bill is $40,000 and here is $9,000 of savings" is read by
2066
+ * four people who each assume it is one of the other three's problem.
2067
+ *
2068
+ * **The unallocated is its own line and is never spread.** See `owners.ts` for
2069
+ * why that is worth breaking a module over.
2070
+ */
2071
+ async function commandOwners(args, config, pricing, t) {
2072
+ const path = args.positional[0];
2073
+ if (path === undefined)
2074
+ throw new Error(t.errors.missingInputFile());
2075
+ console.log();
2076
+ console.log(c.bold(t.owners.heading()));
2077
+ if (config.owners === undefined) {
2078
+ console.log(` ${c.dim(wrap(t.owners.noOwners(), 74, ' '))}`);
2079
+ console.log();
2080
+ return;
2081
+ }
2082
+ const report = profileUsage(await readUsageLog(path, t), { catalogue: pricing });
2083
+ const result = allocate(report.byLabel.map((entry) => ({
2084
+ label: entry.label,
2085
+ usd: entry.breakdown.totalUsd,
2086
+ calls: entry.breakdown.calls,
2087
+ })), config.owners);
2088
+ const n = (value) => value.toLocaleString(t.numberLocale);
2089
+ const pct = (value) => `${(value * 100).toFixed(1)}%`;
2090
+ /**
2091
+ * Problems first, before any figure.
2092
+ *
2093
+ * A split that does not sum to one sends a whole workload to unallocated,
2094
+ * and a reader who saw the table before the explanation would go looking for
2095
+ * a bug in their logs.
2096
+ */
2097
+ if (result.problems.length > 0) {
2098
+ console.log();
2099
+ console.log(` ${c.red('✗')} ${c.bold(t.owners.problemsHeading())}`);
2100
+ for (const problem of result.problems) {
2101
+ const detail = problem.kind === 'budget-for-unknown-owner'
2102
+ ? problem.owner
2103
+ : problem.kind === 'split-does-not-sum'
2104
+ ? `"${problem.label}" sums to ${problem.total}`
2105
+ : problem.kind === 'negative-share' || problem.kind === 'split-names-unknown-owner'
2106
+ ? `"${problem.label}" → ${problem.owner}`
2107
+ : `"${problem.label}"`;
2108
+ console.log(` ${wrap(t.owners.problem(problem.kind, detail), 70, ' ')}`);
2109
+ }
2110
+ process.exitCode = 1;
2111
+ }
2112
+ console.log();
2113
+ const col = t.owners.columns;
2114
+ const rows = result.owners.map((line) => ({
2115
+ owner: line.owner,
2116
+ spend: formatUsd(line.usd),
2117
+ budget: line.budgetUsd === null ? '—' : formatUsd(line.budgetUsd),
2118
+ calls: n(Math.round(line.calls)),
2119
+ verdict: t.owners.verdict(line.verdict),
2120
+ kind: line.verdict,
2121
+ }));
2122
+ const w = {
2123
+ owner: Math.max(...rows.map((r) => r.owner.length), col.owner.length),
2124
+ spend: Math.max(...rows.map((r) => r.spend.length), col.spend.length),
2125
+ budget: Math.max(...rows.map((r) => r.budget.length), col.budget.length),
2126
+ calls: Math.max(...rows.map((r) => r.calls.length), col.calls.length),
2127
+ };
2128
+ console.log(c.dim(` ${col.owner.padEnd(w.owner)} ${col.spend.padStart(w.spend)} ` +
2129
+ `${col.budget.padStart(w.budget)} ${col.calls.padStart(w.calls)}`));
2130
+ for (const row of rows) {
2131
+ const tint = row.kind === 'over' ? c.red : row.kind === 'not-measured' ? c.yellow : c.dim;
2132
+ console.log(` ${row.owner.padEnd(w.owner)} ${row.spend.padStart(w.spend)} ` +
2133
+ `${row.budget.padStart(w.budget)} ${row.calls.padStart(w.calls)} ${tint(row.verdict)}`);
2134
+ }
2135
+ // The 1.37 refusal, applied to people, said in full for each owner it hits.
2136
+ for (const line of result.owners) {
2137
+ if (line.verdict === 'not-measured') {
2138
+ console.log();
2139
+ console.log(` ${c.yellow('!')} ${wrap(t.owners.notMeasured(line.owner), 74, ' ')}`);
2140
+ }
2141
+ }
2142
+ console.log();
2143
+ if (result.unallocated.usd > 0) {
2144
+ console.log(` ${c.yellow('!')} ${wrap(t.owners.unallocated(formatUsd(result.unallocated.usd), report.total.totalUsd > 0 ? pct(result.unallocated.usd / report.total.totalUsd) : '—', result.unallocated.labels.slice(0, 6).join(', ')), 74, ' ')}`);
2145
+ console.log(` ${c.dim(wrap(t.owners.neverSpread(), 72, ' '))}`);
2146
+ }
2147
+ else {
2148
+ console.log(` ${c.dim(t.owners.nothingUnallocated())}`);
2149
+ }
2150
+ /**
2151
+ * The shared rules, printed with the report.
2152
+ *
2153
+ * The whole design: the argument then happens about the rule — "why is
2154
+ * search 60/40?" — rather than about the number, which is an argument nobody
2155
+ * can win because nobody can see where the number came from.
2156
+ */
2157
+ if (result.sharedApplied.length > 0) {
2158
+ console.log();
2159
+ console.log(` ${c.bold(t.owners.sharedHeading())}`);
2160
+ for (const { label, split } of result.sharedApplied) {
2161
+ const rule = Object.entries(split)
2162
+ .map(([owner, share]) => `${owner} ${pct(share)}`)
2163
+ .join(', ');
2164
+ console.log(` ${c.dim(t.owners.sharedRule(label, rule))}`);
2165
+ }
2166
+ }
2167
+ console.log();
2168
+ }
1969
2169
  function commandModels(t, pricing) {
1970
2170
  const n = (value) => value.toLocaleString(t.numberLocale);
1971
2171
  const col = t.models.columns;
@@ -7013,6 +7213,12 @@ async function main() {
7013
7213
  case 'models':
7014
7214
  commandModels(t, pricing);
7015
7215
  break;
7216
+ case 'owners':
7217
+ await commandOwners(args, config, pricing, t);
7218
+ break;
7219
+ case 'semantic':
7220
+ await commandSemantic(args, config, pricing, t);
7221
+ break;
7016
7222
  case 'quality':
7017
7223
  await commandQuality(args, config, pricing, t);
7018
7224
  break;