fix(w05): 重判的行由确定的一侧圈 —— 打回点名工作温度,不许去改静态电流

用户实测:「我打回让修改工作温度的结论,给我改了静态电流的结论」。库里的证据一字不差:
工作温度它没改(还给了理由:候选值 1℃ 远窄于 -40~+85℃,硬约束不满足 —— 这半截是对的),
而没人问过的静态电流被它从 soft_mismatch 翻成 match,evidence_note 里却写着「维持原判」。

根因不是提示词不够严,是**整表交出去、整表写回来**:没有任何东西限制它只动被点名的那一行,
而「我只改了那一条」这句话本身也出自它嘴里。规范 §4.6 纪律一(行由确定的那一侧决定,
不由 AI 决定)—— 昨天刚加的重判轮自己栽在这条上。

S3 复核轮两笔变四笔(全部只在 case_redo_note != "" 那一轮跑):
① 圈范围:按字面点名(contains,与基座给模型摆的「意见点名对上了哪几行」同一种认法)标
   named / kept(明细表新增 redo_scope 列);
② 兜底:一条都没点到(意见说的是整批)→ 整表算被点名,读上一笔刚写下的 tally,不重算;
③ 重判只喂 named 的行 —— 没点名的行 AI 连见都见不到,自然改不着(也省 token);
④ 写回也只写这几行;模型漏答某一条时 else 兜回它上一轮的值。

同批修一条挡了两次路的老毛病(昨夜刚记 debt,今晚又撞停、还卡住了我自己的验证):
人答得比发卡那一拍收尾还快时,回传被判 stale、wake 又被那一拍的 park 覆盖,单子停在
awaiting-event 再没人叫醒。deliver 判了 stale 照旧不跑这一拍,但把停着的单子拨回 running。

验收:e2e w05 5/5 released、被拒 0;案例 0 新断言「打回只动了被点名的那几条,其余行一个字没变」
真跑通过(途中逼出脚本自己的坑:放行会清空草稿层,快照得留在放行之前);
模型调用 案例 0 = 2 次、其余 = 1 次。四处变异验证。W05@2.18.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:17:09 +08:00
co-authored by Claude Fable 5
parent 4d2c3657f9
commit 8314148ebc
8 changed files with 320 additions and 60 deletions
+51
View File
@@ -709,6 +709,16 @@ const SUITES = {
* 这一趟往返没被揉坏——是内容正确性检查,不是归属检查。
*/
expectFinalValue: { address: 'substitution.relation', where: 'candidate_internal_pn', equals: 'XL2596S-5.0', column: 'recommendation_level', contains: 'conditional' },
/*
* **打回只许动被点名的那几条**(2026-09-01 用户实测:「我打回让修改工作温度的结论,
* 给我改了静态电流的结论」)。这条意见点名的是「使能阈值」——
* 交回之前先把明细存一份,收工时逐行比:名字没出现在意见里的行,四态与三段叙述一个字都不许变。
*/
expectRedoTouchesOnlyNamed: {
address: 'substitution.difference_item',
nameCol: 'parameter_name',
cols: ['difference_status', 'impact_statement', 'evidence_note', 'unknown_disposition'],
},
/*
* **跨案例隔离**(2026-08-30,协调者裁定①)—— 这一例为了让 `distinct=1` 成立,
* 把**原 PN(LM2596S-5.0,案例 1/2 也在用它)**转挂到新建的 type_id=90;这一改
@@ -1339,6 +1349,10 @@ async function runCase(suite, kase) {
let dataCardSeen = false;
/** w05「都在库」:匹配**验收工单**那一段的执行卡出现过 —— 数据齐了也要叫人一次 */
let acceptCardSeen = false;
/** 打回之前的明细快照(`expectRedoTouchesOnlyNamed`)—— 只存第一份 */
let redoBefore;
/** 放行前的最后一份明细快照 —— 放行会清空草稿层,收工时取不到 */
let redoAfter;
/** 那张卡的**面孔**不对(标题是验收工单,发出去却是执行工单)—— 2026-08-31 真撞过一次 */
let acceptFaceWrong = false;
/** w05「类型不一致·硬拦」:`sub_eval.case.gate_reason` 命中过 `kase.expectGateReason` 那个词 */
@@ -1446,6 +1460,19 @@ async function runCase(suite, kase) {
gateReasonMatched = caseRow !== undefined && kase.expectGateReason.test(String(caseRow['gate_reason'] ?? ''));
}
/*
* 打回之前把明细存一份(2026-09-01)—— 收工时拿它逐行比「没点名的行有没有被动过」。
* 存在**答卡之前**:答完那一拍配方就重判了,再存就晚了。
*/
if (kase.expectRedoTouchesOnlyNamed !== undefined) {
const rows = draftRows(body.drafts ?? [], kase.expectRedoTouchesOnlyNamed.address);
/* 第一份 = 打回之前(S2 刚判完那一版);此后每轮覆盖,**留住放行前的最后一份** ——
放行会把草稿并进正式表并清空,收工时再取就什么都没有了(实测踩到) */
if (rows.length > 0) {
if (redoBefore === undefined) redoBefore = rows.map((r) => ({ ...r }));
redoAfter = rows.map((r) => ({ ...r }));
}
}
// 决策卡挂在对话区,不在这一摞里 —— 每轮顺手看一眼有没有等着人点的
decisions += await answerDecisions(taskId, kase, decided);
await answerEntries(taskId, kase, entered);
@@ -1678,6 +1705,30 @@ async function runCase(suite, kase) {
console.log(' ⚠️ 这一例没有要补的数据,却派出了一张数据工单 —— 判失败');
ok = false;
}
if (kase.expectRedoTouchesOnlyNamed !== undefined) {
/* `address` 在轮询那一头取快照时用过,这儿只比对已经存下来的两份 */
const { nameCol, cols } = kase.expectRedoTouchesOnlyNamed;
const note = (kase.decideSeq ?? []).find((d) => String(d).startsWith('custom:')) ?? '';
const after = redoAfter ?? [];
if (redoBefore === undefined || redoBefore.length === 0 || after.length === 0) {
console.log(' ⚠️ 打回前后的明细没取到,「只动被点名的行」这条没验成 —— 判失败');
ok = false;
} else {
const moved = [];
for (const row of after) {
const name = String(row[nameCol] ?? '');
// 被点名的那几行本来就该变
if (name === '' || note.includes(name)) continue;
const was = redoBefore.find((r) => String(r[nameCol] ?? '') === name);
if (was === undefined) continue;
for (const col of cols) if (String(was[col] ?? '') !== String(row[col] ?? '')) moved.push(`${name}.${col}`);
}
if (moved.length > 0) {
console.log(` ⚠️ 打回只点了名的那几条,没点名的行却被改了:${moved.join('、')} —— 判失败`);
ok = false;
} else console.log(' 打回只动了被点名的那几条,其余行一个字没变');
}
}
if (kase.expectAcceptCard && !acceptCardSeen) {
console.log(' ⚠️ 数据齐了也该叫人一次(只读的验收工单),一张都没见到 —— 判失败(多半是 S1 又被拍首的 doneWhen 扫描收掉了)');
ok = false;