BLOGE
0.9.9-RC1在线版 · 编制 2026-11-02 · English
第 8 章 —— 把个人 DSL 草稿变成团队资产
本章承诺: 你会跟着订单 graph 完成五次小提交,亲眼看到一个“能编译”的个人草稿如何逐步获得契约、可诊断性、意图和复用边界,最终变成队友敢于修改的
.bloge文件。
学习目标
学完本章,你能够:
- 解释为什么零编译错误不等于 DSL 已经可维护。
- 用 graph input、字段类型和稳定 output 把隐含假设变成团队合同。
- 从
invalid-expression-path找到错误发生在“哪条路径与哪个 schema 不一致”。 - 判断一段说明应成为
///文档注释,还是应该留在代码审查讨论中。 - 用 import alias 把共享流程放到明确边界后面,而不是复制节点。
这一章只增加一个变量
前七章已经让订单流程能够获取用户、读取商品、计算价格并决定是否创建订单。本章不增加新的业务行为,只增加一个团队现实:另一个人下周要安全地修改这份 graph。
这会改变“完成”的定义。个人草稿只需作者记得 ctx.customerEmail 从哪里来;团队资产必须让编译器、编辑器和审查者都能追踪这个假设。
开场:绿色草稿为什么让审查者不安
第一版订单摘要只有几行:
graph orderDraft {
transform orderSummary {
userId = ctx.userId
customerEmail = ctx.customerEmail
}
}
先预测:如果它编译时是 errors=0 warnings=0,下面哪句话成立?
| 说法 | 是否能由零告警推出 |
|---|---|
| 语法可被当前编译器接受 | 能 |
调用方一定提供 customerEmail | 不能 |
| 该字段允许缺失 | 不能 |
| 下游知道输出形状 | 不能 |
| 六个月后重命名会被及时发现 | 不能 |
绿色只说明编译器没有发现违反已声明合同的地方。没有合同,很多错误根本没有成为可检查的问题。
先跑完整轨迹
本书提供五个独立快照和一个 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。编译器现在知道顶层只有 userId 和 customer,于是报告:
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 暴露给下游。
如果导入文件不存在、形成循环,或两个 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 文件,每次只做一种改变:
- 保存当前可解析草稿,并记录尚未声明的输入假设。
- 声明 graph input,让编译器暴露真实路径问题;不要先修。
- 只修路径和可选性,记录 warning 的变化。
- 给稳定输出与关键 transform 加职责边界,不写语法复述。
- 只抽取一个确定共享的子流程,用显式 alias 连接。
最终提交一张审查卡:每次改了哪个假设、获得什么诊断、谁会因此更容易审查。若一次提交同时改业务分支、类型和导入,就拆开重做;否则无法知道哪项变化产生了反馈。
实验验收卡
- 预期与观察: 五次提交依次暴露路径错误、修复类型并用 alias 收口,最终诊断 0/0。
- 失败与恢复: 引入重复 alias 或错误路径;修复唯一声明后重跑 parser。
- 证明边界: 证明 DSL 反馈可定位,不证明表达式是正确业务答案。
- 练习合同: 团队 DSL;每次只改一个提交;交付五段诊断;最终 0 error/0 warning 且变化可解释即停止。
回顾
- 零告警只覆盖已声明合同;opaque 输入会制造虚假的安静。
- schema 的价值是让错误路径、缺失字段和可选性进入反馈循环。
///记录责任和边界,不能替代准确命名与结构。- import alias 把共享 graph 变成显式依赖,不是复制节点的快捷方式。
- 语法、引用、结构合同和业务正确性是逐层递进的结论,不能互相替代。
下一章只增加一个变量:第 9 章——工具链工作流先把可维护 DSL 接入可重复的编辑、lint 与评审循环;第 10 章再引入子图边界。
精确事实入口
DslFeedbackLoopProbeTest.java—— 五次提交的可执行反馈。01-personal-draft.bloge到05-team-asset.bloge—— 五个完整快照。order-process-v6.bloge—— 远端 examples 的订单综合示例。DslCompiler.java—— 编译与诊断事实。FileSystemGraphResolver.java—— 相对 import 解析。ValidatedSchema.java—— 运行时验证策略包装器。
Coding Agent: Open the versioned task guide.