bug: muse blame attributes lines to a commit that never touched the file
Summary
muse blame <file> --range attributes lines to a commit whose own
files_modified list does not include that file, and whose blobs for the
file are byte-identical to its parent — i.e. the blamed commit provably
never touched the file.
Repro
cd ~/ecosystem/musehub
muse blame src/scss/_layout.scss --range 38-41 --json
Returns attribution to sha256:659f09b2a6570076b0ec01897c95ce391467b1a11fb8fa6d3c12e88b2e0e1c77
("fix: update all list_domains/list_repos callers to cursor pagination API").
muse read sha256:659f09b2a6570076b0ec01897c95ce391467b1a11fb8fa6d3c12e88b2e0e1c77 --json \
| jq '.files_modified'
[
"musehub/api/routes/musehub/ui_domains.py",
"musehub/mcp/dispatcher.py",
"musehub/mcp/resources.py",
"musehub/services/musehub_mcp_executor.py"
]
src/scss/_layout.scss is not in that list. Confirming the file content is
identical between the blamed commit and its parent:
diff <(muse cat src/scss/_layout.scss --at sha256:659f09b2a6570076b0ec01897c95ce391467b1a11fb8fa6d3c12e88b2e0e1c77) \
<(muse cat src/scss/_layout.scss --at sha256:9a6e44e84b66174d5e1a275f1d011bb9b100a3bbafc102f7620b36e8db53d1df)
# zero output — byte-identical
Expected
muse blame should attribute each line to the nearest ancestor commit that
actually changed it (per its manifest / diff), never a commit whose
files_modified excludes the file entirely.
Impact
Blame output cannot be trusted for archaeology (e.g. "why does this rule exist, when was it introduced") — it silently returns plausible-looking but false commit history, which is worse than an error since it's not obviously wrong without independent verification.
Fixed. Root cause was in the core attribution algorithm, not the specific file.
Root cause
blame_file()(muse/core/blame.py) walkediter_ancestors()'s flattened BFS order over the full commit DAG and diffed adjacent entries of that list as if they were real parent/child pairs. That's only valid for a strictly linear chain — the moment history has a merge commit, a commit's BFS-neighbor in the flattened list is frequently some unrelated commit reached via a completely different branch, not its real parent.Confirmed exactly, against your repro: the blamed commit
659f09b2's real parent (parent_commit_id) is9a6e44e8— content-identical, confirming it never touched the file, exactly as you found. Butiter_ancestors's BFS order placed an unrelated commit (6d327a10) immediately after659f09b2in the flattened list.blame_filecompared659f09b2against that unrelated BFS-neighbor instead of its real parent, found a content difference, and permanently misattributed the line — never discovering that walking the real parent edge would have shown the file unchanged and continued the search further back.The fix
blame_file()now walks each commit's actualparent_commit_id/parent2_commit_idedges, in reverse-topological order: a commit's own outgoing edges are only explored once every real child of it (within the search's reachable ancestor set) has already had a chance to push attribution into it. This guarantees every content comparison uses a genuine commit-graph edge — never an arbitrary BFS-neighbor pair — and that each commit is visited exactly once with complete information, regardless of how many branches converge on it. The existing object_id-equality fast path (skip reading file bytes entirely when two real-parent commits share the same content) is preserved, now made properly lazy so it doesn't read bytes it doesn't need.Regression test
test_blame_never_attributes_to_commit_that_never_touched_filereproduces your exact DAG shape:base → feature(the only commit that changes the file)→ merge(an unrelated base-side commit + feature, content identical to feature, never touches the file)→ final(also untouched). Confirmed genuine RED first: the old algorithm attributed a line to the merge commit itself — the exact symptom shape you reported (attributed to a commit absent fromfiles_modified, with content byte-identical to its real parent).Live-verified against your exact repro
Line 38 now attributes to
fbd6a93ed2655("refactor: move frontend source from static/js+scss to src/ts+scss") and lines 39-41 toc1d980625041d("fix(#139): app-shell for issue detail..."). Confirmed both commits genuinely differ from their respective real parents for this exact file (direct content diff, not justfiles_modifiedpresence — matching your own verification method). Runtime actually improved slightly (3.45s vs 4.2s original), likely because the DAG-correct walk no longer wastes time comparing against unrelated BFS neighbors whose diffs could never produce useful attribution anyway.Targeted regression suite —
test_core_blame.py(15, including the new test),test_cmd_blame.py(the only direct caller ofblame_filepermuse code impact) — all pass (78 passed), zero regressions.Merged to
dev→main, pushed tolocal/staging/productionfor both branches.