cgcardona / muse public
render_domain_registry.py python
1599 lines 59.5 KB
6b8c455f feat: replace all emoji with inline SVG icons (Lucide style) (#38) Gabriel Cardona <cgcardona@gmail.com> 2d 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 from __future__ import annotations
14
15 import json
16 import pathlib
17 import subprocess
18 import sys
19
20 _ROOT = pathlib.Path(__file__).resolve().parent.parent
21
22
23 # ---------------------------------------------------------------------------
24 # Live domain data from the CLI
25 # ---------------------------------------------------------------------------
26
27
28 def _compute_crdt_demos() -> list[dict]:
29 """Run the four CRDT primitives live and return formatted demo output."""
30 sys.path.insert(0, str(_ROOT))
31 try:
32 from muse.core.crdts import GCounter, LWWRegister, ORSet, VectorClock
33
34 # ORSet
35 base, _ = ORSet().add("annotation-GO:0001234")
36 a, _ = base.add("annotation-GO:0001234")
37 b = base.remove("annotation-GO:0001234", base.tokens_for("annotation-GO:0001234"))
38 merged = a.join(b)
39 orset_out = "\n".join([
40 "ORSet — add-wins concurrent merge:",
41 f" base elements: {sorted(base.elements())}",
42 f" A re-adds → elements: {sorted(a.elements())}",
43 f" B removes → elements: {sorted(b.elements())}",
44 f" join(A, B) → elements: {sorted(merged.elements())}",
45 " [A's new token is not tombstoned — add always wins]",
46 ])
47
48 # LWWRegister
49 ra = LWWRegister.from_dict({"value": "80 BPM", "timestamp": 1.0, "author": "agent-A"})
50 rb = LWWRegister.from_dict({"value": "120 BPM", "timestamp": 2.0, "author": "agent-B"})
51 rm = ra.join(rb)
52 lww_out = "\n".join([
53 "LWWRegister — last-write-wins scalar:",
54 f" Agent A writes: '{ra.read()}' at t=1.0",
55 f" Agent B writes: '{rb.read()}' at t=2.0 (later)",
56 f" join(A, B) → '{rm.read()}' [higher timestamp wins]",
57 " join(B, A) → same result [commutativity]",
58 ])
59
60 # GCounter
61 ca = GCounter().increment("agent-A").increment("agent-A")
62 cb = GCounter().increment("agent-B").increment("agent-B").increment("agent-B")
63 cm = ca.join(cb)
64 gc_out = "\n".join([
65 "GCounter — grow-only distributed counter:",
66 f" Agent A x2 → A slot: {ca.value_for('agent-A')}",
67 f" Agent B x3 → B slot: {cb.value_for('agent-B')}",
68 f" join(A, B) global value: {cm.value()}",
69 " [monotonically non-decreasing — joins never lose counts]",
70 ])
71
72 # VectorClock
73 va = VectorClock().increment("agent-A")
74 vb = VectorClock().increment("agent-B")
75 vm = va.merge(vb)
76 vc_out = "\n".join([
77 "VectorClock — causal ordering:",
78 f" Agent A: {va.to_dict()}",
79 f" Agent B: {vb.to_dict()}",
80 f" concurrent_with(A, B): {va.concurrent_with(vb)}",
81 f" merge(A, B): {vm.to_dict()} [component-wise max]",
82 ])
83
84 return [
85 {"type": "ORSet", "sub": "Observed-Remove Set", "color": "#bc8cff", "icon": _ICONS["union"], "output": orset_out},
86 {"type": "LWWRegister", "sub": "Last-Write-Wins Register", "color": "#58a6ff", "icon": _ICONS["edit"], "output": lww_out},
87 {"type": "GCounter", "sub": "Grow-Only Distributed Counter", "color": "#3fb950", "icon": _ICONS["arrow-up"], "output": gc_out},
88 {"type": "VectorClock", "sub": "Causal Ordering", "color": "#f9a825", "icon": _ICONS["git-branch"], "output": vc_out},
89 ]
90 except Exception as exc:
91 print(f" ⚠ CRDT demo failed ({exc}); using static fallback")
92 return []
93
94
95 def _load_domains() -> list[dict]:
96 """Run `muse domains --json` and return parsed output."""
97 try:
98 result = subprocess.run(
99 [sys.executable, "-m", "muse", "domains", "--json"],
100 capture_output=True,
101 text=True,
102 cwd=str(_ROOT),
103 timeout=15,
104 )
105 if result.returncode == 0:
106 raw = result.stdout.strip()
107 data: list[dict] = json.loads(raw)
108 return data
109 except Exception:
110 pass
111
112 # Fallback: static reference data
113 return [
114 {
115 "domain": "music",
116 "active": "true",
117 "capabilities": ["Typed Deltas", "Domain Schema", "OT Merge"],
118 "schema": {
119 "schema_version": "1",
120 "merge_mode": "three_way",
121 "description": "MIDI and audio file versioning with note-level diff and semantic merge",
122 "dimensions": [
123 {"name": "melodic", "description": "Note pitches and durations over time"},
124 {"name": "harmonic", "description": "Chord progressions and key signatures"},
125 {"name": "dynamic", "description": "Velocity and expression curves"},
126 {"name": "structural", "description": "Track layout, time signatures, tempo map"},
127 ],
128 },
129 }
130 ]
131
132
133 # ---------------------------------------------------------------------------
134 # Scaffold template (shown in the "Build in 3 steps" section)
135 # ---------------------------------------------------------------------------
136
137 _TYPED_DELTA_EXAMPLE = """\
138 # muse show --json (any commit, any domain)
139 {
140 "commit_id": "b26f3c99",
141 "message": "Resolve: integrate shared-state (A+B reconciled)",
142 "operations": [
143 {
144 "op_type": "ReplaceOp",
145 "address": "shared-state.mid",
146 "before_hash": "a1b2c3d4",
147 "after_hash": "e5f6g7h8",
148 "dimensions": ["structural"]
149 },
150 {
151 "op_type": "InsertOp",
152 "address": "beta-a.mid",
153 "after_hash": "09ab1234",
154 "dimensions": ["rhythmic", "dynamic"]
155 }
156 ],
157 "summary": {
158 "inserted": 1,
159 "replaced": 1,
160 "deleted": 0
161 }
162 }"""
163
164 _OT_MERGE_EXAMPLE = """\
165 # Scenario A — independent InsertOps at different addresses → commute → clean merge
166 left: InsertOp("ot-notes-a.mid") # tick=0, C4 E4 G4
167 right: InsertOp("ot-notes-b.mid") # tick=480, D4 F4 A4
168
169 transform(left, right) → no overlap → both applied
170 result: both files present, zero conflicts ✓
171
172 # Scenario B — same address, different content → genuine conflict
173 base: shared-melody.mid # C4 G4
174 left: ReplaceOp("shared-melody.mid") # C4 E4 G4 (major triad)
175 right: ReplaceOp("shared-melody.mid") # C4 Eb4 G4 (minor triad)
176
177 transform(left, right) → same address, non-commuting content
178 result: ❌ Merge conflict in 1 file(s):
179 CONFLICT (both modified): shared-melody.mid
180 [musical intent differs — human must choose major or minor]"""
181
182 _SCAFFOLD_SNIPPET = """\
183 from __future__ import annotations
184 from muse.domain import (
185 MuseDomainPlugin, LiveState, StateSnapshot,
186 StateDelta, DriftReport, MergeResult, DomainSchema,
187 )
188
189 class GenomicsPlugin(MuseDomainPlugin):
190 \"\"\"Version control for genomic sequences.\"\"\"
191
192 def snapshot(self, live_state: LiveState) -> StateSnapshot:
193 # Serialize current genome state to a content-addressable blob
194 raise NotImplementedError
195
196 def diff(self, base: StateSnapshot,
197 target: StateSnapshot) -> StateDelta:
198 # Compute minimal delta between two snapshots
199 raise NotImplementedError
200
201 def merge(self, base: StateSnapshot,
202 left: StateSnapshot,
203 right: StateSnapshot) -> MergeResult:
204 # Three-way merge — surface conflicts per dimension
205 raise NotImplementedError
206
207 def drift(self, committed: StateSnapshot,
208 live: LiveState) -> DriftReport:
209 # Detect uncommitted changes in the working state
210 raise NotImplementedError
211
212 def apply(self, delta: StateDelta,
213 live_state: LiveState) -> LiveState:
214 # Reconstruct historical state from a delta
215 raise NotImplementedError
216
217 def schema(self) -> DomainSchema:
218 # Declare dimensions — drives diff algorithm selection
219 raise NotImplementedError
220 """
221
222 # ---------------------------------------------------------------------------
223 # SVG icon library — Lucide/Feather style, stroke="currentColor", no fixed size
224 # ---------------------------------------------------------------------------
225
226 def _icon(paths: str) -> str:
227 """Wrap SVG paths in a standard icon shell."""
228 return (
229 '<svg class="icon" viewBox="0 0 24 24" fill="none" '
230 'stroke="currentColor" stroke-width="1.75" '
231 'stroke-linecap="round" stroke-linejoin="round">'
232 + paths
233 + "</svg>"
234 )
235
236
237 _ICONS: dict[str, str] = {
238 # Domains
239 "music": _icon('<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>'),
240 "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"/>'),
241 "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"/>'),
242 "trending": _icon('<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/>'),
243 "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"/>'),
244 "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"/>'),
245 "activity": _icon('<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>'),
246 "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"/>'),
247 # Distribution
248 "terminal": _icon('<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>'),
249 "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"/>'),
250 "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"/>'),
251 # Engine capabilities
252 "code": _icon('<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>'),
253 "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"/>'),
254 "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"/>'),
255 "zap": _icon('<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>'),
256 # MuseHub features
257 "search": _icon('<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>'),
258 "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"/>'),
259 # CRDT primitives
260 "union": _icon('<path d="M5 5v8a7 7 0 0 0 14 0V5"/><line x1="3" y1="19" x2="21" y2="19"/>'),
261 "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"/>'),
262 "arrow-up": _icon('<line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/>'),
263 "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"/>'),
264 }
265
266
267 # ---------------------------------------------------------------------------
268 # Planned / aspirational domains
269 # ---------------------------------------------------------------------------
270
271 _PLANNED_DOMAINS = [
272 {
273 "name": "Genomics",
274 "icon": _ICONS["genomics"],
275 "status": "planned",
276 "tagline": "Version sequences, variants, and annotations",
277 "dimensions": ["sequence", "variants", "annotations", "metadata"],
278 "color": "#3fb950",
279 },
280 {
281 "name": "3D / Spatial",
282 "icon": _ICONS["cube"],
283 "status": "planned",
284 "tagline": "Merge spatial fields, meshes, and simulation frames",
285 "dimensions": ["geometry", "materials", "physics", "temporal"],
286 "color": "#58a6ff",
287 },
288 {
289 "name": "Financial",
290 "icon": _ICONS["trending"],
291 "status": "planned",
292 "tagline": "Track model versions, alpha signals, and risk state",
293 "dimensions": ["signals", "positions", "risk", "parameters"],
294 "color": "#f9a825",
295 },
296 {
297 "name": "Scientific Simulation",
298 "icon": _ICONS["atom"],
299 "status": "planned",
300 "tagline": "Snapshot simulation state across timesteps and parameter spaces",
301 "dimensions": ["state", "parameters", "observables", "checkpoints"],
302 "color": "#ab47bc",
303 },
304 {
305 "name": "Your Domain",
306 "icon": _ICONS["plus"],
307 "status": "yours",
308 "tagline": "Six methods. Any multidimensional state. Full VCS for free.",
309 "dimensions": ["your_dim_1", "your_dim_2", "..."],
310 "color": "#4f8ef7",
311 },
312 ]
313
314 # ---------------------------------------------------------------------------
315 # Distribution model description
316 # ---------------------------------------------------------------------------
317
318 _DISTRIBUTION_LEVELS = [
319 {
320 "tier": "Local",
321 "icon": _ICONS["terminal"],
322 "title": "Local plugin (right now)",
323 "color": "#3fb950",
324 "steps": [
325 "muse domains --new &lt;name&gt;",
326 "Implement 6 methods in muse/plugins/&lt;name&gt;/plugin.py",
327 "Register in muse/plugins/registry.py",
328 "muse init --domain &lt;name&gt;",
329 ],
330 "desc": "Works today. Scaffold → implement → register. "
331 "Your plugin lives alongside the core.",
332 },
333 {
334 "tier": "Shareable",
335 "icon": _ICONS["package"],
336 "title": "pip-installable package (right now)",
337 "color": "#58a6ff",
338 "steps": [
339 "Package your plugin as a Python module",
340 "pip install git+https://github.com/you/muse-plugin-genomics",
341 "Register the entry-point in pyproject.toml",
342 "muse init --domain genomics",
343 ],
344 "desc": "Share your plugin as a standard Python package. "
345 "Anyone with pip can install and use it.",
346 },
347 {
348 "tier": "MuseHub",
349 "icon": _ICONS["globe"],
350 "title": "Centralized registry (coming — MuseHub)",
351 "color": "#bc8cff",
352 "steps": [
353 "musehub publish muse-plugin-genomics",
354 "musehub search genomics",
355 "muse init --domain @musehub/genomics",
356 "Browse plugins at musehub.io",
357 ],
358 "desc": "MuseHub is a planned centralized registry — npm for Muse plugins. "
359 "Versioned, searchable, one-command install.",
360 },
361 ]
362
363
364 # ---------------------------------------------------------------------------
365 # HTML template
366 # ---------------------------------------------------------------------------
367
368 def _render_capability_card(cap: dict) -> str:
369 color = cap["color"]
370 return f"""
371 <div class="cap-showcase-card" style="--cap-color:{color}">
372 <div class="cap-showcase-header">
373 <span class="cap-showcase-badge" style="color:{color};background:{color}15;border-color:{color}40">
374 {cap['icon']} {cap['type']}
375 </span>
376 <span class="cap-showcase-sub">{cap['sub']}</span>
377 </div>
378 <div class="cap-showcase-body">
379 <pre class="cap-showcase-output">{cap['output']}</pre>
380 </div>
381 </div>"""
382
383
384 def _render_domain_card(d: dict) -> str:
385 domain = d.get("domain", "unknown")
386 active = d.get("active") == "true"
387 schema = d.get("schema", {})
388 desc = schema.get("description", "")
389 dims = schema.get("dimensions", [])
390 caps = d.get("capabilities", [])
391
392 cap_html = " ".join(
393 f'<span class="cap-pill cap-{c.lower().replace(" ","-")}">{c}</span>'
394 for c in caps
395 )
396 dim_html = " · ".join(
397 f'<span class="dim-tag">{dim["name"]}</span>' for dim in dims
398 )
399
400 status_cls = "active-badge" if active else "reg-badge"
401 status_text = "● active" if active else "○ registered"
402 dot = '<span class="active-dot"></span>' if active else ""
403
404 short_desc = desc[:150] + ("…" if len(desc) > 150 else "")
405
406 return f"""
407 <div class="domain-card{' active-domain' if active else ''}">
408 <div class="domain-card-hdr">
409 <span class="{status_cls}">{status_text}</span>
410 <span class="domain-name-lg">{domain}</span>
411 {dot}
412 </div>
413 <div class="domain-card-body">
414 <p class="domain-desc">{short_desc}</p>
415 <div class="cap-row">{cap_html}</div>
416 <div class="dim-row"><strong>Dimensions:</strong> {dim_html}</div>
417 </div>
418 </div>"""
419
420
421 def _render_planned_card(p: dict) -> str:
422 dims = " · ".join(f'<span class="dim-tag">{d}</span>' for d in p["dimensions"])
423 cls = "planned-card yours" if p["status"] == "yours" else "planned-card"
424 return f"""
425 <div class="{cls}" style="--card-accent:{p['color']}">
426 <div class="planned-icon">{p['icon']}</div>
427 <div class="planned-name">{p['name']}</div>
428 <div class="planned-tag">{p['tagline']}</div>
429 <div class="planned-dims">{dims}</div>
430 {'<a class="cta-btn" href="#build">Build it →</a>' if p["status"] == "yours" else '<span class="coming-soon">coming soon</span>'}
431 </div>"""
432
433
434 def _render_dist_card(d: dict) -> str:
435 steps = "".join(
436 f'<li><code>{s}</code></li>' for s in d["steps"]
437 )
438 return f"""
439 <div class="dist-card" style="--dist-color:{d['color']}">
440 <div class="dist-header">
441 <span class="dist-icon">{d['icon']}</span>
442 <div>
443 <div class="dist-tier">{d['tier']}</div>
444 <div class="dist-title">{d['title']}</div>
445 </div>
446 </div>
447 <p class="dist-desc">{d['desc']}</p>
448 <ol class="dist-steps">{steps}</ol>
449 </div>"""
450
451
452 def render(output_path: pathlib.Path) -> None:
453 """Generate the domain registry HTML page."""
454 print(" Loading live domain data...")
455 domains = _load_domains()
456 print(f" Found {len(domains)} registered domain(s)")
457
458 print(" Computing live CRDT demos...")
459 crdt_demos = _compute_crdt_demos()
460
461 active_domains_html = "\n".join(_render_domain_card(d) for d in domains)
462 planned_html = "\n".join(_render_planned_card(p) for p in _PLANNED_DOMAINS)
463 dist_html = "\n".join(_render_dist_card(d) for d in _DISTRIBUTION_LEVELS)
464 crdt_cards_html = "\n".join(_render_capability_card(c) for c in crdt_demos)
465
466 html = _HTML_TEMPLATE.replace("{{ACTIVE_DOMAINS}}", active_domains_html)
467 html = html.replace("{{PLANNED_DOMAINS}}", planned_html)
468 html = html.replace("{{DIST_CARDS}}", dist_html)
469 html = html.replace("{{SCAFFOLD_SNIPPET}}", _SCAFFOLD_SNIPPET)
470 html = html.replace("{{TYPED_DELTA_EXAMPLE}}", _TYPED_DELTA_EXAMPLE)
471 html = html.replace("{{OT_MERGE_EXAMPLE}}", _OT_MERGE_EXAMPLE)
472 html = html.replace("{{CRDT_CARDS}}", crdt_cards_html)
473
474 # Inject SVG icons into template placeholders
475 _ICON_SLOTS: dict[str, str] = {
476 "MUSIC": _ICONS["music"],
477 "GENOMICS": _ICONS["genomics"],
478 "CUBE": _ICONS["cube"],
479 "TRENDING": _ICONS["trending"],
480 "ATOM": _ICONS["atom"],
481 "PLUS": _ICONS["plus"],
482 "ACTIVITY": _ICONS["activity"],
483 "PEN_TOOL": _ICONS["pen-tool"],
484 "CODE": _ICONS["code"],
485 "LAYERS": _ICONS["layers"],
486 "GIT_MERGE": _ICONS["git-merge"],
487 "ZAP": _ICONS["zap"],
488 "GLOBE": _ICONS["globe"],
489 "SEARCH": _ICONS["search"],
490 "PACKAGE": _ICONS["package"],
491 "LOCK": _ICONS["lock"],
492 }
493 for slot, svg in _ICON_SLOTS.items():
494 html = html.replace(f"{{{{ICON_{slot}}}}}", svg)
495
496 output_path.write_text(html, encoding="utf-8")
497 size_kb = output_path.stat().st_size // 1024
498 print(f" HTML written ({size_kb}KB) → {output_path}")
499
500 # Also write as index.html so the domain registry IS the landing page.
501 index_path = output_path.parent / "index.html"
502 index_path.write_text(html, encoding="utf-8")
503 print(f" Landing page mirrored → {index_path}")
504
505
506 # ---------------------------------------------------------------------------
507 # Large HTML template
508 # ---------------------------------------------------------------------------
509
510 _HTML_TEMPLATE = """\
511 <!DOCTYPE html>
512 <html lang="en">
513 <head>
514 <meta charset="utf-8">
515 <meta name="viewport" content="width=device-width, initial-scale=1">
516 <title>Muse — Version Anything</title>
517 <style>
518 *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
519 :root {
520 --bg: #0d1117;
521 --bg2: #161b22;
522 --bg3: #21262d;
523 --border: #30363d;
524 --text: #e6edf3;
525 --mute: #8b949e;
526 --dim: #484f58;
527 --accent: #4f8ef7;
528 --accent2: #58a6ff;
529 --green: #3fb950;
530 --red: #f85149;
531 --yellow: #d29922;
532 --purple: #bc8cff;
533 --mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
534 --ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
535 --r: 8px;
536 }
537 html { scroll-behavior: smooth; }
538 body {
539 background: var(--bg);
540 color: var(--text);
541 font-family: var(--ui);
542 font-size: 15px;
543 line-height: 1.7;
544 }
545 a { color: var(--accent2); text-decoration: none; }
546 a:hover { text-decoration: underline; }
547 code {
548 font-family: var(--mono);
549 font-size: 0.88em;
550 background: var(--bg3);
551 border: 1px solid var(--border);
552 border-radius: 4px;
553 padding: 1px 6px;
554 }
555
556 /* ---- Hero ---- */
557 .hero {
558 background: linear-gradient(160deg, #0d1117 0%, #161b22 50%, #0d1117 100%);
559 border-bottom: 1px solid var(--border);
560 padding: 80px 40px 100px;
561 text-align: center;
562 position: relative;
563 overflow: hidden;
564 }
565 .hero::before {
566 content: '';
567 position: absolute;
568 inset: 0;
569 background:
570 radial-gradient(ellipse 60% 40% at 20% 50%, rgba(79,142,247,0.07) 0%, transparent 70%),
571 radial-gradient(ellipse 50% 40% at 80% 50%, rgba(188,140,255,0.06) 0%, transparent 70%);
572 pointer-events: none;
573 }
574 .hero-wordmark {
575 font-family: var(--ui);
576 font-size: clamp(72px, 11vw, 130px);
577 font-weight: 800;
578 letter-spacing: -5px;
579 line-height: 1;
580 margin-bottom: 12px;
581 background: linear-gradient(90deg, #6ea8fe 0%, #a78bfa 50%, #c084fc 100%);
582 -webkit-background-clip: text;
583 -webkit-text-fill-color: transparent;
584 background-clip: text;
585 }
586 .hero-version-any {
587 font-size: clamp(18px, 2.8vw, 26px);
588 font-weight: 700;
589 color: #ffffff;
590 letter-spacing: 6px;
591 text-transform: uppercase;
592 margin-bottom: 32px;
593 }
594 .hero-sub {
595 font-size: 18px;
596 color: var(--mute);
597 max-width: 600px;
598 margin: 0 auto 40px;
599 line-height: 1.6;
600 }
601 .hero-sub strong { color: var(--text); }
602 .hero-cta-row {
603 display: flex;
604 gap: 12px;
605 justify-content: center;
606 flex-wrap: wrap;
607 }
608 .btn-primary {
609 background: var(--accent);
610 color: #fff;
611 font-weight: 600;
612 padding: 12px 28px;
613 border-radius: var(--r);
614 font-size: 15px;
615 border: none;
616 cursor: pointer;
617 text-decoration: none;
618 transition: opacity 0.15s, transform 0.1s;
619 display: inline-block;
620 }
621 .btn-primary:hover { opacity: 0.88; transform: translateY(-1px); text-decoration: none; }
622 .btn-outline {
623 background: transparent;
624 color: var(--text);
625 font-weight: 500;
626 padding: 12px 28px;
627 border-radius: var(--r);
628 font-size: 15px;
629 border: 1px solid var(--border);
630 cursor: pointer;
631 text-decoration: none;
632 display: inline-block;
633 transition: border-color 0.15s, color 0.15s;
634 }
635 .btn-outline:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
636
637 /* ---- Domain ticker ---- */
638 .domain-ticker {
639 margin: 32px auto 0;
640 max-width: 700px;
641 overflow: hidden;
642 position: relative;
643 height: 34px;
644 }
645 .domain-ticker::before,
646 .domain-ticker::after {
647 content: '';
648 position: absolute;
649 top: 0; bottom: 0;
650 width: 60px;
651 z-index: 2;
652 }
653 .domain-ticker::before { left: 0; background: linear-gradient(90deg, var(--bg), transparent); }
654 .domain-ticker::after { right: 0; background: linear-gradient(-90deg, var(--bg), transparent); }
655 .ticker-track {
656 display: flex;
657 gap: 10px;
658 animation: ticker-scroll 18s linear infinite;
659 width: max-content;
660 }
661 @keyframes ticker-scroll {
662 0% { transform: translateX(0); }
663 100% { transform: translateX(-50%); }
664 }
665 .ticker-item {
666 font-family: var(--mono);
667 font-size: 13px;
668 padding: 4px 14px;
669 border-radius: 20px;
670 border: 1px solid var(--border);
671 white-space: nowrap;
672 color: var(--mute);
673 }
674 .ticker-item.active { border-color: rgba(79,142,247,0.5); color: var(--accent2); background: rgba(79,142,247,0.08); }
675
676 /* ---- Sections ---- */
677 section { padding: 72px 40px; border-top: 1px solid var(--border); }
678 .inner { max-width: 1100px; margin: 0 auto; }
679 .section-eyebrow {
680 font-family: var(--mono);
681 font-size: 11px;
682 color: var(--accent2);
683 letter-spacing: 2px;
684 text-transform: uppercase;
685 margin-bottom: 10px;
686 }
687 section h2 {
688 font-size: 32px;
689 font-weight: 700;
690 letter-spacing: -0.5px;
691 margin-bottom: 12px;
692 }
693 .section-lead {
694 font-size: 16px;
695 color: var(--mute);
696 max-width: 620px;
697 margin-bottom: 48px;
698 line-height: 1.7;
699 }
700 .section-lead strong { color: var(--text); }
701
702 /* ---- Base icon ---- */
703 .icon {
704 display: inline-block;
705 vertical-align: -0.15em;
706 flex-shrink: 0;
707 }
708 .ticker-item .icon { width: 13px; height: 13px; vertical-align: -0.1em; }
709 .cap-showcase-badge .icon { width: 13px; height: 13px; vertical-align: -0.1em; }
710
711 /* ---- Protocol two-col layout ---- */
712 .proto-layout {
713 display: grid;
714 grid-template-columns: 148px 1fr;
715 gap: 0;
716 border: 1px solid var(--border);
717 border-radius: var(--r);
718 overflow: hidden;
719 margin-bottom: 40px;
720 align-items: stretch;
721 }
722 @media (max-width: 640px) {
723 .proto-layout { grid-template-columns: 1fr; }
724 .stat-strip { border-right: none; border-bottom: 1px solid var(--border); }
725 }
726
727 /* ---- Stat strip (left column) ---- */
728 .stat-strip {
729 display: flex;
730 flex-direction: column;
731 border-right: 1px solid var(--border);
732 }
733 .stat-cell {
734 flex: 1;
735 padding: 18px 20px;
736 border-bottom: 1px solid var(--border);
737 text-align: center;
738 display: flex;
739 flex-direction: column;
740 align-items: center;
741 justify-content: center;
742 }
743 .stat-cell:last-child { border-bottom: none; }
744 .stat-num {
745 font-family: var(--mono);
746 font-size: 26px;
747 font-weight: 700;
748 color: var(--accent2);
749 display: block;
750 line-height: 1.1;
751 }
752 .stat-lbl { font-size: 11px; color: var(--mute); margin-top: 4px; line-height: 1.3; }
753
754 /* ---- Protocol table (right column) ---- */
755 .proto-table {
756 overflow: hidden;
757 }
758 .proto-row {
759 display: grid;
760 grid-template-columns: 90px 240px 1fr;
761 border-bottom: 1px solid var(--border);
762 }
763 .proto-row:last-child { border-bottom: none; }
764 .proto-row.hdr { background: var(--bg3); }
765 .proto-row > div { padding: 11px 16px; }
766 .proto-method { font-family: var(--mono); font-size: 13px; color: var(--accent2); font-weight: 600; }
767 .proto-sig { font-family: var(--mono); font-size: 12px; color: var(--mute); }
768 .proto-desc { font-size: 13px; color: var(--mute); }
769 .proto-row.hdr .proto-method,
770 .proto-row.hdr .proto-sig,
771 .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; }
772
773 /* ---- Engine capability showcase ---- */
774 .cap-showcase-grid {
775 display: grid;
776 grid-template-columns: repeat(auto-fill, minmax(480px, 1fr));
777 gap: 24px;
778 }
779 @media (max-width: 600px) { .cap-showcase-grid { grid-template-columns: 1fr; } }
780 .cap-showcase-card {
781 border: 1px solid var(--border);
782 border-top: 3px solid var(--cap-color, var(--accent));
783 border-radius: var(--r);
784 background: var(--bg);
785 overflow: hidden;
786 transition: transform 0.15s;
787 }
788 .cap-showcase-card:hover { transform: translateY(-2px); }
789 .cap-showcase-header {
790 padding: 14px 18px;
791 border-bottom: 1px solid var(--border);
792 background: var(--bg2);
793 display: flex;
794 align-items: center;
795 gap: 12px;
796 flex-wrap: wrap;
797 }
798 .cap-showcase-badge {
799 font-size: 12px;
800 font-family: var(--mono);
801 padding: 3px 10px;
802 border-radius: 4px;
803 border: 1px solid;
804 white-space: nowrap;
805 }
806 .cap-showcase-sub {
807 font-size: 12px;
808 color: var(--mute);
809 font-style: italic;
810 }
811 .cap-showcase-body { padding: 16px 18px; }
812 .cap-showcase-desc {
813 font-size: 13px;
814 color: var(--mute);
815 margin-bottom: 14px;
816 line-height: 1.6;
817 }
818 .cap-showcase-desc strong { color: var(--text); }
819 .cap-showcase-output {
820 background: #0a0e14;
821 border: 1px solid var(--border);
822 border-radius: 5px;
823 padding: 12px 14px;
824 font-family: var(--mono);
825 font-size: 11.5px;
826 color: #abb2bf;
827 white-space: pre;
828 overflow-x: auto;
829 line-height: 1.65;
830 }
831 .cap-showcase-domain-grid {
832 display: flex;
833 flex-direction: column;
834 gap: 10px;
835 }
836 .crdt-mini-grid {
837 display: grid;
838 grid-template-columns: 1fr 1fr;
839 gap: 10px;
840 }
841 @media (max-width: 700px) { .crdt-mini-grid { grid-template-columns: 1fr; } }
842
843 /* ---- Three steps ---- */
844 .steps-grid {
845 display: grid;
846 grid-template-columns: repeat(3, 1fr);
847 gap: 24px;
848 }
849 @media (max-width: 800px) { .steps-grid { grid-template-columns: 1fr; } }
850 .step-card {
851 border: 1px solid var(--border);
852 border-radius: var(--r);
853 background: var(--bg2);
854 padding: 24px;
855 position: relative;
856 }
857 .step-num {
858 font-family: var(--mono);
859 font-size: 11px;
860 color: var(--accent);
861 font-weight: 700;
862 text-transform: uppercase;
863 letter-spacing: 1px;
864 margin-bottom: 10px;
865 }
866 .step-title { font-size: 17px; font-weight: 700; margin-bottom: 8px; }
867 .step-desc { font-size: 13px; color: var(--mute); line-height: 1.6; margin-bottom: 16px; }
868 .step-cmd {
869 font-family: var(--mono);
870 font-size: 12px;
871 background: var(--bg3);
872 border: 1px solid var(--border);
873 border-radius: 5px;
874 padding: 10px 14px;
875 color: var(--accent2);
876 }
877
878 /* ---- Code block ---- */
879 .code-wrap {
880 border: 1px solid var(--border);
881 border-radius: var(--r);
882 overflow: hidden;
883 }
884 .code-bar {
885 background: var(--bg3);
886 border-bottom: 1px solid var(--border);
887 padding: 8px 16px;
888 display: flex;
889 align-items: center;
890 gap: 8px;
891 }
892 .code-bar-dot {
893 width: 10px; height: 10px; border-radius: 50%;
894 }
895 .code-bar-title {
896 font-family: var(--mono);
897 font-size: 12px;
898 color: var(--mute);
899 margin-left: 6px;
900 }
901 .code-body {
902 background: #0a0e14;
903 padding: 20px 24px;
904 font-family: var(--mono);
905 font-size: 12.5px;
906 line-height: 1.7;
907 color: #abb2bf;
908 white-space: pre;
909 overflow-x: auto;
910 }
911 /* Simple syntax highlights */
912 .kw { color: #c678dd; }
913 .kw2 { color: #e06c75; }
914 .fn { color: #61afef; }
915 .str { color: #98c379; }
916 .cmt { color: #5c6370; font-style: italic; }
917 .cls { color: #e5c07b; }
918 .typ { color: #56b6c2; }
919
920 /* ---- Active domains grid ---- */
921 .domain-grid {
922 display: grid;
923 grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
924 gap: 20px;
925 }
926 .domain-card {
927 border: 1px solid var(--border);
928 border-radius: var(--r);
929 background: var(--bg2);
930 overflow: hidden;
931 transition: border-color 0.2s, transform 0.15s;
932 }
933 .domain-card:hover { border-color: var(--accent); transform: translateY(-2px); }
934 .domain-card.active-domain { border-color: rgba(63,185,80,0.4); }
935 .domain-card-hdr {
936 background: var(--bg3);
937 padding: 12px 16px;
938 border-bottom: 1px solid var(--border);
939 display: flex;
940 align-items: center;
941 gap: 10px;
942 }
943 .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); }
944 .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); }
945 .active-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); margin-left: auto; }
946 .domain-name-lg { font-family: var(--mono); font-size: 16px; font-weight: 700; color: var(--text); }
947 .domain-card-body { padding: 16px; }
948 .domain-desc { font-size: 13px; color: var(--mute); margin-bottom: 12px; }
949 .cap-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
950 .cap-pill { font-size: 10px; padding: 2px 8px; border-radius: 12px; border: 1px solid var(--border); color: var(--mute); background: var(--bg3); }
951 .cap-pill.cap-crdt { border-color: rgba(188,140,255,0.4); color: var(--purple); background: rgba(188,140,255,0.08); }
952 .cap-pill.cap-ot-merge { border-color: rgba(88,166,255,0.4); color: var(--accent2); background: rgba(88,166,255,0.08); }
953 .cap-pill.cap-domain-schema { border-color: rgba(63,185,80,0.4); color: var(--green); background: rgba(63,185,80,0.08); }
954 .cap-pill.cap-typed-deltas { border-color: rgba(249,168,37,0.4); color: #f9a825; background: rgba(249,168,37,0.08); }
955 .dim-row { font-size: 11px; color: var(--dim); }
956 .dim-tag { color: var(--mute); }
957
958 /* ---- Planned domains ---- */
959 .planned-grid {
960 display: grid;
961 grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
962 gap: 16px;
963 }
964 .planned-card {
965 border: 1px solid var(--border);
966 border-radius: var(--r);
967 background: var(--bg2);
968 padding: 20px 16px;
969 display: flex;
970 flex-direction: column;
971 gap: 8px;
972 transition: border-color 0.2s, transform 0.15s;
973 }
974 .planned-card:hover { border-color: var(--card-accent,var(--accent)); transform: translateY(-2px); }
975 .planned-card.yours { border: 2px dashed var(--accent); background: rgba(79,142,247,0.04); }
976 .planned-icon { line-height: 0; }
977 .planned-icon .icon { width: 28px; height: 28px; }
978 .planned-name { font-size: 15px; font-weight: 700; color: var(--text); }
979 .planned-tag { font-size: 12px; color: var(--mute); line-height: 1.5; }
980 .planned-dims { font-size: 10px; color: var(--dim); }
981 .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; }
982 .cta-btn {
983 display: inline-block;
984 margin-top: 6px;
985 font-size: 12px;
986 font-weight: 600;
987 color: var(--accent2);
988 border: 1px solid rgba(88,166,255,0.4);
989 border-radius: 4px;
990 padding: 4px 12px;
991 text-decoration: none;
992 transition: background 0.15s;
993 }
994 .cta-btn:hover { background: rgba(88,166,255,0.1); text-decoration: none; }
995
996 /* ---- Distribution tiers ---- */
997 .dist-grid {
998 display: grid;
999 grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
1000 gap: 24px;
1001 }
1002 .dist-card {
1003 border: 1px solid var(--border);
1004 border-top: 3px solid var(--dist-color, var(--accent));
1005 border-radius: var(--r);
1006 background: var(--bg2);
1007 padding: 24px;
1008 transition: transform 0.15s;
1009 }
1010 .dist-card:hover { transform: translateY(-2px); }
1011 .dist-header { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 14px; }
1012 .dist-icon { line-height: 0; flex-shrink: 0; }
1013 .dist-icon .icon { width: 26px; height: 26px; }
1014 .dist-tier { font-family: var(--mono); font-size: 11px; color: var(--dist-color,var(--accent)); letter-spacing: 1px; text-transform: uppercase; font-weight: 700; }
1015 .dist-title { font-size: 14px; font-weight: 600; color: var(--text); margin-top: 2px; }
1016 .dist-desc { font-size: 13px; color: var(--mute); margin-bottom: 16px; line-height: 1.6; }
1017 .dist-steps { list-style: none; counter-reset: step; display: flex; flex-direction: column; gap: 6px; }
1018 .dist-steps li { counter-increment: step; display: flex; align-items: flex-start; gap: 8px; font-size: 12px; color: var(--mute); }
1019 .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; }
1020 .dist-steps code { background: var(--bg3); border: 1px solid var(--border); border-radius: 4px; padding: 1px 6px; font-size: 11px; }
1021
1022 /* ---- MuseHub teaser ---- */
1023 .musehub-section {
1024 background: linear-gradient(135deg, #0d1117 0%, #1a0d2e 50%, #0d1117 100%);
1025 padding: 80px 40px;
1026 text-align: center;
1027 border-top: 1px solid var(--border);
1028 }
1029 .musehub-logo {
1030 margin-bottom: 20px;
1031 line-height: 0;
1032 }
1033 .musehub-logo .icon { width: 48px; height: 48px; stroke: #bc8cff; }
1034 .musehub-section h2 {
1035 font-size: 36px;
1036 font-weight: 800;
1037 letter-spacing: -1px;
1038 margin-bottom: 12px;
1039 }
1040 .musehub-section h2 span {
1041 background: linear-gradient(135deg, #bc8cff, #4f8ef7);
1042 -webkit-background-clip: text;
1043 -webkit-text-fill-color: transparent;
1044 background-clip: text;
1045 }
1046 .musehub-desc {
1047 font-size: 16px;
1048 color: var(--mute);
1049 max-width: 560px;
1050 margin: 0 auto 36px;
1051 }
1052 .musehub-desc strong { color: var(--text); }
1053 .musehub-features {
1054 display: flex;
1055 gap: 24px;
1056 justify-content: center;
1057 flex-wrap: wrap;
1058 margin-bottom: 40px;
1059 }
1060 .mh-feature {
1061 background: var(--bg2);
1062 border: 1px solid rgba(188,140,255,0.2);
1063 border-radius: var(--r);
1064 padding: 16px 20px;
1065 text-align: left;
1066 min-width: 180px;
1067 }
1068 .mh-feature-icon { margin-bottom: 10px; line-height: 0; }
1069 .mh-feature-icon .icon { width: 22px; height: 22px; stroke: #bc8cff; }
1070 .mh-feature-title { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 4px; }
1071 .mh-feature-desc { font-size: 12px; color: var(--mute); }
1072 .musehub-status {
1073 display: inline-flex;
1074 align-items: center;
1075 gap: 8px;
1076 background: rgba(188,140,255,0.1);
1077 border: 1px solid rgba(188,140,255,0.3);
1078 border-radius: 20px;
1079 padding: 8px 20px;
1080 font-size: 13px;
1081 color: var(--purple);
1082 }
1083 .mh-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--purple); animation: pulse 2s ease-in-out infinite; }
1084 @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
1085
1086 /* ---- Footer ---- */
1087 footer {
1088 background: var(--bg2);
1089 border-top: 1px solid var(--border);
1090 padding: 24px 40px;
1091 display: flex;
1092 justify-content: space-between;
1093 align-items: center;
1094 flex-wrap: wrap;
1095 gap: 12px;
1096 font-size: 13px;
1097 color: var(--mute);
1098 }
1099 footer a { color: var(--accent2); }
1100
1101 /* ---- Nav ---- */
1102 nav {
1103 background: var(--bg2);
1104 border-bottom: 1px solid var(--border);
1105 padding: 0 40px;
1106 display: flex;
1107 align-items: center;
1108 gap: 0;
1109 height: 52px;
1110 }
1111 .nav-logo {
1112 font-family: var(--mono);
1113 font-size: 16px;
1114 font-weight: 700;
1115 color: var(--accent2);
1116 margin-right: 32px;
1117 text-decoration: none;
1118 }
1119 .nav-logo:hover { text-decoration: none; }
1120 .nav-link {
1121 font-size: 13px;
1122 color: var(--mute);
1123 padding: 0 14px;
1124 height: 100%;
1125 display: flex;
1126 align-items: center;
1127 border-bottom: 2px solid transparent;
1128 text-decoration: none;
1129 transition: color 0.15s, border-color 0.15s;
1130 }
1131 .nav-link:hover { color: var(--text); text-decoration: none; }
1132 .nav-link.current { color: var(--text); border-bottom-color: var(--accent); }
1133 .nav-spacer { flex: 1; }
1134 .nav-badge {
1135 font-size: 11px;
1136 background: rgba(79,142,247,0.12);
1137 border: 1px solid rgba(79,142,247,0.3);
1138 color: var(--accent2);
1139 border-radius: 4px;
1140 padding: 2px 8px;
1141 font-family: var(--mono);
1142 }
1143 </style>
1144 </head>
1145 <body>
1146
1147 <nav>
1148 <a class="nav-logo" href="#">muse</a>
1149 <a class="nav-link" href="tour_de_force.html">Demo</a>
1150 <a class="nav-link current" href="index.html">Domain Registry</a>
1151 <a class="nav-link" href="../docs/guide/plugin-authoring-guide.md">Plugin Guide</a>
1152 <div class="nav-spacer"></div>
1153 <span class="nav-badge">v0.1.1</span>
1154 </nav>
1155
1156 <!-- =================== HERO =================== -->
1157 <div class="hero">
1158 <h1 class="hero-wordmark">muse</h1>
1159 <div class="hero-version-any">Version Anything</div>
1160 <p class="hero-sub">
1161 One protocol. Any domain. <strong>Six methods</strong> between you and a
1162 complete version control system — branching, merging, conflict resolution,
1163 time-travel, and typed diffs — for free.
1164 </p>
1165 <div class="hero-cta-row">
1166 <a class="btn-primary" href="#build">Build a Domain Plugin</a>
1167 <a class="btn-outline" href="tour_de_force.html">Watch the Demo →</a>
1168 </div>
1169 <div class="domain-ticker">
1170 <div class="ticker-track">
1171 <span class="ticker-item active">{{ICON_MUSIC}} music</span>
1172 <span class="ticker-item">{{ICON_GENOMICS}} genomics</span>
1173 <span class="ticker-item">{{ICON_CUBE}} 3d-spatial</span>
1174 <span class="ticker-item">{{ICON_TRENDING}} financial</span>
1175 <span class="ticker-item">{{ICON_ATOM}} simulation</span>
1176 <span class="ticker-item">{{ICON_ACTIVITY}} proteomics</span>
1177 <span class="ticker-item">{{ICON_PEN_TOOL}} cad</span>
1178 <span class="ticker-item">{{ICON_ZAP}} game-state</span>
1179 <span class="ticker-item">{{ICON_PLUS}} your-domain</span>
1180 <!-- duplicate for seamless loop -->
1181 <span class="ticker-item active">{{ICON_MUSIC}} music</span>
1182 <span class="ticker-item">{{ICON_GENOMICS}} genomics</span>
1183 <span class="ticker-item">{{ICON_CUBE}} 3d-spatial</span>
1184 <span class="ticker-item">{{ICON_TRENDING}} financial</span>
1185 <span class="ticker-item">{{ICON_ATOM}} simulation</span>
1186 <span class="ticker-item">{{ICON_ACTIVITY}} proteomics</span>
1187 <span class="ticker-item">{{ICON_PEN_TOOL}} cad</span>
1188 <span class="ticker-item">{{ICON_ZAP}} game-state</span>
1189 <span class="ticker-item">{{ICON_PLUS}} your-domain</span>
1190 </div>
1191 </div>
1192 </div>
1193
1194 <!-- =================== PROTOCOL =================== -->
1195 <section id="protocol">
1196 <div class="inner">
1197 <div class="section-eyebrow">The Contract</div>
1198 <h2>The MuseDomainPlugin Protocol</h2>
1199 <p class="section-lead">
1200 Every domain — music, genomics, 3D spatial, financial models — implements
1201 the same <strong>six-method protocol</strong>. The core engine handles
1202 everything else: content-addressed storage, DAG, branches, log, merge base,
1203 cherry-pick, revert, stash, tags.
1204 </p>
1205
1206 <div class="proto-layout">
1207 <div class="stat-strip">
1208 <div class="stat-cell"><span class="stat-num">6</span><span class="stat-lbl">methods to implement</span></div>
1209 <div class="stat-cell"><span class="stat-num">14</span><span class="stat-lbl">CLI commands, free</span></div>
1210 <div class="stat-cell"><span class="stat-num">∞</span><span class="stat-lbl">domains possible</span></div>
1211 <div class="stat-cell"><span class="stat-num">0</span><span class="stat-lbl">core changes needed</span></div>
1212 </div>
1213 <div class="proto-table">
1214 <div class="proto-row hdr">
1215 <div class="proto-method">Method</div>
1216 <div class="proto-sig">Signature</div>
1217 <div class="proto-desc">Purpose</div>
1218 </div>
1219 <div class="proto-row">
1220 <div class="proto-method">snapshot</div>
1221 <div class="proto-sig">snapshot(live) → StateSnapshot</div>
1222 <div class="proto-desc">Capture current state as a content-addressable blob</div>
1223 </div>
1224 <div class="proto-row">
1225 <div class="proto-method">diff</div>
1226 <div class="proto-sig">diff(base, target) → StateDelta</div>
1227 <div class="proto-desc">Compute minimal change between two snapshots (added · removed · modified)</div>
1228 </div>
1229 <div class="proto-row">
1230 <div class="proto-method">merge</div>
1231 <div class="proto-sig">merge(base, left, right) → MergeResult</div>
1232 <div class="proto-desc">Three-way reconcile divergent state lines; surface conflicts per dimension</div>
1233 </div>
1234 <div class="proto-row">
1235 <div class="proto-method">drift</div>
1236 <div class="proto-sig">drift(committed, live) → DriftReport</div>
1237 <div class="proto-desc">Detect uncommitted changes between HEAD and working state</div>
1238 </div>
1239 <div class="proto-row">
1240 <div class="proto-method">apply</div>
1241 <div class="proto-sig">apply(delta, live) → LiveState</div>
1242 <div class="proto-desc">Apply a delta during checkout to reconstruct historical state</div>
1243 </div>
1244 <div class="proto-row">
1245 <div class="proto-method">schema</div>
1246 <div class="proto-sig">schema() → DomainSchema</div>
1247 <div class="proto-desc">Declare data structure — drives diff algorithm selection per dimension</div>
1248 </div>
1249 </div>
1250 </div>
1251 </div>
1252 </section>
1253
1254 <!-- =================== ENGINE CAPABILITIES =================== -->
1255 <section id="capabilities" style="background:var(--bg2)">
1256 <div class="inner">
1257 <div class="section-eyebrow">Engine Capabilities</div>
1258 <h2>What Every Plugin Gets for Free</h2>
1259 <p class="section-lead">
1260 The core engine provides four advanced capabilities that any domain plugin
1261 can opt into. Implement the protocol — the engine does the rest.
1262 </p>
1263
1264 <div class="cap-showcase-grid">
1265
1266 <div class="cap-showcase-card" style="--cap-color:#f9a825">
1267 <div class="cap-showcase-header">
1268 <span class="cap-showcase-badge" style="color:#f9a825;background:#f9a82515;border-color:#f9a82540">
1269 {{ICON_CODE}} Typed Delta Algebra
1270 </span>
1271 <span class="cap-showcase-sub">StructuredDelta — every change is a typed operation</span>
1272 </div>
1273 <div class="cap-showcase-body">
1274 <p class="cap-showcase-desc">
1275 Unlike Git's blob diffs, Muse deltas are <strong>typed objects</strong>:
1276 <code>InsertOp</code>, <code>ReplaceOp</code>, <code>DeleteOp</code> — each
1277 carrying the address, before/after hashes, and affected dimensions.
1278 Machine-readable with <code>muse show --json</code>.
1279 </p>
1280 <pre class="cap-showcase-output" data-lang="json">{{TYPED_DELTA_EXAMPLE}}</pre>
1281 </div>
1282 </div>
1283
1284 <div class="cap-showcase-card" style="--cap-color:#58a6ff">
1285 <div class="cap-showcase-header">
1286 <span class="cap-showcase-badge" style="color:#58a6ff;background:#58a6ff15;border-color:#58a6ff40">
1287 {{ICON_LAYERS}} Domain Schema
1288 </span>
1289 <span class="cap-showcase-sub">Per-domain dimensions drive diff algorithm selection</span>
1290 </div>
1291 <div class="cap-showcase-body">
1292 <p class="cap-showcase-desc">
1293 Each plugin's <code>schema()</code> method declares its dimensions and merge mode.
1294 The engine uses this to select the right diff algorithm per dimension and to
1295 surface only the dimensions that actually conflict.
1296 </p>
1297 <div class="cap-showcase-domain-grid" id="schema-domain-grid">
1298 {{ACTIVE_DOMAINS}}
1299 </div>
1300 </div>
1301 </div>
1302
1303 <div class="cap-showcase-card" style="--cap-color:#ef5350">
1304 <div class="cap-showcase-header">
1305 <span class="cap-showcase-badge" style="color:#ef5350;background:#ef535015;border-color:#ef535040">
1306 {{ICON_GIT_MERGE}} OT Merge
1307 </span>
1308 <span class="cap-showcase-sub">Operational transformation — independent ops commute automatically</span>
1309 </div>
1310 <div class="cap-showcase-body">
1311 <p class="cap-showcase-desc">
1312 Plugins implementing <strong>StructuredMergePlugin</strong> get operational
1313 transformation. Operations at different addresses commute automatically —
1314 only operations on the same address with incompatible intent surface a conflict.
1315 </p>
1316 <pre class="cap-showcase-output">{{OT_MERGE_EXAMPLE}}</pre>
1317 </div>
1318 </div>
1319
1320 <div class="cap-showcase-card" style="--cap-color:#bc8cff">
1321 <div class="cap-showcase-header">
1322 <span class="cap-showcase-badge" style="color:#bc8cff;background:#bc8cff15;border-color:#bc8cff40">
1323 {{ICON_ZAP}} CRDT Primitives
1324 </span>
1325 <span class="cap-showcase-sub">Convergent merge — any two replicas always reach the same state</span>
1326 </div>
1327 <div class="cap-showcase-body">
1328 <p class="cap-showcase-desc">
1329 Plugins implementing <strong>CRDTPlugin</strong> get four battle-tested
1330 convergent data structures. No coordination required between replicas.
1331 </p>
1332 <div class="crdt-mini-grid">
1333 {{CRDT_CARDS}}
1334 </div>
1335 </div>
1336 </div>
1337
1338 </div>
1339 </div>
1340 </section>
1341
1342 <!-- =================== BUILD =================== -->
1343 <section id="build" style="background:var(--bg)">
1344 <div class="inner">
1345 <div class="section-eyebrow">Build</div>
1346 <h2>Build in Three Steps</h2>
1347 <p class="section-lead">
1348 One command scaffolds the entire plugin skeleton. You fill in six methods.
1349 The full VCS follows.
1350 </p>
1351
1352 <div class="steps-grid">
1353 <div class="step-card">
1354 <div class="step-num">Step 1 · Scaffold</div>
1355 <div class="step-title">Generate the skeleton</div>
1356 <div class="step-desc">
1357 One command creates the plugin directory, class, and all six method stubs
1358 with full type annotations.
1359 </div>
1360 <div class="step-cmd">muse domains --new genomics</div>
1361 </div>
1362 <div class="step-card">
1363 <div class="step-num">Step 2 · Implement</div>
1364 <div class="step-title">Fill in the six methods</div>
1365 <div class="step-desc">
1366 Replace each <code>raise NotImplementedError</code> with your domain's
1367 snapshot, diff, merge, drift, apply, and schema logic.
1368 </div>
1369 <div class="step-cmd">vim muse/plugins/genomics/plugin.py</div>
1370 </div>
1371 <div class="step-card">
1372 <div class="step-num">Step 3 · Use</div>
1373 <div class="step-title">Full VCS, instantly</div>
1374 <div class="step-desc">
1375 Register in <code>registry.py</code>, then every Muse command works
1376 for your domain out of the box.
1377 </div>
1378 <div class="step-cmd">muse init --domain genomics</div>
1379 </div>
1380 </div>
1381 </div>
1382 </section>
1383
1384 <!-- =================== CODE =================== -->
1385 <section id="code">
1386 <div class="inner">
1387 <div class="section-eyebrow">The Scaffold</div>
1388 <h2>What <code>muse domains --new genomics</code> produces</h2>
1389 <p class="section-lead">
1390 A fully typed, immediately runnable plugin skeleton. Every method has the
1391 correct signature. You replace the stubs — the protocol does the rest.
1392 </p>
1393 <div class="code-wrap">
1394 <div class="code-bar">
1395 <div class="code-bar-dot" style="background:#ff5f57"></div>
1396 <div class="code-bar-dot" style="background:#febc2e"></div>
1397 <div class="code-bar-dot" style="background:#28c840"></div>
1398 <span class="code-bar-title">muse/plugins/genomics/plugin.py</span>
1399 </div>
1400 <div class="code-body">{{SCAFFOLD_SNIPPET}}</div>
1401 </div>
1402 <p style="margin-top:16px;font-size:13px;color:var(--mute)">
1403 Full walkthrough →
1404 <a href="../docs/guide/plugin-authoring-guide.md">docs/guide/plugin-authoring-guide.md</a>
1405 · CRDT extension →
1406 <a href="../docs/guide/crdt-reference.md">docs/guide/crdt-reference.md</a>
1407 </p>
1408 </div>
1409 </section>
1410
1411 <!-- =================== ACTIVE DOMAINS =================== -->
1412 <section id="registry" style="background:var(--bg2)">
1413 <div class="inner">
1414 <div class="section-eyebrow">Registry</div>
1415 <h2>Registered Domains</h2>
1416 <p class="section-lead">
1417 Domains currently registered in this Muse instance. The active domain
1418 is the one used when you run <code>muse commit</code>, <code>muse diff</code>,
1419 and all other commands.
1420 </p>
1421 <div class="domain-grid">
1422 {{ACTIVE_DOMAINS}}
1423 </div>
1424 </div>
1425 </section>
1426
1427 <!-- =================== PLANNED ECOSYSTEM =================== -->
1428 <section id="ecosystem">
1429 <div class="inner">
1430 <div class="section-eyebrow">Ecosystem</div>
1431 <h2>The Plugin Ecosystem</h2>
1432 <p class="section-lead">
1433 Music is the reference implementation. These are the domains planned
1434 next — and the slot waiting for yours.
1435 </p>
1436 <div class="planned-grid">
1437 {{PLANNED_DOMAINS}}
1438 </div>
1439 </div>
1440 </section>
1441
1442 <!-- =================== DISTRIBUTION =================== -->
1443 <section id="distribute" style="background:var(--bg2)">
1444 <div class="inner">
1445 <div class="section-eyebrow">Distribution</div>
1446 <h2>How to Share Your Plugin</h2>
1447 <p class="section-lead">
1448 Three tiers of distribution — from local prototype to globally searchable
1449 registry. Start local, publish when ready.
1450 </p>
1451 <div class="dist-grid">
1452 {{DIST_CARDS}}
1453 </div>
1454 </div>
1455 </section>
1456
1457 <!-- =================== MUSEHUB TEASER =================== -->
1458 <div class="musehub-section">
1459 <div class="musehub-logo">{{ICON_GLOBE}}</div>
1460 <h2><span>MuseHub</span> is coming</h2>
1461 <p class="musehub-desc">
1462 A <strong>centralized, searchable registry</strong> for Muse domain plugins —
1463 think npm or crates.io, but for any multidimensional versioned state.
1464 One command to publish. One command to install.
1465 </p>
1466 <div class="musehub-features">
1467 <div class="mh-feature">
1468 <div class="mh-feature-icon">{{ICON_SEARCH}}</div>
1469 <div class="mh-feature-title">Searchable</div>
1470 <div class="mh-feature-desc">Find plugins by domain, capability, or keyword</div>
1471 </div>
1472 <div class="mh-feature">
1473 <div class="mh-feature-icon">{{ICON_PACKAGE}}</div>
1474 <div class="mh-feature-title">Versioned</div>
1475 <div class="mh-feature-desc">Semantic versioning, pinned installs, changelogs</div>
1476 </div>
1477 <div class="mh-feature">
1478 <div class="mh-feature-icon">{{ICON_LOCK}}</div>
1479 <div class="mh-feature-title">Private registries</div>
1480 <div class="mh-feature-desc">Self-host for enterprise or research teams</div>
1481 </div>
1482 <div class="mh-feature">
1483 <div class="mh-feature-icon">{{ICON_ZAP}}</div>
1484 <div class="mh-feature-title">One command</div>
1485 <div class="mh-feature-desc"><code>muse init --domain @musehub/genomics</code></div>
1486 </div>
1487 </div>
1488 <div class="musehub-status">
1489 <div class="mh-dot"></div>
1490 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>
1491 </div>
1492 </div>
1493
1494 <footer>
1495 <span>Muse v0.1.1 · domain-agnostic version control for multidimensional state</span>
1496 <span>
1497 <a href="tour_de_force.html">Demo</a> ·
1498 <a href="https://github.com/cgcardona/muse">GitHub</a> ·
1499 <a href="../docs/guide/plugin-authoring-guide.md">Plugin Guide</a>
1500 </span>
1501 </footer>
1502
1503 <script>
1504 (function () {
1505 function esc(s) {
1506 return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1507 }
1508
1509 function tokenizeJSON(raw) {
1510 let html = '';
1511 let i = 0;
1512 while (i < raw.length) {
1513 // Comment line starting with #
1514 if (raw[i] === '#') {
1515 const end = raw.indexOf('\n', i);
1516 const line = end === -1 ? raw.slice(i) : raw.slice(i, end);
1517 html += '<span style="color:#5c6370;font-style:italic">' + esc(line) + '</span>';
1518 i += line.length;
1519 continue;
1520 }
1521 // String literal
1522 if (raw[i] === '"') {
1523 let j = i + 1;
1524 while (j < raw.length && raw[j] !== '"') {
1525 if (raw[j] === '\\') j++;
1526 j++;
1527 }
1528 j++;
1529 const str = raw.slice(i, j);
1530 // Peek past whitespace — key if followed by ':'
1531 let k = j;
1532 while (k < raw.length && (raw[k] === ' ' || raw[k] === '\t')) k++;
1533 const color = raw[k] === ':' ? '#61afef' : '#98c379';
1534 html += '<span style="color:' + color + '">' + esc(str) + '</span>';
1535 i = j;
1536 continue;
1537 }
1538 // Number (including negative)
1539 if (/[0-9]/.test(raw[i]) || (raw[i] === '-' && /[0-9]/.test(raw[i + 1] || ''))) {
1540 let j = i;
1541 if (raw[j] === '-') j++;
1542 while (j < raw.length && /[0-9.eE+\-]/.test(raw[j])) j++;
1543 html += '<span style="color:#d19a66">' + esc(raw.slice(i, j)) + '</span>';
1544 i = j;
1545 continue;
1546 }
1547 // Keywords: true / false / null
1548 const kws = [['true', '#c678dd'], ['false', '#c678dd'], ['null', '#c678dd']];
1549 let matched = false;
1550 for (const [kw, col] of kws) {
1551 if (raw.slice(i, i + kw.length) === kw) {
1552 html += '<span style="color:' + col + '">' + kw + '</span>';
1553 i += kw.length;
1554 matched = true;
1555 break;
1556 }
1557 }
1558 if (matched) continue;
1559 // Default character (punctuation / whitespace)
1560 html += esc(raw[i]);
1561 i++;
1562 }
1563 return html;
1564 }
1565
1566 document.querySelectorAll('pre[data-lang="json"]').forEach(function (pre) {
1567 pre.innerHTML = tokenizeJSON(pre.textContent);
1568 });
1569 })();
1570 </script>
1571
1572 </body>
1573 </html>
1574 """
1575
1576
1577 # ---------------------------------------------------------------------------
1578 # Entry point
1579 # ---------------------------------------------------------------------------
1580
1581 if __name__ == "__main__":
1582 import argparse
1583
1584 parser = argparse.ArgumentParser(
1585 description="Generate the Muse domain registry HTML page"
1586 )
1587 parser.add_argument(
1588 "--out",
1589 default=str(_ROOT / "artifacts" / "domain_registry.html"),
1590 help="Output HTML path",
1591 )
1592 args = parser.parse_args()
1593
1594 out_path = pathlib.Path(args.out)
1595 out_path.parent.mkdir(parents=True, exist_ok=True)
1596
1597 print("Generating domain_registry.html...")
1598 render(out_path)
1599 print(f"Open: file://{out_path.resolve()}")