第 2 章 · 01 SPEC §2 Layout 与依赖方向无环


文档摘要

第 2 章 · 01 SPEC §2 Layout 与依赖方向无环 本节摘要:SPEC §2 定义了 Reasonix 的目录 Layout 和一条最硬的工程约束——依赖方向无环(acyclic)。本节先逐目录走读 Layout( / / / / ),再深讲依赖方向: 这条主干;built-in 子包( 、 )import 父包自注册、父包永不 import 子包的"自注册倒置"模式;Remote-SSH 模块的四层分层( )及其"绝不 import cli/agent/serve、交互全走回调"的桌面友好设计。最后讲 REASONIX.md 的 import cycle 检查规则与 PR hygiene——把"无环"从口号变成可本地复现的检查。

第 2 章 · 01 SPEC §2 Layout 与依赖方向无环

本节摘要:SPEC §2 定义了 Reasonix 的目录 Layout 和一条最硬的工程约束——依赖方向无环(acyclic)。本节先逐目录走读 Layout(go.mod/Makefile/cmd/internal/docs),再深讲依赖方向:cli → {agent, plugin, config} → {tool, provider} 这条主干;built-in 子包(provider/openaitool/builtin)import 父包自注册、父包永不 import 子包的"自注册倒置"模式;Remote-SSH 模块的四层分层(cli → remote/bootstrap → remote → {forward, sftpfs, config, netclient})及其"绝不 import cli/agent/serve、交互全走回调"的桌面友好设计。最后讲 REASONIX.md 的 import cycle 检查规则与 PR hygiene——把"无环"从口号变成可本地复现的检查。

内容来源:原项目源码 docs/SPEC.md §2 Layout、REASONIX.md(Import cycle rule / PR hygiene)、internal/ 目录树。

⚠️ 注意:Go 一旦出现 import cycle,go buildgo test 会直接 [setup failed] 并列出环。Reasonix 因为代码量大(16.5 万行)且采用自注册模式,环的风险点集中在"子包测试文件 import 回父包"这条隐蔽路径,REASONIX.md 为此专门写了检查规则。

学习目标

阅读完本节,你应当能够:

  1. 画出 SPEC §2 的目录树,说出每个顶层目录的职责。
  2. 默写依赖方向主干 cli → {agent, plugin, config} → {tool, provider}
  3. 解释 built-in 子包为何要 import 父包、父包为何永不 import 子包。
  4. 复述 Remote-SSH 四层分层及其"不 import cli/agent/serve"的约束。
  5. go test ./path/to/target/ 在本地检测 import cycle。
  6. 说清 PR hygiene 三条(一次 force-push、最小 diff、amend 不加 commit)。

一、SPEC §2 Layout 目录树

SPEC §2 给出的 Layout(docs/SPEC.md:28-52),精简版:

reasonix/ ├── go.mod / go.sum # module reasonix; require BurntSushi/toml ├── Makefile # build / cross / vet / fmt / test ├── README.md / README.zh-CN.md ├── reasonix.example.toml # 配置示例 ├── docs/SPEC.md # 工程契约 ├── cmd/reasonix/main.go # 入口;blank-import 内置 provider/tool ├── cmd/reasonix-plugin-example/ # MCP stdio 插件参考实现 └── internal/ ├── cli/ # 子命令路由、flag、装配、退出码 ├── config/ # TOML 加载(flag > project > user > defaults) ├── provider/ # Provider interface + 类型 + kind→factory 注册表 │ └── openai/ # OpenAI 兼容实现;init() 注册 "openai" ├── tool/ # Tool interface + Registry │ └── builtin/ # read_file/write_file/edit_file/move_file/bash/ls/glob/grep ├── permission/ # 每次调用的 Policy:allow/ask/deny → Decision ├── command/ # 从 .reasonix/commands/*.md 加载的自定义 slash 命令 ├── plugin/ # stdio JSON-RPC(MCP)客户端;适配远端工具 ├── remote/ # Remote-SSH 模块的 SSH 传输 │ ├── forward/ # -L / -R 端口转发生命周期 │ ├── sftpfs/ # SFTP 文件层(隔离 pkg/sftp) │ └── bootstrap/ # 通过 SSH detached 启动 `reasonix serve` └── agent/ # Session + harness loop

