cgcardona / muse public
render_html.py python
1734 lines 58.2 KB
c338c089 fix: remove Domain Registry link from demo page header Gabriel Cardona <gabriel@tellurstori.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 — Demo</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 /* ---- Registry callout ---- */
462 .registry-callout {
463 background: var(--bg2);
464 border-top: 1px solid var(--border);
465 padding: 40px;
466 }
467 .registry-callout-inner {
468 max-width: 1100px;
469 margin: 0 auto;
470 display: flex;
471 align-items: center;
472 gap: 32px;
473 flex-wrap: wrap;
474 }
475 .registry-callout-text { flex: 1; min-width: 200px; }
476 .registry-callout-title {
477 font-size: 16px;
478 font-weight: 700;
479 color: var(--text);
480 margin-bottom: 6px;
481 }
482 .registry-callout-sub {
483 font-size: 13px;
484 color: var(--text-mute);
485 line-height: 1.6;
486 }
487 .registry-callout-btn {
488 flex-shrink: 0;
489 display: inline-block;
490 padding: 10px 22px;
491 background: var(--accent);
492 color: #fff;
493 font-size: 13px;
494 font-weight: 600;
495 border-radius: var(--radius);
496 text-decoration: none;
497 transition: opacity 0.15s;
498 }
499 .registry-callout-btn:hover { opacity: 0.85; }
500
501 /* ---- Domain Dashboard section ---- */
502 .domain-section {
503 background: var(--bg);
504 border-top: 1px solid var(--border);
505 padding: 60px 40px;
506 }
507 .domain-inner { max-width: 1100px; margin: 0 auto; }
508 .domain-section h2, .crdt-section h2 {
509 font-size: 22px;
510 font-weight: 700;
511 margin-bottom: 8px;
512 color: var(--text);
513 }
514 .domain-section .section-intro, .crdt-section .section-intro {
515 color: var(--text-mute);
516 max-width: 680px;
517 margin-bottom: 36px;
518 line-height: 1.7;
519 }
520 .domain-section .section-intro strong, .crdt-section .section-intro strong { color: var(--text); }
521 .domain-grid {
522 display: grid;
523 grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
524 gap: 20px;
525 }
526 .domain-card {
527 border: 1px solid var(--border);
528 border-radius: var(--radius);
529 background: var(--bg2);
530 overflow: hidden;
531 transition: border-color 0.2s;
532 }
533 .domain-card:hover { border-color: var(--accent); }
534 .domain-card.active-domain { border-color: rgba(249,168,37,0.5); }
535 .domain-card.scaffold-domain { border-style: dashed; opacity: 0.85; }
536 .domain-card-header {
537 padding: 14px 16px;
538 border-bottom: 1px solid var(--border);
539 display: flex;
540 align-items: center;
541 gap: 10px;
542 background: var(--bg3);
543 }
544 .domain-badge {
545 font-family: var(--font-mono);
546 font-size: 11px;
547 padding: 2px 8px;
548 border-radius: 4px;
549 background: rgba(79,142,247,0.12);
550 border: 1px solid rgba(79,142,247,0.3);
551 color: var(--accent2);
552 }
553 .domain-badge.active { background: rgba(249,168,37,0.12); border-color: rgba(249,168,37,0.4); color: #f9a825; }
554 .domain-name {
555 font-weight: 700;
556 font-size: 15px;
557 font-family: var(--font-mono);
558 color: var(--text);
559 }
560 .domain-active-dot {
561 margin-left: auto;
562 width: 8px;
563 height: 8px;
564 border-radius: 50%;
565 background: var(--green);
566 }
567 .domain-card-body { padding: 14px 16px; }
568 .domain-desc {
569 font-size: 13px;
570 color: var(--text-mute);
571 margin-bottom: 12px;
572 line-height: 1.5;
573 }
574 .domain-caps {
575 display: flex;
576 flex-wrap: wrap;
577 gap: 6px;
578 margin-bottom: 12px;
579 }
580 .cap-pill {
581 font-size: 10px;
582 padding: 2px 8px;
583 border-radius: 12px;
584 border: 1px solid var(--border);
585 color: var(--text-mute);
586 background: var(--bg3);
587 }
588 .cap-pill.cap-crdt { border-color: rgba(188,140,255,0.4); color: var(--purple); background: rgba(188,140,255,0.08); }
589 .cap-pill.cap-ot { border-color: rgba(88,166,255,0.4); color: var(--accent2); background: rgba(88,166,255,0.08); }
590 .cap-pill.cap-schema { border-color: rgba(63,185,80,0.4); color: var(--green); background: rgba(63,185,80,0.08); }
591 .cap-pill.cap-delta { border-color: rgba(249,168,37,0.4); color: #f9a825; background: rgba(249,168,37,0.08); }
592 .domain-dims {
593 font-size: 11px;
594 color: var(--text-dim);
595 }
596 .domain-dims strong { color: var(--text-mute); }
597 .domain-new-card {
598 border: 2px dashed var(--border);
599 border-radius: var(--radius);
600 background: transparent;
601 display: flex;
602 flex-direction: column;
603 align-items: center;
604 justify-content: center;
605 padding: 32px 20px;
606 text-align: center;
607 gap: 12px;
608 transition: border-color 0.2s;
609 cursor: default;
610 }
611 .domain-new-card:hover { border-color: var(--accent); }
612 .domain-new-icon { font-size: 28px; color: var(--text-dim); }
613 .domain-new-title { font-size: 14px; font-weight: 600; color: var(--text-mute); }
614 .domain-new-cmd {
615 font-family: var(--font-mono);
616 font-size: 12px;
617 background: var(--bg3);
618 border: 1px solid var(--border);
619 border-radius: 4px;
620 padding: 6px 12px;
621 color: var(--accent2);
622 }
623 .domain-new-link {
624 font-size: 11px;
625 color: var(--text-dim);
626 }
627 .domain-new-link a { color: var(--accent); text-decoration: none; }
628 .domain-new-link a:hover { text-decoration: underline; }
629
630 /* ---- CRDT Primitives section ---- */
631 .crdt-section {
632 background: var(--bg2);
633 border-top: 1px solid var(--border);
634 padding: 60px 40px;
635 }
636 .crdt-inner { max-width: 1100px; margin: 0 auto; }
637 .crdt-grid {
638 display: grid;
639 grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
640 gap: 20px;
641 }
642 .crdt-card {
643 border: 1px solid var(--border);
644 border-radius: var(--radius);
645 background: var(--bg);
646 overflow: hidden;
647 transition: border-color 0.2s;
648 }
649 .crdt-card:hover { border-color: var(--purple); }
650 .crdt-card-header {
651 padding: 12px 16px;
652 border-bottom: 1px solid var(--border);
653 background: rgba(188,140,255,0.06);
654 display: flex;
655 align-items: center;
656 gap: 8px;
657 }
658 .crdt-type-badge {
659 font-family: var(--font-mono);
660 font-size: 11px;
661 padding: 2px 8px;
662 border-radius: 4px;
663 background: rgba(188,140,255,0.12);
664 border: 1px solid rgba(188,140,255,0.3);
665 color: var(--purple);
666 }
667 .crdt-card-title { font-weight: 700; font-size: 14px; color: var(--text); }
668 .crdt-card-sub { font-size: 11px; color: var(--text-mute); }
669 .crdt-card-body { padding: 14px 16px; }
670 .crdt-output {
671 font-family: var(--font-mono);
672 font-size: 11px;
673 color: var(--text-mute);
674 white-space: pre-wrap;
675 line-height: 1.6;
676 background: var(--bg3);
677 border: 1px solid var(--border);
678 border-radius: 4px;
679 padding: 10px 12px;
680 }
681 .crdt-output .out-win { color: var(--green); }
682 .crdt-output .out-key { color: var(--accent2); }
683
684 /* ---- Architecture section ---- */
685 .arch-section {
686 background: var(--bg2);
687 border-top: 1px solid var(--border);
688 padding: 48px 40px;
689 }
690 .arch-inner { max-width: 1100px; margin: 0 auto; }
691 .arch-section h2 {
692 font-size: 22px;
693 font-weight: 700;
694 margin-bottom: 8px;
695 color: var(--text);
696 }
697 .arch-section .section-intro {
698 color: var(--text-mute);
699 max-width: 680px;
700 margin-bottom: 40px;
701 line-height: 1.7;
702 }
703 .arch-section .section-intro strong { color: var(--text); }
704 .arch-content {
705 display: grid;
706 grid-template-columns: 380px 1fr;
707 gap: 48px;
708 align-items: start;
709 }
710
711 /* Architecture flow diagram */
712 .arch-flow {
713 display: flex;
714 flex-direction: column;
715 align-items: center;
716 gap: 0;
717 }
718 .arch-row { width: 100%; display: flex; justify-content: center; }
719 .plugins-row { gap: 8px; flex-wrap: wrap; }
720 .arch-box {
721 border: 1px solid var(--border);
722 border-radius: var(--radius);
723 padding: 12px 16px;
724 background: var(--bg3);
725 width: 100%;
726 max-width: 340px;
727 transition: border-color 0.2s;
728 }
729 .arch-box:hover { border-color: var(--accent); }
730 .arch-box.cli { border-color: rgba(79,142,247,0.4); }
731 .arch-box.registry { border-color: rgba(188,140,255,0.3); }
732 .arch-box.core { border-color: rgba(63,185,80,0.3); background: rgba(63,185,80,0.05); }
733 .arch-box.protocol { border-color: rgba(79,142,247,0.5); background: rgba(79,142,247,0.05); }
734 .arch-box.plugin { max-width: 160px; width: auto; flex: 1; }
735 .arch-box.plugin.active { border-color: rgba(249,168,37,0.5); background: rgba(249,168,37,0.05); }
736 .arch-box.plugin.planned { opacity: 0.6; border-style: dashed; }
737 .box-title { font-weight: 600; font-size: 13px; color: var(--text); }
738 .box-sub { font-size: 11px; color: var(--text-mute); margin-top: 3px; }
739 .box-detail { font-size: 10px; color: var(--text-dim); margin-top: 4px; line-height: 1.5; }
740 .arch-connector {
741 display: flex;
742 flex-direction: column;
743 align-items: center;
744 height: 24px;
745 color: var(--border);
746 }
747 .connector-line { width: 1px; flex: 1; background: var(--border); }
748 .connector-arrow { font-size: 10px; }
749
750 /* Protocol table */
751 .protocol-table { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
752 .proto-row {
753 display: grid;
754 grid-template-columns: 80px 220px 1fr;
755 gap: 0;
756 border-bottom: 1px solid var(--border);
757 }
758 .proto-row:last-child { border-bottom: none; }
759 .proto-row.header { background: var(--bg3); }
760 .proto-row > div { padding: 10px 14px; }
761 .proto-method {
762 font-family: var(--font-mono);
763 font-size: 12px;
764 color: var(--accent2);
765 font-weight: 600;
766 border-right: 1px solid var(--border);
767 }
768 .proto-sig {
769 font-family: var(--font-mono);
770 font-size: 11px;
771 color: var(--text-mute);
772 border-right: 1px solid var(--border);
773 word-break: break-all;
774 }
775 .proto-desc { font-size: 12px; color: var(--text-mute); }
776 .proto-row.header .proto-method,
777 .proto-row.header .proto-sig,
778 .proto-row.header .proto-desc {
779 font-family: var(--font-ui);
780 font-size: 11px;
781 font-weight: 700;
782 text-transform: uppercase;
783 letter-spacing: 0.6px;
784 color: var(--text-dim);
785 }
786
787 /* ---- Footer ---- */
788 footer {
789 background: var(--bg);
790 border-top: 1px solid var(--border);
791 padding: 16px 40px;
792 display: flex;
793 justify-content: space-between;
794 align-items: center;
795 font-size: 12px;
796 color: var(--text-dim);
797 }
798 footer a { color: var(--accent2); text-decoration: none; }
799 footer a:hover { text-decoration: underline; }
800
801 /* ---- Scrollbar ---- */
802 ::-webkit-scrollbar { width: 6px; height: 6px; }
803 ::-webkit-scrollbar-track { background: var(--bg); }
804 ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
805 ::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
806
807 /* ---- Tooltip ---- */
808 .tooltip {
809 position: fixed;
810 background: var(--bg2);
811 border: 1px solid var(--border);
812 border-radius: var(--radius);
813 padding: 10px 14px;
814 font-size: 12px;
815 pointer-events: none;
816 opacity: 0;
817 transition: opacity 0.15s;
818 z-index: 100;
819 max-width: 280px;
820 box-shadow: 0 8px 24px rgba(0,0,0,0.4);
821 }
822 .tooltip.visible { opacity: 1; }
823 .tip-id { font-family: var(--font-mono); font-size: 11px; color: var(--accent2); margin-bottom: 4px; }
824 .tip-msg { color: var(--text); margin-bottom: 4px; }
825 .tip-branch { font-size: 11px; margin-bottom: 4px; }
826 .tip-files { font-size: 11px; color: var(--text-mute); font-family: var(--font-mono); }
827
828 /* ---- Dimension dots on DAG nodes ---- */
829 .dim-dots { pointer-events: none; }
830
831 /* ---- Dimension State Matrix section ---- */
832 .dim-section {
833 background: var(--bg);
834 border-top: 2px solid var(--border);
835 padding: 28px 40px 32px;
836 }
837 .dim-inner { max-width: 1200px; margin: 0 auto; }
838 .dim-section-header { display:flex; align-items:baseline; gap:14px; margin-bottom:6px; }
839 .dim-section h2 { font-size:16px; font-weight:700; color:var(--text); }
840 .dim-section .dim-tagline { font-size:12px; color:var(--text-mute); }
841 .dim-matrix-wrap { overflow-x:auto; margin-top:18px; padding-bottom:4px; }
842 .dim-matrix { display:table; border-collapse:separate; border-spacing:0; min-width:100%; }
843 .dim-matrix-row { display:table-row; }
844 .dim-label-cell {
845 display:table-cell; padding:6px 14px 6px 0;
846 font-size:11px; font-weight:600; color:var(--text-mute);
847 text-transform:uppercase; letter-spacing:0.6px;
848 white-space:nowrap; vertical-align:middle; min-width:100px;
849 }
850 .dim-label-dot { display:inline-block; width:9px; height:9px; border-radius:50%; margin-right:6px; vertical-align:middle; }
851 .dim-cell { display:table-cell; padding:4px 3px; vertical-align:middle; text-align:center; min-width:46px; }
852 .dim-cell-inner {
853 width:38px; height:28px; border-radius:5px; margin:0 auto;
854 display:flex; align-items:center; justify-content:center;
855 font-size:11px; font-weight:700;
856 transition:transform 0.2s, box-shadow 0.2s;
857 cursor:default;
858 background:var(--bg3); border:1px solid transparent; color:transparent;
859 }
860 .dim-cell-inner.active { border-color:currentColor; }
861 .dim-cell-inner.conflict-dim { box-shadow:0 0 0 2px #f85149; }
862 .dim-cell-inner.col-highlight { transform:scaleY(1.12); box-shadow:0 0 14px 2px rgba(255,255,255,0.12); }
863 .dim-commit-cell {
864 display:table-cell; padding:8px 3px 0; text-align:center;
865 font-size:9px; font-family:var(--font-mono); color:var(--text-dim);
866 vertical-align:top; transition:color 0.2s;
867 }
868 .dim-commit-cell.col-highlight { color:var(--accent2); font-weight:700; }
869 .dim-commit-label { display:table-cell; padding-top:10px; vertical-align:top; }
870 .dim-legend { display:flex; gap:18px; margin-top:18px; flex-wrap:wrap; font-size:11px; color:var(--text-mute); }
871 .dim-legend-item { display:flex; align-items:center; gap:6px; }
872 .dim-legend-swatch { width:22px; height:14px; border-radius:3px; border:1px solid currentColor; display:inline-block; }
873 .dim-conflict-note {
874 margin-top:16px; padding:12px 16px;
875 background:rgba(248,81,73,0.08); border:1px solid rgba(248,81,73,0.25);
876 border-radius:6px; font-size:12px; color:var(--text-mute);
877 }
878 .dim-conflict-note strong { color:var(--red); }
879 .dim-conflict-note em { color:var(--green); font-style:normal; }
880
881 /* ---- Dimension pills in the operation log ---- */
882 .dim-pills { display:flex; flex-wrap:wrap; gap:3px; margin-top:4px; }
883 .dim-pill {
884 display:inline-block; padding:1px 6px; border-radius:10px;
885 font-size:9px; font-weight:700; letter-spacing:0.4px; text-transform:uppercase;
886 border:1px solid currentColor; opacity:0.85;
887 }
888 .dim-pill.conflict-pill { background:rgba(248,81,73,0.2); color:var(--red) !important; }
889 </style>
890 </head>
891 <body>
892
893 <header>
894 <div class="header-top">
895 <h1>muse</h1>
896 <span class="tagline">Demo · domain-agnostic version control for multidimensional state</span>
897 <span class="version-badge">v{{VERSION}} · {{DOMAIN}} domain · {{ELAPSED}}s</span>
898 </div>
899 <div class="stats-bar">
900 <div class="stat"><span class="stat-num">{{COMMITS}}</span><span class="stat-label">Commits</span></div>
901 <div class="stat-sep">·</div>
902 <div class="stat"><span class="stat-num">{{BRANCHES}}</span><span class="stat-label">Branches</span></div>
903 <div class="stat-sep">·</div>
904 <div class="stat"><span class="stat-num">{{MERGES}}</span><span class="stat-label">Merges</span></div>
905 <div class="stat-sep">·</div>
906 <div class="stat"><span class="stat-num">{{CONFLICTS}}</span><span class="stat-label">Conflicts Resolved</span></div>
907 <div class="stat-sep">·</div>
908 <div class="stat"><span class="stat-num">{{OPS}}</span><span class="stat-label">Operations</span></div>
909 </div>
910 </header>
911
912 <div class="main-container">
913 <div class="dag-panel">
914 <div class="dag-header">
915 <h2>Commit Graph</h2>
916 <div class="controls">
917 <button class="btn primary" id="btn-play">&#9654; Play Tour</button>
918 <button class="btn" id="btn-prev" title="Previous step (←)">&#9664;</button>
919 <button class="btn" id="btn-next" title="Next step (→)">&#9654;</button>
920 <button class="btn" id="btn-reset">&#8635; Reset</button>
921 <span class="step-counter" id="step-counter"></span>
922 </div>
923 </div>
924 <div class="dag-scroll" id="dag-scroll">
925 <svg id="dag-svg"></svg>
926 </div>
927 <div class="branch-legend" id="branch-legend"></div>
928 </div>
929
930 <div class="log-panel">
931 <div class="log-header"><h2>Operation Log</h2></div>
932 <div class="act-jump-bar" id="act-jump-bar"></div>
933 <div class="log-scroll" id="log-scroll">
934 <div id="event-list"></div>
935 </div>
936 </div>
937 </div>
938
939
940 <div class="dim-section">
941 <div class="dim-inner">
942 <div class="dim-section-header">
943 <h2>Dimension State Matrix</h2>
944 <span class="dim-tagline">
945 Unlike Git (binary file conflicts), Muse merges each orthogonal dimension independently —
946 only conflicting dimensions require human resolution.
947 </span>
948 </div>
949 <div class="dim-matrix-wrap">
950 <div class="dim-matrix" id="dim-matrix"></div>
951 </div>
952 <div class="dim-legend">
953 <div class="dim-legend-item"><span class="dim-legend-swatch" style="background:rgba(188,140,255,0.35);color:#bc8cff"></span> Melodic</div>
954 <div class="dim-legend-item"><span class="dim-legend-swatch" style="background:rgba(63,185,80,0.35);color:#3fb950"></span> Rhythmic</div>
955 <div class="dim-legend-item"><span class="dim-legend-swatch" style="background:rgba(88,166,255,0.35);color:#58a6ff"></span> Harmonic</div>
956 <div class="dim-legend-item"><span class="dim-legend-swatch" style="background:rgba(249,168,37,0.35);color:#f9a825"></span> Dynamic</div>
957 <div class="dim-legend-item"><span class="dim-legend-swatch" style="background:rgba(239,83,80,0.35);color:#ef5350"></span> Structural</div>
958 <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>
959 <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>
960 </div>
961 <div class="dim-conflict-note">
962 <strong>⚡ Merge conflict (shared-state.mid)</strong> — shared-state.mid had both-sides changes in
963 <strong style="color:#ef5350">structural</strong> (manual resolution required).
964 <em>✓ melodic auto-merged from left</em> · <em>✓ harmonic auto-merged from right</em> —
965 only 1 of 5 dimensions conflicted. Git would have flagged the entire file as a conflict.
966 </div>
967 </div>
968 </div>
969
970 <div class="arch-section">
971 <div class="arch-inner">
972 <h2>How Muse Works</h2>
973 <p class="section-intro">
974 Muse is a version control system for <strong>state</strong> — any multidimensional
975 state that can be snapshotted, diffed, and merged. The core engine provides
976 the DAG, content-addressed storage, branching, merging, time-travel, and
977 conflict resolution. A domain plugin implements <strong>6 methods</strong> and
978 gets everything else for free.
979 <br><br>
980 Music is the reference implementation. Genomics sequences, scientific simulation
981 frames, 3D spatial fields, and financial time-series are all the same pattern.
982 </p>
983 <div class="arch-content">
984 {{ARCH_HTML}}
985 </div>
986 </div>
987 </div>
988
989 <div class="registry-callout">
990 <div class="registry-callout-inner">
991 <div class="registry-callout-text">
992 <div class="registry-callout-title">Want to version something else?</div>
993 <div class="registry-callout-sub">
994 Music is the reference implementation. The same engine works for genomics,
995 3D spatial fields, financial models, and any multidimensional state —
996 six methods between you and a complete VCS.
997 </div>
998 </div>
999 <a class="registry-callout-btn" href="domain_registry.html">
1000 Domain Registry &amp; Plugin Guide →
1001 </a>
1002 </div>
1003 </div>
1004
1005 <footer>
1006 <span>Generated {{GENERATED_AT}} · {{ELAPSED}}s · {{OPS}} operations</span>
1007 <span><a href="https://github.com/cgcardona/muse">github.com/cgcardona/muse</a></span>
1008 </footer>
1009
1010 <div class="tooltip" id="tooltip">
1011 <div class="tip-id" id="tip-id"></div>
1012 <div class="tip-msg" id="tip-msg"></div>
1013 <div class="tip-branch" id="tip-branch"></div>
1014 <div class="tip-files" id="tip-files"></div>
1015 <div id="tip-dims" style="margin-top:6px;font-size:10px;line-height:1.8"></div>
1016 </div>
1017
1018 {{D3_SCRIPT}}
1019
1020 <script>
1021 /* ===== Embedded tour data ===== */
1022 const DATA = {{DATA_JSON}};
1023
1024 /* ===== Constants ===== */
1025 const ROW_H = 62;
1026 const COL_W = 90;
1027 const PAD = { top: 30, left: 55, right: 160 };
1028 const R_NODE = 11;
1029 const BRANCH_ORDER = ['main','alpha','beta','gamma','conflict/left','conflict/right'];
1030 const PLAY_INTERVAL_MS = 1200;
1031
1032 /* ===== Dimension data ===== */
1033 const DIM_COLORS = {
1034 melodic: '#bc8cff',
1035 rhythmic: '#3fb950',
1036 harmonic: '#58a6ff',
1037 dynamic: '#f9a825',
1038 structural: '#ef5350',
1039 };
1040 const DIMS = ['melodic','rhythmic','harmonic','dynamic','structural'];
1041
1042 // Commit message → dimension mapping (stable across re-runs, independent of hash)
1043 function getDims(commit) {
1044 const m = (commit.message || '').toLowerCase();
1045 if (m.includes('root') || m.includes('initial state'))
1046 return ['melodic','rhythmic','harmonic','dynamic','structural'];
1047 if (m.includes('layer 1') || m.includes('rhythmic dimension'))
1048 return ['rhythmic','structural'];
1049 if (m.includes('layer 2') || m.includes('harmonic dimension'))
1050 return ['harmonic','structural'];
1051 if (m.includes('texture pattern a') || m.includes('sparse'))
1052 return ['melodic','rhythmic'];
1053 if (m.includes('texture pattern b') || m.includes('dense'))
1054 return ['melodic','dynamic'];
1055 if (m.includes('syncopated'))
1056 return ['rhythmic','dynamic'];
1057 if (m.includes('descending'))
1058 return ['melodic','harmonic'];
1059 if (m.includes('ascending'))
1060 return ['melodic'];
1061 if (m.includes("merge branch 'beta'"))
1062 return ['rhythmic','dynamic'];
1063 if (m.includes('left:') || m.includes('version a'))
1064 return ['melodic','structural'];
1065 if (m.includes('right:') || m.includes('version b'))
1066 return ['harmonic','structural'];
1067 if (m.includes('resolve') || m.includes('reconciled'))
1068 return ['structural'];
1069 if (m.includes('cherry-pick') || m.includes('cherry pick'))
1070 return ['melodic'];
1071 if (m.includes('revert'))
1072 return ['melodic'];
1073 return [];
1074 }
1075
1076 function getConflicts(commit) {
1077 const m = (commit.message || '').toLowerCase();
1078 if (m.includes('resolve') && m.includes('reconciled')) return ['structural'];
1079 return [];
1080 }
1081
1082 // Build per-short-ID lookup tables once the DATA is available (populated at init)
1083 const DIM_DATA = {};
1084 const DIM_CONFLICTS = {};
1085 function _initDimMaps() {
1086 DATA.dag.commits.forEach(c => {
1087 DIM_DATA[c.short] = getDims(c);
1088 DIM_CONFLICTS[c.short] = getConflicts(c);
1089 });
1090 // Also key by the short prefix used in events (some may be truncated)
1091 DATA.events.forEach(ev => {
1092 if (ev.commit_id && !DIM_DATA[ev.commit_id]) {
1093 const full = DATA.dag.commits.find(c => c.short.startsWith(ev.commit_id) || ev.commit_id.startsWith(c.short));
1094 if (full) {
1095 DIM_DATA[ev.commit_id] = getDims(full);
1096 DIM_CONFLICTS[ev.commit_id] = getConflicts(full);
1097 }
1098 }
1099 });
1100 }
1101
1102
1103 /* ===== State ===== */
1104 let currentStep = -1;
1105 let isPlaying = false;
1106 let playTimer = null;
1107
1108 /* ===== Utilities ===== */
1109 function escHtml(s) {
1110 return String(s)
1111 .replace(/&/g,'&amp;')
1112 .replace(/</g,'&lt;')
1113 .replace(/>/g,'&gt;')
1114 .replace(/"/g,'&quot;');
1115 }
1116
1117 /* ===== Topological sort ===== */
1118 function topoSort(commits) {
1119 const map = new Map(commits.map(c => [c.id, c]));
1120 const visited = new Set();
1121 const result = [];
1122 function visit(id) {
1123 if (visited.has(id)) return;
1124 visited.add(id);
1125 const c = map.get(id);
1126 if (!c) return;
1127 (c.parents || []).forEach(pid => visit(pid));
1128 result.push(c);
1129 }
1130 commits.forEach(c => visit(c.id));
1131 // Oldest commit at row 0 (top of DAG); newest at the bottom so the DAG
1132 // scrolls down in sync with the operation log during playback.
1133 return result;
1134 }
1135
1136 /* ===== Layout ===== */
1137 function computeLayout(commits) {
1138 const sorted = topoSort(commits);
1139 const branchCols = {};
1140 let nextCol = 0;
1141 // Assign columns in BRANCH_ORDER first, then any extras
1142 BRANCH_ORDER.forEach(b => { branchCols[b] = nextCol++; });
1143 commits.forEach(c => {
1144 if (!(c.branch in branchCols)) branchCols[c.branch] = nextCol++;
1145 });
1146 const numCols = nextCol;
1147 const positions = new Map();
1148 sorted.forEach((c, i) => {
1149 positions.set(c.id, {
1150 x: PAD.left + (branchCols[c.branch] || 0) * COL_W,
1151 y: PAD.top + i * ROW_H,
1152 row: i,
1153 col: branchCols[c.branch] || 0,
1154 });
1155 });
1156 const svgW = PAD.left + numCols * COL_W + PAD.right;
1157 const svgH = PAD.top + sorted.length * ROW_H + PAD.top;
1158 return { sorted, positions, branchCols, svgW, svgH };
1159 }
1160
1161 /* ===== Draw DAG ===== */
1162 function drawDAG() {
1163 const { dag, dag: { commits, branches } } = DATA;
1164 if (!commits.length) return;
1165
1166 const layout = computeLayout(commits);
1167 const { sorted, positions, svgW, svgH } = layout;
1168 const branchColor = new Map(branches.map(b => [b.name, b.color]));
1169 const commitMap = new Map(commits.map(c => [c.id, c]));
1170
1171 const svg = d3.select('#dag-svg')
1172 .attr('width', svgW)
1173 .attr('height', svgH);
1174
1175 // ---- Edges ----
1176 const edgeG = svg.append('g').attr('class', 'edges');
1177 sorted.forEach(commit => {
1178 const pos = positions.get(commit.id);
1179 (commit.parents || []).forEach((pid, pIdx) => {
1180 const ppos = positions.get(pid);
1181 if (!pos || !ppos) return;
1182 const color = pIdx === 0
1183 ? (branchColor.get(commit.branch) || '#555')
1184 : (branchColor.get(commitMap.get(pid)?.branch || '') || '#555');
1185
1186 let pathStr;
1187 if (Math.abs(pos.x - ppos.x) < 4) {
1188 // Same column → straight line
1189 pathStr = `M${pos.x},${pos.y} L${ppos.x},${ppos.y}`;
1190 } else {
1191 // Different columns → S-curve bezier
1192 const mid = (pos.y + ppos.y) / 2;
1193 pathStr = `M${pos.x},${pos.y} C${pos.x},${mid} ${ppos.x},${mid} ${ppos.x},${ppos.y}`;
1194 }
1195 edgeG.append('path')
1196 .attr('d', pathStr)
1197 .attr('stroke', color)
1198 .attr('stroke-width', 1.8)
1199 .attr('fill', 'none')
1200 .attr('opacity', 0.45)
1201 .attr('class', `edge-from-${commit.id.slice(0,8)}`);
1202 });
1203 });
1204
1205 // ---- Nodes ----
1206 const nodeG = svg.append('g').attr('class', 'nodes');
1207 const tooltip = document.getElementById('tooltip');
1208
1209 sorted.forEach(commit => {
1210 const pos = positions.get(commit.id);
1211 if (!pos) return;
1212 const color = branchColor.get(commit.branch) || '#78909c';
1213 const isMerge = (commit.parents || []).length >= 2;
1214
1215 const g = nodeG.append('g')
1216 .attr('class', 'commit-node')
1217 .attr('data-id', commit.id)
1218 .attr('data-short', commit.short)
1219 .attr('transform', `translate(${pos.x},${pos.y})`);
1220
1221 if (isMerge) {
1222 g.append('circle')
1223 .attr('r', R_NODE + 6)
1224 .attr('fill', 'none')
1225 .attr('stroke', color)
1226 .attr('stroke-width', 1.5)
1227 .attr('opacity', 0.35);
1228 }
1229
1230 g.append('circle')
1231 .attr('r', R_NODE)
1232 .attr('fill', color)
1233 .attr('stroke', '#0d1117')
1234 .attr('stroke-width', 2);
1235
1236 // Short ID
1237 g.append('text')
1238 .attr('x', R_NODE + 7)
1239 .attr('y', 0)
1240 .attr('dy', '0.35em')
1241 .attr('class', 'commit-label')
1242 .text(commit.short);
1243
1244 // Message (truncated)
1245 const maxLen = 38;
1246 const msg = commit.message.length > maxLen
1247 ? commit.message.slice(0, maxLen) + '…'
1248 : commit.message;
1249 g.append('text')
1250 .attr('x', R_NODE + 7)
1251 .attr('y', 13)
1252 .attr('class', 'commit-msg')
1253 .text(msg);
1254
1255
1256 // Dimension dots below node
1257 const dims = DIM_DATA[commit.short] || [];
1258 if (dims.length > 0) {
1259 const dotR = 4, dotSp = 11;
1260 const totalW = (DIMS.length - 1) * dotSp;
1261 const dotsG = g.append('g')
1262 .attr('class', 'dim-dots')
1263 .attr('transform', `translate(${-totalW/2},${R_NODE + 9})`);
1264 DIMS.forEach((dim, di) => {
1265 const active = dims.includes(dim);
1266 const isConf = (DIM_CONFLICTS[commit.short] || []).includes(dim);
1267 dotsG.append('circle')
1268 .attr('cx', di * dotSp).attr('cy', 0).attr('r', dotR)
1269 .attr('fill', active ? DIM_COLORS[dim] : '#21262d')
1270 .attr('stroke', isConf ? '#f85149' : (active ? DIM_COLORS[dim] : '#30363d'))
1271 .attr('stroke-width', isConf ? 1.5 : 0.8)
1272 .attr('opacity', active ? 1 : 0.35);
1273 });
1274 }
1275
1276 // Hover tooltip
1277 g.on('mousemove', (event) => {
1278 tooltip.classList.add('visible');
1279 document.getElementById('tip-id').textContent = commit.id;
1280 document.getElementById('tip-msg').textContent = commit.message;
1281 document.getElementById('tip-branch').innerHTML =
1282 `<span style="color:${color}">⬤</span> ${commit.branch}`;
1283 document.getElementById('tip-files').textContent =
1284 commit.files.length
1285 ? commit.files.join('\\n')
1286 : '(empty snapshot)';
1287 const tipDims = DIM_DATA[commit.short] || [];
1288 const tipConf = DIM_CONFLICTS[commit.short] || [];
1289 const tipDimEl = document.getElementById('tip-dims');
1290 if (tipDimEl) {
1291 tipDimEl.innerHTML = tipDims.length
1292 ? tipDims.map(d => {
1293 const c = tipConf.includes(d);
1294 return `<span style="color:${DIM_COLORS[d]};margin-right:6px">● ${d}${c?' ⚡':''}</span>`;
1295 }).join('')
1296 : '';
1297 }
1298 tooltip.style.left = (event.clientX + 12) + 'px';
1299 tooltip.style.top = (event.clientY - 10) + 'px';
1300 }).on('mouseleave', () => {
1301 tooltip.classList.remove('visible');
1302 });
1303 });
1304
1305 // ---- Branch legend ----
1306 const legend = document.getElementById('branch-legend');
1307 DATA.dag.branches.forEach(b => {
1308 const item = document.createElement('div');
1309 item.className = 'legend-item';
1310 item.innerHTML =
1311 `<span class="legend-dot" style="background:${b.color}"></span>` +
1312 `<span>${escHtml(b.name)}</span>`;
1313 legend.appendChild(item);
1314 });
1315 }
1316
1317 /* ===== Act metadata ===== */
1318 const ACT_ICONS = {
1319 1:'🎵', 2:'🌿', 3:'⚡', 4:'🔀', 5:'⏪',
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.textContent = '✦ 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 ? '⚡ ' : ''}${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.textContent = isConf ? '⚡' : '●';
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').textContent = '⏸ 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').textContent = '✓ 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.1")))
1698 html = html.replace("{{DOMAIN}}", str(meta.get("domain", "music")))
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 tour_de_force.json → HTML")
1722 parser.add_argument("json_file", help="Path to tour_de_force.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()}")