feat(w05): 五条样例与 e2e 套件就位,包已装库并经 SQL 核对 —— 五案例尚未跑

Task 11 的 Step 1-3。装包结果用 SQL 核过库里那一行(不看脚本的话):
part-substitution-assessment@2.0.0 · enabled · steps 恰四步
[S1_inputs, S2_assess, S3_review, T_release] · 零处残留已退役的工具名 ·
declarations.tools=[] · 五条样例名与设计一致。

**Step 4(五案例 e2e)没跑,故本提交不声称任何跑通结果。**
卡在登录:KEYCLOAK_E2E_CLIENT_SECRET 与 KEYCLOAK_PROVISIONING_CLIENT_SECRET
指纹完全相同(已核),e2e 服务账号的 secret 从来没被真正配过,取 token 恒 401。
另一条路 JCAI_E2E_BEARER 解出来是真人的 token 且已过期 —— 试跑绝不派到用户
工作台,政策上不能用,技术上也用不了。

pnpm gates 退出码 0(2407 通过 / 34 跳过)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
xcs
2026-08-29 22:28:42 +08:00
co-authored by Claude Opus 5
parent 882f22a01e
commit a408542bed
3 changed files with 262 additions and 229 deletions
@@ -93,14 +93,21 @@ describe('清单本身合规', () => {
expect(launchProps).not.toContain('datasheets'); expect(launchProps).not.toContain('datasheets');
}); });
it('五条样例:第一条满载走数据工单那条路,名字 ≤14 字,形状各不相同', () => { /*
* 2026-08-29 Task 11:随新样例集换血(单数 `candidateInternalPn`,起单只剩两格)——
* 旧断言查的是已撤下的 `candidateInternalPns`(数组)/ `customerName`,这里同步核新形状:
* 第一条是「候选缺库」那条路(S1 会派数据工单),每条样例的入参恰是启动契约那两格,不多不少。
*/
it('五条样例:第一条候选缺库那条路,名字 ≤14 字,入参恰两格且各不相同', () => {
expect(manifest.samples).toHaveLength(5); expect(manifest.samples).toHaveLength(5);
for (const s of manifest.samples) expect(s.name.length, s.name).toBeLessThanOrEqual(14); 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); expect(new Set(manifest.samples.map((s) => JSON.stringify(s.input))).size).toBe(5);
const first = manifest.samples[0]?.input as { candidateInternalPns?: string[] }; const first = manifest.samples[0]?.input as { originalInternalPn?: string; candidateInternalPn?: string };
expect(first.candidateInternalPns).toContain('XL2596S-5.0'); expect(first.originalInternalPn).toBe('LM2596S-5.0');
const newCustomer = manifest.samples.find((s) => (s.input as { customerName?: string }).customerName !== undefined); expect(first.candidateInternalPn).toBe('XL2596S-5.0');
expect(newCustomer).toBeDefined(); for (const s of manifest.samples) {
expect(Object.keys(s.input as Record<string, unknown>).sort()).toEqual(['candidateInternalPn', 'originalInternalPn']);
}
}); });
/* /*
@@ -462,61 +462,21 @@ export const manifest: ActivityManifest = {
/* /*
* ── 样例集:五条形状真不同(常备案例即验收案例)── * ── 样例集:五条形状真不同(常备案例即验收案例)──
* 本任务不改样例(它们仍是旧九步 / 旧入参形状的素材,Task 3 重写案例模型时一并核对)。 * **2026-08-29 Task 11 换成新入参形状**(单数 `candidateInternalPn`,与 §4.1 的启动两参同步 ——
* 库里只播了 LM2596S-5.0 / LM2596S-ADJ 两颗(见 seed-mvp-data.ts),XL2596S-5.0 / CN-DCDC-32V / LM2596S-3.3 刻意不在库。 * 旧九步遗留的 `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: [ samples: [
{ { name: '候选缺库·新建类型', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'XL2596S-5.0' }, expect: { ports: ['assessment-released'] } },
name: '候选缺库·派数据工单', { name: '都在库·零卡直分析', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'LM2596S-ADJ' }, expect: { ports: ['assessment-released'] } },
input: { { name: '类型不一致·硬拦', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'AMS1117-3.3' }, expect: { ports: ['assessment-released'] } },
customerCode: 'CU-0142', { name: '原PN缺库', input: { originalInternalPn: 'LM2596S-3.3', candidateInternalPn: 'LM2596S-ADJ' }, expect: { ports: ['assessment-released'] } },
requestDescription: '工业电源项目在用 LM2596S-5.0(TI,12V→5V),原厂 PCN 转 NRND,客户要求验证替代料;应用 12V→5V、工业温域、输入上限 40V', { name: '两颗都缺库', input: { originalInternalPn: 'CN-DCDC-32V', candidateInternalPn: 'XL2596S-5.0' }, expect: { ports: ['assessment-released'] } },
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'] },
},
], ],
entry: { file: 'index.js', checksum: 'dev-entry-part-substitution-assessment' }, entry: { file: 'index.js', checksum: 'dev-entry-part-substitution-assessment' },
+237 -171
View File
@@ -53,17 +53,20 @@ const LAND_MASTER = process.env.JCAI_E2E_LAND_MASTER === '1';
const TOKEN_REFRESH_MS = 4 * 60 * 1000; const TOKEN_REFRESH_MS = 4 * 60 * 1000;
/** /**
* 标准化参数子表 `parts.parameter` 的行(w05 专用)—— 按 [参数名, 值, 单位, 约束, 说明] * 标准化参数子表 `parts.parameter` 的行(w05 专用)—— 按 [参数名, 值, 单位, 说明]
* 五元组批量转成保存载荷的行对象;数值照抄各案例原先写在 `normalized_parameter_set` * 四元组批量转成保存载荷的行对象。
* 那个文本列里的那几行,一个数都不改(2026-08-27 结构改:参数集拆成独立子表)。 *
* **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) { 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, internal_pn: internalPn,
param_name, param_name,
param_value, param_value,
param_unit, param_unit,
constraint_type,
param_note, param_note,
})); }));
} }
@@ -543,235 +546,238 @@ const SUITES = {
], ],
}, },
/* /*
* ACT-WORK-05 差异评估 —— 1 数据(条件,P-08 / P-10 各一张)+ 1 执行 + 2 验收(审核走决策卡循环、放行终局)。 * ACT-WORK-05 差异评估 —— **2026-08-29 Task 11 随四步重写整体换掉**(spec
* 库里只播了 LM2596S-5.0 / LM2596S-ADJ:XL2596S-5.0、CN-DCDC-32V、LM2596S-3.3 刻意不在, * `docs/superpowers/specs/2026-08-29-act-work-05-rewrite-design.md`)。旧套件是九步 /
* 「候选缺库 / 原 PN 缺库 / 硬约束不推荐」三条路靠它们。e2e 关落真库,主数据不落正式表,案例可反复跑。 * 两枚工具 / 三张数据工单(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: { w05: {
activityId: 'part-substitution-assessment', activityId: 'part-substitution-assessment',
address: 'sub_eval.input_gap', address: 'parts.profile',
blankCol: 'supplement', blankCol: 'brand_name',
stages: [ stages: [
{ {
// 这张卡摆两张表(表 1 主记录 + 表 2 标准化参数子表)—— 主表照旧,子表挂在 extraTables 里 // 唯一的数据工单:一张卡摆三块内容,写向四张表(主表 parts.profile,
// 加性表达(见 buildEdits 顶注),保存时两张表的行一次并进同一个 `tables` 载荷。 // extraTables 里另外三张)——四张表随同一次「保存」一并发出,不分次交。
match: /数据工单 · 芯片主数据创建/, match: /差异评估的输入还差几格/,
address: 'parts.profile', address: 'parts.profile',
rowKey: 'internal_pn', rowKey: 'internal_pn',
blankCol: 'brand_name', blankCol: 'brand_name',
expectGateBlock: true, expectGateBlock: true,
// 数据工单确认即入库(2026-08-27 用户定)—— 交卷那一刻这张卡的表就该进正式表
landsOnConfirm: true,
edits: [], edits: [],
// 案例没点名的料号兜底(不该走到这里 —— 每个案例都在 stageEdits 里逐颗给了)
fillAllBlank: {
brand_name: '兜底品牌',
specification_description: '兜底规格(案例没给参数集)',
package: 'TO-263-5',
lifecycle_status: 'active',
},
extraTables: [ 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 会永远命中第一条同名行、把不同料的值互相顶掉。 // 不这样区分,rows.find 会永远命中第一条同名行、把不同料的值互相顶掉。
address: 'parts.parameter', address: 'parts.parameter',
rowKey: ['internal_pn', 'param_name'], rowKey: ['internal_pn', 'param_name'],
blankCol: 'param_value', blankCol: 'param_value',
edits: [], 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: [ cases: [
{ {
sample: 0, sample: 0,
name: '候选缺库 · XL2596S-5.0 自动派数据工单(P-08 + P-10)· 审核先点名驳回再通过', name: '候选缺库 · XL2596S-5.0 新建芯片类型(type_id=3,字段清单沿用原型口径)入库,'
// 决策卡序(2026-08-27 起缺库不再问人,数据工单自动派):审核第一张点名意见驳回;第二张通过 + '原 PN 一并转挂新类型才够得上 distinct=1 · 审核先点名意见驳回再通过',
decideSeq: ['custom:使能阈值那条 UNKNOWN 的去向请写明验证任务号与测试条件', 'approve'], decideSeq: ['custom:使能阈值那条 UNKNOWN 的去向请写明验证任务号与测试条件', 'approve'],
expectDecision: true, expectDecision: true,
expectPort: 'assessment-released', expectPort: 'assessment-released',
// 全套只该有一次模型调用(S2 唯一一次 analyze)—— S3 审核循环走的是纯投影重算,不重问模型
expectAnalyzeCalls: 1,
stageEdits: { 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': [ 'parts.profile': [
// 原 PN 转挂新类型 —— distinct(chip_type_id) 要求两颗归同一个值,新建类型时
// 两颗都得落在它上面,不是只给候选料一颗挂新号。
{ internal_pn: 'LM2596S-5.0', chip_type_id: '3' },
{ {
internal_pn: 'XL2596S-5.0', internal_pn: 'XL2596S-5.0',
chip_type_id: '3',
brand_name: 'XLSEMI', brand_name: 'XLSEMI',
manufacturer_name: 'XLSEMI', manufacturer_name: 'XLSEMI',
specification_description: '降压型 DCDC 稳压器,3A,固定 5.0V,150kHz', specification_description: '降压型 DCDC 稳压器,3A,固定 5.0V,150kHz',
package: 'TO-263-5',
process_technology: 'BCD',
lifecycle_status: 'active', lifecycle_status: 'active',
datasheet_ref: 'https://www.xlsemi.com/datasheet/XL2596S.pdf', datasheet_ref: 'https://www.xlsemi.com/datasheet/XL2596S.pdf',
datasheet_version: 'Rev C', datasheet_version: 'Rev C',
application_scenarios: '工业电源;消费电源',
}, },
], ],
// LM2596S-5.0 的既有参数值按同名 lookup 自动带过来,不必在这里重填;
// 只需交代候选料 XL2596S-5.0 那一份。
'parts.parameter': paramRows('XL2596S-5.0', [ 'parts.parameter': paramRows('XL2596S-5.0', [
['输入电压范围', '4.5~40', 'V', 'hard', '客户上限 40V'], ['输入电压范围', '4.5~40', 'V', '客户上限 40V'],
['输出电压', '5.0', 'V', 'hard', '固定版'], ['输出电压', '5.0', 'V', '固定版'],
['输出电流', '3', 'A', 'hard', '满载能力'], ['输出电流', '3', 'A', '满载能力'],
['开关频率', '150', 'kHz', 'hard', '影响外围'], ['开关频率', '150', 'kHz', '影响外围'],
['静态电流', '4.2', 'mA', 'soft', '待机功耗'], ['静态电流', '4.2', 'mA', '待机功耗'],
['工作温度', '-40~+125', '℃', 'hard', '工业温域'], ['工作温度', '-40~+125', '℃', '工业温域'],
['使能阈值', '未披露', 'V', 'hard', '原厂未给'], ['使能阈值', '未披露', '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' }, expectRevised: { address: 'substitution.relation', where: 'candidate_internal_pn', equals: 'XL2596S-5.0', column: 'recommendation_level', contains: 'conditional' },
}, },
{ {
sample: 1, sample: 1,
name: '都在库 · 常路 · 可调版 → 备选 · 审核一次通过', name: '都在库 · 两颗同型且齐备 · 零数据卡直接进 S2 分析 · 审核一次通过',
// S1 的 doneWhen 起手就成立(两颗都在库、同型、参数不缺)—— 断言一张数据工单都不发
expectNoDataCard: true,
decide: 'approve', decide: 'approve',
expectPort: 'assessment-released', expectPort: 'assessment-released',
expectRevised: { address: 'substitution.relation', where: 'candidate_internal_pn', equals: 'LM2596S-ADJ', column: 'recommendation_level', contains: 'alternate' }, expectAnalyzeCalls: 1,
}, },
{ {
sample: 2, 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', decide: 'approve',
expectPort: 'assessment-released', expectPort: 'assessment-released',
expectAnalyzeCalls: 1,
},
{
sample: 3,
name: '原 PN 缺库(候选在库)· 自动派数据工单建档 · 审核通过',
decide: 'approve',
expectPort: 'assessment-released',
expectAnalyzeCalls: 1,
stageEdits: { stageEdits: {
'parts.profile': [ 'parts.profile': [
{ {
internal_pn: 'LM2596S-3.3', internal_pn: 'LM2596S-3.3',
chip_type_id: '1',
brand_name: 'TI', brand_name: 'TI',
manufacturer_name: 'Texas Instruments', manufacturer_name: 'Texas Instruments',
specification_description: '降压型 DCDC 稳压器,3A,固定 3.3V,150kHz', specification_description: '降压型 DCDC 稳压器,3A,固定 3.3V,150kHz',
package: 'TO-263-5',
process_technology: 'BCD',
lifecycle_status: 'nrnd', lifecycle_status: 'nrnd',
datasheet_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf', datasheet_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf',
datasheet_version: 'Rev H (2023-09)', datasheet_version: 'Rev H (2023-09)',
}, },
], ],
'parts.parameter': paramRows('LM2596S-3.3', [ 'parts.parameter': paramRows('LM2596S-3.3', [
['输入电压范围', '4.5~40', 'V', 'hard', '客户上限 40V'], ['输入电压范围', '4.5~40', 'V', '客户上限 40V'],
['输出电压', '3.3', 'V', 'hard', '固定版'], ['输出电压', '3.3', 'V', '固定版'],
['输出电流', '3', 'A', 'hard', '满载能力'], ['输出电流', '3', 'A', '满载能力'],
['开关频率', '150', 'kHz', 'hard', '影响外围'], ['开关频率', '150', 'kHz', '影响外围'],
['静态电流', '5', 'mA', 'soft', '待机功耗'], ['静态电流', '5', 'mA', '待机功耗'],
['工作温度', '-40~+85', '℃', 'hard', '工业温域'], ['工作温度', '-40~+85', '℃', '工业温域'],
['使能阈值', '1.4', 'V', 'hard', 'ON/OFF 阈值'], ['使能阈值', '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, sample: 4,
name: '新客户 · 录入卡先建档 · 审核通过', name: '两颗都缺库 · 自动派数据工单各自建档(原 PN 32V 上限 · 候选 40V 盖住它)· 审核通过',
entryValues: { name: '深圳恒星电源有限公司', industry: '工业电源', region: '广东' },
decide: 'approve', decide: 'approve',
expectPort: 'assessment-released', 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; 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) { function draftRows(drafts, address) {
return (drafts ?? []).filter((d) => d.address === address).map((d) => ({ ...d.key, ...d.patch })); return (drafts ?? []).filter((d) => d.address === address).map((d) => ({ ...d.key, ...d.patch }));
@@ -916,6 +934,14 @@ async function expectGateBlocks(cardId, taskId, stage) {
* *
* 选项按案例声明的 value 认;认不上退「推荐项 → 第一项」—— * 选项按案例声明的 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) { async function answerDecisions(taskId, kase, answered) {
const { body } = await api(`/api/desk/tasks/${taskId}`); const { body } = await api(`/api/desk/tasks/${taskId}`);
@@ -934,14 +960,17 @@ async function answerDecisions(taskId, kase, answered) {
const picked = custom const picked = custom
? { value: want, label: `意见:${want.slice(7)}` } ? { value: want, label: `意见:${want.slice(7)}` }
: (options.find((o) => o.value === want) ?? options.find((o) => o.recommended === true) ?? options[0]); : (options.find((o) => o.value === want) ?? options.find((o) => o.recommended === true) ?? options[0]);
await api('/api/desk/chat-cards/reply', { const approveValue = (options.find((o) => o.value === 'approve') ?? options.find((o) => o.recommended === true) ?? options[0])?.value;
method: 'POST', const reply = { k: 'decision', cardId: card.id, value: picked.value, note: custom ? want.slice(7) : '试跑代答' };
body: JSON.stringify({ 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); answered.add(card.id);
asked.push(String(card.ask ?? card.title ?? '')); asked.push(String(card.ask ?? card.title ?? ''));
count += 1; 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; return count;
} }
@@ -1197,6 +1226,10 @@ async function runCase(suite, kase) {
let bailed = false; let bailed = false;
let receiptChecked = false; let receiptChecked = false;
let receiptMismatch = 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; const deadline = Date.now() + 20 * 60 * 1000;
let last; let last;
@@ -1265,6 +1298,15 @@ async function runCase(suite, kase) {
(r) => String(r[where] ?? '') === equals && String(r[column] ?? '').includes(contains), (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); decisions += await answerDecisions(taskId, kase, decided);
@@ -1402,11 +1444,19 @@ async function runCase(suite, kase) {
: ` ⚠️ 确认屏上补的联系人行没落下(或编码没续上)`, : ` ⚠️ 确认屏上补的联系人行没落下(或编码没续上)`,
); );
} }
await api('/api/desk/work-orders/reply', { /*
method: 'POST', * **人工验收清单**(同上,W05 的 T_release 终局验收卡):试跑观测面的 `cards`
body: JSON.stringify({ reply: { mode: 'acceptance', workOrderId: card.id, verdict: 'pass', values: {} } }), * 摘要故意瘦身(`{id,mode,title,done,result}`,见 governance-ops.ts),没有
}); * `checklist` 这一格 —— 要拿清单原文得走**任务详情**的 `workOrders`(与 `card` 同一个
console.log(` 验收通过:${card.title}`); * `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); replied.add(card.id);
} }
} }
@@ -1470,6 +1520,22 @@ async function runCase(suite, kase) {
console.log(' ⚠️ 确认屏补录的联系人没拿到基座续的号 —— 判失败'); console.log(' ⚠️ 确认屏补录的联系人没拿到基座续的号 —— 判失败');
ok = false; 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 (receiptMismatch) ok = false;
if (forbidHit) { if (forbidHit) {
console.log(` ⚠️ 计划里出现了不该有的字样(${(kase.forbidInPlan ?? []).join('/')})—— 占位人名又回来了,判失败`); console.log(` ⚠️ 计划里出现了不该有的字样(${(kase.forbidInPlan ?? []).join('/')})—— 占位人名又回来了,判失败`);