---
title: "Spec-Driven Development en React Native: un MVP que lista los sismos del mundo, especificado antes de escribir código"
description: "Apliqué Spec-Driven Development a un MVP en React Native que lista los sismos del mundo: la especificación decide qué entra en la lista y sus criterios de aceptación se convierten en los tests."
author: Ramón Chancay
date: 2026-09-05
lang: es
tags: [Spec-Driven Development, React Native, Expo, Agentes de código, MVP]
canonical: https://www.ramonchancay.me/es/blog/spec-driven-development-react-native-app-sismos
---

# Spec-Driven Development en React Native: un MVP que lista los sismos del mundo, especificado antes de escribir código

Un agente de código ya escribe una app funcional en una tarde. El problema dejó de ser producir el código y pasó a ser otro: el agente no escribe peor código sin una especificación, escribe un código excelente para el problema equivocado. Cada pregunta que no contestaste la contesta él con un valor por defecto razonable, y lo descubres semanas después. El Spec-Driven Development (SDD) ataca justo eso: escribir primero qué debe hacer el software, resolver por escrito lo que está ambiguo, y tratar el código como el resultado de esa especificación. Lo apliqué a un MVP en React Native con Expo que hace una sola cosa —mostrar un listado de los sismos recientes en todo el mundo— porque es el tipo de pedido que cabe en una línea y esconde más decisiones de las que parece.

<details class="rc-tldr">
<summary class="rc-tldr-btn"><span class="rc-tldr-dot"></span> Resumen para perezosos <svg class="rc-tldr-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></summary>
<div class="rc-tldr-body">
<ul>
<li>SDD separa tres artefactos: la especificación (qué y por qué), el plan técnico (cómo) y las tareas. El agente implementa contra ellos en vez de contra un prompt suelto.</li>
<li>El valor real no es el documento: es que las ambigüedades quedan marcadas y resueltas por escrito antes de que el agente elija por ti. Qué sismos entran en la lista, en qué orden y qué pasa sin conexión son decisiones de producto, no de código.</li>
<li>Los criterios de aceptación se escriben como una tabla de casos y esa misma tabla se convierte en el test. Si la fila no está en la tabla, el comportamiento no está definido.</li>
</ul>
</div>
</details>

**En este artículo:**

