diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ccdf87f..26471959 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,16 +53,16 @@ them on a private corpus without copying it into the repository, are described i | `domain/commands/` | the commands a caller can ask for (`EditCommand`: `AddElement`, `PlaceShapes`, ...), the ops JSON of `apply` and its aliases | | `domain/diagram/` | the drawing (BPMN DI): `di/` reads and writes planes, frames, groups and the checkpoint every engine shares, `layout/` holds the engines (`clean/` redraws with `FullLayout`, `incremental/` keeps a hand-made drawing), `DrawnDiagram` is the drawing the diagram commands change, `commands/` holds one handler per diagram command (`ShapePlacement`, `ShapeAlignment`, `SpaceInsertion`, ...) and `DiagramCommands`, which dispatches them; `BatchDrawing` is all a batch does to the drawing | | `domain/editing/` | what changes the model, one folder per concern: `elements/` (add, remove, retype, move, order, placement, artifacts, pools, lanes), `connections/` (connect, associations, sequence flows, bridges, flow ids, split), `properties/` (set, value coercion, the set keys, nested slots, vendor elements, decision links), `events/` (triggers, event definitions, root elements, the event-gateway rule), `extensions/` (ext), `ids/` (id renames); a command is a class created with the `EditSession` (`new ElementCreation(edit).add(op)`, `new Connections(edit).connect(op)`, ...) | - | `domain/validation/` | structural validation (`StructuralValidator`, `FlowOrder`), the validator hook, the `FindingBaseline` a write compares with and the `FindingMatcher` that tells the same finding before and after an edit; `profiles/` holds the engine rules that load on demand | + | `domain/validation/` | structural validation (`StructuralValidator`, `FlowOrder`), the validator hook, the `FindingBaseline` a write compares with and the `FindingMatcher` that tells the same finding before and after an edit; `PlatformCheck` adds the Camunda 7 / 8 findings of the injected `EngineLint` | | `domain/views/` | the read views (`show`, `find`, `--around`, `--context`) and `DrawableDoc`, a document ready to draw (one without a diagram laid out in memory) | | `domain/batch/` | one batch of commands on a document in memory: `EditBatch` (guard, commands, validation, layout, kept text, result), `ModelCommands` hands each model command to its domain service and `BatchDrawing` (`domain/diagram/`) lays the drawing out between them; the top of the domain, nothing imports it back | | `application/port/in/` | one contract per use case, the way in: an interface with its `Command` and `Result` (`EditDocumentUseCase`, `RelayoutDiagramUseCase`, `ValidateDocumentUseCase`, `ShowModelUseCase`, ...), also for a pure query that needs no outbound port | - | `application/port/out/` | what the application needs from outside, named after what it does: `DocumentPort` (load a document, resolve where a result goes, save it), `LoadProjectSettingsPort` (the `.bpmnlintrc`), `RenderDiagramPort` (a plane as SVG), `WriteImagePort` | + | `application/port/out/` | what the application needs from outside, named after what it does: `DocumentPort` (load a document, resolve where a result goes, save it), `LoadProjectSettingsPort` (the `.bpmnlintrc`), `RenderDiagramPort` (a plane as SVG), `EngineLintPort` (the Camunda 7 / 8 rules), `WriteImagePort` | | `application/service/` | one service per use case that implements its contract (`EditDocumentService implements EditDocumentUseCase`): load, let the domain work, save, all through the outbound ports; a query asks the domain and returns its answer | | `adapters/in/cli/` | the driving adapter: the `Cli` (commander, streams, exit codes, errors) and one class per command; it knows the use cases only by their contracts and reaches the domain only through them; from the domain it takes the command vocabulary to check its flags (errors, op rules, flag values) | | `adapters/in/report/` | what a use case's result looks like to the one who asked: text and JSON reports (`MutationText`, `MutationReportBuilder`, `ValidationText`, ...), the views as text, and the reference the guide and `bpmn kinds` print; the CLI prints them and `index.ts` / `api.ts` return them | - | `adapters/out/bpmnlint/` | the injected bpmnlint as a validator of the domain, browser-safe: `BpmnlintConfigResolution` (the effective `.bpmnlintrc`), `BpmnlintValidator`, `IdConventionsReader` (the id conventions of `element-id-naming`) | - | `adapters/out/files/` | the driven adapters on disk, Node only: `FileDocumentStore`, `BpmnlintProjectSettings` (loads the `.bpmnlintrc` and its plugins for `adapters/out/bpmnlint/`), `FileImageWriter` (PNG via resvg) | + | `adapters/out/bpmnlint/` | the injected bpmnlint as a validator of the domain, browser-safe: `BpmnlintConfigResolution` (the effective `.bpmnlintrc`), `BpmnlintValidator`, `IdConventionsReader` (the id conventions of `element-id-naming`); `BpmnlintEngineLint`, the Camunda 7 / 8 rules (the `EngineLintPort`), which loads `engine/` on demand: camunda-compat's rules (`compatRules`) and bpmn-cli's own rules (seventy for Camunda 7, fifty for Camunda 8: `engine/rules/c7|c8/`, one rule per file and one check per rule, next to the `support/` helpers of that engine, the shared helpers in `engine/rules/support/`, the rule bases both engines share in `engine/rules/`; a rule is named after what must hold, the id `--` and the class `Rule` in a file of its name (`c7-message-needs-name`, `C7MessageNeedsNameRule.ts`), the requirement `needs-` for what must be there, `no-` for what must not, or `known`, `valid`, `unique`, `allowed`, `supported`, `placement`, `resolves` for a value, a name or a place the engine accepts; it declares one `code` and `impact` in `defineRule`, several rules may share a code; an abstract base without `defineRule` (`C7ExecutableRule`, `C8FeelRule`, ...) holds what two or more rules check alike; the rules on what a typed parse drops read the file as written through `WrittenModel`, written on `EngineRule` of `@miragon/bpmnlint-plugin-rules/engine` and run in one walk with its `lint`), mapped to `W_C7_*` / `W_C8_*` by the engine run in `engine/run/` (`EngineRun`, `EngineFindings`, `EngineCodes`, `EngineHint`; `CompatVerdicts` holds where bpmn-cli reads camunda-compat differently); `engine/` gets the core from `engine-kit.ts` (a rule chunk must not import the entry) | + | `adapters/out/files/` | the driven adapters on disk, Node only: `FileDocumentStore`, `BpmnlintProjectSettings` (loads the `.bpmnlintrc` and its plugins for `adapters/out/bpmnlint/`), `FileImageWriter` (PNG) | | `adapters/out/document/` | `DocumentStore`, what the file store and the in-memory store share behind the `DocumentPort`: a source parsed into a `Doc`, the lossless check, the target of a write and whether to write; browser-safe | | `adapters/out/render/` | the SVG renderer behind the `RenderDiagramPort`, browser-safe: `SvgRenderer` (the port), `DocRenderer` (one plane, start to end), `crop/` (which part of the plane), `paint/` (the shapes, connections and labels as SVG), `compose/` (frame, scale, badges, tiles) | | `adapters/out/memory/` | the driven adapters of the in-memory API: `NoDocumentFiles`, `NoProjectSettings` | @@ -119,9 +119,10 @@ them on a private corpus without copying it into the repository, are described i `bpmn-moddle` (the semantic model; `moddle` for its types), `@resvg/resvg-wasm` (the PNG rasteriser of `bpmn render`), `commander` (the CLI only), and `bpmnlint` (pinned to `11.15.0`) with -`@miragon/bpmnlint-plugin-rules` (pinned to `0.12.1`: bpmnlint's rules, camunda-compat and -Miragon's rules behind one resolver), loaded only when a `.bpmnlintrc` applies (see -[bpmnlint](docs/bpmnlint.md)). Both are pinned exactly: they decide which findings an agent sees, +`@miragon/bpmnlint-plugin-rules` (bpmnlint's rules, camunda-compat and Miragon's rules behind one +resolver). They run when a `.bpmnlintrc` applies (see [bpmnlint](docs/bpmnlint.md)), and the plugin's +`./camunda-7` and `./camunda-8` entries are the Camunda 7 / 8 rules of every check, loaded on demand +(see [Camunda 7 and Camunda 8](docs/camunda.md#where-the-checks-come-from)). Both are pinned exactly: they decide which findings an agent sees, the tests pin those findings, and bpmn-cli uses bpmnlint's `Linter` beyond its documented API. `camunda-bpmn-moddle` (pinned to `8.0.1`) is a development dependency: its Camunda 7 descriptor diff --git a/docs/bpmnlint.md b/docs/bpmnlint.md index 3034bf89..fe213c37 100644 --- a/docs/bpmnlint.md +++ b/docs/bpmnlint.md @@ -52,6 +52,11 @@ and `@miragon/rules` come with bpmn-cli and need no install. Keep the `moddleExtensions` entry. Without it camunda-compat sees no `camunda:` or `zeebe:` properties and reports them missing, as bpmnlint does in CI. +bpmn-cli runs camunda-compat and the Miragon engine layer for every Camunda 7 / 8 file anyway, as +its `W_C7_*` / `W_C8_*` findings ([Camunda 7 and Camunda 8](camunda.md#where-the-checks-come-from)). +Extend them in the `.bpmnlintrc` when CI runs `npx bpmnlint` too: the same findings are then listed +twice, once under their code and once under the rule name. + ::: warning Untrusted repositories A `.bpmnlintrc` can name plugins of the repository, and `bpmn validate` then runs their code, as `npx bpmnlint` would. In a repository you do not trust, pass `--no-lint-config`. See diff --git a/docs/camunda.md b/docs/camunda.md index 48513f03..821859af 100644 --- a/docs/camunda.md +++ b/docs/camunda.md @@ -130,16 +130,151 @@ with the fix: $ bpmn set invoice.bpmn flow_amountOver1000ToApproveInvoice 'condition=${amount > 1000}' ... $ bpmn validate invoice.bpmn --strict -W_C8_DEPLOY_EXPRESSION flow_amountOver1000ToApproveInvoice: The condition "${amount > 1000}" of sequence flow flow_amountOver1000ToApproveInvoice is no FEEL expression; Camunda 8 needs one starting with = and refuses the file (`bpmn set flow_amountOver1000ToApproveInvoice 'condition== amount > 1000'` (check the FEEL syntax: and / or, = for equality).) +W_C8_DEPLOY_EXPRESSION flow_amountOver1000ToApproveInvoice: The condition "${amount > 1000}" of sequence flow flow_amountOver1000ToApproveInvoice is no FEEL expression; Camunda 8 needs one starting with = and refuses the file (`bpmn set flow_amountOver1000ToApproveInvoice condition==`.) platform: c8 (modeler:executionPlatform "Camunda Cloud") - 1 refused at deploy, 0 runtime, 0 practice finding(s) layout: ok valid, 1 warning(s) ``` +## Where the checks come from + +The `W_C7_*` / `W_C8_*` findings come from bpmnlint rules, loaded the first time a file of that +engine is checked and run in one walk of the file by the engine entry point of +`@miragon/bpmnlint-plugin-rules` (`lint`): + +- camunda-compat's rules for the file's `modeler:executionPlatformVersion` (`compatRules` of the + plugin's `camunda-7` / `camunda-8` entry; the latest without one; `--json` platform names the + version); +- bpmn-cli's own rules for what camunda-compat leaves out, seventy for Camunda 7 and fifty for + Camunda 8 in `src/adapters/out/bpmnlint/engine/rules/c7|c8/`, written on the plugin's + `EngineRule` (`bpmn-cli/`). Each rule checks one thing, is named after what must hold + (`c7-message-needs-name`, `c8-error-code-unique`) and declares the code and severity of its + findings; several rules may share a code (the tables below list them by code). It gives its hint + with the finding; +- among them the rules on content the engine's parser drops or rewrites, which read the file as + written: `c7-bpmn-attribute-known`, `c7-camunda-boolean-valid`, `c7-io-parameter-in-input-output`, + `c7-camunda-element-known` and `c8-bpmn-attribute-known`, the four `c8-publish-message-*` rules, + `c8-zeebe-element-placement`, `c8-zeebe-element-known`. + +bpmn-cli maps each camunda-compat rule to its code. The hint of a camunda-compat finding is built +from where it points; its message is camunda-compat's. A rule's severity says what the finding +means: `error` is deploy, `warn` runtime, `info` practice. + +Where camunda-compat reads a file differently than Camunda 8.9 (engine-checked), bpmn-cli drops its +finding and follows the engine: white space only is a value for a job type, a message, signal, +process or decision name, a result variable, a listener type, an error or escalation code and a link +name; timer values are read as Camunda 8 parses them (`bpmn-cli/c8-timer-value-valid`); a +`zeebe:jobPriorityDefinition` deploys on 8.9; a condition on a flow out of a gateway is the +structural `W_CONDITION_IGNORED`; of two none start events or two link catch events with one name, +the second is reported; an element with a doubled extension element reports the doubling only. + +Where camunda-compat is stricter than Camunda 8.9, bpmn-cli follows camunda-compat: + +- a flow without a condition next to conditional ones out of an exclusive or inclusive gateway + without a default is refused at deploy (`W_C8_EXCLUSIVE_GATEWAY`, reported on the flow), as is a + condition on a flow from an element that is no gateway (`W_C8_CONDITION_IGNORED`); +- an error or escalation that a catch event references needs its code (`W_C8_DEPLOY_ERROR`, + `W_C8_DEPLOY_ESCALATION`); +- execution listeners `beforeAll` and `cancel` are accepted from 8.10 on; +- an intermediate catch or boundary event without an event definition is an unsupported element + (`W_C8_DEPLOY_UNSUPPORTED`); +- a target an event-based gateway may not lead to is reported on the target, not the gateway + (`W_C8_DEPLOY_EVENT_GATEWAY`). + +bpmn-cli switches camunda-compat's `user-task-definition` off (a user task runs without a form), +reports `zeebe-user-task` as practice and checks FEEL with its own rules +(`bpmn-cli/c8-feel-expression-valid` and the `bpmn-cli/c8-*-needs-feel` rules, camunda-compat's +`feel` is off). + +
+Camunda 7: code and rules + +| code | rules | +| --- | --- | +| `W_C7_BAD_VALUE` | `bpmn-cli/c7-binding-known`, `bpmn-cli/c7-user-task-priority-valid`, `bpmn-cli/c7-variable-events-known`, `bpmn-cli/c7-camunda-boolean-valid` | +| `W_C7_DANGLING_REF` | `bpmn-cli/c7-camunda-reference-resolves` | +| `W_C7_DEPLOY_AD_HOC_SUBPROCESS` | `bpmn-cli/c7-ad-hoc-sub-process-no-flows`, `bpmn-cli/c7-ad-hoc-sub-process-no-multi-instance` | +| `W_C7_DEPLOY_BAD_VALUE` | `bpmn-cli/c7-job-priority-valid`, `bpmn-cli/c7-decision-result-mapper-known` | +| `W_C7_DEPLOY_BINDING_VERSION` | `bpmn-cli/c7-binding-needs-version` | +| `W_C7_DEPLOY_CALLED_ELEMENT` | `bpmn-cli/c7-call-activity-one-called-element` | +| `W_C7_DEPLOY_COMPLEX_GATEWAY` | `bpmn-cli/c7-complex-gateway-no-flows` | +| `W_C7_DEPLOY_ERROR` | `bpmn-cli/c7-error-end-event-needs-error`, `bpmn-cli/c7-error-needs-code` | +| `W_C7_DEPLOY_ESCALATION` | `bpmn-cli/c7-escalation-throw-needs-escalation`, `bpmn-cli/c7-escalation-needs-code`, `bpmn-cli/c7-escalation-boundary-host-allowed` | +| `W_C7_DEPLOY_EVENT_DEFINITION` | `bpmn-cli/c7-timer-needs-one-time-value`, `bpmn-cli/c7-compensation-names-own-activity`, `bpmn-cli/c7-conditional-event-needs-condition` | +| `W_C7_DEPLOY_EVENT_GATEWAY` | `bpmn-cli/c7-event-gateway-target-allowed`, `bpmn-cli/c7-event-gateway-branches-unique` | +| `W_C7_DEPLOY_EXCLUSIVE_GATEWAY` | `bpmn-cli/c7-exclusive-gateway-needs-outgoing`, `bpmn-cli/c7-exclusive-gateway-default-no-condition`, `bpmn-cli/c7-exclusive-gateway-needs-conditions` | +| `W_C7_DEPLOY_EXTENSION` | `bpmn-cli/c7-call-mapping-needs-source-and-target`, `bpmn-cli/c7-connector-needs-id`, `bpmn-cli/c7-io-parameter-needs-name`, `bpmn-cli/c7-form-field-needs-id-and-type`, `bpmn-cli/c7-field-needs-one-value` | +| `W_C7_DEPLOY_EXTERNAL_TOPIC` | `bpmn-cli/c7-external-task-needs-topic` | +| `W_C7_DEPLOY_FORM` | `bpmn-cli/c7-form-ref-no-form-key`, `bpmn-cli/c7-form-ref-needs-binding` | +| `W_C7_DEPLOY_HISTORY_TTL` | `camunda-compat/history-time-to-live`, `bpmn-cli/c7-history-time-to-live-valid` | +| `W_C7_DEPLOY_IMPLEMENTATION` | `bpmn-cli/c7-task-type-supported`, `bpmn-cli/c7-shell-task-needs-command`, `bpmn-cli/c7-shell-field-values-fixed`, `bpmn-cli/c7-task-needs-implementation` | +| `W_C7_DEPLOY_INPUT_OUTPUT` | `bpmn-cli/c7-io-mapping-host-allowed`, `bpmn-cli/c7-output-mapping-host-allowed` | +| `W_C7_DEPLOY_LINK` | `bpmn-cli/c7-link-catch-name-unique`, `bpmn-cli/c7-link-throw-needs-catch`, `bpmn-cli/c7-link-event-needs-name` | +| `W_C7_DEPLOY_LISTENER` | `bpmn-cli/c7-listener-needs-implementation`, `bpmn-cli/c7-listener-event-known`, `bpmn-cli/c7-listener-script-needs-format`, `bpmn-cli/c7-timeout-listener-needs-timer-and-id` | +| `W_C7_DEPLOY_MESSAGE` | `bpmn-cli/c7-message-subscription-unique`, `bpmn-cli/c7-message-event-needs-message`, `bpmn-cli/c7-message-needs-name` | +| `W_C7_DEPLOY_MULTI_INSTANCE` | `bpmn-cli/c7-multi-instance-needs-cardinality-or-collection`, `bpmn-cli/c7-element-variable-needs-collection` | +| `W_C7_DEPLOY_RESULT_VARIABLE` | `bpmn-cli/c7-result-variable-needs-expression` | +| `W_C7_DEPLOY_SCHEMA` | `bpmn-cli/c7-bpmn-attribute-known` | +| `W_C7_DEPLOY_SCRIPT` | `bpmn-cli/c7-script-task-needs-script` | +| `W_C7_DEPLOY_SIGNAL` | `bpmn-cli/c7-signal-name-unique`, `bpmn-cli/c7-signal-subscription-unique`, `bpmn-cli/c7-signal-event-needs-signal`, `bpmn-cli/c7-signal-needs-name` | +| `W_C7_DEPLOY_START_EVENT` | `bpmn-cli/c7-scope-needs-start-event`, `bpmn-cli/c7-start-event-unique` | +| `W_C7_EXCLUSIVE_GATEWAY_DEFAULT` | `bpmn-cli/c7-exclusive-gateway-needs-default` | +| `W_C7_FOREIGN_CONTENT` | `bpmn-cli/c7-no-zeebe-content` | +| `W_C7_MESSAGE_NO_IMPLEMENTATION` | `bpmn-cli/c7-message-throw-needs-implementation` | +| `W_C7_MISPLACED_ATTRIBUTE` | `bpmn-cli/c7-camunda-attribute-placement` | +| `W_C7_MISPLACED_EXTENSION` | `bpmn-cli/c7-camunda-extension-placement`, `bpmn-cli/c7-io-parameter-in-input-output` | +| `W_C7_TRANSACTION_NO_START` | `bpmn-cli/c7-transaction-needs-start-event` | +| `W_C7_UNKNOWN_ATTRIBUTE` | `bpmn-cli/c7-camunda-attribute-known` | +| `W_C7_UNKNOWN_ELEMENT` | `bpmn-cli/c7-camunda-element-known` | + +
+ +
+Camunda 8: code and rules + +| code | rules | +| --- | --- | +| `W_C8_BAD_VALUE` | `bpmn-cli/c8-job-retries-valid`, `bpmn-cli/c8-task-header-needs-key-and-value` | +| `W_C8_CONDITION_IGNORED` | `camunda-compat/sequence-flow-condition` | +| `W_C8_DEPLOY_AD_HOC_SUBPROCESS` | `camunda-compat/ad-hoc-sub-process`, `bpmn-cli/c8-ad-hoc-sub-process-no-start-or-end-event` | +| `W_C8_DEPLOY_CALLED_ELEMENT` | `camunda-compat/called-element`, `camunda-compat/no-business-id`, `camunda-compat/no-propagate-all-parent-variables` | +| `W_C8_DEPLOY_DUPLICATE_EXTENSION` | `bpmn-cli/c8-extension-element-unique` | +| `W_C8_DEPLOY_ERROR` | `camunda-compat/error-reference`, `bpmn-cli/c8-error-catch-needs-static-code`, `bpmn-cli/c8-error-event-interrupting`, `bpmn-cli/c8-error-code-unique` | +| `W_C8_DEPLOY_ESCALATION` | `camunda-compat/escalation-boundary-event-attached-to-ref`, `camunda-compat/escalation-reference` | +| `W_C8_DEPLOY_EVENT_DEFINITION` | `camunda-compat/wait-for-completion`, `bpmn-cli/c8-event-sub-process-start-needs-trigger`, `bpmn-cli/c8-compensation-names-own-activity`, `bpmn-cli/c8-compensation-boundary-needs-association`, `bpmn-cli/c8-compensation-boundary-no-outgoing-flow`, `bpmn-cli/c8-conditional-event-needs-condition` | +| `W_C8_DEPLOY_EVENT_GATEWAY` | `camunda-compat/event-based-gateway-target`, `bpmn-cli/c8-event-gateway-needs-two-branches`, `bpmn-cli/c8-event-gateway-target-allowed` | +| `W_C8_DEPLOY_EXECUTABLE` | `camunda-compat/executable-process` | +| `W_C8_DEPLOY_EXPRESSION` | `camunda-compat/feel`, `camunda-compat/feel-compatibility`, `camunda-compat/no-expression`, `bpmn-cli/c8-condition-needs-feel`, `bpmn-cli/c8-multi-instance-needs-feel`, `bpmn-cli/c8-script-needs-feel`, `bpmn-cli/c8-correlation-key-needs-feel`, `bpmn-cli/c8-feel-expression-valid` | +| `W_C8_DEPLOY_EXTENSION` | `camunda-compat/agent-fromai-contract`, `camunda-compat/agent-tool-documentation`, `camunda-compat/agent-tool-output-key`, `camunda-compat/connector-properties`, `camunda-compat/duplicate-task-headers`, `camunda-compat/io-mapping`, `camunda-compat/no-binding-type`, `camunda-compat/no-job-priority-definition`, `camunda-compat/no-version-tag`, `camunda-compat/no-zeebe-properties`, `camunda-compat/secrets`, `camunda-compat/start-event-form`, `camunda-compat/start-event-form-embedded`, `camunda-compat/unresolvable-secret-reference`, `camunda-compat/variable-name`, `camunda-compat/version-tag`, `bpmn-cli/c8-linked-resource-needs-type`, `bpmn-cli/c8-binding-type-known`, `bpmn-cli/c8-version-tag-binding-needs-tag`, `bpmn-cli/c8-conditional-filter-events-known` | +| `W_C8_DEPLOY_IMPLEMENTATION` | `camunda-compat/implementation`, `bpmn-cli/c8-thrown-message-needs-name`, `bpmn-cli/c8-publish-message-no-task-definition` | +| `W_C8_DEPLOY_LINK` | `camunda-compat/link-event`, `bpmn-cli/c8-link-event-needs-name`, `bpmn-cli/c8-link-throw-needs-catch` | +| `W_C8_DEPLOY_LISTENER` | `camunda-compat/before-all-execution-listener`, `camunda-compat/cancel-execution-listener`, `camunda-compat/duplicate-execution-listener-headers`, `camunda-compat/duplicate-execution-listeners`, `camunda-compat/execution-listener`, `camunda-compat/no-before-all-execution-listener`, `camunda-compat/no-cancel-execution-listener`, `camunda-compat/no-execution-listener-headers`, `camunda-compat/no-execution-listeners`, `camunda-compat/no-task-listeners`, `camunda-compat/task-listener`, `bpmn-cli/c8-sequence-flow-no-execution-listeners`, `bpmn-cli/c8-execution-listener-event-type-valid`, `bpmn-cli/c8-task-listeners-need-camunda-user-task`, `bpmn-cli/c8-task-listener-event-type-valid` | +| `W_C8_DEPLOY_MESSAGE` | `camunda-compat/message-reference`, `camunda-compat/subscription`, `bpmn-cli/c8-message-start-needs-static-name`, `bpmn-cli/c8-message-subscription-unique`, `bpmn-cli/c8-publish-message-needs-message`, `bpmn-cli/c8-publish-message-needs-correlation-key` | +| `W_C8_DEPLOY_MULTI_INSTANCE` | `camunda-compat/loop-characteristics` | +| `W_C8_DEPLOY_SCHEMA` | `bpmn-cli/c8-bpmn-attribute-known` | +| `W_C8_DEPLOY_SIGNAL` | `camunda-compat/signal-reference`, `bpmn-cli/c8-signal-start-needs-static-name`, `bpmn-cli/c8-signal-subscription-unique` | +| `W_C8_DEPLOY_START_EVENT` | `bpmnlint/start-event-required`, `camunda-compat/no-multiple-none-start-events`, `bpmn-cli/c8-embedded-start-event-no-trigger`, `bpmn-cli/c8-sub-process-start-event-unique` | +| `W_C8_DEPLOY_TIMER` | `camunda-compat/timer`, `bpmn-cli/c8-timer-value-valid` | +| `W_C8_DEPLOY_UNSUPPORTED` | `camunda-compat/collapsed-subprocess`, `camunda-compat/element-type`, `camunda-compat/inclusive-gateway`, `camunda-compat/no-interrupting-event-subprocess`, `camunda-compat/no-loop`, `camunda-compat/no-signal-event-sub-process` | +| `W_C8_DEPLOY_USER_TASK` | `camunda-compat/no-candidate-users`, `camunda-compat/no-priority-definition`, `camunda-compat/no-task-schedule`, `camunda-compat/no-zeebe-user-task`, `camunda-compat/priority-definition`, `camunda-compat/task-schedule`, `camunda-compat/user-task-definition`, `camunda-compat/user-task-form` | +| `W_C8_EXCLUSIVE_GATEWAY` | `camunda-compat/sequence-flow-condition` | +| `W_C8_EXPRESSION` | `bpmn-cli/c8-multi-instance-completion-needs-feel` | +| `W_C8_FOREIGN_CONTENT` | `bpmn-cli/c8-no-camunda-7-content` | +| `W_C8_JOB_WORKER_USER_TASK` | `camunda-compat/zeebe-user-task` | +| `W_C8_MISPLACED_ATTRIBUTE` | `bpmn-cli/c8-zeebe-attribute-placement` | +| `W_C8_MISPLACED_EXTENSION` | `bpmn-cli/c8-zeebe-extension-placement`, `bpmn-cli/c8-input-mapping-placement`, `bpmn-cli/c8-zeebe-element-placement` | +| `W_C8_STANDARD_LOOP` | `camunda-compat/loop-characteristics` | +| `W_C8_UNKNOWN_ATTRIBUTE` | `bpmn-cli/c8-zeebe-attribute-known` | +| `W_C8_UNKNOWN_ELEMENT` | `bpmn-cli/c8-zeebe-element-known` | +| `W_C8_UNSUPPORTED_IMPLEMENTATION` | `bpmn-cli/c8-publish-message-unsupported` | + +
+ ## bpmnlint and camunda-compat -camunda-compat comes with bpmn-cli and runs from the project's `.bpmnlintrc`. Its findings overlap -with the `W_C7_*` / `W_C8_*` rules; both are listed. Setup: [bpmnlint](bpmnlint.md). +A project's `.bpmnlintrc` runs on top, as an advisory validator ([bpmnlint](bpmnlint.md)). When it +extends camunda-compat or the Miragon engine layers too, their findings are listed twice: once +under their bpmn-cli code, once under the rule name of the `.bpmnlintrc`. ## Next steps diff --git a/docs/library.md b/docs/library.md index cc860df4..6ecd3886 100644 --- a/docs/library.md +++ b/docs/library.md @@ -120,14 +120,14 @@ validator like the CLI ([bpmnlint](bpmnlint.md)). Plugins the project installed A host that keeps a parsed model between calls uses the pipeline on a `Doc`: `Doc.fromXml(xml)`, then `mutateDoc(doc, ops, opts)`, `layoutDoc`, `checkDoc`. They never write. -Every other part (views, metrics, the diagram commands, the platform profile) is exported too: +Every other part (views, metrics, the diagram commands, the platform detection) is exported too: `runOps(doc, commands)` runs commands on a `Doc`, `parseOps(json)` reads the ops JSON into `EditCommand`s. -::: warning Load the profile rules before runProfile -`runProfile` and `validateDoc(doc, { platform })` are synchronous: call `await loadProfiles()` -once before them, otherwise they throw `E_PROFILE_NOT_LOADED` on a Camunda 7 / 8 document. -`mutateDoc`, `checkDoc` and the in-memory API load the rules themselves. +::: tip The Camunda 7 / 8 rules run in the async calls +`validateDoc(doc)` is the structural validation and lint alone. The engine rules (`W_C7_*` / +`W_C8_*`) are bpmnlint rules that load on demand; `checkDoc`, `mutateDoc` and the in-memory API run +them. ::: ### bpmnlint as a validator @@ -191,17 +191,24 @@ await applyToXml(xml, ops, { ## Browser bundles -The core runs in a browser; CI fails on anything Node-only. The Camunda 7 and 8 rules are -chunks of their own, fetched the first time a document of that platform is validated -(`loadProfile('c7')` prefetches). PNG rendering and bpmnlint live in `@miragon/bpmn-cli/node` -only. +The core runs in a browser; CI fails on anything Node-only. The Camunda 7 and 8 rules (bpmnlint, +camunda-compat and the Miragon engine layer) are chunks of their own, fetched the first time a +document of that platform is validated. PNG rendering and the project's `.bpmnlintrc` live in +`@miragon/bpmn-cli/node` only. | bundle (minified / gzip) | size | | --- | ---: | -| everything `@miragon/bpmn-cli` exports | 1103.3 / 346.7 KB | -| `applyToXml` only | 799.8 / 250.3 KB | -| Camunda 7 rules, on demand | 64.4 / 18.8 KB | -| Camunda 8 rules, on demand | 88.3 / 25.2 KB | +| everything `@miragon/bpmn-cli` exports | 1067.9 / 335.7 KB | +| `applyToXml` only | 775.7 / 242.4 KB | +| Camunda 7 rules, on demand | 118.7 / 34.3 KB | +| Camunda 8 rules, on demand | 365.3 / 101.8 KB | +| engine run (bpmnlint and the mapping to `W_C7_*` / `W_C8_*`), on demand | 26.9 / 10.0 KB | + +::: warning No Camunda 7 / 8 call at the top level of the entry module +The rule chunks share packages with the core (min-dash), which a bundler puts into the entry chunk: +a rule chunk then imports the entry chunk. An entry module that awaits a Camunda 7 / 8 call at its +top level waits for itself. Make the first call from a function the entry module calls. +::: `node tools/iso/check.mjs --table` prints the current sizes. diff --git a/docs/testing.md b/docs/testing.md index ebf1a0e1..3303619b 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -380,14 +380,16 @@ npx vitest run test/e2e/isomorphic.test.ts not compile. It prints the minified and gzipped sizes, split like a host's bundler would split them: the initial chunks of the whole entry and of an `applyToXml`-only entry, and the chunks loaded on demand (the Camunda 7 - rules, the Camunda 8 rules with the FEEL, timer and schema-text checks). - It fails when a rule module - (`dist/platform/profiles/`: the profiles, their base, utils and checks) is in the initial chunks of - either entry, naming it; when a rule module imports a module the entry + and the Camunda 8 entry of `@miragon/bpmnlint-plugin-rules`, and bpmn-cli's + engine run in `src/adapters/out/bpmnlint/engine/run/` with bpmnlint's linter). + It fails when a rule module is in the initial chunks of + either entry, naming it; when a module of the engine run imports a module the entry loads, naming both (Rollup / Vite would put that module into the host's entry chunk and make the rule chunk import the entry chunk back: a host whose entry module awaits a Camunda 7 / 8 call at top level would hang; - the rules get the core helpers from `src/domain/validation/profiles/rule-kit.ts`); and + the engine run gets the core helpers from `src/adapters/out/bpmnlint/engine-kit.ts`; + the plugin's packages share min-dash with the core, so a host must not await + such a call at top level, [library.md](library.md#browser-bundles)); and when a size is over its budget in `tools/iso/budget.json` (KB = 1024 bytes; set to the measured size plus about 2 %: raise one only in the change that needs it, with the reason in @@ -401,29 +403,28 @@ npx vitest run test/e2e/isomorphic.test.ts `process` read, and runs the bundle in a `vm` context without any Node global: `applyToXml` (also with host validators, and a no-op that must come back `unchanged`), `layoutXml` (both engines), `newXml`, - `validateXml` with the Camunda 7 and the Camunda 8 profile and with host + `validateXml` with the Camunda 7 and the Camunda 8 rules and with host validators, `showXml`, `findXml` and `metricsXml` must give there exactly what they give in Node, and `renderSvg` the same SVG. It also checks that the inlined Camunda 7 descriptor (`src/domain/document/vendor/camunda-descriptor.ts`) equals the installed `camunda-bpmn-moddle`; regenerate it with `node tools/gen-camunda-descriptor.mjs`. - The same file bundles `src/index.ts` as an ESM split bundle (what Vite or - esbuild make of the `import()` in `src/domain/validation/profiles/PlatformProfiles.ts`), writes a + esbuild make of the `import()` in `src/adapters/out/bpmnlint/BpmnlintEngineLint.ts`), writes a fresh copy per case, loads it with Node's own loader and records which chunk is evaluated: a plain model (also with a host validator, whose error, pre-existing error and covered lint warning come out like Node's) and `platform: 'none'` load no profile chunk, a Camunda 7 file only the Camunda 7 rules, a Camunda 8 file only the Camunda 8 rules; a batch that turns a plain file into a Camunda 7 / 8 file loads them after the ops and reports the added - findings of 0.4.0; `runProfile` without loaded rules throws - `E_PROFILE_NOT_LOADED`; a missing chunk rejects with `E_PROFILE_LOAD` + findings; `validateDoc` loads no rule chunk; a missing chunk rejects with `E_PROFILE_LOAD` (its hint says a browser may keep the failed fetch until the page reloads) and a later call imports it again, which in Node loads it. It also checks that the bundle's entry chunk is the requested one and that the rule modules (and so their chunks) import nothing the entry loads. -- Test files that call `runProfile` or `validateDoc(doc, { platform })` - directly load the rules first (`await loadProfiles()` at the top); the - pipeline tests do not, so they run the on-demand path. +- Tests that need the engine findings of a document on their own use + `runProfile` of `test/helpers/engine.ts`; the pipeline tests run the + on-demand path. - `test/e2e/api.test.ts` checks that the in-memory API gives what the CLI writes and prints for the same input. @@ -638,32 +639,23 @@ flow between two lanes). ## Engine checks (Camunda 7) -The Camunda 7 profile of `bpmn validate` (`src/domain/validation/profiles/C7Profile.ts`) states for -every rule what the engines do. `test/domain/validation/c7-profile.test.ts` holds one synthetic -model per rule (82 models) with the expected codes and the engine verdict; -by default it checks only the profile. To re-check the verdicts against live -engines (REST, no authentication), list their REST roots: +The Camunda 7 rules of `bpmn validate` are bpmnlint rules: camunda-compat's `history-time-to-live` +and the `c7-*` rules of `@miragon/bpmnlint-plugin-rules`, whose specs hold one model per rule with +the engines' verdict ([Camunda 7 and Camunda 8](camunda.md#where-the-checks-come-from)). bpmn-cli +tests what it adds: the code and the hint of each finding (`src/adapters/out/bpmnlint/engine/`), +and the checks it makes itself. `test/adapters/out/bpmnlint/engine-codes.test.ts` fails when a rule +of a camunda-compat version or of the Miragon engine layer has no code. +`test/e2e/camunda7-rules.test.ts` holds synthetic models with the engines' verdict (events with +several event definitions, attributes BPMN does not define) and runs their hints through the CLI. To +re-check the verdicts against live engines (REST, no authentication), list their REST roots: ``` -BPMN_C7_ENGINES=http://localhost:8080/engine-rest[,http://host:port/engine-rest ...] npx vitest run test/domain/validation/c7-profile.test.ts +BPMN_C7_ENGINES=http://localhost:8080/engine-rest[,http://host:port/engine-rest ...] npx vitest run test/e2e/camunda7-rules.test.ts ``` -Each model is deployed to every listed engine, the test asserts the engine's -verdict and that a deploy-severity finding (`W_C7_DEPLOY_*`) is reported -exactly for the refused ones, and every deployment is deleted again. The -verdicts were identical on Camunda 7.24.0, CIB seven 2.2.0 and Operaton 2.1.5 -(341 tests with three engines). The follow-up rounds add their own models with -engine verdicts the same way: `test/domain/validation/c7-profile-rules.test.ts` (deploy -rules the engines disagreed with or the profile missed; `operaton` marks a -verdict only Operaton gives, the engine is told apart by -`/telemetry/data`), `test/e2e/camunda7-rules.test.ts` (events with -several event definitions, attributes BPMN does not define) and -`test/domain/validation/c7-schema-rules.test.ts` (schema rules on every event definition, -start events of transactions / empty sub-processes / ad-hoc sub-processes; -`run` also starts the process and checks that it fails or runs, and the -second-start hint is followed and run). Run all four with the same -`BPMN_C7_ENGINES`; `--no-file-parallelism` keeps two files from deploying -one message name at the same time. +Each model is deployed to every listed engine, the test asserts the engine's verdict and that a +deploy-severity finding (`W_C7_DEPLOY_*`) is reported exactly for the refused ones, and every +deployment is deleted again. The other C7 regression tests (`test/domain/editing/c7-semantic.test.ts`, `test/domain/editing/c7-ext-structure.test.ts`, `test/e2e/camunda7.test.ts`, @@ -677,27 +669,18 @@ element by element. ## Engine checks (Camunda 8) -The Camunda 8 profile (`src/domain/validation/profiles/C8Profile.ts`) states for every rule what -Camunda 8.9 does; placement and known names come from the Zeebe descriptor +The Camunda 8 rules are camunda-compat's config for the file's version and the `c8-*` rules of +`@miragon/bpmnlint-plugin-rules` (the FEEL check with its 762 engine verdicts lives there). bpmn-cli +makes the checks a typed parse cannot see itself: unknown zeebe elements, unprefixed attributes +and `zeebe:publishMessage`; placement and known names come from the Zeebe descriptor (`src/domain/document/vendor/ZeebeDescriptor.ts`, the inlined `zeebe-bpmn-moddle`; regenerate with -`node tools/gen-camunda-descriptor.mjs`, `test/e2e/isomorphic.test.ts` fails -while it is stale). `test/domain/validation/c8-profile.test.ts` holds one synthetic model per -rule (151 models) with the expected codes and Camunda 8's verdict, the timer -and FEEL value checks with the values they were checked on, and the -`validate` output; `test/domain/editing/c8-ops.test.ts` covers `set` / `ext` / `retype` / -`show` in Camunda 8 files and runs the profile's hints through the real CLI -until a refused model has no deploy finding left; `test/domain/validation/c8-feel-and-deploy.test.ts` -checks the FEEL syntax check against `test/fixtures/c8/feel-verdicts.json` -(762 synthetic expressions with the verdict of Camunda 8.9.22, one per line: -add a new expression with the verdict you got from the engine) and the -schema-text, white-space, `refAs` and subscription rules; -`test/domain/validation/c8-values.test.ts` holds the timer range, cron field and -xsd:boolean models (27, with the verdict) and the duplicate id. By default they -check the CLI only. To re-check against a live Camunda 8 (REST v2, no -authentication), name its v2 root: +`node tools/gen-camunda-descriptor.mjs`, `test/e2e/isomorphic.test.ts` fails while it is stale). +`test/domain/editing/c8-ops.test.ts` covers `set` / `ext` / `retype` / `show` in Camunda 8 files and +runs the hints through the real CLI until a refused model has no deploy finding left. To re-check +against a live Camunda 8 (REST v2, no authentication), name its v2 root: ``` -BPMN_C8_ENGINE=http://localhost:8088/v2 npx vitest run test/domain/validation/c8-profile.test.ts test/domain/editing/c8-ops.test.ts test/domain/validation/c8-feel-and-deploy.test.ts test/domain/validation/c8-values.test.ts +BPMN_C8_ENGINE=http://localhost:8088/v2 npx vitest run test/domain/editing/c8-ops.test.ts ``` Each model is deployed (`POST /v2/deployments`), the test asserts that the diff --git a/package-lock.json b/package-lock.json index 9837b946..69542f1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,12 @@ "version": "0.8.0", "license": "MIT", "dependencies": { - "@miragon/bpmnlint-plugin-rules": "0.19.0", + "@miragon/bpmnlint-plugin-rules": "file:miragon-bpmnlint-plugin-rules-0.19.0.tgz", "@resvg/resvg-wasm": "2.6.2", "bpmn-moddle": "^10.2.0", "bpmnlint": "11.15.0", + "bpmnlint-plugin-camunda-compat": "2.61.1", + "bpmnlint-utils": "1.1.1", "commander": "^15.0.0", "moddle": "^8.2.1" }, @@ -792,8 +794,8 @@ }, "node_modules/@miragon/bpmnlint-plugin-rules": { "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@miragon/bpmnlint-plugin-rules/-/bpmnlint-plugin-rules-0.19.0.tgz", - "integrity": "sha512-jjCIu2qL3gDfg82HzVKZEoCfKZVWwaLNHyRP5JVwD6nq5VgHG0ZOcXFwD6ROoueRqjn3NI2nbTViK4mv7PLZeQ==", + "resolved": "file:miragon-bpmnlint-plugin-rules-0.19.0.tgz", + "integrity": "sha512-+GDa7Au3fjmfSdmooMMKBdxXeNmhbjBL7Jckt8JkNmjtWdaXLVVDoKUTphXeqJoSBLMa/5mnDOQXxZ5AlsbNxw==", "license": "MIT", "workspaces": [ "docs" diff --git a/package.json b/package.json index 6e3c3f19..19c8cf60 100644 --- a/package.json +++ b/package.json @@ -67,10 +67,12 @@ "architecture": "depcruise src --config .dependency-cruiser.cjs" }, "dependencies": { - "@miragon/bpmnlint-plugin-rules": "0.19.0", + "@miragon/bpmnlint-plugin-rules": "file:miragon-bpmnlint-plugin-rules-0.19.0.tgz", "@resvg/resvg-wasm": "2.6.2", "bpmn-moddle": "^10.2.0", "bpmnlint": "11.15.0", + "bpmnlint-plugin-camunda-compat": "2.61.1", + "bpmnlint-utils": "1.1.1", "commander": "^15.0.0", "moddle": "^8.2.1" }, diff --git a/src/adapters/in/report/command-report.ts b/src/adapters/in/report/command-report.ts index a18efd83..a97fafc7 100644 --- a/src/adapters/in/report/command-report.ts +++ b/src/adapters/in/report/command-report.ts @@ -7,7 +7,7 @@ import type { DiagramCommandResult } from '../../../domain/diagram/commands/Diag import type { Finding } from '../../../domain/document/Finding.js'; import type { CheckResult } from '../../../application/port/in/ValidateDocumentUseCase.js'; import type { LayoutStatus } from '../../../domain/diagram/types.js'; -import type { PlatformSummary } from '../../../domain/validation/profiles/PlatformProfiles.js'; +import type { PlatformSummary } from '../../../domain/validation/PlatformCheck.js'; import type { Change } from '../../../domain/document/ChangeSet.js'; import type { ValidationResult } from '../../../domain/validation/StructuralValidator.js'; import type { ValidatorReport } from '../../../domain/validation/validators.js'; diff --git a/src/adapters/in/report/guide.ts b/src/adapters/in/report/guide.ts index 39c35c66..d63cb54c 100644 --- a/src/adapters/in/report/guide.ts +++ b/src/adapters/in/report/guide.ts @@ -27,8 +27,7 @@ const USAGE_AND_IO_ERRORS: ErrorDoc[] = [ { code: 'E_FILE_EXISTS', meaning: '`bpmn new` refuses to overwrite an existing file.', fix: 'Choose another name, or pass --force to overwrite.' }, { code: 'E_INVALID_XML', meaning: 'The --xml snippet of `ext add` could not be parsed.', fix: 'Pass one well-formed element with declared prefixes, e.g. --xml \'\', or use attr=value instead.' }, { code: 'E_RENDER', meaning: '`bpmn render` (renderPng) could not rasterise the image: the PNG rasteriser (@resvg/resvg-wasm) failed to load or failed on this drawing (exit 4; never a process abort); or nothing on the plane can be drawn because every shape and connection has broken coordinates (not a number, beyond ±1,000,000 px, a negative size).', fix: 'Write the SVG instead (`--format svg`), or render a smaller part; report the file if it keeps failing. Broken coordinates: `bpmn layout ` draws the model again.' }, - { code: 'E_PROFILE_LOAD', meaning: 'Library use (exit 4): the Camunda 7 or Camunda 8 profile rules, which load on demand (a chunk of their own in a browser bundle), could not be loaded; the call rejected before a result existed and nothing was written. `details.platform` names the platform.', fix: 'Check that the chunk is served next to the entry chunk (base URL, network, content security policy). The library does not cache the failure: a later call imports the chunk again, which loads it in Node; a browser may keep the failed module fetch until the page reloads (Chrome does), so reload the page there after fixing the cause. Prefetch with `loadProfile(platform)` when a file is opened to find out early.' }, - { code: 'E_PROFILE_NOT_LOADED', meaning: 'Library use (exit 1): `runProfile` or `validateDoc(doc, { platform })` ran on a Camunda 7 / 8 document whose profile rules are not loaded yet (they load on demand; the async API, `applyToXml`, `validateXml`, `mutateDoc`, `checkDoc`, loads them itself). `details.platform` names the platform.', fix: '`await loadProfile(platform)` (or `await loadProfiles()` for both) once before the synchronous calls.' }, + { code: 'E_PROFILE_LOAD', meaning: 'Library use (exit 4): the Camunda 7 or Camunda 8 engine rules (bpmnlint with camunda-compat and the Miragon engine layer), which load on demand (a chunk of their own in a browser bundle), could not be loaded; the call rejected before a result existed and nothing was written. `details.platform` names the platform.', fix: 'Check that the chunk is served next to the entry chunk (base URL, network, content security policy). The library does not cache the failure: a later call imports the chunk again, which loads it in Node; a browser may keep the failed module fetch until the page reloads (Chrome does), so reload the page there after fixing the cause.' }, ]; /** Codes of ids and their lookup. */ @@ -159,7 +158,7 @@ const LINT_FINDINGS: ErrorDoc[] = [ const VENDOR_CONTENT_ERRORS: ErrorDoc[] = [ { code: 'E_WRONG_HOST', meaning: '`set`: a known camunda attribute was given to an element that does not carry it: the Camunda descriptor places it on a nested element (the event definition, the multi-instance loop or the condition expression), it is a process attribute given to a participant, or a `.` the nested element cannot carry (loop.camunda:collection on a standard loop). Camunda 8: a zeebe attribute of an extension element (zeebe:assignee), or a zeebe:subscription / zeebe:correlationKey for an element that waits for no message (with a message, both go to that message\'s zeebe:subscription, with a note).', fix: 'Use the prefixed key from the hint, e.g. `bpmn set definition.camunda:errorCodeVariable=code`, `bpmn set \'loop.camunda:collection=${items}\'`, `bpmn set condition.camunda:resource=`; set the trigger it needs first (trigger=error ...), loop=parallel for multi-instance attributes, or set it on the process id.' }, { code: 'E_NO_NESTED_ELEMENT', meaning: '`set`: a definition. or condition. key names a nested element that does not exist (an event without trigger has no event definition; a flow or conditional event without condition has no condition expression), or a `definition[]` / `definition[]` selector matches no event definition.', fix: 'Give the event a trigger in the same command (`bpmn set trigger= definition.=`) or set condition= / when= first; a script resource condition is created directly with `condition.camunda:resource= language=`. (loop. never needs this: a parallel multi-instance loop is created.)' }, - { code: 'E_AMBIGUOUS_NESTED', meaning: '`set` / `ext` with `definition.` on an event that has several event definitions: it does not say which one. The message lists them as definition[] = .', fix: 'Name one by position (0-based) or by trigger: `bpmn set \'definition[1].=\'` or `\'definition[timer].=\'` (quote the brackets in a shell; `ext` takes the same prefix), or keep a single definition: `bpmn set trigger=` (the engines act on one only, see W_C7_MULTIPLE_EVENT_DEFINITIONS).' }, + { code: 'E_AMBIGUOUS_NESTED', meaning: '`set` / `ext` with `definition.` on an event that has several event definitions: it does not say which one. The message lists them as definition[] = .', fix: 'Name one by position (0-based) or by trigger: `bpmn set \'definition[1].=\'` or `\'definition[timer].=\'` (quote the brackets in a shell; `ext` takes the same prefix), or keep a single definition: `bpmn set trigger=` (the engines act on one only).' }, { code: 'E_DUPLICATE_EXTENSION', meaning: '`ext add` would create a second single-instance vendor element (camunda:inputOutput, formData, connector, failedJobRetryTimeCycle, properties; nested: connector/connectorId, formField/validation, ...; zeebe:ioMapping, taskDefinition, ...) and merging is ambiguous: an attribute or value conflicts, or the element already holds several. Camunda 7 engines refuse duplicates (ENGINE-01009).', fix: 'Pass --replace to replace it as a whole, or remove the old one first (`bpmn ext remove `, see `bpmn ext list `).' }, { code: 'E_AMBIGUOUS_EXTENSION', meaning: 'An `ext add` path step matches several vendor elements, or an `ext remove` predicate matches items in several places.', fix: 'Name one with [attr=value] or [index] in the path, e.g. \'camunda:formField[id=amount]/camunda:validation\'; the error lists the candidate paths.' }, { code: 'E_AMBIGUOUS_BRANCH', meaning: '`remove --with-branch` on a node that several paths reach (2+ incoming flows): its downstream is not the branch of one path.', fix: 'Remove the branch from a node only one path reaches (the first node after the split), or remove the node alone (`--bridge-all` reconnects the paths to its successor).' }, @@ -170,9 +169,10 @@ const VENDOR_CONTENT_ERRORS: ErrorDoc[] = [ { code: 'W_PROPERTY_INAPPLICABLE', meaning: '`retype`: camunda attributes or extension elements of the element are not allowed on the new kind by the Camunda descriptor (camunda:assignee on a serviceTask, camunda:topic on a userTask, camunda:taskListener outside a userTask). They are kept (nothing was dropped) but have no effect.', fix: 'Remove them with the commands in the hint (`bpmn set camunda:assignee=`, `bpmn ext remove camunda:taskListener`), set what the new kind needs, or retype back.' }, ]; -/** Platform profile findings, from validate and the ones a mutation introduces. */ +/** Camunda 7 / 8 findings, from validate and the ones a mutation introduces. */ const PLATFORM_PROFILE_FINDINGS: ErrorDoc[] = [ - { code: 'W_C7_*', meaning: 'A finding of the Camunda 7 profile, which runs in Camunda 7 files (detected from modeler:executionPlatform or the camunda namespace; CIB seven and Operaton included; `validate --platform c7|c8|none|auto`). A file that uses Operaton\'s own namespace (operaton:*) is read the way Operaton reads it: operaton:* first, camunda:* as the fallback (Camunda 7 and CIB seven ignore operaton:*); `--json` platform says `"operaton": true` and the detail line says so. Every finding has a severity (JSON field `severity`): deploy = the engines (Camunda 7.24, CIB seven 2.2, Operaton 2.1) refuse the file (codes W_C7_DEPLOY_*); runtime = it deploys but the setting is ignored or fails when it runs; practice = it works, the engine logs a warning. Only executable processes are checked, without the content of ad-hoc sub-processes (the engines skip both); the schema rules cover the whole file (W_C7_DEPLOY_SCHEMA, and the event definitions of W_C7_DEPLOY_EVENT_DEFINITION / W_C7_DEPLOY_LINK). A mutation reports only the findings it introduced.', fix: 'Fix every W_C7_DEPLOY_* finding before deploying; the hint of each finding names the exact `bpmn set` / `bpmn ext` / `bpmn add|remove|move` command. `bpmn validate ` lists all of them.' }, + { code: 'W_C7_*', meaning: 'A finding of the Camunda 7 rules (camunda-compat for the file\'s modeler:executionPlatformVersion, else the latest, and the c7-* rules of @miragon/bpmnlint-plugin-rules, mapped to these codes), which run in Camunda 7 files (detected from modeler:executionPlatform or the camunda namespace; CIB seven and Operaton included; `validate --platform c7|c8|none|auto`). A file that uses Operaton\'s own namespace (operaton:*) is read the way Operaton reads it: operaton:* first, camunda:* as the fallback (Camunda 7 and CIB seven ignore operaton:*); `--json` platform says `"operaton": true` and the detail line says so. Every finding has a severity (JSON field `severity`): deploy = the engines (Camunda 7.24, CIB seven 2.2, Operaton 2.1) refuse the file (codes W_C7_DEPLOY_*); runtime = it deploys but the setting is ignored or fails when it runs; practice = it works, the engine logs a warning. Only executable processes are checked, without the content of ad-hoc sub-processes (the engines skip both); the schema rules cover the whole file (W_C7_DEPLOY_SCHEMA, and the event definitions of W_C7_DEPLOY_EVENT_DEFINITION / W_C7_DEPLOY_LINK). A mutation reports only the findings it introduced.', fix: 'Fix every W_C7_DEPLOY_* finding before deploying; the hint of each finding names the exact `bpmn set` / `bpmn ext` / `bpmn add|remove|move` command. `bpmn validate ` lists all of them.' }, + { code: 'W_C7_ENGINE_RULE', meaning: 'A finding of a Camunda 7 engine rule bpmn-cli has no code for yet (a rule a newer camunda-compat adds); the message names what the engine refuses or ignores.', fix: 'Fix what the message names; report the rule so it gets its own code.' }, { code: 'W_C7_DEPLOY_HISTORY_TTL', meaning: 'An executable process has no camunda:historyTimeToLive, or a value that is not a number of days or PD; Camunda 7.20+, CIB seven and Operaton refuse to deploy it (ENGINE-12018).', fix: '`bpmn set camunda:historyTimeToLive=180` (`bpmn new --target camunda7` writes 180).' }, { code: 'W_C7_DEPLOY_IMPLEMENTATION', meaning: 'A service, send or business rule task (or a message throw / end event definition) has no implementation (class, delegateExpression, expression, type=external or shell, connector; decisionRef for business rule tasks), camunda:type is neither external nor shell ("mail" included: the standard distributions cannot load it), or a shell task has no camunda:field command, an expression field, or wait / redirectError / cleanEnv other than true / false; the engines refuse the file.', fix: '`bpmn set camunda:type=external camunda:topic=` (or camunda:delegateExpression=${...}; `definition.camunda:...` for message events); a shell task: `bpmn ext add camunda:field name=command stringValue=`.' }, { code: 'W_C7_DEPLOY_EXTERNAL_TOPIC', meaning: 'camunda:type=external without camunda:topic; the engines refuse the file.', fix: '`bpmn set camunda:topic=`.' }, @@ -182,7 +182,6 @@ const PLATFORM_PROFILE_FINDINGS: ErrorDoc[] = [ { code: 'W_C7_DEPLOY_FORM', meaning: 'camunda:formRef without a formRefBinding of deployment / latest / version, or formKey and formRef together; the engines refuse the file.', fix: '`bpmn set camunda:formRefBinding=latest` or `bpmn set camunda:formKey=`.' }, { code: 'W_C7_DEPLOY_BAD_VALUE', meaning: 'A value the engines refuse at deploy: mapDecisionResult not singleEntry / singleResult / collectEntries / resultList; jobPriority or (process / external) taskPriority not an integer or expression.', fix: '`bpmn set camunda:mapDecisionResult=singleEntry` or `bpmn set camunda:jobPriority=50`.' }, { code: 'W_C7_DEPLOY_MULTI_INSTANCE', meaning: 'A multi-instance activity has neither loop cardinality nor camunda:collection (also when the collection sits on the activity instead of the loop), an empty cardinality, or an elementVariable without collection; the engines refuse the file.', fix: '`bpmn set \'loop.camunda:collection=${items}\' loop.camunda:elementVariable=item` (or cardinality=3).' }, - { code: 'W_C7_DEPLOY_DUPLICATE_EXTENSION', meaning: 'A second camunda:inputOutput / connector / connectorId / formData / failedJobRetryTimeCycle where the engine reads it; the engines refuse the file (ENGINE-01009).', fix: 'Merge them into one: `bpmn ext list --json`, then `bpmn ext add camunda: --replace --xml \'...\'`.' }, { code: 'W_C7_DEPLOY_INPUT_OUTPUT', meaning: 'camunda:inputOutput on a start or boundary event, a gateway or an event sub-process, or output parameters on an end event or a multi-instance activity; the engines refuse the file.', fix: '`bpmn ext remove camunda:inputOutput` (move the mapping to the next activity).' }, { code: 'W_C7_DEPLOY_LISTENER', meaning: 'An execution or task listener without implementation (class / expression / delegateExpression / script) or event, with an event the engine does not know, a script without scriptFormat, or a timeout task listener without timer or without id; the engines refuse the file.', fix: 'Rebuild just that listener (the hint has both commands; --replace would remove its siblings): `bpmn ext remove \'camunda:executionListener[event=finish]\'`, then e.g. `bpmn ext add camunda:executionListener event=start delegateExpression=${...}`.' }, { code: 'W_C7_DEPLOY_EXTENSION', meaning: 'Invalid content of camunda extension elements: camunda:in/out without source and target (and without variables / businessKey), a parameter without name, a form field without id or with a type other than string / long / boolean / date / enum, a connector without connectorId, a camunda:field without a value (or with stringValue and camunda:string together) where the engines read fields (class / delegateExpression tasks, message throws and listeners, shell tasks); the engines refuse the file.', fix: '`bpmn ext list --json`, then re-add the item complete (`bpmn ext add camunda:field name=a stringValue=b`: keyed items replace the old one); a repeatable item without key is removed by its selector and added again (the hint has both commands).' }, @@ -191,30 +190,26 @@ const PLATFORM_PROFILE_FINDINGS: ErrorDoc[] = [ { code: 'W_C7_DEPLOY_SIGNAL', meaning: 'A signal event without signalRef or with a nameless signal, two root signals with one name, or two subscriptions to one signal name in one engine scope (two signal start events, two boundary events of one host, an event sub-process start and a boundary event on its sub-process); the engines refuse the file.', fix: '`bpmn set signal=`, or rename the signal.' }, { code: 'W_C7_DEPLOY_ERROR', meaning: 'An error end event without errorRef, or whose bpmn:Error has no errorCode; the engines refuse the file.', fix: '`bpmn set error= errorCode=`.' }, { code: 'W_C7_DEPLOY_ESCALATION', meaning: 'An escalation throw / end event without escalationRef or escalationCode, or an escalation boundary event on something other than a sub-process, call activity or user task; the engines refuse the file.', fix: '`bpmn set escalationCode=`, or move / remove the boundary event.' }, - { code: 'W_C7_DEPLOY_EVENT_DEFINITION', meaning: 'A timer without date, cycle or duration, or a compensation activityRef outside the throw event\'s (sub-)process (an event sub-process may name activities of its parent scope); or, by the BPMN schema, a conditional event definition without a condition element (an empty one or a camunda:resource script deploys: W_C7_EMPTY_CONDITION / fine) or a timer definition with more than one of timeDate, timeCycle and timeDuration. The engines refuse the file. The schema rules hold for every event definition in the file: also for one the engines ignore next to another (W_C7_MULTIPLE_EVENT_DEFINITIONS) and in non-executable processes.', fix: '`bpmn set timer=PT1H`, `bpmn set \'when=${...}\'`, or `bpmn set definition.activityRef=` (the hint lists the candidates); a definition the engines ignore is dropped with `bpmn set trigger=`.' }, + { code: 'W_C7_DEPLOY_EVENT_DEFINITION', meaning: 'A timer without date, cycle or duration, or a compensation activityRef outside the throw event\'s (sub-)process (an event sub-process may name activities of its parent scope); or, by the BPMN schema, a conditional event definition without a condition element (an empty one or a camunda:resource script deploys) or a timer definition with more than one of timeDate, timeCycle and timeDuration. The engines refuse the file. The schema rules hold for every event definition in the file: also for one the engines ignore next to another and in non-executable processes.', fix: '`bpmn set timer=PT1H`, `bpmn set \'when=${...}\'`, or `bpmn set definition.activityRef=` (the hint lists the candidates); a definition the engines ignore is dropped with `bpmn set trigger=`.' }, { code: 'W_C7_DEPLOY_START_EVENT', meaning: 'A process or embedded sub-process without a start event, also an empty one ("must define a startEvent element"; a transaction without one deploys: W_C7_TRANSACTION_NO_START), a second none or timer start event in a process (message / signal / conditional start events may be added), or a second start event of any kind in a sub-process (event sub-processes and transactions included); the engines refuse the file. Replaces W_NO_START / W_EMPTY_SUBPROCESS for the same (sub-)process.', fix: 'A second start event: remove it when the first one already reaches where it leads (`bpmn remove `, its flows go with it), else start its branch from the first one (`bpmn set source=`, then remove it) or give a process start another trigger (`bpmn set trigger=message message=`); the hint names the flows and never moves one into a node the first start already reaches. A missing start: `bpmn add startEvent "" --before ` (`--in ` when empty).' }, { code: 'W_C7_DEPLOY_COMPLEX_GATEWAY', meaning: 'A complex gateway with a sequence flow into or out of it: the engines do not run complex gateways and refuse the file ("Invalid destination / source of sequence flow"). One that nothing connects deploys.', fix: '`bpmn retype inclusiveGateway` (or `exclusiveGateway` for one path).' }, { code: 'W_C7_DEPLOY_AD_HOC_SUBPROCESS', meaning: 'An ad-hoc sub-process with a sequence flow into or out of it, or a boundary event on it: the engines do not run ad-hoc sub-processes and refuse the file ("Invalid destination / source of sequence flow", "Invalid reference in boundary event"); one with a multi-instance loop is refused by Camunda 7 and CIB seven too (Operaton deploys a valid loop; the loop rules of W_C7_DEPLOY_MULTI_INSTANCE hold on all three). One that nothing connects deploys and is skipped with its content, so the profile does not check that content.', fix: '`bpmn retype subProcess`, then give its content a start event and flows (`bpmn add startEvent "" --in `, `bpmn connect `); or drop the loop: `bpmn set loop=none`.' }, { code: 'W_C7_DEPLOY_LINK', meaning: 'A link event definition without a name (BPMN schema: every one in the file, also one the engines ignore next to another and in non-executable processes), two link catch events with one name in a process (sub-processes included), or a link throw event reached by a flow with no catch event of its name in its own (sub-)process; the engines refuse the file.', fix: '`bpmn set link=`, or add the catching side: `bpmn add intermediateCatchEvent:link "" --in --link `.' }, { code: 'W_C7_DEPLOY_SCHEMA', meaning: 'An attribute without namespace prefix that the BPMN schema does not define (calledDecision="..." on a businessRuleTask, assignee="..." on a userTask), anywhere in the file: non-executable processes, the diagram and bpmn:definitions included. The engines validate the file against the schema and refuse it.', fix: 'Remove it: `bpmn set =` (on an event definition, loop or condition: `definition.=` / `loop.=` / `condition.=`); a vendor attribute needs its prefix: `bpmn set assignee= camunda:assignee=`. A business rule task\'s calledDecision (bpmn-cli\'s decision link for a model without an engine) is converted: `bpmn set calledDecision=` writes camunda:decisionRef and removes it.' }, - { code: 'W_C7_DEPLOY_BOUNDARY_HOST', meaning: 'A boundary event is attached to a compensation handler (isForCompensation=true); the engines refuse the file. `validate` and writes report it as the structural error E_INVALID_HOST instead (the profile alone, `runProfile`, reports this code).', fix: '`bpmn remove ` or `bpmn move --on `.' }, { code: 'W_C7_DEPLOY_SCRIPT', meaning: 'A script task has neither a script nor camunda:resource; the engines refuse the file.', fix: '`bpmn set scriptFormat=groovy \'script=...\'` (or camunda:resource=deployment://).' }, { code: 'W_C7_DEPLOY_EXCLUSIVE_GATEWAY', meaning: 'An exclusive gateway without outgoing flow, a single outgoing flow with a condition, a default flow with a condition, or flows without condition next to a default (or more than one without condition); the engines refuse the file.', fix: '`bpmn set \'condition=${...}\'` or `bpmn set default=true`.' }, - { code: 'W_C7_DEPLOY_DEFINITIONS_EXTENSION', meaning: 'bpmn:extensionElements directly under bpmn:definitions (the BPMN schema does not allow them there); the engines refuse the file.', fix: '`bpmn ext remove `, then `bpmn ext add ...`.' }, { code: 'W_C7_UNKNOWN_ATTRIBUTE', meaning: 'A camunda attribute (on a BPMN element or inside camunda content) the Camunda descriptor does not know, e.g. a typo; the engines ignore it. The message suggests the closest known name.', fix: 'Replace it: `bpmn set camunda:asignee= camunda:assignee=`.' }, { code: 'W_C7_MISPLACED_ATTRIBUTE', meaning: 'A camunda attribute on an element type that does not read it (camunda:assignee on a serviceTask, camunda:collection on the activity, errorCodeVariable on the event instead of its definition), or on the side of an event definition the engines do not read it on: type / topic / class / delegateExpression / expression / resultVariable / taskPriority on a message catch event, camunda:async on a signal catch event, errorCodeVariable / errorMessageVariable on an error end event, escalationCodeVariable on an escalation throw; it has no effect.', fix: 'Move it with the definition. / loop. / condition. keys of set (the hint has the command), or remove it: `bpmn set camunda:=` (`definition.camunda:=`).' }, { code: 'W_C7_UNKNOWN_ELEMENT', meaning: 'A camunda extension element type the Camunda descriptor does not know (camunda:inputOutputt, camunda:values); the engines ignore it.', fix: '`bpmn ext remove ` and add the right type.' }, { code: 'W_C7_MISPLACED_EXTENSION', meaning: 'A camunda extension element on a host that does not read it (taskListener, camunda:in or formData on a serviceTask; inputOutput on the process or a flow; camunda:field / connector on a message catch event definition, camunda:in on a signal catch), a child type loose in extensionElements, or a child in the wrong container; the engines ignore it.', fix: '`bpmn ext remove ` (the hint has it), then add it where it belongs with `bpmn ext add ...` (child types go into their container automatically).' }, { code: 'W_C7_FOREIGN_CONTENT', meaning: 'zeebe:* attributes or extension elements in a Camunda 7 file; Camunda 7 ignores them.', fix: '`bpmn set zeebe:=` or `bpmn ext remove zeebe:`, and use the camunda equivalent.' }, - { code: 'W_C7_BAD_VALUE', meaning: 'A value the engines accept but misread: a boolean other than exactly true / false (asyncBefore=yes stays off), an unknown calledElement / case / decisionRef binding, variableEvents other than create / update / delete, an empty topic or calledElement, a non-numeric user task priority.', fix: '`bpmn set camunda:=` (the hint names it).' }, + { code: 'W_C7_BAD_VALUE', meaning: 'A value the engines accept but misread: a boolean other than exactly true / false (asyncBefore=yes stays off), an unknown calledElement / case / decisionRef binding, variableEvents other than create / update / delete, a non-numeric user task priority.', fix: '`bpmn set camunda:=` (the hint names it).' }, { code: 'W_C7_MESSAGE_NO_IMPLEMENTATION', meaning: 'A message throw or end event without implementation: it sends nothing and behaves like a none event at run time.', fix: '`bpmn set definition.camunda:type=external definition.camunda:topic=` (or definition.camunda:delegateExpression=${...}).' }, { code: 'W_C7_TRANSACTION_NO_START', meaning: '(runtime) A transaction without a start event (also an empty one): unlike an embedded sub-process it deploys, but every instance that reaches it fails ("No initial activity found for subprocess"). Replaces W_NO_START / W_EMPTY_SUBPROCESS for the same transaction.', fix: '`bpmn add startEvent "" --before ` (`--in ` when empty), or `bpmn remove `.' }, - { code: 'W_C7_EMPTY_CONDITION', meaning: '(runtime) A condition with no expression and no camunda:resource, on a conditional event or on a sequence flow the engines evaluate (not a default flow, not out of a parallel or event-based gateway). It deploys, but every evaluation fails ("condition expression returns non-Boolean" / "condition script returns null").', fix: '`bpmn set \'condition=${...}\'` (or `condition=` to drop it); a conditional event: `bpmn set \'when=${...}\'` or `bpmn set condition.camunda:resource=deployment://