cgcardona / muse public
architecture-plan.md markdown
1391 lines 58.5 KB
bb7b9661 feat(demo): midi-demo UX overhaul + track artifacts in git (#110) Gabriel Cardona <cgcardona@gmail.com> 19h ago
1 # Muse — Architecture Plan: From File-Hash MVP to Universal Multidimensional VCS
2
3 > **Status:** Working document — pre-implementation spec.
4 > No backward compatibility constraints. We own every line.
5
6 ---
7
8 ## Table of Contents
9
10 1. [Honest Assessment of Current State](#1-honest-assessment-of-current-state)
11 2. [North Star: What We're Building Toward](#2-north-star-what-were-building-toward)
12 3. [Phase 1 — Typed Delta Algebra](#3-phase-1--typed-delta-algebra)
13 4. [Phase 2 — Domain Schema & Diff Algorithm Library](#4-phase-2--domain-schema--diff-algorithm-library)
14 5. [Phase 3 — Operation-Level Merge Engine](#5-phase-3--operation-level-merge-engine)
15 6. [Phase 4 — CRDT Semantics for Convergent Multi-Agent Writes](#6-phase-4--crdt-semantics-for-convergent-multi-agent-writes)
16 7. [Cross-Cutting Concerns](#7-cross-cutting-concerns)
17 8. [Test Strategy](#8-test-strategy)
18 9. [Implementation Order and Dependencies](#9-implementation-order-and-dependencies)
19
20 ---
21
22 ## 1. Honest Assessment of Current State
23
24 ### What is good and must be preserved
25
26 - **Content-addressed object store** — SHA-256 blobs in `.muse/objects/`. This is correct at every scale. Git proved it. Keep it forever.
27 - **Plugin protocol boundary** — `MuseDomainPlugin` is the right abstraction. Core engine is domain-agnostic. This must remain true through every phase.
28 - **BFS LCA merge-base finder** — mathematically correct for DAG commit graphs.
29 - **File-level three-way merge** — correct for the current granularity.
30 - **`.museattributes` strategy system** — the right place for declarative per-path merge policy.
31
32 ### What is genuinely limited
33
34 The entire system operates at a single fixed level of abstraction: **the file-path level**. The only thing the core engine ever asks about a domain is:
35
36 ```
37 {added: [path, ...], removed: [path, ...], modified: [path, ...]}
38 ```
39
40 `modified` is completely opaque. The engine knows *that* a file changed (SHA-256 differs), but not *what* changed inside it, *where*, *how*, or whether the change commutes with the other branch's changes.
41
42 **Consequences of this ceiling:**
43
44 | Scenario | Current behavior | Correct behavior |
45 |---|---|---|
46 | Insert note at bar 12 on branch A; insert note at bar 45 on branch B | File-level conflict | Auto-merge: non-overlapping positions |
47 | Insert nucleotide at position 1000 on branch A; insert at position 5000 on branch B | File-level conflict | Auto-merge |
48 | Two agents edit different nodes in the same scene graph | File-level conflict | Auto-merge |
49 | `muse show <commit>` | "tracks/drums.mid modified" | "bar 12: C4 quarter inserted; velocity 80→90" |
50 | Every new domain plugin | Must implement its own merge engine | Gets LCS/tree-edit/numerical for free from core |
51
52 The MIDI plugin works around this by implementing MIDI dimension-merge inside the plugin. But that means every new domain has to re-invent sub-file merge from scratch, in isolation, with no shared vocabulary or algorithm library. At thousands of domains and millions of agents, that's impossible.
53
54 ---
55
56 ## 2. North Star: What We're Building Toward
57
58 A universal multidimensional VCS where:
59
60 1. **Any domain** can declare its data structure schema (sequence, tree, graph, tensor, map, or composites) and immediately get the right diff and merge algorithm for free — without implementing one.
61
62 2. **Diffs are meaningful**, not just path lists. `muse show` displays "note C4 inserted at beat 3.5" or "gene BRCA1 exon 7 deleted" or "mesh vertex 42 moved (3.2, 0.0, -1.1)".
63
64 3. **Conflicts are detected at operation granularity**, not file granularity. Two agents editing non-overlapping parts of the same sequence never conflict.
65
66 4. **Millions of agents can converge** without explicit conflict resolution, by opting into CRDT semantics where merge is a mathematical join on a lattice.
67
68 5. **The core engine never changes** when new domains are added. Every improvement to the diff algorithm library automatically benefits all existing plugins.
69
70 ---
71
72 ## 3. Phase 1 — Typed Delta Algebra
73
74 **Goal:** Replace the opaque `{added, removed, modified}` delta with a rich, composable operation vocabulary. Plugins return structured deltas. The core engine stores them and displays them. No core engine conflict logic changes yet — that comes in Phase 3.
75
76 **Estimated scope:** 2–3 weeks of implementation, 1 week of test writing.
77
78 ### 3.1 Motivation
79
80 Today `DeltaManifest.modified` is a `list[str]` of paths. It tells you nothing about what happened inside those files. This makes `muse show`, `muse diff`, and any programmatic consumer completely blind to intra-file changes.
81
82 Phase 1 fixes this without touching the merge engine. The protocol gets richer return types; the core engine stores and forwards them opaquely; plugins can now express sub-file changes precisely.
83
84 ### 3.2 New Type System in `muse/domain.py`
85
86 Replace `DeltaManifest` with a composable operation-tree type system:
87
88 ```python
89 # ---------------------------------------------------------------------------
90 # Atomic position types
91 # ---------------------------------------------------------------------------
92
93 # A DomainAddress is a path within a domain's object graph.
94 # - For file-level ops: a POSIX workspace path ("tracks/drums.mid")
95 # - For sub-file ops: a JSON-pointer fragment within that file ("/notes/42")
96 # - Plugins define what addresses mean in their domain.
97 DomainAddress = str
98
99 # ---------------------------------------------------------------------------
100 # Atomic operation types
101 # ---------------------------------------------------------------------------
102
103 class InsertOp(TypedDict):
104 """An element was inserted into an ordered or unordered collection."""
105 op: Literal["insert"]
106 address: DomainAddress # where in the structure
107 position: int | None # index for ordered sequences; None for sets
108 content_id: str # SHA-256 of inserted content (stored in object store)
109 content_summary: str # human-readable description for display
110
111 class DeleteOp(TypedDict):
112 """An element was removed."""
113 op: Literal["delete"]
114 address: DomainAddress
115 position: int | None
116 content_id: str # SHA-256 of removed content
117 content_summary: str
118
119 class MoveOp(TypedDict):
120 """An element was repositioned within an ordered sequence."""
121 op: Literal["move"]
122 address: DomainAddress
123 from_position: int
124 to_position: int
125 content_id: str
126
127 class ReplaceOp(TypedDict):
128 """An element's value changed (atomic, leaf-level)."""
129 op: Literal["replace"]
130 address: DomainAddress
131 position: int | None
132 old_content_id: str
133 new_content_id: str
134 old_summary: str
135 new_summary: str
136
137 class PatchOp(TypedDict):
138 """A nested structure was modified; carries a child StructuredDelta."""
139 op: Literal["patch"]
140 address: DomainAddress # the container being patched
141 child_delta: StructuredDelta # recursive — describes what changed inside
142
143 # The union of all operation types
144 DomainOp = InsertOp | DeleteOp | MoveOp | ReplaceOp | PatchOp
145
146 # ---------------------------------------------------------------------------
147 # The new StateDelta
148 # ---------------------------------------------------------------------------
149
150 class StructuredDelta(TypedDict):
151 """Rich, composable delta between two domain snapshots.
152
153 ``ops`` is an ordered list of operations that transforms ``base`` into
154 ``target`` when applied in sequence. The core engine treats this as an
155 opaque blob for storage and display. The merge engine in Phase 3 will
156 reason over it for commutativity.
157
158 ``summary`` is a precomputed human-readable string for ``muse show``.
159 Plugins compute this because only they understand their domain semantics.
160 """
161 domain: str
162 ops: list[DomainOp]
163 summary: str # "3 notes added, 1 bar restructured"
164
165 # StateDelta is now StructuredDelta. DeltaManifest is gone.
166 StateDelta = StructuredDelta
167 ```
168
169 **Key design decisions:**
170
171 - `content_id` references the object store (`.muse/objects/`). This means inserted/deleted/replaced sub-elements are content-addressed and retrievable. No bloat in the delta itself.
172 - `content_summary` is plugin-computed human-readable text. The core engine uses it verbatim in `muse show`. Plugins are responsible for making it meaningful.
173 - `PatchOp` is recursive. A MIDI file modification is a `PatchOp` whose `child_delta` contains `InsertOp`/`DeleteOp`/`MoveOp` on individual notes. A genomics sequence modification is a `PatchOp` whose child delta contains nucleotide-level ops.
174 - `position: int | None` — `None` signals an unordered collection (set semantics). The merge engine in Phase 3 uses this to determine commutativity: two inserts into the same unordered collection always commute; two inserts at the same position in an ordered sequence may conflict.
175
176 ### 3.3 Updated Plugin Protocol
177
178 The `diff` method signature stays the same (`base: StateSnapshot, target: StateSnapshot) -> StateDelta`), but `StateDelta` is now `StructuredDelta`. The protocol docstring must document the expectation:
179
180 ```python
181 def diff(self, base: StateSnapshot, target: StateSnapshot) -> StateDelta:
182 """Compute the structured delta between two snapshots.
183
184 Returns a ``StructuredDelta`` where ``ops`` is a minimal list of
185 operations that transforms ``base`` into ``target``. Plugins should:
186
187 1. Compute ops at the finest granularity they can interpret.
188 2. Assign meaningful ``content_summary`` strings to each op.
189 3. Store any new sub-element content in the object store if ``repo_root``
190 is available; otherwise use deterministic synthetic IDs.
191 4. Compute a human-readable ``summary`` across all ops.
192
193 The core engine stores this delta alongside the commit record so that
194 ``muse show`` and ``muse diff`` can display it without reloading blobs.
195 """
196 ...
197 ```
198
199 `apply` also changes — it now receives a `StructuredDelta` and must apply its `ops` list:
200
201 ```python
202 def apply(self, delta: StateDelta, live_state: LiveState) -> LiveState:
203 """Apply a structured delta to produce a new live state.
204
205 Plugins must implement application of all four op types. For plugins
206 where full in-memory application is impractical (e.g. large binary
207 files), ``live_state`` should be a ``pathlib.Path`` and the plugin
208 should apply ops to disk files directly.
209 """
210 ...
211 ```
212
213 ### 3.4 Updated `DriftReport`
214
215 `DriftReport.delta` is now a `StructuredDelta`. This means `muse status` can display the rich summary:
216
217 ```python
218 @dataclass
219 class DriftReport:
220 has_drift: bool
221 summary: str = ""
222 delta: StateDelta = field(default_factory=lambda: StructuredDelta(
223 domain="", ops=[], summary="working tree clean",
224 ))
225 ```
226
227 ### 3.5 Updated `MergeResult`
228
229 `MergeResult` adds an `op_log` — the ordered list of operations that produced the merged snapshot, useful for audit and replay:
230
231 ```python
232 @dataclass
233 class MergeResult:
234 merged: StateSnapshot
235 conflicts: list[str] = field(default_factory=list)
236 applied_strategies: dict[str, str] = field(default_factory=dict)
237 dimension_reports: dict[str, dict[str, str]] = field(default_factory=dict)
238 op_log: list[DomainOp] = field(default_factory=list) # NEW
239
240 @property
241 def is_clean(self) -> bool:
242 return len(self.conflicts) == 0
243 ```
244
245 ### 3.6 Updated Music Plugin
246
247 The MIDI plugin's `diff` method must now return a `StructuredDelta`. File-level ops are the minimum bar. The MIDI dimension merge already has the machinery to go deeper — it should produce `PatchOp` entries for modified `.mid` files:
248
249 ```python
250 def diff(self, base: StateSnapshot, target: StateSnapshot) -> StateDelta:
251 base_files = base["files"]
252 target_files = target["files"]
253 base_paths = set(base_files)
254 target_paths = set(target_files)
255
256 ops: list[DomainOp] = []
257
258 for path in sorted(target_paths - base_paths):
259 ops.append(InsertOp(
260 op="insert",
261 address=path,
262 position=None, # file collection is unordered
263 content_id=target_files[path],
264 content_summary=f"new file: {path}",
265 ))
266
267 for path in sorted(base_paths - target_paths):
268 ops.append(DeleteOp(
269 op="delete",
270 address=path,
271 position=None,
272 content_id=base_files[path],
273 content_summary=f"deleted: {path}",
274 ))
275
276 for path in sorted(p for p in base_paths & target_paths
277 if base_files[p] != target_files[p]):
278 if path.lower().endswith(".mid"):
279 # Attempt deep MIDI diff → PatchOp with note-level child ops
280 child_delta = _diff_midi_deep(base_files[path], target_files[path])
281 if child_delta is not None:
282 ops.append(PatchOp(
283 op="patch",
284 address=path,
285 child_delta=child_delta,
286 ))
287 continue
288 # Fallback: atomic replace
289 ops.append(ReplaceOp(
290 op="replace",
291 address=path,
292 position=None,
293 old_content_id=base_files[path],
294 new_content_id=target_files[path],
295 old_summary=f"{path} (prev)",
296 new_summary=f"{path} (new)",
297 ))
298
299 summary = _summarise_ops(ops)
300 return StructuredDelta(domain=_DOMAIN_TAG, ops=ops, summary=summary)
301 ```
302
303 `_diff_midi_deep` calls into a new `midi_diff.py` module (sibling of `midi_merge.py`) that runs the Myers LCS algorithm on the MIDI note sequence and returns a `StructuredDelta` with note-level `InsertOp`/`DeleteOp`/`MoveOp` entries.
304
305 ### 3.7 Serialisation Contract
306
307 `StructuredDelta` must remain JSON-serialisable (the core engine stores it in commit records). The recursive `PatchOp.child_delta` is also a `StructuredDelta`, so it serialises naturally. All `content_id` references are SHA-256 hex strings pointing to objects already in the store — no embedded binary.
308
309 The commit record format (`CommitRecord`) must add a `structured_delta` field alongside (or replacing) the existing delta storage. This is a commit format change — acceptable since we have no backwards compat requirement.
310
311 ### 3.8 Phase 1 Test Cases
312
313 **New test file: `tests/test_structured_delta.py`**
314
315 ```
316 test_insert_op_round_trips_json
317 test_delete_op_round_trips_json
318 test_move_op_round_trips_json
319 test_replace_op_round_trips_json
320 test_patch_op_with_child_delta_round_trips_json
321 test_structured_delta_satisfies_state_delta_type
322 test_midi_plugin_diff_returns_structured_delta
323 test_midi_plugin_diff_file_add_produces_insert_op
324 test_midi_plugin_diff_file_remove_produces_delete_op
325 test_midi_plugin_diff_file_modify_produces_replace_op
326 test_midi_plugin_diff_midi_modify_produces_patch_op_with_child_ops
327 test_drift_report_delta_is_structured_delta
328 test_muse_show_displays_structured_summary
329 test_muse_diff_displays_per_op_lines
330 ```
331
332 **New test file: `tests/test_midi_diff.py`**
333
334 ```
335 test_midi_diff_empty_to_single_note_is_one_insert
336 test_midi_diff_single_note_to_empty_is_one_delete
337 test_midi_diff_note_velocity_change_is_replace
338 test_midi_diff_note_inserted_in_middle_identified_correctly
339 test_midi_diff_note_transposition_identified_as_replace
340 test_midi_diff_no_change_returns_empty_ops
341 test_midi_diff_summary_string_is_human_readable
342 ```
343
344 ### 3.9 Files Changed in Phase 1
345
346 | File | Change |
347 |---|---|
348 | `muse/domain.py` | Replace `DeltaManifest`/`StateDelta` with `DomainOp` union + `StructuredDelta`. Update `DriftReport`, `MergeResult`. |
349 | `muse/core/store.py` | `CommitRecord` gains `structured_delta: StructuredDelta \| None` field. |
350 | `muse/plugins/music/plugin.py` | `diff()` returns `StructuredDelta`. `apply()` handles `StructuredDelta`. |
351 | `muse/plugins/music/midi_diff.py` | **New.** Myers LCS on MIDI note sequences → `StructuredDelta`. |
352 | `muse/cli/commands/show.py` | Display `structured_delta.summary` and per-op lines. |
353 | `muse/cli/commands/diff.py` | Display structured diff output. |
354 | `tests/test_structured_delta.py` | **New.** All Phase 1 tests. |
355 | `tests/test_midi_diff.py` | **New.** MIDI diff algorithm tests. |
356 | `tests/test_midi_plugin.py` | Update to assert `StructuredDelta` return type. |
357
358 ---
359
360 ## 4. Phase 2 — Domain Schema & Diff Algorithm Library
361
362 **Goal:** Plugins declare their data structure schema. The core engine dispatches to the right diff algorithm automatically. New plugin authors get LCS, tree-edit, and numerical diff for free — no algorithm implementation required.
363
364 **Estimated scope:** 3–4 weeks of implementation, 1 week of test writing.
365
366 ### 4.1 Motivation
367
368 After Phase 1, every plugin must still implement its own diff algorithm. A genomics plugin author has to implement Myers LCS to get note-level (nucleotide-level) diffs. A 3D plugin author has to implement tree-edit distance. This is a PhD-level prerequisite for every new domain.
369
370 Phase 2 inverts this: the plugin *declares* its data structure, and the core engine drives the right algorithm. The genomics plugin says "my data is an ordered sequence of nucleotides, use LCS" and gets exactly that — for free.
371
372 ### 4.2 Domain Schema Types
373
374 **New file: `muse/core/schema.py`**
375
376 ```python
377 """Domain schema declaration types.
378
379 A plugin implements ``schema()`` returning a ``DomainSchema`` to declare
380 the structural shape of its data. The core engine uses this declaration
381 to drive the correct diff algorithm, validate delta types, and offer
382 informed merge conflict messages.
383 """
384 from __future__ import annotations
385 from typing import Literal, TypedDict
386
387
388 # ---------------------------------------------------------------------------
389 # Primitive element schemas
390 # ---------------------------------------------------------------------------
391
392 class SequenceSchema(TypedDict):
393 """Ordered sequence of homogeneous elements (LCS-diffable)."""
394 kind: Literal["sequence"]
395 element_type: str # e.g. "note", "nucleotide", "frame", "voxel"
396 identity: Literal["by_id", "by_position", "by_content"]
397 diff_algorithm: Literal["lcs", "myers", "patience"]
398 # Optional: alphabet constraint for validation
399 alphabet: list[str] | None
400
401 class TreeSchema(TypedDict):
402 """Hierarchical tree structure (tree-edit-diffable)."""
403 kind: Literal["tree"]
404 node_type: str # e.g. "scene_node", "xml_element", "ast_node"
405 diff_algorithm: Literal["zhang_shasha", "gumtree"]
406
407 class TensorSchema(TypedDict):
408 """N-dimensional numerical array (sparse-numerical-diffable)."""
409 kind: Literal["tensor"]
410 dtype: Literal["float32", "float64", "int8", "int16", "int32", "int64"]
411 rank: int # number of dimensions
412 epsilon: float # tolerance: |a - b| < epsilon → "unchanged"
413 diff_mode: Literal["sparse", "block", "full"]
414
415 class MapSchema(TypedDict):
416 """Key-value map with known or dynamic keys."""
417 kind: Literal["map"]
418 key_type: str
419 value_schema: ElementSchema # recursive
420 identity: Literal["by_key"]
421
422 class SetSchema(TypedDict):
423 """Unordered collection of unique elements (current hash-set approach)."""
424 kind: Literal["set"]
425 element_type: str
426 identity: Literal["by_content", "by_id"]
427
428 # The union of all element schema types
429 ElementSchema = SequenceSchema | TreeSchema | TensorSchema | MapSchema | SetSchema
430
431
432 # ---------------------------------------------------------------------------
433 # Dimension spec — a named structural sub-dimension
434 # ---------------------------------------------------------------------------
435
436 class DimensionSpec(TypedDict):
437 """A named semantic sub-dimension of the domain's state.
438
439 For music: "melodic", "harmonic", "dynamic", "structural".
440 For genomics: "exons", "introns", "promoters", "metadata".
441 For 3D spatial: "geometry", "materials", "lighting", "animation".
442
443 Each dimension can use a different element schema and diff algorithm.
444 The merge engine can merge dimensions independently.
445 """
446 name: str
447 description: str
448 schema: ElementSchema
449 # Whether conflicts in this dimension block the whole file's merge
450 # or are resolved independently.
451 independent_merge: bool
452
453
454 # ---------------------------------------------------------------------------
455 # Top-level domain schema
456 # ---------------------------------------------------------------------------
457
458 class DomainSchema(TypedDict):
459 """Complete structural declaration for a domain plugin.
460
461 Returned by ``MuseDomainPlugin.schema()``. The core engine reads this
462 once at plugin registration time and uses it to:
463
464 1. Select the correct diff algorithm for each dimension.
465 2. Generate typed delta validation.
466 3. Provide informed conflict messages (citing dimension names).
467 4. Route to CRDT merge if ``merge_mode`` is ``"crdt"`` (Phase 4).
468 """
469 domain: str
470 description: str
471 # Dimensions that make up this domain's state.
472 # The core engine merges each independently when possible.
473 dimensions: list[DimensionSpec]
474 # The top-level collection of domain objects (e.g. files, sequences, nodes)
475 top_level: ElementSchema
476 # Which merge strategy to use at the top level
477 merge_mode: Literal["three_way", "crdt"] # "crdt" is Phase 4
478 # Version of the schema format itself (for future migrations)
479 schema_version: Literal[1]
480 ```
481
482 ### 4.3 Schema Method Added to Plugin Protocol
483
484 ```python
485 def schema(self) -> DomainSchema:
486 """Declare the structural schema of this domain's state.
487
488 The core engine calls this once at startup. Plugins should return a
489 stable, deterministic ``DomainSchema``. This declaration drives diff
490 algorithm selection, delta validation, and conflict messaging.
491
492 See ``muse.core.schema`` for all available element schema types.
493 """
494 ...
495 ```
496
497 ### 4.4 Example Schema Declarations
498
499 **Music plugin:**
500
501 ```python
502 def schema(self) -> DomainSchema:
503 return DomainSchema(
504 domain="midi",
505 description="MIDI and audio file versioning with note-level diff",
506 top_level=SetSchema(
507 kind="set",
508 element_type="audio_file",
509 identity="by_content",
510 ),
511 dimensions=[
512 DimensionSpec(
513 name="melodic",
514 description="Note pitches and durations over time",
515 schema=SequenceSchema(
516 kind="sequence",
517 element_type="note_event",
518 identity="by_position",
519 diff_algorithm="lcs",
520 alphabet=None,
521 ),
522 independent_merge=True,
523 ),
524 DimensionSpec(
525 name="harmonic",
526 description="Chord progressions and key signatures",
527 schema=SequenceSchema(
528 kind="sequence",
529 element_type="chord_event",
530 identity="by_position",
531 diff_algorithm="lcs",
532 alphabet=None,
533 ),
534 independent_merge=True,
535 ),
536 DimensionSpec(
537 name="dynamic",
538 description="Velocity and expression curves",
539 schema=TensorSchema(
540 kind="tensor",
541 dtype="float32",
542 rank=1,
543 epsilon=1.0, # velocities are integers 0–127; 1.0 tolerance
544 diff_mode="sparse",
545 ),
546 independent_merge=True,
547 ),
548 DimensionSpec(
549 name="structural",
550 description="Track layout, time signatures, tempo map",
551 schema=TreeSchema(
552 kind="tree",
553 node_type="track_node",
554 diff_algorithm="zhang_shasha",
555 ),
556 independent_merge=False, # structural changes block all dimensions
557 ),
558 ],
559 merge_mode="three_way",
560 schema_version=1,
561 )
562 ```
563
564 **Hypothetical genomics plugin:**
565
566 ```python
567 def schema(self) -> DomainSchema:
568 return DomainSchema(
569 domain="genomics",
570 description="DNA/RNA sequence versioning at nucleotide resolution",
571 top_level=MapSchema(
572 kind="map",
573 key_type="sequence_id", # e.g. chromosome name
574 value_schema=SequenceSchema(
575 kind="sequence",
576 element_type="nucleotide",
577 identity="by_position",
578 diff_algorithm="myers",
579 alphabet=["A", "T", "C", "G", "U", "N", "-"],
580 ),
581 identity="by_key",
582 ),
583 dimensions=[
584 DimensionSpec(
585 name="coding_regions",
586 description="Exons and coding sequence",
587 schema=SequenceSchema(
588 kind="sequence",
589 element_type="nucleotide",
590 identity="by_position",
591 diff_algorithm="myers",
592 alphabet=["A", "T", "C", "G", "N"],
593 ),
594 independent_merge=True,
595 ),
596 DimensionSpec(
597 name="regulatory",
598 description="Promoters, enhancers, splice sites",
599 schema=SetSchema(
600 kind="set",
601 element_type="regulatory_element",
602 identity="by_id",
603 ),
604 independent_merge=True,
605 ),
606 DimensionSpec(
607 name="metadata",
608 description="Annotations, quality scores, provenance",
609 schema=MapSchema(
610 kind="map",
611 key_type="annotation_key",
612 value_schema=TensorSchema(
613 kind="tensor",
614 dtype="float32",
615 rank=1,
616 epsilon=0.001,
617 diff_mode="sparse",
618 ),
619 identity="by_key",
620 ),
621 independent_merge=True,
622 ),
623 ],
624 merge_mode="three_way",
625 schema_version=1,
626 )
627 ```
628
629 These declarations are roughly 30 lines each and require zero algorithm knowledge from the plugin author. The core engine does the rest.
630
631 ### 4.5 Diff Algorithm Library
632
633 **New directory: `muse/core/diff_algorithms/`**
634
635 ```
636 muse/core/diff_algorithms/
637 __init__.py → dispatch function
638 lcs.py → Myers / patience diff for ordered sequences
639 tree_edit.py → Zhang-Shasha tree edit distance
640 numerical.py → sparse numerical diff for tensors
641 set_ops.py → hash-set algebra (current approach, extracted)
642 ```
643
644 **`muse/core/diff_algorithms/__init__.py`** — schema-driven dispatch:
645
646 ```python
647 def diff_by_schema(
648 schema: ElementSchema,
649 base: SequenceData | TreeData | TensorData | SetData | MapData,
650 target: SequenceData | TreeData | TensorData | SetData | MapData,
651 *,
652 domain: str,
653 address: str = "",
654 ) -> StructuredDelta:
655 """Dispatch to the correct diff algorithm based on ``schema.kind``."""
656 match schema["kind"]:
657 case "sequence":
658 return lcs.diff(schema, base, target, domain=domain, address=address)
659 case "tree":
660 return tree_edit.diff(schema, base, target, domain=domain, address=address)
661 case "tensor":
662 return numerical.diff(schema, base, target, domain=domain, address=address)
663 case "set":
664 return set_ops.diff(schema, base, target, domain=domain, address=address)
665 case "map":
666 return _diff_map(schema, base, target, domain=domain, address=address)
667 ```
668
669 **`muse/core/diff_algorithms/lcs.py`** — Myers diff algorithm:
670
671 The Myers diff algorithm (the same one Git uses) finds the shortest edit script between two sequences. For ordered sequences, it gives minimal inserts and deletes. For moves, a post-processing pass detects delete+insert pairs of the same element.
672
673 Key functions:
674 - `myers_ses(base: list[T], target: list[T]) -> list[EditOp]` — shortest edit script
675 - `detect_moves(inserts: list[InsertOp], deletes: list[DeleteOp]) -> list[MoveOp]` — post-process
676 - `diff(schema: SequenceSchema, base: list[T], target: list[T], ...) -> StructuredDelta`
677
678 The patience diff variant (used by some Git backends) gives better results when sequences have many repeated elements — expose it as an option.
679
680 **`muse/core/diff_algorithms/tree_edit.py`** — Zhang-Shasha:
681
682 Zhang-Shasha computes the minimum edit distance between two labeled ordered trees. Operations: relabel (→ ReplaceOp), insert, delete. The algorithm is O(n²m) where n, m are tree sizes — acceptable for domain objects up to ~10k nodes.
683
684 Key functions:
685 - `zhang_shasha(base: TreeNode, target: TreeNode) -> list[TreeEditOp]` — edit script
686 - `diff(schema: TreeSchema, base: TreeNode, target: TreeNode, ...) -> StructuredDelta`
687
688 **`muse/core/diff_algorithms/numerical.py`** — sparse tensor diff:
689
690 For numerical arrays (simulation state, velocity curves, weight matrices), exact byte comparison is wrong — floating-point drift doesn't constitute a meaningful change. The numerical diff:
691
692 1. Compares element-wise with `schema.epsilon` tolerance.
693 2. Returns `ReplaceOp` only for elements where `|base[i] - target[i]| >= epsilon`.
694 3. In `"sparse"` mode: emits one `ReplaceOp` per changed element (good for sparse changes).
695 4. In `"block"` mode: groups adjacent changes into contiguous range ops (good for dense changes).
696 5. In `"full"` mode: emits a single `ReplaceOp` for the entire array if anything changed (fallback for very large tensors where element-wise ops are too expensive).
697
698 **`muse/core/diff_algorithms/set_ops.py`** — extracted from current code:
699
700 The current hash-set algebra pulled into a pure function that returns a `StructuredDelta`. No algorithmic change — this is refactoring to put existing logic into the new common library.
701
702 ### 4.6 Plugin Registry Gains Schema Lookup
703
704 **`muse/core/plugin_registry.py`** gains a `schema_for(domain: str) -> DomainSchema | None` function. This allows the CLI and merge engine to look up a domain's schema without having a plugin instance.
705
706 ### 4.7 Phase 2 Test Cases
707
708 **New test file: `tests/test_diff_algorithms.py`**
709
710 ```
711 # LCS / Myers
712 test_lcs_empty_to_sequence_is_all_inserts
713 test_lcs_sequence_to_empty_is_all_deletes
714 test_lcs_identical_sequences_returns_no_ops
715 test_lcs_single_insert_in_middle
716 test_lcs_single_delete_in_middle
717 test_lcs_move_detected_from_delete_plus_insert
718 test_lcs_transposition_of_two_elements
719 test_lcs_patience_mode_handles_repeated_elements
720 test_lcs_produces_valid_structured_delta
721
722 # Tree edit
723 test_tree_edit_leaf_relabel_is_replace
724 test_tree_edit_node_insert
725 test_tree_edit_node_delete
726 test_tree_edit_subtree_move
727 test_tree_edit_identical_trees_returns_no_ops
728 test_tree_edit_produces_valid_structured_delta
729
730 # Numerical
731 test_numerical_within_epsilon_returns_no_ops
732 test_numerical_outside_epsilon_returns_replace
733 test_numerical_sparse_mode_one_op_per_element
734 test_numerical_block_mode_groups_adjacent
735 test_numerical_full_mode_single_op
736 test_numerical_produces_valid_structured_delta
737
738 # Set ops
739 test_set_diff_add_returns_insert
740 test_set_diff_remove_returns_delete
741 test_set_diff_no_change_returns_empty
742 test_set_diff_produces_valid_structured_delta
743
744 # Schema dispatch
745 test_dispatch_sequence_schema_calls_lcs
746 test_dispatch_tree_schema_calls_zhang_shasha
747 test_dispatch_tensor_schema_calls_numerical
748 test_dispatch_set_schema_calls_set_ops
749 test_dispatch_map_schema_recurses
750 ```
751
752 **New test file: `tests/test_domain_schema.py`**
753
754 ```
755 test_midi_plugin_schema_returns_domain_schema
756 test_midi_plugin_schema_has_four_dimensions
757 test_midi_plugin_schema_melodic_dimension_is_sequence
758 test_midi_plugin_schema_structural_dimension_is_tree
759 test_midi_plugin_schema_dynamic_dimension_is_tensor
760 test_schema_round_trips_json
761 test_schema_version_is_1
762 test_plugin_registry_schema_lookup
763 ```
764
765 ### 4.8 Files Changed in Phase 2
766
767 | File | Change |
768 |---|---|
769 | `muse/core/schema.py` | **New.** All schema `TypedDict` types. |
770 | `muse/core/diff_algorithms/__init__.py` | **New.** Schema-driven dispatch. |
771 | `muse/core/diff_algorithms/lcs.py` | **New.** Myers + patience diff. |
772 | `muse/core/diff_algorithms/tree_edit.py` | **New.** Zhang-Shasha implementation. |
773 | `muse/core/diff_algorithms/numerical.py` | **New.** Sparse/block/full tensor diff. |
774 | `muse/core/diff_algorithms/set_ops.py` | **New.** Extracted from `merge_engine.py`. |
775 | `muse/domain.py` | Add `schema()` to `MuseDomainPlugin` protocol. |
776 | `muse/core/plugin_registry.py` | Add `schema_for(domain) -> DomainSchema \| None`. |
777 | `muse/plugins/music/plugin.py` | Implement `schema()` returning full `DomainSchema`. `diff()` dispatches through `diff_by_schema`. |
778 | `tests/test_diff_algorithms.py` | **New.** |
779 | `tests/test_domain_schema.py` | **New.** |
780
781 ---
782
783 ## 5. Phase 3 — Operation-Level Merge Engine
784
785 **Goal:** The core merge engine reasons over `DomainOp` trees, not just path sets. Two operations that touch non-overlapping positions auto-merge without conflict. The commutativity rules are uniform across all domains.
786
787 **Estimated scope:** 4–6 weeks (this is the hardest phase).
788
789 ### 5.1 Motivation
790
791 After Phase 2, we can produce rich structured deltas and display them beautifully. But the merge engine still detects conflicts at file-path granularity. Two agents inserting notes at bar 12 and bar 45 respectively still produce a "conflict" even though their changes commute perfectly.
792
793 Phase 3 fixes this by making the merge engine reason over operations directly. This is operational transformation (OT) — the theory behind Google Docs' real-time collaborative editing, applied to version-controlled multidimensional state.
794
795 ### 5.2 Commutativity Rules
796
797 Two operations `A` and `B` commute (can be auto-merged) if and only if applying them in any order produces the same result. The rules are:
798
799 | Op A | Op B | Commute? | Condition |
800 |---|---|---|---|
801 | `InsertOp(pos=i)` | `InsertOp(pos=j)` | **Yes** | `i ≠ j` (different positions) |
802 | `InsertOp(pos=i)` | `InsertOp(pos=i)` | **No** | Same position — positional conflict |
803 | `InsertOp` | `DeleteOp` | **No** | Unless different subtrees |
804 | `DeleteOp(addr=A)` | `DeleteOp(addr=B)` | **Yes** | `A ≠ B` |
805 | `DeleteOp(addr=A)` | `DeleteOp(addr=A)` | **Yes** | Consensus delete — clean |
806 | `ReplaceOp(addr=A)` | `ReplaceOp(addr=B)` | **Yes** | `A ≠ B` |
807 | `ReplaceOp(addr=A)` | `ReplaceOp(addr=A)` | **No** | Same address — value conflict |
808 | `MoveOp(from=i)` | `MoveOp(from=j)` | **Yes** | `i ≠ j` |
809 | `MoveOp(from=i)` | `DeleteOp(pos=i)` | **No** | Move-delete conflict |
810 | `PatchOp(addr=A)` | `PatchOp(addr=B)` | **Yes** | `A ≠ B` — recurse on children |
811 | `PatchOp(addr=A)` | `PatchOp(addr=A)` | Recurse | Check child ops for conflicts |
812
813 For unordered collections (`position=None`), inserts always commute with other inserts. For ordered sequences, inserts at the same position do NOT commute — this is a genuine conflict that requires resolution.
814
815 ### 5.3 Operation Transformer Functions
816
817 **New file: `muse/core/op_transform.py`**
818
819 ```python
820 """Operational transformation for Muse domain operations.
821
822 Implements the commutativity rules that let the merge engine determine
823 which operation pairs can be auto-merged and which are true conflicts.
824
825 The public API is:
826
827 - ``ops_commute(a, b)`` — True if ops A and B can be applied in any order.
828 - ``transform(a, b)`` — Return a', b' such that applying a then b' = applying b then a'.
829 - ``merge_op_lists(base_ops, ours_ops, theirs_ops)`` → MergeOpsResult
830 """
831 from __future__ import annotations
832 from dataclasses import dataclass, field
833 from muse.domain import DomainOp, InsertOp, DeleteOp, MoveOp, ReplaceOp, PatchOp
834
835
836 @dataclass
837 class MergeOpsResult:
838 """Result of merging two operation lists against a common base."""
839 merged_ops: list[DomainOp] = field(default_factory=list)
840 conflict_ops: list[tuple[DomainOp, DomainOp]] = field(default_factory=list)
841
842 @property
843 def is_clean(self) -> bool:
844 return len(self.conflict_ops) == 0
845
846
847 def ops_commute(a: DomainOp, b: DomainOp) -> bool:
848 """Return True if operations A and B commute (auto-mergeable)."""
849 ...
850
851 def transform(a: DomainOp, b: DomainOp) -> tuple[DomainOp, DomainOp]:
852 """Return (a', b') such that a ∘ b' = b ∘ a'.
853
854 This is the core OT transform function. When two operations a and b
855 are generated concurrently against the same base, transform returns
856 adjusted versions that can be applied sequentially to achieve the same
857 final state.
858 """
859 ...
860
861 def merge_op_lists(
862 base_ops: list[DomainOp],
863 ours_ops: list[DomainOp],
864 theirs_ops: list[DomainOp],
865 ) -> MergeOpsResult:
866 """Three-way merge at operation granularity.
867
868 Applies commutativity rules to detect which pairs of operations truly
869 conflict. Non-conflicting pairs are auto-merged by applying OT transform.
870 Conflicting pairs are collected in ``conflict_ops`` for plugin resolution.
871 """
872 ...
873 ```
874
875 ### 5.4 Updated Core Merge Engine
876
877 `muse/core/merge_engine.py` gains a new entry point:
878
879 ```python
880 def merge_structured(
881 base_delta: StructuredDelta,
882 ours_delta: StructuredDelta,
883 theirs_delta: StructuredDelta,
884 ) -> MergeOpsResult:
885 """Merge two structured deltas against a common base delta.
886
887 Uses ``op_transform.merge_op_lists`` for operation-level conflict
888 detection. Falls back to file-level path detection for ops that do
889 not carry position information (e.g. SetSchema domains).
890 """
891 from muse.core.op_transform import merge_op_lists
892 return merge_op_lists(base_delta["ops"], ours_delta["ops"], theirs_delta["ops"])
893 ```
894
895 The existing `diff_snapshots` / `detect_conflicts` / `apply_merge` functions remain for plugins that have not yet produced `StructuredDelta` from `diff()` — they serve as the fallback.
896
897 ### 5.5 Plugin Protocol Gains `merge_ops`
898
899 A new optional method on the protocol (not required — the core engine falls back to file-level merge if absent):
900
901 ```python
902 def merge_ops(
903 self,
904 base: StateSnapshot,
905 ours_ops: list[DomainOp],
906 theirs_ops: list[DomainOp],
907 *,
908 repo_root: pathlib.Path | None = None,
909 ) -> MergeResult:
910 """Merge two op lists against base, using domain-specific conflict resolution.
911
912 The core engine calls this when both branches have produced
913 ``StructuredDelta`` from ``diff()``. The plugin may use
914 ``muse.core.op_transform.merge_op_lists`` as the foundation and
915 add domain-specific resolution on top (e.g. checking ``.museattributes``).
916
917 If not implemented, the core engine falls back to the existing
918 three-way file-level merge via ``merge()``.
919 """
920 ...
921 ```
922
923 ### 5.6 Position Adjustment After Transform
924
925 A critical detail: when two inserts commute because they're at different positions, the positions of later-applied operations must be adjusted. This is the "index shifting" problem in OT:
926
927 ```
928 Base: [A, B, C]
929 Ours: insert X at position 1 → [A, X, B, C]
930 Theirs: insert Y at position 2 → [A, B, Y, C]
931
932 After transform:
933 ours' (applied after theirs): insert X at position 1 → [A, X, B, Y, C] ✓
934 theirs' (applied after ours): insert Y at position 3 → [A, X, B, Y, C] ✓
935 ```
936
937 The transform function must adjust positions for all sequence operations. This is well-understood in OT literature but requires care in implementation, particularly for interleaved inserts and deletes.
938
939 ### 5.7 Phase 3 Test Cases
940
941 **New test file: `tests/test_op_transform.py`**
942
943 ```
944 # Commutativity oracle
945 test_commute_inserts_at_different_positions_is_true
946 test_commute_inserts_at_same_position_is_false
947 test_commute_deletes_at_different_addresses_is_true
948 test_commute_consensus_delete_is_true
949 test_commute_replaces_at_different_addresses_is_true
950 test_commute_replaces_at_same_address_is_false
951 test_commute_move_and_delete_same_position_is_false
952 test_commute_patch_at_different_addresses_is_true
953 test_commute_patch_at_same_address_recurses_children
954
955 # OT transform function
956 test_transform_two_inserts_adjusts_positions_correctly
957 test_transform_insert_and_delete_produces_adjusted_ops
958 test_transform_identity_when_ops_commute
959
960 # Three-way merge
961 test_merge_op_lists_clean_non_overlapping
962 test_merge_op_lists_same_op_both_sides_is_idempotent
963 test_merge_op_lists_conflict_same_position_insert
964 test_merge_op_lists_conflict_same_address_replace
965 test_merge_op_lists_consensus_delete_both_sides
966 test_merge_op_lists_nested_patch_recurses
967 test_merge_op_lists_position_adjustment_cascades
968 test_merge_op_lists_empty_one_side_applies_other
969 test_merge_op_lists_both_empty_returns_base
970
971 # Integration: merge engine uses op_transform when structured deltas available
972 test_merge_engine_uses_op_transform_for_structured_deltas
973 test_merge_engine_falls_back_to_file_level_without_structured_deltas
974 test_full_merge_non_overlapping_note_inserts_auto_merges
975 test_full_merge_same_note_insert_produces_conflict
976 ```
977
978 ### 5.8 Files Changed in Phase 3
979
980 | File | Change |
981 |---|---|
982 | `muse/core/op_transform.py` | **New.** `ops_commute`, `transform`, `merge_op_lists`. |
983 | `muse/core/merge_engine.py` | Add `merge_structured()`. Fallback logic preserved. |
984 | `muse/domain.py` | Add optional `merge_ops()` to `MuseDomainPlugin` protocol. |
985 | `muse/plugins/music/plugin.py` | Implement `merge_ops()` using `op_transform`. |
986 | `tests/test_op_transform.py` | **New.** |
987 | `tests/test_core_merge_engine.py` | Add structured-delta merge tests. |
988
989 ---
990
991 ## 6. Phase 4 — CRDT Semantics for Convergent Multi-Agent Writes
992
993 **Goal:** Plugin authors can opt into CRDT (Conflict-free Replicated Data Type) semantics. Merge becomes a mathematical `join` on a lattice. No conflict state ever exists. Millions of agents can write concurrently and always converge.
994
995 **Estimated scope:** 6–8 weeks (significant distributed systems work).
996
997 ### 6.1 Motivation
998
999 Phases 1–3 give you an extremely powerful three-way merge system. But three-way merge has a fundamental limit: it requires a common ancestor (merge base). In a world of millions of concurrent agents writing to shared state across unreliable networks, finding and coordinating around a merge base is expensive and sometimes impossible.
1000
1001 CRDTs eliminate this: given any two replicas of a CRDT data structure, the `join` operation produces a deterministic merged state — no base required, no conflicts possible, no coordination needed. This is mathematically guaranteed by the lattice laws (commutativity, associativity, idempotency of join).
1002
1003 This is the endgame for the multi-agent scenario.
1004
1005 ### 6.2 CRDT Primitive Library
1006
1007 **New directory: `muse/core/crdts/`**
1008
1009 ```
1010 muse/core/crdts/
1011 __init__.py
1012 lww_register.py → Last-Write-Wins Register
1013 or_set.py → Observed-Remove Set
1014 rga.py → Replicated Growable Array (ordered sequences)
1015 aw_map.py → Add-Wins Map
1016 g_counter.py → Grow-only Counter
1017 vclock.py → Vector Clock (causal ordering)
1018 ```
1019
1020 **`muse/core/crdts/lww_register.py`** — Last-Write-Wins Register:
1021
1022 Stores a single value with a timestamp. `join` takes the value with the higher timestamp. Appropriate for scalar config values, metadata, labels. Requires a reliable wall clock or logical clock for correct behavior.
1023
1024 ```python
1025 class LWWValue(TypedDict):
1026 value: str # JSON-serialisable value
1027 timestamp: float # Unix timestamp or logical clock
1028 author: str # Agent ID for tiebreaking
1029
1030 class LWWRegister:
1031 """A register where the last write (by timestamp) wins on merge."""
1032 def read(self) -> str: ...
1033 def write(self, value: str, timestamp: float, author: str) -> None: ...
1034 def join(self, other: LWWRegister) -> LWWRegister: ... # convergent merge
1035 def to_dict(self) -> LWWValue: ...
1036 @classmethod
1037 def from_dict(cls, data: LWWValue) -> LWWRegister: ...
1038 ```
1039
1040 **`muse/core/crdts/or_set.py`** — Observed-Remove Set:
1041
1042 An unordered set where adds always win over concurrent removes (the "add-wins" property). Each element carries a unique tag set; removing requires knowing the tags of the current observed value. Safe for sets of domain objects.
1043
1044 **`muse/core/crdts/rga.py`** — Replicated Growable Array:
1045
1046 The RGA (Replicated Growable Array) is a CRDT for ordered sequences — the mathematical foundation of collaborative text editing. Each element carries a unique identifier (timestamp + author). Concurrent inserts at the same position are resolved deterministically by author ID. This gives you Google Docs-style collaborative editing semantics for any ordered sequence domain.
1047
1048 ```python
1049 class RGAElement(TypedDict):
1050 id: str # stable unique ID: f"{timestamp}@{author}"
1051 value: str # content hash of element (references object store)
1052 deleted: bool # tombstone — never actually removed, marked deleted
1053
1054 class RGA:
1055 """Replicated Growable Array — CRDT for ordered sequences."""
1056 def insert(self, after_id: str | None, element: RGAElement) -> None: ...
1057 def delete(self, element_id: str) -> None: ...
1058 def join(self, other: RGA) -> RGA: ... # always succeeds, no conflicts
1059 def to_sequence(self) -> list[str]: ... # materialise visible elements
1060 def to_dict(self) -> list[RGAElement]: ...
1061 @classmethod
1062 def from_dict(cls, data: list[RGAElement]) -> RGA: ...
1063 ```
1064
1065 **`muse/core/crdts/vclock.py`** — Vector Clock:
1066
1067 Required for causal ordering in distributed multi-agent scenarios. A vector clock tracks how many events each agent has seen, enabling detection of concurrent vs. causally-ordered writes. Necessary for correct LWW behavior and for RGA tie-breaking.
1068
1069 ```python
1070 class VectorClock:
1071 """Causal clock for distributed agent writes."""
1072 def increment(self, agent_id: str) -> None: ...
1073 def merge(self, other: VectorClock) -> VectorClock: ...
1074 def happens_before(self, other: VectorClock) -> bool: ...
1075 def concurrent_with(self, other: VectorClock) -> bool: ...
1076 def to_dict(self) -> dict[str, int]: ...
1077 @classmethod
1078 def from_dict(cls, data: dict[str, int]) -> VectorClock: ...
1079 ```
1080
1081 ### 6.3 CRDT-Aware Snapshot Format
1082
1083 When a plugin uses CRDT semantics, the `SnapshotManifest` carries additional metadata:
1084
1085 ```python
1086 class CRDTSnapshotManifest(TypedDict):
1087 """Extended snapshot for CRDT-mode plugins."""
1088 files: dict[str, str] # path → content hash (as before)
1089 domain: str
1090 vclock: dict[str, int] # vector clock at snapshot time
1091 crdt_state: dict[str, str] # path → CRDT state hash (separate from content)
1092 schema_version: Literal[1]
1093 ```
1094
1095 The `crdt_state` stores the CRDT metadata (tombstones, element IDs, timestamps) separately from the content hashes. This keeps the content-addressed object store valid while allowing CRDT state to accumulate.
1096
1097 ### 6.4 CRDTPlugin Protocol Extension
1098
1099 ```python
1100 class CRDTPlugin(MuseDomainPlugin, Protocol):
1101 """Extension of MuseDomainPlugin for CRDT-mode domains.
1102
1103 Plugins implementing this protocol get convergent merge semantics:
1104 ``merge()`` is replaced by ``join()``, which always succeeds.
1105 """
1106
1107 def crdt_schema(self) -> CRDTSchema:
1108 """Declare the CRDT types used for each dimension."""
1109 ...
1110
1111 def join(
1112 self,
1113 a: CRDTSnapshotManifest,
1114 b: CRDTSnapshotManifest,
1115 ) -> CRDTSnapshotManifest:
1116 """Merge two snapshots by computing their lattice join.
1117
1118 This operation is:
1119 - Commutative: join(a, b) = join(b, a)
1120 - Associative: join(join(a, b), c) = join(a, join(b, c))
1121 - Idempotent: join(a, a) = a
1122
1123 These three properties guarantee convergence regardless of message
1124 order or delivery count.
1125 """
1126 ...
1127
1128 def to_crdt_state(self, snapshot: StateSnapshot) -> CRDTSnapshotManifest:
1129 """Lift a plain snapshot into CRDT state representation."""
1130 ...
1131
1132 def from_crdt_state(self, crdt: CRDTSnapshotManifest) -> StateSnapshot:
1133 """Materialise a CRDT state back to a plain snapshot."""
1134 ...
1135 ```
1136
1137 ### 6.5 When to Use CRDT Mode
1138
1139 | Scenario | Recommendation |
1140 |---|---|
1141 | Human-paced commits (once per hour/day) | Three-way merge (Phases 1–3) — richer conflict resolution |
1142 | Many agents writing concurrently (once per second) | CRDT mode — no coordination needed |
1143 | Mix (some slow human commits, some fast agent writes) | CRDT mode with LWW per-dimension |
1144 | Simulation state frames (sequential, one writer) | Three-way merge |
1145 | Shared genomics annotation (many simultaneous annotators) | CRDT OR-Set for annotation set |
1146 | Collaborative score editing (DAW-style) | CRDT RGA for note sequences |
1147
1148 The `DomainSchema.merge_mode` field controls which path the core engine takes. A plugin can declare `merge_mode: "crdt"` for some dimensions and fall back to `"three_way"` for others.
1149
1150 ### 6.6 Phase 4 Test Cases
1151
1152 **New test file: `tests/test_crdts.py`**
1153
1154 ```
1155 # LWWRegister
1156 test_lww_later_timestamp_wins
1157 test_lww_same_timestamp_author_tiebreak
1158 test_lww_join_is_commutative
1159 test_lww_join_is_associative
1160 test_lww_join_is_idempotent
1161
1162 # ORSet
1163 test_or_set_add_survives_concurrent_remove
1164 test_or_set_remove_observed_element_works
1165 test_or_set_join_is_commutative
1166 test_or_set_join_is_associative
1167 test_or_set_join_is_idempotent
1168
1169 # RGA
1170 test_rga_insert_after_none_is_prepend
1171 test_rga_insert_at_end
1172 test_rga_delete_marks_tombstone
1173 test_rga_concurrent_insert_same_position_deterministic
1174 test_rga_join_is_commutative
1175 test_rga_join_is_associative
1176 test_rga_join_is_idempotent
1177 test_rga_to_sequence_excludes_tombstones
1178 test_rga_round_trip_to_from_dict
1179
1180 # VectorClock
1181 test_vclock_increment_own_agent
1182 test_vclock_merge_takes_max_per_agent
1183 test_vclock_happens_before_simple
1184 test_vclock_concurrent_with_neither_dominates
1185 test_vclock_idempotent_merge
1186
1187 # CRDTPlugin integration
1188 test_crdt_plugin_join_produces_crdt_snapshot
1189 test_crdt_plugin_join_commutes
1190 test_crdt_join_via_core_merge_engine_uses_crdt_path
1191 test_crdt_merge_never_produces_conflicts
1192 ```
1193
1194 ### 6.7 Files Changed in Phase 4
1195
1196 | File | Change |
1197 |---|---|
1198 | `muse/core/crdts/__init__.py` | **New.** |
1199 | `muse/core/crdts/lww_register.py` | **New.** |
1200 | `muse/core/crdts/or_set.py` | **New.** |
1201 | `muse/core/crdts/rga.py` | **New.** |
1202 | `muse/core/crdts/aw_map.py` | **New.** |
1203 | `muse/core/crdts/g_counter.py` | **New.** |
1204 | `muse/core/crdts/vclock.py` | **New.** |
1205 | `muse/domain.py` | Add `CRDTPlugin` protocol, `CRDTSnapshotManifest`. |
1206 | `muse/core/schema.py` | Add `CRDTSchema`. `DomainSchema.merge_mode` supports `"crdt"`. |
1207 | `muse/core/merge_engine.py` | Route to CRDT `join` when `merge_mode == "crdt"`. |
1208 | `tests/test_crdts.py` | **New.** |
1209
1210 ---
1211
1212 ## 7. Cross-Cutting Concerns
1213
1214 ### 7.1 Content-Addressed Object Store Compatibility
1215
1216 The object store (`.muse/objects/SHA-256`) requires no changes through all four phases. Phase 1's sub-element `content_id` fields in operation types reference objects already in the store. This means:
1217
1218 - Any binary element (a note, a nucleotide block, a mesh vertex group) stored via `write_object(repo_root, sha256, bytes)` is automatically deduplicated.
1219 - The store scales to millions of fine-grained sub-elements without format changes.
1220 - Pack files (future work) can be added without changing the protocol.
1221
1222 ### 7.2 Commit Record Format Evolution
1223
1224 Each phase adds fields to `CommitRecord`. The commit record must carry a `format_version` field so future readers can understand what they're looking at:
1225
1226 ```python
1227 class CommitRecord(TypedDict):
1228 commit_id: str
1229 snapshot_id: str
1230 parent_commit_id: str | None
1231 parent2_commit_id: str | None
1232 message: str
1233 author: str
1234 committed_at: str
1235 domain: str
1236 structured_delta: StructuredDelta | None # Phase 1
1237 format_version: Literal[1, 2, 3, 4] # tracks schema changes
1238 ```
1239
1240 Since we have no backwards compat requirement, `format_version` starts at `1` and each phase that changes the record bumps it. Old records without new fields are read with `None` defaults.
1241
1242 ### 7.3 Wire Format for Agent-to-Agent Communication
1243
1244 Phase 4 introduces the scenario of multiple agents writing concurrently. This requires a wire format for exchanging operations and CRDT state. All types used (`StructuredDelta`, `CRDTSnapshotManifest`, `VectorClock`) are `TypedDict` and JSON-serialisable by design — this was deliberate. Any transport (HTTP, message queue, filesystem sync) can carry them without additional serialisation work.
1245
1246 ### 7.4 Typing Constraints
1247
1248 All new types must satisfy the zero-`Any` constraint enforced by `tools/typing_audit.py`. Key design decisions that ensure this:
1249
1250 - `DomainOp = InsertOp | DeleteOp | MoveOp | ReplaceOp | PatchOp` — exhaustive union, no `Any`.
1251 - `ElementSchema = SequenceSchema | TreeSchema | TensorSchema | MapSchema | SetSchema` — same.
1252 - `PatchOp.child_delta: StructuredDelta` — the recursive field is typed, not `dict[str, Any]`.
1253 - All CRDT types carry concrete generic parameters.
1254
1255 The `match` statement in `diff_by_schema` uses exhaustive `case` matching on `schema["kind"]` literals — mypy can verify exhaustiveness.
1256
1257 ### 7.5 Synchronous I/O Constraint
1258
1259 All algorithm implementations must remain synchronous. The LCS, tree-edit, and CRDT algorithms are all CPU-bound and complete in bounded time for bounded input sizes. No `async`, no `await`, no `asyncio` anywhere. If a domain's data is too large to diff synchronously, the plugin should chunk it — this is a domain concern, not a core engine concern.
1260
1261 ---
1262
1263 ## 8. Test Strategy
1264
1265 ### 8.1 Test Pyramid
1266
1267 ```
1268 ┌─────────┐
1269 │ E2E CLI │ (slow, few — cover user-visible behaviors)
1270 ┌─┴─────────┴─┐
1271 │ Integration │ (medium — real components wired together)
1272 ┌─┴─────────────┴─┐
1273 │ Unit │ (fast, many — every function in isolation)
1274 └──────────────────┘
1275 ```
1276
1277 Every new function gets a unit test before implementation (TDD). Every new interaction between two modules gets an integration test. Every new CLI behavior gets an E2E test.
1278
1279 ### 8.2 Property-Based Testing for Algorithms
1280
1281 Correctness of LCS, tree-edit, and CRDTs is best verified with property-based tests (using `hypothesis`). Key properties:
1282
1283 **LCS:**
1284 - Round-trip: `apply(base, diff(base, target)) == target` for all inputs
1285 - Minimality: `len(diff(a, b).ops) <= len(a) + len(b)` (LCS is minimal)
1286 - Identity: `diff(a, a).ops == []`
1287
1288 **CRDT lattice laws (all must hold for all inputs):**
1289 - Commutativity: `join(a, b) == join(b, a)`
1290 - Associativity: `join(join(a, b), c) == join(a, join(b, c))`
1291 - Idempotency: `join(a, a) == a`
1292
1293 **OT transform (Phase 3):**
1294 - Diamond property: `apply(apply(base, a), transform(a, b)[1]) == apply(apply(base, b), transform(a, b)[0])`
1295
1296 ### 8.3 Regression Test Naming Convention
1297
1298 Every bug fix gets a regression test named:
1299 ```
1300 test_<what_broke>_<correct_behavior>
1301 ```
1302
1303 Example: `test_concurrent_note_insert_same_bar_does_not_lose_notes`
1304
1305 ### 8.4 Test Isolation
1306
1307 All tests must be hermetic — no shared mutable state, no real filesystem without `tmp_path` fixture. Algorithm tests (LCS, tree-edit, CRDT) are purely in-memory and have no filesystem dependencies at all.
1308
1309 ---
1310
1311 ## 9. Implementation Order and Dependencies
1312
1313 ```
1314 Phase 1 ──────────────────────────────────────────────────────────► Phase 2
1315 │ Typed delta algebra │
1316 │ StructuredDelta replaces DeltaManifest │
1317 │ Music plugin: file→InsertOp, MIDI→PatchOp │
1318 │ midi_diff.py (LCS on note sequences) │
1319 │ │
1320 │ DEPENDS ON: nothing (self-contained) │ Domain schema declaration
1321 │ │ diff_algorithms/ library
1322 ▼ │ schema() on protocol
1323 Phase 3 ◄────────────────────────────────────────────────────────────┘
1324 │ Operation-level merge engine
1325 │ ops_commute(), transform(), merge_op_lists()
1326 │ Core engine routes to op_transform when StructuredDelta available
1327
1328 │ DEPENDS ON: Phase 1 (needs StructuredDelta) + Phase 2 (needs position metadata)
1329
1330
1331 Phase 4
1332 CRDT primitive library (LWWRegister, ORSet, RGA, AWMap, VectorClock)
1333 CRDTPlugin protocol extension
1334 Core merge engine: merge_mode == "crdt" → join()
1335
1336 DEPENDS ON: Phase 1–3 complete
1337 ```
1338
1339 **Critical path:** Phase 1 → Phase 2 → Phase 3 → Phase 4. Each phase requires the previous. Do not skip phases or reorder.
1340
1341 **Parallel work possible within phases:**
1342 - Phase 1: `midi_diff.py` can be implemented in parallel with the type system changes.
1343 - Phase 2: `lcs.py`, `tree_edit.py`, `numerical.py` can be implemented in parallel.
1344 - Phase 4: All CRDT types (`rga.py`, `or_set.py`, etc.) are independent and can be built in parallel.
1345
1346 ### 9.1 Rough Timeline
1347
1348 | Phase | Calendar estimate | Primary difficulty |
1349 |---|---|---|
1350 | Phase 1 | 2–3 weeks | Protocol change propagation; Myers LCS for MIDI |
1351 | Phase 2 | 3–4 weeks | Zhang-Shasha implementation; schema dispatch typing |
1352 | Phase 3 | 4–6 weeks | OT transform correctness; position adjustment cascades |
1353 | Phase 4 | 6–8 weeks | CRDT correctness proofs; vector clock integration |
1354 | **Total** | **15–21 weeks** | |
1355
1356 Phase 3 is the hardest. The OT transform correctness for all op-pair combinations is subtle and requires exhaustive property testing with `hypothesis`. Budget extra time there.
1357
1358 ### 9.2 Definition of Done Per Phase
1359
1360 **Phase 1 done when:**
1361 - [ ] `DeltaManifest` is gone; `StructuredDelta` is the only `StateDelta`
1362 - [ ] Music plugin's `diff()` returns `StructuredDelta` with `PatchOp` for `.mid` files
1363 - [ ] `muse show <commit>` displays note-level diff summary
1364 - [ ] `mypy muse/` zero errors
1365 - [ ] `python tools/typing_audit.py --dirs muse/ tests/ --max-any 0` zero violations
1366 - [ ] All new test cases pass
1367
1368 **Phase 2 done when:**
1369 - [ ] `schema()` is on the `MuseDomainPlugin` protocol
1370 - [ ] Music plugin returns a complete `DomainSchema`
1371 - [ ] `diff_algorithms/` contains LCS, tree-edit, numerical, set implementations
1372 - [ ] All four algorithms have property-based tests passing
1373 - [ ] `diff_by_schema` dispatch is exhaustively typed (no `Any`, mypy verified)
1374
1375 **Phase 3 done when:**
1376 - [ ] `ops_commute()` correctly handles all 25 op-pair combinations
1377 - [ ] `transform()` passes the diamond property for all commuting pairs
1378 - [ ] Music plugin: inserting notes at non-overlapping bars never conflicts
1379 - [ ] Core merge engine uses `merge_op_lists` when `StructuredDelta` is available
1380 - [ ] Property tests verify OT correctness on randomly generated op sequences
1381
1382 **Phase 4 done when:**
1383 - [ ] All five CRDT types pass the three lattice laws (property tests via `hypothesis`)
1384 - [ ] `VectorClock` correctly identifies concurrent vs. causally-ordered events
1385 - [ ] A MIDI plugin in CRDT mode never produces a `MergeResult.conflicts` entry
1386 - [ ] The core merge engine's CRDT path is exercised by integration tests
1387 - [ ] `CRDTPlugin` protocol is verified by a `runtime_checkable` assertion
1388
1389 ---
1390
1391 *End of plan. Implementation begins at Phase 1. Each phase produces a PR against `dev` with its own verification checklist completed.*