几个细节值得注意:

  • go.modrequire BurntSushi/toml——TOML 是 SPEC §1.3 唯一接受的第三方依赖(其他依赖如 mvdan.cc/shtree-sitter 是后来按需加入,但仍守"纯 Go / 不破坏单二进制"的底线)。
  • cmd/reasonix/main.go 是唯一入口;cmd/reasonix-plugin-example 是参考 MCP 插件,可独立编译运行。
  • internal/ 下既有"抽象层"包(providertoolpermissionplugin),也有"实现层"子包(provider/openaitool/builtin),还有"运输层"(remoteserve)。

二、依赖方向主干:acyclic

紧接 Layout,SPEC 用一句话钉死了依赖方向(docs/SPEC.md:54):

Dependency direction (acyclic): cli → {agent, plugin, config} → {tool, provider}.

依赖方向无环,主干只有两层:

cli ──→ agent ──→ tool │ │ │ └──→ provider ├──→ plugin ──→ tool └──→ config

含义:

  • cli 处于最顶层,负责装配;它 import agentpluginconfig
  • agentplugin 处于中间层,它们 import toolprovider(底层抽象)。
  • toolprovider 处于最底层,只 import 标准库与极少数工具包(如 internal/diffinternal/nilutil)。
  • 箭头单向,绝不反向:tool 不 import agent,provider 不 import cli

这条约束保证底层抽象稳定——改 agent 不会影响 tool/provider,改 cli 不会影响 agent。重构时"哪层能动、哪层不能动"一目了然。

💡 契约要点:无环不是"建议",而是 build/test 能否通过的红线。Go 编译器本身拒绝 import cycle,所以这条约束编译期强制;但 test 文件的环编译器查不出来,需要手动规则(见第四节)。

三、built-in 子包:自注册倒置

SPEC §2 紧接着说(docs/SPEC.md:55-56):

Built-in subpackages (provider/openai, tool/builtin) import their parent to self-register; parents never import children.

这是 Go 实现"插件自注册"的经典手法,也是 Reasonix"薄 harness"在依赖方向上的具体落地:

  • 子包 import 父包:internal/provider/openai/openai.go:57-58 调用 provider.Register("openai", New),因此必须 import "reasonix/internal/provider"
  • 父包永不 import 子包:internal/provider/provider.go 里看不到任何对 openaianthropic 的引用,只有 interface 和注册表。
  • 入口负责触发:cmd/reasonix/main.go:10-17 用 blank-import _ "reasonix/internal/provider/openai" 触发子包 init(),把 Factory 注入注册表。

为什么是"倒置"?因为常规直觉是"父包知道自己的实现",但那样就会产生 provider → openai → provider 的环(父包 import 子包,子包又 import 父包拿 interface)。倒置后,只有子包→父包一条边,环消失了:

provider(openai 子包) ──import──→ provider(父包,interface + registry) ▲ │ cmd/reasonix blank-import 触发 init()

这套模式适用于所有"编译期内置":

  • provider/openai 注册 "openai" kind。
  • provider/anthropic 注册 "anthropic" kind。
  • provider/responses 注册 "responses""dashscope-responses"
  • tool/builtin 的每个工具文件(bash.goreadfile.goeditfile.go 等)各自注册一个内置工具。

⚠️ 注意:加新的内置 provider/tool 时,必须cmd/reasonix/main.go 加对应 blank-import,否则它的 init() 永远不会执行,注册表里没有这一项。漏加是新手最常犯的错。

四、Remote-SSH 模块的四层分层

Remote-SSH 是 Reasonix 通过 SSH 连接远端机器、在远端跑 reasonix serve 的能力。SPEC §2 用一段单独的分层描述(docs/SPEC.md:56-60):

The Remote-SSH module layers cli → remote/bootstrap → remote → {remote/forward, remote/sftpfs, config, netclient}; remote and its subpackages never import cli, agent, or serve, and all interactivity flows through callbacks (host-key / secret prompts) so the desktop module consumes the same surface.

