Skip to content

fix: pair microflow splits with nearest merge#327

Merged
ako merged 9 commits intomendixlabs:mainfrom
hjotha:submit/describer-nearest-split-merge
Apr 30, 2026
Merged

fix: pair microflow splits with nearest merge#327
ako merged 9 commits intomendixlabs:mainfrom
hjotha:submit/describer-nearest-split-merge

Conversation

@hjotha
Copy link
Copy Markdown
Contributor

@hjotha hjotha commented Apr 27, 2026

Part of #332.

Summary

  • pair exclusive splits with the nearest common exclusive merge
  • ignore error-handler flows when computing structural split/merge pairing
  • add regression tests for sequential if-without-else continuation placement

Closes #326

Validation

  • make build
  • make lint-go
  • make test

@github-actions
Copy link
Copy Markdown

AI Code Review

Critical Issues

None found.

Moderate Issues

None found.

Minor Issues

  • Unused parameters: In collectReachableDistances, the parameters ctx and activityMap are marked as unused (_ = ctx; _ = activityMap). While harmless, removing them would improve clarity since they aren't used in the function body.
  • Deterministic tie-breaking: In selectNearestCommonMerge, the final tie-breaker uses string(candidates[i].id) < string(candidates[j].id). While this ensures deterministic output, consider whether natural ID ordering (e.g., lexical) is sufficient or if a more semantic tie-breaker (e.g., positional in microflow) would be better. However, the current approach is acceptable for stability.

What Looks Good

  • Algorithm improvement: The new distance-based approach correctly identifies the nearest common exclusive merge by measuring shortest-path distances per branch, fixing the original issue where splits were incorrectly paired with downstream merges (e.g., in sequential ifs).
  • Error-handler exclusion: The use of findNormalFlows to skip error-handler flows during traversal aligns with the requirement to ignore them for structural split/merge pairing.
  • Test coverage: Two new tests validate:
    1. Nearest merge selection before downstream splits (TestFindMergeForSplit_ChoosesNearestMergeBeforeDownstreamIf).
    2. Continuation placement for sequential ifs-without-else (TestTraverseFlow_SequentialIfWithoutElseKeepsContinuationOutsideFirstIf), which directly addresses the regression mentioned in the PR.
  • Code clarity: The refactored logic is well-commented and separates concerns (distance collection vs. merge selection). The sort criteria (minimizing max distance, then sum distance) is logical for identifying the "nearest" merge in a structural sense.
  • No scope creep: Changes are focused exclusively on microflow split/merge pairing logic and its test coverage, with no unrelated modifications.

Recommendation

Approve. The PR correctly fixes the split/merge pairing issue with thoughtful algorithmic improvements, includes relevant regression tests, and maintains code quality. Minor cleanups (unused parameters) are trivial and do not block merging.


Note: No MDL syntax changes were introduced, so syntax design and full-stack consistency checks were not applicable.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

@github-actions
Copy link
Copy Markdown

AI Code Review

Critical Issues

None found.

Moderate Issues

None found.

Minor Issues

  • In mdl/executor/cmd_microflows_show.go, the function collectReachableDistances has unused parameters (ctx and activityMap) that are explicitly ignored with _ = ctx and _ = activityMap. While this is acceptable, removing them would clean up the signature if they're truly not needed for the algorithm.
  • The bug test file's usage example shows mxcli -p app.mpr -c "describe microflow BugTest326.MF_SequentialIfs" which requires quotes around the qualified name due to the dot. This is correct MDL syntax, but worth noting for users.

What Looks Good

  • The fix correctly implements nearest common merge selection using shortest-path distances per branch
  • Error-handler flows are properly excluded from structural pairing via findNormalFlows
  • Comprehensive test coverage added:
    • New bug test file (mdl-examples/bug-tests/326-describer-pair-split-with-nearest-merge.mdl) demonstrating the fix
    • Unit tests verifying the nearest merge selection and sequential if-continuation placement
  • The changes are focused solely on the split/merge pairing logic without touching unrelated functionality
  • Code is well-commented explaining the algorithm and rationale
  • Maintains backward compatibility (still returns empty ID when no merge found)
  • Follows existing patterns for bug tests and executor tests in the project

