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
280 changes: 248 additions & 32 deletions .claude/skills/arquitectura-drawio/SKILL.md

Large diffs are not rendered by default.

113 changes: 95 additions & 18 deletions .claude/skills/arquitectura-drawio/references/estilo.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,35 @@ Pasteles estándar de draw.io para categorizar cajas: azul `#dae8fc` (LLM/servic
## Tipografía y tamaños

Fuente por defecto de draw.io. `fontSize` 12 en nodos, 15 en banners, 10–11 en detalles/almacenes.
Tamaños de caja habituales: tarjeta **160×60** o **170×60**, almacén **≈190×44**, actor **48×60**.

Tamaños medidos en los diagramas de referencia de la organización, que son el estándar a igualar:

| Elemento | Tamaño | Etiqueta |
|---|---|---|
| Caja de componente | **~180×56** (mediana real 176–200 × 52–56) | **30–45 caracteres**, 2–3 líneas cortas |
| Almacén (cilindro) | ≈170×44 | Nombre + una línea de detalle |
| Actor | 48×60 | Dos palabras |
| Zona / banda | ancho completo | Título corto alineado a la izquierda |

**Cajas pequeñas y muchas, no pocas y grandes.** Una caja de 300×88 con cuatro líneas de prosa es
media nota al pie disfrazada de componente: ocupa el espacio de tres cajas reales y obliga a bajar
el número de nodos, que es justo lo que vuelve el diagrama incompleto para un lector técnico. Si el
texto no cabe en 45 caracteres, el detalle va a la etiqueta de la flecha, a la zona que agrupa o a
una nota al pie de la página. `check_layout` avisa cuando la mediana de la página se pasa.

## El rojo se concentra, no se rocía

El rojo (`STYLE["warn"]`, `#f8cecc`) marca deuda técnica o violación de capas. **Va agrupado en una
zona rotulada** —«Deuda técnica», «Piezas sin implementar»— donde el lector lo interpreta como una
sección del diagrama. Repartido sobre los pasos de un flujo produce el efecto contrario al buscado:
si cuatro de los diez pasos de un pipeline salen en rojo, el sistema entero se lee como averiado
cuando lo que está mal puede ser solo dónde viven unos imports.

En un flujo, colorea cada paso por **lo que es** (su capa) y saca la deuda a una nota al pie o a una
página aparte; marca en rojo únicamente el punto exacto de la violación. Y cuida que el texto de la
leyenda cubra **todos** los usos que le das al color: si el rojo también señala «duplica el wiring»,
la leyenda no puede decir solo «viola ADR-001». `check_layout` avisa si más del 25 % de las cajas de
una página están en rojo fuera de una zona de deuda.

## Esquinas redondeadas fijas (dos niveles)

Expand Down Expand Up @@ -103,19 +131,21 @@ rojo = orquestador/crítico, blanco = almacén de apoyo, amarillo = salida). Añ
extra que apliquen (iconos sueltos = fuentes externas; flechas animadas = sentido del flujo).

Dibújala en una franja al pie (dentro del `pageHeight`) con **edges de muestra reales** — mismo `style`
que en el diagrama, con `sourcePoint`/`targetPoint` en la geometría en lugar de `source`/`target` — y
**chips** de color usando el `fillColor`/`strokeColor` de cada arquetipo:
que en el diagrama — y **chips** de color usando el `fillColor`/`strokeColor` de cada arquetipo:

