/** * Deterministic "undiscriminated catch" signal for PR reviews. * * Provenance: PR #752 (this repo) — `postPRReview`'s catch block salvaged * EVERY `createReview` error (auth, rate-limit, 5xx, network) the same way * it salvaged the one 422 anchor-validation failure the fallback was * designed for, silently degrading an infra outage into a success-shaped * `{posted: 0, dropped: }` result instead of rethrowing. CodeRabbit * caught it same-SHA; Lien Review's `error-swallowing` rule did not (3-vote * baseline: 0/3 — see * `test/harness/fixtures/error-swallowing/pr752-undiscriminated-catch-salvage`). * * Follow-up (2026-07-16): a re-calibration on the prod default model found * the fixture had plateaued at 0/10 (down from the 5/10 measured at ship * time) — every vote visited the right catch, then excused it by reading the * docstring on the ADJACENT `postBodyThenRetryCommentsIndividually` function * (which legitimately justifies THAT function's own inner catch) and * borrowing it for the DIFFERENT, actually-buggy catch in `postPRReview`. * The HEADER below was sharpened to require judging the flagged catch on its * own body, not a sibling's. Re-measured post-fix: still 0/10 (non-converging * — see the fixture's header for the full before/after trace analysis), but * the sibling-borrowing mistake itself was eliminated from every vote's * reasoning (traces now correctly separate the two catches by line number); * the plateau persists via an independently-reconstructed "it's reported via * the return value, so it's not really swallowing" rationalization instead — * a distinct failure mode from the one this fix targeted. * * The rule asks the agent to notice, for each catch block, whether it * actually discriminates between error classes before choosing to degrade * instead of rethrow — a judgment call that's easy to skip when a catch's * *shape* (try/catch, some logging, a fallback call) reads as "handled" at * a glance. This module pre-computes the STRUCTURAL fact instead, mirroring * the `` / `` precedents: find * every catch clause the diff ADDS or MODIFIES, and flag it when: * (a) its body never inspects the caught error's type/class/status (no * `instanceof`, no `.status`/`.code`/`.name`/etc. on the caught * binding); * (b) it does not unconditionally rethrow — the last statement not * nested inside a conditional block is not a `throw` (so at least one * path degrades instead of propagating); and * (c) it does something beyond pure logging — a call or a value-returning * `return` (an actual fallback/degrade path, not just "log and let it * fall through"). * A catch clause with NO binding (`catch { ... }`) is never flagged: it * cannot discriminate by construction (there is no reference to check), and * it is also an extremely common, usually-correct idiom for a best-effort * probe ("any failure here means use the safe default") — the byte-diff * census run while building this module caught it over-firing on exactly * that shape in this repo's own `worktree.ts`/`overlay-backend.ts` before * this exclusion was added. * * The result is injected as an `` block * so the agent confirms a handed-to-it candidate instead of re-reading * every catch body itself. * * v1 scope: TS/JS only, operating on the changed-file chunks the engine * already parses (`context.chunks`) — text/light-parsing (brace-depth * tracking to isolate a catch body, no full AST), consistent with this * file's siblings. Known limitations, kept honest rather than papered over: * - Bindingless catches are never flagged (see above) — a true positive * that happens to omit the binding (rare; nothing to check against * anyway) is invisible to this scan by design. * - "discrimination" is a shallow textual check on the bound identifier. * A check performed via a helper function (`if (isRetryable(err))`) is * invisible to this scan — it will still flag such a catch. * - "rethrows" is approximated as "the last statement not nested in a * block is a throw", not full control-flow/exhaustiveness analysis. A * body that rethrows inside a non-trailing branch (rather than as a * trailing statement) can still be flagged as a false positive. * - Catches with no textual overlap with the diff's added/changed lines * (i.e. untouched by this PR) are never considered, regardless of shape. * - Catch-shaped text inside a comment or string literal is excluded via a * masking pre-pass (see `maskNonCode`) — found over-firing on this * repo's own `.assertions.ts` fixtures, whose docstrings quote code * snippets like `catch { return false; }` as narrative prose. * - Masking treats a whole template literal (backtick string) as opaque, * including any `${...}` interpolation slots — real, executing code that * can itself contain a discrimination check (e.g. `` `LLM error: * ${err instanceof Error ? err.message : String(err)}` ``, used only to * format a log message, found via this module's own census against * `voting.ts`). Such a check is invisible to criterion (a), so a catch * that only inspects the error inside an interpolation slot is still * flagged as a candidate — the agent's own read of the actual code is * the backstop, per this block's "confirm before reporting" framing. */ import type { SignalContext } from './signal-context.js'; /** A catch clause this PR added or modified that appears to degrade indiscriminately. */ export interface UndiscriminatedCatchCandidate { file: string; /** Line of the `catch` keyword. */ line: number; /** Line of the catch block's closing brace. */ endLine: number; /** The caught binding's identifier, or null for a bindingless `catch { }`. */ binding: string | null; /** One-line explanation of why this catch was flagged. */ reason: string; } /** * Classify one catch body. Returns a one-line reason when it should be * flagged as an undiscriminated-degrade candidate, or null when it's exempt * (no binding to discriminate on, discriminates by error type, unconditionally * rethrows, or only logs). Exposed for direct unit testing of the heuristic, * independent of diff/chunk plumbing. */ export declare function classifyCatchBody(binding: string | null, body: string): string | null; /** * Find every catch clause this PR adds or modifies that appears to degrade * indiscriminately. Returns [] when there is no diff or no changed-file * chunks to scan. Exposed for testing. */ export declare function computeUndiscriminatedCatches(context: SignalContext): UndiscriminatedCatchCandidate[]; /** * Render undiscriminated-catch candidates as an * `` block for the agent's initial * message. Returns '' when there are no candidates so callers can append * unconditionally. Caps at MAX_CANDIDATES with an explicit omission note — * never truncates silently. Exposed for testing. */ export declare function renderUndiscriminatedCatchCandidates(candidates: UndiscriminatedCatchCandidate[]): string; /** * Build the `` section from the review * context. Returns '' when the PR adds/modifies no catch clause that fits * the shape, or there's no diff/changed-file chunks to scan. */ export declare function renderUndiscriminatedCatchSection(context: SignalContext): string; //# sourceMappingURL=catch-discrimination-signals.d.ts.map