Files
JX-BPOS/apps/api/src/modules/process/CONTRACT.md
T
ChaiHaoandClaude Fable 5 9df2294f6e Mastra 胶合层:FlowRuntime 的第二实现 —— run ↔ 实例,发号权留在主工程
编译器只管推图,胶合层管一单一单的账(createMastraRuntime):

- **发号权在主工程**:实例号 nextInstanceNumber 算、sys.node_instance 落笔;
  Mastra 不发实例号也不发任务号(spike 实测)
- **run ↔ 实例**:runId = <workflowId>#<seq>,重开 = 新 run(seq+1),旧 run 冻结;
  当前 run 的入口记在 vars.__runEntry —— 重开后从回边目标那份变体起步
- **迟到 / 重复回调不甩给 Mastra**:先在实例表定位、确认是当前 open 的那个,
  不是就发 stale-callback 丢弃(spike 实测:错投的 resume 会打坏 run,
  嵌套下甚至不可恢复)
- **失败走向不进 Mastra**:改道/升审批/容忍都当「从目标节点重新入场」,
  起一个以它为入口的新 run —— 绕开 .parallel() 那套「任一失败即整体失败」
- **状态仍是聚合投影**:refreshState 读 sys.node_instance,不读 Mastra 快照
  (run 只有 running/suspended/success/failed,没有「收敛未到终点=abnormal」)
- MastraRunner 是 Mastra 侧的最小口子(startRun/resumeRun),换版本只改它

四闸过。**尚欠**:MastraRunner 的真实现(Mastra 实例 + PostgresStore 挂
mastra schema)与接线开关 —— 编译器与胶合层都有单测但没在真库上跑过整条流,
转正前自持实现仍是唯一在跑的路径;已如实记进 debt M5-1。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
2026-07-29 00:19:37 +08:00

10 KiB

module: process

职责: 流程层:流程包定义与版本、边映射闭合校验(连线即映射 · 画图不检查 · 不闭合不得激活)、 负责人四策略的确定性解析、工作流状态的聚合投影、失败走向的消化判定、节点实例重开 不负责: 活动的执行(见 task 与范式引擎)、触发器(与流程包平级,见 trigger)、 画布的绘制(见 apps/web 的 gov-process)

公开出口: index.ts

  • 类型:ProcessService StartWorkflowInput WorkflowRecord ClosureReport ClosureViolation ActivityContractFace Candidate Resolution NodeInstance WorkflowState Dispatch
  • 错误:NotClosedError PinnedVersionError PositionVacantError
  • 端口类型:ProcessStore ActivityDirectory OrgDirectory TaskGateway ApprovalGateway Sfeel FlowEventPublisher ReceiptNotifier
  • 用例工厂:createProcessService(deps): ProcessService(含 setPackageState 版本启停)
  • 纯函数:checkClosure closed resolveOwner aggregateWorkflowState (resolveOwner 的 bound 入参 = 已校验的工作组绑定,ADR-011) isDigested nextInstanceNumber selectEdges assemble rejectRouteOf dispatchOf startNodeOf advanceOrder (advanceOrder = 推进次序,按边拓扑排、排除回边、并行支按编号定序; 呈现用,不参与流转判断 —— 图纸坐标不配决定次序)
  • adapter:createProcessStore;Drizzle 表:processPackageTable workflowTable nodeInstanceTable

层级: L7(编码规范 §4) 依赖: @contracts;task(L6)、@sfeel 被依赖: trigger, app(治理台 · 工作流管理)

