aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
285 lines (234 loc) • 8.59 kB
JavaScript
/**
* Decision Log Indexer
*
* Maintains an index file of all decision logs for easy discovery.
* Automatically updates when new logs are generated.
*
* @module decision-log-indexer
* @see .aios-core/scripts/decision-recorder.js
*/
const fs = require('fs').promises;
const path = require('path');
/**
* Load configuration to get index file location
*
* @returns {Promise<Object>} Configuration object
*/
async function loadConfig() {
const yaml = require('js-yaml');
try {
const configContent = await fs.readFile('.aios-core/core-config.yaml', 'utf8');
return yaml.load(configContent);
} catch (error) {
console.warn('Warning: Could not load core-config.yaml for indexing:', error.message);
return {
decisionLogging: {
enabled: true,
location: '.ai/',
indexFile: 'decision-logs-index.md',
},
};
}
}
/**
* Parse metadata from decision log file
*
* @param {string} logPath - Path to decision log file
* @returns {Promise<Object>} Metadata object
*/
async function parseLogMetadata(logPath) {
try {
const content = await fs.readFile(logPath, 'utf8');
// Extract metadata from log content
const storyMatch = content.match(/\*\*Story:\*\* (.+)/);
const generatedMatch = content.match(/\*\*Generated:\*\* (.+)/);
const agentMatch = content.match(/\*\*Agent:\*\* (.+)/);
const statusMatch = content.match(/\*\*Status:\*\* (.+)/);
const executionTimeMatch = content.match(/\*\*Execution Time:\*\* (.+)/);
const decisionsMatch = content.match(/\*\*Decisions Made:\*\* (\d+)/);
// Extract story ID from filename (e.g., decision-log-6.1.2.6.2.md)
const filenameMatch = path.basename(logPath).match(/decision-log-(.+)\.md$/);
const storyId = filenameMatch ? filenameMatch[1] : 'unknown';
return {
storyId,
storyPath: storyMatch ? storyMatch[1] : '',
timestamp: generatedMatch ? new Date(generatedMatch[1]) : new Date(),
agent: agentMatch ? agentMatch[1] : 'unknown',
status: statusMatch ? statusMatch[1] : 'unknown',
duration: executionTimeMatch ? executionTimeMatch[1] : '0s',
decisionCount: decisionsMatch ? parseInt(decisionsMatch[1]) : 0,
logPath: path.normalize(logPath),
};
} catch (error) {
console.error(`Error parsing log metadata from ${logPath}:`, error);
return null;
}
}
/**
* Generate index file content from metadata array
*
* @param {Array<Object>} logMetadata - Array of log metadata objects
* @returns {string} Markdown index content
*/
function generateIndexContent(logMetadata) {
// Sort by timestamp (newest first)
const sorted = logMetadata.sort((a, b) => b.timestamp - a.timestamp);
let markdown = `# Decision Log Index
*Automatically generated decision log index*
*Last updated: ${new Date().toISOString()}*
---
## Quick Links
- [Decision Logging Guide](../docs/guides/decision-logging-guide.md)
- [Core Configuration](.aios-core/core-config.yaml)
---
## Decision Logs
Total logs: ${logMetadata.length}
| Story ID | Date | Agent | Status | Duration | Decisions | Log File |
|----------|------|-------|--------|----------|-----------|----------|
`;
sorted.forEach(meta => {
const date = meta.timestamp.toISOString().split('T')[0]; // YYYY-MM-DD
const relativePath = path.relative('.ai', meta.logPath).replace(/\\/g, '/');
markdown += `| ${meta.storyId} | ${date} | ${meta.agent} | ${meta.status} | ${meta.duration} | ${meta.decisionCount} | [View](${relativePath}) |\n`;
});
markdown += `\n---
## Legend
- **Story ID**: Story identifier (e.g., 6.1.2.6.2)
- **Date**: When the decision log was generated
- **Agent**: Which agent executed the story (typically 'dev')
- **Status**: Execution status (completed, failed, cancelled)
- **Duration**: Total execution time
- **Decisions**: Number of autonomous decisions logged
- **Log File**: Link to full decision log
---
*This index is updated automatically when decision logs are generated*
*To manually rebuild the index, run: node .aios-core/scripts/decision-log-indexer.js rebuild*
`;
return markdown;
}
/**
* Add or update a log entry in the index
*
* @param {string} logPath - Path to the decision log file
* @returns {Promise<string>} Path to index file
*/
async function addToIndex(logPath) {
const config = await loadConfig();
if (!config.decisionLogging?.enabled) {
console.log('Decision logging disabled, skipping index update');
return null;
}
const indexDir = config.decisionLogging.location || '.ai/';
const indexFile = path.join(indexDir, config.decisionLogging.indexFile || 'decision-logs-index.md');
try {
// Create .ai directory if it doesn't exist
await fs.mkdir(indexDir, { recursive: true });
// Parse metadata from the new log
const newMetadata = await parseLogMetadata(logPath);
if (!newMetadata) {
console.warn('Could not parse log metadata, index not updated');
return null;
}
// Read existing index (if it exists)
let existingMetadata = [];
try {
const existingContent = await fs.readFile(indexFile, 'utf8');
// Parse existing log entries from table
const tableMatch = existingContent.match(/\| Story ID \|.+\n\|[-\s|]+\n((?:\|.+\n)*)/);
if (tableMatch) {
const rows = tableMatch[1].trim().split('\n');
existingMetadata = rows
.map(row => {
const cells = row.split('|').map(c => c.trim()).filter(c => c);
if (cells.length < 7) return null;
return {
storyId: cells[0],
timestamp: new Date(cells[1]),
agent: cells[2],
status: cells[3],
duration: cells[4],
decisionCount: parseInt(cells[5]) || 0,
logPath: cells[6].match(/\[View\]\((.+)\)/)?.[1] || '',
};
})
.filter(m => m !== null);
}
} catch (_error) {
// Index doesn't exist yet, that's okay
console.log('Creating new decision log index');
}
// Remove old entry for same story ID if exists
existingMetadata = existingMetadata.filter(m => m.storyId !== newMetadata.storyId);
// Add new entry
existingMetadata.push(newMetadata);
// Generate updated index content
const indexContent = generateIndexContent(existingMetadata);
// Write index file
await fs.writeFile(indexFile, indexContent, 'utf8');
console.log(`✅ Decision log index updated: ${indexFile}`);
return indexFile;
} catch (error) {
console.error('Error updating decision log index:', error);
throw error;
}
}
/**
* Rebuild entire index by scanning .ai directory
*
* @returns {Promise<string>} Path to index file
*/
async function rebuildIndex() {
const config = await loadConfig();
if (!config.decisionLogging?.enabled) {
console.log('Decision logging disabled, cannot rebuild index');
return null;
}
const indexDir = config.decisionLogging.location || '.ai/';
const indexFile = path.join(indexDir, config.decisionLogging.indexFile || 'decision-logs-index.md');
try {
// Find all decision log files
const files = await fs.readdir(indexDir);
const logFiles = files.filter(f => f.startsWith('decision-log-') && f.endsWith('.md'));
console.log(`Found ${logFiles.length} decision log files`);
// Parse metadata from all logs
const metadata = [];
for (const file of logFiles) {
const logPath = path.join(indexDir, file);
const meta = await parseLogMetadata(logPath);
if (meta) {
metadata.push(meta);
}
}
// Generate index content
const indexContent = generateIndexContent(metadata);
// Write index file
await fs.writeFile(indexFile, indexContent, 'utf8');
console.log(`✅ Decision log index rebuilt: ${indexFile}`);
console.log(` Indexed ${metadata.length} decision logs`);
return indexFile;
} catch (error) {
console.error('Error rebuilding decision log index:', error);
throw error;
}
}
// CLI usage: node decision-log-indexer.js rebuild
if (require.main === module) {
const command = process.argv[2];
if (command === 'rebuild') {
rebuildIndex()
.then(() => console.log('Index rebuild complete'))
.catch(error => {
console.error('Index rebuild failed:', error);
process.exit(1);
});
} else {
console.log('Usage: node decision-log-indexer.js rebuild');
process.exit(1);
}
}
module.exports = {
addToIndex,
rebuildIndex,
parseLogMetadata,
generateIndexContent,
};