De la especificación al código: mi experiencia reingenierizando un módulo legacy con AI-DLC y Normandy

Johana Cardenas
Septiembre 2026

Resumen ejecutivo

Este fue un caso de reingeniería del módulo de Mantenimiento de un Customer Relationship Management (CRM, gestión de relación con clientes) basado en vtiger. El sistema, entre otros procesos operativos, registra órdenes de mantenimiento de equipos. Sus tecnologías principales son PHP como lenguaje de aplicación y MySQL como motor de base de datos. El objetivo parecía directo: estandarizar la creación de órdenes de mantenimiento de equipos eléctricos. En realidad, el comportamiento estaba repartido entre varios módulos, interfaces de programación de aplicaciones (API) móviles, metadatos de base de datos, rutinas históricas y reglas de cierre. No era un cambio aislado ni un refactor puramente interno; afectaba la forma en que el sistema crea, finaliza y presenta órdenes de mantenimiento.

En este caso usé AI-DLC (AI-Driven Life Cycle, un ciclo de vida de desarrollo asistido por inteligencia artificial) para transformar un requerimiento amplio en una especificación trazable. Después utilicé agentes de IA para construir y revisar las unidades definidas. Normandy —un plugin de Claude Code que creamos para orquestar agentes de desarrollo y de revisión sobre GitLab— fue el marco operativo que ordenó esa colaboración: separó construcción de revisión, exigió evidencia, registró desacuerdos y mantuvo las decisiones de negocio o riesgo en manos humanas. AI-DLC me sirvió para investigar el comportamiento existente, aclarar ambigüedades, modelar reglas y dividir el trabajo.

El resultado fue una entrega estructurada en cinco unidades: campos y metadatos; andamiaje de rutinas; rutinas de creación; migración de orígenes; y finalización, estado final y Hoja de Vida. La validación registrada cerró con 437 pruebas automatizadas en verde y 1.214 aserciones, además de pruebas manuales y reservas operativas documentadas. No fue un proceso lineal ni automático. Hubo cambios de diseño, hallazgos heredados, decisiones de alcance y límites de prueba que debían hacerse visibles en vez de ocultarse tras una etiqueta de “hecho”.

La principal conclusión es simple: en un sistema legacy, el valor de trabajar con agentes no está en producir más código, sino en convertir conocimiento disperso en una entrega verificable. La especificación, la evidencia y la capacidad humana de decidir siguen siendo el centro del proceso.

1. El contexto del proyecto: por qué este caso requería otro modo de trabajar

El punto de partida fue una solicitud de reingeniería de alta complejidad. Había que centralizar la creación de órdenes de mantenimiento de equipos eléctricos en seis rutinas reutilizables: Mantenimiento, Inspección 1, Inspección 2, Inspección Técnica, Alistamiento y Mantenimiento a Domicilio.

La dificultad no estaba en crear seis clases. Estaba en que la creación de órdenes ya ocurría en varios lugares: recogidas, traslados, cuarentenas, campañas de inspección técnica, procesos de sede y API móviles. Cada origen tenía supuestos propios, algunos explícitos y otros implícitos en el código. A eso se sumaban cambios de metadatos en Mantenimiento, Visitas, Activos Retornables y Modelos de Equipos; nueva lógica de finalización; una interfaz de usuario de confirmación para el estado final; y un ajuste de la Hoja de Vida.

El sistema era un monolito heredado (legacy) sobre vtiger y PHP. En ese contexto, una aparente coincidencia de nombres podía ser peligrosa. Por ejemplo, un mismo campo personalizado no significaba lo mismo en todos los módulos donde aparecía. También había campos existentes que el anexo del ticket nombraba de forma imprecisa, scripts de upgrade que podían afectar metadatos y una suite de pruebas con fallos heredados. El riesgo era cambiar algo correcto en un lugar y romper un flujo remoto o una integración no visible desde el repositorio.

