Bruno 磁盘 DSL 变更审查:面向 .bru/.yml 双格式向后兼容的专用代码审查层
2026/9/8 21:04:17 网站建设 项目流程

Bruno 磁盘 DSL 变更审查:面向 .bru/.yml 双格式向后兼容的专用代码审查层

【免费下载链接】brunoOpensource IDE For Exploring and Testing API's (lightweight alternative to Postman/Insomnia)项目地址: https://gitcode.com/GitHub_Trending/br/bruno

本文围绕 Bruno 代码审查技能(code-review skill)中的 "On-disk DSL & serialization reviewer" 展开,完整解读这位审查员的适用范围、发现项的严重级别分类(blocker / suggestion / 非发现项)以及输出契约,并结合 审查规则文件 与仓库源码,说明每一条审查规则背后对应的序列化实现与兼容性设计。读完本文,你可以掌握:如何在 Bruno 中审查一处涉及.bru/.yml磁盘格式的变更、判定它是否会破坏旧版本文件的解析,以及新增持久化字段需要贯通的全部文件链路。

背景:为什么磁盘格式变更属于"公共契约变更"

Bruno 将用户创建的一切——集合(collections)、请求(requests)、环境(environments)、文件夹与配置——都以纯文本文件持久化在用户磁盘和 Git 仓库中。磁盘上存在两种格式:

  • .bru:Bru DSL。读写经由 bruno-lang/v2/src 中的 v2(ohm-js)文法,入口函数为bruToJsonV2/jsonToBruV2,所有.bru变更都应在 v2 中完成。
  • .yml:OpenCollection YAML,由 bruno-filestore/src/formats/yml 处理。这是当前的DEFAULT_COLLECTION_FORMAT(定义于 constants.ts,渲染进程侧在bruno-app/src/utils/common/constants.js有一份副本),因此除非用户另行选择,新集合默认都是.yml
  • 此外还有bruno.json(每集合配置,经stringifyCollection序列化)与.env文件(parseDotEnv/dotenvToJson处理)。

两个容易踩坑的边界:bruno-toml包目前处于**孤立(orphaned)**状态——它没有被任何序列化路径引入,DSL 变更永远不需要触碰它;preferences.json是应用层的 electron-store 状态(位于userData下),不属于集合的磁盘契约,不在审查范围内。

这些文件的生命周期长于写入它们的那个版本:旧版 Bruno 要能读取新版写入的文件,不同版本的用户共享同一个 Git 仓库,用户还会手工提交这些文件。因此对磁盘结构的任何修改都是一次公共契约的变更——处理不当会导致解析失败、集合损坏,或在用户无法触及的集合上静默丢失数据。

另一个关键点是:成为这些文件的内存对象,往往在离序列化层很远的地方组装——在bruno-electron(IPC handlers)或bruno-app(Redux)中——然后被交给bruno-filestore,而后者无法察觉上游字段结构已经变了。所以 DSL 影响的变更可以源自数据链路上的任意包,而不只是格式包。这也是审查范围覆盖 7 个包的根本原因。

审查员的位置、作用域与调用流程

dsl-changes.md 是 code-review 技能 下 8 个并行运行的"镜头"(lens)之一,负责"On-disk DSL & serialization(向后兼容)"这一维度。编排文件中的审查员分工表如下:

