/** * One policy, judged once — the function every door calls. * * The `limits` block states the policy: per-day, per-session and per-label * USD ceilings. This module answers whether a proposed call fits it, and the * design constraint is the 1.62 arc's lesson: **two doors to the same value * agreeing by coincidence is a defect waiting for its input.** So the doors * — the gateway's 402, `serve`'s cost answer, `spend_guard` over MCP — do * not each read a slice of config and do their own arithmetic. They hand * this function the policy, the measured position and the call, and forward * its answer. * * Nothing here re-derives the single-ceiling semantics either. Each ceiling * is judged by `answerCost`, the function that has answered "does this call * fit this budget" for every door since 1.44 — including its refusals: a * negative token count is rejected before it can become money, an unpriced * model is a cannot-tell rather than a guess, and a verdict always names * whether it rests on a measurement alone or needed the estimate. A policy * judgement is those answers, one per applicable ceiling, under one verdict. * * Two refusals are this module's own, and both close the same loophole: a * policy with per-label ceilings judging a call that names no label, or a * per-session ceiling judging a call that names no session, is `cannot-tell` * — not "no ceiling applies". A call that omits its label does not slip past * the label's ceiling; it becomes unjudgeable, and the answer says why. */ import type { PricingCatalogue } from './pricing.js'; import type { LimitsConfig, WaiveEntry } from './config-schema.js'; /** Which ceiling a judgement is about. */ export type LimitScope = 'day' | 'session' | 'label'; export type LimitVerdict = 'within' | 'over' | 'cannot-tell'; /** Why one ceiling could not be judged, when it could not. */ export type CannotJudgeReason = /** The scope's spend has not been measured, so the consumed half is unknown. */ 'nothing-measured' /** The model is not in the catalogue, so the call cannot be priced. */ | 'model-unpriced' /** Per-label ceilings exist and the call names no label. */ | 'no-label-on-call' /** A per-session ceiling exists and the call names no session. */ | 'no-session-on-call'; /** * One ceiling, judged. The three figures a refusal must be auditable from * are all here: the limit, the measured spend, and the window the measured * figure covers — the denominator, not just the number. */ export interface LimitJudgement { scope: LimitScope; /** Which label, when the scope is `label`. Null otherwise. */ label: string | null; limitUsd: number; verdict: LimitVerdict; /** * `measured` when the ceiling is already crossed without the estimate; * `measured+estimated` when it takes the described call to cross — or to * fit. Null when the verdict is `cannot-tell`. */ restsOn: 'measured' | 'measured+estimated' | null; reason: CannotJudgeReason | null; /** Measured spend inside this scope's period. Null when not measured. */ measuredUsd: number | null; /** The period the measured figure covers. Null when unknown. */ window: { fromMs: number; toMs: number; } | null; /** Where the scope would stand after this call. Null when unjudgeable. */ afterCallUsd: number | null; /** * The unexpired waiver silencing this ceiling, when the config carries one. * * The `check` mechanism, applied here unchanged: the verdict stays `over` * — the measurement is the measurement — but a waived `over` does not make * the policy refuse, and the waiver rides in the judgement so every answer * from every door is the record of the silence. Null when nothing is * waived, which is almost always. */ waived: { reason: string; until: string; } | null; } export interface PolicyJudgement { schemaVersion: 1; /** * The strictest verdict wins: one `over` makes the policy `over`, else one * `cannot-tell` makes it `cannot-tell`, else everything judged is `within`. * The order is the only safe one — a door that read "within" off a policy * with an unjudgeable ceiling would be approving a call nobody judged. */ verdict: LimitVerdict; /** Set when the verdict is `cannot-tell` and no ceiling produced it. */ reason: 'no-policy' | null; /** One entry per applicable ceiling, in policy order: day, session, label. */ judgements: LimitJudgement[]; } /** * The measured side: what the store has billed inside each scope's period. * * Every field is a measurement or `null`, never zero-for-absence — the rule * every document in this product holds. A scope the caller could not measure * (no clock on the log, a session nothing recorded) is `null`, and the * judgement for that ceiling is `cannot-tell` rather than "under". */ export interface MeasuredPosition { /** Measured spend in the current UTC day. */ dayUsd: number | null; dayWindow?: { fromMs: number; toMs: number; } | null; /** Measured spend in the call's session. */ sessionUsd: number | null; sessionWindow?: { fromMs: number; toMs: number; } | null; /** Measured spend under the call's label. */ labelUsd: number | null; labelWindow?: { fromMs: number; toMs: number; } | null; } /** The call being asked about — the same fields every door already takes. */ export interface ProposedCall { model?: string; inputTokens?: number; outputTokens?: number; basis?: 'token-count' | 'heuristic'; label?: string; session?: string; } /** * Judges a proposed call against the whole `limits` policy. * * Pure and synchronous for the same reason `answerCost` is: this runs inside * a request that has not been sent yet, and a function that reads a file * cannot promise single-digit milliseconds. The caller measures; this judges. */ export declare function judgeLimits(policy: LimitsConfig | undefined, position: MeasuredPosition, call: ProposedCall, options: { catalogue: PricingCatalogue; on?: Date; waivers?: readonly WaiveEntry[]; }): PolicyJudgement; /** The scope, as a refusal names it. Labels print; session identifiers never do. */ export declare function limitScopeName(judgement: LimitJudgement): string; /** * The refusal, legible — chapter four's rule, written once. * * Every over-limit sentence names the limit, the measured position and the * period, so an agent can log it and a person can audit it without * re-running anything. Every door uses this builder rather than composing * its own, for the same reason every door uses the same judge. */ export declare function limitSentence(judgement: LimitJudgement): string; /** Why the policy could not be judged, in one auditable sentence. */ export declare function unjudgedSentence(judged: PolicyJudgement): string; //# sourceMappingURL=judgement.d.ts.map