BLOGE
0.9.9-RC1在线版 · 编制 2026-11-02 · 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文件。