/**
* 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