aios-core 3.7.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.
Files changed (47) hide show
  1. package/.aios-core/core/session/context-detector.js +3 -0
  2. package/.aios-core/core/session/context-loader.js +154 -0
  3. package/.aios-core/data/learned-patterns.yaml +3 -0
  4. package/.aios-core/data/workflow-patterns.yaml +347 -3
  5. package/.aios-core/development/agents/dev.md +6 -0
  6. package/.aios-core/development/agents/squad-creator.md +30 -0
  7. package/.aios-core/development/scripts/squad/squad-analyzer.js +638 -0
  8. package/.aios-core/development/scripts/squad/squad-extender.js +871 -0
  9. package/.aios-core/development/scripts/squad/squad-generator.js +107 -19
  10. package/.aios-core/development/scripts/squad/squad-migrator.js +3 -5
  11. package/.aios-core/development/scripts/squad/squad-validator.js +98 -0
  12. package/.aios-core/development/tasks/next.md +294 -0
  13. package/.aios-core/development/tasks/patterns.md +334 -0
  14. package/.aios-core/development/tasks/squad-creator-analyze.md +315 -0
  15. package/.aios-core/development/tasks/squad-creator-create.md +26 -3
  16. package/.aios-core/development/tasks/squad-creator-extend.md +411 -0
  17. package/.aios-core/development/tasks/squad-creator-validate.md +9 -1
  18. package/.aios-core/development/tasks/waves.md +205 -0
  19. package/.aios-core/development/templates/squad/agent-template.md +69 -0
  20. package/.aios-core/development/templates/squad/checklist-template.md +82 -0
  21. package/.aios-core/development/templates/squad/data-template.yaml +105 -0
  22. package/.aios-core/development/templates/squad/script-template.js +179 -0
  23. package/.aios-core/development/templates/squad/task-template.md +125 -0
  24. package/.aios-core/development/templates/squad/template-template.md +97 -0
  25. package/.aios-core/development/templates/squad/tool-template.js +103 -0
  26. package/.aios-core/development/templates/squad/workflow-template.yaml +108 -0
  27. package/.aios-core/install-manifest.yaml +89 -25
  28. package/.aios-core/quality/metrics-collector.js +27 -0
  29. package/.aios-core/scripts/session-context-loader.js +13 -254
  30. package/.aios-core/utils/aios-validator.js +25 -0
  31. package/.aios-core/workflow-intelligence/__tests__/confidence-scorer.test.js +334 -0
  32. package/.aios-core/workflow-intelligence/__tests__/integration.test.js +337 -0
  33. package/.aios-core/workflow-intelligence/__tests__/suggestion-engine.test.js +431 -0
  34. package/.aios-core/workflow-intelligence/__tests__/wave-analyzer.test.js +458 -0
  35. package/.aios-core/workflow-intelligence/__tests__/workflow-registry.test.js +302 -0
  36. package/.aios-core/workflow-intelligence/engine/confidence-scorer.js +305 -0
  37. package/.aios-core/workflow-intelligence/engine/output-formatter.js +285 -0
  38. package/.aios-core/workflow-intelligence/engine/suggestion-engine.js +603 -0
  39. package/.aios-core/workflow-intelligence/engine/wave-analyzer.js +676 -0
  40. package/.aios-core/workflow-intelligence/index.js +327 -0
  41. package/.aios-core/workflow-intelligence/learning/capture-hook.js +147 -0
  42. package/.aios-core/workflow-intelligence/learning/index.js +230 -0
  43. package/.aios-core/workflow-intelligence/learning/pattern-capture.js +340 -0
  44. package/.aios-core/workflow-intelligence/learning/pattern-store.js +498 -0
  45. package/.aios-core/workflow-intelligence/learning/pattern-validator.js +309 -0
  46. package/.aios-core/workflow-intelligence/registry/workflow-registry.js +358 -0
  47. 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
+ };