DEV Community

Cover image for Spec-Driven Development (SDD) con IA: Dejando atrás el 'Vibe Coding' para construir software multiplataforma de grado profesional
David Bernardo
David Bernardo

Posted on Originally published at github.com

Spec-Driven Development (SDD) con IA: Dejando atrás el 'Vibe Coding' para construir software multiplataforma de grado profesional

En los últimos meses, el término "Vibe Coding" se ha popularizado en redes y comunidades técnicas: sentarte frente al editor, lanzar prompts vagos a un modelo de lenguaje de última generación, ver cómo el código aparece mágicamente y ajustar a prueba y error hasta que "parezca funcionar".

Para un script de fin de semana o un prototipo desechable, esto puede sentirse como magia. Pero cuando intentas construir software de producción, con concurrencia real, arquitecturas multiplataforma, contratos estrictos y rendimiento de ultra-baja latencia, el "Vibe Coding" se estrella contra la realidad:

  1. La muerte por mil parches: Pides un botón, la IA rompe el estado. Pides arreglar el estado, rompe el ciclo de vida. Pides arreglar el ciclo de vida, introduce fugas de memoria.
  2. La ilusión de avance: El código crece exponencialmente, pero la comprensibilidad del sistema cae en picada.
  3. Imposibilidad de auditoría: Ningún desarrollador humano —ni ningún otro agente de IA en el futuro— puede entender por qué se tomaron ciertas decisiones, cuáles eran los supuestos de diseño o cómo refactorizar sin derrumbar el castillo de naipes.

Existe una alternativa profesional, rigurosa y escalable: Spec-Driven Development (SDD) asistido por Inteligencia Artificial.

En este artículo, desglosaremos la metodología exacta, la arquitectura y los prompts operativos que utilizamos para construir Virtual Studio Companion: una suite multiplataforma con Kotlin Multiplatform (KMP) y Compose Multiplatform que transforma un dispositivo móvil Android en una cámara de transmisión de ultra-baja latencia para OBS Studio en Windows, integrando monitor de video nativo con Skia, código QR ISO/IEC 18004, telemetría bidireccional y cero dependencias de Java en el cliente final.

Todo el código y sus especificaciones están disponibles en el repositorio de código abierto:

👉 GitHub: dhbernardo/virtual-studio-companion (Tag v1.0.1)


1. ¿Qué es Spec-Driven Development (SDD)?

En el desarrollo tradicional, el código fuente suele ser la única fuente de verdad ("el código es la documentación"). Con la llegada de los agentes de IA, esta premisa es peligrosa: los modelos operan sobre ventanas de contexto limitadas y tienden a optimizar soluciones locales a corto plazo sin considerar la integridad global.

En Spec-Driven Development (SDD):

Las especificaciones son documentos vivos y la ÚNICA fuente de verdad (Single Source of Truth). Ninguna línea de código de producción se escribe o modifica si no está expresamente modelada en su especificación.

flowchart LR
    A["📜 spec.md<br/>(Requisitos EARS & Criterios BDD)"] --> B["📐 plan.md<br/>(Arquitectura & Diagramas)"]
    B --> C["✅ tasks.md<br/>(Tareas Atómicas 'Hecho cuando:')"]
    C --> D["🧪 TDD Estricto<br/>(Test Primero -> Código -> Build)"]
    D --> E["📦 Conventional Commit<br/>(Trazabilidad 1:1)"]

La regla de oro ante defectos y restricciones imprevistas

Cuando durante las pruebas de integración descubrimos una restricción de plataforma (por ejemplo, la política Cleartext de Android o una condición de carrera de concurrencia en sockets), NO creamos tareas sueltas ni parches desordenados (e.g. T09: arreglar socket, T10: parche de botones).

Crear tareas sueltas convierte el repositorio en un historial caótico de parches y debilita la especificación principal. Lo canónico en SDD es refinar las especificaciones y tareas existentes en su contexto original (spec.md, plan.md, tasks.md). De este modo, cualquier ingeniero o agente que audite el sistema meses después encontrará la especificación exacta, autocontenida y fiel a la realidad.


