cgcardona / muse public
render_html.py python
1821 lines 62.4 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 Tour de Force — HTML renderer.
3
4 Takes the structured TourData dict produced by tour_de_force.py and renders
5 a self-contained, shareable HTML file with an interactive D3 commit DAG,
6 operation log, architecture diagram, and animated replay.
7
8 Stand-alone usage
9 -----------------
10 python tools/render_html.py artifacts/tour_de_force.json
11 python tools/render_html.py artifacts/tour_de_force.json --out custom.html
12 """
13 from __future__ import annotations
14
15 import json
16 import pathlib
17 import sys
18 import urllib.request
19
20
21 # ---------------------------------------------------------------------------
22 # D3.js fetcher
23 # ---------------------------------------------------------------------------
24
25 _D3_CDN = "https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js"
26 _D3_FALLBACK = f'<script src="{_D3_CDN}"></script>'
27
28
29 def _fetch_d3() -> str:
30 """Download D3.js v7 minified. Returns the source or a CDN script tag."""
31 try:
32 with urllib.request.urlopen(_D3_CDN, timeout=15) as resp:
33 src = resp.read().decode("utf-8")
34 print(f" ↓ D3.js fetched ({len(src)//1024}KB)")
35 return f"<script>\n{src}\n</script>"
36 except Exception as exc:
37 print(f" ⚠ Could not fetch D3 ({exc}); using CDN link in HTML")
38 return _D3_FALLBACK
39
40
41 # ---------------------------------------------------------------------------
42 # Architecture SVG
43 # ---------------------------------------------------------------------------
44
45 _ARCH_HTML = """\
46 <div class="arch-flow">
47 <div class="arch-row">
48 <div class="arch-box cli">
49 <div class="box-title">muse CLI</div>
50 <div class="box-sub">14 commands</div>
51 <div class="box-detail">init · commit · log · diff · show · branch<br>
52 checkout · merge · reset · revert · cherry-pick<br>
53 stash · tag · status</div>
54 </div>
55 </div>
56 <div class="arch-connector"><div class="connector-line"></div><div class="connector-arrow">▼</div></div>
57 <div class="arch-row">
58 <div class="arch-box registry">
59 <div class="box-title">Plugin Registry</div>
60 <div class="box-sub">resolve_plugin(root)</div>
61 </div>
62 </div>
63 <div class="arch-connector"><div class="connector-line"></div><div class="connector-arrow">▼</div></div>
64 <div class="arch-row">
65 <div class="arch-box core">
66 <div class="box-title">Core Engine</div>
67 <div class="box-sub">DAG · Content-addressed Objects · Branches · Store · Log Graph · Merge Base</div>
68 </div>
69 </div>
70 <div class="arch-connector"><div class="connector-line"></div><div class="connector-arrow">▼</div></div>
71 <div class="arch-row">
72 <div class="arch-box protocol">
73 <div class="box-title">MuseDomainPlugin Protocol</div>
74 <div class="box-sub">Implement 6 methods → get the full VCS for free</div>
75 </div>
76 </div>
77 <div class="arch-connector"><div class="connector-line"></div><div class="connector-arrow">▼</div></div>
78 <div class="arch-row plugins-row">
79 <div class="arch-box plugin active">
80 <div class="box-title">MusicPlugin</div>
81 <div class="box-sub">reference impl<br>MIDI · notes · CC · pitch</div>
82 </div>
83 <div class="arch-box plugin planned">
84 <div class="box-title">GenomicsPlugin</div>
85 <div class="box-sub">planned<br>sequences · variants</div>
86 </div>
87 <div class="arch-box plugin planned">
88 <div class="box-title">SpacetimePlugin</div>
89 <div class="box-sub">planned<br>3D fields · time-slices</div>
90 </div>
91 <div class="arch-box plugin planned">
92 <div class="box-title">YourPlugin</div>
93 <div class="box-sub">implement 6 methods<br>get VCS for free</div>
94 </div>
95 </div>
96 </div>
97
98 <div class="protocol-table">
99 <div class="proto-row header">
100 <div class="proto-method">Method</div>
101 <div class="proto-sig">Signature</div>
102 <div class="proto-desc">Purpose</div>
103 </div>
104 <div class="proto-row">
105 <div class="proto-method">snapshot</div>
106 <div class="proto-sig">snapshot(live_state) → StateSnapshot</div>
107 <div class="proto-desc">Capture current state as a content-addressable JSON blob</div>
108 </div>
109 <div class="proto-row">
110 <div class="proto-method">diff</div>
111 <div class="proto-sig">diff(base, target) → StateDelta</div>
112 <div class="proto-desc">Compute minimal change between two snapshots (added · removed · modified)</div>
113 </div>
114 <div class="proto-row">
115 <div class="proto-method">merge</div>
116 <div class="proto-sig">merge(base, left, right) → MergeResult</div>
117 <div class="proto-desc">Three-way reconcile divergent state lines; surface conflicts</div>
118 </div>
119 <div class="proto-row">
120 <div class="proto-method">drift</div>
121 <div class="proto-sig">drift(committed, live) → DriftReport</div>
122 <div class="proto-desc">Detect uncommitted changes between HEAD and working state</div>
123 </div>
124 <div class="proto-row">
125 <div class="proto-method">apply</div>
126 <div class="proto-sig">apply(delta, live_state) → LiveState</div>
127 <div class="proto-desc">Apply a delta during checkout to reconstruct historical state</div>
128 </div>
129 <div class="proto-row">
130 <div class="proto-method">schema</div>
131 <div class="proto-sig">schema() → DomainSchema</div>
132 <div class="proto-desc">Declare data structure — drives diff algorithm selection per dimension</div>
133 </div>
134 </div>
135 """
136
137
138 # ---------------------------------------------------------------------------
139 # HTML template
140 # ---------------------------------------------------------------------------
141
142 _HTML_TEMPLATE = """\
143 <!DOCTYPE html>
144 <html lang="en">
145 <head>
146 <meta charset="utf-8">
147 <meta name="viewport" content="width=device-width, initial-scale=1">
148 <title>Muse — Tour de Force</title>
149 <style>
150 /* ---- Reset & base ---- */
151 *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
152 :root {
153 --bg: #0d1117;
154 --bg2: #161b22;
155 --bg3: #21262d;
156 --border: #30363d;
157 --text: #e6edf3;
158 --text-mute: #8b949e;
159 --text-dim: #484f58;
160 --accent: #4f8ef7;
161 --accent2: #58a6ff;
162 --green: #3fb950;
163 --red: #f85149;
164 --yellow: #d29922;
165 --purple: #bc8cff;
166 --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
167 --font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
168 --radius: 8px;
169 }
170 html { scroll-behavior: smooth; }
171 body {
172 background: var(--bg);
173 color: var(--text);
174 font-family: var(--font-ui);
175 font-size: 14px;
176 line-height: 1.6;
177 min-height: 100vh;
178 }
179
180 /* ---- Header ---- */
181 header {
182 background: var(--bg2);
183 border-bottom: 1px solid var(--border);
184 padding: 24px 40px;
185 }
186 .header-top {
187 display: flex;
188 align-items: baseline;
189 gap: 16px;
190 flex-wrap: wrap;
191 }
192 header h1 {
193 font-size: 28px;
194 font-weight: 700;
195 letter-spacing: -0.5px;
196 color: var(--accent2);
197 font-family: var(--font-mono);
198 }
199 .tagline {
200 color: var(--text-mute);
201 font-size: 14px;
202 }
203 .stats-bar {
204 display: flex;
205 gap: 24px;
206 margin-top: 14px;
207 flex-wrap: wrap;
208 }
209 .stat {
210 display: flex;
211 flex-direction: column;
212 align-items: center;
213 gap: 2px;
214 }
215 .stat-num {
216 font-size: 22px;
217 font-weight: 700;
218 font-family: var(--font-mono);
219 color: var(--accent2);
220 }
221 .stat-label {
222 font-size: 11px;
223 color: var(--text-mute);
224 text-transform: uppercase;
225 letter-spacing: 0.8px;
226 }
227 .stat-sep { color: var(--border); font-size: 22px; align-self: center; }
228 .header-nav-link {
229 margin-left: auto;
230 font-size: 12px;
231 color: var(--accent2);
232 text-decoration: none;
233 border: 1px solid rgba(88,166,255,0.3);
234 border-radius: 4px;
235 padding: 4px 12px;
236 transition: background 0.15s;
237 }
238 .header-nav-link:hover { background: rgba(88,166,255,0.08); }
239 .version-badge {
240 margin-left: auto;
241 padding: 4px 10px;
242 border: 1px solid var(--border);
243 border-radius: 20px;
244 font-size: 12px;
245 font-family: var(--font-mono);
246 color: var(--text-mute);
247 }
248
249 /* ---- Main layout ---- */
250 .main-container {
251 display: grid;
252 grid-template-columns: 1fr 380px;
253 gap: 0;
254 height: calc(100vh - 130px);
255 min-height: 600px;
256 }
257
258 /* ---- DAG panel ---- */
259 .dag-panel {
260 border-right: 1px solid var(--border);
261 display: flex;
262 flex-direction: column;
263 overflow: hidden;
264 }
265 .dag-header {
266 display: flex;
267 align-items: center;
268 gap: 12px;
269 padding: 12px 20px;
270 border-bottom: 1px solid var(--border);
271 background: var(--bg2);
272 flex-shrink: 0;
273 }
274 .dag-header h2 {
275 font-size: 13px;
276 font-weight: 600;
277 color: var(--text-mute);
278 text-transform: uppercase;
279 letter-spacing: 0.8px;
280 }
281 .controls { display: flex; gap: 8px; margin-left: auto; align-items: center; }
282 .btn {
283 padding: 6px 14px;
284 border-radius: var(--radius);
285 border: 1px solid var(--border);
286 background: var(--bg3);
287 color: var(--text);
288 cursor: pointer;
289 font-size: 12px;
290 font-family: var(--font-ui);
291 transition: all 0.15s;
292 }
293 .btn:hover { background: var(--border); }
294 .btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
295 .btn.primary:hover { background: var(--accent2); }
296 .btn:disabled { opacity: 0.35; cursor: not-allowed; }
297 .btn:disabled:hover { background: var(--bg3); }
298 .step-counter {
299 font-size: 11px;
300 font-family: var(--font-mono);
301 color: var(--text-mute);
302 min-width: 80px;
303 text-align: right;
304 }
305 .dag-scroll {
306 flex: 1;
307 overflow: auto;
308 padding: 20px;
309 }
310 #dag-svg { display: block; }
311 .branch-legend {
312 display: flex;
313 flex-wrap: wrap;
314 gap: 10px;
315 padding: 8px 20px;
316 border-top: 1px solid var(--border);
317 background: var(--bg2);
318 flex-shrink: 0;
319 }
320 .legend-item {
321 display: flex;
322 align-items: center;
323 gap: 6px;
324 font-size: 11px;
325 color: var(--text-mute);
326 }
327 .legend-dot {
328 width: 10px;
329 height: 10px;
330 border-radius: 50%;
331 flex-shrink: 0;
332 }
333
334 /* ---- Log panel ---- */
335 .log-panel {
336 display: flex;
337 flex-direction: column;
338 overflow: hidden;
339 background: var(--bg);
340 }
341 .log-header {
342 padding: 12px 16px;
343 border-bottom: 1px solid var(--border);
344 background: var(--bg2);
345 flex-shrink: 0;
346 }
347 .log-header h2 {
348 font-size: 13px;
349 font-weight: 600;
350 color: var(--text-mute);
351 text-transform: uppercase;
352 letter-spacing: 0.8px;
353 }
354 .log-scroll {
355 flex: 1;
356 overflow-y: auto;
357 padding: 0;
358 }
359 .act-header {
360 padding: 10px 16px 6px;
361 font-size: 11px;
362 font-weight: 700;
363 text-transform: uppercase;
364 letter-spacing: 1px;
365 color: var(--text-dim);
366 border-top: 1px solid var(--border);
367 margin-top: 4px;
368 position: sticky;
369 top: 0;
370 background: var(--bg);
371 z-index: 1;
372 }
373 .act-header:first-child { border-top: none; margin-top: 0; }
374 .event-item {
375 padding: 8px 16px;
376 border-bottom: 1px solid #1a1f26;
377 opacity: 0.3;
378 transition: opacity 0.3s, background 0.2s;
379 cursor: default;
380 }
381 .event-item.revealed { opacity: 1; }
382 .event-item.active { background: rgba(79,142,247,0.08); border-left: 2px solid var(--accent); }
383 .event-item.failed { border-left: 2px solid var(--red); }
384 .event-cmd {
385 font-family: var(--font-mono);
386 font-size: 12px;
387 color: var(--text);
388 margin-bottom: 3px;
389 }
390 .event-cmd .cmd-prefix { color: var(--text-dim); }
391 .event-cmd .cmd-name { color: var(--accent2); font-weight: 600; }
392 .event-cmd .cmd-args { color: var(--text); }
393 .event-output {
394 font-family: var(--font-mono);
395 font-size: 11px;
396 color: var(--text-mute);
397 white-space: pre-wrap;
398 word-break: break-all;
399 max-height: 80px;
400 overflow: hidden;
401 text-overflow: ellipsis;
402 }
403 .event-output.conflict { color: var(--red); }
404 .event-output.success { color: var(--green); }
405 .event-item.rich-act .event-output { max-height: 220px; }
406
407 /* ---- Act jump bar ---- */
408 .act-jump-bar {
409 display: flex;
410 flex-wrap: wrap;
411 gap: 4px;
412 padding: 6px 12px;
413 border-bottom: 1px solid var(--border);
414 background: var(--bg2);
415 flex-shrink: 0;
416 }
417 .act-jump-bar span {
418 font-size: 10px;
419 color: var(--text-dim);
420 align-self: center;
421 margin-right: 4px;
422 font-weight: 600;
423 text-transform: uppercase;
424 letter-spacing: 0.6px;
425 }
426 .act-jump-btn {
427 font-size: 10px;
428 padding: 2px 8px;
429 border-radius: 4px;
430 background: var(--bg3);
431 border: 1px solid var(--border);
432 color: var(--text-mute);
433 cursor: pointer;
434 font-family: var(--font-mono);
435 transition: background 0.15s, color 0.15s;
436 }
437 .act-jump-btn:hover { background: var(--bg); color: var(--accent); border-color: var(--accent); }
438 .act-jump-btn.reveal-all { border-color: var(--green); color: var(--green); }
439 .act-jump-btn.reveal-all:hover { background: rgba(63,185,80,0.08); }
440
441 .event-meta {
442 display: flex;
443 gap: 8px;
444 margin-top: 3px;
445 font-size: 10px;
446 color: var(--text-dim);
447 }
448 .tag-commit { background: rgba(79,142,247,0.15); color: var(--accent2); padding: 1px 5px; border-radius: 3px; font-family: var(--font-mono); }
449 .tag-time { color: var(--text-dim); }
450
451 /* ---- DAG SVG styles ---- */
452 .commit-node { cursor: pointer; }
453 .commit-node:hover circle { filter: brightness(1.3); }
454 .commit-node.highlighted circle { filter: brightness(1.5) drop-shadow(0 0 6px currentColor); }
455 .commit-label { font-size: 10px; fill: var(--text-mute); font-family: var(--font-mono); }
456 .commit-msg { font-size: 10px; fill: var(--text-mute); }
457 .commit-node.highlighted .commit-label,
458 .commit-node.highlighted .commit-msg { fill: var(--text); }
459 text { font-family: -apple-system, system-ui, sans-serif; }
460
461 /* ---- Domain Dashboard section ---- */
462 .domain-section {
463 background: var(--bg);
464 border-top: 1px solid var(--border);
465 padding: 60px 40px;
466 }
467 .domain-inner { max-width: 1100px; margin: 0 auto; }
468 .domain-section h2, .crdt-section h2 {
469 font-size: 22px;
470 font-weight: 700;
471 margin-bottom: 8px;
472 color: var(--text);
473 }
474 .domain-section .section-intro, .crdt-section .section-intro {
475 color: var(--text-mute);
476 max-width: 680px;
477 margin-bottom: 36px;
478 line-height: 1.7;
479 }
480 .domain-section .section-intro strong, .crdt-section .section-intro strong { color: var(--text); }
481 .domain-grid {
482 display: grid;
483 grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
484 gap: 20px;
485 }
486 .domain-card {
487 border: 1px solid var(--border);
488 border-radius: var(--radius);
489 background: var(--bg2);
490 overflow: hidden;
491 transition: border-color 0.2s;
492 }
493 .domain-card:hover { border-color: var(--accent); }
494 .domain-card.active-domain { border-color: rgba(249,168,37,0.5); }
495 .domain-card.scaffold-domain { border-style: dashed; opacity: 0.85; }
496 .domain-card-header {
497 padding: 14px 16px;
498 border-bottom: 1px solid var(--border);
499 display: flex;
500 align-items: center;
501 gap: 10px;
502 background: var(--bg3);
503 }
504 .domain-badge {
505 font-family: var(--font-mono);
506 font-size: 11px;
507 padding: 2px 8px;
508 border-radius: 4px;
509 background: rgba(79,142,247,0.12);
510 border: 1px solid rgba(79,142,247,0.3);
511 color: var(--accent2);
512 }
513 .domain-badge.active { background: rgba(249,168,37,0.12); border-color: rgba(249,168,37,0.4); color: #f9a825; }
514 .domain-name {
515 font-weight: 700;
516 font-size: 15px;
517 font-family: var(--font-mono);
518 color: var(--text);
519 }
520 .domain-active-dot {
521 margin-left: auto;
522 width: 8px;
523 height: 8px;
524 border-radius: 50%;
525 background: var(--green);
526 }
527 .domain-card-body { padding: 14px 16px; }
528 .domain-desc {
529 font-size: 13px;
530 color: var(--text-mute);
531 margin-bottom: 12px;
532 line-height: 1.5;
533 }
534 .domain-caps {
535 display: flex;
536 flex-wrap: wrap;
537 gap: 6px;
538 margin-bottom: 12px;
539 }
540 .cap-pill {
541 font-size: 10px;
542 padding: 2px 8px;
543 border-radius: 12px;
544 border: 1px solid var(--border);
545 color: var(--text-mute);
546 background: var(--bg3);
547 }
548 .cap-pill.cap-crdt { border-color: rgba(188,140,255,0.4); color: var(--purple); background: rgba(188,140,255,0.08); }
549 .cap-pill.cap-ot { border-color: rgba(88,166,255,0.4); color: var(--accent2); background: rgba(88,166,255,0.08); }
550 .cap-pill.cap-schema { border-color: rgba(63,185,80,0.4); color: var(--green); background: rgba(63,185,80,0.08); }
551 .cap-pill.cap-delta { border-color: rgba(249,168,37,0.4); color: #f9a825; background: rgba(249,168,37,0.08); }
552 .domain-dims {
553 font-size: 11px;
554 color: var(--text-dim);
555 }
556 .domain-dims strong { color: var(--text-mute); }
557 .domain-new-card {
558 border: 2px dashed var(--border);
559 border-radius: var(--radius);
560 background: transparent;
561 display: flex;
562 flex-direction: column;
563 align-items: center;
564 justify-content: center;
565 padding: 32px 20px;
566 text-align: center;
567 gap: 12px;
568 transition: border-color 0.2s;
569 cursor: default;
570 }
571 .domain-new-card:hover { border-color: var(--accent); }
572 .domain-new-icon { font-size: 28px; color: var(--text-dim); }
573 .domain-new-title { font-size: 14px; font-weight: 600; color: var(--text-mute); }
574 .domain-new-cmd {
575 font-family: var(--font-mono);
576 font-size: 12px;
577 background: var(--bg3);
578 border: 1px solid var(--border);
579 border-radius: 4px;
580 padding: 6px 12px;
581 color: var(--accent2);
582 }
583 .domain-new-link {
584 font-size: 11px;
585 color: var(--text-dim);
586 }
587 .domain-new-link a { color: var(--accent); text-decoration: none; }
588 .domain-new-link a:hover { text-decoration: underline; }
589
590 /* ---- CRDT Primitives section ---- */
591 .crdt-section {
592 background: var(--bg2);
593 border-top: 1px solid var(--border);
594 padding: 60px 40px;
595 }
596 .crdt-inner { max-width: 1100px; margin: 0 auto; }
597 .crdt-grid {
598 display: grid;
599 grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
600 gap: 20px;
601 }
602 .crdt-card {
603 border: 1px solid var(--border);
604 border-radius: var(--radius);
605 background: var(--bg);
606 overflow: hidden;
607 transition: border-color 0.2s;
608 }
609 .crdt-card:hover { border-color: var(--purple); }
610 .crdt-card-header {
611 padding: 12px 16px;
612 border-bottom: 1px solid var(--border);
613 background: rgba(188,140,255,0.06);
614 display: flex;
615 align-items: center;
616 gap: 8px;
617 }
618 .crdt-type-badge {
619 font-family: var(--font-mono);
620 font-size: 11px;
621 padding: 2px 8px;
622 border-radius: 4px;
623 background: rgba(188,140,255,0.12);
624 border: 1px solid rgba(188,140,255,0.3);
625 color: var(--purple);
626 }
627 .crdt-card-title { font-weight: 700; font-size: 14px; color: var(--text); }
628 .crdt-card-sub { font-size: 11px; color: var(--text-mute); }
629 .crdt-card-body { padding: 14px 16px; }
630 .crdt-output {
631 font-family: var(--font-mono);
632 font-size: 11px;
633 color: var(--text-mute);
634 white-space: pre-wrap;
635 line-height: 1.6;
636 background: var(--bg3);
637 border: 1px solid var(--border);
638 border-radius: 4px;
639 padding: 10px 12px;
640 }
641 .crdt-output .out-win { color: var(--green); }
642 .crdt-output .out-key { color: var(--accent2); }
643
644 /* ---- Architecture section ---- */
645 .arch-section {
646 background: var(--bg2);
647 border-top: 1px solid var(--border);
648 padding: 48px 40px;
649 }
650 .arch-inner { max-width: 1100px; margin: 0 auto; }
651 .arch-section h2 {
652 font-size: 22px;
653 font-weight: 700;
654 margin-bottom: 8px;
655 color: var(--text);
656 }
657 .arch-section .section-intro {
658 color: var(--text-mute);
659 max-width: 680px;
660 margin-bottom: 40px;
661 line-height: 1.7;
662 }
663 .arch-section .section-intro strong { color: var(--text); }
664 .arch-content {
665 display: grid;
666 grid-template-columns: 380px 1fr;
667 gap: 48px;
668 align-items: start;
669 }
670
671 /* Architecture flow diagram */
672 .arch-flow {
673 display: flex;
674 flex-direction: column;
675 align-items: center;
676 gap: 0;
677 }
678 .arch-row { width: 100%; display: flex; justify-content: center; }
679 .plugins-row { gap: 8px; flex-wrap: wrap; }
680 .arch-box {
681 border: 1px solid var(--border);
682 border-radius: var(--radius);
683 padding: 12px 16px;
684 background: var(--bg3);
685 width: 100%;
686 max-width: 340px;
687 transition: border-color 0.2s;
688 }
689 .arch-box:hover { border-color: var(--accent); }
690 .arch-box.cli { border-color: rgba(79,142,247,0.4); }
691 .arch-box.registry { border-color: rgba(188,140,255,0.3); }
692 .arch-box.core { border-color: rgba(63,185,80,0.3); background: rgba(63,185,80,0.05); }
693 .arch-box.protocol { border-color: rgba(79,142,247,0.5); background: rgba(79,142,247,0.05); }
694 .arch-box.plugin { max-width: 160px; width: auto; flex: 1; }
695 .arch-box.plugin.active { border-color: rgba(249,168,37,0.5); background: rgba(249,168,37,0.05); }
696 .arch-box.plugin.planned { opacity: 0.6; border-style: dashed; }
697 .box-title { font-weight: 600; font-size: 13px; color: var(--text); }
698 .box-sub { font-size: 11px; color: var(--text-mute); margin-top: 3px; }
699 .box-detail { font-size: 10px; color: var(--text-dim); margin-top: 4px; line-height: 1.5; }
700 .arch-connector {
701 display: flex;
702 flex-direction: column;
703 align-items: center;
704 height: 24px;
705 color: var(--border);
706 }
707 .connector-line { width: 1px; flex: 1; background: var(--border); }
708 .connector-arrow { font-size: 10px; }
709
710 /* Protocol table */
711 .protocol-table { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
712 .proto-row {
713 display: grid;
714 grid-template-columns: 80px 220px 1fr;
715 gap: 0;
716 border-bottom: 1px solid var(--border);
717 }
718 .proto-row:last-child { border-bottom: none; }
719 .proto-row.header { background: var(--bg3); }
720 .proto-row > div { padding: 10px 14px; }
721 .proto-method {
722 font-family: var(--font-mono);
723 font-size: 12px;
724 color: var(--accent2);
725 font-weight: 600;
726 border-right: 1px solid var(--border);
727 }
728 .proto-sig {
729 font-family: var(--font-mono);
730 font-size: 11px;
731 color: var(--text-mute);
732 border-right: 1px solid var(--border);
733 word-break: break-all;
734 }
735 .proto-desc { font-size: 12px; color: var(--text-mute); }
736 .proto-row.header .proto-method,
737 .proto-row.header .proto-sig,
738 .proto-row.header .proto-desc {
739 font-family: var(--font-ui);
740 font-size: 11px;
741 font-weight: 700;
742 text-transform: uppercase;
743 letter-spacing: 0.6px;
744 color: var(--text-dim);
745 }
746
747 /* ---- Footer ---- */
748 footer {
749 background: var(--bg);
750 border-top: 1px solid var(--border);
751 padding: 16px 40px;
752 display: flex;
753 justify-content: space-between;
754 align-items: center;
755 font-size: 12px;
756 color: var(--text-dim);
757 }
758 footer a { color: var(--accent2); text-decoration: none; }
759 footer a:hover { text-decoration: underline; }
760
761 /* ---- Scrollbar ---- */
762 ::-webkit-scrollbar { width: 6px; height: 6px; }
763 ::-webkit-scrollbar-track { background: var(--bg); }
764 ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
765 ::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
766
767 /* ---- Tooltip ---- */
768 .tooltip {
769 position: fixed;
770 background: var(--bg2);
771 border: 1px solid var(--border);
772 border-radius: var(--radius);
773 padding: 10px 14px;
774 font-size: 12px;
775 pointer-events: none;
776 opacity: 0;
777 transition: opacity 0.15s;
778 z-index: 100;
779 max-width: 280px;
780 box-shadow: 0 8px 24px rgba(0,0,0,0.4);
781 }
782 .tooltip.visible { opacity: 1; }
783 .tip-id { font-family: var(--font-mono); font-size: 11px; color: var(--accent2); margin-bottom: 4px; }
784 .tip-msg { color: var(--text); margin-bottom: 4px; }
785 .tip-branch { font-size: 11px; margin-bottom: 4px; }
786 .tip-files { font-size: 11px; color: var(--text-mute); font-family: var(--font-mono); }
787
788 /* ---- Dimension dots on DAG nodes ---- */
789 .dim-dots { pointer-events: none; }
790
791 /* ---- Dimension State Matrix section ---- */
792 .dim-section {
793 background: var(--bg);
794 border-top: 2px solid var(--border);
795 padding: 28px 40px 32px;
796 }
797 .dim-inner { max-width: 1200px; margin: 0 auto; }
798 .dim-section-header { display:flex; align-items:baseline; gap:14px; margin-bottom:6px; }
799 .dim-section h2 { font-size:16px; font-weight:700; color:var(--text); }
800 .dim-section .dim-tagline { font-size:12px; color:var(--text-mute); }
801 .dim-matrix-wrap { overflow-x:auto; margin-top:18px; padding-bottom:4px; }
802 .dim-matrix { display:table; border-collapse:separate; border-spacing:0; min-width:100%; }
803 .dim-matrix-row { display:table-row; }
804 .dim-label-cell {
805 display:table-cell; padding:6px 14px 6px 0;
806 font-size:11px; font-weight:600; color:var(--text-mute);
807 text-transform:uppercase; letter-spacing:0.6px;
808 white-space:nowrap; vertical-align:middle; min-width:100px;
809 }
810 .dim-label-dot { display:inline-block; width:9px; height:9px; border-radius:50%; margin-right:6px; vertical-align:middle; }
811 .dim-cell { display:table-cell; padding:4px 3px; vertical-align:middle; text-align:center; min-width:46px; }
812 .dim-cell-inner {
813 width:38px; height:28px; border-radius:5px; margin:0 auto;
814 display:flex; align-items:center; justify-content:center;
815 font-size:11px; font-weight:700;
816 transition:transform 0.2s, box-shadow 0.2s;
817 cursor:default;
818 background:var(--bg3); border:1px solid transparent; color:transparent;
819 }
820 .dim-cell-inner.active { border-color:currentColor; }
821 .dim-cell-inner.conflict-dim { box-shadow:0 0 0 2px #f85149; }
822 .dim-cell-inner.col-highlight { transform:scaleY(1.12); box-shadow:0 0 14px 2px rgba(255,255,255,0.12); }
823 .dim-commit-cell {
824 display:table-cell; padding:8px 3px 0; text-align:center;
825 font-size:9px; font-family:var(--font-mono); color:var(--text-dim);
826 vertical-align:top; transition:color 0.2s;
827 }
828 .dim-commit-cell.col-highlight { color:var(--accent2); font-weight:700; }
829 .dim-commit-label { display:table-cell; padding-top:10px; vertical-align:top; }
830 .dim-legend { display:flex; gap:18px; margin-top:18px; flex-wrap:wrap; font-size:11px; color:var(--text-mute); }
831 .dim-legend-item { display:flex; align-items:center; gap:6px; }
832 .dim-legend-swatch { width:22px; height:14px; border-radius:3px; border:1px solid currentColor; display:inline-block; }
833 .dim-conflict-note {
834 margin-top:16px; padding:12px 16px;
835 background:rgba(248,81,73,0.08); border:1px solid rgba(248,81,73,0.25);
836 border-radius:6px; font-size:12px; color:var(--text-mute);
837 }
838 .dim-conflict-note strong { color:var(--red); }
839 .dim-conflict-note em { color:var(--green); font-style:normal; }
840
841 /* ---- Dimension pills in the operation log ---- */
842 .dim-pills { display:flex; flex-wrap:wrap; gap:3px; margin-top:4px; }
843 .dim-pill {
844 display:inline-block; padding:1px 6px; border-radius:10px;
845 font-size:9px; font-weight:700; letter-spacing:0.4px; text-transform:uppercase;
846 border:1px solid currentColor; opacity:0.85;
847 }
848 .dim-pill.conflict-pill { background:rgba(248,81,73,0.2); color:var(--red) !important; }
849 </style>
850 </head>
851 <body>
852
853 <header>
854 <div class="header-top">
855 <h1>muse</h1>
856 <span class="tagline">Tour de Force · domain-agnostic version control for multidimensional state</span>
857 <span class="version-badge">v{{VERSION}} · {{DOMAIN}} domain · {{ELAPSED}}s</span>
858 <a class="header-nav-link" href="domain_registry.html">Domain Registry →</a>
859 </div>
860 <div class="stats-bar">
861 <div class="stat"><span class="stat-num">{{COMMITS}}</span><span class="stat-label">Commits</span></div>
862 <div class="stat-sep">·</div>
863 <div class="stat"><span class="stat-num">{{BRANCHES}}</span><span class="stat-label">Branches</span></div>
864 <div class="stat-sep">·</div>
865 <div class="stat"><span class="stat-num">{{MERGES}}</span><span class="stat-label">Merges</span></div>
866 <div class="stat-sep">·</div>
867 <div class="stat"><span class="stat-num">{{CONFLICTS}}</span><span class="stat-label">Conflicts Resolved</span></div>
868 <div class="stat-sep">·</div>
869 <div class="stat"><span class="stat-num">{{OPS}}</span><span class="stat-label">Operations</span></div>
870 </div>
871 </header>
872
873 <div class="main-container">
874 <div class="dag-panel">
875 <div class="dag-header">
876 <h2>Commit Graph</h2>
877 <div class="controls">
878 <button class="btn primary" id="btn-play">&#9654; Play Tour</button>
879 <button class="btn" id="btn-prev" title="Previous step (←)">&#9664;</button>
880 <button class="btn" id="btn-next" title="Next step (→)">&#9654;</button>
881 <button class="btn" id="btn-reset">&#8635; Reset</button>
882 <span class="step-counter" id="step-counter"></span>
883 </div>
884 </div>
885 <div class="dag-scroll" id="dag-scroll">
886 <svg id="dag-svg"></svg>
887 </div>
888 <div class="branch-legend" id="branch-legend"></div>
889 </div>
890
891 <div class="log-panel">
892 <div class="log-header"><h2>Operation Log</h2></div>
893 <div class="act-jump-bar" id="act-jump-bar"></div>
894 <div class="log-scroll" id="log-scroll">
895 <div id="event-list"></div>
896 </div>
897 </div>
898 </div>
899
900
901 <div class="dim-section">
902 <div class="dim-inner">
903 <div class="dim-section-header">
904 <h2>Dimension State Matrix</h2>
905 <span class="dim-tagline">
906 Unlike Git (binary file conflicts), Muse merges each orthogonal dimension independently —
907 only conflicting dimensions require human resolution.
908 </span>
909 </div>
910 <div class="dim-matrix-wrap">
911 <div class="dim-matrix" id="dim-matrix"></div>
912 </div>
913 <div class="dim-legend">
914 <div class="dim-legend-item"><span class="dim-legend-swatch" style="background:rgba(188,140,255,0.35);color:#bc8cff"></span> Melodic</div>
915 <div class="dim-legend-item"><span class="dim-legend-swatch" style="background:rgba(63,185,80,0.35);color:#3fb950"></span> Rhythmic</div>
916 <div class="dim-legend-item"><span class="dim-legend-swatch" style="background:rgba(88,166,255,0.35);color:#58a6ff"></span> Harmonic</div>
917 <div class="dim-legend-item"><span class="dim-legend-swatch" style="background:rgba(249,168,37,0.35);color:#f9a825"></span> Dynamic</div>
918 <div class="dim-legend-item"><span class="dim-legend-swatch" style="background:rgba(239,83,80,0.35);color:#ef5350"></span> Structural</div>
919 <div class="dim-legend-item" style="margin-left:8px"><span style="display:inline-block;width:22px;height:14px;border-radius:3px;border:2px solid #f85149;vertical-align:middle;margin-right:6px"></span> Conflict (required resolution)</div>
920 <div class="dim-legend-item"><span style="display:inline-block;width:22px;height:14px;border-radius:3px;background:var(--bg3);border:1px solid var(--border);vertical-align:middle;margin-right:6px"></span> Unchanged</div>
921 </div>
922 <div class="dim-conflict-note">
923 <strong>⚡ Merge conflict (shared-state.mid)</strong> — shared-state.mid had both-sides changes in
924 <strong style="color:#ef5350">structural</strong> (manual resolution required).
925 <em>✓ melodic auto-merged from left</em> · <em>✓ harmonic auto-merged from right</em> —
926 only 1 of 5 dimensions conflicted. Git would have flagged the entire file as a conflict.
927 </div>
928 </div>
929 </div>
930
931 <div class="arch-section">
932 <div class="arch-inner">
933 <h2>How Muse Works</h2>
934 <p class="section-intro">
935 Muse is a version control system for <strong>state</strong> — any multidimensional
936 state that can be snapshotted, diffed, and merged. The core engine provides
937 the DAG, content-addressed storage, branching, merging, time-travel, and
938 conflict resolution. A domain plugin implements <strong>6 methods</strong> and
939 gets everything else for free.
940 <br><br>
941 Music is the reference implementation. Genomics sequences, scientific simulation
942 frames, 3D spatial fields, and financial time-series are all the same pattern.
943 </p>
944 <div class="arch-content">
945 {{ARCH_HTML}}
946 </div>
947 </div>
948 </div>
949
950 <div class="domain-section" id="domain-section">
951 <div class="domain-inner">
952 <h2>Domain Plugin Registry</h2>
953 <p class="section-intro">
954 Every domain registered with Muse appears here. Each plugin implements the
955 <strong>six-method MuseDomainPlugin protocol</strong> and gets the full VCS — branching,
956 merging, conflict resolution, time-travel, and diff — for free.
957 Scaffold a new domain with a single command.
958 </p>
959 <div class="domain-grid" id="domain-grid"></div>
960 </div>
961 </div>
962
963 <div class="crdt-section" id="crdt-section">
964 <div class="crdt-inner">
965 <h2>CRDT Primitives</h2>
966 <p class="section-intro">
967 Plugins that implement <strong>CRDTPlugin</strong> get four convergent data structures
968 that merge without coordination. Any two replicas always converge to the same state —
969 no central authority required.
970 </p>
971 <div class="crdt-grid" id="crdt-grid"></div>
972 </div>
973 </div>
974
975 <footer>
976 <span>Generated {{GENERATED_AT}} · {{ELAPSED}}s · {{OPS}} operations</span>
977 <span><a href="https://github.com/cgcardona/muse">github.com/cgcardona/muse</a></span>
978 </footer>
979
980 <div class="tooltip" id="tooltip">
981 <div class="tip-id" id="tip-id"></div>
982 <div class="tip-msg" id="tip-msg"></div>
983 <div class="tip-branch" id="tip-branch"></div>
984 <div class="tip-files" id="tip-files"></div>
985 <div id="tip-dims" style="margin-top:6px;font-size:10px;line-height:1.8"></div>
986 </div>
987
988 {{D3_SCRIPT}}
989
990 <script>
991 /* ===== Embedded tour data ===== */
992 const DATA = {{DATA_JSON}};
993
994 /* ===== Constants ===== */
995 const ROW_H = 62;
996 const COL_W = 90;
997 const PAD = { top: 30, left: 55, right: 160 };
998 const R_NODE = 11;
999 const BRANCH_ORDER = ['main','alpha','beta','gamma','conflict/left','conflict/right'];
1000 const PLAY_INTERVAL_MS = 1200;
1001
1002 /* ===== Dimension data ===== */
1003 const DIM_COLORS = {
1004 melodic: '#bc8cff',
1005 rhythmic: '#3fb950',
1006 harmonic: '#58a6ff',
1007 dynamic: '#f9a825',
1008 structural: '#ef5350',
1009 };
1010 const DIMS = ['melodic','rhythmic','harmonic','dynamic','structural'];
1011
1012 // Commit message → dimension mapping (stable across re-runs, independent of hash)
1013 function getDims(commit) {
1014 const m = (commit.message || '').toLowerCase();
1015 if (m.includes('root') || m.includes('initial state'))
1016 return ['melodic','rhythmic','harmonic','dynamic','structural'];
1017 if (m.includes('layer 1') || m.includes('rhythmic dimension'))
1018 return ['rhythmic','structural'];
1019 if (m.includes('layer 2') || m.includes('harmonic dimension'))
1020 return ['harmonic','structural'];
1021 if (m.includes('texture pattern a') || m.includes('sparse'))
1022 return ['melodic','rhythmic'];
1023 if (m.includes('texture pattern b') || m.includes('dense'))
1024 return ['melodic','dynamic'];
1025 if (m.includes('syncopated'))
1026 return ['rhythmic','dynamic'];
1027 if (m.includes('descending'))
1028 return ['melodic','harmonic'];
1029 if (m.includes('ascending'))
1030 return ['melodic'];
1031 if (m.includes("merge branch 'beta'"))
1032 return ['rhythmic','dynamic'];
1033 if (m.includes('left:') || m.includes('version a'))
1034 return ['melodic','structural'];
1035 if (m.includes('right:') || m.includes('version b'))
1036 return ['harmonic','structural'];
1037 if (m.includes('resolve') || m.includes('reconciled'))
1038 return ['structural'];
1039 if (m.includes('cherry-pick') || m.includes('cherry pick'))
1040 return ['melodic'];
1041 if (m.includes('revert'))
1042 return ['melodic'];
1043 return [];
1044 }
1045
1046 function getConflicts(commit) {
1047 const m = (commit.message || '').toLowerCase();
1048 if (m.includes('resolve') && m.includes('reconciled')) return ['structural'];
1049 return [];
1050 }
1051
1052 // Build per-short-ID lookup tables once the DATA is available (populated at init)
1053 const DIM_DATA = {};
1054 const DIM_CONFLICTS = {};
1055 function _initDimMaps() {
1056 DATA.dag.commits.forEach(c => {
1057 DIM_DATA[c.short] = getDims(c);
1058 DIM_CONFLICTS[c.short] = getConflicts(c);
1059 });
1060 // Also key by the short prefix used in events (some may be truncated)
1061 DATA.events.forEach(ev => {
1062 if (ev.commit_id && !DIM_DATA[ev.commit_id]) {
1063 const full = DATA.dag.commits.find(c => c.short.startsWith(ev.commit_id) || ev.commit_id.startsWith(c.short));
1064 if (full) {
1065 DIM_DATA[ev.commit_id] = getDims(full);
1066 DIM_CONFLICTS[ev.commit_id] = getConflicts(full);
1067 }
1068 }
1069 });
1070 }
1071
1072
1073 /* ===== State ===== */
1074 let currentStep = -1;
1075 let isPlaying = false;
1076 let playTimer = null;
1077
1078 /* ===== Utilities ===== */
1079 function escHtml(s) {
1080 return String(s)
1081 .replace(/&/g,'&amp;')
1082 .replace(/</g,'&lt;')
1083 .replace(/>/g,'&gt;')
1084 .replace(/"/g,'&quot;');
1085 }
1086
1087 /* ===== Topological sort ===== */
1088 function topoSort(commits) {
1089 const map = new Map(commits.map(c => [c.id, c]));
1090 const visited = new Set();
1091 const result = [];
1092 function visit(id) {
1093 if (visited.has(id)) return;
1094 visited.add(id);
1095 const c = map.get(id);
1096 if (!c) return;
1097 (c.parents || []).forEach(pid => visit(pid));
1098 result.push(c);
1099 }
1100 commits.forEach(c => visit(c.id));
1101 // Oldest commit at row 0 (top of DAG); newest at the bottom so the DAG
1102 // scrolls down in sync with the operation log during playback.
1103 return result;
1104 }
1105
1106 /* ===== Layout ===== */
1107 function computeLayout(commits) {
1108 const sorted = topoSort(commits);
1109 const branchCols = {};
1110 let nextCol = 0;
1111 // Assign columns in BRANCH_ORDER first, then any extras
1112 BRANCH_ORDER.forEach(b => { branchCols[b] = nextCol++; });
1113 commits.forEach(c => {
1114 if (!(c.branch in branchCols)) branchCols[c.branch] = nextCol++;
1115 });
1116 const numCols = nextCol;
1117 const positions = new Map();
1118 sorted.forEach((c, i) => {
1119 positions.set(c.id, {
1120 x: PAD.left + (branchCols[c.branch] || 0) * COL_W,
1121 y: PAD.top + i * ROW_H,
1122 row: i,
1123 col: branchCols[c.branch] || 0,
1124 });
1125 });
1126 const svgW = PAD.left + numCols * COL_W + PAD.right;
1127 const svgH = PAD.top + sorted.length * ROW_H + PAD.top;
1128 return { sorted, positions, branchCols, svgW, svgH };
1129 }
1130
1131 /* ===== Draw DAG ===== */
1132 function drawDAG() {
1133 const { dag, dag: { commits, branches } } = DATA;
1134 if (!commits.length) return;
1135
1136 const layout = computeLayout(commits);
1137 const { sorted, positions, svgW, svgH } = layout;
1138 const branchColor = new Map(branches.map(b => [b.name, b.color]));
1139 const commitMap = new Map(commits.map(c => [c.id, c]));
1140
1141 const svg = d3.select('#dag-svg')
1142 .attr('width', svgW)
1143 .attr('height', svgH);
1144
1145 // ---- Edges ----
1146 const edgeG = svg.append('g').attr('class', 'edges');
1147 sorted.forEach(commit => {
1148 const pos = positions.get(commit.id);
1149 (commit.parents || []).forEach((pid, pIdx) => {
1150 const ppos = positions.get(pid);
1151 if (!pos || !ppos) return;
1152 const color = pIdx === 0
1153 ? (branchColor.get(commit.branch) || '#555')
1154 : (branchColor.get(commitMap.get(pid)?.branch || '') || '#555');
1155
1156 let pathStr;
1157 if (Math.abs(pos.x - ppos.x) < 4) {
1158 // Same column → straight line
1159 pathStr = `M${pos.x},${pos.y} L${ppos.x},${ppos.y}`;
1160 } else {
1161 // Different columns → S-curve bezier
1162 const mid = (pos.y + ppos.y) / 2;
1163 pathStr = `M${pos.x},${pos.y} C${pos.x},${mid} ${ppos.x},${mid} ${ppos.x},${ppos.y}`;
1164 }
1165 edgeG.append('path')
1166 .attr('d', pathStr)
1167 .attr('stroke', color)
1168 .attr('stroke-width', 1.8)
1169 .attr('fill', 'none')
1170 .attr('opacity', 0.45)
1171 .attr('class', `edge-from-${commit.id.slice(0,8)}`);
1172 });
1173 });
1174
1175 // ---- Nodes ----
1176 const nodeG = svg.append('g').attr('class', 'nodes');
1177 const tooltip = document.getElementById('tooltip');
1178
1179 sorted.forEach(commit => {
1180 const pos = positions.get(commit.id);
1181 if (!pos) return;
1182 const color = branchColor.get(commit.branch) || '#78909c';
1183 const isMerge = (commit.parents || []).length >= 2;
1184
1185 const g = nodeG.append('g')
1186 .attr('class', 'commit-node')
1187 .attr('data-id', commit.id)
1188 .attr('data-short', commit.short)
1189 .attr('transform', `translate(${pos.x},${pos.y})`);
1190
1191 if (isMerge) {
1192 g.append('circle')
1193 .attr('r', R_NODE + 6)
1194 .attr('fill', 'none')
1195 .attr('stroke', color)
1196 .attr('stroke-width', 1.5)
1197 .attr('opacity', 0.35);
1198 }
1199
1200 g.append('circle')
1201 .attr('r', R_NODE)
1202 .attr('fill', color)
1203 .attr('stroke', '#0d1117')
1204 .attr('stroke-width', 2);
1205
1206 // Short ID
1207 g.append('text')
1208 .attr('x', R_NODE + 7)
1209 .attr('y', 0)
1210 .attr('dy', '0.35em')
1211 .attr('class', 'commit-label')
1212 .text(commit.short);
1213
1214 // Message (truncated)
1215 const maxLen = 38;
1216 const msg = commit.message.length > maxLen
1217 ? commit.message.slice(0, maxLen) + '…'
1218 : commit.message;
1219 g.append('text')
1220 .attr('x', R_NODE + 7)
1221 .attr('y', 13)
1222 .attr('class', 'commit-msg')
1223 .text(msg);
1224
1225
1226 // Dimension dots below node
1227 const dims = DIM_DATA[commit.short] || [];
1228 if (dims.length > 0) {
1229 const dotR = 4, dotSp = 11;
1230 const totalW = (DIMS.length - 1) * dotSp;
1231 const dotsG = g.append('g')
1232 .attr('class', 'dim-dots')
1233 .attr('transform', `translate(${-totalW/2},${R_NODE + 9})`);
1234 DIMS.forEach((dim, di) => {
1235 const active = dims.includes(dim);
1236 const isConf = (DIM_CONFLICTS[commit.short] || []).includes(dim);
1237 dotsG.append('circle')
1238 .attr('cx', di * dotSp).attr('cy', 0).attr('r', dotR)
1239 .attr('fill', active ? DIM_COLORS[dim] : '#21262d')
1240 .attr('stroke', isConf ? '#f85149' : (active ? DIM_COLORS[dim] : '#30363d'))
1241 .attr('stroke-width', isConf ? 1.5 : 0.8)
1242 .attr('opacity', active ? 1 : 0.35);
1243 });
1244 }
1245
1246 // Hover tooltip
1247 g.on('mousemove', (event) => {
1248 tooltip.classList.add('visible');
1249 document.getElementById('tip-id').textContent = commit.id;
1250 document.getElementById('tip-msg').textContent = commit.message;
1251 document.getElementById('tip-branch').innerHTML =
1252 `<span style="color:${color}">⬤</span> ${commit.branch}`;
1253 document.getElementById('tip-files').textContent =
1254 commit.files.length
1255 ? commit.files.join('\\n')
1256 : '(empty snapshot)';
1257 const tipDims = DIM_DATA[commit.short] || [];
1258 const tipConf = DIM_CONFLICTS[commit.short] || [];
1259 const tipDimEl = document.getElementById('tip-dims');
1260 if (tipDimEl) {
1261 tipDimEl.innerHTML = tipDims.length
1262 ? tipDims.map(d => {
1263 const c = tipConf.includes(d);
1264 return `<span style="color:${DIM_COLORS[d]};margin-right:6px">● ${d}${c?' ⚡':''}</span>`;
1265 }).join('')
1266 : '';
1267 }
1268 tooltip.style.left = (event.clientX + 12) + 'px';
1269 tooltip.style.top = (event.clientY - 10) + 'px';
1270 }).on('mouseleave', () => {
1271 tooltip.classList.remove('visible');
1272 });
1273 });
1274
1275 // ---- Branch legend ----
1276 const legend = document.getElementById('branch-legend');
1277 DATA.dag.branches.forEach(b => {
1278 const item = document.createElement('div');
1279 item.className = 'legend-item';
1280 item.innerHTML =
1281 `<span class="legend-dot" style="background:${b.color}"></span>` +
1282 `<span>${escHtml(b.name)}</span>`;
1283 legend.appendChild(item);
1284 });
1285 }
1286
1287 /* ===== Act metadata ===== */
1288 const ACT_ICONS = {
1289 1:'🎵', 2:'🌿', 3:'⚡', 4:'🔀', 5:'⏪',
1290 6:'🔬', 7:'🗂️', 8:'⚙️', 9:'🔮'
1291 };
1292 const ACT_COLORS = {
1293 1:'#4f8ef7', 2:'#3fb950', 3:'#f85149', 4:'#ab47bc', 5:'#f9a825',
1294 6:'#26c6da', 7:'#58a6ff', 8:'#ef5350', 9:'#bc8cff'
1295 };
1296 const RICH_ACTS = new Set([6, 7, 8, 9]);
1297 const CRDT_ACT = 9;
1298
1299 /* ===== Act jump navigation ===== */
1300 function buildActJumpBar() {
1301 const bar = document.getElementById('act-jump-bar');
1302 if (!bar) return;
1303
1304 const lbl = document.createElement('span');
1305 lbl.textContent = 'Jump:';
1306 bar.appendChild(lbl);
1307
1308 // Collect unique acts
1309 const acts = [];
1310 let last = -1;
1311 DATA.events.forEach(ev => {
1312 if (ev.act !== last) { acts.push({ num: ev.act, title: ev.act_title }); last = ev.act; }
1313 });
1314
1315 acts.forEach(a => {
1316 const btn = document.createElement('button');
1317 btn.className = 'act-jump-btn';
1318 btn.title = `Jump to Act ${a.num}: ${a.title}`;
1319 const icon = ACT_ICONS[a.num] || '';
1320 btn.innerHTML = `${icon} ${a.num}`;
1321 if (a.num >= 6) btn.style.borderColor = ACT_COLORS[a.num] + '66';
1322 btn.addEventListener('click', () => {
1323 pauseTour();
1324 // Find first event index for this act
1325 const idx = DATA.events.findIndex(ev => ev.act === a.num);
1326 if (idx >= 0) {
1327 // Reveal up to this point
1328 revealStep(idx);
1329 // Scroll the act header into view
1330 const hdr = document.getElementById(`act-hdr-${a.num}`);
1331 if (hdr) hdr.scrollIntoView({ behavior: 'smooth', block: 'start' });
1332 }
1333 });
1334 bar.appendChild(btn);
1335 });
1336
1337 // Reveal All button
1338 const allBtn = document.createElement('button');
1339 allBtn.className = 'act-jump-btn reveal-all';
1340 allBtn.textContent = '✦ Reveal All';
1341 allBtn.title = 'Reveal all 69 events at once';
1342 allBtn.addEventListener('click', () => {
1343 pauseTour();
1344 revealStep(DATA.events.length - 1);
1345 });
1346 bar.appendChild(allBtn);
1347 }
1348
1349 /* ===== Event log ===== */
1350 function buildEventLog() {
1351 const list = document.getElementById('event-list');
1352 let lastAct = -1;
1353
1354 DATA.events.forEach((ev, idx) => {
1355 if (ev.act !== lastAct) {
1356 lastAct = ev.act;
1357
1358 // Act header — always visible (no opacity fade)
1359 const hdr = document.createElement('div');
1360 hdr.className = 'act-header';
1361 hdr.id = `act-hdr-${ev.act}`;
1362 const icon = ACT_ICONS[ev.act] || '';
1363 const col = ACT_COLORS[ev.act] || 'var(--text-dim)';
1364 hdr.innerHTML =
1365 `<span style="color:${col};margin-right:6px">${icon}</span>` +
1366 `Act ${ev.act} <span style="opacity:0.6">—</span> ${ev.act_title}`;
1367 if (ev.act >= 6) {
1368 hdr.style.color = col;
1369 hdr.style.borderTop = `1px solid ${col}33`;
1370 }
1371 list.appendChild(hdr);
1372 }
1373
1374 const isRich = RICH_ACTS.has(ev.act);
1375 const isCrdt = ev.act === CRDT_ACT;
1376 const isCliCmd = ev.cmd.startsWith('muse ') || ev.cmd.startsWith('git ');
1377
1378 const item = document.createElement('div');
1379 item.className = 'event-item' + (isRich ? ' rich-act' : '');
1380 item.id = `ev-${idx}`;
1381
1382 if (ev.exit_code !== 0 && ev.output.toLowerCase().includes('conflict')) {
1383 item.classList.add('failed');
1384 }
1385
1386 // Parse cmd
1387 const parts = ev.cmd.split(' ');
1388 const cmdName = parts.slice(0, 2).join(' ');
1389 const cmdArgs = parts.slice(2).join(' ');
1390
1391 // Output class
1392 let outClass = '';
1393 if (ev.output.toLowerCase().includes('conflict')) outClass = 'conflict';
1394 else if (ev.exit_code === 0 && (ev.commit_id || isRich)) outClass = 'success';
1395
1396 // Line limit: rich acts get 15 lines
1397 const lineLimit = isRich ? 15 : 6;
1398 const outLines = ev.output.split('\\n').slice(0, lineLimit).join('\\n');
1399
1400 // Build cmd line: CRDT/API events don't get the `$` shell prefix
1401 let cmdLine;
1402 if (isCrdt && !isCliCmd) {
1403 const accentCol = ACT_COLORS[CRDT_ACT];
1404 cmdLine =
1405 `<div class="event-cmd">` +
1406 `<span style="color:${accentCol};font-size:10px;margin-right:4px">API</span>` +
1407 `<span class="cmd-name" style="color:${accentCol}">${escHtml(ev.cmd)}</span>` +
1408 `</div>`;
1409 } else {
1410 cmdLine =
1411 `<div class="event-cmd">` +
1412 `<span class="cmd-prefix">$ </span>` +
1413 `<span class="cmd-name">${escHtml(cmdName)}</span>` +
1414 (cmdArgs
1415 ? ` <span class="cmd-args">${escHtml(cmdArgs.slice(0, 80))}${cmdArgs.length > 80 ? '…' : ''}</span>`
1416 : '') +
1417 `</div>`;
1418 }
1419
1420 item.innerHTML =
1421 cmdLine +
1422 (outLines
1423 ? `<div class="event-output ${outClass}">${escHtml(outLines)}</div>`
1424 : '') +
1425 (() => {
1426 if (!ev.commit_id) return '';
1427 const dims = DIM_DATA[ev.commit_id] || [];
1428 const conf = DIM_CONFLICTS[ev.commit_id] || [];
1429 if (!dims.length) return '';
1430 return '<div class="dim-pills">' + dims.map(d => {
1431 const isc = conf.includes(d);
1432 const col = DIM_COLORS[d];
1433 const cls = isc ? 'dim-pill conflict-pill' : 'dim-pill';
1434 const sty = isc ? '' : `color:${col};border-color:${col};background:${col}22`;
1435 return `<span class="${cls}" style="${sty}">${isc ? '⚡ ' : ''}${d}</span>`;
1436 }).join('') + '</div>';
1437 })() +
1438 `<div class="event-meta">` +
1439 (ev.commit_id ? `<span class="tag-commit">${escHtml(ev.commit_id)}</span>` : '') +
1440 `<span class="tag-time">${ev.duration_ms}ms</span>` +
1441 `</div>`;
1442
1443 list.appendChild(item);
1444 });
1445 }
1446
1447 /* ===== Domain Dashboard section ===== */
1448 function buildDomainSection() {
1449 const grid = document.getElementById('domain-grid');
1450 if (!grid) return;
1451
1452 // Extract domain data from the domains_json event (act 7, op domains_json)
1453 const domEv = DATA.events.find(e => e.op === 'domains_json');
1454 let domains = [];
1455 if (domEv) {
1456 try { domains = JSON.parse(domEv.output); } catch(_) {}
1457 }
1458
1459 const capClass = cap => {
1460 if (cap === 'CRDT') return 'cap-pill cap-crdt';
1461 if (cap === 'OT Merge') return 'cap-pill cap-ot';
1462 if (cap === 'Domain Schema')return 'cap-pill cap-schema';
1463 if (cap === 'Typed Deltas') return 'cap-pill cap-delta';
1464 return 'cap-pill';
1465 };
1466
1467 domains.forEach(d => {
1468 const isActive = d.active === 'true';
1469 const isScaffold = d.domain === 'scaffold';
1470 const dims = (d.schema && d.schema.dimensions) ? d.schema.dimensions : [];
1471 const desc = (d.schema && d.schema.description) ? d.schema.description : '';
1472
1473 const card = document.createElement('div');
1474 card.className = 'domain-card' +
1475 (isActive ? ' active-domain' : '') +
1476 (isScaffold ? ' scaffold-domain' : '');
1477
1478 const caps = (d.capabilities || [])
1479 .map(c => `<span class="${capClass(c)}">${escHtml(c)}</span>`).join('');
1480
1481 const dimList = dims.map(dim =>
1482 `<span style="color:var(--text-mute)">${escHtml(dim.name)}</span>`
1483 ).join(' · ') || '—';
1484
1485 card.innerHTML =
1486 `<div class="domain-card-header">` +
1487 `<span class="domain-badge${isActive ? ' active' : ''}">${isActive ? '● active' : '○ registered'}</span>` +
1488 `<span class="domain-name">${escHtml(d.domain)}</span>` +
1489 (isActive ? '<span class="domain-active-dot"></span>' : '') +
1490 `</div>` +
1491 `<div class="domain-card-body">` +
1492 `<div class="domain-desc">${escHtml(desc.slice(0, 120))}${desc.length > 120 ? '…' : ''}</div>` +
1493 `<div class="domain-caps">${caps}</div>` +
1494 `<div class="domain-dims"><strong>Dimensions:</strong> ${dimList}</div>` +
1495 `</div>`;
1496
1497 grid.appendChild(card);
1498 });
1499
1500 // "Scaffold your own" card
1501 const newCard = document.createElement('div');
1502 newCard.className = 'domain-new-card';
1503 newCard.innerHTML =
1504 `<div class="domain-new-icon">+</div>` +
1505 `<div class="domain-new-title">Scaffold a new domain</div>` +
1506 `<div class="domain-new-cmd">muse domains --new &lt;name&gt;</div>` +
1507 `<div class="domain-new-link">` +
1508 `then implement 6 methods · ` +
1509 `<a href="docs/guide/plugin-authoring-guide.md">plugin authoring guide →</a>` +
1510 `</div>`;
1511 grid.appendChild(newCard);
1512 }
1513
1514 /* ===== CRDT Primitives section ===== */
1515 function buildCRDTSection() {
1516 const grid = document.getElementById('crdt-grid');
1517 if (!grid) return;
1518
1519 const crdtEvents = DATA.events.filter(e => e.act === CRDT_ACT);
1520
1521 const meta = [
1522 { type:'ORSet', sub:'Observed-Remove Set', color:'#bc8cff', icon:'∪' },
1523 { type:'LWWRegister', sub:'Last-Write-Wins Register', color:'#58a6ff', icon:'✎' },
1524 { type:'GCounter', sub:'Grow-Only Distributed Counter',color:'#3fb950', icon:'↑' },
1525 { type:'VectorClock', sub:'Causal Ordering', color:'#f9a825', icon:'⊕' },
1526 ];
1527
1528 crdtEvents.forEach((ev, i) => {
1529 const m = meta[i] || { type: ev.op, sub: '', color: '#bc8cff', icon: '◆' };
1530 const card = document.createElement('div');
1531 card.className = 'crdt-card';
1532 card.innerHTML =
1533 `<div class="crdt-card-header" style="background:${m.color}10;border-bottom-color:${m.color}30">` +
1534 `<span class="crdt-type-badge" style="color:${m.color};background:${m.color}15;border-color:${m.color}40">${m.icon} ${m.type}</span>` +
1535 `</div>` +
1536 `<div class="crdt-card-body">` +
1537 `<div style="font-size:12px;color:var(--text-mute);margin-bottom:10px;font-style:italic">${m.sub}</div>` +
1538 `<div class="crdt-output">${escHtml(ev.output)}</div>` +
1539 `</div>`;
1540 grid.appendChild(card);
1541 });
1542 }
1543
1544
1545 /* ===== Dimension Timeline ===== */
1546 function buildDimTimeline() {
1547 const matrix = document.getElementById('dim-matrix');
1548 if (!matrix) return;
1549 const sorted = topoSort(DATA.dag.commits);
1550
1551 // Commit ID header row
1552 const hrow = document.createElement('div');
1553 hrow.className = 'dim-matrix-row';
1554 const sp = document.createElement('div');
1555 sp.className = 'dim-label-cell';
1556 hrow.appendChild(sp);
1557 sorted.forEach(c => {
1558 const cell = document.createElement('div');
1559 cell.className = 'dim-commit-cell';
1560 cell.id = `dim-col-label-${c.short}`;
1561 cell.title = c.message;
1562 cell.textContent = c.short.slice(0,6);
1563 hrow.appendChild(cell);
1564 });
1565 matrix.appendChild(hrow);
1566
1567 // One row per dimension
1568 DIMS.forEach(dim => {
1569 const row = document.createElement('div');
1570 row.className = 'dim-matrix-row';
1571 const lbl = document.createElement('div');
1572 lbl.className = 'dim-label-cell';
1573 const dot = document.createElement('span');
1574 dot.className = 'dim-label-dot';
1575 dot.style.background = DIM_COLORS[dim];
1576 lbl.appendChild(dot);
1577 lbl.appendChild(document.createTextNode(dim.charAt(0).toUpperCase() + dim.slice(1)));
1578 row.appendChild(lbl);
1579
1580 sorted.forEach(c => {
1581 const dims = DIM_DATA[c.short] || [];
1582 const conf = DIM_CONFLICTS[c.short] || [];
1583 const active = dims.includes(dim);
1584 const isConf = conf.includes(dim);
1585 const col = DIM_COLORS[dim];
1586 const cell = document.createElement('div');
1587 cell.className = 'dim-cell';
1588 const inner = document.createElement('div');
1589 inner.className = 'dim-cell-inner' + (active ? ' active' : '') + (isConf ? ' conflict-dim' : '');
1590 inner.id = `dim-cell-${dim}-${c.short}`;
1591 if (active) {
1592 inner.style.background = col + '33';
1593 inner.style.color = col;
1594 inner.textContent = isConf ? '⚡' : '●';
1595 }
1596 cell.appendChild(inner);
1597 row.appendChild(cell);
1598 });
1599 matrix.appendChild(row);
1600 });
1601 }
1602
1603 function highlightDimColumn(shortId) {
1604 document.querySelectorAll('.dim-commit-cell.col-highlight, .dim-cell-inner.col-highlight')
1605 .forEach(el => el.classList.remove('col-highlight'));
1606 if (!shortId) return;
1607 const lbl = document.getElementById(`dim-col-label-${shortId}`);
1608 if (lbl) {
1609 lbl.classList.add('col-highlight');
1610 lbl.scrollIntoView({ behavior:'smooth', block:'nearest', inline:'center' });
1611 }
1612 DIMS.forEach(dim => {
1613 const cell = document.getElementById(`dim-cell-${dim}-${shortId}`);
1614 if (cell) cell.classList.add('col-highlight');
1615 });
1616 }
1617
1618 /* ===== Replay animation ===== */
1619 function revealStep(stepIdx) {
1620 if (stepIdx < 0 || stepIdx >= DATA.events.length) return;
1621
1622 const ev = DATA.events[stepIdx];
1623
1624 // Reveal all events up to this step
1625 for (let i = 0; i <= stepIdx; i++) {
1626 const el = document.getElementById(`ev-${i}`);
1627 if (el) el.classList.add('revealed');
1628 }
1629
1630 // Mark current as active (remove previous)
1631 document.querySelectorAll('.event-item.active').forEach(el => el.classList.remove('active'));
1632 const cur = document.getElementById(`ev-${stepIdx}`);
1633 if (cur) {
1634 cur.classList.add('active');
1635 cur.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1636 }
1637
1638 // Highlight commit node
1639 document.querySelectorAll('.commit-node.highlighted').forEach(el => el.classList.remove('highlighted'));
1640 if (ev.commit_id) {
1641 const node = document.querySelector(`.commit-node[data-short="${ev.commit_id}"]`);
1642 if (node) {
1643 node.classList.add('highlighted');
1644 // Scroll DAG to show the node
1645 const transform = node.getAttribute('transform');
1646 if (transform) {
1647 const m = transform.match(/translate\\(([\\d.]+),([\\d.]+)\\)/);
1648 if (m) {
1649 const scroll = document.getElementById('dag-scroll');
1650 const y = parseFloat(m[2]);
1651 scroll.scrollTo({ top: Math.max(0, y - 200), behavior: 'smooth' });
1652 }
1653 }
1654 }
1655 }
1656
1657 // Highlight dimension matrix column
1658 highlightDimColumn(ev.commit_id || null);
1659
1660 // Update counter and step button states
1661 document.getElementById('step-counter').textContent =
1662 `Step ${stepIdx + 1} / ${DATA.events.length}`;
1663 document.getElementById('btn-prev').disabled = (stepIdx === 0);
1664 document.getElementById('btn-next').disabled = (stepIdx === DATA.events.length - 1);
1665
1666 currentStep = stepIdx;
1667 }
1668
1669 function playTour() {
1670 if (isPlaying) return;
1671 isPlaying = true;
1672 document.getElementById('btn-play').textContent = '⏸ Pause';
1673
1674 function advance() {
1675 if (!isPlaying) return;
1676 const next = currentStep + 1;
1677 if (next >= DATA.events.length) {
1678 pauseTour();
1679 document.getElementById('btn-play').textContent = '✓ Done';
1680 return;
1681 }
1682 revealStep(next);
1683 playTimer = setTimeout(advance, PLAY_INTERVAL_MS);
1684 }
1685 advance();
1686 }
1687
1688 function pauseTour() {
1689 isPlaying = false;
1690 clearTimeout(playTimer);
1691 document.getElementById('btn-play').textContent = '▶ Play Tour';
1692 highlightDimColumn(null);
1693 }
1694
1695 function resetTour() {
1696 pauseTour();
1697 currentStep = -1;
1698 document.querySelectorAll('.event-item').forEach(el => {
1699 el.classList.remove('revealed','active');
1700 });
1701 document.querySelectorAll('.commit-node.highlighted').forEach(el => {
1702 el.classList.remove('highlighted');
1703 });
1704 document.getElementById('step-counter').textContent = '';
1705 document.getElementById('log-scroll').scrollTop = 0;
1706 document.getElementById('dag-scroll').scrollTop = 0;
1707 document.getElementById('btn-play').textContent = '▶ Play Tour';
1708 document.getElementById('btn-prev').disabled = true;
1709 document.getElementById('btn-next').disabled = false;
1710 highlightDimColumn(null);
1711 }
1712
1713 /* ===== Init ===== */
1714 document.addEventListener('DOMContentLoaded', () => {
1715 _initDimMaps();
1716 drawDAG();
1717 buildEventLog();
1718 buildActJumpBar();
1719 buildDimTimeline();
1720 buildDomainSection();
1721 buildCRDTSection();
1722
1723 document.getElementById('btn-prev').disabled = true; // nothing to go back to yet
1724
1725 document.getElementById('btn-play').addEventListener('click', () => {
1726 if (isPlaying) pauseTour(); else playTour();
1727 });
1728 document.getElementById('btn-prev').addEventListener('click', () => {
1729 pauseTour();
1730 if (currentStep > 0) revealStep(currentStep - 1);
1731 });
1732 document.getElementById('btn-next').addEventListener('click', () => {
1733 pauseTour();
1734 if (currentStep < DATA.events.length - 1) revealStep(currentStep + 1);
1735 });
1736 document.getElementById('btn-reset').addEventListener('click', resetTour);
1737
1738 // Keyboard shortcuts: ← → for step, Space for play/pause
1739 document.addEventListener('keydown', (e) => {
1740 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
1741 if (e.key === 'ArrowLeft') {
1742 e.preventDefault();
1743 pauseTour();
1744 if (currentStep > 0) revealStep(currentStep - 1);
1745 } else if (e.key === 'ArrowRight') {
1746 e.preventDefault();
1747 pauseTour();
1748 if (currentStep < DATA.events.length - 1) revealStep(currentStep + 1);
1749 } else if (e.key === ' ') {
1750 e.preventDefault();
1751 if (isPlaying) pauseTour(); else playTour();
1752 }
1753 });
1754 });
1755 </script>
1756 </body>
1757 </html>
1758 """
1759
1760
1761 # ---------------------------------------------------------------------------
1762 # Main render function
1763 # ---------------------------------------------------------------------------
1764
1765
1766 def render(tour: dict, output_path: pathlib.Path) -> None:
1767 """Render the tour data into a self-contained HTML file."""
1768 print(" Rendering HTML visualization...")
1769 d3_script = _fetch_d3()
1770
1771 meta = tour.get("meta", {})
1772 stats = tour.get("stats", {})
1773
1774 # Format generated_at nicely
1775 gen_raw = meta.get("generated_at", "")
1776 try:
1777 from datetime import datetime, timezone
1778 dt = datetime.fromisoformat(gen_raw).astimezone(timezone.utc)
1779 gen_str = dt.strftime("%Y-%m-%d %H:%M UTC")
1780 except Exception:
1781 gen_str = gen_raw[:19]
1782
1783 html = _HTML_TEMPLATE
1784 html = html.replace("{{VERSION}}", str(meta.get("muse_version", "0.1.1")))
1785 html = html.replace("{{DOMAIN}}", str(meta.get("domain", "music")))
1786 html = html.replace("{{ELAPSED}}", str(meta.get("elapsed_s", "?")))
1787 html = html.replace("{{GENERATED_AT}}", gen_str)
1788 html = html.replace("{{COMMITS}}", str(stats.get("commits", 0)))
1789 html = html.replace("{{BRANCHES}}", str(stats.get("branches", 0)))
1790 html = html.replace("{{MERGES}}", str(stats.get("merges", 0)))
1791 html = html.replace("{{CONFLICTS}}", str(stats.get("conflicts_resolved", 0)))
1792 html = html.replace("{{OPS}}", str(stats.get("operations", 0)))
1793 html = html.replace("{{ARCH_HTML}}", _ARCH_HTML)
1794 html = html.replace("{{D3_SCRIPT}}", d3_script)
1795 html = html.replace("{{DATA_JSON}}", json.dumps(tour, separators=(",", ":")))
1796
1797 output_path.write_text(html, encoding="utf-8")
1798 size_kb = output_path.stat().st_size // 1024
1799 print(f" HTML written ({size_kb}KB) → {output_path}")
1800
1801
1802 # ---------------------------------------------------------------------------
1803 # Stand-alone entry point
1804 # ---------------------------------------------------------------------------
1805
1806 if __name__ == "__main__":
1807 import argparse
1808 parser = argparse.ArgumentParser(description="Render tour_de_force.json → HTML")
1809 parser.add_argument("json_file", help="Path to tour_de_force.json")
1810 parser.add_argument("--out", default=None, help="Output HTML path")
1811 args = parser.parse_args()
1812
1813 json_path = pathlib.Path(args.json_file)
1814 if not json_path.exists():
1815 print(f"❌ File not found: {json_path}", file=sys.stderr)
1816 sys.exit(1)
1817
1818 data = json.loads(json_path.read_text())
1819 out_path = pathlib.Path(args.out) if args.out else json_path.with_suffix(".html")
1820 render(data, out_path)
1821 print(f"Open: file://{out_path.resolve()}")