aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
850 lines (729 loc) • 25.4 kB
JavaScript
const yaml = require('js-yaml');
const { createHash } = require('crypto');
const DiffGenerator = require('./diff-generator');
const ModificationValidator = require('./modification-validator');
/**
* Generates structured commit messages following conventional commit standards
* for AIOS framework modifications
*/
class CommitMessageGenerator {
constructor(options = {}) {
this.diffGenerator = new DiffGenerator();
this.validator = new ModificationValidator();
// Conventional commit types
this.commitTypes = {
feat: 'A new feature',
fix: 'A bug fix',
docs: 'Documentation only changes',
style: 'Changes that do not affect the meaning of the code',
refactor: 'A code change that neither fixes a bug nor adds a feature',
perf: 'A code change that improves performance',
test: 'Adding missing tests or correcting existing tests',
build: 'Changes that affect the build system or external dependencies',
ci: 'Changes to CI configuration files and scripts',
chore: 'Other changes that don\'t modify src or test files',
revert: 'Reverts a previous commit',
};
// Component-specific actions
this.componentActions = {
agent: {
enhance: 'Enhanced capabilities or features',
fix: 'Fixed issues or bugs',
update: 'Updated configuration or metadata',
refactor: 'Refactored implementation',
deprecate: 'Marked features as deprecated',
remove: 'Removed deprecated features',
},
task: {
improve: 'Improved task flow or logic',
fix: 'Fixed task execution issues',
update: 'Updated task steps or output',
optimize: 'Optimized performance',
clarify: 'Clarified instructions or prompts',
},
workflow: {
restructure: 'Restructured workflow phases',
add: 'Added new phases or transitions',
update: 'Updated phase configuration',
optimize: 'Optimized workflow execution',
fix: 'Fixed workflow issues',
},
};
// Keywords for categorizing changes
this.changeKeywords = {
feat: ['add', 'new', 'implement', 'introduce', 'create'],
fix: ['fix', 'resolve', 'correct', 'repair', 'patch'],
refactor: ['refactor', 'restructure', 'reorganize', 'improve structure'],
perf: ['optimize', 'performance', 'speed up', 'efficiency'],
docs: ['document', 'docs', 'readme', 'comment', 'clarify'],
};
}
/**
* Generate commit message for a modification
* @param {Object} modification - Modification details
* @returns {Promise<Object>} Generated commit message and metadata
*/
async generateCommitMessage(modification) {
const {
componentType,
componentName,
originalContent,
modifiedContent,
userIntent = '',
metadata = {},
} = modification;
try {
// Analyze the changes
const analysis = await this.analyzeModification(
componentType,
originalContent,
modifiedContent,
);
// Determine commit type and action
const commitType = this.determineCommitType(analysis, userIntent);
const action = this.determineAction(componentType, analysis, userIntent);
// Generate summary
const summary = this.generateSummary(
componentType,
componentName,
action,
analysis,
userIntent,
);
// Generate detailed description
const details = this.generateDetails(analysis, metadata);
// Check for breaking changes
const breakingChanges = await this.detectBreakingChanges(
componentType,
originalContent,
modifiedContent,
);
// Construct the full message
const message = this.constructMessage({
type: commitType,
scope: componentType,
summary,
body: details,
breaking: breakingChanges,
metadata,
});
return {
message,
type: commitType,
scope: componentType,
summary,
analysis,
breakingChanges,
};
} catch (error) {
throw new Error(`Failed to generate commit message: ${error.message}`);
}
}
/**
* Analyze modification to understand changes
* @private
*/
async analyzeModification(componentType, originalContent, modifiedContent) {
const analysis = {
componentType,
changeType: null,
modifications: [],
additions: [],
deletions: [],
statistics: {
linesAdded: 0,
linesRemoved: 0,
filesChanged: 1,
},
semanticChanges: [],
};
// Generate diff for analysis
const diff = this.diffGenerator.generateUnifiedDiff(
originalContent,
modifiedContent,
`${componentType}.before`,
`${componentType}.after`,
);
// Parse diff to extract changes
const lines = diff.split('\n');
let currentSection = null;
for (const line of lines) {
if (line.startsWith('+') && !line.startsWith('+++')) {
analysis.statistics.linesAdded++;
analysis.additions.push(line.substring(1));
} else if (line.startsWith('-') && !line.startsWith('---')) {
analysis.statistics.linesRemoved++;
analysis.deletions.push(line.substring(1));
} else if (line.startsWith('@@')) {
currentSection = this.extractSectionName(line);
}
}
// Analyze semantic changes based on component type
switch (componentType) {
case 'agent':
analysis.semanticChanges = await this.analyzeAgentChanges(
originalContent,
modifiedContent,
);
break;
case 'task':
analysis.semanticChanges = await this.analyzeTaskChanges(
originalContent,
modifiedContent,
);
break;
case 'workflow':
analysis.semanticChanges = await this.analyzeWorkflowChanges(
originalContent,
modifiedContent,
);
break;
}
// Determine overall change type
if (analysis.statistics.linesRemoved === 0 && analysis.statistics.linesAdded > 0) {
analysis.changeType = 'addition';
} else if (analysis.statistics.linesAdded === 0 && analysis.statistics.linesRemoved > 0) {
analysis.changeType = 'deletion';
} else {
analysis.changeType = 'modification';
}
return analysis;
}
/**
* Analyze agent-specific changes
* @private
*/
async analyzeAgentChanges(originalContent, modifiedContent) {
const changes = [];
try {
const originalParts = this.parseAgentContent(originalContent);
const modifiedParts = this.parseAgentContent(modifiedContent);
const originalMeta = yaml.load(originalParts.yaml);
const modifiedMeta = yaml.load(modifiedParts.yaml);
// Check command changes
if (originalMeta.commands || modifiedMeta.commands) {
const originalCmds = Object.keys(originalMeta.commands || {});
const modifiedCmds = Object.keys(modifiedMeta.commands || {});
const added = modifiedCmds.filter(cmd => !originalCmds.includes(cmd));
const removed = originalCmds.filter(cmd => !modifiedCmds.includes(cmd));
const modified = originalCmds.filter(cmd =>
modifiedCmds.includes(cmd) &&
originalMeta.commands[cmd] !== modifiedMeta.commands[cmd],
);
if (added.length > 0) {
changes.push({ type: 'commands_added', items: added });
}
if (removed.length > 0) {
changes.push({ type: 'commands_removed', items: removed });
}
if (modified.length > 0) {
changes.push({ type: 'commands_modified', items: modified });
}
}
// Check dependency changes
if (originalMeta.dependencies || modifiedMeta.dependencies) {
const depChanges = this.compareDependencies(
originalMeta.dependencies || {},
modifiedMeta.dependencies || {},
);
if (depChanges.length > 0) {
changes.push(...depChanges);
}
}
// Check metadata changes
const metadataFields = ['title', 'icon', 'whenToUse', 'description'];
for (const field of metadataFields) {
if (originalMeta[field] !== modifiedMeta[field]) {
changes.push({
type: 'metadata_changed',
field,
from: originalMeta[field],
to: modifiedMeta[field],
});
}
}
} catch (error) {
// If parsing fails, return generic change
changes.push({ type: 'content_modified' });
}
return changes;
}
/**
* Analyze task-specific changes
* @private
*/
async analyzeTaskChanges(originalContent, modifiedContent) {
const changes = [];
// Check section changes
const sections = ['## Purpose', '## Task Execution', '## Output Format'];
for (const section of sections) {
const originalSection = this.extractSection(originalContent, section);
const modifiedSection = this.extractSection(modifiedContent, section);
if (originalSection !== modifiedSection) {
changes.push({
type: 'section_modified',
section: section.replace('## ', ''),
contentChanged: true,
});
}
}
// Check elicitation blocks
const originalElicits = (originalContent.match(/\[\[LLM:[\s\S]*?\]\]/g) || []).length;
const modifiedElicits = (modifiedContent.match(/\[\[LLM:[\s\S]*?\]\]/g) || []).length;
if (originalElicits !== modifiedElicits) {
changes.push({
type: 'elicitation_changed',
from: originalElicits,
to: modifiedElicits,
});
}
// Check task steps
const originalSteps = (originalContent.match(/### \d+\./g) || []).length;
const modifiedSteps = (modifiedContent.match(/### \d+\./g) || []).length;
if (originalSteps !== modifiedSteps) {
changes.push({
type: 'steps_changed',
from: originalSteps,
to: modifiedSteps,
});
}
return changes;
}
/**
* Analyze workflow-specific changes
* @private
*/
async analyzeWorkflowChanges(originalContent, modifiedContent) {
const changes = [];
try {
const originalWorkflow = yaml.load(originalContent);
const modifiedWorkflow = yaml.load(modifiedContent);
// Check phase changes
const originalPhases = Object.keys(originalWorkflow.phases || {});
const modifiedPhases = Object.keys(modifiedWorkflow.phases || {});
const added = modifiedPhases.filter(p => !originalPhases.includes(p));
const removed = originalPhases.filter(p => !modifiedPhases.includes(p));
if (added.length > 0) {
changes.push({ type: 'phases_added', items: added });
}
if (removed.length > 0) {
changes.push({ type: 'phases_removed', items: removed });
}
// Check phase modifications
for (const phase of originalPhases) {
if (modifiedPhases.includes(phase)) {
const originalPhase = originalWorkflow.phases[phase];
const modifiedPhase = modifiedWorkflow.phases[phase];
if (JSON.stringify(originalPhase) !== JSON.stringify(modifiedPhase)) {
changes.push({
type: 'phase_modified',
phase,
details: this.comparePhases(originalPhase, modifiedPhase),
});
}
}
}
} catch (error) {
changes.push({ type: 'structure_modified' });
}
return changes;
}
/**
* Determine commit type based on analysis
* @private
*/
determineCommitType(analysis, userIntent) {
const intent = userIntent.toLowerCase();
// Check user intent first
for (const [type, keywords] of Object.entries(this.changeKeywords)) {
if (keywords.some(keyword => intent.includes(keyword))) {
return type;
}
}
// Analyze semantic changes
const semanticTypes = analysis.semanticChanges.map(change => change.type);
if (semanticTypes.some(type => type.includes('added') || type.includes('new'))) {
return 'feat';
}
if (semanticTypes.some(type => type.includes('fixed') || type.includes('corrected'))) {
return 'fix';
}
if (semanticTypes.some(type => type.includes('performance') || type.includes('optimized'))) {
return 'perf';
}
if (analysis.changeType === 'modification' &&
analysis.statistics.linesAdded > 0 &&
analysis.statistics.linesRemoved > 0) {
return 'refactor';
}
// Default to chore for other changes
return 'chore';
}
/**
* Determine action verb based on changes
* @private
*/
determineAction(componentType, analysis, userIntent) {
const actions = this.componentActions[componentType] || {};
const intent = userIntent.toLowerCase();
// Check if user intent matches known actions
for (const [action, description] of Object.entries(actions)) {
if (intent.includes(action) || intent.includes(description.toLowerCase())) {
return action;
}
}
// Determine from semantic changes
const changeTypes = analysis.semanticChanges.map(c => c.type);
if (changeTypes.includes('commands_added') || changeTypes.includes('phases_added')) {
return 'enhance';
}
if (changeTypes.includes('commands_removed') || changeTypes.includes('phases_removed')) {
return 'remove';
}
if (changeTypes.some(t => t.includes('modified'))) {
return 'update';
}
return 'update'; // Default action
}
/**
* Generate commit summary
* @private
*/
generateSummary(componentType, componentName, action, analysis, userIntent) {
// Use user intent if it's concise
if (userIntent && userIntent.length < 50) {
return userIntent.toLowerCase();
}
// Generate based on analysis
const changeCount = analysis.semanticChanges.length;
const primaryChange = analysis.semanticChanges[0];
if (primaryChange) {
switch (primaryChange.type) {
case 'commands_added':
return `add ${primaryChange.items.join(', ')} command${primaryChange.items.length > 1 ? 's' : ''}`;
case 'commands_removed':
return `remove ${primaryChange.items.join(', ')} command${primaryChange.items.length > 1 ? 's' : ''}`;
case 'phases_added':
return `add ${primaryChange.items.join(', ')} phase${primaryChange.items.length > 1 ? 's' : ''}`;
case 'phases_removed':
return `remove ${primaryChange.items.join(', ')} phase${primaryChange.items.length > 1 ? 's' : ''}`;
case 'metadata_changed':
return `update ${primaryChange.field}`;
default:
return `${action} ${componentName}`;
}
}
return `${action} ${componentName}`;
}
/**
* Generate detailed commit body
* @private
*/
generateDetails(analysis, metadata) {
const details = [];
// Add statistics
if (analysis.statistics.linesAdded > 0 || analysis.statistics.linesRemoved > 0) {
details.push(
`Changed: +${analysis.statistics.linesAdded} -${analysis.statistics.linesRemoved} lines`,
);
}
// Add semantic changes
for (const change of analysis.semanticChanges) {
switch (change.type) {
case 'commands_added':
details.push(`Added commands: ${change.items.join(', ')}`);
break;
case 'commands_removed':
details.push(`Removed commands: ${change.items.join(', ')}`);
break;
case 'commands_modified':
details.push(`Modified commands: ${change.items.join(', ')}`);
break;
case 'phases_added':
details.push(`Added phases: ${change.items.join(', ')}`);
break;
case 'phases_removed':
details.push(`Removed phases: ${change.items.join(', ')}`);
break;
case 'phase_modified':
details.push(`Modified phase '${change.phase}': ${change.details.join(', ')}`);
break;
case 'metadata_changed':
details.push(`Updated ${change.field}: "${change.from}" → "${change.to}"`);
break;
case 'section_modified':
details.push(`Updated ${change.section} section`);
break;
case 'elicitation_changed':
details.push(`Elicitation blocks: ${change.from} → ${change.to}`);
break;
case 'steps_changed':
details.push(`Task steps: ${change.from} → ${change.to}`);
break;
}
}
// Add metadata information
if (metadata.reason) {
details.push(`\nReason: ${metadata.reason}`);
}
if (metadata.impact) {
details.push(`Impact: ${metadata.impact}`);
}
if (metadata.relatedIssues && metadata.relatedIssues.length > 0) {
details.push(`\nRelated: ${metadata.relatedIssues.join(', ')}`);
}
return details;
}
/**
* Detect breaking changes
* @private
*/
async detectBreakingChanges(componentType, originalContent, modifiedContent) {
const validation = await this.validator.validateModification(
componentType,
originalContent,
modifiedContent,
);
return validation.breakingChanges || [];
}
/**
* Construct the full commit message
* @private
*/
constructMessage(parts) {
const { type, scope, summary, body, breaking, metadata } = parts;
// Header
let message = `${type}(${scope}): ${summary}`;
// Body
if (body && body.length > 0) {
message += '\n\n' + body.join('\n');
}
// Breaking changes
if (breaking && breaking.length > 0) {
message += '\n\nBREAKING CHANGE:';
for (const change of breaking) {
message += `\n- ${change.impact}`;
if (change.items) {
message += ` (${change.items.join(', ')})`;
}
}
}
// Footer
const footer = [];
if (metadata.approvedBy) {
footer.push(`Approved-by: ${metadata.approvedBy}`);
}
if (metadata.reviewedBy) {
footer.push(`Reviewed-by: ${metadata.reviewedBy}`);
}
footer.push('Generated-by: aios-developer meta-agent');
if (footer.length > 0) {
message += '\n\n' + footer.join('\n');
}
return message;
}
/**
* Generate commit message for batch modifications
* @param {Array} modifications - Array of modifications
* @returns {Promise<Object>} Batch commit message
*/
async generateBatchCommitMessage(modifications) {
const summaries = [];
const allBreaking = [];
const stats = {
agents: 0,
tasks: 0,
workflows: 0,
total: modifications.length,
};
// Process each modification
for (const mod of modifications) {
const result = await this.generateCommitMessage(mod);
summaries.push(`- ${result.scope}: ${result.summary}`);
allBreaking.push(...result.breakingChanges);
// Count by type
stats[`${mod.componentType}s`]++;
}
// Determine overall type
const hasBreaking = allBreaking.length > 0;
const type = hasBreaking ? 'feat!' : 'chore';
// Construct message
let message = `${type}: batch update ${stats.total} components`;
message += '\n\nModifications:';
message += '\n' + summaries.join('\n');
message += '\n\nSummary:';
if (stats.agents > 0) message += `\n- ${stats.agents} agent(s)`;
if (stats.tasks > 0) message += `\n- ${stats.tasks} task(s)`;
if (stats.workflows > 0) message += `\n- ${stats.workflows} workflow(s)`;
if (hasBreaking) {
message += '\n\nBREAKING CHANGES:';
for (const breaking of allBreaking) {
message += `\n- ${breaking.impact}`;
}
}
message += '\n\nGenerated-by: aios-developer meta-agent';
return {
message,
type,
stats,
breakingChanges: allBreaking,
};
}
/**
* Suggest commit message improvements
* @param {string} message - Original commit message
* @returns {Object} Suggestions for improvement
*/
suggestImprovements(message) {
const suggestions = [];
const lines = message.split('\n');
const header = lines[0];
// Check header format
const headerMatch = header.match(/^(\w+)(\([\w-]+\))?: (.+)$/);
if (!headerMatch) {
suggestions.push({
type: 'format',
issue: 'Header doesn\'t follow conventional format',
suggestion: 'Use format: type(scope): subject',
});
} else {
const [, type, scope, subject] = headerMatch;
// Check type
if (!this.commitTypes[type]) {
suggestions.push({
type: 'type',
issue: `Unknown commit type: ${type}`,
suggestion: `Use one of: ${Object.keys(this.commitTypes).join(', ')}`,
});
}
// Check subject length
if (subject.length > 50) {
suggestions.push({
type: 'length',
issue: 'Subject line too long',
suggestion: 'Keep subject under 50 characters',
});
}
// Check subject format
if (subject[0] === subject[0].toUpperCase()) {
suggestions.push({
type: 'case',
issue: 'Subject should not be capitalized',
suggestion: 'Use lowercase for subject',
});
}
if (subject.endsWith('.')) {
suggestions.push({
type: 'punctuation',
issue: 'Subject should not end with period',
suggestion: 'Remove trailing period',
});
}
}
// Check body
if (lines.length > 1) {
if (lines[1] !== '') {
suggestions.push({
type: 'spacing',
issue: 'Missing blank line after header',
suggestion: 'Add blank line between header and body',
});
}
// Check line length in body
for (let i = 2; i < lines.length; i++) {
if (lines[i].length > 72 && !lines[i].startsWith('BREAKING')) {
suggestions.push({
type: 'line-length',
issue: `Line ${i + 1} exceeds 72 characters`,
suggestion: 'Wrap body text at 72 characters',
});
}
}
}
return {
valid: suggestions.length === 0,
suggestions,
improvedMessage: this.applyImprovements(message, suggestions),
};
}
/**
* Apply improvements to commit message
* @private
*/
applyImprovements(message, suggestions) {
let improved = message;
for (const suggestion of suggestions) {
switch (suggestion.type) {
case 'case':
improved = improved.replace(/^(\w+)(\([\w-]+\))?: (.)/, (match, type, scope, firstChar) =>
`${type}${scope || ''}: ${firstChar.toLowerCase()}`,
);
break;
case 'punctuation':
improved = improved.replace(/^(.+)\.$/, '$1');
break;
case 'spacing':
const lines = improved.split('\n');
if (lines.length > 1 && lines[1] !== '') {
lines.splice(1, 0, '');
improved = lines.join('\n');
}
break;
}
}
return improved;
}
// Utility methods
parseAgentContent(content) {
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!match) {
throw new Error('Invalid agent content format');
}
return {
yaml: match[1],
markdown: match[2],
};
}
extractSection(content, sectionHeader) {
const regex = new RegExp(`${sectionHeader}[\\s\\S]*?(?=\\n##|$)`, 'i');
const match = content.match(regex);
return match ? match[0] : '';
}
extractSectionName(diffLine) {
const match = diffLine.match(/@@ .* @@ (.+)/);
return match ? match[1] : 'unknown';
}
compareDependencies(original, modified) {
const changes = [];
const types = ['tasks', 'workflows', 'agents'];
for (const type of types) {
const originalDeps = original[type] || [];
const modifiedDeps = modified[type] || [];
const added = modifiedDeps.filter(d => !originalDeps.includes(d));
const removed = originalDeps.filter(d => !modifiedDeps.includes(d));
if (added.length > 0) {
changes.push({ type: `${type}_dependencies_added`, items: added });
}
if (removed.length > 0) {
changes.push({ type: `${type}_dependencies_removed`, items: removed });
}
}
return changes;
}
comparePhases(originalPhase, modifiedPhase) {
const details = [];
if (originalPhase.sequence !== modifiedPhase.sequence) {
details.push(`sequence ${originalPhase.sequence}→${modifiedPhase.sequence}`);
}
const originalAgents = originalPhase.agents || [];
const modifiedAgents = modifiedPhase.agents || [];
if (JSON.stringify(originalAgents) !== JSON.stringify(modifiedAgents)) {
details.push('agents changed');
}
if (JSON.stringify(originalPhase.artifacts) !== JSON.stringify(modifiedPhase.artifacts)) {
details.push('artifacts changed');
}
return details;
}
}
module.exports = CommitMessageGenerator;