fix(w05): 复核轮审核人说了算,并把这一轮改了什么说进对话区

用户第二次实测:「重做还是没有修改掉内容」—— 他写「静态电流结论改成不匹配」,而两颗料的
静态电流都是 5mA,AI 搬出第一轮那份规矩里的第 3 条(硬约束不因措辞变成 match)把他顶了回去:
evidence_note 白纸黑字「维持上一轮 match…硬规矩要求按实际值判定,不能因措辞改变结论」。

**行的范围这次是对的**(只把那一条交给 AI、只写回那一条),错在**谁说了算**。三处想岔了:
① 那条规矩管的是它自己判的时候别被措辞带偏,不是用来否决审核人的;
② 方向也反了 —— 它守的是「不许把不满足说成满足」(危险方向),而他要的是收紧结论;
③ 更根本:审核人在件上本来就能亲手改任何一条四态,AI 在重判这条路上顶回去,
   等于同一件事在两条路上给出相反的答案。
改:复核指令明写这条覆盖 —— 他点名要什么结论就落什么,不要顶回去;落的同时 evidence_note
记两句「按谁的意见」+「数据面是什么」,下一个人回看能一眼分清数据判的还是人定的。

同批(用户接着提:「需要把 ai 的修改意见通过右边 ai 聊天窗口展示出来,不然都不知道发生了什么」):
复核完在对话区说一句「按你的意见复核了 N 条 —— <参数名>:<判断依据>」,摆的是件上那一行
自己的话,不另编一句。**它必须排在卡之前** —— 发了卡这一拍就收了,排在卡后面的 say 要等
下一拍才跑:原来那句「差异结论已送审,请核对」实测正是在人答完之后才出现的;而且它与卡自己的
ask 本来就是同一件事,一并退役。

验收:e2e w05 5/5 released、被拒 0;案例 0 三条断言全绿 ——
「复核那一轮在对话区说了话」「打回点名要的结论真落下去了(使能阈值 = conditional)」
「打回只动了被点名的那几条」。包内 +2 测。W05@2.21.0 已装库。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
xcs
2026-09-01 01:57:52 +08:00
co-authored by Claude Fable 5
parent cec19adc85
commit e474275b28
5 changed files with 141 additions and 10 deletions
+32 -1
View File
@@ -637,7 +637,20 @@ const SUITES = {
sample: 0,
name: '候选缺库 · XL2596S-5.0 建档并选已有类型(type_1,2026-08-31 起本活动只选不建)'
+ '· 审核先点名意见驳回再通过',
decideSeq: ['custom:使能阈值那条 UNKNOWN 的去向请写明验证任务号与测试条件', 'approve'],
/*
* **点名要一个结论**(2026-09-01 用户第二次实测:「重做还是没有修改掉内容」——
* 他写「静态电流结论改成不匹配」,AI 搬出「硬约束不因措辞改判」把他顶了回去)。
* 审核人是这一步的责任人:他点名要什么结论就该落什么。这条意见既点了名(使能阈值)
* 又点了结论(conditional),`expectRedoSetsStatus` 收工时逐字核。
*/
decideSeq: ['custom:使能阈值那条不是判不了,改成 conditional(有条件)—— 补一次实测就能用', 'approve'],
expectRedoSetsStatus: { address: 'substitution.difference_item', nameCol: 'parameter_name', param: '使能阈值', column: 'difference_status', equals: 'conditional' },
/*
* 复核完得在对话区说清「按你的意见改了哪一条、改成什么」(2026-09-01 用户:
* 「需要把 ai 的修改意见通过右边 ai 聊天窗口展示出来,不然都不知道发生了什么」)。
* 那句话必须**排在卡之前**才赶得上人看卡 —— 排在卡后面的 say 要等下一拍才跑。
*/
expectRedoNarration: '按你的意见复核了',
expectDecision: true,
expectPort: 'assessment-released',
/*
@@ -1705,6 +1718,24 @@ async function runCase(suite, kase) {
console.log(' ⚠️ 这一例没有要补的数据,却派出了一张数据工单 —— 判失败');
ok = false;
}
if (kase.expectRedoNarration !== undefined) {
const said = (last?.trail ?? []).some((e) => String(e.title ?? '').includes(kase.expectRedoNarration));
if (said) console.log(` 复核那一轮在对话区说了话(「${kase.expectRedoNarration}…」)`);
else {
console.log(` ⚠️ 复核完对话区一个字没说(找不到「${kase.expectRedoNarration}」)—— 人不知道发生了什么,判失败`);
ok = false;
}
}
if (kase.expectRedoSetsStatus !== undefined) {
const { address, nameCol, param, column, equals } = kase.expectRedoSetsStatus;
const row = (redoAfter ?? draftRows(last?.drafts ?? [], address)).find((r) => String(r[nameCol] ?? '') === param);
const got = String(row?.[column] ?? '');
if (got === equals) console.log(` 打回点名要的结论真落下去了(${param}.${column} = ${equals})`);
else {
console.log(` ⚠️ 打回点名要「${equals}」,${param}.${column} 却是「${got || '(没取到)'}」—— 判失败(AI 又把审核人顶回去了?)`);
ok = false;
}
}
if (kase.expectRedoTouchesOnlyNamed !== undefined) {
/* `address` 在轮询那一头取快照时用过,这儿只比对已经存下来的两份 */
const { nameCol, cols } = kase.expectRedoTouchesOnlyNamed;