Skip to content

fix: preserve no-merge branch continuations#355

Open
hjotha wants to merge 6 commits intomendixlabs:mainfrom
hjotha:submit/builder-preserve-no-merge-branch-continuation
Open

fix: preserve no-merge branch continuations#355
hjotha wants to merge 6 commits intomendixlabs:mainfrom
hjotha:submit/builder-preserve-no-merge-branch-continuation

Conversation

@hjotha
Copy link
Copy Markdown
Contributor

@hjotha hjotha commented Apr 27, 2026

Closes part of #351.
Part of #332.

Summary

This extracts one focused split/join builder fix from the audit staging branch.

When an if/else has one terminal branch and one continuing branch, the builder does not need to insert an ExclusiveMerge. In that no-merge shape, the parent flow must continue from the tail of the non-terminal branch. Previously it could continue from the original split instead, leaving the branch body disconnected or causing the following statement to be wired to the wrong origin.

Changes

  • Track the continuation endpoint for no-merge if branches.
  • Preserve pending branch case and anchor state from nested compound statements.
  • Continue the parent flow from the branch tail instead of the stale split when a branch body exists.

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

None found.

What Looks Good

  • The PR is a focused bug fix for a specific issue in the microflow builder's handling of no-merge if/else statements.
  • Changes are localized to the flow builder logic and include appropriate test coverage.
  • The fix correctly tracks continuation endpoints for no-merge branches and preserves pending branch state.
  • The added test validates the specific scenario: continuing ELSE branch tail must connect to following statement.
  • No MDL syntax changes are involved, so full-stack consistency checks don't apply.
  • The PR follows atomicity principles (single concern: fix no-merge branch continuations).
  • Code quality is maintained with clear variable names and logical flow.

Recommendation

Approve the PR. The fix resolves the reported issue where parent flow incorrectly continued from the original split instead of the branch tail in no-merge if/else statements. The solution is well-tested and follows existing code patterns.


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

@github-actions
Copy link
Copy Markdown

AI Code Review

What Looks Good

  • The PR provides a focused bug fix for a specific issue in if/else flow building when one branch terminates and the other continues (no-merge shape).
  • The bug test example (mdl-examples/bug-tests/351-no-merge-branch-continuation.mdl) is well-documented, includes clear usage instructions, and targets the exact symptom described (disconnected branch body or miswired parent flow).
  • The unit tests in cmd_microflows_builder_no_merge_test.go cover both the basic case and a nested scenario to ensure anchor preservation, directly validating the fix.
  • Changes to the flow builder (cmd_microflows_builder_control.go) correctly track continuation endpoints (noMergeExitID, noMergeExitCase, noMergeExitAnchor) and use them to set the parent flow's continuation point when no merge is needed.
  • Helper functions (branchDestinationAnchor, pendingFlowAnchors) in cmd_microflows_builder_flows.go encapsulate anchor logic cleanly, reducing duplication and improving readability.
  • The fix handles both if/else and if (no-else) branches symmetrically, preserving pending branch state (case/anchor) from nested compound statements as intended.
  • All changes are scoped to the executor's flow builder logic—no modifications to grammar, AST, visitor, or backend layers are needed since this is an internal builder correction.
  • The PR follows the project's contribution workflow: includes validation steps (make build, make test, make lint-go) and links to the original issues (Preserve nested branch tails and split-join continuations #351, Microflow roundtrip stabilization for large-project audit findings #332).

Recommendation

Approve the PR. The changes are minimal, well-tested, and directly address the reported bug without introducing syntax changes or violating architectural boundaries. The fix ensures correct flow graph construction for no-merge if/else shapes, preventing disconnected branches or miswired parent flows. Minor observations (e.g., test helper brittleness) do not block approval as they are internal to the test suite and unlikely to cause false negatives in practice.


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

@hjotha hjotha force-pushed the submit/builder-preserve-no-merge-branch-continuation branch from 6bb1846 to 353dde1 Compare April 29, 2026 08:35
@hjotha hjotha force-pushed the submit/builder-preserve-no-merge-branch-continuation branch from 353dde1 to b29a5a2 Compare April 29, 2026 10:19
@github-actions
Copy link
Copy Markdown