2. El Marco de Gobernanza: docs/constitution.md y AGENTS.md

Para que un agente de IA trabaje con autonomía de alto nivel sin descarriarse, necesita barreras innegociables. En nuestro proyecto, esto se implementó a través de dos archivos fundamentales:

1. La Constitución del Proyecto (docs/constitution.md)

Establece las 8 leyes fundamentales que ningún prompt ni decisión técnica puede violar:

Principio Regla Innegociable
1. Núcleo Puro (commonMain) Prohibido importar android.*, java.* o librerías de plataforma en el código compartido. Todo se resuelve mediante Puertos e Interfaces.
2. Cero Java en el Cliente El ejecutable de Windows se empaqueta con jpackage y un micro-runtime Temurin (jlink). El usuario final no necesita Java.
3. Licenciamiento Permisivo Prohibidas librerías con copyleft restrictivo (GPLv3). Solo Apache 2.0, MIT, BSD.
4. Resiliencia de Red Nunca asumir que mDNS funciona (por routers con AP Isolation). El código QR es el mecanismo primario incondicional.
5. Paridad de Temas Soporte estricto para modo oscuro (StudioDarkColors) y modo claro (StudioLightColors), detectando el sistema (isSystemInDarkTheme()).
6. SDD y TDD Primero No se implementa código sin prueba unitaria previa que valide el comportamiento esperado.
7. CI/CD sin Fricción Pipelines automatizados en GitHub Actions para Linux (app-debug.apk) y Windows (.exe / .msi).
8. Cero Hardcoded Strings Prohibido escribir texto literal en UI o composables. El 100% de textos proviene de Res.string.*.

2. El Contexto del Agente (AGENTS.md)

Define la topología del monorepo, los comandos frecuentes (./gradlew allTests, ./gradlew :desktopApp:desktopTest), el flujo de trabajo SDD y la prohibición expresa de improvisar código.


3. La Anatomía de una Especificación SDD (specs/<feature>/)

Cada módulo o funcionalidad del sistema vive dentro de specs/ y se descompone en tres artefactos complementarios:

specs/003-desktop-windows-host/
├── spec.md      # Requisitos del Negocio y Criterios de Aceptación
├── plan.md      # Diseño Técnico, Diagramas y Decisiones Arquitectónicas
└── tasks.md     # Checklist de Tareas Incrementales y Verificables
Enter fullscreen mode Exit fullscreen mode

A. spec.md: Requisitos EARS y Criterios BDD

Los requisitos se redactan usando la sintaxis EARS (Easy Approach to Requirements Syntax) y criterios de aceptación BDD (Dado-Cuando-Entonces):

### RF-003: Integración Automática, Control Simétrico y Resiliencia con OBS Studio
- **Tipo:** Event-driven
- **Definición:** CUANDO el Host detecte que OBS Studio está en ejecución (o al pulsar "Conectar OBS"), el sistema DEBE conectarse al puerto 4455 implementando IObsConnector, resolver el desafío SHA256 y crear/actualizar la fuente Browser Source "Virtual Studio Camera".
- **Criterio de Aceptación:**
  - **DADO QUE** OBS Studio está conectado activamente
  - **CUANDO** el usuario pulsa "Desconectar OBS" en el dashboard
  - **ENTONCES** el Host debe cerrar limpiamente el socket con OBS, transitar a DISCONNECTED y cancelar reintentos, preservando intacta la transmisión móvil activa y el monitor de retorno Skia.
Enter fullscreen mode Exit fullscreen mode

B. plan.md: Diagramas de Secuencia y Arquitectura

Incluye diagramas Mermaid que modelan el flujo de estados y la secuencia de comunicación entre el streamer, el Host Ktor, la app Android y OBS Studio:

