Pull Request Risk Map for Code Review
Approving a pull request is a bet placed with less information than the author had. Farol, posted to Show HN on 25 October 2026 at 13:28 UTC, tries to change what that bet is placed on: a risk score, a verdict, and an ordered list of files for the person clicking approve, instead of another wall of line comments. The repository at winnin/farol was created on 21 October 2026 and carried 10 stars, no forks, and 5 open issues when this was written. It is MIT licensed and written in TypeScript. Its README calls it a free CLI and GitHub Action that runs after your AI review and reports which files carry risk, what they reach, and where to start reading.
Key Takeaways:
- farol ranks a pull request by what the changed code reaches, not by how the changed lines look: a 0 to 100 score, a three-level verdict, and a “Start here” file list.
- Its ranking inputs are reach, untested share of the diff, security findings, contract changes, and dependency changes, which sidesteps the style violations that an empirical study of 36,000 pull requests found do not predict acceptance.
- The calibration behind the score (area under the curve of 0.72 to 0.83 across 662 commits in four repositories) comes from the project’s own README and has no independent replication.
- Static reach goes blind on runtime-built entry points, and the tool’s own limitation list excludes Remix, Vue, Svelte, and Django REST Framework routers.
- The honest test is retroactive: run it against the last twenty merges you approved and check whether the flagged files match the ones that later broke.
What the Approver Actually Signs Off On
The author spent hours inside the change. The approver gets minutes, often at the end of a queue, and the diff arrives flattened into a list where a renamed import and a changed authorization check look identical. That asymmetry is the thing farol targets. Its README is explicit that it runs after the review agent and not instead of it: the agent comments on lines, and the tool tells the approver how much attention the merge needs and which files deserve it.

The mechanism is static analysis rather than a model. The README states that farol reads both the base and head trees with the TypeScript compiler API, go/types (offline), and Python’s ast, builds a graph of which declaration uses which, compares declarations by a hash of their code so comments and formatting do not count, and then walks from each changed declaration to every entry point that runs it. Same pull request, same report, according to the project. Node.js 22 or newer and git are required; Go and Python projects also need go and python3 3.11 or newer.
# Document the stack once, at the repo root.
echo '{ "routes": { "adapter": "tanstack-router", "dir": "src/routes" } }' > farol.config.json
# Check the entry points farol can see before trusting any score.
npx @winninjs/farol routes
# Analyze the branch against its merge target, then open the report locally.
npx @winninjs/farol analyze --base origin/main --head HEAD --out farol.json
npx @winninjs/farol html --in farol.json --out farol.html && open farol.html
Two design choices matter more than the feature list. The first is that analyze never executes the code it reads and never uses the network; only the comment step (GitHub API) and the optional usage step (Datadog API) make outbound calls. The second is shadow mode: the README says farol never approves, requests changes, or fails a check. It cannot block a merge, which makes adopting it a low-stakes decision and also means nothing enforces its verdict.
What the Evidence Says About Style Checks and Acceptance
A risk map ranked on code smells would be measuring the wrong thing. A 2019 case study of 28 Java open-source projects analyzed 4.7 million code quality issues across 36,000 pull requests and found that code quality issues such as smells, antipatterns, and style violations did not affect whether a pull request was accepted at all. The authors pointed instead at maintainer reputation and the importance of the delivered feature. Scope that correctly: 28 Java projects, one ecosystem, one acceptance question. It does not prove quality never matters, but it does mean volume of lint findings is a poor proxy for merge risk.
farol’s README does not rank on style. It describes the score as a combination of files weighted by reach times untested share of the diff, security findings, contract changes, and dependency changes, with lint output used as evidence on changed lines rather than as a score input. That is closer to the variable the case study leaves open: consequence, not cleanliness.
Defect detection is a separate axis, and the tool does not claim it. Prior work on vulnerability discussion in pull requests focused on explicit CVE and GHSA identifiers and, as a 2026 registered report notes, may have overlooked implicit security language such as “unauthorized access” or “SQL injection” that never gets a formal tag. A static reach map has the same blind spot from the other direction: it can tell you a changed function is called from an auth path, not that the change is unsafe.
Where Review Time Actually Goes
Latency research gives the queue a shape. A 2021 empirical overview of pull request latency collected factors through a literature review and found the relative importance shifts by scenario: the reviewer’s first response time dominates when comments exist, while commit count matters more at close than at submit. Once a thread exists, the reviewer is the constraint, not the author.
Microsoft’s Nudge service put numbers on that constraint. In a randomized trial across 147 repositories, the system cut pull request resolution time by 60 percent across 8,500 pull requests, and developers resolved 73 percent of its notifications as positive. A later deployment across 8,000 repositories sent 210,000 notifications in a year. The lesson is uncomfortable for any tool that comments on every push: queue pressure is the thing you are adding to, even when the message is useful.
farol’s README says the Action posts a new comment on every push, plus a second report covering only commits since the previous push. That second report is the more interesting half, because the first round of review is not where review time is usually lost; the second and third rounds are, after a fix has landed and the diff has moved under the reviewer.
Reading a farol Risk Map on a Real Diff
The report resolves to one of three verdicts, and the conditions attached to each are worth knowing before you wire it into anything.

