cgcardona / muse public
type-contracts.md markdown
1425 lines 51.5 KB
45fd2148 fix: config and versioning audit — TOML attributes, v0.1.1, no Phase N labels Gabriel Cardona <cgcardona@gmail.com> 2d ago
1 # Muse VCS — Type Contracts Reference
2
3 > Updated: 2026-03-17 (v0.1.1) | Reflects every named entity in the Muse VCS surface:
4 > domain protocol types, store wire-format TypedDicts, in-memory dataclasses,
5 > merge/config/stash types, MIDI import types, error hierarchy, and CLI enums.
6 > `Any` and `object` do not exist in any production file. Every type boundary
7 > is named. The typing audit ratchet enforces zero violations on every CI run.
8
9 This document is the single source of truth for every named entity —
10 `TypedDict`, `dataclass`, `Protocol`, `Enum`, `TypeAlias` — in the Muse
11 codebase. It covers the full contract of each type: fields, types,
12 optionality, and intended use.
13
14 ---
15
16 ## Table of Contents
17
18 1. [Design Philosophy](#design-philosophy)
19 2. [Domain Protocol Types (`muse/domain.py`)](#domain-protocol-types)
20 - [Snapshot and Delta TypedDicts](#snapshot-and-delta-typeddicts)
21 - [Typed Delta Algebra — StructuredDelta and DomainOp variants](#typed-delta-algebra)
22 - [Type Aliases](#type-aliases)
23 - [MergeResult and DriftReport Dataclasses](#mergeresult-and-driftreport-dataclasses)
24 - [MuseDomainPlugin Protocol](#musedomainplugin-protocol)
25 - [StructuredMergePlugin and CRDTPlugin Extensions](#optional-protocol-extensions)
26 - [CRDT Types](#crdt-types)
27 3. [Domain Schema Types (`muse/core/schema.py`)](#domain-schema-types)
28 4. [OT Merge Types (`muse/core/op_transform.py`)](#ot-merge-types)
29 5. [Store Types (`muse/core/store.py`)](#store-types)
30 - [Wire-Format TypedDicts](#wire-format-typeddicts)
31 - [In-Memory Dataclasses](#in-memory-dataclasses)
32 6. [Merge Engine Types (`muse/core/merge_engine.py`)](#merge-engine-types)
33 7. [Attributes Types (`muse/core/attributes.py`)](#attributes-types)
34 8. [MIDI Dimension Merge Types (`muse/plugins/music/midi_merge.py`)](#midi-dimension-merge-types)
35 9. [Configuration Types (`muse/cli/config.py`)](#configuration-types)
36 10. [MIDI / MusicXML Import Types (`muse/cli/midi_parser.py`)](#midi--musicxml-import-types)
37 11. [Stash Types (`muse/cli/commands/stash.py`)](#stash-types)
38 12. [Error Hierarchy (`muse/core/errors.py`)](#error-hierarchy)
39 13. [Entity Hierarchy](#entity-hierarchy)
40 14. [Entity Graphs (Mermaid)](#entity-graphs-mermaid)
41
42 ---
43
44 ## Design Philosophy
45
46 Every entity in this codebase follows five rules:
47
48 1. **No `Any`. No `object`. Ever.** Both collapse type safety for downstream
49 callers. Every boundary is typed with a concrete named entity — `TypedDict`,
50 `dataclass`, `Protocol`, or a specific union. The CI typing audit enforces
51 this with a ratchet of zero violations.
52
53 2. **No covariance in collection aliases.** `dict[str, str]` and
54 `list[str]` are used directly. If a function's return mixes value types,
55 create a `TypedDict` for that shape instead of using `dict[str, str | int]`.
56
57 3. **Boundaries own coercion.** When external data arrives (JSON from disk,
58 TOML from config, MIDI bytes from disk), the boundary module coerces it
59 to the canonical internal type using `isinstance` narrowing. Downstream
60 code always sees clean types.
61
62 4. **Wire-format TypedDicts for serialisation, dataclasses for in-memory
63 logic.** `CommitDict`, `SnapshotDict`, `TagDict` are JSON-serialisable
64 and used by `to_dict()` / `from_dict()`. `CommitRecord`, `SnapshotRecord`,
65 `TagRecord` are rich dataclasses with typed `datetime` fields used in
66 business logic.
67
68 5. **The plugin protocol is the extension point.** All domain-specific logic
69 lives behind `MuseDomainPlugin`. The core DAG engine, branching, and
70 merge machinery know nothing about music, genomics, or any other domain.
71 Swapping domains is a one-file operation.
72
73 ### What to use instead
74
75 | Banned | Use instead |
76 |--------|-------------|
77 | `Any` | `TypedDict`, `dataclass`, specific union |
78 | `object` | The actual type or a constrained union |
79 | `list` (bare) | `list[X]` with concrete element type |
80 | `dict` (bare) | `dict[K, V]` with concrete key/value types |
81 | `dict[str, X]` with known keys | `TypedDict` — name the keys |
82 | `Optional[X]` | `X \| None` |
83 | Legacy `List`, `Dict`, `Set`, `Tuple` | Lowercase builtins |
84 | `cast(T, x)` | Fix the callee to return `T` |
85 | `# type: ignore` | Fix the underlying type error |
86
87 ---
88
89 ## Domain Protocol Types
90
91 **Path:** `muse/domain.py`
92
93 The six-interface contract that every domain plugin must satisfy. The core
94 engine implements the DAG, branching, merge-base finding, and lineage walking.
95 A domain plugin provides the six methods and gets the full VCS for free.
96 Two optional protocol extensions (`StructuredMergePlugin`, `CRDTPlugin`) unlock
97 richer merge semantics.
98
99 ### Snapshot and Delta TypedDicts
100
101 #### `SnapshotManifest`
102
103 `TypedDict` — Content-addressed snapshot of domain state. Used as the
104 canonical representation of a point-in-time capture. JSON-serialisable and
105 content-addressable via SHA-256.
106
107 | Field | Type | Description |
108 |-------|------|-------------|
109 | `files` | `dict[str, str]` | Workspace-relative POSIX paths → SHA-256 content digests |
110 | `domain` | `str` | Plugin identifier that produced this snapshot (e.g. `"music"`) |
111
112 **Example:**
113 ```json
114 {
115 "files": {
116 "tracks/drums.mid": "a3f8...",
117 "tracks/bass.mid": "b291..."
118 },
119 "domain": "music"
120 }
121 ```
122
123 ### Typed Delta Algebra
124
125 #### `StructuredDelta`
126
127 `TypedDict` — The typed delta produced by `MuseDomainPlugin.diff()`. Replaces the
128 old `DeltaManifest` path-list format with a semantically rich operation list.
129
130 | Field | Type | Description |
131 |-------|------|-------------|
132 | `domain` | `str` | Plugin identifier that produced this delta |
133 | `ops` | `list[DomainOp]` | Ordered list of typed domain operations |
134 | `summary` | `str` | Human-readable summary (e.g. `"3 inserts, 1 delete"`) |
135
136 #### `DomainOp` — Five Variants
137
138 `DomainOp = InsertOp | DeleteOp | MoveOp | ReplaceOp | PatchOp`
139
140 Each variant is a `TypedDict` with an `"op"` discriminator and an `"address"`
141 identifying the element within the domain's namespace (e.g. `"note:4:60"` in
142 the music domain).
143
144 | Variant | `op` | Additional fields | Description |
145 |---------|------|------------------|-------------|
146 | `InsertOp` | `"insert"` | `after_id: str \| None`, `content_id: str` | Insert an element after `after_id` (or at head if `None`) |
147 | `DeleteOp` | `"delete"` | `content_id: str` | Delete the element at `address` |
148 | `MoveOp` | `"move"` | `from_id: str`, `after_id: str \| None` | Move an element to a new position |
149 | `ReplaceOp` | `"replace"` | `old_content_id: str`, `new_content_id: str` | Atomic replace (old → new) |
150 | `PatchOp` | `"patch"` | `patch: dict[str, str]` | Partial field update; keys are field names, values are new content IDs |
151
152 `content_id` values are SHA-256 hex digests of the element's serialised
153 content, stored in the object store.
154
155 ### Type Aliases
156
157 | Alias | Definition | Description |
158 |-------|-----------|-------------|
159 | `LiveState` | `SnapshotManifest \| pathlib.Path` | Current domain state — either an in-memory snapshot dict or a `muse-work/` directory path |
160 | `StateSnapshot` | `SnapshotManifest` | A content-addressed, immutable capture of state at a point in time |
161 | `StateDelta` | `StructuredDelta` | The typed delta between two snapshots |
162
163 `LiveState` carries two forms intentionally: the CLI path is used when commands
164 interact with the filesystem (`muse commit`, `muse status`); the snapshot form
165 is used when the engine constructs merges and diffs entirely in memory.
166
167 ### MergeResult and DriftReport Dataclasses
168
169 #### `MergeResult`
170
171 `@dataclass` — Outcome of a three-way merge between two divergent state lines.
172 An empty `conflicts` list means the merge was clean.
173
174 | Field | Type | Default | Description |
175 |-------|------|---------|-------------|
176 | `merged` | `StateSnapshot` | required | The reconciled snapshot |
177 | `conflicts` | `list[str]` | `[]` | Workspace-relative paths that could not be auto-merged |
178 | `applied_strategies` | `dict[str, str]` | `{}` | Path → strategy applied by `.museattributes` (e.g. `{"drums/kick.mid": "ours"}`) |
179 | `dimension_reports` | `dict[str, dict[str, str]]` | `{}` | Path → per-dimension winner map; only populated for MIDI files that went through dimension-level merge (e.g. `{"keys/piano.mid": {"notes": "left", "harmonic": "right"}}`) |
180
181 **Property:**
182
183 | Name | Returns | Description |
184 |------|---------|-------------|
185 | `is_clean` | `bool` | `True` when `conflicts` is empty |
186
187 #### `DriftReport`
188
189 `@dataclass` — Gap between committed state and current live state. Produced by
190 `MuseDomainPlugin.drift()` and consumed by `muse status`.
191
192 | Field | Type | Default | Description |
193 |-------|------|---------|-------------|
194 | `has_drift` | `bool` | required | `True` when live state differs from committed snapshot |
195 | `summary` | `str` | `""` | Human-readable description (e.g. `"2 added, 1 modified"`) |
196 | `delta` | `StateDelta` | empty `DeltaManifest` | Machine-readable diff for programmatic consumers |
197
198 ### MuseDomainPlugin Protocol
199
200 `@runtime_checkable Protocol` — The six interfaces a domain plugin must
201 implement. Runtime-checkable so that `assert isinstance(plugin, MuseDomainPlugin)`
202 works as a module-load sanity check.
203
204 | Method | Signature | Description |
205 |--------|-----------|-------------|
206 | `snapshot` | `(live_state: LiveState) -> StateSnapshot` | Capture current state as a content-addressed dict; must honour `.museignore` |
207 | `diff` | `(base: StateSnapshot, target: StateSnapshot, *, repo_root: pathlib.Path \| None = None) -> StateDelta` | Compute the typed delta between two snapshots |
208 | `merge` | `(base, left, right: StateSnapshot, *, repo_root: pathlib.Path \| None = None) -> MergeResult` | Three-way merge; when `repo_root` is provided, load `.museattributes` and perform dimension-level merge for supported formats |
209 | `drift` | `(committed: StateSnapshot, live: LiveState) -> DriftReport` | Compare committed state vs current live state |
210 | `apply` | `(delta: StateDelta, live_state: LiveState) -> LiveState` | Apply a delta to produce a new live state |
211 | `schema` | `() -> DomainSchema` | Declare the structural shape of the domain's data (drives diff algorithm selection) |
212
213 The music plugin (`muse.plugins.music.plugin`) is the reference implementation.
214 Every other domain — scientific simulation, genomics, 3D spatial design,
215 spacetime — implements these six methods and registers itself as a plugin.
216
217 ### Optional Protocol Extensions
218
219 #### `StructuredMergePlugin`
220
221 `@runtime_checkable Protocol` — Extends `MuseDomainPlugin` with operation-level
222 OT merge. When both branches produce `StructuredDelta`s, the merge engine detects
223 `isinstance(plugin, StructuredMergePlugin)` and calls `merge_ops()` instead of
224 `merge()`.
225
226 | Method | Signature | Description |
227 |--------|-----------|-------------|
228 | `merge_ops` | `(base, ours_snap, theirs_snap, ours_ops, theirs_ops, *, repo_root) -> MergeResult` | Operation-level three-way merge using OT commutativity rules |
229
230 #### `CRDTPlugin`
231
232 `@runtime_checkable Protocol` — Extends `MuseDomainPlugin` with convergent merge.
233 `join` always succeeds — no conflict state ever exists.
234
235 | Method | Signature | Description |
236 |--------|-----------|-------------|
237 | `join` | `(a: CRDTSnapshotManifest, b: CRDTSnapshotManifest) -> CRDTSnapshotManifest` | Convergent join satisfying commutativity, associativity, idempotency |
238 | `crdt_schema` | `() -> list[CRDTDimensionSpec]` | Per-dimension CRDT primitive specification |
239 | `to_crdt_state` | `(snapshot: StateSnapshot) -> CRDTSnapshotManifest` | Convert a snapshot into CRDT state |
240 | `from_crdt_state` | `(crdt: CRDTSnapshotManifest) -> StateSnapshot` | Convert CRDT state back to a plain snapshot |
241
242 ### CRDT Types
243
244 #### `CRDTSnapshotManifest`
245
246 `TypedDict` — Extended snapshot format for CRDT-mode plugins. Wraps the plain
247 snapshot manifest with a vector clock and serialised CRDT state.
248
249 | Field | Type | Description |
250 |-------|------|-------------|
251 | `schema_version` | `int` | Always `1` |
252 | `domain` | `str` | Plugin domain name |
253 | `files` | `dict[str, str]` | POSIX path → SHA-256 object digest (same as `SnapshotManifest`) |
254 | `vclock` | `dict[str, int]` | Vector clock: agent ID → logical clock value |
255 | `crdt_state` | `dict[str, str]` | Dimension name → serialised CRDT primitive state (JSON-encoded) |
256
257 #### `CRDTDimensionSpec`
258
259 `TypedDict` — Declares which CRDT primitive a dimension uses.
260
261 | Field | Type | Description |
262 |-------|------|-------------|
263 | `name` | `str` | Dimension name (must match a `DimensionSpec.name` in the plugin's `DomainSchema`) |
264 | `crdt_type` | `str` | One of: `"lww_register"`, `"or_set"`, `"rga"`, `"aw_map"`, `"g_counter"`, `"vector_clock"` |
265
266 ---
267
268 ## Domain Schema Types
269
270 **Path:** `muse/core/schema.py`
271
272 The `DomainSchema` family of TypedDicts allow a plugin to declare its data
273 structure. The core engine uses this to select diff algorithms per-dimension
274 and to drive informed conflict reporting during OT merge.
275
276 #### `ElementSchema`
277
278 `TypedDict` — The schema for a single element kind (a top-level data entity).
279
280 | Field | Type | Description |
281 |-------|------|-------------|
282 | `name` | `str` | Element kind name (e.g. `"note"`, `"track"`, `"gene_edit"`) |
283 | `kind` | `str` | Container kind: `"sequence"`, `"set"`, `"map"`, or `"scalar"` |
284
285 #### `DimensionSpec`
286
287 `TypedDict` — Schema for a single orthogonal dimension within the domain.
288
289 | Field | Type | Description |
290 |-------|------|-------------|
291 | `name` | `str` | Dimension name (e.g. `"melodic"`, `"harmonic"`) |
292 | `description` | `str` | Human-readable description for this dimension |
293 | `diff_algorithm` | `str` | Algorithm to use: `"myers_lcs"`, `"tree_edit"`, `"numerical"`, or `"set_ops"` |
294
295 #### `CRDTDimensionSpec`
296
297 `TypedDict` — Schema for a dimension using CRDT convergent merge semantics.
298 See [CRDT Types](#crdt-types) above for the `crdt_type` values.
299
300 #### `MapSchema`
301
302 `TypedDict` — Schema for a map-kind element's value type.
303
304 | Field | Type | Description |
305 |-------|------|-------------|
306 | `value_schema` | `ElementSchema` | Schema of the map's values |
307
308 #### `DomainSchema`
309
310 `TypedDict` — The top-level schema declaration returned by `MuseDomainPlugin.schema()`.
311
312 | Field | Type | Description |
313 |-------|------|-------------|
314 | `domain` | `str` | Plugin domain name |
315 | `schema_version` | `int` | Always `1` |
316 | `description` | `str` | Human-readable domain description |
317 | `merge_mode` | `str` | `"three_way"` (OT merge) or `"crdt"` (convergent join) |
318 | `elements` | `list[ElementSchema]` | Top-level element kind declarations |
319 | `dimensions` | `list[DimensionSpec \| CRDTDimensionSpec]` | Orthogonal dimension declarations |
320
321 ---
322
323 ## OT Merge Types
324
325 **Path:** `muse/core/op_transform.py`
326
327 Operational Transformation types for the `StructuredMergePlugin` extension.
328
329 #### `MergeOpsResult`
330
331 `@dataclass` — Result of `merge_op_lists()`. Carries auto-merged ops and any
332 unresolvable conflicts as pairs.
333
334 | Field | Type | Description |
335 |-------|------|-------------|
336 | `merged_ops` | `list[DomainOp]` | Operations that were auto-merged (commuting ops from both branches) |
337 | `conflict_ops` | `list[tuple[DomainOp, DomainOp]]` | Pairs of non-commuting operations: `(our_op, their_op)` |
338
339 **Lattice contract:** `merged_ops` contains every auto-merged op exactly once;
340 `conflict_ops` contains every unresolvable pair exactly once.
341
342 ---
343
344 ## Store Types
345
346 **Path:** `muse/core/store.py`
347
348 All commit and snapshot metadata is stored as JSON files under `.muse/`.
349 Wire-format `TypedDict`s are the JSON-serialisable shapes used in `to_dict()`
350 and `from_dict()`. In-memory `dataclass`es are the rich representations used
351 in business logic throughout the CLI commands.
352
353 ### Wire-Format TypedDicts
354
355 These types appear at the boundary between Python objects and JSON on disk.
356 `json.loads()` returns an untyped result; `from_dict()` methods consume it and
357 return typed dataclasses. `to_dict()` methods produce these TypedDicts for
358 `json.dumps()`.
359
360 #### `CommitDict`
361
362 `TypedDict` — JSON-serialisable representation of a commit record. All datetime
363 values are ISO-8601 strings; callers convert to `datetime` inside `from_dict()`.
364
365 | Field | Type | Description |
366 |-------|------|-------------|
367 | `commit_id` | `str` | SHA-256 hex digest of the commit's canonical inputs |
368 | `repo_id` | `str` | UUID identifying the repository |
369 | `branch` | `str` | Branch name at time of commit |
370 | `snapshot_id` | `str` | SHA-256 hex digest of the attached snapshot |
371 | `message` | `str` | Commit message |
372 | `committed_at` | `str` | ISO-8601 UTC timestamp |
373 | `parent_commit_id` | `str \| None` | First parent commit ID; `None` for initial commit |
374 | `parent2_commit_id` | `str \| None` | Second parent commit ID; non-`None` only for merge commits |
375 | `author` | `str` | Author name string |
376 | `metadata` | `dict[str, str]` | Extensible string→string metadata bag |
377
378 #### `SnapshotDict`
379
380 `TypedDict` — JSON-serialisable representation of a snapshot record.
381
382 | Field | Type | Description |
383 |-------|------|-------------|
384 | `snapshot_id` | `str` | SHA-256 hex digest of the manifest |
385 | `manifest` | `dict[str, str]` | POSIX path → SHA-256 object digest |
386 | `created_at` | `str` | ISO-8601 UTC timestamp |
387
388 #### `TagDict`
389
390 `TypedDict` — JSON-serialisable representation of a semantic tag.
391
392 | Field | Type | Description |
393 |-------|------|-------------|
394 | `tag_id` | `str` | UUID identifying the tag |
395 | `repo_id` | `str` | UUID identifying the repository |
396 | `commit_id` | `str` | SHA-256 commit ID this tag points to |
397 | `tag` | `str` | Tag name string (e.g. `"v1.0"`) |
398 | `created_at` | `str` | ISO-8601 UTC timestamp |
399
400 #### `RemoteCommitPayload`
401
402 `TypedDict (total=False)` — Wire format received from a remote during push/pull.
403 All fields are optional because the remote payload may omit fields unknown to
404 older protocol versions. Callers validate required fields before constructing
405 a `CommitRecord`.
406
407 | Field | Type | Description |
408 |-------|------|-------------|
409 | `commit_id` | `str` | Commit identifier |
410 | `repo_id` | `str` | Repository UUID |
411 | `branch` | `str` | Branch name |
412 | `snapshot_id` | `str` | Snapshot identifier |
413 | `message` | `str` | Commit message |
414 | `committed_at` | `str` | ISO-8601 timestamp |
415 | `parent_commit_id` | `str \| None` | First parent |
416 | `parent2_commit_id` | `str \| None` | Second parent (merge commits) |
417 | `author` | `str` | Author name |
418 | `metadata` | `dict[str, str]` | Metadata bag |
419 | `manifest` | `dict[str, str]` | Inline snapshot manifest (remote optimisation) |
420
421 ### In-Memory Dataclasses
422
423 These rich types are constructed from wire-format TypedDicts after loading from
424 disk. They carry typed `datetime` values and are used throughout CLI command
425 implementations.
426
427 #### `CommitRecord`
428
429 `@dataclass` — In-memory representation of a commit.
430
431 | Field | Type | Default | Description |
432 |-------|------|---------|-------------|
433 | `commit_id` | `str` | required | SHA-256 hex digest |
434 | `repo_id` | `str` | required | Repository UUID |
435 | `branch` | `str` | required | Branch name |
436 | `snapshot_id` | `str` | required | Attached snapshot digest |
437 | `message` | `str` | required | Commit message |
438 | `committed_at` | `datetime.datetime` | required | UTC commit timestamp |
439 | `parent_commit_id` | `str \| None` | `None` | First parent; `None` for root commits |
440 | `parent2_commit_id` | `str \| None` | `None` | Second parent for merge commits |
441 | `author` | `str` | `""` | Author name |
442 | `metadata` | `dict[str, str]` | `{}` | Extensible string→string metadata |
443
444 **Methods:**
445
446 | Method | Returns | Description |
447 |--------|---------|-------------|
448 | `to_dict()` | `CommitDict` | Serialise to JSON-ready TypedDict |
449 | `from_dict(d: CommitDict)` | `CommitRecord` | Deserialise from JSON-loaded TypedDict |
450
451 #### `SnapshotRecord`
452
453 `@dataclass` — In-memory representation of a content-addressed snapshot.
454
455 | Field | Type | Default | Description |
456 |-------|------|---------|-------------|
457 | `snapshot_id` | `str` | required | SHA-256 hex digest of the manifest |
458 | `manifest` | `dict[str, str]` | required | POSIX path → SHA-256 object digest |
459 | `created_at` | `datetime.datetime` | UTC now | Creation timestamp |
460
461 **Methods:** `to_dict() -> SnapshotDict`, `from_dict(d: SnapshotDict) -> SnapshotRecord`
462
463 #### `TagRecord`
464
465 `@dataclass` — In-memory representation of a semantic tag.
466
467 | Field | Type | Default | Description |
468 |-------|------|---------|-------------|
469 | `tag_id` | `str` | required | UUID |
470 | `repo_id` | `str` | required | Repository UUID |
471 | `commit_id` | `str` | required | Tagged commit's SHA-256 digest |
472 | `tag` | `str` | required | Tag name |
473 | `created_at` | `datetime.datetime` | UTC now | Creation timestamp |
474
475 **Methods:** `to_dict() -> TagDict`, `from_dict(d: TagDict) -> TagRecord`
476
477 ---
478
479 ## Merge Engine Types
480
481 **Path:** `muse/core/merge_engine.py`
482
483 #### `MergeStatePayload`
484
485 `TypedDict (total=False)` — JSON-serialisable form of an in-progress merge
486 state. Written to `.muse/MERGE_STATE.json` when a merge has unresolved
487 conflicts. All fields are optional in the TypedDict because `other_branch` is
488 only set when the merge has a named second branch.
489
490 | Field | Type | Description |
491 |-------|------|-------------|
492 | `base_commit` | `str` | Common ancestor commit ID |
493 | `ours_commit` | `str` | Current branch HEAD at merge start |
494 | `theirs_commit` | `str` | Incoming branch HEAD at merge start |
495 | `conflict_paths` | `list[str]` | POSIX paths with unresolved conflicts |
496 | `other_branch` | `str` | Name of the branch being merged in (optional) |
497
498 #### `MergeState`
499
500 `@dataclass (frozen=True)` — Loaded in-memory representation of
501 `MERGE_STATE.json`. Immutable so it can be passed around without accidental
502 mutation.
503
504 | Field | Type | Default | Description |
505 |-------|------|---------|-------------|
506 | `conflict_paths` | `list[str]` | `[]` | Paths with unresolved conflicts |
507 | `base_commit` | `str \| None` | `None` | Common ancestor commit ID |
508 | `ours_commit` | `str \| None` | `None` | Our HEAD at merge start |
509 | `theirs_commit` | `str \| None` | `None` | Their HEAD at merge start |
510 | `other_branch` | `str \| None` | `None` | Name of the incoming branch |
511
512 ---
513
514 ## Attributes Types
515
516 **Path:** `muse/core/attributes.py`
517
518 Parse and resolve `.museattributes` TOML merge-strategy rules. The parser
519 produces a typed `AttributeRule` list; `resolve_strategy` does first-match
520 lookup with `fnmatch` path patterns and dimension name matching.
521
522 #### `VALID_STRATEGIES`
523
524 `frozenset[str]` — The set of legal strategy strings:
525 `{"ours", "theirs", "union", "auto", "manual"}`.
526
527 #### `AttributesMeta`
528
529 `TypedDict (total=False)` — The `[meta]` section of `.museattributes`.
530
531 | Field | Type | Description |
532 |-------|------|-------------|
533 | `domain` | `str` | Domain name this file targets (optional — validated against `.muse/repo.json` when present) |
534
535 #### `AttributesRuleDict`
536
537 `TypedDict` — A single `[[rules]]` entry as parsed from TOML.
538
539 | Field | Type | Description |
540 |-------|------|-------------|
541 | `path` | `str` | `fnmatch` glob matched against workspace-relative POSIX paths |
542 | `dimension` | `str` | Domain axis name (e.g. `"melodic"`) or `"*"` to match all |
543 | `strategy` | `str` | One of the `VALID_STRATEGIES` strings |
544
545 #### `MuseAttributesFile`
546
547 `TypedDict (total=False)` — The complete `MuseAttributesFile` structure after TOML parsing.
548
549 | Field | Type | Description |
550 |-------|------|-------------|
551 | `meta` | `AttributesMeta` | Optional `[meta]` section |
552 | `rules` | `list[AttributesRuleDict]` | Ordered `[[rules]]` array |
553
554 #### `AttributeRule`
555
556 `@dataclass (frozen=True)` — A single resolved rule from `.museattributes`.
557
558 | Field | Type | Description |
559 |-------|------|-------------|
560 | `path_pattern` | `str` | `fnmatch` glob matched against workspace-relative POSIX paths |
561 | `dimension` | `str` | Domain axis name (e.g. `"melodic"`, `"harmonic"`) or `"*"` to match all |
562 | `strategy` | `str` | One of the `VALID_STRATEGIES` strings |
563 | `source_index` | `int` | 0-based index of the rule in the `[[rules]]` array; defaults to `0` |
564
565 **Public functions:**
566
567 - `load_attributes(root: pathlib.Path, *, domain: str | None = None) -> list[AttributeRule]` —
568 reads `.museattributes` TOML, validates domain if provided, returns rules in
569 file order; raises `ValueError` for parse errors, missing fields, or invalid strategy.
570 - `read_attributes_meta(root: pathlib.Path) -> AttributesMeta` —
571 returns the `[meta]` section only; returns `{}` if file is absent or unparseable.
572 - `resolve_strategy(rules: list[AttributeRule], path: str, dimension: str = "*") -> str` —
573 first-match lookup; returns `"auto"` when no rule matches.
574
575 ---
576
577 ## MIDI Dimension Merge Types
578
579 **Path:** `muse/plugins/music/midi_merge.py`
580
581 The multidimensional merge engine for the music domain. MIDI events are
582 bucketed into four orthogonal dimension slices; each slice has a content hash
583 for fast change detection. A three-way merge resolves each dimension
584 independently using `.museattributes` strategies, then reconstructs a valid
585 MIDI file from the winning slices.
586
587 #### Constants
588
589 | Name | Type | Value / Description |
590 |------|------|---------------------|
591 | `INTERNAL_DIMS` | `list[str]` | `["notes", "harmonic", "dynamic", "structural"]` — the four internal dimension bucket names |
592 | `DIM_ALIAS` | `dict[str, str]` | Maps user-facing names to internal buckets: `"melodic" → "notes"`, `"rhythmic" → "notes"`, `"harmonic" → "harmonic"`, `"dynamic" → "dynamic"`, `"structural" → "structural"` |
593
594 #### `_MsgVal`
595
596 `TypeAlias = int | str | list[int]` — The set of value types that can appear
597 in the serialised form of a MIDI message field. Used by `_msg_to_dict` to
598 avoid `dict[str, object]`.
599
600 #### `DimensionSlice`
601
602 `@dataclass` — All MIDI events belonging to one dimension of a parsed file.
603
604 | Field | Type | Default | Description |
605 |-------|------|---------|-------------|
606 | `name` | `str` | required | Internal dimension name (e.g. `"notes"`, `"harmonic"`) |
607 | `events` | `list[tuple[int, mido.Message]]` | `[]` | `(abs_tick, message)` pairs sorted by ascending absolute tick |
608 | `content_hash` | `str` | `""` | SHA-256 digest of the canonical JSON serialisation; computed in `__post_init__` when not provided |
609
610 #### `MidiDimensions`
611
612 `@dataclass` — All four dimension slices extracted from one MIDI file, plus
613 file-level metadata.
614
615 | Field | Type | Description |
616 |-------|------|-------------|
617 | `ticks_per_beat` | `int` | MIDI timing resolution (pulses per quarter note) from the source file |
618 | `file_type` | `int` | MIDI file type (0 = single-track, 1 = multi-track synchronous) |
619 | `slices` | `dict[str, DimensionSlice]` | Internal dimension name → slice |
620
621 **Method:**
622
623 | Name | Signature | Description |
624 |------|-----------|-------------|
625 | `get` | `(user_dim: str) -> DimensionSlice` | Resolve a user-facing alias (`"melodic"`, `"rhythmic"`) or internal name to the correct slice |
626
627 **Public functions:**
628
629 | Function | Signature | Description |
630 |----------|-----------|-------------|
631 | `extract_dimensions` | `(midi_bytes: bytes) -> MidiDimensions` | Parse MIDI bytes and bucket events by dimension |
632 | `dimension_conflict_detail` | `(base, left, right: MidiDimensions) -> dict[str, str]` | Per-dimension change report: `"unchanged"`, `"left_only"`, `"right_only"`, or `"both"` |
633 | `merge_midi_dimensions` | `(base_bytes, left_bytes, right_bytes: bytes, attrs_rules: list[AttributeRule], path: str) -> tuple[bytes, dict[str, str]] \| None` | Three-way dimension merge; returns `(merged_bytes, dimension_report)` or `None` on unresolvable conflict |
634
635 ---
636
637 ## Configuration Types
638
639 **Path:** `muse/cli/config.py`
640
641 The structured view of `.muse/config.toml`. Loading from TOML uses `isinstance`
642 narrowing from `tomllib`'s untyped output — no `Any` annotation is ever written
643 in source. All mutation functions read the current config, modify the specific
644 section, and write back.
645
646 #### `AuthEntry`
647
648 `TypedDict (total=False)` — `[auth]` section in `.muse/config.toml`.
649
650 | Field | Type | Description |
651 |-------|------|-------------|
652 | `token` | `str` | Bearer token for Muse Hub authentication. **Never logged.** |
653
654 #### `RemoteEntry`
655
656 `TypedDict (total=False)` — `[remotes.<name>]` section in `.muse/config.toml`.
657
658 | Field | Type | Description |
659 |-------|------|-------------|
660 | `url` | `str` | Remote Hub URL (e.g. `"https://hub.example.com/repos/my-repo"`) |
661 | `branch` | `str` | Upstream branch tracked by this remote (set by `--set-upstream`) |
662
663 #### `MuseConfig`
664
665 `TypedDict (total=False)` — Structured view of the entire `.muse/config.toml`
666 file. All sections are optional; an empty dict is a valid `MuseConfig`.
667
668 | Field | Type | Description |
669 |-------|------|-------------|
670 | `auth` | `AuthEntry` | Authentication credentials section |
671 | `remotes` | `dict[str, RemoteEntry]` | Named remote sections |
672 | `domain` | `dict[str, str]` | Domain-specific key/value pairs; keys are domain-defined (e.g. `ticks_per_beat` for music, `reference_assembly` for genomics). The core engine ignores this section; only the active plugin reads it. |
673
674 #### `RemoteConfig`
675
676 `TypedDict` — Public-facing remote descriptor returned by `list_remotes()`.
677 A lightweight projection of `RemoteEntry` that always has both required fields.
678
679 | Field | Type | Description |
680 |-------|------|-------------|
681 | `name` | `str` | Remote name (e.g. `"origin"`) |
682 | `url` | `str` | Remote URL |
683
684 ---
685
686 ## MIDI / MusicXML Import Types
687
688 **Path:** `muse/cli/midi_parser.py`
689
690 Types used by `muse import` to parse Standard MIDI Files and MusicXML documents
691 into Muse's internal note representation.
692
693 #### `MidiMeta`
694
695 `TypedDict` — Format-specific metadata for Standard MIDI Files (`.mid`, `.midi`).
696
697 | Field | Type | Description |
698 |-------|------|-------------|
699 | `num_tracks` | `int` | Number of MIDI tracks in the file |
700
701 #### `MusicXMLMeta`
702
703 `TypedDict` — Format-specific metadata for MusicXML files (`.xml`, `.musicxml`).
704
705 | Field | Type | Description |
706 |-------|------|-------------|
707 | `num_parts` | `int` | Number of parts (instruments) in the score |
708 | `part_names` | `list[str]` | Display names of each part |
709
710 #### `RawMeta`
711
712 `TypeAlias = MidiMeta | MusicXMLMeta` — Discriminated union of all
713 format-specific metadata shapes. The `MuseImportData.raw_meta` field carries
714 one of these two named types depending on the source file's format.
715
716 #### `NoteEvent`
717
718 `@dataclass` — A single sounding note extracted from an imported file.
719
720 | Field | Type | Description |
721 |-------|------|-------------|
722 | `pitch` | `int` | MIDI pitch number (0–127) |
723 | `velocity` | `int` | MIDI velocity (0–127; 0 = note-off) |
724 | `start_tick` | `int` | Onset tick relative to file start |
725 | `duration_ticks` | `int` | Note length in MIDI ticks |
726 | `channel` | `int` | MIDI channel (0–15) |
727 | `channel_name` | `str` | Track/part name for this channel |
728
729 #### `MuseImportData`
730
731 `@dataclass` — All data extracted from a single imported music file. The
732 complete parsed result returned by `parse_file()`.
733
734 | Field | Type | Description |
735 |-------|------|-------------|
736 | `source_path` | `pathlib.Path` | Absolute path to the source file |
737 | `format` | `str` | `"midi"` or `"musicxml"` |
738 | `ticks_per_beat` | `int` | MIDI timing resolution (pulses per quarter note) |
739 | `tempo_bpm` | `float` | Tempo in beats per minute |
740 | `notes` | `list[NoteEvent]` | All sounding notes, in onset order |
741 | `tracks` | `list[str]` | Track/part names present in the file |
742 | `raw_meta` | `RawMeta` | Format-specific metadata (`MidiMeta` or `MusicXMLMeta`) |
743
744 ---
745
746 ## Stash Types
747
748 **Path:** `muse/cli/commands/stash.py`
749
750 #### `StashEntry`
751
752 `TypedDict` — A single entry in the stash stack, persisted to
753 `.muse/stash.json` as one element of a JSON array.
754
755 | Field | Type | Description |
756 |-------|------|-------------|
757 | `snapshot_id` | `str` | SHA-256 content digest of the stashed snapshot |
758 | `manifest` | `dict[str, str]` | POSIX path → SHA-256 object digest of stashed files |
759 | `branch` | `str` | Branch name that was active when the stash was saved |
760 | `stashed_at` | `str` | ISO-8601 UTC timestamp of the stash operation |
761
762 ---
763
764 ## Error Hierarchy
765
766 **Path:** `muse/core/errors.py`
767
768 #### `ExitCode`
769
770 `IntEnum` — Standardised CLI exit codes. Used throughout the CLI commands via
771 `raise typer.Exit(code=ExitCode.USER_ERROR)`.
772
773 | Value | Integer | Meaning |
774 |-------|---------|---------|
775 | `SUCCESS` | `0` | Command completed successfully |
776 | `USER_ERROR` | `1` | Bad arguments or invalid user input |
777 | `REPO_NOT_FOUND` | `2` | Not inside a Muse repository |
778 | `INTERNAL_ERROR` | `3` | Unexpected internal failure |
779
780 #### `MuseCLIError`
781
782 `Exception` — Base exception for all Muse CLI errors. Carries an exit code
783 so that top-level handlers can produce the correct process exit.
784
785 | Field | Type | Description |
786 |-------|------|-------------|
787 | `exit_code` | `ExitCode` | Exit code to use when this exception terminates the process |
788
789 #### `RepoNotFoundError`
790
791 `MuseCLIError` — Raised by `find_repo_root()` callers when a command is invoked
792 outside a Muse repository. Default message: `"Not a Muse repository. Run muse init."` Default exit code: `ExitCode.REPO_NOT_FOUND`.
793
794 **Alias:** `MuseNotARepoError = RepoNotFoundError`
795
796 ---
797
798 ## Entity Hierarchy
799
800 ```
801 Muse VCS
802
803 ├── Domain Protocol (muse/domain.py)
804 │ │
805 │ ├── Snapshot and Delta
806 │ │ ├── SnapshotManifest — TypedDict: {files: dict[str,str], domain: str}
807 │ │ └── DeltaManifest — TypedDict: {domain, added, removed, modified}
808 │ │
809 │ ├── Type Aliases
810 │ │ ├── LiveState — SnapshotManifest | pathlib.Path
811 │ │ ├── StateSnapshot — SnapshotManifest
812 │ │ └── StateDelta — DeltaManifest
813 │ │
814 │ ├── Result Types
815 │ │ ├── MergeResult — dataclass: merged + conflicts + applied_strategies + dimension_reports
816 │ │ └── DriftReport — dataclass: has_drift + summary + delta
817 │ │
818 │ └── MuseDomainPlugin — Protocol (runtime_checkable): 5 methods
819 │ merge() accepts repo_root kwarg for attribute-aware merge
820
821 ├── Store (muse/core/store.py)
822 │ │
823 │ ├── Wire-Format TypedDicts
824 │ │ ├── CommitDict — TypedDict: all commit fields (str timestamps)
825 │ │ ├── SnapshotDict — TypedDict: snapshot_id + manifest + created_at
826 │ │ ├── TagDict — TypedDict: tag identity fields
827 │ │ └── RemoteCommitPayload — TypedDict (total=False): wire format + manifest
828 │ │
829 │ └── In-Memory Dataclasses
830 │ ├── CommitRecord — dataclass: typed datetime, to_dict/from_dict
831 │ ├── SnapshotRecord — dataclass: manifest + datetime
832 │ └── TagRecord — dataclass: tag metadata + datetime
833
834 ├── Merge Engine (muse/core/merge_engine.py)
835 │ ├── MergeStatePayload — TypedDict (total=False): MERGE_STATE.json shape
836 │ └── MergeState — dataclass (frozen): loaded in-memory merge state
837
838 ├── Attributes (muse/core/attributes.py)
839 │ ├── VALID_STRATEGIES — frozenset[str]: {ours, theirs, union, auto, manual}
840 │ ├── AttributesMeta — TypedDict (total=False): [meta] section (domain: str)
841 │ ├── AttributesRuleDict — TypedDict: [[rules]] entry (path, dimension, strategy)
842 │ ├── MuseAttributesFile — TypedDict (total=False): full parsed file structure
843 │ └── AttributeRule — dataclass (frozen): path_pattern + dimension + strategy + source_index
844
845 ├── MIDI Dimension Merge (muse/plugins/music/midi_merge.py)
846 │ ├── INTERNAL_DIMS — list[str]: [notes, harmonic, dynamic, structural]
847 │ ├── DIM_ALIAS — dict[str, str]: user-facing names → internal buckets
848 │ ├── _MsgVal — TypeAlias: int | str | list[int]
849 │ ├── DimensionSlice — dataclass: name + events list + content_hash
850 │ └── MidiDimensions — dataclass: ticks_per_beat + file_type + slices dict
851
852 ├── Configuration (muse/cli/config.py)
853 │ ├── AuthEntry — TypedDict (total=False): [auth] section
854 │ ├── RemoteEntry — TypedDict (total=False): [remotes.<name>] section
855 │ ├── MuseConfig — TypedDict (total=False): full config.toml shape (auth + remotes + domain)
856 │ └── RemoteConfig — TypedDict: public remote descriptor
857
858 ├── MIDI / MusicXML Import (muse/cli/midi_parser.py)
859 │ ├── MidiMeta — TypedDict: num_tracks
860 │ ├── MusicXMLMeta — TypedDict: num_parts + part_names
861 │ ├── RawMeta — TypeAlias: MidiMeta | MusicXMLMeta
862 │ ├── NoteEvent — dataclass: pitch, velocity, timing, channel
863 │ └── MuseImportData — dataclass: full parsed file result
864
865 ├── Stash (muse/cli/commands/stash.py)
866 │ └── StashEntry — TypedDict: snapshot_id + manifest + branch + stashed_at
867
868 └── Errors (muse/core/errors.py)
869 ├── ExitCode — IntEnum: SUCCESS=0 USER_ERROR=1 REPO_NOT_FOUND=2 INTERNAL_ERROR=3
870 ├── MuseCLIError — Exception base: carries ExitCode
871 ├── RepoNotFoundError — MuseCLIError: default exit REPO_NOT_FOUND
872 └── MuseNotARepoError — alias for RepoNotFoundError
873 ```
874
875 ---
876
877 ## Entity Graphs (Mermaid)
878
879 Arrow conventions:
880 - `*--` composition (owns, lifecycle-coupled)
881 - `-->` association (references)
882 - `..>` dependency (uses)
883 - `..>` with label: produces / implements
884
885 ---
886
887 ### Diagram 1 — Domain Protocol and Plugin Contract
888
889 The `MuseDomainPlugin` protocol and the types that flow through its five methods. `MusicPlugin` is the reference implementation that proves the abstraction.
890
891 ```mermaid
892 classDiagram
893 class SnapshotManifest {
894 <<TypedDict>>
895 +files : dict~str, str~
896 +domain : str
897 }
898 class DeltaManifest {
899 <<TypedDict>>
900 +domain : str
901 +added : list~str~
902 +removed : list~str~
903 +modified : list~str~
904 }
905 class MergeResult {
906 <<dataclass>>
907 +merged : StateSnapshot
908 +conflicts : list~str~
909 +applied_strategies : dict~str, str~
910 +dimension_reports : dict~str, dict~str, str~~
911 +is_clean : bool
912 }
913 class DriftReport {
914 <<dataclass>>
915 +has_drift : bool
916 +summary : str
917 +delta : StateDelta
918 }
919 class MuseDomainPlugin {
920 <<Protocol runtime_checkable>>
921 +snapshot(live_state: LiveState) StateSnapshot
922 +diff(base, target: StateSnapshot) StateDelta
923 +merge(base, left, right, *, repo_root) MergeResult
924 +drift(committed: StateSnapshot, live: LiveState) DriftReport
925 +apply(delta: StateDelta, live_state: LiveState) LiveState
926 }
927 class MusicPlugin {
928 <<reference implementation>>
929 +snapshot(live_state) StateSnapshot
930 +diff(base, target) StateDelta
931 +merge(base, left, right, *, repo_root) MergeResult
932 +drift(committed, live) DriftReport
933 +apply(delta, live_state) LiveState
934 }
935
936 MuseDomainPlugin ..> SnapshotManifest : StateSnapshot alias
937 MuseDomainPlugin ..> DeltaManifest : StateDelta alias
938 MuseDomainPlugin --> MergeResult : merge() returns
939 MuseDomainPlugin --> DriftReport : drift() returns
940 MusicPlugin ..|> MuseDomainPlugin : implements
941 MergeResult --> SnapshotManifest : merged
942 DriftReport --> DeltaManifest : delta
943 ```
944
945 ---
946
947 ### Diagram 2 — Store Wire-Format TypedDicts and Dataclasses
948
949 The two-layer design: wire-format TypedDicts for JSON serialisation, rich
950 dataclasses for in-memory logic. Every `from_dict` consumes the TypedDict
951 shape produced by `json.loads()`; every `to_dict` produces it for
952 `json.dumps()`.
953
954 ```mermaid
955 classDiagram
956 class CommitDict {
957 <<TypedDict wire format>>
958 +commit_id : str
959 +repo_id : str
960 +branch : str
961 +snapshot_id : str
962 +message : str
963 +committed_at : str
964 +parent_commit_id : str | None
965 +parent2_commit_id : str | None
966 +author : str
967 +metadata : dict~str, str~
968 }
969 class SnapshotDict {
970 <<TypedDict wire format>>
971 +snapshot_id : str
972 +manifest : dict~str, str~
973 +created_at : str
974 }
975 class TagDict {
976 <<TypedDict wire format>>
977 +tag_id : str
978 +repo_id : str
979 +commit_id : str
980 +tag : str
981 +created_at : str
982 }
983 class RemoteCommitPayload {
984 <<TypedDict total=False>>
985 +commit_id : str
986 +repo_id : str
987 +branch : str
988 +snapshot_id : str
989 +message : str
990 +committed_at : str
991 +parent_commit_id : str | None
992 +parent2_commit_id : str | None
993 +author : str
994 +metadata : dict~str, str~
995 +manifest : dict~str, str~
996 }
997 class CommitRecord {
998 <<dataclass>>
999 +commit_id : str
1000 +repo_id : str
1001 +branch : str
1002 +snapshot_id : str
1003 +message : str
1004 +committed_at : datetime
1005 +parent_commit_id : str | None
1006 +parent2_commit_id : str | None
1007 +author : str
1008 +metadata : dict~str, str~
1009 +to_dict() CommitDict
1010 +from_dict(d: CommitDict) CommitRecord
1011 }
1012 class SnapshotRecord {
1013 <<dataclass>>
1014 +snapshot_id : str
1015 +manifest : dict~str, str~
1016 +created_at : datetime
1017 +to_dict() SnapshotDict
1018 +from_dict(d: SnapshotDict) SnapshotRecord
1019 }
1020 class TagRecord {
1021 <<dataclass>>
1022 +tag_id : str
1023 +repo_id : str
1024 +commit_id : str
1025 +tag : str
1026 +created_at : datetime
1027 +to_dict() TagDict
1028 +from_dict(d: TagDict) TagRecord
1029 }
1030
1031 CommitRecord ..> CommitDict : to_dict produces
1032 CommitDict ..> CommitRecord : from_dict produces
1033 SnapshotRecord ..> SnapshotDict : to_dict produces
1034 SnapshotDict ..> SnapshotRecord : from_dict produces
1035 TagRecord ..> TagDict : to_dict produces
1036 TagDict ..> TagRecord : from_dict produces
1037 RemoteCommitPayload ..> CommitDict : store_pulled_commit builds
1038 CommitRecord --> SnapshotRecord : snapshot_id reference
1039 CommitRecord --> TagRecord : commit_id reference (via TagRecord)
1040 ```
1041
1042 ---
1043
1044 ### Diagram 3 — Merge Engine State
1045
1046 The in-progress merge state written to disk on conflict and loaded on
1047 continuation. `MergeStatePayload` is the JSON shape; `MergeState` is the
1048 loaded, immutable in-memory form.
1049
1050 ```mermaid
1051 classDiagram
1052 class MergeStatePayload {
1053 <<TypedDict total=False>>
1054 +base_commit : str
1055 +ours_commit : str
1056 +theirs_commit : str
1057 +conflict_paths : list~str~
1058 +other_branch : str
1059 }
1060 class MergeState {
1061 <<dataclass frozen>>
1062 +conflict_paths : list~str~
1063 +base_commit : str | None
1064 +ours_commit : str | None
1065 +theirs_commit : str | None
1066 +other_branch : str | None
1067 }
1068 class CommitRecord {
1069 <<dataclass>>
1070 +commit_id : str
1071 +branch : str
1072 +parent_commit_id : str | None
1073 }
1074
1075 MergeStatePayload ..> MergeState : read_merge_state() produces
1076 MergeState ..> MergeStatePayload : write_merge_state() serialises
1077 MergeState --> CommitRecord : base_commit, ours_commit, theirs_commit
1078 ```
1079
1080 ---
1081
1082 ### Diagram 4 — Configuration Type Hierarchy
1083
1084 The structured config.toml types. `MuseConfig` is the root; mutation functions
1085 read, modify a specific section, and write back. `isinstance` narrowing converts
1086 `tomllib`'s untyped output to the typed structure at the load boundary.
1087
1088 ```mermaid
1089 classDiagram
1090 class MuseConfig {
1091 <<TypedDict total=False>>
1092 +auth : AuthEntry
1093 +remotes : dict~str, RemoteEntry~
1094 }
1095 class AuthEntry {
1096 <<TypedDict total=False>>
1097 +token : str
1098 }
1099 class RemoteEntry {
1100 <<TypedDict total=False>>
1101 +url : str
1102 +branch : str
1103 }
1104 class RemoteConfig {
1105 <<TypedDict public API>>
1106 +name : str
1107 +url : str
1108 }
1109
1110 MuseConfig *-- AuthEntry : auth
1111 MuseConfig *-- RemoteEntry : remotes (by name)
1112 RemoteEntry ..> RemoteConfig : list_remotes() projects to
1113 ```
1114
1115 ---
1116
1117 ### Diagram 5 — MIDI / MusicXML Import Types
1118
1119 The parser output types for `muse import`. `RawMeta` is a discriminated union
1120 of two named shapes; no dict with mixed value types is exposed.
1121
1122 ```mermaid
1123 classDiagram
1124 class MidiMeta {
1125 <<TypedDict>>
1126 +num_tracks : int
1127 }
1128 class MusicXMLMeta {
1129 <<TypedDict>>
1130 +num_parts : int
1131 +part_names : list~str~
1132 }
1133 class NoteEvent {
1134 <<dataclass>>
1135 +pitch : int
1136 +velocity : int
1137 +start_tick : int
1138 +duration_ticks : int
1139 +channel : int
1140 +channel_name : str
1141 }
1142 class MuseImportData {
1143 <<dataclass>>
1144 +source_path : Path
1145 +format : str
1146 +ticks_per_beat : int
1147 +tempo_bpm : float
1148 +notes : list~NoteEvent~
1149 +tracks : list~str~
1150 +raw_meta : RawMeta
1151 }
1152 class RawMeta {
1153 <<TypeAlias>>
1154 MidiMeta | MusicXMLMeta
1155 }
1156
1157 MuseImportData *-- NoteEvent : notes
1158 MuseImportData --> RawMeta : raw_meta
1159 RawMeta ..> MidiMeta : MIDI files
1160 RawMeta ..> MusicXMLMeta : MusicXML files
1161 ```
1162
1163 ---
1164
1165 ### Diagram 6 — Error Hierarchy
1166
1167 Exit codes, base exception, and concrete error types. Every CLI command raises
1168 a typed exception or calls `raise typer.Exit(code=ExitCode.X)`.
1169
1170 ```mermaid
1171 classDiagram
1172 class ExitCode {
1173 <<IntEnum>>
1174 SUCCESS = 0
1175 USER_ERROR = 1
1176 REPO_NOT_FOUND = 2
1177 INTERNAL_ERROR = 3
1178 }
1179 class MuseCLIError {
1180 <<Exception>>
1181 +exit_code : ExitCode
1182 }
1183 class RepoNotFoundError {
1184 <<MuseCLIError>>
1185 default exit_code = REPO_NOT_FOUND
1186 default message = Not a Muse repository
1187 }
1188
1189 MuseCLIError --> ExitCode : exit_code
1190 RepoNotFoundError --|> MuseCLIError
1191 ```
1192
1193 ---
1194
1195 ### Diagram 7 — Stash Stack
1196
1197 The stash is a JSON array of `StashEntry` TypedDicts persisted to
1198 `.muse/stash.json`. Push prepends; pop removes index 0.
1199
1200 ```mermaid
1201 classDiagram
1202 class StashEntry {
1203 <<TypedDict>>
1204 +snapshot_id : str
1205 +manifest : dict~str, str~
1206 +branch : str
1207 +stashed_at : str
1208 }
1209 class SnapshotRecord {
1210 <<dataclass>>
1211 +snapshot_id : str
1212 +manifest : dict~str, str~
1213 }
1214
1215 StashEntry ..> SnapshotRecord : snapshot_id + manifest mirror
1216 ```
1217
1218 ---
1219
1220 ### Diagram 8 — Full Entity Overview
1221
1222 All named entities grouped by layer, showing the dependency flow from the
1223 domain protocol down through the store, CLI, and plugin layers.
1224
1225 ```mermaid
1226 classDiagram
1227 class MuseDomainPlugin {
1228 <<Protocol>>
1229 snapshot / diff / merge / drift / apply
1230 }
1231 class SnapshotManifest {
1232 <<TypedDict>>
1233 files: dict~str,str~ · domain: str
1234 }
1235 class DeltaManifest {
1236 <<TypedDict>>
1237 domain · added · removed · modified
1238 }
1239 class MergeResult {
1240 <<dataclass>>
1241 merged · conflicts · applied_strategies · dimension_reports
1242 }
1243 class DriftReport {
1244 <<dataclass>>
1245 has_drift · summary · delta
1246 }
1247 class CommitRecord {
1248 <<dataclass>>
1249 commit_id · branch · snapshot_id · metadata
1250 }
1251 class SnapshotRecord {
1252 <<dataclass>>
1253 snapshot_id · manifest: dict~str,str~
1254 }
1255 class TagRecord {
1256 <<dataclass>>
1257 tag_id · commit_id · tag
1258 }
1259 class CommitDict {
1260 <<TypedDict wire>>
1261 all str fields · committed_at: str
1262 }
1263 class SnapshotDict {
1264 <<TypedDict wire>>
1265 snapshot_id · manifest · created_at
1266 }
1267 class TagDict {
1268 <<TypedDict wire>>
1269 tag_id · repo_id · commit_id · tag
1270 }
1271 class RemoteCommitPayload {
1272 <<TypedDict total=False>>
1273 wire format + manifest
1274 }
1275 class MergeState {
1276 <<dataclass frozen>>
1277 conflict_paths · base/ours/theirs commits
1278 }
1279 class MergeStatePayload {
1280 <<TypedDict total=False>>
1281 MERGE_STATE.json shape
1282 }
1283 class MuseConfig {
1284 <<TypedDict total=False>>
1285 auth · remotes
1286 }
1287 class AuthEntry {
1288 <<TypedDict total=False>>
1289 token: str
1290 }
1291 class RemoteEntry {
1292 <<TypedDict total=False>>
1293 url · branch
1294 }
1295 class RemoteConfig {
1296 <<TypedDict>>
1297 name · url
1298 }
1299 class StashEntry {
1300 <<TypedDict>>
1301 snapshot_id · manifest · branch · stashed_at
1302 }
1303 class MuseImportData {
1304 <<dataclass>>
1305 notes · tracks · raw_meta
1306 }
1307 class NoteEvent {
1308 <<dataclass>>
1309 pitch · velocity · timing · channel
1310 }
1311 class MidiMeta {
1312 <<TypedDict>>
1313 num_tracks: int
1314 }
1315 class MusicXMLMeta {
1316 <<TypedDict>>
1317 num_parts · part_names
1318 }
1319 class AttributeRule {
1320 <<dataclass frozen>>
1321 path_pattern · dimension · strategy
1322 }
1323 class DimensionSlice {
1324 <<dataclass>>
1325 name · events · content_hash
1326 }
1327 class MidiDimensions {
1328 <<dataclass>>
1329 ticks_per_beat · slices: dict~str, DimensionSlice~
1330 }
1331 class ExitCode {
1332 <<IntEnum>>
1333 SUCCESS=0 · USER_ERROR=1 · REPO_NOT_FOUND=2 · INTERNAL_ERROR=3
1334 }
1335 class MuseCLIError {
1336 <<Exception>>
1337 exit_code: ExitCode
1338 }
1339 class RepoNotFoundError {
1340 <<MuseCLIError>>
1341 }
1342
1343 MuseDomainPlugin ..> SnapshotManifest : StateSnapshot
1344 MuseDomainPlugin ..> DeltaManifest : StateDelta
1345 MuseDomainPlugin --> MergeResult : merge() returns
1346 MuseDomainPlugin --> DriftReport : drift() returns
1347 MergeResult --> SnapshotManifest : merged
1348 DriftReport --> DeltaManifest : delta
1349
1350 CommitRecord ..> CommitDict : to_dict / from_dict
1351 SnapshotRecord ..> SnapshotDict : to_dict / from_dict
1352 TagRecord ..> TagDict : to_dict / from_dict
1353 RemoteCommitPayload ..> CommitDict : store_pulled_commit
1354 CommitRecord --> SnapshotRecord : snapshot_id
1355 MergeState ..> MergeStatePayload : serialise / deserialise
1356
1357 MuseConfig *-- AuthEntry : auth
1358 MuseConfig *-- RemoteEntry : remotes
1359 RemoteEntry ..> RemoteConfig : list_remotes()
1360
1361 StashEntry ..> SnapshotRecord : snapshot_id
1362 MuseImportData *-- NoteEvent : notes
1363 MuseImportData --> MidiMeta : raw_meta (MIDI)
1364 MuseImportData --> MusicXMLMeta : raw_meta (XML)
1365
1366 AttributeRule ..> MergeResult : applied_strategies reflects
1367 MidiDimensions *-- DimensionSlice : slices
1368 MidiDimensions ..> MergeResult : dimension_reports reflects
1369
1370 RepoNotFoundError --|> MuseCLIError
1371 MuseCLIError --> ExitCode : exit_code
1372 ```
1373
1374 ---
1375
1376 ### Diagram 9 — Attributes and MIDI Dimension Merge
1377
1378 The attribute rule pipeline and how it flows into the multidimensional MIDI
1379 merge engine. `AttributeRule` objects are produced by `load_attributes()` and
1380 consumed by both `MusicPlugin.merge()` and `merge_midi_dimensions()`.
1381 `DimensionSlice` is the core bucket type; `MidiDimensions` groups the four
1382 slices for one file.
1383
1384 ```mermaid
1385 classDiagram
1386 class AttributeRule {
1387 <<dataclass frozen>>
1388 +path_pattern : str
1389 +dimension : str
1390 +strategy : str
1391 +source_index : int
1392 }
1393 class DimensionSlice {
1394 <<dataclass>>
1395 +name : str
1396 +events : list~tuple~int, Message~~
1397 +content_hash : str
1398 }
1399 class MidiDimensions {
1400 <<dataclass>>
1401 +ticks_per_beat : int
1402 +file_type : int
1403 +slices : dict~str, DimensionSlice~
1404 +get(user_dim: str) DimensionSlice
1405 }
1406 class MergeResult {
1407 <<dataclass>>
1408 +merged : StateSnapshot
1409 +conflicts : list~str~
1410 +applied_strategies : dict~str, str~
1411 +dimension_reports : dict~str, dict~str, str~~
1412 +is_clean : bool
1413 }
1414 class MusicPlugin {
1415 <<MuseDomainPlugin>>
1416 +merge(base, left, right, *, repo_root) MergeResult
1417 }
1418
1419 MusicPlugin ..> AttributeRule : load_attributes()
1420 MusicPlugin ..> MidiDimensions : extract_dimensions()
1421 MusicPlugin --> MergeResult : returns
1422 MergeResult --> AttributeRule : applied_strategies reflects rules used
1423 MidiDimensions *-- DimensionSlice : slices (4 buckets)
1424 AttributeRule ..> DimensionSlice : resolve_strategy selects winner
1425 ```