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:
@@ -357,9 +357,20 @@ type ParamPair = {
|
||||
candidateValue: string;
|
||||
unit?: string;
|
||||
constraintType: 'hard' | 'soft';
|
||||
requirement?: string;
|
||||
evidenceRef?: string;
|
||||
/**
|
||||
* 这两格**模型爱给 null**(「这一项没有客户约束」它写 `requirement: null` 而不是省掉)——
|
||||
* 工具入参是宽松描述式 schema,null 一路落到这里,`null.trim()` 当场崩,
|
||||
* 整个工具调用记成「行动被拒」(2026-08-27 W05 e2e 实测,一单撞两次)。
|
||||
* 类型如实写成可空,取值一律经 `text()`:**边界上的东西按它真会来的样子接**。
|
||||
*/
|
||||
requirement?: string | null;
|
||||
evidenceRef?: string | null;
|
||||
};
|
||||
/** 边界上的字符串:null / undefined / 非字符串一律归成空串(模型给 null 是常态,见 ParamPair 上的注) */
|
||||
function text(v: unknown): string {
|
||||
return typeof v === 'string' ? v.trim() : '';
|
||||
}
|
||||
|
||||
type CompareCounts = { match: number; mismatch: number; unknown: number; conditional: number; hardMismatch: number; softMismatch: number; configurable: number };
|
||||
|
||||
const UNKNOWN_WORDS = /^(未披露|未知|未给出|unknown|n\/?a|tbd|待定|-|—|–)$/i;
|
||||
@@ -397,9 +408,9 @@ function compareParsed(o: Parsed, c: Parsed): CompareResult {
|
||||
}
|
||||
|
||||
/** 客户约束:">= 40" / "<= 85" / "-40~85"(候选必须盖住);解析不了当没给 */
|
||||
function satisfiesRequirement(requirement: string | undefined, c: Parsed): boolean | undefined {
|
||||
if (requirement === undefined || requirement.trim() === '' || c.kind === 'unknown' || c.kind === 'text') return undefined;
|
||||
const s = requirement.trim();
|
||||
function satisfiesRequirement(requirement: string | null | undefined, c: Parsed): boolean | undefined {
|
||||
const s = text(requirement);
|
||||
if (s === '' || c.kind === 'unknown' || c.kind === 'text') return undefined;
|
||||
const ge = new RegExp(`^(?:>=|≥)\\s*(${NUM})$`).exec(s);
|
||||
const le = new RegExp(`^(?:<=|≤)\\s*(${NUM})$`).exec(s);
|
||||
const hi = c.kind === 'range' ? c.hi : c.value;
|
||||
@@ -415,8 +426,8 @@ function satisfiesRequirement(requirement: string | undefined, c: Parsed): boole
|
||||
/** 生命周期不是数值:候选比原料「更活」算 covers(替 NRND 料本就是为了这个),更差才是 differs */
|
||||
const LIFECYCLE_ORDER: Record<string, number> = { active: 3, nrnd: 2, risk: 2, eol: 1, discontinued: 0 };
|
||||
function compareLifecycle(o: string, c: string): CompareResult | undefined {
|
||||
const ro = LIFECYCLE_ORDER[o.trim().toLowerCase()];
|
||||
const rc = LIFECYCLE_ORDER[c.trim().toLowerCase()];
|
||||
const ro = LIFECYCLE_ORDER[text(o).toLowerCase()];
|
||||
const rc = LIFECYCLE_ORDER[text(c).toLowerCase()];
|
||||
if (ro === undefined || rc === undefined) return undefined;
|
||||
return rc === ro ? 'equal' : rc > ro ? 'covers' : 'differs';
|
||||
}
|
||||
@@ -430,11 +441,11 @@ function judgePair(p: ParamPair): { compareResult: CompareResult; differenceStat
|
||||
const req = satisfiesRequirement(p.requirement, c);
|
||||
if (req === false) {
|
||||
// 客户约束不满足:比对结果照实留(narrower/differs),结论一律 MISMATCH 高风险
|
||||
return { compareResult: compareResult === 'unknown' ? 'differs' : compareResult, differenceStatus: 'mismatch', riskLevel: 'high', note: `不满足客户约束 ${p.requirement ?? ''}` };
|
||||
return { compareResult: compareResult === 'unknown' ? 'differs' : compareResult, differenceStatus: 'mismatch', riskLevel: 'high', note: `不满足客户约束 ${text(p.requirement)}` };
|
||||
}
|
||||
if (compareResult === 'unknown') return { compareResult, differenceStatus: 'unknown', riskLevel: 'medium', note: '一侧未披露:标 UNKNOWN,挂验证任务或补来源' };
|
||||
if (compareResult === 'equal' || compareResult === 'covers') {
|
||||
if (hard && (p.evidenceRef === undefined || p.evidenceRef.trim() === '')) {
|
||||
if (hard && text(p.evidenceRef) === '') {
|
||||
return { compareResult, differenceStatus: 'conditional', riskLevel: 'medium', note: '硬约束缺证据引用:未经证据支持不判 MATCH' };
|
||||
}
|
||||
return { compareResult, differenceStatus: 'match', riskLevel: 'low', note: compareResult === 'covers' ? '候选范围盖住原范围' : '一致' };
|
||||
|
||||
Reference in New Issue
Block a user