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.
Files changed (177) hide show
  1. package/CLAUDE.md +425 -0
  2. package/_userland/agentes/.gitkeep +0 -0
  3. package/_userland/habilidades/.gitkeep +0 -0
  4. package/agentes/accesibilidad-wcag-swl.md +683 -0
  5. package/agentes/arquitecto-swl.md +210 -0
  6. package/agentes/auto-evolucion-swl.md +408 -0
  7. package/agentes/backend-api-swl.md +442 -0
  8. package/agentes/backend-node-swl.md +439 -0
  9. package/agentes/backend-python-swl.md +469 -0
  10. package/agentes/backend-workers-swl.md +444 -0
  11. package/agentes/cloud-infra-swl.md +466 -0
  12. package/agentes/consolidador-swl.md +487 -0
  13. package/agentes/datos-swl.md +568 -0
  14. package/agentes/depurador-swl.md +301 -0
  15. package/agentes/devops-ci-swl.md +352 -0
  16. package/agentes/disenador-ui-swl.md +546 -0
  17. package/agentes/documentador-swl.md +323 -0
  18. package/agentes/frontend-angular-swl.md +603 -0
  19. package/agentes/frontend-css-swl.md +700 -0
  20. package/agentes/frontend-react-swl.md +672 -0
  21. package/agentes/frontend-swl.md +483 -0
  22. package/agentes/frontend-tailwind-swl.md +808 -0
  23. package/agentes/implementador-swl.md +235 -0
  24. package/agentes/investigador-swl.md +274 -0
  25. package/agentes/investigador-ux-swl.md +482 -0
  26. package/agentes/migrador-swl.md +389 -0
  27. package/agentes/mobile-android-swl.md +473 -0
  28. package/agentes/mobile-cross-swl.md +501 -0
  29. package/agentes/mobile-ios-swl.md +464 -0
  30. package/agentes/notificador-swl.md +886 -0
  31. package/agentes/observabilidad-swl.md +408 -0
  32. package/agentes/orquestador-swl.md +490 -0
  33. package/agentes/planificador-swl.md +222 -0
  34. package/agentes/producto-prd-swl.md +565 -0
  35. package/agentes/release-manager-swl.md +545 -0
  36. package/agentes/rendimiento-swl.md +691 -0
  37. package/agentes/revisor-codigo-swl.md +254 -0
  38. package/agentes/revisor-seguridad-swl.md +316 -0
  39. package/agentes/tdd-qa-swl.md +323 -0
  40. package/agentes/ux-disenador-swl.md +498 -0
  41. package/bin/swl-ses.js +119 -0
  42. package/comandos/swl/actualizar.md +117 -0
  43. package/comandos/swl/aprender.md +348 -0
  44. package/comandos/swl/auditar-deps.md +390 -0
  45. package/comandos/swl/autoresearch.md +346 -0
  46. package/comandos/swl/checkpoint.md +296 -0
  47. package/comandos/swl/compactar.md +283 -0
  48. package/comandos/swl/crear-skill.md +609 -0
  49. package/comandos/swl/discutir-fase.md +230 -0
  50. package/comandos/swl/ejecutar-fase.md +302 -0
  51. package/comandos/swl/evolucionar.md +377 -0
  52. package/comandos/swl/instalar.md +220 -0
  53. package/comandos/swl/mapear-codebase.md +205 -0
  54. package/comandos/swl/nuevo-proyecto.md +154 -0
  55. package/comandos/swl/planear-fase.md +221 -0
  56. package/comandos/swl/release.md +405 -0
  57. package/comandos/swl/salud.md +382 -0
  58. package/comandos/swl/verificar.md +292 -0
  59. package/habilidades/accesibilidad-a11y/SKILL.md +584 -0
  60. package/habilidades/angular-avanzado/SKILL.md +491 -0
  61. package/habilidades/angular-moderno/SKILL.md +326 -0
  62. package/habilidades/api-rest-diseno/SKILL.md +302 -0
  63. package/habilidades/api-rest-diseno/recursos/openapi-template.yaml +506 -0
  64. package/habilidades/aprendizaje-continuo/SKILL.md +369 -0
  65. package/habilidades/async-python/SKILL.md +474 -0
  66. package/habilidades/auth-patrones/SKILL.md +488 -0
  67. package/habilidades/auto-evolucion-protocolo/SKILL.md +376 -0
  68. package/habilidades/autoresearch/SKILL.md +248 -0
  69. package/habilidades/autoresearch/recursos/checklist-template.md +191 -0
  70. package/habilidades/autoresearch/scripts/calcular-score.js +88 -0
  71. package/habilidades/checklist-calidad/SKILL.md +247 -0
  72. package/habilidades/checklist-calidad/recursos/quality-report-template.md +148 -0
  73. package/habilidades/checklist-seguridad/SKILL.md +224 -0
  74. package/habilidades/checkpoints-verificacion/SKILL.md +309 -0
  75. package/habilidades/checkpoints-verificacion/recursos/checkpoint-templates.md +360 -0
  76. package/habilidades/ci-cd-pipelines/SKILL.md +583 -0
  77. package/habilidades/ci-cd-pipelines/recursos/github-actions-template.yaml +403 -0
  78. package/habilidades/cloud-aws/SKILL.md +497 -0
  79. package/habilidades/compactacion-contexto/SKILL.md +201 -0
  80. package/habilidades/contenedores-docker/SKILL.md +453 -0
  81. package/habilidades/contenedores-docker/recursos/dockerfile-template.dockerfile +160 -0
  82. package/habilidades/css-moderno/SKILL.md +463 -0
  83. package/habilidades/datos-etl/SKILL.md +486 -0
  84. package/habilidades/dependencias-auditoria/SKILL.md +293 -0
  85. package/habilidades/deprecacion-migracion/SKILL.md +485 -0
  86. package/habilidades/design-tokens/SKILL.md +519 -0
  87. package/habilidades/discutir-fase/SKILL.md +167 -0
  88. package/habilidades/diseno-responsivo/SKILL.md +326 -0
  89. package/habilidades/django-experto/SKILL.md +395 -0
  90. package/habilidades/doc-sync/SKILL.md +259 -0
  91. package/habilidades/ejecutar-fase/SKILL.md +199 -0
  92. package/habilidades/estructura-proyecto-claude/SKILL.md +459 -0
  93. package/habilidades/estructura-proyecto-claude/recursos/claude-md-template.md +261 -0
  94. package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +213 -0
  95. package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +77 -0
  96. package/habilidades/estructura-proyecto-claude/recursos/variantes-por-stack.md +177 -0
  97. package/habilidades/event-driven/SKILL.md +580 -0
  98. package/habilidades/extractor-de-aprendizajes/SKILL.md +234 -0
  99. package/habilidades/fastapi-experto/SKILL.md +368 -0
  100. package/habilidades/frontend-avanzado/SKILL.md +555 -0
  101. package/habilidades/git-worktrees-paralelo/SKILL.md +246 -0
  102. package/habilidades/iam-secretos/SKILL.md +511 -0
  103. package/habilidades/instalar-sistema/SKILL.md +140 -0
  104. package/habilidades/kubernetes-orquestacion/SKILL.md +549 -0
  105. package/habilidades/manejo-errores/SKILL.md +512 -0
  106. package/habilidades/mapear-codebase/SKILL.md +199 -0
  107. package/habilidades/microservicios/SKILL.md +473 -0
  108. package/habilidades/mobile-flutter/SKILL.md +566 -0
  109. package/habilidades/mobile-react-native/SKILL.md +493 -0
  110. package/habilidades/monitoring-alertas/SKILL.md +447 -0
  111. package/habilidades/node-experto/SKILL.md +521 -0
  112. package/habilidades/notificaciones-multicanal/SKILL.md +448 -0
  113. package/habilidades/notificaciones-multicanal/recursos/config-template.json +115 -0
  114. package/habilidades/nuevo-proyecto/SKILL.md +183 -0
  115. package/habilidades/patrones-python/SKILL.md +381 -0
  116. package/habilidades/performance-baseline/SKILL.md +243 -0
  117. package/habilidades/planear-fase/SKILL.md +184 -0
  118. package/habilidades/postgresql-experto/SKILL.md +379 -0
  119. package/habilidades/react-experto/SKILL.md +434 -0
  120. package/habilidades/react-optimizacion/SKILL.md +328 -0
  121. package/habilidades/release-semver/SKILL.md +226 -0
  122. package/habilidades/release-semver/scripts/generar-changelog.sh +238 -0
  123. package/habilidades/sql-optimizacion/SKILL.md +314 -0
  124. package/habilidades/tailwind-experto/SKILL.md +412 -0
  125. package/habilidades/tdd-workflow/SKILL.md +267 -0
  126. package/habilidades/testing-python/SKILL.md +350 -0
  127. package/habilidades/threat-model-lite/SKILL.md +218 -0
  128. package/habilidades/typescript-avanzado/SKILL.md +454 -0
  129. package/habilidades/ux-diseno/SKILL.md +488 -0
  130. package/habilidades/validacion-ci-sistema/SKILL.md +543 -0
  131. package/habilidades/validacion-ci-sistema/scripts/validar-sistema.sh +286 -0
  132. package/habilidades/verificar-trabajo/SKILL.md +208 -0
  133. package/habilidades/wireframes-flujos/SKILL.md +396 -0
  134. package/habilidades/workflow-claude-code/SKILL.md +359 -0
  135. package/hooks/calidad-pre-commit.js +578 -0
  136. package/hooks/escaneo-secretos.js +302 -0
  137. package/hooks/extraccion-aprendizajes.js +550 -0
  138. package/hooks/linea-estado.js +249 -0
  139. package/hooks/monitor-contexto.js +230 -0
  140. package/hooks/proteccion-rutas.js +249 -0
  141. package/manifiestos/hooks-config.json +41 -0
  142. package/manifiestos/modulos.json +318 -0
  143. package/manifiestos/perfiles.json +189 -0
  144. package/package.json +45 -0
  145. package/plantillas/PROJECT.md +122 -0
  146. package/plantillas/REQUIREMENTS.md +132 -0
  147. package/plantillas/ROADMAP.md +143 -0
  148. package/plantillas/STATE.md +109 -0
  149. package/plantillas/research/ARCHITECTURE.md +220 -0
  150. package/plantillas/research/FEATURES.md +175 -0
  151. package/plantillas/research/PITFALLS.md +299 -0
  152. package/plantillas/research/STACK.md +233 -0
  153. package/plantillas/research/SUMMARY.md +165 -0
  154. package/plugin.json +144 -0
  155. package/reglas/accesibilidad.md +269 -0
  156. package/reglas/api-diseno.md +400 -0
  157. package/reglas/arquitectura.md +183 -0
  158. package/reglas/cloud-infra.md +247 -0
  159. package/reglas/docs.md +245 -0
  160. package/reglas/estilo-codigo.md +179 -0
  161. package/reglas/git-workflow.md +186 -0
  162. package/reglas/performance.md +195 -0
  163. package/reglas/pruebas.md +159 -0
  164. package/reglas/seguridad.md +151 -0
  165. package/reglas/skills-estandar.md +473 -0
  166. package/scripts/actualizar.js +51 -0
  167. package/scripts/desinstalar.js +86 -0
  168. package/scripts/doctor.js +222 -0
  169. package/scripts/inicializar.js +89 -0
  170. package/scripts/instalador.js +333 -0
  171. package/scripts/lib/detectar-runtime.js +177 -0
  172. package/scripts/lib/estado.js +112 -0
  173. package/scripts/lib/hooks-settings.js +283 -0
  174. package/scripts/lib/manifiestos.js +138 -0
  175. package/scripts/lib/seguridad.js +160 -0
  176. package/scripts/publicar.js +209 -0
  177. 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
+ });