/** * Deterministic "unread field" signal for PR reviews — incomplete-handling's * THIRD omission shape. * * Provenance: the per-rule-loops design review (`.wip/per-rule-loops-design.md`, * gating-matrix row for `incomplete-handling`) names this as the one shape with * ZERO signal coverage today: `variant-sweep-signals.ts` covers added enum/ * union/const-object VARIANTS, `sibling-surface-signals.ts` covers a feature * silently missing from a FAMILY MEMBER file — but a plain interface member / * type-literal property / class field that's declared and never read anywhere * ("the plumbing to the consumer was forgotten") has no precomputed candidate * list; the rule's own prompt (`rules.ts`, `INCOMPLETE_HANDLING`) just tells * the agent to "grep for all consumers" — the exact grep-and-reason anti- * pattern CLAUDE.md's design principle warns against, and this module's own * motivating example (`RuleTriggers.filePatterns`, `rules.ts`'s `example` * field) is EXACTLY this shape. * * The shape: a PR adds a member to an EXISTING or brand-new interface / type * literal / class (TS/JS v1 scope) that has no dot-access, bracket-access, or * destructuring reference anywhere else in the indexed corpus — only its own * declaration. Two steps, both deterministic: * 1. `computeAddedFields` — parse each changed file's diff for a genuinely * new interface member / type-literal property / class field. * 2. `computeUnreadFieldCandidates` — for each added field, sweep the full * indexed corpus (`repoChunks`) for a read site; keep only fields with * none. * * ## Precision instrument, not recall instrument * * This is a "prove absence" signal, the hardest kind — a single missed read * pattern anywhere in a multi-hundred-chunk corpus is a false positive. Every * design choice below errs toward silence over a wrong candidate, per the * design brief: "when in doubt, suppress the candidate — the rule's LLM * judgment still exists without the signal." * * - **Wholesale consumption (spread / `JSON.stringify` / serialization / * shorthand-property hand-off).** If a `: TypeName` annotation has a * spread (`...x`), `JSON.stringify(`, or — see below — a shorthand- * property object-literal reference to the SAME annotated variable, * within `WHOLESALE_PROXIMITY_CHARS` characters after it anywhere in the * corpus, every field of that type is suppressed — we can't prove the * field isn't consumed as part of a whole-object pass-through, so we * don't claim it is unread. Deliberately type-level, not field-level (a * narrower per-field check would need real data-flow tracking this module * doesn't attempt), and proximity-based rather than a bare co-occurrence * check: an earlier version matched the type name and a spread/stringify * ANYWHERE in the same chunk, which false-fired on this module's OWN doc * comment naming `RuleTriggers` (found via dogfooding) — requiring an * actual type annotation, with comments masked out first, fixes that * specific false-suppression while keeping the check textual (not real * data-flow: the nearby spread/stringify need not touch the same * variable the annotation introduced). * The shorthand-property case is narrower and DOES tie back to the * annotated variable's own name (mining sweep, hono #4451 ground truth): * `app.fetch(req, { event, requestContext, context })` hands the whole * `requestContext` value (typed `LatticeRequestContextV2`, carrying the * PR's new `serviceNetworkArn` field) opaquely to another consumer via a * bare shorthand property — not a spread, not `JSON.stringify`, so the * original check missed it and flagged a real, actively-consumed field as * unread. `{ requestContext }` reads as "hand off the CURRENT value of * variable `requestContext`" the same way `...requestContext` would, so * it counts as the same wholesale evidence — scoped to the specific * variable name captured from the type annotation (`varName: TypeName`), * not "any object literal with 2+ shorthand keys anywhere nearby". * - **JSX attribute usage (`
`).** Read detection also * recognizes the field name appearing as a JSX attribute on any element — * invisible to the dot/bracket/destructure patterns above, since a JSX * attribute is compiled to a prop key, never a `.field`/`['field']` * access or a destructured binding in the SOURCE text. Motivating case * (mining sweep, zod's OG-image generator): Satori's custom JSX renderer * reads `HTMLAttributes.tw` (an inline-Tailwind escape hatch) exclusively * via `
`-shaped call sites. Bounded to a fixed character * window after the opening `` comparison operators can't make this * pattern's worst case anything other than linear). * - **Exported "public API" types.** A field on a type that's re-exported by * an `index.ts`/`index.js` barrel is suppressed — its real consumers may * live outside this indexed corpus (an external SDK consumer, or another * package this review run didn't index). Two ways in: a NAMED export * statement mentioning the type directly, or a wildcard `export * from * ''` whose specifier's basename matches the declaring file's * own basename. That basename match is required, not optional — an * earlier version treated ANY wildcard barrel found ANYWHERE in the * corpus as evidence, which (found via dogfooding) suppressed every * candidate in the entire codebase the moment a single, wholly unrelated * package had its own ordinary `export * from './foo.js'` barrel — nearly * universal in real code, and fatal to the signal's recall. It's still a * coarse, corpus-wide textual check, not a per-package public-surface * resolution: it can't tell a PUBLISHED package's real npm-facing barrel * from a PRIVATE monorepo package's own internal `index.ts` (both look * identical texturally), and a same-basename file in an unrelated * directory can still false-match the wildcard form — so it still * over-suppresses sometimes, just no longer catastrophically. A * documented false-negative-prone simplification, not a bug: fewer * candidates, never a wrong one. * - **Test-fixture files.** A field declared in a file matching the test-path * convention (`*.test.ts`, `__tests__/`, etc.) is skipped entirely — test * fixture objects routinely carry properties the test itself never reads * back out, and that's not a production bug. * - **Generated `.d.ts` declaration files.** A field declared in a `.d.ts` * file whose name carries a codegen marker (`-bundle`, `-generated`/ * `.generated.`, `.gen.`) or whose leading content carries a generator * marker comment (`@generated`, `DO NOT EDIT`, "automatically generated") * is skipped the same way a test-fixture file is. Motivating case (mining * sweep, drizzle-kit): `grammar.ohm-bundle.d.ts`, an Ohm.js-generated * grammar action-dictionary type listing EVERY grammar rule as an * optional handler property — the exact same "declared, most never * populated/read by any one consumer" shape as a test fixture, just for * generated parser code instead. Deliberately narrow: a HAND-WRITTEN * `.d.ts` (an ambient module declaration a person authored) carries * neither signal and is NOT suppressed — blindly suppressing every * `.d.ts` would hide real gaps in hand-authored type-only files. * - **Dynamic/bracket access.** Read detection covers `obj.field`, * `obj['field']`/`obj["field"]` (bracket string-literal key access), and * both destructuring shapes (`const { field } = x`, `function f({ field })`) * — not just the naive dot-access form other signals use for qualified * enum/const-object references. A TRULY dynamic key (`obj[someVar]` where * `someVar` happens to equal the field name at runtime) is undetectable * statically and stays a documented gap. * * ## v1 scope, stated honestly * * - TS/JS only (this repo's own surface, mirrors every other TS/JS-scoped * signal in this file set). * - Three declaration shapes: `interface X { field: T; }`, `type X = { field: * T; }` (a DIRECT object-type-literal assignment only — `type X = A & { * field: T }` or a generic-wrapped object type is not parsed, a documented * gap, same tradeoff `variant-sweep-signals.ts` makes for its union shape), * and a single-physical-line TYPED class property (`private readonly * field: T;`, `field?: T;`, `field: T = default;`) — an untyped inferred * field (`field = value;`, no `:`), a decorator-prefixed field * (`@Input() field: T;`), or a multi-line field type are not detected; * narrowing scope here trades recall for never misreading a getter/setter/ * method as a field (see `CLASS_PROPERTY_LINE_RE`'s doc for why). * - Unlike `variant-sweep-signals.ts`, the containing declaration does NOT * need to have existed before this PR — a brand-new interface whose field * nobody reads is the same bug shape (the rule's own `RuleTriggers` * example could plausibly have introduced `filePatterns` alongside a * brand-new interface, not just added it to an old one). * - This module does not verify the field is ever POPULATED (an object * literal setting it, or a `.field = value` assignment) — it fires purely * on "declared + never read". A field that's also never populated is * arguably a smaller bug (dead code) than one that's silently populated * and dropped, but distinguishing the two needs a construction-site trace * this module doesn't attempt; left to the agent's own judgment. * - A member is a genuine ADDITION using the same `isGenuinelyNew` technique * `variant-sweep-signals.ts` uses: its identifier must not appear on a * REMOVED line of this file's diff. A rename (old field removed, new one * added) still counts as an addition — the same accepted overlap with * `rename-sweep-signals.ts` variant-sweep documents for its own shape. */ import type { SignalContext } from './signal-context.js'; export type UnreadFieldKind = 'interface' | 'type-literal' | 'class'; /** A field this PR adds to an interface / type literal / class with no read site found. */ export interface UnreadFieldCandidate { typeName: string; field: string; file: string; line: number; kind: UnreadFieldKind; } /** One field this PR genuinely added, with its containing declaration. */ export interface AddedField { typeName: string; field: string; file: string; line: number; kind: UnreadFieldKind; } /** * Find every interface member / type-literal property / class field this PR * genuinely adds (unlike `variant-sweep-signals.ts`, the containing * declaration need NOT have existed before this PR — see module doc). Exposed * for testing. */ export declare function computeAddedFields(context: SignalContext): AddedField[]; /** * The full `` worklist: every interface member / * type-literal property / class field this PR added that has no read site * anywhere else in the indexed corpus. Exposed for testing. */ export declare function computeUnreadFieldCandidates(context: SignalContext): UnreadFieldCandidate[]; /** * Render unread-field candidates as an `` block for * the agent's initial message. Returns '' when there are none so callers can * append unconditionally. Caps at MAX_CANDIDATES and MAX_BLOCK_CHARS with an * explicit omission note — never truncates silently. Exposed for testing. */ export declare function renderUnreadFieldCandidates(candidates: UnreadFieldCandidate[]): string; /** * Build the `` section from the review context. * Returns '' when the PR adds no interface/type-literal/class field with no * read site, or there's no diff/repo index to check against. */ export declare function renderUnreadFieldSection(context: SignalContext): string; //# sourceMappingURL=unread-field-signals.d.ts.map