domain.py
python
| 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 | |
| 78 | merged: StateSnapshot |
| 79 | conflicts: list[str] = field(default_factory=list) |
| 80 | |
| 81 | @property |
| 82 | def is_clean(self) -> bool: |
| 83 | return len(self.conflicts) == 0 |
| 84 | |
| 85 | |
| 86 | @dataclass |
| 87 | class DriftReport: |
| 88 | """Gap between committed state and current live state. |
| 89 | |
| 90 | ``has_drift`` is ``True`` when the live state differs from the committed |
| 91 | snapshot. ``summary`` is a human-readable description of what changed. |
| 92 | ``delta`` is the machine-readable diff for programmatic consumers. |
| 93 | """ |
| 94 | |
| 95 | has_drift: bool |
| 96 | summary: str = "" |
| 97 | delta: StateDelta = field(default_factory=lambda: DeltaManifest( |
| 98 | domain="", added=[], removed=[], modified=[], |
| 99 | )) |
| 100 | |
| 101 | |
| 102 | # --------------------------------------------------------------------------- |
| 103 | # The plugin protocol |
| 104 | # --------------------------------------------------------------------------- |
| 105 | |
| 106 | |
| 107 | @runtime_checkable |
| 108 | class MuseDomainPlugin(Protocol): |
| 109 | """The five interfaces a domain plugin must implement. |
| 110 | |
| 111 | Muse provides everything else: the DAG, branching, checkout, lineage |
| 112 | walking, ASCII log graph, and merge base finder. Implement these five |
| 113 | methods and your domain gets the full Muse VCS for free. |
| 114 | |
| 115 | Music is the reference implementation (``muse.plugins.music``). |
| 116 | """ |
| 117 | |
| 118 | def snapshot(self, live_state: LiveState) -> StateSnapshot: |
| 119 | """Capture current live state as a serialisable, hashable snapshot. |
| 120 | |
| 121 | The returned ``SnapshotManifest`` must be JSON-serialisable. Muse will |
| 122 | compute a SHA-256 content address from the canonical JSON form and |
| 123 | store the snapshot as a blob in ``.muse/objects/``. |
| 124 | """ |
| 125 | ... |
| 126 | |
| 127 | def diff(self, base: StateSnapshot, target: StateSnapshot) -> StateDelta: |
| 128 | """Compute the minimal delta between two snapshots. |
| 129 | |
| 130 | Returns a ``DeltaManifest`` listing which paths were added, removed, |
| 131 | or modified. Muse stores deltas alongside commits so that ``muse show`` |
| 132 | can display a human-readable summary without reloading full blobs. |
| 133 | """ |
| 134 | ... |
| 135 | |
| 136 | def merge( |
| 137 | self, |
| 138 | base: StateSnapshot, |
| 139 | left: StateSnapshot, |
| 140 | right: StateSnapshot, |
| 141 | ) -> MergeResult: |
| 142 | """Three-way merge two divergent state lines against a common base. |
| 143 | |
| 144 | ``base`` is the common ancestor (merge base). ``left`` and ``right`` |
| 145 | are the two divergent snapshots. Returns a ``MergeResult`` with the |
| 146 | reconciled snapshot and any unresolvable conflicts. |
| 147 | """ |
| 148 | ... |
| 149 | |
| 150 | def drift( |
| 151 | self, |
| 152 | committed: StateSnapshot, |
| 153 | live: LiveState, |
| 154 | ) -> DriftReport: |
| 155 | """Compare committed state against current live state. |
| 156 | |
| 157 | Used by ``muse status`` to detect uncommitted changes. Returns a |
| 158 | ``DriftReport`` describing whether the live state has diverged from |
| 159 | the last committed snapshot and, if so, by how much. |
| 160 | """ |
| 161 | ... |
| 162 | |
| 163 | def apply(self, delta: StateDelta, live_state: LiveState) -> LiveState: |
| 164 | """Apply a delta to produce a new live state. |
| 165 | |
| 166 | Used by ``muse checkout`` to reconstruct a historical state. Applies |
| 167 | ``delta`` on top of ``live_state`` and returns the resulting state. |
| 168 | """ |
| 169 | ... |