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

444 lines 14.2 kB
/** * @file metrics-collector.ts * @description Metrics collection and aggregation system * * Implements F-010/UC-010: Metrics Collection * - Code quality metrics (complexity, duplication, coverage) * - Performance metrics (build time, test time, response time) * - Team metrics (velocity, throughput, lead time) * - Trend analysis and visualization * - Metric export (CSV, JSON, Prometheus) * * @implements NFR-METRICS-001: Collection overhead <5% * @implements NFR-METRICS-002: Real-time dashboard updates <1s * @implements NFR-METRICS-003: Retention 90 days minimum */ import { promises as fs } from 'fs'; import path from 'path'; // ============================================================================ // Metrics Collector Class // ============================================================================ export class MetricsCollector { config; metrics; snapshots; collectionTimer; constructor(config) { this.config = { projectPath: config.projectPath, retentionDays: config.retentionDays || 90, collectInterval: config.collectInterval || 60000, exportFormats: config.exportFormats || ['json'], storePath: config.storePath || path.join(config.projectPath, '.metrics') }; this.metrics = new Map(); this.snapshots = []; } // ======================================================================== // Collection Methods // ======================================================================== /** * Start automatic metric collection */ async startCollection() { // Initial collection await this.collect(); // Schedule periodic collection this.collectionTimer = setInterval(async () => { await this.collect(); }, this.config.collectInterval); } /** * Stop automatic collection */ stopCollection() { if (this.collectionTimer) { clearInterval(this.collectionTimer); this.collectionTimer = undefined; } } /** * Collect all metrics */ async collect() { const snapshot = { timestamp: new Date(), codeQuality: await this.collectCodeQuality(), performance: await this.collectPerformance(), team: await this.collectTeamMetrics() }; this.snapshots.push(snapshot); await this.pruneOldSnapshots(); await this.persist(); return snapshot; } /** * Collect code quality metrics */ async collectCodeQuality() { // Placeholder: In real implementation, would integrate with: // - SonarQube // - ESLint // - Coverage tools (Istanbul, NYC) // - Complexity analyzers return { complexity: this.measureComplexity(), duplication: this.measureDuplication(), coverage: await this.measureCoverage(), linesOfCode: await this.countLinesOfCode(), technicalDebt: this.estimateTechnicalDebt(), codeSmells: 0, bugs: 0, vulnerabilities: 0 }; } /** * Collect performance metrics */ async collectPerformance() { return { buildTime: await this.measureBuildTime(), testTime: await this.measureTestTime(), deployTime: 0, responseTime: 0, throughput: 0, errorRate: 0 }; } /** * Collect team metrics */ async collectTeamMetrics() { return { velocity: await this.calculateVelocity(), throughput: await this.calculateThroughput(), leadTime: await this.calculateLeadTime(), cycleTime: await this.calculateCycleTime(), deployFrequency: 0, changeFailureRate: 0, meanTimeToRecovery: 0 }; } // ======================================================================== // Specific Metric Calculations // ======================================================================== /** * Measure cyclomatic complexity */ measureComplexity() { // Simplified: Would integrate with complexity analyzer return 5.2; // Average complexity } /** * Measure code duplication */ measureDuplication() { // Simplified: Would integrate with duplication detector return 3.5; // % duplicated } /** * Measure test coverage */ async measureCoverage() { // Simplified: Would read from coverage report return 85.3; // % coverage } /** * Count lines of code */ async countLinesOfCode() { let totalLines = 0; const countInDirectory = async (dir) => { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); // Skip node_modules, dist, etc. if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.git') { continue; } if (entry.isDirectory()) { await countInDirectory(fullPath); } else if (entry.isFile() && this.isSourceFile(entry.name)) { const content = await fs.readFile(fullPath, 'utf-8'); totalLines += content.split('\n').length; } } }; try { await countInDirectory(this.config.projectPath); } catch { // Ignore errors } return totalLines; } /** * Check if file is source code */ isSourceFile(filename) { const sourceExtensions = ['.ts', '.js', '.tsx', '.jsx', '.py', '.java', '.go', '.rs']; return sourceExtensions.some(ext => filename.endsWith(ext)); } /** * Estimate technical debt */ estimateTechnicalDebt() { // Simplified: Would integrate with debt tracking tools return 120; // hours } /** * Measure build time */ async measureBuildTime() { // Simplified: Would track actual build duration return 45; // seconds } /** * Measure test time */ async measureTestTime() { // Simplified: Would track actual test duration return 30; // seconds } /** * Calculate team velocity */ async calculateVelocity() { // Simplified: Would integrate with issue tracker return 25; // story points per sprint } /** * Calculate throughput */ async calculateThroughput() { // Simplified: Would count completed issues return 12; // issues per week } /** * Calculate lead time */ async calculateLeadTime() { // Simplified: Would analyze issue lifecycle return 5.2; // days } /** * Calculate cycle time */ async calculateCycleTime() { // Simplified: Would analyze work in progress return 3.8; // days } // ======================================================================== // Time Series Methods // ======================================================================== /** * Record a metric point */ recordMetric(name, value, labels) { let series = this.metrics.get(name); if (!series) { series = { name, type: 'gauge', points: [] }; this.metrics.set(name, series); } series.points.push({ timestamp: new Date(), value, labels }); this.pruneOldPoints(series); } /** * Get metric series */ getMetric(name) { return this.metrics.get(name); } /** * Get all metrics */ getAllMetrics() { return Array.from(this.metrics.values()); } /** * Get recent snapshots */ getSnapshots(count = 100) { return this.snapshots.slice(-count); } /** * Get latest snapshot */ getLatestSnapshot() { return this.snapshots[this.snapshots.length - 1]; } // ======================================================================== // Analysis Methods // ======================================================================== /** * Calculate metric trend */ calculateTrend(metricName, windowSize = 10) { const series = this.metrics.get(metricName); if (!series || series.points.length < windowSize) { return 'stable'; } const recentPoints = series.points.slice(-windowSize); const firstHalf = recentPoints.slice(0, windowSize / 2); const secondHalf = recentPoints.slice(windowSize / 2); const avgFirst = firstHalf.reduce((sum, p) => sum + p.value, 0) / firstHalf.length; const avgSecond = secondHalf.reduce((sum, p) => sum + p.value, 0) / secondHalf.length; const change = ((avgSecond - avgFirst) / avgFirst) * 100; if (change > 5) return 'increasing'; if (change < -5) return 'decreasing'; return 'stable'; } /** * Get metric statistics */ getStatistics(metricName) { const series = this.metrics.get(metricName); if (!series || series.points.length === 0) { return null; } const values = series.points.map(p => p.value); const sorted = [...values].sort((a, b) => a - b); const min = sorted[0]; const max = sorted[sorted.length - 1]; const avg = values.reduce((sum, v) => sum + v, 0) / values.length; const median = sorted[Math.floor(sorted.length / 2)]; const variance = values.reduce((sum, v) => sum + Math.pow(v - avg, 2), 0) / values.length; const stdDev = Math.sqrt(variance); return { min, max, avg, median, stdDev }; } // ======================================================================== // Export Methods // ======================================================================== /** * Export metrics to JSON */ exportJSON() { return JSON.stringify({ snapshots: this.snapshots, series: Array.from(this.metrics.values()) }, null, 2); } /** * Export metrics to CSV */ exportCSV() { const lines = []; // Header lines.push('timestamp,metric,value'); // Data for (const series of this.metrics.values()) { for (const point of series.points) { lines.push(`${point.timestamp.toISOString()},${series.name},${point.value}`); } } return lines.join('\n'); } /** * Export metrics in Prometheus format */ exportPrometheus() { const lines = []; for (const series of this.metrics.values()) { // Type and help lines.push(`# HELP ${series.name} ${series.description || series.name}`); lines.push(`# TYPE ${series.name} ${series.type}`); // Latest value const latest = series.points[series.points.length - 1]; if (latest) { const labels = latest.labels ? '{' + Object.entries(latest.labels).map(([k, v]) => `${k}="${v}"`).join(',') + '}' : ''; lines.push(`${series.name}${labels} ${latest.value}`); } } return lines.join('\n'); } /** * Export to file */ async exportToFile(format, filePath) { let content; switch (format) { case 'json': content = this.exportJSON(); break; case 'csv': content = this.exportCSV(); break; case 'prometheus': content = this.exportPrometheus(); break; } await fs.writeFile(filePath, content, 'utf-8'); } // ======================================================================== // Persistence Methods // ======================================================================== /** * Save metrics to disk */ async persist() { try { await fs.mkdir(this.config.storePath, { recursive: true }); const data = { snapshots: this.snapshots, metrics: Array.from(this.metrics.entries()) }; const filePath = path.join(this.config.storePath, 'metrics.json'); await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8'); } catch { // Ignore persistence errors } } /** * Load metrics from disk */ async load() { try { const filePath = path.join(this.config.storePath, 'metrics.json'); const content = await fs.readFile(filePath, 'utf-8'); const data = JSON.parse(content); this.snapshots = data.snapshots || []; this.metrics = new Map(data.metrics || []); } catch { // No existing data } } /** * Prune old snapshots (NFR-METRICS-003: 90 days retention) */ async pruneOldSnapshots() { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays); this.snapshots = this.snapshots.filter(snapshot => snapshot.timestamp > cutoffDate); } /** * Prune old metric points */ pruneOldPoints(series) { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays); series.points = series.points.filter(point => point.timestamp > cutoffDate); } /** * Clear all metrics */ clear() { this.metrics.clear(); this.snapshots = []; } } //# sourceMappingURL=metrics-collector.js.map