@prcompass/pr-triage-filter
v0.1.0
Published
A deterministic PR file-triage filter. Zero runtime dependencies. Written in TypeScript, runs on Node >=20.
Maintainers
Readme
@prcompass/pr-triage-filter
A deterministic PR file-triage filter. Zero runtime dependencies. Written in TypeScript, runs on Node >=20.
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.
Why zero dependencies
- 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.
Install
npm install @prcompass/pr-triage-filter
# or pnpm / yarn / bunUsage
import { 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) Auto-generated lockfile; re-review by rerunning the package manager, not by reading the diff.
// src/pricing.ts review-candidate (default) Production source code outside tests, docs, and config paths.Rules
Evaluated 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 |
Accepted false positives
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.
API
classifyPrFiles(input: ClassifyInput): ClassifyResult
Pure. No I/O. Deterministic.
Types
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.
Performance
Processes a 100-file PR in well under 500ms on a modern laptop; see
tests/performance.test.ts.
License
Apache-2.0. See LICENSE.
