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