AI Code Review

Critical Issues

  • None found

Moderate Issues

  • None found

Minor Issues

  • None found

What Looks Good

  • The PR includes a well-designed bug test case that clearly demonstrates the issue and verifies the fix
  • The unit test file provides comprehensive coverage for the no-merge branch continuation scenarios, including nested cases
  • The fix correctly tracks continuation endpoints and preserves anchor state as described in the problem statement
  • Changes are minimal and focused, addressing only the specific issue without unnecessary modifications
  • The PR follows the contribution workflow with proper validation steps documented
  • Test cases verify both the positive outcome (correct continuation from branch tail) and negative prevention (avoiding stale split wiring)
  • Anchor information preservation is tested in nested scenarios, ensuring the fix doesn't break existing functionality

Recommendation
The PR is ready for approval. It correctly fixes the no-merge branch continuation bug in the microflow builder with appropriate test coverage. The changes are minimal, focused, and maintain consistency with existing code patterns. All relevant checklist items have been satisfied.


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

hjothamendix and others added 4 commits April 30, 2026 18:23
Symptom: when an if/else did not need an ExclusiveMerge because one branch was terminal, the builder could expose the original split as the continuation point instead of the tail of the non-terminal branch.

Root cause: addIfStatement only carried a deferred split case to the parent flow. It did not remember the actual tail activity, pending branch case, or anchor from the branch that continued without a merge.

Fix: track the no-merge continuation endpoint while building each branch, including pending case and anchor state from nested compound statements, and hand that endpoint to the parent flow instead of the stale split.

Tests: add a synthetic builder regression where the else branch continues after the then branch returns, proving the following statement is wired from the branch tail rather than from the split.
Adds an MDL script under mdl-examples/bug-tests/ exercising an
IF with one terminal branch and one continuing branch (no merge).
After exec, `mx check` reports 0 errors, confirming the parent
flow attaches to the continuing branch tail rather than the split.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Symptom: a nested IF whose continuing branch skips a local merge could lose its
@anchor metadata when the parent IF connected that tail to its merge or next
statement. In affected models this changed branch layout and could make Studio
Pro report variables as out of scope after round-trip.

Root cause: addIfStatement only consumed pending branch metadata when
nextFlowCase was non-empty. No-merge continuations can carry only
nextFlowAnchor, so those anchors were ignored.

Fix: treat pending anchors as pending flow metadata even without a case value,
and route their origin/destination sides through shared helper functions.

Tests: added a synthetic nested no-merge IF regression that asserts the branch
tail preserves a bottom-to-top anchor when it connects to the parent merge, and
ran make build plus make test.
Symptom: describe could print the false-path continuation of an if-without-else
inside an else block when the true branch terminated through multiple
statements.

Root cause: guard detection only recognized a true branch that jumped directly
to an EndEvent. Multi-statement terminal true branches fell back to normal
IF/ELSE rendering.

Fix: detect terminal true branches recursively and use layout to identify the
false flow as the horizontal continuation rather than an explicit else branch.

Tests: add a traversal regression with a multi-statement terminal true branch
and a false-path tail that must remain after end if.
@hjotha hjotha force-pushed the submit/builder-preserve-no-merge-branch-continuation branch from 96b05c6 to c49894a Compare April 30, 2026 16:27
The no-merge branch test helper used the generic name objectByID. When combined
with the enum-split tests, the executor package had two helpers with the same
name and failed to compile.

Rename the helper to noMergeObjectByID so both PRs can be merged together
without changing test behavior.

Tests:
- go test ./mdl/executor -run 'TestBuildFlowGraph_NoMerge|TestBuildFlowGraph_NestedNoMerge' -count=1
- make build
- make test
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: preserve no-merge branch continuations

Overview

Correct fix for a real wiring bug. The core change — tracking noMergeExitID from the non-terminal branch's tail instead of always wiring the parent to the split — is sound, and the TestBuildFlowGraph_NoMergeIfElseContinuesFromBranchTail test directly demonstrates the before/after behaviour. The bug test MDL is well-documented.