Recommendation

Approve. The PR correctly fixes issue #326 with appropriate tests and minimal, focused changes. No blockers identified.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

hjotha pushed a commit to hjotha/mxcli that referenced this pull request Apr 27, 2026
The ctx and activityMap parameters were carried over from a sibling
helper but never used by the BFS traversal. Removes them and the
matching `_ = ctx; _ = activityMap` discard lines, addressing AI
review feedback on PR mendixlabs#327.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

AI Code Review

What Looks Good

The PR correctly addresses issue #326 by fixing the split/merge pairing logic in the microflow describer:

  • Root cause fix: Changed from finding any common downstream merge to finding the nearest common merge using shortest-path distance calculation
  • Error handler exclusion: Properly excludes error-handler flows from structural pairing consideration via findNormalFlows()
  • Algorithm improvement: Uses BFS to compute distances and selects merges with minimal max distance (then sum distance) for optimal pairing
  • Test coverage:
    • Added bug test script showing the fixpoint behavior (describe → exec → describe)
    • Added unit tests verifying nearest merge selection and correct continuation placement
    • Tests cover the specific sequential if-without-else scenario mentioned in the issue
  • Code quality:
    • Clean refactor with well-named helper functions (collectReachableDistances, selectNearestCommonMerge)
    • Deterministic tie-breaking in merge selection
    • No unnecessary changes to unrelated code

Recommendation

Approve the PR. The fix correctly implements the nearest-common-merge pairing logic while ignoring error-handler flows, includes appropriate test coverage, and maintains consistency with the project's architecture and coding standards. No changes to MDL syntax or full-stack feature wiring were needed since this is a behavioral fix to existing DESCRIBE functionality.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

@hjotha
Copy link
Copy Markdown
Contributor Author

hjotha commented Apr 28, 2026

Follow-up pushed in 75a3788.

This extends guard-branch detection from direct EndEvent checks to recursive terminality before the split merge boundary. It preserves the guard form when a terminating branch contains actions or nested splits, instead of wrapping the continuation in a synthetic/empty else.

Validation rerun on the branch:

  • make build
  • make test
  • make lint-go
  • go test ./mdl/executor -run 'TestTraverseFlow_(GuardBranchWithMultipleActivitiesKeepsContinuationOutsideElse|NestedTerminalGuardBranchSuppressesEmptyOuterElse|SequentialIfWithoutElseKeepsContinuationOutsideFirstIf|IfElse)$' -count=1

@github-actions
Copy link
Copy Markdown

AI Code Review

Critical Issues

None found.

Moderate Issues

None found.

Minor Issues

  • Test helper visibility: In mdl/executor/cmd_microflows_traverse_test.go, the added tests use helper functions (mkID, mkObj, mkFlow, mkBranchFlow) that are not visible in the diff. While these are likely defined elsewhere in the file (as existing tests would require them), the diff only shows the new test additions. This is not a blocker but could be clarified if the helpers were recently added. However, since the file already existed and we're only adding tests, this is acceptable.
  • Usage instruction clarity: The bug test script's usage instructions show mxcli exec ... followed by mxcli -p app.mpr -c "describe ..." which is correct, but the first command creates the test artifacts while the second validates them. This is standard for bug tests and acceptable.

