swl-ses 3.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +425 -0
- package/_userland/agentes/.gitkeep +0 -0
- package/_userland/habilidades/.gitkeep +0 -0
- package/agentes/accesibilidad-wcag-swl.md +683 -0
- package/agentes/arquitecto-swl.md +210 -0
- package/agentes/auto-evolucion-swl.md +408 -0
- package/agentes/backend-api-swl.md +442 -0
- package/agentes/backend-node-swl.md +439 -0
- package/agentes/backend-python-swl.md +469 -0
- package/agentes/backend-workers-swl.md +444 -0
- package/agentes/cloud-infra-swl.md +466 -0
- package/agentes/consolidador-swl.md +487 -0
- package/agentes/datos-swl.md +568 -0
- package/agentes/depurador-swl.md +301 -0
- package/agentes/devops-ci-swl.md +352 -0
- package/agentes/disenador-ui-swl.md +546 -0
- package/agentes/documentador-swl.md +323 -0
- package/agentes/frontend-angular-swl.md +603 -0
- package/agentes/frontend-css-swl.md +700 -0
- package/agentes/frontend-react-swl.md +672 -0
- package/agentes/frontend-swl.md +483 -0
- package/agentes/frontend-tailwind-swl.md +808 -0
- package/agentes/implementador-swl.md +235 -0
- package/agentes/investigador-swl.md +274 -0
- package/agentes/investigador-ux-swl.md +482 -0
- package/agentes/migrador-swl.md +389 -0
- package/agentes/mobile-android-swl.md +473 -0
- package/agentes/mobile-cross-swl.md +501 -0
- package/agentes/mobile-ios-swl.md +464 -0
- package/agentes/notificador-swl.md +886 -0
- package/agentes/observabilidad-swl.md +408 -0
- package/agentes/orquestador-swl.md +490 -0
- package/agentes/planificador-swl.md +222 -0
- package/agentes/producto-prd-swl.md +565 -0
- package/agentes/release-manager-swl.md +545 -0
- package/agentes/rendimiento-swl.md +691 -0
- package/agentes/revisor-codigo-swl.md +254 -0
- package/agentes/revisor-seguridad-swl.md +316 -0
- package/agentes/tdd-qa-swl.md +323 -0
- package/agentes/ux-disenador-swl.md +498 -0
- package/bin/swl-ses.js +119 -0
- package/comandos/swl/actualizar.md +117 -0
- package/comandos/swl/aprender.md +348 -0
- package/comandos/swl/auditar-deps.md +390 -0
- package/comandos/swl/autoresearch.md +346 -0
- package/comandos/swl/checkpoint.md +296 -0
- package/comandos/swl/compactar.md +283 -0
- package/comandos/swl/crear-skill.md +609 -0
- package/comandos/swl/discutir-fase.md +230 -0
- package/comandos/swl/ejecutar-fase.md +302 -0
- package/comandos/swl/evolucionar.md +377 -0
- package/comandos/swl/instalar.md +220 -0
- package/comandos/swl/mapear-codebase.md +205 -0
- package/comandos/swl/nuevo-proyecto.md +154 -0
- package/comandos/swl/planear-fase.md +221 -0
- package/comandos/swl/release.md +405 -0
- package/comandos/swl/salud.md +382 -0
- package/comandos/swl/verificar.md +292 -0
- package/habilidades/accesibilidad-a11y/SKILL.md +584 -0
- package/habilidades/angular-avanzado/SKILL.md +491 -0
- package/habilidades/angular-moderno/SKILL.md +326 -0
- package/habilidades/api-rest-diseno/SKILL.md +302 -0
- package/habilidades/api-rest-diseno/recursos/openapi-template.yaml +506 -0
- package/habilidades/aprendizaje-continuo/SKILL.md +369 -0
- package/habilidades/async-python/SKILL.md +474 -0
- package/habilidades/auth-patrones/SKILL.md +488 -0
- package/habilidades/auto-evolucion-protocolo/SKILL.md +376 -0
- package/habilidades/autoresearch/SKILL.md +248 -0
- package/habilidades/autoresearch/recursos/checklist-template.md +191 -0
- package/habilidades/autoresearch/scripts/calcular-score.js +88 -0
- package/habilidades/checklist-calidad/SKILL.md +247 -0
- package/habilidades/checklist-calidad/recursos/quality-report-template.md +148 -0
- package/habilidades/checklist-seguridad/SKILL.md +224 -0
- package/habilidades/checkpoints-verificacion/SKILL.md +309 -0
- package/habilidades/checkpoints-verificacion/recursos/checkpoint-templates.md +360 -0
- package/habilidades/ci-cd-pipelines/SKILL.md +583 -0
- package/habilidades/ci-cd-pipelines/recursos/github-actions-template.yaml +403 -0
- package/habilidades/cloud-aws/SKILL.md +497 -0
- package/habilidades/compactacion-contexto/SKILL.md +201 -0
- package/habilidades/contenedores-docker/SKILL.md +453 -0
- package/habilidades/contenedores-docker/recursos/dockerfile-template.dockerfile +160 -0
- package/habilidades/css-moderno/SKILL.md +463 -0
- package/habilidades/datos-etl/SKILL.md +486 -0
- package/habilidades/dependencias-auditoria/SKILL.md +293 -0
- package/habilidades/deprecacion-migracion/SKILL.md +485 -0
- package/habilidades/design-tokens/SKILL.md +519 -0
- package/habilidades/discutir-fase/SKILL.md +167 -0
- package/habilidades/diseno-responsivo/SKILL.md +326 -0
- package/habilidades/django-experto/SKILL.md +395 -0
- package/habilidades/doc-sync/SKILL.md +259 -0
- package/habilidades/ejecutar-fase/SKILL.md +199 -0
- package/habilidades/estructura-proyecto-claude/SKILL.md +459 -0
- package/habilidades/estructura-proyecto-claude/recursos/claude-md-template.md +261 -0
- package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +213 -0
- package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +77 -0
- package/habilidades/estructura-proyecto-claude/recursos/variantes-por-stack.md +177 -0
- package/habilidades/event-driven/SKILL.md +580 -0
- package/habilidades/extractor-de-aprendizajes/SKILL.md +234 -0
- package/habilidades/fastapi-experto/SKILL.md +368 -0
- package/habilidades/frontend-avanzado/SKILL.md +555 -0
- package/habilidades/git-worktrees-paralelo/SKILL.md +246 -0
- package/habilidades/iam-secretos/SKILL.md +511 -0
- package/habilidades/instalar-sistema/SKILL.md +140 -0
- package/habilidades/kubernetes-orquestacion/SKILL.md +549 -0
- package/habilidades/manejo-errores/SKILL.md +512 -0
- package/habilidades/mapear-codebase/SKILL.md +199 -0
- package/habilidades/microservicios/SKILL.md +473 -0
- package/habilidades/mobile-flutter/SKILL.md +566 -0
- package/habilidades/mobile-react-native/SKILL.md +493 -0
- package/habilidades/monitoring-alertas/SKILL.md +447 -0
- package/habilidades/node-experto/SKILL.md +521 -0
- package/habilidades/notificaciones-multicanal/SKILL.md +448 -0
- package/habilidades/notificaciones-multicanal/recursos/config-template.json +115 -0
- package/habilidades/nuevo-proyecto/SKILL.md +183 -0
- package/habilidades/patrones-python/SKILL.md +381 -0
- package/habilidades/performance-baseline/SKILL.md +243 -0
- package/habilidades/planear-fase/SKILL.md +184 -0
- package/habilidades/postgresql-experto/SKILL.md +379 -0
- package/habilidades/react-experto/SKILL.md +434 -0
- package/habilidades/react-optimizacion/SKILL.md +328 -0
- package/habilidades/release-semver/SKILL.md +226 -0
- package/habilidades/release-semver/scripts/generar-changelog.sh +238 -0
- package/habilidades/sql-optimizacion/SKILL.md +314 -0
- package/habilidades/tailwind-experto/SKILL.md +412 -0
- package/habilidades/tdd-workflow/SKILL.md +267 -0
- package/habilidades/testing-python/SKILL.md +350 -0
- package/habilidades/threat-model-lite/SKILL.md +218 -0
- package/habilidades/typescript-avanzado/SKILL.md +454 -0
- package/habilidades/ux-diseno/SKILL.md +488 -0
- package/habilidades/validacion-ci-sistema/SKILL.md +543 -0
- package/habilidades/validacion-ci-sistema/scripts/validar-sistema.sh +286 -0
- package/habilidades/verificar-trabajo/SKILL.md +208 -0
- package/habilidades/wireframes-flujos/SKILL.md +396 -0
- package/habilidades/workflow-claude-code/SKILL.md +359 -0
- package/hooks/calidad-pre-commit.js +578 -0
- package/hooks/escaneo-secretos.js +302 -0
- package/hooks/extraccion-aprendizajes.js +550 -0
- package/hooks/linea-estado.js +249 -0
- package/hooks/monitor-contexto.js +230 -0
- package/hooks/proteccion-rutas.js +249 -0
- package/manifiestos/hooks-config.json +41 -0
- package/manifiestos/modulos.json +318 -0
- package/manifiestos/perfiles.json +189 -0
- package/package.json +45 -0
- package/plantillas/PROJECT.md +122 -0
- package/plantillas/REQUIREMENTS.md +132 -0
- package/plantillas/ROADMAP.md +143 -0
- package/plantillas/STATE.md +109 -0
- package/plantillas/research/ARCHITECTURE.md +220 -0
- package/plantillas/research/FEATURES.md +175 -0
- package/plantillas/research/PITFALLS.md +299 -0
- package/plantillas/research/STACK.md +233 -0
- package/plantillas/research/SUMMARY.md +165 -0
- package/plugin.json +144 -0
- package/reglas/accesibilidad.md +269 -0
- package/reglas/api-diseno.md +400 -0
- package/reglas/arquitectura.md +183 -0
- package/reglas/cloud-infra.md +247 -0
- package/reglas/docs.md +245 -0
- package/reglas/estilo-codigo.md +179 -0
- package/reglas/git-workflow.md +186 -0
- package/reglas/performance.md +195 -0
- package/reglas/pruebas.md +159 -0
- package/reglas/seguridad.md +151 -0
- package/reglas/skills-estandar.md +473 -0
- package/scripts/actualizar.js +51 -0
- package/scripts/desinstalar.js +86 -0
- package/scripts/doctor.js +222 -0
- package/scripts/inicializar.js +89 -0
- package/scripts/instalador.js +333 -0
- package/scripts/lib/detectar-runtime.js +177 -0
- package/scripts/lib/estado.js +112 -0
- package/scripts/lib/hooks-settings.js +283 -0
- package/scripts/lib/manifiestos.js +138 -0
- package/scripts/lib/seguridad.js +160 -0
- package/scripts/publicar.js +209 -0
- package/scripts/validar.js +120 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook: calidad-pre-commit.js
|
|
6
|
+
* Tipo: PreToolUse (aplica a: Bash — comandos git commit)
|
|
7
|
+
*
|
|
8
|
+
* Verifica calidad mínima del código staged ANTES de que Claude Code
|
|
9
|
+
* ejecute un `git commit`. Si detecta violaciones, bloquea el commit
|
|
10
|
+
* y lista los problemas encontrados con archivo y número de línea.
|
|
11
|
+
*
|
|
12
|
+
* Verificaciones por tipo de archivo:
|
|
13
|
+
*
|
|
14
|
+
* Todos los archivos:
|
|
15
|
+
* - Sin credenciales hardcodeadas (password=, secret=, api_key=, token=)
|
|
16
|
+
* - Sin archivos > 1 MB siendo commiteados
|
|
17
|
+
* - Sin TODO sin referencia a ticket/issue
|
|
18
|
+
*
|
|
19
|
+
* Archivos .py / .ts / .js:
|
|
20
|
+
* - Sin console.log() de debug (.ts / .js)
|
|
21
|
+
* - Sin print() de debug (.py) — excluye print dentro de if __name__
|
|
22
|
+
*
|
|
23
|
+
* Archivos .py:
|
|
24
|
+
* - Funciones públicas deben tener type hints en parámetros y retorno
|
|
25
|
+
*
|
|
26
|
+
* Resultado:
|
|
27
|
+
* - Violaciones encontradas → exit 2, JSON {result:"block", reason:"..."} en stdout
|
|
28
|
+
* - Sin violaciones → exit 0, sin output
|
|
29
|
+
* - Error interno → exit 0 (nunca bloquear por fallo del hook)
|
|
30
|
+
*
|
|
31
|
+
* NOTA: Este hook aplica solo cuando el comando Bash contiene "git commit".
|
|
32
|
+
* Para hooks de git nativos (.git/hooks/pre-commit), usar un script shell
|
|
33
|
+
* que llame a este archivo directamente.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const fs = require('fs');
|
|
37
|
+
const path = require('path');
|
|
38
|
+
const { execSync } = require('child_process');
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Constantes de configuración
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/** Tamaño máximo permitido por archivo a commitear (en bytes). */
|
|
45
|
+
const LIMITE_TAMANO_BYTES = 1024 * 1024; // 1 MB
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Patrones de credenciales hardcodeadas.
|
|
49
|
+
* Se evalúan línea por línea en todos los archivos de texto.
|
|
50
|
+
*/
|
|
51
|
+
const PATRONES_CREDENCIALES = [
|
|
52
|
+
{
|
|
53
|
+
nombre: 'password hardcodeado',
|
|
54
|
+
// password = "valor", password: "valor" — no captura placeholders vacíos
|
|
55
|
+
patron: /\bpassword\s*[=:]\s*["'][^"'\s]{4,}["']/i,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
nombre: 'secret hardcodeado',
|
|
59
|
+
patron: /\bsecret\s*[=:]\s*["'][^"'\s]{4,}["']/i,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
nombre: 'api_key hardcodeada',
|
|
63
|
+
patron: /\bapi_key\s*[=:]\s*["'][^"'\s]{4,}["']/i,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
nombre: 'token hardcodeado',
|
|
67
|
+
// "token" como variable independiente, no como parte de otra palabra
|
|
68
|
+
patron: /\btoken\s*[=:]\s*["'][^"'\s]{8,}["']/i,
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Marcadores que indican que un valor es un placeholder y no una credencial real.
|
|
74
|
+
* Si la línea contiene alguno de estos, se omite la alerta de credencial.
|
|
75
|
+
*/
|
|
76
|
+
const MARCADORES_PLACEHOLDER_CREDENCIAL = [
|
|
77
|
+
'YOUR_',
|
|
78
|
+
'your_',
|
|
79
|
+
'<YOUR',
|
|
80
|
+
'PLACEHOLDER',
|
|
81
|
+
'placeholder',
|
|
82
|
+
'example',
|
|
83
|
+
'fake_',
|
|
84
|
+
'dummy_',
|
|
85
|
+
'xxxxxxxx',
|
|
86
|
+
'os.environ',
|
|
87
|
+
'process.env',
|
|
88
|
+
'getenv(',
|
|
89
|
+
'# nosec',
|
|
90
|
+
'// nosec',
|
|
91
|
+
'pragma: allowlist secret',
|
|
92
|
+
'noqa',
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Extensiones de archivo que se consideran binarias y se omiten en análisis de texto.
|
|
97
|
+
*/
|
|
98
|
+
const EXTENSIONES_BINARIAS = new Set([
|
|
99
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.svg',
|
|
100
|
+
'.pdf', '.zip', '.tar', '.gz', '.rar', '.7z',
|
|
101
|
+
'.exe', '.dll', '.so', '.dylib', '.bin',
|
|
102
|
+
'.woff', '.woff2', '.ttf', '.otf', '.eot',
|
|
103
|
+
'.mp3', '.mp4', '.avi', '.mov', '.mkv',
|
|
104
|
+
'.pyc', '.pyo', '.pyd',
|
|
105
|
+
'.lock',
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Utilidades de sistema
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Obtiene la lista de archivos staged en el índice de git.
|
|
114
|
+
* Retorna un array de rutas relativas al repositorio.
|
|
115
|
+
*
|
|
116
|
+
* @returns {string[]}
|
|
117
|
+
*/
|
|
118
|
+
function obtenerArchivosStagedGit() {
|
|
119
|
+
try {
|
|
120
|
+
const salida = execSync('git diff --cached --name-only --diff-filter=ACM', {
|
|
121
|
+
encoding: 'utf8',
|
|
122
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
123
|
+
});
|
|
124
|
+
return salida
|
|
125
|
+
.split('\n')
|
|
126
|
+
.map(l => l.trim())
|
|
127
|
+
.filter(Boolean);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
// Si git no está disponible o no hay repositorio, retornar lista vacía
|
|
130
|
+
process.stderr.write(`[calidad-pre-commit] No se pudieron obtener archivos staged: ${err.message}\n`);
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Obtiene el contenido staged de un archivo (lo que realmente se commitea,
|
|
137
|
+
* no la versión en disco que puede tener cambios no staged).
|
|
138
|
+
*
|
|
139
|
+
* @param {string} rutaRelativa - Ruta relativa al repositorio.
|
|
140
|
+
* @returns {string|null} Contenido como string UTF-8, o null si falla/es binario.
|
|
141
|
+
*/
|
|
142
|
+
function obtenerContenidoStaged(rutaRelativa) {
|
|
143
|
+
try {
|
|
144
|
+
const contenido = execSync(`git show :${rutaRelativa}`, {
|
|
145
|
+
encoding: 'buffer',
|
|
146
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
147
|
+
});
|
|
148
|
+
// Detectar binario heurísticamente: byte nulo en los primeros 8KB
|
|
149
|
+
const muestra = contenido.slice(0, 8192);
|
|
150
|
+
if (muestra.includes(0)) return null;
|
|
151
|
+
return contenido.toString('utf8');
|
|
152
|
+
} catch (_) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Obtiene el tamaño en bytes del archivo staged.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} rutaRelativa
|
|
161
|
+
* @returns {number} Tamaño en bytes, o 0 si falla.
|
|
162
|
+
*/
|
|
163
|
+
function obtenerTamanoStaged(rutaRelativa) {
|
|
164
|
+
try {
|
|
165
|
+
const salida = execSync(`git cat-file -s :${rutaRelativa}`, {
|
|
166
|
+
encoding: 'utf8',
|
|
167
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
168
|
+
});
|
|
169
|
+
return parseInt(salida.trim(), 10) || 0;
|
|
170
|
+
} catch (_) {
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Verificaciones individuales
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Verifica si una línea tiene marcadores de placeholder para credenciales.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} linea
|
|
183
|
+
* @returns {boolean}
|
|
184
|
+
*/
|
|
185
|
+
function esPlaceholderCredencial(linea) {
|
|
186
|
+
return MARCADORES_PLACEHOLDER_CREDENCIAL.some(m => linea.includes(m));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Verifica que el archivo no supere el límite de tamaño.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} rutaRelativa
|
|
193
|
+
* @returns {{ ok: boolean, mensaje: string }}
|
|
194
|
+
*/
|
|
195
|
+
function verificarTamano(rutaRelativa) {
|
|
196
|
+
const tamano = obtenerTamanoStaged(rutaRelativa);
|
|
197
|
+
if (tamano > LIMITE_TAMANO_BYTES) {
|
|
198
|
+
const mb = (tamano / (1024 * 1024)).toFixed(2);
|
|
199
|
+
return {
|
|
200
|
+
ok: false,
|
|
201
|
+
mensaje: ` [TAMAÑO] ${rutaRelativa}: ${mb} MB supera el límite de 1 MB`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return { ok: true, mensaje: '' };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Verifica que no haya credenciales hardcodeadas en el contenido.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} rutaRelativa
|
|
211
|
+
* @param {string} contenido
|
|
212
|
+
* @returns {{ ok: boolean, mensajes: string[] }}
|
|
213
|
+
*/
|
|
214
|
+
function verificarCredenciales(rutaRelativa, contenido) {
|
|
215
|
+
const lineas = contenido.split('\n');
|
|
216
|
+
const mensajes = [];
|
|
217
|
+
|
|
218
|
+
for (let i = 0; i < lineas.length; i++) {
|
|
219
|
+
const linea = lineas[i];
|
|
220
|
+
|
|
221
|
+
// Saltar líneas comentadas con marcadores de exclusión
|
|
222
|
+
if (esPlaceholderCredencial(linea)) continue;
|
|
223
|
+
|
|
224
|
+
for (const { nombre, patron } of PATRONES_CREDENCIALES) {
|
|
225
|
+
if (patron.test(linea)) {
|
|
226
|
+
mensajes.push(
|
|
227
|
+
` [CREDENCIAL] ${rutaRelativa}:${i + 1} — ${nombre} detectado`
|
|
228
|
+
);
|
|
229
|
+
break; // Una alerta por línea es suficiente
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { ok: mensajes.length === 0, mensajes };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Verifica que no haya sentencias console.log() en archivos TypeScript o JavaScript.
|
|
239
|
+
* Excluye líneas comentadas con // o /* y archivos de test.
|
|
240
|
+
*
|
|
241
|
+
* @param {string} rutaRelativa
|
|
242
|
+
* @param {string} contenido
|
|
243
|
+
* @returns {{ ok: boolean, mensajes: string[] }}
|
|
244
|
+
*/
|
|
245
|
+
function verificarConsoleLog(rutaRelativa, contenido) {
|
|
246
|
+
// Excluir archivos de test y configuración donde console.log es legítimo
|
|
247
|
+
const esArchivoTest = /\.(test|spec)\.[jt]s$/.test(rutaRelativa) ||
|
|
248
|
+
/[/\\]tests?[/\\]/.test(rutaRelativa) ||
|
|
249
|
+
/[/\\]__tests__[/\\]/.test(rutaRelativa);
|
|
250
|
+
|
|
251
|
+
if (esArchivoTest) return { ok: true, mensajes: [] };
|
|
252
|
+
|
|
253
|
+
const lineas = contenido.split('\n');
|
|
254
|
+
const mensajes = [];
|
|
255
|
+
// Patrón: console.log, console.debug, console.info, console.warn que no sean
|
|
256
|
+
// parte de un comentario de línea completa
|
|
257
|
+
const patronLog = /^\s*console\.(log|debug|info|warn)\s*\(/;
|
|
258
|
+
const esComentario = /^\s*(\/\/|\/\*|\*)/;
|
|
259
|
+
|
|
260
|
+
for (let i = 0; i < lineas.length; i++) {
|
|
261
|
+
const linea = lineas[i];
|
|
262
|
+
if (esComentario.test(linea)) continue;
|
|
263
|
+
if (patronLog.test(linea)) {
|
|
264
|
+
mensajes.push(
|
|
265
|
+
` [DEBUG] ${rutaRelativa}:${i + 1} — console.log/debug en código no-test`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { ok: mensajes.length === 0, mensajes };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Verifica que no haya sentencias print() de debug en archivos Python.
|
|
275
|
+
* Reglas de exclusión:
|
|
276
|
+
* - Líneas comentadas con #
|
|
277
|
+
* - print() dentro de bloques if __name__ == '__main__'
|
|
278
|
+
* - Archivos de test
|
|
279
|
+
*
|
|
280
|
+
* @param {string} rutaRelativa
|
|
281
|
+
* @param {string} contenido
|
|
282
|
+
* @returns {{ ok: boolean, mensajes: string[] }}
|
|
283
|
+
*/
|
|
284
|
+
function verificarPrintPython(rutaRelativa, contenido) {
|
|
285
|
+
const esArchivoTest = /test.*\.py$/.test(rutaRelativa) ||
|
|
286
|
+
/[/\\]tests?[/\\]/.test(rutaRelativa);
|
|
287
|
+
|
|
288
|
+
if (esArchivoTest) return { ok: true, mensajes: [] };
|
|
289
|
+
|
|
290
|
+
const lineas = contenido.split('\n');
|
|
291
|
+
const mensajes = [];
|
|
292
|
+
// Patrón de print() al inicio de la sentencia (no como argumento de otra función)
|
|
293
|
+
const patronPrint = /^\s*print\s*\(/;
|
|
294
|
+
const esComentario = /^\s*#/;
|
|
295
|
+
// Detectar si estamos dentro de if __name__ == '__main__':
|
|
296
|
+
const patronMainBlock = /if\s+__name__\s*==\s*['"]__main__['"]\s*:/;
|
|
297
|
+
|
|
298
|
+
let dentroDeMainBlock = false;
|
|
299
|
+
let indentacionMain = -1;
|
|
300
|
+
|
|
301
|
+
for (let i = 0; i < lineas.length; i++) {
|
|
302
|
+
const linea = lineas[i];
|
|
303
|
+
|
|
304
|
+
// Detectar inicio del bloque __main__
|
|
305
|
+
if (patronMainBlock.test(linea)) {
|
|
306
|
+
dentroDeMainBlock = true;
|
|
307
|
+
// La indentación del bloque es la del if más 4 espacios (convención Python)
|
|
308
|
+
indentacionMain = (linea.match(/^(\s*)/)?.[1].length ?? 0) + 1;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Verificar si seguimos dentro del bloque __main__
|
|
313
|
+
if (dentroDeMainBlock) {
|
|
314
|
+
const indentacionLinea = linea.match(/^(\s*)/)?.[1].length ?? 0;
|
|
315
|
+
const esLineaVacia = linea.trim() === '';
|
|
316
|
+
// Salir del bloque si la indentación vuelve al nivel del if o menor
|
|
317
|
+
if (!esLineaVacia && indentacionLinea < indentacionMain) {
|
|
318
|
+
dentroDeMainBlock = false;
|
|
319
|
+
indentacionMain = -1;
|
|
320
|
+
} else {
|
|
321
|
+
continue; // Dentro del bloque main — permitir print()
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (esComentario.test(linea)) continue;
|
|
326
|
+
if (patronPrint.test(linea)) {
|
|
327
|
+
mensajes.push(
|
|
328
|
+
` [DEBUG] ${rutaRelativa}:${i + 1} — print() de debug en código de producción`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return { ok: mensajes.length === 0, mensajes };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Verifica que no haya TODO sin referencia a ticket o issue.
|
|
338
|
+
* Se considera válido si el TODO va seguido de una referencia como:
|
|
339
|
+
* TODO(#123), TODO: #456, TODO(GH-789), TODO: JIRA-123, TODO: https://...
|
|
340
|
+
*
|
|
341
|
+
* @param {string} rutaRelativa
|
|
342
|
+
* @param {string} contenido
|
|
343
|
+
* @returns {{ ok: boolean, mensajes: string[] }}
|
|
344
|
+
*/
|
|
345
|
+
function verificarTodosSinTicket(rutaRelativa, contenido) {
|
|
346
|
+
const lineas = contenido.split('\n');
|
|
347
|
+
const mensajes = [];
|
|
348
|
+
|
|
349
|
+
// Patrón que captura TODO sin referencia explícita
|
|
350
|
+
// Referencias válidas: #123, GH-123, JIRA-123, http...
|
|
351
|
+
const patronTodoSinTicket = /\bTODO\b(?!\s*[:(]\s*(?:#\d+|[A-Z]+-\d+|https?:\/\/))/i;
|
|
352
|
+
// Patrón de TODO con referencia (para excluir falsos positivos)
|
|
353
|
+
const patronTodoConTicket = /\bTODO\s*[:(]\s*(?:#\d+|[A-Z]+-\d+|https?:\/\/)/i;
|
|
354
|
+
|
|
355
|
+
for (let i = 0; i < lineas.length; i++) {
|
|
356
|
+
const linea = lineas[i];
|
|
357
|
+
// Si tiene TODO con ticket, es válido
|
|
358
|
+
if (patronTodoConTicket.test(linea)) continue;
|
|
359
|
+
// Si tiene TODO sin ticket, es una violación
|
|
360
|
+
if (patronTodoSinTicket.test(linea)) {
|
|
361
|
+
mensajes.push(
|
|
362
|
+
` [TODO] ${rutaRelativa}:${i + 1} — TODO sin referencia a ticket/issue`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return { ok: mensajes.length === 0, mensajes };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Verifica que las funciones públicas en Python tengan type hints.
|
|
372
|
+
* Una función "pública" es aquella cuyo nombre NO empieza con "_".
|
|
373
|
+
* Se verifica que los parámetros (excepto self/cls) y el retorno tengan anotaciones.
|
|
374
|
+
*
|
|
375
|
+
* Limitaciones intencionales (análisis estático ligero sin AST):
|
|
376
|
+
* - Solo detecta funciones de una sola línea de definición
|
|
377
|
+
* - No analiza funciones con firmas multi-línea (falso negativo aceptado)
|
|
378
|
+
* - Excluye archivos de test y migraciones
|
|
379
|
+
*
|
|
380
|
+
* @param {string} rutaRelativa
|
|
381
|
+
* @param {string} contenido
|
|
382
|
+
* @returns {{ ok: boolean, mensajes: string[] }}
|
|
383
|
+
*/
|
|
384
|
+
function verificarTypeHintsPython(rutaRelativa, contenido) {
|
|
385
|
+
const esArchivoExcluido =
|
|
386
|
+
/test.*\.py$/.test(rutaRelativa) ||
|
|
387
|
+
/[/\\]tests?[/\\]/.test(rutaRelativa) ||
|
|
388
|
+
/migrations?[/\\]/.test(rutaRelativa) ||
|
|
389
|
+
/conftest\.py$/.test(rutaRelativa) ||
|
|
390
|
+
/setup\.py$/.test(rutaRelativa);
|
|
391
|
+
|
|
392
|
+
if (esArchivoExcluido) return { ok: true, mensajes: [] };
|
|
393
|
+
|
|
394
|
+
const lineas = contenido.split('\n');
|
|
395
|
+
const mensajes = [];
|
|
396
|
+
|
|
397
|
+
// Patrón: def nombre_sin_guion_bajo(params): — función pública en una línea
|
|
398
|
+
// Captura: nombre de función y la lista de parámetros hasta el ':' o '->'
|
|
399
|
+
const patronDef = /^\s*def\s+([a-zA-Z][a-zA-Z0-9_]*)\s*\(([^)]*)\)\s*(->.*?)?:/;
|
|
400
|
+
|
|
401
|
+
for (let i = 0; i < lineas.length; i++) {
|
|
402
|
+
const linea = lineas[i];
|
|
403
|
+
const match = patronDef.exec(linea);
|
|
404
|
+
if (!match) continue;
|
|
405
|
+
|
|
406
|
+
const nombreFuncion = match[1];
|
|
407
|
+
const params = match[2];
|
|
408
|
+
const retorno = match[3] || '';
|
|
409
|
+
|
|
410
|
+
// Solo funciones públicas (sin prefijo _)
|
|
411
|
+
if (nombreFuncion.startsWith('_')) continue;
|
|
412
|
+
|
|
413
|
+
// Verificar type hints en parámetros
|
|
414
|
+
// Excluir self, cls, *args, **kwargs de la verificación
|
|
415
|
+
const paramsList = params
|
|
416
|
+
.split(',')
|
|
417
|
+
.map(p => p.trim())
|
|
418
|
+
.filter(p => p && p !== 'self' && p !== 'cls' && !p.startsWith('*'));
|
|
419
|
+
|
|
420
|
+
// Un parámetro tiene type hint si contiene ":"
|
|
421
|
+
const paramsSinHint = paramsList.filter(p => {
|
|
422
|
+
// Excluir parámetros con valor por defecto que ya tienen hint: "x: int = 0"
|
|
423
|
+
const baseParam = p.split('=')[0].trim();
|
|
424
|
+
return baseParam && !baseParam.includes(':');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (paramsSinHint.length > 0) {
|
|
428
|
+
mensajes.push(
|
|
429
|
+
` [TYPE HINT] ${rutaRelativa}:${i + 1} — def ${nombreFuncion}(): parámetros sin anotación: ${paramsSinHint.join(', ')}`
|
|
430
|
+
);
|
|
431
|
+
continue; // No duplicar alerta si también falta retorno
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Verificar type hint de retorno (-> tipo)
|
|
435
|
+
if (retorno.trim() === '') {
|
|
436
|
+
mensajes.push(
|
|
437
|
+
` [TYPE HINT] ${rutaRelativa}:${i + 1} — def ${nombreFuncion}(): falta anotación de retorno (->)`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return { ok: mensajes.length === 0, mensajes };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
// Verificador principal por archivo
|
|
447
|
+
// ---------------------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Ejecuta todas las verificaciones aplicables sobre un archivo staged.
|
|
451
|
+
*
|
|
452
|
+
* @param {string} rutaRelativa - Ruta relativa del archivo en el repositorio.
|
|
453
|
+
* @returns {string[]} Lista de mensajes de violación. Vacía si pasa todo.
|
|
454
|
+
*/
|
|
455
|
+
function verificarArchivo(rutaRelativa) {
|
|
456
|
+
const ext = path.extname(rutaRelativa).toLowerCase();
|
|
457
|
+
const violaciones = [];
|
|
458
|
+
|
|
459
|
+
// --- Verificación de tamaño (aplica a todos) ---
|
|
460
|
+
const { ok: tamanoOk, mensaje: mensajeTamano } = verificarTamano(rutaRelativa);
|
|
461
|
+
if (!tamanoOk) {
|
|
462
|
+
violaciones.push(mensajeTamano);
|
|
463
|
+
// Si el archivo es muy grande, no intentar leerlo para análisis de texto
|
|
464
|
+
return violaciones;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Omitir análisis de texto en archivos binarios conocidos
|
|
468
|
+
if (EXTENSIONES_BINARIAS.has(ext)) return violaciones;
|
|
469
|
+
|
|
470
|
+
// Obtener contenido staged para análisis de texto
|
|
471
|
+
const contenido = obtenerContenidoStaged(rutaRelativa);
|
|
472
|
+
if (contenido === null) return violaciones; // Binario detectado dinámicamente
|
|
473
|
+
|
|
474
|
+
// --- Verificación de credenciales (aplica a todos los archivos de texto) ---
|
|
475
|
+
const { mensajes: msgsCredencial } = verificarCredenciales(rutaRelativa, contenido);
|
|
476
|
+
violaciones.push(...msgsCredencial);
|
|
477
|
+
|
|
478
|
+
// --- Verificación de TODO sin ticket (aplica a todos los archivos de texto) ---
|
|
479
|
+
const { mensajes: msgsTodo } = verificarTodosSinTicket(rutaRelativa, contenido);
|
|
480
|
+
violaciones.push(...msgsTodo);
|
|
481
|
+
|
|
482
|
+
// --- Verificaciones por extensión ---
|
|
483
|
+
if (ext === '.ts' || ext === '.js') {
|
|
484
|
+
const { mensajes: msgsConsole } = verificarConsoleLog(rutaRelativa, contenido);
|
|
485
|
+
violaciones.push(...msgsConsole);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (ext === '.py') {
|
|
489
|
+
const { mensajes: msgsPrint } = verificarPrintPython(rutaRelativa, contenido);
|
|
490
|
+
violaciones.push(...msgsPrint);
|
|
491
|
+
|
|
492
|
+
const { mensajes: msgsHints } = verificarTypeHintsPython(rutaRelativa, contenido);
|
|
493
|
+
violaciones.push(...msgsHints);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return violaciones;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
// Entrypoint principal
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
|
|
503
|
+
let inputRaw = '';
|
|
504
|
+
|
|
505
|
+
process.stdin.on('data', chunk => {
|
|
506
|
+
inputRaw += chunk;
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
process.stdin.on('end', () => {
|
|
510
|
+
try {
|
|
511
|
+
const data = JSON.parse(inputRaw);
|
|
512
|
+
|
|
513
|
+
const toolName = String(data.tool_name || data.tool?.name || '');
|
|
514
|
+
const toolInput = data.tool_input || data.tool?.input || {};
|
|
515
|
+
const comando = String(toolInput.command || '');
|
|
516
|
+
|
|
517
|
+
// Este hook solo aplica cuando el comando Bash es un git commit
|
|
518
|
+
if (toolName !== 'Bash') {
|
|
519
|
+
process.exit(0);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Verificar que el comando incluye "git commit" (excluir --amend sin cambios,
|
|
523
|
+
// git commit --dry-run, etc. siguen pasando por las verificaciones)
|
|
524
|
+
if (!/\bgit\s+commit\b/.test(comando)) {
|
|
525
|
+
process.exit(0);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Obtener archivos staged
|
|
529
|
+
const archivos = obtenerArchivosStagedGit();
|
|
530
|
+
if (archivos.length === 0) {
|
|
531
|
+
// Sin archivos staged, nada que verificar
|
|
532
|
+
process.exit(0);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Verificar cada archivo
|
|
536
|
+
const todasLasViolaciones = [];
|
|
537
|
+
|
|
538
|
+
for (const archivo of archivos) {
|
|
539
|
+
const violaciones = verificarArchivo(archivo);
|
|
540
|
+
todasLasViolaciones.push(...violaciones);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (todasLasViolaciones.length > 0) {
|
|
544
|
+
const totalArchivos = archivos.length;
|
|
545
|
+
const totalViolaciones = todasLasViolaciones.length;
|
|
546
|
+
|
|
547
|
+
const razon = [
|
|
548
|
+
`Commit bloqueado: se encontraron ${totalViolaciones} violación(es) de calidad en ${totalArchivos} archivo(s) staged.`,
|
|
549
|
+
``,
|
|
550
|
+
`Problemas encontrados:`,
|
|
551
|
+
...todasLasViolaciones,
|
|
552
|
+
``,
|
|
553
|
+
`Acciones requeridas:`,
|
|
554
|
+
` [CREDENCIAL] → Mover el valor a una variable de entorno (.env no commiteado).`,
|
|
555
|
+
` [DEBUG] → Eliminar o comentar las sentencias de debug.`,
|
|
556
|
+
` [TODO] → Agregar referencia: TODO(#123) o TODO: JIRA-456.`,
|
|
557
|
+
` [TAMAÑO] → Usar git-lfs o excluir el archivo en .gitignore.`,
|
|
558
|
+
` [TYPE HINT] → Agregar anotaciones de tipo a la función Python.`,
|
|
559
|
+
``,
|
|
560
|
+
`Para omitir una verificación específica en una línea, usa:`,
|
|
561
|
+
` Python: # noqa: SWL001`,
|
|
562
|
+
` JS/TS: // nosec`,
|
|
563
|
+
` Credencial: # pragma: allowlist secret`,
|
|
564
|
+
].join('\n');
|
|
565
|
+
|
|
566
|
+
process.stdout.write(JSON.stringify({ result: 'block', reason: razon }));
|
|
567
|
+
process.exit(2);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Todas las verificaciones pasaron — permitir el commit
|
|
571
|
+
process.exit(0);
|
|
572
|
+
|
|
573
|
+
} catch (err) {
|
|
574
|
+
// El hook nunca bloquea por errores internos propios
|
|
575
|
+
process.stderr.write(`[calidad-pre-commit] Error interno: ${err.message}\n`);
|
|
576
|
+
process.exit(0);
|
|
577
|
+
}
|
|
578
|
+
});
|