@trazum/core 1.65.0 → 1.67.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.
Files changed (50) hide show
  1. package/dist/config-schema.d.ts +32 -1
  2. package/dist/config-schema.d.ts.map +1 -1
  3. package/dist/config-schema.js +55 -3
  4. package/dist/config-schema.js.map +1 -1
  5. package/dist/conform.d.ts +1 -1
  6. package/dist/conform.d.ts.map +1 -1
  7. package/dist/conform.js +13 -0
  8. package/dist/conform.js.map +1 -1
  9. package/dist/gateway.d.ts +32 -1
  10. package/dist/gateway.d.ts.map +1 -1
  11. package/dist/gateway.js +64 -3
  12. package/dist/gateway.js.map +1 -1
  13. package/dist/guard.d.ts +20 -0
  14. package/dist/guard.d.ts.map +1 -1
  15. package/dist/guard.js +41 -3
  16. package/dist/guard.js.map +1 -1
  17. package/dist/index.d.ts +8 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +4 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/judgement.d.ts +159 -0
  22. package/dist/judgement.d.ts.map +1 -0
  23. package/dist/judgement.js +188 -0
  24. package/dist/judgement.js.map +1 -0
  25. package/dist/position-report.d.ts +107 -0
  26. package/dist/position-report.d.ts.map +1 -0
  27. package/dist/position-report.js +180 -0
  28. package/dist/position-report.js.map +1 -0
  29. package/dist/position.d.ts +74 -0
  30. package/dist/position.d.ts.map +1 -0
  31. package/dist/position.js +93 -0
  32. package/dist/position.js.map +1 -0
  33. package/dist/schemas.d.ts.map +1 -1
  34. package/dist/schemas.js +9 -0
  35. package/dist/schemas.js.map +1 -1
  36. package/dist/session-cost.d.ts +2 -0
  37. package/dist/session-cost.d.ts.map +1 -1
  38. package/dist/session-cost.js +1 -1
  39. package/dist/session-cost.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/config-schema.ts +89 -3
  42. package/src/conform.ts +12 -0
  43. package/src/gateway.ts +108 -4
  44. package/src/guard.ts +73 -3
  45. package/src/index.ts +24 -1
  46. package/src/judgement.ts +346 -0
  47. package/src/position-report.ts +275 -0
  48. package/src/position.ts +126 -0
  49. package/src/schemas.ts +9 -0
  50. package/src/session-cost.ts +1 -1
