三面拆分⑬:流程包纯编排化 —— 编排与传递契约归包,实现方式归库,激活看双判据
模型(用户定):流程包只决定活动编排 + 活动传递的契约,不决定活动的实现方式; 节点只分活动点与审批点;图只定名称与顺序,属性在卡上填,链接好实现即可运行; 激活 = ①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:
@@ -1,8 +1,6 @@
|
||||
/** 边映射闭合校验、负责人四策略、工作流状态聚合 —— domain 测试零 mock(OS 03 / 3.2) */
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { processPackageSchema } from '@contracts';
|
||||
import type { ProcessPackage } from '@contracts';
|
||||
import { joinReady } from '../internal/domain/routing.ts';
|
||||
import { checkClosure } from '../internal/domain/closure.ts';
|
||||
import type { ActivityContractFace } from '../internal/domain/closure.ts';
|
||||
import {
|
||||
@@ -126,25 +124,6 @@ describe('checkClosure —— 不闭合不得激活', () => {
|
||||
expect(violations.filter((v) => v.kind === 'dangling-node').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
/*
|
||||
* 网关已废除(2026-07-28):含网关的定义一律违规 —— 历史版本仅可读,不可激活。
|
||||
* 分支改由源节点的条件出边表达,见下一用例。
|
||||
*/
|
||||
it('网关节点即违规 —— gateway-deprecated,历史版本不可激活', () => {
|
||||
const withGateway = pkg({
|
||||
nodes: [
|
||||
...pkg().nodes,
|
||||
{ id: 'G2' as never, kind: 'gateway', label: 'G2 料号分流', gateway: 'exclusive', mode: 'fork' },
|
||||
],
|
||||
edges: [
|
||||
...pkg().edges,
|
||||
{ id: 'g1', from: 'A4' as never, to: 'G2' as never, mapping: [], reopen: false },
|
||||
{ id: 'g2', from: 'G2' as never, to: 'end' as never, mapping: [], reopen: false },
|
||||
],
|
||||
});
|
||||
const violations = checkClosure({ pkg: withGateway, contracts, positions }).violations;
|
||||
expect(violations.filter((v) => v.kind === 'gateway-deprecated').map((v) => v.nodeId)).toEqual(['G2']);
|
||||
});
|
||||
|
||||
it('多出边异或分支:两条无条件出边 = 都想当默认 —— multi-default-edge', () => {
|
||||
const twoDefaults = pkg({
|
||||
@@ -340,92 +319,8 @@ describe('失败走向的消化与重开', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('joinReady —— 网关直通道不参与等待', () => {
|
||||
const pkg = processPackageSchema.parse({
|
||||
id: 'p-join',
|
||||
name: '汇合测试',
|
||||
version: 1,
|
||||
state: 'active',
|
||||
callInterface: { input: { type: 'object' }, output: { type: 'object' } },
|
||||
nodes: [
|
||||
{ kind: 'start', id: 'S', label: '开始' },
|
||||
{ kind: 'gateway', id: 'F', label: '分叉', gateway: 'parallel', mode: 'fork' },
|
||||
{ kind: 'activity', id: 'X', label: 'X', activityId: 'a-x', owner: { kind: 'position', position: 'BD' } },
|
||||
{ kind: 'activity', id: 'Y', label: 'Y', activityId: 'a-y', owner: { kind: 'position', position: 'BD' } },
|
||||
{ kind: 'gateway', id: 'J', label: '汇合', gateway: 'parallel', mode: 'join' },
|
||||
{ kind: 'end', id: 'E', label: '结束' },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e1', from: 'S', to: 'F', mapping: [], reopen: false },
|
||||
{ id: 'e2', from: 'F', to: 'X', mapping: [], reopen: false },
|
||||
{ id: 'e3', from: 'F', to: 'Y', mapping: [], reopen: false },
|
||||
// 分叉直连汇合的**直通道**(TYPE-A 快路):来源是网关,永远不会有实例
|
||||
{ id: 'e4', from: 'F', to: 'J', mapping: [], reopen: false },
|
||||
{ id: 'e5', from: 'X', to: 'J', mapping: [], reopen: false },
|
||||
{ id: 'e6', from: 'Y', to: 'J', mapping: [], reopen: false },
|
||||
{ id: 'e7', from: 'J', to: 'E', mapping: [], reopen: false },
|
||||
],
|
||||
variables: [],
|
||||
});
|
||||
|
||||
it('两条责任分支都收敛 → 回齐(网关直通道不算一票)', () => {
|
||||
expect(joinReady(pkg, 'J', new Set(['X', 'Y']), new Set())).toBe(true);
|
||||
});
|
||||
|
||||
it('一条容忍消化、一条收敛 → 回齐', () => {
|
||||
expect(joinReady(pkg, 'J', new Set(['X']), new Set(['Y']))).toBe(true);
|
||||
});
|
||||
|
||||
it('还差一条 → 不回齐', () => {
|
||||
expect(joinReady(pkg, 'J', new Set(['X']), new Set())).toBe(false);
|
||||
});
|
||||
|
||||
it('把网关来源也算进去就会永远等不到 —— 这是回归守卫', () => {
|
||||
// 若实现改回「全部入边都要收敛」,上面第一条断言必然转 false
|
||||
expect(joinReady(pkg, 'J', new Set(['X', 'Y']), new Set())).not.toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* 异或汇合不等 —— 这条曾经让流无声停住。
|
||||
*
|
||||
* G2J(寻源汇合)是异或:「需要寻源」走 A3、「不需要」直通,两条互斥、只来一条。
|
||||
* 而 `joinReady` 原本按并行汇合写(全部责任分支收敛才放行),于是在「不需要寻源」那条路上
|
||||
* 永远在等 A3 —— 一个结构上不会激活的分支。症状最坑的地方是**连死路事件都没有**:
|
||||
* 边是选中了的,只是在汇合判定里被 continue 跳过,从外面看就是 A1 办结后什么都不再发生。
|
||||
* joinReady 已随网关一并摘除(2026-07-28 第二轮):多入边即异或汇合 —— 上游互斥,
|
||||
* 只会来一条,**没有「等回齐」这回事**;AND 并行待「并行组」声明时再带自己的汇合语义。
|
||||
* 从前它守的坑(异或汇合等一个不可能到来的分支、流无声停住)从机制上不存在了。
|
||||
*/
|
||||
describe('joinReady —— 异或汇合只来一条,不等', () => {
|
||||
const xor = processPackageSchema.parse({
|
||||
id: 'p-xor-join',
|
||||
name: '异或汇合测试',
|
||||
version: 1,
|
||||
state: 'active',
|
||||
callInterface: { input: { type: 'object' }, output: { type: 'object' } },
|
||||
nodes: [
|
||||
{ kind: 'start', id: 'S', label: '开始' },
|
||||
{ kind: 'gateway', id: 'G', label: '要不要绕', gateway: 'exclusive', mode: 'fork', basis: '判据' },
|
||||
{ kind: 'activity', id: 'X', label: 'X', activityId: 'a-x', owner: { kind: 'position', position: 'BD' } },
|
||||
{ kind: 'gateway', id: 'J', label: '汇合', gateway: 'exclusive', mode: 'join' },
|
||||
{ kind: 'end', id: 'E', label: '结束' },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e1', from: 'S', to: 'G', mapping: [], reopen: false },
|
||||
{ id: 'e2', from: 'G', to: 'X', condition: 'go = true', mapping: [], reopen: false },
|
||||
// 直通:不绕 X,从网关直接进汇合
|
||||
{ id: 'e3', from: 'G', to: 'J', condition: 'go = false', mapping: [], reopen: false },
|
||||
{ id: 'e4', from: 'X', to: 'J', mapping: [], reopen: false },
|
||||
{ id: 'e5', from: 'J', to: 'E', mapping: [], reopen: false },
|
||||
],
|
||||
variables: [],
|
||||
});
|
||||
|
||||
it('走直通那条:X 从未激活,汇合照样放行', () => {
|
||||
expect(joinReady(xor, 'J', new Set(), new Set())).toBe(true);
|
||||
});
|
||||
|
||||
it('走 X 那条:X 收敛后放行', () => {
|
||||
expect(joinReady(xor, 'J', new Set(['X']), new Set())).toBe(true);
|
||||
});
|
||||
|
||||
// 并行汇合「该等的还得等」由上一个 describe 的「还差一条 → 不回齐」守着,不在这儿重复
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user