muse-understands-music.md markdown
478 lines 13.8 KB
Raw
sha256:9b02439fb79adde6039e49901dda4747995a261a3314890c391aed0012e6fa6e Move Episode 11 script into place at muse-understands-music.md Sonnet 5 3 hours ago

Episode 11 --- Muse Understands Music

Working YouTube title:
I Broke My Own Feature On Camera (MIDI, Note-Level Diff)

Thumbnail thought:
one note. four notes. one bug.

Target runtime: ~8:30


[0:00--0:20] COLD OPEN

[CAMERA --- Episode 10's closing line, on screen: "What does 'understanding music' get you?"]

GABRIEL:

Same six methods. A file format that isn't text, isn't symbols --- it's notes, velocity, pitch bend, twenty-one of these things at once. Let's find out what "understanding" actually means here.

[TITLE CARD --- fast]

MUSE UNDERSTANDS MUSIC

[Music enters --- literally, this time.]


[0:20--1:10] FOUR NOTES, ONE FILE

[TERMINAL]

$ python3 -c "import mido; [print(m) for m in mido.MidiFile('phrase.mid').tracks[0]]"
MetaMessage('set_tempo', tempo=500000, time=0)
note_on channel=0 note=60 velocity=80 time=0     # C4
note_off channel=0 note=60 velocity=0 time=480
note_on channel=0 note=64 velocity=80 time=0     # E4
note_off channel=0 note=64 velocity=0 time=480
note_on channel=0 note=67 velocity=80 time=0     # G4
note_off channel=0 note=67 velocity=0 time=480
note_on channel=0 note=72 velocity=80 time=0     # C5
note_off channel=0 note=72 velocity=0 time=480

GABRIEL VO:

C, E, G, C-up-an-octave. A real .mid file, built with a real MIDI library, not a mock. To Git this is 69 bytes of opaque binary.

[beat]

$ muse init --domain midi

GABRIEL:

To Muse, it's about to be something else entirely.


[1:10--1:50] TWENTY-ONE DIMENSIONS

[TERMINAL]

$ muse domain-info --json | jq '.domain_schema.dimensions | length'
21
$ muse domain-info --json | jq '.domain_schema.dimensions[].name'
"notes" "pitch_bend" "channel_pressure" "poly_pressure"
"cc_modulation" "cc_volume" "cc_pan" "cc_expression" "cc_sustain"
"cc_portamento" "cc_sostenuto" "cc_soft_pedal" "cc_reverb" "cc_chorus"
"cc_other" "program_change" "tempo_map" "time_signatures"
"key_signatures" "markers" "track_structure"

GABRIEL VO:

Episode 05's code domain has five dimensions --- structure, symbols, imports, variables, metadata. Same six-method protocol, same MuseDomainPlugin interface. Twenty-one dimensions here, because music has twenty-one independent ways to change.

[beat]

Most of those are independently mergeable --- two agents editing reverb and pan don't even see each other. A couple aren't: change the tempo map and it's declared non-independent, because it affects how every other dimension reads.


[1:50--2:50] ONE NOTE CHANGES

[TERMINAL]

$ muse checkout -b feat/raise-third --intent "raise the third from E to F"
$ python3 -c "
import mido
mid = mido.MidiFile('phrase.mid')
for t in mid.tracks:
    for m in t:
        if m.type in ('note_on','note_off') and m.note == 64:
            m.note = 65
mid.save('phrase.mid')
"
$ muse commit -m "Raise the third from E to F" --sign

GABRIEL:

One semitone. E to F. The kind of edit a composer makes fifty times in an afternoon. Now let's look at what Muse actually recorded.

$ muse read --json --manifest | jq '.structured_delta'
{
  "domain": "midi",
  "ops": [{
    "op": "replace",
    "address": "phrase.mid",
    "old_content_id": "sha256:ed3b5e06...",
    "new_content_id": "sha256:e3c892f5..."
  }],
  "summary": "1 file modified"
}

[beat, GABRIEL reading the output, visibly stopping]

GABRIEL:

That's not what I expected. op: replace. Whole file, opaque hash to opaque hash. That's what Git would show me. That's not "twenty-one independently mergeable dimensions" --- that's a binary diff with extra JSON around it.


[2:50--4:10] FINDING IT LIVE

[CAMERA]

Let's not just accept that. The plugin's source says it should be better than this.

[TERMINAL --- muse code cat]

$ muse code cat "muse/plugins/midi/plugin.py::_diff_modified_file"
if path.lower().endswith(".mid") and repo_root is not None:
    base_bytes = read_object(repo_root, old_hash)
    target_bytes = read_object(repo_root, new_hash)
    if base_bytes is not None and target_bytes is not None:
        try:
            child_delta = diff_midi_notes(base_bytes, target_bytes, file_path=path)
            return PatchOp(op="patch", address=path, child_ops=child_delta["ops"], ...)
        except Exception as exc:
            logger.debug("MIDI deep diff failed: %s", exc)
return ReplaceOp(op="replace", ...)   # ← what we actually got

GABRIEL VO:

There it is in the code: real note-level diffing exists, it's called diff_midi_notes, and it silently falls back to ReplaceOp the moment either blob can't be read from the object store --- logged at debug level, invisible by default.

[beat]

So which blob is missing? Let's just call the exact same function directly, by hand, against the exact same two hashes.

$ python3 -c "
from muse.core.object_store import read_object
from muse.plugins.midi.midi_diff import diff_midi_notes
old = read_object(root, 'sha256:ed3b5e06...')
new = read_object(root, 'sha256:e3c892f5...')
print(diff_midi_notes(old, new, file_path='phrase.mid'))
"
{
  "domain": "midi_notes",
  "ops": [
    {"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"}
  ],
  "summary": "1 note added, 1 note removed"
}

GABRIEL:

Works perfectly. F4 in, E4 out. Same two hashes, called directly. So the note-diff logic isn't broken --- something about when it gets called during muse commit is.

[TERMINAL --- muse code cat commit.py, two line numbers highlighted]

commit.py:532   structured_delta = plugin.diff(base_snap, snap, repo_root=root)
commit.py:658   write_object_from_path(root, object_id, root / rel_path)

GABRIEL VO:

Line 532 runs before line 658. Muse computes the note-level diff before it writes the new file's bytes into the object store. The new hash doesn't exist yet when diff_midi_notes goes looking for it. read_object returns nothing. The except Exception catches the failure silently, and every single MIDI commit, forever, has been falling back to the opaque replace.

[beat]

That's a real bug. In my own product. Found by making a demo for this exact episode.


[4:10--4:40] WHAT HAPPENS NEXT

[CAMERA]

I'm not fixing it on camera --- fixing it means reordering writes inside the commit path, and I want to actually think about the blast radius of that before I touch it. Filed:

$ muse hub issue read 203
#203  MIDI structured_delta always falls back to file-level ReplaceOp
      assignee: gabriel  label: bug

GABRIEL VO:

The commit itself is correct --- the right bytes are stored, nothing is lost. Only the observability of the change is wrong. Still a real defect, still going in the tracker, still getting fixed after this season wraps, not glossed over for a smoother episode.


[4:40--6:00] TWO BRANCHES, ONE NOTE, TWO DIFFERENT ANSWERS

[CAMERA]

The underlying diff logic works, and I can prove the merge engine still uses it correctly for conflict detection, even while commit's delta storage is broken. Watch.

[TERMINAL]

$ muse switch main
$ muse checkout -b feat/louder-melody --intent "raise velocity on the melody notes"
$ python3 -c "
import mido
mid = mido.MidiFile('phrase.mid')
for t in mid.tracks:
    for m in t:
        if m.type == 'note_on': m.velocity = 110
mid.save('phrase.mid')
"
$ muse commit -m "Increase melody velocity to 110" --sign

GABRIEL VO:

Second branch, off the original main --- before the pitch change. Same file, different attribute: this one only touches velocity.

$ muse switch main
$ muse merge feat/raise-third
status: fast_forward

GABRIEL:

First merge --- trivial, only one side touched anything since main last moved. Now the second one.

$ muse merge feat/louder-melody
{ "status": "conflict", "conflicts": ["phrase.mid"] }

GABRIEL VO:

A real conflict. Not staged --- this is what happens when one branch changes a note's pitch and another branch changes that same note's velocity, and both branches think they're the only one who touched it.


[6:00--7:00] RESOLVING BY EAR, NOT BY FLAG

[CAMERA]

No conflict markers to read --- it's a binary format, there's no <<<<<<< ours you can put inside a .mid file. So I do what Episode 08 already established: read what each side actually wanted, by hand.

[TERMINAL]

$ muse conflicts --json
{ "conflicts": [{"path": "phrase.mid", "kind": "file"}],
  "ours_commit": "...raise-third", "theirs_commit": "...louder-melody" }

GABRIEL:

Ours wanted F4. Theirs wanted velocity 110. Neither side is wrong --- they're just two true things about the same note that nobody reconciled yet. So I write the file that's actually correct: F4, velocity 110, every note.

$ python3 -c "... set velocity=110 on the F4-corrected file ..."
$ muse resolve phrase.mid
$ muse commit -m "Merge feat/louder-melody: F pitch + velocity 110" --sign

GABRIEL VO:

muse resolve, not --ours or --theirs. Neither branch alone had the answer. The merged file does.


[7:00--7:40] THE POINT, EVEN WITH THE BUG IN IT

[CAMERA]

Here's what I want you to sit with: the conflict detection worked. The merge engine correctly noticed two branches touched the same note. The problem is scoped to one function, in one file, running in the wrong order relative to one write. That's fixable. It's fixable precisely because Muse knows what a note is in the first place --- you can't have an ordering bug in a note-level diff for a domain that never modeled notes at all.

[beat]

Git wouldn't have this bug. Git also wouldn't have shown you F4 and velocity 110 as two separate, mergeable facts about the same note. You don't get the second thing without some risk of the first.


[7:40--8:15] OUT

[TERMINAL --- fading to a blank prompt]

GABRIEL VO:

Ten episodes of "here's what Muse can tell you." Next: what happens when the thing writing commits isn't a person sitting at this terminal at all.

[CAMERA]

Agents. Real ones, with real identities, making real commits --- several of which you've already watched happen in this very season.

[CUT TO BLACK]

musehub.ai


Production Notes

Episode 11 is structurally different from every episode before it: the demo doesn't just show a feature, it finds a real bug in that feature, live, and keeps going. That's a harder needle to thread than Episode 09's Harmony bug because this one is in the exact command (muse commit) every prior episode has trusted implicitly. Don't let the tone curdle into an apology --- it's a discovery, delivered with the same energy as finding gravity disagree with hotspots in Episode 10.

Why This One, Honestly, Not Worked Around

Per the season's established discipline (Episode 09's Harmony auto-apply bug), when a real defect is found while building an episode's own demo, the choice this time was to show it plainly rather than route around it with a lower-level function call dressed up as the CLI path. The one deliberate exception: the "does the underlying diff logic even work" check does call diff_midi_notes directly, on camera, but it's framed explicitly as debugging, not as what muse commit actually does --- the audience should never think that's the normal user path.