不变量(OS 03 / 3.2):

  • 编辑即其身(产品决策 2026-07-28 第四轮):savePackage 对已存在的版本 就地写回(号不变、状态不变);新版本只出自显式新建(asNew: true)或 新流程首版(插入,retired 不生效)。两道底线: ① 在途钉版不可变 —— 该版有未完结工作流(按 (包, 版本) 钉定义)即抛 PinnedVersionError(HTTP 409),一字不落库;改动请新建一版。 ② 生效版必须始终可运行 —— 写回生效版跑 checkClosure,不闭合抛 NotClosedError(HTTP 412 带整份报告);未生效版画图不检查、随手存。
  • 激活才查完整:setPackageState 激活时跑 checkClosure, 不闭合抛 NotClosedError(HTTP 412 带整份报告)。 违规项:未完成边、必填未映射、类型冲突、孤立节点、多默认出边、 活动未装载、容忍分支被下游必填依赖 —— 任一即 closed = false,激活被拒。 (岗位不入编排,闭合校验没有岗位类违规。)
  • 纯编排模型(产品决策 2026-07-28 二、三轮):流程包只决定活动编排与 活动传递的契约,不决定活动的实现方式(范式/版本住活动库,节点仅以 activityId 链接),也不带负责人岗位(分派归运营;运营分派机制未建前, 活动任务运行时默认全员轮值 —— 在岗候选并集、最闲者先、staffId 稳定排序。 审批点例外:「卡到人的工位」是它的功能语义,approver 留在声明里, 工作组绑定 ADR-011 也只对它生效)。节点只分活动点与审批点 (起点/终点是结构件);kind: 'gateway' 已从 schema 摘除。 分支 = 源节点的多条条件出边(恰取一条,无条件边作默认兜底、至多一条), 汇合 = 多入边异或(上游互斥,只会来一条 —— joinReady 已删,没有「等回齐」)。 AND 并行暂无声明机制,将来以「并行组」加回(对应 Mastra .parallel,见 debt.md)。
  • 边有编号身份:语义边 id 即边号(E1、E2…,服务端/合并器铸号,旧边保号、 新边取下一个空号);画布线标带号、边在图上可选中,条件与映射住属性面。 顺序流一律画成真边(卡点画法已废除)—— 画布保存由画出来的边反推语义边, 图上没画的语义边会在存盘时丢失,种子与测试按「一一对应」守着。
  • 激活双判据:①图编排闭合完善(结构 + 属性 + 传递契约,checkClosure 的 非 unknown-activity 项)②活动装载(节点链接的实现已安装,unknown-activity 项)。 两面都清零才可激活;图照画照存,未装载不拦保存。 系统隐性上下文(审批 verdict、放行选口 outcome 的判据注入、起单载荷穿透) 由引擎决定,不进设计者的编排面。
  • 一条流恰一个生效版本(产品决策 2026-07-28):setPackageState 激活即独占 (同流其余版本一并停用);停用生效版 = 整条流下线,新起单被拒。 起单取生效版;在途工作流按启动时钉住的版本号读定义 —— 启停对它们零影响。
  • 版本号服务端算(新建时,不采信入参):工作流按 (包id, 版本) 读定义; store 的写回(updatePackageDefinition)只许命中已存在的版本,撞不存在报错; 「就地覆盖会改掉在途正在读的编排」由在途钉版闸守着,不再靠只插不改一刀切。
  • 求值语义前后端同源:画布的即时校验与后端裁决共用 checkClosure
  • 负责人解析确定性:轮值并列时按 staffId 稳定排序;同输入必同结果,结果与依据留痕(owner-resolved 事件 + 实例 assignee)
  • 工作流状态是聚合投影:流程层不另设状态机;计量对象是节点实例, 重开产生新实例、旧实例不再入算;XOR 未选分支从未激活、不入集合
  • 「完成」凭到过终点节点,不凭「没有在跑的了」:收敛但未到终点 = 卡住 = abnormal。 aggregateWorkflowState 收第三个入参(包的终点 id 集);不传则退回旧口径,新代码一律传
  • 失败走向的消化:改道 / 容忍 / 升审批一经生效即消化;终止流待人工处置中算异常
  • 流级重做一律重开新实例 —— 同节点、新任务 ID、留痕关联,不复活旧任务

