Archived
feat(w05): 差异评估案例模型 —— CMMN 八项七哨兵、说明书五卡骨架、八条剧本、五段进度
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* 步骤剧本与呈现面 —— 这段错了不报错,只是活动开始转圈 / 界面开始撒谎(与 E04 / I01 同一套惯例)。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { evaluateCondition } from '@sfeel';
|
||||
import { ASSESSMENT_PHASES, ASSESSMENT_STEPS, CMMN_XML, NARRATIVE } from '../src/case-model.ts';
|
||||
import { manifest, D } from '../src/manifest.ts';
|
||||
|
||||
const ITEM_IDS = [...CMMN_XML.matchAll(/<(?:humanTask|decisionTask|stage|milestone)\s+id="([^"]+)"/g)].map((m) => m[1] ?? '');
|
||||
|
||||
function doneOf(item: string): string {
|
||||
const found = ASSESSMENT_STEPS.find((s) => s.item === item)?.doneWhen;
|
||||
if (found === undefined) throw new Error(`${item} 没有 doneWhen`);
|
||||
return found;
|
||||
}
|
||||
/** 谓词写坏了引擎当作「没算出来」——测里也照这个口径 */
|
||||
function safeEval(expression: string, scope: Record<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,
|
||||
substitution_relation_rows: 0,
|
||||
substitution_relation_missing: 0,
|
||||
substitution_relation_analysis_status_fae_confirmed: 0,
|
||||
substitution_difference_item_rows: 0,
|
||||
substitution_difference_item_missing: 0,
|
||||
substitution_difference_item_distinct_substitution_relation_id: 0,
|
||||
substitution_difference_item_difference_status_match: 0,
|
||||
substitution_difference_item_difference_status_mismatch: 0,
|
||||
substitution_difference_item_difference_status_unknown: 0,
|
||||
substitution_difference_item_difference_status_conditional: 0,
|
||||
tool_part_substitution_assessment_compare_parameters_calls: 0,
|
||||
tool_part_substitution_assessment_recommend_calls: 0,
|
||||
});
|
||||
|
||||
describe('剧本与案例模型对得上', () => {
|
||||
it('每一条剧本都指向一个真实存在的计划项,图上每一步也都有剧本', () => {
|
||||
for (const step of ASSESSMENT_STEPS) expect(ITEM_IDS, `剧本里的 ${step.item} 在 CMMN 里不存在`).toContain(step.item);
|
||||
for (const id of ITEM_IDS) expect(ASSESSMENT_STEPS.map((s) => s.item), `计划项 ${id} 没有剧本`).toContain(id);
|
||||
});
|
||||
|
||||
it('清单里挂的就是这份剧本与这份阶段', () => {
|
||||
expect(manifest.assets.businessModel.steps).toBe(ASSESSMENT_STEPS);
|
||||
expect(manifest.assets.businessModel.phases).toBe(ASSESSMENT_PHASES);
|
||||
});
|
||||
});
|
||||
|
||||
describe('剧本声明的读写面 ⊆ 清单声明', () => {
|
||||
const declaredAddresses = new Set(manifest.declarations.ledger.map((d) => `${d.domain}.${d.object}`));
|
||||
const declaredTools = new Set(manifest.declarations.tools.map((t) => t.name));
|
||||
|
||||
it('steps[].writes / reads(台账那几个)全落在声明过的对象上', () => {
|
||||
for (const step of ASSESSMENT_STEPS) {
|
||||
for (const address of step.writes) expect(declaredAddresses.has(address), `${step.item} 要写 ${address}`).toBe(true);
|
||||
for (const address of step.reads.filter((r) => r.includes('.') && !r.startsWith('case:'))) {
|
||||
expect(declaredAddresses.has(address), `${step.item} 要读 ${address}`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('steps[].tools 全部是声明过的工具;只有 S3 调工具', () => {
|
||||
for (const step of ASSESSMENT_STEPS) {
|
||||
for (const tool of step.tools) expect(declaredTools.has(tool), `${step.item} 要调 ${tool}`).toBe(true);
|
||||
if (step.item !== 'S3_assess') expect(step.tools, step.item).toEqual([]);
|
||||
}
|
||||
expect(ASSESSMENT_STEPS.find((s) => s.item === 'S3_assess')?.tools).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('customer_approval_status 在任何剧本里都不许写(只读边界)', () => {
|
||||
for (const step of ASSESSMENT_STEPS) expect(step.guide, step.item).not.toMatch(/customer_approval_status\s*=/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('doneWhen —— 开局不许真,干完必须真', () => {
|
||||
it('**开局一步都不该被自动收掉** —— 五条样例的入参形状逐一试', () => {
|
||||
for (const sample of manifest.samples) {
|
||||
for (const step of ASSESSMENT_STEPS) {
|
||||
if (step.doneWhen === undefined) continue;
|
||||
expect(safeEval(step.doneWhen, fresh(sample.input as Record<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).toContain('"kind":"decide","itemId":"S0d_master_data"');
|
||||
expect(NARRATIVE).toContain('"title":"数据工单 · 芯片主数据创建(P-08)');
|
||||
expect(NARRATIVE).toContain('"title":"数据工单 · 原厂技术资料创建(P-10)');
|
||||
expect(NARRATIVE).toContain('"title":"评估输入与资料补齐确认');
|
||||
expect(NARRATIVE).toContain('"kind":"decide","itemId":"S4_review"');
|
||||
});
|
||||
|
||||
it('剧本里不引反例原话;S3 固定三拍且写明「工具后同拍作废」', () => {
|
||||
const s3 = ASSESSMENT_STEPS.find((s) => s.item === 'S3_assess')?.guide ?? '';
|
||||
expect(s3).toContain('第一拍');
|
||||
expect(s3).toContain('第二拍');
|
||||
expect(s3).toContain('第三拍');
|
||||
expect(s3).toContain('工具调用后同拍其余动作作废');
|
||||
});
|
||||
|
||||
it('S0 剧本写明库里都有就 disable S0d;S0d 剧本写明候选 ≥2 才问、=1 必建、原 PN 硬阻塞', () => {
|
||||
const s0 = ASSESSMENT_STEPS.find((s) => s.item === 'S0_bind')?.guide ?? '';
|
||||
expect(s0).toContain('disable S0d_master_data');
|
||||
const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_master_data')?.guide ?? '';
|
||||
expect(s0d).toContain('硬阻塞');
|
||||
expect(s0d).toContain('候选只有 1 颗');
|
||||
});
|
||||
|
||||
it('五段进度,每段名 ≤10 字;除放行外每项恰属一段', () => {
|
||||
expect(ASSESSMENT_PHASES).toHaveLength(5);
|
||||
for (const p of ASSESSMENT_PHASES) expect(p.name.length, p.name).toBeLessThanOrEqual(10);
|
||||
const all = ASSESSMENT_PHASES.flatMap((p) => p.items);
|
||||
expect(new Set(all).size).toBe(all.length);
|
||||
expect([...all].sort()).toEqual([...ITEM_IDS.filter((i) => i !== 'T_release')].sort());
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user