Lila Modeler v1.0.0-beta.26 EN GitHub
Docs›Proyecto›Decisiones de arquitectura

Architecture Decision Log

Registro de decisiones de arquitectura de Lila Modeler. Son reversibles mientras el proyecto esté en etapa temprana — cada una indica, cuando aplica, cuándo revisarla.

Fuentes:

  • ADR-001 a ADR-008: corpus previo del proyecto (entonces llamado Open Process Platform), docs/DECISIONS-corpus-previo.md. Se copian aquí verbatim.
  • ADR-009 a ADR-023: LILA_MODELER_ESTRUCTURA.md, sección 4 ("Decisiones (ADR)"). Se copian aquí verbatim; ese documento es la fuente de verdad — ante cualquier discrepancia entre este archivo y LILA_MODELER_ESTRUCTURA.md, gana el documento de estructura y este archivo se corrige para reflejarlo.
  • ADR-024 en adelante: decisiones surgidas durante la implementación, con su ticket de prueba.

Varias ADR del corpus previo quedan cerradas o reinterpretadas por decisiones posteriores; cada una lo indica en una nota al final de su entrada, con el ADR que la cierra o reinterpreta.


ADR-001 — BPMN as interchange standard

Status: Accepted

Usaremos archivos .bpmn estándar para importación y exportación.

El XML BPMN no será necesariamente nuestra única base de datos interna.

Reason

  • Interoperabilidad.
  • Evitar lock-in.
  • Compatibilidad con herramientas existentes.
  • Permitir que el archivo sobreviva al producto.

ADR-002 — Web-first platform

Status: Accepted

La aplicación principal será web.

Debe poder ejecutarse:

  • Local.
  • Self-hosted.
  • Servidor.
  • Cloud.

Desktop wrapper será opcional.

Nota: ADR-023 concreta esto: la SPA de apps/web es la única UI, empaquetada como app de escritorio (Electron, apps/desktop) y servida sin cambios por el servidor self-hosted (packages/server, M6). "Desktop wrapper opcional" de este ADR es exactamente lo que ADR-023 diseña.


ADR-003 — API-first and agent-first

Status: Accepted

Las operaciones de dominio deberán exponerse mediante servicios reutilizables.

UI y MCP utilizarán esos servicios.

Nota: ADR-019 concreta esto: la API son las funciones exportadas de @lila-modeler/engine (parseBpmn, validate, resolveScenario, simulate, compare) y la CLI; MCP llega en M4 sobre las mismas funciones, antes que REST.


ADR-004 — Simulation before full BPM suite

Status: Accepted

El primer producto funcional será el motor de simulación.

RACI, repository, interviews y process mining vendrán después.

Nota: Confirmado por el orden de hitos de LILA_MODELER_ESTRUCTURA.md sección 7: M0–M3 son el motor y la CLI con paridad Bizagi; RACI (packages/catalog), packages/interview y mining/ son módulos futuros explícitamente pospuestos (sección 5, "Módulos futuros").


ADR-005 — Evaluate before fork

Status: Accepted

No reescribir ni forkear por reflejo.

Proceso:

Discover
↓
Evaluate
↓
PoC
↓
Decide:
Use / Wrap / Fork / Replace

Nota: Cerrado por ADR-010: resultado Replace para motores DES existentes (Prosimos, Scylla, BIMP/QBP, Apromore, bpmn-engine, Camunda 8, SpiffWorkflow — motor propio) y Use para bpmn-js/bpmn-moddle (ADR-011). Prosimos y Scylla se conservan como oráculos numéricos en desarrollo, no como núcleo.


ADR-006 — Own the abstraction layer

Status: Accepted (reinterpretado, ver nota)

Incluso si utilizamos un motor externo, la plataforma hablará con una interfaz propia.

Esto permite reemplazar componentes en el futuro.

Nota — reinterpretación (LILA_MODELER_ESTRUCTURA.md sección 4): "Own the abstraction layer" no significa una interfaz SimulationScheduler con adaptadores a SimPy/Rust (abstracción especulativa que ningún proposal defendió): significa poseer los tres contratos JSON (IR, escenario, resultado + event log) y la firma simulate(). Es lo que permite reemplazar el kernel sin tocar CLI, UI ni MCP.


ADR-007 — Scenario separated from diagram

Status: Accepted

Los parámetros de simulación deberán poder existir como escenarios independientes del BPMN.

Esto permite múltiples escenarios sobre un mismo proceso.

Nota: Concretado por ADR-013 (dónde vive cada dato) y ADR-015 (formato de escenario v1): parámetros de simulación en *.scenario.json, fuera del .bpmn; el .bpmn solo lleva diagrama, documentación estándar y la extensión lila: (ver docs/BPMN_EXTENSION.md).


ADR-008 — No final product name yet

Status: Closed (ver nota)

Usar Open Process Platform como working title hasta definir:

  • Posicionamiento.
  • Marca.
  • Dominio.
  • Licencia.
  • Público objetivo.

Nota: Cerrado por ADR-020 ("Cierra ADR-008: el proyecto se llama Lila Modeler."), decidido por Brito el 2026-09-03. Licencia cerrada en el mismo ADR-020 (Apache-2.0).


ADR-009 — Lenguaje y kernel del motor: TypeScript, bucle de eventos propio, cero dependencias en core/

Status: Accepted

