@floken-io/engine 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1948 @@
1
+ import { A as ActionRecord, I as InstanceStateHeader, b as TaskDelta, c as Token, d as InstanceState, T as TaskView, V as VoteOutcome, e as InstanceParent, E as EngineEvent, f as TimeoutSpec, g as TraceEntry, h as InstanceStatus, i as EventSink, j as InstanceEventName, S as StateStore, D as DefinitionSource, a as TaskProjection, k as ApproverSource, l as ServiceHandler, m as AuthResolver, F as FormProvider, C as ConditionHandler, n as DecisionHandler, o as Scheduler, p as ConditionCtx } from './spi-BABH0Tcs.js';
2
+ export { q as ApproverCtx, r as AuditEntry, s as DecisionCtx, t as ENGINE_EVENT_NAMES, u as EngineEventName, v as FormCtx, w as INSTANCE_EVENT_NAMES, x as INSTANCE_STATUSES, y as InstanceEvent, z as InstanceStateBody, B as SPI_GROUPS, G as SPI_NAMES, H as STATE_SCHEMA_VERSION, J as ScheduleRequest, K as ServiceCtx, L as ServiceHandlerFn, M as SpiInterfaces, N as SpiName, O as TASK_EVENT_NAMES, P as TASK_STATUSES, Q as TERMINAL_STATUSES, R as TOKEN_STATES, U as TaskEvent, W as TaskEventName, X as TaskStatus, Y as TokenAwait, Z as TokenState, _ as isInstanceEvent, $ as isTaskEvent, a0 as isTerminalStatus, a1 as subjectTokenOf } from './spi-BABH0Tcs.js';
3
+ import { NormalizedApproval, ApprovalMode, OnReject, TimeoutAction, NormalizedTimeout } from '@floken-io/moddle';
4
+
5
+ /**
6
+ * @floken-io/engine · 错误与诊断契约(engine 侧实现)
7
+ *
8
+ * 五包通用的错误处理契约见仓库根 `AGENTS.md` §5「错误处理契约」。本档落实 engine 这一侧:
9
+ *
10
+ * 1. **两条通道不许混**:`EngineError` 系(抛出,调用方无法继续) vs `Diagnostic`(随结果返回,可继续)。
11
+ * 口诀:「重试也救不回来」→ 抛;「换个输入还有救」→ 诊断。
12
+ * 2. **结构契约,不共享基类**:五包各自独立仓,**禁止跨包 import 错误基类**;
13
+ * 改为逐字约定字段形状(`name` / `code` / `pkg` / `node` / `instanceId` / `hint` / `details`)。
14
+ * 3. **错误码是稳定契约**:一旦发布不得改名(性质同 XML 前缀),只能新增。
15
+ * 命名规则 `<域>_<类别>_<对象>`,全大写蛇形,域 = `ENGINE`(本包短名)。
16
+ * 4. **message 面向人、不含易变数据**:计数 / id / 名字一律进 `details`,否则宿主断言会碎。
17
+ *
18
+ * 参考实现:`项目文件/floken-feel/src/core/errors.ts`(形状逐字对齐,基类各自实现)。
19
+ */
20
+ /**
21
+ * 模型定位:engine / moddle / dmn 用(`path` 形如 `a.b[0].c`)。
22
+ * engine 侧另附 `EngineError.instanceId`(`AGENTS.md` §5.4 定位双轨)。
23
+ */
24
+ interface NodeRef {
25
+ id?: string;
26
+ path?: string;
27
+ }
28
+ /**
29
+ * 抛出类错误的码表(`EngineError` 家族)。
30
+ *
31
+ * ★ 类别只允许四个:`ACTION_`(动作受理)/ `STATE_`(实例状态与不变量)/
32
+ * `PERSIST_`(StateStore 写入冲突)/ `OPTION_`(createEngine 配置)。
33
+ */
34
+ declare const ENGINE_ERROR_CODES: {
35
+ /** 动作名不在 19 项之内 */
36
+ readonly ACTION_UNKNOWN: "ENGINE_ACTION_UNKNOWN";
37
+ /** 设计期开关未开启(`approval.X.allowed === false`,DV-2) */
38
+ readonly ACTION_NOT_ALLOWED: "ENGINE_ACTION_NOT_ALLOWED";
39
+ /**
40
+ * 门 1 `beforeAction` 返回 `false` 否决了本次动作(T12)。
41
+ *
42
+ * ★ 与 `ACTION_NOT_ALLOWED` 的区别:后者是**设计期**开关(读定义就知道),
43
+ * 本码是**运行期**宿主否决(只有跑起来才知道)。合成一个码的话,宿主分不清
44
+ * 「按钮本来就不该显示」与「业务条件不满足」—— 前者是前端 bug,后者要提示用户。
45
+ * ⚠️ 否决**必须**抛错:静默返回空差分会让用户以为办完了(红线:不得静默无效果)。
46
+ */
47
+ readonly ACTION_VETOED: "ENGINE_ACTION_VETOED";
48
+ /** 驳回 / 退回目标非法(INV-6:须同时满足 ∈ `completedNodes` 且 ∈ `allowedTargets`) */
49
+ readonly ACTION_TARGET_INVALID: "ENGINE_ACTION_TARGET_INVALID";
50
+ /** `requireComment` 为 true 但未填意见(DV-3) */
51
+ readonly ACTION_COMMENT_REQUIRED: "ENGINE_ACTION_COMMENT_REQUIRED";
52
+ /** 审批人解析为空集且 `onEmpty === 'error'`(INV-13,不得产生 0 办待人的 active 节点) */
53
+ readonly ACTION_APPROVER_EMPTY: "ENGINE_ACTION_APPROVER_EMPTY";
54
+ /** 加签超出设计期 `addSign.maxCount`(INV-12) */
55
+ readonly ACTION_ADD_SIGN_LIMIT: "ENGINE_ACTION_ADD_SIGN_LIMIT";
56
+ /** 票签配置非法(INV-7:`mode:'vote'` ⟺ `vote` 存在,且 `count` / `threshold` 恰有其一) */
57
+ readonly ACTION_VOTE_CONFIG: "ENGINE_ACTION_VOTE_CONFIG";
58
+ /** `StateStore.load()` 返回 null */
59
+ readonly STATE_NOT_FOUND: "ENGINE_STATE_NOT_FOUND";
60
+ /** 实例已终态(completed / terminated / cancelled)后仍尝试推进(INV-2) */
61
+ readonly STATE_TERMINAL: "ENGINE_STATE_TERMINAL";
62
+ /** 实例处于 suspended,除 `resume` 外一律不受理(INV-5) */
63
+ readonly STATE_SUSPENDED: "ENGINE_STATE_SUSPENDED";
64
+ /** 状态结构不合契约(含 AC-E8 / INV-14:出现函数 / Map / Set / 类实例) */
65
+ readonly STATE_SHAPE_INVALID: "ENGINE_STATE_SHAPE_INVALID";
66
+ /** `tokens[].nodeId` 不在该实例**绑定版本**的定义图中(INV-3,不得静默忽略) */
67
+ readonly STATE_TOKEN_ORPHAN: "ENGINE_STATE_TOKEN_ORPHAN";
68
+ /** `DefinitionSource.getDefinition()` 返回 null(AC-E10 要求按实例绑定版本取定义) */
69
+ readonly STATE_DEFINITION_MISSING: "ENGINE_STATE_DEFINITION_MISSING";
70
+ /** 快照结构版本无迁移路径(`stateSchema` 只升不降;升级须登记迁移函数) */
71
+ readonly STATE_SCHEMA_UNSUPPORTED: "ENGINE_STATE_SCHEMA_UNSUPPORTED";
72
+ /** CAS UPDATE 影响 0 行:`expectedRev` 与库中当前 rev 不符(INV-1) */
73
+ readonly PERSIST_CONFLICT: "ENGINE_PERSIST_CONFLICT";
74
+ /** INSERT 冲突:`expectedRev === 0` 但该 `instanceId` 已存在 */
75
+ readonly PERSIST_ALREADY_EXISTS: "ENGINE_PERSIST_ALREADY_EXISTS";
76
+ /** 未知配置项(**禁止静默忽略**,与 feel 的 `FEEL_OPTION_UNKNOWN` 同口径) */
77
+ readonly OPTION_UNKNOWN: "ENGINE_OPTION_UNKNOWN";
78
+ /** 配置项取值非法(如 `maxAuditEntries` 非正整数) */
79
+ readonly OPTION_INVALID: "ENGINE_OPTION_INVALID";
80
+ };
81
+ /**
82
+ * 诊断码表(**不抛**,随结果返回)。
83
+ *
84
+ * ★ 命名空间隔离规则:诊断码**不得使用抛出码的四个类别**
85
+ * (`ACTION_` / `STATE_` / `PERSIST_` / `OPTION_`)—— 由 `test/errors.test.ts` 的
86
+ * 「双命名空间不重叠」断言守(`AGENTS.md` §5.3 两条硬约束之二)。
87
+ *
88
+ * engine 产生诊断的场合只有「**已经发生、流程可以继续、但宿主应当知道**」的观测;
89
+ * 上游(`@floken-io/feel` 求值降级 / `@floken-io/moddle` 校验)的诊断**原样透传、不重新包装**。
90
+ */
91
+ declare const ENGINE_DIAGNOSTIC_CODES: {
92
+ /** INV-18:`pendingProjectionRev` 存在 —— 该 rev 的投影尚未追平,`load()` 会先 `sync()` 补做 */
93
+ readonly EFFECT_PENDING: "ENGINE_EFFECT_PENDING";
94
+ /** INV-17:`auditTrail` 达 `maxAuditEntries` 上限已裁剪;溢出区间记在 `details.dropped*`,**未静默丢弃**
95
+ * ⚠️ engine **不**把溢出条目投 `EventSink`:事件集由 ADR-006 定死为 10 个,审计不走事件通道
96
+ * (审计主源是 `auditTrail` 本身;被裁掉的部分宿主应从 `diagnostics` 转存到自己的归档) */
97
+ readonly AUDIT_TRUNCATED: "ENGINE_AUDIT_TRUNCATED";
98
+ };
99
+ type EngineErrorCode = (typeof ENGINE_ERROR_CODES)[keyof typeof ENGINE_ERROR_CODES];
100
+ type EngineDiagnosticCode = (typeof ENGINE_DIAGNOSTIC_CODES)[keyof typeof ENGINE_DIAGNOSTIC_CODES];
101
+ /**
102
+ * 诊断的严重级别(与 `@floken-io/feel` / `@floken-io/dmn` 的 `Diagnostic` 同口径)。
103
+ *
104
+ * engine 侧目前只用 `warn`(**已发生、流程继续、但宿主应当知道**);
105
+ * `error` / `info` 保留给后续可能的设计期校验与观测类诊断。
106
+ */
107
+ type EngineSeverity = 'error' | 'warn' | 'info';
108
+ /**
109
+ * 诊断条目。
110
+ *
111
+ * ★ 与兄弟包 `Diagnostic` 的两处**刻意差异**:
112
+ * ① 无 `start` / `end` —— 那是**源码文本**偏移,engine 处理的是对象树,没有文本可指;
113
+ * ② 定位改用 `node?: NodeRef` + `instanceId?`(`AGENTS.md` §5.4 的「定位双轨」)。
114
+ */
115
+ interface EngineDiagnostic {
116
+ severity: EngineSeverity;
117
+ code: string;
118
+ message: string;
119
+ node?: NodeRef;
120
+ instanceId?: string;
121
+ details?: Record<string, unknown>;
122
+ }
123
+ interface EngineDiagnosticInit {
124
+ code: EngineDiagnosticCode | (string & {});
125
+ message: string;
126
+ severity?: EngineSeverity;
127
+ node?: NodeRef;
128
+ instanceId?: string;
129
+ details?: Record<string, unknown>;
130
+ }
131
+ /**
132
+ * 构造一条诊断(缺省 `severity: 'warn'`)。
133
+ *
134
+ * ⚠️ 可选字段一律**条件展开**(开了 `exactOptionalPropertyTypes`,显式赋 `undefined` 不合法),
135
+ * 且诊断必须保持**纯数据**(会随 `PlanResult` 一起进 JSON 序列化)。
136
+ */
137
+ declare function engineDiagnostic(init: EngineDiagnosticInit): EngineDiagnostic;
138
+ interface EngineErrorInit {
139
+ code: string;
140
+ /** 模型定位(定义图中的元素 / 路径) */
141
+ node?: NodeRef;
142
+ /** engine 专属定位:出问题的流程实例 */
143
+ instanceId?: string;
144
+ /** 一句修复提示(人读;照着做就能解决) */
145
+ hint?: string;
146
+ /** 结构化补充:计数、名字、合法取值等**可断言**的数据都放这里 */
147
+ details?: Record<string, unknown>;
148
+ }
149
+ /**
150
+ * engine 错误基类。
151
+ * ⚠️ 本类**不出现在任何跨包依赖里**:其余四包各有自己的基类,只保证字段形状一致。
152
+ */
153
+ declare class EngineError extends Error {
154
+ /** 五包统一印记:宿主可据此判断「这是 floken 的结构化错误」 */
155
+ readonly floken = true;
156
+ readonly pkg = "engine";
157
+ readonly code: string;
158
+ readonly node?: NodeRef;
159
+ readonly instanceId?: string;
160
+ readonly hint?: string;
161
+ readonly details?: Record<string, unknown>;
162
+ constructor(message: string, init: EngineErrorInit);
163
+ }
164
+ /** 动作层:19 项动作的受理被拒(未开启 / 未知 / 目标非法 / 意见缺失 / 加签超限 …) */
165
+ declare class EngineActionError extends EngineError {
166
+ }
167
+ /** 状态层:实例定位失败、生命周期冲突、结构不变量被破坏 */
168
+ declare class EngineStateError extends EngineError {
169
+ }
170
+ /** 持久层:`StateStore.save()` 的 INSERT 或 CAS 冲突 */
171
+ declare class EnginePersistError extends EngineError {
172
+ }
173
+ /** 选项层:`createEngine()` 的配置非法(**禁止静默忽略**) */
174
+ declare class EngineOptionError extends EngineError {
175
+ }
176
+ /**
177
+ * CAS 冲突(INV-1)。
178
+ * ★ 宿主实现 `StateStore` 时:`save()` 必须靠**影响行数**判定,不得"先查后写"(那是竞态)。
179
+ */
180
+ declare function persistConflict(instanceId: string, expectedRev: number, actualRev?: number): EnginePersistError;
181
+ /** INSERT 冲突:`expectedRev === 0` 但实例已存在 */
182
+ declare function persistAlreadyExists(instanceId: string): EnginePersistError;
183
+
184
+ /**
185
+ * @floken-io/engine · 门 1 `hooks`(同步、阻塞的动作级钩子)
186
+ *
187
+ * 契约来源:`ARCHITECTURE.md` §7.3 / ADR-004。
188
+ *
189
+ * ★ **选门 1 还是门 2 的判据只有一句:这个写失败了,流程还能不能继续?**
190
+ *
191
+ * | 场景 | 失败后果 | 走哪条 |
192
+ * |---|---|---|
193
+ * | 驳回后发消息、写埋点、同步搜索索引 | 能继续 | **门 1**(本文件)/ `EventSink` —— 最终一致 |
194
+ * | 票签更新计票表、驳回改业务主表状态 | **流程会走错分支** | **门 2** `plan()` 自编排(同一事务) |
195
+ *
196
+ * ⚠️ 与 `EventSink` 的区别(两张网,别混):本线是**动作级**(一次 `submit()` 恰一对
197
+ * `before`/`after`)、**同步阻塞**、失败不吞;`EventSink` 是**事件级**(一次可能多条)、
198
+ * **异步不阻塞**、丢了不影响流程。
199
+ */
200
+
201
+ /**
202
+ * 钩子上下文。
203
+ * ★ `action` 与 `delta.action` **同源**(引擎只造一份 `ActionRecord`)——
204
+ * 宿主据此把「通过了一步」与「被驳回了」路由到不同处理。
205
+ */
206
+ interface ActionContext {
207
+ /** 动作事实(与 `delta.action` 同源) */
208
+ readonly action: ActionRecord;
209
+ /** 动作**前**的状态快照(只读;Header 已够路由,需要体请用 `plan()` 或 `load()`) */
210
+ readonly state: InstanceStateHeader;
211
+ /** `save()` **之后**的状态快照 */
212
+ readonly next: InstanceStateHeader;
213
+ readonly delta: TaskDelta;
214
+ }
215
+ interface EngineHooks {
216
+ /**
217
+ * 写入**前**调用,**可否决**:返回 `false` 或抛错 → 中止本次动作(不写库)。
218
+ *
219
+ * ⚠️ **只可读**:不允许改写 `ctx.action` / `ctx.state`。
220
+ * 想改行为请改定义或改 `ActionInput` 后重新提交 —— 允许就地改写会让
221
+ * 「走 `submit()` 和走 `plan()` 得到不同 `next`」成为可能,直接破坏两条路径一致性。
222
+ *
223
+ * 实现手段见 `freezeActionContext()`:**深拷贝 + 深冻结**,改写在严格模式下直接抛 `TypeError`。
224
+ *
225
+ * @returns `false` = 否决(引擎抛 `ENGINE_ACTION_VETOED`);其余返回值视为放行。
226
+ * 需要带原因时请**抛自己的错误**(会原样冒泡),别只 `return false`。
227
+ */
228
+ beforeAction?(ctx: ActionContext): boolean | void | Promise<boolean | void>;
229
+ /**
230
+ * `save()` **之后**、引擎 `await` 它;**失败不吞**,按可重试上报。
231
+ *
232
+ * 语义 = **至少一次投递,宿主必须幂等**(与 `EventSink` 同一保证,但同步、且按
233
+ * `ctx.action.name` 路由到 19 项动作名)。
234
+ *
235
+ * ⚠️ 抛错时**状态已落库** —— 宿主会看到「`submit()` 失败但流程其实走完了」。
236
+ * 这不是 bug,是「至少一次」的代价:重试时请先查状态,别盲目重放。
237
+ */
238
+ afterAction?(ctx: ActionContext): void | Promise<void>;
239
+ }
240
+
241
+ /**
242
+ * @floken-io/engine · 10 个内核原语(**业务无知**)
243
+ *
244
+ * ★ 分层的红线(`03` §3 / `ARCHITECTURE.md` §8 ADR-001):
245
+ * **内核只认识这 10 个操作,不认识"驳回"** —— 本文件里出现 `if (action === 'reject')` 即视为分层已破。
246
+ * 新增一个中国式审批动作时**只改 `actions/`**;若需要改本文件,说明那个动作被错误地实现成了原语。
247
+ *
248
+ * ★ 原语的自我定位(与 `runtime/plan.ts` 的分工):
249
+ * - 原语 = **状态变换**:只动 `tokens` / `completedNodes` / `status`;
250
+ * - `plan()` = **一次提交的完整演化**:`rev` / `updatedAt` / `lastAction` / `auditTrail` / `delta`。
251
+ * ⇒ 本文件**不碰** `rev`、时间、审计 —— 否则与 `plan()` 重复记账(INV-4:一条 seq 对应一次变更)。
252
+ * ★ **原语级审计已否决**(**D-23 / D-87**):原语**不进** `auditTrail`,
253
+ * 轨迹只在**动作级**记一行(`from` / `to` / `tokenId` 由 `plan()` 填)。
254
+ * 否决的两条理由:① run-to-wait 的令牌推进**不走 `advance` 原语**(`runtime/loop.ts`
255
+ * 直接改 `token.nodeId`),按原语记出来的"轨迹"里没有令牌移动 —— 恰是最该有的那一半;
256
+ * ② 一次提交炸出几十条会把 `maxAuditEntries` 的「保留最近 N **次变更**」
257
+ * 扭曲成「保留最近两次提交」,且裁剪会砍在一次提交的内部。
258
+ *
259
+ * ★ 纯函数性同 `plan()`:不读时钟、不碰存储、不改入参(先 `cloneState`)。
260
+ * 令牌 id 由入参 `groupId` **确定性生成**(`${groupId}#${i}`)—— 纯函数不能用随机数 / 计数器。
261
+ *
262
+ * ⚠️ **错误码的归类裁决(D-17)**:原语的「语义前置条件不满足」(如 `jumpTo` 目标不是已完成节点、
263
+ * `spawnInstances` 传入空办理人、`resume` 用在非挂起实例)一律抛 **`ENGINE_STATE_SHAPE_INVALID`** ——
264
+ * 四族里没有更贴切的类别:`ACTION_` 是动作受理语义(归 T9 `gates.ts`)、`PERSIST_` 是存储、`OPTION_` 是配置。
265
+ * 业务层的 `allowedTargets` 校验(INV-6 ②)**仍归 `actions/gates.ts`**,两层判据不同:
266
+ * **原语保证状态自洽,gates 保证符合设计期配置**。
267
+ */
268
+
269
+ /**
270
+ * 10 个原语名,**顺序即契约**(分组扁平化后须与本表逐项一致,有测试守)。
271
+ *
272
+ * 与 `SPI_NAMES` 同款:凡能算出的数字不手列。
273
+ */
274
+ declare const PRIMITIVE_NAMES: readonly ["advance", "jumpTo", "rollbackTo", "spawnInstances", "cancelInstances", "transfer", "delegate", "halt", "suspend", "resume"];
275
+ /**
276
+ * 分组:**8 令牌级 + 2 实例级**(`03` §3 的口径)。
277
+ *
278
+ * `halt` 会置实例终态,但它属于「清场」动作而非"冻结/恢复"控制,故归令牌级;
279
+ * 实例级只有 `suspend` / `resume` 这对(可恢复的冻结,`03` §162 明确区分于 `halt` 的不可逆清理)。
280
+ */
281
+ declare const PRIMITIVE_GROUPS: {
282
+ readonly token: readonly ["advance", "jumpTo", "rollbackTo", "spawnInstances", "cancelInstances", "transfer", "delegate", "halt"];
283
+ readonly instance: readonly ["suspend", "resume"];
284
+ };
285
+ type PrimitiveName = (typeof PRIMITIVE_NAMES)[number];
286
+ interface AdvanceInput {
287
+ tokenId: string;
288
+ /** 目标节点(**出向流的另一端**,由 `runtime/loop.ts` 结合定义图算出 —— 原语不认识图) */
289
+ to: string;
290
+ }
291
+ interface JumpToInput {
292
+ tokenId: string;
293
+ /** 目标节点,**必须 ∈ `completedNodes`**(`03` §166:驳回 = 回到已完成的节点,不是走另一条线) */
294
+ to: string;
295
+ }
296
+ interface RollbackToInput {
297
+ tokenId: string;
298
+ /** 回滚目标,**必须 ∈ `completedNodes`**;其后完成的节点与下游在途令牌一并撤销 */
299
+ to: string;
300
+ /**
301
+ * ★ **撤销范围收缩到本分支**(T16 · **D-47**):只取消 `branch` 相同的在途令牌。
302
+ *
303
+ * 不传 = 取消**全部**其它在途令牌(单分支流程的既有行为,保持不变)。
304
+ *
305
+ * 为什么必须有它:并行分支下,"撤销下游"若仍按全局取消,A 分支上点一次"撤销"
306
+ * 会把 B 分支上毫不相干的待办一起取消 —— 而 B 分支的人正办着,
307
+ * 表现为"我的待办凭空消失了",且**没有任何报错**可循(这是最难查的一类误伤)。
308
+ *
309
+ * ⚠️ 判据用**相等**而不是"前缀匹配":嵌套并行下子分支的 `branch` 是父分支值的延伸,
310
+ * 按前缀匹配会把兄弟分支也算进来 —— 那就等于没收缩。
311
+ */
312
+ branch?: string;
313
+ }
314
+ interface SpawnInstancesInput {
315
+ nodeId: string;
316
+ /** 实例组(会签 / 加签的归组键);令牌 id 由它确定性生成 */
317
+ groupId: string;
318
+ /** 办理人列表;**不得为空**(INV-13:不得产生 0 办待人却 active 的节点) */
319
+ assignees: readonly string[];
320
+ /** 被展开取代的令牌(会签展开时那一个"占位令牌");不给则只新增 */
321
+ replaceTokenId?: string;
322
+ /**
323
+ * 新令牌是否带 `instanceGroup`(即**是否参与汇聚**)。缺省 `true`。
324
+ *
325
+ * ★ 为什么需要它:会签三项是"**取代**占位令牌的展开"—— 那一组人**就是**这个节点的全部办理人,
326
+ * 必须建组才能汇聚;加签是"**新增**一个人",原令牌还在原地且**不在组内**,
327
+ * 若也给新令牌打组,就会得到一个「只含加签来的人」的组 —— 那个人一通过,
328
+ * 汇聚判据满足,流程被他一个人推走,原办理人的待办变成幽灵待办(且无任何报错)。
329
+ *
330
+ * ⚠️ 加签的汇聚语义尚未定型(**D-33**),故加签**不建组**:宁可不汇聚,也不能静默错汇聚。
331
+ */
332
+ grouped?: boolean;
333
+ }
334
+ interface CancelInstancesInput {
335
+ /** 按实例组取消(或签"其余取消" / 减签) */
336
+ groupId?: string;
337
+ /** 按令牌 id 取消(精确点名);与 `groupId` 可并存(取并集) */
338
+ tokenIds?: readonly string[];
339
+ }
340
+ interface TransferInput {
341
+ tokenId: string;
342
+ assignee: string;
343
+ }
344
+ interface DelegateInput {
345
+ tokenId: string;
346
+ assignee: string;
347
+ }
348
+ interface HaltInput {
349
+ reason?: string;
350
+ }
351
+ interface SuspendInput {
352
+ reason?: string;
353
+ }
354
+ interface ResumeInput {
355
+ reason?: string;
356
+ }
357
+ /** 原语名 → 入参类型的映射(编译期锁:`Primitives` 少一个就红) */
358
+ interface PrimitiveInputMap {
359
+ advance: AdvanceInput;
360
+ jumpTo: JumpToInput;
361
+ rollbackTo: RollbackToInput;
362
+ spawnInstances: SpawnInstancesInput;
363
+ cancelInstances: CancelInstancesInput;
364
+ transfer: TransferInput;
365
+ delegate: DelegateInput;
366
+ halt: HaltInput;
367
+ suspend: SuspendInput;
368
+ resume: ResumeInput;
369
+ }
370
+
371
+ /**
372
+ * @floken-io/engine · **19 项审批动作映射表**(中国式审批的护城河,**单一事实源**)
373
+ *
374
+ * ★ 本表与 `03-包需求-floken-engine.md` §4 主表**逐行对应**;
375
+ * `primitiveExpr` 字段存的是文档「→ 原语」列的**逐字原文**,
376
+ * 于是「文档与代码是否还对得上」从**人肉核对**变成 `test/catalog.test.ts` 里的一条断言。
377
+ *
378
+ * ★ 计数口径(`03` §192~197,别混):
379
+ * - 主表 **19 行**(`ACTION_SPECS.length`);
380
+ * - 其中 **17 行内核原生**(`native: true`),2 行内核外(`timeoutAction` 由调度层触发、`saveDraft` 不进内核);
381
+ * - `suspend` / `resume` 是**一对**实例级控制动作,**按 1 项计入** → 本表里它是 **1 行、2 个名字**;
382
+ * - 因此**可提交的动作名有 20 个**(`ACTION_NAMES.length`),而**动作项数是 19**(`ACTION_SPECS.length`)。
383
+ * 「19」与「20」的差就在这一行,已由自检断言钉死,不是笔误。
384
+ * - 另有 2 条**机制约束不算动作**(动作开关校验、动作留轨迹),全表项合计 21。
385
+ *
386
+ * ★ **DV-1(默认值单一事实源)**:本表**不存任何默认值**,只存「去 `NormalizedApproval` 的哪个字段读」。
387
+ * 默认值一律由 `@floken-io/moddle` 的 `normalizeApproval()` 填好后再进来 ——
388
+ * 在这里写 `requireComment: true` 就是**第二份默认值**,是分叉的开始。
389
+ *
390
+ * ⚠️ **分层红线**(`AGENTS.md` §4.1):`actions/` 可以认识"驳回",
391
+ * `core/primitives.ts` **不认识** —— 这正是分层本身。
392
+ */
393
+
394
+ /**
395
+ * 20 个可提交的动作名。
396
+ *
397
+ * ⚠️ 与「19 项动作」不矛盾:`suspend` / `resume` 属同一项(一对控制动作)。
398
+ */
399
+ type ActionName = 'approve' | 'reject' | 'rejectToPrev' | 'jumpTo' | 'returnTo' | 'takeBack' | 'revoke' | 'terminate' | 'transfer' | 'delegate' | 'addSignBefore' | 'addSignAfter' | 'reduceSign' | 'countersign' | 'orSign' | 'voteSign' | 'timeoutAction' | 'suspend' | 'resume' | 'saveDraft';
400
+ /** 20 个可提交的动作名(由主表展开;顺序同表,`suspend` 先于 `resume`) */
401
+ declare const ACTION_NAMES: readonly ActionName[];
402
+
403
+ /**
404
+ * @floken-io/engine · 设计期开关校验(`AC-E2` / `AC-E3` / `AC-E15` / `INV-6` / `INV-7`)
405
+ *
406
+ * ★ 本文件是**动作受理**的判据层。两层判据要分清(D-17):
407
+ * - `core/primitives.ts`:**状态自洽**(目标是不是已完成节点、令牌在不在途);
408
+ * - `actions/gates.ts`(本文件):**符合设计期配置**(这个动作开了没有、这个目标允不允许)。
409
+ * 两者都抛,但错误码不同 —— 前者 `ENGINE_STATE_SHAPE_INVALID`,后者 `ENGINE_ACTION_*`。
410
+ *
411
+ * ★ **DV-1(默认值单一事实源)**:本文件**不写任何默认值**。
412
+ * 开关值一律从 `@floken-io/moddle` 的 `normalizeApproval()` 结果里读;
413
+ * 在这里写 `?? true` / `?? false` 就是**第二份默认值**,是 engine 与 designer 行为分叉的开始。
414
+ * 唯一的例外是 `reduceSign.requireComment` —— 它在 moddle 里本就是**可选字段**(未配置 = 不强制),
415
+ * `?? false` 是"读可选字段",不是"另立默认值"(见 `readGate` 注释)。
416
+ *
417
+ * ⚠️ **白名单式**(`03` §275):**没配 = 不允许**,不是"先让人做、事后报错"。
418
+ * 推定不了的(如 `allowedTargets` 里的未知取值)一律**判为不允许**,绝不"猜一个"。
419
+ */
420
+
421
+ /**
422
+ * 当前配置下**已开启**的动作名(`ACTION_NOT_ALLOWED` 的 `details.allowed`)。
423
+ *
424
+ * 报"不允许"时只说"不允许"是不够的 —— 调用方还得知道**能用什么**(`AGENTS.md` §5.4)。
425
+ */
426
+ declare function enabledActionNames(approval: NormalizedApproval): readonly ActionName[];
427
+
428
+ /**
429
+ * T10 · 汇聚判定(正向 + 反向三条提前终止)
430
+ *
431
+ * ★ **算法不在本文件** —— 复用 `@floken-io/moddle` 的 `shouldTerminate()` / `requiredVotes()`。
432
+ *
433
+ * 为什么不复写一份:
434
+ * `01-moddle` §4.4.1 的原话是「这三条是 `03-engine` M3 必须实现的汇聚语义,
435
+ * **写在模型层是为了让引擎没有自由心证的空间**」。既然判定已在模型层落地且可执行,
436
+ * 引擎再写一份就是**两份事实源** —— 它们必然漂移,而漂移的表现是「同一份流程定义,
437
+ * 设计器预览的走向和实际跑出来的走向不一样」,极难排查(见 **D-19**)。
438
+ *
439
+ * 本文件只承担 moddle 不负责的三件事:
440
+ * ① **`ConvergeCtx` 的形状校验** —— 模型层信任入参,引擎不能(`pending` 不自洽要抛);
441
+ * ② **结果语义翻译** —— 把 `done / outcome / cancelRest / reason` 收敛成引擎侧的统一形状,
442
+ * 并补一个 `required`(本次判定用的需通过数),供审计与诊断解释「为什么这时候结束」;
443
+ * ③ **INV-9 的落点** —— `restTokenIds()`:判定说「取消其余」时,到底取消哪些令牌。
444
+ * 这一步**必须**在引擎侧,因为「令牌」是引擎的概念,模型层没有。
445
+ */
446
+
447
+ /** 汇聚模式 = `01-moddle` §4.4.1 的 `ApprovalMode`(`countersign` / `orSign` / `voteSign`) */
448
+ type ConvergeMode = ApprovalMode;
449
+ /**
450
+ * 汇聚判定的输入。**全部是数,不含任何令牌 / 节点信息** ——
451
+ * 判定本身是纯算术,与引擎无关,所以它才能放在模型层。
452
+ */
453
+ interface ConvergeCtx {
454
+ mode: ConvergeMode;
455
+ /** 该实例组的办理人总数 */
456
+ total: number;
457
+ approved: number;
458
+ rejected: number;
459
+ /** 尚未表态数 —— 必须与 `total − approved − rejected` 一致(否则视为状态不自洽) */
460
+ pending: number;
461
+ /** `vote.count`(与 `threshold` 互斥) */
462
+ count?: number | undefined;
463
+ /** `vote.threshold`(比例,(0,1]) */
464
+ threshold?: number | undefined;
465
+ onReject: OnReject;
466
+ }
467
+ interface ConvergenceResult {
468
+ /** `pending` = 还没结束,继续等 */
469
+ outcome: 'approved' | 'rejected' | 'pending';
470
+ /**
471
+ * 是否取消该组内**残余**的在途令牌(INV-9)。
472
+ * 注意:正向汇聚时 `cancelRest` 也可能是 `true`(如或签一人通过 → 取消其余 2 人)。
473
+ */
474
+ cancelRest: boolean;
475
+ /** 人可读的判定依据 —— 进审计 / 诊断,**不进 `message`**(`AGENTS.md` §5.6) */
476
+ reason: string;
477
+ /** 本次判定所用的「需通过数」:`vote` 用 `requiredVotes`,其余 = `total` */
478
+ required: number;
479
+ }
480
+ /**
481
+ * 票签需要几票;非票签 = 全员。
482
+ * 与 `requiredVotes()` 同口径:`count` 优先,否则 `ceil(total × threshold)`,**且不超过 `total`**。
483
+ */
484
+ declare function requiredOf(ctx: ConvergeCtx): number;
485
+ /**
486
+ * ★ 汇聚判定的**唯一入口**:一次算完正向与反向,返回统一形状。
487
+ *
488
+ * 之所以不直接暴露 moddle 的 `shouldTerminate` 而包这一层,是为了:
489
+ * - 入参先过形状校验(模型层不做);
490
+ * - 结果补 `required`,让「为什么这时候结束」可被审计解释;
491
+ * - 引擎侧的类型稳定 —— 将来 moddle 的返回形状演进,只改这一处。
492
+ */
493
+ declare function evaluateConvergence(ctx: ConvergeCtx): ConvergenceResult;
494
+ /**
495
+ * **正向**判定:够票了没(`03` §5.2)。
496
+ *
497
+ * ⚠️ 语义边界:它在 `mode:'all'` 下**只在 `rejected === 0` 时成立**(INV-11)——
498
+ * 会签有人驳回时走的是反向终止(规则一),不是「有人驳回也汇聚」。
499
+ */
500
+ declare function shouldConverge(ctx: ConvergeCtx): boolean;
501
+ /**
502
+ * **反向**判定:票数已不可能达标,不该继续等(`03` §5.3 三条规则)。
503
+ *
504
+ * 与正向判定的关系是**互斥且完备**的:`outcome ∈ {approved, rejected, pending}` 三选一,
505
+ * 所以「既不汇聚也不终止」= `pending`(继续等),不存在「两者都 false 却已结束」的中间态。
506
+ */
507
+ declare function shouldTerminate(ctx: ConvergeCtx): boolean;
508
+ /**
509
+ * 一个汇聚组的当前票数。
510
+ *
511
+ * ★ **计票口径(`total = approved + rejected + pending`)** —— 这一步必须由引擎定,
512
+ * 因为「令牌」是引擎的概念,模型层只有抽象的 `total`。三条硬规则:
513
+ *
514
+ * ① `approved` / `rejected` = 组内**带 `vote`** 的令牌数(投过票的);
515
+ * ② `pending` = 组内**仍在途**(`active` / `waiting`)的令牌数;
516
+ * ③ **被取消且未表态的令牌自动退出计数**(`total` 随之变小)。
517
+ *
518
+ * ③ 不是省事,是语义:**减签 = 少一个人 = 分母少一**;**或签"其余取消" = 那些人不再参与**。
519
+ * 若把它们算进 `total`,`pending = total − approved − rejected` 就会大于实际在途人数,
520
+ * `assertConvergeCtx` 会抛「状态不自洽」—— 那是在用"计数口径"掩盖"成员变了"这个事实。
521
+ */
522
+ interface GroupTally {
523
+ readonly groupId: string;
524
+ /** 组所在的节点(组内令牌必然同节点) */
525
+ readonly nodeId: string;
526
+ readonly total: number;
527
+ readonly approved: number;
528
+ readonly rejected: number;
529
+ readonly pending: number;
530
+ }
531
+ /**
532
+ * 列出**所有还能被判定**的组(按令牌顺序,保证确定性)。
533
+ *
534
+ * `total === 0` 的组(全员被取消且无人表态)不返回 —— 它没有可判定的内容,
535
+ * 且 `ConvergeCtx` 要求 `total >= 1`。
536
+ */
537
+ declare function groupTallies(tokens: readonly Token[]): GroupTally[];
538
+ /**
539
+ * 组 → `ConvergeCtx`(把引擎侧的票翻译成模型层要的那四个数)。
540
+ *
541
+ * ⚠️ `approval` **必填**:`mode` / `onReject` / `vote` 的默认值一律由 moddle 的
542
+ * `normalizeApproval()` 填好(DV-1),没有配置就没有汇聚语义 —— 由调用方在**拿不到配置时抛**,
543
+ * 本函数不自己发明默认值。
544
+ */
545
+ declare function convergeCtxOf(tally: GroupTally, approval: NormalizedApproval): ConvergeCtx;
546
+ /**
547
+ * 判定说「取消其余」时,返回该组内**仍需在途中**的令牌 id(供 `cancelInstances` 原语消费)。
548
+ *
549
+ * ★ INV-9 的落点:汇聚触发 `cancelRest` 后,组内**残余令牌必须全部 `cancelled`**,
550
+ * 不得留下 `active` —— 留下 active 会造成「待办还在、实例却已推进」的幽灵待办。
551
+ *
552
+ * @param keep 本次表态者自己(以及任何不该被取消的令牌)的 id
553
+ */
554
+ declare function restTokenIds(tokens: readonly Token[], groupId: string, keep?: readonly string[]): string[];
555
+
556
+ /**
557
+ * @floken-io/engine · 动作输入(`ARCHITECTURE.md` §7.1 `ActionInput`)
558
+ *
559
+ * ★ 这是 `submit()` 与 `plan()` **共用**的入参形状 —— 两条路径的状态演化必须完全一致
560
+ * (§7.1 写死),所以该类型必须住在 `core/`(最底层),不能长在 `runtime/` 里。
561
+ *
562
+ * ★ 本档**不含任何动作语义**:`action` 只是一个字符串名。19 项动作的合法性与开关校验
563
+ * 归 `actions/catalog.ts` + `actions/gates.ts`(T9)—— 把名字表提前写进 core 会让
564
+ * 「内核可脱离审批概念单独测试」(NFR-E6)失守。
565
+ */
566
+ /**
567
+ * 一次提交的输入。
568
+ *
569
+ * `at` 是 **ADR-007 的确定性入口**:给了它,`plan()` 的结果与系统时钟彻底无关;
570
+ * 不给则由 `EngineConfig.clock()` 在 `submit()` 内补上(槽位 3,在调 `plan()` **之前**)。
571
+ */
572
+ interface ActionInput {
573
+ /** 19 项动作名之一(未开启 → 抛错,DV-2) */
574
+ action: string;
575
+ actor: string;
576
+ /** `requireComment` 为 true 时必填(DV-3) */
577
+ comment?: string;
578
+ /** `reject` / `rejectToPrev` / `jumpTo` / `returnTo` 的目标 nodeId(INV-6) */
579
+ target?: string;
580
+ /** 表单增量 / 变量更新 → 并入 `variables` */
581
+ payload?: Record<string, unknown>;
582
+ /** 显式时间(ISO 8601)。缺省由 `clock()` 填 —— ADR-007 */
583
+ at?: string;
584
+ }
585
+
586
+ /**
587
+ * @floken-io/engine · `plan()` 纯函数(**骨架**,T7)
588
+ *
589
+ * ★ 它是 §3.3 九个槽位里的**槽位 4**:`submit()` 只是把它包了一层「load → … → save → …」。
590
+ * 门 2(强一致)下宿主直接调它,自己把 `{ next, delta }` 并入业务事务。
591
+ * ⇒ **两条路径的状态演化必须完全一致**(§7.1 写死),因此这里不许出现任何「只有 submit 才走」的分支。
592
+ *
593
+ * ★ **纯函数性的三条硬约束(NFR-E6)**,改动本档前先默念:
594
+ * ① **不读系统时钟** —— 时间只能来自 `action.at` 或 `options.clock`(ADR-007);
595
+ * ② **不碰存储 / 不发事件 / 不调 SPI** —— 本档只 import `core/`(另加 `runtime/loop.ts` 的类型与
596
+ * `core/task.ts` 的差分算法,都是纯的);不 import `store/`、`nodes/` 的运行时实现、`eval/`;
597
+ * ③ **不改入参** —— 先 `cloneState`,只改副本;`plan(s, a)` 前后 `s` 必须深等。
598
+ *
599
+ * ⚠️ 动作语义**不在本档**(那是 `actions/` 的事),它通过 `options.apply` 这个**纯函数接缝**(D-18)
600
+ * 进来 —— 于是「`submit()` 与门 2 自编排走同一条演化路径」是结构保证,而不是靠人记得同步两处。
601
+ * 注:`suspended` 的受理门禁**不在这里** —— INV-5 的维护方是 `core/primitives.ts`(T8 已落实:
602
+ * suspended 下除 `resume` 外所有原语抛 `ENGINE_STATE_SUSPENDED`),`plan()` 不重复实现,
603
+ * 否则两处门禁会各说各话。
604
+ * 即:**本档只做「与动作语义无关的那部分演化」**。
605
+ */
606
+
607
+ /** `plan()` 的第三个入参:**不确定性一律从参数进来**(ADR-007) */
608
+ interface PlanOptions {
609
+ /** 时间源;缺省则要求 `action.at` 必须显式给(否则抛 `ENGINE_OPTION_INVALID`) */
610
+ clock?: () => string;
611
+ /** 审计上限(INV-17);溢出裁剪并产出 `ENGINE_AUDIT_TRUNCATED` 诊断 */
612
+ maxAuditEntries?: number;
613
+ /**
614
+ * ★ **状态演化的接缝**(D-18):`(draft) => next`,在 ④ 拷贝之后、`rev` +1 之前施加。
615
+ *
616
+ * 为什么必须有它:`compileAction()`(T9)产出的是**原语调用序列**,而原语调用是
617
+ * 「动作语义」的一部分,归 `runtime/` 而不是 `core/`。没有这个接缝,`submit()` 就只能在
618
+ * 调 `plan()` 之后自己再改一次状态 —— 于是出现**两套演化路径**,
619
+ * 「走 `submit()` 和走 `plan()` 得到不同 `next`」(§7.1 写死的一致性)当场失守。
620
+ *
621
+ * ⚠️ **必须是纯函数**:只依赖入参与闭包里**已解析好的**外部知识
622
+ * (办理人、后继节点由 `submit()` 预先取好再闭包进来 —— 本函数不得调任何 SPI)。
623
+ * 不纯的话 `plan()` 的纯函数性(NFR-E6)就被这个接缝整段毁掉。
624
+ */
625
+ apply?: (draft: InstanceState) => InstanceState;
626
+ /**
627
+ * ★ **待办视图投影**(纯函数):`plan()` 据此算 `delta.added/removed/changed`。
628
+ *
629
+ * 与 `apply` 同一条理由(D-18):待办差分若由 `submit()` 单独算一份,
630
+ * 门 2 下宿主直接调 `plan()` 就拿不到差分 —— 两套算法必然漂移。
631
+ * 引擎侧的实现见 `runtime/loop.ts` 的 `tasksOf()`(结合定义图才有 `nodeName` / `formKey`)。
632
+ */
633
+ tasks?: (state: InstanceState) => TaskView[];
634
+ }
635
+ interface PlanResult {
636
+ /** 演化的结果(**新对象**,入参 `state` 不受影响) */
637
+ next: InstanceState;
638
+ /** 待办差分(骨架阶段恒为空差分;填充分 T10/T11) */
639
+ delta: TaskDelta;
640
+ /** 诊断(不抛):如 INV-17 的审计裁剪。「不得静默丢弃」的落点就是这里 */
641
+ diagnostics: EngineDiagnostic[];
642
+ }
643
+ /**
644
+ * ★ 纯函数:`(state, action) → { next, delta, diagnostics }`
645
+ *
646
+ * 演化顺序(**顺序本身是契约** —— 审计 seq 依赖它):
647
+ * ① 形状校验 → ② 终态门禁(INV-2)→ ③ 时间解析(ADR-007)→ ④ 拷贝
648
+ * → ④.5 变量并入(`action.payload`)→ ④.6 `apply()` 施加动作语义(D-18)
649
+ * → ⑤ `rev` +1(INV-1)→ ⑥ `lastAction` → ⑦ 审计追加(INV-4)
650
+ * → ⑧ 审计裁剪(INV-17)→ ⑨ 终态时间戳 → ⑩ 序列化体检(INV-14)→ ⑪ 组装 delta
651
+ *
652
+ * ⚠️ ④.5 / ④.6 必须在 ⑤ 之前:动作语义(含网关条件)要看到**本次提交的变量增量**,
653
+ * 否则「提交表单里把 amount 改成 9000、网关却按旧值走分支」—— 静默走错分支。
654
+ */
655
+ declare function plan(state: InstanceState, action: ActionInput, options?: PlanOptions): PlanResult;
656
+
657
+ /**
658
+ * @floken-io/engine · per-instance FIFO 串行队列(NFR-E5 的**主防线**)
659
+ *
660
+ * ★ 并发三道防线(§6.4 INV-1 的维护方之一):
661
+ * ① **本档**:进程内按 `instanceId` 串行 —— 同一实例的两次提交永不交错;
662
+ * ② **`rev` CAS**(`StateStore.save`):跨进程 / 跨实例的兜底;
663
+ * ③ 不开读从库、不加悲观锁。
664
+ *
665
+ * 为什么必须串行:`submit()` 是「load → plan → save」三步,中间有 `await`。
666
+ * 两个并发提交会各自 load 到同一个 `rev`,后者覆盖前者 —— **典型脏写**。
667
+ * 串行化后同实例内不存在「读到旧 rev」的窗口。
668
+ *
669
+ * ★ 刻意**不做**的事:
670
+ * - **不做重入检测**:`fn` 内再次对同一 key 调 `run()` 会**死锁**(自己等自己)。
671
+ * 检测它需要在调用栈上打标记,收益不抵复杂度;正确做法是钩子里不要二次提交。
672
+ * 若将来确有必要,应在 `runtime/engine.ts` 层用「提交中」标记拦,而不是改本档。
673
+ * - **不提供 `clear()` / `cancel()`**:队列里挂着的都是「已受理」的提交,取消它们没有安全语义。
674
+ */
675
+ interface InstanceQueue {
676
+ /**
677
+ * 把 `fn` 排到 `instanceId` 这条队列的**队尾**并等待其执行完成。
678
+ *
679
+ * - 同一 `instanceId`:严格 FIFO、两两不重叠;
680
+ * - 不同 `instanceId`:**并行**,互不阻塞(NFR-E5 只要求同实例串行)。
681
+ *
682
+ * @returns `fn` 的结果 / 异常原样透传(**不包装**:包装会让宿主的 `instanceof` 判定失效)
683
+ */
684
+ run<T>(instanceId: string, fn: () => Promise<T> | T): Promise<T>;
685
+ /** 等待**当前已排队**的全部任务完成(测试 / 优雅退出用;不阻止期间新进的任务) */
686
+ drain(): Promise<void>;
687
+ /** 仍有任务在队列(含正在执行)的 key 数 */
688
+ size(): number;
689
+ /** 某个 key 上未完成的任务数(含正在执行的那个) */
690
+ depth(instanceId: string): number;
691
+ }
692
+ /**
693
+ * 建一个 per-instance 串行队列。
694
+ *
695
+ * 实现要点(改动前务必理解):
696
+ * 1. **链尾法**:每个 key 只保存「链尾 Promise」,新任务 `prev.then(fn)` 挂上去 → FIFO 天然成立,
697
+ * 无需自己维护数组和调度器。
698
+ * 2. **前驱失败不得卡死队列**:链尾保存的是「吞掉异常的影子 Promise」(`cleanup`),
699
+ * 所以前一个任务抛错不会让后续任务**永远排不上**。真实异常仍由 `run()` 返回的 Promise 抛给调用方。
700
+ * 3. **用完后必须删 key**:否则 `Map` 会随实例数无限增长(长跑进程的内存泄漏)。
701
+ * 在影子 Promise 的 `.then` 里递减 `depth`,归零即删。
702
+ */
703
+ declare function createInstanceQueue(): InstanceQueue;
704
+
705
+ /**
706
+ * @floken-io/engine · `compileAction` —— **动作名 → 原语调用序列**
707
+ *
708
+ * ★ 这是「19 项动作」与「10 个原语」之间的**唯一桥梁**(`ARCHITECTURE.md` §8 ADR-001):
709
+ * 中国式审批的全部语义都在 `catalog.ts` 那张表里,本文件负责把它**编译**成内核能执行的原语调用。
710
+ * `core/primitives.ts` 因此永远不必认识"驳回" —— 加一个动作只改 `actions/`,不动内核。
711
+ *
712
+ * ★ 本函数的职责边界:**受理校验 + 参数解析 + 编译出调用序列**。
713
+ * 它**不执行**原语(执行归 T11 `runtime/loop.ts`)、**不碰** `rev` / 时间 / 审计(归 `runtime/plan.ts`)。
714
+ *
715
+ * ★ 纯函数性同 `plan()`:不读时钟、不碰存储、不改入参。
716
+ *
717
+ * ⚠️ **依赖倒置**:凡是需要**外部知识**的东西都从 `CompileContext` 进来 ——
718
+ * 后继节点要定义图(`nextOf`)、办理人要 `ApproverSource`(`assignees`)、
719
+ * 发起节点要定义(`startNodeId`)。本文件**不 import 任何 SPI**,否则 NFR-E6 失守。
720
+ *
721
+ * ⚠️ **D-18**:编译期"缺少必要上下文"的错(定位不到唯一令牌 / 换人没给目标人 / 减签没点名 /
722
+ * 后继节点解析不出)统一归 **`ENGINE_STATE_SHAPE_INVALID`**(`stateShapeInvalid`)。
723
+ * 理由同 D-17:四族都不贴切,就近归类,**不新增码族**。
724
+ * 唯一例外是"解析不出后继节点"→ `definitionMissing`(它本质就是定义缺失)。
725
+ */
726
+
727
+ /** 一个原语调用(判别联合:原语名与其入参类型**绑定**,写错编译期就红) */
728
+ type PrimitiveCall = {
729
+ readonly [K in PrimitiveName]: {
730
+ readonly primitive: K;
731
+ readonly input: PrimitiveInputMap[K];
732
+ };
733
+ }[PrimitiveName];
734
+ /** 一次投票:谁投的、投的什么 */
735
+ interface VoteCast {
736
+ readonly tokenId: string;
737
+ readonly vote: VoteOutcome;
738
+ }
739
+ /**
740
+ * ★ T15:**原语之后的令牌级微调**(`CompiledAction.post`)。
741
+ *
742
+ * 为什么不是原语:这两件事都带**审批语义**,而 `core/primitives.ts` 必须业务无知(分层红线)。
743
+ * 为什么又不能没有它:它们都是「AC-E6 / D-34 要求的**动作语义**」,落在原语里会污染内核,
744
+ * 落在 `engine.ts` 里门 2 就复制不到 —— 所以做成 `CompiledAction` 的一个**纯数据字段**,
745
+ * 由 `runtime/loop.ts` 的 `applyPost()` 执行(与 `vote` 同款写法)。
746
+ */
747
+ interface PostStep {
748
+ /**
749
+ * **委派回归**(AC-E6):把该令牌的办理人换回 `Token.returnTo` 并**清除回归路径**。
750
+ * 令牌**不推进** —— 委派是"请人代看一眼",代完还得原主确认,不是"替他办完"。
751
+ */
752
+ readonly returnFromTokenId?: string | undefined;
753
+ /**
754
+ * **解散组**:摘掉这些令牌的 `instanceGroup`(D-34)。
755
+ * 组内回退回单人重办时必做 —— 否则目标节点重办后,旧组的 `instanceGroup` 会跟着令牌
756
+ * 走到下一个单人节点,那里的 `groupTallies()` 又会看到这个组并判汇聚 → **流程自己往前走**。
757
+ */
758
+ readonly dissolveTokenIds?: readonly string[] | undefined;
759
+ }
760
+
761
+ /**
762
+ * @floken-io/engine · **活动 / 子流程 4 类的执行语义**(T18 · `nodes/activities.ts`)
763
+ *
764
+ * 契约来源:`03-engine` §6「活动 / 子流程(4)」+ FR-E12 / E13 / E18 / E24 的例外登记。
765
+ *
766
+ * ★ 为什么单独一档(与 `nodes/events.ts` / `nodes/tasks.ts` 同一个理由):4 类在 XML 里
767
+ * 都是容器 / 调用,但**执行语义完全不同** —— 内嵌子流程要**展开成图的一部分**,
768
+ * 调用活动要**另起一个实例**并等它回来,另外两类已知但**跑不了**。
769
+ *
770
+ * ## ★ 四类怎么分(判据:能不能在**单实例的扁平令牌模型**里正确表达)
771
+ *
772
+ * | 类型 | 处置 | 理由 |
773
+ * |---|---|---|
774
+ * | `SubProcess` | **内嵌展开**(建图时拍平进父图) | 它没有自己的实例、自己的 rev、自己的待办;所谓"子令牌树"在本引擎里就是**令牌走进展开后的那几个节点**。拍平后 `nextOf` / `reachable` / 汇聚 / `completedNodes` 全部照旧工作,不需要第二套遍历 |
775
+ * | ★ `Transaction`(T21) | **内嵌展开**(与 `SubProcess` **同处置**) | 它在 BPMN 里就是"带事务语义的子流程",**拍平这部分与 `SubProcess` 毫无区别**。它剩下的意义(补偿)在**边界事件**那一侧 —— 见下 |
776
+ * | `CallActivity` | **子实例 + 等待 + 自动回归** | 被调用的是**另一个 processId**:它有自己的版本绑定(INV-16)、自己的实例 id、自己的待办 —— 这三件事拍平都表达不了,必须另起实例 |
777
+ * | `AdHocSubProcess` | **显式抛错**(FR-E18,C 级) | "由运行时决定执行哪些节点"是一整套编排语义,本引擎尚未定义它的输入 |
778
+ *
779
+ * ## ★ `Transaction` 的「cancel / compensate」拆成两半(T21 的真实边界)
780
+ *
781
+ * - **`cancel`(事务取消)已落地** —— 而且**不需要**为事务单独写一套:
782
+ * 事务拍平后,内部节点 id 带 `Tx_1/` 前缀;挂在事务上的**中断边界事件**触发时,
783
+ * 按 `nodes/boundary.ts` 的 `inScopeOf()` **前缀判据**取消作用域内**全部**在途令牌
784
+ * —— 这正是 BPMN 里「事务被取消、里面正在办的全部撤销」的效果。
785
+ * - **`compensate`(补偿处理器)仍归 v1.x** —— `03` §11 明确把
786
+ * `compensateEventDefinition` 登记为 FR-E13 的 S 级例外:补偿要维护"已完成的活动
787
+ * 及其补偿处理器"的调用链(含顺序与幂等),那是一整套独立语义。
788
+ * ⚠️ 故 `<compensateEventDefinition>` 的边界事件**仍显式抛错**并指名归属,
789
+ * 绝不降级成"可触发但什么都不做"。
790
+ *
791
+ * ## ★ 内嵌展开的三条硬规则
792
+ *
793
+ * ① **只展 `triggeredByEvent !== true` 的 `subProcess`** —— 事件子流程是"被事件触发"的,
794
+ * 与"走进去再出来"是两回事(FR-E24 / T21),展开它等于把它当成顺序执行。
795
+ * ② **内嵌的 `endEvent` 改写成一个引擎内部类型**(`SUBPROCESS_EXIT_TYPE`)。
796
+ * 不改的话令牌到达内嵌结束事件会按"结束事件"处理 → **令牌直接终结**,
797
+ * 子流程出口后面的节点永远走不到 —— 那是最难查的一类静默截断。
798
+ * ③ **进 / 出的流要重接**:指向子流程的流改指它的**内嵌 startEvent**;
799
+ * 子流程的出向流改由**每个内嵌出口**各发一份(多出口 = 多份,条件照抄)。
800
+ *
801
+ * ## ★ `CallActivity` 的版本绑定(INV-16)
802
+ *
803
+ * 被调用定义的版本**必须**是设计期显式写的,引擎**不**替宿主"取最新版" ——
804
+ * 那正是 `AC-E10` 要防的事:主流程没改,被调用的子流程悄悄换了版本,
805
+ * 在途实例的行为随发布而变。故版本读 `extension['floken:call'].version`,
806
+ * **没有就抛**(不是回退、不是猜)。
807
+ *
808
+ * ⚠️ 为什么是 extension 而不是一等字段:BPMN **没有**"被调用版本"这个标准属性
809
+ * (Camunda 用自家 `calledElementVersion` 属性,不是 OMG 的),而模型层目前也没有对应的一等字段。
810
+ * 此处按 `01-moddle` §4.5 的 extension 袋约定落键 `floken:call`,
811
+ * 待模型层把它升成一等字段后本档只需改取值处 —— **语义不变**。
812
+ */
813
+
814
+ /** 被调用目标:`processId` + **设计期显式绑定**的 `definitionVersion` */
815
+ interface CallTarget {
816
+ readonly processId: string;
817
+ readonly definitionVersion: number;
818
+ }
819
+ /**
820
+ * 一次"调用子流程"的**待创建规格**(纯数据,由纯循环产出、由 `runtime/engine.ts` 兑现)。
821
+ *
822
+ * ⚠️ 为什么 `variables` 要**自带快照**:子实例的初始变量是「令牌停在 `callActivity`
823
+ * 那一刻」的父变量,而不是提交前的旧值 —— 不带上就是「脚本 / 服务刚改过 amount,
824
+ * 子流程却按旧值跑」,与 D-60 同一类事故。
825
+ */
826
+ interface PendingCall {
827
+ readonly nodeId: string;
828
+ readonly tokenId: string;
829
+ /** ★ 子实例的 instanceId(**确定性**,见 `callInstanceIdOf`) */
830
+ readonly instanceId: string;
831
+ readonly processId: string;
832
+ readonly definitionVersion: number;
833
+ readonly variables: Readonly<Record<string, unknown>>;
834
+ }
835
+ /**
836
+ * ★ 子实例结束时写在**父实例**审计里的动作名。
837
+ *
838
+ * ⚠️ 它**不是** 19 项动作之一(宿主提交不了它),也不是 10 个内核原语之一 ——
839
+ * 它是"子实例回来了"这条**内核内部事实**。`03` §9.1 的 `AuditEntry.action`
840
+ * 原文写「19 项动作名 或 内核原语名」,此处是第三类:**内核内部推进名**,
841
+ * 目前已登记的只有这一个(见 `ARCHITECTURE.md` 的 D-62)。
842
+ *
843
+ * 为什么不复用 `approve` 之类的现有名字:审计是**合规主源**,
844
+ * 把"子流程自己跑完了"记成"某人审批通过",等于伪造一条操作记录。
845
+ */
846
+ declare const CALL_RETURN_ACTION = "callActivityReturn";
847
+ /**
848
+ * ★ 子实例回归 —— 把父实例里那条停在 `callActivity` 上的令牌**放行**:纯函数。
849
+ *
850
+ * 三步,顺序是契约:
851
+ * ① **先记账**(`markCompleted`):节点是"离开时才记账",而这一步正是离开 ——
852
+ * 它是 `INV-6` 驳回目标的来源,漏了就永远退不回这个节点;
853
+ * ② 令牌转 `active` 并挪到后继节点 —— 转回 `active` 才会被 `runToWait` 继续推进;
854
+ * ③ **清办理人**(`clearAssignment`):新节点的办理人要重新解析,带着旧人的
855
+ * `assignee` 走过去会让下一条待办落在错误的人名下(与 D-25 同口径)。
856
+ *
857
+ * @param nextNodeId 后继节点 —— 由调用方从 `graph.nextOf(parent.nodeId)` 取好传进来
858
+ * (本档不 import `ProcessGraph`,避免与 `nodes/graph.ts` 形成值层面的循环依赖)
859
+ * @throws `ENGINE_STATE_SHAPE_INVALID` —— 找不到那条令牌 / 它不在 `waiting`
860
+ * ("该等的人不等了"是状态被外部改坏的信号,必须报出来)
861
+ */
862
+ declare function callReturnOf(state: InstanceState, parent: InstanceParent, nextNodeId: string): InstanceState;
863
+
864
+ /**
865
+ * @floken-io/engine · **等待外部消息 / 信号的执行语义**(T20 · `nodes/catch.ts`)
866
+ *
867
+ * 契约来源:`03-engine` FR-E14 / `ARCHITECTURE.md` §7.1(`deliverMessage` / `deliverSignal`)。
868
+ *
869
+ * ★ 为什么单独一档(而不是塞进 `nodes/events.ts` 或 `nodes/tasks.ts`):
870
+ * 「等外部投递」这件事**横跨两个节点族** ——
871
+ * - `intermediateCatchEvent` + `<messageEventDefinition>` / `<signalEventDefinition>`(事件族)
872
+ * - `receiveTask`(任务族,`messageRef`)
873
+ * 它们的 XML 形态毫不相干,执行语义却**完全一样**:令牌停住、记下在等什么、被投递唤醒。
874
+ * 塞进任何一族,另一族就得复制一份「怎么取名 / 怎么匹配 / 怎么唤醒」——
875
+ * 于是「消息名判据」出现第二份写法,必然漂移(与 D-52 的教训同型)。
876
+ *
877
+ * ★ **本档是纯的**(NFR-E6):不读时钟、不碰存储、不调 SPI。
878
+ * 投递的**不纯部分**(load / save / 投影 / 钩子 / 事件)在 `runtime/engine.ts`;
879
+ * 「唤醒 + run-to-wait」的纯部分在 `runtime/deliver.ts`。
880
+ *
881
+ * ## ★ 三条硬判据
882
+ *
883
+ * ① **没有名字 = 定义错误,抛**(`intermediateCatchEvent` 不写 `messageRef`、
884
+ * `receiveTask` 不写 `messageRef`):这样的节点**永远等不到东西**,
885
+ * 放行它等于埋一个「流程跑到这儿就停住、且没有任何报错」的坑 ——
886
+ * 与 INV-16「拿不到版本就抛、绝不回退」同一条纪律。
887
+ * ② **不是 message / signal 的等待一律抛**(`timer` / `error` / `escalation` …):
888
+ * 它们要的是 `Scheduler` / 补偿,归 **T21**。静默直通的表现是「事件从来没发生过,
889
+ * 流程却办完了」—— 那是最难查的一类假象。
890
+ * ③ **投递必须精确匹配 `name`**:名字打错(`Msg_paid` vs `msg_paid`)如果静默丢弃,
891
+ * 流程就永久卡在等待节点上,而宿主以为自己投过了。故「一个都没命中 → 抛」。
892
+ */
893
+
894
+ /**
895
+ * 两类外部触发(**语义差别是投递方式**,不是名字):
896
+ * - `'message'` —— **点对点**:BPMN 消息有且只有一个接收者,故 `deliverMessage(instanceId, …)`;
897
+ * - `'signal'` —— **广播**:一个信号可以被任意多个实例/节点接收,故 `deliverSignal(instanceIds[], …)`。
898
+ */
899
+ type CatchKind = 'message' | 'signal';
900
+ declare const CATCH_KINDS: readonly ["message", "signal"];
901
+ /** 一个等待节点在等什么(**定义期**就定下来的事实) */
902
+ interface CatchBinding {
903
+ readonly kind: CatchKind;
904
+ /** `messageRef` / `signalRef` */
905
+ readonly name: string;
906
+ }
907
+ /**
908
+ * ★ 投递到实例时写进审计的**第四类**动作名(D-62 那一类的扩展)。
909
+ *
910
+ * 为什么不复用 19 项动作名:投递**不是审批动作**,它是外部世界的一次输入
911
+ * (与 `start` / `callActivityReturn` 同族)。混进 19 项里,`beforeAction` 按动作名路由时
912
+ * 就会把「银行回调说已付款」当成「某人点了一次通过」。
913
+ */
914
+ declare const MESSAGE_DELIVER_ACTION = "deliverMessage";
915
+ declare const SIGNAL_DELIVER_ACTION = "deliverSignal";
916
+ /** 两个投递动作名(顺序即契约;外部要数就用 `DELIVER_ACTIONS.length`) */
917
+ declare const DELIVER_ACTIONS: readonly ["deliverMessage", "deliverSignal"];
918
+ /** 定义节点的最小形状(只取本档要读的字段,避免与 moddle 的 `FlowNode` 硬耦合) */
919
+ interface CatchNodeLike {
920
+ readonly id: string;
921
+ readonly type: string;
922
+ readonly messageRef?: string | undefined;
923
+ readonly eventDefinition?: {
924
+ readonly type?: unknown;
925
+ readonly [k: string]: unknown;
926
+ } | undefined;
927
+ }
928
+ /**
929
+ * ★ 「这个节点在等什么」的**唯一入口**(图适配层 `graph.catchOf` 就是它)。
930
+ *
931
+ * - 不是等待节点(`userTask` / 网关 / …)→ `undefined`;
932
+ * - 是等待节点、且等的是 **message / signal** → 返回 {@link CatchBinding};
933
+ * - 是等待节点但**没写名字**(缺 `messageRef` / `signalRef`)→ **抛**(判据 ①);
934
+ * - 是等待节点但等的是 `timer` / `error` / `escalation` … → **抛**(判据 ②)。
935
+ *
936
+ * ⚠️ 刻意**不**拆成「纯查询 + 断言」两个函数:那会让「`timer` 到底算不算没实现」
937
+ * 出现两个答案,调用方漏调断言就成了静默直通。一个入口 = 一个答案。
938
+ */
939
+ declare function catchBindingOf(node: CatchNodeLike | undefined): CatchBinding | undefined;
940
+ /** 一次投递要命中的目标 */
941
+ interface DeliverMatch {
942
+ readonly kind: CatchKind;
943
+ readonly name: string;
944
+ /** 只在这些令牌里找(不给 = 全部匹配) */
945
+ readonly tokenIds?: readonly string[] | undefined;
946
+ }
947
+ /**
948
+ * 状态里**正在等** `match` 的那些令牌(去重、保序)。
949
+ *
950
+ * ⚠️ 只认**在途**令牌(`active`;`waiting` 是串行会签里没轮到的,不可能是等待节点):
951
+ * 已被取消的令牌可能还留着 `awaiting`(取消是改 `state` 不改本字段),
952
+ * 把它们算进来就会"唤醒一个已经不存在的等待"。
953
+ */
954
+ declare function matchingTokens(state: InstanceState, match: DeliverMatch): readonly Token[];
955
+ /**
956
+ * 该实例**此刻在等什么**(去重、保序)—— 专供「投递没命中」的报错 `details` 用。
957
+ *
958
+ * ★ 为什么要列出来:`AGENTS.md` §5.4 要求错误必须给**合法取值**,
959
+ * 否则宿主看见「没有在等 Msg_paid」也修不了 —— 他不知道这里其实在等 `Msg_Paid`。
960
+ */
961
+ declare function waitingNamesOf(state: InstanceState): readonly string[];
962
+
963
+ /**
964
+ * @floken-io/engine · **边界事件的执行语义**(T21 · `nodes/boundary.ts`)
965
+ *
966
+ * 契约来源:`03-engine` §6「事件(6)」的 `BoundaryEvent` 一行 + FR-E13。
967
+ *
968
+ * ★ 为什么单独一档(与 `nodes/catch.ts` 同为"事件语义",但**不是一回事**):
969
+ * `catch.ts` 管的是「**令牌停下来等**」—— 等的时候令牌**就是**那个等待节点上的指针;
970
+ * 边界事件**不持有令牌**:它挂在活动上(`attachedTo`),是活动执行期间的**一盏监听器**,
971
+ * 触发时才**产生**令牌(或**夺走**宿主的令牌)。两者的状态变更形状完全不同,
972
+ * 混在一档会让「`awaiting` 到底是给谁的」出现第二个答案。
973
+ *
974
+ * ## ★ 两条硬判据
975
+ *
976
+ * ① **没有 `attachedTo` 的边界事件 = 定义错误,抛**:它挂不到任何活动上 ⇒
977
+ * 永远不会被监听 ⇒ 等于"写了个不存在的分支"。放行它,宿主会以为配了超时/撤回
978
+ * 而实际什么都不会发生 —— 且**没有任何报错**。
979
+ * ② **只有 message / signal 两类触发可被投递**(与 `nodes/catch.ts` 判据 ② 同一条线):
980
+ * `timer` / `error` / `escalation` / `cancel` / `compensate` 一律抛并指名归属。
981
+ * ⚠️ 其中 **`compensate`(补偿处理器)是 `03` §11 明确登记到 v1.x 的例外**
982
+ * (FR-E13 S 级),本档**不**把它降级成"可触发但什么都不做"。
983
+ *
984
+ * ## ★ `cancelActivity`:中断还是继续
985
+ *
986
+ * | `cancelActivity` | 宿主令牌 | 触发后 |
987
+ * |---|---|---|
988
+ * | `true`(**缺省**) | **取消** | 宿主活动的令牌及其**作用域内**全部在途令牌一并取消,令牌改走边界事件的出向 |
989
+ * | `false` | **保留** | 宿主活动继续办,另**新造**一个令牌走边界事件的出向 |
990
+ *
991
+ * ★ 「作用域内」= `nodeId === attachedTo` **或** `nodeId` 以 `attachedTo + '/'` 开头
992
+ * (内嵌子流程 / `transaction` 在建图时已**拍平**,内部节点 id 带该前缀 —— 见
993
+ * `nodes/activities.ts` 的 `SUBPROCESS_PATH_SEP`)。于是「事务被 cancel →
994
+ * 里面正在办的全部撤销」这件事**不需要第二套遍历**:拍平 + 前缀判据就够了。
995
+ *
996
+ * ★ **本档是纯的**(NFR-E6):不读时钟、不碰存储、不调 SPI。
997
+ * 触发的**不纯部分**(load / save / 投影 / 钩子 / 事件)在 `runtime/engine.ts`;
998
+ * 「匹配 + 触发 + run-to-wait」的纯部分在 `runtime/deliver.ts`。
999
+ */
1000
+
1001
+ declare const BOUNDARY_TYPE = "boundaryEvent";
1002
+ /** 一个边界事件在**建图期**就定下来的事实 */
1003
+ interface BoundaryBinding {
1004
+ /** 边界事件自己的节点 id */
1005
+ readonly nodeId: string;
1006
+ /** 宿主活动 id(`attachedTo`) */
1007
+ readonly attachedTo: string;
1008
+ /**
1009
+ * 触发后是否取消宿主活动(BPMN `cancelActivity`)。
1010
+ * ⚠️ **缺省 `true`** 与规范一致:BPMN 的 `cancelActivity` 默认是 `true`,
1011
+ * 且"非中断"是个**显式**声明(`cancelActivity="false"`),不能反过来默认。
1012
+ */
1013
+ readonly cancelActivity: boolean;
1014
+ /** 它在等什么(只有 message / signal 两类能到这一步) */
1015
+ readonly trigger: CatchBinding;
1016
+ }
1017
+ /** 定义节点的最小形状(只取本档要读的字段,避免与 moddle 的 `FlowNode` 硬耦合) */
1018
+ interface BoundaryNodeLike {
1019
+ readonly id: string;
1020
+ readonly type: string;
1021
+ readonly attachedTo?: string | undefined;
1022
+ readonly cancelActivity?: boolean | undefined;
1023
+ readonly eventDefinition?: {
1024
+ readonly type?: unknown;
1025
+ readonly [k: string]: unknown;
1026
+ } | undefined;
1027
+ }
1028
+ /**
1029
+ * ★ 读一个边界事件的绑定。
1030
+ *
1031
+ * - 不是 `boundaryEvent` → `undefined`(让调用方照常处理别的事);
1032
+ * - 是 `boundaryEvent` → 绑定;
1033
+ *
1034
+ * @throws `ENGINE_STATE_SHAPE_INVALID` —— 缺 `attachedTo`(判据 ①)/ 触发种类不可投递(判据 ②)
1035
+ */
1036
+ declare function boundaryBindingOf(node: BoundaryNodeLike | undefined): BoundaryBinding | undefined;
1037
+ /** 一次「某盏监听器被触发」 */
1038
+ interface BoundaryFire {
1039
+ readonly boundary: BoundaryBinding;
1040
+ /** 宿主活动的令牌 id(中断时要取消它;非中断时它是"继续办的那一个") */
1041
+ readonly hostTokenId: string;
1042
+ }
1043
+ /**
1044
+ * ★ 此刻**监听中**且**命中** `match` 的边界事件(保序)。
1045
+ *
1046
+ * 「监听中」= 宿主活动上有**在途**令牌(`active` / `waiting`)。
1047
+ * 令牌不在那儿 ⇒ 那个活动根本没在跑 ⇒ 它的边界事件此刻**不成立**
1048
+ * (投递到一个已经办完的活动上,"取消"就无从谈起)。
1049
+ */
1050
+ declare function armedBoundaries(state: InstanceState, graph: ProcessGraph, match: {
1051
+ readonly kind: CatchKind;
1052
+ readonly name: string;
1053
+ }): readonly BoundaryFire[];
1054
+ /**
1055
+ * 「此刻监听中的东西」的可读清单(去重、保序)—— 专供投递未命中的 `details.waiting`。
1056
+ *
1057
+ * ★ 为什么要连边界事件一起列:`AGENTS.md` §5.4 要求错误必须给**合法取值**。
1058
+ * 只列 catch 节点的话,宿主看见「没有在等 Msg_cancel」也修不了 ——
1059
+ * 他不知道自己其实把消息名写在了**边界事件**上。
1060
+ */
1061
+ declare function armedNamesOf(state: InstanceState, graph: ProcessGraph): readonly string[];
1062
+ /**
1063
+ * ★ 该令牌是否处在 `hostId` **及其内嵌作用域**里。
1064
+ *
1065
+ * `transaction` / `subProcess` 建图时已拍平,内部节点 id 形如 `Tx_1/Task_a` ——
1066
+ * 于是「取消事务内所有在途令牌」= 前缀判据,不需要第二套子令牌树。
1067
+ */
1068
+ declare function inScopeOf(tokenNodeId: string, hostId: string): boolean;
1069
+ /** 中断边界事件要取消的那些在途令牌 id(保序) */
1070
+ declare function cancelTargetsOf(state: InstanceState, hostId: string): readonly string[];
1071
+ /**
1072
+ * ★ 触发产生的新令牌 id —— **必须唯一**。
1073
+ *
1074
+ * ⚠️ 为什么不能直接用 `${hostTokenId}#${boundaryId}`:非中断边界事件可以被**重复**触发
1075
+ * (宿主还在办,第二条同样的消息又来了),两次会撞出同一个 id ——
1076
+ * 于是两条令牌在 `tokens` 里互相覆盖(按 id 查找永远只找到第一个),
1077
+ * 表现为「第二次触发好像没生效」,且没有任何报错。
1078
+ */
1079
+ declare function boundaryTokenIdOf(state: InstanceState, hostTokenId: string, boundaryId: string): string;
1080
+
1081
+ /**
1082
+ * @floken-io/engine · 定义图适配层(`ProcessDefinition` → 引擎能问的问题)
1083
+ *
1084
+ * ★ 为什么要单独一层:`ProcessDefinition` 是**模型层**的形状(XML 的镜像),
1085
+ * 引擎要问的是另一套问题("发起节点是谁"、"这个节点的下一个节点是谁"、"它的审批配置是什么")。
1086
+ * 直接在 `runtime/` 里散写 `def.processes[0].flows.find(...)` 有三个后果:
1087
+ * ① 同一份查询逻辑复制 N 份,改一处漏一处;
1088
+ * ② `INV-3`(token.nodeId 必须在定义图中)没有唯一的判定点;
1089
+ * ③ 图算法(T16 网关 / T18 子流程)深化时,`runtime/` 会被改烂。
1090
+ * 故收敛成**只读适配器**:本文件**不持有状态**,也不改 `ProcessDefinition`。
1091
+ *
1092
+ * ★ 分层:`nodes/` 可 import `core/` 与模型层;`core/` 不得反向 import 本目录。
1093
+ *
1094
+ * ⚠️ **T16 / T17 / T18 落地后的能力边界(诚实标注,勿含糊成"支持")**:
1095
+ * - 认得全部 **6 类事件 + 5 类网关 + 8 类任务**(分类与可达性见图适配层),但其中
1096
+ * `intermediateThrowEvent` / `implicitThrowEvent` /
1097
+ * `complexGateway` / `sendTask` 一律**显式抛错**(分属 T20 / FR-E24 / FR-E17 / T20);
1098
+ * - ★ **T20 起 `intermediateCatchEvent` / `receiveTask` 可执行**:令牌停在它们上面
1099
+ * **等外部投递**(`deliverMessage` / `deliverSignal`);但等 `timer` / `error` 之类
1100
+ * 仍抛(归 v1.x),没写 `messageRef` / `signalRef` 也抛(等不到 = 永久卡死);
1101
+ * - ★ **T21 起 `boundaryEvent` / `eventBasedGateway` 可执行**:前者挂在活动上监听、
1102
+ * 按 `cancelActivity` 决定中断与否;后者**竞速**(第一个到达的事件赢,其余分支取消);
1103
+ * - **单出向的普通节点**(`userTask` 等)有多条 `sequenceFlow` → 仍抛 `D-22`
1104
+ * ("隐式排他 / 隐式包容"没有规格依据,不发明);多出向**只**在网关上被路由;
1105
+ * - **T18 起内嵌子流程在建图时展开**(`nodes/activities.ts` 的 `expandSubProcesses`),
1106
+ * 故本档拿到的 `nodes` / `flows` 已是**拍平后**的全表 —— 展开规则见该文件档首。
1107
+ * 把这三点写成显式抛错而不是"取第一条流走下去",是为了让"这条流程现在跑不了"
1108
+ * 表现为**一条能照着修的错误**,而不是"流程静默走错分支"。
1109
+ */
1110
+
1111
+ /** 一条**出向流**(引擎视角):条件已归一化成"表达式文本或空" */
1112
+ interface OutFlow {
1113
+ readonly id: string;
1114
+ readonly to: string;
1115
+ /** 条件表达式;无条件(BPMN 的默认流)→ `undefined`(D-42:空 = 走) */
1116
+ readonly expression?: string;
1117
+ }
1118
+ /** 一条**入向流** */
1119
+ interface InFlow {
1120
+ readonly id: string;
1121
+ readonly from: string;
1122
+ }
1123
+ interface ProcessGraph {
1124
+ readonly processId: string;
1125
+ readonly definitionVersion: number;
1126
+ /** 发起节点 id(第一个 `startEvent`) */
1127
+ readonly startNodeId: string;
1128
+ /** 图中全部节点 id(含 startEvent / endEvent) */
1129
+ nodeIds(): readonly string[];
1130
+ /** `INV-3` 的判定点 */
1131
+ has(nodeId: string): boolean;
1132
+ /** 节点类型;不在图里返回 `undefined` */
1133
+ typeOf(nodeId: string): string | undefined;
1134
+ nameOf(nodeId: string): string | undefined;
1135
+ formKeyOf(nodeId: string): string | undefined;
1136
+ /**
1137
+ * 出向的**唯一**后继。
1138
+ * - 无出向(如 `endEvent`)→ `undefined`
1139
+ * - 恰好 1 条 → 目标 id
1140
+ * - 2 条及以上 → **抛错**(D-22:多出向只在**网关**上被路由,
1141
+ * 普通节点的多出向语义无规格依据,不得静默取第一条)
1142
+ */
1143
+ nextOf(nodeId: string): string | undefined;
1144
+ /** ★ 出向流全表(T16:网关路由的输入)。顺序 = 定义里的顺序,**即分支判定的优先级** */
1145
+ outFlowsOf(nodeId: string): readonly OutFlow[];
1146
+ /** ★ 入向流全表(T16:网关汇聚要判"还有没有人能来") */
1147
+ inFlowsOf(nodeId: string): readonly InFlow[];
1148
+ /**
1149
+ * 默认流(`Gateway.default` / `Activity.default`)—— 指向一条 `sequenceFlow` 的 id。
1150
+ * 只在「**一条都没选中**」时才走(`exclusive` / `inclusive` 同口径)。
1151
+ */
1152
+ defaultFlowIdOf(nodeId: string): string | undefined;
1153
+ /**
1154
+ * ★ 图上可达性:`from` 沿出向能否走到 `to`(T16 汇聚判据的基础)。
1155
+ *
1156
+ * - `from === to` → **false**("我自己在网关上"不算"还有人能来");
1157
+ * - 只走 `sequenceFlow`,不判条件 —— 判据要的是「**可能**到达」,
1158
+ * 按条件剪枝会把"条件此刻为假但稍后可能为真"算成不可达,从而提前合流。
1159
+ */
1160
+ reachable(from: string, to: string): boolean;
1161
+ /** 该节点的审批配置(**已归一化**);未配置 → `undefined` */
1162
+ approvalOf(nodeId: string): NormalizedApproval | undefined;
1163
+ /**
1164
+ * 该节点上配的 `floken:approval` 键是否**存在**(未归一化前)。
1165
+ * 用于区分「没配」与「配了但归一化失败」—— 后者由 `normalizeApproval` 自己抛。
1166
+ */
1167
+ hasApproval(nodeId: string): boolean;
1168
+ /** `<bpmn:script>` 子元素(`scriptTask`);未配 / 空白 → `undefined` */
1169
+ scriptOf(nodeId: string): string | undefined;
1170
+ /** `scriptFormat`(`scriptTask`);未配 → `undefined`(⇒ 不是 FEEL,走 `handlers` 表) */
1171
+ scriptFormatOf(nodeId: string): string | undefined;
1172
+ /**
1173
+ * ★ `serviceTask` / 非 FEEL 的 `scriptTask` 在 `handlers` 表里的**查找键**。
1174
+ *
1175
+ * 三级回退:`implementation`(非 `##` 前缀的内置标识)→ `operationRef` → **`nodeId`**。
1176
+ *
1177
+ * ⚠️ 为什么 `##unspecified` / `##WebService` 不算:那是 BPMN 的**实现标识**,
1178
+ * 不是宿主处理器的名字 —— 拿它去查 `handlers` 必然查不到,报错还会指错方向。
1179
+ *
1180
+ * ⚠️ 为什么最后回退到 `nodeId`:让「每个服务节点一个 handler」成为零配置可用形态
1181
+ * (`AC-E13` 的精神),而不是逼宿主为每个节点写一遍 `implementation`。
1182
+ */
1183
+ handlerRefOf(nodeId: string): string;
1184
+ /**
1185
+ * ★ `callActivity` 的被调用目标(T18 · **INV-16** 的落点)。
1186
+ *
1187
+ * - 不是 `callActivity` → `undefined`;
1188
+ * - 是 `callActivity` 但**没绑定版本** → **抛**(绝不回退到"最新版",理由见
1189
+ * `nodes/activities.ts` 的 `callTargetOf`)。
1190
+ */
1191
+ callTargetOf(nodeId: string): CallTarget | undefined;
1192
+ /**
1193
+ * ★ 该节点在等什么(T20 · `intermediateCatchEvent` / `receiveTask`)。
1194
+ *
1195
+ * - 不是等待节点 → `undefined`;
1196
+ * - 是等待节点但**没写名字**(缺 `messageRef` / `signalRef`)→ **抛**
1197
+ * (这样的节点永远等不到东西,放行 = 埋一个不报错的永久卡死);
1198
+ * - 是等待节点但等的是 `timer` / `error` 之类 → **抛**(归 T21)。
1199
+ *
1200
+ * ⚠️ 判据不在本档而在 `nodes/catch.ts`:等待语义**横跨**事件族与任务族。
1201
+ */
1202
+ catchOf(nodeId: string): CatchBinding | undefined;
1203
+ /**
1204
+ * ★ 挂在 `nodeId` 上的**边界事件**(T21)。没有 → 空数组(**不是** `undefined`,
1205
+ * 免得每个调用点都要判空)。
1206
+ *
1207
+ * ⚠️ 索引在**建图时**建好并**eager 校验**(与 `expandSubProcesses` 同口径):
1208
+ * 边界事件的定义缺陷(悬空 / 触发种类不可投递 / 没有出向)在建图时就会抛出,
1209
+ * 而不是等触发 —— 那时已经写了一半状态。
1210
+ */
1211
+ boundaryOf(nodeId: string): readonly BoundaryBinding[];
1212
+ }
1213
+
1214
+ /**
1215
+ * @floken-io/engine · **任务 8 类的执行语义**(T17 · `nodes/tasks.ts`)
1216
+ *
1217
+ * 契约来源:`03-engine` §6「任务(8)」/ `01-moddle` §5.3 的覆盖率表。
1218
+ *
1219
+ * ★ 为什么单独一档(与 `nodes/events.ts` 同一个理由):8 类在 XML 里都是 `<bpmn:*Task>`,
1220
+ * 但**执行语义完全不同** —— `userTask` 要等人办、`serviceTask` 要调宿主代码、
1221
+ * `scriptTask` 只能跑 FEEL(**不得**跑任意 JS)、`manualTask` 只是留痕、
1222
+ * 裸 `task` 什么都不做。散写在 `runtime/loop.ts` 的 if 链里,「哪一类跑不了」
1223
+ * 就会变成一句注释而不是一条**可断言的事实**。
1224
+ *
1225
+ * ★ **分类必须是穷举的**(`TASK_TYPES` 就是那 8 个名字):新增一类时 `taskBehaviorOf`
1226
+ * 返回 `undefined` → `runtime/loop.ts` 把它当**自动直通**处理;本档的 8 类里
1227
+ * `sendTask` 是 **`unsupported`(显式抛错)**,绝不静默直通。
1228
+ *
1229
+ * ## ★ 两类消息节点的处置(T20 已分家)
1230
+ * - `receiveTask` → **`'catch'`(等外部消息)**,T20 随 `deliverMessage()` 一并落地。
1231
+ * 它与 `intermediateCatchEvent` 的等待语义**完全同形**,故判据不在本档 ——
1232
+ * 见 `nodes/catch.ts`(横跨任务族与事件族,放哪一族都会长出第二份写法)。
1233
+ * - `sendTask` → **仍抛错(D-56)**:`03` 自己写明它「与 `IntermediateThrowEvent`
1234
+ * 同构」,而后者在 T16 就是因为 **ADR-006 把事件集定死 10 个、其中没有"抛出事件"**
1235
+ * 才推迟的。此处若"顺手发一条",只有两条路:① 偷偷加第 11 个事件(违反 ADR-006,
1236
+ * 且必须走 ADR 修订而不是代码);② 复用 `taskCreated` + `taskCompleted`
1237
+ * ⇒ 与 `manualTask` **完全同形**,等于把两条规格写明的语义**静默合并成一条**。
1238
+ * 两条都不接受 ⇒ 与 `intermediateThrowEvent` 同处置:抛错并指名归属。
1239
+ *
1240
+ * ## ★ `effect` 类为什么必须外源解析(与 `assigneesOf` / `conditionsOf` 同款)
1241
+ * `serviceTask` 调宿主代码、`scriptTask` 跑 FEEL、`businessRuleTask` 走 `decisionHandler` ——
1242
+ * 全是**副作用**,而 `runToWait()` 必须同步纯(NFR-E6)。
1243
+ * 故与办理人 / 条件同一套路:纯循环里只调 `LoopContext.effectsOf()` 这个**同步闭包**,
1244
+ * 闭包在还没有结果时抛 `NodeEffectUnresolved` 哨兵 → `runtime/engine.ts` 解析 → **重跑**。
1245
+ *
1246
+ * ⚠️ **副作用只发生一次**:解析结果按 `${nodeId}::${tokenId}` 缓存,重跑时直接命中。
1247
+ * 若无缓存,"重跑"就会把 `serviceTask` 调 N 次(发 N 封邮件),那比不实现更糟。
1248
+ *
1249
+ * ★ 分层:`nodes/` 可 import `core/` 与模型层;**`core/` 不得反向 import 本目录**。
1250
+ */
1251
+
1252
+ /**
1253
+ * 一个 `effect` 节点的**已解析**副作用。
1254
+ *
1255
+ * ⚠️ 它是**外源产物**:由 `runtime/engine.ts`(唯一允许不纯的地方)解析,
1256
+ * 纯循环只读它。这样「一次副作用只发生一次」与「`runToWait` 保持纯」才可能同时成立。
1257
+ */
1258
+ interface NodeEffect {
1259
+ readonly nodeId: string;
1260
+ /** 并入 `variables` 的增量(`serviceTask` 返回 / `scriptTask` 结果 / `decision` 输出) */
1261
+ readonly variables?: Readonly<Record<string, unknown>>;
1262
+ /**
1263
+ * 要投递的事件(**只** `manualTask` 用:连发 `taskCreated` + `taskCompleted` 留痕)。
1264
+ *
1265
+ * ⚠️ 为什么不在解析处直接 `emit`:`EventSink` 的投递时点是**槽位 9**(状态已落库之后)。
1266
+ * 在解析处就发,一旦后续步骤抛错(比如后面的节点解析不出办理人),
1267
+ * 就会出现「事件说这个任务办完了、状态却没落库」—— 正是 §7.1 要防的那类不一致。
1268
+ */
1269
+ readonly events?: readonly EngineEvent[];
1270
+ }
1271
+
1272
+ /**
1273
+ * @floken-io/engine · run-to-wait 推进循环(ADR-003 的执行模型)
1274
+ *
1275
+ * ★ **ADR-003 的一句话**:一次 `submit()` 同步推进到**下一个稳定点**就返回 ——
1276
+ * 不停在"中间态"(比如刚 `advance` 完、令牌还悬在网关上),也不异步挂起等回调。
1277
+ * 稳定点的定义就两条:① 令牌停在**等待节点**(`userTask`,要人办);② 令牌已终结(结束事件 / 被取消)。
1278
+ *
1279
+ * ★ **纯函数性(NFR-E6)**:本文件整个是纯的 —— 不读时钟、不碰存储、不发事件、不改入参。
1280
+ * 于是它可以被 `plan()`(门 2)与 `submit()`(门 1)**同一份**地调用,
1281
+ * 「两条路径演化不一致」这类事从结构上就不可能发生。
1282
+ *
1283
+ * ⚠️ **外部知识一律从 `LoopContext` 进来**(依赖倒置):
1284
+ * 办理人要 `ApproverSource`(异步 SPI),而本文件必须同步纯 ——
1285
+ * 故由 `runtime/engine.ts` **先探测落点、异步解析、再闭包成同步的 `assigneesOf`** 传进来。
1286
+ * 探测用的解析器返回一个非空占位(`PROBE_ASSIGNEE`),以免触发 INV-13 的空集报错。
1287
+ *
1288
+ * ## ★ 一次动作的完整推进 = `step()`(**顺序本身是契约**)
1289
+ *
1290
+ * ① 施加原语(换人 / 加签 / 单实例的推进与跳转…)
1291
+ * ①b **微调**(`applyPost`:委派回归 / 解散组 —— T15)
1292
+ * ② **记票**(组内 `approve` / `reject` —— 见 `actions/compile.ts` 的 `CompiledAction.vote`)
1293
+ * ③ **串行会签的接力**(`sequential`:上一个办完 → 下一个 `waiting` 转 `active`)
1294
+ * ④ **汇聚判定**(`actions/convergence.ts`)—— 可能触发取消其余 / 推进 / 驳回
1295
+ * ⑤ run-to-wait(落到下一个稳定点)
1296
+ *
1297
+ * ③④ 必须在 ⑤ **之前**:否则令牌会先被推进走,汇聚再判时组里已经没人了。
1298
+ *
1299
+ * ⚠️ **能力边界(诚实标注)**:
1300
+ * - 原语级审计(旧 `TraceEntry.kind:'primitive'`)→ **已否决**,见 **D-23 / D-87**;
1301
+ * - **T20 已落地**:`IntermediateCatchEvent` / `receiveTask` 是本循环**第三种稳定点**
1302
+ * (前两种 = 等人办的 `userTask`、停在 `callActivity` 上等子实例);
1303
+ * - **T22 已落地**:`exportTrace()` 的 `from` / `to` / `tokenId` 由 `runtime/plan.ts` 填
1304
+ * (定位令牌走 `subjectTokenOf()`,与本档认领令牌同一套判据)。
1305
+ *
1306
+ * ## ★ T16:并行分支在这里落地(分叉 / 汇聚两条新路径)
1307
+ *
1308
+ * **分叉**(`forkToken`):网关路由出 N 条出向 → 令牌分裂成 N 个。
1309
+ * 第 0 条**沿用原令牌**(id 不变,内层循环接着推它),其余 N−1 个**新建**并插在它后面
1310
+ * —— 于是外层下标循环自然地把它们逐个推到各自的稳定点,不需要另写一套遍历。
1311
+ *
1312
+ * **汇聚**(`joinPass`):`parallelGateway` / `inclusiveGateway` 入向 ≥2 时**等待**,
1313
+ * 判据是「不存在别的在途令牌**可达**本网关」(不是"来了几个"—— 详见 `nodes/gateways.ts` 档首)。
1314
+ * 合流 = 其余令牌 `completed` + 承接令牌**摘掉 `branch`**(合流点之后又回到单干)。
1315
+ *
1316
+ * ⚠️ 分叉写入的 `Token.branch` 同时是 **D-47** 的解药:`rollbackTo`(拿回 / 撤销)
1317
+ * 据此把"撤销下游"收缩到**本分支**,不再误伤另一条分支上正在办的待办。
1318
+ */
1319
+
1320
+ interface LoopContext {
1321
+ readonly graph: ProcessGraph;
1322
+ /**
1323
+ * 该节点解析出的办理人 —— **同步**。
1324
+ * 由调用方(`runtime/engine.ts`)预先解析并以闭包传入,本文件才可能保持纯。
1325
+ */
1326
+ readonly assigneesOf: (nodeId: string) => readonly string[];
1327
+ /**
1328
+ * ★ 出向流的条件真值 —— **同步**(T16)。
1329
+ *
1330
+ * `ConditionHandler` 是异步 SPI,而本文件必须同步纯 —— 故与 `assigneesOf` 同款:
1331
+ * 由调用方预先求值并闭包进来。**尚未求值**时闭包抛 `ConditionUnresolved` 哨兵,
1332
+ * 引擎解析后重跑(详见 `eval/condition.ts` 的哨兵注释)。
1333
+ *
1334
+ * @param flow 出向流(`expression` 为 `undefined` = 无条件,恒真)
1335
+ * @param nodeId 该网关的 id(条件上下文要用)
1336
+ * @param variables ★ **到达该网关此刻**的变量快照(T17)。
1337
+ * 为什么必须传:`scriptTask` / `serviceTask` 会在本次推进里**改写**变量,
1338
+ * 拿提交前的旧快照去求值,就是「脚本把 amount 改成了 9000、网关却按旧值走分支」
1339
+ * —— §7.2 要防的头号事故的另一副面孔。
1340
+ */
1341
+ readonly conditionsOf: (flow: OutFlow, nodeId: string, variables: Readonly<Record<string, unknown>>) => boolean;
1342
+ /**
1343
+ * ★ **任务副作用**(T17 · `serviceTask` / `scriptTask` / `businessRuleTask` / `manualTask`)
1344
+ * —— **同步**,与 `assigneesOf` / `conditionsOf` 同一套路。
1345
+ *
1346
+ * `ServiceHandler` / `DecisionHandler` 是**异步** SPI,且带真实副作用(发邮件、建单),
1347
+ * 而本文件必须同步纯。故:尚未解析时闭包抛 `NodeEffectUnresolved` 哨兵 →
1348
+ * `runtime/engine.ts` 解析 → **重跑**;结果按 `${nodeId}::${tokenId}` 缓存 ⇒ **只调一次**。
1349
+ *
1350
+ * ⚠️ 本档只**消费** `NodeEffect`(并变量 / 收事件),**绝不**在这里调宿主代码。
1351
+ */
1352
+ readonly effectsOf: (nodeId: string, tokenId: string, variables: Readonly<Record<string, unknown>>) => NodeEffect;
1353
+ /** 本次推进的时刻(填 `Token.createdAt`) */
1354
+ readonly at: string;
1355
+ }
1356
+
1357
+ /** `step()` 的入参 */
1358
+ interface StepInput {
1359
+ readonly calls: readonly PrimitiveCall[];
1360
+ /** 组内投票(`CompiledAction.vote`);非组内动作不给 */
1361
+ readonly vote?: VoteCast | undefined;
1362
+ /** ★ T15 令牌级微调(`CompiledAction.post`:委派回归 / 解散组) */
1363
+ readonly post?: PostStep | undefined;
1364
+ /** 汇聚驳回时的显式退回目标;不给则由 `reject.allowedTargets` 推导 */
1365
+ readonly rejectTarget?: string | undefined;
1366
+ }
1367
+ interface LoopResult {
1368
+ readonly next: InstanceState;
1369
+ /** 令牌停下来的等待节点(去重、保序)—— 调用方据此预先解析办理人 */
1370
+ readonly landings: readonly string[];
1371
+ /**
1372
+ * ★ 本次推进里**由节点副作用产出**的事件(T17:目前只有 `manualTask` 的留痕)。
1373
+ *
1374
+ * ⚠️ 为什么不在本文件里 `emit`:本文件是纯的(NFR-E6)。事件由 `runtime/engine.ts`
1375
+ * 在**槽位 9**(状态已落库之后)统一投递 —— 在推进过程中就发,一旦后续步骤抛错,
1376
+ * 就会出现「事件说办完了、状态却没落库」的不一致。
1377
+ */
1378
+ readonly events: readonly EngineEvent[];
1379
+ /**
1380
+ * ★ 本次推进里**停在 `callActivity` 上、需要建子实例**的那些(T18)。
1381
+ *
1382
+ * 与 `events` 同一套路:纯循环**建不了**实例(那要写存储),只能把"该建什么"
1383
+ * 作为**纯数据**交出去,由 `runtime/engine.ts` 兑现。
1384
+ *
1385
+ * ⚠️ 门 2(宿主自编排)下宿主自己兑现:子实例的 `parent` 指针与父实例的
1386
+ * `childInstanceIds` 都由 `parkForCall()` 算好并在 `next` 里,宿主只需按
1387
+ * `PendingCall` 建出实例;子实例到终态后调 `plan()` 并施加 `callReturnOf()`
1388
+ * 即可完成回归 —— 两条路径的形状因此仍然一致。
1389
+ */
1390
+ readonly pendingCalls: readonly PendingCall[];
1391
+ }
1392
+ /**
1393
+ * ★ 一次动作的完整推进(①~⑤,见档首)。
1394
+ *
1395
+ * 为什么必须是一个**导出**的函数而不是 `engine.ts` 里的几行:
1396
+ * 门 2(宿主自编排)下没有 `submit()`,事件与状态都得宿主自己算 ——
1397
+ * 若推进逻辑长在 `submit()` 里,门 2 就复制一份,于是「两条路径演化不一致」
1398
+ * (§7.1 的硬约束)从**纪律问题**退化成**必然会发生的分叉**。
1399
+ */
1400
+ declare function step(state: InstanceState, ctx: LoopContext, input: StepInput): LoopResult;
1401
+ /**
1402
+ * ★ 执行 `CompiledAction.post`(T15)—— **纯函数**,门 2 自编排要独立完成同样的演化。
1403
+ *
1404
+ * 两件事都带审批语义,故**不做成原语**(`core/primitives.ts` 必须业务无知),
1405
+ * 但也**不能长在 `engine.ts` 里**(门 2 复制不到 → §7.1 两条路径分叉)。
1406
+ */
1407
+ declare function applyPost(state: InstanceState, post: PostStep): InstanceState;
1408
+ /**
1409
+ * 记一票:**令牌一律 `completed`**(他的办理结束了),方向记在 `vote`。
1410
+ *
1411
+ * ⚠️ 不要试图用 `cancelled` 表示"投了驳回" —— 那会让它与「被汇聚取消的人」不可区分,
1412
+ * 事后审计答不出"是谁驳回的"(详见 `core/state.ts` 里 `VoteOutcome` 的注释)。
1413
+ */
1414
+ declare function castVote(state: InstanceState, vote: VoteCast): InstanceState;
1415
+ /**
1416
+ * `sequential:true` —— 组内**至多 1 个 `active`**(INV-8)。
1417
+ *
1418
+ * 组内没人 `active` 且有 `waiting` → 激活最早那一个,并**此时**才填 `createdAt`
1419
+ * (它是"这条待办的创建时刻",轮到他了才算创建)。
1420
+ */
1421
+ declare function promoteSequential(state: InstanceState, ctx: LoopContext): InstanceState;
1422
+ /**
1423
+ * 反复结算**所有**已定局的组,直到没有可结算的为止。
1424
+ *
1425
+ * ★ 为什么是循环:减签 / 或签取消会让**别的**组立刻达线,一次判定不够。
1426
+ * 终止性由「**组一旦结算就解散**」保证(见 `applySettlement`)—— 组数严格递减。
1427
+ */
1428
+ declare function settleGroups(state: InstanceState, ctx: LoopContext, rejectTarget?: string | undefined): InstanceState;
1429
+
1430
+ /**
1431
+ * @floken-io/engine · **投递的纯执行段**(T20 落地 / T21 扩展 · `runtime/deliver.ts`)
1432
+ *
1433
+ * ★ 与 `runtime/loop.ts` 的 `step()` **同层、同性质**:纯函数,门 1(`submit` 系)与
1434
+ * 门 2(宿主自编排)共用同一份。它做的事依次是:
1435
+ *
1436
+ * ```
1437
+ * ① 找出本次投递命中了哪些等待中的令牌(精确匹配 kind + name)
1438
+ * ② 竞速结算:同一 `Token.race` 里只留**第一个**,其余取消(T21 · `EventBasedGateway`)
1439
+ * ③ 兜底 / 并集:命中不到等待令牌时,改问**边界事件**(`nodes/boundary.ts`)
1440
+ * ④ 摘掉等待态(`Token.awaiting`)并**离开等待节点**
1441
+ * ⑤ run-to-wait —— 从等待节点的后继继续走到下一个稳定点(可能一路走到结束事件)
1442
+ * ```
1443
+ *
1444
+ * ★ **④ 为什么"摘等待态"必须和"离开节点"一起做**,不能只摘等待态就交给 `runToWait()`:
1445
+ * `run-to-wait` 见到 `intermediateCatchEvent` / `receiveTask` 就会**再停一次**(那是它的职责)。
1446
+ * 只摘等待态 = 令牌被原地重新停车 —— 表现为「投递返回了差分、状态也写了,
1447
+ * 但令牌一动没动」,且**没有任何报错**。
1448
+ * ⇒ 唤醒的语义本来就是「**离开**等待节点」(与 `callReturnOf()` 放行停在 `callActivity`
1449
+ * 上的令牌同一形态),不是"在同一个节点上再等一次"。
1450
+ *
1451
+ * ★ **为什么必须单独一个函数**(而不是在 `engine.ts` 里"顺手改一下状态"):
1452
+ * 门 2 下宿主自己 `load()` → `plan()` → 写库,投递这一步也得他自己做;
1453
+ * 若它只活在 `deliverMessage()` 里,门 2 就得复制一份「怎么匹配 / 怎么唤醒 / 怎么推进」,
1454
+ * 于是 §7.1「两条路径不得分叉」从**结构保证**退化成**纪律问题**(与 `step()` 必须公开同一条理由)。
1455
+ *
1456
+ * ## ★ T21 新增的两件事
1457
+ *
1458
+ * **② 竞速(`EventBasedGateway`)**:事件网关分叉出来的令牌共享一个 `Token.race`。
1459
+ * BPMN 的语义是「**只走第一个到达的事件**,其余分支**取消**」。少了这一步会变成
1460
+ * 「两个事件都到了、流程走了两条分支」—— 而它**不会报错**,只是莫名多出一条待办。
1461
+ * ⚠️ 取消范围是**同 race 的全部在途令牌**,不只是"本次也匹配上的那些":
1462
+ * 另一条分支在等一个**别的**消息时,它同样输了这场竞速,必须一并退场
1463
+ * (否则它会永远停在那里,而流程已经沿赢家的分支走完了)。
1464
+ *
1465
+ * **③ 边界事件**:边界事件**不持有令牌**,故 `matchingTokens()` 永远看不到它。
1466
+ * 命中集合因此有两类来源,按投递种类区别对待(★ 这是 BPMN 的既有语义,不是发明):
1467
+ * - `message`(**点对点**,1:1)→ **先在等待令牌里找**;找不到才去问边界事件,
1468
+ * 且只取**第一个**。消息只有一个接收者,"两个都命中"是定义问题,引擎按**顺序**取定。
1469
+ * - `signal`(**广播**,1:N)→ 等待令牌与边界事件**全都命中**,一个都不落。
1470
+ *
1471
+ * ⚠️ **本档不含任何不纯动作**:load / save / 投影 / 钩子 / 事件全在 `runtime/engine.ts`
1472
+ * (刻意保持「`engine.ts` 是唯一不纯文件」这条不变 —— 见 NFR-E6)。
1473
+ */
1474
+
1475
+ /**
1476
+ * 投递种类决定**命中集合怎么取**。
1477
+ * - `'point'` —— 点对点(`deliverMessage`):等待令牌优先,兜底边界事件且只取第一个;
1478
+ * - `'broadcast'` —— 广播(`deliverSignal`):等待令牌与边界事件**并集**,一个都不落。
1479
+ */
1480
+ type DeliverMode = 'point' | 'broadcast';
1481
+ /** 一次边界触发的事实(供审计 / 诊断) */
1482
+ interface FiredBoundary {
1483
+ /** 边界事件自己的节点 id */
1484
+ readonly nodeId: string;
1485
+ /** 宿主活动 id */
1486
+ readonly attachedTo: string;
1487
+ readonly cancelActivity: boolean;
1488
+ readonly hostTokenId: string;
1489
+ }
1490
+ /** `deliverStep()` 的结果 */
1491
+ interface DeliverResult extends LoopResult {
1492
+ /** 被本次投递唤醒的令牌 id(顺序 = 状态里的顺序,故可重放) */
1493
+ readonly woken: readonly string[];
1494
+ /** 被唤醒令牌**当时所在的**节点 id(与 `woken` 一一对应)—— 审计与诊断要记「唤醒了哪儿」 */
1495
+ readonly nodeIds: readonly string[];
1496
+ /**
1497
+ * ★ 因本次投递而**输掉竞速**被取消的令牌 id(T21)。
1498
+ * 没有事件网关时恒为空数组(**不是** `undefined` —— 免得每个调用点判空)。
1499
+ */
1500
+ readonly racedOut: readonly string[];
1501
+ /** ★ 本次触发的边界事件(T21) */
1502
+ readonly fired: readonly FiredBoundary[];
1503
+ }
1504
+ /**
1505
+ * ★ 一次投递的**纯**执行段。
1506
+ *
1507
+ * @throws `ENGINE_ACTION_TARGET_INVALID` —— 一个都没命中(**绝不静默丢弃**,理由见
1508
+ * `core/errors.ts` 的 `deliverNoTarget`)。广播场景下调用方应先用
1509
+ * `matchingTokens()` / `armedBoundaries()` 判断,**只把命中的实例交给本函数**。
1510
+ */
1511
+ declare function deliverStep(state: InstanceState, ctx: LoopContext, match: DeliverMatch, mode?: DeliverMode): DeliverResult;
1512
+
1513
+ /**
1514
+ * @floken-io/engine · **超时排程的纯计算段**(T21 · `runtime/timers.ts`)
1515
+ *
1516
+ * ★ 与 `runtime/deliver.ts` 同一个套路:把「该排什么 / 该取消什么」算成**纯数据**,
1517
+ * 由 `runtime/engine.ts`(唯一不纯的文件)去调 `Scheduler`。
1518
+ *
1519
+ * ## ★ 为什么要 diff 而不是"进入就排、离开就消"
1520
+ *
1521
+ * 直觉写法是在 `run-to-wait` 里"落到等待节点时 schedule" —— 但推进循环是**纯**的,
1522
+ * 拿不到 `Scheduler`(它是 SPI,且 `schedule()` 是异步的)。于是只剩两条路:
1523
+ * ① 让纯循环吐出意图(本档的做法);
1524
+ * ② 在不纯层**比对前后两个状态**,问出「谁刚停下、谁刚离开」。
1525
+ * 本档走 ②(diff),因为 ① 会让 `LoopResult` 再多一个字段、且**门 2** 得自己再算一遍;
1526
+ * 而 ② 的判据就是一个纯函数 `timingKeysOf(state)`,门 1 / 门 2 **共用同一份**。
1527
+ *
1528
+ * ## ★ 判据:什么算「正在计时」
1529
+ *
1530
+ * `active` + **有 `assignee`** + 该节点的 `floken:approval.timeout` 存在。
1531
+ *
1532
+ * ⚠️ 为什么要求 `assignee`:「在途」不等于「有人在办」——
1533
+ * 刚分叉出来还没落定的令牌、停在 catch 节点上的令牌都是 `active` 却没有办理人,
1534
+ * 给它们排超时会产出「催办一条根本不存在的待办」。
1535
+ *
1536
+ * ⚠️ 为什么按 `${nodeId}::${tokenId}` 而不是 nodeId:会签组里同节点有 N 个令牌,
1537
+ * 每个人的待办**各自**计时(驳回重办后也是新令牌 = 新计时),按节点去重会漏掉其余人。
1538
+ *
1539
+ * ## ★ handle 的回收
1540
+ *
1541
+ * `Scheduler.schedule()` **返回**一个 handle(形状由调度方定),故取消必须**拿着它**。
1542
+ * 引擎把它写回 `Token.timerHandles`,离开该节点时由不纯层 `cancel()` 后**删除**。
1543
+ *
1544
+ * ⚠️ 为什么不自己拼一个确定性 handle:那等于规定调度方的数据形状(SPI 要避免的耦合)。
1545
+ * ⚠️ 为什么不在 `clearAssignment()` 里删:那是纯函数,删了就没地方记"要取消谁"——
1546
+ * 定时器会在待办办完之后照样触发(最典型的"已办结还在催办")。
1547
+ */
1548
+
1549
+ /** 到点后做什么(`03` §4 的 `timeout.actions[]` 四选) */
1550
+ type TimerKind = TimeoutAction['type'];
1551
+ /** ★ 一个"该排程"的意图(纯数据,由不纯层兑现) */
1552
+ interface PendingTimeout {
1553
+ readonly tokenId: string;
1554
+ readonly nodeId: string;
1555
+ /** 待办创建时刻 —— 调度方据此按工作日历推算到期时刻 */
1556
+ readonly fromAt: string;
1557
+ readonly timeout: TimeoutSpec;
1558
+ /** 本次要排的动作(可并存多个,故是数组) */
1559
+ readonly kinds: readonly TimerKind[];
1560
+ }
1561
+ /** ★ 一个"该取消"的意图 */
1562
+ interface PendingCancel {
1563
+ readonly tokenId: string;
1564
+ /** 当初 `schedule()` 返回的 handle(可能为空 —— 例如排程失败过) */
1565
+ readonly handles: readonly string[];
1566
+ }
1567
+ /** `diffTimers()` 的结果 */
1568
+ interface TimerDiff {
1569
+ readonly schedule: readonly PendingTimeout[];
1570
+ readonly cancel: readonly PendingCancel[];
1571
+ }
1572
+ /** 计时键:`${nodeId}::${tokenId}`(见档首"为什么按令牌") */
1573
+ declare function timerKeyOf(nodeId: string, tokenId: string): string;
1574
+ /** ★ 把 moddle 的归一化超时配置裁成 `TimeoutSpec`(只交**原始配置**,不交算好的时刻) */
1575
+ declare function timeoutSpecOf(t: NormalizedTimeout): TimeoutSpec;
1576
+ /**
1577
+ * ★ 该状态里**正在计时**的那些(键 → 意图)。
1578
+ *
1579
+ * 保序(按 `tokens` 顺序),故两次运行的结果可重放。
1580
+ */
1581
+ declare function timingKeysOf(state: InstanceState, graph: ProcessGraph): ReadonlyMap<string, PendingTimeout>;
1582
+ /**
1583
+ * ★ 比对前后两个状态,算出「该排什么 / 该取消什么」。
1584
+ *
1585
+ * - **新增**的计时键 → 排程;
1586
+ * - **消失**的计时键 → 取消(handle 从 `next` 里那个令牌上取 —— 令牌本身还在 `tokens` 里,
1587
+ * 只是不再 `active`);
1588
+ * - 两边都在的键 → **不动**(不重复排程;重排会让"3 个工作日"从头再数一次)。
1589
+ *
1590
+ * @param prev 推进**前**的状态
1591
+ * @param next 推进**后**的状态(取消用的 handle 从它这里读)
1592
+ */
1593
+ declare function diffTimers(prev: InstanceState, next: InstanceState, graph: ProcessGraph): TimerDiff;
1594
+
1595
+ /**
1596
+ * @floken-io/engine · 令牌轨迹(`exportTrace()` 的纯内核,T22)
1597
+ *
1598
+ * ★ 一句话:**轨迹 = `auditTrail` 的只读投影**,不新增任何存储。
1599
+ * `03` §9.1 的 Plan A 把 `auditTrail` 定为合规主源(随状态整块落库),
1600
+ * 本档只负责把它翻成**对外承诺形状**的 `TraceEntry[]` ——
1601
+ * ⚠️ 若这里"顺手补算"出 auditTrail 里没有的东西,审计与轨迹就会各说各话,
1602
+ * 「库里的审计和导出的轨迹对不上」将成为一类无法定位的缺陷。
1603
+ *
1604
+ * ★ **纯函数性(NFR-E6)同 `plan()` / `emit.ts`**:不读时钟、不碰存储、不改入参。
1605
+ * 于是门 2(宿主自编排)拿着手里的 `InstanceState` 直接调 `traceOf()` 也能得到
1606
+ * 与 `engine.exportTrace()` **逐字相同**的结果(§7.1 的两条路径一致性)。
1607
+ *
1608
+ * ## ★ `kind` 只有两档:审批 / 非审批(**D-87**)
1609
+ *
1610
+ * 判据是「**是不是 19 项审批动作之一**」,不是"谁发起的"。
1611
+ * `start` / `callActivityReturn` / `deliverMessage` / `deliverSignal` 一律 `system`。
1612
+ *
1613
+ * ⚠️ **原语级审计(旧 `kind:'primitive'`,**D-23**)已否决**,两条理由:
1614
+ * ① `runtime/loop.ts` 的 run-to-wait **不走 `advance` 原语**(直接改 `token.nodeId`),
1615
+ * 按"原语调用"记出来的轨迹里**没有令牌移动** —— 恰恰是"轨迹"最该有的那一半;
1616
+ * ② 一次提交会炸出几十条,`maxAuditEntries` 的语义会从「保留最近 N 次变更」
1617
+ * 扭曲成「保留最近两次提交」,且裁剪会砍在**一次提交的内部**。
1618
+ *
1619
+ * ## ★ 完整性必须可见(**INV-17** 的另一半)
1620
+ *
1621
+ * `maxAuditEntries` 会裁掉最旧的审计。若 `exportTrace()` 只回一个数组,
1622
+ * 被裁过的轨迹看起来跟完整的**一模一样** —— 这正是本包最忌的静默降级。
1623
+ * 故返回 `TraceResult`:`truncated` + 被丢掉的 seq 区间(与 `plan()` 那条
1624
+ * `ENGINE_AUDIT_TRUNCATED` 诊断的 `details.dropped*` 同名字、同口径)。
1625
+ * ⚠️ 区间是**从 seq 的空洞推出来的**(INV-4 保证 seq 从 1 起、无空洞),
1626
+ * 不需要为此在状态里新增字段 —— 新增字段就要动 `stateSchema` 与迁移表。
1627
+ */
1628
+
1629
+ /**
1630
+ * ★ 会进 `auditTrail` 的**非审批**动作名(**D-62** 的四类动作名里的后三类)。
1631
+ *
1632
+ * 这是「19 项审批动作之外还有哪些动作名」的**唯一事实源** —— 有测试钉住它与
1633
+ * `traceKindOf()` 一致,免得将来加一个系统动作忘了同步,于是它静默变成 `approval`。
1634
+ */
1635
+ declare const SYSTEM_AUDIT_ACTIONS: readonly ["start", "callActivityReturn", "deliverMessage", "deliverSignal"];
1636
+ type SystemAuditAction = (typeof SYSTEM_AUDIT_ACTIONS)[number];
1637
+ /**
1638
+ * ★ 一条审计属于哪一类。
1639
+ *
1640
+ * `approval` = 19 项审批动作之一;其余(含 19 项之外由门 2 直接写入的名字)一律 `system`。
1641
+ * ⚠️ 判据取**审批名单**而不是 `SYSTEM_AUDIT_ACTIONS` 名单:
1642
+ * 前者是"已定型的 19 项",后者只是"已知的非审批名"—— 将来多一个系统动作,
1643
+ * 按后者判会把它错标成 `approval`(静默),按前者判只是标成 `system`(正确)。
1644
+ */
1645
+ declare function traceKindOf(action: string): 'approval' | 'system';
1646
+ /**
1647
+ * `exportTrace()` 的返回值。
1648
+ *
1649
+ * ⚠️ 为什么不是裸数组:`maxAuditEntries` 裁剪之后,裸数组与完整轨迹**无从区分** ——
1650
+ * 宿主会把"只剩最近 3 条"当成"一共就 3 条"(INV-17 要防的就是这个)。
1651
+ */
1652
+ interface TraceResult {
1653
+ /** 与 `auditTrail` **一一对应**、同序(INV-4 保证 seq 递增) */
1654
+ entries: TraceEntry[];
1655
+ /** ★ 审计被裁剪过 → 本轨迹**不完整**(`entries` 只是最近的一段) */
1656
+ truncated: boolean;
1657
+ /** 被丢掉的最旧 seq;未裁剪时不存在 */
1658
+ droppedFromSeq?: number;
1659
+ /** 被丢掉的最新 seq;未裁剪时不存在 */
1660
+ droppedToSeq?: number;
1661
+ }
1662
+ /**
1663
+ * ★ 纯函数:`state → TraceResult`。
1664
+ *
1665
+ * 门 2 下宿主自己持状态、自己落库,也该得到与 `engine.exportTrace()` 相同的结果 ——
1666
+ * 故投影逻辑**必须在纯函数里**,不许长在 `runtime/engine.ts`(唯一不纯档)里。
1667
+ */
1668
+ declare function traceOf(state: InstanceState): TraceResult;
1669
+
1670
+ /**
1671
+ * @floken-io/engine · 事件派生与投递(T12 · ADR-006「节点级 5 + 实例级 5」)
1672
+ *
1673
+ * ★ **本文件是纯的**:`eventsOf()` 只从入参推事件,不读时钟、不碰存储、不发 I/O。
1674
+ * 为什么必须纯:**门 2(强一致自编排)下宿主自己调 `plan()`**,事件若由 `submit()` 单独推导,
1675
+ * 两条路径就会各发各的(与 D-18 同一个道理)。宿主自编排时调 `eventsOf()` 即可得到同一组事件。
1676
+ *
1677
+ * ★ **事件不是审计**:审计主源是 `InstanceState.auditTrail`(`03` §9.1 Plan A)。
1678
+ * 本文件的事件是**通知**,丢了不影响流程 —— 所以投递(下面的 `emitAll`)**不 await、不抛**。
1679
+ *
1680
+ * ## 派生规则(**顺序本身是契约**)
1681
+ *
1682
+ * ① `started`(仅 `previousStatus === undefined`,即 `start()`)
1683
+ * ② 非终态实例事件 `suspended` / `resumed` —— **先于**待办事件(实例状态是待办状态的原因)
1684
+ * ③ 待办事件:`removed`(completed / cancelled)→ `added`(created → assigned)→ `changed`(updated)
1685
+ * ④ 终态实例事件 `completed` / `terminated` —— **后于**待办事件(待办都结算了实例才终态)
1686
+ *
1687
+ * ★ `taskCreated` **必先于** `taskAssigned`;无 `assignee` 时只发 `created`(ADR-006 的定序)。
1688
+ *
1689
+ * ## ⚠️ 一条诚实的边界
1690
+ * `InstanceStatus` 有 5 个值,实例级事件却只有 5 个且**不含 `cancelled`** ——
1691
+ * `cancelled` 是预留状态,**当前没有任何原语产出它**(只有 `halt` → `terminated`)。
1692
+ * 真出现了也不静默:由 `instanceEventNameOf()` 显式返回 `undefined` 并在头注释记档,
1693
+ * 将来若要有「实例取消」事件,须先改 ADR-006 的事件集,而不是在这里偷偷加。
1694
+ */
1695
+
1696
+ /** `eventsOf()` 的入参 —— 全部可观测数据,无隐藏依赖 */
1697
+ interface EmitInput {
1698
+ /** 提交**前**的待办视图。`removed` 只有 taskId,事件字段(nodeId / assignee…)只能从这里取 */
1699
+ readonly before: readonly TaskView[];
1700
+ /** 本次差分(`added` / `removed` / `changed` + `action`) */
1701
+ readonly delta: TaskDelta;
1702
+ /** 提交**后**的完整状态(取 `tokens` 终态与 Header) */
1703
+ readonly next: InstanceState;
1704
+ /** 提交前的实例状态;**缺省 = 无前状态**(`start()`) */
1705
+ readonly previousStatus?: InstanceStatus | undefined;
1706
+ }
1707
+ /**
1708
+ * ★ 一次提交对应的全部事件(按上面 ①②③④ 的顺序)。
1709
+ *
1710
+ * 入参里没有 `at` —— 时间取 `delta.action.at`,与 `lastAction` / `auditTrail` **同源**
1711
+ * (三处各自取时间就会在重放时对不上)。
1712
+ */
1713
+ declare function eventsOf(input: EmitInput): EngineEvent[];
1714
+ /**
1715
+ * 实例状态迁移 → 实例级事件名。
1716
+ *
1717
+ * @returns `undefined` = 该迁移不产事件(状态没变,或 `cancelled` 这个预留状态)
1718
+ */
1719
+ declare function instanceEventNameOf(previous: InstanceStatus | undefined, next: InstanceStatus): InstanceEventName | undefined;
1720
+ /**
1721
+ * ★ 投递:**不 await、不抛**(`EventSink` 的语义是「丢了不影响流程」)。
1722
+ *
1723
+ * ⚠️ 两个必须防的坑,宿主自己写投递时最容易踩:
1724
+ * ① `emit()` 返回 rejected Promise 却没人接 → **unhandled rejection 会把进程拖崩**;
1725
+ * ② `emit()` 同步抛错 → 若不加捕获会把 `submit()` 一起带崩,而状态**已经落库了**,
1726
+ * 于是宿主看到「提交失败但流程其实走完了」—— 最坏的一类不一致。
1727
+ *
1728
+ * ⇒ 两者都在这里吞掉。想要「提交返回前事件已落地」的保证,请用**门 1 `afterAction`**(引擎 await 它)。
1729
+ */
1730
+ declare function emitAll(sink: EventSink | undefined, events: readonly EngineEvent[]): void;
1731
+
1732
+ /**
1733
+ * @floken-io/engine · `createEngine()` —— 对外唯一门面(T11)
1734
+ *
1735
+ * 契约来源:`ARCHITECTURE.md` §3.3(九个槽位)/ §7.1(`Engine`)/ ADR-003(run-to-wait)/ ADR-007(时钟)。
1736
+ *
1737
+ * ★ **本文件是唯一"允许不纯"的地方**:它是 `runtime/`,可以读时钟、调 SPI、写存储。
1738
+ * 反过来,`plan()`(门 2 入口)与本文件调用的 `runtime/loop.ts` 都必须保持纯 ——
1739
+ * 因此**所有外部知识都在这里取好,再以闭包交给纯函数**:
1740
+ *
1741
+ * ```
1742
+ * submit = 入队 → load → 图/配置 → 【★ 探测落点 → 异步解析办理人】 → plan(纯) → save → 投影
1743
+ * ↑ 这一步是本文件的关键设计
1744
+ * ```
1745
+ *
1746
+ * 为什么必须"先探测再解析":`runToWait()` 要走完图才知道**会落到哪些等待节点**,
1747
+ * 而办理人要 `ApproverSource`(异步 SPI)才拿得到。`plan()` 的 `apply` 接缝(D-18)又要求**同步纯**,
1748
+ * 所以不能把 SPI 塞进 `apply` 里。解法是**跑两次纯函数**:第一次带"占位办理人"问出落点,
1749
+ * 解析完再跑第二次 —— 两次都是纯的,且第二次的结果与门 2 自编排**逐字节相同**。
1750
+ *
1751
+ * ★ **九个槽位全部到位**(§3.3):T11 落了 0~4 / 6~7,T12 补齐 5(门 1 前)、8(门 1 后)、9(事件)。
1752
+ *
1753
+ * ⑤ `beforeAction` 在 `plan()` **之后**、`save()` **之前** —— 这个位置不是随手放的:
1754
+ * 放 save 之后就成了「写了再问能不能写」,否决时状态已经落库;放 plan 之前则拿不到
1755
+ * `ctx.next`(宿主最常见的用法是「看下下一步是谁再决定要不要放行」)。
1756
+ * ⑨ 事件在**最后**且**不 await**:`EventSink` 的语义是「丢了不影响流程」(ADR-006)。
1757
+ *
1758
+ * ★ **T20 追加**:`deliverMessage` / `deliverSignal` 与 `submit()` **同构**(同样九个槽位、
1759
+ * 同样走 `plan()` 的 `apply` 接缝、同样经 `queue.run()` 串行),差别只有两点:
1760
+ * ① 纯执行段是 `deliverStep()`(匹配 → 唤醒 → run-to-wait)而不是 `step()`(原语 → 记票 → …);
1761
+ * ② 动作名是**第四类**(`deliverMessage` / `deliverSignal`),不是 19 项审批动作。
1762
+ *
1763
+ * ★ `exportTrace()`(T22)在本档只有两行 —— 投影逻辑全在 `runtime/trace.ts`(纯函数)。
1764
+ */
1765
+
1766
+ interface EngineConfig {
1767
+ /** 不传 = 内置 `createMemoryStore()`(NFR-E10「默认内存、可切换」) */
1768
+ store?: StateStore;
1769
+ /** ✅ 必填:`AC-E10` 要求按 `(processId, version)` 取图纸 */
1770
+ definitionSource: DefinitionSource;
1771
+ /** 不传 = 宿主自管待办表 */
1772
+ projection?: TaskProjection;
1773
+ /**
1774
+ * 不传 = 内置默认(**只认 `{type:'user'}`**,见 `DEFAULT_APPROVER_SOURCE`)。
1775
+ *
1776
+ * ★ 为什么给默认而不是"必填":只有"内置默认 + 未注入即明确报错"两者结合,
1777
+ * 才能让「零配置跑通一条报销流程」(`AC-E13`)与「换人 / 角色解析必须显式注入」同时成立。
1778
+ */
1779
+ approverSource?: ApproverSource;
1780
+ handlers?: ServiceHandler;
1781
+ authResolver?: AuthResolver;
1782
+ formProvider?: FormProvider;
1783
+ conditionHandler?: ConditionHandler;
1784
+ decisionHandler?: DecisionHandler;
1785
+ events?: EventSink;
1786
+ scheduler?: Scheduler;
1787
+ /** 门 1(T12 接线) */
1788
+ hooks?: EngineHooks;
1789
+ /** ADR-007;不传 = 系统时钟 */
1790
+ clock?: () => string;
1791
+ /** INV-17 审计上限 */
1792
+ maxAuditEntries?: number;
1793
+ }
1794
+ interface StartOptions {
1795
+ /** ✅ 必填(AC-E10:实例绑定定义版本,改版不影响在途) */
1796
+ definitionVersion: number;
1797
+ /** ✅ 必填(审计第一条的 `actor`) */
1798
+ starter: string;
1799
+ businessKey?: string;
1800
+ tenantId?: string;
1801
+ variables?: Record<string, unknown>;
1802
+ }
1803
+ /**
1804
+ * 一次投递的输入(T20 · `deliverMessage` / `deliverSignal`)。
1805
+ *
1806
+ * ★ 与 `ActionInput` **刻意不共用**:投递不是审批动作 —— 它没有「19 项动作名」、
1807
+ * 没有 `target` / `comment`(消息名本身就是目标)。共用会让宿主写出
1808
+ * `submit({action:'approve', name:'Msg_paid'})` 这种四不像,且编译期挡不住。
1809
+ */
1810
+ interface DeliverInput {
1811
+ /** 消息名 / 信号名,与定义里的 `messageRef` / `signalRef` **逐字**匹配 */
1812
+ name: string;
1813
+ /** 谁投的(进审计;外部系统就写系统名,如 `'bank-callback'`) */
1814
+ actor: string;
1815
+ /** 随消息带来的数据 → 并入 `variables`(如 `{ paid: true, amount: 9000 }`) */
1816
+ payload?: Record<string, unknown>;
1817
+ /** 显式时间(ISO 8601);缺省由 `clock()` 填 —— ADR-007 */
1818
+ at?: string;
1819
+ }
1820
+ interface Engine {
1821
+ /** 发起一个实例;返回 `instanceId` */
1822
+ start(processId: string, opts: StartOptions): Promise<string>;
1823
+ /** 提交一次动作;返回待办差分(INV-15) */
1824
+ submit(instanceId: string, action: ActionInput): Promise<TaskDelta>;
1825
+ /**
1826
+ * ★ **点对点**投递一条消息,唤醒该实例里正在等它的令牌(T20 · `intermediateCatchEvent` / `receiveTask`)。
1827
+ *
1828
+ * @throws `ENGINE_ACTION_TARGET_INVALID` —— 该实例没有在等这个名字(**不静默丢弃**)
1829
+ */
1830
+ deliverMessage(instanceId: string, input: DeliverInput): Promise<TaskDelta>;
1831
+ /**
1832
+ * ★ **广播**一个信号,唤醒候选实例里**所有**正在等它的实例(T20)。
1833
+ *
1834
+ * ⚠️ 候选集由宿主给:引擎不知道实例全集(`StateStore` 没有查询接口,见 §3b)。
1835
+ *
1836
+ * @throws `ENGINE_ACTION_TARGET_INVALID` —— 一个都没命中(完全无效果 = 静默丢弃)
1837
+ */
1838
+ deliverSignal(instanceIds: readonly string[], input: DeliverInput): Promise<TaskDelta[]>;
1839
+ /**
1840
+ * ★ 导出**令牌轨迹**(T22 · FR-E15):`auditTrail` 的只读投影,**不新增存储**。
1841
+ *
1842
+ * ⚠️ 返回的是 `TraceResult` 而不是裸数组:`maxAuditEntries` 裁剪之后
1843
+ * 裸数组与完整轨迹**无从区分** —— 宿主会把"只剩最近 3 条"当成"一共就 3 条"。
1844
+ *
1845
+ * @throws `ENGINE_STATE_NOT_FOUND` —— 实例不存在
1846
+ */
1847
+ exportTrace(instanceId: string): Promise<TraceResult>;
1848
+ /**
1849
+ * ★ 门 2 入口:纯函数,不碰存储。
1850
+ * 本档只补上 `EngineConfig` 里的 `clock` / `maxAuditEntries`,其余交给调用方。
1851
+ */
1852
+ plan(state: InstanceState, action: ActionInput, options?: PlanOptions): PlanResult;
1853
+ }
1854
+ declare function createEngine(config: EngineConfig): Engine;
1855
+
1856
+ /**
1857
+ * 条件求值 —— 网关分支 / 顺序流的 `conditionExpression`(`03-engine` §7 与 §8.3)。
1858
+ *
1859
+ * 出口判据 **AC-E9**:`@floken-io/feel` **解析不了**的语法 → **抛错**,不返回 `false`;
1860
+ * 该要求对**注入的自定义 `conditionHandler` 同样生效**(无豁免)。
1861
+ *
1862
+ * ★ 三条硬约束(改本档前先默念):
1863
+ * ① **求值失败必须抛错**,绝不静默返回 `false` —— 表达式出错却返回 false 会让流程
1864
+ * **静默走错分支**,比抛错危险十倍(`03-engine` §7.2);
1865
+ * ② **不得自带求值器** —— S-FEEL 来自 `@floken-io/feel`(§7.4(2) 的反面教材:
1866
+ * 同作者手上有 FEEL,流程侧却退化成字符串模板,最后只能宿主注入 JS);
1867
+ * ③ **不 import 时态** —— Q33:`dist` 产物不得出现 temporal(`tooling/verify.mjs` 的
1868
+ * `check:deps` 守)。本档只用 `evaluate()`,不碰 `@floken-io/feel/temporal`。
1869
+ *
1870
+ * ★ 分层:`eval/` 是域层,可 import `core/`;**`core/` 不得反向 import `eval/`**。
1871
+ */
1872
+
1873
+ /**
1874
+ * 内置 `ConditionHandler` 的可调项(透传给 `@floken-io/feel` 的 `EvaluateOptions`)。
1875
+ *
1876
+ * ⚠️ 这里**刻意不默认开启 `allowedFunctions`**:`03-engine` §7.2 那张表是「**承诺下限**,
1877
+ * 不是能力上限」,引擎实际能力 = **完整 `@floken-io/feel`**。想收紧到 S-FEEL 子集的宿主
1878
+ * 自己传白名单 —— 那是**收窄**动作,不该由引擎替他做。
1879
+ */
1880
+ interface FeelConditionOptions {
1881
+ /**
1882
+ * S-FEEL 子集白名单:设置后,调用白名单外的**具名**函数 → `@floken-io/feel` 抛
1883
+ * `FeelNotAllowedError`(`FEEL_NOT_ALLOWED_*`)。**默认不设**。
1884
+ */
1885
+ readonly allowedFunctions?: readonly string[];
1886
+ /** AST 节点数上限(防构造型攻击) */
1887
+ readonly maxNodes?: number;
1888
+ /** 表达式嵌套深度上限 */
1889
+ readonly maxDepth?: number;
1890
+ /** 协作式超时(毫秒);仅在求值步之间的检查点生效 */
1891
+ readonly timeoutMs?: number;
1892
+ }
1893
+ /**
1894
+ * 内置默认 `ConditionHandler`(**不注入即用它** —— `NFR-E10`「零配置可跑」)。
1895
+ *
1896
+ * 语义 = **FEEL 表达式**(`evaluate()`),不是 unary tests 的顶层判定 —— 见 **D-37**:
1897
+ * `unaryTest()` 的顶层语义是「输入值 `?` 是否满足该测试」,而网关条件**没有单一输入值**,
1898
+ * 实测两个方向都是静默错误:裸 `true` → `false`(被当成 `? = true`)、
1899
+ * 变量缺失 → `true`(`amount > 5000` 在空上下文里恒真)。
1900
+ *
1901
+ * @example
1902
+ * createFeelConditionHandler().evaluate('amount > 5000', ctx) // → true / false
1903
+ */
1904
+ declare function createFeelConditionHandler(options?: FeelConditionOptions): ConditionHandler;
1905
+ /**
1906
+ * ★ 条件求值的**唯一入口**(引擎内部任何地方都不得绕过它直接调 `handler.evaluate`)。
1907
+ *
1908
+ * 第 0 层要求(`AC-E9`,**对任何实现生效、无豁免**)就落在这一个函数里:
1909
+ * - 实现**抛错** → **原样传播**(不 try/catch、不降级为 `false`);
1910
+ * - 实现返回 `Promise` 且 reject → 同样传播;
1911
+ * - 实现返回**非布尔值**(含 `null` / `undefined` / 字符串)→ 抛 `OPTION_INVALID`。
1912
+ *
1913
+ * 把它做成函数而不是「纪律」的理由:宿主注入的 `conditionHandler` 是他自己的代码,
1914
+ * 引擎管不了他里面写什么 —— 但**出口**必须归引擎管,否则「注入了一个会返回 undefined 的
1915
+ * 求值器」会表现成「分支永远不走」且毫无报错。
1916
+ */
1917
+ declare function evaluateCondition(handler: ConditionHandler, expression: string, ctx: ConditionCtx): Promise<boolean>;
1918
+
1919
+ /**
1920
+ * 创建内存版 `StateStore`。
1921
+ *
1922
+ * 每个实例**独占**一份存储 —— 测试里想要干净状态就再调一次。
1923
+ * 刻意**不提供** `clear()` / `size()`:那会让 `StateStore` 接口长出非契约方法,
1924
+ * 与「禁止长出事务接口」是同一条理由(内存实现的门槛必须压在 ~20 行,见 §7.2)。
1925
+ */
1926
+ declare function createMemoryStore(): StateStore;
1927
+
1928
+ /**
1929
+ * @floken-io/engine · 主入口(package 的 `.` 导出 → dist/index.js)
1930
+ *
1931
+ * 本档只做「汇总再导出」,不放实现:实现住在 `core/`(内核契约层)、
1932
+ * `actions/`(19 项动作)、`runtime/`(执行器)、`nodes/`(27 类节点)、`store/`(默认内存实现)。
1933
+ * ★ 本档是**唯一**的公开出口(`06-仓库脚手架与发布约定` §3):`tsup` 的 `entry` 直指这里,
1934
+ * `src/` 下**不再有第二个 `index.ts`**。为什么这条要写成硬规定,见 `ARCHITECTURE.md` §5 注。
1935
+ *
1936
+ * ★ **导出面判据:「宿主为了接入引擎,必须能 import 的东西」**
1937
+ *
1938
+ * ① 契约类型 —— SPI(11 项)/ 状态模型 / 待办视图 / 事件 / 钩子;
1939
+ * ② 错误契约 —— 错误类(宿主 `instanceof` 判定)+ 码表 + 宿主自研 `StateStore` 时必须抛的工厂。
1940
+ *
1941
+ * `core/` 的**内部工具刻意不导出**:序列化守卫、`cloneState` / `deepEqual` / `assertRoundTrip`、
1942
+ * `assertInstanceState`、迁移执行器、`isEmptyDelta` / `touchedTaskIds`,以及引擎内部使用的
1943
+ * 其余错误工厂。它们**不是契约**,改动不应受 semver 约束。
1944
+ */
1945
+ /** 五包同款:供宿主做错误归属 / 版本探测 */
1946
+ declare const PACKAGE: "@floken-io/engine";
1947
+
1948
+ export { ACTION_NAMES, type ActionContext, type ActionInput, type ActionName, ActionRecord, ApproverSource, AuthResolver, BOUNDARY_TYPE, type BoundaryBinding, type BoundaryFire, type BoundaryNodeLike, CALL_RETURN_ACTION, CATCH_KINDS, type CatchBinding, type CatchKind, type CatchNodeLike, ConditionCtx, ConditionHandler, type ConvergeCtx, type ConvergeMode, type ConvergenceResult, DELIVER_ACTIONS, DecisionHandler, DefinitionSource, type DeliverInput, type DeliverMatch, type DeliverMode, type DeliverResult, ENGINE_DIAGNOSTIC_CODES, ENGINE_ERROR_CODES, type EmitInput, type Engine, EngineActionError, type EngineConfig, type EngineDiagnostic, type EngineDiagnosticCode, type EngineDiagnosticInit, EngineError, type EngineErrorCode, type EngineErrorInit, EngineEvent, type EngineHooks, EngineOptionError, EnginePersistError, type EngineSeverity, EngineStateError, EventSink, type FeelConditionOptions, type FiredBoundary, FormProvider, type GroupTally, InstanceEventName, InstanceParent, type InstanceQueue, InstanceState, InstanceStateHeader, InstanceStatus, type LoopContext, type LoopResult, MESSAGE_DELIVER_ACTION, type NodeRef, PACKAGE, PRIMITIVE_GROUPS, PRIMITIVE_NAMES, type PendingCall, type PendingCancel, type PendingTimeout, type PlanOptions, type PlanResult, type PrimitiveName, SIGNAL_DELIVER_ACTION, SYSTEM_AUDIT_ACTIONS, Scheduler, ServiceHandler, type StartOptions, StateStore, type StepInput, type SystemAuditAction, TaskDelta, TaskProjection, TaskView, type TimerDiff, type TimerKind, Token, TraceEntry, type TraceResult, VoteOutcome, applyPost, armedBoundaries, armedNamesOf, boundaryBindingOf, boundaryTokenIdOf, callReturnOf, cancelTargetsOf, castVote, catchBindingOf, convergeCtxOf, createEngine, createFeelConditionHandler, createInstanceQueue, createMemoryStore, deliverStep, diffTimers, emitAll, enabledActionNames, engineDiagnostic, evaluateCondition, evaluateConvergence, eventsOf, groupTallies, inScopeOf, instanceEventNameOf, matchingTokens, persistAlreadyExists, persistConflict, plan, promoteSequential, requiredOf, restTokenIds, settleGroups, shouldConverge, shouldTerminate, step, timeoutSpecOf, timerKeyOf, timingKeysOf, traceKindOf, traceOf, waitingNamesOf };