其它不变量:

  • 流转零 LLM:选边与条件求值全部经 @sfeel;判定依据入事件记录
  • 多出边一律异或,不分节点种类:条件命中优先(按声明序),无条件的边作默认兜底; 一条都没中而该节点确有出边 = 死路,发 route-dead-end 事件 (哪个节点、各出边的条件、判据里当时有哪些键 —— 只记键名不记值)。 审批点的裁决(verdict)由引擎注入判据,两个去向就是它自己的两条条件出边
  • 异或汇合不等:互斥分支结构上只会来一条,等齐 = 等一个不可能到来的分支, 流会无声卡死 —— 汇合等待机制已随网关整体摘除,容忍分支消化后直接推进
  • 流程层不自动重试活动(过程内重试归 Harness); 流级重做一律人裁决后重开新任务实例
  • 驳回走向声明式:审批点的驳回按 reject 声明回指定节点重开新实例,不由临场决定
  • 凡单必有来源:source 必填;终点按输出声明交付、按来源回执

M5-1:运行时切面已立 + Mastra 编译器已落(2026-07-28/29)

「Mastra 编译器作可替换 adapter、usecase 不动」的端口已经存在:port.ts 的 FlowRuntime(begin / onTaskTerminal / applyFailure / onApprovalVerdict / refreshState / terminate)。执行面的语义、留痕与副作用编排整体住在默认实现 internal/runtime/self-hosted-runtime.ts(自持确定性状态机 + sys.node_instance 持久);process-service 是薄壳:定义生命周期(保存/激活/校验)+ 起单前置 (取生效版/调用接口校验/工作组快照/建单留痕)+ 五方法委托。

编译器(internal/adapter/workflow-compiler.ts,出口 compilePackage)已落, 走 spike 定的路线 B(证据:spike/mastra-reopen/FINDINGS.md,实测):

  • 回边不连回去,落成一枚回边出口步骤 —— 本 run 到此为止,把 { reopen, reopenedBy, reopenTarget, payload } 写进 result,胶合层据此起新 run。 理由:①1 run = 1 实例,与「重开产生新实例、旧实例留痕不动」一一对应 (.dowhile 那条路引擎会覆盖上一轮记录);②迟到/重复回调在 .dowhile 下 把整条 run 打成不可恢复的 failed,而活动终态本就来自不受控的外部调用链。
  • 每个回边目标各编译一份「以它为入口」的变体(<包id>@<节点id>)—— 新 run 只能从入口起步。
  • 条件出边 → .branch() + XOR 收口步:Mastra 把未命中的分支键也放进输出 (值 undefined),收口步过滤并断言恰一条 —— 「恰取一条」在运行时照旧成立。
  • 活动点/审批点 → 「起任务 → suspend()」,副作用写在 if (!resumeData) 里 (resume 时 execute 从头重跑,不这么写会起两次任务)。
  • 编译期全验:悬空边、正向边成环(未标 reopen)一律当场报 —— 编译只走到得了的边, 悬空边可能落在递归碰不到的角落。

胶合层(internal/adapter/mastra-runtime.ts,出口 createMastraRuntime)= FlowRuntime 的 Mastra 实现,与自持状态机对等。它管的是「一单一单的账」:

  • 发号权在主工程:实例号 nextInstanceNumber 算、sys.node_instance 落笔 (Mastra 不发实例号也不发任务号);
  • run ↔ 实例:runId = <workflowId>#<seq>,重开 = 新 run(seq+1),旧 run 冻结; 当前 run 的入口记在工作流 vars.__runEntry;
  • 迟到 / 重复回调不甩给 Mastra:先在实例表定位、确认是当前 open 的那个, 不是就发 stale-callback 事件丢弃(错投的 resume 会打坏 run);
  • 失败走向不进 Mastra:改道/升审批/容忍都当作「从目标节点重新入场」—— 起一个以它为入口的新 run,绕开 .parallel() 那套连坐语义;
  • 状态仍是聚合投影:refreshState 读 sys.node_instance,不读 Mastra 快照。

替换路径:ProcessServiceDeps.runtime 注入 createMastraRuntime({..., runner }); MastraRunner 是 Mastra 侧的最小口子(startRun / resumeRun),换版本只改它。 八例端到端是两个实现的等价性护栏;接线开关与真跑验证见 debt M5-1。 仍须绕的语义冲突:.parallel() 任一失败即整体失败(step 内自吞,不让 Mastra 感知)、 三态聚合投影(abnormal 防假成功,Mastra run 无此态)、node_instance 投影表必须照写(治理台按行查、实例发号权仍在主工程)。