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 · 评审放行与落账确认', '工单类型:验收'), +});