ai-workflows 2.0.2 → 2.1.3
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 +4 -5
- package/.turbo/turbo-test.log +169 -0
- package/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +303 -184
- package/dist/barrier.d.ts +153 -0
- package/dist/barrier.d.ts.map +1 -0
- package/dist/barrier.js +339 -0
- package/dist/barrier.js.map +1 -0
- package/dist/cascade-context.d.ts +149 -0
- package/dist/cascade-context.d.ts.map +1 -0
- package/dist/cascade-context.js +324 -0
- package/dist/cascade-context.js.map +1 -0
- package/dist/cascade-executor.d.ts +196 -0
- package/dist/cascade-executor.d.ts.map +1 -0
- package/dist/cascade-executor.js +384 -0
- package/dist/cascade-executor.js.map +1 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +4 -1
- package/dist/context.js.map +1 -1
- package/dist/dependency-graph.d.ts +157 -0
- package/dist/dependency-graph.d.ts.map +1 -0
- package/dist/dependency-graph.js +382 -0
- package/dist/dependency-graph.js.map +1 -0
- package/dist/every.d.ts +31 -2
- package/dist/every.d.ts.map +1 -1
- package/dist/every.js +63 -32
- package/dist/every.js.map +1 -1
- package/dist/graph/index.d.ts +8 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +8 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/topological-sort.d.ts +121 -0
- package/dist/graph/topological-sort.d.ts.map +1 -0
- package/dist/graph/topological-sort.js +292 -0
- package/dist/graph/topological-sort.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/on.d.ts +35 -10
- package/dist/on.d.ts.map +1 -1
- package/dist/on.js +52 -18
- package/dist/on.js.map +1 -1
- package/dist/send.d.ts +0 -5
- package/dist/send.d.ts.map +1 -1
- package/dist/send.js +1 -14
- package/dist/send.js.map +1 -1
- package/dist/timer-registry.d.ts +52 -0
- package/dist/timer-registry.d.ts.map +1 -0
- package/dist/timer-registry.js +120 -0
- package/dist/timer-registry.js.map +1 -0
- package/dist/types.d.ts +171 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -1
- package/dist/types.js.map +1 -1
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +22 -18
- package/dist/workflow.js.map +1 -1
- package/package.json +12 -16
- package/src/barrier.ts +466 -0
- package/src/cascade-context.ts +488 -0
- package/src/cascade-executor.ts +587 -0
- package/src/context.js +83 -0
- package/src/context.ts +12 -7
- package/src/dependency-graph.ts +518 -0
- package/src/every.js +267 -0
- package/src/every.ts +104 -35
- package/src/graph/index.ts +19 -0
- package/src/graph/topological-sort.ts +414 -0
- package/src/index.js +71 -0
- package/src/index.ts +78 -0
- package/src/on.js +79 -0
- package/src/on.ts +81 -25
- package/src/send.js +111 -0
- package/src/send.ts +1 -16
- package/src/timer-registry.ts +145 -0
- package/src/types.js +4 -0
- package/src/types.ts +218 -11
- package/src/workflow.js +455 -0
- package/src/workflow.ts +32 -23
- package/test/barrier-join.test.ts +434 -0
- package/test/barrier-unhandled-rejections.test.ts +359 -0
- package/test/cascade-context.test.ts +390 -0
- package/test/cascade-executor.test.ts +859 -0
- package/test/context.test.js +116 -0
- package/test/dependency-graph.test.ts +512 -0
- package/test/every.test.js +282 -0
- package/test/graph/topological-sort.test.ts +586 -0
- package/test/on.test.js +80 -0
- package/test/schedule-timer-cleanup.test.ts +344 -0
- package/test/send-race-conditions.test.ts +410 -0
- package/test/send.test.js +89 -0
- package/test/type-safety-every.test.ts +303 -0
- package/test/types-event-handler.test.ts +225 -0
- package/test/types-proxy-autocomplete.test.ts +345 -0
- package/test/workflow.test.js +224 -0
- package/vitest.config.js +7 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency Graph Architecture for Workflow Steps
|
|
3
|
+
*
|
|
4
|
+
* Provides a directed acyclic graph (DAG) for managing workflow step dependencies.
|
|
5
|
+
* Supports:
|
|
6
|
+
* - Hard vs soft dependencies
|
|
7
|
+
* - Cycle detection with path reporting
|
|
8
|
+
* - Parallel group identification for concurrent execution
|
|
9
|
+
* - Graph visualization (DOT, JSON)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { EventRegistration } from './types.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Dependency type: hard (must succeed) or soft (can proceed on failure)
|
|
16
|
+
*/
|
|
17
|
+
export type DependencyType = 'hard' | 'soft'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Configuration for step dependencies
|
|
21
|
+
*/
|
|
22
|
+
export interface DependencyConfig {
|
|
23
|
+
/**
|
|
24
|
+
* Step(s) that must complete before this step runs
|
|
25
|
+
* Can be a single step ID or array of step IDs
|
|
26
|
+
*/
|
|
27
|
+
dependsOn: string | string[]
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Type of dependency (default: 'hard')
|
|
31
|
+
* - 'hard': Dependency must complete successfully
|
|
32
|
+
* - 'soft': Step can proceed even if dependency fails
|
|
33
|
+
*/
|
|
34
|
+
type?: DependencyType
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A node in the dependency graph
|
|
39
|
+
*/
|
|
40
|
+
export interface GraphNode {
|
|
41
|
+
/** Unique identifier (e.g., 'Step1.complete') */
|
|
42
|
+
id: string
|
|
43
|
+
|
|
44
|
+
/** Direct dependencies (step IDs that must complete first) */
|
|
45
|
+
dependencies: string[]
|
|
46
|
+
|
|
47
|
+
/** Map of dependency ID to dependency type */
|
|
48
|
+
dependencyTypes?: Record<string, DependencyType>
|
|
49
|
+
|
|
50
|
+
/** Optional metadata */
|
|
51
|
+
metadata?: Record<string, unknown>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A group of nodes that can execute in parallel
|
|
56
|
+
*/
|
|
57
|
+
export interface ParallelGroup {
|
|
58
|
+
/** Execution level (0 = no dependencies, 1 = depends on level 0, etc.) */
|
|
59
|
+
level: number
|
|
60
|
+
|
|
61
|
+
/** Node IDs that can run concurrently at this level */
|
|
62
|
+
nodes: string[]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* JSON representation of the graph
|
|
67
|
+
*/
|
|
68
|
+
export interface GraphJSON {
|
|
69
|
+
nodes: GraphNode[]
|
|
70
|
+
edges: Array<{
|
|
71
|
+
from: string
|
|
72
|
+
to: string
|
|
73
|
+
type: DependencyType
|
|
74
|
+
}>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Error thrown when a circular dependency is detected
|
|
79
|
+
*/
|
|
80
|
+
export class CircularDependencyError extends Error {
|
|
81
|
+
/** The path of nodes forming the cycle */
|
|
82
|
+
cyclePath: string[]
|
|
83
|
+
|
|
84
|
+
constructor(cyclePath: string[]) {
|
|
85
|
+
const pathStr = cyclePath.join(' -> ')
|
|
86
|
+
super(`Circular dependency detected: ${pathStr}`)
|
|
87
|
+
this.name = 'CircularDependencyError'
|
|
88
|
+
this.cyclePath = cyclePath
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Error thrown when a referenced dependency doesn't exist
|
|
94
|
+
*/
|
|
95
|
+
export class MissingDependencyError extends Error {
|
|
96
|
+
/** The missing dependency ID */
|
|
97
|
+
dependency: string
|
|
98
|
+
|
|
99
|
+
/** The node that references the missing dependency */
|
|
100
|
+
node: string
|
|
101
|
+
|
|
102
|
+
constructor(dependency: string, node: string) {
|
|
103
|
+
super(
|
|
104
|
+
`Missing dependency '${dependency}' referenced by '${node}'. ` +
|
|
105
|
+
`Ensure '${dependency}' is added to the graph before '${node}'.`
|
|
106
|
+
)
|
|
107
|
+
this.name = 'MissingDependencyError'
|
|
108
|
+
this.dependency = dependency
|
|
109
|
+
this.node = node
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Extended event registration with optional dependencies
|
|
115
|
+
*/
|
|
116
|
+
export interface EventRegistrationWithDeps extends EventRegistration {
|
|
117
|
+
dependencies?: DependencyConfig
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Directed Acyclic Graph for workflow step dependencies
|
|
122
|
+
*/
|
|
123
|
+
export class DependencyGraph {
|
|
124
|
+
private nodes: Map<string, GraphNode> = new Map()
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create a DependencyGraph from event registrations
|
|
128
|
+
*/
|
|
129
|
+
static fromEventRegistrations(
|
|
130
|
+
registrations: EventRegistrationWithDeps[]
|
|
131
|
+
): DependencyGraph {
|
|
132
|
+
const graph = new DependencyGraph()
|
|
133
|
+
|
|
134
|
+
// First pass: add all nodes without dependencies
|
|
135
|
+
for (const reg of registrations) {
|
|
136
|
+
const nodeId = `${reg.noun}.${reg.event}`
|
|
137
|
+
if (!graph.hasNode(nodeId)) {
|
|
138
|
+
// Add as placeholder first (will be updated with deps in second pass)
|
|
139
|
+
graph.nodes.set(nodeId, {
|
|
140
|
+
id: nodeId,
|
|
141
|
+
dependencies: [],
|
|
142
|
+
dependencyTypes: {},
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Second pass: add dependencies
|
|
148
|
+
for (const reg of registrations) {
|
|
149
|
+
const nodeId = `${reg.noun}.${reg.event}`
|
|
150
|
+
if (reg.dependencies) {
|
|
151
|
+
const deps = Array.isArray(reg.dependencies.dependsOn)
|
|
152
|
+
? reg.dependencies.dependsOn
|
|
153
|
+
: [reg.dependencies.dependsOn]
|
|
154
|
+
|
|
155
|
+
const depType = reg.dependencies.type || 'hard'
|
|
156
|
+
|
|
157
|
+
for (const dep of deps) {
|
|
158
|
+
if (!graph.hasNode(dep)) {
|
|
159
|
+
throw new MissingDependencyError(dep, nodeId)
|
|
160
|
+
}
|
|
161
|
+
const node = graph.nodes.get(nodeId)!
|
|
162
|
+
if (!node.dependencies.includes(dep)) {
|
|
163
|
+
node.dependencies.push(dep)
|
|
164
|
+
node.dependencyTypes = node.dependencyTypes || {}
|
|
165
|
+
node.dependencyTypes[dep] = depType
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check for cycles after adding edges
|
|
170
|
+
const cycle = graph.detectCycles()
|
|
171
|
+
if (cycle) {
|
|
172
|
+
throw new CircularDependencyError(cycle)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return graph
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Check if a node exists
|
|
182
|
+
*/
|
|
183
|
+
hasNode(id: string): boolean {
|
|
184
|
+
return this.nodes.has(id)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Add a node to the graph
|
|
189
|
+
*/
|
|
190
|
+
addNode(id: string, config?: DependencyConfig): void {
|
|
191
|
+
// Check for self-reference
|
|
192
|
+
if (config?.dependsOn) {
|
|
193
|
+
const deps = Array.isArray(config.dependsOn)
|
|
194
|
+
? config.dependsOn
|
|
195
|
+
: [config.dependsOn]
|
|
196
|
+
|
|
197
|
+
if (deps.includes(id)) {
|
|
198
|
+
throw new CircularDependencyError([id, id])
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Verify all dependencies exist
|
|
202
|
+
for (const dep of deps) {
|
|
203
|
+
if (!this.nodes.has(dep)) {
|
|
204
|
+
throw new MissingDependencyError(dep, id)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const dependencies = config?.dependsOn
|
|
210
|
+
? Array.isArray(config.dependsOn)
|
|
211
|
+
? config.dependsOn
|
|
212
|
+
: [config.dependsOn]
|
|
213
|
+
: []
|
|
214
|
+
|
|
215
|
+
const depType = config?.type || 'hard'
|
|
216
|
+
const dependencyTypes: Record<string, DependencyType> = {}
|
|
217
|
+
for (const dep of dependencies) {
|
|
218
|
+
dependencyTypes[dep] = depType
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this.nodes.set(id, {
|
|
222
|
+
id,
|
|
223
|
+
dependencies,
|
|
224
|
+
dependencyTypes,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// Check for cycles after adding node with dependencies
|
|
228
|
+
if (dependencies.length > 0) {
|
|
229
|
+
const cycle = this.detectCycles()
|
|
230
|
+
if (cycle) {
|
|
231
|
+
// Remove the node we just added
|
|
232
|
+
this.nodes.delete(id)
|
|
233
|
+
throw new CircularDependencyError(cycle)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Add an edge (dependency) between existing nodes
|
|
240
|
+
*/
|
|
241
|
+
addEdge(from: string, to: string, type: DependencyType = 'hard'): void {
|
|
242
|
+
if (!this.nodes.has(from)) {
|
|
243
|
+
throw new MissingDependencyError(from, to)
|
|
244
|
+
}
|
|
245
|
+
if (!this.nodes.has(to)) {
|
|
246
|
+
throw new MissingDependencyError(to, from)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const toNode = this.nodes.get(to)!
|
|
250
|
+
|
|
251
|
+
// Check for self-reference
|
|
252
|
+
if (from === to) {
|
|
253
|
+
throw new CircularDependencyError([from, to])
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Add the dependency
|
|
257
|
+
if (!toNode.dependencies.includes(from)) {
|
|
258
|
+
toNode.dependencies.push(from)
|
|
259
|
+
toNode.dependencyTypes = toNode.dependencyTypes || {}
|
|
260
|
+
toNode.dependencyTypes[from] = type
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Check for cycles
|
|
264
|
+
const cycle = this.detectCycles()
|
|
265
|
+
if (cycle) {
|
|
266
|
+
// Rollback the edge
|
|
267
|
+
toNode.dependencies = toNode.dependencies.filter((d) => d !== from)
|
|
268
|
+
if (toNode.dependencyTypes) {
|
|
269
|
+
delete toNode.dependencyTypes[from]
|
|
270
|
+
}
|
|
271
|
+
throw new CircularDependencyError(cycle)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get all nodes in the graph
|
|
277
|
+
*/
|
|
278
|
+
getNodes(): GraphNode[] {
|
|
279
|
+
return Array.from(this.nodes.values())
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get a specific node
|
|
284
|
+
*/
|
|
285
|
+
getNode(id: string): GraphNode | undefined {
|
|
286
|
+
return this.nodes.get(id)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get direct dependencies of a node
|
|
291
|
+
*/
|
|
292
|
+
getDependencies(id: string): string[] {
|
|
293
|
+
const node = this.nodes.get(id)
|
|
294
|
+
if (!node) {
|
|
295
|
+
throw new MissingDependencyError(id, 'getDependencies()')
|
|
296
|
+
}
|
|
297
|
+
return [...node.dependencies]
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get all transitive dependencies of a node
|
|
302
|
+
*/
|
|
303
|
+
getAllDependencies(id: string): string[] {
|
|
304
|
+
const node = this.nodes.get(id)
|
|
305
|
+
if (!node) {
|
|
306
|
+
throw new MissingDependencyError(id, 'getAllDependencies()')
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const visited = new Set<string>()
|
|
310
|
+
const stack = [...node.dependencies]
|
|
311
|
+
|
|
312
|
+
while (stack.length > 0) {
|
|
313
|
+
const current = stack.pop()!
|
|
314
|
+
if (visited.has(current)) continue
|
|
315
|
+
visited.add(current)
|
|
316
|
+
|
|
317
|
+
const currentNode = this.nodes.get(current)
|
|
318
|
+
if (currentNode) {
|
|
319
|
+
for (const dep of currentNode.dependencies) {
|
|
320
|
+
if (!visited.has(dep)) {
|
|
321
|
+
stack.push(dep)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return Array.from(visited)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get nodes that depend on a given node
|
|
332
|
+
*/
|
|
333
|
+
getDependents(id: string): string[] {
|
|
334
|
+
const dependents: string[] = []
|
|
335
|
+
for (const node of this.nodes.values()) {
|
|
336
|
+
if (node.dependencies.includes(id)) {
|
|
337
|
+
dependents.push(node.id)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return dependents
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Get only hard dependencies
|
|
345
|
+
*/
|
|
346
|
+
getHardDependencies(id: string): string[] {
|
|
347
|
+
const node = this.nodes.get(id)
|
|
348
|
+
if (!node) {
|
|
349
|
+
throw new MissingDependencyError(id, 'getHardDependencies()')
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return node.dependencies.filter(
|
|
353
|
+
(dep) => (node.dependencyTypes?.[dep] || 'hard') === 'hard'
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Get only soft dependencies
|
|
359
|
+
*/
|
|
360
|
+
getSoftDependencies(id: string): string[] {
|
|
361
|
+
const node = this.nodes.get(id)
|
|
362
|
+
if (!node) {
|
|
363
|
+
throw new MissingDependencyError(id, 'getSoftDependencies()')
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return node.dependencies.filter(
|
|
367
|
+
(dep) => node.dependencyTypes?.[dep] === 'soft'
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Detect cycles using DFS
|
|
373
|
+
* Returns the cycle path if found, null otherwise
|
|
374
|
+
*/
|
|
375
|
+
detectCycles(): string[] | null {
|
|
376
|
+
const visited = new Set<string>()
|
|
377
|
+
const recursionStack = new Set<string>()
|
|
378
|
+
const path: string[] = []
|
|
379
|
+
|
|
380
|
+
const dfs = (nodeId: string): string[] | null => {
|
|
381
|
+
visited.add(nodeId)
|
|
382
|
+
recursionStack.add(nodeId)
|
|
383
|
+
path.push(nodeId)
|
|
384
|
+
|
|
385
|
+
const node = this.nodes.get(nodeId)
|
|
386
|
+
if (node) {
|
|
387
|
+
for (const dep of node.dependencies) {
|
|
388
|
+
if (!visited.has(dep)) {
|
|
389
|
+
const cycle = dfs(dep)
|
|
390
|
+
if (cycle) return cycle
|
|
391
|
+
} else if (recursionStack.has(dep)) {
|
|
392
|
+
// Found a cycle - construct the cycle path
|
|
393
|
+
const cycleStart = path.indexOf(dep)
|
|
394
|
+
const cyclePath = path.slice(cycleStart)
|
|
395
|
+
cyclePath.push(dep) // Complete the cycle
|
|
396
|
+
return cyclePath
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
path.pop()
|
|
402
|
+
recursionStack.delete(nodeId)
|
|
403
|
+
return null
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
for (const nodeId of this.nodes.keys()) {
|
|
407
|
+
if (!visited.has(nodeId)) {
|
|
408
|
+
const cycle = dfs(nodeId)
|
|
409
|
+
if (cycle) return cycle
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return null
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Get parallel execution groups
|
|
418
|
+
* Returns nodes grouped by execution level (0 = no deps, 1 = depends on 0, etc.)
|
|
419
|
+
*/
|
|
420
|
+
getParallelGroups(): ParallelGroup[] {
|
|
421
|
+
const levels = new Map<string, number>()
|
|
422
|
+
|
|
423
|
+
// Calculate level for each node
|
|
424
|
+
const calculateLevel = (nodeId: string): number => {
|
|
425
|
+
if (levels.has(nodeId)) {
|
|
426
|
+
return levels.get(nodeId)!
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const node = this.nodes.get(nodeId)
|
|
430
|
+
if (!node || node.dependencies.length === 0) {
|
|
431
|
+
levels.set(nodeId, 0)
|
|
432
|
+
return 0
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
let maxDepLevel = -1
|
|
436
|
+
for (const dep of node.dependencies) {
|
|
437
|
+
const depLevel = calculateLevel(dep)
|
|
438
|
+
maxDepLevel = Math.max(maxDepLevel, depLevel)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const level = maxDepLevel + 1
|
|
442
|
+
levels.set(nodeId, level)
|
|
443
|
+
return level
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Calculate levels for all nodes
|
|
447
|
+
for (const nodeId of this.nodes.keys()) {
|
|
448
|
+
calculateLevel(nodeId)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Group nodes by level
|
|
452
|
+
const groups = new Map<number, string[]>()
|
|
453
|
+
for (const [nodeId, level] of levels) {
|
|
454
|
+
if (!groups.has(level)) {
|
|
455
|
+
groups.set(level, [])
|
|
456
|
+
}
|
|
457
|
+
groups.get(level)!.push(nodeId)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Convert to sorted array of ParallelGroups
|
|
461
|
+
const result: ParallelGroup[] = []
|
|
462
|
+
const sortedLevels = Array.from(groups.keys()).sort((a, b) => a - b)
|
|
463
|
+
for (const level of sortedLevels) {
|
|
464
|
+
result.push({
|
|
465
|
+
level,
|
|
466
|
+
nodes: groups.get(level)!,
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return result
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Export graph to DOT format for visualization
|
|
475
|
+
*/
|
|
476
|
+
toDot(): string {
|
|
477
|
+
const lines: string[] = ['digraph DependencyGraph {']
|
|
478
|
+
lines.push(' rankdir=TB;')
|
|
479
|
+
lines.push(' node [shape=box];')
|
|
480
|
+
|
|
481
|
+
// Add nodes
|
|
482
|
+
for (const node of this.nodes.values()) {
|
|
483
|
+
const label = node.id.replace(/\./g, '\\n')
|
|
484
|
+
lines.push(` "${node.id}" [label="${label}"];`)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Add edges
|
|
488
|
+
for (const node of this.nodes.values()) {
|
|
489
|
+
for (const dep of node.dependencies) {
|
|
490
|
+
const style = node.dependencyTypes?.[dep] === 'soft' ? 'dashed' : 'solid'
|
|
491
|
+
lines.push(` "${dep}" -> "${node.id}" [style=${style}];`)
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
lines.push('}')
|
|
496
|
+
return lines.join('\n')
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Export graph to JSON format
|
|
501
|
+
*/
|
|
502
|
+
toJSON(): GraphJSON {
|
|
503
|
+
const nodes = this.getNodes()
|
|
504
|
+
const edges: GraphJSON['edges'] = []
|
|
505
|
+
|
|
506
|
+
for (const node of nodes) {
|
|
507
|
+
for (const dep of node.dependencies) {
|
|
508
|
+
edges.push({
|
|
509
|
+
from: dep,
|
|
510
|
+
to: node.id,
|
|
511
|
+
type: node.dependencyTypes?.[dep] || 'hard',
|
|
512
|
+
})
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return { nodes, edges }
|
|
517
|
+
}
|
|
518
|
+
}
|