```python
# Línea de muestra (sin nodos): usa Page.legend_edge(x1, x2, y, style) — NO Page.edge()
p.legend_edge(960, 1005, 756, EDGE["data"]) # swatch verde = flujo de datos
# Chip de color de caja: un node() normal con el estilo del arquetipo
p.node("Paso determinista", 960, 780, 240, 38, STYLE["det"])
```
# Línea de muestra (sin nodos): reutiliza el EDGE real, solo cambia los puntos
<mxCell id="lg-e-green" style="edgeStyle=none;html=1;flowAnimation=1;strokeColor=#66CC00;strokeWidth=3;startArrow=none;endArrow=classic;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="60" y="1080" as="sourcePoint"/><mxPoint x="104" y="1080" as="targetPoint"/>
</mxGeometry>
</mxCell>
# Chip de color de caja (usa el fillColor/strokeColor del arquetipo)
rounded=1;arcSize=6;absoluteArcSize=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;
```

**Por qué `legend_edge` y no `edge`:** una muestra de flecha en la leyenda no conecta dos nodos reales,
así que es un edge "flotante" — mxGraph exige que declare sus extremos con
`<mxPoint as="sourcePoint">`/`<mxPoint as="targetPoint">` dentro de `mxGeometry`. Sin el atributo `as=`
draw.io no sabe dónde dibujar el segmento y la muestra queda invisible (bug real detectado: el XML
parseaba y el linter de traslapes no lo atrapaba, pero la línea no se veía en draw.io). `legend_edge`
ya emite esos atributos — no construyas el `<mxPoint>` a mano.

## Claridad y anti-traslape (lo más importante para que se entienda)

Expand All @@ -132,23 +162,70 @@ Reglas de diseño:
- **Almacenes de apoyo** pegados **junto** al nodo que los consume (edge corto), no al otro extremo.
- **Flechas que cruzarían un nodo** → usa `points=[(x,y),...]` en `edge()` para sacar la ruta por
encima/alrededor (un carril libre), o reubica los nodos. No dejes una flecha atravesando un icono.
- **Etiquetas de flecha** (`202`, `Post`): ponlas donde el tramo esté libre; si caen sobre un nodo,
- **Etiquetas de flecha: el hueco manda.** Es el traslape más frecuente y el más invisible al
escribir el script. La etiqueta se dibuja en el punto medio del recorrido, o sea **dentro del hueco
entre las dos cajas que conecta**; si el hueco es más angosto que la etiqueta, esta se monta sobre
ambas. Regla operativa: **hueco ≥ 8 px × nº de caracteres de la etiqueta, + 20 px de aire.**
`search_papers(query)` son 20 caracteres → necesita ~180 px de separación, no 40. Si no tienes ese
espacio, tienes tres salidas y ninguna es dejarlo así: acortar la etiqueta, moverla a un tramo
vertical largo con un waypoint, o quitarla y llevar ese dato dentro de la caja destino.
- **Etiquetas de flecha sobre otros nodos**: ponlas donde el tramo esté libre; si caen sobre un nodo,
mueve el nodo o añade un waypoint para desplazar el punto medio.
- **Contenedor/zona**: dibújalo primero (queda detrás), `fillColor=none`; el título va en una esquina
(`align=left`), no centrado sobre el paso de las flechas.

### Patrones de composición (resuelven el 90 % de los enredos)

Antes de pelear con waypoints, prueba a cambiar la disposición. Estos patrones vienen de rehacer
diagramas que habían quedado ilegibles:

- **Serpentina para secuencias largas.** Si un flujo no cabe en una fila, no vuelvas al margen
izquierdo con una flecha de retorno gigante: alterna el sentido por fila (fila 1 →, fila 2 ←,
fila 3 →) y **baja de fila por la misma columna**, con un tramo vertical corto. Así ninguna
flecha retrocede ni cruza otra. Es lo que permite meter 12 pasos en una página sin un solo cruce.
- **Pasos numerados** ① ② ③ en la etiqueta de cada caja. El lector sigue números, no flechas; y de
paso te libera de dibujar los retornos, que son la mitad del enredo en un diagrama de secuencia.
- **Corredores reservados.** Deja carriles (verticales u horizontales) sin ningún nodo, y rutea por
ahí las flechas largas con `points=`. Un par de corredores en los márgenes convierte un ruteo
imposible en uno trivial. Reserva el carril **antes** de colocar los nodos, no después.
- **Codifica la capa en el COLOR de la caja, no en bandas**, cuando la página es de flujo. Dibujar
bandas por capa *y* un flujo encima obliga a que cada paso cruce de banda: es la receta exacta
para que las etiquetas caigan sobre los títulos. Las bandas son para la vista estática; en la
dinámica, el color ya dice la capa y la leyenda lo traduce.
- **Sustituye un haz N:M por una tabla.** Una relación aburrida y densa (qué implementa qué
contrato, qué servicio consume qué cola) son diez flechas cruzadas o una caja de texto con dos
columnas. Gana la caja: se lee mejor y no gasta presupuesto de flechas.
- **Cuidado con las formas de etiqueta inferior** (cilindro `store`, actor `user`): su huella real
es la caja **más ~1.7× su ancho** de texto debajo. No las pongas de vecinas apretadas, no las uses
como chip de leyenda tal cual (el kit ya lo resuelve en `Page.legend`) y no rutees una flecha justo
por debajo.

### Verificar con el linter (obligatorio antes de entregar)

`drawio_kit.write()` corre `check_layout` y avisa. Para el detalle:

```bash
python scripts/check_layout.py <archivo>.drawio
python scripts/check_layout.py docs/architecture.drawio
```

Reporta **nodos-superpuestos**, **etiqueta-sobre-nodo**, **flecha-cruza-nodo** y
**etiqueta-flecha-sobre-nodo**. Itera reubicando nodos / añadiendo waypoints hasta que no queden
traslapes **duros** (nodos-superpuestos, flecha-cruza-nodo). Es heurístico (el ruteo real de draw.io
difiere), así que confirma también visualmente en draw.io.
Reporta ocho tipos de problema: **nodos-superpuestos**, **etiqueta-sobre-nodo**,
**flecha-cruza-nodo**, **etiqueta-flecha-sobre-nodo**, **etiqueta-flecha-sobre-titulo-zona**,
**etiquetas-flecha-encimadas**, **texto-desborda-caja** y **nodo-fuera-de-pagina**. Itera
reubicando nodos / añadiendo waypoints hasta que no queden problemas **duros** (nodos-superpuestos,
flecha-cruza-nodo, nodo-fuera-de-pagina, que hacen salir con código ≠ 0).

Dos notas sobre por qué el linter mira lo que mira:

- Las **zonas se excluyen** de los chequeos de cruce porque contienen nodos por diseño, pero su
**título** sí se comprueba: vive en una banda de ~26 px arriba y ahí es donde aterrizaban las
etiquetas de flecha (bug real: el título quedó como `infrastructure/ — SDK[Atom XML]erno`). Para
que esa detección funcione, dibuja las zonas con `Page.zone()`, que marca la celda como contenedor.
- La posición de una etiqueta de flecha se estima **sobre la polilínea ruteada**, no en el punto
medio recto origen→destino: con waypoints, ambos puntos no tienen nada que ver.

Sigue siendo heurístico (el ruteo real de draw.io difiere) y **no sustituye la revisión visual**:
el linter no ve fuentes, ni iconos que no cargan, ni palabras pegadas. Confirma siempre con un
export real (ver SKILL.md, sección de verificación).

## Etiquetas: pasar texto crudo (evitar doble escape)

Expand Down
65 changes: 58 additions & 7 deletions .claude/skills/arquitectura-drawio/references/iconos.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,74 @@ React, Node, Python, TypeScript, Docker, Kubernetes, PostgreSQL, FastAPI, etc. L
> **Regla:** para cada componente usa SIEMPRE el **logo oficial de la tecnología** que representa;
> si no existe, cae al **glifo genérico**. Nunca inventes un icono ni dejes una caja vacía.

## 1ª opción — logo oficial de la tecnología (recomendado)
## Al generar: `icon()` — la regla completa en una llamada (recomendado)

Punto de entrada único en `scripts/glyph.py`, sirve para cualquier tecnología:
```python
from glyph import icon

