Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/commands/init-proyecto.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions .template/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
31 changes: 31 additions & 0 deletions .template/assets/README.md
Original file line number Diff line number Diff line change
@@ -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.
Binary file added .template/assets/cobertura.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .template/assets/comparativa.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .template/assets/excepcion.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .template/assets/flujo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
124 changes: 124 additions & 0 deletions .template/assets/gen-comparativa.py
Original file line number Diff line number Diff line change
@@ -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'<g stroke="{c}" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round">'
if tipo == "chat":
s += f'<path d="M{x+2} {y+4} h18 v12 h-9 l-5 4 v-4 h-4 z"/>'
elif tipo == "doc":
s += f'<path d="M{x+4} {y+2} h9 l5 5 v13 h-14 z"/><path d="M{x+7} {y+11} h7 M{x+7} {y+15} h7"/>'
elif tipo == "code":
s += f'<path d="M{x+8} {y+6} l-5 5 l5 5 M{x+14} {y+6} l5 5 l-5 5"/>'
elif tipo == "x":
s += f'<circle cx="{x+11}" cy="{y+11}" r="8"/><path d="M{x+8} {y+8} l6 6 M{x+14} {y+8} l-6 6"/>'
elif tipo == "quest":
s += f'<circle cx="{x+11}" cy="{y+11}" r="8"/><path d="M{x+8.5} {y+8.5} a2.5 2.5 0 1 1 2.5 3 v1.5"/><path d="M{x+11} {y+15.5} v0.5"/>'
elif tipo == "box":
s += f'<path d="M{x+3} {y+6} h16 v12 h-16 z"/><path d="M{x+3} {y+10} h16"/>'
elif tipo == "ficha":
s += f'<path d="M{x+4} {y+3} h14 v16 h-14 z"/><path d="M{x+7} {y+8} h8 M{x+7} {y+12} h8 M{x+7} {y+15.5} h4"/>'
elif tipo == "test":
s += f'<path d="M{x+4} {y+11} l4 4 l9 -9"/><path d="M{x+3} {y+4} h16" opacity="0.35"/>'
elif tipo == "pr":
s += f'<circle cx="{x+6}" cy="{y+6}" r="2.6"/><circle cx="{x+6}" cy="{y+17}" r="2.6"/><circle cx="{x+16}" cy="{y+6}" r="2.6"/><path d="M{x+6} {y+8.6} v5.8 M{x+16} {y+8.6} v3 a3 3 0 0 1 -3 3 h-4"/>'
elif tipo == "check":
s += f'<circle cx="{x+11}" cy="{y+11}" r="8"/><path d="M{x+7.5} {y+11} l2.5 2.5 l5 -5.5"/>'
return s + "</g>"


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'<g><rect x="{x}" y="{y}" width="{w}" height="{h}" rx="13" fill="{TARJETA}" stroke="{borde}"/>'
s += f'<rect x="{x+16}" y="{y+16}" width="34" height="34" rx="9" fill="{fondo_ic}"/>'
s += icono(ic, x + 22, y + 22, col)
s += (f'<text x="{x+16}" y="{y+72}" font-family="{FAM}" font-size="12.5" font-weight="600" '
f'fill="{col}">{titulo}</text>')
s += (f'<text x="{x+16}" y="{y+90}" font-family="{FAM}" font-size="8.5" font-weight="600" '
f'letter-spacing="0.9" fill="{TENUE}">{sub}</text>')
return s + "</g>"


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'<rect x="{x0}" y="{y0}" width="{ancho_c}" height="212" rx="20" fill="{fondo}" stroke="{borde}"/>'
s += f'<rect x="{x0+22}" y="{y0-13}" width="{len(etiqueta)*7.4+30:.0f}" height="26" rx="13" fill="{color_et}"/>'
s += (f'<text x="{x0+37}" y="{y0+4}" font-family="{FAM}" font-size="10" font-weight="700" '
f'letter-spacing="1.1" fill="{texto_et}">{etiqueta}</text>')

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'<g opacity="{op}">' + tarjeta(tx, ty, tw, 108, ic, tit, sub, ac) + "</g>"
if i < n - 1:
cx1, cx2 = tx + tw, tx + tw + hueco
conec = revelados is None or i < revelados - 1
s += (f'<line x1="{cx1+2}" y1="{ty+54}" x2="{cx2-2}" y2="{ty+54}" stroke="{borde}" '
f'stroke-width="1.4" opacity="{1 if conec else 0.13}"/>')
s += (f'<circle cx="{(cx1+cx2)/2}" cy="{ty+54}" r="2.4" fill="{color_et}" '
f'opacity="{0.85 if conec else 0.13}"/>')

py = ty + 132
completo = revelados is None or revelados >= n
s += f'<g opacity="{1 if completo else 0.13}">'
if pie[0] == "loop":
s += (f'<line x1="{sx+52}" y1="{py+2}" x2="{sx+total-30}" y2="{py+2}" stroke="{color_pie}" '
f'stroke-width="1.4" stroke-dasharray="5 4"/>')
s += (f'<path d="M{sx+44} {py+2} l8 -4.5 l0 9 z" fill="{color_pie}"/>')
else:
s += f'<line x1="{sx+40}" y1="{py+2}" x2="{sx+total-40}" y2="{py+2}" stroke="{color_pie}" stroke-width="1.4"/>'
s += (f'<rect x="{W/2 - len(pie[1])*3.3 - 14:.0f}" y="{py-8}" width="{len(pie[1])*6.6+28:.0f}" height="20" '
f'rx="10" fill="{BG}"/>')
s += (f'<text x="{W/2}" y="{py+5}" text-anchor="middle" font-family="{FAM}" font-size="9" '
f'font-weight="700" letter-spacing="1.1" fill="{color_pie}">{pie[1]}</text>')
return s + "</g>"


