@saulwade/swl-ses 1.0.1 → 1.1.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 (113) hide show
  1. package/CLAUDE.md +8 -5
  2. package/README.md +3 -3
  3. package/agentes/accesibilidad-wcag-swl.md +5 -7
  4. package/agentes/arquitecto-swl.md +5 -3
  5. package/agentes/auto-evolucion-swl.md +42 -12
  6. package/agentes/backend-api-swl.md +5 -3
  7. package/agentes/backend-csharp-swl.md +5 -3
  8. package/agentes/backend-go-swl.md +5 -3
  9. package/agentes/backend-java-swl.md +5 -3
  10. package/agentes/backend-node-swl.md +5 -3
  11. package/agentes/backend-python-swl.md +5 -3
  12. package/agentes/backend-rust-swl.md +5 -3
  13. package/agentes/backend-workers-swl.md +5 -3
  14. package/agentes/cloud-infra-swl.md +5 -6
  15. package/agentes/consolidador-swl.md +5 -3
  16. package/agentes/datos-swl.md +5 -7
  17. package/agentes/depurador-swl.md +6 -3
  18. package/agentes/devops-ci-swl.md +5 -3
  19. package/agentes/disenador-ui-swl.md +5 -7
  20. package/agentes/documentador-swl.md +5 -3
  21. package/agentes/frontend-angular-swl.md +5 -11
  22. package/agentes/frontend-css-swl.md +5 -9
  23. package/agentes/frontend-react-swl.md +5 -9
  24. package/agentes/frontend-swl.md +5 -9
  25. package/agentes/frontend-tailwind-swl.md +5 -9
  26. package/agentes/implementador-swl.md +6 -3
  27. package/agentes/investigador-swl.md +5 -3
  28. package/agentes/investigador-ux-swl.md +5 -9
  29. package/agentes/llm-apps-swl.md +5 -3
  30. package/agentes/migrador-swl.md +6 -3
  31. package/agentes/mobile-android-swl.md +5 -3
  32. package/agentes/mobile-cross-swl.md +5 -3
  33. package/agentes/mobile-ios-swl.md +5 -3
  34. package/agentes/mobile-testing-swl.md +5 -3
  35. package/agentes/notificador-swl.md +5 -3
  36. package/agentes/observabilidad-swl.md +5 -3
  37. package/agentes/orquestador-swl.md +29 -8
  38. package/agentes/pagos-swl.md +5 -3
  39. package/agentes/perfilador-usuario-swl.md +4 -2
  40. package/agentes/planificador-swl.md +5 -3
  41. package/agentes/producto-prd-swl.md +5 -3
  42. package/agentes/red-team-swl.md +4 -2
  43. package/agentes/release-manager-swl.md +6 -8
  44. package/agentes/rendimiento-swl.md +5 -6
  45. package/agentes/resolutor-build-swl.md +5 -3
  46. package/agentes/revisor-angular-swl.md +5 -3
  47. package/agentes/revisor-codigo-swl.md +90 -4
  48. package/agentes/revisor-csharp-swl.md +5 -3
  49. package/agentes/revisor-go-swl.md +5 -3
  50. package/agentes/revisor-java-swl.md +5 -3
  51. package/agentes/revisor-kotlin-swl.md +5 -3
  52. package/agentes/revisor-nextjs-swl.md +5 -3
  53. package/agentes/revisor-php-swl.md +5 -3
  54. package/agentes/revisor-react-swl.md +5 -3
  55. package/agentes/revisor-rust-swl.md +5 -3
  56. package/agentes/revisor-seguridad-swl.md +5 -3
  57. package/agentes/revisor-swift-swl.md +5 -3
  58. package/agentes/revisor-typescript-swl.md +5 -3
  59. package/agentes/sre-swl.md +5 -3
  60. package/agentes/tdd-qa-swl.md +5 -3
  61. package/agentes/ux-disenador-swl.md +5 -9
  62. package/comandos/swl/evaluar-skill.md +18 -0
  63. package/comandos/swl/evolucion-estado.md +49 -0
  64. package/comandos/swl/release.md +77 -1
  65. package/comandos/swl/salud.md +23 -0
  66. package/habilidades/checklist-seguridad/SKILL.md +57 -1
  67. package/habilidades/extractor-de-aprendizajes/SKILL.md +15 -5
  68. package/habilidades/fastapi-experto/SKILL.md +10 -1
  69. package/habilidades/manejo-errores/.evolved.json +8 -8
  70. package/habilidades/manejo-errores/SKILL.md +63 -4
  71. package/habilidades/patrones-python/SKILL.md +5 -4
  72. package/habilidades/release-semver/.evolved.json +8 -8
  73. package/habilidades/release-semver/SKILL.md +85 -1
  74. package/hooks/auto-evolucion.js +35 -1
  75. package/hooks/clasificador-mensajes.js +50 -3
  76. package/hooks/lib/agent-routing.js +107 -0
  77. package/hooks/lib/delegation-tracker.js +162 -44
  78. package/hooks/lib/evolution-tracker.js +12 -3
  79. package/hooks/lib/memory-search.js +59 -1
  80. package/hooks/lib/nudge-tracker.js +10 -1
  81. package/hooks/lib/provenance-tracker.js +11 -3
  82. package/hooks/lib/text-similarity.js +241 -0
  83. package/hooks/metricas-evolucion.js +168 -1
  84. package/hooks/monitor-contexto.js +54 -6
  85. package/hooks/preservar-estado-pre-compact.js +11 -1
  86. package/hooks/risk-scoring.js +10 -1
  87. package/hooks/tracking-costos.js +10 -1
  88. package/hooks/validar-formato-post-subagente.js +140 -0
  89. package/hooks/validar-memoria-hook.js +218 -0
  90. package/manifiestos/agent-output-schemas.json +57 -0
  91. package/manifiestos/hooks-config.json +18 -0
  92. package/manifiestos/modulos.json +3 -0
  93. package/manifiestos/skills-lock.json +1065 -0
  94. package/package.json +1 -1
  95. package/plugin.json +1 -1
  96. package/reglas/arquitectura.md +20 -0
  97. package/reglas/fragmentos-compartidos.md +152 -0
  98. package/reglas/gobernanza.md +10 -1
  99. package/reglas/seguridad-agentes.md +12 -0
  100. package/reglas/skills-estandar.md +19 -0
  101. package/schemas/agent-frontmatter.schema.json +18 -0
  102. package/scripts/auditar-agentes-gaps.js +9 -1
  103. package/scripts/auditar-cobertura-frameworks.js +9 -1
  104. package/scripts/auditar-skills-gaps.js +9 -1
  105. package/scripts/bootstrap-instintos.js +11 -1
  106. package/scripts/generar-inventario.js +112 -9
  107. package/scripts/generar-matriz-lenguajes.js +271 -0
  108. package/scripts/generar-skills-lock.js +190 -0
  109. package/scripts/lib/estado.js +12 -2
  110. package/scripts/lib/gitignore-manifest.js +32 -2
  111. package/scripts/migrar-csv-a-array.js +168 -0
  112. package/scripts/migrar-fase-dominio.js +201 -0
  113. package/scripts/publicar.js +88 -18
@@ -5,15 +5,15 @@ description: >
5
5
  generics, utility types, module patterns y testing. Detecta uso de any, type assertions
6
6
  inseguras, tipos duplicados y generics innecesarios. Invocar para revisión de
7
7
  código TypeScript puro, librerías, o backends Node.js.