p.node("Frontend React", x, y, w, h, STYLE["card"], icon=icon("react", "code"))
p.node("PostgreSQL", x, y, w, h, STYLE["card"], icon=icon("postgresql", "database"))
p.node("Google ADK", x, y, w, h, STYLE["llm"], icon=icon("adk", "smart_toy"))
p.node("Worker propio", x, y, w, h, STYLE["card"], icon=icon("__worker__", "code"))
```

`icon(name, fallback, color)` intenta el **logo oficial** y, si esa tecnología no lo tiene, cae al
**glifo genérico** `fallback`. Si no hay red devuelve `None` y el kit dibuja la caja sin icono, en
vez de romper la generación. Para forzar el glifo sin intentar el logo, usa un `name` que no exista
como marca (convención: `"__mi_componente__"`).

> **No le pases `color` a un logo de marca salvo que quieras forzarlo.** El tercer argumento existe
> para los glifos genéricos, donde el color codifica la categoría. En un logo, pasar color **apaga el
> arte multicolor**: se salta devicon y tiñe la silueta de simple-icons de un solo tono.
> `icon("python", "code")` da el logo azul-y-amarillo real; `icon("python", "code", "#3776AB")` da
> una silueta plana azul.

## Por qué un icono sale a color o en negro

Los diagramas apagados casi siempre vienen de aquí: **simple-icons es monocromo por diseño**. Cada
logo es un único `<path>` sin `fill`, así que renderiza NEGRO salvo que se le pase un color, y aun
así queda plano. Es un set de siluetas de marca, no de logos a color.

`logo()` prueba las fuentes en este orden, para que el color sea lo normal y el negro la excepción:

| Orden | Fuente | Aspecto | Cobertura |
|---|---|---|---|
| 1 | **devicon** `original` | **Multicolor**, con degradados | Stack clásico: python, docker, postgresql, react, nodejs, kubernetes, fastapi, googlecloud… |
| 2 | **gcp_icon** | **Multicolor** (arte oficial de Google) | Productos Google Cloud: vertex_ai, bigquery, cloud_run… |
| 3 | **simple-icons + `brand_hex()`** | Monocromo, pero en el **color oficial de la marca** | ~3300 marcas: telegram `#26A5E4`, huggingface `#FFD21E`, pydantic `#E92063`… |

