Archived
脚本 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:
@@ -794,33 +794,74 @@ async function submitGate(
|
||||
if (task === undefined) return undefined;
|
||||
const manifest = await container.registry.manifestOf(task.activityId).catch(() => undefined);
|
||||
if (manifest === undefined) return undefined;
|
||||
const addresses = gateAddressesOf(order.blocks, manifest.contract.requiredDeliverables);
|
||||
const addresses = gateAddressesOf(order.blocks, manifest.contract.requiredDeliverables, order.gateLedgers);
|
||||
if (addresses.length === 0) return undefined;
|
||||
const watched = manifest.declarations.ledger.filter((l) => addresses.includes(`${l.domain}.${l.object}`));
|
||||
if (watched.length === 0) return undefined;
|
||||
/*
|
||||
* **卡自己点名要拦的表,「一行都没有」也算没完成**(2026-08-27 W05 e2e 实测)。
|
||||
*
|
||||
* 缺必填的算法只数**已有行**缺不缺格子 —— 零行 = 零缺,闸照开。实测那一单:
|
||||
* 模型压拍,骨架一行没写就把 P-08 数据工单发了出去;人打开件是空的,提交钮却绿着,
|
||||
* 交上去「主数据已创建」而库里一颗料都没有,一路走到 S2 才被 refersTo 那道墙拦下。
|
||||
* 只对**卡自报**(`gateLedgers`)那几张表这么判 —— 那是发卡的人明说「这张卡就是来填它的」;
|
||||
* 回落到必交付物的老路一个字不变(那条路上零行是常态,比如还没到写的时候)。
|
||||
*/
|
||||
const declared = (order.gateLedgers ?? []).map((a) => a.trim()).filter((a) => a !== '');
|
||||
|
||||
// 本单写下的行 —— 与任务视图同一约定(`mine:域.对象`)
|
||||
const mine: Record<string, unknown[]> = {};
|
||||
for (const draft of await container.ledger.taskDrafts(taskId)) {
|
||||
(mine[`mine:${draft.domain}.${draft.object}`] ??= []).push({ ...draft.key, ...draft.patch });
|
||||
}
|
||||
return gateOf(missingReadings(mine, watched), watched);
|
||||
return emptyGate(mine, watched, declared) ?? gateOf(missingReadings(mine, watched), watched);
|
||||
}
|
||||
|
||||
/**
|
||||
* 闸看哪几张表:卡上绑了表就看绑的那几张;没绑(执行卡零输入之后是常态)
|
||||
* 回落到**必交付物的台账声明**(`ledgers` 全表,老清单退 `ledger` 单张)。
|
||||
* 两边都空才真的没有依据。抽成纯函数是为了钉测 —— 回落断了闸就整个失效,
|
||||
* 而失效的闸在页面上看不出来(按钮照常绿着)。
|
||||
* **卡自报要拦的表里,有没有「一行都没有」的** —— 有就是没完成,别的都不用算了。
|
||||
*
|
||||
* 「缺必填」的算法只数**已有行**缺不缺格子:零行 = 零缺,闸照开(2026-08-27 W05 e2e 实测)。
|
||||
* 那一单模型压拍、骨架一行没写就把数据工单发了出去 —— 人打开件是空的、什么也补不了,
|
||||
* 提交钮却绿着;交上去「主数据已创建」而库里一颗料都没有,一路走到 S2 才被引用检查拦下。
|
||||
*
|
||||
* 只对**卡自报**(`gateLedgers`)那几张表这么判:那是发卡的人明说「这张卡就是来填它的」。
|
||||
* 回落到必交付物的老路一个字不变 —— 那条路上零行是常态(还没轮到写的时候)。
|
||||
*/
|
||||
export function emptyGate(
|
||||
mine: Readonly<Record<string, ReadonlyArray<unknown>>>,
|
||||
watched: ReadonlyArray<{ domain: string; object: string; label?: string | undefined }>,
|
||||
declared: ReadonlyArray<string>,
|
||||
): { ready: boolean; why: string; blanks: string[] } | undefined {
|
||||
if (declared.length === 0) return undefined;
|
||||
const empty = watched.filter(
|
||||
(l) => declared.includes(`${l.domain}.${l.object}`) && (mine[`mine:${l.domain}.${l.object}`] ?? []).length === 0,
|
||||
);
|
||||
if (empty.length === 0) return undefined;
|
||||
const names = empty.map((l) => l.label ?? `${l.domain}.${l.object}`);
|
||||
return { ready: false, why: `${names.join('、')} 这一单还一行都没有 —— 这张卡就是来建它的,先在件上加一行填好再提交`, blanks: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 闸看哪几张表,三级取一:
|
||||
* ①卡上绑了表(`blocks`)就看绑的那几张;
|
||||
* ②**卡自己声明了 `gateLedgers`** 就看它 —— 零输入卡里,只有发卡的人知道这一张卡要人补哪几张表
|
||||
* (2026-08-27 W05 实测:数据工单要补的是 `parts.*` 主数据,不在必交付物名单里,
|
||||
* 闸于是盯着一堆与这张卡无关的表、一格不缺、空手交卷照放);
|
||||
* ③都没有才回落到**必交付物的台账声明**(`ledgers` 全表,老清单退 `ledger` 单张)。
|
||||
* 三级全空才真的没有依据 —— **没有依据的时候不拦人**。
|
||||
* 抽成纯函数是为了钉测:回落断了闸就整个失效,而失效的闸在页面上看不出来(按钮照常绿着)。
|
||||
*/
|
||||
export function gateAddressesOf(
|
||||
blocks: ReadonlyArray<ViewBlock> | undefined,
|
||||
deliverables: ReadonlyArray<{ ledger?: string | undefined; ledgers?: ReadonlyArray<string> | undefined }>,
|
||||
declared?: ReadonlyArray<string> | undefined,
|
||||
): string[] {
|
||||
const bound = (blocks ?? [])
|
||||
.filter((b): b is Extract<ViewBlock, { t: 'table' }> => b.t === 'table' && typeof b.ledger === 'string')
|
||||
.map((b) => b.ledger as string);
|
||||
if (bound.length > 0) return bound;
|
||||
const own = (declared ?? []).map((a) => a.trim()).filter((a) => a !== '');
|
||||
if (own.length > 0) return own;
|
||||
return deliverables.flatMap((d) => [...(d.ledgers ?? (d.ledger === undefined ? [] : [d.ledger]))]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user