Farol:一个开源、无LLM的拉取请求风险地图(TypeScript、Go、Python)
Farol 是巴西公司 Winnin 提供的一款免费、MIT 许可的 CLI 和 GitHub Action,它为批准拉取请求的人员生成风险地图。它在 AI 审查代理之后运行,使用静态分析而非模型,并返回 0-100 的合并风险评分、一个判定(快速通道、快速审查、仔细审查),以及风险最高的变更文件排序列表,每个文件涉及到的页面、端点、任务和脚本。
Farol 的作用,用一段话概括
farol 是一个 CLI(npx @winninjs/farol)和一个 GitHub Action(winnin/farol/action),它使用项目语言的真实编译器分析拉取请求的基础分支和当前分支,并报告合并的风险程度以及风险所在位置。它不执行被分析仓库的代码,不将代码发送到任何地方,也从不阻止合并。它被设计为位于 AI 审查之后,而不是替代它:审查代理对 diff 中的行进行评论,而 farol 告诉人类批准者这些行会影响到什么。
核心主张:当小改动影响范围大时,代理审查是不够的
该项目明确提出的论点是,批准是稀缺资源,而将注意力均匀分散到每个拉取请求上会使审查变成浏览。作者在 Hacker News 上的原话是,失败模式是具体的:“一个 40 文件的 PR,其中 38 个文件是孤立的,与一个 3 文件的 PR 改变了 23 个端点使用的模型,是完全不同的。”
因此,farol 在审查者打开 diff 之前进行分诊,回答三个问题:
- 这个改动会影响到什么?每个运行了变更代码的现有页面、API 操作、RPC 方法、任务和脚本,以及到达每个的调用链。
- 合并的风险有多大?一个判定加上一个 0-100 的评分,由影响范围、diff 覆盖率、安全发现、契约破坏和依赖变更计算得出。
- 我从哪里开始?风险最高的文件优先,带有内联 diff、未测试行标记,以及手动尝试的检查清单。
README 引用了 Claude Code 团队对同一原则的描述:“变更集决定层级,而不是作者。”
判定和 0-100 合并风险评分
farol 为每个拉取请求标记三个判定之一,每个判定对应一个具体的审查者操作:
- 快速通道:仅新代码、测试或低风险变更。浏览 diff 并批准。
- 快速审查:现有代码以受控方式变更,或较弱信号如低覆盖率、lint 错误或配置文件。检查“从这里开始”文件,然后批准。
- 仔细审查:可能存在广泛或静默破坏。由评分 60 或以上、破坏性契约变更、高严重性发现、未测试的核心文件或 farol 无法读取的代码触发。
评分(0-100)被描述为至少一个风险项破坏某些东西的概率。贡献项在“什么会增加评分”下列出:按影响范围乘以未测试份额的文件、发现、契约变更和依赖。每个判定都附带其理由。
静态分析,而非模型:为什么答案是确定性的
farol 与 AI 审查者的关键架构差异在于,其可达性分析基于编译器而非语言模型。README 的对比表指出,AI 审查代理读取 diff 和提示中能容纳的任何上下文,并给出概率性答案,每次运行措辞不同;而 farol 使用 TypeScript 类型检查器、go/types 和 Python 的 ast 读取基础分支和当前分支的整个程序,并给出确定性答案,每个声明都附带其调用链。
根据作者的 Hacker News 评论,“分析过程中没有 LLM,没有网络,也从不执行被分析仓库的代码。它是确定性的,仅提供信息;从不阻止合并。”对于 Go 和 Python 路径,辅助程序仅使用其标准库,并且只进行解析。
分析流程是:
- 对合并基线和当前分支进行 diff,检测重命名,忽略仅空白和尾随逗号的行。
- 不运行任何东西,使用 TypeScript 编译器 API、
go list加go/types(离线)和 Pythonast读取两个树。 - 通过代码哈希比较声明,因此注释和格式永远不会影响结果。
- 计算可达性:从每个变更的声明到每个运行它的入口点,保留最短链。
- 分类、加权和规划:类别、评分、判定、要开始的文件、测试清单。
支持的技术栈
farol 支持 TypeScript/React 前端、TypeScript 和 JavaScript 后端、Go 和 Python。入口点按语言读取:
- React 和 TypeScript 前端:React Router 路由表(对象和 JSX 形式、懒路由、拆分路由模块)、Next.js Pages 和 App Router(页面、API 路由、布局、中间件),以及 TanStack Router 基于文件的路由。通过 TypeScript 类型检查器按声明计算可达性。
- TypeScript/JavaScript 后端:来自 schema 文件和
gql模板的 GraphQL 根操作,以及 Fastify/Express 风格的 REST 端点。可达性按模块计算,GraphQL 按解析器计算。契约变更来自 GraphQL schema,分类为破坏性、危险或安全。 - Go:chi、gin、echo 和 net/http 路由、gRPC 服务方法和
main二进制文件,接口调用解析到其实现。契约变更来自处理程序请求和响应类型以及 gRPC 消息。 - Python:FastAPI 和 Flask 路由(路由器、蓝图、前缀)、Django URL、MCP 工具、资源和提示、Celery 任务、Temporal 工作流和活动、Prefect 流程和
__main__脚本。契约变更来自 FastAPI 参数、请求体、响应和 Pydantic 模型。
farol 从 CI 复用的输入包括 Istanbul coverage-final.json(Vitest、Jest)用于 diff 覆盖率、go test -coverprofile、coverage.py JSON、ESLint 和 golangci-lint JSON、ruff JSON、通过 golangci-lint 的 gosec、通过 ruff S 规则的 flake8-bandit,以及 npm audit、pnpm audit、govulncheck 和 pip-audit 用于已知漏洞。它还读取抑制注释,如 eslint-disable、@ts-ignore、//nolint、#noqa 和 #nosec。可选的 Datadog RUM 和 APM 集成添加了每个受影响页面或操作的流量份额和不同的生产错误,并且明确仅提供信息:它永远不会改变评分。
输出内容
farol 生成两个产物:一个包含判定、顶级文件和测试清单的 PR 评论,以及一个从该评论链接的单文件 HTML 报告,审查在其中进行。报告部分包括:
- 判定和评分,在 diff 之前列出理由。
- 从这里开始:变更的代码文件按风险从高到低排序,每行显示有多少现有页面或操作运行该文件、其 diff 覆盖率、其发现及其自身的风险点。没有测试运行的新增行用空心圆标记,有发现的行用警告标记。
- 在功能标志后面:将入口点完全标记为在标志后面;仅由标志入口点使用的文件离开审查。
- 页面 / 操作:每个新增和受影响的入口点,作为列表或按模块的树,以及变更到达它的路径。
- 契约(后端):每个请求或响应形状变更的端点,分类为破坏性、危险或安全,带有确切字段和调用服务。README 的示例是一个破坏性变更,其中
GET /brands不再返回response[].name。 - 安全、依赖、覆盖率:评分背后的证据。未提供的报告显示为“未验证”,而不是通过。
- 测试计划:新增和受影响的页面或操作,供手动尝试,作为 PR 评论中的任务列表。
- 审查者:根据每个文件的风险加权的 git blame 建议的审查者,加上 CODEOWNERS,排除 PR 自己的作者和机器人。
- 结果:审查的 Markdown 记录,可以复制回 PR。
- 自上次推送以来:对于开放 PR 的推送,第二个报告仅覆盖自上次推送以来的提交(
analyze --since <sha>)。
每个 PR 的类别保存在折叠块中,以便审查者先形成自己的意见:isolated(仅添加)、touches-existing、global(全局文件、清单或关键依赖)和 unknown。
失败关闭行为和测量准确性
当 farol 无法读取部分变更时,它报告 unknown 并要求仔细审查,而不是假设安全。当调用无法解析时,它假设变更影响更多,而不是更少。
README 报告了来自四个仓库的 662 个过去提交的校准:快速通道在 56 个提交中有 0 个问题,评分的 AUC 为 0.72 到 0.83。项目附带 farol calibrate 命令,以便团队可以在自己的历史上检查评分。README 明确说明评分和判定阈值是在几个仓库上检查的启发式方法。
运行时和 CI 安全模型
farol 在测试任务之后运行并复用其报告,因此不会重复运行任何东西。README 给出了近似分析时间:一个 300 文件的 Python 服务约 3 秒,一个大型 TypeScript monorepo 或冷缓存的 Go 服务 8-10 秒,基础分支在其辅助输出缓存时不到一秒。
推荐的 GitHub Actions 设置使用两个任务。tests 任务运行拉取请求的代码并保留报告,因此它没有秘密和只读令牌。farol 任务在其之后运行,无论通过还是失败,读取 git 和报告,并在每次推送时发布新评论。草稿被跳过,fork 拉取请求不获得评论,因为其令牌是只读的,并且每个 farol 步骤都是 continue-on-error。只有 comment 和 usage 命令接触网络;错误消息不从 Datadog 复制,仅复制错误类型。
farol 需要 Node.js 22+ 和 git,Go 项目需要 go,Python 项目需要 python3 3.11+。analyze、html 和 report 在 GitHub 之外工作;只有 comment 是 GitHub 特定的。
已知限制
- 静态读取无法看到运行时构建的值:计算的路由路径、
getattr、importlib、依赖注入容器以及在循环中填充的注册表是不可见的。不可读的入口点使 PR 变为unknown。 - TypeScript 前端仅限于 React Router、Next.js 和基于文件的 TanStack Router。基于代码的 TanStack 路由树、Remix 和 React Router 文件约定、Vue 和 Svelte 不受支持。JavaScript 文件、重命名文件和 TypeScript 后端使用模块级可达性。
- 在 Go 中,来自 chi/gin/echo 组以外的中间件堆栈的路由前缀不应用,并且使用自己的注册 API 包装路由器的项目不被读取。在 Python 中,Django REST Framework 路由器不被读取。
- 评分和判定阈值是启发式方法,项目建议在您自己的历史上进行校准。
Hacker News 评论者的反对意见
Show HN 帖子(39 分,25 条评论)提出了四个反复出现的反对意见:报告中的信噪比、UI 的品味、机器分配风险评分的想法,以及更基本的问题——这个工具是否真的需要。
报告的密度是最实质性的批评。一位评论者写道:
报告中有很多噪音,我需要更高的信噪比,我已经在阅读大量 AI 输出了。
另一位将同样的观点表述为产品方向论证:
后端演示报告似乎比直接阅读代码更长更复杂。我也在尝试构建类似的东西,但还没有人完全做到这一点。风险地图 / 辅助审查需要简洁且信息密集,以引导您的注意力。我目前的理念是,我们需要注释代码,而不是用一堆文本替换整个代码审查界面。
第三位直接批评了实时演示中的排名:
看了后端演示,建议首先查看的行是导入的东西。这正是我绝对不需要审查的。
关于信任评分,一位评论者说:
这里最近有一个帖子,“使用 LLM 进行置信度评分是个坏主意”,我立刻想到使用 LLM 进行风险评分。似乎是个坏主意。如果有什么仍然需要人类来做,那就是评估风险。
该反对意见专门针对基于 LLM 的评分;farol 的分析路径不包含 LLM,作者声明输出是确定性的,当代码无法读取时返回 unknown 而不是猜测。
两条评论质疑辅助分诊的前提。一条直言不讳:“或者你可以,你知道,阅读你正在批准的变更。”另一条驳斥了演示:“我喜欢这个概念,风险地图在某些情况下可能有用,但我绝对讨厌 UI 设计;它散发着 AI 的味道,没有人类思考或关怀。”
演示拉取请求本身因其包含的代码而受到关注。一位评论者指出,示例变更(描述为使用 Claude 生成)包括在 SQLAlchemy 代码库中使用 f-string 拼接的 SQL,并认为 SQLAlchemy 的表达式语言允许以编程方式构建 SQL,而无需表映射 ORM 层。
该帖子还包含一些被标记或已删除的评论,至少一位评论者问为什么提交者自己的评论被标记,而帖子本身仍然保留。
这适合谁
farol 针对的是 AI 代理编写和审查大部分代码但仍由人类按下合并按钮的团队。它的论点狭窄且可证伪:审查工作应遵循变更的风险,批准者需要关于变更影响范围的确定性、可复现的陈述。帖子中的反驳同样狭窄:比 diff 更长的报告,或将导入行排在最前面,未能完成其引导注意力的唯一工作。
farol 是 MIT 许可的,没有账户、托管服务或付费层级,通过 npx @winninjs/farol 安装或用作 GitHub Action winnin/farol/action。