What Looks Good

  • Bug fix correctness: The changes directly address the root cause described in Describer can pair a split with a downstream merge instead of the nearest merge #326 by:
    • Modifying findMergeForSplit to ignore error-handler flows via findNormalFlows
    • Implementing shortest-path distance calculation per branch to select the nearest common merge
    • Updating guard condition checks to use the new termination helpers
  • Test coverage:
    • Added a dedicated bug test script (mdl-examples/bug-tests/326-describer-pair-split-with-nearest-merge.mdl) that reproduces the issue and verifies the fixpoint behavior
    • Added comprehensive unit tests in cmd_microflows_traverse_test.go covering:
      • Nearest merge selection with downstream ifs
      • Sequential if continuation placement
      • Guard branch handling with multiple activities
      • Nested terminal guard branch scenarios
  • Code quality:
    • Changes are focused and atomic (single bug fix)
    • No duplication with existing implementations
    • Consistent application of the new logic across all call sites
    • Clear comments explaining the algorithm (shortest path, error-handler exclusion)
    • Deterministic tie-breaking in merge selection (max distance → sum distance → lexicographic ID)
  • Process compliance:

Recommendation

Approve. The PR correctly fixes the microflow split/merge pairing bug with appropriate test coverage and maintains full consistency with the project's architecture and coding standards. The minor notes above do not block merging.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

Copy link
Copy Markdown
Collaborator

@ako ako left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix: pair microflow splits with nearest merge

Summary: Correct fix for a real bug. The two root causes (wrong merge pairing and shallow guard detection) are addressed cleanly with the right algorithmic tools. Good test coverage across the key structural cases.

— nearest-merge selection

Replacing the "any common merge" set intersection with BFS shortest-path distances is the right approach. sorts by max-distance first (nearest merge relative to the farthest branch), then sum-distance, then ID for determinism. Map iteration over is non-deterministic but the sort at the end makes the final selection deterministic. ✓

The BFS in uses a distance-comparison guard instead of a simple visited set:

if previous, ok := distances[item.id]; ok && previous <= item.distance {
    continue
}

With uniform edge weights this guard is never triggered (BFS naturally visits nodes in shortest-path order), so a plain check would be equivalent. No behavior difference — just worth noting for future readers.

Guard detection —

The recursive DFS correctly handles multi-activity terminating branches that the old single-step check missed. The on every recursive branch call avoids cross-branch contamination but allocates O(visited) memory per branch per depth level. For typical microflows (< 100 activities) this is negligible; worth a comment for anyone who hits a pathological deeply-nested case.

with nil

The nested-split case:

nestedMergeID := splitMergeMap[currentID]  // nil map read → returns ""

Reading from a nil map in Go is safe (returns zero value), so passing for doesn't panic — it results in every nested split being traversed with , which means the termination check continues past any merge. The test relies on this implicitly: it passes for and the test graph happens to work because both inner branches are EndEvents. This is fragile — a test graph with an actual merge after the inner split would silently produce wrong results. A nil guard and a comment would make the contract explicit.

Inconsistent test setup pattern

pre-builds using . passes . The inconsistency is confusing — it looks like an oversight rather than a deliberate choice. The nested guard test should build its own to match production behavior.

Minor: called in but old is still present

is no longer called by but remains in the file. If it has no other callers it should be removed to avoid dead code.

grep -r 'collectReachableNodes' mdl/executor/

Bug test script

covers the exact symptom from the issue and documents the before/after clearly. ✓

Verdict

Approve. The fix is correct and addresses both root causes. The test inconsistency and the possible dead code are the two cleanup items worth addressing before or after merge.

@github-actions
Copy link
Copy Markdown

AI Code Review

Critical Issues

None found.

Moderate Issues

None found.

Minor Issues

None found.

What Looks Good

  • Bug fix with comprehensive testing: The PR correctly addresses issue Describer can pair a split with a downstream merge instead of the nearest merge #326 by implementing nearest-common-merge pairing that ignores error-handler flows, with a clear bug test demonstrating the fix.
  • Root cause solution: Replaced the flawed "first common merge" approach with shortest-path distance calculation per branch to find the true nearest merge.
  • Proper error handler exclusion: Error-handler flows are correctly excluded from structural split/merge pairing as intended.
  • Thorough test coverage: Added both a regression test script (mdl-examples/bug-tests/326-describer-pair-split-with-nearest-merge.mdl) and comprehensive unit tests covering the fix and related edge cases.
  • Clean refactoring: The changes replace complex traversal logic with clearer helper functions (collectReachableDistances, selectNearestCommonJoin, etc.) while maintaining existing functionality for guard patterns and nested splits.
  • DESCRIBE roundtrip validation: The bug test explicitly verifies that describe → exec → describe is a fixpoint, ensuring the fix doesn't introduce regressions.
  • Focused scope: The PR addresses a single concern (split/merge pairing in microflow describer) without unrelated changes.

