aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
684 lines (598 loc) • 19.8 kB
JavaScript
/**
* @module WaveAnalyzer
* @description Wave Analysis Engine for parallel task execution detection
* @story WIS-4 - Wave Analysis Engine
* @version 1.0.0
*
* @example
* const { WaveAnalyzer } = require('./wave-analyzer');
* const analyzer = new WaveAnalyzer();
*
* const result = analyzer.analyzeWaves('story_development');
* console.log(result.waves); // [{ waveNumber: 1, tasks: [...], parallel: true }, ...]
*/
'use strict';
/**
* Custom error class for circular dependency detection
*/
class CircularDependencyError extends Error {
/**
* Create a CircularDependencyError
* @param {string[]} cycle - Array of task names forming the cycle
*/
constructor(cycle) {
super(`Circular dependency detected: ${cycle.join(' → ')}`);
this.name = 'CircularDependencyError';
this.cycle = cycle;
}
/**
* Get a suggested resolution for the circular dependency
* @returns {string} Resolution suggestion
*/
getSuggestion() {
if (this.cycle.length < 2) {
return 'Remove the self-referencing dependency';
}
const _lastEdge = `${this.cycle[this.cycle.length - 2]} → ${this.cycle[this.cycle.length - 1]}`;
return `Consider removing the dependency from ${this.cycle[this.cycle.length - 1]} to ${this.cycle[0]}`;
}
}
/**
* Default task duration estimates (in minutes)
* @type {Object}
*/
const DEFAULT_TASK_DURATIONS = {
'read-story': 5,
'setup-branch': 2,
implement: 30,
'write-tests': 10,
'update-docs': 5,
'run-tests': 5,
'review-qa': 15,
'apply-qa-fixes': 10,
default: 10,
};
/**
* WaveAnalyzer class for detecting parallel execution opportunities
*/
class WaveAnalyzer {
/**
* Create a WaveAnalyzer instance
* @param {Object} options - Configuration options
* @param {Object} options.registry - WorkflowRegistry instance (optional)
* @param {Object} options.taskDurations - Custom task duration estimates
*/
constructor(options = {}) {
this.registry = options.registry || null;
this.taskDurations = { ...DEFAULT_TASK_DURATIONS, ...options.taskDurations };
// Lazy-loaded registry
this._registryModule = null;
}
/**
* Get the workflow registry (lazy-loaded)
* @returns {Object} WorkflowRegistry instance
* @private
*/
_getRegistry() {
if (this.registry) {
return this.registry;
}
if (!this._registryModule) {
try {
const { createWorkflowRegistry } = require('../registry/workflow-registry');
this._registryModule = createWorkflowRegistry();
} catch (error) {
throw new Error(`Failed to load WorkflowRegistry: ${error.message}`);
}
}
return this._registryModule;
}
/**
* Analyze waves for a workflow
* @param {string} workflowId - Workflow identifier
* @param {Object} options - Analysis options
* @param {Object} options.customTasks - Custom task definitions with dependencies
* @returns {Object} Wave analysis result
*/
analyzeWaves(workflowId, options = {}) {
const startTime = Date.now();
// Get workflow definition
const workflow = this._getWorkflowTasks(workflowId, options);
if (!workflow || !workflow.tasks || workflow.tasks.length === 0) {
return {
workflowId,
totalTasks: 0,
waves: [],
optimizationGain: '0%',
criticalPath: [],
analysisTime: Date.now() - startTime,
};
}
// Build dependency graph
const graph = this.buildDependencyGraph(workflow.tasks);
// Check for cycles
const cycle = this.findCycle(graph);
if (cycle) {
throw new CircularDependencyError(cycle);
}
// Perform wave analysis using Kahn's algorithm
const waves = this._kahnWaveAnalysis(graph);
// Calculate metrics
const criticalPath = this._findCriticalPath(graph, waves);
const sequentialTime = this._calculateSequentialTime(workflow.tasks);
const parallelTime = this._calculateParallelTime(waves);
const optimizationGain = this._calculateOptimizationGain(sequentialTime, parallelTime);
return {
workflowId,
totalTasks: workflow.tasks.length,
waves: waves.map((wave, index) => ({
waveNumber: index + 1,
tasks: wave.tasks,
parallel: wave.tasks.length > 1,
dependsOn: wave.dependsOn || [],
estimatedDuration: this._formatDuration(wave.duration),
})),
optimizationGain: `${optimizationGain}%`,
criticalPath,
metrics: {
sequentialTime: this._formatDuration(sequentialTime),
parallelTime: this._formatDuration(parallelTime),
analysisTime: Date.now() - startTime,
},
};
}
/**
* Get workflow tasks with dependencies
* @param {string} workflowId - Workflow identifier
* @param {Object} options - Options including custom tasks
* @returns {Object} Workflow with tasks array
* @private
*/
_getWorkflowTasks(workflowId, options = {}) {
// Use custom tasks if provided
if (options.customTasks && options.customTasks.length > 0) {
return { id: workflowId, tasks: options.customTasks };
}
// Get from registry
const registry = this._getRegistry();
const workflowDef = registry.getWorkflow(workflowId);
if (!workflowDef) {
return null;
}
// Extract tasks from workflow definition
return {
id: workflowId,
tasks: this._extractTasksFromWorkflow(workflowDef),
};
}
/**
* Extract tasks from workflow definition
* @param {Object} workflowDef - Workflow definition from registry
* @returns {Object[]} Array of task objects with dependencies
* @private
*/
_extractTasksFromWorkflow(workflowDef) {
const tasks = [];
// If workflow has explicit tasks defined
if (workflowDef.tasks) {
return workflowDef.tasks;
}
// Extract from transitions (implicit task order)
if (workflowDef.transitions) {
const stateOrder = Object.keys(workflowDef.transitions);
for (let i = 0; i < stateOrder.length; i++) {
const state = stateOrder[i];
const transition = workflowDef.transitions[state];
// Create task from transition
const task = {
id: state,
name: state,
dependsOn: i > 0 ? [stateOrder[i - 1]] : [],
};
// Add next_steps as parallel tasks within this state
if (transition.next_steps) {
for (const step of transition.next_steps) {
const stepTask = {
id: step.command,
name: step.command,
description: step.description,
duration:
step.duration || this.taskDurations[step.command] || this.taskDurations.default,
dependsOn: [state], // Depends on the parent state
};
tasks.push(stepTask);
}
}
tasks.push(task);
}
}
// Extract from key_commands if no transitions
if (tasks.length === 0 && workflowDef.key_commands) {
for (let i = 0; i < workflowDef.key_commands.length; i++) {
const cmd = workflowDef.key_commands[i];
tasks.push({
id: cmd,
name: cmd,
dependsOn: i > 0 ? [workflowDef.key_commands[i - 1]] : [],
duration: this.taskDurations[cmd] || this.taskDurations.default,
});
}
}
return tasks;
}
/**
* Build directed acyclic graph from tasks
* @param {Object[]} tasks - Array of task objects
* @returns {Object} Graph object with nodes and adjacency list
*/
buildDependencyGraph(tasks) {
const graph = {
nodes: new Set(),
edges: new Map(), // node -> Set of nodes it points to
inEdges: new Map(), // node -> Set of nodes pointing to it
taskMap: new Map(), // node -> task object
};
// Add all nodes
for (const task of tasks) {
const nodeId = task.id || task.name;
graph.nodes.add(nodeId);
graph.edges.set(nodeId, new Set());
graph.inEdges.set(nodeId, new Set());
graph.taskMap.set(nodeId, task);
}
// Add edges based on dependencies
for (const task of tasks) {
const nodeId = task.id || task.name;
const dependencies = task.dependsOn || [];
for (const dep of dependencies) {
if (graph.nodes.has(dep)) {
// Edge from dependency to this task
graph.edges.get(dep).add(nodeId);
graph.inEdges.get(nodeId).add(dep);
}
}
}
return graph;
}
/**
* Find cycle in graph using DFS
* @param {Object} graph - Dependency graph
* @returns {string[]|null} Cycle path or null if no cycle
*/
findCycle(graph) {
const visited = new Set();
const recursionStack = new Set();
const parent = new Map();
for (const node of graph.nodes) {
if (!visited.has(node)) {
const cycle = this._dfsForCycle(graph, node, visited, recursionStack, parent);
if (cycle) {
return cycle;
}
}
}
return null;
}
/**
* DFS helper for cycle detection
* @param {Object} graph - Dependency graph
* @param {string} node - Current node
* @param {Set} visited - Visited nodes
* @param {Set} recursionStack - Current recursion stack
* @param {Map} parent - Parent map for path reconstruction
* @returns {string[]|null} Cycle path or null
* @private
*/
_dfsForCycle(graph, node, visited, recursionStack, parent) {
visited.add(node);
recursionStack.add(node);
const neighbors = graph.edges.get(node) || new Set();
for (const neighbor of neighbors) {
if (!visited.has(neighbor)) {
parent.set(neighbor, node);
const cycle = this._dfsForCycle(graph, neighbor, visited, recursionStack, parent);
if (cycle) {
return cycle;
}
} else if (recursionStack.has(neighbor)) {
// Found cycle - reconstruct path
const cyclePath = [neighbor];
let current = node;
while (current !== neighbor) {
cyclePath.unshift(current);
current = parent.get(current);
}
cyclePath.unshift(neighbor);
return cyclePath;
}
}
recursionStack.delete(node);
return null;
}
/**
* Perform Kahn's algorithm for topological sort with wave grouping
* @param {Object} graph - Dependency graph
* @returns {Object[]} Array of wave objects
* @private
*/
_kahnWaveAnalysis(graph) {
const waves = [];
const inDegree = new Map();
const remaining = new Set(graph.nodes);
// Calculate initial in-degrees
for (const node of graph.nodes) {
inDegree.set(node, graph.inEdges.get(node)?.size || 0);
}
while (remaining.size > 0) {
// Find all nodes with no incoming edges
const waveTasks = [];
const completedInWave = [];
for (const node of remaining) {
if (inDegree.get(node) === 0) {
waveTasks.push(node);
completedInWave.push(node);
}
}
if (waveTasks.length === 0) {
// Should not happen if we checked for cycles
throw new Error('Unexpected cycle detected during wave analysis');
}
// Calculate wave duration (max of parallel tasks)
let waveDuration = 0;
const dependencies = new Set();
for (const task of waveTasks) {
const taskObj = graph.taskMap.get(task);
const duration =
taskObj?.duration || this.taskDurations[task] || this.taskDurations.default;
waveDuration = Math.max(waveDuration, duration);
// Collect dependencies from previous waves
const deps = graph.inEdges.get(task) || new Set();
for (const dep of deps) {
if (!waveTasks.includes(dep)) {
dependencies.add(dep);
}
}
}
waves.push({
tasks: waveTasks,
duration: waveDuration,
dependsOn: Array.from(dependencies),
});
// Remove wave nodes and update in-degrees
for (const node of completedInWave) {
remaining.delete(node);
const neighbors = graph.edges.get(node) || new Set();
for (const neighbor of neighbors) {
inDegree.set(neighbor, inDegree.get(neighbor) - 1);
}
}
}
return waves;
}
/**
* Find the critical path through the graph
* @param {Object} graph - Dependency graph
* @param {Object[]} waves - Wave analysis result
* @returns {string[]} Critical path tasks
* @private
*/
_findCriticalPath(graph, waves) {
if (waves.length === 0) {
return [];
}
// Build longest path using dynamic programming
const distance = new Map();
const predecessor = new Map();
// Initialize distances
for (const node of graph.nodes) {
distance.set(node, 0);
predecessor.set(node, null);
}
// Process nodes in topological order (wave order)
for (const wave of waves) {
for (const node of wave.tasks) {
const taskObj = graph.taskMap.get(node);
const duration =
taskObj?.duration || this.taskDurations[node] || this.taskDurations.default;
const neighbors = graph.edges.get(node) || new Set();
for (const neighbor of neighbors) {
const newDist = distance.get(node) + duration;
if (newDist > distance.get(neighbor)) {
distance.set(neighbor, newDist);
predecessor.set(neighbor, node);
}
}
}
}
// Find the end node with maximum distance
let maxDist = -1;
let endNode = null;
for (const [node, dist] of distance) {
const taskObj = graph.taskMap.get(node);
const duration = taskObj?.duration || this.taskDurations[node] || this.taskDurations.default;
const totalDist = dist + duration;
if (totalDist > maxDist) {
maxDist = totalDist;
endNode = node;
}
}
// Reconstruct critical path
const criticalPath = [];
let current = endNode;
while (current !== null) {
criticalPath.unshift(current);
current = predecessor.get(current);
}
return criticalPath;
}
/**
* Calculate total sequential execution time
* @param {Object[]} tasks - Array of tasks
* @returns {number} Total time in minutes
* @private
*/
_calculateSequentialTime(tasks) {
return tasks.reduce((sum, task) => {
const duration =
task.duration ||
this.taskDurations[task.id] ||
this.taskDurations[task.name] ||
this.taskDurations.default;
return sum + duration;
}, 0);
}
/**
* Calculate total parallel execution time
* @param {Object[]} waves - Wave analysis result
* @returns {number} Total time in minutes
* @private
*/
_calculateParallelTime(waves) {
return waves.reduce((sum, wave) => sum + wave.duration, 0);
}
/**
* Calculate optimization gain percentage
* @param {number} sequentialTime - Sequential execution time
* @param {number} parallelTime - Parallel execution time
* @returns {number} Percentage improvement
* @private
*/
_calculateOptimizationGain(sequentialTime, parallelTime) {
if (sequentialTime === 0) return 0;
const gain = ((sequentialTime - parallelTime) / sequentialTime) * 100;
return Math.round(gain);
}
/**
* Format duration in minutes to human-readable string
* @param {number} minutes - Duration in minutes
* @returns {string} Formatted duration
* @private
*/
_formatDuration(minutes) {
if (minutes < 60) {
return `${minutes}min`;
}
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return mins > 0 ? `${hours}h ${mins}min` : `${hours}h`;
}
/**
* Get wave context for current workflow state
* @param {string} workflowId - Workflow identifier
* @param {string} currentTask - Currently executing task
* @returns {Object} Wave context including current position
*/
getCurrentWave(workflowId, currentTask) {
try {
const analysis = this.analyzeWaves(workflowId);
let currentWaveNumber = null;
const totalWaves = analysis.waves.length;
let currentWaveInfo = null;
let nextWaveInfo = null;
for (let i = 0; i < analysis.waves.length; i++) {
const wave = analysis.waves[i];
if (wave.tasks.includes(currentTask)) {
currentWaveNumber = i + 1;
currentWaveInfo = wave;
if (i + 1 < analysis.waves.length) {
nextWaveInfo = analysis.waves[i + 1];
}
break;
}
}
return {
workflowId,
currentTask,
currentWaveNumber,
totalWaves,
currentWave: currentWaveInfo,
nextWave: nextWaveInfo,
parallelTasks: currentWaveInfo?.tasks.filter((t) => t !== currentTask) || [],
canParallelize: currentWaveInfo?.parallel || false,
};
} catch (error) {
return {
workflowId,
currentTask,
currentWaveNumber: null,
totalWaves: 0,
error: error.message,
};
}
}
/**
* Format wave analysis for CLI output
* @param {Object} analysis - Wave analysis result
* @param {Object} options - Formatting options
* @param {boolean} options.visual - Include ASCII visualization
* @param {boolean} options.json - Return as JSON string
* @returns {string} Formatted output
*/
formatOutput(analysis, options = {}) {
if (options.json) {
return JSON.stringify(analysis, null, 2);
}
const lines = [];
lines.push(`Wave Analysis: ${analysis.workflowId}`);
lines.push('═'.repeat(40));
lines.push('');
if (options.visual) {
for (const wave of analysis.waves) {
const prefix = `Wave ${wave.waveNumber} `;
if (wave.tasks.length === 1) {
lines.push(`${prefix}──────── ${wave.tasks[0]} (${wave.estimatedDuration})`);
} else {
lines.push(`${prefix}──┬── ${wave.tasks[0]} (${wave.estimatedDuration})`);
for (let i = 1; i < wave.tasks.length; i++) {
const connector = i === wave.tasks.length - 1 ? '└' : '├';
lines.push(` ${connector}── ${wave.tasks[i]}`);
}
}
if (wave.waveNumber < analysis.waves.length) {
lines.push(' │');
}
}
} else {
for (const wave of analysis.waves) {
const parallelIndicator = wave.parallel ? '(parallel)' : '';
lines.push(`Wave ${wave.waveNumber} ${parallelIndicator}:`);
for (const task of wave.tasks) {
lines.push(` └─ ${task}`);
}
lines.push('');
}
}
lines.push('');
lines.push(`Total Sequential: ${analysis.metrics?.sequentialTime || 'N/A'}`);
lines.push(`Total Parallel: ${analysis.metrics?.parallelTime || 'N/A'}`);
lines.push(`Optimization: ${analysis.optimizationGain} faster`);
lines.push('');
lines.push(`Critical Path: ${analysis.criticalPath.join(' → ')}`);
return lines.join('\n');
}
}
/**
* Create a new WaveAnalyzer instance
* @param {Object} options - Configuration options
* @returns {WaveAnalyzer} New analyzer instance
*/
function createWaveAnalyzer(options = {}) {
return new WaveAnalyzer(options);
}
/**
* Analyze waves for a workflow (convenience function)
* @param {string} workflowId - Workflow identifier
* @param {Object} options - Analysis options
* @returns {Object} Wave analysis result
*/
function analyzeWaves(workflowId, options = {}) {
const analyzer = createWaveAnalyzer(options);
return analyzer.analyzeWaves(workflowId, options);
}
module.exports = {
WaveAnalyzer,
CircularDependencyError,
createWaveAnalyzer,
analyzeWaves,
DEFAULT_TASK_DURATIONS,
};