8
- tools: Read, Grep, Glob, Bash
8
+ tools: [Read, Grep, Glob, Bash]
9
9
  model: claude-sonnet-4-6
10
10
  modeloAlterno: claude-haiku-4-5-20251001
11
11
  ventanaContexto: 200k
12
12
  color: blue
13
13
  version: 1.0.0
14
14
  nivelRiesgo: BAJO
15
- skillsInvocables: typescript-avanzado, node-experto, checklist-calidad, tdd-workflow
16
- skillsRestringidos: ninguno
15
+ skillsInvocables: [typescript-avanzado, node-experto, checklist-calidad, tdd-workflow]
16
+ skillsRestringidos: []
17
17
  permisosRed: false
18
18
  permisosEscritura: true
19
19
  permisosComandos: true
@@ -22,6 +22,8 @@ toolBudget:
22
22
  standard: 20
23
23
  complex: 35
24
24
  evolvable: true # nivelRiesgo=BAJO
25
+ fase: verify
26
+ dominio: quality
25
27
  exclusiones:
26
28
  - "No invocar para implementar código TypeScript — este agente solo revisa; la implementación corresponde a frontend-*-swl o backend-node-swl."
27
29
  - "No invocar para revisar lenguajes distintos a TypeScript/JavaScript — usar el revisor especializado correspondiente."
@@ -7,7 +7,7 @@ description: >
7
7
  redacte un post-mortem, se planifique chaos testing, o se necesite
8
8
  cuantificar la confiabilidad de un sistema. Complementa a observabilidad-swl
9
9
  (que implementa métricas) con el framework de confiabilidad.
10
- tools: Read, Write, Edit, Bash, Grep, Glob, Skill
10
+ tools: [Read, Write, Edit, Bash, Grep, Glob, Skill]
11
11
  model: claude-opus-4-7
12
12
  modeloAlterno: claude-sonnet-4-6
13
13
  ventanaContexto: 200k
@@ -15,8 +15,8 @@ permissionMode: acceptEdits
15
15
  color: red
16
16
  version: 1.0.0
17
17
  nivelRiesgo: ALTO
18
- skillsInvocables: sre-patrones, performance-baseline, monitoring-alertas, checklist-seguridad
19
- skillsRestringidos: frontend-css-swl, mobile-flutter
18
+ skillsInvocables: [sre-patrones, performance-baseline, monitoring-alertas, checklist-seguridad]
19
+ skillsRestringidos: [frontend-css-swl, mobile-flutter]
20
20
  permisosRed: false
21
21
  permisosEscritura: true
22
22
  permisosComandos: true
@@ -25,6 +25,8 @@ toolBudget:
25
25
  standard: 30
26
26
  complex: 60
27
27
  evolvable: false # nivelRiesgo=ALTO
28
+ fase: release
29
+ dominio: infra
28
30
  exclusiones:
29
31
  - "No invocar para implementar features de aplicación — este agente trabaja en confiabilidad y operaciones, no en lógica de negocio."
30
32
  - "No invocar para configurar pipelines CI/CD — ese trabajo corresponde a devops-ci-swl."
@@ -6,15 +6,15 @@ description: >
6
6
  y gaps de cobertura. Objetivo: >80% líneas, >70% branches. Invocar antes de
7
7
  implementar (TDD) o después de implementar (auditoría de cobertura). NUNCA
8
8
  modifica código de producción — solo archivos de test y fixtures.
9
- tools: Read, Write, Edit, Bash, Grep, Glob
9
+ tools: [Read, Write, Edit, Bash, Grep, Glob]
10
10
  model: claude-sonnet-4-6
11
11
  modeloAlterno: claude-haiku-4-5-20251001
12
12
  ventanaContexto: 200k
13
13
  color: purple
14
14
  version: 1.0.0
15
15
  nivelRiesgo: BAJO
16
- skillsInvocables: tdd-workflow, testing-python, checklist-calidad, manejo-errores, webapp-testing, php-testing, nextjs-testing
17
- skillsRestringidos: ninguno
16
+ skillsInvocables: [tdd-workflow, testing-python, checklist-calidad, manejo-errores, webapp-testing, php-testing, nextjs-testing]
17
+ skillsRestringidos: []
18
18
  permisosRed: false
19
19
  permisosEscritura: true
20
20
  permisosComandos: true
@@ -23,6 +23,8 @@ toolBudget:
23
23
  standard: 30
24
24
  complex: 50
25
25
  evolvable: true # nivelRiesgo=BAJO
26
+ fase: verify
27
+ dominio: quality
26
28
  exclusiones:
27
29
  - "No invocar para testing E2E de aplicaciones móviles — ese trabajo corresponde a mobile-testing-swl."
28
30
  - "No invocar para testing de seguridad o penetration testing — ese trabajo corresponde a revisor-seguridad-swl o red-team-swl."
@@ -9,7 +9,7 @@ description: >
9
9
  disenador-ui-swl (capa visual y tokens). NO invocar para implementacion de
10
10
  codigo — produce specs, no codigo. Para implementar usar frontend-*-swl con
11
11
  la UI-SPEC.md producida.
12
- tools: Read, Write, Grep, Glob, WebSearch
12
+ tools: [Read, Write, Grep, Glob, WebSearch]
13
13
  model: claude-sonnet-4-6
14
14
  modeloAlterno: claude-haiku-4-5-20251001
15
15
  ventanaContexto: 200k
@@ -17,18 +17,14 @@ permissionMode: plan
17
17
  color: pink
18
18
  version: 1.0.0
19
19
  nivelRiesgo: BAJO
20
- skillsInvocables: ux-diseno, wireframes-flujos, design-tokens, accesibilidad-a11y, diseno-responsivo
21
- skillsRestringidos:
22
- - fastapi-python
23
- - django-expert
24
- - postgresql-table-design
25
- - python-patterns
26
- - angular-component
27
- - angular-forms
20
+ skillsInvocables: [ux-diseno, wireframes-flujos, design-tokens, accesibilidad-a11y, diseno-responsivo]
21
+ skillsRestringidos: [fastapi-python, django-expert, postgresql-table-design, python-patterns, angular-component, angular-forms]
28
22
  permisosRed: true
29
23
  permisosEscritura: true
30
24
  permisosComandos: false
31
25
  evolvable: true # nivelRiesgo=BAJO
26
+ fase: plan
27
+ dominio: ux
32
28
  exclusiones:
33
29
  - "No invocar para implementar código de interfaz — este agente produce UI-SPEC.md, no código; usar frontend-react-swl, frontend-angular-swl o frontend-swl para la implementación."
34
30
  - "No invocar cuando el pipeline tiene roles separados: preferir investigador-ux-swl para research y disenador-ui-swl para especificaciones visuales puras."
@@ -356,6 +356,24 @@ Asignar badge:
356
356
  < 60 → Sin badge — no hacer merge hasta corregir
