@invisiblecities/sidequest-cqo
Version:
Configuration-agnostic TypeScript and ESLint orchestrator with real-time watch mode, SQLite persistence, and intelligent terminal detection
376 lines • 16.2 kB
JavaScript
/**
* Analysis Service for Code Quality Orchestrator
* Provides historical analysis and delta computation
*/
import { arrayAt } from "../utils/node-compatibility.js";
// import type {
// ViolationHistory,
// HistoryQueryParams
// } from '../database/types.js';
// ============================================================================
// Analysis Service Implementation
// ============================================================================
export class AnalysisService {
storageService;
constructor(storageService) {
this.storageService = storageService;
}
// ========================================================================
// Delta Analysis
// ========================================================================
computeViolationDeltas(previousViolations, currentViolations) {
const previousSet = new Set(previousViolations);
const currentSet = new Set(currentViolations);
const added = currentViolations.filter((hash) => !previousSet.has(hash));
const removed = previousViolations.filter((hash) => !currentSet.has(hash));
const unchanged = currentViolations.filter((hash) => previousSet.has(hash));
return { added, removed, unchanged };
}
// ========================================================================
// Historical Analysis
// ========================================================================
async getViolationTrends(timeRange) {
const violations = await this.storageService.getViolations({
since: timeRange.start.toISOString(),
});
// Group violations by date, category, and severity
const trendsMap = new Map();
for (const violation of violations) {
const date = violation.last_seen_at?.split("T")?.[0] ??
new Date().toISOString().split("T")[0]; // Extract date part
const key = `${date}:${violation.category}:${violation.severity}`;
if (trendsMap.has(key)) {
trendsMap.get(key).count++;
}
else {
trendsMap.set(key, {
date,
count: 1,
severity: violation.severity,
category: violation.category,
});
}
}
return [...trendsMap.values()].sort((a, b) => a.date.localeCompare(b.date));
}
getRulePerformanceAnalysis(ruleId) {
// const rulePerformance = await this.storageService.getRulePerformance();
const rulePerformance = []; // TODO: Implement getRulePerformance on IStorageService
let filteredPerformance = rulePerformance;
if (ruleId) {
filteredPerformance = rulePerformance.filter((rule) => rule.rule_id === ruleId);
}
return filteredPerformance.map((rule) => {
const ruleData = rule;
return {
rule: ruleData.rule_id,
engine: ruleData.engine,
avgExecutionTime: ruleData.avg_execution_time_ms || 0,
avgViolationsFound: ruleData.avg_violations_found || 0,
successRate: ruleData.total_runs > 0
? ruleData.successful_runs / ruleData.total_runs
: 0,
lastRun: ruleData.last_run_at || "Never",
trend: this.calculateTrend(ruleData.avg_violations_found ?? 0, ruleData.consecutive_zero_count ?? 0),
};
});
}
async getFileQualityTrends(filePath) {
const violations = await this.storageService.getViolations(filePath ? { file_paths: [filePath] } : {});
// Group by file path
const fileMap = new Map();
for (const violation of violations) {
if (!fileMap.has(violation.file_path)) {
fileMap.set(violation.file_path, { count: 0, categories: new Set() });
}
const fileData = fileMap.get(violation.file_path);
fileData.count++;
fileData.categories.add(violation.category);
}
return [...fileMap.entries()].map(([path, data]) => ({
filePath: path,
violationCount: data.count,
trend: this.calculateFileTrend(data.count), // Simplified trend calculation
categories: [...data.categories],
}));
}
// ========================================================================
// Statistical Analysis
// ========================================================================
async calculateViolationStats(_timeRange) {
// For now, get all active violations to show meaningful stats
// TODO: Fix timestamp filtering when database schema is properly updated
const violations = await this.storageService.getViolations();
const byCategory = {};
const bySeverity = {};
const bySource = {};
const uniqueFiles = new Set();
for (const violation of violations) {
// Count by category
byCategory[violation.category] =
(byCategory[violation.category] || 0) + 1;
// Count by severity
bySeverity[violation.severity] =
(bySeverity[violation.severity] || 0) + 1;
// Count by source
bySource[violation.source] = (bySource[violation.source] || 0) + 1;
// Track unique files
uniqueFiles.add(violation.file_path);
}
return {
total: violations.length,
byCategory,
bySeverity,
bySource,
avgPerFile: uniqueFiles.size > 0 ? violations.length / uniqueFiles.size : 0,
filesAffected: uniqueFiles.size,
};
}
async identifyProblemFiles(threshold = 10) {
const fileQualityTrends = await this.getFileQualityTrends();
return fileQualityTrends
.filter((file) => file.violationCount >= threshold)
.map((file) => ({
filePath: file.filePath,
violationCount: file.violationCount,
severityScore: this.calculateSeverityScore(file.violationCount),
categories: file.categories,
lastModified: new Date().toISOString(), // Simplified - would need file system access
}))
.sort((a, b) => b.severityScore - a.severityScore);
}
detectRuleFlakyness(minRuns = 10) {
// const rulePerformance = await this.storageService.getRulePerformance();
const rulePerformance = []; // TODO: Implement getRulePerformance
return rulePerformance
.filter((rule) => rule.total_runs >= minRuns)
.map((rule) => {
const ruleData = rule;
const variance = this.calculateVariance(ruleData.avg_violations_found ?? 0, ruleData.total_runs);
const stdDeviation = Math.sqrt(variance);
return {
rule: ruleData.rule_id,
engine: ruleData.engine,
varianceScore: variance,
runCount: rule.total_runs,
avgViolations: rule.avg_violations_found ||
0,
stdDeviation,
};
})
.filter((rule) => rule.stdDeviation > 2) // High variance threshold
.sort((a, b) => b.varianceScore - a.varianceScore);
}
// ========================================================================
// Predictive Analysis
// ========================================================================
async predictViolationGrowth(timeRange) {
const trends = await this.getViolationTrends(timeRange);
if (trends.length < 2) {
return {
projectedGrowth: 0,
confidence: 0,
timeframe: "30 days",
factors: ["Insufficient historical data"],
};
}
// Simple linear regression for growth prediction
const totalByDate = this.groupTrendsByDate(trends);
const growthRate = this.calculateGrowthRate(totalByDate);
return {
projectedGrowth: growthRate * 30, // 30-day projection
confidence: Math.min(trends.length / 30, 1), // Confidence based on data points
timeframe: "30 days",
factors: this.identifyGrowthFactors(trends),
};
}
recommendRuleFrequencies() {
// const rulePerformance = await this.storageService.getRulePerformance();
const rulePerformance = []; // TODO: Implement getRulePerformance
return rulePerformance.map((rule) => {
const currentFrequency = 30_000; // Default 30 seconds
let recommendedFrequency = currentFrequency;
let reasoning = "No change recommended";
// Rules that consistently find violations should run more frequently
if (rule.avg_violations_found &&
rule.avg_violations_found > 5) {
recommendedFrequency = currentFrequency / 2;
reasoning = "High violation rate - increase frequency";
}
// Rules that rarely find violations can run less frequently
else if ((rule.avg_violations_found ??
0) < 1 &&
(rule.consecutive_zero_count ??
0) > 5) {
recommendedFrequency = currentFrequency * 2;
reasoning = "Low violation rate - decrease frequency";
}
// Rules with high execution time should run less frequently
else if ((rule.avg_execution_time_ms ??
0) > 1000) {
recommendedFrequency = currentFrequency * 1.5;
reasoning = "High execution time - decrease frequency";
}
return {
rule: rule.rule_id,
engine: rule.engine,
currentFrequency,
recommendedFrequency: Math.max(5000, Math.min(300_000, recommendedFrequency)), // 5s to 5min bounds
reasoning,
};
});
}
// ========================================================================
// Report Generation
// ========================================================================
async generateQualityReport(timeRange) {
const [summary, trends, problemFiles, rulePerformance] = await Promise.all([
this.calculateViolationStats(timeRange),
this.getViolationTrends(timeRange),
this.identifyProblemFiles(5),
this.getRulePerformanceAnalysis(),
]);
const recommendations = this.generateRecommendations(summary, problemFiles, rulePerformance);
return {
timeRange,
summary,
trends,
problemFiles,
rulePerformance,
recommendations,
};
}
async generateRuleEfficiencyReport() {
// const rulePerformance = await this.storageService.getRulePerformance();
const rulePerformance = []; // TODO: Implement getRulePerformance
const recommendations = await this.recommendRuleFrequencies();
const totalRules = rulePerformance.length;
const activeRules = rulePerformance.filter((rule) => rule.enabled).length;
const avgExecutionTime = rulePerformance.reduce((sum, rule) => sum +
(rule.avg_execution_time_ms ||
0), 0) / totalRules;
return {
totalRules,
activeRules,
avgExecutionTime,
resourceUtilization: this.calculateResourceUtilization(rulePerformance),
recommendations,
};
}
// ========================================================================
// Private Helper Methods
// ========================================================================
calculateTrend(avgViolations, consecutiveZeroCount) {
if (consecutiveZeroCount > 3) {
return "improving";
}
if (avgViolations < 1) {
return "stable";
}
if (avgViolations > 10) {
return "degrading";
}
return "stable";
}
calculateFileTrend(violationCount) {
// Simplified trend calculation - in practice would compare historical data
if (violationCount < 3) {
return "improving";
}
if (violationCount > 15) {
return "degrading";
}
return "stable";
}
calculateSeverityScore(violationCount) {
// Simple scoring algorithm - could be more sophisticated
return violationCount * 1.5;
}
calculateVariance(avgValue, sampleSize) {
// Simplified variance calculation - would need actual data points
return avgValue * (1 / Math.sqrt(sampleSize));
}
groupTrendsByDate(trends) {
const dateMap = new Map();
for (const trend of trends) {
const current = dateMap.get(trend.date) || 0;
dateMap.set(trend.date, current + trend.count);
}
return dateMap;
}
calculateGrowthRate(totalByDate) {
const values = [...totalByDate.values()];
if (values.length < 2) {
return 0;
}
const first = values[0];
const last = arrayAt(values, -1);
if (first === undefined || last === undefined) {
return 0;
}
return (last - first) / values.length;
}
identifyGrowthFactors(trends) {
const factors = [];
// Analyze category distribution
const categoryCount = new Map();
for (const trend of trends) {
categoryCount.set(trend.category, (categoryCount.get(trend.category) || 0) + trend.count);
}
// Find dominant categories
const sortedCategories = [...categoryCount.entries()]
.sort(([, a], [, b]) => b - a)
.slice(0, 3);
for (const [category] of sortedCategories) {
factors.push(`High activity in ${category} violations`);
}
return factors;
}
calculateResourceUtilization(rulePerformance) {
// Simplified resource utilization calculation
const totalExecutionTime = rulePerformance.reduce((sum, rule) => sum + (rule.avg_execution_time_ms || 0), 0);
const maxPossibleTime = rulePerformance.length * 1000; // Assume 1s max per rule
return Math.min(totalExecutionTime / maxPossibleTime, 1);
}
generateRecommendations(summary, problemFiles, rulePerformance) {
const recommendations = [];
// High violation count recommendations
if (summary.total > 100) {
recommendations.push("Consider implementing stricter code review processes");
}
// Problem files recommendations
if (problemFiles.length > 5) {
recommendations.push(`Focus refactoring efforts on ${problemFiles.length} high-violation files`);
}
// Rule performance recommendations
const slowRules = rulePerformance.filter((rule) => (rule.avgExecutionTime || 0) > 500);
if (slowRules.length > 0) {
recommendations.push(`Optimize ${slowRules.length} slow-performing rules`);
}
// Category-specific recommendations
if ((summary.byCategory?.["record-type"] || 0) > summary.total * 0.3) {
recommendations.push("High number of record-type violations - consider TypeScript configuration updates");
}
return recommendations;
}
}
// ============================================================================
// Service Factory
// ============================================================================
let analysisServiceInstance;
/**
* Get or create analysis service instance
*/
export function getAnalysisService(storageService) {
if (!analysisServiceInstance) {
analysisServiceInstance = new AnalysisService(storageService);
}
return analysisServiceInstance;
}
/**
* Reset analysis service instance (useful for testing)
*/
export function resetAnalysisService() {
analysisServiceInstance = undefined;
}
//# sourceMappingURL=analysis-service.js.map