@trazum/core 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/budget.d.ts +148 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +200 -0
- package/dist/budget.js.map +1 -0
- package/dist/config-schema.d.ts +18 -1
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config-schema.js +4 -1
- package/dist/config-schema.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/waivers.d.ts +133 -0
- package/dist/waivers.d.ts.map +1 -0
- package/dist/waivers.js +114 -0
- package/dist/waivers.js.map +1 -0
- package/package.json +1 -1
- package/src/budget.ts +321 -0
- package/src/config-schema.ts +21 -1
- package/src/index.ts +13 -0
- package/src/waivers.ts +209 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a team has decided to live with, and how long they have been deciding it.
|
|
3
|
+
*
|
|
4
|
+
* 1.40 named this gap and could not fill it: *no document stores past waivers,
|
|
5
|
+
* and a history invented from the current config would be a guess presented as
|
|
6
|
+
* a record*. That was the right refusal — a config says what is waived **now**,
|
|
7
|
+
* and nothing in it says whether the same finding was waived last quarter under
|
|
8
|
+
* a different reason, or whether the expiry has been pushed forward four times
|
|
9
|
+
* by four people who each assumed somebody else had looked.
|
|
10
|
+
*
|
|
11
|
+
* It is fixable by **recording**, not by inferring. A waiver silences a gate;
|
|
12
|
+
* the moment it does, that use is a fact with a date on it, and this module
|
|
13
|
+
* reads those facts back.
|
|
14
|
+
*
|
|
15
|
+
* Three rules hold the whole thing up:
|
|
16
|
+
*
|
|
17
|
+
* **Nothing is back-filled.** The history starts the day the recording did,
|
|
18
|
+
* and `since` says which day that was. Reconstructing a past from the present
|
|
19
|
+
* config is exactly what 1.40 refused, and it would be worse here than
|
|
20
|
+
* nowhere: a fabricated "waived four times" is an accusation.
|
|
21
|
+
*
|
|
22
|
+
* **A use is recorded when the waiver silences something, not when it is
|
|
23
|
+
* written.** A waiver nobody's build has ever hit is not a habit — it is dead
|
|
24
|
+
* config, and the report says which it is rather than folding the two together.
|
|
25
|
+
*
|
|
26
|
+
* **The verdict describes the record, never the team.** "Renewed without being
|
|
27
|
+
* revisited" is a statement about dates and reasons in a file. Whether that was
|
|
28
|
+
* the right call is a conversation this tool does not get to have.
|
|
29
|
+
*/
|
|
30
|
+
import type { WaiveEntry } from './config-schema.js';
|
|
31
|
+
/** One occasion on which a waiver silenced a gate. */
|
|
32
|
+
export interface WaiverUse {
|
|
33
|
+
schemaVersion: 1;
|
|
34
|
+
/** The UTC day it happened, `YYYY-MM-DD`. */
|
|
35
|
+
day: string;
|
|
36
|
+
/** The gate silenced — a `WAIVABLE_GATES` entry or `byLabel:<label>`. */
|
|
37
|
+
gate: string;
|
|
38
|
+
/** The reason the config gave **at that moment**, not today's. */
|
|
39
|
+
reason: string;
|
|
40
|
+
/** The expiry the waiver carried at that moment. */
|
|
41
|
+
until: string;
|
|
42
|
+
/**
|
|
43
|
+
* The commit that carried it, when the run could see one.
|
|
44
|
+
*
|
|
45
|
+
* Null is common and honest: a waiver used outside a repository, or in a CI
|
|
46
|
+
* job that did not export the SHA. Null never becomes "unknown commit" in a
|
|
47
|
+
* count — a use with no commit is still a use.
|
|
48
|
+
*/
|
|
49
|
+
commit: string | null;
|
|
50
|
+
/** What the gate measured, so a recorded use is checkable rather than asserted. */
|
|
51
|
+
measuredUsd: number | null;
|
|
52
|
+
limitUsd: number | null;
|
|
53
|
+
}
|
|
54
|
+
export type WaiverVerdict =
|
|
55
|
+
/** One recorded use. Nothing to say about it yet. */
|
|
56
|
+
'used-once'
|
|
57
|
+
/** Used repeatedly under one unchanging reason and one unchanging expiry. */
|
|
58
|
+
| 'recurring'
|
|
59
|
+
/**
|
|
60
|
+
* The expiry moved while the reason stayed the same.
|
|
61
|
+
*
|
|
62
|
+
* The shape a decision takes when nobody is revisiting it: the same sentence
|
|
63
|
+
* carried forward past its own deadline. Named, and never called wrong —
|
|
64
|
+
* plenty of real constraints outlive their first estimate.
|
|
65
|
+
*/
|
|
66
|
+
| 'renewed-without-revisiting'
|
|
67
|
+
/**
|
|
68
|
+
* The reason changed between uses.
|
|
69
|
+
*
|
|
70
|
+
* Somebody looked. Worth telling apart from the case above, because it is
|
|
71
|
+
* the opposite behaviour and would otherwise be counted as the same habit.
|
|
72
|
+
*/
|
|
73
|
+
| 'reason-changed';
|
|
74
|
+
export interface WaiverHabit {
|
|
75
|
+
gate: string;
|
|
76
|
+
/** How many recorded uses, never an estimate. */
|
|
77
|
+
uses: number;
|
|
78
|
+
firstDay: string;
|
|
79
|
+
lastDay: string;
|
|
80
|
+
/** Distinct days it fired on — a gate hit twice on one day is one day. */
|
|
81
|
+
days: number;
|
|
82
|
+
/** Every distinct reason given, oldest first. */
|
|
83
|
+
reasons: string[];
|
|
84
|
+
/** Every distinct expiry carried, oldest first. More than one means it moved. */
|
|
85
|
+
expiries: string[];
|
|
86
|
+
verdict: WaiverVerdict;
|
|
87
|
+
/** Whether this gate is still waived in the config as it stands today. */
|
|
88
|
+
stillConfigured: boolean;
|
|
89
|
+
}
|
|
90
|
+
export interface WaiverHistory {
|
|
91
|
+
schemaVersion: 1;
|
|
92
|
+
/**
|
|
93
|
+
* The first day any use was recorded, or null when none has been.
|
|
94
|
+
*
|
|
95
|
+
* Rendered as "the history starts here", because a reader looking at two
|
|
96
|
+
* uses needs to know whether that is two in the project's life or two since
|
|
97
|
+
* Tuesday.
|
|
98
|
+
*/
|
|
99
|
+
since: string | null;
|
|
100
|
+
/** Most-used first — the order somebody would read in. */
|
|
101
|
+
habits: WaiverHabit[];
|
|
102
|
+
/**
|
|
103
|
+
* Waivers in the config today that no recorded run has ever hit.
|
|
104
|
+
*
|
|
105
|
+
* Dead config, not habit. Either the gate stopped failing — which is good
|
|
106
|
+
* news nobody wrote down — or the waiver names a situation that never
|
|
107
|
+
* arises. Both are worth deleting; neither is a team living with a finding.
|
|
108
|
+
*/
|
|
109
|
+
neverUsed: string[];
|
|
110
|
+
/** Total recorded uses across every gate. */
|
|
111
|
+
totalUses: number;
|
|
112
|
+
}
|
|
113
|
+
/** `YYYY-MM-DD` for a moment, UTC — the same day boundary the store uses. */
|
|
114
|
+
export declare function waiverDay(at: Date): string;
|
|
115
|
+
/**
|
|
116
|
+
* Whether a value is a use record this module can read.
|
|
117
|
+
*
|
|
118
|
+
* Same posture as the plan validator: a line that is not a record is skipped
|
|
119
|
+
* and counted by the caller, never coerced into one. A malformed line in an
|
|
120
|
+
* append-only file is a fact about the file, and a reader that silently
|
|
121
|
+
* repairs it produces a history that is wrong by an unknown amount.
|
|
122
|
+
*/
|
|
123
|
+
export declare function isWaiverUse(value: unknown): value is WaiverUse;
|
|
124
|
+
/**
|
|
125
|
+
* The habits, from the record and the config as it stands.
|
|
126
|
+
*
|
|
127
|
+
* `configured` is used for exactly one thing — telling a gate somebody is
|
|
128
|
+
* still waiving from one they stopped — and never to invent a use. A waiver in
|
|
129
|
+
* the config with no recorded use appears in `neverUsed`, with a count of
|
|
130
|
+
* nothing, which is the honest shape.
|
|
131
|
+
*/
|
|
132
|
+
export declare function waiverHistory(uses: readonly WaiverUse[], configured?: readonly WaiveEntry[]): WaiverHistory;
|
|
133
|
+
//# sourceMappingURL=waivers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"waivers.d.ts","sourceRoot":"","sources":["../src/waivers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,sDAAsD;AACtD,MAAM,WAAW,SAAS;IACxB,aAAa,EAAE,CAAC,CAAC;IACjB,6CAA6C;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,mFAAmF;IACnF,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,MAAM,aAAa;AACvB,qDAAqD;AACnD,WAAW;AACb,6EAA6E;GAC3E,WAAW;AACb;;;;;;GAMG;GACD,4BAA4B;AAC9B;;;;;GAKG;GACD,gBAAgB,CAAC;AAErB,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,iFAAiF;IACjF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,aAAa,CAAC;IACvB,0EAA0E;IAC1E,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,CAAC,CAAC;IACjB;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,0DAA0D;IAC1D,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,EAAE,EAAE,IAAI,GAAG,MAAM,CAE1C;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAU9D;AAQD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,SAAS,SAAS,EAAE,EAC1B,UAAU,GAAE,SAAS,UAAU,EAAO,GACrC,aAAa,CAgDf"}
|
package/dist/waivers.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a team has decided to live with, and how long they have been deciding it.
|
|
3
|
+
*
|
|
4
|
+
* 1.40 named this gap and could not fill it: *no document stores past waivers,
|
|
5
|
+
* and a history invented from the current config would be a guess presented as
|
|
6
|
+
* a record*. That was the right refusal — a config says what is waived **now**,
|
|
7
|
+
* and nothing in it says whether the same finding was waived last quarter under
|
|
8
|
+
* a different reason, or whether the expiry has been pushed forward four times
|
|
9
|
+
* by four people who each assumed somebody else had looked.
|
|
10
|
+
*
|
|
11
|
+
* It is fixable by **recording**, not by inferring. A waiver silences a gate;
|
|
12
|
+
* the moment it does, that use is a fact with a date on it, and this module
|
|
13
|
+
* reads those facts back.
|
|
14
|
+
*
|
|
15
|
+
* Three rules hold the whole thing up:
|
|
16
|
+
*
|
|
17
|
+
* **Nothing is back-filled.** The history starts the day the recording did,
|
|
18
|
+
* and `since` says which day that was. Reconstructing a past from the present
|
|
19
|
+
* config is exactly what 1.40 refused, and it would be worse here than
|
|
20
|
+
* nowhere: a fabricated "waived four times" is an accusation.
|
|
21
|
+
*
|
|
22
|
+
* **A use is recorded when the waiver silences something, not when it is
|
|
23
|
+
* written.** A waiver nobody's build has ever hit is not a habit — it is dead
|
|
24
|
+
* config, and the report says which it is rather than folding the two together.
|
|
25
|
+
*
|
|
26
|
+
* **The verdict describes the record, never the team.** "Renewed without being
|
|
27
|
+
* revisited" is a statement about dates and reasons in a file. Whether that was
|
|
28
|
+
* the right call is a conversation this tool does not get to have.
|
|
29
|
+
*/
|
|
30
|
+
/** `YYYY-MM-DD` for a moment, UTC — the same day boundary the store uses. */
|
|
31
|
+
export function waiverDay(at) {
|
|
32
|
+
return at.toISOString().slice(0, 10);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether a value is a use record this module can read.
|
|
36
|
+
*
|
|
37
|
+
* Same posture as the plan validator: a line that is not a record is skipped
|
|
38
|
+
* and counted by the caller, never coerced into one. A malformed line in an
|
|
39
|
+
* append-only file is a fact about the file, and a reader that silently
|
|
40
|
+
* repairs it produces a history that is wrong by an unknown amount.
|
|
41
|
+
*/
|
|
42
|
+
export function isWaiverUse(value) {
|
|
43
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
44
|
+
return false;
|
|
45
|
+
const record = value;
|
|
46
|
+
return (record.schemaVersion === 1 &&
|
|
47
|
+
typeof record.day === 'string' &&
|
|
48
|
+
typeof record.gate === 'string' &&
|
|
49
|
+
typeof record.reason === 'string' &&
|
|
50
|
+
typeof record.until === 'string');
|
|
51
|
+
}
|
|
52
|
+
function verdictFor(reasons, expiries, uses) {
|
|
53
|
+
if (uses <= 1)
|
|
54
|
+
return 'used-once';
|
|
55
|
+
if (reasons.length > 1)
|
|
56
|
+
return 'reason-changed';
|
|
57
|
+
return expiries.length > 1 ? 'renewed-without-revisiting' : 'recurring';
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The habits, from the record and the config as it stands.
|
|
61
|
+
*
|
|
62
|
+
* `configured` is used for exactly one thing — telling a gate somebody is
|
|
63
|
+
* still waiving from one they stopped — and never to invent a use. A waiver in
|
|
64
|
+
* the config with no recorded use appears in `neverUsed`, with a count of
|
|
65
|
+
* nothing, which is the honest shape.
|
|
66
|
+
*/
|
|
67
|
+
export function waiverHistory(uses, configured = []) {
|
|
68
|
+
const byGate = new Map();
|
|
69
|
+
for (const use of uses) {
|
|
70
|
+
const list = byGate.get(use.gate);
|
|
71
|
+
if (list === undefined)
|
|
72
|
+
byGate.set(use.gate, [use]);
|
|
73
|
+
else
|
|
74
|
+
list.push(use);
|
|
75
|
+
}
|
|
76
|
+
const configuredGates = new Set(configured.map((entry) => entry.gate));
|
|
77
|
+
const habits = [];
|
|
78
|
+
for (const [gate, list] of byGate) {
|
|
79
|
+
const sorted = [...list].sort((a, b) => a.day.localeCompare(b.day));
|
|
80
|
+
// Distinct values in the order they were first seen, so "the reason
|
|
81
|
+
// changed" reads chronologically rather than alphabetically.
|
|
82
|
+
const reasons = [...new Set(sorted.map((u) => u.reason))];
|
|
83
|
+
const expiries = [...new Set(sorted.map((u) => u.until))];
|
|
84
|
+
const days = new Set(sorted.map((u) => u.day)).size;
|
|
85
|
+
const first = sorted[0];
|
|
86
|
+
const last = sorted[sorted.length - 1];
|
|
87
|
+
if (first === undefined || last === undefined)
|
|
88
|
+
continue;
|
|
89
|
+
habits.push({
|
|
90
|
+
gate,
|
|
91
|
+
uses: sorted.length,
|
|
92
|
+
firstDay: first.day,
|
|
93
|
+
lastDay: last.day,
|
|
94
|
+
days,
|
|
95
|
+
reasons,
|
|
96
|
+
expiries,
|
|
97
|
+
verdict: verdictFor(reasons, expiries, sorted.length),
|
|
98
|
+
stillConfigured: configuredGates.has(gate),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
habits.sort((a, b) => b.uses - a.uses || a.gate.localeCompare(b.gate));
|
|
102
|
+
const allDays = uses.map((use) => use.day).sort((a, b) => a.localeCompare(b));
|
|
103
|
+
return {
|
|
104
|
+
schemaVersion: 1,
|
|
105
|
+
since: allDays[0] ?? null,
|
|
106
|
+
habits,
|
|
107
|
+
neverUsed: configured
|
|
108
|
+
.filter((entry) => !byGate.has(entry.gate))
|
|
109
|
+
.map((entry) => entry.gate)
|
|
110
|
+
.sort(),
|
|
111
|
+
totalUses: uses.length,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=waivers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"waivers.js","sourceRoot":"","sources":["../src/waivers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AA0FH,6EAA6E;AAC7E,MAAM,UAAU,SAAS,CAAC,EAAQ;IAChC,OAAO,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACtF,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,OAAO,CACL,MAAM,CAAC,aAAa,KAAK,CAAC;QAC1B,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ;QAC9B,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ;QAC/B,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ;QACjC,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,CACjC,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,OAAiB,EAAE,QAAkB,EAAE,IAAY;IACrE,IAAI,IAAI,IAAI,CAAC;QAAE,OAAO,WAAW,CAAC;IAClC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,gBAAgB,CAAC;IAChD,OAAO,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,4BAA4B,CAAC,CAAC,CAAC,WAAW,CAAC;AAC1E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAC3B,IAA0B,EAC1B,UAAU,GAA0B,EAAE;IAEtC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC9C,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,IAAI,KAAK,SAAS;YAAE,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;;YAC/C,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC;IAED,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IACvE,MAAM,MAAM,GAAkB,EAAE,CAAC;IAEjC,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,EAAE,CAAC;QAClC,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACpE,oEAAoE;QACpE,6DAA6D;QAC7D,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC1D,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC1D,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;QACpD,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACxB,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACvC,IAAI,KAAK,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS;YAAE,SAAS;QACxD,MAAM,CAAC,IAAI,CAAC;YACV,IAAI;YACJ,IAAI,EAAE,MAAM,CAAC,MAAM;YACnB,QAAQ,EAAE,KAAK,CAAC,GAAG;YACnB,OAAO,EAAE,IAAI,CAAC,GAAG;YACjB,IAAI;YACJ,OAAO;YACP,QAAQ;YACR,OAAO,EAAE,UAAU,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC;YACrD,eAAe,EAAE,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC;SAC3C,CAAC,CAAC;IACL,CAAC;IAED,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAEvE,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IAE9E,OAAO;QACL,aAAa,EAAE,CAAC;QAChB,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI;QACzB,MAAM;QACN,SAAS,EAAE,UAAU;aAClB,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;aAC1C,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;aAC1B,IAAI,EAAE;QACT,SAAS,EAAE,IAAI,CAAC,MAAM;KACvB,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trazum/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.49.0",
|
|
4
4
|
"description": "Trazum core: priced advisories for LLM prompts (caching, model tier, batching, schemas), plus deterministic trimming, token counting and pricing.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "David Mu\u00f1oz Rey",
|
package/src/budget.ts
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One measured number, wherever it is asked for.
|
|
3
|
+
*
|
|
4
|
+
* By 1.48 there are four ways to ask Trazum about money — a gate in CI, the
|
|
5
|
+
* terminal, the local endpoint an agent consults, the browser — and no
|
|
6
|
+
* guarantee any two of them agree about how much of a budget is left. Each
|
|
7
|
+
* computed its own answer from whatever it happened to be holding: a log, a
|
|
8
|
+
* store, a request body. Four right answers to four slightly different
|
|
9
|
+
* questions is how a CI failure and an agent's refusal come to disagree in
|
|
10
|
+
* front of somebody.
|
|
11
|
+
*
|
|
12
|
+
* This is that number. A budget becomes a **position**: a limit, a period, the
|
|
13
|
+
* measured spend inside it, and — the part that makes it honest — how much of
|
|
14
|
+
* that period was measured at all.
|
|
15
|
+
*
|
|
16
|
+
* **Nothing here forecasts.** "Sixty-one per cent of the budget, consumed over
|
|
17
|
+
* eleven of thirty days" is a measurement. "You will run out on the 24th" is a
|
|
18
|
+
* prediction, and this product has refused those since 1.27 at every scale it
|
|
19
|
+
* operates on. The burn-down below compares two shares that both already
|
|
20
|
+
* happened, and names the shape; it never produces a date.
|
|
21
|
+
*
|
|
22
|
+
* **A period nobody measured is not a period under budget.** The rule
|
|
23
|
+
* `fleetBudgetMissing` established for services in 1.37, applied to time: days
|
|
24
|
+
* inside the period with no measurement are counted and named, and a position
|
|
25
|
+
* standing on three measured days out of thirty says so rather than reporting
|
|
26
|
+
* a comfortable ninety per cent remaining.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { SpendConfig } from './config-schema.js';
|
|
30
|
+
import type { StoreRecord } from './store.js';
|
|
31
|
+
import { effectivePricing } from './pricing.js';
|
|
32
|
+
import type { PricingCatalogue } from './pricing.js';
|
|
33
|
+
|
|
34
|
+
const DAY_MS = 86_400_000;
|
|
35
|
+
|
|
36
|
+
/** The window a budget is spent over. Calendar months, UTC, like everything else. */
|
|
37
|
+
export interface BudgetPeriod {
|
|
38
|
+
kind: 'month';
|
|
39
|
+
/** `YYYY-MM`, UTC. */
|
|
40
|
+
id: string;
|
|
41
|
+
fromMs: number;
|
|
42
|
+
/** Half-open: the first instant of the next month. */
|
|
43
|
+
toMs: number;
|
|
44
|
+
days: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type BudgetScope =
|
|
48
|
+
| { kind: 'total' }
|
|
49
|
+
| { kind: 'label'; label: string }
|
|
50
|
+
| { kind: 'source'; source: string };
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* How much of the period the measurement actually covers.
|
|
54
|
+
*
|
|
55
|
+
* Three values rather than a percentage, because the three lead to different
|
|
56
|
+
* decisions: act on it, act on it knowing it is a floor, or go and find out
|
|
57
|
+
* why nothing was measured.
|
|
58
|
+
*/
|
|
59
|
+
export type BudgetCoverage = 'complete' | 'partial' | 'none';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The shape of the burn, named — never a date.
|
|
63
|
+
*
|
|
64
|
+
* A comparison of two shares that have both already happened: how much of the
|
|
65
|
+
* budget is gone against how much of the period is gone. `ahead` means the
|
|
66
|
+
* money is going faster than the calendar, which is a fact about the past
|
|
67
|
+
* eleven days and not a claim about the next nineteen.
|
|
68
|
+
*/
|
|
69
|
+
export type BurnShape = 'ahead' | 'on-pace' | 'behind' | 'cannot-tell';
|
|
70
|
+
|
|
71
|
+
export interface BurnDown {
|
|
72
|
+
/** Share of the limit consumed, 0-1. Null when the limit is zero. */
|
|
73
|
+
consumedShare: number | null;
|
|
74
|
+
/** Share of the period elapsed at the instant this was computed, 0-1. */
|
|
75
|
+
elapsedShare: number;
|
|
76
|
+
shape: BurnShape;
|
|
77
|
+
/**
|
|
78
|
+
* Deliberately absent: any field naming a date the budget runs out.
|
|
79
|
+
*
|
|
80
|
+
* Stated here rather than left to be noticed, because it is the single most
|
|
81
|
+
* requested number this module will ever be asked for, and every future
|
|
82
|
+
* reader of this file will be tempted to add it. It cannot be measured; it
|
|
83
|
+
* can only be projected from a rate that the log has no reason to keep.
|
|
84
|
+
*/
|
|
85
|
+
readonly forecast?: never;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface BudgetStanding {
|
|
89
|
+
schemaVersion: 1;
|
|
90
|
+
scope: BudgetScope;
|
|
91
|
+
limitUsd: number;
|
|
92
|
+
period: BudgetPeriod;
|
|
93
|
+
/** Measured spend inside the period. Never an estimate — see `provenance`. */
|
|
94
|
+
consumedUsd: number;
|
|
95
|
+
remainingUsd: number;
|
|
96
|
+
provenance: 'measured';
|
|
97
|
+
/** Distinct UTC days inside the period that carry any measurement. */
|
|
98
|
+
measuredDays: number;
|
|
99
|
+
/** Days of the period that have already elapsed at the instant asked. */
|
|
100
|
+
elapsedDays: number;
|
|
101
|
+
/**
|
|
102
|
+
* Elapsed days with no measurement at all, oldest first, capped for
|
|
103
|
+
* rendering. A day missing from a series is the thing a total cannot show.
|
|
104
|
+
*/
|
|
105
|
+
unmeasuredDays: string[];
|
|
106
|
+
coverage: BudgetCoverage;
|
|
107
|
+
burn: BurnDown;
|
|
108
|
+
/**
|
|
109
|
+
* `cannot-tell` when nothing in the period was measured. A budget with no
|
|
110
|
+
* measurement behind it is not a budget under control, and reporting
|
|
111
|
+
* `within` would be the flattering direction — the one this repository
|
|
112
|
+
* refuses everywhere it can occur.
|
|
113
|
+
*/
|
|
114
|
+
verdict: 'within' | 'over' | 'cannot-tell';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface BudgetReport {
|
|
118
|
+
schemaVersion: 1;
|
|
119
|
+
period: BudgetPeriod;
|
|
120
|
+
positions: BudgetStanding[];
|
|
121
|
+
/**
|
|
122
|
+
* Budgets configured with a scope nothing measured touches.
|
|
123
|
+
*
|
|
124
|
+
* Not a position of zero: a label that has no records may have been renamed,
|
|
125
|
+
* or may simply not have run. Both are worth knowing and neither is "under
|
|
126
|
+
* budget".
|
|
127
|
+
*/
|
|
128
|
+
unmeasuredScopes: BudgetScope[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The UTC month containing `at`, as a period. */
|
|
132
|
+
export function monthOf(at: Date): BudgetPeriod {
|
|
133
|
+
const year = at.getUTCFullYear();
|
|
134
|
+
const month = at.getUTCMonth();
|
|
135
|
+
const fromMs = Date.UTC(year, month, 1);
|
|
136
|
+
const toMs = Date.UTC(year, month + 1, 1);
|
|
137
|
+
return {
|
|
138
|
+
kind: 'month',
|
|
139
|
+
id: `${year}-${String(month + 1).padStart(2, '0')}`,
|
|
140
|
+
fromMs,
|
|
141
|
+
toMs,
|
|
142
|
+
days: Math.round((toMs - fromMs) / DAY_MS),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** What a store record's tokens cost, at the catalogue's rates for its window. */
|
|
147
|
+
function priceOf(record: StoreRecord, catalogue: PricingCatalogue): number | null {
|
|
148
|
+
const model = catalogue.byId.get(record.model);
|
|
149
|
+
if (model === undefined) return null;
|
|
150
|
+
const rates = effectivePricing(model, new Date(record.fromMs));
|
|
151
|
+
const write5m = record.write5m * rates.inputPerMTok * 1.25;
|
|
152
|
+
const write1h = record.write1h * rates.inputPerMTok * 2;
|
|
153
|
+
return (
|
|
154
|
+
(record.input * rates.inputPerMTok +
|
|
155
|
+
record.cacheRead * rates.inputPerMTok * 0.1 +
|
|
156
|
+
write5m +
|
|
157
|
+
write1h +
|
|
158
|
+
record.output * rates.outputPerMTok) /
|
|
159
|
+
1_000_000
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The share of a record's window that falls inside the period.
|
|
165
|
+
*
|
|
166
|
+
* A store record covers a bucket, and a bucket can straddle a month boundary.
|
|
167
|
+
* Counting it wholly in or wholly out would move real money between months by
|
|
168
|
+
* up to a day; apportioning by overlap is the only answer that keeps two
|
|
169
|
+
* adjacent months summing to the same total as the pair.
|
|
170
|
+
*
|
|
171
|
+
* This is arithmetic about a window, not an estimate of anything: the record
|
|
172
|
+
* says what the window cost, and the overlap is exact.
|
|
173
|
+
*/
|
|
174
|
+
function overlapShare(record: StoreRecord, period: BudgetPeriod): number {
|
|
175
|
+
const span = record.toMs - record.fromMs;
|
|
176
|
+
if (span <= 0) return record.fromMs >= period.fromMs && record.fromMs < period.toMs ? 1 : 0;
|
|
177
|
+
const from = Math.max(record.fromMs, period.fromMs);
|
|
178
|
+
const to = Math.min(record.toMs, period.toMs);
|
|
179
|
+
return to <= from ? 0 : (to - from) / span;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Every UTC day a record touches inside the period, as `YYYY-MM-DD`. */
|
|
183
|
+
function daysTouched(record: StoreRecord, period: BudgetPeriod): string[] {
|
|
184
|
+
const from = Math.max(record.fromMs, period.fromMs);
|
|
185
|
+
const to = Math.min(Math.max(record.toMs, record.fromMs + 1), period.toMs);
|
|
186
|
+
if (to <= from) return [];
|
|
187
|
+
const days: string[] = [];
|
|
188
|
+
for (let ms = Math.floor(from / DAY_MS) * DAY_MS; ms < to; ms += DAY_MS) {
|
|
189
|
+
days.push(new Date(ms).toISOString().slice(0, 10));
|
|
190
|
+
}
|
|
191
|
+
return days;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function shapeOf(consumedShare: number | null, elapsedShare: number, coverage: BudgetCoverage): BurnShape {
|
|
195
|
+
// Nothing measured, or nothing to measure against: the comparison has no
|
|
196
|
+
// meaning, and inventing one from an elapsed share alone would be a shape
|
|
197
|
+
// drawn from the calendar rather than from the bill.
|
|
198
|
+
if (coverage === 'none' || consumedShare === null) return 'cannot-tell';
|
|
199
|
+
|
|
200
|
+
// A five-point band, so a budget tracking the calendar to within a rounding
|
|
201
|
+
// error is not reported as drifting every time somebody looks.
|
|
202
|
+
const drift = consumedShare - elapsedShare;
|
|
203
|
+
const ahead = drift > 0.05;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* **A floor can prove `ahead` and can never prove `behind`.**
|
|
207
|
+
*
|
|
208
|
+
* Partial coverage means the consumed figure is a floor on the period: the
|
|
209
|
+
* unmeasured days spent *something*, and nobody knows how much. A floor that
|
|
210
|
+
* has already outrun the calendar is unarguably ahead — the real figure is
|
|
211
|
+
* higher still. A floor that looks comfortable proves nothing at all, and
|
|
212
|
+
* reporting it as `behind` would turn missing measurement into good news,
|
|
213
|
+
* which is the flattering direction this repository refuses everywhere.
|
|
214
|
+
*
|
|
215
|
+
* The first version of this returned `behind` for three measured days out of
|
|
216
|
+
* twenty, beside a warning that the figure was a floor. Two sentences that
|
|
217
|
+
* contradicted each other, and the reassuring one came second.
|
|
218
|
+
*/
|
|
219
|
+
if (coverage === 'partial') return ahead ? 'ahead' : 'cannot-tell';
|
|
220
|
+
|
|
221
|
+
if (ahead) return 'ahead';
|
|
222
|
+
if (drift < -0.05) return 'behind';
|
|
223
|
+
return 'on-pace';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** How many unmeasured days are worth naming before the list becomes noise. */
|
|
227
|
+
export const MAX_UNMEASURED_NAMED = 10;
|
|
228
|
+
|
|
229
|
+
export interface BudgetOptions {
|
|
230
|
+
catalogue: PricingCatalogue;
|
|
231
|
+
/** The instant the position is taken. Every share below is as of this moment. */
|
|
232
|
+
now?: Date;
|
|
233
|
+
/** Which period. Defaults to the month containing `now`. */
|
|
234
|
+
period?: BudgetPeriod;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The live position of every configured budget, from measured records alone.
|
|
239
|
+
*
|
|
240
|
+
* Per-label and per-source budgets are **not** computed here, and their
|
|
241
|
+
* absence is the honest answer rather than an omission: a store record carries
|
|
242
|
+
* a provider, a model and the account's own grouping, and it does not carry a
|
|
243
|
+
* workload label — labels live in a per-call usage log, which a bucketed
|
|
244
|
+
* provider API does not serve. Reporting a per-label position from records
|
|
245
|
+
* that cannot distinguish labels would be a number assembled from the wrong
|
|
246
|
+
* denominator. They appear in `unmeasuredScopes` instead, which says what is
|
|
247
|
+
* true: nothing measured here can answer for them.
|
|
248
|
+
*/
|
|
249
|
+
export function budgetPositions(
|
|
250
|
+
records: readonly StoreRecord[],
|
|
251
|
+
spend: SpendConfig | undefined,
|
|
252
|
+
options: BudgetOptions,
|
|
253
|
+
): BudgetReport {
|
|
254
|
+
const { catalogue, now = new Date() } = options;
|
|
255
|
+
const period = options.period ?? monthOf(now);
|
|
256
|
+
|
|
257
|
+
const positions: BudgetStanding[] = [];
|
|
258
|
+
const unmeasuredScopes: BudgetScope[] = [];
|
|
259
|
+
|
|
260
|
+
const elapsedMs = Math.min(Math.max(0, now.getTime() - period.fromMs), period.toMs - period.fromMs);
|
|
261
|
+
const elapsedDays = Math.max(1, Math.ceil(elapsedMs / DAY_MS));
|
|
262
|
+
const elapsedShare = elapsedMs / (period.toMs - period.fromMs);
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* `monthlyUsd`, never `maxUsd`. The two are the same units over different
|
|
266
|
+
* denominators — one gates whatever period a log happens to cover, this
|
|
267
|
+
* gates a calendar month — and reading one for the other is exactly the
|
|
268
|
+
* disagreement this module exists to end.
|
|
269
|
+
*/
|
|
270
|
+
if (spend?.monthlyUsd !== undefined) {
|
|
271
|
+
let consumedUsd = 0;
|
|
272
|
+
const measured = new Set<string>();
|
|
273
|
+
for (const record of records) {
|
|
274
|
+
const share = overlapShare(record, period);
|
|
275
|
+
if (share === 0) continue;
|
|
276
|
+
const usd = priceOf(record, catalogue);
|
|
277
|
+
// An unpriced model contributes no dollars **and no measured day**:
|
|
278
|
+
// counting the day would report the period as covered by money nobody
|
|
279
|
+
// can see, which is the same flattering omission in a different place.
|
|
280
|
+
if (usd === null) continue;
|
|
281
|
+
consumedUsd += usd * share;
|
|
282
|
+
for (const day of daysTouched(record, period)) measured.add(day);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const unmeasured: string[] = [];
|
|
286
|
+
for (let d = 0; d < elapsedDays; d += 1) {
|
|
287
|
+
const day = new Date(period.fromMs + d * DAY_MS).toISOString().slice(0, 10);
|
|
288
|
+
if (!measured.has(day)) unmeasured.push(day);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const coverage: BudgetCoverage =
|
|
292
|
+
measured.size === 0 ? 'none' : unmeasured.length === 0 ? 'complete' : 'partial';
|
|
293
|
+
const limitUsd = spend.monthlyUsd;
|
|
294
|
+
const consumedShare = limitUsd > 0 ? consumedUsd / limitUsd : null;
|
|
295
|
+
|
|
296
|
+
positions.push({
|
|
297
|
+
schemaVersion: 1,
|
|
298
|
+
scope: { kind: 'total' },
|
|
299
|
+
limitUsd,
|
|
300
|
+
period,
|
|
301
|
+
consumedUsd,
|
|
302
|
+
remainingUsd: limitUsd - consumedUsd,
|
|
303
|
+
provenance: 'measured',
|
|
304
|
+
measuredDays: measured.size,
|
|
305
|
+
elapsedDays,
|
|
306
|
+
unmeasuredDays: unmeasured.slice(0, MAX_UNMEASURED_NAMED),
|
|
307
|
+
coverage,
|
|
308
|
+
burn: { consumedShare, elapsedShare, shape: shapeOf(consumedShare, elapsedShare, coverage) },
|
|
309
|
+
verdict: coverage === 'none' ? 'cannot-tell' : consumedUsd > limitUsd ? 'over' : 'within',
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const label of Object.keys(spend?.byLabel ?? {})) {
|
|
314
|
+
unmeasuredScopes.push({ kind: 'label', label });
|
|
315
|
+
}
|
|
316
|
+
for (const source of Object.keys(spend?.bySource ?? {})) {
|
|
317
|
+
unmeasuredScopes.push({ kind: 'source', source });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return { schemaVersion: 1, period, positions, unmeasuredScopes };
|
|
321
|
+
}
|
package/src/config-schema.ts
CHANGED
|
@@ -68,6 +68,23 @@ export interface WaiveEntry {
|
|
|
68
68
|
export interface SpendConfig {
|
|
69
69
|
/** Whole-log budget. `--max-usd` overrides it. */
|
|
70
70
|
maxUsd?: number;
|
|
71
|
+
/**
|
|
72
|
+
* The calendar-month budget, spent against **measured** store records.
|
|
73
|
+
*
|
|
74
|
+
* A separate key from `maxUsd` on purpose, and the reason is worth stating
|
|
75
|
+
* because reusing one would have been so much less code. `maxUsd` gates
|
|
76
|
+
* *this log* — whatever period the file somebody passed happens to cover.
|
|
77
|
+
* This gates *this month*. Same units, different denominators, and one key
|
|
78
|
+
* carrying both is precisely how two surfaces of the same product come to
|
|
79
|
+
* disagree about how much is left: `serve` read `maxUsd` and compared it
|
|
80
|
+
* against the whole store, which could be a year, and reported the result as
|
|
81
|
+
* a budget position with a straight face.
|
|
82
|
+
*
|
|
83
|
+
* Nothing infers one from the other. A repository with a per-log gate and no
|
|
84
|
+
* monthly budget has no monthly position, and the tools say so rather than
|
|
85
|
+
* picking a number that is the right shape.
|
|
86
|
+
*/
|
|
87
|
+
monthlyUsd?: number;
|
|
71
88
|
/**
|
|
72
89
|
* Per-day budget — the gate a whole-log total cannot arm. `--max-day-usd`
|
|
73
90
|
* overrides it, and it inherits that flag's refusals: a log with no clock
|
|
@@ -217,7 +234,7 @@ export const CONFIG_KEYS = [
|
|
|
217
234
|
|
|
218
235
|
export const CONFIG_BASELINE_KEYS = ['path', 'maxGrowthTokens', 'maxGrowthPct'] as const;
|
|
219
236
|
|
|
220
|
-
export const CONFIG_SPEND_KEYS = ['maxUsd', 'maxDayUsd', 'maxSessionUsd', 'maxCacheLossUsd', 'byLabel', 'bySource'] as const;
|
|
237
|
+
export const CONFIG_SPEND_KEYS = ['maxUsd', 'monthlyUsd', 'maxDayUsd', 'maxSessionUsd', 'maxCacheLossUsd', 'byLabel', 'bySource'] as const;
|
|
221
238
|
|
|
222
239
|
export const CONFIG_WAIVE_KEYS = ['gate', 'reason', 'until'] as const;
|
|
223
240
|
|
|
@@ -420,6 +437,9 @@ function parseSpend(raw: unknown, source: string): SpendConfig {
|
|
|
420
437
|
if (raw.maxUsd !== undefined) {
|
|
421
438
|
spend.maxUsd = requireNonNegativeNumber(raw.maxUsd, 'spend.maxUsd', source);
|
|
422
439
|
}
|
|
440
|
+
if (raw.monthlyUsd !== undefined) {
|
|
441
|
+
spend.monthlyUsd = requireNonNegativeNumber(raw.monthlyUsd, 'spend.monthlyUsd', source);
|
|
442
|
+
}
|
|
423
443
|
if (raw.maxCacheLossUsd !== undefined) {
|
|
424
444
|
spend.maxCacheLossUsd = requireNonNegativeNumber(raw.maxCacheLossUsd, 'spend.maxCacheLossUsd', source);
|
|
425
445
|
}
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,19 @@ export type { PlanParseFailure, PlanParseResult } from './plan.js';
|
|
|
49
49
|
export type { PlanAction, PlanActionKind, PlanAssumption, PlanDocument } from './plan.js';
|
|
50
50
|
export { verifyPlan } from './verify.js';
|
|
51
51
|
export { buildHistory, storedReportFrom, MIN_RUN } from './history.js';
|
|
52
|
+
export { budgetPositions, monthOf, MAX_UNMEASURED_NAMED } from './budget.js';
|
|
53
|
+
export type {
|
|
54
|
+
BudgetCoverage,
|
|
55
|
+
BudgetOptions,
|
|
56
|
+
BudgetPeriod,
|
|
57
|
+
BudgetStanding,
|
|
58
|
+
BudgetReport,
|
|
59
|
+
BudgetScope,
|
|
60
|
+
BurnDown,
|
|
61
|
+
BurnShape,
|
|
62
|
+
} from './budget.js';
|
|
63
|
+
export { waiverHistory, waiverDay, isWaiverUse } from './waivers.js';
|
|
64
|
+
export type { WaiverHabit, WaiverHistory, WaiverUse, WaiverVerdict } from './waivers.js';
|
|
52
65
|
export { answerCost } from './answer.js';
|
|
53
66
|
export { guardSpend } from './guard.js';
|
|
54
67
|
export type { GuardAlternative, GuardAnswer, GuardRequest, GuardVerdict } from './guard.js';
|