cgcardona / muse public
render_domain_registry.py python
2387 lines 104.6 KB
8c11c369 feat: add 21-dimensional MIDI demo page (Bach BWV 846 × Muse VCS) Gabriel Cardona <gabriel@tellurstori.com> 1d ago
1 #!/usr/bin/env python3
2 """Muse Domain Registry — standalone HTML generator.
3
4 Produces a self-contained, shareable page that explains the MuseDomainPlugin
5 protocol, shows the registered plugin ecosystem, and guides developers through
6 scaffolding and publishing their own domain plugin.
7
8 Stand-alone usage
9 -----------------
10 python tools/render_domain_registry.py
11 python tools/render_domain_registry.py --out artifacts/domain_registry.html
12 """
13
14 import json
15 import pathlib
16 import subprocess
17 import sys
18
19 _ROOT = pathlib.Path(__file__).resolve().parent.parent
20
21
22 # ---------------------------------------------------------------------------
23 # Live domain data from the CLI
24 # ---------------------------------------------------------------------------
25
26
27 def _compute_crdt_demos() -> list[dict]:
28 """Run the four CRDT primitives live and return formatted demo output."""
29 sys.path.insert(0, str(_ROOT))
30 try:
31 from muse.core.crdts import AWMap, GCounter, LWWRegister, ORSet, RGA, VectorClock
32
33 # ORSet
34 base, _ = ORSet().add("annotation-GO:0001234")
35 a, _ = base.add("annotation-GO:0001234")
36 b = base.remove("annotation-GO:0001234", base.tokens_for("annotation-GO:0001234"))
37 merged = a.join(b)
38 orset_out = "\n".join([
39 "ORSet — add-wins concurrent merge:",
40 f" base elements: {sorted(base.elements())}",
41 f" A re-adds → elements: {sorted(a.elements())}",
42 f" B removes → elements: {sorted(b.elements())}",
43 f" join(A, B) → elements: {sorted(merged.elements())}",
44 " [A's new token is not tombstoned — add always wins]",
45 ])
46
47 # LWWRegister
48 ra = LWWRegister.from_dict({"value": "80 BPM", "timestamp": 1.0, "author": "agent-A"})
49 rb = LWWRegister.from_dict({"value": "120 BPM", "timestamp": 2.0, "author": "agent-B"})
50 rm = ra.join(rb)
51 lww_out = "\n".join([
52 "LWWRegister — last-write-wins scalar:",
53 f" Agent A writes: '{ra.read()}' at t=1.0",
54 f" Agent B writes: '{rb.read()}' at t=2.0 (later)",
55 f" join(A, B) → '{rm.read()}' [higher timestamp wins]",
56 " join(B, A) → same result [commutativity]",
57 ])
58
59 # GCounter
60 ca = GCounter().increment("agent-A").increment("agent-A")
61 cb = GCounter().increment("agent-B").increment("agent-B").increment("agent-B")
62 cm = ca.join(cb)
63 gc_out = "\n".join([
64 "GCounter — grow-only distributed counter:",
65 f" Agent A x2 → A slot: {ca.value_for('agent-A')}",
66 f" Agent B x3 → B slot: {cb.value_for('agent-B')}",
67 f" join(A, B) global value: {cm.value()}",
68 " [monotonically non-decreasing — joins never lose counts]",
69 ])
70
71 # VectorClock
72 va = VectorClock().increment("agent-A")
73 vb = VectorClock().increment("agent-B")
74 vm = va.merge(vb)
75 vc_out = "\n".join([
76 "VectorClock — causal ordering:",
77 f" Agent A: {va.to_dict()}",
78 f" Agent B: {vb.to_dict()}",
79 f" concurrent_with(A, B): {va.concurrent_with(vb)}",
80 f" merge(A, B): {vm.to_dict()} [component-wise max]",
81 ])
82
83 elem = "annotation-GO:0001234"
84 short = "GO:0001234"
85
86 orset_html = f"""<div class="crdt-vis">
87 <div class="crdt-concurrent">
88 <div class="crdt-rep">
89 <div class="crdt-rep-hdr">Replica A</div>
90 <div class="crdt-op crdt-op-add">+ add("{short}")</div>
91 <div class="crdt-rep-state">&rarr;&thinsp;{{{short}}}</div>
92 </div>
93 <div class="crdt-rep">
94 <div class="crdt-rep-hdr">Replica B</div>
95 <div class="crdt-op crdt-op-rm">&times; remove("{short}")</div>
96 <div class="crdt-rep-state">&rarr;&thinsp;{{}}</div>
97 </div>
98 </div>
99 <div class="crdt-join" style="--crdt-c:#bc8cff">
100 <span class="crdt-join-label">join(A, B)</span>
101 <span class="crdt-join-val" style="color:#bc8cff">{{{short}}}</span>
102 <span class="crdt-join-rule">add-wins &mdash; A&rsquo;s new token survives</span>
103 </div>
104 </div>"""
105
106 lww_html = f"""<div class="crdt-vis">
107 <div class="crdt-writes">
108 <div class="crdt-write">
109 <span class="crdt-time">t=1.0</span>
110 <span class="crdt-agent crdt-agent-a">A</span>
111 <span class="crdt-wval">"{ra.read()}"</span>
112 </div>
113 <div class="crdt-write crdt-write-winner">
114 <span class="crdt-time">t=2.0</span>
115 <span class="crdt-agent crdt-agent-b">B</span>
116 <span class="crdt-wval">"{rb.read()}"</span>
117 <span class="crdt-latest">latest &uarr;</span>
118 </div>
119 </div>
120 <div class="crdt-join">
121 <span class="crdt-join-label">join(A,B) = join(B,A)</span>
122 <span class="crdt-join-val" style="color:#58a6ff">"{rm.read()}"</span>
123 <span class="crdt-join-rule">commutative &mdash; higher timestamp always wins</span>
124 </div>
125 </div>"""
126
127 a_val = ca.value_for("agent-A")
128 b_val = cb.value_for("agent-B")
129 total = cm.value()
130 a_pct = int(a_val / total * 100)
131 b_pct = int(b_val / total * 100)
132 gc_html = f"""<div class="crdt-vis">
133 <div class="crdt-gcounter">
134 <div class="crdt-gc-row">
135 <span class="crdt-agent crdt-agent-a">A &times;{a_val}</span>
136 <div class="crdt-bar"><div class="crdt-bar-fill crdt-bar-a" style="width:{a_pct}%">{a_val}</div></div>
137 </div>
138 <div class="crdt-gc-row">
139 <span class="crdt-agent crdt-agent-b">B &times;{b_val}</span>
140 <div class="crdt-bar"><div class="crdt-bar-fill crdt-bar-b" style="width:{b_pct}%">{b_val}</div></div>
141 </div>
142 </div>
143 <div class="crdt-join">
144 <span class="crdt-join-label">join(A, B) global</span>
145 <span class="crdt-join-val" style="color:#3fb950">{total}</span>
146 <span class="crdt-join-rule">component-wise max &mdash; monotonically non-decreasing</span>
147 </div>
148 </div>"""
149
150 concurrent = va.concurrent_with(vb)
151 merged_d = vm.to_dict()
152 vc_html = f"""<div class="crdt-vis">
153 <div class="crdt-vclocks">
154 <div class="crdt-vc">
155 <div class="crdt-vc-hdr">Agent A</div>
156 <div class="crdt-vc-cells">
157 <div class="crdt-vc-cell crdt-vc-self">A:1</div>
158 <div class="crdt-vc-cell crdt-vc-zero">B:0</div>
159 </div>
160 </div>
161 <div class="crdt-vc-sep">&oplus;</div>
162 <div class="crdt-vc">
163 <div class="crdt-vc-hdr">Agent B</div>
164 <div class="crdt-vc-cells">
165 <div class="crdt-vc-cell crdt-vc-zero">A:0</div>
166 <div class="crdt-vc-cell crdt-vc-self">B:1</div>
167 </div>
168 </div>
169 <div class="crdt-vc-sep">=</div>
170 <div class="crdt-vc">
171 <div class="crdt-vc-hdr">merge</div>
172 <div class="crdt-vc-cells">
173 {"".join(f'<div class="crdt-vc-cell crdt-vc-max">{k.split("-")[1].upper()}:{v}</div>' for k, v in sorted(merged_d.items()))}
174 </div>
175 </div>
176 </div>
177 <div class="crdt-concurrent-badge">concurrent_with(A, B) = {concurrent}</div>
178 <div class="crdt-join-rule" style="font-size:10.5px;color:var(--mute);font-style:italic">component-wise max &mdash; causal happens-before tracking</div>
179 </div>"""
180
181 # RGA — ordered note sequence: A inserts C4+E4, B inserts G4 concurrently
182 rga_a = RGA()
183 rga_a = rga_a.insert(None, "hash-C4", element_id="1@agent-A")
184 rga_a = rga_a.insert("1@agent-A", "hash-E4", element_id="2@agent-A")
185 rga_b = RGA()
186 rga_b = rga_b.insert(None, "hash-C4", element_id="1@agent-A") # same start
187 rga_b = rga_b.insert("1@agent-A", "hash-G4", element_id="1@agent-B") # concurrent
188 rga_m = rga_a.join(rga_b)
189 rga_seq_a = ["C4", "E4"]
190 rga_seq_b = ["C4", "G4"]
191 _rga_hash_to_note = {"hash-C4": "C4", "hash-E4": "E4", "hash-G4": "G4"}
192 rga_seq_m = [_rga_hash_to_note.get(h, h) for h in rga_m.to_sequence()]
193 rga_out = "\n".join([
194 "RGA — ordered sequence (Google-Docs-style):",
195 f" Agent A sequence: {rga_seq_a}",
196 f" Agent B sequence: {rga_seq_b}",
197 f" join(A, B): {rga_seq_m} [deterministic, ID-ordered]",
198 " Tombstones kept — deletions never lose causal history",
199 ])
200 rga_html = """<div class="crdt-vis">
201 <div class="crdt-concurrent">
202 <div class="crdt-rep">
203 <div class="crdt-rep-hdr">Replica A</div>
204 <div class="crdt-op crdt-op-add">+ insert("C4", id=1@A)</div>
205 <div class="crdt-op crdt-op-add">+ insert("E4", id=2@A)</div>
206 <div class="crdt-rep-state">&rarr;&thinsp;[C4, E4]</div>
207 </div>
208 <div class="crdt-rep">
209 <div class="crdt-rep-hdr">Replica B</div>
210 <div class="crdt-op crdt-op-add">+ insert("C4", id=1@A)</div>
211 <div class="crdt-op crdt-op-add">+ insert("G4", id=1@B) <em>concurrent</em></div>
212 <div class="crdt-rep-state">&rarr;&thinsp;[C4, G4]</div>
213 </div>
214 </div>
215 <div class="crdt-join" style="--crdt-c:#f9a825">
216 <span class="crdt-join-label">join(A, B)</span>
217 <span class="crdt-join-val" style="color:#f9a825">[C4, E4, G4]</span>
218 <span class="crdt-join-rule">ID-ordered &mdash; larger ID wins leftmost; all elements preserved</span>
219 </div>
220 </div>"""
221
222 # AWMap — key-value map: A sets "tempo", B sets "key_sig", both survive
223 aw_a = AWMap()
224 aw_a = aw_a.set("tempo", "120 BPM")
225 aw_b = AWMap()
226 aw_b = aw_b.set("key_sig", "C major")
227 aw_m = aw_a.join(aw_b)
228 aw_keys = sorted(aw_m.keys())
229 awmap_out = "\n".join([
230 "AWMap — add-wins key/value map:",
231 f" Agent A sets: {{tempo: '120 BPM'}}",
232 f" Agent B sets: {{key_sig: 'C major'}}",
233 f" join(A, B) keys: {aw_keys}",
234 " Both additions survive — concurrent removes cannot win",
235 ])
236 awmap_html = """<div class="crdt-vis">
237 <div class="crdt-concurrent">
238 <div class="crdt-rep">
239 <div class="crdt-rep-hdr">Replica A</div>
240 <div class="crdt-op crdt-op-add">+ set("tempo", "120 BPM")</div>
241 <div class="crdt-rep-state">&rarr;&thinsp;{tempo}</div>
242 </div>
243 <div class="crdt-rep">
244 <div class="crdt-rep-hdr">Replica B</div>
245 <div class="crdt-op crdt-op-add">+ set("key_sig", "C major")</div>
246 <div class="crdt-rep-state">&rarr;&thinsp;{key_sig}</div>
247 </div>
248 </div>
249 <div class="crdt-join" style="--crdt-c:#3fb950">
250 <span class="crdt-join-label">join(A, B)</span>
251 <span class="crdt-join-val" style="color:#3fb950">{key_sig, tempo}</span>
252 <span class="crdt-join-rule">add-wins &mdash; concurrent removes cannot evict new tokens</span>
253 </div>
254 </div>"""
255
256 return [
257 {"type": "ORSet", "sub": "Observed-Remove Set", "color": "#bc8cff", "icon": _ICONS["union"], "output": orset_out, "html_output": orset_html},
258 {"type": "LWWRegister", "sub": "Last-Write-Wins Register", "color": "#58a6ff", "icon": _ICONS["edit"], "output": lww_out, "html_output": lww_html},
259 {"type": "GCounter", "sub": "Grow-Only Distributed Counter", "color": "#3fb950", "icon": _ICONS["arrow-up"], "output": gc_out, "html_output": gc_html},
260 {"type": "VectorClock", "sub": "Causal Ordering", "color": "#f9a825", "icon": _ICONS["git-branch"], "output": vc_out, "html_output": vc_html},
261 {"type": "RGA", "sub": "Replicated Growable Array", "color": "#f9a825", "icon": _ICONS["layers"], "output": rga_out, "html_output": rga_html},
262 {"type": "AWMap", "sub": "Add-Wins Map", "color": "#3fb950", "icon": _ICONS["code"], "output": awmap_out, "html_output": awmap_html},
263 ]
264 except Exception as exc:
265 print(f" ⚠ CRDT demo failed ({exc}); using static fallback")
266 return []
267
268
269 def _load_domains() -> list[dict]:
270 """Run `muse domains --json` and return parsed output."""
271 try:
272 result = subprocess.run(
273 [sys.executable, "-m", "muse", "domains", "--json"],
274 capture_output=True,
275 text=True,
276 cwd=str(_ROOT),
277 timeout=15,
278 )
279 if result.returncode == 0:
280 raw = result.stdout.strip()
281 data: list[dict] = json.loads(raw)
282 return data
283 except Exception:
284 pass
285
286 # Fallback: static reference data
287 return [
288 {
289 "domain": "midi",
290 "active": "true",
291 "capabilities": ["Typed Deltas", "Domain Schema", "OT Merge", "CRDT Primitives", "MidiRGA"],
292 "schema": {
293 "schema_version": "1",
294 "merge_mode": "three_way",
295 "description": "MIDI file versioning with 21-dimension structured merge — notes, CC, pitch bend, tempo, time signatures, and more. Each dimension merges independently. MidiRGA provides voice-aware CRDT ordering (bass→tenor→alto→soprano) for concurrent note edits.",
296 "dimensions": [
297 {"name": "notes", "description": "Note-on/off events (pitch, velocity, channel, timing)"},
298 {"name": "control_change", "description": "CC curves: modulation, sustain, expression, pan"},
299 {"name": "pitch_bend", "description": "Pitch bend envelope per channel"},
300 {"name": "tempo", "description": "Tempo map (BPM changes over time)"},
301 {"name": "time_signature", "description": "Metre changes across the piece"},
302 {"name": "key_signature", "description": "Key and mode declarations"},
303 {"name": "program_change", "description": "Instrument (patch) assignments per channel"},
304 {"name": "aftertouch", "description": "Channel and polyphonic pressure"},
305 {"name": "sysex", "description": "System-exclusive device messages"},
306 {"name": "track_name", "description": "Human-readable track labels"},
307 ],
308 },
309 },
310 {
311 "domain": "code",
312 "active": "true",
313 "capabilities": ["Typed Deltas", "Domain Schema", "OT Merge", ".museattributes"],
314 "schema": {
315 "schema_version": "1",
316 "merge_mode": "symbol_ot",
317 "description": "Source-code versioning with symbol-level operational-transform merge. Tree-sitter parses 11 languages into ASTs; functions, classes, and imports merge independently. .museattributes gives per-path strategy control.",
318 "dimensions": [
319 {"name": "functions", "description": "Function and method definitions"},
320 {"name": "classes", "description": "Class and struct declarations"},
321 {"name": "imports", "description": "Import and module-level declarations"},
322 {"name": "variables", "description": "Module-level variable and constant assignments"},
323 {"name": "expressions", "description": "Top-level expression statements"},
324 ],
325 },
326 },
327 ]
328
329
330 # ---------------------------------------------------------------------------
331 # Scaffold template (shown in the "Build in 3 steps" section)
332 # ---------------------------------------------------------------------------
333
334 _TYPED_DELTA_EXAMPLE = """\
335 # muse show --json (any commit, any domain)
336 {
337 "commit_id": "b26f3c99",
338 "message": "Resolve: integrate shared-state (A+B reconciled)",
339 "operations": [
340 {
341 "op_type": "ReplaceOp",
342 "address": "shared-state.mid",
343 "before_hash": "a1b2c3d4",
344 "after_hash": "e5f6g7h8",
345 "dimensions": ["structural"]
346 },
347 {
348 "op_type": "InsertOp",
349 "address": "beta-a.mid",
350 "after_hash": "09ab1234",
351 "dimensions": ["rhythmic", "dynamic"]
352 }
353 ],
354 "summary": {
355 "inserted": 1,
356 "replaced": 1,
357 "deleted": 0
358 }
359 }"""
360
361
362 _SCAFFOLD_SNIPPET = """\
363 from muse.domain import (
364 MuseDomainPlugin, LiveState, StateSnapshot,
365 StateDelta, DriftReport, MergeResult, DomainSchema,
366 )
367
368 class GenomicsPlugin(MuseDomainPlugin):
369 \"\"\"Version control for genomic sequences.\"\"\"
370
371 def snapshot(self, live_state: LiveState) -> StateSnapshot:
372 # Serialize current genome state to a content-addressable blob
373 raise NotImplementedError
374
375 def diff(self, base: StateSnapshot,
376 target: StateSnapshot) -> StateDelta:
377 # Compute minimal delta between two snapshots
378 raise NotImplementedError
379
380 def merge(self, base: StateSnapshot,
381 left: StateSnapshot,
382 right: StateSnapshot) -> MergeResult:
383 # Three-way merge — surface conflicts per dimension
384 raise NotImplementedError
385
386 def drift(self, committed: StateSnapshot,
387 live: LiveState) -> DriftReport:
388 # Detect uncommitted changes in the working state
389 raise NotImplementedError
390
391 def apply(self, delta: StateDelta,
392 live_state: LiveState) -> LiveState:
393 # Reconstruct historical state from a delta
394 raise NotImplementedError
395
396 def schema(self) -> DomainSchema:
397 # Declare dimensions — drives diff algorithm selection
398 raise NotImplementedError
399 """
400
401 # ---------------------------------------------------------------------------
402 # SVG icon library — Lucide/Feather style, stroke="currentColor", no fixed size
403 # ---------------------------------------------------------------------------
404
405 def _icon(paths: str) -> str:
406 """Wrap SVG paths in a standard icon shell."""
407 return (
408 '<svg class="icon" viewBox="0 0 24 24" fill="none" '
409 'stroke="currentColor" stroke-width="1.75" '
410 'stroke-linecap="round" stroke-linejoin="round">'
411 + paths
412 + "</svg>"
413 )
414
415
416 _ICONS: dict[str, str] = {
417 # Domains
418 "midi": _icon('<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>'),
419 "genomics": _icon('<path d="M2 15c6.667-6 13.333 0 20-6"/><path d="M2 9c6.667 6 13.333 0 20 6"/><line x1="5.5" y1="11" x2="5.5" y2="13"/><line x1="18.5" y1="11" x2="18.5" y2="13"/>'),
420 "cube": _icon('<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>'),
421 "trending": _icon('<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/>'),
422 "atom": _icon('<circle cx="12" cy="12" r="1"/><path d="M20.2 20.2c2.04-2.03.02-7.36-4.5-11.9-4.54-4.52-9.87-6.54-11.9-4.5-2.04 2.03-.02 7.36 4.5 11.9 4.54 4.52 9.87 6.54 11.9 4.5z"/><path d="M15.7 15.7c4.52-4.54 6.54-9.87 4.5-11.9-2.03-2.04-7.36-.02-11.9 4.5-4.52 4.54-6.54 9.87-4.5 11.9 2.03 2.04 7.36.02 11.9-4.5z"/>'),
423 "plus": _icon('<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/>'),
424 "activity": _icon('<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>'),
425 "pen-tool": _icon('<path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/>'),
426 # Distribution
427 "terminal": _icon('<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>'),
428 "package": _icon('<line x1="16.5" y1="9.4" x2="7.5" y2="4.21"/><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>'),
429 "globe": _icon('<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>'),
430 # Engine capabilities
431 "code": _icon('<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>'),
432 "layers": _icon('<polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/>'),
433 "git-merge": _icon('<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M6 21V9a9 9 0 0 0 9 9"/>'),
434 "zap": _icon('<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>'),
435 # MuseHub features
436 "search": _icon('<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>'),
437 "lock": _icon('<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>'),
438 # CRDT primitives
439 "union": _icon('<path d="M5 5v8a7 7 0 0 0 14 0V5"/><line x1="3" y1="19" x2="21" y2="19"/>'),
440 "edit": _icon('<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>'),
441 "arrow-up": _icon('<line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/>'),
442 "git-branch": _icon('<line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>'),
443 # OT scenario outcome badges
444 "check-circle":_icon('<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>'),
445 "x-circle": _icon('<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>'),
446 }
447
448
449 # ---------------------------------------------------------------------------
450 # Planned / aspirational domains
451 # ---------------------------------------------------------------------------
452
453 _PLANNED_DOMAINS = [
454 {
455 "name": "Genomics",
456 "icon": _ICONS["genomics"],
457 "status": "planned",
458 "tagline": "Version sequences, variants, and annotations",
459 "dimensions": ["sequence", "variants", "annotations", "metadata"],
460 "color": "#3fb950",
461 },
462 {
463 "name": "3D / Spatial",
464 "icon": _ICONS["cube"],
465 "status": "planned",
466 "tagline": "Merge spatial fields, meshes, and simulation frames",
467 "dimensions": ["geometry", "materials", "physics", "temporal"],
468 "color": "#58a6ff",
469 },
470 {
471 "name": "Financial",
472 "icon": _ICONS["trending"],
473 "status": "planned",
474 "tagline": "Track model versions, alpha signals, and risk state",
475 "dimensions": ["signals", "positions", "risk", "parameters"],
476 "color": "#f9a825",
477 },
478 {
479 "name": "Scientific Simulation",
480 "icon": _ICONS["atom"],
481 "status": "planned",
482 "tagline": "Snapshot simulation state across timesteps and parameter spaces",
483 "dimensions": ["state", "parameters", "observables", "checkpoints"],
484 "color": "#ab47bc",
485 },
486 {
487 "name": "Your Domain",
488 "icon": _ICONS["plus"],
489 "status": "yours",
490 "tagline": "Six methods. Any multidimensional state. Full VCS for free.",
491 "dimensions": ["your_dim_1", "your_dim_2", "..."],
492 "color": "#4f8ef7",
493 },
494 ]
495
496 # ---------------------------------------------------------------------------
497 # Distribution model description
498 # ---------------------------------------------------------------------------
499
500 _DISTRIBUTION_LEVELS = [
501 {
502 "tier": "Local",
503 "icon": _ICONS["terminal"],
504 "title": "Local plugin (right now)",
505 "color": "#3fb950",
506 "steps": [
507 "muse domains --new &lt;name&gt;",
508 "Implement 6 methods in muse/plugins/&lt;name&gt;/plugin.py",
509 "Register in muse/plugins/registry.py",
510 "muse init --domain &lt;name&gt;",
511 ],
512 "desc": "Works today. Scaffold → implement → register. "
513 "Your plugin lives alongside the core.",
514 },
515 {
516 "tier": "Shareable",
517 "icon": _ICONS["package"],
518 "title": "pip-installable package (right now)",
519 "color": "#58a6ff",
520 "steps": [
521 "Package your plugin as a Python module",
522 "pip install git+https://github.com/you/muse-plugin-genomics",
523 "Register the entry-point in pyproject.toml",
524 "muse init --domain genomics",
525 ],
526 "desc": "Share your plugin as a standard Python package. "
527 "Anyone with pip can install and use it.",
528 },
529 {
530 "tier": "MuseHub",
531 "icon": _ICONS["globe"],
532 "title": "Centralized registry (coming — MuseHub)",
533 "color": "#bc8cff",
534 "steps": [
535 "musehub publish muse-plugin-genomics",
536 "musehub search genomics",
537 "muse init --domain @musehub/genomics",
538 "Browse plugins at musehub.io",
539 ],
540 "desc": "MuseHub is a planned centralized registry — npm for Muse plugins. "
541 "Versioned, searchable, one-command install.",
542 },
543 ]
544
545
546 # ---------------------------------------------------------------------------
547 # HTML template
548 # ---------------------------------------------------------------------------
549
550 def _render_capability_card(cap: dict) -> str:
551 color = cap["color"]
552 body = (
553 cap["html_output"]
554 if "html_output" in cap
555 else f'<pre class="cap-showcase-output">{cap["output"]}</pre>'
556 )
557 return f"""
558 <div class="cap-showcase-card" style="--cap-color:{color}">
559 <div class="cap-showcase-header">
560 <span class="cap-showcase-badge" style="color:{color};background:{color}15;border-color:{color}40">
561 {cap['icon']} {cap['type']}
562 </span>
563 <span class="cap-showcase-sub">{cap['sub']}</span>
564 </div>
565 <div class="cap-showcase-body">
566 {body}
567 </div>
568 </div>"""
569
570
571 def _render_domain_card(d: dict) -> str:
572 domain = d.get("domain", "unknown")
573 active = d.get("active") == "true"
574 schema = d.get("schema", {})
575 desc = schema.get("description", "")
576 dims = schema.get("dimensions", [])
577 caps = d.get("capabilities", [])
578
579 cap_html = " ".join(
580 f'<span class="cap-pill cap-{c.lower().replace(" ","-")}">{c}</span>'
581 for c in caps
582 )
583 dim_html = " · ".join(
584 f'<span class="dim-tag">{dim["name"]}</span>' for dim in dims
585 )
586
587 status_cls = "active-badge" if active else "reg-badge"
588 status_text = "● active" if active else "○ registered"
589 dot = '<span class="active-dot"></span>' if active else ""
590
591 short_desc = desc[:150] + ("…" if len(desc) > 150 else "")
592
593 return f"""
594 <div class="domain-card{' active-domain' if active else ''}">
595 <div class="domain-card-hdr">
596 <span class="{status_cls}">{status_text}</span>
597 <span class="domain-name-lg">{domain}</span>
598 {dot}
599 </div>
600 <div class="domain-card-body">
601 <p class="domain-desc">{short_desc}</p>
602 <div class="cap-row">{cap_html}</div>
603 <div class="dim-row"><strong>Dimensions:</strong> {dim_html}</div>
604 </div>
605 </div>"""
606
607
608 def _render_planned_card(p: dict) -> str:
609 dims = " · ".join(f'<span class="dim-tag">{d}</span>' for d in p["dimensions"])
610 cls = "planned-card yours" if p["status"] == "yours" else "planned-card"
611 return f"""
612 <div class="{cls}" style="--card-accent:{p['color']}">
613 <div class="planned-icon">{p['icon']}</div>
614 <div class="planned-name">{p['name']}</div>
615 <div class="planned-tag">{p['tagline']}</div>
616 <div class="planned-dims">{dims}</div>
617 {'<a class="cta-btn" href="#build">Build it →</a>' if p["status"] == "yours" else '<span class="coming-soon">coming soon</span>'}
618 </div>"""
619
620
621 def _render_dist_card(d: dict) -> str:
622 steps = "".join(
623 f'<li><code>{s}</code></li>' for s in d["steps"]
624 )
625 return f"""
626 <div class="dist-card" style="--dist-color:{d['color']}">
627 <div class="dist-header">
628 <span class="dist-icon">{d['icon']}</span>
629 <div>
630 <div class="dist-tier">{d['tier']}</div>
631 <div class="dist-title">{d['title']}</div>
632 </div>
633 </div>
634 <p class="dist-desc">{d['desc']}</p>
635 <ol class="dist-steps">{steps}</ol>
636 </div>"""
637
638
639 def render(output_path: pathlib.Path) -> None:
640 """Generate the domain registry HTML page."""
641 print(" Loading live domain data...")
642 domains = _load_domains()
643 print(f" Found {len(domains)} registered domain(s)")
644
645 print(" Computing live CRDT demos...")
646 crdt_demos = _compute_crdt_demos()
647
648 active_domains_html = "\n".join(_render_domain_card(d) for d in domains)
649 planned_html = "\n".join(_render_planned_card(p) for p in _PLANNED_DOMAINS)
650 dist_html = "\n".join(_render_dist_card(d) for d in _DISTRIBUTION_LEVELS)
651 crdt_cards_html = "\n".join(_render_capability_card(c) for c in crdt_demos)
652
653 html = _HTML_TEMPLATE.replace("{{ACTIVE_DOMAINS}}", active_domains_html)
654 html = html.replace("{{PLANNED_DOMAINS}}", planned_html)
655 html = html.replace("{{DIST_CARDS}}", dist_html)
656 html = html.replace("{{SCAFFOLD_SNIPPET}}", _SCAFFOLD_SNIPPET)
657 html = html.replace("{{TYPED_DELTA_EXAMPLE}}", _TYPED_DELTA_EXAMPLE)
658 html = html.replace("{{CRDT_CARDS}}", crdt_cards_html)
659 html = html.replace("{{DIFF_ALGEBRA}}", _DIFF_ALGEBRA_HTML)
660
661 # Inject SVG icons into template placeholders
662 _ICON_SLOTS: dict[str, str] = {
663 "MUSIC": _ICONS["midi"],
664 "GENOMICS": _ICONS["genomics"],
665 "CUBE": _ICONS["cube"],
666 "TRENDING": _ICONS["trending"],
667 "ATOM": _ICONS["atom"],
668 "PLUS": _ICONS["plus"],
669 "ACTIVITY": _ICONS["activity"],
670 "PEN_TOOL": _ICONS["pen-tool"],
671 "CODE": _ICONS["code"],
672 "LAYERS": _ICONS["layers"],
673 "GIT_MERGE": _ICONS["git-merge"],
674 "ZAP": _ICONS["zap"],
675 "GLOBE": _ICONS["globe"],
676 "SEARCH": _ICONS["search"],
677 "PACKAGE": _ICONS["package"],
678 "LOCK": _ICONS["lock"],
679 "CHECK_CIRCLE": _ICONS["check-circle"],
680 "X_CIRCLE": _ICONS["x-circle"],
681 }
682 for slot, svg in _ICON_SLOTS.items():
683 html = html.replace(f"{{{{ICON_{slot}}}}}", svg)
684
685 output_path.write_text(html, encoding="utf-8")
686 size_kb = output_path.stat().st_size // 1024
687 print(f" HTML written ({size_kb}KB) → {output_path}")
688
689 # Also write as index.html so the domain registry IS the landing page.
690 index_path = output_path.parent / "index.html"
691 index_path.write_text(html, encoding="utf-8")
692 print(f" Landing page mirrored → {index_path}")
693
694
695 # ---------------------------------------------------------------------------
696 # Diff Algebra section — five algorithm visualizations + StructuredDelta flow
697 # ---------------------------------------------------------------------------
698
699 _DIFF_ALGEBRA_HTML = """
700 <section id="diff-algebra" style="background:var(--bg)">
701 <div class="inner">
702 <div class="section-eyebrow">Diff Algebra</div>
703 <h2>Five Algebras. One Typed Result.</h2>
704 <p class="section-lead">
705 The engine selects the algorithm per dimension from your plugin&rsquo;s
706 <code>schema()</code>. You declare the shape &mdash; the engine handles identity,
707 diffing, and merge selection automatically.
708 </p>
709
710 <div class="da-grid">
711
712 <!-- 1. SEQUENCE — spans full width -->
713 <div class="da-card da-seq-card">
714 <div class="da-card-hdr">
715 <span class="da-chip da-chip-seq">Sequence</span>
716 <span class="da-algo-name">Myers / LCS on SHA-256 IDs</span>
717 </div>
718 <div class="da-domains-row">notes &middot; nucleotides &middot; animation frames &middot; git objects</div>
719 <div class="da-visual" style="flex-direction:column;align-items:flex-start;gap:8px">
720 <div class="seq-vis">
721 <div class="seq-row-lbl">before</div>
722 <div class="seq-row">
723 <div class="seq-blk seq-match"><div class="seq-hash">a1b2</div><div class="seq-name">C4</div></div>
724 <div class="seq-blk seq-del"><div class="seq-hash">c3d4</div><div class="seq-name">E4</div></div>
725 <div class="seq-blk seq-match"><div class="seq-hash">e5f6</div><div class="seq-name">G4</div></div>
726 <div class="seq-blk seq-moved-from"><div class="seq-hash">g7h8</div><div class="seq-name">B&flat;4</div></div>
727 </div>
728 <div class="seq-ops-row">
729 <div class="seq-op-cell"><span class="da-op da-op-match">= match</span></div>
730 <div class="seq-op-cell"><span class="da-op da-op-delete">&times; DeleteOp</span></div>
731 <div class="seq-op-cell"><span class="da-op da-op-match">= match</span></div>
732 <div class="seq-op-cell"><span class="da-op da-op-move">&darr; MoveOp</span></div>
733 </div>
734 <div class="seq-row">
735 <div class="seq-blk seq-match"><div class="seq-hash">a1b2</div><div class="seq-name">C4</div></div>
736 <div class="seq-blk seq-ins"><div class="seq-hash">k1l2</div><div class="seq-name">F4</div></div>
737 <div class="seq-blk seq-match"><div class="seq-hash">e5f6</div><div class="seq-name">G4</div></div>
738 <div class="seq-blk seq-ins"><div class="seq-hash">n5o6</div><div class="seq-name">A4</div></div>
739 <div class="seq-blk seq-moved-to"><div class="seq-hash">g7h8</div><div class="seq-name">B&flat;4</div></div>
740 </div>
741 <div class="seq-ops-row">
742 <div class="seq-op-cell"></div>
743 <div class="seq-op-cell"><span class="da-op da-op-insert">+ InsertOp</span></div>
744 <div class="seq-op-cell"></div>
745 <div class="seq-op-cell"><span class="da-op da-op-insert">+ InsertOp</span></div>
746 <div class="seq-op-cell"><span class="da-op da-op-move">&uarr; arrived</span></div>
747 </div>
748 <div class="seq-row-lbl">after</div>
749 </div>
750 </div>
751 <div class="da-note">Identity is hash-based: two elements are equal iff their SHA-256 hashes match &mdash; content is never inspected by the core. Delete&thinsp;+&thinsp;insert pairs sharing the same hash are collapsed into MoveOps in a post-pass.</div>
752 </div>
753
754 <!-- 2. TREE -->
755 <div class="da-card">
756 <div class="da-card-hdr">
757 <span class="da-chip da-chip-tree">Tree</span>
758 <span class="da-algo-name">Zhang-Shasha / GumTree</span>
759 </div>
760 <div class="da-domains-row">scene graphs &middot; ASTs &middot; track hierarchies</div>
761 <div class="da-visual" style="padding:10px 8px">
762 <svg class="tree-vis" viewBox="0 0 290 128" xmlns="http://www.w3.org/2000/svg">
763 <!-- BEFORE -->
764 <text x="52" y="9" text-anchor="middle" font-size="7" fill="#484f58" font-family="monospace" font-weight="700">BEFORE</text>
765 <line x1="52" y1="27" x2="24" y2="57" stroke="#30363d" stroke-width="1.5"/>
766 <line x1="52" y1="27" x2="80" y2="57" stroke="#30363d" stroke-width="1.5"/>
767 <line x1="24" y1="73" x2="11" y2="100" stroke="#30363d" stroke-width="1.5"/>
768 <line x1="24" y1="73" x2="37" y2="100" stroke="#30363d" stroke-width="1.5"/>
769 <line x1="80" y1="73" x2="80" y2="100" stroke="#bc8cff" stroke-width="1.5" stroke-dasharray="3,2"/>
770 <rect x="31" y="14" width="42" height="16" rx="4" fill="#161b22" stroke="#58a6ff" stroke-width="1.5"/>
771 <text x="52" y="25" text-anchor="middle" font-size="8" fill="#58a6ff" font-family="monospace">session</text>
772 <rect x="7" y="59" width="34" height="16" rx="3" fill="#161b22" stroke="#484f58"/>
773 <text x="24" y="70" text-anchor="middle" font-size="8" fill="#8b949e" font-family="monospace">intro</text>
774 <rect x="63" y="59" width="34" height="16" rx="3" fill="#161b22" stroke="#484f58"/>
775 <text x="80" y="70" text-anchor="middle" font-size="8" fill="#8b949e" font-family="monospace">verse</text>
776 <rect x="4" y="98" width="15" height="13" rx="3" fill="#161b22" stroke="#484f58"/>
777 <text x="11" y="108" text-anchor="middle" font-size="7.5" fill="#8b949e" font-family="monospace">C4</text>
778 <rect x="28" y="98" width="15" height="13" rx="3" fill="#161b22" stroke="#484f58"/>
779 <text x="35" y="108" text-anchor="middle" font-size="7.5" fill="#8b949e" font-family="monospace">E4</text>
780 <rect x="72" y="98" width="15" height="13" rx="3" fill="rgba(188,140,255,0.1)" stroke="#bc8cff" stroke-width="1.5"/>
781 <text x="79" y="108" text-anchor="middle" font-size="7.5" fill="#bc8cff" font-family="monospace">G4</text>
782 <!-- divider -->
783 <line x1="143" y1="8" x2="143" y2="118" stroke="#30363d" stroke-width="1" stroke-dasharray="3,3"/>
784 <text x="143" y="126" text-anchor="middle" font-size="6.5" fill="#484f58" font-family="monospace">MoveOp + InsertOp</text>
785 <!-- AFTER -->
786 <text x="216" y="9" text-anchor="middle" font-size="7" fill="#484f58" font-family="monospace" font-weight="700">AFTER</text>
787 <line x1="216" y1="27" x2="183" y2="57" stroke="#30363d" stroke-width="1.5"/>
788 <line x1="216" y1="27" x2="249" y2="57" stroke="#30363d" stroke-width="1.5"/>
789 <line x1="183" y1="73" x2="162" y2="100" stroke="#30363d" stroke-width="1.5"/>
790 <line x1="183" y1="73" x2="178" y2="100" stroke="#30363d" stroke-width="1.5"/>
791 <line x1="183" y1="73" x2="195" y2="100" stroke="#bc8cff" stroke-width="1.5" stroke-dasharray="3,2"/>
792 <line x1="249" y1="73" x2="249" y2="100" stroke="#3fb950" stroke-width="1.5"/>
793 <rect x="195" y="14" width="42" height="16" rx="4" fill="#161b22" stroke="#58a6ff" stroke-width="1.5"/>
794 <text x="216" y="25" text-anchor="middle" font-size="8" fill="#58a6ff" font-family="monospace">session</text>
795 <rect x="166" y="59" width="34" height="16" rx="3" fill="#161b22" stroke="#484f58"/>
796 <text x="183" y="70" text-anchor="middle" font-size="8" fill="#8b949e" font-family="monospace">intro</text>
797 <rect x="232" y="59" width="34" height="16" rx="3" fill="#161b22" stroke="#484f58"/>
798 <text x="249" y="70" text-anchor="middle" font-size="8" fill="#8b949e" font-family="monospace">verse</text>
799 <rect x="155" y="98" width="15" height="13" rx="3" fill="#161b22" stroke="#484f58"/>
800 <text x="162" y="108" text-anchor="middle" font-size="7.5" fill="#8b949e" font-family="monospace">C4</text>
801 <rect x="171" y="98" width="15" height="13" rx="3" fill="#161b22" stroke="#484f58"/>
802 <text x="178" y="108" text-anchor="middle" font-size="7.5" fill="#8b949e" font-family="monospace">E4</text>
803 <rect x="188" y="98" width="15" height="13" rx="3" fill="rgba(188,140,255,0.12)" stroke="#bc8cff" stroke-width="1.5"/>
804 <text x="195" y="108" text-anchor="middle" font-size="7.5" fill="#bc8cff" font-family="monospace">G4</text>
805 <rect x="241" y="98" width="15" height="13" rx="3" fill="rgba(63,185,80,0.12)" stroke="#3fb950" stroke-width="1.5"/>
806 <text x="249" y="108" text-anchor="middle" font-size="7.5" fill="#3fb950" font-family="monospace">A4</text>
807 <rect x="156" y="116" width="8" height="8" rx="1" fill="rgba(188,140,255,0.1)" stroke="#bc8cff"/>
808 <text x="166" y="122" font-size="6" fill="#bc8cff" font-family="monospace">moved</text>
809 <rect x="204" y="116" width="8" height="8" rx="1" fill="rgba(63,185,80,0.1)" stroke="#3fb950"/>
810 <text x="214" y="122" font-size="6" fill="#3fb950" font-family="monospace">inserted</text>
811 </svg>
812 </div>
813 <div class="da-note">Edit distance over node hierarchy &mdash; moves preserve subtree identity across parent changes.</div>
814 </div>
815
816 <!-- 3. TENSOR -->
817 <div class="da-card">
818 <div class="da-card-hdr">
819 <span class="da-chip da-chip-tensor">Tensor</span>
820 <span class="da-algo-name">Sparse / block numerical diff</span>
821 </div>
822 <div class="da-domains-row">sim state &middot; voxel grids &middot; weight matrices</div>
823 <div class="da-visual" style="flex-direction:column;gap:12px">
824 <div class="tensor-wrap">
825 <div class="tensor-panel">
826 <div class="tensor-label">t&thinsp;=&thinsp;0</div>
827 <div class="tensor-grid">
828 <div class="tc tc-2"></div><div class="tc tc-0"></div><div class="tc tc-1"></div><div class="tc tc-0"></div>
829 <div class="tc tc-0"></div><div class="tc tc-3"></div><div class="tc tc-0"></div><div class="tc tc-1"></div>
830 <div class="tc tc-1"></div><div class="tc tc-0"></div><div class="tc tc-2"></div><div class="tc tc-0"></div>
831 <div class="tc tc-0"></div><div class="tc tc-1"></div><div class="tc tc-0"></div><div class="tc tc-3"></div>
832 </div>
833 </div>
834 <div class="tensor-arrow">&rarr;</div>
835 <div class="tensor-panel">
836 <div class="tensor-label">t&thinsp;=&thinsp;1&thinsp;(&Delta;)</div>
837 <div class="tensor-grid">
838 <div class="tc tc-2"></div><div class="tc tc-0"></div><div class="tc tc-1"></div><div class="tc tc-0"></div>
839 <div class="tc tc-0"></div><div class="tc tc-hot3"></div><div class="tc tc-0"></div><div class="tc tc-warm1"></div>
840 <div class="tc tc-1"></div><div class="tc tc-0"></div><div class="tc tc-hot2"></div><div class="tc tc-0"></div>
841 <div class="tc tc-0"></div><div class="tc tc-1"></div><div class="tc tc-0"></div><div class="tc tc-3"></div>
842 </div>
843 </div>
844 </div>
845 <div class="tensor-legend">
846 <span class="tl-item"><span class="tl-swatch tl-unchanged"></span>|&Delta;| = 0</span>
847 <span class="tl-item"><span class="tl-swatch tl-warm"></span>|&Delta;| &le; &epsilon; (within threshold)</span>
848 <span class="tl-item"><span class="tl-swatch tl-hot"></span>|&Delta;| &gt; &epsilon; &rarr; PatchOp</span>
849 </div>
850 </div>
851 <div class="da-note">Configurable &epsilon; threshold. Sparse mode records only changed blocks &mdash; efficient for large tensors.</div>
852 </div>
853
854 <!-- 4. SET -->
855 <div class="da-card">
856 <div class="da-card-hdr">
857 <span class="da-chip da-chip-set">Set</span>
858 <span class="da-algo-name">Set algebra &middot; add / remove</span>
859 </div>
860 <div class="da-domains-row">annotations &middot; tags &middot; gene ontology terms</div>
861 <div class="da-visual">
862 <div class="set-vis">
863 <div class="set-col">
864 <div class="set-header">before</div>
865 <div class="set-members">
866 <span class="set-member set-kept">GO:0001234</span>
867 <span class="set-member set-kept">GO:0005634</span>
868 <span class="set-member set-removed">GO:0006915</span>
869 <span class="set-member set-kept">GO:0016020</span>
870 </div>
871 </div>
872 <div class="set-ops-col">
873 <div class="set-op-line set-op-keep">&mdash;</div>
874 <div class="set-op-line set-op-keep">&mdash;</div>
875 <div class="set-op-line set-op-rm">&times; del</div>
876 <div class="set-op-line set-op-keep">&mdash;</div>
877 <div class="set-op-line set-op-add">+ ins</div>
878 </div>
879 <div class="set-col">
880 <div class="set-header">after</div>
881 <div class="set-members">
882 <span class="set-member set-kept">GO:0001234</span>
883 <span class="set-member set-kept">GO:0005634</span>
884 <span class="set-member set-kept">GO:0016020</span>
885 <span class="set-member set-added">GO:0042592</span>
886 </div>
887 </div>
888 </div>
889 </div>
890 <div class="da-note">Unordered &mdash; no position tracking. Pure membership delta: {removed} and {added}.</div>
891 </div>
892
893 <!-- 5. MAP -->
894 <div class="da-card">
895 <div class="da-card-hdr">
896 <span class="da-chip da-chip-map">Map</span>
897 <span class="da-algo-name">Recursive key-by-key delegation</span>
898 </div>
899 <div class="da-domains-row">metadata &middot; configs &middot; nested structures</div>
900 <div class="da-visual" style="align-items:flex-start">
901 <div class="map-vis">
902 <div class="map-entry map-entry-changed">
903 <span class="map-key">tempo</span>
904 <span class="map-val-before">120</span><span class="map-delta">&nbsp;&rarr;&nbsp;</span><span class="map-val-after">140</span>
905 <span class="map-sub-algo da-chip-tensor">scalar &rarr; PatchOp</span>
906 </div>
907 <div class="map-entry map-entry-changed">
908 <span class="map-key">notes</span>
909 <span class="map-val-before">[&hellip;]</span><span class="map-delta">&nbsp;&rarr;&nbsp;</span><span class="map-val-after">[&hellip;&prime;]</span>
910 <span class="map-sub-algo da-chip-seq">sequence &rarr; LCS</span>
911 </div>
912 <div class="map-entry map-entry-changed">
913 <span class="map-key">tags</span>
914 <span class="map-val-before">{&hellip;}</span><span class="map-delta">&nbsp;&rarr;&nbsp;</span><span class="map-val-after">{&hellip;&prime;}</span>
915 <span class="map-sub-algo da-chip-set">set &rarr; algebra</span>
916 </div>
917 <div class="map-entry map-entry-unchanged">
918 <span class="map-key">author</span>
919 <span class="map-val-before">"Bach"</span><span class="map-delta">&nbsp;=&nbsp;</span><span class="map-val-after">"Bach"</span>
920 <span style="margin-left:auto;font-size:9px;color:var(--dim);font-family:var(--mono)">unchanged</span>
921 </div>
922 </div>
923 </div>
924 <div class="da-note">Each key is diffed by whichever algorithm matches its declared type &mdash; recursively, to arbitrary depth.</div>
925 </div>
926
927 </div><!-- .da-grid -->
928
929 <!-- StructuredDelta taxonomy -->
930 <div class="da-delta-flow">
931 <div class="da-delta-top">
932 <span class="da-delta-label">diff() &rarr; StructuredDelta</span>
933 <span class="da-delta-sub">all five algorithms produce the same typed operation list</span>
934 </div>
935 <div class="da-delta-ops">
936 <span class="da-dop da-dop-ins">InsertOp</span>
937 <span class="da-dop da-dop-del">DeleteOp</span>
938 <span class="da-dop da-dop-mov">MoveOp</span>
939 <span class="da-dop da-dop-rep">ReplaceOp</span>
940 <span class="da-dop da-dop-pat">PatchOp</span>
941 </div>
942 <div class="da-delta-merge">
943 <div class="da-merge-branch da-merge-ot">
944 <div class="da-merge-mode-label">merge_mode: &ldquo;three_way&rdquo;</div>
945 <div class="da-merge-desc">Operational Transformation &mdash; independent ops commute automatically; conflicting ops surface for human resolution</div>
946 </div>
947 <div class="da-merge-divider">or</div>
948 <div class="da-merge-branch da-merge-crdt">
949 <div class="da-merge-mode-label">merge_mode: &ldquo;crdt&rdquo;</div>
950 <div class="da-merge-desc">CRDT join() &mdash; convergent, no coordination required; any two replicas always reach the same state</div>
951 </div>
952 </div>
953 </div>
954
955 </div>
956 </section>
957 """
958
959 # ---------------------------------------------------------------------------
960 # Large HTML template
961 # ---------------------------------------------------------------------------
962
963 _HTML_TEMPLATE = """\
964 <!DOCTYPE html>
965 <html lang="en">
966 <head>
967 <meta charset="utf-8">
968 <meta name="viewport" content="width=device-width, initial-scale=1">
969 <title>Muse — Version Anything</title>
970 <style>
971 *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
972 :root {
973 --bg: #0d1117;
974 --bg2: #161b22;
975 --bg3: #21262d;
976 --border: #30363d;
977 --text: #e6edf3;
978 --mute: #8b949e;
979 --dim: #484f58;
980 --accent: #4f8ef7;
981 --accent2: #58a6ff;
982 --green: #3fb950;
983 --red: #f85149;
984 --yellow: #d29922;
985 --purple: #bc8cff;
986 --mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
987 --ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
988 --r: 8px;
989 }
990 html { scroll-behavior: smooth; }
991 body {
992 background: var(--bg);
993 color: var(--text);
994 font-family: var(--ui);
995 font-size: 15px;
996 line-height: 1.7;
997 }
998 a { color: var(--accent2); text-decoration: none; }
999 a:hover { text-decoration: underline; }
1000 code {
1001 font-family: var(--mono);
1002 font-size: 0.88em;
1003 background: var(--bg3);
1004 border: 1px solid var(--border);
1005 border-radius: 4px;
1006 padding: 1px 6px;
1007 }
1008
1009 /* ---- Hero ---- */
1010 .hero {
1011 background: linear-gradient(160deg, #0d1117 0%, #161b22 50%, #0d1117 100%);
1012 border-bottom: 1px solid var(--border);
1013 padding: 80px 40px 100px;
1014 text-align: center;
1015 position: relative;
1016 overflow: hidden;
1017 }
1018 .hero::before {
1019 content: '';
1020 position: absolute;
1021 inset: 0;
1022 background:
1023 radial-gradient(ellipse 60% 40% at 20% 50%, rgba(79,142,247,0.07) 0%, transparent 70%),
1024 radial-gradient(ellipse 50% 40% at 80% 50%, rgba(188,140,255,0.06) 0%, transparent 70%);
1025 pointer-events: none;
1026 }
1027 .hero-wordmark {
1028 font-family: var(--ui);
1029 font-size: clamp(72px, 11vw, 130px);
1030 font-weight: 800;
1031 letter-spacing: -5px;
1032 line-height: 1;
1033 margin-bottom: 12px;
1034 background: linear-gradient(90deg, #6ea8fe 0%, #a78bfa 50%, #c084fc 100%);
1035 -webkit-background-clip: text;
1036 -webkit-text-fill-color: transparent;
1037 background-clip: text;
1038 }
1039 .hero-version-any {
1040 font-size: clamp(18px, 2.8vw, 26px);
1041 font-weight: 700;
1042 color: #ffffff;
1043 letter-spacing: 6px;
1044 text-transform: uppercase;
1045 margin-bottom: 32px;
1046 }
1047 .hero-sub {
1048 font-size: 18px;
1049 color: var(--mute);
1050 max-width: 600px;
1051 margin: 0 auto 40px;
1052 line-height: 1.6;
1053 }
1054 .hero-sub strong { color: var(--text); }
1055 .hero-cta-row {
1056 display: flex;
1057 gap: 12px;
1058 justify-content: center;
1059 flex-wrap: wrap;
1060 }
1061 .btn-primary {
1062 background: var(--accent);
1063 color: #fff;
1064 font-weight: 600;
1065 padding: 12px 28px;
1066 border-radius: var(--r);
1067 font-size: 15px;
1068 border: none;
1069 cursor: pointer;
1070 text-decoration: none;
1071 transition: opacity 0.15s, transform 0.1s;
1072 display: inline-block;
1073 }
1074 .btn-primary:hover { opacity: 0.88; transform: translateY(-1px); text-decoration: none; }
1075 .btn-outline {
1076 background: transparent;
1077 color: var(--text);
1078 font-weight: 500;
1079 padding: 12px 28px;
1080 border-radius: var(--r);
1081 font-size: 15px;
1082 border: 1px solid var(--border);
1083 cursor: pointer;
1084 text-decoration: none;
1085 display: inline-block;
1086 transition: border-color 0.15s, color 0.15s;
1087 }
1088 .btn-outline:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
1089
1090 /* ---- Domain ticker ---- */
1091 .domain-ticker {
1092 margin: 32px auto 0;
1093 max-width: 700px;
1094 overflow: hidden;
1095 position: relative;
1096 height: 34px;
1097 }
1098 .domain-ticker::before,
1099 .domain-ticker::after {
1100 content: '';
1101 position: absolute;
1102 top: 0; bottom: 0;
1103 width: 60px;
1104 z-index: 2;
1105 }
1106 .domain-ticker::before { left: 0; background: linear-gradient(90deg, var(--bg), transparent); }
1107 .domain-ticker::after { right: 0; background: linear-gradient(-90deg, var(--bg), transparent); }
1108 .ticker-track {
1109 display: flex;
1110 gap: 10px;
1111 animation: ticker-scroll 18s linear infinite;
1112 width: max-content;
1113 }
1114 @keyframes ticker-scroll {
1115 0% { transform: translateX(0); }
1116 100% { transform: translateX(-50%); }
1117 }
1118 .ticker-item {
1119 font-family: var(--mono);
1120 font-size: 13px;
1121 padding: 4px 14px;
1122 border-radius: 20px;
1123 border: 1px solid var(--border);
1124 white-space: nowrap;
1125 color: var(--mute);
1126 }
1127 .ticker-item.active { border-color: rgba(79,142,247,0.5); color: var(--accent2); background: rgba(79,142,247,0.08); }
1128
1129 /* ---- Sections ---- */
1130 section { padding: 72px 40px; border-top: 1px solid var(--border); }
1131 .inner { max-width: 1100px; margin: 0 auto; }
1132 .section-eyebrow {
1133 font-family: var(--mono);
1134 font-size: 11px;
1135 color: var(--accent2);
1136 letter-spacing: 2px;
1137 text-transform: uppercase;
1138 margin-bottom: 10px;
1139 }
1140 section h2 {
1141 font-size: 32px;
1142 font-weight: 700;
1143 letter-spacing: -0.5px;
1144 margin-bottom: 12px;
1145 }
1146 .section-lead {
1147 font-size: 16px;
1148 color: var(--mute);
1149 max-width: 620px;
1150 margin-bottom: 48px;
1151 line-height: 1.7;
1152 }
1153 .section-lead strong { color: var(--text); }
1154
1155 /* ---- Base icon ---- */
1156 .icon {
1157 display: inline-block;
1158 vertical-align: -0.15em;
1159 flex-shrink: 0;
1160 }
1161 .ticker-item .icon { width: 13px; height: 13px; vertical-align: -0.1em; }
1162 .cap-showcase-badge .icon { width: 13px; height: 13px; vertical-align: -0.1em; }
1163
1164 /* ---- Protocol two-col layout ---- */
1165 .proto-layout {
1166 display: grid;
1167 grid-template-columns: 148px 1fr;
1168 gap: 0;
1169 border: 1px solid var(--border);
1170 border-radius: var(--r);
1171 overflow: hidden;
1172 margin-bottom: 40px;
1173 align-items: stretch;
1174 }
1175 @media (max-width: 640px) {
1176 .proto-layout { grid-template-columns: 1fr; }
1177 .stat-strip { border-right: none; border-bottom: 1px solid var(--border); }
1178 }
1179
1180 /* ---- Stat strip (left column) ---- */
1181 .stat-strip {
1182 display: flex;
1183 flex-direction: column;
1184 border-right: 1px solid var(--border);
1185 }
1186 .stat-cell {
1187 flex: 1;
1188 padding: 18px 20px;
1189 border-bottom: 1px solid var(--border);
1190 text-align: center;
1191 display: flex;
1192 flex-direction: column;
1193 align-items: center;
1194 justify-content: center;
1195 }
1196 .stat-cell:last-child { border-bottom: none; }
1197 .stat-num {
1198 font-family: var(--mono);
1199 font-size: 26px;
1200 font-weight: 700;
1201 color: var(--accent2);
1202 display: block;
1203 line-height: 1.1;
1204 }
1205 .stat-lbl { font-size: 11px; color: var(--mute); margin-top: 4px; line-height: 1.3; }
1206
1207 /* ---- Protocol table (right column) ---- */
1208 .proto-table {
1209 overflow: hidden;
1210 }
1211 .proto-row {
1212 display: grid;
1213 grid-template-columns: 90px 240px 1fr;
1214 border-bottom: 1px solid var(--border);
1215 }
1216 .proto-row:last-child { border-bottom: none; }
1217 .proto-row.hdr { background: var(--bg3); }
1218 .proto-row > div { padding: 11px 16px; }
1219 .proto-method { font-family: var(--mono); font-size: 13px; color: var(--accent2); font-weight: 600; }
1220 .proto-sig { font-family: var(--mono); font-size: 12px; color: var(--mute); }
1221 .proto-desc { font-size: 13px; color: var(--mute); }
1222 .proto-row.hdr .proto-method,
1223 .proto-row.hdr .proto-sig,
1224 .proto-row.hdr .proto-desc { font-family: var(--ui); font-size: 11px; font-weight: 600; color: var(--dim); text-transform: uppercase; letter-spacing: 0.6px; }
1225
1226 /* ---- Engine capability showcase ---- */
1227 .cap-showcase-grid {
1228 display: grid;
1229 grid-template-columns: repeat(auto-fill, minmax(480px, 1fr));
1230 gap: 24px;
1231 }
1232 @media (max-width: 600px) { .cap-showcase-grid { grid-template-columns: 1fr; } }
1233 .cap-showcase-card {
1234 border: 1px solid var(--border);
1235 border-top: 3px solid var(--cap-color, var(--accent));
1236 border-radius: var(--r);
1237 background: var(--bg);
1238 overflow: hidden;
1239 transition: transform 0.15s;
1240 }
1241 .cap-showcase-card:hover { transform: translateY(-2px); }
1242 .cap-showcase-header {
1243 padding: 14px 18px;
1244 border-bottom: 1px solid var(--border);
1245 background: var(--bg2);
1246 display: flex;
1247 align-items: center;
1248 gap: 12px;
1249 flex-wrap: wrap;
1250 }
1251 .cap-showcase-badge {
1252 font-size: 12px;
1253 font-family: var(--mono);
1254 padding: 3px 10px;
1255 border-radius: 4px;
1256 border: 1px solid;
1257 white-space: nowrap;
1258 }
1259 .cap-showcase-sub {
1260 font-size: 12px;
1261 color: var(--mute);
1262 font-style: italic;
1263 }
1264 .cap-showcase-body { padding: 16px 18px; }
1265 .cap-showcase-desc {
1266 font-size: 13px;
1267 color: var(--mute);
1268 margin-bottom: 14px;
1269 line-height: 1.6;
1270 }
1271 .cap-showcase-desc strong { color: var(--text); }
1272 .cap-showcase-output {
1273 background: #0a0e14;
1274 border: 1px solid var(--border);
1275 border-radius: 5px;
1276 padding: 12px 14px;
1277 font-family: var(--mono);
1278 font-size: 11.5px;
1279 color: #abb2bf;
1280 white-space: pre;
1281 overflow-x: auto;
1282 line-height: 1.65;
1283 }
1284 /* ---- Diff Algebra section ---- */
1285 .da-grid {
1286 display: grid;
1287 grid-template-columns: repeat(2, 1fr);
1288 gap: 20px;
1289 margin-bottom: 36px;
1290 }
1291 .da-seq-card { grid-column: span 2; }
1292 @media (max-width: 720px) {
1293 .da-grid { grid-template-columns: 1fr; }
1294 .da-seq-card { grid-column: span 1; }
1295 }
1296 .da-card {
1297 background: var(--bg2);
1298 border: 1px solid var(--border);
1299 border-radius: var(--r);
1300 padding: 18px;
1301 display: flex;
1302 flex-direction: column;
1303 gap: 10px;
1304 }
1305 .da-card-hdr { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
1306 .da-chip {
1307 font-family: var(--mono);
1308 font-size: 10.5px;
1309 font-weight: 700;
1310 padding: 3px 10px;
1311 border-radius: 4px;
1312 border: 1px solid;
1313 }
1314 .da-chip-seq { color:#58a6ff; background:rgba(88,166,255,0.1); border-color:rgba(88,166,255,0.3); }
1315 .da-chip-tree { color:#f9a825; background:rgba(249,168,37,0.1); border-color:rgba(249,168,37,0.3); }
1316 .da-chip-tensor { color:#ab47bc; background:rgba(171,71,188,0.1); border-color:rgba(171,71,188,0.3); }
1317 .da-chip-set { color:#3fb950; background:rgba(63,185,80,0.1); border-color:rgba(63,185,80,0.3); }
1318 .da-chip-map { color:#ef5350; background:rgba(239,83,80,0.1); border-color:rgba(239,83,80,0.3); }
1319 .da-algo-name { font-size:12px; color:var(--mute); font-family:var(--mono); }
1320 .da-domains-row { font-size:11px; color:var(--dim); }
1321 .da-visual {
1322 background: #0a0e14;
1323 border: 1px solid var(--border);
1324 border-radius: 5px;
1325 padding: 14px;
1326 flex: 1;
1327 min-height: 130px;
1328 display: flex;
1329 align-items: center;
1330 justify-content: center;
1331 }
1332 .da-note { font-size:11.5px; color:var(--mute); line-height:1.5; }
1333
1334 /* Sequence LCS */
1335 .seq-vis { display:flex; flex-direction:column; gap:6px; width:100%; }
1336 .seq-row-lbl { font-family:var(--mono); font-size:9px; color:var(--dim); text-transform:uppercase; letter-spacing:.6px; }
1337 .seq-row { display:flex; gap:6px; flex-wrap:wrap; }
1338 .seq-blk {
1339 display:flex; flex-direction:column; align-items:center;
1340 padding:5px 9px; border-radius:5px; border:1.5px solid;
1341 min-width:46px; gap:2px; transition:transform .15s;
1342 }
1343 .seq-blk:hover { transform:translateY(-2px); }
1344 .seq-hash { font-family:var(--mono); font-size:7.5px; opacity:.55; }
1345 .seq-name { font-family:var(--mono); font-size:14px; font-weight:700; }
1346 .seq-match { background:rgba(88,166,255,.07); border-color:rgba(88,166,255,.25); color:#58a6ff; }
1347 .seq-del { background:rgba(239,83,80,.1); border-color:rgba(239,83,80,.35); color:#ef5350; text-decoration:line-through; }
1348 .seq-ins { background:rgba(63,185,80,.1); border-color:rgba(63,185,80,.35); color:#3fb950; }
1349 .seq-moved-from{ background:rgba(188,140,255,.06); border-color:rgba(188,140,255,.25);color:#bc8cff; opacity:.55; }
1350 .seq-moved-to { background:rgba(188,140,255,.12); border-color:rgba(188,140,255,.5); color:#bc8cff; }
1351 .seq-ops-row { display:flex; gap:6px; padding:2px 0; flex-wrap:wrap; }
1352 .seq-op-cell { min-width:46px; display:flex; justify-content:center; }
1353 .da-op { font-size:9px; font-weight:700; letter-spacing:.3px; padding:1px 5px; border-radius:3px; white-space:nowrap; }
1354 .da-op-match { color:var(--dim); }
1355 .da-op-delete { color:#ef5350; background:rgba(239,83,80,.1); }
1356 .da-op-insert { color:#3fb950; background:rgba(63,185,80,.1); }
1357 .da-op-move { color:#bc8cff; background:rgba(188,140,255,.1); }
1358
1359 /* Tree SVG */
1360 .tree-vis { width:100%; height:130px; overflow:visible; }
1361
1362 /* Tensor */
1363 .tensor-wrap { display:flex; align-items:center; gap:14px; }
1364 .tensor-panel { display:flex; flex-direction:column; align-items:center; gap:5px; }
1365 .tensor-label { font-family:var(--mono); font-size:9px; color:var(--dim); }
1366 .tensor-grid { display:grid; grid-template-columns:repeat(4,26px); gap:3px; }
1367 .tc { width:26px; height:26px; border-radius:3px; }
1368 .tc-0 { background:#141c28; }
1369 .tc-1 { background:#1a2538; }
1370 .tc-2 { background:#1d2f50; }
1371 .tc-3 { background:#1a3060; }
1372 .tc-warm1 { background:rgba(249,168,37,.28); border:1px solid rgba(249,168,37,.4); }
1373 .tc-hot2 { background:rgba(239,83,80,.45); border:1px solid rgba(239,83,80,.65); }
1374 .tc-hot3 { background:rgba(239,83,80,.65); border:1px solid rgba(239,83,80,.9); }
1375 .tensor-arrow { font-size:22px; color:var(--dim); }
1376 .tensor-legend { display:flex; flex-direction:column; gap:5px; margin-top:4px; }
1377 .tl-item { display:flex; align-items:center; gap:5px; font-size:9px; color:var(--mute); }
1378 .tl-swatch { width:12px; height:12px; border-radius:2px; flex-shrink:0; }
1379 .tl-unchanged { background:#141c28; }
1380 .tl-warm { background:rgba(249,168,37,.28); border:1px solid rgba(249,168,37,.4); }
1381 .tl-hot { background:rgba(239,83,80,.65); border:1px solid rgba(239,83,80,.9); }
1382
1383 /* Set algebra */
1384 .set-vis { display:grid; grid-template-columns:1fr auto 1fr; gap:8px; align-items:start; width:100%; }
1385 .set-col .set-header { font-family:var(--mono); font-size:9px; color:var(--dim); text-transform:uppercase; letter-spacing:.6px; margin-bottom:6px; }
1386 .set-members { display:flex; flex-direction:column; gap:4px; }
1387 .set-member { font-family:var(--mono); font-size:10px; padding:3px 8px; border-radius:3px; border:1px solid; }
1388 .set-kept { color:var(--mute); border-color:var(--border); background:var(--bg3); }
1389 .set-removed { color:#ef5350; border-color:rgba(239,83,80,.35); background:rgba(239,83,80,.07); text-decoration:line-through; }
1390 .set-added { color:#3fb950; border-color:rgba(63,185,80,.35); background:rgba(63,185,80,.07); }
1391 .set-ops-col { display:flex; flex-direction:column; gap:4px; padding-top:21px; }
1392 .set-op-line { font-size:10px; font-weight:700; white-space:nowrap; height:26px; display:flex; align-items:center; }
1393 .set-op-keep { color:var(--dim); }
1394 .set-op-rm { color:#ef5350; }
1395 .set-op-add { color:#3fb950; }
1396
1397 /* Map recursive */
1398 .map-vis { display:flex; flex-direction:column; gap:5px; width:100%; font-family:var(--mono); }
1399 .map-entry { display:flex; align-items:center; gap:6px; padding:5px 8px; border-radius:4px; font-size:11px; flex-wrap:wrap; }
1400 .map-entry-changed { background:rgba(88,166,255,.04); border:1px solid rgba(88,166,255,.15); }
1401 .map-entry-unchanged { background:var(--bg3); border:1px solid var(--border); opacity:.65; }
1402 .map-key { color:#61afef; font-weight:700; min-width:52px; }
1403 .map-delta { color:var(--dim); }
1404 .map-val-before { color:var(--dim); font-size:10px; }
1405 .map-val-after { color:var(--text); font-size:10px; }
1406 .map-sub-algo { margin-left:auto; font-size:9px; font-weight:700; padding:1px 6px; border-radius:3px; border:1px solid; }
1407
1408 /* StructuredDelta flow */
1409 .da-delta-flow {
1410 background: var(--bg2);
1411 border: 1px solid var(--border);
1412 border-radius: var(--r);
1413 padding: 24px;
1414 }
1415 .da-delta-top { margin-bottom:14px; display:flex; align-items:baseline; gap:12px; flex-wrap:wrap; }
1416 .da-delta-label { font-family:var(--mono); font-size:15px; font-weight:700; color:var(--text); }
1417 .da-delta-sub { font-size:12px; color:var(--mute); }
1418 .da-delta-ops { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:18px; }
1419 .da-dop { font-family:var(--mono); font-size:12px; font-weight:700; padding:5px 14px; border-radius:5px; border:1px solid; }
1420 .da-dop-ins { color:#3fb950; background:rgba(63,185,80,.1); border-color:rgba(63,185,80,.3); }
1421 .da-dop-del { color:#ef5350; background:rgba(239,83,80,.1); border-color:rgba(239,83,80,.3); }
1422 .da-dop-mov { color:#bc8cff; background:rgba(188,140,255,.1);border-color:rgba(188,140,255,.3); }
1423 .da-dop-rep { color:#f9a825; background:rgba(249,168,37,.1); border-color:rgba(249,168,37,.3); }
1424 .da-dop-pat { color:#58a6ff; background:rgba(88,166,255,.1); border-color:rgba(88,166,255,.3); }
1425 .da-delta-merge { display:grid; grid-template-columns:1fr auto 1fr; gap:14px; align-items:center; }
1426 @media (max-width: 640px) { .da-delta-merge { grid-template-columns:1fr; } .da-merge-divider { text-align:center; } }
1427 .da-merge-branch { padding:14px 16px; border-radius:6px; border:1px solid; }
1428 .da-merge-ot { border-color:rgba(239,83,80,.3); background:rgba(239,83,80,.04); }
1429 .da-merge-crdt { border-color:rgba(188,140,255,.3); background:rgba(188,140,255,.04); }
1430 .da-merge-mode-label { font-family:var(--mono); font-size:11px; font-weight:700; margin-bottom:5px; }
1431 .da-merge-ot .da-merge-mode-label { color:#ef5350; }
1432 .da-merge-crdt .da-merge-mode-label { color:#bc8cff; }
1433 .da-merge-desc { font-size:11.5px; color:var(--mute); line-height:1.5; }
1434 .da-merge-divider { color:var(--dim); font-size:13px; font-weight:700; }
1435
1436 /* ---- CRDT visualizations ---- */
1437 .crdt-vis { display:flex; flex-direction:column; gap:10px; width:100%; }
1438 /* concurrent replicas row */
1439 .crdt-concurrent { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
1440 .crdt-rep {
1441 background:var(--bg3); border:1px solid var(--border);
1442 border-radius:5px; padding:9px 10px;
1443 display:flex; flex-direction:column; gap:4px;
1444 min-width:0; overflow:hidden;
1445 }
1446 .crdt-rep-hdr { font-family:var(--mono); font-size:9px; font-weight:700; color:var(--dim); text-transform:uppercase; letter-spacing:.6px; }
1447 .crdt-op { font-family:var(--mono); font-size:10px; padding:2px 6px; border-radius:3px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%; }
1448 .crdt-op-add { color:#3fb950; background:rgba(63,185,80,.1); }
1449 .crdt-op-rm { color:#ef5350; background:rgba(239,83,80,.1); }
1450 .crdt-rep-state { font-family:var(--mono); font-size:11px; color:var(--mute); margin-top:2px; }
1451 /* join result row */
1452 .crdt-join { display:flex; align-items:center; gap:8px; flex-wrap:wrap; padding:8px 10px; background:var(--bg3); border-radius:5px; border:1px solid var(--border); }
1453 .crdt-join-label { font-family:var(--mono); font-size:10px; color:var(--dim); white-space:nowrap; }
1454 .crdt-join-val { font-family:var(--mono); font-size:13px; font-weight:700; }
1455 .crdt-join-rule { font-size:10px; color:var(--mute); font-style:italic; margin-left:auto; }
1456 /* LWW timeline */
1457 .crdt-writes { display:flex; flex-direction:column; gap:5px; }
1458 .crdt-write { display:flex; align-items:center; gap:8px; padding:7px 10px; border-radius:5px; border:1px solid var(--border); background:var(--bg3); font-family:var(--mono); font-size:11px; }
1459 .crdt-write-winner { border-color:rgba(88,166,255,.4); background:rgba(88,166,255,.05); }
1460 .crdt-time { font-size:9px; color:var(--dim); min-width:32px; }
1461 .crdt-agent { font-size:9px; font-weight:700; padding:1px 6px; border-radius:3px; min-width:18px; text-align:center; }
1462 .crdt-agent-a { background:rgba(249,168,37,.15); color:#f9a825; }
1463 .crdt-agent-b { background:rgba(88,166,255,.15); color:#58a6ff; }
1464 .crdt-wval { color:var(--text); flex:1; }
1465 .crdt-latest { font-size:9px; font-weight:700; color:#58a6ff; background:rgba(88,166,255,.12); padding:1px 6px; border-radius:10px; white-space:nowrap; }
1466 /* GCounter bars */
1467 .crdt-gcounter { display:flex; flex-direction:column; gap:7px; }
1468 .crdt-gc-row { display:flex; align-items:center; gap:8px; }
1469 .crdt-bar { flex:1; height:22px; background:var(--bg3); border-radius:4px; overflow:hidden; }
1470 .crdt-bar-fill { height:100%; display:flex; align-items:center; justify-content:flex-end; padding-right:7px; font-family:var(--mono); font-size:11px; font-weight:700; border-radius:4px; transition:width .4s; }
1471 .crdt-bar-a { background:rgba(249,168,37,.35); color:#f9a825; }
1472 .crdt-bar-b { background:rgba(88,166,255,.35); color:#58a6ff; }
1473 /* VectorClock grid */
1474 .crdt-vclocks { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
1475 .crdt-vc { display:flex; flex-direction:column; gap:4px; }
1476 .crdt-vc-hdr { font-family:var(--mono); font-size:9px; color:var(--dim); text-transform:uppercase; letter-spacing:.5px; }
1477 .crdt-vc-cells { display:flex; gap:4px; }
1478 .crdt-vc-cell { font-family:var(--mono); font-size:10.5px; font-weight:700; padding:4px 8px; border-radius:4px; border:1px solid; }
1479 .crdt-vc-self { color:#f9a825; background:rgba(249,168,37,.1); border-color:rgba(249,168,37,.35); }
1480 .crdt-vc-zero { color:var(--dim); background:var(--bg3); border-color:var(--border); }
1481 .crdt-vc-max { color:#3fb950; background:rgba(63,185,80,.1); border-color:rgba(63,185,80,.35); }
1482 .crdt-vc-sep { font-size:16px; color:var(--dim); padding:0 2px; align-self:center; margin-top:14px; }
1483 .crdt-concurrent-badge { font-family:var(--mono); font-size:10px; color:#f9a825; background:rgba(249,168,37,.08); border:1px solid rgba(249,168,37,.25); border-radius:4px; padding:3px 10px; align-self:flex-start; }
1484
1485 /* ---- OT Merge scenario cards ---- */
1486 .ot-scenarios { display: flex; flex-direction: column; gap: 10px; }
1487 .ot-scenario {
1488 background: var(--bg);
1489 border: 1px solid var(--border);
1490 border-left: 3px solid transparent;
1491 border-radius: 6px;
1492 padding: 12px 14px;
1493 display: flex;
1494 flex-direction: column;
1495 gap: 9px;
1496 }
1497 .ot-clean { border-left-color: #3fb950; }
1498 .ot-conflict { border-left-color: #ef5350; }
1499 .ot-scenario-hdr { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
1500 .ot-scenario-label {
1501 font-family: var(--mono);
1502 font-size: 9.5px;
1503 font-weight: 700;
1504 text-transform: uppercase;
1505 letter-spacing: 1px;
1506 color: var(--dim);
1507 }
1508 .ot-scenario-title { font-size: 11.5px; color: var(--mute); }
1509 .ot-ops { display: flex; flex-direction: column; gap: 5px; }
1510 .ot-op {
1511 display: flex;
1512 align-items: center;
1513 gap: 7px;
1514 font-family: var(--mono);
1515 font-size: 11.5px;
1516 flex-wrap: wrap;
1517 }
1518 .ot-op-side {
1519 font-size: 9px;
1520 font-weight: 700;
1521 color: var(--dim);
1522 background: var(--bg3);
1523 padding: 1px 6px;
1524 border-radius: 3px;
1525 min-width: 34px;
1526 text-align: center;
1527 }
1528 .ot-op-type { font-weight: 700; padding: 1px 7px; border-radius: 3px; font-size: 10.5px; }
1529 .ot-insert { background: rgba(63,185,80,0.13); color: #3fb950; }
1530 .ot-replace { background: rgba(249,168,37,0.13); color: #f9a825; }
1531 .ot-op-addr { color: #98c379; }
1532 .ot-op-meta { color: var(--dim); font-size: 10.5px; }
1533 .ot-result {
1534 display: flex;
1535 align-items: center;
1536 justify-content: space-between;
1537 flex-wrap: wrap;
1538 gap: 8px;
1539 padding-top: 9px;
1540 border-top: 1px solid var(--border);
1541 }
1542 .ot-reason { font-family: var(--mono); font-size: 11px; color: var(--mute); }
1543 .ot-badge {
1544 display: inline-flex;
1545 align-items: center;
1546 gap: 5px;
1547 font-size: 11px;
1548 font-weight: 700;
1549 padding: 3px 10px;
1550 border-radius: 12px;
1551 white-space: nowrap;
1552 }
1553 .ot-badge .icon { width: 11px; height: 11px; vertical-align: -0.05em; }
1554 .ot-badge-clean { background: rgba(63,185,80,0.1); color: #3fb950; border: 1px solid rgba(63,185,80,0.3); }
1555 .ot-badge-conflict { background: rgba(239,83,80,0.1); color: #ef5350; border: 1px solid rgba(239,83,80,0.3); }
1556
1557 .cap-showcase-domain-grid {
1558 display: flex;
1559 flex-direction: column;
1560 gap: 10px;
1561 }
1562 .crdt-mini-grid {
1563 display: grid;
1564 grid-template-columns: 1fr 1fr;
1565 gap: 10px;
1566 }
1567 @media (max-width: 700px) { .crdt-mini-grid { grid-template-columns: 1fr; } }
1568
1569 /* ---- Three steps ---- */
1570 .steps-grid {
1571 display: grid;
1572 grid-template-columns: repeat(3, 1fr);
1573 gap: 24px;
1574 }
1575 @media (max-width: 800px) { .steps-grid { grid-template-columns: 1fr; } }
1576 .step-card {
1577 border: 1px solid var(--border);
1578 border-radius: var(--r);
1579 background: var(--bg2);
1580 padding: 24px;
1581 position: relative;
1582 }
1583 .step-num {
1584 font-family: var(--mono);
1585 font-size: 11px;
1586 color: var(--accent);
1587 font-weight: 700;
1588 text-transform: uppercase;
1589 letter-spacing: 1px;
1590 margin-bottom: 10px;
1591 }
1592 .step-title { font-size: 17px; font-weight: 700; margin-bottom: 8px; }
1593 .step-desc { font-size: 13px; color: var(--mute); line-height: 1.6; margin-bottom: 16px; }
1594 .step-cmd {
1595 font-family: var(--mono);
1596 font-size: 12px;
1597 background: var(--bg3);
1598 border: 1px solid var(--border);
1599 border-radius: 5px;
1600 padding: 10px 14px;
1601 color: var(--accent2);
1602 }
1603
1604 /* ---- Code block ---- */
1605 .code-wrap {
1606 border: 1px solid var(--border);
1607 border-radius: var(--r);
1608 overflow: hidden;
1609 }
1610 .code-bar {
1611 background: var(--bg3);
1612 border-bottom: 1px solid var(--border);
1613 padding: 8px 16px;
1614 display: flex;
1615 align-items: center;
1616 gap: 8px;
1617 }
1618 .code-bar-dot {
1619 width: 10px; height: 10px; border-radius: 50%;
1620 }
1621 .code-bar-title {
1622 font-family: var(--mono);
1623 font-size: 12px;
1624 color: var(--mute);
1625 margin-left: 6px;
1626 }
1627 .code-body {
1628 background: #0a0e14;
1629 padding: 20px 24px;
1630 font-family: var(--mono);
1631 font-size: 12.5px;
1632 line-height: 1.7;
1633 color: #abb2bf;
1634 white-space: pre;
1635 overflow-x: auto;
1636 }
1637 /* Simple syntax highlights */
1638 .kw { color: #c678dd; }
1639 .kw2 { color: #e06c75; }
1640 .fn { color: #61afef; }
1641 .str { color: #98c379; }
1642 .cmt { color: #5c6370; font-style: italic; }
1643 .cls { color: #e5c07b; }
1644 .typ { color: #56b6c2; }
1645
1646 /* ---- Active domains grid ---- */
1647 .domain-grid {
1648 display: grid;
1649 grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
1650 gap: 20px;
1651 }
1652 .domain-card {
1653 border: 1px solid var(--border);
1654 border-radius: var(--r);
1655 background: var(--bg2);
1656 overflow: hidden;
1657 transition: border-color 0.2s, transform 0.15s;
1658 }
1659 .domain-card:hover { border-color: var(--accent); transform: translateY(-2px); }
1660 .domain-card.active-domain { border-color: rgba(63,185,80,0.4); }
1661 .domain-card-hdr {
1662 background: var(--bg3);
1663 padding: 12px 16px;
1664 border-bottom: 1px solid var(--border);
1665 display: flex;
1666 align-items: center;
1667 gap: 10px;
1668 }
1669 .active-badge { font-size: 11px; padding: 2px 8px; border-radius: 4px; background: rgba(63,185,80,0.12); border: 1px solid rgba(63,185,80,0.3); color: var(--green); font-family: var(--mono); }
1670 .reg-badge { font-size: 11px; padding: 2px 8px; border-radius: 4px; background: var(--bg); border: 1px solid var(--border); color: var(--mute); font-family: var(--mono); }
1671 .active-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); margin-left: auto; }
1672 .domain-name-lg { font-family: var(--mono); font-size: 16px; font-weight: 700; color: var(--text); }
1673 .domain-card-body { padding: 16px; }
1674 .domain-desc { font-size: 13px; color: var(--mute); margin-bottom: 12px; }
1675 .cap-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
1676 .cap-pill { font-size: 10px; padding: 2px 8px; border-radius: 12px; border: 1px solid var(--border); color: var(--mute); background: var(--bg3); }
1677 .cap-pill.cap-crdt { border-color: rgba(188,140,255,0.4); color: var(--purple); background: rgba(188,140,255,0.08); }
1678 .cap-pill.cap-ot-merge { border-color: rgba(88,166,255,0.4); color: var(--accent2); background: rgba(88,166,255,0.08); }
1679 .cap-pill.cap-domain-schema { border-color: rgba(63,185,80,0.4); color: var(--green); background: rgba(63,185,80,0.08); }
1680 .cap-pill.cap-typed-deltas { border-color: rgba(249,168,37,0.4); color: #f9a825; background: rgba(249,168,37,0.08); }
1681 .dim-row { font-size: 11px; color: var(--dim); }
1682 .dim-tag { color: var(--mute); }
1683
1684 /* ---- Planned domains ---- */
1685 .planned-grid {
1686 display: grid;
1687 grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
1688 gap: 16px;
1689 }
1690 .planned-card {
1691 border: 1px solid var(--border);
1692 border-radius: var(--r);
1693 background: var(--bg2);
1694 padding: 20px 16px;
1695 display: flex;
1696 flex-direction: column;
1697 gap: 8px;
1698 transition: border-color 0.2s, transform 0.15s;
1699 }
1700 .planned-card:hover { border-color: var(--card-accent,var(--accent)); transform: translateY(-2px); }
1701 .planned-card.yours { border: 2px dashed var(--accent); background: rgba(79,142,247,0.04); }
1702 .planned-icon { line-height: 0; }
1703 .planned-icon .icon { width: 28px; height: 28px; }
1704 .planned-name { font-size: 15px; font-weight: 700; color: var(--text); }
1705 .planned-tag { font-size: 12px; color: var(--mute); line-height: 1.5; }
1706 .planned-dims { font-size: 10px; color: var(--dim); }
1707 .coming-soon { font-size: 10px; color: var(--dim); border: 1px solid var(--border); border-radius: 12px; padding: 2px 8px; display: inline-block; margin-top: 4px; }
1708 .cta-btn {
1709 display: inline-block;
1710 margin-top: 6px;
1711 font-size: 12px;
1712 font-weight: 600;
1713 color: var(--accent2);
1714 border: 1px solid rgba(88,166,255,0.4);
1715 border-radius: 4px;
1716 padding: 4px 12px;
1717 text-decoration: none;
1718 transition: background 0.15s;
1719 }
1720 .cta-btn:hover { background: rgba(88,166,255,0.1); text-decoration: none; }
1721
1722 /* ---- Distribution tiers ---- */
1723 .dist-grid {
1724 display: grid;
1725 grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
1726 gap: 24px;
1727 }
1728 .dist-card {
1729 border: 1px solid var(--border);
1730 border-top: 3px solid var(--dist-color, var(--accent));
1731 border-radius: var(--r);
1732 background: var(--bg2);
1733 padding: 24px;
1734 transition: transform 0.15s;
1735 }
1736 .dist-card:hover { transform: translateY(-2px); }
1737 .dist-header { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 14px; }
1738 .dist-icon { line-height: 0; flex-shrink: 0; }
1739 .dist-icon .icon { width: 26px; height: 26px; }
1740 .dist-tier { font-family: var(--mono); font-size: 11px; color: var(--dist-color,var(--accent)); letter-spacing: 1px; text-transform: uppercase; font-weight: 700; }
1741 .dist-title { font-size: 14px; font-weight: 600; color: var(--text); margin-top: 2px; }
1742 .dist-desc { font-size: 13px; color: var(--mute); margin-bottom: 16px; line-height: 1.6; }
1743 .dist-steps { list-style: none; counter-reset: step; display: flex; flex-direction: column; gap: 6px; }
1744 .dist-steps li { counter-increment: step; display: flex; align-items: flex-start; gap: 8px; font-size: 12px; color: var(--mute); }
1745 .dist-steps li::before { content: counter(step); min-width: 18px; height: 18px; background: var(--dist-color,var(--accent)); color: #000; border-radius: 50%; font-size: 10px; font-weight: 700; display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 1px; }
1746 .dist-steps code { background: var(--bg3); border: 1px solid var(--border); border-radius: 4px; padding: 1px 6px; font-size: 11px; }
1747
1748 /* ---- MuseHub teaser ---- */
1749 .musehub-section {
1750 background: linear-gradient(135deg, #0d1117 0%, #1a0d2e 50%, #0d1117 100%);
1751 padding: 80px 40px;
1752 text-align: center;
1753 border-top: 1px solid var(--border);
1754 }
1755 .musehub-logo {
1756 margin-bottom: 20px;
1757 line-height: 0;
1758 }
1759 .musehub-logo .icon { width: 48px; height: 48px; stroke: #bc8cff; }
1760 .musehub-section h2 {
1761 font-size: 36px;
1762 font-weight: 800;
1763 letter-spacing: -1px;
1764 margin-bottom: 12px;
1765 }
1766 .musehub-section h2 span {
1767 background: linear-gradient(135deg, #bc8cff, #4f8ef7);
1768 -webkit-background-clip: text;
1769 -webkit-text-fill-color: transparent;
1770 background-clip: text;
1771 }
1772 .musehub-desc {
1773 font-size: 16px;
1774 color: var(--mute);
1775 max-width: 560px;
1776 margin: 0 auto 36px;
1777 }
1778 .musehub-desc strong { color: var(--text); }
1779 .musehub-features {
1780 display: flex;
1781 gap: 24px;
1782 justify-content: center;
1783 flex-wrap: wrap;
1784 margin-bottom: 40px;
1785 }
1786 .mh-feature {
1787 background: var(--bg2);
1788 border: 1px solid rgba(188,140,255,0.2);
1789 border-radius: var(--r);
1790 padding: 16px 20px;
1791 text-align: left;
1792 min-width: 180px;
1793 }
1794 .mh-feature-icon { margin-bottom: 10px; line-height: 0; }
1795 .mh-feature-icon .icon { width: 22px; height: 22px; stroke: #bc8cff; }
1796 .mh-feature-title { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 4px; }
1797 .mh-feature-desc { font-size: 12px; color: var(--mute); }
1798 .musehub-status {
1799 display: inline-flex;
1800 align-items: center;
1801 gap: 8px;
1802 background: rgba(188,140,255,0.1);
1803 border: 1px solid rgba(188,140,255,0.3);
1804 border-radius: 20px;
1805 padding: 8px 20px;
1806 font-size: 13px;
1807 color: var(--purple);
1808 }
1809 .mh-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--purple); animation: pulse 2s ease-in-out infinite; }
1810 @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
1811
1812 /* ---- Footer ---- */
1813 footer {
1814 background: var(--bg2);
1815 border-top: 1px solid var(--border);
1816 padding: 24px 40px;
1817 display: flex;
1818 justify-content: space-between;
1819 align-items: center;
1820 flex-wrap: wrap;
1821 gap: 12px;
1822 font-size: 13px;
1823 color: var(--mute);
1824 }
1825 footer a { color: var(--accent2); }
1826
1827 /* ---- Nav ---- */
1828 nav {
1829 background: var(--bg2);
1830 border-bottom: 1px solid var(--border);
1831 padding: 0 40px;
1832 display: flex;
1833 align-items: center;
1834 gap: 0;
1835 height: 52px;
1836 }
1837 .nav-logo {
1838 font-family: var(--mono);
1839 font-size: 16px;
1840 font-weight: 700;
1841 color: var(--accent2);
1842 margin-right: 32px;
1843 text-decoration: none;
1844 }
1845 .nav-logo:hover { text-decoration: none; }
1846 .nav-link {
1847 font-size: 13px;
1848 color: var(--mute);
1849 padding: 0 14px;
1850 height: 100%;
1851 display: flex;
1852 align-items: center;
1853 border-bottom: 2px solid transparent;
1854 text-decoration: none;
1855 transition: color 0.15s, border-color 0.15s;
1856 }
1857 .nav-link:hover { color: var(--text); text-decoration: none; }
1858 .nav-link.current { color: var(--text); border-bottom-color: var(--accent); }
1859 .nav-spacer { flex: 1; }
1860 .nav-badge {
1861 font-size: 11px;
1862 background: rgba(79,142,247,0.12);
1863 border: 1px solid rgba(79,142,247,0.3);
1864 color: var(--accent2);
1865 border-radius: 4px;
1866 padding: 2px 8px;
1867 font-family: var(--mono);
1868 }
1869 </style>
1870 </head>
1871 <body>
1872
1873 <nav>
1874 <a class="nav-logo" href="#">muse</a>
1875 <a class="nav-link" href="demo.html">VCS Demo</a>
1876 <a class="nav-link" href="midi-demo.html">🎹 MIDI Demo</a>
1877 <a class="nav-link" href="https://github.com/cgcardona/muse/blob/main/docs/guide/plugin-authoring-guide.md">Plugin Guide</a>
1878 <div class="nav-spacer"></div>
1879 <span class="nav-badge">v0.1.2</span>
1880 </nav>
1881
1882 <!-- =================== HERO =================== -->
1883 <div class="hero">
1884 <h1 class="hero-wordmark">muse</h1>
1885 <div class="hero-version-any">Version Anything</div>
1886 <p class="hero-sub">
1887 One protocol. Any domain. <strong>Six methods</strong> between you and a
1888 complete version control system — branching, merging, conflict resolution,
1889 time-travel, and typed diffs — for free.
1890 </p>
1891 <div class="hero-cta-row">
1892 <a class="btn-primary" href="#build">Build a Domain Plugin</a>
1893 <a class="btn-outline" href="demo.html">VCS Demo →</a>
1894 <a class="btn-outline" href="midi-demo.html">🎹 Bach BWV 846 × 21 Dimensions</a>
1895 </div>
1896 <div class="domain-ticker">
1897 <div class="ticker-track">
1898 <span class="ticker-item active">{{ICON_MUSIC}} midi</span>
1899 <span class="ticker-item active">{{ICON_CODE}} code</span>
1900 <span class="ticker-item">{{ICON_GENOMICS}} genomics</span>
1901 <span class="ticker-item">{{ICON_CUBE}} 3d-spatial</span>
1902 <span class="ticker-item">{{ICON_TRENDING}} financial</span>
1903 <span class="ticker-item">{{ICON_ATOM}} simulation</span>
1904 <span class="ticker-item">{{ICON_ACTIVITY}} proteomics</span>
1905 <span class="ticker-item">{{ICON_PEN_TOOL}} cad</span>
1906 <span class="ticker-item">{{ICON_ZAP}} game-state</span>
1907 <span class="ticker-item">{{ICON_PLUS}} your-domain</span>
1908 <!-- duplicate for seamless loop -->
1909 <span class="ticker-item active">{{ICON_MUSIC}} midi</span>
1910 <span class="ticker-item active">{{ICON_CODE}} code</span>
1911 <span class="ticker-item">{{ICON_GENOMICS}} genomics</span>
1912 <span class="ticker-item">{{ICON_CUBE}} 3d-spatial</span>
1913 <span class="ticker-item">{{ICON_TRENDING}} financial</span>
1914 <span class="ticker-item">{{ICON_ATOM}} simulation</span>
1915 <span class="ticker-item">{{ICON_ACTIVITY}} proteomics</span>
1916 <span class="ticker-item">{{ICON_PEN_TOOL}} cad</span>
1917 <span class="ticker-item">{{ICON_ZAP}} game-state</span>
1918 <span class="ticker-item">{{ICON_PLUS}} your-domain</span>
1919 </div>
1920 </div>
1921 </div>
1922
1923 <!-- =================== PROTOCOL =================== -->
1924 <section id="protocol">
1925 <div class="inner">
1926 <div class="section-eyebrow">The Contract</div>
1927 <h2>The MuseDomainPlugin Protocol</h2>
1928 <p class="section-lead">
1929 Every domain — MIDI, source code, genomics, 3D spatial, financial models —
1930 implements the same <strong>six-method protocol</strong>. The core engine
1931 handles everything else: content-addressed storage, DAG, branches, log,
1932 merge base, cherry-pick, revert, stash, tags.
1933 </p>
1934
1935 <div class="proto-layout">
1936 <div class="stat-strip">
1937 <div class="stat-cell"><span class="stat-num">6</span><span class="stat-lbl">methods to implement</span></div>
1938 <div class="stat-cell"><span class="stat-num">14</span><span class="stat-lbl">CLI commands, free</span></div>
1939 <div class="stat-cell"><span class="stat-num">∞</span><span class="stat-lbl">domains possible</span></div>
1940 <div class="stat-cell"><span class="stat-num">0</span><span class="stat-lbl">core changes needed</span></div>
1941 </div>
1942 <div class="proto-table">
1943 <div class="proto-row hdr">
1944 <div class="proto-method">Method</div>
1945 <div class="proto-sig">Signature</div>
1946 <div class="proto-desc">Purpose</div>
1947 </div>
1948 <div class="proto-row">
1949 <div class="proto-method">snapshot</div>
1950 <div class="proto-sig">snapshot(live) → StateSnapshot</div>
1951 <div class="proto-desc">Capture current state as a content-addressable blob</div>
1952 </div>
1953 <div class="proto-row">
1954 <div class="proto-method">diff</div>
1955 <div class="proto-sig">diff(base, target) → StateDelta</div>
1956 <div class="proto-desc">Compute minimal change between two snapshots (added · removed · modified)</div>
1957 </div>
1958 <div class="proto-row">
1959 <div class="proto-method">merge</div>
1960 <div class="proto-sig">merge(base, left, right) → MergeResult</div>
1961 <div class="proto-desc">Three-way reconcile divergent state lines; surface conflicts per dimension</div>
1962 </div>
1963 <div class="proto-row">
1964 <div class="proto-method">drift</div>
1965 <div class="proto-sig">drift(committed, live) → DriftReport</div>
1966 <div class="proto-desc">Detect uncommitted changes between HEAD and working state</div>
1967 </div>
1968 <div class="proto-row">
1969 <div class="proto-method">apply</div>
1970 <div class="proto-sig">apply(delta, live) → LiveState</div>
1971 <div class="proto-desc">Apply a delta during checkout to reconstruct historical state</div>
1972 </div>
1973 <div class="proto-row">
1974 <div class="proto-method">schema</div>
1975 <div class="proto-sig">schema() → DomainSchema</div>
1976 <div class="proto-desc">Declare data structure — drives diff algorithm selection per dimension</div>
1977 </div>
1978 </div>
1979 </div>
1980 </div>
1981 </section>
1982
1983 <!-- =================== ACTIVE DOMAINS =================== -->
1984 <section id="registry" style="background:var(--bg2)">
1985 <div class="inner">
1986 <div class="section-eyebrow">Registry</div>
1987 <h2>Registered Domains</h2>
1988 <p class="section-lead">
1989 Domains currently registered in this Muse instance. The active domain
1990 is the one used when you run <code>muse commit</code>, <code>muse diff</code>,
1991 and all other commands.
1992 </p>
1993 <div class="domain-grid">
1994 {{ACTIVE_DOMAINS}}
1995 </div>
1996 </div>
1997 </section>
1998
1999 <!-- =================== PLANNED ECOSYSTEM =================== -->
2000 <section id="ecosystem">
2001 <div class="inner">
2002 <div class="section-eyebrow">Ecosystem</div>
2003 <h2>The Plugin Ecosystem</h2>
2004 <p class="section-lead">
2005 MIDI and code are the two shipped domains — both fully active with typed
2006 deltas, structured merge, and <code>.museattributes</code> rule control.
2007 These are the domains planned next — and the slot waiting for yours.
2008 </p>
2009 <div class="planned-grid">
2010 {{PLANNED_DOMAINS}}
2011 </div>
2012 </div>
2013 </section>
2014
2015 <!-- =================== ENGINE CAPABILITIES =================== -->
2016 <section id="capabilities" style="background:var(--bg2)">
2017 <div class="inner">
2018 <div class="section-eyebrow">Engine Capabilities</div>
2019 <h2>What Every Plugin Gets for Free</h2>
2020 <p class="section-lead">
2021 The core engine provides four advanced capabilities that any domain plugin
2022 can opt into. Implement the protocol — the engine does the rest.
2023 </p>
2024
2025 <div class="cap-showcase-grid">
2026
2027 <div class="cap-showcase-card" style="--cap-color:#f9a825">
2028 <div class="cap-showcase-header">
2029 <span class="cap-showcase-badge" style="color:#f9a825;background:#f9a82515;border-color:#f9a82540">
2030 {{ICON_CODE}} Typed Delta Algebra
2031 </span>
2032 <span class="cap-showcase-sub">StructuredDelta — every change is a typed operation</span>
2033 </div>
2034 <div class="cap-showcase-body">
2035 <p class="cap-showcase-desc">
2036 Unlike Git's blob diffs, Muse deltas are <strong>typed objects</strong>:
2037 <code>InsertOp</code>, <code>ReplaceOp</code>, <code>DeleteOp</code> — each
2038 carrying the address, before/after hashes, and affected dimensions.
2039 Machine-readable with <code>muse show --json</code>.
2040 </p>
2041 <pre class="cap-showcase-output" data-lang="json">{{TYPED_DELTA_EXAMPLE}}</pre>
2042 </div>
2043 </div>
2044
2045 <div class="cap-showcase-card" style="--cap-color:#58a6ff">
2046 <div class="cap-showcase-header">
2047 <span class="cap-showcase-badge" style="color:#58a6ff;background:#58a6ff15;border-color:#58a6ff40">
2048 {{ICON_LAYERS}} Domain Schema
2049 </span>
2050 <span class="cap-showcase-sub">Per-domain dimensions drive diff algorithm selection</span>
2051 </div>
2052 <div class="cap-showcase-body">
2053 <p class="cap-showcase-desc">
2054 Each plugin's <code>schema()</code> method declares its dimensions and merge mode.
2055 The engine uses this to select the right diff algorithm per dimension and to
2056 surface only the dimensions that actually conflict.
2057 </p>
2058 <div class="cap-showcase-domain-grid" id="schema-domain-grid">
2059 {{ACTIVE_DOMAINS}}
2060 </div>
2061 </div>
2062 </div>
2063
2064 <div class="cap-showcase-card" style="--cap-color:#ef5350">
2065 <div class="cap-showcase-header">
2066 <span class="cap-showcase-badge" style="color:#ef5350;background:#ef535015;border-color:#ef535040">
2067 {{ICON_GIT_MERGE}} OT Merge
2068 </span>
2069 <span class="cap-showcase-sub">Operational transformation — independent ops commute automatically</span>
2070 </div>
2071 <div class="cap-showcase-body">
2072 <p class="cap-showcase-desc">
2073 Plugins implementing <strong>StructuredMergePlugin</strong> get operational
2074 transformation. Operations at different addresses commute automatically —
2075 only operations on the same address with incompatible intent surface a conflict.
2076 </p>
2077 <div class="ot-scenarios">
2078
2079 <div class="ot-scenario ot-clean">
2080 <div class="ot-scenario-hdr">
2081 <span class="ot-scenario-label">Scenario A</span>
2082 <span class="ot-scenario-title">Independent ops at different addresses</span>
2083 </div>
2084 <div class="ot-ops">
2085 <div class="ot-op">
2086 <span class="ot-op-side">left</span>
2087 <span class="ot-op-type ot-insert">InsertOp</span>
2088 <span class="ot-op-addr">"ot-notes-a.mid"</span>
2089 <span class="ot-op-meta">tick=0 · C4 E4 G4</span>
2090 </div>
2091 <div class="ot-op">
2092 <span class="ot-op-side">right</span>
2093 <span class="ot-op-type ot-insert">InsertOp</span>
2094 <span class="ot-op-addr">"ot-notes-b.mid"</span>
2095 <span class="ot-op-meta">tick=480 · D4 F4 A4</span>
2096 </div>
2097 </div>
2098 <div class="ot-result">
2099 <span class="ot-reason">transform → no overlap → ops commute</span>
2100 <span class="ot-badge ot-badge-clean">{{ICON_CHECK_CIRCLE}} Clean merge · both files applied</span>
2101 </div>
2102 </div>
2103
2104 <div class="ot-scenario ot-conflict">
2105 <div class="ot-scenario-hdr">
2106 <span class="ot-scenario-label">Scenario B</span>
2107 <span class="ot-scenario-title">Same address, conflicting intent — conflict surfaced</span>
2108 </div>
2109 <div class="ot-ops">
2110 <div class="ot-op">
2111 <span class="ot-op-side">left</span>
2112 <span class="ot-op-type ot-replace">ReplaceOp</span>
2113 <span class="ot-op-addr">"shared-melody.mid"</span>
2114 <span class="ot-op-meta">C4 E4 G4 · major triad</span>
2115 </div>
2116 <div class="ot-op">
2117 <span class="ot-op-side">right</span>
2118 <span class="ot-op-type ot-replace">ReplaceOp</span>
2119 <span class="ot-op-addr">"shared-melody.mid"</span>
2120 <span class="ot-op-meta">C4 Eb4 G4 · minor triad</span>
2121 </div>
2122 </div>
2123 <div class="ot-result">
2124 <span class="ot-reason">transform → same address · non-commuting content</span>
2125 <span class="ot-badge ot-badge-conflict">{{ICON_X_CIRCLE}} Conflict · human resolves</span>
2126 </div>
2127 </div>
2128
2129 </div>
2130 </div>
2131 </div>
2132
2133 <div class="cap-showcase-card" style="--cap-color:#bc8cff">
2134 <div class="cap-showcase-header">
2135 <span class="cap-showcase-badge" style="color:#bc8cff;background:#bc8cff15;border-color:#bc8cff40">
2136 {{ICON_ZAP}} CRDT Primitives
2137 </span>
2138 <span class="cap-showcase-sub">Convergent merge — any two replicas always reach the same state</span>
2139 </div>
2140 <div class="cap-showcase-body">
2141 <p class="cap-showcase-desc">
2142 Plugins implementing <strong>CRDTPlugin</strong> get six battle-tested
2143 convergent data structures. No coordination required between replicas.
2144 The MIDI plugin extends RGA into <strong>MidiRGA</strong> &mdash; a
2145 voice-aware variant that orders concurrent note insertions by voice lane
2146 (bass &rarr; tenor &rarr; alto &rarr; soprano) before falling back to
2147 op-id, preventing voice crossings without human intervention.
2148 </p>
2149 <div class="crdt-mini-grid">
2150 {{CRDT_CARDS}}
2151 </div>
2152 </div>
2153 </div>
2154
2155 </div>
2156 </div>
2157 </section>
2158
2159 {{DIFF_ALGEBRA}}
2160
2161 <!-- =================== BUILD =================== -->
2162 <section id="build" style="background:var(--bg)">
2163 <div class="inner">
2164 <div class="section-eyebrow">Build</div>
2165 <h2>Build in Three Steps</h2>
2166 <p class="section-lead">
2167 One command scaffolds the entire plugin skeleton. You fill in six methods.
2168 The full VCS follows.
2169 </p>
2170
2171 <div class="steps-grid">
2172 <div class="step-card">
2173 <div class="step-num">Step 1 · Scaffold</div>
2174 <div class="step-title">Generate the skeleton</div>
2175 <div class="step-desc">
2176 One command creates the plugin directory, class, and all six method stubs
2177 with full type annotations.
2178 </div>
2179 <div class="step-cmd">muse domains --new genomics</div>
2180 </div>
2181 <div class="step-card">
2182 <div class="step-num">Step 2 · Implement</div>
2183 <div class="step-title">Fill in the six methods</div>
2184 <div class="step-desc">
2185 Replace each <code>raise NotImplementedError</code> with your domain's
2186 snapshot, diff, merge, drift, apply, and schema logic.
2187 </div>
2188 <div class="step-cmd">vim muse/plugins/genomics/plugin.py</div>
2189 </div>
2190 <div class="step-card">
2191 <div class="step-num">Step 3 · Use</div>
2192 <div class="step-title">Full VCS, instantly</div>
2193 <div class="step-desc">
2194 Register in <code>registry.py</code>, then every Muse command works
2195 for your domain out of the box.
2196 </div>
2197 <div class="step-cmd">muse init --domain genomics</div>
2198 </div>
2199 </div>
2200 </div>
2201 </section>
2202
2203 <!-- =================== CODE =================== -->
2204 <section id="code">
2205 <div class="inner">
2206 <div class="section-eyebrow">The Scaffold</div>
2207 <h2>What <code>muse domains --new genomics</code> produces</h2>
2208 <p class="section-lead">
2209 A fully typed, immediately runnable plugin skeleton. Every method has the
2210 correct signature. You replace the stubs — the protocol does the rest.
2211 </p>
2212 <div class="code-wrap">
2213 <div class="code-bar">
2214 <div class="code-bar-dot" style="background:#ff5f57"></div>
2215 <div class="code-bar-dot" style="background:#febc2e"></div>
2216 <div class="code-bar-dot" style="background:#28c840"></div>
2217 <span class="code-bar-title">muse/plugins/genomics/plugin.py</span>
2218 </div>
2219 <div class="code-body">{{SCAFFOLD_SNIPPET}}</div>
2220 </div>
2221 <p style="margin-top:16px;font-size:13px;color:var(--mute)">
2222 Full walkthrough →
2223 <a href="https://github.com/cgcardona/muse/blob/main/docs/guide/plugin-authoring-guide.md">docs/guide/plugin-authoring-guide.md</a>
2224 · CRDT extension →
2225 <a href="https://github.com/cgcardona/muse/blob/main/docs/guide/crdt-reference.md">docs/guide/crdt-reference.md</a>
2226 </p>
2227 </div>
2228 </section>
2229
2230 <!-- =================== DISTRIBUTION =================== -->
2231 <section id="distribute" style="background:var(--bg2)">
2232 <div class="inner">
2233 <div class="section-eyebrow">Distribution</div>
2234 <h2>How to Share Your Plugin</h2>
2235 <p class="section-lead">
2236 Three tiers of distribution — from local prototype to globally searchable
2237 registry. Start local, publish when ready.
2238 </p>
2239 <div class="dist-grid">
2240 {{DIST_CARDS}}
2241 </div>
2242 </div>
2243 </section>
2244
2245 <!-- =================== MUSEHUB TEASER =================== -->
2246 <div class="musehub-section">
2247 <div class="musehub-logo">{{ICON_GLOBE}}</div>
2248 <h2><span>MuseHub</span> is coming</h2>
2249 <p class="musehub-desc">
2250 A <strong>centralized, searchable registry</strong> for Muse domain plugins —
2251 think npm or crates.io, but for any multidimensional versioned state.
2252 One command to publish. One command to install.
2253 </p>
2254 <div class="musehub-features">
2255 <div class="mh-feature">
2256 <div class="mh-feature-icon">{{ICON_SEARCH}}</div>
2257 <div class="mh-feature-title">Searchable</div>
2258 <div class="mh-feature-desc">Find plugins by domain, capability, or keyword</div>
2259 </div>
2260 <div class="mh-feature">
2261 <div class="mh-feature-icon">{{ICON_PACKAGE}}</div>
2262 <div class="mh-feature-title">Versioned</div>
2263 <div class="mh-feature-desc">Semantic versioning, pinned installs, changelogs</div>
2264 </div>
2265 <div class="mh-feature">
2266 <div class="mh-feature-icon">{{ICON_LOCK}}</div>
2267 <div class="mh-feature-title">Private registries</div>
2268 <div class="mh-feature-desc">Self-host for enterprise or research teams</div>
2269 </div>
2270 <div class="mh-feature">
2271 <div class="mh-feature-icon">{{ICON_ZAP}}</div>
2272 <div class="mh-feature-title">One command</div>
2273 <div class="mh-feature-desc"><code>muse init --domain @musehub/genomics</code></div>
2274 </div>
2275 </div>
2276 <div class="musehub-status">
2277 <div class="mh-dot"></div>
2278 MuseHub — planned · building in public at <a href="https://github.com/cgcardona/musehub" target="_blank" rel="noopener noreferrer" style="color:inherit;text-decoration:underline;text-underline-offset:3px;">github.com/cgcardona/musehub</a>
2279 </div>
2280 </div>
2281
2282 <footer>
2283 <span>Muse v0.1.2 · domain-agnostic version control for multidimensional state · Python 3.14</span>
2284 <span>
2285 <a href="demo.html">Demo</a> ·
2286 <a href="https://github.com/cgcardona/muse">GitHub</a> ·
2287 <a href="https://github.com/cgcardona/muse/blob/main/docs/guide/plugin-authoring-guide.md">Plugin Guide</a>
2288 </span>
2289 </footer>
2290
2291 <script>
2292 (function () {
2293 function esc(s) {
2294 return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
2295 }
2296
2297 function tokenizeJSON(raw) {
2298 let html = '';
2299 let i = 0;
2300 while (i < raw.length) {
2301 // Comment line starting with #
2302 if (raw[i] === '#') {
2303 const end = raw.indexOf('\n', i);
2304 const line = end === -1 ? raw.slice(i) : raw.slice(i, end);
2305 html += '<span style="color:#5c6370;font-style:italic">' + esc(line) + '</span>';
2306 i += line.length;
2307 continue;
2308 }
2309 // String literal
2310 if (raw[i] === '"') {
2311 let j = i + 1;
2312 while (j < raw.length && raw[j] !== '"') {
2313 if (raw[j] === '\\') j++;
2314 j++;
2315 }
2316 j++;
2317 const str = raw.slice(i, j);
2318 // Peek past whitespace — key if followed by ':'
2319 let k = j;
2320 while (k < raw.length && (raw[k] === ' ' || raw[k] === '\t')) k++;
2321 const color = raw[k] === ':' ? '#61afef' : '#98c379';
2322 html += '<span style="color:' + color + '">' + esc(str) + '</span>';
2323 i = j;
2324 continue;
2325 }
2326 // Number (including negative)
2327 if (/[0-9]/.test(raw[i]) || (raw[i] === '-' && /[0-9]/.test(raw[i + 1] || ''))) {
2328 let j = i;
2329 if (raw[j] === '-') j++;
2330 while (j < raw.length && /[0-9.eE+-]/.test(raw[j])) j++;
2331 html += '<span style="color:#d19a66">' + esc(raw.slice(i, j)) + '</span>';
2332 i = j;
2333 continue;
2334 }
2335 // Keywords: true / false / null
2336 const kws = [['true', '#c678dd'], ['false', '#c678dd'], ['null', '#c678dd']];
2337 let matched = false;
2338 for (const [kw, col] of kws) {
2339 if (raw.slice(i, i + kw.length) === kw) {
2340 html += '<span style="color:' + col + '">' + kw + '</span>';
2341 i += kw.length;
2342 matched = true;
2343 break;
2344 }
2345 }
2346 if (matched) continue;
2347 // Default character (punctuation / whitespace)
2348 html += esc(raw[i]);
2349 i++;
2350 }
2351 return html;
2352 }
2353
2354 document.querySelectorAll('pre[data-lang="json"]').forEach(function (pre) {
2355 pre.innerHTML = tokenizeJSON(pre.textContent);
2356 });
2357 })();
2358 </script>
2359
2360 </body>
2361 </html>
2362 """
2363
2364
2365 # ---------------------------------------------------------------------------
2366 # Entry point
2367 # ---------------------------------------------------------------------------
2368
2369 if __name__ == "__main__":
2370 import argparse
2371
2372 parser = argparse.ArgumentParser(
2373 description="Generate the Muse domain registry HTML page"
2374 )
2375 parser.add_argument(
2376 "--out",
2377 default=str(_ROOT / "artifacts" / "domain_registry.html"),
2378 help="Output HTML path",
2379 )
2380 args = parser.parse_args()
2381
2382 out_path = pathlib.Path(args.out)
2383 out_path.parent.mkdir(parents=True, exist_ok=True)
2384
2385 print("Generating domain_registry.html...")
2386 render(out_path)
2387 print(f"Open: file://{out_path.resolve()}")