codegraph 的确定性测量范例:为什么“丢弃 >50% 工厂闭包范围“的改动(CG-27)被测量否决了
2026/9/7 2:38:36 网站建设 项目流程

codegraph 的确定性测量范例:为什么"丢弃 >50% 工厂闭包范围"的改动(CG-27)被测量否决了

【免费下载链接】codegraphPre-indexed code knowledge graph, auto syncs on code changes, for Claude Code, Codex, Gemini, Cursor, OpenCode, AntiGravity, Kiro, CoPilot, and Hermes Agent — fewer tokens, fewer tool calls, 100% local项目地址: https://gitcode.com/GitHub_Trending/co0degr/codegraph

本文基于 codegraph 仓库中的基准报告 docs/benchmarks/explore-factory-closure-cg27.md,完整还原 CG-27 任务从"提出改动建议"到"被确定性测量否决、关闭为 obsolete"的全过程:一个跨文件(spanning)的工厂闭包范围是否会妨碍 explore 响应对文件内部闭包的细粒度选择。读完本文,你将掌握 codegraph 中ENVELOPE_KINDS丢弃规则、shrinkCluster按成员收缩、集群密度平局决胜与 CG-30 行窗口化四个机制的真实协作关系,并学会用"hermetic fixture + 逐行送达比对"的测量方法,回答"这个改动该不该做"这类问题。

1. 工厂闭包:一种常见且危险的文件形态

所谓工厂闭包(factory closure),是createFoo()这类顶层工厂函数返回一组闭包对象、内部状态全部封闭在函数体内的写法。由于工厂函数往往覆盖文件几乎全部内容,它在代码索引中会形成一个横跨整个文件的符号范围。这不是某个仓库的怪癖,而是一类非常普遍的代码形态:Svelte 5 的.svelte.tsrune store、React 自定义 hook 模块、IIFE / module-pattern 的 JS,以及 Zustand 的create((set, get) => ({ … }))都是这种写法。

CG-27 要检验的机制是 src/mcp/tools.ts#L4975-L4983 中的ENVELOPE_KINDS规则:当一个容器节点覆盖文件行数的一半以上时,把它从该文件的聚簇(cluster)范围中丢弃——因为保留它会把内部所有方法合并成一个跨全文件的巨型集群,最终只渲染出容器头部、埋没掉方法本体:

// src/mcp/tools.ts (L4975-L4983, L4999-L5000) // Container kinds whose body can span most/all of a file. When such a // node covers most of the file we drop it from the ranges: keeping it // would merge every method inside it into one giant cluster ... const ENVELOPE_KINDS = new Set(['file', 'module', 'class', 'struct', 'union', 'interface', 'enum', 'namespace', 'protocol', 'trait', 'component']); // ... // Drop whole-file envelope nodes (containers covering >50% of the file). .filter(n => !(ENVELOPE_KINDS.has(n.kind) && (n.endLine - n.startLine + 1) > fileLines.length * 0.5))

注意这个集合里列的是容器类型(classstructinterfaceenum…),并不包含functionmethod。于是工厂闭包不会触发这条丢弃规则,作为跨文件范围存活下来,把内部所有闭包合并进同一个集群。

CG-27 提出的改动建议是把这条 >50% 丢弃规则扩展为"与 kind 无关",让function也参与丢弃。但报告指出,此前的 CG-30 任务已经约束了此类成员可花费的字节数(见 docs/benchmarks/explore-oversize-member-ab-cg30.md),所以剩下的争议是一个排名层面的主张:跨文件范围会把所有内部符号合并成一个集群,导致选择逻辑无法独立地给相关闭包排名。CG-27 的要求是:先测量这个主张,再谈改动。

2. 测量方法:确定性探针替代 agent A/B

