From e474275b28f30e45acb2b54ee7eba080a2dfde60 Mon Sep 17 00:00:00 2001 From: xcs Date: Tue, 1 Sep 2026 01:57:52 +0800 Subject: [PATCH] =?UTF-8?q?fix(w05):=20=E5=A4=8D=E6=A0=B8=E8=BD=AE?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8=E4=BA=BA=E8=AF=B4=E4=BA=86=E7=AE=97,?= =?UTF-8?q?=E5=B9=B6=E6=8A=8A=E8=BF=99=E4=B8=80=E8=BD=AE=E6=94=B9=E4=BA=86?= =?UTF-8?q?=E4=BB=80=E4=B9=88=E8=AF=B4=E8=BF=9B=E5=AF=B9=E8=AF=9D=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户第二次实测:「重做还是没有修改掉内容」—— 他写「静态电流结论改成不匹配」,而两颗料的 静态电流都是 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 Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet --- CLAUDE.md | 23 +++++++++ .../__tests__/steps.test.ts | 42 +++++++++++++-- .../src/case-model.ts | 51 +++++++++++++++++-- .../src/manifest.ts | 2 +- tools/e2e-a2.mjs | 33 +++++++++++- 5 files changed, 141 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 880e325..a2efa7f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1834,6 +1834,29 @@ .dossiers 三列网格(1fr·76px·1fr),中间列是实体动画道 —— ⇄ 徽呼吸 + 双点绕徽轨道 对转(sw-orbit),总线虚线在道里连两卡;窄屏单列收道。浏览器两帧相位实证。 +- **复核轮审核人说了算 —— AI 不许拿硬规矩顶回去**(2026-09-01 用户第二次实测:「重做还是没有 + 修改掉内容」):他写「静态电流结论改成不匹配」,而两颗料的静态电流都是 5mA,AI 于是搬出 + 第一轮那份规矩里的第 3 条(硬约束不因措辞变成 match)把他顶了回去 —— + `evidence_note` 白纸黑字:「维持上一轮 match…硬规矩要求按实际值判定,不能因措辞改变结论」。 + **行的范围这次是对的**(只把那一条交给 AI、只写回那一条,其余六条一个字没动),错在**谁说了算**。 + 两处想岔了:①那条规矩管的是**它自己判**的时候别被措辞带偏,不是用来否决审核人的; + ②方向也反了 —— 那条守的是「不许把不满足说成满足」(危险方向),而他要的是**收紧**结论; + ③更根本的是:审核人在件上**本来就能亲手改任何一条四态**,AI 在重判这条路上顶回去, + 只是让同一件事在两条路上给出相反的答案。 + 改:复核指令明写这条覆盖 —— **他点名要什么结论就落什么**(「不匹配」= mismatch、「有条件」= + conditional…,不要跟他讲道理、不要顶回去);落的同时 `evidence_note` 记两句 + 「按谁的意见」+「数据面是什么」(例:「按审核人意见调整为 mismatch:<原话>。数据面:两侧均为 5mA。」) + —— 下一个人回看时要能一眼分清**这是数据判出来的**还是**这是人定的**; + 他只是提疑问 / 要补依据的,照旧由 AI 复核后自判。 + e2e 案例 0 的打回意见同批换成**点名要结论**那一种,新断言逐字核那一行真变成了他要的值 + (「打回点名要的结论真落下去了(使能阈值.difference_status = conditional)」)。 + **同批把这一轮的交代摆进对话区**(用户接着提:「需要把 ai 的修改意见通过右边 ai 聊天窗口 + 展示出来,不然都不知道发生了什么」):复核完说一句「按你的意见复核了 N 条 —— <参数名>:<判断依据>」, + 摆的是**件上那一行自己的话**,不另编一句(对话区与件上说的是同一件事,就该是同一句话)。 + **它必须排在卡之前** —— 发了卡这一拍就收了,排在卡后面的 `say` 要等下一拍才跑: + 原来那句「差异结论已送审,请核对」实测正是在**人答完之后**才出现的,而且它与卡自己的 `ask` + 本来就是同一件事,一并退役。 + - **打不回的验收卡就别摆那颗钮**(2026-09-01 用户定:「评审放行与落账确认这一步的验收卡不应该 有重做的选项,因为这里都没有接入 ai」):W05 的 T_release 整个由配方跑完(`guide` 是空串, 不问模型),件又是只读的 —— 打回之后**没有任何东西能接住那句意见**:配方只会原样再提交一次, diff --git a/activities/part-substitution-assessment/__tests__/steps.test.ts b/activities/part-substitution-assessment/__tests__/steps.test.ts index 4174647..cd18259 100644 --- a/activities/part-substitution-assessment/__tests__/steps.test.ts +++ b/activities/part-substitution-assessment/__tests__/steps.test.ts @@ -406,8 +406,8 @@ describe('S3 / S4 配方', () => { const s4 = stepOf('T_release')?.auto?.do ?? []; const s2 = stepOf('S2_assess')?.auto?.do ?? []; - it('S3 九笔:圈范围 → 兜底扩范围 → 复核重判 → 落明细 → 明细重算 → 关系 → 验证记录 → 卡 → 交代', () => { - expect(s3.map((a) => a.kind)).toEqual(['project', 'project', 'analyze', 'project', 'project', 'project', 'project', 'card', 'say']); + it('S3 九笔:圈范围 → 兜底扩范围 → 复核重判 → 落明细 → 明细重算 → 关系 → 验证记录 → 交代 → 卡', () => { + expect(s3.map((a) => a.kind)).toEqual(['project', 'project', 'analyze', 'project', 'project', 'project', 'project', 'say', 'card']); for (const at of [0, 1, 3, 4]) expect(s3[at]?.kind === 'project' && s3[at].into).toBe('substitution.difference_item'); expect(s3[5]?.kind === 'project' && s3[5].into).toBe('substitution.relation'); expect(s3[6]?.kind === 'project' && s3[6].into).toBe('validation.record'); @@ -457,6 +457,19 @@ describe('S3 / S4 配方', () => { /* 判法一处真源:复核指令以第一轮那份规矩打头,后面才接复核纪律 */ const rules = s2[0]?.kind === 'analyze' ? s2[0].instruction : ''; expect(redo?.kind === 'analyze' && redo.instruction.startsWith(rules)).toBe(true); + /* + * **复核轮审核人说了算**(2026-09-01 用户第二次实测:「重做还是没有修改掉内容」)—— + * 他写「静态电流结论改成不匹配」,而两颗料的静态电流都是 5mA,AI 搬出第一轮那条 + * 「硬约束不因措辞变成 match」把他顶了回去。那条规矩管的是**它自己判**的时候别被措辞带偏; + * 审核人是这一步的责任人(要签他的字,他在件上本来就能亲手改任何一条四态),不归它管。 + * 指令里必须**明写这条覆盖**,否则前面那份规矩排在前面、模型照旧照它办。 + */ + const tail = redo?.kind === 'analyze' ? redo.instruction.slice(rules.length) : ''; + expect(tail).toContain('这一轮不归它管'); + expect(tail).toContain('他点名要什么结论,就落什么'); + expect(tail).toContain('不要顶回去'); + /* 落的同时要把「按谁的意见、数据面是什么」记进依据 —— 下一个人得能分清人定的与数据判的 */ + expect(tail).toContain('数据面'); /* 入参:上一轮结论 + 他写的意见 —— 少了任何一样,重判要么无据可依,要么读不到意见 */ const fields: Readonly> = redo?.kind === 'analyze' ? redo.input.fields : {}; for (const key of ['last_status', 'last_impact', 'last_evidence', 'last_unknown_disposition', 'reviewer_note']) { @@ -549,8 +562,31 @@ describe('S3 / S4 配方', () => { expect(stepOf('S3_review')?.auto?.let).toBe(stepOf('S2_assess')?.auto?.let); }); + /* + * **交代必须排在卡之前**(2026-09-01 用户:「需要把 ai 的修改意见通过右边 ai 聊天窗口展示出来, + * 不然都不知道发生了什么」)—— 发了卡这一拍就收了,排在卡后面的 `say` 要等**下一拍**才跑, + * 那时人早看完卡了。实测:原来那句「差异结论已送审,请核对」出现在人答完之后。 + * 两句互斥:被打回那一轮说「按你的意见改了哪一条、改成什么」(摆的是件上那一行自己的 + * `evidence_note`,不另编一句);第一轮说「结论已备好,请核对」。 + */ + it('打回那一轮的交代排在卡之前,摆的是件上那一行的判断依据', () => { + const kinds = s3.map((a) => a.kind); + /* **排在卡之前**:发了卡这一拍就收了,排在后面的 say 要等下一拍 —— 那时人早看完卡了 */ + expect(kinds.lastIndexOf('say')).toBeLessThan(kinds.indexOf('card')); + /* 只在被打回那一轮说 —— 第一轮那句与卡自己的 ask 是同一件事,退役了 */ + const redoSay = s3[7]; + expect(redoSay?.kind === 'say' && redoSay.when).toBe('case_redo_note != ""'); + const text = redoSay?.kind === 'say' ? redoSay.text : undefined; + const parts = JSON.stringify(text); + expect(parts).toContain('按你的意见复核了'); + /* 摆的是件上那一行自己的话与名字 —— 对话区与件上说的是同一件事,就该是同一句话 */ + expect(parts).toContain('evidence_note'); + expect(parts).toContain('parameter_name'); + expect(parts).toContain('substitution_difference_item_redo_scope_named'); + }); + it('S3 的卡:审核面孔、单一选项、挂本步的件', () => { - const card = s3[7]; + const card = s3[8]; expect(card?.kind === 'card' && card.as).toBe('decide'); expect(card?.kind === 'card' && card.variant).toBe('review'); expect(card?.kind === 'card' ? card.options ?? [] : []).toHaveLength(1); diff --git a/activities/part-substitution-assessment/src/case-model.ts b/activities/part-substitution-assessment/src/case-model.ts index b7fb9e8..3466832 100644 --- a/activities/part-substitution-assessment/src/case-model.ts +++ b/activities/part-substitution-assessment/src/case-model.ts @@ -487,10 +487,17 @@ const REVIEW_REDO_ANALYZE = { instruction: [ ASSESS_RULES, '', - '【这一轮是复核】下面**只列了审核人这次点到的那几条**(他写的话在 reviewer_note,每条都带着上一轮的结论 last_*)。', - '· 按他说的复核这几条:该改就改,并在 evidence_note 里写清这一轮改了什么、依据是什么;', - '· 复核下来确实是上一轮判对了,就把 last_* 原样带回,evidence_note 说明为什么维持;', - '· 他要的若与硬规矩冲突(比如要把证据不足的硬约束判成 match),照硬规矩判,并在 evidence_note 里说明为什么不能照办;', + '【这一轮是复核,审核人说了算】下面**只列了他这次点到的那几条**(他写的话在 reviewer_note,每条都带着上一轮的结论 last_*)。', + '', + '**上面第 3 条(硬约束不因措辞变成 match)管的是你自己判的时候别被措辞带偏;这一轮不归它管** ——', + '审核人是这一步的责任人,这份结论要签他的字,而且他在界面上本来就能亲手改任何一条的四态。', + '', + '· **他点名要什么结论,就落什么**:「改成不匹配 / 不符」= mismatch,「一致 / 匹配」= match,', + ' 「判不了 / 待确认」= unknown,「有条件 / 加验证后可用」= conditional。不要跟他讲道理、不要顶回去。', + '· 落的同时把实情记在 evidence_note 里,**一句按谁的意见、一句数据面是什么**,例如:', + ' 「按审核人意见调整为 X:<他的原话>。数据面:原料 5mA / 候选 5mA,两侧一致。」', + ' —— 下一个人回看时要能一眼分清「这是数据判出来的」还是「这是人定的」。', + '· 他只是提疑问、要补依据、要改写措辞(没点名要哪个结论):你复核后自己判,evidence_note 写清这一轮改了什么。', '· **列出来的每一条都要回**,一条不多、一条不少。', ].join('\n'), input: { @@ -948,6 +955,39 @@ export const ASSESSMENT_STEPS = [ 这一步只重算判定(推荐等级跟着四态走),四格交给人,见 `RELATION_RECOMPUTE` 的头注。 */ RELATION_RECOMPUTE, VALIDATION_PROJECTION, + /* + * **重判完要在对话区说清「按你的意见改了哪一条、改成什么」**(2026-09-01 用户: + * 「需要把 ai 的修改意见通过右边 ai 聊天窗口展示出来,不然都不知道发生了什么」)。 + * + * **必须排在卡之前** —— 发了卡这一拍就收了,排在卡后面的笔要等下一拍才跑 + * (那时人早就看完卡了,这句话就成了马后炮:原来那句「差异结论已送审,请核对」 + * 实测正是在**人答完之后**才出现的)。 + * + * 摆的是件上那一行自己的话(`evidence_note` —— 复核指令要求它写成 + * 「按审核人意见调整为 X:<原话>。数据面:…」),不是另编一句: + * 对话区与件上说的是同一件事,就该是同一句话。 + */ + { + kind: 'say' as const, + when: 'case_redo_note != ""', + text: { + cat: [ + '按你的意见复核了 ', + { in: 'substitution_difference_item_redo_scope_named' }, + ' 条 —— ', + { lookup: 'mine:substitution.difference_item', by: { redo_scope: 'named' }, take: 'parameter_name', else: '' }, + ':', + { lookup: 'mine:substitution.difference_item', by: { redo_scope: 'named' }, take: 'evidence_note', else: '' }, + ' 件上那几行已经改好,再核一遍,没问题就点「确认通过」。', + ], + }, + }, + /* + * ~~第一轮那句「差异结论已送审,请在件上逐条核对」~~(2026-09-01 退役): + * 它与卡自己的 `ask` 说的是同一件事(「逐条差异结论已备好。请在工作区的件上逐条核对…」)—— + * 摆两遍是复读。而且它排在卡后面,要等下一拍才跑:实测那句话出现在**人答完之后**。 + * 打回那一轮不同 —— 那时有新东西要交代(改了哪一条、改成什么),所以只留上面那一句。 + */ { kind: 'card' as const, as: 'decide' as const, @@ -961,7 +1001,8 @@ export const ASSESSMENT_STEPS = [ { k: '本轮结论', v: { lookup: 'mine:substitution.relation', by: {}, take: 'difference_summary', else: '' } }, ], }, - { kind: 'say' as const, text: '差异结论已送审,请在件上逐条核对。' }, + /* ~~卡后面那一句 `say`~~(2026-09-01 退役):发了卡这一拍就收了,它要等下一拍才跑 —— + 实测「差异结论已送审,请在件上逐条核对」出现在**人答完之后**。两句交代都提到卡之前去了。 */ /* `] as const`:这个数组也引用了 RELATION_PROJECTION/VALIDATION_PROJECTION —— 理由同 S2 的 do 数组头上那条注释 */ ] as const, }, diff --git a/activities/part-substitution-assessment/src/manifest.ts b/activities/part-substitution-assessment/src/manifest.ts index 46323e1..b12c683 100644 --- a/activities/part-substitution-assessment/src/manifest.ts +++ b/activities/part-substitution-assessment/src/manifest.ts @@ -54,7 +54,7 @@ export const manifest: ActivityManifest = { activityId: ACTIVITY_ID, name: 'ACT-WORK-05 · 差异评估', /* 重写不是改 —— 同版重装是 no-op(静默丢失,踩过) */ - packageVersion: '2.18.0', + packageVersion: '2.21.0', category: '需求分析与方案匹配', sdkVersion: '0.1.0-m5', checksum: 'dev-checksum-part-substitution-assessment', diff --git a/tools/e2e-a2.mjs b/tools/e2e-a2.mjs index 13b1077..56d96a1 100644 --- a/tools/e2e-a2.mjs +++ b/tools/e2e-a2.mjs @@ -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;