脚本 e2e 逼出的八条:闸看错了表 · 拒回喂指针 · 零行骗过闸与哨兵 · 早收不该算撞墙

凭证到位后 `w05` 五案例连跑六轮,逼出八条**浏览器走查逮不到**的缺陷 ——
脚本每张卡都先空手交一次、每个岔路口都按序代答,人手点的时候永远不会去试
「空着交上去会怎样」。末轮:跑到的四例全部 released,三例被拒 0
(第五例停在大模型账户余额不足,不是代码问题)。

平台侧(六条):
- L 数据工单的提交闸没牙 —— 卡没绑表就回落到必交付物,而数据工单要人补的是
  `parts.*` 主数据,不在交付物名单里:闸盯着一堆无关的表、一格不缺,空手交卷一路放行。
  契约新增 `ExecutionWorkOrder.gateLedgers`(这张卡该拦哪几张表,发卡的人说了算),
  `gateAddressesOf` 变三级取一:绑的表 > 卡自报 > 交付物。
- M `finish` 撞 doneWhen 为假的拒回只谈「行与列」,而缺的是一次**工具调用**;
  现在摆出判据原文 + 那几个名字此刻的读数,`tool_*_calls` 读到 0 的单独点名(`unmetWhy`)。
- N 人点了决策卡,模型每拍再发一张,连拒五拍熔断。把「他选的是 approve」写进拒回**没用** ——
  下一拍它又从头翻一次会滚走的窗口。改结构:`answeredCardFact` 把
  「这张卡已答过 → 他选的是什么」做成每拍都在的事实行。
- O 模型传 `requirement: null`,工具 `null.trim()` 当场崩(边界上一律经 `text()` 归一)。
- P 拒回里的 `count=(没写过)` 是 S-FEEL 函数名被当成台账名 —— 假线索模型会去追。
- Q/S 零行 = 零缺,闸与哨兵一起放过空手交卷的数据工单(`emptyGate`:卡自报的表一行都没有
  = 没完成);「早收」头一次只落观察、第二次起才算被拒(与「重发已答过的卡」同一把尺)。

活动侧(1.2.7 → 1.2.9):三张执行工单各自声明 `gateLedgers`;说明书③补「发卡前骨架必须先有行」、
⑤补这张卡的前提(一颗 absent 都不剩);哨兵 `parts_profile_rows >= created` 逐颗对账;
S0_bind 写明发录入卡那一拍别抢写需求行。

