三面拆分⑲:审批点卸岗位 —— 负责人一律运营时分配,绑员工组,实例记 assignee
模型(用户定):审批点不绑岗位;节点负责人是运营时分配的,绑定到员工组。 - 契约:approval 节点删 approver(功能属性只剩驳回走向/催办/委托消息); CLOSURE_VIOLATION_KINDS 删 missing-owner-position(岗位不入编排,无岗可核); 治理审批列表删 approverPosition 字段 - 运行时统一口径(活动点=审批点):工作流绑了员工组按组轮值(组员并集最闲者先), 没绑组全员轮值;staffId 稳定排序保证确定性,组员全不在岗退回全员。 ADR-011「绑定先于策略」随岗位退场 —— 工作组快照的成员集合成为轮值池 - 实例记 assignee(迁移 0014 node_instance.assignee_id):分派结果是流转事实 —— 审批箱收件(i.assigneeId === me)、裁决权(同一判据)、员工面链条报实际承接人、 治理台审批列表报人,全部按它认,不再有岗位比对 - closure 卸 positions 入参;desk/desk-approval/governance-ops 三路由改口; web 属性卡删「审批岗位」选择与警示、positions 管线拆除;种子 A5 卸 PM; 导出依赖文件审批段只留编排语义 测试:closure 25、save-package 6、seed 8、graph-to-pkg 5、端到端八例真库全绿; 五闸退出码全过。库:迁移 + v4 就地同步(唯一生效版)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -56,11 +56,10 @@ const pkg = (over: Partial<ProcessPackage> = {}): ProcessPackage =>
|
||||
}) as ProcessPackage;
|
||||
|
||||
const contracts = new Map([['a4', face()]]);
|
||||
const positions = new Set(['BD', 'PM', 'FAE', 'PRM', 'OP']);
|
||||
|
||||
describe('checkClosure —— 不闭合不得激活', () => {
|
||||
it('必填全映射、岗位存在即可激活', () => {
|
||||
const report = checkClosure({ pkg: pkg(), contracts, positions });
|
||||
const report = checkClosure({ pkg: pkg(), contracts });
|
||||
expect(report.violations).toEqual([]);
|
||||
expect(report.closed).toBe(true);
|
||||
});
|
||||
@@ -72,7 +71,7 @@ describe('checkClosure —— 不闭合不得激活', () => {
|
||||
{ id: 'e2', from: 'A4' as never, to: 'end' as never, mapping: [], reopen: false },
|
||||
],
|
||||
});
|
||||
const report = checkClosure({ pkg: broken, contracts, positions });
|
||||
const report = checkClosure({ pkg: broken, contracts });
|
||||
expect(report.closed).toBe(false);
|
||||
expect(report.violations.map((v) => v.kind)).toContain('edge-unmapped');
|
||||
expect(report.violations.map((v) => v.kind)).toContain('required-unmapped');
|
||||
@@ -91,41 +90,15 @@ describe('checkClosure —— 不闭合不得激活', () => {
|
||||
{ id: 'e2', from: 'A4' as never, to: 'end' as never, mapping: [], reopen: false },
|
||||
],
|
||||
});
|
||||
const violations = checkClosure({ pkg: partial, contracts, positions }).violations;
|
||||
const violations = checkClosure({ pkg: partial, contracts }).violations;
|
||||
expect(violations.filter((v) => v.kind === 'required-unmapped').map((v) => v.field)).toEqual(['basePrice']);
|
||||
});
|
||||
|
||||
it('引用未安装的活动即拦', () => {
|
||||
const report = checkClosure({ pkg: pkg(), contracts: new Map(), positions });
|
||||
const report = checkClosure({ pkg: pkg(), contracts: new Map() });
|
||||
expect(report.violations.map((v) => v.kind)).toContain('unknown-activity');
|
||||
});
|
||||
|
||||
/* 活动点不核岗位(契约不带负责人);审批点的岗位是功能语义,不存在即拦 */
|
||||
it('审批点岗位不存在即拦;活动点没有岗位可核', () => {
|
||||
const withApproval = pkg({
|
||||
nodes: [
|
||||
...pkg().nodes,
|
||||
{
|
||||
id: 'AP' as never,
|
||||
kind: 'approval',
|
||||
label: '审批',
|
||||
approver: { position: 'GONE', strategy: 'sole' },
|
||||
reject: { kind: 'terminate' },
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
...pkg().edges.filter((e) => e.id !== 'e2'),
|
||||
{ id: 'e2', from: 'A4' as never, to: 'AP' as never, mapping: [], reopen: false },
|
||||
{ id: 'e3', from: 'AP' as never, to: 'end' as never, mapping: [], reopen: false },
|
||||
],
|
||||
});
|
||||
const violations = checkClosure({ pkg: withApproval, contracts, positions }).violations;
|
||||
expect(violations.filter((v) => v.kind === 'missing-owner-position').map((v) => v.nodeId)).toEqual(['AP']);
|
||||
|
||||
// 活动节点引用的岗位集合再小也与它无关 —— 不产生岗位类违规
|
||||
const noPositions = checkClosure({ pkg: pkg(), contracts, positions: new Set() }).violations;
|
||||
expect(noPositions.filter((v) => v.kind === 'missing-owner-position')).toEqual([]);
|
||||
});
|
||||
|
||||
it('孤立节点即拦', () => {
|
||||
const orphan = pkg({
|
||||
@@ -140,7 +113,7 @@ describe('checkClosure —— 不闭合不得激活', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
const violations = checkClosure({ pkg: orphan, contracts, positions }).violations;
|
||||
const violations = checkClosure({ pkg: orphan, contracts }).violations;
|
||||
expect(violations.filter((v) => v.kind === 'dangling-node').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -154,7 +127,7 @@ describe('checkClosure —— 不闭合不得激活', () => {
|
||||
{ id: 'e9', from: 'A4' as never, to: 'end2' as never, mapping: [], reopen: false },
|
||||
],
|
||||
});
|
||||
const violations = checkClosure({ pkg: twoDefaults, contracts, positions }).violations;
|
||||
const violations = checkClosure({ pkg: twoDefaults, contracts }).violations;
|
||||
expect(violations.filter((v) => v.kind === 'multi-default-edge').map((v) => v.nodeId)).toEqual(['A4']);
|
||||
|
||||
// 其中一条补上条件即闭合 —— 条件命中优先,无条件边只作默认兜底
|
||||
@@ -163,7 +136,7 @@ describe('checkClosure —— 不闭合不得激活', () => {
|
||||
edges: twoDefaults.edges.map((e) => (e.id === 'e9' ? { ...e, condition: 'ok = true' } : e)),
|
||||
});
|
||||
expect(
|
||||
checkClosure({ pkg: oneDefault, contracts, positions }).violations.filter((v) => v.kind === 'multi-default-edge'),
|
||||
checkClosure({ pkg: oneDefault, contracts }).violations.filter((v) => v.kind === 'multi-default-edge'),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -202,7 +175,7 @@ describe('checkClosure —— 不闭合不得激活', () => {
|
||||
{ id: 'e2', from: 'A4' as never, to: 'end' as never, mapping: [], reopen: false },
|
||||
],
|
||||
});
|
||||
const violations = checkClosure({ pkg: tolerant, contracts, positions }).violations;
|
||||
const violations = checkClosure({ pkg: tolerant, contracts }).violations;
|
||||
expect(violations.map((v) => v.kind)).toContain('tolerate-required-dependency');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user