首页
/ Bruno 代码规范体系深解:conventions.md 的可读性判断层与完整执行链

Bruno 代码规范体系深解:conventions.md 的可读性判断层与完整执行链

2026-09-07 17:05:16作者:薛曦旖Francesca

本篇围绕 Bruno 仓库中 .claude/rules/conventions.md 展开,完整继承其“风格、可读性、注释、替换卫生、收尾检查”五大部分的核心规则,并结合 CODING_STANDARDS.mdeslint.config.jspackage.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-L68stylistic.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#L13eslint.config.js#L90-L91 的一处细节:配置通过 process.argv 检测当前是否处于 --fix 模式(isFixMode),若是,则临时把 eqeqeqprefer-const 降级为 off,非 fix 模式下二者为 warn。从源码结构看,这是为了在自动修复阶段避免这两类警告与批量改写相互干扰。

这些规则的作用范围由 mainLintFileseslint.config.js#L15-L34)界定,精确覆盖:tests/**playwright/**,以及 packages/bruno-appbruno-clibruno-commonbruno-convertersbruno-electronbruno-filestorebruno-schema-typesbruno-jsbruno-langbruno-requestsbruno-testsbruno-sqlite 等按扩展名细分的文件集;全局忽略 node_modulesdist*.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 itemsobj.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.jsonworkspaces 字段,共 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:fixeslint.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 团队,都是可直接参照的范式。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388