sequenceDiagram
    autonumber
    actor Streamer as Streamer (Windows)
    participant Host as Desktop Host (Compose + Ktor)
    participant Phone as Android App
    participant OBS as OBS Studio (:4455)

    Host->>Host: Enlaza Ktor (8080-8090) & Genera QR con TTL (120s)
    Phone->>Host: Escanea QR y envía HANDSHAKE_INIT(token)
    Host->>Phone: HANDSHAKE_ACK (Sesión validada)
    Host->>OBS: CreateInput / SetInputSettings ("browser_source")
    Phone->>Host: Transmite tramas JPEG binarias (/ws/stream)
    Host->>Host: Decodifica con Skia (Image.makeFromEncoded)
    Host->>Streamer: Renderiza Monitor de Retorno 16:9 en Compose Desktop
    Host->>OBS: Sirve stream multipart (/stream/mjpeg)

C. tasks.md: La Condición "Hecho cuando:"

Cada tarea atómica define una condición estricta e incontrovertible. Un checkbox [x] no se marca por intuición; solo se marca si la condición se cumple y la terminal lo certifica:

- [x] **T04: Implementación de `ObsWebSocketAdapter` implementando `IObsConnector`**  
  *Requisitos cubiertos:* `RF-003`, `RNF-003`  
  *Hecho cuando:* El adaptador resuelva el handshake autenticado en `ws://localhost:4455`, implemente `KtorObsTransport` con buffer de repetición (`replay = 1`) previniendo pérdida del frame `op: 0`, soporte desconexión explícita (`disconnect()`) sin bucles en `finally`, configure de forma idempotente la fuente Browser Source `Virtual Studio Camera`, y apruebe las pruebas de `T03`.
Enter fullscreen mode Exit fullscreen mode

4. El Kit de Prompts Operativos (Copia y Pega)

Aquí radica el secreto para que una IA no degrade el proyecto. Diseñamos cuatro prompts clave para cada momento del ciclo de vida del software:

Prompt 1: Ejecución Secuencial Completa (Batch SDD)

Utilízalo cuando la especificación, el plan y las tareas estén completamente definidos y desees que el agente ejecute con rigor senior:

Actúa como un desarrollador senior y ejecuta de forma autónoma y secuencial TODAS las tareas pendientes de `[NUMERO-NOMBRE-SPEC]/tasks.md`.

Antes de comenzar:
1. Lee `docs/constitution.md` y `AGENTS.md` (respetando todas las restricciones: cero strings en crudo, licencias permisivas, independencia de `shared/commonMain`).
2. Lee `[NUMERO-NOMBRE-SPEC]/plan.md` y `[NUMERO-NOMBRE-SPEC]/spec.md`.

Para CADA tarea pendiente en `tasks.md`, ejecuta en bucle estricto:
1. IDENTIFICACIÓN:
   - Identifica los requisitos (RF/RNF) y la condición "Hecho cuando:".
   - Clasifica la tarea:
     a) ¿Es Lógica de Dominio, Protocolo, Caso de Uso o ViewModel? -> APLICA TDD ESTRICTO (Paso 2A).
     b) ¿Es Infraestructura de Build, CI/CD, Tokens o Recursos? -> APLICA VERIFICACIÓN DE BUILD (Paso 2B).

2. EJECUCIÓN SEGÚN EL TIPO:
   - [Paso 2A - TDD]:
     1. Escribe primero las pruebas unitarias pertinentes (`*Test.kt`) cubriendo casos normales, límites y de error.
     2. Implementa el código mínimo para hacer pasar los tests.
     3. Ejecuta el comando de test correspondiente (ej. `./gradlew :shared:allTests` o `./gradlew :desktopApp:desktopTest`).
   - [Paso 2B - Infraestructura / Configuración]:
     1. Implementa los archivos de configuración, scripts de build o recursos.
     2. Ejecuta el comando de validación o compilación (ej. `./gradlew assembleDebug` o linters).

3. EVIDENCIA EN RESPUESTA:
   - Muestra el extracto de la terminal que certifique el éxito (`BUILD SUCCESSFUL` o `Tests PASSED`).

4. ACTUALIZACIÓN DE TASKS:
   - Marca con `[x]` el checkbox en `tasks.md` ÚNICAMENTE si se cumple la condición "Hecho cuando:".

