mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
fix(workflows): preserve intra-overlay order for multiple insert_after edits (#3662)
_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>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -162,6 +162,23 @@ class TestMergeSteps:
|
||||
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")]
|
||||
|
||||
Reference in New Issue
Block a user