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