Codegraph 动态分派覆盖手册:如何系统性地封死静态抽取里的“断链调用”
本文以 codegraph 的设计文档 Dynamic-Dispatch Coverage Playbook 为主体,完整还原“动态分派覆盖缺口”这一问题的分类方法、两套修复机制(resolver 与 synthesizer)、可重复的五步工作流、验证工具链与跨语言/框架的覆盖矩阵现状,并结合 src/resolution/ 下的源码逐条印证关键实现。读完后,你将掌握一套在任何语言与框架上定位、修复并验证“图里缺失的动态调用边”的完整方法论。
该手册开篇就声明了它的受众与任务:面向继续这项工作的 Claude agent,使命是系统性地封死 codegraph 支持的所有语言与框架中动态分派(dynamic dispatch)的静态抽取覆盖缺口,并用同一套方式逐一验证,让跨符号的“流(flows)”在图中处处存在。手册同时指向上层上下文:单个机制(回调合成器)的深度设计在 callback-edge-synthesis.md,按“分派形状”组织的跨切面队列(Redux/RTK Query/NgRx/MediatR/注册表)在 dispatch-synthesizer-backlog.md。
重要更新(2026-06-01):
codegraph_trace与codegraph_context两个 MCP 工具已被移除,codegraph_explore现在是唯一的表层工具,其 "Flow" 部分(buildFlowFromNamedSymbols)正是呈现本手册所关注的合成边之处;覆盖验证改用codegraph_explore/ probe-explore.mjs。因此正文中出现trace(a, b)或把trace/context与工具并列的地方,都应理解为“a→b 的流,现在经由 explore 呈现与验证”。合成器与覆盖矩阵本身不受影响。
1. 目标:为什么“覆盖”才是杠杆
codegraph 的价值在于它是“地图”——回答 grep/Read 回答不了的结构与流问题(trace、impact、callers、“X 如何到达 Y”)。agent 只有在 codegraph 足够时才会用它替代 Read。手册给出了实证结论(引用自项目内部 auto-memory):让答案“足够”的杠杆是覆盖(coverage),而不是 prompt、hook 或新工具——当某条流不在图里时,agent 会去读文件重建它;当流在图里时,agent 可以完全不读文件就答全。
手册引用了 excalidraw 上的端到端验证:在封死 update-flow 缺口之后,2/3 的无头 agent 运行以 Read 0 且答案完整回答了“一次更新如何到达屏幕”——此前因为关键边不在图中,这不可能。手册同时给出谨慎的边界:覆盖使 no-read 路径成为可能,但不强制它(agent 有时仍会读文件确认);答案完整性的提升则是无条件的。
任务就是让上述事实对所有语言/框架成立。
2. 问题分类:动态分派的五种形状与难度梯度
静态 tree-sitter 抽取能捕获显式调用(foo()、this.bar()),但会漏掉所有目标被计算/间接得到的调用。手册把缺口归为反复出现的形状,并给出难度梯度(先做便宜的):
| # | 形状 | 示例 | 修复机制 | 成本 |
|---|---|---|---|---|
| 1 | 命名属性/描述符 | django self._iterable_class(self) |
框架 resolver(claimsReference + resolve()) |
便宜 |
| 2 | 字段回观察者 | onUpdate(cb) + for(cb of cbs)cb() |
回调合成器(全图一遍) | 中等 |
| 3 | 字符串键 EventEmitter | on('e',fn) / emit('e') |
回调合成器(事件键) | 中等 |
| 4 | 内联回调处理函数 | on('e', function h(){}) / () => {} |
抽取(命名)+ 合成器 link-through-body(匿名) | 命名:便宜 · 匿名:困难 |
| 5 | 闭包集合分派 | Swift validators.write{$0.append(v)} … validators.forEach{$0()} |
回调合成器(closureCollectionEdges,元素调用门控) |
中等 |
决定机制选择的关键区分:
- 存在一个命名引用(
_iterable_class是一个属性名)→ 用 resolver。 - 不存在引用(
cb()是匿名的;需要 registrar↔dispatcher 关联)→ 用 synthesizer。
3. 两个机制的端到端实例
3a. Django ORM 描述符——resolver 模式(Python)
- 缺口:
QuerySet._fetch_all调用self._iterable_class(self)(一个运行时选定的可迭代类,默认ModelIterable),其__iter__里执行 SQL 编译器。静态解析无法把“属性当可调用”解析掉 →_fetch_all唯一的被调用方只剩_prefetch_related_objects,trace(_fetch_all, execute_sql)返回无路径。 - 修复:
djangoResolver通过“名字存在”前置过滤器认领未解析的_iterable_class引用,再把它解析到ModelIterable.__iter__。 - 结果:
trace(_fetch_all, execute_sql)→_fetch_all → __iter__ → execute_sql(3 跳)。
源码可以印证这条链路。FrameworkResolver 接口在 src/resolution/types.ts 中定义了可选钩子 claimsReference?(name),注释直接点明用途:“让动态分派中目标不是声明符号而是属性/描述符的引用名通过 resolver 的名字存在前置过滤器(如 Django 的 self._iterable_class(...))”。真正的预过滤逻辑在 src/resolution/index.ts 的 resolveOne 中:
const preFilterPass =
isNixPathImportRef(ref) ||
this.hasAnyPossibleMatch(existenceName) ||
this.matchesAnyImport(ref) ||
this.frameworks.some((f) => f.claimsReference?.(ref.referenceName));
if (!preFilterPass) {
return null; // 引用被丢弃,永远到不了 resolve()
}
而 src/resolution/frameworks/python.ts 中的 djangoResolver 正是利用这个钩子:resolve() 里特判 ref.referenceName === '_iterable_class' 并交给 resolveModelIterableIter(context)(置信度 0.7,resolvedBy: 'framework');claimsReference 则放行 _iterable_class 与 .urls(include('app.urls') 的模块路径)两种引用,使它们不被预过滤丢弃。这是后来 Rails、Laravel、Drupal 修复反复引用的同一个钩子——手册 §7 明确称之为“gotcha”。
3b. Excalidraw 观察者 + EventEmitter——synthesizer(TS)
- 缺口:
Scene.triggerUpdate执行for (cb of this.callbacks) cb();triggerRender通过scene.onUpdate(this.triggerRender)注册。triggerUpdate → triggerRender这条边是动态的 →trace无路径;整条更新流断裂。 - 修复:一遍全图扫描,检测 registrar/dispatcher 通道、关联注册点、合成
dispatcher → callback边。再加上对命名内联回调的抽取,让 express 的function onmount(){}这类处理函数成为节点。 - 结果:
trace(mutateElement, triggerRender)3 跳连通;expressuse → onmount。
实现就在 src/resolution/callback-synthesizer.ts,文件头注释把两条通道写得和手册 §3b 一致(字段回观察者 + 字符串键 EventEmitter),核心常量:
const REGISTRAR_NAME = /^(on[A-Z]\w*|subscribe|addListener|addEventListener|register|watch|listen|addCallback)$/;
const DISPATCHER_NAME = /(emit|trigger|notify|dispatch|fire|publish|flush)/i;
const MAX_CALLBACKS_PER_CHANNEL = 40;
const EVENT_FANOUT_CAP = 6; // 超过此扇出的事件名(error/change 之类)视为过泛,跳过
const ON_RE = /\.(?:on|once|addListener)\(\s*'"['"]\s*,\s*(?:function\s+(\w+)|(?:this\.)?(\w+))/g;
const EMIT_RE = /\.(?:emit|fire|dispatchEvent)\(\s*'"['"]/g;
文件头还声明了设计哲学:“设计上高精确/低召回(high-precision/low-recall):只命名回调;字段通道按 file+field 配对;EventEmitter 通道按事件扇出封顶;所有合成边打 provenance:'heuristic' 标签”。该遍历由 src/resolution/index.ts 在 resolveAndPersistBatched 末尾调用 synthesizeCallbackEdges()——注释说明这是 best-effort,绝不让索引因其失败而失败。
3c. Alamofire 延迟校验——闭包集合分派(Swift)
- 缺口:
DataRequest.validate(_:)构造闭包并validators.write { $0.append(validator) };基类Request.didCompleteTask通过validators.forEach { $0() }执行它们。append 与 dispatch 位于不同文件与不同类(子类追加、基类迭代),字段类型是 Swift 的Protected<[@Sendable () -> Void]>——所以既不是同文件配对,名字型 registrar 匹配(onX/subscribe/…)也够不着。trace(didCompleteTask, validate)无路径;agent 曾 grepvalidators并读三个文件来重建。 - 修复:
closureCollectionEdges(src/resolution/callback-synthesizer.ts)。dispatcher 迭代一个集合且调用每个元素(coll.forEach { $0() }/{ it() });registrar 向同名字段追加闭包(.append/.add/.push/.insert,含 Swift.write { $0.append })。元素调用($0(/it()是精确性门控——它证明集合里装的是闭包——所以一个没有任何闭包集合分派的仓库,无论有多少.append位置都产出 0 条边。dispatcher→registrar 按字段名全局配对(必须跨文件/类),扇出封顶。 - 结果:
trace(didCompleteTask, validate)通过闭包集合跳 + 内联的validators.write { $0.append }接线点连通。Alamofire 上 9 条精确边(validators/streams/finishHandlers/requestsToRetry),所有非 Swift 对照组 0 条。强制 codegraph-only(屏蔽 Read+Grep+Bash):3/3 运行正确回答 build/send/validate。
源码中可以看到三个关键正则与门控(callback-synthesizer.ts#L65-L77):
const CC_DISPATCH_RE = /(\w+)\.forEach\s*\{\s*(?:\$0|it)\s*\(/g;
const CC_APPEND_WRITE_RE = /(\w+)\.write\s*\{\s*\$0(?:\.(\w+))?\.(?:append|add|push|insert)\s*\(/g;
const CC_APPEND_DIRECT_RE = /(\w+)\.(?:append|add|push|insert)\s*\(/g;
const CC_FANOUT_CAP = 8; // 同名字段两侧超过 8 个位置即视为过泛,整字段跳过
const CC_LANGUAGES = new Set(['swift', 'kotlin']); // 尾随闭包语法仅这两种语言
CC_LANGUAGES 门控在注释里被强调不只是精度问题:.push(/.add( 在 JS/PHP 里无处不在,不加语言门控时该 pass 会切分并正则几乎所有函数——在一个 12k 文件的 PHP/JS 应用上曾造成 20 分钟以上的索引尾部与看门狗误杀(#1235)。closureCollectionEdges 主体(L252-L326)按手册描述实现:先收集 dispatcher(forEach { $0( } 匹配)与 registrar(.write { $0.append } 及直接 append 匹配),再按字段名全局配对,超 CC_FANOUT_CAP 即放弃,产出的边带 provenance: 'heuristic' 与 metadata: { synthesizedBy: 'closure-collection', field, registeredAt },其中 registeredAt 记录了追加发生的 file:line 接线点。测试对应 tests/closure-collection-synthesizer.test.ts 与 tests/explore-factory-closure.test.ts。
手册还说明这些边有两种呈现方式:内联进 trace 路径,以及 codegraph_explore 里 “Dynamic-dispatch links among your symbols” 一节(buildFlowFromNamedSymbols),这样即使 agent 只命名了 validate 而没有点名排空列表的 didCompleteTask,关系仍然可见。相关渲染逻辑在 src/mcp/tools.ts(synthEdgeNote 的 closure-collection 分支)。
3d. 洞见:“采用率地板”可能掩盖 trace 端点 bug(Alamofire)
Alamofire(110 文件)曾被认为是 README 里最弱的仓库、被归为“小仓库地板”(原生 grep 便宜,agent 反正会读)。它不是。 读转录——每次 Read 的 file_path+offset 与它之前紧挨着的助手文本——暴露了 agent 自己的话:“trace 撞上了同名符号(44 个 request、8 个 task),让我按行读。” 原因是 codegraph_trace 的端点消歧(scorePair,仅共享目录前缀)把一个重载名解析到了空壳 delegate/协议桩——request → 1 行空操作的 EventMonitor.request(){},而不是真正的 Session.request,因为两个无关的 Source/Features/ 桩共享了比正确 Source/Core/ 对更深的目录前缀。垃圾 trace → 人工读文件,有时螺旋(一次运行 12 读 / 11 grep)。修复:handleTrace 配对打分中加入 nodeRelevance 项,惩罚空壳(≤1 行函数体)与测试文件符号;在真实方法之间它是平的,路径接近度(cosmos EndBlocker)不受影响。结果(n=8):WITH 臂工具调用 12 → 8 中位数,读的方差塌缩(0–12 → 1–4——那些“崩溃”正是 trace 撞名时的乱抓)。这是一个通用 bug:协议/delegate 桩洪水会打击 Swift/Java/C#/Go。
方法论教训:当 agent 在小仓库上读文件时,不要下“采用率地板”的结论——把它读了什么与工具之前返回了什么做 diff。读了工具已给过内容的 = 采用问题;工具返回错误东西(桩端点、撞名)后的读 = 可修 bug。转录里的推理、而不是中位数,告诉你属于哪一类。强制 codegraph-only 的 hook(屏蔽 Read+Grep+Glob+Bash 搜索)是把“充分性”与“采用率”分开确认的无方差方式。
4. 可重复方法论(每个语言/框架照此执行)
Step 1 —— 选定该框架的“标准流”问题
每个框架都有标志性的数据/控制流。选定“X 如何到达/成为 Y”的问题和一个真实仓库(加入 .claude/skills/agent-eval/corpus.json)。示例:React state→DOM、Vue reactive→render、Svelte store→update;Rails request→controller→view、Spring request→@Controller→service;Express/Koa request→middleware→handler、FastAPI request→route→dependency;Redux action→reducer→store、RxJS subscribe→operator→observer;任何 ORM 的 query builder → SQL 执行(django 模式)。
Step 2 —— 度量缺口(确定性,不用 agent)
rm -rf <repo>/.codegraph && ( cd <repo> && codegraph init -i )
node scripts/agent-eval/probe-trace.mjs <repo> <from-symbol> <to-symbol> # 流在哪里断?
node scripts/agent-eval/probe-node.mjs <repo> <break-symbol> # trail:下一跳是否缺失?
“No direct call path … breaks at dynamic dispatch” + 断点处的稀疏 trail 就定位了缺口(_iterable_class 与 triggerUpdate 正是这样被发现的)。读断点符号的函数体确认它是动态的。probe-trace.mjs 的实现很薄:打开目标仓库的 .codegraph 索引,直接驱动 dist/ 构建产物中的 MCP 工具层执行查询,输出文本结果——这解释了下一条注意事项(probe 脚本使用构建后的 dist/,先跑 npm run build)。
Step 3 —— 分类 → 选机制(用 §2 的表)
self.<attr>(...)/ 描述符 / 元类 → resolver(§3a)。for(cb of store)cb()/store.forEach(cb=>cb())→ 字段回观察者 synthesizer(§3b)。on('e',fn)+emit('e')→ EventEmitter synthesizer(§3b)。- 内联处理函数不是节点 → 命名:抽取(已在 src/extraction/tree-sitter.ts 的
visitFunctionBody通用完成);匿名:synthesizer link-through-body(尚未构建)。 - 无法作为类做精确门控的分派(运行时键控
tablekey、getattr(self, expr)、反射、带类型的 mediator 总线、new Proxy)→ 边界呈现(boundary surfacing)(src/mcp/dynamic-boundaries.ts,#687):explore 在静态路径终止处宣布分派点——file:line、形式,键在静态可见时给出候选目标——而不是合成边。只在查询时触发,零图变更,且只在所问的流未能连通时触发。这是前沿的刻意地板:一条错边会毒化地图(沉默优于错误),但一句诚实的“流在这个位置继续,很可能进入这些候选”仍然能省下读文件重建的螺旋。当某个边界形式日后被证明在真实仓库上可精确门控(例如同仓库字面键命令总线),就把它升级为 synthesizer 通道,边界提示自行消失——因为流已连通。
Step 4 —— 实现
- Resolver:加进
src/resolution/frameworks/<lang>.ts—— 一个resolve()分支 +(若引用名不是声明符号)claimsReference(name)。照抄djangoResolver(见 src/resolution/frameworks/python.ts 作为模板)。 - Synthesizer 通道:扩展 src/resolution/callback-synthesizer.ts —— 加该框架的 registrar/dispatcher 名字模式与函数体模式(例如 signals 用
.connect()/.emit();Rx 用.subscribe()/.next())。 - 重建索引(Step 2 命令)并重跑
probe-trace—— 流此时应连通。
Step 5 —— 验证(每次同样方式)
-
确定性:
probe-trace(from,to)找到路径;probe-node显示桥接后的跳。原先断裂的跳已闭合。 -
精确性:统计 + 抽查合成/解析边——无爆炸、目标正确:
sqlite3 <repo>/.codegraph/codegraph.db \ "select s.name||' → '||t.name||' '||coalesce(e.metadata,'') from edges e \ join nodes s on e.source=s.id join nodes t on e.target=t.id where e.provenance='heuristic';"(resolver 的边不是
heuristic;用 trace + callees 验证。) -
回归:节点数稳定(前后各
select count(*) from nodes;—— 大跳说明抽取变更过度触发);对照组仓库的既有 trace 完好。 -
端到端 agent 评测:跑该流问题,测读次数 / 答案完整性 / 成本 vs 修复前基线:
# 无头(精确成本 + 干净的工具序列) bash scripts/agent-eval/run-agent.sh <repo> with "<flow question>" # 或完整 A/B + 交互式 Explore 子代理路径: scripts/agent-eval/audit.sh local <name> <url> "<flow question>" all然后解析:
Read次数、codegraph 工具次数、成本,以及答案中现在是否包含“胶水符号”(此前需要读文件的那些)。
成功标准(每个语言/框架):trace 端到端找到标准流(无动态分派断裂);agent 能在至少某些运行中以 Read 0 回答且胶水符号出现在答案里;无节点爆炸、无对照组回归;抽查下合成边精确(无泛名过度关联)。
5. 验证工具链(参考)
| 工具 | 用途 |
|---|---|
probe-trace.mjs <repo> <from> <to> |
两符号间的调用路径(缺口探测器) |
probe-node.mjs <repo> <sym> [code] |
符号 + trail(callers/callees);code 加函数体 |
probe-context.mjs <repo> "<task>" |
含调用路径的 context 输出 |
probe-explore.mjs <repo> "<query>" |
explore 输出 |
| run-agent.sh / audit.sh / itrun.sh | agent A/B(无头 + 交互式);即 /agent-eval 技能 |
sqlite3 <repo>/.codegraph/codegraph.db |
直接检查边/节点(provenance、metadata、计数) |
注意事项:probe 脚本使用构建后的 dist/ —— 先 npm run build。任何抽取或解析变更之后都要重建索引(rm -rf <repo>/.codegraph && codegraph init -i),因为 synthesizer/resolver 在索引时运行。测试 fixture:为每个模式保留一个极小 fixture(手册提到当时的 /tmp/cb-fixture/bus.js,随正式实现迁入 __tests__/)。
6. 覆盖矩阵(随进展填写)
状态图例:✅ 完成+已验证 · 🔬 已识别缺口 · ⬜ 未开始。机制:R = resolver,S = synthesizer 通道,X = 抽取。手册要求在开工前对照 src/extraction/languages/ 与 src/resolution/frameworks/ 核对实际支持集——下表是起点。以下是各语言/框架行的核心状态(保留关键数字与前沿项):
| 语言 | 框架 | 标准流 | 机制 | 状态要点 |
|---|---|---|---|---|
| TS/JS | React / 观察者 / EventEmitter / React Router | state→render;dispatch→callback;route→component | S + X | ✅ 渲染+分派(excalidraw);React Router <Route path component={C}/>(v5)+element={<C/>}(v6) → react-realworld 0→10, 10/10;对象 data-router 字面形式;Next.js 假阳性已修。🔬 lazy data-router(变量路径 + lazy 模块) |
| TS/JS | Vue / Nuxt | 模板事件(@click→handler);组件组合;reactive→render | S + X | ✅ 事件+组合(vitepress S / vben M / element-plus L)。🔬 reactive→render(vue-core Proxy 运行时,前沿,缓办) |
| TS/JS | Svelte / SvelteKit | 模板调用/组合;action→api;store→DOM | X | ✅ 开箱即用(模板 {fn()}、<Pascal/> 组合、import * as api、load→api)+ 导出常量函数对象抽取(SvelteKit actions)。🔬 $lib 命名空间 + store/reactive 前沿 |
| TS/JS | Express / Koa | request → route → handler → service | R + X | ✅ 命名 handler + middleware + controller/service + 内联箭头 handler → service 体调用(realworld S 19 / ghost L 65 边)。🔬 自定义路由(payload 0 路由——非 app.get 风格) |
| TS/JS | NestJS | request → @Controller → DI service → repo | R | ✅ 已良好覆盖(装饰器路由 + DI 大规模下正确)。无动态分派缺口。🔬 已提交的 dist/ 构建产物被索引(通用 build 目录忽略的后续项) |
| TS/JS | RxJS / signals | subscribe → operator → observer | S | ⬜ |
| Python | Django ORM | QuerySet → SQL compiler | R | ✅(§3a) |
| Python | Django / DRF (views) | url → view → model | R + X | ✅ url→view + DRF router.register→ViewSet(realworld S / wagtail M / saleor L)。🔬 signals、viewset 继承 CRUD 动作、saleor GraphQL resolvers |
| Python | Flask / FastAPI | request → route → handler → dependency | R + X | ✅ Flask 跨中间装饰器 + 堆叠 @x.route(microblog 6→27);FastAPI 空路径路由(Netflix dispatch 290/290 100%)+ 裸名内置守卫(名为 index/get/count… 的 handler 曾被内置过滤器吃掉);Flask-RESTful add_resource(redash 6→77)。🔬 FastAPI Depends() 依赖边 |
| Go | Gin / chi / gorilla/mux / net-http | request → route → handler;中间件链(Use→Next) |
S + X | ✅ 任意组变量路由(v1.GET 等,gin-vue-admin 4→259);gorilla/mux 已覆盖确认;gin 中间件链 synthesizer(ginMiddlewareChainEdges:c.handlersc.index 切片索引分派无法静态解析 → 链接到 .Use/.GET 注册的 HandlerFunc,无 gin 时不触发)。agent A/B:gin 从 codegraph −58% 成本(兔子洞)翻转为 4/4 WITH 运行全干净(0 Read/Grep/Bash)。🔬 内联 func(c){} 匿名 handler;子路由路径前缀不拼接 |
| Go | GoFrame(标准路由) | request-type g.Meta 路由 → 控制器方法(反射 group.Bind) |
R(抽取)+S | ✅ g.Meta 路由覆盖(#747,frameworks/goframe.ts):每个 g.Meta `path:.. method:..` 请求类型标签 → route 节点,goframeRouteEdges 按签名而非名字关联到取该请求类型的控制器方法,pkg.Type 键 + addon-root 决胜。gf-demo-user 7/7、gfast 65/68、hotgo(697 文件)242/247 (98%)、100% 精确。A/B:WITH 1 explore/0 Read,WITHOUT 7.5 读均值 —— −83% 工具调用、2.1× 更快。🔬 反射 Bind 的组前缀不拼接 |
| Rust | Axum / actix / Rocket | request → route → handler | R + X | ✅ Axum 链式方法 + 命名空间 handler(realworld-axum 12→19, 19/19,平衡括号扫描 + 每方法节点 + 末段 :: 段);Rocket 属性宏 550/556 (99%);actix builder API web::resource(...).route(web::get().to(h))(actix-examples 51→128 路由,35→112 解析)。🔬 actix web::scope 前缀 + 匿名 .to 闭包 |
| Java | Spring | request → @RestController → @Autowired service → repo | R + X | ✅ 裸 @GetMapping + 类级 @RequestMapping 前缀拼接(realworld S / mall M / halo L);接口→实现分派 synthesizer(interfaceOverrideEdges,JVM 门控、重载感知;mall 310 / halo 734 合成边,节点数不变)——trace(PmsProductController.getList, PmsProductServiceImpl.list) 3 跳;字段注入具体 bean trace(#389,userbo.toLogin2() → UserBO.toLogin2);@Value("${k}")/@ConfigurationProperties → application.{yml,properties} 绑定(Spring 宽松绑定,mall-tiny 11/11)。⚠️ agent A/B 为 null(n=2,agent 走 context→explore→Read 从未调 trace——采用率门槛的反复出现,见 call-sequence-analysis.md)。🔬 Spring Data JPA 派生查询等 |
| Java | MyBatis(XML mapper) | DAO 接口方法 → <select|insert|update|delete id="X"> SQL |
R(XML 抽取)+S(Java↔XML) | ✅ XML mapper 一等语言(#389,mybatis-extractor.ts):每条语句 → <namespace>::<id> 方法形节点 + <sql> 片段 + <include refid> 引用;非 mapper XML 只出文件节点;mybatisJavaXmlEdges 按后缀匹配关联,歧义丢弃(精确优先)。mall-tiny 6/6 自定义 SQL 桥接,全链 trace(controller → mapper-xml) 4 跳连通。🔬 跨 mapper include、MyBatis Plus 动态方法、注解 mapper |
| Kotlin | Spring Boot / Jetpack Compose | request → @RestController → service;@Composable → 子 | R + X | ✅ Spring resolver 从 ['java'] 扩展到 .kt + Kotlin fun name( 匹配(petclinic-kotlin 0→18, 18/18;DI controller→repo 解析)。Compose 组合本就是静态调用(免费)。🔬 Ktor 内联 lambda、Compose 重组、coroutines/Flow |
| Swift | Vapor | request → route → controller | R + X | ✅ 曾在所有真实应用上 0 路由——重写后:任意 receiver、可选/非字符串路径段、.grouped/.group{} 前缀跟踪、use: 判据。vapor-template 0→3、SteamPress 0→27 (27/27)、SPI 0→14 (14/14)。🔬 类型化路由枚举 + 闭包 handler |
| Swift | Alamofire / 闭包集合 | request → build → send → validate(延迟闭包) | S | ✅ closureCollectionEdges(§3c):Alamofire 9 条精确边、非闭包集合对照 0,强制 codegraph-only 3/3 正确。+ trace 端点相关性(nodeRelevance,§3d:WITH 臂工具调用 12→8、读方差 0–12→1–4)。+ god 文件多相位渲染(handleExplore 六层协调,Alamofire 一次 explore 渲染 build+validators-exec+validate ~16K;A/B 读中位数 2→0.5,工具 8→5.5;excalidraw 对照保持 0 读)。串行流主干不可归约——修法是渲染它而不是封顶它 |
| C# | ASP.NET Core | request → [Http*] action → DI service → EF | X | ✅ 特性文件夹检测(realworld 0→19)+ 裸 [HttpGet] + 类级 [Route] 前缀(eShopOnWeb 9→33 / jellyfin L)。同文件共存,无需 claimsReference。🔬 EF Core LINQ/DbSet(元编程前沿) |
| Ruby | Rails / Sinatra | request → routes.rb → Controller#action → model | R | ✅ RESTful resources/resource 展开 → 精确 controller#action(realworld S 16 / spree M / forem L 635),含 only/except + 复数化 + claimsReference。🔬 Rails Engine 路由(spree 仍 0)、ActiveRecord 动态 finder |
| PHP | Laravel | request → route → controller → Eloquent | R | ✅ 精确 Route::get([Ctrl::class,'m']) / 'Ctrl@m' → Ctrl@method(曾把每个 index 错解析到 ArticleController);realworld 全路由正确,bookstack 267/332。🔬 Eloquent 动态 finder/关系 |
| PHP | Drupal | request → *.routing.yml → _controller/_form | R | ✅ FQCN handler 的 claimsReference(裸 FQCN 与单冒号 Class:method 曾被预过滤丢弃)+ composer type:drupal-* 检测。admin_toolbar 0→14 (14/14)、webform 144、core 536→731/836 (87%)。前沿:实体注解 handler、Drupal 11 的 OOP #[Hook] 属性(418 属性文件 vs 3 过程式) |
| C/C++ | vtable / 继承 | virtual 调用 → 重写;一般直接分派 | S + X | ✅ 直接分派强(redis C 29,464 跨文件调用边 / leveldb C++ 1,462)+ C++ 继承抽取修复(base_class_clause,leveldb 219→298)+ cpp-override synthesizer(leveldb 12 条精确:Iterator::Next→MergingIterator)。🔬 C 回调结构体(422 路扇出,噪声过大故意不合成)、纯虚基类方法(无函数体声明不进图) |
| Dart | Flutter | setState → build;build → 子 widget | S + X | ✅ setState→build synthesizer(setState( 门控,.dart 限定)+ Dart 方法范围根本修复(函数体是签名的兄弟节点,原方法节点只有签名 → endLine 现跨函数体;惠及所有函数体分析,对照组 excalidraw 9,290 / django 302 不变)。widget 组合本静态。🔬 MVVM Command/ChangeNotifier、Navigator.push 路由 |
| Lua / Luau | Neovim / Roblox | 模块分派(require→mod, mod.fn);事件/回调 | — | ✅ 先测量,零代码变更:Neovim 是模块分派重地,通用 import + 名字解析已覆盖(telescope.nvim 220 imports + 335 跨文件 mod.fn 调用,流端到端可 trace)。🔬 事件回调注册 ~12 内联匿名 vs ~2 命名——命名合成器只覆盖一小撮,按“部分覆盖不如没有”不建 |
| Erlang | OTP behaviours | request → behaviour 分派(Var:callback(...) folds)→ 实现者回调 |
S | ✅ behaviour 回调分派 synthesizer(erlangBehaviourDispatchEdges):Var:fn(args) 站点 → 声明 (fn, 站点元数) 的唯一一个仓库内 behaviour 的每个实现者;跨 behaviour 名字+元数冲突则静默(cowboy init/2 被 5 个 behaviour 声明 → 正确地不出边);超扇出上限(24)整站点跳过(ejabberd gen_mod ~230 实现者保持可见的动态边界)。cowboy 38 边(含 cowboy_stream_h::execute 中间件链)、ejabberd 598、emqx 843;精确抽查 36/36;emqx 2,273 文件索引成本 +~1.4s。cowboy 请求流一次 explore 端到端连通。🔬 注册名跨模块目标;多子协议共享契约的终端 Handler:init 跳 |
| Scala | Play / Akka | request → conf/routes → controller action | R + X | ✅ 无扩展名 conf/routes 加入窄文件遍历 opt-in(isPlayRoutesFile)+ Play resolver 解析 METHOD /path Controller.action(args)。computer-database 0→8 (7/8)、starter 0→4 (3/4);文件遍历变更只增加 Play routes 文件,无回归。🔬 SIRD 编程式路由、Akka actor message→handler |
| Swift × ObjC | 混合 iOS 应用 | Swift obj.foo(bar:) → ObjC -fooWithBar:;反向 |
R | ✅ frameworks/swift-objc.ts 实现 @objc 自动桥接命名数学(含 init 形式、属性 getter+setter 对、@objc(custom:));反向剥 Cocoa 前置词(With/For/By/…)。Charts S 28/1、realm-swift M 36/1185、wikipedia-ios L 52/983;泛名黑名单(init/description/count…)保精确;置信度 0.6(名字匹配的 1.0 赢平局)——桥接只在名字匹配无果时触发。🔬 Swift 泛型 over ObjC 协议 |
| JS × native | RN legacy bridge | JS NativeModules.X.fn(...) → ObjC RCT_EXPORT_METHOD / JVM @ReactMethod |
R | ✅ frameworks/react-native.ts 解析 RCT_EXPORT_MODULE/RCT_EXPORT_METHOD/RCT_REMAP_METHOD + @ReactMethod。AsyncStorage S 8/8 精确;react-native-firebase L 内置 RCTEventEmitter 黑名单后 18 条精确(最初 78 含 60 条 addListener: 假阳性)。🔬 动态桥接键(仅字面键) |
| JS × native | RN TurboModules | JS spec 接口 ↔ native 实现 | R | ✅ 部分:解析 TurboModuleRegistry.get*<Spec>('Name') + Spec 接口方法,按选择器首关键词/标识符匹配 native 实现。react-native-svg S 9 条精确。🔬 不用 legacy 宏的 native 实现类(需继承感知桥接) |
| ObjC/Java/Kotlin → JS | RN 事件发射器 | native sendEventWithName:/emit(...) → JS addListener('e', handler) |
S(跨语言通道) | ✅ rn-event-channel synthesizer:按字面事件名匹配,与语言内通道同扇出上限(EVENT_FANOUT_CAP=6,见 callback-synthesizer.ts#L36);订阅包装回退(RN 库 API 的 watchX(listener){ addListener(...) } 归到封闭常量/变量)。RNFirebase L 3 条推送流边、RNGeolocation S 2 条。🔬 内联箭头 handler |
| JS × Swift/Kotlin | Expo Modules | JS requireNativeModule('X').fn(...) → Swift/Kotlin Function("fn") { ... } |
R | ✅ expo-modules 框架抽取器解析 Module { Name("X"); Function("y"){...}; AsyncFunction("z"){...} },为每个声明合成 method 节点,JS 调用点经既有名字匹配器解析。expo-haptics S 6 节点、expo-camera M 41、SDK 扫描 L 134(7 包,72 Swift + 62 Kotlin)。🔬 闭包体抽取 |
| JS × native | RN Fabric / Codegen + legacy Paper 视图 | JSX <MyView prop={v}/> → Codegen spec → native 类 |
R(抽取)+S+JSX | ✅ fabric-view 抽取器(Codegen TS spec 与 Paper 宏双支持)+ fabric-native-impl synthesizer(按 RN 约定名+后缀链接 native 实现类)。RNSegmentedControl S 1 组件+11 props+4 桥、RNScreens M 27 组件+272 props+68 桥(Phase 6 前为 0)、RNSkia L 混合 monorepo 5+14+15。monorepo 检测(root manifest 是 workspace 声明时探测 packages/<sub>/package.json)。🔬 Fabric 事件处理 prop(onTap={cb}) |
不属于覆盖工作的检索 A/B
覆盖决定一条流是否在图中;另一类变更决定 explore 返回的答案是否充分——字节预算如何在找到的文件间分配。通过标准相同(Read → 0,无墙钟回归),--model sonnet --effort high 规则相同,但 harness 用 ab-new-vs-baseline.sh(新构建 vs 基线构建,两边都开 codegraph),因为问题不是“codegraph 有没有用”而是“对 codegraph 的改动有没有用”:
| 变更 | 仓库 | 结果 |
|---|---|---|
| 按分数比例分配字节(#1500, CG-1)2026-08-04,3 运行/臂 | client-go(Go,2,454 文件,2,001 生成文件)、excalidraw、express(对照) | 门禁 FAILED:client-go/excalidraw 两臂 Read 均 0,生成文件占信封 10.5%→0%;但 express 1/3 运行回归(4 Reads, 52s)——比例预订低于文件自身大小的文件不再整段渲染、预订未被花掉(lib/utils.js 6,380 B → 583 B 桩,信封 13.8K → 9.2K)。全记录:explore-allocation-ab-1500.md |
| ↳ CG-21 修复后重跑(6 运行/臂) | 同上 | 门禁 PASSES 全四项:15 个新臂运行 Read 全 0,express 回归 6 次未复现(基线反而 4/6 读);确定性核心:lib/utils.js 583 B 桩 → 6,268 B 整段,信封 9.2K → 14.5K(预算不变) |
| ↳ 门禁(CG-22,SHA 钉定,独立于写修复的任务重测) | 同三仓库 | 门禁 PASSES:12 个新臂运行 Read 全 0(基线 express 3/3 读);生成文件占 0%;唯一诚实反例是 excalidraw 新臂答案份额低于基线(66.6–81.9 vs 75.5–92.7,仍远高于 50% 线);+1.5s 中位差异经 explore 自身延迟与确定性响应字节对比排除构建因素 |
该轮沉淀出两条 harness 教训,现已内置于 ab-new-vs-baseline.sh:
- 这类 A/B 的 prompt 里要点名 codegraph。 agent 是否选用工具是“采用率”轴,检索变更碰不到它;一次从未调 explore 的运行(0 codegraph 调用,3 Reads)测不出任何东西。它不是强制 Read-0——agent 仍自由回退,那才是标准。
- 两臂都设
CODEGRAPH_NO_PROMPT_HOOK=1。 机器环境的前置 hook 解析到dist/里此刻的内容,而脚本自己会在两臂之间改写它。
7. 已知限制与坑(来自 excalidraw/django 及各框架验证)
手册 §7 是全文最有信息量的部分,逐框架记录了修复细节、A/B 数字与剩余前沿。核心要点:
- 覆盖使 no-read 成为可能,但不强制。 agent 有时仍读文件确认来源;成本大致持平(codegraph 调用与读互换)。可靠收益是完整性 + 让 Read-0 可能。不要期待成本保证下降。
- 难度梯度是真的:命名引用分派(resolver)便宜;匿名回调节分派(synthesizer)中等;匿名箭头 handler 是剩下的硬缺口(无身份 → 需要 synthesizer link-through-body,尚未构建)。
- 抽取变更是高爆炸半径。 共享
tree-sitter.tswalker 里的命名内联回调抽取——任何抽取变更后要在多个语言上复查节点数(excalidraw 上保持 +3,因为匿名箭头被跳过)。 - synthesizer 精度守卫:registrar 名唯一性、仅命名 handler、事件扇出上限(
error/change这类泛事件跳过,见EVENT_FANOUT_CAP = 6)。基于接收者类型(经type_of边)的匹配是计划中的精度升级,缓办。 - “如建”捷径(回调合成器):按 file+field 配对 registrar/dispatcher(类的代理)、正则参数恢复(仅命名引用)、
provenance:'heuristic'+metadata.synthesizedBy(枚举里没有'callback-synthesis')。详见 callback-edge-synthesis.md。 - synthesizer 只在
resolveAndPersistBatched(全量索引)中运行 —— 正式交付前需接入resolveAndPersist以支持增量同步(对应 src/resolution/index.ts#L1933-L1948 的调用位置)。 - trace 中的符号歧义:常见名(
render、execute_sql)匹配很多节点;trace 可能从错误的那个开始。从具体方法、而非类名发起 trace。
各框架的具体坑(每条都附带验证数字与剩余前沿,摘录代表性内容):
- Vue(2026-05-23 验证,vitepress S / vben M / element-plus L):SFC
<template>不被解析器处理,模板用法需合成(vueTemplateEdges:@click="fn"→ handler,kebab<el-button>→ElButton;PascalCase<Child/>已由 JSX 通道覆盖)。Composable 解构 handler 已解决(const { close: closeSidebar } = useSidebarControl()沿 alias → composable → 返回的closefn 走通,vitepress sidebar 流 6 → 0 读)——精确优先,返回 fn 定位不到时不 fallback。剩余:前缀约定 kebab(element-plusel-button→button.vue)与 reactive→render(vue-core Proxy 运行时)缓办。 - Svelte / SvelteKit:与 Vue 不同,
.svelte抽取器已解析模板,开箱即赢(realworld login:with 1 读 vs without 4 读)。唯一抽取缺口是函数对象常量(SvelteKitactions),已对导出常量修复。教训:先测量再假设缺口——现代 Svelte 几乎不用on:click={fn},假设的事件 handler 缺口不是真缺口。 - Express / Koa:真实缺口是内联箭头路由 handler(主流现代模式)——handler 正则
[^)]+在箭头的)处断裂,route 连到空、匿名 handler 函数体(request→service 流)丢失(realworldPOST /users/login→ 0 边)。修复(frameworks/express.ts):字符串感知的平衡扫描 + 内联箭头体调用抽取(RESERVED 过滤),realworld 19 / ghost 65 条精确 route→service 边。教训与 Svelte 相反:Express 的主流模式恰是未覆盖的那个,需要真修。 - Rails:
resources/resource展开是主导模式,原 resolver 只见显式路由,realworld+spree 路由节点为 0(realworld 0→16、forem 0→635)。claimsReference预过滤又是坑:articles#index不命名任何声明符号,resolveOne在resolve()之前就把引用丢了。A/B(forem,大):codegraph 1–4 读/0 grep vs 4–5 读/2–3 grep,更少读、无 grep、更快。 - Spring/MyBatis 企业链(mall-tiny S,#389):三个缺口让
HTTP route → Controller → BO/Service → ServiceImpl → DAO/Mapper → XML SQL多处断链。(1) 字段注入具体 bean:this.userbo.toLogin2()的field_access(this, X)解包 +matchMethodCall学会按封闭类字段声明查类型(inferJavaFieldReceiverType,src/resolution/name-matcher.ts)——修复在语言层而非 Spring 层;(2) MyBatis XML 一等语言 + Java↔XML 桥接(见覆盖矩阵);(3) Spring 配置键链接:@Value/@ConfigurationProperties经宽松绑定(kebab↔camel↔snake)解析到 yml/properties 键(mall-tiny 11/11)。 - Spring(bare mapping 修复):mapping 正则曾要求字符串路径,裸方法映射(类级
@RequestMapping带路径——多方法控制器的主导模式)全漏。把类级@RequestMapping当前缀拼接;一次切手曾把 mall 从 292 回归到 1——被跨仓库路由计数检查抓住,手册的回归守卫发挥了作用。 - Django / DRF:
router.register(r'articles', ArticleViewSet)曾被漏(只抽path()/url());字符串首参把它与admin.register(Model, Admin)区分开。wagtail A/B:读更少、grep 更少、更快,纯增量无回归。 - Laravel:resolver 曾丢弃 controller,发出裸
index引用并被名字匹配错解析(每个index→ ArticleController);修复为精确Controller@method+claimsReference放行。firefly 只解析 3/568(其流式->uses()格式未解析)。 - Gin / chi:路由正则只匹配
(router|r|mux|app|e).METHOD(...),真实应用路由在组变量上(gin-vue-admin 625 文件只有 4 路由);放宽为任意标识符(动词 + 字符串路径 + handler 参数三重门控保路由特异性)。A/B(create-user 流):codegraph 0 读/0 grep/26–30s vs without 3/3/52–53s——最干净的后端收益。 - ASP.NET Core:两个洞——
detect()只认/Controllers/目录或根Program.cs(feature-folder 应用从不被检测,realworld 0 路由);属性正则要求字符串路径,裸[HttpGet]漏掉(eShopOnWeb 24 裸/2 字符串)。无需claimsReference——ASP.NET 属性路由与 action 同文件共存,裸方法引用同文件即可解析。 - Flask / FastAPI:三个修复——Flask 要求
@x.route后紧跟def(中间装饰器/堆叠路由全漏,microblog 6/27)改用findHandler扫描(6→27);FastAPI 空路径@router.get("")被[^'"]+拒绝(*修复 + 空路径名守卫,Netflix dispatch 290/290);裸名内置守卫(名为 Python 内置方法名的 handler 被isBuiltInOrExternal过滤,镜像knownNames守卫到裸分支,django 对照 302/373 不变)。教训:内置名过滤是横跨 Python 的静默精确税。 - Drupal:
claimsReference预过滤坑再现(FQCN/单冒号 handler 形态)+ 独立 contrib 模块检测(composerrequire为空,靠name/type+*.info.yml回退,admin_toolbar 0→14)。前沿:实体注解 handler、#[Hook]属性(现代 core 过程式 hook 检测基本失效)。 - Rust / Actix:Axum 平面正则只捕获链中第一个
method(handler)与裸\w+handler;重写为平衡括号扫描 + 每方法节点 + 末段::段(12→19, 19/19)。actix builder API(handler 在.to(h)里,不在get(h)里)曾是主导风格却全漏(51→128 路由, 87.5% 解析)。 - Vapor:抽取器曾只认
(app|router|routes).METHOD("path", use: handler),而现代 Vapor 在RouteCollection.boot(routes:)里用分组 builder(任意变量 receiver、无路径参数)——所有被测真实应用 0 路由。重写后(任意 receiver、可选非字符串路径段、group 前缀映射、use:判据)template 0→3、SteamPress 0→27、SPI 0→14。 - React Router:
react.ts曾返回references: [],<Route>声明零产出;新增<Route>JSX 抽取(<Route\b后开窗扫描,避免element={<Comp/>}的嵌套>截断;v5component={C}与 v6element={<C/>}任意属性顺序),react-realworld 0→10 (10/10);<Routes>容器经\b边界排除。 - Dart / Flutter:setState→build synthesizer 被一个根本性抽取缺口阻塞——Dart 把方法函数体建模为签名的兄弟,所有方法节点
endLine == startLine,一切函数体分析(callees、context 切片、synthesizer 体扫描)都被截断。共享createNode里修复(函数体超出节点时延伸endLine,受保护不伤其他语法)。该修复是根本性的而非 Flutter 专属。 - Kotlin / Compose:Spring resolver
['java']-only + Java 语法正则 → Spring Boot Kotlin 0 路由;扩到['java','kotlin']+.kt+fun name(备选后 petclinic-kotlin 0→18 (18/18)。Compose 组合零成本(普通函数调用)。 - Lua / Luau:矩阵猜“事件/回调节分派”,测量说不是——telescope.nvim 220 imports + 335 跨文件
mod.fn调用已全解析。真前沿(事件注册)以匿名内联闭包为主(~12 内联 vs ~2 命名),按“部分覆盖不如没有”不建合成器,记录为已验证。 - Scala / Play:无扩展名
conf/routes不被文件遍历索引(isSourceFile要求扩展名);窄 opt-in(isPlayRoutesFile)只增加 Play routes 文件,excalidraw 9,290 与全量套件 800 不变。 - C / C++:直接分派开箱即用(redis 29,464 / leveldb 1,462 跨文件调用边)。两个前沿形状:C 回调结构体(redis
proc字段 422 路扇出,噪声过大故意跳过);C++ vtable 重写(被上游base_class_clause未处理阻塞,leveldb 219→298;cpp-override通道后 12 条精确边,C/TS 对照 0)。 - 前沿清扫(2026-05-23):可做的部分(React Router 对象 data-router、Next.js 假阳性、Flask-RESTful
add_resource、Flask tuplemethods、gorilla/mux 确认覆盖)已关闭;明确留下(附理由而非敷衍):C 回调结构体分派(422 路噪声)、元编程 finder(ActiveRecord/Eloquent/Spring-Data-JPA/EF)、响应式运行时(Vue Proxy / Compose 重组)、Akka actor 消息分派、纯匿名内联闭包(def-use 前沿)、React lazy data-router、C++ 纯虚基类方法。强做会加噪声,违反“部分覆盖不如没有”。
8. 完成定义(整个使命)
对每个语言 × 框架:标准流可 trace 端到端;agent 能在至少某些运行中以 Read 0 回答流问题且胶水符号在场;无节点爆炸、无回归——并在 §6 矩阵中记录验证仓库 + 数字。之后是交付准备:每机制的测试、CHANGELOG、接入增量索引、提交。
这套手册的精髓可以压缩成三条工程纪律:(1) 先测量再修——用确定性的 probe 脚本定位断点、读函数体确认动态性,拒绝凭直觉假设缺口;(2) 精确优先于召回——resolver 有 claimsReference 预过滤配合,synthesizer 有名字/语言/扇出三重门控,无法精确门控的形态宁可诚实地“宣布边界”也不投毒地图;(3) 同一套验证方式——确定性 trace、sqlite 精确抽查、节点数回归、agent A/B 四步一个都不省,任何一步(如 mall 292→1 的路由计数回归)都可能救回一次静默劣化。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00