A deterministic PR file-triage filter. Zero runtime dependencies. Written in TypeScript, runs on Node 22+.
Given the files in a pull request, classifies each as skip | skim | review-candidate so a reviewer's (or a downstream LLM's) attention lands
where it matters. Uses path patterns, git metadata, and diff-level heuristics.
No AST parsing. No network. No filesystem. Pure function.
Extracted from PR Compass and published standalone so any review pipeline can use it.
- Every transitive dep is a vulnerability surface we'd have to maintain.
- Ecosystem adoption is easier if adding this package doesn't add 40 MB to
your
node_modules. - If the filter needs a library to work, it's probably over-engineered.
npm install @prcompass/pr-triage-filter
# or pnpm / yarn / bunimport { classifyPrFiles } from "@prcompass/pr-triage-filter";
const result = classifyPrFiles({
files: [
{
path: "pnpm-lock.yaml",
changeType: "modified",
additions: 120,
deletions: 45,
},
{
path: "src/pricing.ts",
changeType: "modified",
additions: 30,
deletions: 5,
patch: "@@ -10,3 +10,4 @@\n ...",
},
],
});
for (const v of result.verdicts) {
console.log(v.path, v.verdict, `(${v.ruleId})`, v.reason);
}
// pnpm-lock.yaml skip (lockfile) Package lockfile — content is auto-generated
// src/pricing.ts review-candidate (default) Source change in non-test, non-config codeEvaluated in order; first match wins. If no rule matches, the default
verdict is review-candidate.
| Rule ID | Verdict | Trigger |
|---|---|---|
rename-only |
skip |
changeType === "renamed" with zero additions + zero deletions |
lockfile |
skip |
Filename is a known lockfile (pnpm-lock.yaml, package-lock.json, yarn.lock, Cargo.lock, go.sum, …) |
generated-path |
skip |
Path is inside a generated directory (dist/, build/, out/, __generated__/, coverage/, …) or matches a minified/bundle suffix |
binary |
skip |
Extension is a known binary (images, fonts, archives, media) |
generated-header |
skip |
First ~500 bytes of the diff content contain @generated, DO NOT EDIT, generated by, auto-generated |
prettier-only |
skip |
Every hunk's removed lines equal its added lines after collapsing whitespace |
import-reorder |
skip |
All touched lines look like imports (or blanks/comments in the import region) AND the multiset of imports is preserved |
docs |
skim |
Path is markdown, docs directory, README, CHANGELOG, CONTRIBUTING, LICENSE |
config |
skim |
Path is a known config file (tsconfig, eslint, prettier, editorconfig, package.json, …) |
test |
skim |
Path matches **/*.test.*, **/*.spec.*, **/tests/**, **/__tests__/** |
default |
review-candidate |
None of the above |
prettier-onlyfalse positives when a change is semantically whitespace-sensitive (e.g., a space inside a string literal). Tier 2 of PR Compass catches this; here it's a deliberate trade.import-reorderfalse positives when imports are reordered AND a new export is added in the same hunk if the export line starts withexport {orexport *. We treat those as import-region lines.
Target: ~85–90% accuracy against hand-reviewed fixtures. The goal is to efficiently remove 50–70% of files from the review budget, not to be perfect.
Pure. No I/O. Deterministic.
type Verdict = "skip" | "skim" | "review-candidate";
interface FileInput {
readonly path: string;
readonly previousPath?: string;
readonly changeType: "added" | "modified" | "deleted" | "renamed";
readonly additions: number;
readonly deletions: number;
readonly patch?: string;
}
interface ClassifyInput {
readonly files: readonly FileInput[];
}
interface FileVerdict {
readonly path: string;
readonly verdict: Verdict;
readonly reason: string;
readonly ruleId: string;
}
interface ClassifyResult {
readonly verdicts: readonly FileVerdict[];
}Internal modules (rule implementations, diff parser) are not re-exported. Consumers import only from the top-level.
Processes a 100-file PR in well under 500ms on a modern laptop. Tested in
CI; see tests/performance.test.ts.
MIT. See LICENSE.