Bruno 代码规范体系深解:conventions.md 的可读性判断层与完整执行链
本篇围绕 Bruno 仓库中 .claude/rules/conventions.md 展开,完整继承其“风格、可读性、注释、替换卫生、收尾检查”五大部分的核心规则,并结合 CODING_STANDARDS.md、eslint.config.js、package.json 与提交钩子等仓库证据,讲清每条规范由谁强制执行、由谁做人工/AI 判断,以及它们如何构成一条从写码到提交再到评审的完整闭环。读完后你可以精确掌握 Bruno 这个 monorepo 的编码纪律:哪些交给 ESLint 自动修复、哪些需要贡献者与 Agent 自行把关,以及“替换代码必须清场”这一系列容易被忽视的收尾规则。
一、两层规范体系的定位:Source of Truth 与判断层
Bruno 的代码规范分为两层,这一分工在 conventions.md 开篇即被明确定义:
CODING_STANDARDS.md(仓库根目录)是 source of truth——Bruno 编码标准的唯一权威来源。它规定了通用的风格规则、测试要求、React/UI 专项约束与可读性/抽象原则。.claude/rules/conventions.md是叠加在标准之上的“judgment layer”(判断层)——即 linter 无法机械判断的那部分可读性与代码卫生决策。
文件头部的 frontmatter 声明了它的作用范围:
paths:
- "packages/**/*"
- "tests/**/*"
- "scripts/**/*"
也就是说,只要修改 packages/ 下任一工作区(bruno-app、bruno-electron、bruno-cli、bruno-common、bruno-requests 等)、顶层 tests/(Playwright E2E 套件)或 scripts/(构建/辅助脚本),这份规则就会作为上下文被加载。按 .claude/README.md 对 .claude/ 目录结构的说明,rules/*.md 属于 path-scoped 规则——“When Claude touches files matching each rule's paths:”时自动挂载;而 .claude/CLAUDE.md 在 Coding Standards 一节同样指出:“Mechanical style ... is ESLint-enforced — run npm run lint:fix, don't hand-police it”,并点名 conventions.md 覆盖 readability、reuse、replacement & pre-submit hygiene 三大主题。
这个两层设计值得任何团队借鉴:能机械执行的规则交给工具,只能靠判断的规则写成明确的文字判据,二者边界清晰,互不重复。
二、机械式风格规则:约定与 ESLint 配置的一一映射
conventions.md 的 Style & formatting 一节列出的每一条“mechanical style”,都能在 eslint.config.js 中找到对应的强制规则。该配置基于 ESLint 9 flat config + @stylistic/eslint-plugin,核心是 eslint.config.js#L63-L68 的 stylistic.configs.customize:
...stylistic.configs.customize({
indent: 2,
quotes: 'single',
semi: true,
jsx: true
}).rules,
在此基线上,eslint.config.js#L69-L87 逐项追加了细则。两条文档的对应关系如下:
| CODING_STANDARDS.md 中的约定 | conventions.md 表述 | 对应 ESLint 规则(eslint.config.js) |
|---|---|---|
| 2 空格缩进,禁用 Tab | 2-space indent | @stylistic/indent: 2(customize 基线) |
| 字符串单引号;JSX/TSX 属性用双引号 | single quotes (double for JSX/TSX attributes) | quotes: 'single' + jsx: true |
| 语句末尾必须分号 | semicolons | semi: true,且 @stylistic/semi-style: ['error', 'last'](分号在行尾而非换行) |
| 禁止尾随逗号 | no trailing commas | @stylistic/comma-dangle: ['error', 'never'] |
| 箭头函数参数一律加括号 | parenthesized arrow params | @stylistic/arrow-parens: ['error', 'always'] |
箭头前后留空格 () => {} |
spacing around arrows | @stylistic/arrow-spacing: ['error', { before: true, after: true }] |
函数名与调用括号之间不留空格 func() |
no space before call parens | @stylistic/function-call-spacing: ['error', 'never'] |
| 多行构造开括号同行 | opening braces on the same line | @stylistic/brace-style: ['error', '1tbs', ...] 与 @stylistic/curly-newline(multiline、minElements: 2) |
| 函数括号内不空行 | no newlines inside function parentheses | @stylistic/function-paren-newline: ['off'] |
| 不设严格行长限制 | — | @stylistic/max-len: ['off'] |
| JSX 允许一行多个表达式 | — | @stylistic/jsx-one-expression-per-line: ['off']、@stylistic/max-statements-per-line: ['off'] |
值得注意的是 eslint.config.js#L13 与 eslint.config.js#L90-L91 的一处细节:配置通过 process.argv 检测当前是否处于 --fix 模式(isFixMode),若是,则临时把 eqeqeq 与 prefer-const 降级为 off,非 fix 模式下二者为 warn。从源码结构看,这是为了在自动修复阶段避免这两类警告与批量改写相互干扰。
这些规则的作用范围由 mainLintFiles(eslint.config.js#L15-L34)界定,精确覆盖:tests/**、playwright/**,以及 packages/bruno-app、bruno-cli、bruno-common、bruno-converters、bruno-electron、bruno-filestore、bruno-schema-types、bruno-js、bruno-lang、bruno-requests、bruno-tests、bruno-sqlite 等按扩展名细分的文件集;全局忽略 node_modules、dist、*.bru(Bruno 自身的 DSL 文件)与构建产物(eslint.config.js#L38-L48)。此外配置还按包注入不同的 globals 与 no-undef: error(例如 eslint.config.js#L170-L185 给 bruno-cli 注入 node + jest 全局并启用未定义变量检查),并在 bruno-app 下启用 react-hooks 插件(eslint.config.js#L136-L144)。
执行入口在根 package.json 的 scripts 中:
npm run lint # cross-env NODE_OPTIONS="--max_old_space_size=4096" npx eslint
npm run lint:fix # 同上,追加 --fix 自动修复
conventions.md 对此的态度很务实:这些偏差“real but low-value to catch by hand”,发现后简要点出即可、不必纠缠;但 ESLint 无法机械修复的命名与大小写问题仍值得认真处理。
三、判断层核心:七条可读性判据
以下七条来自 conventions.md 的 Readability 一节,是 linter 管不到、必须靠人(或 Agent)判断的规则;CODING_STANDARDS.md 的 “Readability and Abstractions” 一节是它们的根目录版本,并额外要求“必要时为抽象添加 JSDoc 注释”。
1. 描述性命名(Descriptive names)。 函数与变量应携带简洁、具描述性的名字;即使代码逻辑完全正确,一个含糊或误导性的名字也值得被提出——这与 CODING_STANDARDS 中“Names for functions need to be concise and descriptive”一致。
2. 先复用,再新写(Reuse before you write)。 这是全文最强调的一条。添加组件、hook 或 helper 之前,先按概念(而不是你本想起的名字)搜索已存在的实现,并阅读最近邻的“同型问题”解决者——因为它的调用点展示了预期的组合方式。文档特别指出:复用通常是一次净删除(bespoke 标记及其 CSS 一并消失)。当现有原语几乎合适时,应当扩展(widen)它,而不是在旁边另立一个近似重复——“two near-identical implementations diverge silently”(两个几乎相同的实现会悄悄分叉)。
3. 抽取与抽象(Extraction & abstraction)。 只要真正提升可读性或服务于明确的、可预见的复用,就应抽取 helper 或共享抽象——这不以“最少调用点数量”为门槛,文档明确鼓励在有帮助处主动提出。唯一要避免的是不必要的抽象:不提升清晰度、也赚不到复用的间接层,例如为单一调用点构建的“通用”工具、或“为了以后”预留的 options/config。把一个长而复杂的函数拆成命名良好的局部 helper 则永远是允许的。
4. 单行间接层(Single-line indirection)。 一行函数若只是转发给另一个函数——只增加一个栈帧而不增加任何含义——应当内联。这与 CODING_STANDARDS 中“Avoid single line abstractions where all that's being done is increasing the call stack”完全对应。
5. 可选链纪律(Optional chaining)。 ?. 只属于“空值情形就在原地被处理”的场景(fallback、提前返回或 guard)。在其它位置使用它会掩盖“该值是否真可为 null”,并与 TypeScript 的类型保证相抵触——正确做法是先修类型或先收窄(narrow),而不是用 ?. 掩盖。
6. 注释解释 why。 真正复杂的流程值得一条注释,覆盖“直观阅读读不出来的理由”;不言自明的代码不需要注释。完整判据见下一节的 Comments 部分。
7. 函数式,但可读(Functional, but readable)。 偏好显然、线性的 pipeline,反对过深的函数式机制(ADT、monad)——代码要让任何贡献者都能轻松跟随与扩展。CODING_STANDARDS 的表述更为具体:“Follow functional programming but just enough to be readable, we don't need to go as deep as ADTs and Monads”。
四、注释纪律:只写“时间无关的事实”
conventions.md 的 Comments 一节给出了四条可逐条执行的判据,本质上是要求注释读起来像项目永久的一部分,而不是产生它的那次任务或会话的产物(原文开篇即定调:“Code and comments must read as a natural, permanent part of the project — never as artifacts of the task or session that produced them”)。
1. 禁止情境式/提示驱动式注释。 注释不得提及那次变更、那个任务或写下它的那个时刻。以下模式一律删除:// added to fix ...、// as requested、// new logic for X、// updated to handle ...、// per review。如果理由确实重要,把它写成关于代码的时间无关事实(或链接 issue/PR),而不是“我刚刚做了什么”。
2. 禁止显而易见注释。 不要复述代码已经说出的事:循环上方的 // loop over items、obj.name = name 上方的 // set the name、// return the result——这些毫无信息量。代码自明时,就让它没有注释。
3. 注释 why,而非 what。 注释只留给代码展示不出来的东西:非显而易见的理由、不变量(invariants)、边缘情况、workaround 及其背后的约束、单位、或指向规范/issue 的指针。“These stay useful long after the change lands”——这类注释在变更落地很久之后依然有用。
4. 禁止脚手架与旁白。 不要 // ... existing code ... 占位、不要留给自己的 TODO 旁白、不要注释里的 changelog 或逐步旁白、不要遗留注释掉的死代码。
这四条与 CODING_STANDARDS.md 中“Add in meaningful comments instead of obvious ones where complex code flow is explained properly”互为表里:根目录标准给出总则,conventions.md 给出可直接判罚的反例清单。
五、注释之外:消费者、风格连续与最小 diff
Everything added needs a live consumer in the same change. 这是全文最“反过度设计”的一条:
- 没有“为了以后”的 options、参数或配置;
- 没有没有任何读者解构的 payload 字段;
- 没有对生产者根本不可能产生的状态的分支;
- 没有被忽略的参数。
文档的论断很锋利:“Each is dead on arrival and reads as a contract honored somewhere else”——每一条都生来即死,读起来像在别处被履行了的契约;如果消费者要等到后续变更才出现,就不要加。同时它留了正确的出口:为可读性或明确可预见复用而抽取 helper 是允许的(回指 Readability 第 3 条)。
风格连续(Match the surrounding code)。 新代码的风格与命名必须与周边代码一致,让一次变更“indistinguishable from the existing codebase, not visibly bolted on”——看不出是后焊上去的。
最小 diff(Keep diffs minimal)。 不夹带无关的重排或空白 churn(这一点在 CODING_STANDARDS.md 第一句就有:“No diffs unless an actual change is made, the code changes need to be as minimal as possible”)。值得做的顺手清理应放进它自己的 commit,且绝不出现在与本次变更不同的包里——交错在一起时,评审者无法把 scope creep 与真正的载荷性编辑区分开。
六、替换代码必须清场:Replacing code leaves nothing behind
这是 conventions.md 中最具工程洞察力的一节,其核心命题是一句话:“Half a migration reads as a complete one”——一半完成的迁移读起来像完整完成的迁移,残留物能通过评审,并误导下一个编辑该文件的人。收尾闭环本身就是变更的一部分。三条规则:
1. 跟随被替换物走到每一个引用点,并在那里删除。 典型残留包括:
- 新标记不再产生的类名对应的 CSS 选择器;
- 最后一个使用点刚刚消失的 import;
- 没有任何调用方传入的 prop。
文档特别警告:样式是最容易被留下、留下时最具误导性的——“CSS for a class that renders nothing fails silently and looks intentional”(为一个不再渲染任何内容的类写的 CSS 会静默失效,且看上去是有意为之)。
2. 复制到新家后,不要留下原件。 而当两份拷贝确实必须同时存在时(独立的调用点,或一条规则在进程边界两侧各自强制执行),应当在两边都用注释写明那条不变量——因为除此之外没有任何机制能让两份拷贝保持同步。
3. 对齐整条流程,而不只是入口点。 重塑任何跨越边界的东西——payload、序列化字段、lookup key——意味着要走到远端的每一个消费者。而删除一个控制环节(sanitizer、校验或转义步骤)是一个需要被陈述的决策,而不是重写的副作用。
七、宣布“完成”之前的收尾检查
conventions.md 的最后一节 Before you call it done 给出三条可操作检查项:
1. 运行受影响工作区的完整测试,而不仅是自己写的那几个 spec。 Bruno 是 npm workspaces monorepo(见根 package.json 的 workspaces 字段,共 15 个工作区),单测按包分发(每包自带 jest.config.js)。按 .claude/CLAUDE.md 的命令约定,最小范围运行方式是:
# 限定到单个工作区,-- 之后给文件路径或模式
npm test --workspace=packages/bruno-app -- path/to/file.spec.js
npm test --workspace=packages/bruno-requests -- -t "test name pattern"
原则是“prefer the smallest scope — one workspace, one spec”,但“smallest scope”指的是不要动辄全量,而不是只跑新写的 spec——收尾时受影响工作区整体都要过。
2. 改动了返回值或 payload 形状? 其它 spec 也在断言它——别处存在的精确相等断言(exact-equality assertions)会因为新增一个 key 而失败。这是对 monorepo 里跨包契约变更的典型陷阱的点名。
3. 对照 diff 逐条走查验收标准(acceptance criteria)。 “Compound criteria are what slip: the half of the sentence you didn't have open never got exercised”——复合条件最容易漏:句子里你当时没打开看的那一半,从未被验证过。
八、执行链:从提交钩子到 AI 评审的闭环
这些规范不是纸面文件,仓库中存在一条清晰的执行链:
1. 提交前自动修复。 .husky/pre-commit 钩子内容为 npx nano-staged,配合根 package.json 中的 nano-staged 配置(prepare: husky 保证钩子安装):
"nano-staged": {
"*.{js,ts,jsx}": [
"npm run lint:fix"
]
}
即每次提交时,所有暂存的 JS/TS/JSX 文件自动跑一遍 ESLint --fix——第二节的机械风格规则因此在提交边界被强制收敛,这也正是 conventions.md 说“don't hand-police it”的底气所在。
2. AI 评审视角按本规则判罚。 .claude/skills/code-review/reviewers/conventions.md 定义了 code-review 技能中的“Coding standards & readability reviewer”:它要求对 diff 逐条对照 .claude/rules/conventions.md 检查,每个违规以 file:line 加严重级上报——可读性问题(命名不清、不必要抽象、单行间接层、错位的 ?.、diff churn、复杂流程缺注释)报为 suggestion;违反 Reuse 或 replacement hygiene 的同样报 suggestion;纯风格/格式偏差(大多可被 ESLint 自动修复)仅报 nit,且明确要求“keep these brief”,与 conventions.md 第二节“note them briefly rather than dwelling”的口径一致。
3. 构建产物隔离。 .claude/settings.json 声明 deny: ["Read(./**/dist/**)"],确保 Agent 基于 src/ 工作、绝不编辑各包生成的 bundle(见 .claude/README.md 对 “Read-deny rules are for build output, not dependencies” 的说明)。
4. 与 E2E 规则的分层。 conventions.md 的 paths: 覆盖 tests/**,而 CODING_STANDARDS.md 的 E2E 章节把 tests/**/*.spec.{ts,js} 定为规范位置,并把 Playwright 工作流细节(page-module 定位器模式、buildCommonLocators 等)指向 docs/playwright-testing-guide.md 与 .claude/rules/testing.md——通用代码卫生归 conventions,测试专项归 testing,主题不重叠。
九、小结:一份规范文档如何与工程基建咬合
回到 conventions.md 本身的结构:它刻意把自己定义为 linter 之上的判断层,于是全文呈现出稳定的三层分工——Style & formatting 全部指向 ESLint 与 npm run lint:fix(eslint.config.js 可逐条验证);Readability / Comments / Beyond comments / Replacing code 是逐条可判罚的文字判据,由贡献者自律与 .claude/skills/code-review/reviewers/conventions.md 的评审视角共同执行;Before you call it done 则是把工作区级测试(npm test --workspace=...)、跨包断言风险与验收标准走查变成收尾动作。配合 husky + nano-staged 的提交钩子与 dist/ 读禁配置,这套约定在 Bruno 仓库中构成了一条“写作时自律 → 提交时自动修复 → 评审时按条判罚”的完整执行链——对任何希望把“规范文档”落地为可执行工程纪律的 monorepo 团队,都是可直接参照的范式。
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 StartedRust0629
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