@trazum/core 1.41.0 → 1.43.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/store.js ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * A year of measured spend on disk, and not one prompt inside it.
3
+ *
4
+ * A connector that re-downloads a month every time it runs is a connector
5
+ * nobody leaves on, and `history` needs stored reports — which until now meant
6
+ * a human curating a directory of `--json` files. The store is where pulled
7
+ * usage lands so neither is true any more.
8
+ *
9
+ * **Pure, and in the core.** This module decides what a record *is*, when two
10
+ * records are the same record, and what a set of them adds up to. The
11
+ * filesystem lives in the CLI, the same split every other browser-safe module
12
+ * here keeps.
13
+ *
14
+ * **Convergence, not accumulation.** Re-pulling an overlapping window must not
15
+ * double the bill. Two records covering the same window, from the same
16
+ * provider, for the same model and grouping, are the *same fact restated* —
17
+ * the later pull wins, because a window pulled again is at worst as complete
18
+ * as it was. That makes overlapping pulls idempotent, which is what lets a
19
+ * scheduled job run every hour over a rolling day without inventing money.
20
+ *
21
+ * **Deduplication that cannot lie.** Two records the store cannot tell apart —
22
+ * a source that served no window, or no model — are kept as *two* and reported
23
+ * as possibly-double. Merging them on a guess makes a bill quietly smaller,
24
+ * and quietly smaller is the flattering direction this repository refuses
25
+ * everywhere it can occur.
26
+ */
27
+ /** Bump when the meaning of a field changes. Readers keep what they cannot read. */
28
+ export const STORE_SCHEMA_VERSION = 1;
29
+ /** What makes two records the same record. See the module note. */
30
+ export function identityOf(record) {
31
+ return [
32
+ record.provider,
33
+ record.fromMs,
34
+ record.toMs,
35
+ record.model,
36
+ JSON.stringify(record.group),
37
+ ].join('\n');
38
+ }
39
+ /**
40
+ * A record whose identity is not trustworthy enough to converge on.
41
+ *
42
+ * A window of zero length, or a record with no model, cannot be told apart
43
+ * from another like it. Those are kept whole and counted separately.
44
+ */
45
+ function identifiable(record) {
46
+ return record.model !== '' && record.toMs > record.fromMs;
47
+ }
48
+ /**
49
+ * Collapses an append-only log into the current truth.
50
+ *
51
+ * Append-only on disk and last-wins at read time, rather than rewriting a file
52
+ * in place: a crash during a rewrite can lose a year, and a crash during an
53
+ * append loses the tail of one line.
54
+ */
55
+ export function resolveStore(records) {
56
+ const byIdentity = new Map();
57
+ const possiblyDouble = [];
58
+ let unknownVersion = 0;
59
+ for (const record of records) {
60
+ if (record.v > STORE_SCHEMA_VERSION) {
61
+ unknownVersion += 1;
62
+ continue;
63
+ }
64
+ if (!identifiable(record)) {
65
+ possiblyDouble.push(record);
66
+ continue;
67
+ }
68
+ const key = identityOf(record);
69
+ const seen = byIdentity.get(key);
70
+ if (seen === undefined || record.pulledAtMs >= seen.pulledAtMs) {
71
+ byIdentity.set(key, record);
72
+ }
73
+ }
74
+ return {
75
+ records: [...byIdentity.values()].sort((a, b) => a.fromMs - b.fromMs || a.model.localeCompare(b.model)),
76
+ possiblyDouble,
77
+ unknownVersion,
78
+ };
79
+ }
80
+ /** Turns a connector's buckets into records ready to append. */
81
+ export function recordsFromBuckets(provider, buckets, pulledAtMs) {
82
+ return buckets.map((bucket) => ({
83
+ v: STORE_SCHEMA_VERSION,
84
+ provider,
85
+ fromMs: bucket.fromMs,
86
+ toMs: bucket.toMs,
87
+ model: bucket.model,
88
+ calls: bucket.calls,
89
+ input: bucket.inputTokens,
90
+ cacheRead: bucket.cacheReadTokens,
91
+ write5m: bucket.cacheWrite5mTokens,
92
+ write1h: bucket.cacheWrite1hTokens,
93
+ ttlKnown: bucket.writeTtlKnown,
94
+ output: bucket.outputTokens,
95
+ group: bucket.group,
96
+ pulledAtMs,
97
+ }));
98
+ }
99
+ /** The reverse, so a stored month prices exactly as a fresh pull does. */
100
+ export function bucketsFromRecords(records) {
101
+ return records.map((record) => ({
102
+ fromMs: record.fromMs,
103
+ toMs: record.toMs,
104
+ model: record.model,
105
+ calls: record.calls,
106
+ inputTokens: record.input,
107
+ cacheReadTokens: record.cacheRead,
108
+ cacheWrite5mTokens: record.write5m,
109
+ cacheWrite1hTokens: record.write1h,
110
+ writeTtlKnown: record.ttlKnown,
111
+ outputTokens: record.output,
112
+ group: record.group,
113
+ }));
114
+ }
115
+ export function storeInventory(resolved) {
116
+ const byProvider = new Map();
117
+ for (const record of resolved.records) {
118
+ const list = byProvider.get(record.provider) ?? [];
119
+ list.push(record);
120
+ byProvider.set(record.provider, list);
121
+ }
122
+ const providers = [...byProvider.entries()]
123
+ .map(([provider, records]) => {
124
+ const anyUnknown = records.some((r) => r.calls === null);
125
+ return {
126
+ provider,
127
+ records: records.length,
128
+ span: {
129
+ fromMs: Math.min(...records.map((r) => r.fromMs)),
130
+ toMs: Math.max(...records.map((r) => r.toMs)),
131
+ },
132
+ calls: anyUnknown ? null : records.reduce((sum, r) => sum + (r.calls ?? 0), 0),
133
+ models: [...new Set(records.map((r) => r.model))].sort(),
134
+ };
135
+ })
136
+ .sort((a, b) => a.provider.localeCompare(b.provider));
137
+ const all = resolved.records;
138
+ return {
139
+ schemaVersion: 1,
140
+ providers,
141
+ totalRecords: all.length,
142
+ span: all.length === 0
143
+ ? null
144
+ : {
145
+ fromMs: Math.min(...all.map((r) => r.fromMs)),
146
+ toMs: Math.max(...all.map((r) => r.toMs)),
147
+ },
148
+ possiblyDouble: resolved.possiblyDouble.length,
149
+ unknownVersion: resolved.unknownVersion,
150
+ };
151
+ }
152
+ /**
153
+ * Drops records whose window ended before the cutoff.
154
+ *
155
+ * Judged on `toMs`: a bucket that *ends* inside the retained period is
156
+ * retained whole, because half a bucket is a measurement of nothing. What goes
157
+ * is returned rather than counted, so the caller can say what went — silence
158
+ * about deleted measurements is the one thing a store must not do.
159
+ */
160
+ export function pruneRecords(records, cutoffMs) {
161
+ const kept = [];
162
+ const dropped = [];
163
+ for (const record of records) {
164
+ (record.toMs < cutoffMs ? dropped : kept).push(record);
165
+ }
166
+ return {
167
+ kept,
168
+ dropped,
169
+ droppedSpan: dropped.length === 0
170
+ ? null
171
+ : {
172
+ fromMs: Math.min(...dropped.map((r) => r.fromMs)),
173
+ toMs: Math.max(...dropped.map((r) => r.toMs)),
174
+ },
175
+ };
176
+ }
177
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * The afternoon the loop burned a quarter of the month, said that afternoon.
3
+ *
4
+ * Every gate in this product fires when a human runs a command. The failures
5
+ * worth catching — a retry loop, a prompt that grew, a model swapped in a
6
+ * deploy — happen at 3pm on a Tuesday, and a report that arrives three weeks
7
+ * later is an obituary.
8
+ *
9
+ * This module decides **what has crossed**, given what is measured and what
10
+ * the operator asked to be told about. The pulling, the storing, the sleeping
11
+ * and the sending live in the CLI; everything here is arithmetic over figures
12
+ * somebody already has, which is what makes an alerting rule testable without
13
+ * waiting for 3pm.
14
+ *
15
+ * **An alert fires on a measured crossing, never on a projection.** "You will
16
+ * exceed" is a forecast, and this product has refused those at every window
17
+ * length since 1.27. "You have spent $412 of a $400 budget, measured over
18
+ * these calls" is a fact, and the difference is the only reason an alert at
19
+ * 3am can be trusted.
20
+ *
21
+ * **A window too short to mean anything does not fire.** A day gate needs a
22
+ * whole day of measurement before it can fail: the first ten minutes of a day
23
+ * are not a day, and a watcher that cries at every dawn gets muted — which is
24
+ * how alerting tools actually fail.
25
+ */
26
+ import type { BucketedReport } from './connector.js';
27
+ export type WatchGate = 'maxUsd' | 'maxDayUsd' | 'maxCacheLossUsd';
28
+ /** Why a gate could not be judged rather than passed. */
29
+ export type NotJudgeable =
30
+ /** Not enough of the period is measured for the threshold to mean anything. */
31
+ 'window-too-short'
32
+ /** The source cannot serve the dimension this gate is written against. */
33
+ | 'dimension-unavailable';
34
+ export interface WatchCrossing {
35
+ gate: WatchGate;
36
+ /** The measured figure that crossed, and the threshold it crossed. */
37
+ measuredUsd: number;
38
+ limitUsd: number;
39
+ /** Which slice of time the figure covers — a day gate names its day. */
40
+ window: {
41
+ fromMs: number;
42
+ toMs: number;
43
+ };
44
+ /** A day gate's UTC day, so the alert names the afternoon it means. */
45
+ day: string | null;
46
+ /**
47
+ * Everything a machine reader needs to know what kind of number this is.
48
+ *
49
+ * `measured` is the only value this module will ever emit: a projected
50
+ * crossing is not a crossing. The field exists anyway, because a consumer
51
+ * that cannot see the provenance will treat whatever arrives as fact — and
52
+ * a later version of this file must not be able to smuggle an estimate past
53
+ * a reader by leaving the question unasked.
54
+ */
55
+ provenance: 'measured';
56
+ }
57
+ export interface WatchAbstention {
58
+ gate: WatchGate;
59
+ reason: NotJudgeable;
60
+ /** What is missing, as a figure the operator can act on. */
61
+ detail: {
62
+ coveredMs: number;
63
+ neededMs: number;
64
+ } | null;
65
+ }
66
+ export interface WatchResult {
67
+ crossings: WatchCrossing[];
68
+ /**
69
+ * Still over the limit, and already reported on an earlier cycle.
70
+ *
71
+ * These are the reason a quiet cycle is not the same as a clean one. A
72
+ * restart that reported "within every threshold" while the budget was still
73
+ * blown would be the flattering reading this product refuses everywhere:
74
+ * the alert was suppressed, the *crossing* was not, and only one of those
75
+ * is news.
76
+ */
77
+ suppressed: WatchCrossing[];
78
+ /**
79
+ * Gates that could not be judged, which is neither a pass nor a failure.
80
+ *
81
+ * Reported rather than swallowed: a gate silently skipped for a week reads
82
+ * exactly like a gate that has been passing for a week, and those are very
83
+ * different states to be in.
84
+ */
85
+ abstentions: WatchAbstention[];
86
+ /**
87
+ * The stretch this cycle did not watch, when a watcher was down or is
88
+ * starting for the first time. A resumed watcher that says nothing implies
89
+ * coverage it did not have.
90
+ */
91
+ gap: {
92
+ fromMs: number;
93
+ toMs: number;
94
+ } | null;
95
+ }
96
+ export interface WatchThresholds {
97
+ maxUsd?: number;
98
+ maxDayUsd?: number;
99
+ maxCacheLossUsd?: number;
100
+ }
101
+ export interface WatchOptions {
102
+ /** Priced measurements for the period being watched. */
103
+ report: BucketedReport;
104
+ thresholds: WatchThresholds;
105
+ /** The cache verdict over the same report, when the caller computed one. */
106
+ cacheDeltaUsd?: number;
107
+ /** Now, so a partly-elapsed day can be told from a whole one. */
108
+ nowMs: number;
109
+ /** Where the previous cycle finished, for the coverage gap. */
110
+ lastCoveredToMs?: number;
111
+ /**
112
+ * Gates already fired, by gate and by day, so a restart is not amnesia.
113
+ *
114
+ * Keyed `gate` for whole-period gates and `gate\nYYYY-MM-DD` for a day, so
115
+ * a day that already alerted stays quiet while a *new* day crossing still
116
+ * speaks.
117
+ */
118
+ alreadyFired?: ReadonlySet<string>;
119
+ }
120
+ /** A day gate cannot judge a day that has not finished being measured. */
121
+ export declare const DAY_MS = 86400000;
122
+ /**
123
+ * How much of a period must be measured before a threshold over it means
124
+ * anything. Nine tenths rather than all of it: a usage API's last bucket is
125
+ * often minutes behind, and a gate that waits for perfection never fires.
126
+ */
127
+ export declare const COVERAGE_FLOOR = 0.9;
128
+ export declare function firedKey(gate: WatchGate, day: string | null): string;
129
+ export declare function evaluateWatch(options: WatchOptions): WatchResult;
130
+ //# sourceMappingURL=watch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,WAAW,GAAG,iBAAiB,CAAC;AAEnE,yDAAyD;AACzD,MAAM,MAAM,YAAY;AACtB,+EAA+E;AAC7E,kBAAkB;AACpB,0EAA0E;GACxE,uBAAuB,CAAC;AAE5B,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,SAAS,CAAC;IAChB,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,uEAAuE;IACvE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB;;;;;;;;OAQG;IACH,UAAU,EAAE,UAAU,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,YAAY,CAAC;IACrB,4DAA4D;IAC5D,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACxD;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B;;;;;;;;OAQG;IACH,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B;;;;;;OAMG;IACH,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B;;;;OAIG;IACH,GAAG,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAC9C;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,wDAAwD;IACxD,MAAM,EAAE,cAAc,CAAC;IACvB,UAAU,EAAE,eAAe,CAAC;IAC5B,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACpC;AAED,0EAA0E;AAC1E,eAAO,MAAM,MAAM,WAAa,CAAC;AAEjC;;;;GAIG;AACH,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC,wBAAgB,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAEpE;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,WAAW,CAqEhE"}
package/dist/watch.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The afternoon the loop burned a quarter of the month, said that afternoon.
3
+ *
4
+ * Every gate in this product fires when a human runs a command. The failures
5
+ * worth catching — a retry loop, a prompt that grew, a model swapped in a
6
+ * deploy — happen at 3pm on a Tuesday, and a report that arrives three weeks
7
+ * later is an obituary.
8
+ *
9
+ * This module decides **what has crossed**, given what is measured and what
10
+ * the operator asked to be told about. The pulling, the storing, the sleeping
11
+ * and the sending live in the CLI; everything here is arithmetic over figures
12
+ * somebody already has, which is what makes an alerting rule testable without
13
+ * waiting for 3pm.
14
+ *
15
+ * **An alert fires on a measured crossing, never on a projection.** "You will
16
+ * exceed" is a forecast, and this product has refused those at every window
17
+ * length since 1.27. "You have spent $412 of a $400 budget, measured over
18
+ * these calls" is a fact, and the difference is the only reason an alert at
19
+ * 3am can be trusted.
20
+ *
21
+ * **A window too short to mean anything does not fire.** A day gate needs a
22
+ * whole day of measurement before it can fail: the first ten minutes of a day
23
+ * are not a day, and a watcher that cries at every dawn gets muted — which is
24
+ * how alerting tools actually fail.
25
+ */
26
+ /** A day gate cannot judge a day that has not finished being measured. */
27
+ export const DAY_MS = 86_400_000;
28
+ /**
29
+ * How much of a period must be measured before a threshold over it means
30
+ * anything. Nine tenths rather than all of it: a usage API's last bucket is
31
+ * often minutes behind, and a gate that waits for perfection never fires.
32
+ */
33
+ export const COVERAGE_FLOOR = 0.9;
34
+ export function firedKey(gate, day) {
35
+ return day === null ? gate : `${gate}\n${day}`;
36
+ }
37
+ export function evaluateWatch(options) {
38
+ const { report, thresholds, cacheDeltaUsd, nowMs, lastCoveredToMs, alreadyFired } = options;
39
+ const fired = alreadyFired ?? new Set();
40
+ const crossings = [];
41
+ const suppressed = [];
42
+ const abstentions = [];
43
+ const span = report.span;
44
+ const push = (gate, measuredUsd, limitUsd, day, window) => {
45
+ if (measuredUsd <= limitUsd)
46
+ return;
47
+ const crossing = { gate, measuredUsd, limitUsd, window, day, provenance: 'measured' };
48
+ // Already told: quiet, but still crossed. The two are kept apart because
49
+ // "we alerted about this" and "this is fine now" are different sentences.
50
+ (fired.has(firedKey(gate, day)) ? suppressed : crossings).push(crossing);
51
+ };
52
+ if (thresholds.maxUsd !== undefined) {
53
+ if (span === null) {
54
+ abstentions.push({ gate: 'maxUsd', reason: 'dimension-unavailable', detail: null });
55
+ }
56
+ else {
57
+ push('maxUsd', report.total.totalUsd, thresholds.maxUsd, null, span);
58
+ }
59
+ }
60
+ if (thresholds.maxCacheLossUsd !== undefined) {
61
+ if (cacheDeltaUsd === undefined || span === null) {
62
+ abstentions.push({ gate: 'maxCacheLossUsd', reason: 'dimension-unavailable', detail: null });
63
+ }
64
+ else {
65
+ push('maxCacheLossUsd', cacheDeltaUsd, thresholds.maxCacheLossUsd, null, span);
66
+ }
67
+ }
68
+ if (thresholds.maxDayUsd !== undefined) {
69
+ if (report.byDay.length === 0) {
70
+ abstentions.push({ gate: 'maxDayUsd', reason: 'dimension-unavailable', detail: null });
71
+ }
72
+ else {
73
+ for (const entry of report.byDay) {
74
+ const dayStart = Date.parse(`${entry.day}T00:00:00Z`);
75
+ const dayEnd = dayStart + DAY_MS;
76
+ /**
77
+ * The day still running is measured only up to now, so a threshold
78
+ * over it is a threshold over a fraction of a day. Once it *has*
79
+ * crossed, the crossing is real whatever the hour — a day that is
80
+ * already over budget at noon does not become less over budget at
81
+ * midnight — so the abstention only applies while the figure is
82
+ * still under the limit.
83
+ */
84
+ const covered = Math.min(nowMs, dayEnd) - dayStart;
85
+ const whole = covered >= DAY_MS * COVERAGE_FLOOR;
86
+ if (entry.usd > thresholds.maxDayUsd) {
87
+ push('maxDayUsd', entry.usd, thresholds.maxDayUsd, entry.day, { fromMs: dayStart, toMs: dayEnd });
88
+ }
89
+ else if (!whole) {
90
+ abstentions.push({
91
+ gate: 'maxDayUsd',
92
+ reason: 'window-too-short',
93
+ detail: { coveredMs: Math.max(0, covered), neededMs: DAY_MS },
94
+ });
95
+ }
96
+ }
97
+ }
98
+ }
99
+ const gap = lastCoveredToMs !== undefined && span !== null && span.fromMs > lastCoveredToMs
100
+ ? { fromMs: lastCoveredToMs, toMs: span.fromMs }
101
+ : null;
102
+ return { crossings, suppressed, abstentions, gap };
103
+ }
104
+ //# sourceMappingURL=watch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.js","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trazum/core",
3
- "version": "1.41.0",
3
+ "version": "1.43.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",
@@ -80,6 +80,17 @@ export interface SpendConfig {
80
80
  * for the day budget's reason.
81
81
  */
82
82
  maxSessionUsd?: number;
83
+ /**
84
+ * What caching may add to the bill before it is a failure.
85
+ *
86
+ * `--max-cache-loss-usd` has gated this since 1.21 and only as a flag,
87
+ * which made it a gate `watch` could not read: a policy that lives in one
88
+ * invocation is a policy nothing else can act on. It inherits the flag's
89
+ * refusal — the worst case is read when the log did not record the write
90
+ * TTL, because a gate reading the flattering half passes the bills it
91
+ * exists to catch.
92
+ */
93
+ maxCacheLossUsd?: number;
83
94
  /** Per-label budgets, each gated against that label's own spend. */
84
95
  byLabel?: Record<string, number>;
85
96
  /**
@@ -147,6 +158,16 @@ export interface TrazumConfig {
147
158
  * one is a silence nobody can audit. Waived failures render as waived,
148
159
  * never hidden — the bill still counts them; only the exit code is quiet.
149
160
  */
161
+ /**
162
+ * How long the local usage store keeps a measurement.
163
+ *
164
+ * Retention is a policy, so it lives in the repository beside the budgets
165
+ * rather than in whichever invocation happened to run the prune. There is
166
+ * deliberately **no default**: deleting measurements on a policy nobody
167
+ * wrote down is not something anybody should get by accident, so `--prune`
168
+ * with neither this nor `--keep` refuses and says so.
169
+ */
170
+ store?: { keepDays?: number };
150
171
  waive?: WaiveEntry[];
151
172
  /** Default for `trazum diff --max-growth`, in tokens. */
152
173
  maxGrowth?: number;
@@ -186,6 +207,7 @@ export const CONFIG_KEYS = [
186
207
  'labels',
187
208
  'spend',
188
209
  'sources',
210
+ 'store',
189
211
  'waive',
190
212
  'maxGrowth',
191
213
  'baseline',
@@ -195,10 +217,12 @@ export const CONFIG_KEYS = [
195
217
 
196
218
  export const CONFIG_BASELINE_KEYS = ['path', 'maxGrowthTokens', 'maxGrowthPct'] as const;
197
219
 
198
- export const CONFIG_SPEND_KEYS = ['maxUsd', 'maxDayUsd', 'maxSessionUsd', 'byLabel', 'bySource'] as const;
220
+ export const CONFIG_SPEND_KEYS = ['maxUsd', 'maxDayUsd', 'maxSessionUsd', 'maxCacheLossUsd', 'byLabel', 'bySource'] as const;
199
221
 
200
222
  export const CONFIG_WAIVE_KEYS = ['gate', 'reason', 'until'] as const;
201
223
 
224
+ export const CONFIG_STORE_KEYS = ['keepDays'] as const;
225
+
202
226
  /**
203
227
  * The gates a waiver can silence. The list is closed on purpose: a waiver
204
228
  * naming a gate that does not exist is a decision about nothing, and the
@@ -396,6 +420,9 @@ function parseSpend(raw: unknown, source: string): SpendConfig {
396
420
  if (raw.maxUsd !== undefined) {
397
421
  spend.maxUsd = requireNonNegativeNumber(raw.maxUsd, 'spend.maxUsd', source);
398
422
  }
423
+ if (raw.maxCacheLossUsd !== undefined) {
424
+ spend.maxCacheLossUsd = requireNonNegativeNumber(raw.maxCacheLossUsd, 'spend.maxCacheLossUsd', source);
425
+ }
399
426
  if (raw.maxDayUsd !== undefined) {
400
427
  spend.maxDayUsd = requireNonNegativeNumber(raw.maxDayUsd, 'spend.maxDayUsd', source);
401
428
  }
@@ -466,6 +493,32 @@ function parseSources(raw: unknown, source: string): Record<string, string[]> {
466
493
  * nothing. The expiry is *not* checked against today here — a config file is
467
494
  * timeless, and whether a waiver is expired is judged where the gate runs.
468
495
  */
496
+ /**
497
+ * `store.keepDays` — retention, in whole days.
498
+ *
499
+ * A fraction of a day is refused rather than rounded: retention decides what
500
+ * gets deleted, and a policy the tool rounded on the operator's behalf is a
501
+ * policy nobody agreed to.
502
+ */
503
+ function parseStore(raw: unknown, source: string): { keepDays?: number } {
504
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
505
+ throw new Error(`"store" in ${source} must be an object, for example {"keepDays": 90}.`);
506
+ }
507
+ const entry = raw as Record<string, unknown>;
508
+ rejectUnknownKeys(entry, CONFIG_STORE_KEYS, source, 'store.');
509
+ const out: { keepDays?: number } = {};
510
+ if (entry.keepDays !== undefined) {
511
+ const days = entry.keepDays;
512
+ if (typeof days !== 'number' || !Number.isInteger(days) || days <= 0) {
513
+ throw new Error(
514
+ `"store.keepDays" in ${source} must be a whole number of days above zero, and it is ${JSON.stringify(days)}.`,
515
+ );
516
+ }
517
+ out.keepDays = days;
518
+ }
519
+ return out;
520
+ }
521
+
469
522
  function parseWaive(raw: unknown, source: string): WaiveEntry[] {
470
523
  if (!Array.isArray(raw)) throw new ConfigError('"waive" must be an array', source);
471
524
  return raw.map((entry, index) => {
@@ -654,6 +707,7 @@ export function parseConfig(raw: string, source = CONFIG_FILENAME): TrazumConfig
654
707
  if (document.labels !== undefined) config.labels = parseLabels(document.labels, source);
655
708
  if (document.spend !== undefined) config.spend = parseSpend(document.spend, source);
656
709
  if (document.sources !== undefined) config.sources = parseSources(document.sources, source);
710
+ if (document.store !== undefined) config.store = parseStore(document.store, source);
657
711
  if (document.waive !== undefined) config.waive = parseWaive(document.waive, source);
658
712
  if (document.baseline !== undefined) {
659
713
  config.baseline = parseBaselineConfig(document.baseline, source);
package/src/history.ts CHANGED
@@ -29,7 +29,12 @@ export interface StoredReport {
29
29
  name: string;
30
30
  span: { fromMs: number; toMs: number } | null;
31
31
  totalUsd: number;
32
- calls: number;
32
+ /**
33
+ * null when the source serves no request count — a bucketed usage API. Zero
34
+ * would read as "no traffic" against real spend, which is the reading this
35
+ * product refuses everywhere it can occur.
36
+ */
37
+ calls: number | null;
33
38
  /** Label → dollars this period. */
34
39
  byLabel: Map<string, number>;
35
40
  /** Model → dollars this period. */
@@ -70,7 +75,7 @@ export interface RepeatedPlanAction {
70
75
  export interface HistoryDocument {
71
76
  schemaVersion: 1;
72
77
  /** Ordered oldest first by span start. */
73
- periods: { name: string; fromMs: number; toMs: number; totalUsd: number; calls: number }[];
78
+ periods: { name: string; fromMs: number; toMs: number; totalUsd: number; calls: number | null }[];
74
79
  /** Per label, dollars per period — null where the label had no traffic. */
75
80
  labelSeries: { label: string; points: (number | null)[] }[];
76
81
  /** Per model, share of that period's total — null where absent. */
package/src/index.ts CHANGED
@@ -48,6 +48,26 @@ export { buildPlan, planLabelName } from './plan.js';
48
48
  export type { PlanAction, PlanActionKind, PlanAssumption, PlanDocument } from './plan.js';
49
49
  export { verifyPlan } from './verify.js';
50
50
  export { buildHistory, storedReportFrom, MIN_RUN } from './history.js';
51
+ export { evaluateWatch, firedKey, COVERAGE_FLOOR, DAY_MS } from './watch.js';
52
+ export type {
53
+ NotJudgeable,
54
+ WatchAbstention,
55
+ WatchCrossing,
56
+ WatchGate,
57
+ WatchOptions,
58
+ WatchResult,
59
+ WatchThresholds,
60
+ } from './watch.js';
61
+ export {
62
+ STORE_SCHEMA_VERSION,
63
+ identityOf,
64
+ resolveStore,
65
+ recordsFromBuckets,
66
+ bucketsFromRecords,
67
+ storeInventory,
68
+ pruneRecords,
69
+ } from './store.js';
70
+ export type { PruneResult, ResolvedStore, StoreInventory, StoreRecord } from './store.js';
51
71
  export {
52
72
  CONNECTORS,
53
73
  connectorFor,