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.

533 lines (532 loc) 15.1 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { ConflictDetector } from "./conflict-detector.js"; class StackDiffVisualizer { conflictDetector; constructor() { this.conflictDetector = new ConflictDetector(); } /** * Visualize divergence between two frame stacks */ visualizeDivergence(baseFrame, branch1, branch2) { const nodes = []; const edges = []; nodes.push({ id: baseFrame.frame_id, type: "common", frame: baseFrame, position: { x: 0, y: 0 }, metadata: { label: "Common Ancestor" } }); const branch1Nodes = this.processBranch( branch1, baseFrame.frame_id, -100, // Left side 100 ); nodes.push(...branch1Nodes.nodes); edges.push(...branch1Nodes.edges); const branch2Nodes = this.processBranch( branch2, baseFrame.frame_id, 100, // Right side 100 ); nodes.push(...branch2Nodes.nodes); edges.push(...branch2Nodes.edges); const conflicts = this.conflictDetector.detectConflicts(branch1, branch2); this.markConflicts(nodes, edges, conflicts); return { nodes, edges, layout: "tree" }; } /** * Render conflict markers for visualization */ renderConflictMarkers(conflicts) { const markers = []; for (const conflict of conflicts) { markers.push({ frameId: conflict.frameId1, type: "conflict", color: this.getSeverityColor(conflict.severity), symbol: this.getConflictSymbol(conflict.type), description: conflict.description }); markers.push({ frameId: conflict.frameId2, type: "conflict", color: this.getSeverityColor(conflict.severity), symbol: this.getConflictSymbol(conflict.type), description: conflict.description }); } return markers; } /** * Generate a merge preview based on resolution strategy */ generateMergePreview(stack1, stack2, strategy) { const mergedFrames = []; const keptFromStack1 = []; const keptFromStack2 = []; const conflicts = this.conflictDetector.detectConflicts(stack1, stack2); switch (strategy) { case "keep_both": return this.previewKeepBoth(stack1, stack2, conflicts); case "team_vote": return this.previewTeamVote(stack1, stack2, conflicts); case "senior_override": return this.previewSeniorOverride(stack1, stack2, conflicts); case "ai_suggest": return this.previewAISuggest(stack1, stack2, conflicts); case "hybrid": return this.previewHybrid(stack1, stack2, conflicts); default: return { mergedFrames, keptFromStack1, keptFromStack2, conflicts, estimatedSuccess: 0 }; } } /** * Create a stack diff comparison */ createStackDiff(baseFrameId, stack1, stack2) { const conflicts = this.conflictDetector.detectConflicts(stack1, stack2); const divergencePoint = this.findDivergencePoint(stack1, stack2); const commonAncestor = this.findCommonAncestor(stack1, stack2); const baseFrame = stack1.frames.find((f) => f.frame_id === baseFrameId) || stack2.frames.find((f) => f.frame_id === baseFrameId); const visualRepresentation = baseFrame ? this.visualizeDivergence(baseFrame, stack1, stack2) : void 0; return { baseFrame: baseFrameId, branch1: stack1, branch2: stack2, divergencePoint, conflicts, commonAncestor, visualRepresentation }; } /** * Process a branch for visualization */ processBranch(stack, parentId, xOffset, yStart) { const nodes = []; const edges = []; let yPos = yStart; const children = stack.frames.filter((f) => f.parent_frame_id === parentId); for (const frame of children) { nodes.push({ id: frame.frame_id, type: xOffset < 0 ? "branch1" : "branch2", frame, position: { x: xOffset, y: yPos }, metadata: { branch: xOffset < 0 ? "left" : "right", depth: frame.depth } }); edges.push({ source: parentId, target: frame.frame_id, type: "parent", weight: 1 }); const childResults = this.processBranch( stack, frame.frame_id, xOffset + (xOffset < 0 ? -50 : 50), yPos + 100 ); nodes.push(...childResults.nodes); edges.push(...childResults.edges); yPos += 150; } return { nodes, edges }; } /** * Mark conflicts in the visualization */ markConflicts(nodes, edges, conflicts) { for (const conflict of conflicts) { const node1 = nodes.find((n) => n.id === conflict.frameId1); const node2 = nodes.find((n) => n.id === conflict.frameId2); if (node1) { node1.type = "conflict"; node1.metadata = { ...node1.metadata, conflictType: conflict.type, severity: conflict.severity }; } if (node2) { node2.type = "conflict"; node2.metadata = { ...node2.metadata, conflictType: conflict.type, severity: conflict.severity }; } if (node1 && node2) { edges.push({ source: conflict.frameId1, target: conflict.frameId2, type: "conflict", weight: this.getSeverityWeight(conflict.severity) }); } } } /** * Get color for severity level */ getSeverityColor(severity) { switch (severity) { case "critical": return "#ff0000"; case "high": return "#ff6600"; case "medium": return "#ffaa00"; case "low": return "#ffdd00"; default: return "#888888"; } } /** * Get symbol for conflict type */ getConflictSymbol(type) { switch (type) { case "parallel_solution": return "\u26A1"; case "conflicting_decision": return "\u26A0\uFE0F"; case "structural_divergence": return "\u{1F500}"; default: return "\u2753"; } } /** * Get weight for severity level */ getSeverityWeight(severity) { switch (severity) { case "critical": return 4; case "high": return 3; case "medium": return 2; case "low": return 1; default: return 0; } } /** * Preview keep both strategy */ previewKeepBoth(stack1, stack2, conflicts) { const mergedFrames = []; const keptFromStack1 = []; const keptFromStack2 = []; for (const frame of stack1.frames) { mergedFrames.push(frame); keptFromStack1.push(frame.frame_id); } for (const frame of stack2.frames) { if (!mergedFrames.find((f) => f.frame_id === frame.frame_id)) { mergedFrames.push(frame); keptFromStack2.push(frame.frame_id); } } const criticalConflicts = conflicts.filter( (c) => c.severity === "critical" ).length; const estimatedSuccess = Math.max(0, 1 - criticalConflicts * 0.2); return { mergedFrames, keptFromStack1, keptFromStack2, conflicts, estimatedSuccess }; } /** * Preview team vote strategy */ previewTeamVote(stack1, stack2, conflicts) { const mergedFrames = []; const keptFromStack1 = []; const keptFromStack2 = []; const conflictingFrameIds = /* @__PURE__ */ new Set(); for (const conflict of conflicts) { conflictingFrameIds.add(conflict.frameId1); conflictingFrameIds.add(conflict.frameId2); } for (const frame of stack1.frames) { if (!conflictingFrameIds.has(frame.frame_id)) { mergedFrames.push(frame); keptFromStack1.push(frame.frame_id); } } for (const frame of stack2.frames) { if (!conflictingFrameIds.has(frame.frame_id) && !mergedFrames.find((f) => f.frame_id === frame.frame_id)) { mergedFrames.push(frame); keptFromStack2.push(frame.frame_id); } } let useStack1 = true; for (const conflict of conflicts) { if (useStack1) { const frame = stack1.frames.find( (f) => f.frame_id === conflict.frameId1 ); if (frame) { mergedFrames.push(frame); keptFromStack1.push(frame.frame_id); } } else { const frame = stack2.frames.find( (f) => f.frame_id === conflict.frameId2 ); if (frame) { mergedFrames.push(frame); keptFromStack2.push(frame.frame_id); } } useStack1 = !useStack1; } return { mergedFrames, keptFromStack1, keptFromStack2, conflicts, estimatedSuccess: 0.75 // Team consensus usually works well }; } /** * Preview senior override strategy */ previewSeniorOverride(stack1, stack2, conflicts) { const mergedFrames = [...stack1.frames]; const keptFromStack1 = stack1.frames.map((f) => f.frame_id); const keptFromStack2 = []; const stack1Ids = new Set(keptFromStack1); for (const frame of stack2.frames) { const hasConflict = conflicts.some((c) => c.frameId2 === frame.frame_id); if (!hasConflict && !stack1Ids.has(frame.frame_id)) { mergedFrames.push(frame); keptFromStack2.push(frame.frame_id); } } return { mergedFrames, keptFromStack1, keptFromStack2, conflicts, estimatedSuccess: 0.85 // Senior override is usually reliable }; } /** * Preview AI suggest strategy */ previewAISuggest(stack1, stack2, conflicts) { const mergedFrames = []; const keptFromStack1 = []; const keptFromStack2 = []; for (const conflict of conflicts) { const frame1 = stack1.frames.find( (f) => f.frame_id === conflict.frameId1 ); const frame2 = stack2.frames.find( (f) => f.frame_id === conflict.frameId2 ); if (frame1 && frame2) { const score1 = this.scoreFrame(frame1); const score2 = this.scoreFrame(frame2); if (score1 >= score2) { mergedFrames.push(frame1); keptFromStack1.push(frame1.frame_id); } else { mergedFrames.push(frame2); keptFromStack2.push(frame2.frame_id); } } } this.addNonConflictingFrames( stack1, stack2, conflicts, mergedFrames, keptFromStack1, keptFromStack2 ); return { mergedFrames, keptFromStack1, keptFromStack2, conflicts, estimatedSuccess: 0.9 // AI suggestions are usually optimal }; } /** * Preview hybrid strategy */ previewHybrid(stack1, stack2, conflicts) { const mergedFrames = []; const keptFromStack1 = []; const keptFromStack2 = []; for (const conflict of conflicts) { if (conflict.type === "parallel_solution") { const frame1 = stack1.frames.find( (f) => f.frame_id === conflict.frameId1 ); const frame2 = stack2.frames.find( (f) => f.frame_id === conflict.frameId2 ); if (frame1) { mergedFrames.push(frame1); keptFromStack1.push(frame1.frame_id); } if (frame2) { mergedFrames.push(frame2); keptFromStack2.push(frame2.frame_id); } } else if (conflict.type === "conflicting_decision") { const frame1 = stack1.frames.find( (f) => f.frame_id === conflict.frameId1 ); const frame2 = stack2.frames.find( (f) => f.frame_id === conflict.frameId2 ); if (frame1 && frame2) { const score1 = this.scoreFrame(frame1); const score2 = this.scoreFrame(frame2); if (score1 >= score2) { mergedFrames.push(frame1); keptFromStack1.push(frame1.frame_id); } else { mergedFrames.push(frame2); keptFromStack2.push(frame2.frame_id); } } } else { const frame1 = stack1.frames.find( (f) => f.frame_id === conflict.frameId1 ); if (frame1) { mergedFrames.push(frame1); keptFromStack1.push(frame1.frame_id); } } } this.addNonConflictingFrames( stack1, stack2, conflicts, mergedFrames, keptFromStack1, keptFromStack2 ); return { mergedFrames, keptFromStack1, keptFromStack2, conflicts, estimatedSuccess: 0.88 // Hybrid is very effective }; } /** * Score a frame for quality */ scoreFrame(frame) { let score = 0; if (frame.state === "closed") score += 0.3; if (frame.outputs && Object.keys(frame.outputs).length > 0) score += 0.2; if (frame.digest_text) score += 0.2; if (frame.closed_at && frame.created_at) { const duration = frame.closed_at - frame.created_at; if (duration < 6e5) score += 0.3; } return score; } /** * Add non-conflicting frames to merge */ addNonConflictingFrames(stack1, stack2, conflicts, mergedFrames, keptFromStack1, keptFromStack2) { const conflictingIds = /* @__PURE__ */ new Set(); const mergedIds = new Set(mergedFrames.map((f) => f.frame_id)); for (const conflict of conflicts) { conflictingIds.add(conflict.frameId1); conflictingIds.add(conflict.frameId2); } for (const frame of stack1.frames) { if (!conflictingIds.has(frame.frame_id) && !mergedIds.has(frame.frame_id)) { mergedFrames.push(frame); keptFromStack1.push(frame.frame_id); mergedIds.add(frame.frame_id); } } for (const frame of stack2.frames) { if (!conflictingIds.has(frame.frame_id) && !mergedIds.has(frame.frame_id)) { mergedFrames.push(frame); keptFromStack2.push(frame.frame_id); mergedIds.add(frame.frame_id); } } } /** * Find divergence point between stacks */ findDivergencePoint(stack1, stack2) { const events1 = stack1.events.sort((a, b) => a.ts - b.ts); const events2 = stack2.events.sort((a, b) => a.ts - b.ts); for (let i = 0; i < Math.min(events1.length, events2.length); i++) { if (events1[i].event_id !== events2[i].event_id) { return events1[i].ts; } } return Math.min( events1[events1.length - 1]?.ts || 0, events2[events2.length - 1]?.ts || 0 ); } /** * Find common ancestor frame */ findCommonAncestor(stack1, stack2) { const frames1 = new Set(stack1.frames.map((f) => f.frame_id)); let deepestCommon; let maxDepth = -1; for (const frame of stack2.frames) { if (frames1.has(frame.frame_id) && frame.depth > maxDepth) { deepestCommon = frame; maxDepth = frame.depth; } } return deepestCommon?.frame_id; } } export { StackDiffVisualizer }; //# sourceMappingURL=stack-diff.js.map