386 lines
11 KiB
TypeScript
386 lines
11 KiB
TypeScript
#!/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<string, Violation[]>);
|
||
|
||
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();
|