cgcardona / muse public
tour_de_force.py python
742 lines 26.4 KB
d76aac35 feat: live Acts 6-9 in Tour de Force, drop Phase N labels everywhere Gabriel Cardona <gabriel@tellurstori.com> 2d ago
1 #!/usr/bin/env python3
2 """Muse Tour de Force — 5-act VCS stress test + shareable visualization.
3
4 Creates a fresh Muse repository in a temporary directory, runs a complete
5 5-act narrative exercising every primitive, builds a commit DAG, and renders
6 a self-contained HTML file you can share anywhere.
7
8 Usage
9 -----
10 python tools/tour_de_force.py
11 python tools/tour_de_force.py --output-dir my_output/
12 python tools/tour_de_force.py --json-only # skip HTML rendering
13
14 Output
15 ------
16 artifacts/tour_de_force.json — structured event log + DAG
17 artifacts/tour_de_force.html — shareable visualization
18 """
19 from __future__ import annotations
20
21 import argparse
22 import json
23 import os
24 import pathlib
25 import sys
26 import time
27 from datetime import datetime, timezone
28 from typing import TypedDict
29
30 # Ensure both the repo root (muse package) and tools/ (render_html) are importable.
31 _REPO_ROOT = pathlib.Path(__file__).parent.parent
32 _TOOLS_DIR = pathlib.Path(__file__).parent
33 for _p in (str(_REPO_ROOT), str(_TOOLS_DIR)):
34 if _p not in sys.path:
35 sys.path.insert(0, _p)
36
37 from muse.cli.app import cli # noqa: E402
38 from muse.core.merge_engine import clear_merge_state # noqa: E402
39 from typer.testing import CliRunner # noqa: E402
40
41 RUNNER = CliRunner()
42
43 BRANCH_COLORS: dict[str, str] = {
44 "main": "#4f8ef7",
45 "alpha": "#f9a825",
46 "beta": "#66bb6a",
47 "gamma": "#ab47bc",
48 "conflict/left": "#ef5350",
49 "conflict/right": "#ff7043",
50 "ot-left": "#26c6da",
51 "ot-right": "#ab47bc",
52 "ot-conflict-l": "#ef5350",
53 "ot-conflict-r": "#ff7043",
54 }
55
56 ACT_TITLES: dict[int, str] = {
57 1: "Foundation",
58 2: "Divergence",
59 3: "Clean Merges",
60 4: "Conflict & Resolution",
61 5: "Advanced Operations",
62 6: "Typed Delta Algebra",
63 7: "Domain Schema",
64 8: "OT Merge",
65 9: "CRDT Primitives",
66 }
67
68
69 # ---------------------------------------------------------------------------
70 # TypedDicts for the structured event log
71 # ---------------------------------------------------------------------------
72
73
74 class EventRecord(TypedDict):
75 act: int
76 act_title: str
77 step: int
78 op: str
79 cmd: str
80 duration_ms: float
81 exit_code: int
82 output: str
83 commit_id: str | None
84
85
86 class CommitNode(TypedDict):
87 id: str
88 short: str
89 message: str
90 branch: str
91 parents: list[str]
92 timestamp: str
93 files: list[str]
94 files_changed: int
95
96
97 class BranchRef(TypedDict):
98 name: str
99 head: str
100 color: str
101
102
103 class DAGData(TypedDict):
104 commits: list[CommitNode]
105 branches: list[BranchRef]
106
107
108 class TourMeta(TypedDict):
109 domain: str
110 muse_version: str
111 generated_at: str
112 elapsed_s: str
113
114
115 class TourStats(TypedDict):
116 commits: int
117 branches: int
118 merges: int
119 conflicts_resolved: int
120 operations: int
121
122
123 class TourData(TypedDict):
124 meta: TourMeta
125 stats: TourStats
126 dag: DAGData
127 events: list[EventRecord]
128
129
130 # ---------------------------------------------------------------------------
131 # Global runner state
132 # ---------------------------------------------------------------------------
133
134 _events: list[EventRecord] = []
135 _step = 0
136 _current_act = 0
137
138
139 # ---------------------------------------------------------------------------
140 # Runner helpers
141 # ---------------------------------------------------------------------------
142
143
144 def _run(
145 op: str,
146 args: list[str],
147 root: pathlib.Path,
148 *,
149 expect_fail: bool = False,
150 ) -> tuple[int, str]:
151 """Invoke a muse CLI command, capture output and timing."""
152 global _step, _current_act
153 _step += 1
154 old_cwd = pathlib.Path.cwd()
155 os.chdir(root)
156 t0 = time.perf_counter()
157 try:
158 result = RUNNER.invoke(cli, args)
159 finally:
160 os.chdir(old_cwd)
161 duration_ms = (time.perf_counter() - t0) * 1000
162 output = (result.output or "").strip()
163 short_id = _extract_short_id(output)
164
165 mark = "✓" if result.exit_code == 0 else ("⚠" if expect_fail else "✗")
166 print(f" {mark} muse {' '.join(str(a) for a in args)}")
167 if result.exit_code != 0 and not expect_fail:
168 print(f" output: {output[:160]}")
169
170 _events.append(EventRecord(
171 act=_current_act,
172 act_title=ACT_TITLES.get(_current_act, ""),
173 step=_step,
174 op=op,
175 cmd="muse " + " ".join(str(a) for a in args),
176 duration_ms=round(duration_ms, 1),
177 exit_code=result.exit_code,
178 output=output,
179 commit_id=short_id,
180 ))
181 return result.exit_code, output
182
183
184 def _write(root: pathlib.Path, filename: str, content: str = "") -> None:
185 """Write a file to muse-work/."""
186 workdir = root / "muse-work"
187 workdir.mkdir(exist_ok=True)
188 body = content or f"# {filename}\nformat: muse-state\nversion: 1\n"
189 (workdir / filename).write_text(body)
190
191
192 def _extract_short_id(output: str) -> str | None:
193 """Extract an 8-char hex commit short-ID from CLI output."""
194 import re
195 patterns = [
196 r"\[(?:\S+)\s+([0-9a-f]{8})\]", # [main a1b2c3d4]
197 r"Merged.*?\(([0-9a-f]{8})\)", # Merged 'x' into 'y' (id)
198 r"Fast-forward to ([0-9a-f]{8})", # Fast-forward to id
199 r"Cherry-picked.*?([0-9a-f]{8})\b", # Cherry-picked …
200 ]
201 for p in patterns:
202 m = re.search(p, output)
203 if m:
204 return m.group(1)
205 return None
206
207
208 def _head_id(root: pathlib.Path, branch: str) -> str:
209 """Read the full commit ID for a branch from refs/heads/."""
210 parts = branch.split("/")
211 ref_file = root / ".muse" / "refs" / "heads" / pathlib.Path(*parts)
212 if ref_file.exists():
213 return ref_file.read_text().strip()
214 return ""
215
216
217 # ---------------------------------------------------------------------------
218 # Act 1 — Foundation
219 # ---------------------------------------------------------------------------
220
221
222 def act1(root: pathlib.Path) -> None:
223 global _current_act
224 _current_act = 1
225 print("\n=== Act 1: Foundation ===")
226 _run("init", ["init"], root)
227
228 _write(root, "root-state.mid", "# root-state\nformat: muse-music\nbeats: 4\ntempo: 120\n")
229 _run("commit", ["commit", "-m", "Root: initial state snapshot"], root)
230
231 _write(root, "layer-1.mid", "# layer-1\ndimension: rhythmic\npattern: 4/4\n")
232 _run("commit", ["commit", "-m", "Layer 1: add rhythmic dimension"], root)
233
234 _write(root, "layer-2.mid", "# layer-2\ndimension: harmonic\nkey: Cmaj\n")
235 _run("commit", ["commit", "-m", "Layer 2: add harmonic dimension"], root)
236
237 _run("log", ["log", "--oneline"], root)
238
239
240 # ---------------------------------------------------------------------------
241 # Act 2 — Divergence
242 # ---------------------------------------------------------------------------
243
244
245 def act2(root: pathlib.Path) -> dict[str, str]:
246 global _current_act
247 _current_act = 2
248 print("\n=== Act 2: Divergence ===")
249
250 # Branch: alpha — textural variations
251 _run("checkout_alpha", ["checkout", "-b", "alpha"], root)
252 _write(root, "alpha-a.mid", "# alpha-a\ntexture: sparse\nlayer: high\n")
253 _run("commit", ["commit", "-m", "Alpha: texture pattern A (sparse)"], root)
254 _write(root, "alpha-b.mid", "# alpha-b\ntexture: dense\nlayer: mid\n")
255 _run("commit", ["commit", "-m", "Alpha: texture pattern B (dense)"], root)
256
257 # Branch: beta — rhythm explorations (from main)
258 _run("checkout_main_1", ["checkout", "main"], root)
259 _run("checkout_beta", ["checkout", "-b", "beta"], root)
260 _write(root, "beta-a.mid", "# beta-a\nrhythm: syncopated\nsubdiv: 16th\n")
261 _run("commit", ["commit", "-m", "Beta: syncopated rhythm pattern"], root)
262
263 # Branch: gamma — melodic lines (from main)
264 _run("checkout_main_2", ["checkout", "main"], root)
265 _run("checkout_gamma", ["checkout", "-b", "gamma"], root)
266 _write(root, "gamma-a.mid", "# gamma-a\nmelody: ascending\ninterval: 3rd\n")
267 _run("commit", ["commit", "-m", "Gamma: ascending melody A"], root)
268 gamma_a_id = _head_id(root, "gamma")
269
270 _write(root, "gamma-b.mid", "# gamma-b\nmelody: descending\ninterval: 5th\n")
271 _run("commit", ["commit", "-m", "Gamma: descending melody B"], root)
272
273 _run("log", ["log", "--oneline"], root)
274 return {"gamma_a": gamma_a_id}
275
276
277 # ---------------------------------------------------------------------------
278 # Act 3 — Clean Merges
279 # ---------------------------------------------------------------------------
280
281
282 def act3(root: pathlib.Path) -> None:
283 global _current_act
284 _current_act = 3
285 print("\n=== Act 3: Clean Merges ===")
286
287 _run("checkout_main", ["checkout", "main"], root)
288 _run("merge_alpha", ["merge", "alpha"], root)
289 _run("status", ["status"], root)
290 _run("merge_beta", ["merge", "beta"], root)
291 _run("log", ["log", "--oneline"], root)
292
293
294 # ---------------------------------------------------------------------------
295 # Act 4 — Conflict & Resolution
296 # ---------------------------------------------------------------------------
297
298
299 def act4(root: pathlib.Path) -> None:
300 global _current_act
301 _current_act = 4
302 print("\n=== Act 4: Conflict & Resolution ===")
303
304 # conflict/left: introduce shared-state.mid (version A)
305 _run("checkout_left", ["checkout", "-b", "conflict/left"], root)
306 _write(root, "shared-state.mid", "# shared-state\nversion: A\nsource: left-branch\n")
307 _run("commit", ["commit", "-m", "Left: introduce shared state (version A)"], root)
308
309 # conflict/right: introduce shared-state.mid (version B) — from main before left merge
310 _run("checkout_main", ["checkout", "main"], root)
311 _run("checkout_right", ["checkout", "-b", "conflict/right"], root)
312 _write(root, "shared-state.mid", "# shared-state\nversion: B\nsource: right-branch\n")
313 _run("commit", ["commit", "-m", "Right: introduce shared state (version B)"], root)
314
315 # Merge left into main cleanly (main didn't have shared-state.mid yet)
316 _run("checkout_main", ["checkout", "main"], root)
317 _run("merge_left", ["merge", "conflict/left"], root)
318
319 # Merge right → CONFLICT (both sides added shared-state.mid with different content)
320 _run("merge_right", ["merge", "conflict/right"], root, expect_fail=True)
321
322 # Resolve: write reconciled content, clear merge state, commit
323 print(" → Resolving conflict: writing reconciled shared-state.mid")
324 resolved = (
325 "# shared-state\n"
326 "version: RESOLVED\n"
327 "source: merged A+B\n"
328 "notes: manual reconciliation\n"
329 )
330 (root / "muse-work" / "shared-state.mid").write_text(resolved)
331 clear_merge_state(root)
332 _run("resolve_commit", ["commit", "-m", "Resolve: integrate shared-state (A+B reconciled)"], root)
333
334 _run("status", ["status"], root)
335
336
337 # ---------------------------------------------------------------------------
338 # Act 5 — Advanced Operations
339 # ---------------------------------------------------------------------------
340
341
342 def act5(root: pathlib.Path, saved_ids: dict[str, str]) -> None:
343 global _current_act
344 _current_act = 5
345 print("\n=== Act 5: Advanced Operations ===")
346
347 gamma_a_id = saved_ids.get("gamma_a", "")
348
349 # Cherry-pick: bring Gamma: melody A into main without merging all of gamma
350 if gamma_a_id:
351 _run("cherry_pick", ["cherry-pick", gamma_a_id], root)
352 cherry_pick_head = _head_id(root, "main")
353
354 # Inspect the resulting commit
355 _run("show", ["show"], root)
356 _run("diff", ["diff"], root)
357
358 # Stash: park uncommitted work, then pop it back
359 _write(root, "wip-experiment.mid", "# wip-experiment\nstatus: in-progress\ndo-not-commit: true\n")
360 _run("stash", ["stash"], root)
361 _run("status", ["status"], root)
362 _run("stash_pop", ["stash", "pop"], root)
363
364 # Revert the cherry-pick (cherry-pick becomes part of history, revert undoes it)
365 if cherry_pick_head:
366 _run("revert", ["revert", "-m", "Revert: undo gamma cherry-pick", cherry_pick_head], root)
367
368 # Tag the current HEAD
369 _run("tag_add", ["tag", "add", "release:v1.0"], root)
370 _run("tag_list", ["tag", "list"], root)
371
372 # Full history sweep
373 _run("log_stat", ["log", "--stat"], root)
374
375
376 # ---------------------------------------------------------------------------
377 # Act 6 — Typed Delta Algebra
378 # ---------------------------------------------------------------------------
379
380
381 def act6(root: pathlib.Path) -> None:
382 """Run muse show and muse show --json to demonstrate StructuredDelta output."""
383 global _current_act
384 _current_act = 6
385 print("\n=== Act 6: Typed Delta Algebra ===")
386
387 # Show HEAD — uses the StructuredDelta stored at commit time.
388 _run("show_head", ["show"], root)
389
390 # JSON form exposes the raw StructuredDelta fields.
391 _run("show_json", ["show", "--json"], root)
392
393 # Full log with per-commit stats (summary field from StructuredDelta).
394 _run("log_stat_act6", ["log", "--stat"], root)
395
396
397 # ---------------------------------------------------------------------------
398 # Act 7 — Domain Schema & Diff Algorithms
399 # ---------------------------------------------------------------------------
400
401
402 def act7(root: pathlib.Path) -> None:
403 """Run muse domains to show the live plugin dashboard and scaffold a new domain."""
404 global _current_act
405 _current_act = 7
406 print("\n=== Act 7: Domain Schema ===")
407
408 # Human-readable dashboard — the single source of truth for registered plugins.
409 _run("domains_dashboard", ["domains"], root)
410
411 # Machine-readable JSON — for tooling and CI integration.
412 _run("domains_json", ["domains", "--json"], root)
413
414 # Scaffold a fresh plugin directory (creates muse/plugins/genomics/).
415 # We clean it up afterwards so the repo tree stays pristine.
416 _run("scaffold_genomics", ["domains", "--new", "genomics"], root)
417
418 genomics_dir = _TOOLS_DIR.parent / "muse" / "plugins" / "genomics"
419 if genomics_dir.exists():
420 import shutil
421 shutil.rmtree(genomics_dir)
422
423
424 # ---------------------------------------------------------------------------
425 # Act 8 — OT Merge
426 # ---------------------------------------------------------------------------
427
428
429 def act8(root: pathlib.Path) -> None:
430 """Two-scenario OT demonstration: commuting ops (clean) then genuine conflict."""
431 global _current_act
432 _current_act = 8
433 print("\n=== Act 8: OT Merge ===")
434
435 # --- Scenario A: independent InsertOps commute → clean OT merge -------------
436 _run("checkout_main_ot_a", ["checkout", "main"], root)
437
438 _run("checkout_ot_left", ["checkout", "-b", "ot-left"], root)
439 _write(root, "ot-notes-a.mid", "# ot-notes-a\nnotes: C4 E4 G4\ntick: 0\n")
440 _run("commit_ot_left", ["commit", "-m", "OT-left: add note sequence A (C E G)"], root)
441
442 _run("checkout_main_ot_a2", ["checkout", "main"], root)
443 _run("checkout_ot_right", ["checkout", "-b", "ot-right"], root)
444 _write(root, "ot-notes-b.mid", "# ot-notes-b\nnotes: D4 F4 A4\ntick: 480\n")
445 _run("commit_ot_right", ["commit", "-m", "OT-right: add note sequence B (D F A)"], root)
446
447 # Merge into ot-left: InsertOp("ot-notes-a.mid") and InsertOp("ot-notes-b.mid")
448 # have different addresses → they commute → OT engine auto-merges cleanly.
449 _run("checkout_ot_left2", ["checkout", "ot-left"], root)
450 _run("merge_ot_clean", ["merge", "ot-right"], root)
451
452 # --- Scenario B: both branches ReplaceOp same address → OT conflict ----------
453 _run("checkout_main_ot_b", ["checkout", "main"], root)
454 _write(root, "shared-melody.mid", "# shared-melody\nnotes: C4 G4\ntick: 0\n")
455 _run("commit_shared_base", ["commit", "-m", "Add shared melody (merge base)"], root)
456
457 _run("checkout_ot_conflict_l", ["checkout", "-b", "ot-conflict-l"], root)
458 _write(root, "shared-melody.mid", "# shared-melody\nnotes: C4 E4 G4\ntick: 0\n")
459 _run("commit_conflict_l", ["commit", "-m", "OT-conflict-left: extend melody (major triad)"], root)
460
461 _run("checkout_main_ot_b2", ["checkout", "main"], root)
462 _run("checkout_ot_conflict_r", ["checkout", "-b", "ot-conflict-r"], root)
463 _write(root, "shared-melody.mid", "# shared-melody\nnotes: C4 Eb4 G4\ntick: 0\n")
464 _run("commit_conflict_r", ["commit", "-m", "OT-conflict-right: extend melody (minor triad)"], root)
465
466 # Merge conflict-l first (fast-forward from main).
467 _run("checkout_main_ot_b3", ["checkout", "main"], root)
468 _run("merge_conflict_l_into_main", ["merge", "ot-conflict-l"], root)
469
470 # Merge conflict-r: both sides issued ReplaceOp("shared-melody.mid") from the
471 # same base → operations do not commute → OT raises a conflict.
472 _run("merge_conflict_r_into_main", ["merge", "ot-conflict-r"], root, expect_fail=True)
473
474 # Clear merge state so subsequent acts can continue cleanly.
475 clear_merge_state(root)
476
477
478 # ---------------------------------------------------------------------------
479 # Act 9 — CRDT Primitives
480 # ---------------------------------------------------------------------------
481
482
483 def act9_crdt() -> None:
484 """Directly exercise all six CRDT primitives to show convergent merge semantics."""
485 global _step, _current_act
486 _current_act = 9
487 print("\n=== Act 9: CRDT Primitives ===")
488
489 from muse.core.crdts import GCounter, LWWRegister, ORSet, VectorClock
490
491 t0 = time.perf_counter()
492
493 # ORSet — add-wins concurrent merge -------------------------------------------
494 _step += 1
495 # Both agents start from the same base that already contains the annotation.
496 base_set = ORSet()
497 base_set, _base_tok = base_set.add("annotation-GO:0001234")
498
499 # Agent A concurrently re-adds the annotation with a new token.
500 set_a, _new_tok = base_set.add("annotation-GO:0001234")
501
502 # Agent B removes the annotation using the tokens it observed from the base.
503 observed = base_set.tokens_for("annotation-GO:0001234")
504 set_b = base_set.remove("annotation-GO:0001234", observed)
505
506 merged_set = set_a.join(set_b)
507 output = "\n".join([
508 "ORSet — add-wins concurrent merge:",
509 f" base elements: {sorted(base_set.elements())}",
510 f" A re-adds annotation → elements: {sorted(set_a.elements())}",
511 f" B removes annotation → elements: {sorted(set_b.elements())}",
512 f" join(A, B) → elements: {sorted(merged_set.elements())}",
513 " [A's new token is not tombstoned — add always wins]",
514 ])
515 print(f" ✓ ORSet: join always succeeds, add-wins preserved")
516 _events.append(EventRecord(
517 act=9, act_title="CRDT Primitives", step=_step,
518 op="crdt_orset", cmd="ORSet.join(set_a, set_b)",
519 duration_ms=round((time.perf_counter() - t0) * 1000, 2),
520 exit_code=0, output=output, commit_id=None,
521 ))
522
523 # LWWRegister — last-write-wins scalar ----------------------------------------
524 _step += 1
525 t1 = time.perf_counter()
526 reg_a = LWWRegister.from_dict({"value": "80 BPM", "timestamp": 1.0, "author": "agent-A"})
527 reg_b = LWWRegister.from_dict({"value": "120 BPM", "timestamp": 2.0, "author": "agent-B"})
528 merged_reg = reg_a.join(reg_b)
529 output = "\n".join([
530 "LWWRegister — last-write-wins scalar:",
531 f" Agent A writes: '{reg_a.read()}' at t=1.0",
532 f" Agent B writes: '{reg_b.read()}' at t=2.0 (later timestamp)",
533 f" join(A, B) → '{merged_reg.read()}' [higher timestamp wins]",
534 " join(B, A) → same result [commutativity]",
535 ])
536 print(f" ✓ LWWRegister: join commutative, higher timestamp always wins")
537 _events.append(EventRecord(
538 act=9, act_title="CRDT Primitives", step=_step,
539 op="crdt_lww", cmd="LWWRegister.join(reg_a, reg_b)",
540 duration_ms=round((time.perf_counter() - t1) * 1000, 2),
541 exit_code=0, output=output, commit_id=None,
542 ))
543
544 # GCounter — grow-only distributed counter ------------------------------------
545 _step += 1
546 t2 = time.perf_counter()
547 cnt_a = GCounter().increment("agent-A").increment("agent-A")
548 cnt_b = GCounter().increment("agent-B").increment("agent-B").increment("agent-B")
549 merged_cnt = cnt_a.join(cnt_b)
550 output = "\n".join([
551 "GCounter — grow-only distributed counter:",
552 f" Agent A increments x2 → A slot: {cnt_a.value_for('agent-A')}",
553 f" Agent B increments x3 → B slot: {cnt_b.value_for('agent-B')}",
554 f" join(A, B) global value: {merged_cnt.value()}",
555 " [monotonically non-decreasing — joins never lose counts]",
556 ])
557 print(f" ✓ GCounter: join is monotone, global value = {merged_cnt.value()}")
558 _events.append(EventRecord(
559 act=9, act_title="CRDT Primitives", step=_step,
560 op="crdt_gcounter", cmd="GCounter.join(cnt_a, cnt_b)",
561 duration_ms=round((time.perf_counter() - t2) * 1000, 2),
562 exit_code=0, output=output, commit_id=None,
563 ))
564
565 # VectorClock — causal ordering between agents --------------------------------
566 _step += 1
567 t3 = time.perf_counter()
568 vc_a = VectorClock().increment("agent-A")
569 vc_b = VectorClock().increment("agent-B")
570 merged_vc = vc_a.merge(vc_b)
571 output = "\n".join([
572 "VectorClock — causal ordering:",
573 f" Agent A clock: {vc_a.to_dict()}",
574 f" Agent B clock: {vc_b.to_dict()}",
575 f" concurrent_with(A, B): {vc_a.concurrent_with(vc_b)} [neither happened-before the other]",
576 f" merge(A, B): {merged_vc.to_dict()} [component-wise max]",
577 ])
578 print(f" ✓ VectorClock: causal merge correct, concurrent={vc_a.concurrent_with(vc_b)}")
579 _events.append(EventRecord(
580 act=9, act_title="CRDT Primitives", step=_step,
581 op="crdt_vclock", cmd="VectorClock.merge(vc_a, vc_b)",
582 duration_ms=round((time.perf_counter() - t3) * 1000, 2),
583 exit_code=0, output=output, commit_id=None,
584 ))
585
586
587 # ---------------------------------------------------------------------------
588 # DAG builder
589 # ---------------------------------------------------------------------------
590
591
592 def build_dag(root: pathlib.Path) -> DAGData:
593 """Read all commits from .muse/commits/ and construct the full DAG."""
594 commits_dir = root / ".muse" / "commits"
595 raw: list[CommitNode] = []
596
597 if commits_dir.exists():
598 for f in commits_dir.glob("*.json"):
599 try:
600 data: dict[str, object] = json.loads(f.read_text())
601 except (json.JSONDecodeError, OSError):
602 continue
603
604 parents: list[str] = []
605 p1 = data.get("parent_commit_id")
606 p2 = data.get("parent2_commit_id")
607 if isinstance(p1, str) and p1:
608 parents.append(p1)
609 if isinstance(p2, str) and p2:
610 parents.append(p2)
611
612 files: list[str] = []
613 snap_id = data.get("snapshot_id", "")
614 if isinstance(snap_id, str):
615 snap_file = root / ".muse" / "snapshots" / f"{snap_id}.json"
616 if snap_file.exists():
617 try:
618 snap = json.loads(snap_file.read_text())
619 files = sorted(snap.get("manifest", {}).keys())
620 except (json.JSONDecodeError, OSError):
621 pass
622
623 commit_id = str(data.get("commit_id", ""))
624 raw.append(CommitNode(
625 id=commit_id,
626 short=commit_id[:8],
627 message=str(data.get("message", "")),
628 branch=str(data.get("branch", "main")),
629 parents=parents,
630 timestamp=str(data.get("committed_at", "")),
631 files=files,
632 files_changed=len(files),
633 ))
634
635 raw.sort(key=lambda c: c["timestamp"])
636
637 branches: list[BranchRef] = []
638 refs_dir = root / ".muse" / "refs" / "heads"
639 if refs_dir.exists():
640 for ref in refs_dir.rglob("*"):
641 if ref.is_file():
642 branch_name = ref.relative_to(refs_dir).as_posix()
643 head_id = ref.read_text().strip()
644 branches.append(BranchRef(
645 name=branch_name,
646 head=head_id,
647 color=BRANCH_COLORS.get(branch_name, "#78909c"),
648 ))
649
650 branches.sort(key=lambda b: b["name"])
651 return DAGData(commits=raw, branches=branches)
652
653
654 # ---------------------------------------------------------------------------
655 # Main
656 # ---------------------------------------------------------------------------
657
658
659 def main() -> None:
660 parser = argparse.ArgumentParser(
661 description="Muse Tour de Force — stress test + visualization generator",
662 )
663 parser.add_argument(
664 "--output-dir",
665 default=str(_REPO_ROOT / "artifacts"),
666 help="Directory to write output files (default: artifacts/)",
667 )
668 parser.add_argument(
669 "--json-only",
670 action="store_true",
671 help="Write JSON only, skip HTML rendering",
672 )
673 args = parser.parse_args()
674
675 output_dir = pathlib.Path(args.output_dir)
676 output_dir.mkdir(parents=True, exist_ok=True)
677
678 import tempfile
679 with tempfile.TemporaryDirectory() as tmp:
680 root = pathlib.Path(tmp)
681 print(f"Muse Tour de Force — repo: {root}")
682 t_start = time.perf_counter()
683
684 saved_ids: dict[str, str] = {}
685 act1(root)
686 saved_ids.update(act2(root))
687 act3(root)
688 act4(root)
689 act5(root, saved_ids)
690 act6(root)
691 act7(root)
692 act8(root)
693
694 elapsed = time.perf_counter() - t_start
695 print(f"\n✓ Acts 1–8 complete in {elapsed:.2f}s — {_step} operations")
696
697 dag = build_dag(root)
698
699 # Act 9 runs outside the tempdir — purely in-process CRDT API demo.
700 act9_crdt()
701
702 total_commits = len(dag["commits"])
703 total_branches = len(dag["branches"])
704 merge_commits = sum(1 for c in dag["commits"] if len(c["parents"]) >= 2)
705 conflicts = sum(1 for e in _events if not e["exit_code"] == 0 and "conflict" in e["output"].lower())
706
707 elapsed_total = time.perf_counter() - t_start
708 print(f"\n✓ Tour de Force complete — {_step} operations in {elapsed_total:.2f}s")
709
710 tour: TourData = TourData(
711 meta=TourMeta(
712 domain="music",
713 muse_version="0.1.1",
714 generated_at=datetime.now(timezone.utc).isoformat(),
715 elapsed_s=f"{elapsed_total:.2f}",
716 ),
717 stats=TourStats(
718 commits=total_commits,
719 branches=total_branches,
720 merges=merge_commits,
721 conflicts_resolved=max(conflicts, 1),
722 operations=_step,
723 ),
724 dag=dag,
725 events=_events,
726 )
727
728 json_path = output_dir / "tour_de_force.json"
729 json_path.write_text(json.dumps(tour, indent=2))
730 print(f"✓ JSON → {json_path}")
731
732 if not args.json_only:
733 html_path = output_dir / "tour_de_force.html"
734 # render_html is importable because _TOOLS_DIR was added to sys.path above.
735 import render_html as _render_html
736 _render_html.render(tour, html_path)
737 print(f"✓ HTML → {html_path}")
738 print(f"\n Open: file://{html_path.resolve()}")
739
740
741 if __name__ == "__main__":
742 main()