Skip to content

Commit 044c77a

Browse files
authored
feat(enrichment): blame-to-PR regression linker analyzer (#2034) (#2620)
* feat(enrichment): blame-to-PR regression linker analyzer (#2034) New REES github-light analyzer: for files a PR modifies/deletes, resolves which prior PR most recently introduced that region and surfaces it, giving the reviewer instant context on what the change is altering. Bounded + fail-safe. - types.ts: BlameLinkFinding ({ file, line, introducedByPr?, introducedByShaPrefix? }) + blameLink? key on BriefFindings (sha prefix only, never contents). - analyzers/blame-link.ts: firstTouchedOldLine() parses the patch for a representative modified/deleted old line; per file, reads the path's most recent base-branch commit and maps it to its PR via the commit->PR association API. Strict maxFilesProbed (6) + maxLookups (12) caps; emits partial on cap; skips added files and pure additions; no-token/bad-slug/fetch-error -> no finding. - registry.ts: descriptor (category history, cost github-light, requires files+github-token) with an inline render(). - render.ts: dispatch the blameLink section via renderDescriptorSection. - Regenerated analyzer-metadata.json, .env.example, and the UI metadata; added blameLink to the registry meta-test's expected list. - test/blame-link.test.ts: resolved PR, sha-only fallback, unresolvable (no finding), added/pure-addition skip, file-probe cap, token-absent skip. review-enrichment test suite: 400/400 pass. * fix(enrichment): skip the no-newline diff marker in blame-link patch parser (#2034) firstTouchedOldLine treated a `\ No newline at end of file` marker line as a context line, wrongly advancing the old-file line counter. Skip it as metadata (like the stray ---/+++ headers). Added a test covering the marker. review-enrichment: 400/400 pass. * fix(enrichment): blame-link deleted-file + old-line clarity fixes (#2034) Address review nits: - Handle removed files without requiring a patch: status === "removed" now blames via the file path alone (anchored to line 1), so a deleted file with a binary/truncated diff is no longer silently skipped despite the analyzer claiming to cover deletes. - Render the coordinate explicitly as `(old line N)` instead of `file:line`, which read like a current line — the value is the OLD-file line. - Give the commit→PR association call its own diagnostics subcall ("github-commit-pulls") instead of sharing "github-commits", for precise per-subcall diagnostics (shared endpoint budget unchanged). - Tests: a shifted-hunk case pinning the old-line semantics (+ its render) and a patchless removed-file case. review-enrichment: 402/402 pass. * refactor(enrichment): soften blame-link wording + precise category + tighter cap test (#2034) Final review nits (all non-blocking): - Render says the file "was most recently touched by" instead of "introduced by" a specific line — accurate to the file-level path-history lookup this does. - Give the commit→PR association call its own endpointCategory ("github-commit-pulls") for precise rate attribution (loop-level MAX_LOOKUPS still bounds the total). - Cap test now asserts exactly 12 fetch calls and that files past the probe cap (f6..f9) are never queried, pinning the bound. review-enrichment: 402/402 pass. * refactor(enrichment): reframe blame-link as file-level last-touch, not line origin (#2034) Resolve the reviewer's false-attribution concern: the analyzer only does a file-level latest-commit lookup, so it must not word its result as introducing a specific line. Make the claim match the behavior (no line-aware blame added). - Rename BlameLinkFinding.introducedBy{Pr,ShaPrefix} → lastTouchedBy{Pr,ShaPrefix}; the `line` field is now documented as a pointer to where THIS PR changes the file, not a line attributed to the PR. - Reframe type doc, analyzer docstring, descriptor title/docs, and the rendered section as "last PR to touch each file — file-level context, bounded scan". - Harden firstTouchedOldLine: only a space-prefixed context line advances the old cursor; additions, the `\` marker, and malformed/extended text no longer count, so a garbled patch fails closed. - Tests: rename fields; add a false-attribution regression (latest file commit touched a different region than the changed line → reported file-level, never attributed to the line) and a malformed-patch fail-closed case. review-enrichment: 403/403 pass. * feat(enrichment): resolve renamed-file history against the old path (#2034) A renamed file lives under its OLD path in the base tree, so blame-link now looks up commit history via `previousPath` for status === "renamed" (falling back to line 1 when a pure rename carries no usable patch), while still displaying the new path to the reviewer. Also trims the rendered section title (was em-dash-heavy and longer than neighboring headings). - Test: a renamed file is probed under its old path and rendered as the new path. review-enrichment: 404/404 pass. * fix(enrichment): treat every in-hunk `-` line as a deletion, incl. `--`/`---` content (#2034) The previous hardening added a `!startsWith("---")` guard to firstTouchedOldLine, but inside a hunk that wrongly skips a real deletion whose CONTENT begins with two hyphens (git renders a deleted `--x` line as `---x`). File headers only appear before a hunk, which the inHunk flag already excludes, so the guard was both wrong and unnecessary — dropped it. Added assertions for `---`/`--`-content deletions. review-enrichment: 404/404 pass. --------- Co-authored-by: Nick M <274344962+nickmopen@users.noreply.github.com>
1 parent f540531 commit 044c77a

9 files changed

Lines changed: 485 additions & 3 deletions

File tree

.env.example

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,17 @@ GITTENSORY_REVIEW_ENRICHMENT=false
6565
# Current analyzer names:
6666
# dependency,lockfileDrift,secret,license,installScript,heavyDependency,actionPin,eol,redos
6767
# provenance,codeowners,secretLog,assetWeight,typosquat,commitSignature,iacMisconfig,nativeBuild
68-
# history,docCommentDrift,duplication,churnHotspot
68+
# history,docCommentDrift,duplication,churnHotspot,blameLink
6969
#
7070
# Profile defaults:
7171
# fast: dependency,lockfileDrift,secret,license,installScript,heavyDependency,actionPin,eol
7272
# redos,provenance,secretLog,typosquat,iacMisconfig,nativeBuild
7373
# balanced (default): dependency,lockfileDrift,secret,license,installScript,heavyDependency
7474
# actionPin,eol,redos,provenance,codeowners,secretLog,assetWeight,typosquat,commitSignature
75-
# iacMisconfig,nativeBuild,history,docCommentDrift,duplication,churnHotspot
75+
# iacMisconfig,nativeBuild,history,docCommentDrift,duplication,churnHotspot,blameLink
7676
# deep: dependency,lockfileDrift,secret,license,installScript,heavyDependency,actionPin,eol
7777
# redos,provenance,codeowners,secretLog,assetWeight,typosquat,commitSignature,iacMisconfig
78-
# nativeBuild,history,docCommentDrift,duplication,churnHotspot
78+
# nativeBuild,history,docCommentDrift,duplication,churnHotspot,blameLink
7979
# END GENERATED REES ANALYZERS
8080

8181
# Submitter-reputation spend control (internal-only): downgrades new/burst/low-rep

apps/gittensory-ui/src/lib/rees-analyzers.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,31 @@ export const REES_ANALYZERS = [
559559
"Distinct from the history analyzer's author track record; this scores the change AREA's defect density.",
560560
},
561561
},
562+
{
563+
name: "blameLink",
564+
title: "Recent file history (last PR to touch)",
565+
category: "history",
566+
cost: "github-light",
567+
defaultEnabled: true,
568+
profiles: ["balanced", "deep"],
569+
requires: ["files", "github-token"],
570+
limits: {
571+
maxFilesProbed: 6,
572+
maxLookups: 12,
573+
},
574+
docs: {
575+
summary:
576+
"For files this PR modifies or deletes, surfaces the last PR to touch each file — file-level history context, not per-line blame.",
577+
looksAt:
578+
"Each changed file's most recent base-branch commit (bounded to the first few files) and that commit's associated PR.",
579+
reports:
580+
"File, a pointer to where this PR changes it, the last-touching PR number, and a short commit-SHA prefix — never file contents.",
581+
network:
582+
"Calls the GitHub commits API and the commit→PR association API, both bounded by a total lookup cap.",
583+
notes:
584+
"File-level, not per-line: it reports each file's most recent prior toucher, never claiming a specific line's origin. Fail-safe and partial on cap.",
585+
},
586+
},
562587
] as const satisfies readonly ReesAnalyzerDoc[];
563588

