跳到正文

ripwire

一个 C++23 的命令行工具兼 MCP 服务:把仓库解析成带排名的调用图,然后回答某个符号能到达哪里、一次改动会碰坏什么、该跑哪些测试、以及这次改动把什么变差了。

Screenshot of ripwire
编辑截图, 30 Sep 2026ripwire ↗

这是什么

一个 C++23 二进制,把仓库读成一个带排名、结果确定的图谱并打到标准输出,瞄准的正是编码 agent 准备花 token 去翻文件的那一刻。一次串行爬取加上每种语言一个 tree-sitter 查询引擎,喂给一次 Personalized PageRank;所有动词都只是对同一张图的不同读法——改动前用 --callers、--deps、--arch、--impact、--situ,改动后用 --edit-check 与 --quality-delta,而 --for=TASK 或 --pack-task 会按 token 预算给某个具名任务打包一份上下文。同一套计算也通过 MCP 暴露给 agent:33 个动词,用架构文档自己的话说,是「a thin front door onto the same computation and the same renderer as a CLI sibling — one output shape, two surfaces」。构建用 cmake -S . -B build && cmake --build build -j;把它注册成 {"type":"stdio","command":"ripwire","args":["--mcp"]};skills/install.sh --hook 会装上那些在工具调用前提醒 agent 的路由钩子与技能文件。输出是打在 stdout 上的压缩 XML,同样的仓库两次运行逐字节相同;爬取时丢掉的东西——体积上限、被 git 忽略的文件、没能解析的调用——都会写出来,而不是悄悄消失。

谁做的仓库归 GitHub 组织 redhat-et 所有,这个名字展开是 Red Hat Emerging Technologies。除此之外就没有旁的凭据了:仓库元数据里没有 owner 字段,README 全文(209,734 字符)里也不出现 "Red Hat" 字样——所以这层归属是靠组织名和仓库内部路径间接支持的,而不是靠项目自己说的任何一句话。报告里唯一直接的红帽标记在提交邮箱直方图里:dbrewste@redhat.com 出现在 4,135 次提交中的 1,651 次上。至于它出自红帽内部哪个团队,材料没有覆盖。干活的人高度集中:有关联账号的提交里 joyful-ii-V-I 占 3,554 次;25 人的贡献者名单由 barefootski(346)、quaterniondrift(93)与 lennix1337(33)领衔。

它是怎么搭起来的

组成 · 6

一条五阶段、没有回边的流水线——ingest → graph → rank → serialize → cli / mcp——每一个阶段的输出都是普通数据结构,这正是让单个阶段可以独立测试、并让每个动词都只是对同一张图的另一种读法的原因。三个后果贯穿代码。爬取是串行的,并且在分配节点 ID 之前先把候选路径排好序,因为节点 ID 是那张表的下标,一旦依赖目录顺序,ID、top-K 截断和每一个与 diff 相关的动词都会在同一次树上抖动;线程都在解析池里。抽取是一个查询引擎跑每种语言一份 queries/<language>/tags.scm,而不是每种语言手写一遍遍历——架构文档点名否掉了另一条路。至于爬取时丢掉什么,都是有数字支撑的决定而不是默认值:通用 4 MB 上限;JSON 超过 256 KB 或嵌套超过 512 层就跳过;YAML 单独给 512 KB,因为套用 JSON 的那条线会把手工维护的配置丢掉;TOML 完全没有上限,因为一次测量显示 321 个文件里最大的一个是 57,759 B;而所有体积原因被丢下的文件都计进头部的 skipped_oversize=,不会凭空消失。

