diff --git a/activities/part-substitution-assessment/__tests__/activity.test.ts b/activities/part-substitution-assessment/__tests__/activity.test.ts index d2fd7de..7454f79 100644 --- a/activities/part-substitution-assessment/__tests__/activity.test.ts +++ b/activities/part-substitution-assessment/__tests__/activity.test.ts @@ -12,12 +12,20 @@ import { createMockHost } from '@sdk'; import { manifest, D } from '../src/manifest.ts'; import activity from '../src/index.ts'; -const ITEMS = ['S0_bind', 'S0d_p08_master', 'S0d_p10_document', 'S1_input_confirm', 'S2_normalize', 'S3_assess', 'S4_review', 'S5_prepare_landing', 'T_release']; /* - * 2026-08-27 晚:两张数据工单改成**给 doneWhen**(只读入库事实)—— 引擎只在人答过卡之后求它, - * 不再靠模型记得 finish。仍不给的三处:输入确认(交人补缺口)/ 审核循环 / 放行。 + * 2026-08-29 Task 3 整体重写(spec §4):九步砍成四步,案例模型换成 CMMN + `case-model.ts` 里的 + * `ASSESSMENT_STEPS` 四条剧本。这份包内自测原是照旧九步写的,`案例模型与清单的对齐` 这组的多数 + * 断言在结构上仍然成立(CMMN humanTask id 与剧本 item 一一对应、除放行外每项恰属一个阶段…), + * 只是数字与名单要随新模型换 —— 下面这几处常量与断言标题已按 `case-model.ts` 的新四步同步更新。 */ -const NO_DONE_WHEN = new Set(['S1_input_confirm', 'S4_review', 'T_release']); +const ITEMS = ['S1_inputs', 'S2_assess', 'S3_review', 'T_release']; +/* + * 2026-08-29 Task 3:四步里只有 `S3_review`(审核,可循环)与 `T_release`(放行)不给 + * `doneWhen`——判据不许摆在还欠一张卡的那一步眼前(2026-08-28 用户实测逼出的立墙, + * `humanWrites`/`acceptanceChecklist` 同一课)。`S1_inputs` / `S2_assess` 全 auto, + * 入库事实 / 分析结论一落地引擎就自己收步,不再需要人工激活等待。 + */ +const NO_DONE_WHEN = new Set(['S3_review', 'T_release']); const VIEWS = ['p08-master-create', 'p10-document-create', 'eval-input-list', 'difference-review', 'landing-confirm']; /** 运营中心 L2 沙箱里那张关键台账数据看板 —— 不是工单件,只在「都声明了」这一条里出现;自己那组断言在文件尾 */ const BOARD_VIEW = 'activity-ledger'; @@ -222,35 +230,43 @@ 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(审核 / 放行),其余两处都给 —— 校验入参与逐项分析全 auto、入库/结论事实一落地就收步', () => { 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('两张数据工单件各自绑自己的 view(P-08 与 P-10 不共用同一份画面)', () => { + /* + * SKIP(2026-08-29,Task 3):`S0d_p08_master` / `S0d_p10_document` 两个计划项已随四步重写撤下 —— + * 「补芯片主数据」与「补原厂技术资料」不再是两张各自的数据工单,统一并入 `S1_inputs` + * (校验入参与补数据)一步之内。Task 3 的 brief 明写这一步不设 `view`(它是可选的, + * Task 7–9 各自补上自己那一张),所以这条「两张数据工单件各自绑自己的 view」眼下没有对应的 + * 断言对象可钉 —— 不是「暂时还没做」,是这个二选一视图的设计前提本身已经不存在了。 + */ + it.skip('两张数据工单件各自绑自己的 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 里都标了人工激活', () => { + it('那两项在 CMMN 里都标了人工激活', () => { for (const id of NO_DONE_WHEN) { const block = new RegExp(``).exec(manifest.assets.businessModel.cmmnXml)?.[0] ?? ''; - expect(block, id).toContain('manualActivationRule'); + /* Task 3 起改用 `manualActivation="true"` 属性直接标在 humanTask 元素上(与 CMMN 解析器的两种认法之一同款),不再嵌套 */ + expect(block, id).toContain('manualActivation="true"'); } }); - it('除放行外每一项恰好属于一个阶段;5 段 + 放行 = x/6,与文档 0.5/1/2/3/4/5 六步对齐', () => { + it('除放行外每一项恰好属于一个阶段;3 段 + 放行 = x/4', () => { const phases = manifest.assets.businessModel.phases ?? []; - expect(phases).toHaveLength(5); + expect(phases).toHaveLength(3); 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); @@ -261,7 +277,14 @@ describe('案例模型与清单的对齐', () => { for (const name of mentioned) expect(ITEMS, `说明书提到但不存在的计划项: ${name}`).toContain(name); }); - it('说明书里只剩审核那一种决策卡(approve/reject/custom:);候选放弃/创建那张已退役 —— 缺库一律自动派数据工单(2026-08-27 用户定)', () => { + /* + * SKIP(2026-08-29,Task 3):这条测的是「决策卡骨架住在说明书的散文里,模型抄卡骨架时照读」—— + * 那正是**零 `guide`**重写要撤掉的机制本身(卡骨架挪进配方 `auto.do[].kind:'card'` 的结构化 + * `options` 字段,由引擎按声明发卡,不再靠模型从说明书里抄 JSON 片段)。新版 `NARRATIVE` + * 因此不再嵌任何 `"value":"..."` 这类卡面 JSON —— 不是「还没写全」,是这条断言检查的机制 + * 在四步全 auto 的设计里已经不存在了(决策卡的选项由 Task 4–6 的配方声明,不由说明书文本决定)。 + */ + it.skip('说明书里只剩审核那一种决策卡(approve/reject/custom:);候选放弃/创建那张已退役 —— 缺库一律自动派数据工单(2026-08-27 用户定)', () => { const n = manifest.assets.businessModel.narrative; expect(n).not.toContain('"value":"create"'); expect(n).not.toContain('"value":"abandon"'); @@ -271,10 +294,13 @@ describe('案例模型与清单的对齐', () => { }); /* - * SKIP(2026-08-29,Task 2 →Task 3 补回):`contract.acceptanceCriteria` 已按 spec §7 整体重写, - * 旧的九步 T_release 剧本(`case-model.ts`,本任务不动)引的还是旧的三条 L4 label - * (「本单数据工单的创建记录」等旧措辞)。Task 3 整体重写四步案例模型时,新 T_release 会照新的 - * 两条 L4 判据重新引用,那时再放开这条断言。 + * SKIP(2026-08-29,Task 2 → Task 3 补回,复核后维持 skip):原计划这条断言的是 + * 「T_release 剧本里 `judge criterion="..."` 的原文逐字对得上 acceptanceCriteria 的 label」—— + * 而 Task 3 的四步重写是**全程零 guide**:`T_release` 的 `guide` 现在是 `''`, + * 「prose guide 里嵌 `judge criterion="..."` 给模型执行」这套机制随零 guide 重写整体撤掉了 + * (裁量判据改由人工验收清单 `acceptanceChecklist` 让人亲自核,不再靠模型在 guide 里自判)。 + * 这不是「Task 3 会把断言点的字符串换成新的两条」——是断言检查的机制本身不复存在, + * 无法在不改变检查对象的前提下让它变绿,继续保留跳过并记录清楚原因。 */ it.skip('T_release 剧本里 judge criterion 的原文逐字对得上 acceptanceCriteria 的 label', () => { const tRelease = (manifest.assets.businessModel.steps as ReadonlyArray<{ item: string; guide?: string }>).find((s) => s.item === 'T_release'); diff --git a/activities/part-substitution-assessment/__tests__/steps.test.ts b/activities/part-substitution-assessment/__tests__/steps.test.ts index cf9cb1c..8cc6612 100644 --- a/activities/part-substitution-assessment/__tests__/steps.test.ts +++ b/activities/part-substitution-assessment/__tests__/steps.test.ts @@ -1,506 +1,115 @@ -/** - * 步骤剧本与呈现面 —— 这段错了不报错,只是活动开始转圈 / 界面开始撒谎(与 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'; +import { planStepSchema } from '@contracts'; +import { ASSESSMENT_STEPS, ASSESSMENT_PHASES, CMMN_XML, NARRATIVE } from '../src/case-model.ts'; +import { manifest } from '../src/manifest.ts'; -const ITEM_IDS = [...CMMN_XML.matchAll(/<(?:humanTask|decisionTask|stage|milestone)\s+id="([^"]+)"/g)].map((m) => m[1] ?? ''); +const ITEMS = ['S1_inputs', 'S2_assess', 'S3_review', 'T_release']; +const stepOf = (item: string) => ASSESSMENT_STEPS.find((s) => s.item === item); -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, - 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, - 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); +describe('四步案例模型', () => { + it('恰好四步,次序即业务次序', () => { + expect(ASSESSMENT_STEPS.map((s) => s.item)).toEqual(ITEMS); }); - it('清单里挂的就是这份剧本与这份阶段', () => { - expect(manifest.assets.businessModel.steps).toBe(ASSESSMENT_STEPS); - expect(manifest.assets.businessModel.phases).toBe(ASSESSMENT_PHASES); + it('每一步都过得了 PlanStep 契约', () => { + for (const step of ASSESSMENT_STEPS) expect(planStepSchema.safeParse(step).success).toBe(true); + }); + + it('**全程零 guide、零工具** —— 这是这次重写的立场', () => { + for (const step of ASSESSMENT_STEPS) { + expect(step.guide).toBe(''); + expect(step.tools).toEqual([]); + expect(step.auto).toBeDefined(); + } + }); + + it('CMMN 里四个计划项都在,且触达世界的两步标了人工激活', () => { + for (const item of ITEMS) expect(CMMN_XML).toContain(`id="${item}"`); + /* 审核与放行触达的是人 —— 完成与否不该由数据形状推出来 */ + for (const item of ['S3_review', 'T_release']) { + expect(new RegExp(`id="${item}"[^>]*manualActivation="true"`).test(CMMN_XML)).toBe(true); + } + }); + + it('**触达世界的两步不给 doneWhen** —— 给了等于给引擎一把替人签字的钥匙', () => { + expect(stepOf('S1_inputs')?.doneWhen).toBeTruthy(); + expect(stepOf('S2_assess')?.doneWhen).toBeTruthy(); + expect(stepOf('S3_review')?.doneWhen).toBeUndefined(); + expect(stepOf('T_release')?.doneWhen).toBeUndefined(); + }); + + it('**判据不许编一个数不出来的数**:doneWhen 里不出现「本类型有几个字段」这类', () => { + /* `parts_type_param_rows` 数的是本单草稿写下的字段行(人新建的那几个),不是整本字典 —— 拿它当分母是错的 */ + for (const step of ASSESSMENT_STEPS) { + expect(step.doneWhen ?? '').not.toContain('parts_type_param_rows'); + } + }); + + it('S1 收步:两颗都在库、都归了类型、且是同一个类型、主数据与参数都不缺', () => { + const done = stepOf('S1_inputs')?.doneWhen ?? ''; + expect(done).toContain('sub_eval_part_exists_status_found = 2'); + expect(done).toContain('sub_eval_part_missing = 0'); + expect(done).toContain('sub_eval_part_distinct_chip_type_id = 1'); + expect(done).toContain('parts_profile_missing = 0'); + expect(done).toContain('parts_parameter_missing = 0'); + }); + + it('S2 收步:明细写了且不缺、关系一行、验证记录一行', () => { + const done = stepOf('S2_assess')?.doneWhen ?? ''; + expect(done).toContain('substitution_difference_item_rows > 0'); + expect(done).toContain('substitution_difference_item_missing = 0'); + expect(done).toContain('substitution_relation_rows = 1'); + expect(done).toContain('validation_record_rows = 1'); + }); + + it('两张卡各带人工验收清单 —— 一条一句人眼核得动的动作', () => { + for (const item of ['S3_review', 'T_release']) { + const list = stepOf(item)?.acceptanceChecklist ?? []; + expect(list.length).toBeGreaterThanOrEqual(4); + for (const line of list) expect(line.length).toBeGreaterThan(6); + } + expect(stepOf('S3_review')?.cardVariant).toBe('review'); + }); + + it('阶段:除放行外每一项恰好属于一个阶段', () => { + const covered = ASSESSMENT_PHASES.flatMap((p) => p.items); + expect(covered.sort()).toEqual(['S1_inputs', 'S2_assess', 'S3_review'].sort()); + expect(new Set(covered).size).toBe(covered.length); + }); + + it('说明书只讲整单的事 —— 逐步做法归配方,而配方不是给模型看的', () => { + expect(NARRATIVE.length).toBeLessThan(2000); + /* **示例不引具体料号与参数名** —— 模型会把示例当成这一单的值(2026-08-27 复现三次) */ + for (const forbidden of ['LM2596S', 'XL2596S', 'AMS1117', '使能阈值']) { + expect(NARRATIVE).not.toContain(forbidden); + } }); }); +/* + * 恢复自 Task 2 遗留的两条 `it.skip`(原住在这份文件里,注释指名「Task 2 → Task 3 补回」)—— + * 旧版断言的是旧九步剧本引旧台账地址 / 旧两枚工具,那两个前提随四步重写一起撤了, + * 这里按新的四步剧本与九张新台账重新写实(逻辑不变:读写面必须落在清单声明里、 + * 工具必须落在声明过的工具里),不是新造的断言。 + */ 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)); - /* - * SKIP(2026-08-29,Task 2 → Task 3 补回):`manifest.declarations.ledger` 已按 - * `docs/superpowers/specs/2026-08-29-act-work-05-rewrite-design.md` §3 整体重写成九张新表, - * 而 `ASSESSMENT_STEPS`(`case-model.ts`)还是旧的九步、引用的还是已退休的旧台账地址 - * (`demand.request` / `customer.profile` / `parts.document` / `sub_eval.pn_check` 等)—— - * Task 3 整体重写案例模型(四步、零 guide)时,`writes`/`reads` 会重新对齐新的九张表。 - */ - it.skip('steps[].writes / reads(台账那几个)全落在声明过的对象上', () => { + 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); + /* reads 里混着入参名(originalInternalPn 等)与 case: 前缀伪读数,只有「域.对象」形状的才对声明表 */ for (const address of step.reads.filter((r) => r.includes('.') && !r.startsWith('case:'))) { expect(declaredAddresses.has(address), `${step.item} 要读 ${address}`).toBe(true); } } }); - /* - * SKIP(2026-08-29,Task 2 → Task 3 补回):`declarations.tools` 已清空(零工具是这次重写的 - * 立场),而旧的 S3_assess 步骤仍声明着两枚已退役工具的字面量(constants.ts 头注)。 - * Task 3 重写案例模型时四步「全程零 guide、零工具」,这条断言到时会被新版覆盖。 - */ - it.skip('steps[].tools 全部是声明过的工具;只有 S3 调工具', () => { + it('steps[].tools 全部是声明过的工具 —— 且四步全是空数组(零工具是这次重写的立场,不是疏漏)', () => { 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).not.toContain('"kind":"decide","itemId":"S0d_p08_master"'); - // 两张数据工单都得标 variant:"data" —— 对话区凭它换成「数据工单」那副面孔 - expect(NARRATIVE.match(/"variant":"data"/g)?.length).toBe(2); - // 但模型抄卡会把它丢掉(2026-08-27 实测)—— 真正作数的是剧本上的声明,引擎按它决定 - for (const item of ['S0d_p08_master', 'S0d_p10_document']) { - expect(ASSESSMENT_STEPS.find((s) => s.item === item)?.cardVariant, item).toBe('data'); - } - expect(ASSESSMENT_STEPS.find((s) => s.item === 'S4_review')?.cardVariant).toBe('review'); - for (const step of ASSESSMENT_STEPS) if (!['S0d_p08_master', 'S0d_p10_document', 'S4_review'].includes(step.item)) expect(step.cardVariant, step.item).toBeUndefined(); - 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('工具调用后同拍其余动作作废'); - }); - - /* - * 2026-08-27 晚,用户:「这个做差异评估比对的 harness 肯定有问题,要让它认真比对每一条」。实测(0bddd862)模型把剧本示例里的 - * 「4.5~32 / >= 40」当成这单的值念出来;比对入参改由工具从台账现读,模型只报料号与客户约束,示例里不再有任何具体参数值。 - */ - it('S3 第一拍只报料号与客户约束:入参形状是 originalInternalPn + candidates[].requirements,示例里没有具体参数值', () => { - const s3 = ASSESSMENT_STEPS.find((s) => s.item === 'S3_assess')?.guide ?? ''; - expect(s3).toContain('"originalInternalPn":"<原 PN>"'); - expect(s3).toContain('"requirements"'); - expect(s3).not.toContain('"pairs"'); - expect(s3).not.toMatch(/4\.5~32|4\.5~40|32V|40V/); - expect(s3).toMatch(/一个参数值都不抄/); - // 写盘照抄出参:值与 compare_result 都不许换 - expect(s3).toMatch(/compare_result 也照抄/); - }); - - 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(/收不了步|收不掉/); - }); - - /* - * 数据工单确认即入库(2026-08-27 用户定:「数据工单里填的数据,确认后就应该入库」)。 - * 基座在确认那一刻把卡上 gateLedgers 那几张表并进正式表;剧本这一头不能再教模型「已经落草稿层」—— - * 那句话会让它以为放行前还能改、还能 drop,而在库的键写下来会被当场拒。 - */ - it('两张数据工单的剧本都写明:确认后已入正式表,别再写、别 drop 那几行;P-10 的卡骨架也说「确认后即入库」', () => { - for (const item of ['S0d_p08_master', 'S0d_p10_document']) { - const guide = ASSESSMENT_STEPS.find((s) => s.item === item)?.guide ?? ''; - expect(guide, item).toContain('入了正式表'); - expect(guide, item).not.toContain('已经落草稿层'); - expect(guide, item).toContain('别去 drop'); - } - expect(NARRATIVE).toContain('确认后这批主数据即入库'); - expect(NARRATIVE).toContain('确认后这批资料记录即入库'); - }); - - /* - * S1 的「待确认」行只许来自真写着「未披露」的参数(2026-08-27 用户:「我在第一步已经补充了 - * XL2596S-5.0 使能阈值,当前这一步还要填写一次?」)—— 剧本从前给了一句原话示例 - * 「input_item 如『XL2596S-5.0 使能阈值』」,模型照抄示例、不读现值,把人刚填的 2.5 摆成缺口。 - * 提示词里唯一该出现的原话,是你想听到的那句;示例行一律不引具体料号 + 参数名。 - */ - /* - * 第三次同一病(2026-08-27):措辞改了两遍照样复现 —— 台账里每个参数都有值,模型还是写了一行 - * 「使能阈值 · 原厂未披露」。参数值有没有是台账里的事实,不该由模型在清单里宣布: - * S1 从此**一行参数都不写**,件上摆一张基座现读的「候选参数现值」表;说明书全文不再出现「使能阈值」。 - */ - it('S1 不写参数行(item_key 不许以 unknown_ 开头);说明书全文没有「使能阈值」这个可照抄的名字', () => { - const s1 = ASSESSMENT_STEPS.find((s) => s.item === 'S1_input_confirm')?.guide ?? ''; - expect(s1).toContain('候选参数一行都不写'); - expect(s1).toContain('不许以 unknown_ 开头'); - expect(s1).not.toContain('待确认'); - expect(NARRATIVE).not.toContain('使能阈值'); - for (const step of ASSESSMENT_STEPS) expect(step.guide, step.item).not.toContain('使能阈值'); - const skeleton = NARRATIVE.slice(NARRATIVE.indexOf('⑤ 执行工单 01'), NARRATIVE.indexOf('"standard":"每一条「待补」行')); - expect(skeleton).toContain('候选参数现值'); - expect(skeleton).not.toContain('待确认'); - }); - - it('S0 剧本写明都在库时数据工单两项本就不会进「当下可为」;S0d_p08_master 剧本写明 found 的一颗都不建', () => { - const s0 = ASSESSMENT_STEPS.find((s) => s.item === 'S0_bind')?.guide ?? ''; - expect(s0).toContain('当下可为'); - expect(s0).toMatch(/都在库/); - const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? ''; - expect(s0d).toContain('absent'); - expect(s0d).toMatch(/found" 的一行都不建/); - }); - - it('S0d_p08_master:查无记录一律派数据工单,不问人 —— 决策卡退役(2026-08-27 用户定:「只要有缺失就要自动派数据工单不需要人选择」)', () => { - const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? ''; - expect(s0d).toMatch(/一律建,不问人/); - expect(s0d).toMatch(/exists_status = "found" 的一行都不建/); - expect(s0d).not.toMatch(/发一张决策卡问人/); - expect(s0d).not.toMatch(/候选总数\s*≥\s*2/); - }); - - 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); - }); - - /* - * 2026-08-28 用户实测:模型把台账里原 PN 那一行的规格书(标题/版本/链接逐字)抄到了候选料号头上。 - * 那三格只可能从人那儿来 —— 声明归人填,模型 write 当场被拒。 - */ - it('P-10 的资料记录归人填:humanWrites 声明了 parts.document,writes 里不再有它,剧本也写明「你写不进去」', () => { - const p10 = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p10_document'); - expect(p10?.humanWrites).toEqual(['parts.document']); - expect(p10?.writes).not.toContain('parts.document'); - expect(p10?.guide).toMatch(/由人在件上填,你写不进去/); - expect(p10?.guide).toMatch(/别拿原 PN 或别的料号的规格书顶上/); - // 骨架仍由上一步铺(P-08 那步要写 parts.document) - const p08 = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master'); - expect(p08?.writes).toContain('parts.document'); - expect(p08?.humanWrites ?? []).toEqual([]); - }); - - it('S0d 拆成两项各自绑自己的 view(P-10 那份件不会永远推不出来)', () => { - 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'); - }); - - /* - * **两张数据工单的 doneWhen 只读入库事实**(2026-08-27 晚):引擎只在人答过卡之后求它 —— 值是人在件上填的、 - * 引擎数得出来,不再靠模型记得 finish(剧本写了两处它照样忘,件停在 P-08)。 - */ - it('P-08:要建的每颗料主数据与参数都落了、必填不缺 → 收得掉;少建一颗 / 还缺必填 / 参数行对不上账 → 收不掉', () => { - const ok = { parts_profile_rows: 2, [`${D}_pn_check_exists_status_absent`]: 2, [`${D}_pn_check_exists_status_created`]: 0, parts_profile_missing: 0, parts_parameter_missing: 0, parts_parameter_rows: 14, parts_parameter_distinct_param_name: 7 }; - expect(safeEval(doneOf('S0d_p08_master'), ok)).toBe(true); - // 模型把其中一颗改成 created 也一样算(absent + created 才是要建的料数) - expect(safeEval(doneOf('S0d_p08_master'), { ...ok, [`${D}_pn_check_exists_status_absent`]: 1, [`${D}_pn_check_exists_status_created`]: 1 })).toBe(true); - expect(safeEval(doneOf('S0d_p08_master'), { ...ok, parts_profile_rows: 1, parts_parameter_rows: 7 })).toBe(false); - expect(safeEval(doneOf('S0d_p08_master'), { ...ok, parts_profile_missing: 1 })).toBe(false); - expect(safeEval(doneOf('S0d_p08_master'), { ...ok, parts_parameter_rows: 13 })).toBe(false); - }); - - it('P-10:建过的每颗料都有一行资料记录且必填不缺 → 收得掉;资料少一行 / 骨架还空着 → 收不掉', () => { - const ok = { parts_document_rows: 2, parts_profile_rows: 2, parts_document_missing: 0 }; - expect(safeEval(doneOf('S0d_p10_document'), ok)).toBe(true); - expect(safeEval(doneOf('S0d_p10_document'), { ...ok, parts_document_rows: 1 })).toBe(false); - expect(safeEval(doneOf('S0d_p10_document'), { ...ok, parts_document_missing: 3 })).toBe(false); - }); - - it('S1 的入场哨兵不再等模型写 created 旗:入库事实齐了门就开', () => { - const cond = /[\s\S]*?([\s\S]*?)<\/condition>/.exec(CMMN_XML)?.[1] ?? ''; - expect(cond).not.toContain('sub_eval_pn_check_exists_status_absent = 0'); - expect(cond).toContain('sub_eval_pn_check_exists_status_absent + sub_eval_pn_check_exists_status_created'); - }); - - it('P-08 交回之后的收步不再靠模型记得 finish:剧本写明引擎按入库事实自己收,finish 只是省一拍(2026-08-27 晚:「顺序写死」两处模型照样忘)', () => { - const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? ''; - expect(s0d).toContain('finish S0d_p08_master'); - expect(s0d).toMatch(/引擎也会自己收/); - expect(s0d).not.toMatch(/顺序不能反/); - const s0d10 = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p10_document')?.guide ?? ''; - expect(s0d10).toMatch(/引擎按入库事实自己收/); - }); - - 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('三张执行工单各自声明 gateLedgers —— 零输入卡不声明,闸就盯着与它无关的表(2026-08-27 e2e 实测:两张数据工单空手交卷都放行了)', () => { - for (const [item, ledger] of [ - ['芯片主数据创建', '"gateLedgers":["parts.profile","parts.parameter"]'], - ['原厂技术资料创建', '"gateLedgers":["parts.document"]'], - ['评估输入与资料补齐确认', '"gateLedgers":["sub_eval.input_gap"]'], - ] as const) { - expect(NARRATIVE, item).toContain(ledger); - } - }); - - it('数据工单也要「先写骨架再发卡」写进说明书,且哨兵不认零行主数据(2026-08-27 e2e:模型压拍没写骨架就发卡,人打开是空表,零行同时骗过闸与哨兵,一路走到 S2 才被引用检查拦下)', () => { - // 两张数据工单各要一条(P-08 的 profile/parameter、P-10 的 document) - expect(NARRATIVE.match(/必须先有骨架行/g)?.length ?? 0).toBeGreaterThanOrEqual(2); - expect(NARRATIVE).toContain('parts.document` 必须先有骨架行'); - expect(NARRATIVE).toMatch(/压拍/); - // 标了几颗建好,主数据就得有几行 —— 逐颗对账(第一刀写成「≥1 行就算数」太松,三候选那单从缝里过去了) - expect(CMMN_XML).toContain('parts_profile_rows >= sub_eval_pn_check_exists_status_absent + sub_eval_pn_check_exists_status_created'); - }); - - it('评估输入那张卡的**前提**写在说明书的卡骨架上(2026-08-27 e2e:两颗缺库,模型只问只建了第一颗,CN 还 absent 就把 S1 的卡发了出去 —— 发卡的授权面是调人预算,绕得过哨兵)', () => { - expect(NARRATIVE).toContain('一颗 absent 都不剩'); - expect(NARRATIVE).toMatch(/有几颗 absent 就要办几颗/); - }); - - it('发数据录入卡那一拍不许顺手写需求行(客户还没落草稿层,引用检查必拒)', () => { - const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0_bind')?.guide ?? ''; - expect(g).toMatch(/那一拍只发卡/); - expect(g).toContain('引用检查'); - }); - - it('absent 没清零之前不许收掉数据工单那一项(2026-08-27 e2e:建完第一颗就 finish,再想给第二颗发卡撞上「这一步已经办完收掉了」,连拒四次)', () => { - const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? ''; - expect(g).toMatch(/还有没有 absent 的料号/); - expect(g).toMatch(/这一项千万别 finish/); - expect(g).toMatch(/收掉了就回不去了/); - }); - - it('参数名一律中文、与库里既有行同名(2026-08-27 e2e:模型写成 input_voltage_range,而原 PN 是「输入电压范围」—— 主键的一半对不上,27 条里 14 条判成 UNKNOWN)', () => { - const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? ''; - expect(g).toMatch(/参数名一律用中文/); - // 措辞写过两遍都没拦住 → 上结构:把「同一套名字」变成基座算得出的数 - expect(CMMN_XML).toContain('parts_parameter_rows = parts_parameter_distinct_param_name * (sub_eval_pn_check_exists_status_absent + sub_eval_pn_check_exists_status_created)'); - expect(g).toContain('input_voltage_range'); - expect(g).toMatch(/不是同一行/); - }); - - it('第二颗缺库的候选也要先问(每一颗都有被放弃的权利)', () => { - const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? ''; - expect(g).toMatch(/不问人,直接建/); - expect(g).not.toMatch(/这一颗也要先发一张决策卡问人/); - }); - - it('created 认的是「卡交回来了」,不是「表里有行」—— 写骨架 ≠ 建好了(2026-08-27 e2e:模型铺完骨架就标 created、直接 finish,撞「从没给它发过卡」连拒五拍)', () => { - const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? ''; - expect(g).toMatch(/写骨架 ≠ 建好了/); - expect(g).toMatch(/写骨架 → \*\*发卡\*\* → 这一拍结束 → 人交回/); - expect(g).not.toMatch(/只有它在 \\`parts\.profile\\` 里真有一行/); - }); - - /* - * SKIP(2026-08-29,Task 2 → Task 7 补回):`p08-master-create` 局面已随重写撤下(它的 - * 编辑态 CSS 现在无处可查)。共享的横滑规则(`.jc-tbl-wrap, .tbl-wrap { overflow-x: auto;`) - * 与中文表头那条(下面一条测试)仍读 `manifest.assets.viewTemplates` 整体、不点名具体局面, - * 继续钉着看板;这条要等 Task 7 重做数据工单件才谈得上继续钉编辑态细节。 - */ - it.skip('件上三处样式毛病钉死(2026-08-27 用户:表显示不全不能右滑 / 编辑态内部料号只读字与输入框并排 / input 与 select 没对齐)', () => { - const html = manifest.assets.viewTemplates.find((v) => v.name === 'p08-master-create')?.html ?? ''; - // 两种包装都要能横滑:验收件用目标件的 .tbl-wrap(它的验收版写着 overflow:hidden),2026-08-27 用户在结论审核页撞上 - // 运营侧的看板也在这条纪律里(它六张表都宽) - for (const v of manifest.assets.viewTemplates) expect(v.html, v.name).toContain('.jc-tbl-wrap, .tbl-wrap { overflow-x: auto;'); - expect(html).toContain('#tbl1.on td > .jc-vcell:not(.ro) { display: none; }'); - expect(html).toContain('input.jc-cell, select.jc-cell { height: 32px;'); - // 徽标不再包 .ro(编辑态要随只读格一起藏,只留下拉框) - expect(html).not.toContain(' { - for (const v of manifest.assets.viewTemplates) { - expect(v.html, v.name).not.toContain('class="thd"'); - for (const th of v.html.match(/]*>[^<]*<\/th>/g) ?? []) expect(th, `${v.name} th`).not.toMatch(/[a-z]_[a-z]/); - for (const td of v.html.match(/[^<]*<\/td>/g) ?? []) expect(td, `${v.name} 字段格`).not.toMatch(/[a-z]_[a-z]/); - } - }); - - it('审核通过与否读基座数 decision_last,不由模型自写的旗说了算;驳回分支明写不许写旗、不许 finish(2026-08-27 实测:人答「重做」它照样推到放行)', () => { - expect(CMMN_XML).toContain('decision_last = "approve"'); - expect(CMMN_XML).toContain(' s.item === 'S4_review')?.guide ?? ''; - expect(g).toMatch(/不许写 review_approved_total、不许 finish/); - expect(g).toMatch(/ask 的开头必须写明这一轮改了什么/); - }); - - 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()); - }); -}); - -/* - * 人工验收清单(2026-08-27 用户定):验收卡上的条目是人核这份工单要走的步骤,不是系统判的判据; - * 每份验收工单固定 —— 差异评估结论审核(S4_review 审核卡)与评审放行与落账确认(T_release 终局验收卡)各一份。 - */ -describe('两张验收工单的人工验收清单', () => { - it('S4_review 与 T_release 各声明一份固定清单:至少 4 条,每条是一句可核对的动作,互不重复', () => { - for (const item of ['S4_review', 'T_release']) { - const list = ASSESSMENT_STEPS.find((s) => s.item === item)?.acceptanceChecklist ?? []; - expect(list.length, item).toBeGreaterThanOrEqual(4); - expect(new Set(list).size, item).toBe(list.length); - for (const line of list) expect(line.length, `${item}: ${line}`).toBeGreaterThan(12); - } - // 别的步骤不发验收卡,不该有清单 - for (const step of ASSESSMENT_STEPS) { - if (step.item === 'S4_review' || step.item === 'T_release') continue; - expect(step.acceptanceChecklist, step.item).toBeUndefined(); + expect(step.tools, step.item).toEqual([]); } }); }); - -/* - * 评估输入清单每行六列都要写(2026-08-27 实测:模型 11 行全漏了必填列 input_item,件上红带点名「缺输入项」, - * 人把别的格填满也过不了闸;而剧本从前只在括号里提过一次 input_item,逐行的做法里没再说)。 - */ -describe('S1 清单:每行六列都要写', () => { - it('剧本逐行写明 input_item 的人读名与 source_ledger,并让模型发卡前核 _missing = 0', () => { - const s1 = ASSESSMENT_STEPS.find((s) => s.item === 'S1_input_confirm')?.guide ?? ''; - expect(s1).toContain('每一行六列都要写'); - for (const name of ['customer「客户」', 'request「客户需求」', 'original_pn「原 PN」', 'application_scenario「应用场景」', 'bom_position「BOM 位」', 'original_doc_version「原 PN 资料版本」']) { - expect(s1, name).toContain(name); - } - expect(s1).toContain(`${D}_input_gap_missing`); - }); -}); - -/* 放行那一步只 submit,不发工单(2026-08-27 实测:先一张执行单「评审放行与落账确认」,人答完才 submit 又一张验收单) */ -describe('T_release 只 submit', () => { - it('剧本写明不发执行工单 / 决策卡', () => { - const t = ASSESSMENT_STEPS.find((s) => s.item === 'T_release')?.guide ?? ''; - expect(t).toContain('不发执行工单'); - expect(t).toContain('submit'); - }); -}); diff --git a/activities/part-substitution-assessment/src/case-model.ts b/activities/part-substitution-assessment/src/case-model.ts index 5482854..d3751b7 100644 --- a/activities/part-substitution-assessment/src/case-model.ts +++ b/activities/part-substitution-assessment/src/case-model.ts @@ -1,688 +1,191 @@ /** * 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台账字段细化与统一方案》(字段口径)。 + * **2026-08-29 整体重写(Task 3,spec `docs/superpowers/specs/2026-08-29-act-work-05-rewrite-design.md` §4)**: + * 九步砍成四步 —— `S1_inputs`(校验入参与补数据)→ `S2_assess`(逐项差异分析)→ + * `S3_review`(结论审核,人工激活)→ `T_release`(落账放行,人工激活)。三道入场哨兵全读基座数, + * `caseFileModel` 里**一面记账旗都没有**——旗是活动自己写的数,「机检只读基座数」这条纪律在本仓 + * 被违反过六次,每次都是同一种事故(见 `docs/ACT-WORK-05-端到端测试记录.md` A–K 发现表)。 * - * 文档六步收成 9 个案例模型项(0.5 那一步因 view 各不相同拆成两项 —— 引擎里剧本声明的 - * view 恒压过模型当拍报的 say.view,一个计划项只能绑一份画面,P-08 与 P-10 各自要一份可编辑的 - * 工作区件,不能共用同一个 view): - * 启动期 → S0_bind(AI:铸案号、建 P-01 V1、锁规则版本、逐个 PN 查库、写存在性核对表) - * 0.5a 等待数据(建主数据)→ S0d_p08_master(人:数据工单 P-08,件上表 1 主记录 + 表 2 标准化 - * 参数子表 `parts.parameter`;候选总数 ≥2 时缺库先决策卡; - * 都在库时哨兵不开,这一项与下一项都不会进「当下可为」) - * 0.5b 等待数据(建资料)→ S0d_p10_document(人:数据工单 P-10;都在库时同样不会进「当下可为」) - * 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,终局放行) + * **全程零 `guide`**:四步的行为改由 `PlanStep.auto` 声明式配方接管(ADR-014)—— 这一份只搭骨架 + * (每步 `auto` 先各放一笔 `say` 占位),配方的实体在 Task 4–6 逐步填进去;`view` 这一步不写 + * (它是可选的,Task 7–9 各自补上自己那一张)。 * - * 五处不给 doneWhen:S0d_p08_master / S0d_p10_document(触达人)、S1(交人补缺口)、 - * S4(拍板且可循环)、T_release(签下去)。 + * 触达世界的两步(`S3_review` 审核、`T_release` 放行)标了人工激活且**不给 `doneWhen`**—— + * 完成与否不该由数据形状推出来。`ASSESSMENT_STEPS` / `ASSESSMENT_PHASES` 不用 `as const` + * (与 E04 / I01 同款):它会把 `writes`/`reads` 这类数组字面量收窄成只读元组, + * 装配进 `manifest.ts` 的 `businessModel.steps`(类型是可变的 `PlanStep[]`)时会因为 + * `ReadonlyArray` 缺 `push` 等方法而过不了类型检查。 */ -/* - * `TOOL_COMPARE` / `TOOL_RECOMMEND` 退役(见 constants.ts 头注)—— 下面几处原本 `${TOOL_COMPARE}` - * / `${TOOL_RECOMMEND}` 的插值已就地换成同样的字面量,文字与结构一字不改。Task 3 整体重写本文件 - * (四步、零工具)时会把这些引用连同工具调用一起撤掉,这里只是维持包能编译的最小机械改动。 - */ -import { D, RULE_VERSION } from './constants.ts'; +import { RULE_VERSION } from './constants.ts'; export const CMMN_XML = ` - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - + + + + - - - update - demand_request_rows > 0 and sub_eval_pn_check_rows > 0 and sub_eval_pn_check_exists_status_absent > 0 + + + update + sub_eval_part_exists_status_found = 2 and sub_eval_part_missing = 0 and sub_eval_part_distinct_chip_type_id = 1 and parts_profile_missing = 0 and parts_parameter_missing = 0 - - - update - parts_profile_rows > 0 and parts_profile_missing = 0 and parts_parameter_missing = 0 - - - - update - - - sub_eval_pn_check_rows = count(candidateInternalPns) + 1 and sub_eval_pn_check_missing = 0 and sub_eval_pn_check_exists_status_abandoned < count(candidateInternalPns) and parts_profile_missing = 0 and parts_parameter_missing = 0 and parts_document_missing = 0 and parts_profile_rows >= sub_eval_pn_check_exists_status_absent + sub_eval_pn_check_exists_status_created and parts_document_rows >= sub_eval_pn_check_exists_status_absent + sub_eval_pn_check_exists_status_created and parts_parameter_rows = parts_parameter_distinct_param_name * (sub_eval_pn_check_exists_status_absent + sub_eval_pn_check_exists_status_created) - - - - 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 + substitution_difference_item_rows > 0 and substitution_difference_item_missing = 0 and substitution_relation_rows = 1 and validation_record_rows = 1 - - - update - - review_approved_total > 0 and decision_last = "approve" 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 + + + update + substitution_relation_rows = 1 and substitution_relation_missing = 0 and validation_record_missing = 0 - - + + - - - - + + + - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + `; /** - * 案例说明书 —— **只写整单一以贯之的事**(逐步做法归剧本)。 - * 卡骨架住在这里而不是剧本里(模型会压拍,压过之后只有说明书还在 —— A1/A6/I01 同一课)。 + * 案例说明书 —— **只写整单一以贯之的事**,不讲逐步做法(逐步做法归配方,而配方不是给模型看的, + * 是引擎按声明确定性执行的数据)。四步全部 `guide: ''`,这份说明书理论上不会再被任何一拍摊开给 + * 模型看 —— 留着它是因为 `narrative` 契约仍要求非空,且它仍是给人读的活动说明。 + * + * **示例一律占位形**(`<料号>` / `<参数名>`),不写具体料号与参数名 —— 本仓 2026-08-27 复现过三次: + * 关思考的模型会把示例里的具体值当成这一单的值照抄。 */ -export const NARRATIVE = `§0 开案:**判断候选料号在技术上能不能替代原料号,逐参数给出可复核的差异结论,审核后落账**。 -入参: -· \`customerCode\` —— 客户编码(L-02);\`customerName\` 只在客户不在库时给。 -· \`requestDescription\` —— 客户需求原话,据它新建 P-01 需求主记录(版本 V1)。 -· \`originalInternalPn\` —— 原内部料号;\`candidateInternalPns\` —— 候选内部料号数组(≥1,逐个评估、各一条 P-11)。 -· \`datasheets\` —— 选填的真文件,基座已读成文本(每件 \`{name, text?, note}\`);有就拿它在数据工单里预填,没有就人全填。 +export const NARRATIVE = `§0 本活动只回答一件事:**候选内部料号在技术上能不能替代原内部料号**——逐条核心参数给出可复核的 +差异结论,人审核通过后落账。不做数量 / 价格 / 交期 / 采购判断;客户批准状态由独立审批流维护, +本活动没有那一列的写权。 -**本活动只回答一件事:技术上哪种料能替**。不做数量 / 价格 / 交期 / 采购判断;客户批准状态由审批流维护,你没有那一列的写权。 +入参两格,都选填:原内部料号、候选内部料号(如 <料号>)——库里有就用,没有就自己写一个新的, +启动后由第一步核对补齐,不必人先建好档。 -**四项绑定后不可改**(客户 / 需求 / 原 PN / 候选数组):绑错只能退回启动人重新启动。 +**四步各自的产出**: +① 校验入参与补数据(S1_inputs)—— 核对两颗料在不在库、是否归了同一个芯片类型、核心参数 + (如 <参数名>)是否齐备;缺什么就派数据工单让人补。产出:两颗料的存在性与类型核对、 + 补齐的芯片主数据与标准化参数。 +② 逐项差异分析(S2_assess)—— 把两颗料的核心参数逐条比对,判定 MATCH / MISMATCH / UNKNOWN / + CONDITIONAL 四态,依规则算出推荐等级与风险。产出:替代关系一行 + 差异明细逐参数一行 + + 验证记录一行(全推荐也要写「本轮无待验项」)。规则版本 ${RULE_VERSION} 随结论一并锁定。 +③ 结论审核(S3_review,人工激活)—— 交人核对:参数比对对不对、硬约束有没有被含糊带过、 + UNKNOWN 有没有去向、判断依据站不站得住;可就地改四态与影响说明,改到没问题为止。 +④ 落账放行(T_release,人工激活)—— 把这一签下去会落哪几张表、几行摆清楚,人签字后正式落账, + 发射 assessment-released。 -**能算的不问人也不自己判**:四态(MATCH / MISMATCH / UNKNOWN / CONDITIONAL)与推荐等级 -(推荐 / 条件推荐 / 备选 / 不推荐)全由两枚工具算 —— \`part-substitution-assessment.compare-parameters\`、\`part-substitution-assessment.recommend\`, -一批一次。你负责对照表、证据引用、影响说明、UNKNOWN 的去向。**规则版本 \`${RULE_VERSION}\` 随每条结论落账**。 - -**UNKNOWN 是一等公民**:查不到的参数值写「未披露」,不许估、不许留空串冒充;每个 UNKNOWN 必须写去向 -(挂验证任务号或补来源动作)。硬约束 MISMATCH 不许改判,要改只能补证据后重比。 - -**存在性检查与数据工单**:启动时逐个料号在 \`parts.profile\` 里查(原 PN + 每个候选)。查无记录 = 「完全没有」, -要人从零建整条主数据(表 1 主记录 + 表 2 标准化参数子表 \`parts.parameter\`)—— 发**数据工单**(一张单:P-08 一张、P-10 一张); -PN 在库但个别字段缺 = 「有而不全」,归执行工单 01 补。**查无记录的一律派数据工单,不问人**(2026-08-27 用户定: -「只要有缺失就要自动派数据工单,不需要人选择」)—— 原 PN 缺是硬阻塞、候选缺同样直接建,**没有「放弃这颗候选」这个岔路口**。 -**库里都有的话这两项的入场前提本就不成立**(此单不走这条),不会出现在你的「当下可为」里,别发空单。 - -**六步走**:绑定与存在性核对 → (等待数据)→ 人补评估输入 → 标准化对照表 → 逐项评估 → 人审结论(可循环)→ 拟落账 → 人放行。 - -【本单会用到的卡,全部照下面的骨架发,别自己另编】 - -① 新客户建档(只在 \`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":"区域"} - ], - "basis":[{"k":"触发原因","v":"按客户编码 查客户档案,查无记录"}, - {"k":"这一单要评估的","v":"原 PN → 个候选"}, - {"k":"确认之后","v":"这家客户落进本单草稿层,评估照常往下走"}]} -**\`basis\` 这三行必须给**:不给的话基座会兜上「这一步 / 当下可为 / 目标缺口」那几行内部家具 —— -把九个计划项的全名摆到人脸上,他一个字也用不上(2026-08-27 实测)。 -人确认那一下基座已把这一行落进草稿层,**你不许再 write 一遍 customer.profile**。 - -② (已退役)候选缺库不再问人 —— 2026-08-27 用户定「只要有缺失就自动派数据工单」;所有查无记录的料号直接进③那张卡。 - -③ 数据工单 · P-08(所有要建的料号一起,一张单;零输入,填报全在工作区的件上)—— -**发这张卡之前,\`parts.profile\` 与 \`parts.parameter\` 必须先有骨架行**(名单里每颗料各一行 + 它的参数各一行)。 -不管这一拍是不是顺手把上一步收掉的(压拍常发生,那时这一步的剧本还没摊开),写骨架与发卡都得在同一串里,顺序不能反 —— -件上一行都没有的话,人打开是一张空表、什么也补不了,而这一单的主数据从头到尾不会存在 -(实测走到 S2 才被引用检查拦下,那时两张数据工单都已经「办完」了) -{"kind":"human","itemId":"S0d_p08_master","workOrder":"execution","card":{"variant":"data", - "title":"数据工单 · 芯片主数据创建(P-08):<料号列表,顿号分隔>", - "ask":"这些料号在芯片主数据里查无记录,请在工作区的件上把整条记录建齐(一颗一行,可加行)。", - "steps":["表 1 每一行:品牌、规格描述、封装、生命周期状态为必填,缺一格确认不了", - "表 2 一行一个参数:所属料号、参数名、值、单位、约束(硬/软)必填,说明是只读注释;原厂未披露的值写「未披露」,它会进入 UNKNOWN 并挂验证任务", - "有 datasheet 的把引用与版本一起填(成对);制造商与品牌不一致时分开写", - "改完保存,回这张卡确认 —— 确认后这批主数据即入库,活动从等待数据唤醒"], - "standard":"每颗料号表 1 一行,必填列全齐;表 2 至少含输入电压范围 / 输出电压 / 输出电流 / 工作温度 / 封装相关的硬约束项各一行。", - "gateLedgers":["parts.profile","parts.parameter"], - "basis":[{"k":"触发原因","v":"按 internal_pn 查 P-08 查无记录(完全没有,不是字段缺失)"}, - {"k":"AI 预填","v":"<有 datasheet 就写「已按 datasheet 预填 N 项,请核对」,没有写「未上传 datasheet,请全填」>"}]}} -**\`gateLedgers\` 必须给**:这是零输入卡,基座不知道这一张要人补哪几张表 —— -不给它就回落到必交付物那几张(与这张卡无关),于是件上红带点着「缺品牌名称」而提交钮照样绿, -空手交卷一路放行(2026-08-27 实测)。给了之后,闸与红带、放行门读的是同一个数。 - -④ 数据工单 · P-10(同一批料号的资料记录,一张单;在 P-08 那张确认之后再发)—— -**与③同一条:发这张卡之前 \`parts.document\` 必须先有骨架行**(名单里每颗料至少一行, -key 是 {internal_pn, doc_seq:"01"});一行都没有的话人打开是空表、什么也补不了, -提交闸会如实拦住(「这一单还一行都没有」),这一单就卡在这儿了(2026-08-27 实测)。 -{"kind":"human","itemId":"S0d_p10_document","workOrder":"execution","card":{"variant":"data", - "title":"数据工单 · 原厂技术资料创建(P-10):<料号列表>", - "ask":"请把这些料号的 datasheet / 承诺函等资料记录补进原厂技术资料库(一份资料一行,可加行)。", - "steps":["资料类型、标题、版本、文件链接、资料状态为必填;证据定位写页码或章节,差异评估的证据引用会指向它", - "同一料号多份资料就多加几行,序号 01/02… 自己续", - "改完保存,回这张卡确认 —— 确认后这批资料记录即入库,活动接着往下办"], - "standard":"每颗料号至少一行 datasheet 记录,必填列全齐。", - "gateLedgers":["parts.document"], - "basis":[{"k":"触发原因","v":"按 internal_pn 查 P-10 查无记录"}]}} - -⑤ 执行工单 01 · 评估输入与资料补齐确认(S1_input_confirm;零输入)—— -**发它的前提:所有料号都已落定** —— \`${D}.pn_check\` 里**一颗 absent 都不剩**(每颗要么 found、要么经数据工单 created、 -要么人点了放弃 abandoned),且建过的那几颗在 \`parts.profile\` 里真有行。还有 absent 就**回去把它们办完** -(候选总数 ≥2 的逐颗发决策卡问、原 PN 与独苗候选直接建),别先发这张卡: -硬发出去人也补不成 —— 后面写替代关系时会被引用检查逐颗拦下,而那时数据工单都已经「办完」了(2026-08-27 实测两轮)。 -**有几颗 absent 就要办几颗**,办完第一颗别忘了还有第二颗。 -**发这张卡之前,\`${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":{ - "gateLedgers":["${D}.input_gap"], - "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\` 口。`; +**三条不许**: +· 不伪造参数值——查不到就显式写「未披露」,不许估、不许拿别的料号或别的参数的值顶替; +· 硬约束 MISMATCH 不许被措辞改判成 MATCH,要改只能补证据后重比; +· 不写客户批准状态——那是独立审批流的事,本活动只答技术可替代性。`; /** - * 步骤剧本 —— 一个计划项一条;`doneWhen` 是「成果已存在」的判据,不是「该不该做」。 - * 五处不给 doneWhen:S0d_p08_master / S0d_p10_document(发数据工单触达人)、S1(交人补缺口)、 - * S4(拍板且循环)、T_release(签下去)。 + * 步骤剧本 —— 一个计划项一条。四步**全程零 `guide`、零工具**:行为改由 `auto` 配方接管 + * (ADR-014),这一份只放骨架 —— 每步 `auto.do` 先各摆一笔 `say` 占位,配方的实体 + * (`project` / `analyze` / `card` / `submit`)在 Task 4–6 逐步填进去。 + * + * 两处不给 `doneWhen`:`S3_review`(人拍板且可循环)、`T_release`(签下去)—— + * 判据不许摆在还欠一张卡的那一步眼前(会诱使它去凑判据,2026-08-28 用户实测逼出的立墙)。 */ 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\`** —— 需求行的客户编码有引用检查(客户还没落草稿层,写下去当场被拒); -等人确认完,下一拍再写需求行与核对表,一次写齐。 - -【写 \`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" **且 \`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(数据工单),照常往下办。 -**都在库的话你什么都不用做** —— 数据工单那两项的入场前提本就不成立,它们不会出现在你的「当下可为」里, -下一拍你会直接看到「评估输入与资料补齐确认」那一步。**任何时候都不要对计划项用 \`disable\`**: -本引擎里 \`Disabled\` 只能从 \`Enabled\` 来,而普通计划项一直停在 \`Available\`,disable 一定被拒。`, - }, - { - item: 'S0d_p08_master', - /* 这一步派的是数据工单 —— 由剧本声明,模型抄不抄 variant 都一样(2026-08-27 实测它丢了) */ - cardVariant: 'data' as const, - intent: '料号不在库:派 P-08 数据工单让人从零建芯片主数据 + 标准化参数子表 —— 查无记录一律派,不问人', - writes: ['parts.profile', 'parts.parameter', 'parts.document', `${D}.pn_check`], - reads: [`${D}.pn_check`, 'candidateInternalPns', 'originalInternalPn', 'datasheets', 'parts.profile', 'parts.parameter', 'parts.document'], - view: 'p08-master-create', - tools: [], - /* - * 人工激活:发卡就算启动。doneWhen 只在**人答过卡之后**才由引擎求(2026-08-27 晚,harness 立此规矩): - * 要建的每一颗(absent + created)主数据与参数都落了、必填不缺 —— 值是人在件上填的,引擎数得出来, - * 不再靠模型记得 finish(剧本写了两处它照样忘,两步同时 Active、件停在 P-08)。名单里还有没建的料时它为假, - * 「再发一张 P-08」那条路照旧走得通。 - */ - doneWhen: `parts_profile_rows > 0 and parts_profile_rows >= ${D}_pn_check_exists_status_absent + ${D}_pn_check_exists_status_created and parts_profile_missing = 0 and parts_parameter_missing = 0 and parts_parameter_rows = parts_parameter_distinct_param_name * parts_profile_rows`, - guide: `这一项只在 \`${D}.pn_check\` 里有 absent 行时走 —— 都在库时入场前提本就不成立,这一项与下一项 S0d_p10_document 都不会进你的「当下可为」。**先分岔,再动作**: - -【名单】= \`${D}.pn_check\` 里 **exists_status = "absent" 的每一行**,不管它是原 PN 还是候选 —— **一律建,不问人** -(2026-08-27 用户定:只要有缺失就自动派数据工单;从前那张「派数据工单还是放弃这颗候选」的决策卡已退役,别再发)。 -**exists_status = "found" 的一行都不建** —— 那几颗的主数据本来就在库里,给它们派数据工单等于让人把已有的东西再填一遍, -而真正缺的那颗还是没人建、单子照样卡死。**这三张表在本单只建不改、且只建核对表里核过的料号**:在库的料号写下来(哪怕只是一行空骨架)会被当场拒, -凭空拼出来的料号(2026-08-27 实测:模型写了一颗不存在的 XL2596S-ADJ,把两个候选的名字拼在了一起)也会被当场拒; -真写了就用 drop 撤掉那一行,别留在件上让人去填。**名单只有一个来源:pn_check 里 absent 的那几行,一个字都别改。**名单为空(一行 absent 都没有)这一项根本不会出现在你的「当下可为」里。 -**名单里的料号全放进同一张 P-08 卡**(标题列全料号,件上一颗一行,参数各归各的料号)—— 别一颗一张。 - -【「建」的名单定了 → 写骨架行,发 P-08 数据工单(一张单,件上两张表)】 -1. 给名单里每颗料写一行 \`parts.profile\` 骨架(表 1):key {internal_pn},patch 只带你确有依据的列 —— 有 \`datasheets[].text\` 就从里面读 - brand_name / specification_description / package / lifecycle_status(active/nrnd/eol/discontinued/risk 之一)/ datasheet_ref + datasheet_version; - 没有 datasheet 就只写 internal_pn 这一列(其余留给人填,**别编**)。 -2. 同一拍给每颗料写标准化参数集骨架(表 2,\`parts.parameter\`,一行一参数): - **参数名一律用中文,与库里既有料号的那几行同名**(输入电压范围 / 输出电压 / 输出电流 / 开关频率 / - 静态电流 / 工作温度…)—— \`param_name\` 是主键的一半,写成 \`input_voltage_range\` - 与写成「输入电压范围」**不是同一行**:原 PN 那边是中文、候选这边是英文,比对时两套名字对不上, - 逐条判成 UNKNOWN,结论就废了(2026-08-27 实测:27 条里 14 条 UNKNOWN)。 - 封装、工艺、生命周期这些**不是参数**,它们在表 1,别往表 2 里塞。 - **建几颗料就用同一套名字**:门上有一条对账 —— 参数行数 = 不同参数名数 × 建过的料数, - 只有每颗料的名字集合一模一样时才成立;混着写(这颗中文那颗英文、或同一颗写两套)这一步就过不去。有 \`datasheets[].text\` 就从里面读出典型参数 - (如输入电压范围 / 输出电压 / 输出电流 / 开关频率 / 静态电流 / 工作温度等,按品类而定)各写一行, - key {internal_pn, param_name},patch 带 param_value / param_unit / constraint_type(hard/soft)/ param_note(一句话说清这项为什么要卡, - **param_value 的写法**:范围写「下限~上限」(如 1~10、-20~+60 —— 这表示一整段,比对工具按范围读),单位放 param_unit 不写进值里, - 定值只写一个数; - 如「须覆盖客户输入上限」);原厂没写的值写「未披露」(它会进入 UNKNOWN 并挂验证任务)。没有 datasheet 就只写 - internal_pn + param_name 骨架(把你判断这颗料该评估的参数名列出来,**param_value 留空给人填,别编**)。 -3. 同一拍**给名单里每一颗料各写一行** \`parts.document\` 骨架(两颗就两行,别只写一行 —— - 门上有一条对账:建好几颗就得有几行资料记录;少一行,那颗料的资料工单人打开是空的):key {internal_pn, doc_seq: "01"},patch {document_type: "datasheet", document_status: "pending", - brand_name 有就写}(这一行是骨架,下一项 S0d_p10_document 会把它填成正式记录)。 -4. 同一拍发说明书③那张 P-08 数据工单(title 里列全料号),发完这一拍结束(挂起)。 - -**\`created\` 认的是「那张卡交回来了」,不是「表里有行」** —— **写骨架 ≠ 建好了**: -骨架是**你**铺的(只有料号和参数名),值是**人**在件上填的。骨架一铺就把 pn_check 改成 created, -等于替人宣布他已经办完了:接着你会以为数据工单收工、去 finish 这一项,而卡根本没发出去 —— -撞上「这一单里从没给它发过卡」,连撞五拍熔断(2026-08-27 实测,整单白跑)。 -**顺序只有一个**:写骨架 → **发卡** → 这一拍结束 → 人交回 → 这时才把那几颗改成 created。 -门上还有一条逐颗对账(建好几颗就得有几行主数据、几行资料),标了却没建,这一步照样过不去。 - -【P-08 那张确认回来了】**先看名单里还有没有 absent 的料号**(\`${D}.pn_check\` 现读): -· **还有** → **这一项千万别 finish**。把还 absent 的那几颗的骨架写好(profile + parameter,有 datasheet 就预填), - 再发一张 P-08 卡(标题列上它们的料号)—— 不问人,直接建;同一个计划项可以先后发好几张卡。 - **收掉了就回不去了**:这一项一旦 Completed,再想给它挂第二张卡会被拒「这一步已经办完收掉了」, - 而那颗料从此没人建,后面写替代关系时被引用检查逐颗拦下(2026-08-27 实测,一单连拒四次)。 -· **一颗都不剩了** → 才走下面这两步。 - -这一拍的动作串:① \`finish S0d_p08_master\`(建好的料主数据与参数都已入库、必填不缺时引擎也会自己收这一项 —— -你 finish 一下更省一拍,忘了也不会卡住);② 照说明书④发 P-10 数据工单 —— 下一项是 S0d_p10_document,同一批料号接着补资料记录。 - -【这张卡一单只发一次】人交回来之后不许再发;答复里没有值(空 values)也算答过 —— 人在件上填的东西已随确认**入了正式表** -(轨迹里有一条「数据工单确认入库」的痕点名哪几张表各几行;试跑单不落主数据时那条痕会如实写「只留在活动缓存」)。 -这几颗料从此在库:**别再往 \`parts.profile\` / \`parts.parameter\` 写它们的键**(在库的键会被当场拒),也别去 drop 它们 -(那是本单自己建好的行,撤不掉);值要改由人在件上改。`, - }, - { - item: 'S0d_p10_document', - /* 这一步派的是数据工单 —— 由剧本声明,模型抄不抄 variant 都一样(2026-08-27 实测它丢了) */ - cardVariant: 'data' as const, - intent: '料号不在库:派 P-10 数据工单让人从零建原厂技术资料', - /* parts.document 归人填(见下面 humanWrites)—— 这一步只改核对表的旗 */ - writes: [`${D}.pn_check`], - reads: [`${D}.pn_check`, 'parts.profile', 'parts.document'], - view: 'p10-document-create', - tools: [], - /* - * **资料记录归人填**(2026-08-28 用户实测):标题 / 版本 / 链接只可能从人那儿来 —— - * 模型手边唯一「像样」的资料就是台账里原 PN 那一行,实测它逐字抄了过来安在候选头上, - * 而这份假证据会直接喂进差异比对当「硬约束有证据支持」。骨架(键 + 类型 + 状态) - * 上一步 S0d_p08_master 已经铺好,这一步不需要写它;写就当场拒,drop 仍可撤。 - */ - humanWrites: ['parts.document'], - /* 人工激活:发卡就算启动。doneWhen 同 P-08 的道理:人答过卡之后,建过的每颗料都有一行资料记录且必填不缺 → 引擎自己收 */ - doneWhen: `parts_document_rows > 0 and parts_document_rows >= parts_profile_rows and parts_document_missing = 0`, - guide: `这一项只在 \`${D}.pn_check\` 里有 absent 行时走(要走到这一步,S0d_p08_master 已经收完 —— 引擎按入库事实自己收);都在库时入场前提本就不成立, -这一项与 S0d_p08_master 都不会进你的「当下可为」。 - -**资料记录那三格(标题 / 版本 / 文件链接)由人在件上填,你写不进去**(写了当场拒)—— -别拿原 PN 或别的料号的规格书顶上:那是另一颗料的资料,后面会被当成这颗料的证据用。骨架上一步已经铺好了。 - -【发 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)也算答过 —— 人在件上填的东西已随确认**入了正式表** -(轨迹里有一条「数据工单确认入库」的痕)。这几份资料记录从此在库:**别再往 \`parts.document\` 写它们的键**(会被当场拒),也别去 drop 它们。`, - }, - { - 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.parameter', 'parts.document', 'candidateInternalPns', 'originalInternalPn'], - view: 'eval-input-list', - tools: [], - /* 交人补缺口,不给 doneWhen;人工激活:发卡就算启动 */ - guide: `料号全部落定了,**该 PW 补评估输入了** —— 这是本活动唯一的执行工单,零输入。**先分岔**:【当前这一步】那行引擎事实说 -「从没给它发过卡」→ 走【发卡】;说「人已经被问过了」→ 走【收口】。 - -【发卡】先把评估输入清单写下来(\`${D}.input_gap\`,一行一项),一拍连做。 -**每一行六列都要写,一列都不能少**:item_key(英文短键)/ input_item(人读名,就是下面每行「」里那几个字)/ source_ledger(来源台账)/ -status / detail / origin = "ai"。input_item 是必填列 —— 漏了它,件上红带会点名「缺输入项」,人对着一张没有名字的清单什么也补不了, -卡就此交不回来(2026-08-27 实测:11 行全漏了 input_item,人把别的格填满也过不了闸)。 -· 已绑定四项(status = "ok",detail 写绑定值):item_key = customer「客户」(source_ledger = "L-02")/ request「客户需求」(source_ledger = "P-01")/ - original_pn「原 PN」(source_ledger = "P-08")/ 候选多颗就 candidate_1「候选 1」、candidate_2「候选 2」… 一颗一行(source_ledger = "P-08",detail 写料号与在库状态); -· 待补两项(status = "gap",detail 写「启动时未收」):application_scenario「应用场景」(source_ledger = "P-01")、bom_position「BOM 位」(source_ledger = "P-09"); -· 默认可改三项(status = "default",detail 写默认值,source_ledger = "GOV"):substitution_type「替代类型」= "Pin-to-Pin"、rule_version「规则版本」= "${RULE_VERSION}"、owner「负责人 / 期限」= "按客户线默认 · 1 个工作日"; -· **候选参数一行都不写**:原厂未披露的参数不是这份清单的事 —— 差异评估那一步(S3)会把它判成 UNKNOWN 并挂验证任务; - 件上另有一张只读的「候选参数现值」表(P-08 标准化参数集,一颗料一张,基座从台账现读),人看得见每颗料每个参数此刻的值。 - 这份清单里**不许出现任何参数行**(item_key 不许以 unknown_ 开头)—— 参数值有没有,是台账里的事实,不由你在这里替它宣布 - (2026-08-27 实测三次:人在 P-08 填好的参数被这份清单当成缺口再问一遍); -· 齐备项(status = "ok"):original_doc_version「原 PN 资料版本」(source_ledger = "P-10",detail 写 datasheet 版本)。 -发卡前自己核一遍:\`${D}_input_gap_missing\` 这个读数必须是 0(它只该等 supplement 那一列由人来填;不是 0 就是你漏写了必填列,回去补写再发)。 -写完同一拍照说明书⑤发执行工单,发完这一拍结束(挂起)。 - -【收口】人交回之后只做三件事,然后 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.parameter', 'parts.document', 'demand.request', `${D}.pn_check`, `${D}.input_gap`, `${D}.verdict`, 'candidateInternalPns', 'originalInternalPn'], - view: 'difference-review', + item: 'S1_inputs', + intent: '核对两颗料在不在库、归没归同一个类型、参数值齐不齐;缺什么就派一张数据工单让人补', + guide: '', + writes: ['sub_eval.part', 'sub_eval.case', 'parts.profile', 'parts.parameter'], + reads: ['originalInternalPn', 'candidateInternalPn', 'case:openedAt', 'parts.profile', 'parts.parameter', 'parts.chip_type', 'parts.type_param'], 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.parameter\` 里的那几行**(它是独立子表,一行一参数, -按 internal_pn 归属):原 PN 有的每个参数一行 —— difference_item_id = "<关系id>-P01/P02…"、parameter_name(取原 PN 那一行的 param_name)、 -original_value(取原 PN 那一行的 param_value)、candidate_value(候选在 \`parts.parameter\` 里同 param_name 那一行的 param_value; -候选没有这个参数 → 写「未披露」)、parameter_unit(以原 PN 那一行的 param_unit 为准;候选单位不同先换算, -换算不了原值照写、evidence_ref 里注明)、constraint_type(取原 PN 那一行的 constraint_type;没标的按口径:封装 / 工艺 / 电压 / 电流 / 温度 / 阈值 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 **这一步不写**。 - -**缺参数不得伪造**:候选那一侧查不到就写「未披露」。写完引擎自己收这一项(每条关系都有明细行才算)。`, + 'sub_eval_part_exists_status_found = 2 and sub_eval_part_missing = 0 and ' + + 'sub_eval_part_distinct_chip_type_id = 1 and parts_profile_missing = 0 and parts_parameter_missing = 0', + auto: { do: [{ kind: 'say' as const, text: '正在核对两颗料的在库状态与参数齐备情况。' }] }, }, { - 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: ['part-substitution-assessment.compare-parameters', 'part-substitution-assessment.recommend'], + item: 'S2_assess', + intent: '把两颗料的核心参数逐条交给模型判四态,系统按规则算推荐等级、风险与验证要求', + guide: '', + writes: ['substitution.difference_item', 'substitution.relation', 'validation.record'], + reads: ['parts.type_param', 'parts.parameter', 'sub_eval.part', 'sub_eval.case'], + tools: [], 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: `对照表铺好了,**固定三拍**:比对 → 定级 → 写盘。**工具调用后同拍其余动作作废**,别把工具和写台账串在一起;两枚工具各只调一次。 - -【第一拍:只调 \`part-substitution-assessment.compare-parameters\`,别的什么都不写】**只报料号与客户约束,一个参数值都不抄** —— 参数、封装、生命周期、证据引用 -由工具自己从台账现读(parts.parameter / parts.profile / parts.document,本单刚建的也算),每一对都是台账原值: -{"originalInternalPn":"<原 PN>","candidates":[{"candidateInternalPn":"<候选>","requirements":{"<参数名>":"<约束>"}}, …]} -· 全部候选一次打包; -· **requirements 只写客户在需求 / 应用场景里给了边界的参数**(如客户说输入上限是某个电压 → 输入电压范围写 ">= <那个数>"; - 说工业温域 → 工作温度写 "-40~85");客户没给边界的参数**不写**,不要替客户编约束; -· evidenceRef 不用给(工具从候选的 P-10 资料记录取);资料记录真没有时硬约束会退成 CONDITIONAL —— 那正是要的。 -调完这一拍结束。出参 items 里每一项带 originalValue / candidateValue / unit / constraintType / evidenceRef 与四态,写盘照抄。 -**值的读法(工具与你必须一致)**:「下限~上限」是**范围**—— 如 1~10 是 1 到 10 的整段,-20~+60 是 -20 到 60 的整段,**不是两个数、 -不是「1 或 10」**;两颗写着同一段就是一致。出参每项的 readAs 写着工具把两边各读成了什么(范围 / 定值 / 单边界 / 文本), -写 impact_statement 时按它说,别自己重新解释一遍值。 - -【第二拍:只调 \`part-substitution-assessment.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 覆盖写:parameter_name / original_value / candidate_value / parameter_unit / constraint_type / - evidence_ref / difference_status / compare_result / risk_level 照第一拍出参**一字不改**(值是台账原值,不是你记忆里的; - compare_result 也照抄,别换成自己的词); - impact_statement(MATCH 写「无」;其余说清对应用 / 验证 / 报价的影响,引出参里的具体数值与工具 note 的措辞 —— note 说「需求边界」的, - 影响就写「需求边界与在用料的差距,请核对需求」,不写成候选的短板); - 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),**这一拍不要发卡**。 - -**不许把工具的结论改成别的**:不推荐就是不推荐;要改只能补证据后重比。`, + 'substitution_difference_item_rows > 0 and substitution_difference_item_missing = 0 and ' + + 'substitution_relation_rows = 1 and validation_record_rows = 1', + auto: { do: [{ kind: 'say' as const, text: '正在逐条比对两颗料的核心参数。' }] }, }, { - item: 'S4_review', - /* 中间审核摆成验收工单的样子(机制是可循环的决策卡);由剧本声明,模型抄不抄都一样 */ + item: 'S3_review', + intent: '把逐条结论摆给人过目,他就地改四态与影响说明,改到没问题为止', + guide: '', + writes: ['substitution.difference_item', 'substitution.relation', 'validation.record'], + reads: ['substitution.difference_item', 'substitution.relation'], + tools: [], cardVariant: 'review' as const, - /* 人工验收清单(2026-08-27 用户定):FAE/RI 拿着「差异评估结论审核」这份件要亲自核的步骤,固定 */ + /* 人工验收清单:一条一句他拿着这份工单要**亲自核**的动作,不是系统判的判据 */ acceptanceChecklist: [ - '逐个候选核对结论汇总:推荐程度、风险级别与它下方明细表的四态分布吻合(有硬约束 MISMATCH 或悬空 UNKNOWN 的候选没有给「推荐」)', - '逐条核对差异明细:每个候选的每个参数都有 MATCH / MISMATCH / UNKNOWN / CONDITIONAL 之一,没有漏项', - '核对 UNKNOWN 项的去向:每个 UNKNOWN 都挂了验证任务或写明补来源动作,没有悬空', - '核对证据引用:每项结论能追溯到 P-08 / P-10 / P-06 的记录或本单数据工单的创建记录;硬约束的 MATCH 都有证据支持', + '逐条对过原料与候选料的参数值,没有把需求边界写成候选料的问题', + '硬约束那几条逐条看过,没有一条是「没证据也判了 MATCH」', + '判 UNKNOWN 的都写了去向:实测哪一项、回填到哪儿', + '影响说明站得住:说的是「换上去会不会比现在差」,不是复述参数值', ], - 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:…" → **驳回**。**不许写 review_approved_total、不许 finish** —— 下一步的门读的是基座数 - \`decision_last\`(人最后一次真点了 approve 才开),写了旗也开不了门,只会把自己绕进死循环。按意见改台账(同 PK 覆盖写,不开新行、不改行数): - · 意见点名某个参数 → 改那一行 \`substitution.difference_item\` 的 impact_statement / unknown_disposition / evidence_ref;要挂验证的补一行 \`validation.record\`; - · 意见说推荐理由站不住 → 改 \`substitution.relation.recommendation_reason\`(等级**不许自己填** —— 它只能由工具算;写清楚为什么维持); - · 意见说**某条判定本身错了**(客户约束译错、或原 PN 自己也不满足的客户线被记在候选头上)→ 改对 requirements - (值不归你改:参数是台账原值,工具自己读),再**重新调 \`part-substitution-assessment.compare-parameters\` 与 \`part-substitution-assessment.recommend\`**(这一轮各再调一次, - 工具调用后同拍其余动作作废,下一拍照出参覆盖写四态、风险、备注与推荐等级 —— 这是等级唯一合法的改法); - · 没写具体意见("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**。`, + auto: { do: [{ kind: 'say' as const, text: '差异结论已备好,请过目。' }] }, }, { item: 'T_release', - intent: '拟落账内容已备好,提交 ACT-GATE-03 交 FAE/RI 放行;被打回就改台账再交', - /* 人工验收清单(2026-08-27 用户定):FAE/RI 拿着「评审放行与落账确认」这份件要亲自核的步骤,固定 */ + intent: '把这一签下去会落哪几张表、几行摆清楚,人签字后落账并发口', + guide: '', + /* 人工验收清单:一条一句他拿着这份工单要**亲自核**的动作,不是系统判的判据 */ acceptanceChecklist: [ - '核对拟写 P-11 的每条替代关系:推荐等级与差异明细一致(有 UNKNOWN / CONDITIONAL 的候选没有给「推荐」),推荐理由与差异摘要说得通', - '核对分析状态:每条只写分析层(分析状态 = FAE 确认),客户批准状态保持「未提交」,不由本单产生', - '核对规则版本:每条结论都带本次锁定的 GOV 差异规则版本(R-2026.07)', - '核对拟回写 P-01 的五项:风险等级、验证要求、输出链接、下一步动作、当前阶段都填了,且与结论一致', - '核对验证任务:每个 UNKNOWN 对应的验证记录都在,要测什么、什么条件写清楚了', + '推荐等级与四态分布对得上:有硬约束不符的没给「推荐」', + '待验项清单认可:该实测的都列出来了,没有该验而漏掉的', + '落账内容核对过:进哪几张表、各几行,与屏上摆的一致', + '参数值来源可追:每一条都能说出它是库里既有的还是这一单补录的', + '签下去就发给下游,这一步之后本活动不再改动这些行', ], /* 被打回重做时要改的就是这几张 —— 不声明就写不进去 */ - 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', + writes: ['sub_eval.case'], + reads: ['substitution.relation', 'substitution.difference_item', 'validation.record', 'sub_eval.case'], tools: [], - /* 放行触达世界,必须人按一下 —— 引擎不自动收它 */ - guide: `落账确认那一屏摆着拟写 P-11 主记录与拟回写 P-01。**这一步不发执行工单、不发决策卡**:放行卡由 submit 发 -(基座预检后自己发验收卡),发执行工单等于把同一件事问人两遍(2026-08-27 实测:先一张执行单、人答完才 submit 又一张验收单)。 -**先看轨迹里有没有「验收被打回重做」**: -有 → 这一拍不是再提交一遍。照意见改**台账本身**(同 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\`,不必你选出口。终局唯放行:你不能自己宣布完成。`, + auto: { do: [{ kind: 'say' as const, text: '拟落账内容已备好,请核对后签字。' }] }, }, ]; /** * 阶段 —— 进度条的刻度,只给人看。除放行外每个计划项恰属一个阶段; - * 放行那一格由基座统一加,人看到的是 **x/6** —— 与文档的步骤 0.5 / 1 / 2 / 3 / 4 / 5 六步逐格对齐: - * 0.5 启动与存在性检查(等待数据)· 1 建立评估上下文 · 2 读取并标准化 · 3 逐项差异评估 · 4 FAE 审核与补证(含拟落账)· 5 提交评审放行(基座那格) + * 放行那一格由基座统一加(与 E04 / I01 同款),人看到的是 **x/4**。 */ export const ASSESSMENT_PHASES = [ - { name: '启动与存在性检查', items: ['S0_bind', 'S0d_p08_master', 'S0d_p10_document'] }, - { name: '建立评估上下文', items: ['S1_input_confirm'] }, - { name: '读取并标准化资料', items: ['S2_normalize'] }, - { name: '逐项差异评估', items: ['S3_assess'] }, - { name: 'FAE 审核与补证', items: ['S4_review', 'S5_prepare_landing'] }, + { name: '校验入参', items: ['S1_inputs'] }, + { name: '差异分析', items: ['S2_assess'] }, + { name: '结论审核', items: ['S3_review'] }, ];