e2e 脚本加 `JCAI_E2E_BEARER` 直供旁路(服务账号 client_credentials 仍是正路)。
门禁 typecheck / lint / depcruise / test(2091)全绿;api:check 的差异仍只在 org/* 与 api/*
(工作树里先前那批鉴权改动),card.d.ts 是本轮的纯新增字段。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
This commit is contained in:
xcs
2026-08-27 09:31:35 +08:00
co-authored by Claude Opus 5
parent e235207e4a
commit df18882322
16 changed files with 490 additions and 43 deletions
+41 -1
View File
@@ -3,7 +3,9 @@
> 这份文档记的是**每一轮实跑逼出了什么、改了什么、怎么验证的**。
> 活动包:`activities/part-substitution-assessment`;业务出处 `docs/SS02-ACT-WORK-05-html/`。
> 台账字段口径见 `docs/数据域字段说明-W05.md`。
> 全部走查在**浏览器**里做(测试台起单 → 工作台真人点),不是脚本代答。
> 走查分两条线:**浏览器**(测试台起单 → 工作台真人点,六轮)与**脚本 e2e**
> (`node tools/e2e-a2.mjs w05`,五案例代答,可一键复跑)。两条线逮到的缺陷不重合 ——
> 浏览器逮呈现与交互(件、闸、徽标、翻页、画面切换),脚本逮机制(空手交卷、循环、崩、归属)。
## 一、这个活动是什么
@@ -177,6 +179,23 @@ P-11 一条(`LM2596S-3.3 → LM2596S-ADJ | alternate | fae_confirmed`);P-01 回
版本轨迹:1.0.0 → 1.0.1(A)→ 1.1.0/1.1.1(B·C·D)→ 1.2.0(参数子表)→ 1.2.1(F)→ 1.2.2(G)
→ 1.2.3(H)→ 1.2.4(I)→ 1.2.5(J)→ **1.2.6(K)**。包内自测 16 → **61 条**。
## 二·六、脚本 e2e 的六轮(2026-08-27)
浏览器那六轮之后,凭证到位、`w05` 套件跑起来,又逼出六条 —— **全是浏览器逮不到的那一类**
(脚本每张卡都先空手交一次、每个岔路口都按序代答,人手点的时候会先把格子填好再按提交):
| 轮 | 结果 | 这一轮逼出什么 |
|---|---|---|
| 1 | 1 通过 | **L 数据工单的提交闸没牙** |
| 2 | 3 通过 | **M 拒回数不出「缺的是一次工具调用」· N 决策卡拒回只给指针** |
| 3 | 1 通过 | N 的措辞版没用 → **改结构**;**O 工具被 null 打崩** · P `count` 假线索 |
| 4 | 2 通过 | **H 的残留面:零行同时骗过闸与哨兵** |
| 5 | 4 通过 | **created 逐颗对账太松 · S1 卡的前提没写在说明书** |
| 6 | **跑到的 4 例全 released,3 例被拒 0** | 案例 4 停在 `Insufficient Balance`(大模型余额) |
第 6 轮逐条实证:早收容忍让案例 1/2 的最后 1 次被拒消失(轨迹里是**观察**不是拒回);
三候选那例**第一次跑通**(前三轮:超时 / 超时 / 烧穿预算),两颗缺库一起办、三条结论全对。
## 三、走查发现总表
| # | 现象 | 病根 | 治法 | 层 |
@@ -192,6 +211,14 @@ P-11 一条(`LM2596S-3.3 → LM2596S-ADJ | alternate | fae_confirmed`);P-01 回
| I | 原 PN 根本不在库,模型两颗都判「在库」,硬阻塞整条路被跳过 | 存在性核对是模型自报的一句话,基座数不出来;剧本已写到头 | 加 `found_pn_ref`(`refersTo` + `requiredWhen`):判在库就必须填一个基座查得到的料号,查无此值当场拒、填不出则收不了步 | 活动(结构) |
| J | 数据工单派给了在库的那颗,缺的那颗没人建 | 剧本把「建哪几颗」与「要不要问人」写在同一串分岔里,从没写过「found 的不建」 | 拆成两个问题分别作答,并写明后果 | 活动 |
| K | 数据录入卡的「参考信息」栏摆着引擎内部家具(九个计划项全名) | `entryCard.basis` 契约里可选,活动没声明,基座兜底把内部事实摆了上去 | 卡骨架补 `basis` 三行,并写明不给的后果 | 活动 |
| L | **数据工单的提交闸没牙**:件上红带点着「缺品牌名称」,提交钮却是绿的,空手交卷一路放行 | `gateAddressesOf` 只有两级 —— 没绑表就回落到**必交付物**,而数据工单要人补的是 `parts.*` 主数据,不在交付物名单里:闸盯着一堆与这张卡无关的表,一格不缺 | 契约新增 `ExecutionWorkOrder.gateLedgers`(**这张卡该拦哪几张表,发卡的人说了算**),闸改三级取一:绑的表 > 卡自报 > 交付物 | **平台**(结构) |
| M | `finish` 撞 doneWhen 为假时,拒回只谈「行与列」,而缺的是一次**工具调用** | 拒回从不摆判据本身,更不摆读数 | 拒回摆出**判据原文 + 那几个名字此刻的读数**;`tool_*_calls` 读到 0 的单独点名 | **平台** |
| N | 人点了决策卡,模型每拍再发一张,连拒五拍熔断 | 剧本让模型「去轨迹里找 card-reply」,而那条痕会被挤走 —— 拒回把「他选的是 approve」写明白也没用,**下一拍它又从头翻一次** | `answeredCardFact`:把「这张卡已答过 → 他选的是什么」做成**每拍都在的事实行** | **平台**(结构) |
| O | 模型传 `requirement: null`,工具 `null.trim()` 当场崩,整次调用记成「行动被拒」 | 工具入参是宽松描述式 schema,null 一路落到处理器;处理器只判了 `undefined` | 边界上一律经 `text()` 归一,类型如实写成可空 | **平台**(我写的工具) |
| P | 拒回里摆着 `count=(没写过)` —— 那是 S-FEEL 的函数名,不是谁忘了记的账 | `readingsOf` 把内建函数当台账名 | `SFEEL_WORDS` 补上 count/sum/min/max/all/any | 平台 |
| Q | 模型压拍没写骨架就发数据工单,人打开是空表,而**零行 = 零缺**,闸与哨兵一起放过 | 「缺必填」只数已有行;`_missing = 0` 在零行时恒真 | 平台 `emptyGate`(**卡自报的表一行都没有 = 没完成**)+ 说明书③「发卡前骨架必须先有行」+ 哨兵 `profile_rows >= created` 逐颗对账 | 平台 + 活动 |
| R | 两颗缺库只办了第一颗,第二颗还 absent 就把 S1 的卡发了出去 | **发卡的授权面是调人预算,不是计划项** —— 它绕得过哨兵;而 S1 那张卡的**前提**只写在剧本里 | 前提写进说明书的卡骨架(「一颗 absent 都不剩」「有几颗 absent 就要办几颗」) | 活动 |
| S | 每例白吃一次的「早收」:工具还没轮到调,`finish` 先到了 | 那是**计划次序**的小错,却被记成「行动被拒」去喂熔断计数 | 与「重发已答过的卡」同一把尺:**头一次只落观察,第二次起才算被拒** | **平台** |
**可推广的三条**(已记 `docs/debt.md`):
@@ -199,6 +226,9 @@ P-11 一条(`LM2596S-3.3 → LM2596S-ADJ | alternate | fae_confirmed`);P-01 回
2. 拒回文案只谈行与列,数不出「缺的是一次工具调用」(发现 G);
3. 执行工单绑的必交付台账**一行都没有**时提交闸无牙 —— 回落只数「已有行缺不缺必填」,零行 = 零缺(发现 H)。
发现 L 是这三条里**唯一当场治了平台的**:闸看的表与件上摆的表不是同一张,闸就等于没有 ——
而这件事只有脚本 e2e 逮得到(它每张卡都先空手交一次;人手点的时候会先把格子填好再按提交)。
**还有一条**(平台侧,已记 debt):`disable` 这条路从来没通过(发现 A)——
`Disabled` 只能从 `Enabled` 来,而 `Enabled` 只由 `plan` 动作写。
@@ -222,3 +252,13 @@ P-11 一条(`LM2596S-3.3 → LM2596S-ADJ | alternate | fae_confirmed`);P-01 回
- 落账逐单 SQL 复核:P-11 主记录 / 差异明细 / P-01 回写 / 验证任务 / 主数据 P-08 与 P-10,值与业务一致;
- **只读边界守住**:`customer_approval_status` 每单都留空(本活动不产生),`rule_version` 随结论落账;
- 出口只有一口 `assessment-released`,每单都真发射了。
4. **两条线逮到的缺陷不重合,缺一条就漏一半。**
浏览器逮的是**人看见什么、点得动什么**(件、闸、徽标、翻页、画面切换、快照 1:1);
脚本逮的是**机制**(空手交卷、循环、崩、归属)。发现 L–S 八条没有一条是浏览器走查逮到的 ——
因为人手点的时候会先把格子填好再按提交,永远不会去试「空着交上去会怎样」。
5. **拒回喂事实,不喂指针。**
发现 N 那一轮把「他选的是 approve、下一步写哪面旗」写进拒回,日志里逐字可见,模型照发不误 ——
因为**下一拍它又从头翻一次会滚走的窗口**。把同一件事做成「每拍都在的事实行」之后,一次就好了。
同一条道理已经在 worldTouched(2026-08-21)、E04 的 worldTouched 事实行(2026-08-24)上各现过一次。
+2 -2
View File
@@ -165,7 +165,7 @@ activities/<id>/src/ ──(pnpm activities:build)──> dist/{manifest.json,in
| 门 | 在哪 | 规则 |
|---|---|---|
| **起单宽进** | `process.start` | 不按 Schema 拒缺项。三活动 `input.required` 一律空(宽进严出),起单补全卡那条老路实际不再触发(彻底移除记 debt) |
| **执行卡提交闸** | 卡面 `gate` + 服务端 | 必填缺着按不下提交(`{ready,why,blanks}`,与放行门 L1 **同一个数同一套算法**);**服务端在收下回传之前也把一遍**(前端灰按钮只是给人看的)。看哪几张表:卡上绑了 blocks 看绑的;**没绑(执行卡零输入后是常态)回落到必交付物声明的 `ledgers`/`ledger`**(`gateAddressesOf`,有测钉着)。两边都空才不给闸 —— 没有依据的时候不拦人 |
| **执行卡提交闸** | 卡面 `gate` + 服务端 | 必填缺着按不下提交(`{ready,why,blanks}`,与放行门 L1 **同一个数同一套算法**);**服务端在收下回传之前也把一遍**(前端灰按钮只是给人看的)。看哪几张表**三级取一**(`gateAddressesOf`,有测钉着):①卡上绑了 blocks 看绑的;②**卡自己声明的 `gateLedgers`**(零输入卡该给的就是它 —— 只有发卡的人知道这一张要人补哪几张表);③回落到必交付物声明的 `ledgers`/`ledger`。三级全空才不给闸 —— 没有依据的时候不拦人。**主数据类的数据工单必须给 `gateLedgers`**:它要补的表(`parts.*`)不在必交付物名单里,不给就回落到一堆无关的表、一格不缺,件上红带点着缺项而提交钮照样绿(2026-08-27 W05 e2e 实测) |
| **提交门**(`submit` 预检) | engines | 机检(L1/L2)**必须为真**;裁量(L3/L4)**裁过即可**(裁成「不满足」照样能提交,裁断连理由随卡交给人 —— 一条只接受「是」的判据是一句教模型说谎的提示);**L4 带谓词时谓词说了算**(谓词真 = 已达标,模型那句相反的理由一并丢掉;谓词假才轮到裁量);必交付物按 ledger 地址数本单草稿行 |
| **放行门入场哨兵** | CMMN sentry | **必须有 ifPart,且读基座数、认「办完了没有」** —— 只有 onPart 的哨兵等于「有人写过一笔就开门」,模型写个 `false` 就推开了(实测:跳过三步直达放行门) |
| **熔断** | Harness | 连续 5 拍行动被拒 → 停问模型,发熔断卡把拒绝理由摆给人(比对削掉串位前缀、另开 60 条窗) |
@@ -252,7 +252,7 @@ activities/<id>/src/ ──(pnpm activities:build)──> dist/{manifest.json,in
| `ask` | ~~已退役~~ | 基座原地转成零输入执行工单(字段名并进指导书);别再用 |
| `entry` | `{itemId, ask, ledger, entryKey, fields, also?}` | 数据录入卡,挂起;确认那一下基座直写 |
| `decide` | `{itemId, ask, options(≥2), basis}` | 决策卡,挂起 |
| `human` | `{itemId, workOrder, card{title,ask,steps?,standard?,basis}}` | 执行/验收工单,挂起。**执行卡零输入**(2026-08-02 用户定):不摆 `fields` 不绑 `blocks`,填写全在工作区的件上(editGate + 孪生件),人按「保存」基座直写草稿层;必填由提交闸看住(闸回落到必交付物的台账声明) |
| `human` | `{itemId, workOrder, card{title,ask,steps?,standard?,basis}}` | 执行/验收工单,挂起。**执行卡零输入**(2026-08-02 用户定):不摆 `fields` 不绑 `blocks`,填写全在工作区的件上(editGate + 孪生件),人按「保存」基座直写草稿层;必填由提交闸看住 —— **卡上加 `gateLedgers:["域.对象"]` 告诉闸这一张要拦哪几张表**,不给才回落到必交付物声明 |
| `listen` | `{itemId, eventType, keyFilter, deadline}` | 立监听,挂起;时限必配 |
| `finish` | `{itemId, rationale}` | 宣告一项完成;非人工激活的项就地过 Active 再收 |
| `submit` | `{rationale, fields?}` | 提交放行 → 预检 → 发验收卡挂起 |