Por eso decidí no tratar el caso como una cadena de tareas de código. Primero había que reconstruir el comportamiento actual, convertirlo en reglas verificables y separar cuidadosamente lo que debía cambiar de lo que debía conservarse.

2. El caso y sus límites

El alcance final incluyó cuatro bloques de trabajo.

Primero, los prerrequisitos de datos y metadatos: nuevos campos, modificaciones de campos existentes, picklists, etiquetas y scripts de upgrade idempotentes. Segundo, la nueva capa de rutinas para que la creación de órdenes dejara de depender de instanciaciones dispersas de Mantenimiento. Tercero, la migración de los orígenes existentes para que utilizaran esas rutinas. Cuarto, la lógica de finalización: cálculos de falla y daño, actualización de estado, una acción de Estado Final Equipo y el ajuste de la Hoja de Vida.

También fue importante definir lo que no se iba a hacer. El caso excluyó el recálculo de mantenimientos históricos y los equipos de gas. La rutina de Alistamiento se construyó, pero no se conectó a un evento porque ese disparador no existía. Estas exclusiones no son detalles administrativos: son una forma de proteger el alcance y evitar que un caso de reingeniería se convierta silenciosamente en una reescritura de dominio.

La especificación también dejó por escrito decisiones que podrían parecer pequeñas, pero que cambian el comportamiento. Por ejemplo, las órdenes creadas por Inspección Técnica y Mantenimiento a Domicilio nacen terminadas, mientras que las órdenes abiertas requieren un cierre manual con confirmación del estado final. La nueva interfaz de usuario muestra siempre tres estados válidos y exige confirmar con una palabra asociada al estado elegido. Ese nivel de precisión evita que una interpretación razonable, pero distinta, termine convertida en código.

3. Cómo usé AI-DLC para especificar antes de construir

AI-DLC funcionó como una secuencia de reducción de incertidumbre. No lo usé como una colección de plantillas, sino como una forma de hacer visibles las preguntas que el código por sí solo no respondía.

flowchart LR
    subgraph INC["Inception"]
        RE["Reverse engineering"] --> RA["Requisitos"] --> US["Historias de usuario"] --> WP["Planificación"] --> AD["Diseño de aplicación"] --> UG["Unidades de trabajo"]
    end
    subgraph CON["Construction, por unidad"]
        FD["Diseño funcional"] --> NR["Requisitos no funcionales"] --> ND["Diseño no funcional"] --> CG["Generación de código"]
    end
    UG --> FD
    CG --> BT["Build and test"]

Figura 1. Etapas de AI-DLC ejecutadas en el caso. El diseño de infraestructura se omitió porque el caso no la modificaba.

El primer paso fue el reverse engineering acotado al caso. Se levantó un inventario de componentes, campos, puntos de creación y rutas de finalización. Ese trabajo permitió contrastar el ticket con el sistema real. En un caso de este tipo, el ticket expresa intención; la base de datos y el código expresan conducta. Ambas fuentes son necesarias, pero no siempre coinciden.

Después convertí esa evidencia en requisitos funcionales y no funcionales. Los funcionales describieron los campos, las seis rutinas, el mapeo de evento a rutina, la eliminación de creación manual, la finalización y la Hoja de Vida. Los no funcionales introdujeron tres reglas que afectaron de forma concreta el diseño: consultas parametrizadas en el código tocado, respeto de permisos y creación fail-closed; además de pruebas basadas en propiedades para las funciones de cálculo.

La fase de diseño de aplicación convirtió esas reglas en componentes y relaciones. Se identificaron el contexto de evento, la clase base de las rutinas, las rutinas concretas, la calculadora de mantenimiento, los adaptadores de origen y los componentes de finalización. El diseño inicial contemplaba una factory central para escoger la rutina. Durante la construcción se comprobó que el modelo más conveniente era otro: cada origen instancia directamente su rutina y le entrega un contexto normalizado. No fue una contradicción del proceso; fue precisamente la razón para mantener diseño, decisiones y evidencia versionados. La especificación no es una pieza estática: debe poder evolucionar cuando la implementación aporta una mejor lectura del sistema.

