/** * CHOICE COST — the policy layer. * =============================== * * Analysis says what the grammar costs. This says which of those numbers fail a build. * They are separate because they have different consumers and different lifetimes: a * developer wants the ranked list on every run, CI wants one bit, and the threshold * that produces that bit is a decision the owner makes once and writes down. * * This module is PURE — it takes a report and a baseline and returns a verdict. It * reads no files, spawns nothing, and prints nothing, so it is testable without a * fixture checkout and cannot be the reason a gate is flaky. * * THE BASELINE IS ABSOLUTE, NOT DIFFERENTIAL * ------------------------------------------ * Gating each commit against its parent lets +2% per commit land forever: every * individual step is under tolerance and the total is unbounded. So the committed * baseline records the ACTUAL byte counts, and drift is measured against those * numbers, not against yesterday's. * * A baseline is committed DATA, reviewed like any other diff. Nothing here writes one. * Rebaselining is a deliberate act with a reviewable artifact and owner sign-off; there * is no automatic refresh, because a gate that rebaselines itself is a gate that * records regressions rather than catching them. * * AND IT IS TWO-SIDED — A WIN MUST BE BANKED * ------------------------------------------ * Growth past the committed number fails, and so does an unbanked IMPROVEMENT. This is * the half usually left to a comment asking a human politely, and it is the half that * rots: `bench/grammar-density/config.json` and `bench/workloads/config.json` each * carried exactly such a comment and sat unbumped for TEN releases. If a reorder takes * a site from 7,846 wasted bytes to 2,000 and the baseline does not move with it, 5,846 * bytes of fresh headroom silently become budget for the next regression — and the gate * that was supposed to catch that regression reads green through the whole of it. * * So the committed number is a BAND, not a floor: leaving it in either direction fails, * and only the remedy differs. Raising a number needs owner sign-off; lowering one is * mandatory. Both are the same one-line rebaseline and the same reviewable diff. * * `bench/size-guard.ts` is the shape this follows — it earned the tightness in 0.45 by * catching +0.14% moves a 1% tolerance would have waved through. * * A BASELINE CANNOT LAUNDER A CEILING * ----------------------------------- * If a ceiling is configured, a baseline that records a number ABOVE it is rejected as * invalid rather than honoured. Otherwise the ceiling would be waivable by rebaselining, * which makes it a suggestion. (`bench/size-guard.ts` validates its own baseline for * exactly this reason; this follows it.) * * FAILS CLOSED * ------------ * Every way of not having measured is a failure, never a pass: a missing baseline, a * baseline of the wrong shape, an empty baseline, a report over zero corpus files, a * report where no site was instrumentable, a report whose grammar walk was incomplete * (`unresolvedRoots` non-empty), a baselined key that no longer exists, and a measured * key with no baseline entry. This repo already contains the alternative — `ratio: * ordered.length === 0 ? 1` in src/coverage.ts reports 100% covered when nothing was * analysable — and the whole point of a gate is to not be that. */ import type { WastedWorkReport } from './choice-cost.ts'; export type WastedWorkBaseline = { readonly schema: 'parseman.wasted-work-baseline/1'; /** Informational: which revision produced the numbers. Never compared. */ gitRev: string; /** Informational, `YYYY-MM-DD`. Never compared — comparing it would make the * verdict depend on the clock. */ updatedAt: string; /** * Absolute per-corpus totals, keyed by corpus id. * * `totalWastedBytes` is the INTERPRETED column, and that is deliberate even though * `WastedWorkReport` names `totalGatedWastedBytes` as the headline. For BYTES the two * are the same number, structurally: the modelled first-char guard is derived from the * arm's first SET, which over-approximates what the arm can start with, so wherever the * guard rejects, the arm's own leading terminal would have rejected at the same position * having consumed nothing. Measured over four dialect grammars, zero arms differ (see the * header of `choice-cost.ts`). What the guard removes is ATTEMPTS, not rescanned bytes — * so `gatedAttempts`, `gatedFailures` and the inversion ranking DO read the gated columns, * and only the byte totals are recorded from the interpreted one, where it is additionally * the conservative UPPER bound of the two. * * That identity is not enforced anywhere it could be relied on silently: `checkWastedWork` * judges the compiled column against this same band wherever the two columns part, so a * divergence goes red instead of unnoticed. */ totals: Record; /** Absolute per-site wasted bytes, keyed by `::`. Interpreted column, * for the reason given on `totals` above. */ sites: Record; }; export type WastedWorkPolicy = { /** * Maximum wasted bytes per corpus byte. A hard ceiling no rebaseline can waive. * Omit for drift-only gating; a project should set it once it knows its own number. */ ceilingRatio?: number; /** * Half-width of the band around each committed number, as a percentage. Default 1. * * SYMMETRIC: a measurement more than this ABOVE its baseline is a regression, and a * measurement more than this BELOW it is an unbanked win. Both fail. * * This is not a noise allowance — the metric is a deterministic count of input bytes, * so its noise floor is exactly zero and any non-zero value here is pure headroom. * It exists only so incidental one-byte churn does not thrash the baseline in both * directions. Set it as near zero as the corpus permits; `bench/choice-cost-guard.ts` * runs at 0.1, matching `bench/size-guard.ts`'s ratchet slack. */ driftTolerancePct?: number; /** * Fail on an ordering inversion — an arm that failed every one of its attempts while * a later arm at the same site matched. Off by default: on an existing grammar this * fires immediately, and a gate that is red on arrival for a pre-existing condition * gets disabled rather than fixed. Turn it on once the backlog is empty, to keep it * empty. */ failOnInversions?: boolean; }; export type GateBreach = { /** `drift` is growth past the band; `shrank` is an improvement that was not banked. * Both are failures, and the `detail` says which remedy applies. */ kind: 'ceiling' | 'drift' | 'shrank' | 'unbaselined' | 'stale' | 'inversion' | 'unmeasurable' | 'invalid-baseline'; key: string; /** One line. Says what happened and what number to look at — never advice. */ detail: string; }; export type GateVerdict = { readonly schema: 'parseman.wasted-work-verdict/1'; ok: boolean; /** Ascending by (kind, key). Deterministic, so a verdict is diffable. */ breaches: readonly GateBreach[]; checkedCorpora: number; checkedSites: number; }; /** * Judge one or more measured corpora against a committed baseline. * * `reports` is keyed by corpus id — a grammar is usually gated over more than one body * of input, and a single blended number would let a regression on one hide behind an * improvement on another. */ export declare function checkWastedWork(reports: Readonly>, baseline: unknown, policy?: WastedWorkPolicy): GateVerdict; /** * Build the baseline a passing measurement would record. * * Deliberately NOT called by the gate. It exists so a rebaseline is one explicit * command whose output is a file in the diff — the reviewable record — rather than * something the gate can do to itself on a red run. */ export declare function buildWastedWorkBaseline(reports: Readonly>, meta: { gitRev: string; updatedAt: string; }): WastedWorkBaseline; //# sourceMappingURL=choice-cost-gate.d.ts.map