@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/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { open, readdir, readFile, stat, writeFile } from 'node:fs/promises';
3
3
  import { join, resolve as resolvePath } from 'node:path';
4
4
  import { gunzipSync } from 'node:zlib';
5
- 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, 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';
5
+ 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, 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';
6
6
  import { cacheDir, cacheStats, cachingProvider, clearCache } from './suggest-cache.js';
7
7
  import { dayOf, formatGap, median, spanDays } from './time.js';
8
8
  // Everything that reads the filesystem, on its own entry point so the web
@@ -30,6 +30,7 @@ const c = {
30
30
  };
31
31
  const VALUE_FLAGS = new Set([
32
32
  'against',
33
+ 'contract',
33
34
  'from-log',
34
35
  'min-usd',
35
36
  'payload',
@@ -311,6 +312,7 @@ const COMMAND_FLAGS = {
311
312
  models: [],
312
313
  rank: ['level', 'model', 'calls', 'output-tokens', 'batch', 'disable', 'prompt', 'markdown-out'],
313
314
  init: ['dry-run', 'yes', 'json', 'pricing', 'pricing-live'],
315
+ conform: ['contract', 'json'],
314
316
  where: [],
315
317
  rules: [],
316
318
  blame: ['limit', 'model', 'calls', 'output-tokens', 'batch', 'prompt', 'markdown-out'],
@@ -1037,6 +1039,18 @@ const INIT_LOG_CANDIDATES = [
1037
1039
  * answers for a file somebody named while this answers for a repository
1038
1040
  * nobody has vouched for.
1039
1041
  */
1042
+ /** Problems listed before the rest are counted. A wall of them helps nobody. */
1043
+ const MAX_CONFORM_PROBLEMS = 20;
1044
+ /** The contracts `--contract` accepts, so a typo is refused with the list. */
1045
+ const CONTRACT_NAMES = [
1046
+ 'usage-log',
1047
+ 'profile',
1048
+ 'plan',
1049
+ 'verification',
1050
+ 'history',
1051
+ 'connected',
1052
+ 'cost-answer',
1053
+ ];
1040
1054
  /** How many source files, and how large each may be. Both reported when they bite. */
1041
1055
  const INIT_MAX_SOURCE_FILES = 400;
1042
1056
  const INIT_MAX_SOURCE_BYTES = 256 * 1024;
@@ -1381,6 +1395,78 @@ async function commandInit(args, config, pricing, t) {
1381
1395
  console.log(c.green(t.init.wrote(configPath)));
1382
1396
  console.log();
1383
1397
  }
1398
+ /**
1399
+ * `trazum conform <file>` — does this document conform, and what will it not
1400
+ * be able to answer?
1401
+ *
1402
+ * The command that makes the five contracts something to build against rather
1403
+ * than something to read about. An emitter — a logging wrapper somebody wrote
1404
+ * this afternoon, a connector for a provider this repository has never heard
1405
+ * of, a dashboard writing profile documents of its own — points this at what
1406
+ * it produced and finds out before shipping.
1407
+ *
1408
+ * **The second half is the useful half.** "Valid" is a yes or no. "Here is
1409
+ * what a valid document of this shape cannot tell you, and the field that
1410
+ * would unlock each" is the answer somebody acts on: a usage log with no
1411
+ * `session` is perfectly conformant and simply has no conversation growth in
1412
+ * it, and an emitter that only ever hears "valid" ships it and never finds out
1413
+ * why half the report is empty.
1414
+ *
1415
+ * Exits 1 on a problem, so it gates. It never exits 1 on an *unavailable
1416
+ * finding*: choosing not to log sessions is a decision, not a defect, and a
1417
+ * gate that failed on it would be this tool telling somebody what to record.
1418
+ */
1419
+ async function commandConform(args, t) {
1420
+ const target = args.positional[0];
1421
+ if (target === undefined)
1422
+ throw new Error(t.conform.noTarget());
1423
+ const named = stringFlag(args, 'contract');
1424
+ if (named !== undefined && !CONTRACT_NAMES.includes(named)) {
1425
+ throw new Error(t.conform.badContract(named, CONTRACT_NAMES.join(', ')));
1426
+ }
1427
+ const text = target === '-' ? await readInput('-', t) : await readUsageLog(target, t);
1428
+ const report = conform(text, named === undefined ? {} : { contract: named });
1429
+ if (boolFlag(args, 'json')) {
1430
+ console.log(JSON.stringify(report, null, 2));
1431
+ if (!report.conforms)
1432
+ process.exitCode = 1;
1433
+ return;
1434
+ }
1435
+ console.log();
1436
+ if (report.contract === null) {
1437
+ console.log(c.red(t.conform.unrecognised(target)));
1438
+ console.log(` ${c.dim(wrap(report.because ?? '', 74, ' '))}`);
1439
+ console.log();
1440
+ process.exitCode = 1;
1441
+ return;
1442
+ }
1443
+ console.log(c.bold(report.records === null
1444
+ ? t.conform.heading(target, report.contract)
1445
+ : t.conform.headingLog(target, report.contract, report.records)));
1446
+ if (report.problems.length === 0) {
1447
+ console.log(` ${c.green(t.conform.conforms())}`);
1448
+ }
1449
+ else {
1450
+ for (const problem of report.problems.slice(0, MAX_CONFORM_PROBLEMS)) {
1451
+ console.log(` ${c.red(t.conform.problem(problem.at, problem.kind, problem.detail))}`);
1452
+ }
1453
+ if (report.problems.length > MAX_CONFORM_PROBLEMS) {
1454
+ console.log(` ${c.dim(t.conform.moreProblems(report.problems.length - MAX_CONFORM_PROBLEMS))}`);
1455
+ }
1456
+ process.exitCode = 1;
1457
+ }
1458
+ if (report.unavailable.length > 0) {
1459
+ console.log();
1460
+ console.log(c.bold(t.conform.unavailableHeading()));
1461
+ for (const gap of report.unavailable) {
1462
+ console.log(` ${c.dim(wrap(t.conform.unavailable(gap.finding, gap.because, gap.unlockedBy), 74, ' '))}`);
1463
+ }
1464
+ // Said out loud, because the exit code says it silently and somebody
1465
+ // reading a red-and-yellow screen will assume both halves gated.
1466
+ console.log(` ${c.dim(wrap(t.conform.unavailableNeverGates(), 74, ' '))}`);
1467
+ }
1468
+ console.log();
1469
+ }
1384
1470
  function commandModels(t, pricing) {
1385
1471
  const n = (value) => value.toLocaleString(t.numberLocale);
1386
1472
  const col = t.models.columns;
@@ -2082,36 +2168,36 @@ function parseWhen(args, flag, endOfDay, t, now) {
2082
2168
  */
2083
2169
  async function commandServe(args, config, pricing, t) {
2084
2170
  const root = process.cwd();
2085
- const limitUsd = config.spend?.maxUsd;
2086
2171
  const { resolved } = await readStore(root);
2087
- const measured = resolved.records.length > 0;
2088
2172
  /**
2089
- * The window the measurement covers, carried into every answer.
2173
+ * The live budget, from `budgetPositions` the same number `store` prints
2174
+ * and the same one the MCP guard consults.
2090
2175
  *
2091
- * The position is read once at start, so a caller has to be able to see how
2092
- * old it is. A null window here would let a figure from last month read as
2093
- * current, which is the staleness this endpoint is otherwise honest about.
2176
+ * **This used to read `spend.maxUsd` against the whole store**, which is a
2177
+ * per-log gate compared against however much history the store happened to
2178
+ * hold. A year of records against a monthly limit reported as a budget
2179
+ * position, with a straight face and no way for a caller to tell. Same
2180
+ * units, different denominators, and the two surfaces disagreed by exactly
2181
+ * as much history as the machine had. `spend.monthlyUsd` is the key for a
2182
+ * calendar month and nothing infers one key from the other: a repository
2183
+ * with a per-log gate and no monthly budget has no monthly position, and
2184
+ * this says so rather than picking a number that is the right shape.
2094
2185
  */
2095
- const window = measured
2096
- ? {
2097
- fromMs: Math.min(...resolved.records.map((record) => record.fromMs)),
2098
- toMs: Math.max(...resolved.records.map((record) => record.toMs)),
2099
- }
2100
- : null;
2101
- const report = bucketedProfile({
2102
- provider: 'store',
2103
- granularity: 'bucketed',
2104
- buckets: bucketsFromRecords(resolved.records),
2105
- window,
2106
- gaps: [],
2107
- unavailable: [],
2108
- }, { catalogue: pricing });
2186
+ const budget = budgetPositions(resolved.records, config.spend, { catalogue: pricing });
2187
+ const standing = budget.positions[0] ?? null;
2188
+ const limitUsd = config.spend?.monthlyUsd;
2189
+ const measured = standing !== null && standing.coverage !== 'none';
2109
2190
  const server = buildServer({
2110
2191
  catalogue: pricing,
2111
2192
  position: () => ({
2112
- consumedUsd: measured ? report.total.totalUsd : undefined,
2193
+ // Nothing measured inside the period is `undefined`, never zero: the
2194
+ // endpoint's `cannot-tell` exists for exactly this, and a $0 consumed
2195
+ // would be the healthiest-looking budget a dead store can produce.
2196
+ consumedUsd: measured ? standing.consumedUsd : undefined,
2113
2197
  limitUsd,
2114
- window: report.span,
2198
+ // The period, not the store's span. A caller judging staleness needs to
2199
+ // know which month the figure is about.
2200
+ window: standing === null ? null : { fromMs: standing.period.fromMs, toMs: standing.period.toMs },
2115
2201
  }),
2116
2202
  });
2117
2203
  const socket = stringFlag(args, 'socket');
@@ -2123,7 +2209,10 @@ async function commandServe(args, config, pricing, t) {
2123
2209
  const where = await listen(server, socket !== undefined ? { socket } : { port });
2124
2210
  console.log(c.bold(t.serve.listening(where)));
2125
2211
  console.log(` ${c.dim(wrap(t.serve.loopbackOnly(), 74, ' '))}`);
2126
- console.log(` ${c.dim(wrap(measured ? t.serve.measuredFrom(formatUsd(report.total.totalUsd)) : t.serve.nothingMeasured(STORE_DIR), 74, ' '))}`);
2212
+ console.log(` ${c.dim(wrap(measured ? t.serve.measuredFrom(formatUsd(standing.consumedUsd)) : t.serve.nothingMeasured(STORE_DIR), 74, ' '))}`);
2213
+ if (standing !== null && standing.coverage === 'partial') {
2214
+ console.log(` ${c.yellow(wrap(t.serve.partialCoverage(standing.measuredDays, standing.elapsedDays, standing.period.id), 74, ' '))}`);
2215
+ }
2127
2216
  if (limitUsd === undefined) {
2128
2217
  console.log(` ${c.dim(wrap(t.serve.noBudget(), 74, ' '))}`);
2129
2218
  }
@@ -2376,6 +2465,51 @@ async function commandStore(args, config, pricing, t) {
2376
2465
  }
2377
2466
  const keepDays = config.store?.keepDays;
2378
2467
  console.log(` ${c.dim(wrap(keepDays === undefined ? t.store.noRetention() : t.store.retention(String(keepDays)), 74, ' '))}`);
2468
+ /**
2469
+ * The live budget, printed here because this is where the measurement lives.
2470
+ *
2471
+ * The same call `serve` makes and the same call the MCP guard makes, so the
2472
+ * three cannot disagree about how much of the month is gone — which is the
2473
+ * whole point of the number existing in one place.
2474
+ */
2475
+ renderBudget(budgetPositions(resolved.records, config.spend, { catalogue: pricing }), t, n);
2476
+ }
2477
+ /**
2478
+ * One budget standing, rendered.
2479
+ *
2480
+ * Coverage before the money, deliberately. A reader who sees "$61 of $100"
2481
+ * first has already formed a view by the time they reach "over three of
2482
+ * nineteen elapsed days", and the second sentence has to undo the first.
2483
+ */
2484
+ function renderBudget(report, t, n) {
2485
+ const standing = report.positions[0];
2486
+ if (standing === undefined) {
2487
+ if (report.unmeasuredScopes.length > 0) {
2488
+ console.log();
2489
+ console.log(` ${c.dim(wrap(t.store.budgetScopesUnmeasured(report.unmeasuredScopes.length), 74, ' '))}`);
2490
+ }
2491
+ return;
2492
+ }
2493
+ console.log();
2494
+ console.log(c.bold(t.store.budgetHeading(standing.period.id)));
2495
+ if (standing.coverage === 'none') {
2496
+ // Nothing measured is never rendered as nothing spent. A dead store and a
2497
+ // quiet month produce the same zero, and only one of them is good news.
2498
+ console.log(` ${c.red(wrap(t.store.budgetNothingMeasured(standing.elapsedDays), 74, ' '))}`);
2499
+ return;
2500
+ }
2501
+ if (standing.coverage === 'partial') {
2502
+ console.log(` ${c.yellow(wrap(t.store.budgetPartial(standing.measuredDays, standing.elapsedDays, standing.unmeasuredDays.join(', ')), 74, ' '))}`);
2503
+ }
2504
+ const share = standing.burn.consumedShare;
2505
+ console.log(` ${t.store.budgetStanding(formatUsd(standing.consumedUsd), formatUsd(standing.limitUsd), share === null ? '—' : `${Math.round(share * 100)}%`, n(standing.measuredDays), n(standing.period.days))}`);
2506
+ const line = t.store.budgetShape(standing.burn.shape, Math.round(standing.burn.elapsedShare * 100), standing.coverage);
2507
+ console.log(` ${standing.verdict === 'over' ? c.red(line) : c.dim(wrap(line, 74, ' '))}`);
2508
+ // Only where there is a shape to disclaim. "That is a shape, not a forecast"
2509
+ // under "nothing to compare against" is a disclaimer about nothing.
2510
+ if (standing.burn.shape !== 'cannot-tell') {
2511
+ console.log(` ${c.dim(wrap(t.store.budgetNeverForecast(), 74, ' '))}`);
2512
+ }
2379
2513
  }
2380
2514
  /**
2381
2515
  * `trazum connect <provider>` — the bill, read from the provider.
@@ -6231,6 +6365,9 @@ async function main() {
6231
6365
  case 'models':
6232
6366
  commandModels(t, pricing);
6233
6367
  break;
6368
+ case 'conform':
6369
+ await commandConform(args, t);
6370
+ break;
6234
6371
  case 'init':
6235
6372
  await commandInit(args, config, pricing, t);
6236
6373
  break;