判据 ⑥ 解锁:容忍分支推得动汇合,汇合不再等一个永不到来的网关

三处修:

① `joinReady` 把**网关直连汇合的直通道**也算作一票要等的分支。
   网关不是责任单元、永远不会有实例,于是汇合永远等不齐,整条流卡死。
   改为:来源是网关的入边不参与等待 —— 分叉一发生它就算过了。
   (统一报价图里这条直通道就是 TYPE-A 快路。)加了回归守卫测试。

② 容忍(tolerate)只把实例标成「已消化」就收手,不往下推 ——
   `joinReady` 认 tolerated,可没人再去问一次汇合够不够齐。改为标完即续推。

③ 蒸馏钩子失败会把已落定的终态掀翻(治理台终止返回 500,任务卡在中间态)。
   蒸馏是进化管线的入口、不是终态的一部分:失败就失败,终态照落。
   失败也不再往任务记忆里追加 —— 此刻记忆已封卷,那个守卫拒得对。

结果:A1 released → G2 分叉 → A3 / A2 两个 loop 起在正确工位 →
按声明容忍收束 → **汇合回齐 → A4 成本定价起到冯兵工位**。

gates: tsc / eslint / depcruise / 397 测试 全绿

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-27 05:03:58 +08:00
co-authored by Claude Opus 5
parent c68a562db1
commit 9201f6b094
6 changed files with 109 additions and 8 deletions
@@ -1,6 +1,8 @@
/** 边映射闭合校验、负责人四策略、工作流状态聚合 —— 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 {
@@ -291,3 +293,49 @@ describe('失败走向的消化与重开', () => {
expect(nextInstanceNumber(instances, 'A1')).toBe(1);
});
});
describe('joinReady —— 网关直通道不参与等待', () => {
const pkg = processPackageSchema.parse({
id: 'p-join',
name: '汇合测试',
version: 1,
state: 'published',
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);
});
});