Skip to content

fix: create or modify does not move a node the script places (#1508 case 6) - #1546

Merged
ako merged 3 commits into
fix/flow-graph-geometryfrom
fix/1508-splice-shift-stated-positions
Oct 22, 2026
Merged

ako merged 3 commits into
fix/flow-graph-geometryfrom
fix/1508-splice-shift-stated-positions

Conversation

@ako

@ako ako commented Oct 21, 2026

Copy link
Copy Markdown
Owner

Fixes case 6 of #1508 (mirror of mendixlabs#1598). Stacked on #1528 (fix/flow-graph-geometry, cases 1–5; this builds on its Pinned handling). Base is that branch; retarget to main once #1528 merges.

The bug

create or modify of a nanoflow drawn with @position on every statement, adding an if/else after the declare without @position and editing the calls in the next if's branch:

Modified nanoflow: MyFirstModule.NF_Do (spliced: 1 inserted, 1 replaced, 1 dropped)
@position(40, 200)     if $Flag        was -299  (+339)
@merge(525, 200)       its merge       was 186   (+339)
@position(-124, 100)   call javascript action ... 'b'   (unchanged — left of its own decision)
@position(60, 100)     $Ok = call microflow             (unchanged)
@position(869, 200)    return          was 530   (+339)

Re-running the same script was refused (the connectors of the If at (40, 200) are redrawn). With a plain if (no else) the new if took over the stored decision and the restated if $Flag was drawn on top of it, both merges at (186, 200).

Cause, confirmed from source and reproduced on PedApp:

  • makeRoom shifts every top-level node past the cut by the deficit (339);
  • the branch Replace states @position, so it is Placed and never shifted;
  • movedNode only records a move when stated ≠ stored, so the shifted nodes were never moved back (the merge is not a move candidate at all);
  • no-else: lcsStatements weighed ifConditionChanged the same as sameIfShell; the tie went to the earlier pair.

The decision (option a, chosen by the maintainer)

Shifting consistently cannot make the re-run report Unchanged while the script still states the old positions — measured even for a plain one-activity insert into a described flow (+104, then the re-run is refused on @merge). So:

  1. Under mdl 1, a node at a stated position is pinned. The executor collects every point the script states a node at — @position, @merge, @start, note position:, parameter @position; loop bodies skipped (relative coordinates) — and the mutator pins whatever is drawn there (MicroflowMutator.Pin). A make-room shift (insert or longer replace) that would move a pinned node is refused, naming the room and the node:

    the inserted statements need 339px more room than there is between the ActionActivity at (-500, 200) and the ExclusiveSplit '$Flag' at (-299, 200), which the script draws where it is; state @position on the new statements, where there is room for them, or leave unstated the positions of what comes after them (@position, @merge, a note's position:) so that it can move along

    Pinning is by point, not $ID, so a node a later Placed fragment puts at its stated position is pinned too.

    mdl 0 is unchanged (ADR-0011). The option as proposed let mdl 0 fall back to the whole-flow rebuild, like every splice refusal. TestFlowModify_InsertIsSpliced (ADR-0012's describe → insert → exec acceptance test, headerless) caught what that means: a headerless describe+insert that used to splice became a rebuild that deletes the Studio Pro merges and $IDs. ADR-0011 says a new refusal applies only under the version that introduces it, so the pins are gated on flowRebuildRefused.Applies; a headerless script makes room exactly as before.

  2. LCS weights split: exact 4, position-only / same if shell / editable loop 2, condition changed 1 (all existing ratios kept; only the condition-changed pair drops). The restated if $Flag now pairs with the stored one; the new if is an insert, refused like the else variant instead of stacking.

alter microflow is unaffected (it states no kept positions, so nothing is pinned).

Evidence

  • TestSplice_MakingRoomRefusesToMoveAPinnedNode (mfmutator): insert / longer replace / earlier placed fragment refused; controls (nothing pinned → b moves 420→570; pins only before the gap → accepted).
  • TestLCSStatements_TheSameConditionWinsOverAChangedOne.
  • TestCreateOrModify_InsertDoesNotMoveStatedNodes1508 (PedApp, modelsdk backend): both reported variants refused and nothing written; control = the same change with positions after the insert left unstated → spliced, both calls between if $Flag and its merge, re-run Unchanged nanoflow.

Control runs (fix stubbed, tests run):

# pin guard stubbed (pinnedPast returns nil):
--- FAIL: TestSplice_MakingRoomRefusesToMoveAPinnedNode (0.00s)
    --- FAIL: TestSplice_MakingRoomRefusesToMoveAPinnedNode/an_insert,_the_node_after_the_gap_pinned (0.00s)
        splice_pin_test.go:41: got <nil>, want a refusal naming the pinned activity at (420, 200) and the 150px lacking
    --- FAIL: TestSplice_MakingRoomRefusesToMoveAPinnedNode/a_replace_longer_than_what_it_replaces,_the_end_pinned (0.00s)
        splice_pin_test.go:74: got <nil>, want a refusal naming the pinned end event
    --- FAIL: TestSplice_MakingRoomRefusesToMoveAPinnedNode/an_earlier_fragment_drawn_at_a_stated_point (0.00s)
        splice_pin_test.go:92: got <nil>, want a refusal naming the placed activity at (600, 400)
FAIL
FAIL	github.com/mendixlabs/mxcli/mdl/backend/mfmutator	0.004s
--- FAIL: TestCreateOrModify_InsertDoesNotMoveStatedNodes1508 (3.11s)
    --- FAIL: TestCreateOrModify_InsertDoesNotMoveStatedNodes1508/the_reported_if/else (2.15s)
        flow_modify_stated_positions_1508_test.go:114: want a refusal naming the decision the script draws at (-299, 200), got <nil>
        flow_modify_stated_positions_1508_test.go:117: the refused statement wrote the nanoflow:
              @position(40, 200)
              @merge(525, 200)
                @position(-124, 100)
              @position(869, 200)
    --- FAIL: TestCreateOrModify_InsertDoesNotMoveStatedNodes1508/a_plain_if (0.50s)
        flow_modify_stated_positions_1508_test.go:114: want a refusal naming the decision the script draws at (-299, 200), got <nil>
        flow_modify_stated_positions_1508_test.go:117: the refused statement wrote the nanoflow:
              @position(40, 200)
              @merge(525, 200)
                @position(-124, 100)
              @position(869, 200)
FAIL
FAIL	github.com/mendixlabs/mxcli/mdl/executor	3.150s
FAIL

# LCS split reverted (condition changed weighs 2 again):
--- FAIL: TestLCSStatements_TheSameConditionWinsOverAChangedOne (0.08s)
    flow_declared_match_test.go:199: pairs [[0 0] [1 1]], want [[0 0] [2 1]]: the restated if paired with the stored one, the new if inserted
--- FAIL: TestCreateOrModify_InsertDoesNotMoveStatedNodes1508 (7.28s)
    --- FAIL: TestCreateOrModify_InsertDoesNotMoveStatedNodes1508/a_plain_if (2.12s)
        flow_modify_stated_positions_1508_test.go:114: want a refusal naming the decision the script draws at (-299, 200), got <nil>
        flow_modify_stated_positions_1508_test.go:117: the refused statement wrote the nanoflow:
              @position(-500, 200)
              @position(-299, 200)
              @merge(186, 200)
              if $Out then
                @position(-124, 100)
              @position(-299, 200)
              @merge(186, 200)
              if $Flag then
                @position(-124, 100)
                @position(60, 100)
              @position(530, 200)
FAIL
FAIL	github.com/mendixlabs/mxcli/mdl/executor	7.434s
FAIL

Real binary on a PedApp copy (mdl 1): both variants refused with the message above; the free variant → Modified … (spliced: 1 inserted, 1 replaced, 1 dropped), describe shows decision 40 / calls 215.. / merge 685 / return 1029, re-run → Unchanged nanoflow: MyFirstModule.NF_Do. mxcli docker check on the result: no error on NF_Do / JS_Do / SUB_Check (the 1078 errors are PedApp's baseline: CE6083/CE0462/CE0535 from its stripped theme and widgets). Headerless: spliced as before (test case "mdl 0 splices as before").

Gates

  • make build, make test, make lint, make check-mdl, make check-findings: pass. (A first make test failed only on no space left on device: /tmp is a 12 GB tmpfs shared with other sessions; re-run with TMPDIR on the overlay disk, green.)
  • Integration (-tags integration ./mdl/roundtrip): TestFlowRerunProperty (+ controls), TestPedAppFlowSpliceParity, TestPedAppSpliceParity_*, every TestFlowModify_* (incl. TestFlowModify_InsertIsSpliced), TestSpliceRerun_*, TestFlowVerdictAgreement_*: pass. Skipped here (no TestApp fixture in this checkout): TestTestAppFlowSpliceParity, TestFlowModify_TestAppHeaderAndPositions.

Review notes (/mxcli-dev:review)

No blockers. Two moderate points left open on purpose:

  • No mdl 0 warning. A headerless exec of a statement mdl 1 would refuse splices silently (ADR-0011 asks headerless scripts to warn where meaning differs). check -p / fmt --upgrade -p under mdl 1 do report it, through the same decideFlowModify. A warning on plain mdl 0 exec would need a "spliced, but warn" verdict that exec, diff and check do not have yet.
  • statedPoints names the position-bearing annotation fields by hand (Position, Merge, Start, note positions, parameters). A new one would have to be added there.

Also

  • Bug-test: mdl-examples/bug-tests/flow-geometry-1508-splice-keeps-stated-positions.mdl
  • Finding: .claude/skills/fix-issue/findings/mdl-backend/2026-10-21-splice-shift-leaves-placed-fragment-behind.json
  • docs-wiki/bug-patterns/flow-graph-geometry.md: "making room is a move the script did not ask for"
  • write-microflows (pitfall 13) / write-nanoflows skills: the new refusal and its two ways out

Not touched, noticed: the make-room shift also moves the parameters (y 53) along with the row ($Flag 200 → 539/699 here), so describe starts printing a parameter @position after any insert. Pre-existing; separate follow-up.

🤖 Generated with Claude Code

ako and others added 3 commits October 21, 2026 10:38
`create or modify` pairing a new `if` and a restated one against the same
stored decision weighed "the same if but for its condition" exactly like
"the same if with a change in a branch", and lcsStatements breaks a tie
toward the earlier pair. So in

    if $Out then …; end if;          -- new, no @position
    @position(-299, 200)
    if $Flag then …; end if;         -- restated, branch edited

the new `if $Out` took over the stored decision (its condition set in
place) and the restated `if $Flag` was inserted as a new node at its stated
position, on top of it: two decisions at (-299, 200), both merges at
(186, 200).

The weights are doubled and a changed condition now weighs 1, below every
other partial match, so the restated if pairs with its stored decision and
the new one is an insert. Every other ratio is unchanged.

Part of #1508 case 6 (mendixlabs#1598).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `create or modify` that inserts statements without @position into a
flow whose later statements state theirs made room by moving everything
past the gap along. The stored flow then said something other than the
script: in #1508 case 6 `if $Flag` went -299 -> 40, its merge
186 -> 525 and the return 530 -> 869, while the calls replaced in its
branch — placed at their own stated positions, never shifted — stayed at
(-124, 100) and (60, 100), left of their decision. Re-running the script
was refused (the @merge no longer matched), and shifting "consistently"
cannot fix that: the next run must move the nodes back.

Under mdl 1 every point the script states a node at — @position,
@merge, @start, a note's position:, a parameter's @position; loop bodies
skipped, their coordinates are relative — is pinned on the mutator
(MicroflowMutator.Pin), and a make-room shift (an insert, or a replace
longer than what it replaces) that would move a pinned node is refused,
naming the room it needs and the node in the way, with the two ways out:
state @position on the new statements, or leave the positions after them
unstated so they can move along. Pinning is by point, so a node a placed
fragment puts at its stated position is pinned too.

mdl 0 is unchanged (ADR-0011): there the refusal would fall back to the
whole-flow rebuild, losing the merges, curves and $IDs the shift keeps —
TestFlowModify_InsertIsSpliced, ADR-0012's headerless describe+insert
acceptance test, caught exactly that.

Fixes #1508 case 6 (mendixlabs#1598).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
versions.md enumerates what `create or modify` refuses under mdl 1 where
mdl 0 rebuilds; add the insert that needs room past a node the script
places. The review command gains the pattern that #1508 case 6 hit: a new
splice refusal changes mdl 0 into a rebuild unless it is gated on the
language version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ako
ako merged commit 3b08bb7 into fix/flow-graph-geometry Oct 22, 2026
25 checks passed
ako added a commit that referenced this pull request Oct 22, 2026
fix: create or modify does not move a node the script places (#1508 case 6) — re-land of #1546
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.

1 participant