farol

了解合并拉取请求的风险,以及风险所在。

一个免费、开源的 CLI 和 GitHub Action。在你的 AI 审查之后,farol 会告诉批准者这次合并的风险有多高、哪些文件承载了风险,从而让风险最高的改动获得最多关注。

网站·在线演示·快速开始·参考

License: MIT Node.js 22+ TypeScript · Go · Python Shadow mode


智能体写代码,智能体审查代码。最后拍板的仍然是人:批准合并与否。这个批准环节才是稀缺的部分,而它被均匀分摊到每一个拉取请求上,就变成了走马观花,于是一行对共享 hook 的改动就能让四十个页面挂掉。

farol 出现在 AI 审查之后,而不是取代它。你的审查智能体会对 diff 中的行发表评论。farol 给批准者提供智能体无法可靠给出的东西:接受这项改动的风险,以及风险所在。目标不只是更快的审查:低风险的拉取请求可以快速通过,而有风险的则要逐行阅读,从承载风险的文件开始。它基于静态分析(编译器自身对你代码的看法,而非模型的猜测),因此同一个拉取请求总是得到相同的答案。

Claude Code 团队在Claude Code 团队如何做代码审查中描述了同样的原则:审查投入跟随改动的风险。正如他们所说,“是改动决定层级,而不是作者。”

farol(葡萄牙语意为灯塔)在审查者打开 diff 之前完成分诊:

  • 这项改动触及了什么?运行被改动代码的每一个现有页面、API 操作、RPC 方法、任务和脚本,以及到每一个的调用链。
  • 这次合并风险有多高?一个判定(快速通道、快速审查、仔细审查)和一个 0–100 的风险评分,依据是触及范围、diff 覆盖率、安全发现、契约破坏和依赖变更。
  • 我从哪里开始?风险最高的文件优先,附带行内 diff、标记未测试的行,以及一份需要手动尝试的检查清单。

免费且开源(MIT)。以 CLI 方式运行,使用npx @winninjs/farol,或在每个拉取请求上通过 GitHub Actionwinnin/farol/action运行。无需账号,无托管服务:它运行在你的机器或 CI runner 上,对每个 PR 发表评论,且从不阻止合并。它从不执行你的代码,也从不把代码发送到任何地方。

The farol report of a pull request: a careful review at 74/100, the two files to start with, and three breaking contract changes

在线演示

在一个真实的拉取请求上查看 farol →

该演示展示了 farol 在 CI 中为某个拉取请求生成的报告,该拉取请求基于FastAPI 全栈模板:一个 FastAPI 后端和一个使用 TanStack Router 的 React 前端。该拉取请求为条目添加了状态,并新增了一个 Insights 页面,其中包含评审者应当发现的几类问题:破坏性的 API 变更、用 f-string 拼接的 SQL、一个没有测试的共享组件。

  • 后端报告:⚠ 需仔细评审,一处破坏性的契约变更和一项高严重性的安全问题。
  • 前端报告:⚠ 需仔细评审,一个新页面以及某个共享组件所触及的现有页面。

这些报告是可交互的:可将文件标记为已评审、将页面标记为处于功能开关之后、打开内联 diff、按测试计划操作,并从 Result 标签页复制评审记录。它们与拉取请求评论中链接的是同一批文件(该拉取请求);副本位于docs/demo/。

AI 评审智能体 farol
读取 diff,以及能塞进提示词的所有上下文 在 base 和 head 处的整个程序,并借助编译器(TypeScript 类型检查器、go/types、Pythonast)
告诉你 这些行里可能有什么问题 这些行触及了什么、这次合并有多危险,以及应当先看哪里
答案是 概率性的:每次运行措辞都不同 确定性的:同一个 PR 给出同一份报告,且每条结论都附带其调用链
不确定时 仍可能显得很自信 会说明unknown并请求仔细评审
运行 一个模型,通常是托管服务 在本地或你的 CI 中:它从不执行你的代码,也从不把代码发送到任何地方

