muse-attributes.md
markdown
| 1 | # `.museattributes` Reference |
| 2 | |
| 3 | `.museattributes` is a per-repository configuration file that declares merge |
| 4 | strategies for specific paths and dimensions. It lives in the repository root, |
| 5 | alongside `muse-work/`. |
| 6 | |
| 7 | --- |
| 8 | |
| 9 | ## Why Muse is different |
| 10 | |
| 11 | Git treats every file as an opaque byte sequence. If two branches both touch |
| 12 | the same file, that is a conflict — full stop. Git cannot know that one |
| 13 | collaborator edited the drumbeat rhythm while another adjusted the key-change |
| 14 | harmonic, because it has no concept of *dimensions* within a file. |
| 15 | |
| 16 | Muse does. A MIDI file has five orthogonal axes of change: |
| 17 | |
| 18 | | Dimension | What it covers | |
| 19 | |---|---| |
| 20 | | `melodic` | `note_on` / `note_off` events — the notes played | |
| 21 | | `rhythmic` | Same events as `melodic` — timing is inseparable from pitch in the MIDI model; provided as a distinct user-facing label | |
| 22 | | `harmonic` | `pitchwheel` events | |
| 23 | | `dynamic` | `control_change` events | |
| 24 | | `structural` | Tempo, time-signature, key-signature, program changes, markers | |
| 25 | |
| 26 | When two branches both modify the same `.mid` file, Muse asks: |
| 27 | *did they change the same dimension?* If not, the merge is clean — no human |
| 28 | intervention required. `.museattributes` is where you encode domain knowledge |
| 29 | to guide this process. |
| 30 | |
| 31 | --- |
| 32 | |
| 33 | ## File location |
| 34 | |
| 35 | ``` |
| 36 | my-project/ |
| 37 | ├── .muse/ ← VCS metadata |
| 38 | ├── muse-work/ ← tracked workspace |
| 39 | ├── .museignore ← snapshot exclusion rules |
| 40 | └── .museattributes ← merge strategies (this file) |
| 41 | ``` |
| 42 | |
| 43 | --- |
| 44 | |
| 45 | ## File format |
| 46 | |
| 47 | ``` |
| 48 | <path-pattern> <dimension> <strategy> |
| 49 | ``` |
| 50 | |
| 51 | - **path-pattern** — an `fnmatch` glob matched against workspace-relative POSIX |
| 52 | paths (e.g. `drums/*`, `src/models/**`, `*`). |
| 53 | - **dimension** — a domain-defined dimension name or `*` to match all dimensions. |
| 54 | - **strategy** — `ours | theirs | union | auto | manual` |
| 55 | |
| 56 | Lines beginning with `#` and blank lines are ignored. **First matching rule |
| 57 | wins.** |
| 58 | |
| 59 | --- |
| 60 | |
| 61 | ## Strategies |
| 62 | |
| 63 | | Strategy | Behaviour | |
| 64 | |---|---| |
| 65 | | `ours` | Take the current branch's version. Skip conflict detection for this path/dimension. | |
| 66 | | `theirs` | Take the incoming branch's version. Skip conflict detection for this path/dimension. | |
| 67 | | `union` | Include both sides' changes. Falls through to auto-merge logic (equivalent to `auto` at file level; reserved for future sub-event union). | |
| 68 | | `auto` | Let the merge engine decide. Default when no rule matches. | |
| 69 | | `manual` | Flag this path/dimension for mandatory human resolution, even if the engine would auto-resolve it. | |
| 70 | |
| 71 | --- |
| 72 | |
| 73 | ## Merge algorithm |
| 74 | |
| 75 | `muse merge` applies `.museattributes` in three sequential passes: |
| 76 | |
| 77 | ### Pass 1 — File-level strategy |
| 78 | |
| 79 | For each path that both branches changed, `resolve_strategy(rules, path, "*")` |
| 80 | is called. |
| 81 | |
| 82 | - `ours` → take the left branch's version; path is removed from the conflict |
| 83 | list. |
| 84 | - `theirs` → take the right branch's version; path is removed from the conflict |
| 85 | list. |
| 86 | - `manual` → keep in conflict list even if the engine would auto-merge. |
| 87 | - `auto` / `union` → proceed to Pass 2. |
| 88 | |
| 89 | ### Pass 2 — Dimension-level merge (MIDI files) |
| 90 | |
| 91 | For `.mid` files that survive Pass 1 (no file-level rule resolved them), Muse: |
| 92 | |
| 93 | 1. Reads the base, left, and right MIDI content from the object store. |
| 94 | 2. Parses each file and buckets events into four internal dimensions: |
| 95 | `notes`, `harmonic`, `dynamic`, `structural`. |
| 96 | 3. For each dimension, determines which sides changed it: |
| 97 | - **Unchanged** → keep base. |
| 98 | - **One side only** → take that side automatically. |
| 99 | - **Both sides** → call `resolve_strategy(rules, path, dim)` for each |
| 100 | user-facing alias of that dimension. |
| 101 | - `ours` or `theirs` → apply and continue. |
| 102 | - Anything else → dimension conflict; fall back to Pass 3. |
| 103 | 4. If all dimensions are resolved, reconstructs a merged MIDI file (type 0, |
| 104 | preserving `ticks_per_beat` from the base) and stores it as a new object. |
| 105 | |
| 106 | The merged file contains the winning dimension events interleaved by absolute |
| 107 | tick time. |
| 108 | |
| 109 | ### Pass 3 — True conflict |
| 110 | |
| 111 | Paths and dimensions that no rule resolves are reported as conflicts. |
| 112 | `MERGE_STATE.json` is written and `muse merge` exits non-zero. |
| 113 | |
| 114 | ### Manual forcing |
| 115 | |
| 116 | For paths that auto-merged cleanly on both sides, a `manual` rule in |
| 117 | `.museattributes` forces them into the conflict list anyway. This is useful for |
| 118 | contractually sensitive files that always require human sign-off. |
| 119 | |
| 120 | --- |
| 121 | |
| 122 | ## Music domain examples |
| 123 | |
| 124 | ``` |
| 125 | # Drums are always authoritative — take our version on every dimension: |
| 126 | drums/* * ours |
| 127 | |
| 128 | # Accept a collaborator's harmonic changes on key instruments: |
| 129 | keys/* harmonic theirs |
| 130 | bass/* harmonic theirs |
| 131 | |
| 132 | # Require manual review for all structural changes project-wide: |
| 133 | * structural manual |
| 134 | |
| 135 | # Default for everything else: |
| 136 | * * auto |
| 137 | ``` |
| 138 | |
| 139 | ### What this achieves |
| 140 | |
| 141 | If both branches modify `keys/piano.mid`: |
| 142 | |
| 143 | - The `harmonic` dimension → `theirs` (collaborator's pitch-bends win). |
| 144 | - The `notes` dimension → no matching rule → dimension-level auto-merge. |
| 145 | - If only one side changed notes → clean. |
| 146 | - If both sides changed notes → conflict (no rule resolved it). |
| 147 | - The `structural` dimension → `manual` → always flagged for review. |
| 148 | |
| 149 | --- |
| 150 | |
| 151 | ## Generic domain examples |
| 152 | |
| 153 | The `.museattributes` format is not music-specific. Domain plugins define their |
| 154 | own dimension names. Path patterns and strategy syntax are identical. |
| 155 | |
| 156 | ### Genomics |
| 157 | |
| 158 | ``` |
| 159 | # Reference sequence is always canonical: |
| 160 | reference/* * ours |
| 161 | |
| 162 | # Accept collaborator's annotations: |
| 163 | annotations/* semantic theirs |
| 164 | |
| 165 | # All structural edits require manual review: |
| 166 | * structural manual |
| 167 | |
| 168 | # Default: |
| 169 | * * auto |
| 170 | ``` |
| 171 | |
| 172 | ### Scientific simulation |
| 173 | |
| 174 | ``` |
| 175 | # Boundary conditions are owned by the lead author: |
| 176 | boundary/* * ours |
| 177 | |
| 178 | # Accept collaborator's solver parameters: |
| 179 | params/* numeric theirs |
| 180 | |
| 181 | # Require sign-off on mesh topology changes: |
| 182 | mesh/* topology manual |
| 183 | ``` |
| 184 | |
| 185 | --- |
| 186 | |
| 187 | ## CLI |
| 188 | |
| 189 | ```bash |
| 190 | muse attributes # tabular display of rules |
| 191 | muse attributes --json # JSON array for scripting |
| 192 | ``` |
| 193 | |
| 194 | Example output: |
| 195 | |
| 196 | ``` |
| 197 | Path pattern Dimension Strategy |
| 198 | ------------ ---------- -------- |
| 199 | drums/* * ours |
| 200 | keys/* harmonic theirs |
| 201 | * structural manual |
| 202 | * * auto |
| 203 | ``` |
| 204 | |
| 205 | --- |
| 206 | |
| 207 | ## `muse merge` output with attributes |
| 208 | |
| 209 | When `.museattributes` auto-resolves a conflict, `muse merge` reports it: |
| 210 | |
| 211 | ``` |
| 212 | ✔ [ours] drums/kick.mid |
| 213 | ✔ dimension-merge: keys/piano.mid (harmonic=right, notes=left, dynamic=base, structural=base) |
| 214 | Merged 'feature/harmonics' into 'main' (a1b2c3d4) |
| 215 | ``` |
| 216 | |
| 217 | --- |
| 218 | |
| 219 | ## Notes |
| 220 | |
| 221 | - `ours` and `theirs` are positional: `ours` = the branch merging INTO (current |
| 222 | HEAD), `theirs` = the branch merging FROM (incoming). |
| 223 | - Path patterns follow POSIX conventions (forward slashes). |
| 224 | - The file is optional. Its absence has no effect on merge correctness — all |
| 225 | paths use `auto`. |
| 226 | - `union` at the file level is equivalent to `auto` in the current |
| 227 | implementation. True event-level union (include both sides' note events) |
| 228 | is reserved for a future release. |
| 229 | - MIDI dimension merge reconstructs a type-0 (single-track) file. The |
| 230 | original multi-track structure is preserved when all events fit into one |
| 231 | track; multi-track reconstruction is a planned enhancement. |
| 232 | |
| 233 | --- |
| 234 | |
| 235 | ## Resolution precedence |
| 236 | |
| 237 | Rules are evaluated top-to-bottom. The first rule where **both** `path-pattern` |
| 238 | and `dimension` match (via `fnmatch`) wins. |
| 239 | |
| 240 | If no rule matches, `auto` is returned. |
| 241 | |
| 242 | --- |
| 243 | |
| 244 | ## Implementation |
| 245 | |
| 246 | Parsing and strategy resolution live in `muse/core/attributes.py`: |
| 247 | |
| 248 | ```python |
| 249 | from muse.core.attributes import load_attributes, resolve_strategy |
| 250 | |
| 251 | rules = load_attributes(repo_root) # reads .museattributes |
| 252 | strategy = resolve_strategy(rules, "keys/piano.mid", "harmonic") # → "theirs" |
| 253 | ``` |
| 254 | |
| 255 | MIDI dimension merge lives in `muse/plugins/music/midi_merge.py`: |
| 256 | |
| 257 | ```python |
| 258 | from muse.plugins.music.midi_merge import extract_dimensions, merge_midi_dimensions |
| 259 | |
| 260 | dims = extract_dimensions(midi_bytes) # → MidiDimensions |
| 261 | result = merge_midi_dimensions( # → (merged_bytes, report) | None |
| 262 | base_bytes, left_bytes, right_bytes, rules, "keys/piano.mid" |
| 263 | ) |
| 264 | ``` |