@trazum/cli 1.47.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 +19 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +19 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +21 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +132 -12
- 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 +29 -0
- package/src/i18n/es.ts +29 -0
- package/src/i18n/types.ts +21 -0
- package/src/index.ts +142 -11
- 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: {
|
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: {
|
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
|
/**
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
detectFromSource,
|
|
39
39
|
matchLocale,
|
|
40
40
|
parsePlanDocument,
|
|
41
|
+
waiverDay,
|
|
42
|
+
waiverHistory,
|
|
41
43
|
proposeInit,
|
|
42
44
|
MIN_RATE_DAYS,
|
|
43
45
|
parseConfig,
|
|
@@ -126,6 +128,7 @@ import type {
|
|
|
126
128
|
} from '@trazum/core';
|
|
127
129
|
import type {
|
|
128
130
|
UsageProfileReport,
|
|
131
|
+
WaiverUse,
|
|
129
132
|
InitDecline,
|
|
130
133
|
InitJustification,
|
|
131
134
|
InitObservations,
|
|
@@ -168,6 +171,7 @@ import {
|
|
|
168
171
|
import type { Revision } from './git.js';
|
|
169
172
|
import { fetchProviderUsage, findCredential } from './connect.js';
|
|
170
173
|
import { STORE_DIR, appendRecords, readStore, rewriteStore } from './store-fs.js';
|
|
174
|
+
import { WAIVER_LOG, appendWaiverUse, readWaiverLog } from './waiver-log.js';
|
|
171
175
|
import { DEFAULT_PORT, buildServer, listen } from './serve.js';
|
|
172
176
|
import {
|
|
173
177
|
WATCH_STATE_VERSION,
|
|
@@ -3348,7 +3352,12 @@ async function commandConnect(
|
|
|
3348
3352
|
* same action planned twice — and no series, however long, becomes a
|
|
3349
3353
|
* forecast.
|
|
3350
3354
|
*/
|
|
3351
|
-
async function commandHistory(
|
|
3355
|
+
async function commandHistory(
|
|
3356
|
+
args: Args,
|
|
3357
|
+
config: TrazumConfig,
|
|
3358
|
+
pricing: PricingCatalogue,
|
|
3359
|
+
t: CliMessages,
|
|
3360
|
+
): Promise<void> {
|
|
3352
3361
|
/**
|
|
3353
3362
|
* `--store` builds the series from measured spend already on disk.
|
|
3354
3363
|
*
|
|
@@ -3452,7 +3461,23 @@ async function commandHistory(args: Args, pricing: PricingCatalogue, t: CliMessa
|
|
|
3452
3461
|
throw new Error(t.history.needsThree(String(history.periods.length)));
|
|
3453
3462
|
}
|
|
3454
3463
|
|
|
3455
|
-
|
|
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 };
|
|
3456
3481
|
const n = (value: number): string => value.toLocaleString(t.numberLocale);
|
|
3457
3482
|
const day = (ms: number): string => new Date(ms).toISOString().slice(0, 10);
|
|
3458
3483
|
const pct = (value: number): string => `${(value * 100).toFixed(1)}%`;
|
|
@@ -3506,6 +3531,58 @@ async function commandHistory(args: Args, pricing: PricingCatalogue, t: CliMessa
|
|
|
3506
3531
|
for (const name of unrecognized) {
|
|
3507
3532
|
out.push(md ? `- ${t.history.unrecognized(name)}` : ` ${t.history.unrecognized(name)}`);
|
|
3508
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
|
+
|
|
3509
3586
|
if (fromStore) {
|
|
3510
3587
|
out.push('');
|
|
3511
3588
|
const note = t.history.storeNoLabels();
|
|
@@ -4248,7 +4325,16 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4248
4325
|
* way, because a waived failure that vanished from the output would be a
|
|
4249
4326
|
* finding deleted with extra steps.
|
|
4250
4327
|
*/
|
|
4251
|
-
|
|
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 => {
|
|
4252
4338
|
const found = waiverFor(gate);
|
|
4253
4339
|
if (found === null) return false;
|
|
4254
4340
|
if (found.expired) {
|
|
@@ -4264,6 +4350,29 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4264
4350
|
console.error(
|
|
4265
4351
|
c.yellow(t.profile.waiveActive(gate, found.entry.reason, found.entry.until, String(daysLeft))),
|
|
4266
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
|
+
});
|
|
4267
4376
|
return true;
|
|
4268
4377
|
};
|
|
4269
4378
|
const applyGates = (): void => {
|
|
@@ -4312,7 +4421,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4312
4421
|
}
|
|
4313
4422
|
if (usd > limit) {
|
|
4314
4423
|
console.error(c.red(t.profile.labelBudgetFailed(label, formatUsd(usd), formatUsd(limit))));
|
|
4315
|
-
if (!waived(`byLabel:${label}
|
|
4424
|
+
if (!waived(`byLabel:${label}`, usd, limit)) process.exitCode = 1;
|
|
4316
4425
|
} else {
|
|
4317
4426
|
console.error(c.dim(t.profile.labelBudgetOk(label, formatUsd(usd), formatUsd(limit))));
|
|
4318
4427
|
}
|
|
@@ -4385,7 +4494,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4385
4494
|
* copy says so.
|
|
4386
4495
|
*/
|
|
4387
4496
|
explainFailure(report.total.totalUsd - maxUsd);
|
|
4388
|
-
if (!waived('maxUsd')) process.exitCode = 1;
|
|
4497
|
+
if (!waived('maxUsd', report.total.totalUsd, maxUsd)) process.exitCode = 1;
|
|
4389
4498
|
} else {
|
|
4390
4499
|
console.error(c.dim(t.profile.maxUsdOk(formatUsd(report.total.totalUsd), formatUsd(maxUsd))));
|
|
4391
4500
|
explainMargin(report.total.totalUsd, maxUsd);
|
|
@@ -4425,7 +4534,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4425
4534
|
process.exitCode = 1;
|
|
4426
4535
|
} else if (againstDelta > maxGrowth) {
|
|
4427
4536
|
console.error(c.red(t.profile.maxGrowthUsdFailed(formatSignedUsd(againstDelta), formatUsd(maxGrowth))));
|
|
4428
|
-
if (!waived('maxGrowthUsd')) process.exitCode = 1;
|
|
4537
|
+
if (!waived('maxGrowthUsd', againstDelta, maxGrowth)) process.exitCode = 1;
|
|
4429
4538
|
}
|
|
4430
4539
|
}
|
|
4431
4540
|
/**
|
|
@@ -4443,7 +4552,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4443
4552
|
console.error(
|
|
4444
4553
|
c.red(t.profile.maxCacheLossFailed(formatUsd(gateCache.deltaUsd), formatUsd(maxLoss))),
|
|
4445
4554
|
);
|
|
4446
|
-
if (!waived('maxCacheLossUsd')) process.exitCode = 1;
|
|
4555
|
+
if (!waived('maxCacheLossUsd', gateCache.deltaUsd, maxLoss)) process.exitCode = 1;
|
|
4447
4556
|
} else if (gateCache.worstCaseDeltaUsd > maxLoss) {
|
|
4448
4557
|
console.error(
|
|
4449
4558
|
c.red(
|
|
@@ -4454,7 +4563,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4454
4563
|
),
|
|
4455
4564
|
),
|
|
4456
4565
|
);
|
|
4457
|
-
if (!waived('maxCacheLossUsd')) process.exitCode = 1;
|
|
4566
|
+
if (!waived('maxCacheLossUsd', gateCache.worstCaseDeltaUsd, maxLoss)) process.exitCode = 1;
|
|
4458
4567
|
} else {
|
|
4459
4568
|
console.error(
|
|
4460
4569
|
c.dim(t.profile.maxCacheLossOk(formatUsd(Math.max(0, gateCache.worstCaseDeltaUsd)), formatUsd(maxLoss))),
|
|
@@ -4503,7 +4612,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4503
4612
|
c.red(`${t.profile.maxDayFailed(worst.day, formatUsd(worst.usd), formatUsd(maxDay))}${suspect}`),
|
|
4504
4613
|
);
|
|
4505
4614
|
explainFailure(worst.usd - maxDay, { namesLargest: true });
|
|
4506
|
-
if (!waived('maxDayUsd')) process.exitCode = 1;
|
|
4615
|
+
if (!waived('maxDayUsd', worst.usd, maxDay)) process.exitCode = 1;
|
|
4507
4616
|
} else {
|
|
4508
4617
|
console.error(c.dim(t.profile.maxDayOk(worst.day, formatUsd(worst.usd), formatUsd(maxDay))));
|
|
4509
4618
|
explainMargin(worst.usd, maxDay);
|
|
@@ -4545,7 +4654,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4545
4654
|
c.red(t.profile.maxSessionFailed(formatUsd(report.sessionSpend.maxUsd), formatUsd(maxSession), n(report.sessionSpend.sessions))),
|
|
4546
4655
|
);
|
|
4547
4656
|
explainFailure(report.sessionSpend.maxUsd - maxSession);
|
|
4548
|
-
if (!waived('maxSessionUsd')) process.exitCode = 1;
|
|
4657
|
+
if (!waived('maxSessionUsd', report.sessionSpend.maxUsd, maxSession)) process.exitCode = 1;
|
|
4549
4658
|
} else {
|
|
4550
4659
|
console.error(
|
|
4551
4660
|
c.dim(t.profile.maxSessionOk(formatUsd(report.sessionSpend.maxUsd), formatUsd(maxSession), n(report.sessionSpend.sessions))),
|
|
@@ -4579,6 +4688,26 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4579
4688
|
}
|
|
4580
4689
|
};
|
|
4581
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
|
+
|
|
4582
4711
|
/**
|
|
4583
4712
|
* The side files the caller asked for. Written on **both** output paths:
|
|
4584
4713
|
* under --json the human rendering returns early, and the first version of
|
|
@@ -4745,6 +4874,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
4745
4874
|
),
|
|
4746
4875
|
);
|
|
4747
4876
|
recordGates();
|
|
4877
|
+
await recordWaiverUses();
|
|
4748
4878
|
await writeSideFiles();
|
|
4749
4879
|
return;
|
|
4750
4880
|
}
|
|
@@ -5899,6 +6029,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
5899
6029
|
reportProfileGaps(report, t, n, pricingStale);
|
|
5900
6030
|
|
|
5901
6031
|
recordGates();
|
|
6032
|
+
await recordWaiverUses();
|
|
5902
6033
|
|
|
5903
6034
|
await writeSideFiles();
|
|
5904
6035
|
}
|
|
@@ -7612,7 +7743,7 @@ async function main(): Promise<void> {
|
|
|
7612
7743
|
await commandVerify(args, pricing, t);
|
|
7613
7744
|
break;
|
|
7614
7745
|
case 'history':
|
|
7615
|
-
await commandHistory(args, pricing, t);
|
|
7746
|
+
await commandHistory(args, config, pricing, t);
|
|
7616
7747
|
break;
|
|
7617
7748
|
case 'connect':
|
|
7618
7749
|
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
|
+
}
|