审查员文件镜头作用域
reviewers/correctness.md正确性与根因全部源码(除tests/**
reviewers/architecture.md架构与依赖边界packages/**
reviewers/conventions.md编码规范与可读性全部文件
reviewers/react.mdReact(应用文件)packages/bruno-app/**
reviewers/cross-platform.md跨平台(macOS/Windows/Linux)全部文件
reviewers/security.md安全与数据安全全部源码(除tests/**
reviewers/dsl-changes.md磁盘 DSL 与序列化(向后兼容)bruno-appbruno-electronbruno-clibruno-langbruno-filestorebruno-schema(-types)bruno-converters
reviewers/e2e-tests.mdPlaywright E2E 测试tests/**

DSL 审查员的 Scope 行刻意列入了序列化层之外的三个包:格式包拥有序列化,但 DSL 对象在bruno-app(Redux)和bruno-electron(IPC)中组装,而bruno-cli直接读写这些文件——在这些地方做出的结构变更不会被序列化层捕获,因此全部纳入范围。

编排流程(见 SKILL.md)分四步,理解它对"何时会唤醒 DSL 审查员"至关重要:

  1. 获取 diff——只审查变更的代码,从不碰未改动的部分。两种模式:已提交区间(默认git diff main...HEAD,需先git fetch确认基准分支是最新的,过期的基准会把已合入的无关变更灌进 diff);工作树/未提交变更(先git add -N .让未跟踪文件可见,再把git diff HEAD一次性捕获到快照文件,避免树在审查中途移动导致各审查员看到不同快照)。
  2. 枚举变更文件git diff --name-only)——据此跳过作用域未被触及的镜头。例如没有任何packages/bruno-app/**变更就跳过react.md;没有tests/**变更就跳过e2e-tests.md
  3. 并行扇出——单条消息内为每个在范围内的审查员启动一个子代理,统一简报:diff 来源、该审查员拥有的文件 glob、以及"先读_contract.md,再读你自己的审查员文件及其指向的规则/源码,只应用这一个镜头,不审查范围之外的内容"。
  4. 合并与报告——去重,同一file:line被多个镜头命中时保留更高严重级别,按文件重新分组输出。

输出契约:一条一行、带严重级别的扁平发现列表

DSL 审查员采用专家审查员人设,输出格式在共享契约 _contract.md 中统一定义(每个审查员只读这一个契约文件,避免重复定义):

<blocker|suggestion|nit> | <file>:<line> | <one-sentence finding>

契约同时规定了几条纪律:每条发现一句话、被要求时才展开;严重级别不因作者身份或意图假设而软化;每条发现必须落地到真实代码——文档、指南或注释与仓库不一致时以仓库为准,禁止引用未验证的行号或编造示例值;作用域干净时只返回no findings,不为凑数而编造 nit。

发现项的严重级别分类

dsl-changes.md 定义了三类发现项与一类"非发现项",全部要求附带file:line并标注级别:

blocker:破坏性的字段变更与格式漂移

  • 破坏性字段变更:重命名、删除、改变类型、改变用途(repurpose),或改变默认值/语义;
  • 新增的字段不是可选的、且没有安全默认值;
  • bruyml之间的格式漂移(format drift),或同一格式内部 parse 与 stringify 之间的漂移;
  • 有损的往返(lossy round-trip):stringify丢弃未知字段,或parse(stringify(x)) !== x
  • 结构变更(shape change)却没有配套的读取期兼容垫片(read-time compat shim);
  • 晦涩、缩写或与现有命名不一致的属性名——属性名是永久性的,且直接面向用户。

blocker:新.bru语法与转义缺陷

  • 旧解析器读不懂的新.bru语法(新的转义形式、块定界符、注解文法)——这比字段丢失更严重:新版本的文件在旧版本中会直接解析失败,需要显式决策和兼容路径后才能发布;
  • 转义/定界符变更没有通过磁盘重解析(on-disk reparse,即对真实字节执行parse(read(serialize(x))))证明;
  • 用"按整个定界符切分"来转义多字符定界符的错误做法——split("'''")会在长度 ≥ 定界符的连续字符片段处留下可重新组合的残片。正确做法是先转义反斜杠,再逐个转义每个定界符字符。

规则文件把这条指向了具体实现:escapeMultilineDescription。源码与规则描述完全对应——先把已存在的\'加倍,再转义'''

// Escapes a multiline description's own delimiter (''') so it can safely round-trip // inside a '''...''' block. Any pre-existing \' must be doubled first so decoding // can tell it apart from the backslashes introduced by escaping '''. const escapeMultilineDescription = (value) => value.split('\\\'').join('\\\\\'').split('\'\'\'').join('\\\'\\\'\\\''); const unescapeMultilineDescription = (value) => value.split('\\\'\\\'\\\'').join('\'\'\'').split('\\\\\'').join('\\\'');

值得注意的是,规则文件同时点出了为什么"内存中的转义往返测试通过"不足以证明正确:解析器在遇到第一个被重新引入的定界符时就会终止,内存里的 escape/unescape 配对测试无法暴露磁盘文件层面的损坏。因此审查时要求对真实字节做parse(read(serialize(x))),并对引号/反斜杠的连续序列做模糊(fuzz)测试。

suggestion:测试缺口与可扩展性

  • 缺少两种格式的往返测试,或缺少旧格式的黄金测试夹具(golden fixture);
  • 命名或结构不可扩展:位置式(positional)而非键控(keyed)的列表、不断加宽的联合类型(widening union)。

抵制"不必要的 DSL 变更",以及明确的"非发现项"

审查员还必须对任何不必要的 DSL 变更提出质疑:凡是能在内存中、在 UI 中或作为派生值解决的增强,都不应改动序列化结构——大多数增强都可以这样做,只有数据确实需要持久化时才改磁盘格式。

同时文档明确划出了一条"非发现项"(Not a finding):description字段"读取接受{content}对象 / 写入只输出字符串"的不对称性。这不是有损往返,而是既有约定(string-only internally),不能作为 bug 上报。这条边界非常重要——它防止审查员把已确立的约定误报为 blocker,也解释了为什么规则文件中要用整整一节去证明这个约定(见下文)。

源码级实现证据:每条规则如何落到代码

dsl-changes.md 要求审查员"对照 .claude/rules/dsl-changes.md 审查 diff(并实际去读它)"。规则文件承载了审查所依据的完整技术细节,以下按规则逐条给出仓库内的实现锚点。

双格式体系与默认格式

前面已列出两种格式的处理位置。规则还补充了两个细节:

  • bruno.json(每集合配置)经stringifyCollection透传,.env文件经parseDotEnv/dotenvToJson处理;
  • 从源码结构看,bruno-filestore/src/formats/yml目录本身就是"parse/stringify 成对"的组织方式:parseItem.tsstringifyItem.tsparseItem.spec.tsstringifyItem.spec.ts一一对应(collection、folder、environment 同理),common/子目录下则放字段级的共享解析器(headers.tsvariables.tsassertions.tsactions.ts等)。

往返必须无损:parse(stringify(x)) === x

规则要求:parse(stringify(x))必须等于x,且stringify永远不能丢弃它不认识的字段。新版本写入的文件,被旧版本打开并重新保存后,不得丢失新增字段。测试模式是成对的 spec 文件紧邻各自序列化器放置——parseItem.spec.ts 与 stringifyItem.spec.ts 即为此例,字段级用例见 variables.spec.ts 与datatype.spec.ts

描述字段:string-only 约定及其唯一例外

规则用一整节确立了description的约定,这也是审查员"非发现项"条款的依据:

  • 每个内联description字段(headers、params、assertions、variables、body 条目等)在内部都以纯string存储;
  • yml 侧:解析器在读取时接受裸字符串遗留的{ content }对象,并统一归一化为字符串(formats/yml/common/{headers,variables,assertions,actions}.tsparseEnvironment.tsbody.ts);写入方只输出裸字符串;
  • bru 侧:根本没有{ content }的处理,描述直接从文法存为注解/文本块字符串;
  • 该"读对象 / 写字符串"的不对称性由 variables.spec.ts 证明,是既定约定而非有损往返。新的描述类字段应遵循这个 string-only 形状。

例外——docs不是 string-only。集合/文件夹文档(docs)在 yml 侧被写成对象{ content, type: 'text/markdown' }(见 stringifyCollection.ts 与stringifyFolder.ts),读取时再归一化回字符串;.bru侧则直接写成纯文本块。因此新增字段时必须先判断它属于哪个家族,再决定复制哪种模式。

结构变更:迁移发生在读取期,永远不让用户改文件

当形状不得不变更时,规则要求添加读取期兼容垫片(read-time compat shim),在解析时把旧形状升级为新形状。仓库中有两个现成模式:

  • ensureAuthV3Rc1BackwardsCompatibility(位于formats/yml/parseItem.ts,在解析 yml item 后立即调用);
  • formats/bru/index.ts中 pre-v3 的 status/statusText 交换处理。

未来的删除必须门槛在次版本号(major version)之后,并留下带日期的TODO(remove after vN)标记。

双格式 lockstep:没有专门的 bru↔yml 转换器

任何新增或变更的字段必须在bruyml两侧的 parse 与 stringify 中都处理,否则同一个集合会因为格式不同而行为不同。从源码结构看,仓库里没有专门的格式转换器:请求在两种格式之间迁移,靠的是先按源格式解析为共享的内存对象,再按目标格式重新序列化——SaveTransientRequest就是把sourceFormat+targetFormat传给renderer:save-transient-request的典型例子。这意味着只在一种格式中处理的字段,会在请求从.bru集合拷贝/保存到.yml集合(或反向)的瞬间被静默丢弃

规则还特别提醒"未设置(unset)情形"的漂移:某个字段在.bru中默认为''而在.yml中默认为'1',这类默认值分歧本身就是 bug。默认 app-data 工作区与自定义文件系统工作区是同一类"孪生"问题——在一个工作区持久化、在另一个工作区被静默丢弃的值属于同一 bug 类别。

bruno-schema(Yup)不是可选的:它会拒绝你的新字段

集合/请求/环境的 Yup schema 声明为.noUnknown(true).strict(),并且在保存路径上执行校验(如bruno-appslices/collections/actions.js中的itemSchema.validate/environmentSchema.validateSyncbruno-electronstore/global-environments.js)。一个新字段如果通过了 filestore 的往返、却没有加进 bruno-schema,保存时就会抛校验错误。bruno-schema(运行时 Yup)与bruno-schema-types(TS 类型)是两个独立的包,两者都必须更新

命名按"永久性"对待

DSL 键名一旦写下就基本是永恒的:重命名会破坏所有现有文件并强制引入兼容垫片。这些名字还是面向用户的——人们会直接阅读和手工编辑.bru/.yml,AI 代理也会基于这些集合推理。因此命名要求清晰、描述性强、完整拼写,无晦涩缩写,与现有键一致,"第一次就做对"。属性名值得比普通变量更严格的审查。

为新字段设计可扩展结构

新键遵循现有meta/ section 块的命名与嵌套(meta.namemeta.seqmeta.typemeta.tags等),优先选择可干净扩展的结构——键控列表优于位置式列表,对象优于不断加宽的联合类型,并与相邻字段的建模方式保持一致。规则还指出一个容易误解的细节:磁盘上的meta {}块与类型不是1:1 映射——在bruno-schema-types中这些键被平铺到Item上(seqnametypetagsdescription直接挂在 item 上),而 v2 ohm 文法把meta当作开放的字典处理:上述四个名字只是被建模的键,不是封闭集合。

新增一个持久化字段:通常要触碰的文件清单

规则文件给出了新增单个字段时的典型文件清单(.bru中心的变更大部分落在bruno-lang/v2):

  • bruno-lang/v2/src——.bru入口:bruToJson.jscollectionBruToJson.jsenvToJson.jsjsonToBru.jsjsonToCollectionBru.jsjsonToEnv.js,以及共享助手utils.js(已核对,文件列表与源码目录一致);
  • bruno-filestore/src/formats/yml——.yml侧的 parsestringify;
  • bruno-schema/src/collections/index.js——Yup schema(跳过它,保存就会失败);
  • bruno-schema-types/src/collection/item.ts——TypeScript 类型;
  • bruno-converters,以及bruno-electron/bruno-app中组装对象的集合工具;
  • 紧邻序列化器的往返 spec(formats/yml/parseItem.spec.ts+stringifyItem.spec.ts)。

规则的最后一条实操建议:先在代码里找到一个可比的已有字段,照着它的接线方式再添加你的——这条经验对审查员判定"新字段是否按既有模式贯通了所有层"同样适用。

变更前检查清单(Checklist)

规则文件以一份 10 项清单收尾,审查员实际上是在用同一份清单反过来验证 diff:

  • 变更确实是必要的(无法用内存 / UI 解决)
  • 新字段可选且有安全默认值;没有任何重命名、删除或改类型
  • bruyml两侧都处理了 parsestringify
  • 类型(bruno-schema-types)、Yup schema(bruno-schema)、converters 都已更新
  • 旧文件仍能解析——形状变更时已加读取期兼容垫片
  • 没有旧解析器读不懂的新.bru语法(或已决定前向兼容路径)
  • 新转义已通过磁盘重解析 + fuzz 证明,转义的是单字符而非整个定界符
  • bru/yml(以及默认 vs 自定义工作区)行为一致,包括 unset 情形
  • 已添加往返 + 旧格式夹具测试
  • 命名/结构与现有块一致且可扩展

小结

Bruno 的 DSL 审查员不是泛泛的"代码风格检查",而是针对磁盘契约设计的专用镜头:它把"序列化格式是面向所有历史版本的公共 API"这一前提,转译成可执行的 blocker / suggestion / 非发现项三级判定标准,并让每条判定都能落到仓库中可验证的锚点上——从 v2 文法的转义实现 到 Yup 严格校验,从 读取期兼容垫片 到成对的 parse/stringify spec。对于要修改 Bruno 持久化层的贡献者,这份审查规则与其检查清单本身,就是一份比任何 issue 讨论都更权威的格式演进规范。

【免费下载链接】brunoOpensource IDE For Exploring and Testing API's (lightweight alternative to Postman/Insomnia)项目地址: https://gitcode.com/GitHub_Trending/br/bruno

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询