@trazum/cli 1.47.0 → 1.49.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/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +36 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +36 -1
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +36 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +203 -35
- package/dist/index.js.map +1 -1
- package/dist/waiver-log.d.ts +42 -0
- package/dist/waiver-log.d.ts.map +1 -0
- package/dist/waiver-log.js +74 -0
- package/dist/waiver-log.js.map +1 -0
- package/package.json +2 -2
- package/src/i18n/en.ts +53 -1
- package/src/i18n/es.ts +53 -1
- package/src/i18n/types.ts +36 -0
- package/src/index.ts +239 -37
- package/src/waiver-log.ts +81 -0
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, 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, 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';
|
|
@@ -2081,36 +2082,36 @@ function parseWhen(args, flag, endOfDay, t, now) {
|
|
|
2081
2082
|
*/
|
|
2082
2083
|
async function commandServe(args, config, pricing, t) {
|
|
2083
2084
|
const root = process.cwd();
|
|
2084
|
-
const limitUsd = config.spend?.maxUsd;
|
|
2085
2085
|
const { resolved } = await readStore(root);
|
|
2086
|
-
const measured = resolved.records.length > 0;
|
|
2087
2086
|
/**
|
|
2088
|
-
* The
|
|
2087
|
+
* The live budget, from `budgetPositions` — the same number `store` prints
|
|
2088
|
+
* and the same one the MCP guard consults.
|
|
2089
2089
|
*
|
|
2090
|
-
*
|
|
2091
|
-
*
|
|
2092
|
-
*
|
|
2090
|
+
* **This used to read `spend.maxUsd` against the whole store**, which is a
|
|
2091
|
+
* per-log gate compared against however much history the store happened to
|
|
2092
|
+
* hold. A year of records against a monthly limit reported as a budget
|
|
2093
|
+
* position, with a straight face and no way for a caller to tell. Same
|
|
2094
|
+
* units, different denominators, and the two surfaces disagreed by exactly
|
|
2095
|
+
* as much history as the machine had. `spend.monthlyUsd` is the key for a
|
|
2096
|
+
* calendar month and nothing infers one key from the other: a repository
|
|
2097
|
+
* with a per-log gate and no monthly budget has no monthly position, and
|
|
2098
|
+
* this says so rather than picking a number that is the right shape.
|
|
2093
2099
|
*/
|
|
2094
|
-
const
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
}
|
|
2099
|
-
: null;
|
|
2100
|
-
const report = bucketedProfile({
|
|
2101
|
-
provider: 'store',
|
|
2102
|
-
granularity: 'bucketed',
|
|
2103
|
-
buckets: bucketsFromRecords(resolved.records),
|
|
2104
|
-
window,
|
|
2105
|
-
gaps: [],
|
|
2106
|
-
unavailable: [],
|
|
2107
|
-
}, { catalogue: pricing });
|
|
2100
|
+
const budget = budgetPositions(resolved.records, config.spend, { catalogue: pricing });
|
|
2101
|
+
const standing = budget.positions[0] ?? null;
|
|
2102
|
+
const limitUsd = config.spend?.monthlyUsd;
|
|
2103
|
+
const measured = standing !== null && standing.coverage !== 'none';
|
|
2108
2104
|
const server = buildServer({
|
|
2109
2105
|
catalogue: pricing,
|
|
2110
2106
|
position: () => ({
|
|
2111
|
-
|
|
2107
|
+
// Nothing measured inside the period is `undefined`, never zero: the
|
|
2108
|
+
// endpoint's `cannot-tell` exists for exactly this, and a $0 consumed
|
|
2109
|
+
// would be the healthiest-looking budget a dead store can produce.
|
|
2110
|
+
consumedUsd: measured ? standing.consumedUsd : undefined,
|
|
2112
2111
|
limitUsd,
|
|
2113
|
-
|
|
2112
|
+
// The period, not the store's span. A caller judging staleness needs to
|
|
2113
|
+
// know which month the figure is about.
|
|
2114
|
+
window: standing === null ? null : { fromMs: standing.period.fromMs, toMs: standing.period.toMs },
|
|
2114
2115
|
}),
|
|
2115
2116
|
});
|
|
2116
2117
|
const socket = stringFlag(args, 'socket');
|
|
@@ -2122,7 +2123,10 @@ async function commandServe(args, config, pricing, t) {
|
|
|
2122
2123
|
const where = await listen(server, socket !== undefined ? { socket } : { port });
|
|
2123
2124
|
console.log(c.bold(t.serve.listening(where)));
|
|
2124
2125
|
console.log(` ${c.dim(wrap(t.serve.loopbackOnly(), 74, ' '))}`);
|
|
2125
|
-
console.log(` ${c.dim(wrap(measured ? t.serve.measuredFrom(formatUsd(
|
|
2126
|
+
console.log(` ${c.dim(wrap(measured ? t.serve.measuredFrom(formatUsd(standing.consumedUsd)) : t.serve.nothingMeasured(STORE_DIR), 74, ' '))}`);
|
|
2127
|
+
if (standing !== null && standing.coverage === 'partial') {
|
|
2128
|
+
console.log(` ${c.yellow(wrap(t.serve.partialCoverage(standing.measuredDays, standing.elapsedDays, standing.period.id), 74, ' '))}`);
|
|
2129
|
+
}
|
|
2126
2130
|
if (limitUsd === undefined) {
|
|
2127
2131
|
console.log(` ${c.dim(wrap(t.serve.noBudget(), 74, ' '))}`);
|
|
2128
2132
|
}
|
|
@@ -2375,6 +2379,51 @@ async function commandStore(args, config, pricing, t) {
|
|
|
2375
2379
|
}
|
|
2376
2380
|
const keepDays = config.store?.keepDays;
|
|
2377
2381
|
console.log(` ${c.dim(wrap(keepDays === undefined ? t.store.noRetention() : t.store.retention(String(keepDays)), 74, ' '))}`);
|
|
2382
|
+
/**
|
|
2383
|
+
* The live budget, printed here because this is where the measurement lives.
|
|
2384
|
+
*
|
|
2385
|
+
* The same call `serve` makes and the same call the MCP guard makes, so the
|
|
2386
|
+
* three cannot disagree about how much of the month is gone — which is the
|
|
2387
|
+
* whole point of the number existing in one place.
|
|
2388
|
+
*/
|
|
2389
|
+
renderBudget(budgetPositions(resolved.records, config.spend, { catalogue: pricing }), t, n);
|
|
2390
|
+
}
|
|
2391
|
+
/**
|
|
2392
|
+
* One budget standing, rendered.
|
|
2393
|
+
*
|
|
2394
|
+
* Coverage before the money, deliberately. A reader who sees "$61 of $100"
|
|
2395
|
+
* first has already formed a view by the time they reach "over three of
|
|
2396
|
+
* nineteen elapsed days", and the second sentence has to undo the first.
|
|
2397
|
+
*/
|
|
2398
|
+
function renderBudget(report, t, n) {
|
|
2399
|
+
const standing = report.positions[0];
|
|
2400
|
+
if (standing === undefined) {
|
|
2401
|
+
if (report.unmeasuredScopes.length > 0) {
|
|
2402
|
+
console.log();
|
|
2403
|
+
console.log(` ${c.dim(wrap(t.store.budgetScopesUnmeasured(report.unmeasuredScopes.length), 74, ' '))}`);
|
|
2404
|
+
}
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
console.log();
|
|
2408
|
+
console.log(c.bold(t.store.budgetHeading(standing.period.id)));
|
|
2409
|
+
if (standing.coverage === 'none') {
|
|
2410
|
+
// Nothing measured is never rendered as nothing spent. A dead store and a
|
|
2411
|
+
// quiet month produce the same zero, and only one of them is good news.
|
|
2412
|
+
console.log(` ${c.red(wrap(t.store.budgetNothingMeasured(standing.elapsedDays), 74, ' '))}`);
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
if (standing.coverage === 'partial') {
|
|
2416
|
+
console.log(` ${c.yellow(wrap(t.store.budgetPartial(standing.measuredDays, standing.elapsedDays, standing.unmeasuredDays.join(', ')), 74, ' '))}`);
|
|
2417
|
+
}
|
|
2418
|
+
const share = standing.burn.consumedShare;
|
|
2419
|
+
console.log(` ${t.store.budgetStanding(formatUsd(standing.consumedUsd), formatUsd(standing.limitUsd), share === null ? '—' : `${Math.round(share * 100)}%`, n(standing.measuredDays), n(standing.period.days))}`);
|
|
2420
|
+
const line = t.store.budgetShape(standing.burn.shape, Math.round(standing.burn.elapsedShare * 100), standing.coverage);
|
|
2421
|
+
console.log(` ${standing.verdict === 'over' ? c.red(line) : c.dim(wrap(line, 74, ' '))}`);
|
|
2422
|
+
// Only where there is a shape to disclaim. "That is a shape, not a forecast"
|
|
2423
|
+
// under "nothing to compare against" is a disclaimer about nothing.
|
|
2424
|
+
if (standing.burn.shape !== 'cannot-tell') {
|
|
2425
|
+
console.log(` ${c.dim(wrap(t.store.budgetNeverForecast(), 74, ' '))}`);
|
|
2426
|
+
}
|
|
2378
2427
|
}
|
|
2379
2428
|
/**
|
|
2380
2429
|
* `trazum connect <provider>` — the bill, read from the provider.
|
|
@@ -2518,7 +2567,7 @@ async function commandConnect(args, pricing, t) {
|
|
|
2518
2567
|
* same action planned twice — and no series, however long, becomes a
|
|
2519
2568
|
* forecast.
|
|
2520
2569
|
*/
|
|
2521
|
-
async function commandHistory(args, pricing, t) {
|
|
2570
|
+
async function commandHistory(args, config, pricing, t) {
|
|
2522
2571
|
/**
|
|
2523
2572
|
* `--store` builds the series from measured spend already on disk.
|
|
2524
2573
|
*
|
|
@@ -2617,7 +2666,22 @@ async function commandHistory(args, pricing, t) {
|
|
|
2617
2666
|
if (history.periods.length < 3) {
|
|
2618
2667
|
throw new Error(t.history.needsThree(String(history.periods.length)));
|
|
2619
2668
|
}
|
|
2620
|
-
|
|
2669
|
+
/**
|
|
2670
|
+
* The waiver record — closing the gap 1.40 named and could not fill.
|
|
2671
|
+
*
|
|
2672
|
+
* 1.40 wanted to say "this finding has been waived three times in a row" and
|
|
2673
|
+
* refused to, because the only material available was the config as it
|
|
2674
|
+
* stands, and a past reconstructed from a present is a guess wearing a
|
|
2675
|
+
* record's clothes. The material exists now: since 1.48 a waiver that
|
|
2676
|
+
* silences a gate writes down that it did, and this reads those lines back.
|
|
2677
|
+
*
|
|
2678
|
+
* Read from the working directory rather than from the reports directory:
|
|
2679
|
+
* the waiver record belongs to the repository whose gates fired, and the
|
|
2680
|
+
* stored reports may have come from anywhere.
|
|
2681
|
+
*/
|
|
2682
|
+
const waivers = await readWaiverLog('.');
|
|
2683
|
+
const waiverReport = waiverHistory(waivers.uses, config.waive ?? []);
|
|
2684
|
+
const stamped = { ...history, unrecognizedFiles: unrecognized, waivers: waiverReport };
|
|
2621
2685
|
const n = (value) => value.toLocaleString(t.numberLocale);
|
|
2622
2686
|
const day = (ms) => new Date(ms).toISOString().slice(0, 10);
|
|
2623
2687
|
const pct = (value) => `${(value * 100).toFixed(1)}%`;
|
|
@@ -2659,6 +2723,56 @@ async function commandHistory(args, pricing, t) {
|
|
|
2659
2723
|
for (const name of unrecognized) {
|
|
2660
2724
|
out.push(md ? `- ${t.history.unrecognized(name)}` : ` ${t.history.unrecognized(name)}`);
|
|
2661
2725
|
}
|
|
2726
|
+
/**
|
|
2727
|
+
* The waiver record, printed only once something has been recorded.
|
|
2728
|
+
*
|
|
2729
|
+
* Silent on a repository that has never waived anything, rather than a
|
|
2730
|
+
* heading over "0 uses" — an empty section teaches a reader to skip the
|
|
2731
|
+
* section, and this is the one they should not learn to skip.
|
|
2732
|
+
*/
|
|
2733
|
+
if (waivers.present) {
|
|
2734
|
+
out.push('');
|
|
2735
|
+
out.push(md ? `### ${t.history.waiverHeading()}` : t.history.waiverHeading());
|
|
2736
|
+
if (waiverReport.totalUses === 0) {
|
|
2737
|
+
out.push(md ? `- ${t.history.waiverNoneRecorded()}` : ` ${t.history.waiverNoneRecorded()}`);
|
|
2738
|
+
}
|
|
2739
|
+
else {
|
|
2740
|
+
const since = t.history.waiverSince(waiverReport.since ?? '', waiverReport.totalUses);
|
|
2741
|
+
out.push(md ? `- ${since}` : ` ${since}`);
|
|
2742
|
+
out.push(md ? `- _${t.history.waiverStartsHere()}_` : ` ${t.history.waiverStartsHere()}`);
|
|
2743
|
+
for (const habit of waiverReport.habits) {
|
|
2744
|
+
out.push('');
|
|
2745
|
+
const head = t.history.waiverHabit(habit.gate, habit.uses, habit.days, habit.firstDay, habit.lastDay);
|
|
2746
|
+
out.push(md ? `- **${head}**` : ` ${head}`);
|
|
2747
|
+
const rows = [t.history.waiverVerdict(habit.verdict)];
|
|
2748
|
+
// The reason as it stands *now* — never read backwards onto an
|
|
2749
|
+
// older use, which is the same mistake the record exists to avoid.
|
|
2750
|
+
const latest = habit.reasons[habit.reasons.length - 1];
|
|
2751
|
+
if (latest !== undefined)
|
|
2752
|
+
rows.push(t.history.waiverReasonNow(latest));
|
|
2753
|
+
if (habit.reasons.length > 1)
|
|
2754
|
+
rows.push(t.history.waiverReasonsChanged(habit.reasons.length));
|
|
2755
|
+
const firstExpiry = habit.expiries[0];
|
|
2756
|
+
const lastExpiry = habit.expiries[habit.expiries.length - 1];
|
|
2757
|
+
if (habit.expiries.length > 1 && firstExpiry !== undefined && lastExpiry !== undefined) {
|
|
2758
|
+
rows.push(t.history.waiverExpiriesMoved(firstExpiry, lastExpiry, habit.expiries.length - 1));
|
|
2759
|
+
}
|
|
2760
|
+
if (!habit.stillConfigured)
|
|
2761
|
+
rows.push(t.history.waiverNoLongerConfigured());
|
|
2762
|
+
for (const row of rows)
|
|
2763
|
+
out.push(md ? ` - ${row}` : ` ${row}`);
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
if (waiverReport.neverUsed.length > 0) {
|
|
2767
|
+
out.push('');
|
|
2768
|
+
const dead = t.history.waiverNeverUsed(waiverReport.neverUsed.join(', '));
|
|
2769
|
+
out.push(md ? `- ${dead}` : ` ${dead}`);
|
|
2770
|
+
}
|
|
2771
|
+
if (waivers.unreadable.length > 0) {
|
|
2772
|
+
const bad = t.history.waiverUnreadable(waivers.unreadable.length, WAIVER_LOG);
|
|
2773
|
+
out.push(md ? `- ${bad}` : ` ${bad}`);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2662
2776
|
if (fromStore) {
|
|
2663
2777
|
out.push('');
|
|
2664
2778
|
const note = t.history.storeNoLabels();
|
|
@@ -3332,7 +3446,16 @@ async function commandProfile(args, config, pricing, t) {
|
|
|
3332
3446
|
* way, because a waived failure that vanished from the output would be a
|
|
3333
3447
|
* finding deleted with extra steps.
|
|
3334
3448
|
*/
|
|
3335
|
-
|
|
3449
|
+
/**
|
|
3450
|
+
* Uses recorded this run, flushed after the gates have finished.
|
|
3451
|
+
*
|
|
3452
|
+
* Collected rather than written inline because `waived` is synchronous and
|
|
3453
|
+
* called from seven places inside the gate pass. Writing from each of them
|
|
3454
|
+
* would mean seven awaits threaded through the exit-code logic — the one
|
|
3455
|
+
* part of this command where a mistake turns a red build green.
|
|
3456
|
+
*/
|
|
3457
|
+
const waiverUses = [];
|
|
3458
|
+
const waived = (gate, measuredUsd = null, limitUsd = null) => {
|
|
3336
3459
|
const found = waiverFor(gate);
|
|
3337
3460
|
if (found === null)
|
|
3338
3461
|
return false;
|
|
@@ -3342,6 +3465,29 @@ async function commandProfile(args, config, pricing, t) {
|
|
|
3342
3465
|
}
|
|
3343
3466
|
const daysLeft = Math.max(0, Math.ceil((Date.parse(`${found.entry.until}T00:00:00Z`) + 86_400_000 - Date.now()) / 86_400_000));
|
|
3344
3467
|
console.error(c.yellow(t.profile.waiveActive(gate, found.entry.reason, found.entry.until, String(daysLeft))));
|
|
3468
|
+
/**
|
|
3469
|
+
* Recorded **when it silences something**, never when it is configured.
|
|
3470
|
+
*
|
|
3471
|
+
* A waiver nobody's build has ever hit is not a habit — it is dead config
|
|
3472
|
+
* — and the history reports the two apart. This is also the only honest
|
|
3473
|
+
* way to build the record 1.40 refused to invent: it starts today and
|
|
3474
|
+
* says so, rather than reconstructing a past from the present.
|
|
3475
|
+
*
|
|
3476
|
+
* The reason and the expiry are taken from the config **as it stands at
|
|
3477
|
+
* this moment**, because that is the decision that was actually in force.
|
|
3478
|
+
* Reading today's reason back onto last quarter's use is the same mistake
|
|
3479
|
+
* one layer down.
|
|
3480
|
+
*/
|
|
3481
|
+
waiverUses.push({
|
|
3482
|
+
schemaVersion: 1,
|
|
3483
|
+
day: waiverDay(new Date()),
|
|
3484
|
+
gate,
|
|
3485
|
+
reason: found.entry.reason,
|
|
3486
|
+
until: found.entry.until,
|
|
3487
|
+
commit: process.env.GITHUB_SHA ?? process.env.CI_COMMIT_SHA ?? null,
|
|
3488
|
+
measuredUsd,
|
|
3489
|
+
limitUsd,
|
|
3490
|
+
});
|
|
3345
3491
|
return true;
|
|
3346
3492
|
};
|
|
3347
3493
|
const applyGates = () => {
|
|
@@ -3391,7 +3537,7 @@ async function commandProfile(args, config, pricing, t) {
|
|
|
3391
3537
|
}
|
|
3392
3538
|
if (usd > limit) {
|
|
3393
3539
|
console.error(c.red(t.profile.labelBudgetFailed(label, formatUsd(usd), formatUsd(limit))));
|
|
3394
|
-
if (!waived(`byLabel:${label}
|
|
3540
|
+
if (!waived(`byLabel:${label}`, usd, limit))
|
|
3395
3541
|
process.exitCode = 1;
|
|
3396
3542
|
}
|
|
3397
3543
|
else {
|
|
@@ -3453,7 +3599,7 @@ async function commandProfile(args, config, pricing, t) {
|
|
|
3453
3599
|
* copy says so.
|
|
3454
3600
|
*/
|
|
3455
3601
|
explainFailure(report.total.totalUsd - maxUsd);
|
|
3456
|
-
if (!waived('maxUsd'))
|
|
3602
|
+
if (!waived('maxUsd', report.total.totalUsd, maxUsd))
|
|
3457
3603
|
process.exitCode = 1;
|
|
3458
3604
|
}
|
|
3459
3605
|
else {
|
|
@@ -3488,7 +3634,7 @@ async function commandProfile(args, config, pricing, t) {
|
|
|
3488
3634
|
}
|
|
3489
3635
|
else if (againstDelta > maxGrowth) {
|
|
3490
3636
|
console.error(c.red(t.profile.maxGrowthUsdFailed(formatSignedUsd(againstDelta), formatUsd(maxGrowth))));
|
|
3491
|
-
if (!waived('maxGrowthUsd'))
|
|
3637
|
+
if (!waived('maxGrowthUsd', againstDelta, maxGrowth))
|
|
3492
3638
|
process.exitCode = 1;
|
|
3493
3639
|
}
|
|
3494
3640
|
}
|
|
@@ -3505,12 +3651,12 @@ async function commandProfile(args, config, pricing, t) {
|
|
|
3505
3651
|
const gateCache = cacheEconomics(report.total);
|
|
3506
3652
|
if (gateCache.deltaUsd > maxLoss) {
|
|
3507
3653
|
console.error(c.red(t.profile.maxCacheLossFailed(formatUsd(gateCache.deltaUsd), formatUsd(maxLoss))));
|
|
3508
|
-
if (!waived('maxCacheLossUsd'))
|
|
3654
|
+
if (!waived('maxCacheLossUsd', gateCache.deltaUsd, maxLoss))
|
|
3509
3655
|
process.exitCode = 1;
|
|
3510
3656
|
}
|
|
3511
3657
|
else if (gateCache.worstCaseDeltaUsd > maxLoss) {
|
|
3512
3658
|
console.error(c.red(t.profile.maxCacheLossWorstCase(report.total.assumedWriteTtlCalls, formatUsd(gateCache.worstCaseDeltaUsd), formatUsd(maxLoss))));
|
|
3513
|
-
if (!waived('maxCacheLossUsd'))
|
|
3659
|
+
if (!waived('maxCacheLossUsd', gateCache.worstCaseDeltaUsd, maxLoss))
|
|
3514
3660
|
process.exitCode = 1;
|
|
3515
3661
|
}
|
|
3516
3662
|
else {
|
|
@@ -3556,7 +3702,7 @@ async function commandProfile(args, config, pricing, t) {
|
|
|
3556
3702
|
if (worst.usd > maxDay) {
|
|
3557
3703
|
console.error(c.red(`${t.profile.maxDayFailed(worst.day, formatUsd(worst.usd), formatUsd(maxDay))}${suspect}`));
|
|
3558
3704
|
explainFailure(worst.usd - maxDay, { namesLargest: true });
|
|
3559
|
-
if (!waived('maxDayUsd'))
|
|
3705
|
+
if (!waived('maxDayUsd', worst.usd, maxDay))
|
|
3560
3706
|
process.exitCode = 1;
|
|
3561
3707
|
}
|
|
3562
3708
|
else {
|
|
@@ -3598,7 +3744,7 @@ async function commandProfile(args, config, pricing, t) {
|
|
|
3598
3744
|
else if (report.sessionSpend.maxUsd > maxSession) {
|
|
3599
3745
|
console.error(c.red(t.profile.maxSessionFailed(formatUsd(report.sessionSpend.maxUsd), formatUsd(maxSession), n(report.sessionSpend.sessions))));
|
|
3600
3746
|
explainFailure(report.sessionSpend.maxUsd - maxSession);
|
|
3601
|
-
if (!waived('maxSessionUsd'))
|
|
3747
|
+
if (!waived('maxSessionUsd', report.sessionSpend.maxUsd, maxSession))
|
|
3602
3748
|
process.exitCode = 1;
|
|
3603
3749
|
}
|
|
3604
3750
|
else {
|
|
@@ -3631,6 +3777,26 @@ async function commandProfile(args, config, pricing, t) {
|
|
|
3631
3777
|
gateFailed = process.exitCode === 1;
|
|
3632
3778
|
}
|
|
3633
3779
|
};
|
|
3780
|
+
/**
|
|
3781
|
+
* Writes down every waiver that silenced something this run.
|
|
3782
|
+
*
|
|
3783
|
+
* **A failure here never fails the build.** The gate's job is the exit code;
|
|
3784
|
+
* a read-only checkout or a full disk must not turn a passing build red on
|
|
3785
|
+
* account of bookkeeping. The problem is reported and the gate's own verdict
|
|
3786
|
+
* stands — which is also why this runs after `recordGates` rather than
|
|
3787
|
+
* inside it: nothing about the exit code depends on the write.
|
|
3788
|
+
*/
|
|
3789
|
+
const recordWaiverUses = async () => {
|
|
3790
|
+
if (waiverUses.length === 0)
|
|
3791
|
+
return;
|
|
3792
|
+
for (const use of waiverUses) {
|
|
3793
|
+
const failed = await appendWaiverUse('.', use);
|
|
3794
|
+
if (failed !== null) {
|
|
3795
|
+
console.error(c.dim(t.profile.waiveNotRecorded(WAIVER_LOG, failed)));
|
|
3796
|
+
return;
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
};
|
|
3634
3800
|
/**
|
|
3635
3801
|
* The side files the caller asked for. Written on **both** output paths:
|
|
3636
3802
|
* under --json the human rendering returns early, and the first version of
|
|
@@ -3779,6 +3945,7 @@ async function commandProfile(args, config, pricing, t) {
|
|
|
3779
3945
|
...(whatIf !== null ? { whatIf } : {}),
|
|
3780
3946
|
}, null, 2));
|
|
3781
3947
|
recordGates();
|
|
3948
|
+
await recordWaiverUses();
|
|
3782
3949
|
await writeSideFiles();
|
|
3783
3950
|
return;
|
|
3784
3951
|
}
|
|
@@ -4786,6 +4953,7 @@ async function commandProfile(args, config, pricing, t) {
|
|
|
4786
4953
|
}
|
|
4787
4954
|
reportProfileGaps(report, t, n, pricingStale);
|
|
4788
4955
|
recordGates();
|
|
4956
|
+
await recordWaiverUses();
|
|
4789
4957
|
await writeSideFiles();
|
|
4790
4958
|
}
|
|
4791
4959
|
/**
|
|
@@ -6082,7 +6250,7 @@ async function main() {
|
|
|
6082
6250
|
await commandVerify(args, pricing, t);
|
|
6083
6251
|
break;
|
|
6084
6252
|
case 'history':
|
|
6085
|
-
await commandHistory(args, pricing, t);
|
|
6253
|
+
await commandHistory(args, config, pricing, t);
|
|
6086
6254
|
break;
|
|
6087
6255
|
case 'connect':
|
|
6088
6256
|
await commandConnect(args, pricing, t);
|