Know the risk of merging a pull request, and where it lives.
A free, open-source CLI and GitHub Action. After your AI review, farol tells the person who approves how risky the
merge is and which files carry the risk, so the riskiest changes get the most attention.
---
Agents write the code and agents review it. The last call is still a person's: approve the merge or not. That
approval is the scarce part, and spread evenly over every pull request it turns into a skim, which is how a one-line
change to a shared hook takes down forty pages.
**farol comes after the AI review, not instead of it.** Your review agent comments on the lines in the diff. farol
gives the person who approves what the agent cannot reliably give: **the risk of accepting the change, and where it
lives**. The goal is not only a faster review: low-risk pull requests can go fast, and the risky ones get read line by
line, starting with the files that carry the risk. It works from static analysis (the compilers' own view of your
code, not a model's guess), so the same pull request always gets the same answer.
The Claude Code team describes the same principle in
[How the Claude Code team does code review](https://claude.dev/blog/solving-code-review-at-anthropic/): review effort
follows the risk of the change. As they put it, "The change sets the tier, not the author."
**farol** (Portuguese for *lighthouse*) does the triage before the reviewer opens the diff:
- **What does this change reach?** Every existing page, API operation, RPC method, job and script that runs the changed code, with the call chain to each.
- **How risky is the merge?** A verdict (*fast track*, *quick review*, *careful review*) and a 0–100 risk score from reach, diff coverage, security findings, contract breaks and dependency changes.
- **Where do I start?** The riskiest files first, with inline diffs, untested lines marked, and a checklist of what to try by hand.
**Free and open source (MIT).** Run it as a CLI with `npx @winninjs/farol`, or on every pull request with the GitHub
Action `winnin/farol/action`. No account, no hosted service: it runs on your machine or in your CI runner, comments on
every PR and never blocks a merge. It **never executes your code** and never sends it anywhere.
## Live demo
**[See farol on a real pull request →](https://winnin.github.io/farol/demo/)**
The demo holds the reports farol produced in CI for a pull request on the
[FastAPI full-stack template](https://github.com/fastapi/full-stack-fastapi-template): a FastAPI backend and a React
frontend with TanStack Router. The pull request adds a status to items and an Insights page, with the kind of problems
a reviewer should catch: a breaking API change, SQL built with an f-string, a shared component without tests.
- [Backend report](https://winnin.github.io/farol/demo/backend.html): ⚠ careful review, a breaking contract change and a high-severity security finding.
- [Frontend report](https://winnin.github.io/farol/demo/frontend.html): ⚠ careful review, a new page and the existing pages a shared component reaches.
The reports are interactive: mark files as reviewed, mark a page as behind a feature flag, open the inline diffs,
follow the test plan and copy the review record from the Result tab. They are the same files the pull request's
comments link to ([the pull request](https://github.com/erich-oliveira-winnin/full-stack-fastapi-template/pull/1)); the
copies live in [`docs/demo/`](docs/demo/).
## farol after your AI review
| | AI review agents | farol |
|---|---|---|
| **Reads** | the diff, and whatever context fits in the prompt | the whole program at the base and the head, with the compilers (TypeScript type checker, `go/types`, Python `ast`) |
| **Tells you** | what may be wrong in these lines | what these lines reach, how risky the merge is, and where to look first |
| **Answers are** | probabilistic: worded differently on every run | deterministic: the same PR gives the same report, and every claim comes with its call chain |
| **When unsure** | may sound confident anyway | says `unknown` and asks for a careful review |
| **Runs** | a model, usually a hosted service | locally or in your CI: it never executes your code and never sends it anywhere |
Use both, in that order. The agent reviews the lines first; then farol tells the person who approves how much attention
the merge needs, which files deserve it, which pages and endpoints to test by hand and which contract changes need
their clients checked. The agent's review is not enough exactly when a small change has a large reach, and that is
what farol shows.
## Why farol
- **Review attention goes where the risk is.** A 40-file PR usually has two or three files that can actually hurt. farol ranks them, so the reviewer reads those closely and skims the rest with a clear conscience.
- **It follows the code, not the file names.** farol builds a declaration graph with the real compilers (the TypeScript type checker, `go/types`, Python's `ast`). A change to `formatPrice` lists the twelve pages that render it, not "1 file changed".
- **Contract breaks are caught before clients are.** Removed response fields, new required parameters, changed types in GraphQL, FastAPI, Go handlers and gRPC, with the services that call each endpoint.
- **Review the whole PR, or just what changed since your last look.** Every push gets two reports: the full pull request, and only the commits since the previous push. After a round of fixes, the reviewer checks the new commits instead of starting over.
- **It uses what your CI already has.** Coverage, lint, security and audit reports become diff coverage and findings on the changed lines. Nothing runs twice.
- **Fail closed.** When farol cannot read part of a change, it says so (`unknown`) and asks for a careful review. It never calls something safe because it did not understand it.
- **Built for the human in the loop.** Mark files as reviewed, mark pages as behind a feature flag, check off findings, and paste the result back in the PR as a record of what was reviewed.
## Contents
- [Live demo](#live-demo)
- [farol after your AI review](#farol-after-your-ai-review)
- [Get started](#get-started)
- [Recipes](#recipes): [React Router](#react-frontend-react-router) · [Next.js](#nextjs) · [TanStack Router](#tanstack-router) · [TypeScript API](#typescript-api-graphql-or-rest) · [Go](#go-service) · [Python](#python-service) · [Datadog](#add-datadog-traffic-and-errors)
- [What is supported](#what-is-supported)
- [Reading the review](#reading-the-review)
- [How it works](#how-it-works) · [Security](#security-and-privacy) · [Limitations](#limitations) · [FAQ](#faq)
- [Reference](docs/reference.md) (every command, option and configuration key)
## Get started
You need **Node.js 22+** and **git** (plus `go` for Go projects, `python3` 3.11+ for Python ones). Nothing to
install: run farol with `npx @winninjs/farol`.
**1. Describe your project** in a `farol.config.json` at its root. One line is enough to start; pick your stack:
| Your project | `farol.config.json` | Full recipe |
|---|---|---|
| React app with React Router | `{ "routes": { "file": "src/routes.tsx" } }` (the file that builds the route table) | [React Router](#react-frontend-react-router) |
| Next.js app (Pages or App Router) | `{ "routes": { "adapter": "next" } }` | [Next.js](#nextjs) |
| React app with TanStack Router | `{ "routes": { "adapter": "tanstack-router", "dir": "src/routes" } }` | [TanStack Router](#tanstack-router) |
| TypeScript API (GraphQL, REST) | `{ "surface": "backend" }` | [TypeScript API](#typescript-api-graphql-or-rest) |
| Go service | `{ "language": "go", "surface": "backend" }` | [Go](#go-service) |
| Python service (FastAPI, Flask, Django, Celery…) | `{ "language": "python", "surface": "backend" }` | [Python](#python-service) |
**2. Check what farol sees:** the pages, endpoints, jobs and scripts it found.
```sh
npx @winninjs/farol routes
```
**3. Analyze a branch** against its target and open the report.
```sh
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 # xdg-open on Linux
```
### Example: a React app with React Router
```sh
cd path/to/your/react-app
echo '{ "routes": { "file": "src/routes.tsx" } }' > farol.config.json
npx @winninjs/farol routes # lists your pages: "/", "/products", "/settings"…
git checkout my-feature-branch
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
```
The report already shows the verdict, the score, the files to start with, the pages the change reaches and what to
test by hand. **Next steps:** add your coverage, lint and audit reports and run farol on every PR with the
[recipe for your stack](#recipes). Every option is in the [reference](docs/reference.md) and, per stack, in
[examples/README.md](examples/README.md).
> Running it often? `npm install --global @winninjs/farol` gives you a plain `farol` command.
## Recipes
Each recipe has three parts: the `farol.config.json` to commit, the reports to produce, and the GitHub Actions
workflow. The workflows in [`examples/`](examples/) are complete and share one shape:
1. **`tests`** runs your tests, linter and audit and keeps the reports. It runs the PR's code, so it has no secrets and a read-only token. If you already have a test job, keep it and add its upload step.
2. **`farol`** runs after it (pass or fail), reads git and the reports, and posts a **new comment on every push** with a link that opens the HTML report in the browser, plus a second report with only the commits since the previous push. It never runs the PR's code.
3. Drafts are skipped, fork PRs get no comment (their token is read-only), and every farol step is `continue-on-error`.
Pin the action to a commit once it works for you: `winnin/farol/action@ # main as of `.
[`examples/README.md`](examples/README.md) goes deeper for each stack: every configuration key, CLI parameter and
action input, and how to check a setup.
### React frontend (React Router)
React Router route tables (object and JSX forms, lazy routes, split route modules), reach per declaration.
```json
{
"routes": { "file": "src/routes.tsx" },
"roles": { "global": ["src/main.tsx", "src/App.tsx", "src/styles/global.css", "package.json", "vite.config.ts"] }
}
```
```sh
npx vitest run --coverage.enabled --coverage.reporter=json --coverage.reportsDirectory=reports/coverage
npx eslint -f json -o reports/eslint.json
npm audit --json > reports/audit.json # or pnpm audit --json
npx @winninjs/farol analyze --base origin/main --head HEAD \
--coverage reports/coverage/coverage-final.json --eslint reports/eslint.json --audit reports/audit.json --out farol.json
```
**CI:** copy [`examples/workflow.yml`](examples/workflow.yml) to `.github/workflows/farol.yml`.
Set `coverage.include: ['src/**/*.{ts,tsx}']` in the Vitest config so files no test loads are reported as untested.
Split route trees go in `routes.extraFiles`; a fuller config is in [examples/farol.config.json](examples/farol.config.json).
### Next.js
Pages Router and App Router: every page, API route, layout, `_app`/`_document` and middleware, read from the file
names. Dynamic segments (`[id]`), catch-alls, route groups and parallel slots are understood.
```json
{
"routes": { "adapter": "next" },
"roles": { "global": ["next.config.*", "package.json"] }
}
```
`pages/` and `app/` are found in the project or in its `src/`; set `"dir"` when they live elsewhere. Reports and CI
are the same as for [React Router](#react-frontend-react-router) (Jest or Vitest coverage, ESLint, audit). In a
monorepo whose tsconfig extends a workspace package (`"extends": "tsconfig/nextjs.json"`), install dependencies
before analyzing, so the package is in `node_modules`.
### TanStack Router
File-based routing: every module calling `createFileRoute('')` under the routes directory, with the root route,
pathless layouts (`_app`) and parent routes as layouts of their children.
```json
{ "routes": { "adapter": "tanstack-router", "dir": "src/routes" } }
```
Reports and CI are the same as for [React Router](#react-frontend-react-router).
### TypeScript API (GraphQL or REST)
GraphQL root operations (schema files and `gql` templates, per resolver) and Fastify/Express-style REST endpoints.
```json
{
"surface": "backend",
"backend": { "graphql": true, "rest": true },
"roles": { "global": ["package.json", "src/app.ts"] }
}
```
Reports and CI are the same as for [React Router](#react-frontend-react-router): [`examples/workflow.yml`](examples/workflow.yml). If
resolvers reach data through an injected context (`ctx.loaders.Brand.byId()`), tell farol where the providers live:
`"backend": { "context": { "loaders": ["src/modules/*/loaders/index.ts"] } }`.
### Go service
chi, gin, echo and net/http routes, gRPC service methods and `main` binaries, reach per declaration with interface
calls resolved to their implementations.
```json
{ "language": "go", "surface": "backend" }
```
```sh
go test -coverpkg=./... -coverprofile=reports/cover.out ./...
golangci-lint run --output.json.path=reports/golangci.json --issues-exit-code=0 ./... # enable gosec
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
```
**CI:** copy [`examples/workflow-go.yml`](examples/workflow-go.yml). farol reads modules offline, so the workflow runs
`go mod download` for the base and the head first.
### Python service
FastAPI and Flask routes (routers, blueprints, prefixes), Django URLs, MCP tools/resources/prompts, Celery tasks,
Temporal workflows and activities, Prefect flows and `__main__` scripts. farol parses your code; it never imports it.
```json
{ "language": "python", "surface": "backend", "python": { "roots": ["src", "."] } }
```
```sh
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
```
**CI:** copy [`examples/workflow-python.yml`](examples/workflow-python.yml) (uses `uv`; swap in your installer).
### Add Datadog traffic and errors
Optional, and information only: it never changes the score. With it the report shows how much traffic each affected
page or operation gets, its **distinct production errors** (linked to Datadog), the changed files that already throw
errors, and the services that call each changed endpoint.
```json
{
"datadog": {
"env": "prod",
"rum": { "applicationId": "" },
"apm": { "service": "orders-api", "operations": ["fastapi.request"] }
}
}
```
```sh
DD_API_KEY=… DD_APP_KEY=… npx @winninjs/farol usage --out usage.json # last 14 days
npx @winninjs/farol analyze … --usage usage.json
```
**CI:** the example workflows have a `farol-usage` job: set the `DD_API_KEY` and `DD_APP_KEY` secrets and the
repository variable `FAROL_DATADOG=true`. It reads Datadog once a day and never sees the PR's code. Keys, scopes and
matching rules are in the [reference](docs/reference.md#traffic-and-errors-from-datadog).
## What is supported
| | TypeScript / React frontends | TypeScript / JavaScript backends | Go | Python |
|---|---|---|---|---|
| **Entry points** | React Router route tables (object and JSX forms, lazy routes, split route modules); Next.js Pages and App Router (pages, API routes, layouts, middleware); TanStack Router file-based routes | GraphQL root operations (schema files and `gql` templates) and REST endpoints (Fastify/Express style) | chi, gin, echo and net/http routes; gRPC service methods; `main` binaries | FastAPI and Flask routes (routers, blueprints, prefixes); Django URLs; MCP tools, resources and prompts; Celery tasks; Temporal workflows and activities; Prefect flows; `__main__` scripts |
| **Reach precision** | per declaration (TypeScript type checker) | per module, per resolver for GraphQL | per declaration (`go/types`, interface calls resolved to implementations) | per declaration (Python `ast` with local type inference) |
| **Contract changes** | — | GraphQL schema (breaking / dangerous / safe) | Handler request and response types (net/http, chi, gin, echo); gRPC messages | FastAPI parameters, body, response and Pydantic models |
| **Dependencies** | `package.json` + npm/pnpm/yarn/bun lockfiles | same | `go.mod` + `go.sum` | `pyproject.toml` (PEP 621, extras, groups, uv, Poetry), `requirements.txt` + `uv.lock`/`poetry.lock`/`Pipfile.lock` |
| **Diff coverage** | Istanbul `coverage-final.json` (Vitest, Jest) | same | `go test -coverprofile` | coverage.py JSON (`pytest --cov`) |
| **Lint** | `eslint -f json` | same | golangci-lint JSON | ruff JSON |
| **Security** | built-in rules (XSS sinks, `eval`, hard-coded secrets, `javascript:` URLs…) | same | gosec (through golangci-lint) | flake8-bandit (ruff `S` rules) |
| **Vulnerabilities** | `npm audit --json`, `pnpm audit --json` | same | `govulncheck -json` | `pip-audit --format json` |
| **Suppressions** | `eslint-disable`, `@ts-ignore`, `@ts-expect-error`, `as any`… | same | `//nolint`, `//lint:ignore`, `#nosec` | `# noqa`, `# type: ignore`, `# nosec`, `# pragma: no cover`… |
| **Traffic and errors** (optional) | Datadog RUM page views and errors | Datadog APM requests, error spans and callers | same | same |
In every language: environment, infrastructure and CI files (`.env*`, Dockerfiles, compose, CI pipelines, helm, k8s,
terraform…) in a section of their own; incremental analysis since any commit (`--since`); suggested reviewers from
git blame and CODEOWNERS; reports as a GitHub comment, Markdown or a single-file HTML page.
## Reading the review
farol produces two things: a **PR comment** with the verdict, the top files and a test checklist, and a
**single-file HTML report** (linked from the comment) where the review happens. Here is what each part is for and how
to use it.
### Verdict and merge risk score
What it tells you is how much of your time the PR deserves.
| Verdict | Means | What to do |
|---|---|---|
| ✓ **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 (low coverage, lint errors, a config file). | Check the *Start here* files, then approve. |
| ⚠ **Careful review** | Wide or silent breakage is possible: a score of 60+, a breaking contract change, a high-severity finding, an untested central file, or code farol could not read. | Read the *Start here* files line by line and test what they reach. |
The **score** (0–100) is the chance that at least one risky item breaks something. Expand *What adds to it* to see
each item and its points: the files by reach × untested share, findings, contract changes, dependencies. The verdict
always comes with its reasons; read them before the diff.
### Start here
The changed code files, riskiest first. Each row shows how many existing pages or operations run the file, its diff
coverage, its findings and its own risk points; with Datadog, also its traffic and production errors. Open the inline
diff: added lines no test runs are marked **○**, lines with findings **⚠**.
**How to use it:** review top-down and tick **Mark as reviewed** on each file. The score drops as you go, so you
always know how much risk is left. Below it come the low-risk files to skim, environment/infrastructure/CI files
(check their values per environment), and files that need no review (docs, lockfiles, tests).
### Behind a feature flag
On every page or operation row, and in a panel at the top: mark an entry point that is entirely behind a flag. Files
that **only** flagged entry points use leave the review, labeled *behind a flag* rather than reviewed. Use it for
work-in-progress features shipped dark, so the remaining score reflects what users will actually hit.
### Pages / Operations
Every new and affected entry point, as a list or a tree by module. Opening one draws **how** the change reaches it,
file by file. With Datadog each one shows its traffic share and its distinct errors, busiest first. **Use it** to
judge blast radius: a change reaching 3 rarely used admin pages is not the same as one reaching the checkout.
### Contracts (backends)
Each endpoint whose request or response shape changed, as **breaking** (clients built for the base can fail),
**dangerous** (they may misbehave) or **safe** (compatible additions), with the exact field and the calling services
from Datadog. **Use it** to confirm every breaking change is intended and its clients are updated, then tick
*checked with clients*.