Stately Studio: diseñar en el lienzo y aterrizar en el repositorio
El estudio de Stately no es un dibujador bonito sino un editor del mismo objeto que ejecuta tu aplicación. La lección analiza el ciclo completo de ida y vuelta: diseñar en el lienzo, simular sin código, exportar a XState v5 y volver a importar lo que el equipo cambió. El núcleo técnico es por qué ese round trip funciona hoy donde el de UML fracasó: la API `setup` parte la máquina en un esqueleto estrictamente serializable y unas implementaciones que la notación nunca intenta representar. De esa frontera salen todas las disciplinas prácticas del capítulo.
La primera reacción ante un editor visual de máquinas suele ser la sospecha de que se trata de una herramienta para quien no sabe escribir código, o de una capa de presentación sobre algo que se acabará tocando a mano de todas formas. Es una lectura razonable y equivocada. El estudio de Stately no edita una representación del sistema: edita el sistema, porque el objeto que manipula el lienzo y el objeto que interpreta tu aplicación son el mismo grafo de estados y transiciones expresado en dos sintaxis. Lo interesante no es que se pueda arrastrar un estado con el ratón, sino la condición técnica que hace que arrastrarlo produzca código que un ingeniero aceptaría en una revisión, y esa condición tiene nombre y vive en la API setup de XState v5.
- Manejar el estudio de Stately como editor del mismo artefacto que ejecuta la aplicación, no como diagramador.
- Explicar por qué la partición entre esqueleto serializable e implementaciones hace viable el ciclo de ida y vuelta.
- Establecer una disciplina de archivos que convierta el pegado desde el lienzo en una operación mecánica y revisable.
- Reconocer la frontera de lo que el estudio no debe tocar y qué señales indican que se ha cruzado.
El lienzo como editor del artefacto
El estudio ofrece cuatro capacidades que conviene separar mentalmente porque se usan en momentos distintos. La primera es la edición: crear estados, anidarlos, conectarlos con transiciones etiquetadas por evento, añadir guardas y acciones por nombre, marcar estados finales y regiones paralelas. La segunda es la simulación: entrar en un modo donde el diagrama se recorre haciendo clic sobre los eventos disponibles, con el estado activo resaltado, sin arrancar ninguna aplicación ni escribir una línea de interfaz. La tercera es el intercambio: importar código pegado, exportar a XState v5, a JSON o a imagen, y sincronizar con un repositorio. La cuarta es la colaboración: comentarios sobre el lienzo, versiones guardadas y bifurcaciones para explorar alternativas sin destruir la que ya se acordó.
Editar
Estados, jerarquía, transiciones, guardas y acciones por nombre. Todo lo que se dibuja pertenece al núcleo serializable de la máquina.
Simular
Recorrer el modelo a golpe de evento antes de que exista la interfaz. Valida el comportamiento cuando cambiarlo aún es gratis.
Intercambiar
Importar desde código, exportar a XState v5, a JSON o a imagen, y sincronizar con el repositorio como fuente.
Colaborar
Comentarios, versiones y bifurcaciones. El desacuerdo sobre el comportamiento se discute sobre el modelo, no sobre un documento.
La colaboración merece un comentario aparte porque se subestima. Poder bifurcar un modelo y proponer una variante sin destruir la acordada convierte una discusión de producto en algo parecido a una petición de cambio: hay una versión base, una propuesta, un lugar donde comentar y una decisión explícita de adoptar o descartar. Los desacuerdos sobre comportamiento que en un documento se resuelven por cansancio o por antigüedad, aquí se resuelven comparando dos modelos y recorriéndolos, que es un procedimiento con muchas menos aristas políticas.
De las cuatro, la que más cambia la forma de trabajar es la simulación, y por una razón económica. El coste de corregir una decisión de comportamiento crece con lo que se ha construido encima: cambiar una transición en el lienzo cuesta segundos, cambiarla cuando ya hay tres componentes, dos pruebas y una llamada al servidor colgando de ella cuesta una tarde y una discusión. Simular es adelantar el descubrimiento del error al momento en que es barato. Esta es la misma lógica del desplazamiento a la izquierda que se predica para las pruebas, aplicada un paso antes: no se prueba la implementación, se prueba la especificación.
Además del estudio en la web existe la extensión de Stately para el editor de código, que abre el lienzo junto al archivo y edita la máquina en su sitio. Esto elimina el paso de copiar y pegar, que es donde se pierden los cambios y donde aparece la tentación de tocar el diagrama sin tocar el código. Cuando el lienzo vive dentro del repositorio, la pregunta sobre cuál de las dos versiones es la buena desaparece: solo hay una, la del archivo.
Por qué este round trip sí funciona
La lección anterior dejó el diagnóstico del fracaso de las herramientas CASE: el round trip entre dos representaciones de expresividad desigual degenera en una fusión perpetua. La solución de XState v5 es no intentar la simetría. En lugar de traducir todo el código a diagrama y de vuelta, parte la máquina en dos mitades con dueños distintos. La mitad que el lienzo posee es el esqueleto: estados, jerarquía, transiciones, eventos, y los nombres de las acciones, guardas y actores. La mitad que el repositorio posee son las implementaciones: las funciones que esos nombres designan. setup es exactamente la costura entre ambas.
import { setup, assign, fromPromise } from 'xstate'
export const pago = setup({
types: {
context: {} as { intentos: number; error: string | null },
events: {} as { type: 'PAGAR' } | { type: 'REINTENTAR' },
},
// Implementaciones: viven en el repositorio, el lienzo solo ve sus nombres.
actions: {
contarIntento: assign({ intentos: ({ context }) => context.intentos + 1 }),
limpiarError: assign({ error: null }),
},
guards: {
quedanIntentos: ({ context }) => context.intentos < 3,
},
actors: {
cobrar: fromPromise(async () => {
const res = await fetch('/api/cobro', { method: 'POST' })
if (!res.ok) throw new Error('cobro rechazado')
return res.json()
}),
},
}).createMachine({
// Esqueleto: esto es lo que el lienzo dibuja y devuelve.
id: 'pago',
initial: 'listo',
context: { intentos: 0, error: null },
states: {
listo: {
on: { PAGAR: { target: 'cobrando', actions: 'contarIntento' } },
},
cobrando: {
invoke: {
src: 'cobrar',
onDone: 'hecho',
onError: [
{ target: 'fallido', guard: 'quedanIntentos' },
{ target: 'agotado' },
],
},
},
fallido: { on: { REINTENTAR: { target: 'cobrando', actions: 'limpiarError' } } },
hecho: { type: 'final' },
agotado: { type: 'final' },
},
})
Obsérvese que en el bloque de createMachine no hay una sola función: contarIntento, quedanIntentos y cobrar son cadenas de texto que apuntan a implementaciones declaradas arriba. Ese bloque es JSON con azúcar sintáctico, y por eso el lienzo puede leerlo, dibujarlo, dejar que alguien mueva una flecha y devolverlo sin haber tocado ni una línea de lógica. La asimetría que hundió al round trip de UML se resuelve no reduciéndola sino declarándola: hay una frontera explícita, cada lado tiene un dueño y la herramienta jamás cruza.
flowchart LR A[Esqueleto en el repositorio] -->|importar o sincronizar| B[Lienzo del estudio] B -->|editar y simular| B B -->|exportar a XState v5| C[Esqueleto revisado] C -->|revision de cambios| A D[Implementaciones en setup] -.el lienzo nunca las toca.-> A
La consecuencia práctica es una regla de organización de archivos que conviene adoptar desde el primer día: el archivo de la máquina contiene el setup con implementaciones cortas y el createMachine con el esqueleto, y nada más. Sin componentes, sin utilidades, sin constantes de otra parte del dominio. Si las implementaciones crecen, se extraen a un módulo aparte y se importan; si necesitan depender del entorno —una implementación falsa en pruebas, otra real en producción— se dejan como referencias y se inyectan con el método provide en el punto de creación del actor. Cuanto más fina sea la capa de implementación dentro del archivo, más mecánico es aplicar lo que vuelve del lienzo.
La frontera y sus señales
Toda la propiedad descansa en que el esqueleto siga siendo serializable, y es fácil perforarlo sin darse cuenta. Cuatro señales indican que se ha cruzado la frontera y que alguna proyección acaba de morir en silencio.
Funciones anónimas en el esqueleto
Una acción o guarda escrita en línea dentro de un on deja de ser un nombre dibujable. El diagrama muestra una transición sin etiqueta y la simulación no puede evaluarla.
Configuración calculada
Construir los estados con un bucle o extender la configuración con una utilidad genérica produce una máquina que solo existe tras ejecutar código. El lienzo no puede leerla.
Nombres técnicos
Acciones llamadas handleClick3 o estados llamados stateB hacen el diagrama ilegible para quien no escribió el código, que es precisamente el público del diagrama.
Diferencias ruidosas
Si cada exportación reordena claves y reformatea, la revisión de cambios se vuelve imposible de leer y el equipo deja de confiar en el ciclo.
Las dos últimas señales tienen remedios simples y vale la pena aplicarlos antes de que hagan daño. Contra los nombres técnicos, la convención de nombrar los estados con adjetivos o participios del dominio —revisando, expirado, pendiente de firma— y las acciones con verbos que un responsable de producto reconocería. Contra el ruido en las diferencias, pasar siempre el archivo por el formateador del proyecto tras pegar desde el lienzo, y tratar el bloque de createMachine como si fuera generado: no se reordena a mano por gusto estético, porque cada reordenación manual se pierde en la siguiente exportación.
Los tipos merecen una nota, porque son el punto donde más gente tropieza al adoptar el ciclo. El bloque types de setup declara la forma del contexto y la unión de eventos, y es código TypeScript, no dato: el lienzo no lo deduce ni lo mantiene. Cuando en el estudio se añade una transición con un evento nuevo, el código exportado lo referencia y el compilador protesta hasta que alguien amplía la unión a mano. Eso no es un defecto del ciclo, es la frontera funcionando: el modelo propone un evento nuevo y el sistema de tipos exige que se declare antes de aceptarlo. Conviene explicarlo antes de la primera exportación para que nadie interprete el error como un fallo de la herramienta.
Si tu equipo no puede o no quiere usar un servicio externo, casi todo lo de esta lección sigue en pie. Lo que produce el beneficio es mantener el esqueleto serializable y separado de las implementaciones: con esa disciplina puedes generar diagramas tú mismo, como se verá en la última lección, revisar el comportamiento en las peticiones de cambio y simular recorriendo el modelo con las utilidades de grafos. El estudio hace todo eso más cómodo y añade el lienzo colaborativo, pero no es la fuente del valor. Quien adopta el editor sin la partición obtiene una herramienta bonita que no puede usar; quien adopta la partición sin el editor obtiene casi todo.
Hay además una decisión de gobierno que ningún manual resuelve por ti: quién es la fuente de verdad cuando el estudio y el repositorio divergen. La respuesta que funciona en equipos de producto es que el repositorio siempre gana, y el estudio se trata como un espacio de trabajo desde el que se proponen cambios que entran por el mismo camino que cualquier otro código: una rama, una revisión, una fusión. La sincronización con el repositorio que ofrece el estudio se configura precisamente para eso. Invertir la jerarquía —que el estudio sea canónico y el repositorio un reflejo— parece más elegante y produce, sin excepción, la clase de conflictos que arruinaron el round trip de los años dos mil.
El estudio permite generar una máquina a partir de una descripción en lenguaje natural, y el resultado suele ser un esqueleto razonable en segundos. Es una excelente ayuda para arrancar y una pésima base para acordar comportamiento, porque produce los estados obvios y omite exactamente los que importan: el que aparece cuando la petición tarda demasiado, el que aparece cuando el usuario vuelve atrás a mitad del flujo, el que aparece cuando el token caduca durante el envío. Úsalo para el primer borrador y dedica la sesión de modelado a lo que el generador nunca va a preguntarte, que es qué puede salir mal.
Cuando alguien dibuja una máquina en el lienzo cree estar describiendo lo que el sistema hace, y está haciendo algo bastante más profundo: está declarando el complemento, todo lo que el sistema no hará jamás. Un estado con dos flechas salientes no dice solo que hay dos caminos, dice que no hay un tercero; una transición ausente entre dos cajas es una prohibición tan vinculante como cualquier regla escrita, y mucho más difícil de violar por accidente, porque el intérprete simplemente ignorará el evento. Esta inversión es la que separa el modelado del diagramado. Un diagrama de arquitectura enumera lo que existe y calla sobre el resto; un statechart enumera lo que existe y, por el mismo acto, clausura el resto. Por eso el lienzo produce conversaciones que ninguna reunión sobre requisitos había producido: cuando alguien mira las flechas que salen de cobrando y pregunta qué pasa si el usuario cierra la pestaña, no está pidiendo una funcionalidad nueva, está señalando un hueco en un espacio que el dibujo ha hecho finito y por tanto auditable. La prosa de un documento de requisitos nunca es finita —siempre queda un caso del que no se habló y nadie puede demostrar que falta—, mientras que un statechart tiene un número contable de estados y de eventos y, por consiguiente, un número contable de celdas vacías. Diseñar sobre el lienzo no es dibujar más deprisa: es cambiar de un medio donde las omisiones son invisibles a uno donde tienen coordenadas.
- Elige una máquina existente de tu proyecto y comprueba si su bloque de
createMachinecontiene alguna función. Extrae cada una asetupcon un nombre de dominio hasta que el esqueleto sea puro. - Impórtala en el estudio y comprueba que el diagrama resultante es legible sin explicaciones. Si no lo es, el problema son los nombres, no el diagrama.
- Recorre el flujo en modo simulación y anota cada punto donde te sorprenda lo que ocurre. Cada sorpresa es un desajuste entre el modelo que tienes en la cabeza y el que ejecuta tu aplicación.
- Añade en el lienzo un estado que hoy falte —una espera, una caducidad, una cancelación— y exporta el resultado. Aplica el formateador y revisa la diferencia línea a línea.
- Comprueba que la aplicación sigue compilando sin tocar ninguna implementación. Si has tenido que tocarlas, localiza dónde se cruzó la frontera.
- Escribe en el archivo de la máquina un comentario de tres líneas que declare la regla de gobierno: quién es la fuente de verdad y por qué camino entran los cambios del lienzo.