MVP底座④:统一报价 1:1 流程包单实例 —— 四域物化,种子整体切换
14 语义节点/16 边:A5 是内置审批点(驳回走 reject 声明 reopen A4), G2 的两条互斥分支显式汇合于 G2J —— 隐式合并过不了闭合校验(实测)。 顺手修 graph-to-pkg:卡点带 to 时还原成语义边,否则回边画成卡点一存盘就没了。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 画布图 → 流程包:**合并**,不是重建。
|
||||
*
|
||||
* 这里守两条容易丢东西的路:
|
||||
* ① 画布装不下的声明(边映射、驳回走向、负责人策略)保存后必须还在;
|
||||
* ② 回边以**卡点**呈现(MVP 的画法)—— 存盘要还原成语义边,不能连边一起丢。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { processPackageSchema } from '@contracts';
|
||||
import type { ProcessPackage } from '@contracts';
|
||||
import type { FlowGraph } from '../flow-layout.ts';
|
||||
import { mergeGraphIntoPackage } from '../graph-to-pkg.ts';
|
||||
|
||||
const basePkg: ProcessPackage = processPackageSchema.parse({
|
||||
id: 'p',
|
||||
name: '包',
|
||||
version: 1,
|
||||
state: 'draft',
|
||||
callInterface: { input: { type: 'object' }, output: { type: 'object' } },
|
||||
nodes: [
|
||||
{ id: 'S', kind: 'start', label: '起' },
|
||||
{
|
||||
id: 'A',
|
||||
kind: 'activity',
|
||||
label: '甲',
|
||||
activityId: 'act-a',
|
||||
owner: { position: 'BD', strategy: 'rotation' },
|
||||
failure: { kind: 'terminate' },
|
||||
},
|
||||
{ id: 'G', kind: 'gateway', label: '岔', gateway: 'exclusive', mode: 'fork' },
|
||||
{ id: 'E', kind: 'end', label: '终' },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e-s-a', from: 'S', to: 'A', mapping: [{ target: 'x', source: { from: 'initiation', path: 'x' } }] },
|
||||
{ id: 'e-a-g', from: 'A', to: 'G', mapping: [] },
|
||||
{ id: 'e-g-e', from: 'G', to: 'E', condition: 'ok = true', mapping: [] },
|
||||
{ id: 'e-g-a', from: 'G', to: 'A', condition: 'ok = false', mapping: [], reopen: true },
|
||||
],
|
||||
variables: [],
|
||||
});
|
||||
|
||||
/** 画布的样子:回边不画长线,画一枚卡点 */
|
||||
const graph: FlowGraph = {
|
||||
manual: true,
|
||||
rows: [],
|
||||
anchors: [],
|
||||
nodes: [
|
||||
{ id: 'S', kind: 'start', label: '起' },
|
||||
{ id: 'A', kind: 'activity', label: '甲', owner: 'BD' },
|
||||
{ id: 'G', kind: 'gateway', label: '岔', mode: 'x' },
|
||||
{ id: 'E', kind: 'end', label: '终' },
|
||||
{ id: 'c1', kind: 'callout', label: '返回调整', to: 'A' },
|
||||
],
|
||||
edges: [
|
||||
{ from: 'S', to: 'A' },
|
||||
{ from: 'A', to: 'G' },
|
||||
{ from: 'G', to: 'E', label: '通过', tone: 'ok' },
|
||||
{ from: 'G', to: 'c1', label: '有异议', tone: 'bad' },
|
||||
],
|
||||
};
|
||||
|
||||
describe('mergeGraphIntoPackage', () => {
|
||||
it('卡点还原成语义边 —— 回边不因「画成卡点」而消失', () => {
|
||||
const merged = mergeGraphIntoPackage(basePkg, graph);
|
||||
const back = merged.edges.find((e) => e.from === 'G' && e.to === 'A');
|
||||
expect(back).toBeDefined();
|
||||
// 旧边的 id 与 reopen 标记跟着保留:合并按 from→to 认亲
|
||||
expect(back?.id).toBe('e-g-a');
|
||||
expect(back?.reopen).toBe(true);
|
||||
expect(back?.condition).toBe('ok = false');
|
||||
});
|
||||
|
||||
it('卡点本身不入语义节点,但整张图存进 diagram', () => {
|
||||
const merged = mergeGraphIntoPackage(basePkg, graph);
|
||||
expect(merged.nodes.map((n) => n.id)).toEqual(['S', 'A', 'G', 'E']);
|
||||
const diagram = merged.diagram as { nodes: { id: string }[] };
|
||||
expect(diagram.nodes.map((n) => n.id)).toContain('c1');
|
||||
});
|
||||
|
||||
it('画布装不下的声明原样保留:边映射与负责人策略', () => {
|
||||
const merged = mergeGraphIntoPackage(basePkg, graph);
|
||||
expect(merged.edges.find((e) => e.id === 'e-s-a')?.mapping).toHaveLength(1);
|
||||
const a = merged.nodes.find((n) => n.id === 'A');
|
||||
expect(a?.kind === 'activity' && a.owner.strategy).toBe('rotation');
|
||||
});
|
||||
});
|
||||
@@ -109,7 +109,23 @@ export function mergeGraphIntoPackage(
|
||||
});
|
||||
|
||||
const semanticIds = new Set(semantic.map((n) => n.id));
|
||||
const edges: Edge[] = graph.edges
|
||||
|
||||
/*
|
||||
* 卡点是**回跳的呈现**:图上不画长线,画一枚「返回调整→A4」的小牌子。
|
||||
* 所以「X → 卡点(to: Y)」就是语义边 X→Y —— 落包时把它还原回去,
|
||||
* 否则一存盘回边就没了(卡点不入语义,这条边两端有一端不在 semanticIds 里,会被滤掉)。
|
||||
*/
|
||||
const calloutTarget = new Map(
|
||||
graph.nodes
|
||||
.filter((n) => n.kind === 'callout' && n.to !== undefined && semanticIds.has(n.to))
|
||||
.map((n) => [n.id, n.to!]),
|
||||
);
|
||||
const resolved = graph.edges.map((e) => {
|
||||
const via = calloutTarget.get(e.to);
|
||||
return via === undefined ? e : { ...e, to: via };
|
||||
});
|
||||
|
||||
const edges: Edge[] = resolved
|
||||
.filter((e) => semanticIds.has(e.from) && semanticIds.has(e.to))
|
||||
.map((e) => {
|
||||
const prior = priorEdge.get(`${e.from}→${e.to}`);
|
||||
|
||||
Reference in New Issue
Block a user