CodeGraph 生成文件检测:从路径约定到内容 Banner 的双信号设计(CG-5)
CodeGraph 生成文件检测从路径约定到内容 Banner 的双信号设计CG-5【免费下载链接】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 这类预构建代码知识图谱中这个文件是不是工具生成的直接决定了符号消歧、文件排序和 explore 预算分配的准确性。本文围绕设计文档 generated-file-detection.md 展开解释为什么仅靠文件命名约定.pb.go、.g.dart等漏掉了 Go 生态中最主流的生成文件形态CodeGraph 如何把内容 Banner 检测在索引期一次性判定并持久化到files.generated字段schema v9以及如何通过有界查询、部分索引和精度优先的标记表把索引期开销压到不可测量。读完后你将掌握一套可复用的路径信号 内容信号 索引期持久化检测架构以及其背后的精度/召回权衡和迁移策略。一、问题路径约定在 Go 世界里是盲的CodeGraph 最初的生成文件判定函数isGeneratedFile只认路径。它匹配的是basename.tool.ext这一类命名约定——.pb.go、_grpc.pb.go、.g.dart、_pb2.py等绝大多数代码生成工具的输出确实长这样这在对 cosmos-sdk 的审计中是够用的。但它对 Go 不够。因为Go 的生成文件约定是内容标记而不是文件名标记// Code generated by tool. DO NOT EDIT.这条 Banner 由go generate规范化被 gofmt、golangci-lint 和 GitHub linguist 遵循protoc-gen-go、mockgen、sqlc、ent、wire、stringer 以及各类内部生成器都会原样输出它。issue #1500 正是这个场景一个 Go 单体仓库里FKIT 生成的 CRUD 代码放在普通命名的文件payroll.go中与手写的 workflow use-case 并排放在同一目录。路径上没有任何线索能把它暴露出来于是整个代码库里的生成文件降权对它完全失效。缺口有多大client-go 实测设计文档在kubernetes/client-go的浅克隆上做了测量2,453 个 Go 文件信号被标记文件数基准真值grep 前 60 行内的标准 Banner2,001路径约定isGeneratedFile0内容 BannerhasGeneratedHeader2,001—— 0 误报、0 漏报也就是说该仓库 82% 的代码都是普通文件名的生成代码而路径检查一个都没看到。这不是长尾情况是主流情况。这组数据在测试文件 generated-detection.test.ts 的头部注释中被原样保留作为后续修改标记表时的回归契约。二、设计索引期判定DB 中读取核心原则一句话概括在索引期决定存到文件记录上读取时从 DB 拿。explore 等请求路径永远不应该为每次查询去读文件头。实现分布在四个层面均见源码isGeneratedFile(path)— 保持不变。纯路径、纯函数、同步、零开销可以放心在排序比较器里调用服务于没有数据库句柄的调用方。完整的路径模式表见 generated-detection.ts 中的GENERATED_PATTERNS覆盖 Goprotobuf/gRPC/pulsar/mockgen、TypeScript/JavaScriptApollo、Prisma、Hasura、ts-proto 等包括.min.mjs压缩包、Python_pb2.py、C、C#、Java、Swift、Dartbuild_runner/freezed/json_serializable、Rust 等后缀约定。hasGeneratedHeader(content)— 内容信号。判定文件头部是否携带可识别的生成 Banner见下文算法细节。detectGeneratedFile(path, content)— 两者取并集。这就是索引器持久化的那个值在 extraction/index.ts 的文件存储路径上被调用每次文件变更重索引时重新判定Banner 被编辑加入或移除都会在下次 sync 中反映且在文件未变更则早退之后计算所以未触碰的文件不付出任何代价。持久化与查询。schema.sql 的files表新增generated INTEGER NOT NULL DEFAULT 0schema v9并配一个部分索引-- idx_files_generated is PARTIAL: the generated set is a small minority of any -- repo, so a lookup that intersects a bounded candidate list with it stays -- proportional to the generated files, not to the repo. CREATE INDEX IF NOT EXISTS idx_files_generated ON files(path) WHERE generated 1;部分索引的意义在于查询代价正比于生成文件这个少数集而不是整个仓库。对外暴露的查询 API 是QueryBuilder.generatedPredicateFor(paths)见 queries.ts和CodeGraph.generatedFilePredicate(paths)index.ts先做一次有界探查之后每次比较都是 O(1)且结果与路径检查取并集。实际消费方遍布所有排序路径——MCP 工具层的explore结果排序tools.ts、CLI 查询codegraph.ts、上下文格式化context/index.ts、主导文件/路由文件选取getDominantFile/getTopRouteFile/getRoutingManifest等。为什么用有界查询而不是缓存一个集合每个消费方手里本来就持有一个很短的候选列表——一个排序后的文件组、一页 FTS 结果、一个LIMIT 20的聚合。把这个列表与部分索引相交不需要物化整个仓库的生成文件集合更重要的是不需要任何缓存去失效一次排序调用不可能吐出上一次 sync 早已推翻的判定。替代方案惰性物化一个全部生成路径的Set则必须在每次文件写入时失效而且会在只读查询池的 worker 上产生陈旧数据——换来的收益不过是一条亚毫秒查询的时间。queries.ts 中getGeneratedPathsAmong的注释把这个权衡写得很直白分块SELECT path FROM files WHERE generated 1 AND path IN (...)只针对候选列表做部分索引探查。三、内容检测算法三重围栏精度优先误报会静默地在每一条排序路径里把手写代码降级所以标记表是精度优先的扫描也被三重围栏约束住实现见 generated-detection.ts围栏 1只看头部窗口HEADER_SCAN_CHARS 8192字符 /HEADER_SCAN_LINES 60行。这个宽度对build tag Apache-2.0 许可证前言压在 Banner 上方是宽裕的又紧到足以保证生成器自己的源码——它把 Banner 作为字符串常量放在函数体里——不会被误标记。测试用例明确验证了这一点// 80 行填充之后才出现的 Banner —— 不算 Banner expect(hasGeneratedHeader(${filler}\n// Code generated by foo. DO NOT EDIT.\n...)).toBe(false); // 20 行填充之后的 Banner —— 在窗口内能抓到 expect(hasGeneratedHeader(${shortFiller}\n// Code generated by foo. DO NOT EDIT.\n...)).toBe(true);围栏 2标记必须出现在注释行上Banner 必须位于带注释引导符//、#、--、!--、%、;、、!、(*、{-、、、begin、#等的行上或者位于一个已打开的块注释/* */、!-- --、、、begin、# #内部。模块用一个小型状态机在窗口内追踪块注释的打开/关闭const COMMENT_LEADER /^\s*(?:\/\/|\/\*|\*\/?|#|--|!--|%|;||!|\(\*|\{-|||begin|#|rem\b|rem\b)/i; const BLOCK_DELIMS: ReadonlyArray{ open: string; close: string } [ { open: /*, close: */ }, { open: !--, close: -- }, { open: , close: }, { open: , close: }, { open: begin, close: end }, { open: #, close: # }, ];生成器总是把 Banner 作为注释输出要求这一点就排除了仅仅包含这些词的标识符和字符串字面量——测试里const banner Code generated by tool. DO NOT EDIT.;这种裸语句断言为false。围栏 3标记本身足够严格标记表GENERATED_CONTENT_PATTERNS中每条都有明确的为什么需要这个限定词例如Go 标准 Banner/\bcode generated\b.{0,200}?\bdo not edit\b/i—— 对应// Code generated by tool. DO NOT EDIT.automatically generated单独出现不算——它是散文该表在运行时自动生成必须带by/from/with且后跟do not edit/modify/change才算 BannerDO NOT EDIT单独出现不算——它只是风格指令与生成声明配对时才算generated标记JS/TS 生态的约定Relay、GraphQL codegen、protobuf-es/Buf带守卫防止foogenerated、generated误匹配generated by X by running …双 by 从句Wrangler 等 CLI 工具的形状Generated by Wrangler by running wrangler types。单纯的 generated by 是普通散文不足以判定必须同时点名工具并给出复现指令——这排除了 报告由运行夜间任务生成 这类单 by 从句的散文.NET 的Roslyn、WinForms 设计器、T4 模板的输出。每个标记在 generated-detection.test.ts 中都有对应的真实生成器输出用例22 个正例覆盖 Go/protoc Java/protoc Python/C#/Relay/Thrift/OpenAPI/FlatBuffers/bindgen/ANTLR/Wrangler/YAML/SQL/HTML 等以及 9 个精度反例生成器自己的源码、散文、邮箱地址含generated、无生成声明的 DO NOT EDIT 等。测试注释说明每条正例是生成器真实输出的原文不是转述——如果某条正则被收窄导致它失效的那个用例会按名字失败。模块不标记自己一个精巧的自洽约束该模块内部引用的 Banner 字符串字面量刻意放在 8,192 字符头部窗口之下因此检测器不会把generated-detection.ts自己分类为生成文件。generated-detection.test.ts 直接读入该源文件并断言hasGeneratedHeader(self) false——如果有人把模式表往上挪测试失败而不是仓库静默降级自己的文件。四、迁移DDL only天然无法回填v9 迁移见 migrations.ts只做 DDL加列 建部分索引任何规模的库上都是瞬时完成。它没有也无法回填原因是结构性的flag 派生自文件内容而files表存的是内容哈希不是字节迁移阶段根本看不到文件内容。后果与兜底设计是配套的迁移后的存量行保持generated 0直到下次全量索引重新提取由于所有读取方都把 flag 与路径检查取并集一个未回填的数据库保留的恰好是 #1500 之前的行为而不是回退——新信号只加路径信号从不覆盖它sync会随着文件逐个变更把它逐步修复这正是 CHANGELOG 中写明需要重新索引才能启用新检测的原因。迁移还处理了一个幂等细节SQLite 的ALTER TABLE没有IF NOT EXISTS所以先查PRAGMA table_info(files)再决定加不加列——对从当前 schema.sql 直接建库的数据库列已存在重跑迁移时不会报错。五、成本验收标准是不可测量的索引期回归设计文档给出的验收门槛是索引期没有可测量的成本回归实现上靠两层廉价预过滤。所有标记都含词干 generat所以一条未锚定的/generat/i扫描在任何分行发生之前就拒绝掉几乎所有手写文件。而String.prototype.slice在长字符串上产生的是 V8 的切片视图而非拷贝快路径零分配。实测数据。微基准detectGeneratedFile扫整个语料5 轮client-go 上4.6 µs/文件2,453 文件、14.2 MB、82% 生成率——这是最坏情况因为预过滤通过、完整行扫描真正跑起来了本仓库src上 7.3 µs/文件。端到端codegraph initclient-gon3 交替组当前构建 vs. 同一构建但内容扫描被 stub 掉组三次运行s中位数含内容检测5.66, 5.73, 5.895.73纯路径基线5.52, 5.76, 5.885.76两组在运行间互相交叉——差异在逐次运行噪声之内。六、这个任务刻意没有改什么生成文件状态仍然是同分时的稳定 tiebreak位置不变explore的文件排序、findSymbolMatches、findAllSymbols、搜索结果格式化、getDominantFile/getTopRouteFile/getRoutingManifest、上下文格式化。一个原始分更高的生成文件依然排在手写文件前面——它只是相关度提示不是硬过滤生成节点仍在图中、仍然可达只在存在同名的真实实现时排到最后。把生成状态升级为强负向信号是后续任务 CG-10 的范围本任务通过让信号正确且可用为它解除了阻塞。端到端验证用一个双文件 Go 包完成对应测试 explore-allocation-1500.test.ts 与 generated-flag-index.test.ts生成文件payroll.go和手写文件workflow.go都定义了ProcessPayroll——flag 置位时手写文件排第一在同一索引中清掉 flag即 #1500 之前行为则生成文件排第一。小结这套方案给出的可复用经验有三条判定下沉到索引期。内容类信号需要读文件在解析阶段算一次并持久化查询路径只读 DB代价正比于数据中真实的少数集部分索引有界查询优于缓存集合。候选列表 部分索引探查换来零缓存失效逻辑和只读 worker 上的强一致性精度优先的启发式必须被测试钉死。每条正则对应一条生成器真实输出的正例和一组更松的表会误报的反例外加模块不标记自己的自洽测试让后续任何收窄/放宽都会以具名失败的方式出现。相关延伸阅读预算分配的姊妹文档 explore-budget-allocation.mdCG-4 的计量工具本文档是其前置条件。【免费下载链接】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),仅供参考