El paso 3 solo queda oscuro cuando el color de marca **es** oscuro: Anthropic es `#191919`, así que
su logo es casi negro por definición y está bien así.

```bash
python scripts/glyph.py logo python # devicon multicolor
python scripts/glyph.py logo telegram # simple-icons teñido con su #26A5E4
```

**Peso:** el arte multicolor pesa entre 2 y 3 veces más que la silueta (python 1,6 → 3,1 KB; docker
2,0 → 5,6 KB; postgresql 5,7 → 10,6 KB), porque lleva varios `path` y a veces degradados. Teñir un
simple-icons no cuesta nada (+25 bytes). En un diagrama normal la diferencia son unas decenas de KB,
muy lejos del límite de 500 KB; si alguna vez aprieta, la palanca es reducir iconos repetidos, no
volver al monocromo.

## 1ª opción por dentro — logo oficial de la tecnología

Si quieres controlar el fallo tú mismo:

```python
from glyph import logo
p.node("LangChain service", x, y, w, h, istyle(logo("langchain")))
p.node("Frontend React", x, y, w, h, istyle(logo("react", "#61DAFB")))
p.node("Frontend React", x, y, w, h, istyle(logo("react"))) # multicolor
p.node("PostgreSQL", x, y, w, h, istyle(logo("postgresql")))
p.node("Vertex AI", x, y, w, h, istyle(logo("vertex_ai"))) # cae a producto GCP
```

`logo(name, color)` prueba, en orden: **marca** (simple-icons, ~3000 logos oficiales) y luego
**producto Google Cloud** (`gcp_icon`). Si ninguno tiene el logo, lanza un error que te guía al
glifo genérico (no inventa iconos). `color` es opcional (respeta el color de marca).
`logo(name, color)` prueba las tres fuentes de la tabla de arriba en orden, priorizando el arte a
color. Si ninguna tiene el logo, lanza un error que te guía al glifo genérico (no inventa iconos).
`color` es opcional y, si lo pasas, **salta devicon** y tiñe la silueta de simple-icons: úsalo solo
cuando quieras un color concreto por encima del arte de marca.

