diff --git a/.claude/commands/init-proyecto.md b/.claude/commands/init-proyecto.md index 279054a..13a6c14 100644 --- a/.claude/commands/init-proyecto.md +++ b/.claude/commands/init-proyecto.md @@ -40,7 +40,8 @@ Ejecuta el checklist de "Inicialización del proyecto" de `CLAUDE.md`: de cada sesión se pare a preguntar por él. `docs/features/` se queda vacía, solo con su `README.md`. 7. `mejoras/backlog.md` — borra el ejemplo comentado. -8. `.template/` — bórrala (`rm -rf .template`). +8. `.template/` — bórrala (`rm -rf .template`). Quita después las referencias a + `.template/assets/` que queden en otros archivos: hay una en `docs/features/README.md`. 9. `changelog/` — crea la primera entrada real del proyecto (tipo: Configuración) con `/changelog` y limpia de `changelog/README.md` la referencia a la plantilla. 10. Verifica que no queden restos: diff --git a/.template/README.md b/.template/README.md index 4b7af1e..585b448 100644 --- a/.template/README.md +++ b/.template/README.md @@ -6,6 +6,7 @@ Su contenido: - `changelog/` — historial de cambios de la propia plantilla (cómo ha ido evolucionando este andamiaje). No es el changelog de tu proyecto: ese vive en `changelog/`, en la raíz. +- `assets/` — los GIF del `README.md` de la plantilla y los scripts que los generan. ## Si estás usando la plantilla para un proyecto diff --git a/.template/assets/README.md b/.template/assets/README.md new file mode 100644 index 0000000..78c7277 --- /dev/null +++ b/.template/assets/README.md @@ -0,0 +1,31 @@ +# Imágenes del README + +Los GIF que ilustran el `README.md` de la plantilla, y los scripts que los generan. + +Viven aquí, dentro de `.template/`, por el mismo motivo que el changelog de la plantilla: son +material del andamiaje, no del producto. Al inicializar un proyecto esta carpeta se borra entera y +el README se reescribe, así que las imágenes y sus referencias desaparecen juntas. + +| Archivo | Qué muestra | +|---------|-------------| +| `comparativa.gif` | Sin plantilla frente a con plantilla | +| `flujo.gif` | El repositorio montándose fase a fase | +| `cobertura.gif` | El script bloqueando un test prometido que no existe | +| `excepcion.gif` | El script rechazando una excepción sin justificar | + +## Regenerarlos + +Hace falta Python con Pillow, `rsvg-convert` y `ffmpeg`. Los dos diagramas se dibujan en SVG y se +rasterizan; los de terminal se componen frame a frame a partir de **salida real** del script. + +```bash +python3 gen-comparativa.py salida.svg # diagrama de carriles +python3 gen-flujo.py salida.svg [fase] # repositorio por fases (0-6) +``` + +`gen-terminal.py` es una librería: se importa y se le pasa un guion de comandos y su salida. + +**Regla al tocarlos:** el texto que aparece en los GIF de terminal se copia de una ejecución de +verdad. Si cambias el script y cambian sus mensajes, hay que volver a ejecutarlo y regenerar el +GIF, no editar el texto a mano. Un GIF que enseña una salida que el programa ya no produce miente +igual que una casilla marcada sin comprobar. diff --git a/.template/assets/cobertura.gif b/.template/assets/cobertura.gif new file mode 100644 index 0000000..cd48457 Binary files /dev/null and b/.template/assets/cobertura.gif differ diff --git a/.template/assets/comparativa.gif b/.template/assets/comparativa.gif new file mode 100644 index 0000000..5207d11 Binary files /dev/null and b/.template/assets/comparativa.gif differ diff --git a/.template/assets/excepcion.gif b/.template/assets/excepcion.gif new file mode 100644 index 0000000..d5db5ef Binary files /dev/null and b/.template/assets/excepcion.gif differ diff --git a/.template/assets/flujo.gif b/.template/assets/flujo.gif new file mode 100644 index 0000000..d5877c5 Binary files /dev/null and b/.template/assets/flujo.gif differ diff --git a/.template/assets/gen-comparativa.py b/.template/assets/gen-comparativa.py new file mode 100644 index 0000000..15757f9 --- /dev/null +++ b/.template/assets/gen-comparativa.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""Genera el diagrama 'sin plantilla / con plantilla' en SVG.""" + +W, H = 1200, 560 +BG = "#faf8f5" +CARRIL_MAL = "#f3efe9"; BORDE_MAL = "#e7e0d5" +CARRIL_BIEN = "#eef1f8"; BORDE_BIEN = "#dde3f2" +TARJETA = "#ffffff"; BORDE_T = "#e8e3da" +TINTA = "#2c2c33"; TENUE = "#938d80" +ROJO = "#c8635c"; AZUL = "#4a63c8"; VERDE = "#4f9563" +FAM = "Helvetica Neue, Helvetica, Arial, sans-serif" + +MAL = [ + ("chat", "Sesión nueva", "CONTEXTO CERO", None), + ("doc", "Le explicas todo", "OTRA VEZ", None), + ("code", "Escribe a ciegas", "SIN PREGUNTAR", None), + ("x", "No era eso", "REHACER", ROJO), + ("quest", "Decisiones perdidas", "NADIE SE ACUERDA", None), + ("box", "Entregado", "SIN SABER SI CUMPLE", None), +] +BIEN = [ + ("doc", "Sesión nueva", "LEE docs/", None), + ("ficha", "Ficha de feature", "QUÉ Y CÓMO SE VALIDA", None), + ("code", "Construye", "SOBRE LO ACORDADO", None), + ("test", "Tests del código", "TRAS IMPLEMENTAR", None), + ("pr", "PR con evidencia", "SALIDA REAL PEGADA", None), + ("check", "Mergeado", "COBERTURA VERIFICADA", VERDE), +] + + +def icono(tipo, x, y, c): + """Icono de línea 22x22 dibujado desde (x,y). Trazo fino, sin relleno.""" + s = f'' + if tipo == "chat": + s += f'' + elif tipo == "doc": + s += f'' + elif tipo == "code": + s += f'' + elif tipo == "x": + s += f'' + elif tipo == "quest": + s += f'' + elif tipo == "box": + s += f'' + elif tipo == "ficha": + s += f'' + elif tipo == "test": + s += f'' + elif tipo == "pr": + s += f'' + elif tipo == "check": + s += f'' + return s + "" + + +def tarjeta(x, y, w, h, ic, titulo, sub, acento): + col = acento or TINTA + fondo_ic = "#fdf1f0" if acento == ROJO else ("#eef7f1" if acento == VERDE else "#f6f4f0") + borde = "#f0d9d7" if acento == ROJO else ("#d8ebdf" if acento == VERDE else BORDE_T) + s = f'' + s += f'' + s += icono(ic, x + 22, y + 22, col) + s += (f'{titulo}') + s += (f'{sub}') + return s + "" + + +def carril(y0, etiqueta, pasos, fondo, borde, color_et, texto_et, pie, color_pie, revelados=None): + n = len(pasos) + x0, ancho_c = 50, W - 100 + s = f'' + s += f'' + s += (f'{etiqueta}') + + tw, hueco = 168, 18 + total = n * tw + (n - 1) * hueco + sx = x0 + (ancho_c - total) / 2 + ty = y0 + 46 + for i, (ic, tit, sub, ac) in enumerate(pasos): + tx = sx + i * (tw + hueco) + vis = revelados is None or i < revelados + op = 1 if vis else 0.13 + s += f'' + tarjeta(tx, ty, tw, 108, ic, tit, sub, ac) + "" + if i < n - 1: + cx1, cx2 = tx + tw, tx + tw + hueco + conec = revelados is None or i < revelados - 1 + s += (f'') + s += (f'') + + py = ty + 132 + completo = revelados is None or revelados >= n + s += f'' + if pie[0] == "loop": + s += (f'') + s += (f'') + else: + s += f'' + s += (f'') + s += (f'{pie[1]}') + return s + "" + + +def svg(rev_mal=None, rev_bien=None): + s = (f'' + f'') + s += carril(56, "SIN PLANTILLA", MAL, CARRIL_MAL, BORDE_MAL, "#6f6759", "#ffffff", + ("loop", "SE REPITE EN CADA SESIÓN, CON CADA AGENTE"), ROJO, rev_mal) + s += carril(330, "CON PLANTILLA", BIEN, CARRIL_BIEN, BORDE_BIEN, AZUL, "#ffffff", + ("linea", "EL CONTEXTO VIVE EN EL REPO, NO EN LA CONVERSACIÓN"), AZUL, rev_bien) + return s + "" + + +if __name__ == "__main__": + import sys + open(sys.argv[1], "w", encoding="utf-8").write(svg()) diff --git a/.template/assets/gen-flujo.py b/.template/assets/gen-flujo.py new file mode 100644 index 0000000..a1e7581 --- /dev/null +++ b/.template/assets/gen-flujo.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""De la conversación al producto: el repositorio montándose solo.""" + +W, H = 1200, 620 +BG = "#faf8f5"; TARJETA = "#ffffff"; BORDE = "#e8e3da" +TINTA = "#2c2c33"; TENUE = "#938d80"; SUAVE = "#b8b2a5" +AZUL = "#4a63c8"; VERDE = "#4f9563"; AMBAR = "#c8933f"; ROJO = "#c8635c" +FAM = "Helvetica Neue, Helvetica, Arial, sans-serif" +MONO = "Menlo, monospace" + +FASES = [ + ("Conversación", "EL AGENTE PREGUNTA ANTES DE ESCRIBIR"), + ("Inicialización", "DEJA DE SER UNA PLANTILLA"), + ("Acuerdo", "QUÉ SE CONSTRUYE Y CÓMO SE VALIDA"), + ("Construcción", "SOBRE LO ACORDADO"), + ("Validación", "TESTS SOBRE EL CÓDIGO REAL"), + ("Cierre", "PR CON EVIDENCIA, CI EN VERDE"), +] + +# (sangría, nombre, tipo, nace, muere, nota_por_fase) +FILAS = [ + (0, ".template/", "dir", 0, 2, {}), + (0, "CLAUDE.md", "file", 0, 9, {2: ("rellenado", AZUL)}), + (0, "README.md", "file", 0, 9, {2: ("reescrito", AZUL)}), + (0, "changelog/", "dir", 6, 9, {}), + (1, "2026-08-18_exportar.md", "file", 6, 9, {}), + (0, "docs/", "dir", 0, 9, {}), + (1, "prd.md", "file", 0, 9, {0: ("vacío", SUAVE), 1: ("M-01 · M-02 · M-03", AZUL)}), + (1, "architecture.md", "file", 0, 9, {0: ("vacío", SUAVE), 1: None}), + (1, "design-system.md", "file", 0, 9, {0: ("vacío", SUAVE), 1: None}), + (1, "data-model.md", "file", 0, 9, {0: ("vacío", SUAVE), 1: None}), + (1, "features/", "dir", 3, 9, {}), + (2, "exportar-coleccion.md", "ficha",3, 9, {}), + (0, "src/app/exportar/", "dir", 4, 9, {}), + (1, "route.ts", "file", 4, 9, {}), + (0, "tests/", "dir", 5, 9, {}), + (1, "exportar.spec.ts", "file", 5, 9, {}), +] +PILL = {3: ("Acordada", TENUE, "#f2efe9"), 4: ("En construcción", AMBAR, "#fbf4e6"), + 5: ("Verificada", VERDE, "#eef7f1"), 6: ("Verificada", VERDE, "#eef7f1")} + + +def glifo(tipo, x, y, c): + g = f'' + if tipo == "dir": + g += f'' + elif tipo == "ficha": + g += f'' + else: + g += f'' + return g + "" + + +def pastilla(x, y, texto, color, fondo, tam=9): + an = len(texto) * tam * 0.6 + 18 + return (f'' + f'{texto}') + + +def svg(fase=6): + s = (f'' + f'') + + # ── Raíl de fases ── + rx, ry, paso = 92, 108, 74 + s += f'' + if fase >= 1: + s += (f'') + for i, (tit, sub) in enumerate(FASES): + y = ry + i * paso + act, hecha = (fase == i + 1), (fase > i + 1) + col = AZUL if (act or hecha) else SUAVE + s += f'' + if act or hecha: + s += f'' + s += (f'{tit}') + s += (f'{sub}') + + # ── El repositorio ── + cx, cy, cw = 450, 62, 660 + visibles = [f for f in FILAS if f[3] <= fase and (f[4] > fase or f[4] == fase)] + ch = 44 + len(visibles) * 27 + 20 + s += f'' + s += f'' + for i, c in enumerate(["#e8a49f", "#e5c98a", "#a8cdb0"]): + s += f'' + s += (f'mi-proyecto') + + y = cy + 62 + for sang, nom, tipo, nace, muere, notas in visibles: + muriendo = (muere == fase) + nuevo = (nace == fase and fase > 0) + col = ROJO if muriendo else (TINTA if tipo != "dir" else TENUE) + x = cx + 24 + sang * 20 + if nuevo: + s += f'' + s += f'' + s += glifo(tipo, x, y - 9, ROJO if muriendo else (TENUE if tipo == "dir" else SUAVE)) + s += (f'{nom}') + if muriendo: + an = len(nom) * 7.5 + s += f'' + s += pastilla(x + 26 + an, y - 4, "se borra", ROJO, "#fdf1f0", 8.5) + elif tipo == "ficha" and fase in PILL: + t, c1, c2 = PILL[fase] + s += pastilla(x + 26 + len(nom) * 7.5, y - 4, t, c1, c2) + else: + claves = [k for k in notas if k <= fase] + nota = notas[max(claves)] if claves else None + if nota: + s += (f'{nota[0]}') + y += 27 + + if fase >= 6: + s += pastilla(cx + 24, cy + ch + 26, "CI · cobertura verificada", VERDE, "#eef7f1", 10.5) + return s + "" + + +if __name__ == "__main__": + import sys + open(sys.argv[1], "w", encoding="utf-8").write(svg(int(sys.argv[2]) if len(sys.argv) > 2 else 6)) diff --git a/.template/assets/gen-terminal.py b/.template/assets/gen-terminal.py new file mode 100644 index 0000000..3324f79 --- /dev/null +++ b/.template/assets/gen-terminal.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""Renderiza una sesión de terminal a GIF. Frames con Pillow, ensamblado con ffmpeg.""" +import subprocess, sys, shutil +from pathlib import Path +from PIL import Image, ImageDraw, ImageFont + +FUENTE = "/System/Library/Fonts/SFNSMono.ttf" +TAM = 15 +INTERLINEA = 24 +PAD_X, PAD_Y = 26, 20 +CHROME = 38 + +C = { + "fondo": "#151723", + "chrome": "#1d2030", + "borde": "#2a2e42", + "texto": "#c5cee0", + "tenue": "#6b7594", + "prompt": "#4fd6be", + "cmd": "#eef1f8", + "fallo": "#ff757f", + "aviso": "#ffc777", + "ok": "#c3e88d", + "ruta": "#82aaff", +} + +fuente = ImageFont.truetype(FUENTE, TAM) +_probe = Image.new("RGB", (10, 10)); _d = ImageDraw.Draw(_probe) +CHAR_W = _d.textlength("M", font=fuente) + + +def estilar(linea): + """Una línea de salida real -> tramos coloreados. No altera el texto.""" + t = linea + if t.startswith("$ "): + resto = t[2:] + if resto.startswith("#"): + return [("$ ", C["prompt"]), (resto, C["tenue"])] + return [("$ ", C["prompt"]), (resto, C["cmd"])] + s = t.strip() + if s.startswith("FALLO"): + i = t.index("FALLO") + return [(t[:i], C["texto"]), ("FALLO", C["fallo"]), (t[i + 5:], C["texto"])] + if s.startswith("ATENCIÓN"): + i = t.index("ATENCIÓN") + return [(t[:i], C["texto"]), ("ATENCIÓN", C["aviso"]), (t[i + 8:], C["texto"])] + if s.startswith("docs/") or s.startswith("scripts/"): + return [(t, C["ruta"])] + if s.startswith("Todo en orden") or s.startswith("Sin fallos"): + return [(t, C["ok"])] + if s.endswith("fallo(s).") or " fallo(s)" in s: + return [(t, C["fallo"])] + if s.startswith("Verificación de cobertura"): + return [(t, C["tenue"])] + return [(t, C["texto"])] + + +def pintar(lineas, cursor_en=None, ancho=None, alto=None): + img = Image.new("RGB", (ancho, alto), C["fondo"]) + d = ImageDraw.Draw(img) + d.rectangle([0, 0, ancho, CHROME], fill=C["chrome"]) + d.line([(0, CHROME), (ancho, CHROME)], fill=C["borde"]) + for i, col in enumerate(["#ff5f57", "#febc2e", "#28c840"]): + cx = 22 + i * 20 + d.ellipse([cx - 6, CHROME // 2 - 6, cx + 6, CHROME // 2 + 6], fill=col) + + y = CHROME + PAD_Y + for li, linea in enumerate(lineas): + x = PAD_X + for texto, color in estilar(linea): + d.text((x, y), texto, font=fuente, fill=color) + x += d.textlength(texto, font=fuente) + if cursor_en == li: + d.rectangle([x + 1, y + 2, x + CHAR_W, y + TAM + 4], fill=C["texto"]) + y += INTERLINEA + return img + + +def render(guion, salida, fps=16): + """guion: lista de ('cmd', txt) | ('out', [lineas]) | ('esperar', frames)""" + todas = [] + for tipo, val in guion: + if tipo == "cmd": + todas.append("$ " + val) + elif tipo == "out": + todas.extend(val) + max_chars = max((len(l) for l in todas), default=60) + ancho = int(PAD_X * 2 + max_chars * CHAR_W) + 8 + alto = CHROME + PAD_Y * 2 + len(todas) * INTERLINEA + + frames, lineas = [], [] + for tipo, val in guion: + if tipo == "cmd": + lineas.append("$ ") + for i in range(0, len(val) + 1, 2): + lineas[-1] = "$ " + val[:i] + frames.append(pintar(lineas, len(lineas) - 1, ancho, alto)) + lineas[-1] = "$ " + val + for _ in range(5): + frames.append(pintar(lineas, len(lineas) - 1, ancho, alto)) + elif tipo == "out": + for linea in val: + lineas.append(linea) + frames.append(pintar(lineas, None, ancho, alto)) + elif tipo == "esperar": + for _ in range(val): + frames.append(pintar(lineas, None, ancho, alto)) + return frames, ancho, alto, fps + + +def a_gif(frames, destino, fps): + tmp = Path(destino).parent / "_frames" + if tmp.exists(): + shutil.rmtree(tmp) + tmp.mkdir(parents=True) + for i, f in enumerate(frames): + f.save(tmp / f"{i:04d}.png") + paleta = tmp / "pal.png" + subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-framerate", str(fps), + "-i", str(tmp / "%04d.png"), + "-vf", "palettegen=max_colors=128:stats_mode=diff", str(paleta)], check=True) + subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-framerate", str(fps), + "-i", str(tmp / "%04d.png"), "-i", str(paleta), + "-lavfi", "paletteuse=dither=none:diff_mode=rectangle", + "-loop", "0", str(destino)], check=True) + subprocess.run(["magick", str(destino), "-layers", "Optimize", str(destino)], check=True) + shutil.rmtree(tmp) diff --git a/.template/changelog/2026-08-18_16-23_gifs-del-readme.md b/.template/changelog/2026-08-18_16-23_gifs-del-readme.md new file mode 100644 index 0000000..b6e5d4f --- /dev/null +++ b/.template/changelog/2026-08-18_16-23_gifs-del-readme.md @@ -0,0 +1,60 @@ +# Cuatro GIF para el README: el argumento, el artefacto y la prueba + +**Fecha:** 2026-08-18 16:23 +**Tipo:** Documentación +**Requisitos:** Ninguno (cambio sobre el andamiaje de la plantilla) + +## Qué se hizo + +El README explicaba el protocolo entero en prosa. Ahora lo enseña, con cuatro GIF que hablan tres +idiomas distintos a propósito: + +- **`comparativa.gif`** (cabecera) — dos carriles de tarjetas: sin plantilla, cada sesión empieza + de cero y la flecha vuelve al principio; con plantilla, la línea llega recta a un check verde. + Es un **argumento**. +- **`flujo.gif`** ("¿Cómo funciona el protocolo?") — un árbol de archivos creciendo fase a fase: + los `docs/` marcados `vacío` que se llenan, `.template/` tachándose, y la pastilla de la ficha + pasando de Acordada a En construcción y a Verificada. Es un **artefacto**: no explica el + producto, es el producto. +- **`cobertura.gif`** (sección nueva "La regla que lo sostiene") — el script pasando con la ficha + en construcción, fallando al marcarla Verificada sin el test, y volviendo a pasar cuando existe. + Es una **prueba**. +- **`excepcion.gif`** (`docs/features/README.md`) — columna vacía y "no aplica" rechazados; con la + razón concreta, verde. + +El diagrama Mermaid del ciclo se elimina: contaba lo mismo que `flujo.gif` con menos detalle, y +tener los dos era repetirse. + +Todo vive en `.template/assets/`, junto con los tres generadores. Al inicializar un proyecto la +carpeta se borra y el README se reescribe, así que imágenes y referencias desaparecen juntas. + +## Qué se modificó + +- `.template/assets/` — nueva: cuatro GIF, tres generadores (`gen-comparativa.py`, `gen-flujo.py`, + `gen-terminal.py`) y un `README.md` con cómo regenerarlos +- `README.md` — GIF de cabecera bajo la llamada a la acción; `flujo.gif` sustituye al diagrama + Mermaid; nueva sección "La regla que lo sostiene" con `cobertura.gif` +- `docs/features/README.md` — `excepcion.gif` tras la regla de la tercera columna +- `.template/README.md` — documenta la carpeta `assets/` +- `CLAUDE.md` y `.claude/commands/init-proyecto.md` — al borrar `.template/` hay que quitar las + referencias que apuntan a sus imágenes; se nombra la de `docs/features/README.md` + +## Por qué + +Un repositorio plantilla tiene la particularidad de que su página de inicio es el producto: lo +primero que hace alguien es mirar el README y decidir en quince segundos si esto le sirve. Diez +puntos numerados no ganan esa decisión. + +Los tres registros son deliberados y no intercambiables. El diagrama de carriles es una afirmación +que nadie puede verificar —como toda comparativa de antes y después, es una caricatura amable del +problema—. El árbol que crece es concreto pero sigue siendo un dibujo. Los GIF de terminal son lo +único del README que un escéptico puede reproducir en su máquina, y por eso se quedan aunque sean +los más feos: son la parte que demuestra en vez de prometer. + +## Verificado + +- Los GIF de terminal se componen de la salida literal de `verificar-cobertura.mjs` ejecutado + sobre un proyecto de prueba (un catálogo de vinilos, el ejemplo del propio `CLAUDE.md`). Ni una + línea de ese texto está escrita a mano. +- Peso total de los cuatro: 675 KB. El mayor, `flujo.gif`, 244 KB. +- `node scripts/verificar-cobertura.mjs` sigue saliendo limpio. diff --git a/CLAUDE.md b/CLAUDE.md index b57d464..566b105 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -108,7 +108,8 @@ Puedes lanzar el proceso completo con `/init-proyecto`. `docs/features/` se queda como está: empieza sin fichas, solo con su `README.md`. 8. **`mejoras/backlog.md`** — borra el ejemplo comentado y déjalo listo para entradas reales. 9. **`.template/`** — bórrala entera (`rm -rf .template`). Es el historial de la plantilla, no - del proyecto. + del proyecto. Con ella se van también las imágenes del README, así que quita las referencias + que queden apuntando a `.template/assets/` (hay una en `docs/features/README.md`). 10. **Verificación final** — busca referencias sobrantes: `grep -ril "plantilla\|template" . --exclude-dir=.git --exclude-dir=node_modules`. Revisa cada resultado y corrígelo si habla de la plantilla en lugar del proyecto. diff --git a/README.md b/README.md index b0cc62d..6d504c6 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ Usar esta plantilla →