按此顺序两者都用。智能体先评审这些行;然后 farol 告诉批准者这次合并需要多少关注、哪些文件值得关注、哪些页面和端点需要手动测试,以及哪些契约变更需要检查其客户端。恰恰当一个小改动有大范围影响时,智能体的评审就不够用了,而这正是 farol 所展示的。

为什么选择 farol

  • 评审注意力流向风险所在之处。一个 40 个文件的 PR 通常只有两三个文件真正可能造成伤害。farol 对它们排序,因此评审者会仔细阅读这些文件,并心安理得地略过其余部分。
  • 它跟随代码,而非文件名。farol 用真正的编译器构建声明图(TypeScript 类型检查器、go/types、Python 的ast)。对formatPrice列出渲染它的十二个页面,而不是“1 file changed”。
  • 契约破坏在客户端之前被发现。移除的响应字段、新增的必填参数、GraphQL、FastAPI、Go handler 和 gRPC 中变更的类型,以及调用每个端点的服务。
  • 审查整个 PR,或只看你上次查看后变更的部分。每次推送都会生成两份报告:完整的 pull request,以及仅包含自上次推送以来的提交。经过一轮修复后,审查者检查新的提交,而不是从头开始。
  • 它使用你 CI 中已有的内容。覆盖率、lint、安全和审计报告会转化为变更行上的差异覆盖率和发现项。不会重复运行任何内容。
  • 失败即关闭。当 farol 无法读取某处变更时,它会说明(unknown)并要求仔细审查。它绝不会因为不理解某事物就称其安全。
  • 为流程中的人而构建。将文件标记为已审查,将页面标记为位于功能开关之后,勾选发现项,并将结果粘贴回 PR 作为审查记录。

目录

开始使用

你需要Node.js 22+和git(以及go用于 Go 项目,python3Python 项目需要 3.11+)。无需安装:使用npx @winninjs/farol运行 farol。

1. 描述你的项目在项目根目录的farol.config.json中。一行就足以开始;选择你的技术栈:

你的项目 farol.config.json 完整配方
使用 React Router 的 React 应用 { "routes": { "file": "src/routes.tsx" } }(构建路由表的文件) React Router
Next.js 应用(Pages 或 App Router) { "routes": { "adapter": "next" } } Next.js
使用 TanStack Router 的 React 应用 { "routes": { "adapter": "tanstack-router", "dir": "src/routes" } } TanStack Router
TypeScript API(GraphQL、REST) { "surface": "backend" } TypeScript API
Go 服务 { "language": "go", "surface": "backend" } Go
Python 服务(FastAPI、Flask、Django、Celery……) { "language": "python", "surface": "backend" } Python

2. 检查 farol 看到的内容:它找到的页面、端点、任务和脚本。

npx @winninjs/farol routes

3. 分析一个分支对照其目标分支并打开报告。

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     #Linux 上的 xdg-open

示例:使用 React Router 的 React 应用

cd path/to/your/react-app
echo '{ "routes": { "file": "src/routes.tsx" } }' > farol.config.json

npx @winninjs/farol routes                                        #列出你的页面:"/"、"/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

报告已经显示结论、评分、应优先查看的文件、变更触及的页面以及需要手动测试的内容。后续步骤:添加你的覆盖率、lint 和审计报告,并在每个 PR 上运行 farol,使用适合你技术栈的配方。每个选项都在参考文档中,并且按技术栈在examples/README.md中。

经常运行它?npm install --global @winninjs/farol会给你一个简单的farol命令。

配方

每个配方包含三部分:要提交的farol.config.json、要生成的报告,以及 GitHub Actions 工作流。examples/中的工作流是完整的,并且共享同一种形态:

  1. tests运行你的测试、linter 和审计,并保留报告。它运行 PR 的代码,因此没有密钥,且使用只读令牌。如果你已经有测试任务,保留它并添加其上传步骤。
  2. farol在其后运行(无论通过还是失败),读取 git 和报告,并在每次推送时发布一条新评论,其中包含一个在浏览器中打开 HTML 报告的链接,外加一份仅包含自上次推送以来提交的第二份报告。它从不运行 PR 的代码。
  3. 草稿会被跳过,fork PR 不会收到评论(其令牌是只读的),并且每个 farol 步骤都是continue-on-error。

