Skip to content

fix: preserve decision/loop captions across nested control flow#263

Merged
ako merged 2 commits intomendixlabs:mainfrom
hjotha:submit/microflow-nested-caption-preservation
Apr 23, 2026
Merged

fix: preserve decision/loop captions across nested control flow#263
ako merged 2 commits intomendixlabs:mainfrom
hjotha:submit/microflow-nested-caption-preservation

Conversation

@hjotha
Copy link
Copy Markdown
Contributor

@hjotha hjotha commented Apr 22, 2026

Summary

@caption '...' annotations on nested decisions (if) and loops were being dropped when the describer emitted MDL. The annotation state on the flow builder was overwritten by each child statement as it was processed, so when the outer loop in buildFlowGraph consumed the pending annotations for a parent, it picked up the wrong ones.

Fix snapshots each IF's / loop's own pending annotations into local scope before recursing into children, then re-applies them to the outer split/loop after the body is processed. Captions on nested control flow now round-trip.

Part of umbrella #257.

Test plan

  • Regression test exercises @caption on nested if inside loop inside if.
  • go build ./... && go test ./... pass.

Stack

This PR is the base of a 3-PR stack. The other two PRs build on it:

  1. This PR — nested caption preservation (no dependencies).
  2. fix: preserve rule-based decision subtype across microflow roundtrips #265 — rule-based decision subtype preservation (depends on this PR).
  3. fix: preserve rule split describer output and free annotations #266 — describer side of rule splits + free-floating annotations (depends on PR 2).

The stacking is needed because all three touch shared structs in the flow-graph builder; duplicating fields would produce nasty conflicts.

@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 fix correctly addresses the root cause: shared pendingAnnotations state being overwritten during recursive processing of nested control flow statements.
  • Changes are minimal and focused:
    • cmd_microflows_builder_annotations.go: Added handling for ExclusiveSplit and InheritanceSplit captions (previously only handled ActionActivity).
    • cmd_microflows_builder_control.go: Added snapshot/restore of pendingAnnotations before/after recursing into statement bodies for if, loop, and while statements.
    • New comprehensive test file cmd_microflows_builder_annotations_test.go with three test cases covering nested captions, baseline behavior, and annotation text preservation.
  • Test cases are well-structured and directly reproduce the reported issue (nested if inside loop inside if scenario mentioned in PR description).
  • Changes follow existing code patterns and maintain consistency with the flow builder's architecture.
  • No MDL syntax changes were needed (pure internal fix), so full-stack consistency checks for new syntax don't apply.
  • PR is tightly scoped to a single bug fix with no unrelated changes.

Recommendation

Approve the PR. The fix is correct, well-tested, and ready for merge.


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

hjotha pushed a commit to hjotha/mxcli that referenced this pull request Apr 22, 2026
# Conflicts:
#	mdl/executor/cmd_microflows_builder_control.go
@ako
Copy link
Copy Markdown
Collaborator

ako commented Apr 22, 2026

Code Review — fix: preserve decision/loop captions across nested control flow

Overview

Clean fix for shared-state contamination in the flow builder. The root cause is clear from reading addStatement + buildFlowGraph:

  1. addStatement calls mergeStatementAnnotations(stmt) which writes into fb.pendingAnnotations.
  2. For an IF/LOOP/WHILE, addStatement then dispatches into addIfStatement / addLoopStatement / addWhileStatement.
  3. Those methods recurse into child statements, which call addStatementmergeStatementAnnotations again, overwriting fb.pendingAnnotations.
  4. When buildFlowGraph applies fb.pendingAnnotations to the returned activityID (lines 62–64 of cmd_microflows_builder_graph.go), it reads a child's annotations, not the outer statement's.

The fix is correct: snapshot + clear before recursion, re-apply after the activity is created. buildFlowGraph's post-call check then finds pendingAnnotations == nil and no-ops cleanly — no double-apply.


Correctness

addIfStatement fix — Applies annotations immediately after the ExclusiveSplit is appended to fb.objects and before any body recursion. applyAnnotations searches fb.objects for the ID, so the split must already be appended — it is. Correct.

addLoopStatement / addWhileStatement fix — Snapshot before body processing, re-apply after loop is created. Identical pattern, same reasoning. Correct.

No double-applyaddIfStatement clears fb.pendingAnnotations; buildFlowGraph's post-call check (fb.pendingAnnotations != nil) fails and skips. Clean.

ExclusiveSplit / InheritanceSplit in applyAnnotations — Previously only ActionActivity was handled; a @caption on an IF fell through the type switch silently. Now both split types set Caption when the annotation provides one. Correct.


Tests

TestNestedIfPreservesCaptions directly reproduces the bug: outer IF wraps inner IF, each with a different @caption, verifies both ExclusiveSplit.Caption values are correct. This is the right regression test.

TestIfCaptionWithoutNesting is a useful sanity baseline.

TestIfAnnotationStaysWithCorrectSplit goes further — tests @annotation text routing through annotationFlows to the correct split. This is the most thorough of the three.


Gaps

Loop caption fix is untestedaddLoopStatement and addWhileStatement get the same snapshot/restore treatment but no test covers @caption on a loop statement. Given the pattern is identical to IF, the correctness is clear from inspection, but a test would close the gap.

InheritanceSplit caption fix is untested — The new case *microflows.InheritanceSplit branch in applyAnnotations has no corresponding test case.

No MDL bug-test script — CLAUDE.md checklist requires mdl-examples/bug-tests/ for every bug fix. The unit tests here are thorough, but an MDL-level script (the @caption 'X' if ... else if ... pattern) would also serve as documentation of the fixed syntax.