+

+ Comparativa: sin plantilla, cada sesión empieza de cero y el trabajo se rehace; con plantilla, el agente lee la documentación, acuerda qué construir, lo valida y lo cierra con evidencia. +

+ --- ## ¿Qué es esto? @@ -52,16 +56,9 @@ Es agnóstica al stack. El protocolo funciona igual con Next.js, Astro, FastAPI ## ¿Cómo funciona el protocolo? -```mermaid -flowchart LR - A["Requisito en docs/prd.md
con criterio de aceptación"] - B["Ficha de feature
Acordada"] - C["En construcción"] - D["Tests escritos
sobre el código real"] - E["Verificada"] - F["PR con evidencia
CI verifica la cobertura"] - A --> B --> C --> D --> E --> F -``` +

+ El repositorio montándose fase a fase: los documentos se llenan durante la conversación, la carpeta de plantilla se borra al inicializar, aparece la ficha de feature y su estado avanza de Acordada a En construcción y a Verificada según llegan el código y los tests. +

1. **Cualquier sesión empieza leyendo `docs/`.** Si están vacíos o incompletos, el agente pregunta antes de actuar. Y no pide los ocho documentos: pide los que correspondan al tamaño del proyecto. 2. **Cada funcionalidad del PRD lleva ID y criterio de aceptación comprobable.** "Dado…, cuando…, entonces…", con un resultado que se pueda mirar. Ese criterio es el que después se convierte en test. @@ -74,6 +71,20 @@ flowchart LR 9. **Antes de mergear a producción**, se ejecuta `/security-review` para detectar vulnerabilidades, credenciales filtradas y problemas comunes. 10. **Las ideas que no entran ahora se anotan en `mejoras/`** sin interrumpir el flujo actual. +### La regla que lo sostiene + +Ningún requisito se queda sin su tercera columna: **o lleva la ruta del test que lo valida, o lleva +la razón concreta por la que no se puede validar así.** Y no es una norma de buena voluntad — hay +un script que la comprueba, y corre en CI con cada pull request. + +La gracia está en cuándo aprieta y cuándo no. Mientras la ficha está en construcción no exige nada: +los tests se escriben después de implementar. En el momento en que la das por **Verificada**, el +test prometido tiene que existir. + +

+ Ejecución real del script: con la ficha en construcción pasa sin quejarse aunque el test no exista; al marcarla como Verificada falla porque el test prometido no está; una vez escrito, vuelve a pasar. +

+ --- ## Comandos diff --git a/docs/features/README.md b/docs/features/README.md index a784ff8..5686bd3 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -99,6 +99,10 @@ construcción* no exige que los archivos existan: los tests van después de impl Una fila puede declarar varios tests separándolos por comas. +

+ Ejecución real del script: la columna vacía falla; una excepción que dice solo 'no aplica' también falla; con la razón concreta y cómo se comprueba, pasa. +

+ --- ## Estado de la ficha