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