357
357
  ```
358
358
 
359
+ ### Gate de promoción para auto-evolucion-swl (G8)
360
+
361
+ Este comando es el **gate obligatorio** del flujo de promoción de skills
362
+ auto-generados desde `_userland/plugins/` a `habilidades/`. El agente
363
+ `auto-evolucion-swl` invoca este comando antes de mover un skill al core
364
+ (ver `agentes/auto-evolucion-swl.md` sección "Gate G8").
365
+
366
+ Umbral de promoción:
367
+
368
+ | Badge | Acción de auto-evolucion |
369
+ |-------|-------------------------|
370
+ | Platino, Oro, Plata | Promover a `habilidades/`, registrar en manifiestos |
371
+ | Bronce | NO promover, devolver a `_userland/` con feedback |
372
+ | Sin badge | NO promover, registrar rechazo en `.planning/evolucion/promociones-rechazadas.jsonl` |
373
+
374
+ Si un skill es rechazado ≥3 veces consecutivas, el agente escala al usuario
375
+ con análisis de qué dimensiones bajan el score. Origen: ADR 0013 sección 3C.
376
+
359
377
  ## Paso 4 — Emitir reporte
360
378
 
361
379
  Generar el siguiente reporte directamente en la conversación:
@@ -38,6 +38,30 @@ Si el archivo no existe: ejecutar el regenerado del Paso 0. Si sigue sin
38
38
  existir: reportar "el ciclo de evolución no ha corrido aún; ejecuta cualquier
39
39
  tarea para poblar métricas iniciales".
40
40
 
41
+ ## Paso 1.5 — Cargar contexto del kernel (gobernanza)
42
+
43
+ Para el bloque **KERNEL** del dashboard, recolectar:
44
+
45
+ ```bash
46
+ # 1. Conteo de agentes evolvable=false vs total
47
+ TOTAL=$(ls agentes/*.md | grep -v "^agentes/_" | wc -l)
48
+ EVOL_FALSE=$(grep -l '^evolvable: false' agentes/*.md | wc -l)
49
+
50
+ # 2. Último ADR del repo madre — el más reciente por fecha en frontmatter
51
+ ULTIMO_ADR=$(ls .planning/adrs/[0-9]*.md | sort -r | head -1)
52
+ TITULO=$(head -1 "$ULTIMO_ADR" | sed 's/^# //')
53
+ FECHA=$(grep -m1 '^\*\*Fecha\*\*:' "$ULTIMO_ADR" | sed 's/.*\*\*Fecha\*\*:\s*//')
54
+
55
+ echo "ADR_KERNEL_LINEA=\"$TITULO — $FECHA\""
56
+ echo "RATIO=\"$EVOL_FALSE/$TOTAL\""
57
+ ```
58
+
59
+ Estos valores alimentan la sección KERNEL del template.
60
+
61
+ Si `.planning/evolucion/politica-evolvable.md` no existe, reportar como
62
+ hallazgo crítico: el kernel SIN política documentada es violación de
63
+ `reglas/gobernanza.md`.
64
+
41
65
  ## Paso 2 — Emitir el dashboard (formato humano)
42
66
 
43
67
  Template de salida:
@@ -74,6 +98,26 @@ Template de salida:
74
98
  Aplicadas ............. <n>
75
99
  Revertidas ............ <n>
76
100
  Neta .................. <n>
101
+ Rollback ratio ........ <%> (N/A si aplicadas == 0)
102
+
103
+ CALIDAD CONDUCTUAL (14d)
104
+ Fallos por tipo:
105
+ bad_output_format <n>
106
+ tool_error <n>
107
+ timeout <n>
108
+ schema_violation <n>
109
+ task_incomplete <n>
110
+ unknown <n>
111
+ Reintentos consecutivos
112
+ Total ............... <n> (mismo agente, misma task, ≤30 min)
113
+ Top agente .......... <agente>: <n>
114
+ Latencia top agente
115
+ Mediana ............. <ms>
116
+ p95 ................. <ms>
117
+ Sin instrumentación todavía:
118
+ • precision_routing_fase_dominio
119
+ • utilidad_memoria_recuperada
120
+ • violaciones_formato_post_ejecucion
77
121
 
78
122
  ALERTAS PERSISTENTES
79
123
  <si alertasActivas.length === 0>
@@ -82,6 +126,11 @@ Template de salida:
82
126
  [!] kind=X target=Y count=Z (primera: fecha)
83
127
  ...
84
128
 
129
+ KERNEL (gobernanza)
130
+ Política evolvable .... .planning/evolucion/politica-evolvable.md
131
+ Último ADR kernel ..... <ADR-NNNN — título — fecha>
132
+ Agentes evolvable=false <n>/<total>
133
+
85
134
  ═══════════════════════════════════════════════════════════════
86
135
  ```
87
136
 
@@ -120,7 +120,57 @@ Actualiza la versión en TODOS los archivos que la contienen. Para proyectos SWL
120
120
 
121
121
  Para proyectos no-SWL: actualiza los archivos detectados en Paso 1 (package.json, pyproject.toml, VERSION).
122
122
 
123
- En ambos casos, verificar consistencia después de actualizar con `grep -r "versión-anterior" .` para detectar referencias olvidadas.
123
+ ### Tres capas de versionado NO confundir
124
+
125
+ El sistema SWL versiona en tres capas independientes. El bump de versión del SISTEMA toca SOLO la capa 1:
126
+
127
+ | Capa | Qué versiona | Cuándo cambia | Tocar en `/swl:release`? |
128
+ |------|--------------|---------------|--------------------------|
129
+ | **1. Sistema** | El paquete `@saulwade/swl-ses` como conjunto | En cada release | **SÍ — los 15 archivos del checklist arriba** |
130
+ | **2. Componente individual** | Frontmatter `version:` de cada agente o skill (`agentes/*.md`, `habilidades/*/SKILL.md`) | Cuando el componente específico cambia (ver `/swl:aprender` Paso 6 acción 2) | **NO — cada componente versiona independiente** |
131
+ | **3. Histórico** | Referencias a versiones pasadas en `CHANGELOG.md`, `CHANGELOG-LEGACY.md`, ADRs, RELEASE-NOTES, comentarios `Histórico: hasta vX.Y...` | Nunca (son inmutables por definición) | **NO — son registros históricos** |
132
+
133
+ Verificación post-bump con `grep -rn "<versión-anterior>"` revelará decenas o cientos de matches en capas 2 y 3 — eso es esperado y correcto. Filtrar ruido para confirmar que las únicas líneas modificadas son de la capa 1:
134
+
135
+ ```bash
136
+ # Después del bump, validar consistencia SOLO en archivos canónicos del sistema
137
+ node -e "
138
+ const p=require('./package.json'),l=require('./plugin.json');
139
+ const lock=require('./package-lock.json');
140
+ const ok = p.version===l.version && l.version===lock.version;
141
+ console.log(ok ? 'OK consistente: '+p.version : 'INCONSISTENTE');
142
+ "
143
+ ```
144
+
145
+ Si el chequeo del nodo arriba dice OK pero `grep` aún muestra matches de la versión anterior, esos matches son **capa 2 (frontmatter)** o **capa 3 (histórico)** y son legítimos — no tocarlos.
146
+
147
+ ### Republish-only entre registries (caso especial)
148
+
149
+ Si el publish dual falla en uno de los 2 registries (typically npmjs o GitHub
150
+ Packages) pero el otro queda publicado, **NO se puede reintentar la misma versión**
151
+ — ningún registry permite sobreescribir versiones. Ejecutar inmediatamente un bump
152
+ PATCH (1.X.Y → 1.X.(Y+1)) siguiendo el checklist arriba, y publicar solo al
153
+ registry faltante con `node scripts/publicar.js --solo-npmjs` o `--solo-github`.
154
+
155
+ Documentar el republish exclusivamente como "republish de coordinación entre
156
+ registries" en CHANGELOG, sin atribuirle cambios funcionales que no existen.
157
+
158
+ Ver `Skill("release-semver")` sección "Publish a múltiples registries" para detalles.
159
+
160
+ ## Paso 6.5 — Regenerar skills-lock.json
161
+
162
+ Antes del CHANGELOG, regenerar el lock de skills para capturar el estado de
163
+ los 151 SKILL.md de la release:
164
+
165
+ ```bash
166
+ node scripts/generar-skills-lock.js
167
+ git add manifiestos/skills-lock.json
168
+ ```
169
+
170
+ El lock contiene SHA256 de cada SKILL.md y permite que `/swl:salud` detecte
171
+ drift silencioso entre releases. Si el lock no cambió respecto al anterior,
172
+ el commit lo refleja como no-op (idempotente). El archivo es pequeño (~37KB)
173
+ y debe versionarse.
124
174
 
125
175
  ## Paso 7 — Generar CHANGELOG
126
176
 
@@ -178,6 +228,32 @@ Resultado: 13/15 OK, 2 FALLA(S) obligatoria(s)
178
228
 
179
229
  Esta gate existe porque el agente `release-manager-swl` ha omitido archivos en 3 releases consecutivos (5.10.3, 5.10.4, 5.10.5 — ver APRENDIZAJES.md) pese a tener la checklist documentada en su frontmatter. Un checklist textual no basta: se necesita ejecución.
180
230
 
231
+ ### 10.1.1 Gate de cobertura por lenguaje (regeneración obligatoria)
232
+
233
+ Tras el verificar-release.js, regenerar la matriz lenguaje × cobertura SWL:
234
+
235
+ ```bash
236
+ node scripts/generar-matriz-lenguajes.js
237
+ ```
238
+
239
+ El script reescribe `.planning/cobertura-lenguajes.md` con el estado actual.
240
+ Comparar contra el commit anterior:
241
+
242
+ ```bash
243
+ git diff .planning/cobertura-lenguajes.md
244
+ ```
245
+
246
+ Si algún lenguaje **bajó de status** (completo → parcial, parcial → faltante)
247
+ sin ADR documentado en `.planning/adrs/` justificando la regresión, **NO hacer
248
+ release**. La regresión silenciosa de cobertura erosiona la promesa "polyglot"
249
+ del repo.
250
+
251
+ Si el cambio es esperado (eliminación deliberada de un componente), el ADR
252
+ debe existir con número y fecha y referenciarse en el commit del release.
253
+
254
+ Lo mismo aplica para releases con incremento de cobertura: la nueva matriz se
255
+ commitea junto al release y se menciona en RELEASE-NOTES.
256
+
181
257
  ### 10.2 Verificación manual post-gate
182
258
 
183
259
  ```bash
@@ -286,6 +286,29 @@ Si `SWL_AUDIT_AGENTES` no está definida, este paso se omite — los reportes
286
286
  son opt-in por diseño (CLAUDE.md: "Variables de entorno opt-in para
287
287
  integraciones enterprise").
288
288
 
289
+ ## Paso 5e — Verificación de drift de skills (skills-lock)
290
+
291
+ Si existe `manifiestos/skills-lock.json`, comparar el hash actual de cada
292
+ `habilidades/*/SKILL.md` contra el lock. Detecta ediciones silenciosas de
293
+ skills entre release y release.
294
+
295
+ ```bash
296
+ node scripts/generar-skills-lock.js --check
297
+ ```
298
+
299
+ Estados posibles:
300
+
301
+ | Salida | Significado | Acción |
302
+ |--------|-------------|--------|
303
+ | `✓ skills-lock OK: N skills sin drift` | Todos los hashes coinciden | Continuar |
304
+ | `✗ skills-lock DRIFT detectado` | Skills modificados, nuevos o faltantes | Listar diferencias y proponer regenerar |
305
+
306
+ Si NO existe el lock (proyecto fresco o pre-1.1.0), este paso se omite y
307
+ emite advertencia sugerente: `node scripts/generar-skills-lock.js`.
308
+
309
+ El lock se regenera automáticamente como parte de `/swl:release`. Drift
310
+ fuera de release indica ediciones manuales no committed.
311
+
289
312
  ## Paso 6b — Formato de salida enriquecido (HealthRow)
290
313
 
291
314
  El módulo `scripts/lib/health-row.js` genera filas de salud con formato visual enriquecido
@@ -1,7 +1,12 @@
1
1
  ---
2
2
  name: checklist-seguridad
3
3
  description: Checklist de seguridad basado en OWASP Top 10 + seguridad de agentes autónomos (A11). Cubre inyección, autenticación, exposición de datos, control de acceso, configuración insegura, XSS, deserialización, componentes vulnerables, logging y agencia excesiva de IA. Produce reporte con hallazgos y remediaciones.
4
- version: "1.0.0"
4
+ version: "1.1.1"
5
+ evolved: true
6
+ evolved-from: "1.1.0"
7
+ evolved-at: "2026-05-04"
8
+ evolved-by: "aprender"
9
+ evolved-note: "Corregir contradicción interna en ejemplo BIEN de IDOR pre-side-effect: el detail exponía UUID interno violando el gotcha 'vocabulario interno' de fastapi-experto v1.1.1. Detail al cliente ahora es genérico (Recurso no encontrado); UUID y tenant solo en logger.info para auditoría."
5
10
  herramientasPermitidas: [Read, Grep]
6
11
  evolvable: true # default para skill estandar
7
12
  nist_csf: [PR.PS-01, PR.DS-02, PR.DS-10, DE.CM-09, RS.MI-01]
@@ -58,6 +63,57 @@ grep -rn "user_id.*Query\|owner_id.*Query" --include="*.py" | head -20
58
63
  **Señal de vulnerabilidad**: endpoint que filtra por `user_id` recibido como
59
64
  parámetro de URL sin compararlo con el `user_id` del token JWT.
60
65
 
66
+ ### IDOR pre-side-effect: validar pertenencia ANTES de operaciones costosas
67
+
68
+ Un endpoint que sube/cifra/procesa material antes de validar que el recurso pertenece al
69
+ tenant del usuario es un IDOR amplificado: aunque RLS o el FK rechacen la operación al
70
+ final, los **side-effects intermedios** (lectura de bytes del cliente, cifrado AES-GCM,
71
+ upload a MinIO/S3 bajo un namespace, llamada costosa a API externa) **ya ocurrieron** y
72
+ pueden contaminar storage, agotar recursos o filtrar PII bajo el namespace incorrecto.
73
+
74
+ ```python
75
+ # MAL — IDOR pre-side-effect: el upload ocurre antes del check RLS
76
+ async def subir_evidencia(entrega_id: UUID, archivo: UploadFile, ...):
77
+ contenido = await archivo.read() # side-effect: bytes leídos
78
+ cifrado = aes_gcm_encrypt(contenido, master_key) # side-effect: PII cifrada
79
+ object_key = f"tenant_{usuario.tenant_id}/{entrega_id}/{uuid4()}"
80
+ await minio.put_object(object_key, cifrado) # side-effect: subido a MinIO
81
+ # AHORA el INSERT corre con RLS — si entrega_id es de otro tenant, falla con 404,
82
+ # pero el PII ya quedó cifrado bajo el namespace de tenant_A para entrega_B.
83
+ await repo.insert_evidencia(entrega_id, object_key, ...)
84
+ ```
85
+
86
+ ```python
87
+ # BIEN — validar pertenencia ANTES de cualquier side-effect costoso
88
+ async def subir_evidencia(entrega_id: UUID, archivo: UploadFile, ...):
89
+ if await repo_entrega.obtener_por_id(entrega_id) is None:
90
+ # RLS filtra el SELECT — None significa "no existe en este tenant".
91
+ # Detail genérico al cliente; UUID + tenant solo en log interno.
92
+ # (Ver gotcha "vocabulario interno en detail" de fastapi-experto.)
93
+ logger.info(
94
+ "IDOR check fallido: recurso %s no accesible para tenant %s",
95
+ entrega_id, usuario.tenant_id,
96
+ )
97
+ raise HTTPException(404, "Recurso no encontrado.")
98
+
99
+ contenido = await archivo.read() # solo si validación pasó
100
+ cifrado = aes_gcm_encrypt(contenido, master_key)
101
+ ...
102
+ ```
103
+
104
+ **Cuándo aplicar**: cualquier endpoint que combine path-param de recurso (`{recurso_id}`)
105
+ con uno o más de estos side-effects: `archivo.read()`, cifrado, upload a storage externo,
106
+ llamada a API costosa de PSP/proveedor, generación de PDF, envío de email/SMS. Antes
107
+ del primer side-effect, ejecutar `repo.obtener_por_id(recurso_id)` y retornar 404 si None.
108
+
109
+ **Mecanismos que NO son suficientes por sí solos**:
110
+ - RLS DENY-by-default: protege el INSERT/UPDATE/SELECT, pero NO los side-effects intermedios.
111
+ - FK constraint: protege la integridad referencial, pero el side-effect ya ocurrió.
112
+ - `require_permiso`: valida que el usuario tiene permiso de la acción, NO que el recurso
113
+ específico pertenezca a su tenant.
114
+
115
+ La validación pre-side-effect es **defensa en profundidad**: complementa RLS/FK, no las reemplaza.
116
+
61
117
  ---
62
118
 
63
119
  ## A02 — Cryptographic Failures (Exposición de datos sensibles)
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: extractor-de-aprendizajes
3
3
  description: Convertir errores y patrones descubiertos durante la implementación en nuevas habilidades o reglas. Ciclo de mejora continua del sistema SWL.
4
- version: "1.0.2"
4
+ version: "1.0.3"
5
5
  herramientasPermitidas: [Read]
6
6
  exclusiones:
7
7
  - "No cargar para actualizar el perfil del usuario — las correcciones explícitas del usuario van a `instintos/perfil-usuario.yaml` vía `perfilador-usuario-swl`, no a APRENDIZAJES.md."
@@ -10,10 +10,10 @@ exclusiones:
10
10
  - "No cargar si el aprendizaje no tiene causa raíz identificada — documentar síntoma sin causa produce reglas que no previenen el error real."
11
11
  evolvable: true # default para skill estandar
12
12
  evolved: true
13
- evolved-from: "5.12.3"
14
- evolved-at: "2026-04-25"
13
+ evolved-from: "1.1.1"
14
+ evolved-at: "2026-05-02"
15
15
  evolved-by: "aprender"
16
- evolved-note: "2 gotchas nuevos: Explore sobreestima papers + hooks calidad falsos positivos"
16
+ evolved-note: "Extender gotcha Explore (papers papers+repos) tras confirmación x4 — sync desde skill global"
17
17
  ---
18
18
  # Extractor de Aprendizajes
19
19
 
@@ -296,7 +296,17 @@ Durante `/swl:aprender`, aplicar estas reglas:
296
296
  - **`rating: HIGH` asignado sin verificar criterio de irreversibilidad**: el agente promueve a CLAUDE.md un aprendizaje "MEDIUM" por el entusiasmo del momento. Causa: no se aplicó el criterio de "decisión irreversible o bug crítico". Solución: antes de asignar HIGH, verificar: ¿cambiar esto en el futuro requeriría refactorizar múltiples archivos o migrar datos? Si no, mantener MEDIUM.
297
297
  - **Regla incompleta sobre registro de hooks**: una entrada en memoria dice "registrar en modulos.json" pero omite `hooks-config.json`, y en la siguiente iteración se repite el fallo en CI porque ambos manifiestos son obligatorios. Causa: la regla de la lección anterior no cubrió todos los manifiestos afectados. Solución: al documentar una regla sobre registro en manifiestos, listar EXPLÍCITAMENTE cada manifiesto con su responsabilidad distinta (`modulos.json` = qué copiar; `hooks-config.json` = cómo registrar evento). Evidencia: tres incidentes históricos del mismo patrón incompleto (v5.7.1, v5.7.2/3, v5.11.0).
298
298
  - **Inventario estimado a mano en vez de regenerar**: el agente cuenta "28 hooks" visualmente y propaga la cifra a 5 archivos (CLAUDE/README/package/plugin/SALUD); al regenerar con `scripts/generar-inventario.js` el número real es 30. Causa: confiar en la observación directa en vez de la fuente de verdad determinista. Solución: antes de modificar cualquier contador en documentación oficial, ejecutar el script de inventario y usar su salida como ground truth.
299
- - **Sub-agente Explore sobreestima costos al analizar papers académicos**: cuando se delega análisis de un paper (arXiv, MIT, etc.) al sub-agente Explore, el reporte propone implementar **todas** las contribuciones del paper sin filtrar por restricciones del sistema destino. Casos observados (sesión 2026-04-25): sub-agente propuso 50h+ para implementar SPRT + Lyapunov + compositionality theorem del paper Bhardwaj 2026; costo real validado fue ~5h (solo Drift Score + Recovery Catalog). Causa: el Explore evalúa portabilidad técnica sin aplicar filtro de "datos disponibles" ni "infraestructura zero-deps". Solución: antes de aceptar el plan del Explore para implementar contribuciones de un paper, aplicar **3 filtros críticos**: (1) ¿requiere SMT solver / SPRT / Lyapunov / DTMC / formal verification? → descartar (rompe zero-deps); (2) ¿requiere N>100 sesiones de campo para validación estadística? → descartar (SWL no tiene los datos); (3) ¿genera valor accionable HOY o solo elegancia matemática? → solo implementar si HOY. Evidencia: 3 papers analizados (evolver, Bhardwaj 2026, Zhang et al. 2026) con descarte sistemático ~70-90% del contenido propuesto.
299
+ - **Sub-agente Explore sobreestima al analizar fuentes externas (papers + repos)** [CONFIRMADO x4]: cuando se delega análisis de una fuente externa (paper arXiv, repositorio GitHub, documentación de framework) al sub-agente Explore, el reporte propone implementar contribuciones sin filtrar por restricciones del sistema destino Y, peor, sin verificar qué de eso ya existe en el proyecto. **Dos modos de falla observados**:
300
+
301
+ **Modo A — papers académicos** (sesión 2026-04-25): sub-agente propuso 50h+ para implementar SPRT + Lyapunov + compositionality theorem del paper Bhardwaj 2026; costo real validado fue ~5h (solo Drift Score + Recovery Catalog). Causa: el Explore evalúa portabilidad técnica sin aplicar filtro de "datos disponibles" ni "infraestructura zero-deps". Evidencia: 3 papers analizados (evolver, Bhardwaj 2026, Zhang et al. 2026) con descarte sistemático ~70-90% del contenido propuesto.
302
+
303
+ **Modo B — repositorios externos** (sesión 2026-05-02 cosecha-temp/ EMAIA): sub-agente analizando `temp/docetl-main` (UC Berkeley, arXiv:2410.12189) afirmó dos cosas FALSAS verificables contra el código del proyecto destino: (1) "EMAIA ya usa LiteLLM" — verificación contra `pyproject.toml` + `grep -r litellm core/` mostró que EMAIA tiene clientes propios (`OllamaClient`/`NIMClient`/`vLLMClient`) y NO usa LiteLLM en absoluto; (2) recomendó portar Gleaning como patrón nuevo, cuando `core/learning/evaluator_optimizer.py` y `core/llm/quality_judge.py` YA implementan Anthropic Evaluator-Optimizer + LLM-as-judge. El fix real era WIRING (~30 LOC), no porting. Costo "MEDIO" del agente quedó como BAJO real. Causa: el Explore evalúa el repo externo en aislamiento, sin leer paths del proyecto destino para verificar qué mecanismos ya existen.
304
+
305
+ **Solución unificada — aplicar filtros antes de aceptar propuesta del Explore**:
306
+
307
+ *Para papers académicos (Modo A)*: 3 filtros críticos: (1) ¿requiere SMT solver / SPRT / Lyapunov / DTMC / formal verification? → descartar (rompe zero-deps); (2) ¿requiere N>100 sesiones de campo para validación estadística? → descartar; (3) ¿genera valor accionable HOY o solo elegancia matemática? → solo implementar si HOY.
308
+
309
+ *Para repos externos (Modo B)*: 4 filtros críticos: (1) ¿qué % es teoría/código no-portable vs portable? — si >70% no-portable, recortar; (2) **¿la propuesta reescribe mecanismos existentes en el proyecto destino?** — VERIFICAR con `Grep`/`Read` 2-3 afirmaciones concretas del agente contra el código real ANTES de aceptar (ej: agente dice "X usa Y" → `grep -l Y` para confirmar); (3) ¿LOC nuevas estimadas vs reutilizar lo existente? — si la propuesta supera 500 LOC para un solo patrón, hay sobre-ingeniería; (4) ¿el alcance reducido cubre 80% del valor? — Pareto: identificar el patrón mínimo que captura la mayor parte del beneficio. **Patrón de validación obligatorio**: extraer 2-3 afirmaciones factuales del reporte del Explore (ej: "X usa LiteLLM", "no existe Gleaning en el proyecto") y verificar cada una con `Grep`/`Read` antes de aceptar el plan.
300
310
  - **Hooks de calidad pre-commit bloquean fixtures de tests como falsos positivos**: el hook `calidad-pre-commit.js` aplica regex `\b(api_key|password|token|secret)\s*[=:]\s*["'][^"'\s]{4,}["']` que matchea fixtures legítimos en archivos de test. Caso real: test que valida que la función `sanitizar()` redacta `api_key="abc12345xyz"` se bloquea. Causa: el hook no distingue contexto de test vs producción. Solución: en archivos de test, construir fixtures con concatenación de strings (`'api' + '_key'`, `'pass' + 'word'`) o agregar marcador placeholder reconocido por el hook (`fake_`, `dummy_`, `placeholder`, `example`, `os.environ`). NUNCA bypassear el hook con `--no-verify` — el detector cumple su función; ajustar el fixture es lo correcto.
301
311
 
302
312
  ## Anti-patrones del proceso de extracción
@@ -5,7 +5,12 @@ description: >
5
5
  testing con httpx. Incluye el anti-patrón crítico MissingGreenlet (lazy loading
6
6
  en async). Cargar cuando se implementen endpoints FastAPI, schemas Pydantic v2,
7
7
  queries SQLAlchemy async, WebSockets, SSE o tests de integración con httpx.
8
- version: "1.1.0"
8
+ version: "1.1.2"
9
+ evolved: true
10
+ evolved-from: "1.1.1"
11
+ evolved-at: "2026-05-04"
12
+ evolved-by: "aprender"
13
+ evolved-note: "Fix lógica del gotcha endswith asyncpg: el formato 'INSERT oid N' tiene 3 partes (no 2), len(partes) == 2 fallaba para INSERT. Solución correcta: int(partes[-1]) — siempre el count, sea con o sin oid."
9
14
  herramientasPermitidas: [Read]
10
15
  exclusiones:
11
16
  - "No cargar para proyectos Django o Flask — los patrones de ORM sync, Class-Based Views y middleware difieren fundamentalmente; cargar `django-experto` o el skill del framework correspondiente."
@@ -210,6 +215,10 @@ class Factura(Base):
210
215
  - **El endpoint GET hace `db.commit()` y el test pasa, pero en producción los datos se modifican inesperadamente**: un GET que comitea no es idempotente — herramientas de monitoreo, crawlers de SEO o retries del cliente pueden ejecutar el GET múltiples veces. Causa: `db.commit()` en un GET activa transacciones que modifican estado. Solución: según la regla del skill, los endpoints GET NUNCA deben hacer `db.commit()` — si el endpoint necesita registrar que se accedió al recurso, usar una tarea async en background con `BackgroundTasks`.
211
216
  - **Pydantic v2 `model_validator(mode='before')` silencia errores de tipo al recibir None donde se espera un dict**: Pydantic v2 convierte `None` a `{}` en algunos contextos de validación `before`, produciendo un modelo con todos los campos como `None` en lugar de fallar con `ValidationError`. Causa: el `mode='before'` recibe el valor crudo antes de la coerción de tipos; si el validator retorna el valor sin verificar, Pydantic intenta instanciar el modelo con datos inválidos. Solución: en el `model_validator(mode='before')`, verificar explícitamente que el valor no es `None` antes de procesarlo: `if values is None: raise ValueError("...")`.
212
217
  - **Baseline Alembic con `create_all()` genera deuda estructural en toda la cadena de migraciones posterior**: si el baseline `0001_initial.py` usa `Base.metadata.create_all(bind=op.get_bind())` en lugar de `op.create_table(...)` explícito, cada migración posterior (0002, 0003, ...) debe asumir que el estado real del schema puede diverger del árbol de modelos declarado y volverse idempotente con helpers tipo `_column_if_missing`, `_index_if_missing`. Causa: `create_all` refleja el estado actual de los modelos Python, no un snapshot explícito del schema — si el schema de producción diverge (índices agregados a mano, columnas con defaults distintos), las migraciones posteriores fallan con errores de "already exists" o no ejecutan acciones que asumen que el estado previo era el declarado. Solución: **nunca** usar `create_all()` en migraciones de Alembic. Baseline debe tener `op.create_table(...)` explícito por cada tabla del schema inicial. Si el proyecto ya tiene esta deuda, dos opciones: (a) migraciones posteriores 100% idempotentes con `IF NOT EXISTS` y helpers, (b) regenerar baseline con `alembic revision --autogenerate` tras un `alembic stamp head` a una base limpia y squash del historial.
218
+ - **`tag.endswith("1")` para parsear count de asyncpg DELETE/UPDATE matchea falsos positivos**: `conn.execute("DELETE FROM ...")` retorna un string `"DELETE N"` donde N es el número de filas afectadas. Usar `tag.endswith("1")` para verificar "exactamente 1 fila afectada" matchea **incorrectamente** `"DELETE 10"`, `"DELETE 11"`, `"DELETE 21"`, `"DELETE 100"`. Aunque el query actual sea `WHERE id = $1` (PK, máximo 1 fila), un refactor futuro a `WHERE programa_id = $1` haría que el método retornara True silenciosamente para 10+ filas. Causa: `endswith` opera sobre sufijo textual, no parsea el número. **El formato del tag varía por comando**: `"UPDATE N"` y `"DELETE N"` tienen 2 partes, pero `"INSERT oid N"` tiene 3 partes (asyncpg/PostgreSQL siempre incluye el oid en INSERT, generalmente `0` en tablas modernas sin OIDs). Usar `len(partes) == 2` falla para INSERT. Solución portable que cubre los tres casos: leer el **último** componente, que siempre es el count: `partes = tag.split(); afectadas = int(partes[-1]) if len(partes) >= 2 and partes[-1].isdigit() else 0`. Para verificar "exactamente 1": `afectadas == 1`. Aplicable a UPDATE, DELETE e INSERT por igual.
219
+ - **`return result or {}` después de UPDATE/INSERT enmascara errores de BD silenciosamente**: patrón típico `result = await repo.actualizar_estatus(...); return result or {}` devuelve `{}` al cliente cuando el UPDATE no encontró la fila (race condition, RLS, FK violado). El cliente recibe HTTP 200 con body vacío en lugar del 404/500 esperado, los bugs quedan invisibles en monitoring. Causa: `or {}` trata `None` como "datos no disponibles" cuando en realidad significa "el UPDATE falló post-INSERT". Solución: explicit None check con raise — usar `if result is None: raise HTTPException(404, "Recurso no encontrado")` cuando el ID viene del cliente, o `raise HTTPException(500, "Error interno...")` cuando es un invariante post-INSERT (la fila acaba de crearse, debe existir).
220
+ - **`detail=str(exc)` en HTTPException — solo aceptable cuando la excepción es de DOMINIO con mensaje diseñado para usuario**: las excepciones de capa externa (MinIO/S3, BD driver, HTTP client de un PSP, parser PDF) tienen mensajes que pueden contener bucket/host/paths/credenciales parciales/stack traces. Pasar `str(exc)` directo al `detail` los expone al cliente. Causa: tratar todas las excepciones igual sin distinguir dominio (controlado) de capa externa (no controlado). Solución: dos patrones distintos. Para excepciones de dominio (`MIMENoPermitidoError("MIME 'X' no permitido")`, `EmailDuplicadoError`): `except DominioError as exc: raise HTTPException(422, detail=str(exc))` OK. Para capa externa (`ErrorEvidencia`, `boto3.ClientError`, `httpx.RequestError`): `except CapaExternaError as exc: logger.exception("contexto"); raise HTTPException(502, detail="Error genérico al cliente")`. La diferencia es que el mensaje de DominioError fue diseñado para el usuario; el de CapaExternaError no.
221
+ - **Vocabulario interno (nombres de funciones PL/pgSQL, schemas, tablas, "RLS", "transaccional") en `detail` al cliente fuga arquitectura**: detail genérico al cliente y detail con detalles de infraestructura para diagnóstico **no son lo mismo**. Patrones de fuga típicos: `detail=f"fn_evaluar_X no retornó resultado"` (revela nombre de función PL/pgSQL), `detail="Posible inconsistencia transaccional o RLS"` (revela motor + capa de seguridad), `detail=f"Recurso {id} no encontrado en activacion tras INSERT"` (revela UUID interno y secuencia de operaciones). Causa: el desarrollador escribe el mensaje pensando en debug, no en exposición. Solución: detail al cliente = mensaje genérico orientado al recurso ("Error interno al activar el recurso. Contacte al administrador."); detalles internos solo en `logger.error("contexto detallado %s %s", uuid, programa_id, ...)` con format strings estructurados (NO concatenación con `+`). Aplica a todos los HTTPException 500/503; el 404/422 puede ser más específico si el mensaje es del dominio.
213
222
 
214
223
  ## Referencias especializadas
215
224
 
@@ -1,9 +1,9 @@
1
- {
2
- "SKILL.md": {
3
- "evolved": true,
4
- "evolvedFrom": "5.10.4",
5
- "evolvedAt": "2026-04-20",
6
- "evolvedBy": "aprender",
7
- "evolvedNote": "throttle adaptativo ante fallo transitorio"
8
- }
1
+ {
2
+ "SKILL.md": {
3
+ "evolved": true,
4
+ "evolvedFrom": "1.1.0",
5
+ "evolvedAt": "2026-05-02",
6
+ "evolvedBy": "aprender",
7
+ "evolvedNote": "Gotcha nueva: try/catch require para módulos opcionales (15 archivos migrados)"
8
+ }
9
9
  }
@@ -4,12 +4,12 @@ description: >
4
4
  Patrones de manejo de errores en Python y TypeScript. Jerarquía de excepciones,
5
5
  excepciones personalizadas, códigos de error, logging estructurado, error boundaries,
6
6
  degradación elegante, patrones de reintento y circuit breakers.
7
- version: "1.1.0"
7
+ version: "1.2.0"
8
8
  evolved: true
9
- evolved-from: "1.0.2"
10
- evolved-at: "2026-04-24"
9
+ evolved-from: "1.1.1"
10
+ evolved-at: "2026-05-04"
11
11
  evolved-by: "aprender"
12
- evolved-note: "Sección nueva: except Exception: pass en cleanup es válido, pero con logger.debug para trazabilidad"
12
+ evolved-note: "+sección anti-patrón except Exception con isinstance interno usar excepts planos por tipo (sync desde global tras sesión SIGM Fase 5b). Preserva gotcha try/catch require de v1.1.1."
13
13
  herramientasPermitidas: [Read]
14
14
  exclusiones:
15
15
  - "No cargar para monitoreo y alertas en producción (Prometheus, Grafana, PagerDuty) — para observabilidad cargar `monitoring-alertas` o `sre-patrones`."
@@ -76,6 +76,50 @@ class ErrorExterno(ErrorAplicacion):
76
76
 
77
77
  ---
78
78
 
79
+ ## Anti-patrón: `except Exception` con `isinstance` interno
80
+
81
+ **Problema**: capturar todo con `except Exception` y luego despachar por tipo con
82
+ `isinstance` enmascara excepciones futuras del módulo y dificulta seguir el flujo
83
+ de control.
84
+
85
+ ```python
86
+ # MAL — except genérico con isinstance interno
87
+ try:
88
+ registro = await ev_svc.subir_evidencia(...)
89
+ except (MIMENoPermitidoError, TamanoExcedidoError) as exc:
90
+ raise HTTPException(422, detail=...) from exc
91
+ except Exception as exc: # ← captura TODO lo que no caía arriba
92
+ if isinstance(exc, ErrorEvidencia):
93
+ raise HTTPException(502, detail="MinIO error") from exc
94
+ raise # cualquier otra excepción se re-lanza
95
+ ```
96
+
97
+ Problemas:
98
+ 1. Si en el futuro `ev_svc` lanza una nueva excepción `EvidenciaCorruptaError` que debería
99
+ ser 422, queda capturada por `except Exception` y se re-lanza como 500 silenciosamente.
100
+ 2. La intención del código no es clara: ¿qué tipos esperamos? ¿qué hace fallback?
101
+ 3. Auditores de seguridad no pueden verificar fácilmente qué excepciones están manejadas.
102
+
103
+ ```python
104
+ # BIEN — except plano por tipo, sin isinstance interno
105
+ try:
106
+ registro = await ev_svc.subir_evidencia(...)
107
+ except (MIMENoPermitidoError, TamanoExcedidoError) as exc:
108
+ raise HTTPException(422, detail=str(exc)) from exc
109
+ except ErrorEvidencia as exc:
110
+ logger.exception("Error MinIO al subir evidencia")
111
+ raise HTTPException(502, detail="Error genérico al cliente") from exc
112
+ # Cualquier otra excepción se propaga al handler global — explícito.
113
+ ```
114
+
115
+ Reglas:
116
+ - Un `except` por tipo (o tupla de tipos relacionados) que requiera tratamiento distinto.
117
+ - NO usar `except Exception` salvo en handlers globales explícitos (FastAPI exception_handler).
118
+ - Si necesitas distinguir múltiples tipos en un mismo handler, usa `except (A, B, C)` no
119
+ `except Exception` + `isinstance`.
120
+
121
+ ---
122
+
79
123
  ## Python — Encadenamiento de Excepciones
80
124
 
81
125
  SIEMPRE usar `raise ... from exc` para preservar la cadena de errores:
@@ -238,6 +282,21 @@ function debeVerificar() {
238
282
 
239
283
  Aplica a: health checks periódicos, verificación de nuevas versiones, consultas de cuota/rate-limit de APIs externas, chequeos de certificados, polling de colas. La línea divisoria es "¿el resultado anterior fue información real o fue ausencia de información?".
240
284
 
285
+ **Patrón `try { require } catch` para módulos opcionales en Node con fallback noop**: un hook o lib en `hooks/lib/` requiere de otra lib del mismo directorio (ej. `atomic-write.js`) que puede o no existir según la versión instalada del sistema o si el archivo se ejecuta standalone (CLI ad-hoc, test aislado). Importar con `require('./atomic-write')` directo lanza `MODULE_NOT_FOUND` y el hook crashea, bloqueando cualquier operación que lo invoque. Causa: `require` es síncrono y propaga la excepción al caller. Fix: envolver el require en try/catch y proveer un fallback que cumpla el mismo contrato de la dependencia, sin garantías adicionales (atomicidad, persistencia, etc.):
286
+
287
+ ```js
288
+ let atomicWriteSync, atomicWriteJSON;
289
+ try {
290
+ ({ atomicWriteSync, atomicWriteJSON } = require('./atomic-write'));
291
+ } catch {
292
+ // Fallback no-atómico — funciona pero pierde la garantía de write atómico
293
+ atomicWriteSync = (p, c, e) => fs.writeFileSync(p, c, e);
294
+ atomicWriteJSON = (p, o) => fs.writeFileSync(p, JSON.stringify(o, null, 2), 'utf8');
295
+ }
296
+ ```
297
+
298
+ Aplica a: módulos en `hooks/lib/` que se cargan desde otros hooks pero también pueden ejecutarse vía `node -e` o tests aislados, libs que dependen de otras libs del mismo paquete pero queremos que el lib funcione si alguien la copia sola, scripts de mantenimiento que viven en `scripts/` y referencian utilidades de `hooks/lib/`. NO aplica a dependencias del package.json (esas SÍ deben fallar si no están — son contratos firmes); aplica solo a dependencias internas del propio repositorio cuya disponibilidad no es garantizada en todos los contextos de ejecución. Evidencia: 15 archivos críticos de `hooks/` y `scripts/` migraron a este patrón en sesión 2026-05-02.
299
+
241
300
  ## Gotcha — Retry con cliente HTTP interno: propagar el timeout
242
301
 
243
302
  ### NUNCA: wrapper de retry que promete N segundos sobre cliente HTTP con timeout M << N
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: patrones-python
3
3
  description: Idiomas pythonicos, PEP 8, type hints modernos, dataclasses, async/await, context managers, decorators y generators. Patrones de código limpio en Python.
4
- version: "1.3.0"
4
+ version: "1.3.1"
5
5
  evolved: true
6
- evolved-from: "1.2.0"
7
- evolved-at: "2026-04-27"
6
+ evolved-from: "1.3.0"
7
+ evolved-at: "2026-05-04"
8
8
  evolved-by: "aprender"
9
- evolved-note: "3 secciones nuevas (regex multi-pattern al extender scope, tracer/replicador con marca SYNC, fixtures crudos vs condensados) + refinamiento de la sección caché content-addressable (sharding, bypass env var, key multi-dimensión, tests obligatorios). Preserva F401 en soft imports. Patrones avanzados extraídos a recursos/patrones-avanzados.md para respetar el límite de 300 líneas de SKILL.md."
9
+ evolved-note: "+1 gotcha: assert se elimina con PYTHONOPTIMIZE=1 usar if/raise para invariantes (sync desde global tras sesión SIGM Fase 5b)"
10
10
  herramientasPermitidas: [Read, Glob, Grep]
11
11
  exclusiones:
12
12
  - "No cargar para patrones de un framework específico (FastAPI, Django, Celery) — los idiomas generales de este skill aplican, pero los patrones de framework tienen restricciones adicionales; cargar el skill del framework correspondiente."
@@ -204,6 +204,7 @@ ver [recursos/referencia-completa.md](recursos/referencia-completa.md).
204
204
  - **Decorator que usa `functools.wraps` pero no preserva type hints si la función decorada tiene anotaciones genéricas**: el `@wraps` copia `__wrapped__`, `__doc__` y `__name__`, pero el tipo de retorno inferido por `mypy` es el del wrapper, no el del wrapped. Causa: `functools.wraps` no puede preservar el tipado estático del wrapped — mypy ve el tipo del wrapper `Callable[..., Any]`. Solución: usar `TypeVar` y tipado genérico en el decorator con `ParamSpec` (Python 3.10+): `P = ParamSpec('P'); T = TypeVar('T')` y tipar el wrapper como `Callable[P, T]`.
205
205
  - **`__slots__` en clase Python produce `TypeError: multiple bases have instance lay-out conflict`** al heredar de otra clase con `__slots__`: las subclases con `__slots__` requieren que todos los ancestros también tengan `__slots__`, o que el ancestro directo sea `object`. Causa: si `ClaseBase` no tiene `__slots__`, tiene un `__dict__` implícito; si `ClaseHija` tiene `__slots__`, hay conflicto de layout de memoria. Solución: o agregar `__slots__ = ()` vacío a la clase base, o eliminar `__slots__` de la subclase — no mezclar clases con y sin `__slots__` en la misma jerarquía.
206
206
  - **`property` setter que modifica un campo privado no refleja el cambio en `__repr__` generado por dataclass**: el `@property` en un dataclass crea un campo de clase que conflictúa con el campo de instancia del dataclass. Causa: `@dataclass` genera `__repr__` basado en los campos declarados en `__init__` — si el setter modifica un atributo con nombre diferente (ej: `_valor`), `__repr__` muestra el campo original sin la modificación. Solución: usar `field(init=False, repr=False)` para el campo interno y exponer solo la `property` en la interfaz pública.
207
+ - **`assert` no es guard de invariantes en producción con `PYTHONOPTIMIZE=1` o `python -O`**: el bytecode optimizado **elimina** todos los `assert` del módulo, por lo que `assert x is not None; return x` puede retornar `None` violando el contrato `-> dict` en producción aunque pase tests en desarrollo. El test runner por defecto NO usa `-O`, por lo que el bug es invisible hasta que alguien despliega con `PYTHONOPTIMIZE=1` (configuración común para reducir memoria en imágenes Docker production). Causa: `assert` está documentado en Python como herramienta de **debugging**, no de validación. Solución: para invariantes que DEBEN cumplirse en producción, usar guard explícito con raise: `if x is None: raise HTTPException(500, "Invariante violado")` o `if x is None: raise RuntimeError(...)`. Reservar `assert` solo para tests, scripts, o pre-condiciones triviales en código de desarrollo. Regla rápida: si el assert protege un caso que activa una respuesta del usuario o un side-effect, NO es assert — es validación y debe ser `if/raise`.
207
208
 
208
209
  ---
209
210