Skip to main content

BLOGE 0.9.8-RC1 在线版 · 事实校验 2026-09-15 · English

第 8 章 —— 把个人 DSL 草稿变成团队资产

本章承诺: 你会跟着订单 graph 完成五次小提交,亲眼看到一个“能编译”的个人草稿如何逐步获得契约、可诊断性、意图和复用边界,最终变成队友敢于修改的 .bloge 文件。

学习目标

学完本章,你能够:

  1. 解释为什么零编译错误不等于 DSL 已经可维护。
  2. 用 graph input、字段类型和稳定 output 把隐含假设变成团队合同。
  3. invalid-expression-path 找到错误发生在“哪条路径与哪个 schema 不一致”。
  4. 判断一段说明应成为 /// 文档注释,还是应该留在代码审查讨论中。
  5. 用 import alias 把共享流程放到明确边界后面,而不是复制节点。

这一章只增加一个变量

前七章已经让订单流程能够获取用户、读取商品、计算价格并决定是否创建订单。本章不增加新的业务行为,只增加一个团队现实:另一个人下周要安全地修改这份 graph。

这会改变“完成”的定义。个人草稿只需作者记得 ctx.customerEmail 从哪里来;团队资产必须让编译器、编辑器和审查者都能追踪这个假设。

开场:绿色草稿为什么让审查者不安

第一版订单摘要只有几行:

graph orderDraft {
transform orderSummary {
userId = ctx.userId
customerEmail = ctx.customerEmail
}
}

先预测:如果它编译时是 errors=0 warnings=0,下面哪句话成立?

说法是否能由零告警推出
语法可被当前编译器接受
调用方一定提供 customerEmail不能
该字段允许缺失不能
下游知道输出形状不能
六个月后重命名会被及时发现不能

绿色只说明编译器没有发现违反已声明合同的地方。没有合同,很多错误根本没有成为可检查的问题。

图 8-1:五次提交把隐含假设逐步变成可检查合同

先跑完整轨迹

本书提供五个独立快照和一个 RC1 probe。先从固定 BLOGE submodule 安装 core 与 DSL,然后运行:

cd submodule/bloge
mvn -pl bloge-core,bloge-dsl -am -DskipTests install

cd ../../probes/ch08-dsl
mvn test

2026-09-14 在 BLOGE commit cc38fbe5 上的关键输出:

commit-1 graph=orderDraft errors=0 warnings=0 firstRule=none
commit-2 graph=orderContractDraft errors=0 warnings=1 firstRule=invalid-expression-path
commit-3 graph=orderTypedDraft errors=0 warnings=0 firstRule=none
commit-4 graph=orderReviewable errors=0 warnings=0 firstRule=none
commit-5 graph=orderTeamAsset errors=0 warnings=0 firstRule=none
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

注意这不是“提交 2 比提交 1 更差”。提交 2 让一个原本不可见的假设变成可检查合同,因此编译器终于有能力指出问题。

提交 1:先让业务路径可见

第一版只保留 userId 和邮箱投影。它适合在个人探索中回答:“这种数据流写法是否表达得通?”

它不适合进入团队主干,因为 ctx 是不透明的。编译器不能区分“调用方真的提供一个顶层 customerEmail”与“作者把 customer.email 记错了”。

本次提交的审查结论不是“失败”,而是:

syntax accepted
input contract = opaque
safe refactor boundary = unknown

提交 2:声明输入合同,主动让错误浮出水面

第二次提交声明订单请求的真实形状:

input: OrderRequest

schema OrderRequest {
userId: String
customer: {
name: String
email: String?
}
}

但摘要仍读取 ctx.customerEmail。编译器现在知道顶层只有 userIdcustomer,于是报告:

warnings=1
firstRule=invalid-expression-path
Path 'ctx.customerEmail' — field 'customerEmail' not found in graph input schema

这个 warning 的价值不只是“有一个拼写错了”。它给出了三段定位信息:

  • 根对象是 graph input,也就是 ctx
  • 出错路径是 customerEmail
  • 对照合同是 OrderRequest

恢复动作不是关闭 schema validation,而是决定现实数据究竟是顶层邮箱,还是嵌套客户邮箱,然后让 schema 与表达式一致。

提交 3:修正路径,并把可选性写进类型

本例的现实答案是 customer.email,且有些客户没有邮箱:

transform customer {
name: String = ctx.customer.name
email: String? = ctx.customer.email
}

transform orderSummary {
userId: String = ctx.userId
customerEmail: String? = customer.output.email
}

再次编译变为零告警。这里同时建立了两条不同关系:

  • 路径关系:orderSummary 通过 customer.output.email 依赖上游 transform。
  • 类型关系:String? 明确“缺失是合同允许的”,不再由作者口头解释。

不要为了清除 warning 把所有字段改成可选。可选性是业务事实:若订单通知必须有邮箱,就应保留 String,让缺失尽早失败。

提交 4:让审查者知道这段 DSL 为什么存在

路径正确仍不等于意图清楚。第四次提交增加 graph output 合同,并给 transform 一句职责说明:

output: OrderGraphResult

schema OrderGraphResult {
orderSummary: OrderSummary
}

/// Projects the boundary fields; it does not make a business decision.
transform orderSummary { ... }

