判据 ⑨ 解锁:选择组 → 出口 → 网关判据,三处断点接通

A6 放行选了 lost,却始终走不到 A7。三处各断一截:

① 装配输出口只看**必交付口**。A6 根本没有必交付口(它的输出是选择组
   won/revise/lost),于是得到一张空表,下游连判据都读不到。改为必交付口与
   选择组的口一并装配。
② `onTaskTerminal` 只取**第一口**的载荷做路由作用域;没有必交付口时就什么都拿不到。
   改为各口载荷合并成一份。
③ 网关拿到的是**边映射装配后**的载荷,而 A6→G4 这条边没有映射,装配出来是空的 ——
   判据依据在这一步整个丢掉,异或网关一条分支都选不中,流悄无声息地停住。
   改为:网关按**到达的东西**路由(上游载荷原样穿过);活动与审批点仍只收装配后的载荷,
   它们有契约,不该被上游多余字段污染。

另立一条约定并写进注释:**选择组发射的那一口就是这次运行的 outcome**
(「恰发一口,随放行一并确认」),下游异或网关的判据读的就是它。

现在:A6 放行选 lost → G4 按 `outcome = "lost"` 判定 → **A7 情报归档起到冯兵工位**。

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

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:48:04 +08:00
co-authored by Claude Opus 5
parent 691c61b6b8
commit 1577927214
6 changed files with 91 additions and 9 deletions
+31 -6
View File
@@ -348,13 +348,27 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
* 输出口声明的属性名与台账列名可能不同形(驼峰 / 下划线),两种写法都认。
*/
function outputPortsOf(
manifest: { contract: { output: { mandatory: ReadonlyArray<{ name: string; payload: unknown }> } } },
manifest: {
contract: {
output: {
mandatory: ReadonlyArray<{ name: string; payload: unknown }>;
selective?: ReadonlyArray<{ group: string; ports: ReadonlyArray<{ name: string; payload: unknown }> }>;
};
};
},
landed: LandedFacts,
): Record<string, Record<string, unknown>> {
const snake = (name: string): string => name.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
const ports: Record<string, Record<string, unknown>> = {};
for (const port of manifest.contract.output.mandatory) {
// 必交付口与选择组的口都要装配:活动可以只有选择组(如 A6 的成交/有异议/拒绝),
// 只看必交付口会得到一张空表,下游连分支判据都读不到。
const declaredPorts = [
...manifest.contract.output.mandatory,
...(manifest.contract.output.selective ?? []).flatMap((g) => g.ports),
];
for (const port of declaredPorts) {
const payload = port.payload as { properties?: Record<string, { type?: string }> };
const declared = Object.entries(payload.properties ?? {});
const picked: Record<string, unknown> = {};
@@ -442,10 +456,21 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
ledgerRowsMerged: outcome.ledgerRowsMerged,
// 输出口载荷 = 已落地的事实:铺平供标量取值,逐行供数组取值
facts: {
flat: outcome.merged.reduce<Record<string, unknown>>(
(acc, row) => ({ ...acc, ...row.key, ...row.values }),
{},
),
flat: {
...outcome.merged.reduce<Record<string, unknown>>(
(acc, row) => ({ ...acc, ...row.key, ...row.values }),
{},
),
/*
* **选择组发射的那一口就是这次运行的 outcome**。
* 「恰发一口,随放行一并确认」——那一口本身就是这一单的分支结论,
* 下游异或网关的判据读的就是它(如 G4:成交 / 有异议 / 拒绝)。
* 只有选择组才产生 outcome;必交付口是「都要交」,不构成选择。
*/
...(Object.values(input.selectedPorts).length > 0
? { outcome: Object.values(input.selectedPorts)[0] }
: {}),
},
rows: outcome.merged.map((r) => ({ key: r.key, values: r.values })),
},
};
@@ -177,8 +177,16 @@ export function createProcessService(deps: ProcessServiceDeps): ProcessService {
}
const assembled = assemble(edge, { ...scope, ports: { ...scope.ports, [fromNodeId]: payload } }, deps.sfeel.expression);
/*
* 网关不是责任单元,**按到达的东西路由**:它没有入参契约要满足,
* 上游的输出口载荷要原样穿过去,判据才读得到(如 G4 读 outcome)。
* 若只给边映射装配出来的那份,无映射的边就把判据依据整个丢掉,
* 异或网关会一条分支都选不中,整条流悄无声息地停住。
* 活动与审批点仍只收**装配后的**载荷 —— 它们有契约,不该被上游多余字段污染。
*/
const arriving = target.kind === 'gateway' ? { ...payload, ...assembled } : assembled;
try {
await enter(record, pkg, target, assembled);
await enter(record, pkg, target, arriving);
} catch (error) {
/*
* 一条分支起不来,不该把整条终态上报连坐掀翻 —— 那会让**上游任务的终态也报不上去**,
@@ -297,8 +305,9 @@ export function createProcessService(deps: ProcessServiceDeps): ProcessService {
const merged = await deps.store.updateWorkflow(record.id, { ports: { ...record.ports, ...ports } });
if (terminal === 'released') {
// 落地收敛后才发射输出口 —— 下游只见落地完成的任务
const payload = Object.values(ports)[0] ?? {};
// 落地收敛后才发射输出口 —— 下游只见落地完成的任务。
// 各口载荷合并成一份路由作用域:只取第一口,活动没有必交付口时就什么都拿不到。
const payload: Record<string, unknown> = Object.assign({}, ...Object.values(ports));
await advanceFrom(merged, pkg, located.nodeId, payload);
} else if (node?.kind === 'activity') {
// 非完成终态触发**声明的失败走向**;流程层不自动重试