三面拆分⑮:对齐 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:
2026-07-29 14:25:27 +08:00
co-authored by Claude Opus 5
parent 8d50ceda6a
commit 03eb666ac2
22 changed files with 264 additions and 689 deletions
@@ -25,7 +25,7 @@ import type {
} from '../../port.ts';
import { aggregateWorkflowState, isDigested, nextInstanceNumber, resolveOwner } from '../domain/owner-resolution.ts';
import type { NodeInstance } from '../domain/owner-resolution.ts';
import { assemble, dispatchOf, rejectRouteOf, selectEdges, startNodeOf } from '../domain/routing.ts';
import { dispatchOf, flatScope, rejectRouteOf, selectEdges, startNodeOf } from '../domain/routing.ts';
export type SelfHostedRuntimeDeps = {
readonly store: ProcessStore;
@@ -231,7 +231,8 @@ export function createSelfHostedRuntime(deps: SelfHostedRuntimeDeps): FlowRuntim
* 节点只收**装配后的**载荷:活动与审批点都有契约,不被上游多余字段污染;
* 判据要用的隐性上下文(审批 verdict、放行选口 outcome)由系统注入判据作用域,不走映射。
*/
const arriving = assemble(edge, { ...scope, ports: { ...scope.ports, [fromNodeId]: payload } }, deps.sfeel.expression);
// 数据按字段名流动(对齐 Mastra):边不带映射 —— 下游收铺平作用域,按自己的契约取字段
const arriving = flatScope({ ...scope, ports: { ...scope.ports, [fromNodeId]: payload } }, payload);
try {
await enter(record, pkg, target, arriving);
} catch (error) {
@@ -302,8 +303,8 @@ export function createSelfHostedRuntime(deps: SelfHostedRuntimeDeps): FlowRuntim
const edge = pkg.edges.find((e) => e.id === disposition.edgeId);
const target = edge === undefined ? undefined : pkg.nodes.find((n) => n.id === edge.to);
if (edge !== undefined && target !== undefined) {
// 失败载荷 = 终态、原因码与说明
const payload = assemble(edge, scopeOf(record), deps.sfeel.expression);
// 失败改道:载荷照样是铺平作用域 + 终态标记 —— 接失败的节点也按自己的契约取字段
const payload = flatScope(scopeOf(record));
await enter(record, pkg, target, { ...payload, __terminal: terminal });
}
}
@@ -401,7 +402,7 @@ export function createSelfHostedRuntime(deps: SelfHostedRuntimeDeps): FlowRuntim
*/
const routedByEdge = await advanceFrom(record, pkg, nodeId, carried, true);
if (!routedByEdge) {
const route = rejectRouteOf(node, scopeOf(record), deps.sfeel.expression);
const route = rejectRouteOf(node, scopeOf(record));
if (route.kind === 'terminate') {
await deps.store.updateWorkflow(workflowId, { state: 'terminated', finishedAt: deps.clock.now().toISOString() });
} else {