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.4metadata:5 source: skills/pr-logic-reviewer/pr-logic-reviewer.md6---78# PR Logic Reviewer910## 1. System Architecture & Prerequisites1112Most automated review checks style; the bugs live in logic. This skill reads a13diff (from `git diff` or a PR), extracts the changed hunks, and applies14targeted heuristic probes for the highest-risk bug classes — then presents a15report ordered by severity that a human reviewer verifies. Node 18+ stdlib.1617Bug classes it drives you toward:1819- `BOUNDARY` — off-by-one, `<=` vs `<`, empty arrays, `first`/`last` on empty20- `NULL` — dereference without guard after async, `?.` misplacement, unfiltered21 falsy (0, "", false treated as "not set")22- `STATE` — read-then-write races, loops mutating the collection they iterate,23 stale closure values24- `ASYNC` — promise not awaited, parallel writes to same resource25- `INVARIANT` — duplicated branches, dead condition, swapped operands2627## 2. Input/Output Data Contracts2829Input: a unified diff (`git diff main...HEAD`) on stdin, or a file path.3031Output `PR_LOGIC_REVIEW.md` + `pr-logic-report.json`:3233```json34{ "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?" } ] }35```3637Exit: `0` = no hits (still recommend human review), `1` = hits that need38attention (report is advisory — always requires a human sign-off).3940## 3. Production Reference Implementation4142```js43// pr-logic-reviewer.js44const fs = require('fs');45const path = require('path');4647const input = process.argv[2]; // '-' for stdin or a diff file48let diff;49if (input === '-') diff = fs.readFileSync(0, 'utf-8');50else diff = fs.readFileSync(input, 'utf-8');5152const files = [];53const fileRe = /^diff --git a\/(\S+) b\/(\S+)/gm;54let m;55while ((m = fileRe.exec(diff))) files.push(m[2]);5657const hunks = [];58const hunkRe = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,?(\d+))? @@([\s\S]*?)(?=^@@ |^diff --git |(?![\s\S]))/gm;59let hm;60while ((hm = hunkRe.exec(diff))) {61 const base = Number(hm[1]);62 let ln = base;63 const lines = [];64 for (const l of hm[3].split(/\r?\n/)) {65 if (l.startsWith('+')) { lines.push({ ln, text: l.slice(1) }); ln++; }66 else if (l.startsWith('-')) { /* skip removed */ }67 else if (l.startsWith(' ')) { ln++; }68 }69 hunks.push({ lines: lines.filter((x) => x.text.trim()) });70}7172const PROBES = [73 { re: /(\w+)\s*[<>=]=?\s*[^;]*\.length\s*-1|length\s*-\s*1\s*[<>=]/, cls: 'BOUNDARY', hint: 'off-by-one near .length - 1' },74 { re: /\b(?:arr|list|items)\.(?:forEach|map)\([\s\S]{0,80}\1\.(?:push|splice|unshift)/, cls: 'STATE', hint: 'mutating collection during iteration' },75 { 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' },76 { re: /(?:async\s+\w+\(.*\)\s*{[^}]*})\s*(?!await)\n/, cls: 'ASYNC', hint: 'possible un-awaited async (verify on multi-statement bodies)' },77 { re: /(?:==|===)\s*false|!==\s*true\b/, cls: 'INVARIANT', hint: 'redundant boolean comparison' },78 { re: /if\s*\(([\w.]+)\s*[!==]=?\s*[^)]+\)[^{]*\{\s*\n\s*return\s+\3/, cls: 'DUPLICATE', hint: 'adjacent identical return branches — likely copy/paste' },79];8081const hits = [];82const seen = new Set();83for (const h of hunks) {84 for (const line of h.lines) {85 for (const probe of PROBES) {86 if (probe.re.test(line.text) && !seen.has(`${line.ln}|${probe.cls}`)) {87 seen.add(`${line.ln}|${probe.cls}`);88 hits.push({ line: line.ln, cls: probe.cls, snippet: line.text.slice(0, 120), hint: probe.hint });89 }90 }91 }92}9394const report = { files: files.length, probes: PROBES.length, hits };95fs.writeFileSync('pr-logic-report.json', JSON.stringify(report, null, 2));96const md = ['# PR Logic Review', '',97 `Changed files: **${files.length}** · Probe hits: **${hits.length}**`, '',98 '## Hits (human verifies each)', ''].concat(99 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_'],100 '', '## Files changed', '',101 ...files.map((f) => `- \`${f}\``));102fs.writeFileSync('PR_LOGIC_REVIEW.md', md.join('\n'));103console.log(md.join('\n'));104process.exit(hits.length ? 1 : 0);105```106107## 4. Execution Protocol & Step-by-Step Workflow1081091. Generate a diff: `git diff main...HEAD --no-color > pr.diff` (or export the110 PR patch from your forge).1112. `node pr-logic-reviewer.js pr.diff` (or `| node pr-logic-reviewer.js -`).1123. For each hit, perform the human verification it asks for — the probe names113 the class, the snippet locates it, and the `hint` tells you exactly what to114 confirm in the reviewer's head.1154. Beyond the automated classes, walk the diff with the "three questions":116 empty input? max input? concurrent input? That trio catches most remaining117 logic bugs the probes can't.1185. Resolve by editing code or by confirming intentional behavior (comment +119 test), and re-run the reviewer to see the hit count drop.1206. Gate merges on the *human* sign-off note, not on the exit code alone — the121 tool is a force-multiplier for reading diffs, never a replacement.122123## 5. Edge Cases & Error Handling124125- Diffs with conflicting hunk headers (complex merges) are tolerated: each hunk126 is parsed independently and line positions best-effort.127- Binary file diffs (`Binary files ... differ`) are skipped gracefully.128- Probe regexes are intentionally conservative to keep the false-positive rate129 low; they under-fire rather than over-fire, so zero hits is *not* a proof of130 correctness.131- Duplicate changes are deduped by line+class to avoid noise in large PRs.132- Multi-line constructs (mutating loop with a body >1 line) are approximated133 via very small windows; genuine multi-line cases are deliberately under-caught,134 and the manual "three questions" pass covers them.