5. GIT COMMIT:
   - Crea un commit con formato Conventional Commits en inglés (según `git-conventions`).

AL FINALIZAR TODAS LAS TAREAS DEL SPEC:
- Presenta la Matriz de Trazabilidad de Requisitos.
- Confirma que no se vulneró ningún Principio de `docs/constitution.md`.
Enter fullscreen mode Exit fullscreen mode

Prompt 2: Ejecución Tarea por Tarea (Paso a Paso Supervisado)

Ideal para desarrolladores que prefieren un control minucioso antes de cada avance:

Actúa como un desarrollador senior.
Lee `docs/constitution.md`, `AGENTS.md`, y el trío `specs/[MODULO]/spec.md`, `plan.md` y `tasks.md`.

Ejecuta EXCLUSIVAMENTE la tarea [ID_TAREA] de `tasks.md`.
1. Aplica TDD estricto si es lógica/UI: escribe el test unitario primero, hazlo fallar, implementa el código mínimo para que pase.
2. Ejecuta el comando de verificación en terminal correspondiente.
3. Cumplida la condición "Hecho cuando:", marca `[x]` en `tasks.md`.
4. Crea el commit atómico en inglés siguiendo Conventional Commits.
5. DETENTE y espera mi confirmación antes de proceder con la siguiente tarea.
Enter fullscreen mode Exit fullscreen mode

Prompt 3: El Diagnóstico Crítico ("Cero Parches Prematuros")

Cuando ocurre un error durante las pruebas manuales o automatizadas, nunca le pidas a la IA que lo arregle de inmediato. Primero aíslala para que analice la causa raíz:

[Pega aquí los logs de terminal, trazas de excepción, comportamiento anómalo observado o contexto de la prueba realizada]

Identifica los problemas, no plantees soluciones todavía. Solicita si requieres más contexto, trazas o mayor detalle de lo que está ocurriendo.
Enter fullscreen mode Exit fullscreen mode

¿Por qué este prompt es vital?

Si le dices a la IA "tengo este error, arréglalo", asumirá una solución apresurada que casi siempre consiste en parchotear el código local, rompiendo contratos arquitectónicos. Al forzarla a limitarse a diagnosticar, la IA analiza el flujo de llamadas, identifica condiciones de carrera o inconsistencias de estado, y tú como desarrollador validas la hipótesis técnica antes de que toque un solo archivo.


Prompt 4: El Refinamiento SDD Canónico (Sin Tareas Sueltas)

Una vez identificado y consensuado el problema, utiliza este prompt para actualizar la especificación:

Crear un plan para refinar las especificaciones y tareas existentes, planes, evitar crear tareas sueltas, ya que esto convierte el proyecto en un historial caótico de parches y debilita la especificación principal (rompiendo las reglas de SDD).
De esta manera, cualquier desarrollador o agente que en el futuro vuelva a implementar o auditar el sistema tendrá la especificación exacta y autocontenida.
Enter fullscreen mode Exit fullscreen mode

5. Caso de Estudio Real: Virtual Studio Companion

Para ilustrar el poder de esta metodología, veamos cómo resolvimos cuatro desafíos de alta complejidad durante el desarrollo de nuestro sistema.

¿Qué hace la aplicación?

  • Móvil Android: Captura video a 1080p con CameraX, comprime cuadros a JPEG aplicando la matriz de rotación del sensor en un subproceso dedicado (CameraX-Worker), calcula FPS/bitrate reales en una ventana deslizante de 1000 ms, y transmite tramas binarias por WebSocket hacia el Host. Escanea códigos QR con Google ML Kit acelerado por hardware.
  • Desktop Windows (Host): Servidor local embebido con Ktor (fallback automático en puertos 8080-8090). Genera códigos QR conforme a ISO/IEC 18004 con ZXing Core. Decodifica las tramas de video en tiempo real mediante Skia (org.jetbrains.skia.Image.makeFromEncoded) para renderizar un Monitor de Retorno de Video Nativo en Compose Desktop.
  • Integración con OBS Studio: Sirve un stream multipart HTTP continuo en /stream/mjpeg y una página web responsive en /stream/preview. Se comunica vía WebSocket con OBS Studio v5, resolviendo desafíos criptográficos SHA256 para inyectar automáticamente una fuente Browser Source en la escena activa.
  • Sincronización Bidireccional: Control remoto de zoom y linterna en ambas direcciones sin bucles de rebote (echo loops), junto a telemetría viva de batería, estado térmico y latencia RTT (Ping/Pong).