Recommendation

Approve the PR. The fix correctly resolves the reported bug with appropriate testing, follows project conventions, and maintains full-stack consistency for the existing DESCRIBE command functionality. No changes are needed.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

hjotha pushed a commit to hjotha/mxcli that referenced this pull request Apr 30, 2026
The describer no longer references collectReachableNodes; selectNearestCommonMerge uses collectReachableDistances
exclusively. Drop the unused helper to keep the file focused.

Addresses ako's cleanup note on PR mendixlabs#327.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

AI Code Review

Critical Issues

None found.

Moderate Issues

None found.

Minor Issues

What Looks Good

  • Bug test: The new test file mdl-examples/bug-tests/326-describer-pair-split-with-nearest-merge.mdl is well-documented, includes a clear symptom description, root cause, and validation steps. It follows the bug test convention perfectly.
  • Algorithm improvement: The replacement of the naive first-common-merge search with a shortest-path-based nearest common join (ignoring error-handler flows) correctly fixes the sequential if/nesting issue. The use of BFS for distance calculation and tie-breaking (max distance then sum distance) is sound.
  • Test coverage: The added unit tests in cmd_microflows_traverse_test.go comprehensively validate:
    • Nearest merge selection before downstream merges
    • Common activity joins (non-merge convergence points)
    • Sequential if-without-else continuation placement
    • Guard branch behavior
    • Nested terminal guard suppression
    • Inheritance split fall-through cases
      These prevent regressions and cover edge cases.
  • Helper functions: The new functions (findNormalFlows, collectReachableDistances, selectNearestCommonJoin, isSplitJoinCandidate, continueAfterSplitJoin, etc.) are well-named, single-purpose, and improve code clarity. Error-handler flow exclusion is properly encapsulated.
  • Atomicity: The PR focuses solely on the split/merge pairing bug (fixing Describer can pair a split with a downstream merge instead of the nearest merge #326) without unrelated changes. All modifications serve this single concern.
  • No syntax changes: As this is a behavioral fix to an existing DESCRIBE feature, no MDL grammar/AST/visitor/executor pipeline changes were needed—consistent with the scope.

Recommendation

Approve the PR. The fix is correct, well-tested, and maintains project conventions. The bug test provides executable validation, and the unit tests ensure robustness. No blocking issues remain.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

hjothamendix and others added 7 commits April 30, 2026 10:43
Symptom: sequential if-without-else structures could be described with the continuation of the first split nested inside that split.

Root cause: split/merge pairing selected an arbitrary common reachable merge from map iteration, so a later downstream merge could be chosen before the immediate branch convergence.

Fix: rank common merge candidates by nearest branch distance, then total distance and ID, while ignoring non-normal flows for structural pairing.

Tests: add unit coverage for nearest-merge selection and for keeping the continuation after a sequential split at top level.
Adds an MDL script under mdl-examples/bug-tests/ exercising sequential
if-without-else blocks. The describe → exec → describe fixpoint
confirms the first split is paired with its nearest merge, keeping the
ifs as siblings rather than nesting the continuation inside the first
if's body.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The ctx and activityMap parameters were carried over from a sibling
helper but never used by the BFS traversal. Removes them and the
matching `_ = ctx; _ = activityMap` discard lines, addressing AI
review feedback on PR mendixlabs#327.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Symptom: describer could wrap a fall-through continuation in an empty or
synthetic ELSE when the true branch terminated through nested or multi-activity
flow instead of a direct EndEvent.

Root cause: guard detection only checked whether the true branch destination was
an EndEvent. It missed branches that reached a return through actions, nested
IFs, enum splits, or inheritance splits.

Fix: detect branch terminality recursively before the split merge boundary and
use the guard form whenever the true branch terminates while the false branch
starts the continuation path. Direct return-vs-return splits still keep the
normal IF/ELSE shape.

Tests: added traversal regressions for multi-activity terminal guards and nested
terminal guards, while keeping existing sequential IF and IF/ELSE traversal
coverage green.
Symptom: describe could indent a shared tail under one branch when two split branches converged directly on an activity or downstream split instead of an ExclusiveMerge.

Root cause: split pairing only considered ExclusiveMerge objects, so traversal had no stop point for branches that rejoined at a regular object.

Fix: choose the nearest common executable join, and continue from that object after emitting the enclosing split when the join is not an ExclusiveMerge.

Tests: added a synthetic common-tail traversal regression and ran make test.
Symptom: a branch containing a nested inheritance split could be classified as a terminal guard even when one split case fell through to the parent merge. Describe then closed the parent if without emitting the shared continuation, producing a microflow with a missing return value.

Root cause: terminal-branch analysis recursed into nested splits using only the nested split merge. When that nested split had no own merge, the analysis ignored the parent merge and followed the fallthrough branch all the way to a terminal end event.

Fix: reuse the parent merge as the nested stop point when a nested split has no merge of its own. This keeps fallthrough branches non-terminal for guard detection.

Tests: added a synthetic inheritance-split fallthrough regression for branchFlowTerminatesBeforeMerge.
The describer no longer references collectReachableNodes; selectNearestCommonMerge uses collectReachableDistances
exclusively. Drop the unused helper to keep the file focused.

Addresses ako's cleanup note on PR mendixlabs#327.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@hjotha hjotha force-pushed the submit/describer-nearest-split-merge branch from cd308e5 to 7ccab5d Compare April 30, 2026 08:47
@github-actions
Copy link
Copy Markdown

AI Code Review

What Looks Good

  • Bug fix focus: The PR addresses a specific, well-defined issue (Describer can pair a split with a downstream merge instead of the nearest merge #326) with microflow split/merge pairing in the describer
  • Comprehensive testing: Includes both an MDL bug test (mdl-examples/bug-tests/326-describer-pair-split-with-nearest-merge.mdl) and multiple unit tests covering the fix and related scenarios
  • Correct algorithmic approach: Implements shortest-path distance calculation to find the nearest common merge, ignoring error-handler flows as intended
  • Maintains existing patterns: Doesn't modify MDL syntax or architecture layers unnecessarily; changes are confined to the executor/describe logic where they belong
  • Clear documentation: The test file includes symptom description, root cause, and usage instructions for verification
  • Atomic changes: All modifications relate to the same core issue (split/merge pairing) without mixing unrelated concerns

Recommendation

Approve the PR. The changes correctly fix the split/merge pairing bug with appropriate test coverage, follow the project's architectural boundaries, and maintain scope discipline. The solution properly addresses the root cause (finding nearest merge while ignoring error-handler flows) and includes verification that the describe→exec→describe cycle becomes a fixpoint.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

@hjotha
Copy link
Copy Markdown
Contributor Author

hjotha commented Apr 30, 2026

Review follow-up status on the current head:

  • Removed the dead collectReachableNodes helper.
  • Cleaned up the nearest-merge/guard tests so they build the same split-merge map shape as production traversal.
  • Kept the nested-split fallthrough guard explicit in the current implementation/tests.

Local validation after the update was green:

make build
make test
make lint-go

@ako ako merged commit 1456340 into mendixlabs:main Apr 30, 2026
1 of 2 checks passed
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.

Describer can pair a split with a downstream merge instead of the nearest merge

3 participants