erp-construccion-backend-v2/scripts/validate-constants-usage.ts
rckrdmrd 7c1480a819 Migración desde erp-construccion/backend - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:11:14 -06:00

386 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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();