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