Archived
feat(w05): 案例模型换成四步 —— 零 guide、零工具、零记账旗
九步砍成四步:校验入参 / 差异分析 / 结论审核 / 落账放行。 三道入场哨兵全读基座数,caseFileModel 里一面记账旗都没有 —— 旗是活动自己写的数,而「机检只读基座数」这条在本仓被违反过六次。 审核与放行触达的是人:标人工激活、不给 doneWhen。 说明书瘦成「整单的事」,示例一律占位形 —— 模型会把示例当成这一单的值。 manualActivation 未照 brief 给的 planItem+definitionRef 写法(与本仓 cmmn-parse.ts 不兼容,会解析出错误的计划项 id):改用本仓既有的单元素写法 —— <humanTask id="..." manualActivation="true">,与 steps.test.ts 给定的 RED 测试、 E04/I01/旧九步 W05 的既有约定一致。 顺带恢复 Task 2 遗留的两条「Task 3 补回」skip(读写面 ⊆ 台账声明 / 工具 ⊆ 声明工具, 后者按零工具的新立场做了修正而非照抄旧断言);第三条(T_release judge criterion 逐字对应 acceptanceCriteria label)调查后判定为结构性不可恢复 —— 它检查的「guide 散文里嵌 judge criterion 给模型执行」这套机制随零 guide 重写整体撤掉,继续保留 skip 并订正注释说明原因。 四步重写让 activity.test.ts 的「案例模型与清单的对齐」组里 7 条既有(未 skip)断言 硬编码了旧九步结构,不改不可能 pnpm gates 绿:仍然成立的按新模型更新数字/名单 (四计划项 / 两处不给 doneWhen / 三段阶段);检查对象已被撤掉机制的转 skip 并写明 原因(P-08/P-10 双视图已合并进单一 S1_inputs;决策卡 JSON 骨架已从说明书散文挪进 配方的结构化 card 声明)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -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(/<humanTask id="([^"]+)"/g)].map((m) => 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(`<humanTask id="${id}"[\\s\\S]*?</humanTask>`).exec(manifest.assets.businessModel.cmmnXml)?.[0] ?? '';
|
||||
expect(block, id).toContain('manualActivationRule');
|
||||
/* Task 3 起改用 `manualActivation="true"` 属性直接标在 humanTask 元素上(与 CMMN 解析器的两种认法之一同款),不再嵌套 <manualActivationRule> */
|
||||
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');
|
||||
|
||||
@@ -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<string, unknown>): boolean {
|
||||
try {
|
||||
return evaluateCondition(expression, scope);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/** 开局局面:入参 + 所有基座读数归零 */
|
||||
const fresh = (input: Record<string, unknown>): Record<string, unknown> => ({
|
||||
...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<string, unknown>)), `样例「${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 = /<sentry id="s_bound">[\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 = /<sentry id="s_pns_resolved">[\s\S]*?<condition>([\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('<span class="jc-vcell ro"><span class="stx"');
|
||||
});
|
||||
|
||||
it('表头与「字段」格只留中文:没有英文列名、没有 .thd 小字 —— 五张件一律(2026-08-27 用户在数据工单页、审核页各指出一次)', () => {
|
||||
for (const v of manifest.assets.viewTemplates) {
|
||||
expect(v.html, v.name).not.toContain('class="thd"');
|
||||
for (const th of v.html.match(/<th[^>]*>[^<]*<\/th>/g) ?? []) expect(th, `${v.name} th`).not.toMatch(/[a-z]_[a-z]/);
|
||||
for (const td of v.html.match(/<td class="cust">[^<]*<\/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('<caseFileItem id="decision_last"');
|
||||
const g = ASSESSMENT_STEPS.find((s) => 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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<definitions xmlns="http://www.omg.org/spec/CMMN/20151109/MODEL">
|
||||
<case id="part-substitution-assessment">
|
||||
<!--
|
||||
案卷项的 id 就是任务视图里的键名。入参顶层字段(customerCode / customerName / requestDescription /
|
||||
originalInternalPn / candidateInternalPns / datasheets)不进这份 caseFileModel —— 谓词里只用
|
||||
count(candidateInternalPns)。其余全是基座读数(行数 / 缺必填 / 按值计数 / 工具调用数)与
|
||||
sub_eval.verdict 里的三面记账旗(每面旗都配着一半基座数)。
|
||||
案卷项的 id 就是任务视图里的键名 —— **全是基座读数**(本单草稿写下的行数 / 缺必填数 /
|
||||
按值计数 / 不同值个数)。这一版里**一面记账旗都没有**:旗是活动自己写的数,
|
||||
而「机检只读基座数」这条纪律在本仓被违反过六次,每次都是同一种事故。
|
||||
-->
|
||||
<caseFileModel>
|
||||
<caseFileItem id="demand_request_rows" name="本单写下的需求主记录行数(基座现数)"/>
|
||||
<caseFileItem id="demand_request_missing" name="需求主记录缺必填的行数(基座现数)"/>
|
||||
<caseFileItem id="sub_eval_pn_check_rows" name="存在性核对行数(基座现数)"/>
|
||||
<caseFileItem id="sub_eval_pn_check_missing" name="存在性核对缺必填的行数(基座现数)"/>
|
||||
<caseFileItem id="sub_eval_pn_check_exists_status_absent" name="仍不在库的料号数(基座现数)"/>
|
||||
<caseFileItem id="sub_eval_pn_check_exists_status_abandoned" name="已放弃的候选数(基座现数)"/>
|
||||
<caseFileItem id="sub_eval_verdict_rows" name="结论行数(基座现数)"/>
|
||||
<caseFileItem id="parts_profile_rows" name="本单建的芯片主数据行数(基座现数)"/>
|
||||
<caseFileItem id="sub_eval_pn_check_exists_status_created" name="经数据工单建好的料号数(基座现数)"/>
|
||||
<caseFileItem id="parts_profile_missing" name="本单建的芯片主数据缺必填的行数(基座现数)"/>
|
||||
<caseFileItem id="parts_parameter_missing" name="本单建的标准化参数集缺必填的行数(基座现数)"/>
|
||||
<caseFileItem id="parts_document_missing" name="本单建的资料记录缺必填的行数(基座现数)"/>
|
||||
<caseFileItem id="parts_document_rows" name="本单建的资料记录行数(基座现数)"/>
|
||||
<caseFileItem id="parts_parameter_rows" name="本单建的参数行数(基座现数)"/>
|
||||
<caseFileItem id="parts_parameter_distinct_param_name" name="本单用到的不同参数名个数(基座现数不同值)"/>
|
||||
<caseFileItem id="input_confirmed_total" name="评估输入已确认(人环凭据)"/>
|
||||
<caseFileItem id="sub_eval_input_gap_rows" name="评估输入清单行数(基座现数)"/>
|
||||
<caseFileItem id="sub_eval_input_gap_missing" name="评估输入清单缺必填的行数(基座现数)"/>
|
||||
<caseFileItem id="substitution_relation_rows" name="替代关系行数(基座现数)"/>
|
||||
<caseFileItem id="substitution_relation_missing" name="替代关系缺必填的行数(基座现数)"/>
|
||||
<caseFileItem id="substitution_relation_analysis_status_fae_confirmed" name="分析状态已 FAE 确认的关系数(基座现数)"/>
|
||||
<caseFileItem id="sub_eval_part_exists_status_found" name="两颗料里已在库的个数(基座现数)"/>
|
||||
<caseFileItem id="sub_eval_part_missing" name="本单两颗料缺必填的行数(基座现数)"/>
|
||||
<caseFileItem id="sub_eval_part_distinct_chip_type_id" name="两颗料用到的不同类型个数(基座现数不同值)"/>
|
||||
<caseFileItem id="parts_profile_missing" name="本单碰过的芯片主数据缺必填的行数(基座现数)"/>
|
||||
<caseFileItem id="parts_parameter_missing" name="本单碰过的参数行缺必填的行数(基座现数)"/>
|
||||
<caseFileItem id="substitution_difference_item_rows" name="差异明细行数(基座现数)"/>
|
||||
<caseFileItem id="substitution_difference_item_missing" name="差异明细缺必填的行数(基座现数)"/>
|
||||
<caseFileItem id="substitution_difference_item_distinct_substitution_relation_id" name="有明细的关系数(基座现数不同值)"/>
|
||||
<caseFileItem id="substitution_difference_item_difference_status_match" name="MATCH 条数(基座现数)"/>
|
||||
<caseFileItem id="substitution_difference_item_difference_status_mismatch" name="MISMATCH 条数(基座现数)"/>
|
||||
<caseFileItem id="substitution_difference_item_difference_status_unknown" name="UNKNOWN 条数(基座现数)"/>
|
||||
<caseFileItem id="substitution_difference_item_difference_status_conditional" name="CONDITIONAL 条数(基座现数)"/>
|
||||
<caseFileItem id="tool_part_substitution_assessment_compare_parameters_calls" name="比对工具成功调用次数(基座现数)"/>
|
||||
<caseFileItem id="tool_part_substitution_assessment_recommend_calls" name="定级工具成功调用次数(基座现数)"/>
|
||||
<caseFileItem id="review_approved_total" name="差异结论已审核通过(仅通过时置一)"/>
|
||||
<caseFileItem id="decision_last" name="最后一张决策卡人选的值(基座按留痕数)"/>
|
||||
<caseFileItem id="landing_prepared_total" name="拟落账已备好(S5 写完时置一)"/>
|
||||
<caseFileItem id="substitution_relation_rows" name="替代关系行数(基座现数)"/>
|
||||
<caseFileItem id="substitution_relation_missing" name="替代关系缺必填的行数(基座现数)"/>
|
||||
<caseFileItem id="validation_record_rows" name="验证记录行数(基座现数)"/>
|
||||
<caseFileItem id="validation_record_missing" name="验证记录缺必填的行数(基座现数)"/>
|
||||
</caseFileModel>
|
||||
<casePlanModel id="plan" name="差异评估" autoComplete="true">
|
||||
<!-- ① 已绑定且真有料号不在库:需求主记录与存在性核对都写下了、至少一颗还 absent —— 数据工单那一项的入场前提 -->
|
||||
<sentry id="s_bound">
|
||||
<onPart id="op_b" caseFileItemRef="sub_eval_pn_check_exists_status_absent"><standardEvent>update</standardEvent></onPart>
|
||||
<ifPart id="if_b"><condition>demand_request_rows > 0 and sub_eval_pn_check_rows > 0 and sub_eval_pn_check_exists_status_absent > 0</condition></ifPart>
|
||||
<!-- ① 入参齐备:两颗都在库、都归了类型、且是同一个类型、主数据与参数都不缺 —— 分析那一步的入场前提 -->
|
||||
<sentry id="s_inputs_ready">
|
||||
<onPart id="op_i" caseFileItemRef="sub_eval_part_exists_status_found"><standardEvent>update</standardEvent></onPart>
|
||||
<ifPart id="if_i"><condition>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</condition></ifPart>
|
||||
</sentry>
|
||||
<!-- ②′ 芯片主数据已建齐(表 1 主记录 + 表 2 参数子表都不缺必填)—— 补资料那一步的入场前提(基座现数) -->
|
||||
<sentry id="s_p08_created">
|
||||
<onPart id="op_p8" caseFileItemRef="parts_profile_rows"><standardEvent>update</standardEvent></onPart>
|
||||
<ifPart id="if_p8"><condition>parts_profile_rows > 0 and parts_profile_missing = 0 and parts_parameter_missing = 0</condition></ifPart>
|
||||
</sentry>
|
||||
<!--
|
||||
② 料号全部落定:原 PN + 每个候选各一行核对,没有一颗还「不在库」,放弃的少于候选数,
|
||||
本单建的主数据 / 参数子表 / 资料不缺必填 —— 评估输入确认那一步的入场前提。全是基座现数。
|
||||
-->
|
||||
<sentry id="s_pns_resolved">
|
||||
<onPart id="op_pr" caseFileItemRef="sub_eval_pn_check_exists_status_absent"><standardEvent>update</standardEvent></onPart>
|
||||
<!--
|
||||
**标了 created 的每一颗都要有一行主数据**(2026-08-27 e2e 两轮各现一形):
|
||||
「没写过 ≠ 写了 0 行」(2026-08-01 A1 立的那一课,在这里以新形状复现):
|
||||
\`_missing\` 只数**已有行**缺不缺格子 —— 一行都没写时它是 0,门就开了。
|
||||
实测:模型压拍没写骨架就把两张数据工单发了出去、人也「交」了,
|
||||
\`parts_profile_missing = 0\` 成立,这一步照过,而库里那颗料根本不存在。
|
||||
第一刀写成「created ≥ 1 就得有行」还是太松:三候选那单里模型把**第二颗**也标成 created
|
||||
却只建了第一颗,一行足以让门开着,直到 S2 写关系时才被引用检查拦下。
|
||||
改成**逐颗对账**:\`parts_profile_rows >= created\` —— 标了几颗建好,就得有几行。
|
||||
-->
|
||||
<!--
|
||||
**不再等模型把 absent 改成 created**(2026-08-27 晚):那面旗是模型记的账,忘了记就永远开不了门
|
||||
(「机检只读基座数」的哨兵形)。「一颗 absent 都不剩」的基座事实 = 要建的每一颗(absent + created)
|
||||
主数据、参数、资料都落了且必填不缺;created 仍可写(说明书照旧让它记),但门不靠它。
|
||||
-->
|
||||
<ifPart id="if_pr"><condition>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)</condition></ifPart>
|
||||
</sentry>
|
||||
<!-- ③ 评估输入已确认(旗 + 清单有行且必填齐)—— 标准化那一步的入场前提 -->
|
||||
<sentry id="s_input_confirmed">
|
||||
<onPart id="op_ic" caseFileItemRef="input_confirmed_total"><standardEvent>update</standardEvent></onPart>
|
||||
<ifPart id="if_ic"><condition>input_confirmed_total > 0 and sub_eval_input_gap_rows > 0 and sub_eval_input_gap_missing = 0</condition></ifPart>
|
||||
</sentry>
|
||||
<!-- ④ 对照表已铺好:每个未放弃的候选一条关系、每条关系都有明细 —— 逐项评估的入场前提 -->
|
||||
<sentry id="s_normalized">
|
||||
<onPart id="op_n" caseFileItemRef="substitution_difference_item_rows"><standardEvent>update</standardEvent></onPart>
|
||||
<ifPart id="if_n"><condition>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</condition></ifPart>
|
||||
</sentry>
|
||||
<!-- ⑤ 逐项评估已完成:明细与关系必填齐、四态之和 = 明细行数、两枚工具都真调过 —— 审核的入场前提 -->
|
||||
<!-- ② 结论已成形:逐条明细、一条关系、一条验证记录都写下了且不缺 —— 审核那一步的入场前提 -->
|
||||
<sentry id="s_assessed">
|
||||
<onPart id="op_a" caseFileItemRef="substitution_difference_item_missing"><standardEvent>update</standardEvent></onPart>
|
||||
<ifPart id="if_a"><condition>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</condition></ifPart>
|
||||
<onPart id="op_a" caseFileItemRef="substitution_difference_item_rows"><standardEvent>update</standardEvent></onPart>
|
||||
<ifPart id="if_a"><condition>substitution_difference_item_rows > 0 and substitution_difference_item_missing = 0 and substitution_relation_rows = 1 and validation_record_rows = 1</condition></ifPart>
|
||||
</sentry>
|
||||
<!-- ⑥ 审核已通过 —— 拟落账的入场前提 -->
|
||||
<sentry id="s_reviewed">
|
||||
<onPart id="op_r" caseFileItemRef="review_approved_total"><standardEvent>update</standardEvent></onPart>
|
||||
<!--
|
||||
审核通过与否**不由模型自写的旗说了算**(2026-08-27 实测:人两次答「重做」,模型照拒回的指引写了旗,
|
||||
S4 被收掉一路推到放行)。\`decision_last\` 是基座按 card-reply 数出来的:人最后一次真点了 approve 才开门。
|
||||
旗留着记账,但它单独说了不算。
|
||||
-->
|
||||
<ifPart id="if_r"><condition>review_approved_total > 0 and decision_last = "approve" and substitution_difference_item_missing = 0</condition></ifPart>
|
||||
</sentry>
|
||||
<!-- ⑦ 拟落账已备好:每条关系都到 FAE 确认、P-01 回写五项齐 —— 放行门的入场前提 -->
|
||||
<sentry id="s_landing_ready">
|
||||
<onPart id="op_l" caseFileItemRef="landing_prepared_total"><standardEvent>update</standardEvent></onPart>
|
||||
<ifPart id="if_l"><condition>landing_prepared_total > 0 and substitution_relation_analysis_status_fae_confirmed = substitution_relation_rows and substitution_relation_missing = 0 and demand_request_missing = 0</condition></ifPart>
|
||||
<!-- ③ 放行的入场前提与 ② 同源:审核那一步收没收由人答卡决定,不在这儿判 -->
|
||||
<sentry id="s_reviewable">
|
||||
<onPart id="op_r" caseFileItemRef="substitution_relation_rows"><standardEvent>update</standardEvent></onPart>
|
||||
<ifPart id="if_r"><condition>substitution_relation_rows = 1 and substitution_relation_missing = 0 and validation_record_missing = 0</condition></ifPart>
|
||||
</sentry>
|
||||
|
||||
<!-- 启动期:铸案号、建需求 V1、锁规则版本、逐个料号查库(能算的不问人) -->
|
||||
<humanTask id="S0_bind" name="建立评估上下文:绑定四项、新建需求、存在性核对"/>
|
||||
<!-- 1 校验入参与补数据:核对两颗料在不在库、归没归同一类型、参数齐不齐;缺什么派数据工单让人补(全 auto,doneWhen 收步) -->
|
||||
<humanTask id="S1_inputs" name="校验入参与补数据"/>
|
||||
|
||||
<!-- 0.5a 等待数据(建主数据):P-08 数据工单;人工激活,doneWhen 只在人答过卡之后求(入库事实);都在库时 s_bound 不开,不进「当下可为」 -->
|
||||
<humanTask id="S0d_p08_master" name="数据工单:补建芯片主数据(P-08)">
|
||||
<entryCriterion id="ec_s0d8" sentryRef="s_bound"/>
|
||||
<itemControl><manualActivationRule id="m_s0d8"/></itemControl>
|
||||
<!-- 2 逐项差异分析:核心参数逐条比对,四态、推荐等级、风险由系统按规则算(全 auto,doneWhen 收步) -->
|
||||
<humanTask id="S2_assess" name="逐项差异分析">
|
||||
<entryCriterion id="e_assess" sentryRef="s_inputs_ready"/>
|
||||
</humanTask>
|
||||
|
||||
<!-- 0.5b 等待数据(建资料):P-10 数据工单;人工激活,doneWhen 只在人答过卡之后求(入库事实);都在库时 s_p08_created 不开,不进「当下可为」 -->
|
||||
<humanTask id="S0d_p10_document" name="数据工单:补建原厂技术资料(P-10)">
|
||||
<entryCriterion id="ec_s0d10" sentryRef="s_p08_created"/>
|
||||
<itemControl><manualActivationRule id="m_s0d10"/></itemControl>
|
||||
<!-- 3 结论审核**触达的是人**:标人工激活,且不给 doneWhen —— 完成与否不该由数据形状推出来 -->
|
||||
<humanTask id="S3_review" name="差异评估结论审核" manualActivation="true">
|
||||
<entryCriterion id="e_review" sentryRef="s_assessed"/>
|
||||
</humanTask>
|
||||
|
||||
<!-- 1 建立评估上下文:交 PW 在件上补缺口(执行工单 01)—— 不给 doneWhen -->
|
||||
<humanTask id="S1_input_confirm" name="评估输入与资料补齐确认">
|
||||
<entryCriterion id="ec_s1" sentryRef="s_pns_resolved"/>
|
||||
<itemControl><manualActivationRule id="m_s1"/></itemControl>
|
||||
</humanTask>
|
||||
|
||||
<!-- 2 读取并标准化资料(纯 AI) -->
|
||||
<humanTask id="S2_normalize" name="标准化参数对照表:P-11 主记录骨架 + 差异明细对照行">
|
||||
<entryCriterion id="ec_s2" sentryRef="s_input_confirmed"/>
|
||||
</humanTask>
|
||||
|
||||
<!-- 3 逐项差异评估(AI + 两枚工具) -->
|
||||
<humanTask id="S3_assess" name="逐项差异评估:四态、风险、证据、推荐等级、验证任务">
|
||||
<entryCriterion id="ec_s3" sentryRef="s_normalized"/>
|
||||
</humanTask>
|
||||
|
||||
<!-- 4 FAE/RI 审核:决策卡循环,通过或驳回 + AI 改台账重发 —— 不给 doneWhen -->
|
||||
<humanTask id="S4_review" name="差异评估结论审核:通过或驳回(驳回则 AI 修订重提交)">
|
||||
<entryCriterion id="ec_s4" sentryRef="s_assessed"/>
|
||||
<itemControl><manualActivationRule id="m_s4"/></itemControl>
|
||||
</humanTask>
|
||||
|
||||
<!-- 5(前半)拟落账:分析状态推到 FAE 确认、回写 P-01(不触达世界,给 doneWhen) -->
|
||||
<humanTask id="S5_prepare_landing" name="拟落账:P-11 分析状态 → FAE 确认,回写 P-01">
|
||||
<entryCriterion id="ec_s5" sentryRef="s_reviewed"/>
|
||||
</humanTask>
|
||||
|
||||
<!-- 5(后半)提交 ACT-GATE-03:终局唯放行,必须人工激活 -->
|
||||
<humanTask id="T_release" name="评审放行与落账确认(ACT-GATE-03)">
|
||||
<entryCriterion id="ec_rel" sentryRef="s_landing_ready"/>
|
||||
<itemControl><manualActivationRule id="m_rl"/></itemControl>
|
||||
<!-- 4 落账放行**触达的是人**:标人工激活,且不给 doneWhen —— 签字这件事只能由人做 -->
|
||||
<humanTask id="T_release" name="落账放行" manualActivation="true">
|
||||
<entryCriterion id="e_release" sentryRef="s_reviewable"/>
|
||||
</humanTask>
|
||||
</casePlanModel>
|
||||
</case>
|
||||
</definitions>`;
|
||||
|
||||
/**
|
||||
* 案例说明书 —— **只写整单一以贯之的事**(逐步做法归剧本)。
|
||||
* 卡骨架住在这里而不是剧本里(模型会压拍,压过之后只有说明书还在 —— 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":"客户 <customerCode> 不在客户档案里,请核对建档信息后确认",
|
||||
"ledger":"customer.profile","key":{"customer_code":"<customerCode>"},
|
||||
"fields":[
|
||||
{"key":"name","label":"客户名称","value":"<customerName,没有就整格不写 value>","required":true},
|
||||
{"key":"industry","label":"所属行业"},
|
||||
{"key":"region","label":"区域"}
|
||||
],
|
||||
"basis":[{"k":"触发原因","v":"按客户编码 <customerCode> 查客户档案,查无记录"},
|
||||
{"k":"这一单要评估的","v":"原 PN <originalInternalPn> → <N> 个候选"},
|
||||
{"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> → <N> 个候选",
|
||||
"ask":"启动绑定的四项已锁定。请在工作区的件上补齐「待补」行的补充内容,默认值可改可确认。",
|
||||
"steps":["应用场景、BOM 位是启动时未收的:必须补(写在「补充内容」列)",
|
||||
"每颗料每个参数此刻的值摆在件上的「候选参数现值」表里(只读参考);原厂未披露的参数这里不用补 —— 差异评估会把它判成 UNKNOWN 并挂验证任务",
|
||||
"替代类型 / 规则版本 / 负责人期限三行是系统默认,可改可不改",
|
||||
"加一行 = 补充 AI 没列出的资料项;删一行 = 该项不适用;已绑定的四项不可改",
|
||||
"改完保存,回这张卡提交"],
|
||||
"standard":"每一条「待补」行的补充内容都有;补不了的写明理由。",
|
||||
"basis":[{"k":"已绑定","v":"客户 <customerCode> · 需求 <request_id> V1 · 原 PN <原 PN> · 候选 <N> 颗"},
|
||||
{"k":"缺口","v":"<N> 项待补"}]}}
|
||||
|
||||
⑥ 验收工单 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":"差异评估结论已算出:<N> 个候选、共 <M> 条参数比对(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'] },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user