src/ingest*.h
ingest 阶段:一个编译单元,脊梁是 src/ingest.cpp(29,564 字节),各个家族放在带守卫的分节里——ingest_cache.h(255,885 字节)、ingest_relations.h(140,685)、ingest_names.h(130,361)、ingest_sidecap.h(130,077)、ingest_binds.h(123,635)、ingest_crawl.h(116,419)、ingest_astquery.h(108,592),再加上预热的 tags.scm、解析池、文档后处理与模型收尾几节。src/ 整体是 151 个文件、11,275 KB。
src/graph.h 与 src/pagerank.cpp
graph 与 rank 两个阶段。graph.h 有 453,160 字节;pagerank.cpp 10,939 字节,配一个 2,174 字节的头文件——刻意保持小,因为确定性规则禁止在这个编译单元里对浮点做重结合。符号靠 Personalized PageRank 排名,而那个顺序是其余大部分动词赖以成立的东西。
src/main.cpp、src/cli.h 与 src/verbs_*.h
动词家族与共用的输出机制。serialize.h 564,798 字节、cli.h 523,742、quality.h 541,224、main.cpp 347,197。家族本身是 verbs_for.h(287,589)、verbs_report.h(241,378)、verbs_navigate.h(175,046)、verbs_quality.h(160,168)、verbs_lint.h(126,375)、verbs_change.h(91,385)、verbs_grep.h(71,657)与 verbs_doctor.h(66,917)。src/infra/ 有 39 个文件,其中有 sortutil.h(9,889)——#342 之后五处比较器统一改走的就是它。
src/mcp*.h
MCP 面:33 个动词,按架构文档自己的说法,它们是通往「与 CLI 同源的那套计算与同一个渲染器」的一扇薄门。mcpverbs.h 352,422 字节、mcp.h 201,355、mcprefusal.h 96,085、mcpedit.h 86,563、mcpindex.h 77,824、mcpjson.h 43,368、mcpserver.h 30,626。仓库根还有一个 101 字节的 .mcp.json,把这个服务注册给它自己。
queries/ 与 third_party/
抽取用的数据与整棵 vendored 依赖树。queries/ 下有 23 个 <language>/tags.scm,从 1,367 字节(bash)到 28,032 字节(C++)。third_party/deps 有 211 个文件、242,952 KB——tree-sitter 内核、doctest,以及 bash、C、C++、C#、CUDA、Dart、Elixir、GDScript、Go、Java、JavaScript、JSON、Kotlin、Lua、Markdown、Objective-C、PHP、Python、Ruby、Rust、Swift、TOML、TypeScript 与 YAML 的语法。third_party/patches/ 里是对这些 vendored 扫描器打的 14 个补丁,third_party/ 本身放着那些 header-only 的部件,CMakeLists.txt 有 100,647 字节。
test/、skills/ 与 hooks/
闸门套件与 agent 一侧的管线。test/ 有 718 个文件、13,573 KB,其中 661 个是由 test/pargates.py 驱动的 *check.sh 闸门,登记在 test/regression.sh 里、由 test/manifestcheck.sh 稽查;光是 test/fuzz/ 就有 90 个文件。skills/ 里放着 install.sh(33,469 字节)与 17 份技能文档,hooks/ 里是五个 shell 钩子(ripwire-nudge.sh 有 107,068 字节),而 .claude/skills/、.codex-plugin/ 与 .coderabbit.yaml 装着按 agent 分别的配置。

取舍,以及它替代了什么

  • 爬取保持串行,并在任何节点 ID 出现之前先把候选表排好 替代 按目录顺序分配 ID,并把遍历并行化

    节点 ID 就是那张有序候选表的下标,所以文档写的是后果而不是偏好:按目录顺序会让节点 ID、top-K 截断和每一个与 diff 相关的动词在同一次树上抖动。它同样明确说了并行去了哪里——爬取「is the cheap half and is deliberately not parallelized; the parse pool is where the threads are」。

  • 一个查询引擎读每种语言一份 tags.scm 替代 每种语言手写一遍 AST 遍历

    架构文档点名并否掉了另一条路:「a bespoke AST traversal per language — is forbidden here」,理由是那条路等于「five fragile walkers that break on every grammar bump instead of one query loop that survives them」。于是每种语言要做的变成了数据——一份查询文件加一张 capture 名到角色的表——这也是仓库里能装下 23 份的原因。

  • Rails schema 里的列只是定义,不产生边 替代 让 create_table 的列带上调用边与 PageRank 权重

    pull request 里的原话是「Definitions only (maintainer decision after review).」由 create_table 块铸出的列是按内容而不是按路径识别的,能回答 --uses、--whereis、--grep 和地图,但 buildGraph 的 byName 会跳过 Section && Lang::Ruby,于是 --callers=id 报的是 defs="14" count="0"——定义找得到,边被拒绝。

  • TOML 不给专属上限,YAML 给 512 KB 而不是 JSON 的 256 KB 替代 让所有配置格式共用同一条上限

    这件事被写成「TOML has no lane-specific ceiling, and that is a measured decision rather than a missing sibling」:在 90 多个公开仓库、321 个 .toml 文件上,最大的一个是 57,759 B,所以任何上限都不可能既高于实测最大值、又低于通用的 4 MB 跳过线。YAML 正好相反——套用 JSON 的 256 KB 会把真实手工维护的配置丢掉,因为 NeMo 的 cicd-main.yml 有 293 KB。

  • 目录符号链接一概不跟随 替代 跟随它们,再用 inode 记录来断开环

    「There is no inode tracking, because with symlink-following off there is nothing for it to do.」遍历只用 skip_permission_denied 打开,所以被软链的目录根本不会被进入,环也就无从产生。