Porqué: un solo runtime cubre navegador (Worker), CLI, tests y MCP; benchmark medido hoy (100k casos: JS 143 ms, SimPy 1 273 ms, SimPy/Pyodide 2 798 ms más ~12 MB de runtime); ninguna librería DES en TS tiene adopción (0–35 estrellas, un autor cada una); los crates Rust no traen recursos/colas y el toolchain wasm se acaba de reorganizar. Descarta: Python/SimPy (segundo runtime, parser BPMN sin opción permisiva mantenida: SpiffWorkflow es LGPL), Rust/WASM (sin necesidad demostrada), cualquier librería DES. Revisar: solo si un spike muestra que TS no alcanza un objetivo real de rendimiento; entonces core/ se reimplementa detrás de la misma simulate().


ADR-010 — Motor existente: ninguno. Se escribe propio. Prosimos y Scylla solo como oráculos numéricos en desarrollo

Status: Accepted

Porqué: Prosimos no tiene archivo LICENSE en ninguna rama (todos los derechos reservados), embebe jars propietarios de QBP, exige Python < 3.12 y su main está parado desde 2025-01-30; Scylla es MIT pero Java + Swing + DESMO-J 2017; BIMP/QBP es cerrado; Apromore archivado 2025-08-29; bpmn-engine, Camunda 8 y SpiffWorkflow son motores de ejecución con semántica y licencias equivocadas. Cierra ADR-005 (evaluate before fork) con resultado Replace para motores y Use para bpmn-js/bpmn-moddle. Revisar: si Prosimos publica una licencia permisiva, reevaluar como oráculo en CI (no como núcleo).


ADR-011 — Editor: bpmn-js tal cual, con la marca de agua bpmn.io visible; panel de propiedades propio en React

Status: Accepted

Porqué: bpmn-js 18.27.1 (publicado 2026-09-03, 27 releases en 2026) es la única librería del sector viva, con tipos y ejemplos oficiales de moddle extensions, properties providers y renderers; "nueva tarea → nombre editable sin Task 1" viene de serie. La licencia es MIT más una cláusula: la marca de agua no se puede quitar, ocultar ni tapar, tampoco en un fork (Camunda Desktop Modeler, Fluxnova y Miragon la llevan); reemplazarla por diagram-js son ~28 000 líneas. Los paquetes oficiales del panel de propiedades no publican tipos y arrastran dependencias Camunda. Descarta: fork, KIE bpmn-editor (una sola release, React ≤ 18, semántica jBPM; revisitar en 12 meses), LogicFlow, Camunda Web Modeler (propietario). Consecuencia de diseño: reservar la esquina inferior derecha del canvas.


ADR-012 — Identidad de elemento y de proceso

Status: Accepted

Elemento = atributo id BPMN, NCName, generado con prefijo por tipo y sufijo aleatorio (Task_7f3k2q1), nunca regenerado al importar/exportar/renombrar, nuevo al copiar; ids ajenos no-NCName (Bizagi puede emitirlos) se sanitizan con un mapa reversible. Proceso = bpmn:process@id como clave lógica (slug ASCII) + lila:versionTag; exporter="Lila Modeler" y exporterVersion en definitions. Porqué: es la única clave que comparten todas las herramientas (Signavio sid-uuid, bpmn-js Activity_x, jBPM _uuid) y de la que cuelgan BPSim, qbp, Prosimos y Scylla; usar nombres como clave es la decisión que fuerza reescrituras. Patrón Flowable (definition key + version) / Bonita / Camunda (process id + versionTag).

Implementación operativa completa en docs/BPMN_EXTENSION.md.


ADR-013 — Dónde vive cada dato

Status: Accepted

Diagrama y documentación por elemento dentro del .bpmn (bpmn:documentation estándar + lila: en extensionElements); parámetros de simulación fuera, en *.scenario.json; catálogo (roles, sistemas, documentos, riesgos, controles, KPIs) en catalog.json con ids estables; resultados en JSON + CSV. Los elementos referencian el catálogo (lila:roleRef), nunca contienen texto libre: es lo que hacen ADONIS, Signavio (dictionary) y ARIS (definición/ocurrencia), y es la otra decisión que forzaría reescritura si se toma mal. Confirma ADR-001 y ADR-007.


ADR-014 — Un solo namespace de extensión, definido una sola vez

Status: Accepted

xmlns:lila="https://lila-modeler.org/schema/bpmn/1" (el IRI no necesita resolver; debe ser estable y decidirse antes de M4). Descriptor moddle JSON en packages/engine/src/bpmn/lila.moddle.json, compartido por editor, CLI y servidor. Elementos (no atributos) para permitir listas. Crece de forma aditiva. bpmn-moddle preserva namespaces desconocidos en round-trip (verificado hoy con archivos BPSim, qbp y Bizagi reales); la preservación por Camunda/Signavio/ADONIS se prueba en M4 y el plan B es annotations.json sidecar keyed por id con el mismo descriptor.

Implementación operativa completa en docs/BPMN_EXTENSION.md.


ADR-015 — Formato de escenario v1: JSON propio, separado del .bpmn, keyed por id, vocabulario BPSim 2.0, parámetros nombrados, segundos, extends

Status: Accepted

Porqué: cumple los cuatro criterios a la vez (N escenarios por diagrama; diffs de git sin ruido de coordenadas DI; parcheable por agentes como elements["Task_1"].processingTime.mean = 400; migra 1:1 a jsonb). BPSim está congelado desde 2016 sin tooling open source y, embebido, mezcla layout con parámetros; qbp admite un solo escenario y un recurso por tarea; Prosimos usa parámetros posicionales de scipy y no tiene licencia; Bizagi no exporta parámetros. Descarta: BPSim o qbp como formato canónico. Adaptadores en los bordes cuando aparezca un consumidor: import qbp (~150 líneas, 182 archivos en GitHub, fixtures de Prosimos/Simod), export/import BPSim 2.0 como archivo .bpsim cuando haya usuario de Sparx EA, export Prosimos solo para oráculos.


