审批点后面补上网关 G3,并让引擎**真的走它** —— 顺带给去向旗找到落脚处

两件事,都是「图上画着、实际走不到」这一类。

## ① 审批点后面没有网关(用户提出)

A5 只有一条出边 A5→A6,驳回走向藏在 `reject` 声明里,图上只是一枚牌子。
BPMN 的常规是审批的两个去向都由网关决定 —— 补 G3「审批结果」,
`verdict = "pass"` → A6,`verdict = "reject"` → A4(回边)。

补的时候撞上真问题:`onApprovalVerdict` 只在 pass 时走 `advanceFrom`,
而且 payload 里**不含 verdict** —— 于是 G3 没有判据可读。异或分叉的每条出边都得带条件、
没有无条件兜底边,判据一缺就全不成立,流会无声停在 G3。

能让闸过的最省事办法是在 `e-a5-g3` 上喂一枚常量 `verdict: "pass"`。**没那么做** ——
那样 G3 的条件恒真、驳回边永不可达,网关就是个摆设,又是一处「画着两个去向、
其中一个结构上走不到」的谎。

改的是引擎:裁决进载荷,通过与驳回**都先看图**,只有没画驳回边的包才退回 `reject` 声明
(它 schema 必填,且是通用机制)。`advanceFrom` 因此回报「有没有真的推出去」,
并加 `quiet` 参数 —— 「先试边、不成再退回声明」这一步试空是正常分支,不是死路,
不该报 route-dead-end 假警。

`reject` 保留:schema 必填、CONTRACT.md 的不变量、且审批人界面拿它渲染人读文案。
它与 `e-g3-a4` 必须字字对齐,加了测试守。

## ② 几何重排:两带换行,不回形

原坐标逐点抄自 MVP 的 SVG,是回形走线(row 0 左→右、row 1 右→左、row 2 折回最左)——
这正是上一轮流程链读成 A1→A7→A2 的根源。重排为两带:
受理·选型·寻源(y 60..100)→ 换行车道 y=190 → 定价·审批·议价·成交(y 310..350)→
未成交闭环(y 410..450)。每带内 x 随推进次序单调递增。

实测渲染(画布 639×325):带 0 的 A1 57→G1 159→A2 223→G2 325→A3 389→G2J 491,
带 1 的 A4 38→A5 153→G3 242→A6 293→G4 395,**零框重叠**。
两枚回 A4 的卡点同一条车道、同朝向(向上=回退)。

## ③ 顺带:两枚去向旗终于有地方落

上一轮我在债务簿写「A1 过不了 G1 是诚实状态、不是缺陷」——**判断错了**。
出口的标量字段只从落地事实取,而 `needAlternative` / `needSourcing`
在任何台账对象上都没有列,活动实现了也一样取不到。

新增 `project.intake_verdict`「受理结论」**一单一行**(主键只有询价单号)。
为什么不挂在按料号分行的「统一受理记录」上:`flat` 是把这一单所有落地行 reduce 展开,
六个料号就有六行同名列、最后合并的那行赢 —— 而归并规则是「任一条命中 C 即需选型」,
last-write-wins 会静默给出错的去向。测试正反两面都守:结论表主键只能是 rfq_ref,
且**其它对象上不许出现去向旗**。

A2 也要回写这一行,所以 `rfqRef` 进了 G1→A2 的边映射,并**正式声明进 A2 的入参**——
不声明也能偷渡(闭合校验遇到未声明的目标直接跳过),但那是默许不是正当。
A2 的 contract.version 因此升 0.2.0,包版升 0.5.0。

顺手删了一个陈旧草稿 v2:它是我早前测编辑器留下的旧图(14 节点、无 G3),
而种子刚把 v1 用新图重发布 —— 草稿内容比已发布版更旧,谁发布它 G3 就没了。
删前确认零工作流引用。系统没有「丢弃草稿」的入口,这条记进债务簿。

typecheck ✓ lint ✓ depcruise 374 模块 ✓ api:check ✓ 495 测试 ✓
发布闸实测:15 节点 / 18 边,边映射闭合校验通过。

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 12:24:40 +08:00
co-authored by Claude Opus 5
parent 9802550ae8
commit 78622b8799
6 changed files with 373 additions and 99 deletions
+20 -4
View File
@@ -112,10 +112,11 @@ const identity = (activityId: string, name: string, paradigm: 'flow' | 'loop') =
* `exhausted` 终态、结构上永远走不完(实测)。授权面申报值同源。
* 0.4.0:flow 骨架末步追加**收口验收**—— 没有它活动永远不 release,
* 六步答完就卡在「等人却没东西给人答」(实测)。
* 0.5.0:A1/A2 声明「受理结论」对象(两枚去向旗有处落地);A2 入参加 rfqRef。
* 契约的入参出口一直没动,故 `contract.version` 始终 0.1.0。
* 在途任务按启动时版本跑完,不受影响。
*/
packageVersion: '0.4.0',
packageVersion: '0.5.0',
category: '统一报价',
paradigm,
sdkVersion: '0.1.0-m5',
@@ -222,6 +223,8 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
'project.rfq_file',
'project.intake_record',
'project.pn_class',
// 受理结论:两枚去向旗落在这里(一单一行,G1/G2 读同一个值)
'project.intake_verdict',
'project.alt_request',
'project.inquiry_request',
'intel.history_price',
@@ -250,17 +253,24 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
identity: identity('alt-selection', '替代选型', 'loop'),
contract: {
activityId: 'alt-selection',
version: '0.1.0',
// 0.2.0:入参加 rfqRef —— 办结时要回写「受理结论」那一行,主键就是它
version: '0.2.0',
input: {
data: {
type: 'object',
required: ['material'],
required: ['material', 'rfqRef'],
properties: {
material: { type: 'string' },
chip_type: { type: 'string' },
spec_params: { type: 'string' },
scenario: { type: 'string' },
needSourcing: { type: 'boolean' },
/*
* 询价单号。声明它是为了**不偷渡**:边上映射一个契约没声明的字段,
* 闭合校验的类型检查会因 `targetType === undefined` 直接跳过 —— 不报错,
* 但那是默许不是正当,而且下一个读代码的人无从知道 A2 还收这个字段。
*/
rfqRef: { type: 'string' },
},
},
messageAllowed: true,
@@ -288,7 +298,13 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
listeners: [],
harness,
tools: [{ name: 'crm.history-price-read' }],
ledger: declarationsFor(['intel.chip_spec', 'intel.public_select', 'project.alt_benchmark']),
// 受理结论同在声明里:A2 要把 needSourcing 回写**同一行**,去向只有一个出处
ledger: declarationsFor([
'intel.chip_spec',
'intel.public_select',
'project.alt_benchmark',
'project.intake_verdict',
]),
},
assets: {
businessModel: loopSkeleton(