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