---
name: deprecacion-migracion
description: >
  Estrategias de deprecación y migración: avisos de deprecación, guías de migración,
  compatibilidad hacia atrás, feature flags para migraciones, patrones de migración de
  datos, migraciones sin tiempo de inactividad, patrón strangler fig.
version: "1.0.0"
herramientasPermitidas: [Read, Grep]
evolvable: true  # default para skill estandar
exclusiones:
  - "No cargar para migraciones de esquema de base de datos generadas automáticamente (Alembic autogenerate, Flyway) — para ORM migrations cargar `postgresql-experto`."
  - "No cargar para auditoría de dependencias desactualizadas (CVEs, licencias) — para dependencias cargar `dependencias-auditoria`."
  - "No cargar para refactoring de código sin cambio de interfaz pública — para refactoring sin deprecación usar directamente el skill del lenguaje."
  - "No cargar para versionado semántico de releases — para SemVer y ciclo de release usar `/swl:release`."
---
# Deprecación y Migración — Estrategias de Producción

## Cuándo NO cargar

- La migración es de esquema de BD generada automáticamente (Alembic autogenerate, Flyway scripts): cargar `postgresql-experto`.
- La auditoría es de dependencias desactualizadas con CVEs: cargar `dependencias-auditoria`.
- El refactoring no cambia la interfaz pública de ningún módulo: usar directamente el skill del lenguaje sin periodo de deprecación.
- La tarea es gestionar el número de versión del release (SemVer, CHANGELOG): usar `/swl:release`.

## Principios Fundamentales

La deprecación no es eliminar — es guiar la transición.
Una buena deprecación tiene: **aviso anticipado**, **guía de migración**, **período de gracia**, **eliminación limpia**.

**Reglas de oro:**
1. Nunca eliminar sin deprecar primero (mínimo 1 versión de aviso).
2. Una API deprecada sigue funcionando durante el período de gracia.
3. Los mensajes de deprecación incluyen qué usar en su lugar y cuándo se eliminará.
4. Las migraciones de datos son reversibles hasta confirmar éxito.

---

## Ciclo de Vida de una Deprecación

```
VIGENTE → DEPRECADO → ELIMINADO

Períodos mínimos de gracia:
- Librerías: 1 versión mayor
- APIs internas: 2 sprints
- APIs públicas: 6 meses mínimo
- Bases de datos: 3 meses mínimo
```

---

## Avisos de Deprecación en Python

```python
import warnings
import functools

def deprecado(*, desde: str, eliminar_en: str, usar_en_su_lugar: str, razon: str = ""):
    def decorador(func):
        mensaje = (
            f"`{func.__qualname__}` está deprecado desde v{desde} y será "
            f"eliminado en v{eliminar_en}. Usar: {usar_en_su_lugar}."
        )
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            warnings.warn(mensaje, DeprecationWarning, stacklevel=2)
            return func(*args, **kwargs)
        return wrapper
    return decorador

@deprecado(
    desde="2.1.0", eliminar_en="3.0.0",
    usar_en_su_lugar="calcular_descuento(precio, porcentaje, moneda='MXN')",
)
def calcular_descuento_v1(precio: float, porcentaje: float) -> float:
    return precio * (1 - porcentaje / 100)
```

---

## Deprecación de Endpoints REST

OBLIGATORIO: headers estándar de deprecación HTTP.

```python
@router.get("/v1/usuarios/{usuario_id}")
async def obtener_usuario_v1(usuario_id: str, response: Response) -> dict:
    response.headers["Deprecation"] = "true"
    response.headers["Sunset"] = "2026-09-01"
    response.headers["Link"] = f'</v2/usuarios/{usuario_id}>; rel="successor-version"'
    response.headers["Warning"] = (
        '299 - "Endpoint deprecado. Migrar a /v2/usuarios/{id}"'
    )
    usuario = await servicio_usuarios.obtener(usuario_id)
    return {"id": usuario.id, "nombre_completo": usuario.nombre}
```

---

## Migraciones de Base de Datos sin Tiempo de Inactividad

### Estrategia: Expand-Contract (3 fases)

```sql
-- FASE 1: EXPAND — agregar lo nuevo sin eliminar lo viejo
ALTER TABLE usuarios ADD COLUMN nombre_display TEXT;
CREATE INDEX CONCURRENTLY idx_usuarios_nombre_display ON usuarios (nombre_display);
```