flowchart TD
    subgraph ORI["Orígenes existentes"]
        O1["Recogida"]
        O2["Traslado"]
        O3["Campaña de inspección técnica"]
        O4["Domicilio (API móvil)"]
    end
    ADP["Adaptador de origen"]
    CTX["Contexto de evento normalizado"]
    subgraph RUT["Rutinas de creación"]
        BASE["Clase base: creación transaccional y fail-closed"]
        R["Mantenimiento · Inspección 1 · Inspección 2 · Inspección Técnica · Alistamiento · Domicilio"]
    end
    CALC["Calculadora de mantenimiento (funciones puras)"]
    OM["Orden de mantenimiento"]
    FIN["Finalización"]

    O1 & O2 & O3 & O4 --> ADP --> CTX --> R
    R -->|hereda| BASE
    BASE --> CALC
    BASE --> OM
    OM -->|cierre| FIN --> CALC

Figura 2. Diseño resultante: cada origen instancia directamente su rutina con un contexto normalizado, sin una factory central.

Finalmente, AI-DLC ayudó a descomponer el caso en unidades de trabajo con dependencias explícitas. No se trató de partir la solicitud por carpetas, sino de ordenar el riesgo. Los campos y metadatos debían existir antes de las rutinas; las rutinas antes de migrar los orígenes; y la finalización debía apoyarse en las decisiones de creación ya estabilizadas.

UnidadResponsabilidad principalDependencia relevante
U1Campos y metadatosPrerrequisito de las demás unidades
U2Andamiaje de rutinasDepende de U1
U3Seis rutinas y lógica de creaciónDepende de U1 y U2
U4Migración de orígenes existentesDepende de U3
U5Finalización, estado final y Hoja de VidaDepende de U1 y U3
flowchart LR
    U1["U1 · Campos y metadatos"] --> U2["U2 · Andamiaje de rutinas"]
    U1 --> U3["U3 · Rutinas y lógica de creación"]
    U2 --> U3
    U3 --> U4["U4 · Migración de orígenes"]
    U1 --> U5["U5 · Finalización, estado final y Hoja de Vida"]
    U3 --> U5

Figura 3. Dependencias entre unidades de trabajo.

Esta separación hizo que cada unidad tuviera su propio diseño funcional, requisitos no funcionales, diseño de calidad, plan de generación de código y evidencia de prueba.

4. De diseño a unidades construibles

La construcción comenzó por U1 porque los metadatos no eran un detalle de implementación: definían el contrato de datos de todas las rutinas posteriores. Aquí apareció una de las lecciones más importantes del caso. Un requerimiento que parecía pedir eliminar un campo podía ser peligroso si se aplicaba literalmente.

Al investigar uno de esos campos personalizados, se encontró que el nombre estaba sobrecargado y que una eliminación física podía invalidar vistas de base de datos consumidas fuera del repositorio. La decisión final fue retirar únicamente el metadato correspondiente al módulo objetivo, conservar la columna física y evitar afectar campos homónimos de otros módulos. En otras palabras, el trabajo no consistió en “cumplir el anexo”; consistió en cumplir la intención sin destruir información ni romper consumidores no visibles.

U2 creó el andamiaje común: un objeto de transferencia de datos (DTO) para el contexto, una excepción tipada y una clase base abstracta con un método de creación transaccional. El detalle relevante es que el comportamiento fail-closed —detener la operación ante un fallo en lugar de dejar un resultado parcial— no quedó como una frase en un documento. La creación debía validar precondiciones, iniciar y cerrar la transacción correctamente, registrar contexto y revertir ante un fallo para no persistir una orden a medias.

La revisión encontró dificultades propias de vtiger y ADODB, la biblioteca de abstracción de acceso a bases de datos que usa esta aplicación PHP. La entidad de mantenimiento podía usar una conexión distinta a la de la rutina, de modo que algunas escrituras escapaban a la transacción. Además, las transacciones anidadas necesitaban manejarse hasta la profundidad correcta para que una reversión fuera real. Son hallazgos que difícilmente aparecen en un diseño abstracto; aparecieron porque el proceso exigía contrastar la intención con el comportamiento de la plataforma.

