UNPKG

jay-code

Version:

Streamlined AI CLI orchestration engine with mathematical rigor and enterprise-grade reliability

514 lines (429 loc) 15.8 kB
/** * Jay-Code QA Agent: Statistical Code Analysis with Mathematical Scoring * Implements information theory metrics and mathematical validation */ import { EventEmitter } from 'events'; export interface CodeAnalysis { complexity: number; maintainability: number; testability: number; performance: number; security: number; documentation: number; overallScore: number; issues: CodeIssue[]; metrics: CodeMetrics; } export interface CodeIssue { type: 'error' | 'warning' | 'suggestion'; severity: number; // 0-1 scale line?: number; column?: number; message: string; rule: string; fixSuggestion?: string; } export interface CodeMetrics { linesOfCode: number; cyclomaticComplexity: number; cognitiveComplexity: number; halsteadVolume: number; maintainabilityIndex: number; testCoverage?: number; duplicateLines: number; technicalDebt: number; // in hours } export interface QAReport { analysisId: string; timestamp: Date; codeHash: string; analysis: CodeAnalysis; validationResults: ValidationResult[]; recommendations: Improvement[]; passThreshold: number; passed: boolean; } export interface ValidationResult { rule: string; status: 'passed' | 'failed' | 'warning'; score: number; details: string; expectedValue?: any; actualValue?: any; } export interface Improvement { priority: 'high' | 'medium' | 'low'; category: string; description: string; impact: number; // Expected improvement in overall score effort: number; // Estimated effort in hours codeExample?: string; } export class QAAgent extends EventEmitter { private passThreshold: number = 0.7; private strictMode: boolean = false; private autoFix: boolean = true; constructor(options: { passThreshold?: number; strictMode?: boolean; autoFix?: boolean; } = {}) { super(); this.passThreshold = options.passThreshold || 0.7; this.strictMode = options.strictMode || false; this.autoFix = options.autoFix || true; } /** * Review code with statistical analysis and mathematical scoring */ async reviewCode(code: string, requirements: any): Promise<QAReport> { const startTime = Date.now(); const codeHash = this.calculateCodeHash(code); // Comprehensive code analysis with mathematical rigor const analysis = await this.analyzeCode(code); const validationResults = await this.validateAgainstRequirements(code, analysis, requirements); const recommendations = this.generateRecommendations(analysis); const passed = analysis.overallScore >= this.passThreshold; const report: QAReport = { analysisId: `qa_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, timestamp: new Date(), codeHash, analysis, validationResults, recommendations, passThreshold: this.passThreshold, passed }; this.emit('review-completed', { analysisId: report.analysisId, passed, score: analysis.overallScore, duration: Date.now() - startTime }); return report; } private calculateCodeHash(code: string): string { // Simple hash for code versioning - could use crypto.createHash in production let hash = 0; for (let i = 0; i < code.length; i++) { const char = code.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash).toString(16); } private async analyzeCode(code: string): Promise<CodeAnalysis> { const metrics = this.calculateMetrics(code); const issues = this.detectIssues(code); // Mathematical scoring with weighted factors const complexity = this.scoreComplexity(metrics); const maintainability = this.scoreMaintainability(metrics, issues); const testability = this.scoreTestability(code, metrics); const performance = this.scorePerformance(code, issues); const security = this.scoreSecurity(code, issues); const documentation = this.scoreDocumentation(code); // Weighted overall score using mathematical optimization const weights = { complexity: 0.20, maintainability: 0.25, testability: 0.15, performance: 0.15, security: 0.15, documentation: 0.10 }; const overallScore = complexity * weights.complexity + maintainability * weights.maintainability + testability * weights.testability + performance * weights.performance + security * weights.security + documentation * weights.documentation; return { complexity, maintainability, testability, performance, security, documentation, overallScore, issues, metrics }; } private calculateMetrics(code: string): CodeMetrics { const lines = code.split('\n'); const linesOfCode = lines.filter(line => line.trim().length > 0).length; // Cyclomatic complexity calculation const cyclomaticComplexity = this.calculateCyclomaticComplexity(code); // Cognitive complexity (more human-focused than cyclomatic) const cognitiveComplexity = this.calculateCognitiveComplexity(code); // Halstead metrics for program volume const halsteadVolume = this.calculateHalsteadVolume(code); // Maintainability index using mathematical formula const maintainabilityIndex = Math.max(0, 171 - 5.2 * Math.log(halsteadVolume) - 0.23 * cyclomaticComplexity - 16.2 * Math.log(linesOfCode) ) / 171; const duplicateLines = this.countDuplicateLines(lines); const technicalDebt = this.estimateTechnicalDebt(code, cyclomaticComplexity); return { linesOfCode, cyclomaticComplexity, cognitiveComplexity, halsteadVolume, maintainabilityIndex, duplicateLines, technicalDebt }; } private calculateCyclomaticComplexity(code: string): number { // Count decision points: if, while, for, case, catch, &&, ||, ? const patterns = [ /\bif\b/g, /\bwhile\b/g, /\bfor\b/g, /\bcase\b/g, /\bcatch\b/g, /&&/g, /\|\|/g, /\?/g, /\belif\b/g, /\belse\s+if\b/g ]; let complexity = 1; // Base complexity for (const pattern of patterns) { const matches = code.match(pattern); if (matches) complexity += matches.length; } return complexity; } private calculateCognitiveComplexity(code: string): number { // Simplified cognitive complexity - considers nesting and logical operators let complexity = 0; let nestingLevel = 0; const lines = code.split('\n'); for (const line of lines) { // Track nesting level const openBraces = (line.match(/{/g) || []).length; const closeBraces = (line.match(/}/g) || []).length; nestingLevel += openBraces - closeBraces; // Add complexity for control structures with nesting penalty if (/\b(if|while|for|switch)\b/.test(line)) { complexity += 1 + nestingLevel; } // Logical operators add complexity const logicalOps = (line.match(/&&|\|\|/g) || []).length; complexity += logicalOps; } return complexity; } private calculateHalsteadVolume(code: string): number { // Simplified Halstead volume calculation const operators = code.match(/[+\-*/%=<>!&|^~(){}[\];,.:?]/g) || []; const operands = code.match(/\b[a-zA-Z_$][a-zA-Z0-9_$]*\b/g) || []; const uniqueOperators = new Set(operators).size; const uniqueOperands = new Set(operands).size; const totalOperators = operators.length; const totalOperands = operands.length; const vocabulary = uniqueOperators + uniqueOperands; const length = totalOperators + totalOperands; return vocabulary > 0 && length > 0 ? length * Math.log2(vocabulary) : 0; } private countDuplicateLines(lines: string[]): number { const lineMap = new Map<string, number>(); let duplicates = 0; for (const line of lines) { const trimmed = line.trim(); if (trimmed.length > 5) { // Only consider meaningful lines const count = lineMap.get(trimmed) || 0; lineMap.set(trimmed, count + 1); if (count === 1) duplicates++; } } return duplicates; } private estimateTechnicalDebt(code: string, complexity: number): number { // Estimate technical debt in hours based on various factors let debt = 0; // Complexity debt debt += Math.max(0, (complexity - 10) * 0.5); // Documentation debt const comments = (code.match(/\/\*[\s\S]*?\*\/|\/\/.*$/gm) || []).length; const codeLines = code.split('\n').filter(l => l.trim()).length; const commentRatio = comments / Math.max(codeLines, 1); if (commentRatio < 0.1) debt += 2; // Poor documentation // TODO and FIXME debt const todos = (code.match(/TODO|FIXME|HACK/gi) || []).length; debt += todos * 0.5; return debt; } private detectIssues(code: string): CodeIssue[] { const issues: CodeIssue[] = []; const lines = code.split('\n'); // Static analysis patterns const patterns = [ { pattern: /console\.log|debugger/gi, message: 'Debug statement found in production code', type: 'warning' as const, severity: 0.3, rule: 'no-debug' }, { pattern: /eval\(/gi, message: 'Use of eval() is dangerous and should be avoided', type: 'error' as const, severity: 0.9, rule: 'no-eval' }, { pattern: /var\s+/gi, message: 'Use let/const instead of var', type: 'suggestion' as const, severity: 0.2, rule: 'prefer-const' } ]; lines.forEach((line, index) => { for (const { pattern, message, type, severity, rule } of patterns) { if (pattern.test(line)) { issues.push({ type, severity, line: index + 1, message, rule, fixSuggestion: this.generateFixSuggestion(rule, line) }); } } }); return issues; } private generateFixSuggestion(rule: string, line: string): string { switch (rule) { case 'no-debug': return 'Remove debug statement or replace with proper logging'; case 'no-eval': return 'Replace eval() with safer alternatives like JSON.parse()'; case 'prefer-const': return line.replace(/var\s+/, 'const '); default: return 'Follow best practices for this rule'; } } private scoreComplexity(metrics: CodeMetrics): number { // Lower complexity is better, normalize to 0-1 scale const cyclomaticScore = Math.max(0, 1 - (metrics.cyclomaticComplexity - 1) / 20); const cognitiveScore = Math.max(0, 1 - metrics.cognitiveComplexity / 30); return (cyclomaticScore + cognitiveScore) / 2; } private scoreMaintainability(metrics: CodeMetrics, issues: CodeIssue[]): number { // Start with maintainability index let score = metrics.maintainabilityIndex; // Penalize for high-severity issues const severityPenalty = issues .filter(i => i.severity > 0.7) .reduce((sum, i) => sum + i.severity, 0) * 0.1; score -= severityPenalty; // Penalize for duplicates const duplicatePenalty = Math.min(metrics.duplicateLines / metrics.linesOfCode, 0.3); score -= duplicatePenalty; return Math.max(0, Math.min(1, score)); } private scoreTestability(code: string, metrics: CodeMetrics): number { // Higher testability for lower complexity and better structure let score = 1 - (metrics.cyclomaticComplexity - 1) / 20; // Bonus for dependency injection patterns if (/constructor.*inject|@Injectable/i.test(code)) score += 0.1; // Penalty for static methods and global variables const staticMethods = (code.match(/static\s+\w+/g) || []).length; const globalVars = (code.match(/window\.|global\./g) || []).length; score -= (staticMethods + globalVars) * 0.05; return Math.max(0, Math.min(1, score)); } private scorePerformance(code: string, issues: CodeIssue[]): number { let score = 1.0; // Check for performance anti-patterns const performanceIssues = [ { pattern: /for.*in.*Object/, penalty: 0.1 }, { pattern: /\.innerHTML\s*=/, penalty: 0.05 }, { pattern: /document\.getElementById/, penalty: 0.02 } ]; for (const { pattern, penalty } of performanceIssues) { const matches = (code.match(pattern) || []).length; score -= matches * penalty; } return Math.max(0, score); } private scoreSecurity(code: string, issues: CodeIssue[]): number { let score = 1.0; // Security issue penalties const securityScore = issues .filter(i => ['no-eval', 'no-innerHTML'].includes(i.rule)) .reduce((sum, i) => sum + i.severity, 0); score -= securityScore * 0.2; return Math.max(0, score); } private scoreDocumentation(code: string): number { const lines = code.split('\n').filter(l => l.trim()); const comments = (code.match(/\/\*[\s\S]*?\*\/|\/\/.*$/gm) || []).length; const functions = (code.match(/function\s+\w+|=>\s*{|\w+\s*\(/g) || []).length; // Documentation ratio const commentRatio = comments / Math.max(lines.length, 1); const functionDocRatio = comments / Math.max(functions, 1); const score = Math.min(1, commentRatio * 5 + functionDocRatio * 0.5); return Math.max(0, score); } private async validateAgainstRequirements( code: string, analysis: CodeAnalysis, requirements: any ): Promise<ValidationResult[]> { const results: ValidationResult[] = []; // Standard validation rules results.push({ rule: 'complexity-threshold', status: analysis.complexity >= 0.6 ? 'passed' : 'failed', score: analysis.complexity, details: `Code complexity score: ${analysis.complexity.toFixed(2)}` }); results.push({ rule: 'maintainability-threshold', status: analysis.maintainability >= 0.7 ? 'passed' : 'failed', score: analysis.maintainability, details: `Maintainability score: ${analysis.maintainability.toFixed(2)}` }); return results; } private generateRecommendations(analysis: CodeAnalysis): Improvement[] { const recommendations: Improvement[] = []; if (analysis.complexity < 0.6) { recommendations.push({ priority: 'high', category: 'complexity', description: 'Reduce cyclomatic complexity by extracting methods', impact: 0.2, effort: 4, codeExample: 'Extract complex conditional logic into separate methods' }); } if (analysis.documentation < 0.3) { recommendations.push({ priority: 'medium', category: 'documentation', description: 'Add comprehensive documentation and comments', impact: 0.15, effort: 2, codeExample: 'Add JSDoc comments to all public methods' }); } return recommendations; } async validateOutput(result: any, expected: any): Promise<ValidationResult> { // Simple output validation const passed = JSON.stringify(result) === JSON.stringify(expected); return { rule: 'output-validation', status: passed ? 'passed' : 'failed', score: passed ? 1 : 0, details: passed ? 'Output matches expected result' : 'Output differs from expected result', expectedValue: expected, actualValue: result }; } async suggestImprovements(analysis: CodeAnalysis): Promise<Improvement[]> { return this.generateRecommendations(analysis); } }