@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/src/index.ts
CHANGED
|
@@ -35,9 +35,12 @@ import {
|
|
|
35
35
|
computeSavings,
|
|
36
36
|
countTokensAnthropic,
|
|
37
37
|
DEFAULT_USAGE,
|
|
38
|
+
budgetPositions,
|
|
38
39
|
detectFromSource,
|
|
39
40
|
matchLocale,
|
|
40
41
|
parsePlanDocument,
|
|
42
|
+
waiverDay,
|
|
43
|
+
waiverHistory,
|
|
41
44
|
proposeInit,
|
|
42
45
|
MIN_RATE_DAYS,
|
|
43
46
|
parseConfig,
|
|
@@ -125,7 +128,9 @@ import type {
|
|
|
125
128
|
UsageProfile,
|
|
126
129
|
} from '@trazum/core';
|
|
127
130
|
import type {
|
|
131
|
+
BudgetReport,
|
|
128
132
|
UsageProfileReport,
|
|
133
|
+
WaiverUse,
|
|
129
134
|
InitDecline,
|
|
130
135
|
InitJustification,
|
|
131
136
|
InitObservations,
|
|
@@ -168,6 +173,7 @@ import {
|
|
|
168
173
|
import type { Revision } from './git.js';
|
|
169
174
|
import { fetchProviderUsage, findCredential } from './connect.js';
|
|
170
175
|
import { STORE_DIR, appendRecords, readStore, rewriteStore } from './store-fs.js';
|
|
176
|
+
import { WAIVER_LOG, appendWaiverUse, readWaiverLog } from './waiver-log.js';
|
|
171
177
|
import { DEFAULT_PORT, buildServer, listen } from './serve.js';
|
|
172
178
|
import {
|
|
173
179
|
WATCH_STATE_VERSION,
|
|
@@ -2743,41 +2749,39 @@ async function commandServe(
|
|
|
2743
2749
|
t: CliMessages,
|
|
2744
2750
|
): Promise<void> {
|
|
2745
2751
|
const root = process.cwd();
|
|
2746
|
-
const limitUsd = config.spend?.maxUsd;
|
|
2747
2752
|
|
|
2748
2753
|
const { resolved } = await readStore(root);
|
|
2749
|
-
|
|
2754
|
+
|
|
2750
2755
|
/**
|
|
2751
|
-
* The
|
|
2756
|
+
* The live budget, from `budgetPositions` — the same number `store` prints
|
|
2757
|
+
* and the same one the MCP guard consults.
|
|
2752
2758
|
*
|
|
2753
|
-
*
|
|
2754
|
-
*
|
|
2755
|
-
*
|
|
2759
|
+
* **This used to read `spend.maxUsd` against the whole store**, which is a
|
|
2760
|
+
* per-log gate compared against however much history the store happened to
|
|
2761
|
+
* hold. A year of records against a monthly limit reported as a budget
|
|
2762
|
+
* position, with a straight face and no way for a caller to tell. Same
|
|
2763
|
+
* units, different denominators, and the two surfaces disagreed by exactly
|
|
2764
|
+
* as much history as the machine had. `spend.monthlyUsd` is the key for a
|
|
2765
|
+
* calendar month and nothing infers one key from the other: a repository
|
|
2766
|
+
* with a per-log gate and no monthly budget has no monthly position, and
|
|
2767
|
+
* this says so rather than picking a number that is the right shape.
|
|
2756
2768
|
*/
|
|
2757
|
-
const
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
}
|
|
2762
|
-
: null;
|
|
2763
|
-
const report = bucketedProfile(
|
|
2764
|
-
{
|
|
2765
|
-
provider: 'store',
|
|
2766
|
-
granularity: 'bucketed',
|
|
2767
|
-
buckets: bucketsFromRecords(resolved.records),
|
|
2768
|
-
window,
|
|
2769
|
-
gaps: [],
|
|
2770
|
-
unavailable: [],
|
|
2771
|
-
},
|
|
2772
|
-
{ catalogue: pricing },
|
|
2773
|
-
);
|
|
2769
|
+
const budget = budgetPositions(resolved.records, config.spend, { catalogue: pricing });
|
|
2770
|
+
const standing = budget.positions[0] ?? null;
|
|
2771
|
+
const limitUsd = config.spend?.monthlyUsd;
|
|
2772
|
+
const measured = standing !== null && standing.coverage !== 'none';
|
|
2774
2773
|
|
|
2775
2774
|
const server = buildServer({
|
|
2776
2775
|
catalogue: pricing,
|
|
2777
2776
|
position: () => ({
|
|
2778
|
-
|
|
2777
|
+
// Nothing measured inside the period is `undefined`, never zero: the
|
|
2778
|
+
// endpoint's `cannot-tell` exists for exactly this, and a $0 consumed
|
|
2779
|
+
// would be the healthiest-looking budget a dead store can produce.
|
|
2780
|
+
consumedUsd: measured ? standing.consumedUsd : undefined,
|
|
2779
2781
|
limitUsd,
|
|
2780
|
-
|
|
2782
|
+
// The period, not the store's span. A caller judging staleness needs to
|
|
2783
|
+
// know which month the figure is about.
|
|
2784
|
+
window: standing === null ? null : { fromMs: standing.period.fromMs, toMs: standing.period.toMs },
|
|
2781
2785
|
}),
|
|
2782
2786
|
});
|
|
2783
2787
|
|
|
@@ -2792,8 +2796,13 @@ async function commandServe(
|
|
|
2792
2796
|
console.log(c.bold(t.serve.listening(where)));
|
|
2793
2797
|
console.log(` ${c.dim(wrap(t.serve.loopbackOnly(), 74, ' '))}`);
|
|
2794
2798
|
console.log(
|
|
2795
|
-
` ${c.dim(wrap(measured ? t.serve.measuredFrom(formatUsd(
|
|
2799
|
+
` ${c.dim(wrap(measured ? t.serve.measuredFrom(formatUsd(standing.consumedUsd)) : t.serve.nothingMeasured(STORE_DIR), 74, ' '))}`,
|
|
2796
2800
|
);
|
|
2801
|
+
if (standing !== null && standing.coverage === 'partial') {
|
|
2802
|
+
console.log(
|
|
2803
|
+
` ${c.yellow(wrap(t.serve.partialCoverage(standing.measuredDays, standing.elapsedDays, standing.period.id), 74, ' '))}`,
|
|
2804
|
+
);
|
|
2805
|
+
}
|
|
2797
2806
|
if (limitUsd === undefined) {
|
|
2798
2807
|
console.log(` ${c.dim(wrap(t.serve.noBudget(), 74, ' '))}`);
|
|
2799
2808
|
}
|
|
@@ -3160,6 +3169,72 @@ async function commandStore(
|
|
|
3160
3169
|
console.log(
|
|
3161
3170
|
` ${c.dim(wrap(keepDays === undefined ? t.store.noRetention() : t.store.retention(String(keepDays)), 74, ' '))}`,
|
|
3162
3171
|
);
|
|
3172
|
+
|
|
3173
|
+
/**
|
|
3174
|
+
* The live budget, printed here because this is where the measurement lives.
|
|
3175
|
+
*
|
|
3176
|
+
* The same call `serve` makes and the same call the MCP guard makes, so the
|
|
3177
|
+
* three cannot disagree about how much of the month is gone — which is the
|
|
3178
|
+
* whole point of the number existing in one place.
|
|
3179
|
+
*/
|
|
3180
|
+
renderBudget(budgetPositions(resolved.records, config.spend, { catalogue: pricing }), t, n);
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
/**
|
|
3184
|
+
* One budget standing, rendered.
|
|
3185
|
+
*
|
|
3186
|
+
* Coverage before the money, deliberately. A reader who sees "$61 of $100"
|
|
3187
|
+
* first has already formed a view by the time they reach "over three of
|
|
3188
|
+
* nineteen elapsed days", and the second sentence has to undo the first.
|
|
3189
|
+
*/
|
|
3190
|
+
function renderBudget(report: BudgetReport, t: CliMessages, n: (value: number) => string): void {
|
|
3191
|
+
const standing = report.positions[0];
|
|
3192
|
+
if (standing === undefined) {
|
|
3193
|
+
if (report.unmeasuredScopes.length > 0) {
|
|
3194
|
+
console.log();
|
|
3195
|
+
console.log(
|
|
3196
|
+
` ${c.dim(wrap(t.store.budgetScopesUnmeasured(report.unmeasuredScopes.length), 74, ' '))}`,
|
|
3197
|
+
);
|
|
3198
|
+
}
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
console.log();
|
|
3203
|
+
console.log(c.bold(t.store.budgetHeading(standing.period.id)));
|
|
3204
|
+
|
|
3205
|
+
if (standing.coverage === 'none') {
|
|
3206
|
+
// Nothing measured is never rendered as nothing spent. A dead store and a
|
|
3207
|
+
// quiet month produce the same zero, and only one of them is good news.
|
|
3208
|
+
console.log(` ${c.red(wrap(t.store.budgetNothingMeasured(standing.elapsedDays), 74, ' '))}`);
|
|
3209
|
+
return;
|
|
3210
|
+
}
|
|
3211
|
+
if (standing.coverage === 'partial') {
|
|
3212
|
+
console.log(
|
|
3213
|
+
` ${c.yellow(wrap(t.store.budgetPartial(standing.measuredDays, standing.elapsedDays, standing.unmeasuredDays.join(', ')), 74, ' '))}`,
|
|
3214
|
+
);
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
const share = standing.burn.consumedShare;
|
|
3218
|
+
console.log(
|
|
3219
|
+
` ${t.store.budgetStanding(
|
|
3220
|
+
formatUsd(standing.consumedUsd),
|
|
3221
|
+
formatUsd(standing.limitUsd),
|
|
3222
|
+
share === null ? '—' : `${Math.round(share * 100)}%`,
|
|
3223
|
+
n(standing.measuredDays),
|
|
3224
|
+
n(standing.period.days),
|
|
3225
|
+
)}`,
|
|
3226
|
+
);
|
|
3227
|
+
const line = t.store.budgetShape(
|
|
3228
|
+
standing.burn.shape,
|
|
3229
|
+
Math.round(standing.burn.elapsedShare * 100),
|
|
3230
|
+
standing.coverage,
|
|
3231
|
+
);
|
|
3232
|
+
console.log(` ${standing.verdict === 'over' ? c.red(line) : c.dim(wrap(line, 74, ' '))}`);
|
|
3233
|
+
// Only where there is a shape to disclaim. "That is a shape, not a forecast"
|
|
3234
|
+
// under "nothing to compare against" is a disclaimer about nothing.
|
|
3235
|
+
if (standing.burn.shape !== 'cannot-tell') {
|
|
3236
|
+
console.log(` ${c.dim(wrap(t.store.budgetNeverForecast(), 74, ' '))}`);
|
|
3237
|
+
}
|
|
3163
3238
|
}
|
|
3164
3239
|
|
|
3165
3240
|
/**
|
|
@@ -3348,7 +3423,12 @@ async function commandConnect(
|
|
|
3348
3423
|
* same action planned twice — and no series, however long, becomes a
|
|
3349
3424
|
* forecast.
|
|
3350
3425
|
*/
|
|
3351
|
-
async function commandHistory(
|
|
3426
|
+
async function commandHistory(
|
|
3427
|
+
args: Args,
|
|
3428
|
+
config: TrazumConfig,
|
|
3429
|
+
pricing: PricingCatalogue,
|
|
3430
|
+
t: CliMessages,
|
|
3431
|
+
): Promise<void> {
|
|
3352
3432
|
/**
|
|
3353
3433
|
* `--store` builds the series from measured spend already on disk.
|
|
3354
3434
|
*
|
|
@@ -3452,7 +3532,23 @@ async function commandHistory(args: Args, pricing: PricingCatalogue, t: CliMessa
|
|
|
3452
3532
|
throw new Error(t.history.needsThree(String(history.periods.length)));
|
|
3453
3533
|
}
|
|
3454
3534
|
|
|
3455
|
-
|
|
3535
|
+
/**
|
|
3536
|
+
* The waiver record — closing the gap 1.40 named and could not fill.
|
|
3537
|
+
*
|
|
3538
|
+
* 1.40 wanted to say "this finding has been waived three times in a row" and
|
|
3539
|
+
* refused to, because the only material available was the config as it
|
|
3540
|
+
* stands, and a past reconstructed from a present is a guess wearing a
|
|
3541
|
+
* record's clothes. The material exists now: since 1.48 a waiver that
|
|
3542
|
+
* silences a gate writes down that it did, and this reads those lines back.
|
|
3543
|
+
*
|
|
3544
|
+
* Read from the working directory rather than from the reports directory:
|
|
3545
|
+
* the waiver record belongs to the repository whose gates fired, and the
|
|
3546
|
+
* stored reports may have come from anywhere.
|
|
3547
|
+
*/
|
|
3548
|
+
const waivers = await readWaiverLog('.');
|
|
3549
|
+
const waiverReport = waiverHistory(waivers.uses, config.waive ?? []);
|
|
3550
|
+
|
|
3551
|
+
const stamped = { ...history, unrecognizedFiles: unrecognized, waivers: waiverReport };
|
|
3456
3552
|
const n = (value: number): string => value.toLocaleString(t.numberLocale);
|
|
3457
3553
|
const day = (ms: number): string => new Date(ms).toISOString().slice(0, 10);
|
|
3458
3554
|
const pct = (value: number): string => `${(value * 100).toFixed(1)}%`;
|
|
@@ -3506,6 +3602,58 @@ async function commandHistory(args: Args, pricing: PricingCatalogue, t: CliMessa
|
|
|
3506
3602
|
for (const name of unrecognized) {
|
|
3507
3603
|
out.push(md ? `- ${t.history.unrecognized(name)}` : ` ${t.history.unrecognized(name)}`);
|
|
3508
3604
|
}
|
|
3605
|
+
/**
|
|
3606
|
+
* The waiver record, printed only once something has been recorded.
|
|
3607
|
+
*
|
|
3608
|
+
* Silent on a repository that has never waived anything, rather than a
|
|
3609
|
+
* heading over "0 uses" — an empty section teaches a reader to skip the
|
|
3610
|
+
* section, and this is the one they should not learn to skip.
|
|
3611
|
+
*/
|
|
3612
|
+
if (waivers.present) {
|
|
3613
|
+
out.push('');
|
|
3614
|
+
out.push(md ? `### ${t.history.waiverHeading()}` : t.history.waiverHeading());
|
|
3615
|
+
if (waiverReport.totalUses === 0) {
|
|
3616
|
+
out.push(md ? `- ${t.history.waiverNoneRecorded()}` : ` ${t.history.waiverNoneRecorded()}`);
|
|
3617
|
+
} else {
|
|
3618
|
+
const since = t.history.waiverSince(waiverReport.since ?? '', waiverReport.totalUses);
|
|
3619
|
+
out.push(md ? `- ${since}` : ` ${since}`);
|
|
3620
|
+
out.push(md ? `- _${t.history.waiverStartsHere()}_` : ` ${t.history.waiverStartsHere()}`);
|
|
3621
|
+
for (const habit of waiverReport.habits) {
|
|
3622
|
+
out.push('');
|
|
3623
|
+
const head = t.history.waiverHabit(
|
|
3624
|
+
habit.gate,
|
|
3625
|
+
habit.uses,
|
|
3626
|
+
habit.days,
|
|
3627
|
+
habit.firstDay,
|
|
3628
|
+
habit.lastDay,
|
|
3629
|
+
);
|
|
3630
|
+
out.push(md ? `- **${head}**` : ` ${head}`);
|
|
3631
|
+
const rows = [t.history.waiverVerdict(habit.verdict)];
|
|
3632
|
+
// The reason as it stands *now* — never read backwards onto an
|
|
3633
|
+
// older use, which is the same mistake the record exists to avoid.
|
|
3634
|
+
const latest = habit.reasons[habit.reasons.length - 1];
|
|
3635
|
+
if (latest !== undefined) rows.push(t.history.waiverReasonNow(latest));
|
|
3636
|
+
if (habit.reasons.length > 1) rows.push(t.history.waiverReasonsChanged(habit.reasons.length));
|
|
3637
|
+
const firstExpiry = habit.expiries[0];
|
|
3638
|
+
const lastExpiry = habit.expiries[habit.expiries.length - 1];
|
|
3639
|
+
if (habit.expiries.length > 1 && firstExpiry !== undefined && lastExpiry !== undefined) {
|
|
3640
|
+
rows.push(t.history.waiverExpiriesMoved(firstExpiry, lastExpiry, habit.expiries.length - 1));
|
|
3641
|
+
}
|
|
3642
|
+
if (!habit.stillConfigured) rows.push(t.history.waiverNoLongerConfigured());
|
|
3643
|
+
for (const row of rows) out.push(md ? ` - ${row}` : ` ${row}`);
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
if (waiverReport.neverUsed.length > 0) {
|
|
3647
|
+
out.push('');
|
|
3648
|
+
const dead = t.history.waiverNeverUsed(waiverReport.neverUsed.join(', '));
|
|
3649
|
+
out.push(md ? `- ${dead}` : ` ${dead}`);
|
|
3650
|
+
}
|
|
3651
|
+
if (waivers.unreadable.length > 0) {
|
|
3652
|
+
const bad = t.history.waiverUnreadable(waivers.unreadable.length, WAIVER_LOG);
|
|
3653
|
+
out.push(md ? `- ${bad}` : ` ${bad}`);
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3509
3657
|
if (fromStore) {
|
|
3510
3658
|
out.push('');
|
|
3511
3659
|
const note = t.history.storeNoLabels();
|
|
@@ -4248,7 +4396,16 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4248
4396
|
* way, because a waived failure that vanished from the output would be a
|
|
4249
4397
|
* finding deleted with extra steps.
|
|
4250
4398
|
*/
|
|
4251
|
-
|
|
4399
|
+
/**
|
|
4400
|
+
* Uses recorded this run, flushed after the gates have finished.
|
|
4401
|
+
*
|
|
4402
|
+
* Collected rather than written inline because `waived` is synchronous and
|
|
4403
|
+
* called from seven places inside the gate pass. Writing from each of them
|
|
4404
|
+
* would mean seven awaits threaded through the exit-code logic — the one
|
|
4405
|
+
* part of this command where a mistake turns a red build green.
|
|
4406
|
+
*/
|
|
4407
|
+
const waiverUses: WaiverUse[] = [];
|
|
4408
|
+
const waived = (gate: string, measuredUsd: number | null = null, limitUsd: number | null = null): boolean => {
|
|
4252
4409
|
const found = waiverFor(gate);
|
|
4253
4410
|
if (found === null) return false;
|
|
4254
4411
|
if (found.expired) {
|
|
@@ -4264,6 +4421,29 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4264
4421
|
console.error(
|
|
4265
4422
|
c.yellow(t.profile.waiveActive(gate, found.entry.reason, found.entry.until, String(daysLeft))),
|
|
4266
4423
|
);
|
|
4424
|
+
/**
|
|
4425
|
+
* Recorded **when it silences something**, never when it is configured.
|
|
4426
|
+
*
|
|
4427
|
+
* A waiver nobody's build has ever hit is not a habit — it is dead config
|
|
4428
|
+
* — and the history reports the two apart. This is also the only honest
|
|
4429
|
+
* way to build the record 1.40 refused to invent: it starts today and
|
|
4430
|
+
* says so, rather than reconstructing a past from the present.
|
|
4431
|
+
*
|
|
4432
|
+
* The reason and the expiry are taken from the config **as it stands at
|
|
4433
|
+
* this moment**, because that is the decision that was actually in force.
|
|
4434
|
+
* Reading today's reason back onto last quarter's use is the same mistake
|
|
4435
|
+
* one layer down.
|
|
4436
|
+
*/
|
|
4437
|
+
waiverUses.push({
|
|
4438
|
+
schemaVersion: 1,
|
|
4439
|
+
day: waiverDay(new Date()),
|
|
4440
|
+
gate,
|
|
4441
|
+
reason: found.entry.reason,
|
|
4442
|
+
until: found.entry.until,
|
|
4443
|
+
commit: process.env.GITHUB_SHA ?? process.env.CI_COMMIT_SHA ?? null,
|
|
4444
|
+
measuredUsd,
|
|
4445
|
+
limitUsd,
|
|
4446
|
+
});
|
|
4267
4447
|
return true;
|
|
4268
4448
|
};
|
|
4269
4449
|
const applyGates = (): void => {
|
|
@@ -4312,7 +4492,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4312
4492
|
}
|
|
4313
4493
|
if (usd > limit) {
|
|
4314
4494
|
console.error(c.red(t.profile.labelBudgetFailed(label, formatUsd(usd), formatUsd(limit))));
|
|
4315
|
-
if (!waived(`byLabel:${label}
|
|
4495
|
+
if (!waived(`byLabel:${label}`, usd, limit)) process.exitCode = 1;
|
|
4316
4496
|
} else {
|
|
4317
4497
|
console.error(c.dim(t.profile.labelBudgetOk(label, formatUsd(usd), formatUsd(limit))));
|
|
4318
4498
|
}
|
|
@@ -4385,7 +4565,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4385
4565
|
* copy says so.
|
|
4386
4566
|
*/
|
|
4387
4567
|
explainFailure(report.total.totalUsd - maxUsd);
|
|
4388
|
-
if (!waived('maxUsd')) process.exitCode = 1;
|
|
4568
|
+
if (!waived('maxUsd', report.total.totalUsd, maxUsd)) process.exitCode = 1;
|
|
4389
4569
|
} else {
|
|
4390
4570
|
console.error(c.dim(t.profile.maxUsdOk(formatUsd(report.total.totalUsd), formatUsd(maxUsd))));
|
|
4391
4571
|
explainMargin(report.total.totalUsd, maxUsd);
|
|
@@ -4425,7 +4605,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4425
4605
|
process.exitCode = 1;
|
|
4426
4606
|
} else if (againstDelta > maxGrowth) {
|
|
4427
4607
|
console.error(c.red(t.profile.maxGrowthUsdFailed(formatSignedUsd(againstDelta), formatUsd(maxGrowth))));
|
|
4428
|
-
if (!waived('maxGrowthUsd')) process.exitCode = 1;
|
|
4608
|
+
if (!waived('maxGrowthUsd', againstDelta, maxGrowth)) process.exitCode = 1;
|
|
4429
4609
|
}
|
|
4430
4610
|
}
|
|
4431
4611
|
/**
|
|
@@ -4443,7 +4623,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4443
4623
|
console.error(
|
|
4444
4624
|
c.red(t.profile.maxCacheLossFailed(formatUsd(gateCache.deltaUsd), formatUsd(maxLoss))),
|
|
4445
4625
|
);
|
|
4446
|
-
if (!waived('maxCacheLossUsd')) process.exitCode = 1;
|
|
4626
|
+
if (!waived('maxCacheLossUsd', gateCache.deltaUsd, maxLoss)) process.exitCode = 1;
|
|
4447
4627
|
} else if (gateCache.worstCaseDeltaUsd > maxLoss) {
|
|
4448
4628
|
console.error(
|
|
4449
4629
|
c.red(
|
|
@@ -4454,7 +4634,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4454
4634
|
),
|
|
4455
4635
|
),
|
|
4456
4636
|
);
|
|
4457
|
-
if (!waived('maxCacheLossUsd')) process.exitCode = 1;
|
|
4637
|
+
if (!waived('maxCacheLossUsd', gateCache.worstCaseDeltaUsd, maxLoss)) process.exitCode = 1;
|
|
4458
4638
|
} else {
|
|
4459
4639
|
console.error(
|
|
4460
4640
|
c.dim(t.profile.maxCacheLossOk(formatUsd(Math.max(0, gateCache.worstCaseDeltaUsd)), formatUsd(maxLoss))),
|
|
@@ -4503,7 +4683,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4503
4683
|
c.red(`${t.profile.maxDayFailed(worst.day, formatUsd(worst.usd), formatUsd(maxDay))}${suspect}`),
|
|
4504
4684
|
);
|
|
4505
4685
|
explainFailure(worst.usd - maxDay, { namesLargest: true });
|
|
4506
|
-
if (!waived('maxDayUsd')) process.exitCode = 1;
|
|
4686
|
+
if (!waived('maxDayUsd', worst.usd, maxDay)) process.exitCode = 1;
|
|
4507
4687
|
} else {
|
|
4508
4688
|
console.error(c.dim(t.profile.maxDayOk(worst.day, formatUsd(worst.usd), formatUsd(maxDay))));
|
|
4509
4689
|
explainMargin(worst.usd, maxDay);
|
|
@@ -4545,7 +4725,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4545
4725
|
c.red(t.profile.maxSessionFailed(formatUsd(report.sessionSpend.maxUsd), formatUsd(maxSession), n(report.sessionSpend.sessions))),
|
|
4546
4726
|
);
|
|
4547
4727
|
explainFailure(report.sessionSpend.maxUsd - maxSession);
|
|
4548
|
-
if (!waived('maxSessionUsd')) process.exitCode = 1;
|
|
4728
|
+
if (!waived('maxSessionUsd', report.sessionSpend.maxUsd, maxSession)) process.exitCode = 1;
|
|
4549
4729
|
} else {
|
|
4550
4730
|
console.error(
|
|
4551
4731
|
c.dim(t.profile.maxSessionOk(formatUsd(report.sessionSpend.maxUsd), formatUsd(maxSession), n(report.sessionSpend.sessions))),
|
|
@@ -4579,6 +4759,26 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4579
4759
|
}
|
|
4580
4760
|
};
|
|
4581
4761
|
|
|
4762
|
+
/**
|
|
4763
|
+
* Writes down every waiver that silenced something this run.
|
|
4764
|
+
*
|
|
4765
|
+
* **A failure here never fails the build.** The gate's job is the exit code;
|
|
4766
|
+
* a read-only checkout or a full disk must not turn a passing build red on
|
|
4767
|
+
* account of bookkeeping. The problem is reported and the gate's own verdict
|
|
4768
|
+
* stands — which is also why this runs after `recordGates` rather than
|
|
4769
|
+
* inside it: nothing about the exit code depends on the write.
|
|
4770
|
+
*/
|
|
4771
|
+
const recordWaiverUses = async (): Promise<void> => {
|
|
4772
|
+
if (waiverUses.length === 0) return;
|
|
4773
|
+
for (const use of waiverUses) {
|
|
4774
|
+
const failed = await appendWaiverUse('.', use);
|
|
4775
|
+
if (failed !== null) {
|
|
4776
|
+
console.error(c.dim(t.profile.waiveNotRecorded(WAIVER_LOG, failed)));
|
|
4777
|
+
return;
|
|
4778
|
+
}
|
|
4779
|
+
}
|
|
4780
|
+
};
|
|
4781
|
+
|
|
4582
4782
|
/**
|
|
4583
4783
|
* The side files the caller asked for. Written on **both** output paths:
|
|
4584
4784
|
* under --json the human rendering returns early, and the first version of
|
|
@@ -4745,6 +4945,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4745
4945
|
),
|
|
4746
4946
|
);
|
|
4747
4947
|
recordGates();
|
|
4948
|
+
await recordWaiverUses();
|
|
4748
4949
|
await writeSideFiles();
|
|
4749
4950
|
return;
|
|
4750
4951
|
}
|
|
@@ -5899,6 +6100,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
5899
6100
|
reportProfileGaps(report, t, n, pricingStale);
|
|
5900
6101
|
|
|
5901
6102
|
recordGates();
|
|
6103
|
+
await recordWaiverUses();
|
|
5902
6104
|
|
|
5903
6105
|
await writeSideFiles();
|
|
5904
6106
|
}
|
|
@@ -7612,7 +7814,7 @@ async function main(): Promise<void> {
|
|
|
7612
7814
|
await commandVerify(args, pricing, t);
|
|
7613
7815
|
break;
|
|
7614
7816
|
case 'history':
|
|
7615
|
-
await commandHistory(args, pricing, t);
|
|
7817
|
+
await commandHistory(args, config, pricing, t);
|
|
7616
7818
|
break;
|
|
7617
7819
|
case 'connect':
|
|
7618
7820
|
await commandConnect(args, pricing, t);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a waiver's uses are written down.
|
|
3
|
+
*
|
|
4
|
+
* The core decides what a use means and what a run of them adds up to; this
|
|
5
|
+
* decides where the bytes go — the same split every module here follows, so
|
|
6
|
+
* `@trazum/core` stays browser-safe and the CLI keeps its monopoly on I/O.
|
|
7
|
+
*
|
|
8
|
+
* **Append-only, and never rewritten.** There is deliberately no prune, no
|
|
9
|
+
* compaction and no `--clear`: a record of decisions that the tool can erase
|
|
10
|
+
* is a record nobody can rely on, and the one thing a waiver history is for is
|
|
11
|
+
* being awkward six months later. Deleting the file is a thing a person does
|
|
12
|
+
* with `rm`, on purpose, having seen it.
|
|
13
|
+
*
|
|
14
|
+
* **A write that fails never fails the run.** The gate's job is the exit code.
|
|
15
|
+
* A read-only checkout, a full disk or a directory somebody's CI cannot create
|
|
16
|
+
* must not turn a passing build red on account of bookkeeping — the failure is
|
|
17
|
+
* reported and the gate's own verdict stands.
|
|
18
|
+
*
|
|
19
|
+
* **A line that will not parse is counted and skipped**, exactly as in the
|
|
20
|
+
* store. Losing the whole history because one line is broken would be the
|
|
21
|
+
* worst possible response; pretending the history is complete would be the
|
|
22
|
+
* second worst.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { isWaiverUse } from '@trazum/core';
|
|
28
|
+
import type { WaiverUse } from '@trazum/core';
|
|
29
|
+
|
|
30
|
+
/** One file, not one per month: a waiver history is small and read whole. */
|
|
31
|
+
export const WAIVER_LOG = '.trazum/waivers.jsonl';
|
|
32
|
+
|
|
33
|
+
export interface WaiverReadResult {
|
|
34
|
+
uses: WaiverUse[];
|
|
35
|
+
/** 1-based positions of lines that would not parse. Named, never dropped quietly. */
|
|
36
|
+
unreadable: number[];
|
|
37
|
+
/** False when the file does not exist — "nothing recorded" is not "no file". */
|
|
38
|
+
present: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function readWaiverLog(root: string): Promise<WaiverReadResult> {
|
|
42
|
+
let raw: string;
|
|
43
|
+
try {
|
|
44
|
+
raw = await readFile(join(root, WAIVER_LOG), 'utf8');
|
|
45
|
+
} catch {
|
|
46
|
+
// Absent is the normal state of a repository that has never waived
|
|
47
|
+
// anything, and it is not an error.
|
|
48
|
+
return { uses: [], unreadable: [], present: false };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const uses: WaiverUse[] = [];
|
|
52
|
+
const unreadable: number[] = [];
|
|
53
|
+
raw.split('\n').forEach((line, index) => {
|
|
54
|
+
if (line.trim() === '') return;
|
|
55
|
+
try {
|
|
56
|
+
const parsed: unknown = JSON.parse(line);
|
|
57
|
+
if (isWaiverUse(parsed)) uses.push(parsed);
|
|
58
|
+
else unreadable.push(index + 1);
|
|
59
|
+
} catch {
|
|
60
|
+
unreadable.push(index + 1);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return { uses, unreadable, present: true };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Appends one use, and swallows any failure after reporting it.
|
|
68
|
+
*
|
|
69
|
+
* Returns the error message rather than throwing, so the caller can print it
|
|
70
|
+
* beside the gate's own output without the gate ever depending on the write.
|
|
71
|
+
*/
|
|
72
|
+
export async function appendWaiverUse(root: string, use: WaiverUse): Promise<string | null> {
|
|
73
|
+
const path = join(root, WAIVER_LOG);
|
|
74
|
+
try {
|
|
75
|
+
await mkdir(join(path, '..'), { recursive: true });
|
|
76
|
+
await writeFile(path, `${JSON.stringify(use)}\n`, { flag: 'a', mode: 0o600 });
|
|
77
|
+
return null;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return error instanceof Error ? error.message : String(error);
|
|
80
|
+
}
|
|
81
|
+
}
|