依据docs/ARCHITECTURE.md(50,755 字节/50,456 字符——报告只打印前 12,000 字符)、AGENTS.md(2,930 字符)、架构文档候选清单、完整的 2,951 个文件树及其逐个文件体积,以及用自己话写下决定的那些 pull request(#339、#359、#363)。

制作过程

6 个阶段
  1. 01

    七月底出现,到九月已经发了二十个 release

    仓库建于 2026-07-29,而它最老的一条提交落在 2026-07-31,标题是 import from internal development tree——材料里关于这段代码此前从哪来的唯一说法,就只有这一句。此后它的速度是本档案很少记到的:4,135 次提交,其中 7 月 17 次、8 月 1,294 次、9 月 2,824 次;对应 2,374 个星、151 个 fork、8 个 watcher、58 个待处理 issue,Apache-2.0,语言 C++。它发了 20 个 release,每一个都既不是 prerelease 也不是 draft,从 2026-08-02 的 v0.1.0(标题为 ripwire v0.1.0 — the ripgrep of AI context)到 2026-09-27 的 v0.6.5;其中八个落在 29 小时之内:v0.3.0 在 2026-08-11 的 23:31,v0.3.8 在 2026-08-13 的 04:11。标签列表里有 v0.3.7 而它没有对应的 release,也没有 v0.1.0;标签一共 20 条,和 release 列表一样长。提交里数出 3,205 条共同作者尾注,最多的是 Claude Opus 5 的 1,417 条,其后是 Claude Fable 5 的 698 条与 Claude Fable 5.1 的 543 条,另有 Qwen3.8 Max 的 2 条。三份提交直方图彼此并不一致——joyful-ii-V-I 按作者名算是 3,993 次、按关联账号算是 3,554 次,而邮箱那一栏由 dbrewste@redhat.com 的 1,651 次领先。

  2. 02

    五个阶段、没有回边,以及为每一句断言配一道闸

    架构文档把流水线写成五个阶段——ingest → graph → rank → serialize → cli / mcp——并用一句话把规矩定死:「Five stages, in that order, with no back edges. Each stage’s output is a plain data structure, so any stage can be tested in isolation and every verb is a different way of reading the same graph.」第一阶段里有两个选择担着大部分重量。爬取会先把每一个候选路径收齐,按字节字典序排序,然后才分配节点 ID,因为 ID 就是那张有序表的下标;文档说,若按目录顺序来,节点 ID、top-K 截断以及每一个与 diff 相关的动词都会在同一次树上产生抖动。而遍历本身保持串行——它「is the cheap half and is deliberately not parallelized; the parse pool is where the threads are」。抽取是一个查询引擎跑每种语言一份 queries/<language>/tags.scm(树里 23 个),另一条路被点名否掉:「a bespoke AST traversal per language — is forbidden here」。同一套直觉也管着测试:AGENTS.md 把规矩写成「Gate first, code second」,而机制是——新增一个 test/*check.sh 必须在同一次提交里登记进 test/regression.sh,否则 test/manifestcheck.sh 会失败。test/ 有 718 个文件,其中 661 个是 *check.sh 闸门,而这个数字每周都在动:#337 里重建 deck 时是 649 道,#341 是 650 道,一位贡献者引的是 651 道,而某次在负载机器上跑的完整 pargates.py -j 6 返回的是 665 道闸、657 通过、4 个环境跳过、4 个失败。

  3. 03

    那个花掉 67 GB 的家目录

    #350 由 KilimcininKorOglu 在 2026-09-27 提出,正是内存守卫存在的理由。当 MCP 服务以 {"type":"stdio","command":"ripwire","args":["--mcp"]} 注册进 Claude Code、而会话开在家目录里时,一次 grep 调用把 --mcp 进程在七小时里推到 67 GB 的 phys_footprint;系统 swap 涨到 28 GB、只剩 458 MB 可用——机器是 Apple M1 Max、64 GB 内存、macOS 27.0。原因写在报告里并被维护者确认:在不是 git 仓库的根目录上无法套用 .gitignore,于是爬取会把该目录下的一切都做成图,而没有任何东西给这块内存设上界。#363 分几层作答。没人主动选过的根会被拒绝——$HOME 本身(即便它是 git 仓库)、文件系统或盘符根、各用户家目录的父目录、以及 /System、/usr、/etc、/proc、/sys、%WINDIR%、Program Files 这类系统树——回一行「no project root: <dir> is a home/system directory; pass a project path」,同一条规则也管到 MCP 请求里的 path=。为了证明它,还需要一个专门的测试接缝:RIPWIRE_TEST_MEMGUARD=hard:N 让第 N 次硬上限读数算作越界,而 B14 这条臂用 hard:1 跑两个极小的根,断言只存在一个 ingest 缓存块、且 stderr 里点名的是「root 1 of 2」。B12 那条臂则关掉 AddressSanitizer 的隔离区来跑,因为隔离区会把这颗被测量的 footprint 抬大。

  4. 04

    一条从来没跑通过的健康检查流水线

    2026-09-27,llvm-x86 用 #342 报告「文档里写的 Linux sanitizer 仪式在 main 上根本跑不完」,并在同一分钟里拆出六个子 issue——#343 到 #348。根因是一个库的细节:libstdc++ 把 string_view 的比较算成 size_type 里的 n1 - n2,当一个是另一个的前缀时这个减法会回绕;而 G1 那条线开着 -fsanitize=integer 与 -fno-sanitize-recover=all,于是文档里写的自检会中止在一行「unsigned integer overflow: 4 - 16 cannot be represented in type size_type」上。有五处用裸 < 比较 std::string_view:src/ingest_crawl.h 的 pathInIgnoreSet、src/situ.h 里 --situ 的词典序邻居索引两处、以及 src/mention.h 的 finalizeNamedIdents 两处。修法是把五处统一走 rw::sortutil::svLess;维护者在那条评论里把「坏了什么、没坏什么」说得很准:「The ordering is still correct, so release binaries answer correctly.」另有两个子 issue 与 C++ 无关:两个闸门脚本以 100644 模式提交,而这套测试里其它每一个 *check.sh 都是 100755,直接调用会以 126 退出——而 CI 是隔着 shell 调它们的,所以从来没发现;以及文档里那条 GCC -DRIPWIRE_ASAN=ON 路径根本构建不过,因为一个 constexpr 谓词把一个来自 findByField 的指针喂进了 static_assert。#351 用一次提交关掉了 #342 连同六个子 issue,新加的 #6c 臂在 main 上是红的、打上修复后变绿。

  5. 05

    一份报告要长成什么样,才配被信任

    #353 由 toasterbook88 在 2026-09-28 提出,是一份关于报告的报道。对一个 2,077 个文件的 skill 家目录跑 --scan-skills 返回了 156 条发现,其中 107 条是 EXFILTRATE:net-exfil;报告者逐行读了每一条被标出的行,把 107 条按目标地址分了类,并把触发条件收敛到三行。维护者认同这个判断——那条规则是按「a network verb plus any $VAR on the same line」触发的,对变量的值从哪里来一无所知,所以有文档的 API 调用和回环检测被当成 critical 报了出来。真正让这个 issue 值得留下的是第二条评论。报告者用「a single frozen script」重跑了一遍扫描,对它标出的三行逐一手工核对,然后发出更正后的直方图,并写道:第一版「was wrong in two places — one from over-claiming, one from a too-narrow var-trace — and I’d rather correct it explicitly than leave a number standing that a reader can’t reproduce」;更正之后,107 行里大约 89 行是有文档的服务 API 调用。维护者的回复是本档案可以直接引用的标准:「It’s exactly the kind of care that makes a report trustworthy. You withdrew the claims you couldn’t back, froze the classifier, and published the row-level TSV.」后续也走同一条路——#359 在 docs/LINEAGE.md 里给 NVIDIA 的 SkillSpector 补上署名,并且只取它基于代码的那部分检查,理由是「only the code checks come across. The agent’s LLM reads ripwire’s output and does the judging」——而 #360 把同一套推理推到 agent 会加载的指令文件上。

  6. 06

    作者把数字连同它们自己的边界一起发出来

    docs/EVALS.md 有 1,132,024 字节,docs/COMMANDS.md 有 720,784 字节——一个项目把「可测量的说法」当成交付物而不是事后补的东西,就长这样。README 里还放着一份现场报告,由 #361 加上,默认折叠在页面靠上的位置,署名是「Claude Fable 5.0, the frontier model orchestrating ~20 coding agents over two days on a ~1,500-file C++/Metal codebase」。它的数字很大,标注也一样明确:审计与研究阶段大约一半的 token 花费,原文称这是「the operator’s whole-phase estimate」;一次 --callers 查询返回了零个生产调用方;--edit-check 标出了文本搜索漏掉的 6 个调用点中的 5 个;以及「a dozen-plus real code-quality defects fixed, not waived」。pull request 正文在同一口气里写明边界——这只是某一次合作、发生在更早的版本上,是「the orchestrating model’s own report and not a controlled measurement」——而报告的全文是一张点击展开的图片 docs/assets/field-report-multi-agent.jpg,1144×1600、567 KB,图下另附一份为无障碍加的纯文本版。README 自己的那些数字也放在同一个框里:建索引 0.25–0.45 秒、strict file@10 为 58.3%(对照 40.0%)、签名比函数体少 74.7% 字节、profile-guided optimisation 带来 14–25% 的提升——全部出自作者,而且 README 自己说这些计时列没有被重新测过。

相关档案

全部档案 →