一旦它对你有效,就将 action 固定到某个提交:winnin/farol/action@<sha> # main as of <date>。

examples/README.md针对每个技术栈深入介绍:每个配置键、CLI 参数和 action 输入,以及如何检查配置。

React 前端(React Router)

React Router 路由表(对象和 JSX 形式、懒加载路由、拆分路由模块),按声明计算可达性。

{
  "routes": { "file": "src/routes.tsx" },
  "roles": { "global": ["src/main.tsx", "src/App.tsx", "src/styles/global.css", "package.json", "vite.config.ts"] }
}
npx vitest run --coverage.enabled --coverage.reporter=json --coverage.reportsDirectory=reports/coverage
npx eslint -f json -o reports/eslint.json <changed files>
npm audit --json > reports/audit.json                     #或 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:复制examples/workflow.yml到.github/workflows/farol.yml。

在 Vitest 配置中设置coverage.include: ['src/**/*.{ts,tsx}'],以便没有测试加载的文件被报告为未测试。拆分路由树放入routes.extraFiles;更完整的配置在examples/farol.config.json中。

Next.js

Pages Router 和 App Router:每个页面、API 路由、布局、_app/_document和中间件,均从文件名读取。动态段([id])、全捕获、路由组和并行槽都能被理解。

{
  "routes": { "adapter": "next" },
  "roles": { "global": ["next.config.*", "package.json"] }
}

pages/和app/在项目中或其src/中找到;当它们位于别处时设置"dir"。报告和 CI 与React Router相同(Jest 或 Vitest 覆盖率、ESLint、审计)。在 tsconfig 继承工作区包("extends": "tsconfig/nextjs.json")的 monorepo 中,在分析前安装依赖,以便该包位于node_modules中。

TanStack Router

基于文件的路由:在 routes 目录下每个调用createFileRoute('<id>')的模块,根路由、无路径布局(_app)和父路由作为其子路由的布局。

{ "routes": { "adapter": "tanstack-router", "dir": "src/routes" } }

报告和 CI 与React Router相同。

TypeScript API(GraphQL 或 REST)

GraphQL 根操作(schema 文件和gql模板,按 resolver 划分)以及 Fastify/Express 风格的 REST 端点。

{
  "surface": "backend",
  "backend": { "graphql": true, "rest": true },
  "roles": { "global": ["package.json", "src/app.ts"] }
}

报告和 CI 与React Router相同:examples/workflow.yml。如果 resolver 通过注入的 context(ctx.loaders.Brand.byId())访问数据,请告诉 farol 这些 provider 所在的位置:"backend": { "context": { "loaders": ["src/modules/*/loaders/index.ts"] } }。

Go 服务

chi、gin、echo 和 net/http 路由,gRPC 服务方法和main二进制文件,按声明可达,接口调用会解析到其实现。

{ "language": "go", "surface": "backend" }
go test -coverpkg=./... -coverprofile=reports/cover.out ./...
golangci-lint run --output.json.path=reports/golangci.json --issues-exit-code=0 ./...   #启用 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:复制examples/workflow-go.yml. farol 离线读取模块,因此工作流会先go mod download为 base 和 head 运行。

Python 服务

FastAPI 和 Flask 路由(router、blueprint、前缀)、Django URL、MCP 工具/资源/提示词、Celery 任务、Temporal 工作流和活动、Prefect flow 和__main__脚本。farol 解析你的代码;它从不导入代码。

{ "language": "python", "surface": "backend", "python": { "roots": ["src", "。"] } }
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:复制examples/workflow-python.yml(使用uv;替换为你的安装器)。

添加 Datadog 流量和错误

可选,仅供参考:它从不改变评分。启用后,报告会显示每个受影响页面或操作获得多少流量、其不同的生产错误(链接到 Datadog)、已经抛出错误的变更文件,以及调用每个变更端点的服务。

