import type { AbandonOrigin } from "./abandon-load.js"; import type { HeapSample } from "./control-server.js"; import type { LoadOutcome, SettleOutcome } from "./ritual.js"; import { type TrendResult, type TrendVerdict } from "./trend.js"; /** * Why a measurement may not support its own verdict. * * `unsettled` — the heap was still moving when sampled * `settle-unverified` — the idle budget was too short to check * `load-incomplete` — fewer requests landed than were asked for * `abandon-ineffective` — early-disconnect run that disconnected nothing * `abandon-before-response` — cut before the server sent a byte, so the * mid-stream teardown path was never reached * `spiky-growth` — one cycle dominates, so the mean describes little * `near-threshold` — growth barely clears the noise floor * `thin-evidence` — a leak called on too few cycles for its size * `near-heap-ceiling` — the heap approached the cap the process ran under, * so the curve was measured against a ceiling instead of running free * `warm-up-baseline` — the baseline sat far above the level the cycles * settled at, so it carries warm-up's own retention rather than the app's * resting size */ export type WarningCode = "unsettled" | "settle-unverified" | "load-incomplete" | "abandon-ineffective" | "abandon-before-response" | "spiky-growth" | "near-threshold" | "thin-evidence" | "near-heap-ceiling" | "warm-up-baseline" /** * The load filled a cache the route never had to hold, and the run has no * second experiment that would settle how much of the growth that was. * * A cache filling up and memory going missing both retain and both climb; * only a bounded-key re-measurement tells them apart. On an ISR route that * re-measurement is not available: a bounded key set is served from the * cache, so the run drives revalidation to reach the renderer and lands on a * different path of Next's. Measured on the vercel/next.js#99077 * reproduction, `/plain` — the same app with the leak taken out — reported * `leak (+180.39 MB/1000 req)` with a fresh key per request and `stable * (+1.81)` bounded, and the two builds that retain 7x apart came out at * +1809.97 and +1815.05 once driven. The number stands as measured; what * cannot stand is a paste-ready draft built on it. */ | "cache-residency" /** * Repeated measurements of the same route did not agree. * * More cycles watch one process for longer; repetitions watch different * ones, and that is where the spread lives — vercel/next.js#84648 gave 602, * 826 and 875 MB on three runs of one build and 39 MB on a fourth. A verdict * a second run contradicts is not a verdict. */ | "repetitions-disagree"; export type MeasurementWarning = { code: WarningCode; detail: string; }; export type ConfidenceReport = { level: "high" | "low"; warnings: MeasurementWarning[]; /** * Verdict the evidence actually supports, when the measurement is not merely * noisy but invalid. * * Two different mechanisms set this, and they mean different things. * `assessConfidence` sets it when a run did not observe what its own verdict * requires, and only ever downgrades `leak`: accusing an app of leaking on * evidence that does not hold is the expensive error — it sends someone * chasing a ghost and ends as an issue against this tool. `aggregateRepetitions` * sets it for *any* verdict when repetitions of the same route disagreed, * which is not a flaw in this run at all. `withdrawnByDisagreement` below * tells the two apart; anything that explains a withdrawal to a reader has to. */ supersededVerdict?: TrendVerdict; }; /** * Whether a withdrawal came from repetitions disagreeing rather than from this * run failing its own audit. * * The distinction matters to anyone phrasing it. An audited withdrawal means * the run never observed what the verdict needs — it never settled, the * disconnects cut nothing, the deltas were too thin. A disagreement means the * opposite: this run observed exactly what it needed, and another run of the * same route observed something else. Telling a reader the run "did not observe * what that verdict needs" in the second case is simply untrue, and the * disagreement already carries its own warning naming the verdicts it produced. * * Both can be true of one report, and then this answers `true` and the audit's * sentence is dropped along with the disagreement's. `aggregateRepetitions` * keeps the winning pass's own warnings and overwrites only `supersededVerdict`, * which both mechanisms set to `inconclusive`, so no reading is lost: the * audit's warning still prints its own detail, which names the cause the generic * sentence never did. Separating the two would mean carrying the winner's * withdrawal reason through the aggregation, and it would buy the reader a * sentence they already have in a more specific form. */ export declare function withdrawnByDisagreement(confidence: ConfidenceReport): boolean; export type ConfidenceInput = { trend: TrendResult; loadOutcomes: readonly LoadOutcome[]; settleOutcomes: readonly SettleOutcome[]; /** Set when the run asked for early disconnects. */ abandonAfterMs?: number; /** Which deadline origin those disconnects used. Defaults to `first-byte`. */ abandonFrom?: AbandonOrigin; /** Threshold the verdict used, for the noise-floor check. */ minGrowthPerCycle?: number; /** Post-GC samples, for the heap-ceiling check. */ memorySamples?: readonly HeapSample[]; /** Old-space cap the measured process ran under (MB). */ maxOldSpaceMb?: number; /** Warm-up requests the run sent before the baseline, for the warm-up check. */ warmupRequests?: number; /** * Whether this route is served from the ISR cache. Decides whether the * cache-residency remedy exists — see `cacheResidencyWarnings`. */ revalidatesFromCache?: boolean; }; /** * Cycles a re-measurement uses when a verdict came back `inconclusive` — the * same figure the report's manual re-run hint prints. One definition, imported * by both, so the tool can never recommend one number and use another. */ export declare const resolveCycles: (cycles: number) => number; /** * The verdict a route's evidence actually supports. * * `trend.verdict` stays exactly as measured — the raw record must survive — so * every consumer that shows a verdict to a human reads it through here * instead, or it will report a leak the audit already withdrew. * * Structurally typed on purpose: it lives here, next to the audit, so the * reporters can reach it without importing the runner (and, through it, * memlab) just to render a line of text. */ export declare function effectiveVerdict(report: { trend: TrendResult; confidence: ConfidenceReport; }): TrendVerdict; /** * Whether a route's evidence is solid enough to draft an issue for. * * Stricter than the verdict on purpose: a draft is written to be pasted into * someone else's tracker, so it needs a leak that is plain, not one that * merely cleared the threshold. Measuring a healthy route on a real app * (`/server-plp`, 4 cycles × 2000 requests) produced deltas of * [0.9, 0.25, 0.33] MB and a draft; at 8 cycles × 5000 the same route * oscillated around a flat 39 MB and was plainly stable. */ export declare function warrantsIssueDraft(report: { trend: TrendResult; confidence: ConfidenceReport; }): boolean; /** * Audits a route measurement against its own evidence. * * A leak detector is an instrument, and a miscalibrated instrument does not * fail loudly — it reports confident, wrong numbers. Two implementations of * early disconnects shipped in this repo that abandoned nothing, and both * produced a verdict indistinguishable from the correct one; only the audit * trail caught them. This turns that trail into a check that runs every time. */ export declare function assessConfidence(input: ConfidenceInput): ConfidenceReport;