La mayoría de las migraciones de funcionalidades comienzan con una idea tentadora: copiar el código anterior, adaptarlo lo suficiente y seguir adelante.
Es un impulso comprensible. Pero también suele arrastrar al nuevo repositorio el acoplamiento, los nombres, las suposiciones y la complejidad accidental de la aplicación anterior.
Así que esta es la hipótesis que quiero poner a prueba en este artículo:
Si una funcionalidad pasa de una aplicación anterior a un repositorio nuevo con un stack diferente, quizá lo primero que debamos migrar no sea el código. Quizá debamos migrar la especificación.
Más concretamente:
- aplicar ingeniería inversa a la funcionalidad anterior para crear artefactos de su estado actual
- convertir esos artefactos en un contrato de comportamiento que se pueda revisar
- generar nuevos artefactos de Spec Kit en el nuevo repositorio a partir de ese contrato
- implementar pequeñas partes de la funcionalidad y comprobar la paridad en lugar de copiar y pegar código
El April 14, 2026, contrasté de nuevo esta idea con la documentación y el README actuales de GitHub Spec Kit, las guías del SEI sobre reconstrucción y modernización de arquitectura, los materiales de Martin Fowler y Thoughtworks sobre el reemplazo incremental de sistemas legacy, y los escritos de Gojko Adzic sobre Specification by Example.
Una aclaración importante desde el principio: los elementos de base están respaldados por fuentes, pero el flujo completo de «migración de especificación a especificación» es una síntesis mía. No quiero dar a entender que GitHub Spec Kit ya ofrece una versión mágica de todo esto con un solo comando.
Este artículo también complementa mi publicación anterior sobre cómo aplicar ingeniería inversa a funcionalidades existentes para crear artefactos de GitHub Spec Kit. Ese artículo se centraba en documentar una funcionalidad existente en su propio repositorio. Este plantea una pregunta diferente:
¿Pueden esos artefactos convertirse en el puente entre una aplicación legacy y una nueva?
TL;DR
- Sí, creo que la migración de especificación a especificación es una buena opción predeterminada para trasladar una funcionalidad de alcance delimitado de un repositorio anterior a uno nuevo con un stack diferente.
- La idea clave es migrar comportamiento, contratos, ejemplos e invariantes, no controladores, helpers ni abstracciones legacy.
- Las partes de este argumento que cuentan con mayor respaldo en las fuentes son:
- aplicar ingeniería inversa a sistemas existentes para obtener representaciones de más alto nivel es una práctica habitual de modernización
- el reemplazo incremental supera al reemplazo de una sola vez en muchas migraciones riesgosas
- la paridad de resultados importa más que copiar a ciegas cada detalle de la funcionalidad legacy
- los ejemplos creados en colaboración suelen ser más valiosos que las pruebas generadas a partir de ellos
- GitHub Spec Kit ofrece el pipeline de especificación, aclaración, planificación y tareas, pero no un comando propio dedicado a la ingeniería inversa
- El flujo que yo usaría es:
- elegir una funcionalidad de alcance delimitado
- aplicar ingeniería inversa al repositorio anterior para obtener evidencia y especificaciones del estado actual
- extraer un contrato de paridad
- crear especificaciones del estado objetivo en el nuevo repositorio
- implementar una pequeña parte de la funcionalidad
- ejecutar ambos sistemas en paralelo o comparar resultados
- hacer la transición gradualmente
- Este enfoque es más sólido cuando el nuevo repositorio tiene una arquitectura o un stack diferente, y menos útil cuando el cambio consiste básicamente en un pequeño refactor dentro del mismo modelo técnico.
Qué aprenderás aquí
- Por qué creo que la migración de especificación a especificación es una mejor forma de plantear muchas migraciones de funcionalidades en sistemas existentes que la migración de código
- Qué demuestran realmente las fuentes actuales y qué sigue dependiendo del criterio del equipo
- Cómo definir el contrato del estado actual de una funcionalidad anterior sin rediseñarla demasiado pronto
- Cómo convertir ese contrato en artefactos de Spec Kit para el estado objetivo en un repositorio nuevo
- Cómo los PMs y los ingenieros pueden revisar la misma migración usando especificaciones, en lugar de limitarse a leer grandes diffs de código
- Dónde encajan las comprobaciones de paridad, los puntos de intervención y la ejecución en paralelo dentro del ciclo de entrega
- Cuándo sigue siendo mejor portar el código o rediseñar la funcionalidad desde cero
Recorrido de lectura sugerido
- Si eres PM o engineering manager, lee:
The Hypothesis,Why This Idea Is Plausible,When This Beats Copying Code,A Simple Decision TableyThe Gaps to Watch. - Si eres ingeniero, lee el artículo completo en orden, pero dedica la mayor parte del tiempo a
The Workflow I Would Actually Use,Prompt ExamplesyThe Validation Loop. - Si lideras una migración entre repositorios, no te saltes la parte central. Lo importante es la transferencia entre los artefactos del estado actual del repositorio anterior y el plan del estado objetivo del nuevo repositorio.
La hipótesis
Quiero definir la idea con claridad, porque «migración de especificación a especificación» no es un término oficial de GitHub Spec Kit.
Esto es lo que quiero decir:
- En el repositorio anterior, aplica ingeniería inversa a una funcionalidad existente para obtener una descripción confiable de su estado actual.
- Trata esa descripción como un contrato de comportamiento, no como una propuesta de rediseño.
- En el nuevo repositorio, escribe una nueva especificación y un nuevo plan para la funcionalidad que preserven los resultados necesarios, pero se ajusten a la nueva arquitectura, convenciones y stack.
- Valida la paridad con ejemplos, pruebas y comprobaciones de efectos secundarios, en lugar de asumir que copiar código equivale a preservar el comportamiento.
Eso es fundamentalmente diferente de:
- copiar handlers y utilidades al nuevo repositorio
- traducir un framework a otro archivo por archivo
- preservar los límites entre módulos anteriores que solo existían por la plataforma antigua
En otras palabras:
code migration
-> moves implementation
spec-to-spec migration
-> moves intent, behavior, contracts, examples, and rollout rules
Esa distinción importa porque la mayoría de las migraciones fallan menos por falta de código que por:
- casos límite mal entendidos
- dependencias ocultas
- acoplamiento accidental
- criterios de éxito poco claros
- comportamiento del producto que nunca se documentó con claridad
Por qué esta idea es plausible
La versión más sólida del argumento de este artículo no es «me gustan las especificaciones».
Es esta: las disciplinas que lo sustentan ya existen y encajan sorprendentemente bien.
1. Aplicar ingeniería inversa para obtener representaciones de más alto nivel es una práctica normal de modernización
La guía de reconstrucción de arquitectura del SEI explica claramente que la reconstrucción obtiene una representación arquitectónica útil de un sistema existente a partir del sistema legacy implementado. Eso importa porque el primer paso de una migración segura suele ser subir el nivel de abstracción, no trasladar directamente de un lugar a otro.
Eso ya es un argumento sólido a favor de:
- extraer el comportamiento actual
- identificar y nombrar los límites
- documentar las dependencias
- hacer que la estructura legacy se pueda revisar antes de cambiarla
Que es exactamente lo que una especificación del estado actual intenta hacer a nivel de funcionalidad.
2. La modernización funciona mejor cuando avanzas en pequeñas partes
El material de Thoughtworks sobre el reemplazo de sistemas legacy enfatiza las pequeñas partes, la reducción temprana del riesgo, la entrega frecuente y la comparación en paralelo durante la transición. Eso se acerca a lo opuesto de «copiar todo y esperar que siga funcionando».
Si aceptamos que la modernización debe hacerse por partes, entonces la unidad de migración no debería ser «todo el código relacionado con esta área». Debería ser:
- una capacidad visible para el usuario
- un contrato que se pueda revisar
- una parte de la migración
Eso convierte a las especificaciones en una unidad de coordinación mucho mejor que las carpetas copiadas entre frameworks.
3. La paridad de resultados suele ser más valiosa que la paridad ingenua de funcionalidades
Una de mis ideas favoritas en los escritos recientes sobre modernización es la diferencia entre preservar los resultados correctos y clonar a ciegas toda la superficie de la aplicación anterior.
Encaja perfectamente con esta hipótesis.
Cuando migramos de especificación a especificación, podemos preservar:
- la regla de negocio
- el comportamiento de entrada y salida requerido
- los efectos secundarios o de auditoría que importan
- las expectativas de despliegue y de manejo de fallos
Sin preservar:
- las estructuras de clases anteriores
- los diseños obsoletos de rutas
- los patrones incidentales de acceso a datos
- las formalidades específicas de un framework
Así evitamos construir una aplicación que parece moderna, pero que lleva pegadas por debajo las entrañas de la aplicación anterior.
4. Los puntos de intervención y las sondas nos dan una forma práctica de observar el comportamiento legacy
Los escritos más recientes de Martin Fowler sobre puntos de intervención en sistemas legacy son útiles aquí porque plantean que estos son más que simples herramientas para pruebas.
También sirven para:
- observar el comportamiento legacy
- capturar resultados para analizarlos
- redirigir gradualmente el comportamiento hacia una nueva implementación
Eso le da a la migración de especificación a especificación un modelo de validación muy práctico:
- introducir suficientes puntos de intervención u observabilidad para medir la funcionalidad anterior
- convertir esas observaciones en ejemplos y contratos
- comparar la nueva implementación con esas expectativas
5. Los ejemplos suelen importar más que las herramientas de prueba que los rodean
La reflexión de Gojko Adzic sobre Specification by Example plantea algo importante que creo que encaja bien aquí: las conversaciones y los ejemplos suelen ser más valiosos que las pruebas automatizadas que se crean después.
Eso es muy importante para las migraciones.
Si puedo obtener:
- cinco ejemplos críticos
- los casos límite que hacen perder la confianza
- los efectos secundarios esperados
- los modos de fallo aceptables
entonces ya tengo el núcleo de un contrato de migración que los PMs, los ingenieros y los agentes pueden revisar.
6. GitHub Spec Kit nos da la estructura de apoyo, pero no resuelve todo lo necesario para trabajar con sistemas existentes
La documentación y el README actuales de GitHub Spec Kit respaldan claramente las etapas centrales de planificación:
/speckit.constitution/speckit.specify/speckit.clarify/speckit.plan/speckit.tasks- herramientas opcionales de validación como
/speckit.analyzey/speckit.checklist
La guía de instalación también permite inicializar en el directorio actual con specify init . o specify init --here.
Eso basta para respaldar la mecánica de este flujo de trabajo.
Pero la documentación no presenta actualmente un comando propio para «aplicar ingeniería inversa a esta funcionalidad existente y convertirla en artefactos» como la vía oficial. Y las conversaciones de la comunidad sobre el uso en proyectos brownfield dejan claro que los equipos todavía quieren más orientación en ese aspecto.
Así que la forma honesta de plantearlo es esta:
- Spec Kit proporciona las piezas básicas del flujo de trabajo
- la migración brownfield de especificación a especificación sigue siendo un modelo operativo que debemos diseñar deliberadamente
Está bien. Solo significa que debemos llamar al flujo una síntesis, no una promesa integrada en el producto.
Cuándo supera este enfoque a copiar código
Creo que la migración de especificación a especificación es especialmente útil cuando se cumplen estas condiciones:
- el nuevo repositorio usa un stack diferente, por ejemplo, al pasar de Rails a Next.js con Go, o de un módulo de monolito a una separación entre servicio y aplicación web
- la funcionalidad anterior tiene valor en su comportamiento, pero su código es poco reutilizable
- la implementación anterior arrastra una complejidad incidental evidente
- los PMs o las partes interesadas necesitan un artefacto de migración que se pueda revisar
- varios equipos o agentes necesitan un contrato compartido y estable antes de comenzar la implementación
Aquí es donde el viejo impulso de «simplemente portar el código» causa más daño.
No quieres migrar:
- una solución alternativa del ORM que solo existe por la capa de persistencia anterior
- nombres de rutas que solo tenían sentido en la aplicación anterior
- lógica de reintentos que debería estar en una cola moderna o en un motor de workflows
- suposiciones de UI que correspondían a un modelo de navegación diferente
Quieres migrar el significado de la funcionalidad, no su registro fósil.
El flujo de trabajo que realmente usaría
Si tuviera que aplicar esto a una funcionalidad real la próxima semana, seguiría este orden.
Paso 1: Elige una funcionalidad de alcance delimitado y visible en el producto
No empieces con:
- «autenticación»
- «facturación»
- «notificaciones»
Empieza con algo más pequeño y visible, como:
- exportación de facturas a CSV
- flujo de configuración de SSO para un tenant
- aplicación de cupones durante el checkout
- filtrado de un dashboard con vistas guardadas
La funcionalidad debe tener:
- puntos de entrada claros
- estados de éxito y fallo claros
- efectos secundarios observables y claros
Eso hace que la ingeniería inversa y las pruebas de paridad sean mucho más prácticas.
Paso 2: Reúne un paquete de evidencia en el repositorio anterior
Antes de pedirle a cualquier herramienta que escriba una especificación, reúne la evidencia que describe el comportamiento actual:
- rutas, handlers y jobs en segundo plano
- estructuras de solicitudes y respuestas
- tablas o entidades almacenadas relevantes
- pruebas existentes
- logs, analíticas y eventos de auditoría
- feature flags
- capturas de pantalla o tickets de soporte si explican el comportamiento de los usuarios
- particularidades del despliegue y excepciones conocidas
Vista en ASCII:
old repo
-> routes
-> handlers
-> tests
-> logs
-> screenshots
-> flags
-> side effects
-> evidence pack
Esta es la parte que mantiene la especificación posterior anclada en la realidad.
Paso 3: Crea artefactos de Spec Kit del estado actual en el repositorio anterior
Aquí quiero ser muy disciplinado.
El propósito de la especificación en el repositorio anterior no es rediseñar la funcionalidad.
El propósito es documentar:
- qué hace la funcionalidad hoy
- qué esperan los usuarios y los sistemas que dependen de ella
- qué casos límite ya existen
- qué efectos secundarios son necesarios para generar confianza
Yo usaría el repositorio anterior como el lugar para crear un contrato de la funcionalidad en su estado actual.
Eso podría significar una rama y un conjunto de especificaciones orientados explícitamente a la documentación:
old-repo
specs/058-invoice-export-current-state/
spec.md
plan.md
tasks.md
Los nombres exactos de las carpetas dependen de ti, pero lo importante es que la intención sea evidente:
- esta es la funcionalidad actual tal como existe ahora
- esto todavía no es la implementación de la modernización
Paso 4: Extrae un contrato de paridad de los artefactos del repositorio anterior
Este es el puente.
No quiero que el nuevo repositorio dependa de una afirmación vaga como:
«haz que funcione como la aplicación anterior»
Quiero un contrato de migración conciso que responda:
- qué entradas importan
- qué salidas importan
- qué efectos secundarios importan
- qué expectativas no funcionales importan
- qué particularidades legacy son requisitos reales
- qué particularidades legacy son accidentales y pueden eliminarse
Este contrato puede escribirse como:
- ejemplos desarrollados
- listas de escenarios
- muestras de referencia de entradas y salidas
- expectativas sobre eventos y auditoría
- reglas y tolerancias para discrepancias
Una versión sencilla puede verse así:
Feature: invoice CSV export
Required outcomes
- Export includes only invoices visible to the requesting tenant.
- Columns must appear in the approved business order.
- Empty tax values serialize as empty strings, not zero.
- Export creation writes an audit event with actor, tenant, and export ID.
Allowed modernization differences
- Job execution may move from synchronous controller logic to async worker flow.
- Internal storage layer may change.
- UI trigger can move from page action to command palette if PM approves.
Eso ya es mucho más útil que copiar tres servicios y esperar que se conserve el significado.
Paso 5: Crea artefactos de Spec Kit del estado objetivo en el nuevo repositorio
Ahora cambiamos de repositorio.
La especificación del nuevo repositorio debe escribirse a partir del contrato de paridad, no del código copiado.
Aquí es donde el nuevo stack puede ser moderno de manera deliberada.
La especificación debe preservar:
- los resultados de negocio
- el comportamiento observable
- los efectos secundarios requeridos
- la seguridad del despliegue
La especificación debe adaptar:
- la arquitectura
- los límites entre módulos
- el uso del framework
- el flujo de datos
- la implementación de la observabilidad
Flujo en ASCII:
old repo evidence
-> current-state spec
-> parity contract
-> new repo spec
-> new repo plan
-> new repo tasks
Este es el momento en que la migración pasa a ser de especificación a especificación en lugar de código a código.
Paso 6: Implementa una pequeña parte y compara los resultados
En este punto yo resistiría la tentación de «terminar toda la migración».
En su lugar:
- elige la parte útil más pequeña
- impleméntala en el nuevo repositorio
- compara los resultados con el sistema anterior
- registra las discrepancias
- decide si la discrepancia es:
- un bug
- un cambio aceptable de modernización
- un comportamiento legacy que se entendió mal
Aquí es donde los puntos de intervención, las sondas y la ejecución en paralelo se vuelven muy prácticos.
Un harness de comparación puede ser muy pequeño:
const input = buildInvoiceExportRequest(sampleTenant, sampleFilters);
const legacyResult = await legacyExport(input);
const modernResult = await modernExport(input);
expect(normalizeExport(modernResult.csv)).toEqual(
normalizeExport(legacyResult.csv)
);
expect(modernResult.auditEvent).toMatchObject({
actorId: legacyResult.auditEvent.actorId,
tenantId: legacyResult.auditEvent.tenantId,
action: "invoice_export_created",
});
Ese ejemplo es sencillo a propósito.
Lo importante no es la biblioteca de pruebas exacta. Lo importante es:
- comparar el comportamiento
- normalizar las diferencias irrelevantes
- mantener visible el contrato de migración
Paso 7: Haz la transición gradualmente y sigue actualizando el contrato
Una vez que una parte se comporte correctamente, mueve el tráfico de forma gradual:
- haz un lanzamiento oscuro si es posible
- usa un despliegue canario si corresponde
- compara logs y resultados
- conserva un registro de discrepancias
Después actualiza los artefactos de especificación según lo que aprendiste.
Esto importa porque una migración casi siempre revela una de estas tres cosas:
- el sistema legacy tenía un comportamiento que nadie documentó
- el nuevo sistema expuso una suposición oculta
- el equipo se dio cuenta de que no valía la pena preservar algún comportamiento legacy
Un buen proceso de migración hace explícitos esos descubrimientos.
Ejemplos de prompts
Así es como escribiría los prompts para el repositorio anterior y para el nuevo.
Repositorio anterior: prompt de ingeniería inversa del estado actual
/speckit.specify
Document the current behavior of the invoice CSV export feature exactly as it
works today.
Use the existing routes, handlers, tests, logs, feature flags, and audit events
as evidence.
Capture:
- user-visible behavior
- required side effects
- error cases
- edge cases already covered by tests
Do not redesign the feature.
Do not propose improvements yet.
If behavior is unclear, mark it as a clarification question instead of inventing
an answer.
Después:
/speckit.clarify
Focus on undocumented edge cases, tenant isolation rules, export column ordering,
empty-value serialization, and audit event expectations.
Nuevo repositorio: prompt de migración al estado objetivo
/speckit.specify
Implement invoice CSV export in this repository using the approved migration
contract from the legacy system.
Preserve:
- tenant visibility rules
- export column order
- empty tax serialization behavior
- required audit event semantics
Adapt the implementation to this repo's architecture, observability, and async
processing model.
Do not copy legacy module boundaries unless they are required by the contract.
Después:
/speckit.plan
Create a thin-slice implementation plan for the new repository.
Include:
- the minimal vertical slice
- rollout and comparison strategy
- parity validation points
- discrepancy logging approach
- cutover criteria
El ciclo de validación
Creo que esta es la pieza que hace creíble todo el flujo de trabajo.
Sin un ciclo de validación, la «migración de especificación a especificación» corre el riesgo de convertirse en una reescritura con un nombre atractivo.
Con un ciclo de validación, se convierte en un ciclo de modernización controlado:
legacy feature
-> observe
-> specify
-> extract examples
-> build thin slice in new repo
-> compare outputs and side effects
-> resolve discrepancies
-> expand traffic
-> retire old path
Por eso prefiero esta idea a copiar código cuando hay cambios importantes de stack.
El código copiado suele dar una falsa sensación de seguridad.
La comparación de resultados te da seguridad real.
Una tabla de decisión sencilla
| Situación | Mejor opción predeterminada |
|---|---|
| Mismo repositorio, mismo stack, refactor pequeño | Refactorizar o extraer el código directamente |
| Nuevo repositorio, stack diferente, acoplamiento legacy oculto | Migración de especificación a especificación |
| El comportamiento del producto de todos modos está cambiando mucho | Nueva especificación del estado objetivo con evidencia legacy seleccionada |
| Todavía no existe evidencia confiable | Hacer una pausa y reunir primero el paquete de evidencia |
| Se requiere reutilizar exactamente un binario o algoritmo | Reutilizar el componente probado y después especificar la integración a su alrededor |
Los riesgos a tener en cuenta
Me gusta este flujo de trabajo, pero no lo presentaría como algo sin fricciones.
1. La ingeniería inversa también puede preservar comportamientos incorrectos con demasiada fidelidad
Si no tienes cuidado, la documentación del estado actual convierte bugs y particularidades accidentales en requisitos intocables.
Por eso me gusta separar:
- paridad requerida
- bugs conocidos
- comportamientos legacy cuya eliminación se aprobó explícitamente
2. Los equipos pueden confundir los «ejemplos» con «toda la especificación»
Los ejemplos son poderosos, pero por sí solos no capturan:
- reglas de despliegue
- expectativas de observabilidad
- restricciones de retención de datos o cumplimiento normativo
- responsabilidades operativas
Todavía necesitas la especificación y el plan completos.
3. Este flujo de trabajo todavía necesita un modelo operativo para proyectos brownfield
GitHub Spec Kit nos da los comandos y el flujo de artefactos. Pero todavía no elimina la necesidad de decidir:
- dónde vive el contrato canónico de migración
- cómo se referencian entre sí las especificaciones del repositorio anterior y del nuevo
- cómo se clasifican y priorizan las discrepancias
- cuándo es aceptable una brecha de paridad
Ese es trabajo de diseño del equipo.
4. Algunas migraciones sí justifican reutilizar código
Si una biblioteca pequeña y bien aislada ya hace lo correcto y es portable, reutilizarla puede ser más inteligente que volver a especificarla.
Así que no convertiría esto en una ideología.
La pregunta no es:
«¿nunca debemos copiar código?»
La verdadera pregunta es:
«¿cuál es la unidad de migración más segura para esta funcionalidad?»
Para muchas migraciones de funcionalidades entre stacks, creo que la respuesta sigue siendo la especificación.
Conclusión
Si tuviera que condensar todo el artículo en una frase, sería esta:
Cuando una funcionalidad pasa a un nuevo repositorio y un nuevo stack, migra primero el contrato de comportamiento y deja que la implementación renazca a partir de ese contrato.
Ese es el corazón de la migración de especificación a especificación.
No está en contra del código.
Está en contra del acoplamiento accidental.
Y para los ingenieros y PMs que intentan modernizar una funcionalidad sin arrastrar toda una visión legacy a una nueva base de código, esa es una distinción muy práctica.
Lista de fuentes
Validado con estas fuentes el April 14, 2026.
Fuentes oficiales y primarias
- GitHub Spec Kit, README
- Documentación de GitHub Spec Kit, Guía de instalación
- Documentación de GitHub Spec Kit, Guía de inicio rápido
- Carnegie Mellon SEI, Guías para la reconstrucción de arquitectura, 3.ª edición
- Carnegie Mellon SEI, Un framework para la modernización incremental de sistemas legacy
- Martin Fowler, Aplicación Strangler Fig
- Martin Fowler, Punto de intervención en sistemas legacy
- Thoughtworks, Descubrir puntos de intervención en mainframes para la modernización incremental
- Gojko Adzic, Specification by Example, 10 años después
- Gojko Adzic, Cambiemos el discurso
Señales de la comunidad que usé para entender la brecha en proyectos brownfield
- Discusión de GitHub Spec Kit #746, Spec-kit para un proyecto brownfield complejo