Minor

The test comment "splits are appended in creation order: outer first" is an implementation assumption rather than a contract. It's stable given the current builder logic, but worth flagging as a test fragility if the builder is ever reordered.


Verdict

LGTM with minor gaps. The core fix is correct and well-tested for the primary case. The loop/InheritanceSplit coverage gaps and missing MDL script are worth a follow-up but are not blockers given the identical pattern is tested on the IF side.

hjotha and others added 2 commits April 23, 2026 07:34
When an IF, LOOP, or WHILE statement carried an @caption or @annotation
and contained other annotated statements in its body, the inner
statement's annotations would overwrite fb.pendingAnnotations (shared
builder state) before the outer loop in buildFlowGraph attached them to
the right object. The outer split/loop then ended up with the wrong
caption — or with the inner statement's caption applied to it.

The fix snapshots & clears pendingAnnotations at the top of each compound
statement handler, re-applies them to the split/loop/while activity
right after it's created, and (for IF) moves applyAnnotations above the
branch recursion so the split is decorated before the bodies are walked.

Also extends applyAnnotations to recognise ExclusiveSplit and
InheritanceSplit — on main these were only handled for ActionActivity,
so @caption on a split was silently dropped even without nesting.

Verified end-to-end against Apps.GetOrCreateMendixVersionFromString:
describe -> exec -> describe now produces byte-identical @caption,
@annotation, and @position output.

Regression tests cover:
- nested IFs with explicit @caption on each level
- single IF @caption baseline
- @annotation attachment targeting the correct split when nested

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- mergeStatementAnnotations: add WhileStmt case (was falling through to
  default: nil, so @caption on a WHILE was silently dropped).
- applyAnnotations: add LoopedActivity case — LOOP and WHILE activities can
  now carry the captions they already parse.
- Add TestLoopCaptionPreserved, TestWhileLoopCaptionPreserved, and
  TestInheritanceSplitCaptionApplied to cover the gaps ako flagged (loop and
  InheritanceSplit caption paths were previously untested).
- Add `mdl-examples/bug-tests/263-nested-caption-preservation.mdl`
  reproducer per CLAUDE.md checklist.
@hjotha hjotha force-pushed the submit/microflow-nested-caption-preservation branch from 48881e7 to 94bded8 Compare April 23, 2026 05:42
@hjotha
Copy link
Copy Markdown
Contributor Author

hjotha commented Apr 23, 2026

Thanks @ako — addressed the LGTM gaps in 94bded8:

  1. Loop caption test addedTestLoopCaptionPreserved and TestWhileLoopCaptionPreserved pin the @caption behaviour on LoopStmt and WhileStmt.
  2. InheritanceSplit caption test addedTestInheritanceSplitCaptionApplied exercises the new applyAnnotations case directly.
  3. Bug-test MDL script added at mdl-examples/bug-tests/263-nested-caption-preservation.mdl.

While wiring the loop caption test I also caught two related gaps the original fix didn't close:

  • mergeStatementAnnotations was missing a *ast.WhileStmt case, so @caption on a while silently fell through to default: nil. Added.
  • applyAnnotations had no *microflows.LoopedActivity case, so the loop's own caption couldn't land on the activity even when the snapshot/restore delivered it correctly. Added.

Both gaps are covered by the new tests.

Re: the minor note about "splits appended in creation order: outer first" — fair flag. Left the comment as-is for now since reordering the builder would be a separate refactor and the assumption is documented in the test itself rather than hidden in the production code.

@github-actions
Copy link
Copy Markdown

AI Code Review

What Looks Good

  • Bug fix correctness: The PR correctly identifies and resolves the root cause where pendingAnnotations (shared mutable state) was being overwritten during recursive statement processing. The fix snapshots each control flow's own annotations before recursing into children and re-applies them afterward, ensuring captions stay bound to the correct split/loop activities.
  • Test coverage:
    • Adds a regression test in mdl-examples/bug-tests/263-nested-caption-preservation.mdl that exercises nested @caption on if inside loop inside if.
    • Includes comprehensive unit tests in cmd_microflows_builder_annotations_test.go covering:
      • Nested IF caption preservation (primary fix)
      • Baseline single IF caption (sanity check)
      • Annotation placement correctness (ensuring annotations attach to the right split)
      • Loop and WHILE caption preservation (extending the same pattern)
      • InheritanceSplit caption application (defensive coverage)
    • Tests avoid time.Sleep and use direct state verification.
  • Code quality:
    • The fix applies the same snapshot/restore pattern consistently to addIfStatement, addLoopStatement, and addWhileStatement.
    • Changes are minimal and focused—no refactoring or unrelated modifications.
    • Method receivers and error handling are correct (nil checks before applying annotations).
    • Follows existing code style and patterns in the flow builder.
  • No syntax changes: Since this is a pure behavioral fix (no new MDL syntax), the MDL syntax design and full-stack consistency checklist items are not applicable—appropriate for a bug fix.
  • Scope adherence: The PR addresses a single concern (nested caption preservation) without mixing in unrelated changes. It serves as a clean base for dependent PRs (fix: preserve rule-based decision subtype across microflow roundtrips #265, fix: preserve rule split describer output and free annotations #266).

Recommendation

Approve the PR. The fix is correct, well-tested, and maintains code quality standards. It resolves the reported issue without introducing regressions or violating project conventions. Ready for merging.


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

@ako ako merged commit 25a4252 into mendixlabs:main Apr 23, 2026
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.

3 participants