cgcardona / muse public
render_domain_registry.py python
1450 lines 51.8 KB
48a13b74 fix: collapse stats into left column beside protocol table (#36) 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(--ui);
509 font-size: clamp(72px, 11vw, 130px);
510 font-weight: 800;
511 letter-spacing: -5px;
512 line-height: 1;
513 margin-bottom: 12px;
514 background: linear-gradient(90deg, #6ea8fe 0%, #a78bfa 50%, #c084fc 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(18px, 2.8vw, 26px);
521 font-weight: 700;
522 color: #ffffff;
523 letter-spacing: 6px;
524 text-transform: uppercase;
525 margin-bottom: 32px;
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 /* ---- Protocol two-col layout ---- */
636 .proto-layout {
637 display: grid;
638 grid-template-columns: 148px 1fr;
639 gap: 0;
640 border: 1px solid var(--border);
641 border-radius: var(--r);
642 overflow: hidden;
643 margin-bottom: 40px;
644 align-items: stretch;
645 }
646 @media (max-width: 640px) {
647 .proto-layout { grid-template-columns: 1fr; }
648 .stat-strip { border-right: none; border-bottom: 1px solid var(--border); }
649 }
650
651 /* ---- Stat strip (left column) ---- */
652 .stat-strip {
653 display: flex;
654 flex-direction: column;
655 border-right: 1px solid var(--border);
656 }
657 .stat-cell {
658 flex: 1;
659 padding: 18px 20px;
660 border-bottom: 1px solid var(--border);
661 text-align: center;
662 display: flex;
663 flex-direction: column;
664 align-items: center;
665 justify-content: center;
666 }
667 .stat-cell:last-child { border-bottom: none; }
668 .stat-num {
669 font-family: var(--mono);
670 font-size: 26px;
671 font-weight: 700;
672 color: var(--accent2);
673 display: block;
674 line-height: 1.1;
675 }
676 .stat-lbl { font-size: 11px; color: var(--mute); margin-top: 4px; line-height: 1.3; }
677
678 /* ---- Protocol table (right column) ---- */
679 .proto-table {
680 overflow: hidden;
681 }
682 .proto-row {
683 display: grid;
684 grid-template-columns: 90px 240px 1fr;
685 border-bottom: 1px solid var(--border);
686 }
687 .proto-row:last-child { border-bottom: none; }
688 .proto-row.hdr { background: var(--bg3); }
689 .proto-row > div { padding: 11px 16px; }
690 .proto-method { font-family: var(--mono); font-size: 13px; color: var(--accent2); font-weight: 600; }
691 .proto-sig { font-family: var(--mono); font-size: 12px; color: var(--mute); }
692 .proto-desc { font-size: 13px; color: var(--mute); }
693 .proto-row.hdr .proto-method,
694 .proto-row.hdr .proto-sig,
695 .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; }
696
697 /* ---- Engine capability showcase ---- */
698 .cap-showcase-grid {
699 display: grid;
700 grid-template-columns: repeat(auto-fill, minmax(480px, 1fr));
701 gap: 24px;
702 }
703 @media (max-width: 600px) { .cap-showcase-grid { grid-template-columns: 1fr; } }
704 .cap-showcase-card {
705 border: 1px solid var(--border);
706 border-top: 3px solid var(--cap-color, var(--accent));
707 border-radius: var(--r);
708 background: var(--bg);
709 overflow: hidden;
710 transition: transform 0.15s;
711 }
712 .cap-showcase-card:hover { transform: translateY(-2px); }
713 .cap-showcase-header {
714 padding: 14px 18px;
715 border-bottom: 1px solid var(--border);
716 background: var(--bg2);
717 display: flex;
718 align-items: center;
719 gap: 12px;
720 flex-wrap: wrap;
721 }
722 .cap-showcase-badge {
723 font-size: 12px;
724 font-family: var(--mono);
725 padding: 3px 10px;
726 border-radius: 4px;
727 border: 1px solid;
728 white-space: nowrap;
729 }
730 .cap-showcase-sub {
731 font-size: 12px;
732 color: var(--mute);
733 font-style: italic;
734 }
735 .cap-showcase-body { padding: 16px 18px; }
736 .cap-showcase-desc {
737 font-size: 13px;
738 color: var(--mute);
739 margin-bottom: 14px;
740 line-height: 1.6;
741 }
742 .cap-showcase-desc strong { color: var(--text); }
743 .cap-showcase-output {
744 background: #0a0e14;
745 border: 1px solid var(--border);
746 border-radius: 5px;
747 padding: 12px 14px;
748 font-family: var(--mono);
749 font-size: 11.5px;
750 color: #abb2bf;
751 white-space: pre;
752 overflow-x: auto;
753 line-height: 1.65;
754 }
755 .cap-showcase-domain-grid {
756 display: flex;
757 flex-direction: column;
758 gap: 10px;
759 }
760 .crdt-mini-grid {
761 display: grid;
762 grid-template-columns: 1fr 1fr;
763 gap: 10px;
764 }
765 @media (max-width: 700px) { .crdt-mini-grid { grid-template-columns: 1fr; } }
766
767 /* ---- Three steps ---- */
768 .steps-grid {
769 display: grid;
770 grid-template-columns: repeat(3, 1fr);
771 gap: 24px;
772 }
773 @media (max-width: 800px) { .steps-grid { grid-template-columns: 1fr; } }
774 .step-card {
775 border: 1px solid var(--border);
776 border-radius: var(--r);
777 background: var(--bg2);
778 padding: 24px;
779 position: relative;
780 }
781 .step-num {
782 font-family: var(--mono);
783 font-size: 11px;
784 color: var(--accent);
785 font-weight: 700;
786 text-transform: uppercase;
787 letter-spacing: 1px;
788 margin-bottom: 10px;
789 }
790 .step-title { font-size: 17px; font-weight: 700; margin-bottom: 8px; }
791 .step-desc { font-size: 13px; color: var(--mute); line-height: 1.6; margin-bottom: 16px; }
792 .step-cmd {
793 font-family: var(--mono);
794 font-size: 12px;
795 background: var(--bg3);
796 border: 1px solid var(--border);
797 border-radius: 5px;
798 padding: 10px 14px;
799 color: var(--accent2);
800 }
801
802 /* ---- Code block ---- */
803 .code-wrap {
804 border: 1px solid var(--border);
805 border-radius: var(--r);
806 overflow: hidden;
807 }
808 .code-bar {
809 background: var(--bg3);
810 border-bottom: 1px solid var(--border);
811 padding: 8px 16px;
812 display: flex;
813 align-items: center;
814 gap: 8px;
815 }
816 .code-bar-dot {
817 width: 10px; height: 10px; border-radius: 50%;
818 }
819 .code-bar-title {
820 font-family: var(--mono);
821 font-size: 12px;
822 color: var(--mute);
823 margin-left: 6px;
824 }
825 .code-body {
826 background: #0a0e14;
827 padding: 20px 24px;
828 font-family: var(--mono);
829 font-size: 12.5px;
830 line-height: 1.7;
831 color: #abb2bf;
832 white-space: pre;
833 overflow-x: auto;
834 }
835 /* Simple syntax highlights */
836 .kw { color: #c678dd; }
837 .kw2 { color: #e06c75; }
838 .fn { color: #61afef; }
839 .str { color: #98c379; }
840 .cmt { color: #5c6370; font-style: italic; }
841 .cls { color: #e5c07b; }
842 .typ { color: #56b6c2; }
843
844 /* ---- Active domains grid ---- */
845 .domain-grid {
846 display: grid;
847 grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
848 gap: 20px;
849 }
850 .domain-card {
851 border: 1px solid var(--border);
852 border-radius: var(--r);
853 background: var(--bg2);
854 overflow: hidden;
855 transition: border-color 0.2s, transform 0.15s;
856 }
857 .domain-card:hover { border-color: var(--accent); transform: translateY(-2px); }
858 .domain-card.active-domain { border-color: rgba(63,185,80,0.4); }
859 .domain-card-hdr {
860 background: var(--bg3);
861 padding: 12px 16px;
862 border-bottom: 1px solid var(--border);
863 display: flex;
864 align-items: center;
865 gap: 10px;
866 }
867 .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); }
868 .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); }
869 .active-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); margin-left: auto; }
870 .domain-name-lg { font-family: var(--mono); font-size: 16px; font-weight: 700; color: var(--text); }
871 .domain-card-body { padding: 16px; }
872 .domain-desc { font-size: 13px; color: var(--mute); margin-bottom: 12px; }
873 .cap-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
874 .cap-pill { font-size: 10px; padding: 2px 8px; border-radius: 12px; border: 1px solid var(--border); color: var(--mute); background: var(--bg3); }
875 .cap-pill.cap-crdt { border-color: rgba(188,140,255,0.4); color: var(--purple); background: rgba(188,140,255,0.08); }
876 .cap-pill.cap-ot-merge { border-color: rgba(88,166,255,0.4); color: var(--accent2); background: rgba(88,166,255,0.08); }
877 .cap-pill.cap-domain-schema { border-color: rgba(63,185,80,0.4); color: var(--green); background: rgba(63,185,80,0.08); }
878 .cap-pill.cap-typed-deltas { border-color: rgba(249,168,37,0.4); color: #f9a825; background: rgba(249,168,37,0.08); }
879 .dim-row { font-size: 11px; color: var(--dim); }
880 .dim-tag { color: var(--mute); }
881
882 /* ---- Planned domains ---- */
883 .planned-grid {
884 display: grid;
885 grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
886 gap: 16px;
887 }
888 .planned-card {
889 border: 1px solid var(--border);
890 border-radius: var(--r);
891 background: var(--bg2);
892 padding: 20px 16px;
893 display: flex;
894 flex-direction: column;
895 gap: 8px;
896 transition: border-color 0.2s, transform 0.15s;
897 }
898 .planned-card:hover { border-color: var(--card-accent,var(--accent)); transform: translateY(-2px); }
899 .planned-card.yours { border: 2px dashed var(--accent); background: rgba(79,142,247,0.04); }
900 .planned-icon { font-size: 28px; }
901 .planned-name { font-size: 15px; font-weight: 700; color: var(--text); }
902 .planned-tag { font-size: 12px; color: var(--mute); line-height: 1.5; }
903 .planned-dims { font-size: 10px; color: var(--dim); }
904 .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; }
905 .cta-btn {
906 display: inline-block;
907 margin-top: 6px;
908 font-size: 12px;
909 font-weight: 600;
910 color: var(--accent2);
911 border: 1px solid rgba(88,166,255,0.4);
912 border-radius: 4px;
913 padding: 4px 12px;
914 text-decoration: none;
915 transition: background 0.15s;
916 }
917 .cta-btn:hover { background: rgba(88,166,255,0.1); text-decoration: none; }
918
919 /* ---- Distribution tiers ---- */
920 .dist-grid {
921 display: grid;
922 grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
923 gap: 24px;
924 }
925 .dist-card {
926 border: 1px solid var(--border);
927 border-top: 3px solid var(--dist-color, var(--accent));
928 border-radius: var(--r);
929 background: var(--bg2);
930 padding: 24px;
931 transition: transform 0.15s;
932 }
933 .dist-card:hover { transform: translateY(-2px); }
934 .dist-header { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 14px; }
935 .dist-icon { font-size: 26px; line-height: 1; }
936 .dist-tier { font-family: var(--mono); font-size: 11px; color: var(--dist-color,var(--accent)); letter-spacing: 1px; text-transform: uppercase; font-weight: 700; }
937 .dist-title { font-size: 14px; font-weight: 600; color: var(--text); margin-top: 2px; }
938 .dist-desc { font-size: 13px; color: var(--mute); margin-bottom: 16px; line-height: 1.6; }
939 .dist-steps { list-style: none; counter-reset: step; display: flex; flex-direction: column; gap: 6px; }
940 .dist-steps li { counter-increment: step; display: flex; align-items: flex-start; gap: 8px; font-size: 12px; color: var(--mute); }
941 .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; }
942 .dist-steps code { background: var(--bg3); border: 1px solid var(--border); border-radius: 4px; padding: 1px 6px; font-size: 11px; }
943
944 /* ---- MuseHub teaser ---- */
945 .musehub-section {
946 background: linear-gradient(135deg, #0d1117 0%, #1a0d2e 50%, #0d1117 100%);
947 padding: 80px 40px;
948 text-align: center;
949 border-top: 1px solid var(--border);
950 }
951 .musehub-logo {
952 font-size: 48px;
953 margin-bottom: 20px;
954 }
955 .musehub-section h2 {
956 font-size: 36px;
957 font-weight: 800;
958 letter-spacing: -1px;
959 margin-bottom: 12px;
960 }
961 .musehub-section h2 span {
962 background: linear-gradient(135deg, #bc8cff, #4f8ef7);
963 -webkit-background-clip: text;
964 -webkit-text-fill-color: transparent;
965 background-clip: text;
966 }
967 .musehub-desc {
968 font-size: 16px;
969 color: var(--mute);
970 max-width: 560px;
971 margin: 0 auto 36px;
972 }
973 .musehub-desc strong { color: var(--text); }
974 .musehub-features {
975 display: flex;
976 gap: 24px;
977 justify-content: center;
978 flex-wrap: wrap;
979 margin-bottom: 40px;
980 }
981 .mh-feature {
982 background: var(--bg2);
983 border: 1px solid rgba(188,140,255,0.2);
984 border-radius: var(--r);
985 padding: 16px 20px;
986 text-align: left;
987 min-width: 180px;
988 }
989 .mh-feature-icon { font-size: 20px; margin-bottom: 8px; }
990 .mh-feature-title { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 4px; }
991 .mh-feature-desc { font-size: 12px; color: var(--mute); }
992 .musehub-status {
993 display: inline-flex;
994 align-items: center;
995 gap: 8px;
996 background: rgba(188,140,255,0.1);
997 border: 1px solid rgba(188,140,255,0.3);
998 border-radius: 20px;
999 padding: 8px 20px;
1000 font-size: 13px;
1001 color: var(--purple);
1002 }
1003 .mh-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--purple); animation: pulse 2s ease-in-out infinite; }
1004 @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
1005
1006 /* ---- Footer ---- */
1007 footer {
1008 background: var(--bg2);
1009 border-top: 1px solid var(--border);
1010 padding: 24px 40px;
1011 display: flex;
1012 justify-content: space-between;
1013 align-items: center;
1014 flex-wrap: wrap;
1015 gap: 12px;
1016 font-size: 13px;
1017 color: var(--mute);
1018 }
1019 footer a { color: var(--accent2); }
1020
1021 /* ---- Nav ---- */
1022 nav {
1023 background: var(--bg2);
1024 border-bottom: 1px solid var(--border);
1025 padding: 0 40px;
1026 display: flex;
1027 align-items: center;
1028 gap: 0;
1029 height: 52px;
1030 }
1031 .nav-logo {
1032 font-family: var(--mono);
1033 font-size: 16px;
1034 font-weight: 700;
1035 color: var(--accent2);
1036 margin-right: 32px;
1037 text-decoration: none;
1038 }
1039 .nav-logo:hover { text-decoration: none; }
1040 .nav-link {
1041 font-size: 13px;
1042 color: var(--mute);
1043 padding: 0 14px;
1044 height: 100%;
1045 display: flex;
1046 align-items: center;
1047 border-bottom: 2px solid transparent;
1048 text-decoration: none;
1049 transition: color 0.15s, border-color 0.15s;
1050 }
1051 .nav-link:hover { color: var(--text); text-decoration: none; }
1052 .nav-link.current { color: var(--text); border-bottom-color: var(--accent); }
1053 .nav-spacer { flex: 1; }
1054 .nav-badge {
1055 font-size: 11px;
1056 background: rgba(79,142,247,0.12);
1057 border: 1px solid rgba(79,142,247,0.3);
1058 color: var(--accent2);
1059 border-radius: 4px;
1060 padding: 2px 8px;
1061 font-family: var(--mono);
1062 }
1063 </style>
1064 </head>
1065 <body>
1066
1067 <nav>
1068 <a class="nav-logo" href="#">muse</a>
1069 <a class="nav-link" href="tour_de_force.html">Demo</a>
1070 <a class="nav-link current" href="index.html">Domain Registry</a>
1071 <a class="nav-link" href="../docs/guide/plugin-authoring-guide.md">Plugin Guide</a>
1072 <div class="nav-spacer"></div>
1073 <span class="nav-badge">v0.1.1</span>
1074 </nav>
1075
1076 <!-- =================== HERO =================== -->
1077 <div class="hero">
1078 <h1 class="hero-wordmark">muse</h1>
1079 <div class="hero-version-any">Version Anything</div>
1080 <p class="hero-sub">
1081 One protocol. Any domain. <strong>Six methods</strong> between you and a
1082 complete version control system — branching, merging, conflict resolution,
1083 time-travel, and typed diffs — for free.
1084 </p>
1085 <div class="hero-cta-row">
1086 <a class="btn-primary" href="#build">Build a Domain Plugin</a>
1087 <a class="btn-outline" href="tour_de_force.html">Watch the Demo →</a>
1088 </div>
1089 <div class="domain-ticker">
1090 <div class="ticker-track">
1091 <span class="ticker-item active">🎵 music</span>
1092 <span class="ticker-item">🧬 genomics</span>
1093 <span class="ticker-item">🌐 3d-spatial</span>
1094 <span class="ticker-item">📈 financial</span>
1095 <span class="ticker-item">⚛️ simulation</span>
1096 <span class="ticker-item">🔬 proteomics</span>
1097 <span class="ticker-item">🏗️ cad</span>
1098 <span class="ticker-item">🎮 game-state</span>
1099 <span class="ticker-item">✦ your-domain</span>
1100 <!-- duplicate for seamless loop -->
1101 <span class="ticker-item active">🎵 music</span>
1102 <span class="ticker-item">🧬 genomics</span>
1103 <span class="ticker-item">🌐 3d-spatial</span>
1104 <span class="ticker-item">📈 financial</span>
1105 <span class="ticker-item">⚛️ simulation</span>
1106 <span class="ticker-item">🔬 proteomics</span>
1107 <span class="ticker-item">🏗️ cad</span>
1108 <span class="ticker-item">🎮 game-state</span>
1109 <span class="ticker-item">✦ your-domain</span>
1110 </div>
1111 </div>
1112 </div>
1113
1114 <!-- =================== PROTOCOL =================== -->
1115 <section id="protocol">
1116 <div class="inner">
1117 <div class="section-eyebrow">The Contract</div>
1118 <h2>The MuseDomainPlugin Protocol</h2>
1119 <p class="section-lead">
1120 Every domain — music, genomics, 3D spatial, financial models — implements
1121 the same <strong>six-method protocol</strong>. The core engine handles
1122 everything else: content-addressed storage, DAG, branches, log, merge base,
1123 cherry-pick, revert, stash, tags.
1124 </p>
1125
1126 <div class="proto-layout">
1127 <div class="stat-strip">
1128 <div class="stat-cell"><span class="stat-num">6</span><span class="stat-lbl">methods to implement</span></div>
1129 <div class="stat-cell"><span class="stat-num">14</span><span class="stat-lbl">CLI commands, free</span></div>
1130 <div class="stat-cell"><span class="stat-num">∞</span><span class="stat-lbl">domains possible</span></div>
1131 <div class="stat-cell"><span class="stat-num">0</span><span class="stat-lbl">core changes needed</span></div>
1132 </div>
1133 <div class="proto-table">
1134 <div class="proto-row hdr">
1135 <div class="proto-method">Method</div>
1136 <div class="proto-sig">Signature</div>
1137 <div class="proto-desc">Purpose</div>
1138 </div>
1139 <div class="proto-row">
1140 <div class="proto-method">snapshot</div>
1141 <div class="proto-sig">snapshot(live) → StateSnapshot</div>
1142 <div class="proto-desc">Capture current state as a content-addressable blob</div>
1143 </div>
1144 <div class="proto-row">
1145 <div class="proto-method">diff</div>
1146 <div class="proto-sig">diff(base, target) → StateDelta</div>
1147 <div class="proto-desc">Compute minimal change between two snapshots (added · removed · modified)</div>
1148 </div>
1149 <div class="proto-row">
1150 <div class="proto-method">merge</div>
1151 <div class="proto-sig">merge(base, left, right) → MergeResult</div>
1152 <div class="proto-desc">Three-way reconcile divergent state lines; surface conflicts per dimension</div>
1153 </div>
1154 <div class="proto-row">
1155 <div class="proto-method">drift</div>
1156 <div class="proto-sig">drift(committed, live) → DriftReport</div>
1157 <div class="proto-desc">Detect uncommitted changes between HEAD and working state</div>
1158 </div>
1159 <div class="proto-row">
1160 <div class="proto-method">apply</div>
1161 <div class="proto-sig">apply(delta, live) → LiveState</div>
1162 <div class="proto-desc">Apply a delta during checkout to reconstruct historical state</div>
1163 </div>
1164 <div class="proto-row">
1165 <div class="proto-method">schema</div>
1166 <div class="proto-sig">schema() → DomainSchema</div>
1167 <div class="proto-desc">Declare data structure — drives diff algorithm selection per dimension</div>
1168 </div>
1169 </div>
1170 </div>
1171 </div>
1172 </section>
1173
1174 <!-- =================== ENGINE CAPABILITIES =================== -->
1175 <section id="capabilities" style="background:var(--bg2)">
1176 <div class="inner">
1177 <div class="section-eyebrow">Engine Capabilities</div>
1178 <h2>What Every Plugin Gets for Free</h2>
1179 <p class="section-lead">
1180 The core engine provides four advanced capabilities that any domain plugin
1181 can opt into. Implement the protocol — the engine does the rest.
1182 </p>
1183
1184 <div class="cap-showcase-grid">
1185
1186 <div class="cap-showcase-card" style="--cap-color:#f9a825">
1187 <div class="cap-showcase-header">
1188 <span class="cap-showcase-badge" style="color:#f9a825;background:#f9a82515;border-color:#f9a82540">
1189 🔬 Typed Delta Algebra
1190 </span>
1191 <span class="cap-showcase-sub">StructuredDelta — every change is a typed operation</span>
1192 </div>
1193 <div class="cap-showcase-body">
1194 <p class="cap-showcase-desc">
1195 Unlike Git's blob diffs, Muse deltas are <strong>typed objects</strong>:
1196 <code>InsertOp</code>, <code>ReplaceOp</code>, <code>DeleteOp</code> — each
1197 carrying the address, before/after hashes, and affected dimensions.
1198 Machine-readable with <code>muse show --json</code>.
1199 </p>
1200 <pre class="cap-showcase-output">{{TYPED_DELTA_EXAMPLE}}</pre>
1201 </div>
1202 </div>
1203
1204 <div class="cap-showcase-card" style="--cap-color:#58a6ff">
1205 <div class="cap-showcase-header">
1206 <span class="cap-showcase-badge" style="color:#58a6ff;background:#58a6ff15;border-color:#58a6ff40">
1207 🗂️ Domain Schema
1208 </span>
1209 <span class="cap-showcase-sub">Per-domain dimensions drive diff algorithm selection</span>
1210 </div>
1211 <div class="cap-showcase-body">
1212 <p class="cap-showcase-desc">
1213 Each plugin's <code>schema()</code> method declares its dimensions and merge mode.
1214 The engine uses this to select the right diff algorithm per dimension and to
1215 surface only the dimensions that actually conflict.
1216 </p>
1217 <div class="cap-showcase-domain-grid" id="schema-domain-grid">
1218 {{ACTIVE_DOMAINS}}
1219 </div>
1220 </div>
1221 </div>
1222
1223 <div class="cap-showcase-card" style="--cap-color:#ef5350">
1224 <div class="cap-showcase-header">
1225 <span class="cap-showcase-badge" style="color:#ef5350;background:#ef535015;border-color:#ef535040">
1226 ⚙️ OT Merge
1227 </span>
1228 <span class="cap-showcase-sub">Operational transformation — independent ops commute automatically</span>
1229 </div>
1230 <div class="cap-showcase-body">
1231 <p class="cap-showcase-desc">
1232 Plugins implementing <strong>StructuredMergePlugin</strong> get operational
1233 transformation. Operations at different addresses commute automatically —
1234 only operations on the same address with incompatible intent surface a conflict.
1235 </p>
1236 <pre class="cap-showcase-output">{{OT_MERGE_EXAMPLE}}</pre>
1237 </div>
1238 </div>
1239
1240 <div class="cap-showcase-card" style="--cap-color:#bc8cff">
1241 <div class="cap-showcase-header">
1242 <span class="cap-showcase-badge" style="color:#bc8cff;background:#bc8cff15;border-color:#bc8cff40">
1243 🔮 CRDT Primitives
1244 </span>
1245 <span class="cap-showcase-sub">Convergent merge — any two replicas always reach the same state</span>
1246 </div>
1247 <div class="cap-showcase-body">
1248 <p class="cap-showcase-desc">
1249 Plugins implementing <strong>CRDTPlugin</strong> get four battle-tested
1250 convergent data structures. No coordination required between replicas.
1251 </p>
1252 <div class="crdt-mini-grid">
1253 {{CRDT_CARDS}}
1254 </div>
1255 </div>
1256 </div>
1257
1258 </div>
1259 </div>
1260 </section>
1261
1262 <!-- =================== BUILD =================== -->
1263 <section id="build" style="background:var(--bg)">
1264 <div class="inner">
1265 <div class="section-eyebrow">Build</div>
1266 <h2>Build in Three Steps</h2>
1267 <p class="section-lead">
1268 One command scaffolds the entire plugin skeleton. You fill in six methods.
1269 The full VCS follows.
1270 </p>
1271
1272 <div class="steps-grid">
1273 <div class="step-card">
1274 <div class="step-num">Step 1 · Scaffold</div>
1275 <div class="step-title">Generate the skeleton</div>
1276 <div class="step-desc">
1277 One command creates the plugin directory, class, and all six method stubs
1278 with full type annotations.
1279 </div>
1280 <div class="step-cmd">muse domains --new genomics</div>
1281 </div>
1282 <div class="step-card">
1283 <div class="step-num">Step 2 · Implement</div>
1284 <div class="step-title">Fill in the six methods</div>
1285 <div class="step-desc">
1286 Replace each <code>raise NotImplementedError</code> with your domain's
1287 snapshot, diff, merge, drift, apply, and schema logic.
1288 </div>
1289 <div class="step-cmd">vim muse/plugins/genomics/plugin.py</div>
1290 </div>
1291 <div class="step-card">
1292 <div class="step-num">Step 3 · Use</div>
1293 <div class="step-title">Full VCS, instantly</div>
1294 <div class="step-desc">
1295 Register in <code>registry.py</code>, then every Muse command works
1296 for your domain out of the box.
1297 </div>
1298 <div class="step-cmd">muse init --domain genomics</div>
1299 </div>
1300 </div>
1301 </div>
1302 </section>
1303
1304 <!-- =================== CODE =================== -->
1305 <section id="code">
1306 <div class="inner">
1307 <div class="section-eyebrow">The Scaffold</div>
1308 <h2>What <code>muse domains --new genomics</code> produces</h2>
1309 <p class="section-lead">
1310 A fully typed, immediately runnable plugin skeleton. Every method has the
1311 correct signature. You replace the stubs — the protocol does the rest.
1312 </p>
1313 <div class="code-wrap">
1314 <div class="code-bar">
1315 <div class="code-bar-dot" style="background:#ff5f57"></div>
1316 <div class="code-bar-dot" style="background:#febc2e"></div>
1317 <div class="code-bar-dot" style="background:#28c840"></div>
1318 <span class="code-bar-title">muse/plugins/genomics/plugin.py</span>
1319 </div>
1320 <div class="code-body">{{SCAFFOLD_SNIPPET}}</div>
1321 </div>
1322 <p style="margin-top:16px;font-size:13px;color:var(--mute)">
1323 Full walkthrough →
1324 <a href="../docs/guide/plugin-authoring-guide.md">docs/guide/plugin-authoring-guide.md</a>
1325 · CRDT extension →
1326 <a href="../docs/guide/crdt-reference.md">docs/guide/crdt-reference.md</a>
1327 </p>
1328 </div>
1329 </section>
1330
1331 <!-- =================== ACTIVE DOMAINS =================== -->
1332 <section id="registry" style="background:var(--bg2)">
1333 <div class="inner">
1334 <div class="section-eyebrow">Registry</div>
1335 <h2>Registered Domains</h2>
1336 <p class="section-lead">
1337 Domains currently registered in this Muse instance. The active domain
1338 is the one used when you run <code>muse commit</code>, <code>muse diff</code>,
1339 and all other commands.
1340 </p>
1341 <div class="domain-grid">
1342 {{ACTIVE_DOMAINS}}
1343 </div>
1344 </div>
1345 </section>
1346
1347 <!-- =================== PLANNED ECOSYSTEM =================== -->
1348 <section id="ecosystem">
1349 <div class="inner">
1350 <div class="section-eyebrow">Ecosystem</div>
1351 <h2>The Plugin Ecosystem</h2>
1352 <p class="section-lead">
1353 Music is the reference implementation. These are the domains planned
1354 next — and the slot waiting for yours.
1355 </p>
1356 <div class="planned-grid">
1357 {{PLANNED_DOMAINS}}
1358 </div>
1359 </div>
1360 </section>
1361
1362 <!-- =================== DISTRIBUTION =================== -->
1363 <section id="distribute" style="background:var(--bg2)">
1364 <div class="inner">
1365 <div class="section-eyebrow">Distribution</div>
1366 <h2>How to Share Your Plugin</h2>
1367 <p class="section-lead">
1368 Three tiers of distribution — from local prototype to globally searchable
1369 registry. Start local, publish when ready.
1370 </p>
1371 <div class="dist-grid">
1372 {{DIST_CARDS}}
1373 </div>
1374 </div>
1375 </section>
1376
1377 <!-- =================== MUSEHUB TEASER =================== -->
1378 <div class="musehub-section">
1379 <div class="musehub-logo">🌐</div>
1380 <h2><span>MuseHub</span> is coming</h2>
1381 <p class="musehub-desc">
1382 A <strong>centralized, searchable registry</strong> for Muse domain plugins —
1383 think npm or crates.io, but for any multidimensional versioned state.
1384 One command to publish. One command to install.
1385 </p>
1386 <div class="musehub-features">
1387 <div class="mh-feature">
1388 <div class="mh-feature-icon">🔍</div>
1389 <div class="mh-feature-title">Searchable</div>
1390 <div class="mh-feature-desc">Find plugins by domain, capability, or keyword</div>
1391 </div>
1392 <div class="mh-feature">
1393 <div class="mh-feature-icon">📦</div>
1394 <div class="mh-feature-title">Versioned</div>
1395 <div class="mh-feature-desc">Semantic versioning, pinned installs, changelogs</div>
1396 </div>
1397 <div class="mh-feature">
1398 <div class="mh-feature-icon">🔒</div>
1399 <div class="mh-feature-title">Private registries</div>
1400 <div class="mh-feature-desc">Self-host for enterprise or research teams</div>
1401 </div>
1402 <div class="mh-feature">
1403 <div class="mh-feature-icon">⚡</div>
1404 <div class="mh-feature-title">One command</div>
1405 <div class="mh-feature-desc"><code>muse init --domain @musehub/genomics</code></div>
1406 </div>
1407 </div>
1408 <div class="musehub-status">
1409 <div class="mh-dot"></div>
1410 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>
1411 </div>
1412 </div>
1413
1414 <footer>
1415 <span>Muse v0.1.1 · domain-agnostic version control for multidimensional state</span>
1416 <span>
1417 <a href="tour_de_force.html">Demo</a> ·
1418 <a href="https://github.com/cgcardona/muse">GitHub</a> ·
1419 <a href="../docs/guide/plugin-authoring-guide.md">Plugin Guide</a>
1420 </span>
1421 </footer>
1422
1423 </body>
1424 </html>
1425 """
1426
1427
1428 # ---------------------------------------------------------------------------
1429 # Entry point
1430 # ---------------------------------------------------------------------------
1431
1432 if __name__ == "__main__":
1433 import argparse
1434
1435 parser = argparse.ArgumentParser(
1436 description="Generate the Muse domain registry HTML page"
1437 )
1438 parser.add_argument(
1439 "--out",
1440 default=str(_ROOT / "artifacts" / "domain_registry.html"),
1441 help="Output HTML path",
1442 )
1443 args = parser.parse_args()
1444
1445 out_path = pathlib.Path(args.out)
1446 out_path.parent.mkdir(parents=True, exist_ok=True)
1447
1448 print("Generating domain_registry.html...")
1449 render(out_path)
1450 print(f"Open: file://{out_path.resolve()}")