forked from ChaiHao/BPOS-MVP
M5/M6 修复串:判据 ③ 全链路跑通 —— 六处「声明与引擎对不上」
判据 ③ 现在从入站到 released 一路走通:步序推进 → LLM 步(密钥是占位符,
按声明降级为人兜底)→ 台账草稿层 → D1 条件跳步 → S4 工具 → S5 DMN 三分类
(LM358DR → Type A · 直接快答)→ 验收放行 → 草稿合并正式版 2 行 → 输出口
intake 发射 → 终态 released。
沿路修掉的六处,每一处都是「声明是这么写的,引擎不这么读」:
1. 人兜底卡只问一个笼统「结果」框 → 改问**这一步声明的出参字段**。
出参契约属于步、不属于执行者:同一步谁做完,落进作用域的形状都得一样。
2. 人事件的载荷是回传协议壳 `{mode,workOrderId,values}`,
引擎把整个壳当出参写 → 取 `values`。
3. 引擎作用域是 `{'域.对象': 行数组}`,而所有活动的步声明写的都是裸字段名 →
任务视图三层铺开:起单入参在底、各对象最新行的字段铺顶层、地址式整表保留。
4. 起单入参从不落库、也进不了作用域 → task 存 `input_data`,断点续跑后仍在。
5. 步出参无法声明**写哪一行** → 出参加 `key`(主键列 → 作用域路径);
六个活动包的每处出参补上键。台账几乎都有主键,不声明键就只能写无主键行。
6. `tick` 不处理 `finished`:谱走完仍按 running 一拍拍推,把预算烧到 exhausted,
把「没人裁」误报成「跑爆了」→ 放行通过则落地收敛后 released;未放行则挂起等人。
`flow.advance` 据末步验收裁决报 released —— **完成只出自放行通过**。
另两处装配缺口:
· `release.land()` 实现完整却无人调用 —— 放行后从不落地。经 LandingGateway 接上。
· `process.onTaskTerminal()` 实现完整却无人调用 —— 任务终态报不到流程层,
流就停在那个节点上,既不流转也不走失败走向。task 在 L6 不得反向依赖 L7,
这一跳由 app 缝合(延后绑定,同 container 里 TriggerMatcher 的手法)。
还有一处版本化缺陷:资产播种恒用 v1,`shouldSeedAsset` 一比就跳过 ——
只有**第一个**包版本的资产此生有效,之后装什么都跑旧的。改为播下一版;
进化管线放行出的更高版仍不被盖过;同版重复安装不再播种。
gates: tsc / eslint / depcruise(317 模块)/ api-report / 393 测试 全绿
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -86,6 +86,20 @@ const sfeel = {
|
||||
},
|
||||
};
|
||||
|
||||
/** 延后绑定的流程层引用 —— 只用到「任务终态上报」这一件 */
|
||||
function createDeferredProcess(): {
|
||||
onTaskTerminal: ProcessService['onTaskTerminal'];
|
||||
bind(real: ProcessService): void;
|
||||
} {
|
||||
let real: ProcessService | undefined;
|
||||
return {
|
||||
onTaskTerminal: async (taskId, terminal, ports) => real?.onTaskTerminal(taskId, terminal, ports),
|
||||
bind(next) {
|
||||
real = next;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||
const { db, clock, ids } = deps;
|
||||
|
||||
@@ -101,10 +115,22 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||
|
||||
/* ── 任务数据:引擎读**任务视图** = 正式版 + 本任务草稿层 ───────── */
|
||||
const taskData = {
|
||||
/**
|
||||
* 任务视图 = 正式版 + 本任务草稿层(AM 4.2),再叠上起单入参。
|
||||
*
|
||||
* 作用域的**形状要对得上声明的写法**:步声明里写的是裸字段名
|
||||
* (`input: { pn: 'pn' }`),不是 `域.对象.行号.字段`。所以这里三层铺开:
|
||||
* ① 起单入参在底 —— 契约的 data 是这单的出发事实;
|
||||
* ② 各对象**最新一行**的字段名铺到顶层 —— 过程工作区的数据形态即草稿层;
|
||||
* ③ `域.对象` 仍保留整表,要整表的照样取得到。
|
||||
* 顶层同名以「后写的赢」为准:草稿层是这一单最新的过程事实,盖过起单入参。
|
||||
*/
|
||||
async view(taskId: TaskId) {
|
||||
// 引擎对任务视图求值,不依赖事件(AM 4.2)
|
||||
const task = await taskStore.get(taskId);
|
||||
if (task === undefined) throw new Error(`任务不存在:${taskId}`);
|
||||
const objects = await deps.ledger.listObjects();
|
||||
const scope: Record<string, unknown> = {};
|
||||
const scope: Record<string, unknown> = { ...task.data };
|
||||
|
||||
for (const address of objects) {
|
||||
const page = await deps.ledger.taskView(taskId, {
|
||||
address,
|
||||
@@ -114,6 +140,12 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||
offset: 0,
|
||||
});
|
||||
scope[`${address.domain}.${address.object}`] = page.rows;
|
||||
const latest = page.rows[page.rows.length - 1];
|
||||
if (latest === undefined) continue;
|
||||
for (const [field, value] of Object.entries(latest)) {
|
||||
// 空值不盖掉已有事实:没写进去的字段不该把起单入参抹成 undefined
|
||||
if (value !== null && value !== undefined) scope[field] = value;
|
||||
}
|
||||
}
|
||||
return scope;
|
||||
},
|
||||
@@ -295,6 +327,29 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||
clock,
|
||||
});
|
||||
|
||||
/**
|
||||
* process 晚于 task 构造 —— 延后绑定(与 container 里 `TriggerMatcher` 同一手法)。
|
||||
* 未绑定时静默不报:装配尚未完成的那一瞬没有任务会终态。
|
||||
*/
|
||||
const processRef = createDeferredProcess();
|
||||
|
||||
/** 任务的输出口载荷:活动声明的必交付输出口 → 该任务视图上的字段 */
|
||||
async function outputPortsOf(taskId: TaskId): Promise<Record<string, Record<string, unknown>>> {
|
||||
const task = await taskStore.get(taskId);
|
||||
if (task === undefined) return {};
|
||||
const manifest = await deps.registry.manifestOf(task.activityId);
|
||||
const scope = await taskData.view(taskId);
|
||||
const ports: Record<string, Record<string, unknown>> = {};
|
||||
for (const port of manifest.contract.output.mandatory) {
|
||||
const payload = port.payload as { properties?: Record<string, unknown> };
|
||||
const fields = Object.keys(payload.properties ?? {});
|
||||
ports[port.name] = Object.fromEntries(
|
||||
fields.filter((f) => scope[f] !== undefined).map((f) => [f, scope[f]]),
|
||||
);
|
||||
}
|
||||
return ports;
|
||||
}
|
||||
|
||||
/* ── task(L6):范式在此汇流,对上层不可区分 ──────────────────── */
|
||||
const task = createTaskService({
|
||||
store: taskStore,
|
||||
@@ -330,7 +385,12 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||
load: (taskId, model) => flow.load(taskId, model as never).then(() => undefined),
|
||||
advance: async (taskId) => {
|
||||
const outcome = await flow.advance(taskId);
|
||||
return { status: outcome.status, ...(outcome.blockedReason === undefined ? {} : { reason: outcome.blockedReason }) };
|
||||
return {
|
||||
status: outcome.status,
|
||||
...(outcome.blockedReason === undefined ? {} : { reason: outcome.blockedReason }),
|
||||
// 谱走完时把放行裁决带上来 —— 完成只出自放行通过
|
||||
...(outcome.released === undefined ? {} : { released: outcome.released }),
|
||||
};
|
||||
},
|
||||
onEvent: async (taskId, event) => {
|
||||
const outcome = await flow.resume(taskId, {
|
||||
@@ -342,6 +402,22 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||
},
|
||||
},
|
||||
},
|
||||
landing: {
|
||||
/** 放行通过即刻落地:草稿合并 + 动作执行(附幂等键)→ 收敛才发射输出口(OS 7.3) */
|
||||
async land(input) {
|
||||
const outcome = await deps.release.land({
|
||||
taskId: input.taskId,
|
||||
ownerId: input.ownerId,
|
||||
humanCallCap: input.humanCallCap,
|
||||
selectedPorts: {},
|
||||
});
|
||||
return {
|
||||
converged: outcome.verdict.kind === 'converged',
|
||||
emittedPorts: outcome.emittedPorts,
|
||||
ledgerRowsMerged: outcome.ledgerRowsMerged,
|
||||
};
|
||||
},
|
||||
},
|
||||
memory: {
|
||||
append: memoryRecorder.append as never,
|
||||
seal: (taskId) => deps.memory.seal(taskId),
|
||||
@@ -375,6 +451,17 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||
occurredAt: clock.now().toISOString(),
|
||||
source: { kind: 'engine', detail: 'task' },
|
||||
});
|
||||
|
||||
/*
|
||||
* 任务终态到达 → 通知流程层(L7)。
|
||||
*
|
||||
* task 在 L6、process 在 L7,task 不得反向依赖 process —— 这一跳由 app 这层缝合。
|
||||
* 「工作流状态是聚合投影」的前提是节点实例的终态**报得上去**:不报,
|
||||
* 流就停在那个节点上,既不流转也不走失败走向。
|
||||
* 用延后绑定是因为 process 在本函数里晚于 task 构造。
|
||||
*/
|
||||
const ports = await outputPortsOf(taskId);
|
||||
await processRef.onTaskTerminal(taskId, terminal, ports);
|
||||
},
|
||||
},
|
||||
notifier: {
|
||||
@@ -395,7 +482,7 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||
});
|
||||
|
||||
/* ── process(L7) ─────────────────────────────────────────────── */
|
||||
const process = createProcessService({
|
||||
const process: ProcessService = createProcessService({
|
||||
store: createProcessStore(db),
|
||||
activities: {
|
||||
async faces() {
|
||||
@@ -556,5 +643,8 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||
ids,
|
||||
});
|
||||
|
||||
// 延后绑定落定:此后任务终态才报得到流程层
|
||||
processRef.bind(process);
|
||||
|
||||
return { task, process, trigger };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user