```python
# FASE 2: MIGRAR — rellenar en background, sin downtime
def migrar_nombres_display(batch_size: int = 1000) -> None:
    ultimo_id = None
    while True:
        lote = db.execute(
            f"SELECT id, nombre FROM usuarios WHERE nombre_display IS NULL "
            f"{'AND id > :uid' if ultimo_id else ''} ORDER BY id LIMIT :bs",
            {"uid": ultimo_id, "bs": batch_size},
        ).fetchall()
        if not lote:
            break
        for fila in lote:
            db.execute(
                "UPDATE usuarios SET nombre_display = :nd WHERE id = :id",
                {"nd": formatear_nombre(fila.nombre), "id": fila.id},
            )
        db.commit()
        ultimo_id = lote[-1].id
        time.sleep(0.1)  # No saturar BD
```

```sql
-- FASE 3: CONTRACT — eliminar columna vieja (solo cuando TODOS los servicios migraron)
ALTER TABLE usuarios DROP COLUMN nombre;
```

---

## Implementaciones Completas

Para Feature Flags con rollout gradual, Patrón Strangler Fig (proxy de migración),
Plantilla de Guía de Migración para usuarios, y Monitoreo de uso de endpoints
deprecados, ver
[recursos/implementaciones-completas.md](recursos/implementaciones-completas.md).

---

## Checklist de Revisión — Deprecación y Migración

- [ ] El aviso de deprecación incluye: versión desde, versión de eliminación, alternativa.
- [ ] El código deprecado sigue funcionando durante el período de gracia completo.
- [ ] Los endpoints deprecados retornan headers `Deprecation`, `Sunset` y `Link`.
- [ ] Hay una guía de migración publicada ANTES de deprecar, no después.
- [ ] Las migraciones de base de datos usan el patrón Expand-Contract (3 fases).
- [ ] Las migraciones de datos son idempotentes y se pueden re-ejecutar sin duplicados.
- [ ] Los índices se crean con `CREATE INDEX CONCURRENTLY` para no bloquear.
- [ ] Las migraciones del Strangler Fig tienen feature flags con rollout gradual.
- [ ] Hay un proceso para identificar y notificar clientes que aún usan APIs deprecadas.
- [ ] El plan de eliminación incluye qué retornar después: `410 Gone` con cuerpo explicativo.

## Gotchas / Errores comunes no obvios

**El decorator `@deprecado` emite `DeprecationWarning` pero los warnings de Python están silenciados por defecto en producción**: en CPython, los `DeprecationWarning` se suprimen a menos que el código corra en modo test o con `python -W default`. Causa: Python suprime `DeprecationWarning` deliberadamente para no saturar a los usuarios finales con warnings de bibliotecas. Fix: para asegurar visibilidad del aviso, complementar `warnings.warn` con un log estructurado en el wrapper: `logger.warning("deprecado: %s — migrar a %s", func.__qualname__, usar_en_su_lugar)`. El log pasa a través de cualquier configuración de Python warnings.

**La Fase 2 del patrón Expand-Contract con migración en background bloquea la tabla en PostgreSQL por el `UPDATE` masivo**: actualizar millones de filas con `UPDATE usuarios SET nombre_display = :nd WHERE nombre_display IS NULL` puede escalar a un lock de tabla que bloquea lecturas. Causa: PostgreSQL escala el lock de fila a lock de tabla cuando el porcentaje de filas actualizadas es alto. Fix: procesar siempre en lotes pequeños (máximo 1000 filas por transacción) con `time.sleep(0.1)` entre lotes. Usar `SELECT ... FOR UPDATE SKIP LOCKED` para evitar deadlocks si múltiples workers corren la migración en paralelo.

**El header `Sunset` en endpoints deprecados usa formato de fecha incorrecto y los clientes no lo parsean**: `response.headers["Sunset"] = "2026-09-01"` usa formato ISO 8601, pero el RFC 8594 especifica que `Sunset` debe usar formato HTTP-date: `"Mon, 01 Sep 2026 00:00:00 GMT"`. Causa: el estándar del header `Sunset` es diferente al formato de fecha que los desarrolladores usan intuitivamente. Fix: generar el valor con `email.utils.formatdate(time.mktime(fecha_sunset.timetuple()), usegmt=True)` en Python para producir el formato HTTP-date correcto.

**El Strangler Fig con feature flag al 100% nunca elimina el código legacy porque "ya está en producción sin problemas"**: el rollout llega al 100% gradualmente, el código nuevo funciona bien, pero el código legacy nunca se elimina porque "no hay urgencia". Causa: sin fecha de eliminación forzada, el Strangler Fig crea deuda técnica permanente — dos implementaciones que coexisten indefinidamente. Fix: establecer en el plan de migración una fecha de eliminación del código legacy como hito del proyecto, antes de llegar al 100% del rollout. El 100% en feature flag no es el final — la eliminación del código viejo lo es.
