@saulwade/swl-ses 1.3.7 → 1.4.0
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 +12 -4
- package/README.md +1 -1
- package/bin/swl-mcp-server.js +187 -187
- package/bin/swl-webhook-server.js +198 -0
- package/comandos/swl/.evolved.json +22 -22
- package/comandos/swl/adoptar-proyecto.md +21 -1
- package/comandos/swl/claudemd.md +14 -1
- package/comandos/swl/contribuir.md +233 -233
- package/comandos/swl/exportar-vault.md +207 -7
- package/comandos/swl/nuevo-proyecto.md +24 -2
- package/gateway/adapters/base.js +109 -0
- package/gateway/adapters/discord.js +167 -0
- package/gateway/adapters/email.js +221 -0
- package/gateway/adapters/slack.js +192 -0
- package/gateway/adapters/telegram.js +183 -0
- package/gateway/adapters/webhook.js +113 -0
- package/gateway/adapters/whatsapp.js +214 -0
- package/gateway/agent-executor.js +322 -0
- package/gateway/command-relay.js +271 -0
- package/gateway/cron/jobs.js +263 -0
- package/gateway/cron/scheduler.js +322 -0
- package/gateway/cron/store.js +335 -0
- package/gateway/index.js +320 -0
- package/gateway/lib/event-channel.js +191 -0
- package/gateway/session.js +131 -0
- package/gateway/webhook-server.js +324 -0
- package/habilidades/backend-production-resilience/SKILL.md +288 -288
- package/habilidades/benchmark-memoria/SKILL.md +186 -186
- package/habilidades/build-errors-nextjs/SKILL.md +55 -1
- package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
- package/habilidades/doubt-driven-review/SKILL.md +171 -171
- package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
- package/habilidades/eval-framework/SKILL.md +212 -212
- package/habilidades/extractor-de-aprendizajes/SKILL.md +24 -10
- package/habilidades/harness-claude-code/SKILL.md +299 -299
- package/habilidades/infra-github-actions/SKILL.md +166 -166
- package/habilidades/legacy-code-rescue/SKILL.md +267 -267
- package/habilidades/manejo-errores/.evolved.json +8 -8
- package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
- package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
- package/habilidades/nextjs-testing/SKILL.md +89 -5
- package/habilidades/node-experto/SKILL.md +37 -1
- package/habilidades/patrones-python/SKILL.md +229 -229
- package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
- package/habilidades/planear-fase/SKILL.md +319 -319
- package/habilidades/react-experto/SKILL.md +45 -4
- package/habilidades/release-semver/.evolved.json +8 -8
- package/habilidades/swl-claudemd/SKILL.md +15 -1
- package/habilidades/tdd-workflow/SKILL.md +36 -4
- package/habilidades/testing-python/SKILL.md +340 -340
- package/hooks/claudemd-bloat-detector.js +161 -161
- package/hooks/inyeccion-contexto.js +8 -3
- package/hooks/lib/agent-routing.js +107 -107
- package/hooks/lib/auto-consolidator.js +335 -335
- package/hooks/lib/error-classifier.js +308 -308
- package/hooks/lib/merkle-audit.js +96 -96
- package/hooks/lib/provenance-tracker.js +191 -191
- package/hooks/lib/rate-limit-ip.js +177 -0
- package/hooks/lib/rate-limit-tracker.js +253 -253
- package/hooks/lib/resource-quota.js +122 -122
- package/hooks/lib/retry-jitter.js +165 -165
- package/hooks/lib/skill-auditor.js +588 -588
- package/hooks/lib/sync-status.js +228 -228
- package/hooks/lib/taint-tracker.js +107 -107
- package/hooks/lib/text-similarity.js +241 -241
- package/hooks/lib/toon-compressor.js +245 -245
- package/hooks/lib/webhook-dedup.js +184 -0
- package/hooks/lib/webhook-verify.js +123 -0
- package/hooks/proteccion-rutas.js +120 -15
- package/hooks/registro-turnos.js +209 -209
- package/hooks/sugerir-regenerar-inventario.js +170 -170
- package/hooks/validar-formato-post-subagente.js +140 -140
- package/hooks/validar-memoria-hook.js +218 -218
- package/instintos/prompt-appendices.yaml +57 -57
- package/manifiestos/agent-output-schemas.json +57 -57
- package/manifiestos/modulos.json +1 -0
- package/manifiestos/skills-lock.json +37 -37
- package/package.json +5 -3
- package/plantillas/auditor-veto-template.md +105 -105
- package/plantillas/github-workflows/README.md +47 -47
- package/plantillas/github-workflows/release-please.yml +44 -44
- package/plantillas/github-workflows/swl-ci.yml +107 -107
- package/plantillas/github-workflows/swl-security.yml +51 -51
- package/plugin.json +1 -1
- package/reglas/analisis-previo-tareas-grandes.md +172 -172
- package/reglas/arreglar-al-detectar.md +147 -147
- package/reglas/fragmentos-compartidos.md +152 -152
- package/reglas/harness-claude-code.md +213 -213
- package/reglas/usar-context7.md +226 -226
- package/reglas/usar-sistema-swl.md +251 -0
- package/schemas/diary-entry.schema.json +80 -80
- package/scripts/benchmark-memoria.js +167 -167
- package/scripts/comandos/skills.js +251 -2
- package/scripts/configurar-branch-protection.js +418 -418
- package/scripts/detectar-aprendizajes-duplicados.js +151 -151
- package/scripts/field-report.js +199 -199
- package/scripts/generar-checklists-consolidados.js +273 -273
- package/scripts/generar-inventario.js +420 -420
- package/scripts/generar-matriz-lenguajes.js +271 -271
- package/scripts/lib/artefactos-python.js +43 -43
- package/scripts/lib/benchmark-metrics.js +160 -160
- package/scripts/lib/budget-enforcer.js +252 -252
- package/scripts/lib/configurar-ci.js +380 -380
- package/scripts/lib/contadores-inventario.js +217 -217
- package/scripts/lib/detectar-stack-detallado.js +307 -307
- package/scripts/lib/diary-entry.js +234 -234
- package/scripts/lib/eval-metrics-store.js +218 -218
- package/scripts/lib/eval-quality.js +171 -171
- package/scripts/lib/eval-schemas.js +144 -144
- package/scripts/lib/eval-self-correct.js +106 -106
- package/scripts/lib/eval-validator.js +185 -185
- package/scripts/lib/jaccard-similarity.js +98 -98
- package/scripts/lib/longmemeval-runner.js +125 -125
- package/scripts/lib/npm-version.js +261 -261
- package/scripts/lib/paquetes-conocidos.js +50 -50
- package/scripts/lib/prompt-builder.js +264 -264
- package/scripts/lib/rrf-fusion.js +175 -175
- package/scripts/lib/scoring-instintos.js +277 -277
- package/scripts/lib/semantic-search.js +252 -252
- package/scripts/limpiar-artefactos-python.js +131 -131
- package/scripts/mcp-server/README.md +128 -128
- package/scripts/mcp-server/handlers.js +206 -206
- package/scripts/migrar-csv-a-array.js +168 -168
- package/scripts/migrar-fase-dominio.js +201 -201
- package/scripts/publicar.js +511 -511
- package/scripts/run-eval.js +141 -141
- package/scripts/validar-manifest.js +195 -195
- package/scripts/validar-userland-vacio.js +110 -110
- package/scripts/verificar-release.js +110 -0
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: node-experto
|
|
3
3
|
description: Node.js y TypeScript backend moderno. Cubre patterns de Express/Fastify/NestJS, error handling middleware, streams y buffers, worker threads y clustering, Prisma/Drizzle ORM, validación con Zod, graceful shutdown y anti-patrones críticos como callback hell y event loop blocking.
|
|
4
|
-
version: "1.0.
|
|
4
|
+
version: "1.0.1"
|
|
5
|
+
evolved: true
|
|
6
|
+
evolved-from: "1.0.0"
|
|
7
|
+
evolved-at: "2026-05-14"
|
|
8
|
+
evolved-by: "aprender"
|
|
9
|
+
evolved-note: "Gotcha req.destroy() vs req.pause() en handlers HTTP nativos — origen PR #13 webhook-server"
|
|
5
10
|
herramientasPermitidas: [Read, Write, Glob]
|
|
6
11
|
exclusiones:
|
|
7
12
|
- "No cargar para aplicaciones Next.js con App Router — Next.js tiene patrones de Server Components, SSR y caché que difieren del backend Node puro; cargar `nextjs-experto`."
|
|
@@ -264,3 +269,34 @@ if (require.main === module) {
|
|
|
264
269
|
**Zod `.parse()` vs `.safeParse()` en middleware**: usar `schema.parse(body)` en un middleware Express lanza una excepción no capturada si no hay un try/catch, y el error llega al cliente con stack trace completo. Fix: usar siempre `schema.safeParse(body)` en middleware y verificar `result.success` antes de continuar; pasar `result.error` al error handler con `next(result.error)`.
|
|
265
270
|
|
|
266
271
|
**`response.json()` sobre body vacío lanza `SyntaxError: Unexpected end of JSON input`**: cuando el servidor responde con 204 No Content, 205 Reset Content, o con `Content-Length: 0`, el body está vacío — `fetch(...).then(r => r.json())` falla en el cliente porque JSON.parse sobre string vacío lanza excepción. Causa: el contrato de fetch no verifica el status antes de parsear; el cliente asume que siempre hay JSON. Fix: verificar status y Content-Length antes de parsear: `if (r.status === 204 || r.status === 205 || r.headers.get('content-length') === '0') return null;`. Si el servidor FastAPI retorna 200 con body vacío por bug, considerar helper `await r.text()` antes y `JSON.parse` con try/catch — o mejor, devolver 204 explícito en endpoints que no retornan cuerpo.
|
|
272
|
+
|
|
273
|
+
**`req.destroy()` en un handler `http` cierra el socket antes de poder enviar la respuesta**: cuando se aborta la lectura del body (por ejemplo al exceder `maxPayloadBytes`), llamar `req.destroy()` en el listener `data` cierra el socket inmediatamente. El subsiguiente `res.writeHead(413); res.end()` del handler falla porque ya no hay socket — el cliente recibe `ECONNRESET` en lugar de 413. Causa: `destroy()` no espera al flush de la respuesta pendiente; equivale a un `RST` TCP. Fix: usar `req.pause()` + un flag `abortado = true` + rechazar la promesa de lectura; dejar que el handler envíe `res.writeHead(413); res.end(...)` normalmente. Node cierra el socket por sí solo tras `res.end()`. Aplica a servidores HTTP nativos sin Express/Fastify, donde el control del body es manual.
|
|
274
|
+
|
|
275
|
+
```js
|
|
276
|
+
// MAL — ECONNRESET en cliente, nunca recibe 413
|
|
277
|
+
req.on('data', chunk => {
|
|
278
|
+
total += chunk.length;
|
|
279
|
+
if (total > MAX) {
|
|
280
|
+
req.destroy(); // ← cierra socket. res.end() después es no-op
|
|
281
|
+
return reject(new Error('PAYLOAD_TOO_LARGE'));
|
|
282
|
+
}
|
|
283
|
+
chunks.push(chunk);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// BIEN — cliente recibe 413 con body JSON
|
|
287
|
+
let abortado = false;
|
|
288
|
+
req.on('data', chunk => {
|
|
289
|
+
if (abortado) return;
|
|
290
|
+
total += chunk.length;
|
|
291
|
+
if (total > MAX) {
|
|
292
|
+
abortado = true;
|
|
293
|
+
req.pause(); // ← TCP backpressure, socket vivo
|
|
294
|
+
return reject(new Error('PAYLOAD_TOO_LARGE'));
|
|
295
|
+
}
|
|
296
|
+
chunks.push(chunk);
|
|
297
|
+
});
|
|
298
|
+
req.on('end', () => { if (!abortado) resolve(Buffer.concat(chunks)); });
|
|
299
|
+
req.on('error', err => { if (!abortado) reject(err); });
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Origen: PR #13 sesión 2026-05-13, test de payload-too-large en `gateway/webhook-server.js`.
|
|
@@ -1,229 +1,229 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: patrones-python
|
|
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.1"
|
|
5
|
-
evolved: true
|
|
6
|
-
evolved-from: "1.3.0"
|
|
7
|
-
evolved-at: "2026-05-04"
|
|
8
|
-
evolved-by: "aprender"
|
|
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
|
-
herramientasPermitidas: [Read, Glob, Grep]
|
|
11
|
-
exclusiones:
|
|
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."
|
|
13
|
-
- "No cargar para errores de instalación o dependencias Python — si el problema es pip, virtualenv o incompatibilidad de versiones, cargar `build-errors-python`."
|
|
14
|
-
- "No cargar para patrones de concurrencia avanzados (TaskGroup, semáforos, event loops) — para esos cargar `async-python` que cubre asyncio en profundidad."
|
|
15
|
-
- "No cargar para testing pytest (fixtures, mocking, cobertura) — ese dominio está en `testing-python` que cubre la toolchain completa de tests."
|
|
16
|
-
evolvable: true # default para skill estandar
|
|
17
|
-
---
|
|
18
|
-
# Patrones Python
|
|
19
|
-
|
|
20
|
-
## Cuándo NO cargar
|
|
21
|
-
|
|
22
|
-
- La tarea es específica de FastAPI o Django — cargar el skill del framework que tiene restricciones adicionales a los patrones generales.
|
|
23
|
-
- El problema es de instalación de dependencias Python — cargar `build-errors-python`.
|
|
24
|
-
- La pregunta es sobre concurrencia avanzada con asyncio — cargar `async-python`.
|
|
25
|
-
|
|
26
|
-
## PEP 8 — Convenciones de estilo
|
|
27
|
-
|
|
28
|
-
### Nomenclatura
|
|
29
|
-
|
|
30
|
-
```python
|
|
31
|
-
# Módulos y paquetes: snake_case minúsculas
|
|
32
|
-
import mi_modulo
|
|
33
|
-
|
|
34
|
-
# Clases: PascalCase
|
|
35
|
-
class FacturaElectronica:
|
|
36
|
-
pass
|
|
37
|
-
|
|
38
|
-
# Funciones y variables: snake_case
|
|
39
|
-
def calcular_iva(monto: float) -> float:
|
|
40
|
-
tasa_iva = 0.16
|
|
41
|
-
return monto * tasa_iva
|
|
42
|
-
|
|
43
|
-
# Constantes: UPPER_SNAKE_CASE
|
|
44
|
-
MAX_REINTENTOS = 3
|
|
45
|
-
URL_BASE_API = "https://api.ejemplo.com/v1"
|
|
46
|
-
|
|
47
|
-
# Variables privadas de instancia: prefijo _
|
|
48
|
-
class Factura:
|
|
49
|
-
def __init__(self):
|
|
50
|
-
self._numero_interno = None # privado por convención
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Longitud de línea y formato
|
|
54
|
-
|
|
55
|
-
```python
|
|
56
|
-
# Máximo 88 caracteres (configuración Black estándar)
|
|
57
|
-
|
|
58
|
-
# Importaciones agrupadas:
|
|
59
|
-
# 1. Stdlib
|
|
60
|
-
import os
|
|
61
|
-
from pathlib import Path
|
|
62
|
-
|
|
63
|
-
# 2. Terceros
|
|
64
|
-
from fastapi import FastAPI, Depends
|
|
65
|
-
from sqlalchemy.orm import Session
|
|
66
|
-
|
|
67
|
-
# 3. Módulos locales
|
|
68
|
-
from app.models.usuario import Usuario
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
---
|
|
72
|
-
|
|
73
|
-
## Type Hints modernos (Python 3.10+)
|
|
74
|
-
|
|
75
|
-
```python
|
|
76
|
-
# Union con | en vez de Union[X, Y]
|
|
77
|
-
def procesar(valor: int | str | None) -> str:
|
|
78
|
-
return str(valor or "")
|
|
79
|
-
|
|
80
|
-
# list, dict, tuple nativos (no de typing)
|
|
81
|
-
def filtrar(items: list[str], lookup: dict[str, int]) -> list[str]:
|
|
82
|
-
return [i for i in items if i in lookup]
|
|
83
|
-
|
|
84
|
-
# TypeAlias explícito
|
|
85
|
-
type JSON = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None
|
|
86
|
-
|
|
87
|
-
# ParamSpec para decoradores que preservan firma
|
|
88
|
-
from typing import ParamSpec, Callable
|
|
89
|
-
P = ParamSpec("P")
|
|
90
|
-
|
|
91
|
-
def con_log(fn: Callable[P, T]) -> Callable[P, T]:
|
|
92
|
-
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
93
|
-
print(f"Llamando {fn.__name__}")
|
|
94
|
-
return fn(*args, **kwargs)
|
|
95
|
-
return wrapper
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
## Dataclasses
|
|
101
|
-
|
|
102
|
-
```python
|
|
103
|
-
from dataclasses import dataclass, field
|
|
104
|
-
from datetime import datetime
|
|
105
|
-
|
|
106
|
-
@dataclass
|
|
107
|
-
class Producto:
|
|
108
|
-
id: str
|
|
109
|
-
nombre: str
|
|
110
|
-
precio: float
|
|
111
|
-
activo: bool = True
|
|
112
|
-
etiquetas: list[str] = field(default_factory=list)
|
|
113
|
-
|
|
114
|
-
def __post_init__(self) -> None:
|
|
115
|
-
if self.precio < 0:
|
|
116
|
-
raise ValueError(f"Precio no puede ser negativo: {self.precio}")
|
|
117
|
-
self.nombre = self.nombre.strip()
|
|
118
|
-
|
|
119
|
-
# Dataclass inmutable
|
|
120
|
-
@dataclass(frozen=True)
|
|
121
|
-
class Coordenada:
|
|
122
|
-
latitud: float
|
|
123
|
-
longitud: float
|
|
124
|
-
|
|
125
|
-
# Dataclass con slots (Python 3.10+) — ahorra memoria
|
|
126
|
-
@dataclass(slots=True)
|
|
127
|
-
class EventoAuditoria:
|
|
128
|
-
usuario_id: str
|
|
129
|
-
accion: str
|
|
130
|
-
timestamp: datetime
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
---
|
|
134
|
-
|
|
135
|
-
## Context Managers
|
|
136
|
-
|
|
137
|
-
```python
|
|
138
|
-
from contextlib import contextmanager, asynccontextmanager
|
|
139
|
-
|
|
140
|
-
# Context manager con generador
|
|
141
|
-
@contextmanager
|
|
142
|
-
def medir_tiempo(nombre: str):
|
|
143
|
-
inicio = time.perf_counter()
|
|
144
|
-
try:
|
|
145
|
-
yield
|
|
146
|
-
finally:
|
|
147
|
-
transcurrido = time.perf_counter() - inicio
|
|
148
|
-
print(f"{nombre}: {transcurrido:.4f}s")
|
|
149
|
-
|
|
150
|
-
# Context manager async
|
|
151
|
-
@asynccontextmanager
|
|
152
|
-
async def sesion_db(engine):
|
|
153
|
-
async with engine.begin() as conn:
|
|
154
|
-
try:
|
|
155
|
-
yield conn
|
|
156
|
-
await conn.commit()
|
|
157
|
-
except Exception:
|
|
158
|
-
await conn.rollback()
|
|
159
|
-
raise
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
---
|
|
163
|
-
|
|
164
|
-
## Anti-patrones Python Críticos
|
|
165
|
-
|
|
166
|
-
```python
|
|
167
|
-
# MAL: argumento mutable como default
|
|
168
|
-
def agregar(item, lista=[]): # La lista persiste entre llamadas
|
|
169
|
-
lista.append(item)
|
|
170
|
-
|
|
171
|
-
# BIEN: usar None como centinela
|
|
172
|
-
def agregar(item, lista=None):
|
|
173
|
-
if lista is None:
|
|
174
|
-
lista = []
|
|
175
|
-
lista.append(item)
|
|
176
|
-
|
|
177
|
-
# MAL: bare except
|
|
178
|
-
try:
|
|
179
|
-
operacion_riesgosa()
|
|
180
|
-
except:
|
|
181
|
-
pass # silencia KeyboardInterrupt y SystemExit
|
|
182
|
-
|
|
183
|
-
# BIEN: excepción específica
|
|
184
|
-
try:
|
|
185
|
-
operacion_riesgosa()
|
|
186
|
-
except ValueError as e:
|
|
187
|
-
logger.error(f"Valor inválido: {e}")
|
|
188
|
-
|
|
189
|
-
# MAL: concatenación en loop (O(n²))
|
|
190
|
-
resultado = ""
|
|
191
|
-
for parte in partes:
|
|
192
|
-
resultado += parte
|
|
193
|
-
|
|
194
|
-
# BIEN: join (O(n))
|
|
195
|
-
resultado = "".join(partes)
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
Para generators, decoradores avanzados, async/await y manejo de errores idiomático,
|
|
199
|
-
ver [recursos/referencia-completa.md](recursos/referencia-completa.md).
|
|
200
|
-
|
|
201
|
-
## Gotchas / Errores comunes no obvios
|
|
202
|
-
|
|
203
|
-
- **`dataclass` con campo mutable como default (`field: list = []`) produce un objeto compartido entre todas las instancias**: Python reutiliza el objeto por defecto en vez de crear uno nuevo. Causa: los defaults de dataclass se evalúan en tiempo de definición de la clase, no en cada instanciación — una lista mutable como default es el mismo objeto en memoria para todas las instancias. Solución: usar `field(default_factory=list)` para campos mutables: `from dataclasses import field; tags: list = field(default_factory=list)`.
|
|
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
|
-
- **`__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
|
-
- **`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`.
|
|
208
|
-
|
|
209
|
-
---
|
|
210
|
-
|
|
211
|
-
## Patrones avanzados (carga bajo demanda)
|
|
212
|
-
|
|
213
|
-
Los siguientes patrones aparecen al integrar Python con pipelines reales
|
|
214
|
-
(conversores de documentos, clientes heterogéneos, cachés determinísticos,
|
|
215
|
-
detectores de texto, duplicación deliberada de lógica). El detalle completo
|
|
216
|
-
está en [recursos/patrones-avanzados.md](recursos/patrones-avanzados.md).
|
|
217
|
-
|
|
218
|
-
| # | Patrón | Cargar cuando |
|
|
219
|
-
|---|--------|---------------|
|
|
220
|
-
| 1 | Normalizadores: colapsar al formato canónico del PROYECTO | escribir `_normalizar(texto)` después de un conversor externo (MarkItDown, Pandoc, mammoth) |
|
|
221
|
-
| 2 | kwargs opcionales entre clientes hermanos: try/except TypeError | propagar un kwarg nuevo a clientes con signature heterogénea sin romper la interfaz |
|
|
222
|
-
| 3 | Caché content-addressable por SHA256 | función pura costosa (>1 s) que se invoca repetidamente con los mismos inputs |
|
|
223
|
-
| 4 | F401 en archivos con soft imports es intencional | módulo importa una dependencia opcional dentro de `try/except ImportError` |
|
|
224
|
-
| 5 | Detectores regex multi-pattern: extender scope sin refinar genera FP | un detector con OR de patterns se va a aplicar a un texto más ruidoso que el original |
|
|
225
|
-
| 6 | Tracer/replicador paralelo del motor: marca SYNC obligatoria | duplicar lógica del motor para telemetría, explainability o shadow runs |
|
|
226
|
-
| 7 | Fixtures con datos pre-procesados NO ejercen el path crudo | el motor prefiere leer un campo crudo opcional sobre uno condensado, y los fixtures son condensados |
|
|
227
|
-
|
|
228
|
-
Cargar la sección específica del recurso cuando aplique al problema en mano —
|
|
229
|
-
no cargar el archivo completo si solo se necesita un patrón.
|
|
1
|
+
---
|
|
2
|
+
name: patrones-python
|
|
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.1"
|
|
5
|
+
evolved: true
|
|
6
|
+
evolved-from: "1.3.0"
|
|
7
|
+
evolved-at: "2026-05-04"
|
|
8
|
+
evolved-by: "aprender"
|
|
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
|
+
herramientasPermitidas: [Read, Glob, Grep]
|
|
11
|
+
exclusiones:
|
|
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."
|
|
13
|
+
- "No cargar para errores de instalación o dependencias Python — si el problema es pip, virtualenv o incompatibilidad de versiones, cargar `build-errors-python`."
|
|
14
|
+
- "No cargar para patrones de concurrencia avanzados (TaskGroup, semáforos, event loops) — para esos cargar `async-python` que cubre asyncio en profundidad."
|
|
15
|
+
- "No cargar para testing pytest (fixtures, mocking, cobertura) — ese dominio está en `testing-python` que cubre la toolchain completa de tests."
|
|
16
|
+
evolvable: true # default para skill estandar
|
|
17
|
+
---
|
|
18
|
+
# Patrones Python
|
|
19
|
+
|
|
20
|
+
## Cuándo NO cargar
|
|
21
|
+
|
|
22
|
+
- La tarea es específica de FastAPI o Django — cargar el skill del framework que tiene restricciones adicionales a los patrones generales.
|
|
23
|
+
- El problema es de instalación de dependencias Python — cargar `build-errors-python`.
|
|
24
|
+
- La pregunta es sobre concurrencia avanzada con asyncio — cargar `async-python`.
|
|
25
|
+
|
|
26
|
+
## PEP 8 — Convenciones de estilo
|
|
27
|
+
|
|
28
|
+
### Nomenclatura
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
# Módulos y paquetes: snake_case minúsculas
|
|
32
|
+
import mi_modulo
|
|
33
|
+
|
|
34
|
+
# Clases: PascalCase
|
|
35
|
+
class FacturaElectronica:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
# Funciones y variables: snake_case
|
|
39
|
+
def calcular_iva(monto: float) -> float:
|
|
40
|
+
tasa_iva = 0.16
|
|
41
|
+
return monto * tasa_iva
|
|
42
|
+
|
|
43
|
+
# Constantes: UPPER_SNAKE_CASE
|
|
44
|
+
MAX_REINTENTOS = 3
|
|
45
|
+
URL_BASE_API = "https://api.ejemplo.com/v1"
|
|
46
|
+
|
|
47
|
+
# Variables privadas de instancia: prefijo _
|
|
48
|
+
class Factura:
|
|
49
|
+
def __init__(self):
|
|
50
|
+
self._numero_interno = None # privado por convención
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Longitud de línea y formato
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
# Máximo 88 caracteres (configuración Black estándar)
|
|
57
|
+
|
|
58
|
+
# Importaciones agrupadas:
|
|
59
|
+
# 1. Stdlib
|
|
60
|
+
import os
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
|
|
63
|
+
# 2. Terceros
|
|
64
|
+
from fastapi import FastAPI, Depends
|
|
65
|
+
from sqlalchemy.orm import Session
|
|
66
|
+
|
|
67
|
+
# 3. Módulos locales
|
|
68
|
+
from app.models.usuario import Usuario
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Type Hints modernos (Python 3.10+)
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# Union con | en vez de Union[X, Y]
|
|
77
|
+
def procesar(valor: int | str | None) -> str:
|
|
78
|
+
return str(valor or "")
|
|
79
|
+
|
|
80
|
+
# list, dict, tuple nativos (no de typing)
|
|
81
|
+
def filtrar(items: list[str], lookup: dict[str, int]) -> list[str]:
|
|
82
|
+
return [i for i in items if i in lookup]
|
|
83
|
+
|
|
84
|
+
# TypeAlias explícito
|
|
85
|
+
type JSON = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None
|
|
86
|
+
|
|
87
|
+
# ParamSpec para decoradores que preservan firma
|
|
88
|
+
from typing import ParamSpec, Callable
|
|
89
|
+
P = ParamSpec("P")
|
|
90
|
+
|
|
91
|
+
def con_log(fn: Callable[P, T]) -> Callable[P, T]:
|
|
92
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
93
|
+
print(f"Llamando {fn.__name__}")
|
|
94
|
+
return fn(*args, **kwargs)
|
|
95
|
+
return wrapper
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Dataclasses
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from dataclasses import dataclass, field
|
|
104
|
+
from datetime import datetime
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class Producto:
|
|
108
|
+
id: str
|
|
109
|
+
nombre: str
|
|
110
|
+
precio: float
|
|
111
|
+
activo: bool = True
|
|
112
|
+
etiquetas: list[str] = field(default_factory=list)
|
|
113
|
+
|
|
114
|
+
def __post_init__(self) -> None:
|
|
115
|
+
if self.precio < 0:
|
|
116
|
+
raise ValueError(f"Precio no puede ser negativo: {self.precio}")
|
|
117
|
+
self.nombre = self.nombre.strip()
|
|
118
|
+
|
|
119
|
+
# Dataclass inmutable
|
|
120
|
+
@dataclass(frozen=True)
|
|
121
|
+
class Coordenada:
|
|
122
|
+
latitud: float
|
|
123
|
+
longitud: float
|
|
124
|
+
|
|
125
|
+
# Dataclass con slots (Python 3.10+) — ahorra memoria
|
|
126
|
+
@dataclass(slots=True)
|
|
127
|
+
class EventoAuditoria:
|
|
128
|
+
usuario_id: str
|
|
129
|
+
accion: str
|
|
130
|
+
timestamp: datetime
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Context Managers
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from contextlib import contextmanager, asynccontextmanager
|
|
139
|
+
|
|
140
|
+
# Context manager con generador
|
|
141
|
+
@contextmanager
|
|
142
|
+
def medir_tiempo(nombre: str):
|
|
143
|
+
inicio = time.perf_counter()
|
|
144
|
+
try:
|
|
145
|
+
yield
|
|
146
|
+
finally:
|
|
147
|
+
transcurrido = time.perf_counter() - inicio
|
|
148
|
+
print(f"{nombre}: {transcurrido:.4f}s")
|
|
149
|
+
|
|
150
|
+
# Context manager async
|
|
151
|
+
@asynccontextmanager
|
|
152
|
+
async def sesion_db(engine):
|
|
153
|
+
async with engine.begin() as conn:
|
|
154
|
+
try:
|
|
155
|
+
yield conn
|
|
156
|
+
await conn.commit()
|
|
157
|
+
except Exception:
|
|
158
|
+
await conn.rollback()
|
|
159
|
+
raise
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Anti-patrones Python Críticos
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
# MAL: argumento mutable como default
|
|
168
|
+
def agregar(item, lista=[]): # La lista persiste entre llamadas
|
|
169
|
+
lista.append(item)
|
|
170
|
+
|
|
171
|
+
# BIEN: usar None como centinela
|
|
172
|
+
def agregar(item, lista=None):
|
|
173
|
+
if lista is None:
|
|
174
|
+
lista = []
|
|
175
|
+
lista.append(item)
|
|
176
|
+
|
|
177
|
+
# MAL: bare except
|
|
178
|
+
try:
|
|
179
|
+
operacion_riesgosa()
|
|
180
|
+
except:
|
|
181
|
+
pass # silencia KeyboardInterrupt y SystemExit
|
|
182
|
+
|
|
183
|
+
# BIEN: excepción específica
|
|
184
|
+
try:
|
|
185
|
+
operacion_riesgosa()
|
|
186
|
+
except ValueError as e:
|
|
187
|
+
logger.error(f"Valor inválido: {e}")
|
|
188
|
+
|
|
189
|
+
# MAL: concatenación en loop (O(n²))
|
|
190
|
+
resultado = ""
|
|
191
|
+
for parte in partes:
|
|
192
|
+
resultado += parte
|
|
193
|
+
|
|
194
|
+
# BIEN: join (O(n))
|
|
195
|
+
resultado = "".join(partes)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Para generators, decoradores avanzados, async/await y manejo de errores idiomático,
|
|
199
|
+
ver [recursos/referencia-completa.md](recursos/referencia-completa.md).
|
|
200
|
+
|
|
201
|
+
## Gotchas / Errores comunes no obvios
|
|
202
|
+
|
|
203
|
+
- **`dataclass` con campo mutable como default (`field: list = []`) produce un objeto compartido entre todas las instancias**: Python reutiliza el objeto por defecto en vez de crear uno nuevo. Causa: los defaults de dataclass se evalúan en tiempo de definición de la clase, no en cada instanciación — una lista mutable como default es el mismo objeto en memoria para todas las instancias. Solución: usar `field(default_factory=list)` para campos mutables: `from dataclasses import field; tags: list = field(default_factory=list)`.
|
|
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
|
+
- **`__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
|
+
- **`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`.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Patrones avanzados (carga bajo demanda)
|
|
212
|
+
|
|
213
|
+
Los siguientes patrones aparecen al integrar Python con pipelines reales
|
|
214
|
+
(conversores de documentos, clientes heterogéneos, cachés determinísticos,
|
|
215
|
+
detectores de texto, duplicación deliberada de lógica). El detalle completo
|
|
216
|
+
está en [recursos/patrones-avanzados.md](recursos/patrones-avanzados.md).
|
|
217
|
+
|
|
218
|
+
| # | Patrón | Cargar cuando |
|
|
219
|
+
|---|--------|---------------|
|
|
220
|
+
| 1 | Normalizadores: colapsar al formato canónico del PROYECTO | escribir `_normalizar(texto)` después de un conversor externo (MarkItDown, Pandoc, mammoth) |
|
|
221
|
+
| 2 | kwargs opcionales entre clientes hermanos: try/except TypeError | propagar un kwarg nuevo a clientes con signature heterogénea sin romper la interfaz |
|
|
222
|
+
| 3 | Caché content-addressable por SHA256 | función pura costosa (>1 s) que se invoca repetidamente con los mismos inputs |
|
|
223
|
+
| 4 | F401 en archivos con soft imports es intencional | módulo importa una dependencia opcional dentro de `try/except ImportError` |
|
|
224
|
+
| 5 | Detectores regex multi-pattern: extender scope sin refinar genera FP | un detector con OR de patterns se va a aplicar a un texto más ruidoso que el original |
|
|
225
|
+
| 6 | Tracer/replicador paralelo del motor: marca SYNC obligatoria | duplicar lógica del motor para telemetría, explainability o shadow runs |
|
|
226
|
+
| 7 | Fixtures con datos pre-procesados NO ejercen el path crudo | el motor prefiere leer un campo crudo opcional sobre uno condensado, y los fixtures son condensados |
|
|
227
|
+
|
|
228
|
+
Cargar la sección específica del recurso cuando aplique al problema en mano —
|
|
229
|
+
no cargar el archivo completo si solo se necesita un patrón.
|