{
  "datadog": {
    "env": "prod",
    "rum": { "applicationId": "<RUM application id>" },
    "apm": { "service": "orders-api", "operations": ["fastapi.request"] }
  }
}
DD_API_KEY=… DD_APP_KEY=… npx @winninjs/farol usage --out usage.json      #最近 14 天
npx @winninjs/farol analyze … --usage usage.json

CI:示例工作流有一个farol-usagejob:设置DD_API_KEY和DD_APP_KEYsecret 以及仓库变量FAROL_DATADOG=true。它每天读取一次 Datadog,从不查看 PR 的代码。密钥、权限范围和匹配规则见参考。

支持的内容

TypeScript / React 前端 TypeScript / JavaScript 后端 Go Python
入口点 React Router 路由表(对象和 JSX 形式、懒加载路由、拆分路由模块);Next.js Pages 和 App Router(页面、API 路由、布局、中间件);TanStack Router 基于文件的路由 GraphQL 根操作(schema 文件和gql模板)以及 REST 端点(Fastify/Express 风格) chi、gin、echo 和 net/http 路由;gRPC 服务方法;main二进制文件 FastAPI 和 Flask 路由(routers、blueprints、prefixes);Django URL;MCP 工具、资源和提示词;Celery 任务;Temporal 工作流和活动;Prefect flow;__main__脚本
达到精确 按声明(TypeScript 类型检查器) 按模块、按 resolver(GraphQL) 按声明(go/types,接口调用解析到实现) 按声明(Pythonast带本地类型推断)
契约变更 — GraphQL schema(破坏性 / 危险 / 安全) 处理函数请求和响应类型(net/http、chi、gin、echo);gRPC 消息 FastAPI 参数、请求体、响应和 Pydantic 模型
依赖 package.json+ npm/pnpm/yarn/bun 锁文件 同上 go.mod+go.sum pyproject.toml(PEP 621、extras、groups、uv、Poetry),requirements.txt+uv.lock/poetry.lock/Pipfile.lock
Diff 覆盖率 Istanbulcoverage-final.json(Vitest、Jest) 同上 go test -coverprofile coverage.py JSON(pytest --cov)
Lint eslint -f json 同上 golangci-lint JSON ruff JSON
安全 内置规则(XSS sink、eval、硬编码密钥、javascript:URL……) 同上 gosec(通过 golangci-lint) flake8-bandit(ruffS规则)
漏洞 npm audit --json,pnpm audit --json 同上 govulncheck -json pip-audit --format json
抑制 eslint-disable,@ts-ignore,@ts-expect-error,as any…… 同上 //nolint,//lint:ignore,#nosec # noqa,# type: ignore,# nosec,# pragma: no cover……
流量和错误(可选) Datadog RUM 页面浏览量和错误 Datadog APM 请求、错误 span 和调用方 同上 同上

在每种语言中:环境、基础设施和 CI 文件(.env*、Dockerfile、compose、CI 流水线、helm、k8s、terraform……)单独放在一个部分;自任意提交起的增量分析(--since);根据 git blame 和 CODEOWNERS 推荐审查者;报告以 GitHub 评论、Markdown 或单文件 HTML 页面形式呈现。

阅读审查结果

farol 产出两样东西:一条PR 评论包含结论、重点文件和测试清单,以及一份单文件 HTML 报告(从评论中链接),审查就在其中进行。以下是每个部分的用途以及如何使用它。

结论和合并风险评分

它告诉你的是这个 PR 值得你花多少时间。

结论 含义 该怎么做
✓快速通道 只有新代码、测试或低风险变更。 略读 diff 并批准。
◐快速审查 现有代码以受控方式变更,或信号较弱(覆盖率低、lint 错误、配置文件)。 检查从这里开始文件,然后批准。
⚠仔细审查 可能出现大范围或静默破坏:评分 60+、破坏性契约变更、高严重度发现、未经测试的核心文件,或 farol 无法读取的代码。 阅读从这里开始文件,逐行检查并测试它们触及的内容。

