@longzai-intelligence-workflow/core 0.0.2 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-lint.log +1305 -0
- package/.turbo/turbo-typecheck.log +12 -0
- package/CHANGELOG.md +18 -0
- package/coverage/.lcov.info.0e095d78ac9192b0.tmp +329 -0
- package/coverage/.lcov.info.8f2985e5b3ad22cc.tmp +329 -0
- package/coverage/.lcov.info.a6ce2b7c3df2438b.tmp +228 -0
- package/coverage/.lcov.info.cd0e8c028b2d5097.tmp +329 -0
- package/coverage/.lcov.info.fbb8af17757faa2d.tmp +329 -0
- package/coverage/lcov.info +28 -28
- package/dist/index.d.ts +1 -0
- package/docs/overview.md +1 -1
- package/package.json +1 -1
- package/src/__tests__/unit/constants.test.ts +75 -30
- package/src/__tests__/unit/utils.test.ts +110 -38
- package/src/__tests__/work-item-rules.test.ts +18 -5
- package/src/index.ts +19 -5
- package/src/ports/action-executor.port.ts +2 -1
- package/src/ports/feed-event.port.ts +1 -1
- package/src/ports/index.ts +1 -0
- package/src/schemas/action-executor.schema.ts +2 -1
- package/src/schemas/definition.schema.ts +1 -1
- package/src/schemas/feed-event-trigger.schema.ts +1 -1
- package/src/schemas/index.ts +7 -0
- package/src/schemas/instance.schema.ts +1 -1
- package/src/schemas/monitoring.schema.ts +1 -1
- package/src/schemas/trigger.schema.ts +1 -1
- package/src/schemas/workflow-doc.schema.ts +4 -2
- package/src/types/action-executor.ts +1 -1
- package/src/types/feed-event-trigger.ts +1 -1
- package/src/types/index.ts +5 -0
- package/src/utils/index.ts +3 -1
- package/src/utils/work-item-rules.utils.ts +25 -6
- package/tsconfig/.cache/build.tsbuildinfo +1 -1
- package/tsconfig/.cache/node.tsbuildinfo +1 -1
- package/tsconfig/.cache/test.tsbuildinfo +1 -1
- package/tsconfig/node.json +1 -1
- package/tsconfig.json +9 -1
|
@@ -1,69 +1,99 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test';
|
|
2
2
|
|
|
3
|
-
import { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE } from '
|
|
4
|
-
import { formatDate, getPagination, getTotalPages } from '
|
|
3
|
+
import { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE } from '@/constants/workflow';
|
|
4
|
+
import { formatDate, getPagination, getTotalPages } from '@/utils/index';
|
|
5
5
|
|
|
6
6
|
describe('getPagination', () => {
|
|
7
7
|
it('默认参数返回 offset=0, limit=DEFAULT_PAGE_SIZE', () => {
|
|
8
|
-
|
|
8
|
+
/*
|
|
9
|
+
* 不传参直接调用 getPagination 取得分页结果
|
|
10
|
+
*/
|
|
9
11
|
const result = getPagination();
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
/*
|
|
14
|
+
* 断言默认偏移为 0 且条数取默认分页大小
|
|
15
|
+
*/
|
|
12
16
|
expect(result.offset).toBe(0);
|
|
13
17
|
expect(result.limit).toBe(DEFAULT_PAGE_SIZE);
|
|
14
18
|
});
|
|
15
19
|
|
|
16
20
|
it('page=2, pageSize=10 返回 offset=10, limit=10', () => {
|
|
17
|
-
|
|
21
|
+
/*
|
|
22
|
+
* 以第二页与每页十条的参数调用 getPagination
|
|
23
|
+
*/
|
|
18
24
|
const result = getPagination(2, 10);
|
|
19
25
|
|
|
20
|
-
|
|
26
|
+
/*
|
|
27
|
+
* 断言偏移跳过第一页且条数原样返回
|
|
28
|
+
*/
|
|
21
29
|
expect(result.offset).toBe(10);
|
|
22
30
|
expect(result.limit).toBe(10);
|
|
23
31
|
});
|
|
24
32
|
|
|
25
33
|
it('pageSize 超过 MAX_PAGE_SIZE 时限制为 MAX_PAGE_SIZE', () => {
|
|
26
|
-
|
|
34
|
+
/*
|
|
35
|
+
* 构造超出 MAX_PAGE_SIZE 的每页条数
|
|
36
|
+
*/
|
|
27
37
|
const oversizedPageSize = MAX_PAGE_SIZE + 50;
|
|
28
38
|
|
|
29
|
-
|
|
39
|
+
/*
|
|
40
|
+
* 将超限条数传入 getPagination
|
|
41
|
+
*/
|
|
30
42
|
const result = getPagination(1, oversizedPageSize);
|
|
31
43
|
|
|
32
|
-
|
|
44
|
+
/*
|
|
45
|
+
* 断言条数被收口到 MAX_PAGE_SIZE
|
|
46
|
+
*/
|
|
33
47
|
expect(result.limit).toBe(MAX_PAGE_SIZE);
|
|
34
48
|
});
|
|
35
49
|
|
|
36
50
|
it('page=0 时默认为 page=1', () => {
|
|
37
|
-
|
|
51
|
+
/*
|
|
52
|
+
* 以零页码调用 getPagination
|
|
53
|
+
*/
|
|
38
54
|
const result = getPagination(0, 10);
|
|
39
55
|
|
|
40
|
-
|
|
56
|
+
/*
|
|
57
|
+
* 断言零页码被归位到首页偏移
|
|
58
|
+
*/
|
|
41
59
|
expect(result.offset).toBe(0);
|
|
42
60
|
expect(result.limit).toBe(10);
|
|
43
61
|
});
|
|
44
62
|
|
|
45
63
|
it('page 为负数时默认为 page=1', () => {
|
|
46
|
-
|
|
64
|
+
/*
|
|
65
|
+
* 以负数页码调用 getPagination
|
|
66
|
+
*/
|
|
47
67
|
const result = getPagination(-1, 10);
|
|
48
68
|
|
|
49
|
-
|
|
69
|
+
/*
|
|
70
|
+
* 断言负页码同样回落到首页偏移
|
|
71
|
+
*/
|
|
50
72
|
expect(result.offset).toBe(0);
|
|
51
73
|
});
|
|
52
74
|
|
|
53
75
|
it('仅传入 page 时 pageSize 使用默认值', () => {
|
|
54
|
-
|
|
76
|
+
/*
|
|
77
|
+
* 只传页码而省略每页条数调用 getPagination
|
|
78
|
+
*/
|
|
55
79
|
const result = getPagination(3);
|
|
56
80
|
|
|
57
|
-
|
|
81
|
+
/*
|
|
82
|
+
* 断言偏移按默认条数折算且条数取默认值
|
|
83
|
+
*/
|
|
58
84
|
expect(result.offset).toBe((3 - 1) * DEFAULT_PAGE_SIZE);
|
|
59
85
|
expect(result.limit).toBe(DEFAULT_PAGE_SIZE);
|
|
60
86
|
});
|
|
61
87
|
|
|
62
88
|
it('仅传入 pageSize 时 page 默认为 1', () => {
|
|
63
|
-
|
|
89
|
+
/*
|
|
90
|
+
* 页码缺省而只传每页条数调用 getPagination
|
|
91
|
+
*/
|
|
64
92
|
const result = getPagination(undefined, 15);
|
|
65
93
|
|
|
66
|
-
|
|
94
|
+
/*
|
|
95
|
+
* 断言首页偏移为零且条数原样保留
|
|
96
|
+
*/
|
|
67
97
|
expect(result.offset).toBe(0);
|
|
68
98
|
expect(result.limit).toBe(15);
|
|
69
99
|
});
|
|
@@ -71,86 +101,128 @@ describe('getPagination', () => {
|
|
|
71
101
|
|
|
72
102
|
describe('getTotalPages', () => {
|
|
73
103
|
it('正确计算总页数', () => {
|
|
74
|
-
|
|
104
|
+
/*
|
|
105
|
+
* 准备总数 45 与每页 10 条的输入
|
|
106
|
+
*/
|
|
75
107
|
const total = 45;
|
|
76
108
|
const pageSize = 10;
|
|
77
109
|
|
|
78
|
-
|
|
110
|
+
/*
|
|
111
|
+
* 用该输入计算总页数
|
|
112
|
+
*/
|
|
79
113
|
const result = getTotalPages(total, pageSize);
|
|
80
114
|
|
|
81
|
-
|
|
115
|
+
/*
|
|
116
|
+
* 断言有余数时向上取整为 5 页
|
|
117
|
+
*/
|
|
82
118
|
expect(result).toBe(5);
|
|
83
119
|
});
|
|
84
120
|
|
|
85
121
|
it('总数恰好整除时返回精确页数', () => {
|
|
86
|
-
|
|
122
|
+
/*
|
|
123
|
+
* 准备总数 40 与每页 10 条的整除输入
|
|
124
|
+
*/
|
|
87
125
|
const total = 40;
|
|
88
126
|
const pageSize = 10;
|
|
89
127
|
|
|
90
|
-
|
|
128
|
+
/*
|
|
129
|
+
* 用该输入计算总页数
|
|
130
|
+
*/
|
|
91
131
|
const result = getTotalPages(total, pageSize);
|
|
92
132
|
|
|
93
|
-
|
|
133
|
+
/*
|
|
134
|
+
* 断言整除时返回精确的 4 页
|
|
135
|
+
*/
|
|
94
136
|
expect(result).toBe(4);
|
|
95
137
|
});
|
|
96
138
|
|
|
97
139
|
it('总数为 0 时返回 0', () => {
|
|
98
|
-
|
|
140
|
+
/*
|
|
141
|
+
* 准备总数为 0 的空集输入
|
|
142
|
+
*/
|
|
99
143
|
const total = 0;
|
|
100
144
|
const pageSize = 10;
|
|
101
145
|
|
|
102
|
-
|
|
146
|
+
/*
|
|
147
|
+
* 用该输入计算总页数
|
|
148
|
+
*/
|
|
103
149
|
const result = getTotalPages(total, pageSize);
|
|
104
150
|
|
|
105
|
-
|
|
151
|
+
/*
|
|
152
|
+
* 断言空集时总页数为 0
|
|
153
|
+
*/
|
|
106
154
|
expect(result).toBe(0);
|
|
107
155
|
});
|
|
108
156
|
|
|
109
157
|
it('总数为 1 时返回 1', () => {
|
|
110
|
-
|
|
158
|
+
/*
|
|
159
|
+
* 准备总数为 1 的单条输入
|
|
160
|
+
*/
|
|
111
161
|
const total = 1;
|
|
112
162
|
const pageSize = 10;
|
|
113
163
|
|
|
114
|
-
|
|
164
|
+
/*
|
|
165
|
+
* 用该输入计算总页数
|
|
166
|
+
*/
|
|
115
167
|
const result = getTotalPages(total, pageSize);
|
|
116
168
|
|
|
117
|
-
|
|
169
|
+
/*
|
|
170
|
+
* 断言单条数据占 1 页
|
|
171
|
+
*/
|
|
118
172
|
expect(result).toBe(1);
|
|
119
173
|
});
|
|
120
174
|
});
|
|
121
175
|
|
|
122
176
|
describe('formatDate', () => {
|
|
123
177
|
it('将 Date 对象转换为 ISO 字符串', () => {
|
|
124
|
-
|
|
178
|
+
/*
|
|
179
|
+
* 构造固定时刻的日期对象
|
|
180
|
+
*/
|
|
125
181
|
const date = new Date('2025-01-15T10:30:00.000Z');
|
|
126
182
|
|
|
127
|
-
|
|
183
|
+
/*
|
|
184
|
+
* 将日期对象交给 formatDate 转换
|
|
185
|
+
*/
|
|
128
186
|
const result = formatDate(date);
|
|
129
187
|
|
|
130
|
-
|
|
188
|
+
/*
|
|
189
|
+
* 断言输出与输入的 ISO 表示逐字符一致
|
|
190
|
+
*/
|
|
131
191
|
expect(result).toBe('2025-01-15T10:30:00.000Z');
|
|
132
192
|
});
|
|
133
193
|
|
|
134
194
|
it('将日期字符串转换为 ISO 字符串', () => {
|
|
135
|
-
|
|
195
|
+
/*
|
|
196
|
+
* 准备纯日期字符串输入
|
|
197
|
+
*/
|
|
136
198
|
const dateStr = '2025-06-01';
|
|
137
199
|
|
|
138
|
-
|
|
200
|
+
/*
|
|
201
|
+
* 把字符串交给 formatDate 转换
|
|
202
|
+
*/
|
|
139
203
|
const result = formatDate(dateStr);
|
|
140
204
|
|
|
141
|
-
|
|
205
|
+
/*
|
|
206
|
+
* 断言输出包含原日期且类型为字符串
|
|
207
|
+
*/
|
|
142
208
|
expect(result).toContain('2025-06-01');
|
|
143
209
|
expect(typeof result).toBe('string');
|
|
144
210
|
});
|
|
145
211
|
|
|
146
212
|
it('返回值为有效的 ISO 格式', () => {
|
|
147
|
-
|
|
213
|
+
/*
|
|
214
|
+
* 取当前时刻构造日期对象
|
|
215
|
+
*/
|
|
148
216
|
const date = new Date();
|
|
149
217
|
|
|
150
|
-
|
|
218
|
+
/*
|
|
219
|
+
* 将当前日期交给 formatDate 转换
|
|
220
|
+
*/
|
|
151
221
|
const result = formatDate(date);
|
|
152
222
|
|
|
153
|
-
|
|
223
|
+
/*
|
|
224
|
+
* 断言输出经日期还原后与自身一致
|
|
225
|
+
*/
|
|
154
226
|
expect(new Date(result).toISOString()).toBe(result);
|
|
155
227
|
});
|
|
156
228
|
});
|
|
@@ -3,8 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { describe, expect, test } from 'bun:test';
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
parseGateLine,
|
|
8
|
+
parseItemLine,
|
|
9
|
+
type WorkflowEdge,
|
|
10
|
+
type WorkflowGate,
|
|
11
|
+
type WorkflowItem,
|
|
12
|
+
} from '@/schemas/workflow-doc.schema';
|
|
13
|
+
import { checkStatusTransition, deriveWorkflowView } from '@/utils/work-item-rules.utils';
|
|
8
14
|
|
|
9
15
|
describe('声明方言四段制解析', () => {
|
|
10
16
|
test('工作项行:状态段执法', () => {
|
|
@@ -19,9 +25,13 @@ describe('声明方言四段制解析', () => {
|
|
|
19
25
|
});
|
|
20
26
|
|
|
21
27
|
test('裁定门行:裁定段四形态执法', () => {
|
|
22
|
-
expect(parseGateLine('t-013 | ruling | 载体指示待裁 | pending')?.ruling).toEqual({
|
|
28
|
+
expect(parseGateLine('t-013 | ruling | 载体指示待裁 | pending')?.ruling).toEqual({
|
|
29
|
+
status: 'pending',
|
|
30
|
+
});
|
|
23
31
|
expect(parseGateLine('workflow | ruling | TE 悬置 | pending')?.at).toBe('workflow');
|
|
24
|
-
expect(
|
|
32
|
+
expect(
|
|
33
|
+
parseGateLine('t-013 | ruling | 载体指示待裁 | approved:user:decisions/019.md')?.ruling,
|
|
34
|
+
).toEqual({
|
|
25
35
|
status: 'approved',
|
|
26
36
|
decidedBy: 'user',
|
|
27
37
|
evidence: 'decisions/019.md',
|
|
@@ -68,7 +78,10 @@ describe('视图派生(依赖门控与未决裁定)', () => {
|
|
|
68
78
|
expect(byId.get('b')?.blockedOnPrerequisite).toBe(true);
|
|
69
79
|
expect(byId.get('b')?.canStart).toBe(false);
|
|
70
80
|
expect(byId.get('c')?.canStart).toBe(false);
|
|
71
|
-
expect(view.pendingRulings.map((ruling) => ruling.key)).toEqual([
|
|
81
|
+
expect(view.pendingRulings.map((ruling) => ruling.key)).toEqual([
|
|
82
|
+
'b:ruling',
|
|
83
|
+
'workflow:ruling',
|
|
84
|
+
]);
|
|
72
85
|
});
|
|
73
86
|
|
|
74
87
|
test('前置闭环解除阻塞;门批准解除门控;escalated 仍属未决', () => {
|
package/src/index.ts
CHANGED
|
@@ -4,13 +4,27 @@
|
|
|
4
4
|
* 提供共享基础类型、Schema、常量和工具函数。
|
|
5
5
|
* 不依赖任何 domain 包。
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
/*
|
|
8
|
+
* 类型导出
|
|
9
|
+
*/
|
|
8
10
|
export * from './types/index';
|
|
9
|
-
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* 常量导出
|
|
14
|
+
*/
|
|
10
15
|
export * from './constants/index';
|
|
11
|
-
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* 工具函数导出
|
|
19
|
+
*/
|
|
12
20
|
export * from './utils/index';
|
|
13
|
-
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* zod schema 导出
|
|
24
|
+
*/
|
|
14
25
|
export * from './schemas/index';
|
|
15
|
-
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* 端口契约导出(C1 合规,零框架依赖)
|
|
29
|
+
*/
|
|
16
30
|
export * from './ports/index';
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* 当工作流执行到 action 节点时,根据 action.type 调用对应 handler。
|
|
7
7
|
*
|
|
8
8
|
* 支持的 action 类型:
|
|
9
|
+
*
|
|
9
10
|
* - feed.applyTags:调用 feed F-2 applyTags(Phase 2)
|
|
10
11
|
* - feed.summarize:调用 feed F-3 summarize(Phase 2)
|
|
11
12
|
* - llm.complete:调用 agent LlmCompletePort
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
* C1 合规:本端口为零框架依赖的纯类型契约,由具体适配器层提供实现。
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
|
-
import type { Action, ActionContext, ActionResult, ActionType } from '
|
|
20
|
+
import type { Action, ActionContext, ActionResult, ActionType } from '@/types';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* 单个 action handler 函数签名
|
package/src/ports/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* 供 execution 子插件注册 action handler 时使用。
|
|
6
6
|
*
|
|
7
7
|
* 支持的 action 类型:
|
|
8
|
+
*
|
|
8
9
|
* - feed.applyTags:调用 feed F-2 applyTags,为条目打标签
|
|
9
10
|
* - feed.summarize:调用 feed F-3 summarize,生成摘要
|
|
10
11
|
* - llm.complete:调用 agent LlmCompletePort,完成文本
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
|
|
16
17
|
import { z } from 'zod';
|
|
17
18
|
|
|
18
|
-
import { ACTION_TYPES } from '
|
|
19
|
+
import { ACTION_TYPES } from '@/constants/workflow';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* action 类型 Schema
|
package/src/schemas/index.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
export * from './definition.schema';
|
|
2
|
+
|
|
2
3
|
export * from './workflow-doc.schema';
|
|
4
|
+
|
|
3
5
|
export * from './instance.schema';
|
|
6
|
+
|
|
4
7
|
export * from './trigger.schema';
|
|
8
|
+
|
|
5
9
|
export * from './monitoring.schema';
|
|
10
|
+
|
|
6
11
|
export * from './feed-event-trigger.schema';
|
|
12
|
+
|
|
7
13
|
export * from './action-executor.schema';
|
|
14
|
+
|
|
8
15
|
export * from './workflow-doc.schema';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
import { INSTANCE_STATUSES, NODE_EXECUTION_STATUSES } from '
|
|
3
|
+
import { INSTANCE_STATUSES, NODE_EXECUTION_STATUSES } from '@/constants/workflow';
|
|
4
4
|
|
|
5
5
|
export const instanceStatusDtoSchema = z.enum(INSTANCE_STATUSES);
|
|
6
6
|
|
|
@@ -26,7 +26,8 @@ const KEBAB_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
|
26
26
|
/**
|
|
27
27
|
* 工作项指针备选:`task:<仓>/<slug>`(task 台账引用)或 `ref:<仓>/<仓内路径>`
|
|
28
28
|
*/
|
|
29
|
-
const ITEM_REF_ALTERNATION =
|
|
29
|
+
const ITEM_REF_ALTERNATION =
|
|
30
|
+
'task:[a-z0-9][a-z0-9-]*\\/[a-z0-9\\/-]+|ref:[a-z0-9][a-z0-9-]*\\/[a-z0-9\\/. _-]+';
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* 工作项行:`<itemId> | <标题> | <指针> | <状态>`(四段制)
|
|
@@ -362,4 +363,5 @@ export function parseGateLine(line: string): WorkflowGate | null {
|
|
|
362
363
|
/**
|
|
363
364
|
* 家族引用(条目档 frontmatter 的 ref 字段值)
|
|
364
365
|
*/
|
|
365
|
-
export const WORKFLOW_REF_PATTERN =
|
|
366
|
+
export const WORKFLOW_REF_PATTERN =
|
|
367
|
+
/^workflow:(global|[a-z0-9][a-z0-9-]*)\/([a-z0-9]+(?:-[a-z0-9]+)*)$/;
|
package/src/types/index.ts
CHANGED
package/src/utils/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE } from '
|
|
1
|
+
import { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE } from '@/constants/workflow';
|
|
2
2
|
|
|
3
3
|
export function getPagination(page?: number, pageSize?: number): { offset: number; limit: number } {
|
|
4
4
|
const limit = Math.min(pageSize ?? DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE);
|
|
@@ -15,5 +15,7 @@ export function formatDate(date: string | Date): string {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export * from './declaration-frontmatter.utils';
|
|
18
|
+
|
|
18
19
|
export * from './work-item-rules.utils';
|
|
20
|
+
|
|
19
21
|
export * from './dag.utils';
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WorkflowEdge,
|
|
3
|
+
WorkflowGate,
|
|
4
|
+
WorkflowItem,
|
|
5
|
+
WorkItemStatus,
|
|
6
|
+
} from '@/schemas/workflow-doc.schema';
|
|
7
|
+
|
|
1
8
|
/**
|
|
2
9
|
* 工作项判定核(纯函数、零 I/O——输入声明形态,输出视图与违规)
|
|
3
10
|
*
|
|
@@ -6,12 +13,12 @@
|
|
|
6
13
|
*/
|
|
7
14
|
import { dependenciesMet } from '@longzai-intelligence-dag/core';
|
|
8
15
|
|
|
9
|
-
import type { WorkflowEdge, WorkflowGate, WorkflowItem, WorkItemStatus } from '../schemas/workflow-doc.schema';
|
|
10
|
-
|
|
11
16
|
/**
|
|
12
17
|
* 工作项合法状态转移表(状态机唯一注册点;供编辑工具与判定器共用的规则真源)
|
|
13
18
|
*/
|
|
14
|
-
export const ALLOWED_STATUS_TRANSITIONS: Readonly<
|
|
19
|
+
export const ALLOWED_STATUS_TRANSITIONS: Readonly<
|
|
20
|
+
Record<WorkItemStatus, readonly WorkItemStatus[]>
|
|
21
|
+
> = {
|
|
15
22
|
todo: ['doing', 'blocked', 'dropped'],
|
|
16
23
|
doing: ['blocked', 'done', 'dropped'],
|
|
17
24
|
blocked: ['todo', 'doing', 'dropped'],
|
|
@@ -41,7 +48,10 @@ export type InvalidTransitionError = {
|
|
|
41
48
|
* @param to - 目标状态
|
|
42
49
|
* @returns 合法时为 null;非法时携带合法后继清单
|
|
43
50
|
*/
|
|
44
|
-
export function checkStatusTransition(
|
|
51
|
+
export function checkStatusTransition(
|
|
52
|
+
from: WorkItemStatus,
|
|
53
|
+
to: WorkItemStatus,
|
|
54
|
+
): InvalidTransitionError | null {
|
|
45
55
|
/**
|
|
46
56
|
* 注册表中的合法后继
|
|
47
57
|
*/
|
|
@@ -50,7 +60,10 @@ export function checkStatusTransition(from: WorkItemStatus, to: WorkItemStatus):
|
|
|
50
60
|
return { code: 'INVALID_TRANSITION', message: `未知状态:${from}` };
|
|
51
61
|
}
|
|
52
62
|
if (!allowed.includes(to)) {
|
|
53
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
code: 'INVALID_TRANSITION',
|
|
65
|
+
message: `非法转移:${from} → ${to}(合法后继:${allowed.join(' / ')})`,
|
|
66
|
+
};
|
|
54
67
|
}
|
|
55
68
|
return null;
|
|
56
69
|
}
|
|
@@ -134,6 +147,7 @@ export type WorkflowView = {
|
|
|
134
147
|
* 已闭环状态集合(依赖满足判定基准:done 或 dropped 均不再阻塞后继)
|
|
135
148
|
*
|
|
136
149
|
* @param status - 工作项状态
|
|
150
|
+
* @returns 状态为 done 或 dropped 时为 true,否则为 false
|
|
137
151
|
*/
|
|
138
152
|
function isSettled(status: WorkItemStatus): boolean {
|
|
139
153
|
return status === 'done' || status === 'dropped';
|
|
@@ -145,6 +159,7 @@ function isSettled(status: WorkItemStatus): boolean {
|
|
|
145
159
|
* @param items - 工作项清单(声明档现状)
|
|
146
160
|
* @param edges - 依赖边清单
|
|
147
161
|
* @param gates - 裁定门清单(含裁定事实)
|
|
162
|
+
* @returns 工作流声明视图,含全部工作项视图与未决裁定清单
|
|
148
163
|
*/
|
|
149
164
|
export function deriveWorkflowView(
|
|
150
165
|
items: readonly WorkflowItem[],
|
|
@@ -159,7 +174,11 @@ export function deriveWorkflowView(
|
|
|
159
174
|
/**
|
|
160
175
|
* 各工作项挂靠的未批准门(ruling.status 非 approved 即阻塞)
|
|
161
176
|
*/
|
|
162
|
-
const unapprovedGateAt = new Set(
|
|
177
|
+
const unapprovedGateAt = new Set(
|
|
178
|
+
gates
|
|
179
|
+
.filter((gate) => gate.ruling.status !== 'approved' && gate.at !== 'workflow')
|
|
180
|
+
.map((gate) => gate.at),
|
|
181
|
+
);
|
|
163
182
|
|
|
164
183
|
/**
|
|
165
184
|
* 工作项视图累积
|