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.

432 lines (431 loc) 13.2 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 { logger } from "../monitoring/logger.js"; class ConflictDetector { SIMILARITY_THRESHOLD = 0.8; /** * Detect all types of conflicts between two frame stacks */ detectConflicts(stack1, stack2) { const conflicts = []; const parallelConflicts = this.detectParallelSolutions(stack1, stack2); conflicts.push(...parallelConflicts); const decisionConflicts = this.detectConflictingDecisions( stack1.events, stack2.events ); conflicts.push(...decisionConflicts); const structuralConflicts = this.detectStructuralDivergence( stack1.frames, stack2.frames ); conflicts.push(...structuralConflicts); logger.info(`Detected ${conflicts.length} conflicts between stacks`, { stack1Id: stack1.id, stack2Id: stack2.id, conflictTypes: this.summarizeConflictTypes(conflicts) }); return conflicts; } /** * Analyze frames to find parallel solutions to the same problem */ analyzeParallelSolutions(frames) { const solutions = []; const groupedFrames = this.groupSimilarFrames(frames); for (const group of groupedFrames) { if (group.length > 1) { group.forEach((frame) => { solutions.push({ frameId: frame.frame_id, solution: this.extractSolution(frame), approach: this.analyzeApproach(frame), author: frame.inputs?.author || "unknown", timestamp: frame.created_at, effectiveness: this.calculateEffectiveness(frame) }); }); } } return solutions; } /** * Identify conflicting decisions in event streams */ identifyConflictingDecisions(events) { const conflicts = []; const decisions = this.extractDecisions(events); for (let i = 0; i < decisions.length; i++) { for (let j = i + 1; j < decisions.length; j++) { if (this.decisionsConflict(decisions[i], decisions[j])) { conflicts.push({ decision1: decisions[i].payload?.decision || "", decision2: decisions[j].payload?.decision || "", impact: this.assessImpact(decisions[i], decisions[j]), canCoexist: this.canCoexist(decisions[i], decisions[j]) }); } } } return conflicts; } /** * Detect parallel solutions between two stacks */ detectParallelSolutions(stack1, stack2) { const conflicts = []; for (const frame1 of stack1.frames) { for (const frame2 of stack2.frames) { if (this.framesAreSimilar(frame1, frame2) && !this.framesAreIdentical(frame1, frame2)) { conflicts.push({ id: uuidv4(), type: "parallel_solution", frameId1: frame1.frame_id, frameId2: frame2.frame_id, severity: this.calculateParallelSeverity(frame1, frame2), description: `Parallel solutions detected: "${frame1.name}" vs "${frame2.name}"`, detectedAt: Date.now(), conflictingPaths: this.extractPaths(frame1, frame2) }); } } } return conflicts; } /** * Detect conflicting decisions between event streams */ detectConflictingDecisions(events1, events2) { const conflicts = []; const decisions1 = this.extractDecisions(events1); const decisions2 = this.extractDecisions(events2); for (const d1 of decisions1) { for (const d2 of decisions2) { if (this.decisionsConflict(d1, d2)) { conflicts.push({ id: uuidv4(), type: "conflicting_decision", frameId1: d1.frame_id, frameId2: d2.frame_id, severity: this.assessImpact(d1, d2), description: `Conflicting decisions: "${d1.payload?.decision}" vs "${d2.payload?.decision}"`, detectedAt: Date.now() }); } } } return conflicts; } /** * Detect structural divergence in frame hierarchies */ detectStructuralDivergence(frames1, frames2) { const conflicts = []; const tree1 = this.buildFrameTree(frames1); const tree2 = this.buildFrameTree(frames2); const divergences = this.findDivergences(tree1, tree2); for (const divergence of divergences) { conflicts.push({ id: uuidv4(), type: "structural_divergence", frameId1: divergence.node1, frameId2: divergence.node2, severity: this.calculateDivergenceSeverity(divergence), description: `Structural divergence at depth ${divergence.depth}`, detectedAt: Date.now() }); } return conflicts; } /** * Helper: Check if two frames are similar (solving same problem) */ framesAreSimilar(frame1, frame2) { const nameSimilarity = this.calculateSimilarity(frame1.name, frame2.name); if (nameSimilarity > this.SIMILARITY_THRESHOLD) return true; if (frame1.type === frame2.type && frame1.parent_frame_id === frame2.parent_frame_id) { return true; } const inputSimilarity = this.compareInputs(frame1.inputs, frame2.inputs); return inputSimilarity > this.SIMILARITY_THRESHOLD; } /** * Helper: Check if frames are identical */ framesAreIdentical(frame1, frame2) { return frame1.frame_id === frame2.frame_id; } /** * Helper: Calculate string similarity (Levenshtein-based) */ calculateSimilarity(str1, str2) { const maxLen = Math.max(str1.length, str2.length); if (maxLen === 0) return 1; const distance = this.levenshteinDistance(str1, str2); return 1 - distance / maxLen; } /** * Helper: Levenshtein distance implementation */ levenshteinDistance(str1, str2) { const matrix = []; for (let i = 0; i <= str2.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= str1.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= str2.length; i++) { for (let j = 1; j <= str1.length; j++) { if (str2.charAt(i - 1) === str1.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1 ); } } } return matrix[str2.length][str1.length]; } /** * Helper: Compare frame inputs */ compareInputs(inputs1, inputs2) { const keys1 = Object.keys(inputs1 || {}); const keys2 = Object.keys(inputs2 || {}); const allKeys = /* @__PURE__ */ new Set([...keys1, ...keys2]); if (allKeys.size === 0) return 1; let matches = 0; for (const key of allKeys) { if (JSON.stringify(inputs1[key]) === JSON.stringify(inputs2[key])) { matches++; } } return matches / allKeys.size; } /** * Helper: Calculate severity for parallel solutions */ calculateParallelSeverity(frame1, frame2) { if (frame1.state === "closed" && frame2.state === "closed") { const outputSimilarity = this.compareInputs( frame1.outputs, frame2.outputs ); if (outputSimilarity < 0.5) return "critical"; if (outputSimilarity < 0.7) return "high"; } if (frame1.parent_frame_id === frame2.parent_frame_id) { return "high"; } return "medium"; } /** * Helper: Extract decision events */ extractDecisions(events) { return events.filter( (e) => e.event_type === "decision" || e.payload?.type === "decision" || e.payload?.decision !== void 0 ); } /** * Helper: Check if two decisions conflict */ decisionsConflict(d1, d2) { const resource1 = d1.payload?.resource || d1.payload?.path; const resource2 = d2.payload?.resource || d2.payload?.path; if (resource1 && resource2 && resource1 === resource2) { return d1.payload?.decision !== d2.payload?.decision; } return this.hasLogicalConflict(d1.payload, d2.payload); } /** * Helper: Check for logical conflicts in payloads */ hasLogicalConflict(payload1, payload2) { if (payload1?.architecture && payload2?.architecture) { return payload1.architecture !== payload2.architecture; } if (payload1?.technology && payload2?.technology) { return payload1.technology !== payload2.technology; } return false; } /** * Helper: Assess impact of conflicting decisions */ assessImpact(d1, d2) { if (d1.payload?.type === "architecture" || d2.payload?.type === "architecture") { return "high"; } if (d1.payload?.scope === "implementation" || d2.payload?.scope === "implementation") { return "medium"; } return "low"; } /** * Helper: Check if decisions can coexist */ canCoexist(d1, d2) { if (d1.payload?.scope !== d2.payload?.scope) { return true; } const resource1 = d1.payload?.resource; const resource2 = d2.payload?.resource; return resource1 !== resource2; } /** * Helper: Build frame tree structure */ buildFrameTree(frames) { const tree = /* @__PURE__ */ new Map(); for (const frame of frames) { if (frame.parent_frame_id) { if (!tree.has(frame.parent_frame_id)) { tree.set(frame.parent_frame_id, /* @__PURE__ */ new Set()); } const parentSet = tree.get(frame.parent_frame_id); if (parentSet) { parentSet.add(frame.frame_id); } } } return tree; } /** * Helper: Find divergence points in trees */ findDivergences(tree1, tree2) { const divergences = []; for (const [node, children1] of tree1) { if (tree2.has(node)) { const children2 = tree2.get(node); if (children2 && !this.setsEqual(children1, children2)) { divergences.push({ node1: node, node2: node, depth: this.calculateDepth(node, tree1) }); } } } return divergences; } /** * Helper: Check if two sets are equal */ setsEqual(set1, set2) { if (set1.size !== set2.size) return false; for (const item of set1) { if (!set2.has(item)) return false; } return true; } /** * Helper: Calculate node depth in tree */ calculateDepth(node, tree) { let depth = 0; let current = node; for (const [parent, children] of tree) { if (children.has(current)) { depth++; current = parent; } } return depth; } /** * Helper: Calculate divergence severity */ calculateDivergenceSeverity(divergence) { if (divergence.depth === 0) return "critical"; if (divergence.depth === 1) return "high"; if (divergence.depth === 2) return "medium"; return "low"; } /** * Helper: Extract paths from frames */ extractPaths(frame1, frame2) { const paths = []; if (frame1.inputs?.path) paths.push(frame1.inputs.path); if (frame2.inputs?.path) paths.push(frame2.inputs.path); if (frame1.outputs?.files) paths.push(...frame1.outputs.files); if (frame2.outputs?.files) paths.push(...frame2.outputs.files); return [...new Set(paths)]; } /** * Helper: Extract solution from frame */ extractSolution(frame) { return frame.outputs?.solution || frame.digest_text || "No solution description"; } /** * Helper: Analyze approach taken */ analyzeApproach(frame) { if (frame.outputs?.approach) return frame.outputs.approach; if (frame.type === "debug") return "Debug approach"; if (frame.type === "review") return "Review approach"; return "Standard approach"; } /** * Helper: Calculate solution effectiveness */ calculateEffectiveness(frame) { let score = 0.5; if (frame.state === "closed") score += 0.2; if (frame.outputs && Object.keys(frame.outputs).length > 0) score += 0.1; if (frame.digest_text) score += 0.1; if (frame.closed_at && frame.created_at) { const duration = frame.closed_at - frame.created_at; if (duration < 3e5) score += 0.1; } return Math.min(score, 1); } /** * Helper: Group similar frames together */ groupSimilarFrames(frames) { const groups = []; const processed = /* @__PURE__ */ new Set(); for (const frame of frames) { if (processed.has(frame.frame_id)) continue; const group = [frame]; processed.add(frame.frame_id); for (const other of frames) { if (!processed.has(other.frame_id) && this.framesAreSimilar(frame, other)) { group.push(other); processed.add(other.frame_id); } } groups.push(group); } return groups; } /** * Helper: Summarize conflict types */ summarizeConflictTypes(conflicts) { const summary = { parallel_solution: 0, conflicting_decision: 0, structural_divergence: 0 }; for (const conflict of conflicts) { summary[conflict.type]++; } return summary; } } export { ConflictDetector }; //# sourceMappingURL=conflict-detector.js.map