UNPKG

@invisiblecities/sidequest-cqo

Version:

Configuration-agnostic TypeScript and ESLint orchestrator with real-time watch mode, SQLite persistence, and intelligent terminal detection

369 lines 15.3 kB
/** * Violation Tracker Service for Code Quality Orchestrator * Manages violation lifecycle and deduplication */ import { generateViolationHash } from "../database/utils.js"; // ============================================================================ // Violation Tracker Implementation // ============================================================================ export class ViolationTracker { storageService; validationCache = new Map(); hashCache = new Map(); silent = false; constructor(storageService) { this.storageService = storageService; } setSilentMode(silent) { this.silent = silent; } // ======================================================================== // Violation Processing // ======================================================================== async processViolations(violations) { const startTime = performance.now(); if (!this.silent) { console.log(`[ViolationTracker] Processing ${violations.length} violations...`); } // Completely silent in silent mode - no debug logs during watch // Step 1: Deduplicate violations const deduplicated = this.deduplicateViolations(violations); const deduplicatedCount = violations.length - deduplicated.length; // Step 2: Validate violations const validated = []; const errors = []; for (const violation of deduplicated) { const validationResult = this.validateViolation(violation); if (validationResult.isValid) { validated.push(this.sanitizeViolation(violation)); } else { errors.push(`Invalid violation in ${violation.file}:${violation.line} - ${validationResult.errors.join(", ")}`); } } // Step 3: Store validated violations let inserted = 0; let updated = 0; if (validated.length > 0) { if (!this.silent) { console.log(`[ViolationTracker] Storing ${validated.length} validated violations to database`); } try { const storeResult = await this.storageService.storeViolations(validated); inserted = storeResult.inserted; updated = storeResult.updated; errors.push(...storeResult.errors); if (!this.silent) { console.log(`[ViolationTracker] Storage result: inserted=${inserted}, updated=${updated}, errors=${storeResult.errors.length}`); } } catch (error) { errors.push(`Storage operation failed: ${error}`); if (!this.silent) { console.warn("[ViolationTracker] Storage operation failed:", error); } } } else { if (!this.silent) { console.log(`[ViolationTracker] No validated violations to store (original=${violations.length}, deduplicated=${deduplicated.length}, validated=${validated.length})`); } } const executionTime = performance.now() - startTime; // Record performance metric try { await this.storageService.recordPerformanceMetric("violation_processing", executionTime, "ms", `processed: ${violations.length}, validated: ${validated.length}`); } catch (error) { // Don't fail the entire operation if metrics recording fails if (!this.silent) { console.warn("[ViolationTracker] Failed to record performance metric:", error); } } const result = { processed: violations.length, inserted, updated, deduplicated: deduplicatedCount, errors, }; if (!this.silent) { console.log(`[ViolationTracker] Processing completed in ${Math.round(executionTime)}ms:`, result); } return result; } deduplicateViolations(violations) { const seenHashes = new Set(); const deduplicated = []; for (const violation of violations) { const hash = this.generateViolationHash(violation); if (!seenHashes.has(hash)) { seenHashes.add(hash); deduplicated.push(violation); } } return deduplicated; } // ======================================================================== // Lifecycle Management // ======================================================================== async markAsResolved(violationHashes) { console.log(`[ViolationTracker] Marking ${violationHashes.length} violations as resolved`); return await this.storageService.resolveViolations(violationHashes); } markAsIgnored(violationHashes) { console.log(`[ViolationTracker] Marking ${violationHashes.length} violations as ignored`); // Note: StorageService doesn't have markAsIgnored method yet, // this would need to be implemented similarly to resolveViolations // For now, we'll log the operation console.log("markAsIgnored operation logged - implementation needed in StorageService"); return Promise.resolve(violationHashes.length); } reactivateViolations(violationHashes) { console.log(`[ViolationTracker] Reactivating ${violationHashes.length} violations`); // Note: StorageService doesn't have reactivateViolations method yet // This would need to be implemented to set status back to 'active' console.log("reactivateViolations operation logged - implementation needed in StorageService"); return Promise.resolve(violationHashes.length); } // ======================================================================== // Hash Management // ======================================================================== generateViolationHash(violation) { // Create cache key for this violation const cacheKey = `${violation.file}:${violation.line}:${violation.rule}:${violation.message}`; // Check cache first if (this.hashCache.has(cacheKey)) { return this.hashCache.get(cacheKey); } // Use the utility function to generate hash const hash = generateViolationHash({ file_path: violation.file, line_number: violation.line || undefined, rule_id: violation.rule || undefined, message: violation.message || "", }); // Cache the result this.hashCache.set(cacheKey, hash); return hash; } validateViolationHash(violation, hash) { const computedHash = this.generateViolationHash(violation); return computedHash === hash; } // ======================================================================== // Filtering and Querying // ======================================================================== filterViolationsByRule(violations, ruleIds) { const ruleSet = new Set(ruleIds); return violations.filter((violation) => violation.rule && ruleSet.has(violation.rule)); } filterViolationsBySeverity(violations, severities) { const severitySet = new Set(severities); return violations.filter((violation) => severitySet.has(violation.severity)); } filterViolationsByFile(violations, filePaths) { const fileSet = new Set(filePaths); return violations.filter((violation) => fileSet.has(violation.file)); } // ======================================================================== // Validation // ======================================================================== validateViolation(violation) { // Create cache key const cacheKey = `${violation.file}:${violation.line}:${violation.message}`; // Check cache first if (this.validationCache.has(cacheKey)) { return this.validationCache.get(cacheKey); } const errors = []; const warnings = []; // Required field validation if (!violation.file || typeof violation.file !== "string" || violation.file.trim().length === 0) { errors.push("File path is required"); } if (!violation.message || typeof violation.message !== "string" || violation.message.trim().length === 0) { errors.push("Message is required"); } if (!violation.category || typeof violation.category !== "string" || violation.category.trim().length === 0) { errors.push("Category is required"); } if (!violation.severity || typeof violation.severity !== "string" || violation.severity.trim().length === 0) { errors.push("Severity is required"); } if (!violation.source || typeof violation.source !== "string" || violation.source.trim().length === 0) { errors.push("Source is required"); } // Field format validation if (violation.line !== undefined && (violation.line < 1 || !Number.isInteger(violation.line))) { errors.push("Line number must be a positive integer"); } if (violation.column !== undefined && (violation.column < 0 || !Number.isInteger(violation.column))) { errors.push("Column number must be a non-negative integer"); } // Severity validation const validSeverities = ["error", "warn", "info"]; if (violation.severity && !validSeverities.includes(violation.severity)) { errors.push(`Severity must be one of: ${validSeverities.join(", ")}`); } // Source validation const validSources = ["typescript", "eslint"]; if (violation.source && !validSources.includes(violation.source)) { warnings.push(`Unusual source '${violation.source}' - expected one of: ${validSources.join(", ")}`); } // File path validation if (violation.file && !/\.(ts|tsx|js|jsx)$/.test(violation.file)) { warnings.push(`File '${violation.file}' does not have a typical TypeScript/JavaScript extension`); } // Message length validation if (violation.message && violation.message.length > 500) { warnings.push("Message is unusually long (>500 characters)"); } const result = { isValid: errors.length === 0, errors, warnings, }; // Cache the result this.validationCache.set(cacheKey, result); return result; } sanitizeViolation(violation) { const result = { file: violation.file?.trim() || "", message: violation.message?.trim() || "", category: (violation.category?.trim() || "unknown"), severity: (violation.severity?.trim() || "info"), source: (violation.source?.trim() || "unknown"), }; if (violation.line !== undefined) { result.line = violation.line; } if (violation.column !== undefined) { result.column = violation.column; } if (violation.rule !== undefined) { result.rule = violation.rule.trim() || undefined; } if (violation.code !== undefined) { result.code = violation.code.trim() || undefined; } return result; } // ======================================================================== // Cache Management // ======================================================================== /** * Clear internal caches to free memory */ clearCaches() { this.validationCache.clear(); this.hashCache.clear(); console.log("[ViolationTracker] Caches cleared"); } /** * Get cache statistics for monitoring */ getCacheStats() { return { validationCacheSize: this.validationCache.size, hashCacheSize: this.hashCache.size, totalCacheSize: this.validationCache.size + this.hashCache.size, }; } // ======================================================================== // Batch Operations // ======================================================================== /** * Process violations in batches for better performance */ async processBatchedViolations(violations, batchSize = 100) { const results = []; for (let index = 0; index < violations.length; index += batchSize) { const batch = violations.slice(index, index + batchSize); const result = await this.processViolations(batch); results.push(result); } return results; } /** * Get aggregated statistics from batch processing results */ aggregateBatchResults(results) { const total = { processed: 0, inserted: 0, updated: 0, deduplicated: 0, errors: [], }; for (const result of results) { total.processed += result.processed; total.inserted += result.inserted; total.updated += result.updated; total.deduplicated += result.deduplicated; total.errors.push(...result.errors); } return total; } // ======================================================================== // Advanced Filtering // ======================================================================== /** * Apply multiple filters to violations */ applyFilters(violations, filters) { let filtered = violations; if (filters.ruleIds) { filtered = this.filterViolationsByRule(filtered, filters.ruleIds); } if (filters.severities) { filtered = this.filterViolationsBySeverity(filtered, filters.severities); } if (filters.filePaths) { filtered = this.filterViolationsByFile(filtered, filters.filePaths); } if (filters.categories) { filtered = filtered.filter((v) => filters.categories.includes(v.category)); } if (filters.sources) { filtered = filtered.filter((v) => filters.sources.includes(v.source)); } return filtered; } } // ============================================================================ // Service Factory // ============================================================================ let violationTrackerInstance; /** * Get or create violation tracker instance */ export function getViolationTracker(storageService) { if (!violationTrackerInstance) { violationTrackerInstance = new ViolationTracker(storageService); } return violationTrackerInstance; } /** * Reset violation tracker instance (useful for testing) */ export function resetViolationTracker() { if (violationTrackerInstance) { violationTrackerInstance.clearCaches(); } violationTrackerInstance = undefined; } //# sourceMappingURL=violation-tracker.js.map