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