首页
/ Codegraph 动态分派覆盖手册:如何系统性地封死静态抽取里的“断链调用”

Codegraph 动态分派覆盖手册:如何系统性地封死静态抽取里的“断链调用”

2026-09-06 12:09:47作者:咎岭娴Homer

本文以 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_tracecodegraph_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_objectstrace(_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.tsresolveOne 中:

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.urlsinclude('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 跳连通;express use → 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.tsresolveAndPersistBatched 末尾调用 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 曾 grep validators 并读三个文件来重建。
  • 修复closureCollectionEdgessrc/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.tstests/explore-factory-closure.test.ts

手册还说明这些边有两种呈现方式:内联进 trace 路径,以及 codegraph_explore 里 “Dynamic-dispatch links among your symbols” 一节(buildFlowFromNamedSymbols),这样即使 agent 只命名了 validate 而没有点名排空列表的 didCompleteTask,关系仍然可见。相关渲染逻辑在 src/mcp/tools.tssynthEdgeNote 的 closure-collection 分支)。

3d. 洞见:“采用率地板”可能掩盖 trace 端点 bug(Alamofire)

Alamofire(110 文件)曾被认为是 README 里最弱的仓库、被归为“小仓库地板”(原生 grep 便宜,agent 反正会读)。它不是。转录——每次 Readfile_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_classtriggerUpdate 正是这样被发现的)。读断点符号的函数体确认它是动态的。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.tsvisitFunctionBody 通用完成);匿名:synthesizer link-through-body(尚未构建)。
  • 无法作为类做精确门控的分派(运行时键控 tablekeygetattr(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 —— 验证(每次同样方式)

  1. 确定性probe-trace(from,to) 找到路径;probe-node 显示桥接后的跳。原先断裂的跳已闭合。

  2. 精确性:统计 + 抽查合成/解析边——无爆炸、目标正确:

    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 验证。)

  3. 回归:节点数稳定(前后各 select count(*) from nodes; —— 大跳说明抽取变更过度触发);对照组仓库的既有 trace 完好。

  4. 端到端 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 apiload→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;中间件链(UseNext S + X ✅ 任意组变量路由(v1.GET 等,gin-vue-admin 4→259);gorilla/mux 已覆盖确认;gin 中间件链 synthesizerginMiddlewareChainEdgesc.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);接口→实现分派 synthesizerinterfaceOverrideEdges,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 synthesizersetState( 门控,.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 回调分派 synthesizererlangBehaviourDispatchEdges):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.ts walker 里的命名内联回调抽取——任何抽取变更后要在多个语言上复查节点数(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 中的符号歧义:常见名(renderexecute_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 → 返回的 close fn 走通,vitepress sidebar 流 6 → 0 读)——精确优先,返回 fn 定位不到时不 fallback。剩余:前缀约定 kebab(element-plus el-buttonbutton.vue)与 reactive→render(vue-core Proxy 运行时)缓办。
  • Svelte / SvelteKit:与 Vue 不同,.svelte 抽取器已解析模板,开箱即赢(realworld login:with 1 读 vs without 4 读)。唯一抽取缺口是函数对象常量(SvelteKit actions),已对导出常量修复。教训:先测量再假设缺口——现代 Svelte 几乎不用 on:click={fn},假设的事件 handler 缺口不是真缺口。
  • Express / Koa:真实缺口是内联箭头路由 handler(主流现代模式)——handler 正则 [^)]+ 在箭头的 ) 处断裂,route 连到空、匿名 handler 函数体(request→service 流)丢失(realworld POST /users/login → 0 边)。修复(frameworks/express.ts):字符串感知的平衡扫描 + 内联箭头体调用抽取(RESERVED 过滤),realworld 19 / ghost 65 条精确 route→service 边。教训与 Svelte 相反:Express 的主流模式恰是未覆盖的那个,需要真修。
  • Railsresources/resource 展开是主导模式,原 resolver 只见显式路由,realworld+spree 路由节点为 0(realworld 0→16、forem 0→635)。claimsReference 预过滤又是坑articles#index 不命名任何声明符号,resolveOneresolve() 之前就把引用丢了。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 学会按封闭类字段声明查类型(inferJavaFieldReceiverTypesrc/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 / DRFrouter.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 的静默精确税。
  • DrupalclaimsReference 预过滤坑再现(FQCN/单冒号 handler 形态)+ 独立 contrib 模块检测(composer require 为空,靠 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 Routerreact.ts 曾返回 references: []<Route> 声明零产出;新增 <Route> JSX 抽取(<Route\b 后开窗扫描,避免 element={<Comp/>} 的嵌套 > 截断;v5 component={C} 与 v6 element={<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 tuple methods、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 的路由计数回归)都可能救回一次静默劣化。

登录后查看全文
热门项目推荐
相关项目推荐