@longzai-intelligence-workflow/core 0.0.1
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/CHANGELOG.md +20 -0
- package/README.md +14 -0
- package/coverage/.lcov.info.0d8fc8c4f6acaa77.tmp +159 -0
- package/coverage/.lcov.info.1ca0aee1c681607a.tmp +61 -0
- package/coverage/.lcov.info.5a9690152fb62c73.tmp +159 -0
- package/coverage/.lcov.info.984d9ad89a4b1598.tmp +12 -0
- package/coverage/.lcov.info.a607374a9831c63e.tmp +61 -0
- package/coverage/.lcov.info.b58246c9a6df7fc5.tmp +167 -0
- package/coverage/.lcov.info.b5f068b7c1d63f36.tmp +12 -0
- package/coverage/.lcov.info.bbac83a4ec8a1294.tmp +159 -0
- package/coverage/.lcov.info.bf3e6cf9d98abfc0.tmp +159 -0
- package/coverage/.lcov.info.fb2d882b78dc47d4.tmp +169 -0
- package/coverage/lcov.info +169 -0
- package/dist/index.d.ts +577 -0
- package/dist/index.js +8 -0
- package/dist/schemas-D0Zuq8_5.js +1 -0
- package/dist/schemas.d.ts +684 -0
- package/dist/schemas.js +1 -0
- package/docs/overview.md +17 -0
- package/lzi-builder.config.ts +13 -0
- package/lzi-dev.config.ts +7 -0
- package/oxlint.config.ts +6 -0
- package/package.json +38 -0
- package/src/__tests__/helpers/setup.ts +9 -0
- package/src/__tests__/unit/constants.test.ts +129 -0
- package/src/__tests__/unit/utils.test.ts +156 -0
- package/src/__tests__/work-item-rules.test.ts +90 -0
- package/src/constants/index.ts +1 -0
- package/src/constants/workflow.ts +114 -0
- package/src/index.ts +16 -0
- package/src/ports/action-executor.port.ts +73 -0
- package/src/ports/feed-event.port.ts +74 -0
- package/src/ports/index.ts +9 -0
- package/src/schemas/action-executor.schema.ts +209 -0
- package/src/schemas/api.schema.ts +11 -0
- package/src/schemas/definition.schema.ts +53 -0
- package/src/schemas/feed-event-trigger.schema.ts +102 -0
- package/src/schemas/index.ts +8 -0
- package/src/schemas/instance.schema.ts +47 -0
- package/src/schemas/monitoring.schema.ts +53 -0
- package/src/schemas/trigger.schema.ts +26 -0
- package/src/schemas/workflow-doc.schema.ts +365 -0
- package/src/types/action-executor.ts +64 -0
- package/src/types/api.ts +10 -0
- package/src/types/definition.ts +49 -0
- package/src/types/feed-event-trigger.ts +38 -0
- package/src/types/index.ts +6 -0
- package/src/types/instance.ts +49 -0
- package/src/types/monitoring.ts +54 -0
- package/src/types/trigger.ts +39 -0
- package/src/utils/dag.utils.ts +95 -0
- package/src/utils/declaration-frontmatter.utils.ts +138 -0
- package/src/utils/index.ts +19 -0
- package/src/utils/work-item-rules.utils.ts +194 -0
- package/tsconfig/.cache/build.tsbuildinfo +1 -0
- package/tsconfig/.cache/node.tsbuildinfo +1 -0
- package/tsconfig/.cache/test.tsbuildinfo +1 -0
- package/tsconfig/app.json +12 -0
- package/tsconfig/build.json +15 -0
- package/tsconfig/node.json +12 -0
- package/tsconfig/test.json +15 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DAG 机件(环检测 + 拓扑分层 + 依赖满足判定)
|
|
3
|
+
*
|
|
4
|
+
* 共享基础(设计对齐 scheduler workflow 域已验证机件;真源归本包,禁止再镜像)。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 依赖边(from 完成后 to 可启)
|
|
9
|
+
*/
|
|
10
|
+
export type DependencyEdge = { readonly from: string; readonly to: string };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 环检测(DFS 三色标记;返回首个环路径,无环为 null)
|
|
14
|
+
*
|
|
15
|
+
* @param edges - 依赖边集合
|
|
16
|
+
*/
|
|
17
|
+
export function detectCycle(edges: readonly DependencyEdge[]): readonly string[] | null {
|
|
18
|
+
/**
|
|
19
|
+
* 邻接表
|
|
20
|
+
*/
|
|
21
|
+
const adjacency = new Map<string, string[]>();
|
|
22
|
+
for (const edge of edges) {
|
|
23
|
+
adjacency.set(edge.from, [...(adjacency.get(edge.from) ?? []), edge.to]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 节点访问状态(0=未访问 1=在栈 2=完成)
|
|
28
|
+
*/
|
|
29
|
+
const state = new Map<string, 0 | 1 | 2>();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 当前 DFS 路径
|
|
33
|
+
*/
|
|
34
|
+
const path: string[] = [];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 单节点深搜(返回环路径或 null)
|
|
38
|
+
*/
|
|
39
|
+
const visit = (node: string): readonly string[] | null => {
|
|
40
|
+
/**
|
|
41
|
+
* 当前节点状态
|
|
42
|
+
*/
|
|
43
|
+
const current = state.get(node) ?? 0;
|
|
44
|
+
if (current === 1) {
|
|
45
|
+
/**
|
|
46
|
+
* 环路径:当前路径中该节点之后的部分 + 首尾相接
|
|
47
|
+
*/
|
|
48
|
+
const start = path.indexOf(node);
|
|
49
|
+
return [...path.slice(start), node];
|
|
50
|
+
}
|
|
51
|
+
if (current === 2) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
state.set(node, 1);
|
|
55
|
+
path.push(node);
|
|
56
|
+
for (const next of adjacency.get(node) ?? []) {
|
|
57
|
+
/**
|
|
58
|
+
* 后继环路径
|
|
59
|
+
*/
|
|
60
|
+
const cycle = visit(next);
|
|
61
|
+
if (cycle !== null) {
|
|
62
|
+
return cycle;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
path.pop();
|
|
66
|
+
state.set(node, 2);
|
|
67
|
+
return null;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 全部节点(确定性排序)
|
|
72
|
+
*/
|
|
73
|
+
const nodes = [...new Set(edges.flatMap((edge) => [edge.from, edge.to]))].sort((left, right) => (left < right ? -1 : 1));
|
|
74
|
+
for (const node of nodes) {
|
|
75
|
+
/**
|
|
76
|
+
* 各节点环检测
|
|
77
|
+
*/
|
|
78
|
+
const cycle = visit(node);
|
|
79
|
+
if (cycle !== null) {
|
|
80
|
+
return cycle;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 依赖满足判定(全部前置达完成/验收才算满足;无前置视为满足)
|
|
88
|
+
*
|
|
89
|
+
* @param node - 目标工作项 id
|
|
90
|
+
* @param edges - 依赖边集合
|
|
91
|
+
* @param settled - 已闭环(完成/验收)的工作项集合
|
|
92
|
+
*/
|
|
93
|
+
export function dependenciesMet(node: string, edges: readonly DependencyEdge[], settled: ReadonlySet<string>): boolean {
|
|
94
|
+
return edges.filter((edge) => edge.to === node).every((edge) => settled.has(edge.from));
|
|
95
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 声明档 frontmatter 解析(极简 YAML 方言 + 字符串数组扩展;自足实现零外部依赖)
|
|
3
|
+
*
|
|
4
|
+
* 方言与 data-engine stringifyFrontmatter 同源:顶层标量 + 字符串数组
|
|
5
|
+
* (`key:` 后随 ` - item` 行);数组/嵌套之外的结构不在声明档契约内。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 声明档解析结果(frontmatter 标量与字符串数组 + 正文)
|
|
10
|
+
*/
|
|
11
|
+
export type ParsedDeclaration = {
|
|
12
|
+
/**
|
|
13
|
+
* 顶层标量键值
|
|
14
|
+
*/
|
|
15
|
+
readonly head: Record<string, string>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 字符串数组键值(空数组键不出现)
|
|
19
|
+
*/
|
|
20
|
+
readonly arrays: Record<string, readonly string[]>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 正文(frontmatter 结束标记之后全文)
|
|
24
|
+
*/
|
|
25
|
+
readonly body: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 剥标量引号(与 data-engine parseFrontmatterHead 同规则)
|
|
30
|
+
*
|
|
31
|
+
* @param raw - 原始值文本
|
|
32
|
+
* @returns 剥引号后的值
|
|
33
|
+
*/
|
|
34
|
+
function unquoteScalar(raw: string): string {
|
|
35
|
+
/**
|
|
36
|
+
* 首尾成对双引号判定后的剥壳结果
|
|
37
|
+
*/
|
|
38
|
+
const unquoted =
|
|
39
|
+
raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"') ? raw.slice(1, -1) : raw;
|
|
40
|
+
|
|
41
|
+
return unquoted;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 提取正文(frontmatter 结束标记之后全文;无 frontmatter 时为原文)
|
|
46
|
+
*
|
|
47
|
+
* @param documentText - 文档全文
|
|
48
|
+
* @returns 正文文本
|
|
49
|
+
*/
|
|
50
|
+
function extractBody(documentText: string): string {
|
|
51
|
+
if (!documentText.startsWith('---\n')) {
|
|
52
|
+
return documentText;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* frontmatter 结束标记位置
|
|
57
|
+
*/
|
|
58
|
+
const end = documentText.indexOf('\n---\n', 4);
|
|
59
|
+
if (end === -1) {
|
|
60
|
+
return documentText;
|
|
61
|
+
}
|
|
62
|
+
return documentText.slice(end + '\n---\n'.length);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 解析声明档(frontmatter 标量 + 字符串数组;非法行跳过)
|
|
67
|
+
*
|
|
68
|
+
* @param documentText - 文档全文
|
|
69
|
+
* @returns 解析结果(无 frontmatter 时 head/arrays 为空对象、body 为全文)
|
|
70
|
+
*/
|
|
71
|
+
export function parseDeclaration(documentText: string): ParsedDeclaration {
|
|
72
|
+
/**
|
|
73
|
+
* 顶层标量键值累积
|
|
74
|
+
*/
|
|
75
|
+
const head: Record<string, string> = {};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 字符串数组键值累积
|
|
79
|
+
*/
|
|
80
|
+
const arrays: Record<string, readonly string[]> = {};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 正文(自足提取,零外部依赖)
|
|
84
|
+
*/
|
|
85
|
+
const body = extractBody(documentText);
|
|
86
|
+
if (!documentText.startsWith('---\n')) {
|
|
87
|
+
return { head, arrays, body };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* frontmatter 结束标记位置
|
|
92
|
+
*/
|
|
93
|
+
const end = documentText.indexOf('\n---\n', 4);
|
|
94
|
+
if (end === -1) {
|
|
95
|
+
return { head, arrays, body };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* frontmatter 头部行序列
|
|
100
|
+
*/
|
|
101
|
+
const lines = documentText.slice(4, end).split('\n');
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 当前数组键(最近一个 `key:` 或 `key: []` 行;非数组项行复位为 null)
|
|
105
|
+
*/
|
|
106
|
+
let currentArrayKey: string | null = null;
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
if (line.startsWith(' - ') && currentArrayKey !== null) {
|
|
109
|
+
/**
|
|
110
|
+
* 数组项文本(剥引号)
|
|
111
|
+
*/
|
|
112
|
+
const item = unquoteScalar(line.slice(4).trim());
|
|
113
|
+
arrays[currentArrayKey] = [...(arrays[currentArrayKey] ?? []), item];
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
currentArrayKey = null;
|
|
117
|
+
if (line.endsWith(': []')) {
|
|
118
|
+
currentArrayKey = line.slice(0, -4);
|
|
119
|
+
arrays[currentArrayKey] = [];
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (line.endsWith(':')) {
|
|
123
|
+
currentArrayKey = line.slice(0, -1);
|
|
124
|
+
arrays[currentArrayKey] = [];
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 键值分隔符位置
|
|
130
|
+
*/
|
|
131
|
+
const separator = line.indexOf(': ');
|
|
132
|
+
if (separator > 0 && !line.startsWith(' ')) {
|
|
133
|
+
head[line.slice(0, separator)] = unquoteScalar(line.slice(separator + 2));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { head, arrays, body };
|
|
138
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE } from '../constants/workflow';
|
|
2
|
+
|
|
3
|
+
export function getPagination(page?: number, pageSize?: number): { offset: number; limit: number } {
|
|
4
|
+
const limit = Math.min(pageSize ?? DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE);
|
|
5
|
+
const offset = (Math.max(page ?? 1, 1) - 1) * limit;
|
|
6
|
+
return { offset, limit };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getTotalPages(total: number, pageSize: number): number {
|
|
10
|
+
return Math.ceil(total / pageSize);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatDate(date: string | Date): string {
|
|
14
|
+
return new Date(date).toISOString();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export * from './declaration-frontmatter.utils';
|
|
18
|
+
export * from './dag.utils';
|
|
19
|
+
export * from './work-item-rules.utils';
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工作项判定核(纯函数、零 I/O——输入声明形态,输出视图与违规)
|
|
3
|
+
*
|
|
4
|
+
* 判定器契约:只判不动(对齐家族 verify 模式);执行状态归声明档由属主维护,
|
|
5
|
+
* 本模块不做任何状态写入,仅依据声明现状派生视图与转移合法性。
|
|
6
|
+
*/
|
|
7
|
+
import type { WorkflowEdge, WorkflowGate, WorkflowItem, WorkItemStatus } from '../schemas/workflow-doc.schema';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 工作项合法状态转移表(状态机唯一注册点;供编辑工具与判定器共用的规则真源)
|
|
11
|
+
*/
|
|
12
|
+
export const ALLOWED_STATUS_TRANSITIONS: Readonly<Record<WorkItemStatus, readonly WorkItemStatus[]>> = {
|
|
13
|
+
todo: ['doing', 'blocked', 'dropped'],
|
|
14
|
+
doing: ['blocked', 'done', 'dropped'],
|
|
15
|
+
blocked: ['todo', 'doing', 'dropped'],
|
|
16
|
+
done: ['todo', 'dropped'],
|
|
17
|
+
dropped: ['todo'],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 非法转移错误形态
|
|
22
|
+
*/
|
|
23
|
+
export type InvalidTransitionError = {
|
|
24
|
+
/**
|
|
25
|
+
* 错误码
|
|
26
|
+
*/
|
|
27
|
+
readonly code: 'INVALID_TRANSITION';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 人读信息(含合法后继清单)
|
|
31
|
+
*/
|
|
32
|
+
readonly message: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 判定状态转移合法性(注册表执法)
|
|
37
|
+
*
|
|
38
|
+
* @param from - 当前状态
|
|
39
|
+
* @param to - 目标状态
|
|
40
|
+
* @returns 合法时为 null;非法时携带合法后继清单
|
|
41
|
+
*/
|
|
42
|
+
export function checkStatusTransition(from: WorkItemStatus, to: WorkItemStatus): InvalidTransitionError | null {
|
|
43
|
+
/**
|
|
44
|
+
* 注册表中的合法后继
|
|
45
|
+
*/
|
|
46
|
+
const allowed = ALLOWED_STATUS_TRANSITIONS[from];
|
|
47
|
+
if (allowed === undefined) {
|
|
48
|
+
return { code: 'INVALID_TRANSITION', message: `未知状态:${from}` };
|
|
49
|
+
}
|
|
50
|
+
if (!allowed.includes(to)) {
|
|
51
|
+
return { code: 'INVALID_TRANSITION', message: `非法转移:${from} → ${to}(合法后继:${allowed.join(' / ')})` };
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 工作项视图(快照派生:依赖满足与可启动判定)
|
|
58
|
+
*/
|
|
59
|
+
export type WorkItemView = {
|
|
60
|
+
/**
|
|
61
|
+
* 工作项 id
|
|
62
|
+
*/
|
|
63
|
+
readonly id: string;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 执行状态(声明档现状)
|
|
67
|
+
*/
|
|
68
|
+
readonly status: WorkItemStatus;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 依赖是否满足(全部前置达 done/dropped)
|
|
72
|
+
*/
|
|
73
|
+
readonly depsMet: boolean;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 派生标注:todo 且依赖未满足
|
|
77
|
+
*/
|
|
78
|
+
readonly blockedOnPrerequisite: boolean;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 现在是否可启动(todo + 依赖满足 + 挂靠门已批准或无门)
|
|
82
|
+
*/
|
|
83
|
+
readonly canStart: boolean;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 未决裁定视图项
|
|
88
|
+
*/
|
|
89
|
+
export type PendingRulingView = {
|
|
90
|
+
/**
|
|
91
|
+
* 门键(at:kind)
|
|
92
|
+
*/
|
|
93
|
+
readonly key: string;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 挂靠位置
|
|
97
|
+
*/
|
|
98
|
+
readonly at: string;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 门类型
|
|
102
|
+
*/
|
|
103
|
+
readonly kind: string;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 说明
|
|
107
|
+
*/
|
|
108
|
+
readonly note: string;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 裁定状态(pending 未裁 / escalated 升级待裁)
|
|
112
|
+
*/
|
|
113
|
+
readonly rulingStatus: 'pending' | 'escalated';
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 工作流声明视图(判定器输出:全部工作项视图 + 未决裁定清单)
|
|
118
|
+
*/
|
|
119
|
+
export type WorkflowView = {
|
|
120
|
+
/**
|
|
121
|
+
* 工作项视图清单(按声明序)
|
|
122
|
+
*/
|
|
123
|
+
readonly items: readonly WorkItemView[];
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 未决裁定清单(pending 与 escalated)
|
|
127
|
+
*/
|
|
128
|
+
readonly pendingRulings: readonly PendingRulingView[];
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 已闭环状态集合(依赖满足判定基准:done 或 dropped 均不再阻塞后继)
|
|
133
|
+
*
|
|
134
|
+
* @param status - 工作项状态
|
|
135
|
+
*/
|
|
136
|
+
function isSettled(status: WorkItemStatus): boolean {
|
|
137
|
+
return status === 'done' || status === 'dropped';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 派生工作流声明视图(纯函数:声明现状 → 可启动/阻塞/未决裁定视图)
|
|
142
|
+
*
|
|
143
|
+
* @param items - 工作项清单(声明档现状)
|
|
144
|
+
* @param edges - 依赖边清单
|
|
145
|
+
* @param gates - 裁定门清单(含裁定事实)
|
|
146
|
+
*/
|
|
147
|
+
export function deriveWorkflowView(
|
|
148
|
+
items: readonly WorkflowItem[],
|
|
149
|
+
edges: readonly WorkflowEdge[],
|
|
150
|
+
gates: readonly WorkflowGate[],
|
|
151
|
+
): WorkflowView {
|
|
152
|
+
/**
|
|
153
|
+
* 已闭环工作项 id 集合
|
|
154
|
+
*/
|
|
155
|
+
const settled = new Set(items.filter((item) => isSettled(item.status)).map((item) => item.id));
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 各工作项挂靠的未批准门(ruling.status 非 approved 即阻塞)
|
|
159
|
+
*/
|
|
160
|
+
const unapprovedGateAt = new Set(gates.filter((gate) => gate.ruling.status !== 'approved' && gate.at !== 'workflow').map((gate) => gate.at));
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 工作项视图累积
|
|
164
|
+
*/
|
|
165
|
+
const views: WorkItemView[] = items.map((item) => {
|
|
166
|
+
/**
|
|
167
|
+
* 依赖满足判定(全部前置闭环)
|
|
168
|
+
*/
|
|
169
|
+
const depsMet = edges.filter((edge) => edge.to === item.id).every((edge) => settled.has(edge.from));
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
id: item.id,
|
|
173
|
+
status: item.status,
|
|
174
|
+
depsMet,
|
|
175
|
+
blockedOnPrerequisite: item.status === 'todo' && !depsMet,
|
|
176
|
+
canStart: item.status === 'todo' && depsMet && !unapprovedGateAt.has(item.id),
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 未决裁定视图累积(pending 与 escalated)
|
|
182
|
+
*/
|
|
183
|
+
const pendingRulings: PendingRulingView[] = gates
|
|
184
|
+
.filter((gate) => gate.ruling.status === 'pending' || gate.ruling.status === 'escalated')
|
|
185
|
+
.map((gate) => ({
|
|
186
|
+
key: `${gate.at}:${gate.kind}`,
|
|
187
|
+
at: gate.at,
|
|
188
|
+
kind: gate.kind,
|
|
189
|
+
note: gate.note,
|
|
190
|
+
rulingStatus: gate.ruling.status as 'pending' | 'escalated',
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
return { items: views, pendingRulings };
|
|
194
|
+
}
|