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