CodeGraph Explore 集群饥饿修复全解析:CG-36 确定性测量与簇间预算再分配机制

CodeGraph Explore 集群饥饿修复全解析:CG-36 确定性测量与簇间预算再分配机制 CodeGraph Explore 集群饥饿修复全解析CG-36 确定性测量与簇间预算再分配机制【免费下载链接】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/codegraphCodeGraph 的codegraph_explore工具在把代码上下文塞进有限 token 信封时存在一个隐蔽缺陷单个文件内部的簇cluster即按相邻性聚合的代码片段分配是全有或全无的——榜首簇被整块拿走后排名靠后的簇要么整体放下、要么整体丢弃导致高相关文件的预算被浪费、答案代码被低分文件顺走。本文基于仓库中的 CG-36 基准文档 explore-cluster-starvation-cg36.md完整还原这一缺陷的成因、误诊过程、两处修复、六仓库套件验证结果以及配套落地的探针脚本、hermetic 夹具与回归测试并逐条对照 src/mcp/tools.ts 中的实际实现代码讲清簇间预算再分配这一修复的源码级细节。读完本文你将掌握Explore 响应预算从文件级分配到簇级选择的完整链路、MIN_CHARS/FILE_OVERHEAD等关键常量的实际作用、饥饿缺陷为何对信封总量类探测不可见、以及如何用配对式pair-based指标对上下文投递质量做可门禁化的回归测量。一、缺陷本体簇选择的全有或全无在 CG-36 之前的实现里一个文件的簇选择规则是排名第一的簇总是被拿走——如果它超出该文件的预算就收缩shrink到能装下的高重要度完整符号区间它下面的每一个簇都先按完整形态渲染然后要么整块放入剩余空间、要么被整体丢弃。问题在于如果一个文件的榜首簇非常琐碎trivial真正的答案代码就整个被丢掉了。以基准文档中的真实测量为例基线为feature/CG-24分支尖端76ab1ferepo文件scorereservedspentsharedjangodb/models/sql/query.py837,9471,92324%djangocontrib/admin/filters.py182,2718,057355%okhttp.../RealInterceptorChain.kt866,0581,47424%okhttp.../CallServerInterceptor.kt201,9745,832295%score 83 的文件只花掉自己预留预算的四分之一而 score 18 的文件却拿走了自己预留的 3.5 倍。这个缺陷之所以长期不可见是因为响应始终看起来是满的未花掉的预留额会按 CG-31 的设计原样结转carry forward给排名靠后的文件于是低分文件拿走了这些字节而所有信封份额类指标仍然显示健康。这是 CG-36 对测量方法论最重要的启示只看响应总量永远发现不了投递错误必须看每个文件的预留-实际消耗对账。簇排名顺序为何如此簇的排序逻辑可以直接在源码中确认。src/mcp/tools.ts 中的排序比较器按以下优先级依次比较hasSpine是否承载流程主干/调用路径——主干簇优先因为渲染出的调用链本身就是流程类问题的答案maxImportance簇内最高符号重要度密度score / span总分score更小的 span跨度。这一顺序是刻意的、有保护性的——后面会说明它为何不能动。二、误诊纠正问题不在密度排名原始 issue 给出了两个候选修复点并怀疑第一个簇排名在hasSpine→maxImportance之后用密度score / span平局决胜这在结构上偏向小而琐碎的簇胜过大而承载答案的簇。但把簇集合 dump 出来之后结论相反——在两个真实案例里输掉的都是maxImportance而不是密度django/db/models/sql/query.py budget 10,135 spent 1,923 KEPT 1379–1400 span 22 score 14 maxImp 6 (check_related_objects — 一个胶水符号) DROPPED 306– 929 span 624 score 290 maxImp 3 (133 members: 整个 Query 类) okhttp .../RealInterceptorChain.kt budget 6,058 spent 1,474 KEPT 16– 44 span 29 score 44 maxImp 6 (包声明 import 块) DROPPED 113– 373 span 261 score 171 maxImp 3 (73 members: 拦截器链本身)maxImportance排在前面是刻意且必要的——正是它阻止了 Alamofire 的Session.swift把预算输给文件头部的属性列表该文件perform/didCreateURLRequest/task等方法位于约 200 行的密集低重要度声明之下。因此排名顺序一行未动真正的杠杆是第二个修复点不要整体丢弃落败的簇。三、修复内容两处站点同一条规则修复的核心规则一句话概括只要剩余空间还值得构成一个代码段就把它保住——这正是 CG-26 在文件之间已经验证过的教训现在被应用到簇之间。3.1 选择阶段后位簇收缩进剩余空间在 src/mcp/tools.ts 的选择循环中修复后的逻辑是// Later clusters used to be all-or-nothing: rendered whole, then taken // only if the whole thing fit the remainder. ... // So a later cluster is shrunk INTO the remainder by the same whole-member // rule the first one already uses — CG-26s between-FILES lesson ... // applied between CLUSTERS. const room cap - projectedChars - GAP_MARKER.length; if (room EXPLORE_ALLOCATION.MIN_CHARS) continue; const section renderCluster(rc.c, room, room); const text sectionText(section.parts); if (text.length 0) continue; // The never-empty floors inside the windowing may overrun room ... // The first cluster is allowed that overshoot — an empty section is worse — // but a later one is not. if (projectedChars text.length GAP_MARKER.length cap) continue;要点后位簇现在收缩进文件预算的剩余部分使用的与第一个簇相同的完整成员规则绝不从方法体中间截断当剩余空间低于MIN_CHARS700 字符定义见 src/mcp/tools.ts时剩余空间装不下一个可读的完整代码块因此保持丢弃而不是产生一堆碎片——碎片反而会成为下一次调用去重逻辑必须绕开撕扯的对象一个不对称细节永不空never-empty的窗口下限可能让渲染结果超出room。第一个簇被允许这种超射空段落更糟但后位簇不允许——因为它超出的部分实际花掉的是排名更低文件的名额。MIN_CHARS 700的语义在源码注释中解释得很清楚低于这个长度一片代码装不下一个完整的方法而碎片严格劣于指针——它会迫使调用者发起本工具正是要预防的 Read。它同时是分配器的地板allocateExploreBudget中人人先拿MIN_CHARS剩余部分按权重切分src/mcp/tools.ts。3.2 上限修剪阶段先重渲染再丢弃第二处站点是渲染上限renderCeiling修剪当一段的精确成本超出上限时最弱的已选簇会被重新渲染进剩余空间而不是直接整块丢弃。源码实现见 src/mcp/tools.ts// The weakest cluster is SHRUNK into the room that is left before it is // dropped (CG-36). Dropping it whole makes this loop as all-or-nothing as // the selection above it was, and at the same cost: on excalidraws // typeChecks.ts the estimate missed by 13 chars and a 1,512-char cluster // — the files highest-SCORING one, last only because rank breaks ties on // density — was thrown away to pay for it. ... if (room EXPLORE_ALLOCATION.MIN_CHARS !reshrunkOnce.has(weakest)) { reshrunkOnce.add(weakest); const reshrunk renderCluster(clusters[weakest]!, room, room); ... }这一处值得单独点名在 excalidraw 的typeChecks.ts上段成本估算只差了13 个字符却把一个 1,512 字符的簇——该文件得分最高的簇只因排名用密度平局决胜而排在最后——整块丢掉只为省下这 13 字符。修复后收回了 excalidraw 1,449 字符损失中的 1,501 字符含弹性结尾段的补位。实现里还有一个防自旋细节每个簇最多尝试一次重收缩reshrunkOnce集合——第二次尝试说明第一次重渲染并没有省下足够头部随内容移动了此时直接丢弃避免逐字符地磨。四、套件结果字节沿评分顺序上移node scripts/agent-eval/probe-file-spend.mjs在 6 个仓库上做了干净重建clean full-rebuilt indexesCG-33 保证。下表只列出消耗发生移动的文件score 是候选的排名分reserved 是其分配额repo文件scorereservedbeforeafterdjangodb/models/sql/query.py837,9471,92310,082djangocontrib/admin/filters.py182,2718,0572,198djangoutils/autoreload.py121,7473,1451,709djangodb/models/fields/related_descriptors.py111,6602,5161,493excalidrawelement/src/typeChecks.ts232,7403,1022,573excalidrawexcalidraw/types.ts141,9428191,372okhttp.../RealInterceptorChain.kt866,0581,4746,038okhttp.../Interceptor.kt644,6972,0274,659okhttp.../RealCall.kt523,9723,6283,922okhttp.../Call.kt544,0974,0972,073okhttp.../CallServerInterceptor.kt201,9745,8321,959okhttpandroidMain/.../AndroidDns.kt211,9991,8120tokiotask/local.rs404,3614,5994,798tokioruntime/task/harness.rs141,9812,5652,341ginroutergroup.go875,7823,2735,632gintree.go171,6938921,969ginginS/gins.go262,2134,4312,171alamofireSource/Core/Session.swift342,7922,7973,396alamofireSource/Core/Request.swift1489,1008,8658,453每个仓库中字节都沿评分顺序上移。信封总量reposource beforeafterΔfilesceilingdjango20,87820,719−1596 → 624,963 ≤ 25,000excalidraw19,65219,704528 → 824,813 ≤ 25,000okhttp18,87018,651−2196 →524,985 ≤ 25,000tokio21,60721,582−255 → 524,777 ≤ 25,000gin10,77611,9521,1764 → 414,655 ≤ 19,500alamofire11,66211,8491872 → 212,862 ≤ 19,500total103,445104,4571,012饥饿标志starvation flags8 → 0。所有仓库都保持在或低于各自的硬上限净收益1,012 源码字符。五、唯一代价直白陈述okhttp 丢掉了排名第 6 的文件androidMain/.../AndroidDns.ktscore 21一个平台 DNS 辅助文件与拦截器链如何工作的问题无关并损失 219 源码字符换来的是RealInterceptorChain.kt4,564 和Interceptor.kt2,632——这两个文件才是回答问题的文件。文档明确指出这不是新缺陷也不是修复越界。okhttp 的预留额结构性超订over-subscribed分配器在切分maxOutputChars时按每文件固定收取FILE_OVERHEAD 200见 src/mcp/tools.ts而真实头部要 300–500 字符所以承诺总和约 22,800 源码 约 2,100 真实头部超过了约 24,760 字符渲染上限能承载的量。owedPayableBelow机制已经拒绝为能预见会被丢弃的文件留字节而 AndroidDns.kt 恰在那条线之后它在旧基线上存活只是因为其上方的文件恰好花得少——是运气不是设计。关闭超订的正确做法是让分配器按每文件头部估算计费而非固定 200但那比本 issue 的改动范围更大CG-26 曾刻意保留FILE_OVERHEAD作为分配器自己的常量。六、什么没有变簇排名顺序hasSpine→maxImportance→ 密度 → score → span一行未动对应 src/mcp/tools.ts 的排序器AlamofireSession.swift密度优先规则所保护的那种形态不但没受损反而净增 599 字符CG-27 工厂闭包结果probe-factory-closure.mjs显示 11 个内部闭包定义中投递 7 个与基线完全一致CG-31/CG-26 预留不变量[explore-reservation-invariant.test.ts] 保持绿每个仓库都停留在硬上限之下四个分配夹具全部通过——payroll-go、self-query以及本次新增的两个。七、落地的可测量设施为使这一缺陷可长期测量仓库中随修复一起交付了四类设施7.1 常驻探针probe-file-spend.mjsscripts/agent-eval/probe-file-spend.mjs 是每文件预留 vs 实际投递的常驻扫描。它的关键设计是标志一个配对pair而不是单个文件一个大份额未花掉的文件同时存在一个得分显著更低却超支的文件。单边单独出现都是合法的小文件本来就没那么多可说结转机制正是用来把余量传给下家的——这也是信封探针永远看不见此缺陷的原因。任一仓库出现标志即退出码 1因此可以直接做 CI 门禁。源码中四个阈值参数scripts/agent-eval/probe-file-spend.mjsconst STARVED_SHARE 0.5; // spent half its reservation花掉不足预留的一半 const OVERSPEND_RATIO 1.5; // spent 1.5x its own reservation超支超过 1.5 倍 const SCORE_RATIO 2; // ...while scoring less than half the starved file得分不足被饿文件的一半 const MIN_RESERVED 2000; // 忽略预留额小到不足以说明问题的文件配对判定逻辑findStarvation要求两侧同时成立被饿方finalChars allowance * 0.5且预留 ≥ 2,000超支方finalChars allowance * 1.5且overspent.score * 2 starved.score。数据源是CODEGRAPH_EXPLORE_DEBUG诊断旁路因此它测量的是真实出货的分配器而非从 markdown 反推份额。典型用法node scripts/agent-eval/probe-file-spend.mjs # 需先 npm run build 全量重建索引 node scripts/agent-eval/probe-file-spend.mjs --json /tmp/new.json node scripts/agent-eval/probe-file-spend.mjs --baseline /tmp/base.json node scripts/agent-eval/probe-file-spend.mjs django --all # 打印全部文件而非仅标志 CORPUS/tmp/codegraph-corpus node scripts/agent-eval/probe-file-spend.mjs其扫描的六仓库套件与查询scripts/agent-eval/probe-file-spend.mjsconst SUITE [ { id: django, q: How does a QuerySet turn into SQL and fetch rows from the database? }, { id: excalidraw, q: How does updating an element re-render the canvas on screen? }, { id: okhttp, q: How does a call go through the interceptor chain to the network? }, { id: tokio, q: How does a spawned task get scheduled and run by a worker? }, { id: gin, q: How does a registered route handler get invoked for an incoming HTTP request? }, { id: alamofire, q: How does a request get built and sent through the session? }, ];7.2 两个 hermetic 夹具方向相反tests/fixtures/starved-cluster-ts/——django 与 okhttp 形态的还原体。目标文件 src/pipeline/chain.ts 里榜首簇是文件头部的describeChain一行胶水函数与入口点相邻因而携带文件内最高每符号重要度而真正承载答案的RequestChain类proceed→advance→writeAndRead流程位于簇间隙阈值之外、形成第二个大簇。在 CG-24 基线上该夹具失败仅花掉预留的 28.8%proceed与writeAndRead均未投递带修复后通过。tests/fixtures/dense-header-ts/——Session.swift形态的字节级锚点。目标 src/net/session.ts 在perform方法上方有 20 个相邻低重要度声明文件中最密集区域查询点名perform/didCreateURLRequest/task三个方法它们必须继续赢得预算。这是反向砝码如果未来某次改动让密度重新压过重要度这个夹具会失败。两个夹具必须一起读、一起满足。7.3 回归测试门禁tests/explore-cluster-starvation.test.ts 通过npm test把两个夹具钉死在套件里。它的断言结构分两层夹具形状自检如果这里腐化了下面的门禁就毫无意义目标文件必须走clusters渲染路径、两个符号在行号上相隔足够远以保证独立成簇、目标文件必须拿到最大分配额allowance 4000且大于所有其他文件行为门禁finalChars / allowance 0.6故意定得远低于修复前后的两个实际值 28.8% 与 131%使普通预算波动不会打红套件、响应必须包含流程两端的具体函数签名如async proceed(request: PipelineRequest)、private async writeAndRead(request: PipelineRequest)、且响应总长不超过hardCeiling。测试通过CODEGRAPH_EXPLORE_DEBUG侧车文件获取分配诊断报告ExploreDiagnosticReport而不是解析响应文本本身——这正是必须看预留对账不能只看渲染结果的方法论落地。八、方法论注记为什么渲染出的 markdown 什么都看不出来基准文档最后的方法论段落值得所有做上下文预算优化的开发者借鉴簇选择的缺陷在渲染出的 markdown 中不可见。要看见它必须在 dist/mcp/tools.js 的let assembled assembleSection(chosenIndices);之前打补丁日志输出fileBudget/projectedChars/ 每个已排名簇的 span、score、maxImportance、是否被选中及成员列表。只读响应会让成员选择效应看起来像预算效应。构建间源码字符数的差异不自动等于回归excalidraw 第一刀上的 −1,449 字符并非分配丢失而是弹性结尾段elastic epilogue扩张进了一段 13 字符记账误差释放出的空间。该缺陷的完整验证链是确定性测量而非 agent A/Bagent 运行太有噪声看不出 2K 级别的位移所以声明只针对字节去了哪个文件并用干净重建的索引CG-33排除索引漂移干扰。九、总结CG-36 修复的价值在于它同时交付了三样东西一个具体缺陷的修复簇间收缩进剩余空间规则两处站点选择循环与上限修剪、一个可门禁的测量方法配对式饥饿标志 确定性探针而非 agent A/B、一组方向相反的回归锚点starved-cluster-ts与dense-header-ts两个夹具防止修复本身在未来被回退。所有结论均可在仓库中复核核心实现在 src/mcp/tools.ts 的EXPLORE_ALLOCATION常量表、allocateExploreBudget与簇选择/上限修剪循环测量设施在 scripts/agent-eval/probe-file-spend.mjs回归门禁在tests/explore-cluster-starvation.test.ts。同系列的 CG-24、CG-26、CG-27、CG-30、CG-31 基准文档位于 docs/benchmarks/可作为理解 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),仅供参考