U3 concentró la lógica de las seis rutinas y de los cálculos de creación. Separar los cálculos en una CalculadoraMantenimiento con funciones puras permitió probar combinaciones de falla, daño, tipo y estado sin depender de todo el CRM. U4 llevó esa capa a los cinco orígenes reales. U5 completó el ciclo con los cálculos de cierre, la validación de Estado Final Equipo y la Hoja de Vida.

flowchart TD
    CM["Cierre de la orden"] --> LD["Lectura de daños registrados"]
    LD --> CALC["Calculadora: falla final, daño final y tipo de cierre"]
    CALC --> SAVE["Campos escritos al guardar"]
    CM --> Q{"¿La orden está abierta?"}
    Q -->|Sí| AC["Acción Estado Final Equipo"]
    AC --> VAL["Validación de servidor: bloquea un estado inválido"]
    Q -->|No| SK["Nace terminada, sin confirmación"]

Figura 4. Flujo de finalización especificado para U5.

La unidad final también dejó una decisión de producto explícita: los registros históricos muestran vacío en la nueva columna de Mantenimiento de la Hoja de Vida. No se hizo un backfill aproximado porque reconstruir la falla inicial a partir de historia incompleta habría presentado datos inventados como si fueran hechos. En un sistema que se usa para seguimiento operativo, la honestidad del dato tiene más valor que la apariencia de completitud.

5. Construcción de software con agentes de IA

La especificación sólo aporta valor si se puede convertir en software sin perder su intención. En este caso, los agentes de IA se usaron para construir unidades delimitadas, revisar resultados desde perspectivas independientes y volver sobre hallazgos con evidencia. El objetivo no fue automatizar el juicio de ingeniería, sino distribuir tareas concretas de análisis, implementación y verificación.

Cada unidad pasó por construcción, revisión y compuertas. Un agente constructor implementaba contra una especificación concreta; un panel de agentes revisaba desde perspectivas de diseño, corrección y pruebas, seguridad y estándares; los hallazgos quedaban anclados a evidencia; y las decisiones que alteraban alcance, riesgo o comportamiento se elevaban al responsable humano. Normandy aportó el protocolo de coordinación de esas etapas. El sistema no estaba diseñado para que el agente “ganara” una discusión, sino para que una decisión quedara registrada con su razón.

Esta separación fue especialmente valiosa en un sistema legacy. Un mismo hallazgo puede ser técnicamente válido y, al mismo tiempo, no ser una corrección que deba entrar en el caso. El proceso permitió distinguir entre cuatro situaciones: un defecto que se corrige ahora; una deuda preexistente que se documenta para otro caso; una decisión de negocio que debe tomar la persona dueña del producto; y una afirmación que requiere más evidencia antes de considerarse cierta.

Un ejemplo fue la revisión de un upgrader de U3. Dos lentes detectaron que, en una instancia donde faltara un campo, una llamada posterior podía producir un error fatal. La observación técnica era correcta. Sin embargo, el campo existía en el ambiente objetivo y el patrón existente no incluía esa guarda. La decisión humana fue descartar la corrección para ese caso y registrar el motivo. La calidad del proceso no consistió en aceptar automáticamente todo hallazgo, sino en no dejarlo desaparecer ni convertirlo en cambio sin autorización.

Otro ejemplo ocurrió en U4, al revisar la migración de un origen móvil. El panel identificó que cierta absorción de errores estaba cubriendo también una rama de equipos de gas, que estaba fuera de alcance. La decisión fue conservar el comportamiento de gas y limitar la nueva lógica a los equipos eléctricos. La discusión dejó trazable una frontera que el código no hacía evidente por sí solo.

