cgcardona / muse public
render_domain_registry.py python
1139 lines 39.7 KB
90e5a0eb feat: supercharge Tour de Force acts 6-9 and build domain registry page (#28) 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 _load_domains() -> list[dict]:
29 """Run `muse domains --json` and return parsed output."""
30 try:
31 result = subprocess.run(
32 [sys.executable, "-m", "muse", "domains", "--json"],
33 capture_output=True,
34 text=True,
35 cwd=str(_ROOT),
36 timeout=15,
37 )
38 if result.returncode == 0:
39 raw = result.stdout.strip()
40 data: list[dict] = json.loads(raw)
41 return data
42 except Exception:
43 pass
44
45 # Fallback: static reference data
46 return [
47 {
48 "domain": "music",
49 "active": "true",
50 "capabilities": ["Typed Deltas", "Domain Schema", "OT Merge"],
51 "schema": {
52 "schema_version": "1",
53 "merge_mode": "three_way",
54 "description": "MIDI and audio file versioning with note-level diff and semantic merge",
55 "dimensions": [
56 {"name": "melodic", "description": "Note pitches and durations over time"},
57 {"name": "harmonic", "description": "Chord progressions and key signatures"},
58 {"name": "dynamic", "description": "Velocity and expression curves"},
59 {"name": "structural", "description": "Track layout, time signatures, tempo map"},
60 ],
61 },
62 }
63 ]
64
65
66 # ---------------------------------------------------------------------------
67 # Scaffold template (shown in the "Build in 3 steps" section)
68 # ---------------------------------------------------------------------------
69
70 _SCAFFOLD_SNIPPET = """\
71 from __future__ import annotations
72 from muse.domain import (
73 MuseDomainPlugin, LiveState, StateSnapshot,
74 StateDelta, DriftReport, MergeResult, DomainSchema,
75 )
76
77 class GenomicsPlugin(MuseDomainPlugin):
78 \"\"\"Version control for genomic sequences.\"\"\"
79
80 def snapshot(self, live_state: LiveState) -> StateSnapshot:
81 # Serialize current genome state to a content-addressable blob
82 raise NotImplementedError
83
84 def diff(self, base: StateSnapshot,
85 target: StateSnapshot) -> StateDelta:
86 # Compute minimal delta between two snapshots
87 raise NotImplementedError
88
89 def merge(self, base: StateSnapshot,
90 left: StateSnapshot,
91 right: StateSnapshot) -> MergeResult:
92 # Three-way merge — surface conflicts per dimension
93 raise NotImplementedError
94
95 def drift(self, committed: StateSnapshot,
96 live: LiveState) -> DriftReport:
97 # Detect uncommitted changes in the working state
98 raise NotImplementedError
99
100 def apply(self, delta: StateDelta,
101 live_state: LiveState) -> LiveState:
102 # Reconstruct historical state from a delta
103 raise NotImplementedError
104
105 def schema(self) -> DomainSchema:
106 # Declare dimensions — drives diff algorithm selection
107 raise NotImplementedError
108 """
109
110 # ---------------------------------------------------------------------------
111 # Planned / aspirational domains
112 # ---------------------------------------------------------------------------
113
114 _PLANNED_DOMAINS = [
115 {
116 "name": "Genomics",
117 "icon": "🧬",
118 "status": "planned",
119 "tagline": "Version sequences, variants, and annotations",
120 "dimensions": ["sequence", "variants", "annotations", "metadata"],
121 "color": "#3fb950",
122 },
123 {
124 "name": "3D / Spatial",
125 "icon": "🌐",
126 "status": "planned",
127 "tagline": "Merge spatial fields, meshes, and simulation frames",
128 "dimensions": ["geometry", "materials", "physics", "temporal"],
129 "color": "#58a6ff",
130 },
131 {
132 "name": "Financial",
133 "icon": "📈",
134 "status": "planned",
135 "tagline": "Track model versions, alpha signals, and risk state",
136 "dimensions": ["signals", "positions", "risk", "parameters"],
137 "color": "#f9a825",
138 },
139 {
140 "name": "Scientific Simulation",
141 "icon": "⚛️",
142 "status": "planned",
143 "tagline": "Snapshot simulation state across timesteps and parameter spaces",
144 "dimensions": ["state", "parameters", "observables", "checkpoints"],
145 "color": "#ab47bc",
146 },
147 {
148 "name": "Your Domain",
149 "icon": "✦",
150 "status": "yours",
151 "tagline": "Six methods. Any multidimensional state. Full VCS for free.",
152 "dimensions": ["your_dim_1", "your_dim_2", "..."],
153 "color": "#4f8ef7",
154 },
155 ]
156
157 # ---------------------------------------------------------------------------
158 # Distribution model description
159 # ---------------------------------------------------------------------------
160
161 _DISTRIBUTION_LEVELS = [
162 {
163 "tier": "Local",
164 "icon": "💻",
165 "title": "Local plugin (right now)",
166 "color": "#3fb950",
167 "steps": [
168 "muse domains --new &lt;name&gt;",
169 "Implement 6 methods in muse/plugins/&lt;name&gt;/plugin.py",
170 "Register in muse/plugins/registry.py",
171 "muse init --domain &lt;name&gt;",
172 ],
173 "desc": "Works today. Scaffold → implement → register. "
174 "Your plugin lives alongside the core.",
175 },
176 {
177 "tier": "Shareable",
178 "icon": "📦",
179 "title": "pip-installable package (right now)",
180 "color": "#58a6ff",
181 "steps": [
182 "Package your plugin as a Python module",
183 "pip install git+https://github.com/you/muse-plugin-genomics",
184 "Register the entry-point in pyproject.toml",
185 "muse init --domain genomics",
186 ],
187 "desc": "Share your plugin as a standard Python package. "
188 "Anyone with pip can install and use it.",
189 },
190 {
191 "tier": "MuseHub",
192 "icon": "🌐",
193 "title": "Centralized registry (coming — MuseHub)",
194 "color": "#bc8cff",
195 "steps": [
196 "musehub publish muse-plugin-genomics",
197 "musehub search genomics",
198 "muse init --domain @musehub/genomics",
199 "Browse plugins at musehub.io",
200 ],
201 "desc": "MuseHub is a planned centralized registry — npm for Muse plugins. "
202 "Versioned, searchable, one-command install.",
203 },
204 ]
205
206
207 # ---------------------------------------------------------------------------
208 # HTML template
209 # ---------------------------------------------------------------------------
210
211 def _render_domain_card(d: dict) -> str:
212 domain = d.get("domain", "unknown")
213 active = d.get("active") == "true"
214 schema = d.get("schema", {})
215 desc = schema.get("description", "")
216 dims = schema.get("dimensions", [])
217 caps = d.get("capabilities", [])
218
219 cap_html = " ".join(
220 f'<span class="cap-pill cap-{c.lower().replace(" ","-")}">{c}</span>'
221 for c in caps
222 )
223 dim_html = " · ".join(
224 f'<span class="dim-tag">{dim["name"]}</span>' for dim in dims
225 )
226
227 status_cls = "active-badge" if active else "reg-badge"
228 status_text = "● active" if active else "○ registered"
229 dot = '<span class="active-dot"></span>' if active else ""
230
231 short_desc = desc[:150] + ("…" if len(desc) > 150 else "")
232
233 return f"""
234 <div class="domain-card{' active-domain' if active else ''}">
235 <div class="domain-card-hdr">
236 <span class="{status_cls}">{status_text}</span>
237 <span class="domain-name-lg">{domain}</span>
238 {dot}
239 </div>
240 <div class="domain-card-body">
241 <p class="domain-desc">{short_desc}</p>
242 <div class="cap-row">{cap_html}</div>
243 <div class="dim-row"><strong>Dimensions:</strong> {dim_html}</div>
244 </div>
245 </div>"""
246
247
248 def _render_planned_card(p: dict) -> str:
249 dims = " · ".join(f'<span class="dim-tag">{d}</span>' for d in p["dimensions"])
250 cls = "planned-card yours" if p["status"] == "yours" else "planned-card"
251 return f"""
252 <div class="{cls}" style="--card-accent:{p['color']}">
253 <div class="planned-icon">{p['icon']}</div>
254 <div class="planned-name">{p['name']}</div>
255 <div class="planned-tag">{p['tagline']}</div>
256 <div class="planned-dims">{dims}</div>
257 {'<a class="cta-btn" href="#build">Build it →</a>' if p["status"] == "yours" else '<span class="coming-soon">coming soon</span>'}
258 </div>"""
259
260
261 def _render_dist_card(d: dict) -> str:
262 steps = "".join(
263 f'<li><code>{s}</code></li>' for s in d["steps"]
264 )
265 return f"""
266 <div class="dist-card" style="--dist-color:{d['color']}">
267 <div class="dist-header">
268 <span class="dist-icon">{d['icon']}</span>
269 <div>
270 <div class="dist-tier">{d['tier']}</div>
271 <div class="dist-title">{d['title']}</div>
272 </div>
273 </div>
274 <p class="dist-desc">{d['desc']}</p>
275 <ol class="dist-steps">{steps}</ol>
276 </div>"""
277
278
279 def render(output_path: pathlib.Path) -> None:
280 """Generate the domain registry HTML page."""
281 print(" Loading live domain data...")
282 domains = _load_domains()
283 print(f" Found {len(domains)} registered domain(s)")
284
285 active_domains_html = "\n".join(_render_domain_card(d) for d in domains)
286 planned_html = "\n".join(_render_planned_card(p) for p in _PLANNED_DOMAINS)
287 dist_html = "\n".join(_render_dist_card(d) for d in _DISTRIBUTION_LEVELS)
288
289 html = _HTML_TEMPLATE.replace("{{ACTIVE_DOMAINS}}", active_domains_html)
290 html = html.replace("{{PLANNED_DOMAINS}}", planned_html)
291 html = html.replace("{{DIST_CARDS}}", dist_html)
292 html = html.replace("{{SCAFFOLD_SNIPPET}}", _SCAFFOLD_SNIPPET)
293
294 output_path.write_text(html, encoding="utf-8")
295 size_kb = output_path.stat().st_size // 1024
296 print(f" HTML written ({size_kb}KB) → {output_path}")
297
298
299 # ---------------------------------------------------------------------------
300 # Large HTML template
301 # ---------------------------------------------------------------------------
302
303 _HTML_TEMPLATE = """\
304 <!DOCTYPE html>
305 <html lang="en">
306 <head>
307 <meta charset="utf-8">
308 <meta name="viewport" content="width=device-width, initial-scale=1">
309 <title>Muse — Version Anything · Domain Plugin Registry</title>
310 <style>
311 *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
312 :root {
313 --bg: #0d1117;
314 --bg2: #161b22;
315 --bg3: #21262d;
316 --border: #30363d;
317 --text: #e6edf3;
318 --mute: #8b949e;
319 --dim: #484f58;
320 --accent: #4f8ef7;
321 --accent2: #58a6ff;
322 --green: #3fb950;
323 --red: #f85149;
324 --yellow: #d29922;
325 --purple: #bc8cff;
326 --mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
327 --ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
328 --r: 8px;
329 }
330 html { scroll-behavior: smooth; }
331 body {
332 background: var(--bg);
333 color: var(--text);
334 font-family: var(--ui);
335 font-size: 15px;
336 line-height: 1.7;
337 }
338 a { color: var(--accent2); text-decoration: none; }
339 a:hover { text-decoration: underline; }
340 code {
341 font-family: var(--mono);
342 font-size: 0.88em;
343 background: var(--bg3);
344 border: 1px solid var(--border);
345 border-radius: 4px;
346 padding: 1px 6px;
347 }
348
349 /* ---- Hero ---- */
350 .hero {
351 background: linear-gradient(160deg, #0d1117 0%, #161b22 50%, #0d1117 100%);
352 border-bottom: 1px solid var(--border);
353 padding: 80px 40px 100px;
354 text-align: center;
355 position: relative;
356 overflow: hidden;
357 }
358 .hero::before {
359 content: '';
360 position: absolute;
361 inset: 0;
362 background:
363 radial-gradient(ellipse 60% 40% at 20% 50%, rgba(79,142,247,0.07) 0%, transparent 70%),
364 radial-gradient(ellipse 50% 40% at 80% 50%, rgba(188,140,255,0.06) 0%, transparent 70%);
365 pointer-events: none;
366 }
367 .hero-eyebrow {
368 font-family: var(--mono);
369 font-size: 12px;
370 color: var(--accent2);
371 letter-spacing: 2px;
372 text-transform: uppercase;
373 margin-bottom: 20px;
374 opacity: 0.8;
375 }
376 .hero h1 {
377 font-size: clamp(42px, 6vw, 72px);
378 font-weight: 800;
379 letter-spacing: -2px;
380 color: var(--text);
381 line-height: 1.05;
382 margin-bottom: 16px;
383 }
384 .hero h1 span {
385 background: linear-gradient(135deg, #4f8ef7, #bc8cff);
386 -webkit-background-clip: text;
387 -webkit-text-fill-color: transparent;
388 background-clip: text;
389 }
390 .hero-sub {
391 font-size: 18px;
392 color: var(--mute);
393 max-width: 600px;
394 margin: 0 auto 40px;
395 line-height: 1.6;
396 }
397 .hero-sub strong { color: var(--text); }
398 .hero-cta-row {
399 display: flex;
400 gap: 12px;
401 justify-content: center;
402 flex-wrap: wrap;
403 }
404 .btn-primary {
405 background: var(--accent);
406 color: #fff;
407 font-weight: 600;
408 padding: 12px 28px;
409 border-radius: var(--r);
410 font-size: 15px;
411 border: none;
412 cursor: pointer;
413 text-decoration: none;
414 transition: opacity 0.15s, transform 0.1s;
415 display: inline-block;
416 }
417 .btn-primary:hover { opacity: 0.88; transform: translateY(-1px); text-decoration: none; }
418 .btn-outline {
419 background: transparent;
420 color: var(--text);
421 font-weight: 500;
422 padding: 12px 28px;
423 border-radius: var(--r);
424 font-size: 15px;
425 border: 1px solid var(--border);
426 cursor: pointer;
427 text-decoration: none;
428 display: inline-block;
429 transition: border-color 0.15s, color 0.15s;
430 }
431 .btn-outline:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
432
433 /* ---- Domain ticker ---- */
434 .domain-ticker {
435 margin: 32px auto 0;
436 max-width: 700px;
437 overflow: hidden;
438 position: relative;
439 height: 34px;
440 }
441 .domain-ticker::before,
442 .domain-ticker::after {
443 content: '';
444 position: absolute;
445 top: 0; bottom: 0;
446 width: 60px;
447 z-index: 2;
448 }
449 .domain-ticker::before { left: 0; background: linear-gradient(90deg, var(--bg), transparent); }
450 .domain-ticker::after { right: 0; background: linear-gradient(-90deg, var(--bg), transparent); }
451 .ticker-track {
452 display: flex;
453 gap: 10px;
454 animation: ticker-scroll 18s linear infinite;
455 width: max-content;
456 }
457 @keyframes ticker-scroll {
458 0% { transform: translateX(0); }
459 100% { transform: translateX(-50%); }
460 }
461 .ticker-item {
462 font-family: var(--mono);
463 font-size: 13px;
464 padding: 4px 14px;
465 border-radius: 20px;
466 border: 1px solid var(--border);
467 white-space: nowrap;
468 color: var(--mute);
469 }
470 .ticker-item.active { border-color: rgba(79,142,247,0.5); color: var(--accent2); background: rgba(79,142,247,0.08); }
471
472 /* ---- Sections ---- */
473 section { padding: 72px 40px; border-top: 1px solid var(--border); }
474 .inner { max-width: 1100px; margin: 0 auto; }
475 .section-eyebrow {
476 font-family: var(--mono);
477 font-size: 11px;
478 color: var(--accent2);
479 letter-spacing: 2px;
480 text-transform: uppercase;
481 margin-bottom: 10px;
482 }
483 section h2 {
484 font-size: 32px;
485 font-weight: 700;
486 letter-spacing: -0.5px;
487 margin-bottom: 12px;
488 }
489 .section-lead {
490 font-size: 16px;
491 color: var(--mute);
492 max-width: 620px;
493 margin-bottom: 48px;
494 line-height: 1.7;
495 }
496 .section-lead strong { color: var(--text); }
497
498 /* ---- Stat strip ---- */
499 .stat-strip {
500 display: flex;
501 gap: 0;
502 border: 1px solid var(--border);
503 border-radius: var(--r);
504 overflow: hidden;
505 margin-bottom: 48px;
506 }
507 .stat-cell {
508 flex: 1;
509 padding: 20px 24px;
510 border-right: 1px solid var(--border);
511 text-align: center;
512 }
513 .stat-cell:last-child { border-right: none; }
514 .stat-num {
515 font-family: var(--mono);
516 font-size: 28px;
517 font-weight: 700;
518 color: var(--accent2);
519 display: block;
520 }
521 .stat-lbl { font-size: 12px; color: var(--mute); }
522
523 /* ---- Protocol table ---- */
524 .proto-table {
525 border: 1px solid var(--border);
526 border-radius: var(--r);
527 overflow: hidden;
528 margin-bottom: 40px;
529 }
530 .proto-row {
531 display: grid;
532 grid-template-columns: 90px 240px 1fr;
533 border-bottom: 1px solid var(--border);
534 }
535 .proto-row:last-child { border-bottom: none; }
536 .proto-row.hdr { background: var(--bg3); }
537 .proto-row > div { padding: 11px 16px; }
538 .proto-method { font-family: var(--mono); font-size: 13px; color: var(--accent2); font-weight: 600; }
539 .proto-sig { font-family: var(--mono); font-size: 12px; color: var(--mute); }
540 .proto-desc { font-size: 13px; color: var(--mute); }
541 .proto-row.hdr .proto-method,
542 .proto-row.hdr .proto-sig,
543 .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; }
544
545 /* ---- Three steps ---- */
546 .steps-grid {
547 display: grid;
548 grid-template-columns: repeat(3, 1fr);
549 gap: 24px;
550 }
551 @media (max-width: 800px) { .steps-grid { grid-template-columns: 1fr; } }
552 .step-card {
553 border: 1px solid var(--border);
554 border-radius: var(--r);
555 background: var(--bg2);
556 padding: 24px;
557 position: relative;
558 }
559 .step-num {
560 font-family: var(--mono);
561 font-size: 11px;
562 color: var(--accent);
563 font-weight: 700;
564 text-transform: uppercase;
565 letter-spacing: 1px;
566 margin-bottom: 10px;
567 }
568 .step-title { font-size: 17px; font-weight: 700; margin-bottom: 8px; }
569 .step-desc { font-size: 13px; color: var(--mute); line-height: 1.6; margin-bottom: 16px; }
570 .step-cmd {
571 font-family: var(--mono);
572 font-size: 12px;
573 background: var(--bg3);
574 border: 1px solid var(--border);
575 border-radius: 5px;
576 padding: 10px 14px;
577 color: var(--accent2);
578 }
579
580 /* ---- Code block ---- */
581 .code-wrap {
582 border: 1px solid var(--border);
583 border-radius: var(--r);
584 overflow: hidden;
585 }
586 .code-bar {
587 background: var(--bg3);
588 border-bottom: 1px solid var(--border);
589 padding: 8px 16px;
590 display: flex;
591 align-items: center;
592 gap: 8px;
593 }
594 .code-bar-dot {
595 width: 10px; height: 10px; border-radius: 50%;
596 }
597 .code-bar-title {
598 font-family: var(--mono);
599 font-size: 12px;
600 color: var(--mute);
601 margin-left: 6px;
602 }
603 .code-body {
604 background: #0a0e14;
605 padding: 20px 24px;
606 font-family: var(--mono);
607 font-size: 12.5px;
608 line-height: 1.7;
609 color: #abb2bf;
610 white-space: pre;
611 overflow-x: auto;
612 }
613 /* Simple syntax highlights */
614 .kw { color: #c678dd; }
615 .kw2 { color: #e06c75; }
616 .fn { color: #61afef; }
617 .str { color: #98c379; }
618 .cmt { color: #5c6370; font-style: italic; }
619 .cls { color: #e5c07b; }
620 .typ { color: #56b6c2; }
621
622 /* ---- Active domains grid ---- */
623 .domain-grid {
624 display: grid;
625 grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
626 gap: 20px;
627 }
628 .domain-card {
629 border: 1px solid var(--border);
630 border-radius: var(--r);
631 background: var(--bg2);
632 overflow: hidden;
633 transition: border-color 0.2s, transform 0.15s;
634 }
635 .domain-card:hover { border-color: var(--accent); transform: translateY(-2px); }
636 .domain-card.active-domain { border-color: rgba(63,185,80,0.4); }
637 .domain-card-hdr {
638 background: var(--bg3);
639 padding: 12px 16px;
640 border-bottom: 1px solid var(--border);
641 display: flex;
642 align-items: center;
643 gap: 10px;
644 }
645 .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); }
646 .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); }
647 .active-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); margin-left: auto; }
648 .domain-name-lg { font-family: var(--mono); font-size: 16px; font-weight: 700; color: var(--text); }
649 .domain-card-body { padding: 16px; }
650 .domain-desc { font-size: 13px; color: var(--mute); margin-bottom: 12px; }
651 .cap-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
652 .cap-pill { font-size: 10px; padding: 2px 8px; border-radius: 12px; border: 1px solid var(--border); color: var(--mute); background: var(--bg3); }
653 .cap-pill.cap-crdt { border-color: rgba(188,140,255,0.4); color: var(--purple); background: rgba(188,140,255,0.08); }
654 .cap-pill.cap-ot-merge { border-color: rgba(88,166,255,0.4); color: var(--accent2); background: rgba(88,166,255,0.08); }
655 .cap-pill.cap-domain-schema { border-color: rgba(63,185,80,0.4); color: var(--green); background: rgba(63,185,80,0.08); }
656 .cap-pill.cap-typed-deltas { border-color: rgba(249,168,37,0.4); color: #f9a825; background: rgba(249,168,37,0.08); }
657 .dim-row { font-size: 11px; color: var(--dim); }
658 .dim-tag { color: var(--mute); }
659
660 /* ---- Planned domains ---- */
661 .planned-grid {
662 display: grid;
663 grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
664 gap: 16px;
665 }
666 .planned-card {
667 border: 1px solid var(--border);
668 border-radius: var(--r);
669 background: var(--bg2);
670 padding: 20px 16px;
671 display: flex;
672 flex-direction: column;
673 gap: 8px;
674 transition: border-color 0.2s, transform 0.15s;
675 }
676 .planned-card:hover { border-color: var(--card-accent,var(--accent)); transform: translateY(-2px); }
677 .planned-card.yours { border: 2px dashed var(--accent); background: rgba(79,142,247,0.04); }
678 .planned-icon { font-size: 28px; }
679 .planned-name { font-size: 15px; font-weight: 700; color: var(--text); }
680 .planned-tag { font-size: 12px; color: var(--mute); line-height: 1.5; }
681 .planned-dims { font-size: 10px; color: var(--dim); }
682 .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; }
683 .cta-btn {
684 display: inline-block;
685 margin-top: 6px;
686 font-size: 12px;
687 font-weight: 600;
688 color: var(--accent2);
689 border: 1px solid rgba(88,166,255,0.4);
690 border-radius: 4px;
691 padding: 4px 12px;
692 text-decoration: none;
693 transition: background 0.15s;
694 }
695 .cta-btn:hover { background: rgba(88,166,255,0.1); text-decoration: none; }
696
697 /* ---- Distribution tiers ---- */
698 .dist-grid {
699 display: grid;
700 grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
701 gap: 24px;
702 }
703 .dist-card {
704 border: 1px solid var(--border);
705 border-top: 3px solid var(--dist-color, var(--accent));
706 border-radius: var(--r);
707 background: var(--bg2);
708 padding: 24px;
709 transition: transform 0.15s;
710 }
711 .dist-card:hover { transform: translateY(-2px); }
712 .dist-header { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 14px; }
713 .dist-icon { font-size: 26px; line-height: 1; }
714 .dist-tier { font-family: var(--mono); font-size: 11px; color: var(--dist-color,var(--accent)); letter-spacing: 1px; text-transform: uppercase; font-weight: 700; }
715 .dist-title { font-size: 14px; font-weight: 600; color: var(--text); margin-top: 2px; }
716 .dist-desc { font-size: 13px; color: var(--mute); margin-bottom: 16px; line-height: 1.6; }
717 .dist-steps { list-style: none; counter-reset: step; display: flex; flex-direction: column; gap: 6px; }
718 .dist-steps li { counter-increment: step; display: flex; align-items: flex-start; gap: 8px; font-size: 12px; color: var(--mute); }
719 .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; }
720 .dist-steps code { background: var(--bg3); border: 1px solid var(--border); border-radius: 4px; padding: 1px 6px; font-size: 11px; }
721
722 /* ---- MuseHub teaser ---- */
723 .musehub-section {
724 background: linear-gradient(135deg, #0d1117 0%, #1a0d2e 50%, #0d1117 100%);
725 padding: 80px 40px;
726 text-align: center;
727 border-top: 1px solid var(--border);
728 }
729 .musehub-logo {
730 font-size: 48px;
731 margin-bottom: 20px;
732 }
733 .musehub-section h2 {
734 font-size: 36px;
735 font-weight: 800;
736 letter-spacing: -1px;
737 margin-bottom: 12px;
738 }
739 .musehub-section h2 span {
740 background: linear-gradient(135deg, #bc8cff, #4f8ef7);
741 -webkit-background-clip: text;
742 -webkit-text-fill-color: transparent;
743 background-clip: text;
744 }
745 .musehub-desc {
746 font-size: 16px;
747 color: var(--mute);
748 max-width: 560px;
749 margin: 0 auto 36px;
750 }
751 .musehub-desc strong { color: var(--text); }
752 .musehub-features {
753 display: flex;
754 gap: 24px;
755 justify-content: center;
756 flex-wrap: wrap;
757 margin-bottom: 40px;
758 }
759 .mh-feature {
760 background: var(--bg2);
761 border: 1px solid rgba(188,140,255,0.2);
762 border-radius: var(--r);
763 padding: 16px 20px;
764 text-align: left;
765 min-width: 180px;
766 }
767 .mh-feature-icon { font-size: 20px; margin-bottom: 8px; }
768 .mh-feature-title { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 4px; }
769 .mh-feature-desc { font-size: 12px; color: var(--mute); }
770 .musehub-status {
771 display: inline-flex;
772 align-items: center;
773 gap: 8px;
774 background: rgba(188,140,255,0.1);
775 border: 1px solid rgba(188,140,255,0.3);
776 border-radius: 20px;
777 padding: 8px 20px;
778 font-size: 13px;
779 color: var(--purple);
780 }
781 .mh-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--purple); animation: pulse 2s ease-in-out infinite; }
782 @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
783
784 /* ---- Footer ---- */
785 footer {
786 background: var(--bg2);
787 border-top: 1px solid var(--border);
788 padding: 24px 40px;
789 display: flex;
790 justify-content: space-between;
791 align-items: center;
792 flex-wrap: wrap;
793 gap: 12px;
794 font-size: 13px;
795 color: var(--mute);
796 }
797 footer a { color: var(--accent2); }
798
799 /* ---- Nav ---- */
800 nav {
801 background: var(--bg2);
802 border-bottom: 1px solid var(--border);
803 padding: 0 40px;
804 display: flex;
805 align-items: center;
806 gap: 0;
807 height: 52px;
808 }
809 .nav-logo {
810 font-family: var(--mono);
811 font-size: 16px;
812 font-weight: 700;
813 color: var(--accent2);
814 margin-right: 32px;
815 text-decoration: none;
816 }
817 .nav-logo:hover { text-decoration: none; }
818 .nav-link {
819 font-size: 13px;
820 color: var(--mute);
821 padding: 0 14px;
822 height: 100%;
823 display: flex;
824 align-items: center;
825 border-bottom: 2px solid transparent;
826 text-decoration: none;
827 transition: color 0.15s, border-color 0.15s;
828 }
829 .nav-link:hover { color: var(--text); text-decoration: none; }
830 .nav-link.current { color: var(--text); border-bottom-color: var(--accent); }
831 .nav-spacer { flex: 1; }
832 .nav-badge {
833 font-size: 11px;
834 background: rgba(79,142,247,0.12);
835 border: 1px solid rgba(79,142,247,0.3);
836 color: var(--accent2);
837 border-radius: 4px;
838 padding: 2px 8px;
839 font-family: var(--mono);
840 }
841 </style>
842 </head>
843 <body>
844
845 <nav>
846 <a class="nav-logo" href="#">muse</a>
847 <a class="nav-link" href="tour_de_force.html">Tour de Force</a>
848 <a class="nav-link current" href="#">Domain Registry</a>
849 <a class="nav-link" href="../docs/guide/plugin-authoring-guide.md">Plugin Guide</a>
850 <div class="nav-spacer"></div>
851 <span class="nav-badge">v0.1.1</span>
852 </nav>
853
854 <!-- =================== HERO =================== -->
855 <div class="hero">
856 <div class="hero-eyebrow">Muse · Domain Plugin Registry</div>
857 <h1>Version <span>Anything</span></h1>
858 <p class="hero-sub">
859 One protocol. Any domain. <strong>Six methods</strong> between you and a
860 complete version control system — branching, merging, conflict resolution,
861 time-travel, and typed diffs — for free.
862 </p>
863 <div class="hero-cta-row">
864 <a class="btn-primary" href="#build">Build a Domain Plugin</a>
865 <a class="btn-outline" href="tour_de_force.html">Watch the Demo →</a>
866 </div>
867 <div class="domain-ticker">
868 <div class="ticker-track">
869 <span class="ticker-item active">🎵 music</span>
870 <span class="ticker-item">🧬 genomics</span>
871 <span class="ticker-item">🌐 3d-spatial</span>
872 <span class="ticker-item">📈 financial</span>
873 <span class="ticker-item">⚛️ simulation</span>
874 <span class="ticker-item">🔬 proteomics</span>
875 <span class="ticker-item">🏗️ cad</span>
876 <span class="ticker-item">🎮 game-state</span>
877 <span class="ticker-item">✦ your-domain</span>
878 <!-- duplicate for seamless loop -->
879 <span class="ticker-item active">🎵 music</span>
880 <span class="ticker-item">🧬 genomics</span>
881 <span class="ticker-item">🌐 3d-spatial</span>
882 <span class="ticker-item">📈 financial</span>
883 <span class="ticker-item">⚛️ simulation</span>
884 <span class="ticker-item">🔬 proteomics</span>
885 <span class="ticker-item">🏗️ cad</span>
886 <span class="ticker-item">🎮 game-state</span>
887 <span class="ticker-item">✦ your-domain</span>
888 </div>
889 </div>
890 </div>
891
892 <!-- =================== PROTOCOL =================== -->
893 <section id="protocol">
894 <div class="inner">
895 <div class="section-eyebrow">The Contract</div>
896 <h2>The MuseDomainPlugin Protocol</h2>
897 <p class="section-lead">
898 Every domain — music, genomics, 3D spatial, financial models — implements
899 the same <strong>six-method protocol</strong>. The core engine handles
900 everything else: content-addressed storage, DAG, branches, log, merge base,
901 cherry-pick, revert, stash, tags.
902 </p>
903
904 <div class="stat-strip">
905 <div class="stat-cell"><span class="stat-num">6</span><span class="stat-lbl">methods to implement</span></div>
906 <div class="stat-cell"><span class="stat-num">14</span><span class="stat-lbl">CLI commands, free</span></div>
907 <div class="stat-cell"><span class="stat-num">∞</span><span class="stat-lbl">domains possible</span></div>
908 <div class="stat-cell"><span class="stat-num">0</span><span class="stat-lbl">core changes needed</span></div>
909 </div>
910
911 <div class="proto-table">
912 <div class="proto-row hdr">
913 <div class="proto-method">Method</div>
914 <div class="proto-sig">Signature</div>
915 <div class="proto-desc">Purpose</div>
916 </div>
917 <div class="proto-row">
918 <div class="proto-method">snapshot</div>
919 <div class="proto-sig">snapshot(live) → StateSnapshot</div>
920 <div class="proto-desc">Capture current state as a content-addressable blob</div>
921 </div>
922 <div class="proto-row">
923 <div class="proto-method">diff</div>
924 <div class="proto-sig">diff(base, target) → StateDelta</div>
925 <div class="proto-desc">Compute minimal change between two snapshots (added · removed · modified)</div>
926 </div>
927 <div class="proto-row">
928 <div class="proto-method">merge</div>
929 <div class="proto-sig">merge(base, left, right) → MergeResult</div>
930 <div class="proto-desc">Three-way reconcile divergent state lines; surface conflicts per dimension</div>
931 </div>
932 <div class="proto-row">
933 <div class="proto-method">drift</div>
934 <div class="proto-sig">drift(committed, live) → DriftReport</div>
935 <div class="proto-desc">Detect uncommitted changes between HEAD and working state</div>
936 </div>
937 <div class="proto-row">
938 <div class="proto-method">apply</div>
939 <div class="proto-sig">apply(delta, live) → LiveState</div>
940 <div class="proto-desc">Apply a delta during checkout to reconstruct historical state</div>
941 </div>
942 <div class="proto-row">
943 <div class="proto-method">schema</div>
944 <div class="proto-sig">schema() → DomainSchema</div>
945 <div class="proto-desc">Declare data structure — drives diff algorithm selection per dimension</div>
946 </div>
947 </div>
948 </div>
949 </section>
950
951 <!-- =================== BUILD =================== -->
952 <section id="build" style="background:var(--bg2)">
953 <div class="inner">
954 <div class="section-eyebrow">Build</div>
955 <h2>Build in Three Steps</h2>
956 <p class="section-lead">
957 One command scaffolds the entire plugin skeleton. You fill in six methods.
958 The full VCS follows.
959 </p>
960
961 <div class="steps-grid">
962 <div class="step-card">
963 <div class="step-num">Step 1 · Scaffold</div>
964 <div class="step-title">Generate the skeleton</div>
965 <div class="step-desc">
966 One command creates the plugin directory, class, and all six method stubs
967 with full type annotations.
968 </div>
969 <div class="step-cmd">muse domains --new genomics</div>
970 </div>
971 <div class="step-card">
972 <div class="step-num">Step 2 · Implement</div>
973 <div class="step-title">Fill in the six methods</div>
974 <div class="step-desc">
975 Replace each <code>raise NotImplementedError</code> with your domain's
976 snapshot, diff, merge, drift, apply, and schema logic.
977 </div>
978 <div class="step-cmd">vim muse/plugins/genomics/plugin.py</div>
979 </div>
980 <div class="step-card">
981 <div class="step-num">Step 3 · Use</div>
982 <div class="step-title">Full VCS, instantly</div>
983 <div class="step-desc">
984 Register in <code>registry.py</code>, then every Muse command works
985 for your domain out of the box.
986 </div>
987 <div class="step-cmd">muse init --domain genomics</div>
988 </div>
989 </div>
990 </div>
991 </section>
992
993 <!-- =================== CODE =================== -->
994 <section id="code">
995 <div class="inner">
996 <div class="section-eyebrow">The Scaffold</div>
997 <h2>What <code>muse domains --new genomics</code> produces</h2>
998 <p class="section-lead">
999 A fully typed, immediately runnable plugin skeleton. Every method has the
1000 correct signature. You replace the stubs — the protocol does the rest.
1001 </p>
1002 <div class="code-wrap">
1003 <div class="code-bar">
1004 <div class="code-bar-dot" style="background:#ff5f57"></div>
1005 <div class="code-bar-dot" style="background:#febc2e"></div>
1006 <div class="code-bar-dot" style="background:#28c840"></div>
1007 <span class="code-bar-title">muse/plugins/genomics/plugin.py</span>
1008 </div>
1009 <div class="code-body">{{SCAFFOLD_SNIPPET}}</div>
1010 </div>
1011 <p style="margin-top:16px;font-size:13px;color:var(--mute)">
1012 Full walkthrough →
1013 <a href="../docs/guide/plugin-authoring-guide.md">docs/guide/plugin-authoring-guide.md</a>
1014 · CRDT extension →
1015 <a href="../docs/guide/crdt-reference.md">docs/guide/crdt-reference.md</a>
1016 </p>
1017 </div>
1018 </section>
1019
1020 <!-- =================== ACTIVE DOMAINS =================== -->
1021 <section id="registry" style="background:var(--bg2)">
1022 <div class="inner">
1023 <div class="section-eyebrow">Registry</div>
1024 <h2>Registered Domains</h2>
1025 <p class="section-lead">
1026 Domains currently registered in this Muse instance. The active domain
1027 is the one used when you run <code>muse commit</code>, <code>muse diff</code>,
1028 and all other commands.
1029 </p>
1030 <div class="domain-grid">
1031 {{ACTIVE_DOMAINS}}
1032 </div>
1033 </div>
1034 </section>
1035
1036 <!-- =================== PLANNED ECOSYSTEM =================== -->
1037 <section id="ecosystem">
1038 <div class="inner">
1039 <div class="section-eyebrow">Ecosystem</div>
1040 <h2>The Plugin Ecosystem</h2>
1041 <p class="section-lead">
1042 Music is the reference implementation. These are the domains planned
1043 next — and the slot waiting for yours.
1044 </p>
1045 <div class="planned-grid">
1046 {{PLANNED_DOMAINS}}
1047 </div>
1048 </div>
1049 </section>
1050
1051 <!-- =================== DISTRIBUTION =================== -->
1052 <section id="distribute" style="background:var(--bg2)">
1053 <div class="inner">
1054 <div class="section-eyebrow">Distribution</div>
1055 <h2>How to Share Your Plugin</h2>
1056 <p class="section-lead">
1057 Three tiers of distribution — from local prototype to globally searchable
1058 registry. Start local, publish when ready.
1059 </p>
1060 <div class="dist-grid">
1061 {{DIST_CARDS}}
1062 </div>
1063 </div>
1064 </section>
1065
1066 <!-- =================== MUSEHUB TEASER =================== -->
1067 <div class="musehub-section">
1068 <div class="musehub-logo">🌐</div>
1069 <h2><span>MuseHub</span> is coming</h2>
1070 <p class="musehub-desc">
1071 A <strong>centralized, searchable registry</strong> for Muse domain plugins —
1072 think npm or crates.io, but for any multidimensional versioned state.
1073 One command to publish. One command to install.
1074 </p>
1075 <div class="musehub-features">
1076 <div class="mh-feature">
1077 <div class="mh-feature-icon">🔍</div>
1078 <div class="mh-feature-title">Searchable</div>
1079 <div class="mh-feature-desc">Find plugins by domain, capability, or keyword</div>
1080 </div>
1081 <div class="mh-feature">
1082 <div class="mh-feature-icon">📦</div>
1083 <div class="mh-feature-title">Versioned</div>
1084 <div class="mh-feature-desc">Semantic versioning, pinned installs, changelogs</div>
1085 </div>
1086 <div class="mh-feature">
1087 <div class="mh-feature-icon">🔒</div>
1088 <div class="mh-feature-title">Private registries</div>
1089 <div class="mh-feature-desc">Self-host for enterprise or research teams</div>
1090 </div>
1091 <div class="mh-feature">
1092 <div class="mh-feature-icon">⚡</div>
1093 <div class="mh-feature-title">One command</div>
1094 <div class="mh-feature-desc"><code>muse init --domain @musehub/genomics</code></div>
1095 </div>
1096 </div>
1097 <div class="musehub-status">
1098 <div class="mh-dot"></div>
1099 MuseHub — planned · building in public at github.com/cgcardona/muse
1100 </div>
1101 </div>
1102
1103 <footer>
1104 <span>Muse v0.1.1 · domain-agnostic version control for multidimensional state</span>
1105 <span>
1106 <a href="tour_de_force.html">Tour de Force</a> ·
1107 <a href="https://github.com/cgcardona/muse">GitHub</a> ·
1108 <a href="../docs/guide/plugin-authoring-guide.md">Plugin Guide</a>
1109 </span>
1110 </footer>
1111
1112 </body>
1113 </html>
1114 """
1115
1116
1117 # ---------------------------------------------------------------------------
1118 # Entry point
1119 # ---------------------------------------------------------------------------
1120
1121 if __name__ == "__main__":
1122 import argparse
1123
1124 parser = argparse.ArgumentParser(
1125 description="Generate the Muse domain registry HTML page"
1126 )
1127 parser.add_argument(
1128 "--out",
1129 default=str(_ROOT / "artifacts" / "domain_registry.html"),
1130 help="Output HTML path",
1131 )
1132 args = parser.parse_args()
1133
1134 out_path = pathlib.Path(args.out)
1135 out_path.parent.mkdir(parents=True, exist_ok=True)
1136
1137 print("Generating domain_registry.html...")
1138 render(out_path)
1139 print(f"Open: file://{out_path.resolve()}")