ripple-ai-detector
Version:
🌊 Ripple AI Bug Detector - Built by an AI that knows its flaws. Catch AI-generated bugs before you commit.
216 lines (181 loc) • 7.05 kB
text/typescript
import { AnalysisResult, Issue, AnalysisSummary, AnalysisMetadata } from '../types/analysis';
import { RippleConfig } from '../config/config';
import { FunctionSignatureDetector } from '../detectors/function-signature-detector';
import { AIDetector } from '../detectors/ai-detector';
import { FileUtils } from '../utils/file-utils';
export class RippleAnalyzer {
private config: RippleConfig;
private functionDetector: FunctionSignatureDetector;
private aiDetector: AIDetector;
constructor(config: RippleConfig) {
this.config = config;
this.functionDetector = new FunctionSignatureDetector();
this.aiDetector = new AIDetector();
}
// Main analysis method
async analyze(files: string[]): Promise<AnalysisResult> {
const startTime = Date.now();
// Filter and validate files
const validFiles = await this.filterValidFiles(files);
if (validFiles.length === 0) {
return this.createEmptyResult(startTime);
}
// Limit number of files to analyze
const filesToAnalyze = validFiles.slice(0, this.config.analysis.maxFiles);
const issues: Issue[] = [];
// Run function signature detection
if (this.config.rules.functionSignatureChange.enabled) {
const functionIssues = await this.functionDetector.detect(filesToAnalyze);
issues.push(...functionIssues);
}
// Run AI detection
let aiGenerated = false;
let aiConfidence = 0;
if (this.config.aiDetection.enabled) {
const aiResult = await this.aiDetector.detect(filesToAnalyze);
aiGenerated = aiResult.isAIGenerated;
aiConfidence = aiResult.confidence;
// Add AI detection as an issue if confidence is high
if (aiGenerated && aiConfidence > 0.7) {
issues.push({
id: `ai-detection-${Date.now()}`,
type: 'ai-pattern-detected',
severity: 'warning',
message: `AI-generated changes detected (${Math.round(aiConfidence * 100)}% confidence)`,
file: filesToAnalyze[0], // Primary file
details: {
aiPatterns: aiResult.patterns.map(p => p.type),
aiConfidence,
context: aiResult.reasoning.join('; ')
},
suggestions: [
'Review changes carefully before committing',
'Test all affected functionality',
'Consider running additional validation'
],
confidence: aiConfidence
});
}
}
const endTime = Date.now();
// Calculate overall confidence
const overallConfidence = this.calculateOverallConfidence(issues, aiConfidence);
// Create summary
const summary: AnalysisSummary = {
filesAnalyzed: filesToAnalyze.length,
errors: issues.filter(i => i.severity === 'error').length,
warnings: issues.filter(i => i.severity === 'warning').length,
aiDetections: aiGenerated ? 1 : 0,
timeMs: endTime - startTime
};
// Create metadata
const metadata: AnalysisMetadata = {
version: '1.0.0',
timestamp: new Date().toISOString(),
aiDetectionEnabled: this.config.aiDetection.enabled,
rulesUsed: this.getEnabledRules()
};
return {
success: issues.filter(i => i.severity === 'error').length === 0,
confidence: overallConfidence,
aiGenerated,
issues,
summary,
metadata
};
}
// Filter files to only include valid, supported files
private async filterValidFiles(files: string[]): Promise<string[]> {
const validFiles: string[] = [];
for (const file of files) {
try {
const exists = await FileUtils.exists(file);
const isSupported = FileUtils.isSupportedFile(file);
const shouldInclude = this.shouldIncludeFile(file);
// Check if file exists and is supported
if (exists && isSupported) {
// Check against include/exclude patterns
if (shouldInclude) {
validFiles.push(file);
}
}
} catch (error) {
// Skip files that can't be accessed
}
}
return validFiles;
}
// Check if file should be included based on config patterns
private shouldIncludeFile(file: string): boolean {
const relativePath = FileUtils.getRelativePath(file);
// For MVP: Simple check for JS/TS files
return relativePath.endsWith('.js') || relativePath.endsWith('.ts') ||
relativePath.endsWith('.jsx') || relativePath.endsWith('.tsx');
}
// Simple pattern matching (in production, use a proper glob library)
private matchesPattern(filePath: string, pattern: string): boolean {
// Handle {js,ts,jsx,tsx} syntax
let regexPattern = pattern;
// Expand {js,ts,jsx,tsx} to (js|ts|jsx|tsx)
regexPattern = regexPattern.replace(/\{([^}]+)\}/g, (match, group) => {
const options = group.split(',').map((s: string) => s.trim());
return `(${options.join('|')})`;
});
// Convert glob pattern to regex (order matters!)
regexPattern = regexPattern
.replace(/\*\*/g, '__DOUBLE_STAR__') // Temporarily replace **
.replace(/\./g, '\\.') // Escape dots first
.replace(/__DOUBLE_STAR__/g, '.*') // Then replace ** with .*
.replace(/\*/g, '[^/]*'); // Finally replace single * with [^/]*
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(filePath);
}
// Calculate overall confidence score
private calculateOverallConfidence(issues: Issue[], aiConfidence: number): number {
if (issues.length === 0) {
return 0.95; // High confidence when no issues found
}
// Average confidence of all issues
const issueConfidences = issues.map(i => i.confidence);
const avgIssueConfidence = issueConfidences.reduce((sum, conf) => sum + conf, 0) / issueConfidences.length;
// Combine with AI detection confidence
const combinedConfidence = (avgIssueConfidence + aiConfidence) / 2;
return Math.min(combinedConfidence, 0.99); // Cap at 99%
}
// Get list of enabled rules
private getEnabledRules(): string[] {
const rules: string[] = [];
if (this.config.rules.functionSignatureChange.enabled) {
rules.push('function-signature-change');
}
if (this.config.rules.importExportMismatch.enabled) {
rules.push('import-export-mismatch');
}
if (this.config.rules.typeMismatch.enabled) {
rules.push('type-mismatch');
}
return rules;
}
// Create empty result when no files to analyze
private createEmptyResult(startTime: number): AnalysisResult {
return {
success: true,
confidence: 1.0,
aiGenerated: false,
issues: [],
summary: {
filesAnalyzed: 0,
errors: 0,
warnings: 0,
aiDetections: 0,
timeMs: Date.now() - startTime
},
metadata: {
version: '1.0.0',
timestamp: new Date().toISOString(),
aiDetectionEnabled: this.config.aiDetection.enabled,
rulesUsed: this.getEnabledRules()
}
};
}
}