CodeGraph Ruby 内核移植:语法升级、bug-for-bug 保真与零差异门禁的全记录
CodeGraph 的提取内核正在把原本运行在 wasm 上的 TreeSitter 逐语言迁移到原生 Rust walker,docs/design/ruby-kernel-port-checklist.md 就是 Ruby 这一站(代号 R7b)的完整移植检查单:从语法(grammar)单独升级的选型与 SHA 验证,到每一个 TS 侧钩子怪癖的 bug-for-bug 复现清单,再到三重门禁(standalone bump gate、torture fixture、全仓 parity 扫描)的通过标准。这篇指南以该检查单为主体骨架展开,结合仓库中已落地的 walker codegraph-kernel/src/ruby.rs、TS 侧配置 src/extraction/languages/ruby.ts 与测试 tests/kernel-ruby-parity.test.ts,讲清"一个 bug-for-bug 移植到底要保真到什么粒度"。读完后你能掌握:如何为一次内核化改造做语法升级的独立隔离验证、如何用探针把新旧语法的形态差异分类到 hunk 级、以及如何用 torture fixture + 全仓 dump diff 把"行为完全一致"钉成可回归的测试不变量。
移植的最终状态:全部门禁通过
检查单头部即给出结论:Status: PORT COMPLETE (2026-07-20) —— walker 落地于 codegraph-kernel/src/ruby.rs,所有门禁通过:
- 语法单独升级门禁通过:sinatra/jekyll 的 dump 在旧/新语法下字节级一致,rails 恰好只有那一处被分类的
&.!=hunk; - parity 扫描 0-diff:sinatra 147/147、jekyll 164/164、rails 3452/3452,0 deferrals(共 3,763 个文件);
- full-init dump 门禁字节级一致 ×3;
kernel-ruby-parity测试套件通过;DEFAULT_ROUTED += ruby。
仓库中可以逐条核对这些结论:src/extraction/kernel/index.ts 中 ruby 已进入 DEFAULT_ROUTED 集合,注释记录了 R7b (2026-07-20) 的 3,763 文件 byte-parity 与 0 deferrals;codegraph-kernel/Cargo.toml 中精确 pin 了 tree-sitter-ruby = "=0.23.1"。
调研基线(survey basis)是:每一个 .rb/.rake 文件会触及的 TS 侧分支,带 f1ca991(调研时 HEAD)的 file:line 锚点。所有关于语法形态的论断都不是假设,而是**对旧 tree-sitter-wasms 构建和新 v0.23.1 构建各做一次探针(probe)**后得出的。检查单明确要求与 rust-kernel-migration-plan.md(§0a 配方、§5 门禁)及两份格式先例(rust-lang-kernel-port-checklist.md、ccpp-kernel-port-checklist.md)配套阅读。阻塞性发现:无 —— 语法升级在所有提取器相关构造上是形态中立的(两个已分类的 delta,一个无影响、一个精度为正),三个门禁仓库上两臂错误率均为 0.00%,且 codegraph-kernel/src/python.rs 是一个结构非常接近的 walker 骨架。
阶段一:语法升级(Grammar prep)—— 必须最先做、独立合入
旧版语法从哪里来
Ruby 不在 VENDORED_WASM_LANGS(src/extraction/grammars.ts 第 291-304 行)里 —— 生产环境加载的是 node_modules/tree-sitter-wasms/out/tree-sitter-ruby.wasm,由 tree-sitter-ruby ^0.20.1 构建(tree-sitter-wasms 0.1.13 的 devDependency,2024 年 2 月时代的产物)。升级目标:
- Crate:
tree-sitter-ruby = "=0.23.1"(crates.io 最高版本,2024-11-11 发布)= git tagv0.23.1= commit71bd32fb7607035768799732addba884a37a6210(同时是当时的 master)。crate tarball 与 tag 做了 sha256 交叉验证,ruby 带外部 scanner,所以两个生成物都验:src/parser.c的 sha256 为4ce468358b6f4e25a35c8cf6bc0eaf60665bc22d602f8c939323c2347255cd15src/scanner.c的 sha256 为e7a6196d6e78bf4c6728502e924c867dee5d851c6253e43fcdb8ba169009bc58
ABI 注意(与 rust 先例不同)
v0.23.1 check-in 的 parser.c 声明 LANGUAGE_VERSION 14 —— 该 tag 早于 ABI-15 生成器,所以这是一次 grammar 内容升级,ABI 保持 14(web-tree-sitter 与 native tree-sitter 0.25 都接受,最低兼容 13)。因此不要期望 kernel-grammar-parity 出现 ABI 变化,但要断言 same-revision。这一点在落地仓库的 src/extraction/grammars.ts 注释里可以原样看到:R7b (Ruby kernel port prep): tree-sitter-ruby v0.23.1 (71bd32f) … Content bump only — the tag's checked-in parser.c is still ABI 14 (predates the ABI-15 generator)。
构建方式:只从 check-in 的 parser.c 构建
永远不要跑 tree-sitter generate,直接使用该 tag 自带的生成物:
git clone https://github.com/tree-sitter/tree-sitter-ruby && cd tree-sitter-ruby
git checkout v0.23.1
npx -y tree-sitter-cli@0.25.10 build --wasm -o tree-sitter-ruby.wasm .
调研产物 sha256 为 4cb5a4b12870876ca864c1e92fe1f5cd47036b2adc083e9306488af88867dbb4(2,106,097 字节)。仓库中该 wasm 已 vendor 至 src/extraction/wasm/tree-sitter-ruby.wasm。
暂存计划(Grammar-bump PR:先于任何 walker 存在时)
- 把 wasm vendor 到
src/extraction/wasm/tree-sitter-ruby.wasm; - 在
VENDORED_WASM_LANGS(grammars.ts:291)中加入'ruby',附 R7b 注释(沿用 rust 模式:tag + sha-matched 说明); - 在 codegraph-kernel/Cargo.toml 的精确-pin 注释块下 pin
tree-sitter-ruby = "=0.23.1"(=精确版本,与 c/cpp/rust 一致 —— crate 与 wasm 必须一起动,否则 kernel-grammar-parity 会失败); - walker 落地时在 langs.rs 注册内核符号
tree_sitter_ruby::LANGUAGE; - 全量套件绿 + 独立 bump 门禁(见后文 §Gates)通过后才开始 walker 工作。
形态 delta(OLD→NEW):完整分类,共两处
__END__数据拖尾 —— 无影响(INERT)。 OLD 有独立的__END__匿名 token + 从换行之后开始的uninterpreted节点;NEW 删掉了__END__的 kind 表条目(353 → 351 kinds,kind id 重编号),uninterpreted紧跟__END__之后开始(把前导\n也含进文本)。提取器没有任何分支触碰uninterpreted(不在 ruby 的任何类型列表里,递归进也什么都匹配不到;文件的 endLine 来自source.split('\n')),所以不产生任何输出变化。字段表(32 个 field)完全一致。- Safe-nav 操作符方法调用 —— 行为改变,精度为正。
recv&.!= arg(rails 的activerecord/lib/active_record/relation/where_clause.rb:62,在 3,763 个真实文件中唯一的命中):OLD 误解析为assignment(recv&.!方法!,然后= arg)→ wasm 输出recv.!的 calls 引用;NEW 正确解析为call,method 是operator类型的!=→ 输出recv.!=的 calls 引用,参数按 arguments 走。这是 bump 时点仅 wasm 路径的一次 churn(两臂此后结论一致),在 bump 门禁的 dump diff 中按 rust 的"小型精度为正的边缘 churn"方式分类并接受。
其余探针全部在 OLD/NEW 间字节级一致:完整 torture 探针(modules/mixins/visibility/inline-def/calls/blocks/heredocs/hooks —— 848 行 CST dump 相同)、现代语法探针(endless methods def f(x) = …、.../&/*/** 转发、case/in 模式匹配、右向赋值、hash 简写、%-literals、操作符方法定义 —— 全部一致,含完全一致的报错行为)、CRLF 变体(两臂都无错误,形态一致,node-type 序列与 LF 相同)。
错误率与 deferral 基线
两臂、全部 ≤1MiB 的 .rb/.rake 文件:sinatra 0/147(0.00%/0.00%)、jekyll 0/164、rails 0/3452 —— 零分歧。Ruby 属于 ts/java/py/go 这一类语言:预期 ~0% deferral,默认 --max-deferral 0.1 有巨大余量;一次 ruby 扫描出现两位数 deferral 就是 walker 坏了(没有 c/cpp 那种 0.5 豁免)。
探针脚本与输出存放在调研 scratchpad(svy-ruby/):shape-probe-ruby.cjs(OLD vs NEW 的 CST dumper)、table-compare.cjs(kind/field 表 + 错误定位)、error-sweep.cjs(逐仓库 has_error + 完整 CST sexp 对比)、extract-probe.cjs(跑真实的 dist 提取器 —— 它的 extract-{dblcap,vis,req,vref,misc,reqedge}.txt dump 是本文档通篇引用的 pinned ground truth,同时充当 walker 测试的预期值),以及 probe.rb/edge.rb/setter.rb/datatrailer.rb/probe-crlf.rb 等 fixture 与 shape-{OLD,NEW}*.txt、kinds-{OLD,NEW}.txt dump。scratch 目录是一次性的 —— 丢了就从本文档重新推导。
阶段二:五个架构决策
- 没有 preParse。
rubyExtractor没有preParse钩子 —— 路由点的preParsedSource(kernel/index.ts:82)是 no-op;两臂都解析原始字节,没有需要 hoist 的东西。 - Rails 应用走 DECODED 路径,门禁仓库不走。
railsResolver(resolution/frameworks/ruby.ts 第 11 行,languages: ['ruby'],注册于 frameworks/index.ts:52)带一个extract()钩子,而 parse-worker.ts:93-99 会把任何"有适用框架extract()"的语言强制到 decoded 的extractFromSource路径。但detect()(ruby.ts:22-39)要求仓库根目录存在包含单引号'rails'的 Gemfile、config/application.rb、app/controllers/application_controller.rb或config/routes.rb—— sinatra/jekyll/rails 框架仓库本身一个都不触发(已验证),所以三个 parity 仓库走的都是 raw buffer 传输。因此:不要从 Rails 应用的性能运行中得出"raw 路径坏了"的结论,也不要从门禁仓库得出"decode 路径没被测过"的结论 —— torture fixture 套件在测试里覆盖了 decode。 - 框架提取器无需移植。 它是对原始源码的正则(ruby.ts:109-190),在任意外臂之后都在
extractFromSource(tree-sitter.ts:6736-6758)内部以相同方式运行,合并route节点 +controller#action引用。其输入契约见下文 Frameworks 一节。 - 一个 walker 模块。 建议
codegraph-kernel/src/ruby.rs(这次没有 crate-语言命名冲突),在 langs.rs 注册(LANGUAGES+grammar_for+tree_sitter_ruby::LANGUAGE);逐文件has_error()→defer:,与其他所有 walker 相同。python.rs是最接近的骨架 —— 共享形态:无花括号、def 风格、模块级assignment→ 恒为variable(无 isConst)、类内函数 → method、不是 TYPE_ANNOTATION 语言、完整 value-ref 机制、从根带 scope 栈遍历。Ruby 与它有六处分歧,后文逐一展开:(a)visitNode钩子(modules + mixins)对每个节点最先运行;(b)importTypes: ['call']吞掉每个顶层 call;(c) extractCall 里 ruby 专属分支(receiver/method 字段、.new→ instantiates、常量 receiver 引用);(d)extractBareCall(语句级标识符);(e) sibling-scan 的 getVisibility;(f) 空 idTypes +call/simple_symbol特例的 fn-ref spec。 .rb/.rake→ruby(detectLanguage,grammars.ts:103-104),无内容嗅探、无方言。无扩展名的Gemfile/Rakefile解析为unknown(lastIndexOf('.') === -1),永远不是 ruby。MAX_FILE_SIZE(1 MiB,extraction/index.ts:132)、vendor/跳过(Bundler,extraction/index.ts:168)与生成文件检测都在编排器/TS 侧,两臂共享。method_call(见下文提取器配置)在语法里不存在 —— 已探针确认:两种构建里都没有这个节点 kind。
阶段三:提取器配置与四个钩子(languages/ruby.ts,共 147 行,必须通读)
类型表(src/extraction/languages/ruby.ts 开头几行即可核对):
| 字段 | 取值 | 备注 |
|---|---|---|
| functionTypes | [method] |
|
| classTypes | [class] |
|
| methodTypes | [method, singleton_method] |
|
| interfaceTypes | [] | module 由钩子处理 |
| structTypes / enumTypes / typeAliasTypes | [] | |
| importTypes | [call] |
承重怪癖,见 visitNode 调度 |
| callTypes | [call, method_call] |
DEAD:语法里没有 method_call,为 parity 保留 |
| variableTypes | [assignment] |
|
| nameField / bodyField / paramsField | name / body / parameters |
params 未被使用 —— 没有 getSignature |
没有 enumMemberTypes、propertyTypes、fieldTypes、packageTypes。
钩子一:visitNode(ruby.ts:19-76)—— 对每个节点先于调度阶梯运行
在调度阶梯(tree-sitter.ts:943-953)之前执行,两项职责:
1. Mixins: 无 receiver 字段的 call,且其 method 字段文本是 include/extend/prepend → 对 arguments 字段(?? namedChildren.find(type==='argument_list'))中每个类型为 constant 或 scope_resolution 的 namedChild,推一个 implements unresolved 引用 {fromNodeId: nodeStack 栈顶, referenceName: 参数完整文本(Foo::Bar 原样), line/column: call 节点起点(同一调用的所有参数同行同列)} —— 然后 return true(已处理;该 call 永远到不了阶梯)。必须守住的门禁:nodeStack 非空 且 找到了 args 节点,否则 fall through 未处理。extend self → 参数类型为 self,跳过(无引用)但仍算 handled。带 receiver 的形式 Foo.include Bar 有 receiver → 钩子拒绝 → 该 call 死在 extractImport(什么都不输出)。
2. Modules: 类型为 module 且有 name 字段 → 创建一个 module 节点(name = name 字段文本 —— module A::B 就是 A::B 原样;无 docstring、无 visibility、无 extras —— ctx.createNode 不带任何 extra),push 其 id,对 body 字段的每个 namedChild 调 ctx.visitNode,pop,return true。无 name 字段 → false(fall through:children 裸递归;合法 ruby 中不可能发生)。当钩子处理了某节点,调度器会对其运行 scanFnRefSubtree(tree-sitter.ts:951)—— 仅捕获,depth>0 时在嵌套 functionTypes(method)处停下,但不在 class/module 节点停,深度 ≤12。
怪癖,经验性钉死(dist 探针 extract-dblcap.txt):钩子处理的 module 会对 fn-ref 容器"多重捕获"。 钩子内部 ctx.visitNode 走一遍,在真实作用域捕获一次;随后钩子后的 scan 用"扫描时刻的栈顶"(module 已 pop)再捕获同一批容器 —— 嵌套 module 还会复合放大(每一层钩子 return 都触发对其整棵子树的另一次 scan)。module A { module B { class C < Base { before_action :hooked … } } } 的 ground truth 是 三个 function_ref "this.hooked" 引用 —— 分别来自 class:C(内层走查)、module:A(对 B 的 scan,此刻 A 仍在栈上)、file(对 A 的 scan),严格按这个候选顺序,且全部 flush(this. 前缀候选跳过门禁;flush 去重按 (fromNodeId,name),不同作用域全部存活)。类体中不在任何 module 内的容器只捕获一次(文件级的裸 class C 不受钩子处理)。必须精确复现这种多重捕获 —— Rails(module Admin; class XController; before_action …)无处不在地命中它。Mixin 处理的 call 同样会被 scan(其 argument_list 产不出东西 —— constant/self 不是候选形态)。
落地实现里可以直接对照 codegraph-kernel/src/ruby.rs 顶部注释:它开宗明义列出了这份"故意保留的承重怪癖"清单 —— importTypes: ['call'] 漏斗、钩子 module 的多重捕获、sibling-scan visibility 三件套、brace-block 不可见、value-ref DFS 逆序 —— 与检查单一一对应。
钩子二:extractBareCall(ruby.ts:77-105)—— 只从 visitFunctionBody 调用
针对"不是 callTypes/instantiation"的节点(tree-sitter.ts:5159-5173)。节点类型必须是 identifier;父类型必须在 BLOCK_PARENTS = {body_statement, then, else, do, begin, rescue, ensure, when} 中;名字不在 SKIP = {true, false, nil, self, super, FILE, LINE, dir} 中;首字符不是 ASCII A-Z(charCodeAt(0) 在 [65,90] —— 仅 ASCII:Unicode 大写开头的标识符不会被跳过);否则返回名字 → 调度器输出一个 calls 引用 {caller = 栈顶, name, line = identifier 的 startRow+1, col = startColumn}。
已探针确认的推论:def/do_block 体内的语句级 reset → 引用;brace-block 体是 block_body —— 不在集合里 → 5.times { beep } 对 beep 什么都不输出,而 do…end 体(body_statement)会;modifier 形式(cleanup unless done?、compute rescue nil)父节点是 unless_modifier/rescue_modifier → 什么都不输出;三元分支(父 conditional)→ 无;interpolation 内裸标识符 → 无;begin/rescue(经其 then 体)/else/ensure/when(then)/while(体 do) 的语句标识符 → 引用。注意 ?/! 结尾的零参调用(done?、block_given?)解析为 call(只有 method 字段)—— 走 extractCall 而不是本钩子,因此在任何位置(包括条件中)都会得到引用。
钩子三:getVisibility(ruby.ts:106-122)—— sibling 扫描
从 def 节点沿 previousNamedSibling 链走(无上限,穿过多轮不匹配的兄弟);第一个类型为 call 且 method 字段文本为 private/protected/public 的兄弟说了算;否则 'public'。已探针确认的怪癖,全部保留:
- 一行裸的
private/protected/public解析为identifier而非call→ 不可见 —— 其后方法保持 'public'; private :greet/private def x; end是 method 为private的 call → 其后所有 def(任意距离)都变 'private',与 ruby 真实语义无关(private :sym实际是 arg-scoped);private def foo内部的 def 位于该 call 的 argument_list 中、没有前驱 named sibling → 'public';- 适用于
extractFunction、extractMethod、extractClass(private :x之后的 class 也会变 'private')与顶层 def(扫描在 program 层运行)。extractInterface/module 钩子不计算 visibility。
钩子四:extractImport(ruby.ts:123-146)—— 每个进入 importTypes 分支的 call
signature = 整个 call 的 source.substring(startIndex, endIndex).trim()(UTF-16 substring)。门禁:第一个类型为 identifier 的 namedChild(通常是 method 字段子节点;对带 receiver 的 foo.require 会先找到 receiver foo 而拒绝;对 Kernel.require "x",receiver 是 constant,find 会到达 method 标识符 require → 当作 require 处理 —— 保留此怪癖),文本必须恰为 require|require_relative,否则 null。然后:第一个 argument_list namedChild → 其第一个 string namedChild → 其第一个 string_content namedChild → moduleName = 内容文本;任何一环缺失 → null(require :sym → 无)。已探针:%q() 是 string 节点 → require %q(pct/lib) 是完整 require(import 节点 + 引用);插值路径只取第一个 string_content —— require "interp/#{x}" → moduleName interp/ → 引用 interp/ + interp/.rb(垃圾但确定 —— 保留)。返回 {moduleName, signature},无 handledRefs → 通用路径同样触发(见下文 extractImport 漏斗)。
不存在的钩子 —— walker 严禁做这些事
preParse、resolveName、recoverMangledName、isMisparsedFunction、isConst、isStatic、isExported、isAsync、getSignature、getReturnType、getReceiverType、resolveBody、classifyClassNode、classifyMethodNode、extractPropertyName、interfaceKind、extraClassNodeTypes、packageTypes/extractPackage、extractModifiers、synthesizeMembers、skipBodilessClass、methodsAreTopLevel。
推论:每个 ruby 函数/方法节点的 signature/isAsync/isStatic/returnType/isExported 都是 undefined(file 节点 isExported:false);没有 isConst 意味着每个 assignment 提取为 kind 'variable',绝不是 'constant' —— 包括 MAX = 3(value-ref 定位仍然有效:kind variable 是目标);attr_accessor/attr_reader/attr_writer 什么都不合成(无 synthesizeMembers —— 它们就是类体里的普通 call,完全无输出);没有参数节点,没有 decorates 引用(extractDecoratorsFor,tree-sitter.ts:4897 会运行,但 ruby 没有 decorator/annotation/marker_annotation 节点 kind,反向扫描在第一个非装饰器兄弟处停止 —— 永远是 no-op)。
阶段四:tree-sitter.ts 调度阶梯 —— 每个 ruby 节点命中什么
以下表格的锚点以 f1ca991 为准(调度阶梯在 936-1303 行):
| 节点 | 分支 | 行为 |
|---|---|---|
| 每个节点 | visitNode 钩子最先(943) | mixin call + module 在此处理;handled → scanFnRefSubtree + 停 |
| 每个节点 | maybeCaptureFnRefs(990) | 对 RUBY_SPEC 的调度键 argument_list / pair 同样在 visitNode 上下文中触发 —— 这就是类体钩子 DSL 符号的捕获方式 |
method |
functionTypes:994 | 在 class-like 内(isInsideClassLikeNode:1486 —— module 也算,1498)且在 methodTypes → extractMethod:1737;否则 extractFunction:1517。skipChildren |
singleton_method(def self.x / def Foo.x / def obj.x) |
不是 functionTypes → methodTypes:1027 | 无 classifyMethodNode → extractMethod。顶层时:非 class-like、无 methodsAreTopLevel、无 receiver 钩子 → 门禁 1747 送入 extractFunction → 顶层 def self.x 是名为 x 的普通 function 节点。object 字段处处被忽略 —— def self.x 与 def x 在图中不可区分(isStatic undefined) |
class |
classTypes:1005 | 无 classifyClassNode → extractClass:1679(kind 'class') |
singleton_class(class << self) |
无分支 | 递归 —— 其 body 里 body_statement 的 def 提取为外层类的 method,与实例方法不可区分(已探针) |
assignment(顶层/class/module) |
variableTypes:1098 | 门禁:!isInsideClassLikeNode() || isClassScopeConstantAssignment(1508:type==='assignment' 且(left 字段 ?? namedChild(0)).type === 'constant')。文件作用域:identifier 与 constant LHS 都提取。class/module 作用域:仅 constant LHS。然后 extractVariable:2538 + scanFnRefSubtree(1110)+ skipChildren —— RHS 永不遍历:顶层/class 级 X = Foo.new 不产生任何 instantiates/calls |
operator_assignment(+=、||=) |
无分支 | 不是 assignment → 任何作用域都不提取;递归(children 在非 body 作用域也不产出) |
call(顶层/class/module 体) |
importTypes:1209 —— 绝不是 callTypes | extractImport:3170。require/require_relative → import 节点 + 引用;其余所有 call → 无(钩子 null → 3350 处 if (this.extractor.extractImport) return;)。skipChildren 保持 false → children 会被访问:嵌套 call 同样落到这里(无输出),argument_list 获得 fn-ref 捕获。这把 attr_accessor、has_many、define_method(连同其 do_block 体)、get '/x' do…end 路由块、裸 DSL call 全部杀掉 —— 在非 body 作用域对提取完全不可见,例外只有 fn-ref 候选 + rails 正则提取器 |
call(方法/函数体内) |
visitFunctionBody:5143 | extractCall:3684 → ruby 分支 3905-3960 |
alias / alias_method |
无分支 / class 作用域的 call | 两者都不输出(已探针:alias 子节点是 name/alias identifier 字段,父节点不在 BLOCK_PARENTS) |
comment |
无分支 | 只被 docstring 的 sibling 扫描消费 |
顶层 if/unless/case/while/begin |
无分支 | 经 visitNode 递归 —— 所以顶层 if 里的 require 仍然到达 extractImport(全 visitNode 递归);但其中的 call/标识符无输出 |
uninterpreted(__END__ 数据) |
无分支 | 无(见语法准备一节) |
对 ruby 不适用(可尽早廉价验证的 early-out):interfaceTypes/structTypes/enumTypes/typeAliasTypes/propertyTypes/fieldTypes 分支;swift property 分支(1121);TS re-export/vue-store export_statement 分支(1219/1235);INSTANTIATION_KINDS(354-361 —— 没有 ruby 节点 kind;ruby 的 .new 在 extractCall 中处理,所以 extractInstantiation:4610 对 ruby 不可达);impl_item(1274);property_signature/method_signature(1282,门禁在 TYPE_ANNOTATION_LANGUAGES,排除 ruby,5752-5754);extractFilePackage:1397(无 packageTypes → 无 namespace 节点);namespacePrefix 恒为空(cpp 专属)。
节点创建、ID 与限定名
createNode(1308):id =generateNodeId(filePath, kind, name, startRow+1)=`${kind}:${sha256(filePath:kind:name:line).hex.slice(0,32)}`(tree-sitter-helpers.ts:18-30)。FILE 节点 id 是字面量file:${filePath}(509)。去重/自检比较的是 ID 字符串(node_idsvec 模式)。- resolveBody 的 endLine 扩展(1329)是 no-op(无钩子);节点 endLine = node.endPosition.row+1(对
method,tree-sitter 的 method 节点本身已跨 def…end)。 - 每个创建的节点都有来自 nodeStack 栈顶的 contains 边(1363);每次 create 都调 captureValueRefScope(1374)。
- qualifiedName = nodeStack 名字以
::连接(buildQualifiedName:1447,namespacePrefix 空)——module Outer; module Inner; class Deep→Outer::Inner::Deep,方法Outer::Inner::Deep::greet。紧凑形式class A::B::C/module A::B保留完整 scope_resolution 文本作为节点 NAME(extractName → nameField → getNodeText),所以其下嵌套类的 QN 形如A::B::C::m—— 名字段内的::原样组合。无 receiver-QN 路径(没有 getReceiverType)。 - File 节点:kind
file,name 为 basename,qualifiedName = filePath,endLine =source.split('\n').length,isExported false。
extractFunction / extractMethod / extractClass(1517 / 1737 / 1679)
- extractFunction(顶层
method,以及被 1747 门禁弹回的singleton_method):无 receiver 钩子(1522 跳过);名字经 nameFieldname(identifier;?/!/=结尾的 def 名 ——done?、save!、value=—— 名字保留后缀)。<anonymous>路径(1549)不会触发(语法强制要求 name)。无 misparse 钩子。节点 extras:docstring(getPrecedingDocstring)、signature undefined、visibility(sibling 扫描)、isExported/isAsync/isStatic/returnType undefined。extractTypeAnnotations → no-op(ruby ∉ TYPE_ANNOTATION_LANGUAGES:5752)。extractDecoratorsFor → no-op。push,沿body字段(body_statement)经 visitFunctionBody 走,pop。parameters(method_parameters)永不遍历 —— 默认参数里的调用def f(x = compute())什么都不输出。 - extractMethod(class/module 内的 def + class-like 内的 singleton_method):receiverType undefined(1742);门禁 1747 经 class-like 通过;object-literal 父节点检查(1751)不会匹配 ruby(
object/object_expression是 TS 节点 kind)。extras 同 function;无 receiver QN(1790),无 owner-contains 回退(1799 —— 需要 receiverType)。extractEnumMembers/interface 永不触发。 - extractClass:resolvedBody =
body字段;无 skipBodilessClass → 无体的class TopDoc; end(body 字段缺失 —— 已探针)仍然铸造 class 节点;body 遍历目标回退到 class 节点自身(1714)→ namedChildren = [name-constant, superclass?] 逐个 visitNode(无输出 —— 但注意 superclass 子树被再次访问,无害)。extras:docstring、visibility(sibling 扫描)、isExported undefined。extractInheritance(1704,见下);extractCsharpPrimaryCtorParamRefs/extractDecoratorsFor 为 no-op;synthesizeMembers 缺失(1727)。然后 class 入栈后走 body children。 - 方法体内的嵌套 def:visitFunctionBody:5245 ——
method属 functionTypes,named → extractFunction → 此刻不是 class-like(栈顶是外层 METHOD 节点)→ extractFunction 路径;其内部调度是直接的(无门禁)→ 嵌在 def 里的def提取为被外层方法包含的 function。body 内的class:命中 5255 → extractClass(被方法包含)。body 内的module节点不被 visitForCallsAndStructure 匹配(钩子在那里不运行)—— visitFunctionBody 从不调用提取器的 visitNode 钩子,所以方法体内定义的 module 不铸造任何 module 节点;其 children 递归(5277),其 def 命中 5245 → 归到外层方法的 function。body 内的includecall 同理:走 extractCall(callTypes)→ 名为include的calls引用 —— 不是 implements 引用。两个怪癖都保留。
extractImport 漏斗(3170):所有顶层 call 的归口
钩子只在"带 string 的 require/require_relative"时返回 {moduleName, signature}(见上)。之后:
- import 节点:
createNode('import', moduleName, node, {signature})—— id 来自 kindimport,name = moduleName(json、sidekiq/fetch、../foo/bar)。 - 通用
imports引用(3183-3194,钩子不设 handledRefs):{fromNodeId: 栈顶(file/class/module/何处均可), referenceName: moduleName 原样, line: call startRow+1, column: call startColumn}。 emitRubyRequireRefs(3231-3234 → 3532-3560):自行重新推导 method 名 + string 内容(namedChildren.find identifier / argument_list / string / string_content —— 同样的形态);req = 内容.trim()。require_relative→ refPath =path.posix.normalize(dirname(filePath) + '/' + req)(dirname 用 as-indexed filePath 的 lastIndexOf('/') —— posix 语义);require→ refPath = req 不变。然后:refPath 中没有/→ 返回(裸 gem/stdlib require 不产生文件引用);末尾追加.rb(除非已endsWith('.rb'));推 {fromNodeId, referenceName: refPath, referenceKind:'imports', call 的 line/col}。所以require "sidekiq/fetch"在 import 节点之后输出两个 imports 引用(sidekiq/fetch+sidekiq/fetch.rb)—— 输出顺序:节点、通用引用、require 引用。- 钩子 null(每个非 require call):穿过 python/go/php 的多 import 分支落到 3350 的
return—— 什么都不输出,children 仍被阶梯访问(import 分支 skipChildren 为 false)。
body 内的 require 永不走到这里(visitFunctionBody 把 call 路由到 extractCall)—— def 内的 require "x" 只输出一个裸 calls 引用 require;body 内 Kernel.require "some/lib" → calls Kernel.require + references Kernel(已探针)。只有 visitNode 上下文的 require(顶层、类体、module 体、顶层 if/begin 内)才创建 import 节点。
extractCall(3684):ruby 分支(3905-3960),永远提前返回
仅从 visitFunctionBody:5143 到达。门禁:language==='ruby' && (type 'call' || 'method_call') —— 位于 LITERAL_RECEIVER_TYPES / 通用 field_expression 机制之前,所以 ruby 永远不运行后者。步骤:
- methodName =
method字段文本;空/缺失 → 什么都不输出返回(操作符/元素引用形态;注意element_reference是自有的节点 kind,本来就到不了这里)。 - 无
receiver字段 → 一个calls引用 {name: methodName, line: call startRow+1, col: call startColumn}。覆盖无括号命令、puts、done?式零参调用、lambda/proc(block 体随后递归)、body 内require、body 内include(就是普通calls引用!)。 - receiverName = 完整 receiver 文本(
getNodeText—— 原样,sigil 与换行都算:@name、@@cv、$g、"literal"、5、[1, 2]、chained.first_call、a.b(x))。 methodName === 'new':className = receiverName 在最后一个::之后(slice(lastIndexOf('::')+2));若/^[A-Z]/→instantiates引用 {name: className} 位于 call 位置,返回。Widget.new→ instantiatesWidget;NS::Widget.new→Widget(不带限定名!);lower.new→ 不大写 → 落到第 5 步 → calls 引用lower.new。- SKIP_RECEIVERS = {
self,super} 按文本(ruby 专属集合 —— 比通用的 {self,this,cls,super,parent,static} 更小):命中 → 裸 methodName;否则`${receiverName}.${methodName}`。safe navigation&.以普通.连接(deep&.safe_call→deep.safe_call);链保留原始 receiver 文本(chained.first_call.second_call、Widget.create(x).save→Widget.create(x).save—— 参数文本不规范化为(),与 java/php 的链编码不同);字面量 receiver 不被过滤("literal".upcase→"literal".upcase、5.times→5.times—— 不可解析的噪声,保留)。 - receiver 节点类型恰为
constant(且未被 skip)→ 额外references引用 {name: receiverName, line/col: receiver 的位置} ——Klass.static_call输出 callsKlass.static_call+ referencesKlass;对 VALUE 常量同样触发(RETRY_MAX.times {…}→ callsRETRY_MAX.times+ referencesRETRY_MAX—— 已钉死)。scope_resolutionreceiver(Foo::Bar.baz)不得到 references 引用(类型 ≠ constant)。保留。 - return —— 内层 receiver call 之后也会被访问(body walker 在 5277 递归 children —— extractCall 不消费其子树),所以
chained.first_call.second_call同时输出chained.first_call.second_call与chained.first_call;参数 string 里的插值调用
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00