gabriel / muse public
domain.py python
334 lines 12.2 KB
d7054e63 feat(phase-1): typed delta algebra — replace DeltaManifest with Structu… Gabriel Cardona <gabriel@tellurstori.com> 6d 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 Phase 1 — Typed Delta Algebra
12 ------------------------------
13 ``StateDelta`` is now a ``StructuredDelta`` carrying a typed operation list
14 rather than the old opaque ``{added, removed, modified}`` path lists. Each
15 operation knows its kind (insert / delete / move / replace / patch), the
16 address it touched, and a content-addressed ID for the before/after content.
17
18 This replaces ``DeltaManifest`` entirely. Plugins that previously returned
19 ``DeltaManifest`` must now return ``StructuredDelta``.
20 """
21 from __future__ import annotations
22
23 import pathlib
24 from dataclasses import dataclass, field
25 from typing import Literal, Protocol, TypedDict, runtime_checkable
26
27
28 # ---------------------------------------------------------------------------
29 # Snapshot types (unchanged from pre-Phase-1)
30 # ---------------------------------------------------------------------------
31
32
33 class SnapshotManifest(TypedDict):
34 """Content-addressed snapshot of domain state.
35
36 ``files`` maps workspace-relative POSIX paths to their SHA-256 content
37 digests. ``domain`` identifies which plugin produced this snapshot.
38 """
39
40 files: dict[str, str]
41 domain: str
42
43
44 # ---------------------------------------------------------------------------
45 # Typed delta algebra — Phase 1
46 # ---------------------------------------------------------------------------
47
48 #: A domain-specific address identifying a location within the state graph.
49 #: For file-level ops this is a workspace-relative POSIX path.
50 #: For sub-file ops this is a domain-specific coordinate (e.g. "note:42").
51 DomainAddress = str
52
53
54 class InsertOp(TypedDict):
55 """An element was inserted into a collection.
56
57 For ordered sequences ``position`` is the integer index at which the
58 element was inserted. For unordered sets ``position`` is ``None``.
59 ``content_id`` is the SHA-256 of the inserted content — either a blob
60 already in the object store (for file-level ops) or a deterministic hash
61 of the element's canonical serialisation (for sub-file ops).
62 """
63
64 op: Literal["insert"]
65 address: DomainAddress
66 position: int | None
67 content_id: str
68 content_summary: str
69
70
71 class DeleteOp(TypedDict):
72 """An element was removed from a collection."""
73
74 op: Literal["delete"]
75 address: DomainAddress
76 position: int | None
77 content_id: str
78 content_summary: str
79
80
81 class MoveOp(TypedDict):
82 """An element was repositioned within an ordered sequence."""
83
84 op: Literal["move"]
85 address: DomainAddress
86 from_position: int
87 to_position: int
88 content_id: str
89
90
91 class ReplaceOp(TypedDict):
92 """An element's value changed (atomic, leaf-level replacement)."""
93
94 op: Literal["replace"]
95 address: DomainAddress
96 position: int | None
97 old_content_id: str
98 new_content_id: str
99 old_summary: str
100 new_summary: str
101
102
103 #: The four non-recursive (leaf) operation types.
104 LeafDomainOp = InsertOp | DeleteOp | MoveOp | ReplaceOp
105
106
107 class PatchOp(TypedDict):
108 """A container element was internally modified.
109
110 ``address`` names the container (e.g. a file path). ``child_ops`` lists
111 the sub-element changes inside that container. In Phase 1 these are always
112 leaf ops. Phase 3 will introduce true recursion via a nested
113 ``StructuredDelta`` when the operation-level merge engine requires it.
114
115 ``child_domain`` identifies the sub-element domain (e.g. ``"midi_notes"``
116 for note-level ops inside a ``.mid`` file). ``child_summary`` is a
117 human-readable description of the child changes for ``muse show``.
118 """
119
120 op: Literal["patch"]
121 address: DomainAddress
122 child_ops: list[DomainOp]
123 child_domain: str
124 child_summary: str
125
126
127 #: Union of all operation types — the atoms of a ``StructuredDelta``.
128 DomainOp = LeafDomainOp | PatchOp
129
130
131 class StructuredDelta(TypedDict):
132 """Rich, composable delta between two domain snapshots.
133
134 ``ops`` is an ordered list of operations that transforms ``base`` into
135 ``target`` when applied in sequence. The core engine stores this alongside
136 commit records so that ``muse show`` and ``muse diff`` can display it
137 without reloading full blobs.
138
139 ``summary`` is a precomputed human-readable string — for example
140 ``"3 notes added, 1 note removed"``. Plugins compute it because only they
141 understand their domain semantics.
142 """
143
144 domain: str
145 ops: list[DomainOp]
146 summary: str
147
148
149 # ---------------------------------------------------------------------------
150 # Type aliases used in the protocol signatures
151 # ---------------------------------------------------------------------------
152
153 #: Live state is either an already-snapshotted manifest dict or a workdir path.
154 #: The music plugin accepts both: a Path (for CLI commit/status) and a
155 #: SnapshotManifest dict (for in-memory merge and diff operations).
156 LiveState = SnapshotManifest | pathlib.Path
157
158 #: A content-addressed, immutable snapshot of state at a point in time.
159 StateSnapshot = SnapshotManifest
160
161 #: The minimal change between two snapshots — a list of typed domain operations.
162 StateDelta = StructuredDelta
163
164
165 # ---------------------------------------------------------------------------
166 # Merge and drift result types
167 # ---------------------------------------------------------------------------
168
169
170 @dataclass
171 class MergeResult:
172 """Outcome of a three-way merge between two divergent state lines.
173
174 ``merged`` is the reconciled snapshot. ``conflicts`` is a list of
175 workspace-relative file paths that could not be auto-merged and require
176 manual resolution. An empty ``conflicts`` list means the merge was clean.
177 The CLI is responsible for formatting user-facing messages from these paths.
178
179 ``applied_strategies`` maps each path where a ``.museattributes`` rule
180 overrode the default conflict behaviour to the strategy that was applied.
181
182 ``dimension_reports`` maps conflicting paths to their per-dimension
183 resolution detail.
184
185 ``op_log`` is the ordered list of ``DomainOp`` entries applied to produce
186 the merged snapshot. Empty for file-level merges; populated by plugins
187 that implement operation-level merge (Phase 3).
188 """
189
190 merged: StateSnapshot
191 conflicts: list[str] = field(default_factory=list)
192 applied_strategies: dict[str, str] = field(default_factory=dict)
193 dimension_reports: dict[str, dict[str, str]] = field(default_factory=dict)
194 op_log: list[DomainOp] = field(default_factory=list)
195
196 @property
197 def is_clean(self) -> bool:
198 return len(self.conflicts) == 0
199
200
201 @dataclass
202 class DriftReport:
203 """Gap between committed state and current live state.
204
205 ``has_drift`` is ``True`` when the live state differs from the committed
206 snapshot. ``summary`` is a human-readable description of what changed.
207 ``delta`` is the machine-readable structured delta for programmatic consumers.
208 """
209
210 has_drift: bool
211 summary: str = ""
212 delta: StateDelta = field(default_factory=lambda: StructuredDelta(
213 domain="", ops=[], summary="working tree clean",
214 ))
215
216
217 # ---------------------------------------------------------------------------
218 # The plugin protocol
219 # ---------------------------------------------------------------------------
220
221
222 @runtime_checkable
223 class MuseDomainPlugin(Protocol):
224 """The five interfaces a domain plugin must implement.
225
226 Muse provides everything else: the DAG, branching, checkout, lineage
227 walking, ASCII log graph, and merge base finder. Implement these five
228 methods and your domain gets the full Muse VCS for free.
229
230 Music is the reference implementation (``muse.plugins.music``).
231 """
232
233 def snapshot(self, live_state: LiveState) -> StateSnapshot:
234 """Capture current live state as a serialisable, hashable snapshot.
235
236 The returned ``SnapshotManifest`` must be JSON-serialisable. Muse will
237 compute a SHA-256 content address from the canonical JSON form and
238 store the snapshot as a blob in ``.muse/objects/``.
239
240 **``.museignore`` contract** — when *live_state* is a
241 ``pathlib.Path`` (the ``muse-work/`` directory), domain plugin
242 implementations **must** honour ``.museignore`` by calling
243 :func:`muse.core.ignore.load_patterns` on the repository root and
244 filtering out paths matched by :func:`muse.core.ignore.is_ignored`.
245 """
246 ...
247
248 def diff(
249 self,
250 base: StateSnapshot,
251 target: StateSnapshot,
252 *,
253 repo_root: pathlib.Path | None = None,
254 ) -> StateDelta:
255 """Compute the structured delta between two snapshots.
256
257 Returns a ``StructuredDelta`` where ``ops`` is a minimal list of
258 typed operations that transforms ``base`` into ``target``. Plugins
259 should:
260
261 1. Compute ops at the finest granularity they can interpret.
262 2. Assign meaningful ``content_summary`` strings to each op.
263 3. When ``repo_root`` is provided, load sub-file content from the
264 object store and produce ``PatchOp`` entries with note/element-level
265 ``child_ops`` instead of coarse ``ReplaceOp`` entries.
266 4. Compute a human-readable ``summary`` across all ops.
267
268 The core engine stores this delta alongside the commit record so that
269 ``muse show`` and ``muse diff`` can display it without reloading blobs.
270 """
271 ...
272
273 def merge(
274 self,
275 base: StateSnapshot,
276 left: StateSnapshot,
277 right: StateSnapshot,
278 *,
279 repo_root: pathlib.Path | None = None,
280 ) -> MergeResult:
281 """Three-way merge two divergent state lines against a common base.
282
283 ``base`` is the common ancestor (merge base). ``left`` and ``right``
284 are the two divergent snapshots. Returns a ``MergeResult`` with the
285 reconciled snapshot and any unresolvable conflicts.
286
287 **``.museattributes`` and multidimensional merge contract** — when
288 *repo_root* is provided, domain plugin implementations should:
289
290 1. Load ``.museattributes`` via
291 :func:`muse.core.attributes.load_attributes`.
292 2. For each conflicting path, call
293 :func:`muse.core.attributes.resolve_strategy` with the relevant
294 dimension name (or ``"*"`` for file-level resolution).
295 3. Apply the returned strategy:
296
297 - ``"ours"`` — take the *left* version; remove from conflict list.
298 - ``"theirs"`` — take the *right* version; remove from conflict list.
299 - ``"manual"`` — force into conflict list even if the engine would
300 auto-resolve.
301 - ``"auto"`` / ``"union"`` — defer to the engine's default logic.
302
303 4. For domain formats that support true multidimensional content (e.g.
304 MIDI: melodic, rhythmic, harmonic, dynamic, structural), attempt
305 sub-file dimension merge before falling back to a file-level conflict.
306 """
307 ...
308
309 def drift(
310 self,
311 committed: StateSnapshot,
312 live: LiveState,
313 ) -> DriftReport:
314 """Compare committed state against current live state.
315
316 Used by ``muse status`` to detect uncommitted changes. Returns a
317 ``DriftReport`` describing whether the live state has diverged from
318 the last committed snapshot and, if so, by how much.
319 """
320 ...
321
322 def apply(self, delta: StateDelta, live_state: LiveState) -> LiveState:
323 """Apply a delta to produce a new live state.
324
325 Used by ``muse checkout`` to reconstruct a historical state. Applies
326 ``delta`` on top of ``live_state`` and returns the resulting state.
327
328 For ``InsertOp`` and ``ReplaceOp``, the new content is identified by
329 ``content_id`` (a SHA-256 hash). When ``live_state`` is a
330 ``pathlib.Path``, the plugin reads the content from the object store.
331 When ``live_state`` is a ``SnapshotManifest``, only ``DeleteOp`` and
332 ``ReplaceOp`` at the file level can be applied in-memory.
333 """
334 ...