564589
export const REES_ANALYZER_NAMES = REES_ANALYZERS.map((analyzer) => analyzer.name);

review-enrichment/analyzer-metadata.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,32 @@
636636
"network": "Calls the GitHub commits API once per probed file. Requires GitHub token forwarding for private repos.",
637637
"notes": "Distinct from the history analyzer's author track record; this scores the change AREA's defect density."
638638
}
639+
},
640+
{
641+
"name": "blameLink",
642+
"title": "Recent file history (last PR to touch)",
643+
"category": "history",
644+
"cost": "github-light",
645+
"defaultEnabled": true,
646+
"profiles": [
647+
"balanced",
648+
"deep"
649+
],
650+
"requires": [
651+
"files",
652+
"github-token"
653+
],
654+
"limits": {
655+
"maxFilesProbed": 6,
656+
"maxLookups": 12
657+
},
658+
"docs": {
659+
"summary": "For files this PR modifies or deletes, surfaces the last PR to touch each file — file-level history context, not per-line blame.",
660+
"looksAt": "Each changed file's most recent base-branch commit (bounded to the first few files) and that commit's associated PR.",
661+
"reports": "File, a pointer to where this PR changes it, the last-touching PR number, and a short commit-SHA prefix — never file contents.",
662+
"network": "Calls the GitHub commits API and the commit→PR association API, both bounded by a total lookup cap.",
663+
"notes": "File-level, not per-line: it reports each file's most recent prior toucher, never claiming a specific line's origin. Fail-safe and partial on cap."
664+
}
639665
}
640666
]
641667
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// Blame-to-PR regression linker (#2034, part of #1499). For files this PR MODIFIES or DELETES, surfaces the prior
2+
// PR that most recently touched that FILE, so the reviewer sees at a glance what recent history the change sits on
3+
// top of. It is deliberately NOT per-line blame (no checkout, no blame API): for each touched file it reads the
4+
// path's most recent commit on the base branch and maps that commit to its PR via the commit→PR association API —
5+
// so the result is file-level "last touched by", never a claim that the surfaced PR introduced a specific line.
6+
// Bounded (maxFilesProbed + maxLookups) and fail-safe — any missing token, bad slug, or fetch error yields no
7+
// finding rather than an error. Surfaces only a PR number and a short SHA prefix, never contents.
8+
import type {
9+
AnalyzerDiagnostics,
10+
EnrichRequest,
11+
BlameLinkFinding,
12+
} from "../types.js";
13+
import type { AnalysisContext } from "../analysis-context.js";
14+
import { boundedFetchJson } from "../external-fetch.js";
15+
16+
const GITHUB_API = "https://api.github.com";
17+
const SLUG_RE = /^[A-Za-z0-9._-]+$/;
18+
const MAX_FILES_PROBED = 6; // bound the files we probe, matching the other history-class analyzers
19+
const MAX_LOOKUPS = 12; // hard cap on total GitHub round-trips (each file costs up to 2: commits + pulls)
20+
const SHA_PREFIX_LEN = 12;
21+
// Files whose commit history is not a useful "who introduced this" signal — lockfiles, generated output, binaries.
22+
const SKIP_RE =
23+
/(?:^|\/)(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml|poetry\.lock|go\.sum)$|\.(?:lock|min\.js|map|snap|png|jpe?g|gif|svg|ico|pdf|zip|gz|woff2?)$|(?:^|\/)(?:dist|build|vendor)\//i;
24+
25+
interface ScanOptions {
26+
signal?: AbortSignal;
27+
analysis?: Pick<AnalysisContext, "fetchJson">;
28+
diagnostics?: AnalyzerDiagnostics;
29+
}
30+
31+
/** The slice of a GitHub commit-list item this analyzer reads. */
32+
interface CommitListItem {
33+
sha?: string;
34+
}
35+
/** The slice of a commit→PR association item this analyzer reads. */
36+
interface AssociatedPr {
37+
number?: number;
38+
}
39+
40+
/**
41+
* The old-file line number of the FIRST line this patch modifies or deletes, or null when the patch only ADDS
42+
* lines (nothing pre-existing is being altered, so there is no prior author to attribute). Walks unified-diff
43+
* hunks: a `@@ -old,+new @@` header resets the old-line cursor; a deletion line reports the cursor; only a real
44+
* space-prefixed CONTEXT line advances it. Additions, the `\ No newline` marker, and any malformed/extended patch
45+
* text are NOT counted as old-file lines, so a garbled patch fails closed (returns null) rather than reporting a
46+
* drifted line. Pure. */
47+
export function firstTouchedOldLine(patch: string): number | null {
48+
let oldLine = 0;
49+
let inHunk = false;
50+
for (const raw of patch.split("\n")) {
51+
const header = raw.match(/^@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@/);
52+
if (header) {
53+
oldLine = Number(header[1]);
54+
inHunk = true;
55+
continue;
56+
}
57+
if (!inHunk) continue; // file headers (`---`/`+++`) only appear before a hunk; the flag skips them
58+
// Inside a hunk EVERY `-`-prefixed line is an old-file deletion — including content that itself starts with
59+
// `--`/`---` (git renders a deleted `--x` line as `---x`). The marker is the first char; the rest is content.
60+
if (raw.startsWith("-")) return oldLine; // first modified/deleted old-file line
61+
if (raw.startsWith(" ")) oldLine += 1; // a real context line — the ONLY thing that advances the old cursor
62+
// Everything else (additions, `\`/`+` markers, malformed text) is not an old-file line: do not advance.
63+
}
64+
return null;
65+
}
66+
67+
function githubHeaders(token: string): Record<string, string> {
68+
return {
69+
Authorization: `Bearer ${token}`,
70+
Accept: "application/vnd.github+json",
71+
"X-GitHub-Api-Version": "2022-11-28",
72+
};
73+
}
74+
75+
async function fetchGithubJson<T>(
76+
url: string,
77+
headers: Record<string, string>,
78+
fetchFn: typeof fetch,
79+
signal: AbortSignal | undefined,
80+
options: Pick<ScanOptions, "analysis" | "diagnostics">,
81+
subcall: string,
82+
endpointCategory: string,
83+
): Promise<T | null> {
84+
const fetchOptions = {
85+
endpointCategory,
86+
headers,
87+
signal,
88+
fetchImpl: fetchFn,
89+
diagnostics: options.diagnostics,
90+
phase: "blame-link",
91+
subcall,
92+
maxBytes: 256 * 1024,
93+
maxCallsPerCategory: MAX_LOOKUPS,
94+
};
95+
const response = options.analysis
96+
? await options.analysis.fetchJson<T>(url, fetchOptions)
97+
: await boundedFetchJson<T>(url, fetchOptions);
98+
return response.ok ? response.data : null;
99+
}
100+
101+
/** The SHA of the most recent commit touching `path` on the base branch, or null. Anchoring to `baseSha` keeps
102+
* the PR's own commits out of the answer so we attribute PRIOR authorship, not this change. */
103+
export async function fetchLatestCommitSha(
104+
owner: string,
105+
repo: string,
106+
path: string,
107+
baseSha: string | undefined,
108+
headers: Record<string, string>,
109+
fetchFn: typeof fetch,
110+
signal: AbortSignal | undefined,
111+
options: Pick<ScanOptions, "analysis" | "diagnostics">,
112+
): Promise<string | null> {
113+
const shaQuery = baseSha ? `&sha=${encodeURIComponent(baseSha)}` : "";
114+
const url =
115+
`${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits` +
116+
`?path=${encodeURIComponent(path)}&per_page=1${shaQuery}`;
117+
const commits = await fetchGithubJson<CommitListItem[]>(
118+
url, headers, fetchFn, signal, options, "github-commits", "github-commits",
119+
);
120+
const sha = Array.isArray(commits) ? commits[0]?.sha : undefined;
121+
return typeof sha === "string" && sha ? sha : null;
122+
}
123+
124+
/** The number of the PR that a commit belongs to, via the commit→PR association API, or null when unassociated. */
125+
export async function fetchPrForCommit(
126+
owner: string,
127+
repo: string,
128+
sha: string,
129+
headers: Record<string, string>,
130+
fetchFn: typeof fetch,
131+
signal: AbortSignal | undefined,
132+
options: Pick<ScanOptions, "analysis" | "diagnostics">,
133+
): Promise<number | null> {
134+
const url = `${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits/${encodeURIComponent(sha)}/pulls`;
135+
const pulls = await fetchGithubJson<AssociatedPr[]>(
136+
url, headers, fetchFn, signal, options, "github-commit-pulls", "github-commit-pulls",
137+
);
138+
const number = Array.isArray(pulls) ? pulls[0]?.number : undefined;
139+
return typeof number === "number" ? number : null;
140+
}
141+
142+
/** Analyzer entrypoint: changed files that alter existing lines → the prior PR/commit that introduced them.
143+
* Fail-safe — no token, bad slug, or fetch error yields no finding. Stops and returns partial results at the
144+
* lookup cap. */
145+
export async function scanBlameLink(
146+
req: EnrichRequest,
147+
fetchFn: typeof fetch = fetch,
148+
options: ScanOptions = {},
149+
): Promise<BlameLinkFinding[]> {
150+
const { repoFullName, githubToken, baseSha, files = [] } = req;
151+
if (!githubToken) return [];
152+
const parts = repoFullName.split("/");
153+
const owner = parts[0];
154+
const repo = parts[1];
155+
if (parts.length !== 2 || !owner || !repo || !SLUG_RE.test(owner) || !SLUG_RE.test(repo)) return [];
156+
157+
const headers = githubHeaders(githubToken);
158+
// Only files that alter pre-existing lines can be blamed: skip added files, generated/binary paths, and pure
159+
// additions (a patch with no deletion line has no prior author to attribute).
160+
const candidates: Array<{ lookupPath: string; displayPath: string; line: number }> = [];
161+
for (const file of files) {
162+
if (file.status === "added" || SKIP_RE.test(file.path)) continue;
163+
let line = file.patch ? firstTouchedOldLine(file.patch) : null;
164+
// A removed OR renamed file resolves against the base tree even without a usable patch (binary/truncated, or a
165+
// pure rename with no content change). Anchor to line 1 as the representative point.
166+
if (line === null && (file.status === "removed" || file.status === "renamed")) line = 1;
167+
if (line === null) continue; // a modified file with only additions / no usable patch → nothing to blame
168+
// The base tree holds a renamed file under its OLD path, so resolve history against `previousPath` while still
169+
// showing the reviewer the new (display) path.
170+
const lookupPath = file.status === "renamed" && file.previousPath ? file.previousPath : file.path;
171+
candidates.push({ lookupPath, displayPath: file.path, line });
172+
if (candidates.length >= MAX_FILES_PROBED) break;
173+
}
174+
175+
const findings: BlameLinkFinding[] = [];
176+
let lookups = 0;
177+
for (const { lookupPath, displayPath, line } of candidates) {
178+
if (options.signal?.aborted) break;
179+
if (lookups >= MAX_LOOKUPS) break; // cap reached → emit partial
180+
lookups += 1;
181+
const sha = await fetchLatestCommitSha(owner, repo, lookupPath, baseSha, headers, fetchFn, options.signal, options);
182+
if (!sha) continue; // no prior commit on the path → no finding
183+
let lastTouchedByPr: number | null = null;
184+
if (lookups < MAX_LOOKUPS && !options.signal?.aborted) {
185+
lookups += 1;
186+
lastTouchedByPr = await fetchPrForCommit(owner, repo, sha, headers, fetchFn, options.signal, options);
187+
}
188+
findings.push({
189+
file: displayPath,
190+
line,
191+
lastTouchedByShaPrefix: sha.slice(0, SHA_PREFIX_LEN),
192+
...(lastTouchedByPr !== null ? { lastTouchedByPr } : {}),
193+
});
194+
}
195+
return findings;
196+
}