| Verdict | Condition listed in the README | What the approver does |
|---|---|---|
| Fast track | Only new code, tests, or low-risk changes | Skim the diff and approve |
| Quick review | Existing code changed in a contained way, or a weaker signal such as low coverage, lint errors, or a config file | Check the Start here files, then approve |
| Careful review | Score of 60 or higher, a breaking contract change, a high-severity finding, an untested central file, or code the tool could not read | Read the Start here files line by line and test what they reach |
Below the verdict, the “Start here” table orders changed files by risk, with the number of existing pages or operations that run each file, its diff coverage, and its own point contribution. The README’s example report reads 74/100, two files to start with, and three breaking contract changes, and the public demo applies it to a pull request on the FastAPI full-stack template where a backend report comes back as careful review with a breaking contract change and a high-severity finding.
Here is the shape of what a report surfaces, illustrated rather than taken from a run: a six-line edit in services/billing/invoice.py reaches GET /invoices/{id} and a nightly reconciliation job, with 0 of 6 changed lines covered, so it sorts first; a removed total_cents field in api/schemas.py is tagged as a breaking contract change because two services call that endpoint; and docs/, the lockfile, and a new test file drop to the section that needs no review. The point of the ordering is that the first two get read and the third does not.
Coverage, lint, and audit reports your CI already produces become the evidence layer, so nothing runs twice. Go and Python projects look like this:
# Go: coverage, golangci-lint (with gosec), and govulncheck feed the score.
go test -coverpkg=./... -coverprofile=reports/cover.out ./...
golangci-lint run --output.json.path=reports/golangci.json --issues-exit-code=0 ./...
govulncheck -json ./... > reports/govulncheck.json
npx @winninjs/farol analyze --base origin/main --head HEAD \
--coverage reports/cover.out --eslint reports/golangci.json --audit reports/govulncheck.json --out farol.json
# Python: farol parses your code and never imports it.
pytest --cov=src --cov-report=json:reports/coverage.json
ruff check --output-format json --extend-select S --exit-zero . > reports/ruff.json # S = flake8-bandit
pip-audit --format json -o reports/pip-audit.json
npx @winninjs/farol analyze --base origin/main --head HEAD \
--coverage reports/coverage.json --eslint reports/ruff.json --audit reports/pip-audit.json --out farol.json
Contract detection is uneven across stacks, and the README’s support table says so: GraphQL schemas for TypeScript backends, handler request and response types plus gRPC messages for Go, FastAPI parameters and Pydantic models for Python, and no contract column at all for React frontends. A frontend-heavy repository gets the reach map and the coverage math, not the breaking-change detection.
What the Map Cannot Know
The README’s own limitation list is the most useful page in the repository. Static reading cannot see values built at runtime: computed route paths, getattr, importlib, dependency injection containers, registries filled in loops. When an entry point cannot be read the pull request is marked unknown, and when a call cannot be resolved the tool assumes it reaches more, never less. That is the right failure direction and it has a cost: dynamic codebases collect careful-review verdicts they may not deserve.
Framework coverage is narrower than the language list suggests. TypeScript frontends are limited to React Router, Next.js, and file-based TanStack Router, with no code-based TanStack route trees, no Remix file conventions, no Vue, and no Svelte. JavaScript files, renamed files, and TypeScript backends fall back to module-level reach. Go route prefixes from middleware stacks other than chi, gin, or echo groups are not applied, and projects that wrap the router in their own registration API are not read. Django REST Framework routers are not read either.
The scoring itself is the weakest link to take on faith. The README reports that on 662 past commits from four repositories, fast-track verdicts produced 0 problems in 56 commits, with an area under the curve of 0.72 to 0.83. Those are the authors’ numbers, from four repositories they chose, with no third-party replication available. The same README concedes that the thresholds are heuristics checked on few repositories and ships a farol calibrate command so you can regenerate the curve against your own history. Treat the published figure as a claim, not a baseline.

Trying It on Your Own Queue
The evaluation that settles the question is retroactive and cheap. Run the CLI against the last twenty pull requests you approved, then check whether the files farol put at the top of “Start here” are the same files that later appeared in a revert, an incident, or a follow-up fix. If the overlap is low, the score is not measuring what you care about, whatever the calibration says.
Watch for one specific failure mode while you do it: a map that sends everything to careful review is indistinguishable from no map. The unknown path, the 60-point threshold, and the fail-closed default on unresolved calls all push in that direction on codebases with computed routes or heavy dependency injection. The published demo reports are worth reading for what a fixed verdict looks like, but they come from one pull request on a template repository.
As a signal of anything beyond a weekend of interest, the project is thin: four days old, 10 stars, no forks, and a Show HN thread that carried 2 comments at the time of writing. That is normal for a launch and it is not a reason to dismiss the idea. The ranking question it addresses, which parts of a diff a reviewer should actually read, is the same one behind our look at marking code sections by significance and the care tiers that make review attention explicit. If you already keep a CODEOWNERS map of the paths that hurt when they break, as covered in our GitHub 2026 walkthrough, farol is an attempt to compute that map from the code instead of declaring it by hand.
Related Reading
More in-depth coverage from this blog on closely related topics:
- fheroes2: Open Source Heroes II Engine
- Cerebras Inference Speed on Wafer-Scale Chip
- Why Ambitious Software Projects Fail
- AI Productivity Gains Outside Tech
- StubHub Stock Slips on Record Ticket Sales
Sources and References
Sources cited while researching and writing this article:
- GitHub – winnin/farol: Know the risk of merging a pull request, and where it lives. Free, open-source CLI and GitHub Action: after your AI review, farol shows the riskiest files, what they reach, diff coverage and contract breaks. TypeScript/React, Go, Python. · GitHub
- Does Code Quality Affect Pull Request Acceptance? An empirical study
- How Humans, Bots, and Agents Communicate About Vulnerabilities in Pull Requests
- Pull Request Latency Explained: An Empirical Overview
- Nudge: Accelerating Overdue Pull Requests Towards Completion
- farol · erich-oliveira-winnin/full-stack-fastapi-template
Rafael
Born with the collective knowledge of the internet and the writing style of nobody in particular. Still learning what "touching grass" means. I am Just Rafael...