- **Fundamentos** — [Qué es SDD](#qué-es-el-spec-driven-development-y-en-qué-se-diferencia-de-prompting) · [Por qué un listado de sismos](#por-qué-un-listado-de-sismos-esconde-más-decisiones-de-las-que-parece)
- **Especificación** — [El qué antes del cómo](#la-especificación-el-qué-antes-del-cómo) · [El plan técnico](#el-plan-técnico-cómo-se-implementa-en-react-native) · [De la spec a los tests](#de-la-especificación-a-los-tests-la-tabla-de-aceptación)
- **Implementación** — [Implementar con un agente](#implementar-contra-la-especificación-con-un-agente) · [Cuando los datos contradicen](#cuando-los-datos-reales-contradicen-la-especificación) · [Lo que no se deja especificar](#qué-parte-de-una-app-móvil-no-se-deja-especificar) · [Cuándo no usarlo](#cuándo-no-usar-spec-driven-development)

## Qué es el Spec-Driven Development y en qué se diferencia de prompting

Un prompt es una instrucción que se consume y desaparece. Una especificación es un archivo versionado en el repositorio, que se revisa en un pull request y que sobrevive a la sesión del agente. Esa es toda la diferencia, y es más grande de lo que parece.

En el flujo típico con un agente, describes la funcionalidad en el chat y el resultado son mil líneas de código más una conversación que nadie va a releer. Las decisiones importantes —la magnitud mínima, cuántas horas hacia atrás, qué pasa sin conexión— quedaron en un turno intermedio del chat. Cuando alguien pregunta por qué la app muestra un sismo de magnitud 2.6 y no uno de 2.4, la respuesta no está en ninguna parte.

SDD separa el trabajo en artefactos distintos, cada uno con su propio nivel de abstracción:

```text
Especificación   →  qué debe pasar y por qué
      │
      ▼
Plan técnico     →  cómo se implementa (stack, librerías, estructura)
      │
      ▼
Tareas           →  unidades de trabajo verificables
      │
      ▼
Implementación   →  el código que el agente escribe contra lo anterior
```

La regla que ordena todo es que cada nivel habla de su nivel. La especificación evita decisiones de implementación salvo que sean una restricción real del producto: "debe funcionar sin conexión" o "los datos no salen del dispositivo" son requisitos aunque tengan consecuencias técnicas; "usar TanStack Query" no lo es. El plan sí nombra tecnología, pero no reabre decisiones de producto. Cuando esa separación se respeta, puedes cambiar de stack sin reescribir la especificación, y cambiar un umbral sin tocar el plan.

Hay herramientas que formalizan este flujo. [Spec Kit](https://github.com/github/spec-kit), de GitHub, instala una serie de comandos en tu agente que producen exactamente estos artefactos en carpetas del repositorio:

```bash
# Instala el flujo en el proyecto y elige el agente que vas a usar.
uvx --from git+https://github.com/github/spec-kit.git specify init sismos-app

# A partir de ahí, dentro del agente:
#   /constitution  → principios del proyecto (aplican a todas las features)
#   /specify       → la especificación de esta feature
#   /clarify       → resuelve las ambigüedades marcadas en la spec
#   /plan          → el plan técnico
#   /tasks         → el desglose en tareas
#   /implement     → ejecuta las tareas
```

No hace falta la herramienta: tres archivos markdown en `specs/` y la disciplina de mantenerlos alcanzan. Lo que aporta es que el agente tiene los comandos, las plantillas y el orden ya cargados, y no se salta pasos.

## Por qué un listado de sismos esconde más decisiones de las que parece

El pedido inicial fue una frase: "una app que liste los sismos del mundo". Un agente puede implementar eso en una tarde y el resultado será plausible, compilará y mostrará una lista. También estará mal, porque la frase esconde por lo menos seis decisiones:

- **Qué es "un sismo"**: las redes sísmicas registran muchos más eventos de los que una persona percibe, la mayoría por debajo de magnitud 2. ¿Se listan todos?
- **Qué es "reciente"**: ¿la última hora? ¿el último día? ¿la última semana?
- **En qué orden**: ¿por hora? ¿por magnitud? ¿Qué va primero si dos ocurren al mismo tiempo?
- **Qué se muestra de cada uno**: magnitud, lugar, hora, profundidad… ¿en qué zona horaria?
- **Qué pasa sin conexión**: ¿lista vacía? ¿la última descargada? ¿con qué aviso?
- **Qué pasa si el evento cambia**: la magnitud de un sismo se revisa después de la detección automática. ¿Aparece dos veces?

Ninguna de esas preguntas es técnica. Todas cambian el producto. Y todas, si no las contestas tú, las contesta el agente en silencio: la lista termina mostrando cuatrocientos microsismos de magnitud 1 y el único de magnitud 6 del día queda en la posición ochenta.

## La especificación: el qué antes del cómo

La especificación ocupa una página y no menciona ni una librería. Su estructura es la que usan casi todas las plantillas de SDD: contexto, historias de usuario, requisitos funcionales numerados, requisitos no funcionales, y lo que queda fuera del alcance.

```markdown
# Especificación: listado de sismos recientes en el mundo

## Contexto
El usuario quiere ver, en una sola pantalla, qué sismos importantes
ocurrieron en el mundo en el último día, sin configurar nada y sin
crear una cuenta.

## Historias de usuario
- Como usuario, quiero abrir la app y ver los sismos recientes del
  mundo ordenados del más nuevo al más viejo, para saber qué pasó hoy.
- Como usuario, quiero distinguir de un vistazo cuáles fueron fuertes,
  sin leer cada magnitud.
- Como usuario, quiero que la app me muestre algo útil aunque no tenga
  conexión en ese momento.

## Requisitos funcionales
RF-1  La lista muestra los sismos ocurridos en las últimas 24 horas en
      cualquier parte del mundo, con magnitud mayor o igual a 2.5.
RF-2  El orden es del más reciente al más antiguo. A igual hora, el de
      mayor magnitud va primero.
RF-3  Cada fila muestra magnitud con un decimal, referencia geográfica
      del epicentro, hora relativa ("hace 12 min") y profundidad en km.
RF-4  La magnitud se clasifica en tres niveles visuales:
      menor (< 4.0), moderado (4.0 a 5.9) y fuerte (>= 6.0).
RF-5  Si la fuente publica una revisión de un sismo ya listado, la
      lista muestra la versión más reciente y nunca el evento repetido.
RF-6  Sin conexión, la app muestra la última lista descargada junto
      con la hora de la última actualización exitosa.
RF-7  Deslizar hacia abajo actualiza la lista. La lista vacía y el
      error de red tienen cada uno su propio mensaje.

## Requisitos no funcionales
RNF-1 Con conexión, la lista es visible en menos de 3 segundos desde
      que se abre la app.
RNF-2 La app no pide ninguna cuenta ni ningún permiso del sistema.

## Fuera de alcance (v1)
- Notificaciones. Requieren decidir qué es "relevante" para cada
  usuario y un servidor que observe la fuente; es otra feature.
- Ubicación del usuario y "sismos cerca de mí".
- Mapa, filtros por país o por magnitud, historial más allá de 24 h.

## Necesita aclaración
- [ACLARAR] ¿La hora se muestra en UTC o en la zona horaria del
  dispositivo?
- [ACLARAR] ¿Qué se hace con los eventos que la fuente publica sin
  magnitud todavía?
```

Los dos bloques más valiosos son los últimos. **Fuera de alcance** es la herramienta más eficaz que conozco para que un MVP siga siendo un MVP: el pedido original hablaba también de avisos cuando hubiera un sismo cerca del usuario, y dejarlo escrito como excluido evita que el agente lo implemente "ya que está". **Necesita aclaración** es lo que separa SDD de un requerimiento cualquiera: la ambigüedad queda marcada y bloquea esa parte hasta que alguien decida.

Las dos se resolvieron así, y la decisión quedó escrita en la propia especificación:

- **Zona horaria**: la del dispositivo, en formato relativo ("hace 12 min", "hace 3 h"). La hora absoluta en UTC va en la pantalla de detalle, que no existe en v1.
- **Eventos sin magnitud**: no se listan. Un evento sin magnitud es un registro que la red todavía no procesó, y mostrarlo como "M ?" confunde más de lo que informa. En cuanto la fuente lo publica con magnitud, entra por RF-5 como cualquier otro.

La diferencia entre "no lo pensamos" y "lo pensamos y lo dejamos fuera" no se nota el primer día. Se nota cuando alguien pregunta y la respuesta está en el archivo.

## El plan técnico: cómo se implementa en React Native

Recién en el plan aparece la tecnología. Este documento contesta el cómo y no reabre el qué: si al escribirlo aparece una pregunta de producto, vuelve a la especificación.

| Decisión | Elección | Motivo |
|---|---|---|
| Framework | React Native con Expo | Una base de código, sin configuración nativa para un MVP |
| Fuente de datos | Feed GeoJSON público del USGS | Sin API key, cobertura mundial, se actualiza cada minuto |
| Estado de servidor | TanStack Query | Caché, reintentos, revalidación al volver a primer plano |
| Persistencia | Caché de TanStack Query persistida en AsyncStorage | Cubre RF-6 sin una base de datos propia |
| Lista | `FlatList` con `RefreshControl` | Cubre RF-7 con componentes de la plataforma |
| Lógica del catálogo | Módulo puro sin dependencias de RN | Testeable sin simulador |

La última fila es la que más rendimiento dio. Todo lo que dicen RF-1, RF-2, RF-4 y RF-5 recibe una lista de eventos y devuelve otra, sin tocar pantalla ni red. En un módulo puro se prueba con `node` en milisegundos, sin emulador.

```text
Feed GeoJSON (USGS)
        │
        ▼
  Cliente de datos ──► normaliza a {id, mag, place, time, updated, depth}
        │
        ▼
  Módulo de catálogo (puro)
        │
        ├── descarta sin magnitud, < 2.5 o fuera de 24 h     (RF-1)
        ├── resuelve revisiones: gana el `updated` mayor     (RF-5)
        ├── ordena por hora desc, luego magnitud desc        (RF-2)
        └── asigna nivel: menor / moderado / fuerte          (RF-4)
        │
        ▼
  TanStack Query (caché persistida) ──► FlatList
                                          ├── con datos ──► filas
                                          ├── sin datos ──► estado vacío
                                          └── error + caché ──► lista vieja + hora (RF-6)
```

Un detalle del plan salió de un requisito no funcional. RNF-1 pide que la lista sea visible en menos de tres segundos, y el feed "todos los eventos del día" del USGS trae miles de eventos que RF-1 iba a descartar. Descargar eso en una red móvil ponía en riesgo RNF-1 sin ganar nada, así que el plan eligió el feed que ya viene filtrado a 2.5 y 24 horas. El filtro del módulo puro se mantiene: es la garantía de RF-1 sin importar qué feed haya detrás.

## De la especificación a los tests: la tabla de aceptación

Aquí SDD deja de ser documentación y empieza a ser ingeniería. RF-1 no se escribe solo en prosa: se escribe como una tabla de casos con el resultado esperado.

| # | Magnitud | Ocurrió hace | Esperado | Por qué |
|---|---|---|---|---|
| 1 | 2.4 | 1 h | Fuera | Bajo el mínimo de magnitud |
| 2 | 2.5 | 1 h | Dentro | Justo en el mínimo |
| 3 | 5.0 | 23 h 59 min | Dentro | Justo dentro de la ventana |
| 4 | 5.0 | 24 h 1 min | Fuera | Fuera de la ventana por 1 min |
| 5 | sin magnitud | 1 h | Fuera | Evento aún no procesado |
| 6 | 6.1 | 30 h | Fuera | Viejo, aunque sea fuerte |
| 7 | 5.0 | 2 min en el futuro | Fuera | Reloj del dispositivo atrasado o dato corrupto |

Las filas 2, 3 y 4 son las que un agente no escribe si no se las pides: los bordes exactos. La fila 6 fue la más discutida —un sismo de magnitud 6.1 suena a "eso tiene que estar"— y es donde la especificación gana: no listarlo es deliberado, está escrito, y si mañana la ventana pasa a 48 horas, se cambia la fila y el test falla solo. La fila 7 no estaba en la primera versión; más abajo cuento de dónde salió.

RF-2 y RF-5 tienen su propia tabla, porque lo que se prueba es una secuencia y no un valor:

| # | Entrada | Esperado |
|---|---|---|
| 8 | A (10:00, M 4.0), B (11:00, M 3.0) | B, A |
| 9 | A (10:00, M 4.0), C (10:00, M 5.2) | C, A |
| 10 | A (10:00, M 4.0), A' (mismo id, M 4.6, revisado después) | solo A' con M 4.6 |

La regla en código es un módulo puro. El "ahora" entra como parámetro para que el test no dependa del reloj:

```ts
// src/domain/catalog.ts

const MIN_MAG = 2.5;
const WINDOW_MS = 24 * 60 * 60 * 1000;

export type Quake = {
  id: string;
  mag: number | null;
  place: string | null;
  time: number; // epoch ms
  updated: number; // epoch ms
  depth: number; // km
};

export type Level = "minor" | "moderate" | "strong";

// RF-4: tres niveles visuales por magnitud.
export function level(mag: number): Level {
  if (mag >= 6.0) return "strong";
  if (mag >= 4.0) return "moderate";
  return "minor";
}

// RF-1, RF-2 y RF-5 en una sola función, sin tocar red ni pantalla.
export function prepareList(quakes: Quake[], now: number): Quake[] {
  // RF-5: si un id aparece varias veces, se conserva la revisión más reciente.
  const byId = new Map<string, Quake>();
  for (const e of quakes) {
    const prev = byId.get(e.id);
    if (!prev || e.updated > prev.updated) byId.set(e.id, e);
  }

  return [...byId.values()]
    // RF-1: con magnitud, >= 2.5 y ocurrido dentro de las últimas 24 horas.
    // "Ocurrido" excluye el futuro: un evento con hora posterior a `now` queda fuera.
    .filter((e) => {
      const age = now - e.time;
      return e.mag !== null && e.mag >= MIN_MAG && age >= 0 && age <= WINDOW_MS;
    })
    // RF-2: más reciente primero; a igual hora, mayor magnitud primero.
    .sort((a, b) => b.time - a.time || (b.mag ?? 0) - (a.mag ?? 0));
}
```

Y el test es la tabla, fila por fila. Si agregas una fila a la especificación, agregas una fila al arreglo:

```ts
// src/domain/catalog.test.ts
import { describe, expect, it } from "vitest";
import { prepareList, type Quake } from "./catalog";

const NOW = Date.UTC(2026, 8, 5, 12, 0, 0);
const MIN = 60 * 1000;

// Construye un evento ocurrido hace N minutos (negativo = en el futuro).
function minutesAgo(min: number, mag: number | null, id = "e"): Quake {
  const time = NOW - min * MIN;
  return { id, mag, place: null, time, updated: time, depth: 10 };
}

// Cada fila corresponde a un caso de la tabla de aceptación de RF-1.
const CASES = [
  { n: 1, mag: 2.4, min: 60, included: false },
  { n: 2, mag: 2.5, min: 60, included: true },
  { n: 3, mag: 5.0, min: 24 * 60 - 1, included: true },
  { n: 4, mag: 5.0, min: 24 * 60 + 1, included: false },
  { n: 5, mag: null, min: 60, included: false },
  { n: 6, mag: 6.1, min: 30 * 60, included: false },
  { n: 7, mag: 5.0, min: -2, included: false },
];

describe("RF-1 qué entra en la lista", () => {
  for (const c of CASES) {
    it(`caso ${c.n}: mag ${c.mag} hace ${c.min} min -> ${c.included}`, () => {
      expect(prepareList([minutesAgo(c.min, c.mag)], NOW).length === 1).toBe(c.included);
    });
  }
});

// Los casos 8 y 9 tienen la misma forma que este; se omiten por brevedad.
it("caso 10: una revisión reemplaza al evento, no lo duplica", () => {
  const a = minutesAgo(120, 4.0, "A");
  const revised = { ...a, mag: 4.6, updated: a.updated + 5 * MIN };
  expect(prepareList([a, revised], NOW)).toEqual([revised]);
});
```

Cambiar el producto pasa a ser una operación de dos ediciones simétricas: la fila en la especificación y la fila en el test. El código se adapta o falla.

## Implementar contra la especificación con un agente

Con la especificación y el plan escritos, el desglose en tareas es casi mecánico, y esa es la señal de que ambos documentos están bien. Cada tarea es verificable por sí sola:

```text
T-01  Módulo de catálogo (RF-1, RF-2, RF-4, RF-5) + tests de las tablas
T-02  Cliente del feed GeoJSON con normalización al modelo interno
T-03  Hook de datos con caché persistida (RF-6)
T-04  Pantalla de lista: filas (RF-3), niveles (RF-4), refresco (RF-7)
T-05  Estados vacío, error y sin conexión (RF-6, RF-7)
```

T-01 va primero porque no depende de React Native: se verifica sola y el resto del código se apoya en una pieza ya probada. Al agente no le pedí "una app de sismos", sino cada tarea con su referencia al requisito. La instrucción de T-02 cabía en tres líneas porque el contexto estaba en los archivos:

```text
Implementa T-02 según specs/sismos/plan.md.
Devuelve Quake[] con el tipo definido en src/domain/catalog.ts, sin filtrar nada.
El filtrado es responsabilidad del módulo de catálogo (T-01); no lo dupliques.
```

El cliente resultante solo normaliza el GeoJSON al tipo `Quake`. Donde sí hay una decisión es en el hook que lo une con la lista: ahí se cumple RF-6 sin una base de datos, porque la caché de TanStack Query se persiste y, cuando el `fetch` falla, la pantalla conserva los últimos datos y la hora en que llegaron.

```tsx
// src/hooks/useQuakes.ts
import { useQuery } from "@tanstack/react-query";
import { fetchQuakes } from "../data/usgs";
import { prepareList } from "../domain/catalog";

export function useQuakes() {
  const q = useQuery({
    queryKey: ["quakes"],
    queryFn: fetchQuakes,
    // La caché persistida (T-03) sobrevive al reinicio; esto es lo que cumple RF-6.
    staleTime: 60 * 1000,
    // RF-1: el "ahora" se toma al preparar la lista, no al descargarla.
    select: (quakes) => prepareList(quakes, Date.now()),
  });

  return {
    quakes: q.data ?? [],
    refreshing: q.isFetching,
    error: q.error,
    // RF-6: hora de la última descarga exitosa, para mostrarla junto a la lista vieja.
    lastUpdatedAt: q.dataUpdatedAt ? new Date(q.dataUpdatedAt) : null,
    refresh: q.refetch, // RF-7
  };
}
```

Los comentarios citan requisitos, no explican el código. Fue lo más útil del ejercicio: cuando el agente vuelve al archivo tres tareas después, cada bloque le dice a qué regla responde, y una revisión humana comprueba cobertura leyendo los comentarios contra la especificación.

## Cuando los datos reales contradicen la especificación

La especificación no es inmutable. En el momento en que la implementación toca datos reales, aparecen cosas que el documento no previó, y ahí se ve si el proceso aguanta. En este proyecto pasó tres veces, y la tercera fue escribiendo este artículo.

La primera, con la referencia del epicentro. RF-3 dice que cada fila muestra "referencia geográfica del epicentro", y da por hecho que siempre hay una. En el feed real, el campo viene en inglés ("58 km SSW of Puerto Ayora, Ecuador") y para eventos en medio del océano viene vacío. Resolverlo en el código habría metido dos decisiones de producto dentro del cliente de datos, invisibles para quien leyera la especificación. Volví al documento:

```markdown
RF-3b La referencia del epicentro se muestra tal como la publica la
      fuente, sin traducir. Si viene vacía, la fila muestra las
      coordenadas con dos decimales ("-0.74, -90.32").
```

Con eso escrito, el texto en inglés dejó de ser un descuido y pasó a ser una decisión con fecha, revisable el día que se quiera traducir.

La segunda fue el feed "todos los eventos" contra RNF-1, que ya conté: el requisito era correcto y la fuente elegida por defecto no lo permitía. La resolución fue cambiar el plan, no bajar el requisito ni tocar la especificación.

La tercera la encontré al revisar el módulo de catálogo para este post, y es la que más me sirve para explicar la tesis. RF-1 dice "ocurridos en las últimas 24 horas". La primera versión del filtro era `now - e.time <= WINDOW_MS`, que deja pasar cualquier evento con hora en el futuro, porque una resta negativa siempre es menor que la ventana. Un reloj de dispositivo atrasado o un dato corrupto ponían el evento primero en la lista. Es el caso 7 de la tabla: la palabra "ocurridos" ya lo excluía, pero yo no lo había escrito como fila, así que nadie lo probó. Y al mirar ese código apareció una segunda cosa: la deduplicación conserva la revisión más reciente y *después* se filtra por magnitud, así que si un evento con magnitud recibe una revisión sin magnitud, desaparece de la lista. Parece un detalle técnico. No lo es: es la pregunta de si una revisión incompleta borra información buena, y eso lo decide producto. Quedó así:

```markdown
RF-5b Una revisión sin magnitud reemplaza al evento igual que cualquier
      otra: el evento sale de la lista hasta que la fuente lo vuelva a
      publicar con magnitud. La lista nunca muestra datos que la fuente
      ya retiró.
```

Con una fila más en la tabla y un test más. La decisión pudo haber sido la contraria; lo importante es que ahora está escrita.

> El patrón que se repitió en las tres: cuando la implementación no encaja con la especificación, casi nunca es que el requisito esté de más. Es que hay un requisito que nadie escribió y el código lo estaba supliendo con un valor por defecto. Escribirlo cuesta cinco minutos y convierte una decisión enterrada en una decisión revisable.

El criterio para decidir dónde cambiar: si la contradicción es sobre **qué debe pasar**, se corrige la especificación y el código la sigue. Si es sobre **cómo se logra**, se corrige el plan y la especificación no se toca. Confundir las dos es lo que hace que las especificaciones se vuelvan obsoletas y la gente deje de leerlas.

## Qué parte de una app móvil no se deja especificar

Lo que se especifica bien es todo lo que tiene una respuesta verificable: umbrales, ventanas de tiempo, orden, formatos de datos, comportamiento sin conexión, qué se guarda y por cuánto tiempo. Toda la parte del MVP que decide *qué* mostrar cabe en la especificación, y por eso las tablas de aceptación cubren el núcleo del producto.

Lo que no se deja especificar es el criterio visual y de interacción. RF-4 dice que hay tres niveles de magnitud, pero no dice qué color tiene cada uno ni cómo se ve una fila "fuerte" al lado de una "menor", y eso se decide mirando la pantalla. En la práctica quedó una división clara: la lógica del catálogo se implementó contra la especificación y se verificó con tests; la pantalla se implementó con el agente de forma iterativa, mirando el resultado y corrigiendo.

Hay además un costo real. Mantener los artefactos sincronizados es trabajo, y una especificación desactualizada es peor que no tener ninguna: da confianza falsa. La regla que me funcionó es tratarla como código: ningún pull request que cambie una regla de producto se mergea sin el cambio correspondiente en la especificación, igual que no se mergea sin tests. Si esa disciplina no está, en dos meses los archivos de `specs/` describen un producto que ya no existe.

## Cuándo NO usar Spec-Driven Development

- **Prototipos y pruebas de concepto**: si el objetivo es descubrir si algo vale la pena, especificar antes es escribir el documento equivocado. Explora primero, especifica cuando sepas qué construir.
- **Cambios pequeños y locales**: corregir un texto o un margen no necesita tres artefactos. El costo del proceso tiene que ser menor que el costo del error.
- **Trabajo puramente visual**: si la tarea es afinar una pantalla, la iteración mirando el resultado gana.
- **Proyectos sin nadie que decida**: SDD obliga a contestar preguntas de producto. Si no hay quien las conteste, el bloque de aclaraciones se queda sin resolver y el proceso se detiene.

SDD paga cuando se juntan dos condiciones: hay reglas con casos borde reales, y hay más de una persona —o más de una sesión de agente— tocando el mismo código. Un listado de sismos, con todo lo pequeño que es, las cumple las dos. Una landing page, ninguna.

## Preguntas frecuentes

### ¿Qué diferencia hay entre Spec-Driven Development y escribir requerimientos de toda la vida?

El artefacto se parece; el ciclo de vida no. Un documento de requerimientos se escribe una vez, se aprueba y se archiva. En SDD la especificación vive en el repositorio, se versiona con el código y alimenta al agente y a los tests. Si puedes cambiar una regla sin tocar el archivo, no estás haciendo SDD.

### ¿Necesito una herramienta como Spec Kit o Kiro para aplicar SDD?

No. Tres archivos markdown en `specs/<feature>/` y la disciplina de mantenerlos cubren la mayor parte del beneficio. Las herramientas aportan que el agente no se salte el paso de las aclaraciones ni empiece a escribir código antes del plan. Es orden, no capacidad.

### ¿Tiene sentido SDD para un MVP tan pequeño?

Justamente porque es pequeño: una página de especificación cuesta poco y el beneficio se ve en la primera semana. Un MVP es donde más tentador es dejar que el agente decida todo, y donde más cuesta después desenterrar por qué la app hace lo que hace.

### ¿Qué pasa si el agente ignora la especificación?

Pasa, sobre todo si la especificación es larga o contradictoria. Dos cosas lo reducen: tareas pequeñas que citen requisitos concretos, y criterios de aceptación como tests ejecutables. Lo segundo es lo decisivo, porque convierte "el agente se desvió" en un test rojo, un problema con solución mecánica.

## Conclusión

Los agentes ya escriben el código rápido. El trabajo ahora es evitar que escriban, con toda precisión, la interpretación equivocada. Spec-Driven Development obliga a que las decisiones de producto se tomen y se escriban antes de que el código las tome por defecto. En el MVP de sismos el beneficio no fue el documento sino sus consecuencias: las ambigüedades marcadas, el alcance defendido por escrito, las tablas de aceptación convertidas en tests, y un módulo puro donde vive todo lo que puede estar mal sin que compile mal.

Si vas a empezar, hazlo en este orden: escribe la especificación sin decisiones de implementación, deja por escrito lo que queda fuera, marca lo que no está decidido y resuélvelo antes de seguir, convierte los criterios de aceptación en una tabla de casos con sus bordes, y recién entonces deja que el agente implemente tarea por tarea. Y cuando los datos reales contradigan la especificación, corrige el documento en vez de esconder la regla en el código.
