diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5419366..03d6ca3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -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 diff --git a/README.md b/README.md index 7a0a73d..60db9bc 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/bpmn-coverage.md b/docs/bpmn-coverage.md index 130e5d9..945e517 100644 --- a/docs/bpmn-coverage.md +++ b/docs/bpmn-coverage.md @@ -9,57 +9,57 @@ A file holding an id outside ASCII letters, digits, `_`, `-` and `.`, starting w ## Activities -| BPMN element | BPMNscript | Import | -| ------------------------------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `bpmn:task` | `step X` | imported | -| `bpmn:manualTask` | not authored | imported as `step`, warned (`rewritten`): history reports `task` where the source wrote `manualTask` | -| `bpmn:userTask` | `user X(...)` | imported; refused on a `bpmn:humanPerformer` beside `operaton:assignee` or a second `bpmn:humanPerformer` (`UnsupportedAssignmentError`) | -| `bpmn:humanPerformer`, `bpmn:potentialOwner` on a user task | `assignee`, `candidateUsers`, `candidateGroups` keys | imported onto those keys, warned (`rewritten`) naming the rewrite | -| `bpmn:performer`, `bpmn:resourceRole`, a role on any other activity | not authored | dropped, warned (`unmappedConstruct`) | -| `bpmn:serviceTask` | `service X(...)` | imported; refused without a binding the engine reaches, with a `type` outside `external`, `mail`, and `shell`, or with an `operaton:connector` (`UnsupportedServiceTaskFormError`) | -| `bpmn:sendTask` | `send X(...)` | imported, refused as a service task is | -| `bpmn:businessRuleTask` | `decide X(...)` | imported, refused as a service task is, plus on a `decisionRefTenantId` or a `decisionRefBinding` of `versionTag` (`UnsupportedServiceTaskFormError`) | -| `bpmn:receiveTask` | `receive X(...)` | imported | -| `bpmn:scriptTask` | `script X` with a fenced body | imported; refused on an empty `scriptFormat`, on neither a body nor a resource, or on an `operaton:resource` in place of the body (`UnsupportedExtensionFormError`) | -| `bpmn:callActivity` | `call X(process: "...")` | imported; refused on a `caseRef`, a `calledElementTenantId`, a `versionTag` binding, or an `in`/`out` mapping outside the recognized shapes (`UnsupportedCallActivityError`), or a mapping whose `source` or `target` names a variable no plain name spells, such as `var` (`UnsupportedVariableNameError`) | -| `bpmn:subProcess` | `subprocess X { }` | imported; refused without a start event (`UnsupportedEventFeatureError`) | -| `bpmn:subProcess` with `triggeredByEvent="true"` | `on { }` | imported as an event handler, see the start event rows | -| `bpmn:transaction` | `attempt X { }` | imported, a missing start event added, warned (`behaviourChanged`); `method`, `protocol`, and `triggeredByEvent` dropped, warned (`unmappedConstruct`) | -| `bpmn:adHocSubProcess` | not authored | refused (`UnsupportedElementError`) | -| `bpmn:multiInstanceLoopCharacteristics` | `for each x in c`, `for 3`, with `sequentially` | imported; refused on an event handler, beside an `operaton:outputParameter`, or with a `loopCardinality` body the script cannot spell (`UnsupportedLoopCharacteristicsError`) | -| `bpmn:standardLoopCharacteristics` | not authored | dropped, warned (`unmappedConstruct`): Operaton runs the activity once regardless | -| `startQuantity`, `completionQuantity` other than `1` | not authored | dropped, warned (`unmappedConstruct`): Operaton never reads either | +| BPMN element | BPMNscript | Import | +| ------------------------------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `bpmn:task` | `step X` | imported | +| `bpmn:manualTask` | not authored | imported as `step`, warned (`rewritten`): history reports `task` where the source wrote `manualTask` | +| `bpmn:userTask` | `user X(...)` | imported; refused on a `bpmn:humanPerformer` beside `operaton:assignee` or a second `bpmn:humanPerformer` (`UnsupportedAssignmentError`) | +| `bpmn:humanPerformer`, `bpmn:potentialOwner` on a user task | `assignee`, `candidateUsers`, `candidateGroups` keys | imported onto those keys, warned (`rewritten`) naming the rewrite; a role with no formal expression dropped, warned (`unmappedConstruct`) | +| `bpmn:performer`, `bpmn:resourceRole`, a role on any other activity | not authored | dropped, warned (`unmappedConstruct`) | +| `bpmn:serviceTask` | `service X(...)` | imported; refused without a binding the engine reaches, with a `type` outside `external`, `mail`, and `shell`, with `mail` or `shell` fields the engine refuses, with a result variable beside a `class` or `delegateExpression`, or with an `operaton:connector` (`UnsupportedServiceTaskFormError`) | +| `bpmn:sendTask` | `send X(...)` | imported, refused as a service task is | +| `bpmn:businessRuleTask` | `decide X(...)` | imported, refused as a service task is, plus on a `decisionRefTenantId`, a `decisionRefBinding` of `versionTag`, an unrecognised `mapDecisionResult`, or a `version` binding without a version (`UnsupportedServiceTaskFormError`) | +| `bpmn:receiveTask` | `receive X(...)` | imported; refused on a `messageRef` naming no `bpmn:message` root or a root with no `name` (`UnsupportedEventFeatureError`) | +| `bpmn:scriptTask` | `script X` with a fenced body | imported; refused on an empty `scriptFormat`, on neither a body nor a resource, on an `operaton:resource` in place of the body, or on a body holding three consecutive backticks (`UnsupportedExtensionFormError`) | +| `bpmn:callActivity` | `call X(process: "...")` | imported; refused without a `calledElement`, on a `caseRef`, a `calledElementTenantId`, a `versionTag` binding, a `version` binding without a version, or an `in`/`out` mapping outside the recognized shapes (`UnsupportedCallActivityError`), or a mapping whose `source` or `target` names a variable no plain name spells, such as `var` (`UnsupportedVariableNameError`) | +| `bpmn:subProcess` | `subprocess X { }` | imported; refused without a start event (`UnsupportedEventFeatureError`) | +| `bpmn:subProcess` with `triggeredByEvent="true"` | `on { }` | imported as an event handler, see the start event rows | +| `bpmn:transaction` | `attempt X { }` | imported, a missing start event added, warned (`behaviourChanged`); `method`, `protocol`, and `triggeredByEvent` dropped, warned (`unmappedConstruct`) | +| `bpmn:adHocSubProcess` | not authored | refused (`UnsupportedElementError`) | +| `bpmn:multiInstanceLoopCharacteristics` | `for each x in c`, `for 3`, with `sequentially` | imported; refused on an event handler, beside an `operaton:outputParameter`, with neither a count nor a collection, with an element variable but no collection, or with a `loopCardinality` or element variable the script cannot spell (`UnsupportedLoopCharacteristicsError`) | +| `bpmn:standardLoopCharacteristics` | not authored | dropped, warned (`unmappedConstruct`): Operaton runs the activity once regardless | +| `startQuantity`, `completionQuantity` other than `1` | not authored | dropped, warned (`unmappedConstruct`): Operaton never reads either | ## Gateways -| BPMN element | BPMNscript | Import | -| ------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `bpmn:exclusiveGateway` | `if` / `else if` / `else`, `while`, `do ... while` | imported; refused on a shape `BpmnParse.validateExclusiveGateway` fails, such as two unconditioned routes (`UnsupportedGatewayShapeError`); a route with no condition listed before a conditioned one prints in its place as an `if (true)` head | -| `bpmn:parallelGateway` | `parallel { { } { } }` | imported | -| `bpmn:inclusiveGateway` | `parallel { if (c) { } else { } }` | imported | -| `bpmn:eventBasedGateway` | `await { (...) { } (...) { } }` | imported; refused when a branch leads to anything but a catch event or the gateway carries `asyncAfter` (`UnsupportedEventFeatureError`), or without an id (`UnsupportedDocumentError`); `instantiate` and `eventGatewayType` dropped, warned (`unmappedConstruct`) | -| `bpmn:complexGateway` | not authored | refused (`UnsupportedElementError`) | -| `name` on any gateway | not authored | imported and then left out of the script, warned on print (`label`) | -| a listener on any gateway | not authored | dropped, warned (`extensionAttribute`) | +| BPMN element | BPMNscript | Import | +| ------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `bpmn:exclusiveGateway` | `if` / `else if` / `else`, `while`, `do ... while` | imported; refused on a shape `BpmnParse.validateExclusiveGateway` fails, such as two unconditioned routes (`UnsupportedGatewayShapeError`); a route with no condition listed before a conditioned one prints in its place as an `if (true)` head | +| `bpmn:parallelGateway` | `parallel { { } { } }` | imported | +| `bpmn:inclusiveGateway` | `parallel { if (c) { } else { } }` | imported | +| `bpmn:eventBasedGateway` | `await { (...) { } (...) { } }` | imported; refused when a branch leads to anything but a catch event, a branch's catch event has a second incoming flow, or the gateway carries `asyncAfter` (`UnsupportedEventFeatureError`), or without an id (`UnsupportedDocumentError`); `instantiate` and `eventGatewayType` dropped, warned (`unmappedConstruct`) | +| `bpmn:complexGateway` | not authored | refused (`UnsupportedElementError`) | +| `name` on any gateway | not authored | imported and then left out of the script, warned on print (`label`) | +| a listener on any gateway | not authored | dropped, warned (`extensionAttribute`) | ## Events ### Start events -| BPMN element | BPMNscript | Import | -| -------------------------------------------------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -| `bpmn:startEvent` with no definition | `start X` | imported; a second one on the process refused (`UnsupportedEventFeatureError`); any start without an id refused (`UnsupportedDocumentError`) | -| message start | `start X message("Name")` | imported; refused when the name is an expression (`UnsupportedEventFeatureError`) | -| signal start | `start X signal("Name")` | imported | -| timer start | `start X timer("PT1H")`, `at:`, `every:` | imported; refused without an id (`UnsupportedDocumentError`) | -| conditional start | `start X condition(expr)` | imported | -| error, escalation, or compensation start on the process | not authored | refused, since Operaton ignores the trigger (`UnsupportedEventFeatureError`) | -| any other definition on a process start | not authored | refused (`UnsupportedEventDefinitionError`) | -| a start with a definition inside a sub-process or transaction | not authored | refused (`UnsupportedEventFeatureError`) | -| a second start event inside a `bpmn:subProcess` or `bpmn:transaction` | not authored | refused (`UnsupportedEventFeatureError`) | -| an event sub-process start: error, escalation, message, signal, timer, condition | `on (...) { }`, `alongside` when allowed | imported as the handler's trigger; refused with more or fewer than one definition (`UnsupportedEventFeatureError`) | -| an event sub-process start carrying a cancel, link, or terminate definition | not authored | refused (`UnsupportedEventDefinitionError`) | -| an event sub-process start: compensation | `on compensation { }` inside a `subprocess` | imported inside a sub-process or transaction; refused on the process or in another handler (`UnsupportedEventFeatureError`) | +| BPMN element | BPMNscript | Import | +| -------------------------------------------------------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `bpmn:startEvent` with no definition | `start X` | imported; a second plain or timer start on the process refused (`UnsupportedEventFeatureError`); any start without an id refused (`UnsupportedDocumentError`) | +| message start | `start X message("Name")` | imported; refused when the name is an expression (`UnsupportedEventFeatureError`) | +| signal start | `start X signal("Name")` | imported | +| timer start | `start X timer("PT1H")`, `at:`, `every:` | imported; refused without an id (`UnsupportedDocumentError`), or without exactly one non-empty `timeDuration`, `timeDate`, or `timeCycle` (`UnsupportedEventFeatureError`); a timer start beside another plain or timer start refused (`UnsupportedEventFeatureError`) | +| conditional start | `start X condition(expr)` | imported | +| error, escalation, or compensation start on the process | not authored | refused, since Operaton ignores the trigger (`UnsupportedEventFeatureError`) | +| any other definition on a process start | not authored | refused (`UnsupportedEventDefinitionError`) | +| a start with a definition inside a sub-process or transaction | not authored | refused (`UnsupportedEventFeatureError`) | +| a second start event inside a `bpmn:subProcess` or `bpmn:transaction` | not authored | refused (`UnsupportedEventFeatureError`) | +| an event sub-process start: error, escalation, message, signal, timer, condition | `on (...) { }`, `alongside` when allowed | imported as the handler's trigger; refused with more or fewer than one definition (`UnsupportedEventFeatureError`) | +| an event sub-process start carrying a cancel, link, or terminate definition | not authored | refused (`UnsupportedEventDefinitionError`) | +| an event sub-process start: compensation | `on compensation { }` inside a `subprocess` | imported inside a sub-process or transaction; refused on the process or in another handler (`UnsupportedEventFeatureError`) | ### End events @@ -80,7 +80,7 @@ A file holding an id outside ASCII letters, digits, `_`, `-` and `.`, starting w | BPMN element | BPMNscript | Import | | ------------------------------------------------ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `bpmn:intermediateThrowEvent` with no definition | not authored | imported as `step`, warned (`rewritten`): history reports `task` | -| escalation throw | `emit escalation(CODE)` | imported | +| escalation throw | `emit escalation(CODE)` | imported; refused without an `escalationRef`, with one naming no root, or with a root carrying no code (`UnsupportedEventFeatureError`) | | message throw | `emit message("Name")` | imported; refused with a connector (`UnsupportedEventFeatureError`), or a `type` other than `external`, or `external` with no `topic` (`UnsupportedServiceTaskFormError`); `type="external"` with a `topic` imports as a binding, the same rule a message end follows | | signal throw | `emit signal("Name")` | imported | | compensation throw | `emit compensation` | imported; refused with an `activityRef` naming one activity, or `waitForCompletion="false"` (`UnsupportedEventFeatureError`) | @@ -90,39 +90,39 @@ A file holding an id outside ASCII letters, digits, `_`, `-` and `.`, starting w ### Intermediate catch events -| BPMN element | BPMNscript | Import | -| -------------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------- | -| message catch | `await message("Name")` | imported | -| signal catch | `await signal("Name")` | imported | -| timer catch | `await timer("PT1H")` | imported; refused without an id (`UnsupportedDocumentError`) | -| conditional catch | `await condition(expr)` | imported | -| link catch | `await link("Name")` | imported; refused when it names no link or a sequence flow enters it (`UnsupportedEventFeatureError`) | -| a catch with no definition or any other kind | not authored | refused (`UnsupportedEventFeatureError`) | -| `parallelMultiple="true"` | not authored | refused (`UnsupportedEventFeatureError`) | +| BPMN element | BPMNscript | Import | +| -------------------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| message catch | `await message("Name")` | imported | +| signal catch | `await signal("Name")` | imported | +| timer catch | `await timer("PT1H")` | imported; refused without an id (`UnsupportedDocumentError`), or without exactly one non-empty `timeDuration`, `timeDate`, or `timeCycle` (`UnsupportedEventFeatureError`) | +| conditional catch | `await condition(expr)` | imported | +| link catch | `await link("Name")` | imported; refused when it names no link or a sequence flow enters it (`UnsupportedEventFeatureError`) | +| a catch with no definition or any other kind | not authored | refused (`UnsupportedEventFeatureError`) | +| `parallelMultiple="true"` | not authored | refused (`UnsupportedEventFeatureError`) | ### Boundary events -| BPMN element | BPMNscript | Import | -| ---------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| error boundary | `on Host: error(CODE) { }` | imported; refused with `cancelActivity="false"` (`UnsupportedEventFeatureError`) | -| escalation, message, signal, timer, or conditional boundary | `on Host: (...) { }`, `alongside` for `cancelActivity="false"` | imported | -| cancel boundary | `on Block: cancel { }` beside an `attempt` | imported on a `bpmn:transaction` host; refused on any other host, with `cancelActivity="false"`, or as a second cancel boundary (`UnsupportedEventFeatureError`) | -| compensation boundary with its `bpmn:association` and handler activity | not authored; write `on compensation { }` inside a `subprocess` | refused, the message names the rewrite (`UnsupportedEventFeatureError`) | -| any other definition on a boundary | not authored | refused (`UnsupportedEventDefinitionError`) | -| a boundary with no `attachedToRef`, an incoming flow, or no definition | not authored | refused (`UnsupportedEventFeatureError`) | -| a boundary without an id, other than a compensation boundary | not authored | refused (`UnsupportedDocumentError`) | +| BPMN element | BPMNscript | Import | +| ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| error boundary | `on Host: error(CODE) { }` | imported; refused with `cancelActivity="false"` (`UnsupportedEventFeatureError`) | +| escalation, message, signal, timer, or conditional boundary | `on Host: (...) { }`, `alongside` for `cancelActivity="false"` | imported; an escalation boundary refused on a host other than a sub-process, transaction, call activity, or user task, or with an `escalationRef` naming no root (`UnsupportedEventFeatureError`) | +| cancel boundary | `on Block: cancel { }` beside an `attempt` | imported on a `bpmn:transaction` host; refused on any other host, with `cancelActivity="false"`, or as a second cancel boundary (`UnsupportedEventFeatureError`) | +| compensation boundary with its `bpmn:association` and handler activity | not authored; write `on compensation { }` inside a `subprocess` | refused, the message names the rewrite (`UnsupportedEventFeatureError`) | +| any other definition on a boundary | not authored | refused (`UnsupportedEventDefinitionError`) | +| a boundary with no `attachedToRef`, one naming no activity in its own container, an incoming flow, or other than one definition | not authored | refused (`UnsupportedEventFeatureError`) | +| a boundary without an id, other than a compensation boundary | not authored | refused (`UnsupportedDocumentError`) | ## Sequence flow -| BPMN element | BPMNscript | Import | -| --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `bpmn:sequenceFlow` | never written: steps run top to bottom, `goto X` for a jump | imported, one without an id under an id minted from its ends; the printer rebuilds blocks from the flow graph and writes `goto` where no block fits; a route it cannot place is left out, warned on print (`droppedEdge`, `degradedSplit`) | -| `bpmn:conditionExpression`, and a conditional event definition's `bpmn:condition` | the condition of `if`, `while`, `do ... while`, a `parallel` branch, and of `condition(expr)` | imported; refused with a `language` attribute or an `xsi:type` other than `tFormalExpression` (`UnsupportedConditionExpressionError`) | -| `default` on a gateway | `else`, the fallback branch | imported as written (`mapDefaultingGateway`); on an exclusive gateway the shape check above can still refuse the gateway around it | -| `default` on an activity | `else`, the fallback branch | imported when it names one of the activity's own outgoing flows; dropped, warned (`unmappedConstruct`) otherwise | -| no `default` where every route out is conditioned | `else fail`, closing the `if` chain or the `parallel` block | imported; prints as `else fail`, so the engine still stops the run when no condition holds | -| `name` on a sequence flow | not authored, derived from the condition | warned (`label`) when it differs from the derived label | -| a flow into or out of an event sub-process | not authored | refused, `BpmnParse.parseSequenceFlow` fails on it (`UnsupportedEventFeatureError`) | +| BPMN element | BPMNscript | Import | +| --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `bpmn:sequenceFlow` | never written: steps run top to bottom, `goto X` for a jump | imported, one without an id under an id minted from its ends; the printer rebuilds blocks from the flow graph and writes `goto` where no block fits; a route it cannot place is left out, warned on print (`droppedEdge`, `degradedSplit`) | +| `bpmn:conditionExpression`, and a conditional event definition's `bpmn:condition` | the condition of `if`, `while`, `do ... while`, a `parallel` branch, and of `condition(expr)` | imported; refused with a `language` attribute or an `xsi:type` other than `tFormalExpression` (`UnsupportedConditionExpressionError`); a conditional event definition with an empty `bpmn:condition`, an `operaton:variableName`, or an `operaton:variableEvents` refused (`UnsupportedEventFeatureError`) | +| `default` on a gateway | `else`, the fallback branch | imported as written; on an exclusive gateway the shape check above can still refuse the gateway around it | +| `default` on an activity | `else`, the fallback branch | imported when it names one of the activity's own outgoing flows; dropped, warned (`unmappedConstruct`) otherwise; on a multi-instance activity warned (`behaviourChanged`) whether imported or dropped, since the engine fails every run of it on completion | +| no `default` where every route out is conditioned | `else fail`, closing the `if` chain or the `parallel` block | imported; prints as `else fail`, so the engine still stops the run when no condition holds | +| `name` on a sequence flow | not authored, derived from the condition | warned (`label`) when it differs from the derived label | +| a flow into or out of an event sub-process | not authored | refused, `BpmnParse.parseSequenceFlow` fails on it (`UnsupportedEventFeatureError`) | ## Data @@ -136,20 +136,20 @@ A file holding an id outside ASCII letters, digits, `_`, `-` and `.`, starting w ## Collaboration and root elements -| BPMN element | BPMNscript | Import | -| ---------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `bpmn:process`, one marked `isExecutable="true"` | `process X { }`, one per file | imported | -| a second executable `bpmn:process` | not authored | refused (`UnsupportedCollaborationError`) | -| a `bpmn:process` not marked executable beside the executable one | not authored | dropped, warned (`unmappedConstruct`) | -| a lone `bpmn:process` not marked executable | `process X { }` | imported as an executable process, warned (`behaviourChanged`) | -| several `bpmn:process` elements, none marked executable | not authored | refused (`UnsupportedDocumentError`) | -| `bpmn:collaboration`, `bpmn:participant` | not authored | dropped, warned (`unmappedConstruct`), one warning per pool | -| `bpmn:messageFlow` | not authored | dropped, warned (`unmappedConstruct`), one warning per flow | -| `bpmn:laneSet`, `bpmn:lane` | not authored | dropped, warned (`lane`), one warning per lane | -| `bpmn:error`, `bpmn:escalation` | `error CODE(message: "...")`, `escalation CODE` in the process header | imported as declarations under the root's `name`; refused on an error root with an `operaton:errorMessage` and no code, or two roots sharing a code with two messages (`UnsupportedEventFeatureError`); a root with no code warned (`unreferencedRoot`) | -| `bpmn:message`, `bpmn:signal` | synthesized from the names the events use | imported by reference; one nothing references warned (`unreferencedRoot`); two signals sharing a name refused (`UnsupportedEventFeatureError`) | -| `bpmn:category`, `bpmn:interface`, any other root | not authored | dropped, warned (`unmappedConstruct`) | -| `bpmn:import` | not authored | refused (`UnsupportedDocumentError`) | +| BPMN element | BPMNscript | Import | +| ---------------------------------------------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `bpmn:process`, one marked `isExecutable="true"` | `process X { }`, one per file | imported; refused without an id (`UnsupportedDocumentError`) or without a start event (`UnsupportedEventFeatureError`) | +| a second executable `bpmn:process` | not authored | refused (`UnsupportedCollaborationError`) | +| a `bpmn:process` not marked executable beside the executable one | not authored | dropped, warned (`unmappedConstruct`) | +| a lone `bpmn:process` not marked executable | `process X { }` | imported as an executable process, warned (`behaviourChanged`) | +| several `bpmn:process` elements, none marked executable | not authored | refused (`UnsupportedDocumentError`) | +| `bpmn:collaboration`, `bpmn:participant` | not authored | dropped, warned (`unmappedConstruct`), one warning per pool | +| `bpmn:messageFlow` | not authored | dropped, warned (`unmappedConstruct`), one warning per flow | +| `bpmn:laneSet`, `bpmn:lane` | not authored | dropped, warned (`lane`), one warning per lane | +| `bpmn:error`, `bpmn:escalation` | `error CODE(message: "...")`, `escalation CODE` at the top of the process body | imported as declarations under the root's `name`; refused on an error root with an `operaton:errorMessage` and no code, or two roots sharing a code with two messages (`UnsupportedEventFeatureError`); a root with no code warned (`unreferencedRoot`) | +| `bpmn:message`, `bpmn:signal` | synthesized from the names the events use | imported by reference; one nothing references warned (`unreferencedRoot`); an event definition whose `messageRef` or `signalRef` is missing, names no root, or names a root with no `name` refused, as are two signals sharing a name (`UnsupportedEventFeatureError`) | +| `bpmn:category`, `bpmn:interface`, any other root | not authored | dropped, warned (`unmappedConstruct`) | +| `bpmn:import` | not authored | refused (`UnsupportedDocumentError`) | ## Artifacts and diagram diff --git a/docs/decisions/0003-use-langium-as-language-workbench.md b/docs/decisions/0003-use-langium-as-language-workbench.md index 243868e..198f7b2 100644 --- a/docs/decisions/0003-use-langium-as-language-workbench.md +++ b/docs/decisions/0003-use-langium-as-language-workbench.md @@ -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. @@ -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 diff --git a/docs/decisions/0008-fork-camunda-moddle-extension.md b/docs/decisions/0008-fork-camunda-moddle-extension.md index c14c52c..0d3eb86 100644 --- a/docs/decisions/0008-fork-camunda-moddle-extension.md +++ b/docs/decisions/0008-fork-camunda-moddle-extension.md @@ -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. diff --git a/docs/decisions/0010-deterministic-synthesized-ids.md b/docs/decisions/0010-deterministic-synthesized-ids.md index ddc67c9..f99fda8 100644 --- a/docs/decisions/0010-deterministic-synthesized-ids.md +++ b/docs/decisions/0010-deterministic-synthesized-ids.md @@ -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_` and `EndEvent_` 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. @@ -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__`, and any other element `_`, 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_` for every id in the document and so usually imports as `EndEvent__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. @@ -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. diff --git a/docs/decisions/0012-honest-bpmn-import.md b/docs/decisions/0012-honest-bpmn-import.md index 92bce91..41ecb43 100644 --- a/docs/decisions/0012-honest-bpmn-import.md +++ b/docs/decisions/0012-honest-bpmn-import.md @@ -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. @@ -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)). @@ -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. diff --git a/docs/decisions/0013-validator-mirrors-parse-time-rules.md b/docs/decisions/0013-validator-mirrors-parse-time-rules.md index 4deb84c..bdb318b 100644 --- a/docs/decisions/0013-validator-mirrors-parse-time-rules.md +++ b/docs/decisions/0013-validator-mirrors-parse-time-rules.md @@ -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 @@ -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 diff --git a/docs/decisions/0014-restructure-the-ir-into-a-dsl-with-dominator-analysis.md b/docs/decisions/0014-restructure-the-ir-into-a-dsl-with-dominator-analysis.md index a44fec6..b2af91b 100644 --- a/docs/decisions/0014-restructure-the-ir-into-a-dsl-with-dominator-analysis.md +++ b/docs/decisions/0014-restructure-the-ir-into-a-dsl-with-dominator-analysis.md @@ -81,6 +81,9 @@ The block is inclusive when any route carries a condition or the step names a `d The one shape a choice routes the same way, exactly one weighed route beside the `default`, keeps the `if`/`else` the choice prints, which reads better and lowers to the same routing. The fork printer is keyed on a split's id and kind rather than on a gateway, so a step's block takes the merge's `join` settings and no head settings of its own. +A fork with no merge gateway of its kind still prints as a `parallel` block when its routes share no step and each ends or jumps to a step already printed, or when its routes meet at a step that only they enter, the fork's immediate post-dominator (`routesEndApart`, `unsyncedMeet`). +Each branch then ends in a `goto` to that step, which prints after the block, and the compiler leaves out the join no route reaches, so the step runs once per route, as in the model. + A fork or race the catalog cannot fold prints one marker line naming the split and its kind, `split G degraded to jumps; was inclusive`, and then an `if` chain of jumps with each route's condition on its own jump and the split's fallback as the `else`, each jump taking a branch of that chain since a jump ends its block and a second one beside it could never run. The report says the conditions are on the jumps and that the split's kind, whether it opened one branch, every branch whose condition held, or the first to resolve, is written nowhere. An exclusive split with no clean join prints its jumps with no marker: an `if` chain of jumps is a choice, and an exclusive split takes the first route whose condition holds (`ExclusiveGatewayActivityBehavior.doLeave`), so nothing of it is lost. @@ -119,7 +122,7 @@ Either way the fix it names is a rename in the diagram. - Good, because the restructurer reports what it drops instead of leaving the caller to find it, including where the drop changes what a recompiled document runs, such as a degraded split whose kind is written nowhere. - Good, because a synthesized plain end that is not its block's tail is refused loudly instead of rewiring its chain into whatever the printer appended next: the reader finds a reserved id on the page, a print warning, and a validator error naming the element to rename. - Neutral, because a route running straight into an enclosing construct's stop node prints as an empty branch, which the validator warns about; the condition on it survives, where a jump would have lost the branch. -- Neutral, because a step whose routes reconverge at a merge of another kind degrades to jumps under the marker: the block form synchronizes at a merge of its own kind, which the model does not have there. +- Neutral, because a step whose routes reconverge at a merge gateway of another kind degrades to jumps under the marker: the block form synchronizes at a merge of its own kind, which the model does not have there. - Neutral, because a jump into an elided end is still dropped and marked. `forwardToRealTarget` reads the model-level answer, and at the top level the jump is printed before the end's position is known; for a plain end in the same container the re-derived tail end is the same terminal, so the process runs the same and the marker is the only cost. - Neutral, because RPST would recover more structured patterns, for example nested switch-like gotos, but is left for later, once the scope justifies the added machinery. @@ -128,8 +131,8 @@ Either way the fix it names is a rename in the diagram. - Bad, because "tail" for a synthesized end is decidable only after the block's passes, so the emitter carries a deferred list and a resolve step that a reader has to find. - Bad, because the decompiler always produces parseable source but not always source that validates, and not every edge survives. - Bad, because a modeller's document with a default-named plain end that is not last, or that carries a label, prints a reserved id and is refused until renamed, and the import-side documentation warning can only name the two outcomes rather than the one that will happen. -- Bad, because a step whose routes merge at a merge of another kind prints jumps under a marker where an `if` chain would read better, that chain being a choice the model is not. -- Bad, because a branch that ends inside a loop body, a `do { end } while`, a loop gateway the compiler leaves without an incoming flow, and a post-test loop whose body ends inside a branch still print as jumps or markers, the last of them a jump the validator refuses; these are reported and pinned as they are. +- Bad, because a step whose routes meet at a merge gateway of another kind prints jumps under a marker where an `if` chain would read better, that chain being a choice the model is not. +- Bad, because a loop gateway without an incoming flow, the shape `do { end } while` would lower to and the validator refuses in source, has no structured print: in an imported document its unreachable loop test and what only it reaches are left out with a warning, pinned as they are in `tests/goto-fallback.round-trip.test.ts`. ### Confirmation diff --git a/docs/decisions/0015-the-print-declares-every-variable-it-reads.md b/docs/decisions/0015-the-print-declares-every-variable-it-reads.md index 323b61e..0d85c17 100644 --- a/docs/decisions/0015-the-print-declares-every-variable-it-reads.md +++ b/docs/decisions/0015-the-print-declares-every-variable-it-reads.md @@ -31,7 +31,7 @@ Where should the declarations a rebuilt script needs come from? Chosen option: declare on print every variable the body reads bare, typed `any`, because the print already knows every position it writes a variable in, and a third-party document never carries a declaration, so the derivation is needed whatever the XML holds. -The header gets one `var : any` line per name the body reads in a position the script writes bare and nothing types, in first-appearance order over one fixed walk of the model. +The header gets one `var : any` line per name the body reads in a position the script writes bare and nothing types, sorted by name. The positions are the ones the validator reads a variable at: a flow's condition, a loop's count and completion condition, a conditional trigger, an error mapping's condition, an `in` mapping's source, and a bare collection. A body prints bare only when it fits the expression subset, so only those bodies contribute names; a raw `"${...}"` body is opaque to the validator and to the print alike. The typed set is the validator's symbol table, `DefaultVariableSymbolProvider.collect`. @@ -51,13 +51,13 @@ A condition the print had to drop still declares its variable, since the model r A fixture that needs the type back declares the variable on a form, whose type the XML carries. - Bad, because the positions and the typed set are spelled in the language package and mirrored in the transform package. A position or a symbol source added on one side has to be added on the other. - The mirror is stated in one comment on each side, and the round-trip suites turn red on the first program that drifts. + The comment on `variableDecls` states the mirror, and the round-trip suites turn red on the first program that drifts. - Neutral, because a variable a dropped condition read is declared and never read in the printed body. ### Confirmation -The row under "the header declares every variable the body reads bare" in `packages/transform/test/ir-to-dsl.test.ts` prints one process reading a variable at every position and typing one through every source, pinning the whole header and an empty diagnostic list on the re-parse. -The two-pass table in `tests/round-trip-constructs.test.ts` requires an empty diagnostic list on every printed script, and its last row is a condition variable coming back declared and re-printing byte for byte. +The collection rows in `packages/transform/test/ir-to-dsl.test.ts` pin a bare collection declared once at any depth and left undeclared where the element variable, a form field or a catch binding types it. +The two-pass table in `tests/round-trip-constructs.test.ts` requires an empty diagnostic list on every printed script, and its row for a variable a condition reads has the variable come back declared and re-print byte for byte. ## More Information diff --git a/docs/decisions/0017-service-task-bindings.md b/docs/decisions/0017-service-task-bindings.md index 4e706cf..30fd330 100644 --- a/docs/decisions/0017-service-task-bindings.md +++ b/docs/decisions/0017-service-task-bindings.md @@ -52,7 +52,7 @@ The tables behind all of them live once in `packages/language/src/vocabulary.ts` Three extras ride the `topic` binding and no other, because `parseExternalServiceTask` alone reads them. `taskPriority` is a settings key, while a property and an error mapping are member lines of the block the step already opens. -`error when ` is parsed as `ID ID ID Expr`, told apart from a parameter's `ID ID '='` at the third token, so neither `error` nor `when` is reserved. +`error when ` is parsed as `ID Name ID Expr`, told apart from a parameter's `ID Name '='` at the third token, so neither `error` nor `when` is reserved. The code slot is a cross-reference in the scope a `throw error(X)` resolves in, so an undeclared code draws the linker's message, jump-to-definition works, and a `bpmn:error` root is derived for the use as for a throw. The condition is evaluated on the task's execution by `ExternalTaskEntity.evaluateThrowBpmnError`, where `VariableScopeElResolver` resolves `externalTask` to the task entity beside every process variable. In the IR the three sit on the external variant of `ServiceTaskBinding`, so the type cannot carry them under a class, expression, delegate, decision, or built-in binding and every direction's guard stays a type check. diff --git a/docs/decisions/0018-forms-and-field-injection.md b/docs/decisions/0018-forms-and-field-injection.md index 7585231..3122b26 100644 --- a/docs/decisions/0018-forms-and-field-injection.md +++ b/docs/decisions/0018-forms-and-field-injection.md @@ -61,7 +61,7 @@ A member block, where `params+=IoParameter*` already repeats freely, is the only Operaton hands a field list to a class-bound and a delegate-expression-bound listener of either kind exactly as it does to a service task, so `on (...) { }` takes the same rule the task's own block uses. A field is none of the three things a listener's block still refuses to nest: it is not a form, not an input or output parameter, and not a nested listener (ADR-0020). -A field's value has one DSL spelling regardless of which of Operaton's two XML slots it lowers to: a quoted string lowers to the `stringValue` attribute and a `"${...}"` raw expression to an `` child, the same literal-versus-expression split `renderIoValue` already draws for an io parameter's value. +A field's value has one DSL spelling regardless of which of Operaton's two XML slots it lowers to: a quoted string lowers to the `stringValue` attribute and a `"${...}"` raw expression to an `` child. The moddle already normalizes an `expression` attribute into a child element on its own, so a normalizing round trip is not new here. The one shape this rule cannot close is a `stringValue` that reads `${...}`: writing it back would turn a literal Operaton injects verbatim into an expression Operaton evaluates instead, so that shape keeps a drop warning naming the reason rather than being carried. diff --git a/docs/decisions/0019-a-call-activitys-variable-mapping.md b/docs/decisions/0019-a-call-activitys-variable-mapping.md index c906921..88c0f18 100644 --- a/docs/decisions/0019-a-call-activitys-variable-mapping.md +++ b/docs/decisions/0019-a-call-activitys-variable-mapping.md @@ -71,7 +71,7 @@ Nothing on the authoring or the import hop can check that the named class implem ### Confirmation The mapper block in `tests/call-activity.round-trip.test.ts` authors each spelling beside an `in` and an `out` mapping on the same call and asserts the whole IR node, the serialized Operaton attribute, an empty warning list, an identical re-import, the printed setting in its canonical position, and that the printed source revalidates. -The import table in `packages/transform/test/xml-to-ir.test.ts` pins both attributes under both namespace prefixes and the both-set case, asserting the whole mapper object and the whole warning list rather than membership in either. +The import table in `packages/transform/test/xml-to-ir.test.ts` pins a delegate mapper alone and the both-set case, asserting the whole mapper object and the whole warning list rather than membership in either. ## More Information diff --git a/docs/decisions/0020-listeners-reuse-on-inside-the-attribute-block.md b/docs/decisions/0020-listeners-reuse-on-inside-the-attribute-block.md index 99f7d8b..0b394af 100644 --- a/docs/decisions/0020-listeners-reuse-on-inside-the-attribute-block.md +++ b/docs/decisions/0020-listeners-reuse-on-inside-the-attribute-block.md @@ -51,6 +51,8 @@ The event word is a soft word, like a trigger word: every task listener event le `start` and `end` lex as statement keywords instead, so the grammar names those two as the listener event position's exceptions. An `on` clause becomes an execution listener or a task listener by its event word alone, since the two event sets never overlap, so the surface never has to spell the distinction the XML draws between `operaton:executionListener` and `operaton:taskListener`. +A fenced script counts as a listener's binding, so a listener that also sets `class`, `expression`, or `delegate` is refused. + Every `timeout` task listener is written with the id `_timeout_`, `n` counting the task's timeout listeners from one in document order. An id the document already holds is stepped past, the way a synthesized element's id is (ADR-0010). @@ -78,8 +80,8 @@ The timer job's own settings, `jobPriority`, `retryCycle` and `exclusive`, go on ### Confirmation `packages/language/test/validating.test.ts` pins `on assignment` accepted and `on assign` refused with the list, and a parameter on either handler form refused with the parameter-host message. -`packages/transform/test/ir-to-xml.test.ts` pins two timeout listeners on one task written as `_timeout_1` and `_timeout_2`, and the second stepping past a task already holding its id. -`packages/transform/test/xml-to-ir.test.ts` pins `event="assignment"` imported, `event="assign"` refused, and a listener id consumed without a warning. +`packages/transform/test/ir-to-xml.test.ts` pins a timeout listener written as `_timeout_1`. +`packages/transform/test/xml-to-ir.test.ts` pins `event="assignment"` imported and an event a listener's position does not accept refused, and the listeners golden pins a listener id consumed without a warning. The frozen pairs `tests/golden/listeners.{bpmnscript,bpmn}` and `tests/golden/input-output.{bpmnscript,bpmn}` carry the three shapes and import without a warning. ## More Information diff --git a/docs/decisions/0021-operaton-engine-attributes-as-named-ir-fields.md b/docs/decisions/0021-operaton-engine-attributes-as-named-ir-fields.md index 81dc2c9..3f60cc1 100644 --- a/docs/decisions/0021-operaton-engine-attributes-as-named-ir-fields.md +++ b/docs/decisions/0021-operaton-engine-attributes-as-named-ir-fields.md @@ -49,7 +49,7 @@ Chosen options: named vendor-neutral fields on the IR node, a gateway's settings The IR already carries `assignee`, `formKey`, and the service-task and call-activity bindings under plain names, so the naming extends a precedent rather than amending one. Each setting becomes an optional field on the IR node that owns it, named for what it means rather than how it serializes, so the retry cycle is `retryCycle`, not `failedJobRetryTimeCycle`. -Flow-node settings group into one mixin interface and input/output into a second, both as tagged unions so an illegal combination is unrepresentable: `JobSettings` carries the five job settings alone, and `EngineAttributes` is the wider mixin that adds listeners, for everything that keeps a textual identity to attach one to. +Flow-node settings group into one mixin and input/output into a second, `IoMapped`, and the values inside them are tagged unions, so an illegal combination is unrepresentable: `JobSettings` carries the five job settings alone, and `EngineAttributes` is the wider mixin that adds listeners, for everything that keeps a textual identity to attach one to. The naming rule holds at the boundary: values are stored under plain names, and `irToXml` adds the `operaton:` prefix at the one point where it builds the moddle element. The engine reads the five off a gateway as it does off an activity. @@ -94,7 +94,7 @@ The `gateway-settings` golden pair under `tests/golden` carries all five plain a The validator table in `packages/language/test/validating.test.ts` pins the loop and `await` refusals and the pruned-join warning. `packages/transform/test/operaton-moddle.test.ts` pins the typed attribute on the definition and its omission at the default, and `packages/transform/test/ir-to-xml.test.ts` pins the definition and the tag both carrying the lock on a boundary timer and an await, and neither when none is written. `packages/transform/test/xml-to-ir.test.ts` pins the gateway carry, the `asyncAfter` refusal, the definition's value on a boundary and an event sub-process start, the tag's as the fallback, the one warning when the two differ, and the warning under which a timer-started event sub-process's own timer job key is dropped. -`packages/transform/test/ast-to-ir.test.ts` and `packages/transform/test/ir-to-dsl.test.ts` pin the three settings on the synthesized start, the async flags on the sub-process, the lifted head, the authored start keeping its own line, and `droppedSetting`. +`packages/transform/test/ir-to-dsl.test.ts` pins the lifted head, the sub-process's async flags printed ahead of it, the authored start keeping its own line, the three settings back on the synthesized start after a rebuild, and `droppedSetting`. ## More Information diff --git a/docs/decisions/0022-repetition-the-for-clause-and-its-per-run-job-settings.md b/docs/decisions/0022-repetition-the-for-clause-and-its-per-run-job-settings.md index 9f02420..b11e06a 100644 --- a/docs/decisions/0022-repetition-the-for-clause-and-its-per-run-job-settings.md +++ b/docs/decisions/0022-repetition-the-for-clause-and-its-per-run-job-settings.md @@ -83,7 +83,8 @@ Everything Operaton's own parse rejects, a repetition with neither a count nor a `packages/language/test/` pins every form of the clause token by token, its placement against the id and the settings, the output-parameter rule, the seeded variables, and what the position completes to, and the validator table in `validating.test.ts` pins the without-a-clause refusal, the `runJobPriority` refusal, and the repeatable kinds that admit the four `run` keys. `packages/transform/test/` pins the lowering, each refusal by error class and by a substring of its message, each shadowing warning, the emitted child, the loop element's written attributes, the import back into `LoopCharacteristics`, and the printed line. The frozen pair `tests/golden/repetition.{bpmnscript,bpmn}` carries every form of the clause across eight statements of one process and the four `run` keys on at least one repeated statement, and `tests/repetition.round-trip.test.ts` compares the compiled XML byte for byte and requires an import with no warning at all. -`tests/e2e/repetition.test.ts` deploys to a real Operaton and drives it over REST, offering a parallel repetition's three runs at once and a sequential one's one at a time on the same activity, ending a repetition after two runs by completion condition, driving a service task by count, binding each element of a collection in turn, never running the step inside at a count of zero, and showing `runAsyncBefore: true` make the job executor create one job per run rather than one job around the whole loop. +`tests/e2e/repetition.test.ts` deploys to a real Operaton and drives it over REST, offering a parallel repetition's three runs at once and a sequential one's one at a time on the same activity, ending a repetition after two runs by completion condition, driving a service task by count, binding each element of a collection in turn, and never running the step inside at a count of zero. +`tests/e2e/job-settings-and-mail.test.ts` shows `runAsyncBefore: true` make the job executor create one job per run rather than one job around the whole loop. ## More Information diff --git a/docs/decisions/0023-event-roots-from-usage-and-declared-codes.md b/docs/decisions/0023-event-roots-from-usage-and-declared-codes.md index b96d66c..8394f5a 100644 --- a/docs/decisions/0023-event-roots-from-usage-and-declared-codes.md +++ b/docs/decisions/0023-event-roots-from-usage-and-declared-codes.md @@ -110,8 +110,8 @@ This also pre-pays the next event kinds, since `message` and `signal` become val ### Confirmation `packages/transform/test/ir-to-xml.test.ts` asserts the derivation directly: a document using one code from a handler, a throw, and a catch-all handler produces exactly one root element referenced by every coded use and none by the catch-all, with dedicated cases for id sanitization and collision suffixing. -`packages/transform/test/ast-to-ir.test.ts` and `ir-to-dsl.test.ts` pin the branch-tail counterexample as a round-trip case: an intermediate escalation throw as the last statement of a branch block prints as `emit` and re-imports as the same intermediate throw, never an end event. -`packages/language/test/validating.test.ts` pins the soft-word behaviour, `var message: string` and a task named `error` validating cleanly while an unrecognized trigger word produces the options-naming diagnostic, and `scoping.test.ts` pins the scope in both directions, a bare word in a code position resolving to a declaration of its own kind alone and the same word in an `if` condition resolving to nothing. +The golden pairs `tests/golden/compensation` and `tests/golden/engine-attributes` pin the branch-tail counterexample: an `emit escalation` as the last statement of an `if` branch compiles to an intermediate throw that rejoins the flow, and both round trips bring it back as the same throw, never an end event. +`packages/language/test/validating.test.ts` pins the soft-word behaviour, `var message: string` validating cleanly while an unrecognized trigger word produces the options-naming diagnostic, and `scoping.test.ts` pins the scope in both directions, a bare word in a code position resolving to a declaration of its own kind alone and the same word in an `if` condition resolving to nothing. The golden pair `tests/golden/event-handlers.{bpmnscript,bpmn}` carries a declaration whose code no name can spell, `error GatewayTimeout(code: "gateway.timeout")`, and a declaration nothing raises or catches, `error STOCK_UNAVAILABLE(...)`, and both gates see them, the byte comparison against the frozen `.bpmn` and the IR-equality comparison across `DSL -> IR -> XML -> IR -> DSL` (`tests/event-handlers.round-trip.test.ts`). ## More Information diff --git a/docs/decisions/0025-events-at-the-process-boundary.md b/docs/decisions/0025-events-at-the-process-boundary.md index 174a496..dc576df 100644 --- a/docs/decisions/0025-events-at-the-process-boundary.md +++ b/docs/decisions/0025-events-at-the-process-boundary.md @@ -65,7 +65,7 @@ Such a document says something no conforming reading can give it, so the import A standard loop is valid BPMN that the engine does not implement, and refusing it would reject a conforming document that deploys, against the import contract of ADR-0012. Neither clause needs a new reserved word. -`name=ID` is mandatory and comes first in both the `StartEvent` and `EndEvent` rules (`packages/language/src/bpmn-script.langium`), so the second token decides everything: an `ID` is the trigger, a `(` the settings, and a `{` the members. +`name=Name` (an identifier or a backtick-quoted name) is mandatory and comes first in both the `StartEvent` and `EndEvent` rules (`packages/language/src/bpmn-script.langium`), so the next token decides everything: an `ID` is the trigger, a `(` the settings, and a `{` the members. Nothing else can appear there, because every statement in this grammar is keyword-led, and both rules were built into a live Langium parser with Chevrotain's self-analysis on, which reported no ambiguity. `terminate` and the four start words therefore stay soft identifiers, and `var terminate: string` parses beside `end Done terminate`. @@ -97,7 +97,7 @@ No source spells that shape, since `goto` names an authored statement and never - Neutral, because `initiator` is read off every start and set on the process definition by `BpmnParse.parseProcessDefinitionStartEvent`, last one wins, and the validator warns on every start but the last that names one. - Neutral, because a message start whose name is already subscribed by another deployed definition fails in `BpmnDeployer.addMessageStartEventSubscription`, a fact about the deployment rather than the file, and out of this tool's reach. - Bad, because a terminate end always prints, even when its id was synthesized (`isElidedOnPrint`, `packages/transform/src/ir-to-dsl.ts`). - A modeler document decompiles to `end EndEvent_1 terminate`, and re-parsing reports the reserved prefix. + A terminate end carrying the minted id decompiles to `end EndEvent_ terminate`, and re-parsing reports that name as the id generated for the implicit end event. - Bad, because a branch that terminates does not rejoin. An `if` whose branch ends at a terminate prints as a `goto`, and a `parallel` whose branch terminates leaves its join with one incoming flow (`joinContinuation`). - Bad, because the printed form of a process with several starts is not always the authored one: starts entering the same step print back to back above it, even one written after an `end` with a `goto` onto that step. @@ -108,8 +108,8 @@ No source spells that shape, since `goto` names an authored statement and never `packages/language/test/parsing.test.ts` and `validating.test.ts` pin the two clauses token by token and every diagnostic behind them, and the validator rows pin every position a start may take, the chain error, and both warnings. `packages/transform/test/xml-to-ir.test.ts` pins each refusal by error class and message, `ast-to-ir.test.ts` and `ir-to-dsl.test.ts` the lowering and the printed line for every authored shape, and `packages/cli/test/decompile-contract.e2e.test.ts` that a refused trigger exits nonzero and writes no output file. -The frozen pair `tests/golden/event-positions.{bpmnscript,bpmn}` holds a message start, an `emit message`, a `throw message`, and a terminate end, and `tests/event-positions.round-trip.test.ts` compares the compiled XML byte for byte and requires an import with no warning at all; the frozen pair `tests/golden/event-triggers.{bpmnscript,bpmn}` carries the timer, signal, and conditional starts, and the frozen pair `tests/golden/multiple-starts.{bpmnscript,bpmn}` holds four starts across two chains, its suite asserting at every hop that all four keep their trigger and none is the target of a sequence flow. -`tests/e2e/event-positions.test.ts` deploys to a real Operaton through Testcontainers: a correlated message with no instance to aim at creates one, a broadcast signal creates one with both branches active, a timer parks a job, the terminate cancels a sibling branch parked on a user task, and both thrown messages pass the token through to completion, which is what shows an implementation-free message throw deploys and runs. +The frozen pair `tests/golden/event-positions.{bpmnscript,bpmn}` holds a message start, an `emit message`, a `throw message`, and a terminate end, and `tests/event-positions.round-trip.test.ts` compares the compiled XML byte for byte and requires an import with no warning at all; the frozen pair `tests/golden/multiple-starts.{bpmnscript,bpmn}` holds four starts across two chains, a signal start beside two message starts and a plain one, its suite asserting at every hop that all four keep their trigger and none is the target of a sequence flow. +`tests/e2e/event-positions.test.ts` deploys to a real Operaton through Testcontainers: a correlated message with no instance to aim at creates one, a broadcast signal creates one with both branches active, a timer parks a job, the terminate cancels a sibling branch parked on a user task, and both thrown messages, each bound to a delegate, pass the token through to completion. The same suite starts `support-ticket`, a process with a plain and a message start, once by key and once by message, and each instance's history holds exactly its own start and the shared task, which is the assertion a pass-through start would fail. ## More Information diff --git a/docs/decisions/0027-boundary-events-attached-to-an-activity.md b/docs/decisions/0027-boundary-events-attached-to-an-activity.md index b926e2c..221a131 100644 --- a/docs/decisions/0027-boundary-events-attached-to-an-activity.md +++ b/docs/decisions/0027-boundary-events-attached-to-an-activity.md @@ -58,7 +58,7 @@ That is what makes `goto` legal in both directions across a hosted handler's bod The restructuring analysis wires every boundary event to its container's virtual entry, unconditionally, the same way it already wires every start event. This is the honest model: at runtime a token genuinely appears at a boundary event the moment its trigger fires while its host is running, without ever traversing a sequence flow into it. Without this wiring, an escape chain has no immediate dominator at all, and the restructurer can recognize an `if`, a `while`, or a clean join only relative to a dominance relationship, so the chain could never print as anything but unstructured `goto`s. -The accepted trade-off is that a node reachable from both the main flow and an escape chain loses a tight immediate dominator, since neither the main flow's split nor the escape chain alone reaches it unconditionally, so an `if`/`else` whose join such a chain jumps into degrades to plain `goto`s on decompile. +The accepted trade-off is that a node reachable from both the main flow and an escape chain loses a tight immediate dominator, since neither the main flow's split nor the escape chain alone reaches it unconditionally. The dominance result is correct: a region two independent entry points can both reach is genuinely not dominated by either one's split. A boundary event's id is `Boundary__`, collision-resolved against the same document-wide `taken` set the implicit start and end ids already share. @@ -78,7 +78,7 @@ The round-trip normalizer compensates on the comparison side instead, keying a b - Good, because the self-contained body is uniform across interrupting and non-interrupting boundaries, so there is one rule to learn instead of one rule per cancellation mode. - Good, because wiring a boundary event to the virtual entry makes an escape chain's own control flow, including a nested `if`/`else`, restructurable on the same terms as the main flow. - Good, because a host-derived id survives every reordering the decompiler's trailing-position rule performs, without adding a re-key rule for the generation direction. -- Bad, because a node reachable from both the main flow and an escape chain loses a tight immediate dominator, degrading a main-flow `if`/`else` whose join such a chain jumps into into `goto`s on decompile. +- Bad, because a node reachable from both the main flow and an escape chain loses a tight immediate dominator, so the printer has to structure such a region without one. - Bad, because a host-less `on` handler written inside a hosted handler's body lowers into the outer container and therefore guards that whole container rather than just the escape path it is written inside, which follows necessarily from transparency and which an author cannot see from the source alone. - Bad, because two boundary handlers sharing a host and a trigger but differing only in code cannot be told apart by id text until one carries a minted rank. Two such handlers that both import under an id outside the minted pattern still fall back to model order, exposed to whatever order another tool presents them in on a first import. diff --git a/docs/decisions/0031-inclusive-and-event-based-gateways.md b/docs/decisions/0031-inclusive-and-event-based-gateways.md index c3a8c6a..5634704 100644 --- a/docs/decisions/0031-inclusive-and-event-based-gateways.md +++ b/docs/decisions/0031-inclusive-and-event-based-gateways.md @@ -112,7 +112,7 @@ Neither that shape nor the one Operaton refuses is authorable, so the extra widt - Good, because neither construct spends a reserved word; reserving `race` would take `var race: string` and `user race` out of the language. - Good, because a compiled inclusive fork beside an unconditioned branch prints back without a warning and re-parses without an error. - Good, because a `while` whose body ends in a loop compiles. -- Good, because an imported split whose routes are all conditioned and that names no default rebuilds unchanged, the invoice variant included. +- Good, because an imported split whose routes are all conditioned and that names no default rebuilds unchanged when it prints structured, the invoice variant included. - Bad, because a conditioned `parallel` reads as one construct but compiles to either of two BPMN elements, so a BPMN-literate reader must read every branch. - Bad, because `irToDsl` changes its return shape, touching every call site, and the drop that motivated it costs the reader a gateway's name rather than anything the process does. - Bad, because the label comes back on a BPMN-to-BPMN pass but never on a BPMN-to-DSL one, so a decompiled and recompiled document loses every gateway label. diff --git a/docs/decisions/0032-expressions-raw-templates-and-escapes.md b/docs/decisions/0032-expressions-raw-templates-and-escapes.md index 5ad191a..42c7403 100644 --- a/docs/decisions/0032-expressions-raw-templates-and-escapes.md +++ b/docs/decisions/0032-expressions-raw-templates-and-escapes.md @@ -50,4 +50,4 @@ The importer carries a message or signal name opening with `${` as written, sinc ### Confirmation The `renderExpression` table in `packages/language/test/parsing.test.ts` pins the converter's read, both openers and the nested splice, and `packages/transform/test/juel.test.ts` re-parses every printed raw fallback through the grammar and asserts the body it reads back. -`packages/transform/test/ir-to-dsl.test.ts` pins the escaped map key, the one-line io value, the `#{` priority and form default, and `packages/transform/test/xml-to-ir.test.ts` pins the expression names that import and the five body positions that report a rewrite. +`packages/transform/test/ir-to-dsl.test.ts` pins the escaped enum value, the `#{` priority and form default, and `packages/transform/test/xml-to-ir.test.ts` pins the refused expression name on a message start and the rewrite a `#{...}` loop cardinality reports. diff --git a/docs/glossary.md b/docs/glossary.md index 2524836..f07ef91 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -4,61 +4,61 @@ Terms that show up across the READMEs, the ADRs, and the code. ## The domain -- **BPMN 2.0**: Business Process Model and Notation. +- BPMN 2.0: Business Process Model and Notation. The standard for describing business workflows as diagrams of tasks, gateways, and events. BPMNscript compiles to BPMN 2.0 XML. -- **DMN**: Decision Model and Notation, the companion standard for decision tables. +- DMN: Decision Model and Notation, the companion standard for decision tables. A `decide` step names a deployed table by its key and reads back what it returns; the `.dmn` artifact itself is authored and deployed outside this tool. -- **Operaton**: the process engine this project targets. +- Operaton: the process engine this project targets. It loads a BPMN file and executes the workflow it describes. -- **Token**: BPMN's representation of a thread of execution inside a running process instance. +- Token: BPMN's representation of a thread of execution inside a running process instance. It sits on one flow node at a time and moves along sequence flows, splitting into several at a parallel gateway fork and merging back at its join. -- **Job**: what Operaton schedules for anything that runs asynchronously, such as an async continuation or a timer. +- Job: what Operaton schedules for anything that runs asynchronously, such as an async continuation or a timer. A job carries a priority and, on failure, works through a retry cycle before the engine gives up on it. -- **Java delegate**: a Java class implementing Operaton's `JavaDelegate` interface. +- Java delegate: a Java class implementing Operaton's `JavaDelegate` interface. A `service X(class: "...")` step binds to one, and Operaton calls it synchronously in the engine's JVM. -- **External task and worker**: an external task is a service task the engine does not run itself; a `service X(topic: "...")` step publishes work under a topic instead. +- External task and worker: an external task is a service task the engine does not run itself; a `service X(topic: "...")` step publishes work under a topic instead. A worker is the separate process that polls Operaton's REST API, fetches work under that topic, and reports it complete or failed. -- **DSL**: domain-specific language. +- DSL: domain-specific language. A small language built for one narrow purpose. BPMNscript is a DSL for writing BPMN processes as text. -- **DI**: diagram interchange. +- DI: diagram interchange. The `` part of a BPMN file that stores diagram coordinates, meaning where each box sits. BPMNscript generates it on every compile and discards it on import ([ADR-0009](decisions/0009-auto-layout-and-expansion-hint.md)). -- **Multi-instance**: the BPMN marker that makes one activity run several times, once per element of a collection or a fixed number of times. +- Multi-instance: the BPMN marker that makes one activity run several times, once per element of a collection or a fixed number of times. BPMNscript writes it from a `for` clause on the step; Operaton keeps the run counters on the repetition and a `loopCounter` on each run. -- **MIWG**: the Model Interchange Working Group. +- MIWG: the Model Interchange Working Group. It publishes BPMN reference models and interchange conventions, such as explicit ``/`` references on every node, which the exporter follows. ## The toolchain -- **Langium**: the TypeScript toolkit used to build the language. +- Langium: the TypeScript toolkit used to build the language. From one grammar file it generates the parser, the AST types, and a language server. See [ADR-0003](decisions/0003-use-langium-as-language-workbench.md). -- **Grammar**: the file (`bpmn-script.langium`) that defines the DSL's syntax, meaning which keywords and shapes are valid. +- Grammar: the file (`bpmn-script.langium`) that defines the DSL's syntax, meaning which keywords and shapes are valid. Langium turns it into a parser. -- **Parser**: the code that reads `.bpmnscript` text and builds the AST. +- Parser: the code that reads `.bpmnscript` text and builds the AST. Generated by Langium from the grammar. -- **AST**: abstract syntax tree. +- AST: abstract syntax tree. The parser's structured, in-memory representation of a source file, with nodes for each `process`, `user`, `service`, `if`, `while`, `parallel`, and so on. -- **IR**: intermediate representation. +- IR: intermediate representation. A small set of plain objects (`packages/transform/src/ir/types.ts`) that every transform reads from or writes to, in both directions. See [ADR-0007](decisions/0007-intermediate-representation-ast-bpmn.md). [ADR-0021](decisions/0021-operaton-engine-attributes-as-named-ir-fields.md) and [ADR-0020](decisions/0020-listeners-reuse-on-inside-the-attribute-block.md) extend that shape to the engine's execution settings, input/output parameters, and lifecycle listeners. -- **Validator**: checks the rules a parser can't express, such as types and engine constraints, and reports errors in the editor and the CLI. +- Validator: checks the rules a parser can't express, such as types and engine constraints, and reports errors in the editor and the CLI. The full list of checks is in [packages/language/README.md](../packages/language/README.md#diagnostics). -- **LSP**: Language Server Protocol. +- LSP: Language Server Protocol. The standard that lets one language server provide highlighting, autocompletion, and inline errors to any editor that speaks it. Langium provides the server, and the VS Code extension hosts it. -- **moddle / bpmn-moddle**: the library that reads and writes BPMN XML as objects. +- moddle / bpmn-moddle: the library that reads and writes BPMN XML as objects. A _moddle extension_ (here, `operaton-moddle.json`) teaches it about the extra `operaton:` attributes, and about the nested types they can carry, such as an input/output value or a listener binding. Declaring the nested shapes lets an import warning name the step a dropped element sat on ([ADR-0021](decisions/0021-operaton-engine-attributes-as-named-ir-fields.md)). -- **Round trip**: compiling a `.bpmnscript` file to BPMN and decompiling the result back to `.bpmnscript`, or the reverse. +- Round trip: compiling a `.bpmnscript` file to BPMN and decompiling the result back to `.bpmnscript`, or the reverse. The tests use it to check what each direction keeps, warns about, or drops. ## The repository -- **Golden file**: a known-good reference output checked into the repo so a test can diff against it instead of recomputing the expected result. +- Golden file: a known-good reference output checked into the repo so a test can diff against it instead of recomputing the expected result. See [tests/golden/README.md](../tests/golden/README.md). -- **Monorepo / workspace**: this repo holds several npm packages (`packages/*`) in one place; npm "workspaces" link them so they can depend on each other without publishing. +- Monorepo / workspace: this repo holds several npm packages (`packages/*`) in one place; npm "workspaces" link them so they can depend on each other without publishing. See [ADR-0002](decisions/0002-use-mono-repo-structure.md). diff --git a/examples/README.md b/examples/README.md index 6d96b4f..1c19382 100644 --- a/examples/README.md +++ b/examples/README.md @@ -50,6 +50,7 @@ Each source under `spring-boot/processes/` covers one construct or combination, The tests in `tests/e2e/` use [testcontainers](https://testcontainers.com/) to start the Docker image, deploy compiled BPMN over the Operaton REST API, start instances, and check what the engine does. The suite builds the image and starts one engine, and every test file runs against it. `deploy-sweep` deploys every golden under `tests/golden/`, every fixture under `tests/fixtures/`, and every example here, once as written and once rebuilt from the tool's own print of it. +The engine is expected to refuse the `bad-service-task-no-binding` golden, and that golden and `tests/fixtures/error-start.bpmn`, which the importer refuses, deploy only as written. The Docker tests run by default, in CI too, and are skipped only when `SKIP_DOCKER_TESTS=true`. diff --git a/package-lock.json b/package-lock.json index 62d5d72..c06aa2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bpmn-script-workspace", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bpmn-script-workspace", - "version": "0.1.0", + "version": "0.1.1", "license": "Apache-2.0", "workspaces": [ "packages/language", @@ -7160,7 +7160,7 @@ } }, "node_modules/yocto-queue": { - "version": "0.1.0", + "version": "0.1.1", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, @@ -7189,11 +7189,11 @@ }, "packages/cli": { "name": "@bpmn-script/cli", - "version": "0.1.0", + "version": "0.1.1", "license": "Apache-2.0", "dependencies": { - "@bpmn-script/language": "0.1.0", - "@bpmn-script/transform": "0.1.0", + "@bpmn-script/language": "0.1.1", + "@bpmn-script/transform": "0.1.1", "chalk": "^6.0.1", "commander": "^15.0.0", "langium": "^4.4.0" @@ -7223,11 +7223,11 @@ }, "packages/extension": { "name": "vscode-bpmnscript", - "version": "0.1.0", + "version": "0.1.1", "license": "Apache-2.0", "dependencies": { - "@bpmn-script/language": "0.1.0", - "@bpmn-script/transform": "0.1.0", + "@bpmn-script/language": "0.1.1", + "@bpmn-script/transform": "0.1.1", "langium": "^4.4.0", "vscode-languageclient": "~10.1.2" }, @@ -7251,7 +7251,7 @@ }, "packages/language": { "name": "@bpmn-script/language", - "version": "0.1.0", + "version": "0.1.1", "license": "Apache-2.0", "dependencies": { "langium": "^4.4.0", @@ -7268,10 +7268,10 @@ }, "packages/transform": { "name": "@bpmn-script/transform", - "version": "0.1.0", + "version": "0.1.1", "license": "Apache-2.0", "dependencies": { - "@bpmn-script/language": "0.1.0", + "@bpmn-script/language": "0.1.1", "bpmn-auto-layout": "^1.3.0", "bpmn-moddle": "^10.3.1", "langium": "^4.4.0", @@ -7286,11 +7286,11 @@ } }, "tests": { - "version": "0.1.0", + "version": "0.1.1", "license": "Apache-2.0", "devDependencies": { - "@bpmn-script/language": "0.1.0", - "@bpmn-script/transform": "0.1.0", + "@bpmn-script/language": "0.1.1", + "@bpmn-script/transform": "0.1.1", "langium": "^4.4.0", "testcontainers": "^12.2.1", "vitest": "^5.0.5", diff --git a/package.json b/package.json index f30ea54..bc71eb9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bpmn-script-workspace", "description": "Write executable business processes as text, compile to BPMN 2.0 XML, and read existing BPMN files back as text.", - "version": "0.1.0", + "version": "0.1.1", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/cli/README.md b/packages/cli/README.md index ef60d57..233550c 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -40,7 +40,7 @@ The import and print rules behind those warnings are in [packages/transform/READ | `0` | Success, with or without warnings | | `1` | Validation errors or no process on `build`, a BPMN construct the language can't express on `parse` | | `2` | A file or path problem, or input that can't be read as BPMN XML | -| `3` | An unexpected exception inside a transform, which is a bug in `bpmns` | +| `3` | An unexpected exception inside any transform, the importer included, which is a bug in `bpmns` | A command that exits `1` writes nothing. Every message opens with a headline naming what is wrong and where, with its `note:` and `help:` lines indented beneath it. diff --git a/packages/cli/package.json b/packages/cli/package.json index e796085..1c116c0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bpmn-script/cli", "description": "Command line interface compiling BPMNscript to BPMN 2.0 XML and back", - "version": "0.1.0", + "version": "0.1.1", "license": "Apache-2.0", "repository": { "type": "git", @@ -30,8 +30,8 @@ "test": "vitest run" }, "dependencies": { - "@bpmn-script/language": "0.1.0", - "@bpmn-script/transform": "0.1.0", + "@bpmn-script/language": "0.1.1", + "@bpmn-script/transform": "0.1.1", "chalk": "^6.0.1", "commander": "^15.0.0", "langium": "^4.4.0" diff --git a/packages/cli/src/parse.ts b/packages/cli/src/parse.ts index cac71cf..1cf029b 100644 --- a/packages/cli/src/parse.ts +++ b/packages/cli/src/parse.ts @@ -9,6 +9,7 @@ import { readableParseError, xmlInputProblem, UnsupportedConstructError, + MalformedDocumentError, } from '@bpmn-script/transform'; import type { ImportWarning, PrintWarning } from '@bpmn-script/transform'; import { @@ -58,10 +59,11 @@ export async function parseAction( if (err instanceof UnsupportedConstructError) { fail(1, `Error: ${fileName}: ${indentNotes(err.message)}`); } + if (!(err instanceof MalformedDocumentError)) failInternal(err); fail( 2, `Error: failed to parse ${fileName}: ` + - indentNotes(readableParseError((err as Error).message, xml)), + indentNotes(readableParseError(err.message, xml)), ); } diff --git a/packages/cli/test/build-parse.smoke.test.ts b/packages/cli/test/build-parse.smoke.test.ts index 95ca518..3ce2bda 100644 --- a/packages/cli/test/build-parse.smoke.test.ts +++ b/packages/cli/test/build-parse.smoke.test.ts @@ -602,6 +602,18 @@ describe('bpmns build / bpmns parse: guards against destructive or unclear failu " note: BPMN expects '' in the namespace " + "'http://www.omg.org/spec/BPMN/20100524/MODEL'", ), + refusedFile( + 'parse: a sequence flow whose target names no element exits 2 as unreadable input', + 'parse', + 'dangling-flow.bpmn', + '' + + '' + + '' + + '', + 2, + (inputPath) => + `Error: failed to parse ${inputPath}: sequence flow 'f' has no resolvable 'targetRef'`, + ), refusedFile( 'parse: a wrong bpmn namespace names the namespace it found', 'parse', diff --git a/packages/cli/test/internal-failure.test.ts b/packages/cli/test/internal-failure.test.ts index 867ceba..dd4fec0 100644 --- a/packages/cli/test/internal-failure.test.ts +++ b/packages/cli/test/internal-failure.test.ts @@ -1,16 +1,22 @@ // Its own file because `vi.mock` applies to the whole file. import { describe, it, expect, vi } from 'vitest'; -vi.mock('@bpmn-script/transform', async (importOriginal) => ({ - ...(await importOriginal()), - irToXml: vi.fn(() => { - throw new Error('boom in irToXml'); - }), - irToDsl: vi.fn(() => { - throw new Error('boom in irToDsl'); - }), -})); +vi.mock('@bpmn-script/transform', async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + xmlToIr: vi.fn(actual.xmlToIr), + irToXml: vi.fn(() => { + throw new Error('boom in irToXml'); + }), + irToDsl: vi.fn(() => { + throw new Error('boom in irToDsl'); + }), + }; +}); +import { xmlToIr } from '@bpmn-script/transform'; import { runBuild, runParse } from './helpers/actions.js'; const BPMN = ` @@ -34,6 +40,14 @@ describe('an exception inside a transform', () => { () => runParse({ text: BPMN }), 'boom in irToDsl', ], + [ + 'parse exits 3 on an exception inside the importer', + () => { + vi.mocked(xmlToIr).mockRejectedValueOnce(new Error('boom in xmlToIr')); + return runParse({ text: BPMN }); + }, + 'boom in xmlToIr', + ], ])('%s', async (_title, go, boom) => { const run = await go(); diff --git a/packages/extension/README.md b/packages/extension/README.md index 0daade5..21ee27e 100644 --- a/packages/extension/README.md +++ b/packages/extension/README.md @@ -12,9 +12,11 @@ What is specific to this project is the conversion layer: three commands (`bpmns ```bash npm run build npm run package -w packages/extension -code --install-extension packages/extension/vscode-bpmnscript-0.1.0.vsix +code --install-extension packages/extension/vscode-bpmnscript-0.1.1.vsix ``` +To install a downloaded release instead, run `code --install-extension vscode-bpmnscript-0.1.1.vsix` in the folder holding it, or pick the file under Extensions: Install from VSIX... in the command palette. + VS Code then shows the BPMNscript icon in the activity bar and offers `BPMNscript: Compile to BPMN` in the command palette when a `.bpmnscript` file is open. To try a change to the extension without packaging it, press F5 in VS Code from the repo root. diff --git a/packages/extension/package.json b/packages/extension/package.json index 71b18c1..73d0790 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,7 +1,7 @@ { "name": "vscode-bpmnscript", "description": "VS Code extension for BPMNscript, bundling the Langium language server", - "version": "0.1.0", + "version": "0.1.1", "displayName": "BPMNscript", "publisher": "bpmnscript", "license": "Apache-2.0", @@ -113,8 +113,8 @@ "package": "vsce package" }, "dependencies": { - "@bpmn-script/language": "0.1.0", - "@bpmn-script/transform": "0.1.0", + "@bpmn-script/language": "0.1.1", + "@bpmn-script/transform": "0.1.1", "langium": "^4.4.0", "vscode-languageclient": "~10.1.2" }, diff --git a/packages/language/README.md b/packages/language/README.md index 12029a2..afd02ce 100644 --- a/packages/language/README.md +++ b/packages/language/README.md @@ -60,7 +60,8 @@ A value inside an element is documented by a key named for the word that introdu `var : ` declares a process variable so its uses get type-checked. The types are `string`, `number`, `boolean`, `date`, `json`, and `any`. -Reading a variable nothing declares draws a warning, using it against its declared type an error, and declaring one twice an error. +Reading a variable nothing declares draws a warning, and declaring one twice is an error. +Using a declared variable where its type cannot fit, as a condition or an operand of `<`, `<=`, `>`, `>=`, arithmetic, `&&` or `||`, is an error; `==` and `!=` are not checked. A form field, a catch binding, a parameter and a repetition's element declare a variable as well, and the repetition variables listed under [Repetition](#repetition) need no declaration. The importer declares `var x: any` for every variable a printed script reads and nothing else declares ([ADR-0015](../../docs/decisions/0015-the-print-declares-every-variable-it-reads.md)). @@ -126,13 +127,14 @@ An element also carries members, which are not statements: | [`property x = ...`](#external-task-extras) | a key and text on a form field or an external task | `operaton:property` | | [`on create(class: "...")`](#listeners) | code run on a step's lifecycle | execution or task listener | -Every element that takes settings takes the [engine settings](#engine-settings) and an execution listener, and most take input and output parameters. +Every step, event, and handler takes the [engine settings](#engine-settings) and an execution listener, except an `emit link`, and most take input and output parameters. +The gateway statements take the engine settings alone, and the process header takes neither. The ten activities (`user`, `service`, `script`, `step`, `send`, `receive`, `decide`, `subprocess`, `attempt`, and `call`) also take a repetition clause between the name and the parens. Inside an element's braces, `on` declares a listener; at statement position it declares an event handler, which always carries a body of statements ([ADR-0020](../../docs/decisions/0020-listeners-reuse-on-inside-the-attribute-block.md)). A few rules hold for every body: -- A process, subprocess, attempt block, handler body, or loop body can't be empty, and an empty `if` or `parallel` branch draws a warning. +- A process, subprocess, attempt block, or loop body can't be empty, and an empty handler body or an empty `if`, `parallel`, or `await { }` branch draws a warning. - A step after an `end`, a `throw`, a `goto`, an `emit link`, or a block whose every branch ends that way can never run, which is an error, unless a `goto` targets it. A `start`, an `await link`, or a handler can follow such a statement. - A `do` body that always ends or redirects the flow is an error, since nothing would reach the loop's check. @@ -321,7 +323,7 @@ process tally { The tags are `juel`, `javascript`, `js` or `ecmascript`, `groovy`, `python` or `py`, `ruby` or `rb`, and `feel`, in any case. The tag runs to the first whitespace, so on a one-line fence a space separates it from the code. Only `juel` runs on every Operaton installation; the others need a JSR-223 script engine on the engine's classpath, which is looked up the first time the script runs, not at deployment. -An unknown tag, an empty body, and an unterminated fence are errors, here and on every other fenced script (a listener, a parameter value). +An unknown tag and an unterminated fence are errors, and an empty body draws a warning, here and on every other fenced script (a listener, a parameter value). ## What an element carries @@ -336,7 +338,7 @@ Five engine settings are legal on every element that takes settings: `asyncBefore`, `asyncAfter` and `exclusive` are unquoted booleans, and `retryCycle` is quoted. A `retryCycle` the engine cannot read is dropped at deployment rather than refused, so it draws a warning. -`jobPriority` is an integer or a `"${...}"` expression. +`jobPriority` is an integer, a quoted integer, a bare variable name, or a `"${...}"` expression. The job exists only when `asyncBefore` or `asyncAfter` is set, so `exclusive`, `jobPriority`, or `retryCycle` without one of them configures nothing and draws a warning. Two cases are exempt. @@ -440,12 +442,13 @@ A mapping headed by a word other than `error`, missing its `when`, or naming an A listener runs code on a step's lifecycle and goes in the element's braces as `on ` plus what to run. -| Listener | Legal on | Events | -| --------- | ----------------------------- | ----------------------------------------------------------------- | -| execution | every element taking settings | `start`, `end` | -| task | a `user` task | `create`, `assignment`, `complete`, `update`, `delete`, `timeout` | +| Listener | Legal on | Events | +| --------- | ------------------------------------------------- | ----------------------------------------------------------------- | +| execution | every step, event, and handler except `emit link` | `start`, `end` | +| task | a `user` task | `create`, `assignment`, `complete`, `update`, `delete`, `timeout` | A listener runs exactly one thing: a `class`, `expression`, or `delegate` in its parens, valued as on a service task, or a fenced script. +A listener with both a fenced script and a `class`, `expression`, or `delegate` is an error. A `class` or `delegate` listener may open braces holding [fields](#field-injection). `on timeout` carries the timer saying when it fires, an `after`, `at`, or `every` particle and a time, and no other event takes one. The same event may carry several listeners on one element, and each compiles to its own listener in the order written. @@ -543,7 +546,7 @@ A hosted handler (`on Host: ...`) is the exception: its body shares the host's c A host-less handler written inside a hosted handler's body therefore guards the whole surrounding container, not just that body. A `goto` into a `parallel` or `await { }` branch from outside it is an error, since the token would leave the join waiting for branches the run never entered. -A target across a boundary fails to resolve, and the message explains the boundary when the name exists elsewhere in the process: +A `goto` target or a hosted handler's host across a boundary fails to resolve, and the message explains the boundary when the name exists elsewhere in the process: ```text 'X' is inside subprocess 'Y' @@ -561,6 +564,7 @@ A target across a boundary fails to resolve, and the message explains the bounda Its [engine settings](#engine-settings) go in parens on the statement head, after the condition for `if` and the loops. `if`, `parallel`, and `await { }` also create a join, whose settings share the same parens with a `join` prefix: `joinAsyncBefore`, `joinAsyncAfter`, `joinExclusive`, `joinJobPriority`, `joinRetryCycle`. An `if` chain is one gateway however many `else if` branches it has, so an `else if` takes no parens of its own. +A `join` job key without `joinAsyncBefore` or `joinAsyncAfter` beside it configures nothing and draws a warning, as on any element. ```bpmnscript process order-fulfillment { @@ -700,7 +704,7 @@ process payment { A catch binding pairs a field of the event (`code`, and for `error` also `message`) with the variable that receives it, like the `e` in `catch (Exception e)`. Leaving the code out, as in `on error { }`, catches any event of that kind. -`alongside` makes a handler non-interrupting, so the guarded body keeps running beside it. +`alongside`, written as a bare word in the trigger's parens as in `on Review: timer("PT1H", alongside) { ... }`, makes a handler non-interrupting, so the guarded body keeps running beside it. `throw ` raises an event and ends the current path, and `emit ` raises it and continues. `emit link` is the one emit that ends its path, see [Link events](#link-events). @@ -736,14 +740,14 @@ What each trigger means: - `cancel` gives up an `attempt` block, see [Giving a block of work up](#giving-a-block-of-work-up). - `link` is a jump drawn as two events, see [Link events](#link-events). -Two handlers that register the same catch on one scope are an error; a handler inside a block and one attached to that block share the scope. +Two handlers that register the same catch on one scope are an error; for a message or a signal, a handler inside a block and one attached to that block share the scope. So is an escalation catch-all beside a coded escalation catch on the same scope. ### Timers A timer value has to fit its particle: -- a duration (`after`, or a bare value) is an ISO 8601 duration such as `"PT8H"`, alone or beside a start or end date-time, +- a duration (a bare value, or `after` on an `on timeout` listener) is an ISO 8601 duration such as `"PT8H"`, alone or beside a start or end date-time, - `at` is an ISO date-time or a period, - `every` is a repeat such as `"R3/PT10M"`, a six-field cron expression, or a nickname such as `"@daily"`. @@ -908,7 +912,7 @@ The engine limits how many starts a scope has, and each limit is an error: - A process has one plain or timer start, one message or signal start per name, and one condition start per condition. - A body that doesn't open with a `start` gets a plain one from the compiler, which counts, so a plain or timer start further down is a second one. -A process whose starts all carry a message, signal, or condition trigger can't be started by key, so it draws a warning. +A process with two or more starts that all carry a message, signal, or condition trigger can't be started by key, so it draws a warning. The engine shows a start form only on the plain or timer start, so a `form` on any other start draws a warning too. ### Ending every path diff --git a/packages/language/package.json b/packages/language/package.json index 04bfb12..9a8b88b 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,7 +1,7 @@ { "name": "@bpmn-script/language", "description": "Langium grammar, validator and language server for BPMNscript", - "version": "0.1.0", + "version": "0.1.1", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/language/src/bpmn-script-validator.ts b/packages/language/src/bpmn-script-validator.ts index a678213..b40c6a1 100644 --- a/packages/language/src/bpmn-script-validator.ts +++ b/packages/language/src/bpmn-script-validator.ts @@ -3707,18 +3707,11 @@ export class BpmnScriptValidator { listener: Listener, accept: ValidationAcceptor, ): void { - if (listener.script !== undefined) { - checkFencedScript( - listener.script, - `the 'on ${listener.event}' listener`, - { node: listener, property: 'script' }, - accept, - ); - return; - } - + const settings = settingsOf(listener.items); + const subject = `the 'on ${listener.event}' listener`; + const target = { node: listener, property: 'event' } as const; this.checkAttributeKeys( - settingsOf(listener.items).filter( + settings.filter( (setting) => listener.event !== 'timeout' || setting.key !== VALUE_DOCUMENTATION_KEYS.timer, @@ -3728,14 +3721,39 @@ export class BpmnScriptValidator { accept, ); this.checkFlags(listener.items, [], 'a listener', accept); - this.checkExactlyOneBinding( - settingsOf(listener.items), - LISTENER_BINDING_KEYS, - `the 'on ${listener.event}' listener`, - { node: listener, property: 'event' }, + if (listener.script === undefined) { + this.checkExactlyOneBinding( + settings, + LISTENER_BINDING_KEYS, + subject, + target, + accept, + ', nor a fenced script body', + ); + return; + } + + checkFencedScript( + listener.script, + subject, + { node: listener, property: 'script' }, accept, - ', nor a fenced script body', ); + // The script is the binding, so a setting beside it would be dropped. + const written = bindingKeysOf(settings, LISTENER_BINDING_KEYS); + if (written.length > 0) { + accept( + 'error', + diagnosticText( + `${subject} declares more than one binding: ${formatPlainWordList( + [...written.map((key) => `'${key}'`), 'a fenced script body'], + 'and', + )}`, + `note: exactly one of ${formatWordList(LISTENER_BINDING_KEYS)}, or a fenced script body, is allowed`, + ), + target, + ); + } } /** diff --git a/packages/language/test/validating.test.ts b/packages/language/test/validating.test.ts index d047dfa..482d74a 100644 --- a/packages/language/test/validating.test.ts +++ b/packages/language/test/validating.test.ts @@ -314,6 +314,9 @@ const DECISION_BINDINGS = `'class', 'expression', 'delegate', 'topic', 'type', o /** No `type`: a thrown message has no member block for a built-in behaviour's fields. */ const THROW_BINDINGS = `'class', 'expression', 'delegate', or 'topic'`; const LISTENER_BINDINGS = `'class', 'expression', or 'delegate'`; +const scriptListenerBindingConflict = (subject: string) => + `${subject} declares more than one binding: 'class' and a fenced script body\n` + + `note: exactly one of ${LISTENER_BINDINGS}, or a fenced script body, is allowed`; const emptyBinding = (key: string, noun: string) => `setting '${key}' is empty\nhelp: name the ${noun}`; const bindingRequired = (subject: string, keys: string, alternative = '') => @@ -4118,6 +4121,7 @@ checks('Validation - injected fields', [ LISTENER_FIELD_BINDINGS, ), scriptListenerFieldMessage(`the 'on start' listener`), + scriptListenerBindingConflict(`the 'on start' listener`), scriptListenerFieldMessage(`the 'on start' listener`), unknownDirectionMessage('input', ['field']), ], @@ -4245,6 +4249,8 @@ checks('Validation - listeners', [ user U10 { on timeout whenever "PT1H2" (class: "com.example.T3") } user U11 { on start ${FENCE}php\necho 1;\n${FENCE} } user U12 { on start ${FENCE}groovy\n${FENCE} } + user U13 { on start(class: "com.example.C6") ${FENCE}groovy\nx = 1\n${FENCE} } + user U14 { on end(wibble: "w") ${FENCE}groovy\nx = 1\n${FENCE} } }`, [ taskListenerOnly('create', 'a service task'), @@ -4272,6 +4278,8 @@ checks('Validation - listeners', [ unknownParticle('whenever'), unsupportedScriptTag(`the 'on start' listener`, 'php'), emptyScript(`the 'on start' listener`), + scriptListenerBindingConflict(`the 'on start' listener`), + notValidOn('wibble', 'a listener'), ], ], [ diff --git a/packages/transform/README.md b/packages/transform/README.md index e7a5729..5b6002b 100644 --- a/packages/transform/README.md +++ b/packages/transform/README.md @@ -129,7 +129,7 @@ const { source, warnings: printWarnings } = irToDsl(ir); // sync; warns for what ``` `exporterVersion` is written on `bpmn:definitions` and defaults to `0.0.0`. -`src/index.ts` also exports the IR types, `isGateway` and `gatewayDefaultFlowId` for reading them at runtime, the deterministic id helpers, the JUEL parser (`parseJuel`, `renderRawFallback`), the `Unsupported*Error` classes, `LayoutError`, the `ImportWarning` and `PrintWarning` types, and the input checks the CLI and the extension share (`xmlInputProblem`, `readableParseError`). +`src/index.ts` also exports the IR types, `isGateway` and `gatewayDefaultFlowId` for reading them at runtime, the deterministic id helpers, the JUEL parser (`parseJuel`, `renderRawFallback`), the `Unsupported*Error` classes, `LayoutError`, `MalformedDocumentError`, the `ImportWarning` and `PrintWarning` types, and the input checks the CLI and the extension share (`xmlInputProblem`, `readableParseError`). ## The import contract @@ -138,7 +138,8 @@ What it cannot represent falls into two buckets, split where the engine's own pa Content the IR cannot express at all is refused: `xmlToIr` throws a subclass of `UnsupportedConstructError` before producing any IR, so there is no partial output. Content Operaton refuses to deploy is refused the same way, since there is nothing that runs to write back, and the message names the engine method that fails the deployment. -A document whose root is not `bpmn:definitions` or a sequence flow whose `sourceRef` or `targetRef` resolves to nothing is malformed rather than unsupported and throws a plain `Error`, which the CLI reports as a parse failure and exits `2`. +A document that is malformed rather than unsupported, such as one moddle cannot read, one whose root is not `bpmn:definitions`, or one with a sequence flow whose `sourceRef` or `targetRef` resolves to nothing, throws a `MalformedDocumentError`, which the CLI reports as a parse failure and exits `2`. +Any other exception from `xmlToIr` is a bug, and the CLI exits `3`. Content the IR does not carry (an extra Operaton extension attribute, a lane, a text annotation) comes back through the `warnings` array instead. Every drop of a setting the engine reads names the method that reads it and says the document written back runs without it, so a reader can tell a cosmetic drop from one that changes what runs. @@ -156,7 +157,7 @@ A connector is refused under either prefix wherever it sits: beside a service, s What is dropped without a warning is an attribute `xmlToIr` does not read, and it comes in two shapes. One is an attribute in a foreign namespace written directly on a mapped BPMN element, which is where an editor parks its own bookkeeping. The same attribute on an Operaton extension element the IR reads is reported, a whole foreign-namespace extension element is reported too, and an attribute written in no namespace at all that BPMN does not declare is reported as well, since no editor writes there. -The other is a BPMN attribute this surface neither reads nor reports: a process's `processType` or `isClosed`. +The other is a BPMN attribute neither Operaton nor this surface reads, such as a process's `processType` or `isClosed`, or a send task's `messageRef`. Each of those is content left out. A lone `operaton:resource` on a sequence flow's condition expression or a conditional event definition's condition is the one exception that is reported rather than dropped: Operaton reads it only alongside a `language`, so on its own it reaches nothing, and the condition still imports as the expression its body writes. @@ -303,17 +304,17 @@ A call activity's `variableMappingClass` and `variableMappingDelegateExpression` `lane` covers a `bpmn:Lane`, one warning per lane, a lane nested in a `bpmn:childLaneSet` included. The flat IR has no lane concept, so every step lands in one process and the assignment goes. -`label` covers a `name` on an event handler, a typed end event, an intermediate throw, an intermediate catch, or a boundary event. +`label` covers a `name` on an event handler, a throw end event, an intermediate throw, an intermediate catch, or a boundary event. Those read from their trigger and code, so a label has nowhere to render. A link event's `name` is written from its link name on export, so a `name` equal to the link name is derived and a differing one alone is reported. A sequence flow's `name` is reported here when it differs from the label the rebuilt document derives, which is the condition's body inside its `${...}` on a conditioned flow and nothing on a plain one; `name=""`, which the Modeler writes on every unconditioned flow, is not a label. It also covers a label on a start whose id is the one the compiler mints for its container (`StartEvent_`), since a script cannot spell that id back and the statement that would have carried the label is left out whole; a label on an end under such an id (`EndEvent_`, or `EndEvent_` for a boundary escape) makes the end print under its reserved id, and the print reports it. -Any other id prints, respelled if the script cannot spell it, with the print reporting the rename ([ADR-0010](../../docs/decisions/0010-deterministic-synthesized-ids.md)). +Any other id prints as written, in backticks where the `ID` terminal cannot spell it ([ADR-0010](../../docs/decisions/0010-deterministic-synthesized-ids.md)). `unreferencedRoot` covers a `bpmn:Message` or `bpmn:Signal` root that nothing in the process references, a receive task's `messageRef` included. It also covers an error or escalation root carrying no code, which nothing can key it by, unless it is an error root carrying an `operaton:errorMessage`, which refuses; one carrying a code imports as a declaration whether or not anything raises it. -`documentation` covers `bpmn:documentation` at every position where `label` above is warned, for the same reason: an event handler, a typed end event, an intermediate throw, an intermediate catch, a boundary event, and a start or an end whose id is the one the compiler mints for its container. +`documentation` covers `bpmn:documentation` at every position where `label` above is warned, for the same reason: an event handler, a throw end event, an intermediate throw, an intermediate catch, a boundary event, and a start or an end whose id is the one the compiler mints for its container. It also covers a `bpmn:documentation` on a position that reads no `name` at all: the definitions root, an event definition, an external task's `operaton:errorEventDefinition`, an Error/Escalation/Message/Signal root, a multi-instance loop characteristics element, a `bpmn:inputDataItem` without a name, and a sequence flow. A second `bpmn:documentation` child on one element, or one whose `textFormat` is set to anything but plaintext, falls under the category regardless of position. Everywhere else the text is carried onto the IR node alongside its `name` ([ADR-0012](../../docs/decisions/0012-honest-bpmn-import.md)). @@ -364,13 +365,15 @@ On a form field that is a bound on a type its validator refuses, a bound whose ` A literal default the field's type cannot convert goes the same way: a `number` default that is not an integer, which `LongFormType.convertValue` throws on, a `boolean` default outside `true`/`false`, which `BooleanFormType.convertValue` reads as false, and an ISO date default on a `date` field naming no pattern, which `DateFormType` parses under `dd/MM/yyyy`. A shell task's `wait`, `redirectError`, or `cleanEnv` flag spelled in any case but lowercase, such as `wait="TRUE"`, deploys, and `ShellActivityBehavior.readFields` compares it with `"true"` case-sensitively, so the engine reads it as false. A `resultVariable` beside an `external` or a built-in `type` binding on a task deploys and is never written, since `parseServiceTaskLike` hands it to an expression binding alone. -A script whose `scriptFormat` lies outside the fence tags this surface knows imports under that tag, and a `bpmn:script` or `operaton:script` with an empty body imports empty, since `ScriptUtil.getScript` checks the source for null and not for emptiness. +A script whose `scriptFormat` lies outside the fence tags this surface knows imports under that tag. An execution listener with `expression=""` and a task listener with `class=""`, `expression=""`, or `delegateExpression=""` carry the empty text: `ExpressionExecutionListener` and `ExpressionTaskListener` evaluate it, while `ClassDelegateTaskListener` and `DelegateExpressionTaskListener` fail when the listener's event fires. A cancel end and the cancel boundary event that catches it are wired together when Operaton parses the boundary, and nothing but such an end ever reaches such a boundary, so either half alone deploys and then goes wrong at run time. A block holding a cancel end with no cancel boundary attached imports whole, and the warning names the error the engine stops with the first time that end is reached. A cancel boundary on a block nothing inside gives up imports whole too, and the warning names the path that can never run. Refusing either would reject a file the engine deploys although the rebuilt document fails the same way the source does; a boundary event without an id is refused even where it deploys, because the id minted for it would change what runs. +A `bpmn:script` or `operaton:script` with an empty body imports empty and without a warning, since `ScriptUtil.getScript` checks the source for null and not for emptiness, and the printed script draws the validator's empty-body warning when it is built. + ### Print warnings `irToDsl` returns `{ source, warnings }`, and each `PrintWarning` names one thing the script could not carry from the IR, or one it wrote that draws an error when the source is read back. @@ -458,7 +461,7 @@ npm test | `src/cfg-analysis.ts` | `analyzeCfg`: dominator, post-dominator, and back-edge analysis for `irToDsl` | | `src/ir-to-dsl.ts` | `irToDsl`: restructure flat IR into `{ source, warnings }`, degrading to `goto` ([ADR-0014](../../docs/decisions/0014-restructure-the-ir-into-a-dsl-with-dominator-analysis.md)) | | `src/juel.ts` | `parseJuel`, `renderRawFallback`: the JUEL-subset parser and serializer for the import path | -| `src/errors.ts` | `UnsupportedConstructError` and its refusal subclasses | +| `src/errors.ts` | `UnsupportedConstructError` and its refusal subclasses, and `MalformedDocumentError` | | `src/humanize.ts` | `humanize`: the label the export derives from a task or sub-process id | | `src/host-messages.ts` | Messages for files that cannot be converted, shared by the CLI and the extension | | `src/bpmn-moddle.d.ts` | Type declarations for `bpmn-moddle`, `bpmn-auto-layout` and `saxen`, which ship none | @@ -473,7 +476,7 @@ When `bpmn-auto-layout` throws on a validator-clean shape (a `goto` restructurin Operaton deploys it the same, since `BpmnParse.parseDiagramInterchangeElements` reads a diagram only when one is present. The CLI and the extension write that document either way and report the layout failure as a warning. Version 1.x exposes `layoutProcess(xml)` as a flat named export; the `new BpmnAutoLayout()` constructor belongs to the 0.x series and is not used here. -The Operaton namespace comes from `src/operaton-moddle.json`, a trimmed fork of the camunda-bpmn-moddle descriptor ([ADR-0008](../../docs/decisions/0008-fork-camunda-moddle-extension.md)). +The Operaton namespace comes from `src/operaton-moddle.json`, a trimmed fork of the MIT-licensed camunda-bpmn-moddle descriptor ([ADR-0008](../../docs/decisions/0008-fork-camunda-moddle-extension.md)), whose license notice ships beside it in `src/operaton-moddle.LICENSE`. `irToDsl` recognizes structured patterns through the dominator analysis in `cfg-analysis.ts`, and edges that match nothing become `goto` ([ADR-0014](../../docs/decisions/0014-restructure-the-ir-into-a-dsl-with-dominator-analysis.md)). diff --git a/packages/transform/package.json b/packages/transform/package.json index f739a38..af2474a 100644 --- a/packages/transform/package.json +++ b/packages/transform/package.json @@ -1,7 +1,7 @@ { "name": "@bpmn-script/transform", "description": "IR type definitions and bidirectional BPMN transforms for BPMNscript", - "version": "0.1.0", + "version": "0.1.1", "license": "Apache-2.0", "repository": { "type": "git", @@ -42,7 +42,7 @@ "test": "vitest run" }, "dependencies": { - "@bpmn-script/language": "0.1.0", + "@bpmn-script/language": "0.1.1", "bpmn-auto-layout": "^1.3.0", "bpmn-moddle": "^10.3.1", "langium": "^4.4.0", diff --git a/packages/transform/src/errors.ts b/packages/transform/src/errors.ts index e535dd5..00bdd45 100644 --- a/packages/transform/src/errors.ts +++ b/packages/transform/src/errors.ts @@ -35,6 +35,18 @@ export abstract class UnsupportedConstructError extends Error { } } +/** + * Input that cannot be read as a BPMN document at all, such as XML that does + * not parse or a flow whose end names nothing: a host reports it as bad input, + * while any other exception from `xmlToIr` is a bug. + */ +export class MalformedDocumentError extends Error { + constructor(message: string) { + super(message); + this.name = 'MalformedDocumentError'; + } +} + /** * Keeps a quoted value on its headline or note line, and its whitespace * visible: it is often why the value is reported. diff --git a/packages/transform/src/index.ts b/packages/transform/src/index.ts index dcce9e8..46b5b97 100644 --- a/packages/transform/src/index.ts +++ b/packages/transform/src/index.ts @@ -96,6 +96,7 @@ export { UnsupportedErrorMappingError, UnsupportedAssignmentError, LayoutError, + MalformedDocumentError, SERVICE_TASK_FORM_ATTRIBUTES, } from './errors.js'; diff --git a/packages/transform/src/operaton-moddle.LICENSE b/packages/transform/src/operaton-moddle.LICENSE new file mode 100644 index 0000000..8b84b45 --- /dev/null +++ b/packages/transform/src/operaton-moddle.LICENSE @@ -0,0 +1,23 @@ +packages/transform/src/operaton-moddle.json is derived from resources/camunda.json of camunda-bpmn-moddle (https://github.com/camunda/camunda-bpmn-moddle), which is licensed under the MIT License: + +The MIT License (MIT) + +Copyright (c) 2014-2017 camunda Services GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/transform/src/xml-to-ir.ts b/packages/transform/src/xml-to-ir.ts index 24f800f..ad2546f 100644 --- a/packages/transform/src/xml-to-ir.ts +++ b/packages/transform/src/xml-to-ir.ts @@ -91,6 +91,7 @@ import { import { FORM_CONSTRAINT_NAME_LINES, HANDLER_TRIGGER_NOTE, + MalformedDocumentError, oneLine, PLAIN_NAME_NOTE, SERVICE_TASK_FORM_LINES, @@ -816,13 +817,15 @@ async function importDocument( // moddle reports "unparsable content" only for undeclared `operaton:` // elements. - const parsed = await moddle.fromXML(normalizedXml); + const parsed = await moddle.fromXML(normalizedXml).catch((err: Error) => { + throw new MalformedDocumentError(err.message); + }); const { rootElement, elementsById } = parsed; const moddleWarnings = (parsed.warnings ?? []) as ModdleWarning[]; const root = rootElement as ModdleElement; if (root.$type !== 'bpmn:Definitions') { - throw new Error( + throw new MalformedDocumentError( `expected the root element 'bpmn:Definitions', got '${root.$type}'`, ); } @@ -6539,7 +6542,9 @@ function requireFlowEndpoint( ): string { const endpoint = el.get(property) as ModdleElement | undefined; if (endpoint === undefined || endpoint.id === undefined) { - throw new Error(`sequence flow '${id}' has no resolvable '${property}'`); + throw new MalformedDocumentError( + `sequence flow '${id}' has no resolvable '${property}'`, + ); } return endpoint.id; } @@ -6565,7 +6570,9 @@ function eventDefinitionsOf(el: ModdleElement): ModdleElement[] { function requireId(el: ModdleElement): string { if (el.id === undefined || el.id === '') { - throw new Error(`'${el.$type}' is missing its required 'id' attribute`); + throw new MalformedDocumentError( + `'${el.$type}' is missing its required 'id' attribute`, + ); } return el.id; } diff --git a/tests/golden/README.md b/tests/golden/README.md index 8657541..7241829 100644 --- a/tests/golden/README.md +++ b/tests/golden/README.md @@ -57,7 +57,7 @@ Each pair has a round-trip test at `tests/.round-trip.test.ts`, and `tests Every pair but `intermediate-catch` also asserts that the frozen `.bpmn` imports without a single warning and re-desugars back to the IR the fixture compiled to. Nested shapes stay inside their parent only through the `isExpanded` hint from `irToXml`, without which a disconnected event sub-process leaks into the root plane. Per-fixture extras, such as which root elements must be shared or how strictly the restructured DSL is validated, live in the test file. -`tests/e2e/deploy-sweep.test.ts` deploys every frozen `.bpmn` here, every fixture under `tests/fixtures/`, and every example, on the engine, both as written and rebuilt from its own print; that suite lives outside the pair suites and runs once per file rather than once per pair. +`tests/e2e/deploy-sweep.test.ts` deploys every frozen `.bpmn` here, every fixture under `tests/fixtures/`, and every example, on the engine, both as written and rebuilt from its own print, except that the engine is expected to refuse `bad-service-task-no-binding` and the two files the importer refuses deploy only as written; that suite lives outside the pair suites and runs once per file rather than once per pair. Run the pair tests with `npm test --workspace tests`, or one pair with `npm test --workspace tests -- forms.round-trip.test.ts`. @@ -67,7 +67,7 @@ Three rules keep a pair comparable across the round trip. - Unless a pair says otherwise, a variable read by a condition or an expression is declared as a form field rather than a `var`, so the declaration comes back out of the XML on import; the IR carries no `var` line. - Every `throw`, `emit` and link catch is named, so its printed id re-parses unchanged; an unnamed one re-derives a coordinate id that moves when the restructured DSL reorders statements. -- No label equals the name humanized from its id, because the importer drops a label it can derive and the fixture would come back with it missing. +- No process or activity label equals the name humanized from its id, because the importer drops a label it can derive there and the fixture would come back with it missing. ## `invoice-approval-handwritten.bpmn` @@ -80,7 +80,7 @@ This is the input for the XML to IR direction. ## `invoice-approval-generated.bpmn` The frozen output of the full pipeline, checked in: `irToXml(astToIr(parse(examples/spring-boot/processes/invoice-approval.bpmnscript)))`. -The `irToXml` test (`packages/transform/test/ir-to-xml.test.ts`, describe block "irToXml: full-pipeline golden diff") reproduces that pipeline and compares byte-for-byte, so any accidental change to the parser, the desugarer, or the serializer shows up as a failed diff. +The `irToXml` test (`packages/transform/test/ir-to-xml.test.ts`, "serializes the invoice example byte-for-byte as the generated golden" under "irToXml: document") reproduces that pipeline and compares byte-for-byte, so any accidental change to the parser, the desugarer, or the serializer shows up as a failed diff. `tests/e2e/deploy-sweep.test.ts` deploys this frozen file as written, while `tests/e2e/invoice-approval.test.ts` deploys the same source compiled fresh through the CLI; the two documents differ in `exporterVersion` alone (see "Regenerating a frozen output" above). Contract: the process id `invoice-approval`, the userTask ids `ReviewInvoice` and `SeniorApproval`, `operaton:class="com.example.invoice.AutoApproveDelegate"`, the `demo` and `manager` assignees, and the condition `${amount > 1000}`. diff --git a/tests/package.json b/tests/package.json index e878d25..864c515 100644 --- a/tests/package.json +++ b/tests/package.json @@ -1,7 +1,7 @@ { "name": "tests", "description": "Integration and E2E tests for BPMNscript", - "version": "0.1.0", + "version": "0.1.1", "license": "Apache-2.0", "repository": { "type": "git", @@ -20,8 +20,8 @@ "clean": "node -e \"for (const f of [...fs.globSync('*.tsbuildinfo'), 'out']) fs.rmSync(f, { recursive: true, force: true })\"" }, "devDependencies": { - "@bpmn-script/language": "0.1.0", - "@bpmn-script/transform": "0.1.0", + "@bpmn-script/language": "0.1.1", + "@bpmn-script/transform": "0.1.1", "langium": "^4.4.0", "testcontainers": "^12.2.1", "vitest": "^5.0.5",