fix(workflows): keep an overlay's replace when the same overlay also inserts on that anchor - #4140
Open
jawwad-ali wants to merge 2 commits into
Open
fix(workflows): keep an overlay's replace when the same overlay also inserts on that anchor#4140jawwad-ali wants to merge 2 commits into
replace when the same overlay also inserts on that anchor#4140jawwad-ali wants to merge 2 commits into
Conversation
…t anchor
`_traverse_and_apply` decided an anchor's fate with `edits[-1]`, which treats
declaration order *inside a single overlay file* as a precedence signal.
Priority is a per-overlay property, so two edits from one overlay have no
priority relation to break — yet a trailing `insert_after` reverted the
anchor to the base step and silently discarded that same overlay's
`replace`.
Measured through the real resolver, one overlay declaring both edits:
replace-then-insert (main): implement run='make build' <-- LOST
attribution: ('implement', 'base')
insert-then-replace (main): implement run='make build-hardened'
either order (fixed): implement run='make build-hardened'
attribution: ('implement', 'project:my-overlay')
So `specify workflow run demo` executed `make build` instead of
`make build-hardened`, with no error, and `workflow resolve` attributed the
untouched step to "base".
Scoped to `replace` only. A replace leaves the anchor in place so both edits
can be honoured; `remove` destroys it, so an insert relative to it cannot
also apply and choosing between them is a separate question — that
combination keeps its existing behaviour, pinned by a test.
The ancestor-conflict map uses the same fate rule so the guard cannot drift,
while still listing every anchor: `_check_anchor_conflicts` reads its key set
to find descendant anchors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes workflow overlay merging so a same-overlay trailing insert does not discard its replacement.
Changes:
- Adds shared anchor-fate selection logic.
- Adds regression tests for edit ordering and precedence.
- Preserves existing remove-plus-insert behavior.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/workflows/overlays/merge.py |
Resolves replacement fate consistently. |
tests/workflows/test_overlay_merge.py |
Covers same-anchor edit combinations. |
Review details
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Files reviewed: 2/2 changed files
- Comments generated: 3
- Review effort level: Balanced
Addresses review feedback on the same-overlay fate rescue.
1. `_winning_fate_edit` also rescued the `replace` when the winning layer
declared `replace`, `remove` AND a trailing insert on one anchor. That
changed behaviour for a combination this PR deliberately scoped out. The
rescue now bails out when the winning layer has a `remove` on the anchor,
so such layers stay byte-identical to their pre-rescue outcome:
one overlay's edits upstream/main before now
replace, remove, insert base kept replaced base kept
remove, replace, insert base kept replaced base kept
replace, insert (target) base kept replaced replaced
remove, insert base kept base kept base kept
Only the intended case now differs from main.
2. `_traverse_and_apply`'s docstring still said the winning edit "is
`edits[-1]`", which stopped being true on this path. It now points at
`_winning_fate_edit` so future changes do not bypass it.
3. The test class docstring said an overlay's "replace/remove" must survive a
trailing insert; only `replace` is rescued. Limited to `replace` and made
the `remove` exclusion explicit.
New parametrized regression test pins the ambiguous layer in both declaration
orders, so the rescue cannot start honouring whichever of replace/remove
happens to come first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
_traverse_and_applypicks the winning edit for an anchor withedits[-1]. That list is built by iterating overlays in merge order and, within each overlay, its edits in declaration order — soedits[-1]treats the order of lines inside a single YAML file as a precedence signal.Priority is a per-overlay property (
docs/reference/workflows.md), so two edits from one overlay have no priority relation to break. But when an overlay declares areplaceand then aninsert_afteron the same anchor, the trailing insert becomes the "winning edit", the anchor's fate reverts to keep the base step, and that overlay's ownreplaceis silently discarded.Reproduction on current
main(bf88c9f)A real overlay through the real resolver:
So
specify workflow run demoexecutesmake buildinstead ofmake build-hardened— no error, andworkflow resolveeven attributes the untouched step to base. Swapping two lines in the YAML changes what runs.Fix
When the last edit on an anchor is an
insert_*, look back within the same layer for areplaceand let that decide the fate.Deliberately scoped to
replace. Areplaceleaves the anchor in place, so both edits can be honoured and nothing is lost. Aremovedestroys the anchor, so an insert relative to it cannot also apply — something must be dropped either way, and choosing which is a separate question. That combination keeps its existing behaviour, pinned bytest_remove_then_insert_after_same_overlay_is_unchanged.The ancestor-conflict map uses the same fate rule so the two cannot drift, while still listing every anchor —
_check_anchor_conflictsreads its key set to find descendant anchors, so dropping insert-only anchors would stop conflicts being detected against them. (I found that the hard way: my first attempt broke two existing conflict tests, which the regression gate caught.)Breaking risk: only the replace-then-insert-on-the-same-anchor shape changes, and it changes from silently losing the replacement to applying it. A test pins that a higher-priority insert-only overlay still leaves a lower layer's replace unapplied, so cross-overlay precedence is untouched.
Verification
removeunchanged; cross-overlay precedence unchanged.tests/workflows: no new failures vs a clean-mainbaseline captured onbf88c9f9(10 pre-existing, all Windows symlink-privilege).uvx ruff@0.15.0 check src tests→ cleanWritten with assistance from Claude Code. Bug found, reproduced, and verified by me on current
main.