gabriel / musehub public
Open #203 Bug
filed by gabriel human · 33 days ago

MIDI structured_delta always falls back to file-level ReplaceOp — diff() runs before new blob is written

0 Anchors
— Blast radius
— Churn 30d
0 Proposals
No description provided.
◎ Activity1
●
gabriel opened this issue 33 days ago
○
gabriel 33 days ago

Confirmed while building Episode 11 ("Muse Understands Music") demo material.

Root cause: muse/cli/commands/commit.py computes the structured delta at line 532:

structured_delta = plugin.diff(base_snap, snap, repo_root=root)

...before the new content's objects are written to the store at line 658:

write_object_from_path(root, object_id, root / rel_path)

For MIDI files, MidiPlugin.diff() -> _diff_modified_file() tries to load both old and new blobs via read_object() to run diff_midi_notes() (note-level LCS diff). Since the new blob isn't in the store yet, read_object() returns None for the new hash, diff_midi_notes is never even called, and the function falls straight to the ReplaceOp fallback (file-level, opaque before/after hash pair).

Confirmed reproducible: committed a 4-note phrase.mid, branched, changed one note's pitch (E4 -> F4), committed again. muse read --json --manifest on the second commit shows structured_delta.ops == [{"op": "replace", ...}] — never the PatchOp with note-level child_ops the domain schema advertises (domain-info --json claims "note-level diff and 21-dimension independent merge").

Verified the underlying note-diff logic itself is correct: calling diff_midi_notes() directly against the same two blobs (once both exist in the store, e.g. post-commit) correctly returns:

{"op": "insert", "content_summary": "F4 vel=80 @beat=1.00 dur=1.00"},
{"op": "delete", "content_summary": "E4 vel=80 @beat=1.00 dur=1.00"}

So this is purely an ordering bug in commit.py, not a defect in midi_diff.py.

Secondary issue compounding this: _diff_modified_file() in muse/plugins/midi/plugin.py wraps the deep-diff attempt in except (ValueError, Exception) (redundant — Exception already covers ValueError) and only logs at .debug level. Any failure — including this one — is silently invisible at default log verbosity, which is how the fallback went unnoticed.

Suggested fix: move the object-write loop in commit.py before the plugin.diff() call (or otherwise make the new manifest's blobs readable — e.g. by passing the live working-tree bytes directly rather than round-tripping through the not-yet-written object store).

Data-integrity-adjacent but not itself a data-loss bug — the commit content is correct, only the note-level observability of the change is lost. Deferred per usual until after the Build With Muse season wraps.