fix(w05): S0d 拆成 P-08/P-10 两个计划项各自绑 view;补齐 fresh() 三面旗;候选全放弃防死锁

Task 4 审查发现 S0d_master_data 固定单一 view 导致 P-10 那份件在引擎里永不可达
(剧本声明的 view 恒压过模型的 say.view),按裁决拆成 S0d_p08_master / S0d_p10_document
两个计划项,各自绑 p08-master-create / p10-document-create;caseFileModel 补
parts_profile_rows、新增哨兵 s_p08_created 串起两步的入场前提;说明书三处 itemId、
S0_bind 的 disable 提示、ASSESSMENT_PHASES 第一段随之更新。steps.test.ts 的 fresh()
补齐 input_confirmed_total / review_approved_total / landing_prepared_total 三面旗
(此前因未绑定名比较求值为假才没暴露)。S0d_p08_master 剧本加「至少留一颗候选」规则,
防止候选全被放弃导致 s_pns_resolved 永远不成立的死锁。46/46 测试与 typecheck 全绿。

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-27 00:01:39 +08:00
co-authored by Claude Fable 5
parent f9dbdc8c9c
commit 0629a7f054
3 changed files with 92 additions and 33 deletions
@@ -10,8 +10,8 @@ import { createMockHost } from '@sdk';
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 ITEMS = ['S0_bind', 'S0d_p08_master', 'S0d_p10_document', 'S1_input_confirm', 'S2_normalize', 'S3_assess', 'S4_review', 'S5_prepare_landing', 'T_release'];
const NO_DONE_WHEN = new Set(['S0d_p08_master', 'S0d_p10_document', '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);
const viewHtml = (name: string) => manifest.assets.viewTemplates.find((t) => t.name === name)?.html ?? '';
@@ -124,20 +124,26 @@ 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(两张数据工单 / 输入确认 / 审核 / 放行),其余四处都给', () => {
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 里都标了人工激活', () => {
it('两张数据工单件各自绑自己的 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 里都标了人工激活', () => {
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');
@@ -159,7 +165,7 @@ describe('案例模型与清单的对齐', () => {
it('两种决策卡骨架都在说明书里:候选放弃/创建(create/abandon)与审核(approve/reject/custom:)', () => {
const n = manifest.assets.businessModel.narrative;
expect(n).toContain('"kind":"decide","itemId":"S0d_master_data"');
expect(n).toContain('"kind":"decide","itemId":"S0d_p08_master"');
expect(n).toContain('"value":"create"');
expect(n).toContain('"value":"abandon"');
expect(n).toContain('"kind":"decide","itemId":"S4_review"');