type-contracts.md
markdown
| 1 | # Muse VCS — Type Contracts Reference |
| 2 | |
| 3 | > Updated: 2026-03-16 | 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 | - [Type Aliases](#type-aliases) |
| 22 | - [MergeResult and DriftReport Dataclasses](#mergeresult-and-driftreport-dataclasses) |
| 23 | - [MuseDomainPlugin Protocol](#musedomainplugin-protocol) |
| 24 | 3. [Store Types (`muse/core/store.py`)](#store-types) |
| 25 | - [Wire-Format TypedDicts](#wire-format-typeddicts) |
| 26 | - [In-Memory Dataclasses](#in-memory-dataclasses) |
| 27 | 4. [Merge Engine Types (`muse/core/merge_engine.py`)](#merge-engine-types) |
| 28 | 5. [Configuration Types (`muse/cli/config.py`)](#configuration-types) |
| 29 | 6. [MIDI / MusicXML Import Types (`muse/cli/midi_parser.py`)](#midi--musicxml-import-types) |
| 30 | 7. [Stash Types (`muse/cli/commands/stash.py`)](#stash-types) |
| 31 | 8. [Error Hierarchy (`muse/core/errors.py`)](#error-hierarchy) |
| 32 | 9. [Entity Hierarchy](#entity-hierarchy) |
| 33 | 10. [Entity Graphs (Mermaid)](#entity-graphs-mermaid) |
| 34 | |
| 35 | --- |
| 36 | |
| 37 | ## Design Philosophy |
| 38 | |
| 39 | Every entity in this codebase follows five rules: |
| 40 | |
| 41 | 1. **No `Any`. No `object`. Ever.** Both collapse type safety for downstream |
| 42 | callers. Every boundary is typed with a concrete named entity — `TypedDict`, |
| 43 | `dataclass`, `Protocol`, or a specific union. The CI typing audit enforces |
| 44 | this with a ratchet of zero violations. |
| 45 | |
| 46 | 2. **No covariance in collection aliases.** `dict[str, str]` and |
| 47 | `list[str]` are used directly. If a function's return mixes value types, |
| 48 | create a `TypedDict` for that shape instead of using `dict[str, str | int]`. |
| 49 | |
| 50 | 3. **Boundaries own coercion.** When external data arrives (JSON from disk, |
| 51 | TOML from config, MIDI bytes from disk), the boundary module coerces it |
| 52 | to the canonical internal type using `isinstance` narrowing. Downstream |
| 53 | code always sees clean types. |
| 54 | |
| 55 | 4. **Wire-format TypedDicts for serialisation, dataclasses for in-memory |
| 56 | logic.** `CommitDict`, `SnapshotDict`, `TagDict` are JSON-serialisable |
| 57 | and used by `to_dict()` / `from_dict()`. `CommitRecord`, `SnapshotRecord`, |
| 58 | `TagRecord` are rich dataclasses with typed `datetime` fields used in |
| 59 | business logic. |
| 60 | |
| 61 | 5. **The plugin protocol is the extension point.** All domain-specific logic |
| 62 | lives behind `MuseDomainPlugin`. The core DAG engine, branching, and |
| 63 | merge machinery know nothing about music, genomics, or any other domain. |
| 64 | Swapping domains is a one-file operation. |
| 65 | |
| 66 | ### What to use instead |
| 67 | |
| 68 | | Banned | Use instead | |
| 69 | |--------|-------------| |
| 70 | | `Any` | `TypedDict`, `dataclass`, specific union | |
| 71 | | `object` | The actual type or a constrained union | |
| 72 | | `list` (bare) | `list[X]` with concrete element type | |
| 73 | | `dict` (bare) | `dict[K, V]` with concrete key/value types | |
| 74 | | `dict[str, X]` with known keys | `TypedDict` — name the keys | |
| 75 | | `Optional[X]` | `X \| None` | |
| 76 | | Legacy `List`, `Dict`, `Set`, `Tuple` | Lowercase builtins | |
| 77 | | `cast(T, x)` | Fix the callee to return `T` | |
| 78 | | `# type: ignore` | Fix the underlying type error | |
| 79 | |
| 80 | --- |
| 81 | |
| 82 | ## Domain Protocol Types |
| 83 | |
| 84 | **Path:** `muse/domain.py` |
| 85 | |
| 86 | The five-interface contract that every domain plugin must satisfy. The core |
| 87 | engine implements the DAG, branching, merge-base finding, and lineage walking. |
| 88 | A domain plugin provides the five methods and gets the full VCS for free. |
| 89 | |
| 90 | ### Snapshot and Delta TypedDicts |
| 91 | |
| 92 | #### `SnapshotManifest` |
| 93 | |
| 94 | `TypedDict` — Content-addressed snapshot of domain state. Used as the |
| 95 | canonical representation of a point-in-time capture. JSON-serialisable and |
| 96 | content-addressable via SHA-256. |
| 97 | |
| 98 | | Field | Type | Description | |
| 99 | |-------|------|-------------| |
| 100 | | `files` | `dict[str, str]` | Workspace-relative POSIX paths → SHA-256 content digests | |
| 101 | | `domain` | `str` | Plugin identifier that produced this snapshot (e.g. `"music"`) | |
| 102 | |
| 103 | **Example:** |
| 104 | ```json |
| 105 | { |
| 106 | "files": { |
| 107 | "tracks/drums.mid": "a3f8...", |
| 108 | "tracks/bass.mid": "b291..." |
| 109 | }, |
| 110 | "domain": "music" |
| 111 | } |
| 112 | ``` |
| 113 | |
| 114 | #### `DeltaManifest` |
| 115 | |
| 116 | `TypedDict` — Minimal change description between two snapshots. Each list |
| 117 | contains workspace-relative POSIX paths. Produced by `MuseDomainPlugin.diff()`. |
| 118 | |
| 119 | | Field | Type | Description | |
| 120 | |-------|------|-------------| |
| 121 | | `domain` | `str` | Plugin identifier that produced this delta | |
| 122 | | `added` | `list[str]` | Paths present in target but absent from base | |
| 123 | | `removed` | `list[str]` | Paths present in base but absent from target | |
| 124 | | `modified` | `list[str]` | Paths present in both with differing digests | |
| 125 | |
| 126 | ### Type Aliases |
| 127 | |
| 128 | | Alias | Definition | Description | |
| 129 | |-------|-----------|-------------| |
| 130 | | `LiveState` | `SnapshotManifest \| pathlib.Path` | Current domain state — either an in-memory snapshot dict or a `muse-work/` directory path | |
| 131 | | `StateSnapshot` | `SnapshotManifest` | A content-addressed, immutable capture of state at a point in time | |
| 132 | | `StateDelta` | `DeltaManifest` | The minimal change between two snapshots | |
| 133 | |
| 134 | `LiveState` carries two forms intentionally: the CLI path is used when commands |
| 135 | interact with the filesystem (`muse commit`, `muse status`); the snapshot form |
| 136 | is used when the engine constructs merges and diffs entirely in memory. |
| 137 | |
| 138 | ### MergeResult and DriftReport Dataclasses |
| 139 | |
| 140 | #### `MergeResult` |
| 141 | |
| 142 | `@dataclass` — Outcome of a three-way merge between two divergent state lines. |
| 143 | An empty `conflicts` list means the merge was clean. |
| 144 | |
| 145 | | Field | Type | Default | Description | |
| 146 | |-------|------|---------|-------------| |
| 147 | | `merged` | `StateSnapshot` | required | The reconciled snapshot | |
| 148 | | `conflicts` | `list[str]` | `[]` | Human-readable conflict descriptions | |
| 149 | |
| 150 | **Property:** |
| 151 | |
| 152 | | Name | Returns | Description | |
| 153 | |------|---------|-------------| |
| 154 | | `is_clean` | `bool` | `True` when `conflicts` is empty | |
| 155 | |
| 156 | #### `DriftReport` |
| 157 | |
| 158 | `@dataclass` — Gap between committed state and current live state. Produced by |
| 159 | `MuseDomainPlugin.drift()` and consumed by `muse status`. |
| 160 | |
| 161 | | Field | Type | Default | Description | |
| 162 | |-------|------|---------|-------------| |
| 163 | | `has_drift` | `bool` | required | `True` when live state differs from committed snapshot | |
| 164 | | `summary` | `str` | `""` | Human-readable description (e.g. `"2 added, 1 modified"`) | |
| 165 | | `delta` | `StateDelta` | empty `DeltaManifest` | Machine-readable diff for programmatic consumers | |
| 166 | |
| 167 | ### MuseDomainPlugin Protocol |
| 168 | |
| 169 | `@runtime_checkable Protocol` — The five interfaces a domain plugin must |
| 170 | implement. Runtime-checkable so that `assert isinstance(plugin, MuseDomainPlugin)` |
| 171 | works as a module-load sanity check. |
| 172 | |
| 173 | | Method | Signature | Description | |
| 174 | |--------|-----------|-------------| |
| 175 | | `snapshot` | `(live_state: LiveState) -> StateSnapshot` | Capture current state as a content-addressed dict | |
| 176 | | `diff` | `(base: StateSnapshot, target: StateSnapshot) -> StateDelta` | Compute the minimal delta between two snapshots | |
| 177 | | `merge` | `(base, left, right: StateSnapshot) -> MergeResult` | Three-way merge two divergent state lines | |
| 178 | | `drift` | `(committed: StateSnapshot, live: LiveState) -> DriftReport` | Compare committed state vs current live state | |
| 179 | | `apply` | `(delta: StateDelta, live_state: LiveState) -> LiveState` | Apply a delta to produce a new live state | |
| 180 | |
| 181 | The music plugin (`muse.plugins.music.plugin`) is the reference implementation. |
| 182 | Every other domain — scientific simulation, genomics, 3D spatial design, |
| 183 | spacetime — implements these five methods and registers itself as a plugin. |
| 184 | |
| 185 | --- |
| 186 | |
| 187 | ## Store Types |
| 188 | |
| 189 | **Path:** `muse/core/store.py` |
| 190 | |
| 191 | All commit and snapshot metadata is stored as JSON files under `.muse/`. |
| 192 | Wire-format `TypedDict`s are the JSON-serialisable shapes used in `to_dict()` |
| 193 | and `from_dict()`. In-memory `dataclass`es are the rich representations used |
| 194 | in business logic throughout the CLI commands. |
| 195 | |
| 196 | ### Wire-Format TypedDicts |
| 197 | |
| 198 | These types appear at the boundary between Python objects and JSON on disk. |
| 199 | `json.loads()` returns an untyped result; `from_dict()` methods consume it and |
| 200 | return typed dataclasses. `to_dict()` methods produce these TypedDicts for |
| 201 | `json.dumps()`. |
| 202 | |
| 203 | #### `CommitDict` |
| 204 | |
| 205 | `TypedDict` — JSON-serialisable representation of a commit record. All datetime |
| 206 | values are ISO-8601 strings; callers convert to `datetime` inside `from_dict()`. |
| 207 | |
| 208 | | Field | Type | Description | |
| 209 | |-------|------|-------------| |
| 210 | | `commit_id` | `str` | SHA-256 hex digest of the commit's canonical inputs | |
| 211 | | `repo_id` | `str` | UUID identifying the repository | |
| 212 | | `branch` | `str` | Branch name at time of commit | |
| 213 | | `snapshot_id` | `str` | SHA-256 hex digest of the attached snapshot | |
| 214 | | `message` | `str` | Commit message | |
| 215 | | `committed_at` | `str` | ISO-8601 UTC timestamp | |
| 216 | | `parent_commit_id` | `str \| None` | First parent commit ID; `None` for initial commit | |
| 217 | | `parent2_commit_id` | `str \| None` | Second parent commit ID; non-`None` only for merge commits | |
| 218 | | `author` | `str` | Author name string | |
| 219 | | `metadata` | `dict[str, str]` | Extensible string→string metadata bag | |
| 220 | |
| 221 | #### `SnapshotDict` |
| 222 | |
| 223 | `TypedDict` — JSON-serialisable representation of a snapshot record. |
| 224 | |
| 225 | | Field | Type | Description | |
| 226 | |-------|------|-------------| |
| 227 | | `snapshot_id` | `str` | SHA-256 hex digest of the manifest | |
| 228 | | `manifest` | `dict[str, str]` | POSIX path → SHA-256 object digest | |
| 229 | | `created_at` | `str` | ISO-8601 UTC timestamp | |
| 230 | |
| 231 | #### `TagDict` |
| 232 | |
| 233 | `TypedDict` — JSON-serialisable representation of a semantic tag. |
| 234 | |
| 235 | | Field | Type | Description | |
| 236 | |-------|------|-------------| |
| 237 | | `tag_id` | `str` | UUID identifying the tag | |
| 238 | | `repo_id` | `str` | UUID identifying the repository | |
| 239 | | `commit_id` | `str` | SHA-256 commit ID this tag points to | |
| 240 | | `tag` | `str` | Tag name string (e.g. `"v1.0"`) | |
| 241 | | `created_at` | `str` | ISO-8601 UTC timestamp | |
| 242 | |
| 243 | #### `RemoteCommitPayload` |
| 244 | |
| 245 | `TypedDict (total=False)` — Wire format received from a remote during push/pull. |
| 246 | All fields are optional because the remote payload may omit fields unknown to |
| 247 | older protocol versions. Callers validate required fields before constructing |
| 248 | a `CommitRecord`. |
| 249 | |
| 250 | | Field | Type | Description | |
| 251 | |-------|------|-------------| |
| 252 | | `commit_id` | `str` | Commit identifier | |
| 253 | | `repo_id` | `str` | Repository UUID | |
| 254 | | `branch` | `str` | Branch name | |
| 255 | | `snapshot_id` | `str` | Snapshot identifier | |
| 256 | | `message` | `str` | Commit message | |
| 257 | | `committed_at` | `str` | ISO-8601 timestamp | |
| 258 | | `parent_commit_id` | `str \| None` | First parent | |
| 259 | | `parent2_commit_id` | `str \| None` | Second parent (merge commits) | |
| 260 | | `author` | `str` | Author name | |
| 261 | | `metadata` | `dict[str, str]` | Metadata bag | |
| 262 | | `manifest` | `dict[str, str]` | Inline snapshot manifest (remote optimisation) | |
| 263 | |
| 264 | ### In-Memory Dataclasses |
| 265 | |
| 266 | These rich types are constructed from wire-format TypedDicts after loading from |
| 267 | disk. They carry typed `datetime` values and are used throughout CLI command |
| 268 | implementations. |
| 269 | |
| 270 | #### `CommitRecord` |
| 271 | |
| 272 | `@dataclass` — In-memory representation of a commit. |
| 273 | |
| 274 | | Field | Type | Default | Description | |
| 275 | |-------|------|---------|-------------| |
| 276 | | `commit_id` | `str` | required | SHA-256 hex digest | |
| 277 | | `repo_id` | `str` | required | Repository UUID | |
| 278 | | `branch` | `str` | required | Branch name | |
| 279 | | `snapshot_id` | `str` | required | Attached snapshot digest | |
| 280 | | `message` | `str` | required | Commit message | |
| 281 | | `committed_at` | `datetime.datetime` | required | UTC commit timestamp | |
| 282 | | `parent_commit_id` | `str \| None` | `None` | First parent; `None` for root commits | |
| 283 | | `parent2_commit_id` | `str \| None` | `None` | Second parent for merge commits | |
| 284 | | `author` | `str` | `""` | Author name | |
| 285 | | `metadata` | `dict[str, str]` | `{}` | Extensible string→string metadata | |
| 286 | |
| 287 | **Methods:** |
| 288 | |
| 289 | | Method | Returns | Description | |
| 290 | |--------|---------|-------------| |
| 291 | | `to_dict()` | `CommitDict` | Serialise to JSON-ready TypedDict | |
| 292 | | `from_dict(d: CommitDict)` | `CommitRecord` | Deserialise from JSON-loaded TypedDict | |
| 293 | |
| 294 | #### `SnapshotRecord` |
| 295 | |
| 296 | `@dataclass` — In-memory representation of a content-addressed snapshot. |
| 297 | |
| 298 | | Field | Type | Default | Description | |
| 299 | |-------|------|---------|-------------| |
| 300 | | `snapshot_id` | `str` | required | SHA-256 hex digest of the manifest | |
| 301 | | `manifest` | `dict[str, str]` | required | POSIX path → SHA-256 object digest | |
| 302 | | `created_at` | `datetime.datetime` | UTC now | Creation timestamp | |
| 303 | |
| 304 | **Methods:** `to_dict() -> SnapshotDict`, `from_dict(d: SnapshotDict) -> SnapshotRecord` |
| 305 | |
| 306 | #### `TagRecord` |
| 307 | |
| 308 | `@dataclass` — In-memory representation of a semantic tag. |
| 309 | |
| 310 | | Field | Type | Default | Description | |
| 311 | |-------|------|---------|-------------| |
| 312 | | `tag_id` | `str` | required | UUID | |
| 313 | | `repo_id` | `str` | required | Repository UUID | |
| 314 | | `commit_id` | `str` | required | Tagged commit's SHA-256 digest | |
| 315 | | `tag` | `str` | required | Tag name | |
| 316 | | `created_at` | `datetime.datetime` | UTC now | Creation timestamp | |
| 317 | |
| 318 | **Methods:** `to_dict() -> TagDict`, `from_dict(d: TagDict) -> TagRecord` |
| 319 | |
| 320 | --- |
| 321 | |
| 322 | ## Merge Engine Types |
| 323 | |
| 324 | **Path:** `muse/core/merge_engine.py` |
| 325 | |
| 326 | #### `MergeStatePayload` |
| 327 | |
| 328 | `TypedDict (total=False)` — JSON-serialisable form of an in-progress merge |
| 329 | state. Written to `.muse/MERGE_STATE.json` when a merge has unresolved |
| 330 | conflicts. All fields are optional in the TypedDict because `other_branch` is |
| 331 | only set when the merge has a named second branch. |
| 332 | |
| 333 | | Field | Type | Description | |
| 334 | |-------|------|-------------| |
| 335 | | `base_commit` | `str` | Common ancestor commit ID | |
| 336 | | `ours_commit` | `str` | Current branch HEAD at merge start | |
| 337 | | `theirs_commit` | `str` | Incoming branch HEAD at merge start | |
| 338 | | `conflict_paths` | `list[str]` | POSIX paths with unresolved conflicts | |
| 339 | | `other_branch` | `str` | Name of the branch being merged in (optional) | |
| 340 | |
| 341 | #### `MergeState` |
| 342 | |
| 343 | `@dataclass (frozen=True)` — Loaded in-memory representation of |
| 344 | `MERGE_STATE.json`. Immutable so it can be passed around without accidental |
| 345 | mutation. |
| 346 | |
| 347 | | Field | Type | Default | Description | |
| 348 | |-------|------|---------|-------------| |
| 349 | | `conflict_paths` | `list[str]` | `[]` | Paths with unresolved conflicts | |
| 350 | | `base_commit` | `str \| None` | `None` | Common ancestor commit ID | |
| 351 | | `ours_commit` | `str \| None` | `None` | Our HEAD at merge start | |
| 352 | | `theirs_commit` | `str \| None` | `None` | Their HEAD at merge start | |
| 353 | | `other_branch` | `str \| None` | `None` | Name of the incoming branch | |
| 354 | |
| 355 | --- |
| 356 | |
| 357 | ## Configuration Types |
| 358 | |
| 359 | **Path:** `muse/cli/config.py` |
| 360 | |
| 361 | The structured view of `.muse/config.toml`. Loading from TOML uses `isinstance` |
| 362 | narrowing from `tomllib`'s untyped output — no `Any` annotation is ever written |
| 363 | in source. All mutation functions read the current config, modify the specific |
| 364 | section, and write back. |
| 365 | |
| 366 | #### `AuthEntry` |
| 367 | |
| 368 | `TypedDict (total=False)` — `[auth]` section in `.muse/config.toml`. |
| 369 | |
| 370 | | Field | Type | Description | |
| 371 | |-------|------|-------------| |
| 372 | | `token` | `str` | Bearer token for Muse Hub authentication. **Never logged.** | |
| 373 | |
| 374 | #### `RemoteEntry` |
| 375 | |
| 376 | `TypedDict (total=False)` — `[remotes.<name>]` section in `.muse/config.toml`. |
| 377 | |
| 378 | | Field | Type | Description | |
| 379 | |-------|------|-------------| |
| 380 | | `url` | `str` | Remote Hub URL (e.g. `"https://hub.example.com/repos/my-repo"`) | |
| 381 | | `branch` | `str` | Upstream branch tracked by this remote (set by `--set-upstream`) | |
| 382 | |
| 383 | #### `MuseConfig` |
| 384 | |
| 385 | `TypedDict (total=False)` — Structured view of the entire `.muse/config.toml` |
| 386 | file. All sections are optional; an empty dict is a valid `MuseConfig`. |
| 387 | |
| 388 | | Field | Type | Description | |
| 389 | |-------|------|-------------| |
| 390 | | `auth` | `AuthEntry` | Authentication credentials section | |
| 391 | | `remotes` | `dict[str, RemoteEntry]` | Named remote sections | |
| 392 | |
| 393 | #### `RemoteConfig` |
| 394 | |
| 395 | `TypedDict` — Public-facing remote descriptor returned by `list_remotes()`. |
| 396 | A lightweight projection of `RemoteEntry` that always has both required fields. |
| 397 | |
| 398 | | Field | Type | Description | |
| 399 | |-------|------|-------------| |
| 400 | | `name` | `str` | Remote name (e.g. `"origin"`) | |
| 401 | | `url` | `str` | Remote URL | |
| 402 | |
| 403 | --- |
| 404 | |
| 405 | ## MIDI / MusicXML Import Types |
| 406 | |
| 407 | **Path:** `muse/cli/midi_parser.py` |
| 408 | |
| 409 | Types used by `muse import` to parse Standard MIDI Files and MusicXML documents |
| 410 | into Muse's internal note representation. |
| 411 | |
| 412 | #### `MidiMeta` |
| 413 | |
| 414 | `TypedDict` — Format-specific metadata for Standard MIDI Files (`.mid`, `.midi`). |
| 415 | |
| 416 | | Field | Type | Description | |
| 417 | |-------|------|-------------| |
| 418 | | `num_tracks` | `int` | Number of MIDI tracks in the file | |
| 419 | |
| 420 | #### `MusicXMLMeta` |
| 421 | |
| 422 | `TypedDict` — Format-specific metadata for MusicXML files (`.xml`, `.musicxml`). |
| 423 | |
| 424 | | Field | Type | Description | |
| 425 | |-------|------|-------------| |
| 426 | | `num_parts` | `int` | Number of parts (instruments) in the score | |
| 427 | | `part_names` | `list[str]` | Display names of each part | |
| 428 | |
| 429 | #### `RawMeta` |
| 430 | |
| 431 | `TypeAlias = MidiMeta | MusicXMLMeta` — Discriminated union of all |
| 432 | format-specific metadata shapes. The `MuseImportData.raw_meta` field carries |
| 433 | one of these two named types depending on the source file's format. |
| 434 | |
| 435 | #### `NoteEvent` |
| 436 | |
| 437 | `@dataclass` — A single sounding note extracted from an imported file. |
| 438 | |
| 439 | | Field | Type | Description | |
| 440 | |-------|------|-------------| |
| 441 | | `pitch` | `int` | MIDI pitch number (0–127) | |
| 442 | | `velocity` | `int` | MIDI velocity (0–127; 0 = note-off) | |
| 443 | | `start_tick` | `int` | Onset tick relative to file start | |
| 444 | | `duration_ticks` | `int` | Note length in MIDI ticks | |
| 445 | | `channel` | `int` | MIDI channel (0–15) | |
| 446 | | `channel_name` | `str` | Track/part name for this channel | |
| 447 | |
| 448 | #### `MuseImportData` |
| 449 | |
| 450 | `@dataclass` — All data extracted from a single imported music file. The |
| 451 | complete parsed result returned by `parse_file()`. |
| 452 | |
| 453 | | Field | Type | Description | |
| 454 | |-------|------|-------------| |
| 455 | | `source_path` | `pathlib.Path` | Absolute path to the source file | |
| 456 | | `format` | `str` | `"midi"` or `"musicxml"` | |
| 457 | | `ticks_per_beat` | `int` | MIDI timing resolution (pulses per quarter note) | |
| 458 | | `tempo_bpm` | `float` | Tempo in beats per minute | |
| 459 | | `notes` | `list[NoteEvent]` | All sounding notes, in onset order | |
| 460 | | `tracks` | `list[str]` | Track/part names present in the file | |
| 461 | | `raw_meta` | `RawMeta` | Format-specific metadata (`MidiMeta` or `MusicXMLMeta`) | |
| 462 | |
| 463 | --- |
| 464 | |
| 465 | ## Stash Types |
| 466 | |
| 467 | **Path:** `muse/cli/commands/stash.py` |
| 468 | |
| 469 | #### `StashEntry` |
| 470 | |
| 471 | `TypedDict` — A single entry in the stash stack, persisted to |
| 472 | `.muse/stash.json` as one element of a JSON array. |
| 473 | |
| 474 | | Field | Type | Description | |
| 475 | |-------|------|-------------| |
| 476 | | `snapshot_id` | `str` | SHA-256 content digest of the stashed snapshot | |
| 477 | | `manifest` | `dict[str, str]` | POSIX path → SHA-256 object digest of stashed files | |
| 478 | | `branch` | `str` | Branch name that was active when the stash was saved | |
| 479 | | `stashed_at` | `str` | ISO-8601 UTC timestamp of the stash operation | |
| 480 | |
| 481 | --- |
| 482 | |
| 483 | ## Error Hierarchy |
| 484 | |
| 485 | **Path:** `muse/core/errors.py` |
| 486 | |
| 487 | #### `ExitCode` |
| 488 | |
| 489 | `IntEnum` — Standardised CLI exit codes. Used throughout the CLI commands via |
| 490 | `raise typer.Exit(code=ExitCode.USER_ERROR)`. |
| 491 | |
| 492 | | Value | Integer | Meaning | |
| 493 | |-------|---------|---------| |
| 494 | | `SUCCESS` | `0` | Command completed successfully | |
| 495 | | `USER_ERROR` | `1` | Bad arguments or invalid user input | |
| 496 | | `REPO_NOT_FOUND` | `2` | Not inside a Muse repository | |
| 497 | | `INTERNAL_ERROR` | `3` | Unexpected internal failure | |
| 498 | |
| 499 | #### `MuseCLIError` |
| 500 | |
| 501 | `Exception` — Base exception for all Muse CLI errors. Carries an exit code |
| 502 | so that top-level handlers can produce the correct process exit. |
| 503 | |
| 504 | | Field | Type | Description | |
| 505 | |-------|------|-------------| |
| 506 | | `exit_code` | `ExitCode` | Exit code to use when this exception terminates the process | |
| 507 | |
| 508 | #### `RepoNotFoundError` |
| 509 | |
| 510 | `MuseCLIError` — Raised by `find_repo_root()` callers when a command is invoked |
| 511 | outside a Muse repository. Default message: `"Not a Muse repository. Run muse init."` Default exit code: `ExitCode.REPO_NOT_FOUND`. |
| 512 | |
| 513 | **Alias:** `MuseNotARepoError = RepoNotFoundError` |
| 514 | |
| 515 | --- |
| 516 | |
| 517 | ## Entity Hierarchy |
| 518 | |
| 519 | ``` |
| 520 | Muse VCS |
| 521 | │ |
| 522 | ├── Domain Protocol (muse/domain.py) |
| 523 | │ │ |
| 524 | │ ├── Snapshot and Delta |
| 525 | │ │ ├── SnapshotManifest — TypedDict: {files: dict[str,str], domain: str} |
| 526 | │ │ └── DeltaManifest — TypedDict: {domain, added, removed, modified} |
| 527 | │ │ |
| 528 | │ ├── Type Aliases |
| 529 | │ │ ├── LiveState — SnapshotManifest | pathlib.Path |
| 530 | │ │ ├── StateSnapshot — SnapshotManifest |
| 531 | │ │ └── StateDelta — DeltaManifest |
| 532 | │ │ |
| 533 | │ ├── Result Types |
| 534 | │ │ ├── MergeResult — dataclass: merged + conflicts list |
| 535 | │ │ └── DriftReport — dataclass: has_drift + summary + delta |
| 536 | │ │ |
| 537 | │ └── MuseDomainPlugin — Protocol (runtime_checkable): 5 methods |
| 538 | │ |
| 539 | ├── Store (muse/core/store.py) |
| 540 | │ │ |
| 541 | │ ├── Wire-Format TypedDicts |
| 542 | │ │ ├── CommitDict — TypedDict: all commit fields (str timestamps) |
| 543 | │ │ ├── SnapshotDict — TypedDict: snapshot_id + manifest + created_at |
| 544 | │ │ ├── TagDict — TypedDict: tag identity fields |
| 545 | │ │ └── RemoteCommitPayload — TypedDict (total=False): wire format + manifest |
| 546 | │ │ |
| 547 | │ └── In-Memory Dataclasses |
| 548 | │ ├── CommitRecord — dataclass: typed datetime, to_dict/from_dict |
| 549 | │ ├── SnapshotRecord — dataclass: manifest + datetime |
| 550 | │ └── TagRecord — dataclass: tag metadata + datetime |
| 551 | │ |
| 552 | ├── Merge Engine (muse/core/merge_engine.py) |
| 553 | │ ├── MergeStatePayload — TypedDict (total=False): MERGE_STATE.json shape |
| 554 | │ └── MergeState — dataclass (frozen): loaded in-memory merge state |
| 555 | │ |
| 556 | ├── Configuration (muse/cli/config.py) |
| 557 | │ ├── AuthEntry — TypedDict (total=False): [auth] section |
| 558 | │ ├── RemoteEntry — TypedDict (total=False): [remotes.<name>] section |
| 559 | │ ├── MuseConfig — TypedDict (total=False): full config.toml shape |
| 560 | │ └── RemoteConfig — TypedDict: public remote descriptor |
| 561 | │ |
| 562 | ├── MIDI / MusicXML Import (muse/cli/midi_parser.py) |
| 563 | │ ├── MidiMeta — TypedDict: num_tracks |
| 564 | │ ├── MusicXMLMeta — TypedDict: num_parts + part_names |
| 565 | │ ├── RawMeta — TypeAlias: MidiMeta | MusicXMLMeta |
| 566 | │ ├── NoteEvent — dataclass: pitch, velocity, timing, channel |
| 567 | │ └── MuseImportData — dataclass: full parsed file result |
| 568 | │ |
| 569 | ├── Stash (muse/cli/commands/stash.py) |
| 570 | │ └── StashEntry — TypedDict: snapshot_id + manifest + branch + stashed_at |
| 571 | │ |
| 572 | └── Errors (muse/core/errors.py) |
| 573 | ├── ExitCode — IntEnum: SUCCESS=0 USER_ERROR=1 REPO_NOT_FOUND=2 INTERNAL_ERROR=3 |
| 574 | ├── MuseCLIError — Exception base: carries ExitCode |
| 575 | ├── RepoNotFoundError — MuseCLIError: default exit REPO_NOT_FOUND |
| 576 | └── MuseNotARepoError — alias for RepoNotFoundError |
| 577 | ``` |
| 578 | |
| 579 | --- |
| 580 | |
| 581 | ## Entity Graphs (Mermaid) |
| 582 | |
| 583 | Arrow conventions: |
| 584 | - `*--` composition (owns, lifecycle-coupled) |
| 585 | - `-->` association (references) |
| 586 | - `..>` dependency (uses) |
| 587 | - `..>` with label: produces / implements |
| 588 | |
| 589 | --- |
| 590 | |
| 591 | ### Diagram 1 — Domain Protocol and Plugin Contract |
| 592 | |
| 593 | The `MuseDomainPlugin` protocol and the types that flow through its five methods. `MusicPlugin` is the reference implementation that proves the abstraction. |
| 594 | |
| 595 | ```mermaid |
| 596 | classDiagram |
| 597 | class SnapshotManifest { |
| 598 | <<TypedDict>> |
| 599 | +files : dict~str, str~ |
| 600 | +domain : str |
| 601 | } |
| 602 | class DeltaManifest { |
| 603 | <<TypedDict>> |
| 604 | +domain : str |
| 605 | +added : list~str~ |
| 606 | +removed : list~str~ |
| 607 | +modified : list~str~ |
| 608 | } |
| 609 | class MergeResult { |
| 610 | <<dataclass>> |
| 611 | +merged : StateSnapshot |
| 612 | +conflicts : list~str~ |
| 613 | +is_clean : bool |
| 614 | } |
| 615 | class DriftReport { |
| 616 | <<dataclass>> |
| 617 | +has_drift : bool |
| 618 | +summary : str |
| 619 | +delta : StateDelta |
| 620 | } |
| 621 | class MuseDomainPlugin { |
| 622 | <<Protocol runtime_checkable>> |
| 623 | +snapshot(live_state: LiveState) StateSnapshot |
| 624 | +diff(base, target: StateSnapshot) StateDelta |
| 625 | +merge(base, left, right: StateSnapshot) MergeResult |
| 626 | +drift(committed: StateSnapshot, live: LiveState) DriftReport |
| 627 | +apply(delta: StateDelta, live_state: LiveState) LiveState |
| 628 | } |
| 629 | class MusicPlugin { |
| 630 | <<reference implementation>> |
| 631 | +snapshot(live_state) StateSnapshot |
| 632 | +diff(base, target) StateDelta |
| 633 | +merge(base, left, right) MergeResult |
| 634 | +drift(committed, live) DriftReport |
| 635 | +apply(delta, live_state) LiveState |
| 636 | } |
| 637 | |
| 638 | MuseDomainPlugin ..> SnapshotManifest : StateSnapshot alias |
| 639 | MuseDomainPlugin ..> DeltaManifest : StateDelta alias |
| 640 | MuseDomainPlugin --> MergeResult : merge() returns |
| 641 | MuseDomainPlugin --> DriftReport : drift() returns |
| 642 | MusicPlugin ..|> MuseDomainPlugin : implements |
| 643 | MergeResult --> SnapshotManifest : merged |
| 644 | DriftReport --> DeltaManifest : delta |
| 645 | ``` |
| 646 | |
| 647 | --- |
| 648 | |
| 649 | ### Diagram 2 — Store Wire-Format TypedDicts and Dataclasses |
| 650 | |
| 651 | The two-layer design: wire-format TypedDicts for JSON serialisation, rich |
| 652 | dataclasses for in-memory logic. Every `from_dict` consumes the TypedDict |
| 653 | shape produced by `json.loads()`; every `to_dict` produces it for |
| 654 | `json.dumps()`. |
| 655 | |
| 656 | ```mermaid |
| 657 | classDiagram |
| 658 | class CommitDict { |
| 659 | <<TypedDict wire format>> |
| 660 | +commit_id : str |
| 661 | +repo_id : str |
| 662 | +branch : str |
| 663 | +snapshot_id : str |
| 664 | +message : str |
| 665 | +committed_at : str |
| 666 | +parent_commit_id : str | None |
| 667 | +parent2_commit_id : str | None |
| 668 | +author : str |
| 669 | +metadata : dict~str, str~ |
| 670 | } |
| 671 | class SnapshotDict { |
| 672 | <<TypedDict wire format>> |
| 673 | +snapshot_id : str |
| 674 | +manifest : dict~str, str~ |
| 675 | +created_at : str |
| 676 | } |
| 677 | class TagDict { |
| 678 | <<TypedDict wire format>> |
| 679 | +tag_id : str |
| 680 | +repo_id : str |
| 681 | +commit_id : str |
| 682 | +tag : str |
| 683 | +created_at : str |
| 684 | } |
| 685 | class RemoteCommitPayload { |
| 686 | <<TypedDict total=False>> |
| 687 | +commit_id : str |
| 688 | +repo_id : str |
| 689 | +branch : str |
| 690 | +snapshot_id : str |
| 691 | +message : str |
| 692 | +committed_at : str |
| 693 | +parent_commit_id : str | None |
| 694 | +parent2_commit_id : str | None |
| 695 | +author : str |
| 696 | +metadata : dict~str, str~ |
| 697 | +manifest : dict~str, str~ |
| 698 | } |
| 699 | class CommitRecord { |
| 700 | <<dataclass>> |
| 701 | +commit_id : str |
| 702 | +repo_id : str |
| 703 | +branch : str |
| 704 | +snapshot_id : str |
| 705 | +message : str |
| 706 | +committed_at : datetime |
| 707 | +parent_commit_id : str | None |
| 708 | +parent2_commit_id : str | None |
| 709 | +author : str |
| 710 | +metadata : dict~str, str~ |
| 711 | +to_dict() CommitDict |
| 712 | +from_dict(d: CommitDict) CommitRecord |
| 713 | } |
| 714 | class SnapshotRecord { |
| 715 | <<dataclass>> |
| 716 | +snapshot_id : str |
| 717 | +manifest : dict~str, str~ |
| 718 | +created_at : datetime |
| 719 | +to_dict() SnapshotDict |
| 720 | +from_dict(d: SnapshotDict) SnapshotRecord |
| 721 | } |
| 722 | class TagRecord { |
| 723 | <<dataclass>> |
| 724 | +tag_id : str |
| 725 | +repo_id : str |
| 726 | +commit_id : str |
| 727 | +tag : str |
| 728 | +created_at : datetime |
| 729 | +to_dict() TagDict |
| 730 | +from_dict(d: TagDict) TagRecord |
| 731 | } |
| 732 | |
| 733 | CommitRecord ..> CommitDict : to_dict produces |
| 734 | CommitDict ..> CommitRecord : from_dict produces |
| 735 | SnapshotRecord ..> SnapshotDict : to_dict produces |
| 736 | SnapshotDict ..> SnapshotRecord : from_dict produces |
| 737 | TagRecord ..> TagDict : to_dict produces |
| 738 | TagDict ..> TagRecord : from_dict produces |
| 739 | RemoteCommitPayload ..> CommitDict : store_pulled_commit builds |
| 740 | CommitRecord --> SnapshotRecord : snapshot_id reference |
| 741 | CommitRecord --> TagRecord : commit_id reference (via TagRecord) |
| 742 | ``` |
| 743 | |
| 744 | --- |
| 745 | |
| 746 | ### Diagram 3 — Merge Engine State |
| 747 | |
| 748 | The in-progress merge state written to disk on conflict and loaded on |
| 749 | continuation. `MergeStatePayload` is the JSON shape; `MergeState` is the |
| 750 | loaded, immutable in-memory form. |
| 751 | |
| 752 | ```mermaid |
| 753 | classDiagram |
| 754 | class MergeStatePayload { |
| 755 | <<TypedDict total=False>> |
| 756 | +base_commit : str |
| 757 | +ours_commit : str |
| 758 | +theirs_commit : str |
| 759 | +conflict_paths : list~str~ |
| 760 | +other_branch : str |
| 761 | } |
| 762 | class MergeState { |
| 763 | <<dataclass frozen>> |
| 764 | +conflict_paths : list~str~ |
| 765 | +base_commit : str | None |
| 766 | +ours_commit : str | None |
| 767 | +theirs_commit : str | None |
| 768 | +other_branch : str | None |
| 769 | } |
| 770 | class CommitRecord { |
| 771 | <<dataclass>> |
| 772 | +commit_id : str |
| 773 | +branch : str |
| 774 | +parent_commit_id : str | None |
| 775 | } |
| 776 | |
| 777 | MergeStatePayload ..> MergeState : read_merge_state() produces |
| 778 | MergeState ..> MergeStatePayload : write_merge_state() serialises |
| 779 | MergeState --> CommitRecord : base_commit, ours_commit, theirs_commit |
| 780 | ``` |
| 781 | |
| 782 | --- |
| 783 | |
| 784 | ### Diagram 4 — Configuration Type Hierarchy |
| 785 | |
| 786 | The structured config.toml types. `MuseConfig` is the root; mutation functions |
| 787 | read, modify a specific section, and write back. `isinstance` narrowing converts |
| 788 | `tomllib`'s untyped output to the typed structure at the load boundary. |
| 789 | |
| 790 | ```mermaid |
| 791 | classDiagram |
| 792 | class MuseConfig { |
| 793 | <<TypedDict total=False>> |
| 794 | +auth : AuthEntry |
| 795 | +remotes : dict~str, RemoteEntry~ |
| 796 | } |
| 797 | class AuthEntry { |
| 798 | <<TypedDict total=False>> |
| 799 | +token : str |
| 800 | } |
| 801 | class RemoteEntry { |
| 802 | <<TypedDict total=False>> |
| 803 | +url : str |
| 804 | +branch : str |
| 805 | } |
| 806 | class RemoteConfig { |
| 807 | <<TypedDict public API>> |
| 808 | +name : str |
| 809 | +url : str |
| 810 | } |
| 811 | |
| 812 | MuseConfig *-- AuthEntry : auth |
| 813 | MuseConfig *-- RemoteEntry : remotes (by name) |
| 814 | RemoteEntry ..> RemoteConfig : list_remotes() projects to |
| 815 | ``` |
| 816 | |
| 817 | --- |
| 818 | |
| 819 | ### Diagram 5 — MIDI / MusicXML Import Types |
| 820 | |
| 821 | The parser output types for `muse import`. `RawMeta` is a discriminated union |
| 822 | of two named shapes; no dict with mixed value types is exposed. |
| 823 | |
| 824 | ```mermaid |
| 825 | classDiagram |
| 826 | class MidiMeta { |
| 827 | <<TypedDict>> |
| 828 | +num_tracks : int |
| 829 | } |
| 830 | class MusicXMLMeta { |
| 831 | <<TypedDict>> |
| 832 | +num_parts : int |
| 833 | +part_names : list~str~ |
| 834 | } |
| 835 | class NoteEvent { |
| 836 | <<dataclass>> |
| 837 | +pitch : int |
| 838 | +velocity : int |
| 839 | +start_tick : int |
| 840 | +duration_ticks : int |
| 841 | +channel : int |
| 842 | +channel_name : str |
| 843 | } |
| 844 | class MuseImportData { |
| 845 | <<dataclass>> |
| 846 | +source_path : Path |
| 847 | +format : str |
| 848 | +ticks_per_beat : int |
| 849 | +tempo_bpm : float |
| 850 | +notes : list~NoteEvent~ |
| 851 | +tracks : list~str~ |
| 852 | +raw_meta : RawMeta |
| 853 | } |
| 854 | class RawMeta { |
| 855 | <<TypeAlias>> |
| 856 | MidiMeta | MusicXMLMeta |
| 857 | } |
| 858 | |
| 859 | MuseImportData *-- NoteEvent : notes |
| 860 | MuseImportData --> RawMeta : raw_meta |
| 861 | RawMeta ..> MidiMeta : MIDI files |
| 862 | RawMeta ..> MusicXMLMeta : MusicXML files |
| 863 | ``` |
| 864 | |
| 865 | --- |
| 866 | |
| 867 | ### Diagram 6 — Error Hierarchy |
| 868 | |
| 869 | Exit codes, base exception, and concrete error types. Every CLI command raises |
| 870 | a typed exception or calls `raise typer.Exit(code=ExitCode.X)`. |
| 871 | |
| 872 | ```mermaid |
| 873 | classDiagram |
| 874 | class ExitCode { |
| 875 | <<IntEnum>> |
| 876 | SUCCESS = 0 |
| 877 | USER_ERROR = 1 |
| 878 | REPO_NOT_FOUND = 2 |
| 879 | INTERNAL_ERROR = 3 |
| 880 | } |
| 881 | class MuseCLIError { |
| 882 | <<Exception>> |
| 883 | +exit_code : ExitCode |
| 884 | } |
| 885 | class RepoNotFoundError { |
| 886 | <<MuseCLIError>> |
| 887 | default exit_code = REPO_NOT_FOUND |
| 888 | default message = Not a Muse repository |
| 889 | } |
| 890 | |
| 891 | MuseCLIError --> ExitCode : exit_code |
| 892 | RepoNotFoundError --|> MuseCLIError |
| 893 | ``` |
| 894 | |
| 895 | --- |
| 896 | |
| 897 | ### Diagram 7 — Stash Stack |
| 898 | |
| 899 | The stash is a JSON array of `StashEntry` TypedDicts persisted to |
| 900 | `.muse/stash.json`. Push prepends; pop removes index 0. |
| 901 | |
| 902 | ```mermaid |
| 903 | classDiagram |
| 904 | class StashEntry { |
| 905 | <<TypedDict>> |
| 906 | +snapshot_id : str |
| 907 | +manifest : dict~str, str~ |
| 908 | +branch : str |
| 909 | +stashed_at : str |
| 910 | } |
| 911 | class SnapshotRecord { |
| 912 | <<dataclass>> |
| 913 | +snapshot_id : str |
| 914 | +manifest : dict~str, str~ |
| 915 | } |
| 916 | |
| 917 | StashEntry ..> SnapshotRecord : snapshot_id + manifest mirror |
| 918 | ``` |
| 919 | |
| 920 | --- |
| 921 | |
| 922 | ### Diagram 8 — Full Entity Overview |
| 923 | |
| 924 | All named entities grouped by layer, showing the dependency flow from the |
| 925 | domain protocol down through the store, CLI, and plugin layers. |
| 926 | |
| 927 | ```mermaid |
| 928 | classDiagram |
| 929 | class MuseDomainPlugin { |
| 930 | <<Protocol>> |
| 931 | snapshot / diff / merge / drift / apply |
| 932 | } |
| 933 | class SnapshotManifest { |
| 934 | <<TypedDict>> |
| 935 | files: dict~str,str~ · domain: str |
| 936 | } |
| 937 | class DeltaManifest { |
| 938 | <<TypedDict>> |
| 939 | domain · added · removed · modified |
| 940 | } |
| 941 | class MergeResult { |
| 942 | <<dataclass>> |
| 943 | merged: StateSnapshot · conflicts: list~str~ |
| 944 | } |
| 945 | class DriftReport { |
| 946 | <<dataclass>> |
| 947 | has_drift · summary · delta |
| 948 | } |
| 949 | class CommitRecord { |
| 950 | <<dataclass>> |
| 951 | commit_id · branch · snapshot_id · metadata |
| 952 | } |
| 953 | class SnapshotRecord { |
| 954 | <<dataclass>> |
| 955 | snapshot_id · manifest: dict~str,str~ |
| 956 | } |
| 957 | class TagRecord { |
| 958 | <<dataclass>> |
| 959 | tag_id · commit_id · tag |
| 960 | } |
| 961 | class CommitDict { |
| 962 | <<TypedDict wire>> |
| 963 | all str fields · committed_at: str |
| 964 | } |
| 965 | class SnapshotDict { |
| 966 | <<TypedDict wire>> |
| 967 | snapshot_id · manifest · created_at |
| 968 | } |
| 969 | class TagDict { |
| 970 | <<TypedDict wire>> |
| 971 | tag_id · repo_id · commit_id · tag |
| 972 | } |
| 973 | class RemoteCommitPayload { |
| 974 | <<TypedDict total=False>> |
| 975 | wire format + manifest |
| 976 | } |
| 977 | class MergeState { |
| 978 | <<dataclass frozen>> |
| 979 | conflict_paths · base/ours/theirs commits |
| 980 | } |
| 981 | class MergeStatePayload { |
| 982 | <<TypedDict total=False>> |
| 983 | MERGE_STATE.json shape |
| 984 | } |
| 985 | class MuseConfig { |
| 986 | <<TypedDict total=False>> |
| 987 | auth · remotes |
| 988 | } |
| 989 | class AuthEntry { |
| 990 | <<TypedDict total=False>> |
| 991 | token: str |
| 992 | } |
| 993 | class RemoteEntry { |
| 994 | <<TypedDict total=False>> |
| 995 | url · branch |
| 996 | } |
| 997 | class RemoteConfig { |
| 998 | <<TypedDict>> |
| 999 | name · url |
| 1000 | } |
| 1001 | class StashEntry { |
| 1002 | <<TypedDict>> |
| 1003 | snapshot_id · manifest · branch · stashed_at |
| 1004 | } |
| 1005 | class MuseImportData { |
| 1006 | <<dataclass>> |
| 1007 | notes · tracks · raw_meta |
| 1008 | } |
| 1009 | class NoteEvent { |
| 1010 | <<dataclass>> |
| 1011 | pitch · velocity · timing · channel |
| 1012 | } |
| 1013 | class MidiMeta { |
| 1014 | <<TypedDict>> |
| 1015 | num_tracks: int |
| 1016 | } |
| 1017 | class MusicXMLMeta { |
| 1018 | <<TypedDict>> |
| 1019 | num_parts · part_names |
| 1020 | } |
| 1021 | class ExitCode { |
| 1022 | <<IntEnum>> |
| 1023 | SUCCESS=0 · USER_ERROR=1 · REPO_NOT_FOUND=2 · INTERNAL_ERROR=3 |
| 1024 | } |
| 1025 | class MuseCLIError { |
| 1026 | <<Exception>> |
| 1027 | exit_code: ExitCode |
| 1028 | } |
| 1029 | class RepoNotFoundError { |
| 1030 | <<MuseCLIError>> |
| 1031 | } |
| 1032 | |
| 1033 | MuseDomainPlugin ..> SnapshotManifest : StateSnapshot |
| 1034 | MuseDomainPlugin ..> DeltaManifest : StateDelta |
| 1035 | MuseDomainPlugin --> MergeResult : merge() returns |
| 1036 | MuseDomainPlugin --> DriftReport : drift() returns |
| 1037 | MergeResult --> SnapshotManifest : merged |
| 1038 | DriftReport --> DeltaManifest : delta |
| 1039 | |
| 1040 | CommitRecord ..> CommitDict : to_dict / from_dict |
| 1041 | SnapshotRecord ..> SnapshotDict : to_dict / from_dict |
| 1042 | TagRecord ..> TagDict : to_dict / from_dict |
| 1043 | RemoteCommitPayload ..> CommitDict : store_pulled_commit |
| 1044 | CommitRecord --> SnapshotRecord : snapshot_id |
| 1045 | MergeState ..> MergeStatePayload : serialise / deserialise |
| 1046 | |
| 1047 | MuseConfig *-- AuthEntry : auth |
| 1048 | MuseConfig *-- RemoteEntry : remotes |
| 1049 | RemoteEntry ..> RemoteConfig : list_remotes() |
| 1050 | |
| 1051 | StashEntry ..> SnapshotRecord : snapshot_id |
| 1052 | MuseImportData *-- NoteEvent : notes |
| 1053 | MuseImportData --> MidiMeta : raw_meta (MIDI) |
| 1054 | MuseImportData --> MusicXMLMeta : raw_meta (XML) |
| 1055 | |
| 1056 | RepoNotFoundError --|> MuseCLIError |
| 1057 | MuseCLIError --> ExitCode : exit_code |
| 1058 | ``` |