← Back to home

Examples

Copy-paste integrations for the four places this filter lives in real projects: a GitHub PR webhook, an Octokit fetch, a GitHub Action step, and a one-shot CI script.

1

GitHub PR webhook handler

Map GitHub's pull_request.synchronize payload into FileInput. The status field is the one that needs translating — everything else maps 1:1.

import { classifyPrFiles, type ChangeType } from '@prcompass/pr-triage-filter';
import type { Octokit } from '@octokit/rest';

const STATUS_TO_CHANGE_TYPE: Record<string, ChangeType> = {
  added: 'added',
  modified: 'modified',
  removed: 'deleted',
  renamed: 'renamed',
  changed: 'modified',
  copied: 'added'
};

export async function triagePr(
  octokit: Octokit,
  owner: string,
  repo: string,
  pull_number: number
) {
  const files = await octokit.paginate(octokit.rest.pulls.listFiles, {
    owner, repo, pull_number, per_page: 100
  });

  const { verdicts } = classifyPrFiles({
    files: files.map((f) => ({
      path: f.filename,
      previousPath: f.previous_filename,
      changeType: STATUS_TO_CHANGE_TYPE[f.status] ?? 'modified',
      additions: f.additions,
      deletions: f.deletions,
      patch: f.patch
    }))
  });

  return verdicts;
}
2

Bucket the verdicts before passing to the next tier

The whole point of triage is to shrink the input set passed to a more expensive analyzer. Group by verdict and forward only what survives.

const { verdicts } = classifyPrFiles({ files });

const skipped = verdicts.filter((v) => v.verdict === 'skip');
const skimmed = verdicts.filter((v) => v.verdict === 'skim');
const reviewable = verdicts.filter((v) => v.verdict === 'review-candidate');

console.log(`triage: ${skipped.length} skip / ${skimmed.length} skim / ${reviewable.length} review`);
console.log(`saved ${Math.round((skipped.length / verdicts.length) * 100)}% of files`);

// Pass only review-candidates to the LLM tier.
await runTier2(reviewable.map((v) => v.path));
3

GitHub Action — comment a triage summary

A minimal action that runs the filter on every PR sync and posts a collapsed summary as a sticky comment. Useful as a "what did the bot hide from me" sanity check during rollout.

# .github/workflows/triage.yml
name: PR triage

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  pull-requests: write

jobs:
  triage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '22' }
      - run: npm install --no-save @prcompass/pr-triage-filter @octokit/rest
      - run: node .github/scripts/triage.mjs
        env:
          GH_TOKEN: ${{ github.token }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          REPO: ${{ github.repository }}
4

Branch on a stable rule ID

ruleId is part of the public contract — pattern-matching on it is safe across minor versions. reason is not — it's reworded between versions. Use the right field for the right job.

// OK — ruleId is a stable identifier.
for (const v of verdicts) {
  if (v.ruleId === 'lockfile' || v.ruleId === 'generated-path') {
    metrics.increment('triage.skipped.generated');
  }
}

// BAD — reason is human-readable, not UI-stable.
if (v.reason === 'Package lockfile — content is auto-generated') {
  // …will silently break when the wording changes.
}
5

Local diff — using the @prcompass/cli wrapper

The companion CLI runs this filter (alongside the rest of the deterministic pipeline) over a local git diff. No GitHub round-trip needed during dev.

npx @prcompass/cli analyze --repo . --diff main..HEAD --format human

# JSON for piping into another tool:
npx @prcompass/cli analyze --repo . --diff HEAD~1 | jq '.triage.verdicts'

Ready to integrate?

The whole API is one function. Two minutes to install, an afternoon to be sure of the verdicts on your repo's PRs.

@prcompass/pr-triage-filter Deterministic PR file-triage filter