UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

304 lines (303 loc) 9.74 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { v4 as uuidv4 } from "uuid"; import { ConflictDetector } from "./conflict-detector.js"; import { StackDiffVisualizer } from "./stack-diff.js"; import { ResolutionEngine } from "./resolution-engine.js"; import { logger } from "../monitoring/logger.js"; class UnifiedMergeResolver { conflictDetector; diffVisualizer; resolutionEngine; activeSessions = /* @__PURE__ */ new Map(); rollbackSnapshots = /* @__PURE__ */ new Map(); statistics = { totalConflicts: 0, resolvedConflicts: 0, averageResolutionTime: 0, successRate: 0, rollbackCount: 0 }; constructor() { this.conflictDetector = new ConflictDetector(); this.diffVisualizer = new StackDiffVisualizer(); this.resolutionEngine = new ResolutionEngine(); logger.debug("UnifiedMergeResolver initialized"); } /** * Start a new merge session with automatic conflict detection */ async startMergeSession(stack1, stack2, options) { const sessionId = `unified-merge-${Date.now()}-${uuidv4().substring(0, 8)}`; const conflicts = this.conflictDetector.detectConflicts(stack1, stack2); let rollbackPoint; if (options?.preserveRollback !== false) { rollbackPoint = this.createRollbackSnapshot(sessionId, stack1, stack2); } const session = { sessionId, stack1, stack2, conflicts, status: "analyzing", rollbackPoint, startedAt: Date.now(), metadata: { totalFrames: stack1.frames.length + stack2.frames.length, conflictCount: conflicts.length, resolvedCount: 0 } }; this.activeSessions.set(sessionId, session); this.statistics.totalConflicts += conflicts.length; logger.info(`Merge session started: ${sessionId}`, { stack1Id: stack1.id, stack2Id: stack2.id, conflictCount: conflicts.length }); if (options?.autoResolve && conflicts.length > 0 && options.context) { const defaultStrategy = options.strategy || "ai_suggest"; await this.resolveConflicts(sessionId, defaultStrategy, options.context); } return sessionId; } /** * Generate a preview of the merge result */ async generatePreview(sessionId, strategy) { const session = this.activeSessions.get(sessionId); if (!session) { throw new Error(`Session not found: ${sessionId}`); } const preview = this.diffVisualizer.generateMergePreview( session.stack1, session.stack2, strategy ); session.preview = preview; session.status = "preview"; session.metadata.strategyUsed = strategy; this.activeSessions.set(sessionId, session); logger.info(`Preview generated for session: ${sessionId}`, { mergedFrameCount: preview.mergedFrames.length, estimatedSuccess: preview.estimatedSuccess }); return preview; } /** * Resolve conflicts using the specified strategy */ async resolveConflicts(sessionId, strategy, context) { const session = this.activeSessions.get(sessionId); if (!session) { throw new Error(`Session not found: ${sessionId}`); } session.status = "resolving"; try { const result = await this.resolutionEngine.resolveConflicts( session.stack1, session.stack2, strategy, context ); session.resolution = result.resolution; session.status = result.success ? "completed" : "failed"; session.completedAt = Date.now(); session.metadata.resolvedCount = session.conflicts.filter( (c) => c.resolution !== void 0 ).length; session.metadata.strategyUsed = strategy; this.activeSessions.set(sessionId, session); if (result.success) { this.statistics.resolvedConflicts += session.metadata.resolvedCount; this.updateSuccessRate(); this.updateAverageResolutionTime(session); } logger.info(`Conflicts resolved for session: ${sessionId}`, { success: result.success, strategy, resolvedCount: session.metadata.resolvedCount }); return result; } catch (error) { session.status = "failed"; this.activeSessions.set(sessionId, session); logger.error( `Failed to resolve conflicts for session: ${sessionId}`, error ); throw error; } } /** * Rollback a merge to its original state */ async rollback(sessionId) { const session = this.activeSessions.get(sessionId); if (!session || !session.rollbackPoint) { logger.warn(`Cannot rollback session: ${sessionId} - no rollback point`); return false; } const snapshot = this.rollbackSnapshots.get(session.rollbackPoint); if (!snapshot) { logger.error(`Rollback snapshot not found: ${session.rollbackPoint}`); return false; } session.stack1 = snapshot.stack1; session.stack2 = snapshot.stack2; session.status = "rolled_back"; session.resolution = void 0; session.conflicts = this.conflictDetector.detectConflicts( snapshot.stack1, snapshot.stack2 ); session.metadata.resolvedCount = 0; this.activeSessions.set(sessionId, session); this.statistics.rollbackCount++; logger.info(`Session rolled back: ${sessionId}`); return true; } /** * Get merge session details */ getSession(sessionId) { return this.activeSessions.get(sessionId); } /** * List all active merge sessions */ listActiveSessions() { return Array.from(this.activeSessions.values()).filter( (s) => s.status !== "completed" && s.status !== "rolled_back" && s.status !== "failed" ); } /** * Get merge statistics */ getStatistics() { return { ...this.statistics }; } /** * Analyze parallel solutions across stacks */ analyzeParallelSolutions(frames) { const solutions = this.conflictDetector.analyzeParallelSolutions(frames); const recommendations = []; if (solutions.length > 1) { const grouped = /* @__PURE__ */ new Map(); for (const sol of solutions) { const key = sol.approach.toLowerCase(); if (!grouped.has(key)) { grouped.set(key, []); } grouped.get(key).push(sol); } for (const [approach, group] of grouped) { if (group.length > 1) { const avgEffectiveness = group.reduce((sum, s) => sum + (s.effectiveness || 0), 0) / group.length; recommendations.push( `${group.length} parallel solutions using "${approach}" approach (avg effectiveness: ${(avgEffectiveness * 100).toFixed(1)}%)` ); } } const best = solutions.reduce( (a, b) => (a.effectiveness || 0) > (b.effectiveness || 0) ? a : b, solutions[0] ); if (best && best.effectiveness && best.effectiveness > 0.7) { recommendations.push( `Recommended: Use solution from frame "${best.frameId}" (${(best.effectiveness * 100).toFixed(1)}% effectiveness)` ); } } return { solutions: solutions.map((s) => ({ frameId: s.frameId, approach: s.approach, effectiveness: s.effectiveness || 0 })), recommendations }; } /** * Create a visual diff between two stacks */ createVisualDiff(baseFrame, stack1, stack2) { const diff = this.diffVisualizer.visualizeDivergence( baseFrame, stack1, stack2 ); const conflicts = this.conflictDetector.detectConflicts(stack1, stack2); return { nodes: diff.nodes.map((n) => ({ id: n.id, type: n.type, depth: n.frame?.depth })), edges: diff.edges.map((e) => ({ source: e.source, target: e.target, type: e.type })), conflicts: conflicts.map((c) => ({ frameId1: c.frameId1, frameId2: c.frameId2, severity: c.severity })) }; } /** * Close a merge session and clean up resources */ closeSession(sessionId) { const session = this.activeSessions.get(sessionId); if (session) { if (session.rollbackPoint) { this.rollbackSnapshots.delete(session.rollbackPoint); } this.activeSessions.delete(sessionId); logger.debug(`Session closed: ${sessionId}`); } } // Private helpers createRollbackSnapshot(sessionId, stack1, stack2) { const snapshotId = `rollback-${sessionId}`; this.rollbackSnapshots.set(snapshotId, { stack1: this.deepCloneStack(stack1), stack2: this.deepCloneStack(stack2) }); return snapshotId; } deepCloneStack(stack) { return { ...stack, frames: stack.frames.map((f) => ({ ...f })), events: stack.events.map((e) => ({ ...e })) }; } updateSuccessRate() { if (this.statistics.totalConflicts > 0) { this.statistics.successRate = this.statistics.resolvedConflicts / this.statistics.totalConflicts; } } updateAverageResolutionTime(session) { if (session.completedAt && session.startedAt) { const duration = session.completedAt - session.startedAt; const completedSessions = Array.from(this.activeSessions.values()).filter( (s) => s.completedAt ).length; if (completedSessions === 1) { this.statistics.averageResolutionTime = duration; } else { this.statistics.averageResolutionTime = (this.statistics.averageResolutionTime * (completedSessions - 1) + duration) / completedSessions; } } } } export { UnifiedMergeResolver }; //# sourceMappingURL=unified-merge-resolver.js.map