- Slugs de marca típicos: `langchain`, `react`, `nodejs`, `typescript`, `python`, `docker`,
- Slugs típicos (sirven igual en devicon y simple-icons): `langchain`, `react`, `nodejs`, `typescript`, `python`, `docker`,
`kubernetes`, `fastapi`, `postgresql`, `redis`, `awslambda`, `amazonsqs`, `microsoftazure`,
`apache`, `openai`, `huggingface`. Catálogo completo: <https://simpleicons.org>.
- Productos GCP: `vertex_ai`, `bigquery`, `cloud_run`, `cloud_storage`, `cloud_vision_api`,
Expand Down
71 changes: 71 additions & 0 deletions .claude/skills/arquitectura-drawio/scripts/check_labels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""check_labels — detecta etiquetas mal partidas en el SCRIPT que genera un diagrama.

Las etiquetas de un diagrama son textos largos y chocan con el límite de columnas del
repo (ruff/flake8). Al partirlas en concatenación implícita es fácil perder el espacio
final de un trozo, y draw.io las muestra pegadas:

"... dependiendo de " "infrastructure/" -> "dependiendo de infrastructure/" OK
"... dependiendo de" "infrastructure/" -> "dependiendo deinfrastructure/" MAL

Es un fallo invisible: el XML es válido, el layout no se traslapa y el linter de
legibilidad pasa; solo se ve al mirar el diagrama renderizado. Este chequeo lo atrapa en
el fuente, que es donde la información existe (sobre el .drawio ya solo hay texto plano y
`deinfrastructure` no se distingue de un identificador legítimo).

CLI:
python check_labels.py build_mi_diagrama.py # sale !=0 si encuentra algo

Como librería:
from check_labels import check
problemas = check("build_mi_diagrama.py") # -> [(línea, izquierda, derecha)]
"""

from __future__ import annotations

import pathlib
import re
import sys

# Una línea que es EXACTAMENTE un literal de string (posiblemente con coma final).
_LITERAL = re.compile(r'^\s*"((?:[^"\\]|\\.)*)"(,?)\s*$')

# Finales/inicios donde la concatenación sin espacio es intencional y correcta:
# un token de estilo (`fillColor=#fff;`), un salto de línea explícito, un guion de
# palabra partida, o el trozo derecho empieza por puntuación.
_FIN_OK = (" ", "\\n", "-", "(", ";", ":", "/", "=")
_INI_OK = (" ", "\\n", ")", ",", ".", ";", ":", "/")


def check(path: str) -> list[tuple[int, str, str]]:
lineas = pathlib.Path(path).read_text(encoding="utf-8").split("\n")
fallos: list[tuple[int, str, str]] = []
for i in range(len(lineas) - 1):
a, b = _LITERAL.match(lineas[i]), _LITERAL.match(lineas[i + 1])
if not (a and b):
continue
if a.group(2): # coma final -> son argumentos distintos, no se concatenan
continue
izq, der = a.group(1), b.group(1)
if not izq or not der:
continue
if izq.endswith(_FIN_OK) or der.startswith(_INI_OK):
continue
fallos.append((i + 1, izq, der))
return fallos


if __name__ == "__main__":
if len(sys.argv) < 2:
print("uso: python check_labels.py <script_generador>.py [...]")
sys.exit(2)
total = 0
for archivo in sys.argv[1:]:
for linea, izq, der in check(archivo):
total += 1
print(f"{archivo}:{linea} «…{izq[-24:]}» + «{der[:24]}…»")
print(f"{' ' * len(archivo)} -> quedaría «{izq[-14:]}{der[:14]}»")
if total:
print(f"\n⚠ {total} etiqueta(s) partida(s) sin espacio de separación.")
print(' Añade el espacio al FINAL del trozo izquierdo: "…de " "infrastructure/"')
sys.exit(1)
print("✓ check_labels: etiquetas bien partidas")
Loading
Loading