feat(intent): multi-step parallel branch chains + nested parallels (#6568) - #6623
Merged
Conversation
…6568) #6556 shipped `kind: parallel` at v1 scope: every branch was a single declared step that joined directly, and forks could not nest. Both were rejected at parse with a "not yet supported" message. This lifts them. A branch is now a CHAIN: everything reachable from the branch step through its own routing - its `next`, a decision's `then`/`else`, a boundary `timeout`/ `expire` branch - and it may itself be a nested `parallel` with its own fork/join pair. That reachable set is the branch REGION, and the whole region (not just the branch step) is off the linear chain, so declaration order stays irrelevant. Inside a region there is deliberately no positional fall-through: a step routes explicitly, or - declaring no routing at all - is a branch terminal and flows into the join. The new routing literal `join` converges on the innermost enclosing join gateway, which is what lets a decision inside a branch rejoin from both arms (`else: join`); a nested fork does the same implicitly by omitting `next`. - ProcessParallelSupport gains the region walk (`regions`) and the routing-target extraction both the generator and the parser now use, so the definition of a branch region has exactly one implementation and validation cannot drift from emission. - BpmnIntentGenerator wires the region flows explicitly (`branchChainFlows`) and learns entry/exit nodes: augmentation inserts delegates around a step (resolver / field / timer loader before, writer / setter after), and off the linear chain nothing connects by adjacency any more - so a flow into a region step lands on its first inserted node and a flow out of it leaves from its last. - IntentParser replaces the two v1 rejections with the rules that keep a region a closed sub-flow: `join` only inside a branch (and never a step name), no `end` inside a branch (the join would wait forever for a token that ended), no step in two branches, and nothing outside a branch routing into one - which is also how "a branch routed to the fork's own `next` instead of converging on `join`" surfaces. - The Intent Editor diagram mirrors the same walk, so a chained / nested branch draws the way it is emitted. Also fixes a self-flow the end event could pick up (`end -> end`, invalid BPMN) when pulling the branch and join nodes out of the linear chain left it adjacent to itself - the dedupe now runs after the filter. Covered by ParallelBranchChainTest (emission against the real generator, incl. the writer/loader entry-exit mapping), ProcessParallelSupportTest (the region walk) and IntentEngineIT.parallel_branches_chain_onward_and_nest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #6568. Follow-up to #6556, which shipped
kind: parallelat v1 scope: every branch was a single declared step that joined directly, and forks could not nest — both rejected at parse with a "not yet supported" message. This lifts them.What a branch is now
A branch is a chain: it starts at the declared branch step and continues through that step's own routing — its
next, a decision'sthen/else, a boundarytimeout/expirebranch — and it may itself be a nestedparallelcontributing its own fork/join pair. Everything reachable that way is the branch region, and the whole region (not just the branch step) is off the linear chain, so declaration order stays irrelevant.Inside a branch there is deliberately no positional fall-through: a step routes explicitly, or — declaring no routing at all — is a branch terminal and flows into the join. The new routing literal
joinconverges on the innermost enclosing join gateway, which is what lets a decision inside a branch rejoin from both arms (else: join); a nested fork does the same implicitly by omittingnext.Changes
ProcessParallelSupportgains the region walk (regions) and the routing-target extraction. Both the generator and the parser use it, so the definition of a branch region has exactly one implementation and validation cannot drift from emission. (This is whyIntentParsernow imports one class outsideintent.model— the same shared-parser/generator roleLabelExpressionplays.)BpmnIntentGeneratorwires the region flows explicitly (branchChainFlows) and learns entry/exit nodes. This is the subtle part: augmentation inserts delegates around a step (a resolver / field / timer loader before, the writer / setter after), and off the linear chain nothing connects by adjacency any more — so a flow into a region step lands on its first inserted node, and a flow out of it leaves from its last (the branch'snextrides on the writer, exactly as on the main chain). A v1 branch user task with editable fields was silently splicing its writer into the main chain; that is fixed as a consequence.IntentParserreplaces the two v1 rejections with the rules that keep a region a closed sub-flow:joinonly inside a branch (and never a step name), noendinside a branch (the join would wait forever for a token that ended), no step in two branches (two tokens would run it twice and still leave the join waiting), and nothing outside a branch routing into one — which is also how "a branch routed to the fork's ownnextinstead of converging onjoin" surfaces.intent-diagrams.js) mirrors the same walk, so a chained / nested branch draws the way it is emitted.end -> end, invalid BPMN) when pulling the branch and join nodes out of the linear chain left it adjacent to itself. The dedupe now runs after the filter.Tests
ParallelBranchChainTest(new) — emission against the real generator: the chain through the writer, the terminal joining, the nested fork/join pair,else: join, the field loader as the branch entry, DI shapes, idempotency, and no outgoing flow on the end event.ProcessParallelSupportTest— the region walk: chains, nested ownership (innermost join wins), a step shared by two branches, and a loop back inside one branch (legitimate, and must not walk forever).IntentParserTest— the chained/nested fixture parses; the five rejections.IntentEngineIT.parallel_branches_chain_onward_and_nest— the end-to-end generate. Verified locally together with the existing v1 parallel IT (Tests run: 2, Failures: 0).mvn formatter:validateand the release javadoc pass are clean; the fullengine-intentsuite is green (356 tests).Docs
Per the module's documentation-sync rule: the platform-branded page is dirigible-io.github.io#167, and the vendor-neutral specification is IntentFile/intentfile.github.io#7 (the still-open v1 spec PR, extended — left for the maintainer). The in-repo
engine-intent/CLAUDE.mdbullet and the AI assistant guide are in this PR.🤖 Generated with Claude Code