三面拆分⑮:对齐 Mastra 数据流 —— 边只表达顺序,数据按输入/输出字段名流动
用户定的机制对齐:Mastra 的边不传数据,所有约束力靠输入/输出字段(schema 咬合)完成。
照此把「边映射」整个退役:
· 契约:edge 去 mapping,审批 reject 去 mapping —— 边只剩 顺序 + 条件(读上游
输出字段)+ 标签/回边。fieldMapping 只留给触发器的起单装配(系统边界)。
存量包无需迁移:zod 解析时把旧 jsonb 里的 mapping 静默剥掉,图照常有效。
· 运行时:assemble(边装配)→ flatScope(铺平作用域)——
起单入参垫底 ← 沿途各输出口字段依次盖上 ← 刚发射的载荷(Mastra 语义:
下一步的 input 首先是上一步的 output)。下游按自己的契约取字段;
「__」前缀是运行时自留地,不入业务作用域。审批驳回重开、失败改道同一条路。
· 闭合校验:边未映射/必填未映射/容忍依赖三条映射类检查退役
(宽进之下必填有没有着落是运行时补全单的事,不拦激活);
激活闸只留一条硬伤 —— **同名不同型**:名字即管道,名同型异必坏,
容忍依赖改按名判。改名映射不存在:名不对改契约,不是加映射。
· 种子:A2 的入参 material 更名 pn(它就是 PN;上游 A1 发的名就得是它收的名),
契约 0.3.0、槽位 0.8.0;流程种子边全部去映射。
· 前端:边属性卡砍掉「映射 · 载荷装配」整节,只剩 E号/方向/条件/标签;
节点属性卡新增「数据接口 · 按字段名流动」——每个输入字段标出运行时从哪来
(起单入参 / 上游 X / 补全单),输出口列出发什么名;沿边回溯推导,设计期看得见。
画布保存与依赖文件导出同步去映射。
E2E 顺带把宽进接进真流:快速归档流的 A3 要 outcome 而流里没人产 ——
测试如实走补全路(等人 → 答卡 outcome=won → 开跑 → completed)。
实测走查:A2 节点卡上 pn←上游A1、rfqRef←起单入参、scenario←补全单,推导全对。
五闸绿;契约快照随本提交入库。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/** 边映射闭合校验、负责人四策略、工作流状态聚合 —— domain 测试零 mock(OS 03 / 3.2) */
|
||||
/** 闭合校验(名流动版)、负责人四策略、工作流状态聚合 —— domain 测试零 mock(OS 03 / 3.2) */
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { ProcessPackage } from '@contracts';
|
||||
import { checkClosure } from '../internal/domain/closure.ts';
|
||||
@@ -39,17 +39,9 @@ const pkg = (over: Partial<ProcessPackage> = {}): ProcessPackage =>
|
||||
{ id: 'end' as never, kind: 'end', label: '终点' },
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'e1',
|
||||
from: 'start' as never,
|
||||
to: 'A4' as never,
|
||||
mapping: [
|
||||
{ target: 'pn', source: { from: 'initiation', path: 'pn' } },
|
||||
{ target: 'basePrice', source: { from: 'initiation', path: 'basePrice' } },
|
||||
],
|
||||
reopen: false,
|
||||
},
|
||||
{ id: 'e2', from: 'A4' as never, to: 'end' as never, mapping: [], reopen: false },
|
||||
// 边只表达顺序与条件(2026-07-30 对齐 Mastra)—— 数据按字段名流动,不带映射
|
||||
{ id: 'e1', from: 'start' as never, to: 'A4' as never, reopen: false },
|
||||
{ id: 'e2', from: 'A4' as never, to: 'end' as never, reopen: false },
|
||||
],
|
||||
variables: [],
|
||||
...over,
|
||||
@@ -58,40 +50,32 @@ const pkg = (over: Partial<ProcessPackage> = {}): ProcessPackage =>
|
||||
const contracts = new Map([['a4', face()]]);
|
||||
|
||||
describe('checkClosure —— 不闭合不得激活', () => {
|
||||
it('必填全映射、岗位存在即可激活', () => {
|
||||
it('结构闭合、活动在库即可激活 —— 必填有没有着落是运行时补全单的事,不拦激活', () => {
|
||||
const report = checkClosure({ pkg: pkg(), contracts });
|
||||
expect(report.violations).toEqual([]);
|
||||
expect(report.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('边未完成映射即拦', () => {
|
||||
const broken = pkg({
|
||||
it('同名字段类型相抵即拦 —— 名字即管道,名同型异必坏', () => {
|
||||
// 上游 done 口发 quote: number …下游若声明 quote: string 就是硬伤
|
||||
const clash = new Map([
|
||||
['a4', face({ inputTypes: { pn: 'string', quote: 'string' }, requiredInputs: ['pn'] })],
|
||||
]);
|
||||
const chain = pkg({
|
||||
nodes: [
|
||||
{ id: 'start' as never, kind: 'start', label: '起点' },
|
||||
{ id: 'A3' as never, kind: 'activity', label: '上游', activityId: 'a4' as never, failure: { kind: 'terminate' } },
|
||||
{ id: 'A4' as never, kind: 'activity', label: '下游', activityId: 'a4' as never, failure: { kind: 'terminate' } },
|
||||
{ id: 'end' as never, kind: 'end', label: '终点' },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e1', from: 'start' as never, to: 'A4' as never, mapping: [], reopen: false },
|
||||
{ id: 'e2', from: 'A4' as never, to: 'end' as never, mapping: [], reopen: false },
|
||||
{ id: 'e0', from: 'start' as never, to: 'A3' as never, reopen: false },
|
||||
{ id: 'e1', from: 'A3' as never, to: 'A4' as never, reopen: false },
|
||||
{ id: 'e2', from: 'A4' as never, to: 'end' as never, reopen: false },
|
||||
],
|
||||
});
|
||||
const report = checkClosure({ pkg: broken, contracts });
|
||||
expect(report.closed).toBe(false);
|
||||
expect(report.violations.map((v) => v.kind)).toContain('edge-unmapped');
|
||||
expect(report.violations.map((v) => v.kind)).toContain('required-unmapped');
|
||||
});
|
||||
|
||||
it('必填未齐即拦', () => {
|
||||
const partial = pkg({
|
||||
edges: [
|
||||
{
|
||||
id: 'e1',
|
||||
from: 'start' as never,
|
||||
to: 'A4' as never,
|
||||
mapping: [{ target: 'pn', source: { from: 'initiation', path: 'pn' } }],
|
||||
reopen: false,
|
||||
},
|
||||
{ id: 'e2', from: 'A4' as never, to: 'end' as never, mapping: [], reopen: false },
|
||||
],
|
||||
});
|
||||
const violations = checkClosure({ pkg: partial, contracts }).violations;
|
||||
expect(violations.filter((v) => v.kind === 'required-unmapped').map((v) => v.field)).toEqual(['basePrice']);
|
||||
const violations = checkClosure({ pkg: chain, contracts: clash }).violations;
|
||||
expect(violations.filter((v) => v.kind === 'type-conflict').map((v) => v.field)).toEqual(['quote']);
|
||||
});
|
||||
|
||||
it('引用未安装的活动即拦', () => {
|
||||
@@ -124,7 +108,7 @@ describe('checkClosure —— 不闭合不得激活', () => {
|
||||
edges: [
|
||||
...pkg().edges,
|
||||
// A4 已有一条无条件出边 A4→end;再加一条无条件出边即违规
|
||||
{ id: 'e9', from: 'A4' as never, to: 'end2' as never, mapping: [], reopen: false },
|
||||
{ id: 'e9', from: 'A4' as never, to: 'end2' as never, reopen: false },
|
||||
],
|
||||
});
|
||||
const violations = checkClosure({ pkg: twoDefaults, contracts }).violations;
|
||||
@@ -161,18 +145,14 @@ describe('checkClosure —— 不闭合不得激活', () => {
|
||||
{ id: 'end' as never, kind: 'end', label: '终点' },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e0', from: 'start' as never, to: 'A3' as never, mapping: [{ target: 'pn', source: { from: 'initiation', path: 'pn' } }, { target: 'basePrice', source: { from: 'initiation', path: 'b' } }], reopen: false },
|
||||
{ id: 'e0', from: 'start' as never, to: 'A3' as never, reopen: false },
|
||||
{
|
||||
id: 'e1',
|
||||
from: 'A3' as never,
|
||||
to: 'A4' as never,
|
||||
mapping: [
|
||||
{ target: 'pn', source: { from: 'output-port', port: 'done', path: 'pn' } },
|
||||
{ target: 'basePrice', source: { from: 'output-port', port: 'done', path: 'quote' } },
|
||||
],
|
||||
reopen: false,
|
||||
},
|
||||
{ id: 'e2', from: 'A4' as never, to: 'end' as never, mapping: [], reopen: false },
|
||||
{ id: 'e2', from: 'A4' as never, to: 'end' as never, reopen: false },
|
||||
],
|
||||
});
|
||||
const violations = checkClosure({ pkg: tolerant, contracts }).violations;
|
||||
|
||||
Reference in New Issue
Block a user