diff --git a/activities/part-substitution-assessment/__tests__/activity.test.ts b/activities/part-substitution-assessment/__tests__/activity.test.ts index 3ed2df9..903b685 100644 --- a/activities/part-substitution-assessment/__tests__/activity.test.ts +++ b/activities/part-substitution-assessment/__tests__/activity.test.ts @@ -93,14 +93,21 @@ describe('清单本身合规', () => { expect(launchProps).not.toContain('datasheets'); }); - it('五条样例:第一条满载走数据工单那条路,名字 ≤14 字,形状各不相同', () => { + /* + * 2026-08-29 Task 11:随新样例集换血(单数 `candidateInternalPn`,起单只剩两格)—— + * 旧断言查的是已撤下的 `candidateInternalPns`(数组)/ `customerName`,这里同步核新形状: + * 第一条是「候选缺库」那条路(S1 会派数据工单),每条样例的入参恰是启动契约那两格,不多不少。 + */ + it('五条样例:第一条候选缺库那条路,名字 ≤14 字,入参恰两格且各不相同', () => { expect(manifest.samples).toHaveLength(5); for (const s of manifest.samples) expect(s.name.length, s.name).toBeLessThanOrEqual(14); expect(new Set(manifest.samples.map((s) => JSON.stringify(s.input))).size).toBe(5); - const first = manifest.samples[0]?.input as { candidateInternalPns?: string[] }; - expect(first.candidateInternalPns).toContain('XL2596S-5.0'); - const newCustomer = manifest.samples.find((s) => (s.input as { customerName?: string }).customerName !== undefined); - expect(newCustomer).toBeDefined(); + const first = manifest.samples[0]?.input as { originalInternalPn?: string; candidateInternalPn?: string }; + expect(first.originalInternalPn).toBe('LM2596S-5.0'); + expect(first.candidateInternalPn).toBe('XL2596S-5.0'); + for (const s of manifest.samples) { + expect(Object.keys(s.input as Record).sort()).toEqual(['candidateInternalPn', 'originalInternalPn']); + } }); /* diff --git a/activities/part-substitution-assessment/src/manifest.ts b/activities/part-substitution-assessment/src/manifest.ts index 7e14a6d..b676688 100644 --- a/activities/part-substitution-assessment/src/manifest.ts +++ b/activities/part-substitution-assessment/src/manifest.ts @@ -462,61 +462,21 @@ export const manifest: ActivityManifest = { /* * ── 样例集:五条形状真不同(常备案例即验收案例)── - * 本任务不改样例(它们仍是旧九步 / 旧入参形状的素材,Task 3 重写案例模型时一并核对)。 - * 库里只播了 LM2596S-5.0 / LM2596S-ADJ 两颗(见 seed-mvp-data.ts),XL2596S-5.0 / CN-DCDC-32V / LM2596S-3.3 刻意不在库。 + * **2026-08-29 Task 11 换成新入参形状**(单数 `candidateInternalPn`,与 §4.1 的启动两参同步 —— + * 旧九步遗留的 `customerCode`/`requestDescription`/`candidateInternalPns`(数组)三键随之撤下, + * 起单表单本就只剩两格)。五条覆盖 S1 的五种入场局面:①候选缺库(新建类型那条路,S1 数据工单 + * 三块都要动)②两颗都在库且同型(零卡直接进 S2)③两颗都在库但分属不同芯片类型(硬拦, + * 光凭其它格子填满绕不过去)④原 PN 缺库(候选在库)⑤两颗都缺库。 + * 库里播了 LM2596S-5.0 / LM2596S-ADJ(DC-DC 降压稳压器,type_id=1)与 AMS1117-3.3 + * (LDO 线性稳压器,type_id=2)三颗(见 seed-mvp-data.ts);XL2596S-5.0 / CN-DCDC-32V / + * LM2596S-3.3 刻意不在库。 */ samples: [ - { - name: '候选缺库·派数据工单', - input: { - customerCode: 'CU-0142', - requestDescription: '工业电源项目在用 LM2596S-5.0(TI,12V→5V),原厂 PCN 转 NRND,客户要求验证替代料;应用 12V→5V、工业温域、输入上限 40V', - originalInternalPn: 'LM2596S-5.0', - candidateInternalPns: ['XL2596S-5.0', 'LM2596S-ADJ'], - }, - expect: { ports: ['assessment-released'] }, - }, - { - name: '都在库·常路', - input: { - customerCode: 'CU-0142', - requestDescription: 'LM2596S-5.0 转 NRND,客户先评估同族可调版 LM2596S-ADJ 能否替代(愿意改外围);应用 12V→5V、工业温域', - originalInternalPn: 'LM2596S-5.0', - candidateInternalPns: ['LM2596S-ADJ'], - }, - expect: { ports: ['assessment-released'] }, - }, - { - name: '原PN缺库·硬阻塞', - input: { - customerCode: 'CU-0142', - requestDescription: '客户另一块板在用 LM2596S-3.3(3.3V 固定版),同批 PCN,要求评估 LM2596S-ADJ 替代;应用 12V→3.3V', - originalInternalPn: 'LM2596S-3.3', - candidateInternalPns: ['LM2596S-ADJ'], - }, - expect: { ports: ['assessment-released'] }, - }, - { - name: '三候选·含硬约束不推荐', - input: { - customerCode: 'CU-0142', - requestDescription: '工业电源 12V→5V,输入上限 40V,工业温域;三个候选:XL2596S-5.0、LM2596S-ADJ、国产 CN-DCDC-32V(输入上限只有 32V)', - originalInternalPn: 'LM2596S-5.0', - candidateInternalPns: ['XL2596S-5.0', 'LM2596S-ADJ', 'CN-DCDC-32V'], - }, - expect: { ports: ['assessment-released'] }, - }, - { - name: '新客户·先建档', - input: { - customerCode: 'CU-9001', - customerName: '深圳恒星电源有限公司', - requestDescription: '新客户口述:工业电源 12V→5V 在用 LM2596S-5.0,想评估 LM2596S-ADJ', - originalInternalPn: 'LM2596S-5.0', - candidateInternalPns: ['LM2596S-ADJ'], - }, - expect: { ports: ['assessment-released'] }, - }, + { name: '候选缺库·新建类型', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'XL2596S-5.0' }, expect: { ports: ['assessment-released'] } }, + { name: '都在库·零卡直分析', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'LM2596S-ADJ' }, expect: { ports: ['assessment-released'] } }, + { name: '类型不一致·硬拦', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'AMS1117-3.3' }, expect: { ports: ['assessment-released'] } }, + { name: '原PN缺库', input: { originalInternalPn: 'LM2596S-3.3', candidateInternalPn: 'LM2596S-ADJ' }, expect: { ports: ['assessment-released'] } }, + { name: '两颗都缺库', input: { originalInternalPn: 'CN-DCDC-32V', candidateInternalPn: 'XL2596S-5.0' }, expect: { ports: ['assessment-released'] } }, ], entry: { file: 'index.js', checksum: 'dev-entry-part-substitution-assessment' }, diff --git a/tools/e2e-a2.mjs b/tools/e2e-a2.mjs index 12c6684..ea69863 100644 --- a/tools/e2e-a2.mjs +++ b/tools/e2e-a2.mjs @@ -53,17 +53,20 @@ const LAND_MASTER = process.env.JCAI_E2E_LAND_MASTER === '1'; const TOKEN_REFRESH_MS = 4 * 60 * 1000; /** - * 标准化参数子表 `parts.parameter` 的行(w05 专用)—— 按 [参数名, 值, 单位, 约束, 说明] - * 五元组批量转成保存载荷的行对象;数值照抄各案例原先写在 `normalized_parameter_set` - * 那个文本列里的那几行,一个数都不改(2026-08-27 结构改:参数集拆成独立子表)。 + * 标准化参数子表 `parts.parameter` 的行(w05 专用)—— 按 [参数名, 值, 单位, 说明] + * 四元组批量转成保存载荷的行对象。 + * + * **2026-08-29 Task 11 四元组(从前是五元组)**:`constraint_type` 已随四步重写搬去 + * `parts.type_param`(约束按类型标一次,不再逐颗料各标一遍 —— 见 manifest.ts 该字段的头注)。 + * `parts.parameter` 现在只有 internal_pn / param_name / param_value / param_unit / param_note + * 五列,没有 `constraint_type` 这一列了;继续按老的五元组喂会把一个物理表没有的列塞进 patch。 */ function paramRows(internalPn, rows) { - return rows.map(([param_name, param_value, param_unit, constraint_type, param_note]) => ({ + return rows.map(([param_name, param_value, param_unit, param_note]) => ({ internal_pn: internalPn, param_name, param_value, param_unit, - constraint_type, param_note, })); } @@ -543,235 +546,238 @@ const SUITES = { ], }, /* - * ACT-WORK-05 差异评估 —— 1 数据(条件,P-08 / P-10 各一张)+ 1 执行 + 2 验收(审核走决策卡循环、放行终局)。 - * 库里只播了 LM2596S-5.0 / LM2596S-ADJ:XL2596S-5.0、CN-DCDC-32V、LM2596S-3.3 刻意不在, - * 「候选缺库 / 原 PN 缺库 / 硬约束不推荐」三条路靠它们。e2e 关落真库,主数据不落正式表,案例可反复跑。 + * ACT-WORK-05 差异评估 —— **2026-08-29 Task 11 随四步重写整体换掉**(spec + * `docs/superpowers/specs/2026-08-29-act-work-05-rewrite-design.md`)。旧套件是九步 / + * 两枚工具 / 三张数据工单(P-08 主数据 + P-10 资料 + 评估输入清单)那个模型的素材, + * 新模型只有一张数据工单(`input-supplement`,三块竖排:要评估什么 / 芯片类型核心参数 + * 字段 / 参数值,合在一张卡上、一次保存四张表)+ 一次 AI 分析(S2,零人环)+ + * 一张审核决策卡(S3,可循环)+ 一张终局放行验收卡(T_release)。 + * 库里播了 LM2596S-5.0 / LM2596S-ADJ(DC-DC 降压稳压器 type_id=1)与 + * AMS1117-3.3(LDO 线性稳压器 type_id=2)三颗;XL2596S-5.0 / CN-DCDC-32V / LM2596S-3.3 + * 刻意不在库。e2e 关落真库,主数据不落正式表,案例可反复跑。 + * + * **两处平台机制这一次是第一个用到的调用方,`runCase`/`answerDecisions` 主循环里 + * 补了对应支持**(其余八套件都没用过,行为不变): + * ① **人工验收清单**(2026-08-27 用户定,`PlanStep.acceptanceChecklist`):S3 审核卡 + * 与 T_release 终局验收卡都带 `checklist`,选「通过」/`verdict:'pass'` 必须 + * `checked ⊇ checklist`,不然回传吃 `checklist-incomplete`(400)。清单原文由 + * 卡自己带下来(`card.checklist` / `workOrders[].checklist`),代答脚本原样照抄 + * 回传,不必知道清单具体写的是哪几句话。 + * ② **零数据卡直分析**(样例 1):S1 的 `doneWhen` 一开局就成立,`auto.do` 的 + * `card` 那一笔永不触发 —— 断言机制是 `expectNoDataCard`(见 `stage` 定义处)。 */ w05: { activityId: 'part-substitution-assessment', - address: 'sub_eval.input_gap', - blankCol: 'supplement', + address: 'parts.profile', + blankCol: 'brand_name', stages: [ { - // 这张卡摆两张表(表 1 主记录 + 表 2 标准化参数子表)—— 主表照旧,子表挂在 extraTables 里 - // 加性表达(见 buildEdits 顶注),保存时两张表的行一次并进同一个 `tables` 载荷。 - match: /数据工单 · 芯片主数据创建/, + // 唯一的数据工单:一张卡摆三块内容,写向四张表(主表 parts.profile, + // extraTables 里另外三张)——四张表随同一次「保存」一并发出,不分次交。 + match: /差异评估的输入还差几格/, address: 'parts.profile', rowKey: 'internal_pn', blankCol: 'brand_name', expectGateBlock: true, - // 数据工单确认即入库(2026-08-27 用户定)—— 交卷那一刻这张卡的表就该进正式表 - landsOnConfirm: true, edits: [], - // 案例没点名的料号兜底(不该走到这里 —— 每个案例都在 stageEdits 里逐颗给了) - fillAllBlank: { - brand_name: '兜底品牌', - specification_description: '兜底规格(案例没给参数集)', - package: 'TO-263-5', - lifecycle_status: 'active', - }, extraTables: [ + /* + * `parts.chip_type` **没有任何 `data-edit` 绑定**(manifest.ts / case-model.ts + * 头注明写:新建类型时人在 `parts.type_param` 那张表加行,`parts.chip_type` 那 + * 张字典表本身没有编辑面 —— `ViewContainer.addRow` 靠克隆末行实现「加一行」, + * 而它在任何一单里都是从零行起,没有可克隆的样子)。写的授权面是**台账声明** + * 不是「当下卡上有没有绑定」(`writesOf` 只认 `declarations.ledger`),所以这条 + * 写入走得通 —— 只是脚本代脚本走 API,不是真人代替点了一个并不存在的按钮。 + * 只有样例 0(候选缺库·新建类型)会喂 `edits`,其余案例这里留空、这张表不写。 + */ + { address: 'parts.chip_type', rowKey: 'type_id', blankCol: 'type_name', edits: [] }, { - // 主键是 (internal_pn, param_name) 复合列 —— 两颗料同名参数(如都有「工作温度」) + // 复合主键 (type_id, param_name) —— 同一类型两个字段不能靠单列区分。 + address: 'parts.type_param', + rowKey: ['type_id', 'param_name'], + blankCol: 'param_desc', + edits: [], + }, + { + // 复合主键 (internal_pn, param_name) —— 两颗料同名参数(如都有「工作温度」) // 不这样区分,rows.find 会永远命中第一条同名行、把不同料的值互相顶掉。 address: 'parts.parameter', rowKey: ['internal_pn', 'param_name'], blankCol: 'param_value', edits: [], - // 案例没点名的参数行兜底(模型骨架只给了 internal_pn+param_name、案例没接住时才会用到) - fillAllBlank: { param_value: '未披露', constraint_type: 'hard', param_note: '兜底(案例没给这一条)' }, }, ], }, - { - match: /数据工单 · 原厂技术资料创建/, - address: 'parts.document', - rowKey: 'internal_pn', - blankCol: 'document_title', - expectGateBlock: true, - landsOnConfirm: true, - edits: [], - fillAllBlank: { - doc_seq: '01', - document_type: 'datasheet', - document_title: 'Datasheet(e2e 兜底)', - document_version: 'Rev A', - document_ref: 'https://example.com/datasheet.pdf', - citation_locator: 'p.1', - document_status: 'valid', - }, - }, - { - match: /评估输入与资料补齐确认/, - address: 'sub_eval.input_gap', - rowKey: 'item_key', - blankCol: 'supplement', - expectGateBlock: true, - edits: [ - { item_key: 'application_scenario', supplement: '12V→5V、工业温域(-40~85℃)、输入上限 40V' }, - { item_key: 'bom_position', supplement: 'U3 电源位' }, - ], - // 其余 gap 行(原厂未披露的参数)统一「留 UNKNOWN」—— 补不了也是合法答案 - fillWhere: { column: 'status', equals: 'gap', values: { supplement: '原厂未披露,留 UNKNOWN 并挂验证任务' } }, - }, ], cases: [ { sample: 0, - name: '候选缺库 · XL2596S-5.0 自动派数据工单(P-08 + P-10)· 审核先点名驳回再通过', - // 决策卡序(2026-08-27 起缺库不再问人,数据工单自动派):审核第一张点名意见驳回;第二张通过 + name: '候选缺库 · XL2596S-5.0 新建芯片类型(type_id=3,字段清单沿用原型口径)入库,' + + '原 PN 一并转挂新类型才够得上 distinct=1 · 审核先点名意见驳回再通过', decideSeq: ['custom:使能阈值那条 UNKNOWN 的去向请写明验证任务号与测试条件', 'approve'], expectDecision: true, expectPort: 'assessment-released', + // 全套只该有一次模型调用(S2 唯一一次 analyze)—— S3 审核循环走的是纯投影重算,不重问模型 + expectAnalyzeCalls: 1, stageEdits: { + 'parts.chip_type': [ + { + type_id: '3', + type_name: 'DC-DC 降压稳压器(评估自定义)', + description: '本次评估自定义的降压转换器类型,核心参数字段清单沿用原类型口径,供原 PN 与候选料共用比对。', + }, + ], + // 字段名 / 说明 / 约束 / 单位逐字照抄 type_id=1 的字典行(见 seed-mvp-data.ts)—— + // 换个类型号但比对维度不变,原 PN 既有的参数值才能按同名继续 lookup 到。 + 'parts.type_param': [ + { type_id: '3', param_name: '输入电压范围', param_desc: '芯片允许的输入直流电压区间。候选料的上限低于应用输入上限就是硬伤,烧的是芯片。', constraint_type: 'hard', param_unit: 'V' }, + { type_id: '3', param_name: '输出电压', param_desc: '固定版给定值;可调版给可调区间,需外围反馈电阻网络设定。固定换可调要动外围。', constraint_type: 'hard', param_unit: 'V' }, + { type_id: '3', param_name: '输出电流', param_desc: '满载持续输出能力。低于原料即降额,负载重时会掉压或过热保护。', constraint_type: 'hard', param_unit: 'A' }, + { type_id: '3', param_name: '开关频率', param_desc: '决定外围电感与输出电容的取值。频率变了外围多半要跟着换,不是换颗料就完。', constraint_type: 'soft', param_unit: 'kHz' }, + { type_id: '3', param_name: '静态电流', param_desc: '空载待机时自身消耗。电池供电场景敏感,市电场景通常不敏感。', constraint_type: 'soft', param_unit: 'mA' }, + { type_id: '3', param_name: '工作温度', param_desc: '保证参数指标的环境温度区间。工业温域窄于应用要求就是硬伤。', constraint_type: 'hard', param_unit: '℃' }, + { type_id: '3', param_name: '使能阈值', param_desc: 'ON/OFF 引脚的开关门限。上游用 MCU 电平直驱时,阈值对不上会关不掉或开不起来。', constraint_type: 'hard', param_unit: 'V' }, + ], 'parts.profile': [ + // 原 PN 转挂新类型 —— distinct(chip_type_id) 要求两颗归同一个值,新建类型时 + // 两颗都得落在它上面,不是只给候选料一颗挂新号。 + { internal_pn: 'LM2596S-5.0', chip_type_id: '3' }, { internal_pn: 'XL2596S-5.0', + chip_type_id: '3', brand_name: 'XLSEMI', manufacturer_name: 'XLSEMI', specification_description: '降压型 DCDC 稳压器,3A,固定 5.0V,150kHz', - package: 'TO-263-5', - process_technology: 'BCD', lifecycle_status: 'active', datasheet_ref: 'https://www.xlsemi.com/datasheet/XL2596S.pdf', datasheet_version: 'Rev C', - application_scenarios: '工业电源;消费电源', }, ], + // LM2596S-5.0 的既有参数值按同名 lookup 自动带过来,不必在这里重填; + // 只需交代候选料 XL2596S-5.0 那一份。 'parts.parameter': paramRows('XL2596S-5.0', [ - ['输入电压范围', '4.5~40', 'V', 'hard', '客户上限 40V'], - ['输出电压', '5.0', 'V', 'hard', '固定版'], - ['输出电流', '3', 'A', 'hard', '满载能力'], - ['开关频率', '150', 'kHz', 'hard', '影响外围'], - ['静态电流', '4.2', 'mA', 'soft', '待机功耗'], - ['工作温度', '-40~+125', '℃', 'hard', '工业温域'], - ['使能阈值', '未披露', 'V', 'hard', '原厂未给'], + ['输入电压范围', '4.5~40', 'V', '客户上限 40V'], + ['输出电压', '5.0', 'V', '固定版'], + ['输出电流', '3', 'A', '满载能力'], + ['开关频率', '150', 'kHz', '影响外围'], + ['静态电流', '4.2', 'mA', '待机功耗'], + ['工作温度', '-40~+125', '℃', '工业温域'], + ['使能阈值', '未披露', 'V', '原厂未给'], ]), - 'parts.document': [ - { - internal_pn: 'XL2596S-5.0', - doc_seq: '01', - document_type: 'datasheet', - brand_name: 'XLSEMI', - document_title: 'XL2596S 150KHz 3A Buck DC/DC Converter', - document_version: 'Rev C', - document_ref: 'https://www.xlsemi.com/datasheet/XL2596S.pdf', - citation_locator: 'p.3 Electrical Characteristics', - document_status: 'valid', - }, - ], }, - // 使能阈值未披露 → UNKNOWN → 条件推荐(工具算的,不是模型说的) + // 使能阈值未披露 → UNKNOWN → 有条件推荐(规则算的,不是模型说的) expectRevised: { address: 'substitution.relation', where: 'candidate_internal_pn', equals: 'XL2596S-5.0', column: 'recommendation_level', contains: 'conditional' }, }, { sample: 1, - name: '都在库 · 常路 · 可调版 → 备选 · 审核一次通过', + name: '都在库 · 两颗同型且齐备 · 零数据卡直接进 S2 分析 · 审核一次通过', + // S1 的 doneWhen 起手就成立(两颗都在库、同型、参数不缺)—— 断言一张数据工单都不发 + expectNoDataCard: true, decide: 'approve', expectPort: 'assessment-released', - expectRevised: { address: 'substitution.relation', where: 'candidate_internal_pn', equals: 'LM2596S-ADJ', column: 'recommendation_level', contains: 'alternate' }, + expectAnalyzeCalls: 1, }, { sample: 2, - name: '原 PN 缺库 · 硬阻塞建档(不问人)· 审核通过', + name: '两颗都在库但分属不同芯片类型 · 硬拦(填别的格子绕不过去,只有改类型才行)· 审核通过', + // gate_reason 要点名「不是同一个芯片类型」——数据卡一出现就查这句话,latch 住 + expectGateReason: /芯片类型/, + // 唯一的修复是把其中一颗转挂到另一颗的类型上;缺的那一个新必填参数一并补上, + // 不改类型光填别的格子过不去(S1 的 doneWhen/distinct 与提交闸的必填检查不是同一件事—— + // 提交闸不查 distinct,这条路真正的硬拦在 S2 的入场哨兵,详见任务报告)。 + stageEdits: { + 'parts.profile': [{ internal_pn: 'LM2596S-5.0', chip_type_id: '2' }], + // 转去 LDO 类型后,LM2596S-5.0 多出一条它从没记过的必填参数「压差」 + 'parts.parameter': [ + { internal_pn: 'LM2596S-5.0', param_name: '压差', param_value: '0.4', param_unit: 'V', param_note: '满载时(本次评估按 LDO 口径补充,DC-DC 拓扑本无此规格)' }, + ], + }, decide: 'approve', expectPort: 'assessment-released', + expectAnalyzeCalls: 1, + }, + { + sample: 3, + name: '原 PN 缺库(候选在库)· 自动派数据工单建档 · 审核通过', + decide: 'approve', + expectPort: 'assessment-released', + expectAnalyzeCalls: 1, stageEdits: { 'parts.profile': [ { internal_pn: 'LM2596S-3.3', + chip_type_id: '1', brand_name: 'TI', manufacturer_name: 'Texas Instruments', specification_description: '降压型 DCDC 稳压器,3A,固定 3.3V,150kHz', - package: 'TO-263-5', - process_technology: 'BCD', lifecycle_status: 'nrnd', datasheet_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf', datasheet_version: 'Rev H (2023-09)', }, ], 'parts.parameter': paramRows('LM2596S-3.3', [ - ['输入电压范围', '4.5~40', 'V', 'hard', '客户上限 40V'], - ['输出电压', '3.3', 'V', 'hard', '固定版'], - ['输出电流', '3', 'A', 'hard', '满载能力'], - ['开关频率', '150', 'kHz', 'hard', '影响外围'], - ['静态电流', '5', 'mA', 'soft', '待机功耗'], - ['工作温度', '-40~+85', '℃', 'hard', '工业温域'], - ['使能阈值', '1.4', 'V', 'hard', 'ON/OFF 阈值'], + ['输入电压范围', '4.5~40', 'V', '客户上限 40V'], + ['输出电压', '3.3', 'V', '固定版'], + ['输出电流', '3', 'A', '满载能力'], + ['开关频率', '150', 'kHz', '影响外围'], + ['静态电流', '5', 'mA', '待机功耗'], + ['工作温度', '-40~+85', '℃', '工业温域'], + ['使能阈值', '1.4', 'V', 'ON/OFF 阈值'], ]), - 'parts.document': [ - { - internal_pn: 'LM2596S-3.3', - doc_seq: '01', - document_type: 'datasheet', - brand_name: 'TI', - document_title: 'LM2596 SIMPLE SWITCHER Power Converter', - document_version: 'Rev H (2023-09)', - document_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf', - citation_locator: '§6.5 Electrical Characteristics(3.3V 列)', - document_status: 'valid', - }, - ], }, }, - { - sample: 3, - name: '三候选 · 两颗缺库一律自动派单 · 国产 32V 硬约束不推荐', - decideSeq: ['approve'], - expectDecision: true, - // 缺库不再问人(2026-08-27 用户定):这一例一张「查无记录」决策卡都不该出现 - expectAsks: [{ re: /查无记录/, count: 0, max: 0 }], - expectPort: 'assessment-released', - stageEdits: { - 'parts.profile': [ - { - internal_pn: 'XL2596S-5.0', - brand_name: 'XLSEMI', - specification_description: '降压型 DCDC 稳压器,3A,固定 5.0V,150kHz', - package: 'TO-263-5', - lifecycle_status: 'active', - }, - { - internal_pn: 'CN-DCDC-32V', - brand_name: '国产某厂', - specification_description: '降压型 DCDC,3A,固定 5.0V,输入上限 32V', - package: 'TO-263-5', - lifecycle_status: 'active', - }, - ], - 'parts.parameter': [ - ...paramRows('XL2596S-5.0', [ - ['输入电压范围', '4.5~40', 'V', 'hard', '客户上限 40V'], - ['输出电压', '5.0', 'V', 'hard', '固定版'], - ['输出电流', '3', 'A', 'hard', '满载能力'], - ['开关频率', '150', 'kHz', 'hard', '影响外围'], - ['静态电流', '4.2', 'mA', 'soft', '待机功耗'], - ['工作温度', '-40~+125', '℃', 'hard', '工业温域'], - ['使能阈值', '未披露', 'V', 'hard', '原厂未给'], - ]), - ...paramRows('CN-DCDC-32V', [ - ['输入电压范围', '4.5~32', 'V', 'hard', '上限 32V'], - ['输出电压', '5.0', 'V', 'hard', '固定版'], - ['输出电流', '3', 'A', 'hard', '满载能力'], - ['开关频率', '150', 'kHz', 'hard', '影响外围'], - ['静态电流', '6', 'mA', 'soft', '待机功耗'], - ['工作温度', '-40~+85', '℃', 'hard', '工业温域'], - ['使能阈值', '1.3', 'V', 'hard', 'ON/OFF 阈值'], - ]), - ], - 'parts.document': [ - { internal_pn: 'XL2596S-5.0', doc_seq: '01', document_type: 'datasheet', document_title: 'XL2596S Datasheet', document_version: 'Rev C', document_ref: 'https://www.xlsemi.com/datasheet/XL2596S.pdf', citation_locator: 'p.3', document_status: 'valid' }, - { internal_pn: 'CN-DCDC-32V', doc_seq: '01', document_type: 'datasheet', document_title: 'CN-DCDC-32V Datasheet', document_version: 'V1.0', document_ref: 'https://example.com/cn-dcdc-32v.pdf', citation_locator: 'p.2', document_status: 'valid' }, - ], - }, - expectRevised: { address: 'substitution.relation', where: 'candidate_internal_pn', equals: 'CN-DCDC-32V', column: 'recommendation_level', contains: 'not_recommended' }, - }, { sample: 4, - name: '新客户 · 录入卡先建档 · 审核通过', - entryValues: { name: '深圳恒星电源有限公司', industry: '工业电源', region: '广东' }, + name: '两颗都缺库 · 自动派数据工单各自建档(原 PN 32V 上限 · 候选 40V 盖住它)· 审核通过', decide: 'approve', expectPort: 'assessment-released', + expectAnalyzeCalls: 1, + stageEdits: { + 'parts.profile': [ + { + internal_pn: 'CN-DCDC-32V', + chip_type_id: '1', + brand_name: '国产某厂', + manufacturer_name: '国产某厂', + specification_description: '降压型 DCDC 稳压器,3A,固定 5.0V,150kHz,输入上限 32V', + lifecycle_status: 'active', + datasheet_ref: 'https://example.invalid/cn-dcdc-32v.pdf', + datasheet_version: 'V1.0', + }, + { + internal_pn: 'XL2596S-5.0', + chip_type_id: '1', + brand_name: 'XLSEMI', + manufacturer_name: 'XLSEMI', + specification_description: '降压型 DCDC 稳压器,3A,固定 5.0V,150kHz', + lifecycle_status: 'active', + datasheet_ref: 'https://www.xlsemi.com/datasheet/XL2596S.pdf', + datasheet_version: 'Rev C', + }, + ], + 'parts.parameter': [ + ...paramRows('CN-DCDC-32V', [ + ['输入电压范围', '4.5~32', 'V', '上限 32V'], + ['输出电压', '5.0', 'V', '固定版'], + ['输出电流', '3', 'A', '满载能力'], + ['开关频率', '150', 'kHz', '影响外围'], + ['静态电流', '6', 'mA', '待机功耗'], + ['工作温度', '-40~+85', '℃', '工业温域'], + ['使能阈值', '1.3', 'V', 'ON/OFF 阈值'], + ]), + ...paramRows('XL2596S-5.0', [ + ['输入电压范围', '4.5~40', 'V', '客户上限 40V'], + ['输出电压', '5.0', 'V', '固定版'], + ['输出电流', '3', 'A', '满载能力'], + ['开关频率', '150', 'kHz', '影响外围'], + ['静态电流', '4.2', 'mA', '待机功耗'], + ['工作温度', '-40~+125', '℃', '工业温域'], + ['使能阈值', '未披露', 'V', '原厂未给'], + ]), + ], + }, + // 原 PN(CN-DCDC-32V)输入上限只有 32V,候选 XL2596S-5.0 上限 40V 盖住原 PN —— 不该拦; + // 真正的硬约束不符落在别处案例,这条只钉「两颗都缺库仍能走完全程」,故不设 expectRevised。 }, ], }, @@ -843,6 +849,18 @@ function rejectedCount(trail) { return trail.filter((t) => t.kind === 'action-rejected' || /行动被拒|被拒[::]|拒绝执行/.test(t.title ?? '')).length; } +/** + * **AI 出场次数**(w05 专用,ADR-014 的 `analyze` 那一笔)—— 轨迹里数 + * 「已把 N 条参数交给模型逐条判定,拿回 M 条结论」这句话(`auto-runner.ts` 的 + * `kind:'observation'` 留痕,字面原样)。W05 全程零 agent,唯一一次模型调用长在 S2; + * 「行动被拒 0」这条验收线对没有 agent 决策的确定性步骤活动自动成立(没有行动可拒), + * 所以这里改守的是这个数 —— 它才是这次真正防回归的那条线(S3 审核循环 / T_release + * 重做都不该让它超过 1,S2 的入场哨兵 `s_inputs_ready` 也不该被绕过去多算一次)。 + */ +function analyzeCallCount(trail) { + return trail.filter((t) => /已把 \d+ 条参数交给模型逐条判定/.test(t.title ?? '')).length; +} + /** 草稿层的行(带主键),按台账地址取 */ function draftRows(drafts, address) { return (drafts ?? []).filter((d) => d.address === address).map((d) => ({ ...d.key, ...d.patch })); @@ -916,6 +934,14 @@ async function expectGateBlocks(cardId, taskId, stage) { * * 选项按案例声明的 value 认;认不上退「推荐项 → 第一项」—— * 代答脚本不该因为模型换了个选项名就判活动失败。 + * + * **人工验收清单**(2026-08-27 用户定,W05 的 S3 审核卡第一个用到这条机制): + * 审核卡(`variant:'review'`)可能带 `card.checklist` ——选**通过**那一项时必须 + * `checked ⊇ checklist`,否则回传吃 `checklist-incomplete`(400)。这里**原样照抄** + * `card.checklist` 回传(不必知道清单具体写的是哪几句话,清单原文由卡自己带下来); + * 只在选中的是「通过」那个值时才带(驳回/自定义意见不需要点全清单,与服务端 + * `reviewApproveValue` 同一条判法:优先 value==='approve',否则退回推荐项、再退第一项)。 + * 没有 `checklist` 的卡(其余八套件)`checked` 不出现在载荷里,行为与从前一字不差。 */ async function answerDecisions(taskId, kase, answered) { const { body } = await api(`/api/desk/tasks/${taskId}`); @@ -934,14 +960,17 @@ async function answerDecisions(taskId, kase, answered) { const picked = custom ? { value: want, label: `意见:${want.slice(7)}` } : (options.find((o) => o.value === want) ?? options.find((o) => o.recommended === true) ?? options[0]); - await api('/api/desk/chat-cards/reply', { - method: 'POST', - body: JSON.stringify({ reply: { k: 'decision', cardId: card.id, value: picked.value, note: custom ? want.slice(7) : '试跑代答' } }), - }); + const approveValue = (options.find((o) => o.value === 'approve') ?? options.find((o) => o.recommended === true) ?? options[0])?.value; + const reply = { k: 'decision', cardId: card.id, value: picked.value, note: custom ? want.slice(7) : '试跑代答' }; + if (Array.isArray(card.checklist) && card.checklist.length > 0 && picked.value === approveValue) reply.checked = card.checklist; + await api('/api/desk/chat-cards/reply', { method: 'POST', body: JSON.stringify({ reply }) }); answered.add(card.id); asked.push(String(card.ask ?? card.title ?? '')); count += 1; - console.log(` 决策卡拍板:${card.ask?.slice(0, 40) ?? ''} → ${picked.label ?? picked.value}`); + console.log( + ` 决策卡拍板:${card.ask?.slice(0, 40) ?? ''} → ${picked.label ?? picked.value}` + + (reply.checked ? `(人工验收清单 ${reply.checked.length} 条已点全)` : ''), + ); } return count; } @@ -1197,6 +1226,10 @@ async function runCase(suite, kase) { let bailed = false; let receiptChecked = false; let receiptMismatch = false; + /** w05「都在库·零数据卡直分析」:任意一张匹配 stages 的执行卡出现过(不论此刻答没答)就算见过 */ + let dataCardSeen = false; + /** w05「类型不一致·硬拦」:`sub_eval.case.gate_reason` 命中过 `kase.expectGateReason` 那个词 */ + let gateReasonMatched = false; const deadline = Date.now() + 20 * 60 * 1000; let last; @@ -1265,6 +1298,15 @@ async function runCase(suite, kase) { (r) => String(r[where] ?? '') === equals && String(r[column] ?? '').includes(contains), ); } + // w05「都在库·零数据卡直分析」:S1 的 doneWhen 起手就成立,断言一张数据工单都没出现过 + if (kase.expectNoDataCard && !dataCardSeen) { + dataCardSeen = (body.cards ?? []).some((c) => (suite.stages ?? []).some((s) => s.match.test(c.title ?? ''))); + } + // w05「类型不一致·硬拦」:gate_reason 那句话要点了名(latch 住,答完卡草稿就清了,过了这一拍再查就晚了) + if (kase.expectGateReason && !gateReasonMatched) { + const caseRow = draftRows(body.drafts ?? [], 'sub_eval.case')[0]; + gateReasonMatched = caseRow !== undefined && kase.expectGateReason.test(String(caseRow['gate_reason'] ?? '')); + } // 决策卡挂在对话区,不在这一摞里 —— 每轮顺手看一眼有没有等着人点的 decisions += await answerDecisions(taskId, kase, decided); @@ -1402,11 +1444,19 @@ async function runCase(suite, kase) { : ` ⚠️ 确认屏上补的联系人行没落下(或编码没续上)`, ); } - await api('/api/desk/work-orders/reply', { - method: 'POST', - body: JSON.stringify({ reply: { mode: 'acceptance', workOrderId: card.id, verdict: 'pass', values: {} } }), - }); - console.log(` 验收通过:${card.title}`); + /* + * **人工验收清单**(同上,W05 的 T_release 终局验收卡):试跑观测面的 `cards` + * 摘要故意瘦身(`{id,mode,title,done,result}`,见 governance-ops.ts),没有 + * `checklist` 这一格 —— 要拿清单原文得走**任务详情**的 `workOrders`(与 `card` 同一个 + * `id`,那边走的是完整契约 `acceptanceWorkOrderSchema`)。没有清单的卡(其余套件) + * 这次多查的一趟落空,`checked` 不进载荷,行为不变。 + */ + const detail = await api(`/api/desk/tasks/${taskId}`); + const workOrder = (detail.body.workOrders ?? []).find((w) => w.id === card.id); + const acceptReply = { mode: 'acceptance', workOrderId: card.id, verdict: 'pass', values: {} }; + if (Array.isArray(workOrder?.checklist) && workOrder.checklist.length > 0) acceptReply.checked = workOrder.checklist; + await api('/api/desk/work-orders/reply', { method: 'POST', body: JSON.stringify({ reply: acceptReply }) }); + console.log(` 验收通过:${card.title}${acceptReply.checked ? `(人工验收清单 ${acceptReply.checked.length} 条已点全)` : ''}`); replied.add(card.id); } } @@ -1470,6 +1520,22 @@ async function runCase(suite, kase) { console.log(' ⚠️ 确认屏补录的联系人没拿到基座续的号 —— 判失败'); ok = false; } + if (kase.expectNoDataCard && dataCardSeen) { + console.log(' ⚠️ 这一例本该是零数据卡直分析,却见到了一张数据工单 —— 判失败'); + ok = false; + } + if (kase.expectGateReason && !gateReasonMatched) { + console.log(` ⚠️ gate_reason 没点名(期望命中 ${kase.expectGateReason})—— 判失败`); + ok = false; + } + if (kase.expectAnalyzeCalls !== undefined) { + const analyzeCalls = analyzeCallCount(last?.trail ?? []); + console.log(` 模型调用 ${analyzeCalls} 次`); + if (analyzeCalls !== kase.expectAnalyzeCalls) { + console.log(` ⚠️ 模型调用该恰 ${kase.expectAnalyzeCalls} 次,实际 ${analyzeCalls} 次 —— 判失败`); + ok = false; + } + } if (receiptMismatch) ok = false; if (forbidHit) { console.log(` ⚠️ 计划里出现了不该有的字样(${(kase.forbidInPlan ?? []).join('/')})—— 占位人名又回来了,判失败`);