Skip to content

Commit 63e1cc2

Browse files
jawwad-aliclaude
andcommitted
fix(workflows): preserve intra-overlay order for multiple insert_after edits
_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) <noreply@anthropic.com>
1 parent 8db7228 commit 63e1cc2

2 files changed

Lines changed: 32 additions & 3 deletions

File tree

src/specify_cli/workflows/overlays/merge.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,21 @@ def _traverse_and_apply(
292292
cases[case_key] = _traverse_and_apply(case_steps, edits_by_anchor, sources)
293293
result.append(step)
294294

295-
# Insert after (highest priority closest to anchor — reversed merge order).
296-
for layer, edit in reversed(edits):
297-
if edit.operation == "insert_after":
295+
# Insert after: higher-priority overlays land closer to the anchor
296+
# (reversed merge order), but a single overlay's own inserts must keep
297+
# their declared order — mirroring the forward insert_before loop above.
298+
# Reversing the whole flat list would also flip an overlay's own edits,
299+
# so group contiguous same-layer edits and reverse the GROUP order only.
300+
after_groups: list[list[tuple[OverlayLayer, OverlayEdit]]] = []
301+
for layer, edit in edits:
302+
if edit.operation != "insert_after":
303+
continue
304+
if after_groups and after_groups[-1][0][0] is layer:
305+
after_groups[-1].append((layer, edit))
306+
else:
307+
after_groups.append([(layer, edit)])
308+
for group in reversed(after_groups):
309+
for layer, edit in group:
298310
new_step = copy.deepcopy(edit.step)
299311
_record_sources_recursively(new_step, layer.source, sources)
300312
result.append(new_step)

tests/workflows/test_overlay_merge.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,23 @@ def test_merge_steps_higher_priority_wins(self):
162162
ComposedStep("low-step", "project:low"),
163163
]
164164

165+
def test_merge_steps_multiple_insert_after_same_overlay_preserves_order(self):
166+
# Two insert_after edits from ONE overlay on the same anchor must keep
167+
# their declared order (a, x, y, b) — mirroring insert_before. The old
168+
# reversed(edits) over the flat list flipped them to (a, y, x, b).
169+
base = [_step("a"), _step("b")]
170+
overlay = Overlay(
171+
id="ov1",
172+
extends="wf",
173+
priority=10,
174+
edits=[
175+
OverlayEdit("insert_after", "a", _step("x")),
176+
OverlayEdit("insert_after", "a", _step("y")),
177+
],
178+
)
179+
steps, _ = merge_steps(base, [_layer(overlay, "project:ov1")])
180+
assert [s["id"] for s in steps] == ["a", "x", "y", "b"]
181+
165182
def test_merge_steps_replace_wins_over_insert(self):
166183
"""Overlays apply to the original tree only; targeting an overlay-introduced step raises."""
167184
base = [_step("a")]

0 commit comments

Comments
 (0)