/** * The failure that actually matters, and the four ways of getting it wrong. * * CI has been able to fail a build for tokens since 1.4 and for dollars since * 1.21. A prompt edit that quietly made the product worse has never been * gateable — which means every cost saving this tool has ever recommended went * into a repository with its most important consequence unmeasured. * * ## This is a before-and-after, not an experiment * * Worth saying in the first paragraph, because the arithmetic is the same as * `experiment`'s and the epistemics are not. An experiment splits traffic * randomly, so the two arms differ only in the thing under test. A * before-and-after splits it by **time**, and everything else that changed at * the same time is in the difference too. * * So this module spends most of its code looking for reasons *not* to blame the * prompt, and it reports `cannot-tell` far more readily than a randomised * comparison would. That is not timidity. A gate that blames the prompt because * the prompt is the thing it can see will be switched off within a month, and a * switched-off gate catches nothing at all. * * ## The three confounders it can actually see * * **The model moved.** If the mix of models shifted across the boundary, the * prompt is not the only variable, and the drop may be entirely somebody else's * migration. * * **The volume moved.** A workload whose traffic tripled is usually a workload * whose *population* changed — a new surface, a new customer, a marketing * campaign — and the questions being asked are not the questions from before. * * **The coverage moved.** The subtle one, and the one nobody thinks of: if the * share of calls recording an outcome changed, the two rates describe different * populations. A team that started instrumenting its hard cases will see its * measured rate fall without anything getting worse. * * Any of them present, and the verdict is `cannot-tell` with the confounder * **named**. Not a hedge attached to a blame — a refusal to blame. * * ## What it cannot see, and says so * * Everything else deployed that day. This module has one label's numbers and no * knowledge of the world, and it never pretends otherwise: a `dropped` verdict * is a statement that the rate fell and the three things it can check did not * move, which is a smaller claim than "the prompt did it" and is the largest * claim the evidence supports. */ import type { ArmResult, ExperimentArm } from './experiment.js'; import type { OutcomeVocabulary } from './outcome.js'; /** * Outcomes each side of the change needs before a verdict is offered. * * A hundred rather than the ten used for a rate elsewhere, because this one * fails builds. The cost of a wrong `dropped` is somebody reverting a good * change and losing the saving; the cost of a wrong `cannot-tell` is waiting a * day. Those are not symmetric and the threshold is not either. */ export declare const MIN_OUTCOMES_EACH_SIDE = 100; /** How far the model mix may move before the comparison stops being about the prompt. */ export declare const MAX_MODEL_MIX_DRIFT = 0.1; /** * How far the call volume may move, as a ratio. * * Half again, either way. Ordinary week-to-week traffic moves less than that; * a workload that doubled is usually a workload whose population changed. */ export declare const MAX_VOLUME_RATIO = 1.5; /** How far outcome coverage may move before the two rates describe different populations. */ export declare const MAX_COVERAGE_DRIFT = 0.1; export type Confounder = { kind: 'model-mix-moved'; drift: number; model: string; } | { kind: 'volume-moved'; beforeCalls: number; afterCalls: number; ratio: number; } | { kind: 'coverage-moved'; before: number; after: number; }; export type GateVerdict = 'held' | 'dropped' | 'cannot-tell'; export type GateUnknown = 'too-few-before' | 'too-few-after' | 'confounded' | 'no-vocabulary' | 'not-separable'; /** One side of the boundary, as the caller measured it. */ export interface GateSide { arm: ExperimentArm; /** Every call in the window, outcome or not — the coverage denominator. */ calls: number; /** Spend per model, for the mix comparison. */ usdByModel: Array<{ model: string; usd: number; }>; } export interface QualityGate { verdict: GateVerdict; /** Why, when the verdict is `cannot-tell`. A refusal never arrives bare. */ unknown: GateUnknown | null; before: ArmResult; after: ArmResult; /** The 95% interval on (after − before), or null. */ difference: { point: number; low: number; high: number; } | null; /** * Everything found that means the prompt is not the only variable. * * Non-empty forces `cannot-tell`. Reported even on a `held` verdict, because * a rate that held while the model changed underneath is not evidence that * the prompt is fine either. */ confounders: Confounder[]; /** * The cost half — measured, and kept beside the quality half rather than * merged with it. The sentence teams argue about needs both. */ cost: { beforeUsdPerCall: number; afterUsdPerCall: number; deltaUsdPerCall: number; } | null; /** Outcomes behind the comparison, so the claim carries its own sample size. */ outcomes: { before: number; after: number; }; } export declare function qualityGate(before: GateSide, after: GateSide, vocabulary: OutcomeVocabulary | null): QualityGate; //# sourceMappingURL=quality-gate.d.ts.map