:让每一次代码编辑都得到图级即时反馈与 CI 门禁)
code-graph-rag 结构增量Structural Delta让每一次代码编辑都得到图级即时反馈与 CI 门禁【免费下载链接】code-graph-ragThe ultimate RAG for your monorepo. Query, understand, and edit multi-language codebases with the power of AI and knowledge graphs项目地址: https://gitcode.com/GitHub_Trending/co/code-graph-rag导读在 code-graph-rag 中当 AI Agent 通过 MCP 工具编辑代码时write_file、surgical_replace_code、structural_replace等写入工具会重新摄取受影响的文件并在工具结果中追加一段 JSON 结构增量structural delta告诉 Agent 这次编辑对程序结构产生了什么影响新增/删除/重命名了哪些符号、哪些调用点变成了悬空引用、哪些函数签名变了、是否产生了新的重复代码或导入环以及哪些测试会触及被改动的符号。与之配套的cgr check --base ref命令则把同一套算法应用到整个工作树作为 CI 与 pre-commit 的结构回归门禁。读完本文你将掌握结构增量的计算原理、JSON 报告每个字段的精确语义、arity 判定与重命名配对的核心算法以及如何用cgr check在提交前拦截结构性破坏。从写文件到图变了结构增量的提出背景一个通过 cgr 修改代码的 Agent 需要立即知道编辑对程序结构做了什么对应 issue #1525。传统做法是让 Agent 自行 diff 文本、猜测影响面这在多语言 monorepo 中极易遗漏跨文件调用点。cgr 的做法是把编辑与图绑定surgical_replace_code、write_file以及structural_replacedry_runfalse执行后被触碰的文件会通过作用域重摄取scoped re-ingest进入图一份 JSON 增量随后被追加到工具结果中cgr check --base ref对整棵工作树计算同样的增量用于 CI 与 pre-commit。测试 test_write_file_appends_the_structural_delta 与 test_surgical_replace_appends_the_structural_delta 分别验证了write_file与surgical_replace_code会在结果中带上以MCP_DELTA_HEADER分隔的 delta 负载而 test_write_on_an_unindexed_project_appends_nothing 则证明项目未被索引时写入不会产生 delta——作用域重摄取只能补全一个已有图不能代替首次全量索引。如何计算内存中的 graph_diff 孪生codebase_rag.structural_delta是services/graph_diff.py的内存版孪生——后者在离线状态下对导出的索引做 diff参见 services/graph_diff.py 的diff_indexes/diff_is_empty而前者直接在运行中的图上工作。核心流程由 observe() 承担它把读取两次快照 执行重摄取 客户端 diff编排成一个原子过程在重摄取之前读取被触碰文件子图before快照执行apply即作用域重摄取记录reingest_ms在重摄取之后再次读取同样路径的子图after快照调用 structural_delta() 在客户端做集合差运算并汇总报告。两次读取与重摄取运行在与其他所有 MCP 图访问相同的锁之下因此增量总是描述图的同一代状态——不会出现读到一半图被别的写入改掉的竞态。四次固定读取快照读取由 snapshot() 完成全部使用限定到当前项目前缀STARTS WITH $project_prefix的固定 Cypher 查询读取内容对应查询codebase_rag/cypher_queries.pydefinitions被触碰文件中定义的符号含声明的位置参数与整棵骨架指纹CYPHER_DELTA_DEFINITIONScallees派生被触碰文件调用点所解析到的、定义在别处的被调者按名称回取以获知其签名CYPHER_DELTA_DEFINITIONS_BY_QNsites进出被触碰文件的每一条CALLS/REFERENCES/INSTANTIATES边含每个调用点的位置与参数形态来自 edge-site 属性CYPHER_DELTA_SITESmodule imports项目完整的Module -IMPORTS- Module图CYPHER_DELTA_MODULE_IMPORTS值得注意的细节CYPHER_DELTA_DEFINITIONS_BY_QN同时限定$qns与项目前缀因为CYPHER_DELTA_SITES的被调侧不做前缀限定未解析或外部目标可能命中它——在共享图中若不做前缀过滤来自另一个项目的同名定义就会错误地决定本次 arity 判定。两次快照在客户端做差集另外还有一次项目级的线性读取重复指纹索引服务于重复代码查找而触及被改符号的测试通过逐步回走调用者得到CYPHER_DELTA_CALLERS_OF一次一跳。整条链路是固定 Cypher 客户端集合运算因此报告是确定性的且开销与触碰文件的边数成正比。报告内容一个字段一个字段地拆解structural_delta()返回的StructuralDelta字典定义见 structural_delta.py结构如下{ paths: [pkg/util.py], reparsed: [pkg/util.py], affected: [pkg/app.py], removed_files: [], symbols: { added: [], removed: [], renamed: [{old: proj.pkg.util.helper, new: proj.pkg.util.assist, path: pkg/util.py}], changed: [] }, dangling_callers: [ {caller: proj.pkg.app.run, path: pkg/app.py, line: 5, col: 11, target: proj.pkg.util.helper, renamed_to: proj.pkg.util.assist} ], signature_changes: [], arity_findings: [], new_duplicates: [], new_import_cycles: [], tests_reaching: [ {qualified_name: proj.tests.test_app.test_run, path: tests/test_app.py, depth: 2, through: proj.pkg.app.run} ], reingest_ms: 41.2, delta_ms: 3.8 }各字段语义与文档原文一致并补充源码细节字段含义paths本次参与比较的路径集合before 与 after 快照路径的并集reparsed/affected/removed_files直接来自重摄取的ReingestReport被重新解析的文件、受影响依赖文件、被删除的文件symbols.renamed一个符号消失同时同一文件内出现一个整棵骨架指纹完全相同的符号一对一配对symbols.changed符号的骨架指纹或声明的位置参数发生了变化仅字面量的改动不会在此登记dangling_callers仍以旧名引用已被删除/重命名符号的调用点编辑未涉及的文件中的所有调用者以及编辑文件中未重新绑定到新名的调用者line/col是调用点记录的位置signature_changes位置参数发生变化的符号附上每一个调用点及各自的判定结论arity_findings编辑文件中实参位置参数多于被调者声明的调用点too_many这是唯一不需要知道默认值即可断言的判定new_duplicates新增或变更的函数其指纹exact或分支集合similar以重复阈值做 Jaccard 相似度与既有函数匹配original是更早的那个。重复检测器的最小规模门槛同样适用new_import_cycles模块导入图的强连通分量中包含被编辑模块、且编辑前不存在的那些tests_reaching从调用图中可达被编辑文件任一符号的测试函数含最短距离与所经符号reingest_ms/delta_ms重摄取耗时与增量自身的开销两次快照 diff此外StructuralDelta还携带stale_importers符号被移走、遗留的仍指向空模块的导入者仅 move 操作产生与call_sites被触碰符号编辑前后的入站调用计数SymbolDelta/RenameFinding等类型定义见 structural_delta.py。符号重命名的四轮配对算法_renames()structural_delta.py不依赖 git 的文本相似度启发式而是基于重命名保留函数体这一事实同一文件内、整棵骨架指纹相同、名字不同的符号即是一次重命名按排序顺序一对一配对避免重复的函数体被报成同一符号的两次重命名。配对分四轮一轮比一轮窄纯重命名_pair_by_shape同文件 同指纹 新名字移动_pair_by_moveissue #1534名字与函数体相同但文件不同容器随成员迁移_carried_container类本身没有指纹类被重命名时读作删除 新增但其方法带指纹且已被配对——若新增容器是某个被删容器所有已配对方法的唯一目标则把容器也配成一次重命名空容器_pair_lone_containers空容器没有指纹也没有后代唯一变化的只有名字两份快照无法区分重命名与替换——这一轮不做推断。只有调用方如 rename 操作显式declared的配对才会被接受否则一律如实报告为删除 新增。第四轮的保守有其理由凭空猜测会产生从未发生的重命名而契约把意外的重命名视为失败一个被发明的重命名会回滚一次正确的编辑Greptile, PR #1547。测试 test_an_unrelated_empty_class_is_not_reported_as_a_rename、test_an_empty_class_rename_is_reported_when_the_caller_DECLARES_it 与 test_an_undeclared_empty_class_rename_is_a_removal_plus_an_addition 精确锁定了这一语义。悬空调用者与签名变更_dangling()structural_delta.py遍历 before 快照中所有指向已删除或已重命名符号的调用点若调用者位于本次重新解析的文件中且已重新绑定到新名(caller, new_name)出现在 after 边中或调用者本身已从 after 定义中消失则跳过否则报告为dangling_callers并附带renamed_to提示目标新名。这直接对应测试 test_rename_without_updating_callers_reports_dangling_callers 与 test_updated_caller_is_not_dangling。signature_changes则只在symbols.changed中位置参数列表实际发生变化的符号上生成并为 after 快照中每一个指向它的CALLS调用点计算判定_signature_changes。Arity 判定接收者算术与未知的诚实判定逻辑复用crash_correlation.diagnose_aritycrash_correlation.py的接收者算术方法Method标签的self在 CPython 中计数但不是调用方提供的实参。只有 Python 定义携带positional_params因此其他语言调用点的判定读作unknownDELTA_ARITY_UNKNOWN。几个精妙的细节变参豁免存储的参数列表在*args处截断因此仅凭列表无法区分f(a)与f(a, *rest)。_is_variadic()structural_delta.py会把定义头部读回来用正则(?!\*)\*(?!\*)\s*[A-Za-z_]识别变参确保变参被调者永远不会被报为实参过多测试 test_variadic_callee_is_never_too_many。裸*仅限关键字不接受额外位置参数**name接受的是关键字而非位置参数因此都被排除关键字算术arg_count也把关键字计入issue #1522。判定时先扣除关键字名数量得到位置实参再把命名了已声明位置参数的关键字加回命名未声明参数的关键字是中性的——它可能是仅限关键字的参数或**kwargs存储的位置列表看不见且名字错误不属于 arity 故障测试 test_keyword_arguments_are_counted_once、test_keyword_arguments_to_a_kwargs_callee_are_not_too_many仅此两种确定too_many是唯一不需要知道默认值即可断言的结论possibly_missing实参少于参数只是提示而非发现——图不记录默认值因此它不会触发--fail-on-found。常量见 constants/duplicates.pyok/too_many/possibly_missing/unknown。测试 test_two_arg_call_to_one_arg_function_is_an_arity_finding 与 test_extra_positional_to_a_keyword_only_callee_is_too_many 分别覆盖了too_many的触发与不误报。新重复代码与新导入环重复代码_new_duplicates()structural_delta.py对项目做一次线性扫描CYPHER_DUPLICATE_FINGERPRINTS只关注本次新增或变更的符号。匹配规则_match_duplicate_shapes指纹完全相同 →exact相似度 1.0否则用分支集合做 Jaccard 相似度达到阈值DUPLICATES_DEFAULT_THRESHOLD 0.8→similar。候选函数必须满足最小规模门槛DUPLICATES_DEFAULT_MIN_NODES 15个 AST 节点original指向更早定义的那个同一批新增符号之间只在字典序更早的一侧报告一次避免成对重复。测试 test_pasting_a_helper_under_a_new_name_is_a_new_duplicate 验证了换个名字粘贴一个助手函数会被识别。导入环import_cycles()structural_delta.py用迭代版 Tarjan 算法求模块导入图的强连通分量strongly_connectedstructural_delta.py因为模块图可能深达数千层递归会爆栈环 成员数 1 的分量或自环。_new_import_cycles只报告包含被编辑模块且 before 中不存在的分量每个环只报一次测试 test_new_import_cycle_is_reported_once、test_strongly_connected_components。测试可达性该跑哪些测试_tests_reaching()structural_delta.py解决编辑之后该跑哪些测试的问题以被编辑文件的所有符号不只是骨架/签名变化的因为重新解析的文件里每个符号的行为都可能改变为源沿反向调用图做多源 BFS一跳一次CYPHER_DELTA_CALLERS_OF查询最深DELTA_REACH_MAX_DEPTH 12跳constants/duplicates.py。成本与实际可达范围成正比而不是与项目规模成正比——构建全项目反向调用图的成本高于绝大多数增量本身。对 Rust 还有专门的分类输入当 BFS 到达.rs路径时额外读取CYPHER_DELTA_RUST_MODULES#[cfg(test)]模块标记与CYPHER_DELTA_RUST_TEST_FNS测试函数跨度让测试符号分类覆盖 Rust 的 cfg 门控测试_rust_inputs。测试 test_tests_reaching_the_changed_symbols 验证了depth与through字段。cgr check把增量变成 CI 门禁cgr check --base origin/main --fail-on-found命令行入口定义在 cli.py 的 check_command核心逻辑在 structural_check.py前置假设图被认为已反映--base在该基线处索引然后编辑。changed_since()structural_check.py用git diff --name-status --no-renames --relative -z找出与基线不同的文件再用git ls-files --others --exclude-standard -z收集未跟踪文件——未跟踪文件也计入新增重命名在 git 中显示为一次删除 一次新增因此增量会把旧符号报为删除或重命名。-zNUL 分隔确保含制表符/换行符的文件名不会被 C 引号转义成不存在的路径测试 test_check_reads_paths_git_would_c_quotecgr 自身的未跟踪状态文件.cgr-*前缀如哈希缓存、目录 mtime会被排除不会混入reparsed测试 test_check_ignores_cgr_state_files。基线校验--base必须以^后缀经git rev-parse --verify验证为单个提交——HEAD~1..HEAD这类区间会让git diff比较两个端点而静默漏掉当前编辑因此被拒绝错误消息CHECK_BASE_NOT_A_COMMIT以-开头的值会被 git 读成 diff 选项如--cached比较的是暂存区而非修订号同样被拒测试 test_check_rejects_a_dash_prefixed_base、test_check_rejects_a_base_that_is_not_one_commit。作用域一致性indexed_scope()structural_check.py读取上次索引运行盖印的排除/取消忽略集合必须与--project归属匹配——用错误项目的 scope 重摄取会把索引故意排除的文件放回图或丢掉索引刻意保留的文件测试 test_check_uses_the_scope_the_graph_was_indexed_under、test_check_keeps_excluded_files_out_of_the_graph 等一组 scope 归属测试。门禁语义--fail-on-found时只要增量报告了悬空调用者、too_manyarity 发现、新重复或新导入环命令即以退出码 1 结束由 has_findings() 判定possibly_missing与unknown不触发。未索引即拒绝CHECK_NOT_INDEXED明确提示Project {project} is not indexed; run cgr start --update-graph at the base ref first——作用域重摄取只能补全图不能代替首次索引。幂等性check 把图更新到工作树状态因此对未变更的工作树第二次运行将报告为空增量已被应用要再次测量同一编辑需在基线重建图或先在基线索引再编辑见 structural_check.py 模块 docstring。与所有写图的命令一样运行时应无并发的 MCP 服务器写入。CLI 还提供--repo-path默认MCP_DEFAULT_DIRECTORY与--project选项。测试 test_check_reports_the_delta_since_a_git_ref 端到端验证了 check 的完整流程重命名一个函数、删除一个文件、新增一个文件然后断言dangling_callers、symbols.removed与symbols.added全部正确。开销与基准delta 有多贵observe()的计时口径明确reingest_ms只覆盖重摄取内部工作delta_ms是observe在重摄取之上增加的全部开销——两次快照读取加上 diff。基准 benchmarks/bench_reingest.py 专门测量这一项uv run python -m benchmarks.bench_reingest . --file codebase_rag/graph_updater.py该基准先对语料做一次全量索引然后反复对单个文件做中性编辑交替追加/移除尾注释改变字节与哈希但不改变 AST分别记录reingest的 p50/p95、observe的delta_p50_ms/delta_p95_ms以及整树update_repository路径的 p50 作为对照。测试 test_delta_overhead_is_small 把硬性约束固化了下来reingest_ms 0且delta_ms 200毫秒——结构增量必须便宜到可以伴随每一次写入工具返回。结论与最佳实践结构增量把图变成了 Agent 编辑循环中的一等公民写时反馈让write_file/surgical_replace_code/structural_replace的每次调用都返回结构性后果Agent 可以据此自我纠错——在返回给用户前就发现悬空调用者与实参过多CI 门禁cgr check --base origin/main --fail-on-found在 pre-commit 或 CI 中拦截删了符号却留下调用点粘贴重复代码引入新导入环这三类最常见的结构性退化测试选择tests_reaching按调用图给出最短可达距离与途经符号为编辑后该跑哪些测试提供图级依据而不是靠文件名猜测。核心实现集中在 codebase_rag/structural_delta.py计算与 codebase_rag/structural_check.pycgr check固定查询在 codebase_rag/cypher_queries.py语义约束由 codebase_rag/tests/test_structural_delta.py 的 40 余个测试逐条锁定——任何对重命名配对、arity 判定、门禁触发条件的改动都会在这里被精确地验证。【免费下载链接】code-graph-ragThe ultimate RAG for your monorepo. Query, understand, and edit multi-language codebases with the power of AI and knowledge graphs项目地址: https://gitcode.com/GitHub_Trending/co/code-graph-rag创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考