@@ -0,0 +1,188 @@
1
+ /**
2
+ * One policy, judged once — the function every door calls.
3
+ *
4
+ * The `limits` block states the policy: per-day, per-session and per-label
5
+ * USD ceilings. This module answers whether a proposed call fits it, and the
6
+ * design constraint is the 1.62 arc's lesson: **two doors to the same value
7
+ * agreeing by coincidence is a defect waiting for its input.** So the doors
8
+ * — the gateway's 402, `serve`'s cost answer, `spend_guard` over MCP — do
9
+ * not each read a slice of config and do their own arithmetic. They hand
10
+ * this function the policy, the measured position and the call, and forward
11
+ * its answer.
12
+ *
13
+ * Nothing here re-derives the single-ceiling semantics either. Each ceiling
14
+ * is judged by `answerCost`, the function that has answered "does this call
15
+ * fit this budget" for every door since 1.44 — including its refusals: a
16
+ * negative token count is rejected before it can become money, an unpriced
17
+ * model is a cannot-tell rather than a guess, and a verdict always names
18
+ * whether it rests on a measurement alone or needed the estimate. A policy
19
+ * judgement is those answers, one per applicable ceiling, under one verdict.
20
+ *
21
+ * Two refusals are this module's own, and both close the same loophole: a
22
+ * policy with per-label ceilings judging a call that names no label, or a
23
+ * per-session ceiling judging a call that names no session, is `cannot-tell`
24
+ * — not "no ceiling applies". A call that omits its label does not slip past
25
+ * the label's ceiling; it becomes unjudgeable, and the answer says why.
26
+ */
27
+ import { answerCost } from './answer.js';
28
+ import { formatUsd } from './savings.js';
29
+ const judgementOf = (scope, label, limitUsd, answer) => ({
30
+ scope,
31
+ label,
32
+ limitUsd,
33
+ verdict: answer.verdict,
34
+ restsOn: answer.restsOn,
35
+ // The single-ceiling reasons map one-to-one; the two call-shape refusals
36
+ // (`no-label-on-call`, `no-session-on-call`) never reach answerCost.
37
+ reason: answer.reason === null ? null : answer.reason === 'model-unpriced' ? 'model-unpriced' : 'nothing-measured',
38
+ measuredUsd: answer.budget?.consumedUsd ?? null,
39
+ window: answer.budget?.window ?? null,
40
+ afterCallUsd: answer.afterCall?.usd ?? null,
41
+ waived: null,
42
+ });
43
+ const unjudgeable = (scope, label, limitUsd, reason) => ({
44
+ scope,
45
+ label,
46
+ limitUsd,
47
+ verdict: 'cannot-tell',
48
+ restsOn: null,
49
+ reason,
50
+ measuredUsd: null,
51
+ window: null,
52
+ afterCallUsd: null,
53
+ waived: null,
54
+ });
55
+ /** The waiver gate that names one judgement's ceiling. */
56
+ const gateOf = (judgement) => judgement.scope === 'day'
57
+ ? 'limits.dayUsd'
58
+ : judgement.scope === 'session'
59
+ ? 'limits.sessionUsd'
60
+ : `limits.byLabel:${judgement.label ?? ''}`;
61
+ /**
62
+ * The unexpired waiver for one judgement, if the config carries one.
63
+ *
64
+ * Judged in UTC at run time, the same rule `check` applies: the day the
65
+ * waiver expires, the ceiling refuses again — nobody has to remember.
66
+ */
67
+ const waiverFor = (judgement, waivers, on) => {
68
+ const today = on.toISOString().slice(0, 10);
69
+ const match = waivers.find((entry) => entry.gate === gateOf(judgement) && entry.until >= today);
70
+ return match === undefined ? null : { reason: match.reason, until: match.until };
71
+ };
72
+ /**
73
+ * Judges a proposed call against the whole `limits` policy.
74
+ *
75
+ * Pure and synchronous for the same reason `answerCost` is: this runs inside
76
+ * a request that has not been sent yet, and a function that reads a file
77
+ * cannot promise single-digit milliseconds. The caller measures; this judges.
78
+ */
79
+ export function judgeLimits(policy, position, call, options) {
80
+ const judgements = [];
81
+ const judge = (consumedUsd, limitUsd, window) => answerCost({
82
+ model: call.model,
83
+ inputTokens: call.inputTokens,
84
+ outputTokens: call.outputTokens,
85
+ basis: call.basis,
86
+ limitUsd,
87
+ consumedUsd: consumedUsd ?? undefined,
88
+ window,
89
+ }, options);
90
+ if (policy?.dayUsd !== undefined) {
91
+ judgements.push(judgementOf('day', null, policy.dayUsd, judge(position.dayUsd, policy.dayUsd, position.dayWindow ?? null)));
92
+ }
93
+ if (policy?.sessionUsd !== undefined) {
94
+ judgements.push(call.session === undefined
95
+ ? unjudgeable('session', null, policy.sessionUsd, 'no-session-on-call')
96
+ : judgementOf('session', null, policy.sessionUsd, judge(position.sessionUsd, policy.sessionUsd, position.sessionWindow ?? null)));
97
+ }
98
+ if (policy?.byLabel !== undefined && Object.keys(policy.byLabel).length > 0) {
99
+ if (call.label === undefined) {
100
+ /*
101
+ The loophole this closes: per-label ceilings with an unlabelled call.
102
+ Skipping the scope would mean the ceiling only binds calls polite
103
+ enough to name themselves — the same defect as the negative token
104
+ count, worn as an omission. One judgement, no label, cannot-tell.
105
+ The ceiling reported is the smallest, because that is the one the
106
+ call might be dodging.
107
+ */
108
+ const smallest = Math.min(...Object.values(policy.byLabel));
109
+ judgements.push(unjudgeable('label', null, smallest, 'no-label-on-call'));
110
+ }
111
+ else if (policy.byLabel[call.label] !== undefined) {
112
+ const limitUsd = policy.byLabel[call.label];
113
+ judgements.push(judgementOf('label', call.label, limitUsd, judge(position.labelUsd, limitUsd, position.labelWindow ?? null)));
114
+ }
115
+ // A labelled call whose label has no ceiling is judged by the other
116
+ // scopes alone: the policy said nothing about this label, and inventing
117
+ // a ceiling for it would be enforcing config nobody wrote.
118
+ }
119
+ if (judgements.length === 0) {
120
+ return { schemaVersion: 1, verdict: 'cannot-tell', reason: 'no-policy', judgements: [] };
121
+ }
122
+ /**
123
+ * Waivers, applied after every ceiling is judged: the measurement stays —
124
+ * a waived judgement still says `over` — but the silence is attached, on
125
+ * the record, and the verdict below treats a waived `over` as not-refusing.
126
+ */
127
+ const { on = new Date(), waivers = [] } = options;
128
+ if (waivers.length > 0) {
129
+ for (const judgement of judgements) {
130
+ if (judgement.verdict === 'over')
131
+ judgement.waived = waiverFor(judgement, waivers, on);
132
+ }
133
+ }
134
+ const verdict = judgements.some((entry) => entry.verdict === 'over' && entry.waived === null)
135
+ ? 'over'
136
+ : judgements.some((entry) => entry.verdict === 'cannot-tell')
137
+ ? 'cannot-tell'
138
+ : 'within';
139
+ return { schemaVersion: 1, verdict, reason: null, judgements };
140
+ }
141
+ /** The scope, as a refusal names it. Labels print; session identifiers never do. */
142
+ export function limitScopeName(judgement) {
143
+ return judgement.scope === 'day'
144
+ ? "the day's ceiling"
145
+ : judgement.scope === 'session'
146
+ ? "this session's ceiling"
147
+ : judgement.label === null
148
+ ? 'the per-label ceiling'
149
+ : `the "${judgement.label}" label's ceiling`;
150
+ }
151
+ /** The period a measured figure covers, when the judgement carries one. */
152
+ function periodOf(judgement) {
153
+ if (judgement.window === null)
154
+ return '';
155
+ const from = new Date(judgement.window.fromMs).toISOString();
156
+ const to = new Date(judgement.window.toMs).toISOString();
157
+ return ` between ${from} and ${to}`;
158
+ }
159
+ /**
160
+ * The refusal, legible — chapter four's rule, written once.
161
+ *
162
+ * Every over-limit sentence names the limit, the measured position and the
163
+ * period, so an agent can log it and a person can audit it without
164
+ * re-running anything. Every door uses this builder rather than composing
165
+ * its own, for the same reason every door uses the same judge.
166
+ */
167
+ export function limitSentence(judgement) {
168
+ const limit = formatUsd(judgement.limitUsd);
169
+ const measured = judgement.measuredUsd === null ? 'nothing measured' : `${formatUsd(judgement.measuredUsd)} measured`;
170
+ return judgement.restsOn === 'measured'
171
+ ? `${capital(limitScopeName(judgement))} of ${limit} is already spent: ${measured}${periodOf(judgement)}.`
172
+ : `This call would take ${limitScopeName(judgement)} of ${limit} past its limit: ${measured}${periodOf(judgement)}, plus this call's estimate.`;
173
+ }
174
+ /** Why the policy could not be judged, in one auditable sentence. */
175
+ export function unjudgedSentence(judged) {
176
+ const first = judged.judgements.find((entry) => entry.verdict === 'cannot-tell');
177
+ const which = first === undefined ? 'a ceiling' : limitScopeName(first);
178
+ const why = first?.reason === 'no-label-on-call'
179
+ ? 'the call names no label'
180
+ : first?.reason === 'no-session-on-call'
181
+ ? 'the call names no session'
182
+ : first?.reason === 'model-unpriced'
183
+ ? 'the model is not in the price catalogue'
184
+ : 'its spend has not been measured';
185
+ return `${capital(which)} in the limits policy cannot be judged: ${why}.`;
186
+ }
187
+ const capital = (text) => `${text[0].toUpperCase()}${text.slice(1)}`;
188
+ //# sourceMappingURL=judgement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"judgement.js","sourceRoot":"","sources":["../src/judgement.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The month, ended on a measured position — the 1.67 arc's one answer.
3
+ *
4
+ * The product refuses to forecast, and that refusal stays. But "refuses to
5
+ * forecast" and "cannot say where the month stands" are different sentences:
6
+ * the measured burn, the days measured, and the arithmetic distance to each
7
+ * configured ceiling are all measurements — and before this module they were
8
+ * scattered across `profile`, `budgetPositions` and `watch` rather than
9
+ * answerable as one question.
10
+ *
11
+ * **One source, named.** Everything here is measured from a usage log — the
12
+ * record of what was called, priced record by record. The store's monthly
13
+ * standing (provider-billed buckets, the number `serve` and `store` report)
14
+ * is a different measurement of a different thing, and folding the two into
15
+ * one figure would be the two-doors defect with extra steps. The document
16
+ * says `source: "usage-log"` so no reader has to guess which this is.
17
+ *
18
+ * **The distance is division, labelled as division.** "At this measured
19
+ * rate, the limit is N days away" is `remaining ÷ (measured ÷ days
20
+ * measured)` — arithmetic on the past, stated with its own denominators. It
21
+ * is absent, not zeroed, when the rate stands on fewer measured days than
22
+ * `MIN_SCALE_DAYS`, the same floor every scaled figure in this product
23
+ * respects; absent when nothing was measured; and absent on an `over`,
24
+ * because the distance to a place you have already passed is not a number
25
+ * this tool prints. There is no field naming a date, here or anywhere.
26
+ */
27
+ import type { BudgetPeriod } from './budget.js';
28
+ import type { UsageRecord } from './usage.js';
29
+ import type { PricingCatalogue } from './pricing.js';
30
+ import type { LimitsConfig, SpendConfig } from './config-schema.js';
31
+ /** What a position is measured against, and over which window. */
32
+ export type PositionScope = 'month' | 'day' | 'label';
33
+ export interface PositionDistance {
34
+ /** `remainingUsd ÷ usdPerDay`. Division on the past, not a prediction. */
35
+ daysAway: number;
36
+ /** The measured rate: month-to-date spend over the days that measured it. */
37
+ usdPerDay: number;
38
+ /** The denominator of the rate — how many measured days stand behind it. */
39
+ overDays: number;
40
+ /** Always `division`: what this number is, stated in the number. */
41
+ arithmetic: 'division';
42
+ }
43
+ export interface PositionStanding {
44
+ scope: PositionScope;
45
+ /** Which label, when the scope is `label`. Null otherwise. */
46
+ label: string | null;
47
+ limitUsd: number;
48
+ /** Priced from the log's own records. Never an estimate. */
49
+ measuredUsd: number;
50
+ remainingUsd: number;
51
+ /** The window the measured figure covers. */
52
+ window: {
53
+ fromMs: number;
54
+ toMs: number;
55
+ };
56
+ /** Distinct UTC days inside the window carrying any measurement. */
57
+ daysMeasured: number;
58
+ /** Days of the window already elapsed when this was computed. */
59
+ daysElapsed: number;
60
+ verdict: 'within' | 'over' | 'cannot-tell';
61
+ /**
62
+ * Present only when it means something: a `within` verdict standing on at
63
+ * least `MIN_SCALE_DAYS` measured days and a non-zero rate. Null is "this
64
+ * arithmetic would mislead", never "we forgot".
65
+ */
66
+ distance: PositionDistance | null;
67
+ }
68
+ /** A configured ceiling this log cannot measure, with the reason stated. */
69
+ export interface UnmeasuredPosition {
70
+ scope: PositionScope;
71
+ label: string | null;
72
+ limitUsd: number;
73
+ why: 'no-clock' | 'no-labels' | 'nothing-recorded' | 'label-unseen';
74
+ }
75
+ export interface PositionDocument {
76
+ schemaVersion: 1;
77
+ /** Where every figure below was measured from. */
78
+ source: 'usage-log';
79
+ month: BudgetPeriod;
80
+ positions: PositionStanding[];
81
+ /** Configured ceilings the log cannot answer for, each with the reason. */
82
+ unmeasured: UnmeasuredPosition[];
83
+ /**
84
+ * What this document deliberately does not answer, stated rather than
85
+ * implied. The per-session ceiling is judged per call at the doors — a
86
+ * session is not a calendar scope, and a "session position for the month"
87
+ * would be an average wearing a limit's name.
88
+ */
89
+ cannotSay: string[];
90
+ /** Records whose model the catalogue cannot price — money nobody can see. */
91
+ unpricedRecords: number;
92
+ }
93
+ /**
94
+ * The position, measured from one parsed usage log.
95
+ *
96
+ * Pure over its inputs, like `judgeLimits` and for the same reason: the
97
+ * same function answers at the CLI, in the HTML door and over MCP, so the
98
+ * surfaces cannot disagree about where the month stands.
99
+ */
100
+ export declare function positionReport(records: readonly UsageRecord[], config: {
101
+ spend?: SpendConfig;
102
+ limits?: LimitsConfig;
103
+ }, options: {
104
+ catalogue: PricingCatalogue;
105
+ on?: Date;
106
+ }): PositionDocument;
107
+ //# sourceMappingURL=position-report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"position-report.d.ts","sourceRoot":"","sources":["../src/position-report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAIpE,kEAAkE;AAClE,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;AAEtD,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,UAAU,EAAE,UAAU,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,aAAa,CAAC;IACrB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,QAAQ,GAAG,MAAM,GAAG,aAAa,CAAC;IAC3C;;;;OAIG;IACH,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAC;CACnC;AAED,4EAA4E;AAC5E,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,aAAa,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,UAAU,GAAG,WAAW,GAAG,kBAAkB,GAAG,cAAc,CAAC;CACrE;AAED,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,CAAC,CAAC;IACjB,kDAAkD;IAClD,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,YAAY,CAAC;IACpB,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,2EAA2E;IAC3E,UAAU,EAAE,kBAAkB,EAAE,CAAC;IACjC;;;;;OAKG;IACH,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,6EAA6E;IAC7E,eAAe,EAAE,MAAM,CAAC;CACzB;AAQD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,SAAS,WAAW,EAAE,EAC/B,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,WAAW,CAAC;IAAC,MAAM,CAAC,EAAE,YAAY,CAAA;CAAE,EACtD,OAAO,EAAE;IAAE,SAAS,EAAE,gBAAgB,CAAC;IAAC,EAAE,CAAC,EAAE,IAAI,CAAA;CAAE,GAClD,gBAAgB,CA4JlB"}
@@ -0,0 +1,180 @@
1
+ /**
2
+ * The month, ended on a measured position — the 1.67 arc's one answer.
3
+ *
4
+ * The product refuses to forecast, and that refusal stays. But "refuses to
5
+ * forecast" and "cannot say where the month stands" are different sentences:
6
+ * the measured burn, the days measured, and the arithmetic distance to each
7
+ * configured ceiling are all measurements — and before this module they were
8
+ * scattered across `profile`, `budgetPositions` and `watch` rather than
9
+ * answerable as one question.
10
+ *
11
+ * **One source, named.** Everything here is measured from a usage log — the
12
+ * record of what was called, priced record by record. The store's monthly
13
+ * standing (provider-billed buckets, the number `serve` and `store` report)
14
+ * is a different measurement of a different thing, and folding the two into
15
+ * one figure would be the two-doors defect with extra steps. The document
16
+ * says `source: "usage-log"` so no reader has to guess which this is.
17
+ *
18
+ * **The distance is division, labelled as division.** "At this measured
19
+ * rate, the limit is N days away" is `remaining ÷ (measured ÷ days
20
+ * measured)` — arithmetic on the past, stated with its own denominators. It
21
+ * is absent, not zeroed, when the rate stands on fewer measured days than
22
+ * `MIN_SCALE_DAYS`, the same floor every scaled figure in this product
23
+ * respects; absent when nothing was measured; and absent on an `over`,
24
+ * because the distance to a place you have already passed is not a number
25
+ * this tool prints. There is no field naming a date, here or anywhere.
26
+ */
27
+ import { monthOf } from './budget.js';
28
+ import { MIN_SCALE_DAYS } from './measured-profile.js';
29
+ import { costOf } from './session-cost.js';
30
+ const DAY_MS = 24 * 60 * 60 * 1000;
31
+ const daysIn = (stamps) => {
32
+ const days = new Set();
33
+ for (const ms of stamps)
34
+ days.add(new Date(ms).toISOString().slice(0, 10));
35
+ return days.size;
36
+ };
37
+ /**
38
+ * The position, measured from one parsed usage log.
39
+ *
40
+ * Pure over its inputs, like `judgeLimits` and for the same reason: the
41
+ * same function answers at the CLI, in the HTML door and over MCP, so the
42
+ * surfaces cannot disagree about where the month stands.
43
+ */
44
+ export function positionReport(records, config, options) {
45
+ const { catalogue, on = new Date() } = options;
46
+ const month = monthOf(on);
47
+ const dayFromMs = Math.floor(on.getTime() / DAY_MS) * DAY_MS;
48
+ const dayWindow = { fromMs: dayFromMs, toMs: dayFromMs + DAY_MS };
49
+ const monthWindow = { fromMs: month.fromMs, toMs: month.toMs };
50
+ const elapsedDays = Math.max(1, Math.min(month.days, Math.ceil((on.getTime() - month.fromMs) / DAY_MS)));
51
+ let unpricedRecords = 0;
52
+ let clocksSeen = false;
53
+ let labelsSeen = false;
54
+ let monthUsd = 0;
55
+ let dayUsd = 0;
56
+ const monthDays = new Set();
57
+ const labelUsd = new Map();
58
+ const labelDays = new Map();
59
+ for (const record of records) {
60
+ if (record.ts !== null)
61
+ clocksSeen = true;
62
+ if (record.label !== null)
63
+ labelsSeen = true;
64
+ const usd = costOf(record, catalogue, on);
65
+ if (usd === null) {
66
+ unpricedRecords += 1;
67
+ continue;
68
+ }
69
+ if (record.ts === null)
70
+ continue; // a clockless record belongs to no window
71
+ const day = Math.floor(record.ts / DAY_MS) * DAY_MS;
72
+ if (record.ts >= month.fromMs && record.ts < month.toMs) {
73
+ monthUsd += usd;
74
+ monthDays.add(day);
75
+ if (record.label !== null) {
76
+ labelUsd.set(record.label, (labelUsd.get(record.label) ?? 0) + usd);
77
+ if (!labelDays.has(record.label))
78
+ labelDays.set(record.label, new Set());
79
+ labelDays.get(record.label).add(day);
80
+ }
81
+ }
82
+ if (record.ts >= dayWindow.fromMs && record.ts < dayWindow.toMs)
83
+ dayUsd += usd;
84
+ }
85
+ const positions = [];
86
+ const unmeasured = [];
87
+ const standing = (scope, label, limitUsd, measuredUsd, window, daysMeasured, daysElapsed, measured) => {
88
+ const verdict = !measured ? 'cannot-tell' : measuredUsd > limitUsd ? 'over' : 'within';
89
+ const usdPerDay = daysMeasured > 0 ? measuredUsd / daysMeasured : 0;
90
+ const distance = verdict === 'within' && daysMeasured >= MIN_SCALE_DAYS && usdPerDay > 0
91
+ ? {
92
+ daysAway: (limitUsd - measuredUsd) / usdPerDay,
93
+ usdPerDay,
94
+ overDays: daysMeasured,
95
+ arithmetic: 'division',
96
+ }
97
+ : null;
98
+ return {
99
+ scope,
100
+ label,
101
+ limitUsd,
102
+ measuredUsd,
103
+ remainingUsd: limitUsd - measuredUsd,
104
+ window,
105
+ daysMeasured,
106
+ daysElapsed,
107
+ verdict,
108
+ distance,
109
+ };
110
+ };
111
+ const monthly = config.spend?.monthlyUsd;
112
+ if (monthly !== undefined) {
113
+ if (!clocksSeen) {
114
+ unmeasured.push({ scope: 'month', label: null, limitUsd: monthly, why: records.length === 0 ? 'nothing-recorded' : 'no-clock' });
115
+ }
116
+ else {
117
+ /**
118
+ * A month with zero measured days is a stale log, not a quiet month:
119
+ * `budgetPositions` has held that line since 1.49 ("coverage none is
120
+ * cannot-tell"), and this module agreeing with it is the point of the
121
+ * arc. Contrast the day scope below, deliberately.
122
+ */
123
+ positions.push(standing('month', null, monthly, monthUsd, monthWindow, monthDays.size, elapsedDays, monthDays.size > 0));
124
+ }
125
+ }
126
+ const day = config.limits?.dayUsd;
127
+ if (day !== undefined) {
128
+ if (!clocksSeen) {
129
+ unmeasured.push({ scope: 'day', label: null, limitUsd: day, why: records.length === 0 ? 'nothing-recorded' : 'no-clock' });
130
+ }
131
+ else {
132
+ /**
133
+ * A day with no records inside a log that records clocks is a quiet
134
+ * day, measured at $0 — the same reading `positionAt` gives the doors,
135
+ * because a position page and an enforcement door disagreeing about
136
+ * today's spend is the exact defect the 1.66 arc closed. And one day
137
+ * is one day: the distance arithmetic needs a multi-day rate, so a
138
+ * day position never carries a `distance` — always under the floor.
139
+ */
140
+ positions.push(standing('day', null, day, dayUsd, dayWindow, monthDays.has(dayFromMs) ? 1 : 0, 1, true));
141
+ }
142
+ }
143
+ for (const [label, limitUsd] of Object.entries(config.limits?.byLabel ?? {})) {
144
+ if (!clocksSeen || !labelsSeen) {
145
+ unmeasured.push({
146
+ scope: 'label',
147
+ label,
148
+ limitUsd,
149
+ why: records.length === 0 ? 'nothing-recorded' : !clocksSeen ? 'no-clock' : 'no-labels',
150
+ });
151
+ }
152
+ else if (!labelUsd.has(label)) {
153
+ // The log records labels and has never seen this one this month. That
154
+ // is a measured empty history — but a position of $0 with no days
155
+ // behind it would print the healthiest possible line for a label that
156
+ // may simply have been renamed, so it is named instead.
157
+ unmeasured.push({ scope: 'label', label, limitUsd, why: 'label-unseen' });
158
+ }
159
+ else {
160
+ positions.push(standing('label', label, limitUsd, labelUsd.get(label), monthWindow, daysIn(labelDays.get(label)), elapsedDays, true));
161
+ }
162
+ }
163
+ const cannotSay = [];
164
+ if (config.limits?.sessionUsd !== undefined) {
165
+ cannotSay.push('limits.sessionUsd is judged per call at the doors. A session is not a calendar scope, and a "session position for the month" would be an average wearing a limit\'s name.');
166
+ }
167
+ if (monthly === undefined && config.limits?.dayUsd === undefined && Object.keys(config.limits?.byLabel ?? {}).length === 0) {
168
+ cannotSay.push('No monthly budget and no limits are configured, so there is no ceiling to state a position against. spend.monthlyUsd and the limits block are where ceilings live.');
169
+ }
170
+ return {
171
+ schemaVersion: 1,
172
+ source: 'usage-log',
173
+ month,
174
+ positions,
175
+ unmeasured,
176
+ cannotSay,
177
+ unpricedRecords,
178
+ };
179
+ }
180
+ //# sourceMappingURL=position-report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"position-report.js","sourceRoot":"","sources":["../src/position-report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The measured side of the limits policy: what a usage log says each scope
3
+ * has already spent.
4
+ *
5
+ * `judgeLimits` judges; this measures. The split is the product's oldest
6
+ * rule — the judging function must be pure and instant, so whatever reads
7
+ * and prices records happens once, here, and the doors keep the result.
8
+ *
9
+ * **Absence is `null`, never zero.** A log with no clock cannot say what
10
+ * today has cost; a log that records no sessions cannot say what a session
11
+ * has spent. Both come back `null`, and `judgeLimits` turns a `null` into
12
+ * `cannot-tell` rather than an approval. The one deliberate asymmetry: a log
13
+ * that *does* record sessions and has never seen this one answers `0` — the
14
+ * history of a conversation that has not started is complete and empty,
15
+ * which is a measurement, not an absence.
16
+ *
17
+ * **Session keys never leave this module.** The index groups by them — that
18
+ * is the whole job — but the map lives in door memory, `positionAt` returns
19
+ * only dollar figures, and nothing here is serialised. The guarantee the
20
+ * usage module states (a session key is somebody's conversation and is never
21
+ * printed) holds through this door too.
22
+ */
23
+ import type { UsageRecord } from './usage.js';
24
+ import type { PricingCatalogue } from './pricing.js';
25
+ import type { MeasuredPosition } from './judgement.js';
26
+ /**
27
+ * Everything a door needs to answer `positionAt` in constant time, built
28
+ * once from the log it was pointed at.
29
+ */
30
+ export interface MeasuredIndex {
31
+ /** Spend inside the current UTC day. Null when no record carries a clock. */
32
+ dayUsd: number | null;
33
+ /** The UTC day the figure covers. Null exactly when `dayUsd` is. */
34
+ dayWindow: {
35
+ fromMs: number;
36
+ toMs: number;
37
+ } | null;
38
+ /** Whether any record carried a label / a session at all. */
39
+ labelsSeen: boolean;
40
+ sessionsSeen: boolean;
41
+ /** Priced spend per label. Keys are labels, which do print. */
42
+ labels: ReadonlyMap<string, number>;
43
+ /** Priced spend per session. Keys are session identifiers — never printed. */
44
+ sessions: ReadonlyMap<string, number>;
45
+ /** Whether the log held any records at all. */
46
+ any: boolean;
47
+ /** Records whose model the catalogue cannot price — money nobody can see. */
48
+ unpriced: number;
49
+ }
50
+ /**
51
+ * Prices and groups a parsed usage log, once.
52
+ *
53
+ * An unpriced record contributes nothing to any figure and is **counted**
54
+ * instead of dropped silently: a door that reports "measured $3" over a log
55
+ * with a thousand unpriced records is wrong by an amount nobody can see, in
56
+ * the flattering direction, and `unpriced` is how the door says so.
57
+ */
58
+ export declare function indexUsage(records: readonly UsageRecord[], options: {
59
+ catalogue: PricingCatalogue;
60
+ on?: Date;
61
+ }): MeasuredIndex;
62
+ /**
63
+ * The measured position for one proposed call, from an index built once.
64
+ *
65
+ * A scope the log cannot see is `null`; a scope it can see and has nothing
66
+ * recorded for is `0`. The difference is the difference between "unknown"
67
+ * and "a conversation that has not started yet", and collapsing them is how
68
+ * a dead log approves a live spend.
69
+ */
70
+ export declare function positionAt(index: MeasuredIndex, call: {
71
+ label?: string;
72
+ session?: string;
73
+ }): MeasuredPosition;
74
+ //# sourceMappingURL=position.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"position.d.ts","sourceRoot":"","sources":["../src/position.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAIvD;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,oEAAoE;IACpE,SAAS,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACnD,6DAA6D;IAC7D,UAAU,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IACtB,+DAA+D;IAC/D,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,8EAA8E;IAC9E,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,+CAA+C;IAC/C,GAAG,EAAE,OAAO,CAAC;IACb,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,SAAS,WAAW,EAAE,EAC/B,OAAO,EAAE;IAAE,SAAS,EAAE,gBAAgB,CAAC;IAAC,EAAE,CAAC,EAAE,IAAI,CAAA;CAAE,GAClD,aAAa,CAsCf;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,aAAa,EACpB,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACzC,gBAAgB,CAWlB"}