/** * Deterministic "enum/union-variant consumer sweep" signal for PR reviews. * * Provenance: the omission-pass design doc (`.wip/omission-pass-design.md`, * §6 item 2) names this as the most promising, ready-to-build v1 item — * `incomplete-handling`'s prompt today just tells the agent to "grep for all * consumers" of a newly-added field/variant (`rules.ts`, the * `INCOMPLETE_HANDLING` rule) — the exact grep-and-reason anti-pattern * CLAUDE.md's design principle warns against, and the same anti-pattern * `removed-export-signals.ts` and `catch-discrimination-signals.ts` already * replaced for their own shapes (PR #770's catch-discrimination signal is * the direct template for this module's wiring). * * The shape: a PR ADDS a member to an enum, a new arm to a union type, or a * new key to a `const X = {...} as const` value-map — and a switch/if-chain/ * mapping-table elsewhere that enumerates the family's OTHER (pre-existing) * members was never updated to cover it. This module pre-computes both * halves of that fact deterministically: * 1. `computeAddedVariants` — parse each changed file's diff for a member * added to an EXISTING enum/union/const-object declaration (a brand * new declaration has no prior consumers to go stale, so it's not a * candidate). * 2. `computeVariantSweepContexts` — for each added variant, resolve the * family's full current (post-PR) membership from the changed-file * chunk, then sweep the head corpus (`repoChunks`) for a switch/ * if-chain/mapping site that references >= 2 of the family's OTHER * members but never mentions the new one. * * Conservative by design: a consumer must reference at least two EXISTING * members to count as "enumerating the family" — a single reference is far * too common a coincidence (e.g. one specific case handled on purpose, * unrelated identifier reuse) to be worth an agent's attention. Silence is * the correct default; the agent's own judgment (documented/intentional, * heuristic false positive) is the backstop for the rest, exactly per the * design doc's §2 "LLM judgment's job." * * v1 scope, stated honestly: * - TS/JS only (this repo's own surface). * - Three family shapes: `enum X { ... }`, `type X = A | B | C` (single- or * simple multi-line pipe-style, arms that are bare string/numeric * literals or plain identifiers only — an inline object-shaped arm like * `{ kind: 'x' }` is NOT parsed, a documented gap), and `const X = { * ...} as const` value-maps (the `as const` suffix is REQUIRED — an * ordinary object literal without it is deliberately never treated as a * variant family, so a coincidental object literal with keys that happen * to match some other family's member names is never misread as one; * this directly satisfies the "non-enum object literals" case). * - Consumer detection requires a DOT-QUALIFIED reference (`TypeName. * Member`) for enum/const-object members — the idiomatic, high-precision * form (`case Color.Red:`, `x === Color.Red`, `[Color.Red]: ...`). A * const-object key that isn't a valid identifier (e.g. kebab-case) falls * back to bracket notation (`Editors['claude-code']`) instead, since dot * access isn't valid syntax for it. Union members (string/numeric * literals, no natural qualifier) fall back to the bare quoted/keyed * literal value (unquoted for numeric arms), which is lower precision by * construction — documented, not papered over. * - A consumer site is only ever a `case` label, an equality comparison * (`===`/`==`/`!==`/`!=`), an `instanceof` check (union-of-identifier * arms), or an object-literal key (bare or computed `[Type.Member]`) — * not a full control-flow/exhaustiveness analysis. A family member * referenced only through a helper function is invisible to this scan, * same caveat `catch-discrimination-signals.ts` documents for its own * shallow textual check. * - Brace/body extraction for enum and const-object bodies stops at the * FIRST unmatched-depth `}` via simple depth counting with string/ * comment skipping — a member whose VALUE itself contains a nested * object is not expected for these idiomatic shapes and is a documented * limitation, not a crash. * - `isGenuinelyNew`'s "was this identifier ever removed" check scans the * WHOLE file's removed-diff-text, not just the lines belonging to this * variant's own family/declaration. A same-named member removed from an * unrelated declaration elsewhere in the same file's diff can therefore * suppress a genuine addition (false negative). Scoping this to the * owning declaration/hunk would need old-side-aware family parsing (the * diff-side scan is currently new-side only) — left as a known gap * rather than a v1 blocker; a coincidental same-named removal elsewhere * in the same file's diff is a narrow enough case that it wasn't judged * worth the parsing cost yet. */ import type { SignalContext } from './signal-context.js'; export type VariantFamilyKind = 'enum' | 'union' | 'const-object'; /** A member/arm/key this PR adds to an existing enum, union, or const-object family. */ export interface AddedVariant { typeName: string; variant: string; file: string; kind: VariantFamilyKind; } /** A site elsewhere in the head corpus that enumerates the family but omits the new variant. */ export interface VariantConsumerSite { file: string; line: number; /** The family's OTHER (pre-existing) members this site was found to reference. */ handledVariants: string[]; } /** One added variant with the stale consumer sites found for it. */ export interface VariantSweepContext { typeName: string; variant: string; file: string; kind: VariantFamilyKind; consumers: VariantConsumerSite[]; } /** * Find every enum member / union arm / const-object-as-const key this PR * adds to an EXISTING family declaration (a brand-new declaration has no * prior consumers, so it's never a candidate). Exposed for testing. */ export declare function computeAddedVariants(context: SignalContext): AddedVariant[]; interface ResolvedFamily { existingVariants: string[]; declStartLine: number; declEndLine: number; } /** * Re-locate the family's current (post-PR) declaration in the changed * file's chunks and return its members MINUS every variant this PR added to * it (`addedNames`) — i.e. the pre-existing membership a consumer would * have been written against. Null when the declaration can't be found * (defensive; the diff-side scan and this static re-scan use the same * parser, so this should only miss on exotic formatting). Exposed for * testing. */ export declare function resolveFamilyExisting(context: SignalContext, file: string, kind: VariantFamilyKind, typeName: string, addedNames: Set): ResolvedFamily | null; /** * The full `` worklist: every enum/union/ * const-object variant this PR added, paired with the (capped) consumer * sites found to enumerate the family's other members without it. Only * variants with at least one such site are included — there's nothing for * the agent to check otherwise. Exposed for testing. */ export declare function computeVariantSweepContexts(context: SignalContext): VariantSweepContext[]; /** * Render variant-sweep contexts as a `` block for * the agent's initial message. Returns '' when there are none so callers * can append unconditionally. Caps at MAX_ENTRIES and MAX_BLOCK_CHARS with * an explicit omission note — never truncates silently. Exposed for testing. */ export declare function renderVariantSweepCandidates(contexts: VariantSweepContext[]): string; /** * Build the `` section from the review context. * Returns '' when the PR adds no enum/union/const-object variant with a * stale consumer, or there's no diff/repo index to check against. */ export declare function renderVariantSweepSection(context: SignalContext): string; export {}; //# sourceMappingURL=variant-sweep-signals.d.ts.map