- Configure workspace Git repository with comprehensive .gitignore - Add Odoo as submodule for ERP reference code - Include documentation: SETUP.md, GIT-STRUCTURE.md - Add gitignore templates for projects (backend, frontend, database) - Structure supports independent repos per project/subproject level Workspace includes: - core/ - Reusable patterns, modules, orchestration system - projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.) - knowledge-base/ - Reference code and patterns (includes Odoo submodule) - devtools/ - Development tools and templates - customers/ - Client implementations template 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
642 lines
21 KiB
TypeScript
642 lines
21 KiB
TypeScript
/**
|
||
* Validate Constants Usage (Detect Hardcoding - SSOT Violations)
|
||
*
|
||
* @description Script para detectar hardcoding de nombres y valores (violaciones SSOT)
|
||
* @usage npm run validate:constants
|
||
*
|
||
* @project GAMILIT
|
||
* @subagent SA-SCRIPTS-02
|
||
* @created 2025-11-02
|
||
*
|
||
* @see /docs-analysis/.../CONSTANTS-ARCHITECTURE.md
|
||
* @see /docs-analysis/.../POLITICA-CONSTANTS-SSOT.md
|
||
*/
|
||
|
||
import { readFileSync } from 'fs';
|
||
import { exec } from 'child_process';
|
||
import { promisify } from 'util';
|
||
import * as path from 'path';
|
||
|
||
const execAsync = promisify(exec);
|
||
|
||
/**
|
||
* Tipos de violaciones
|
||
*/
|
||
interface ViolationType {
|
||
file: string;
|
||
pattern: string;
|
||
message: string;
|
||
severity: 'P0' | 'P1' | 'P2';
|
||
matches: string[];
|
||
count: number;
|
||
lineNumbers?: number[];
|
||
suggestion?: string;
|
||
}
|
||
|
||
interface PatternConfig {
|
||
pattern: RegExp;
|
||
message: string;
|
||
severity: 'P0' | 'P1' | 'P2';
|
||
exclude: string[];
|
||
suggestion?: string;
|
||
}
|
||
|
||
/**
|
||
* Patrones a detectar (hardcoding)
|
||
* Agrupados por categoría y ordenados por severidad
|
||
*/
|
||
const PATTERNS_TO_DETECT: PatternConfig[] = [
|
||
// ========================================
|
||
// P0 - CRÍTICO: DATABASE SCHEMAS
|
||
// ========================================
|
||
{
|
||
pattern: /['"]auth_management['"]/g,
|
||
message: 'Hardcoded schema "auth_management"',
|
||
severity: 'P0',
|
||
exclude: ['database.constants.ts', '.sql', 'ddl/', 'migrations/', 'schema.constants.ts'],
|
||
suggestion: 'Usa DB_SCHEMAS.AUTH en su lugar',
|
||
},
|
||
{
|
||
pattern: /['"]gamification_system['"]/g,
|
||
message: 'Hardcoded schema "gamification_system"',
|
||
severity: 'P0',
|
||
exclude: ['database.constants.ts', '.sql', 'ddl/', 'migrations/', 'schema.constants.ts'],
|
||
suggestion: 'Usa DB_SCHEMAS.GAMIFICATION en su lugar',
|
||
},
|
||
{
|
||
pattern: /['"]educational_content['"]/g,
|
||
message: 'Hardcoded schema "educational_content"',
|
||
severity: 'P0',
|
||
exclude: ['database.constants.ts', '.sql', 'ddl/', 'migrations/', 'schema.constants.ts'],
|
||
suggestion: 'Usa DB_SCHEMAS.EDUCATIONAL en su lugar',
|
||
},
|
||
{
|
||
pattern: /['"]analytics_tracking['"]/g,
|
||
message: 'Hardcoded schema "analytics_tracking"',
|
||
severity: 'P0',
|
||
exclude: ['database.constants.ts', '.sql', 'ddl/', 'migrations/', 'schema.constants.ts'],
|
||
suggestion: 'Usa DB_SCHEMAS.ANALYTICS en su lugar',
|
||
},
|
||
{
|
||
pattern: /['"]public['"]\s*\.\s*['"]users['"]/g,
|
||
message: 'Hardcoded schema.table reference "public"."users"',
|
||
severity: 'P0',
|
||
exclude: ['database.constants.ts', '.sql', 'migrations/'],
|
||
suggestion: 'Usa DB_SCHEMAS y DB_TABLES constants',
|
||
},
|
||
|
||
// ========================================
|
||
// P0 - CRÍTICO: DATABASE TABLES
|
||
// ========================================
|
||
{
|
||
pattern: /['"]users['"](?!\s*[:}])/g,
|
||
message: 'Hardcoded table "users"',
|
||
severity: 'P0',
|
||
exclude: ['database.constants.ts', 'table.constants.ts', '.sql', 'test/', 'spec.ts', '__tests__/'],
|
||
suggestion: 'Usa DB_TABLES.AUTH.USERS en su lugar',
|
||
},
|
||
{
|
||
pattern: /['"]tenants['"](?!\s*[:}])/g,
|
||
message: 'Hardcoded table "tenants"',
|
||
severity: 'P0',
|
||
exclude: ['database.constants.ts', 'table.constants.ts', '.sql', 'test/', 'spec.ts', '__tests__/'],
|
||
suggestion: 'Usa DB_TABLES.AUTH.TENANTS en su lugar',
|
||
},
|
||
{
|
||
pattern: /['"]roles['"](?!\s*[:}])/g,
|
||
message: 'Hardcoded table "roles"',
|
||
severity: 'P0',
|
||
exclude: ['database.constants.ts', 'table.constants.ts', '.sql', 'test/', 'spec.ts', '__tests__/'],
|
||
suggestion: 'Usa DB_TABLES.AUTH.ROLES en su lugar',
|
||
},
|
||
{
|
||
pattern: /['"]permissions['"](?!\s*[:}])/g,
|
||
message: 'Hardcoded table "permissions"',
|
||
severity: 'P0',
|
||
exclude: ['database.constants.ts', 'table.constants.ts', '.sql', 'test/', 'spec.ts', '__tests__/'],
|
||
suggestion: 'Usa DB_TABLES.AUTH.PERMISSIONS en su lugar',
|
||
},
|
||
{
|
||
pattern: /['"]achievements['"](?!\s*[:}])/g,
|
||
message: 'Hardcoded table "achievements"',
|
||
severity: 'P0',
|
||
exclude: ['database.constants.ts', 'table.constants.ts', '.sql', 'test/', 'spec.ts', '__tests__/'],
|
||
suggestion: 'Usa DB_TABLES.GAMIFICATION.ACHIEVEMENTS en su lugar',
|
||
},
|
||
{
|
||
pattern: /['"]badges['"](?!\s*[:}])/g,
|
||
message: 'Hardcoded table "badges"',
|
||
severity: 'P0',
|
||
exclude: ['database.constants.ts', 'table.constants.ts', '.sql', 'test/', 'spec.ts', '__tests__/'],
|
||
suggestion: 'Usa DB_TABLES.GAMIFICATION.BADGES en su lugar',
|
||
},
|
||
{
|
||
pattern: /['"]user_progress['"](?!\s*[:}])/g,
|
||
message: 'Hardcoded table "user_progress"',
|
||
severity: 'P0',
|
||
exclude: ['database.constants.ts', 'table.constants.ts', '.sql', 'test/', 'spec.ts', '__tests__/'],
|
||
suggestion: 'Usa DB_TABLES.GAMIFICATION.USER_PROGRESS en su lugar',
|
||
},
|
||
|
||
// ========================================
|
||
// P0 - CRÍTICO: BACKEND CONTROLLERS
|
||
// ========================================
|
||
{
|
||
pattern: /@Controller\(\s*['"][^{]+['"]\s*\)/g,
|
||
message: 'Hardcoded @Controller() path',
|
||
severity: 'P0',
|
||
exclude: ['routes.constants.ts', 'api-routes.constants.ts'],
|
||
suggestion: 'Usa API_ROUTES constants en su lugar',
|
||
},
|
||
|
||
// ========================================
|
||
// P0 - CRÍTICO: FRONTEND API URLS
|
||
// ========================================
|
||
{
|
||
pattern: /fetch\(\s*['"]http:\/\/localhost:3000[^'"]+['"]/g,
|
||
message: 'Hardcoded localhost API URL in fetch()',
|
||
severity: 'P0',
|
||
exclude: ['api-endpoints.ts', 'api.constants.ts', 'test/', 'spec.ts', '__tests__/', '.spec.tsx'],
|
||
suggestion: 'Usa API_ENDPOINTS constants con baseURL del config',
|
||
},
|
||
{
|
||
pattern: /fetch\(\s*['"]http:\/\/localhost:4000[^'"]+['"]/g,
|
||
message: 'Hardcoded localhost API URL in fetch()',
|
||
severity: 'P0',
|
||
exclude: ['api-endpoints.ts', 'api.constants.ts', 'test/', 'spec.ts', '__tests__/', '.spec.tsx'],
|
||
suggestion: 'Usa API_ENDPOINTS constants con baseURL del config',
|
||
},
|
||
{
|
||
pattern: /axios\.(get|post|put|delete|patch)\(\s*['"]http[^'"]+['"]/g,
|
||
message: 'Hardcoded API URL in axios',
|
||
severity: 'P0',
|
||
exclude: ['api-endpoints.ts', 'api.constants.ts', 'test/', 'spec.ts', '__tests__/', '.spec.tsx'],
|
||
suggestion: 'Usa API_ENDPOINTS constants con axios instance configurado',
|
||
},
|
||
{
|
||
pattern: /fetch\(\s*['"]\/(api|auth|users|gamification|educational)[^'"]+['"]/g,
|
||
message: 'Hardcoded relative API path in fetch()',
|
||
severity: 'P0',
|
||
exclude: ['api-endpoints.ts', 'api.constants.ts', 'test/', 'spec.ts', '__tests__/', '.spec.tsx'],
|
||
suggestion: 'Usa API_ENDPOINTS constants en su lugar',
|
||
},
|
||
{
|
||
pattern: /axios\.(get|post|put|delete|patch)\(\s*['"]\/(api|auth|users)[^'"]+['"]/g,
|
||
message: 'Hardcoded relative API path in axios',
|
||
severity: 'P0',
|
||
exclude: ['api-endpoints.ts', 'api.constants.ts', 'test/', 'spec.ts', '__tests__/', '.spec.tsx'],
|
||
suggestion: 'Usa API_ENDPOINTS constants en su lugar',
|
||
},
|
||
|
||
// ========================================
|
||
// P1 - IMPORTANTE: ROUTE DECORATORS
|
||
// ========================================
|
||
{
|
||
pattern: /@(Get|Post|Put|Delete|Patch)\(\s*['"][a-z0-9/-]+['"]\s*\)/g,
|
||
message: 'Hardcoded route decorator path',
|
||
severity: 'P1',
|
||
exclude: ['routes.constants.ts', 'api-routes.constants.ts'],
|
||
suggestion: 'Considera usar API_ROUTES constants (aceptable para rutas internas simples)',
|
||
},
|
||
|
||
// ========================================
|
||
// P1 - IMPORTANTE: AUTH PROVIDERS
|
||
// ========================================
|
||
{
|
||
pattern: /['"]local['"].*provider/gi,
|
||
message: 'Hardcoded auth provider "local"',
|
||
severity: 'P1',
|
||
exclude: ['enums.constants.ts', 'auth-provider.constants.ts', 'test/', 'spec.ts'],
|
||
suggestion: 'Usa AuthProviderEnum.LOCAL',
|
||
},
|
||
{
|
||
pattern: /['"]google['"].*provider/gi,
|
||
message: 'Hardcoded auth provider "google"',
|
||
severity: 'P1',
|
||
exclude: ['enums.constants.ts', 'auth-provider.constants.ts', 'test/', 'spec.ts'],
|
||
suggestion: 'Usa AuthProviderEnum.GOOGLE',
|
||
},
|
||
{
|
||
pattern: /['"]github['"].*provider/gi,
|
||
message: 'Hardcoded auth provider "github"',
|
||
severity: 'P1',
|
||
exclude: ['enums.constants.ts', 'auth-provider.constants.ts', 'test/', 'spec.ts'],
|
||
suggestion: 'Usa AuthProviderEnum.GITHUB',
|
||
},
|
||
|
||
// ========================================
|
||
// P1 - IMPORTANTE: SUBSCRIPTION TIERS
|
||
// ========================================
|
||
{
|
||
pattern: /subscriptionTier\s*[=:]\s*['"]free['"]/gi,
|
||
message: 'Hardcoded subscription tier "free"',
|
||
severity: 'P1',
|
||
exclude: ['enums.constants.ts', 'subscription.constants.ts', 'test/', 'spec.ts'],
|
||
suggestion: 'Usa SubscriptionTierEnum.FREE',
|
||
},
|
||
{
|
||
pattern: /subscriptionTier\s*[=:]\s*['"]pro['"]/gi,
|
||
message: 'Hardcoded subscription tier "pro"',
|
||
severity: 'P1',
|
||
exclude: ['enums.constants.ts', 'subscription.constants.ts', 'test/', 'spec.ts'],
|
||
suggestion: 'Usa SubscriptionTierEnum.PRO',
|
||
},
|
||
{
|
||
pattern: /subscriptionTier\s*[=:]\s*['"]enterprise['"]/gi,
|
||
message: 'Hardcoded subscription tier "enterprise"',
|
||
severity: 'P1',
|
||
exclude: ['enums.constants.ts', 'subscription.constants.ts', 'test/', 'spec.ts'],
|
||
suggestion: 'Usa SubscriptionTierEnum.ENTERPRISE',
|
||
},
|
||
|
||
// ========================================
|
||
// P1 - IMPORTANTE: USER ROLES
|
||
// ========================================
|
||
{
|
||
pattern: /role\s*[=:]\s*['"]admin['"]/gi,
|
||
message: 'Hardcoded role "admin"',
|
||
severity: 'P1',
|
||
exclude: ['enums.constants.ts', 'role.constants.ts', 'test/', 'spec.ts', 'seed'],
|
||
suggestion: 'Usa UserRoleEnum.ADMIN',
|
||
},
|
||
{
|
||
pattern: /role\s*[=:]\s*['"]user['"]/gi,
|
||
message: 'Hardcoded role "user"',
|
||
severity: 'P1',
|
||
exclude: ['enums.constants.ts', 'role.constants.ts', 'test/', 'spec.ts', 'seed'],
|
||
suggestion: 'Usa UserRoleEnum.USER',
|
||
},
|
||
{
|
||
pattern: /role\s*[=:]\s*['"]teacher['"]/gi,
|
||
message: 'Hardcoded role "teacher"',
|
||
severity: 'P1',
|
||
exclude: ['enums.constants.ts', 'role.constants.ts', 'test/', 'spec.ts', 'seed'],
|
||
suggestion: 'Usa UserRoleEnum.TEACHER',
|
||
},
|
||
|
||
// ========================================
|
||
// P1 - IMPORTANTE: ENVIRONMENT VARIABLES
|
||
// ========================================
|
||
{
|
||
pattern: /process\.env\.[A-Z_]+(?!\s*\|\|)/g,
|
||
message: 'Direct process.env access without fallback',
|
||
severity: 'P1',
|
||
exclude: ['config/', 'env.constants.ts', '.config.ts', 'test/'],
|
||
suggestion: 'Usa ENV_CONFIG constants con validación y fallbacks',
|
||
},
|
||
|
||
// ========================================
|
||
// P2 - MENOR: HTTP STATUS CODES
|
||
// ========================================
|
||
{
|
||
pattern: /\.status\(\s*(200|201|204|400|401|403|404|500)\s*\)/g,
|
||
message: 'Hardcoded HTTP status code',
|
||
severity: 'P2',
|
||
exclude: ['http-status.constants.ts', 'test/', 'spec.ts'],
|
||
suggestion: 'Considera usar HttpStatus enum de NestJS o constants propias',
|
||
},
|
||
|
||
// ========================================
|
||
// P2 - MENOR: MIME TYPES
|
||
// ========================================
|
||
{
|
||
pattern: /['"]application\/json['"]/g,
|
||
message: 'Hardcoded MIME type "application/json"',
|
||
severity: 'P2',
|
||
exclude: ['mime.constants.ts', 'test/', 'spec.ts'],
|
||
suggestion: 'Considera usar MIME_TYPES constants',
|
||
},
|
||
];
|
||
|
||
/**
|
||
* Configuración de rutas a escanear
|
||
*/
|
||
const PATHS_TO_SCAN = [
|
||
'gamilit/projects/gamilit/apps/backend/src/**/*.ts',
|
||
'gamilit/projects/gamilit/apps/frontend/src/**/*.ts',
|
||
'gamilit/projects/gamilit/apps/frontend/src/**/*.tsx',
|
||
];
|
||
|
||
/**
|
||
* Archivos y directorios a excluir globalmente
|
||
*/
|
||
const GLOBAL_EXCLUDES = [
|
||
'node_modules',
|
||
'dist',
|
||
'build',
|
||
'.next',
|
||
'coverage',
|
||
'.git',
|
||
'*.min.js',
|
||
'*.bundle.js',
|
||
];
|
||
|
||
/**
|
||
* Extraer números de línea donde aparece el patrón
|
||
*/
|
||
function findLineNumbers(content: string, pattern: RegExp): number[] {
|
||
const lines = content.split('\n');
|
||
const lineNumbers: number[] = [];
|
||
|
||
lines.forEach((line, index) => {
|
||
if (pattern.test(line)) {
|
||
lineNumbers.push(index + 1); // Line numbers start at 1
|
||
}
|
||
// Reset regex lastIndex for next iteration
|
||
pattern.lastIndex = 0;
|
||
});
|
||
|
||
return lineNumbers;
|
||
}
|
||
|
||
/**
|
||
* Validar un archivo
|
||
*/
|
||
async function validateFile(filePath: string): Promise<ViolationType[]> {
|
||
let content: string;
|
||
|
||
try {
|
||
content = readFileSync(filePath, 'utf-8');
|
||
} catch (error) {
|
||
console.warn(`⚠️ No se pudo leer ${filePath}: ${error}`);
|
||
return [];
|
||
}
|
||
|
||
const violations: ViolationType[] = [];
|
||
|
||
for (const config of PATTERNS_TO_DETECT) {
|
||
const { pattern, message, severity, exclude, suggestion } = config;
|
||
|
||
// Skip if file is in exclude list
|
||
if (exclude && exclude.some((ex) => filePath.includes(ex))) {
|
||
continue;
|
||
}
|
||
|
||
// Skip if file is in global excludes
|
||
if (GLOBAL_EXCLUDES.some((ex) => filePath.includes(ex))) {
|
||
continue;
|
||
}
|
||
|
||
// Create a fresh regex to avoid lastIndex issues
|
||
const regexCopy = new RegExp(pattern.source, pattern.flags);
|
||
const matches = content.match(regexCopy);
|
||
|
||
if (matches && matches.length > 0) {
|
||
const lineNumbers = findLineNumbers(content, new RegExp(pattern.source, pattern.flags));
|
||
|
||
violations.push({
|
||
file: filePath,
|
||
pattern: pattern.toString(),
|
||
message,
|
||
severity,
|
||
matches: matches.slice(0, 5), // Primeros 5 matches
|
||
count: matches.length,
|
||
lineNumbers: lineNumbers.slice(0, 5), // Primeros 5 números de línea
|
||
suggestion,
|
||
});
|
||
}
|
||
}
|
||
|
||
return violations;
|
||
}
|
||
|
||
/**
|
||
* Expandir glob patterns a lista de archivos
|
||
*/
|
||
async function expandGlobPattern(pathPattern: string, basePath: string): Promise<string[]> {
|
||
try {
|
||
// Convertir el glob pattern a find command
|
||
const parts = pathPattern.split('/');
|
||
const lastPart = parts[parts.length - 1];
|
||
const dirPath = parts.slice(0, -1).join('/');
|
||
|
||
let findCmd: string;
|
||
|
||
if (lastPart === '**/*.ts') {
|
||
findCmd = `find ${basePath}/${dirPath} -type f -name "*.ts" 2>/dev/null`;
|
||
} else if (lastPart === '**/*.tsx') {
|
||
findCmd = `find ${basePath}/${dirPath} -type f -name "*.tsx" 2>/dev/null`;
|
||
} else {
|
||
findCmd = `find ${basePath}/${dirPath} -type f \\( -name "*.ts" -o -name "*.tsx" \\) 2>/dev/null`;
|
||
}
|
||
|
||
const { stdout } = await execAsync(findCmd);
|
||
const files = stdout
|
||
.trim()
|
||
.split('\n')
|
||
.filter((f) => {
|
||
if (!f) return false;
|
||
// Excluir node_modules y otros directorios globales
|
||
return !GLOBAL_EXCLUDES.some((ex) => f.includes(ex));
|
||
});
|
||
|
||
return files;
|
||
} catch (error) {
|
||
console.warn(`⚠️ Error al expandir patrón ${pathPattern}: ${error}`);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Generar reporte detallado
|
||
*/
|
||
function generateReport(violations: ViolationType[]): void {
|
||
// Agrupar por severidad
|
||
const p0Violations = violations.filter((v) => v.severity === 'P0');
|
||
const p1Violations = violations.filter((v) => v.severity === 'P1');
|
||
const p2Violations = violations.filter((v) => v.severity === 'P2');
|
||
|
||
console.log('\n' + '='.repeat(80));
|
||
console.log('📊 REPORTE DE VALIDACIÓN DE CONSTANTES (SSOT)');
|
||
console.log('='.repeat(80) + '\n');
|
||
|
||
if (violations.length === 0) {
|
||
console.log('✅ ¡EXCELENTE! No se encontraron violaciones de hardcoding.\n');
|
||
console.log(' Todas las constantes están correctamente utilizadas según SSOT.');
|
||
console.log(' El código cumple con la política de constants-first.\n');
|
||
return;
|
||
}
|
||
|
||
// P0 - Violaciones Críticas
|
||
if (p0Violations.length > 0) {
|
||
console.log(`❌ VIOLACIONES P0 (CRÍTICAS) - BLOQUEAN CI/CD: ${p0Violations.length}\n`);
|
||
console.log(' Estas violaciones DEBEN corregirse antes de hacer merge.\n');
|
||
|
||
p0Violations.forEach((v, index) => {
|
||
console.log(`${index + 1}. 📄 ${v.file}`);
|
||
console.log(` 🚨 ${v.message}`);
|
||
if (v.suggestion) {
|
||
console.log(` 💡 ${v.suggestion}`);
|
||
}
|
||
console.log(` 📍 Líneas: ${v.lineNumbers?.join(', ') || 'N/A'}`);
|
||
console.log(` 🔢 Total de ocurrencias: ${v.count}`);
|
||
console.log(` 📝 Ejemplos:`);
|
||
v.matches.slice(0, 3).forEach((m) => console.log(` - ${m}`));
|
||
console.log();
|
||
});
|
||
}
|
||
|
||
// P1 - Violaciones Importantes
|
||
if (p1Violations.length > 0) {
|
||
console.log(`⚠️ VIOLACIONES P1 (IMPORTANTES) - REVISAR: ${p1Violations.length}\n`);
|
||
console.log(' Estas violaciones deben revisarse y corregirse en la medida de lo posible.\n');
|
||
|
||
p1Violations.forEach((v, index) => {
|
||
console.log(`${index + 1}. 📄 ${v.file}`);
|
||
console.log(` ⚠️ ${v.message}`);
|
||
if (v.suggestion) {
|
||
console.log(` 💡 ${v.suggestion}`);
|
||
}
|
||
console.log(` 🔢 Ocurrencias: ${v.count}`);
|
||
console.log();
|
||
});
|
||
}
|
||
|
||
// P2 - Violaciones Menores
|
||
if (p2Violations.length > 0) {
|
||
console.log(`ℹ️ VIOLACIONES P2 (MENORES) - INFORMATIVO: ${p2Violations.length}\n`);
|
||
console.log(' Estas violaciones son informativas. Corrígelas cuando sea conveniente.\n');
|
||
|
||
// Solo mostrar resumen para P2
|
||
const p2ByType = p2Violations.reduce((acc, v) => {
|
||
acc[v.message] = (acc[v.message] || 0) + 1;
|
||
return acc;
|
||
}, {} as Record<string, number>);
|
||
|
||
Object.entries(p2ByType).forEach(([message, count]) => {
|
||
console.log(` - ${message}: ${count} archivos`);
|
||
});
|
||
console.log();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Generar resumen final
|
||
*/
|
||
function generateSummary(violations: ViolationType[]): void {
|
||
const p0Count = violations.filter((v) => v.severity === 'P0').length;
|
||
const p1Count = violations.filter((v) => v.severity === 'P1').length;
|
||
const p2Count = violations.filter((v) => v.severity === 'P2').length;
|
||
|
||
console.log('='.repeat(80));
|
||
console.log('📈 RESUMEN:');
|
||
console.log('='.repeat(80));
|
||
console.log(` P0 (Críticas): ${p0Count} violaciones`);
|
||
console.log(` P1 (Importantes): ${p1Count} violaciones`);
|
||
console.log(` P2 (Menores): ${p2Count} violaciones`);
|
||
console.log(` ────────────────────────────────`);
|
||
console.log(` TOTAL: ${violations.length} violaciones`);
|
||
console.log('='.repeat(80) + '\n');
|
||
}
|
||
|
||
/**
|
||
* Generar instrucciones de corrección
|
||
*/
|
||
function generateInstructions(violations: ViolationType[]): void {
|
||
const p0Count = violations.filter((v) => v.severity === 'P0').length;
|
||
const p1Count = violations.filter((v) => v.severity === 'P1').length;
|
||
|
||
console.log('📋 PRÓXIMOS PASOS:\n');
|
||
|
||
if (p0Count > 0) {
|
||
console.log('1. ❌ URGENTE: Corregir TODAS las violaciones P0 (críticas)');
|
||
console.log(' → Estas bloquean el pipeline de CI/CD');
|
||
console.log(' → Reemplazar hardcoded values por constants del SSOT\n');
|
||
}
|
||
|
||
if (p1Count > 0) {
|
||
console.log('2. ⚠️ Revisar y corregir violaciones P1 (importantes)');
|
||
console.log(' → Priorizar antes del merge a main\n');
|
||
}
|
||
|
||
console.log('3. ✅ Re-ejecutar validación:');
|
||
console.log(' → npm run validate:constants\n');
|
||
|
||
console.log('4. 📚 Consultar documentación:');
|
||
console.log(' → docs-analysis/.../CONSTANTS-ARCHITECTURE.md');
|
||
console.log(' → docs-analysis/.../POLITICA-CONSTANTS-SSOT.md\n');
|
||
|
||
console.log('5. 🔍 Ver ubicación de constants:');
|
||
console.log(' → apps/backend/src/shared/constants/');
|
||
console.log(' → apps/frontend/src/shared/constants/\n');
|
||
}
|
||
|
||
/**
|
||
* Determinar exit code
|
||
*/
|
||
function determineExitCode(violations: ViolationType[]): number {
|
||
const p0Count = violations.filter((v) => v.severity === 'P0').length;
|
||
const p1Count = violations.filter((v) => v.severity === 'P1').length;
|
||
|
||
if (p0Count > 0) {
|
||
console.error('❌ FALLÓ: Existen violaciones P0 que bloquean el CI/CD.\n');
|
||
return 1;
|
||
}
|
||
|
||
if (p1Count > 5) {
|
||
console.warn('⚠️ ADVERTENCIA: Demasiadas violaciones P1 (>5).\n');
|
||
console.warn(' Se recomienda corregir antes de merge.\n');
|
||
return 1;
|
||
}
|
||
|
||
if (violations.length === 0) {
|
||
console.log('✅ ÉXITO: No se encontraron violaciones.\n');
|
||
} else {
|
||
console.log('✅ PASÓ: No hay violaciones críticas (solo P1/P2 menores).\n');
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* Main
|
||
*/
|
||
async function main() {
|
||
console.log('🔍 Validando uso de constantes (detectando hardcoding SSOT)...\n');
|
||
console.log(`📅 Fecha: ${new Date().toISOString()}`);
|
||
console.log(`🤖 Subagente: SA-SCRIPTS-02`);
|
||
console.log(`📦 Proyecto: GAMILIT\n`);
|
||
|
||
const basePath = '/home/isem/workspace/workspace-gamilit';
|
||
let allViolations: ViolationType[] = [];
|
||
let totalFilesScanned = 0;
|
||
|
||
// Escanear cada path pattern
|
||
for (const pathPattern of PATHS_TO_SCAN) {
|
||
const files = await expandGlobPattern(pathPattern, basePath);
|
||
|
||
if (files.length === 0) {
|
||
console.log(`⚠️ No se encontraron archivos para: ${pathPattern}`);
|
||
continue;
|
||
}
|
||
|
||
console.log(`📂 Escaneando ${files.length} archivos en: ${pathPattern}`);
|
||
totalFilesScanned += files.length;
|
||
|
||
for (const file of files) {
|
||
const violations = await validateFile(file);
|
||
allViolations = allViolations.concat(violations);
|
||
}
|
||
}
|
||
|
||
console.log(`\n✅ Escaneados ${totalFilesScanned} archivos en total`);
|
||
console.log(`🔍 Patrones de detección: ${PATTERNS_TO_DETECT.length}`);
|
||
console.log(`📊 Violaciones encontradas: ${allViolations.length}\n`);
|
||
|
||
// Generar reportes
|
||
generateReport(allViolations);
|
||
generateSummary(allViolations);
|
||
generateInstructions(allViolations);
|
||
|
||
// Determinar exit code
|
||
const exitCode = determineExitCode(allViolations);
|
||
process.exit(exitCode);
|
||
}
|
||
|
||
// Execute
|
||
main().catch((error) => {
|
||
console.error('❌ Error fatal durante la validación:', error);
|
||
console.error('\n Stack trace:', error.stack);
|
||
process.exit(1);
|
||
});
|