#!/usr/bin/env ts-node /** * Validate Constants Usage - SSOT Enforcement * * Este script detecta hardcoding de schemas, tablas, rutas API y enums * que deberian estar usando las constantes centralizadas del SSOT. * * Ejecutar: npm run validate:constants * * @author Architecture-Analyst * @date 2025-12-12 */ import * as fs from 'fs'; import * as path from 'path'; // ============================================================================= // CONFIGURACION // ============================================================================= interface ValidationPattern { pattern: RegExp; message: string; severity: 'P0' | 'P1' | 'P2'; suggestion: string; exclude?: RegExp[]; } const PATTERNS: ValidationPattern[] = [ // Database Schemas { pattern: /['"`]auth['"`](?!\s*:)/g, message: 'Hardcoded schema "auth"', severity: 'P0', suggestion: 'Usa DB_SCHEMAS.AUTH', exclude: [/from\s+['"`]\.\/database\.constants['"`]/], }, { pattern: /['"`]construction['"`](?!\s*:)/g, message: 'Hardcoded schema "construction"', severity: 'P0', suggestion: 'Usa DB_SCHEMAS.CONSTRUCTION', }, { pattern: /['"`]hr['"`](?!\s*:)(?!\.entity)/g, message: 'Hardcoded schema "hr"', severity: 'P0', suggestion: 'Usa DB_SCHEMAS.HR', }, { pattern: /['"`]hse['"`](?!\s*:)(?!\/)/g, message: 'Hardcoded schema "hse"', severity: 'P0', suggestion: 'Usa DB_SCHEMAS.HSE', }, { pattern: /['"`]estimates['"`](?!\s*:)/g, message: 'Hardcoded schema "estimates"', severity: 'P0', suggestion: 'Usa DB_SCHEMAS.ESTIMATES', }, { pattern: /['"`]infonavit['"`](?!\s*:)/g, message: 'Hardcoded schema "infonavit"', severity: 'P0', suggestion: 'Usa DB_SCHEMAS.INFONAVIT', }, { pattern: /['"`]inventory['"`](?!\s*:)/g, message: 'Hardcoded schema "inventory"', severity: 'P0', suggestion: 'Usa DB_SCHEMAS.INVENTORY', }, { pattern: /['"`]purchase['"`](?!\s*:)/g, message: 'Hardcoded schema "purchase"', severity: 'P0', suggestion: 'Usa DB_SCHEMAS.PURCHASE', }, // API Routes { pattern: /['"`]\/api\/v1\/proyectos['"`]/g, message: 'Hardcoded API route "/api/v1/proyectos"', severity: 'P0', suggestion: 'Usa API_ROUTES.PROYECTOS.BASE', }, { pattern: /['"`]\/api\/v1\/fraccionamientos['"`]/g, message: 'Hardcoded API route "/api/v1/fraccionamientos"', severity: 'P0', suggestion: 'Usa API_ROUTES.FRACCIONAMIENTOS.BASE', }, { pattern: /['"`]\/api\/v1\/employees['"`]/g, message: 'Hardcoded API route "/api/v1/employees"', severity: 'P0', suggestion: 'Usa API_ROUTES.EMPLOYEES.BASE', }, { pattern: /['"`]\/api\/v1\/incidentes['"`]/g, message: 'Hardcoded API route "/api/v1/incidentes"', severity: 'P0', suggestion: 'Usa API_ROUTES.INCIDENTES.BASE', }, // Common Table Names { pattern: /FROM\s+proyectos(?!\s+AS|\s+WHERE)/gi, message: 'Hardcoded table name "proyectos"', severity: 'P1', suggestion: 'Usa DB_TABLES.CONSTRUCTION.PROYECTOS', }, { pattern: /FROM\s+fraccionamientos(?!\s+AS|\s+WHERE)/gi, message: 'Hardcoded table name "fraccionamientos"', severity: 'P1', suggestion: 'Usa DB_TABLES.CONSTRUCTION.FRACCIONAMIENTOS', }, { pattern: /FROM\s+employees(?!\s+AS|\s+WHERE)/gi, message: 'Hardcoded table name "employees"', severity: 'P1', suggestion: 'Usa DB_TABLES.HR.EMPLOYEES', }, { pattern: /FROM\s+incidentes(?!\s+AS|\s+WHERE)/gi, message: 'Hardcoded table name "incidentes"', severity: 'P1', suggestion: 'Usa DB_TABLES.HSE.INCIDENTES', }, // Status Values { pattern: /status\s*===?\s*['"`]active['"`]/gi, message: 'Hardcoded status "active"', severity: 'P1', suggestion: 'Usa PROJECT_STATUS.ACTIVE o USER_STATUS.ACTIVE', }, { pattern: /status\s*===?\s*['"`]borrador['"`]/gi, message: 'Hardcoded status "borrador"', severity: 'P1', suggestion: 'Usa BUDGET_STATUS.DRAFT o ESTIMATION_STATUS.DRAFT', }, { pattern: /status\s*===?\s*['"`]aprobado['"`]/gi, message: 'Hardcoded status "aprobado"', severity: 'P1', suggestion: 'Usa BUDGET_STATUS.APPROVED o ESTIMATION_STATUS.APPROVED', }, // Role Names { pattern: /role\s*===?\s*['"`]admin['"`]/gi, message: 'Hardcoded role "admin"', severity: 'P0', suggestion: 'Usa ROLES.ADMIN', }, { pattern: /role\s*===?\s*['"`]supervisor['"`]/gi, message: 'Hardcoded role "supervisor"', severity: 'P1', suggestion: 'Usa ROLES.SUPERVISOR_OBRA o ROLES.SUPERVISOR_HSE', }, ]; // Archivos a excluir const EXCLUDED_PATHS = [ 'node_modules', 'dist', '.git', 'coverage', 'database.constants.ts', 'api.constants.ts', 'enums.constants.ts', 'index.ts', '.sql', '.md', '.json', '.yml', '.yaml', ]; // Extensiones a validar const VALID_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx']; // ============================================================================= // TIPOS // ============================================================================= interface Violation { file: string; line: number; column: number; pattern: string; message: string; severity: 'P0' | 'P1' | 'P2'; suggestion: string; context: string; } // ============================================================================= // FUNCIONES // ============================================================================= function shouldExclude(filePath: string): boolean { return EXCLUDED_PATHS.some(excluded => filePath.includes(excluded)); } function hasValidExtension(filePath: string): boolean { return VALID_EXTENSIONS.some(ext => filePath.endsWith(ext)); } function getFiles(dir: string): string[] { const files: string[] = []; if (!fs.existsSync(dir)) { return files; } const items = fs.readdirSync(dir); for (const item of items) { const fullPath = path.join(dir, item); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { if (!shouldExclude(fullPath)) { files.push(...getFiles(fullPath)); } } else if (stat.isFile() && hasValidExtension(fullPath) && !shouldExclude(fullPath)) { files.push(fullPath); } } return files; } function findViolations(filePath: string, content: string, patterns: ValidationPattern[]): Violation[] { const violations: Violation[] = []; const lines = content.split('\n'); for (const patternConfig of patterns) { let match: RegExpExecArray | null; const regex = new RegExp(patternConfig.pattern.source, patternConfig.pattern.flags); while ((match = regex.exec(content)) !== null) { // Check exclusions if (patternConfig.exclude) { const shouldSkip = patternConfig.exclude.some(excludePattern => excludePattern.test(content) ); if (shouldSkip) continue; } // Find line number const beforeMatch = content.substring(0, match.index); const lineNumber = beforeMatch.split('\n').length; const lineStart = beforeMatch.lastIndexOf('\n') + 1; const column = match.index - lineStart + 1; violations.push({ file: filePath, line: lineNumber, column, pattern: match[0], message: patternConfig.message, severity: patternConfig.severity, suggestion: patternConfig.suggestion, context: lines[lineNumber - 1]?.trim() || '', }); } } return violations; } function formatViolation(v: Violation): string { const severityColor = { P0: '\x1b[31m', // Red P1: '\x1b[33m', // Yellow P2: '\x1b[36m', // Cyan }; const reset = '\x1b[0m'; return ` ${severityColor[v.severity]}[${v.severity}]${reset} ${v.message} File: ${v.file}:${v.line}:${v.column} Found: "${v.pattern}" Context: ${v.context} Suggestion: ${v.suggestion} `; } function generateReport(violations: Violation[]): void { const p0 = violations.filter(v => v.severity === 'P0'); const p1 = violations.filter(v => v.severity === 'P1'); const p2 = violations.filter(v => v.severity === 'P2'); console.log('\n========================================'); console.log('SSOT VALIDATION REPORT'); console.log('========================================\n'); console.log(`Total Violations: ${violations.length}`); console.log(` P0 (Critical): ${p0.length}`); console.log(` P1 (High): ${p1.length}`); console.log(` P2 (Medium): ${p2.length}`); if (violations.length > 0) { console.log('\n----------------------------------------'); console.log('VIOLATIONS FOUND:'); console.log('----------------------------------------'); // Group by file const byFile = violations.reduce((acc, v) => { if (!acc[v.file]) acc[v.file] = []; acc[v.file].push(v); return acc; }, {} as Record); for (const [file, fileViolations] of Object.entries(byFile)) { console.log(`\nšŸ“ ${file}`); for (const v of fileViolations) { console.log(formatViolation(v)); } } } console.log('\n========================================'); if (p0.length > 0) { console.log('\nāŒ FAILED: P0 violations found. Fix before merging.\n'); process.exit(1); } else if (violations.length > 0) { console.log('\nāš ļø WARNING: Non-critical violations found. Consider fixing.\n'); process.exit(0); } else { console.log('\nāœ… PASSED: No SSOT violations found!\n'); process.exit(0); } } // ============================================================================= // MAIN // ============================================================================= function main(): void { const backendDir = path.resolve(__dirname, '../src'); const frontendDir = path.resolve(__dirname, '../../frontend/web/src'); console.log('šŸ” Validating SSOT constants usage...\n'); console.log(`Backend: ${backendDir}`); console.log(`Frontend: ${frontendDir}`); const allViolations: Violation[] = []; // Scan backend if (fs.existsSync(backendDir)) { const backendFiles = getFiles(backendDir); console.log(`\nScanning ${backendFiles.length} backend files...`); for (const file of backendFiles) { const content = fs.readFileSync(file, 'utf-8'); const violations = findViolations(file, content, PATTERNS); allViolations.push(...violations); } } // Scan frontend if (fs.existsSync(frontendDir)) { const frontendFiles = getFiles(frontendDir); console.log(`Scanning ${frontendFiles.length} frontend files...`); for (const file of frontendFiles) { const content = fs.readFileSync(file, 'utf-8'); const violations = findViolations(file, content, PATTERNS); allViolations.push(...violations); } } generateReport(allViolations); } main();