@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
JavaScript
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