脚本 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
+11 -1
View File
@@ -1002,7 +1002,7 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
throw new Error(
`${field.label ?? field.name}「${value}」在${refLabel}里不存在 —— ` +
`${target.label} 的这一列只许引用台账里真实存在的值,不许自造编号或占位引用。` +
'要指的那个人/那一行还不存在的话,先把它真实建出来(要经营的人不存在就发数据录入卡收人),' +
'要指的那一行还不存在的话,先把它真实建出来(该派数据工单的派数据工单、该发数据录入卡的发录入卡),' +
'或者把这一笔挂到已存在的那一行名下',
);
}
@@ -1685,6 +1685,15 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
? input.card['steps'].map((x) => String(x)).filter((x) => x.trim() !== '')
: [];
const standard = String(input.card['standard'] ?? '').trim();
/*
* **这张卡该拦哪几张表** —— 零输入卡没有 `blocks`,闸只能回落到必交付物,
* 而数据工单要人补的是 `parts.*` 这类主数据(不在交付物名单里):闸盯着一堆
* 与这张卡无关的表,一格不缺,空手交卷一路放行(2026-08-27 W05 实测)。
* 认不出形状的一律丢掉,不把脏东西端给闸。
*/
const gateLedgers = Array.isArray(input.card['gateLedgers'])
? input.card['gateLedgers'].map((x) => String(x).trim()).filter((x) => /^[a-z0-9_]+\.[a-z0-9_]+$/i.test(x))
: [];
const issued = await deps.humanTool.issueExecution(context, {
title,
basis: basis.length > 0 ? basis : [{ k: '来源', v: '案例模型当下局面' }],
@@ -1693,6 +1702,7 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
...(steps.length === 0 ? {} : { steps }),
...(standard === '' ? {} : { standard }),
...(blocks === undefined ? {} : { blocks: blocks as ExecutionWorkOrderDraft['blocks'] }),
...(gateLedgers.length === 0 ? {} : { gateLedgers }),
});
// 打扰率的计量对象:每发一张卡就是调了一次人 —— 不计就等于没有上限
await taskStore.addConsumption(input.taskId, { humanCalls: 1 });