cgcardona / muse public
domain.py python
219 lines 8.4 KB
0e0cbf44 feat: .museattributes + multidimensional MIDI merge (#11) Gabriel Cardona <cgcardona@gmail.com> 3d ago
1 """MuseDomainPlugin — the five-interface protocol that defines a Muse domain.
2
3 Muse provides the DAG engine, content-addressed object store, branching,
4 lineage walking, topological log graph, and merge base finder. A domain plugin
5 implements these five interfaces and Muse does the rest.
6
7 The music plugin (``muse.plugins.music``) is the reference implementation.
8 Every other domain — scientific simulation, genomics, 3D spatial design,
9 spacetime — is a new plugin.
10 """
11 from __future__ import annotations
12
13 import pathlib
14 from dataclasses import dataclass, field
15 from typing import Protocol, TypedDict, runtime_checkable
16
17
18 # ---------------------------------------------------------------------------
19 # Named snapshot and delta types
20 # ---------------------------------------------------------------------------
21
22
23 class SnapshotManifest(TypedDict):
24 """Content-addressed snapshot of domain state.
25
26 ``files`` maps workspace-relative POSIX paths to their SHA-256 content
27 digests. ``domain`` identifies which plugin produced this snapshot.
28 """
29
30 files: dict[str, str]
31 domain: str
32
33
34 class DeltaManifest(TypedDict):
35 """Minimal change description between two snapshots.
36
37 Each list contains workspace-relative POSIX paths. ``domain`` identifies
38 the plugin that produced this delta.
39 """
40
41 domain: str
42 added: list[str]
43 removed: list[str]
44 modified: list[str]
45
46
47 # ---------------------------------------------------------------------------
48 # Type aliases used in the protocol signatures
49 # ---------------------------------------------------------------------------
50
51 #: Live state is either an already-snapshotted manifest dict or a workdir path.
52 #: The music plugin accepts both: a Path (for CLI commit/status) and a
53 #: SnapshotManifest dict (for in-memory merge and diff operations).
54 LiveState = SnapshotManifest | pathlib.Path
55
56 #: A content-addressed, immutable snapshot of state at a point in time.
57 StateSnapshot = SnapshotManifest
58
59 #: The minimal change between two snapshots — additions, removals, mutations.
60 StateDelta = DeltaManifest
61
62
63 # ---------------------------------------------------------------------------
64 # Merge and drift result types
65 # ---------------------------------------------------------------------------
66
67
68 @dataclass
69 class MergeResult:
70 """Outcome of a three-way merge between two divergent state lines.
71
72 ``merged`` is the reconciled snapshot. ``conflicts`` is a list of
73 workspace-relative file paths that could not be auto-merged and require
74 manual resolution. An empty ``conflicts`` list means the merge was clean.
75 The CLI is responsible for formatting user-facing messages from these paths.
76
77 ``applied_strategies`` maps each path where a ``.museattributes`` rule
78 overrode the default conflict behaviour to the strategy that was applied.
79 Paths absent from this dict were resolved by the standard three-way merge
80 logic. Example::
81
82 {"tracks/drums.mid": "ours", "keys/piano.mid": "theirs"}
83
84 ``dimension_reports`` maps conflicting paths to their per-dimension
85 resolution detail. Each inner dict maps a dimension name to the strategy
86 or winner chosen for that dimension (e.g. ``{"melodic": "ours", "dynamic":
87 "theirs"}``). Only populated for files where dimension-level merge was
88 attempted.
89 """
90
91 merged: StateSnapshot
92 conflicts: list[str] = field(default_factory=list)
93 applied_strategies: dict[str, str] = field(default_factory=dict)
94 dimension_reports: dict[str, dict[str, str]] = field(default_factory=dict)
95
96 @property
97 def is_clean(self) -> bool:
98 return len(self.conflicts) == 0
99
100
101 @dataclass
102 class DriftReport:
103 """Gap between committed state and current live state.
104
105 ``has_drift`` is ``True`` when the live state differs from the committed
106 snapshot. ``summary`` is a human-readable description of what changed.
107 ``delta`` is the machine-readable diff for programmatic consumers.
108 """
109
110 has_drift: bool
111 summary: str = ""
112 delta: StateDelta = field(default_factory=lambda: DeltaManifest(
113 domain="", added=[], removed=[], modified=[],
114 ))
115
116
117 # ---------------------------------------------------------------------------
118 # The plugin protocol
119 # ---------------------------------------------------------------------------
120
121
122 @runtime_checkable
123 class MuseDomainPlugin(Protocol):
124 """The five interfaces a domain plugin must implement.
125
126 Muse provides everything else: the DAG, branching, checkout, lineage
127 walking, ASCII log graph, and merge base finder. Implement these five
128 methods and your domain gets the full Muse VCS for free.
129
130 Music is the reference implementation (``muse.plugins.music``).
131 """
132
133 def snapshot(self, live_state: LiveState) -> StateSnapshot:
134 """Capture current live state as a serialisable, hashable snapshot.
135
136 The returned ``SnapshotManifest`` must be JSON-serialisable. Muse will
137 compute a SHA-256 content address from the canonical JSON form and
138 store the snapshot as a blob in ``.muse/objects/``.
139
140 **``.museignore`` contract** — when *live_state* is a
141 ``pathlib.Path`` (the ``muse-work/`` directory), domain plugin
142 implementations **must** honour ``.museignore`` by calling
143 :func:`muse.core.ignore.load_patterns` on the repository root and
144 filtering out paths matched by :func:`muse.core.ignore.is_ignored`.
145 This ensures that OS artifacts, build outputs, and domain-specific
146 scratch files are never committed, regardless of which plugin is active.
147 See ``docs/reference/museignore.md`` for the full format reference.
148 """
149 ...
150
151 def diff(self, base: StateSnapshot, target: StateSnapshot) -> StateDelta:
152 """Compute the minimal delta between two snapshots.
153
154 Returns a ``DeltaManifest`` listing which paths were added, removed,
155 or modified. Muse stores deltas alongside commits so that ``muse show``
156 can display a human-readable summary without reloading full blobs.
157 """
158 ...
159
160 def merge(
161 self,
162 base: StateSnapshot,
163 left: StateSnapshot,
164 right: StateSnapshot,
165 *,
166 repo_root: pathlib.Path | None = None,
167 ) -> MergeResult:
168 """Three-way merge two divergent state lines against a common base.
169
170 ``base`` is the common ancestor (merge base). ``left`` and ``right``
171 are the two divergent snapshots. Returns a ``MergeResult`` with the
172 reconciled snapshot and any unresolvable conflicts.
173
174 **``.museattributes`` and multidimensional merge contract** — when
175 *repo_root* is provided, domain plugin implementations should:
176
177 1. Load ``.museattributes`` via
178 :func:`muse.core.attributes.load_attributes`.
179 2. For each conflicting path, call
180 :func:`muse.core.attributes.resolve_strategy` with the relevant
181 dimension name (or ``"*"`` for file-level resolution).
182 3. Apply the returned strategy:
183
184 - ``"ours"`` — take the *left* version; remove from conflict list.
185 - ``"theirs"`` — take the *right* version; remove from conflict list.
186 - ``"manual"`` — force into conflict list even if the engine would
187 auto-resolve.
188 - ``"auto"`` / ``"union"`` — defer to the engine's default logic.
189
190 4. For domain formats that support true multidimensional content (e.g.
191 MIDI: melodic, rhythmic, harmonic, dynamic, structural), attempt
192 sub-file dimension merge before falling back to a file-level conflict.
193
194 Record every override in :attr:`MergeResult.applied_strategies` and
195 per-dimension detail in :attr:`MergeResult.dimension_reports`. See
196 ``docs/reference/muse-attributes.md`` for the full format reference.
197 """
198 ...
199
200 def drift(
201 self,
202 committed: StateSnapshot,
203 live: LiveState,
204 ) -> DriftReport:
205 """Compare committed state against current live state.
206
207 Used by ``muse status`` to detect uncommitted changes. Returns a
208 ``DriftReport`` describing whether the live state has diverged from
209 the last committed snapshot and, if so, by how much.
210 """
211 ...
212
213 def apply(self, delta: StateDelta, live_state: LiveState) -> LiveState:
214 """Apply a delta to produce a new live state.
215
216 Used by ``muse checkout`` to reconstruct a historical state. Applies
217 ``delta`` on top of ``live_state`` and returns the resulting state.
218 """
219 ...