ADR-016 — Semántica de calendarios (Bizagi no documenta la suya)

Status: Accepted

Patrón semanal relativo a run.start; sin DST ni festivos en v1 (campos reservados). Una tarea solo arranca dentro del calendario de su recurso y su processingTime consume solo tiempo de calendario (se pausa al cerrar el turno y reanuda al abrir). El tiempo cerrado se reporta como offHoursWait, separado de resourceWait. Utilización = tiempo ocupado / Σᵢ (capacidadᵢ × tiempo abiertoᵢ), sumando los tramos de capacidad del pool sobre la ventana de medida [warmup, t_stop] — no la duración declarada del escenario, que es lo que usa Bizagi en su nivel 4 (conversión exacta en docs/BIZAGI_PARITY.md § D7). Es la única definición que hace comparables niveles 3 y 4. Documentado en docs/SEMANTICS.md; ajustable si alguien aporta el comportamiento real de L-Sim/Bizagi.

LILA-164 (R-CAL-11): un mismo pool puede tener capacidad distinta por calendario — capacity: [{ calendar, capacity }], el «Resources → Calendars → quantity» de Bizagi — en vez de partirse en un pool por turno. El pool está abierto por unión de sus calendarios (tres turnos que cubren las 24 h son un 24×7, sin offHoursWait), su capacidad en t es la suma de los tramos abiertos (dos calendarios que se solapan suman), cerrar un tramo no interrumpe lo que está en curso, y durante el cierre del pool entero vale la capacidad del primer instante abierto posterior, que es lo que conserva R-CAL-6 y deja la forma numérica bit a bit igual a M3 (R-DEG-2). Sin esto el nivel 4 de Bizagi no se puede replicar.

#82 (R-CAL-12 … R-CAL-15): holidays deja de ser reservado, y un intervalo puede repetirse cada mes (monthDays, monthWeekdays) o cada año (dates) además de cada semana. Todo se sigue leyendo en el offset fijo de run.start, como aritmética de días civiles en core/ sin Date, Intl ni datos de zonas horarias; un calendario solo con days y sin festivos conserva el camino de código semanal bit a bit (R-DEG-2). timezone (zona propia con DST) sigue reservado: hacerlo bien exige la base IANA dentro de core/, y el límite queda escrito como R-CAL-15 en vez de aproximarse.

Fórmulas y definiciones operativas completas en docs/RESULTS_FORMAT.md.


ADR-017 — Determinismo

Status: Accepted

Heap ordenado por (t, seq) con seq monótono; PRNG propio sembrado (mulberry32/xoshiro) con un stream por elemento derivado de hash(seed, replicación, elementId): cinco líneas que dan common random numbers, es decir, añadir un cajero no cambia los números de las tareas no tocadas y los what-if se leen limpios. Nunca Math.random ni Date. Garantía: bytes idénticos dentro de un mismo runtime (test en Node 22 y 24); entre navegadores solo estadísticamente idénticos (Math.log/exp pueden diferir en el último bit).


ADR-018 — Persistencia de la modalidad instalable: archivos del usuario; nada de servidor

Status: Accepted

Un proyecto es una carpeta (model.bpmn + *.scenario.json). En la app de escritorio: abrir y guardar con los diálogos nativos del sistema (dialog.showOpenDialog / showSaveDialog y fs en el proceso principal de Electron, expuestos al renderer por preload + IPC con una API mínima: openFile, saveFile, readProject, recentFiles); recientes y estado de ventana en app.getPath('userData'). Sin SQLite, sin IndexedDB, sin cuentas, sin servidor. La demo online (GitHub Pages) usa <input type=file> y descarga: sirve para probar sin instalar, no es una modalidad. Porqué: un estudiante o analista trabaja con archivos; git da versionado AS-IS/TO-BE y diff gratis para quien lo use; los bytes son los mismos que guardará la modalidad servidor. Revisar: nunca por sí sola; la modalidad servidor (ADR-023) es la respuesta al trabajo compartido, no una evolución de esta.


ADR-019 — MCP antes que REST; ambos sobre las mismas funciones

Status: Accepted

La API del MVP son las funciones exportadas de @lila-modeler/engine (parseBpmn, validate, resolveScenario, simulate, compare) y la CLI. packages/mcp (stdio, @modelcontextprotocol/server 2.0.0, spec 2026-07-28) llega en M4, justo después de la paridad en CLI y antes de la UI, con 5 tools: cuesta ~100 líneas, no depende de la UI y Brito trabaja con agentes; desde ahí un agente valida, simula, parchea escenarios y compara en su computadora. REST (hono/fastify, una pantalla) llega con el servidor y el repositorio. Ninguna lógica vive en el borde. Cumple ADR-003 (UI y agentes hacen lo mismo) sin construir un servidor que hoy no sirve a nadie.


ADR-020 — Licencias

Status: Accepted

