aios-core 3.6.0 → 3.8.0
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/.aios-core/core/session/context-detector.js +3 -0
- package/.aios-core/core/session/context-loader.js +154 -0
- package/.aios-core/data/learned-patterns.yaml +3 -0
- package/.aios-core/data/workflow-patterns.yaml +347 -3
- package/.aios-core/development/agents/dev.md +13 -0
- package/.aios-core/development/agents/squad-creator.md +30 -0
- package/.aios-core/development/scripts/squad/squad-analyzer.js +638 -0
- package/.aios-core/development/scripts/squad/squad-extender.js +871 -0
- package/.aios-core/development/scripts/squad/squad-generator.js +107 -19
- package/.aios-core/development/scripts/squad/squad-migrator.js +3 -5
- package/.aios-core/development/scripts/squad/squad-validator.js +98 -0
- package/.aios-core/development/tasks/create-service.md +391 -0
- package/.aios-core/development/tasks/next.md +294 -0
- package/.aios-core/development/tasks/patterns.md +334 -0
- package/.aios-core/development/tasks/squad-creator-analyze.md +315 -0
- package/.aios-core/development/tasks/squad-creator-create.md +26 -3
- package/.aios-core/development/tasks/squad-creator-extend.md +411 -0
- package/.aios-core/development/tasks/squad-creator-validate.md +9 -1
- package/.aios-core/development/tasks/waves.md +205 -0
- package/.aios-core/development/templates/service-template/README.md.hbs +158 -0
- package/.aios-core/development/templates/service-template/__tests__/index.test.ts.hbs +237 -0
- package/.aios-core/development/templates/service-template/client.ts.hbs +403 -0
- package/.aios-core/development/templates/service-template/errors.ts.hbs +182 -0
- package/.aios-core/development/templates/service-template/index.ts.hbs +120 -0
- package/.aios-core/development/templates/service-template/jest.config.js +89 -0
- package/.aios-core/development/templates/service-template/package.json.hbs +87 -0
- package/.aios-core/development/templates/service-template/tsconfig.json +45 -0
- package/.aios-core/development/templates/service-template/types.ts.hbs +145 -0
- package/.aios-core/development/templates/squad/agent-template.md +69 -0
- package/.aios-core/development/templates/squad/checklist-template.md +82 -0
- package/.aios-core/development/templates/squad/data-template.yaml +105 -0
- package/.aios-core/development/templates/squad/script-template.js +179 -0
- package/.aios-core/development/templates/squad/task-template.md +125 -0
- package/.aios-core/development/templates/squad/template-template.md +97 -0
- package/.aios-core/development/templates/squad/tool-template.js +103 -0
- package/.aios-core/development/templates/squad/workflow-template.yaml +108 -0
- package/.aios-core/infrastructure/scripts/ide-sync/agent-parser.js +45 -1
- package/.aios-core/infrastructure/scripts/ide-sync/transformers/antigravity.js +6 -6
- package/.aios-core/infrastructure/scripts/ide-sync/transformers/cursor.js +5 -4
- package/.aios-core/infrastructure/scripts/ide-sync/transformers/trae.js +3 -3
- package/.aios-core/infrastructure/scripts/ide-sync/transformers/windsurf.js +3 -3
- package/.aios-core/install-manifest.yaml +139 -35
- package/.aios-core/quality/metrics-collector.js +27 -0
- package/.aios-core/scripts/session-context-loader.js +13 -254
- package/.aios-core/utils/aios-validator.js +25 -0
- package/.aios-core/workflow-intelligence/__tests__/confidence-scorer.test.js +334 -0
- package/.aios-core/workflow-intelligence/__tests__/integration.test.js +337 -0
- package/.aios-core/workflow-intelligence/__tests__/suggestion-engine.test.js +431 -0
- package/.aios-core/workflow-intelligence/__tests__/wave-analyzer.test.js +458 -0
- package/.aios-core/workflow-intelligence/__tests__/workflow-registry.test.js +302 -0
- package/.aios-core/workflow-intelligence/engine/confidence-scorer.js +305 -0
- package/.aios-core/workflow-intelligence/engine/output-formatter.js +285 -0
- package/.aios-core/workflow-intelligence/engine/suggestion-engine.js +603 -0
- package/.aios-core/workflow-intelligence/engine/wave-analyzer.js +676 -0
- package/.aios-core/workflow-intelligence/index.js +327 -0
- package/.aios-core/workflow-intelligence/learning/capture-hook.js +147 -0
- package/.aios-core/workflow-intelligence/learning/index.js +230 -0
- package/.aios-core/workflow-intelligence/learning/pattern-capture.js +340 -0
- package/.aios-core/workflow-intelligence/learning/pattern-store.js +498 -0
- package/.aios-core/workflow-intelligence/learning/pattern-validator.js +309 -0
- package/.aios-core/workflow-intelligence/registry/workflow-registry.js +358 -0
- package/package.json +1 -1
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module WaveAnalyzer
|
|
3
|
+
* @description Wave Analysis Engine for parallel task execution detection
|
|
4
|
+
* @story WIS-4 - Wave Analysis Engine
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const { WaveAnalyzer } = require('./wave-analyzer');
|
|
9
|
+
* const analyzer = new WaveAnalyzer();
|
|
10
|
+
*
|
|
11
|
+
* const result = analyzer.analyzeWaves('story_development');
|
|
12
|
+
* console.log(result.waves); // [{ waveNumber: 1, tasks: [...], parallel: true }, ...]
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Custom error class for circular dependency detection
|
|
19
|
+
*/
|
|
20
|
+
class CircularDependencyError extends Error {
|
|
21
|
+
/**
|
|
22
|
+
* Create a CircularDependencyError
|
|
23
|
+
* @param {string[]} cycle - Array of task names forming the cycle
|
|
24
|
+
*/
|
|
25
|
+
constructor(cycle) {
|
|
26
|
+
super(`Circular dependency detected: ${cycle.join(' → ')}`);
|
|
27
|
+
this.name = 'CircularDependencyError';
|
|
28
|
+
this.cycle = cycle;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get a suggested resolution for the circular dependency
|
|
33
|
+
* @returns {string} Resolution suggestion
|
|
34
|
+
*/
|
|
35
|
+
getSuggestion() {
|
|
36
|
+
if (this.cycle.length < 2) {
|
|
37
|
+
return 'Remove the self-referencing dependency';
|
|
38
|
+
}
|
|
39
|
+
const lastEdge = `${this.cycle[this.cycle.length - 2]} → ${this.cycle[this.cycle.length - 1]}`;
|
|
40
|
+
return `Consider removing the dependency from ${this.cycle[this.cycle.length - 1]} to ${this.cycle[0]}`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default task duration estimates (in minutes)
|
|
46
|
+
* @type {Object}
|
|
47
|
+
*/
|
|
48
|
+
const DEFAULT_TASK_DURATIONS = {
|
|
49
|
+
'read-story': 5,
|
|
50
|
+
'setup-branch': 2,
|
|
51
|
+
'implement': 30,
|
|
52
|
+
'write-tests': 10,
|
|
53
|
+
'update-docs': 5,
|
|
54
|
+
'run-tests': 5,
|
|
55
|
+
'review-qa': 15,
|
|
56
|
+
'apply-qa-fixes': 10,
|
|
57
|
+
'default': 10
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* WaveAnalyzer class for detecting parallel execution opportunities
|
|
62
|
+
*/
|
|
63
|
+
class WaveAnalyzer {
|
|
64
|
+
/**
|
|
65
|
+
* Create a WaveAnalyzer instance
|
|
66
|
+
* @param {Object} options - Configuration options
|
|
67
|
+
* @param {Object} options.registry - WorkflowRegistry instance (optional)
|
|
68
|
+
* @param {Object} options.taskDurations - Custom task duration estimates
|
|
69
|
+
*/
|
|
70
|
+
constructor(options = {}) {
|
|
71
|
+
this.registry = options.registry || null;
|
|
72
|
+
this.taskDurations = { ...DEFAULT_TASK_DURATIONS, ...options.taskDurations };
|
|
73
|
+
|
|
74
|
+
// Lazy-loaded registry
|
|
75
|
+
this._registryModule = null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get the workflow registry (lazy-loaded)
|
|
80
|
+
* @returns {Object} WorkflowRegistry instance
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
83
|
+
_getRegistry() {
|
|
84
|
+
if (this.registry) {
|
|
85
|
+
return this.registry;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!this._registryModule) {
|
|
89
|
+
try {
|
|
90
|
+
const { createWorkflowRegistry } = require('../registry/workflow-registry');
|
|
91
|
+
this._registryModule = createWorkflowRegistry();
|
|
92
|
+
} catch (error) {
|
|
93
|
+
throw new Error(`Failed to load WorkflowRegistry: ${error.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return this._registryModule;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Analyze waves for a workflow
|
|
102
|
+
* @param {string} workflowId - Workflow identifier
|
|
103
|
+
* @param {Object} options - Analysis options
|
|
104
|
+
* @param {Object} options.customTasks - Custom task definitions with dependencies
|
|
105
|
+
* @returns {Object} Wave analysis result
|
|
106
|
+
*/
|
|
107
|
+
analyzeWaves(workflowId, options = {}) {
|
|
108
|
+
const startTime = Date.now();
|
|
109
|
+
|
|
110
|
+
// Get workflow definition
|
|
111
|
+
const workflow = this._getWorkflowTasks(workflowId, options);
|
|
112
|
+
|
|
113
|
+
if (!workflow || !workflow.tasks || workflow.tasks.length === 0) {
|
|
114
|
+
return {
|
|
115
|
+
workflowId,
|
|
116
|
+
totalTasks: 0,
|
|
117
|
+
waves: [],
|
|
118
|
+
optimizationGain: '0%',
|
|
119
|
+
criticalPath: [],
|
|
120
|
+
analysisTime: Date.now() - startTime
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Build dependency graph
|
|
125
|
+
const graph = this.buildDependencyGraph(workflow.tasks);
|
|
126
|
+
|
|
127
|
+
// Check for cycles
|
|
128
|
+
const cycle = this.findCycle(graph);
|
|
129
|
+
if (cycle) {
|
|
130
|
+
throw new CircularDependencyError(cycle);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Perform wave analysis using Kahn's algorithm
|
|
134
|
+
const waves = this._kahnWaveAnalysis(graph);
|
|
135
|
+
|
|
136
|
+
// Calculate metrics
|
|
137
|
+
const criticalPath = this._findCriticalPath(graph, waves);
|
|
138
|
+
const sequentialTime = this._calculateSequentialTime(workflow.tasks);
|
|
139
|
+
const parallelTime = this._calculateParallelTime(waves);
|
|
140
|
+
const optimizationGain = this._calculateOptimizationGain(sequentialTime, parallelTime);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
workflowId,
|
|
144
|
+
totalTasks: workflow.tasks.length,
|
|
145
|
+
waves: waves.map((wave, index) => ({
|
|
146
|
+
waveNumber: index + 1,
|
|
147
|
+
tasks: wave.tasks,
|
|
148
|
+
parallel: wave.tasks.length > 1,
|
|
149
|
+
dependsOn: wave.dependsOn || [],
|
|
150
|
+
estimatedDuration: this._formatDuration(wave.duration)
|
|
151
|
+
})),
|
|
152
|
+
optimizationGain: `${optimizationGain}%`,
|
|
153
|
+
criticalPath,
|
|
154
|
+
metrics: {
|
|
155
|
+
sequentialTime: this._formatDuration(sequentialTime),
|
|
156
|
+
parallelTime: this._formatDuration(parallelTime),
|
|
157
|
+
analysisTime: Date.now() - startTime
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get workflow tasks with dependencies
|
|
164
|
+
* @param {string} workflowId - Workflow identifier
|
|
165
|
+
* @param {Object} options - Options including custom tasks
|
|
166
|
+
* @returns {Object} Workflow with tasks array
|
|
167
|
+
* @private
|
|
168
|
+
*/
|
|
169
|
+
_getWorkflowTasks(workflowId, options = {}) {
|
|
170
|
+
// Use custom tasks if provided
|
|
171
|
+
if (options.customTasks && options.customTasks.length > 0) {
|
|
172
|
+
return { id: workflowId, tasks: options.customTasks };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Get from registry
|
|
176
|
+
const registry = this._getRegistry();
|
|
177
|
+
const workflowDef = registry.getWorkflow(workflowId);
|
|
178
|
+
|
|
179
|
+
if (!workflowDef) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Extract tasks from workflow definition
|
|
184
|
+
return {
|
|
185
|
+
id: workflowId,
|
|
186
|
+
tasks: this._extractTasksFromWorkflow(workflowDef)
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Extract tasks from workflow definition
|
|
192
|
+
* @param {Object} workflowDef - Workflow definition from registry
|
|
193
|
+
* @returns {Object[]} Array of task objects with dependencies
|
|
194
|
+
* @private
|
|
195
|
+
*/
|
|
196
|
+
_extractTasksFromWorkflow(workflowDef) {
|
|
197
|
+
const tasks = [];
|
|
198
|
+
|
|
199
|
+
// If workflow has explicit tasks defined
|
|
200
|
+
if (workflowDef.tasks) {
|
|
201
|
+
return workflowDef.tasks;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Extract from transitions (implicit task order)
|
|
205
|
+
if (workflowDef.transitions) {
|
|
206
|
+
const stateOrder = Object.keys(workflowDef.transitions);
|
|
207
|
+
|
|
208
|
+
for (let i = 0; i < stateOrder.length; i++) {
|
|
209
|
+
const state = stateOrder[i];
|
|
210
|
+
const transition = workflowDef.transitions[state];
|
|
211
|
+
|
|
212
|
+
// Create task from transition
|
|
213
|
+
const task = {
|
|
214
|
+
id: state,
|
|
215
|
+
name: state,
|
|
216
|
+
dependsOn: i > 0 ? [stateOrder[i - 1]] : []
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// Add next_steps as parallel tasks within this state
|
|
220
|
+
if (transition.next_steps) {
|
|
221
|
+
for (const step of transition.next_steps) {
|
|
222
|
+
const stepTask = {
|
|
223
|
+
id: step.command,
|
|
224
|
+
name: step.command,
|
|
225
|
+
description: step.description,
|
|
226
|
+
duration: step.duration || this.taskDurations[step.command] || this.taskDurations.default,
|
|
227
|
+
dependsOn: [state] // Depends on the parent state
|
|
228
|
+
};
|
|
229
|
+
tasks.push(stepTask);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
tasks.push(task);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Extract from key_commands if no transitions
|
|
238
|
+
if (tasks.length === 0 && workflowDef.key_commands) {
|
|
239
|
+
for (let i = 0; i < workflowDef.key_commands.length; i++) {
|
|
240
|
+
const cmd = workflowDef.key_commands[i];
|
|
241
|
+
tasks.push({
|
|
242
|
+
id: cmd,
|
|
243
|
+
name: cmd,
|
|
244
|
+
dependsOn: i > 0 ? [workflowDef.key_commands[i - 1]] : [],
|
|
245
|
+
duration: this.taskDurations[cmd] || this.taskDurations.default
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return tasks;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Build directed acyclic graph from tasks
|
|
255
|
+
* @param {Object[]} tasks - Array of task objects
|
|
256
|
+
* @returns {Object} Graph object with nodes and adjacency list
|
|
257
|
+
*/
|
|
258
|
+
buildDependencyGraph(tasks) {
|
|
259
|
+
const graph = {
|
|
260
|
+
nodes: new Set(),
|
|
261
|
+
edges: new Map(), // node -> Set of nodes it points to
|
|
262
|
+
inEdges: new Map(), // node -> Set of nodes pointing to it
|
|
263
|
+
taskMap: new Map() // node -> task object
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Add all nodes
|
|
267
|
+
for (const task of tasks) {
|
|
268
|
+
const nodeId = task.id || task.name;
|
|
269
|
+
graph.nodes.add(nodeId);
|
|
270
|
+
graph.edges.set(nodeId, new Set());
|
|
271
|
+
graph.inEdges.set(nodeId, new Set());
|
|
272
|
+
graph.taskMap.set(nodeId, task);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Add edges based on dependencies
|
|
276
|
+
for (const task of tasks) {
|
|
277
|
+
const nodeId = task.id || task.name;
|
|
278
|
+
const dependencies = task.dependsOn || [];
|
|
279
|
+
|
|
280
|
+
for (const dep of dependencies) {
|
|
281
|
+
if (graph.nodes.has(dep)) {
|
|
282
|
+
// Edge from dependency to this task
|
|
283
|
+
graph.edges.get(dep).add(nodeId);
|
|
284
|
+
graph.inEdges.get(nodeId).add(dep);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return graph;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Find cycle in graph using DFS
|
|
294
|
+
* @param {Object} graph - Dependency graph
|
|
295
|
+
* @returns {string[]|null} Cycle path or null if no cycle
|
|
296
|
+
*/
|
|
297
|
+
findCycle(graph) {
|
|
298
|
+
const visited = new Set();
|
|
299
|
+
const recursionStack = new Set();
|
|
300
|
+
const parent = new Map();
|
|
301
|
+
|
|
302
|
+
for (const node of graph.nodes) {
|
|
303
|
+
if (!visited.has(node)) {
|
|
304
|
+
const cycle = this._dfsForCycle(graph, node, visited, recursionStack, parent);
|
|
305
|
+
if (cycle) {
|
|
306
|
+
return cycle;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* DFS helper for cycle detection
|
|
316
|
+
* @param {Object} graph - Dependency graph
|
|
317
|
+
* @param {string} node - Current node
|
|
318
|
+
* @param {Set} visited - Visited nodes
|
|
319
|
+
* @param {Set} recursionStack - Current recursion stack
|
|
320
|
+
* @param {Map} parent - Parent map for path reconstruction
|
|
321
|
+
* @returns {string[]|null} Cycle path or null
|
|
322
|
+
* @private
|
|
323
|
+
*/
|
|
324
|
+
_dfsForCycle(graph, node, visited, recursionStack, parent) {
|
|
325
|
+
visited.add(node);
|
|
326
|
+
recursionStack.add(node);
|
|
327
|
+
|
|
328
|
+
const neighbors = graph.edges.get(node) || new Set();
|
|
329
|
+
|
|
330
|
+
for (const neighbor of neighbors) {
|
|
331
|
+
if (!visited.has(neighbor)) {
|
|
332
|
+
parent.set(neighbor, node);
|
|
333
|
+
const cycle = this._dfsForCycle(graph, neighbor, visited, recursionStack, parent);
|
|
334
|
+
if (cycle) {
|
|
335
|
+
return cycle;
|
|
336
|
+
}
|
|
337
|
+
} else if (recursionStack.has(neighbor)) {
|
|
338
|
+
// Found cycle - reconstruct path
|
|
339
|
+
const cyclePath = [neighbor];
|
|
340
|
+
let current = node;
|
|
341
|
+
while (current !== neighbor) {
|
|
342
|
+
cyclePath.unshift(current);
|
|
343
|
+
current = parent.get(current);
|
|
344
|
+
}
|
|
345
|
+
cyclePath.unshift(neighbor);
|
|
346
|
+
return cyclePath;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
recursionStack.delete(node);
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Perform Kahn's algorithm for topological sort with wave grouping
|
|
356
|
+
* @param {Object} graph - Dependency graph
|
|
357
|
+
* @returns {Object[]} Array of wave objects
|
|
358
|
+
* @private
|
|
359
|
+
*/
|
|
360
|
+
_kahnWaveAnalysis(graph) {
|
|
361
|
+
const waves = [];
|
|
362
|
+
const inDegree = new Map();
|
|
363
|
+
const remaining = new Set(graph.nodes);
|
|
364
|
+
|
|
365
|
+
// Calculate initial in-degrees
|
|
366
|
+
for (const node of graph.nodes) {
|
|
367
|
+
inDegree.set(node, graph.inEdges.get(node)?.size || 0);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
while (remaining.size > 0) {
|
|
371
|
+
// Find all nodes with no incoming edges
|
|
372
|
+
const waveTasks = [];
|
|
373
|
+
const completedInWave = [];
|
|
374
|
+
|
|
375
|
+
for (const node of remaining) {
|
|
376
|
+
if (inDegree.get(node) === 0) {
|
|
377
|
+
waveTasks.push(node);
|
|
378
|
+
completedInWave.push(node);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (waveTasks.length === 0) {
|
|
383
|
+
// Should not happen if we checked for cycles
|
|
384
|
+
throw new Error('Unexpected cycle detected during wave analysis');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Calculate wave duration (max of parallel tasks)
|
|
388
|
+
let waveDuration = 0;
|
|
389
|
+
const dependencies = new Set();
|
|
390
|
+
|
|
391
|
+
for (const task of waveTasks) {
|
|
392
|
+
const taskObj = graph.taskMap.get(task);
|
|
393
|
+
const duration = taskObj?.duration || this.taskDurations[task] || this.taskDurations.default;
|
|
394
|
+
waveDuration = Math.max(waveDuration, duration);
|
|
395
|
+
|
|
396
|
+
// Collect dependencies from previous waves
|
|
397
|
+
const deps = graph.inEdges.get(task) || new Set();
|
|
398
|
+
for (const dep of deps) {
|
|
399
|
+
if (!waveTasks.includes(dep)) {
|
|
400
|
+
dependencies.add(dep);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
waves.push({
|
|
406
|
+
tasks: waveTasks,
|
|
407
|
+
duration: waveDuration,
|
|
408
|
+
dependsOn: Array.from(dependencies)
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Remove wave nodes and update in-degrees
|
|
412
|
+
for (const node of completedInWave) {
|
|
413
|
+
remaining.delete(node);
|
|
414
|
+
|
|
415
|
+
const neighbors = graph.edges.get(node) || new Set();
|
|
416
|
+
for (const neighbor of neighbors) {
|
|
417
|
+
inDegree.set(neighbor, inDegree.get(neighbor) - 1);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return waves;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Find the critical path through the graph
|
|
427
|
+
* @param {Object} graph - Dependency graph
|
|
428
|
+
* @param {Object[]} waves - Wave analysis result
|
|
429
|
+
* @returns {string[]} Critical path tasks
|
|
430
|
+
* @private
|
|
431
|
+
*/
|
|
432
|
+
_findCriticalPath(graph, waves) {
|
|
433
|
+
if (waves.length === 0) {
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Build longest path using dynamic programming
|
|
438
|
+
const distance = new Map();
|
|
439
|
+
const predecessor = new Map();
|
|
440
|
+
|
|
441
|
+
// Initialize distances
|
|
442
|
+
for (const node of graph.nodes) {
|
|
443
|
+
distance.set(node, 0);
|
|
444
|
+
predecessor.set(node, null);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Process nodes in topological order (wave order)
|
|
448
|
+
for (const wave of waves) {
|
|
449
|
+
for (const node of wave.tasks) {
|
|
450
|
+
const taskObj = graph.taskMap.get(node);
|
|
451
|
+
const duration = taskObj?.duration || this.taskDurations[node] || this.taskDurations.default;
|
|
452
|
+
|
|
453
|
+
const neighbors = graph.edges.get(node) || new Set();
|
|
454
|
+
for (const neighbor of neighbors) {
|
|
455
|
+
const newDist = distance.get(node) + duration;
|
|
456
|
+
if (newDist > distance.get(neighbor)) {
|
|
457
|
+
distance.set(neighbor, newDist);
|
|
458
|
+
predecessor.set(neighbor, node);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Find the end node with maximum distance
|
|
465
|
+
let maxDist = -1;
|
|
466
|
+
let endNode = null;
|
|
467
|
+
|
|
468
|
+
for (const [node, dist] of distance) {
|
|
469
|
+
const taskObj = graph.taskMap.get(node);
|
|
470
|
+
const duration = taskObj?.duration || this.taskDurations[node] || this.taskDurations.default;
|
|
471
|
+
const totalDist = dist + duration;
|
|
472
|
+
|
|
473
|
+
if (totalDist > maxDist) {
|
|
474
|
+
maxDist = totalDist;
|
|
475
|
+
endNode = node;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Reconstruct critical path
|
|
480
|
+
const criticalPath = [];
|
|
481
|
+
let current = endNode;
|
|
482
|
+
|
|
483
|
+
while (current !== null) {
|
|
484
|
+
criticalPath.unshift(current);
|
|
485
|
+
current = predecessor.get(current);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return criticalPath;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Calculate total sequential execution time
|
|
493
|
+
* @param {Object[]} tasks - Array of tasks
|
|
494
|
+
* @returns {number} Total time in minutes
|
|
495
|
+
* @private
|
|
496
|
+
*/
|
|
497
|
+
_calculateSequentialTime(tasks) {
|
|
498
|
+
return tasks.reduce((sum, task) => {
|
|
499
|
+
const duration = task.duration || this.taskDurations[task.id] || this.taskDurations[task.name] || this.taskDurations.default;
|
|
500
|
+
return sum + duration;
|
|
501
|
+
}, 0);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Calculate total parallel execution time
|
|
506
|
+
* @param {Object[]} waves - Wave analysis result
|
|
507
|
+
* @returns {number} Total time in minutes
|
|
508
|
+
* @private
|
|
509
|
+
*/
|
|
510
|
+
_calculateParallelTime(waves) {
|
|
511
|
+
return waves.reduce((sum, wave) => sum + wave.duration, 0);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Calculate optimization gain percentage
|
|
516
|
+
* @param {number} sequentialTime - Sequential execution time
|
|
517
|
+
* @param {number} parallelTime - Parallel execution time
|
|
518
|
+
* @returns {number} Percentage improvement
|
|
519
|
+
* @private
|
|
520
|
+
*/
|
|
521
|
+
_calculateOptimizationGain(sequentialTime, parallelTime) {
|
|
522
|
+
if (sequentialTime === 0) return 0;
|
|
523
|
+
const gain = ((sequentialTime - parallelTime) / sequentialTime) * 100;
|
|
524
|
+
return Math.round(gain);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Format duration in minutes to human-readable string
|
|
529
|
+
* @param {number} minutes - Duration in minutes
|
|
530
|
+
* @returns {string} Formatted duration
|
|
531
|
+
* @private
|
|
532
|
+
*/
|
|
533
|
+
_formatDuration(minutes) {
|
|
534
|
+
if (minutes < 60) {
|
|
535
|
+
return `${minutes}min`;
|
|
536
|
+
}
|
|
537
|
+
const hours = Math.floor(minutes / 60);
|
|
538
|
+
const mins = minutes % 60;
|
|
539
|
+
return mins > 0 ? `${hours}h ${mins}min` : `${hours}h`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Get wave context for current workflow state
|
|
544
|
+
* @param {string} workflowId - Workflow identifier
|
|
545
|
+
* @param {string} currentTask - Currently executing task
|
|
546
|
+
* @returns {Object} Wave context including current position
|
|
547
|
+
*/
|
|
548
|
+
getCurrentWave(workflowId, currentTask) {
|
|
549
|
+
try {
|
|
550
|
+
const analysis = this.analyzeWaves(workflowId);
|
|
551
|
+
|
|
552
|
+
let currentWaveNumber = null;
|
|
553
|
+
let totalWaves = analysis.waves.length;
|
|
554
|
+
let currentWaveInfo = null;
|
|
555
|
+
let nextWaveInfo = null;
|
|
556
|
+
|
|
557
|
+
for (let i = 0; i < analysis.waves.length; i++) {
|
|
558
|
+
const wave = analysis.waves[i];
|
|
559
|
+
if (wave.tasks.includes(currentTask)) {
|
|
560
|
+
currentWaveNumber = i + 1;
|
|
561
|
+
currentWaveInfo = wave;
|
|
562
|
+
|
|
563
|
+
if (i + 1 < analysis.waves.length) {
|
|
564
|
+
nextWaveInfo = analysis.waves[i + 1];
|
|
565
|
+
}
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return {
|
|
571
|
+
workflowId,
|
|
572
|
+
currentTask,
|
|
573
|
+
currentWaveNumber,
|
|
574
|
+
totalWaves,
|
|
575
|
+
currentWave: currentWaveInfo,
|
|
576
|
+
nextWave: nextWaveInfo,
|
|
577
|
+
parallelTasks: currentWaveInfo?.tasks.filter(t => t !== currentTask) || [],
|
|
578
|
+
canParallelize: currentWaveInfo?.parallel || false
|
|
579
|
+
};
|
|
580
|
+
} catch (error) {
|
|
581
|
+
return {
|
|
582
|
+
workflowId,
|
|
583
|
+
currentTask,
|
|
584
|
+
currentWaveNumber: null,
|
|
585
|
+
totalWaves: 0,
|
|
586
|
+
error: error.message
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Format wave analysis for CLI output
|
|
593
|
+
* @param {Object} analysis - Wave analysis result
|
|
594
|
+
* @param {Object} options - Formatting options
|
|
595
|
+
* @param {boolean} options.visual - Include ASCII visualization
|
|
596
|
+
* @param {boolean} options.json - Return as JSON string
|
|
597
|
+
* @returns {string} Formatted output
|
|
598
|
+
*/
|
|
599
|
+
formatOutput(analysis, options = {}) {
|
|
600
|
+
if (options.json) {
|
|
601
|
+
return JSON.stringify(analysis, null, 2);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const lines = [];
|
|
605
|
+
|
|
606
|
+
lines.push(`Wave Analysis: ${analysis.workflowId}`);
|
|
607
|
+
lines.push('═'.repeat(40));
|
|
608
|
+
lines.push('');
|
|
609
|
+
|
|
610
|
+
if (options.visual) {
|
|
611
|
+
for (const wave of analysis.waves) {
|
|
612
|
+
const prefix = `Wave ${wave.waveNumber} `;
|
|
613
|
+
|
|
614
|
+
if (wave.tasks.length === 1) {
|
|
615
|
+
lines.push(`${prefix}──────── ${wave.tasks[0]} (${wave.estimatedDuration})`);
|
|
616
|
+
} else {
|
|
617
|
+
lines.push(`${prefix}──┬── ${wave.tasks[0]} (${wave.estimatedDuration})`);
|
|
618
|
+
for (let i = 1; i < wave.tasks.length; i++) {
|
|
619
|
+
const connector = i === wave.tasks.length - 1 ? '└' : '├';
|
|
620
|
+
lines.push(` ${connector}── ${wave.tasks[i]}`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (wave.waveNumber < analysis.waves.length) {
|
|
625
|
+
lines.push(' │');
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
} else {
|
|
629
|
+
for (const wave of analysis.waves) {
|
|
630
|
+
const parallelIndicator = wave.parallel ? '(parallel)' : '';
|
|
631
|
+
lines.push(`Wave ${wave.waveNumber} ${parallelIndicator}:`);
|
|
632
|
+
for (const task of wave.tasks) {
|
|
633
|
+
lines.push(` └─ ${task}`);
|
|
634
|
+
}
|
|
635
|
+
lines.push('');
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
lines.push('');
|
|
640
|
+
lines.push(`Total Sequential: ${analysis.metrics?.sequentialTime || 'N/A'}`);
|
|
641
|
+
lines.push(`Total Parallel: ${analysis.metrics?.parallelTime || 'N/A'}`);
|
|
642
|
+
lines.push(`Optimization: ${analysis.optimizationGain} faster`);
|
|
643
|
+
lines.push('');
|
|
644
|
+
lines.push(`Critical Path: ${analysis.criticalPath.join(' → ')}`);
|
|
645
|
+
|
|
646
|
+
return lines.join('\n');
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Create a new WaveAnalyzer instance
|
|
652
|
+
* @param {Object} options - Configuration options
|
|
653
|
+
* @returns {WaveAnalyzer} New analyzer instance
|
|
654
|
+
*/
|
|
655
|
+
function createWaveAnalyzer(options = {}) {
|
|
656
|
+
return new WaveAnalyzer(options);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Analyze waves for a workflow (convenience function)
|
|
661
|
+
* @param {string} workflowId - Workflow identifier
|
|
662
|
+
* @param {Object} options - Analysis options
|
|
663
|
+
* @returns {Object} Wave analysis result
|
|
664
|
+
*/
|
|
665
|
+
function analyzeWaves(workflowId, options = {}) {
|
|
666
|
+
const analyzer = createWaveAnalyzer(options);
|
|
667
|
+
return analyzer.analyzeWaves(workflowId, options);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
module.exports = {
|
|
671
|
+
WaveAnalyzer,
|
|
672
|
+
CircularDependencyError,
|
|
673
|
+
createWaveAnalyzer,
|
|
674
|
+
analyzeWaves,
|
|
675
|
+
DEFAULT_TASK_DURATIONS
|
|
676
|
+
};
|