La ciudad ya tiene rostro
Racoonity deja de ser una máquina que funciona detrás de una puerta y empieza a convertirse en un producto que una persona puede usar.
La ciudad ya tiene rostro
En la iteración anterior conseguimos algo que, aunque pequeño, era fundamental: la puerta existía.
Racoonity podía arrancar, detectar si una instalación estaba preparada, completar el setup, iniciar sesión y llevar a una persona hasta un Product Shell coherente. Por primera vez, el sistema dejó de sentirse únicamente como un backend con buenos contratos.
Pero abrir la puerta no sirve de mucho si, detrás de ella, todavía no hay una ciudad que recorrer.
La siguiente pregunta era bastante más difícil:
¿Cómo hacemos que Racoonity empiece a tener una interfaz real sin convertir cada pantalla en otra implementación artesanal de las mismas reglas?
La respuesta de esta iteración fue Racoon Painter.
Y, con él, la ciudad empezó a tener rostro.
El problema no era “hacer unas pantallas”
Era perfectamente posible crear una lista de productos, un formulario de categorías y un detalle de producto con React tradicional.
De hecho, habría sido más rápido.
El problema es que Racoonity no pretende quedarse para siempre con un catálogo estático. Una de sus ideas centrales es que el sistema pueda crecer sin exigir que cada personalización termine en:
abre el frontend
→ agrega otro if
→ agrega otro componente
→ recompila
→ despliega otra vez
Eso funciona durante un tiempo.
Después aparecen campos personalizados, diferentes vistas, permisos, relaciones, reglas de visibilidad y necesidades particulares de cada negocio. Y, poco a poco, la interfaz empieza a conocer demasiadas cosas que no debería conocer.
Por eso Painter no debía ser “un generador de formularios”.
Tenía que convertirse en una pieza real del producto.
La arquitectura que terminamos consolidando se parece a esto:
Atlas
↓
Presentation Contract
↓
Painter Definition Adapter
↓
View Runtime
├── List
├── Form
└── Detail
↓
Field Registry
Atlas resuelve qué debería mostrarse.
Painter decide cómo representarlo.
Catalog coordina las operaciones reales.
Y cada uno conserva su responsabilidad.
La primera cara de la ciudad: Categorías y Productos
Para comprobar que Painter servía para algo más que una demostración técnica necesitábamos un consumidor real.
Ese consumidor fue Catalog.
Durante esta iteración, Categorías quedó operable con:
- lista;
- creación;
- edición;
- archivado;
- reactivación.
Productos quedó operable con:
- lista;
- búsqueda;
- filtros;
- paginación;
- creación;
- edición;
- detalle;
- archivado;
- reactivación.
Lo importante no es únicamente que esas pantallas existan.
Lo importante es cómo existen.
Las listas, formularios y detalles no están construidos como tres mundos separados que casualmente muestran los mismos datos. Todos consumen la misma definición de presentación y pasan por el mismo runtime.
Un campo de texto, un decimal, un booleano, una fecha o una relación no necesita que cada pantalla vuelva a decidir cómo funciona.
Painter tiene un Field Registry.
La metadata describe la intención.
El renderer decide la implementación.
Ese detalle va a importar mucho más adelante.
El campo que el frontend nunca conoció
Probablemente la prueba que mejor resume toda la iteración fue muy pequeña:
warranty_months
Un campo de garantía en meses para Product.
El frontend no tenía ningún componente escrito específicamente para él.
No había:
if field == "warranty_months"
No había una sección llamada “Campos personalizados”.
No había un badge especial.
No había JSX escrito para ese campo.
El campo entró al mismo pipeline que cualquier otro:
metadata
→ Atlas
→ Presentation Contract
→ Painter
En una lista apareció como columna.
En Detail apareció como otro dato.
En Create apareció como un control normal.
En Edit recuperó su valor existente.
Al guardar, Catalog separó los campos conocidos del dominio de Product y colocó el resto en custom_values.
Painter nunca necesitó enterarse de esa diferencia.
Para Painter:
un campo es un campo.
Esa separación es importante porque mantiene una frontera muy concreta:
Painter
→ renderiza intención
Catalog
→ conoce Product
Backend
→ protege las reglas reales
La complejidad no desaparece.
Simplemente deja de estar desperdigada por todo el sistema.
También descubrimos dónde estaba mal puesta una responsabilidad
Las iteraciones verticales sirven para encontrar cosas que se ven correctas mientras las pruebas son demasiado pequeñas.
Durante el trabajo con custom fields apareció un caso así.
La metadata ya tenía un atributo visible, pero la responsabilidad de respetarlo no estaba realmente consolidada dentro de Painter.
La primera solución funcional fue filtrarlo desde Catalog.
Visualmente funcionaba.
Arquitectónicamente era incorrecto.
Si Catalog tenía que entender cómo funciona visible, entonces Customers, Inventory y cualquier otro consumidor futuro también tendrían que hacerlo.
La corrección final quedó donde debía:
PresentationContract.fields[].visible
→ Painter Definition Adapter
→ PainterDefinition
→ List / Form / Detail
Ahora visible=false significa lo mismo independientemente del feature que utilice Painter.
Parece una corrección pequeña.
Es exactamente el tipo de corrección pequeña que evita copiar la misma decisión ocho veces dentro de un producto.
Painter también tiene que saber fallar
Un sistema dinámico no se demuestra únicamente con metadata correcta.
También tiene que sobrevivir metadata incorrecta.
Durante esta iteración endurecimos el comportamiento de diagnóstico de Painter para que una definición inválida no termine en una pantalla en blanco, un crash de toda la aplicación o, peor, detalles internos expuestos al usuario.
El principio quedó bastante sencillo:
fallo conocido de Painter
→ estado seguro
throw realmente inesperado
→ Shell Error Boundary
Y encontramos un bug real durante ese proceso.
Un METADATA_RESOLUTION_FAILED podía terminar mostrando directamente el mensaje técnico recibido del backend.
Eso podía convertir algo interno en copy visible.
Se corrigió y se añadieron pruebas explícitas contra cosas que una interfaz jamás debería enseñar:
SQL
stack traces
nombres de tablas
secrets
mensajes internos
La interfaz necesita decir que algo salió mal.
No necesita contarle al usuario cómo está construido el sótano.
La accesibilidad no se dejó para “después”
Otra parte de esta iteración fue bastante menos llamativa, pero probablemente más importante para convertir una demo en producto.
Auditamos teclado, foco, labels, errores, relaciones, estados y layout responsive.
Ahí aparecieron varios detalles reales:
- presionar Enter sobre una acción de una fila podía activar también la fila;
- algunos labels apuntaban a elementos que no podían ser etiquetados;
- ciertos errores eran visibles pero no movían el foco al campo correspondiente;
- una tabla ancha podía hacer overflow de toda la página;
- los formularios permanecían en una sola columna incluso en ventanas amplias.
Ninguno era un “feature”.
Todos afectaban cómo se siente usar el sistema.
Después del hardening, las listas contienen su propio overflow, los formularios responden al espacio disponible, los errores utilizan semántica accesible y el foco se mueve de manera predecible.
La regla sigue siendo la misma:
una iteración no se cierra porque la pantalla “se ve bien”.
601 tests verdes y un sistema que no funcionaba
La parte más divertida de esta iteración llegó casi al final.
Para entonces el frontend tenía:
601 tests
72 archivos de test
13 reglas arquitectónicas
0 violaciones
Todo verde.
Entonces abrimos un navegador real.
Levantamos Postgres real.
Levantamos el backend real.
Levantamos Vite.
Ejecutamos Chromium.
Hicimos login.
Y Catalog respondió, básicamente:
sesión expirada.
El problema era bastante simple: las queries y mutations de Catalog no estaban enviando el token de sesión al backend real.
Los tests de componente no lo habían visto porque trabajaban con fetch controlado.
El navegador sí.
Ese fue probablemente el mejor recordatorio de toda la iteración:
una suite verde demuestra exactamente lo que prueba, no lo que imaginamos que prueba.
El E2E existía precisamente para encontrar la costura entre piezas que individualmente parecían correctas.
Y la encontró.
El fix mantuvo la frontera existente: ApiClient no comenzó mágicamente a conocer Session. La orchestration del feature obtiene el token y lo entrega explícitamente al servicio que ejecuta la operación.
En la misma prueba aparecieron otros dos huecos más mundanos:
- Product List no tenía una entrada visible hacia “Nuevo producto”.
- Product Detail no tenía una entrada visible hacia “Editar producto”.
Las rutas existían.
Los formularios existían.
Los tests existían.
El usuario no tenía cómo llegar a ellos de manera natural.
Otra diferencia importante entre software implementado y producto utilizable.
La prueba completa
Al final, el journey de I1 quedó probado en un navegador real contra un backend y una base de datos reales:
login
→ navegación resuelta por Atlas
→ crear categoría
→ crear producto
→ usar un custom field
→ editar producto
→ ver detalle
→ archivar producto
→ reactivar producto
El custom field utilizado por la prueba se prepara directamente como metadata publicada de test.
Eso es deliberado.
Esta iteración no intenta construir todavía la experiencia completa de Artifier. Solo necesitábamos comprobar una propiedad más fundamental:
si el campo ya existe en la definición efectiva, Painter debe tratarlo como un ciudadano de primera clase.
Y lo hace.
Algunas limitaciones siguen siendo limitaciones
Cerrar una iteración no significa fingir que todos los contratos son perfectos.
Durante el trabajo quedaron documentadas algunas restricciones reales.
Las Actions que recibe Painter todavía están asociadas al modelo y no a una vista concreta, así que Catalog filtra qué acciones tienen sentido en List, Form o Detail.
Las Actions tampoco traen todavía suficiente información de presentación para resolver todos sus labels, así que Catalog mantiene copy para las capacidades que conoce.
El transporte de custom_filters existe, pero todavía no existe una señal contractual completa que permita descubrir dinámicamente qué custom fields son filtrables.
Y el correlationId de una Query exitosa se conserva en ApiClient, pero actualmente se pierde cuando la abstracción devuelve únicamente data, por lo que un fallo posterior durante adaptación de metadata no siempre puede heredarlo.
Ninguna de estas limitaciones impide I1.
Todas están registradas.
Eso también forma parte de cerrar correctamente una iteración.
Qué quedó al final
El cierre terminó con:
Frontend
→ 601 tests
→ 72 archivos
→ build verde
→ lint verde
→ 13 reglas arquitectónicas
→ 0 violaciones
Backend
→ 49 paquetes con tests verdes
→ 0 FAIL
E2E
→ journey I0 PASS
→ journey I1 PASS
Specs
→ 124 / 124 escenarios mapeados a evidencia
Known P0 bugs
→ none
Known P1 bugs
→ none
Backend contract changes
→ none
Más importante que los números: el backend productivo no tuvo que cambiar para construir esta experiencia.
I1 consumió los contratos que ya existían y convirtió esa capacidad en producto.
La ciudad ya tiene rostro
Después de I0 podíamos decir:
Racoonity tiene una puerta.
Ahora podemos decir algo diferente:
Racoonity ya tiene una forma de mostrarse.
No una pantalla.
No una colección de formularios.
Una forma de traducir definiciones a experiencias sin obligar al renderer a conocer cada detalle del negocio.
Todavía falta muchísimo.
No tenemos Inventory productivo en frontend.
No tenemos POS.
No tenemos Artifier como experiencia completa.
No tenemos Diagnostics completo.
No estamos cerca de abrir la ciudad al público.
Pero ya ocurrió algo importante.
Por primera vez, una persona puede entrar a Racoonity, navegar a Catalog y trabajar con entidades reales a través de una interfaz coherente.
Y por primera vez podemos agregar un campo que el frontend nunca conoció al compilarlo y verlo aparecer como si siempre hubiera estado ahí.
Eso acerca un poco más a Racoonity a la idea que guía todo el proyecto:
Simple por defecto, monstruoso por elección.
La puerta existía.
Ahora, detrás de ella, la ciudad ya tiene rostro.