Núcleo, CLI, MCP y web bajo Apache-2.0 (decidido por Brito el 2026-09-03: cláusula de patentes explícita y adopción empresarial; compatible con la licencia bpmn.io, MIT de bpmn-moddle y Apache-2.0 de Simod). Prohibido AGPL/LGPL/Camunda License en packages/* (pm4py es AGPL-3.0 desde 2.7.12; SpiffWorkflow LGPL; Camunda 8 licencia propia). Prosimos y el jar de QBP jamás entran al repositorio ni a CI pública. La marca de agua de bpmn.io se acepta y se anuncia en el README. Cierra ADR-008: el proyecto se llama Lila Modeler.


ADR-021 — Alcance BPMN y política de "no soportado"

Status: Accepted

Soportado en v1: la lista de la sección 3 de LILA_MODELER_ESTRUCTURA.md (ver docs/BIZAGI_PARITY.md). Todo lo demás produce error de validación explícito con el mismo texto que Bizagi ("no soportado por el simulador"), nunca un fallo silencioso. Cada elemento extra se añade cuando lo pida un usuario real.


ADR-022 — Estructura del repositorio: un paquete que se publica, una app, y nada especulativo

Status: Accepted

packages/engine (con core/ puro como subcarpeta), apps/web, packages/mcp en M4. npm workspaces (viene con Node; sin pnpm/turbo/nx). Sin paquetes shared, types ni utils. Porqué: un paquete por cosa que se publica; el aislamiento de core/ se garantiza con un test que comprueba que el bundle del Worker no incluye bpmn-moddle, React ni node:*, no con un paquete aparte. Se divide en más paquetes cuando publicar por separado importe.


ADR-023 — Dos modalidades de despliegue, una sola SPA, un solo motor

Status: Accepted

(1) App de escritorio: la SPA de apps/web empaquetada con Electron en apps/desktop (proceso principal + preload), construida con electron-builder para macOS (dmg), Windows (nsis) y Linux (AppImage y deb) desde una matriz de CI; fileAssociations para abrir .bpmn con doble clic; auto-update opcional cuando haya releases frecuentes. El motor corre en el Web Worker del renderer; la persistencia es ADR-018. Precedente directo: Camunda Desktop Modeler (MIT) es Electron + bpmn-js + electron-builder con asociación de .bpmn; su electron-builder.json es la plantilla.

(2) Servidor self-hosted: packages/server (M6) en Node sirve la misma SPA compilada, expone REST y MCP por HTTP sobre las funciones de @lila-modeler/engine, autentica usuarios, guarda procesos/versiones/escenarios/runs en SQLite o PostgreSQL, y se distribuye como imagen Docker con docker-compose.yml. La simulación interactiva sigue corriendo en el Worker del navegador de cada usuario; el servidor solo simula cuando lo piden agentes, la CLI remota o corridas programadas.

La costura entre ambas es una interfaz ProjectStore en la SPA (listar/leer/escribir procesos, escenarios y runs) con implementaciones DesktopStore (IPC → fs), RemoteStore (REST, M6) y un BrowserStore mínimo (input/descarga) para la demo online. Se define en M5; la remota llega en M6 sin tocar vistas ni motor.

Porqué Electron y no Tauri: Tauri 2 produce instaladores de ~10 MB frente a ~150 MB y usa menos memoria, pero depende del webview del sistema, y en Linux (WebKitGTK) hay problemas de rendimiento y estabilidad documentados (reportes de 40 fps frente a 240 fps en Chromium para la misma app; hilo "WebKit is totally unstable" en las discusiones de Tauri); bpmn-js es un canvas SVG intensivo donde la consistencia de Chromium en los tres sistemas vale más que el tamaño; y Tauri exige toolchain Rust. Electron 43, electron-builder 26 y electron-forge (ESM, Node ≥ 22.12) están activos en 2026. Descarta: PWA como modalidad principal (sin diálogos nativos en Safari/Firefox, sin asociación de archivos). Reversible: si el tamaño del instalador se vuelve problema real, Tauri envuelve la misma SPA y solo cambia DesktopStore.

Porqué TypeScript sale reforzado: el mismo bundle corre en el Worker de la app de escritorio y en el Node del servidor; un motor Python habría exigido empaquetar un runtime Python dentro del instalador o cargar Pyodide (~12 MB).

Nota: enmendada por ADR-031 (2026-10-05). En Windows el canal principal pasa a ser la app web instalada como PWA desde Chrome o Edge; la objeción de arriba a «PWA como modalidad principal» es sobre Safari y Firefox, y ahí sigue en pie.


ADR-024 — Agregado top-level de varias replicaciones

Status: Accepted

simulate() publica en los campos numéricos top-level la media aritmética del mismo campo ya agregado en cada replicación. replications.kpis conserva, para esos mismos paths, media, desviación muestral e IC95. Esto deja un resultado directamente consumible por CLI/UI sin obligar a navegar el mapa de KPI y mantiene una observación estadística por replicación.

Se descarta usar la primera replicación: sería determinista pero no representativa y podría contradecir el mean publicado al lado. Se descarta agrupar todos los casos de todas las replicaciones: daría más peso a las corridas con más observaciones y rompería la unidad estadística con la que se calcula el IC. En cancelación, el top-level incluye el trabajo de la réplica parcial para no ocultarlo; el IC usa solo replicaciones completas y se omite con menos de dos.

Revisar solo si un consumidor necesita explícitamente resultados por réplica; en ese caso se añade un campo separado, sin cambiar el significado del top-level. (prueba: LILA-029)

Adenda (2026-09-22): replicaciones sin observaciones (#356)

Decidido por: el dueño del proyecto.

Una replicación en la que una tarea nunca se completó tiene processing = {min: 0, max: 0, mean: 0, total: 0}: el 0 es la identidad del conjunto vacío, no una duración. Promediarlo como observación hacía que el top-level y replications.kpis publicaran un tiempo que ninguna instancia tardó (una tarea constante de 360 s alcanzada en 18 de 30 replicaciones publicaba 216 s, con un IC que excluía 360 s) y daba una dispersión que medía si la tarea ocurrió, no cuánto tardó.

El estimando pasa a ser condicional: para un KPI que solo existe cuando algo ocurrió —las duraciones de una tarea o de un timer que no es de borde, el tiempo de ciclo y de espera, el costo por caso y el nivel de servicio del proceso y de cada desenlace— la media se toma sobre los valores por replicación de las replicaciones que lo observaron, siempre una observación por replicación, nunca un pool de los casos. KpiSummary lleva n, el número de replicaciones que aportan; sd y ci95 solo aparecen con n ≥ 2, n = 1 publica {mean, n} y n = 0 publica {mean: 0, n: 0} para que el conjunto de claves siga estable. La misma regla vale para una sd dentro de la replicación calculada con una sola observación, que vale 0 por convención. Los KPI incondicionales (conteos, totales, costos, flujos, recursos, integrales de cola) conservan n = count. El top-level usa la misma selección mediante un único recorrido de paths, así que R-ARR-9 se cumple por construcción. Una corrida en la que algunas replicaciones observaron un sujeto y otras no lleva W-REPLICACIONES-SIN-OBSERVACIONES.

Precedente: meanBottlenecks ya promediaba bottlenecks[].utilization solo sobre las replicaciones donde el elemento aparecía, por la misma razón (RESULTS_FORMAT.md § 6); esa regla y saturationWarnings no cambian.

Se descarta ponderar por el número de casos (el estimando agrupado que ADR-024 ya rechaza) y mantener los ceros añadiendo solo un aviso (el número publicado seguiría mal). Consecuencia: es un cambio revisado del contrato público —la forma de KpiSummary y un código de aviso nuevo—. Los nombres de columna, los ids BPMN, las unidades y la frontera de core/ no cambian; los resultados guardados antes de 1.0.0-beta.1 no traen n, se calcularon con la definición anterior y se siguen aceptando. (prueba: #356)


ADR-025 — Event log plano por asignación, agrupado por instancia de actividad

Status: Accepted

Cada asignación de pool produce una fila plana y todas las filas de una ocurrencia comparten activityInstanceId y su posición original en allocationIndex. Una actividad sin recurso, o una que se cierra todavía en cola, produce una fila sentinel (resourceId = null, resourceQuantity = null, allocationIndex = null). El lifecycle parcial distingue terminated de inFlight, conserva timestamps anulables y observedUntil. Los costos se descomponen en elementCost y resourceCost; el fijo del elemento aparece una sola vez en la fila emitida de menor allocationIndex y cost es su suma exacta.

Se descarta una fila por actividad con resources[]: duplica estructura dentro del CSV, dificulta streaming/XES y contradice el formato plano comprometido. Se descartan acumuladores internos sin reconstrucción desde el log: violan R-COST-3 y no permiten auditar utilización/costos sin volver a simular. La alternativa elegida conserva CSV plano, representa quantity y AND/OR, evita duplicar el costo fijo y mantiene observables las tareas en cola o en curso al cortar.

Revisar solo con una nueva versión del formato de resultados; consumidores v1 dependen de estas columnas. (prueba: LILA-033, LILA-036, LILA-037)


ADR-026 — Scheduler AND por firmas y heap de cabezas elegibles

Status: Accepted

Las solicitudes AND con la misma firma ordenada de (pool, quantity) comparten una cola FIFO. Cada pool indexa solo las firmas que lo usan; al cambiar su capacidad se reevalúan esas cabezas. Un heap separado, comparado explícitamente por el (enabledAt, seq) original, elige el primer candidato satisfacible y reserva todos sus pools en una sola mutación. Las solicitudes single-pool comparten una única clase por pool, incluso con cantidades distintas, para conservar FIFO estricto.

Se descarta recorrer y reinsertar toda la cola global en cada llegada: bajo saturación produce O(n² log n). También se descarta crear una clase single-pool por cantidad, porque permitiría que una solicitud pequeña adelantase a la cabeza del mismo pool. Versiones/tombstones invalidan cabezas sin búsquedas lineales y release/cancel reúnen todos los pools afectados antes de planificar.

LILA-035 usa esa estructura sin añadir nada nuevo: una selección OR se encola como una entrada por alternativa, cada una en la clase single-pool de su pool y todas con el mismo seq, así que comparten posición FIFO y compiten en igualdad con las solicitudes de un solo pool. Conceder una alternativa deja a las demás como lápidas — el mismo mecanismo que ya invalida cabezas — y marca sus clases para reevaluar en el acto, que es lo que impide que la cabeza siguiente de un pool retirado espere a un evento que ya no va a llegar. El desempate entre alternativas libres a la vez es el índice declarado en el escenario (R-REC-6): (enabledAt, seq, altIndex) sigue siendo un orden total y no altera el de AND/single, donde altIndex siempre vale 0. Se descarta elegir el pool más libre o el de menor utilización: obligaría a un criterio global y a reordenar colas, y no hay paridad Bizagi que lo pida. ResourceManager sigue sin ser API pública. (prueba: LILA-034, LILA-035)


ADR-027 — .lila: la carpeta de proyecto, zipeada

Estado: Aceptada

Un archivo .lila es la carpeta de proyecto de ADR-018 comprimida en ZIP, con la misma disposición y los mismos nombres de archivo (lila-project.json, model.bpmn, <nombre>.scenario.json, runs/<id>.result.json). Un zip -r de una carpeta de proyecto ya existente es un .lila válido, y un unzip de un .lila es una carpeta de proyecto válida; no hay conversión ni migración. El manifiesto gana un único campo opcional, engine, que nombra la versión del motor que escribió las corridas. MIME application/vnd.lila-modeler+zip; ver docs/PROJECT_FORMAT.md.

Por qué un contenedor: la carpeta es lo correcto para git y lo equivocado para entregarle el trabajo a alguien — la demo en línea solo podía descargar un .lila.json que ninguna instalación de escritorio sabía abrir, y «mándame tu modelo» significaba un zip hecho a mano sin garantía de qué llevaba dentro. Por qué la carpeta zipeada y no un formato nuevo: la carpeta ya es el contrato que escriben todas las modalidades, así que el contenedor cuesta un lector y un escritor en vez de una segunda definición de qué es un proyecto, y cualquiera puede inspeccionar o reparar un .lila con la herramienta de descompresión que ya trae su sistema.

fflate es la única dependencia nueva (MIT, ~30 KB, sin dependencias transitivas, funciona igual en Node y en el worker del navegador) — un DEFLATE hecho a mano no es algo que este proyecto deba mantener, y las alternativas o asumen Node (node:zlib, inservible en la SPA) o arrastran un árbol de paquetes. Los tipos de documento compartidos se mudaron a @lila-modeler/engine/project, con lo que termina la copia a mano que apps/desktop/src/projectTypes.ts mantenía de la de apps/web.

Las entradas desconocidas dentro de un .lila (notes.md, attachments/…) se reportan en problems y se descartan, no se conservan: guardarlas metería bytes opacos dentro de ProjectDocument, que cruza structuredClone por IPC y JSON.stringify hacia el espejo en localStorage del navegador. La forma de carpeta sí las deja en paz. Se revisará si alguien quiere adjuntos de verdad — eso es una decisión de formato (un área attachments/ declarada, con su entrada en el manifiesto), no un truco de serialización. (pruebas: packages/engine/test/project/lila.test.ts, apps/desktop/src/lilaFile.test.ts)


ADR-028 — Ruteo condicionado al desenlace previo del caso (conditions)

Estado: Aceptada

Un sequence flow que sale de un XOR divergente puede declarar conditions: [{ flowTaken, probability }]. Cuando un token llega a la compuerta, la probabilidad declarada de cada salida es la probability de la primera entrada cuyo flowTaken el caso ya recorrió, y la probability a secas si no coincide ninguna; de ahí en adelante R-XOR-1…5 se aplican sin cambios a ese vector por caso, y la compuerta sigue sacando un uniforme de su propio stream (SEMANTICS.md § 6.1, R-COND-1…5). En cualquier elemento que no sea ese flujo, conditions sigue siendo el campo reservado de § 15, con el mismo texto E-RESERVADO de antes.

Contexto: v1 rutea solo por probabilidad, así que un modelo que fusiona dos ramas no puede volver a distinguirlas aguas abajo. El ejemplo de la solicitud de servicio (packages/engine/test/fixtures/service-request) duplica la ruta de rechazo entera —negar e informar, dos veces— sin más motivo que mantener contables las dos causas de rechazo. Esa duplicación es lo primero con lo que se topa cualquier usuario de Bizagi y es el hueco más barato de cerrar de la épica #335.

Por qué no variables de caso: un lenguaje de expresiones sobre datos del caso implica un modelo de datos, un evaluador, un sistema de tipos y un depurador para él, y dejaría sin cota el estado del motor por caso. Los flujos que un caso recorrió son estado que el motor ya tiene, se nombran con ids que el escenario ya usa y cubren la duplicación que motivó el ticket. Bizagi mismo solo ofrece esto en su nivel 4 mediante condiciones de compuerta sobre atributos; esto es el 10 % de aquello que quita el 90 % de la duplicación.

Techos, todos deliberados:

  • Un conjunto, no un historial. flowsTaken tiene semántica de conjunto: un bucle no lo reinicia y la segunda pasada por un flujo es indistinguible de la primera (R-COND-3). Rutear por cuántas veces pasó algo no se puede expresar.
  • El lint no puede anticipar del todo la corrida. La sustitución de probabilidades ocurre por caso, así que la comprobación estática de R10 no puede predecir W-XOR-NORMALIZADA ni E-XOR-SUMA-CERO para un vector sustituido. El motor emite el aviso de normalización en tiempo de corrida, una vez por firma, y un vector sustituido que sume 0 cae en el descarte de R-XOR-7 (el último flujo con p > 0, o el último flujo) en vez de abortar.
  • W-COND-INALCANZABLE es alcanzabilidad, no concurrencia. Recorre el IR hacia atrás desde la compuerta por incoming. Un flujo de una rama paralela de un AND fork no es alcanzable hacia atrás y avisa, aunque un token sí pudiera haberlo recorrido. Es un aviso justamente porque el análisis es el barato; la corrida se comporta bien en cualquier caso.
  • extends reemplaza el array entero. deepMerge reemplaza arrays, así que un delta que declara conditions sustituye la lista heredada; no hay mezcla entrada por entrada.
  • Sin UI para elegir el id del flujo. El panel de escenario dibuja flowTaken como una entrada de texto, no como un select de los flujos del modelo: un id equivocado sale como E-REF-DESCONOCIDA bajo su propia ruta.

(pruebas: packages/engine/test/conditions.test.ts, fixture packages/engine/test/fixtures/service-shared-denial.bpmn)


ADR-029 — El repositorio: un .lila puede contener una organización entera

Estado: Aceptada (dirección, decidida por Brito el 2026-09-26). Mínimo implementado (#498, #461): un proyecto con más de un proceso es un repositorio versión 2 con cada proceso en processes/<slug>/, las pestañas del lienzo son sus procesos y una actividad de llamada abre el proceso al que llama. El primer ticket fijó dos puntos del layout de abajo: el manifiesto conserva su nombre, lila-project.json, con "version": 2 (un lila-repository.json aparte lo leería una versión 1 como «sin manifiesto» en vez de como una versión más nueva), y la versión 2 solo se escribe cuando el proyecto tiene un segundo proceso: un proyecto de un proceso sigue en la versión 1, byte a byte. process.json, los mapas, los análisis, el catálogo, los tableros, los registros y el ProjectStore por objeto todavía no existen. La descripción operativa está en docs/es/PROJECT_FORMAT.md § Versión 2.

Lila Modeler crece de alternativa a Bizagi a plataforma de modelado, análisis, diseño, simulación y calidad al estilo de ADONIS: mapas de procesos desde los macroprocesos, RACI, riesgos y controles, Ishikawa y otros análisis, dashboards, gestión de calidad. La automatización queda fuera: motores como n8n consumen el BPMN estándar y la API/MCP. La única suposición de ADR-018 y ADR-027 que no sobrevive es un proyecto es un modelo. La sustituye el repositorio: una carpeta (en zip, un .lila) que puede contener un proceso o una organización entera.

mi-empresa/                          carpeta (git) · zip = mi-empresa.lila
  lila-repository.json               manifiesto, version 2
  maps/<slug>.map.json               mapas de procesos; cada nodo apunta a un modelo por id
  processes/<slug>/                  exactamente el layout de proyecto de la versión 1:
    model.bpmn                         un proceso por .bpmn (pools solo para colaboraciones)
    <nombre>.scenario.json
    runs/<id>.result.json
    process.json                       dueño, estado Borrador/Liberado, vigencia, padre
  analysis/<slug>.<tipo>.json        Ishikawa y otros análisis
  catalog/<tipo>/<id>.json           roles, unidades, sistemas, documentos, riesgos, controles, KPIs, requisitos
  dashboards/<slug>.dashboard.json   solo la definición; las cifras se calculan
  records/<tipo>/<id>.json           registros de calidad opcionales (ver abajo)

Reglas:

  • Un proyecto de la versión 1 es un repositorio con un proceso. processes/<slug>/ es el layout de ADR-027 byte a byte, así que un .lila o carpeta de la versión 1 se abre como repositorio de un proceso y nada se migra. El lector de la versión 2 acepta la 1; el de la versión 1 rechaza la 2, como ya exige docs/PROJECT_FORMAT.md § Versioning.
  • Un proceso, un .bpmn. Cada proceso tiene su dueño, versión y liberación, así que es su propio archivo. Varios bpmn:process en un archivo solo en diagramas de colaboración. Las pestañas del lienzo son los modelos abiertos del repositorio; + crea un modelo en el mismo repositorio.
  • Referencias por id, nunca por ruta. Las carpetas se pueden renombrar o mover sin romper enlaces. El índice id → archivo se deriva al abrir y nunca se guarda.
  • Un archivo por objeto del catálogo. Mantiene separados los merges de git y las ediciones concurrentes, y corresponde uno a uno con una fila del servidor. Completa el catalog.json de ADR-013.
  • BPMN es el único formato estándar. Mapas, análisis, organigramas y dashboards no tienen un estándar de intercambio útil, así que son JSON propio con esquema versionado (ADR-001: BPMN para intercambiar, no necesariamente el único almacenamiento).
  • La carpeta es la exportación sin pérdida del servidor. La modalidad servidor (ADR-023) guarda los mismos objetos como filas; ir de carpeta a servidor y de vuelta no pierde nada. La colaboración en tiempo real llega con el servidor (un CRDT sobre estos objetos) y no cambia el formato.
  • Las corridas son opcionales en un .lila compartido. Son lo único pesado; el modelo es texto y se comprime bien.

Registros de calidad — auditorías, hallazgos, no conformidades, acciones correctivas (CAPA) — son datos operativos: cambian a diario, los tocan muchas personas y necesitan asignación, fechas límite, avisos e historial. Su lugar es la modalidad servidor. Quien no tenga servidor (un consultor que trabaja solo, por ejemplo) puede guardarlos en el repositorio bajo records/, por elección propia y sin recomendarlo: la interfaz lo advierte, y los archivos no protegen contra dos personas editando dos copias. Los registros apuntan a objetos del diseño por id (NC-034 → control, proceso), que es lo que permite a un dashboard contar no conformidades por control.

Consecuencia para la app: el ProjectDocument completo deja de cruzar el IPC y el espejo de localStorage de una vez. ProjectStore pasa a listar/leer/escribir por objeto con carga perezosa; la demo en línea puede mantener un repositorio pequeño en memoria.

No se decide aquí, y no se construye hasta que se pida uno por uno: los esquemas de mapas, análisis, dashboards y registros, el flujo de liberación, el CRDT del servidor.


ADR-030 — El servidor MCP es su propio paquete de npm, @lila-modeler/mcp

Estado: Aceptada (decidida por Brito el 2026-10-05).

El servidor MCP se publica como un paquete público aparte, @lila-modeler/mcp, con el bin lila-mcp, en lugar de meterlo en @lila-modeler/engine. Los agentes lo instalan como se instala cualquier servidor MCP: npx -y @lila-modeler/mcp. Antes, packages/mcp era private, así que el motor publicado traía el subcomando lila mcp pero no el servidor que carga, y npx -y @lila-modeler/engine mcp se detenía con «falta el paquete @lila-modeler/mcp».

  • El motor sigue ligero. Solo depende de bpmn-moddle, bpmn-auto-layout, fflate y zod, y también lo consumen la app web y la de escritorio. Meter el servidor obligaría a todos ellos a instalar @modelcontextprotocol/server.
  • El paquete ya existía. packages/mcp tenía el bin, files, un script de build y la dependencia fijada al motor; publicarlo es quitar private y añadir los metadatos de npm.
  • Ritmo propio para el SDK del protocolo. El SDK de MCP (v2) cambia a su propio ritmo; subirlo no toca el árbol de dependencias del motor.
  • Misma versión, misma release. Los dos paquetes llevan la versión del lote (los seis lugares que revisa release.yml), y @lila-modeler/mcp fija la versión exacta del motor. release.yml publica primero el motor y después el servidor, con la misma dist-tag (beta para una prerelease, latest si no), --provenance y la misma guarda publish_npm. El paquete contiene solo dist, bin, README.md, package.json, y LICENSE y NOTICE como el motor.
  • lila mcp se queda. Sigue cargando @lila-modeler/mcp con un import() dinámico (sin ciclo entre paquetes), así que funciona donde estén instalados los dos, como en un clon del repo.

La primera publicación la hace el dueño a mano, y después se configura el publicador de confianza de npm (OIDC) para el paquete nuevo, como se hizo con el motor. (prueba: #569)


ADR-031 — En Windows el canal principal es la PWA en Chrome y Edge

Estado: Aceptada (decidida por Brito el 2026-10-05). Enmienda ADR-023.

Quien usa Windows corre la app web desde GitHub Pages (https://alambritodito.github.io/lila-modeler/app/) en Chrome o Edge y la instala como aplicación web progresiva (PWA). El .exe NSIS sin firmar queda como canal secundario; la firma (SignPath, #501) está en pausa. macOS conserva la app de Electron como canal principal y Linux el AppImage y el .deb.

  • Por qué la objeción de ADR-023 ya no aplica en Windows. ADR-023 descartó la PWA como modalidad principal porque Safari y Firefox no tienen diálogos de archivo nativos ni asociación de archivos. Chromium, que es lo que ya tienen los usuarios de Windows (Edge viene con el sistema), tiene las dos cosas: la File System Access API (showOpenFilePicker / showSaveFilePicker, un identificador de archivo sobre el que Guardar reescribe) y los file_handlers del manifiesto con window.launchQueue, que hacen de la PWA instalada la app que abre un .lila con doble clic.
  • Por qué no el .exe firmado. Un instalador sin firmar se topa con el «Windows protegió su PC» de SmartScreen y algunos equipos administrados lo rechazan sin más; un certificado de firma exige un historial de organización que el proyecto aún no tiene (#501). Instalar una PWA no pide permisos de administrador ni firma, y cada despliegue de Pages la actualiza.
  • Qué es. apps/web gana manifest.webmanifest (nombre, start_url y scope relativos para que el mismo archivo sirva en / y en /lila-modeler/app/, display: standalone, iconos de marca 192, 512 y maskable, file_handlers para .lila con launch_handler focus-existing, para que un doble clic vaya a la ventana ya abierta y a su aviso de cambios sin guardar), un service worker que guarda el esqueleto de la app en una caché con el nombre de la versión y un hash del build para que la app instalada abra sin conexión, y mejora progresiva en BrowserStore: con la File System Access API, Abrir conserva el identificador del archivo y Guardar escribe sobre él; sin ella (Safari, Firefox, un Chromium antiguo) Abrir es el selector de archivos y Guardar descarga, exactamente como antes. DesktopStore y Electron no cambian.
  • Qué no hace. No hay manejador de archivos para .bpmn (en el navegador un diagrama suelto se importa, no se abre como proyecto); no hay proyectos carpeta (ADR-018) en el navegador, solo el contenedor .lila (ADR-027); no hay actualización automática con la app abierta: una versión nueva entra la siguiente vez que se hayan cerrado todas las ventanas de la app.

Revisar: si la firma de Windows llega a estar disponible y los probadores reportan que el manejo de archivos de la PWA no basta, el .exe puede volver a ser el principal sin cambiar código; la SPA es la misma. (prueba: #576 — Page.getInstallabilityErrors vacío sobre el build de Pages; guía del probador #575)


Ver también

  • LILA_MODELER_ESTRUCTURA.md — documento de estructura completo (fuente de verdad de todas las ADR de este archivo).
  • docs/BIZAGI_PARITY.md — tabla de paridad referenciada por ADR-021.
  • docs/BPMN_EXTENSION.md — implementación operativa de ADR-012 y ADR-014.
  • docs/RESULTS_FORMAT.md — implementación operativa de la parte de calendarios/utilización de ADR-016.
  • docs/PROJECT_FORMAT.md — implementación operativa de ADR-018 y ADR-027 (la carpeta de proyecto y el contenedor .lila).
  • docs/DECISIONS-corpus-previo.md — texto original de ADR-001 a ADR-008 (corpus previo, proyecto entonces llamado Open Process Platform).
  • BACKLOG.md — desglose en épicas y tickets por hito.
✎ Editar esta páginaPara v1.0.0-beta.26