三面拆分⑬:流程包纯编排化 —— 编排与传递契约归包,实现方式归库,激活看双判据
模型(用户定):流程包只决定活动编排 + 活动传递的契约,不决定活动的实现方式; 节点只分活动点与审批点;图只定名称与顺序,属性在卡上填,链接好实现即可运行; 激活 = ①Mastra 式图编排闭合完善 + ②活动装载,两面判据。 schema(库经外科手术清空,无历史包袱,网关彻底出局): - contracts:删 gateway 节点变体 / GATEWAY_KINDS / GATEWAY_KIND_LABEL / 'gateway-deprecated' 违规项 —— 节点四种:start/end(结构件)+ activity/approval - routing:joinReady 整个删除(多入边即异或汇合,没有「等回齐」;AND 待并行组); selectEdges 精简;closure 删 checkGateway;process-service 删汇合等待与网关直传, 节点一律只收装配后载荷(verdict/outcome 判据由系统注入,不走映射) 编排面(画布 = 名称 + 顺序,其余在属性卡): - 元素盘五件:活动点(单一,不再分标准/案例 —— 那是实现方式)/审批点/起止/卡点 - 属性卡:节点类型二分(活动点/审批点);新增「链接活动实现」选择器(装载面, FlowNode.act 经 merge 成为语义 activityId);范式/版本降为只读装载信息 - 边属性可填:条件(S-FEEL)+ 映射(上游输出口/起单载荷/常量三种来源; workflow-var/expression 归系统隐性,不进设计者面)—— FlowEdge.condition/mapping 工作副本,pkg-to-graph 回填、graph-to-pkg 合并,清空条件即变默认兜底边 - 异常看板 → 激活双判据看板:图编排(结构+属性+契约)与活动装载(unknown-activity) 分面亮灯,两面清零激活才亮 - pkg-to-graph 去 paradigms 绿化与网关分支;图例/文案全线「活动点/审批点」 测试:closure 27(joinReady 两组 describe 随机制摘除)、advance-order 按真图形状重写 (去 as never 捏造的网关节点)、graph-to-pkg 9(新增 act 合并 + 边契约合并/清空用例)、 seed 8、端到端八例真库全绿;五闸退出码验过(api:check 提交后转绿)。 库:purge 后重种,唯一 unified-quote@v1 生效(10 节点 14 边,双面判据齐)。 债:M5-5 清账(兼容路径连 schema 一起摘);M5-4(并行组)仍挂。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -6,8 +6,6 @@
|
||||
* 存在未完成边、未满足必填或类型冲突的流程包**存不进来**。
|
||||
* 多出边即异或分支(恰取一条,默认兜底至多一条);多入边即异或汇合。」
|
||||
*
|
||||
* 网关已废除(2026-07-28):schema 仍收 gateway 只为读历史 —— 见 checkGateway。
|
||||
*
|
||||
* 原文是「可存草稿、不得发布」——**已不成立**:没有草稿态也没有发布门,
|
||||
* 闸落在保存上(严格检查 + 保存即生效)。画布上的半成品留在浏览器里,存不进库。
|
||||
*
|
||||
@@ -68,10 +66,6 @@ export function checkClosure(input: ClosureInput): ClosureReport {
|
||||
message: `审批点「${node.label}」的岗位不存在:${node.approver.position}`,
|
||||
});
|
||||
}
|
||||
if (node.kind === 'gateway') {
|
||||
violations.push(...checkGateway(node, incoming.get(node.id) ?? [], outgoing.get(node.id) ?? []));
|
||||
}
|
||||
|
||||
/*
|
||||
* 多出边 = 异或分支(恰取一条):条件命中优先,无条件边作默认兜底。
|
||||
* 默认兜底**至多一条** —— 两条无条件出边就是「都想当默认」,运行时按声明序
|
||||
@@ -169,28 +163,6 @@ function checkActivityNode(
|
||||
return out;
|
||||
}
|
||||
|
||||
function checkGateway(
|
||||
node: Extract<ProcessNode, { kind: 'gateway' }>,
|
||||
inbound: readonly Edge[],
|
||||
outbound: readonly Edge[],
|
||||
): ClosureViolation[] {
|
||||
void inbound;
|
||||
void outbound;
|
||||
/*
|
||||
* **网关已废除**(产品决策 2026-07-28):分支 = 源节点的条件出边,汇合 = 多入边异或。
|
||||
* schema 仍收 gateway 只为**读历史版本** —— 在途工作流照走(运行时兼容),
|
||||
* 但含网关的定义不再可激活:这里判违规,激活闸(不闭合不得激活)就会拦下。
|
||||
* 想复用旧图,在生效版上把网关的出边改画成源活动的条件出边即可。
|
||||
*/
|
||||
return [
|
||||
{
|
||||
kind: 'gateway-deprecated',
|
||||
nodeId: node.id,
|
||||
message: `网关「${node.label}」已废除 —— 把分支写成上游节点的条件出边(历史版本仅可读,不可激活)`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 容忍(仅限 AND 分支):静态校验其**下游映射不得依赖该分支的必填字段**(OS 03 失败走向)。
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 流程引擎的确定性流转 —— 纯逻辑,零 IO、**零 LLM**(编码规范 §5.3;语义出处 OS 04)。
|
||||
*
|
||||
* 「流程引擎 —— 确定性流转:接收调用接口的起单、按结构调度节点、把审批点卡到人的工位、
|
||||
* 执行网关的分合、把每次流转记入事件。**零 LLM、零临场**。」
|
||||
* 按条件出边选路、把每次流转记入事件。**零 LLM、零临场**。」
|
||||
*/
|
||||
import type { Edge, ProcessNode, ProcessPackage } from '@contracts';
|
||||
|
||||
@@ -72,66 +72,20 @@ export function selectEdges(
|
||||
scope: Readonly<Record<string, unknown>>,
|
||||
evaluate: PredicateEvaluator,
|
||||
): Edge[] {
|
||||
const node = pkg.nodes.find((n) => n.id === fromNodeId);
|
||||
const outgoing = pkg.edges.filter((e) => e.from === fromNodeId);
|
||||
|
||||
/*
|
||||
* **多出边一律异或,不分节点种类**(产品决策 2026-07-28:网关去节点化)。
|
||||
*
|
||||
* 分支不再是网关节点的专利 —— 活动/审批点自己的多条条件出边就是分支,
|
||||
* 判据读的是它自己的出参(与 Mastra 的 `.branch` 同构:条件挂在步与步之间)。
|
||||
* 语义与从前的异或网关逐字相同:条件命中优先(按声明序),无条件边作默认兜底,
|
||||
* 恰取一条。并行(AND)没有节点了也没有声明机制 —— 要并行时以「并行组」声明加回
|
||||
* (对应 Mastra 的 `.parallel` 结构块),见债务簿。
|
||||
*
|
||||
* 遗留的网关节点(历史版本里还有)走同一条逻辑 —— 异或网关的语义本来就是这个;
|
||||
* 并行网关分叉的「全发」不再支持,历史包若有,激活时已被闭合校验拦下。
|
||||
* **多出边一律异或,不分节点种类**(与 Mastra 的 `.branch` 同构:条件挂在步与步之间)。
|
||||
* 条件命中优先(按声明序),无条件边作默认兜底,恰取一条。
|
||||
* 并行(AND)没有声明机制 —— 要并行时以「并行组」加回(对应 `.parallel`,债 M5-4)。
|
||||
*/
|
||||
if (outgoing.length <= 1) {
|
||||
// 并行网关的遗留直通不受损:单出边不判条件,原样走
|
||||
void node;
|
||||
return outgoing;
|
||||
}
|
||||
if (outgoing.length <= 1) return outgoing;
|
||||
const hit = outgoing.find((e) => e.condition !== undefined && evaluate(e.condition, scope));
|
||||
const fallback = outgoing.find((e) => e.condition === undefined);
|
||||
const chosen = hit ?? fallback;
|
||||
return chosen === undefined ? [] : [chosen];
|
||||
}
|
||||
|
||||
/**
|
||||
* AND 汇合是否回齐:全部**责任分支**的来源都已收敛。
|
||||
*
|
||||
* 网关本身不是责任单元、永远不会有实例,所以来源是网关的入边(分叉直连汇合的**直通道**)
|
||||
* 不参与等待 —— 分叉一发生它就算过了。若把它也算进去,汇合会永远等一个不可能到来的
|
||||
* 「网关收敛」,整条流卡死在这里。
|
||||
*/
|
||||
export function joinReady(
|
||||
pkg: ProcessPackage,
|
||||
joinNodeId: string,
|
||||
converged: ReadonlySet<string>,
|
||||
tolerated: ReadonlySet<string>,
|
||||
): boolean {
|
||||
const node = pkg.nodes.find((n) => n.id === joinNodeId);
|
||||
|
||||
/*
|
||||
* **异或汇合不等。**
|
||||
*
|
||||
* 上面那句「回齐」是按并行汇合写的:AND 分叉出去的每一支都要跑,所以汇合处要等齐。
|
||||
* 异或汇合是另一回事 —— 两条分支互斥,**结构上只会来一条**,另一条永远不激活。
|
||||
* 拿「全部入边都收敛」去要求它,就是在等一个不可能到来的分支:
|
||||
* 实测 G2J(寻源汇合)在「不需要寻源」这条路上永远 false,流无声停在 G2J,
|
||||
* 而且**连死路事件都没有** —— 边是选中了的,只是在汇合判定里被 continue 跳过,
|
||||
* 从外面看就是 A1 办结后什么都不再发生。
|
||||
*/
|
||||
if (node?.kind === 'gateway' && node.gateway === 'exclusive') return true;
|
||||
|
||||
const isGateway = (nodeId: string): boolean =>
|
||||
pkg.nodes.find((n) => n.id === nodeId)?.kind === 'gateway';
|
||||
const inbound = pkg.edges.filter((e) => e.to === joinNodeId && !isGateway(e.from));
|
||||
// 容忍分支以「已消化」计,不阻汇合
|
||||
return inbound.every((e) => converged.has(e.from) || tolerated.has(e.from));
|
||||
}
|
||||
|
||||
/** 节点该被怎么调度 */
|
||||
export function dispatchOf(node: ProcessNode, payload: Record<string, unknown>): Dispatch | undefined {
|
||||
if (node.kind === 'activity') return { kind: 'start-task', node, payload };
|
||||
|
||||
@@ -23,7 +23,7 @@ import type {
|
||||
import { checkClosure } from '../domain/closure.ts';
|
||||
import { aggregateWorkflowState, isDigested, nextInstanceNumber, resolveOwner } from '../domain/owner-resolution.ts';
|
||||
import type { NodeInstance } from '../domain/owner-resolution.ts';
|
||||
import { assemble, dispatchOf, joinReady, rejectRouteOf, selectEdges, startNodeOf } from '../domain/routing.ts';
|
||||
import { assemble, dispatchOf, rejectRouteOf, selectEdges, startNodeOf } from '../domain/routing.ts';
|
||||
|
||||
export type ProcessServiceDeps = {
|
||||
readonly store: ProcessStore;
|
||||
@@ -229,23 +229,13 @@ export function createProcessService(deps: ProcessServiceDeps): ProcessService {
|
||||
const target = pkg.nodes.find((n) => n.id === edge.to);
|
||||
if (target === undefined) continue;
|
||||
|
||||
if (target.kind === 'gateway' && target.mode === 'join') {
|
||||
const instances = await deps.store.listInstances(record.id);
|
||||
const converged = new Set(instances.filter((i) => i.terminal === 'released').map((i) => i.nodeId));
|
||||
const tolerated = new Set(instances.filter((i) => i.failureDigested === true).map((i) => i.nodeId));
|
||||
// 未回齐就等着 —— 别的分支到了会再来一次
|
||||
if (!joinReady(pkg, target.id, converged, tolerated)) continue;
|
||||
}
|
||||
|
||||
const assembled = assemble(edge, { ...scope, ports: { ...scope.ports, [fromNodeId]: payload } }, deps.sfeel.expression);
|
||||
/*
|
||||
* 网关不是责任单元,**按到达的东西路由**:它没有入参契约要满足,
|
||||
* 上游的输出口载荷要原样穿过去,判据才读得到(如 G4 读 outcome)。
|
||||
* 若只给边映射装配出来的那份,无映射的边就把判据依据整个丢掉,
|
||||
* 异或网关会一条分支都选不中,整条流悄无声息地停住。
|
||||
* 活动与审批点仍只收**装配后的**载荷 —— 它们有契约,不该被上游多余字段污染。
|
||||
* 多入边即异或汇合:上游互斥,只会来一条 —— **没有「等回齐」这回事**。
|
||||
* (等待逻辑随网关一起摘除;AND 并行待「并行组」声明时再带自己的汇合语义。)
|
||||
* 节点只收**装配后的**载荷:活动与审批点都有契约,不被上游多余字段污染;
|
||||
* 判据要用的隐性上下文(审批 verdict、放行选口 outcome)由系统注入判据作用域,不走映射。
|
||||
*/
|
||||
const arriving = target.kind === 'gateway' ? { ...payload, ...assembled } : assembled;
|
||||
const arriving = assemble(edge, { ...scope, ports: { ...scope.ports, [fromNodeId]: payload } }, deps.sfeel.expression);
|
||||
try {
|
||||
await enter(record, pkg, target, arriving);
|
||||
} catch (error) {
|
||||
@@ -450,8 +440,7 @@ export function createProcessService(deps: ProcessServiceDeps): ProcessService {
|
||||
if (disposition.kind === 'tolerate') {
|
||||
/*
|
||||
* 容忍 = 这条支线的失败被吸收,**流继续往下走**。
|
||||
* 只把实例标成「已消化」而不往下推,汇合就永远等不到它:
|
||||
* `joinReady` 认 tolerated,可没人再去问一次它够不够齐。
|
||||
* 光把实例标成「已消化」而不往下推,这条线就断在这儿 —— 下游谁也不会再被叫起。
|
||||
* 载荷为空 —— 容忍分支本就没有产出,下游的必填映射不得依赖它(闭合校验已拦)。
|
||||
*/
|
||||
await advanceFrom(record, pkg, nodeId, {});
|
||||
|
||||
Reference in New Issue
Block a user