Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ body:
attributes:
label: BPMNscript Version
description: Which version of BPMNscript are you using?
placeholder: e.g. 0.1.0
placeholder: e.g. 0.1.1
validations:
required: true
- type: dropdown
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Write executable business processes as text, compile them to BPMN 2.0 XML, and read existing BPMN files back as text.

> Status: version 0.1.0 is the first release, from a bachelor's thesis project.
> Status: version 0.1.1 is the current release, from a bachelor's thesis project.
> Download the `.vsix` of the VS Code extension from the GitHub Release, or build everything from source as described below.
> The packages are not published to npm or the VS Code Marketplace.

Expand Down
170 changes: 85 additions & 85 deletions docs/bpmn-coverage.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/decisions/0003-use-langium-as-language-workbench.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Which language workbench should be used to implement the DSL infrastructure?
- Langium: TypeScript throughout, with built-in scoping, cross-reference resolution and validation infrastructure, and a Chevrotain parser with unbounded (ALL(\*)) lookahead.
- Eclipse Xtext: the most mature workbench (20 years), with EMF integration giving native access to the BPMN 2.0 metamodel and a built-in AST-to-text serializer, but VS Code support needs a hybrid Java/TypeScript stack with a JVM backend, no browser deployment path exists, the learning curve is steep (EMF, Guice, Xtend, Eclipse, MWE2), and its maintenance future has been in question since a 2020 sustainability discussion.
- MontiCore: has an existing BPMN Workflow DSL grammar, auto-generates pretty printers from grammars, and has strong language composition (inheritance, embedding, aggregation).
A language server and IntelliJ and VS Code plugins can be generated from a MontiCore grammar (Charles, Hellwig and Rumpe, Modellierung 2026), but its toolchain is Java, and the BPMN libraries this project builds on are TypeScript.
A language server and IntelliJ and VS Code plugins can be generated from a MontiCore grammar (Charles, Hellwig and Rumpe, Modellierung 2026), but its toolchain is Java, and the BPMN libraries this project builds on are JavaScript.
- ANTLR with a manual LSP server: the most widely used parser generator, with full control and no framework lock-in, and grammar-aware completion via `antlr4-c3`, but every IDE feature would need to be implemented by hand, an estimated 3-5 weeks of the timeline, with no generated VS Code extension scaffolding.
- JetBrains MPS: backed by JetBrains, with trivial language composition in its projectional editor, but it stores files as XML rather than human-readable text, has no VS Code or LSP integration, and is architecturally incompatible with a textual DSL.

Expand All @@ -38,7 +38,7 @@ Browser deployment for a future playground was a secondary factor, all within a

### Consequences

- Good, because the entire stack is TypeScript, eliminating context-switching between languages
- Good, because all of this project's code is TypeScript and uses the JavaScript BPMN libraries through type declarations, eliminating context-switching between languages
- Good, because the grammar yields the AST types and syntax highlighting at build time, and the parser, completion, go-to-definition, and diagnostics at run time
- Good, because the language server can run in a web worker, enabling a browser-based playground with approximately 95% code reuse
- Good, because `bpmn-moddle` (the standard JavaScript library for BPMN 2.0 XML) integrates naturally
Expand Down
6 changes: 4 additions & 2 deletions docs/decisions/0008-fork-camunda-moddle-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ How should BPMNscript register Operaton's extension attributes with `bpmn-moddle
## Decision Outcome

Chosen option: "Ship a trimmed fork", because it avoids namespace collisions, eliminates fragile string-replacement post-processing, and keeps the descriptor small and auditable.
The fork is based on `camunda-bpmn-moddle/resources/camunda.json` from the `camunda/camunda-bpmn-moddle` repository on GitHub, with the prefix and namespace URI replaced and only the attributes BPMNscript uses kept.
The fork is based on `camunda-bpmn-moddle/resources/camunda.json` from the `camunda/camunda-bpmn-moddle` repository on GitHub, with the prefix and namespace URI replaced, cut down to what the compiler writes and the importer has to recognize, and adjusted where the importer must see what a document carries.
A parameter holds its nested value in a list, since `bpmn-moddle` keeps only the last of two children in a single-valued property without a warning, and `isStartableInTasklist` is text, since Operaton accepts any capitalization of `true`.
Types the compiler never writes, such as connectors, stay so that the importer recognizes and refuses or reports them.

### Consequences

