From 63e1cc2fcfb69638a2a56e7eceaa7c345c567dbc Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Wed, 22 Jul 2026 22:25:22 +0500 Subject: [PATCH] fix(workflows): preserve intra-overlay order for multiple insert_after edits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _traverse_and_apply's insert_after loop iterated reversed(edits) over the flat per-anchor edit list. The reversal is only meant to place a higher-priority OVERLAY closer to the anchor (mirroring insert_before's winner-closest behaviour), but reversing the flat list also flipped the declared order of multiple insert_after edits authored within a SINGLE overlay: [insert_after a->x, insert_after a->y] produced [a, y, x, b] instead of [a, x, y, b]. insert_before (a forward loop) already preserves order, so the two operations were asymmetric. Group contiguous same-layer edits and reverse the GROUP order only, keeping each overlay's own inserts in declared order. Cross-overlay priority is unchanged (higher-priority overlay still lands closest to the anchor). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) --- src/specify_cli/workflows/overlays/merge.py | 18 +++++++++++++++--- tests/workflows/test_overlay_merge.py | 17 +++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/specify_cli/workflows/overlays/merge.py b/src/specify_cli/workflows/overlays/merge.py index 5521b10a76..bf28a1f133 100644 --- a/src/specify_cli/workflows/overlays/merge.py +++ b/src/specify_cli/workflows/overlays/merge.py @@ -292,9 +292,21 @@ def _traverse_and_apply( cases[case_key] = _traverse_and_apply(case_steps, edits_by_anchor, sources) result.append(step) - # Insert after (highest priority closest to anchor — reversed merge order). - for layer, edit in reversed(edits): - if edit.operation == "insert_after": + # Insert after: higher-priority overlays land closer to the anchor + # (reversed merge order), but a single overlay's own inserts must keep + # their declared order — mirroring the forward insert_before loop above. + # Reversing the whole flat list would also flip an overlay's own edits, + # so group contiguous same-layer edits and reverse the GROUP order only. + after_groups: list[list[tuple[OverlayLayer, OverlayEdit]]] = [] + for layer, edit in edits: + if edit.operation != "insert_after": + continue + if after_groups and after_groups[-1][0][0] is layer: + after_groups[-1].append((layer, edit)) + else: + after_groups.append([(layer, edit)]) + for group in reversed(after_groups): + for layer, edit in group: new_step = copy.deepcopy(edit.step) _record_sources_recursively(new_step, layer.source, sources) result.append(new_step) diff --git a/tests/workflows/test_overlay_merge.py b/tests/workflows/test_overlay_merge.py index 17790d6801..c924d1c271 100644 --- a/tests/workflows/test_overlay_merge.py +++ b/tests/workflows/test_overlay_merge.py @@ -162,6 +162,23 @@ def test_merge_steps_higher_priority_wins(self): ComposedStep("low-step", "project:low"), ] + def test_merge_steps_multiple_insert_after_same_overlay_preserves_order(self): + # Two insert_after edits from ONE overlay on the same anchor must keep + # their declared order (a, x, y, b) — mirroring insert_before. The old + # reversed(edits) over the flat list flipped them to (a, y, x, b). + base = [_step("a"), _step("b")] + overlay = Overlay( + id="ov1", + extends="wf", + priority=10, + edits=[ + OverlayEdit("insert_after", "a", _step("x")), + OverlayEdit("insert_after", "a", _step("y")), + ], + ) + steps, _ = merge_steps(base, [_layer(overlay, "project:ov1")]) + assert [s["id"] for s in steps] == ["a", "x", "y", "b"] + def test_merge_steps_replace_wins_over_insert(self): """Overlays apply to the original tree only; targeting an overlay-introduced step raises.""" base = [_step("a")]