PR Logic Reviewer
1. System Architecture & Prerequisites
Most automated review checks style; the bugs live in logic. This skill reads a
diff (from git diff or a PR), extracts the changed hunks, and applies
targeted heuristic probes for the highest-risk bug classes — then presents a
report ordered by severity that a human reviewer verifies. Node 18+ stdlib.
Bug classes it drives you toward:
BOUNDARY — off-by-one, <= vs <, empty arrays, first/last on empty
NULL — dereference without guard after async, ?. misplacement, unfiltered
falsy (0, "", false treated as "not set")
STATE — read-then-write races, loops mutating the collection they iterate,
stale closure values
ASYNC — promise not awaited, parallel writes to same resource
INVARIANT — duplicated branches, dead condition, swapped operands
2. Input/Output Data Contracts
Input: a unified diff (git diff main...HEAD) on stdin, or a file path.
Output PR_LOGIC_REVIEW.md + pr-logic-report.json:
{ "files": 6, "probes": 42, "hits": [ { "file": "src/auth.ts", "line": 120, "class": "BOUNDARY", "snippet": "if (i >= arr.length-1) ...", "prompt": "verify: does -1 make the last element unreachable?" } ] }
Exit: 0 = no hits (still recommend human review), 1 = hits that need
attention (report is advisory — always requires a human sign-off).
3. Production Reference Implementation
// pr-logic-reviewer.js
const fs = require('fs');
const path = require('path');
const input = process.argv[2]; // '-' for stdin or a diff file
let diff;
if (input === '-') diff = fs.readFileSync(0, 'utf-8');
else diff = fs.readFileSync(input, 'utf-8');
const files = [];
const fileRe = /^diff --git a\/(\S+) b\/(\S+)/gm;
let m;
while ((m = fileRe.exec(diff))) files.push(m[2]);
const hunks = [];
const hunkRe = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,?(\d+))? @@([\s\S]*?)(?=^@@ |^diff --git |(?![\s\S]))/gm;
let hm;
while ((hm = hunkRe.exec(diff))) {
const base = Number(hm[1]);
let ln = base;
const lines = [];
for (const l of hm[3].split(/\r?\n/)) {
if (l.startsWith('+')) { lines.push({ ln, text: l.slice(1) }); ln++; }
else if (l.startsWith('-')) { /* skip removed */ }
else if (l.startsWith(' ')) { ln++; }
}
hunks.push({ lines: lines.filter((x) => x.text.trim()) });
}
const PROBES = [
{ re: /(\w+)\s*[<>=]=?\s*[^;]*\.length\s*-1|length\s*-\s*1\s*[<>=]/, cls: 'BOUNDARY', hint: 'off-by-one near .length - 1' },
{ re: /\b(?:arr|list|items)\.(?:forEach|map)\([\s\S]{0,80}\1\.(?:push|splice|unshift)/, cls: 'STATE', hint: 'mutating collection during iteration' },
{ re: /if\s*\(!\w+(\.\w+)*\)\s*\{[\s\S]{0,60}\.(\w+)\s*[.\[(]/, cls: 'NULL', hint: 'falsy check followed by property access that may be undefined' },
{ re: /(?:async\s+\w+\(.*\)\s*{[^}]*})\s*(?!await)\n/, cls: 'ASYNC', hint: 'possible un-awaited async (verify on multi-statement bodies)' },
{ re: /(?:==|===)\s*false|!==\s*true\b/, cls: 'INVARIANT', hint: 'redundant boolean comparison' },
{ re: /if\s*\(([\w.]+)\s*[!==]=?\s*[^)]+\)[^{]*\{\s*\n\s*return\s+\3/, cls: 'DUPLICATE', hint: 'adjacent identical return branches — likely copy/paste' },
];
const hits = [];
const seen = new Set();
for (const h of hunks) {
for (const line of h.lines) {
for (const probe of PROBES) {
if (probe.re.test(line.text) && !seen.has(`${line.ln}|${probe.cls}`)) {
seen.add(`${line.ln}|${probe.cls}`);
hits.push({ line: line.ln, cls: probe.cls, snippet: line.text.slice(0, 120), hint: probe.hint });
}
}
}
}
const report = { files: files.length, probes: PROBES.length, hits };
fs.writeFileSync('pr-logic-report.json', JSON.stringify(report, null, 2));
const md = ['# PR Logic Review', '',
`Changed files: **${files.length}** · Probe hits: **${hits.length}**`, '',
'## Hits (human verifies each)', ''].concat(
hits.length ? hits.map((h) => `- \`L${h.line}\` [${h.cls}] ${h.snippet}\n → check: ${h.hint}`) : ['_no automated hits — still perform a manual logic pass_'],
'', '## Files changed', '',
...files.map((f) => `- \`${f}\``));
fs.writeFileSync('PR_LOGIC_REVIEW.md', md.join('\n'));
console.log(md.join('\n'));
process.exit(hits.length ? 1 : 0);
4. Execution Protocol & Step-by-Step Workflow
- Generate a diff:
git diff main...HEAD --no-color > pr.diff (or export the
PR patch from your forge).
node pr-logic-reviewer.js pr.diff (or | node pr-logic-reviewer.js -).
- For each hit, perform the human verification it asks for — the probe names
the class, the snippet locates it, and the
hint tells you exactly what to
confirm in the reviewer's head.
- Beyond the automated classes, walk the diff with the "three questions":
empty input? max input? concurrent input? That trio catches most remaining
logic bugs the probes can't.
- Resolve by editing code or by confirming intentional behavior (comment +
test), and re-run the reviewer to see the hit count drop.
- Gate merges on the human sign-off note, not on the exit code alone — the
tool is a force-multiplier for reading diffs, never a replacement.
5. Edge Cases & Error Handling
- Diffs with conflicting hunk headers (complex merges) are tolerated: each hunk
is parsed independently and line positions best-effort.
- Binary file diffs (
Binary files ... differ) are skipped gracefully.
- Probe regexes are intentionally conservative to keep the false-positive rate
low; they under-fire rather than over-fire, so zero hits is not a proof of
correctness.
- Duplicate changes are deduped by line+class to avoid noise in large PRs.
- Multi-line constructs (mutating loop with a body >1 line) are approximated
via very small windows; genuine multi-line cases are deliberately under-caught,
and the manual "three questions" pass covers them.
1---2name: pr-logic-reviewer3description: Review a pull-request diff for logic bugs — boundary conditions, null safety, state and edge semantics — not style, and emit a bug-first review report.4---56# PR Logic Reviewer78## 1. System Architecture & Prerequisites910Most automated review checks style; the bugs live in logic. This skill reads a11diff (from `git diff` or a PR), extracts the changed hunks, and applies12targeted heuristic probes for the highest-risk bug classes — then presents a13report ordered by severity that a human reviewer verifies. Node 18+ stdlib.1415Bug classes it drives you toward:1617- `BOUNDARY` — off-by-one, `<=` vs `<`, empty arrays, `first`/`last` on empty18- `NULL` — dereference without guard after async, `?.` misplacement, unfiltered19 falsy (0, "", false treated as "not set")20- `STATE` — read-then-write races, loops mutating the collection they iterate,21 stale closure values22- `ASYNC` — promise not awaited, parallel writes to same resource23- `INVARIANT` — duplicated branches, dead condition, swapped operands2425## 2. Input/Output Data Contracts2627Input: a unified diff (`git diff main...HEAD`) on stdin, or a file path.2829Output `PR_LOGIC_REVIEW.md` + `pr-logic-report.json`:3031```json32{ "files": 6, "probes": 42, "hits": [ { "file": "src/auth.ts", "line": 120, "class": "BOUNDARY", "snippet": "if (i >= arr.length-1) ...", "prompt": "verify: does -1 make the last element unreachable?" } ] }33```3435Exit: `0` = no hits (still recommend human review), `1` = hits that need36attention (report is advisory — always requires a human sign-off).3738## 3. Production Reference Implementation3940```js41// pr-logic-reviewer.js42const fs = require('fs');43const path = require('path');4445const input = process.argv[2]; // '-' for stdin or a diff file46let diff;47if (input === '-') diff = fs.readFileSync(0, 'utf-8');48else diff = fs.readFileSync(input, 'utf-8');4950const files = [];51const fileRe = /^diff --git a\/(\S+) b\/(\S+)/gm;52let m;53while ((m = fileRe.exec(diff))) files.push(m[2]);5455const hunks = [];56const hunkRe = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,?(\d+))? @@([\s\S]*?)(?=^@@ |^diff --git |(?![\s\S]))/gm;57let hm;58while ((hm = hunkRe.exec(diff))) {59 const base = Number(hm[1]);60 let ln = base;61 const lines = [];62 for (const l of hm[3].split(/\r?\n/)) {63 if (l.startsWith('+')) { lines.push({ ln, text: l.slice(1) }); ln++; }64 else if (l.startsWith('-')) { /* skip removed */ }65 else if (l.startsWith(' ')) { ln++; }66 }67 hunks.push({ lines: lines.filter((x) => x.text.trim()) });68}6970const PROBES = [71 { re: /(\w+)\s*[<>=]=?\s*[^;]*\.length\s*-1|length\s*-\s*1\s*[<>=]/, cls: 'BOUNDARY', hint: 'off-by-one near .length - 1' },72 { re: /\b(?:arr|list|items)\.(?:forEach|map)\([\s\S]{0,80}\1\.(?:push|splice|unshift)/, cls: 'STATE', hint: 'mutating collection during iteration' },73 { re: /if\s*\(!\w+(\.\w+)*\)\s*\{[\s\S]{0,60}\.(\w+)\s*[.\[(]/, cls: 'NULL', hint: 'falsy check followed by property access that may be undefined' },74 { re: /(?:async\s+\w+\(.*\)\s*{[^}]*})\s*(?!await)\n/, cls: 'ASYNC', hint: 'possible un-awaited async (verify on multi-statement bodies)' },75 { re: /(?:==|===)\s*false|!==\s*true\b/, cls: 'INVARIANT', hint: 'redundant boolean comparison' },76 { re: /if\s*\(([\w.]+)\s*[!==]=?\s*[^)]+\)[^{]*\{\s*\n\s*return\s+\3/, cls: 'DUPLICATE', hint: 'adjacent identical return branches — likely copy/paste' },77];7879const hits = [];80const seen = new Set();81for (const h of hunks) {82 for (const line of h.lines) {83 for (const probe of PROBES) {84 if (probe.re.test(line.text) && !seen.has(`${line.ln}|${probe.cls}`)) {85 seen.add(`${line.ln}|${probe.cls}`);86 hits.push({ line: line.ln, cls: probe.cls, snippet: line.text.slice(0, 120), hint: probe.hint });87 }88 }89 }90}9192const report = { files: files.length, probes: PROBES.length, hits };93fs.writeFileSync('pr-logic-report.json', JSON.stringify(report, null, 2));94const md = ['# PR Logic Review', '',95 `Changed files: **${files.length}** · Probe hits: **${hits.length}**`, '',96 '## Hits (human verifies each)', ''].concat(97 hits.length ? hits.map((h) => `- \`L${h.line}\` [${h.cls}] ${h.snippet}\n → check: ${h.hint}`) : ['_no automated hits — still perform a manual logic pass_'],98 '', '## Files changed', '',99 ...files.map((f) => `- \`${f}\``));100fs.writeFileSync('PR_LOGIC_REVIEW.md', md.join('\n'));101console.log(md.join('\n'));102process.exit(hits.length ? 1 : 0);103```104105## 4. Execution Protocol & Step-by-Step Workflow1061071. Generate a diff: `git diff main...HEAD --no-color > pr.diff` (or export the108 PR patch from your forge).1092. `node pr-logic-reviewer.js pr.diff` (or `| node pr-logic-reviewer.js -`).1103. For each hit, perform the human verification it asks for — the probe names111 the class, the snippet locates it, and the `hint` tells you exactly what to112 confirm in the reviewer's head.1134. Beyond the automated classes, walk the diff with the "three questions":114 empty input? max input? concurrent input? That trio catches most remaining115 logic bugs the probes can't.1165. Resolve by editing code or by confirming intentional behavior (comment +117 test), and re-run the reviewer to see the hit count drop.1186. Gate merges on the *human* sign-off note, not on the exit code alone — the119 tool is a force-multiplier for reading diffs, never a replacement.120121## 5. Edge Cases & Error Handling122123- Diffs with conflicting hunk headers (complex merges) are tolerated: each hunk124 is parsed independently and line positions best-effort.125- Binary file diffs (`Binary files ... differ`) are skipped gracefully.126- Probe regexes are intentionally conservative to keep the false-positive rate127 low; they under-fire rather than over-fire, so zero hits is *not* a proof of128 correctness.129- Duplicate changes are deduped by line+class to avoid noise in large PRs.130- Multi-line constructs (mutating loop with a body >1 line) are approximated131 via very small windows; genuine multi-line cases are deliberately under-caught,132 and the manual "three questions" pass covers them.