@trazum/cli 1.46.0 → 1.48.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, 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, 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
@@ -11,6 +11,7 @@ import { CONFIG_FILENAME, DEFAULT_EXTENSIONS, budgetFor, BUNDLED_CATALOGUE, SAFE
11
11
  import { contentAt, gitAvailable, namesByRevision, pathInRepository, repositoryRoot, revisionsFor, } from './git.js';
12
12
  import { fetchProviderUsage, findCredential } from './connect.js';
13
13
  import { STORE_DIR, appendRecords, readStore, rewriteStore } from './store-fs.js';
14
+ import { WAIVER_LOG, appendWaiverUse, readWaiverLog } from './waiver-log.js';
14
15
  import { DEFAULT_PORT, buildServer, listen } from './serve.js';
15
16
  import { WATCH_STATE_VERSION, checkWebhook, postWebhook, readWatchState, writeWatchState, } from './watch-run.js';
16
17
  import { LOCALE_ENV_VARS, detectLocale, getCliMessages } from './i18n/index.js';
@@ -2518,7 +2519,7 @@ async function commandConnect(args, pricing, t) {
2518
2519
  * same action planned twice — and no series, however long, becomes a
2519
2520
  * forecast.
2520
2521
  */
2521
- async function commandHistory(args, pricing, t) {
2522
+ async function commandHistory(args, config, pricing, t) {
2522
2523
  /**
2523
2524
  * `--store` builds the series from measured spend already on disk.
2524
2525
  *
@@ -2617,7 +2618,22 @@ async function commandHistory(args, pricing, t) {
2617
2618
  if (history.periods.length < 3) {
2618
2619
  throw new Error(t.history.needsThree(String(history.periods.length)));
2619
2620
  }
2620
- const stamped = { ...history, unrecognizedFiles: unrecognized };
2621
+ /**
2622
+ * The waiver record — closing the gap 1.40 named and could not fill.
2623
+ *
2624
+ * 1.40 wanted to say "this finding has been waived three times in a row" and
2625
+ * refused to, because the only material available was the config as it
2626
+ * stands, and a past reconstructed from a present is a guess wearing a
2627
+ * record's clothes. The material exists now: since 1.48 a waiver that
2628
+ * silences a gate writes down that it did, and this reads those lines back.
2629
+ *
2630
+ * Read from the working directory rather than from the reports directory:
2631
+ * the waiver record belongs to the repository whose gates fired, and the
2632
+ * stored reports may have come from anywhere.
2633
+ */
2634
+ const waivers = await readWaiverLog('.');
2635
+ const waiverReport = waiverHistory(waivers.uses, config.waive ?? []);
2636
+ const stamped = { ...history, unrecognizedFiles: unrecognized, waivers: waiverReport };
2621
2637
  const n = (value) => value.toLocaleString(t.numberLocale);
2622
2638
  const day = (ms) => new Date(ms).toISOString().slice(0, 10);
2623
2639
  const pct = (value) => `${(value * 100).toFixed(1)}%`;
@@ -2659,6 +2675,56 @@ async function commandHistory(args, pricing, t) {
2659
2675
  for (const name of unrecognized) {
2660
2676
  out.push(md ? `- ${t.history.unrecognized(name)}` : ` ${t.history.unrecognized(name)}`);
2661
2677
  }
2678
+ /**
2679
+ * The waiver record, printed only once something has been recorded.
2680
+ *
2681
+ * Silent on a repository that has never waived anything, rather than a
2682
+ * heading over "0 uses" — an empty section teaches a reader to skip the
2683
+ * section, and this is the one they should not learn to skip.
2684
+ */
2685
+ if (waivers.present) {
2686
+ out.push('');
2687
+ out.push(md ? `### ${t.history.waiverHeading()}` : t.history.waiverHeading());
2688
+ if (waiverReport.totalUses === 0) {
2689
+ out.push(md ? `- ${t.history.waiverNoneRecorded()}` : ` ${t.history.waiverNoneRecorded()}`);
2690
+ }
2691
+ else {
2692
+ const since = t.history.waiverSince(waiverReport.since ?? '', waiverReport.totalUses);
2693
+ out.push(md ? `- ${since}` : ` ${since}`);
2694
+ out.push(md ? `- _${t.history.waiverStartsHere()}_` : ` ${t.history.waiverStartsHere()}`);
2695
+ for (const habit of waiverReport.habits) {
2696
+ out.push('');
2697
+ const head = t.history.waiverHabit(habit.gate, habit.uses, habit.days, habit.firstDay, habit.lastDay);
2698
+ out.push(md ? `- **${head}**` : ` ${head}`);
2699
+ const rows = [t.history.waiverVerdict(habit.verdict)];
2700
+ // The reason as it stands *now* — never read backwards onto an
2701
+ // older use, which is the same mistake the record exists to avoid.
2702
+ const latest = habit.reasons[habit.reasons.length - 1];
2703
+ if (latest !== undefined)
2704
+ rows.push(t.history.waiverReasonNow(latest));
2705
+ if (habit.reasons.length > 1)
2706
+ rows.push(t.history.waiverReasonsChanged(habit.reasons.length));
2707
+ const firstExpiry = habit.expiries[0];
2708
+ const lastExpiry = habit.expiries[habit.expiries.length - 1];
2709
+ if (habit.expiries.length > 1 && firstExpiry !== undefined && lastExpiry !== undefined) {
2710
+ rows.push(t.history.waiverExpiriesMoved(firstExpiry, lastExpiry, habit.expiries.length - 1));
2711
+ }
2712
+ if (!habit.stillConfigured)
2713
+ rows.push(t.history.waiverNoLongerConfigured());
2714
+ for (const row of rows)
2715
+ out.push(md ? ` - ${row}` : ` ${row}`);
2716
+ }
2717
+ }
2718
+ if (waiverReport.neverUsed.length > 0) {
2719
+ out.push('');
2720
+ const dead = t.history.waiverNeverUsed(waiverReport.neverUsed.join(', '));
2721
+ out.push(md ? `- ${dead}` : ` ${dead}`);
2722
+ }
2723
+ if (waivers.unreadable.length > 0) {
2724
+ const bad = t.history.waiverUnreadable(waivers.unreadable.length, WAIVER_LOG);
2725
+ out.push(md ? `- ${bad}` : ` ${bad}`);
2726
+ }
2727
+ }
2662
2728
  if (fromStore) {
2663
2729
  out.push('');
2664
2730
  const note = t.history.storeNoLabels();
@@ -2695,19 +2761,21 @@ async function commandVerify(args, pricing, t) {
2695
2761
  const againstPath = stringFlag(args, 'against');
2696
2762
  if (againstPath === undefined)
2697
2763
  throw new Error(t.verify.needsAgainst());
2698
- let plan;
2699
- try {
2700
- const parsed = JSON.parse(await readFile(planPath, 'utf8'));
2701
- if (parsed?.schemaVersion !== 1 || !Array.isArray(parsed.actions)) {
2702
- throw new Error(t.verify.badPlan(planPath));
2703
- }
2704
- plan = parsed;
2705
- }
2706
- catch (error) {
2707
- if (error instanceof SyntaxError)
2708
- throw new Error(t.verify.badPlan(planPath));
2709
- throw error;
2764
+ /**
2765
+ * One validator, shared with the browser since 1.47.
2766
+ *
2767
+ * The check here used to be `schemaVersion === 1 && Array.isArray(actions)`
2768
+ * and nothing more, which accepts a file whose actions are arbitrary
2769
+ * objects — `verifyPlan` would then read `label` off `undefined`, match it
2770
+ * against no slice, and report `cannot-tell: workload-vanished` for every
2771
+ * one. A verification of a document that was never a plan, rendered exactly
2772
+ * like a real one.
2773
+ */
2774
+ const parsed = parsePlanDocument(await readFile(planPath, 'utf8'));
2775
+ if (!parsed.ok) {
2776
+ throw new Error(t.verify.badPlan(planPath, t.verify.planRefusal(parsed.why)));
2710
2777
  }
2778
+ const plan = parsed.plan;
2711
2779
  const GZ = LOG_EXTENSIONS.map((ext) => `${ext}.gz`);
2712
2780
  const READABLE = [...LOG_EXTENSIONS, ...GZ];
2713
2781
  const target = await stat(againstPath).catch(() => null);
@@ -3330,7 +3398,16 @@ async function commandProfile(args, config, pricing, t) {
3330
3398
  * way, because a waived failure that vanished from the output would be a
3331
3399
  * finding deleted with extra steps.
3332
3400
  */
3333
- const waived = (gate) => {
3401
+ /**
3402
+ * Uses recorded this run, flushed after the gates have finished.
3403
+ *
3404
+ * Collected rather than written inline because `waived` is synchronous and
3405
+ * called from seven places inside the gate pass. Writing from each of them
3406
+ * would mean seven awaits threaded through the exit-code logic — the one
3407
+ * part of this command where a mistake turns a red build green.
3408
+ */
3409
+ const waiverUses = [];
3410
+ const waived = (gate, measuredUsd = null, limitUsd = null) => {
3334
3411
  const found = waiverFor(gate);
3335
3412
  if (found === null)
3336
3413
  return false;
@@ -3340,6 +3417,29 @@ async function commandProfile(args, config, pricing, t) {
3340
3417
  }
3341
3418
  const daysLeft = Math.max(0, Math.ceil((Date.parse(`${found.entry.until}T00:00:00Z`) + 86_400_000 - Date.now()) / 86_400_000));
3342
3419
  console.error(c.yellow(t.profile.waiveActive(gate, found.entry.reason, found.entry.until, String(daysLeft))));
3420
+ /**
3421
+ * Recorded **when it silences something**, never when it is configured.
3422
+ *
3423
+ * A waiver nobody's build has ever hit is not a habit — it is dead config
3424
+ * — and the history reports the two apart. This is also the only honest
3425
+ * way to build the record 1.40 refused to invent: it starts today and
3426
+ * says so, rather than reconstructing a past from the present.
3427
+ *
3428
+ * The reason and the expiry are taken from the config **as it stands at
3429
+ * this moment**, because that is the decision that was actually in force.
3430
+ * Reading today's reason back onto last quarter's use is the same mistake
3431
+ * one layer down.
3432
+ */
3433
+ waiverUses.push({
3434
+ schemaVersion: 1,
3435
+ day: waiverDay(new Date()),
3436
+ gate,
3437
+ reason: found.entry.reason,
3438
+ until: found.entry.until,
3439
+ commit: process.env.GITHUB_SHA ?? process.env.CI_COMMIT_SHA ?? null,
3440
+ measuredUsd,
3441
+ limitUsd,
3442
+ });
3343
3443
  return true;
3344
3444
  };
3345
3445
  const applyGates = () => {
@@ -3389,7 +3489,7 @@ async function commandProfile(args, config, pricing, t) {
3389
3489
  }
3390
3490
  if (usd > limit) {
3391
3491
  console.error(c.red(t.profile.labelBudgetFailed(label, formatUsd(usd), formatUsd(limit))));
3392
- if (!waived(`byLabel:${label}`))
3492
+ if (!waived(`byLabel:${label}`, usd, limit))
3393
3493
  process.exitCode = 1;
3394
3494
  }
3395
3495
  else {
@@ -3451,7 +3551,7 @@ async function commandProfile(args, config, pricing, t) {
3451
3551
  * copy says so.
3452
3552
  */
3453
3553
  explainFailure(report.total.totalUsd - maxUsd);
3454
- if (!waived('maxUsd'))
3554
+ if (!waived('maxUsd', report.total.totalUsd, maxUsd))
3455
3555
  process.exitCode = 1;
3456
3556
  }
3457
3557
  else {
@@ -3486,7 +3586,7 @@ async function commandProfile(args, config, pricing, t) {
3486
3586
  }
3487
3587
  else if (againstDelta > maxGrowth) {
3488
3588
  console.error(c.red(t.profile.maxGrowthUsdFailed(formatSignedUsd(againstDelta), formatUsd(maxGrowth))));
3489
- if (!waived('maxGrowthUsd'))
3589
+ if (!waived('maxGrowthUsd', againstDelta, maxGrowth))
3490
3590
  process.exitCode = 1;
3491
3591
  }
3492
3592
  }
@@ -3503,12 +3603,12 @@ async function commandProfile(args, config, pricing, t) {
3503
3603
  const gateCache = cacheEconomics(report.total);
3504
3604
  if (gateCache.deltaUsd > maxLoss) {
3505
3605
  console.error(c.red(t.profile.maxCacheLossFailed(formatUsd(gateCache.deltaUsd), formatUsd(maxLoss))));
3506
- if (!waived('maxCacheLossUsd'))
3606
+ if (!waived('maxCacheLossUsd', gateCache.deltaUsd, maxLoss))
3507
3607
  process.exitCode = 1;
3508
3608
  }
3509
3609
  else if (gateCache.worstCaseDeltaUsd > maxLoss) {
3510
3610
  console.error(c.red(t.profile.maxCacheLossWorstCase(report.total.assumedWriteTtlCalls, formatUsd(gateCache.worstCaseDeltaUsd), formatUsd(maxLoss))));
3511
- if (!waived('maxCacheLossUsd'))
3611
+ if (!waived('maxCacheLossUsd', gateCache.worstCaseDeltaUsd, maxLoss))
3512
3612
  process.exitCode = 1;
3513
3613
  }
3514
3614
  else {
@@ -3554,7 +3654,7 @@ async function commandProfile(args, config, pricing, t) {
3554
3654
  if (worst.usd > maxDay) {
3555
3655
  console.error(c.red(`${t.profile.maxDayFailed(worst.day, formatUsd(worst.usd), formatUsd(maxDay))}${suspect}`));
3556
3656
  explainFailure(worst.usd - maxDay, { namesLargest: true });
3557
- if (!waived('maxDayUsd'))
3657
+ if (!waived('maxDayUsd', worst.usd, maxDay))
3558
3658
  process.exitCode = 1;
3559
3659
  }
3560
3660
  else {
@@ -3596,7 +3696,7 @@ async function commandProfile(args, config, pricing, t) {
3596
3696
  else if (report.sessionSpend.maxUsd > maxSession) {
3597
3697
  console.error(c.red(t.profile.maxSessionFailed(formatUsd(report.sessionSpend.maxUsd), formatUsd(maxSession), n(report.sessionSpend.sessions))));
3598
3698
  explainFailure(report.sessionSpend.maxUsd - maxSession);
3599
- if (!waived('maxSessionUsd'))
3699
+ if (!waived('maxSessionUsd', report.sessionSpend.maxUsd, maxSession))
3600
3700
  process.exitCode = 1;
3601
3701
  }
3602
3702
  else {
@@ -3629,6 +3729,26 @@ async function commandProfile(args, config, pricing, t) {
3629
3729
  gateFailed = process.exitCode === 1;
3630
3730
  }
3631
3731
  };
3732
+ /**
3733
+ * Writes down every waiver that silenced something this run.
3734
+ *
3735
+ * **A failure here never fails the build.** The gate's job is the exit code;
3736
+ * a read-only checkout or a full disk must not turn a passing build red on
3737
+ * account of bookkeeping. The problem is reported and the gate's own verdict
3738
+ * stands — which is also why this runs after `recordGates` rather than
3739
+ * inside it: nothing about the exit code depends on the write.
3740
+ */
3741
+ const recordWaiverUses = async () => {
3742
+ if (waiverUses.length === 0)
3743
+ return;
3744
+ for (const use of waiverUses) {
3745
+ const failed = await appendWaiverUse('.', use);
3746
+ if (failed !== null) {
3747
+ console.error(c.dim(t.profile.waiveNotRecorded(WAIVER_LOG, failed)));
3748
+ return;
3749
+ }
3750
+ }
3751
+ };
3632
3752
  /**
3633
3753
  * The side files the caller asked for. Written on **both** output paths:
3634
3754
  * under --json the human rendering returns early, and the first version of
@@ -3777,6 +3897,7 @@ async function commandProfile(args, config, pricing, t) {
3777
3897
  ...(whatIf !== null ? { whatIf } : {}),
3778
3898
  }, null, 2));
3779
3899
  recordGates();
3900
+ await recordWaiverUses();
3780
3901
  await writeSideFiles();
3781
3902
  return;
3782
3903
  }
@@ -4784,6 +4905,7 @@ async function commandProfile(args, config, pricing, t) {
4784
4905
  }
4785
4906
  reportProfileGaps(report, t, n, pricingStale);
4786
4907
  recordGates();
4908
+ await recordWaiverUses();
4787
4909
  await writeSideFiles();
4788
4910
  }
4789
4911
  /**
@@ -6080,7 +6202,7 @@ async function main() {
6080
6202
  await commandVerify(args, pricing, t);
6081
6203
  break;
6082
6204
  case 'history':
6083
- await commandHistory(args, pricing, t);
6205
+ await commandHistory(args, config, pricing, t);
6084
6206
  break;
6085
6207
  case 'connect':
6086
6208
  await commandConnect(args, pricing, t);