/** * One measured number, wherever it is asked for. * * By 1.48 there are four ways to ask Trazum about money — a gate in CI, the * terminal, the local endpoint an agent consults, the browser — and no * guarantee any two of them agree about how much of a budget is left. Each * computed its own answer from whatever it happened to be holding: a log, a * store, a request body. Four right answers to four slightly different * questions is how a CI failure and an agent's refusal come to disagree in * front of somebody. * * This is that number. A budget becomes a **position**: a limit, a period, the * measured spend inside it, and — the part that makes it honest — how much of * that period was measured at all. * * **Nothing here forecasts.** "Sixty-one per cent of the budget, consumed over * eleven of thirty days" is a measurement. "You will run out on the 24th" is a * prediction, and this product has refused those since 1.27 at every scale it * operates on. The burn-down below compares two shares that both already * happened, and names the shape; it never produces a date. * * **A period nobody measured is not a period under budget.** The rule * `fleetBudgetMissing` established for services in 1.37, applied to time: days * inside the period with no measurement are counted and named, and a position * standing on three measured days out of thirty says so rather than reporting * a comfortable ninety per cent remaining. */ import type { SpendConfig } from './config-schema.js'; import type { StoreRecord } from './store.js'; import type { PricingCatalogue } from './pricing.js'; /** The window a budget is spent over. Calendar months, UTC, like everything else. */ export interface BudgetPeriod { kind: 'month'; /** `YYYY-MM`, UTC. */ id: string; fromMs: number; /** Half-open: the first instant of the next month. */ toMs: number; days: number; } export type BudgetScope = { kind: 'total'; } | { kind: 'label'; label: string; } | { kind: 'source'; source: string; }; /** * How much of the period the measurement actually covers. * * Three values rather than a percentage, because the three lead to different * decisions: act on it, act on it knowing it is a floor, or go and find out * why nothing was measured. */ export type BudgetCoverage = 'complete' | 'partial' | 'none'; /** * The shape of the burn, named — never a date. * * A comparison of two shares that have both already happened: how much of the * budget is gone against how much of the period is gone. `ahead` means the * money is going faster than the calendar, which is a fact about the past * eleven days and not a claim about the next nineteen. */ export type BurnShape = 'ahead' | 'on-pace' | 'behind' | 'cannot-tell'; export interface BurnDown { /** Share of the limit consumed, 0-1. Null when the limit is zero. */ consumedShare: number | null; /** Share of the period elapsed at the instant this was computed, 0-1. */ elapsedShare: number; shape: BurnShape; /** * Deliberately absent: any field naming a date the budget runs out. * * Stated here rather than left to be noticed, because it is the single most * requested number this module will ever be asked for, and every future * reader of this file will be tempted to add it. It cannot be measured; it * can only be projected from a rate that the log has no reason to keep. */ readonly forecast?: never; } export interface BudgetStanding { schemaVersion: 1; scope: BudgetScope; limitUsd: number; period: BudgetPeriod; /** Measured spend inside the period. Never an estimate — see `provenance`. */ consumedUsd: number; remainingUsd: number; provenance: 'measured'; /** Distinct UTC days inside the period that carry any measurement. */ measuredDays: number; /** Days of the period that have already elapsed at the instant asked. */ elapsedDays: number; /** * Elapsed days with no measurement at all, oldest first, capped for * rendering. A day missing from a series is the thing a total cannot show. */ unmeasuredDays: string[]; coverage: BudgetCoverage; burn: BurnDown; /** * `cannot-tell` when nothing in the period was measured. A budget with no * measurement behind it is not a budget under control, and reporting * `within` would be the flattering direction — the one this repository * refuses everywhere it can occur. */ verdict: 'within' | 'over' | 'cannot-tell'; } export interface BudgetReport { schemaVersion: 1; period: BudgetPeriod; positions: BudgetStanding[]; /** * Budgets configured with a scope nothing measured touches. * * Not a position of zero: a label that has no records may have been renamed, * or may simply not have run. Both are worth knowing and neither is "under * budget". */ unmeasuredScopes: BudgetScope[]; } /** The UTC month containing `at`, as a period. */ export declare function monthOf(at: Date): BudgetPeriod; /** How many unmeasured days are worth naming before the list becomes noise. */ export declare const MAX_UNMEASURED_NAMED = 10; export interface BudgetOptions { catalogue: PricingCatalogue; /** The instant the position is taken. Every share below is as of this moment. */ now?: Date; /** Which period. Defaults to the month containing `now`. */ period?: BudgetPeriod; } /** * The live position of every configured budget, from measured records alone. * * Per-label and per-source budgets are **not** computed here, and their * absence is the honest answer rather than an omission: a store record carries * a provider, a model and the account's own grouping, and it does not carry a * workload label — labels live in a per-call usage log, which a bucketed * provider API does not serve. Reporting a per-label position from records * that cannot distinguish labels would be a number assembled from the wrong * denominator. They appear in `unmeasuredScopes` instead, which says what is * true: nothing measured here can answer for them. */ export declare function budgetPositions(records: readonly StoreRecord[], spend: SpendConfig | undefined, options: BudgetOptions): BudgetReport; //# sourceMappingURL=budget.d.ts.map