vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
797 lines (792 loc) • 37.3 kB
JavaScript
import { getEpicService } from './epic-service.js';
import { getTaskOperations } from '../core/operations/task-operations.js';
import { getDependencyOperations } from '../core/operations/dependency-operations.js';
import { DependencyValidator } from './dependency-validator.js';
import logger from '../../../logger.js';
const DEFAULT_EPIC_CONFIG = {
minDependencyStrength: 0.3,
maxEpicDepth: 5,
autoGeneratePhases: true,
enableParallelization: true,
minTasksPerEpic: 2,
enableIntelligentDiscovery: true,
enableFilePathAnalysis: true,
enableSemanticAnalysis: true
};
export class EpicDependencyManager {
config;
dependencyValidator;
constructor(config = {}) {
this.config = { ...DEFAULT_EPIC_CONFIG, ...config };
this.dependencyValidator = new DependencyValidator();
}
async analyzeEpicDependencies(projectId) {
const startTime = Date.now();
try {
logger.info({ projectId }, 'Starting epic dependency analysis');
const epicService = getEpicService();
const taskOps = getTaskOperations();
const dependencyOps = getDependencyOperations();
const epicsResult = await epicService.listEpics({ projectId });
if (!epicsResult.success) {
throw new Error(`Failed to get epics: ${epicsResult.error}`);
}
const tasksResult = await taskOps.listTasks({ projectId });
if (!tasksResult.success) {
throw new Error(`Failed to get tasks: ${tasksResult.error}`);
}
const epics = epicsResult.data || [];
const tasks = tasksResult.data || [];
const allTaskDependencies = [];
for (const task of tasks) {
const taskDepsResult = await dependencyOps.getDependenciesForTask(task.id);
if (taskDepsResult.success && taskDepsResult.data) {
allTaskDependencies.push(...taskDepsResult.data);
}
}
const epicDependencies = await this.deriveEpicDependencies(epics, tasks, allTaskDependencies);
const epicExecutionOrder = await this.calculateEpicExecutionOrder(epics, epicDependencies);
const phases = this.config.autoGeneratePhases
? await this.generateProjectPhases(epics, epicDependencies, epicExecutionOrder)
: [];
const conflicts = await this.detectEpicConflicts(epics, epicDependencies, tasks);
const recommendations = await this.generateEpicRecommendations(epics, epicDependencies, tasks, allTaskDependencies);
const analysisTime = Date.now() - startTime;
const analysis = {
epicDependencies,
epicExecutionOrder,
phases,
conflicts,
recommendations,
metadata: {
analyzedAt: new Date(),
projectId,
totalEpics: epics.length,
totalTaskDependencies: allTaskDependencies.length,
analysisTime
}
};
logger.info({
projectId,
epicDependencies: epicDependencies.length,
phases: phases.length,
conflicts: conflicts.length,
recommendations: recommendations.length,
analysisTime
}, 'Epic dependency analysis completed');
return {
success: true,
data: analysis,
metadata: {
filePath: 'epic-dependency-manager',
operation: 'analyze_epic_dependencies',
timestamp: new Date()
}
};
}
catch (error) {
const analysisTime = Date.now() - startTime;
logger.error({
err: error,
projectId,
analysisTime
}, 'Epic dependency analysis failed');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: 'epic-dependency-manager',
operation: 'analyze_epic_dependencies',
timestamp: new Date()
}
};
}
}
async createEpicDependency(fromEpicId, toEpicId, taskDependencies, createdBy = 'system') {
try {
logger.info({
fromEpicId,
toEpicId,
taskDependencyCount: taskDependencies.length,
createdBy
}, 'Creating epic dependency');
const validationResult = await this.validateEpicDependency(fromEpicId, toEpicId);
if (!validationResult.isValid) {
return {
success: false,
error: `Epic dependency validation failed: ${validationResult.errors.map((e) => e.message).join(', ')}`,
metadata: {
filePath: 'epic-dependency-manager',
operation: 'create_epic_dependency',
timestamp: new Date()
}
};
}
const strength = await this.calculateDependencyStrength(fromEpicId, toEpicId, taskDependencies);
const epicDependency = {
id: `epic-dep-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
fromEpicId,
toEpicId,
type: strength > 0.7 ? 'blocks' : strength > 0.5 ? 'requires' : 'suggests',
description: `Epic dependency derived from ${taskDependencies.length} task dependencies`,
critical: strength > 0.7,
strength,
metadata: {
createdAt: new Date(),
createdBy,
reason: `Derived from task dependencies with strength ${strength.toFixed(2)}`,
taskDependencies
}
};
await this.updateEpicDependencyLists(fromEpicId, toEpicId, epicDependency.id);
logger.info({
epicDependencyId: epicDependency.id,
fromEpicId,
toEpicId,
strength
}, 'Epic dependency created successfully');
return {
success: true,
data: epicDependency,
metadata: {
filePath: 'epic-dependency-manager',
operation: 'create_epic_dependency',
timestamp: new Date()
}
};
}
catch (error) {
logger.error({
err: error,
fromEpicId,
toEpicId
}, 'Failed to create epic dependency');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: 'epic-dependency-manager',
operation: 'create_epic_dependency',
timestamp: new Date()
}
};
}
}
async deriveEpicDependencies(epics, tasks, taskDependencies) {
const epicDependencies = [];
const epicTaskMap = new Map();
epics.forEach(epic => {
epicTaskMap.set(epic.id, epic.taskIds);
});
const epicDependencyMap = new Map();
for (const taskDep of taskDependencies) {
const fromTask = tasks.find(t => t.id === taskDep.fromTaskId);
const toTask = tasks.find(t => t.id === taskDep.toTaskId);
if (!fromTask || !toTask || !fromTask.epicId || !toTask.epicId)
continue;
if (fromTask.epicId === toTask.epicId)
continue;
const epicPairKey = `${fromTask.epicId}->${toTask.epicId}`;
if (!epicDependencyMap.has(epicPairKey)) {
epicDependencyMap.set(epicPairKey, []);
}
epicDependencyMap.get(epicPairKey).push(taskDep.id);
}
for (const [epicPairKey, taskDepIds] of epicDependencyMap) {
const [fromEpicId, toEpicId] = epicPairKey.split('->');
const strength = await this.calculateDependencyStrength(fromEpicId, toEpicId, taskDepIds);
if (strength >= this.config.minDependencyStrength) {
const epicDependency = {
id: `epic-dep-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
fromEpicId,
toEpicId,
type: strength > 0.7 ? 'blocks' : strength > 0.5 ? 'requires' : 'suggests',
description: `Derived from ${taskDepIds.length} task dependencies (strength: ${strength.toFixed(2)})`,
critical: strength > 0.7,
strength,
metadata: {
createdAt: new Date(),
createdBy: 'system',
reason: `Auto-derived from task dependency analysis`,
taskDependencies: taskDepIds
}
};
epicDependencies.push(epicDependency);
}
}
return epicDependencies;
}
async calculateDependencyStrength(fromEpicId, toEpicId, taskDependencies) {
try {
const epicService = getEpicService();
const fromEpicResult = await epicService.getEpic(fromEpicId);
const toEpicResult = await epicService.getEpic(toEpicId);
if (!fromEpicResult.success || !toEpicResult.success) {
return 0;
}
const fromEpic = fromEpicResult.data;
const toEpic = toEpicResult.data;
const fromTaskCount = fromEpic.taskIds.length;
const toTaskCount = toEpic.taskIds.length;
const dependencyCount = taskDependencies.length;
const maxPossibleDependencies = fromTaskCount * toTaskCount;
const densityStrength = maxPossibleDependencies > 0 ? dependencyCount / maxPossibleDependencies : 0;
const proportionStrength = Math.min(dependencyCount / Math.max(fromTaskCount, toTaskCount), 1);
const strength = (densityStrength * 0.4) + (proportionStrength * 0.6);
return Math.min(strength, 1);
}
catch (error) {
logger.warn({
err: error,
fromEpicId,
toEpicId
}, 'Failed to calculate dependency strength');
return 0;
}
}
async calculateEpicExecutionOrder(epics, epicDependencies) {
const inDegree = new Map();
const adjacencyList = new Map();
epics.forEach(epic => {
inDegree.set(epic.id, 0);
adjacencyList.set(epic.id, []);
});
epicDependencies.forEach(dep => {
adjacencyList.get(dep.fromEpicId)?.push(dep.toEpicId);
inDegree.set(dep.toEpicId, (inDegree.get(dep.toEpicId) || 0) + 1);
});
const queue = [];
const result = [];
for (const [epicId, degree] of inDegree) {
if (degree === 0) {
queue.push(epicId);
}
}
while (queue.length > 0) {
const epicId = queue.shift();
result.push(epicId);
const dependents = adjacencyList.get(epicId) || [];
for (const dependent of dependents) {
const newDegree = (inDegree.get(dependent) || 0) - 1;
inDegree.set(dependent, newDegree);
if (newDegree === 0) {
queue.push(dependent);
}
}
}
return result;
}
async generateProjectPhases(epics, epicDependencies, executionOrder) {
const phases = [];
const epicToPhase = new Map();
let currentPhase = 0;
const processedEpics = new Set();
while (processedEpics.size < epics.length) {
const currentPhaseEpics = [];
for (const epicId of executionOrder) {
if (processedEpics.has(epicId))
continue;
const dependencies = epicDependencies.filter(dep => dep.toEpicId === epicId);
const allDependenciesSatisfied = dependencies.every(dep => processedEpics.has(dep.fromEpicId));
if (allDependenciesSatisfied) {
currentPhaseEpics.push(epicId);
epicToPhase.set(epicId, currentPhase);
}
}
if (currentPhaseEpics.length === 0) {
for (const epicId of executionOrder) {
if (!processedEpics.has(epicId)) {
currentPhaseEpics.push(epicId);
epicToPhase.set(epicId, currentPhase);
}
}
}
const phaseEpics = epics.filter(epic => currentPhaseEpics.includes(epic.id));
const estimatedDuration = Math.max(...phaseEpics.map(epic => epic.estimatedHours));
const phase = {
id: `phase-${currentPhase + 1}`,
name: `Phase ${currentPhase + 1}`,
description: `Project phase containing ${currentPhaseEpics.length} epic(s)`,
epicIds: currentPhaseEpics,
order: currentPhase,
estimatedDuration,
canRunInParallel: currentPhaseEpics.length > 1,
prerequisites: currentPhase > 0 ? [`phase-${currentPhase}`] : []
};
phases.push(phase);
currentPhaseEpics.forEach(epicId => processedEpics.add(epicId));
currentPhase++;
}
return phases;
}
async detectEpicConflicts(epics, epicDependencies, tasks) {
const conflicts = [];
const circularDeps = await this.detectCircularEpicDependencies(epics, epicDependencies);
conflicts.push(...circularDeps);
const priorityConflicts = await this.detectPriorityConflicts(epics, epicDependencies);
conflicts.push(...priorityConflicts);
const resourceConflicts = await this.detectResourceConflicts(epics, tasks);
conflicts.push(...resourceConflicts);
return conflicts;
}
async generateEpicRecommendations(epics, epicDependencies, tasks, _taskDependencies) {
const recommendations = [];
if (this.config.enableParallelization) {
const parallelizationRecs = await this.identifyParallelizationOpportunities(epics, epicDependencies);
recommendations.push(...parallelizationRecs);
}
const splittingRecs = await this.identifyEpicSplittingOpportunities(epics, tasks);
recommendations.push(...splittingRecs);
const mergingRecs = await this.identifyEpicMergingOpportunities(epics, epicDependencies);
recommendations.push(...mergingRecs);
return recommendations;
}
async validateEpicDependency(fromEpicId, toEpicId) {
return await this.dependencyValidator.validateDependencyBeforeCreation(fromEpicId, toEpicId, 'project-id');
}
async updateEpicDependencyLists(fromEpicId, toEpicId, dependencyId) {
try {
const epicService = getEpicService();
const toEpicResult = await epicService.getEpic(toEpicId);
if (toEpicResult.success) {
const toEpic = toEpicResult.data;
if (!toEpic.dependencies.includes(fromEpicId)) {
toEpic.dependencies.push(fromEpicId);
await epicService.updateEpic(toEpicId, { dependencies: toEpic.dependencies });
}
}
}
catch (error) {
logger.warn({
err: error,
fromEpicId,
toEpicId,
dependencyId
}, 'Failed to update epic dependency lists');
}
}
async detectCircularEpicDependencies(epics, epicDependencies) {
const conflicts = [];
const visited = new Set();
const recursionStack = new Set();
const adjacencyList = new Map();
epics.forEach(epic => adjacencyList.set(epic.id, []));
epicDependencies.forEach(dep => {
const dependents = adjacencyList.get(dep.fromEpicId) || [];
dependents.push(dep.toEpicId);
adjacencyList.set(dep.fromEpicId, dependents);
});
const dfs = (epicId, path) => {
if (recursionStack.has(epicId)) {
const cycleStart = path.indexOf(epicId);
const cycle = path.slice(cycleStart).concat([epicId]);
conflicts.push({
type: 'circular_dependency',
severity: 'critical',
description: `Circular epic dependency detected: ${cycle.join(' → ')}`,
affectedEpics: cycle,
resolutionOptions: [{
type: 'reorder',
description: 'Reorder epics to break the circular dependency',
complexity: 'medium'
}]
});
return true;
}
if (visited.has(epicId)) {
return false;
}
visited.add(epicId);
recursionStack.add(epicId);
path.push(epicId);
const dependents = adjacencyList.get(epicId) || [];
for (const dependent of dependents) {
if (dfs(dependent, [...path])) {
}
}
recursionStack.delete(epicId);
return false;
};
for (const epic of epics) {
if (!visited.has(epic.id)) {
dfs(epic.id, []);
}
}
return conflicts;
}
async detectPriorityConflicts(epics, epicDependencies) {
const conflicts = [];
const priorityOrder = { 'critical': 4, 'high': 3, 'medium': 2, 'low': 1 };
for (const dep of epicDependencies) {
const fromEpic = epics.find(e => e.id === dep.fromEpicId);
const toEpic = epics.find(e => e.id === dep.toEpicId);
if (!fromEpic || !toEpic)
continue;
const fromPriority = priorityOrder[fromEpic.priority] || 0;
const toPriority = priorityOrder[toEpic.priority] || 0;
if (fromPriority < toPriority) {
conflicts.push({
type: 'priority_mismatch',
severity: 'medium',
description: `Lower priority epic "${fromEpic.title}" blocks higher priority epic "${toEpic.title}"`,
affectedEpics: [fromEpic.id, toEpic.id],
resolutionOptions: [{
type: 'adjust_priority',
description: 'Adjust epic priorities to match dependency order',
complexity: 'low'
}]
});
}
}
return conflicts;
}
async detectResourceConflicts(epics, tasks) {
const conflicts = [];
for (let i = 0; i < epics.length; i++) {
for (let j = i + 1; j < epics.length; j++) {
const epic1 = epics[i];
const epic2 = epics[j];
const epic1Tasks = tasks.filter(t => t.epicId === epic1.id);
const epic2Tasks = tasks.filter(t => t.epicId === epic2.id);
const epic1Files = new Set(epic1Tasks.flatMap(t => t.filePaths));
const epic2Files = new Set(epic2Tasks.flatMap(t => t.filePaths));
const commonFiles = [...epic1Files].filter(file => epic2Files.has(file));
if (commonFiles.length > 0) {
conflicts.push({
type: 'resource_conflict',
severity: 'low',
description: `Epics "${epic1.title}" and "${epic2.title}" modify common files: ${commonFiles.join(', ')}`,
affectedEpics: [epic1.id, epic2.id],
resolutionOptions: [{
type: 'reorder',
description: 'Ensure epics that modify common files are properly sequenced',
complexity: 'medium'
}]
});
}
}
}
return conflicts;
}
async identifyParallelizationOpportunities(epics, epicDependencies) {
const recommendations = [];
const dependencyMap = new Map();
epicDependencies.forEach(dep => {
if (!dependencyMap.has(dep.toEpicId)) {
dependencyMap.set(dep.toEpicId, []);
}
dependencyMap.get(dep.toEpicId).push(dep.fromEpicId);
});
const independentEpics = epics.filter(epic => !dependencyMap.has(epic.id));
if (independentEpics.length > 1) {
recommendations.push({
type: 'parallelization',
description: `${independentEpics.length} epics can be executed in parallel`,
affectedEpics: independentEpics.map(e => e.id),
estimatedBenefit: 'Reduced overall project timeline',
implementationComplexity: 'low',
priority: 'high'
});
}
return recommendations;
}
async identifyEpicSplittingOpportunities(epics, tasks) {
const recommendations = [];
for (const epic of epics) {
const epicTasks = tasks.filter(t => t.epicId === epic.id);
if (epicTasks.length > 10) {
recommendations.push({
type: 'splitting',
description: `Epic "${epic.title}" has ${epicTasks.length} tasks and could be split for better management`,
affectedEpics: [epic.id],
estimatedBenefit: 'Better task organization and parallel execution',
implementationComplexity: 'medium',
priority: 'medium'
});
}
}
return recommendations;
}
async identifyEpicMergingOpportunities(epics, epicDependencies) {
const recommendations = [];
for (const dep of epicDependencies) {
if (dep.strength > 0.8 && dep.critical) {
const fromEpic = epics.find(e => e.id === dep.fromEpicId);
const toEpic = epics.find(e => e.id === dep.toEpicId);
if (fromEpic && toEpic && fromEpic.taskIds.length < 5 && toEpic.taskIds.length < 5) {
recommendations.push({
type: 'merging',
description: `Epics "${fromEpic.title}" and "${toEpic.title}" have strong dependency and could be merged`,
affectedEpics: [fromEpic.id, toEpic.id],
estimatedBenefit: 'Simplified project structure and reduced coordination overhead',
implementationComplexity: 'high',
priority: 'low'
});
}
}
}
return recommendations;
}
async discoverIntelligentRelationships(projectId, epics, tasks) {
try {
logger.info({ projectId, epicsCount: epics.length }, 'Starting intelligent relationship discovery');
const discoveredDependencies = [];
const semanticRelationships = [];
const filePathRelationships = [];
const reasoning = [];
if (this.config.enableFilePathAnalysis) {
const filePathResults = await this.analyzeFilePathRelationships(epics, tasks);
filePathRelationships.push(...filePathResults.relationships);
reasoning.push(...filePathResults.reasoning);
for (const fpRel of filePathResults.relationships) {
if (fpRel.severity === 'high') {
discoveredDependencies.push({
id: `fp-${fpRel.fromEpicId}-${fpRel.toEpicId}`,
fromEpicId: fpRel.fromEpicId,
toEpicId: fpRel.toEpicId,
type: 'requires',
description: `File path dependency: ${fpRel.sharedFilePaths.join(', ')}`,
critical: true,
strength: 0.8,
metadata: {
createdAt: new Date(),
createdBy: 'intelligent-discovery',
reason: `File path conflict analysis: ${fpRel.resolutionSuggestion}`,
taskDependencies: []
}
});
reasoning.push(`Created dependency from file path analysis: ${fpRel.resolutionSuggestion}`);
}
}
}
if (this.config.enableSemanticAnalysis) {
const semanticResults = await this.analyzeSemanticRelationships(epics, tasks);
semanticRelationships.push(...semanticResults.relationships);
reasoning.push(...semanticResults.reasoning);
for (const semRel of semanticResults.relationships) {
if (semRel.confidence > 0.7 && semRel.strength > 0.6) {
discoveredDependencies.push({
id: `sem-${semRel.fromEpicId}-${semRel.toEpicId}`,
fromEpicId: semRel.fromEpicId,
toEpicId: semRel.toEpicId,
type: semRel.relationshipType === 'temporal' ? 'blocks' : 'enables',
description: semRel.description,
critical: semRel.confidence > 0.8,
strength: semRel.strength,
metadata: {
createdAt: new Date(),
createdBy: 'semantic-analysis',
reason: `Semantic relationship analysis: ${semRel.description}`,
taskDependencies: []
}
});
reasoning.push(`Created dependency from semantic analysis: ${semRel.description}`);
}
}
}
const confidence = this.calculateDiscoveryConfidence(discoveredDependencies.length, semanticRelationships.length, filePathRelationships.length);
const result = {
discoveredDependencies,
semanticRelationships,
filePathRelationships,
confidence,
reasoning
};
logger.info({
projectId,
discoveredCount: discoveredDependencies.length,
semanticCount: semanticRelationships.length,
filePathCount: filePathRelationships.length,
confidence
}, 'Completed intelligent relationship discovery');
return result;
}
catch (error) {
logger.error({ err: error, projectId }, 'Failed to discover intelligent relationships');
return {
discoveredDependencies: [],
semanticRelationships: [],
filePathRelationships: [],
confidence: 0,
reasoning: [`Error during discovery: ${error}`]
};
}
}
async analyzeFilePathRelationships(epics, tasks) {
const relationships = [];
const reasoning = [];
const epicTaskMap = new Map();
epics.forEach(epic => {
epicTaskMap.set(epic.id, tasks.filter(task => task.epicId === epic.id));
});
for (const [fromEpicId, fromTasks] of epicTaskMap.entries()) {
for (const [toEpicId, toTasks] of epicTaskMap.entries()) {
if (fromEpicId === toEpicId)
continue;
const sharedPaths = [];
const conflictTypes = [];
fromTasks.forEach(fromTask => {
fromTask.filePaths.forEach(fromPath => {
toTasks.forEach(toTask => {
if (toTask.filePaths.includes(fromPath)) {
if (!sharedPaths.includes(fromPath)) {
sharedPaths.push(fromPath);
if (fromTask.type === 'development' && toTask.type === 'development') {
conflictTypes.push('modification');
}
else if (fromTask.type === 'testing') {
conflictTypes.push('read_dependency');
}
else {
conflictTypes.push('creation');
}
}
}
});
});
});
if (sharedPaths.length > 0) {
const severity = sharedPaths.length > 3 ? 'high' : sharedPaths.length > 1 ? 'medium' : 'low';
const primaryConflictType = conflictTypes[0] || 'modification';
const fromEpic = epics.find(e => e.id === fromEpicId);
const toEpic = epics.find(e => e.id === toEpicId);
const resolutionSuggestion = severity === 'high'
? `Consider sequencing "${fromEpic?.title}" before "${toEpic?.title}" due to significant file overlap`
: `Monitor coordination between "${fromEpic?.title}" and "${toEpic?.title}" for file conflicts`;
relationships.push({
fromEpicId,
toEpicId,
sharedFilePaths: sharedPaths,
conflictType: primaryConflictType,
severity: severity,
resolutionSuggestion
});
reasoning.push(`File path analysis: ${sharedPaths.length} shared paths between ${fromEpic?.title} and ${toEpic?.title}`);
}
}
}
return { relationships, reasoning };
}
async analyzeSemanticRelationships(epics, tasks) {
const relationships = [];
const reasoning = [];
try {
const { performFormatAwareLlmCall } = await import('../../../utils/llmHelper.js');
const { OpenRouterConfigManager } = await import('../../../utils/openrouter-config-manager.js');
const configManager = OpenRouterConfigManager.getInstance();
const config = await configManager.getOpenRouterConfig();
for (let i = 0; i < epics.length; i++) {
for (let j = i + 1; j < epics.length; j++) {
const epicA = epics[i];
const epicB = epics[j];
const epicATitle = epicA.title;
const epicBTitle = epicB.title;
const epicADesc = epicA.description;
const epicBDesc = epicB.description;
const epicATasks = tasks.filter(t => t.epicId === epicA.id);
const epicBTasks = tasks.filter(t => t.epicId === epicB.id);
const epicATaskTitles = epicATasks.map(t => t.title).join(', ');
const epicBTaskTitles = epicBTasks.map(t => t.title).join(', ');
const prompt = `Analyze the relationship between these two project epics:
Epic A: "${epicATitle}"
Description: ${epicADesc}
Tasks: ${epicATaskTitles}
Epic B: "${epicBTitle}"
Description: ${epicBDesc}
Tasks: ${epicBTaskTitles}
Determine if there's a meaningful relationship between these epics. Consider:
1. Conceptual relationships (similar domain/functionality)
2. Functional relationships (one provides services to the other)
3. Temporal relationships (one should happen before the other)
4. Hierarchical relationships (one is a component of the other)
Respond with JSON:
{
"hasRelationship": boolean,
"relationshipType": "conceptual|functional|temporal|hierarchical",
"direction": "A_to_B|B_to_A|bidirectional",
"strength": number (0-1),
"confidence": number (0-1),
"description": "brief explanation"
}`;
try {
const response = await performFormatAwareLlmCall(prompt, 'You are an expert software architect analyzing project dependencies.', config, 'epic_relationship_analysis', 'json');
const analysis = JSON.parse(response);
if (analysis.hasRelationship && analysis.confidence > 0.5) {
const fromEpicId = analysis.direction === 'B_to_A' ? epicB.id : epicA.id;
const toEpicId = analysis.direction === 'B_to_A' ? epicA.id : epicB.id;
relationships.push({
fromEpicId,
toEpicId,
relationshipType: analysis.relationshipType,
strength: analysis.strength,
description: analysis.description,
confidence: analysis.confidence
});
reasoning.push(`Semantic analysis found ${analysis.relationshipType} relationship between ${epicATitle} and ${epicBTitle}: ${analysis.description}`);
if (analysis.direction === 'bidirectional') {
relationships.push({
fromEpicId: toEpicId,
toEpicId: fromEpicId,
relationshipType: analysis.relationshipType,
strength: analysis.strength,
description: analysis.description,
confidence: analysis.confidence
});
}
}
}
catch (llmError) {
logger.debug({ err: llmError, epicA: epicATitle, epicB: epicBTitle }, 'LLM analysis failed for epic pair');
reasoning.push(`LLM analysis failed for ${epicATitle} - ${epicBTitle}: ${llmError}`);
}
}
}
}
catch (error) {
logger.error({ err: error }, 'Failed to perform semantic relationship analysis');
reasoning.push(`Semantic analysis error: ${error}`);
}
return { relationships, reasoning };
}
calculateDiscoveryConfidence(dependencyCount, semanticCount, filePathCount) {
let confidence = 0.3;
if (dependencyCount > 0)
confidence += 0.3;
if (semanticCount > 0)
confidence += 0.2;
if (filePathCount > 0)
confidence += 0.2;
return Math.min(confidence, 1.0);
}
async validateDiscoveredRelationships(discoveredDependencies, existingDependencies) {
const validatedDependencies = [];
const conflicts = [];
const optimizations = [];
for (const discovered of discoveredDependencies) {
const isDuplicate = existingDependencies.some(existing => existing.fromEpicId === discovered.fromEpicId &&
existing.toEpicId === discovered.toEpicId &&
existing.type === discovered.type);
if (!isDuplicate) {
validatedDependencies.push(discovered);
}
}
const allDependencies = [...existingDependencies, ...validatedDependencies];
const circularConflicts = await this.detectCircularEpicDependencies([], allDependencies);
conflicts.push(...circularConflicts);
if (validatedDependencies.length > 0) {
optimizations.push({
type: 'reordering',
description: `Consider reordering epics based on ${validatedDependencies.length} newly discovered dependencies`,
affectedEpics: [...new Set(validatedDependencies.flatMap(d => [d.fromEpicId, d.toEpicId]))],
estimatedBenefit: 'Improved project flow and reduced blocking',
implementationComplexity: 'medium',
priority: 'high'
});
}
return {
validatedDependencies,
conflicts,
optimizations
};
}
}