cgcardona / muse public
museignore.md markdown
321 lines 8.0 KB
dfaf1b77 refactor: rename muse-work/ → state/ Gabriel Cardona <gabriel@tellurstori.com> 8h ago
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.