UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

169 lines 6.29 kB
/** * Artifact Index Statistics * * Reports index health, coverage, and distribution metrics. * * @implements #418 * @source @src/artifacts/types.ts * @tests @test/unit/artifacts/stats.test.ts */ import fs from 'fs'; import path from 'path'; import { GRAPH_CONFIGS, loadUserGraphConfigs } from './types.js'; import { loadIndexStats, loadGraphIndexFile } from './index-reader.js'; /** * Count total indexable files under scan directories (excluding .index/) */ function countArtifactFiles(cwd, graphType) { const config = graphType ? GRAPH_CONFIGS[graphType] : undefined; const scanDirs = config ? config.scanDirs.map(d => path.join(cwd, d)) : [path.join(cwd, '.aiwg')]; const extensions = config?.extensions ?? ['.md', '.yaml', '.json']; let count = 0; function walk(dir) { if (!fs.existsSync(dir)) return; const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { if (entry.name.startsWith('.')) continue; // Skip .index, etc. walk(full); } else if (extensions.some(ext => entry.name.endsWith(ext))) { count++; } } } for (const dir of scanDirs) { walk(dir); } return count; } /** * Show artifact index statistics */ export async function showStats(cwd, options = {}) { const { graph } = options; if (graph) { // Single graph mode const stats = loadGraphIndexFile(cwd, 'stats.json', graph); if (!stats) { console.error(`Error: No artifact index found for graph '${graph}'.`); console.log("Run 'aiwg index build' first to create the index."); process.exit(1); } await renderStats(cwd, stats, options, graph); return; } // No graph specified: show all graphs with defaultBuild=true loadUserGraphConfigs(cwd); const graphTypes = Object.entries(GRAPH_CONFIGS) .filter(([, config]) => config.defaultBuild) .map(([name]) => name); const availableGraphs = []; for (const g of graphTypes) { const s = loadGraphIndexFile(cwd, 'stats.json', g); if (s) availableGraphs.push({ type: g, stats: s }); } // Fall back to legacy root index if (availableGraphs.length === 0) { const legacyStats = loadIndexStats(cwd); if (!legacyStats) { console.error('Error: No artifact index found.'); console.log("Run 'aiwg index build' first to create the index."); process.exit(1); } await renderStats(cwd, legacyStats, options); return; } if (options.json) { // JSON mode: aggregate all graphs into one response const combined = {}; for (const { type, stats: s } of availableGraphs) { const totalFiles = countArtifactFiles(cwd, type); combined[type] = { ...s, coverage: { indexed: s.totalArtifacts, totalFiles, percentage: totalFiles > 0 ? Math.round((s.totalArtifacts / totalFiles) * 100) : 100, }, }; } console.log(JSON.stringify(combined, null, 2)); return; } // Human-readable: show each graph for (const { type, stats: s } of availableGraphs) { console.log(`\n[${type.toUpperCase()} GRAPH]`); await renderStats(cwd, s, { ...options, json: false }, type); } } /** * Render stats for a single graph (JSON or human-readable) */ async function renderStats(cwd, stats, options, graphType) { if (options.json) { const totalFiles = countArtifactFiles(cwd, graphType); console.log(JSON.stringify({ ...stats, coverage: { indexed: stats.totalArtifacts, totalFiles, percentage: totalFiles > 0 ? Math.round((stats.totalArtifacts / totalFiles) * 100) : 100, }, }, null, 2)); return; } // Human-readable output console.log('Artifact Index Statistics'); console.log('─'.repeat(40)); console.log(`Index version: ${stats.version}`); console.log(`Last built: ${stats.builtAt}`); console.log(`Build time: ${stats.buildTimeMs}ms`); console.log(''); // By phase console.log('Artifacts by Phase:'); const phases = Object.entries(stats.byPhase).sort((a, b) => b[1] - a[1]); for (const [phase, count] of phases) { console.log(` ${phase.padEnd(20)} ${count} artifacts`); } console.log(` ${'─'.repeat(20)} ${'─'.repeat(12)}`); console.log(` ${'Total'.padEnd(20)} ${stats.totalArtifacts} artifacts`); console.log(''); // By type console.log('Artifacts by Type:'); const types = Object.entries(stats.byType).sort((a, b) => b[1] - a[1]); for (const [type, count] of types) { console.log(` ${type.padEnd(20)} ${count}`); } console.log(''); // Tags const tagEntries = Object.entries(stats.tagDistribution).sort((a, b) => b[1] - a[1]); if (tagEntries.length > 0) { console.log('Tags (top 10):'); const top10 = tagEntries.slice(0, 10); console.log(` ${top10.map(([tag, count]) => `${tag} (${count})`).join(', ')}`); console.log(''); } // Dependency graph console.log('Dependency Graph:'); console.log(` Total edges: ${stats.graphMetrics.totalEdges}`); console.log(` Orphaned artifacts: ${stats.graphMetrics.orphanedArtifacts}`); if (stats.graphMetrics.mostReferenced) { console.log(` Most referenced: ${stats.graphMetrics.mostReferenced.path} (${stats.graphMetrics.mostReferenced.count} dependents)`); } console.log(''); // Coverage const totalFiles = countArtifactFiles(cwd, graphType); const coverage = totalFiles > 0 ? Math.round((stats.totalArtifacts / totalFiles) * 100) : 100; console.log('Index Health:'); console.log(` Coverage: ${stats.totalArtifacts}/${totalFiles} artifacts indexed (${coverage}%)`); } //# sourceMappingURL=stats.js.map