The Merge Still Has To Be Real

The Episode 11 conflict is not staged around the bug --- it's a genuine two-branch conflict (pitch changed on one side, velocity on the other, same note) that the merge engine correctly detects despite commit's structured-delta bug, because conflict detection and delta storage are different code paths. Verify this distinction holds on whatever muse build is current at record time before claiming it on camera again.

Ticket Discipline

staging#203 was filed the moment the root cause was confirmed (read the exact two line numbers in commit.py, reproduced the underlying diff function working correctly in isolation), not before confirming it wasn't user error. Assigned to gabriel, deferred until after the season, matching #90--#93.

Everything Here Is Real

phrase.mid was built with mido, a real MIDI library --- not a byte-literal fake. Every commit, branch, conflict, and the bug itself were reproduced twice against the actual current build before being described on camera. Re-run make-midi-episode11-demo.sh at record time and re-verify the exact hashes and line numbers still match --- if commit.py has since been reordered, this episode's central beat needs re-shooting, not just re-narrating.

The Seed

The viewer arrives thinking:

Okay, MIDI is a cute proof that domains aren't just "code."

They should leave thinking:

Wait --- he just found and filed a real bug in his own tool, mid-episode, and the fix was obvious once he saw it. What else is in here that nobody's looked at yet?

That question is the entire back half of the season.

File History 1 commit
sha256:9b02439fb79adde6039e49901dda4747995a261a3314890c391aed0012e6fa6e Move Episode 11 script into place at muse-understands-music.md Sonnet 5 3 hours ago