BLOGE
0.9.8-RC1在线版 · 事实校验 2026-09-15 · English
第 9 章 —— 工具链工作流
承诺: 读完本章后,你将理解 BLOGE 工具链的 TypeScript 编写路径与 Java 执行路径如何协同,并且知道怎样为
.bloge文件建立高效编写循环,在代码审查和上线前捕获错误。
学习目标
- 理解 两条实现路径:TypeScript
bloge-lang服务 LSP/Studio 编写,Javabloge-dsl与bloge-lint服务 runtime/CI。 - 了解 诊断信息如何流转:从词法分析器、解析器、编译器和 lint 规则,经过 LSP,最终变成编辑器中的红色波浪线。
- 使用 保存时格式化 通过 DSL 代码生成器保持
.bloge文件的一致性。 - 在 CI 中运行
bloge-lintCLI,并理解它的 Java 规则集与编辑器诊断有重叠但不完全相同。 - 追踪
operator-metadata.json如何从bloge-maven-plugin进入 BLOGE Studio 的 operator 面板,再用分屏视图在 DSL 文本和画布之间往返编辑。
前置条件
- 第 8 章——把个人 DSL 草稿变成团队资产 —— 你需要熟悉一个完整的
.bloge文件。 - 第 2 章——你的第一个 Graph —— 你应该会读取编译和执行结果。
- 对 VS Code 或 IntelliJ IDEA 有基本了解。
源示例
| 文件 | 展示内容 |
|---|---|
bloge-lang/README.md | TypeScript 编写核心:词法分析器、解析器、编译器、代码生成器、schema |
bloge-lsp/README.md | LSP 服务器能力与架构 |
bloge-lsp/src/server.ts | LSP 服务器入口 —— 能力注册 |
bloge-lsp/src/document-manager.ts | 五阶段分析流水线(lex → parse → compile → lint → index) |
bloge-lsp/src/features/hover.ts | 关键字、内置函数、节点、schema 和持续时间的悬停文档 |
bloge-lsp/src/features/completion.ts | 上下文感知的自动补全 |
bloge-lsp/src/features/formatting.ts | 通过 DslCodeGenerator 实现的整文档格式化 |
bloge-lsp/src/lint/lint-rule.ts | LintRule 接口和 LintRunner |
bloge-vscode/src/extension.ts | VS Code 扩展入口 —— LSP 客户端接线 |
bloge-vscode/syntaxes/bloge.tmLanguage.json | 用于语法高亮的 TextMate 语法 |
bloge-intellij/README.md | IntelliJ 插件 —— 双模式架构(PSI + 可选 LSP 叠加层) |
bloge-studio/README.md | 可视化 DAG 编辑器 —— React 19 + React Flow |
bloge-studio/public/operator-metadata.json | 内置 operator 目录(名称、描述、I/O schema) |
bloge-maven-plugin/README.md | 从 @BlogeOperator 类生成 operator-metadata.json 的 Maven 插件 |
bloge-lint/README.md | Java CLI linter —— 规则、.blogerc.json 配置、退出码 |
ticket-routing.bloge | 本章通篇使用的示例 |
missing-timeout.bloge | 被 lint 捕获的反模式 |
为什么这很重要
你可以在纯文本编辑器中编写语法正确的 .bloge 文件。但没有工具链的话:
- 节点 ID 中的拼写错误 要到 graph 引擎启动时才会被发现。
- 未使用的节点 和 缺失的 timeout 会在审查中被忽略。
- 格式漂移 使 PR 充满噪音,掩盖了真正的变更。
- Operator schema 不匹配(Java 代码和 DSL 之间的不一致)在运行时
ClassCastException之前都是不可见的。
BLOGE 工具链的设计原则是 最快的反馈循环也是成本最低的:输入时红色波浪线就会出现,保存 时格式自动规范化,CLI linter 作为最终关卡在 CI 中运行。这些界面并不共享一套实现:LSP 与 Studio 走 TypeScript,runtime 解析与 CLI lint 走 Java;conformance fixture 和导出合同负责约束两条路径的一致性。
心智模型
把工具链想象成两条由兼容性证据连接的反馈路径:
关键洞察:对齐边界是共享合同,不是共享代码。 bloge-lang 提供 bloge-lsp 与 Studio 使用的 TypeScript parser/compiler;runtime 与 Maven 走 Java bloge-dsl,bloge-lint 提供 Java CI 规则;IntelliJ Community 保留 PSI/JFlex 路径,并在支持时叠加 LSP。实现可能漂移,因此一致性需要验证,不能靠假设。
第一个可运行的示例
在安装了 bloge-vscode 扩展的 VS Code 中打开
ticket-routing.bloge
示例。你应该立即看到:
-
语法高亮 ——
graph、node、branch、depends_on等关键字被着色;文档注释(///)获得文档注释作用域;像FetchCustomerOperator这样的 operator 引用被高亮为类型。 -
诊断信息 —— 如果你故意拼错一个
depends_on目标(比如把fetchCustomer改成fetchCustmer),保存之前就会出现红色下划线。 -
悬停 —— 将鼠标悬停在
timeout上,你会看到:"Sets the maximum execution time for a node."。悬停在3s上,你会看到:"Duration: 3s = 3000ms"。悬停在analyzeSentiment上,你会看到它的 operator 引用、文档注释、依赖和 timeout。 -
补全 —— 在
input { }块内,输入=,LSP 会建议ctx、所有节点 ID 以及concat、size、coalesce等内置函数。 -
格式化 —— 触发"格式化文档",整个文件会通过
DslCodeGenerator.generate(ast)重新输出,规范化缩进和空格。
以上五个功能都由 document-manager.ts 中定义的单一流水线驱动:
// Phase 1: Lexing → tokens
// Phase 2: Parsing → AST (GraphDef)
// Phase 3: Compile → CompiledGraph + diagnostics
// Phase 4: Lint → LintRunner.run(ast, compiled)
// Phase 5: Index → buildSymbolIndex(ast, uri, text)
这条五阶段流水线在 每次按键时 运行(通过 server.ts 中的 documents.onDidChangeContent),缓存的 ParseState 会被 hover、completion、definition、references 和 rename 处理器复用。在这五个阶段之后,DocumentManager 还会刷新 import graph 的簿记信息,以保持跨文件特性同步。
跟着一个 PR 走完反馈闭环
工具链的价值,在于缩短“做出编辑”到“得到可评审事实”的距离。设想一个 PR
删除了 missing-timeout.bloge 中外部调用的 timeout。真正有用的故事不是
参观编辑器功能,而是让一个缺陷连续经过四个反馈面。
一个缺陷使用一个稳定身份
| 反馈面 | 读者看到什么 | 必须交接什么 |
|---|---|---|
| 编辑器 | node 位置上的行内诊断 | ruleId=missing-timeout 与源码位置 |
| CLI lint | 可复现的非零结果 | 与 CI 相同的 rule id 和 severity |
| Scenario 测试 | 代表性 Case 的运行行为 | Case verdict 加 node/effect 证据 |
| PR 评审 | 为什么这个编辑安全 | 诊断已消失,聚焦测试已通过 |
自动补全能帮你写语法,但不能闭合这个循环。团队真正共享的合同,是稳定的 诊断身份,以及另一名开发者可以重新执行的命令。
先诊断,再决定是否运行全部测试
最短的有效序列是:
EDIT 一处声明
→ 在源码位置 DIAGNOSE
→ 使用与 CI 相同的规则集执行 LINT
→ 运行覆盖变更路径的最小 Scenario
→ 把两类结果一起交给 REVIEW
如果 lint 仍失败,就回到声明;此时运行完整集成套件只会增加噪音。如果 lint 已通过但 Scenario 失败,应检查运行证据;修改 lint 配置修不好业务行为。 只有两个聚焦信号都通过,才值得运行更大范围构建。
这里的声明边界很清楚:lint 证明声明式编写规则,Scenario 证明 fixture 条件 下的行为,任何一项单独成立都不能证明生产正确性。
拆解分析
1 — 共享语言核心:bloge-lang
bloge-lang 是一个纯 TypeScript 库,零依赖。它导出:
Lexer—— 手写的扫描器,从.bloge源码产生 token。Parser—— 递归下降解析器,带有优先级爬升的表达式解析。处理 import、graph 级别的input/outputschema、signal_schema、oneOf(...)、联合类型、?.安全导航等。compile(ast, options?)—— 前端语义分析:依赖解析、环检测、schema 校验、联合类型收窄、分支穷尽性检查。当提供了CompileOptions.importResolver时,import 会被递归解析。DslCodeGenerator.generate(ast)—— AST 到源码的格式化器。LSP 的格式化功能和 Studio 的导出都使用同一条代码路径。
三行代码即可使用:
import { Lexer, Parser, compile, DslCodeGenerator } from 'bloge-lang';
const tokens = new Lexer(source).tokenize();
const ast = new Parser(tokens).parse();
const result = compile(ast);
// result.diagnostics → 错误、警告
// DslCodeGenerator.generate(ast) → 格式化后的源码
2 — LSP:bloge-lsp
bloge-lsp 将 bloge-lang 包装在一个通过 stdio 通信的 Language Server Protocol 服务器中。
server.ts 入口注册了以下能力:
| 能力 | 处理器文件 | 功能 |
|---|---|---|
| Diagnostics | document-manager.ts | 来自 lexer + parser + compiler + lint 的实时 error/warning/info |
| Completion | features/completion.ts | 上下文感知:关键字、节点 ID、schema 类型、内置函数、oneOf(...) 和代码片段模板 |
| Hover | features/hover.ts | 关键字文档、约 70 个内置函数、node/schema/transform 声明、持续时间 |
| Go to Definition | features/definition.ts | 基于符号索引的跳转到声明的 nameRange;通过 resolveSymbolCrossFile 支持跨文件 |
| Find References | features/references.ts | 节点/schema/transform 的所有引用,包括传递 importer |
| Rename | features/rename.ts | 重命名声明 + 所有引用;传播到传递 importer;冲突检查 |
| Formatting | features/formatting.ts | 通过 DslCodeGenerator.generate(ast) 实现整文档格式化 |
| Code Actions | features/code-actions.ts | missing-timeout、missing-doc-comment、unused-node、no-duplicate-node-id 的快速修复;以及 Source Fix All 批量安全修复 |
| Semantic Tokens | features/semantic-tokens.ts | 全文档语义高亮:graph 名称、operator 引用、节点引用、路径段、字面量、关键字——用 AST 感知的 token 类细化 TextMate 作用域 |
Import 解析 对 file:// 文档是自动的 —— DocumentManager 接入了一个基于文件的 ImportResolver,相对于打开的文件解析路径,并在需要时追加 .bloge 后缀。被导入文件中的错误会作为诊断信息显示在导入文件中。
3 — Lint 规则
BLOGE lint 采用三层结构。知道一条规则属于哪一层,就知道它在哪里运行、 能看到什么上下文,以及增加新规则时要重建哪个工件。
| 层 | 在哪里运行 | 看到什么 | 例子 |
|---|---|---|---|
| Core(15 条) | bloge-lint CLI + LSP(翻译成 TS) | 单个 .bloge 文件解析并编译后的 AST | no-duplicate-node-id、no-duplicate-schema-name、unused-node、missing-timeout、excessive-fan-out、missing-doc-comment、no-cycle、no-unresolved-dependency、no-unresolved-branch-target、max-script-nodes、script-timeout-required、script-line-limit、loop-exit-completeness、cron-expression-valid、deadline-in-past |
| 编译器驱动(3 条) | 编译器自身 | 符号解析和类型检查后的整图 IR | schema-validation-strictness-mismatch、dynamic-subgraph-output-untyped、ambiguous-timer |
| 扩展(目前 2 条,可通过 SPI 扩展) | 对应 *-ext JAR 在 classpath 上时被发现 | 扩展自身的 DSL 表面 | session-backward-transition-guard(来自 bloge-session-ext)、state-machine-structure(来自 bloge-state-ext) |
Core 规则 —— LSP 子集
LSP 内置六条交互式规则
(bloge-lsp/src/lint/rules/):
| 规则 ID | 严重级别 | 捕获内容 |
|---|---|---|
no-duplicate-node-id | Error | 节点 ID 被声明了多次 |
no-duplicate-schema-name | Error | Schema 名称被声明了多次 |
unused-node | Info | 节点从未被下游引用 |
missing-timeout | Info | 节点没有 timeout |
excessive-fan-out | Info | 节点有超过 5 条出边 |
missing-doc-comment | Info | 节点缺少 /// 文档注释 |
Java 端的 CLI linter(bloge-lint)在批量
检查中包含这些核心规则,同时增加上面表里图级和脚本相关的规则,以及扩展
通过 LintExtensionContributor SPI 贡献的规则。
编译器驱动规则
这 3 条规则由编译器发出,不是 rule runner。你在 .blogerc.json 里
关不掉 —— 它们对应编译器生成合法 Graph 时必须做的结构检查。它们
在 LSP 和 CLI 里和普通规则一样,作为诊断浮出来。
扩展规则
把 bloge-session-ext 或 bloge-state-ext 加进项目就同时引入了它们的
lint 贡献。你也可以通过 LintRuleProvider(Java SPI,CLI)和
BLOGE_LSP_EXTRA_RULES(LSP)注册自定义规则。严重级别和规则参数都可在
.blogerc.json 配置:
{
"rules": {
"missing-timeout": "warning",
"missing-doc-comment": "off"
}
}
决策表规则
bloge LSP 和 CLI 都内置了两条专门针对 decision_table 节点的 lint 规则。使用以下实战配方让决策表保持完备性和意图清晰。
| 规则 ID | 严重度 | 触发条件 |
|---|---|---|
decision-table/missing-otherwise | WARNING | first、unique 或 any 表没有 otherwise 子句 |
decision-table/collect-otherwise | INFO | collect 表存在 otherwise 子句(总是追加——可能并非有意为之) |
配方 1 —— 通过添加 otherwise 消除 decision-table/missing-otherwise:
// [WARN] decision-table/missing-otherwise
decision_table credit_tier(score = ctx.score) hit=first -> String {
rule (score: score >= 750) -> "platinum"
rule (score: 680 <= score < 750) -> "gold"
// 没有 otherwise —— score < 680 时会发生什么?
}
// ✅ 修复后
decision_table credit_tier(score = ctx.score) hit=first -> String {
rule (score: score >= 750) -> "platinum"
rule (score: 680 <= score < 750) -> "gold"
otherwise -> "rejected"
}
配方 2 —— 若 decision-table/collect-otherwise 属于有意为之,显式确认:
// [INFO] decision-table/collect-otherwise
// "baseline" 在每次运行时都会被追加,即使其他规则也命中了。
decision_table discounts(score = ctx.score) hit=collect -> String {
rule (score: score >= 700) -> "loyalty"
otherwise -> "baseline" // 无条件追加——这是有意为之吗?
}
// ✅ 若 有意为之,通过 .blogerc.json 覆盖来抑制:
// { "rules": { "decision-table/collect-otherwise": "off" } }
// 若并非有意为之,删除 otherwise 子句。
完整的编译期与运行时错误码列表,请见 附录 F —— 决策表。
4 — 编辑器扩展
VS Code(bloge-vscode):
syntaxes/bloge.tmLanguage.json处的 TextMate 语法为关键字、文档注释、字符串、操作符、类型和内置函数提供语法高亮作用域。language-configuration.json处的语言配置提供括号匹配、自动闭合对、注释切换(//、/* */)和代码块折叠。- 扩展入口(
extension.ts)将bloge-lsp作为子进程通过 stdio 启动,并将其接线为bloge语言 ID 的 LSP 客户端。
IntelliJ IDEA(bloge-intellij):
- Community 版 —— 纯 PSI 模式:手写的 JFlex 词法分析器、轻量级
PsiBuilder解析器、ExternalAnnotator委托给bloge-dslParser 和bloge-lintLintRunner、结构视图、代码折叠、语义高亮。无需外部进程。 - Ultimate / 商业版 —— 可选 LSP 叠加层:
BlogeLspServerSupportProvider启动捆绑的bloge-lspNode 服务器;重叠的 PSI 提供者(diagnostics、completion、formatting)退让以避免重复结果。需要PATH上有 Node.js 或设置BLOGE_NODE_PATH。
5 — BLOGE Studio
BLOGE Studio 是一个基于浏览器的可视化 DAG 编辑器,使用 React 19 + React Flow 12 构建。它以本地 file: 依赖的方式使用 bloge-lang,复用相同的解析器、编译器和代码生成器。
与你的编写工作流相关的关键功能:
- 三种视图模式:Visual(仅画布)、Code(仅 Monaco 编辑器)、Split(画布 + 编辑器并排)。Zustand store 在它们之间同步。
- Operator 面板:从
public/operator-metadata.json加载。Operator 按层级(基础设施 / 能力 / 领域)标记,支持搜索。拖到画布上即可创建节点。 - 诊断面板:
DiagnosticsPanel.tsx—— 显示来自编译器和 lint 规则的 error、warning 和 info。点击一条诊断信息可以在画布上选中对应的节点。 - 撤销/重做:基于 DSL 快照的历史记录(最多 50 步)。
- 导出:校验 → 运行代码生成器 → 下载
.bloge文件。
6 — Operator 元数据流水线
你的 Java operator 代码与可视化工具之间的桥梁是
bloge-maven-plugin:
该插件在 process-classes 阶段扫描带有 @BlogeOperator 注解的类,反射 Operator<I,O> 的泛型参数,对每个类型调用 SchemaIntrospector.introspect()(支持 Record 和 POJO),并写入一个 JSON 文件。Studio 加载此文件以填充 operator 面板,并为字段补全提供 I/O schema 信息。
内嵌运维控制台 —— /bloge-console
Studio 适合用来编排图。生产环境需要的是另一种视角:"现在哪些 execution
在跑、下一个 timer 什么时候触发、哪个 operator 在抛错。"这个视角就是
/bloge-console —— 一个随 bloge-spring-web 一起发布的小型只读
运维 UI,挂在配置的 base path 上。
spring:
bloge:
console:
enabled: true
base-path: /bloge-console # 默认
require-role: BLOGE_OPS # Spring Security 角色门禁
它展示什么:
- Executions —— 运行中 / 等待中 / 已完成 / 已失败;点进去看单次 run 的审计日志。
- Timers —— 当前已 arm 的持久化 timer 及下次触发时间;支持手动
fire now(受
require-role限制)。 - Leases —— 当前持有者及剩余 TTL。
- Schema —— Flyway 的
currentVersion、pending/applied migrations,以及UP_TO_DATE、PENDING或FAILED状态。 - Operator registry —— 所有注册的 operator、所在模块,以及
@BlogeOperator元数据,便于快速发现。
控制台设计上是观测用途。它不让你编辑图(那是 Studio 的事),也不让 你改写 checkpoint(那要走迁移)。"fire timer" 和 "release lease" 是显式的 运维恢复工具,每个动作都以调用者主体写入审计日志。
小测验: 什么时候用
/bloge-console,什么时候用 Studio?提示: Studio 写图,控制台读运行时。
常见陷阱
❌ 假设编辑器和 CI 检查的是相同的规则
LSP 的 lint 规则(TypeScript,在 bloge-lsp 中)和 CLI 的 lint 规则(Java,在 bloge-lint 中)是 两套独立的实现,规则集有重叠但并不完全相同。LSP 有 unused-node 和 excessive-fan-out;CLI 额外覆盖了 no-unresolved-dependency、no-unresolved-branch-target、no-cycle 以及若干脚本 规则。如果你只依赖编辑器中的绿色波浪线,CI 仍然可能在 LSP 没有检查的规则上失败。
# 推送前在本地运行 Java linter:
java -jar bloge-lint.jar check src/main/resources/bloge/
# 或者在构建中配置:
# .blogerc.json 控制编辑器和 CLI 使用时的严重级别覆盖。
最佳实践:将 bloge-lint check 作为 pre-commit hook 或 CI 步骤运行,以确保在合并前应用完整的规则集。
引导式重写
拿
missing-timeout.bloge
反模式文件,使用工具链来修复它:
-
在 VS Code 中打开(需要安装
bloge-vscode扩展)。你应该在loadReport上看到一条来自missing-timeoutlint 规则的 info 诊断。 -
悬停 在
loadReport上,确认悬停弹窗中没有列出 timeout。 -
添加 timeout:
node loadReport : LoadReportOperator {input {reportId = ctx.reportId}timeout = 5s} -
格式化文档(VS Code 中按 Shift+Alt+F)。
DslCodeGenerator会规范化缩进。 -
在
loadReport上方添加///文档注释。确认missing-doc-comment诊断消失。 -
对该文件运行 CLI linter:
java -jar bloge-lint.jar check src/main/resources/bloge/antipatterns/missing-timeout.bloge添加了 timeout 和文档注释后,输出应报告零个错误。
-
在 BLOGE Studio 中打开同一文件(在
bloge-studio/中运行npm run dev,然后使用工具栏中的"Open")。切换到 Split 视图,观察画布和 DSL 代码同步。检查底部的诊断面板 —— 应该是空的。
思维检查
-
LSP 的格式化功能实际委托给了哪个模块? (
bloge-lang中的DslCodeGenerator。参见formatting.ts—— 它调用的是DslCodeGenerator.generate(state.ast)。) -
LSP 是如何检测到
depends_on目标拼写错误的? (bloge-lang中的compile(ast)运行依赖解析。无法解析的依赖会产生一个error级别的诊断信息,DocumentManager将其转换为 LSPDiagnosticSeverity.Error。) -
在 IntelliJ 插件中,当 IDE 是 Community 版且没有安装 Node.js 时会怎样? (插件以纯 PSI 模式运行 —— 所有功能由内置的 JFlex 词法分析器、PsiBuilder 解析器和 ExternalAnnotator 提供。LSP 叠加层不会激活,因为
com.intellij.modules.lsp不存在。) -
BLOGE Studio 是如何知道在面板中显示哪些 operator 的? (它在启动时加载
public/operator-metadata.json。该文件可以由bloge-maven-plugin的export-metadatagoal 生成,也可以手动编写。) -
能否在 CLI 中禁用某条 lint 规则?如何操作? (可以 —— 创建一个
.blogerc.json文件,包含rulesmap。将规则 ID 设为"off"。例如:{ "rules": { "missing-doc-comment": "off" } }。参见LintConfig.java。)
实验
-
编辑器设置:安装
bloge-vscode扩展(或在bloge-intellij/中运行./gradlew runIde启动 IntelliJ 插件沙箱)。打开ticket-routing.bloge。 验证:- 语法高亮覆盖了所有关键字和文档注释。
- 悬停在
analyzeSentiment上会显示其 operator、描述、依赖和 timeout。 - 在
depends_on列表中 Ctrl+点击fetchCustomer可以跳转到节点声明。 - 重命名
classifyPriority会更新分支条件和所有depends_on引用。
-
故意破坏一个 graph:在
ticket-routing.bloge中,将classifyPriority上的depends_on = [analyzeSentiment]改为depends_on = [nonExistentNode]。观察出现的诊断信息。然后对该文件运行bloge-lint check,对比 CLI 输出和编辑器波浪线。 -
Operator 元数据往返:如果你有一个包含
@BlogeOperator类的 Maven 项目,运行mvn bloge:export-metadata,将生成的operator-metadata.json复制到bloge-studio/public/中。启动 Studio 并确认你的 operator 出现在面板中,且 I/O schema 正确。 -
Studio 分屏视图:在 BLOGE Studio 中打开
ticket-routing.bloge。 切换到 Split 视图。在代码编辑器中添加一个新节点,观察它出现在画布上。删除它,然后使用撤销(Ctrl+Z)恢复。