/** * Deterministic "blast-radius for untouched docs" signal for PR reviews. * * doc-truth (see `doc-claims-signals.ts`) verifies the docs a PR TOUCHED against the code it * changed. This module covers the inverse, structurally symmetric gap: a PR REMOVES or RENAMES a * code symbol, or deletes a file/directory, and some OTHER doc — one the PR never opened — still * describes the old form as if it were current. That doc silently rots. * * Mirrors the `` / `` / `` precedents: * pre-compute the structural facts (which old forms are provably GONE, and which untouched * doc/config lines still name them) instead of asking the agent to grep-and-reason across the * whole repo on every PR. Three referand kinds, each backed by an existing extractor or a small * new one: * - a removed exported symbol (`extractRemovedExports`, `removed-export-signals.ts`); * - a renamed identifier's old name (`detectRenameSweeps`, `rename-sweep-signals.ts`); * - a deleted file/directory path (new: `isFullFileDeletion` below, parsed straight off the * hunk headers `getPRPatchData` already gives every plugin — no `diff --git` header needed, * since production patches are hunk-only). * * The raw signal — "some doc mentions some changed identifier" — fires on nearly every PR and is * useless as a gate on its own, the same trap `stale-literal-signals.ts`'s confidence tiering * exists to avoid. Two independent, deliberately narrow gates keep this selective: (1) the * referand set is restricted to REMOVED/RENAMED/DELETED forms, not merely changed ones — most PRs * add and modify but don't remove public surface; (2) each surviving reference is tiered by WHERE * in the doc it sits (a falsifiable behavioral claim, or a structural heading/bullet naming a now- * gone symbol/path) and, when in doubt, SUPPRESSED (fenced code samples, changelog/changeset * entries, a referand that sits ONLY inside a link/URL span, and past-tense/historical prose are * never candidates — that is the single biggest false-positive class for this shape of signal). * The link/URL suppression is deliberately NARROW — it fires only when the referand's own * occurrence sits inside the link markup, not merely because the line contains a link ANYWHERE * (this repo's dominant doc idiom cites an ADR link — `(see [ADR-012](docs/.../0012-...md))` — * right next to a genuine structural bullet; a blanket "line has a link" suppression silently ate * real deletion-drift candidates until this was narrowed). * * Referand tokens are deliberately the FULL path/identifier only — no generic shorter alternate * spelling is swept (a prior "trailing path segment" alt-token for deleted directories, e.g. * `packages/runner` -> `runner`, was tried and removed: a bare segment is often a common English * word — this repo's own packages include `core`/`cli`/`site`/`action`/`runner` — and produced * false candidates on unrelated ambient prose, e.g. a CI workflow comment about the GitHub Actions * "hosted runner" machine). Residual known limit, not fixed here: a bare TOP-LEVEL-directory * referand (e.g. `platform`) is itself already a single common word, so it carries the same * generic-word false-positive risk the removed alt-token had — a future distinctiveness/stopword * filter (minimum length, excluding common English words, requiring a directory-listing context) * is the natural fast-follow; precision-first v1 accepts this narrow residual risk rather than * building that filter now. * * This module only computes candidates; the dedicated pass that judges them * (`plugins/agent/docs-drift-pass.ts`, dark by default) lives separately (see the docs-drift * design doc, `.wip/docs-drift-design.md`, §2-3). `classifyRawDocReferences` exists purely so the * zero-LLM census (design doc §4) can show how much this tiering collapses the raw ~100%-of-PRs * signal down to a selective candidate rate. */ import type { CodeChunk } from '../types.js'; import type { SignalContext } from './signal-context.js'; /** Which deterministic extractor produced a referand's "the old form is gone" fact. */ export type ReferandKind = 'removed-export' | 'renamed-identifier' | 'deleted-path'; /** Where in the doc a surviving reference sits — the precision-tiering axis (see module header). */ export type PositionTier = 'behavioral-claim' | 'structural-mention'; /** * A surviving untouched-doc reference to a removed/renamed/deleted referand, tiered and past every * suppression check. The code-side hunk that proves the referand is gone is deliberately NOT * carried here — it is cheap to re-derive from `context.pr.patches` when a pass actually consumes * this candidate (mirrors `findRemovalHunk` in `removed-exports-pass.ts`), so this stays a minimal, * easily-serializable worklist entry. */ export interface DocsDriftCandidate { /** The old-form token this doc still names (a symbol, an old identifier, or a path). */ referand: string; referandKind: ReferandKind; docFile: string; /** 1-based line in the doc file's current (head) content. */ docLine: number; positionTier: PositionTier; /** A short window around the reference line, for a reviewer to judge without re-opening the file. */ excerpt: string; } /** One referand to sweep the untouched-doc corpus for — the FULL path/identifier only, no shorter * alternate spelling (see module header for why a generic trailing-segment alt-token was tried * and removed). */ interface Referand { token: string; kind: ReferandKind; } /** * True iff `patch` is a full-file deletion: it has at least one hunk, and EVERY hunk header's * new-side start is `0` (`@@ -a,b +0,0 @@`) — the shape a whole-file removal always produces, * whether or not the patch also carries a `diff --git`/`deleted file mode` header. Production * patches from `getPRPatchData` (`github-api.ts`, via `octokit.pulls.listFiles`) are HUNK-ONLY — * no such header is ever present — so this is the only extractor shape that works in both prod * and a fixture's full-header capture. Exposed for testing. */ export declare function isFullFileDeletion(patch: string): boolean; /** * True iff EVERY word-boundary occurrence of a BARE top-level directory referand (e.g. `platform`, * no `packages/` prefix) across the repo's doc/config corpus reads as a path/identifier — never as * ordinary prose describing something unrelated (e.g. "supports every platform", "the existing * platform .env"). A single prose hit disqualifies the referand: when in doubt, suppress (the same * precision-first posture as `isSuppressed`). * * This is the fast-follow the module header flags as a residual risk: a bare single-word directory * name is, by construction, generic-word risk that a `packages/` grouped referand or an * individual file path never carries (both always contain a `/`, so neither can spuriously match a * plain English word in the middle of a sentence). * * Delegates to `isDistinctiveToken` (`doc-reference-matching.ts`) — the same corpus-driven "does every * occurrence read as code/path context" check, shared with the CLI's edit-time docRefs lookup (see * `doc-reference-matching.ts`'s docstring). Kept as a named export here, rather than inlining the * import at call sites, because this module's own test suite exercises it directly under this name. * Verified against this repo's own corpus, `platform` reads as ordinary prose in both * `STYLE_GUIDE.md` ("...documentation site, platform app...") and the review harness's * `README.md` ("...the existing platform .env...") — exactly the false-positive risk this gate * exists to close. Exposed for testing. */ export declare function isDistinctiveBareDirectory(token: string, docChunks: CodeChunk[]): boolean; /** * Deleted-path referands: every fully-deleted file, grouped up to its containing package/top-level * directory when that whole directory is confirmed gone (see `directoryIsGone`) — the shape a * removed CLAUDE.md structure bullet describes (a directory, not one of its files) — else left as * the individual file's own path. A BARE top-level directory (grouped outside `packages/`, so its * token carries no `/`) additionally must pass `isDistinctiveBareDirectory` against the repo's own * doc/config corpus — failing that, this falls back to the individual file's own path (still full * recall for that file, just not the risky generic grouping). A `packages/` grouping is never * gated: it always carries a `/`, so it never risks matching incidental prose. Deduped. Full path * only — no trailing-segment alt-token (see module header). Exposed for testing. */ export declare function extractDeletedPaths(patches: Map, repoChunks: CodeChunk[] | undefined): Referand[]; /** * Compute the docs-drift candidates for a review: untouched doc/config lines that still name a * symbol this PR removed, an identifier it renamed, or a path it deleted — tiered by position and * past every suppression check (module header). Returns `[]` when there's no diff, no * removed/renamed/deleted referand, or no untouched doc/config corpus to sweep. Capped at * `MAX_CANDIDATES`, sorted deterministically (Tier-1 first, then referand, then doc file:line). */ export declare function computeDocsDriftCandidates(context: SignalContext): DocsDriftCandidate[]; /** Raw (untiered/unsuppressed) reference tally — see `classifyRawDocReferences`. */ export interface RawDocReferenceTally { total: number; tier1: number; tier2: number; suppressed: number; } /** * Census/debugging helper (not used by any pass): classifies every RAW word-boundary reference to * a removed/renamed/deleted referand in the untouched doc/config corpus — uncapped by * `MAX_REFS_PER_REFERAND` or `MAX_CANDIDATES` — into tier-1 / tier-2 / suppressed / (neither tier). * Exists so the zero-LLM docs-drift census can show how far the tiering discipline collapses the * raw reference count down to the selective candidate rate. `total` is the same count * `computeDocsDriftCandidates` would sweep before its own per-referand cap and position filtering. */ export declare function classifyRawDocReferences(context: SignalContext): RawDocReferenceTally; export {}; //# sourceMappingURL=docs-drift-signals.d.ts.map