feat: add per-file +/− summary and noise-filtered scope to review prompts

Emit a ### Changed Files Markdown table (| File | +/− | Type | rows plus
total added/removed) ahead of the raw diff in both committed and
uncommitted review prompts, parsed from the diff via the shared
parseDiff engine. Add an ### Excluded Files (n) section listing filtered
noise (path, +/− counts, reason), and replace byte-truncation of oversized
diffs with a file-list + read instruction when the cleaned diff exceeds
50KB or touches more than 20 files.

Also: distinguish diff-computation failure from genuinely no changes in the
review loop (tri-state result, never treats a broken base ref as a clean
verified task), map critical→blocker in finding severity parsing, inject a
per-review custom focus/instructions section from config, and make the
noise-filter ignore rules project-configurable via review.extraIgnorePatterns
and review.ignorePaths. Add same-model retry before cycling to the next
model in task/follow-up/fix sessions.
This commit is contained in:
2026-08-08 18:10:08 -04:00
parent 88f6b4df93
commit b86174782f
9 changed files with 698 additions and 95 deletions

View File

@@ -910,17 +910,46 @@ export function captureGitHead(projectDir: string): string | undefined {
* made since the base reference. Used by the review-gated loop so the reviewer
* sees the full task diff (all commits, not just the latest) across execution
* attempts and re-execution fixes. `baseRef` must be a validated hex SHA from
* captureGitHead(). Returns the short HEAD hash, HEAD subject, and range diff,
* or null when git is unavailable / baseRef is invalid / no changes exist.
* captureGitHead().
*
* Returns a tri-state so the review loop can tell a FAILED range computation
* (invalid/stale base ref, git error) apart from a GENUINELY EMPTY range — a
* broken base must never be silently treated as a clean, verified task.
*/
export type CommitRangeDiffResult =
| { kind: "ok"; hash: string; subject: string; diff: string }
| { kind: "no-changes" }
| { kind: "error"; error: string };
/**
* Whether the `baseRef..HEAD` range can be computed — i.e. the base ref is a
* resolvable commit in this repo (mirrors @piex-dev/review's canCompareToBase).
* Only validated hex SHAs are passed to the shell.
*/
export function canComputeRange(projectDir: string, baseRef: string): boolean {
const { execSync } = require("node:child_process");
if (!/^[0-9a-f]{7,40}$/i.test(baseRef)) return false;
try {
execSync(`git rev-parse --verify ${baseRef}`, {
cwd: projectDir,
stdio: "pipe",
});
return true;
} catch {
return false;
}
}
export function getCommitRangeDiff(
projectDir: string,
baseRef: string,
): { hash: string; subject: string; diff: string } | null {
): CommitRangeDiffResult {
const { execSync } = require("node:child_process");
// Only pass validated hex SHAs to the shell.
if (!/^[0-9a-f]{7,40}$/i.test(baseRef)) return null;
if (!/^[0-9a-f]{7,40}$/i.test(baseRef)) {
return { kind: "error", error: "invalid or stale base ref" };
}
try {
execSync("git rev-parse --git-dir", {
@@ -928,7 +957,18 @@ export function getCommitRangeDiff(
stdio: "pipe",
});
} catch {
return null;
return { kind: "error", error: "not a git repository" };
}
// Verify the base ref resolves before diffing — a stale/unfetched ref is a
// computation failure, not a clean "no changes" signal.
try {
execSync(`git rev-parse --verify ${baseRef}`, {
cwd: projectDir,
stdio: "pipe",
});
} catch {
return { kind: "error", error: `base ref ${baseRef} cannot be resolved` };
}
try {
@@ -946,17 +986,20 @@ export function getCommitRangeDiff(
// the snapshot. Includes stat overview + full patch.
//
// maxBuffer is set high (10 MB) so larger tasks don't cause execSync to
// throw. The review prompt builder truncates to MAX_DIFF_BYTES (50 KB)
// before sending to the model, so the full diff in memory is fine.
// throw. The review prompt builder filters noise and inlines only under
// MAX_DIFF_BYTES, so the full diff in memory is fine.
const diff = execSync(`git diff ${baseRef} HEAD --stat --patch`, {
cwd: projectDir,
encoding: "utf-8",
maxBuffer: 10 * 1024 * 1024,
}).trim();
if (!diff) return null; // no changes since baseRef
return { hash, subject, diff };
} catch {
return null;
if (!diff) return { kind: "no-changes" }; // genuinely no changes since baseRef
return { kind: "ok", hash, subject, diff };
} catch (error) {
return {
kind: "error",
error: error instanceof Error ? error.message : String(error),
};
}
}