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.

559 lines (558 loc) 16.9 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 { logger } from "../monitoring/logger.js"; class ResolutionEngine { conflictDetector; diffVisualizer; resolutionHistory = /* @__PURE__ */ new Map(); constructor() { this.conflictDetector = new ConflictDetector(); this.diffVisualizer = new StackDiffVisualizer(); } /** * Resolve conflicts using the specified strategy */ async resolveConflicts(stack1, stack2, strategy, context) { const conflicts = this.conflictDetector.detectConflicts(stack1, stack2); logger.info( `Resolving ${conflicts.length} conflicts using ${strategy} strategy`, { userId: context.userId, userRole: context.userRole } ); let resolution; switch (strategy) { case "keep_both": resolution = await this.keepBothStrategy( conflicts, stack1, stack2, context ); break; case "team_vote": resolution = await this.teamVoteStrategy( conflicts, stack1, stack2, context ); break; case "senior_override": resolution = await this.seniorOverrideStrategy( conflicts, stack1, stack2, context ); break; case "ai_suggest": resolution = await this.aiSuggestStrategy( conflicts, stack1, stack2, context ); break; case "hybrid": resolution = await this.hybridStrategy( conflicts, stack1, stack2, context ); break; default: throw new Error(`Unknown resolution strategy: ${strategy}`); } const mergeResult = await this.executeMerge( stack1, stack2, conflicts, resolution ); this.storeResolution(mergeResult.mergedFrameId || "", resolution); return mergeResult; } /** * Strategy: Keep Both Solutions * Creates a merged frame that includes both approaches */ async keepBothStrategy(conflicts, stack1, stack2, context) { logger.info("Applying keep_both strategy"); const strategy = { type: "keep_both", confidence: 0.8, reasoning: "Preserving both solutions to maintain all work and allow future evaluation" }; for (const conflict of conflicts) { conflict.resolution = { strategy, resolvedBy: context.userId, resolvedAt: Date.now(), notes: "Both solutions preserved in merged frame" }; } return { strategy, resolvedBy: context.userId, resolvedAt: Date.now(), notes: `Kept all ${stack1.frames.length + stack2.frames.length} frames from both stacks` }; } /** * Strategy: Team Vote * Uses democratic voting to choose between options */ async teamVoteStrategy(conflicts, stack1, stack2, context) { logger.info("Applying team_vote strategy"); if (!context.teamVotes || context.teamVotes.length === 0) { throw new Error("Team vote strategy requires votes from team members"); } const voteResults = this.countVotes(context.teamVotes); const strategy = { type: "team_vote", confidence: this.calculateVoteConfidence(voteResults), reasoning: `Team consensus from ${context.teamVotes.length} votes`, votes: context.teamVotes }; for (const conflict of conflicts) { conflict.resolution = { strategy, resolvedBy: "team_consensus", resolvedAt: Date.now(), notes: `Resolved by ${voteResults.consensus}% consensus` }; } return { strategy, resolvedBy: "team_consensus", resolvedAt: Date.now(), notes: `Democratic resolution with ${voteResults.consensus}% agreement` }; } /** * Strategy: Senior Override * Senior developer's choice takes precedence */ async seniorOverrideStrategy(conflicts, stack1, stack2, context) { logger.info("Applying senior_override strategy"); if (context.userRole !== "senior" && context.userRole !== "lead") { throw new Error("Senior override requires senior or lead role"); } this.determinePreferredStack(stack1, stack2, context); const strategy = { type: "senior_override", confidence: 0.95, reasoning: `Senior developer (${context.userId}) selected based on experience and architectural knowledge` }; for (const conflict of conflicts) { conflict.resolution = { strategy, resolvedBy: context.userId, resolvedAt: Date.now(), notes: `Overridden by ${context.userRole} authority` }; } return { strategy, resolvedBy: context.userId, resolvedAt: Date.now(), notes: `Senior override applied to all ${conflicts.length} conflicts` }; } /** * Strategy: AI Suggest * Uses AI analysis to recommend best resolution */ async aiSuggestStrategy(conflicts, stack1, stack2, context) { logger.info("Applying ai_suggest strategy"); const analysis = await this.analyzeFrameQuality(stack1, stack2); const strategy = { type: "ai_suggest", confidence: context.aiConfidence || 0.85, reasoning: this.generateAIReasoning(analysis) }; for (const conflict of conflicts) { const recommendation = this.getAIRecommendation(conflict, analysis); conflict.resolution = { strategy, resolvedBy: "ai_system", resolvedAt: Date.now(), notes: recommendation }; } return { strategy, resolvedBy: "ai_system", resolvedAt: Date.now(), notes: `AI analysis with ${strategy.confidence * 100}% confidence` }; } /** * Strategy: Hybrid * Combines multiple strategies based on conflict type */ async hybridStrategy(conflicts, stack1, stack2, context) { logger.info("Applying hybrid strategy"); const strategy = { type: "hybrid", confidence: 0.9, reasoning: "Using optimal strategy for each conflict type" }; for (const conflict of conflicts) { let subStrategy; switch (conflict.type) { case "parallel_solution": subStrategy = "keep_both"; break; case "conflicting_decision": subStrategy = "ai_suggest"; break; case "structural_divergence": subStrategy = context.userRole === "senior" || context.userRole === "lead" ? "senior_override" : "team_vote"; break; default: subStrategy = "ai_suggest"; } conflict.resolution = { strategy: { ...strategy, reasoning: `${subStrategy} for ${conflict.type}` }, resolvedBy: context.userId, resolvedAt: Date.now(), notes: `Hybrid resolution using ${subStrategy}` }; } return { strategy, resolvedBy: context.userId, resolvedAt: Date.now(), notes: `Hybrid strategy optimized for each conflict type` }; } /** * Execute the merge based on resolution */ async executeMerge(stack1, stack2, conflicts, resolution) { const mergedFrameId = uuidv4(); const rollbackPoint = this.createRollbackPoint(stack1, stack2); const notifications = []; try { const mergedFrames = this.mergeFrames( stack1, stack2, conflicts, resolution ); const isValid = this.validateMerge(mergedFrames, conflicts); if (!isValid) { throw new Error("Merge validation failed"); } const notifyResults = await this.sendNotifications(conflicts, resolution); notifications.push(...notifyResults); logger.info("Merge executed successfully", { mergedFrameId, frameCount: mergedFrames.length, strategy: resolution.strategy.type }); return { success: true, mergedFrameId, conflicts, resolution, rollbackPoint, notifications }; } catch (error) { logger.error("Merge execution failed", error); return { success: false, conflicts, resolution, rollbackPoint, notifications }; } } /** * Count votes from team members */ countVotes(votes) { const counts = { frame1: 0, frame2: 0, both: 0, neither: 0, total: votes.length, consensus: 0 }; for (const vote of votes) { counts[vote.choice]++; } const maxVotes = Math.max( counts.frame1, counts.frame2, counts.both, counts.neither ); counts.consensus = Math.round(maxVotes / counts.total * 100); return counts; } /** * Calculate confidence based on vote distribution */ calculateVoteConfidence(voteResults) { if (voteResults.consensus >= 80) return 0.95; if (voteResults.consensus >= 60) return 0.75; if (voteResults.consensus >= 40) return 0.5; return 0.3; } /** * Determine winner from vote results */ determineVoteWinner(conflict, voteResults) { if (voteResults.frame1 > voteResults.frame2) return conflict.frameId1; if (voteResults.frame2 > voteResults.frame1) return conflict.frameId2; if (voteResults.both > voteResults.neither) return "both"; return "neither"; } /** * Determine preferred stack for senior override */ determinePreferredStack(stack1, stack2, context) { if (stack1.owner === context.userId) return stack1; if (stack2.owner === context.userId) return stack2; if (stack1.lastModified > stack2.lastModified) return stack1; if (stack2.frames.filter((f) => f.state === "closed").length > stack1.frames.filter((f) => f.state === "closed").length) { return stack2; } return stack1; } /** * Analyze frame quality for AI suggestions */ async analyzeFrameQuality(stack1, stack2) { const analysis = { stack1: { completeness: this.calculateCompleteness(stack1), efficiency: this.calculateEfficiency(stack1), quality: this.calculateQuality(stack1) }, stack2: { completeness: this.calculateCompleteness(stack2), efficiency: this.calculateEfficiency(stack2), quality: this.calculateQuality(stack2) } }; return analysis; } /** * Calculate stack completeness */ calculateCompleteness(stack) { const closedFrames = stack.frames.filter( (f) => f.state === "closed" ).length; return closedFrames / stack.frames.length; } /** * Calculate stack efficiency */ calculateEfficiency(stack) { let totalDuration = 0; let completedFrames = 0; for (const frame of stack.frames) { if (frame.closed_at && frame.created_at) { totalDuration += frame.closed_at - frame.created_at; completedFrames++; } } if (completedFrames === 0) return 0; const avgDuration = totalDuration / completedFrames; return Math.max(0, Math.min(1, 3e5 / avgDuration)); } /** * Calculate stack quality */ calculateQuality(stack) { let qualityScore = 0; for (const frame of stack.frames) { if (frame.outputs && Object.keys(frame.outputs).length > 0) qualityScore += 0.3; if (frame.digest_text) qualityScore += 0.3; if (frame.state === "closed") qualityScore += 0.4; } return Math.min(1, qualityScore / stack.frames.length); } /** * Generate AI reasoning for resolution */ generateAIReasoning(analysis) { const stack1Score = analysis.stack1.completeness * 0.3 + analysis.stack1.efficiency * 0.3 + analysis.stack1.quality * 0.4; const stack2Score = analysis.stack2.completeness * 0.3 + analysis.stack2.efficiency * 0.3 + analysis.stack2.quality * 0.4; if (stack1Score > stack2Score) { return `Stack 1 shows higher overall quality (${(stack1Score * 100).toFixed(1)}% vs ${(stack2Score * 100).toFixed(1)}%)`; } else { return `Stack 2 shows higher overall quality (${(stack2Score * 100).toFixed(1)}% vs ${(stack1Score * 100).toFixed(1)}%)`; } } /** * Get AI recommendation for specific conflict */ getAIRecommendation(conflict) { switch (conflict.type) { case "parallel_solution": return "Recommend keeping both solutions for A/B testing"; case "conflicting_decision": return "Recommend the decision with higher quality score"; case "structural_divergence": return "Recommend restructuring to accommodate both approaches"; default: return "Recommend manual review for this conflict"; } } /** * Create rollback point before merge */ createRollbackPoint(_stack1, _stack2) { const rollbackId = uuidv4(); logger.info("Created rollback point", { rollbackId }); return rollbackId; } /** * Merge frames based on resolution */ mergeFrames(stack1, stack2, conflicts, resolution) { const mergedFrames = []; const processedIds = /* @__PURE__ */ new Set(); switch (resolution.strategy.type) { case "keep_both": mergedFrames.push(...stack1.frames, ...stack2.frames); break; case "team_vote": case "senior_override": case "ai_suggest": for (const frame of stack1.frames) { const conflict = conflicts.find((c) => c.frameId1 === frame.frame_id); if (!conflict || conflict.resolution?.strategy.type === "keep_both") { mergedFrames.push(frame); processedIds.add(frame.frame_id); } } for (const frame of stack2.frames) { if (!processedIds.has(frame.frame_id)) { const conflict = conflicts.find( (c) => c.frameId2 === frame.frame_id ); if (!conflict || conflict.resolution?.strategy.type === "keep_both") { mergedFrames.push(frame); } } } break; case "hybrid": this.hybridMerge(stack1, stack2, conflicts, mergedFrames); break; } return mergedFrames; } /** * Hybrid merge implementation */ hybridMerge(stack1, stack2, conflicts, mergedFrames) { const conflictMap = /* @__PURE__ */ new Map(); for (const conflict of conflicts) { conflictMap.set(conflict.frameId1, conflict); conflictMap.set(conflict.frameId2, conflict); } for (const frame of [...stack1.frames, ...stack2.frames]) { const conflict = conflictMap.get(frame.frame_id); if (!conflict) { if (!mergedFrames.find((f) => f.frame_id === frame.frame_id)) { mergedFrames.push(frame); } } else if (conflict.type === "parallel_solution") { if (!mergedFrames.find((f) => f.frame_id === frame.frame_id)) { mergedFrames.push(frame); } } } } /** * Validate merge integrity */ validateMerge(mergedFrames, conflicts) { const ids = /* @__PURE__ */ new Set(); for (const frame of mergedFrames) { if (ids.has(frame.frame_id)) { logger.error("Duplicate frame ID in merge", { frameId: frame.frame_id }); return false; } ids.add(frame.frame_id); } for (const frame of mergedFrames) { if (frame.parent_frame_id) { const parent = mergedFrames.find( (f) => f.frame_id === frame.parent_frame_id ); if (!parent) { logger.warn("Orphaned frame in merge", { frameId: frame.frame_id }); } } } for (const conflict of conflicts) { if (!conflict.resolution) { logger.error("Unresolved conflict in merge", { conflictId: conflict.id }); return false; } } return true; } /** * Send notifications about merge */ async sendNotifications(conflicts, resolution) { const notifications = []; const notification = { userId: resolution.resolvedBy || "team", type: "in-app", sent: true, timestamp: Date.now() }; notifications.push(notification); logger.info("Notifications sent", { count: notifications.length }); return notifications; } /** * Store resolution in history */ storeResolution(mergeId, resolution) { this.resolutionHistory.set(mergeId, resolution); logger.info("Resolution stored in history", { mergeId, strategy: resolution.strategy.type }); } /** * Get resolution history for analysis */ getResolutionHistory() { return this.resolutionHistory; } } export { ResolutionEngine }; //# sourceMappingURL=resolution-engine.js.map