四层从上到下:

  1. cli——用户输入 reasonix remote ... 子命令的入口。
  2. remote/bootstrap——通过 SSH detached 启动远端 reasonix serve
  3. remote——SSH 传输核心。
  4. remote/forward(端口转发 -L/-R)、remote/sftpfs(SFTP 文件层)、confignetclient——底层能力。

关键约束有两条:

  • remote 及其子包绝不 import cliagentserve。这保证远端逻辑与本地 CLI/Agent 解耦——远端可以单独编译、单独测试。
  • 所有交互走回调(host-key 提示、密码/密钥提示)。即 remote 不直接 fmt.Scan 读用户输入,而是通过回调函数把"需要用户确认 host key""需要密码"这类事件抛给上层;上层在 CLI 里走 TUI 弹窗,在桌面里走 React 弹窗。

第二条是"三前端共用单 Controller"哲学的延伸——remote 是传输无关的,CLI 和桌面能用同一套 surface。第 10 章会详讲 Remote-SSH。

五、import cycle 检查规则

Go 编译器对非测试文件的 import cycle 零容忍——go build 直接报错。但测试文件的环更隐蔽:agent/agent_test.go import tool/builtin,tool/builtin/sessions.go 又 import agent,这种环 go build 看不见(因为 test 不进二进制),但 go test 会以 [setup failed] 失败。

REASONIX.md 给出了检查规则与反例(REASONIX.md:49-56):

## Import cycle rule Before importing a new internal package from a non-test file, verify the target package's **test files** aren't already importing back to you: # BAD: agent(_test.go) → tool/builtin(sessions.go) → agent → setup failed Use `go test ./path/to/target/` to detect cycles **before** pushing. A `[setup failed]` message means a cycle exists.

实操:在 agent 里要 import tool/builtin 之前,先跑 go test ./internal/tool/builtin/,看它的测试文件是否已经 import 回 agent。若是,就会形成 agent(_test) → tool/builtin → agent 的环,go test ./internal/agent/ 会报 [setup failed]

💡 契约要点:import cycle 的检查不是"等 CI 报错再修",而是 PR 作者在本地用 go test ./path/to/target/ 主动验证。[setup failed] 这个错误信息几乎专指环。

六、PR hygiene

REASONIX.md 还列了三条 PR 卫生(REASONIX.md:58-62):

  • One force-push per round of review feedback(每轮评审反馈只 force-push 一次)。多次 force-push 会摧毁评审历史,让评审者困惑。
  • Keep the PR diff minimal(保持 PR diff 最小)。只包含与 PR 目的相关文件的改动,不夹带其它分支的零散修改。
  • Amend, don't add commits, for review feedback(评审反馈用 amend,不要加新 commit)。保持 commit 历史干净。

这三条不是 Go 特有,但 Reasonix 把它们写进项目记忆(REASONIX.md),意味着每个 PR 作者都默认遵守。配合 SPEC §1.6"Evolve, don't over-engineer",整体工程文化是"小步、最小 diff、可追溯"。

本节要点回顾

  1. Layout:go.mod(只 require BurntSushi/toml)、cmd/reasonix(入口)、internal/(40+ 内核包)、cmd/reasonix-plugin-example(MCP 插件样板)。
  2. 依赖方向无环主干:cli → {agent, plugin, config} → {tool, provider},箭头单向、绝不反向。
  3. built-in 自注册倒置:子包(provider/openaitool/builtin)import 父包自注册,父包永不 import 子包;入口 blank-import 触发 init()
  4. Remote-SSH 四层:cli → remote/bootstrap → remote → {forward, sftpfs, config, netclient};remote 绝不 import cli/agent/serve,交互全走回调。
  5. import cycle 检查:本地 go test ./path/to/target/ 验证;[setup failed] 几乎专指环;测试文件的环最隐蔽。
  6. PR hygiene:一次 force-push、最小 diff、amend 不加 commit。

下一节,我们看 Reasonix 怎么用 TOML 把这一切配置化——config 四级覆盖(flag > project > user > defaults)与 reasonix.example.toml 260 行逐段精读,理解"加新模型是 config 编辑,不是代码改动"。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U