This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-TMP-REPO/apps/api/src/modules/process/CONTRACT.md
T
ChaiHaoandClaude Opus 5 22aa44c775 手工走通整条流,揪出五处编排层问题 —— 最要命的是假成功
把 `unified-quote` 从起单一路手工答到底(这是逐个开发活动之前该做的验证),
一路撞出五处,按撞到的顺序:

① 七个活动派出七张一模一样的「请处理」
   槽位骨架给的是 `card: {}`,工单标题/字段全落回默认。收卡的人不知道自己走到
   哪一步、这一步要干什么,整条流没法手工走通。→ 卡面自述:标题就是步骤标签,
   依据带「第几步 / 共几步」,并如实说「这一步的自动化还没开发,先由你来办」。

② 走到第 3 步就 `exhausted` 终态、工作流转 abnormal
   六个槽位共用 `humanCalls: 3`,而 A1 声明了 6 个人类步 —— 活动**结构上永远走不完**。
   → `budgetFor(n)` 按各槽位自己的步数给。步/项清单提成 `STEPS` 常量,
   预算与骨架共用同一个数组,数不上不会漂移。治理台申报值也同源(否则台上摆假数)。

③ 六步答完,任务卡在「等人,却没有任何东西等人答」
   `flow-runner` 写死了 `released = lastStepIsAcceptance && lastVerdictPassed` ——
   末步不是验收工单,活动就永远不 release。→ 声明的六步来自 MVP 作业指导书,
   不篡改语义,在其后**追加**一步「收口验收」。

④ 只走了 A1(一个节点实例)却报 `completed` ← **最危险的一处**
   状态聚合把「没有在跑的了」当成完成。界面上是一单已完成的报价,业务上一步没走。
   → 「完成」凭**到过终点节点**;收敛而未到终点 = 卡住 = `abnormal`,进驾驶舱异常卡。
   `aggregateWorkflowState` 收第三个入参(终点 id 集),不传退回旧口径。

⑤ 异或网关一条边都选不中时,流无声停住
   ④ 让它至少报异常了,但只说「有问题」不说「哪儿」。→ 发 `route-dead-end` 事件:
   哪个节点、各出边的条件、判据里当时有哪些键。只记键名不记值 —— 诊断够用,
   载荷不进事件明文。实测这条痕直接指出「G1 要 needAlternative,而键里没有」。

顺带:改槽位内容必须升包版(0.1.0 → 0.4.0)。「安装恒为新增、版本并存」是硬不变量,
同版重装是 no-op、manifest 一字不动 —— 我第一次改完重播种,屏幕报「已装入」而库里还是旧的。
`installSlots` 现在先问库、如实报「已装」还是「已在库,未改动 —— 要换内容请升包版」。

**走查后的诚实状态**:A1 走得完、活动 released;流**过不了 G1**,因为出口的必填去向旗
产不出来。这不是缺陷 —— 出口载荷由台账落地事实推导,槽位没实现台账写入就没有事实,
凭空捏一个就是在系统内部 mock。于是 A1 的开发完成判据也具体了:必须让
`needAlternative` / `needSourcing` 成为落地事实,否则 G1 永远判不了。债务簿已记。

另记一条闸的盲点:api:check 只快照 index/contract/port.d.ts,而 index 是 re-export 名单 ——
给纯函数加形参它看不见(本次实测)。改签名要手动同步 CONTRACT.md,别指望闸提醒。

typecheck ✓ lint ✓ depcruise 374 模块 ✓ api:check ✓ 490 测试 ✓(均以退出码验)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
2026-07-28 11:11:32 +08:00

4.0 KiB

module: process

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

公开出口: index.ts

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

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

不变量(OS 03 / 3.2):

  • 不闭合不得发布:未完成边、必填未映射、类型冲突、孤立节点、异或出边缺条件、 汇合同名字段来源不明、容忍分支被下游必填依赖 —— 任一即 publishable = false
  • 求值语义前后端同源:画布的即时校验与后端裁决共用 checkClosure
  • 负责人解析确定性:轮值并列时按 staffId 稳定排序;同输入必同结果,结果与依据留痕
  • 工作流状态是聚合投影:流程层不另设状态机;计量对象是节点实例, 重开产生新实例、旧实例不再入算;XOR 未选分支从未激活、不入集合
  • 「完成」凭到过终点节点,不凭「没有在跑的了」:收敛但未到终点 = 卡住 = abnormal。 aggregateWorkflowState 收第三个入参(包的终点 id 集);不传则退回旧口径,新代码一律传
  • 失败走向的消化:改道 / 容忍 / 升审批一经生效即消化;终止流待人工处置中算异常
  • 流级重做一律重开新实例 —— 同节点、新任务 ID、留痕关联,不复活旧任务

其它不变量:

  • 流转零 LLM:选边与条件求值全部经 @sfeel;判定依据入事件记录
  • XOR 恰取一条:条件命中优先,无条件的边作默认分支兜底; 一条都没中而该节点确有出边 = 死路,发 route-dead-end 事件 (哪个节点、各出边的条件、判据里当时有哪些键 —— 只记键名不记值)
  • AND 汇合要回齐:未回齐就不进汇合节点;容忍分支以「已消化」计,不阻汇合
  • 流程层不自动重试活动(过程内重试归 Harness); 流级重做一律人裁决后重开新任务实例
  • 驳回走向声明式:审批点的驳回按 reject 声明回指定节点重开新实例,不由临场决定
  • 凡单必有来源:source 必填;终点按输出声明交付、按来源回执

M5 待补(docs/PROGRESS.md · docs/debt.md M5-1)

internal/adapter/workflow-compiler.ts —— 编码规范 §6.1 规定流程包动态编译为 Mastra createWorkflow/createStep(XOR → .branch(),AND → .parallel(),节点 suspend()/resume())。 当前流转由本模块自持的确定性状态机 + sys.node_instance 持久承担,语义等价且已可测; Mastra 编译器作为可替换的 adapter 后补,替换时 usecase 不动。