Architecture Decision Log
Lila Modeler's architecture decision log. These are reversible while the project is in its early stage — each one states, where it applies, when to revisit it.
Sources:
- ADR-001 through ADR-008: the project's earlier corpus (then called Open Process Platform),
docs/DECISIONS-corpus-previo.md. Copied here verbatim. - ADR-009 through ADR-023:
LILA_MODELER_ESTRUCTURA.md, section 4 ("Decisiones (ADR)"). Copied here verbatim; that document is the source of truth — for any discrepancy between this file andLILA_MODELER_ESTRUCTURA.md, the structure document wins and this file is corrected to match. - ADR-024 onward: decisions that came up during implementation, with their test ticket.
Several ADRs from the earlier corpus end up closed or reinterpreted by later decisions; each one says so in a note at the end of its entry, naming the ADR that closes or reinterprets it.
ADR-001 — BPMN as interchange standard
Status: Accepted
We will use standard .bpmn files for import and export.
BPMN XML will not necessarily be our only internal data store.
Reason
- Interoperability.
- Avoid lock-in.
- Compatibility with existing tools.
- Let the file outlive the product.
ADR-002 — Web-first platform
Status: Accepted
The main application will be web-based.
It must be able to run:
- Local.
- Self-hosted.
- Server.
- Cloud.
A desktop wrapper will be optional.
Note: ADR-023 makes this concrete: the
apps/webSPA is the only UI, packaged as a desktop app (Electron,apps/desktop) and served unchanged by the self-hosted server (packages/server, M6). This ADR's "optional desktop wrapper" is exactly what ADR-023 designs.
ADR-003 — API-first and agent-first
Status: Accepted
Domain operations must be exposed through reusable services.
The UI and MCP will use those services.
Note: ADR-019 makes this concrete: the API is the exported functions of
@lila-modeler/engine(parseBpmn,validate,resolveScenario,simulate,compare) and the CLI; MCP arrives in M4 on top of the same functions, ahead of REST.
ADR-004 — Simulation before full BPM suite
Status: Accepted
The first working product will be the simulation engine.
RACI, repository, interviews, and process mining come later.
Note: Confirmed by the milestone order in
LILA_MODELER_ESTRUCTURA.mdsection 7: M0-M3 are the engine and the CLI, matching Bizagi Modeler's published reference results; RACI (packages/catalog),packages/interview, andmining/are future modules explicitly deferred (section 5, "Future modules").
ADR-005 — Evaluate before fork
Status: Accepted
Do not rewrite or fork by reflex.
Process:
Discover
↓
Evaluate
↓
PoC
↓
Decide:
Use / Wrap / Fork / Replace
Note: Closed by ADR-010: Replace result for existing DES engines (Prosimos, Scylla, BIMP/QBP, Apromore, bpmn-engine, Camunda 8, SpiffWorkflow — build our own engine) and Use for bpmn-js/bpmn-moddle (ADR-011). Prosimos and Scylla are kept as numeric oracles under development, not as the core.
ADR-006 — Own the abstraction layer
Status: Accepted (reinterpreted, see note)
Even if we use an external engine, the platform will talk to its own interface.
This allows replacing components in the future.
Note — reinterpretation (
LILA_MODELER_ESTRUCTURA.mdsection 4): "Own the abstraction layer" does not mean aSimulationSchedulerinterface with adapters to SimPy/Rust (a speculative abstraction no proposal actually argued for): it means owning the three JSON contracts (IR, scenario, result + event log) and thesimulate()signature. That is what lets the kernel be replaced without touching the CLI, UI, or MCP.
ADR-007 — Scenario separated from diagram
Status: Accepted
Simulation parameters must be able to exist as scenarios independent of the BPMN.
This allows multiple scenarios over the same process.
Note: Made concrete by ADR-013 (where each piece of data lives) and ADR-015 (scenario format v1): simulation parameters live in
*.scenario.json, outside the.bpmn; the.bpmncarries only the diagram, standard documentation, and thelila:extension (seedocs/BPMN_EXTENSION.md).
ADR-008 — No final product name yet
Status: Closed (see note)
Use Open Process Platform as a working title until we define:
- Positioning.
- Brand.
- Domain.
- License.
- Target audience.
Note: Closed by ADR-020 ("Closes ADR-008: the project is called Lila Modeler."), decided by Brito on 2026-09-03. The license was also settled in ADR-020 (Apache-2.0).
ADR-009 — Engine language and kernel: TypeScript, our own event loop, zero dependencies in core/
Status: Accepted
Why: a single runtime covers the browser (Worker), CLI, tests, and MCP; a benchmark measured today (100k cases: JS 143 ms, SimPy 1,273 ms, SimPy/Pyodide 2,798 ms plus ~12 MB of runtime); no DES library in TS has real adoption (0-35 stars, one author each); Rust crates carry no resources/queues, and the wasm toolchain was just reorganized. Rules out: Python/SimPy (a second runtime, no BPMN parser with a maintained permissive license: SpiffWorkflow is LGPL), Rust/WASM (no demonstrated need), any DES library. Revisit: only if a spike shows TS cannot meet a real performance target; then core/ gets reimplemented behind the same simulate().
ADR-010 — Existing engine: none. We write our own. Prosimos and Scylla only as numeric oracles under development
Status: Accepted
Why: Prosimos has no LICENSE file on any branch (all rights reserved), embeds proprietary QBP jars, requires Python < 3.12, and its main has been stalled since 2025-01-30; Scylla is MIT but Java + Swing + DESMO-J from 2017; BIMP/QBP is closed source; Apromore was archived on 2025-08-29; bpmn-engine, Camunda 8, and SpiffWorkflow are execution engines with the wrong semantics and licenses. Closes ADR-005 (evaluate before fork) with a Replace result for engines and Use for bpmn-js/bpmn-moddle. Revisit: if Prosimos publishes a permissive license, re-evaluate it as a CI oracle (not as the core).
ADR-011 — Editor: bpmn-js as-is, with the bpmn.io watermark visible; our own properties panel in React
Status: Accepted
Why: bpmn-js 18.27.1 (published 2026-09-03, 27 releases in 2026) is the only actively maintained library in this space, with official types and examples for moddle extensions, properties providers, and renderers; "new task → editable name instead of Task 1" comes out of the box. The license is MIT plus one clause: the watermark cannot be removed, hidden, or covered, not even in a fork (Camunda Desktop Modeler, Fluxnova, and Miragon keep it); replacing it with diagram-js is ~28,000 lines. The official properties-panel packages ship no types and drag in Camunda dependencies. Rules out: forking, KIE bpmn-editor (a single release, React ≤ 18, jBPM semantics; revisit in 12 months), LogicFlow, Camunda Web Modeler (proprietary). Design consequence: reserve the canvas's bottom-right corner.
ADR-012 — Element and process identity
Status: Accepted
Element = BPMN id attribute, NCName, generated with a type prefix and random suffix (Task_7f3k2q1), never regenerated on import/export/rename, new on copy; foreign non-NCName ids (Bizagi can emit them) are sanitized with a reversible map. Process = bpmn:process@id as the logical key (ASCII slug) + lila:versionTag; exporter="Lila Modeler" and exporterVersion in definitions. Why: it is the only key every tool shares (Signavio sid-uuid, bpmn-js Activity_x, jBPM _uuid) and the one BPSim, qbp, Prosimos, and Scylla hang off of; using names as the key is the decision that forces rewrites. Pattern from Flowable (definition key + version) / Bonita / Camunda (process id + versionTag).
Full operational implementation in
docs/BPMN_EXTENSION.md.
ADR-013 — Where each piece of data lives
Status: Accepted
Diagram and per-element documentation inside the .bpmn (standard bpmn:documentation + lila: in extensionElements); simulation parameters outside, in *.scenario.json; catalog (roles, systems, documents, risks, controls, KPIs) in catalog.json with stable ids; results in JSON + CSV. Elements reference the catalog (lila:roleRef), never contain free text: that is what ADONIS, Signavio (dictionary), and ARIS (definition/occurrence) do, and it is the other decision that would force a rewrite if made wrong. Confirms ADR-001 and ADR-007.
ADR-014 — A single extension namespace, defined once
Status: Accepted
xmlns:lila="https://lila-modeler.org/schema/bpmn/1" (the IRI does not need to resolve; it must be stable and decided before M4). moddle JSON descriptor in packages/engine/src/bpmn/lila.moddle.json, shared by the editor, CLI, and server. Elements (not attributes) to allow lists. Grows additively. bpmn-moddle preserves unknown namespaces on round-trip (verified today with real BPSim, qbp, and Bizagi files); preservation by Camunda/Signavio/ADONIS is tested in M4, and the fallback plan is an annotations.json sidecar keyed by id with the same descriptor.
Full operational implementation in
docs/BPMN_EXTENSION.md.
ADR-015 — Scenario format v1: our own JSON, separate from the .bpmn, keyed by id, BPSim 2.0 vocabulary, named parameters, seconds, extends
Status: Accepted
Why: it satisfies all four criteria at once (N scenarios per diagram; git diffs without DI-coordinate noise; patchable by agents like elements["Task_1"].processingTime.mean = 400; migrates 1:1 to jsonb). BPSim has been frozen since 2016 with no open source tooling and, embedded, mixes layout with parameters; qbp allows only one scenario and one resource per task; Prosimos uses positional scipy parameters and has no license; Bizagi does not export parameters. Rules out: BPSim or qbp as the canonical format. Edge adapters when a consumer shows up: qbp import (~150 lines, 182 files on GitHub, Prosimos/Simod fixtures), BPSim 2.0 export/import as a .bpsim file when there is a Sparx EA user, Prosimos export for oracles only.
ADR-016 — Calendar semantics (Bizagi does not document its own)
Status: Accepted
A weekly pattern relative to run.start; no DST or holidays in v1 (reserved fields). A task only starts within its resource's calendar, and its processingTime consumes only calendar time (it pauses when the shift closes and resumes when it opens). Closed time is reported as offHoursWait, separate from resourceWait. Utilization = busy time / Σᵢ (capacityᵢ × openTimeᵢ), summing the pool's capacity segments over the measurement window [warmup, t_stop] — not the scenario's declared duration, which is what Bizagi uses at its level 4 (exact conversion in docs/BIZAGI_PARITY.md § D7). It is the only definition that makes levels 3 and 4 comparable. Documented in docs/SEMANTICS.md; adjustable if someone provides L-Sim/Bizagi's actual behaviour.
LILA-164 (R-CAL-11): a single pool can have different capacity per calendar — capacity: [{ calendar, capacity }], Bizagi's «Resources → Calendars → quantity» — instead of splitting into one pool per shift. The pool is open by the union of its calendars (three shifts covering 24 h are a 24×7, with no offHoursWait), its capacity at t is the sum of the open segments (two overlapping calendars add up), closing a segment does not interrupt what is already in progress, and while the whole pool is closed the capacity of the next open instant applies, which is what preserves R-CAL-6 and keeps the numeric shape bit-for-bit identical to M3 (R-DEG-2). Without this, Bizagi's level 4 cannot be reproduced.
#82 (R-CAL-12 … R-CAL-15): holidays stops being reserved, and an interval can repeat monthly (monthDays, monthWeekdays) or yearly (dates) as well as weekly. Everything is still read in run.start's fixed offset, as civil-day arithmetic in core/ with no Date, Intl or timezone data; a calendar with only days and no holidays keeps the weekly code path byte for byte (R-DEG-2). timezone (own zone with DST) stays reserved: doing it right needs the IANA database inside core/, and the limit is stated as R-CAL-15 instead of approximated.
Full operational formulas and definitions in
docs/RESULTS_FORMAT.md.
ADR-017 — Determinism
Status: Accepted
A heap ordered by (t, seq) with a monotonic seq; our own seeded PRNG (mulberry32/xoshiro) with one stream per element derived from hash(seed, replication, elementId): five lines that give common random numbers, meaning adding a cashier does not change the numbers for untouched tasks, and what-ifs read clean. Never Math.random or Date. Guarantee: byte-identical within the same runtime (tested on Node 22 and 24); across browsers, only statistically identical (Math.log/exp can differ in the last bit).
ADR-018 — Persistence for the installable mode: the user's own files; no server at all
Status: Accepted
A project is a folder (model.bpmn + *.scenario.json). In the desktop app: open and save with the system's native dialogs (dialog.showOpenDialog / showSaveDialog and fs in Electron's main process, exposed to the renderer via preload + IPC with a minimal API: openFile, saveFile, readProject, recentFiles); recents and window state in app.getPath('userData'). No SQLite, no IndexedDB, no accounts, no server. The online demo (GitHub Pages) uses <input type=file> and downloads: it is there to try without installing, not a mode of its own. Why: a student or analyst works with files; git gives AS-IS/TO-BE versioning and diff for free to whoever uses it; the bytes are the same ones the server mode will save. Revisit: never on its own; the server mode (ADR-023) is the answer to shared work, not an evolution of this one.
ADR-019 — MCP before REST; both over the same functions
Status: Accepted
The MVP's API is the exported functions of @lila-modeler/engine (parseBpmn, validate, resolveScenario, simulate, compare) and the CLI. packages/mcp (stdio, @modelcontextprotocol/server 2.0.0, spec 2026-07-28) arrives in M4, right after the CLI matches Bizagi Modeler's reference results and before the UI, with 5 tools: it costs ~100 lines, does not depend on the UI, and Brito works with agents; from there an agent can validate, simulate, patch scenarios, and compare on its own machine. REST (hono/fastify, one screen) arrives with the server and the repository. No logic lives at the edge. This satisfies ADR-003 (the UI and agents do the same thing) without building a server that serves no one today.
ADR-020 — Licenses
Status: Accepted
Core, CLI, MCP, and web under Apache-2.0 (decided by Brito on 2026-09-03: an explicit patent clause and enterprise adoption; compatible with bpmn.io's license, bpmn-moddle's MIT, and Simod's Apache-2.0). AGPL/LGPL/Camunda License are forbidden in packages/* (pm4py has been AGPL-3.0 since 2.7.12; SpiffWorkflow is LGPL; Camunda 8 has its own license). Prosimos and the QBP jar never enter the repository or public CI. The bpmn.io watermark is accepted and disclosed in the README. Closes ADR-008: the project is called Lila Modeler.
ADR-021 — BPMN scope and the "not supported" policy
Status: Accepted
Supported in v1: the list in section 3 of LILA_MODELER_ESTRUCTURA.md (see docs/BIZAGI_PARITY.md). Everything else produces an explicit validation error with the same text Bizagi Modeler uses ("not supported by the simulator"), never a silent failure. Each extra element gets added when a real user asks for it.
ADR-022 — Repository structure: one package that gets published, one app, and nothing speculative
Status: Accepted
packages/engine (with core/ as a pure subfolder), apps/web, packages/mcp in M4. npm workspaces (comes with Node; no pnpm/turbo/nx). No shared, types, or utils packages. Why: one package per thing that gets published; core/'s isolation is guaranteed by a test that checks the Worker bundle includes no bpmn-moddle, React, or node:*, not by a separate package. It splits into more packages once publishing separately actually matters.
ADR-023 — Two deployment modes, one SPA, one engine
Status: Accepted
(1) Desktop app: the apps/web SPA packaged with Electron in apps/desktop (main process + preload), built with electron-builder for macOS (dmg), Windows (nsis), and Linux (AppImage and deb) from a CI matrix; fileAssociations to open .bpmn on double click; optional auto-update once releases are frequent. The engine runs in the renderer's Web Worker; persistence is ADR-018. Direct precedent: Camunda Desktop Modeler (MIT) is Electron + bpmn-js + electron-builder with a .bpmn file association; its electron-builder.json is the template.
(2) Self-hosted server: packages/server (M6) in Node serves the same compiled SPA, exposes REST and MCP over HTTP on top of @lila-modeler/engine's functions, authenticates users, stores processes/versions/scenarios/runs in SQLite or PostgreSQL, and ships as a Docker image with docker-compose.yml. Interactive simulation keeps running in each user's browser Worker; the server only simulates when agents, the remote CLI, or scheduled runs ask for it.
The seam between the two is a ProjectStore interface in the SPA (list/read/write processes, scenarios, and runs) with DesktopStore (IPC → fs), RemoteStore (REST, M6), and a minimal BrowserStore (input/download) for the online demo. Defined in M5; the remote one arrives in M6 without touching views or the engine.
Why Electron and not Tauri: Tauri 2 produces ~10 MB installers versus ~150 MB and uses less memory, but it depends on the system's webview, and on Linux (WebKitGTK) there are documented performance and stability problems (reports of 40 fps versus 240 fps in Chromium for the same app; a "WebKit is totally unstable" thread in Tauri's discussions); bpmn-js is an SVG-heavy canvas where Chromium's consistency across all three OSes is worth more than installer size; and Tauri requires a Rust toolchain. Electron 43, electron-builder 26, and electron-forge (ESM, Node ≥ 22.12) are all active in 2026. Rules out: a PWA as the primary mode (no native dialogs in Safari/Firefox, no file association). Reversible: if installer size becomes a real problem, Tauri wraps the same SPA and only DesktopStore changes.
Why TypeScript comes out stronger: the same bundle runs in the desktop app's Worker and in the server's Node; a Python engine would have required bundling a Python runtime inside the installer or loading Pyodide (~12 MB).
Note: amended by ADR-031 (2026-10-05). On Windows the primary channel is now the web app installed as a PWA from Chrome or Edge; the "PWA as the primary mode" objection above is about Safari and Firefox and still holds there.
ADR-024 — Top-level aggregate across multiple replications
Status: Accepted
simulate() publishes, in the top-level numeric fields, the arithmetic mean of that same field
already aggregated within each replication. replications.kpis keeps, for those same paths, the
mean, sample deviation, and 95% CI. This leaves a result directly consumable by the CLI/UI without
forcing a walk through the KPI map, while keeping one statistical observation per replication.
Using the first replication is ruled out: it would be deterministic but not representative, and
could contradict the mean published right next to it. Pooling all cases from all replications
together is also ruled out: it would give more weight to runs with more observations and break the
statistical unit the CI is computed on. On cancellation, the top-level includes the partial
replication's work so it is not hidden; the CI uses only complete replications and is omitted with
fewer than two.
Revisit only if a consumer explicitly needs per-replication results; in that case a separate field is added, without changing the top-level's meaning. (test: LILA-029)
Addendum (2026-09-22): replications without observations (#356)
Decided by: the project owner.
A replication in which a task never completed has processing = {min: 0, max: 0, mean: 0, total: 0}: 0 is the identity of the empty set, not a duration. Averaging it in as an observation
made both the top level and replications.kpis report a time no instance ever took (a constant
360 s task reached in 18 of 30 replications published 216 s, with a CI that excluded 360 s) and
gave a spread that measured whether the task happened, not how long it took.
The estimand is now conditional: for a KPI that only exists when something happened — the
durations of a task or non-boundary timer, the cycle and wait times, cost per case and service
level of the process and of each outcome — the mean is taken over the per-replication values of
the replications that observed it, still one observation per replication, never a pool of the
cases. KpiSummary carries n, the number of contributing replications; sd and ci95 appear
only with n ≥ 2, n = 1 publishes {mean, n} and n = 0 publishes {mean: 0, n: 0} so the key
set stays stable. The same rule applies to a within-replication sd computed from a single
observation, which is 0 by convention. Unconditional KPIs (counts, totals, costs, flows, resources,
queue integrals) keep n = count. The top level uses the same selection through a single
leaf-path walker, so R-ARR-9 holds by construction. A run where some replications observed a
subject and others did not carries W-REPLICACIONES-SIN-OBSERVACIONES.
Precedent: meanBottlenecks already averaged bottlenecks[].utilization only over the
replications where the element appeared, for the same reason (RESULTS_FORMAT.md § 6); that rule
and saturationWarnings are unchanged.
Ruled out: weighting by the number of cases (the pooled estimand ADR-024 already rejects), and
keeping zeros while only adding a warning (the published number would stay wrong). Consequence:
this is a reviewed change of the public contract — the KpiSummary shape and a new warning code.
Column names, BPMN ids, units and the core/ boundary are unchanged; results stored before
1.0.0-beta.1 carry no n, were computed with the previous definition and are still accepted.
(test: #356)
ADR-025 — Flat event log by allocation, grouped by activity instance
Status: Accepted
Each pool allocation produces one flat row, and all the rows of one occurrence share
activityInstanceId and their original position in allocationIndex. An activity with no
resource, or one that closes while still queued, produces a sentinel row (resourceId = null,
resourceQuantity = null, allocationIndex = null). The partial lifecycle distinguishes
terminated from inFlight, keeps nullable timestamps and observedUntil. Costs are broken down
into elementCost and resourceCost; the element's fixed cost appears exactly once, in the
emitted row with the lowest allocationIndex, and cost is its exact sum.
One row per activity with resources[] is ruled out: it duplicates structure inside the CSV,
hinders streaming/XES, and contradicts the committed flat format. Internal accumulators with no
reconstruction from the log are also ruled out: they violate R-COST-3 and prevent auditing
utilization/costs without re-running the simulation. The chosen alternative keeps the CSV flat,
represents quantity and AND/OR, avoids duplicating the fixed cost, and keeps queued or in-flight
tasks observable at cutoff.
Revisit only with a new version of the results format; v1 consumers depend on these columns. (test: LILA-033, LILA-036, LILA-037)
ADR-026 — AND scheduler by signature, and a heap of eligible heads
Status: Accepted
AND requests with the same sorted (pool, quantity) signature share one FIFO queue. Each pool
indexes only the signatures that use it; when its capacity changes, those heads get re-evaluated.
A separate heap, compared explicitly by the original (enabledAt, seq), picks the first
satisfiable candidate and reserves all its pools in a single mutation. Single-pool requests share
one class per pool, even across different quantities, to keep strict FIFO.
Walking and reinserting the whole global queue on every arrival is ruled out: under saturation it produces O(n² log n). Creating one single-pool class per quantity is also ruled out, because it would let a small request cut ahead of the same pool's head. Versions/tombstones invalidate heads without linear searches, and release/cancel gather all affected pools before scheduling.
LILA-035 uses this same structure without adding anything new: an OR selection is enqueued as
one entry per alternative, each in its pool's single-pool class and all sharing the same
seq, so they share FIFO position and compete on equal footing with single-pool requests.
Granting one alternative leaves the others as tombstones — the same mechanism that already
invalidates heads — and flags their classes for immediate re-evaluation, which is what stops the
next head of a withdrawn pool from waiting on an event that will never arrive. The tie-break among
alternatives freed at the same time is the index declared in the scenario (R-REC-6):
(enabledAt, seq, altIndex) is still a total order and does not change the AND/single order,
where altIndex is always 0. Picking the freest pool or the least-utilized one is ruled out: it
would require a global criterion and reordering queues, and no reference behaviour from Bizagi
Modeler calls for it. ResourceManager remains a non-public API. (test: LILA-034, LILA-035)
ADR-027 — .lila: the project folder, zipped
Status: Accepted
A .lila file is the ADR-018 project folder compressed as a ZIP, with the same layout and the
same file names (lila-project.json, model.bpmn, <name>.scenario.json, runs/<id>.result.json).
zip -r of an existing project folder is a valid .lila and unzip of a .lila is a valid
project folder; nothing converts and there is no migration. The manifest gains one optional field,
engine, naming the engine version that wrote the runs. MIME application/vnd.lila-modeler+zip;
see docs/PROJECT_FORMAT.md.
Why a container at all: the folder is right for git and wrong for handing work to somebody — the
online demo could only download a .lila.json blob that no desktop install could open, and «send
me your model» meant a zip built by hand with no guarantee of what was in it. Why the folder
zipped, and not a new format: the folder is already the contract every mode writes, so the
container costs one reader and one writer instead of a second definition of what a project is,
and anyone can inspect or repair a .lila with the unzip tool their system already has.
fflate is the only new dependency (MIT, ~30 KB, no transitive dependencies, works unchanged in
Node and in the browser worker) — a hand-rolled DEFLATE is not a thing this project should own,
and the alternatives either assume Node (node:zlib, unusable in the SPA) or pull a tree of
packages. The shared document types moved to @lila-modeler/engine/project, ending the hand-maintained
copy apps/desktop/src/projectTypes.ts kept of apps/web's.
Unknown entries inside a .lila (notes.md, attachments/…) are reported in problems and
dropped, not preserved: keeping them would put opaque bytes inside ProjectDocument, which
crosses structuredClone over IPC and JSON.stringify into the browser's localStorage mirror.
The folder form still leaves such files alone. Revisit if anyone actually wants attachments —
that is a format decision (a declared attachments/ area with a manifest entry), not a
serialization trick. (tests: packages/engine/test/project/lila.test.ts,
apps/desktop/src/lilaFile.test.ts)
ADR-028 — Routing conditioned on the case's previous outcome (conditions)
Status: Accepted
A sequence flow leaving a diverging XOR may declare
conditions: [{ flowTaken, probability }]. When a token reaches the gateway, each outgoing flow's
declared probability is the probability of the first entry whose flowTaken the case has
already traversed, and the plain probability otherwise; from there R-XOR-1…5 apply unchanged to
that per-case vector, and the gateway still draws one uniform from its own stream
(SEMANTICS.md § 6.1, R-COND-1…5). On any element that is not such a flow, conditions stays the
reserved field of § 15, with the same E-RESERVADO text as before.
Context: v1 routes on probability only, so a model that merges two branches cannot tell them apart
again downstream. The service request example (packages/engine/test/fixtures/service-request) duplicates the whole denial
path — deny and inform, twice — for no reason other than to keep the two rejection causes
countable. That duplication is what every Bizagi user hits first, and it is the cheapest gap in
epic #335 to close.
Why not case variables: an expression language over case data means a data model, an evaluator, a type system and a debugger for it, and it would make the engine's state per case unbounded. The flows a case took are state the engine already has, they are named by ids the scenario already uses, and they cover the duplication that motivated the ticket. Bizagi itself only offers this at its level 4 through "gateway conditions" over attributes; this is the 10 % of that which removes 90 % of the duplication.
Ceilings, all deliberate:
- A set, not a history.
flowsTakenhas set semantics: a loop does not reset it and the second pass through a flow is indistinguishable from the first (R-COND-3). Routing by how many times something happened is not expressible. - The lint cannot fully anticipate the run. Substituting probabilities happens per case, so
the static check of R10 cannot predict
W-XOR-NORMALIZADAorE-XOR-SUMA-CEROfor a substituted vector. The engine emits the normalization warning at run time, once per signature, and a substituted vector summing to 0 falls into R-XOR-7's discard path (the last flow withp > 0, or the last flow) rather than aborting. W-COND-INALCANZABLEis reachability, not concurrency. It walks the IR backwards from the gateway throughincoming. A flow on a parallel branch of an AND fork is not backwards reachable and warns, even though a token could in fact have traversed it. It is a warning precisely because the analysis is the cheap one; the run behaves correctly either way.extendsreplaces the whole array.deepMergereplaces arrays, so a delta that declaresconditionsreplaces the inherited list; there is no per-entry merge.- No UI for picking a flow id. The scenario panel renders
flowTakenas a text input, not a select of the model's flows: a wrong id shows up asE-REF-DESCONOCIDAunder its own path.
(tests: packages/engine/test/conditions.test.ts, fixture
packages/engine/test/fixtures/service-shared-denial.bpmn)
ADR-029 — The repository: one .lila can hold a whole organization
Status: Accepted (direction, decided by Brito on 2026-09-26). Minimum implemented (#498, #461):
a project with more than one process is a version 2 repository with each process under
processes/<slug>/, the canvas tabs are its processes, and a call activity opens the process it
calls. The first ticket settled two points of the layout below: the manifest keeps its name,
lila-project.json, with "version": 2 (a separate lila-repository.json would read as «no
manifest» to a version 1 build instead of as a newer version), and version 2 is written only once a
project has a second process — a one-process project stays version 1, byte for byte. process.json,
maps, analyses, the catalog, dashboards, records and the per-object ProjectStore are not built yet.
The operative description is docs/PROJECT_FORMAT.md § Version 2.
Lila Modeler grows past a Bizagi alternative into a modelling, analysis, design, simulation and
quality platform in the spirit of ADONIS: process maps down from macroprocesses, RACI, risks and
controls, Ishikawa and other analyses, dashboards, quality management. Automation stays out —
engines such as n8n consume the standard BPMN and the API/MCP. The one assumption of ADR-018 and
ADR-027 that does not survive that is a project is one model. It is replaced by a
repository: a folder (zipped, a .lila) that can hold one process or an entire organization.
mi-empresa/ folder (git) · zip = mi-empresa.lila
lila-repository.json manifest, version 2
maps/<slug>.map.json process maps; each node points at a model by id
processes/<slug>/ exactly the version 1 project layout:
model.bpmn one process per .bpmn (pools only for collaborations)
<name>.scenario.json
runs/<id>.result.json
process.json owner, status Draft/Released, validity, parent
analysis/<slug>.<kind>.json Ishikawa and other analyses
catalog/<type>/<id>.json roles, units, systems, documents, risks, controls, KPIs, requirements
dashboards/<slug>.dashboard.json the definition only; figures are computed
records/<type>/<id>.json optional quality records (see below)
Rules:
- A version 1 project is a repository with one process.
processes/<slug>/is the ADR-027 layout byte for byte, so a version 1.lilaor folder opens as a one-process repository and nothing migrates. A version 2 reader accepts version 1; a version 1 reader refuses version 2, asdocs/PROJECT_FORMAT.md§ Versioning already requires. - One process, one
.bpmn. Each process has its own owner, version and release, so it is its own file. Severalbpmn:processin one file only for collaboration diagrams. The canvas tabs are the repository's open models;+creates a model in the same repository. - References by id, never by path. Folders can be renamed or moved without breaking links. The id → file index is derived on open and never stored.
- One file per catalog object. It keeps git merges and concurrent edits apart, and it maps
one to one onto a server row. Completes ADR-013's
catalog.json. - BPMN is the only standard format. Maps, analyses, org charts and dashboards have no useful interchange standard, so they are our own JSON with a versioned schema (ADR-001: BPMN for interchange, not necessarily the only store).
- The folder is the server's lossless export. Server mode (ADR-023) stores the same objects as rows; going folder → server → folder loses nothing. Real-time collaboration arrives with the server (a CRDT over these objects) and does not change the format.
- Runs are optional in a shared
.lila. They are the only heavy part; the model is text and compresses well.
Quality records — audits, findings, nonconformities, corrective actions (CAPA) — are
operational data: they change daily, many people touch them, and they need assignment, due dates,
notifications and an audit trail. Their home is server mode. A user without a server (a consultant
working alone, say) may keep them in the repository under records/, opt-in and not recommended:
the UI says so, and the files carry no guarantee against two people editing two copies. Records
point at design objects by id (NC-034 → control, process), which is what lets a dashboard count
nonconformities per control.
Consequence for the app: the whole ProjectDocument no longer crosses IPC and the
localStorage mirror at once. ProjectStore becomes list/read/write per object with lazy loading;
the online demo may keep a small repository in memory.
Not decided here, and not built until asked for one by one: the schemas of maps, analyses, dashboards and records, the release workflow, the server's CRDT.
ADR-030 — The MCP server is its own npm package, @lila-modeler/mcp
Status: Accepted (decided by Brito on 2026-10-05).
The MCP server ships as a separate public package, @lila-modeler/mcp, with the lila-mcp bin,
instead of being moved into @lila-modeler/engine. Agents install it the usual way for MCP
servers: npx -y @lila-modeler/mcp. Before this, packages/mcp was private, so the published
engine had the lila mcp subcommand but not the server it loads, and
npx -y @lila-modeler/engine mcp stopped with «the package @lila-modeler/mcp is missing».
- The engine stays light. It depends only on
bpmn-moddle,bpmn-auto-layout,fflateandzod, and the web and desktop apps consume it too. Folding the server in would make every consumer install@modelcontextprotocol/server. - The package already existed.
packages/mcphad the bin,files, a build script and a pinned engine dependency; publishing it means removingprivateand adding the npm metadata. - Independent cadence for the protocol SDK. The MCP SDK (v2) changes on its own schedule; a bump there does not touch the engine's dependency tree.
- Same version, same release. Both packages carry the version of the batch (the six places
release.ymlchecks), and@lila-modeler/mcppins the exact engine version.release.ymlpublishes the engine first and then the server, with the same dist-tag (betafor a prerelease,latestotherwise),--provenanceand the samepublish_npmguard. The package contains onlydist,bin,README.md,package.json, andLICENSEandNOTICElike the engine. lila mcpstays. It still loads@lila-modeler/mcpwith a dynamicimport()(no package cycle), so it works wherever both packages are installed, as in a repository checkout.
The first publication is by hand by the owner, and then the npm trusted publisher (OIDC) is set up for the new package, as was done for the engine. (test: #569)
ADR-031 — On Windows the main channel is the PWA in Chrome and Edge
Status: Accepted (decided by Brito on 2026-10-05). Amends ADR-023.
Windows users run the web app from GitHub Pages (https://alambritodito.github.io/lila-modeler/app/)
in Chrome or Edge and install it as a Progressive Web App. The unsigned NSIS .exe stays as a
secondary channel; signing it (SignPath, #501) is paused. macOS keeps the Electron app as its
primary channel and Linux keeps the AppImage and .deb.
- Why the ADR-023 objection no longer applies on Windows. ADR-023 ruled out a PWA as the
primary mode because Safari and Firefox have no native file dialogs and no file association.
Chromium, which is what Windows users already have (Edge ships with the system), has both: the
File System Access API (
showOpenFilePicker/showSaveFilePicker, a file handle that Save rewrites in place) and the manifest'sfile_handlerswithwindow.launchQueue, which makes an installed PWA the handler for.lilaon double click. - Why not the signed
.exe. An unsigned installer meets SmartScreen's «Windows protected your PC» and some managed machines refuse it outright; a signing certificate needs an organization history the project does not have yet (#501). Installing a PWA needs no administrator rights and no signature, and every deploy of Pages updates it. - What it is.
apps/webgainsmanifest.webmanifest(name, relativestart_urlandscopeso the same file works at/and at/lila-modeler/app/,display: standalone, brand icons 192, 512 and maskable,file_handlersfor.lilawithlaunch_handlerfocus-existing, so a double-click goes to the window already open and its unsaved-changes prompt), a service worker that caches the app shell under a cache named after the app version and a hash of the build so the installed app opens offline, and progressive enhancement inBrowserStore: with the File System Access API, Open keeps the file handle and Save writes back to it; without it (Safari, Firefox, an older Chromium) Open is the file input and Save downloads, exactly as before.DesktopStoreand Electron are untouched. - What it does not do. No
.bpmnfile handler (a loose diagram is imported, not opened as a project, in the browser); no folder projects (ADR-018) in the browser, only the.lilacontainer (ADR-027); no automatic background update while the app is open — a new version takes over the next time every window of the app has been closed.
Revisit: if Windows signing becomes available and testers report that the PWA's file handling is
not enough, the .exe can become primary again without code changes; the SPA is the same.
(test: #576 — Page.getInstallabilityErrors empty on the Pages build; tester guide #575)
See also
LILA_MODELER_ESTRUCTURA.md— the full structure document (source of truth for every ADR in this file).docs/BIZAGI_PARITY.md— reference-behaviour table cited by ADR-021.docs/BPMN_EXTENSION.md— operational implementation of ADR-012 and ADR-014.docs/RESULTS_FORMAT.md— operational implementation of the calendar/utilization part of ADR-016.docs/PROJECT_FORMAT.md— operational implementation of ADR-018 and ADR-027 (the project folder and the.lilacontainer).docs/DECISIONS-corpus-previo.md— original text of ADR-001 through ADR-008 (earlier corpus, project then called Open Process Platform).BACKLOG.md— breakdown into epics and tickets per milestone.