automagik-genie
Version:
Universal AI development companion that can be initialized in any codebase
630 lines (546 loc) • 18.9 kB
JavaScript
const fs = require('fs').promises;
const path = require('path');
/**
* DiffEngine - Handles change detection and analysis between templates and user files
* Provides smart merge strategies and conflict resolution
*/
class DiffEngine {
constructor() {
this.sectionMarkers = {
start: /^## (.+)$/,
customStart: /^## (Custom|User|Project)/i,
templateStart: /^## (GENIE|Development|Template)/i
};
}
/**
* Analyze changes between current agent file and template
* @param {string} agentName - Name of the agent
* @param {string} currentContent - Current file content
* @param {string} templateContent - Template content
* @returns {Object} Analysis results
*/
async analyzeAgentChanges(agentName, currentContent, templateContent) {
const analysis = {
agentName,
hasChanges: false,
templateSections: [],
userSections: [],
conflicts: [],
mergeStrategy: 'safe', // 'safe' | 'merge' | 'manual' | 'skip'
confidence: 'high', // 'high' | 'medium' | 'low'
changeCategories: {
templateUpdates: [],
userCustomizations: [],
conflicts: [],
newSections: []
}
};
// Parse both files into sections
const currentSections = this.parseFileIntoSections(currentContent);
const templateSections = this.parseFileIntoSections(templateContent);
// Compare sections
const sectionComparison = this.compareSections(currentSections, templateSections);
analysis.hasChanges = sectionComparison.hasChanges;
analysis.templateSections = sectionComparison.templateSections;
analysis.userSections = sectionComparison.userSections;
analysis.conflicts = sectionComparison.conflicts;
// Determine merge strategy based on analysis
analysis.mergeStrategy = this.determineMergeStrategy(sectionComparison);
analysis.confidence = this.calculateConfidence(sectionComparison);
// Categorize changes
analysis.changeCategories = this.categorizeChanges(sectionComparison);
return analysis;
}
/**
* Parse file content into structured sections
* @param {string} content - File content
* @returns {Array} Array of sections with metadata
*/
parseFileIntoSections(content) {
const lines = content.split('\n');
const sections = [];
let currentSection = null;
let lineNumber = 0;
for (const line of lines) {
lineNumber++;
const sectionMatch = line.match(this.sectionMarkers.start);
if (sectionMatch) {
// Save previous section if exists
if (currentSection) {
sections.push(currentSection);
}
// Start new section
currentSection = {
title: sectionMatch[1],
startLine: lineNumber,
endLine: null,
content: [line],
type: this.determineSectionType(sectionMatch[1]),
isCustom: this.isCustomSection(sectionMatch[1]),
isTemplate: this.isTemplateSection(sectionMatch[1])
};
} else if (currentSection) {
currentSection.content.push(line);
} else {
// Content before first section (preamble)
if (!sections.length || sections[0].title !== '__preamble__') {
sections.unshift({
title: '__preamble__',
startLine: 1,
endLine: null,
content: [line],
type: 'preamble',
isCustom: false,
isTemplate: true
});
} else {
sections[0].content.push(line);
}
}
}
// Close last section
if (currentSection) {
currentSection.endLine = lineNumber;
sections.push(currentSection);
}
// Set end lines for all sections
for (let i = 0; i < sections.length - 1; i++) {
sections[i].endLine = sections[i + 1].startLine - 1;
}
return sections;
}
/**
* Determine section type from title
* @param {string} title - Section title
* @returns {string} Section type
*/
determineSectionType(title) {
const lowerTitle = title.toLowerCase();
if (lowerTitle.includes('custom') || lowerTitle.includes('user') || lowerTitle.includes('project')) {
return 'custom';
}
if (lowerTitle.includes('genie') || lowerTitle.includes('template') || lowerTitle.includes('core')) {
return 'template';
}
// Check common template section patterns
const templatePatterns = [
'personality', 'core', 'routing', 'behavior', 'implementation',
'patterns', 'workflow', 'architecture', 'development', 'system'
];
if (templatePatterns.some(pattern => lowerTitle.includes(pattern))) {
return 'template';
}
// Default to mixed if unclear
return 'mixed';
}
/**
* Check if section is a custom user section
* @param {string} title - Section title
* @returns {boolean} True if custom section
*/
isCustomSection(title) {
return this.sectionMarkers.customStart.test(`## ${title}`);
}
/**
* Check if section is a template section
* @param {string} title - Section title
* @returns {boolean} True if template section
*/
isTemplateSection(title) {
return this.sectionMarkers.templateStart.test(`## ${title}`) ||
this.determineSectionType(title) === 'template';
}
/**
* Compare sections between current and template files
* @param {Array} currentSections - Current file sections
* @param {Array} templateSections - Template file sections
* @returns {Object} Comparison results
*/
compareSections(currentSections, templateSections) {
const comparison = {
hasChanges: false,
templateSections: [],
userSections: [],
conflicts: [],
unchanged: [],
added: [],
removed: [],
modified: []
};
// Create maps for easier lookup
const currentMap = new Map(currentSections.map(s => [s.title, s]));
const templateMap = new Map(templateSections.map(s => [s.title, s]));
// Find all unique section titles
const allTitles = new Set([...currentMap.keys(), ...templateMap.keys()]);
for (const title of allTitles) {
const currentSection = currentMap.get(title);
const templateSection = templateMap.get(title);
if (currentSection && templateSection) {
// Section exists in both - check for changes
const contentMatch = this.compareSectionContent(currentSection, templateSection);
if (contentMatch.identical) {
comparison.unchanged.push({
title,
type: currentSection.type,
action: 'keep'
});
} else {
comparison.hasChanges = true;
if (currentSection.isCustom) {
// User customized section - preserve it
comparison.userSections.push({
title,
type: 'custom',
action: 'preserve',
content: currentSection.content
});
} else if (templateSection.type === 'template') {
// Template section with updates
comparison.templateSections.push({
title,
type: 'template',
action: 'update',
oldContent: currentSection.content,
newContent: templateSection.content,
changes: contentMatch.changes
});
} else {
// Mixed section - potential conflict
comparison.conflicts.push({
title,
type: 'conflict',
action: 'manual',
currentContent: currentSection.content,
templateContent: templateSection.content,
reason: 'Modified template section with unclear intent'
});
}
comparison.modified.push({
title,
changeType: contentMatch.changeType,
severity: contentMatch.severity
});
}
} else if (currentSection && !templateSection) {
// Section only in current - user addition
comparison.hasChanges = true;
comparison.userSections.push({
title,
type: 'user-addition',
action: 'preserve',
content: currentSection.content
});
comparison.added.push({ title, source: 'user' });
} else if (!currentSection && templateSection) {
// Section only in template - new template section
comparison.hasChanges = true;
comparison.templateSections.push({
title,
type: 'template-addition',
action: 'add',
content: templateSection.content
});
comparison.added.push({ title, source: 'template' });
}
}
return comparison;
}
/**
* Compare content of two sections
* @param {Object} currentSection - Current section
* @param {Object} templateSection - Template section
* @returns {Object} Content comparison
*/
compareSectionContent(currentSection, templateSection) {
const currentContent = currentSection.content.join('\n');
const templateContent = templateSection.content.join('\n');
if (currentContent === templateContent) {
return {
identical: true,
changeType: 'none',
severity: 'none',
changes: []
};
}
// Analyze type of changes
const changes = this.analyzeContentChanges(currentContent, templateContent);
return {
identical: false,
changeType: changes.type,
severity: changes.severity,
changes: changes.details
};
}
/**
* Analyze the nature of content changes
* @param {string} current - Current content
* @param {string} template - Template content
* @returns {Object} Change analysis
*/
analyzeContentChanges(current, template) {
const currentLines = current.split('\n').filter(line => line.trim());
const templateLines = template.split('\n').filter(line => line.trim());
const analysis = {
type: 'modification',
severity: 'medium',
details: {
linesAdded: 0,
linesRemoved: 0,
linesModified: 0,
significantChanges: false
}
};
// Simple diff analysis
const maxLength = Math.max(currentLines.length, templateLines.length);
const minLength = Math.min(currentLines.length, templateLines.length);
analysis.details.linesAdded = Math.max(0, templateLines.length - currentLines.length);
analysis.details.linesRemoved = Math.max(0, currentLines.length - templateLines.length);
// Count modified lines
let modifiedLines = 0;
for (let i = 0; i < minLength; i++) {
if (currentLines[i] !== templateLines[i]) {
modifiedLines++;
}
}
analysis.details.linesModified = modifiedLines;
// Determine severity
const totalChanges = analysis.details.linesAdded + analysis.details.linesRemoved + analysis.details.linesModified;
const changePercentage = (totalChanges / maxLength) * 100;
if (changePercentage > 50) {
analysis.severity = 'high';
analysis.details.significantChanges = true;
} else if (changePercentage > 20) {
analysis.severity = 'medium';
} else {
analysis.severity = 'low';
}
// Determine change type
if (analysis.details.linesAdded > 0 && analysis.details.linesRemoved === 0) {
analysis.type = 'addition';
} else if (analysis.details.linesRemoved > 0 && analysis.details.linesAdded === 0) {
analysis.type = 'removal';
} else if (analysis.details.linesModified > 0) {
analysis.type = 'modification';
}
return analysis;
}
/**
* Determine appropriate merge strategy
* @param {Object} sectionComparison - Section comparison results
* @returns {string} Merge strategy
*/
determineMergeStrategy(sectionComparison) {
const { conflicts, userSections, templateSections } = sectionComparison;
// If there are conflicts, require manual resolution
if (conflicts.length > 0) {
return 'manual';
}
// If only template updates, safe to auto-merge
if (templateSections.length > 0 && userSections.length === 0) {
return 'safe';
}
// If mix of template and user changes, use merge strategy
if (templateSections.length > 0 && userSections.length > 0) {
return 'merge';
}
// If only user changes, skip update
if (userSections.length > 0 && templateSections.length === 0) {
return 'skip';
}
// Default to safe
return 'safe';
}
/**
* Calculate confidence level for merge operation
* @param {Object} sectionComparison - Section comparison results
* @returns {string} Confidence level
*/
calculateConfidence(sectionComparison) {
const { conflicts, modified } = sectionComparison;
if (conflicts.length > 0) {
return 'low';
}
const highSeverityChanges = modified.filter(m => m.severity === 'high').length;
const mediumSeverityChanges = modified.filter(m => m.severity === 'medium').length;
if (highSeverityChanges > 0) {
return 'low';
}
if (mediumSeverityChanges > 2) {
return 'medium';
}
return 'high';
}
/**
* Categorize changes for user presentation
* @param {Object} sectionComparison - Section comparison results
* @returns {Object} Categorized changes
*/
categorizeChanges(sectionComparison) {
const categories = {
templateUpdates: [],
userCustomizations: [],
conflicts: [],
newSections: []
};
// Template updates
categories.templateUpdates = sectionComparison.templateSections
.filter(s => s.action === 'update')
.map(s => ({
title: s.title,
type: 'update',
description: `Template section "${s.title}" has updates`,
severity: this.getSeverityFromChanges(s.changes)
}));
// New template sections
categories.newSections = sectionComparison.templateSections
.filter(s => s.action === 'add')
.map(s => ({
title: s.title,
type: 'addition',
description: `New template section "${s.title}" available`,
severity: 'low'
}));
// User customizations
categories.userCustomizations = sectionComparison.userSections.map(s => ({
title: s.title,
type: s.type,
description: `User customized section "${s.title}" will be preserved`,
severity: 'info'
}));
// Conflicts
categories.conflicts = sectionComparison.conflicts.map(c => ({
title: c.title,
type: 'conflict',
description: c.reason,
severity: 'high'
}));
return categories;
}
/**
* Get severity from change analysis
* @param {Object} changes - Change details
* @returns {string} Severity level
*/
getSeverityFromChanges(changes) {
if (!changes) return 'low';
return changes.severity || 'medium';
}
/**
* Generate merge preview for conflicts
* @param {Array} conflicts - Array of conflicts
* @returns {Object} Merge preview
*/
async generateMergePreview(conflicts) {
const preview = {
totalConflicts: conflicts.length,
resolutions: [],
recommendations: []
};
for (const conflict of conflicts) {
const resolution = {
section: conflict.title,
options: [
{
name: 'keep-current',
description: 'Keep current version (ignore template updates)',
risk: 'medium',
impact: 'May miss important template improvements'
},
{
name: 'use-template',
description: 'Use template version (lose customizations)',
risk: 'high',
impact: 'Will overwrite user customizations'
},
{
name: 'manual-merge',
description: 'Manually merge changes',
risk: 'low',
impact: 'Requires user review and decision'
}
],
recommended: 'manual-merge'
};
preview.resolutions.push(resolution);
}
// Generate overall recommendations
if (conflicts.length === 0) {
preview.recommendations.push('Safe to proceed with automatic merge');
} else if (conflicts.length <= 2) {
preview.recommendations.push('Review conflicts manually - small number of issues');
} else {
preview.recommendations.push('Consider creating backup before proceeding - multiple conflicts detected');
}
return preview;
}
/**
* Detect custom sections in content
* @param {string} content - File content to analyze
* @returns {Array} Array of detected custom sections
*/
async detectCustomSections(content) {
const sections = this.parseFileIntoSections(content);
const customSections = [];
for (const section of sections) {
if (section.isCustom || this.hasCustomPatterns(section.content.join('\n'))) {
customSections.push({
title: section.title,
startLine: section.startLine,
endLine: section.endLine,
confidence: this.getCustomConfidence(section),
patterns: this.getCustomPatterns(section.content.join('\n'))
});
}
}
return customSections;
}
/**
* Check if section content has custom patterns
* @param {string} content - Section content
* @returns {boolean} True if custom patterns detected
*/
hasCustomPatterns(content) {
const customPatterns = [
/# Custom|# User|# Project/i,
/TODO:|FIXME:|NOTE:/i,
/\{\{[^}]+\}\}/g, // Template variables
/\b(your|my|our|this project|this codebase)\b/gi
];
return customPatterns.some(pattern => pattern.test(content));
}
/**
* Get confidence level for custom section detection
* @param {Object} section - Section object
* @returns {string} Confidence level
*/
getCustomConfidence(section) {
const content = section.content.join('\n');
const patterns = this.getCustomPatterns(content);
if (section.isCustom) return 'high';
if (patterns.length >= 3) return 'high';
if (patterns.length >= 1) return 'medium';
return 'low';
}
/**
* Get custom patterns found in content
* @param {string} content - Content to analyze
* @returns {Array} Array of found patterns
*/
getCustomPatterns(content) {
const patterns = [];
if (/# Custom|# User|# Project/i.test(content)) {
patterns.push('explicit-custom-marker');
}
if (/TODO:|FIXME:|NOTE:/i.test(content)) {
patterns.push('development-markers');
}
if (/\{\{[^}]+\}\}/g.test(content)) {
patterns.push('template-variables');
}
if (/\b(your|my|our|this project|this codebase)\b/gi.test(content)) {
patterns.push('personal-references');
}
return patterns;
}
}
module.exports = { DiffEngine };