El valor de este enfoque agentic estuvo en esa disciplina: los agentes pueden investigar, proponer, implementar y revisar, pero no deben sustituir al responsable cuando la respuesta es una decisión de producto, de operación o de aceptación de riesgo.

6. Calidad, seguridad y pruebas en un entorno legacy

En un proyecto con historial largo, la frase “la suite está verde” puede ser engañosa. El caso tenía fallos de arnés preexistentes, especialmente en la suite de las rutinas. El criterio de regresión se definió de forma explícita: cuatro errores heredados no eran una regresión; cinco sí lo serían. Esto evitó dos errores frecuentes: aceptar fallos nuevos como si fueran deuda conocida, o gastar trabajo intentando reparar el arnés en lugar de validar el cambio solicitado.

También se preparó una base de datos desechable con metadatos representativos y sin datos sensibles. Las pruebas fabricaban y borraban sus datos de negocio. Esa decisión permitió medir cambios de esquema y flujos de mantenimiento sin usar staging como ambiente de experimentación.

La estrategia de calidad combinó varios niveles:

  • Pruebas unitarias y basadas en propiedades para cálculos de creación y finalización.
  • Pruebas de integración para los orígenes que crean órdenes.
  • Pruebas de seguridad para parametrización, permisos y comportamiento fail-closed.
  • Pruebas manuales para elementos cuyo valor depende de interacción visual y permisos reales, como la interfaz Estado Final Equipo.

La seguridad tampoco se trató como una lista de verificación decorativa. Se verificó que las consultas nuevas y refactorizadas usaran parametrización; que las acciones respetaran permisos del registro; y que una falla no dejara una orden parcialmente creada. En U5, por ejemplo, se incluyeron pruebas que volvían rojas al retirar guardas de cierre, de atomicidad y de protección frente a inyección.

La evidencia de cierre registró 437 pruebas automatizadas en verde y 1.214 aserciones. Además, la interfaz de Estado Final y la Hoja de Vida se verificaron manualmente. Aun así, el estado no se presentó como una certeza absoluta: quedaron dos escenarios manuales por ejecutar y se documentaron límites del fixture. Esa es una práctica que considero esencial: una reserva conocida y visible es mucho más útil que una aprobación que aparenta cubrir lo que no se midió.

7. Resultados, reservas y trazabilidad

El caso dejó una arquitectura más clara para la creación de órdenes de mantenimiento. La lógica dispersa se concentró en una capa de rutinas; los orígenes se migraron para usarla; los cálculos se aislaron en funciones probables; y el cierre se convirtió en un flujo con reglas explícitas y validación de servidor.

También dejó trazabilidad completa entre requisitos, unidades, diseños, código, decisiones, pruebas y estado de revisión. Ese rastro fue útil durante la construcción, pero su valor aumenta después de entregar. Cuando aparece una duda sobre por qué un campo no se eliminó, por qué un comportamiento se dejó fuera de alcance o por qué una prueba queda roja, no es necesario reconstruir la historia desde memoria o desde commits dispersos.

La entrega conservó reservas operativas importantes. La fase operativa de AI-DLC aún no tenía un flujo definido, por lo que las instrucciones de despliegue y validación se mantuvieron dentro de la fase de construcción y pruebas. Además, los cambios de metadatos requerían seguir un orden de ejecución y verificar la migración antes de retirar el metadato antiguo. No eran condiciones para esconder al final del proyecto; eran parte de la definición honesta de “listo para desplegar”.

8. Coste y aprendizaje operativo

La estimación humana vigente para el alcance definido fue de 106 horas, incluyendo implementación y pruebas del desarrollador, pero no reuniones, despliegue ni UAT. Ese número no debe compararse de manera simplista con el consumo de agentes. Las tareas no son idénticas y la telemetría de las primeras unidades fue incompleta.

Lo que sí puede afirmarse es que el registro de ejecuciones agentic documentó al menos 15.726.648 tokens para el caso, después de deduplicar ejecuciones. Es una cota mínima: U1 y U2 no contaban con registros completos y parte de U3 tampoco. La lección no es que una métrica sustituya a la otra. La lección es que, si se va a usar un proceso de agentes de IA para construir software, el costo y la evidencia deben ser observables igual que lo son en cualquier otra capacidad de ingeniería.

