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:
2026-07-28 16:37:59 +08:00
co-authored by Claude Opus 5
parent 894769e2ca
commit 545103af1b
11 changed files with 169 additions and 12 deletions
+20 -6
View File
@@ -261,10 +261,11 @@ const identity = (activityId: string, name: string, paradigm: 'flow' | 'loop') =
* 0.6.0:占位期骨架改**机器步落台账 + 一张验收单**(用户定:只留验收要人签)。
* 六个槽位一律 flow 范式;预算固定 humanCalls: 2。
* 0.6.1:修验收步的出参映射 —— 键是**台账列名**(小写蛇形),不是回传字段名。
* 0.7.0:A6 的三个去向改**选择组**(契约 0.2.0)—— outcome 由放行选口给出,G4 才判得了。
* 契约的入参出口一直没动,故 `contract.version` 始终 0.1.0。
* 在途任务按启动时版本跑完,不受影响。
*/
packageVersion: '0.6.1',
packageVersion: '0.7.0',
category: '统一报价',
paradigm,
sdkVersion: '0.1.0-m5',
@@ -609,7 +610,7 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
identity: identity('quote-negotiation', '报价议价', 'flow'),
contract: {
activityId: 'quote-negotiation',
version: '0.1.0',
version: '0.2.0',
input: {
data: {
type: 'object',
@@ -622,12 +623,10 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
mandatory: [
{
name: 'formal',
description: '正式报价单(已送出)+ 跟进提醒 + 议价结果',
description: '正式报价单(已送出)+ 跟进提醒',
payload: {
type: 'object',
required: ['outcome'],
properties: {
outcome: { type: 'string', enum: ['won', 'revise', 'lost'] },
quote_no: { type: 'string' },
customer_code: { type: 'string' },
rows: { type: 'array' },
@@ -635,7 +634,22 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
},
},
],
selective: [],
/*
* 三个去向是**选择组**,不是必交口里的一个字段:人在放行时恰选一口,
* 那一口就是这次运行的 outcome(引擎按选口注入),G4 判的正是它。
* 从前写成 formal 口的必填 outcome + selective: [] —— 引擎只在选择组
* 恰发一口时才注入 outcome,没有选择组就永远没有 outcome,G4 三条边全不成立。
*/
selective: [
{
group: '议价结果',
ports: [
{ name: 'won', description: '成交 —— 转订单履约', payload: { type: 'object' } },
{ name: 'revise', description: '有异议 —— 退回重新定价', payload: { type: 'object' } },
{ name: 'lost', description: '拒绝或超时 —— 归档闭环', payload: { type: 'object' } },
],
},
],
},
...contractRest,
authorization: authorizationFor(),