Dashboard de Transmisión del Host con Monitor Skia


Caso 1: La Política Cleartext y el Visor Perforado en Android

  • El problema: Al intentar conectar por primera vez el teléfono al Host por Wi-Fi, Android abortaba con UnknownServiceException: CLEARTEXT communication not permitted. Además, el visor de escaneo de QR renderizaba un recuadro negro sólido sobre la cámara.
  • El enfoque "Vibe Coding" habría hecho: Agregar un flag suelto en el manifest y poner hacks en la vista.
  • Cómo se resolvió en SDD:
    1. Se refinó RF-001 en specs/004-android-mobile-app/spec.md para abarcar el soporte de contingencia manual (ingreso de IP y token) y la política de red local como requisito no funcional formal (RNF-005).
    2. Se implementó network_security_config.xml habilitando tráfico local en redes privadas.
    3. En ScannerOverlay, se descubrió que BlendMode.Clear perforaba las capas de Compose hasta el fondo negro nativo. Se refinó la tarea canónica T08 para aplicar Modifier.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen), logrando un recorte translúcido al 50% con nitidez cristalina.

Dashboard de Transmisión del Host con Monitor Skia


Caso 2: El Rebote Reactivo en el Arranque de Sesión

  • El problema: Al enfocar el código QR, la app móvil detectaba el token, abría la pantalla de streaming y de inmediato rebotaba hacia la pantalla de escaneo.
  • El diagnóstico con Prompt 3: La pantalla CameraScreen observaba uiState.connectionState. Como el ViewModel nacía por defecto en Disconnected y la corrutina de conexión demoraba unos milisegundos en arrancar, el observador interpretaba el estado inicial como una desconexión y llamaba a onDisconnect().
  • El refinamiento con Prompt 4: Se actualizaron las tareas canónicas T06 y T07 de 004-android-mobile-app:
    • CameraStreamViewModel ahora acepta initialConfig: PairingConfig? y nace de inmediato en estado ConnectionState.Pairing(config).
    • Se incorporó un guardián reactivo (hasActiveSession) que solo permite ejecutar onDisconnect() si la sesión estuvo previamente en Connected o Streaming.

HUD de Estudio en Android con Controles Nítidos


Caso 3: La Condición de Carrera en el WebSocket de OBS (replay = 1)

  • El problema: Si el usuario conectaba OBS antes de emparejar el móvil, funcionaba perfecto. Pero si emparejaba el móvil primero y luego pulsaba "Conectar OBS", el Host caía invariablemente en OBS Error y Conectando..., a pesar de que el stream de video seguía activo en OBS.
  • El diagnóstico con Prompt 3:

    1. En KtorObsTransport, el flujo de mensajes entrantes estaba configurado como:
     // ❌ ANTES: replay = 0 por defecto
     private val _incoming = MutableSharedFlow<String>(extraBufferCapacity = 64)
    
  1. Cuando el móvil ya está transmitiendo video, el despachador de hilos de la CPU se encuentra bajo carga procesando tramas binarias y decodificando imágenes Skia.
  2. Al pulsar "Conectar OBS", el socket se abría y OBS Studio respondía de inmediato con el frame inicial op: 0 (Hello).
  3. Pero el recolector de corrutinas (transport.incoming.collect) tardaba unos pocos milisegundos en suscribirse. Al tener replay = 0, el mensaje op: 0 se descartaba.
  4. El adaptador se quedaba esperando un saludo que nunca llegaría, alcanzaba el timeout de 4 segundos y caía en error.
  5. En las pruebas unitarias pasaba porque el mock FakeObsTransport tenía por casualidad replay = 1 configurado.
    • El refinamiento con Prompt 4: Se refinaron RF-003 y las tareas T03 y T04 en specs/003-desktop-windows-host/:
  // ✅ DESPUÉS: replay = 1 garantizado y suscripción atómica previa
  private val _incoming = MutableSharedFlow<String>(replay = 1, extraBufferCapacity = 64)
