Code domain's address-keyed merge falsely conflicts when both sides add a trailing comment to different functions in the same file
Summary
While researching Build With Muse Episode 08 (The Merge Engine), found that muse merge produces a false, file-level conflict when two branches each add a trailing inline comment to two different functions in the same file — directly contradicting Episode 05's (already-shipped) claim that address-keyed merge means edits at different symbol addresses never conflict.
Confirmed this is narrower than it first looked: substantial body rewrites to different functions in the same file merge cleanly, as expected. The false conflict is specific to comment-only edits.
Reproduction
# calc.py, committed on main
def add(a, b):
return a + b
def subtract(a, b):
return a - b
Branch feature/alice: add a trailing comment to add only:
def add(a, b):
return a + b # comment
Branch feature/bob (from the same base): add a trailing comment to subtract only:
def subtract(a, b):
return a - b # comment
$ muse merge feature/alice # fast_forward, fine
$ muse merge feature/bob
{"status": "conflict", "conflicts": ["calc.py"]}
$ muse conflicts --json
[{"path": "calc.py", "file": "calc.py", "symbol": null, "kind": "file"}]
"kind": "file", "symbol": null — this is a whole-file conflict, not scoped
to either symbol, despite both edits being comment-only changes to two
different, uncontested functions.
What does NOT reproduce it (ruled out)
- Substantial body rewrites (not just comments) to two different functions in
the same file — merges cleanly,
conflicts: [], exactly as Episode 05 demonstrated and claimed. - A comment edit on one side + an entirely unrelated new file on the other side — merges cleanly.
So this is specifically: same file, comment-only edits, two different functions. Real rename/move detection (Episode 07) and real independent body edits (Episodes 01, 05, 06) are all unaffected.
Hypothesis (not confirmed)
Comments likely aren't attached to either function's AST node in the
code_symbols diff dimension the way a body change is — they may get
represented as a change to the file's own structure/metadata dimension
(or dropped from symbol-level identity entirely), which would explain why
plugin.merge_ops() can't address-key the two edits apart and falls back to
a whole-file conflict.
Ask
- Confirm where comment-only edits are represented in the code domain's diff/merge pipeline (symbol body content vs. a separate/no dimension).
- If comments are dropped from symbol identity: teach
merge_ops()(or the underlying diff) to still address-key a comment-only change to its enclosing symbol, so two independent comment edits merge the same way two independent body edits already do. - Add a regression test: two branches, each adding a trailing comment to a different function in the same file, must merge cleanly with zero conflicts.
Priority
Not urgent — circle back once Build With Muse Season 1 is wrapped, same as #91. Episode 08 will use a genuine same-symbol conflict example instead of this scenario, so this doesn't block the episode.
Related
- #91 — a different, unrelated inconsistency found the same week (semver classifier on reorder-only diffs). Not the same root cause as far as I can tell, but both surfaced while building out Season 1's merge/diff episodes.
Source
Found by Claude Sonnet 5 while researching Episode 08, 2026-09-12.
Fixed. Root cause and the fix were narrower than this ticket's original scope suggested.
Root cause
content_id(symbol identity,muse/plugins/code/ast_parser.py) is computed viaast.unparse(), which strips comments by design — this is intentional, documented behavior relied on broadly acrossmuse diff/muse log/rename-detection/muse code stable, so it was never going to change. A trailing-comment-only edit is therefore AST-identical to base,build_diff_ops()finds no changedchild_ops, and falls back to a whole-fileReplaceOplabeled "reformatted — no semantic change" instead of a symbol-addressedPatchOp.When two branches each comment a different function in the same file,
merge_ops()'s Step 1.5 (independence-aware blob reconstruction) had noPatchOpon either side to work with, so it just skipped reconstruction entirely — and Step 2's coarse OT check then flagged the two competing file-levelReplaceOps as a hard conflict, even though the edits never overlap.Fix (
CodePlugin.merge_ops(), two parts):cohen_transform.three_way_merge_lines()— the same primitive_independence_merge_blob()already uses for confirmed-independent symbol edits. This is unconditionally safe: a clean, non-overlapping line-level merge is correct regardless of symbol knowledge or file type. Only used when the merge reports zero conflict regions; any real overlap still falls through to the existing conflict path.ReplaceOps regardless of what Step 1.5 resolved, becauseconflict_addresseswas never filtered againstindependence_resolvedbefore Step 3 assembled the final conflict list. Filteredconflict_addressesagainstindependence_resolved's keys immediately after Step 2.This is deliberately narrower than the "thread raw source through ast_parser.py → symbol_diff.py" approach originally scoped —
content_id/AST-normalization semantics are untouched, which keeps this a small, low-risk change instead of a cross-file architectural one.Verification
muse-devagainst a disposable/tmpsandbox: two branches each add a trailing comment to a different function incalc.py. Merge now reportsstatus: mergedwith zero conflicts, and the committed file contains both comments.tests/test_comment_only_independence_merge.pyreproduces the exact scenario through the real CLI (not synthetic objects). Confirmed genuine RED (reverted the fix, test failed on the pre-fix conflict) before GREEN with the fix restored.test_core_merge_engine,test_cmd_merge,test_code_harmony_plugin,test_phase1_merge_engine,test_phase5_phantom_guard,test_phase6_unified_merge_engine,test_op_merge,test_octopus_merge,test_harmony_merge_commit_integrity,test_code_plugin,test_code_plugin_attributes,test_phase2_or_set_symbol_independence, plus the new test — all pass (636 passed, 19 skipped), zero regressions.Merged to
dev→main, pushed tolocal/staging/productionfor both branches.