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:
xcs
2026-08-26 23:37:16 +08:00
co-authored by Claude Fable 5
parent 13183b9287
commit f7fe8e3836
3 changed files with 707 additions and 6 deletions
@@ -11,6 +11,8 @@ import { activityManifestSchema } from '@contracts';
import { manifest, D, RULE_VERSION } from '../src/manifest.ts';
import activity from '../src/index.ts';
const ITEMS = ['S0_bind', 'S0d_master_data', 'S1_input_confirm', 'S2_normalize', 'S3_assess', 'S4_review', 'S5_prepare_landing', 'T_release'];
const NO_DONE_WHEN = new Set(['S0d_master_data', 'S1_input_confirm', 'S4_review', 'T_release']);
const VIEWS = ['p08-master-create', 'p10-document-create', 'eval-input-list', 'difference-review', 'landing-confirm'];
const byAddress = (domain: string, object: string) => manifest.declarations.ledger.find((l) => l.domain === domain && l.object === object);
@@ -121,6 +123,60 @@ describe('台账声明:授权面与《统一方案》口径', () => {
});
});
describe('案例模型与清单的对齐', () => {
it('八个计划项,剧本 item 与 CMMN humanTask id 一一对应', () => {
const cmmnIds = [...manifest.assets.businessModel.cmmnXml.matchAll(/<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(数据工单 / 输入确认 / 审核 / 放行),其余四处都给', () => {
for (const s of manifest.assets.businessModel.steps as ReadonlyArray<{ item: string; doneWhen?: string }>) {
if (NO_DONE_WHEN.has(s.item)) expect(s.doneWhen, s.item).toBeUndefined();
else expect(s.doneWhen, s.item).toBeDefined();
}
});
it('那四项在 CMMN 里都标了人工激活', () => {
for (const id of NO_DONE_WHEN) {
const block = new RegExp(`<humanTask id="${id}"[\\s\\S]*?</humanTask>`).exec(manifest.assets.businessModel.cmmnXml)?.[0] ?? '';
expect(block, id).toContain('manualActivationRule');
}
});
it('除放行外每一项恰好属于一个阶段;5 段 + 放行 = x/6,与文档 0.5/1/2/3/4/5 六步对齐', () => {
const phases = manifest.assets.businessModel.phases ?? [];
expect(phases).toHaveLength(5);
const phaseItems = phases.flatMap((p) => p.items);
expect([...phaseItems].sort()).toEqual([...ITEMS.filter((i) => i !== 'T_release')].sort());
expect(new Set(phaseItems).size).toBe(phaseItems.length);
});
it('说明书点名的计划项都真实存在', () => {
const mentioned = [...manifest.assets.businessModel.narrative.matchAll(/\b(S\d[a-z]?_[a-zA-Z_]+|T_release)\b/g)].map((m) => m[1] ?? '');
for (const name of mentioned) expect(ITEMS, `说明书提到但不存在的计划项: ${name}`).toContain(name);
});
it('两种决策卡骨架都在说明书里:候选放弃/创建(create/abandon)与审核(approve/reject/custom:)', () => {
const n = manifest.assets.businessModel.narrative;
expect(n).toContain('"kind":"decide","itemId":"S0d_master_data"');
expect(n).toContain('"value":"create"');
expect(n).toContain('"value":"abandon"');
expect(n).toContain('"kind":"decide","itemId":"S4_review"');
expect(n).toContain('"approve"');
expect(n).toContain('"reject"');
expect(n).toContain('"custom:"');
});
it('T_release 剧本里 judge criterion 的原文逐字对得上 acceptanceCriteria 的 label', () => {
const tRelease = (manifest.assets.businessModel.steps as ReadonlyArray<{ item: string; guide?: string }>).find((s) => s.item === 'T_release');
const quoted = [...(tRelease?.guide ?? '').matchAll(/judge criterion="([^"]+)"/g)].map((m) => m[1] ?? '');
expect(quoted).toHaveLength(3);
const labels = new Set(manifest.contract.acceptanceCriteria.map((c) => c.label));
for (const q of quoted) expect(labels.has(q), q).toBe(true);
});
});
describe('活动入口', () => {
it('没有 run —— 行为全在清单里', () => {
expect(activity.run).toBeUndefined();