forked from ChaiHao/BPOS-MVP
判据 ⑥ 解锁:容忍分支推得动汇合,汇合不再等一个永不到来的网关
三处修: ① `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:
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"messageId": "rfq-050@x.com",
|
||||
"from": "buyer@x.com",
|
||||
"to": [
|
||||
"rfq@we.com"
|
||||
],
|
||||
"cc": [],
|
||||
"subject": "【询价】客户 C040 RFQ-20260727R 求报价",
|
||||
"body": "客户编码 C040,询价单号 RFQ-20260727R。\nPN-NEW-888 TI 年用量 9000 目标价 6.00 交期 10w",
|
||||
"receivedAt": "2026-07-27T17:00:00.000Z",
|
||||
"attachments": []
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"messageId": "rfq-060@x.com",
|
||||
"from": "buyer@x.com",
|
||||
"to": [
|
||||
"rfq@we.com"
|
||||
],
|
||||
"cc": [],
|
||||
"subject": "【询价】客户 C050 RFQ-20260727S 求报价",
|
||||
"body": "客户编码 C050,询价单号 RFQ-20260727S。\nPN-NEW-999 TI 年用量 9000 目标价 6.00 交期 10w",
|
||||
"receivedAt": "2026-07-27T18:00:00.000Z",
|
||||
"attachments": []
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -87,14 +87,22 @@ export function selectEdges(
|
||||
return outgoing;
|
||||
}
|
||||
|
||||
/** AND 汇合是否回齐:全部入边的来源分支都已收敛 */
|
||||
/**
|
||||
* AND 汇合是否回齐:全部**责任分支**的来源都已收敛。
|
||||
*
|
||||
* 网关本身不是责任单元、永远不会有实例,所以来源是网关的入边(分叉直连汇合的**直通道**)
|
||||
* 不参与等待 —— 分叉一发生它就算过了。若把它也算进去,汇合会永远等一个不可能到来的
|
||||
* 「网关收敛」,整条流卡死在这里。
|
||||
*/
|
||||
export function joinReady(
|
||||
pkg: ProcessPackage,
|
||||
joinNodeId: string,
|
||||
converged: ReadonlySet<string>,
|
||||
tolerated: ReadonlySet<string>,
|
||||
): boolean {
|
||||
const inbound = pkg.edges.filter((e) => e.to === joinNodeId);
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -337,6 +337,16 @@ export function createProcessService(deps: ProcessServiceDeps): ProcessService {
|
||||
if (target !== undefined) await enter(record, pkg, target, { __terminal: terminal, __from: nodeId });
|
||||
}
|
||||
|
||||
if (disposition.kind === 'tolerate') {
|
||||
/*
|
||||
* 容忍 = 这条支线的失败被吸收,**流继续往下走**。
|
||||
* 只把实例标成「已消化」而不往下推,汇合就永远等不到它:
|
||||
* `joinReady` 认 tolerated,可没人再去问一次它够不够齐。
|
||||
* 载荷为空 —— 容忍分支本就没有产出,下游的必填映射不得依赖它(闭合校验已拦)。
|
||||
*/
|
||||
await advanceFrom(record, pkg, nodeId, {});
|
||||
}
|
||||
|
||||
return this.refreshState(workflowId);
|
||||
},
|
||||
|
||||
|
||||
@@ -266,12 +266,23 @@ export function createTaskService(deps: TaskServiceDeps): TaskService {
|
||||
|
||||
// 终态封卷 → 归档 → 蒸馏钩子产出活动记忆候选条目
|
||||
await deps.memory.seal(taskId);
|
||||
await deps.memory.distill({
|
||||
taskId,
|
||||
activityId: task.activityId,
|
||||
trigger: 'terminal',
|
||||
prompt: `任务「${task.title}」以 ${terminal} 收场(${reason})。归纳可复用的事实、模式、裁决与例外。`,
|
||||
});
|
||||
try {
|
||||
await deps.memory.distill({
|
||||
taskId,
|
||||
activityId: task.activityId,
|
||||
trigger: 'terminal',
|
||||
prompt: `任务「${task.title}」以 ${terminal} 收场(${reason})。归纳可复用的事实、模式、裁决与例外。`,
|
||||
});
|
||||
} catch {
|
||||
/*
|
||||
* 蒸馏是**进化管线**的入口,不是终态的一部分:它产出的是待回放待放行的候选增量。
|
||||
* 它失败(模型不可达等)绝不能把已经落定的终态掀翻 —— 那会让任务卡在
|
||||
* 「实际已终、状态却没落」的中间态,治理台也看不到。
|
||||
*
|
||||
* 也不能把失败记进任务记忆:此刻记忆**已封卷**,追加会被只追加不改的守卫拒掉
|
||||
* (那正是它该做的)。蒸馏的成败属于进化管线的账,不属于这一单的过程留痕。
|
||||
*/
|
||||
}
|
||||
|
||||
return updated;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user