恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
codegraph 的确定性测量范例:为什么“丢弃 >50% 工厂闭包范围“的改动(CG-27)被测量否决了
首页
资讯中心
/
codegraph 的确定性测量范例:为什么“丢弃 >50% 工厂闭包范围“的改动(CG-27)被测量否决了
codegraph 的确定性测量范例:为什么“丢弃 >50% 工厂闭包范围“的改动(CG-27)被测量否决了
发布时间:2026/9/7 2:38:47
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))注意这个集合里列的是容器类型class、struct、interface、enum…并不包含function或method。于是工厂闭包不会触发这条丢弃规则作为跨文件范围存活下来把内部所有闭包合并进同一个集群。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 fixturetests/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#L4829const WHOLE_FILE_MAX_LINES isCentralFile ? 280 : 220;非中心文件的 220 行上限意味着 385 行的dashboard-store.ts和 148 行的session-store.ts中只有前者被强制走集群路径后者靠 fixture 中的其他竞争文件与预算约束间接施压。4. 结果 1envelope 几乎一开始就选不中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.tsrank #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 15span 7和4-362全部闭包score 116span 359。前者密度 15/7 ≈ 2.14后者 116/359 ≈ 0.32——当两者的maxImportance平局时密度平局决胜让那个琐碎集群获胜。它先被取走而且是唯一允许被收缩shrink的集群——后续集群永远不会被收缩只能整体取舍。装答案的大集群随后装不下剩余预算被整体丢弃。这是本报告最重要的架构洞察那个跨文件范围正是把文件保持为一个集群的东西而shrinkCluster已经在集群内部完成了 issue 所要求的按符号排名。丢弃 envelope 不是获得细粒度而是摧毁了细粒度排名赖以工作的容器。6. 结果 3更谨慎的同意图改动只是噪音如果不动聚类粒度、只把 envelope成员在shrinkCluster里延后处理defer就绕开了结果 2 的拆分。9 种查询形态、同一 fixture、同一索引下送达的内部闭包定义数查询目标baselinedeferredhow does the dashboard store refresh its metrics and apply a filterdashboard7/118/11createDashboardStoredashboard8/118/11how is the dashboard store created and wired updashboard9/118/11createDashboardStore exportCsv summarizedashboard9/119/11where is the dashboard store constructeddashboard7/117/11how are widgets loaded and the layout reconcileddashboard4/114/11createSessionStore不利形态工厂是顶级层唯一成员alerts6/97/9how are alerts refreshed and acknowledgedalerts9/99/9createAlertsStorealerts9/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–1400score 14306–929score 290okhttp .../RealInterceptorChain.kt6,0581,4744,584 (76%)16–44score 44113–373score 171okhttp .../Interceptor.kt4,6972,0272,670 (57%)85–138score 21154–257score 10gin/routergroup.go5,7823,2732,509 (43%)33–91score 116103–188score 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查询点名的refreshMetrics、applyFilter两个闭包的定义行必须被送达内部闭包送达率不低于半数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 是一个用测量否决改动的完整范例其方法论可以迁移到任何预算敏感的选择系统改代码前先测量主张本身。issue 的真正问题不是envelope 该不该丢而是不丢它有没有造成可观测的损失——后者是可以用确定性探针回答的前者则不必动手。A/B 实验有边界。当被测信号文件内符号选择的粒度低于 agent 运行噪音时切换到确定性 harness 是唯一可行的测量路径。大容器范围可能是结构而非噪声。ENVELOPE_KINDS对function的漏掉实际上是整个选择管线的支点集群是排名和收缩的基本单元拆掉跨文件范围等于拆掉单元本身。门禁 pin 结果而不是机制。tests/explore-factory-closure.test.ts 不锁死任何实现细节只锁死工厂闭包文件必须持续交付内部闭包这一结果为后续演进如 docs/benchmarks/explore-tail-render-cg38.md 所述的尾渲染保护留出了空间。只有首个集群可收缩是结构性风险点。结果 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),仅供参考