cgcardona / muse public
render_domain_registry.py python
1434 lines 51.0 KB
ffec191c feat: domain registry is the landing page — muse / Version Anything her… 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": "∪", "output": orset_out},
86 {"type": "LWWRegister", "sub": "Last-Write-Wins Register", "color": "#58a6ff", "icon": "✎", "output": lww_out},
87 {"type": "GCounter", "sub": "Grow-Only Distributed Counter", "color": "#3fb950", "icon": "↑", "output": gc_out},
88 {"type": "VectorClock", "sub": "Causal Ordering", "color": "#f9a825", "icon": "⊕", "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 # Planned / aspirational domains
224 # ---------------------------------------------------------------------------
225
226 _PLANNED_DOMAINS = [
227 {
228 "name": "Genomics",
229 "icon": "🧬",
230 "status": "planned",
231 "tagline": "Version sequences, variants, and annotations",
232 "dimensions": ["sequence", "variants", "annotations", "metadata"],
233 "color": "#3fb950",
234 },
235 {
236 "name": "3D / Spatial",
237 "icon": "🌐",
238 "status": "planned",
239 "tagline": "Merge spatial fields, meshes, and simulation frames",
240 "dimensions": ["geometry", "materials", "physics", "temporal"],
241 "color": "#58a6ff",
242 },
243 {
244 "name": "Financial",
245 "icon": "📈",
246 "status": "planned",
247 "tagline": "Track model versions, alpha signals, and risk state",
248 "dimensions": ["signals", "positions", "risk", "parameters"],
249 "color": "#f9a825",
250 },
251 {
252 "name": "Scientific Simulation",
253 "icon": "⚛️",
254 "status": "planned",
255 "tagline": "Snapshot simulation state across timesteps and parameter spaces",
256 "dimensions": ["state", "parameters", "observables", "checkpoints"],
257 "color": "#ab47bc",
258 },
259 {
260 "name": "Your Domain",
261 "icon": "✦",
262 "status": "yours",
263 "tagline": "Six methods. Any multidimensional state. Full VCS for free.",
264 "dimensions": ["your_dim_1", "your_dim_2", "..."],
265 "color": "#4f8ef7",
266 },
267 ]
268
269 # ---------------------------------------------------------------------------
270 # Distribution model description
271 # ---------------------------------------------------------------------------
272
273 _DISTRIBUTION_LEVELS = [
274 {
275 "tier": "Local",
276 "icon": "💻",
277 "title": "Local plugin (right now)",
278 "color": "#3fb950",
279 "steps": [
280 "muse domains --new &lt;name&gt;",
281 "Implement 6 methods in muse/plugins/&lt;name&gt;/plugin.py",
282 "Register in muse/plugins/registry.py",
283 "muse init --domain &lt;name&gt;",
284 ],
285 "desc": "Works today. Scaffold → implement → register. "
286 "Your plugin lives alongside the core.",
287 },
288 {
289 "tier": "Shareable",
290 "icon": "📦",
291 "title": "pip-installable package (right now)",
292 "color": "#58a6ff",
293 "steps": [
294 "Package your plugin as a Python module",
295 "pip install git+https://github.com/you/muse-plugin-genomics",
296 "Register the entry-point in pyproject.toml",
297 "muse init --domain genomics",
298 ],
299 "desc": "Share your plugin as a standard Python package. "
300 "Anyone with pip can install and use it.",
301 },
302 {
303 "tier": "MuseHub",
304 "icon": "🌐",
305 "title": "Centralized registry (coming — MuseHub)",
306 "color": "#bc8cff",
307 "steps": [
308 "musehub publish muse-plugin-genomics",
309 "musehub search genomics",
310 "muse init --domain @musehub/genomics",
311 "Browse plugins at musehub.io",
312 ],
313 "desc": "MuseHub is a planned centralized registry — npm for Muse plugins. "
314 "Versioned, searchable, one-command install.",
315 },
316 ]
317
318
319 # ---------------------------------------------------------------------------
320 # HTML template
321 # ---------------------------------------------------------------------------
322
323 def _render_capability_card(cap: dict) -> str:
324 color = cap["color"]
325 return f"""
326 <div class="cap-showcase-card" style="--cap-color:{color}">
327 <div class="cap-showcase-header">
328 <span class="cap-showcase-badge" style="color:{color};background:{color}15;border-color:{color}40">
329 {cap['icon']} {cap['type']}
330 </span>
331 <span class="cap-showcase-sub">{cap['sub']}</span>
332 </div>
333 <div class="cap-showcase-body">
334 <pre class="cap-showcase-output">{cap['output']}</pre>
335 </div>
336 </div>"""
337
338
339 def _render_domain_card(d: dict) -> str:
340 domain = d.get("domain", "unknown")
341 active = d.get("active") == "true"
342 schema = d.get("schema", {})
343 desc = schema.get("description", "")
344 dims = schema.get("dimensions", [])
345 caps = d.get("capabilities", [])
346
347 cap_html = " ".join(
348 f'<span class="cap-pill cap-{c.lower().replace(" ","-")}">{c}</span>'
349 for c in caps
350 )
351 dim_html = " · ".join(
352 f'<span class="dim-tag">{dim["name"]}</span>' for dim in dims
353 )
354
355 status_cls = "active-badge" if active else "reg-badge"
356 status_text = "● active" if active else "○ registered"
357 dot = '<span class="active-dot"></span>' if active else ""
358
359 short_desc = desc[:150] + ("…" if len(desc) > 150 else "")
360
361 return f"""
362 <div class="domain-card{' active-domain' if active else ''}">
363 <div class="domain-card-hdr">
364 <span class="{status_cls}">{status_text}</span>
365 <span class="domain-name-lg">{domain}</span>
366 {dot}
367 </div>
368 <div class="domain-card-body">
369 <p class="domain-desc">{short_desc}</p>
370 <div class="cap-row">{cap_html}</div>
371 <div class="dim-row"><strong>Dimensions:</strong> {dim_html}</div>
372 </div>
373 </div>"""
374
375
376 def _render_planned_card(p: dict) -> str:
377 dims = " · ".join(f'<span class="dim-tag">{d}</span>' for d in p["dimensions"])
378 cls = "planned-card yours" if p["status"] == "yours" else "planned-card"
379 return f"""
380 <div class="{cls}" style="--card-accent:{p['color']}">
381 <div class="planned-icon">{p['icon']}</div>
382 <div class="planned-name">{p['name']}</div>
383 <div class="planned-tag">{p['tagline']}</div>
384 <div class="planned-dims">{dims}</div>
385 {'<a class="cta-btn" href="#build">Build it →</a>' if p["status"] == "yours" else '<span class="coming-soon">coming soon</span>'}
386 </div>"""
387
388
389 def _render_dist_card(d: dict) -> str:
390 steps = "".join(
391 f'<li><code>{s}</code></li>' for s in d["steps"]
392 )
393 return f"""
394 <div class="dist-card" style="--dist-color:{d['color']}">
395 <div class="dist-header">
396 <span class="dist-icon">{d['icon']}</span>
397 <div>
398 <div class="dist-tier">{d['tier']}</div>
399 <div class="dist-title">{d['title']}</div>
400 </div>
401 </div>
402 <p class="dist-desc">{d['desc']}</p>
403 <ol class="dist-steps">{steps}</ol>
404 </div>"""
405
406
407 def render(output_path: pathlib.Path) -> None:
408 """Generate the domain registry HTML page."""
409 print(" Loading live domain data...")
410 domains = _load_domains()
411 print(f" Found {len(domains)} registered domain(s)")
412
413 print(" Computing live CRDT demos...")
414 crdt_demos = _compute_crdt_demos()
415
416 active_domains_html = "\n".join(_render_domain_card(d) for d in domains)
417 planned_html = "\n".join(_render_planned_card(p) for p in _PLANNED_DOMAINS)
418 dist_html = "\n".join(_render_dist_card(d) for d in _DISTRIBUTION_LEVELS)
419 crdt_cards_html = "\n".join(_render_capability_card(c) for c in crdt_demos)
420
421 html = _HTML_TEMPLATE.replace("{{ACTIVE_DOMAINS}}", active_domains_html)
422 html = html.replace("{{PLANNED_DOMAINS}}", planned_html)
423 html = html.replace("{{DIST_CARDS}}", dist_html)
424 html = html.replace("{{SCAFFOLD_SNIPPET}}", _SCAFFOLD_SNIPPET)
425 html = html.replace("{{TYPED_DELTA_EXAMPLE}}", _TYPED_DELTA_EXAMPLE)
426 html = html.replace("{{OT_MERGE_EXAMPLE}}", _OT_MERGE_EXAMPLE)
427 html = html.replace("{{CRDT_CARDS}}", crdt_cards_html)
428
429 output_path.write_text(html, encoding="utf-8")
430 size_kb = output_path.stat().st_size // 1024
431 print(f" HTML written ({size_kb}KB) → {output_path}")
432
433 # Also write as index.html so the domain registry IS the landing page.
434 index_path = output_path.parent / "index.html"
435 index_path.write_text(html, encoding="utf-8")
436 print(f" Landing page mirrored → {index_path}")
437
438
439 # ---------------------------------------------------------------------------
440 # Large HTML template
441 # ---------------------------------------------------------------------------
442
443 _HTML_TEMPLATE = """\
444 <!DOCTYPE html>
445 <html lang="en">
446 <head>
447 <meta charset="utf-8">
448 <meta name="viewport" content="width=device-width, initial-scale=1">
449 <title>Muse — Version Anything</title>
450 <style>
451 *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
452 :root {
453 --bg: #0d1117;
454 --bg2: #161b22;
455 --bg3: #21262d;
456 --border: #30363d;
457 --text: #e6edf3;
458 --mute: #8b949e;
459 --dim: #484f58;
460 --accent: #4f8ef7;
461 --accent2: #58a6ff;
462 --green: #3fb950;
463 --red: #f85149;
464 --yellow: #d29922;
465 --purple: #bc8cff;
466 --mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
467 --ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
468 --r: 8px;
469 }
470 html { scroll-behavior: smooth; }
471 body {
472 background: var(--bg);
473 color: var(--text);
474 font-family: var(--ui);
475 font-size: 15px;
476 line-height: 1.7;
477 }
478 a { color: var(--accent2); text-decoration: none; }
479 a:hover { text-decoration: underline; }
480 code {
481 font-family: var(--mono);
482 font-size: 0.88em;
483 background: var(--bg3);
484 border: 1px solid var(--border);
485 border-radius: 4px;
486 padding: 1px 6px;
487 }
488
489 /* ---- Hero ---- */
490 .hero {
491 background: linear-gradient(160deg, #0d1117 0%, #161b22 50%, #0d1117 100%);
492 border-bottom: 1px solid var(--border);
493 padding: 80px 40px 100px;
494 text-align: center;
495 position: relative;
496 overflow: hidden;
497 }
498 .hero::before {
499 content: '';
500 position: absolute;
501 inset: 0;
502 background:
503 radial-gradient(ellipse 60% 40% at 20% 50%, rgba(79,142,247,0.07) 0%, transparent 70%),
504 radial-gradient(ellipse 50% 40% at 80% 50%, rgba(188,140,255,0.06) 0%, transparent 70%);
505 pointer-events: none;
506 }
507 .hero-wordmark {
508 font-family: var(--mono);
509 font-size: clamp(56px, 9vw, 108px);
510 font-weight: 800;
511 letter-spacing: -4px;
512 line-height: 1;
513 margin-bottom: 12px;
514 background: linear-gradient(135deg, #4f8ef7 0%, #bc8cff 100%);
515 -webkit-background-clip: text;
516 -webkit-text-fill-color: transparent;
517 background-clip: text;
518 }
519 .hero-version-any {
520 font-size: clamp(20px, 3vw, 30px);
521 font-weight: 300;
522 color: var(--mute);
523 letter-spacing: 2px;
524 text-transform: uppercase;
525 margin-bottom: 28px;
526 }
527 .hero-sub {
528 font-size: 18px;
529 color: var(--mute);
530 max-width: 600px;
531 margin: 0 auto 40px;
532 line-height: 1.6;
533 }
534 .hero-sub strong { color: var(--text); }
535 .hero-cta-row {
536 display: flex;
537 gap: 12px;
538 justify-content: center;
539 flex-wrap: wrap;
540 }
541 .btn-primary {
542 background: var(--accent);
543 color: #fff;
544 font-weight: 600;
545 padding: 12px 28px;
546 border-radius: var(--r);
547 font-size: 15px;
548 border: none;
549 cursor: pointer;
550 text-decoration: none;
551 transition: opacity 0.15s, transform 0.1s;
552 display: inline-block;
553 }
554 .btn-primary:hover { opacity: 0.88; transform: translateY(-1px); text-decoration: none; }
555 .btn-outline {
556 background: transparent;
557 color: var(--text);
558 font-weight: 500;
559 padding: 12px 28px;
560 border-radius: var(--r);
561 font-size: 15px;
562 border: 1px solid var(--border);
563 cursor: pointer;
564 text-decoration: none;
565 display: inline-block;
566 transition: border-color 0.15s, color 0.15s;
567 }
568 .btn-outline:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
569
570 /* ---- Domain ticker ---- */
571 .domain-ticker {
572 margin: 32px auto 0;
573 max-width: 700px;
574 overflow: hidden;
575 position: relative;
576 height: 34px;
577 }
578 .domain-ticker::before,
579 .domain-ticker::after {
580 content: '';
581 position: absolute;
582 top: 0; bottom: 0;
583 width: 60px;
584 z-index: 2;
585 }
586 .domain-ticker::before { left: 0; background: linear-gradient(90deg, var(--bg), transparent); }
587 .domain-ticker::after { right: 0; background: linear-gradient(-90deg, var(--bg), transparent); }
588 .ticker-track {
589 display: flex;
590 gap: 10px;
591 animation: ticker-scroll 18s linear infinite;
592 width: max-content;
593 }
594 @keyframes ticker-scroll {
595 0% { transform: translateX(0); }
596 100% { transform: translateX(-50%); }
597 }
598 .ticker-item {
599 font-family: var(--mono);
600 font-size: 13px;
601 padding: 4px 14px;
602 border-radius: 20px;
603 border: 1px solid var(--border);
604 white-space: nowrap;
605 color: var(--mute);
606 }
607 .ticker-item.active { border-color: rgba(79,142,247,0.5); color: var(--accent2); background: rgba(79,142,247,0.08); }
608
609 /* ---- Sections ---- */
610 section { padding: 72px 40px; border-top: 1px solid var(--border); }
611 .inner { max-width: 1100px; margin: 0 auto; }
612 .section-eyebrow {
613 font-family: var(--mono);
614 font-size: 11px;
615 color: var(--accent2);
616 letter-spacing: 2px;
617 text-transform: uppercase;
618 margin-bottom: 10px;
619 }
620 section h2 {
621 font-size: 32px;
622 font-weight: 700;
623 letter-spacing: -0.5px;
624 margin-bottom: 12px;
625 }
626 .section-lead {
627 font-size: 16px;
628 color: var(--mute);
629 max-width: 620px;
630 margin-bottom: 48px;
631 line-height: 1.7;
632 }
633 .section-lead strong { color: var(--text); }
634
635 /* ---- Stat strip ---- */
636 .stat-strip {
637 display: flex;
638 gap: 0;
639 border: 1px solid var(--border);
640 border-radius: var(--r);
641 overflow: hidden;
642 margin-bottom: 48px;
643 }
644 .stat-cell {
645 flex: 1;
646 padding: 20px 24px;
647 border-right: 1px solid var(--border);
648 text-align: center;
649 }
650 .stat-cell:last-child { border-right: none; }
651 .stat-num {
652 font-family: var(--mono);
653 font-size: 28px;
654 font-weight: 700;
655 color: var(--accent2);
656 display: block;
657 }
658 .stat-lbl { font-size: 12px; color: var(--mute); }
659
660 /* ---- Protocol table ---- */
661 .proto-table {
662 border: 1px solid var(--border);
663 border-radius: var(--r);
664 overflow: hidden;
665 margin-bottom: 40px;
666 }
667 .proto-row {
668 display: grid;
669 grid-template-columns: 90px 240px 1fr;
670 border-bottom: 1px solid var(--border);
671 }
672 .proto-row:last-child { border-bottom: none; }
673 .proto-row.hdr { background: var(--bg3); }
674 .proto-row > div { padding: 11px 16px; }
675 .proto-method { font-family: var(--mono); font-size: 13px; color: var(--accent2); font-weight: 600; }
676 .proto-sig { font-family: var(--mono); font-size: 12px; color: var(--mute); }
677 .proto-desc { font-size: 13px; color: var(--mute); }
678 .proto-row.hdr .proto-method,
679 .proto-row.hdr .proto-sig,
680 .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; }
681
682 /* ---- Engine capability showcase ---- */
683 .cap-showcase-grid {
684 display: grid;
685 grid-template-columns: repeat(auto-fill, minmax(480px, 1fr));
686 gap: 24px;
687 }
688 @media (max-width: 600px) { .cap-showcase-grid { grid-template-columns: 1fr; } }
689 .cap-showcase-card {
690 border: 1px solid var(--border);
691 border-top: 3px solid var(--cap-color, var(--accent));
692 border-radius: var(--r);
693 background: var(--bg);
694 overflow: hidden;
695 transition: transform 0.15s;
696 }
697 .cap-showcase-card:hover { transform: translateY(-2px); }
698 .cap-showcase-header {
699 padding: 14px 18px;
700 border-bottom: 1px solid var(--border);
701 background: var(--bg2);
702 display: flex;
703 align-items: center;
704 gap: 12px;
705 flex-wrap: wrap;
706 }
707 .cap-showcase-badge {
708 font-size: 12px;
709 font-family: var(--mono);
710 padding: 3px 10px;
711 border-radius: 4px;
712 border: 1px solid;
713 white-space: nowrap;
714 }
715 .cap-showcase-sub {
716 font-size: 12px;
717 color: var(--mute);
718 font-style: italic;
719 }
720 .cap-showcase-body { padding: 16px 18px; }
721 .cap-showcase-desc {
722 font-size: 13px;
723 color: var(--mute);
724 margin-bottom: 14px;
725 line-height: 1.6;
726 }
727 .cap-showcase-desc strong { color: var(--text); }
728 .cap-showcase-output {
729 background: #0a0e14;
730 border: 1px solid var(--border);
731 border-radius: 5px;
732 padding: 12px 14px;
733 font-family: var(--mono);
734 font-size: 11.5px;
735 color: #abb2bf;
736 white-space: pre;
737 overflow-x: auto;
738 line-height: 1.65;
739 }
740 .cap-showcase-domain-grid {
741 display: flex;
742 flex-direction: column;
743 gap: 10px;
744 }
745 .crdt-mini-grid {
746 display: grid;
747 grid-template-columns: 1fr 1fr;
748 gap: 10px;
749 }
750 @media (max-width: 700px) { .crdt-mini-grid { grid-template-columns: 1fr; } }
751
752 /* ---- Three steps ---- */
753 .steps-grid {
754 display: grid;
755 grid-template-columns: repeat(3, 1fr);
756 gap: 24px;
757 }
758 @media (max-width: 800px) { .steps-grid { grid-template-columns: 1fr; } }
759 .step-card {
760 border: 1px solid var(--border);
761 border-radius: var(--r);
762 background: var(--bg2);
763 padding: 24px;
764 position: relative;
765 }
766 .step-num {
767 font-family: var(--mono);
768 font-size: 11px;
769 color: var(--accent);
770 font-weight: 700;
771 text-transform: uppercase;
772 letter-spacing: 1px;
773 margin-bottom: 10px;
774 }
775 .step-title { font-size: 17px; font-weight: 700; margin-bottom: 8px; }
776 .step-desc { font-size: 13px; color: var(--mute); line-height: 1.6; margin-bottom: 16px; }
777 .step-cmd {
778 font-family: var(--mono);
779 font-size: 12px;
780 background: var(--bg3);
781 border: 1px solid var(--border);
782 border-radius: 5px;
783 padding: 10px 14px;
784 color: var(--accent2);
785 }
786
787 /* ---- Code block ---- */
788 .code-wrap {
789 border: 1px solid var(--border);
790 border-radius: var(--r);
791 overflow: hidden;
792 }
793 .code-bar {
794 background: var(--bg3);
795 border-bottom: 1px solid var(--border);
796 padding: 8px 16px;
797 display: flex;
798 align-items: center;
799 gap: 8px;
800 }
801 .code-bar-dot {
802 width: 10px; height: 10px; border-radius: 50%;
803 }
804 .code-bar-title {
805 font-family: var(--mono);
806 font-size: 12px;
807 color: var(--mute);
808 margin-left: 6px;
809 }
810 .code-body {
811 background: #0a0e14;
812 padding: 20px 24px;
813 font-family: var(--mono);
814 font-size: 12.5px;
815 line-height: 1.7;
816 color: #abb2bf;
817 white-space: pre;
818 overflow-x: auto;
819 }
820 /* Simple syntax highlights */
821 .kw { color: #c678dd; }
822 .kw2 { color: #e06c75; }
823 .fn { color: #61afef; }
824 .str { color: #98c379; }
825 .cmt { color: #5c6370; font-style: italic; }
826 .cls { color: #e5c07b; }
827 .typ { color: #56b6c2; }
828
829 /* ---- Active domains grid ---- */
830 .domain-grid {
831 display: grid;
832 grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
833 gap: 20px;
834 }
835 .domain-card {
836 border: 1px solid var(--border);
837 border-radius: var(--r);
838 background: var(--bg2);
839 overflow: hidden;
840 transition: border-color 0.2s, transform 0.15s;
841 }
842 .domain-card:hover { border-color: var(--accent); transform: translateY(-2px); }
843 .domain-card.active-domain { border-color: rgba(63,185,80,0.4); }
844 .domain-card-hdr {
845 background: var(--bg3);
846 padding: 12px 16px;
847 border-bottom: 1px solid var(--border);
848 display: flex;
849 align-items: center;
850 gap: 10px;
851 }
852 .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); }
853 .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); }
854 .active-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); margin-left: auto; }
855 .domain-name-lg { font-family: var(--mono); font-size: 16px; font-weight: 700; color: var(--text); }
856 .domain-card-body { padding: 16px; }
857 .domain-desc { font-size: 13px; color: var(--mute); margin-bottom: 12px; }
858 .cap-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
859 .cap-pill { font-size: 10px; padding: 2px 8px; border-radius: 12px; border: 1px solid var(--border); color: var(--mute); background: var(--bg3); }
860 .cap-pill.cap-crdt { border-color: rgba(188,140,255,0.4); color: var(--purple); background: rgba(188,140,255,0.08); }
861 .cap-pill.cap-ot-merge { border-color: rgba(88,166,255,0.4); color: var(--accent2); background: rgba(88,166,255,0.08); }
862 .cap-pill.cap-domain-schema { border-color: rgba(63,185,80,0.4); color: var(--green); background: rgba(63,185,80,0.08); }
863 .cap-pill.cap-typed-deltas { border-color: rgba(249,168,37,0.4); color: #f9a825; background: rgba(249,168,37,0.08); }
864 .dim-row { font-size: 11px; color: var(--dim); }
865 .dim-tag { color: var(--mute); }
866
867 /* ---- Planned domains ---- */
868 .planned-grid {
869 display: grid;
870 grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
871 gap: 16px;
872 }
873 .planned-card {
874 border: 1px solid var(--border);
875 border-radius: var(--r);
876 background: var(--bg2);
877 padding: 20px 16px;
878 display: flex;
879 flex-direction: column;
880 gap: 8px;
881 transition: border-color 0.2s, transform 0.15s;
882 }
883 .planned-card:hover { border-color: var(--card-accent,var(--accent)); transform: translateY(-2px); }
884 .planned-card.yours { border: 2px dashed var(--accent); background: rgba(79,142,247,0.04); }
885 .planned-icon { font-size: 28px; }
886 .planned-name { font-size: 15px; font-weight: 700; color: var(--text); }
887 .planned-tag { font-size: 12px; color: var(--mute); line-height: 1.5; }
888 .planned-dims { font-size: 10px; color: var(--dim); }
889 .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; }
890 .cta-btn {
891 display: inline-block;
892 margin-top: 6px;
893 font-size: 12px;
894 font-weight: 600;
895 color: var(--accent2);
896 border: 1px solid rgba(88,166,255,0.4);
897 border-radius: 4px;
898 padding: 4px 12px;
899 text-decoration: none;
900 transition: background 0.15s;
901 }
902 .cta-btn:hover { background: rgba(88,166,255,0.1); text-decoration: none; }
903
904 /* ---- Distribution tiers ---- */
905 .dist-grid {
906 display: grid;
907 grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
908 gap: 24px;
909 }
910 .dist-card {
911 border: 1px solid var(--border);
912 border-top: 3px solid var(--dist-color, var(--accent));
913 border-radius: var(--r);
914 background: var(--bg2);
915 padding: 24px;
916 transition: transform 0.15s;
917 }
918 .dist-card:hover { transform: translateY(-2px); }
919 .dist-header { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 14px; }
920 .dist-icon { font-size: 26px; line-height: 1; }
921 .dist-tier { font-family: var(--mono); font-size: 11px; color: var(--dist-color,var(--accent)); letter-spacing: 1px; text-transform: uppercase; font-weight: 700; }
922 .dist-title { font-size: 14px; font-weight: 600; color: var(--text); margin-top: 2px; }
923 .dist-desc { font-size: 13px; color: var(--mute); margin-bottom: 16px; line-height: 1.6; }
924 .dist-steps { list-style: none; counter-reset: step; display: flex; flex-direction: column; gap: 6px; }
925 .dist-steps li { counter-increment: step; display: flex; align-items: flex-start; gap: 8px; font-size: 12px; color: var(--mute); }
926 .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; }
927 .dist-steps code { background: var(--bg3); border: 1px solid var(--border); border-radius: 4px; padding: 1px 6px; font-size: 11px; }
928
929 /* ---- MuseHub teaser ---- */
930 .musehub-section {
931 background: linear-gradient(135deg, #0d1117 0%, #1a0d2e 50%, #0d1117 100%);
932 padding: 80px 40px;
933 text-align: center;
934 border-top: 1px solid var(--border);
935 }
936 .musehub-logo {
937 font-size: 48px;
938 margin-bottom: 20px;
939 }
940 .musehub-section h2 {
941 font-size: 36px;
942 font-weight: 800;
943 letter-spacing: -1px;
944 margin-bottom: 12px;
945 }
946 .musehub-section h2 span {
947 background: linear-gradient(135deg, #bc8cff, #4f8ef7);
948 -webkit-background-clip: text;
949 -webkit-text-fill-color: transparent;
950 background-clip: text;
951 }
952 .musehub-desc {
953 font-size: 16px;
954 color: var(--mute);
955 max-width: 560px;
956 margin: 0 auto 36px;
957 }
958 .musehub-desc strong { color: var(--text); }
959 .musehub-features {
960 display: flex;
961 gap: 24px;
962 justify-content: center;
963 flex-wrap: wrap;
964 margin-bottom: 40px;
965 }
966 .mh-feature {
967 background: var(--bg2);
968 border: 1px solid rgba(188,140,255,0.2);
969 border-radius: var(--r);
970 padding: 16px 20px;
971 text-align: left;
972 min-width: 180px;
973 }
974 .mh-feature-icon { font-size: 20px; margin-bottom: 8px; }
975 .mh-feature-title { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 4px; }
976 .mh-feature-desc { font-size: 12px; color: var(--mute); }
977 .musehub-status {
978 display: inline-flex;
979 align-items: center;
980 gap: 8px;
981 background: rgba(188,140,255,0.1);
982 border: 1px solid rgba(188,140,255,0.3);
983 border-radius: 20px;
984 padding: 8px 20px;
985 font-size: 13px;
986 color: var(--purple);
987 }
988 .mh-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--purple); animation: pulse 2s ease-in-out infinite; }
989 @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
990
991 /* ---- Footer ---- */
992 footer {
993 background: var(--bg2);
994 border-top: 1px solid var(--border);
995 padding: 24px 40px;
996 display: flex;
997 justify-content: space-between;
998 align-items: center;
999 flex-wrap: wrap;
1000 gap: 12px;
1001 font-size: 13px;
1002 color: var(--mute);
1003 }
1004 footer a { color: var(--accent2); }
1005
1006 /* ---- Nav ---- */
1007 nav {
1008 background: var(--bg2);
1009 border-bottom: 1px solid var(--border);
1010 padding: 0 40px;
1011 display: flex;
1012 align-items: center;
1013 gap: 0;
1014 height: 52px;
1015 }
1016 .nav-logo {
1017 font-family: var(--mono);
1018 font-size: 16px;
1019 font-weight: 700;
1020 color: var(--accent2);
1021 margin-right: 32px;
1022 text-decoration: none;
1023 }
1024 .nav-logo:hover { text-decoration: none; }
1025 .nav-link {
1026 font-size: 13px;
1027 color: var(--mute);
1028 padding: 0 14px;
1029 height: 100%;
1030 display: flex;
1031 align-items: center;
1032 border-bottom: 2px solid transparent;
1033 text-decoration: none;
1034 transition: color 0.15s, border-color 0.15s;
1035 }
1036 .nav-link:hover { color: var(--text); text-decoration: none; }
1037 .nav-link.current { color: var(--text); border-bottom-color: var(--accent); }
1038 .nav-spacer { flex: 1; }
1039 .nav-badge {
1040 font-size: 11px;
1041 background: rgba(79,142,247,0.12);
1042 border: 1px solid rgba(79,142,247,0.3);
1043 color: var(--accent2);
1044 border-radius: 4px;
1045 padding: 2px 8px;
1046 font-family: var(--mono);
1047 }
1048 </style>
1049 </head>
1050 <body>
1051
1052 <nav>
1053 <a class="nav-logo" href="#">muse</a>
1054 <a class="nav-link" href="tour_de_force.html">Tour de Force</a>
1055 <a class="nav-link current" href="index.html">Domain Registry</a>
1056 <a class="nav-link" href="../docs/guide/plugin-authoring-guide.md">Plugin Guide</a>
1057 <div class="nav-spacer"></div>
1058 <span class="nav-badge">v0.1.1</span>
1059 </nav>
1060
1061 <!-- =================== HERO =================== -->
1062 <div class="hero">
1063 <h1 class="hero-wordmark">muse</h1>
1064 <div class="hero-version-any">Version Anything</div>
1065 <p class="hero-sub">
1066 One protocol. Any domain. <strong>Six methods</strong> between you and a
1067 complete version control system — branching, merging, conflict resolution,
1068 time-travel, and typed diffs — for free.
1069 </p>
1070 <div class="hero-cta-row">
1071 <a class="btn-primary" href="#build">Build a Domain Plugin</a>
1072 <a class="btn-outline" href="tour_de_force.html">Watch the Demo →</a>
1073 </div>
1074 <div class="domain-ticker">
1075 <div class="ticker-track">
1076 <span class="ticker-item active">🎵 music</span>
1077 <span class="ticker-item">🧬 genomics</span>
1078 <span class="ticker-item">🌐 3d-spatial</span>
1079 <span class="ticker-item">📈 financial</span>
1080 <span class="ticker-item">⚛️ simulation</span>
1081 <span class="ticker-item">🔬 proteomics</span>
1082 <span class="ticker-item">🏗️ cad</span>
1083 <span class="ticker-item">🎮 game-state</span>
1084 <span class="ticker-item">✦ your-domain</span>
1085 <!-- duplicate for seamless loop -->
1086 <span class="ticker-item active">🎵 music</span>
1087 <span class="ticker-item">🧬 genomics</span>
1088 <span class="ticker-item">🌐 3d-spatial</span>
1089 <span class="ticker-item">📈 financial</span>
1090 <span class="ticker-item">⚛️ simulation</span>
1091 <span class="ticker-item">🔬 proteomics</span>
1092 <span class="ticker-item">🏗️ cad</span>
1093 <span class="ticker-item">🎮 game-state</span>
1094 <span class="ticker-item">✦ your-domain</span>
1095 </div>
1096 </div>
1097 </div>
1098
1099 <!-- =================== PROTOCOL =================== -->
1100 <section id="protocol">
1101 <div class="inner">
1102 <div class="section-eyebrow">The Contract</div>
1103 <h2>The MuseDomainPlugin Protocol</h2>
1104 <p class="section-lead">
1105 Every domain — music, genomics, 3D spatial, financial models — implements
1106 the same <strong>six-method protocol</strong>. The core engine handles
1107 everything else: content-addressed storage, DAG, branches, log, merge base,
1108 cherry-pick, revert, stash, tags.
1109 </p>
1110
1111 <div class="stat-strip">
1112 <div class="stat-cell"><span class="stat-num">6</span><span class="stat-lbl">methods to implement</span></div>
1113 <div class="stat-cell"><span class="stat-num">14</span><span class="stat-lbl">CLI commands, free</span></div>
1114 <div class="stat-cell"><span class="stat-num">∞</span><span class="stat-lbl">domains possible</span></div>
1115 <div class="stat-cell"><span class="stat-num">0</span><span class="stat-lbl">core changes needed</span></div>
1116 </div>
1117
1118 <div class="proto-table">
1119 <div class="proto-row hdr">
1120 <div class="proto-method">Method</div>
1121 <div class="proto-sig">Signature</div>
1122 <div class="proto-desc">Purpose</div>
1123 </div>
1124 <div class="proto-row">
1125 <div class="proto-method">snapshot</div>
1126 <div class="proto-sig">snapshot(live) → StateSnapshot</div>
1127 <div class="proto-desc">Capture current state as a content-addressable blob</div>
1128 </div>
1129 <div class="proto-row">
1130 <div class="proto-method">diff</div>
1131 <div class="proto-sig">diff(base, target) → StateDelta</div>
1132 <div class="proto-desc">Compute minimal change between two snapshots (added · removed · modified)</div>
1133 </div>
1134 <div class="proto-row">
1135 <div class="proto-method">merge</div>
1136 <div class="proto-sig">merge(base, left, right) → MergeResult</div>
1137 <div class="proto-desc">Three-way reconcile divergent state lines; surface conflicts per dimension</div>
1138 </div>
1139 <div class="proto-row">
1140 <div class="proto-method">drift</div>
1141 <div class="proto-sig">drift(committed, live) → DriftReport</div>
1142 <div class="proto-desc">Detect uncommitted changes between HEAD and working state</div>
1143 </div>
1144 <div class="proto-row">
1145 <div class="proto-method">apply</div>
1146 <div class="proto-sig">apply(delta, live) → LiveState</div>
1147 <div class="proto-desc">Apply a delta during checkout to reconstruct historical state</div>
1148 </div>
1149 <div class="proto-row">
1150 <div class="proto-method">schema</div>
1151 <div class="proto-sig">schema() → DomainSchema</div>
1152 <div class="proto-desc">Declare data structure — drives diff algorithm selection per dimension</div>
1153 </div>
1154 </div>
1155 </div>
1156 </section>
1157
1158 <!-- =================== ENGINE CAPABILITIES =================== -->
1159 <section id="capabilities" style="background:var(--bg2)">
1160 <div class="inner">
1161 <div class="section-eyebrow">Engine Capabilities</div>
1162 <h2>What Every Plugin Gets for Free</h2>
1163 <p class="section-lead">
1164 The core engine provides four advanced capabilities that any domain plugin
1165 can opt into. Implement the protocol — the engine does the rest.
1166 </p>
1167
1168 <div class="cap-showcase-grid">
1169
1170 <div class="cap-showcase-card" style="--cap-color:#f9a825">
1171 <div class="cap-showcase-header">
1172 <span class="cap-showcase-badge" style="color:#f9a825;background:#f9a82515;border-color:#f9a82540">
1173 🔬 Typed Delta Algebra
1174 </span>
1175 <span class="cap-showcase-sub">StructuredDelta — every change is a typed operation</span>
1176 </div>
1177 <div class="cap-showcase-body">
1178 <p class="cap-showcase-desc">
1179 Unlike Git's blob diffs, Muse deltas are <strong>typed objects</strong>:
1180 <code>InsertOp</code>, <code>ReplaceOp</code>, <code>DeleteOp</code> — each
1181 carrying the address, before/after hashes, and affected dimensions.
1182 Machine-readable with <code>muse show --json</code>.
1183 </p>
1184 <pre class="cap-showcase-output">{{TYPED_DELTA_EXAMPLE}}</pre>
1185 </div>
1186 </div>
1187
1188 <div class="cap-showcase-card" style="--cap-color:#58a6ff">
1189 <div class="cap-showcase-header">
1190 <span class="cap-showcase-badge" style="color:#58a6ff;background:#58a6ff15;border-color:#58a6ff40">
1191 🗂️ Domain Schema
1192 </span>
1193 <span class="cap-showcase-sub">Per-domain dimensions drive diff algorithm selection</span>
1194 </div>
1195 <div class="cap-showcase-body">
1196 <p class="cap-showcase-desc">
1197 Each plugin's <code>schema()</code> method declares its dimensions and merge mode.
1198 The engine uses this to select the right diff algorithm per dimension and to
1199 surface only the dimensions that actually conflict.
1200 </p>
1201 <div class="cap-showcase-domain-grid" id="schema-domain-grid">
1202 {{ACTIVE_DOMAINS}}
1203 </div>
1204 </div>
1205 </div>
1206
1207 <div class="cap-showcase-card" style="--cap-color:#ef5350">
1208 <div class="cap-showcase-header">
1209 <span class="cap-showcase-badge" style="color:#ef5350;background:#ef535015;border-color:#ef535040">
1210 ⚙️ OT Merge
1211 </span>
1212 <span class="cap-showcase-sub">Operational transformation — independent ops commute automatically</span>
1213 </div>
1214 <div class="cap-showcase-body">
1215 <p class="cap-showcase-desc">
1216 Plugins implementing <strong>StructuredMergePlugin</strong> get operational
1217 transformation. Operations at different addresses commute automatically —
1218 only operations on the same address with incompatible intent surface a conflict.
1219 </p>
1220 <pre class="cap-showcase-output">{{OT_MERGE_EXAMPLE}}</pre>
1221 </div>
1222 </div>
1223
1224 <div class="cap-showcase-card" style="--cap-color:#bc8cff">
1225 <div class="cap-showcase-header">
1226 <span class="cap-showcase-badge" style="color:#bc8cff;background:#bc8cff15;border-color:#bc8cff40">
1227 🔮 CRDT Primitives
1228 </span>
1229 <span class="cap-showcase-sub">Convergent merge — any two replicas always reach the same state</span>
1230 </div>
1231 <div class="cap-showcase-body">
1232 <p class="cap-showcase-desc">
1233 Plugins implementing <strong>CRDTPlugin</strong> get four battle-tested
1234 convergent data structures. No coordination required between replicas.
1235 </p>
1236 <div class="crdt-mini-grid">
1237 {{CRDT_CARDS}}
1238 </div>
1239 </div>
1240 </div>
1241
1242 </div>
1243 </div>
1244 </section>
1245
1246 <!-- =================== BUILD =================== -->
1247 <section id="build" style="background:var(--bg)">
1248 <div class="inner">
1249 <div class="section-eyebrow">Build</div>
1250 <h2>Build in Three Steps</h2>
1251 <p class="section-lead">
1252 One command scaffolds the entire plugin skeleton. You fill in six methods.
1253 The full VCS follows.
1254 </p>
1255
1256 <div class="steps-grid">
1257 <div class="step-card">
1258 <div class="step-num">Step 1 · Scaffold</div>
1259 <div class="step-title">Generate the skeleton</div>
1260 <div class="step-desc">
1261 One command creates the plugin directory, class, and all six method stubs
1262 with full type annotations.
1263 </div>
1264 <div class="step-cmd">muse domains --new genomics</div>
1265 </div>
1266 <div class="step-card">
1267 <div class="step-num">Step 2 · Implement</div>
1268 <div class="step-title">Fill in the six methods</div>
1269 <div class="step-desc">
1270 Replace each <code>raise NotImplementedError</code> with your domain's
1271 snapshot, diff, merge, drift, apply, and schema logic.
1272 </div>
1273 <div class="step-cmd">vim muse/plugins/genomics/plugin.py</div>
1274 </div>
1275 <div class="step-card">
1276 <div class="step-num">Step 3 · Use</div>
1277 <div class="step-title">Full VCS, instantly</div>
1278 <div class="step-desc">
1279 Register in <code>registry.py</code>, then every Muse command works
1280 for your domain out of the box.
1281 </div>
1282 <div class="step-cmd">muse init --domain genomics</div>
1283 </div>
1284 </div>
1285 </div>
1286 </section>
1287
1288 <!-- =================== CODE =================== -->
1289 <section id="code">
1290 <div class="inner">
1291 <div class="section-eyebrow">The Scaffold</div>
1292 <h2>What <code>muse domains --new genomics</code> produces</h2>
1293 <p class="section-lead">
1294 A fully typed, immediately runnable plugin skeleton. Every method has the
1295 correct signature. You replace the stubs — the protocol does the rest.
1296 </p>
1297 <div class="code-wrap">
1298 <div class="code-bar">
1299 <div class="code-bar-dot" style="background:#ff5f57"></div>
1300 <div class="code-bar-dot" style="background:#febc2e"></div>
1301 <div class="code-bar-dot" style="background:#28c840"></div>
1302 <span class="code-bar-title">muse/plugins/genomics/plugin.py</span>
1303 </div>
1304 <div class="code-body">{{SCAFFOLD_SNIPPET}}</div>
1305 </div>
1306 <p style="margin-top:16px;font-size:13px;color:var(--mute)">
1307 Full walkthrough →
1308 <a href="../docs/guide/plugin-authoring-guide.md">docs/guide/plugin-authoring-guide.md</a>
1309 · CRDT extension →
1310 <a href="../docs/guide/crdt-reference.md">docs/guide/crdt-reference.md</a>
1311 </p>
1312 </div>
1313 </section>
1314
1315 <!-- =================== ACTIVE DOMAINS =================== -->
1316 <section id="registry" style="background:var(--bg2)">
1317 <div class="inner">
1318 <div class="section-eyebrow">Registry</div>
1319 <h2>Registered Domains</h2>
1320 <p class="section-lead">
1321 Domains currently registered in this Muse instance. The active domain
1322 is the one used when you run <code>muse commit</code>, <code>muse diff</code>,
1323 and all other commands.
1324 </p>
1325 <div class="domain-grid">
1326 {{ACTIVE_DOMAINS}}
1327 </div>
1328 </div>
1329 </section>
1330
1331 <!-- =================== PLANNED ECOSYSTEM =================== -->
1332 <section id="ecosystem">
1333 <div class="inner">
1334 <div class="section-eyebrow">Ecosystem</div>
1335 <h2>The Plugin Ecosystem</h2>
1336 <p class="section-lead">
1337 Music is the reference implementation. These are the domains planned
1338 next — and the slot waiting for yours.
1339 </p>
1340 <div class="planned-grid">
1341 {{PLANNED_DOMAINS}}
1342 </div>
1343 </div>
1344 </section>
1345
1346 <!-- =================== DISTRIBUTION =================== -->
1347 <section id="distribute" style="background:var(--bg2)">
1348 <div class="inner">
1349 <div class="section-eyebrow">Distribution</div>
1350 <h2>How to Share Your Plugin</h2>
1351 <p class="section-lead">
1352 Three tiers of distribution — from local prototype to globally searchable
1353 registry. Start local, publish when ready.
1354 </p>
1355 <div class="dist-grid">
1356 {{DIST_CARDS}}
1357 </div>
1358 </div>
1359 </section>
1360
1361 <!-- =================== MUSEHUB TEASER =================== -->
1362 <div class="musehub-section">
1363 <div class="musehub-logo">🌐</div>
1364 <h2><span>MuseHub</span> is coming</h2>
1365 <p class="musehub-desc">
1366 A <strong>centralized, searchable registry</strong> for Muse domain plugins —
1367 think npm or crates.io, but for any multidimensional versioned state.
1368 One command to publish. One command to install.
1369 </p>
1370 <div class="musehub-features">
1371 <div class="mh-feature">
1372 <div class="mh-feature-icon">🔍</div>
1373 <div class="mh-feature-title">Searchable</div>
1374 <div class="mh-feature-desc">Find plugins by domain, capability, or keyword</div>
1375 </div>
1376 <div class="mh-feature">
1377 <div class="mh-feature-icon">📦</div>
1378 <div class="mh-feature-title">Versioned</div>
1379 <div class="mh-feature-desc">Semantic versioning, pinned installs, changelogs</div>
1380 </div>
1381 <div class="mh-feature">
1382 <div class="mh-feature-icon">🔒</div>
1383 <div class="mh-feature-title">Private registries</div>
1384 <div class="mh-feature-desc">Self-host for enterprise or research teams</div>
1385 </div>
1386 <div class="mh-feature">
1387 <div class="mh-feature-icon">⚡</div>
1388 <div class="mh-feature-title">One command</div>
1389 <div class="mh-feature-desc"><code>muse init --domain @musehub/genomics</code></div>
1390 </div>
1391 </div>
1392 <div class="musehub-status">
1393 <div class="mh-dot"></div>
1394 MuseHub — planned · building in public at github.com/cgcardona/muse
1395 </div>
1396 </div>
1397
1398 <footer>
1399 <span>Muse v0.1.1 · domain-agnostic version control for multidimensional state</span>
1400 <span>
1401 <a href="tour_de_force.html">Tour de Force</a> ·
1402 <a href="https://github.com/cgcardona/muse">GitHub</a> ·
1403 <a href="../docs/guide/plugin-authoring-guide.md">Plugin Guide</a>
1404 </span>
1405 </footer>
1406
1407 </body>
1408 </html>
1409 """
1410
1411
1412 # ---------------------------------------------------------------------------
1413 # Entry point
1414 # ---------------------------------------------------------------------------
1415
1416 if __name__ == "__main__":
1417 import argparse
1418
1419 parser = argparse.ArgumentParser(
1420 description="Generate the Muse domain registry HTML page"
1421 )
1422 parser.add_argument(
1423 "--out",
1424 default=str(_ROOT / "artifacts" / "domain_registry.html"),
1425 help="Output HTML path",
1426 )
1427 args = parser.parse_args()
1428
1429 out_path = pathlib.Path(args.out)
1430 out_path.parent.mkdir(parents=True, exist_ok=True)
1431
1432 print("Generating domain_registry.html...")
1433 render(out_path)
1434 print(f"Open: file://{out_path.resolve()}")