把架构决策写成 lint
同一份 AGENTS.md 里,有命令的规则会在三台操作系统上亮红。只有路径的那条,重命名之后没人发现。
本页解决的问题
先给结论「把架构决策写成 lint」要解决的关键问题是什么?
同一份 AGENTS.md 里,有命令的规则会在三台操作系统上亮红。只有路径的那条,重命名之后没人发现。
跟着交接处走,不要只看 Demo。 系统是否可靠,往往取决于模型、工具、状态、权限和人的交接处。把每次交接都当成可以观察、测试和恢复的地方。
为一个自动化步骤写清输入、负责人、审批和恢复动作。
一次运行成功了,却说不清发生了什么,也无法安全重放。
- 调用点是不是匿名字面量lib.rs L261
- 注释名字是否等于参数名lib.rs L222
- 被调方是不是 workspace cratelib.rs L177
- CI 是否三平台同时跑rust-ci.yml L174
- Markdown 路径是否存在AGENTS.md L35
- Feature 是否登记在穷尽表lib.rs L379
- 开发中特性默认必须关闭tests.rs L18
新人接到任务:改 MCP 工具调用。它打开 AGENTS.md,抄下第 35 行的路径。文件不存在。真实文件叫 connection_manager.rs,就在同一个目录。文档里那个带 mcp_ 前缀的名字,是一次重命名之后没改干净的残留。
出处:AGENTS.md 第 32 至 36 行;codex-rs/codex-mcp/src/connection_manager.rs 第 1 至 15 行
同一份文件里,位置参数少了 /*base_url*/,本地命令会红。改了 Cargo.toml 忘刷 Bazel 锁,CI 会红。第 35 行那条路径没有检查器。Markdown 不会自己核对文件在不在。
先改 API,让调用点自己能读。foo(false) 的读者必须跳到定义才能知道这个 false 管什么。改不了 API,才允许 /*param_name*/。lint 是退路。
出处:AGENTS.md 第 14 至 20 行
实现住在独立的 Dylint 库,当一次 rustc。类型解析完成后,才能拿到被调方的参数名。入口只看函数调用和方法调用,宏展开出来的直接跳过。
检查按这个顺序走。
1. 只查本仓库 crate,std 和 tokio 直接放过。
2. 注释从参数前的空隙、前 64 字节、参数文本自身三处找。
3. 名字不对报 mismatch。错注释不会再落到没写注释那条。
4. 没写时,方法名等于唯一参数名就豁免,例如 .enabled(false)。
5. 剩下的只拦匿名字面量。None、布尔、数字要写,字符串和字符放过。
出处:tools/argument-comment-lint/src/lib.rs 第 165 至 180 行;tools/argument-comment-lint/src/lib.rs 第 261 至 274 行
仓库入口把默认 Allow 的那条抬成 deny。CI 在 Linux、macOS、Windows 各跑一次,一台失败另外两台继续跑完。人在 macOS 上绿了,Windows 目标的宏展开若多出一处 None,第三台仍会拦住。
出处:.github/workflows/rust-ci.yml 第 164 至 187 行
调用点局部、名字可解析、误报能用豁免收住。换个语言,形状一样:先改名字,改不了就要求行内名字。TypeScript 用 ESLint,Python 用 ruff,都用得上。
第 35 行和第 265 行是同一种腐坏。app-server 指南还写着 v2.rs,当前是目录 v2/,下面拆成三十多个文件。文件靠近 800 行就要拆。拆了之后,指南里的单文件路径没人改。
出处:AGENTS.md 第 260 至 266 行
模块行数规则点名五个高频文件,四个已经越过 800,一个贴着 900。chat_composer.rs 按行计有 12859 行。仓库里没有数行数的命令。行数能数,CI 不数。一次改动是不是机械,机器做不好,所以 800 行上限停在评审。
出处:AGENTS.md 第 49 至 61 行;AGENTS.md 第 125 至 131 行
把规则分成两套来读。一套有命令或编译器,合并前会亮红。一套只能被人和评审读,漏看就过。路径是否存在本来最容易检查:抽出反引号路径,对仓库根做存在性判断。仓库没做。预算花在调用点可读性上,没有花在路径存在性上。
文档不会自己复查。能局部检查却只写在 Markdown 里,重命名和拆文件的那天,文字还在,对象已经搬家。最小形态是二十行脚本核对路径,不需要 rustc 插件。
特性开关如果只靠布尔和一篇说明,漏登记、开发中默认打开、Deprecated 一直待着,都不会第一时间亮红。
Feature 枚举旁边有一张 FEATURES 表。FeatureSpec 把标识、配置键、阶段、默认是否打开焊在同一行。表里找不到对应项就 unreachable!。枚举多一个变体、表少一行,运行到 key() 会直接崩。
出处:codex-rs/features/src/lib.rs 第 41 至 58 行;codex-rs/features/src/lib.rs 第 819 至 826 行;codex-rs/features/src/lib.rs 第 379 至 384 行
旁边两道测试锁住默认值。开发中的特性默认必须关闭。默认打开的特性,阶段只能是 Stable 或 Removed。阶段有五态,多出来的 Experimental 带着菜单名和公告。Deprecated 没有过期日,三个 Deprecated 项仍能打开。阶段能表达不该再用,不能表达下个版本删。
出处:codex-rs/features/src/tests.rs 第 17 至 28 行;codex-rs/features/src/tests.rs 第 82 至 94 行
穷尽表加两条测试,换语言也成立。漏登记就崩,默认值被锁住。换不来自动删除,只换来这两条不变量。
DSH:每个包必须露面,空也要解释
DeepSeek Harness 把「每个包必须拥有 ./invariant」同时写成散文和门禁。散文在 packages/AGENTS.md。门禁是 21 行的 verify-package-invariants,失败就 process.exit(1)。空安装器必须带固定前缀 No runtime invariant:。空是显式架构结论,以后引入可变状态,必须换成真正的检查。
笔记回答为什么允许空,检查器保证空必须解释。两者缺一,就会回到 Codex 第 35 行那种状态:文字还在,对象已经搬家。DSH 没有 rustc 插件去管 foo(false)。Codex 没有穷尽式包门禁去管路径存在性。
出处:packages/AGENTS.md 第 18 行;scripts/verify-package-invariants.ts 第 1 至 21 行
Grok:能局部化的决策直接丢进 clippy
Grok Build 仓库根没有 AGENTS.md。它仍把一条架构决策写成 lint:clippy.toml 禁止 canonicalize,理由是 Windows 上会得到 verbatim 前缀,破坏 git、泄漏进模型上下文。执行边界写在同一份文件:这条禁令由各 crate 的 cargo clippy presubmit 执行,只走 Bazel 的 crate 要靠人看。
和 Codex 的参数注释是同一类判断:调用点局部、误报面可控。Grok 承认 Bazel 覆盖不全。Codex 承认本地只跑当前操作系统。小团队先抄路径存在性和 21 行 verify 脚本,比抄 Dylint 便宜。
出处:clippy.toml 第 9 至 28 行
先做哪一道自动检查
AGENTS.md 第 35 行和第 265 行都是失效路径。若你只能先做一道自动检查,你检查带 codex-rs/ 前缀的路径,还是检查所有反引号里含 / 的字符串?
第一种会漏掉 app-server-protocol/src/protocol/v2.rs 这种相对写法。第二种会把命令名、crate 名和网址碎片误伤。写出你的过滤规则,并用这两条失效路径当正例。
「先玩一遍 · 一次提交过架构检查」的能力藏在每次交接里
「新人接到任务:改 MCP 工具调用。它打开 AGENTS.md ,抄下第 35 行的路径。文件不存在。真实文件叫 connection_manager.rs ,就在同一个目录。文档里那个带 mcp_ 前缀的名字,是一次重命名之后没改干净的残留」说明,Agent 的表现不只由模型决定。模型、上下文、工具、状态、权限和人之间的每次交接,都会改变任务能否继续以及出了问题能否恢复。
先写清状态,再增加能力
从「出处: AGENTS.md 第 32 至 36 行;」出发,可以把流程拆成起始状态、下一步动作、工具返回、状态更新和停止条件。这样调试时找的是第一处丢失信息或权限的位置,而不是笼统地说“模型变笨了”。
- 调用点是不是匿名字面量 lib.rs L261
- 注释名字是否等于参数名 lib.rs L222
- 被调方是不是 workspace crate lib.rs L177
成功路径不能代表系统可靠
用「第一种会漏掉 app-server-protocol/src/protocol/v2.rs 这种相对写法。第二种会把命令名、crate 名和网址碎片误伤。写出你的过滤规则,并用这两条失效路径当正例」重放一次成功和一次失败,记录每一轮真正传入的上下文、工具结果和负责人;只要第二个人能复述这条链,系统才有可维护性。
从「先玩一遍 · 一次提交过架构检查」走到「思路一 · 能局部检查的决策,写成机器能跑的红灯」
「先玩一遍 · 一次提交过架构检查」先把问题落在「同一份规范,五张改动卡片:看它被哪一层拦住,以及那一层想守住什么 播放 单步 重置 这次改动 裸 None 错名字 失效路径 再加行 漏登记 点播放看门禁怎么走。也可以直接点右侧某一层,看它放行还是拦住。 提交与门禁 待命 create_openai_url(None) 调用点写了裸 None。编译能过,读者必须跳到定义才知道它管什么。 1 编译器 2 自定义 lint 3 表与测试 4 三平台 CI 5 人工评审…」上;到了「思路一 · 能局部检查的决策,写成机器能跑的红灯」,讨论继续推进到「新人接到任务:改 MCP 工具调用。它打开 AGENTS.md ,抄下第 35 行的路径。文件不存在。真实文件叫 connection_manager.rs ,就在同一个目录。文档里那个带 mcp_ 前缀的名字,是一次重命名之后没改干净的残留」。两段连起来,重点就不只是记住一个结论,而是看清它成立所依赖的条件。
把这条判断带到下一个场景
分析 Agent 时,沿着状态、动作、工具结果和下一步的顺序走一遍;每次交接都要能说明信息从哪里来、由谁确认、失败时停在哪里。
- 「先玩一遍 · 一次提交过架构检查」:同一份规范,五张改动卡片:看它被哪一层拦住,以及那一层想守住什么 播放 单步 重置 这次改动 裸 None 错名字 失效路径 再加行 漏登记 点播放看门禁怎么走。也可以直接点右侧某一层,看它放行还是拦住。 提交与门禁 待命 create_openai_url(None) 调用点写了裸 None。编译能过,读者必须跳到定义才知道它管什么。 1 编译器 2 自定义 lint 3 表与测试 4 三平台 CI 5 人工评审…
- 「思路一 · 能局部检查的决策,写成机器能跑的红灯」:新人接到任务:改 MCP 工具调用。它打开 AGENTS.md ,抄下第 35 行的路径。文件不存在。真实文件叫 connection_manager.rs ,就在同一个目录。文档里那个带 mcp_ 前缀的名字,是一次重命名之后没改干净的残留
- 「最后的要点」:Markdown 路径是否存在 AGENTS.md L35
最后的「最后的要点」把讨论落到「Markdown 路径是否存在 AGENTS.md L35」。回看这条线索时,最值得保留的是:当输入、规模或风险改变,哪些判断需要重新做一遍。
我把这篇文章里的一个判断改写成了今天可以验证的小实验。比记住结论更有用的是,知道下一步要观察什么。
读完以后我先回头找它成立的条件,而不是直接把方法搬进项目。这个顺序让后面的取舍清楚很多。
如果把这个判断放到真实工作里,最先需要补的约束是什么?我想知道从阅读到第一次实践之间,哪一步最值得先做。
还没有这篇文章的讨论。