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