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,580 @@
1
+ ---
2
+ name: event-driven
3
+ description: Arquitectura event-driven. Message brokers RabbitMQ y Kafka, event sourcing, CQRS, pub/sub, event schemas, idempotency, dead letter queues, retry strategies, eventual consistency. Con ejemplos Python.
4
+ ---
5
+
6
+ # Arquitectura Orientada a Eventos
7
+
8
+ Los sistemas event-driven desacoplan productores de consumidores, permitiendo escalar
9
+ componentes independientemente y resilir ante fallos. Este skill cubre desde pub/sub
10
+ básico hasta event sourcing avanzado.
11
+
12
+ ---
13
+
14
+ ## 1. Cuándo Usar Event-Driven
15
+
16
+ ```
17
+ USAR cuando:
18
+ - Múltiples consumidores necesitan reaccionar al mismo evento
19
+ - El productor no necesita respuesta inmediata del consumidor
20
+ - Necesitas escalar productores y consumidores independientemente
21
+ - Quieres desacoplar servicios que hoy están fuertemente acoplados
22
+ - Necesitas auditoría completa de cambios (event log = audit log)
23
+
24
+ NO USAR cuando:
25
+ - Necesitas respuesta síncrona inmediata para el usuario
26
+ - El flujo es simple y lineal (no hay múltiples consumidores)
27
+ - El equipo no tiene experiencia operando brokers de mensajes
28
+ - La consistencia eventual no es aceptable para el dominio
29
+ ```
30
+
31
+ ---
32
+
33
+ ## 2. RabbitMQ — Pub/Sub con Python
34
+
35
+ ```python
36
+ # pip install aio-pika
37
+ import aio_pika
38
+ import asyncio
39
+ import json
40
+ from dataclasses import dataclass, asdict
41
+ from datetime import datetime, timezone
42
+ from typing import Any, Callable, Awaitable
43
+ import uuid
44
+
45
+ @dataclass
46
+ class Evento:
47
+ tipo: str
48
+ payload: dict
49
+ id: str = ""
50
+ timestamp: str = ""
51
+ correlation_id: str = ""
52
+ version: str = "1.0"
53
+
54
+ def __post_init__(self):
55
+ if not self.id:
56
+ self.id = str(uuid.uuid4())
57
+ if not self.timestamp:
58
+ self.timestamp = datetime.now(timezone.utc).isoformat()
59
+
60
+ class BrokerEventos:
61
+ """Wrapper sobre aio_pika para publicar y consumir eventos."""
62
+
63
+ def __init__(self, url: str):
64
+ self._url = url
65
+ self._conexion: aio_pika.RobustConnection | None = None
66
+ self._canal: aio_pika.abc.AbstractChannel | None = None
67
+
68
+ async def conectar(self) -> None:
69
+ self._conexion = await aio_pika.connect_robust(
70
+ self._url,
71
+ heartbeat=60,
72
+ reconnect_interval=5.0,
73
+ )
74
+ self._canal = await self._conexion.channel()
75
+ await self._canal.set_qos(prefetch_count=10) # Procesar 10 mensajes a la vez
76
+
77
+ async def publicar(
78
+ self,
79
+ evento: Evento,
80
+ exchange: str = "eventos.rrhh",
81
+ routing_key: str | None = None,
82
+ ) -> None:
83
+ """Publicar evento al exchange con routing key = tipo del evento."""
84
+ assert self._canal, "Llamar connect() primero"
85
+
86
+ exchange_obj = await self._canal.declare_exchange(
87
+ exchange,
88
+ aio_pika.ExchangeType.TOPIC,
89
+ durable=True,
90
+ )
91
+
92
+ await exchange_obj.publish(
93
+ aio_pika.Message(
94
+ body=json.dumps(asdict(evento), ensure_ascii=False).encode(),
95
+ content_type="application/json",
96
+ delivery_mode=aio_pika.DeliveryMode.PERSISTENT, # Sobrevive restart
97
+ message_id=evento.id,
98
+ timestamp=datetime.now(timezone.utc),
99
+ headers={
100
+ "x-correlation-id": evento.correlation_id,
101
+ "x-event-version": evento.version,
102
+ },
103
+ ),
104
+ routing_key=routing_key or evento.tipo, # "empleado.creado"
105
+ )
106
+
107
+ async def suscribir(
108
+ self,
109
+ cola: str,
110
+ exchange: str,
111
+ routing_keys: list[str],
112
+ handler: Callable[[Evento], Awaitable[None]],
113
+ ) -> None:
114
+ """Suscribir a eventos con manejo automático de DLQ."""
115
+ assert self._canal
116
+
117
+ # Dead Letter Queue — eventos que fallan tras N reintentos
118
+ dlq_nombre = f"{cola}.dlq"
119
+ await self._canal.declare_queue(
120
+ dlq_nombre,
121
+ durable=True,
122
+ )
123
+
124
+ # Cola principal con referencia a DLQ
125
+ queue = await self._canal.declare_queue(
126
+ cola,
127
+ durable=True,
128
+ arguments={
129
+ "x-dead-letter-exchange": "",
130
+ "x-dead-letter-routing-key": dlq_nombre,
131
+ "x-message-ttl": 86400000, # 24h en ms
132
+ },
133
+ )
134
+
135
+ exchange_obj = await self._canal.declare_exchange(
136
+ exchange, aio_pika.ExchangeType.TOPIC, durable=True
137
+ )
138
+
139
+ for routing_key in routing_keys:
140
+ await queue.bind(exchange_obj, routing_key=routing_key)
141
+
142
+ async def procesar_mensaje(mensaje: aio_pika.IncomingMessage) -> None:
143
+ async with mensaje.process(requeue=False): # requeue=False → DLQ al fallar
144
+ try:
145
+ datos = json.loads(mensaje.body)
146
+ evento = Evento(**datos)
147
+ await handler(evento)
148
+ except Exception as e:
149
+ logger.error("Error procesando evento %s: %s", mensaje.message_id, e)
150
+ raise # Enviar a DLQ
151
+
152
+ await queue.consume(procesar_mensaje)
153
+ ```
154
+
155
+ ---
156
+
157
+ ## 3. Kafka — Streaming de Alta Escala
158
+
159
+ ```python
160
+ # pip install aiokafka
161
+ from aiokafka import AIOKafkaProducer, AIOKafkaConsumer
162
+ from aiokafka.errors import KafkaConnectionError
163
+ import asyncio
164
+ import json
165
+
166
+ class ProductorKafka:
167
+ """Productor Kafka idempotente con reintentos."""
168
+
169
+ def __init__(self, bootstrap_servers: str):
170
+ self._producer = AIOKafkaProducer(
171
+ bootstrap_servers=bootstrap_servers,
172
+ enable_idempotence=True, # Garantiza exactamente-una-vez
173
+ max_batch_size=65536, # 64KB por batch
174
+ compression_type="gzip",
175
+ value_serializer=lambda v: json.dumps(v, ensure_ascii=False).encode(),
176
+ key_serializer=lambda k: k.encode() if k else None,
177
+ )
178
+
179
+ async def __aenter__(self) -> "ProductorKafka":
180
+ await self._producer.start()
181
+ return self
182
+
183
+ async def __aexit__(self, *args: Any) -> None:
184
+ await self._producer.stop()
185
+
186
+ async def enviar(
187
+ self,
188
+ topico: str,
189
+ evento: dict,
190
+ clave: str | None = None, # Misma clave → mismo partition → orden garantizado
191
+ ) -> None:
192
+ await self._producer.send_and_wait(
193
+ topico,
194
+ value=evento,
195
+ key=clave,
196
+ headers=[
197
+ ("content-type", b"application/json"),
198
+ ("event-id", evento.get("id", "").encode()),
199
+ ],
200
+ )
201
+
202
+ class ConsumidorKafka:
203
+ """Consumidor Kafka con manejo de errores y commit manual."""
204
+
205
+ def __init__(self, bootstrap_servers: str, group_id: str, topicos: list[str]):
206
+ self._consumer = AIOKafkaConsumer(
207
+ *topicos,
208
+ bootstrap_servers=bootstrap_servers,
209
+ group_id=group_id,
210
+ auto_offset_reset="earliest",
211
+ enable_auto_commit=False, # Commit manual después de procesar
212
+ max_poll_records=100,
213
+ session_timeout_ms=30000,
214
+ )
215
+
216
+ async def consumir(
217
+ self,
218
+ handler: Callable[[dict], Awaitable[None]],
219
+ ) -> None:
220
+ await self._consumer.start()
221
+ try:
222
+ async for mensaje in self._consumer:
223
+ try:
224
+ datos = json.loads(mensaje.value)
225
+ await handler(datos)
226
+ await self._consumer.commit() # Commit SOLO si procesó exitosamente
227
+ except Exception as e:
228
+ logger.error(
229
+ "Error en topico=%s partition=%s offset=%s: %s",
230
+ mensaje.topic, mensaje.partition, mensaje.offset, e,
231
+ )
232
+ # Dependiendo de la política: commit igualmente (skip) o no (retry)
233
+ finally:
234
+ await self._consumer.stop()
235
+ ```
236
+
237
+ ---
238
+
239
+ ## 4. Schemas de Eventos — Contrato entre servicios
240
+
241
+ ```python
242
+ # SIEMPRE definir un schema explícito para cada tipo de evento
243
+ # Los schemas son el contrato entre productor y consumidores
244
+
245
+ from pydantic import BaseModel, field_validator
246
+ from datetime import datetime
247
+ from typing import Literal
248
+ import uuid
249
+
250
+ class EventoBase(BaseModel):
251
+ """Base para todos los eventos del sistema."""
252
+ id: str = ""
253
+ timestamp: datetime = None
254
+ version: str = "1.0"
255
+ correlation_id: str = ""
256
+ origen: str # Nombre del servicio que genera el evento
257
+
258
+ def model_post_init(self, *args):
259
+ if not self.id:
260
+ self.id = str(uuid.uuid4())
261
+ if not self.timestamp:
262
+ self.timestamp = datetime.now(timezone.utc)
263
+
264
+ # Evento de dominio: empleado.creado
265
+ class EmpleadoCreadoPayload(BaseModel):
266
+ empleado_id: str
267
+ numero_empleado: str
268
+ nombre_completo: str
269
+ email_corporativo: str
270
+ departamento_id: str
271
+ puesto: str
272
+ fecha_ingreso: str # ISO 8601
273
+
274
+ class EventoEmpleadoCreado(EventoBase):
275
+ tipo: Literal["empleado.creado"] = "empleado.creado"
276
+ payload: EmpleadoCreadoPayload
277
+
278
+ # Versioning de schemas — NUNCA romper compatibilidad hacia atrás
279
+ class EmpleadoCreadoPayloadV2(EmpleadoCreadoPayload):
280
+ """v2 agrega campos opcionales — compatible con consumidores de v1."""
281
+ nss: str | None = None # Nuevo campo opcional
282
+ curp: str | None = None
283
+
284
+ class EventoEmpleadoCreadoV2(EventoBase):
285
+ tipo: Literal["empleado.creado"] = "empleado.creado"
286
+ version: str = "2.0"
287
+ payload: EmpleadoCreadoPayloadV2
288
+ ```
289
+
290
+ ---
291
+
292
+ ## 5. Idempotency — Procesar exactamente una vez
293
+
294
+ ```python
295
+ import hashlib
296
+ from datetime import datetime, timezone
297
+
298
+ class RegistroIdempotencia:
299
+ """Previene procesamiento duplicado de eventos."""
300
+
301
+ def __init__(self, redis_client):
302
+ self._redis = redis_client
303
+ self._ttl = 86400 # 24 horas
304
+
305
+ async def ya_procesado(self, evento_id: str, consumer_id: str) -> bool:
306
+ clave = f"idempotencia:{consumer_id}:{evento_id}"
307
+ return bool(await self._redis.exists(clave))
308
+
309
+ async def marcar_procesado(self, evento_id: str, consumer_id: str) -> None:
310
+ clave = f"idempotencia:{consumer_id}:{evento_id}"
311
+ await self._redis.setex(clave, self._ttl, "1")
312
+
313
+ # Uso en handler de eventos
314
+ class HandlerNuevaAlta:
315
+ def __init__(self, repo, idempotencia: RegistroIdempotencia):
316
+ self._repo = repo
317
+ self._idempotencia = idempotencia
318
+
319
+ async def manejar(self, evento: EventoEmpleadoCreado) -> None:
320
+ consumer_id = "handler.nueva_alta.configurar_nomina"
321
+
322
+ # Verificar idempotencia ANTES de procesar
323
+ if await self._idempotencia.ya_procesado(evento.id, consumer_id):
324
+ logger.info("Evento %s ya procesado, ignorando", evento.id)
325
+ return
326
+
327
+ # Procesar el evento
328
+ await self._repo.configurar_nomina(evento.payload)
329
+
330
+ # Marcar como procesado DESPUÉS del procesamiento exitoso
331
+ await self._idempotencia.marcar_procesado(evento.id, consumer_id)
332
+ ```
333
+
334
+ ---
335
+
336
+ ## 6. Retry Strategies — Reintentos con Backoff
337
+
338
+ ```python
339
+ import asyncio
340
+ from datetime import datetime, timezone, timedelta
341
+
342
+ class PoliticaReintento:
343
+ """Backoff exponencial con jitter para evitar thundering herd."""
344
+
345
+ def __init__(
346
+ self,
347
+ max_intentos: int = 5,
348
+ delay_inicial: float = 1.0,
349
+ delay_maximo: float = 60.0,
350
+ multiplicador: float = 2.0,
351
+ ):
352
+ self.max_intentos = max_intentos
353
+ self.delay_inicial = delay_inicial
354
+ self.delay_maximo = delay_maximo
355
+ self.multiplicador = multiplicador
356
+
357
+ def delay_para_intento(self, intento: int) -> float:
358
+ """Calcula delay con jitter para evitar reintentos sincronizados."""
359
+ import random
360
+ delay = min(
361
+ self.delay_inicial * (self.multiplicador ** (intento - 1)),
362
+ self.delay_maximo,
363
+ )
364
+ # Jitter: ±25% del delay calculado
365
+ jitter = delay * 0.25 * (2 * random.random() - 1)
366
+ return delay + jitter
367
+
368
+ # Dead Letter Queue — reintentos agotados
369
+ class ProcesadorDLQ:
370
+ """Procesa mensajes en la Dead Letter Queue para análisis y reintento manual."""
371
+
372
+ async def analizar_dlq(self, cola_dlq: str) -> list[dict]:
373
+ """Obtiene mensajes fallidos para investigación."""
374
+ mensajes = []
375
+ async for mensaje in self._consumer.consumir_dlq(cola_dlq):
376
+ mensajes.append({
377
+ "id": mensaje.message_id,
378
+ "timestamp_original": mensaje.headers.get("x-first-death-time"),
379
+ "motivo_fallo": mensaje.headers.get("x-death")[0]["reason"],
380
+ "intentos": mensaje.headers.get("x-death")[0]["count"],
381
+ "cuerpo": json.loads(mensaje.body),
382
+ })
383
+ return mensajes
384
+
385
+ async def reintentar_mensaje(self, mensaje_id: str) -> None:
386
+ """Reintento manual después de investigación."""
387
+ mensaje = await self._obtener_de_dlq(mensaje_id)
388
+ await self._publicar_a_cola_original(mensaje)
389
+ ```
390
+
391
+ ---
392
+
393
+ ## 7. CQRS — Command Query Responsibility Segregation
394
+
395
+ ```python
396
+ # Separar modelos de lectura y escritura
397
+ # Escritura (Commands) → actualiza BD principal → emite evento
398
+ # Lectura (Queries) → consulta vista desnormalizada optimizada para lectura
399
+
400
+ # COMMAND SIDE
401
+ class ComandoCrearEmpleado(BaseModel):
402
+ nombre: str
403
+ apellido_paterno: str
404
+ apellido_materno: str
405
+ email: str
406
+ departamento_id: str
407
+
408
+ class HandlerComandoCrearEmpleado:
409
+ async def manejar(
410
+ self,
411
+ comando: ComandoCrearEmpleado,
412
+ db: AsyncSession,
413
+ broker: BrokerEventos,
414
+ ) -> str:
415
+ # Crear en BD normalizada (source of truth)
416
+ empleado = Empleado(**comando.model_dump())
417
+ db.add(empleado)
418
+ await db.flush()
419
+
420
+ # Emitir evento ANTES del commit (transactional outbox pattern)
421
+ evento = EventoEmpleadoCreado(
422
+ origen="servicio-empleados",
423
+ payload=EmpleadoCreadoPayload(
424
+ empleado_id=str(empleado.id),
425
+ nombre_completo=f"{comando.nombre} {comando.apellido_paterno}",
426
+ email_corporativo=comando.email,
427
+ departamento_id=comando.departamento_id,
428
+ numero_empleado=empleado.numero_empleado,
429
+ puesto=empleado.puesto,
430
+ fecha_ingreso=empleado.fecha_ingreso.isoformat(),
431
+ ),
432
+ )
433
+ await broker.publicar(evento)
434
+ await db.commit()
435
+ return str(empleado.id)
436
+
437
+ # QUERY SIDE — Vista desnormalizada actualizada por eventos
438
+ class VistaEmpleado(Base):
439
+ """Tabla de lectura: desnormalizada para queries de UI."""
440
+ __tablename__ = "vista_empleados"
441
+ id = Column(UUID, primary_key=True)
442
+ nombre_completo = Column(String)
443
+ email = Column(String)
444
+ departamento_nombre = Column(String) # Desnormalizado — no requiere JOIN
445
+ puesto = Column(String)
446
+ activo = Column(Boolean, default=True)
447
+ ultima_actualizacion = Column(DateTime)
448
+
449
+ class ActualizadorVistaEmpleados:
450
+ """Consumidor de eventos que mantiene la vista actualizada."""
451
+
452
+ async def en_empleado_creado(self, evento: EventoEmpleadoCreado) -> None:
453
+ departamento = await self._repo.obtener_departamento(
454
+ evento.payload.departamento_id
455
+ )
456
+ vista = VistaEmpleado(
457
+ id=uuid.UUID(evento.payload.empleado_id),
458
+ nombre_completo=evento.payload.nombre_completo,
459
+ email=evento.payload.email_corporativo,
460
+ departamento_nombre=departamento.nombre,
461
+ puesto=evento.payload.puesto,
462
+ ultima_actualizacion=evento.timestamp,
463
+ )
464
+ await self._db.merge(vista)
465
+ await self._db.commit()
466
+ ```
467
+
468
+ ---
469
+
470
+ ## 8. Transactional Outbox Pattern
471
+
472
+ ```python
473
+ # Problema: publicar evento Y hacer commit en BD de forma atómica
474
+ # Solución: escribir evento a tabla "outbox" en misma transacción,
475
+ # un worker asíncrono publica al broker
476
+
477
+ class EventoOutbox(Base):
478
+ """Tabla de outbox para publicación garantizada de eventos."""
479
+ __tablename__ = "eventos_outbox"
480
+ id = Column(UUID, primary_key=True, default=uuid.uuid4)
481
+ tipo = Column(String(100), nullable=False)
482
+ payload = Column(JSONB, nullable=False)
483
+ exchange = Column(String(100), nullable=False)
484
+ routing_key = Column(String(100), nullable=False)
485
+ creado_en = Column(DateTime(timezone=True), server_default=func.now())
486
+ publicado_en = Column(DateTime(timezone=True), nullable=True)
487
+ intentos = Column(Integer, default=0)
488
+
489
+ async def crear_empleado_con_outbox(
490
+ comando: ComandoCrearEmpleado,
491
+ db: AsyncSession,
492
+ ) -> str:
493
+ """Crea empleado y registra evento en outbox — todo en una transacción."""
494
+ empleado = Empleado(**comando.model_dump())
495
+ db.add(empleado)
496
+ await db.flush()
497
+
498
+ # Mismo commit = garantía de consistencia
499
+ evento_outbox = EventoOutbox(
500
+ tipo="empleado.creado",
501
+ payload={...},
502
+ exchange="eventos.rrhh",
503
+ routing_key="empleado.creado",
504
+ )
505
+ db.add(evento_outbox)
506
+ await db.commit() # Atómico: empleado + evento en outbox
507
+
508
+ return str(empleado.id)
509
+
510
+ # Worker que publica desde outbox al broker
511
+ async def worker_outbox(db: AsyncSession, broker: BrokerEventos) -> None:
512
+ """Ejecuta cada 5 segundos, publica eventos pendientes."""
513
+ while True:
514
+ eventos = await db.execute(
515
+ select(EventoOutbox)
516
+ .where(EventoOutbox.publicado_en.is_(None))
517
+ .where(EventoOutbox.intentos < 5)
518
+ .order_by(EventoOutbox.creado_en)
519
+ .limit(100)
520
+ .with_for_update(skip_locked=True) # Para múltiples workers
521
+ )
522
+ for evento in eventos.scalars():
523
+ try:
524
+ await broker.publicar_raw(
525
+ tipo=evento.tipo,
526
+ payload=evento.payload,
527
+ exchange=evento.exchange,
528
+ routing_key=evento.routing_key,
529
+ )
530
+ evento.publicado_en = datetime.now(timezone.utc)
531
+ except Exception as e:
532
+ evento.intentos += 1
533
+ logger.error("Error publicando evento %s: %s", evento.id, e)
534
+ await db.commit()
535
+ await asyncio.sleep(5)
536
+ ```
537
+
538
+ ---
539
+
540
+ ## 9. Eventual Consistency — Manejo en UI
541
+
542
+ ```python
543
+ # El frontend debe manejar que los datos pueden no estar actualizados inmediatamente
544
+
545
+ # Backend: responder con estado "aceptado" (202 Accepted)
546
+ @router.post("/empleados", status_code=202)
547
+ async def crear_empleado(comando: ComandoCrearEmpleado, ...):
548
+ empleado_id = await handler.manejar(comando, db, broker)
549
+ return {
550
+ "empleado_id": empleado_id,
551
+ "estado": "procesando",
552
+ "mensaje": "El empleado está siendo procesado. Estará disponible en segundos.",
553
+ "consultar_en": f"/empleados/{empleado_id}/estado",
554
+ }
555
+
556
+ # Polling endpoint para que el frontend verifique
557
+ @router.get("/empleados/{id}/estado")
558
+ async def estado_empleado(id: UUID, db: AsyncSession = Depends(get_db)):
559
+ vista = await db.get(VistaEmpleado, id)
560
+ if vista is None:
561
+ return {"estado": "procesando"}
562
+ return {"estado": "completado", "datos": vista}
563
+ ```
564
+
565
+ ---
566
+
567
+ ## 10. Checklist de Sistema Event-Driven
568
+
569
+ - [ ] Schemas de eventos definidos con Pydantic (tipado fuerte)
570
+ - [ ] Versioning en schemas (campo `version: "1.0"`)
571
+ - [ ] Idempotency implementada en todos los handlers
572
+ - [ ] Dead Letter Queue configurada para cada cola
573
+ - [ ] Retry con backoff exponencial y jitter
574
+ - [ ] Correlation ID propagado en todos los eventos
575
+ - [ ] Transactional Outbox para garantía de publicación
576
+ - [ ] Monitoring de DLQ (alertas cuando hay mensajes ahí)
577
+ - [ ] Tests de integración con broker real (no mocks)
578
+ - [ ] Documentación de todos los tipos de eventos (event catalog)
579
+ - [ ] Schemas backward-compatible (cambios aditivos únicamente)
580
+ - [ ] Consumer group names descriptivos y estables