Moderate: branchDestinationAnchor reversal has no test and no explanatory comment

-    if stmtAnchor != nil && stmtAnchor.To != ast.AnchorSideUnset {
-        return stmtAnchor
+    if branchAnchor != nil && branchAnchor.To != ast.AnchorSideUnset {
+        return branchAnchor
     }
-    return branchAnchor
+    return stmtAnchor

This swaps which anchor takes precedence for the first activity in a branch — a 4-line change that quietly reverses existing behaviour with no comment explaining why the previous order was wrong, and no regression test covering it.

The new order (branch anchor's To beats statement anchor's To) is plausible — the branch-level annotation from the split should govern how the flow arrives at the first branch statement, not the statement's own anchor — but without a test or comment this is invisible to reviewers and future maintainers. The TestBuildFlowGraph_NestedNoMergeTailCarriesAnchorToParentMerge test touches anchor propagation but doesn't exercise this specific precedence swap.


Moderate: flowLooksLikeGuardContinuation uses Y-position equality as a heuristic

return dest.GetPosition().Y == split.GetPosition().Y

This works for builder-generated layouts where the guard's false path is always placed horizontally at split-Y. For MPRs with manually repositioned activities (Studio Pro drag-and-drop), or for builder layouts that drift due to future spacing changes, the equality check could misidentify or silently miss the guard pattern.

The previous !branchFlowStartsAtTerminal check was wrong because it triggered when the false branch led to a real merge, but the Y-position substitute is equally brittle in the other direction. Worth a comment documenting the layout contract this relies on, and ideally a test with an MPR fixture that verifies the heuristic doesn't regress when the guard is at a non-standard Y.


Minor: else { fb.nextConnectionPoint = splitID } drops nextFlowCase/nextFlowAnchor

The old code always set nextFlowCase and nextFlowAnchor in the deferred path. The new code only sets them when noMergeExitID != "":

if noMergeExitID != "" {
    fb.nextConnectionPoint = noMergeExitID
    fb.nextFlowCase = noMergeExitCase
    fb.nextFlowAnchor = noMergeExitAnchor
} else {
    fb.nextConnectionPoint = splitID
    // nextFlowCase and nextFlowAnchor NOT set here
}

Analysis shows noMergeExitID is always populated when this path is reached (both IF-with-ELSE and IF-without-ELSE set it before this block), so the else branch is a dead code path in practice. But if it were ever reached, the missing nextFlowCase/nextFlowAnchor would silently produce an uncased flow from the split — the same bug this PR is fixing. Worth a // unreachable: noMergeExitID is always set above comment, or a panic/return to make it fail loudly.


Minor: pending case/anchor pattern duplicated three times

pendingThenCase / pendingThenAnchor / pendingElseCase / pendingElseAnchor appears in three separate loops (IF+ELSE then-body, IF+ELSE else-body, IF-without-ELSE then-body), each with the same 10-line check block. Not a blocker, but the repetition will make future anchor-handling changes error-prone.


Positive

  • noMergeExitID correctly handles all four combinations: then-returns/else-continues, else-returns/then-continues, empty then-body, empty else-body.
  • Builder test is precise — it asserts the specific elseID → afterID flow exists and that afterID has no other origin.
  • !needMerge guard structure is clean: only enters the no-merge branch when the invariant holds.

@ako ako added this to the v0.8.0 milestone Apr 30, 2026
Symptom: review noted that no-merge branch continuation routing relied on
anchor precedence and guard-layout heuristics that were not visible in code
or tests.

Root cause: the implementation preserved the right flow metadata but left the
split-to-branch destination precedence and guard continuation layout contract
implicit.

Fix: add comments for branch destination precedence, guard continuation Y
matching, and the defensive no-merge fallback; add a regression test pinning
that branch-level `to` anchors win on the split-to-branch flow.

Tests: run the focused no-merge/anchor tests, then make build and make test.
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.

3 participants