该评分(0–100) 是至少一个风险项破坏某些东西的概率。展开增加风险的因素查看每个条目及其分值:按触达范围 × 未测试比例排列的文件、发现项、契约变更、依赖。结论总是附带理由;在查看 diff 之前先读这些理由。

从这里开始

变更的代码文件,风险最高的在前。每一行显示有多少现有页面或操作会运行该文件、其 diff 覆盖率、其发现项及其自身风险分值;使用 Datadog 时,还会显示其流量和生产错误。打开内联 diff:没有测试运行的添加行会被标记○,带有发现项的行标记为⚠。

如何使用:自上而下审查并勾选标记为已审查在每个文件上。随着你的推进分数会下降,因此你始终知道还剩多少风险。其下是需快速浏览的低风险文件、环境/基础设施/CI 文件(按环境检查其取值),以及无需审查的文件(文档、锁文件、测试)。

位于功能开关之后

在每个页面或操作行上,以及顶部的面板中:标记一个完全位于开关之后的入口点。仅使用仅被开关控制的入口点的文件会退出审查,标记为位于开关之后而非已审查。将其用于以暗发布方式上线的进行中功能,这样剩余分数反映的是用户实际会触及的内容。

页面 / 操作

每个新增和受影响的入口点,以列表或按模块的树形展示。打开一个即可查看变更如何逐文件地触达它。使用 Datadog 时,每个入口点会显示其流量占比和其不同错误,最繁忙的在前。用它来判断影响范围:一个变更触达 3 个很少使用的管理页面,与触达结账流程并不相同。

契约(后端)

每个请求或响应结构发生变更的端点,标记为破坏性(为基线构建的客户端可能失败)、危险(它们可能行为异常)或安全(兼容性新增),并附上确切的字段以及来自 Datadog 的调用服务。用它来确认每个破坏性变更都是有意为之且其客户端已更新,然后勾选已与客户端核对。

A breaking contract change: GET /brands no longer returns response[].name

安全、依赖、覆盖率

分数背后的证据:变更行上的安全发现项(每项带一个已核对复选框)、带有已知漏洞的新增或升级依赖,以及每个文件的 diff 覆盖率。未提供的报告显示为未验证,绝不要当作通过。

测试计划

需要手动尝试的新增和受影响的页面或操作,先试新增的,再按触及它们的文件的风险排序。PR 评论中将其列为任务列表。使用它当你在本地或预览环境运行该分支时。它跟随你的审查进度:一旦某个条目背后的每个已更改文件都被标记为已审查,该条目也会被标记(✓ 文件已审查),或在标志后显示 🚩。它自己的复选框(用于手动测试)仍由你勾选。

审查者

谁应该审查:编写此 PR 所更改代码的人(git blame,按每个文件的风险加权),加上 CODEOWNERS。PR 自身的作者和机器人被排除,不活跃的人会被标注。使用它来请求了解风险最高文件的人,而不只是任何有空的人。

结果

审查的 Markdown 记录:初始判定和分数、当前剩余的分数、被标记的入口点、已审查的文件,以及已检查的发现和契约变更。复制为 Markdown并粘贴到 PR 中作为你的审查评论,以便下一个人知道查看了什么。

自上次推送以来

在向开放的 PR 推送后,评论会附带第二份报告,仅包含自上次推送以来的提交(analyze --since <sha>;在强制推送后,从其合并基点开始)。其判定、分数和从这里开始仅覆盖使用它用于审查的第二轮:检查修复是否完成了要求且没有其他改动,然后仅当新提交触及新地方时才回到完整报告。

类别和原因

保留在详情以及评论的折叠块中,以便审查者先形成自己的意见:isolated(仅新增)、touches-existing(现有代码已更改)、global(全局文件、清单或关键依赖)以及unknown(部分更改无法读取:绝不安全)。每个原因代码都在docs/reasons.md中。