También aprendí que las rondas de revisión tienen un costo y que no toda observación justifica una ronda adicional. Un hallazgo de diseño, una deuda preexistente y una preferencia de estilo no deben recibir el mismo tratamiento. El proceso funciona mejor cuando reserva el esfuerzo de construcción para cambios que preservan una capacidad, corrigen un riesgo medido o cumplen un requisito explícito.

9. Lo que repetiría y lo que cambiaría

Repetiría cinco prácticas de este caso.

Primero, haría reverse engineering antes de repartir trabajo. En un legacy, el nombre de un campo o una lectura del ticket rara vez bastan para conocer su impacto.

Segundo, mantendría una especificación que conecte reglas de negocio, diseño, decisiones no funcionales y pruebas. Eso permitió que las unidades fueran construibles sin perder el propósito del caso.

Tercero, conservaría la separación entre construcción, revisión y decisión humana. Los agentes aportan más cuando se especializan y se contradicen con evidencia que cuando intentan resolver todo en una sola pasada.

Cuarto, seguiría usando líneas base explícitas para las pruebas heredadas. Un fallo antiguo no es automáticamente aceptable, pero tampoco debe desviar el caso si está medido y aislado.

Quinto, documentaría las reservas como parte de la entrega. Una fase pendiente, una prueba manual o un cambio operativo no desaparecen porque el código se haya mergeado.

Cambiaría dos cosas. Buscaría capturar la telemetría de costo desde la primera unidad y definiría antes qué decisiones necesitan una respuesta humana para evitar rondas de revisión sobre asuntos de producto. También trataría los documentos de diseño inicial como hipótesis verificables: deben poder evolucionar, pero cada cambio de dirección tiene que quedar explicado para no convertir el proceso en una sucesión de documentos contradictorios.

Conclusión

Este caso confirmó que usar AI-DLC y agentes de IA no equivale a automatizar el ciclo de desarrollo. Equivale a hacerlo más explícito.

AI-DLC ayudó a convertir un requerimiento amplio y ambiguo en una especificación que podía discutirse, partirse y probarse. La construcción con agentes de IA aportó capacidad de implementación y revisión especializada; Normandy dio la disciplina de coordinación para saber quién implementa, quién revisa, qué evidencia sostiene un hallazgo y cuándo la decisión debe permanecer humana. Juntos, los enfoques hicieron posible avanzar sobre un módulo legacy sin fingir que el legado era simple ni que la IA elimina el juicio de ingeniería.

El resultado más importante no fueron sólo las seis rutinas o los scripts de upgrade. Fue dejar un cambio entendible: qué se modificó, qué se decidió no tocar, qué evidencia se obtuvo y qué trabajo queda para la siguiente persona. Para mí, esa es la medida de una buena entrega en un sistema que seguirá evolucionando después de este caso.

Anexo A. Hechos del caso utilizados en este documento

  • Caso: reingeniería del módulo de Mantenimiento.
  • Plataforma: CRM basado en vtiger, sobre PHP y MySQL.
  • Alcance construido: fases de especificación (Inception) y construcción (Construction); la fase operativa aún no tiene un flujo activo en AI-DLC.
  • Unidades: U1 Campos y Metadatos; U2 Andamiaje de Rutinas; U3 Rutinas y Lógica de Creación; U4 Migración de Orígenes; U5 Finalización, Estado Final y Hoja de Vida.
  • Estimación humana vigente: 106 horas, sin colchón y bajo los supuestos documentados.
  • Validación registrada: 437 pruebas automatizadas en verde y 1.214 aserciones; cuatro errores de la suite de rutinas clasificados como línea base heredada.
  • Telemetría agentic registrada: al menos 15.726.648 tokens tras deduplicación; registro incompleto para parte de U1, U2 y U3.