- Good, because emitted XML correctly carries `xmlns:operaton` and `operaton:`-prefixed attributes without post-processing.
- Good, because the descriptor defines only the attributes actually used, which keeps it easy to review and extend.
- Good, because the descriptor defines only what the compiler writes and the importer must recognize, which keeps it easy to review and extend.
- Good, because no external dependency is needed for the Operaton namespace.
- Bad, because if Operaton publishes an official moddle extension in the future, the local fork must be replaced.
- Bad, because the fork must be maintained manually when new Operaton attributes are needed.
6 changes: 3 additions & 3 deletions docs/decisions/0010-deterministic-synthesized-ids.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ If a base id is already in the taken set, the resolution appends `_2`, `_3`, and
Gateway ids skip the `taken`/`resolveCollision` guard, because the position-path scheme never generates the same id twice.
What collides with them is an author-chosen statement name matching a synthesized-id pattern.
The `checkReservedNames` validator in `packages/language/src/bpmn-script-validator.ts` rejects such a name at validation time.
It reserves by prefix `Gateway_*_(split|join|fork|loop|race)`, `Throw_*`, `EventSubProcess_*`, `Boundary_*`, `Catch_*`, the two-segment `Flow_*_*` form (`/^Flow_.+_.+$/`), and the `*_di` suffix diagram-interchange ids carry.
It reserves by prefix `Gateway_*_(split|join|fork|loop|race)`, `Throw_*`, `EventSubProcess_*`, `Boundary_*`, `Catch_*`, `BPMNDiagram_*` and `BPMNPlane_*`, the two-segment `Flow_*_*` form (`/^Flow_.+_.+$/`), and the `*_di` suffix diagram-interchange ids carry.
Single-segment names such as `Flow_Control` stay legal, because synthesized flow ids occupy only `SequenceFlow.id` and never node names, so they cannot collide.
For a start and an end it reserves, for each statement, the exact `StartEvent_<c>` and `EndEvent_<c>` of the container `c` whose body holds it: the process, or the nearest `subprocess` or `attempt` block.
The message says the compiler generates that id for the container's implicit start or end.
Expand Down Expand Up @@ -106,7 +106,7 @@ A code is referenced inside a trigger's parens, which the grammar parses as an e
An element the document gives no id gets one on import, before anything keys it.
A sequence flow gets `Flow_<source>_<target>`, and any other element `<Type>_<container>`, from the local name of its BPMN type and the id of the process or sub-process holding it, such as `UserTask_invoice`; both resolve against every id in the document the way the compiler's ids do.
An end event also skips `EndEvent_<id>` for every id in the document and so usually imports as `EndEvent_<container>_2`, since the printer would take such an id for the implicit end the compiler gives a container or a boundary escape and leave the event out.
Operaton deploys such a flow as an unnamed transition and such a task, end or catch as an activity nothing can flow into, since a `sourceRef` or `targetRef` needs an id, so the step prints and the validator reports that it can never run.
Operaton deploys such a flow as an unnamed transition and such a task, end or catch as an activity nothing can flow into, since a `sourceRef` or `targetRef` needs an id, so the printer leaves the step out of the script with a warning that no route reaches it.
A start event, a timer event, an event-based gateway, and a message or signal boundary event without an id refuse the import instead, since Operaton fails the deployment on each.
An error, escalation, cancel or conditional boundary event without an id refuses too: Operaton deploys it and fails the run when an error is thrown inside its step, an escalation reaches it, its transaction is cancelled, or its step runs.
A minted id would make the rebuilt document deploy and run where the source does not.
Expand All @@ -133,7 +133,7 @@ The engine sets the variable under that exact name (`FormFieldHandler.handleSubm

`packages/transform/test/synthesize-ids.test.ts` verifies every template, the collision-resolution rule, and the exact predicates for a minted start and end.
The reserved-name table in `packages/language/test/validating.test.ts` pins the exact rule per container and the freed `StartEvent_1`.
The backtick table in `packages/transform/test/ir-to-dsl.test.ts` prints every id site in backticks with no warning, re-parses and validates the source, and compiles it back to the same ids; the Modeler-default row asserts the printed start, end and initiator with no warning.
The backtick test in `packages/transform/test/ir-to-dsl.test.ts` prints every id site in backticks with no warning, re-parses and validates the source, and compiles it back to the same ids, and the synthesized-terminal test in the same file prints a Modeler default's start, end and initiator with no warning.
`tests/backtick-names.round-trip.test.ts` imports a document per id the plain form cannot spell, asserts the exact printed script, and compiles it to a document with the same ids and no model difference.
The refusal matrix in `packages/transform/test/xml-to-ir.test.ts` pins the two import refusals.
The two id-less tables in the same file pin the minted ids and the id-less events that refuse.
Expand Down
6 changes: 3 additions & 3 deletions docs/decisions/0012-honest-bpmn-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Some of those drops change what the imported process executes; others do not.
What should happen when `xmlToIr` meets content the IR cannot carry, and should every such case be treated the same way?

A surface that answers from its own reading of the IR disagrees with `BpmnParse` in both directions, refusing files the engine deploys while carrying files it refuses, and it says nothing about what the engine does with what it drops.
Camunda Modeler's default export is the case that costs most: it writes a `bpmn:collaboration` with one participant for any diagram with a pool or a lane, an `isExecutable="false"` process for every empty pool, and `xmlns:camunda` on every file, which is also the shape of the Operaton invoice example and four MIWG reference files.
Camunda Modeler's default export is the case that costs most: it writes a `bpmn:collaboration` with one participant for any diagram with a pool or a lane, an `isExecutable="false"` process for every empty pool, and `xmlns:camunda` on every file, and the Operaton invoice example and four MIWG reference files carry the same one-participant collaboration.
Document shapes the engine rejects before `BpmnParse` sees an element pass through instead, since moddle-xml and saxen read them where a validating parse does not.
`bpmn:documentation` has no place at all, which keeps the letter of the contract while working against everything the contract exists for.

Expand Down Expand Up @@ -51,7 +51,7 @@ A shape the engine deploys and this surface cannot spell either drops with a war
`packages/transform/README.md` lists the refused ones, among them a `bpmn:complexGateway`, a compensation boundary event, two input parameters sharing a name, a script body no fence can enclose, and a thrown message's result variable beside an `expression` binding, where the engine stores a value the throw has no slot for.

A refusal throws a subclass of `UnsupportedConstructError` before any IR is produced, so there is never a partial IR, and a consumer classifies the whole family with a single `instanceof` check while each subclass still carries construct-specific metadata for a tailored message.
A warning is returned in a `warnings: ImportWarning[]` array alongside the IR, so `xmlToIr` returns `{ ir, warnings }` rather than a bare `BpmnProcess`, which makes the channel unignorable at the type level: every call site must destructure or explicitly discard `warnings`, where an optional collector parameter (`xmlToIr(xml, sink?)`) or a second `xmlToIrWithWarnings` function leaves it easy to skip.
A warning is returned in a `warnings: ImportWarning[]` array alongside the IR, so `xmlToIr` returns `{ ir, warnings }` rather than a bare `BpmnProcess`, which hands every caller the warnings in the same value as the IR rather than through an optional collector parameter (`xmlToIr(xml, sink?)`) or a second `xmlToIrWithWarnings` function a caller has to know to use.
What the engine deploys and then fails on at run time is carried as written, with a warning that the printed script draws an error there.
The exception is an event without an id that the engine deploys and cannot run, which is an error, escalation, cancel or conditional boundary event.
The IR names every node, so such an event cannot be carried as written, and an id minted for it would make the handler run where the source fails, so the import refuses it ([ADR-0010](0010-deterministic-synthesized-ids.md)).
Expand Down Expand Up @@ -106,7 +106,7 @@ Every one of them is warned rather than refused, because a refusal has no meanin
### Confirmation

The refusal and warning tables in `packages/transform/test/xml-to-ir.test.ts` pin every documented row with the whole warning list or the refusal class and detail, including the document level, the `camunda:` file and the setting spelled under both prefixes, the resource-role merge, and the quantity warning.
`tests/review-loop.round-trip.test.ts` and the invoice fixture in `tests/` pin the Modeler-shaped documents end to end, and `tests/e2e/forms-and-external-tasks.test.ts` deploys the re-export of a task assigned in BPMN's own words to a real engine and reads the identity links back.
`tests/invoice-receipt.import.test.ts` pins the Modeler-shaped Operaton invoice example end to end and `tests/review-loop.round-trip.test.ts` its approve and review loop, and `tests/e2e/forms-and-external-tasks.test.ts` deploys the re-export of a task assigned in BPMN's own words to a real engine and reads the identity links back.
The frozen `documentation` golden pair and a parameterized sweep per hop name every position that carries documentation and every position that reports it instead, so a position wired to neither branch fails the sweep rather than passing in silence.
`tests/value-documentation.round-trip.test.ts` takes every value position through one table, from script to XML and back and from that XML to a rebuild with no model difference.

Expand Down
6 changes: 3 additions & 3 deletions docs/decisions/0013-validator-mirrors-parse-time-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Which of the engine's parse-time rules should the validator repeat, and how shou

- A document the validator passes should deploy, and a refusal should reach the author on the line that caused it rather than as an engine message naming a generated id.
- A setting that configures nothing is a mistake the author cannot see on the page, since the document deploys and runs.
- Every mirrored rule cites the engine method it repeats, so a reader can check it against the engine's source when the engine moves.
- Every mirrored rule states in its message what the engine does, and this record names the method behind it, so a reader can check the rule against the engine's source when the engine moves.

## Considered Options

Expand Down Expand Up @@ -60,9 +60,9 @@ A timer carrier is exempt for all three, since the timer job reads them off the

### Consequences

- Good, because a script that compiles also deploys where these rules are concerned, and the message names the scope and the engine method.
- Good, because a script that compiles also deploys where these rules are concerned, and the message says where the rule applies and that the engine refuses the shape, naming an engine method in a note only where that explains the cause.
- Good, because a setting that configures nothing is reported at the key that carries it.
- Bad, because the validator restates engine rules that can move under a new engine version; each rule cites the method it mirrors so the drift can be checked.
- Bad, because the validator restates engine rules that can move under a new engine version; this record names the method behind each rule so the drift can be checked.
- Neutral, because a script with a nested start form, an unpaired job setting, or a `resultVariable` beside `topic` draws a diagnostic although it deploys.

### Confirmation
Expand Down
Loading
Loading