review-enrichment/src/analyzers/registry.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { scanActionPins } from "./actions-pin.js";
22
import { scanAssetWeight } from "./asset-weight.js";
33
import { scanChurnHotspot } from "./churn-hotspot.js";
4+
import { scanBlameLink } from "./blame-link.js";
45
import { scanCodeowners } from "./codeowners.js";
56
import { scanCommitSignature } from "./commit-signature.js";
67
import { dependencyAnalyzer } from "./dependency/descriptor.js";
@@ -440,6 +441,43 @@ export const ANALYZER_DESCRIPTORS = [
440441
run: (req, { signal, analysis, diagnostics }) =>
441442
scanChurnHotspot(req, fetch, { signal, analysis, diagnostics }),
442443
}),
444+
descriptor({
445+
name: "blameLink",
446+
title: "Recent file history (last PR to touch)",
447+
category: "history",
448+
cost: "github-light",
449+
defaultEnabled: true,
450+
requires: ["files", "github-token"],
451+
limits: { maxFilesProbed: 6, maxLookups: 12 },
452+
docs: {
453+
summary:
454+
"For files this PR modifies or deletes, surfaces the last PR to touch each file — file-level history context, not per-line blame.",
455+
looksAt:
456+
"Each changed file's most recent base-branch commit (bounded to the first few files) and that commit's associated PR.",
457+
reports: "File, a pointer to where this PR changes it, the last-touching PR number, and a short commit-SHA prefix — never file contents.",
458+
network: "Calls the GitHub commits API and the commit→PR association API, both bounded by a total lookup cap.",
459+
notes:
460+
"File-level, not per-line: it reports each file's most recent prior toucher, never claiming a specific line's origin. Fail-safe and partial on cap.",
461+
},
462+
render: (findings, helpers) => {
463+
if (!findings.length) return [];
464+
const lines = ["### Recent history of changed files (last PR to touch each, file-level)"];
465+
for (const item of findings) {
466+
const toucher =
467+
item.lastTouchedByPr !== undefined
468+
? `#${item.lastTouchedByPr}`
469+
: item.lastTouchedByShaPrefix
470+
? `commit ${helpers.safeCodeSpan(item.lastTouchedByShaPrefix)}`
471+
: "an unknown prior change";
472+
lines.push(
473+
`- ${helpers.safeCodeSpan(item.file)} (this PR changes it around old line ${item.line}) was last touched by ${toucher}`,
474+
);
475+
}
476+
return lines;
477+
},
478+
run: (req, { signal, analysis, diagnostics }) =>
479+
scanBlameLink(req, fetch, { signal, analysis, diagnostics }),
480+
}),
443481
] as const satisfies readonly AnyAnalyzerDescriptor[];
444482