Enter fullscreen mode Exit fullscreen mode

Además, la suscripción al canal se realiza antes de abrir el socket de red y bajo protección de un Mutex.

Configuración Idempotente de Browser Source en OBS Studio


Caso 4: Desconexión Simétrica e Independiente de OBS

  • El problema: Cuando OBS se conectaba, el botón "Conectar OBS" desaparecía de la interfaz y no había forma de desconectarlo. El único botón visible era "Desconectar", pero ese botón cerraba la sesión del móvil y devolvía la app al código QR.
  • El refinamiento SDD: Se actualizaron RF-003, RF-004, T05 y T06:
    • Se implementó DesktopHostViewModel.disconnectObs() desacoplado de disconnectSession().
    • Se diseñaron botones contextuales en Compose Desktop: si está conectado, muestra Desconectar OBS (Res.string.desktop_action_disconnect_obs); si está desconectado, muestra Conectar OBS.
    • El botón rojo general queda reservado para la sesión del teléfono.

Pantalla de Emparejamiento QR con Token Legible y Estado OBS

airing.png)


6. El Flujo Git: Tareas Atómicas y Conventional Commits

En un flujo SDD profesional, Git no es un vertedero de cambios acumulados. Cada commit representa la culminación verificable de una tarea o refinamiento:

# Historial real del repositorio Virtual Studio Companion
58deef0 feat(desktop): support independent obs disconnect and fix websocket transport race condition
66c9063 docs(specs): refine obs connection and disconnect specifications and tasks
81e14c8 feat(android): implement jpeg frame streaming, live telemetry, and camera controls synchronization
0aaff28 feat(desktop): implement video frame ingestion, skia return monitor, and mjpeg preview for obs
36e58fc docs(specs): refine video streaming and camera controls sync specifications
8395458 fix(android): prevent premature screen exit and initialize CameraStreamViewModel in pairing state
75e5b79 docs(specs): refine mobile pairing initialization and transition guard in spec and tasks
2a64def docs(specs): refine desktop, android, and design system specs and tasks
0c9d6dc fix(desktop): handle client disconnect, restore session disconnect and obs connect in dashboard
a2db8ef fix(android): consume scanned config, handle server disconnect and lifecycle events
Enter fullscreen mode Exit fullscreen mode

Observemos la simetría: cada cambio de código (feat o fix) está precedido o acompañado por su correspondiente actualización en especificaciones (docs(specs)).


7. Conclusiones: El Futuro del Desarrollo Asistido por IA

El debate actual no debería ser si los modelos de lenguaje pueden escribir código rápido —ya sabemos que pueden—. El verdadero desafío es cómo gobernar a la IA para construir sistemas duraderos, tolerantes a fallos y arquitectónicamente limpios.

Al adoptar Spec-Driven Development (SDD):

  1. La IA se convierte en un multiplicador de ingeniería: En lugar de perder horas persiguiendo regresiones aleatorias, guías a la IA a través de contratos estrictos, TDD y validación determinista en terminal.
  2. El proyecto es 100% auditable: Cualquier ingeniero humano o nuevo agente puede inspeccionar specs/, leer los requisitos EARS, revisar los diagramas de secuencia y entender la totalidad del sistema en minutos.
  3. El código resiste el paso del tiempo: La arquitectura Clean y la separación de capas garantizan que el núcleo multiplataforma permanezca puro e inmune a las vicisitudes de las APIs de plataforma.

Recursos y Código Fuente


¿Has probado implementar Spec-Driven Development en tus flujos de trabajo con agentes de IA? ¡Déjame tus comentarios, dudas o experiencias abajo!

Top comments (0)