UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

475 lines (410 loc) 13.1 kB
#!/usr/bin/env node /** * AIOS Path Analyzer * Story 2.2: Analyzes dependencies between assets and detects broken references * * Usage: * node path-analyzer.js [--verbose] [--json] [--fix] [--output path] * * @module path-analyzer */ const fs = require('fs').promises; const path = require('path'); // Reference patterns to detect const REFERENCE_PATTERNS = [ // Markdown code refs to .aios-core { pattern: /`\.aios-core\/[^`]+`/g, type: 'code-ref' }, // Task references { pattern: /tasks\/([a-z0-9-]+)\.md/gi, type: 'task' }, // Template references (yaml, yml, md) { pattern: /templates\/([a-z0-9-]+)\.(yaml|yml|md)/gi, type: 'template' }, // Checklist references { pattern: /checklists\/([a-z0-9-]+)\.md/gi, type: 'checklist' }, // Script references { pattern: /scripts\/([a-z0-9-]+)\.js/gi, type: 'script' }, // Data file references { pattern: /data\/([a-z0-9-]+)\.(md|yaml|yml|json)/gi, type: 'data' }, // Schema references { pattern: /schemas\/([a-z0-9-]+)\.(json|js)/gi, type: 'schema' }, // Direct file paths in dependencies section { pattern: /^\s+-\s+([a-z0-9-]+\.(?:md|yaml|yml|js))$/gim, type: 'dependency-item' }, ]; // Asset directories for path resolution const ASSET_DIRS = { task: '.aios-core/development/tasks', template: '.aios-core/product/templates', checklist: '.aios-core/product/checklists', script: '.aios-core/infrastructure/scripts', data: '.aios-core/development/data', schema: '.aios-core/schemas', agent: '.aios-core/development/agents', }; /** * Find all files to analyze */ async function findFilesToAnalyze(rootPath) { const files = []; const dirsToScan = [ { dir: '.aios-core/development/agents', type: 'agent' }, { dir: '.aios-core/development/tasks', type: 'task' }, { dir: '.aios-core/product/templates', type: 'template' }, { dir: '.aios-core/product/checklists', type: 'checklist' }, ]; for (const { dir, type } of dirsToScan) { const fullDir = path.join(rootPath, dir); try { const entries = await fs.readdir(fullDir); for (const entry of entries) { if (entry.endsWith('.md') || entry.endsWith('.yaml') || entry.endsWith('.yml')) { files.push({ path: path.join(dir, entry), fullPath: path.join(fullDir, entry), type, }); } } } catch (e) { // Directory doesn't exist } } return files; } /** * Extract references from file content */ function extractReferences(content, filePath) { const references = []; for (const { pattern, type } of REFERENCE_PATTERNS) { // Reset regex pattern.lastIndex = 0; let match; while ((match = pattern.exec(content)) !== null) { const fullMatch = match[0]; let refName = match[1] || fullMatch; // Clean up the reference refName = refName.replace(/^`|`$/g, ''); // Skip if it's a self-reference if (refName === path.basename(filePath)) continue; references.push({ type, reference: refName, fullMatch, position: match.index, }); } } return references; } /** * Resolve reference to actual file path */ function resolveReference(ref, rootPath) { // If it's a full path already if (ref.reference.startsWith('.aios-core/')) { return path.join(rootPath, ref.reference); } // If it's a dependency item (just filename) if (ref.type === 'dependency-item') { // Try to find in common locations const possibleDirs = Object.values(ASSET_DIRS); for (const dir of possibleDirs) { const fullPath = path.join(rootPath, dir, ref.reference); return fullPath; // Return first match attempt } } // Resolve based on type const dirMap = { task: ASSET_DIRS.task, template: ASSET_DIRS.template, checklist: ASSET_DIRS.checklist, script: ASSET_DIRS.script, data: ASSET_DIRS.data, schema: ASSET_DIRS.schema, }; const baseDir = dirMap[ref.type]; if (baseDir) { // Extract just the filename if it includes directory const filename = ref.reference.includes('/') ? path.basename(ref.reference) : ref.reference; return path.join(rootPath, baseDir, filename); } return null; } /** * Check if file exists */ async function fileExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } /** * Analyze a single file */ async function analyzeFile(file, rootPath) { const content = await fs.readFile(file.fullPath, 'utf-8'); const references = extractReferences(content, file.path); const analysis = { file: file.path, type: file.type, references: [], brokenReferences: [], validReferences: [], }; for (const ref of references) { const resolvedPath = resolveReference(ref, rootPath); if (resolvedPath) { const exists = await fileExists(resolvedPath); const relativePath = path.relative(rootPath, resolvedPath); const refInfo = { type: ref.type, reference: ref.reference, resolvedPath: relativePath, exists, }; analysis.references.push(refInfo); if (exists) { analysis.validReferences.push(refInfo); } else { analysis.brokenReferences.push(refInfo); } } } return analysis; } /** * Suggest fixes for broken references */ async function suggestFixes(brokenRef, rootPath) { const suggestions = []; const filename = path.basename(brokenRef.reference); const nameWithoutExt = path.basename(filename, path.extname(filename)); // Search for similar files for (const [type, dir] of Object.entries(ASSET_DIRS)) { const fullDir = path.join(rootPath, dir); try { const files = await fs.readdir(fullDir); for (const file of files) { const fileWithoutExt = path.basename(file, path.extname(file)); // Check for exact match if (file === filename || file === brokenRef.reference) { suggestions.push({ type: 'exact-match', suggestedPath: path.join(dir, file), confidence: 'high', }); } // Check for similar name (case insensitive) else if (fileWithoutExt.toLowerCase() === nameWithoutExt.toLowerCase()) { suggestions.push({ type: 'similar-name', suggestedPath: path.join(dir, file), confidence: 'medium', }); } // Check for partial match else if ( fileWithoutExt.includes(nameWithoutExt) || nameWithoutExt.includes(fileWithoutExt) ) { suggestions.push({ type: 'partial-match', suggestedPath: path.join(dir, file), confidence: 'low', }); } } } catch (e) { // Directory doesn't exist } } return suggestions; } /** * Main path analysis */ async function analyzePaths(rootPath, options = {}) { const { verbose = false, fix = false } = options; const files = await findFilesToAnalyze(rootPath); const results = []; const allBrokenRefs = []; const allValidRefs = []; for (const file of files) { const analysis = await analyzeFile(file, rootPath); results.push(analysis); analysis.brokenReferences.forEach((ref) => { allBrokenRefs.push({ ...ref, sourceFile: file.path, }); }); analysis.validReferences.forEach((ref) => { allValidRefs.push({ ...ref, sourceFile: file.path, }); }); } // Generate fix suggestions if requested const fixSuggestions = []; if (fix) { for (const broken of allBrokenRefs) { const suggestions = await suggestFixes(broken, rootPath); if (suggestions.length > 0) { fixSuggestions.push({ broken, suggestions, }); } } } // Build reference graph const referenceGraph = {}; for (const result of results) { referenceGraph[result.file] = result.validReferences.map((r) => r.resolvedPath); } // Calculate statistics const stats = { filesAnalyzed: files.length, totalReferences: allValidRefs.length + allBrokenRefs.length, validReferences: allValidRefs.length, brokenReferences: allBrokenRefs.length, byType: {}, }; // Count by type [...allValidRefs, ...allBrokenRefs].forEach((ref) => { stats.byType[ref.type] = (stats.byType[ref.type] || 0) + 1; }); return { generated: new Date().toISOString(), rootPath, stats, brokenReferences: allBrokenRefs, fixSuggestions, referenceGraph, details: verbose ? results : undefined, }; } /** * Format output for console */ function formatConsoleOutput(report, verbose = false) { const lines = []; lines.push(''); lines.push('═══════════════════════════════════════════════════════════'); lines.push(' AIOS Path Analysis Report'); lines.push(` Generated: ${report.generated}`); lines.push('═══════════════════════════════════════════════════════════'); lines.push(''); lines.push('SUMMARY'); lines.push('───────'); lines.push(` Files Analyzed: ${report.stats.filesAnalyzed}`); lines.push(` Total References: ${report.stats.totalReferences}`); lines.push(` Valid References: ${report.stats.validReferences}`); lines.push(` Broken References: ${report.stats.brokenReferences}`); lines.push(''); lines.push('REFERENCES BY TYPE'); lines.push('──────────────────'); for (const [type, count] of Object.entries(report.stats.byType)) { lines.push(` ${type}: ${count}`); } lines.push(''); if (report.brokenReferences.length > 0) { lines.push('BROKEN REFERENCES'); lines.push('─────────────────'); report.brokenReferences.forEach((ref) => { lines.push(` ❌ ${ref.sourceFile}`); lines.push(` └─ Missing: ${ref.reference} (type: ${ref.type})`); }); lines.push(''); } if (report.fixSuggestions && report.fixSuggestions.length > 0) { lines.push('FIX SUGGESTIONS'); lines.push('───────────────'); report.fixSuggestions.forEach((fix) => { lines.push(` 📍 ${fix.broken.reference} in ${fix.broken.sourceFile}`); fix.suggestions.slice(0, 3).forEach((s) => { const icon = s.confidence === 'high' ? '✅' : s.confidence === 'medium' ? '🔶' : '🔷'; lines.push(` ${icon} ${s.suggestedPath} (${s.confidence})`); }); }); lines.push(''); } if (report.brokenReferences.length === 0) { lines.push('✅ All references are valid!'); lines.push(''); } lines.push('═══════════════════════════════════════════════════════════'); return lines.join('\n'); } /** * CLI handler */ async function main() { const args = process.argv.slice(2); if (args.includes('--help')) { console.log(` AIOS Path Analyzer Usage: node path-analyzer.js [options] Options: --verbose Show detailed file analysis --json Output as JSON --fix Include fix suggestions for broken references --output <path> Save report to file (default: stdout) --help Show this help message `); return; } const verbose = args.includes('--verbose'); const jsonOutput = args.includes('--json'); const fix = args.includes('--fix'); const outputIndex = args.indexOf('--output'); const outputPath = outputIndex !== -1 ? args[outputIndex + 1] : null; // Find project root let rootPath = process.cwd(); while (rootPath !== '/') { try { await fs.access(path.join(rootPath, '.aios-core')); break; } catch { rootPath = path.dirname(rootPath); } } if (rootPath === '/') { console.error('Error: Could not find .aios-core directory. Run from project root.'); process.exit(1); } const report = await analyzePaths(rootPath, { verbose, fix }); let output; if (jsonOutput) { output = JSON.stringify(report, null, 2); } else { output = formatConsoleOutput(report, verbose); } if (outputPath) { const fullOutputPath = path.resolve(outputPath); await fs.writeFile(fullOutputPath, output); console.log(`Report saved to: ${fullOutputPath}`); } else { console.log(output); } // Exit with error code if there are broken references process.exit(report.brokenReferences.length > 0 ? 1 : 0); } // Export for programmatic use module.exports = { analyzePaths, extractReferences, REFERENCE_PATTERNS, ASSET_DIRS, }; // Run CLI if called directly if (require.main === module) { main().catch((error) => { console.error('Error:', error.message); process.exit(1); }); }