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'] }, +];