@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/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +29 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +29 -1
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +43 -1
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +146 -24
- 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 +41 -2
- package/src/i18n/es.ts +41 -2
- package/src/i18n/types.ts +37 -1
- package/src/index.ts +157 -21
- package/src/waiver-log.ts +81 -0
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
import type { WaiverUse } from '@trazum/core';
|
|
25
|
+
/** One file, not one per month: a waiver history is small and read whole. */
|
|
26
|
+
export declare const WAIVER_LOG = ".trazum/waivers.jsonl";
|
|
27
|
+
export interface WaiverReadResult {
|
|
28
|
+
uses: WaiverUse[];
|
|
29
|
+
/** 1-based positions of lines that would not parse. Named, never dropped quietly. */
|
|
30
|
+
unreadable: number[];
|
|
31
|
+
/** False when the file does not exist — "nothing recorded" is not "no file". */
|
|
32
|
+
present: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare function readWaiverLog(root: string): Promise<WaiverReadResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Appends one use, and swallows any failure after reporting it.
|
|
37
|
+
*
|
|
38
|
+
* Returns the error message rather than throwing, so the caller can print it
|
|
39
|
+
* beside the gate's own output without the gate ever depending on the write.
|
|
40
|
+
*/
|
|
41
|
+
export declare function appendWaiverUse(root: string, use: WaiverUse): Promise<string | null>;
|
|
42
|
+
//# sourceMappingURL=waiver-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"waiver-log.d.ts","sourceRoot":"","sources":["../src/waiver-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,6EAA6E;AAC7E,eAAO,MAAM,UAAU,0BAA0B,CAAC;AAElD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,qFAAqF;IACrF,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,gFAAgF;IAChF,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAuB3E;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAS1F"}
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { isWaiverUse } from '@trazum/core';
|
|
27
|
+
/** One file, not one per month: a waiver history is small and read whole. */
|
|
28
|
+
export const WAIVER_LOG = '.trazum/waivers.jsonl';
|
|
29
|
+
export async function readWaiverLog(root) {
|
|
30
|
+
let raw;
|
|
31
|
+
try {
|
|
32
|
+
raw = await readFile(join(root, WAIVER_LOG), 'utf8');
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Absent is the normal state of a repository that has never waived
|
|
36
|
+
// anything, and it is not an error.
|
|
37
|
+
return { uses: [], unreadable: [], present: false };
|
|
38
|
+
}
|
|
39
|
+
const uses = [];
|
|
40
|
+
const unreadable = [];
|
|
41
|
+
raw.split('\n').forEach((line, index) => {
|
|
42
|
+
if (line.trim() === '')
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(line);
|
|
46
|
+
if (isWaiverUse(parsed))
|
|
47
|
+
uses.push(parsed);
|
|
48
|
+
else
|
|
49
|
+
unreadable.push(index + 1);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
unreadable.push(index + 1);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return { uses, unreadable, present: true };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Appends one use, and swallows any failure after reporting it.
|
|
59
|
+
*
|
|
60
|
+
* Returns the error message rather than throwing, so the caller can print it
|
|
61
|
+
* beside the gate's own output without the gate ever depending on the write.
|
|
62
|
+
*/
|
|
63
|
+
export async function appendWaiverUse(root, use) {
|
|
64
|
+
const path = join(root, WAIVER_LOG);
|
|
65
|
+
try {
|
|
66
|
+
await mkdir(join(path, '..'), { recursive: true });
|
|
67
|
+
await writeFile(path, `${JSON.stringify(use)}\n`, { flag: 'a', mode: 0o600 });
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
return error instanceof Error ? error.message : String(error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=waiver-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"waiver-log.js","sourceRoot":"","sources":["../src/waiver-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAG3C,6EAA6E;AAC7E,MAAM,CAAC,MAAM,UAAU,GAAG,uBAAuB,CAAC;AAUlD,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAY;IAC9C,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,mEAAmE;QACnE,oCAAoC;QACpC,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IACtD,CAAC;IAED,MAAM,IAAI,GAAgB,EAAE,CAAC;IAC7B,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QACtC,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAO;QAC/B,IAAI,CAAC;YACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACzC,IAAI,WAAW,CAAC,MAAM,CAAC;gBAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;;gBACtC,UAAU,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QAClC,CAAC;QAAC,MAAM,CAAC;YACP,UAAU,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC7C,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAY,EAAE,GAAc;IAChE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACnD,MAAM,SAAS,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9E,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAChE,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trazum/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.48.0",
|
|
4
4
|
"description": "Trazum CLI: find where your LLM bill goes, price every finding per month, and enforce token budgets in CI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "David Mu\u00f1oz Rey",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"prepublishOnly": "npm run build && npm test"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@trazum/core": "1.
|
|
40
|
+
"@trazum/core": "1.48.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^26.2.0",
|
package/src/i18n/en.ts
CHANGED
|
@@ -1554,6 +1554,8 @@ ${bold('EXAMPLES')}
|
|
|
1554
1554
|
`WAIVED — the ${gate} failure above is on the record and silenced until ${until} (${daysLeft} days left): "${reason}". The bill still counts it; only the exit code is quiet, and the day the waiver expires this gate fails again.`,
|
|
1555
1555
|
waiveExpired: (gate, until, reason) =>
|
|
1556
1556
|
`The waiver on ${gate} expired on ${until} and no longer silences anything. It was written for: "${reason}". Renew it with a new date and a current reason, or fix what it was covering — an expired waiver left in place is a finding deleted with extra steps.`,
|
|
1557
|
+
waiveNotRecorded: (path, because) =>
|
|
1558
|
+
` (This waiver was not written to ${path}: ${because}. The gate's verdict above is unaffected.)`,
|
|
1557
1559
|
summaryNoComparison: () =>
|
|
1558
1560
|
'No previous log was given, so nothing here says whether the bill moved — a summary without a comparison states the bill, not its stability.',
|
|
1559
1561
|
summaryFooter: () =>
|
|
@@ -1807,6 +1809,33 @@ ${bold('EXAMPLES')}
|
|
|
1807
1809
|
unrecognized: (name) => `${name} is neither a stored report nor a saved plan, so it is in no series above.`,
|
|
1808
1810
|
footer: () =>
|
|
1809
1811
|
'A series names shapes, not futures. Twenty points make a trend visible; they do not make next month knowable — where these lines go next is yours to judge.',
|
|
1812
|
+
waiverHeading: () => 'What this repository has been living with',
|
|
1813
|
+
waiverSince: (day, uses) =>
|
|
1814
|
+
`${uses} recorded ${uses === 1 ? 'use' : 'uses'} since ${day}, when recording started.`,
|
|
1815
|
+
waiverNoneRecorded: () =>
|
|
1816
|
+
'No waiver has silenced a gate since recording started. Nothing here is inferred from the config — a waiver written down and never hit is not a decision anyone is living with.',
|
|
1817
|
+
waiverHabit: (gate, uses, days, firstDay, lastDay) =>
|
|
1818
|
+
`${gate}: ${uses} ${uses === 1 ? 'use' : 'uses'} across ${days} ${days === 1 ? 'day' : 'days'}, ${firstDay} to ${lastDay}`,
|
|
1819
|
+
waiverVerdict: (verdict) =>
|
|
1820
|
+
verdict === 'used-once'
|
|
1821
|
+
? 'Used once. Nothing to read into it yet.'
|
|
1822
|
+
: verdict === 'recurring'
|
|
1823
|
+
? 'The same decision, holding. The gate keeps firing and the reason has not moved.'
|
|
1824
|
+
: verdict === 'renewed-without-revisiting'
|
|
1825
|
+
? 'The expiry has moved and the reason has not. That is the shape a decision takes when nobody is revisiting it — which is sometimes exactly right, and worth saying out loud either way.'
|
|
1826
|
+
: 'The reason changed between uses. Somebody looked again.',
|
|
1827
|
+
waiverReasonNow: (reason) => `Reason: ${reason}`,
|
|
1828
|
+
waiverReasonsChanged: (count) => `${count} different reasons given over that time.`,
|
|
1829
|
+
waiverExpiriesMoved: (from, to, count) =>
|
|
1830
|
+
`Expiry moved ${count} ${count === 1 ? 'time' : 'times'}: ${from} → ${to}.`,
|
|
1831
|
+
waiverNoLongerConfigured: () =>
|
|
1832
|
+
'Not in the config any more. The decision was reversed; the record keeps it.',
|
|
1833
|
+
waiverNeverUsed: (gates) =>
|
|
1834
|
+
`Waived in the config and never hit by a recorded run: ${gates}. Either the gate stopped failing — good news nobody wrote down — or the waiver names a situation that does not arise. Both are worth deleting.`,
|
|
1835
|
+
waiverUnreadable: (count, path) =>
|
|
1836
|
+
`${count} ${count === 1 ? 'line' : 'lines'} in ${path} could not be read and are not counted above.`,
|
|
1837
|
+
waiverStartsHere: () =>
|
|
1838
|
+
'Nothing before that day exists. This record began when recording did, and no past was reconstructed from the config as it stands.',
|
|
1810
1839
|
},
|
|
1811
1840
|
|
|
1812
1841
|
verify: {
|
|
@@ -1814,8 +1843,18 @@ ${bold('EXAMPLES')}
|
|
|
1814
1843
|
'Point this at a saved plan and a newer log: trazum verify plan.json --against usage.jsonl. It says, per action, whether the change arrived, did not arrive, or cannot be told — and never fewer than those three.',
|
|
1815
1844
|
needsAgainst: () =>
|
|
1816
1845
|
'--against <newer.jsonl|dir> is required. A plan can only be verified against a log that came after it; without one there is nothing to hold the prediction to.',
|
|
1817
|
-
badPlan: (path) =>
|
|
1818
|
-
`${path} is not a plan document this tool can verify
|
|
1846
|
+
badPlan: (path, why) =>
|
|
1847
|
+
`${path} is not a plan document this tool can verify: ${why}. Expected the JSON that "trazum plan -o" writes.`,
|
|
1848
|
+
planRefusal: (why) =>
|
|
1849
|
+
why.kind === 'not-json'
|
|
1850
|
+
? 'it is not valid JSON'
|
|
1851
|
+
: why.kind === 'not-an-object'
|
|
1852
|
+
? 'the top level is not a JSON object'
|
|
1853
|
+
: why.kind === 'wrong-schema-version'
|
|
1854
|
+
? `schemaVersion is ${JSON.stringify(why.found)} rather than 1`
|
|
1855
|
+
: why.kind === 'actions-not-a-list'
|
|
1856
|
+
? 'there is no actions array'
|
|
1857
|
+
: `action ${why.index + 1} is malformed (${why.because})`,
|
|
1819
1858
|
heading: (actions, planDate) =>
|
|
1820
1859
|
planDate === null
|
|
1821
1860
|
? `Did it work? ${actions} actions from an undated plan, against this log`
|
package/src/i18n/es.ts
CHANGED
|
@@ -1584,6 +1584,8 @@ ${bold('EJEMPLOS')}
|
|
|
1584
1584
|
`WAIVED — el fallo de ${gate} de arriba queda registrado y silenciado hasta ${until} (${daysLeft} días restantes): "${reason}". La factura lo sigue contando; solo el código de salida calla, y el día que caduque el waiver este gate vuelve a fallar.`,
|
|
1585
1585
|
waiveExpired: (gate, until, reason) =>
|
|
1586
1586
|
`El waiver de ${gate} caducó el ${until} y ya no silencia nada. Se escribió por: "${reason}". Renuévalo con fecha nueva y razón vigente, o arregla lo que cubría — un waiver caducado dejado ahí es un hallazgo borrado con pasos extra.`,
|
|
1587
|
+
waiveNotRecorded: (path, because) =>
|
|
1588
|
+
` (Este waiver no se pudo escribir en ${path}: ${because}. El veredicto de la puerta de arriba no cambia.)`,
|
|
1587
1589
|
summaryNoComparison: () =>
|
|
1588
1590
|
'No se dio un registro anterior, así que nada de aquí dice si la factura se movió — un resumen sin comparación indica la factura, no su estabilidad.',
|
|
1589
1591
|
summaryFooter: () =>
|
|
@@ -1837,6 +1839,33 @@ ${bold('EJEMPLOS')}
|
|
|
1837
1839
|
unrecognized: (name) => `${name} no es ni un informe guardado ni un plan guardado, así que no está en ninguna serie de arriba.`,
|
|
1838
1840
|
footer: () =>
|
|
1839
1841
|
'Una serie nombra formas, no futuros. Veinte puntos hacen visible una tendencia; no hacen conocible el mes que viene — adónde van estas líneas después lo juzgas tú.',
|
|
1842
|
+
waiverHeading: () => 'Con lo que este repositorio ha estado conviviendo',
|
|
1843
|
+
waiverSince: (day, uses) =>
|
|
1844
|
+
`${uses} ${uses === 1 ? 'uso registrado' : 'usos registrados'} desde el ${day}, cuando empezó el registro.`,
|
|
1845
|
+
waiverNoneRecorded: () =>
|
|
1846
|
+
'Ningún waiver ha silenciado una puerta desde que empezó el registro. Nada de aquí se deduce de la configuración — un waiver escrito y nunca usado no es una decisión con la que nadie conviva.',
|
|
1847
|
+
waiverHabit: (gate, uses, days, firstDay, lastDay) =>
|
|
1848
|
+
`${gate}: ${uses} ${uses === 1 ? 'uso' : 'usos'} en ${days} ${days === 1 ? 'día' : 'días'}, del ${firstDay} al ${lastDay}`,
|
|
1849
|
+
waiverVerdict: (verdict) =>
|
|
1850
|
+
verdict === 'used-once'
|
|
1851
|
+
? 'Usado una vez. Todavía no hay nada que leer en ello.'
|
|
1852
|
+
: verdict === 'recurring'
|
|
1853
|
+
? 'La misma decisión, sosteniéndose. La puerta sigue saltando y el motivo no ha cambiado.'
|
|
1854
|
+
: verdict === 'renewed-without-revisiting'
|
|
1855
|
+
? 'La fecha de caducidad se ha movido y el motivo no. Esa es la forma que toma una decisión que nadie está revisando — a veces es exactamente lo correcto, y merece decirse en voz alta igualmente.'
|
|
1856
|
+
: 'El motivo cambió entre usos. Alguien volvió a mirarlo.',
|
|
1857
|
+
waiverReasonNow: (reason) => `Motivo: ${reason}`,
|
|
1858
|
+
waiverReasonsChanged: (count) => `${count} motivos distintos dados en ese tiempo.`,
|
|
1859
|
+
waiverExpiriesMoved: (from, to, count) =>
|
|
1860
|
+
`La caducidad se movió ${count} ${count === 1 ? 'vez' : 'veces'}: ${from} → ${to}.`,
|
|
1861
|
+
waiverNoLongerConfigured: () =>
|
|
1862
|
+
'Ya no está en la configuración. La decisión se revirtió; el registro la conserva.',
|
|
1863
|
+
waiverNeverUsed: (gates) =>
|
|
1864
|
+
`Con waiver en la configuración y nunca usados por una ejecución registrada: ${gates}. O la puerta dejó de fallar — buenas noticias que nadie anotó — o el waiver nombra una situación que no ocurre. Ambos merecen borrarse.`,
|
|
1865
|
+
waiverUnreadable: (count, path) =>
|
|
1866
|
+
`${count} ${count === 1 ? 'línea' : 'líneas'} de ${path} no se pudieron leer y no cuentan arriba.`,
|
|
1867
|
+
waiverStartsHere: () =>
|
|
1868
|
+
'Nada anterior a ese día existe. Este registro empezó cuando empezó a registrarse, y no se reconstruyó ningún pasado a partir de la configuración actual.',
|
|
1840
1869
|
},
|
|
1841
1870
|
|
|
1842
1871
|
verify: {
|
|
@@ -1844,8 +1873,18 @@ ${bold('EJEMPLOS')}
|
|
|
1844
1873
|
'Apunta esto a un plan guardado y a un registro posterior: trazum verify plan.json --against usage.jsonl. Dice, por acción, si el cambio llegó, no llegó o no se puede saber — y nunca menos de esos tres.',
|
|
1845
1874
|
needsAgainst: () =>
|
|
1846
1875
|
'--against <nuevo.jsonl|dir> es obligatorio. Un plan solo puede verificarse contra un registro posterior; sin uno no hay nada a lo que someter la predicción.',
|
|
1847
|
-
badPlan: (path) =>
|
|
1848
|
-
`${path} no es un documento de plan que esta herramienta pueda verificar
|
|
1876
|
+
badPlan: (path, why) =>
|
|
1877
|
+
`${path} no es un documento de plan que esta herramienta pueda verificar: ${why}. Se esperaba el JSON que escribe "trazum plan -o".`,
|
|
1878
|
+
planRefusal: (why) =>
|
|
1879
|
+
why.kind === 'not-json'
|
|
1880
|
+
? 'no es JSON válido'
|
|
1881
|
+
: why.kind === 'not-an-object'
|
|
1882
|
+
? 'el nivel superior no es un objeto JSON'
|
|
1883
|
+
: why.kind === 'wrong-schema-version'
|
|
1884
|
+
? `schemaVersion es ${JSON.stringify(why.found)} en vez de 1`
|
|
1885
|
+
: why.kind === 'actions-not-a-list'
|
|
1886
|
+
? 'no hay un array actions'
|
|
1887
|
+
: `la acción ${why.index + 1} está mal formada (${why.because})`,
|
|
1849
1888
|
heading: (actions, planDate) =>
|
|
1850
1889
|
planDate === null
|
|
1851
1890
|
? `¿Funcionó? ${actions} acciones de un plan sin fecha, contra este registro`
|
package/src/i18n/types.ts
CHANGED
|
@@ -877,6 +877,14 @@ export interface CliMessages {
|
|
|
877
877
|
*/
|
|
878
878
|
waiveActive(gate: string, reason: string, until: string, daysLeft: string): string;
|
|
879
879
|
waiveExpired(gate: string, until: string, reason: string): string;
|
|
880
|
+
/**
|
|
881
|
+
* A use that could not be written down.
|
|
882
|
+
*
|
|
883
|
+
* Printed dim and never as an error: the gate's verdict is unaffected, and
|
|
884
|
+
* a read-only checkout must not turn a passing build red on account of
|
|
885
|
+
* bookkeeping.
|
|
886
|
+
*/
|
|
887
|
+
waiveNotRecorded(path: string, because: string): string;
|
|
880
888
|
/**
|
|
881
889
|
* `--markdown-summary`: the short form for a pull-request body or a weekly
|
|
882
890
|
* note. A view over the same report, never a different set of figures.
|
|
@@ -1254,6 +1262,19 @@ export interface CliMessages {
|
|
|
1254
1262
|
undated(name: string): string;
|
|
1255
1263
|
unrecognized(name: string): string;
|
|
1256
1264
|
footer(): string;
|
|
1265
|
+
/** The waiver record — what this team has been living with, and for how long. */
|
|
1266
|
+
waiverHeading(): string;
|
|
1267
|
+
waiverSince(day: string, uses: number): string;
|
|
1268
|
+
waiverNoneRecorded(): string;
|
|
1269
|
+
waiverHabit(gate: string, uses: number, days: number, firstDay: string, lastDay: string): string;
|
|
1270
|
+
waiverVerdict(verdict: string): string;
|
|
1271
|
+
waiverReasonNow(reason: string): string;
|
|
1272
|
+
waiverReasonsChanged(count: number): string;
|
|
1273
|
+
waiverExpiriesMoved(from: string, to: string, count: number): string;
|
|
1274
|
+
waiverNoLongerConfigured(): string;
|
|
1275
|
+
waiverNeverUsed(gates: string): string;
|
|
1276
|
+
waiverUnreadable(count: number, path: string): string;
|
|
1277
|
+
waiverStartsHere(): string;
|
|
1257
1278
|
};
|
|
1258
1279
|
|
|
1259
1280
|
/**
|
|
@@ -1269,7 +1290,22 @@ export interface CliMessages {
|
|
|
1269
1290
|
noTarget(): string;
|
|
1270
1291
|
needsAgainst(): string;
|
|
1271
1292
|
/** Not a plan document: wrong shape, wrong version, or not JSON at all. */
|
|
1272
|
-
|
|
1293
|
+
/**
|
|
1294
|
+
* `why` is the typed reason from `parsePlanDocument`, rendered so the
|
|
1295
|
+
* refusal names what is wrong rather than only that something is. A file
|
|
1296
|
+
* that is valid JSON and not a plan and a plan with one bad action are
|
|
1297
|
+
* different problems with different fixes.
|
|
1298
|
+
*/
|
|
1299
|
+
badPlan(path: string, why: string): string;
|
|
1300
|
+
/** The typed refusal from `parsePlanDocument`, in one clause. */
|
|
1301
|
+
planRefusal(
|
|
1302
|
+
why:
|
|
1303
|
+
| { kind: 'not-json' }
|
|
1304
|
+
| { kind: 'not-an-object' }
|
|
1305
|
+
| { kind: 'wrong-schema-version'; found: unknown }
|
|
1306
|
+
| { kind: 'actions-not-a-list' }
|
|
1307
|
+
| { kind: 'action-malformed'; index: number; because: string },
|
|
1308
|
+
): string;
|
|
1273
1309
|
heading(actions: string, planDate: string | null): string;
|
|
1274
1310
|
counts(arrived: string, notArrived: string, cannotTell: string): string;
|
|
1275
1311
|
/** Two price lists are two measurements; every dollar line inherits this. */
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,9 @@ import {
|
|
|
37
37
|
DEFAULT_USAGE,
|
|
38
38
|
detectFromSource,
|
|
39
39
|
matchLocale,
|
|
40
|
+
parsePlanDocument,
|
|
41
|
+
waiverDay,
|
|
42
|
+
waiverHistory,
|
|
40
43
|
proposeInit,
|
|
41
44
|
MIN_RATE_DAYS,
|
|
42
45
|
parseConfig,
|
|
@@ -125,6 +128,7 @@ import type {
|
|
|
125
128
|
} from '@trazum/core';
|
|
126
129
|
import type {
|
|
127
130
|
UsageProfileReport,
|
|
131
|
+
WaiverUse,
|
|
128
132
|
InitDecline,
|
|
129
133
|
InitJustification,
|
|
130
134
|
InitObservations,
|
|
@@ -167,6 +171,7 @@ import {
|
|
|
167
171
|
import type { Revision } from './git.js';
|
|
168
172
|
import { fetchProviderUsage, findCredential } from './connect.js';
|
|
169
173
|
import { STORE_DIR, appendRecords, readStore, rewriteStore } from './store-fs.js';
|
|
174
|
+
import { WAIVER_LOG, appendWaiverUse, readWaiverLog } from './waiver-log.js';
|
|
170
175
|
import { DEFAULT_PORT, buildServer, listen } from './serve.js';
|
|
171
176
|
import {
|
|
172
177
|
WATCH_STATE_VERSION,
|
|
@@ -3347,7 +3352,12 @@ async function commandConnect(
|
|
|
3347
3352
|
* same action planned twice — and no series, however long, becomes a
|
|
3348
3353
|
* forecast.
|
|
3349
3354
|
*/
|
|
3350
|
-
async function commandHistory(
|
|
3355
|
+
async function commandHistory(
|
|
3356
|
+
args: Args,
|
|
3357
|
+
config: TrazumConfig,
|
|
3358
|
+
pricing: PricingCatalogue,
|
|
3359
|
+
t: CliMessages,
|
|
3360
|
+
): Promise<void> {
|
|
3351
3361
|
/**
|
|
3352
3362
|
* `--store` builds the series from measured spend already on disk.
|
|
3353
3363
|
*
|
|
@@ -3451,7 +3461,23 @@ async function commandHistory(args: Args, pricing: PricingCatalogue, t: CliMessa
|
|
|
3451
3461
|
throw new Error(t.history.needsThree(String(history.periods.length)));
|
|
3452
3462
|
}
|
|
3453
3463
|
|
|
3454
|
-
|
|
3464
|
+
/**
|
|
3465
|
+
* The waiver record — closing the gap 1.40 named and could not fill.
|
|
3466
|
+
*
|
|
3467
|
+
* 1.40 wanted to say "this finding has been waived three times in a row" and
|
|
3468
|
+
* refused to, because the only material available was the config as it
|
|
3469
|
+
* stands, and a past reconstructed from a present is a guess wearing a
|
|
3470
|
+
* record's clothes. The material exists now: since 1.48 a waiver that
|
|
3471
|
+
* silences a gate writes down that it did, and this reads those lines back.
|
|
3472
|
+
*
|
|
3473
|
+
* Read from the working directory rather than from the reports directory:
|
|
3474
|
+
* the waiver record belongs to the repository whose gates fired, and the
|
|
3475
|
+
* stored reports may have come from anywhere.
|
|
3476
|
+
*/
|
|
3477
|
+
const waivers = await readWaiverLog('.');
|
|
3478
|
+
const waiverReport = waiverHistory(waivers.uses, config.waive ?? []);
|
|
3479
|
+
|
|
3480
|
+
const stamped = { ...history, unrecognizedFiles: unrecognized, waivers: waiverReport };
|
|
3455
3481
|
const n = (value: number): string => value.toLocaleString(t.numberLocale);
|
|
3456
3482
|
const day = (ms: number): string => new Date(ms).toISOString().slice(0, 10);
|
|
3457
3483
|
const pct = (value: number): string => `${(value * 100).toFixed(1)}%`;
|
|
@@ -3505,6 +3531,58 @@ async function commandHistory(args: Args, pricing: PricingCatalogue, t: CliMessa
|
|
|
3505
3531
|
for (const name of unrecognized) {
|
|
3506
3532
|
out.push(md ? `- ${t.history.unrecognized(name)}` : ` ${t.history.unrecognized(name)}`);
|
|
3507
3533
|
}
|
|
3534
|
+
/**
|
|
3535
|
+
* The waiver record, printed only once something has been recorded.
|
|
3536
|
+
*
|
|
3537
|
+
* Silent on a repository that has never waived anything, rather than a
|
|
3538
|
+
* heading over "0 uses" — an empty section teaches a reader to skip the
|
|
3539
|
+
* section, and this is the one they should not learn to skip.
|
|
3540
|
+
*/
|
|
3541
|
+
if (waivers.present) {
|
|
3542
|
+
out.push('');
|
|
3543
|
+
out.push(md ? `### ${t.history.waiverHeading()}` : t.history.waiverHeading());
|
|
3544
|
+
if (waiverReport.totalUses === 0) {
|
|
3545
|
+
out.push(md ? `- ${t.history.waiverNoneRecorded()}` : ` ${t.history.waiverNoneRecorded()}`);
|
|
3546
|
+
} else {
|
|
3547
|
+
const since = t.history.waiverSince(waiverReport.since ?? '', waiverReport.totalUses);
|
|
3548
|
+
out.push(md ? `- ${since}` : ` ${since}`);
|
|
3549
|
+
out.push(md ? `- _${t.history.waiverStartsHere()}_` : ` ${t.history.waiverStartsHere()}`);
|
|
3550
|
+
for (const habit of waiverReport.habits) {
|
|
3551
|
+
out.push('');
|
|
3552
|
+
const head = t.history.waiverHabit(
|
|
3553
|
+
habit.gate,
|
|
3554
|
+
habit.uses,
|
|
3555
|
+
habit.days,
|
|
3556
|
+
habit.firstDay,
|
|
3557
|
+
habit.lastDay,
|
|
3558
|
+
);
|
|
3559
|
+
out.push(md ? `- **${head}**` : ` ${head}`);
|
|
3560
|
+
const rows = [t.history.waiverVerdict(habit.verdict)];
|
|
3561
|
+
// The reason as it stands *now* — never read backwards onto an
|
|
3562
|
+
// older use, which is the same mistake the record exists to avoid.
|
|
3563
|
+
const latest = habit.reasons[habit.reasons.length - 1];
|
|
3564
|
+
if (latest !== undefined) rows.push(t.history.waiverReasonNow(latest));
|
|
3565
|
+
if (habit.reasons.length > 1) rows.push(t.history.waiverReasonsChanged(habit.reasons.length));
|
|
3566
|
+
const firstExpiry = habit.expiries[0];
|
|
3567
|
+
const lastExpiry = habit.expiries[habit.expiries.length - 1];
|
|
3568
|
+
if (habit.expiries.length > 1 && firstExpiry !== undefined && lastExpiry !== undefined) {
|
|
3569
|
+
rows.push(t.history.waiverExpiriesMoved(firstExpiry, lastExpiry, habit.expiries.length - 1));
|
|
3570
|
+
}
|
|
3571
|
+
if (!habit.stillConfigured) rows.push(t.history.waiverNoLongerConfigured());
|
|
3572
|
+
for (const row of rows) out.push(md ? ` - ${row}` : ` ${row}`);
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
if (waiverReport.neverUsed.length > 0) {
|
|
3576
|
+
out.push('');
|
|
3577
|
+
const dead = t.history.waiverNeverUsed(waiverReport.neverUsed.join(', '));
|
|
3578
|
+
out.push(md ? `- ${dead}` : ` ${dead}`);
|
|
3579
|
+
}
|
|
3580
|
+
if (waivers.unreadable.length > 0) {
|
|
3581
|
+
const bad = t.history.waiverUnreadable(waivers.unreadable.length, WAIVER_LOG);
|
|
3582
|
+
out.push(md ? `- ${bad}` : ` ${bad}`);
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3508
3586
|
if (fromStore) {
|
|
3509
3587
|
out.push('');
|
|
3510
3588
|
const note = t.history.storeNoLabels();
|
|
@@ -3546,17 +3624,21 @@ async function commandVerify(
|
|
|
3546
3624
|
const againstPath = stringFlag(args, 'against');
|
|
3547
3625
|
if (againstPath === undefined) throw new Error(t.verify.needsAgainst());
|
|
3548
3626
|
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3627
|
+
/**
|
|
3628
|
+
* One validator, shared with the browser since 1.47.
|
|
3629
|
+
*
|
|
3630
|
+
* The check here used to be `schemaVersion === 1 && Array.isArray(actions)`
|
|
3631
|
+
* and nothing more, which accepts a file whose actions are arbitrary
|
|
3632
|
+
* objects — `verifyPlan` would then read `label` off `undefined`, match it
|
|
3633
|
+
* against no slice, and report `cannot-tell: workload-vanished` for every
|
|
3634
|
+
* one. A verification of a document that was never a plan, rendered exactly
|
|
3635
|
+
* like a real one.
|
|
3636
|
+
*/
|
|
3637
|
+
const parsed = parsePlanDocument(await readFile(planPath, 'utf8'));
|
|
3638
|
+
if (!parsed.ok) {
|
|
3639
|
+
throw new Error(t.verify.badPlan(planPath, t.verify.planRefusal(parsed.why)));
|
|
3559
3640
|
}
|
|
3641
|
+
const plan = parsed.plan;
|
|
3560
3642
|
|
|
3561
3643
|
const GZ = LOG_EXTENSIONS.map((ext) => `${ext}.gz`);
|
|
3562
3644
|
const READABLE = [...LOG_EXTENSIONS, ...GZ];
|
|
@@ -4243,7 +4325,16 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4243
4325
|
* way, because a waived failure that vanished from the output would be a
|
|
4244
4326
|
* finding deleted with extra steps.
|
|
4245
4327
|
*/
|
|
4246
|
-
|
|
4328
|
+
/**
|
|
4329
|
+
* Uses recorded this run, flushed after the gates have finished.
|
|
4330
|
+
*
|
|
4331
|
+
* Collected rather than written inline because `waived` is synchronous and
|
|
4332
|
+
* called from seven places inside the gate pass. Writing from each of them
|
|
4333
|
+
* would mean seven awaits threaded through the exit-code logic — the one
|
|
4334
|
+
* part of this command where a mistake turns a red build green.
|
|
4335
|
+
*/
|
|
4336
|
+
const waiverUses: WaiverUse[] = [];
|
|
4337
|
+
const waived = (gate: string, measuredUsd: number | null = null, limitUsd: number | null = null): boolean => {
|
|
4247
4338
|
const found = waiverFor(gate);
|
|
4248
4339
|
if (found === null) return false;
|
|
4249
4340
|
if (found.expired) {
|
|
@@ -4259,6 +4350,29 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4259
4350
|
console.error(
|
|
4260
4351
|
c.yellow(t.profile.waiveActive(gate, found.entry.reason, found.entry.until, String(daysLeft))),
|
|
4261
4352
|
);
|
|
4353
|
+
/**
|
|
4354
|
+
* Recorded **when it silences something**, never when it is configured.
|
|
4355
|
+
*
|
|
4356
|
+
* A waiver nobody's build has ever hit is not a habit — it is dead config
|
|
4357
|
+
* — and the history reports the two apart. This is also the only honest
|
|
4358
|
+
* way to build the record 1.40 refused to invent: it starts today and
|
|
4359
|
+
* says so, rather than reconstructing a past from the present.
|
|
4360
|
+
*
|
|
4361
|
+
* The reason and the expiry are taken from the config **as it stands at
|
|
4362
|
+
* this moment**, because that is the decision that was actually in force.
|
|
4363
|
+
* Reading today's reason back onto last quarter's use is the same mistake
|
|
4364
|
+
* one layer down.
|
|
4365
|
+
*/
|
|
4366
|
+
waiverUses.push({
|
|
4367
|
+
schemaVersion: 1,
|
|
4368
|
+
day: waiverDay(new Date()),
|
|
4369
|
+
gate,
|
|
4370
|
+
reason: found.entry.reason,
|
|
4371
|
+
until: found.entry.until,
|
|
4372
|
+
commit: process.env.GITHUB_SHA ?? process.env.CI_COMMIT_SHA ?? null,
|
|
4373
|
+
measuredUsd,
|
|
4374
|
+
limitUsd,
|
|
4375
|
+
});
|
|
4262
4376
|
return true;
|
|
4263
4377
|
};
|
|
4264
4378
|
const applyGates = (): void => {
|
|
@@ -4307,7 +4421,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4307
4421
|
}
|
|
4308
4422
|
if (usd > limit) {
|
|
4309
4423
|
console.error(c.red(t.profile.labelBudgetFailed(label, formatUsd(usd), formatUsd(limit))));
|
|
4310
|
-
if (!waived(`byLabel:${label}
|
|
4424
|
+
if (!waived(`byLabel:${label}`, usd, limit)) process.exitCode = 1;
|
|
4311
4425
|
} else {
|
|
4312
4426
|
console.error(c.dim(t.profile.labelBudgetOk(label, formatUsd(usd), formatUsd(limit))));
|
|
4313
4427
|
}
|
|
@@ -4380,7 +4494,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4380
4494
|
* copy says so.
|
|
4381
4495
|
*/
|
|
4382
4496
|
explainFailure(report.total.totalUsd - maxUsd);
|
|
4383
|
-
if (!waived('maxUsd')) process.exitCode = 1;
|
|
4497
|
+
if (!waived('maxUsd', report.total.totalUsd, maxUsd)) process.exitCode = 1;
|
|
4384
4498
|
} else {
|
|
4385
4499
|
console.error(c.dim(t.profile.maxUsdOk(formatUsd(report.total.totalUsd), formatUsd(maxUsd))));
|
|
4386
4500
|
explainMargin(report.total.totalUsd, maxUsd);
|
|
@@ -4420,7 +4534,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4420
4534
|
process.exitCode = 1;
|
|
4421
4535
|
} else if (againstDelta > maxGrowth) {
|
|
4422
4536
|
console.error(c.red(t.profile.maxGrowthUsdFailed(formatSignedUsd(againstDelta), formatUsd(maxGrowth))));
|
|
4423
|
-
if (!waived('maxGrowthUsd')) process.exitCode = 1;
|
|
4537
|
+
if (!waived('maxGrowthUsd', againstDelta, maxGrowth)) process.exitCode = 1;
|
|
4424
4538
|
}
|
|
4425
4539
|
}
|
|
4426
4540
|
/**
|
|
@@ -4438,7 +4552,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4438
4552
|
console.error(
|
|
4439
4553
|
c.red(t.profile.maxCacheLossFailed(formatUsd(gateCache.deltaUsd), formatUsd(maxLoss))),
|
|
4440
4554
|
);
|
|
4441
|
-
if (!waived('maxCacheLossUsd')) process.exitCode = 1;
|
|
4555
|
+
if (!waived('maxCacheLossUsd', gateCache.deltaUsd, maxLoss)) process.exitCode = 1;
|
|
4442
4556
|
} else if (gateCache.worstCaseDeltaUsd > maxLoss) {
|
|
4443
4557
|
console.error(
|
|
4444
4558
|
c.red(
|
|
@@ -4449,7 +4563,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4449
4563
|
),
|
|
4450
4564
|
),
|
|
4451
4565
|
);
|
|
4452
|
-
if (!waived('maxCacheLossUsd')) process.exitCode = 1;
|
|
4566
|
+
if (!waived('maxCacheLossUsd', gateCache.worstCaseDeltaUsd, maxLoss)) process.exitCode = 1;
|
|
4453
4567
|
} else {
|
|
4454
4568
|
console.error(
|
|
4455
4569
|
c.dim(t.profile.maxCacheLossOk(formatUsd(Math.max(0, gateCache.worstCaseDeltaUsd)), formatUsd(maxLoss))),
|
|
@@ -4498,7 +4612,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4498
4612
|
c.red(`${t.profile.maxDayFailed(worst.day, formatUsd(worst.usd), formatUsd(maxDay))}${suspect}`),
|
|
4499
4613
|
);
|
|
4500
4614
|
explainFailure(worst.usd - maxDay, { namesLargest: true });
|
|
4501
|
-
if (!waived('maxDayUsd')) process.exitCode = 1;
|
|
4615
|
+
if (!waived('maxDayUsd', worst.usd, maxDay)) process.exitCode = 1;
|
|
4502
4616
|
} else {
|
|
4503
4617
|
console.error(c.dim(t.profile.maxDayOk(worst.day, formatUsd(worst.usd), formatUsd(maxDay))));
|
|
4504
4618
|
explainMargin(worst.usd, maxDay);
|
|
@@ -4540,7 +4654,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4540
4654
|
c.red(t.profile.maxSessionFailed(formatUsd(report.sessionSpend.maxUsd), formatUsd(maxSession), n(report.sessionSpend.sessions))),
|
|
4541
4655
|
);
|
|
4542
4656
|
explainFailure(report.sessionSpend.maxUsd - maxSession);
|
|
4543
|
-
if (!waived('maxSessionUsd')) process.exitCode = 1;
|
|
4657
|
+
if (!waived('maxSessionUsd', report.sessionSpend.maxUsd, maxSession)) process.exitCode = 1;
|
|
4544
4658
|
} else {
|
|
4545
4659
|
console.error(
|
|
4546
4660
|
c.dim(t.profile.maxSessionOk(formatUsd(report.sessionSpend.maxUsd), formatUsd(maxSession), n(report.sessionSpend.sessions))),
|
|
@@ -4574,6 +4688,26 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4574
4688
|
}
|
|
4575
4689
|
};
|
|
4576
4690
|
|
|
4691
|
+
/**
|
|
4692
|
+
* Writes down every waiver that silenced something this run.
|
|
4693
|
+
*
|
|
4694
|
+
* **A failure here never fails the build.** The gate's job is the exit code;
|
|
4695
|
+
* a read-only checkout or a full disk must not turn a passing build red on
|
|
4696
|
+
* account of bookkeeping. The problem is reported and the gate's own verdict
|
|
4697
|
+
* stands — which is also why this runs after `recordGates` rather than
|
|
4698
|
+
* inside it: nothing about the exit code depends on the write.
|
|
4699
|
+
*/
|
|
4700
|
+
const recordWaiverUses = async (): Promise<void> => {
|
|
4701
|
+
if (waiverUses.length === 0) return;
|
|
4702
|
+
for (const use of waiverUses) {
|
|
4703
|
+
const failed = await appendWaiverUse('.', use);
|
|
4704
|
+
if (failed !== null) {
|
|
4705
|
+
console.error(c.dim(t.profile.waiveNotRecorded(WAIVER_LOG, failed)));
|
|
4706
|
+
return;
|
|
4707
|
+
}
|
|
4708
|
+
}
|
|
4709
|
+
};
|
|
4710
|
+
|
|
4577
4711
|
/**
|
|
4578
4712
|
* The side files the caller asked for. Written on **both** output paths:
|
|
4579
4713
|
* under --json the human rendering returns early, and the first version of
|
|
@@ -4740,6 +4874,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4740
4874
|
),
|
|
4741
4875
|
);
|
|
4742
4876
|
recordGates();
|
|
4877
|
+
await recordWaiverUses();
|
|
4743
4878
|
await writeSideFiles();
|
|
4744
4879
|
return;
|
|
4745
4880
|
}
|
|
@@ -5894,6 +6029,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
5894
6029
|
reportProfileGaps(report, t, n, pricingStale);
|
|
5895
6030
|
|
|
5896
6031
|
recordGates();
|
|
6032
|
+
await recordWaiverUses();
|
|
5897
6033
|
|
|
5898
6034
|
await writeSideFiles();
|
|
5899
6035
|
}
|
|
@@ -7607,7 +7743,7 @@ async function main(): Promise<void> {
|
|
|
7607
7743
|
await commandVerify(args, pricing, t);
|
|
7608
7744
|
break;
|
|
7609
7745
|
case 'history':
|
|
7610
|
-
await commandHistory(args, pricing, t);
|
|
7746
|
+
await commandHistory(args, config, pricing, t);
|
|
7611
7747
|
break;
|
|
7612
7748
|
case 'connect':
|
|
7613
7749
|
await commandConnect(args, pricing, t);
|