Archived
A6 三个去向改选择组,「恰选一口」全链打通 —— 整条流第一次真正跑完
G4 是整条流最后的断点:三个去向写成了必交口里的 outcome 字段 + selective: [], 而引擎只在选择组恰发一口时才注入 outcome —— 结构上永远没有判据。 ## 四层贯通 ① 契约:验收工单加 `portChoice`(组名 + 可选出口)。draft 是 Omit 透传,human-tool 零改动。 ② A6 声明:outcome 移出必交口,三个去向成选择组(won/revise/lost 各一口带人读说明)。 契约 0.2.0、包版 0.7.0。G4 三条边一行没改 —— outcome 由放行选口注入落地事实。 ③ 引擎:验收发卡时从活动契约现读选择组上卡(组是契约的事实,不在步的卡面里声明)。 ④ 前端 AcceptForm:选择组渲染为必选单选,不选「通过」不亮;选中的口随放行回传。 ## 「恰选一口」拦在收单,不拦在落地 —— 拦错位置实测翻过车 第一版把 validateSelection 放进 release.land:回传是**同步收单**(200 = 收到裁决), 落地在推进器里**异步**跑 —— 拦晚了就是「工单已答完、任务卡死、推进器循环报错」(实测)。 改到 checkWorkOrderReply(与「驳回必附意见」同族的收单规矩):工单投影自带 portChoice, 放行回传缺口/口不在组内 → 400 同步拒收,工单还活着,人补选就是。 land 里的 validateSelection 留作兜底 —— 这条从 contracts 导出以来从未被执行的不变量, 现在两个岗位都有了。 ## 实测四连 不选口通过 → 400「放行前先选『议价结果』」,工单未被吃掉 选 won → end_won released,流 completed —— **整条流第一次真正跑完** 选 revise → A4#2 重开新实例(A4#1 留痕不动) 选 lost → A7 情报归档 → end_arch released,流 completed 全程 route-dead-end 事件 0 条 typecheck ✓ lint ✓ depcruise ✓ 498 测试 ✓ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -118,6 +118,23 @@ export function checkWorkOrderReply(order: WorkOrder, reply: WorkOrderReply): Ca
|
||||
if (rejectionNeedsComment(reply)) {
|
||||
out.push({ code: 'comment-required', message: '驳回必附意见(OS 05)' });
|
||||
}
|
||||
/*
|
||||
* **恰选一口,随放行一并确认**(AM 1.2)—— 与「驳回必附意见」同族的收单规矩。
|
||||
* 工单投影自己声明了选择组(portChoice),放行回传就必须带组内恰一口:
|
||||
* 选的这一口是这次运行的分支结论,下游网关判的正是它,不能靠默认值溜过去。
|
||||
* 只在**通过**时强制 —— 驳回是打回重做,没有分支结论可言。
|
||||
* 为什么拦在这里而不是落地(land):回传是同步收单、落地在推进器里异步跑,
|
||||
* 拦晚了就是「工单已答完、任务卡死、推进器循环报错」(实测)。
|
||||
*/
|
||||
const choice = (order as AcceptanceWorkOrder).portChoice;
|
||||
if (choice !== undefined && reply.verdict === 'pass') {
|
||||
const picked = reply.selectedPorts[choice.group];
|
||||
if (picked === undefined) {
|
||||
out.push({ code: 'port-required', message: `放行前先选「${choice.group}」—— 选的这一口就是这单的去向` });
|
||||
} else if (!choice.ports.some((p) => p.name === picked)) {
|
||||
out.push({ code: 'port-invalid', message: `「${picked}」不在「${choice.group}」的可选出口里` });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* → 落地收敛输出口才发射 → 终态方成「放行完成」。
|
||||
*/
|
||||
import type { ActionIntent, TaskId } from '@contracts';
|
||||
import { emissionPortNames } from '@contracts';
|
||||
import { emissionPortNames, validateSelection } from '@contracts';
|
||||
import type {
|
||||
LandInput,
|
||||
LandOutcome,
|
||||
@@ -201,6 +201,26 @@ export function createReleaseService(deps: ReleaseServiceDeps): ReleaseService {
|
||||
{},
|
||||
);
|
||||
const ports = await deps.tasks.outputPortsOf(input.taskId);
|
||||
|
||||
/*
|
||||
* **恰选一口是硬约束**,不是前端的善意:选择组未选(或选了组外的口)就不落地。
|
||||
* 前端已强制必选;这里兜底拦 API 直调 —— `validateSelection` 这条不变量
|
||||
* 从 contracts 导出以来一直没人执行过,现在它有了岗位。
|
||||
*/
|
||||
const selectionErrors = validateSelection(
|
||||
{
|
||||
mandatory: ports.mandatory.map((name) => ({ name, payload: {} })),
|
||||
selective: ports.selective.map((g) => ({
|
||||
group: g.group,
|
||||
ports: g.ports.map((name) => ({ name, payload: {} })),
|
||||
})),
|
||||
},
|
||||
input.selectedPorts,
|
||||
);
|
||||
if (selectionErrors.length > 0) {
|
||||
throw new Error(`放行未确认分支结论:${selectionErrors.join('; ')} —— 验收单上恰选一口再通过`);
|
||||
}
|
||||
|
||||
const emitted = emissionPortNames(
|
||||
{
|
||||
mandatory: ports.mandatory.map((name) => ({ name, payload: {} })),
|
||||
|
||||
Reference in New Issue
Block a user