muse-attributes.md
markdown
| 1 | # `.museattributes` Reference |
| 2 | |
| 3 | > **Format:** TOML · **Location:** repository root (next to `.muse/`) |
| 4 | > **Loaded by:** `muse merge`, `muse cherry-pick`, `muse attributes` |
| 5 | > **Updated:** v0.1.2 — added `base` strategy, `comment` and `priority` fields, |
| 6 | > priority-based rule ordering, and full code-domain support. |
| 7 | |
| 8 | `.museattributes` declares per-path, per-dimension merge strategy overrides for |
| 9 | a Muse repository. It uses TOML syntax for consistency with `.muse/config.toml` |
| 10 | and to allow richer structure (comments, typed values, named sections). |
| 11 | |
| 12 | The file is domain-agnostic — the same format works for MIDI, code, genomics, |
| 13 | 3D design, scientific simulation, or any future domain. |
| 14 | |
| 15 | --- |
| 16 | |
| 17 | ## File Structure |
| 18 | |
| 19 | ```toml |
| 20 | # .museattributes |
| 21 | # Merge strategy overrides for this repository. |
| 22 | # Documentation: docs/reference/muse-attributes.md |
| 23 | |
| 24 | [meta] |
| 25 | domain = "midi" # optional — validated against .muse/repo.json "domain" |
| 26 | |
| 27 | [[rules]] |
| 28 | path = "drums/*" # fnmatch glob |
| 29 | dimension = "*" # domain axis, or "*" for any |
| 30 | strategy = "ours" # resolution strategy |
| 31 | comment = "Drums are always authored on this branch." |
| 32 | priority = 20 # higher = evaluated first |
| 33 | |
| 34 | [[rules]] |
| 35 | path = "keys/*" |
| 36 | dimension = "pitch_bend" |
| 37 | strategy = "theirs" |
| 38 | comment = "Remote always has the better pitch-bend automation." |
| 39 | priority = 15 |
| 40 | |
| 41 | [[rules]] |
| 42 | path = "*" |
| 43 | dimension = "*" |
| 44 | strategy = "auto" |
| 45 | ``` |
| 46 | |
| 47 | --- |
| 48 | |
| 49 | ## Sections |
| 50 | |
| 51 | ### `[meta]` (optional) |
| 52 | |
| 53 | | Key | Type | Description | |
| 54 | |-----|------|-------------| |
| 55 | | `domain` | string | The domain this file targets. When present, validated against `.muse/repo.json "domain"`. A mismatch logs a warning but does not abort. | |
| 56 | |
| 57 | `[meta]` has no effect on merge resolution. It provides a machine-readable |
| 58 | declaration of the intended domain, enabling tooling to warn when rules may be |
| 59 | targeting the wrong plugin. |
| 60 | |
| 61 | --- |
| 62 | |
| 63 | ### `[[rules]]` (array) |
| 64 | |
| 65 | Each `[[rules]]` entry is a single merge strategy rule. |
| 66 | |
| 67 | | Field | Type | Required | Default | Description | |
| 68 | |-------|------|----------|---------|-------------| |
| 69 | | `path` | string | **yes** | — | `fnmatch` glob matched against workspace-relative POSIX paths (e.g. `"drums/*"`, `"src/**/*.py"`). | |
| 70 | | `dimension` | string | **yes** | — | Domain axis name (e.g. `"pitch_bend"`, `"symbols"`) or `"*"` to match any dimension. | |
| 71 | | `strategy` | string | **yes** | — | One of the six strategies (see below). | |
| 72 | | `comment` | string | no | `""` | Free-form documentation explaining *why* the rule exists. Ignored at runtime. | |
| 73 | | `priority` | integer | no | `0` | Ordering weight. Higher-priority rules are evaluated before lower-priority ones, regardless of their position in the file. Ties preserve declaration order. | |
| 74 | |
| 75 | --- |
| 76 | |
| 77 | ## Strategies |
| 78 | |
| 79 | | Strategy | Behaviour | |
| 80 | |----------|-----------| |
| 81 | | `auto` | **Default.** Defer to the three-way merge engine. Unmatched paths always use this. | |
| 82 | | `ours` | Take the current-branch (left) version. Remove the path from the conflict list. | |
| 83 | | `theirs` | Take the incoming-branch (right) version. Remove the path from the conflict list. | |
| 84 | | `union` | Include **all** additions from both sides. Deletions are honoured only when both sides agree. Best for independent element sets (MIDI notes, symbol additions, import sets, genomic mutations). Falls back to `ours` for binary blobs where full unification is impossible. | |
| 85 | | `base` | Revert to the **common merge-base version** — discard changes from *both* branches. Use this for generated files, lock files, or any path that must stay at a known-good state during a merge. | |
| 86 | | `manual` | Force the path into the conflict list for human review, even when the engine would auto-resolve it. | |
| 87 | |
| 88 | --- |
| 89 | |
| 90 | ## Rule Evaluation Order |
| 91 | |
| 92 | Rules are sorted by `priority` (descending) then by declaration order (ascending) |
| 93 | before evaluation. The **first matching rule wins**. |
| 94 | |
| 95 | ``` |
| 96 | Rule evaluation order = sort by -priority, then by file position |
| 97 | ``` |
| 98 | |
| 99 | This means: |
| 100 | |
| 101 | - A `priority = 100` rule declared *anywhere* in the file always beats a |
| 102 | `priority = 0` catch-all, no matter where either appears. |
| 103 | - Rules with equal `priority` preserve the order they were written in. |
| 104 | - When no rule matches, the strategy falls back to `"auto"`. |
| 105 | |
| 106 | **Recommended pattern:** assign high `priority` values to narrow, safety-critical |
| 107 | rules (secrets, generated files, master tracks); assign `priority = 0` to your |
| 108 | broad catch-all rule. |
| 109 | |
| 110 | --- |
| 111 | |
| 112 | ## Matching Rules |
| 113 | |
| 114 | - **Path matching** uses Python's `fnmatch.fnmatch()`. Patterns are matched |
| 115 | against workspace-relative POSIX path strings (forward slashes, no leading `/`). |
| 116 | `*` matches within a directory segment; `**` matches across segments. |
| 117 | - **Dimension matching**: `"*"` in the `dimension` field matches any dimension. |
| 118 | A named dimension (e.g. `"pitch_bend"`) matches only that exact name. |
| 119 | - **When the caller passes `dimension="*"`**, any rule dimension matches — this is |
| 120 | used when issuing a file-level strategy query that does not target a specific axis. |
| 121 | |
| 122 | --- |
| 123 | |
| 124 | ## Domain Integration |
| 125 | |
| 126 | ### MIDI domain |
| 127 | |
| 128 | Rules are applied at two levels: |
| 129 | |
| 130 | 1. **File level** — strategy resolved against the full file path and `dimension="*"`. |
| 131 | `ours` / `theirs` / `base` / `union` / `manual` all fire before any MIDI-specific |
| 132 | processing. |
| 133 | 2. **Dimension level** — for `.mid` files not resolved at file level, |
| 134 | `merge_midi_dimensions` checks the named dimension (e.g. `"notes"`, |
| 135 | `"pitch_bend"`) against the rule list. Dimension aliases (e.g. `"tempo"` for |
| 136 | `"tempo_map"`) are also matched. |
| 137 | |
| 138 | **MIDI dimensions** (usable in `dimension`): |
| 139 | `notes`, `pitch_bend`, `channel_pressure`, `poly_pressure`, |
| 140 | `cc_modulation`, `cc_volume`, `cc_pan`, `cc_expression`, |
| 141 | `cc_sustain`, `cc_portamento`, `cc_sostenuto`, `cc_soft_pedal`, |
| 142 | `cc_reverb`, `cc_chorus`, `cc_other`, `program_change`, |
| 143 | `tempo_map`, `time_signatures`, `key_signatures`, `markers`, |
| 144 | `track_structure` |
| 145 | |
| 146 | **Aliases**: `aftertouch`, `poly_aftertouch`, `modulation`, `volume`, `pan`, |
| 147 | `expression`, `sustain`, `portamento`, `sostenuto`, `soft_pedal`, `reverb`, |
| 148 | `chorus`, `automation`, `program`, `tempo`, `time_sig`, `key_sig` |
| 149 | |
| 150 | **Non-independent dimensions** (`tempo_map`, `time_signatures`, `track_structure`): |
| 151 | a conflict in any of these blocks the entire file merge, because they are |
| 152 | structurally coupled. |
| 153 | |
| 154 | ### Code domain |
| 155 | |
| 156 | Rules are applied at two levels: |
| 157 | |
| 158 | 1. **File level** — inside `CodePlugin.merge()`, strategy resolved against each |
| 159 | file path and `dimension="*"`. All six strategies are fully implemented. |
| 160 | `manual` also fires on one-sided auto-resolved paths (i.e. a path where only |
| 161 | one branch changed — `manual` forces it into the conflict list anyway). |
| 162 | 2. **Symbol level** — inside `CodePlugin.merge_ops()`, symbol-level conflict |
| 163 | addresses (`"src/utils.py::calculate_total"`) are checked by extracting the |
| 164 | file path and calling `resolve_strategy`. A `path = "src/**/*.py"` / |
| 165 | `strategy = "ours"` rule suppresses symbol-level conflicts inside those files, |
| 166 | not just file-level manifest conflicts. |
| 167 | |
| 168 | **Code dimensions** (usable in `dimension`): |
| 169 | `structure`, `symbols`, `imports`, `variables`, `metadata` |
| 170 | |
| 171 | --- |
| 172 | |
| 173 | ## Examples |
| 174 | |
| 175 | ### MIDI — drums always ours, pitch-bend from remote, union on stems |
| 176 | |
| 177 | ```toml |
| 178 | [meta] |
| 179 | domain = "midi" |
| 180 | |
| 181 | [[rules]] |
| 182 | path = "master.mid" |
| 183 | dimension = "*" |
| 184 | strategy = "manual" |
| 185 | comment = "Master track must always be reviewed by a human." |
| 186 | priority = 100 |
| 187 | |
| 188 | [[rules]] |
| 189 | path = "drums/*" |
| 190 | dimension = "*" |
| 191 | strategy = "ours" |
| 192 | comment = "Drum tracks are always authored on this branch." |
| 193 | priority = 20 |
| 194 | |
| 195 | [[rules]] |
| 196 | path = "keys/*.mid" |
| 197 | dimension = "pitch_bend" |
| 198 | strategy = "theirs" |
| 199 | comment = "Remote always has better pitch-bend automation." |
| 200 | priority = 15 |
| 201 | |
| 202 | [[rules]] |
| 203 | path = "stems/*" |
| 204 | dimension = "notes" |
| 205 | strategy = "union" |
| 206 | comment = "Combine note additions from both arrangers." |
| 207 | |
| 208 | [[rules]] |
| 209 | path = "mixdown.mid" |
| 210 | dimension = "*" |
| 211 | strategy = "base" |
| 212 | comment = "Mixdown is generated — revert to ancestor during merge." |
| 213 | |
| 214 | [[rules]] |
| 215 | path = "*" |
| 216 | dimension = "*" |
| 217 | strategy = "auto" |
| 218 | ``` |
| 219 | |
| 220 | ### Code — generated files reverted, test additions unioned, core reviewed |
| 221 | |
| 222 | ```toml |
| 223 | [meta] |
| 224 | domain = "code" |
| 225 | |
| 226 | [[rules]] |
| 227 | path = "config/secrets.*" |
| 228 | dimension = "*" |
| 229 | strategy = "manual" |
| 230 | comment = "Secrets require human review — never auto-merge." |
| 231 | priority = 100 |
| 232 | |
| 233 | [[rules]] |
| 234 | path = "src/generated/**" |
| 235 | dimension = "*" |
| 236 | strategy = "base" |
| 237 | comment = "Generated — always revert to ancestor; re-run codegen after merge." |
| 238 | priority = 30 |
| 239 | |
| 240 | [[rules]] |
| 241 | path = "src/core/**" |
| 242 | dimension = "*" |
| 243 | strategy = "manual" |
| 244 | comment = "Core changes need human review on every merge." |
| 245 | priority = 25 |
| 246 | |
| 247 | [[rules]] |
| 248 | path = "tests/**" |
| 249 | dimension = "symbols" |
| 250 | strategy = "union" |
| 251 | comment = "Test additions from both branches are always safe to combine." |
| 252 | |
| 253 | [[rules]] |
| 254 | path = "src/**/*.py" |
| 255 | dimension = "imports" |
| 256 | strategy = "union" |
| 257 | comment = "Import sets are independent; accumulate additions from both sides." |
| 258 | |
| 259 | [[rules]] |
| 260 | path = "package-lock.json" |
| 261 | dimension = "*" |
| 262 | strategy = "ours" |
| 263 | comment = "Lock file is managed by this branch's CI." |
| 264 | |
| 265 | [[rules]] |
| 266 | path = "*" |
| 267 | dimension = "*" |
| 268 | strategy = "auto" |
| 269 | ``` |
| 270 | |
| 271 | ### Genomics — reference always ours, mutation sets unioned |
| 272 | |
| 273 | ```toml |
| 274 | [meta] |
| 275 | domain = "genomics" |
| 276 | |
| 277 | [[rules]] |
| 278 | path = "reference/*" |
| 279 | dimension = "*" |
| 280 | strategy = "ours" |
| 281 | comment = "Reference sequence is always maintained on main." |
| 282 | priority = 50 |
| 283 | |
| 284 | [[rules]] |
| 285 | path = "mutations/*" |
| 286 | dimension = "*" |
| 287 | strategy = "union" |
| 288 | comment = "Accumulate mutations from both experimental branches." |
| 289 | |
| 290 | [[rules]] |
| 291 | path = "*" |
| 292 | dimension = "*" |
| 293 | strategy = "auto" |
| 294 | ``` |
| 295 | |
| 296 | ### Force manual review on all structural changes (any domain) |
| 297 | |
| 298 | ```toml |
| 299 | [[rules]] |
| 300 | path = "*" |
| 301 | dimension = "track_structure" |
| 302 | strategy = "manual" |
| 303 | priority = 50 |
| 304 | |
| 305 | [[rules]] |
| 306 | path = "*" |
| 307 | dimension = "*" |
| 308 | strategy = "auto" |
| 309 | ``` |
| 310 | |
| 311 | --- |
| 312 | |
| 313 | ## `applied_strategies` in MergeResult |
| 314 | |
| 315 | After a merge, `MergeResult.applied_strategies` is a `dict[str, str]` mapping |
| 316 | each path (or symbol address for the code domain) where a `.museattributes` rule |
| 317 | fired to the strategy that was applied. The `muse merge` CLI prints this as: |
| 318 | |
| 319 | ``` |
| 320 | ✔ [ours] drums/kick.mid |
| 321 | ✔ [base] mixdown.mid |
| 322 | ✔ [union] stems/bass.mid |
| 323 | ✔ [manual] master.mid |
| 324 | ``` |
| 325 | |
| 326 | Paths resolved by the default `"auto"` strategy are not included — only explicit |
| 327 | overrides appear in the map. |
| 328 | |
| 329 | --- |
| 330 | |
| 331 | ## Generated Template |
| 332 | |
| 333 | `muse init --domain <name>` writes a fully-commented template to the repository |
| 334 | root. The template documents all six strategies, all five rule fields, and |
| 335 | includes annotated examples for MIDI, code, and generic repositories. |
| 336 | |
| 337 | --- |
| 338 | |
| 339 | ## Related |
| 340 | |
| 341 | - `.muse/config.toml` — per-repository user, auth, remote, and domain configuration |
| 342 | - `.museignore` — snapshot exclusion rules (TOML; `[global]` + `[domain.<name>]` sections; paths excluded from `muse commit`) |
| 343 | - `muse attributes` — CLI command to display current rules and `[meta]` domain |
| 344 | - `docs/reference/type-contracts.md` — `AttributeRule`, `MuseAttributesFile`, `MergeResult` TypedDict definitions |
| 345 | - `docs/reference/code-domain.md` — code domain schema and dimensions |