museignore.md
markdown
| 1 | # `.museignore` Reference |
| 2 | |
| 3 | > **Format:** TOML · **Location:** repository root (next to `.muse/`) |
| 4 | > **Loaded by:** every `plugin.snapshot()` call via `muse.core.ignore` |
| 5 | |
| 6 | `.museignore` tells Muse which files to exclude from every snapshot. |
| 7 | It lives in the **repository root** (the directory that contains `.muse/` and |
| 8 | `state/`) and uses TOML syntax for consistency with `.muse/config.toml` |
| 9 | and `.museattributes`. |
| 10 | |
| 11 | --- |
| 12 | |
| 13 | ## Why it matters |
| 14 | |
| 15 | `muse commit` snapshots everything in `state/`. Without `.museignore`, |
| 16 | OS artifacts (`.DS_Store`), DAW temp files (`*.bak`, `*.tmp`), rendered |
| 17 | previews, and build outputs enter the content-addressed object store and |
| 18 | contribute to diff noise on every commit. |
| 19 | |
| 20 | `.museignore` lets you declare — once, in a machine-readable file — exactly |
| 21 | what belongs in version history and what does not. |
| 22 | |
| 23 | --- |
| 24 | |
| 25 | ## File location |
| 26 | |
| 27 | ``` |
| 28 | my-project/ |
| 29 | ├── .muse/ ← VCS metadata |
| 30 | ├── state/ ← tracked workspace (content here is snapshotted) |
| 31 | ├── .museignore ← ignore rules (lives here, next to state/) |
| 32 | └── .museattributes ← merge strategies |
| 33 | ``` |
| 34 | |
| 35 | --- |
| 36 | |
| 37 | ## File structure |
| 38 | |
| 39 | `.museignore` is a TOML file with two kinds of sections: |
| 40 | |
| 41 | ```toml |
| 42 | # .museignore |
| 43 | # Ignore rules for this repository. |
| 44 | # Docs: docs/reference/museignore.md |
| 45 | |
| 46 | [global] |
| 47 | # Patterns applied to every domain. |
| 48 | # Gitignore-compatible glob syntax. Last match wins. |
| 49 | # Prefix a pattern with ! to un-ignore a previously matched path. |
| 50 | patterns = [ |
| 51 | ".DS_Store", |
| 52 | "Thumbs.db", |
| 53 | "*.tmp", |
| 54 | "*.log", |
| 55 | ] |
| 56 | |
| 57 | [domain.midi] |
| 58 | # Patterns applied only when the active domain plugin is "midi". |
| 59 | patterns = [ |
| 60 | "*.bak", |
| 61 | "*.autosave", |
| 62 | "/renders/", |
| 63 | "/exports/", |
| 64 | ] |
| 65 | |
| 66 | [domain.code] |
| 67 | # Patterns applied only when the active domain plugin is "code". |
| 68 | patterns = [ |
| 69 | "__pycache__/", |
| 70 | "*.pyc", |
| 71 | "node_modules/", |
| 72 | "dist/", |
| 73 | "build/", |
| 74 | ".venv/", |
| 75 | ] |
| 76 | ``` |
| 77 | |
| 78 | --- |
| 79 | |
| 80 | ## Sections |
| 81 | |
| 82 | ### `[global]` (optional) |
| 83 | |
| 84 | Patterns in `[global]` are loaded first and applied to **every domain**. |
| 85 | This is the right place for OS artifacts and truly cross-cutting rules. |
| 86 | |
| 87 | ### `[domain.<name>]` (optional, repeatable) |
| 88 | |
| 89 | Patterns in `[domain.<name>]` are applied **only when the active domain |
| 90 | plugin matches `<name>`**. Use the same string your plugin reports as its |
| 91 | domain tag (e.g. `"midi"`, `"code"`, `"genomics"`). |
| 92 | |
| 93 | Patterns from all other `[domain.*]` sections are never loaded. |
| 94 | |
| 95 | --- |
| 96 | |
| 97 | ## Evaluation order |
| 98 | |
| 99 | When `muse` runs any command that reads the workspace: |
| 100 | |
| 101 | 1. `[global]` patterns are loaded in array order. |
| 102 | 2. The active domain's `[domain.<name>]` patterns are appended in array order. |
| 103 | 3. Each file path is tested against the combined list — **last matching rule wins**. |
| 104 | 4. A negation rule (`!pattern`) can un-ignore a path matched by an earlier rule. |
| 105 | |
| 106 | This means a `[domain.midi]` negation rule can override a `[global]` ignore, |
| 107 | and vice versa — just put the rule you want to win later in the list. |
| 108 | |
| 109 | --- |
| 110 | |
| 111 | ## Pattern syntax |
| 112 | |
| 113 | Each string in a `patterns` array uses gitignore-compatible glob syntax: |
| 114 | |
| 115 | | Syntax | Meaning | |
| 116 | |--------|---------| |
| 117 | | `*.ext` | Ignore all files with this extension, at any depth | |
| 118 | | `name` | Ignore any file named exactly `name`, at any depth | |
| 119 | | `dir/*.ext` | Ignore matching files inside `dir/` at that exact depth | |
| 120 | | `**/name` | Ignore `name` inside any subdirectory at any depth | |
| 121 | | `name/` | Directory pattern — silently skipped (Muse tracks files, not directories) | |
| 122 | | `/pattern` | Anchor to root — only matches at the top level of `state/` | |
| 123 | | `!pattern` | Negate — un-ignore a previously matched path | |
| 124 | |
| 125 | ### Patterns without a `/` |
| 126 | |
| 127 | Matched against the **filename only**, so they apply at every depth: |
| 128 | |
| 129 | ``` |
| 130 | *.tmp → ignores tracks/session.tmp, session.tmp, and a/b/c.tmp |
| 131 | .DS_Store → ignores any file named .DS_Store at any depth |
| 132 | ``` |
| 133 | |
| 134 | ### Patterns with an embedded `/` |
| 135 | |
| 136 | Matched against the **full relative path** from the right: |
| 137 | |
| 138 | ``` |
| 139 | tracks/*.tmp → ignores tracks/session.tmp |
| 140 | does NOT ignore exports/tracks/session.tmp |
| 141 | **/cache/*.dat → ignores a/b/cache/index.dat |
| 142 | also ignores cache/index.dat |
| 143 | ``` |
| 144 | |
| 145 | ### Anchored patterns (leading `/`) |
| 146 | |
| 147 | Matched against the **full path from the root** — only the top level of `state/`: |
| 148 | |
| 149 | ``` |
| 150 | /renders/ → directory entry at root (directory patterns skipped for files) |
| 151 | /scratch.mid → ignores scratch.mid at the root of state/ |
| 152 | does NOT ignore tracks/scratch.mid |
| 153 | ``` |
| 154 | |
| 155 | ### Negation (`!pattern`) |
| 156 | |
| 157 | Re-includes a path that was previously ignored: |
| 158 | |
| 159 | ```toml |
| 160 | [global] |
| 161 | patterns = [ |
| 162 | "*.bak", |
| 163 | "!tracks/keeper.bak", # keeper.bak is NOT ignored despite *.bak above |
| 164 | ] |
| 165 | ``` |
| 166 | |
| 167 | ```toml |
| 168 | [global] |
| 169 | patterns = ["*.bak"] |
| 170 | |
| 171 | [domain.midi] |
| 172 | patterns = ["!session.bak"] # domain-level negation overrides global ignore |
| 173 | ``` |
| 174 | |
| 175 | --- |
| 176 | |
| 177 | ## Dotfiles are always excluded |
| 178 | |
| 179 | Regardless of `.museignore`, any file whose **name** begins with `.` is |
| 180 | always excluded from snapshots by the built-in plugin rule. This prevents |
| 181 | OS metadata files (`.DS_Store`, `._.DS_Store`) and editor state from |
| 182 | accumulating in the object store. |
| 183 | |
| 184 | --- |
| 185 | |
| 186 | ## Domain plugin contract |
| 187 | |
| 188 | Every domain plugin that implements `snapshot(live_state)` with a |
| 189 | `pathlib.Path` argument **must** honour `.museignore`. Use the helpers |
| 190 | provided by `muse.core.ignore`: |
| 191 | |
| 192 | ```python |
| 193 | from muse.core.ignore import is_ignored, load_ignore_config, resolve_patterns |
| 194 | |
| 195 | def snapshot(self, live_state: LiveState) -> StateSnapshot: |
| 196 | if isinstance(live_state, pathlib.Path): |
| 197 | workdir = live_state |
| 198 | repo_root = workdir.parent # .museignore lives here |
| 199 | # load_ignore_config returns the full TOML config. |
| 200 | # resolve_patterns merges global + domain-specific patterns. |
| 201 | patterns = resolve_patterns(load_ignore_config(repo_root), self.DOMAIN) |
| 202 | files = {} |
| 203 | for file_path in sorted(workdir.rglob("*")): |
| 204 | if not file_path.is_file(): |
| 205 | continue |
| 206 | rel = file_path.relative_to(workdir).as_posix() |
| 207 | if is_ignored(rel, patterns): |
| 208 | continue |
| 209 | files[rel] = hash_file(file_path) |
| 210 | return {"files": files, "domain": self.DOMAIN} |
| 211 | return live_state |
| 212 | ``` |
| 213 | |
| 214 | Patterns from `[domain.<other>]` sections are never loaded — each plugin |
| 215 | only sees global patterns plus its own domain section. |
| 216 | |
| 217 | --- |
| 218 | |
| 219 | ## Interaction with `.museattributes` |
| 220 | |
| 221 | `.museignore` and `.museattributes` are independent: |
| 222 | |
| 223 | - `.museignore` controls **what enters the snapshot** at commit time. |
| 224 | - `.museattributes` controls **how conflicts are resolved** during merge. |
| 225 | |
| 226 | A file ignored by `.museignore` is never committed, so it never appears in |
| 227 | a merge and `.museattributes` rules never apply to it. |
| 228 | |
| 229 | --- |
| 230 | |
| 231 | ## Domain-specific recommended configurations |
| 232 | |
| 233 | ### MIDI / music |
| 234 | |
| 235 | ```toml |
| 236 | [global] |
| 237 | patterns = [ |
| 238 | ".DS_Store", |
| 239 | "Thumbs.db", |
| 240 | "*.tmp", |
| 241 | ] |
| 242 | |
| 243 | [domain.midi] |
| 244 | patterns = [ |
| 245 | "*.bak", |
| 246 | "*.autosave", |
| 247 | "/renders/", |
| 248 | "/exports/", |
| 249 | ] |
| 250 | ``` |
| 251 | |
| 252 | ### Code |
| 253 | |
| 254 | ```toml |
| 255 | [global] |
| 256 | patterns = [ |
| 257 | ".DS_Store", |
| 258 | "*.log", |
| 259 | ] |
| 260 | |
| 261 | [domain.code] |
| 262 | patterns = [ |
| 263 | "__pycache__/", |
| 264 | "*.pyc", |
| 265 | "node_modules/", |
| 266 | "dist/", |
| 267 | "build/", |
| 268 | ".venv/", |
| 269 | ] |
| 270 | ``` |
| 271 | |
| 272 | ### Genomics |
| 273 | |
| 274 | ```toml |
| 275 | [domain.genomics] |
| 276 | patterns = [ |
| 277 | "*.sam", |
| 278 | "*.bam.bai", |
| 279 | "pipeline-cache/", |
| 280 | "!final/*.bam", # keep final alignments |
| 281 | ] |
| 282 | ``` |
| 283 | |
| 284 | ### Scientific simulation |
| 285 | |
| 286 | ```toml |
| 287 | [domain.simulation] |
| 288 | patterns = [ |
| 289 | "frames/raw/", |
| 290 | "*.frame.bin", |
| 291 | "!checkpoints/*.gz", # keep compressed checkpoints |
| 292 | ] |
| 293 | ``` |
| 294 | |
| 295 | ### 3D Spatial |
| 296 | |
| 297 | ```toml |
| 298 | [domain.spatial] |
| 299 | patterns = [ |
| 300 | "previews/", |
| 301 | "*.preview.vdb", |
| 302 | "**/.shadercache/", |
| 303 | ] |
| 304 | ``` |
| 305 | |
| 306 | --- |
| 307 | |
| 308 | ## Implementation |
| 309 | |
| 310 | Parsing, resolution, and matching are in `muse/core/ignore.py`: |
| 311 | |
| 312 | ```python |
| 313 | from muse.core.ignore import load_ignore_config, resolve_patterns, is_ignored |
| 314 | |
| 315 | config = load_ignore_config(repo_root) # reads .museignore TOML |
| 316 | patterns = resolve_patterns(config, "midi") # global + [domain.midi] |
| 317 | ignored = is_ignored("tracks/x.tmp", patterns) # → True / False |
| 318 | ``` |
| 319 | |
| 320 | `load_ignore_config` returns an empty mapping when `.museignore` is absent |
| 321 | (nothing is ignored). `is_ignored` is a pure function with no filesystem access. |