graphify 语言抽取器迁移指南:把 extract.py 按语言逐条拆分到 extractors 包的完整 Playbook
【免费下载链接】graphifyTurn any codebase, with its docs, SQL schemas, configs, and PDFs, into a queryable knowledge graph. A /graphify skill for Claude Code, Cursor, Codex, and Gemini CLI: local deterministic AST parsing, every edge explained, no vector store.项目地址: https://gitcode.com/GitHub_Trending/graph/graphify
本文围绕 MIGRATION.md 展开:这是 graphify 仓库中把巨型单文件graphify/extract.py(约 7300 行)按"每语言一个 PR"的节奏、逐条搬运到graphify/extractors/包的官方操作手册。读完后,你可以独立完成一次语言抽取器的字节级迁移:知道哪些语言已迁完、必须遵守的五条不可妥协不变量、辅助函数如何分类归属、迁移前后的检查命令,以及为什么"一个测试都不改"本身就是行为保真的证明。
一、背景:为什么要把语言抽取器从 extract.py 拆出去
graphify 的核心能力是本地确定性 AST 解析——用 tree-sitter 把代码库、文档、SQL schema、配置和 PDF 变成可查询的知识图谱。所有语言的抽取入口都曾经集中在 graphify/extract.py 这一个文件里,每个语言对应一个extract_<lang>(path: Path) -> dict函数,输出统一的{"nodes": [...], "edges": [...]}结构。随着支持语言增多,该文件膨胀到 7000+ 行,任何语言的改动都可能与其他语言的改动在同一个巨型文件里产生合并冲突。
仓库为此启动了拆分(上游 issue #1212),目标结构已经落地:
- graphify/extractors/ 包,一个语言一个模块,例如 terraform.py、go.py、rust.py、sql.py、powershell.py 等;
- graphify/extractors/base.py 存放多语言共享的辅助函数(如
_make_id、_file_stem、_read_text、内置全局名过滤表_LANGUAGE_BUILTIN_GLOBALS); graphify/extract.py退化为门面(facade):它仍然 re-export 所有已迁移的名字,保证__main__.py、watch.py、pg_introspect.py、测试等既有导入方一行代码都不用改;- graphify/extractors/init.py 提供
LANGUAGE_EXTRACTORS注册表种子——从源码结构看,dispatch 目前仍走graphify.extract,接线到注册表是后续单独一步,MIGRATION.md 也明确禁止在迁移 PR 里提前做这件事。
MIGRATION.md 特意写成"AI agent 可以在单次会话内执行"的 Playbook 形式,这一点决定了它的每一步都给出可直接运行的命令。
二、迁移状态:谁迁完了,谁还没迁
MIGRATION.md 内嵌一张状态表,是当前拆分进度的权威记录:
| module | migrated |
|---|---|
| blade | yes |
| zig | yes |
| elixir | yes |
| razor | yes |
| dart | yes |
| rust | yes |
| go | yes |
| powershell (ps1 + psd1 manifest) | yes |
| fortran | yes |
| sql | yes |
| dm (dm/dmm/dmi/dmf) | yes |
| bash | yes |
| apex | yes |
| terraform | yes |
| sln | yes |
| pascal_forms (dfm + lfm) | yes |
| json_config | yes |
| (config-driven core: python, js, java, c, cpp, csharp, kotlin, scala, php, lua, swift, groovy, vue, svelte, astro, xaml, groovy) | no — shared _extract_generic core, move as one batch |
| (other bespoke: julia, verilog, markdown, objc, csproj, slnx, lazarus_package, pascal) | no |
(注:上表按 MIGRATION.md 原文继承;仓库演进后部分"bespoke"条目如 julia、verilog、markdown、objc、pascal 已在 extractors/init.py 的LANGUAGE_EXTRACTORS注册表中出现,实际状态应以该注册表与最新提交为准。)
文档中特别强调一条选择规则:不要选 config-driven 语言。python、js、java、c、cpp、csharp、kotlin、scala、php、lua、swift、groovy 这些语言的extract_<lang>只是几行_extract_generic(path, LanguageConfig(...))包装,真正的逻辑在共享的_extract_generic核心(约 1300 行)里,且该核心在 extract.py 中仍有 20 多处调用点。逐个搬这些语言毫无意义——核心必须作为一次协调好的批次整体迁移,所以 Playbook 建议:"Pick a bespoke extractor"(挑一个自带完整函数体的语言)。
三、五条不变量(non-negotiable)
这是整篇 Playbook 的核心纪律,任何一条被破坏都意味着迁移失败:
- 只做逐字搬运(Verbatim moves only)。不许改名、不许改 docstring、不许重排格式、不许加类型标注、不许顺手"改进"。验证方法:剪下代码块前先存一份临时文件,贴到目标模块后确认两个块字节一致。
- 一个 PR 只迁一个语言。小 diff 让评审变得平凡,也避免与其他在途迁移在
extract.py同一区域产生冲突。 - 门面 re-export 是强制的。
extract.py必须继续在标记好的迁移块里导出所有已迁移的名字,形如from graphify.extractors.<mod> import extract_<lang> # noqa: F401,并保持字母序。既有导入方(__main__.py、watch.py、pg_introspect.py、tests)一行都不能改。在 extract.py 中可以验证这一块的现状:第 28 行起有# --- migrated to graphify/extractors/ (see graphify/extractors/MIGRATION.md) ---标记,其下是按字母序排列的 re-export 列表,包括从base导入的_LANGUAGE_BUILTIN_GLOBALS、_file_stem、_make_id、_read_text等共享辅助名。 - 包内永远不许反向导入
graphify.extract。依赖方向严格是extract.py → extractors/。base.py 第 1 行就写着一行注释防线:# DO NOT import from graphify.extract here — direction is extract.py → extractors/ only.。如果你需要用到只存在于extract.py的辅助函数,走下面的"辅助函数分类"流程把它也迁走。 - 测试零改动——唯一例外是
tests/test_extractors_registry.py。原有语言测试原封不动地通过,本身就是行为保真的证明:你既没有改函数体(字节一致),也没有改测试(测试仍在测同一个对象),那么行为没变。
四、辅助函数分类:搬函数前必须做的一次 grep
被迁移的extract_<lang>会引用许多_name开头的前置辅助函数。对每一个定义在函数外部的名字,Playbook 给出确定性的判定算法:
- 先完成你候选函数体的剪切(此时
extract.py里只剩其他语言对这些名字的使用); - 执行
grep -c '_name' graphify/extract.py; - 剩余使用数> 0→ 判定为shared:把它迁到 extractors/base.py,并在
extract.py的门面 re-import 中加上它(如现状中的from graphify.extractors.base import (_LANGUAGE_BUILTIN_GLOBALS, _file_stem, _make_id, _read_text),见 extract.py); - 剩余使用数= 0→ 判定为private:直接搬进你的语言模块内部。
两条补充规则:
- 函数体内定义的闭包、常量、
import语句随函数体免费迁移,留在原样即可; - 只给"粘贴后的代码在模块作用域引用、但函数体内部没有满足"的名字添加模块头部 import,并且逐一验证每个头部 import 都被实际使用——这正是 terraform.py 头部只导入
Path和_make_id的原因:它的闭包_read、_label_text、_add_node都在函数体内自给自足。
分类错误的典型症状会在验证阶段暴露:ImportError或NameError,此时 Playbook 的处方是回到本节重新分类,而不是临时打补丁。
五、Pre-flight:动刀前的三项检查
查上游冲突:看是否有在途 PR/issue 提到目标语言,并检查近三个月的改动热度:
git log --oneline --since="3 months ago" upstream/<default> | grep -i <lang>热度高 → 换一个语言,避免迁移到一半源文件被别人改了。
确认你的抽取器是 bespoke 的:它的
extract_<lang>必须是一个完整函数体,而不是 5 行的_extract_generic(path, LanguageConfig(...))包装(对应第二节的选择规则)。检查测试覆盖:在
tests/里 greptest_<lang>。如果该语言没有行为测试,那么步骤 3 中的字节一致性检查就是保真的全部证明——此时 PR 描述中必须附git diff --color-moved的输出作为证据。
六、迁移六步法
Playbook 的主体是六个步骤,每一步都有明确的产出物和失败处理:
Step 1 — 先写失败测试。往 tests/test_extractors_registry.py 追加一个失败测试,覆盖三重断言:模块可导入 + 门面同一性(facade identity)+ 注册表同一性(registry identity),直接复制现有的test_<lang>_migrated做模板。该测试文件的 docstring 解释了为什么要做"同一性"而非"等价性"检查:graphify.extract必须 re-export同一个函数对象——如果门面持有一份过期拷贝或发生了影子导入,对象会"静默分叉"。仓库现状中还有一个泛化版守卫test_every_registry_extractor_is_reexported_from_facade,它遍历整个LANGUAGE_EXTRACTORS注册表,任何遗漏门面 re-export 或 re-export 错对象的未来迁移都会在这里响亮地失败。
Step 2 — 定位函数跨度。grep -n 'def extract_<lang>' graphify/extract.py,跨度一直延伸到下一个顶层语句(^def或^_CONST)之前。文档特别提醒了一个真实踩过的坑:函数后面的顶层常量可能属于下一个函数——例如_CONFIG_JSON_*常量位于当年extract_razor曾经驻留的位置之后,但它们从来不是 razor 的,属于 json_config。
Step 3 — 存临时文件、建新模块、验证字节一致。把跨度存到临时文件;创建graphify/extractors/<lang>.py,结构固定为:模块 docstring("""<Lang> extractor. Moved verbatim from graphify/extract.py.""",terraform.py 第 1 行即是标准样板)、from __future__ import annotations、最小化的标准库 import、base 导入,然后粘贴函数体,并与临时文件做字节一致性比对。
Step 4 — 从 extract.py 删除并接线。删掉该跨度,使现在相邻的两个顶层定义之间恰好留两个空行;在标记好的迁移块里按字母序添加门面 re-import;在 extractors/init.py 的LANGUAGE_EXTRACTORS字典中按字母序加注册表条目;更新 MIGRATION.md 里的 Status 表。
Step 5 — 全量跑测试。uv run pytest -q要求 0 失败,且除注册表测试外没有任何测试文件被修改。若出现ImportError/NameError,说明有辅助函数被错误分类——回到第四节。
Step 6 — 单一提交。commit message 固定格式:
refactor(extract): move extract_<lang> to extractors/<lang>.py (verbatim)七、明确不做的事(What NOT to do)
Playbook 用三条负面清单划出机制层改造的边界:
- 不要重接 dispatch、不要加类、不要加懒加载 import——机制层改造属于"后续单独协商"的事项(见 #1212 讨论),不属于迁移 PR 的范围;
- 不要"顺手"在一个 PR 里迁两个语言;
- 不要碰
__main__.py——门面 re-export 的设计目的就是让graphify/extract.py对__main__.py等调用方保持 API 完全不变。
八、仓库现状佐证:拆分架构已经跑通
结合当前仓库代码,可以验证这套 Playbook 的落地形态与文档描述完全一致:
- 门面块:extract.py 第 28 行的迁移标记块内按字母序 re-export 了 apex、bash、blade、csharp、dart、dm、elixir、fortran、go、json_config、commonlisp、markdown、ocaml、pascal_forms、powershell、razor、rust、sln、sql、terraform、verilog、zig 等模块的名字,全部带
# noqa: F401; - 注册表:extractors/init.py 中
LANGUAGE_EXTRACTORS: dict[str, Callable[[Path], dict]]把 27 个键(含delphi_form、lazarus_form、powershell_manifest这类非语言名 key)映射到各自模块的函数对象,docstring 明确写着 "wiring dispatch through it is a later, separate step",与 MIGRATION.md 的边界声明一致; - 同一性守卫测试:test_extractors_registry.py 中的
test_terraform_migrated是 #1721 的具体锚点——断言facade.extract_terraform is extract_terraform且LANGUAGE_EXTRACTORS["terraform"] is extract_terraform,is比较的正是对象同一性而非相等性; - 共享 base:base.py 只依赖
graphify.ids的标准库与内部模块,不 importgraphify.extract,方向约束得到物理落实;其中的_file_stemdocstring 还记录了迁移后仍持续演进的共享逻辑(如 #502 路径编码、#1504 同名文件区分)。
九、这套 Playbook 背后的工程思想
MIGRATION.md 的价值不仅在于步骤本身,还在于它把"如何安全重构一个 7000 行单文件"沉淀成了可机械执行的纪律,其思想值得单独提炼:
- 用"不动测试"当回归证明。行为测试没改、函数体字节没改,那么通过即保真——这比新增测试更接近对"零行为变化"的形式化论证;没有行为测试的语言,则用
git diff --color-moved证据补位。 - 门面模式解耦迁移与消费方。
graphify.extract的对外 API 在整个迁移周期内保持冻结,__main__.py、watch.py、MCP 相关入口和测试零改动,迁移只发生在一个方向(extract.py → extractors/)上。 - 用 grep 计数做依赖分类。"移动后剩余使用数 > 0 即 shared"是一个无需判断力的客观判据,把最容易出错的"这个辅助函数归谁"问题变成了可重复执行的命令。
- 用负面清单防止范围蔓延。"不重接 dispatch、不加类、不动
__main__.py、一次只迁一个"——每条都在防止 PR 从纯搬运膨胀成架构改造,这是让"每次评审都平凡"得以成立的最后防线。
对贡献者而言,按 MIGRATION.md 执行一次完整迁移的入口动作只有三步:在状态表里挑一个 bespoke 语言、按 Pre-flight 三条检查、然后照六步法推进到uv run pytest -q全绿、以固定格式的单一 commit 收尾。
参考文件
- graphify/extractors/MIGRATION.md — 迁移 Playbook 原文
- graphify/extract.py — 门面 re-export 迁移块
- graphify/extractors/init.py —
LANGUAGE_EXTRACTORS注册表 - graphify/extractors/base.py — 共享辅助函数
- graphify/extractors/terraform.py — 已迁移语言模块样板
- tests/test_extractors_registry.py — 门面/注册表同一性守卫
【免费下载链接】graphifyTurn any codebase, with its docs, SQL schemas, configs, and PDFs, into a queryable knowledge graph. A /graphify skill for Claude Code, Cursor, Codex, and Gemini CLI: local deterministic AST parsing, every edge explained, no vector store.项目地址: https://gitcode.com/GitHub_Trending/graph/graphify
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考