def svg(rev_mal=None, rev_bien=None):
s = (f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" viewBox="0 0 {W} {H}">'
f'<rect width="{W}" height="{H}" fill="{BG}"/>')
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 + "</svg>"


if __name__ == "__main__":
import sys
open(sys.argv[1], "w", encoding="utf-8").write(svg())
130 changes: 130 additions & 0 deletions .template/assets/gen-flujo.py
Original file line number Diff line number Diff line change
@@ -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'<g stroke="{c}" stroke-width="1.3" fill="none" stroke-linejoin="round">'
if tipo == "dir":
g += f'<path d="M{x} {y+2} h5 l1.6 2 h6.4 v8 h-13 z"/>'
elif tipo == "ficha":
g += f'<path d="M{x+1} {y} h9 l2.5 2.5 v9.5 h-11.5 z"/><path d="M{x+3.5} {y+5} h6 M{x+3.5} {y+8} h6"/>'
else:
g += f'<path d="M{x+1} {y} h7 l3 3 v9 h-10 z"/>'
return g + "</g>"


def pastilla(x, y, texto, color, fondo, tam=9):
an = len(texto) * tam * 0.6 + 18
return (f'<g><rect x="{x}" y="{y-10}" width="{an:.0f}" height="17" rx="8.5" fill="{fondo}" '
f'stroke="{color}" stroke-opacity="0.28"/>'
f'<text x="{x+an/2:.0f}" y="{y+2}" text-anchor="middle" font-family="{FAM}" '
f'font-size="{tam}" font-weight="600" letter-spacing="0.4" fill="{color}">{texto}</text></g>')


def svg(fase=6):
s = (f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" viewBox="0 0 {W} {H}">'
f'<rect width="{W}" height="{H}" fill="{BG}"/>')

# ── Raíl de fases ──
rx, ry, paso = 92, 108, 74
s += f'<line x1="{rx}" y1="{ry}" x2="{rx}" y2="{ry + paso*5}" stroke="{BORDE}" stroke-width="2"/>'
if fase >= 1:
s += (f'<line x1="{rx}" y1="{ry}" x2="{rx}" y2="{ry + paso*min(fase-1,5)}" '
f'stroke="{AZUL}" stroke-width="2"/>')
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'<circle cx="{rx}" cy="{y}" r="{8 if act else 5.5}" fill="{TARJETA}" stroke="{col}" stroke-width="2"/>'
if act or hecha:
s += f'<circle cx="{rx}" cy="{y}" r="{3.5 if act else 2.5}" fill="{col}"/>'
s += (f'<text x="{rx+26}" y="{y+1}" font-family="{FAM}" font-size="14.5" '
f'font-weight="{700 if act else 500}" fill="{TINTA if (act or hecha) else SUAVE}">{tit}</text>')
s += (f'<text x="{rx+26}" y="{y+17}" font-family="{FAM}" font-size="8.5" font-weight="600" '
f'letter-spacing="0.9" fill="{TENUE if act else SUAVE}" '
f'opacity="{1 if act else 0.55}">{sub}</text>')

# ── 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'<rect x="{cx}" y="{cy}" width="{cw}" height="{ch}" rx="14" fill="{TARJETA}" stroke="{BORDE}"/>'
s += f'<line x1="{cx}" y1="{cy+36}" x2="{cx+cw}" y2="{cy+36}" stroke="{BORDE}"/>'
for i, c in enumerate(["#e8a49f", "#e5c98a", "#a8cdb0"]):
s += f'<circle cx="{cx+22+i*17}" cy="{cy+18}" r="4.5" fill="{c}"/>'
s += (f'<text x="{cx+cw-20}" y="{cy+22}" text-anchor="end" font-family="{MONO}" font-size="10" '
f'fill="{SUAVE}">mi-proyecto</text>')

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'<rect x="{cx+12}" y="{y-14}" width="{cw-24}" height="24" rx="6" fill="#eef1f8"/>'
s += f'<rect x="{cx+12}" y="{y-14}" width="2.5" height="24" rx="1.2" fill="{AZUL}"/>'
s += glifo(tipo, x, y - 9, ROJO if muriendo else (TENUE if tipo == "dir" else SUAVE))
s += (f'<text x="{x+20}" y="{y}" font-family="{MONO}" font-size="12.5" fill="{col}" '
f'{"text-decoration=" + chr(34) + "line-through" + chr(34) if muriendo else ""}>{nom}</text>')
if muriendo:
an = len(nom) * 7.5
s += f'<line x1="{x+20}" y1="{y-4}" x2="{x+20+an}" y2="{y-4}" stroke="{ROJO}" stroke-width="1.2"/>'
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'<text x="{x+26+len(nom)*7.5:.0f}" y="{y}" font-family="{FAM}" font-size="10" '
f'font-style="italic" fill="{nota[1]}">{nota[0]}</text>')
y += 27

if fase >= 6:
s += pastilla(cx + 24, cy + ch + 26, "CI · cobertura verificada", VERDE, "#eef7f1", 10.5)
return s + "</svg>"


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))
Loading
Loading