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