/// 会进入 AST 并与紧随其后的元素关联,可供编辑器或文档工具读取。普通 // 只给源码读者看,解析器不会把它当成合同元数据。

好的文档注释回答“责任和边界”,而不是把语法再念一遍:

注释审查价值
/// Creates order summary低:名字已经说过
/// Projects boundary fields; makes no business decision高:说明变换与决策的边界
/// This is important低:没有可检查含义

提交 5:共享行为不再复制,用 alias 建立边界

订单流程需要支付能力。复制支付节点看似最快,却让重试、金额校验和状态映射出现多个事实源。第五次提交把支付流程抽成独立 graph,并显式命名:

import "./payment-flow" as paymentFlow

node payment : paymentFlow {
input {
orderId = ctx.orderId
amount = ctx.amount
}
}

alias 不是缩写糖。它是当前文件对导入能力使用的稳定名字。编译器需要完成四件事:解析相对路径、编译被导入 graph、用其 input schema 检查当前 binding、把其 output schema 暴露给下游。

图 8-2:源码、导入�边界、编译诊断和团队审查共享同一份事实

如果导入文件不存在、形成循环,或两个 import 使用同一 alias,编译应该在建立 graph 前停止。不要在运行时才猜某个 alias 指向什么。

原理:声明越具体,反馈越有意义

五次提交背后是一条反馈管线:

source
→ lexer/parser:语法是否合法
→ import resolver:引用指向哪份 graph
→ schema resolver:字段和类型意味着什么
→ path/boundary validator:连接是否兼容
→ compiled graph:运行时可执行模型

前一层失败时,后一层结论就没有基础。语法通过不能替代 import 解析;import 能找到文件也不能替代 schema 兼容;schema 兼容更不能证明业务规则正确。

这也是团队 DSL 的核心价值:把原本留在作者脑中的假设,逐层变成机器与人都能复核的反馈。

失败实验:错误 alias 不是重试问题

保持两个被导入文件不变,只让它们都使用 as paymentFlow。RC1 会产生稳定诊断 duplicate-import-alias。恢复动作是给不同能力不同名字,或删除重复事实源;重跑运行时没有意义,因为 graph 尚未成功建立。

同理,invalid-expression-path 的恢复动作是修合同或路径,不是给节点增加 retry。编译期反馈与运行期韧性处理的是不同世界。

这一章刻意不展开的内容

完整 schema 类型目录、bare import 的 alias 推导、import resolver SPI、schema evolution 和 ValidatedSchema 策略都属于参考层,不应打断五次提交的主线。

特别是 ValidatedSchema:它是 Java runtime 中给 schema 附加 STRICT / COERCE / LENIENT 验证策略的包装器,不是本章 .bloge 文件必须掌握的新语法。当前 DSL 编译器在结构兼容检查时会先读取其底层 expected schema;运行时策略细节将在参考附录中集中说明。

换到现实世界:从个人菜谱到餐厅出品卡

个人做菜时,“适量盐”可能够用;餐厅交接需要原料规格、过敏原、出品形状、关键原因和共用酱汁版本。对应关系是:

餐厅交接团队 DSL
原料清单graph input schema
成品规格graph/output schema
工序卡nodes/transforms 与依赖
为什么这样做/// 责任注释
共用酱汁配方imported graph + alias
试做反馈compiler/linter diagnostics

规范不是为了把菜谱写长,而是为了让新厨师能安全修改并知道何时偏离标准。

轮到你:做五次可审查提交

选一份你维护的 .bloge 文件,每次只做一种改变:

  1. 保存当前可解析草稿,并记录尚未声明的输入假设。
  2. 声明 graph input,让编译器暴露真实路径问题;不要先修。
  3. 只修路径和可选性,记录 warning 的变化。
  4. 给稳定输出与关键 transform 加职责边界,不写语法复述。
  5. 只抽取一个确定共享的子流程,用显式 alias 连接。

最终提交一张审查卡:每次改了哪个假设、获得什么诊断、谁会因此更容易审查。若一次提交同时改业务分支、类型和导入,就拆开重做;否则无法知道哪项变化产生了反馈。

实验验收卡

  • 预期与观察: 五次提交依次暴露路径错误、修复类型并用 alias 收口,最终诊断 0/0。
  • 失败与恢复: 引入重复 alias 或错误路径;修复唯一声明后重跑 parser。
  • 证明边界: 证明 DSL 反馈可定位,不证明表达式是正确业务答案。
  • 练习合同: 团队 DSL;每次只改一个提交;交付五段诊断;最终 0 error/0 warning 且变化可解释即停止。

回顾

  • 零告警只覆盖已声明合同;opaque 输入会制造虚假的安静。
  • schema 的价值是让错误路径、缺失字段和可选性进入反馈循环。
  • /// 记录责任和边界,不能替代准确命名与结构。
  • import alias 把共享 graph 变成显式依赖,不是复制节点的快捷方式。
  • 语法、引用、结构合同和业务正确性是逐层递进的结论,不能互相替代。

下一章只增加一个变量:第 9 章——工具链工作流先把可维护 DSL 接入可重复的编辑、lint 与评审循环;第 10 章再引入子图边界。

精确事实入口

Coding Agent: Open the versioned task guide.