Skip to content

feat(intent): multi-step parallel branch chains + nested parallels (#6568) - #6623

Merged
delchev merged 1 commit into
masterfrom
feat/intent-parallel-branch-chains
Aug 9, 2026
Merged

feat(intent): multi-step parallel branch chains + nested parallels (#6568)#6623
delchev merged 1 commit into
masterfrom
feat/intent-parallel-branch-chains

Conversation

@delchev

@delchev delchev commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Closes #6568. Follow-up to #6556, which shipped kind: parallel at 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's then/else, a boundary timeout/expire branch — and it may itself be a nested parallel contributing 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.

steps:
  - { name: reviews, kind: parallel, args: { branches: [techReview, commercial], next: consolidate } }
  # branch 1 - a two-step chain; the second step declares no routing, so it joins
  - { name: techReview,  kind: userTask,    args: { assignee: engineer, form: ReviewOrder, next: techSignoff } }
  - { name: techSignoff, kind: serviceTask, args: { setRelationField: TechStatus, value: 2 } }
  # branch 2 - a nested fork; no `next`, so its join flows into the outer join
  - { name: commercial,  kind: parallel,    args: { branches: [pricing, legal] } }
  - { name: pricing,     kind: decision,    args: { if: "amount > 1000", then: escalate, else: join } }
  - { name: escalate,    kind: userTask,    args: { assignee: manager, form: ReviewOrder } }
  - { name: legal,       kind: userTask,    args: { assignee: legal,   form: ReviewOrder } }
  - { name: consolidate, kind: serviceTask, args: { setRelationField: Status, value: 3 } }

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 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.

Changes

  • ProcessParallelSupport gains 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 why IntentParser now imports one class outside intent.model — the same shared-parser/generator role LabelExpression plays.)
  • BpmnIntentGenerator wires 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's next rides 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.
  • 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 (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 own next instead of converging on join" surfaces.
  • Intent Editor diagram (intent-diagrams.js) mirrors the same walk, so a chained / nested branch draws the way it is emitted.
  • Drive-by, in the same code path: the end event could pick up a self-flow (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:validate and the release javadoc pass are clean; the full engine-intent suite 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.md bullet and the AI assistant guide are in this PR.

🤖 Generated with Claude Code

…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>
@delchev
delchev merged commit 11185c1 into master Aug 9, 2026
10 checks passed
@delchev
delchev deleted the feat/intent-parallel-branch-chains branch August 9, 2026 08:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

intent: parallel branches - multi-step branch chains + nested parallels (follow-up to #6556)

1 participant