工作原理

  1. 对比合并基点和头部,检测重命名并忽略仅空白和尾随逗号的差异行。
  2. 在不运行任何东西的情况下读取两棵树:TypeScript 编译器 API、go list+go/types(离线)、Pythonast。从中得到入口点以及哪个声明使用哪个声明的图。
  3. 比较声明通过其代码的哈希,因此注释和格式从不计入。新声明和入口点是增量的。
  4. 可达从每个变更的声明到运行它的每个入口点,保留最短链路。
  5. 分类、权衡并规划:类别、评分、判定、起始文件、测试清单。

在四个仓库的 662 个历史提交上,快速通道有在 56 个提交中 0 个问题且评分的 AUC 为0.72–0.83;在你自己的历史记录上运行。farol calibrate各语言的细节见docs/how-it-works.md,每个选择背后的理由见docs/DECISIONS.md。

安全与隐私

  • analyze 从不执行代码被分析仓库的从不使用网络。Go 和 Python 辅助程序只使用其标准库且只做解析。
  • 只有comment(GitHub API)和usage(Datadog API)会访问网络。错误消息不会从 Datadog 复制,只复制错误类型。
  • 在 CI 中,运行你代码的任务没有密钥也没有写令牌;发表评论的任务从不运行你的代码。
  • 影子模式:farol 只提供信息,从不批准、请求更改或使检查失败。

局限性

  • 静态读取:运行时构建的值不可见(计算出的路由路径、getattr,importlib,DI 容器、循环填充的注册表)。当入口点无法读取时,PR 会被unknown;当调用无法解析时,farol 假定它触达更多,而非更少。
  • TypeScript 前端:仅支持 React Router、Next.js 和 TanStack Router(基于文件):不支持基于代码的 TanStack 路由树、Remix/React Router 文件约定、Vue 或 Svelte。JavaScript 文件、重命名文件和 TypeScript 后端使用模块级可达性。
  • Go:来自 chi/gin/echo 分组之外中间件栈的路由前缀不会被应用,用自身注册 API 包装路由器的项目类型(Gitea 的web.Router)不会被读取。Python:Django REST Framework 的路由器不会被读取。
  • 评分和判定阈值是在少数仓库上检验过的启发式规则。请在你的仓库上校准它们。

常见问题

我们已经有 AI 审查器了。为什么还要加 farol?它们回答不同的问题。智能体评论 diff 中的代码行;在它之后,farol 通过对整个程序的静态分析,告诉批准者这次合并有多大风险以及哪些文件带来风险。两者都运行:farol 的评论和报告与智能体的并排显示,且都不阻塞合并。

是免费的吗?是的。farol 在 MIT 许可证下开源,无需账号、托管服务或付费层级:用以下命令运行它npx @winninjs/farol或使用 GitHub Actionwinnin/farol/action。

它会拖慢 CI 吗?不会。farol 在你的测试之后运行,并复用它们的报告。对于一个 300 个文件的 Python 服务,分析大约需要 3 秒;对于大型 TypeScript monorepo 或冷缓存的 Go 服务,需要 8–10 秒;当辅助输出被缓存时,基础部分耗时不到一秒。

我可以在 GitHub 之外使用它吗?可以。analyze、html和report可在任何地方使用;只有comment是 GitHub 专属的。将npx @winninjs/farol report的输出与你的平台 API 一起提交。

报告显示“未验证”。该报告未被提供,或者路径有误。传给analyze的路径相对于当前目录;farol.config.json中的路径相对于项目。

我修改的页面未被列出。运行npx @winninjs/farol routes以查看 farol 读取的入口点,运行npx @winninjs/farol impact <file>以查看哪些内容到达某个文件。无法解析的路由构造会列在诊断信息下。

Go:“farol 离线读取 Go 模块:运行 go mod download”。模块缓存缺少 base 或 head 的某个依赖。在分析前对两者都运行go mod download。

Monorepo?将--cwd指向该应用。路径保持相对于仓库根目录,项目未使用的应用中的更改会被列为在其之外。

贡献

欢迎贡献:路由适配器、框架、语言、报告格式以及更好的启发式方法。参见CONTRIBUTING.md了解环境搭建、架构和约定,参见SECURITY.md报告漏洞。

许可证

MIT© 2026 Winnin