cgcardona / muse public
muse-attributes.md markdown
264 lines 8.1 KB
0e0cbf44 feat: .museattributes + multidimensional MIDI merge (#11) Gabriel Cardona <cgcardona@gmail.com> 3d ago
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 ```