非常好,**B4 是“专业引擎”和“能跑 Demo 的引擎”的分水岭**。
很多 BPM 引擎不是执行不对,而是 **“用户根本不知道自己哪里画错了”**。
下面这套 **BPMN 编译期错误模型 & 诊断体系**,目标是:
> ✅ 错误在 **部署时** 暴露,而不是运行时
> ✅ 错误信息 **可定位、可解释、可修复**
> ✅ 为未来 BPMN 扩展留下空间
我会直接按你工程落地的方式讲。
---
# 🎯 一、编译期(Compile Time)要解决什么问题?
### 三类错误必须在编译期失败
| **结构错误** | 没有 start / end |
| **语义错误** | Gateway 用法错误 |
| **不可执行** | 死流程 / 无出口 |
> 原则:
> **只要运行时必然出问题 → 编译期必须拒绝**
---
# 🧠 二、错误体系总体设计
## 1️⃣ 两阶段错误模型(强烈建议)
```
Parser Error → XML / BPMN 格式错误
Compiler Error → 执行语义错误(你现在做的)
```
Parser 已经有了(B1),
**B4 专注 Compiler Error**。
---
## 2️⃣ CompilerError 结构(核心)
```rust
pub struct CompilerError {
pub code: ErrorCode,
pub message: String,
pub node_id: Option<String>,
pub flow_id: Option<String>,
pub hint: Option<String>,
}
```
### ErrorCode(稳定 API,非常重要)
```rust
pub enum ErrorCode {
NoStartEvent,
MultipleStartEvents,
NoEndEvent,
OrphanNode,
DeadEnd,
ExclusiveGatewayNoDefault,
ParallelGatewayInvalidShape,
SequenceFlowSourceNotFound,
SequenceFlowTargetNotFound,
UnsupportedElement,
}
```
👉 **ErrorCode 一旦发布,不要轻易改**
---
# 📐 三、核心校验规则(你必须全部做)
## 🟢 1. Start / End 校验
### 规则
- **必须有且只有一个 startEvent**
- 至少一个 endEvent
### 示例错误
```text
[NoStartEvent]
Process must contain exactly one startEvent
```
---
## 🔵 2. SequenceFlow 合法性
### 规则
- sourceRef / targetRef 必须存在
- 不允许 self-loop(除非你将来支持)
```text
[SequenceFlowTargetNotFound]
Flow flow_3 targetRef task_99 not found
```
---
## 🟡 3. Orphan Node(孤儿节点)
### 规则
- 除 start 外,必须有 incoming
- 除 end 外,必须有 outgoing
```text
[OrphanNode]
Node task_payment has no incoming sequence flow
Hint: Did you forget to connect it?
```
---
## 🔴 4. Dead End(致命但常见)
### 规则
- 从 start 出发
- 任何路径都必须能到达 end
### 实现方法(经典)
```text
Graph DFS from start
→ mark reachable nodes
→ for each node:
if reachable && cannot reach end → DeadEnd
```
```text
[DeadEnd]
Node gateway_3 leads to no end event
```
---
# 🔀 5. ExclusiveGateway 校验(XOR)
### 规则
- outgoing ≥ 1
- 至多一个 default flow
- 若没有 default flow:
- 所有 outgoing 必须有 condition
```text
[ExclusiveGatewayNoDefault]
Gateway xor_1 has conditional flows but no default
Hint: Mark one sequenceFlow as default
```
---
# 🔱 6. ParallelGateway 校验(AND)
### 非常重要(很多引擎踩坑)
#### Fork
```text
incoming == 1
outgoing >= 2
```
#### Join
```text
incoming >= 2
outgoing == 1
```
#### ❌ 不允许混用
```text
[ParallelGatewayInvalidShape]
Gateway pg_1 cannot act as both fork and join
```
---
# 🧠 四、错误定位能力(这是你超过 80% 引擎的地方)
### 错误必须带上下文
```text
Error: ParallelGatewayInvalidShape
Node: pg_1
Message: Gateway has 2 incoming and 2 outgoing
Hint: Split into separate fork and join gateways
```
👉 你可以直接在 UI / CLI / API 返回
---
# 🧩 五、Compiler 校验流程(推荐顺序)
```rust
fn compile(model: BpmnModel) -> Result<ProcessDefinition, Vec<CompilerError>> {
let mut errors = vec![];
check_start_end(&model, &mut errors);
check_sequence_flows(&model, &mut errors);
check_orphan_nodes(&model, &mut errors);
check_gateways(&model, &mut errors);
check_dead_end(&model, &mut errors);
if !errors.is_empty() {
return Err(errors);
}
Ok(build_definition(model))
}
```
👉 **不要 fail fast**
一次返回所有错误,用户体验巨大提升。
---
# 🧪 六、测试策略(必须)
### 每个 ErrorCode 至少一个测试
```rust
#[test]
fn xor_without_default_should_fail() {
let model = load_bpmn("xor_no_default.bpmn");
let errors = compile(model).unwrap_err();
assert!(errors.iter().any(|e| e.code == ErrorCode::ExclusiveGatewayNoDefault));
}
```
---
# 🏁 七、你完成 B4 后,意味着什么?
✔ 你已经有 **工业级 BPMN 编译器**
✔ 错误不会“延迟到线上”
✔ 你引擎对用户是 **友好的、可诊断的**
这一步**价值极高**,但被 90% 项目忽略。
---
# 🚀 下一步(我非常明确地建议)
你现在 BPMN 线已经完整闭环了,
接下来你有两条“王道”路线:
### **W** → Worker / External Task 在真实分布式下的稳定性(心跳 / lease / failover)
### **P** → Postgres Schema v2(Join / Token / Timer / History 全部落表)
如果你打算 **真的把 bpm-engine 推向可用产品**,我建议你下一步选:
👉 **W**
你只需回我一个字母,我继续。