445483
export const ANALYZER_NAMES = ANALYZER_DESCRIPTORS.map(

review-enrichment/src/render.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,8 @@ export function renderBrief(
379379
}
380380
}
381381

382+
lines.push(...renderDescriptorSection("blameLink", findings.blameLink));
383+
382384
if (!lines.length) return { promptSection: "", systemSuffix: "" };
383385

384386
const header =

review-enrichment/src/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,22 @@ export interface ChurnHotspotFinding {
285285
capped: boolean;
286286
}
287287

288+
/** For a changed file that MODIFIES or DELETES existing lines, the prior PR (or commit) that most recently touched
289+
* that FILE — resolved from the path's latest base-branch commit + the commit→PR association API. This is
290+
* FILE-LEVEL context (the last change to land on the file before this PR), not per-line blame: it does not claim
291+
* the surfaced PR introduced any specific line. Surfaces only a PR number and a short SHA prefix, never file
292+
* contents. (#2034, part of #1499) */
293+
export interface BlameLinkFinding {
294+
file: string;
295+
/** A representative old-file line from THIS PR's change (its first modified/deleted line) — a pointer to where
296+
* the change lands, NOT a line attributed to `lastTouchedByPr`. */
297+
line: number;
298+
/** The last PR to touch this file before the change, when its commit maps to one via the commit/PR-association API. */
299+
lastTouchedByPr?: number;
300+
/** Short prefix of that most-recent commit's SHA (prefix only — never the full SHA). */
301+
lastTouchedByShaPrefix?: string;
302+
}
303+
288304
/** Structured analyzer output. Each analyzer fills its own key; more land as analyzers ship (#1477/#1478). */
289305
export interface BriefFindings {
290306
dependency?: DependencyFinding[];
@@ -308,6 +324,7 @@ export interface BriefFindings {
308324
docCommentDrift?: DocCommentDriftFinding[];
309325
duplication?: DuplicationFinding[];
310326
churnHotspot?: ChurnHotspotFinding[];
327+
blameLink?: BlameLinkFinding[];
311328
}
312329

313330
/** A JSDoc/TSDoc block whose `@param` tags name parameters the adjacent function no longer declares — a

review-enrichment/test/analyzer-registry.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const EXPECTED_ANALYZERS = [
3131
"docCommentDrift",
3232
"duplication",
3333
"churnHotspot",
34+
"blameLink",
3435
];
3536

3637
test("analyzer descriptors cover the runtime registry in stable order", () => {

0 commit comments

Comments
 (0)