From 63034f5ab2957112a4a29cbeb14fddcc92d8aa86 Mon Sep 17 00:00:00 2001 From: xcs Date: Wed, 26 Aug 2026 23:10:08 +0800 Subject: [PATCH 01/21] =?UTF-8?q?feat(w05):=20=E4=B8=BB=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=9F=BA=E7=BA=BF=E5=8A=A0=E6=96=99=E4=BB=B6/=E9=9C=80?= =?UTF-8?q?=E6=B1=82/=E6=9B=BF=E4=BB=A3/=E9=AA=8C=E8=AF=81=E5=9B=9B?= =?UTF-8?q?=E5=9F=9F=E5=85=AD=E8=A1=A8(P-08/P-10/P-01/P-11/P-06)=E4=B8=8E?= =?UTF-8?q?=E6=A0=B7=E4=BE=8B=E6=96=99=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh --- .../src/app/__tests__/seed-mvp-data.test.ts | 25 ++- apps/api/src/app/seed-activities.ts | 2 +- apps/api/src/app/seed-mvp-data.ts | 212 +++++++++++++++++- 3 files changed, 230 insertions(+), 9 deletions(-) diff --git a/apps/api/src/app/__tests__/seed-mvp-data.test.ts b/apps/api/src/app/__tests__/seed-mvp-data.test.ts index c854677..1b326d0 100644 --- a/apps/api/src/app/__tests__/seed-mvp-data.test.ts +++ b/apps/api/src/app/__tests__/seed-mvp-data.test.ts @@ -6,19 +6,36 @@ * 2026-07-31 重分:单据归各活动自己的域,这个文件只留**主数据**,按常规 CRM / ERP 分四域。 * * 守的是三条,每条都能机检: - * · 只有这四个域,不多不少 —— 多一个就说明有人把单据塞进主数据了; + * · 八个域,多一个就说明有人把单据塞进主数据了; * · 每张表都是 `master` 且**不带单标识** —— 带了就是单据归错类(安装静态审也会拦); * · 主键是**业务实体**,不是单。 */ import { describe, expect, it } from 'vitest'; -import { MASTER_DOMAINS, MASTER_OBJECTS } from '../seed-mvp-data.ts'; +import { MASTER_DOMAINS, MASTER_OBJECTS, MASTER_SAMPLE_ROWS } from '../seed-mvp-data.ts'; describe('主数据四域', () => { - it('只有客户 / 项目 / 供应商 / 市场四个域', () => { - expect([...MASTER_DOMAINS]).toEqual(['customer', 'project', 'supplier', 'market']); + it('八个域:客户 / 项目 / 供应商 / 市场 + 料件 / 需求 / 替代 / 验证(SS02 差异评估 2026-08-26 加后四域)', () => { + expect([...MASTER_DOMAINS]).toEqual(['customer', 'project', 'supplier', 'market', 'parts', 'demand', 'substitution', 'validation']); expect([...new Set(MASTER_OBJECTS.map((o) => o.domain))].sort()).toEqual([...MASTER_DOMAINS].sort()); }); + it('SS02 差异评估要读写的六张主数据表都在基线里,主键照《统一方案》', () => { + const pk = (domain: string, object: string) => + MASTER_OBJECTS.find((o) => o.domain === domain && o.object === object)?.fields.filter((f) => f.primaryKey).map((f) => f.name); + expect(pk('parts', 'profile')).toEqual(['internal_pn']); + expect(pk('parts', 'document')).toEqual(['internal_pn', 'doc_seq']); + expect(pk('demand', 'request')).toEqual(['request_id']); + expect(pk('substitution', 'relation')).toEqual(['substitution_relation_id']); + expect(pk('substitution', 'difference_item')).toEqual(['substitution_relation_id', 'difference_item_id']); + expect(pk('validation', 'record')).toEqual(['validation_id']); + }); + + it('样例料号:原 PN 与同族可调版在库,候选 XL2596S-5.0 刻意不播(数据工单那条路要试得出来)', () => { + const parts = MASTER_SAMPLE_ROWS.filter((r) => r.address.domain === 'parts' && r.address.object === 'profile').map((r) => r.key['internal_pn']); + expect(parts).toEqual(['LM2596S-5.0', 'LM2596S-ADJ']); + expect(parts).not.toContain('XL2596S-5.0'); + }); + it('四个域各自都有表 —— 空占位也得立得住', () => { for (const d of MASTER_DOMAINS) { expect(MASTER_OBJECTS.filter((o) => o.domain === d).length, `${d} 域一张表都没有`).toBeGreaterThan(0); diff --git a/apps/api/src/app/seed-activities.ts b/apps/api/src/app/seed-activities.ts index 5d6a417..03e9dab 100644 --- a/apps/api/src/app/seed-activities.ts +++ b/apps/api/src/app/seed-activities.ts @@ -140,5 +140,5 @@ export async function installRealActivities(container: Container): Promise cause: { kind: 'governance', ref: 'seed:master-sample' }, }); } - log(` 样例客户已就位:${MASTER_SAMPLE_ROWS[1]?.values.email ?? ''}(样例 2 用它试老客户那条路)`); + log(` 样例主数据已就位:${MASTER_SAMPLE_ROWS.length} 行(客户 / 项目 / 联系人 / 料号)`); } diff --git a/apps/api/src/app/seed-mvp-data.ts b/apps/api/src/app/seed-mvp-data.ts index 249fe89..5bb8d2a 100644 --- a/apps/api/src/app/seed-mvp-data.ts +++ b/apps/api/src/app/seed-mvp-data.ts @@ -19,7 +19,7 @@ * 表建出来但一行数据都没有 —— 治理台「主数据」那一区能看见它们等着被接上。 */ import { objectDeclarationSchema } from '@contracts'; -import type { LedgerFieldType, ObjectDeclaration } from '@contracts'; +import type { LedgerAddress, LedgerFieldType, LedgerKey, LedgerRow, ObjectDeclaration } from '@contracts'; const f = (name: string, type: LedgerFieldType, label: string, primaryKey = false) => ({ name, @@ -142,10 +142,127 @@ export const MASTER_OBJECTS: readonly ObjectDeclaration[] = [ f('stock', 'integer', '可见库存'), f('trend', 'text', '走势'), ]), + + /* ── 料件:P-08 芯片主数据 / P-10 原厂技术资料库(SS02 差异评估 2026-08-26 加)── + 字段名与《SS02-SS03-SS04_Demo台账字段细化与统一方案》§4.2 一字不差;全链路只认 internal_pn。 + 参数集是 P-08 的一个字段(用户定:一张表,不建子表),一行一参数: + `参数名 | 值 | 单位 | hard/soft | 说明`。老的 market.part(主键 pn)不复用,记 debt 待并。 */ + master('parts', 'profile', '芯片主数据(P-08)', [ + f('internal_pn', 'text', '内部料号', true), + f('customer_pn', 'text', '客户料号'), + f('brand_name', 'text', '品牌名称'), + f('manufacturer_name', 'text', '制造商名称'), + f('specification_description', 'text', '规格描述'), + f('normalized_parameter_set', 'text', '标准化参数集(一行一参数:参数名 | 值 | 单位 | hard/soft | 说明)'), + f('package', 'text', '封装'), + f('process_technology', 'text', '工艺'), + f('lifecycle_status', 'text', '生命周期状态'), + f('eol_pcn_status', 'text', 'EOL/PCN 状态'), + f('datasheet_ref', 'text', 'Datasheet 引用'), + f('datasheet_version', 'text', 'Datasheet 版本'), + f('application_scenarios', 'text', '适配应用场景'), + f('pin_to_pin_relation_ref', 'text', 'Pin-to-Pin 关系引用'), + ]), + master('parts', 'document', '原厂技术资料库(P-10)', [ + f('internal_pn', 'text', '关联内部料号', true), + f('doc_seq', 'text', '资料序号(同一料号下 01/02…)', true), + f('document_type', 'text', '资料类型'), + f('brand_name', 'text', '品牌名称'), + f('document_title', 'text', '资料标题'), + f('document_version', 'text', '资料版本'), + f('document_date', 'date', '资料发布日期'), + f('document_ref', 'text', '资料文件链接'), + f('citation_locator', 'text', '证据定位(页码/章节/表格)'), + f('source_contact_ref', 'text', '原厂支持联系人'), + f('document_status', 'text', '资料状态'), + ]), + + /* ── 需求:P-01 客户具体需求与项目任务主台账 ── */ + master('demand', 'request', '客户需求主记录(P-01)', [ + f('request_id', 'text', '需求编号', true), + f('customer_code', 'text', '客户编码'), + f('project_code', 'text', '项目编码'), + f('requirement_version', 'text', '需求版本'), + f('request_type', 'text', '需求类型'), + f('request_description', 'text', '需求描述'), + f('input_snapshot_ref', 'text', '输入快照'), + f('bom_id', 'text', 'BOM ID'), + f('bom_position', 'text', 'BOM 位'), + f('original_internal_pn', 'text', '原内部料号'), + f('customer_pn', 'text', '客户料号'), + f('application_scenario', 'text', '应用场景'), + f('technical_constraints', 'text', '技术约束'), + f('risk_level', 'text', '风险等级'), + f('verification_requirement', 'text', '验证/复测/确认要求'), + f('request_status', 'text', '需求状态'), + f('current_activity', 'text', '当前 Activity'), + f('current_stage', 'text', '当前阶段'), + f('next_action', 'text', '下一步动作'), + f('output_ref', 'text', '标准输出链接'), + ]), + + /* ── 替代:P-11 替代选型与批准关系库 —— 主记录 + 差异明细子表(《统一方案》§六:重复明细用子表)── */ + master('substitution', 'relation', '替代关系主记录(P-11)', [ + f('substitution_relation_id', 'text', '替代关系ID', true), + f('request_id', 'text', '关联需求编号'), + f('project_code', 'text', '关联项目编码'), + f('bom_position', 'text', '关联 BOM 位'), + f('original_internal_pn', 'text', '原内部料号'), + f('candidate_internal_pn', 'text', '候选内部料号'), + f('substitution_type', 'text', '替代类型'), + f('recommendation_level', 'text', '推荐等级'), + f('recommendation_reason', 'text', '推荐理由'), + f('difference_summary', 'text', '差异摘要'), + f('risk_level', 'text', '替代风险等级'), + f('verification_requirement', 'text', '验证要求'), + f('validation_id', 'text', '关联验证ID'), + f('analysis_status', 'text', '分析状态'), + /* 客户批准三列由独立审批流维护 —— 基线建列,差异评估活动**不声明写权** */ + f('customer_approval_status', 'text', '客户批准状态'), + f('customer_approval_at', 'timestamp', '客户批准时间'), + f('customer_approval_ref', 'text', '客户批准凭证'), + f('customer_feedback', 'text', '客户反馈'), + f('analysis_report_ref', 'text', '替代分析报告'), + f('confidence_score', 'number', '证据可信度'), + f('rule_version', 'text', '差异规则版本'), + ]), + master('substitution', 'difference_item', '差异明细(P-11 子表)', [ + f('substitution_relation_id', 'text', '替代关系ID', true), + f('difference_item_id', 'text', '差异明细ID', true), + f('parameter_name', 'text', '标准参数名'), + f('original_value', 'text', '原物料参数值'), + f('candidate_value', 'text', '候选物料参数值'), + f('parameter_unit', 'text', '参数单位'), + f('constraint_type', 'text', '约束类型'), + f('difference_status', 'text', '差异状态'), + f('impact_statement', 'text', '影响说明'), + f('risk_level', 'text', '风险等级'), + f('evidence_ref', 'text', '差异证据引用'), + f('confidence', 'number', 'AI 置信度'), + f('rule_version', 'text', '差异规则版本'), + ]), + + /* ── 验证:P-06 样品交付与芯片验证记录(事实类:只能实测产生,数据工单不许建)── */ + master('validation', 'record', '芯片验证记录(P-06)', [ + f('validation_id', 'text', '验证ID', true), + f('request_id', 'text', '关联需求编号'), + f('customer_code', 'text', '客户编码'), + f('validation_internal_pn', 'text', '验证内部料号'), + f('validation_type', 'text', '验证类型'), + f('test_condition', 'text', '测试条件'), + f('test_parameters', 'text', '测试参数'), + f('test_result', 'text', '测试结果'), + f('validation_status', 'text', '验证状态'), + f('issue_risk', 'text', '问题与风险'), + f('sample_version', 'text', '样品版本'), + f('sample_lot', 'text', '样品批次'), + f('evidence_ref', 'text', '验证证据引用'), + f('next_validation_action', 'text', '后续验证动作'), + ]), ]; -/** 四域的呈现次序 —— 谁在买 → 为什么买 → 从哪买 → 买什么 */ -export const MASTER_DOMAINS = ['customer', 'project', 'supplier', 'market'] as const; +/** 八域的呈现次序 —— 谁在买 → 为什么买 → 从哪买 → 买什么 → 料件 → 需求 → 替代 → 验证 */ +export const MASTER_DOMAINS = ['customer', 'project', 'supplier', 'market', 'parts', 'demand', 'substitution', 'validation'] as const; /** * **样例客户** —— 让「老客户」那条路试得出来。 @@ -158,7 +275,7 @@ export const MASTER_DOMAINS = ['customer', 'project', 'supplier', 'market'] as c * 只播这一位,且是**幂等的按主键写**:它是让样例跑得起来的最小夹具, * 不是往主数据里塞演示数据 —— 真实客户由 A1 建档时一位一位落进来。 */ -export const MASTER_SAMPLE_ROWS = [ +export const MASTER_SAMPLE_ROWS: readonly { address: LedgerAddress; key: LedgerKey; values: LedgerRow }[] = [ /* * **Itelma** —— 老客户测试案例的那一位(照 `JCAI_PRD/docs/活动执行页/` 两份目标件)。 * 俄罗斯车规电子、项目制;客户码 CU-0142 就是需求件上写的「已命中客户资料库」那一条。 @@ -344,4 +461,91 @@ export const MASTER_SAMPLE_ROWS = [ review_note: '', }, }, + /* + * ── SS02 差异评估的样例料号(2026-08-26)── + * 只播**原 PN**与**同族可调版**两颗:「都在库」那条路靠它们;候选 XL2596S-5.0 与国产 DCDC + * **刻意不播** —— 「候选缺库 → 派数据工单」「硬约束不推荐」两条路要试得出来,库里得真没有。 + * 参数集一行一参数(参数名 | 值 | 单位 | hard/soft | 说明),与 P-08 声明的行语法同一份。 + */ + { + address: { domain: 'parts', object: 'profile' }, + key: { internal_pn: 'LM2596S-5.0' }, + values: { + internal_pn: 'LM2596S-5.0', + brand_name: 'TI', + manufacturer_name: 'Texas Instruments', + specification_description: '降压型 DCDC 稳压器,3A,固定 5.0V,150kHz', + normalized_parameter_set: [ + '输入电压范围 | 4.5~40 | V | hard | 客户输入上限 40V', + '输出电压 | 5.0 | V | hard | 固定版', + '输出电流 | 3 | A | hard | 满载能力', + '开关频率 | 150 | kHz | hard | 影响外围电感/电容', + '静态电流 | 5 | mA | soft | 待机功耗', + '工作温度 | -40~+85 | ℃ | hard | 工业温域', + '使能阈值 | 1.4 | V | hard | ON/OFF 引脚阈值', + ].join('\n'), + package: 'TO-263-5', + process_technology: 'BCD', + lifecycle_status: 'nrnd', + eol_pcn_status: 'PCN-2026-07:转 NRND,2027-01 起不接新设计', + datasheet_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf', + datasheet_version: 'Rev H (2023-09)', + application_scenarios: '工业电源;消费电源', + }, + }, + { + address: { domain: 'parts', object: 'profile' }, + key: { internal_pn: 'LM2596S-ADJ' }, + values: { + internal_pn: 'LM2596S-ADJ', + brand_name: 'TI', + manufacturer_name: 'Texas Instruments', + specification_description: '降压型 DCDC 稳压器,3A,可调输出 1.23~37V,150kHz(LM2596 同族)', + normalized_parameter_set: [ + '输入电压范围 | 4.5~40 | V | hard | 与固定版同', + '输出电压 | 1.23~37 | V | hard | 可调版,靠外围反馈网络定', + '输出电流 | 3 | A | hard | 满载能力', + '开关频率 | 150 | kHz | hard | 影响外围电感/电容', + '静态电流 | 5 | mA | soft | 待机功耗', + '工作温度 | -40~+85 | ℃ | hard | 工业温域', + '使能阈值 | 1.4 | V | hard | ON/OFF 引脚阈值', + ].join('\n'), + package: 'TO-263-5', + process_technology: 'BCD', + lifecycle_status: 'active', + datasheet_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf', + datasheet_version: 'Rev H (2023-09)', + application_scenarios: '工业电源;消费电源', + }, + }, + { + address: { domain: 'parts', object: 'document' }, + key: { internal_pn: 'LM2596S-5.0', doc_seq: '01' }, + values: { + internal_pn: 'LM2596S-5.0', + doc_seq: '01', + document_type: 'datasheet', + brand_name: 'TI', + document_title: 'LM2596 SIMPLE SWITCHER Power Converter 150-kHz 3-A Step-Down Voltage Regulator', + document_version: 'Rev H (2023-09)', + document_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf', + citation_locator: '§6.5 Electrical Characteristics 表 / §6.3 Recommended Operating Conditions', + document_status: 'valid', + }, + }, + { + address: { domain: 'parts', object: 'document' }, + key: { internal_pn: 'LM2596S-ADJ', doc_seq: '01' }, + values: { + internal_pn: 'LM2596S-ADJ', + doc_seq: '01', + document_type: 'datasheet', + brand_name: 'TI', + document_title: 'LM2596 SIMPLE SWITCHER Power Converter 150-kHz 3-A Step-Down Voltage Regulator', + document_version: 'Rev H (2023-09)', + document_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf', + citation_locator: '§6.5 Electrical Characteristics 表(ADJ 列)', + document_status: 'valid', + }, + }, ] as const; From f52f0168df062f1e3874f40f5fef922b72244b0b Mon Sep 17 00:00:00 2001 From: xcs Date: Wed, 26 Aug 2026 23:17:43 +0800 Subject: [PATCH 02/21] =?UTF-8?q?feat(w05):=20=E5=B7=AE=E5=BC=82=E8=AF=84?= =?UTF-8?q?=E4=BC=B0=E4=B8=A4=E6=9E=9A=E7=BA=AF=E5=B7=A5=E5=85=B7=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20=E9=80=90=E5=8F=82=E6=95=B0=E5=9B=9B?= =?UTF-8?q?=E6=80=81=E6=AF=94=E5=AF=B9=E4=B8=8E=E6=8E=A8=E8=8D=90=E5=AE=9A?= =?UTF-8?q?=E7=BA=A7(R-2026.07)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh --- .../first-party-substitution.test.ts | 129 +++++++++++++++ apps/api/src/app/seed-mvp.ts | 5 +- apps/api/src/app/tools-w05.ts | 87 ++++++++++ apps/api/src/app/tools/first-party.ts | 150 ++++++++++++++++++ 4 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 apps/api/src/app/__tests__/first-party-substitution.test.ts create mode 100644 apps/api/src/app/tools-w05.ts diff --git a/apps/api/src/app/__tests__/first-party-substitution.test.ts b/apps/api/src/app/__tests__/first-party-substitution.test.ts new file mode 100644 index 0000000..86bebe3 --- /dev/null +++ b/apps/api/src/app/__tests__/first-party-substitution.test.ts @@ -0,0 +1,129 @@ +/** + * ACT-WORK-05 差异评估的两枚纯工具 —— 「能算的不问 LLM」:同输入必同结论。 + * 用例逐条对应《ACT-WORK-05-活动流程与工单说明》§八的示例矩阵: + * A XL2596S-5.0 条件推荐(使能阈值未披露 → UNKNOWN)/ B LM2596S-ADJ 备选(可调版靠外围配成 5V)/ + * C 国产 DCDC 不推荐(输入上限 32V < 客户 40V,硬约束 MISMATCH)。 + */ +import { describe, expect, it } from 'vitest'; +import { FIRST_PARTY_TOOLS } from '../tools/first-party.ts'; + +const handler = (name: string) => { + const found = FIRST_PARTY_TOOLS.find(([n]) => n === name); + if (found === undefined) throw new Error(`没有工具 ${name}`); + return found[1]; +}; +/** ToolHandler 的入参形状见 `apps/api/src/modules/toolbox/internal/adapter/toolbox-store.ts`:纯工具不用连接目标 */ +const call = async (name: string, input: Record) => { + const out = await handler(name)({ input, connection: {}, operation: undefined, idempotencyKey: undefined }); + if (!out.ok) throw new Error(`工具失败:${JSON.stringify(out)}`); + return out.output as Record; +}; + +type Item = { parameterName: string; compareResult: string; differenceStatus: string; riskLevel: string }; +type Result = { candidateInternalPn: string; items: Item[]; counts: Record }; + +const pair = (parameterName: string, originalValue: string, candidateValue: string, constraintType: 'hard' | 'soft', extra: Record = {}) => ({ + parameterName, + originalValue, + candidateValue, + constraintType, + evidenceRef: 'P-08 · P-10 Rev H', + ...extra, +}); + +describe('compare-parameters:逐参数四态', () => { + it('候选温域更宽 → covers → MATCH 低风险;固定 vs 可调 → configurable → CONDITIONAL;未披露 → UNKNOWN', async () => { + const out = await call('part-substitution-assessment.compare-parameters', { + candidates: [ + { + candidateInternalPn: 'XL2596S-5.0', + pairs: [ + pair('工作温度', '-40~+85', '-40~+125', 'hard', { unit: '℃' }), + pair('输出电压', '5.0', '5.0', 'hard', { unit: 'V' }), + pair('使能阈值', '1.4', '未披露', 'hard', { unit: 'V' }), + pair('静态电流', '5', '4.2', 'soft', { unit: 'mA' }), + ], + }, + { + candidateInternalPn: 'LM2596S-ADJ', + pairs: [pair('输出电压', '5.0', '1.23~37', 'hard', { unit: 'V' })], + }, + ], + }); + const results = out['results'] as Result[]; + const a = results[0]!; + const b = results[1]!; + expect(out['ruleVersion']).toBe('R-2026.07'); + const by = (r: Result, name: string) => r.items.find((i) => i.parameterName === name); + expect(by(a, '工作温度')).toMatchObject({ compareResult: 'covers', differenceStatus: 'match', riskLevel: 'low' }); + expect(by(a, '输出电压')).toMatchObject({ compareResult: 'equal', differenceStatus: 'match' }); + expect(by(a, '使能阈值')).toMatchObject({ compareResult: 'unknown', differenceStatus: 'unknown', riskLevel: 'medium' }); + expect(by(a, '静态电流')).toMatchObject({ differenceStatus: 'conditional' }); // 软约束数值不同 → 条件 + expect(a.counts).toMatchObject({ match: 2, unknown: 1, conditional: 1, mismatch: 0, hardMismatch: 0 }); + expect(by(b, '输出电压')).toMatchObject({ compareResult: 'configurable', differenceStatus: 'conditional' }); + expect(b.counts['configurable']).toBe(1); + }); + + it('硬约束范围变窄 → MISMATCH 高风险;客户约束(requirement)不满足也是 MISMATCH', async () => { + const out = await call('part-substitution-assessment.compare-parameters', { + candidates: [ + { + candidateInternalPn: 'CN-DCDC-32V', + pairs: [ + pair('输入电压范围', '4.5~40', '4.5~32', 'hard', { unit: 'V', requirement: '>= 40' }), + pair('封装', 'TO-263-5', 'TO-263-5', 'hard'), + ], + }, + ], + }); + const c = (out['results'] as Result[])[0]!; + expect(c.items[0]).toMatchObject({ compareResult: 'narrower', differenceStatus: 'mismatch', riskLevel: 'high' }); + expect(c.items[1]).toMatchObject({ compareResult: 'equal', differenceStatus: 'match' }); + expect(c.counts).toMatchObject({ mismatch: 1, hardMismatch: 1, match: 1 }); + }); + + it('硬约束相等但没有证据引用 → 不判 MATCH,降为 CONDITIONAL(硬约束项未经证据支持不得判 MATCH)', async () => { + const out = await call('part-substitution-assessment.compare-parameters', { + candidates: [{ candidateInternalPn: 'X', pairs: [{ parameterName: '封装', originalValue: 'TO-263-5', candidateValue: 'TO-263-5', constraintType: 'hard' }] }], + }); + const x = (out['results'] as Result[])[0]!; + expect(x.items[0]).toMatchObject({ compareResult: 'equal', differenceStatus: 'conditional' }); + }); + + it('数值带单位/括号注记也认得:「5.0(固定)」「3A」「150 kHz」;生命周期按活跃度比', async () => { + const out = await call('part-substitution-assessment.compare-parameters', { + candidates: [{ candidateInternalPn: 'X', pairs: [pair('输出电压', '5.0(固定)', '5.0 V', 'hard'), pair('输出电流', '3A', '3', 'hard'), pair('开关频率', '150 kHz', '150', 'hard'), pair('生命周期', 'nrnd', 'active', 'soft')] }], + }); + const x = (out['results'] as Result[])[0]!; + // 生命周期不是数值:候选比原料更活(nrnd → active)算 covers → MATCH,替 NRND 料本就是为了这个 + expect(x.items.map((i) => i.differenceStatus)).toEqual(['match', 'match', 'match', 'match']); + expect(x.items[3]?.compareResult).toBe('covers'); + }); +}); + +describe('recommend:推荐等级四档', () => { + it('硬约束 MISMATCH → 不推荐;有 UNKNOWN → 条件推荐;只有 configurable → 备选;全 MATCH → 推荐', async () => { + const out = await call('part-substitution-assessment.recommend', { + candidates: [ + { candidateInternalPn: 'C', counts: { match: 5, mismatch: 1, unknown: 0, conditional: 0, hardMismatch: 1, softMismatch: 0, configurable: 0 } }, + { candidateInternalPn: 'A', counts: { match: 5, mismatch: 0, unknown: 1, conditional: 0, hardMismatch: 0, softMismatch: 0, configurable: 0 } }, + { candidateInternalPn: 'B', counts: { match: 6, mismatch: 0, unknown: 0, conditional: 1, hardMismatch: 0, softMismatch: 0, configurable: 1 } }, + { candidateInternalPn: 'D', counts: { match: 7, mismatch: 0, unknown: 0, conditional: 0, hardMismatch: 0, softMismatch: 0, configurable: 0 } }, + ], + }); + const results = out['results'] as Array<{ candidateInternalPn: string; recommendationLevel: string; riskLevel: string }>; + expect(results.map((r) => [r.candidateInternalPn, r.recommendationLevel, r.riskLevel])).toEqual([ + ['C', 'not_recommended', 'high'], + ['A', 'conditional', 'medium'], + ['B', 'alternate', 'medium'], + ['D', 'recommended', 'low'], + ]); + }); + + it('既有 UNKNOWN 又有 configurable:UNKNOWN 优先 → 条件推荐(验证闭环前不给备选)', async () => { + const out = await call('part-substitution-assessment.recommend', { + candidates: [{ candidateInternalPn: 'E', counts: { match: 1, mismatch: 0, unknown: 1, conditional: 1, hardMismatch: 0, softMismatch: 0, configurable: 1 } }], + }); + expect((out['results'] as Array<{ recommendationLevel: string }>)[0]?.recommendationLevel).toBe('conditional'); + }); +}); diff --git a/apps/api/src/app/seed-mvp.ts b/apps/api/src/app/seed-mvp.ts index cad8758..fc6d5d4 100644 --- a/apps/api/src/app/seed-mvp.ts +++ b/apps/api/src/app/seed-mvp.ts @@ -12,6 +12,7 @@ import type { Container } from './container.ts'; import { installRealActivities } from './seed-activities.ts'; import { installSlots } from './seed-mvp-slots.ts'; import { seedMvpFlow } from './seed-mvp-flow.ts'; +import { W05_TOOLS } from './tools-w05.ts'; /** 第一方功能工具:查历史价 —— MVP 用录制回放替身代真 CRM(README:mock 只在外界) */ const HISTORY_TOOL = { @@ -328,7 +329,7 @@ export async function seedMvp(container: Container): Promise { log(`① LLM 实例已绑定:deepseek-v4-flash · deepseek-v4-pro${apiKey === undefined ? '(环境无密钥 —— 两条都是未绑定,去治理台补)' : ''}`); /* ② 功能工具注册两审 + 逐槽位白名单 */ - for (const contract of [HISTORY_TOOL, MAIL_TOOL, CLASSIFY_TOOL, PLACEHOLDER_TOOL, COVERAGE_SCORE_TOOL, RANK_KEY_PERSONS_TOOL]) { + for (const contract of [HISTORY_TOOL, MAIL_TOOL, CLASSIFY_TOOL, PLACEHOLDER_TOOL, COVERAGE_SCORE_TOOL, RANK_KEY_PERSONS_TOOL, ...W05_TOOLS]) { const verdict = await container.toolbox.register({ contract, samples: [ @@ -359,7 +360,7 @@ export async function seedMvp(container: Container): Promise { for (const [activityId, tools] of Object.entries(ALLOWLIST)) { await container.toolbox.setAllowlist(activityId, tools); } - log('② 功能工具已注册并入白名单(读类 1 · 写类 1 · 纯工具 4,逐活动授权)'); + log('② 功能工具已注册并入白名单(读类 1 · 写类 1 · 纯工具 6,逐活动授权)'); /* * ③ 活动装入 —— 两批: diff --git a/apps/api/src/app/tools-w05.ts b/apps/api/src/app/tools-w05.ts new file mode 100644 index 0000000..219e0c7 --- /dev/null +++ b/apps/api/src/app/tools-w05.ts @@ -0,0 +1,87 @@ +/** + * ACT-WORK-05 差异评估的两枚纯工具契约(活动私有作用域)。 + * 两处消费:`seed-mvp.ts`(db:seed 全套播种)与 `seed-activities.ts`(activities:install 装活动时顺带注册 —— + * 「安装即用」包含它声明过的纯工具能调得动;从前 E04 的工具只在 db:seed 里注册,开发机上只跑 + * activities:install 的人第一次跑活动就撞「工具不存在」)。 + */ +const PURE = { + effect: 'read' as const, + errors: [] as Array<{ code: string; retryable: boolean; message: string }>, + idempotent: true, + timeoutMs: 5_000, + retries: 0, + connectionKeys: [] as string[], + pure: true, + ownerActivityId: 'part-substitution-assessment', +}; + +/** 逐参数比对 —— 出参键与台账列同名(difference_status / risk_level),模型照抄 */ +export const COMPARE_PARAMETERS_TOOL = { + ...PURE, + name: 'part-substitution-assessment.compare-parameters', + displayName: '替代料逐参数比对', + version: '1.0.0', + description: + '按硬/软约束逐参数比对原 PN 与候选 PN,输出四态 difference_status(match/mismatch/unknown/conditional)、' + + 'compare_result、risk_level 与各候选的计数。一次可比一批候选(candidates 数组),每对参数可带客户约束 requirement(如 ">= 40")与证据引用 evidenceRef;' + + '硬约束没有证据引用不判 match。规则版本 R-2026.07。', + input: { + type: 'object', + properties: { + candidates: { + type: 'array', + description: '[{candidateInternalPn, pairs:[{parameterName, originalValue, candidateValue, unit?, constraintType:"hard"|"soft", requirement?, evidenceRef?}]}]', + }, + }, + }, + output: { + type: 'object', + properties: { + ruleVersion: { type: 'string' }, + results: { + type: 'array', + description: '[{candidateInternalPn, items:[{parameterName, unit, constraintType, compareResult, differenceStatus, riskLevel, note}], counts:{match,mismatch,unknown,conditional,hardMismatch,softMismatch,configurable}}]', + }, + }, + }, +}; + +/** 推荐定级 —— 出参 recommendationLevel / riskLevel / reason 直接落 P-11 */ +export const RECOMMEND_TOOL = { + ...PURE, + name: 'part-substitution-assessment.recommend', + displayName: '替代推荐定级', + version: '1.0.0', + description: + '按每个候选的比对计数定推荐等级:硬约束 mismatch>0 → not_recommended;unknown>0 → conditional;' + + '只有可调配成(configurable)→ alternate;其余 conditional → conditional;全 match → recommended。', + input: { + type: 'object', + properties: { + candidates: { type: 'array', description: '[{candidateInternalPn, counts:{match,mismatch,unknown,conditional,hardMismatch,softMismatch,configurable}}]' }, + }, + }, + output: { + type: 'object', + properties: { + ruleVersion: { type: 'string' }, + results: { type: 'array', description: '[{candidateInternalPn, recommendationLevel, riskLevel, reason}]' }, + }, + }, +}; + +export const W05_TOOLS = [COMPARE_PARAMETERS_TOOL, RECOMMEND_TOOL] as const; + +/** 试运行审的样例(两件共用一份:并集覆盖各家入参) */ +export const W05_TOOL_SAMPLE = { + input: { + candidates: [ + { + candidateInternalPn: 'XL2596S-5.0', + pairs: [{ parameterName: '输出电压', originalValue: '5.0', candidateValue: '5.0', unit: 'V', constraintType: 'hard', evidenceRef: 'P-08' }], + counts: { match: 1, mismatch: 0, unknown: 0, conditional: 0, hardMismatch: 0, softMismatch: 0, configurable: 0 }, + }, + ], + }, + expectShape: { type: 'object' }, +}; diff --git a/apps/api/src/app/tools/first-party.ts b/apps/api/src/app/tools/first-party.ts index 367da49..691efbf 100644 --- a/apps/api/src/app/tools/first-party.ts +++ b/apps/api/src/app/tools/first-party.ts @@ -332,6 +332,154 @@ const slotPlaceholder: ToolHandler = async ({ input }) => { }; }; +/* ── ACT-WORK-05 差异评估:逐参数比对 —— 纯工具(活动私有作用域)───────────────── + * + * 「能算的不问 LLM」:同输入必同结论。规则与活动包 manifest 里的 D01/D02 两张 DMN 同一份; + * 这里是求值器,那里是随包冻结的文档。`RULE_VERSION` 随结论落账(rule_version)。 + * + * 比对语义(按硬/软约束): + * equal / covers(候选范围 ⊇ 原范围) → match,低风险; + * configurable(原是定值、候选是含它的可调范围)→ conditional(要改外围); + * narrower / differs / partial → 硬:mismatch 高风险;软:conditional 中风险; + * 任一侧未披露 → unknown 中风险; + * 硬约束 equal/covers 但没给 evidenceRef → conditional(硬约束未经证据支持不得判 MATCH); + * requirement(客户约束,如 ">= 40")候选不满足 → 一律 mismatch 高风险。 + */ +export const SUBSTITUTION_RULE_VERSION = 'R-2026.07'; + +type Parsed = { kind: 'number'; value: number } | { kind: 'range'; lo: number; hi: number } | { kind: 'text'; value: string } | { kind: 'unknown' }; +type CompareResult = 'equal' | 'covers' | 'configurable' | 'narrower' | 'partial' | 'differs' | 'unknown'; +type DifferenceStatus = 'match' | 'mismatch' | 'unknown' | 'conditional'; +type RiskLevel = 'low' | 'medium' | 'high'; +type ParamPair = { + parameterName: string; + originalValue: string; + candidateValue: string; + unit?: string; + constraintType: 'hard' | 'soft'; + requirement?: string; + evidenceRef?: string; +}; +type CompareCounts = { match: number; mismatch: number; unknown: number; conditional: number; hardMismatch: number; softMismatch: number; configurable: number }; + +const UNKNOWN_WORDS = /^(未披露|未知|未给出|unknown|n\/?a|tbd|待定|-|—|–)$/i; +const NUM = '[-+]?\\d+(?:\\.\\d+)?'; +const RANGE_RE = new RegExp(`^(${NUM})\\s*(?:~|~|-|–|—|至|to)\\s*(${NUM})`, 'i'); +const NUM_RE = new RegExp(`^(${NUM})`); + +export function parseParamValue(raw: unknown): Parsed { + const s = String(raw ?? '').trim().replace(/\s+/g, ' '); + if (s === '' || UNKNOWN_WORDS.test(s)) return { kind: 'unknown' }; + const r = RANGE_RE.exec(s); + if (r !== null) { + const lo = Number(r[1]); + const hi = Number(r[2]); + if (Number.isFinite(lo) && Number.isFinite(hi)) return { kind: 'range', lo: Math.min(lo, hi), hi: Math.max(lo, hi) }; + } + const n = NUM_RE.exec(s); + if (n !== null && Number.isFinite(Number(n[1]))) return { kind: 'number', value: Number(n[1]) }; + return { kind: 'text', value: s.toLowerCase() }; +} + +function compareParsed(o: Parsed, c: Parsed): CompareResult { + if (o.kind === 'unknown' || c.kind === 'unknown') return 'unknown'; + if (o.kind === 'range' && c.kind === 'range') { + if (c.lo === o.lo && c.hi === o.hi) return 'equal'; + if (c.lo <= o.lo && c.hi >= o.hi) return 'covers'; + if (c.lo >= o.lo && c.hi <= o.hi) return 'narrower'; + return c.hi < o.lo || c.lo > o.hi ? 'differs' : 'partial'; + } + if (o.kind === 'number' && c.kind === 'number') return Math.abs(o.value - c.value) <= 1e-9 ? 'equal' : 'differs'; + if (o.kind === 'number' && c.kind === 'range') return c.lo <= o.value && o.value <= c.hi ? 'configurable' : 'differs'; + if (o.kind === 'range' && c.kind === 'number') return o.lo <= c.value && c.value <= o.hi ? 'narrower' : 'differs'; + if (o.kind === 'text' && c.kind === 'text') return o.value === c.value ? 'equal' : 'differs'; + return 'differs'; +} + +/** 客户约束:">= 40" / "<= 85" / "-40~85"(候选必须盖住);解析不了当没给 */ +function satisfiesRequirement(requirement: string | undefined, c: Parsed): boolean | undefined { + if (requirement === undefined || requirement.trim() === '' || c.kind === 'unknown' || c.kind === 'text') return undefined; + const s = requirement.trim(); + const ge = new RegExp(`^(?:>=|≥)\\s*(${NUM})$`).exec(s); + const le = new RegExp(`^(?:<=|≤)\\s*(${NUM})$`).exec(s); + const hi = c.kind === 'range' ? c.hi : c.value; + const lo = c.kind === 'range' ? c.lo : c.value; + if (ge !== null) return hi >= Number(ge[1]); + if (le !== null) return lo <= Number(le[1]); + const req = parseParamValue(s); + if (req.kind === 'range') return lo <= req.lo && hi >= req.hi; + if (req.kind === 'number') return lo <= req.value && hi >= req.value; + return undefined; +} + +/** 生命周期不是数值:候选比原料「更活」算 covers(替 NRND 料本就是为了这个),更差才是 differs */ +const LIFECYCLE_ORDER: Record = { active: 3, nrnd: 2, risk: 2, eol: 1, discontinued: 0 }; +function compareLifecycle(o: string, c: string): CompareResult | undefined { + const ro = LIFECYCLE_ORDER[o.trim().toLowerCase()]; + const rc = LIFECYCLE_ORDER[c.trim().toLowerCase()]; + if (ro === undefined || rc === undefined) return undefined; + return rc === ro ? 'equal' : rc > ro ? 'covers' : 'differs'; +} + +function judgePair(p: ParamPair): { compareResult: CompareResult; differenceStatus: DifferenceStatus; riskLevel: RiskLevel; note: string } { + const o = parseParamValue(p.originalValue); + const c = parseParamValue(p.candidateValue); + const hard = p.constraintType === 'hard'; + let compareResult = compareParsed(o, c); + if (/生命周期|lifecycle/i.test(p.parameterName)) compareResult = compareLifecycle(p.originalValue, p.candidateValue) ?? compareResult; + const req = satisfiesRequirement(p.requirement, c); + if (req === false) { + // 客户约束不满足:比对结果照实留(narrower/differs),结论一律 MISMATCH 高风险 + return { compareResult: compareResult === 'unknown' ? 'differs' : compareResult, differenceStatus: 'mismatch', riskLevel: 'high', note: `不满足客户约束 ${p.requirement ?? ''}` }; + } + if (compareResult === 'unknown') return { compareResult, differenceStatus: 'unknown', riskLevel: 'medium', note: '一侧未披露:标 UNKNOWN,挂验证任务或补来源' }; + if (compareResult === 'equal' || compareResult === 'covers') { + if (hard && (p.evidenceRef === undefined || p.evidenceRef.trim() === '')) { + return { compareResult, differenceStatus: 'conditional', riskLevel: 'medium', note: '硬约束缺证据引用:未经证据支持不判 MATCH' }; + } + return { compareResult, differenceStatus: 'match', riskLevel: 'low', note: compareResult === 'covers' ? '候选范围盖住原范围' : '一致' }; + } + if (compareResult === 'configurable') return { compareResult, differenceStatus: 'conditional', riskLevel: 'medium', note: '候选可调,须改外围把它配成原值' }; + if (compareResult === 'partial') return { compareResult, differenceStatus: 'conditional', riskLevel: hard ? 'medium' : 'low', note: '范围部分重叠,须按应用边界确认' }; + compareResult = compareResult === 'narrower' ? 'narrower' : 'differs'; + return hard + ? { compareResult, differenceStatus: 'mismatch', riskLevel: 'high', note: '硬约束不一致' } + : { compareResult, differenceStatus: 'conditional', riskLevel: 'medium', note: '软约束不一致,须评估影响' }; +} + +const compareParameters: ToolHandler = async ({ input }) => { + const candidates = Array.isArray(input['candidates']) ? (input['candidates'] as Array<{ candidateInternalPn: string; pairs: ParamPair[] }>) : []; + const results = candidates.map((cand) => { + const counts: CompareCounts = { match: 0, mismatch: 0, unknown: 0, conditional: 0, hardMismatch: 0, softMismatch: 0, configurable: 0 }; + const items = (Array.isArray(cand.pairs) ? cand.pairs : []).map((p) => { + const j = judgePair(p); + counts[j.differenceStatus] += 1; + if (j.differenceStatus === 'mismatch') counts[p.constraintType === 'hard' ? 'hardMismatch' : 'softMismatch'] += 1; + if (j.compareResult === 'configurable') counts.configurable += 1; + return { parameterName: p.parameterName, unit: p.unit ?? '', constraintType: p.constraintType, ...j }; + }); + return { candidateInternalPn: cand.candidateInternalPn, items, counts }; + }); + return { ok: true, output: { ruleVersion: SUBSTITUTION_RULE_VERSION, results } }; +}; + +/** + * 推荐定级(D02,FIRST):硬 MISMATCH → 不推荐;有 UNKNOWN → 条件推荐(等验证闭环); + * 有 configurable(改外围配得成)→ 备选;其余 conditional → 条件推荐;全 MATCH → 推荐。与 manifest 里的 D02 表同一份规则。 + */ +const recommend: ToolHandler = async ({ input }) => { + const candidates = Array.isArray(input['candidates']) ? (input['candidates'] as Array<{ candidateInternalPn: string; counts: Partial }>) : []; + const results = candidates.map((cand) => { + const n = (k: keyof CompareCounts) => Number(cand.counts?.[k] ?? 0); + if (n('hardMismatch') > 0) return { candidateInternalPn: cand.candidateInternalPn, recommendationLevel: 'not_recommended', riskLevel: 'high', reason: `硬约束 MISMATCH ${n('hardMismatch')} 项:硬约束失败直接不推荐` }; + if (n('unknown') > 0) return { candidateInternalPn: cand.candidateInternalPn, recommendationLevel: 'conditional', riskLevel: 'medium', reason: `UNKNOWN ${n('unknown')} 项待验证闭环,通过后可升级推荐` }; + if (n('configurable') > 0) return { candidateInternalPn: cand.candidateInternalPn, recommendationLevel: 'alternate', riskLevel: 'medium', reason: `${n('configurable')} 项须改外围配成原值(另有 ${n('conditional') - n('configurable')} 项条件差异):客户接受改设计则启用` }; + if (n('conditional') > 0) return { candidateInternalPn: cand.candidateInternalPn, recommendationLevel: 'conditional', riskLevel: 'medium', reason: `CONDITIONAL ${n('conditional')} 项须按应用边界确认` }; + return { candidateInternalPn: cand.candidateInternalPn, recommendationLevel: 'recommended', riskLevel: 'low', reason: `${n('match')} 项全部 MATCH` }; + }); + return { ok: true, output: { ruleVersion: SUBSTITUTION_RULE_VERSION, results } }; +}; + export const FIRST_PARTY_TOOLS: ReadonlyArray = [ ['crm.history-price-read', historyPriceRead], ['quote-intake.classify', classify], @@ -343,4 +491,6 @@ export const FIRST_PARTY_TOOLS: ReadonlyArray = ['slot.placeholder', slotPlaceholder], ['crm-relationship-coverage.recompute-score', recomputeScore], ['crm-relationship-coverage.rank-key-persons', rankKeyPersons], + ['part-substitution-assessment.compare-parameters', compareParameters], + ['part-substitution-assessment.recommend', recommend], ]; From 13183b9287d643e7569073416baea8a773d060b2 Mon Sep 17 00:00:00 2001 From: xcs Date: Wed, 26 Aug 2026 23:28:17 +0800 Subject: [PATCH 03/21] =?UTF-8?q?feat(w05):=20=E5=B7=AE=E5=BC=82=E8=AF=84?= =?UTF-8?q?=E4=BC=B0=E6=B4=BB=E5=8A=A8=E5=8C=85=E9=AA=A8=E6=9E=B6=E4=B8=8E?= =?UTF-8?q?=E6=B8=85=E5=8D=95=20=E2=80=94=E2=80=94=20=E5=85=AD=E5=BC=A0?= =?UTF-8?q?=E4=B8=BB=E6=95=B0=E6=8D=AE=E8=BF=BD=E5=8A=A0=E5=A3=B0=E6=98=8E?= =?UTF-8?q?=E3=80=81=E4=B8=89=E5=BC=A0=E8=87=AA=E6=8C=81=E5=8D=95=E6=8D=AE?= =?UTF-8?q?=E3=80=81=E4=B8=A4=E5=BC=A0=20DMN=E3=80=81=E4=BA=94=E6=9D=A1?= =?UTF-8?q?=E6=A0=B7=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh --- .../__tests__/activity.test.ts | 128 ++++ .../part-substitution-assessment/package.json | 1 + .../src/case-model.ts | 13 + .../src/constants.ts | 8 + .../part-substitution-assessment/src/index.ts | 11 + .../src/manifest.ts | 558 ++++++++++++++++++ .../src/skills.ts | 55 ++ .../src/views/prd-style.ts | 158 +++++ .../src/views/report.ts | 5 + 9 files changed, 937 insertions(+) create mode 100644 activities/part-substitution-assessment/__tests__/activity.test.ts create mode 100644 activities/part-substitution-assessment/package.json create mode 100644 activities/part-substitution-assessment/src/case-model.ts create mode 100644 activities/part-substitution-assessment/src/constants.ts create mode 100644 activities/part-substitution-assessment/src/index.ts create mode 100644 activities/part-substitution-assessment/src/manifest.ts create mode 100644 activities/part-substitution-assessment/src/skills.ts create mode 100644 activities/part-substitution-assessment/src/views/prd-style.ts create mode 100644 activities/part-substitution-assessment/src/views/report.ts diff --git a/activities/part-substitution-assessment/__tests__/activity.test.ts b/activities/part-substitution-assessment/__tests__/activity.test.ts new file mode 100644 index 0000000..089437b --- /dev/null +++ b/activities/part-substitution-assessment/__tests__/activity.test.ts @@ -0,0 +1,128 @@ +/** + * ACT-WORK-05 差异评估的包内自测 —— **只准 import @sdk / @contracts / @sfeel**。 + * 案例活动没有 `run`,自测不跑回合;测的是清单本身与授权面对齐,结构照 E04 / I01。 + * + * 本文件目前只写三组(清单本身合规 / 台账声明 / 活动入口); + * 「案例模型与清单的对齐」「报告模版:编辑面与只读面各就各位」两组留给 Task 4/5 追加 —— + * `src/case-model.ts` 与 `src/views/report.ts` 眼下还是占位,写了那两组只会红。 + */ +import { describe, expect, it } from 'vitest'; +import { activityManifestSchema } from '@contracts'; +import { manifest, D, RULE_VERSION } from '../src/manifest.ts'; +import activity from '../src/index.ts'; + +const VIEWS = ['p08-master-create', 'p10-document-create', 'eval-input-list', 'difference-review', 'landing-confirm']; +const byAddress = (domain: string, object: string) => manifest.declarations.ledger.find((l) => l.domain === domain && l.object === object); + +describe('清单本身合规', () => { + it('过 @contracts 的 schema', () => { + expect(() => activityManifestSchema.parse(manifest)).not.toThrow(); + }); + + it('没有静态监听(等外部回音一律落在执行工单上,listen 在本仓不可用)', () => { + expect(manifest.declarations.listeners).toEqual([]); + }); + + it('L1/L2 判据都给了谓词;L4 三条不给(裁量项);L5 恰一条', () => { + for (const c of manifest.contract.acceptanceCriteria) { + if (c.level === 'L1' || c.level === 'L2') expect(c.predicate, c.key).toBeDefined(); + if (c.level === 'L4') expect(c.predicate, c.key).toBeUndefined(); + } + expect(manifest.contract.acceptanceCriteria.filter((c) => c.level === 'L4')).toHaveLength(3); + expect(manifest.contract.acceptanceCriteria.filter((c) => c.level === 'L5')).toHaveLength(1); + }); + + it('机检谓词只读基座读数与记账旗:不出现活动自报的汇总列名', () => { + const forbidden = ['relation_total', 'candidate_total', 'recommended_total', 'not_recommended_total']; + for (const c of manifest.contract.acceptanceCriteria) { + for (const name of forbidden) expect(c.predicate ?? '', `${c.key} 读了自报数 ${name}`).not.toContain(name); + } + }); + + it('三件必交付物都有台账地址;人环预算严格大于「基线 8 + 两轮审核循环 4」', () => { + expect(manifest.contract.requiredDeliverables).toHaveLength(3); + for (const d of manifest.contract.requiredDeliverables) expect(d.ledger, d.key).toBeDefined(); + expect(manifest.contract.authorization.humanCallBudget).toBeGreaterThan(12); + expect(manifest.contract.resources.budget.humanCalls).toBeGreaterThan(12); + }); + + it('起单只必填 4 项(客户 / 需求 / 原 PN / 候选数组);datasheet 是选填文件', () => { + expect(manifest.contract.input.data['required']).toEqual(['customerCode', 'requestDescription', 'originalInternalPn', 'candidateInternalPns']); + const properties = manifest.contract.input.data['properties'] as Record; + expect(properties['candidateInternalPns']?.type).toBe('array'); + expect(properties['datasheets']?.format).toBe('file'); + }); + + 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(); + }); + + it('五张视图模版都声明了,剧本每一步引用的 view 都在其中', () => { + expect(new Set(manifest.assets.viewTemplates.map((v) => v.name))).toEqual(new Set(VIEWS)); + for (const step of manifest.assets.businessModel.steps as ReadonlyArray<{ view?: string }>) { + if (step.view !== undefined) expect(VIEWS, step.view).toContain(step.view); + } + }); +}); + +describe('台账声明:授权面与《统一方案》口径', () => { + it('P-08 一张表:主键 internal_pn,参数集是它的一个必填字段,没有 parts.parameter 子表', () => { + const p08 = byAddress('parts', 'profile'); + expect(p08?.fields.filter((f) => f.primaryKey).map((f) => f.name)).toEqual(['internal_pn']); + expect(p08?.fields.find((f) => f.name === 'normalized_parameter_set')?.required).toBe(true); + expect(byAddress('parts', 'parameter')).toBeUndefined(); + }); + + it('P-11 主记录的原 PN / 候选 PN 都是引用墙(查无此料当场拒 —— 存在性检查的硬门)', () => { + const rel = byAddress('substitution', 'relation'); + for (const name of ['original_internal_pn', 'candidate_internal_pn']) { + expect(rel?.fields.find((f) => f.name === name)?.refersTo, name).toBe('parts.profile.internal_pn'); + } + }); + + it('客户批准状态**不在声明里**:本活动没有它的写权(只读边界)', () => { + const rel = byAddress('substitution', 'relation'); + expect(rel?.fields.map((f) => f.name)).not.toContain('customer_approval_status'); + }); + + it('UNKNOWN 必有去向:unknown_disposition 钉在 difference_status = unknown 的行上;四态是 ASCII tally', () => { + const item = byAddress('substitution', 'difference_item'); + expect(item?.fields.find((f) => f.name === 'unknown_disposition')?.requiredWhen).toEqual({ column: 'difference_status', value: 'unknown' }); + expect(item?.fields.find((f) => f.name === 'difference_status')?.tally).toEqual(['match', 'mismatch', 'unknown', 'conditional']); + expect(item?.fields.find((f) => f.name === 'substitution_relation_id')?.distinct).toBe(true); + }); + + it('P-01 回写五项钉在 current_stage = gate03 那一刻(放行前不拦,放行门 L2 数得出来)', () => { + const p01 = byAddress('demand', 'request'); + for (const name of ['risk_level', 'verification_requirement', 'output_ref', 'next_action', 'current_activity']) { + expect(p01?.fields.find((f) => f.name === name)?.requiredWhen, name).toEqual({ column: 'current_stage', value: 'gate03' }); + } + }); + + it('自持单据都带 caseKey eval_ref 且在主键里;放弃候选必须写原因', () => { + for (const object of ['pn_check', 'input_gap', 'verdict']) { + const t = byAddress(D, object); + expect(t?.kind, object).toBe('record'); + expect(t?.caseKey, object).toBe('eval_ref'); + expect(t?.fields.find((f) => f.name === 'eval_ref')?.primaryKey, object).toBe(true); + } + expect(byAddress(D, 'pn_check')?.fields.find((f) => f.name === 'abandon_reason')?.requiredWhen).toEqual({ column: 'exists_status', value: 'abandoned' }); + }); + + it('规则版本常量随包冻结', () => { + expect(RULE_VERSION).toBe('R-2026.07'); + expect(manifest.assets.businessModel.dmn.tables.map((t) => t.id)).toEqual(['d01-difference-status', 'd02-recommendation']); + }); +}); + +describe('活动入口', () => { + it('没有 run —— 行为全在清单里', () => { + expect(activity.run).toBeUndefined(); + }); +}); diff --git a/activities/part-substitution-assessment/package.json b/activities/part-substitution-assessment/package.json new file mode 100644 index 0000000..362ed54 --- /dev/null +++ b/activities/part-substitution-assessment/package.json @@ -0,0 +1 @@ +{ "name": "@activities/part-substitution-assessment", "version": "0.1.0", "private": true, "type": "module", "exports": { ".": "./src/index.ts" } } diff --git a/activities/part-substitution-assessment/src/case-model.ts b/activities/part-substitution-assessment/src/case-model.ts new file mode 100644 index 0000000..a12f581 --- /dev/null +++ b/activities/part-substitution-assessment/src/case-model.ts @@ -0,0 +1,13 @@ +/** 占位:真正的案例模型见 Task 4(整份替换本文件)。这里只放一份 schema 过得去的最小件。 */ +export const CMMN_XML = ` + + + + + + + +`; +export const NARRATIVE = '(占位:案例说明书见 Task 4)'; +export const ASSESSMENT_STEPS: Array<{ item: string; intent: string; guide: string; writes: string[]; reads: string[]; tools: string[]; view?: string; doneWhen?: string }> = []; +export const ASSESSMENT_PHASES: Array<{ name: string; items: string[] }> = []; diff --git a/activities/part-substitution-assessment/src/constants.ts b/activities/part-substitution-assessment/src/constants.ts new file mode 100644 index 0000000..1febf6a --- /dev/null +++ b/activities/part-substitution-assessment/src/constants.ts @@ -0,0 +1,8 @@ +/** 清单与案例模型共用的常量 —— 单独成文件,免得 manifest ↔ case-model 循环 import 撞 TDZ。 */ +export const ACTIVITY_ID_TEXT = 'part-substitution-assessment'; +/** 活动自持单据的域名 */ +export const D = 'sub_eval'; +/** GOV 差异规则版本 —— 随包冻结,随每条结论落账(rule_version) */ +export const RULE_VERSION = 'R-2026.07'; +export const TOOL_COMPARE = 'part-substitution-assessment.compare-parameters'; +export const TOOL_RECOMMEND = 'part-substitution-assessment.recommend'; diff --git a/activities/part-substitution-assessment/src/index.ts b/activities/part-substitution-assessment/src/index.ts new file mode 100644 index 0000000..71c42a2 --- /dev/null +++ b/activities/part-substitution-assessment/src/index.ts @@ -0,0 +1,11 @@ +/** + * ACT-WORK-05 · 差异评估 —— 活动包代码入口(ADR-004:默认导出 defineActivity 的返回值)。 + * + * 这个活动没有 `run`。案例活动的推进由 Loop 驱动,行为全部写在清单里: + * 案例模型 + 说明书 + 步骤剧本 + 三条技能 + 台账声明 + 五张 HTML 报告模版。 + * 这个文件之所以还存在,是因为清单的 `entry` 指着它 —— + * 它是**被静态审扫描的产物文本**(查 import 与全局引用,证明活动包零环境能力)。 + */ +import { defineActivity } from '@sdk'; + +export default defineActivity({ name: 'part-substitution-assessment' }); diff --git a/activities/part-substitution-assessment/src/manifest.ts b/activities/part-substitution-assessment/src/manifest.ts new file mode 100644 index 0000000..1dad0f8 --- /dev/null +++ b/activities/part-substitution-assessment/src/manifest.ts @@ -0,0 +1,558 @@ +/** + * 活动包清单 —— ACT-WORK-05 差异评估(案例活动)。 + * + * 业务出处:`docs/SS02-ACT-WORK-05-html/`(活动流程与工单说明 / 一键启动入参说明 / + * 《SS02-SS03-SS04_Demo台账字段细化与统一方案》/ 四份目标件 / 全流程分支图)。 + * 骨架照 E04 / I01 已验证的样子:双级入参 → 活动自持单据 + 共享主数据 → decide 循环做中间审核 → + * 终局放行。三处与前两个活动刻意不同,写在这里免得下一个读者当成疏漏: + * ① **主数据是六张(P-08/P-10/P-01/P-11 主+子/P-06)**,物理表由平台基线 + * (`apps/api/src/app/seed-mvp-data.ts`)物化,这里只**追加**本活动要用的列与标记 + * (required / tally / refersTo / requiredWhen 是活动的授权面;声明合并是纯 add-column)。 + * ② **数据工单**(PN 不在库时让人从零建整条主数据)= 零输入执行工单 + 工作区的件 + * (`mine:parts.profile` / `mine:parts.document` 可加行),确认落草稿层、放行并入正式表 + * (守铁律②;文档里「确认即入库」在本平台落成「无审核环节、放行并入」)。一张表一张单。 + * ③ 逐项四态与推荐等级**由两枚纯工具算**(规则同时以 D01/D02 两张 DMN 随包冻结当文档), + * 模型只负责对照表、证据与影响说明 —— 硬约束 MISMATCH 不可能被措辞改判。 + */ +import type { ActivityManifest, DmnModel } from '@contracts'; +import { activityIdSchema } from '@contracts'; +import { ASSESSMENT_SKILLS } from './skills.ts'; +import { CMMN_XML, NARRATIVE, ASSESSMENT_PHASES, ASSESSMENT_STEPS } from './case-model.ts'; +import { P08_MASTER_CREATE, P10_DOCUMENT_CREATE, EVAL_INPUT_LIST, DIFFERENCE_REVIEW, LANDING_CONFIRM } from './views/report.ts'; +import { ACTIVITY_ID_TEXT, D, TOOL_COMPARE, TOOL_RECOMMEND } from './constants.ts'; + +export { D, RULE_VERSION, TOOL_COMPARE, TOOL_RECOMMEND } from './constants.ts'; +const ACTIVITY_ID = activityIdSchema.parse(ACTIVITY_ID_TEXT); + +/** + * 两张判定表 —— **文档形态**(求值器在 `apps/api/src/app/tools/first-party.ts`,与这里同一份规则): + * D01 逐参数四态、D02 推荐等级。都是 FIRST:规则从上往下第一条命中即出结论,顺序就是优先级。 + */ +export const D01_DMN: DmnModel['tables'][number] = { + id: 'd01-difference-status', + label: '差异四态 D01(R-2026.07)', + hitPolicy: 'FIRST', + inputs: [ + { name: 'compare_result', expression: 'compare_result' }, + { name: 'constraint_type', expression: 'constraint_type' }, + { name: 'evidence_present', expression: 'evidence_present' }, + ], + outputs: [{ name: 'difference_status' }, { name: 'risk_level' }], + rules: [ + { id: 'd01-u', when: ['"unknown"', '-', '-'], then: ['unknown', 'medium'], annotation: '一侧未披露 → UNKNOWN,挂验证任务' }, + { id: 'd01-m0', when: ['"equal","covers"', '"hard"', 'false'], then: ['conditional', 'medium'], annotation: '硬约束缺证据不判 MATCH' }, + { id: 'd01-m', when: ['"equal","covers"', '-', '-'], then: ['match', 'low'], annotation: '一致或候选范围盖住原范围' }, + { id: 'd01-c', when: ['"configurable","partial"', '-', '-'], then: ['conditional', 'medium'], annotation: '可调配成 / 部分重叠 → 条件' }, + { id: 'd01-x', when: ['"narrower","differs"', '"hard"', '-'], then: ['mismatch', 'high'], annotation: '硬约束不一致' }, + { id: 'd01-s', when: ['"narrower","differs"', '"soft"', '-'], then: ['conditional', 'medium'], annotation: '软约束不一致 → 评估影响' }, + ], +}; + +export const D02_DMN: DmnModel['tables'][number] = { + id: 'd02-recommendation', + label: '推荐等级 D02(R-2026.07)', + hitPolicy: 'FIRST', + inputs: [ + { name: 'hard_mismatch_total', expression: 'hard_mismatch_total' }, + { name: 'unknown_total', expression: 'unknown_total' }, + { name: 'configurable_total', expression: 'configurable_total' }, + { name: 'conditional_total', expression: 'conditional_total' }, + ], + outputs: [{ name: 'recommendation_level' }], + rules: [ + { id: 'd02-n', when: ['> 0', '-', '-', '-'], then: ['not_recommended'], annotation: '硬约束失败直接不推荐' }, + { id: 'd02-u', when: ['0', '> 0', '-', '-'], then: ['conditional'], annotation: '有 UNKNOWN → 等验证闭环' }, + { id: 'd02-a', when: ['0', '0', '> 0', '-'], then: ['alternate'], annotation: '只靠改外围配得成 → 备选' }, + { id: 'd02-c', when: ['0', '0', '0', '> 0'], then: ['conditional'], annotation: '其余条件项 → 条件推荐' }, + { id: 'd02-r', when: ['0', '0', '0', '0'], then: ['recommended'], annotation: '全 MATCH → 推荐' }, + ], +}; + +export const ASSESSMENT_DMN: DmnModel = { + drd: [ + { decision: 'd01-difference-status', requires: [], inputs: ['compare_result', 'constraint_type', 'evidence_present'] }, + { decision: 'd02-recommendation', requires: [], inputs: ['hard_mismatch_total', 'unknown_total', 'configurable_total', 'conditional_total'] }, + ], + tables: [D01_DMN, D02_DMN], +}; + +/** 字段声明的短写(与 E04 同一套写法:显式 primaryKey / masked,其余按需) */ +type F = { name: string; type: 'text' | 'number' | 'integer' | 'boolean' | 'date' | 'timestamp' | 'json'; label: string; primaryKey?: boolean; required?: boolean; expect?: boolean; tally?: string[]; auto?: boolean; distinct?: boolean; refersTo?: string; requiredWhen?: { column: string; value: string } }; +const col = (f: F) => ({ primaryKey: false, masked: false, ...f }); + +export const manifest: ActivityManifest = { + identity: { + activityId: ACTIVITY_ID, + name: 'ACT-WORK-05 · 差异评估', + packageVersion: '1.0.0', + category: '需求分析与方案匹配', + sdkVersion: '0.1.0-m5', + checksum: 'dev-checksum-part-substitution-assessment', + }, + + /* ── 契约七件 ────────────────────────────────────────────────── */ + contract: { + activityId: ACTIVITY_ID, + version: '1.0.0', + input: { + /* + * 一键启动只收 4 项(文档《一键启动入参说明》极简原则):客户 / 客户需求 / 原 PN / 候选 PN 数组。 + * 应用场景、BOM 位、替代类型、规则版本、负责人期限**不是入参** —— 启动后在执行工单 01 里补。 + * datasheet 选填文件:传了 AI 在数据工单件上预填,没传人全填(文档 0c:同为合法路径)。 + * `customerName` 只在客户不在库(分支 0a)时用:S0 发数据录入卡建档,名字先摆上去让人核。 + */ + data: { + type: 'object', + required: ['customerCode', 'requestDescription', 'originalInternalPn', 'candidateInternalPns'], + properties: { + customerCode: { type: 'string', title: '客户编码(L-02 客户主台账)', examples: ['CU-0142'] }, + customerName: { type: 'string', title: '客户名称(仅客户不在库时填,启动后先建档)', examples: ['深圳恒星电源有限公司'] }, + requestDescription: { + type: 'string', + title: '客户需求(原始描述 —— 据此新建 P-01 需求主记录 · 版本 V1)', + examples: ['工业电源项目在用 LM2596S-5.0(TI,12V→5V),原厂 PCN 转 NRND,客户要求验证替代料'], + }, + originalInternalPn: { type: 'string', title: '原内部料号(P-08)', examples: ['LM2596S-5.0'] }, + candidateInternalPns: { + type: 'array', + title: '候选内部料号(数组,≥1;不在库的启动后经数据工单创建)', + items: { type: 'string' }, + examples: [['XL2596S-5.0', 'LM2596S-ADJ']], + }, + datasheets: { + type: 'array', + format: 'file', + title: '候选 datasheet(选填;传了 AI 在数据工单里预填,没传人全填)', + items: { + type: 'object', + properties: { + name: { type: 'string', title: '文件名' }, + contentType: { type: 'string', title: '类型' }, + size: { type: 'integer', title: '字节数' }, + ref: { type: 'string', title: '存档号' }, + text: { type: 'string', title: '读出来的文本' }, + note: { type: 'string', title: '读取说明' }, + }, + }, + }, + }, + }, + messageAllowed: true, + messageHint: 'FAE 可在此说明客户的应用边界、验证条件或本轮评估的特殊要求', + }, + output: { + /* 单一必发口:本活动终点是 ACT-GATE-03 评审放行;下游 ACT-WORK-08 尚未实现,按「契约先立」声明 */ + mandatory: [ + { + name: 'assessment-released', + description: '差异评估结论已落账(P-11 替代关系主记录 + 差异明细、P-01 回写)—— 供 ACT-WORK-08 证据归集消费;当前无消费者,载荷 = 已落地事实', + payload: { + type: 'object', + required: ['evalRef', 'requestId', 'relationTotal'], + properties: { + evalRef: { type: 'string', title: '评估案号' }, + requestId: { type: 'string', title: '需求编号(P-01)' }, + relationTotal: { type: 'integer', title: '落账的替代关系条数' }, + recommendedTotal: { type: 'integer', title: '推荐' }, + conditionalTotal: { type: 'integer', title: '条件推荐' }, + alternateTotal: { type: 'integer', title: '备选' }, + notRecommendedTotal: { type: 'integer', title: '不推荐' }, + }, + }, + }, + ], + selective: [], + }, + requiredDeliverables: [ + { key: 'eval-input', label: '评估输入清单', ledger: `${D}.input_gap`, ledgers: [`${D}.input_gap`, `${D}.pn_check`, `${D}.verdict`], editableAtRelease: [] }, + { key: 'difference-items', label: '差异评估结论(P-11 差异明细)', ledger: 'substitution.difference_item', ledgers: ['substitution.difference_item', 'substitution.relation'], editableAtRelease: [] }, + { key: 'landing', label: '拟落账内容(P-11 主记录 + P-01 回写)', ledger: 'substitution.relation', ledgers: ['substitution.relation', 'demand.request'], editableAtRelease: [] }, + ], + /* + * 验收判据 —— L1/L2 只读基座读数;L4 三条是文档验收工单 01/02 里留给人的裁量; + * 「推荐等级与明细一致」「硬约束不判 MATCH」机器已由工具守住,这里仍留 L4 让签字的人过目。 + */ + acceptanceCriteria: [ + { + key: 'input-confirmed', + label: '评估输入已经 PW 补齐确认:执行工单 01 已交回,缺口行的补充内容都有,补不了的留了 UNKNOWN', + level: 'L1', + predicate: `input_confirmed_total > 0 and ${D}_input_gap_rows > 0 and ${D}_input_gap_missing = 0`, + }, + { + key: 'review-approved', + label: '差异评估结论已经 FAE/RI 审核通过:验收工单 01 已过,不处在驳回待改状态', + level: 'L1', + predicate: 'review_approved_total > 0 and substitution_difference_item_rows > 0 and substitution_difference_item_missing = 0', + }, + { + key: 'every-candidate-assessed', + label: '每个未放弃的候选都有一条替代关系记录(N 候选 = N 条 P-11,并行独立落账)', + level: 'L2', + predicate: `substitution_relation_rows > 0 and substitution_relation_rows + ${D}_pn_check_exists_status_abandoned + 1 = ${D}_pn_check_rows`, + }, + { + key: 'every-item-has-status', + label: '关键参数差异均有明确状态:每条明细都是 MATCH / MISMATCH / UNKNOWN / CONDITIONAL 之一', + level: 'L2', + predicate: + 'substitution_difference_item_rows > 0 and substitution_difference_item_difference_status_match + substitution_difference_item_difference_status_mismatch + ' + + 'substitution_difference_item_difference_status_unknown + substitution_difference_item_difference_status_conditional = substitution_difference_item_rows', + }, + { + key: 'unknown-has-disposition', + label: 'UNKNOWN 项均有去向:每个 UNKNOWN 都挂了验证任务或补来源动作,影响说明与证据引用逐条齐全', + level: 'L2', + predicate: 'substitution_difference_item_missing = 0', + }, + { + key: 'judged-by-rule', + label: '四态与推荐等级由规则算出:逐参数比对工具与推荐定级工具都真调过', + level: 'L2', + predicate: `tool_part_substitution_assessment_compare_parameters_calls > 0 and tool_part_substitution_assessment_recommend_calls > 0`, + }, + { + key: 'analysis-status-confirmed', + label: '分析状态与客户批准状态分离:每条 P-11 只写分析层 analysis_status = FAE 确认,规则版本已锁定随结论落账', + level: 'L2', + predicate: 'substitution_relation_analysis_status_fae_confirmed = substitution_relation_rows and substitution_relation_missing = 0', + }, + { + key: 'p01-writeback-complete', + label: '回写 P-01 字段完整:风险等级、验证要求、输出链接、下一步动作、当前阶段五项齐全', + level: 'L2', + predicate: 'demand_request_rows > 0 and demand_request_missing = 0', + }, + { key: 'evidence-credible', label: '证据链可信:每条结论都能追溯到 P-08 / P-10 / P-06 的具体记录,或本单数据工单的创建记录(人工录入)', level: 'L4' }, + { key: 'hard-not-auto-match', label: '硬约束项未被自动判 MATCH:封装 / 工艺 / 电气边界等硬约束的 MATCH 都有证据支持', level: 'L4' }, + { key: 'recommendation-consistent', label: '推荐等级与差异明细一致:存在 UNKNOWN / CONDITIONAL 的候选没有给「推荐」', level: 'L4' }, + /* L5 是责任关:那张卡本身就是这道关,卡面上不摆它(基座统一滤) */ + { key: 'gate03-release', label: 'ACT-GATE-03 评审放行:签下去 P-11 落账、P-01 回写,结论供下游消费', level: 'L5' }, + ], + revisableAtRelease: [], + prohibitions: [ + { key: 'no-fabricated-parameter', statement: 'UNKNOWN 是一等公民:查不到的参数值必须显式写「未披露」并记去向,不得伪造、不得用估计值或空串冒充' }, + { key: 'no-override-hard-mismatch', statement: '硬约束 MISMATCH 不得被任何措辞改判 MATCH;要改只能补证据后重比' }, + { key: 'no-customer-approval-write', statement: '客户批准状态由独立审批流维护,本活动不写 customer_approval_status(没有这一列的写权)' }, + { key: 'no-commercial-judgement', statement: '本活动只答技术可替代性:不做数量、价格、交期、采购判断' }, + { key: 'no-rebind', statement: '启动绑定的 4 项(客户 / 需求 / 原 PN / 候选数组)工单内不可改;绑错只能退回触发方重新启动' }, + { key: 'no-external-write', statement: '放行前不得触达外部系统 —— 触达世界的动作集中在放行那一刻' }, + ], + authorization: { + discretion: [], + preauthorizedActions: [], + /* + * 基线:数据工单 ≤2 + 候选放弃/创建决策 ≤2 + 新客户录入卡 1 + 执行单 01 + 审核决策 1 + 放行 1 = 8; + * 审核循环预留两轮(+4)= 12。cap 必须严格大于最后一次拍板时的实际次数(E04 实测栽过),留到 16。 + */ + humanCallBudget: 16, + }, + resources: { + tools: [{ name: TOOL_COMPARE }, { name: TOOL_RECOMMEND }], + skills: ASSESSMENT_SKILLS.map((s) => s.id), + models: ['deepseek-v4-flash'], + roles: ['FAE/RI'], + budget: { steps: 90, tokens: 600_000, activeMinutes: 120, humanCalls: 16 }, + }, + }, + + /* ── 声明 ────────────────────────────────────────────────────── */ + declarations: { + models: ['deepseek-v4-flash'], + tools: [{ name: TOOL_COMPARE }, { name: TOOL_RECOMMEND }], + ledger: [ + /* L-02 客户主台账:只在分支 0a(客户不在库)由数据录入卡建一行 —— 录入卡写的列都得声明 */ + { + domain: 'customer', + object: 'profile', + kind: 'master', + label: '客户档案', + fields: [ + col({ name: 'customer_code', type: 'text', label: '客户编码', primaryKey: true }), + col({ name: 'name', type: 'text', label: '客户名称', required: true }), + col({ name: 'industry', type: 'text', label: '所属行业', expect: true }), + col({ name: 'region', type: 'text', label: '区域', expect: true }), + ], + }, + /* P-08 芯片主数据:数据工单建整条;参数集是它的一个字段(一张表,不建子表) */ + { + domain: 'parts', + object: 'profile', + kind: 'master', + label: '芯片主数据(P-08)', + fields: [ + col({ name: 'internal_pn', type: 'text', label: '内部料号', primaryKey: true }), + col({ name: 'brand_name', type: 'text', label: '品牌名称', required: true }), + col({ name: 'manufacturer_name', type: 'text', label: '制造商名称', expect: true }), + col({ name: 'specification_description', type: 'text', label: '规格描述', required: true }), + col({ name: 'normalized_parameter_set', type: 'text', label: '标准化参数集(一行一参数:参数名 | 值 | 单位 | hard/soft | 说明)', required: true }), + col({ name: 'package', type: 'text', label: '封装', required: true }), + col({ name: 'process_technology', type: 'text', label: '工艺', expect: true }), + col({ name: 'lifecycle_status', type: 'text', label: '生命周期状态', required: true, tally: ['active', 'nrnd', 'eol', 'discontinued', 'risk'] }), + col({ name: 'eol_pcn_status', type: 'text', label: 'EOL/PCN 状态', expect: true }), + col({ name: 'datasheet_ref', type: 'text', label: 'Datasheet 引用', expect: true }), + col({ name: 'datasheet_version', type: 'text', label: 'Datasheet 版本', expect: true }), + col({ name: 'application_scenarios', type: 'text', label: '适配应用场景', expect: true }), + ], + }, + /* P-10 原厂技术资料库:数据工单建;评估时只读做证据回链 */ + { + domain: 'parts', + object: 'document', + kind: 'master', + label: '原厂技术资料库(P-10)', + fields: [ + col({ name: 'internal_pn', type: 'text', label: '关联内部料号', primaryKey: true }), + col({ name: 'doc_seq', type: 'text', label: '资料序号(同一料号下 01/02…)', primaryKey: true }), + col({ name: 'document_type', type: 'text', label: '资料类型', required: true, tally: ['datasheet', 'app_note', 'reference_design', 'white_paper', 'sample_doc', 'commitment_letter', 'other'] }), + col({ name: 'brand_name', type: 'text', label: '品牌名称', expect: true }), + col({ name: 'document_title', type: 'text', label: '资料标题', required: true }), + col({ name: 'document_version', type: 'text', label: '资料版本', required: true }), + col({ name: 'document_ref', type: 'text', label: '资料文件链接', required: true }), + col({ name: 'citation_locator', type: 'text', label: '证据定位(页码/章节/表格)', expect: true }), + col({ name: 'document_status', type: 'text', label: '资料状态', required: true, tally: ['valid', 'expired', 'pending', 'void'] }), + ], + }, + /* P-01 需求主记录:S0 新建 V1,放行时回写五项(current_stage = gate03 那一刻五项必填) */ + { + domain: 'demand', + object: 'request', + kind: 'master', + label: '客户需求主记录(P-01)', + fields: [ + col({ name: 'request_id', type: 'text', label: '需求编号', primaryKey: true }), + col({ name: 'customer_code', type: 'text', label: '客户编码', required: true, refersTo: 'customer.profile.customer_code' }), + col({ name: 'requirement_version', type: 'text', label: '需求版本', required: true }), + col({ name: 'request_type', type: 'text', label: '需求类型', required: true }), + col({ name: 'request_description', type: 'text', label: '需求描述', required: true }), + col({ name: 'input_snapshot_ref', type: 'text', label: '输入快照', required: true }), + col({ name: 'original_internal_pn', type: 'text', label: '原内部料号', required: true }), + col({ name: 'bom_position', type: 'text', label: 'BOM 位', expect: true }), + col({ name: 'application_scenario', type: 'text', label: '应用场景', expect: true }), + col({ name: 'technical_constraints', type: 'text', label: '技术约束' }), + col({ name: 'risk_level', type: 'text', label: '风险等级', tally: ['low', 'medium', 'high', 'critical'], requiredWhen: { column: 'current_stage', value: 'gate03' } }), + col({ name: 'verification_requirement', type: 'text', label: '验证/复测/确认要求', requiredWhen: { column: 'current_stage', value: 'gate03' } }), + col({ name: 'request_status', type: 'text', label: '需求状态', required: true, tally: ['pending', 'responded', 'won', 'abandoned', 'closed'] }), + col({ name: 'current_activity', type: 'text', label: '当前 Activity', requiredWhen: { column: 'current_stage', value: 'gate03' } }), + col({ name: 'current_stage', type: 'text', label: '当前阶段' }), + col({ name: 'next_action', type: 'text', label: '下一步动作', requiredWhen: { column: 'current_stage', value: 'gate03' } }), + col({ name: 'output_ref', type: 'text', label: '标准输出链接', requiredWhen: { column: 'current_stage', value: 'gate03' } }), + ], + }, + /* P-11 替代关系主记录:一候选一条;两枚 PN 都是引用墙(查无此料当场拒 —— 存在性检查的硬门) */ + { + domain: 'substitution', + object: 'relation', + kind: 'master', + label: '替代关系主记录(P-11)', + fields: [ + col({ name: 'substitution_relation_id', type: 'text', label: '替代关系ID', primaryKey: true }), + col({ name: 'eval_ref', type: 'text', label: '评估案号', required: true }), + col({ name: 'request_id', type: 'text', label: '关联需求编号', required: true }), + col({ name: 'bom_position', type: 'text', label: '关联 BOM 位', expect: true }), + col({ name: 'original_internal_pn', type: 'text', label: '原内部料号', required: true, refersTo: 'parts.profile.internal_pn' }), + col({ name: 'candidate_internal_pn', type: 'text', label: '候选内部料号', required: true, refersTo: 'parts.profile.internal_pn' }), + col({ name: 'substitution_type', type: 'text', label: '替代类型', required: true, tally: ['pin_to_pin', 'functional', 'parametric', 'other'] }), + col({ name: 'recommendation_level', type: 'text', label: '推荐等级', required: true, tally: ['recommended', 'conditional', 'alternate', 'not_recommended'] }), + col({ name: 'recommendation_reason', type: 'text', label: '推荐理由', required: true }), + col({ name: 'difference_summary', type: 'text', label: '差异摘要', required: true }), + col({ name: 'risk_level', type: 'text', label: '替代风险等级', required: true, tally: ['low', 'medium', 'high', 'critical'] }), + col({ name: 'verification_requirement', type: 'text', label: '验证要求', requiredWhen: { column: 'recommendation_level', value: 'conditional' } }), + col({ name: 'validation_id', type: 'text', label: '关联验证ID' }), + col({ name: 'analysis_status', type: 'text', label: '分析状态', required: true, tally: ['ai_draft', 'fae_confirmed', 'pending_customer', 'customer_approved', 'rejected', 'void'] }), + col({ name: 'analysis_report_ref', type: 'text', label: '替代分析报告', expect: true }), + col({ name: 'confidence_score', type: 'number', label: '证据可信度', expect: true }), + col({ name: 'rule_version', type: 'text', label: '差异规则版本', required: true }), + ], + }, + /* P-11 差异明细子表:一条参数一条记录;relation id 上 distinct 让基座数「几条关系有明细」 */ + { + domain: 'substitution', + object: 'difference_item', + kind: 'master', + label: '差异明细(P-11 子表)', + fields: [ + col({ name: 'substitution_relation_id', type: 'text', label: '替代关系ID', primaryKey: true, distinct: true, refersTo: 'substitution.relation.substitution_relation_id' }), + col({ name: 'difference_item_id', type: 'text', label: '差异明细ID', primaryKey: true }), + col({ name: 'parameter_name', type: 'text', label: '标准参数名', required: true }), + col({ name: 'original_value', type: 'text', label: '原物料参数值', required: true }), + col({ name: 'candidate_value', type: 'text', label: '候选物料参数值', required: true }), + col({ name: 'parameter_unit', type: 'text', label: '参数单位', expect: true }), + col({ name: 'constraint_type', type: 'text', label: '约束类型', required: true, tally: ['hard', 'soft'] }), + col({ name: 'compare_result', type: 'text', label: '比对结果(工具出参)', expect: true }), + col({ name: 'difference_status', type: 'text', label: '差异状态', required: true, tally: ['match', 'mismatch', 'unknown', 'conditional'] }), + col({ name: 'impact_statement', type: 'text', label: '影响说明(MATCH 写「无」)', required: true }), + col({ name: 'unknown_disposition', type: 'text', label: 'UNKNOWN 去向(验证任务号 / 补来源动作)', requiredWhen: { column: 'difference_status', value: 'unknown' } }), + col({ name: 'risk_level', type: 'text', label: '风险等级', required: true, tally: ['low', 'medium', 'high'] }), + col({ name: 'evidence_ref', type: 'text', label: '差异证据引用', required: true }), + col({ name: 'confidence', type: 'number', label: 'AI 置信度', expect: true }), + col({ name: 'rule_version', type: 'text', label: '差异规则版本', required: true }), + ], + }, + /* P-06 验证记录:事实类 —— 本活动只写「验证要求」(状态 not_started),不经数据工单建 */ + { + domain: 'validation', + object: 'record', + kind: 'master', + label: '芯片验证记录(P-06)', + fields: [ + col({ name: 'validation_id', type: 'text', label: '验证ID', primaryKey: true }), + col({ name: 'request_id', type: 'text', label: '关联需求编号', required: true }), + col({ name: 'validation_internal_pn', type: 'text', label: '验证内部料号', required: true, refersTo: 'parts.profile.internal_pn' }), + col({ name: 'validation_type', type: 'text', label: '验证类型', required: true, tally: ['pin_to_pin', 'functional', 'reliability', 'certification', 'retest'] }), + col({ name: 'validation_status', type: 'text', label: '验证状态', required: true, tally: ['not_started', 'in_progress', 'passed', 'failed', 'conditional', 'pending_evidence'] }), + col({ name: 'test_condition', type: 'text', label: '测试条件', expect: true }), + col({ name: 'next_validation_action', type: 'text', label: '后续验证动作', required: true }), + col({ name: 'evidence_ref', type: 'text', label: '验证证据引用', expect: true }), + ], + }, + /* ── 活动自持单据(record,caseKey eval_ref)── */ + { + domain: D, + object: 'pn_check', + kind: 'record', + caseKey: 'eval_ref', + label: '料号存在性核对', + fields: [ + col({ name: 'eval_ref', type: 'text', label: '评估案号', primaryKey: true }), + col({ name: 'internal_pn', type: 'text', label: '内部料号', primaryKey: true }), + col({ name: 'role', type: 'text', label: '角色', required: true, tally: ['original', 'candidate'] }), + col({ name: 'exists_status', type: 'text', label: '在库状态', required: true, tally: ['found', 'absent', 'created', 'abandoned'] }), + col({ name: 'abandon_reason', type: 'text', label: '放弃原因', requiredWhen: { column: 'exists_status', value: 'abandoned' } }), + col({ name: 'note', type: 'text', label: '备注' }), + ], + }, + { + domain: D, + object: 'input_gap', + kind: 'record', + caseKey: 'eval_ref', + label: '评估输入清单', + fields: [ + col({ name: 'eval_ref', type: 'text', label: '评估案号', primaryKey: true }), + col({ name: 'item_key', type: 'text', label: '输入项键', primaryKey: true }), + col({ name: 'input_item', type: 'text', label: '输入项', required: true }), + col({ name: 'source_ledger', type: 'text', label: '来源台账', expect: true }), + col({ name: 'status', type: 'text', label: '状态', required: true, tally: ['gap', 'default', 'ok'] }), + col({ name: 'detail', type: 'text', label: '绑定值 / 缺口说明', expect: true }), + col({ name: 'supplement', type: 'text', label: '补充内容', requiredWhen: { column: 'status', value: 'gap' } }), + col({ name: 'origin', type: 'text', label: '来源(ai/human-added)' }), + ], + }, + { + domain: D, + object: 'verdict', + kind: 'record', + caseKey: 'eval_ref', + label: '本次评估结论', + fields: [ + col({ name: 'eval_ref', type: 'text', label: '评估案号', primaryKey: true }), + col({ name: 'request_id', type: 'text', label: '需求编号' }), + col({ name: 'rule_version', type: 'text', label: '规则版本' }), + col({ name: 'original_internal_pn', type: 'text', label: '原内部料号' }), + col({ name: 'candidate_total', type: 'integer', label: '候选数' }), + col({ name: 'input_confirmed_total', type: 'integer', label: '评估输入已确认' }), + col({ name: 'review_approved_total', type: 'integer', label: '差异结论已审核通过' }), + col({ name: 'landing_prepared_total', type: 'integer', label: '拟落账已备好' }), + col({ name: 'relation_total', type: 'integer', label: '替代关系条数' }), + col({ name: 'recommended_total', type: 'integer', label: '推荐' }), + col({ name: 'conditional_total', type: 'integer', label: '条件推荐' }), + col({ name: 'alternate_total', type: 'integer', label: '备选' }), + col({ name: 'not_recommended_total', type: 'integer', label: '不推荐' }), + col({ name: 'summary', type: 'text', label: '本次评估结论' }), + ], + }, + ], + listeners: [], + harness: { + memoryWindow: 24, + stallAfter: 'PT4H', + nudge: [{ after: 'PT2H', action: 'nudge' }], + retry: { times: 2, backoff: 'PT30S' }, + }, + }, + + /* ── 资产初始版 ──────────────────────────────────────────────── */ + assets: { + businessModel: { cmmnXml: CMMN_XML, narrative: NARRATIVE, dmn: ASSESSMENT_DMN, steps: ASSESSMENT_STEPS, phases: ASSESSMENT_PHASES }, + skills: ASSESSMENT_SKILLS, + /* + * 五个局面各一张: + * · `p08-master-create` —— S0d 摆,数据工单(P-08),件上可加行、可编辑; + * · `p10-document-create` —— S0d 摆,数据工单(P-10),同上; + * · `eval-input-list` —— S1 摆,评估输入清单:绑定区只读 + 缺口表可编辑; + * · `difference-review` —— S2/S3/S4 摆,差异明细 + 各候选推荐等级,只读; + * · `landing-confirm` —— S5/T_release 摆,拟写 P-11 / 拟回写 P-01,只读;放行后即归档件。 + */ + viewTemplates: [ + { name: 'p08-master-create', html: P08_MASTER_CREATE }, + { name: 'p10-document-create', html: P10_DOCUMENT_CREATE }, + { name: 'eval-input-list', html: EVAL_INPUT_LIST }, + { name: 'difference-review', html: DIFFERENCE_REVIEW }, + { name: 'landing-confirm', html: LANDING_CONFIRM }, + ], + cardSchemas: {}, + distillHooks: [{ on: 'terminal', prompt: '归纳本次差异评估:哪些候选、各自推荐等级与关键依据、UNKNOWN 挂了什么验证任务、审核是否被驳回过(为什么)、数据工单建了哪些主数据。' }], + }, + + /* + * ── 样例集:五条形状真不同(常备案例即验收案例)── + * 第一条满载且走「候选缺库 → 数据工单」那条最长的路(测试台落地即预填的是它);样例名 ≤14 字。 + * 库里只播了 LM2596S-5.0 / LM2596S-ADJ 两颗(见 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'] }, + }, + ], + + entry: { file: 'index.js', checksum: 'dev-entry-part-substitution-assessment' }, +}; diff --git a/activities/part-substitution-assessment/src/skills.ts b/activities/part-substitution-assessment/src/skills.ts new file mode 100644 index 0000000..f271e7f --- /dev/null +++ b/activities/part-substitution-assessment/src/skills.ts @@ -0,0 +1,55 @@ +/** + * ACT-WORK-05 差异评估的技能 —— 载方法,不载权力。 + * 剧本写「这一步做什么、写哪几列」,技能写「为什么这么判、拿不准时按哪条走」。 + */ +import { skillIdSchema } from '@contracts'; +import type { Skill } from '@contracts'; + +export const ASSESSMENT_SKILLS: Skill[] = [ + { + id: skillIdSchema.parse('substitution.normalize-parameters'), + label: '把多来源资料整成可比对的标准参数对照表', + applicability: '输入确认之后、逐项比对之前:原 PN 与每个候选的参数集要摆成同名同单位的一对一。', + method: [ + '参数集在 `parts.profile.normalized_parameter_set` 里,一行一参数:`参数名 | 值 | 单位 | hard/soft | 说明`。', + '对照表按**原 PN 的参数名**为骨架:原 PN 有的参数,候选没有对应行 → 候选值写「未披露」(它会成为 UNKNOWN);', + '候选多出来的参数不比(比对只回答「能不能替」,多出来的能力不构成差异)。', + '单位以原 PN 为准:候选单位不同先换算(mV→V、kHz→MHz),换算不了就不改值、在说明里写明 —— 不许静默改数。', + '约束类型以原 PN 那一行的 hard/soft 为准;原 PN 没标的按 GOV 口径:封装 / 工艺 / 电气边界(电压 / 电流 / 温度 / 阈值)一律 hard,其余 soft。', + '**缺参数不得伪造**:查不到的写「未披露」,不写估计值、不写「约」。', + ].join('\n'), + crystallized: [], + knowledgeRefs: ['标准化参数集行语法', '硬/软约束口径'], + version: '1.0.0', + }, + { + id: skillIdSchema.parse('substitution.difference-judgement'), + label: '逐项四态由工具算,证据与影响由你写', + applicability: '对照表齐了,要出 MATCH / MISMATCH / UNKNOWN / CONDITIONAL 时。', + method: [ + '四态**不许自己判**:调 `part-substitution-assessment.compare-parameters`,把每个候选的全部参数对一次性传进去(candidates 数组,一次一批)。', + '每对参数都带 `evidenceRef`(值从哪份记录来:「P-08」「P-10 Rev H §6.5」「数据工单 <单号> · 确认人 <人>」)——硬约束没有证据引用,工具不判 MATCH。', + '客户在应用场景里给了边界(如「输入上限 40V」)就写进那一对的 `requirement`(`>= 40`),候选不满足直接 MISMATCH。', + '工具出参照抄进 `substitution.difference_item`:difference_status / compare_result / risk_level 一字不改;', + '`impact_statement` 你写:MATCH 写「无」,其余说清对应用 / 验证 / 报价的影响;UNKNOWN 必须写 `unknown_disposition`(挂验证任务号或补来源动作),不许悬空。', + '推荐等级同样由工具定(`part-substitution-assessment.recommend`,传每个候选的 counts),`recommendation_reason` 照工具的 reason 扩写,不许把不推荐改成推荐。', + ].join('\n'), + crystallized: [], + knowledgeRefs: ['四态口径', '推荐等级四档'], + version: '1.0.0', + }, + { + id: skillIdSchema.parse('substitution.landing'), + label: '落账只写分析层,客户批准不碰', + applicability: '审核通过之后拟 P-11 / 回写 P-01 时。', + method: [ + '`analysis_status` 从 `ai_draft` 推到 `fae_confirmed`;`customer_approval_status` 由独立审批流维护 —— 本活动**没有这一列的写权**,写了会被拒。', + 'P-01 回写五项:risk_level(取各候选里最高的)、verification_requirement(把所有验证任务号列全)、output_ref(替代分析报告号)、next_action(「提交 ACT-GATE-03 评审放行」)、current_stage 写 `gate03`。', + '规则版本随结论落账:每条 P-11 主记录与差异明细都写 rule_version(工具出参的 ruleVersion),中途规则更新不影响进行中的评估。', + '被打回重做时改的是**台账本身**(理由 / 依据 / 去向),不是把同一份原样再递一遍。', + ].join('\n'), + crystallized: [], + knowledgeRefs: ['analysis_status 与 customer_approval_status 分离'], + version: '1.0.0', + }, +]; diff --git a/activities/part-substitution-assessment/src/views/prd-style.ts b/activities/part-substitution-assessment/src/views/prd-style.ts new file mode 100644 index 0000000..4f252b3 --- /dev/null +++ b/activities/part-substitution-assessment/src/views/prd-style.ts @@ -0,0 +1,158 @@ +/** + * ACT-WORK-05 差异评估 · 视觉底 —— 逐字节抄自 I01(`brand-content-toolkit/src/views/prd-style.ts`)。 + * + * 本活动的四份目标件(`docs/SS02-ACT-WORK-05-html/*.html`)与 E04 / I01 的目标件是同一套 + * 设计语言产出的(卡壳 / 卡头渐变 / 判据框 / 表格排版全同);`activities/*` 只准 import + * `@sdk` / `@contracts`,包之间没有代码通道,只能各自持有一份。改这份不要改口径: + * 数据驱动的开关(三档占位 / 缺项红带 / 状态徽 / 编辑闸)在 `report.ts` 的 `BIND_CSS`。 + */ + +export const PRD_CSS = ` +.proto-root { + --ink:#1f2329; --ink-2:#41464e; --ink-3:#646a73; --ink-4:#8f959e; + --line:#dee0e3; --line-soft:#e8eaed; --soft:#f8f9fb; + --brand:#3370ff; --brand-deep:#245bdb; --brand-soft:#e1eaff; --brand-wash:#f0f4ff; + --human:#a9711e; --human-soft:#f6ecd8; --human-line:#e6d3a8; + --ledger:#b23a2e; --ledger-soft:#f7e4e0; --ledger-line:#ecc5bd; + --ok:#278717; --ok-soft:#e8f8e4; + --danger:#d83931; --danger-soft:#feeae9; + --warn-accent:#a9711e; --warn-soft:#f6ecd8; + --mono: "SF Mono", Menlo, Consolas, monospace; + --t-fast: .16s; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; + color: var(--ink); + font-size: 14px; + line-height: 1.7; +} +.proto-root, .proto-root * { box-sizing: border-box; } + +/* ==================== 卡壳(嵌进工作区,不自带浮起——外层已经是一张卡)==================== */ +.proto-root .jc-report { background: #fff; overflow: hidden; } + +.proto-root .jc-rp-head { display: flex; align-items: center; gap: 14px; padding: 18px 22px; border-bottom: 2px solid var(--ink); } +.proto-root .jc-report.tone-human .jc-rp-head { background: linear-gradient(120deg, #fffaf1 0%, #fff 55%, #fbf6ea 100%); } +.proto-root .jc-report.tone-ledger .jc-rp-head { background: linear-gradient(120deg, #fdf3f1 0%, #fff 55%, #fbf2f0 100%); } + +.proto-root .jc-rp-badge { + flex: none; width: 44px; height: 44px; border-radius: 10px; + display: grid; place-items: center; color: #fff; font-weight: 800; font-size: 13px; +} +.proto-root .jc-report.tone-human .jc-rp-badge { background: var(--human); box-shadow: 0 6px 16px rgba(169,113,30,.25); } +.proto-root .jc-report.tone-ledger .jc-rp-badge { background: var(--ledger); box-shadow: 0 6px 16px rgba(178,58,46,.25); } + +.proto-root .jc-rp-tit { flex: 1; min-width: 0; } +.proto-root .jc-rp-tit em { font-style: normal; font-size: 11px; letter-spacing: .08em; color: var(--ink-4); font-weight: 700; } +.proto-root .jc-rp-tit h3 { margin: 2px 0 0; font-size: 19px; font-weight: 800; line-height: 1.3; } +.proto-root .jc-rp-sub { font-size: 12px; color: var(--ink-3); margin-top: 3px; } + +.proto-root .jc-rp-side { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex: none; } +/* 目标件 side 第二行:步骤(静态文案),同目标件 .step 的字号字色 */ +.proto-root .jc-rp-step { font-size: 12px; color: var(--ink-3); } +/* 关联任务号:全长 UUID 如实给,排版上截尾 —— 截断是排版,不是数据 */ +.proto-root .jc-rp-taskid { + display: inline-block; max-width: 150px; overflow: hidden; text-overflow: ellipsis; + white-space: nowrap; vertical-align: bottom; font-family: var(--mono); font-size: 11px; +} +.proto-root .jc-rp-tag { font-size: 11px; font-weight: 800; padding: 2px 10px; border-radius: 999px; border: 1px solid; } +.proto-root .jc-report.tone-human .jc-rp-tag { color: var(--human); background: var(--human-soft); border-color: var(--human-line); } +.proto-root .jc-report.tone-ledger .jc-rp-tag { color: var(--ledger); background: var(--ledger-soft); border-color: var(--ledger-line); } +.proto-root .jc-rp-wait { font-size: 12px; color: var(--ink-3); } +.proto-root .jc-rp-wait.warn { color: var(--danger); font-weight: 700; } + +.proto-root .jc-rp-bd { padding: 20px 22px; } + +/* 【做什么】/【交回什么】这类分区小标——括号label + 破折号说明,同一行 */ +.proto-root .jc-rp-part { font-size: 11px; font-weight: 800; letter-spacing: .12em; color: var(--ink-4); margin: 20px 0 8px; } +.proto-root .jc-rp-part:first-child { margin-top: 0; } +.proto-root .jc-rp-part b { color: var(--ink-3); } + +/* AI 已完成 / 需要你 —— 说明框,左侧色条按 tone 走 */ +.proto-root .jc-derive { border: 1px solid var(--line); border-left: 3px solid; border-radius: 8px; background: var(--soft); padding: 14px 16px; margin-bottom: 16px; } +.proto-root .jc-report.tone-human .jc-derive { border-left-color: var(--human); } +.proto-root .jc-report.tone-ledger .jc-derive { border-left-color: var(--ledger); } +.proto-root .jc-derive p { margin: 0; font-size: 13px; color: var(--ink-2); } +.proto-root .jc-derive p + p { margin-top: 6px; } +.proto-root .jc-derive b { color: var(--ink); } +.proto-root .jc-derive .done { color: var(--ok); font-weight: 700; } +.proto-root .jc-report.tone-human .jc-derive .need { color: var(--human); font-weight: 700; } +.proto-root .jc-report.tone-ledger .jc-derive .need { color: var(--ledger); font-weight: 700; } + +/* 验收判据框——目标件里两份验收工单同一色(赭红),不随 tone 变 */ +.proto-root .jc-criteria { border: 1px solid var(--ledger-line); border-radius: 10px; background: var(--ledger-soft); padding: 13px 16px; margin-bottom: 18px; } +.proto-root .jc-criteria-t { font-size: 11px; font-weight: 800; letter-spacing: .08em; color: var(--ledger); margin-bottom: 7px; } +.proto-root .jc-criteria ul { margin: 0; padding-left: 18px; font-size: 12.5px; color: var(--ink-2); } +.proto-root .jc-criteria li { margin: 4px 0; } +.proto-root .jc-criteria b { color: var(--ink); } + +/* 表格基础排版(交互态的显隐开关在 BIND_CSS) */ +/* + * **表宽超出就得能横着滑**(2026-08-23 用户:「表格已经显示不全了,不能左右滑动」)。 + * 从前这里是 overflow: hidden —— 那是为了让圆角裁住表头,可它同时把超出的列 + * **直接裁掉且滑不动**:列一多(或某列为了放下下拉框加宽)就看不全,人还没有任何办法。 + * quote-* 各包一直是 overflow-x: auto,这两个新包抄版式时把这一条抄丢了。 + * (注释里不写反引号 —— 整段 CSS 是模版字符串,一个反引号就把它截断,本轮踩第三次。) + * 竖向仍 hidden:件是整篇铺开跟着外层滚的,表格自己不该再套一层竖滚动条。 + */ +.proto-root .jc-tbl-wrap { border: 1px solid var(--line-soft); border-radius: 10px; overflow-x: auto; overflow-y: hidden; margin-bottom: 4px; } + +/* + * 资料名 = 可点开的那一格(2026-08-23)。**看得出能点**才会有人点: + * 品牌蓝 + 下划虚线 + 手型;悬停实线。点了去哪由外层接(件里一行脚本也没有)。 + */ +.proto-root .jc-fileln { color: var(--brand); border-bottom: 1px dashed var(--brand-soft); cursor: pointer; transition: all var(--t-fast); } +.proto-root .jc-fileln:hover { border-bottom-style: solid; background: var(--brand-wash); } +/* 空标题不该长得像能点开 —— 那一格根本没有资料 */ +.proto-root .jc-fileln:empty { border-bottom: 0; cursor: default; background: none; } + +/* 格式徽:摆的是这份资料交出去时的样子(md / html / docx / xlsx) */ +.proto-root .jc-fmt { display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 19px; + padding: 1px 7px; border-radius: 5px; font-size: 10px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; + background: var(--soft); color: var(--ink-3); border: 1px solid var(--line); } +.proto-root .jc-fmt.v-docx { background: #eaf1ff; color: #2b5fd9; border-color: #c3d6ff; } +.proto-root .jc-fmt.v-xlsx { background: #e9f7ee; color: #1f7a45; border-color: #bfe6cd; } +.proto-root .jc-fmt.v-html { background: #fdf0e6; color: #b25e15; border-color: #f3d3b4; } +.proto-root .jc-fmt.v-md { background: #f2f3f5; color: #5a6068; border-color: #dfe1e5; } +.proto-root .jc-tbl-in { width: 100%; border-collapse: collapse; font-size: 13px; } +.proto-root .jc-tbl-in th { text-align: left; background: var(--soft); font-size: 11px; letter-spacing: .04em; font-weight: 800; color: var(--ink-3); padding: 9px 11px; border-bottom: 1px solid var(--line); } +.proto-root .jc-tbl-in th.req::after { content: " *"; color: var(--danger); font-size: 11px; } +.proto-root .jc-tbl-in th.sug::after { content: " 建议"; color: var(--warn-accent); font-size: 10px; font-weight: 600; } +.proto-root .jc-tbl-in th.opt::after { content: " 选填"; color: var(--ink-4); font-size: 10px; font-weight: 600; } +.proto-root .jc-tbl-in td { padding: 9px 11px; border-bottom: 1px solid var(--line-soft); vertical-align: top; } +.proto-root .jc-tbl-in tr:last-child td { border-bottom: 0; } +.proto-root .jc-tbl-in td.jc-rn { width: 38px; white-space: nowrap; font-family: var(--mono); font-size: 11px; color: var(--ink-4); text-align: center; position: relative; } + +.proto-root .jc-vcell { display: block; font-size: 12.5px; color: var(--ink); line-height: 1.5; overflow-wrap: anywhere; } +.proto-root .jc-vcell.mono { font-family: var(--mono); font-size: 11.5px; color: var(--ink-2); } +.proto-root .jc-vcell.cust { font-weight: 700; } +.proto-root .jc-vcell.dim { color: var(--ink-4); text-decoration: line-through; } +.proto-root .jc-diffnew { font-weight: 800; color: var(--ok); } + +.proto-root .jc-cell, .proto-root select.jc-cell, .proto-root textarea.jc-cell { + width: 100%; min-width: 60px; border: 1px solid var(--line); border-radius: 6px; + padding: 6px 8px; font: inherit; font-size: 12.5px; color: var(--ink); background: #fff; +} +.proto-root .jc-cell.mono { font-family: var(--mono); font-size: 11.5px; } +.proto-root .jc-cell:focus { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-wash); outline: none; } + +.proto-root .jc-rowdel { + width: 22px; height: 22px; margin: 0 auto; cursor: pointer; + border: 1px solid var(--line); border-radius: 6px; padding: 0; + font: inherit; font-size: 14px; line-height: 1; color: var(--ink-3); background: #fff; + transition: border-color var(--t-fast), color var(--t-fast), background var(--t-fast); +} +.proto-root .jc-rowdel:hover { border-color: var(--danger); color: var(--danger); background: var(--danger-soft); } + +.proto-root .jc-addrow em { font-style: normal; font-size: 11.5px; color: var(--ink-4); } +.proto-root .jc-addrow button { + cursor: pointer; border: 1px solid var(--brand-soft); border-radius: 7px; padding: 5px 12px; + font: inherit; font-size: 12px; font-weight: 700; color: var(--brand-deep); background: var(--brand-wash); +} +.proto-root .jc-addrow button:hover { background: var(--brand-soft); } + +/* 本工单只承载内容,提交/审批请在对话区——目标件三份文件都有这一条 */ +.proto-root .jc-flow { margin-top: 16px; padding: 9px 12px; border-radius: 8px; background: var(--soft); border: 1px dashed var(--line); font-size: 12px; color: var(--ink-3); } +.proto-root .jc-flow b { color: var(--ink); } + +.proto-root .jc-rp-foot { padding: 13px 22px; border-top: 1px solid var(--line-soft); font-size: 11.5px; color: var(--ink-4); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px; } +.proto-root .jc-rp-foot span:last-child { font-family: var(--mono); } +`; diff --git a/activities/part-substitution-assessment/src/views/report.ts b/activities/part-substitution-assessment/src/views/report.ts new file mode 100644 index 0000000..e03127b --- /dev/null +++ b/activities/part-substitution-assessment/src/views/report.ts @@ -0,0 +1,5 @@ +export const P08_MASTER_CREATE = '
'; +export const P10_DOCUMENT_CREATE = '
'; +export const EVAL_INPUT_LIST = '
'; +export const DIFFERENCE_REVIEW = '
'; +export const LANDING_CONFIRM = '
'; From f7fe8e383660b6426b3f3e743795af2dd2ba2eea Mon Sep 17 00:00:00 2001 From: xcs Date: Wed, 26 Aug 2026 23:37:16 +0800 Subject: [PATCH 04/21] =?UTF-8?q?feat(w05):=20=E5=B7=AE=E5=BC=82=E8=AF=84?= =?UTF-8?q?=E4=BC=B0=E6=A1=88=E4=BE=8B=E6=A8=A1=E5=9E=8B=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20CMMN=20=E5=85=AB=E9=A1=B9=E4=B8=83=E5=93=A8?= =?UTF-8?q?=E5=85=B5=E3=80=81=E8=AF=B4=E6=98=8E=E4=B9=A6=E4=BA=94=E5=8D=A1?= =?UTF-8?q?=E9=AA=A8=E6=9E=B6=E3=80=81=E5=85=AB=E6=9D=A1=E5=89=A7=E6=9C=AC?= =?UTF-8?q?=E3=80=81=E4=BA=94=E6=AE=B5=E8=BF=9B=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh --- .../__tests__/activity.test.ts | 56 ++ .../__tests__/steps.test.ts | 169 ++++++ .../src/case-model.ts | 488 +++++++++++++++++- 3 files changed, 707 insertions(+), 6 deletions(-) create mode 100644 activities/part-substitution-assessment/__tests__/steps.test.ts diff --git a/activities/part-substitution-assessment/__tests__/activity.test.ts b/activities/part-substitution-assessment/__tests__/activity.test.ts index 089437b..21590d3 100644 --- a/activities/part-substitution-assessment/__tests__/activity.test.ts +++ b/activities/part-substitution-assessment/__tests__/activity.test.ts @@ -11,6 +11,8 @@ import { activityManifestSchema } from '@contracts'; import { manifest, D, RULE_VERSION } from '../src/manifest.ts'; import activity from '../src/index.ts'; +const ITEMS = ['S0_bind', 'S0d_master_data', 'S1_input_confirm', 'S2_normalize', 'S3_assess', 'S4_review', 'S5_prepare_landing', 'T_release']; +const NO_DONE_WHEN = new Set(['S0d_master_data', 'S1_input_confirm', 'S4_review', 'T_release']); const VIEWS = ['p08-master-create', 'p10-document-create', 'eval-input-list', 'difference-review', 'landing-confirm']; const byAddress = (domain: string, object: string) => manifest.declarations.ledger.find((l) => l.domain === domain && l.object === object); @@ -121,6 +123,60 @@ describe('台账声明:授权面与《统一方案》口径', () => { }); }); +describe('案例模型与清单的对齐', () => { + it('八个计划项,剧本 item 与 CMMN humanTask id 一一对应', () => { + const cmmnIds = [...manifest.assets.businessModel.cmmnXml.matchAll(/ m[1] ?? ''); + expect(cmmnIds).toEqual(ITEMS); + expect((manifest.assets.businessModel.steps as ReadonlyArray<{ item: string }>).map((s) => s.item)).toEqual(ITEMS); + }); + + it('四处不给 doneWhen(数据工单 / 输入确认 / 审核 / 放行),其余四处都给', () => { + for (const s of manifest.assets.businessModel.steps as ReadonlyArray<{ item: string; doneWhen?: string }>) { + if (NO_DONE_WHEN.has(s.item)) expect(s.doneWhen, s.item).toBeUndefined(); + else expect(s.doneWhen, s.item).toBeDefined(); + } + }); + + it('那四项在 CMMN 里都标了人工激活', () => { + for (const id of NO_DONE_WHEN) { + const block = new RegExp(``).exec(manifest.assets.businessModel.cmmnXml)?.[0] ?? ''; + expect(block, id).toContain('manualActivationRule'); + } + }); + + it('除放行外每一项恰好属于一个阶段;5 段 + 放行 = x/6,与文档 0.5/1/2/3/4/5 六步对齐', () => { + const phases = manifest.assets.businessModel.phases ?? []; + expect(phases).toHaveLength(5); + const phaseItems = phases.flatMap((p) => p.items); + expect([...phaseItems].sort()).toEqual([...ITEMS.filter((i) => i !== 'T_release')].sort()); + expect(new Set(phaseItems).size).toBe(phaseItems.length); + }); + + it('说明书点名的计划项都真实存在', () => { + const mentioned = [...manifest.assets.businessModel.narrative.matchAll(/\b(S\d[a-z]?_[a-zA-Z_]+|T_release)\b/g)].map((m) => m[1] ?? ''); + for (const name of mentioned) expect(ITEMS, `说明书提到但不存在的计划项: ${name}`).toContain(name); + }); + + it('两种决策卡骨架都在说明书里:候选放弃/创建(create/abandon)与审核(approve/reject/custom:)', () => { + const n = manifest.assets.businessModel.narrative; + expect(n).toContain('"kind":"decide","itemId":"S0d_master_data"'); + expect(n).toContain('"value":"create"'); + expect(n).toContain('"value":"abandon"'); + expect(n).toContain('"kind":"decide","itemId":"S4_review"'); + expect(n).toContain('"approve"'); + expect(n).toContain('"reject"'); + expect(n).toContain('"custom:"'); + }); + + it('T_release 剧本里 judge criterion 的原文逐字对得上 acceptanceCriteria 的 label', () => { + const tRelease = (manifest.assets.businessModel.steps as ReadonlyArray<{ item: string; guide?: string }>).find((s) => s.item === 'T_release'); + const quoted = [...(tRelease?.guide ?? '').matchAll(/judge criterion="([^"]+)"/g)].map((m) => m[1] ?? ''); + expect(quoted).toHaveLength(3); + const labels = new Set(manifest.contract.acceptanceCriteria.map((c) => c.label)); + for (const q of quoted) expect(labels.has(q), q).toBe(true); + }); +}); + describe('活动入口', () => { it('没有 run —— 行为全在清单里', () => { expect(activity.run).toBeUndefined(); diff --git a/activities/part-substitution-assessment/__tests__/steps.test.ts b/activities/part-substitution-assessment/__tests__/steps.test.ts new file mode 100644 index 0000000..b9df010 --- /dev/null +++ b/activities/part-substitution-assessment/__tests__/steps.test.ts @@ -0,0 +1,169 @@ +/** + * 步骤剧本与呈现面 —— 这段错了不报错,只是活动开始转圈 / 界面开始撒谎(与 E04 / I01 同一套惯例)。 + */ +import { describe, expect, it } from 'vitest'; +import { evaluateCondition } from '@sfeel'; +import { ASSESSMENT_PHASES, ASSESSMENT_STEPS, CMMN_XML, NARRATIVE } from '../src/case-model.ts'; +import { manifest, D } from '../src/manifest.ts'; + +const ITEM_IDS = [...CMMN_XML.matchAll(/<(?:humanTask|decisionTask|stage|milestone)\s+id="([^"]+)"/g)].map((m) => m[1] ?? ''); + +function doneOf(item: string): string { + const found = ASSESSMENT_STEPS.find((s) => s.item === item)?.doneWhen; + if (found === undefined) throw new Error(`${item} 没有 doneWhen`); + return found; +} +/** 谓词写坏了引擎当作「没算出来」——测里也照这个口径 */ +function safeEval(expression: string, scope: Record): boolean { + try { + return evaluateCondition(expression, scope); + } catch { + return false; + } +} +/** 开局局面:入参 + 所有基座读数归零 */ +const fresh = (input: Record): Record => ({ + ...input, + demand_request_rows: 0, + demand_request_missing: 0, + [`${D}_verdict_rows`]: 0, + [`${D}_pn_check_rows`]: 0, + [`${D}_pn_check_missing`]: 0, + [`${D}_pn_check_exists_status_absent`]: 0, + [`${D}_pn_check_exists_status_abandoned`]: 0, + [`${D}_input_gap_rows`]: 0, + [`${D}_input_gap_missing`]: 0, + substitution_relation_rows: 0, + substitution_relation_missing: 0, + substitution_relation_analysis_status_fae_confirmed: 0, + substitution_difference_item_rows: 0, + substitution_difference_item_missing: 0, + substitution_difference_item_distinct_substitution_relation_id: 0, + substitution_difference_item_difference_status_match: 0, + substitution_difference_item_difference_status_mismatch: 0, + substitution_difference_item_difference_status_unknown: 0, + substitution_difference_item_difference_status_conditional: 0, + tool_part_substitution_assessment_compare_parameters_calls: 0, + tool_part_substitution_assessment_recommend_calls: 0, +}); + +describe('剧本与案例模型对得上', () => { + it('每一条剧本都指向一个真实存在的计划项,图上每一步也都有剧本', () => { + for (const step of ASSESSMENT_STEPS) expect(ITEM_IDS, `剧本里的 ${step.item} 在 CMMN 里不存在`).toContain(step.item); + for (const id of ITEM_IDS) expect(ASSESSMENT_STEPS.map((s) => s.item), `计划项 ${id} 没有剧本`).toContain(id); + }); + + it('清单里挂的就是这份剧本与这份阶段', () => { + expect(manifest.assets.businessModel.steps).toBe(ASSESSMENT_STEPS); + expect(manifest.assets.businessModel.phases).toBe(ASSESSMENT_PHASES); + }); +}); + +describe('剧本声明的读写面 ⊆ 清单声明', () => { + const declaredAddresses = new Set(manifest.declarations.ledger.map((d) => `${d.domain}.${d.object}`)); + const declaredTools = new Set(manifest.declarations.tools.map((t) => t.name)); + + it('steps[].writes / reads(台账那几个)全落在声明过的对象上', () => { + for (const step of ASSESSMENT_STEPS) { + for (const address of step.writes) expect(declaredAddresses.has(address), `${step.item} 要写 ${address}`).toBe(true); + for (const address of step.reads.filter((r) => r.includes('.') && !r.startsWith('case:'))) { + expect(declaredAddresses.has(address), `${step.item} 要读 ${address}`).toBe(true); + } + } + }); + + it('steps[].tools 全部是声明过的工具;只有 S3 调工具', () => { + for (const step of ASSESSMENT_STEPS) { + for (const tool of step.tools) expect(declaredTools.has(tool), `${step.item} 要调 ${tool}`).toBe(true); + if (step.item !== 'S3_assess') expect(step.tools, step.item).toEqual([]); + } + expect(ASSESSMENT_STEPS.find((s) => s.item === 'S3_assess')?.tools).toHaveLength(2); + }); + + it('customer_approval_status 在任何剧本里都不许写(只读边界)', () => { + for (const step of ASSESSMENT_STEPS) expect(step.guide, step.item).not.toMatch(/customer_approval_status\s*=/); + }); +}); + +describe('doneWhen —— 开局不许真,干完必须真', () => { + it('**开局一步都不该被自动收掉** —— 五条样例的入参形状逐一试', () => { + for (const sample of manifest.samples) { + for (const step of ASSESSMENT_STEPS) { + if (step.doneWhen === undefined) continue; + expect(safeEval(step.doneWhen, fresh(sample.input as Record)), `样例「${sample.name}」下 ${step.item} 开局就为真`).toBe(false); + } + } + }); + + it('S0:两颗候选 + 原 PN = 3 行核对齐、需求与结论都写了 → 收得掉;少写一颗 → 收不掉', () => { + const base = { candidateInternalPns: ['XL2596S-5.0', 'LM2596S-ADJ'], demand_request_rows: 1, demand_request_missing: 0, [`${D}_verdict_rows`]: 1, [`${D}_pn_check_missing`]: 0 }; + expect(safeEval(doneOf('S0_bind'), { ...base, [`${D}_pn_check_rows`]: 3 })).toBe(true); + expect(safeEval(doneOf('S0_bind'), { ...base, [`${D}_pn_check_rows`]: 2 })).toBe(false); + }); + + it('S2:未放弃的候选各一条关系且每条都有明细 → 收得掉;放弃一颗后关系数随之减一', () => { + const two = { candidateInternalPns: ['A', 'B'], [`${D}_pn_check_exists_status_abandoned`]: 0, substitution_difference_item_rows: 14 }; + expect(safeEval(doneOf('S2_normalize'), { ...two, substitution_relation_rows: 2, substitution_difference_item_distinct_substitution_relation_id: 2 })).toBe(true); + expect(safeEval(doneOf('S2_normalize'), { ...two, substitution_relation_rows: 2, substitution_difference_item_distinct_substitution_relation_id: 1 })).toBe(false); + expect(safeEval(doneOf('S2_normalize'), { ...two, [`${D}_pn_check_exists_status_abandoned`]: 1, substitution_relation_rows: 1, substitution_difference_item_distinct_substitution_relation_id: 1 })).toBe(true); + }); + + it('S3:四态之和 = 明细行数、必填齐、两枚工具都调过 → 收得掉;工具没调过 → 收不掉', () => { + const ok = { + substitution_difference_item_missing: 0, + substitution_relation_missing: 0, + substitution_difference_item_rows: 7, + substitution_difference_item_difference_status_match: 4, + substitution_difference_item_difference_status_mismatch: 1, + substitution_difference_item_difference_status_unknown: 1, + substitution_difference_item_difference_status_conditional: 1, + tool_part_substitution_assessment_compare_parameters_calls: 1, + tool_part_substitution_assessment_recommend_calls: 1, + }; + expect(safeEval(doneOf('S3_assess'), ok)).toBe(true); + expect(safeEval(doneOf('S3_assess'), { ...ok, tool_part_substitution_assessment_recommend_calls: 0 })).toBe(false); + expect(safeEval(doneOf('S3_assess'), { ...ok, substitution_difference_item_difference_status_match: 3 })).toBe(false); + }); + + it('S5:旗 + 全部关系 FAE 确认 + P-01 回写齐 → 收得掉;缺一头都收不掉', () => { + const ok = { landing_prepared_total: 1, substitution_relation_analysis_status_fae_confirmed: 2, substitution_relation_rows: 2, substitution_relation_missing: 0, demand_request_missing: 0 }; + expect(safeEval(doneOf('S5_prepare_landing'), ok)).toBe(true); + expect(safeEval(doneOf('S5_prepare_landing'), { ...ok, substitution_relation_analysis_status_fae_confirmed: 1 })).toBe(false); + expect(safeEval(doneOf('S5_prepare_landing'), { ...ok, demand_request_missing: 1 })).toBe(false); + }); +}); + +describe('说明书与剧本的骨架', () => { + it('五张卡的骨架都住在说明书里(压拍之后只有说明书还在)', () => { + expect(NARRATIVE).toContain('"kind":"entry","itemId":"S0_bind"'); + expect(NARRATIVE).toContain('"kind":"decide","itemId":"S0d_master_data"'); + expect(NARRATIVE).toContain('"title":"数据工单 · 芯片主数据创建(P-08)'); + expect(NARRATIVE).toContain('"title":"数据工单 · 原厂技术资料创建(P-10)'); + expect(NARRATIVE).toContain('"title":"评估输入与资料补齐确认'); + expect(NARRATIVE).toContain('"kind":"decide","itemId":"S4_review"'); + }); + + it('剧本里不引反例原话;S3 固定三拍且写明「工具后同拍作废」', () => { + const s3 = ASSESSMENT_STEPS.find((s) => s.item === 'S3_assess')?.guide ?? ''; + expect(s3).toContain('第一拍'); + expect(s3).toContain('第二拍'); + expect(s3).toContain('第三拍'); + expect(s3).toContain('工具调用后同拍其余动作作废'); + }); + + it('S0 剧本写明库里都有就 disable S0d;S0d 剧本写明候选 ≥2 才问、=1 必建、原 PN 硬阻塞', () => { + const s0 = ASSESSMENT_STEPS.find((s) => s.item === 'S0_bind')?.guide ?? ''; + expect(s0).toContain('disable S0d_master_data'); + const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_master_data')?.guide ?? ''; + expect(s0d).toContain('硬阻塞'); + expect(s0d).toContain('候选只有 1 颗'); + }); + + it('五段进度,每段名 ≤10 字;除放行外每项恰属一段', () => { + expect(ASSESSMENT_PHASES).toHaveLength(5); + for (const p of ASSESSMENT_PHASES) expect(p.name.length, p.name).toBeLessThanOrEqual(10); + const all = ASSESSMENT_PHASES.flatMap((p) => p.items); + expect(new Set(all).size).toBe(all.length); + expect([...all].sort()).toEqual([...ITEM_IDS.filter((i) => i !== 'T_release')].sort()); + }); +}); diff --git a/activities/part-substitution-assessment/src/case-model.ts b/activities/part-substitution-assessment/src/case-model.ts index a12f581..3afede6 100644 --- a/activities/part-substitution-assessment/src/case-model.ts +++ b/activities/part-substitution-assessment/src/case-model.ts @@ -1,13 +1,489 @@ -/** 占位:真正的案例模型见 Task 4(整份替换本文件)。这里只放一份 schema 过得去的最小件。 */ +/** + * ACT-WORK-05 差异评估的案例模型 —— **双表示同版本**:CMMN XML(机器面)与案例说明书(叙事面)。 + * + * 业务出处:`docs/SS02-ACT-WORK-05-html/ACT-WORK-05-活动流程与工单说明.md`(步骤 0.5 + 5 步, + * 1 数据(条件)+ 1 执行 + 2 验收)与《SS02-SS03-SS04_Demo台账字段细化与统一方案》(字段口径)。 + * + * 文档六步收成 8 个案例模型项: + * 启动期 → S0_bind(AI:铸案号、建 P-01 V1、锁规则版本、逐个 PN 查库、写存在性核对表) + * 0.5 等待数据 → S0d_master_data(人:数据工单 —— P-08 一张、P-10 一张;候选 ≥2 缺库先决策卡; + * 库里都有就 disable 这一项) + * 1 建立评估上下文 → S1_input_confirm(人:执行工单 01,评估输入清单,件上补缺口) + * 2 读取并标准化 → S2_normalize(AI:P-11 主记录骨架 + 差异明细对照行) + * 3 逐项差异评估 → S3_assess(AI + 两枚工具:四态 / 风险 / 证据 / 影响 / 推荐等级 / 验证任务) + * 4 FAE/RI 审核 → S4_review(人:验收工单 01 = 决策卡循环,驳回则 AI 改台账再发) + * 5 提交评审放行 → S5_prepare_landing(AI:analysis_status 推 fae_confirmed、回写 P-01)+ T_release(人:验收工单 02,终局放行) + * + * 四处不给 doneWhen:S0d(触达人)、S1(交人补缺口)、S4(拍板且可循环)、T_release(签下去)。 + */ +import { D, RULE_VERSION, TOOL_COMPARE, TOOL_RECOMMEND } from './constants.ts'; + export const CMMN_XML = ` - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + update + demand_request_rows > 0 and sub_eval_pn_check_rows > 0 + + + + update + sub_eval_pn_check_rows = count(candidateInternalPns) + 1 and sub_eval_pn_check_missing = 0 and sub_eval_pn_check_exists_status_absent = 0 and sub_eval_pn_check_exists_status_abandoned < count(candidateInternalPns) and parts_profile_missing = 0 and parts_document_missing = 0 + + + + update + input_confirmed_total > 0 and sub_eval_input_gap_rows > 0 and sub_eval_input_gap_missing = 0 + + + + update + substitution_relation_rows > 0 and substitution_relation_rows = count(candidateInternalPns) - sub_eval_pn_check_exists_status_abandoned and substitution_difference_item_rows > 0 and substitution_difference_item_distinct_substitution_relation_id = substitution_relation_rows + + + + update + substitution_difference_item_missing = 0 and substitution_relation_missing = 0 and substitution_difference_item_difference_status_match + substitution_difference_item_difference_status_mismatch + substitution_difference_item_difference_status_unknown + substitution_difference_item_difference_status_conditional = substitution_difference_item_rows and tool_part_substitution_assessment_compare_parameters_calls > 0 and tool_part_substitution_assessment_recommend_calls > 0 + + + + update + review_approved_total > 0 and substitution_difference_item_missing = 0 + + + + update + landing_prepared_total > 0 and substitution_relation_analysis_status_fae_confirmed = substitution_relation_rows and substitution_relation_missing = 0 and demand_request_missing = 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `; -export const NARRATIVE = '(占位:案例说明书见 Task 4)'; -export const ASSESSMENT_STEPS: Array<{ item: string; intent: string; guide: string; writes: string[]; reads: string[]; tools: string[]; view?: string; doneWhen?: string }> = []; -export const ASSESSMENT_PHASES: Array<{ name: string; items: string[] }> = []; + +/** + * 案例说明书 —— **只写整单一以贯之的事**(逐步做法归剧本)。 + * 卡骨架住在这里而不是剧本里(模型会压拍,压过之后只有说明书还在 —— A1/A6/I01 同一课)。 + */ +export const NARRATIVE = `§0 开案:**判断候选料号在技术上能不能替代原料号,逐参数给出可复核的差异结论,审核后落账**。 +入参: +· \`customerCode\` —— 客户编码(L-02);\`customerName\` 只在客户不在库时给。 +· \`requestDescription\` —— 客户需求原话,据它新建 P-01 需求主记录(版本 V1)。 +· \`originalInternalPn\` —— 原内部料号;\`candidateInternalPns\` —— 候选内部料号数组(≥1,逐个评估、各一条 P-11)。 +· \`datasheets\` —— 选填的真文件,基座已读成文本(每件 \`{name, text?, note}\`);有就拿它在数据工单里预填,没有就人全填。 + +**本活动只回答一件事:技术上哪种料能替**。不做数量 / 价格 / 交期 / 采购判断;客户批准状态由审批流维护,你没有那一列的写权。 + +**四项绑定后不可改**(客户 / 需求 / 原 PN / 候选数组):绑错只能退回启动人重新启动。 + +**能算的不问人也不自己判**:四态(MATCH / MISMATCH / UNKNOWN / CONDITIONAL)与推荐等级 +(推荐 / 条件推荐 / 备选 / 不推荐)全由两枚工具算 —— \`${TOOL_COMPARE}\`、\`${TOOL_RECOMMEND}\`, +一批一次。你负责对照表、证据引用、影响说明、UNKNOWN 的去向。**规则版本 \`${RULE_VERSION}\` 随每条结论落账**。 + +**UNKNOWN 是一等公民**:查不到的参数值写「未披露」,不许估、不许留空串冒充;每个 UNKNOWN 必须写去向 +(挂验证任务号或补来源动作)。硬约束 MISMATCH 不许改判,要改只能补证据后重比。 + +**存在性检查与数据工单**:启动时逐个料号在 \`parts.profile\` 里查(原 PN + 每个候选)。查无记录 = 「完全没有」, +要人从零建整条主数据 —— 发**数据工单**(一张表一张单:P-08 一张、P-10 一张);PN 在库但个别字段缺 = 「有而不全」, +归执行工单 01 补。原 PN 缺 = 硬阻塞必须建;候选缺:候选 ≥2 时先问人放弃还是创建,候选 = 1 时必须建。 +**库里都有就把 S0d_master_data disable 掉**(此单不走这条),别发空单。 + +**六步走**:绑定与存在性核对 → (等待数据)→ 人补评估输入 → 标准化对照表 → 逐项评估 → 人审结论(可循环)→ 拟落账 → 人放行。 + +【本单会用到的卡,全部照下面的骨架发,别自己另编】 + +① 新客户建档(只在 \`customerCode\` 在 \`customer.profile\` 里查无时,S0_bind 里发;编码用入参给的那个,人核名): +{"kind":"entry","itemId":"S0_bind","ask":"客户 不在客户档案里,请核对建档信息后确认", + "ledger":"customer.profile","key":{"customer_code":""}, + "fields":[ + {"key":"name","label":"客户名称","value":"","required":true}, + {"key":"industry","label":"所属行业"}, + {"key":"region","label":"区域"} + ]} +人确认那一下基座已把这一行落进草稿层,**你不许再 write 一遍 customer.profile**。 + +② 候选不在库、候选 ≥2 时,一颗一张决策卡(按候选数组顺序,一次只发一张;候选只有一颗时不问、直接建): +{"kind":"decide","itemId":"S0d_master_data", + "ask":"候选 在芯片主数据里查无记录。是从零建它的主数据(派数据工单),还是本轮放弃这颗候选?", + "options":[{"value":"create","label":"创建主数据(派数据工单)","recommended":true}, + {"value":"abandon","label":"放弃这颗候选"}],"allowCustom":true} +\`value\` 只有 \`"create"\` / \`"abandon"\` / \`"custom:"\` 开头三种:\`"custom:"\` 开头按放弃处理、\`note\` 就是放弃原因; +点了 \`"abandon"\` 没写原因,\`abandon_reason\` 写「负责人本轮放弃(未注明原因)」。 + +③ 数据工单 · P-08(所有要建的料号一起,一张单;零输入,填报全在工作区的件上): +{"kind":"human","itemId":"S0d_master_data","workOrder":"execution","card":{ + "title":"数据工单 · 芯片主数据创建(P-08):<料号列表,顿号分隔>", + "ask":"这些料号在芯片主数据里查无记录,请在工作区的件上把整条记录建齐(一颗一行,可加行)。", + "steps":["每一行:品牌、规格描述、封装、生命周期状态、标准化参数集为必填,缺一格确认不了", + "标准化参数集一行一参数:参数名 | 值 | 单位 | hard/soft | 说明;原厂未披露的值写「未披露」,它会进入 UNKNOWN 并挂验证任务", + "有 datasheet 的把引用与版本一起填(成对);制造商与品牌不一致时分开写", + "改完保存,回这张卡确认 —— 确认后这批主数据即入库,活动从等待数据唤醒"], + "standard":"每颗料号一行,必填列全齐;参数集至少含输入电压范围 / 输出电压 / 输出电流 / 工作温度 / 封装相关的硬约束项。", + "basis":[{"k":"触发原因","v":"按 internal_pn 查 P-08 查无记录(完全没有,不是字段缺失)"}, + {"k":"AI 预填","v":"<有 datasheet 就写「已按 datasheet 预填 N 项,请核对」,没有写「未上传 datasheet,请全填」>"}]}} + +④ 数据工单 · P-10(同一批料号的资料记录,一张单;在 P-08 那张确认之后再发): +{"kind":"human","itemId":"S0d_master_data","workOrder":"execution","card":{ + "title":"数据工单 · 原厂技术资料创建(P-10):<料号列表>", + "ask":"请把这些料号的 datasheet / 承诺函等资料记录补进原厂技术资料库(一份资料一行,可加行)。", + "steps":["资料类型、标题、版本、文件链接、资料状态为必填;证据定位写页码或章节,差异评估的证据引用会指向它", + "同一料号多份资料就多加几行,序号 01/02… 自己续", + "改完保存,回这张卡确认"], + "standard":"每颗料号至少一行 datasheet 记录,必填列全齐。", + "basis":[{"k":"触发原因","v":"按 internal_pn 查 P-10 查无记录"}]}} + +⑤ 执行工单 01 · 评估输入与资料补齐确认(S1_input_confirm;零输入): +{"kind":"human","itemId":"S1_input_confirm","workOrder":"execution","card":{ + "title":"评估输入与资料补齐确认:<原 PN> → 个候选", + "ask":"启动绑定的四项已锁定。请在工作区的件上补齐「待补 / 待确认」行的补充内容,默认值可改可确认。", + "steps":["应用场景、BOM 位是启动时未收的:必须补(写在「补充内容」列)", + "原厂未披露的参数(如使能阈值)补不到就写「挂验证任务」或「留 UNKNOWN」—— 补不了也是合法答案,不拦流程", + "替代类型 / 规则版本 / 负责人期限三行是系统默认,可改可不改", + "加一行 = 补充 AI 没列出的资料项;删一行 = 该项不适用;已绑定的四项不可改", + "改完保存,回这张卡提交"], + "standard":"每一条「待补」行的补充内容都有;补不了的写明理由。", + "basis":[{"k":"已绑定","v":"客户 · 需求 V1 · 原 PN <原 PN> · 候选 颗"}, + {"k":"缺口","v":" 项待补 / 待确认"}]}} + +⑥ 验收工单 01 · 差异评估结论审核(S4_review;决策卡循环,不是终局验收)—— 发之前先在轨迹里找有没有已答过、还没处理的 \`[card-reply]\`: +· \`value\` 是 \`"approve"\` → 写 \`${D}.verdict.review_approved_total = 1\`,finish 这一项,别再发卡; +· \`value\` 是 \`"reject"\` 或以 \`"custom:"\` 开头 → 都算驳回:\`"reject"\` 没写意见,**不许再发卡追问**;\`"custom:"\` 的 \`note\` 就是他的意见, + 按意见改 \`substitution.difference_item\` / \`substitution.relation\`(同 PK 覆盖写,不开新行),意见要挂验证的写一行 \`validation.record\`, + 然后**换一种问法**再发一张(措辞不换会被当成同一张卡静默吞掉); +· 没有 → 才发第一张: +{"kind":"decide","itemId":"S4_review","allowCustom":true, + "ask":"差异评估结论已算出: 个候选、共 条参数比对(MATCH a / MISMATCH b / UNKNOWN c / CONDITIONAL d),推荐等级 <逐候选一句>。请审核。", + "options":[{"value":"approve","label":"通过","recommended":true}, + {"value":"reject","label":"驳回(不写具体意见)"}]} +三个 \`value\` 只有 \`"approve"\` / \`"reject"\` / \`"custom:"\` 开头三种,别造第四种。 + +【一路上要说话】每一拍带一句 say,说业务的话:查了几颗料、缺几颗、对照了几项、四态各几条、推荐等级各是什么。到了要找人的那一拍,先说清这一轮办完了什么、为什么现在需要他。用件上那三个词(必填 / 建议 / 选填)说缺口,只说事实。 + +【怎么把一项干完 · 一拍连做】write 和 tool 都不会自动收步 —— 做完用 finish 宣告(有 doneWhen 的项引擎自己收)。 +动作排进同一拍的 actions 串一口气给;只有发卡 / 提交(挂起)必须停拍;tool 之后同拍其余动作作废。 +**常路 9–12 拍收口**(候选缺库那条路多两张数据工单、多两拍)。 + +§Z 结案:FAE/RI 在验收单上签字,系统把草稿合并进 P-08 / P-10 / P-01 / P-11 / P-06 正式版并发射 \`assessment-released\` 口。`; + +/** + * 步骤剧本 —— 一个计划项一条;`doneWhen` 是「成果已存在」的判据,不是「该不该做」。 + * 四处不给 doneWhen:S0d(发数据工单触达人)、S1(交人补缺口)、S4(拍板且循环)、T_release(签下去)。 + */ +export const ASSESSMENT_STEPS = [ + { + item: 'S0_bind', + intent: '铸案号、建需求 V1、锁规则版本、逐个料号查库、写存在性核对表', + writes: ['demand.request', `${D}.pn_check`, `${D}.verdict`, 'customer.profile'], + reads: ['customerCode', 'customerName', 'requestDescription', 'originalInternalPn', 'candidateInternalPns', 'datasheets', 'case:openedAt', 'customer.profile', 'parts.profile'], + view: 'eval-input-list', + tools: [], + /* + * 「该有几行」也进判据:原 PN + 每个候选各一行核对(count(candidateInternalPns) + 1); + * 少一行 = 一颗料从这一单里凭空消失。count() 对未绑定名回 1 —— candidateInternalPns 是必填入参,恒绑定。 + */ + doneWhen: + `demand_request_rows > 0 and demand_request_missing = 0 and ${D}_verdict_rows > 0 and ` + + `${D}_pn_check_rows = count(candidateInternalPns) + 1 and ${D}_pn_check_missing = 0`, + guide: `第一件事:**把这一单的上下文立起来**,一拍连做(客户不在库时多一张录入卡、多一拍)。 + +【先铸案号】\`eval_ref = 'W05-' + case:openedAt 按 YYYYMMDDHHmmss 格式化\`(\`case:openedAt\` 形如 "2026-08-26 21:03:15",去掉分隔符即得); +\`request_id = 'REQ-' + 同一串数字\`;\`input_snapshot_ref = 'SNAP-' + eval_ref\`。**不许从正式表里别单的案号里挑**,整单只有这一个 eval_ref。 + +【客户在不在库】在 \`customer.profile\` 里找 \`customerCode\`:在 → 直接用;不在 → 照说明书①发数据录入卡(名字用 \`customerName\`), +发完这一拍结束;人确认后这家客户就在草稿层里了,下一拍接着做。**客户不在库也不许自己 write customer.profile**。 + +【写 \`demand.request\` 一行(V1)】request_id、customer_code(= customerCode)、requirement_version = "V1"、request_type = "技术需求"、 +request_description(入参原话,不改写)、input_snapshot_ref、original_internal_pn、request_status = "pending"、current_activity = "ACT-WORK-05"、 +current_stage = "assessing"。application_scenario / bom_position **这一步不写**(启动时未收,S1 由人补)。 + +【逐个料号查库,写 \`${D}.pn_check\`】\`parts.profile\` 整表在你的作用域里,按 \`internal_pn\` 逐个核对:原 PN 一行(role = "original"), +每个候选一行(role = "candidate");在 → exists_status = "found";不在 → exists_status = "absent"(**这一格先写 absent,等数据工单确认后再改**)。 +一颗都不能少:数组里点了几颗候选,就写几行 + 原 PN 一行。note 选填(如「PCN 转 NRND」)。 + +【写 \`${D}.verdict\` 一行】eval_ref、request_id、rule_version = "${RULE_VERSION}"、original_internal_pn、candidate_total = 候选数组长度。 + +写完引擎自己收这一项。收完先看 \`${D}.pn_check\`:有 absent 的 → 下一项是 S0d_master_data(数据工单);一个 absent 都没有 → +**disable S0d_master_data**(reason 写「原 PN 与全部候选都在库,无需建主数据」),然后等 S1 那一项 Enabled。`, + }, + { + item: 'S0d_master_data', + intent: '料号不在库:派数据工单让人从零建整条主数据(P-08 一张、P-10 一张);候选 ≥2 先问放弃还是创建', + writes: ['parts.profile', 'parts.document', `${D}.pn_check`], + reads: [`${D}.pn_check`, 'candidateInternalPns', 'originalInternalPn', 'datasheets', 'parts.profile', 'parts.document'], + view: 'p08-master-create', + tools: [], + /* 触达人,不给 doneWhen;人工激活:发卡就算启动 */ + guide: `这一项只在 \`${D}.pn_check\` 里有 absent 行时走;都在库就 disable 它(见 S0 剧本)。**先分岔,再动作**: + +【先看轨迹】这一项里已经发过哪些卡、人答了什么(【当前这一步】下面那行引擎事实说得清楚): +· 原 PN absent → **硬阻塞**,不问人,直接进入「建」的名单; +· 候选 absent 且候选数 ≥2 → 每颗 absent 的候选照说明书②各发一张决策卡(一次一张,按数组顺序);人选 "abandon"/"custom:" 的, + 把那一行 \`${D}.pn_check\` 改成 exists_status = "abandoned" + abandon_reason(同 PK 覆盖写),这颗不再评估;选 "create" 的进「建」的名单; +· 候选 absent 且候选只有 1 颗 → 不问,必须建。 +**别把同一颗候选问两遍**:决策卡答过就按答案办。 + +【「建」的名单定了 → 先写骨架行,再发 P-08 数据工单】 +1. 给名单里每颗料写一行 \`parts.profile\` 骨架:key {internal_pn},patch 只带你确有依据的列 —— 有 \`datasheets[].text\` 就从里面读 + brand_name / specification_description / package / lifecycle_status(active/nrnd/eol/discontinued/risk 之一)/ normalized_parameter_set + (一行一参数:参数名 | 值 | 单位 | hard/soft | 说明,原厂没写的值写「未披露」)/ datasheet_ref + datasheet_version;没有 datasheet 就只写 internal_pn + 这一列(其余留给人填,**别编**)。 +2. 同一拍给每颗料写一行 \`parts.document\` 骨架:key {internal_pn, doc_seq: "01"},patch {document_type: "datasheet", document_status: "pending", + brand_name 有就写}。 +3. 同一拍发说明书③那张 P-08 数据工单(title 里列全料号),发完这一拍结束(挂起)。 + +【P-08 那张确认回来了】看轨迹里的 [card-reply]:再发说明书④那张 P-10 数据工单(只发一次),发完这一拍结束。 + +【P-10 那张也确认回来了】把名单里每颗料的 \`${D}.pn_check\` 改成 exists_status = "created"(同 PK 覆盖写),然后 finish 这一项。 +这时 S1_input_confirm 的入场前提(全部料号落定、本单建的主数据不缺必填)自己就成立了 —— 缺必填由提交闸兜着,卡回来时一定是齐的。 + +【这两张卡一单各只发一次】人交回来之后不许再发;答复里没有值(空 values)也算答过 —— 人在件上填的东西已经落草稿层。`, + }, + { + item: 'S1_input_confirm', + intent: '评估输入清单:绑定四项只读核对,缺口行交 PW 在件上补', + writes: [`${D}.input_gap`, `${D}.verdict`, 'demand.request'], + reads: [`${D}.input_gap`, `${D}.pn_check`, 'demand.request', 'parts.profile', 'parts.document', 'candidateInternalPns', 'originalInternalPn'], + view: 'eval-input-list', + tools: [], + /* 交人补缺口,不给 doneWhen;人工激活:发卡就算启动 */ + guide: `料号全部落定了,**该 PW 补评估输入了** —— 这是本活动唯一的执行工单,零输入。**先分岔**:【当前这一步】那行引擎事实说 +「从没给它发过卡」→ 走【发卡】;说「人已经被问过了」→ 走【收口】。 + +【发卡】先把评估输入清单写下来(\`${D}.input_gap\`,一行一项,item_key 用英文短键,input_item 用人读名),一拍连做: +· 已绑定四项(status = "ok",detail 写绑定值,origin = "ai"):item_key = customer / request / original_pn / candidates + (候选多颗就 candidate_1、candidate_2… 一颗一行,detail 写料号与在库状态); +· 待补两项(status = "gap",detail 写「启动时未收」):application_scenario「应用场景」(source_ledger = "P-01")、bom_position「BOM 位」(source_ledger = "P-09"); +· 默认可改三项(status = "default",detail 写默认值):substitution_type「替代类型」= "Pin-to-Pin"、rule_version「规则版本」= "${RULE_VERSION}"、owner「负责人 / 期限」= "按客户线默认 · 1 个工作日"; +· 待确认(status = "gap"):逐颗候选看 \`parts.profile.normalized_parameter_set\`,值是「未披露」的参数各一行(item_key = unknown_<料号>_<序号>, + input_item 如「XL2596S-5.0 使能阈值」,source_ledger = "P-08",detail 写「原厂未披露(有而不全)」); +· 齐备项(status = "ok"):原 PN 资料版本(source_ledger = "P-10",detail 写 datasheet 版本)。 +写完同一拍照说明书⑤发执行工单,发完这一拍结束(挂起)。 + +【收口】人交回之后只做三件事,然后 finish: +① 读 \`${D}.input_gap\` 现值:把 application_scenario / bom_position 两行的 supplement 抄进 \`demand.request\`(同 PK 覆盖写这两列; + supplement 写「补不了」「UNKNOWN」之类的,这两列不写);人加的行(origin = "human-added")同等作数,写进 technical_constraints 一句汇总。 +② 写 \`${D}.verdict.input_confirmed_total = 1\`。 +③ finish 这一项。**别在这一拍做对照表** —— 那是下一步的活。 + +【这张卡一单只发一次】人交回来之后不许再发;缺必填有提交闸兜着。`, + }, + { + item: 'S2_normalize', + intent: '读取并标准化:每个未放弃的候选一条 P-11 主记录骨架 + 逐参数对照行', + writes: ['substitution.relation', 'substitution.difference_item'], + reads: ['parts.profile', 'parts.document', 'demand.request', `${D}.pn_check`, `${D}.input_gap`, `${D}.verdict`, 'candidateInternalPns', 'originalInternalPn'], + view: 'difference-review', + tools: [], + doneWhen: + `substitution_relation_rows > 0 and substitution_relation_rows = count(candidateInternalPns) - ${D}_pn_check_exists_status_abandoned and ` + + 'substitution_difference_item_rows > 0 and substitution_difference_item_distinct_substitution_relation_id = substitution_relation_rows', + guide: `输入齐了,**把多来源资料整成可比对的对照表**,一拍连做(先写关系,再写明细 —— 明细引用关系 id,顺序反了会被拒)。 + +【每个未放弃的候选一条 \`substitution.relation\` 骨架】(\`${D}.pn_check\` 里 exists_status 是 found / created 的候选;abandoned 的不写) +substitution_relation_id = "REL-<案号数字串>-01/02…"(按候选数组顺序)、eval_ref、request_id、original_internal_pn、candidate_internal_pn、 +substitution_type = "pin_to_pin"(人在评估输入清单里改了替代类型就照改:功能替代 → functional、参数替代 → parametric)、 +analysis_status = "ai_draft"、rule_version = "${RULE_VERSION}"、bom_position(照 demand.request 现值抄,没有不写)。 +recommendation_level / recommendation_reason / difference_summary / risk_level **这一步一格都不写**(S3 的活)。 + +【每条关系逐参数写 \`substitution.difference_item\` 对照行】骨架按**原 PN 的参数集**(\`parts.profile.normalized_parameter_set\`,一行一参数 +「参数名 | 值 | 单位 | hard/soft | 说明」):原 PN 有的每个参数一行 —— difference_item_id = "<关系id>-P01/P02…"、parameter_name、 +original_value、candidate_value(候选参数集里同名那一行的值;候选没有这个参数 → 写「未披露」)、parameter_unit(以原 PN 为准;候选单位不同先换算, +换算不了原值照写、evidence_ref 里注明)、constraint_type(原 PN 那一行的 hard/soft;没标的按口径:封装 / 工艺 / 电压 / 电流 / 温度 / 阈值 hard,其余 soft)、 +rule_version = "${RULE_VERSION}"、evidence_ref(值从哪来:「P-08 · P-10 <版本>」;本单数据工单建的料号写「人工录入 · 数据工单 · P-08」)。 +另加两行**封装**与**生命周期**(constraint_type = hard / soft,值分别取 \`package\` 与 \`lifecycle_status\`)。 +difference_status / compare_result / risk_level / impact_statement / unknown_disposition **这一步不写**。 + +**缺参数不得伪造**:候选那一侧查不到就写「未披露」。写完引擎自己收这一项(每条关系都有明细行才算)。`, + }, + { + item: 'S3_assess', + intent: '逐项差异评估:四态与推荐等级由工具算,证据 / 影响 / UNKNOWN 去向由你写', + writes: ['substitution.relation', 'substitution.difference_item', 'validation.record', `${D}.verdict`], + reads: ['substitution.relation', 'substitution.difference_item', 'demand.request', `${D}.input_gap`, `${D}.verdict`], + view: 'difference-review', + tools: [TOOL_COMPARE, TOOL_RECOMMEND], + doneWhen: + 'substitution_difference_item_missing = 0 and substitution_relation_missing = 0 and ' + + 'substitution_difference_item_difference_status_match + substitution_difference_item_difference_status_mismatch + ' + + 'substitution_difference_item_difference_status_unknown + substitution_difference_item_difference_status_conditional = substitution_difference_item_rows and ' + + 'tool_part_substitution_assessment_compare_parameters_calls > 0 and tool_part_substitution_assessment_recommend_calls > 0', + guide: `对照表铺好了,**固定三拍**:比对 → 定级 → 写盘。**工具调用后同拍其余动作作废**,别把工具和写台账串在一起;两枚工具各只调一次。 + +【第一拍:只调 \`${TOOL_COMPARE}\`,别的什么都不写】把**全部**关系一次打包: +{"candidates":[{"candidateInternalPn":"<候选>","pairs":[{"parameterName":"输入电压范围","originalValue":"4.5~40","candidateValue":"4.5~32", + "unit":"V","constraintType":"hard","requirement":">= 40","evidenceRef":"P-08 · P-10 Rev H"}, …]}, …]} +· pairs 逐条照 \`substitution.difference_item\` 抄(parameterName / originalValue / candidateValue / unit / constraintType / evidenceRef); +· **客户在应用场景里给了边界的参数才写 requirement**(如「输入上限 40V」→ 输入电压范围那一对写 ">= 40";「工业温域」→ 工作温度写 "-40~85"); +· 每一对都带 evidenceRef —— 硬约束没有证据引用工具不判 MATCH,那正是要的。 +调完这一拍结束。 + +【第二拍:只调 \`${TOOL_RECOMMEND}\`】把第一拍出参里每个候选的 counts 原样传:{"candidates":[{"candidateInternalPn":"…","counts":{…}}, …]}。调完结束。 + +【第三拍:写盘,一拍连做,一次都不再调工具】 +① 逐条 \`substitution.difference_item\` 同 PK 覆盖写:difference_status / compare_result / risk_level 照第一拍出参**一字不改**; + impact_statement(MATCH 写「无」;其余说清对应用 / 验证 / 报价的影响,如「输入上限 32V < 客户 40V,不能上板」); + difference_status = unknown 的行**必须**写 unknown_disposition(「挂验证任务 VAL-<案号数字串>-01:实测使能阈值」或「向原厂补来源」);confidence(0~1)。 +② 每个 UNKNOWN 写一行 \`validation.record\`:validation_id = "VAL-<案号数字串>-01/02…"、request_id、validation_internal_pn(候选)、 + validation_type = "pin_to_pin"、validation_status = "not_started"、next_validation_action(要测什么、什么条件)、test_condition。 +③ 逐条 \`substitution.relation\` 同 PK 覆盖写:recommendation_level / risk_level 照第二拍出参、recommendation_reason(工具 reason 扩写成人话, + 引具体参数)、difference_summary(「7 项比对:4 MATCH / 1 MISMATCH / 1 UNKNOWN / 1 CONDITIONAL」)、analysis_report_ref = "RPT-<案号数字串>"、 + confidence_score(取该候选各行 confidence 的最小值);recommendation_level = conditional 的行**必须**写 verification_requirement(列出 VAL 号)、validation_id。 +④ \`${D}.verdict\` 同 PK 覆盖写:relation_total、recommended_total / conditional_total / alternate_total / not_recommended_total(数关系表)。 +写完引擎自己收这一项;下一项是审核(S4),**这一拍不要发卡**。 + +**不许把工具的结论改成别的**:不推荐就是不推荐;要改只能补证据后重比。`, + }, + { + item: 'S4_review', + intent: '差异评估结论审核:决策卡循环,通过或驳回 + AI 按意见改台账重发', + writes: ['substitution.relation', 'substitution.difference_item', 'validation.record', `${D}.verdict`], + reads: ['substitution.relation', 'substitution.difference_item', 'validation.record', `${D}.verdict`], + view: 'difference-review', + tools: [], + /* 要人拍板,且这一项内部可能循环 —— 不给 doneWhen */ + guide: `结论算出来了,**该 FAE/RI 审核了** —— 这是验收工单 01,走决策卡(不是终局验收)。**先分岔,再动作**: +轨迹里找这一项已答过、还没处理的 [card-reply]: +· 没有 → 照说明书⑥发第一张决策卡(ask 里把候选数、比对条数、四态各几条、逐候选推荐等级说全),发完这一拍结束。 +· "approve" → 写 \`${D}.verdict.review_approved_total = 1\`,finish 这一项。别再发卡。 +· "reject" / "custom:…" → **驳回**。按意见改台账(同 PK 覆盖写,不开新行、不改行数): + · 意见点名某个参数 → 改那一行 \`substitution.difference_item\` 的 impact_statement / unknown_disposition / evidence_ref;要挂验证的补一行 \`validation.record\`; + · 意见说推荐理由站不住 → 改 \`substitution.relation.recommendation_reason\`(等级不许改 —— 那是工具算的;要改先回 S3 补证据重比,本活动不回头,写清楚为什么维持); + · 没写具体意见("reject")→ 自己挑最站不住的一处改实(证据最含糊的那条 evidence_ref、去向最空的那条 unknown_disposition),**至少改对一处**; + 然后**换一种问法**再发一张决策卡(把改了什么写进 ask),发完结束。 +**基座对着字面比对意见与草稿**:观察里点名了哪一行,就改那一行,不挪到别的行头上。`, + }, + { + item: 'S5_prepare_landing', + intent: '拟落账:P-11 分析状态推到 FAE 确认,回写 P-01 五项', + writes: ['substitution.relation', 'demand.request', `${D}.verdict`], + reads: ['substitution.relation', 'substitution.difference_item', 'validation.record', 'demand.request', `${D}.verdict`], + view: 'landing-confirm', + tools: [], + doneWhen: + 'landing_prepared_total > 0 and substitution_relation_analysis_status_fae_confirmed = substitution_relation_rows and ' + + 'substitution_relation_missing = 0 and demand_request_missing = 0', + guide: `审核通过了,**把拟落账的内容备好**,一拍连做: +① 每条 \`substitution.relation\` 同 PK 覆盖写 analysis_status = "fae_confirmed"(只推分析层;客户批准状态你没有写权,别碰)。 +② \`demand.request\` 同 PK 覆盖写五项 + 状态:risk_level(取各候选 risk_level 里最高的一档)、verification_requirement(把所有 VAL 号与要测什么列全; + 没有 UNKNOWN 写「无需验证」)、output_ref = "RPT-<案号数字串>"、next_action = "提交 ACT-GATE-03 评审放行"、current_activity = "ACT-WORK-05"、 + current_stage = "gate03"、request_status = "responded"。 +③ \`${D}.verdict\` 同 PK 覆盖写:landing_prepared_total = 1、summary 一句人话(几个候选各什么等级、挂了几个验证任务)。 +写完引擎自己收这一项,下一项是放行。**这一拍不要 submit**。`, + }, + { + item: 'T_release', + intent: '拟落账内容已备好,提交 ACT-GATE-03 交 FAE/RI 放行;被打回就改台账再交', + /* 被打回重做时要改的就是这几张 —— 不声明就写不进去 */ + writes: ['substitution.relation', 'substitution.difference_item', 'validation.record', 'demand.request', `${D}.verdict`], + reads: ['substitution.relation', 'substitution.difference_item', 'validation.record', 'demand.request', `${D}.verdict`], + view: 'landing-confirm', + tools: [], + /* 放行触达世界,必须人按一下 —— 引擎不自动收它 */ + guide: `落账确认那一屏摆着拟写 P-11 主记录与拟回写 P-01。**先看轨迹里有没有「验收被打回重做」**: +有 → 这一拍不是再提交一遍。照意见改**台账本身**(同 PK 覆盖写,不开新行): +· 他说推荐等级与明细不符 → 核对 \`substitution.relation.recommendation_level\` 与该关系明细的四态计数,不符的把 recommendation_reason 改实、把漏挂的验证要求补上; +· 他说 P-01 回写不全 → 补 \`demand.request\` 那五项; +· 他说证据不可信 → 改那几行 \`substitution.difference_item.evidence_ref\`(指到具体资料版本与页码); +· 没写具体意见 → 自己挑最站不住的一处改实,至少改对一处; +改完这一拍再 judge + submit。**基座那道闸:打回之后台账一个字没改就不许再提交。** + +没被打回(或已改过)→ 先裁三条裁量判据,然后 submit,同一拍连做: +· judge criterion="证据链可信:每条结论都能追溯到 P-08 / P-10 / P-06 的具体记录,或本单数据工单的创建记录(人工录入)" —— 逐条看 evidence_ref 指不指得到具体记录; +· judge criterion="硬约束项未被自动判 MATCH:封装 / 工艺 / 电气边界等硬约束的 MATCH 都有证据支持" —— 看 hard 且 match 的行 evidence_ref 是否非空且具体; +· judge criterion="推荐等级与差异明细一致:存在 UNKNOWN / CONDITIONAL 的候选没有给「推荐」" —— 逐候选核对; +**裁成不满足照样能提交** —— 理由连着卡一起交给签字的人。criterion 照判据名整句原样抄。 +这一单没有分支去向,只发一口 \`assessment-released\`,不必你选出口。终局唯放行:你不能自己宣布完成。`, + }, +]; + +/** + * 阶段 —— 进度条的刻度,只给人看。除放行外每个计划项恰属一个阶段; + * 放行那一格由基座统一加,人看到的是 **x/6** —— 与文档的步骤 0.5 / 1 / 2 / 3 / 4 / 5 六步逐格对齐: + * 0.5 启动与存在性检查(等待数据)· 1 建立评估上下文 · 2 读取并标准化 · 3 逐项差异评估 · 4 FAE 审核与补证(含拟落账)· 5 提交评审放行(基座那格) + */ +export const ASSESSMENT_PHASES = [ + { name: '启动与存在性检查', items: ['S0_bind', 'S0d_master_data'] }, + { name: '建立评估上下文', items: ['S1_input_confirm'] }, + { name: '读取并标准化资料', items: ['S2_normalize'] }, + { name: '逐项差异评估', items: ['S3_assess'] }, + { name: 'FAE 审核与补证', items: ['S4_review', 'S5_prepare_landing'] }, +]; From f9dbdc8c9c20adf11620ade23736dd84295a0139 Mon Sep 17 00:00:00 2001 From: xcs Date: Wed, 26 Aug 2026 23:45:17 +0800 Subject: [PATCH 05/21] =?UTF-8?q?feat(w05):=20=E4=BA=94=E5=BC=A0=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E6=A8=A1=E7=89=88=20=E2=80=94=E2=80=94=20=E4=B8=A4?= =?UTF-8?q?=E5=BC=A0=E6=95=B0=E6=8D=AE=E5=B7=A5=E5=8D=95=E4=BB=B6=E3=80=81?= =?UTF-8?q?=E8=AF=84=E4=BC=B0=E8=BE=93=E5=85=A5=E6=B8=85=E5=8D=95=E3=80=81?= =?UTF-8?q?=E5=B7=AE=E5=BC=82=E5=AE=A1=E6=A0=B8=E4=BB=B6=E3=80=81=E8=90=BD?= =?UTF-8?q?=E8=B4=A6=E7=A1=AE=E8=AE=A4=E4=BB=B6(=E7=9B=AE=E6=A0=87?= =?UTF-8?q?=E4=BB=B6=E5=B9=B3=E7=A7=BB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh --- .../__tests__/activity.test.ts | 67 +- .../src/views/report.ts | 717 +++++++++++++++++- 2 files changed, 776 insertions(+), 8 deletions(-) diff --git a/activities/part-substitution-assessment/__tests__/activity.test.ts b/activities/part-substitution-assessment/__tests__/activity.test.ts index 21590d3..90f9f14 100644 --- a/activities/part-substitution-assessment/__tests__/activity.test.ts +++ b/activities/part-substitution-assessment/__tests__/activity.test.ts @@ -2,12 +2,11 @@ * ACT-WORK-05 差异评估的包内自测 —— **只准 import @sdk / @contracts / @sfeel**。 * 案例活动没有 `run`,自测不跑回合;测的是清单本身与授权面对齐,结构照 E04 / I01。 * - * 本文件目前只写三组(清单本身合规 / 台账声明 / 活动入口); - * 「案例模型与清单的对齐」「报告模版:编辑面与只读面各就各位」两组留给 Task 4/5 追加 —— - * `src/case-model.ts` 与 `src/views/report.ts` 眼下还是占位,写了那两组只会红。 + * 五组:清单本身合规 / 台账声明 / 案例模型与清单的对齐 / 报告模版:编辑面与只读面各就各位 / 活动入口。 */ import { describe, expect, it } from 'vitest'; import { activityManifestSchema } from '@contracts'; +import { createMockHost } from '@sdk'; import { manifest, D, RULE_VERSION } from '../src/manifest.ts'; import activity from '../src/index.ts'; @@ -15,6 +14,7 @@ const ITEMS = ['S0_bind', 'S0d_master_data', 'S1_input_confirm', 'S2_normalize', const NO_DONE_WHEN = new Set(['S0d_master_data', 'S1_input_confirm', 'S4_review', 'T_release']); const VIEWS = ['p08-master-create', 'p10-document-create', 'eval-input-list', 'difference-review', 'landing-confirm']; const byAddress = (domain: string, object: string) => manifest.declarations.ledger.find((l) => l.domain === domain && l.object === object); +const viewHtml = (name: string) => manifest.assets.viewTemplates.find((t) => t.name === name)?.html ?? ''; describe('清单本身合规', () => { it('过 @contracts 的 schema', () => { @@ -177,8 +177,69 @@ describe('案例模型与清单的对齐', () => { }); }); +describe('报告模版:编辑面与只读面各就各位', () => { + it('两张数据工单件可加行可编辑:data-edit / data-edit-add 与主键 data-col 都在', () => { + const p08 = viewHtml('p08-master-create'); + expect(p08).toContain('data-edit="parts.profile"'); + expect(p08).toContain('data-edit-add="parts.profile"'); + expect(p08).toContain('data-col="internal_pn"'); + expect(p08).toContain('data-col="normalized_parameter_set"'); + const p10 = viewHtml('p10-document-create'); + expect(p10).toContain('data-edit="parts.document"'); + expect(p10).toContain('data-col="doc_seq"'); + }); + + it('评估输入清单:缺口表可编辑(supplement),绑定区只读', () => { + const html = viewHtml('eval-input-list'); + expect(html).toContain(`data-edit="${D}.input_gap"`); + expect(html).toContain('data-col="supplement"'); + expect(html).toContain(`data-rows="mine:${D}.pn_check"`); + }); + + it('差异审核与落账确认只读:没有任何 data-edit / data-col', () => { + for (const name of ['difference-review', 'landing-confirm']) { + expect(viewHtml(name), name).not.toContain('data-edit='); + expect(viewHtml(name), name).not.toContain('data-col='); + } + }); + + it('每个可编辑格子的 data-col 都在对应台账对象的声明里', () => { + const cases: Array<[string, string, string]> = [ + ['p08-master-create', 'parts', 'profile'], + ['p10-document-create', 'parts', 'document'], + ['eval-input-list', D, 'input_gap'], + ]; + for (const [view, domain, object] of cases) { + const cols = new Set([...viewHtml(view).matchAll(/data-col="([^"]+)"/g)].map((m) => m[1] ?? '')); + const declared = new Set(byAddress(domain, object)?.fields.map((f) => f.name)); + for (const c of cols) expect(declared.has(c), `${view}: ${c}`).toBe(true); + } + }); + + it('枚举列只用 data-class-of 投影,不摆 ASCII 原值', () => { + expect(viewHtml('difference-review')).toContain('data-class-of="difference_status"'); + expect(viewHtml('difference-review')).toContain('data-class-of="recommendation_level"'); + expect(viewHtml('difference-review')).not.toContain('data-slot="difference_status"'); + }); + + it('宿主拒推含脚本的模版', async () => { + const host = createMockHost({ contract: CONTRACT }); + await expect(host.base.interaction.pushHtml('
')).rejects.toThrow(/无脚本执行权/); + }); +}); + describe('活动入口', () => { it('没有 run —— 行为全在清单里', () => { expect(activity.run).toBeUndefined(); }); }); + +const CONTRACT = { + activityId: 'part-substitution-assessment', + version: '1.0.0', + data: {}, + message: undefined, + outputPorts: { mandatory: ['assessment-released'], selective: [] }, + prohibitions: [], + discretion: [], +} as const; diff --git a/activities/part-substitution-assessment/src/views/report.ts b/activities/part-substitution-assessment/src/views/report.ts index e03127b..32d3828 100644 --- a/activities/part-substitution-assessment/src/views/report.ts +++ b/activities/part-substitution-assessment/src/views/report.ts @@ -1,5 +1,712 @@ -export const P08_MASTER_CREATE = '
'; -export const P10_DOCUMENT_CREATE = '
'; -export const EVAL_INPUT_LIST = '
'; -export const DIFFERENCE_REVIEW = '
'; -export const LANDING_CONFIRM = '
'; +/** + * ACT-WORK-05 差异评估的 HTML 报告模版 —— **活动自己持有的资产**,随包版本冻结。 + * + * 五个局面各一张: + * · `p08-master-create` —— S0d 摆,数据工单(P-08 芯片主数据),可加行、可编辑; + * · `p10-document-create` —— S0d 摆,数据工单(P-10 原厂技术资料),可加行、可编辑; + * · `eval-input-list` —— S0/S1 摆,评估输入清单:绑定区只读 + 缺口表可编辑; + * · `difference-review` —— S2/S3/S4 摆,各候选推荐等级 + 差异明细,**只读**(通过/驳回在对话区决策卡); + * · `landing-confirm` —— S5/T_release 摆,拟写 P-11 / 拟回写 P-01,**只读**,放行后即归档件。 + * + * 版面语言逐字量自 `docs/SS02-ACT-WORK-05-html/` 四份目标件;取值三纪律:顶层只取汇总列与单值列, + * 逐条走 `mine:域.对象`,列名逐字照 `declarations.ledger` 抄。**模版不算不判**。 + */ +import { PRD_CSS } from './prd-style.ts'; +import { D } from '../constants.ts'; + +/* ==================== + * 绑定期补的样式(原样复制自 E04 `activities/crm-relationship-coverage/src/views/report.ts` + * 第 25–211 行的 `BIND_CSS`,共 187 行 —— 不是 I01 那份(裁决:见任务交底)。 + * 原因:本活动的件里有「只读格与可改格同表混排」(枚举列是 ${options.map(([v, l]) => ``).join('')}`; + +/* ==================== 局面 0 · 数据工单 P-08 ==================== */ + +const P08_TABLE = `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
内部料号品牌制造商规格描述封装工艺生命周期EOL / PCNDatasheet 引用Datasheet 版本适配场景标准化参数集(一行一参数:参数名 | 值 | 单位 | hard/soft | 说明)
${cell('internal_pn', 'mono')}${cell('brand_name')}${cell('manufacturer_name')}${cell('specification_description')}${cell('package', 'mono')}${cell('process_technology')}${select('lifecycle_status', [['active', 'Active'], ['nrnd', 'NRND'], ['eol', 'EOL'], ['discontinued', '停产'], ['risk', '风险']], true)}${tag('t-life', 'lifecycle_status')}${cell('eol_pcn_status')}${cell('datasheet_ref', 'mono')}${cell('datasheet_version')}${cell('application_scenarios')}
`; + +export const P08_MASTER_CREATE = doc({ + tone: 'human', + badge: '数据', + tag: '数据工单', + kicker: 'SS02 · ACT-WORK-05 差异评估 · 数据工单(P-08)', + title: '芯片主数据创建', + step: '步骤 0.5 · 等待数据', + state: `待建 / 行`, + body: + part('做什么', '批量创建 P-08 主数据记录') + + `
+

模板定位:本单是「数据工单」的实例 —— 一单一类(可多条):目标台账 = 芯片主数据(P-08); + 字段即表头、每行一条新记录;标准化参数集是这条记录的一个字段(一行一参数)。

+

触发原因:系统按内部料号查芯片主数据查无记录 —— 是「完全没有」,不是字段缺失(那归执行工单)。

+

AI 预填:上传了 datasheet 的料号 AI 已按资料预填,标着值的格子请核对;没上传的料号全部手填,同为合法路径。

+

纪律:必填列全齐才允许确认;确认后这批记录进本单草稿,放行时并入正式库并留痕(确认人 = 数据责任人);只建主数据,验证记录等事实类不在此建。

+
` + + part('创建什么', '主数据记录(每行一条,*为必填列)') + + gapbar('parts_profile', '必填') + + editGate('parts.profile', `待建 条记录,点「编辑」核对预填、补齐空缺;「+ 加一行」= 再建一条`) + + `
必填列(*)全齐才允许确认;原厂未披露的参数值请直接写「未披露」,它会进入 UNKNOWN 并挂验证任务;删一行 = 该记录不建。改动只落任务草稿层,保存不等于放行。
` + + P08_TABLE + + `
+ + 新行的内部料号必须写(它是这一行的钥匙);其余必填列全部填写后才允许确认。 +
` + + flow('提交 / 确认:本工单只承载内容;确认请在对话区的【执行卡】中进行。确认后活动自动从「等待数据」唤醒。'), + foot: sign('SS02 · ACT-WORK-05 差异评估 · 数据工单 · 芯片主数据创建(P-08)', '工单类型:数据'), +}); + +/* ==================== 局面 0′ · 数据工单 P-10 ==================== */ + +const P10_TABLE = `
+ + + + + + + + + + + + + + + + + + + + + + + + +
关联料号序号资料类型品牌资料标题版本文件链接证据定位状态
${cell('internal_pn', 'mono')}${cell('doc_seq', 'mono')}${select('document_type', [['datasheet', 'Datasheet'], ['app_note', '应用笔记'], ['reference_design', '参考设计'], ['white_paper', '白皮书'], ['sample_doc', '样片资料'], ['commitment_letter', '承诺函'], ['other', '其他']], true)}${tag('t-doc', 'document_type')}${cell('brand_name')}${cell('document_title')}${cell('document_version')}${cell('document_ref', 'mono')}${cell('citation_locator')}${select('document_status', [['valid', '有效'], ['expired', '过期'], ['pending', '待核实'], ['void', '作废']], true)}${tag('t-docst', 'document_status')}
`; + +export const P10_DOCUMENT_CREATE = doc({ + tone: 'human', + badge: '数据', + tag: '数据工单', + kicker: 'SS02 · ACT-WORK-05 差异评估 · 数据工单(P-10)', + title: '原厂技术资料创建', + step: '步骤 0.5 · 等待数据', + state: `待建 / 行`, + body: + part('做什么', '为本单新建的料号登记原厂资料(P-10)') + + `
+

触发原因:按内部料号查原厂技术资料库查无记录。一份资料一行;同一料号多份资料序号 01/02… 自己续。

+

为什么要它:差异明细的证据引用要指到具体资料的版本与页码 —— 没有这一行,结论就追溯不到来源。

+
` + + part('创建什么', '资料记录(每行一条,*为必填列)') + + gapbar('parts_document', '必填') + + editGate('parts.document', `待建 条资料记录,点「编辑」补齐`) + + `
资料类型、标题、版本、文件链接、状态为必填;证据定位写页码或章节。改动只落任务草稿层,保存不等于放行。
` + + P10_TABLE + + `
+ + 关联料号与序号是这一行的钥匙,新行都要写;必填列全齐才允许确认。 +
` + + flow('提交 / 确认:本工单只承载内容;确认请在对话区的【执行卡】中进行。'), + foot: sign('SS02 · ACT-WORK-05 差异评估 · 数据工单 · 原厂技术资料创建(P-10)', '工单类型:数据'), +}); + +/* ==================== 局面 1 · 评估输入清单(S0/S1)==================== */ + +const PN_CHECK_TABLE = `
+ + + + + + + + + + + + + + +
料号角色在库状态备注 / 放弃原因
${ro('internal_pn', 'mono')}${tag('t-role', 'role')}${tag('t-exist', 'exists_status')}
`; + +const GAP_TABLE = `
+ + + + + + + + + + + + + + + + + + +
键输入项来源状态绑定值 / 缺口说明补充内容
${cell('item_key', 'mono')}${cell('input_item')}${cell('source_ledger', 'mono')}${select('status', [['gap', '待补'], ['default', '默认·可改'], ['ok', '齐备']])}${tag('t-gap', 'status')}${cell('detail')}
`; + +export const EVAL_INPUT_LIST = doc({ + tone: 'human', + badge: '执行', + tag: '执行工单', + kicker: 'SS02 · ACT-WORK-05 差异评估 · 执行工单 1/1', + title: '评估输入与资料补齐确认', + step: '步骤 1/5 · 建立评估上下文', + state: `待补 项`, + body: + part('做什么', '启动 4 项已绑定,其余信息在工单里补齐') + + `
+

AI 已完成:系统已按启动传入的 4 项完成绑定 —— 客户、客户需求(新建 P-01 · 版本 V1)、原 PN、候选 PN(数组,逐个评估)—— 并生成输入快照。

+

需要你补齐:启动时未收的信息(应用场景、BOM 位)直接在下表补;默认值(替代类型 / 规则版本 / 负责人期限)可确认可改。本单缺口只覆盖「有而不全」;「完全没有」(料号不在库)已由数据工单创建。未补齐的缺口项将按 UNKNOWN 进入差异评估。

+
` + + part('已绑定', '启动时冻结,工单内不可改 —— 绑错请退回启动人重新启动') + + `
+ 客户 + 客户需求 · V1 · + 原内部料号 + 规则版本 +
` + + PN_CHECK_TABLE + + part('交回什么', '评估输入清单(已绑定项只读核对,缺口项补齐,*为必填)') + + gapbar(`${D}_input_gap`, '补充内容') + + editGate(`${D}.input_gap`, `还有 项待补 / 待确认,点「编辑」在「补充内容」列填入`) + + `
加一行 = 补充 AI 未列出的资料项(新行的「输入项」与「补充内容」必填);删一行 = 该输入项不适用;补不了的写「留 UNKNOWN」或「挂验证任务」—— 那也是一个答案。改动只落任务草稿层,保存不等于放行。
` + + GAP_TABLE + + `
+ + 键写一个英文短名(如 thermal_note);「绑定值 / 缺口说明」栏写清这项资料是什么、AI 为什么拿不到。 +
` + + flow('审批 / 提交:本工单只承载内容;「提交」请在对话区的【执行卡】中进行。提交后去向:进入步骤 2 · AI 读取与标准化,仍缺失的项将按 UNKNOWN 进入差异评估。'), + foot: sign('SS02 · ACT-WORK-05 差异评估 · 执行工单 1/1 · 评估输入与资料补齐确认', '工单类型:执行'), +}); + +/* ==================== 局面 2 · 差异评估结论审核(S2/S3/S4,只读)==================== */ + +const RELATION_TABLE = `
+ + + + + + + + + + + + + + + + + + + + + + +
候选料号替代类型推荐等级推荐理由差异摘要风险验证要求置信度
${ro('candidate_internal_pn', 'mono')}${tag('t-sub', 'substitution_type')}${tag('t-rec', 'recommendation_level')}${ro('recommendation_reason')}${ro('difference_summary')}${tag('t-risk', 'risk_level')}${ro('verification_requirement')}${ro('confidence_score', 'mono')}
`; + +const ITEM_TABLE = `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
关系参数项原 PN 值候选 PN 值单位约束类型结论风险证据引用影响说明UNKNOWN 去向置信度
${ro('substitution_relation_id', 'mono')}${ro('original_value', 'mono')}${ro('candidate_value', 'mono')}${ro('parameter_unit')}${tag('t-con', 'constraint_type')}${tag('t-diff', 'difference_status')}${tag('t-risk', 'risk_level')}${ro('evidence_ref', 'mono')}${ro('impact_statement')}${ro('unknown_disposition')}${ro('confidence', 'mono')}
`; + +export const DIFFERENCE_REVIEW = doc({ + tone: 'ledger', + badge: '验收', + tag: '验收工单', + kicker: 'SS02 · ACT-WORK-05 差异评估 · 验收工单 1/2', + title: '差异评估结论审核', + step: '步骤 3~4/5 · 逐项差异评估 + FAE/RI 审核', + state: `共 个候选 · 条比对`, + body: + part('AI 结论说明', '结论是怎么算出来的') + + `
+

AI 已完成的计算:按 GOV 评估规则(锁定版本 ),对原 PN + 与每个候选逐项比对,四态结论 —— MATCH / + MISMATCH / + UNKNOWN / + CONDITIONAL ,并给出风险等级、证据引用与置信度。 + 差异明细 = P-11 差异明细子表(一条参数一条记录);当前为 AI 草稿,审核通过前不落账。本单数据工单创建的主数据,其参数证据标注「人工录入」。

+
` + + part('验收判据', '你据此判断「通过 / 驳回」') + + criteria('判断这份差异评估结论是否可信,请逐项核对', [ + '关键参数差异均有明确状态:逐项结论无漏项,每个参数都有四态之一的判定;', + 'UNKNOWN 项均有去向:每个 UNKNOWN 都挂了验证任务或补来源动作,不允许悬空;', + '证据链完整:每项结论可追溯至 P-08 / P-10 / P-06 的具体记录,或本单数据工单的创建记录(人工录入);', + '硬约束项未被自动判 MATCH:封装 / 工艺等硬约束未经证据支持不得判 MATCH。', + ]) + + part('各候选推荐等级', '只读展示 · 每个候选一条 P-11 主记录草稿') + + RELATION_TABLE + + part('AI 产出的差异明细', '只读展示 · P-11 差异明细子表草稿') + + ITEM_TABLE + + flow('审批:本工单只承载内容;「通过 / 驳回」请在对话区的【验收卡】中进行。审核意见与补充条件写入 P-06(验证要求);通过后去向:进入步骤 5 · 评审放行与落账确认。'), + foot: sign('SS02 · ACT-WORK-05 差异评估 · 验收工单 1/2 · 差异评估结论审核', '工单类型:验收'), +}); + +/* ==================== 局面 3 · 评审放行与落账确认(S5/T_release,只读)==================== */ + +const P11_TABLE = `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
替代关系ID替代类型原内部料号候选内部料号推荐等级推荐理由差异摘要替代分析报告分析状态客户批准状态风险验证要求证据可信度规则版本
${ro('substitution_relation_id', 'mono')}${tag('t-sub', 'substitution_type')}${ro('original_internal_pn', 'mono')}${ro('candidate_internal_pn', 'mono')}${tag('t-rec', 'recommendation_level')}${ro('recommendation_reason')}${ro('difference_summary')}${ro('analysis_report_ref', 'mono')}${tag('t-anal', 'analysis_status')}未提交 由审批流程维护,本活动不产生${tag('t-risk', 'risk_level')}${ro('verification_requirement')}${ro('confidence_score', 'mono')}${ro('rule_version', 'mono')}
`; + +const P01_TABLE = `
+ + + + + + + + + + + + + + + + + + + + +
需求编号风险等级验证要求输出链接下一步动作当前阶段需求状态
${ro('request_id', 'mono')}${tag('t-risk', 'risk_level')}${ro('verification_requirement')}${ro('output_ref', 'mono')}${ro('next_action')}${ro('current_stage', 'mono')}${ro('request_status', 'mono')}
`; + +export const LANDING_CONFIRM = doc({ + tone: 'ledger', + badge: '验收', + tag: '验收工单', + kicker: 'SS02 · ACT-WORK-05 差异评估 · 验收工单 2/2', + title: '评审放行与落账确认', + step: '步骤 5/5 · 提交 ACT-GATE-03', + state: `拟落账 条 P-11`, + body: + part('AI 结论说明', '本单确认什么') + + `
+

本活动以 ACT-GATE-03 评审放行为终点。本单确认拟落账内容:评估结论层写 P-11 替代关系主记录(每个候选一条)与差异明细子表,需求状态回写 P-01(风险等级 / 验证要求 / 输出链接 / 下一步动作 / 当前阶段)。 + 客户批准不属于本活动 —— 客户批准状态由审批流程维护,本单只写分析层(分析状态 = FAE 确认)。

+
` + + part('验收判据', '你据此判断「通过 / 驳回」') + + criteria('放行与落账前,请逐项核对', [ + '推荐等级与差异明细一致:存在 UNKNOWN / CONDITIONAL 的候选,推荐等级不得给「推荐」;', + '分析状态与客户批准状态分离:本单只写分析层(FAE 确认),客户批准状态保持「未提交」;', + 'GOV 规则版本已锁定:规则版本随结论一并落账;', + '回写 P-01 字段完整:风险等级、验证要求、输出链接、下一步动作、当前阶段五项齐全。', + ]) + + part('拟写 P-11 · 替代关系主记录', '只读展示') + + P11_TABLE + + part('拟回写 P-01 · 需求状态', '只读展示') + + P01_TABLE + + flow('审批:本工单只承载内容;「通过 / 驳回」请在对话区的【验收卡】中进行。通过后去向:P-11 落账、P-01 回写、放行;结论供 ACT-WORK-08 证据归集消费。'), + foot: sign('SS02 · ACT-WORK-05 差异评估 · 验收工单 2/2 · 评审放行与落账确认', '工单类型:验收'), +}); From 0629a7f05418ad05b29cf0cdda000bc43d36b1ff Mon Sep 17 00:00:00 2001 From: xcs Date: Thu, 27 Aug 2026 00:01:39 +0800 Subject: [PATCH 06/21] =?UTF-8?q?fix(w05):=20S0d=20=E6=8B=86=E6=88=90=20P-?= =?UTF-8?q?08/P-10=20=E4=B8=A4=E4=B8=AA=E8=AE=A1=E5=88=92=E9=A1=B9?= =?UTF-8?q?=E5=90=84=E8=87=AA=E7=BB=91=20view;=E8=A1=A5=E9=BD=90=20fresh()?= =?UTF-8?q?=20=E4=B8=89=E9=9D=A2=E6=97=97;=E5=80=99=E9=80=89=E5=85=A8?= =?UTF-8?q?=E6=94=BE=E5=BC=83=E9=98=B2=E6=AD=BB=E9=94=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 4 审查发现 S0d_master_data 固定单一 view 导致 P-10 那份件在引擎里永不可达 (剧本声明的 view 恒压过模型的 say.view),按裁决拆成 S0d_p08_master / S0d_p10_document 两个计划项,各自绑 p08-master-create / p10-document-create;caseFileModel 补 parts_profile_rows、新增哨兵 s_p08_created 串起两步的入场前提;说明书三处 itemId、 S0_bind 的 disable 提示、ASSESSMENT_PHASES 第一段随之更新。steps.test.ts 的 fresh() 补齐 input_confirmed_total / review_approved_total / landing_prepared_total 三面旗 (此前因未绑定名比较求值为假才没暴露)。S0d_p08_master 剧本加「至少留一颗候选」规则, 防止候选全被放弃导致 s_pns_resolved 永远不成立的死锁。46/46 测试与 typecheck 全绿。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh --- .../__tests__/activity.test.ts | 18 ++-- .../__tests__/steps.test.ts | 25 +++++- .../src/case-model.ts | 82 +++++++++++++------ 3 files changed, 92 insertions(+), 33 deletions(-) diff --git a/activities/part-substitution-assessment/__tests__/activity.test.ts b/activities/part-substitution-assessment/__tests__/activity.test.ts index 90f9f14..e1770a8 100644 --- a/activities/part-substitution-assessment/__tests__/activity.test.ts +++ b/activities/part-substitution-assessment/__tests__/activity.test.ts @@ -10,8 +10,8 @@ import { createMockHost } from '@sdk'; import { manifest, D, RULE_VERSION } from '../src/manifest.ts'; import activity from '../src/index.ts'; -const ITEMS = ['S0_bind', 'S0d_master_data', 'S1_input_confirm', 'S2_normalize', 'S3_assess', 'S4_review', 'S5_prepare_landing', 'T_release']; -const NO_DONE_WHEN = new Set(['S0d_master_data', 'S1_input_confirm', 'S4_review', 'T_release']); +const ITEMS = ['S0_bind', 'S0d_p08_master', 'S0d_p10_document', 'S1_input_confirm', 'S2_normalize', 'S3_assess', 'S4_review', 'S5_prepare_landing', 'T_release']; +const NO_DONE_WHEN = new Set(['S0d_p08_master', 'S0d_p10_document', 'S1_input_confirm', 'S4_review', 'T_release']); const VIEWS = ['p08-master-create', 'p10-document-create', 'eval-input-list', 'difference-review', 'landing-confirm']; const byAddress = (domain: string, object: string) => manifest.declarations.ledger.find((l) => l.domain === domain && l.object === object); const viewHtml = (name: string) => manifest.assets.viewTemplates.find((t) => t.name === name)?.html ?? ''; @@ -124,20 +124,26 @@ describe('台账声明:授权面与《统一方案》口径', () => { }); describe('案例模型与清单的对齐', () => { - it('八个计划项,剧本 item 与 CMMN humanTask id 一一对应', () => { + it('九个计划项,剧本 item 与 CMMN humanTask id 一一对应', () => { const cmmnIds = [...manifest.assets.businessModel.cmmnXml.matchAll(/ m[1] ?? ''); expect(cmmnIds).toEqual(ITEMS); expect((manifest.assets.businessModel.steps as ReadonlyArray<{ item: string }>).map((s) => s.item)).toEqual(ITEMS); }); - it('四处不给 doneWhen(数据工单 / 输入确认 / 审核 / 放行),其余四处都给', () => { + it('五处不给 doneWhen(两张数据工单 / 输入确认 / 审核 / 放行),其余四处都给', () => { for (const s of manifest.assets.businessModel.steps as ReadonlyArray<{ item: string; doneWhen?: string }>) { if (NO_DONE_WHEN.has(s.item)) expect(s.doneWhen, s.item).toBeUndefined(); else expect(s.doneWhen, s.item).toBeDefined(); } }); - it('那四项在 CMMN 里都标了人工激活', () => { + it('两张数据工单件各自绑自己的 view(P-08 与 P-10 不共用同一份画面)', () => { + const steps = manifest.assets.businessModel.steps as ReadonlyArray<{ item: string; view?: string }>; + expect(steps.find((s) => s.item === 'S0d_p08_master')?.view).toBe('p08-master-create'); + expect(steps.find((s) => s.item === 'S0d_p10_document')?.view).toBe('p10-document-create'); + }); + + it('那五项在 CMMN 里都标了人工激活', () => { for (const id of NO_DONE_WHEN) { const block = new RegExp(``).exec(manifest.assets.businessModel.cmmnXml)?.[0] ?? ''; expect(block, id).toContain('manualActivationRule'); @@ -159,7 +165,7 @@ describe('案例模型与清单的对齐', () => { it('两种决策卡骨架都在说明书里:候选放弃/创建(create/abandon)与审核(approve/reject/custom:)', () => { const n = manifest.assets.businessModel.narrative; - expect(n).toContain('"kind":"decide","itemId":"S0d_master_data"'); + expect(n).toContain('"kind":"decide","itemId":"S0d_p08_master"'); expect(n).toContain('"value":"create"'); expect(n).toContain('"value":"abandon"'); expect(n).toContain('"kind":"decide","itemId":"S4_review"'); diff --git a/activities/part-substitution-assessment/__tests__/steps.test.ts b/activities/part-substitution-assessment/__tests__/steps.test.ts index b9df010..41549fc 100644 --- a/activities/part-substitution-assessment/__tests__/steps.test.ts +++ b/activities/part-substitution-assessment/__tests__/steps.test.ts @@ -33,6 +33,9 @@ const fresh = (input: Record): Record => ({ [`${D}_pn_check_exists_status_abandoned`]: 0, [`${D}_input_gap_rows`]: 0, [`${D}_input_gap_missing`]: 0, + input_confirmed_total: 0, + review_approved_total: 0, + landing_prepared_total: 0, substitution_relation_rows: 0, substitution_relation_missing: 0, substitution_relation_analysis_status_fae_confirmed: 0, @@ -136,7 +139,7 @@ describe('doneWhen —— 开局不许真,干完必须真', () => { describe('说明书与剧本的骨架', () => { it('五张卡的骨架都住在说明书里(压拍之后只有说明书还在)', () => { expect(NARRATIVE).toContain('"kind":"entry","itemId":"S0_bind"'); - expect(NARRATIVE).toContain('"kind":"decide","itemId":"S0d_master_data"'); + expect(NARRATIVE).toContain('"kind":"decide","itemId":"S0d_p08_master"'); expect(NARRATIVE).toContain('"title":"数据工单 · 芯片主数据创建(P-08)'); expect(NARRATIVE).toContain('"title":"数据工单 · 原厂技术资料创建(P-10)'); expect(NARRATIVE).toContain('"title":"评估输入与资料补齐确认'); @@ -151,14 +154,28 @@ describe('说明书与剧本的骨架', () => { expect(s3).toContain('工具调用后同拍其余动作作废'); }); - it('S0 剧本写明库里都有就 disable S0d;S0d 剧本写明候选 ≥2 才问、=1 必建、原 PN 硬阻塞', () => { + it('S0 剧本写明库里都有就 disable S0d 两项;S0d_p08_master 剧本写明候选 ≥2 才问、=1 必建、原 PN 硬阻塞', () => { const s0 = ASSESSMENT_STEPS.find((s) => s.item === 'S0_bind')?.guide ?? ''; - expect(s0).toContain('disable S0d_master_data'); - const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_master_data')?.guide ?? ''; + expect(s0).toContain('disable S0d_p08_master'); + const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? ''; expect(s0d).toContain('硬阻塞'); expect(s0d).toContain('候选只有 1 颗'); }); + it('S0d 拆成两项各自绑自己的 view(P-10 那份件不会永远推不出来),两项都没有 doneWhen', () => { + const p08 = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master'); + const p10 = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p10_document'); + expect(p08?.view).toBe('p08-master-create'); + expect(p10?.view).toBe('p10-document-create'); + expect(p08?.doneWhen).toBeUndefined(); + expect(p10?.doneWhen).toBeUndefined(); + }); + + it('候选放弃到只剩最后一颗时不许再放弃(至少留一颗,防止全放弃死锁)', () => { + const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? ''; + expect(s0d).toContain('至少留一颗候选'); + }); + it('五段进度,每段名 ≤10 字;除放行外每项恰属一段', () => { expect(ASSESSMENT_PHASES).toHaveLength(5); for (const p of ASSESSMENT_PHASES) expect(p.name.length, p.name).toBeLessThanOrEqual(10); diff --git a/activities/part-substitution-assessment/src/case-model.ts b/activities/part-substitution-assessment/src/case-model.ts index 3afede6..701ec80 100644 --- a/activities/part-substitution-assessment/src/case-model.ts +++ b/activities/part-substitution-assessment/src/case-model.ts @@ -4,17 +4,21 @@ * 业务出处:`docs/SS02-ACT-WORK-05-html/ACT-WORK-05-活动流程与工单说明.md`(步骤 0.5 + 5 步, * 1 数据(条件)+ 1 执行 + 2 验收)与《SS02-SS03-SS04_Demo台账字段细化与统一方案》(字段口径)。 * - * 文档六步收成 8 个案例模型项: + * 文档六步收成 9 个案例模型项(0.5 那一步因 view 各不相同拆成两项 —— 引擎里剧本声明的 + * view 恒压过模型当拍报的 say.view,一个计划项只能绑一份画面,P-08 与 P-10 各自要一份可编辑的 + * 工作区件,不能共用同一个 view): * 启动期 → S0_bind(AI:铸案号、建 P-01 V1、锁规则版本、逐个 PN 查库、写存在性核对表) - * 0.5 等待数据 → S0d_master_data(人:数据工单 —— P-08 一张、P-10 一张;候选 ≥2 缺库先决策卡; + * 0.5a 等待数据(建主数据)→ S0d_p08_master(人:数据工单 P-08;候选 ≥2 缺库先决策卡; * 库里都有就 disable 这一项) + * 0.5b 等待数据(建资料)→ S0d_p10_document(人:数据工单 P-10;库里都有就 disable 这一项) * 1 建立评估上下文 → S1_input_confirm(人:执行工单 01,评估输入清单,件上补缺口) * 2 读取并标准化 → S2_normalize(AI:P-11 主记录骨架 + 差异明细对照行) * 3 逐项差异评估 → S3_assess(AI + 两枚工具:四态 / 风险 / 证据 / 影响 / 推荐等级 / 验证任务) * 4 FAE/RI 审核 → S4_review(人:验收工单 01 = 决策卡循环,驳回则 AI 改台账再发) * 5 提交评审放行 → S5_prepare_landing(AI:analysis_status 推 fae_confirmed、回写 P-01)+ T_release(人:验收工单 02,终局放行) * - * 四处不给 doneWhen:S0d(触达人)、S1(交人补缺口)、S4(拍板且可循环)、T_release(签下去)。 + * 五处不给 doneWhen:S0d_p08_master / S0d_p10_document(触达人)、S1(交人补缺口)、 + * S4(拍板且可循环)、T_release(签下去)。 */ import { D, RULE_VERSION, TOOL_COMPARE, TOOL_RECOMMEND } from './constants.ts'; @@ -35,6 +39,7 @@ export const CMMN_XML = ` + @@ -61,6 +66,11 @@ export const CMMN_XML = ` update demand_request_rows > 0 and sub_eval_pn_check_rows > 0 + + + update + parts_profile_rows > 0 and parts_profile_missing = 0 + - - - - + + + + + + + + + + @@ -165,7 +181,7 @@ export const NARRATIVE = `§0 开案:**判断候选料号在技术上能不能 **存在性检查与数据工单**:启动时逐个料号在 \`parts.profile\` 里查(原 PN + 每个候选)。查无记录 = 「完全没有」, 要人从零建整条主数据 —— 发**数据工单**(一张表一张单:P-08 一张、P-10 一张);PN 在库但个别字段缺 = 「有而不全」, 归执行工单 01 补。原 PN 缺 = 硬阻塞必须建;候选缺:候选 ≥2 时先问人放弃还是创建,候选 = 1 时必须建。 -**库里都有就把 S0d_master_data disable 掉**(此单不走这条),别发空单。 +**库里都有就把 S0d_p08_master 与 S0d_p10_document 两项都 disable 掉**(此单不走这条),别发空单。 **六步走**:绑定与存在性核对 → (等待数据)→ 人补评估输入 → 标准化对照表 → 逐项评估 → 人审结论(可循环)→ 拟落账 → 人放行。 @@ -182,7 +198,7 @@ export const NARRATIVE = `§0 开案:**判断候选料号在技术上能不能 人确认那一下基座已把这一行落进草稿层,**你不许再 write 一遍 customer.profile**。 ② 候选不在库、候选 ≥2 时,一颗一张决策卡(按候选数组顺序,一次只发一张;候选只有一颗时不问、直接建): -{"kind":"decide","itemId":"S0d_master_data", +{"kind":"decide","itemId":"S0d_p08_master", "ask":"候选 在芯片主数据里查无记录。是从零建它的主数据(派数据工单),还是本轮放弃这颗候选?", "options":[{"value":"create","label":"创建主数据(派数据工单)","recommended":true}, {"value":"abandon","label":"放弃这颗候选"}],"allowCustom":true} @@ -190,7 +206,7 @@ export const NARRATIVE = `§0 开案:**判断候选料号在技术上能不能 点了 \`"abandon"\` 没写原因,\`abandon_reason\` 写「负责人本轮放弃(未注明原因)」。 ③ 数据工单 · P-08(所有要建的料号一起,一张单;零输入,填报全在工作区的件上): -{"kind":"human","itemId":"S0d_master_data","workOrder":"execution","card":{ +{"kind":"human","itemId":"S0d_p08_master","workOrder":"execution","card":{ "title":"数据工单 · 芯片主数据创建(P-08):<料号列表,顿号分隔>", "ask":"这些料号在芯片主数据里查无记录,请在工作区的件上把整条记录建齐(一颗一行,可加行)。", "steps":["每一行:品牌、规格描述、封装、生命周期状态、标准化参数集为必填,缺一格确认不了", @@ -202,7 +218,7 @@ export const NARRATIVE = `§0 开案:**判断候选料号在技术上能不能 {"k":"AI 预填","v":"<有 datasheet 就写「已按 datasheet 预填 N 项,请核对」,没有写「未上传 datasheet,请全填」>"}]}} ④ 数据工单 · P-10(同一批料号的资料记录,一张单;在 P-08 那张确认之后再发): -{"kind":"human","itemId":"S0d_master_data","workOrder":"execution","card":{ +{"kind":"human","itemId":"S0d_p10_document","workOrder":"execution","card":{ "title":"数据工单 · 原厂技术资料创建(P-10):<料号列表>", "ask":"请把这些料号的 datasheet / 承诺函等资料记录补进原厂技术资料库(一份资料一行,可加行)。", "steps":["资料类型、标题、版本、文件链接、资料状态为必填;证据定位写页码或章节,差异评估的证据引用会指向它", @@ -246,7 +262,8 @@ export const NARRATIVE = `§0 开案:**判断候选料号在技术上能不能 /** * 步骤剧本 —— 一个计划项一条;`doneWhen` 是「成果已存在」的判据,不是「该不该做」。 - * 四处不给 doneWhen:S0d(发数据工单触达人)、S1(交人补缺口)、S4(拍板且循环)、T_release(签下去)。 + * 五处不给 doneWhen:S0d_p08_master / S0d_p10_document(发数据工单触达人)、S1(交人补缺口)、 + * S4(拍板且循环)、T_release(签下去)。 */ export const ASSESSMENT_STEPS = [ { @@ -281,41 +298,60 @@ current_stage = "assessing"。application_scenario / bom_position **这一步不 【写 \`${D}.verdict\` 一行】eval_ref、request_id、rule_version = "${RULE_VERSION}"、original_internal_pn、candidate_total = 候选数组长度。 -写完引擎自己收这一项。收完先看 \`${D}.pn_check\`:有 absent 的 → 下一项是 S0d_master_data(数据工单);一个 absent 都没有 → -**disable S0d_master_data**(reason 写「原 PN 与全部候选都在库,无需建主数据」),然后等 S1 那一项 Enabled。`, +写完引擎自己收这一项。收完先看 \`${D}.pn_check\`:有 absent 的 → 下一项是 S0d_p08_master(数据工单);一个 absent 都没有 → +**disable S0d_p08_master 与 S0d_p10_document 两项**(reason 写「原 PN 与全部候选都在库,无需建主数据」),然后等 S1 那一项 Enabled。`, }, { - item: 'S0d_master_data', - intent: '料号不在库:派数据工单让人从零建整条主数据(P-08 一张、P-10 一张);候选 ≥2 先问放弃还是创建', + item: 'S0d_p08_master', + intent: '料号不在库:派 P-08 数据工单让人从零建芯片主数据;候选 ≥2 先问放弃还是创建', writes: ['parts.profile', 'parts.document', `${D}.pn_check`], reads: [`${D}.pn_check`, 'candidateInternalPns', 'originalInternalPn', 'datasheets', 'parts.profile', 'parts.document'], view: 'p08-master-create', tools: [], /* 触达人,不给 doneWhen;人工激活:发卡就算启动 */ - guide: `这一项只在 \`${D}.pn_check\` 里有 absent 行时走;都在库就 disable 它(见 S0 剧本)。**先分岔,再动作**: + guide: `这一项只在 \`${D}.pn_check\` 里有 absent 行时走;都在库就由 S0_bind 把这一项与下一项 S0d_p10_document 一起 disable 掉。**先分岔,再动作**: 【先看轨迹】这一项里已经发过哪些卡、人答了什么(【当前这一步】下面那行引擎事实说得清楚): · 原 PN absent → **硬阻塞**,不问人,直接进入「建」的名单; · 候选 absent 且候选数 ≥2 → 每颗 absent 的候选照说明书②各发一张决策卡(一次一张,按数组顺序);人选 "abandon"/"custom:" 的, 把那一行 \`${D}.pn_check\` 改成 exists_status = "abandoned" + abandon_reason(同 PK 覆盖写),这颗不再评估;选 "create" 的进「建」的名单; · 候选 absent 且候选只有 1 颗 → 不问,必须建。 +**至少留一颗候选**:放弃到只剩最后一颗还是 absent 的候选时,那一颗按「候选只有一颗」办 —— 不再发决策卡问,直接进「建」的名单 +(候选全被放弃,存在性核对永远凑不齐、单子会死锁,这一条就是防它)。 **别把同一颗候选问两遍**:决策卡答过就按答案办。 -【「建」的名单定了 → 先写骨架行,再发 P-08 数据工单】 +【「建」的名单定了 → 写骨架行,发 P-08 数据工单】 1. 给名单里每颗料写一行 \`parts.profile\` 骨架:key {internal_pn},patch 只带你确有依据的列 —— 有 \`datasheets[].text\` 就从里面读 brand_name / specification_description / package / lifecycle_status(active/nrnd/eol/discontinued/risk 之一)/ normalized_parameter_set (一行一参数:参数名 | 值 | 单位 | hard/soft | 说明,原厂没写的值写「未披露」)/ datasheet_ref + datasheet_version;没有 datasheet 就只写 internal_pn 这一列(其余留给人填,**别编**)。 2. 同一拍给每颗料写一行 \`parts.document\` 骨架:key {internal_pn, doc_seq: "01"},patch {document_type: "datasheet", document_status: "pending", - brand_name 有就写}。 + brand_name 有就写}(这一行是骨架,下一项 S0d_p10_document 会把它填成正式记录)。 3. 同一拍发说明书③那张 P-08 数据工单(title 里列全料号),发完这一拍结束(挂起)。 -【P-08 那张确认回来了】看轨迹里的 [card-reply]:再发说明书④那张 P-10 数据工单(只发一次),发完这一拍结束。 +【P-08 那张确认回来了】finish 这一项 —— 下一项是 S0d_p10_document,同一批料号接着补资料记录。 -【P-10 那张也确认回来了】把名单里每颗料的 \`${D}.pn_check\` 改成 exists_status = "created"(同 PK 覆盖写),然后 finish 这一项。 +【这张卡一单只发一次】人交回来之后不许再发;答复里没有值(空 values)也算答过 —— 人在件上填的东西已经落草稿层。`, + }, + { + item: 'S0d_p10_document', + intent: '料号不在库:派 P-10 数据工单让人从零建原厂技术资料', + writes: ['parts.document', `${D}.pn_check`], + reads: [`${D}.pn_check`, 'parts.profile', 'parts.document'], + view: 'p10-document-create', + tools: [], + /* 触达人,不给 doneWhen;人工激活:发卡就算启动 */ + guide: `这一项只在 \`${D}.pn_check\` 里有 absent 行时走(要走到这一步,S0d_p08_master 必然已经收完);都在库就由 S0_bind 把这一项与 +S0d_p08_master 一起 disable 掉。 + +【发 P-10 数据工单】「建」的名单就是 \`${D}.pn_check\` 里仍是 exists_status = "absent" 的那几行(候选 ≥2 时被放弃的那些, +S0d_p08_master 那一步已经把它们改成 "abandoned",不在这份名单里)。同一拍照说明书④发那张 P-10 数据工单(title 里列全料号,只发一次), +发完这一拍结束(挂起)。 + +【P-10 那张确认回来了】把名单里每颗料的 \`${D}.pn_check\` 改成 exists_status = "created"(同 PK 覆盖写),然后 finish 这一项。 这时 S1_input_confirm 的入场前提(全部料号落定、本单建的主数据不缺必填)自己就成立了 —— 缺必填由提交闸兜着,卡回来时一定是齐的。 -【这两张卡一单各只发一次】人交回来之后不许再发;答复里没有值(空 values)也算答过 —— 人在件上填的东西已经落草稿层。`, +【这张卡一单只发一次】人交回来之后不许再发;答复里没有值(空 values)也算答过 —— 人在件上填的东西已经落草稿层。`, }, { item: 'S1_input_confirm', @@ -481,7 +517,7 @@ difference_status / compare_result / risk_level / impact_statement / unknown_dis * 0.5 启动与存在性检查(等待数据)· 1 建立评估上下文 · 2 读取并标准化 · 3 逐项差异评估 · 4 FAE 审核与补证(含拟落账)· 5 提交评审放行(基座那格) */ export const ASSESSMENT_PHASES = [ - { name: '启动与存在性检查', items: ['S0_bind', 'S0d_master_data'] }, + { name: '启动与存在性检查', items: ['S0_bind', 'S0d_p08_master', 'S0d_p10_document'] }, { name: '建立评估上下文', items: ['S1_input_confirm'] }, { name: '读取并标准化资料', items: ['S2_normalize'] }, { name: '逐项差异评估', items: ['S3_assess'] }, From ec3de1f19e6c1c74e7857afd990cbb7c17146cf4 Mon Sep 17 00:00:00 2001 From: xcs Date: Thu, 27 Aug 2026 00:05:24 +0800 Subject: [PATCH 07/21] =?UTF-8?q?fix(w05):=20landing-confirm=20=E7=9A=84?= =?UTF-8?q?=20request=5Fstatus=20/=20current=5Fstage=20=E6=94=B9=E5=BE=BD?= =?UTF-8?q?=E6=A0=87=20=E2=80=94=E2=80=94=20=E6=94=BE=E8=A1=8C=E7=AD=BE?= =?UTF-8?q?=E5=AD=97=E9=A1=B5=E4=B8=8D=E5=86=8D=E6=91=86=20ASCII=20?= =?UTF-8?q?=E6=9C=BA=E5=99=A8=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审查发现 P01_TABLE 的需求状态(tally 列)与当前阶段仍用 ro() 直显原值,把 responded/gate03 这类机器码摆在了放行签字页上(本仓栽过两次同款:BT 层级、key_person_change)。改用 tag() + data-class-of 投影,EXTRA_CSS 补 t-reqstat(五档)与 t-stage(assessing/gate03 两档,取值域 由本活动写死)人读名;台账写入值不变,requiredWhen 机检仍读原值。测试组「枚举列只用 data-class-of 投影」扩到 landing-confirm 的四个枚举列(含既有的 analysis_status / recommendation_level),把这次的回归钉死。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh --- .../__tests__/activity.test.ts | 5 +++++ .../src/views/report.ts | 16 ++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/activities/part-substitution-assessment/__tests__/activity.test.ts b/activities/part-substitution-assessment/__tests__/activity.test.ts index e1770a8..5eb036c 100644 --- a/activities/part-substitution-assessment/__tests__/activity.test.ts +++ b/activities/part-substitution-assessment/__tests__/activity.test.ts @@ -226,6 +226,11 @@ describe('报告模版:编辑面与只读面各就各位', () => { expect(viewHtml('difference-review')).toContain('data-class-of="difference_status"'); expect(viewHtml('difference-review')).toContain('data-class-of="recommendation_level"'); expect(viewHtml('difference-review')).not.toContain('data-slot="difference_status"'); + const landing = viewHtml('landing-confirm'); + for (const col of ['request_status', 'current_stage', 'analysis_status', 'recommendation_level']) { + expect(landing, col).toContain(`data-class-of="${col}"`); + expect(landing, col).not.toContain(`data-slot="${col}"`); + } }); it('宿主拒推含脚本的模版', async () => { diff --git a/activities/part-substitution-assessment/src/views/report.ts b/activities/part-substitution-assessment/src/views/report.ts index 32d3828..ceb3637 100644 --- a/activities/part-substitution-assessment/src/views/report.ts +++ b/activities/part-substitution-assessment/src/views/report.ts @@ -282,6 +282,18 @@ const EXTRA_CSS = ` .proto-root .jc-tag.t-risk.v-low { color: var(--ok); background: var(--ok-soft); } .proto-root .jc-tag.t-risk.v-medium { color: var(--warn-accent); background: var(--warn-soft); } .proto-root .jc-tag.t-risk.v-high, .proto-root .jc-tag.t-risk.v-critical { color: var(--danger); background: var(--danger-soft); } +/* + * 需求状态(P-01 tally 列)与当前阶段(自由文本,但活动只写 assessing/gate03 两个机器码)—— + * 落账确认件是放行签字页,不许把机器码摆给签字的人看(本仓栽过两次同款:BT 层级、 + * key_person_change);current_stage 的取值域由本活动自己写死,两档映射即完整。 + */ +.proto-root .jc-tag.t-reqstat.v-pending::after { content: "待处理"; } +.proto-root .jc-tag.t-reqstat.v-responded::after { content: "已响应"; } +.proto-root .jc-tag.t-reqstat.v-won::after { content: "已成交"; } +.proto-root .jc-tag.t-reqstat.v-abandoned::after { content: "已放弃"; } +.proto-root .jc-tag.t-reqstat.v-closed::after { content: "已关闭"; } +.proto-root .jc-tag.t-stage.v-assessing::after { content: "评估中"; } +.proto-root .jc-tag.t-stage.v-gate03::after { content: "提交 ACT-GATE-03"; } /* 绑定区:四项锁定的灰底只读行 */ .proto-root .jc-bound { display: grid; grid-template-columns: 140px 1fr; gap: 6px 14px; padding: 10px 14px; margin: 8px 0 14px; background: var(--soft); border: 1px solid var(--line-soft); border-radius: 9px; font-size: 13px; } .proto-root .jc-bound b { color: var(--ink-3); font-weight: 600; } @@ -677,8 +689,8 @@ const P01_TABLE = `
- - + +
${ro('verification_requirement')} ${ro('output_ref', 'mono')} ${ro('next_action')}${ro('current_stage', 'mono')}${ro('request_status', 'mono')}${tag('t-stage', 'current_stage')}${tag('t-reqstat', 'request_status')}
`; From 7d743635eb4485e84fcabd0af5456b3893429eee Mon Sep 17 00:00:00 2001 From: xcs Date: Thu, 27 Aug 2026 00:10:28 +0800 Subject: [PATCH 08/21] =?UTF-8?q?docs(w05):=20=E6=95=B0=E6=8D=AE=E5=9F=9F?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E8=AF=B4=E6=98=8E=20=E2=80=94=E2=80=94=20ACT?= =?UTF-8?q?-WORK-05=20=E5=B7=AE=E5=BC=82=E8=AF=84=E4=BC=B0=E5=8D=81?= =?UTF-8?q?=E5=BC=A0=E5=8F=B0=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh --- docs/数据域字段说明-W05.md | 472 +++++++++++++++++++++++++++++++++++++ 1 file changed, 472 insertions(+) create mode 100644 docs/数据域字段说明-W05.md diff --git a/docs/数据域字段说明-W05.md b/docs/数据域字段说明-W05.md new file mode 100644 index 0000000..f9c5262 --- /dev/null +++ b/docs/数据域字段说明-W05.md @@ -0,0 +1,472 @@ +# 数据域字段说明 —— ACT-WORK-05 差异评估 + +> 对应设计中心 → 数据域「ACT-WORK-05 · 差异评估」分组。字段以活动清单的台账声明为准 +> (`activities/part-substitution-assessment/src/manifest.ts` 的 `declarations.ledger`); +> 主数据六张(`customer.profile` / `parts.profile` / `parts.document` / `demand.request` / +> `substitution.relation` + `difference_item` / `validation.record`)的**物理结构**由平台基线 +> `apps/api/src/app/seed-mvp-data.ts` 的 `MASTER_OBJECTS` 物化(字段名与《SS02-SS03-SS04_Demo +> 台账字段细化与统一方案》§4.2 一字不差),声明合并对基线是**纯 add-column**: +> 活动只追加自己要用的列与标记(required / tally / refersTo / requiredWhen / distinct), +> 不改基线已有列的语义。那份声明同时是**授权面**:没声明的列写不进去,也不会出现在件上、 +> 判据里、数据域的活动分组里。 + +## 怎么读这份文档 + +- **★ = 重点字段**。每张表挑 5–8 个,在「重点字段」小节里**按重要程度从高到低**排; + 数据结构表里在列名后打 ★。 +- **【人填】= 人在活动过程中亲手填、提交后入库的字段**(执行工单 / 数据工单的件上编辑, + 或数据录入卡)。这类字段**一律进重点**:它们是这一单里唯一不能靠 AI 补出来的事实。 +- 「写入者」一栏的口径与「约束」一栏的词表,与 `docs/数据域字段说明-E04-I01.md` + 「怎么读这份文档」是**同一套**,不重复抄一遍:人(件上)/ 人(录入卡)/ AI / 工具 / 基座 / + 承继;主键 / 自增 / 必填 / 建议填 / 取值域 / 引用 / 去重计数 / 脱敏。 + requiredWhen 类字段(某一列到某个值才必填)统一写「条件必填 · <字段>=<值> 时」—— + 与 E04-I01 的「发布时必填」是同一种约束机制,ACT-WORK-05 的触发条件不止「发布」一种, + 所以逐列写清条件,不套用那个专用词。 +- ACT-WORK-05 比 E04/I01 多两种「写入者」写法,单列在这里: + - **AI(据人的意见改)** —— S4 审核决策卡驳回、T_release 打回重做时,人写的是自然语言意见, + 不是结构化字段值;基座按字面比对定位到哪一行,AI 照意见改那一行的具体列(同 PK 覆盖写), + 意见本身不落进任何一张台账的某一列。 + - **AI(抄自 input_gap.supplement)** —— `demand.request` 的 `application_scenario` / + `bom_position` 两列,人是在 `sub_eval.input_gap` 的「补充内容」列作答,S1 收口那一拍才由 + AI 抄进 P-01;人没有直接编辑过 `demand.request` 本身。 + +### 一览 + +| 全景 | 台账地址 | 人读名 | 性质 | 本活动用到的列 | 人填列 | +|---|---|---|---|---|---| +| L-02 | `customer.profile` | 客户档案 | 主数据 · 共享 | 4 | 3(录入卡,仅新客户) | +| P-08 | `parts.profile` | 芯片主数据 | 主数据 · 共享 | 12 | 12(数据工单件上) | +| P-10 | `parts.document` | 原厂技术资料库 | 主数据 · 共享 | 9 | 9(数据工单件上) | +| P-01 | `demand.request` | 客户需求主记录 | 主数据 · 共享 | 17 | 0(应用场景 / BOM 位经执行单由 AI 抄入) | +| P-11 | `substitution.relation` | 替代关系主记录 | 主数据 · 共享 | 17 | 0(只审不改) | +| P-11 子 | `substitution.difference_item` | 差异明细 | 主数据 · 共享 | 15 | 0 | +| P-06 | `validation.record` | 芯片验证记录 | 主数据 · 共享 | 8 | 0(只写验证要求) | +| — | `sub_eval.pn_check` | 料号存在性核对 | 本活动工作台账 | 6 | 0(决策卡定放弃) | +| — | `sub_eval.input_gap` | 评估输入清单 | 本活动工作台账 | 8 | 3(补充内容 / 加行) | +| — | `sub_eval.verdict` | 本次评估结论 | 本活动工作台账 | 14 | 0 | + +九步(0.5 拆两项数据工单):绑定与存在性核对(S0_bind)→(等待数据:S0d_p08_master → +S0d_p10_document,人)→ 人补评估输入(S1_input_confirm)→ 标准化对照表(S2_normalize)→ +逐项评估(S3_assess)→ 人审结论(S4_review,可循环)→ 拟落账(S5_prepare_landing)→ +人放行(T_release)。四道人环里 S0d_p08_master / S0d_p10_document / S1_input_confirm 三道 +往台账里写下人亲手填的事实,S4_review / T_release 两道是审核与签字(人写意见,AI 据意见改台账)。 + +--- + +## 一、`customer.profile` 客户档案(L-02 · 共享主数据) + +本活动平时**不建这张表**,只在客户不在库(启动入参 `customerCode` 在 `customer.profile` +里查无记录)那一刻代行建档职责:S0_bind 发一张数据录入卡,人核对名称与行业区域后确认。 +除此之外整个活动流程里再不碰这张表。 + +**重点字段(按重要程度)** + +1. `customer_code` ★ 主键 —— 值取自启动入参 `customerCode`,AI 不许自编;人在录入卡上核对确认,不是自由填写 +2. `name` ★【人填】必填 —— 无名档案进不了正式表(与 E04 同一条墙);`customerName` 有给就 AI 预填,人核对 +3. `industry` ★【人填】建议填 —— 所属行业,录入卡上从零填 +4. `region` ★【人填】建议填 —— 区域,录入卡上从零填 + +**数据结构** + +| 列名 | 人读名 | 类型 | 约束 | 写入者 | 说明 | +|---|---|---|---|---|---| +| `customer_code` ★ | 客户编码 | text | 主键 | AI(抄入参 customerCode)/ 人(录入卡确认) | 老客户沿用,不经这张卡 | +| `name` ★ | 客户名称 | text | 必填 | AI 预填(`customerName`)/ 人(录入卡) | 没传 `customerName` 就整格空着让人填 | +| `industry` ★ | 所属行业 | text | 建议填 | 人(录入卡) | | +| `region` ★ | 区域 | text | 建议填 | 人(录入卡) | | + +--- + +## 二、`parts.profile` 芯片主数据(P-08 · 共享主数据) + +一张表:料号一行,标准化参数集是它的一个字段(一行一参数 +`参数名 | 值 | 单位 | hard/soft | 说明`,值是「未披露」的进 UNKNOWN)。库里按 `internal_pn` +查无记录 → 发**数据工单**,人在工作区的件上从零建整条(可加行);有上传 datasheet 就 +AI 先按文本预填、人核对,没有就人全填;确认落草稿层,T_release 放行时才与其余表一起并入 +正式版。 + +**重点字段(按重要程度)** + +1. `internal_pn` ★【人填】主键 —— 全链路只认它,不可自由文本 +2. `normalized_parameter_set` ★【人填】必填 —— 差异评估逐项比对的唯一输入;「未披露」进 UNKNOWN +3. `lifecycle_status` ★【人填】必填 · 取值域 `active/nrnd/eol/discontinued/risk` +4. `package` ★【人填】必填 —— 硬约束 +5. `specification_description` ★【人填】必填 +6. `brand_name` ★【人填】必填 +7. `datasheet_ref` / `datasheet_version` ★【人填】建议填(成对)—— 证据回链 + +**数据结构** + +| 列名 | 人读名 | 类型 | 约束 | 写入者 | 说明 | +|---|---|---|---|---|---| +| `internal_pn` ★ | 内部料号 | text | 主键 | 人(件上)/ 种子 | 已在库的候选(如样例里预播的 LM2596S-5.0)不经这张单 | +| `brand_name` ★ | 品牌名称 | text | 必填 | 人(件上)/ AI 预填 | | +| `manufacturer_name` | 制造商名称 | text | 建议填 | 人(件上) | 与品牌不一致时分开记 | +| `specification_description` ★ | 规格描述 | text | 必填 | 人(件上)/ AI 预填 | | +| `normalized_parameter_set` ★ | 标准化参数集 | text | 必填 | 人(件上)/ AI 预填 | 一行一参数,不建子表 | +| `package` ★ | 封装 | text | 必填 | 人(件上)/ AI 预填 | 硬约束 | +| `process_technology` | 工艺 | text | 建议填 | 人(件上) | 硬约束 | +| `lifecycle_status` ★ | 生命周期状态 | text | 必填 · 取值域 | 人(件上,下拉) | | +| `eol_pcn_status` | EOL/PCN 状态 | text | 建议填 | 人(件上) | | +| `datasheet_ref` ★ | Datasheet 引用 | text | 建议填 | 人(件上)/ AI 预填 | | +| `datasheet_version` ★ | Datasheet 版本 | text | 建议填 | 人(件上)/ AI 预填 | 有引用就该有版本 | +| `application_scenarios` | 适配应用场景 | text | 建议填 | 人(件上) | | + +--- + +## 三、`parts.document` 原厂技术资料库(P-10 · 共享主数据) + +按 `internal_pn` 查无资料记录 → 发**数据工单**(与 P-08 那张分开,一张表一张单, +在 P-08 那张确认之后再发);S0d_p08_master 那一步先写骨架(`document_type` / +`document_status` / `brand_name` 三格能填就填),人在 P-10 数据工单的件上补齐其余列。 +同一料号多份资料就多加几行,序号 `01`/`02`… 人自己续(不是自增列)。 + +**重点字段(按重要程度)** + +1. `document_ref` ★【人填】必填 —— 资料文件链接,差异评估的证据引用要指到它 +2. `internal_pn` ★【人填】主键之一 —— 关联料号,与 P-08 同一个内部料号 +3. `doc_seq` ★【人填】主键之一 —— 同一料号下 01/02… 人自己续号 +4. `document_type` ★【人填】必填 · 取值域 —— `datasheet/app_note/reference_design/white_paper/sample_doc/commitment_letter/other` +5. `document_title` ★【人填】必填 +6. `document_version` ★【人填】必填 +7. `document_status` ★【人填】必填 · 取值域 `valid/expired/pending/void` +8. `citation_locator` ★【人填】建议填 —— 页码 / 章节 / 表格,证据定位精确到这里 + +**数据结构** + +| 列名 | 人读名 | 类型 | 约束 | 写入者 | 说明 | +|---|---|---|---|---|---| +| `internal_pn` ★ | 关联内部料号 | text | 主键之一 | 人(件上) | 与 P-08 那一行同一个键 | +| `doc_seq` ★ | 资料序号(同一料号下 01/02…) | text | 主键之一 | 人(件上) | 不是自增列,人自己续号 | +| `document_type` ★ | 资料类型 | text | 必填 · 取值域 | AI 预填骨架(S0d_p08_master)/ 人(件上,下拉) | | +| `brand_name` | 品牌名称 | text | 建议填 | AI 预填骨架 / 人(件上) | | +| `document_title` ★ | 资料标题 | text | 必填 | 人(件上) | | +| `document_version` ★ | 资料版本 | text | 必填 | 人(件上) | | +| `document_ref` ★ | 资料文件链接 | text | 必填 | 人(件上) | | +| `citation_locator` ★ | 证据定位(页码/章节/表格) | text | 建议填 | 人(件上) | | +| `document_status` ★ | 资料状态 | text | 必填 · 取值域 | AI 预填骨架(pending)/ 人(件上,下拉) | | + +--- + +## 四、`demand.request` 客户需求主记录(P-01 · 共享主数据) + +S0_bind 新建 V1(客户 / 需求描述 / 原 PN 三项照入参原样落,启动时未收的 `application_scenario` +/ `bom_position` 先不写);S1_input_confirm 人在评估输入清单上把这两项的「补充内容」补上, +AI 收口那一拍原样抄回本表;S5_prepare_landing 放行前回写风险等级 / 验证要求 / 输出链接 / +下一步动作 / 当前阶段五项(`current_stage = "gate03"` 那一刻这五项转为必填)。**本表没有一列 +是人直接编辑的**——人的手笔经由 `sub_eval.input_gap.supplement` 间接进来。 + +**重点字段(按重要程度)** + +1. `request_id` ★ 主键 —— 需求编号,与 `eval_ref` 用同一串时刻数字铸出 +2. `customer_code` ★ 必填 · 引用 `customer.profile.customer_code` +3. `request_description` ★ 必填 —— 客户需求原话,S0 落一次,不许改写 +4. `original_internal_pn` ★ 必填 +5. `application_scenario` ★ 建议填 —— 启动时未收,人在评估输入清单补的「补充内容」由 AI 抄进这一格(本表唯一带着人手笔的两列之一) +6. `bom_position` ★ 建议填 —— 同上 +7. `risk_level` ★ 条件必填 · `current_stage=gate03` 时 —— 放行回写五项之一,取各候选风险最高档 +8. `verification_requirement` ★ 条件必填 · `current_stage=gate03` 时 —— 列出挂的验证任务号 + +**数据结构** + +| 列名 | 人读名 | 类型 | 约束 | 写入者 | 说明 | +|---|---|---|---|---|---| +| `request_id` ★ | 需求编号 | text | 主键 | AI | `REQ-` + 案号数字串 | +| `customer_code` ★ | 客户编码 | text | 必填 · 引用 `customer.profile.customer_code` | AI | | +| `requirement_version` | 需求版本 | text | 必填 | AI | 恒 `V1` | +| `request_type` | 需求类型 | text | 必填 | AI | 恒「技术需求」 | +| `request_description` ★ | 需求描述 | text | 必填 | AI | 入参原话,不改写 | +| `input_snapshot_ref` | 输入快照 | text | 必填 | AI | `SNAP-` + 案号数字串 | +| `original_internal_pn` ★ | 原内部料号 | text | 必填 | AI | | +| `bom_position` ★ | BOM 位 | text | 建议填 | AI(抄自 `sub_eval.input_gap.supplement`) | 启动时未收 | +| `application_scenario` ★ | 应用场景 | text | 建议填 | AI(抄自 `sub_eval.input_gap.supplement`) | 启动时未收 | +| `technical_constraints` | 技术约束 | text | — | AI | 人加的 input_gap 行汇总成一句 | +| `risk_level` ★ | 风险等级 | text | 条件必填 · `current_stage=gate03` | AI | 取各候选 `risk_level` 最高档 | +| `verification_requirement` ★ | 验证/复测/确认要求 | text | 条件必填 · `current_stage=gate03` | AI | 列出所有挂的 VAL 号 | +| `request_status` | 需求状态 | text | 必填 · 取值域 | AI | `pending → responded` | +| `current_activity` | 当前 Activity | text | 条件必填 · `current_stage=gate03` | AI | 恒 `ACT-WORK-05` | +| `current_stage` | 当前阶段 | text | — | AI | `assessing → gate03` | +| `next_action` | 下一步动作 | text | 条件必填 · `current_stage=gate03` | AI | 恒「提交 ACT-GATE-03 评审放行」 | +| `output_ref` | 标准输出链接 | text | 条件必填 · `current_stage=gate03` | AI | `RPT-` + 案号数字串 | + +--- + +## 五、`substitution.relation` 替代关系主记录(P-11 · 共享主数据) + +一个未放弃的候选一条:S2_normalize 写骨架(不含推荐/风险/摘要)、S3_assess 用两枚工具的 +出参回填推荐等级与风险(**AI 原样抄,不改一字**)、S4_review 只审不改(人写意见,AI 据 +意见改具体列)、S5_prepare_landing 把 `analysis_status` 推到 `fae_confirmed`。**本活动没有 +一列是人直接编辑的** —— 人的动作是通过 S4/T_release 的决策卡意见间接改台账。 + +**重点字段(按重要程度)** + +1. `substitution_relation_id` ★ 主键 +2. `eval_ref` ★ 必填 —— **本活动新增列(基线没有)**,评估案号,与 `pn_check` / `verdict` 同一个 `eval_ref` 串联全单 +3. `candidate_internal_pn` ★ 必填 · 引用 `parts.profile.internal_pn` +4. `original_internal_pn` ★ 必填 · 引用 `parts.profile.internal_pn` +5. `recommendation_level` ★ 必填 · 取值域 `recommended/conditional/alternate/not_recommended` —— 工具算,AI 照抄不改一字 +6. `analysis_status` ★ 必填 · 取值域 —— 分析层状态,S5 推到 `fae_confirmed` +7. `risk_level` ★ 必填 · 取值域 `low/medium/high/critical` —— 工具算 +8. `rule_version` ★ 必填 —— 随结论落账,锁定 `R-2026.07` + +**数据结构** + +| 列名 | 人读名 | 类型 | 约束 | 写入者 | 说明 | +|---|---|---|---|---|---| +| `substitution_relation_id` ★ | 替代关系ID | text | 主键 | AI | `REL-<案号数字串>-01/02…` | +| `eval_ref` ★ | 评估案号 | text | 必填 | AI | 本活动新增列(基线没有) | +| `request_id` | 关联需求编号 | text | 必填 | AI | | +| `bom_position` | 关联 BOM 位 | text | 建议填 | AI | 照 `demand.request` 现值抄 | +| `original_internal_pn` ★ | 原内部料号 | text | 必填 · 引用 `parts.profile.internal_pn` | AI | | +| `candidate_internal_pn` ★ | 候选内部料号 | text | 必填 · 引用 `parts.profile.internal_pn` | AI | | +| `substitution_type` | 替代类型 | text | 必填 · 取值域 | AI | `pin_to_pin/functional/parametric/other` | +| `recommendation_level` ★ | 推荐等级 | text | 必填 · 取值域 | 工具(`recommend`)→ AI 照抄 | | +| `recommendation_reason` | 推荐理由 | text | 必填 | AI(可按人的驳回意见改) | 工具 reason 扩写成人话 | +| `difference_summary` | 差异摘要 | text | 必填 | AI | 「7 项比对:4 MATCH / 1 MISMATCH / 1 UNKNOWN / 1 CONDITIONAL」句式 | +| `risk_level` ★ | 替代风险等级 | text | 必填 · 取值域 | 工具(`recommend`)→ AI 照抄 | | +| `verification_requirement` | 验证要求 | text | 条件必填 · `recommendation_level=conditional` | AI | 列出 VAL 号 | +| `validation_id` | 关联验证ID | text | — | AI | | +| `analysis_status` ★ | 分析状态 | text | 必填 · 取值域 | AI | `ai_draft → fae_confirmed`(其余三档本活动不写) | +| `analysis_report_ref` | 替代分析报告 | text | 建议填 | AI | `RPT-<案号数字串>` | +| `confidence_score` | 证据可信度 | number | 建议填 | AI | 取各行 `confidence` 最小值 | +| `rule_version` ★ | 差异规则版本 | text | 必填 | AI | 锁定 `R-2026.07` | + +> **只读边界**:基线在这张表上还建了 `customer_approval_status` / `customer_approval_at` / +> `customer_approval_ref` 三列,由**独立审批流**维护;本活动的声明里没有这三列的写权 +> (禁令 `no-customer-approval-write`)。落账确认那一屏那一格是静态文案 +> 「未提交 · 由审批流程维护,本活动不产生」,不是可取值的 `data-slot` +> ——审批流做起来后才改成真读数(见 `docs/debt.md`)。 + +--- + +## 六、`substitution.difference_item` 差异明细(P-11 子表 · 共享主数据) + +一条参数一条记录。S2_normalize 按原 PN 的参数集写骨架(不含四态/影响)、S3_assess 第一拍 +调 `compare-parameters` 拿到四态与风险后**原样抄回** `difference_status` / `compare_result` / +`risk_level`,AI 自己只补写证据引用 / 影响说明 / UNKNOWN 去向;S4_review 驳回时人写的意见由 +AI 定位到具体行后改 `impact_statement` / `unknown_disposition` / `evidence_ref`。 +`substitution_relation_id` 标了 `distinct`,「几条关系已经有明细」就是数它的不同值数 +(S2 收步、S3 收步都读这个数)。 + +**重点字段(按重要程度)** + +1. `substitution_relation_id` ★ 主键之一 · 去重计数 —— 「几条关系已有明细」就是数它的不同值 +2. `difference_item_id` ★ 主键之一 +3. `difference_status` ★ 必填 · 取值域 `match/mismatch/unknown/conditional` —— 工具算,AI 原样抄 +4. `compare_result` ★ 建议填 —— **本活动新增列(基线没有)**,工具出参原样存底,供复核 +5. `constraint_type` ★ 必填 · 取值域 `hard/soft` —— 决定这一项差异判得严不严 +6. `evidence_ref` ★ 必填 —— 证据引用,验收判据「证据链可信」逐行核对它 +7. `impact_statement` ★ 必填 —— MATCH 写「无」,其余写清对应用 / 验证 / 报价的影响 +8. `unknown_disposition` ★ 条件必填 · `difference_status=unknown` 时 —— **本活动新增列(基线没有)**,UNKNOWN 的去向(挂验证任务号 / 补来源动作) + +**数据结构** + +| 列名 | 人读名 | 类型 | 约束 | 写入者 | 说明 | +|---|---|---|---|---|---| +| `substitution_relation_id` ★ | 替代关系ID | text | 主键之一 · 去重计数 | AI | | +| `difference_item_id` ★ | 差异明细ID | text | 主键之一 | AI | `<关系id>-P01/P02…` | +| `parameter_name` | 标准参数名 | text | 必填 | AI | | +| `original_value` | 原物料参数值 | text | 必填 | AI | | +| `candidate_value` | 候选物料参数值 | text | 必填 | AI | 候选没有这个参数写「未披露」 | +| `parameter_unit` | 参数单位 | text | 建议填 | AI | 以原 PN 为准 | +| `constraint_type` ★ | 约束类型 | text | 必填 · 取值域 | AI | 原 PN 那一行的 hard/soft | +| `compare_result` ★ | 比对结果(工具出参) | text | 建议填 | 工具(`compare-parameters`)→ AI 照抄 | 本活动新增列 | +| `difference_status` ★ | 差异状态 | text | 必填 · 取值域 | 工具(`compare-parameters`)→ AI 照抄 | | +| `impact_statement` ★ | 影响说明(MATCH 写「无」) | text | 必填 | AI | | +| `unknown_disposition` ★ | UNKNOWN 去向(验证任务号 / 补来源动作) | text | 条件必填 · `difference_status=unknown` | AI(可按人的驳回意见改) | 本活动新增列 | +| `risk_level` | 风险等级 | text | 必填 · 取值域 `low/medium/high` | 工具(`compare-parameters`)→ AI 照抄 | | +| `evidence_ref` ★ | 差异证据引用 | text | 必填 | AI(可按人的驳回意见改) | | +| `confidence` | AI 置信度 | number | 建议填 | AI | 0~1 | +| `rule_version` | 差异规则版本 | text | 必填 | AI | 锁定 `R-2026.07` | + +--- + +## 七、`validation.record` 芯片验证记录(P-06 · 共享主数据) + +事实类表:本活动只在 S3_assess 给每个 UNKNOWN 差异项挂一行「验证要求」占位 +(`validation_status` 恒写 `not_started`),不经数据工单建 —— 真正测完、写测试结果是 +另一件事(下游活动或线下流程的事)。S4_review 驳回若要补验证任务,同样由 AI 加一行。 + +**重点字段(按重要程度)** + +1. `validation_id` ★ 主键 +2. `validation_internal_pn` ★ 必填 · 引用 `parts.profile.internal_pn` +3. `validation_status` ★ 必填 · 取值域 —— 本活动只写 `not_started`(占位待测) +4. `next_validation_action` ★ 必填 —— 要测什么、什么条件 +5. `validation_type` ★ 必填 · 取值域 +6. `request_id` ★ 必填 —— 关联需求编号 + +**数据结构** + +| 列名 | 人读名 | 类型 | 约束 | 写入者 | 说明 | +|---|---|---|---|---|---| +| `validation_id` ★ | 验证ID | text | 主键 | AI | `VAL-<案号数字串>-01/02…` | +| `request_id` ★ | 关联需求编号 | text | 必填 | AI | | +| `validation_internal_pn` ★ | 验证内部料号 | text | 必填 · 引用 `parts.profile.internal_pn` | AI | 候选 | +| `validation_type` ★ | 验证类型 | text | 必填 · 取值域 | AI | 恒 `pin_to_pin`(本活动只挂这一种) | +| `validation_status` ★ | 验证状态 | text | 必填 · 取值域 | AI | 恒 `not_started` | +| `test_condition` | 测试条件 | text | 建议填 | AI | | +| `next_validation_action` ★ | 后续验证动作 | text | 必填 | AI(可按人的驳回意见改) | 要测什么、什么条件 | +| `evidence_ref` | 验证证据引用 | text | 建议填 | AI | | + +--- + +## 八、`sub_eval.pn_check` 料号存在性核对(本活动工作台账) + +S0_bind 逐个料号(原 PN + 每个候选)在 `parts.profile` 里核对一遍写下的表:原 PN 一行、 +候选各一行。`exists_status` 是这张表的核心结论 —— 两项数据工单的入场哨兵、S1 的入场哨兵 +都读它。候选 ≥2 且不在库时,S0d_p08_master 发决策卡问放弃还是创建,人选的答案决定这一行 +`exists_status` 改成 `abandoned` 还是等数据工单确认后改成 `created`。 + +**重点字段(按重要程度)** + +1. `exists_status` ★ 必填 · 取值域 `found/absent/created/abandoned` —— 存在性检查的核心结论,S0d 两项与 S1 的入场哨兵都读它 +2. `internal_pn` ★ 主键之一 +3. `eval_ref` ★ 主键之一 —— 串联本单十张表的案号 +4. `role` ★ 必填 · 取值域 `original/candidate` +5. `abandon_reason` ★ 条件必填 · `exists_status=abandoned` 时 —— 人在决策卡选放弃、没写理由时,AI 补写「负责人本轮放弃(未注明原因)」 + +**数据结构** + +| 列名 | 人读名 | 类型 | 约束 | 写入者 | 说明 | +|---|---|---|---|---|---| +| `eval_ref` ★ | 评估案号 | text | 主键之一 | AI | | +| `internal_pn` ★ | 内部料号 | text | 主键之一 | AI | | +| `role` ★ | 角色 | text | 必填 · 取值域 | AI | `original`(恰一行)/ `candidate` | +| `exists_status` ★ | 在库状态 | text | 必填 · 取值域 | AI | `found`(在库)→ 或 `absent` → `created`/`abandoned`(数据工单/决策卡确认后改) | +| `abandon_reason` | 放弃原因 | text | 条件必填 · `exists_status=abandoned` | AI(据人的决策卡选项改) | 人选放弃未写理由时 AI 补写默认句 | +| `note` | 备注 | text | — | AI | 如「PCN 转 NRND」 | + +--- + +## 九、`sub_eval.input_gap` 评估输入清单(本活动工作台账) + +S1_input_confirm 一拍写全:已绑定四项(status=ok)、待补两项(应用场景/BOM 位,status=gap)、 +默认可改三项(status=default)、逐颗候选「未披露」参数各一行(status=gap)、齐备项(原 PN 资料 +版本,status=ok)。**这张表是本活动唯一让人直接在件上补内容的执行工单载体**:人在 +「补充内容」列把 status=gap 的行填上答案,也可以加一行补充 AI 没列出的资料项、删一行标 +「不适用」。 + +**重点字段(按重要程度)** + +1. `supplement` ★【人填】条件必填 · `status=gap` 时 —— 缺口行的答案,这一单里唯一不能靠 AI 补出来的事实 +2. `status` ★ 必填 · 取值域 `gap/default/ok` —— 决定哪一行必须有 `supplement` +3. `item_key` ★【人填,加行时】主键之一 —— 英文短键;已绑定 / 待补 / 默认 / 待确认各占一批行 +4. `input_item` ★【人填,加行时】必填 —— 人读名,加一行 = 补充 AI 没列出的资料项 +5. `origin` ★ —— `ai` / `human-added`,标出哪几行是人加的(与 E04 `candidate.origin` 同一口径) +6. `source_ledger` ★ 建议填 —— 缺口来自哪张台账(P-01/P-08/P-09/P-10) + +**数据结构** + +| 列名 | 人读名 | 类型 | 约束 | 写入者 | 说明 | +|---|---|---|---|---|---| +| `eval_ref` | 评估案号 | text | 主键之一 | AI | | +| `item_key` ★ | 输入项键 | text | 主键之一 | AI / **人(件上,加行时)** | 英文短键,如 `application_scenario` | +| `input_item` ★ | 输入项 | text | 必填 | AI / **人(件上,加行时)** | 人读名 | +| `source_ledger` ★ | 来源台账 | text | 建议填 | AI | | +| `status` ★ | 状态 | text | 必填 · 取值域 | AI | `gap`(待补)/`default`(默认·可改)/`ok`(齐备) | +| `detail` | 绑定值 / 缺口说明 | text | 建议填 | AI | | +| `supplement` ★ | 补充内容 | text | 条件必填 · `status=gap` | **人(件上)** | 唯一的人手笔答案 | +| `origin` ★ | 来源(ai/human-added) | text | — | AI | | + +--- + +## 十、`sub_eval.verdict` 本次评估结论(本活动工作台账) + +一单一行的收口结论,挂着几面「人环走到哪儿了」的记账旗 —— 它们是下一步能不能开工的哨兵 +(与 E04 `crm_coverage.verdict` 同一种设计)。 + +**重点字段(按重要程度)** + +1. `input_confirmed_total` ★ —— 评估输入已确认(人环凭据),S2 标准化那一步的入场哨兵之一 +2. `review_approved_total` ★ —— 差异结论已审核通过(仅通过时置一),S5 拟落账那一步的入场哨兵 +3. `landing_prepared_total` ★ —— 拟落账已备好,T_release 放行门的入场哨兵 +4. `summary` ★ —— 本次评估结论,归档件上的那段话 +5. `relation_total` ★ —— 替代关系条数 +6. `recommended_total` / `conditional_total` / `alternate_total` / `not_recommended_total` ★ —— 四档分布,归档件摘要与放行口 `assessment-released` 载荷都用它 + +**数据结构** + +| 列名 | 人读名 | 类型 | 约束 | 写入者 | 说明 | +|---|---|---|---|---|---| +| `eval_ref` | 评估案号 | text | 主键 | AI | | +| `request_id` | 需求编号 | text | — | AI | | +| `rule_version` | 规则版本 | text | — | AI | 锁定 `R-2026.07` | +| `original_internal_pn` | 原内部料号 | text | — | AI | | +| `candidate_total` | 候选数 | integer | — | AI | 候选数组长度 | +| `input_confirmed_total` ★ | 评估输入已确认 | integer | — | AI | 人环凭据 | +| `review_approved_total` ★ | 差异结论已审核通过 | integer | — | AI | 人环凭据(仅通过时置一) | +| `landing_prepared_total` ★ | 拟落账已备好 | integer | — | AI | S5 写完时置一 | +| `relation_total` ★ | 替代关系条数 | integer | — | AI | | +| `recommended_total` ★ | 推荐 | integer | — | AI | | +| `conditional_total` ★ | 条件推荐 | integer | — | AI | | +| `alternate_total` ★ | 备选 | integer | — | AI | | +| `not_recommended_total` ★ | 不推荐 | integer | — | AI | | +| `summary` ★ | 本次评估结论 | text | — | AI | 归档件上的那段话 | + +--- + +## 十一、人填字段总表(按人办事的那一屏) + +| 活动 · 屏 | 卡的形式 | 落进哪张表 | 人亲手填的列 | +|---|---|---|---| +| S0_bind · 新客户建档(仅客户不在库) | 数据录入卡(entry) | `customer.profile` | `name` · `industry` · `region`(`customer_code` 由入参给,人核对) | +| S0d_p08_master · 候选放弃/创建抉择(候选≥2 且部分缺库) | 决策卡(decide) | 间接改 `sub_eval.pn_check.exists_status` | 选项值(`create`/`abandon`/`custom:`,不直接写字段) | +| S0d_p08_master · 数据工单·芯片主数据创建(P-08) | 执行工单(件上编辑 + 增删行) | `parts.profile` | 全部 12 列(`internal_pn` / `brand_name` / `manufacturer_name` / `specification_description` / `normalized_parameter_set` / `package` / `process_technology` / `lifecycle_status` / `eol_pcn_status` / `datasheet_ref` / `datasheet_version` / `application_scenarios`) | +| S0d_p10_document · 数据工单·原厂技术资料创建(P-10) | 执行工单(件上编辑 + 增删行) | `parts.document` | 全部 9 列(`internal_pn` / `doc_seq` / `document_type` / `brand_name` / `document_title` / `document_version` / `document_ref` / `citation_locator` / `document_status`) | +| S1_input_confirm · 评估输入与资料补齐确认 | 执行工单(件上编辑 + 增删行) | `sub_eval.input_gap` | `supplement`(缺口行必填);加一行时另填 `item_key` / `input_item` | +| S4_review · 差异评估结论审核 | 决策卡循环(通过 / 驳回 + 意见) | 间接改 `substitution.difference_item` / `substitution.relation` / `validation.record` | 意见文本(AI 据此改 `evidence_ref` / `impact_statement` / `unknown_disposition` / `recommendation_reason`;推荐等级不许改) | +| T_release · 评审放行与落账确认 | 验收工单(签字,可打回重做) | `parts.profile` / `parts.document` / `demand.request` / `substitution.relation` + `difference_item` / `validation.record` 并入正式版 | 裁决与意见(打回后 AI 按意见改台账本身,不开新行) | + +--- + +## 十二、在数据域里看这些表时的几条提醒 + +1. **主数据是共享的,物理表里还有别的用途留下的列,本活动不写它们**: + - `customer.profile` 还有 `grade` / `owner` / `payment_terms` / `credit_limit` / + `profile_tags` / `status`(报价链早先声明的客户主台账列) + - `parts.profile` 还有 `customer_pn` / `pin_to_pin_relation_ref` + - `parts.document` 还有 `document_date` / `source_contact_ref` + - `demand.request` 还有 `project_code` / `bom_id` / `customer_pn` + - `substitution.relation` 还有 `project_code`,以及客户批准三列 + `customer_feedback` + (见下条,单独说明只读边界) + - `validation.record` 还有 `customer_code` / `test_parameters` / `test_result` / + `issue_risk` / `sample_version` / `sample_lot` —— 本活动只写「验证要求」占位阶段, + 不写实测结果(那是真验证发生后才有的事实)。 + + 记录浏览里看到它们空着是正常的;判断一列还算不算数,以**活动清单的声明**为准, + 不以物理表结构为准。 + +2. **本活动往两张主数据表新增了列(基线没有,声明里 add-column 长出来的)**: + `substitution.relation.eval_ref`(评估案号,串联本单十张表)与 + `substitution.difference_item.compare_result` / `unknown_disposition` + (工具出参原样存底、UNKNOWN 去向)。这三列只对 ACT-WORK-05 生效,不影响其它活动 + 对这两张表的读写。 + +3. **`customer_approval_status` 是只读边界,不是本活动没做完**:基线在 + `substitution.relation` 上建了 `customer_approval_status` / `customer_approval_at` / + `customer_approval_ref` 三列,由**独立审批流**维护;本活动的声明里没有这三列的写权 + (禁令 `no-customer-approval-write`)。放行前那一屏那一格是静态文案 + 「未提交 · 由审批流程维护,本活动不产生」,不是取值的 `data-slot`(见 `docs/debt.md`)。 + +4. **`parts.profile` 的标准化参数集是一个 text 列,不是子表**:`normalized_parameter_set` + 存的是一行一参数的行语法「参数名 | 值 | 单位 | hard/soft | 说明」,四态比对工具的入参 + 都靠**解析这段文本**,不是关系型的多行记录 —— 用户当场定的口径,《SS02-SS03-SS04》 + 字面上写的 Structure / 子表在本平台落成这样(见 `docs/debt.md`「参数集是 text 列的行语法」)。 + +5. **数据工单是零输入执行工单 + 工作区可加行的件**:料号在 `parts.profile` / + `parts.document` 里查无记录时,人从零建整条记录;确认落草稿层,T_release 放行那一刻 + 才与其余表一起并入正式版(平台铁律②:写只有草稿与意向,终局唯放行)。一张表一张单: + P-08 一张、P-10 一张。 + +6. **试跑单默认也落真库**:与 E04 / I01 同一个开关(`sys.task.land_master`,测试台起单 + 默认打开)—— 跑一遍活动,新建的客户档案、料号、资料记录都会真的进正式表;要它不落, + 起单时显式关掉。 + +--- + +*字段口径以 `activities/part-substitution-assessment/src/manifest.ts` 的 +`declarations.ledger` 为准;六张主数据的物理结构见 `apps/api/src/app/seed-mvp-data.ts` 的 +`MASTER_OBJECTS`;计划项与卡骨架见 `activities/part-substitution-assessment/src/case-model.ts` +的 `ASSESSMENT_STEPS` 与 `NARRATIVE`;业务设计源见 `docs/SS02-ACT-WORK-05-html/` +(活动流程与工单说明 / 一键启动入参说明)与《SS02-SS03-SS04_Demo台账字段细化与统一方案》。* From e1b04f01aae6b1fd94cbcde0cb6d91fd76f7cf50 Mon Sep 17 00:00:00 2001 From: xcs Date: Thu, 27 Aug 2026 00:13:26 +0800 Subject: [PATCH 09/21] =?UTF-8?q?feat(w05):=20=E5=B7=AE=E5=BC=82=E8=AF=84?= =?UTF-8?q?=E4=BC=B0=E6=B4=BB=E5=8A=A8=E7=99=BB=E8=AE=B0=E8=BF=9B=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E4=B8=8E=E5=AE=89=E8=A3=85=E9=80=9A=E9=81=93;?= =?UTF-8?q?=E8=A3=85=E5=8C=85=E6=97=B6=E9=A1=BA=E5=B8=A6=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E7=BA=AF=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh --- apps/api/src/app/seed-activities.ts | 19 ++++++++++++++++--- tools/build-activities.ts | 2 ++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/api/src/app/seed-activities.ts b/apps/api/src/app/seed-activities.ts index 03e9dab..c94bd7c 100644 --- a/apps/api/src/app/seed-activities.ts +++ b/apps/api/src/app/seed-activities.ts @@ -15,7 +15,8 @@ import { readFile } from 'node:fs/promises'; import { join, resolve } from 'node:path'; import { activityManifestSchema } from '@contracts'; import type { Container } from './container.ts'; -import { MASTER_OBJECTS, MASTER_SAMPLE_ROWS } from './seed-mvp-data.ts'; +import { MASTER_DOMAINS, MASTER_OBJECTS, MASTER_SAMPLE_ROWS } from './seed-mvp-data.ts'; +import { W05_TOOLS, W05_TOOL_SAMPLE } from './tools-w05.ts'; /** 已开发完成、要随播种装上的真活动包(目录名 = 包目录,不是 activityId) */ /* @@ -29,8 +30,9 @@ import { MASTER_OBJECTS, MASTER_SAMPLE_ROWS } from './seed-mvp-data.ts'; * 再跑 activities:install 即可 —— 它们的 dist 照旧随 activities:build 产出。 * 退役名单:quote-intake · quote-alternative · quote-sourcing · quote-pricing · * quote-negotiate · quote-archive + * 2026-08-26 加 ACT-WORK-05(part-substitution-assessment):差异评估活动开发完成,随本轮登记进安装通道。 */ -const REAL_PACKAGES = ['crm-relationship-coverage', 'brand-content-toolkit'] as const; +const REAL_PACKAGES = ['crm-relationship-coverage', 'brand-content-toolkit', 'part-substitution-assessment'] as const; /** * 活动包根目录。 @@ -61,10 +63,21 @@ export async function installRealActivities(container: Container): Promise const applied = await container.ledger.applyDeclarations(MASTER_OBJECTS); const conflicts = applied.flatMap((r) => r.conflicts); log( - ` 主数据四域已物化:${MASTER_OBJECTS.length} 张表` + + ` 主数据 ${MASTER_DOMAINS.length} 域已物化:${MASTER_OBJECTS.length} 张表` + // 冲突要如实报:静默吞掉的话,表结构和声明对不上而没人知道 (conflicts.length === 0 ? '' : ` —— 合并冲突 ${conflicts.length} 处:${conflicts.map((c) => c.field).join('、')}`), ); + + /* + * **纯工具随活动一起注册**(2026-08-26):从前工具契约只在 db:seed 里注册,只跑 activities:install 的 + * 开发机上,活动清单声明了工具、白名单也发了,`sys.tool` 里却没有那件工具 —— 第一次跑就撞「工具不存在」。 + * 「安装即用」包含它声明过的纯工具能调得动。register 按 name+version 幂等,重复装是 no-op。 + */ + for (const contract of W05_TOOLS) { + const verdict = await container.toolbox.register({ contract, samples: [W05_TOOL_SAMPLE] }); + if (!verdict.accepted) log(` 工具 ${contract.name} 未过两审:${verdict.staticFindings.map((f) => f.message).join('; ')}`); + } + const root = activitiesRoot(); for (const dir of REAL_PACKAGES) { diff --git a/tools/build-activities.ts b/tools/build-activities.ts index 511fc07..1b1955b 100644 --- a/tools/build-activities.ts +++ b/tools/build-activities.ts @@ -19,6 +19,7 @@ import { manifest as quoteSourcing } from '../activities/quote-sourcing/src/mani import { manifest as quoteAlternative } from '../activities/quote-alternative/src/manifest.ts'; import { manifest as crmRelationshipCoverage } from '../activities/crm-relationship-coverage/src/manifest.ts'; import { manifest as brandContentToolkit } from '../activities/brand-content-toolkit/src/manifest.ts'; +import { manifest as partSubstitutionAssessment } from '../activities/part-substitution-assessment/src/manifest.ts'; const root = dirname(dirname(fileURLToPath(import.meta.url))); @@ -31,6 +32,7 @@ const PACKAGES = [ { dir: 'quote-alternative', manifest: quoteAlternative }, { dir: 'crm-relationship-coverage', manifest: crmRelationshipCoverage }, { dir: 'brand-content-toolkit', manifest: brandContentToolkit }, + { dir: 'part-substitution-assessment', manifest: partSubstitutionAssessment }, ] as const; for (const pkg of PACKAGES) { From 148d16a1b7d85dc9d35300468bc60ffbdd27f584 Mon Sep 17 00:00:00 2001 From: xcs Date: Thu, 27 Aug 2026 00:23:48 +0800 Subject: [PATCH 10/21] =?UTF-8?q?test(w05):=20e2e=20=E5=A5=97=E4=BB=B6=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20=E4=BA=94=E6=A1=88=E4=BE=8B(=E5=80=99?= =?UTF-8?q?=E9=80=89=E7=BC=BA=E5=BA=93=20/=20=E5=B8=B8=E8=B7=AF=20/=20?= =?UTF-8?q?=E5=8E=9F=20PN=20=E7=A1=AC=E9=98=BB=E5=A1=9E=20/=20=E4=B8=89?= =?UTF-8?q?=E5=80=99=E9=80=89=E5=90=AB=E4=B8=8D=E6=8E=A8=E8=8D=90=20/=20?= =?UTF-8?q?=E6=96=B0=E5=AE=A2=E6=88=B7=E5=BB=BA=E6=A1=A3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 套件代码就位(SUITES.w05 + buildEdits/expectGateBlocks 加性扩展 stageEdits/fillWhere + main() 套件名单加入 w05);node --check 与 pnpm lint 全绿,五案例结构(sample 0..4、三条 stage 正则、decideSeq/decide 取值集合、stageEdits 地址与 stages 地址对齐)已用一次性脚本 静态核对。缺 KEYCLOAK_E2E_CLIENT_SECRET,套件本身待凭证到位后跑。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh --- tools/e2e-a2.mjs | 240 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 235 insertions(+), 5 deletions(-) diff --git a/tools/e2e-a2.mjs b/tools/e2e-a2.mjs index 1364080..e609ca2 100644 --- a/tools/e2e-a2.mjs +++ b/tools/e2e-a2.mjs @@ -9,6 +9,7 @@ * node tools/e2e-a2.mjs a1 # 只跑 A1 回归(执行卡零输入 + 提交闸回落) * node tools/e2e-a2.mjs e04 # 只跑 E04 三案例(两张执行单 + 决策卡审批循环) * node tools/e2e-a2.mjs i01 # 只跑 I01 三案例(一张执行单 + 初稿审核决策卡 + 发布放行) + * node tools/e2e-a2.mjs w05 # 只跑 W05 五案例(ACT-WORK-05 差异评估:条件数据工单 P-08/P-10 + 执行单 + 决策卡审核循环 + 放行) * node tools/e2e-a2.mjs # 全跑 * node tools/e2e-a2.mjs a2 1 # 只跑 A2 的第 1 条(0 起) * @@ -507,6 +508,219 @@ 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 关落真库,主数据不落正式表,案例可反复跑。 + */ + w05: { + activityId: 'part-substitution-assessment', + address: 'sub_eval.input_gap', + blankCol: 'supplement', + stages: [ + { + match: /数据工单 · 芯片主数据创建/, + address: 'parts.profile', + rowKey: 'internal_pn', + blankCol: 'brand_name', + expectGateBlock: true, + edits: [], + // 案例没点名的料号兜底(不该走到这里 —— 每个案例都在 stageEdits 里逐颗给了) + fillAllBlank: { + brand_name: '兜底品牌', + specification_description: '兜底规格(案例没给参数集)', + package: 'TO-263-5', + lifecycle_status: 'active', + normalized_parameter_set: '输入电压范围 | 4.5~40 | V | hard | 兜底', + }, + }, + { + match: /数据工单 · 原厂技术资料创建/, + address: 'parts.document', + rowKey: 'internal_pn', + blankCol: 'document_title', + expectGateBlock: 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)· 审核先点名驳回再通过', + // 决策卡序:候选 ≥2 缺库先问 → create;审核第一张点名意见驳回;第二张通过 + decideSeq: ['create', 'custom:使能阈值那条 UNKNOWN 的去向请写明验证任务号与测试条件', 'approve'], + expectDecision: true, + expectPort: 'assessment-released', + stageEdits: { + 'parts.profile': [ + { + internal_pn: 'XL2596S-5.0', + 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: '工业电源;消费电源', + normalized_parameter_set: [ + '输入电压范围 | 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 | 原厂未给', + ].join('\n'), + }, + ], + '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 → 条件推荐(工具算的,不是模型说的) + expectRevised: { address: 'substitution.relation', where: 'candidate_internal_pn', equals: 'XL2596S-5.0', column: 'recommendation_level', contains: 'conditional' }, + }, + { + sample: 1, + name: '都在库 · 常路 · 可调版 → 备选 · 审核一次通过', + decide: 'approve', + expectPort: 'assessment-released', + expectRevised: { address: 'substitution.relation', where: 'candidate_internal_pn', equals: 'LM2596S-ADJ', column: 'recommendation_level', contains: 'alternate' }, + }, + { + sample: 2, + name: '原 PN 缺库 · 硬阻塞建档(不问人)· 审核通过', + decide: 'approve', + expectPort: 'assessment-released', + stageEdits: { + 'parts.profile': [ + { + internal_pn: 'LM2596S-3.3', + 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)', + normalized_parameter_set: [ + '输入电压范围 | 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 阈值', + ].join('\n'), + }, + ], + '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: ['create', 'create', 'approve'], + expectDecision: true, + 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', + normalized_parameter_set: [ + '输入电压范围 | 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 | 原厂未给', + ].join('\n'), + }, + { + internal_pn: 'CN-DCDC-32V', + brand_name: '国产某厂', + specification_description: '降压型 DCDC,3A,固定 5.0V,输入上限 32V', + package: 'TO-263-5', + lifecycle_status: 'active', + normalized_parameter_set: [ + '输入电压范围 | 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 阈值', + ].join('\n'), + }, + ], + '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: '广东' }, + decide: 'approve', + expectPort: 'assessment-released', + }, + ], + }, }; let bearer = ''; @@ -608,7 +822,9 @@ async function expectGateBlocks(cardId, taskId, stage) { if (column === undefined) return 'nothing-missing'; const { body } = await api(`/api/gov/testbed/activity/${taskId}`); const rows = draftRows(body.drafts ?? [], stage.address); - const blank = rows.filter((r) => String(r[column] ?? '').trim() === '').length; + // 有 fillWhere 的段只数该档的行:default / ok 行空着是本分,不是「闸没拦」 + const scoped = stage.fillWhere ? rows.filter((r) => String(r[stage.fillWhere.column] ?? '') === stage.fillWhere.equals) : rows; + const blank = scoped.filter((r) => String(r[column] ?? '').trim() === '').length; return blank > 0 ? 'leaked' : 'nothing-missing'; } @@ -673,14 +889,16 @@ async function answerEntries(taskId, kase, answered) { * 键为空串 = 按序发给待填行;`fillMissing` 单列兜底扫空; * `fillAllBlank` 整行兜底扫空(回执这类「几行由模型定、每行要填一把」的表用它)。 */ -function buildEdits(stage, drafts) { +function buildEdits(stage, drafts, kase = {}) { const rows = draftRows(drafts, stage.address); const rowKey = stage.rowKey ?? 'pn'; const blankCol = stage.blankCol ?? 'base_price'; const pending = rows.filter((r) => String(r[blankCol] ?? '') === ''); const lines = []; const taken = new Set(); - for (const [i, edit] of (stage.edits ?? []).entries()) { + // 案例级按表的编辑叠在套件级之上(同一套件不同案例要建的料号不同 —— 参数集不能是同一份) + const edits = [...(stage.edits ?? []), ...((kase.stageEdits ?? {})[stage.address] ?? [])]; + for (const [i, edit] of edits.entries()) { const { [rowKey]: keyed, ...cells } = edit; const target = keyed !== '' ? rows.find((r) => String(r[rowKey]) === keyed) : pending[i]; if (!target) { @@ -705,6 +923,18 @@ function buildEdits(stage, drafts) { } } } + /* + * 按条件整行扫空:只给某一档状态的行填(评估输入清单里只有 status = gap 的行要补充内容, + * default / ok 的行本就不该填 —— fillAllBlank 会把它们也填上,那是替人说了他没说的话)。 + */ + if (stage.fillWhere) { + const { column, equals, values } = stage.fillWhere; + for (const r of rows) { + if (String(r[column] ?? '') === equals && !taken.has(String(r[rowKey]))) { + lines.push({ [rowKey]: String(r[rowKey]), ...values }); + } + } + } return lines; } @@ -958,7 +1188,7 @@ async function runCase(suite, kase) { : ` ⚠️ 缺着必填还放过去了:提交闸失效`, ); } - const lines = buildEdits(stage, body.drafts ?? []); + const lines = buildEdits(stage, body.drafts ?? [], kase); // 人加行:与真人在件上「加一行」同一条链 —— 新主键的行落进保存载荷即是新增 if (kase.adds && kase.adds.address === stage.address && !addedRows) { lines.push(...kase.adds.rows); @@ -1098,7 +1328,7 @@ async function main() { const which = process.argv[2] ?? 'all'; const only = process.argv[3] === undefined ? undefined : Number(process.argv[3]); await login(); - const suites = which === 'all' || which === 'both' ? ['a1', 'a2', 'a3', 'a4', 'a6', 'a7', 'e04', 'i01'] : [which]; + const suites = which === 'all' || which === 'both' ? ['a1', 'a2', 'a3', 'a4', 'a6', 'a7', 'e04', 'i01', 'w05'] : [which]; let pass = 0; let total = 0; for (const key of suites) { From c3ab236f703e0b4c078c6c720132d0c2eba69f5c Mon Sep 17 00:00:00 2001 From: xcs Date: Thu, 27 Aug 2026 00:33:27 +0800 Subject: [PATCH 11/21] =?UTF-8?q?fix(w05):=20s=5Fbound=20=E5=93=A8?= =?UTF-8?q?=E5=85=B5=E5=8F=AA=E5=9C=A8=E7=9C=9F=E6=9C=89=E6=96=99=E5=8F=B7?= =?UTF-8?q?=E4=B8=8D=E5=9C=A8=E5=BA=93=E6=97=B6=E5=BC=80=E9=97=A8;?= =?UTF-8?q?=E5=85=A8=E5=8C=85=E6=B8=85=E6=8E=89=20disable=20=E6=95=99?= =?UTF-8?q?=E5=AD=A6;version=201.0.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 浏览器全流程走查(样例 1「都在库·常路」)熔断,根因两条:①disable 在本引擎里不可达 (Disabled 只能从 Enabled 来,普通计划项一直停在 Available,disable 恒被拒);②s_bound 在每条路上都开(只看 demand_request_rows/sub_eval_pn_check_rows 两个恒真的读数),都在库 时把两个数据工单项照样推进「当下可为」,逼模型去 disable 它们、连拒五拍。 修:s_bound 的 onPart/ifPart 都改盯 sub_eval_pn_check_exists_status_absent > 0 —— 都在库时门不开,两项静静停在 Available、不进「当下可为」、根本用不上 disable;有缺库时 门照常开。S0_bind 的 guide 与其余各处教 disable 的句子全部清掉,改写清楚「都在库你什么 都不用做」,并明写「任何时候都不要对计划项用 disable」。s_p08_created 未动(都在库时 parts_profile_rows 本就是 0,它本来就不开)。 steps.test.ts 补两条回归测试:s_bound 条件文本含 sub_eval_pn_check_exists_status_absent > 0(退回旧写法会红);NARRATIVE 与全部 ASSESSMENT_STEPS[].guide 里不出现「disable + 计划 项 id」这种祈使形(正则只逮教模型真去调用的句子,不误伤「不要用 disable」这类禁止句)。 manifest.ts 的 identity.packageVersion 1.0.0 → 1.0.1(安装恒新增,不升版本改动装不进去); pnpm activities:install 已重装,静态审无提示(零发现,清单行未追加 findings 后缀)。 48/48 测试(steps.test.ts 18 + activity.test.ts 30)与 typecheck 全绿。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh --- .../__tests__/steps.test.ts | 20 +++++++++++-- .../src/case-model.ts | 28 ++++++++++--------- .../src/manifest.ts | 2 +- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/activities/part-substitution-assessment/__tests__/steps.test.ts b/activities/part-substitution-assessment/__tests__/steps.test.ts index 41549fc..a6a68f2 100644 --- a/activities/part-substitution-assessment/__tests__/steps.test.ts +++ b/activities/part-substitution-assessment/__tests__/steps.test.ts @@ -154,14 +154,30 @@ describe('说明书与剧本的骨架', () => { expect(s3).toContain('工具调用后同拍其余动作作废'); }); - it('S0 剧本写明库里都有就 disable S0d 两项;S0d_p08_master 剧本写明候选 ≥2 才问、=1 必建、原 PN 硬阻塞', () => { + it('S0 剧本写明都在库时数据工单两项本就不会进「当下可为」;S0d_p08_master 剧本写明候选 ≥2 才问、=1 必建、原 PN 硬阻塞', () => { const s0 = ASSESSMENT_STEPS.find((s) => s.item === 'S0_bind')?.guide ?? ''; - expect(s0).toContain('disable S0d_p08_master'); + expect(s0).toContain('当下可为'); + expect(s0).toContain('不要对计划项用'); const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? ''; expect(s0d).toContain('硬阻塞'); expect(s0d).toContain('候选只有 1 颗'); }); + it('s_bound 哨兵真的检查了「有料号不在库」这件事(都在库时门不该开;退回旧写法会红)', () => { + expect(CMMN_XML).toContain('sub_eval_pn_check_exists_status_absent > 0'); + const sBoundBlock = /[\s\S]*?<\/sentry>/.exec(CMMN_XML)?.[0] ?? ''; + expect(sBoundBlock).toContain('caseFileItemRef="sub_eval_pn_check_exists_status_absent"'); + expect(sBoundBlock).toContain('sub_eval_pn_check_exists_status_absent > 0'); + }); + + it('说明书与全部剧本里不会教模型对计划项调用 disable(允许出现在「不要 disable」这类禁止句里)', () => { + // 只逮「disable + 计划项 id」这种祈使形(教模型真的去调用它); + // 「不要…用 `disable`」「Disabled 只能从 Enabled 来」这类禁止句/状态名不含「disable + id」,不会被误伤。 + const disableCallPattern = /disable\s+(S\d[a-zA-Z_]*|T_release)/; + expect(NARRATIVE).not.toMatch(disableCallPattern); + for (const step of ASSESSMENT_STEPS) expect(step.guide, step.item).not.toMatch(disableCallPattern); + }); + it('S0d 拆成两项各自绑自己的 view(P-10 那份件不会永远推不出来),两项都没有 doneWhen', () => { const p08 = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master'); const p10 = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p10_document'); diff --git a/activities/part-substitution-assessment/src/case-model.ts b/activities/part-substitution-assessment/src/case-model.ts index 701ec80..a70c194 100644 --- a/activities/part-substitution-assessment/src/case-model.ts +++ b/activities/part-substitution-assessment/src/case-model.ts @@ -9,8 +9,8 @@ * 工作区件,不能共用同一个 view): * 启动期 → S0_bind(AI:铸案号、建 P-01 V1、锁规则版本、逐个 PN 查库、写存在性核对表) * 0.5a 等待数据(建主数据)→ S0d_p08_master(人:数据工单 P-08;候选 ≥2 缺库先决策卡; - * 库里都有就 disable 这一项) - * 0.5b 等待数据(建资料)→ S0d_p10_document(人:数据工单 P-10;库里都有就 disable 这一项) + * 都在库时哨兵不开,这一项与下一项都不会进「当下可为」) + * 0.5b 等待数据(建资料)→ S0d_p10_document(人:数据工单 P-10;都在库时同样不会进「当下可为」) * 1 建立评估上下文 → S1_input_confirm(人:执行工单 01,评估输入清单,件上补缺口) * 2 读取并标准化 → S2_normalize(AI:P-11 主记录骨架 + 差异明细对照行) * 3 逐项差异评估 → S3_assess(AI + 两枚工具:四态 / 风险 / 证据 / 影响 / 推荐等级 / 验证任务) @@ -61,10 +61,10 @@ export const CMMN_XML = ` - + - update - demand_request_rows > 0 and sub_eval_pn_check_rows > 0 + update + demand_request_rows > 0 and sub_eval_pn_check_rows > 0 and sub_eval_pn_check_exists_status_absent > 0 @@ -108,13 +108,13 @@ export const CMMN_XML = ` - + - + @@ -181,7 +181,7 @@ export const NARRATIVE = `§0 开案:**判断候选料号在技术上能不能 **存在性检查与数据工单**:启动时逐个料号在 \`parts.profile\` 里查(原 PN + 每个候选)。查无记录 = 「完全没有」, 要人从零建整条主数据 —— 发**数据工单**(一张表一张单:P-08 一张、P-10 一张);PN 在库但个别字段缺 = 「有而不全」, 归执行工单 01 补。原 PN 缺 = 硬阻塞必须建;候选缺:候选 ≥2 时先问人放弃还是创建,候选 = 1 时必须建。 -**库里都有就把 S0d_p08_master 与 S0d_p10_document 两项都 disable 掉**(此单不走这条),别发空单。 +**库里都有的话这两项的入场前提本就不成立**(此单不走这条),不会出现在你的「当下可为」里,别发空单。 **六步走**:绑定与存在性核对 → (等待数据)→ 人补评估输入 → 标准化对照表 → 逐项评估 → 人审结论(可循环)→ 拟落账 → 人放行。 @@ -298,8 +298,10 @@ current_stage = "assessing"。application_scenario / bom_position **这一步不 【写 \`${D}.verdict\` 一行】eval_ref、request_id、rule_version = "${RULE_VERSION}"、original_internal_pn、candidate_total = 候选数组长度。 -写完引擎自己收这一项。收完先看 \`${D}.pn_check\`:有 absent 的 → 下一项是 S0d_p08_master(数据工单);一个 absent 都没有 → -**disable S0d_p08_master 与 S0d_p10_document 两项**(reason 写「原 PN 与全部候选都在库,无需建主数据」),然后等 S1 那一项 Enabled。`, +写完这一项就结束。有 absent 的 → 下一项是 S0d_p08_master(数据工单),照常往下办。 +**都在库的话你什么都不用做** —— 数据工单那两项的入场前提本就不成立,它们不会出现在你的「当下可为」里, +下一拍你会直接看到「评估输入与资料补齐确认」那一步。**任何时候都不要对计划项用 \`disable\`**: +本引擎里 \`Disabled\` 只能从 \`Enabled\` 来,而普通计划项一直停在 \`Available\`,disable 一定被拒。`, }, { item: 'S0d_p08_master', @@ -309,7 +311,7 @@ current_stage = "assessing"。application_scenario / bom_position **这一步不 view: 'p08-master-create', tools: [], /* 触达人,不给 doneWhen;人工激活:发卡就算启动 */ - guide: `这一项只在 \`${D}.pn_check\` 里有 absent 行时走;都在库就由 S0_bind 把这一项与下一项 S0d_p10_document 一起 disable 掉。**先分岔,再动作**: + guide: `这一项只在 \`${D}.pn_check\` 里有 absent 行时走 —— 都在库时入场前提本就不成立,这一项与下一项 S0d_p10_document 都不会进你的「当下可为」。**先分岔,再动作**: 【先看轨迹】这一项里已经发过哪些卡、人答了什么(【当前这一步】下面那行引擎事实说得清楚): · 原 PN absent → **硬阻塞**,不问人,直接进入「建」的名单; @@ -341,8 +343,8 @@ current_stage = "assessing"。application_scenario / bom_position **这一步不 view: 'p10-document-create', tools: [], /* 触达人,不给 doneWhen;人工激活:发卡就算启动 */ - guide: `这一项只在 \`${D}.pn_check\` 里有 absent 行时走(要走到这一步,S0d_p08_master 必然已经收完);都在库就由 S0_bind 把这一项与 -S0d_p08_master 一起 disable 掉。 + guide: `这一项只在 \`${D}.pn_check\` 里有 absent 行时走(要走到这一步,S0d_p08_master 必然已经收完);都在库时入场前提本就不成立, +这一项与 S0d_p08_master 都不会进你的「当下可为」。 【发 P-10 数据工单】「建」的名单就是 \`${D}.pn_check\` 里仍是 exists_status = "absent" 的那几行(候选 ≥2 时被放弃的那些, S0d_p08_master 那一步已经把它们改成 "abandoned",不在这份名单里)。同一拍照说明书④发那张 P-10 数据工单(title 里列全料号,只发一次), diff --git a/activities/part-substitution-assessment/src/manifest.ts b/activities/part-substitution-assessment/src/manifest.ts index 1dad0f8..4d32c57 100644 --- a/activities/part-substitution-assessment/src/manifest.ts +++ b/activities/part-substitution-assessment/src/manifest.ts @@ -84,7 +84,7 @@ export const manifest: ActivityManifest = { identity: { activityId: ACTIVITY_ID, name: 'ACT-WORK-05 · 差异评估', - packageVersion: '1.0.0', + packageVersion: '1.0.1', category: '需求分析与方案匹配', sdkVersion: '0.1.0-m5', checksum: 'dev-checksum-part-substitution-assessment', From 00133c2ca042e0dbb50a3e9d14df30f839352eb8 Mon Sep 17 00:00:00 2001 From: xcs Date: Thu, 27 Aug 2026 01:15:44 +0800 Subject: [PATCH 12/21] =?UTF-8?q?fix(w05):=20=E4=BA=94=E5=BC=A0=E6=A8=A1?= =?UTF-8?q?=E7=89=88=E6=95=B4=E4=BB=BD=E9=87=8D=E5=81=9A=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E9=80=90=E5=9D=97=E6=90=AC=E8=87=AA=E5=9B=9B?= =?UTF-8?q?=E4=BB=BD=E7=9B=AE=E6=A0=87=E4=BB=B6,=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E7=85=A7=20E04/I01=20=E9=82=A3=E5=A5=97=E7=89=88=E9=9D=A2?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E5=81=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户实跑截图指出:芯片主数据创建的模版样式与给的目标件对不上。病根是 brief 一边说 「版面语言逐字量自目标件」,一边让照 I01 的 prd-style.ts 当视觉底 —— 两句互相矛盾, 《HTML报告生成规范》§7 写的是「搬」不是「照着做」。 - 新增 src/views/target-style.ts:四份目标件的 -
-
-
-
${input.badge}
-
- ${input.kicker} -

${input.title}

-
收件人: · 关联任务:
-
-
- ${input.tag} - ${input.step} - ${input.state} -
-
-
${input.body}
-
${input.foot}
-
-
`; +/* ==================== 验收工单 01(差异评估结论审核):徽标别名 ==================== */ +const BIND_ACC1 = ` +/* 差异四态与推荐等级**都**落在这一屏(本活动数据面的既有约定 —— 各候选推荐等级摘要与 + 逐项差异明细同屏摆),两者取值域都含 "conditional",光认 .st 分不清是哪一列, + 加 .dst / .rec 两个字段级修饰类各自配色(**原样照抄目标件 .st.ok/.miss/.unknown/.cond + 四条声明**,只是同一份颜色现在要接两条不同的人读文案)。 */ +${badgeCss('.st.dst', [ + ['match', 'MATCH', OK], + ['mismatch', 'MISMATCH', DANGER], + ['unknown', 'UNKNOWN', AMBER], + ['conditional', 'CONDITIONAL', 'color: var(--ledger); background: var(--ledger-soft); border-color: var(--ledger-line);'], +])} +${badgeCss('.st.rec', [ + ['recommended', '推荐', OK], + ['conditional', '条件推荐', 'color: var(--ledger); background: var(--ledger-soft); border-color: var(--ledger-line);'], + ['alternate', '备选', AMBER], + ['not_recommended', '不推荐', DANGER], +])} +/* + * 风险等级(原样照抄目标件 .risk.low/.mid/.hi 三条声明)——加 critical 一档:这一屏「各候选 + * 推荐等级」摘要读的是 substitution.relation.risk_level(声明了 low/medium/high/critical + * 四档),差异明细本身的 risk_level 只有三档,共用同一族选择器,多出的第四档不会撞车。 + */ +${badgeCss('.risk', [ + ['low', '低', 'color: var(--ok);'], + ['medium', '中', 'color: #8f6a12;'], + ['high', '高', 'color: var(--danger);'], + ['critical', '危急', 'color: var(--danger); font-weight: 900;'], +])} +/* + * 约束类型(硬/软)—— 目标件的差异明细表里这一列是纯文字,没有配色(数据工单-01 的 .ct.hard/ + * .ct.soft 才是这两个值真正的徽标声明,只是它长在另一份目标件里)。这里把那两条颜色声明 + * 原样搬过来当 v- 版(数值不改,只换了外壳类名,因为这一页的样式表里从未定义过 .ct)。 + */ +.con { display: inline-block; font-size: 11px; font-weight: 700; padding: 1px 8px; border-radius: 999px; border: 1px solid transparent; white-space: nowrap; } +${badgeCss('.con', [ + ['hard', '硬约束', DANGER], + ['soft', '软约束', MUTED], +])} +/* 替代类型 —— 目标件里也是纯文字,不配色,只补文案 */ +${badgeCss('.enumtxt', [ + ['pin_to_pin', 'Pin-to-Pin'], + ['functional', '功能替代'], + ['parametric', '参数替代'], + ['other', '其他'], +])} +`; -const part = (label: string, aside = ''): string => `

【${label}】${aside === '' ? '' : ` —— ${aside}`}

`; +/* ==================== 验收工单 02(评审放行与落账确认):徽标别名 ==================== */ +const BIND_ACC2 = ` +/* 这一屏四个枚举列(推荐等级 / 分析状态 / 需求状态 / 当前阶段)取值域两两不重叠,共用 .st 即可。 + 推荐等级、分析状态目标件里本就是 .st 徽标(原样照抄 .st.ok/.miss/.unknown/.cond 四条声明); + 需求状态 / 当前阶段目标件没有这两列,借同一套配色,不新配颜色。 */ +${badgeCss('.st', [ + ['recommended', '推荐', OK], + ['conditional', '条件推荐', 'color: var(--ledger); background: var(--ledger-soft); border-color: var(--ledger-line);'], + ['alternate', '备选', AMBER], + ['not_recommended', '不推荐', DANGER], + ['ai_draft', 'AI 草稿', AMBER], + ['fae_confirmed', 'FAE确认', OK], + ['pending_customer', '待客户验证', 'color: var(--ledger); background: var(--ledger-soft); border-color: var(--ledger-line);'], + ['customer_approved', '客户已批准', OK], + ['rejected', '不批准', DANGER], + ['void', '作废', MUTED], + ['pending', '待处理', MUTED], + ['responded', '已响应', 'color: var(--ledger); background: var(--ledger-soft); border-color: var(--ledger-line);'], + ['won', '已成交', OK], + ['abandoned', '已放弃', DANGER], + ['closed', '已关闭', MUTED], + ['assessing', '评估中', MUTED], + ['gate03', '提交 ACT-GATE-03', OK], +])} +/* 风险等级比验收 01 多一档 critical(P-11 主记录的 risk_level 声明了它,差异明细没有) */ +${badgeCss('.risk', [ + ['low', '低', 'color: var(--ok);'], + ['medium', '中', 'color: #8f6a12;'], + ['high', '高', 'color: var(--danger);'], + ['critical', '危急', 'color: var(--danger); font-weight: 900;'], +])} +/* 替代类型 —— 目标件里是纯文字,不配色,只补文案(与验收 01 同一套值域,各自页面各存一份) */ +${badgeCss('.enumtxt', [ + ['pin_to_pin', 'Pin-to-Pin'], + ['functional', '功能替代'], + ['parametric', '参数替代'], + ['other', '其他'], +])} +`; + +/** 三层壳照目标件:`
`,不再套 `.proto-root`/`.app-shell`/`.jc-stage` + * (那是 E04/I01 自己的视觉底约定,不是平台要求 —— ui-kit / view-render 从未引用过这几个类名; + * 目标件本身就是直接 `page > card`,搬版面就该原样搬这一层)。 + * 收件人 / 关联任务两格由引擎注入;目标件的「最迟交回」(`card.deadline`)本仓没有真数据源,不摆; + * 工单编号(`card.id`)换成本次评估案号 `eval_ref`。 + */ +const doc = (input: { css: string; alias: string; badge: string; hic?: string; tag: string; kicker: string; title: string; step: string; body: string; footTag: string }): string => + ` +
+
+
${input.badge}
+ ${input.hic ?? ''} +
+
${input.kicker}
+

${input.title}

+
收件人: · 关联任务:
+
+
+ ${input.tag} + ${input.step} +
+
+
${input.body}
+
+ ${input.kicker} · ${input.title} + ${input.footTag} · +
+
`; + +/* ==================== 内联 SVG 图标 —— 逐字节搬自「数据工单-01」目标件(它们是版面语言的一部分) ==================== */ +const HIC = + ''; +const ZIC_TASK = + ''; +const ZIC_TABLE = + ''; + +/* ==================== 通用绑定小工具 ==================== */ +const zone = (label: string, aside: string, icon = ''): string => `

${icon}【${label}】—— ${aside}

`; +const note = (items: readonly string[]): string => `
${items.map((p) => `

${p}

`).join('')}
`; const criteria = (title: string, items: readonly string[]): string => - `
${title}
    ${items.map((i) => `
  • ${i}
  • `).join('')}
`; -const flow = (text: string): string => `
${text}
`; -/** 页脚右侧「工单类型:× · <案号>」 */ -const sign = (note: string, tag: string): string => `${note}${tag} · `; + `
${title}
    ${items.map((i) => `
  • ${i}
  • `).join('')}
`; +const flow = (text: string): string => `
${text}
`; const editGate = (address: string, label: string): string => `
${label} - - - + + +
`; const gapbar = (object: string, what: string): string => `
还差 行${what}没填 —— 。按「编辑」开表,红框那几格就是要填的。补齐了才确认得了。
`; -const tag = (scope: string, col: string): string => ``; +const addrow = (address: string, label: string, hint: string): string => `
+ + ${hint} +
`; +/** 可改格:只读那一份留空,渲染器自动配孪生 ``(手写 input 会不填值,人一点编辑就是白框) */ const cell = (col: string, cls = ''): string => ``; -const ro = (col: string, cls = ''): string => ``; +/** 只读格:即便挂在可改的表里也不许被 `table.on` 藏起来 —— 目标件自己就有这条覆盖(`.jc-vcell.ro`) */ +const ro = (col: string, cls = ''): string => ``; const select = (col: string, options: ReadonlyArray, required = false): string => ``; +/** 枚举徽标(可改表里用,旁边挨着 `.jc-cell` 孪生件时要包一层 `.ro` 才不被编辑态藏起来) */ +const badge = (klass: string, col: string, wrapRo = false): string => { + const inner = ``; + return wrapRo ? `${inner}` : inner; +}; -/* ==================== 局面 0 · 数据工单 P-08 ==================== */ +/* ==================== 局面 0 · 数据工单 P-08(搬自「数据工单-01」)==================== */ -const P08_TABLE = `
+const P08_TABLE = `
- - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + +
内部料号品牌制造商规格描述封装工艺生命周期EOL / PCNDatasheet 引用Datasheet 版本适配场景标准化参数集(一行一参数:参数名 | 值 | 单位 | hard/soft | 说明)#内部料号 internal_pn全链路统一基准,不可自由文本品牌 brand_name与制造商、供应商分开制造商 manufacturer_name与品牌不一致时分开记录规格描述 specification_description按原厂资料口径封装 package影响硬约束比对工艺 process_technology硬约束比较项生命周期 lifecycle_statusActive/NRND/EOL/停产/风险EOL/PCN eol_pcn_status通知/影响/生效时间Datasheet 引用 datasheet_refDatasheet 版本 datasheet_version与引用成对适配场景 application_scenarios判断实际适用性标准化参数集 normalized_parameter_set一行一参数:参数名|值|单位|hard/soft|说明
${cell('internal_pn', 'mono')}${cell('brand_name')}${cell('manufacturer_name')}${cell('specification_description')}${cell('package', 'mono')}${cell('process_technology')}${select('lifecycle_status', [['active', 'Active'], ['nrnd', 'NRND'], ['eol', 'EOL'], ['discontinued', '停产'], ['risk', '风险']], true)}${tag('t-life', 'lifecycle_status')}${cell('eol_pcn_status')}${cell('datasheet_ref', 'mono')}${cell('datasheet_version')}${cell('application_scenarios')}${cell('internal_pn', 'mono')}${cell('brand_name')}${cell('manufacturer_name')}${cell('specification_description')}${cell('package', 'mono')}${cell('process_technology')}${badge('stx', 'lifecycle_status', true)}${select('lifecycle_status', [['active', 'Active'], ['nrnd', 'NRND'], ['eol', 'EOL'], ['discontinued', '停产'], ['risk', '风险']], true)}${cell('eol_pcn_status')}${cell('datasheet_ref', 'mono')}${cell('datasheet_version')}${cell('application_scenarios')}
`; export const P08_MASTER_CREATE = doc({ - tone: 'human', + css: P08_CSS, + alias: BIND_P08, badge: '数据', + hic: HIC, tag: '数据工单', - kicker: 'SS02 · ACT-WORK-05 差异评估 · 数据工单(P-08)', - title: '芯片主数据创建', + kicker: 'SS02 · ACT-WORK-05 差异评估 · 数据工单 1/2', + title: '芯片主数据创建(P-08)', step: '步骤 0.5 · 等待数据', - state: `待建 / 行`, + footTag: '工单类型:数据', body: - part('做什么', '批量创建 P-08 主数据记录') + - `
-

模板定位:本单是「数据工单」的实例 —— 一单一类(可多条):目标台账 = 芯片主数据(P-08); - 字段即表头、每行一条新记录;标准化参数集是这条记录的一个字段(一行一参数)。

-

触发原因:系统按内部料号查芯片主数据查无记录 —— 是「完全没有」,不是字段缺失(那归执行工单)。

-

AI 预填:上传了 datasheet 的料号 AI 已按资料预填,标着值的格子请核对;没上传的料号全部手填,同为合法路径。

-

纪律:必填列全齐才允许确认;确认后这批记录进本单草稿,放行时并入正式库并留痕(确认人 = 数据责任人);只建主数据,验证记录等事实类不在此建。

-
` + - part('创建什么', '主数据记录(每行一条,*为必填列)') + + zone('做什么', '批量创建 P-08 主数据记录', ZIC_TASK) + + note([ + '模板定位:本单是「数据工单」统一模板的实例——一单一类(可多条):目标台账 = 芯片主数据(P-08);字段即表头、每行一条新记录;标准化参数集是这条记录的一个字段(一行一参数)。', + '触发原因:系统按内部料号查芯片主数据查无记录——是「完全没有」,不是字段缺失(那归执行工单)。', + 'AI 预填:上传了 datasheet 的料号 AI 已按资料预填,标着值的格子请核对;没上传的料号全部手填,同为合法路径。', + '纪律:必填列全齐才允许确认;确认后这批记录进本单草稿,放行时并入正式库并留痕(确认人 = 数据责任人);只建主数据,验证记录等事实类不在此建。', + ]) + + zone('创建什么', '主数据记录(每行一条,*为必填列)', ZIC_TABLE) + gapbar('parts_profile', '必填') + editGate('parts.profile', `待建 条记录,点「编辑」核对预填、补齐空缺;「+ 加一行」= 再建一条`) + - `
必填列(*)全齐才允许确认;原厂未披露的参数值请直接写「未披露」,它会进入 UNKNOWN 并挂验证任务;删一行 = 该记录不建。改动只落任务草稿层,保存不等于放行。
` + P08_TABLE + - `
- - 新行的内部料号必须写(它是这一行的钥匙);其余必填列全部填写后才允许确认。 -
` + + addrow('parts.profile', '+ 加一行(再建一条记录)', '新行的内部料号必须写(它是这一行的钥匙);其余必填列全部填写后才允许确认;删一行 = 该记录不建') + flow('提交 / 确认:本工单只承载内容;确认请在对话区的【执行卡】中进行。确认后活动自动从「等待数据」唤醒。'), - foot: sign('SS02 · ACT-WORK-05 差异评估 · 数据工单 · 芯片主数据创建(P-08)', '工单类型:数据'), }); -/* ==================== 局面 0′ · 数据工单 P-10 ==================== */ +/* ==================== 局面 0′ · 数据工单 P-10(复用「数据工单-01」的样式与骨架,没有独立目标件)==================== */ -const P10_TABLE = `
+const P10_TABLE = `
- - - - - - - - - - + + + + + + + + + + - - - - - - - - - - + + + + + + + + + +
关联料号序号资料类型品牌资料标题版本文件链接证据定位状态#关联料号 internal_pn对应 P-08 那条记录资料序号 doc_seq同一料号下 01/02… 自己续资料类型 document_type品牌 brand_name资料标题 document_title资料版本 document_version文件链接 document_ref证据定位 citation_locator页码/章节/表格资料状态 document_status
${cell('internal_pn', 'mono')}${cell('doc_seq', 'mono')}${select('document_type', [['datasheet', 'Datasheet'], ['app_note', '应用笔记'], ['reference_design', '参考设计'], ['white_paper', '白皮书'], ['sample_doc', '样片资料'], ['commitment_letter', '承诺函'], ['other', '其他']], true)}${tag('t-doc', 'document_type')}${cell('brand_name')}${cell('document_title')}${cell('document_version')}${cell('document_ref', 'mono')}${cell('citation_locator')}${select('document_status', [['valid', '有效'], ['expired', '过期'], ['pending', '待核实'], ['void', '作废']], true)}${tag('t-docst', 'document_status')}${cell('internal_pn', 'mono')}${cell('doc_seq', 'mono')}${badge('stx', 'document_type', true)}${select('document_type', [['datasheet', 'Datasheet'], ['app_note', '应用笔记'], ['reference_design', '参考设计'], ['white_paper', '白皮书'], ['sample_doc', '样片资料'], ['commitment_letter', '承诺函'], ['other', '其他']], true)}${cell('brand_name')}${cell('document_title')}${cell('document_version')}${cell('document_ref', 'mono')}${cell('citation_locator')}${badge('stx', 'document_status', true)}${select('document_status', [['valid', '有效'], ['expired', '过期'], ['pending', '待核实'], ['void', '作废']], true)}
`; export const P10_DOCUMENT_CREATE = doc({ - tone: 'human', + css: P08_CSS, + alias: BIND_P08, badge: '数据', + hic: HIC, tag: '数据工单', - kicker: 'SS02 · ACT-WORK-05 差异评估 · 数据工单(P-10)', - title: '原厂技术资料创建', + kicker: 'SS02 · ACT-WORK-05 差异评估 · 数据工单 2/2', + title: '原厂技术资料创建(P-10)', step: '步骤 0.5 · 等待数据', - state: `待建 / 行`, + footTag: '工单类型:数据', body: - part('做什么', '为本单新建的料号登记原厂资料(P-10)') + - `
-

触发原因:按内部料号查原厂技术资料库查无记录。一份资料一行;同一料号多份资料序号 01/02… 自己续。

-

为什么要它:差异明细的证据引用要指到具体资料的版本与页码 —— 没有这一行,结论就追溯不到来源。

-
` + - part('创建什么', '资料记录(每行一条,*为必填列)') + + zone('做什么', '为本单新建的料号登记原厂资料(P-10)', ZIC_TASK) + + note([ + '模板定位:与 P-08 同一套「数据工单」统一模板——目标台账 = 原厂技术资料库(P-10);一份资料一行,同一料号多份资料序号 01/02… 自己续。', + '触发原因:按内部料号查原厂技术资料库查无记录(P-08 那张先确认过,这一批料号才会走到这里)。', + '为什么要它:差异明细的证据引用要指到具体资料的版本与页码——没有这一行,结论就追溯不到来源。', + ]) + + zone('创建什么', '资料记录(每行一条,*为必填列)', ZIC_TABLE) + gapbar('parts_document', '必填') + editGate('parts.document', `待建 条资料记录,点「编辑」补齐`) + - `
资料类型、标题、版本、文件链接、状态为必填;证据定位写页码或章节。改动只落任务草稿层,保存不等于放行。
` + P10_TABLE + - `
- - 关联料号与序号是这一行的钥匙,新行都要写;必填列全齐才允许确认。 -
` + + addrow('parts.document', '+ 加一行(再登记一份资料)', '关联料号与序号是这一行的钥匙,新行都要写;必填列全齐才允许确认;删一行 = 该资料不建') + flow('提交 / 确认:本工单只承载内容;确认请在对话区的【执行卡】中进行。'), - foot: sign('SS02 · ACT-WORK-05 差异评估 · 数据工单 · 原厂技术资料创建(P-10)', '工单类型:数据'), }); -/* ==================== 局面 1 · 评估输入清单(S0/S1)==================== */ +/* ==================== 局面 1 · 评估输入清单(搬自「执行工单-01」)==================== */ -const PN_CHECK_TABLE = `
+const PN_CHECK_TABLE = `
- - - - - + + + + + - - - - - + + + + +
料号角色在库状态备注 / 放弃原因#料号角色在库状态备注 / 放弃原因
${ro('internal_pn', 'mono')}${tag('t-role', 'role')}${tag('t-exist', 'exists_status')} ${ro('internal_pn', 'mono')}${badge('st', 'exists_status', true)}${ro('note')} ${ro('abandon_reason')}
`; -const GAP_TABLE = `
+const GAP_TABLE = `
- - - - - - - + + + + + + - - - - - - - + + + + + +
键输入项来源状态绑定值 / 缺口说明补充内容#输入项来源状态绑定值 / 缺口说明补充内容
${cell('item_key', 'mono')}${cell('input_item')}${cell('source_ledger', 'mono')}${select('status', [['gap', '待补'], ['default', '默认·可改'], ['ok', '齐备']])}${tag('t-gap', 'status')}${cell('detail')}${cell('input_item')}${cell('source_ledger', 'mono')}${badge('st', 'status', true)}${cell('detail')}${cell('supplement')}
`; export const EVAL_INPUT_LIST = doc({ - tone: 'human', + css: EXEC_CSS, + alias: BIND_EXEC, badge: '执行', tag: '执行工单', kicker: 'SS02 · ACT-WORK-05 差异评估 · 执行工单 1/1', title: '评估输入与资料补齐确认', step: '步骤 1/5 · 建立评估上下文', - state: `待补 项`, + footTag: '工单类型:执行', body: - part('做什么', '启动 4 项已绑定,其余信息在工单里补齐') + - `
-

AI 已完成:系统已按启动传入的 4 项完成绑定 —— 客户、客户需求(新建 P-01 · 版本 V1)、原 PN、候选 PN(数组,逐个评估)—— 并生成输入快照。

-

需要你补齐:启动时未收的信息(应用场景、BOM 位)直接在下表补;默认值(替代类型 / 规则版本 / 负责人期限)可确认可改。本单缺口只覆盖「有而不全」;「完全没有」(料号不在库)已由数据工单创建。未补齐的缺口项将按 UNKNOWN 进入差异评估。

-
` + - part('已绑定', '启动时冻结,工单内不可改 —— 绑错请退回启动人重新启动') + - `
- 客户 - 客户需求 · V1 · - 原内部料号 - 规则版本 -
` + + zone('做什么', '启动 4 项已绑定,其余信息在工单里补齐') + + note([ + 'AI 已完成:系统已按启动传入的 4 项完成绑定——客户、客户需求(新建 P-01 · 版本 V1)、原 PN、候选 PN(数组,逐个评估)——并生成输入快照。', + '需要你补齐:启动时未收的信息(应用场景、BOM 位)直接在下表补;默认值(替代类型 / 规则版本 / 负责人期限)可确认可改。本单缺口只覆盖「有而不全」;「完全没有」(料号不在库)已由数据工单创建。未补齐的缺口项将按 UNKNOWN 进入差异评估。', + ]) + + zone('料号存在性核对', '只读参考 · 与下表「补充内容」互不冲突') + PN_CHECK_TABLE + - part('交回什么', '评估输入清单(已绑定项只读核对,缺口项补齐,*为必填)') + + zone('交回什么', '评估输入清单(已绑定项照实核对,缺口项补齐,*为必填)') + gapbar(`${D}_input_gap`, '补充内容') + - editGate(`${D}.input_gap`, `还有 项待补 / 待确认,点「编辑」在「补充内容」列填入`) + - `
加一行 = 补充 AI 未列出的资料项(新行的「输入项」与「补充内容」必填);删一行 = 该输入项不适用;补不了的写「留 UNKNOWN」或「挂验证任务」—— 那也是一个答案。改动只落任务草稿层,保存不等于放行。
` + + editGate(`${D}.input_gap`, `还有 项待补 / 待确认,点「编辑」在「补充内容」列填入 —— 已绑定那几行的「输入项/来源/绑定值」抄的是启动入参,发现记录本身有误请退回触发方重新启动,不要在这里改`) + GAP_TABLE + - `
- - 键写一个英文短名(如 thermal_note);「绑定值 / 缺口说明」栏写清这项资料是什么、AI 为什么拿不到。 -
` + - flow('审批 / 提交:本工单只承载内容;「提交」请在对话区的【执行卡】中进行。提交后去向:进入步骤 2 · AI 读取与标准化,仍缺失的项将按 UNKNOWN 进入差异评估。'), - foot: sign('SS02 · ACT-WORK-05 差异评估 · 执行工单 1/1 · 评估输入与资料补齐确认', '工单类型:执行'), + addrow(`${D}.input_gap`, '+ 加一行(补充资料项)', '新加行的「输入项」与「补充内容」为必填;删一行 = 该输入项不适用;补不了的写「留 UNKNOWN」或「挂验证任务」') + + flow( + '审批 / 提交:本工单只承载内容;「提交」请在对话区的【执行卡】中进行。提交后去向:进入步骤 2 · AI 读取与标准化,仍缺失的项将按 UNKNOWN 进入差异评估。注意:启动参数如有误,需退回触发方修订后重新触发,本单不可改。', + ), }); -/* ==================== 局面 2 · 差异评估结论审核(S2/S3/S4,只读)==================== */ +/* ==================== 局面 2 · 差异评估结论审核(搬自「验收工单-01」)==================== */ -const RELATION_TABLE = `
+const RELATION_SUMMARY_TABLE = `
- - - - - - - - - + + + + + + + - - - - - - - - - + + + + + + +
候选料号替代类型推荐等级推荐理由差异摘要风险验证要求置信度#候选料号替代类型推荐等级风险等级推荐理由差异摘要
${ro('candidate_internal_pn', 'mono')}${tag('t-sub', 'substitution_type')}${tag('t-rec', 'recommendation_level')}${ro('recommendation_reason')}${ro('difference_summary')}${tag('t-risk', 'risk_level')}${ro('verification_requirement')}${ro('confidence_score', 'mono')}${badge('st rec', 'recommendation_level')}${badge('risk', 'risk_level')}
`; -const ITEM_TABLE = `
- - - - - - - - - - - - - - - +const DIFFERENCE_ITEM_TABLE = `
关系参数项原 PN 值候选 PN 值单位约束类型结论风险证据引用影响说明UNKNOWN 去向置信度
+ - - - - - - - - - - - - - + + + + + + + + + +
#参数项 parameter_name原 PN 值 original_value候选 PN 值 candidate_value单位 parameter_unit约束类型 constraint_type结论 difference_status风险等级证据引用 evidence_refAI 置信度
${ro('substitution_relation_id', 'mono')}${ro('original_value', 'mono')}${ro('candidate_value', 'mono')}${ro('parameter_unit')}${tag('t-con', 'constraint_type')}${tag('t-diff', 'difference_status')}${tag('t-risk', 'risk_level')}${ro('evidence_ref', 'mono')}${ro('impact_statement')}${ro('unknown_disposition')}${ro('confidence', 'mono')}${badge('st dst', 'difference_status')}${badge('risk', 'risk_level')}
`; export const DIFFERENCE_REVIEW = doc({ - tone: 'ledger', + css: ACC1_CSS, + alias: BIND_ACC1, badge: '验收', tag: '验收工单', kicker: 'SS02 · ACT-WORK-05 差异评估 · 验收工单 1/2', title: '差异评估结论审核', step: '步骤 3~4/5 · 逐项差异评估 + FAE/RI 审核', - state: `共 个候选 · 条比对`, + footTag: '工单类型:验收', body: - part('AI 结论说明', '结论是怎么算出来的') + - `
-

AI 已完成的计算:按 GOV 评估规则(锁定版本 ),对原 PN - 与每个候选逐项比对,四态结论 —— MATCH / - MISMATCH / - UNKNOWN / - CONDITIONAL ,并给出风险等级、证据引用与置信度。 - 差异明细 = P-11 差异明细子表(一条参数一条记录);当前为 AI 草稿,审核通过前不落账。本单数据工单创建的主数据,其参数证据标注「人工录入」。

-
` + - part('验收判据', '你据此判断「通过 / 驳回」') + + zone('AI 结论说明', '结论是怎么算出来的') + + note([ + 'AI 已完成的计算:按 GOV 评估规则(锁定版本 ),对原 PN 与每个候选逐项比对,产出四态结论——MATCH / MISMATCH / UNKNOWN / CONDITIONAL,并给出风险等级、证据引用与置信度。差异明细 = P-11 差异明细子表(一条参数差异一条记录);当前为 AI 草稿,FAE 审核通过前不落账。评估参数可能含本次数据工单创建的主数据(确认人背书;未附 datasheet 的参数项会显式标注「人工录入」)。', + ]) + criteria('判断这份差异评估结论是否可信,请逐项核对', [ '关键参数差异均有明确状态:逐项结论无漏项,每个参数都有四态之一的判定;', 'UNKNOWN 项均有去向:每个 UNKNOWN 都挂了验证任务或补来源动作,不允许悬空;', - '证据链完整:每项结论可追溯至 P-08 / P-10 / P-06 的具体记录,或本单数据工单的创建记录(人工录入);', + '证据链完整:每项结论可追溯至 P-08 / P-10 / P-06 的具体记录,或本单数据工单的创建记录(标注:人工录入);', '硬约束项未被自动判 MATCH:封装 / 工艺等硬约束未经证据支持不得判 MATCH。', ]) + - part('各候选推荐等级', '只读展示 · 每个候选一条 P-11 主记录草稿') + - RELATION_TABLE + - part('AI 产出的差异明细', '只读展示 · P-11 差异明细子表草稿') + - ITEM_TABLE + - flow('审批:本工单只承载内容;「通过 / 驳回」请在对话区的【验收卡】中进行。审核意见与补充条件写入 P-06(验证要求);通过后去向:进入步骤 5 · 评审放行与落账确认。'), - foot: sign('SS02 · ACT-WORK-05 差异评估 · 验收工单 1/2 · 差异评估结论审核', '工单类型:验收'), + zone('各候选推荐等级', '只读展示(P-11 主记录草稿,一个候选一条)') + + RELATION_SUMMARY_TABLE + + zone('AI 产出的差异明细', '只读展示(P-11 差异明细子表草稿 · 一条参数一条记录 difference_item_id)') + + DIFFERENCE_ITEM_TABLE + + flow( + '审批:本工单只承载内容;「通过 / 驳回」请在对话区的【验收卡】中进行。审核意见与补充条件写入 P-06(验证要求 verification_requirement);通过后去向:进入步骤 5 · 评审放行与落账确认。', + ), }); -/* ==================== 局面 3 · 评审放行与落账确认(S5/T_release,只读)==================== */ +/* ==================== 局面 3 · 评审放行与落账确认(搬自「验收工单-02」)==================== */ -const P11_TABLE = `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/** 字段:值 两列表 —— 逐字节搬自目标件「拟写 P-11」「拟回写 P-01」的版式(一行一个字段), + * 用 `data-rows` 包一整张小表当行模版,一个候选 / 一条需求各重复出一份,天然支持多候选。 */ +const P11_FIELD_TABLE = `
替代关系ID替代类型原内部料号候选内部料号推荐等级推荐理由差异摘要替代分析报告分析状态客户批准状态风险验证要求证据可信度规则版本
${ro('substitution_relation_id', 'mono')}${tag('t-sub', 'substitution_type')}${ro('original_internal_pn', 'mono')}${ro('candidate_internal_pn', 'mono')}${tag('t-rec', 'recommendation_level')}${ro('recommendation_reason')}${ro('difference_summary')}${ro('analysis_report_ref', 'mono')}${tag('t-anal', 'analysis_status')}未提交 由审批流程维护,本活动不产生${tag('t-risk', 'risk_level')}${ro('verification_requirement')}${ro('confidence_score', 'mono')}${ro('rule_version', 'mono')}
+ + + + + + + + + + + + + + + +
#字段拟写入值
替代类型 substitution_type
原内部料号 original_internal_pn
候选内部料号 candidate_internal_pn
推荐等级 recommendation_level${badge('st', 'recommendation_level')} (枚举:推荐 / 条件推荐 / 备选 / 不推荐,四态之一)
推荐理由 recommendation_reason
差异摘要 difference_summary
替代分析报告 analysis_report_ref
分析状态 analysis_status${badge('st', 'analysis_status')} (枚举:AI草稿 / FAE确认 / 待客户验证 / 客户已批准 / 不批准 / 作废)
客户批准状态 customer_approval_status未提交 (由审批流程维护,本活动不产生)
风险等级 risk_level${badge('risk', 'risk_level')}
验证要求 verification_requirement
证据可信度 confidence_score
差异规则版本 rule_version
`; -const P01_TABLE = `
- - - - - - - - - - - - - - - - - - - - +const P01_FIELD_TABLE = `
需求编号风险等级验证要求输出链接下一步动作当前阶段需求状态
${ro('request_id', 'mono')}${tag('t-risk', 'risk_level')}${ro('verification_requirement')}${ro('output_ref', 'mono')}${ro('next_action')}${tag('t-stage', 'current_stage')}${tag('t-reqstat', 'request_status')}
+ + + + + + + + +
#字段拟写入值
风险等级 risk_level${badge('risk', 'risk_level')}
验证要求 verification_requirement
输出链接 output_ref
下一步动作 next_action
当前阶段 current_stage${badge('st', 'current_stage')}
需求状态 request_status${badge('st', 'request_status')}
`; export const LANDING_CONFIRM = doc({ - tone: 'ledger', + css: ACC2_CSS, + alias: BIND_ACC2, badge: '验收', tag: '验收工单', kicker: 'SS02 · ACT-WORK-05 差异评估 · 验收工单 2/2', title: '评审放行与落账确认', step: '步骤 5/5 · 提交 ACT-GATE-03', - state: `拟落账 条 P-11`, + footTag: '工单类型:验收', body: - part('AI 结论说明', '本单确认什么') + - `
-

本活动以 ACT-GATE-03 评审放行为终点。本单确认拟落账内容:评估结论层写 P-11 替代关系主记录(每个候选一条)与差异明细子表,需求状态回写 P-01(风险等级 / 验证要求 / 输出链接 / 下一步动作 / 当前阶段)。 - 客户批准不属于本活动 —— 客户批准状态由审批流程维护,本单只写分析层(分析状态 = FAE 确认)。

-
` + - part('验收判据', '你据此判断「通过 / 驳回」') + + zone('AI 结论说明', '本单确认什么') + + note([ + '本活动以 ACT-GATE-03 评审放行为终点。本单确认拟落账内容:评估结论层写 P-11 替代关系主记录(替代类型 / 原内部料号 / 候选内部料号 / 推荐等级 / 推荐理由 / 差异摘要 / 替代分析报告 / 分析状态 / 风险等级 / 验证要求 / 证据可信度 / 差异规则版本),需求状态回写 P-01(风险等级 / 验证要求 / 输出链接 / 下一步动作 / 当前阶段)。客户批准不属于本活动——客户批准状态由审批流程维护,本单只写分析层(分析状态 = FAE 确认),customer_approval_status 保持「未提交」,不由差异评估产生。', + ]) + criteria('放行与落账前,请逐项核对', [ - '推荐等级与差异明细一致:存在 UNKNOWN / CONDITIONAL 的候选,推荐等级不得给「推荐」;', - '分析状态与客户批准状态分离:本单只写分析层(FAE 确认),客户批准状态保持「未提交」;', + '推荐等级与差异明细一致:差异明细中存在 UNKNOWN / CONDITIONAL 时,推荐等级不得给「推荐」;', + '分析状态与客户批准状态分离:本单只写分析层(分析状态 = FAE 确认),客户批准状态保持「未提交」;', 'GOV 规则版本已锁定:规则版本随结论一并落账;', '回写 P-01 字段完整:风险等级、验证要求、输出链接、下一步动作、当前阶段五项齐全。', ]) + - part('拟写 P-11 · 替代关系主记录', '只读展示') + - P11_TABLE + - part('拟回写 P-01 · 需求状态', '只读展示') + - P01_TABLE + - flow('审批:本工单只承载内容;「通过 / 驳回」请在对话区的【验收卡】中进行。通过后去向:P-11 落账、P-01 回写、放行;结论供 ACT-WORK-08 证据归集消费。'), - foot: sign('SS02 · ACT-WORK-05 差异评估 · 验收工单 2/2 · 评审放行与落账确认', '工单类型:验收'), + zone('拟写 P-11 · 替代关系主记录', '只读展示') + + P11_FIELD_TABLE + + zone('拟回写 P-01 · 需求状态', '只读展示') + + P01_FIELD_TABLE + + flow( + '审批:本工单只承载内容;「通过 / 驳回」请在对话区的【验收卡】中进行。通过后去向:在对话区卡提交 ACT-GATE-03 评审放行;放行后结论供 ACT-WORK-08 证据归集消费。', + ), }); diff --git a/activities/part-substitution-assessment/src/views/target-style.ts b/activities/part-substitution-assessment/src/views/target-style.ts new file mode 100644 index 0000000..3026657 --- /dev/null +++ b/activities/part-substitution-assessment/src/views/target-style.ts @@ -0,0 +1,336 @@ +/** + * ACT-WORK-05 差异评估的四份目标件样式表 —— **机械抽取,一个数值都不许改** + * (《HTML报告生成规范》§7:有目标件时「搬」,不要「照着做」;样式表整段搬,标记逐块搬)。 + * + * 来源(`docs/SS02-ACT-WORK-05-html/`): + * · `P08_CSS` ← `ACT-WORK-05-数据工单-01-芯片主数据创建.html` 的两段 `` 之间的原文,再拿抽出来的内容包回 + * `` 与源文件同一行区间做 `diff`,四份全部「零差异」——过程写在 + * task-5-report.md,这里不重复贴证据。 + * + * 这份文件只装**目标件自己的样式**,不装平台绑定用的开关(三态占位、缺项红带、 + * `data-class-of` 的 `v-*` 别名这类)——那些是本活动自己加出来的东西,活在 + * `report.ts` 的 `BIND_CSS_*` 常量里,两处不要混。 + */ + +export const P08_CSS = ` +:root{ + --ink:#1f2329; --ink-2:#41464e; --ink-3:#646a73; --ink-4:#8f959e; + --line:#dee0e3; --line-soft:#e8eaed; --soft:#f8f9fb; + --brand:#3370ff; --brand-deep:#245bdb; --brand-soft:#e1eaff; --brand-wash:#f0f4ff; + --data:#3370ff; --data-soft:#e1eaff; --data-line:#c3d6ff; + --ok:#278717; --ok-soft:#e8f8e4; --danger:#d83931; --danger-soft:#feeae9; + --mono:"SF Mono",Menlo,Consolas,monospace; +} +*{box-sizing:border-box} +html,body{margin:0;padding:0} +body{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;background:#eef1f6;color:var(--ink);font-size:14px;line-height:1.7;-webkit-font-smoothing:antialiased} +.page{margin:0;padding:0} +.card{background:#fff;overflow:hidden;border:0;border-radius:0;box-shadow:none} +.card-head{display:flex;align-items:center;gap:14px;padding:18px 22px;border-bottom:2px solid var(--ink);background:linear-gradient(120deg,#f0f5ff 0%,#fff 55%,#eef4ff 100%)} +.badge{flex:none;width:44px;height:44px;border-radius:10px;display:grid;place-items:center;background:var(--data);color:#fff;font-weight:800;font-size:13px;box-shadow:0 6px 16px rgba(51,112,255,.25)} +.tit{flex:1;min-width:0} +.tit .kick{font-size:11px;letter-spacing:.08em;color:var(--ink-4);font-weight:700} +.tit h1{margin:2px 0 0;font-size:19px;font-weight:800;line-height:1.3} +.tit .sub{font-size:12px;color:var(--ink-3);margin-top:3px} +.side{display:flex;flex-direction:column;align-items:flex-end;gap:6px;flex:none} +.tag{font-size:11px;font-weight:800;padding:2px 10px;border-radius:999px;color:var(--data);background:var(--data-soft);border:1px solid var(--data-line)} +.step{font-size:12px;color:var(--ink-3)} +.deadline{font-size:12px;color:var(--ink-3)} +.deadline b{color:var(--danger)} +.card-bd{padding:20px 22px} +.zone{font-size:11px;font-weight:800;letter-spacing:.12em;color:var(--ink-4);margin:0 0 8px} +.zone b{color:var(--ink-3)} +.note{border:1px solid var(--line);border-left:3px solid var(--data);border-radius:8px;background:var(--soft);padding:14px 16px;margin-bottom:16px} +.note p{margin:0 0 6px;font-size:13px;color:var(--ink-2)} +.note p:last-child{margin-bottom:0} +.note b{color:var(--ink)} +.note .done{color:var(--ok);font-weight:700} +.note .need{color:var(--data);font-weight:700} +.jc-editgate{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:14px 0 10px;padding:9px 12px;border-radius:9px;background:var(--soft);border:1px solid var(--line-soft);font-size:12px;color:var(--ink-3)} +.jc-editgate-t{flex:1 1 auto;min-width:0} +.jc-editgate-t b{color:var(--ink-2);font-weight:700} +.jc-egbtn{flex:none;display:inline-flex;align-items:center;gap:5px;cursor:pointer;border:1px solid var(--line);border-radius:7px;padding:6px 13px;font:inherit;font-size:12px;font-weight:700;color:var(--ink-2);background:#fff;transition:border-color .16s,color .16s,background .16s} +.jc-egbtn:hover{border-color:var(--brand);color:var(--brand)} +.jc-egbtn.save{color:#fff;background:var(--data);border-color:var(--data)} +.jc-egbtn.save:hover{filter:brightness(1.06);color:#fff} +.jc-editgate .eg-cancel,.jc-editgate .eg-save{display:none} +.jc-editgate.on{background:var(--brand-wash);border-color:var(--data-line);color:#1e40af} +.jc-editgate.on .eg-open{display:none} +.jc-editgate.on .eg-cancel,.jc-editgate.on .eg-save{display:inline-flex} +.tbl-wrap{border:1px solid var(--line-soft);border-radius:10px;overflow:hidden} +table{width:100%;border-collapse:collapse;font-size:12.5px} +th{text-align:left;background:var(--soft);font-size:11px;letter-spacing:.03em;font-weight:800;color:var(--ink-3);padding:9px 10px;border-bottom:1px solid var(--line)} +th.req::after{content:" *";color:var(--danger);font-size:11px} +td{padding:8px 10px;border-bottom:1px solid var(--line-soft);vertical-align:middle} +tr:last-child td{border-bottom:0} +td.rn{position:relative;width:38px;white-space:nowrap;font-family:var(--mono);font-size:11px;color:var(--ink-4);text-align:center} +.jc-vcell{display:block;font-size:12.5px;color:var(--ink);line-height:1.5;overflow-wrap:anywhere} +.jc-vcell.mono{font-family:var(--mono);font-size:11.5px;color:var(--ink-2)} +.jc-cell{display:none;width:100%;min-width:60px;border:1px solid var(--line);border-radius:6px;padding:6px 8px;font:inherit;font-size:12.5px;color:var(--ink);background:#fff} +.jc-cell.mono{font-family:var(--mono);font-size:11.5px} +.jc-cell:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--brand-wash);outline:none} +table.on td>.jc-vcell{display:none} +table.on td>.jc-cell{display:block} +td.rn .jc-rowdel{display:none;width:22px;height:22px;margin:0 auto;cursor:pointer;border:1px solid var(--line);border-radius:6px;padding:0;font:inherit;font-size:14px;line-height:1;color:var(--ink-3);background:#fff} +td.rn .jc-rowdel:hover{border-color:var(--danger);color:var(--danger);background:var(--danger-soft)} +table.on td.rn .jc-rn-num{display:none} +table.on td.rn .jc-rowdel{display:block} +.jc-addrow{display:none;margin:8px 0 0;padding:8px 12px;border-radius:9px;background:var(--soft);border:1px dashed var(--line);font-size:12px;color:var(--ink-3)} +.jc-addrow.show{display:flex;align-items:center;gap:8px} +.jc-addrow button{cursor:pointer;border:1px solid var(--brand-soft);border-radius:7px;padding:5px 12px;font:inherit;font-size:12px;font-weight:700;color:var(--brand-deep);background:var(--brand-wash)} +.jc-addrow button:hover{background:var(--brand-soft)} +.jc-addrow em{font-style:normal;font-size:11.5px;color:var(--ink-4)} +.flow{margin-top:16px;padding:9px 12px;border-radius:8px;background:var(--soft);border:1px dashed var(--line);font-size:12px;color:var(--ink-3)} +.flow b{color:var(--ink)} +.card-foot{padding:13px 22px;border-top:1px solid var(--line-soft);font-size:11.5px;color:var(--ink-4);display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px} +@media(max-width:600px){.card-head{flex-wrap:wrap}.side{align-items:flex-start}} +/* 补充:数据工单专用标识(仅追加,不改动基件样式) */ +.mono{font-family:var(--mono);font-size:12px;color:var(--ink-2)} +.pre{font-style:normal;font-size:10.5px;font-weight:800;color:var(--data);background:var(--data-soft);border:1px solid var(--data-line);border-radius:999px;padding:0 7px;margin-left:6px;white-space:nowrap} +table.on td>.pre{display:none} +.ct{display:inline-block;font-size:11px;font-weight:700;padding:1px 8px;border-radius:999px;border:1px solid transparent;white-space:nowrap} +.ct.hard{color:var(--danger);background:var(--danger-soft);border-color:#f3b9b5} +.ct.soft{color:var(--ink-3);background:#eef1f6;border-color:var(--line)} +.tbl-gap{margin-top:18px} +/* ===== 视觉补充段 v3(通用批量模板:字段即表头、每行一条;不改基件样式与 JS 钩子) ===== */ +.hic{flex:none;width:26px;height:26px;color:var(--data)} +.zic{width:14px;height:14px;color:var(--ink-3);vertical-align:-2px;margin-right:3px} +/* 表头内字段说明(第二行小字) */ +.thd{display:block;font-size:10px;font-weight:600;color:var(--ink-4);letter-spacing:0;margin-top:1px;line-height:1.4} +/* 表 1:字段即表头,内容多列,允许横向滚动 */ +#tbl1{min-width:1240px} +#tbl1 .jc-vcell{display:inline} +/* 表 2 列宽 */ +#tbl2 th:nth-child(2){width:150px} +#tbl2 th:nth-child(3){width:18%} +#tbl2 th:nth-child(4){width:14%} +#tbl2 th:nth-child(5){width:8%;text-align:center;white-space:nowrap} +#tbl2 th:nth-child(6){width:13%;text-align:center;white-space:nowrap} +#tbl2 td:nth-child(5),#tbl2 td:nth-child(6){text-align:center;white-space:nowrap} +/* 说明列(只读注释,编辑态保持可见) */ +.desc{color:var(--ink-3);font-size:12px;line-height:1.5} +table.on td>.jc-vcell.ro{display:block} +.tbl-wrap{overflow-x:auto} +@media (prefers-reduced-motion: reduce){*{animation:none!important;transition:none!important}} +`; + +export const EXEC_CSS = ` +:root{ + --ink:#1f2329; --ink-2:#41464e; --ink-3:#646a73; --ink-4:#8f959e; + --line:#dee0e3; --line-soft:#e8eaed; --soft:#f8f9fb; + --brand:#3370ff; --brand-deep:#245bdb; --brand-soft:#e1eaff; --brand-wash:#f0f4ff; + --human:#a9711e; --human-soft:#f6ecd8; --human-line:#e6d3a8; + --ok:#278717; --ok-soft:#e8f8e4; --danger:#d83931; --danger-soft:#feeae9; + --mono:"SF Mono",Menlo,Consolas,monospace; +} +*{box-sizing:border-box} +html,body{margin:0;padding:0} +body{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;background:#eef1f6;color:var(--ink);font-size:14px;line-height:1.7;-webkit-font-smoothing:antialiased} +.page{margin:0;padding:0} +.card{background:#fff;overflow:hidden;border:0;border-radius:0;box-shadow:none} +.card-head{display:flex;align-items:center;gap:14px;padding:18px 22px;border-bottom:2px solid var(--ink);background:linear-gradient(120deg,#fffaf1 0%,#fff 55%,#fbf6ea 100%)} +.badge{flex:none;width:44px;height:44px;border-radius:10px;display:grid;place-items:center;background:var(--human);color:#fff;font-weight:800;font-size:13px;box-shadow:0 6px 16px rgba(169,113,30,.25)} +.tit{flex:1;min-width:0} +.tit .kick{font-size:11px;letter-spacing:.08em;color:var(--ink-4);font-weight:700} +.tit h1{margin:2px 0 0;font-size:19px;font-weight:800;line-height:1.3} +.tit .sub{font-size:12px;color:var(--ink-3);margin-top:3px} +.side{display:flex;flex-direction:column;align-items:flex-end;gap:6px;flex:none} +.tag{font-size:11px;font-weight:800;padding:2px 10px;border-radius:999px;color:var(--human);background:var(--human-soft);border:1px solid var(--human-line)} +.step{font-size:12px;color:var(--ink-3)} +.deadline{font-size:12px;color:var(--ink-3)} +.deadline b{color:var(--danger)} +.card-bd{padding:20px 22px} +.zone{font-size:11px;font-weight:800;letter-spacing:.12em;color:var(--ink-4);margin:0 0 8px} +.zone b{color:var(--ink-3)} +.note{border:1px solid var(--line);border-left:3px solid var(--human);border-radius:8px;background:var(--soft);padding:14px 16px;margin-bottom:16px} +.note p{margin:0;font-size:13px;color:var(--ink-2)} +.note b{color:var(--ink)} +.note .done{color:var(--ok);font-weight:700} +.note .need{color:var(--human);font-weight:700} +.jc-editgate{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:14px 0 10px;padding:9px 12px;border-radius:9px;background:var(--soft);border:1px solid var(--line-soft);font-size:12px;color:var(--ink-3)} +.jc-editgate-t{flex:1 1 auto;min-width:0} +.jc-editgate-t b{color:var(--ink-2);font-weight:700} +.jc-egbtn{flex:none;display:inline-flex;align-items:center;gap:5px;cursor:pointer;border:1px solid var(--line);border-radius:7px;padding:6px 13px;font:inherit;font-size:12px;font-weight:700;color:var(--ink-2);background:#fff;transition:border-color .16s,color .16s,background .16s} +.jc-egbtn:hover{border-color:var(--brand);color:var(--brand)} +.jc-egbtn.save{color:#fff;background:var(--human);border-color:var(--human)} +.jc-egbtn.save:hover{filter:brightness(1.06);color:#fff} +.jc-editgate .eg-cancel,.jc-editgate .eg-save{display:none} +.jc-editgate.on{background:var(--human-soft);border-color:var(--human-line);color:#7c4a12} +.jc-editgate.on .eg-open{display:none} +.jc-editgate.on .eg-cancel,.jc-editgate.on .eg-save{display:inline-flex} +.tbl-wrap{border:1px solid var(--line-soft);border-radius:10px;overflow:hidden} +table{width:100%;border-collapse:collapse;font-size:12.5px} +th{text-align:left;background:var(--soft);font-size:11px;letter-spacing:.03em;font-weight:800;color:var(--ink-3);padding:9px 10px;border-bottom:1px solid var(--line)} +th.req::after{content:" *";color:var(--danger);font-size:11px} +td{padding:8px 10px;border-bottom:1px solid var(--line-soft);vertical-align:middle} +tr:last-child td{border-bottom:0} +td.rn{position:relative;width:38px;white-space:nowrap;font-family:var(--mono);font-size:11px;color:var(--ink-4);text-align:center} +.jc-vcell{display:block;font-size:12.5px;color:var(--ink);line-height:1.5;overflow-wrap:anywhere} +.jc-vcell.mono{font-family:var(--mono);font-size:11.5px;color:var(--ink-2)} +.jc-cell{display:none;width:100%;min-width:60px;border:1px solid var(--line);border-radius:6px;padding:6px 8px;font:inherit;font-size:12.5px;color:var(--ink);background:#fff} +.jc-cell.mono{font-family:var(--mono);font-size:11.5px} +.jc-cell:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--brand-wash);outline:none} +table.on td>.jc-vcell{display:none} +table.on td>.jc-cell{display:block} +td.rn .jc-rowdel{display:none;width:22px;height:22px;margin:0 auto;cursor:pointer;border:1px solid var(--line);border-radius:6px;padding:0;font:inherit;font-size:14px;line-height:1;color:var(--ink-3);background:#fff} +td.rn .jc-rowdel:hover{border-color:var(--danger);color:var(--danger);background:var(--danger-soft)} +table.on td.rn .jc-rn-num{display:none} +table.on td.rn .jc-rowdel{display:block} +.jc-addrow{display:none;margin:8px 0 0;padding:8px 12px;border-radius:9px;background:var(--soft);border:1px dashed var(--line);font-size:12px;color:var(--ink-3)} +.jc-addrow.show{display:flex;align-items:center;gap:8px} +.jc-addrow button{cursor:pointer;border:1px solid var(--brand-soft);border-radius:7px;padding:5px 12px;font:inherit;font-size:12px;font-weight:700;color:var(--brand-deep);background:var(--brand-wash)} +.jc-addrow button:hover{background:var(--brand-soft)} +.jc-addrow em{font-style:normal;font-size:11.5px;color:var(--ink-4)} +.flow{margin-top:16px;padding:9px 12px;border-radius:8px;background:var(--soft);border:1px dashed var(--line);font-size:12px;color:var(--ink-3)} +.flow b{color:var(--ink)} +.card-foot{padding:13px 22px;border-top:1px solid var(--line-soft);font-size:11.5px;color:var(--ink-4);display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px} +@media(max-width:600px){.card-head{flex-wrap:wrap}.side{align-items:flex-start}} +/* 补充:评估输入清单状态标识(仅追加,不改动基件样式) */ +.mono{font-family:var(--mono);font-size:12px;color:var(--ink-2)} +.st{display:inline-block;font-size:11px;font-weight:700;padding:1px 8px;border-radius:999px;border:1px solid transparent;white-space:nowrap} +.st.bound{color:var(--ink-3);background:#eef1f6;border-color:var(--line)} +.st.ok{color:var(--ok);background:var(--ok-soft);border-color:#c9ecc2} +.st.miss{color:var(--human);background:var(--human-soft);border-color:var(--human-line)} +.st.def{color:var(--brand-deep);background:var(--brand-wash);border-color:var(--brand-soft)} +tr.ro td{background:var(--soft)} +table.on td>.jc-vcell.ro{display:block} +`; + +export const ACC1_CSS = ` +:root{ + --ink:#1f2329; --ink-2:#41464e; --ink-3:#646a73; --ink-4:#8f959e; + --line:#dee0e3; --line-soft:#e8eaed; --soft:#f8f9fb; + --brand:#3370ff; --brand-deep:#245bdb; --brand-soft:#e1eaff; --brand-wash:#f0f4ff; + --ledger:#b23a2e; --ledger-soft:#f7e4e0; --ledger-line:#ecc5bd; + --ok:#278717; --ok-soft:#e8f8e4; --danger:#d83931; --danger-soft:#feeae9; + --mono:"SF Mono",Menlo,Consolas,monospace; +} +*{box-sizing:border-box} +html,body{margin:0;padding:0} +body{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;background:#eef1f6;color:var(--ink);font-size:14px;line-height:1.7;-webkit-font-smoothing:antialiased} +.page{margin:0;padding:0} +.card{background:#fff;overflow:hidden;border:0;border-radius:0;box-shadow:none} +.card-head{display:flex;align-items:center;gap:14px;padding:18px 22px;border-bottom:2px solid var(--ink);background:linear-gradient(120deg,#fdf3f1 0%,#fff 55%,#fbf2f0 100%)} +.badge{flex:none;width:44px;height:44px;border-radius:10px;display:grid;place-items:center;background:var(--ledger);color:#fff;font-weight:800;font-size:13px;box-shadow:0 6px 16px rgba(178,58,46,.25)} +.tit{flex:1;min-width:0} +.tit .kick{font-size:11px;letter-spacing:.08em;color:var(--ink-4);font-weight:700} +.tit h1{margin:2px 0 0;font-size:19px;font-weight:800;line-height:1.3} +.tit .sub{font-size:12px;color:var(--ink-3);margin-top:3px} +.side{display:flex;flex-direction:column;align-items:flex-end;gap:6px;flex:none} +.tag{font-size:11px;font-weight:800;padding:2px 10px;border-radius:999px;color:var(--ledger);background:var(--ledger-soft);border:1px solid var(--ledger-line)} +.step{font-size:12px;color:var(--ink-3)} +.deadline{font-size:12px;color:var(--ink-3)} +.deadline b{color:var(--danger)} +.card-bd{padding:20px 22px} +.zone{font-size:11px;font-weight:800;letter-spacing:.12em;color:var(--ink-4);margin:0 0 8px} +.zone b{color:var(--ink-3)} +.note{border:1px solid var(--line);border-left:3px solid var(--ledger);border-radius:8px;background:var(--soft);padding:14px 16px;margin-bottom:16px} +.note p{margin:0;font-size:13px;color:var(--ink-2)} +.note b{color:var(--ink)} +.criteria{border:1px solid var(--ledger-line);border-radius:10px;background:var(--ledger-soft);padding:13px 16px;margin-bottom:18px} +.criteria .c-title{font-size:11px;font-weight:800;letter-spacing:.08em;color:var(--ledger);margin-bottom:7px} +.criteria ul{margin:0;padding-left:18px;font-size:12.5px;color:var(--ink-2)} +.criteria li{margin:4px 0} +.criteria b{color:var(--ink)} +.tbl-wrap{border:1px solid var(--line-soft);border-radius:10px;overflow:hidden} +table{width:100%;border-collapse:collapse;font-size:13px} +th{text-align:left;background:var(--soft);font-size:11px;letter-spacing:.05em;font-weight:800;color:var(--ink-3);padding:9px 12px;border-bottom:1px solid var(--line)} +td{padding:11px 12px;border-bottom:1px solid var(--line-soft);vertical-align:top} +tr:last-child td{border-bottom:0} +.num{font-family:var(--mono);color:var(--ink-4);white-space:nowrap} +.cust{font-weight:700} +.prio{font-family:var(--mono);font-weight:800} +.prio.hi{color:var(--danger)} +.prio.mid{color:var(--ink-2)} +.reason{font-size:12.5px;color:var(--ink-2)} +.flow{margin-top:16px;padding:9px 12px;border-radius:8px;background:var(--soft);border:1px dashed var(--line);font-size:12px;color:var(--ink-3)} +.flow b{color:var(--ink)} +.card-foot{padding:13px 22px;border-top:1px solid var(--line-soft);font-size:11.5px;color:var(--ink-4);display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px} +@media(max-width:600px){.card-head{flex-wrap:wrap}.side{align-items:flex-start}} +/* 补充:四态结论 pill 与风险等级(仅追加,不改动基件样式) */ +.st{display:inline-block;padding:1px 9px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.04em;font-family:var(--mono);white-space:nowrap;border:1px solid transparent} +.st.ok{color:var(--ok);background:var(--ok-soft);border-color:#bfe6b4} +.st.miss{color:var(--danger);background:var(--danger-soft);border-color:#f3b9b5} +.st.unknown{color:#8f6a12;background:#fdf3d7;border-color:#ecd9a0} +.st.cond{color:var(--ledger);background:var(--ledger-soft);border-color:var(--ledger-line)} +.risk{font-weight:800;font-size:12.5px} +.risk.low{color:var(--ok)} +.risk.mid{color:#8f6a12} +.risk.hi{color:var(--danger)} +.mono{font-family:var(--mono);font-size:12px;color:var(--ink-2)} +`; + +export const ACC2_CSS = ` +:root{ + --ink:#1f2329; --ink-2:#41464e; --ink-3:#646a73; --ink-4:#8f959e; + --line:#dee0e3; --line-soft:#e8eaed; --soft:#f8f9fb; + --brand:#3370ff; --brand-deep:#245bdb; --brand-soft:#e1eaff; --brand-wash:#f0f4ff; + --ledger:#b23a2e; --ledger-soft:#f7e4e0; --ledger-line:#ecc5bd; + --ok:#278717; --ok-soft:#e8f8e4; --danger:#d83931; --danger-soft:#feeae9; + --mono:"SF Mono",Menlo,Consolas,monospace; +} +*{box-sizing:border-box} +html,body{margin:0;padding:0} +body{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;background:#eef1f6;color:var(--ink);font-size:14px;line-height:1.7;-webkit-font-smoothing:antialiased} +.page{margin:0;padding:0} +.card{background:#fff;overflow:hidden;border:0;border-radius:0;box-shadow:none} +.card-head{display:flex;align-items:center;gap:14px;padding:18px 22px;border-bottom:2px solid var(--ink);background:linear-gradient(120deg,#fdf3f1 0%,#fff 55%,#fbf2f0 100%)} +.badge{flex:none;width:44px;height:44px;border-radius:10px;display:grid;place-items:center;background:var(--ledger);color:#fff;font-weight:800;font-size:13px;box-shadow:0 6px 16px rgba(178,58,46,.25)} +.tit{flex:1;min-width:0} +.tit .kick{font-size:11px;letter-spacing:.08em;color:var(--ink-4);font-weight:700} +.tit h1{margin:2px 0 0;font-size:19px;font-weight:800;line-height:1.3} +.tit .sub{font-size:12px;color:var(--ink-3);margin-top:3px} +.side{display:flex;flex-direction:column;align-items:flex-end;gap:6px;flex:none} +.tag{font-size:11px;font-weight:800;padding:2px 10px;border-radius:999px;color:var(--ledger);background:var(--ledger-soft);border:1px solid var(--ledger-line)} +.step{font-size:12px;color:var(--ink-3)} +.deadline{font-size:12px;color:var(--ink-3)} +.deadline b{color:var(--danger)} +.card-bd{padding:20px 22px} +.zone{font-size:11px;font-weight:800;letter-spacing:.12em;color:var(--ink-4);margin:0 0 8px} +.zone b{color:var(--ink-3)} +.note{border:1px solid var(--line);border-left:3px solid var(--ledger);border-radius:8px;background:var(--soft);padding:14px 16px;margin-bottom:16px} +.note p{margin:0;font-size:13px;color:var(--ink-2)} +.note b{color:var(--ink)} +.criteria{border:1px solid var(--ledger-line);border-radius:10px;background:var(--ledger-soft);padding:13px 16px;margin-bottom:18px} +.criteria .c-title{font-size:11px;font-weight:800;letter-spacing:.08em;color:var(--ledger);margin-bottom:7px} +.criteria ul{margin:0;padding-left:18px;font-size:12.5px;color:var(--ink-2)} +.criteria li{margin:4px 0} +.criteria b{color:var(--ink)} +.tbl-wrap{border:1px solid var(--line-soft);border-radius:10px;overflow:hidden} +table{width:100%;border-collapse:collapse;font-size:13px} +th{text-align:left;background:var(--soft);font-size:11px;letter-spacing:.05em;font-weight:800;color:var(--ink-3);padding:9px 12px;border-bottom:1px solid var(--line)} +td{padding:11px 12px;border-bottom:1px solid var(--line-soft);vertical-align:top} +tr:last-child td{border-bottom:0} +.num{font-family:var(--mono);color:var(--ink-4);white-space:nowrap} +.cust{font-weight:700} +.prio{font-family:var(--mono);font-weight:800} +.prio.hi{color:var(--danger)} +.prio.mid{color:var(--ink-2)} +.reason{font-size:12.5px;color:var(--ink-2)} +.flow{margin-top:16px;padding:9px 12px;border-radius:8px;background:var(--soft);border:1px dashed var(--line);font-size:12px;color:var(--ink-3)} +.flow b{color:var(--ink)} +.card-foot{padding:13px 22px;border-top:1px solid var(--line-soft);font-size:11.5px;color:var(--ink-4);display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px} +@media(max-width:600px){.card-head{flex-wrap:wrap}.side{align-items:flex-start}} +/* 补充:推荐等级 pill 与状态强调(仅追加,不改动基件样式) */ +.st{display:inline-block;padding:1px 9px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.04em;font-family:var(--mono);white-space:nowrap;border:1px solid transparent} +.st.ok{color:var(--ok);background:var(--ok-soft);border-color:#bfe6b4} +.st.miss{color:var(--danger);background:var(--danger-soft);border-color:#f3b9b5} +.st.unknown{color:#8f6a12;background:#fdf3d7;border-color:#ecd9a0} +.st.cond{color:var(--ledger);background:var(--ledger-soft);border-color:var(--ledger-line)} +.risk{font-weight:800;font-size:12.5px} +.risk.mid{color:#8f6a12} +.mono{font-family:var(--mono);font-size:12px;color:var(--ink-2)} +.tbl-gap{margin-top:18px} +`; From 02ef572470d2276add2a61d288f3bc9e3c477ad7 Mon Sep 17 00:00:00 2001 From: xcs Date: Thu, 27 Aug 2026 01:43:06 +0800 Subject: [PATCH 13/21] =?UTF-8?q?fix(w05):=20=E8=AF=84=E4=BC=B0=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E6=B8=85=E5=8D=95=E4=B8=A4=E6=9D=A1=20Critical=20?= =?UTF-8?q?=E5=9B=9E=E5=BD=92=20=E2=80=94=E2=80=94=20=E4=B8=BB=E9=94=AE?= =?UTF-8?q?=E4=B8=A2=E9=92=A5=E5=8C=99=E3=80=81=E5=BF=85=E5=A1=AB=E5=A1=AB?= =?UTF-8?q?=E4=B8=8D=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 定向复审发现上一轮模版重做(00133c2)新引入两条回归,都出在 sub_eval.input_gap 的缺口表: - item_key 那格丢了 data-col:一单 11 行,item_key 逐行不同,服务端 resolveRowKey 既在件上 找不到它、又不是「全表一致」的案号型列(那条路只对 caseKey 成立),人保存任何一行都会 「认不出它是哪一行」。补回 ,并把「加一行」提示补上「键必须填」。 - status 声明 required:true,又给了「加一行」,却被降成只读徽标——人新加的行永远没有格子 填这一列,_missing 恒正、提交闸永远拦着(墙不配路)。改回 E04 执行回执同款的混排: + +
`; + +/* + * 表 2 · 标准化参数集(P-08 子表 `parts.parameter`)—— 粒度与表 1 不同(一个参数一行, + * 行数不固定),不能塞进表 1 的一格(《统一方案》§六:重复明细用子表承载), + * 逐块搬自目标件《数据工单-01》的【创建什么 · 表 2】,`id="tbl2"` 沿用目标件自己的 + * 列宽声明(P08_CSS 里 `#tbl2 th:nth-child(n)` 那几条,不必在这里重复)。 + * 「说明」列是系统只读注释(目标件原话:「说明列为系统注释,无需填写」),不给 data-col。 + */ +const P08_PARAMS_TABLE = `
+ + + + + + + + + + + + + + + + +
#所属料号 internal_pn归属表 1 哪条记录参数名值单位约束(硬/软)说明
${cell('internal_pn', 'mono')}${cell('param_name')}${cell('param_value', 'mono')}${cell('param_unit')}${badge('ct', 'constraint_type', true)}${select('constraint_type', [['hard', '硬约束'], ['soft', '软约束']], true)}${ro('param_note')}
`; @@ -347,16 +385,21 @@ export const P08_MASTER_CREATE = doc({ body: zone('做什么', '批量创建 P-08 主数据记录', ZIC_TASK) + note([ - '模板定位:本单是「数据工单」统一模板的实例——一单一类(可多条):目标台账 = 芯片主数据(P-08);字段即表头、每行一条新记录;标准化参数集是这条记录的一个字段(一行一参数)。', + '模板定位:本单是「数据工单」统一模板的实例——一单一类(可多条):目标台账 = 芯片主数据(P-08);字段即表头、每行一条新记录;标准化参数集是它的结构子表(表 2,一行一参数,按所属料号归属)。', '触发原因:系统按内部料号查芯片主数据查无记录——是「完全没有」,不是字段缺失(那归执行工单)。', 'AI 预填:上传了 datasheet 的料号 AI 已按资料预填,标着值的格子请核对;没上传的料号全部手填,同为合法路径。', '纪律:必填列全齐才允许确认;确认后这批记录进本单草稿,放行时并入正式库并留痕(确认人 = 数据责任人);只建主数据,验证记录等事实类不在此建。', ]) + - zone('创建什么', '主数据记录(每行一条,*为必填列)', ZIC_TABLE) + + zone('创建什么 · 表 1', '主数据记录(每行一条,*为必填列)', ZIC_TABLE) + gapbar('parts_profile', '必填') + editGate('parts.profile', `待建 条记录,点「编辑」核对预填、补齐空缺;「+ 加一行」= 再建一条`) + P08_TABLE + addrow('parts.profile', '+ 加一行(再建一条记录)', '新行的内部料号必须写(它是这一行的钥匙);其余必填列全部填写后才允许确认;删一行 = 该记录不建') + + `

${ZIC_TABLE}【创建什么 · 表 2】—— 标准化参数集(每行一个参数,按所属料号归属,*必填)

` + + gapbar('parts_parameter', '必填') + + editGate('parts.parameter', `已铺 行参数(有 datasheet 的已按资料预填),点「编辑」核对修正;「+ 加一行」补充未列出的参数`) + + P08_PARAMS_TABLE + + addrow('parts.parameter', '+ 加一行(补充参数)', '所属料号 / 参数名 / 值 / 单位 / 约束类型齐全;「未披露」请直接写未披露,将进入 UNKNOWN 并挂验证任务;「说明」列为系统注释,无需填写') + flow('提交 / 确认:本工单只承载内容;确认请在对话区的【执行卡】中进行。确认后活动自动从「等待数据」唤醒。'), }); diff --git a/apps/api/src/app/__tests__/seed-mvp-data.test.ts b/apps/api/src/app/__tests__/seed-mvp-data.test.ts index 1b326d0..45a85a2 100644 --- a/apps/api/src/app/__tests__/seed-mvp-data.test.ts +++ b/apps/api/src/app/__tests__/seed-mvp-data.test.ts @@ -19,10 +19,11 @@ describe('主数据四域', () => { expect([...new Set(MASTER_OBJECTS.map((o) => o.domain))].sort()).toEqual([...MASTER_DOMAINS].sort()); }); - it('SS02 差异评估要读写的六张主数据表都在基线里,主键照《统一方案》', () => { + it('SS02 差异评估要读写的七张主数据表都在基线里,主键照《统一方案》', () => { const pk = (domain: string, object: string) => MASTER_OBJECTS.find((o) => o.domain === domain && o.object === object)?.fields.filter((f) => f.primaryKey).map((f) => f.name); expect(pk('parts', 'profile')).toEqual(['internal_pn']); + expect(pk('parts', 'parameter')).toEqual(['internal_pn', 'param_name']); expect(pk('parts', 'document')).toEqual(['internal_pn', 'doc_seq']); expect(pk('demand', 'request')).toEqual(['request_id']); expect(pk('substitution', 'relation')).toEqual(['substitution_relation_id']); @@ -36,6 +37,21 @@ describe('主数据四域', () => { expect(parts).not.toContain('XL2596S-5.0'); }); + it('样例料号的标准化参数集是独立子表 parts.parameter:两颗料各 7 行,主键是 (internal_pn, param_name)', () => { + const params = MASTER_SAMPLE_ROWS.filter((r) => r.address.domain === 'parts' && r.address.object === 'parameter'); + const byPn = (pn: string) => params.filter((r) => r.key['internal_pn'] === pn); + expect(byPn('LM2596S-5.0')).toHaveLength(7); + expect(byPn('LM2596S-ADJ')).toHaveLength(7); + for (const row of params) { + expect(Object.keys(row.key).sort()).toEqual(['internal_pn', 'param_name']); + expect(row.values['param_value'], JSON.stringify(row.key)).toBeTruthy(); + expect(row.values['constraint_type']).toMatch(/^(hard|soft)$/); + } + // parts.profile 不再声明这一列的值 —— 两行样例主记录里它应为空(物理列留在基线,不再写) + const profiles = MASTER_SAMPLE_ROWS.filter((r) => r.address.domain === 'parts' && r.address.object === 'profile'); + for (const row of profiles) expect(row.values['normalized_parameter_set']).toBeUndefined(); + }); + it('四个域各自都有表 —— 空占位也得立得住', () => { for (const d of MASTER_DOMAINS) { expect(MASTER_OBJECTS.filter((o) => o.domain === d).length, `${d} 域一张表都没有`).toBeGreaterThan(0); diff --git a/apps/api/src/app/seed-mvp-data.ts b/apps/api/src/app/seed-mvp-data.ts index 5bb8d2a..0a544d8 100644 --- a/apps/api/src/app/seed-mvp-data.ts +++ b/apps/api/src/app/seed-mvp-data.ts @@ -143,17 +143,22 @@ export const MASTER_OBJECTS: readonly ObjectDeclaration[] = [ f('trend', 'text', '走势'), ]), - /* ── 料件:P-08 芯片主数据 / P-10 原厂技术资料库(SS02 差异评估 2026-08-26 加)── + /* ── 料件:P-08 芯片主数据 / P-10 原厂技术资料库(SS02 差异评估 2026-08-26 加,2026-08-27 结构改)── 字段名与《SS02-SS03-SS04_Demo台账字段细化与统一方案》§4.2 一字不差;全链路只认 internal_pn。 - 参数集是 P-08 的一个字段(用户定:一张表,不建子表),一行一参数: - `参数名 | 值 | 单位 | hard/soft | 说明`。老的 market.part(主键 pn)不复用,记 debt 待并。 */ + 标准化参数集**改为独立子表 `parts.parameter`**(2026-08-27 用户当场核对实跑页面后指出: + 「做出来的页面结构必须与目标件一致」——目标件《ACT-WORK-05-数据工单-01》本就是两张表, + 表 1 一个料一行(主记录)、表 2 一个参数一行(子表,按所属料号归属);塞进主记录的一个文本 + 列违反《统一方案》§六「重复明细用子表承载,不在单一文本字段堆叠」,§4.2 里 + `normalized_parameter_set` 的数据类型本就写的是 `Structure`。物理列 `parts.profile. + normalized_parameter_set` 仍留在基线里(不删列),只是活动清单不再声明它、不再写它。 + 老的 market.part(主键 pn)不复用,记 debt 待并。 */ master('parts', 'profile', '芯片主数据(P-08)', [ f('internal_pn', 'text', '内部料号', true), f('customer_pn', 'text', '客户料号'), f('brand_name', 'text', '品牌名称'), f('manufacturer_name', 'text', '制造商名称'), f('specification_description', 'text', '规格描述'), - f('normalized_parameter_set', 'text', '标准化参数集(一行一参数:参数名 | 值 | 单位 | hard/soft | 说明)'), + f('normalized_parameter_set', 'text', '标准化参数集(已改子表 parts.parameter,这一列不再声明/不再写)'), f('package', 'text', '封装'), f('process_technology', 'text', '工艺'), f('lifecycle_status', 'text', '生命周期状态'), @@ -163,6 +168,15 @@ export const MASTER_OBJECTS: readonly ObjectDeclaration[] = [ f('application_scenarios', 'text', '适配应用场景'), f('pin_to_pin_relation_ref', 'text', 'Pin-to-Pin 关系引用'), ]), + /* P-08 子表:标准化参数集,一行一参数,按 internal_pn 归属主记录(《统一方案》§六:重复明细用子表) */ + master('parts', 'parameter', '标准化参数集(P-08 子表)', [ + f('internal_pn', 'text', '所属料号', true), + f('param_name', 'text', '参数名', true), + f('param_value', 'text', '值'), + f('param_unit', 'text', '单位'), + f('constraint_type', 'text', '约束类型'), + f('param_note', 'text', '说明'), + ]), master('parts', 'document', '原厂技术资料库(P-10)', [ f('internal_pn', 'text', '关联内部料号', true), f('doc_seq', 'text', '资料序号(同一料号下 01/02…)', true), @@ -462,10 +476,11 @@ export const MASTER_SAMPLE_ROWS: readonly { address: LedgerAddress; key: LedgerK }, }, /* - * ── SS02 差异评估的样例料号(2026-08-26)── + * ── SS02 差异评估的样例料号(2026-08-26,2026-08-27 参数集改子表)── * 只播**原 PN**与**同族可调版**两颗:「都在库」那条路靠它们;候选 XL2596S-5.0 与国产 DCDC * **刻意不播** —— 「候选缺库 → 派数据工单」「硬约束不推荐」两条路要试得出来,库里得真没有。 - * 参数集一行一参数(参数名 | 值 | 单位 | hard/soft | 说明),与 P-08 声明的行语法同一份。 + * 标准化参数集是独立子表 `parts.parameter`,一颗料 7 行(与目标件《数据工单-01》的 + * 表 2 示例预填同形);数值照旧版 `normalized_parameter_set` 文本行逐条搬,一个字不改。 */ { address: { domain: 'parts', object: 'profile' }, @@ -475,15 +490,6 @@ export const MASTER_SAMPLE_ROWS: readonly { address: LedgerAddress; key: LedgerK brand_name: 'TI', manufacturer_name: 'Texas Instruments', specification_description: '降压型 DCDC 稳压器,3A,固定 5.0V,150kHz', - normalized_parameter_set: [ - '输入电压范围 | 4.5~40 | V | hard | 客户输入上限 40V', - '输出电压 | 5.0 | V | hard | 固定版', - '输出电流 | 3 | A | hard | 满载能力', - '开关频率 | 150 | kHz | hard | 影响外围电感/电容', - '静态电流 | 5 | mA | soft | 待机功耗', - '工作温度 | -40~+85 | ℃ | hard | 工业温域', - '使能阈值 | 1.4 | V | hard | ON/OFF 引脚阈值', - ].join('\n'), package: 'TO-263-5', process_technology: 'BCD', lifecycle_status: 'nrnd', @@ -493,6 +499,13 @@ export const MASTER_SAMPLE_ROWS: readonly { address: LedgerAddress; key: LedgerK application_scenarios: '工业电源;消费电源', }, }, + { address: { domain: 'parts', object: 'parameter' }, key: { internal_pn: 'LM2596S-5.0', param_name: '输入电压范围' }, values: { internal_pn: 'LM2596S-5.0', param_name: '输入电压范围', param_value: '4.5~40', param_unit: 'V', constraint_type: 'hard', param_note: '客户输入上限 40V' } }, + { address: { domain: 'parts', object: 'parameter' }, key: { internal_pn: 'LM2596S-5.0', param_name: '输出电压' }, values: { internal_pn: 'LM2596S-5.0', param_name: '输出电压', param_value: '5.0', param_unit: 'V', constraint_type: 'hard', param_note: '固定版' } }, + { address: { domain: 'parts', object: 'parameter' }, key: { internal_pn: 'LM2596S-5.0', param_name: '输出电流' }, values: { internal_pn: 'LM2596S-5.0', param_name: '输出电流', param_value: '3', param_unit: 'A', constraint_type: 'hard', param_note: '满载能力' } }, + { address: { domain: 'parts', object: 'parameter' }, key: { internal_pn: 'LM2596S-5.0', param_name: '开关频率' }, values: { internal_pn: 'LM2596S-5.0', param_name: '开关频率', param_value: '150', param_unit: 'kHz', constraint_type: 'hard', param_note: '影响外围电感/电容' } }, + { address: { domain: 'parts', object: 'parameter' }, key: { internal_pn: 'LM2596S-5.0', param_name: '静态电流' }, values: { internal_pn: 'LM2596S-5.0', param_name: '静态电流', param_value: '5', param_unit: 'mA', constraint_type: 'soft', param_note: '待机功耗' } }, + { address: { domain: 'parts', object: 'parameter' }, key: { internal_pn: 'LM2596S-5.0', param_name: '工作温度' }, values: { internal_pn: 'LM2596S-5.0', param_name: '工作温度', param_value: '-40~+85', param_unit: '℃', constraint_type: 'hard', param_note: '工业温域' } }, + { address: { domain: 'parts', object: 'parameter' }, key: { internal_pn: 'LM2596S-5.0', param_name: '使能阈值' }, values: { internal_pn: 'LM2596S-5.0', param_name: '使能阈值', param_value: '1.4', param_unit: 'V', constraint_type: 'hard', param_note: 'ON/OFF 引脚阈值' } }, { address: { domain: 'parts', object: 'profile' }, key: { internal_pn: 'LM2596S-ADJ' }, @@ -501,15 +514,6 @@ export const MASTER_SAMPLE_ROWS: readonly { address: LedgerAddress; key: LedgerK brand_name: 'TI', manufacturer_name: 'Texas Instruments', specification_description: '降压型 DCDC 稳压器,3A,可调输出 1.23~37V,150kHz(LM2596 同族)', - normalized_parameter_set: [ - '输入电压范围 | 4.5~40 | V | hard | 与固定版同', - '输出电压 | 1.23~37 | V | hard | 可调版,靠外围反馈网络定', - '输出电流 | 3 | A | hard | 满载能力', - '开关频率 | 150 | kHz | hard | 影响外围电感/电容', - '静态电流 | 5 | mA | soft | 待机功耗', - '工作温度 | -40~+85 | ℃ | hard | 工业温域', - '使能阈值 | 1.4 | V | hard | ON/OFF 引脚阈值', - ].join('\n'), package: 'TO-263-5', process_technology: 'BCD', lifecycle_status: 'active', @@ -518,6 +522,13 @@ export const MASTER_SAMPLE_ROWS: readonly { address: LedgerAddress; key: LedgerK application_scenarios: '工业电源;消费电源', }, }, + { address: { domain: 'parts', object: 'parameter' }, key: { internal_pn: 'LM2596S-ADJ', param_name: '输入电压范围' }, values: { internal_pn: 'LM2596S-ADJ', param_name: '输入电压范围', param_value: '4.5~40', param_unit: 'V', constraint_type: 'hard', param_note: '与固定版同' } }, + { address: { domain: 'parts', object: 'parameter' }, key: { internal_pn: 'LM2596S-ADJ', param_name: '输出电压' }, values: { internal_pn: 'LM2596S-ADJ', param_name: '输出电压', param_value: '1.23~37', param_unit: 'V', constraint_type: 'hard', param_note: '可调版,靠外围反馈网络定' } }, + { address: { domain: 'parts', object: 'parameter' }, key: { internal_pn: 'LM2596S-ADJ', param_name: '输出电流' }, values: { internal_pn: 'LM2596S-ADJ', param_name: '输出电流', param_value: '3', param_unit: 'A', constraint_type: 'hard', param_note: '满载能力' } }, + { address: { domain: 'parts', object: 'parameter' }, key: { internal_pn: 'LM2596S-ADJ', param_name: '开关频率' }, values: { internal_pn: 'LM2596S-ADJ', param_name: '开关频率', param_value: '150', param_unit: 'kHz', constraint_type: 'hard', param_note: '影响外围电感/电容' } }, + { address: { domain: 'parts', object: 'parameter' }, key: { internal_pn: 'LM2596S-ADJ', param_name: '静态电流' }, values: { internal_pn: 'LM2596S-ADJ', param_name: '静态电流', param_value: '5', param_unit: 'mA', constraint_type: 'soft', param_note: '待机功耗' } }, + { address: { domain: 'parts', object: 'parameter' }, key: { internal_pn: 'LM2596S-ADJ', param_name: '工作温度' }, values: { internal_pn: 'LM2596S-ADJ', param_name: '工作温度', param_value: '-40~+85', param_unit: '℃', constraint_type: 'hard', param_note: '工业温域' } }, + { address: { domain: 'parts', object: 'parameter' }, key: { internal_pn: 'LM2596S-ADJ', param_name: '使能阈值' }, values: { internal_pn: 'LM2596S-ADJ', param_name: '使能阈值', param_value: '1.4', param_unit: 'V', constraint_type: 'hard', param_note: 'ON/OFF 引脚阈值' } }, { address: { domain: 'parts', object: 'document' }, key: { internal_pn: 'LM2596S-5.0', doc_seq: '01' }, diff --git a/tools/e2e-a2.mjs b/tools/e2e-a2.mjs index e609ca2..418f80f 100644 --- a/tools/e2e-a2.mjs +++ b/tools/e2e-a2.mjs @@ -34,6 +34,22 @@ const E2E_CLIENT_SECRET = process.env.KEYCLOAK_E2E_CLIENT_SECRET ?? ''; /** access token 只活 5 分钟:超过 4 分钟就换一枚,免得长案例中途 401 */ const TOKEN_REFRESH_MS = 4 * 60 * 1000; +/** + * 标准化参数子表 `parts.parameter` 的行(w05 专用)—— 按 [参数名, 值, 单位, 约束, 说明] + * 五元组批量转成保存载荷的行对象;数值照抄各案例原先写在 `normalized_parameter_set` + * 那个文本列里的那几行,一个数都不改(2026-08-27 结构改:参数集拆成独立子表)。 + */ +function paramRows(internalPn, rows) { + return rows.map(([param_name, param_value, param_unit, constraint_type, param_note]) => ({ + internal_pn: internalPn, + param_name, + param_value, + param_unit, + constraint_type, + param_note, + })); +} + /** 每套活动的案例:样例序号 + 人在件上要填的格子(按 pn 对行) */ const SUITES = { a1: { @@ -519,6 +535,8 @@ const SUITES = { blankCol: 'supplement', stages: [ { + // 这张卡摆两张表(表 1 主记录 + 表 2 标准化参数子表)—— 主表照旧,子表挂在 extraTables 里 + // 加性表达(见 buildEdits 顶注),保存时两张表的行一次并进同一个 `tables` 载荷。 match: /数据工单 · 芯片主数据创建/, address: 'parts.profile', rowKey: 'internal_pn', @@ -531,8 +549,19 @@ const SUITES = { specification_description: '兜底规格(案例没给参数集)', package: 'TO-263-5', lifecycle_status: 'active', - normalized_parameter_set: '输入电压范围 | 4.5~40 | V | hard | 兜底', }, + extraTables: [ + { + // 主键是 (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: /数据工单 · 原厂技术资料创建/, @@ -586,17 +615,17 @@ const SUITES = { datasheet_ref: 'https://www.xlsemi.com/datasheet/XL2596S.pdf', datasheet_version: 'Rev C', application_scenarios: '工业电源;消费电源', - normalized_parameter_set: [ - '输入电压范围 | 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 | 原厂未给', - ].join('\n'), }, ], + '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', '原厂未给'], + ]), 'parts.document': [ { internal_pn: 'XL2596S-5.0', @@ -638,17 +667,17 @@ const SUITES = { lifecycle_status: 'nrnd', datasheet_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf', datasheet_version: 'Rev H (2023-09)', - normalized_parameter_set: [ - '输入电压范围 | 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 阈值', - ].join('\n'), }, ], + '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 阈值'], + ]), 'parts.document': [ { internal_pn: 'LM2596S-3.3', @@ -678,15 +707,6 @@ const SUITES = { specification_description: '降压型 DCDC 稳压器,3A,固定 5.0V,150kHz', package: 'TO-263-5', lifecycle_status: 'active', - normalized_parameter_set: [ - '输入电压范围 | 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 | 原厂未给', - ].join('\n'), }, { internal_pn: 'CN-DCDC-32V', @@ -694,17 +714,28 @@ const SUITES = { specification_description: '降压型 DCDC,3A,固定 5.0V,输入上限 32V', package: 'TO-263-5', lifecycle_status: 'active', - normalized_parameter_set: [ - '输入电压范围 | 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 阈值', - ].join('\n'), }, ], + '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' }, @@ -888,38 +919,53 @@ async function answerEntries(taskId, kase, answered) { * 把案例声明的格子对齐成保存载荷。行键随套件走(`rowKey`,老套件默认 `pn`): * 键为空串 = 按序发给待填行;`fillMissing` 单列兜底扫空; * `fillAllBlank` 整行兜底扫空(回执这类「几行由模型定、每行要填一把」的表用它)。 + * + * **复合主键**(2026-08-27,w05 的 `parts.parameter` 主键是 `(internal_pn, param_name)`)—— + * `rowKey` 除了旧的单列字符串形态,也接受一个列名数组:两颗料同名参数(如都有「工作温度」) + * 若只按 `param_name` 单列匹配,`rows.find` 会永远命中第一条同名行、把不同料的值互相顶掉; + * 数组形态按**全部列同时相等**才算命中同一行,单列字符串形态原样保留旧行为(不影响既有八套件)。 */ function buildEdits(stage, drafts, kase = {}) { const rows = draftRows(drafts, stage.address); - const rowKey = stage.rowKey ?? 'pn'; + const keyCols = Array.isArray(stage.rowKey) ? stage.rowKey : [stage.rowKey ?? 'pn']; const blankCol = stage.blankCol ?? 'base_price'; const pending = rows.filter((r) => String(r[blankCol] ?? '') === ''); const lines = []; const taken = new Set(); + const sig = (obj) => keyCols.map((k) => String(obj[k] ?? '')).join(''); + const keyOf = (obj) => Object.fromEntries(keyCols.map((k) => [k, String(obj[k] ?? '')])); // 案例级按表的编辑叠在套件级之上(同一套件不同案例要建的料号不同 —— 参数集不能是同一份) const edits = [...(stage.edits ?? []), ...((kase.stageEdits ?? {})[stage.address] ?? [])]; for (const [i, edit] of edits.entries()) { - const { [rowKey]: keyed, ...cells } = edit; - const target = keyed !== '' ? rows.find((r) => String(r[rowKey]) === keyed) : pending[i]; + const cells = { ...edit }; + const keyVals = {}; + for (const k of keyCols) { + keyVals[k] = cells[k]; + delete cells[k]; + } + const allGiven = keyCols.every((k) => keyVals[k] !== undefined && keyVals[k] !== ''); + const target = allGiven ? rows.find((r) => keyCols.every((k) => String(r[k]) === String(keyVals[k]))) : pending[i]; if (!target) { - console.log(` ⚠️ 找不到要填的行(${rowKey}=${keyed || `待填第${i}行`});现有行:${rows.map((r) => r[rowKey]).join(', ')}`); + const wanted = allGiven ? keyCols.map((k) => keyVals[k]).join('/') : `待填第${i}行`; + const have = rows.map((r) => keyCols.map((k) => r[k]).join('/')).join(', '); + console.log(` ⚠️ 找不到要填的行(${keyCols.join('+')}=${wanted});现有行:${have}`); continue; } - taken.add(String(target[rowKey])); - lines.push({ [rowKey]: String(target[rowKey]), ...cells }); + taken.add(sig(target)); + lines.push({ ...keyOf(target), ...cells }); } if (stage.fillMissing) { const { column, value } = stage.fillMissing; for (const r of rows) { - if (String(r[column] ?? '') === '' && !taken.has(String(r[rowKey]))) { - lines.push({ [rowKey]: String(r[rowKey]), [column]: value }); + if (String(r[column] ?? '') === '' && !taken.has(sig(r))) { + lines.push({ ...keyOf(r), [column]: value }); } } } if (stage.fillAllBlank) { for (const r of rows) { - if (String(r[blankCol] ?? '') === '' && !taken.has(String(r[rowKey]))) { - lines.push({ [rowKey]: String(r[rowKey]), ...stage.fillAllBlank }); + if (String(r[blankCol] ?? '') === '' && !taken.has(sig(r))) { + lines.push({ ...keyOf(r), ...stage.fillAllBlank }); } } } @@ -930,8 +976,8 @@ function buildEdits(stage, drafts, kase = {}) { if (stage.fillWhere) { const { column, equals, values } = stage.fillWhere; for (const r of rows) { - if (String(r[column] ?? '') === equals && !taken.has(String(r[rowKey]))) { - lines.push({ [rowKey]: String(r[rowKey]), ...values }); + if (String(r[column] ?? '') === equals && !taken.has(sig(r))) { + lines.push({ ...keyOf(r), ...values }); } } } @@ -1195,12 +1241,25 @@ async function runCase(suite, kase) { addedRows = true; console.log(` 件上加 ${kase.adds.rows.length} 行(${kase.adds.rows.map((r) => Object.values(r)[0]).join('、')})`); } - if (lines.length > 0) { + /* + * **一张卡填两张表**(w05:P-08 数据工单同一张卡上摆着表 1 主记录 + 表 2 标准化参数子表)—— + * 加性写法:`stage.extraTables`(可选)是同形状的段描述数组,各自按自己的 address 走 + * 同一套 `buildEdits`;保存载荷 `{tables:{地址:行[]}}` 本就支持多地址一次发,不必分两次保存、 + * 也不影响没有 `extraTables` 的老套件(a1/a2/a3/a4/a6/a7/e04/i01 的 stage 都没有这个键)。 + */ + const tables = {}; + if (lines.length > 0) tables[stage.address] = lines; + for (const extra of stage.extraTables ?? []) { + const extraLines = buildEdits(extra, body.drafts ?? [], kase); + if (extraLines.length > 0) tables[extra.address] = extraLines; + } + const tableAddresses = Object.keys(tables); + if (tableAddresses.length > 0) { await api(`/api/desk/tasks/${taskId}/table`, { method: 'POST', - body: JSON.stringify({ tables: { [stage.address]: lines } }), + body: JSON.stringify({ tables }), }); - console.log(` 件上保存 ${lines.length} 行(${card.title})`); + console.log(` 件上保存 ${tableAddresses.map((a) => `${a}:${tables[a].length}行`).join(' + ')}(${card.title})`); } try { await submitExecution(card.id); From c121349dead325624e4c01fb01191b04df4b5b39 Mon Sep 17 00:00:00 2001 From: xcs Date: Thu, 27 Aug 2026 02:54:09 +0800 Subject: [PATCH 15/21] =?UTF-8?q?ACT-WORK-05:P-08=20=E4=BA=A4=E5=9B=9E?= =?UTF-8?q?=E5=90=8E=E5=85=88=20finish=20=E5=86=8D=E5=8F=91=20P-10=20?= =?UTF-8?q?=E5=8D=A1,=E9=A1=BA=E5=BA=8F=E5=86=99=E6=AD=BB=E8=BF=9B=20guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实测逮到:人交回 P-08 那张卡(表1+表2)后,模型下一拍直接发了 P-10 的卡,却没先 finish S0d_p08_master——引擎挑工作区画面按"当下还没干完 的第一步"取 view,S0d_p08_master 还 Active 时画面就停在 p08-master-create 不动,人拿到 P-10 的卡却看不见资料表一格,提交闸必然拦死。 两处 guide 补齐先后顺序:S0d_p08_master 交回段落写死"finish 在先、 发卡在后"两步动作串,并点出不收步的后果(画面停在哪);S0d_p10_document 开头加兜底句,若进来时上一项还开着,先补 finish 再发卡。 包内测试钉死这段顺序表述不会被静默删掉或改软。版本号 1.2.0 → 1.2.1。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh --- .../__tests__/steps.test.ts | 10 ++++++++++ .../part-substitution-assessment/src/case-model.ts | 10 ++++++++-- .../part-substitution-assessment/src/manifest.ts | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/activities/part-substitution-assessment/__tests__/steps.test.ts b/activities/part-substitution-assessment/__tests__/steps.test.ts index e50ab49..39d2ec8 100644 --- a/activities/part-substitution-assessment/__tests__/steps.test.ts +++ b/activities/part-substitution-assessment/__tests__/steps.test.ts @@ -201,6 +201,16 @@ describe('说明书与剧本的骨架', () => { expect(s0d).toContain('至少留一颗候选'); }); + it('P-08 交回之后先 finish 再发 P-10 卡,顺序写死进两段 guide(2026-08-27 实测:漏收步会让工作区画面停在 P-08 那两张表,P-10 卡发出后人却看不见资料表、提交闸拦死)', () => { + const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? ''; + expect(s0d).toContain('finish S0d_p08_master'); + expect(s0d).toMatch(/顺序不能反/); + expect(s0d).toContain('p08-master-create'); + const s0d10 = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p10_document')?.guide ?? ''; + expect(s0d10).toContain('finish S0d_p08_master'); + expect(s0d10).toMatch(/若它还开着|先.*finish/); + }); + it('五段进度,每段名 ≤10 字;除放行外每项恰属一段', () => { expect(ASSESSMENT_PHASES).toHaveLength(5); for (const p of ASSESSMENT_PHASES) expect(p.name.length, p.name).toBeLessThanOrEqual(10); diff --git a/activities/part-substitution-assessment/src/case-model.ts b/activities/part-substitution-assessment/src/case-model.ts index 1c7b176..bc095c0 100644 --- a/activities/part-substitution-assessment/src/case-model.ts +++ b/activities/part-substitution-assessment/src/case-model.ts @@ -342,7 +342,12 @@ current_stage = "assessing"。application_scenario / bom_position **这一步不 brand_name 有就写}(这一行是骨架,下一项 S0d_p10_document 会把它填成正式记录)。 4. 同一拍发说明书③那张 P-08 数据工单(title 里列全料号),发完这一拍结束(挂起)。 -【P-08 那张确认回来了】finish 这一项 —— 下一项是 S0d_p10_document,同一批料号接着补资料记录。 +【P-08 那张确认回来了】这一拍的动作串**恰好两个,顺序不能反**: +① 先 \`finish S0d_p08_master\` —— **必须先把这一项收掉**。不收掉,引擎挑工作区画面时按 +「当下还没干完的第一步」取 view,S0d_p08_master 只要还 Active,画面就一直停在 +\`p08-master-create\`(P-08 那两张表);下一拍就算发出了 P-10 的卡,人的工作区也看不见 +P-10 的资料表,一格都填不了,提交闸必然拦死。 +② 再照说明书④发 P-10 数据工单 —— 下一项是 S0d_p10_document,同一批料号接着补资料记录。 【这张卡一单只发一次】人交回来之后不许再发;答复里没有值(空 values)也算答过 —— 人在件上填的东西已经落草稿层。`, }, @@ -355,7 +360,8 @@ current_stage = "assessing"。application_scenario / bom_position **这一步不 tools: [], /* 触达人,不给 doneWhen;人工激活:发卡就算启动 */ guide: `这一项只在 \`${D}.pn_check\` 里有 absent 行时走(要走到这一步,S0d_p08_master 必然已经收完);都在库时入场前提本就不成立, -这一项与 S0d_p08_master 都不会进你的「当下可为」。 +这一项与 S0d_p08_master 都不会进你的「当下可为」。**进到这一项时,S0d_p08_master 应当已经收掉了;若它还开着(Active), +先 \`finish S0d_p08_master\` 再发这张卡** —— 它没收掉,画面会一直停在 P-08 那两张表,人拿到 P-10 的卡也看不见资料表。 【发 P-10 数据工单】「建」的名单就是 \`${D}.pn_check\` 里仍是 exists_status = "absent" 的那几行(候选总数 ≥2 时被放弃的那些, S0d_p08_master 那一步已经把它们改成 "abandoned",不在这份名单里)。同一拍照说明书④发那张 P-10 数据工单(title 里列全料号,只发一次), diff --git a/activities/part-substitution-assessment/src/manifest.ts b/activities/part-substitution-assessment/src/manifest.ts index 83000e0..5346f86 100644 --- a/activities/part-substitution-assessment/src/manifest.ts +++ b/activities/part-substitution-assessment/src/manifest.ts @@ -89,7 +89,7 @@ export const manifest: ActivityManifest = { identity: { activityId: ACTIVITY_ID, name: 'ACT-WORK-05 · 差异评估', - packageVersion: '1.2.0', + packageVersion: '1.2.1', category: '需求分析与方案匹配', sdkVersion: '0.1.0-m5', checksum: 'dev-checksum-part-substitution-assessment', From 7ea08fb16a4afb11263642422be53a85dfbf9b60 Mon Sep 17 00:00:00 2001 From: xcs Date: Thu, 27 Aug 2026 03:25:08 +0800 Subject: [PATCH 16/21] =?UTF-8?q?ACT-WORK-05:S3=20=E5=86=99=E7=9B=98?= =?UTF-8?q?=E5=89=8D=E5=85=88=E8=87=AA=E6=A3=80=E5=B7=A5=E5=85=B7=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E6=95=B0,=E5=A0=B5=E6=AD=BB=E6=BC=8F=E8=B0=83=20recom?= =?UTF-8?q?mend=20=E6=AD=BB=E5=BE=AA=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实测(R2,两候选)在 S3_assess 熔断:doneWhen 有一半读 tool_..._recommend_calls > 0,而模型只调了 compare-parameters、没调 recommend 就直接写盘、自己拍了推荐等级(数据面看着对,机制上是它自己 定的,绕过了"能算的不问 LLM"这条设计意图)。doneWhen 永远不真、finish 必被拒;拒回文案只说"是不是该写的行/列漏了",把模型的注意力导向行列, 于是连续5拍死循环重写同样的18行明细、2行关系,直至熔断——拒回指错方向 比不给指引更糟这条设计律的又一次现形。 不动 doneWhen(那半句工具读数正是"机检只读基座数"的落点)。改动收在 guide 两处:第二拍结尾点出"推荐等级只能由工具算,不调它这一步收不了步"; 第三拍开头加一道自检——写盘前先看两个基座读数是否都>0,recommend 还是 0 就回去调它,不要往下写更不要自拍推荐等级。 包内测试钉死:第三拍段落里必须同时出现两个工具读数名与"自检/先看/必须>0" 这类先决语,第二拍段落里必须点出"推荐等级只能由它算"与"收不了步"。 版本号 1.2.1 → 1.2.2。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh --- .../__tests__/steps.test.ts | 12 ++++++++++++ .../part-substitution-assessment/src/case-model.ts | 10 ++++++++-- .../part-substitution-assessment/src/manifest.ts | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/activities/part-substitution-assessment/__tests__/steps.test.ts b/activities/part-substitution-assessment/__tests__/steps.test.ts index 39d2ec8..d58983a 100644 --- a/activities/part-substitution-assessment/__tests__/steps.test.ts +++ b/activities/part-substitution-assessment/__tests__/steps.test.ts @@ -154,6 +154,18 @@ describe('说明书与剧本的骨架', () => { expect(s3).toContain('工具调用后同拍其余动作作废'); }); + it('S3 第三拍写盘前先自检两枚工具的调用次数(2026-08-27 实测:漏调 recommend 会让 doneWhen 恒假、模型死循环重写同样的行)', () => { + const s3 = ASSESSMENT_STEPS.find((s) => s.item === 'S3_assess')?.guide ?? ''; + const thirdPass = s3.slice(s3.indexOf('第三拍')); + expect(thirdPass).toContain('tool_part_substitution_assessment_recommend_calls'); + expect(thirdPass).toContain('tool_part_substitution_assessment_compare_parameters_calls'); + expect(thirdPass).toMatch(/自检|先.*找这两个数|必须\s*>\s*0/); + // 第二拍那一段要点出「推荐等级只能由工具算,不调它收不了步」,别让模型自己拍推荐等级 + const secondPass = s3.slice(s3.indexOf('第二拍'), s3.indexOf('第三拍')); + expect(secondPass).toContain('推荐等级只能由它算'); + expect(secondPass).toMatch(/收不了步|收不掉/); + }); + it('S0 剧本写明都在库时数据工单两项本就不会进「当下可为」;S0d_p08_master 剧本按候选总数判断该不该问人(=1 必建、≥2 才问、原 PN 硬阻塞)', () => { const s0 = ASSESSMENT_STEPS.find((s) => s.item === 'S0_bind')?.guide ?? ''; expect(s0).toContain('当下可为'); diff --git a/activities/part-substitution-assessment/src/case-model.ts b/activities/part-substitution-assessment/src/case-model.ts index bc095c0..8ff9838 100644 --- a/activities/part-substitution-assessment/src/case-model.ts +++ b/activities/part-substitution-assessment/src/case-model.ts @@ -452,9 +452,15 @@ difference_status / compare_result / risk_level / impact_statement / unknown_dis · 每一对都带 evidenceRef —— 硬约束没有证据引用工具不判 MATCH,那正是要的。 调完这一拍结束。 -【第二拍:只调 \`${TOOL_RECOMMEND}\`】把第一拍出参里每个候选的 counts 原样传:{"candidates":[{"candidateInternalPn":"…","counts":{…}}, …]}。调完结束。 +【第二拍:只调 \`${TOOL_RECOMMEND}\`,别的什么都不做】把第一拍出参里每个候选的 counts 原样打包传进去:{"candidates":[{"candidateInternalPn":"…","counts":{…}}, …]}。调完这一拍结束。 +**推荐等级只能由它算**——你自己填一个进 \`substitution.relation.recommendation_level\`,数据面看着像对的,机制上却是你拍的; +而且这一步的收步判据数着这枚工具的调用次数,不调它这一步永远收不掉。 -【第三拍:写盘,一拍连做,一次都不再调工具】 +【第三拍:写盘前先自检,再一拍连做写盘,一次都不再调工具】 +动手写之前,先在局面的基座读数里找这两个数:\`tool_part_substitution_assessment_compare_parameters_calls\` 与 +\`tool_part_substitution_assessment_recommend_calls\`。**两个都必须 > 0 才能写盘**。若 \`recommend\` 那个还是 0, +说明第二拍没调成——**回去调它**(入参就是第一拍出参里每个候选的 counts),不要往下写、更不要自己拍推荐等级。 +这一步的收步判据里就有这两个数,**不调够,写多少行都收不了步**。 ① 逐条 \`substitution.difference_item\` 同 PK 覆盖写:difference_status / compare_result / risk_level 照第一拍出参**一字不改**; impact_statement(MATCH 写「无」;其余说清对应用 / 验证 / 报价的影响,如「输入上限 32V < 客户 40V,不能上板」); difference_status = unknown 的行**必须**写 unknown_disposition(「挂验证任务 VAL-<案号数字串>-01:实测使能阈值」或「向原厂补来源」);confidence(0~1)。 diff --git a/activities/part-substitution-assessment/src/manifest.ts b/activities/part-substitution-assessment/src/manifest.ts index 5346f86..a952602 100644 --- a/activities/part-substitution-assessment/src/manifest.ts +++ b/activities/part-substitution-assessment/src/manifest.ts @@ -89,7 +89,7 @@ export const manifest: ActivityManifest = { identity: { activityId: ACTIVITY_ID, name: 'ACT-WORK-05 · 差异评估', - packageVersion: '1.2.1', + packageVersion: '1.2.2', category: '需求分析与方案匹配', sdkVersion: '0.1.0-m5', checksum: 'dev-checksum-part-substitution-assessment', From eb8087655406dc1964db6b0beccc6f9b4a06b8dd Mon Sep 17 00:00:00 2001 From: xcs Date: Thu, 27 Aug 2026 04:29:40 +0800 Subject: [PATCH 17/21] =?UTF-8?q?=E8=B5=B0=E6=9F=A5=E9=80=BC=E5=87=BA?= =?UTF-8?q?=E7=9A=84=E5=85=AD=E6=9D=A1:=E5=8F=91=E5=8D=A1=E5=89=8D?= =?UTF-8?q?=E6=B8=85=E5=8D=95=E8=A6=81=E6=9C=89=E8=A1=8C=20=C2=B7=20?= =?UTF-8?q?=E5=9C=A8=E5=BA=93=E8=A6=81=E6=8B=BF=E5=BE=97=E5=87=BA=E4=BE=9D?= =?UTF-8?q?=E6=8D=AE=20=C2=B7=20=E6=95=B0=E6=8D=AE=E5=B7=A5=E5=8D=95?= =?UTF-8?q?=E5=8F=AA=E5=BB=BA=E7=BC=BA=E7=9A=84=E9=82=A3=E9=A2=97=20=C2=B7?= =?UTF-8?q?=20=E5=BD=95=E5=85=A5=E5=8D=A1=E8=87=AA=E5=B8=A6=20basis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ACT-WORK-05 差异评估的浏览器全流程走查(六轮六条路径,全部 released), 逐条治了实跑里现形的缺陷。活动包 1.2.2 → 1.2.6: - H(1.2.3):模型压拍把执行工单发了、清单一行没写,人对着空表 —— 「先写清单再发卡」只在剧本里,压过拍就只有说明书还在;规矩搬进说明书。 - I(1.2.4,结构治):原 PN 根本不在库,模型两颗都判「在库」,硬阻塞整条路被跳过。 剧本已写到头 → pn_check 加 found_pn_ref(refersTo + requiredWhen): 判在库就必须填一个基座查得到的料号,查无此值当场拒、填不出则收不了步。 这是「机检只读基座数」第六次现形,前五次都是计数,这次是存在性。 - J(1.2.5):数据工单派给了在库的那颗,缺的那颗没人建 —— 剧本把「建哪几颗」和「要不要问人」写在同一串分岔里,从没写过「found 的不建」;拆成两问。 - K(1.2.6):数据录入卡的参考信息栏摆着引擎内部家具(九个计划项全名)—— entryCard.basis 契约里可选,活动没声明,基座就兜了内部事实;卡骨架补三行人话。 包内自测 56 → 61 条;门禁 typecheck / lint / depcruise / test(2071)全绿 (api:check 的差异全在 org/* 与 api/*,是工作树里先前那批鉴权改动,本轮未添)。 docs/ACT-WORK-05-端到端测试记录.md:六轮走查的逐轮结论与 A–K 十一条发现总表。 docs/debt.md:四条平台欠账(disable 无路可走 · 前一项不收后一项的件推不出来 · 拒回数不出缺的是工具调用 · 零行台账的提交闸无牙)。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh --- .../__tests__/activity.test.ts | 7 + .../__tests__/steps.test.ts | 29 +++ .../src/case-model.ts | 34 ++- .../src/manifest.ts | 16 +- docs/ACT-WORK-05-端到端测试记录.md | 224 ++++++++++++++++++ docs/debt.md | 91 +++++++ 6 files changed, 394 insertions(+), 7 deletions(-) create mode 100644 docs/ACT-WORK-05-端到端测试记录.md diff --git a/activities/part-substitution-assessment/__tests__/activity.test.ts b/activities/part-substitution-assessment/__tests__/activity.test.ts index 73dd907..82e1aed 100644 --- a/activities/part-substitution-assessment/__tests__/activity.test.ts +++ b/activities/part-substitution-assessment/__tests__/activity.test.ts @@ -125,6 +125,13 @@ describe('台账声明:授权面与《统一方案》口径', () => { expect(byAddress(D, 'pn_check')?.fields.find((f) => f.name === 'abandon_reason')?.requiredWhen).toEqual({ column: 'exists_status', value: 'abandoned' }); }); + it('判「在库」必须填得出引用依据,基座拿它去主数据里查(2026-08-27 实测:原 PN 不在库而模型把两颗都写成 found,硬阻塞那条路整个被跳过)', () => { + const ref = byAddress(D, 'pn_check')?.fields.find((f) => f.name === 'found_pn_ref'); + expect(ref?.refersTo).toBe('parts.profile.internal_pn'); + expect(ref?.requiredWhen).toEqual({ column: 'exists_status', value: 'found' }); + expect(ref?.primaryKey ?? false).toBe(false); + }); + it('规则版本常量随包冻结', () => { expect(RULE_VERSION).toBe('R-2026.07'); expect(manifest.assets.businessModel.dmn.tables.map((t) => t.id)).toEqual(['d01-difference-status', 'd02-recommendation']); diff --git a/activities/part-substitution-assessment/__tests__/steps.test.ts b/activities/part-substitution-assessment/__tests__/steps.test.ts index d58983a..cf714f2 100644 --- a/activities/part-substitution-assessment/__tests__/steps.test.ts +++ b/activities/part-substitution-assessment/__tests__/steps.test.ts @@ -223,6 +223,35 @@ describe('说明书与剧本的骨架', () => { expect(s0d10).toMatch(/若它还开着|先.*finish/); }); + it('「发卡前清单必须先有行」写在说明书里,不只在剧本里(2026-08-27 实测:模型把 P-10 收步与 S1 发卡压成一拍,那一拍 S1 剧本没摊开,卡发出去时 input_gap 一行都没有,人对着空表无从下手)', () => { + expect(NARRATIVE).toContain('sub_eval.input_gap` 必须先有行'); + expect(NARRATIVE).toMatch(/压拍/); + expect(NARRATIVE).toMatch(/顺序不能反/); + }); + + it('存在性核对的剧本教的是「填得出依据才算在库」,不是「你看着办」(结构治,配 found_pn_ref 那道墙)', () => { + const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0_bind')?.guide ?? ''; + expect(g).toContain('found_pn_ref'); + expect(g).toMatch(/查无此值当场拒/); + expect(g).toMatch(/名单里没有就是没有/); + expect(g).toMatch(/把这一行改成 absent/); + }); + + it('数据工单的名单只认 absent,「建哪几颗」与「要不要问人」分开写(2026-08-27 实测:原 PN 缺库那单,模型把在库的候选派成了数据工单,缺的那颗没人建)', () => { + const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? ''; + expect(g).toMatch(/问题一 · 建哪几颗/); + expect(g).toMatch(/问题二 · 建之前要不要先问人/); + expect(g).toMatch(/exists_status = "found" 的一行都不建/); + }); + + it('数据录入卡自带 basis,不让基座兜上内部家具(2026-08-27 实测:没声明 basis 的那张卡,参考信息栏摆的是「这一步 / 当下可为 / 目标缺口」加九个计划项全名)', () => { + const i = NARRATIVE.indexOf('"kind":"entry"'); + expect(i).toBeGreaterThan(-1); + const block = NARRATIVE.slice(i, i + 900); + expect(block).toContain('"basis"'); + expect(block).toContain('触发原因'); + }); + it('五段进度,每段名 ≤10 字;除放行外每项恰属一段', () => { expect(ASSESSMENT_PHASES).toHaveLength(5); for (const p of ASSESSMENT_PHASES) expect(p.name.length, p.name).toBeLessThanOrEqual(10); diff --git a/activities/part-substitution-assessment/src/case-model.ts b/activities/part-substitution-assessment/src/case-model.ts index 8ff9838..24a5a32 100644 --- a/activities/part-substitution-assessment/src/case-model.ts +++ b/activities/part-substitution-assessment/src/case-model.ts @@ -197,7 +197,12 @@ PN 在库但个别字段缺 = 「有而不全」,归执行工单 01 补。**该 {"key":"name","label":"客户名称","value":"","required":true}, {"key":"industry","label":"所属行业"}, {"key":"region","label":"区域"} - ]} + ], + "basis":[{"k":"触发原因","v":"按客户编码 查客户档案,查无记录"}, + {"k":"这一单要评估的","v":"原 PN → 个候选"}, + {"k":"确认之后","v":"这家客户落进本单草稿层,评估照常往下走"}]} +**\`basis\` 这三行必须给**:不给的话基座会兜上「这一步 / 当下可为 / 目标缺口」那几行内部家具 —— +把九个计划项的全名摆到人脸上,他一个字也用不上(2026-08-27 实测)。 人确认那一下基座已把这一行落进草稿层,**你不许再 write 一遍 customer.profile**。 ② 候选不在库、且候选总数 ≥2 时,一颗一张决策卡(按候选数组顺序,一次只发一张;候选总数 = 1 时不问、直接建): @@ -230,7 +235,12 @@ PN 在库但个别字段缺 = 「有而不全」,归执行工单 01 补。**该 "standard":"每颗料号至少一行 datasheet 记录,必填列全齐。", "basis":[{"k":"触发原因","v":"按 internal_pn 查 P-10 查无记录"}]}} -⑤ 执行工单 01 · 评估输入与资料补齐确认(S1_input_confirm;零输入): +⑤ 执行工单 01 · 评估输入与资料补齐确认(S1_input_confirm;零输入)—— +**发这张卡之前,\`${D}.input_gap\` 必须先有行**:清单为空的卡就是让人对着一张空表签字,他补不了任何东西。 +不管这一拍是不是顺手把上一步收掉的(压拍常发生,那时 S1 的剧本还没摊开),写清单与发卡都得在同一串里,顺序不能反: +已绑定四项(customer / request / original_pn / candidate_N,status="ok")、待补两项(application_scenario / bom_position,status="gap")、 +默认三项(substitution_type / rule_version / owner,status="default")、每颗候选的「未披露」参数各一行(status="gap")、原 PN 资料版本(status="ok"); +逐列口径见这一步的剧本。 {"kind":"human","itemId":"S1_input_confirm","workOrder":"execution","card":{ "title":"评估输入与资料补齐确认:<原 PN> → 个候选", "ask":"启动绑定的四项已锁定。请在工作区的件上补齐「待补 / 待确认」行的补充内容,默认值可改可确认。", @@ -296,9 +306,14 @@ request_description(入参原话,不改写)、input_snapshot_ref、original_inte current_stage = "assessing"。application_scenario / bom_position **这一步不写**(启动时未收,S1 由人补)。 【逐个料号查库,写 \`${D}.pn_check\`】\`parts.profile\` 整表在你的作用域里,按 \`internal_pn\` 逐个核对:原 PN 一行(role = "original"), -每个候选一行(role = "candidate");在 → exists_status = "found";不在 → exists_status = "absent"(**这一格先写 absent,等数据工单确认后再改**)。 +每个候选一行(role = "candidate");在 → exists_status = "found" **且 \`found_pn_ref\` 填你在 \`parts.profile\` 里真找到的那一行的 internal_pn** +(与本行 internal_pn 一模一样);不在 → exists_status = "absent"、\`found_pn_ref\` 留空(**等数据工单确认后再改成 created**)。 一颗都不能少:数组里点了几颗候选,就写几行 + 原 PN 一行。note 选填(如「PCN 转 NRND」)。 +**「在库」这句话基座要拿去查**:\`found_pn_ref\` 声明了引用 \`parts.profile.internal_pn\`,写下去那一刻基座就去主数据里查, +**查无此值当场拒**。被拒不是让你换个号去顶 —— 那说明这颗料真的不在库,把这一行改成 absent(依据留空),数据工单那一步的门自己会开。 +判 found 却填不出依据也走不动:这一步的收步条件里有「核对表不缺必填」。**别凭料号长得像就判在库,名单里没有就是没有。** + 【写 \`${D}.verdict\` 一行】eval_ref、request_id、rule_version = "${RULE_VERSION}"、original_internal_pn、candidate_total = 候选数组长度。 写完这一项就结束。有 absent 的 → 下一项是 S0d_p08_master(数据工单),照常往下办。 @@ -319,9 +334,16 @@ current_stage = "assessing"。application_scenario / bom_position **这一步不 【先数两个数,再分岔】(先看轨迹:这一项里已经发过哪些卡、人答了什么,【当前这一步】下面那行引擎事实说得清楚) · **候选总数**:入参 \`candidateInternalPns\` 的长度(\`${D}.pn_check\` 里 role = "candidate" 的行数也是它,不随本单进展变化); · **不在库的候选数**:\`${D}.pn_check\` 里 role = "candidate" 且 exists_status = "absent" 的行数(这个数会随着人放弃/创建而变小)。 -**该不该问人,看的是候选总数,不是不在库的颗数**——**「不在库的只有一颗」不是跳过决策卡的理由**: -· 原 PN absent → **硬阻塞**,不问人,直接进入「建」的名单; -· **候选总数 = 1** → 那一颗不许放弃,不问人,直接进入「建」的名单; +**这里要分开答两个问题,别混成一个**: + +**问题一 · 建哪几颗?** 名单 = \`${D}.pn_check\` 里 **exists_status = "absent" 的每一行**,不管它是原 PN 还是候选。 +**exists_status = "found" 的一行都不建** —— 那几颗的主数据本来就在库里,给它们派数据工单等于让人把已有的东西再填一遍, +而真正缺的那颗还是没人建、单子照样卡死。名单为空(一行 absent 都没有)这一项根本不会出现在你的「当下可为」里。 + +**问题二 · 建之前要不要先问人?** 只有 **absent 的候选**才谈得上问,**看的是候选总数,不是不在库的颗数** +——**「不在库的只有一颗」不是跳过决策卡的理由**: +· 原 PN absent → **硬阻塞**,不问人,直接进名单(它是评估基准,没有它整单无从谈起); +· **候选总数 = 1** → 那一颗候选**如果是 absent** 就不许放弃、不问人,直接进名单(候选是 found 的话它压根不在名单里,没有可问的); · **候选总数 ≥ 2** → **每一颗 absent 的候选都要先发一张决策卡问人**(一次一张,按候选数组顺序;哪怕这一单不在库的候选只有一颗, 只要候选总数 ≥ 2 就要问)照说明书②;人选 "abandon"/"custom:" 的,把那一行 \`${D}.pn_check\` 改成 exists_status = "abandoned" + abandon_reason(同 PK 覆盖写),这颗不再评估;选 "create" 的进「建」的名单。 diff --git a/activities/part-substitution-assessment/src/manifest.ts b/activities/part-substitution-assessment/src/manifest.ts index a952602..14b82d5 100644 --- a/activities/part-substitution-assessment/src/manifest.ts +++ b/activities/part-substitution-assessment/src/manifest.ts @@ -89,7 +89,7 @@ export const manifest: ActivityManifest = { identity: { activityId: ACTIVITY_ID, name: 'ACT-WORK-05 · 差异评估', - packageVersion: '1.2.2', + packageVersion: '1.2.6', category: '需求分析与方案匹配', sdkVersion: '0.1.0-m5', checksum: 'dev-checksum-part-substitution-assessment', @@ -439,6 +439,20 @@ export const manifest: ActivityManifest = { col({ name: 'internal_pn', type: 'text', label: '内部料号', primaryKey: true }), col({ name: 'role', type: 'text', label: '角色', required: true, tally: ['original', 'candidate'] }), col({ name: 'exists_status', type: 'text', label: '在库状态', required: true, tally: ['found', 'absent', 'created', 'abandoned'] }), + /* + * 「在库」不是模型说了算的一句话 —— 判 found 就必须把**在主数据里真找到的那个料号**填在这里, + * 基座拿它去 `parts.profile` 查(refersTo),查无此值当场拒。requiredWhen 让这一格只对 found 行必填, + * 而 S0_bind 的 doneWhen 里有 `_missing = 0` —— 于是「判了 found 却填不出依据」这一步就收不掉。 + * 2026-08-27 实测逼出来的:原 PN 根本不在库,模型把两颗都写成 found、交代里还说「都在库」, + * 硬阻塞那条路整个被跳过(剧本早写着「整表在你的作用域里逐个核对」—— 措辞治不了的结构治)。 + */ + col({ + name: 'found_pn_ref', + type: 'text', + label: '在库核对依据', + refersTo: 'parts.profile.internal_pn', + requiredWhen: { column: 'exists_status', value: 'found' }, + }), col({ name: 'abandon_reason', type: 'text', label: '放弃原因', requiredWhen: { column: 'exists_status', value: 'abandoned' } }), col({ name: 'note', type: 'text', label: '备注' }), ], diff --git a/docs/ACT-WORK-05-端到端测试记录.md b/docs/ACT-WORK-05-端到端测试记录.md new file mode 100644 index 0000000..1231f37 --- /dev/null +++ b/docs/ACT-WORK-05-端到端测试记录.md @@ -0,0 +1,224 @@ +# ACT-WORK-05 差异评估 —— 端到端测试记录 + +> 这份文档记的是**每一轮实跑逼出了什么、改了什么、怎么验证的**。 +> 活动包:`activities/part-substitution-assessment`;业务出处 `docs/SS02-ACT-WORK-05-html/`。 +> 台账字段口径见 `docs/数据域字段说明-W05.md`。 +> 全部走查在**浏览器**里做(测试台起单 → 工作台真人点),不是脚本代答。 + +## 一、这个活动是什么 + +原 PN 与候选替代料之间**逐项差异评估**:比参数、判四态(MATCH / MISMATCH / UNKNOWN / CONDITIONAL)、 +定推荐等级(preferred / alternate / conditional / not_recommended)、留证据与验证任务, +最后把结论落进 P-11(替代关系 + 差异明细)并回写 P-01 需求,发射 `assessment-released` 交 ACT-GATE-03 评审。 + +**九个计划项**(五处不给 doneWhen —— 触达人的一律交人收): + +| 项 | 谁办 | 卡 / 件 | doneWhen | +|---|---|---|---| +| `S0_bind` | AI | —— | 有 | +| `S0d_p08_master` | 人 · 数据工单 | `p08-master-create`(表1 芯片主数据 + 表2 标准化参数集) | 无 | +| `S0d_p10_document` | 人 · 数据工单 | `p10-document-create`(原厂技术资料) | 无 | +| `S1_input_confirm` | 人 · 执行工单 | `eval-input-list`(评估输入清单) | 无 | +| `S2_normalize` | AI | —— | 有 | +| `S3_assess` | AI + 两枚工具 | —— | 有 | +| `S4_review` | 人 · 决策卡(可循环) | `difference-review` | 无 | +| `S5_prepare_landing` | AI | —— | 有 | +| `T_release` | 人 · 终局验收 | `landing-confirm` | 无 | + +## 二、逐轮走查 + +### R1 · 样例「都在库·常路」 + +**第 1 次 —— 熔断**,逮到两条真缺陷: + +- 轨迹里 `S0_bind` 写完三张表后连续 5 拍只做一件事「弃用计划项 `S0d_p08_master`」,每拍被拒 + 「生命周期迁移非法:Available → Disabled」→ 熔断交人;工作区还摆着一张「待建 0 条记录」的空数据工单件。 +- **根因一(平台事实,本轮不改)**:`lifecycle.ts` 里 `Disabled` 只能从 `Enabled` 来,而 `Enabled` 只由 + `plan` 动作写;普通计划项一直停在 `Available`。全仓只有本活动用过 `disable` —— **这条路从没人走通过**。 +- **根因二(活动缺陷,已修)**:哨兵 `s_bound` 只看「需求写了 + 核对表写了」,而 `S0_bind` 在**每条路**上都写这两张表, + 于是都在库时数据工单那两项照样进「当下可为」,剧本就让模型去 disable。 +- 修:哨兵加 `sub_eval_pn_check_exists_status_absent > 0`;全包清 `disable`;两条回归测试。**v1.0.1**。 + +**第 2 次 —— 通了**:两拍写完 `demand.request` / `pn_check` / `verdict`,直接派出执行工单; +提交闸如实拦住空手交卷并点名三条缺项(与件上红带同名同数);填三格保存 → 闸自收 → 提交。 + +**收工:`released` · 12 拍 · 人环 3 · 行动被拒 0**。落账逐条 SQL 核过: + +- P-11 主记录 1 条:`pin_to_pin | alternate | fae_confirmed | 客批=(空) | R-2026.07` —— **客户批准状态确实留空**(只读边界守住),规则版本随结论落账; +- P-11 差异明细 9 条(8 MATCH / 1 CONDITIONAL);P-01 回写五项齐;出口 `assessment-released` 已发射; +- 验收卡 11 条判据全部达标(L1×2 / L2×6 / L4×3),一次通过。 + +### R2 · 样例「候选缺库·派数据工单」 + +四次开跑,逼出四条缺陷(E / F / G / H),逐条治了再跑;第 4 次(v1.2.3)全绿。 + +| 次 | 版本 | 结果 | 逼出什么 | +|---|---|---|---| +| 1 | 1.2.0 | 中途停 | E(决策卡被跳过)已在本次前修好并当场验证;F 现形 | +| 2 | 1.2.1 | 熔断 | F 修复通过;**G 现形**(`recommend` 工具被跳过 → S3 收步恒假) | +| 3 | 1.2.2 | 中途停 | G 修复通过;**H 现形**(压拍发卡,清单一行没有) | +| 4 | 1.2.3 | **`released` · 被拒 0 · 人环 5** | 四条修复全部实测通过 | + +第 4 次逐段实证: + +- 决策卡按规格出现(候选总数 2 ≥ 2)→ 答「创建主数据」; +- P-08 数据工单**一件两表**(主数据 1 行 + 参数 6 行),两张表各有自己的编辑闸与缺项红带; + 填好保存 → 提交 → **画面自己切到 P-10**(发现 F 的修复稳定); +- **执行工单出来时清单已有 12 行**(上一轮是 0)—— 发现 H 的修复见效;提交闸如实拦住剩下那条 + 「未披露」缺口,补上才放行 —— 闸与件同源; +- **S3 两枚工具都真调了**(轨迹里 `compare-parameters 返回` / `recommend 返回` 各一条),自动收步,没有熔断; +- **审核驳回循环**(本轮顺带覆盖):在「写下你的口径」里写两条具体意见 → 模型改台账 → **换问法重发**新卡。 + 逐条核过落点:XL2596S-5.0 降 `conditional`、两条 UNKNOWN 明细写明处置(挂 `VAL-…` 验证任务、 + 「闭环前不升级推荐等级」)、LM2596S-ADJ 的推荐理由点名「R1/R2 分压电阻」—— **意见落在被点名的那两处**; +- 终局验收 11 条判据全部达标 → 确认通过 → `released`。 + +落账 SQL 复核:P-11 两条关系(`pin_to_pin | conditional | fae_confirmed` / `pin_to_pin | alternate | fae_confirmed`, +客批均空、规则版本 `R-2026.07`)· 差异明细 18 条(15 MATCH / 2 UNKNOWN / 1 CONDITIONAL)· +验证任务 1 条(`not_started`)· P-01 回写五项齐 · **XL2596S-5.0 真进了主数据**(`XLSEMI | active`,6 行参数 + 1 份资料)。 + +### R3 · 样例「三候选·含硬约束不推荐」—— `released` · 被拒 0 + +一次跑通,验的是**硬约束这条路**: + +- 决策卡按规格出现(3 颗候选,只有 CN-DCDC-32V 缺库)→ 建主数据 → P-08 填 + **输入电压范围 4.5~32(hard)**、工作温度 -20~+85(hard); +- 清单 13 行、三行 default 的 detail 写对了默认值本身;缺口里把客户约束写成可解析式「输入上限硬指标 >= 40(V)」; +- S2 铺 3 条关系 + 27 条明细;S3 两枚工具都调过; +- **CN-DCDC-32V = `not_recommended` / high**(两条硬约束 MISMATCH:输入 4.5~32 vs 4.5~40、温域 -20 vs -40), + XL2596S-5.0 `conditional`、LM2596S-ADJ `alternate`; +- 验收件上三颗候选各一份「拟写 P-11」字段表,徽标逐个核过(不推荐/高 · 条件推荐/中 · 备选/中 · Pin-to-Pin · FAE确认)。 + +**一次自纠**:我先按 `innerText` 判定「四个枚举列是空的」,险些记成缺陷 —— +徽标文字是 CSS `::after` 生成的,`innerText` 读不到。**验证呈现要看 computed style,不看 innerText**。 + +### R4 · 样例「原PN缺库·硬阻塞」 + +三次开跑,逼出两条**同族**的真缺陷: + +**第 1 次(v1.2.3)—— 发现 I:「在库」是模型自报的一句话** + +原 PN `LM2596S-3.3` 主数据里**一行都没有**(SQL 复核 count = 0),模型把原 PN 与候选**两颗都写成 `found`**, +交代里还写「原 PN 与候选都在库」—— 数据工单一张没派,直接把评估输入执行工单发给了人。 +剧本早写着「`parts.profile` 整表在你的作用域里,按 internal_pn 逐个核对」——**措辞已到头**。 + +**结构治**:`sub_eval.pn_check` 加一列 `found_pn_ref`, +`refersTo: parts.profile.internal_pn` + `requiredWhen: {exists_status: found}`。 +于是「判在库」这句话必须配一个**基座拿去主数据里查得到**的料号: + +- 查无此值 → `draftWrite` 当场拒; +- 填不出 → `pn_check_missing > 0` → `S0_bind` 的 `doneWhen` 收不掉。 + +两条路都堵死,唯一走得通的就是如实写 `absent`。**这是「机检只读基座数」第六次现形** +(前五次是 missing_key_total / missing_minor_total / untyped_total / draft_total / 哨兵读 line_total), +这次的形状不是计数,是**存在性**。 + +**第 2 次(v1.2.4)—— 墙生效了,但逮出发现 J:数据工单派给了在库的那颗** + +核对表这次写对了(`LM2596S-3.3 = absent`、`LM2596S-ADJ = found` 且依据经基座查过), +可数据工单派给了 **LM2596S-ADJ**(它在库、参数资料齐全),真正缺的那颗没人建。 +交代原话:「候选只有 LM2596S-ADJ 一颗,按规则直接建主数据」—— +它把「候选总数 = 1 → 不问人」这条**关于问不问**的规则,读成了**关于建哪颗**的规则。 +病根:剧本把两个问题写在同一串项目符号里,三档分岔每档都以「→ 直接进入建的名单」收尾, +而**从没写过「found 的不建」**。与发现 E 同形。 + +**治法(措辞,精准化)**:拆成「**问题一 · 建哪几颗**」(名单 = `absent` 的每一行,`found` 的一行都不建,并写明后果) +与「**问题二 · 建之前要不要先问人**」(只有 absent 的候选才谈得上问)。这一处不上结构 —— +系统其实兜得住(原 PN 还 absent → S1 哨兵不开 → 依赖断点),只是要白烧一次人环; +而这一刀是**补上从没写过的那句话**,不是把已经写过的话再说一遍。 + +**第 3 次(v1.2.5)—— `released`**:数据工单派给了 `LM2596S-3.3`,**没发决策卡**(硬阻塞不问人),两条修复都实测通过。 + +这一轮还留下了**发现 I 那道墙开火的现场证据**(轨迹 seq 6): + +> 行动被拒:在库核对依据「LM2596S-3.3」在芯片主数据(P-08)里不存在 —— 这一列只许引用台账里真实存在的值…… + +模型确实又想判「在库」,基座当场拒回并点名;它随后如实写了 `absent`,硬阻塞照规格走了下去。 + +**终局「重做」分支也在这一轮验了**(本活动此前未验):在验收卡上写明两条意见后点「重做」—— +模型没有原样再递一次,而是**真去改了台账**(`demand.request.verification_requirement` 重写、 +5 行 `difference_item.evidence_ref` 从「只写文件名」改成指到 `Rev H (2023-09) §6.5 Electrical Characteristics`), +再重新裁量 + 提交;第二张验收卡确认通过 → `released`。 + +整单 2 次被拒:一次是上面那道墙(它该拒),一次是 S3 的「先 finish 后调工具」—— +拒回文案照旧只谈「行与列」(平台欠账已记),但模型下一拍自己去调了 `recommend`、随即自动收步,没有熔断。 + +落账:`parts.profile.LM2596S-3.3`(TI / nrnd,7 行参数)真进了主数据; +P-11 一条(`LM2596S-3.3 → LM2596S-ADJ | alternate | fae_confirmed`);P-01 回写齐。 + +### R5 · 样例「新客户·先建档」 + +验的是**数据录入卡**这条路(客户不在档案里,先收人、再评估): + +- 卡按规格出现:「客户 CU-9001 不在客户档案里,请核对建档信息后确认」,客户名称已从入参带出、行业与区域留白给人补; +- 补两格 → 确认写入 → 客户档案落草稿层(`深圳恒星电源有限公司 / 工业电源 / 华南`)→ 流程直奔 S1(两颗料都在库); +- 缺口补齐 → 审核通过 → 终局确认 → `released`。 + +**发现 K**:那张录入卡的「参考信息」栏摆的是引擎兜底的**内部家具** —— +「这一步 / 当下可为 / 目标缺口」,后面跟着**九个计划项的全名**,人一个字用不上。 +病根:`entryCard.basis` 在契约里是**可选**的,活动的卡骨架没声明,基座就把内部事实兜了上去。 +与 A1 2026-08-03 那次同族(那次是执行卡的 steps/standard 没进说明书)。 +治法:说明书里的录入卡骨架补 `basis` 三行(触发原因 / 这一单要评估的 / 确认之后会发生什么), +并在骨架下写明「不给基座会兜内部家具」。配一条测试。 + +**一处操作教训(不是缺陷)**:测试台在「清场」之后会回落到样例 0 —— +选样例与「起单试跑」必须在同一次操作里完成,否则起的是上一个默认样例。 + +## 二·五、六轮走查一览 + +| 轮 | 样例 | 结果 | 这一轮覆盖的分支 | +|---|---|---|---| +| R1 | 都在库·常路 | `released` · 12 拍 · 人环 3 · 被拒 0 | 无数据工单直通;验收判据 11/11 | +| R2 | 候选缺库·派数据工单 | `released` · 被拒 0 · 人环 5 | 决策卡(创建)· P-08 一件两表 · P-10 · **审核驳回循环** | +| R3 | 三候选·含硬约束不推荐 | `released` · 被拒 0 | 三候选并行 · **硬约束 MISMATCH → `not_recommended`** | +| R4 | 原PN缺库·硬阻塞 | `released` · 被拒 2(其中一次是墙该拒) | **硬阻塞不问人** · **终局「重做」循环** | +| R5 | 新客户·先建档 | `released` · 被拒 1 | **数据录入卡建档** | +| R5b | 新客户(v1.2.6) | 验完即停 | 录入卡 `basis` 呈现 | + +版本轨迹:1.0.0 → 1.0.1(A)→ 1.1.0/1.1.1(B·C·D)→ 1.2.0(参数子表)→ 1.2.1(F)→ 1.2.2(G) +→ 1.2.3(H)→ 1.2.4(I)→ 1.2.5(J)→ **1.2.6(K)**。包内自测 16 → **61 条**。 + +## 三、走查发现总表 + +| # | 现象 | 病根 | 治法 | 层 | +|---|---|---|---|---| +| A | 缺口表**每一行**都摆红「必填」,而闸只认 3 条 | 那一格用静态 `req` class,`supplement` 的声明是 `requiredWhen:{status:'gap'}` | 按行投影 `data-class-of="status"`,判断留样式表 | 活动 | +| B | 五张模版是 E04/I01 的 `jc-rp-*` 外壳,不是目标件的版面 | 计划文本自相矛盾(一边说逐字量自目标件,一边让用 `prd-style.ts`) | 目标件 `