测量工具是 scripts/agent-eval/probe-factory-closure.mjs,针对一个 hermetic fixture(tests/fixtures/factory-closure-ts/)运行。探针的确定性来自严格的复现流程:每次运行前把 fixture 复制到临时目录、删除既有.codegraph索引、全量重新索引,因此同一个构建跑两次会得到相同的数字(见 probe-factory-closure.mjs#L54-L61 的cpSync/rmSync/initSync/indexAll序列)。

报告特别说明为什么不用 agent A/B 实验:被测的主张是"一个文件内部选了哪些符号",而真实 agent 运行的噪音远大于这个粒度的差异,A/B 根本看不清。这是一个值得借鉴的方法论决策——测量对象是确定性管线(索引 → explore → 渲染),就应该在确定性管线上测。

探针的核心度量不是"输出多少字符",而是"哪些内部符号到达了 agent"。它把响应中形如<行号>\t<文本>的每一行与目标文件的真实源码逐字比对,只有行号与内容同时吻合才算"送达"(probe-factory-closure.mjs#L83-L93):

// 行号在散文里被引用不算数——必须文本与源行完全一致 for (const line of text.split('\n')) { const m = /^(\d+)\t(.*)$/.exec(line); if (!m) continue; const n = Number(m[1]); if (n >= 1 && n <= source.length && source[n - 1] === m[2]) delivered.add(n); }

然后对每个内部闭包检查其定义行是否落在已送达行集合中。

3. 测量夹具:三个 store、两个服务、一个 UI 消费方

fixturetests/fixtures/factory-closure-ts/ 是一个小型 dashboard 应用:三个工厂闭包 store、两个无状态服务和 UI 消费方,竞争同一份响应预算。

文件行数形态
src/stores/dashboard-store.ts385createDashboardStore跨 15–376 行(94%),内含 11 个闭包;文件尾部有一个类型别名 + 辅助函数
src/stores/alerts-store.ts141createAlertsStore跨 19–138 行(85%),内含 9 个闭包
src/stores/session-store.ts148文件作用域里只有一个工厂,没有任何伴生类型或尾部辅助函数
src/services/metric-service.ts、src/services/filter-parser.ts105、62普通顶层函数——对照组

fixture 的设计意图很明确:两个工厂文件都超过了WHOLE_FILE_MAX_LINES,因此无法走"整文件直出"路径,必须经由集群路径渲染——envelope 才真正起作用。这个阈值定义在 src/mcp/tools.ts#L4829:

const WHOLE_FILE_MAX_LINES = isCentralFile ? 280 : 220;

非中心文件的 220 行上限意味着 385 行的dashboard-store.ts和 148 行的session-store.ts中,只有前者被强制走集群路径(后者靠 fixture 中的其他竞争文件与预算约束间接施压)。

4. 结果 1:envelope 几乎一开始就选不中

shrinkCluster是处理超尺寸集群的核心函数(src/mcp/tools.ts#L5179-L5207)。它把集群成员按(importance 降序, size 升序)排序,并且一旦已经保留了任何成员,任何会使累计超出 cap 的成员都会被拒绝

// src/mcp/tools.ts (L5180-L5195) if (c.members.length < 2) return null; const byImportance = [...c.members].sort((a, b) => b.importance - a.importance || (a.end - a.start) - (b.end - b.start) || a.start - b.start); // ... for (const r of byImportance) { const sz = sizeOf(r) + GAP_MARKER.length; // Always keep the most important range, even if it alone is oversize — // an empty section sends the agent to Read, which costs far more. if (keep.length > 0 && kept + sz > cap) continue; keep.push(r); kept += sz; }

这个顺序规则有一个直接推论:一个跨文件的 envelope 成员只有在它是第一个候选时才会被保留——而这要求它必须是顶级重要性层的唯一成员。实测的 9 种查询形态中,有 8 种都存在某个更小的成员与工厂共享同一重要性层(一行的类型别名、尾部辅助函数、另一个闭包),工厂因此排在最后,从未被保留。envelope 规则在这个 fixture 上是无效的——因为选择逻辑已经在内部完成了按符号的细粒度排名。

5. 结果 2:提议的改动是一次大退步

把 >50% 丢弃规则改为与 kind 无关,在主查询"how does the dashboard store refresh its metrics and apply a filter"上的测量结果:

指标baseline丢弃该范围后
dashboard-store.ts(rank #1)交付内容7,539 字符397 字符
送达的内部闭包定义11 个中的 7 个11 个中的 0 个
该文件预留(reservation)未被花掉的部分05,601 中约 5,200

退步机制来自集群排序。集群排名在 src/mcp/tools.ts#L5425-L5439 实现:spine 集群优先,其次maxImportance降序,平局时按密度(score / span)决胜

// src/mcp/tools.ts (L5433-L5438) if (b.c.maxImportance !== a.c.maxImportance) return b.c.maxImportance - a.c.maxImportance; const densityA = a.c.score / a.span; const densityB = b.c.score / b.span; if (densityB !== densityA) return densityB - densityA;

丢弃 envelope 范围后,dashboard-store.ts拆成两个集群378-384(一行的类型别名 + 四行的辅助函数,score 15,span 7)和4-362(全部闭包,score 116,span 359)。前者密度 15/7 ≈ 2.14,后者 116/359 ≈ 0.32——当两者的maxImportance平局时,密度平局决胜让那个琐碎集群获胜。它先被取走,而且是唯一允许被收缩(shrink)的集群——后续集群永远不会被收缩,只能整体取舍。装答案的大集群随后装不下剩余预算,被整体丢弃

这是本报告最重要的架构洞察:那个跨文件范围正是把文件保持为一个集群的东西,而shrinkCluster已经在集群内部完成了 issue 所要求的按符号排名。丢弃 envelope 不是"获得细粒度",而是"摧毁了细粒度排名赖以工作的容器"。

6. 结果 3:更谨慎的同意图改动只是噪音

如果不动聚类粒度、只把 envelope成员shrinkCluster里延后处理(defer),就绕开了结果 2 的拆分。9 种查询形态、同一 fixture、同一索引下,送达的内部闭包定义数:

查询目标baselinedeferred
how does the dashboard store refresh its metrics and apply a filterdashboard7/118/11
createDashboardStoredashboard8/118/11
how is the dashboard store created and wired updashboard9/118/11
createDashboardStore exportCsv summarizedashboard9/119/11
where is the dashboard store constructeddashboard7/117/11
how are widgets loaded and the layout reconcileddashboard4/114/11
createSessionStore(不利形态:工厂是顶级层唯一成员)alerts6/97/9
how are alerts refreshed and acknowledgedalerts9/99/9
createAlertsStorealerts9/99/9
总计6869

在一个专门把该模式做到最大可见度的 fixture 上:一处变好、一处变差、七处不变。报告判定"这不是可测量的选择改进",于是什么都没有合入。CG-27 因此关闭为 obsolete,功劳记在 CG-30 名下。

7. envelope 何时真的会被选中——而 CG-30 已经吸收了它

上面表格里的不利形态(createSessionStore查询、目标是 alerts store)是唯一一种排序规则无法中立化的配置:createAlertsStore是 importance 10 层的唯一成员,于是它作为第一个候选被保留,以 3,939 字符对 2,468 的上限,所有内部闭包被跳过。

此时出场的是 CG-30 的行窗口化机制:与其整块输出或整文件丢弃,不如按整行开窗——响应携带了该文件 16–108 行,一段连续、可读的工厂头部,覆盖 9 个闭包定义中的 6 个。有界、充分、绝不为空。这正是当年 CG-27 issue 所针对的症状,且已被吸收。

实现上,这个机制由 src/mcp/tools.ts#L5222-L5243 的headWindowOf等窗口函数支撑:

// src/mcp/tools.ts (L5222-L5225) const MIN_WINDOW_LINES = 12; /** Rendered cost of one source line, line numbering included. */ const lineCost = (ln: number): number => (fileLines[ln - 1] ?? '').length + 1 + (withLineNumbers ? String(ln).length + 1 : 0);

窗口按"渲染成本"(含行号前缀)逐行累加,永远在整行边界上截断(正文从不被切到一半),短于MIN_WINDOW_LINES的残片会被直接丢弃——除非什么都没输出,此时"绝不为空"的底线优先于上限。窗口与后续部分之间的 GAP_MARKER 和行号跳跃是 agent 判断"这里被裁剪过"的信号。

8. 副产品:测量暴露的一个真实缺陷(另行立案)

结果 2 的机制并不限于假设中的改动。报告用同一探针在确定性 6 仓库套件上巡检"丢弃了某个集群、却把大部分预留闲置"的文件:

文件预算已花闲置保留的集群丢弃的集群
django/db/models/sql/query.py10,1351,9238,212 (81%)1379–1400,score 14306–929,score 290
okhttp .../RealInterceptorChain.kt6,0581,4744,584 (76%)16–44,score 44113–373,score 171
okhttp .../Interceptor.kt4,6972,0272,670 (57%)85–138,score 21154–257,score 10
gin/routergroup.go5,7823,2732,509 (43%)33–91,score 116103–188,score 128

模式清晰:一个按密度排名的顶级集群如果是琐碎的,它会赢下预算,而携带 20 倍 score 的集群被整体丢弃,文件自己的预留也大部分闲置——根源仍是"只有首个被选中的集群允许被收缩"这条规则。其中query.py正是 codegraph 的 CLAUDE.md 点名的_fetch_all案例文件(见 CLAUDE.md)。这个发现说明:否定一个提议改动的测量过程,顺带审计出了存量缺陷的分布。

9. 常设门禁:pin 住结果,而不是 pin 住机制

与报告配套的门禁测试是tests/explore-factory-closure.test.ts。它的设计哲学写在文件头注释里:"this file pins the OUTCOME, not the mechanism"——无论未来谁对聚类做了什么改动,工厂闭包文件必须继续把内部闭包送达 agent,这才是阻止 agent 回退到整文件 Read 的底线。

门禁分两层。先自证 fixture 没有腐化(否则下面的断言没有意义):

// __tests__/explore-factory-closure.test.ts (L104-L121) expect(factory!.kind).toBe('function'); expect(factory!.endLine - factory!.startLine + 1) .toBeGreaterThan(sourceLines.length * 0.5); // 恰好是 >50% envelope 条件 expect(innerClosures().length).toBeGreaterThanOrEqual(8); // 超过 WHOLE_FILE_MAX_LINES,确保走集群路径 expect(sourceLines.length).toBeGreaterThan(220); expect(report.files.find((f) => f.path === TARGET)?.render).toBe('clusters');

再锁结果(tests/explore-factory-closure.test.ts#L124-L156):

  • 查询点名的refreshMetricsapplyFilter两个闭包的定义行必须被送达;
  • 内部闭包送达率不低于半数(feature/CG-24 分支测得 7/11),且送达不能只堆在工厂头部——最深处被送达的闭包必须越过首尾闭包定义行的中点;
  • 该文件的响应段绝不能为空(emittedChars > 0);
  • 总响应不超过硬上限(report.envelope.chars <= report.budget.hardCeiling)。

送达判定与探针脚本同一套规则(行号 + 文本逐字匹配,L78-L87),保证"散文里引用过的行号不算数"。

10. 复现步骤

npm run build node scripts/agent-eval/probe-factory-closure.mjs # 主查询 node scripts/agent-eval/probe-factory-closure.mjs \ --target src/stores/alerts-store.ts --factory createAlertsStore \ --query "createSessionStore" # 不利形态 npx vitest run __tests__/explore-factory-closure.test.ts # 常设门禁

探针还支持--json输出(probe-factory-closure.mjs#L129-L146),会打印每个文件的 rank / render / emitted / final、目标文件的送达行区间、以及逐闭包的送达清单;--target/--factory/--query三个参数分别切换目标文件、工厂名和查询文本,默认值即主查询配置。

11. 结论:测量先行的判断框架

CG-27 是一个"用测量否决改动"的完整范例,其方法论可以迁移到任何预算敏感的选择系统:

  1. 改代码前先测量主张本身。issue 的真正问题不是"envelope 该不该丢",而是"不丢它有没有造成可观测的损失"——后者是可以用确定性探针回答的,前者则不必动手。
  2. A/B 实验有边界。当被测信号(文件内符号选择)的粒度低于 agent 运行噪音时,切换到确定性 harness 是唯一可行的测量路径。
  3. 大容器范围可能是结构而非噪声ENVELOPE_KINDSfunction的"漏掉"实际上是整个选择管线的支点:集群是排名和收缩的基本单元,拆掉跨文件范围等于拆掉单元本身。
  4. 门禁 pin 结果而不是机制tests/explore-factory-closure.test.ts 不锁死任何实现细节,只锁死"工厂闭包文件必须持续交付内部闭包"这一结果,为后续演进(如 docs/benchmarks/explore-tail-render-cg38.md 所述的尾渲染保护)留出了空间。
  5. "只有首个集群可收缩"是结构性风险点。结果 2 与第 8 节的存量缺陷共享同一根源:密度平局决胜让琐碎集群赢下预算后,高分集群只能整体被丢、预留大量闲置。这条规则在 src/mcp/tools.ts#L5425-L5439 的排序之后生效,是 codegraph explore 管线中值得持续关注的约束。

【免费下载链接】codegraphPre-indexed code knowledge graph, auto syncs on code changes, for Claude Code, Codex, Gemini, Cursor, OpenCode, AntiGravity, Kiro, CoPilot, and Hermes Agent — fewer tokens, fewer tool calls, 100% local项目地址: https://gitcode.com/GitHub_Trending/co0degr/codegraph

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询