gabriel / musehub public
seed_narratives.py python
1269 lines 61.4 KB
e6fad116 Remove all Stori, Maestro, and AgentCeption references; rebrand to Muse VCS Gabriel Cardona <gabriel@tellurstori.com> 6d ago
1 """MuseHub narrative scenario seed script.
2
3 Creates 5 interconnected stories that make demo data feel alive:
4
5 1. Bach Remix War — marcus forks gabriel/neo-baroque, 808 bass PR rejected
6 with 15-comment traditionalist vs modernist debate, consolation trap release.
7
8 2. Chopin+Coltrane — yuki + fatou 3-way merge conflict, 20-comment resolution
9 debate, joint authorship merge commit, jazz edition release.
10
11 3. Ragtime EDM Collab — 3-participant session (marcus, fatou, aaliya), 8
12 commits, fatou's polyrhythm commit gets fire reactions, "Maple Leaf Drops"
13 release.
14
15 4. Community Chaos — "community-jam" repo with 5 simultaneous open PRs, 25-
16 comment key signature debate, 3 conflict PRs, 70 % milestone completion.
17
18 5. Goldberg Milestone — gabriel's 30-variation project, 28/30 done, Variation
19 25 debate (18 comments), Variation 29 PR with yuki requesting ornamentation.
20
21 Run inside the container (after seed_musehub.py):
22 docker compose exec muse python3 /app/scripts/seed_narratives.py
23
24 Idempotent: checks for the sentinel repo ID before inserting.
25 Pass --force to wipe narrative data and re-insert.
26 """
27 from __future__ import annotations
28
29 import asyncio
30 import hashlib
31 import sys
32 import uuid
33 from datetime import datetime, timedelta, timezone
34 from typing import Any
35
36 from sqlalchemy import text
37 from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
38 from sqlalchemy.orm import sessionmaker
39
40 from musehub.config import settings
41 from musehub.db.musehub_models import (
42 MusehubBranch,
43 MusehubComment,
44 MusehubCommit,
45 MusehubFork,
46 MusehubIssue,
47 MusehubIssueComment,
48 MusehubMilestone,
49 MusehubPRComment,
50 MusehubPRReview,
51 MusehubProfile,
52 MusehubPullRequest,
53 MusehubReaction,
54 MusehubRelease,
55 MusehubRepo,
56 MusehubSession,
57 )
58
59 # ---------------------------------------------------------------------------
60 # Helpers
61 # ---------------------------------------------------------------------------
62
63 UTC = timezone.utc
64
65
66 def _now(days: int = 0, hours: int = 0) -> datetime:
67 return datetime.now(tz=UTC) - timedelta(days=days, hours=hours)
68
69
70 def _sha(seed: str) -> str:
71 return hashlib.sha256(seed.encode()).hexdigest()
72
73
74 def _uid(seed: str) -> str:
75 return str(uuid.UUID(bytes=hashlib.md5(seed.encode()).digest()))
76
77
78 # ---------------------------------------------------------------------------
79 # Stable IDs for narrative repos (never conflict with seed_musehub IDs)
80 # ---------------------------------------------------------------------------
81
82 # Scenario 1 — Bach Remix War
83 REPO_NEO_BAROQUE = "narr-neo-baroque-00001"
84 REPO_NEO_BAROQUE_FORK = "narr-neo-baroque-fork1"
85
86 # Scenario 2 — Chopin+Coltrane
87 REPO_NOCTURNE = "narr-nocturne-op9-00001"
88
89 # Scenario 3 — Ragtime EDM Collab
90 REPO_RAGTIME_EDM = "narr-ragtime-edm-00001"
91
92 # Scenario 4 — Community Chaos
93 REPO_COMMUNITY_JAM = "narr-community-jam-001"
94
95 # Scenario 5 — Goldberg Milestone
96 REPO_GOLDBERG = "narr-goldberg-var-00001"
97
98 # Sentinel: if this repo exists we consider the narratives already seeded.
99 SENTINEL_REPO_ID = REPO_NEO_BAROQUE
100
101 # User stable IDs (match seed_musehub.py)
102 GABRIEL = "user-gabriel-001"
103 MARCUS = "user-marcus-003"
104 YUKI = "user-yuki-004"
105 FATOU = "user-fatou-007"
106 PIERRE = "user-pierre-008"
107 SOFIA = "user-sofia-002"
108 AALIYA = "user-aaliya-005"
109 CHEN = "user-chen-006"
110
111
112 # ---------------------------------------------------------------------------
113 # Scenario 1 — Bach Remix War
114 # ---------------------------------------------------------------------------
115
116 async def _seed_bach_remix_war(db: AsyncSession) -> None:
117 """Bach Remix War: marcus forks gabriel/neo-baroque, proposes an 808 bass
118 line, gets rejected after 15 comments between purists and modernists, then
119 releases the fork as 'v1.0.0-trap'.
120
121 Story arc: gabriel creates neo-baroque in strict counterpoint style.
122 marcus adds an 808 sub-bass and opens a PR. gabriel, pierre, and chen
123 defend the original; marcus, fatou, and aaliya champion the fusion.
124 PR is closed as "not a fit for this project." marcus releases his fork.
125 """
126 # Repos
127 db.add(MusehubRepo(
128 repo_id=REPO_NEO_BAROQUE,
129 name="Neo-Baroque Counterpoint",
130 owner="gabriel",
131 slug="neo-baroque-counterpoint",
132 owner_user_id=GABRIEL,
133 visibility="public",
134 description="Strict counterpoint in the style of J.S. Bach — no anachronisms.",
135 tags=["baroque", "counterpoint", "Bach", "harpsichord", "strict"],
136 key_signature="D minor",
137 tempo_bpm=72,
138 created_at=_now(days=60),
139 ))
140 db.add(MusehubRepo(
141 repo_id=REPO_NEO_BAROQUE_FORK,
142 name="Neo-Baroque Counterpoint",
143 owner="marcus",
144 slug="neo-baroque-counterpoint",
145 owner_user_id=MARCUS,
146 visibility="public",
147 description="Fork of gabriel/neo-baroque-counterpoint — trap-baroque fusion experiment.",
148 tags=["baroque", "trap", "808", "fusion", "fork"],
149 key_signature="D minor",
150 tempo_bpm=72,
151 created_at=_now(days=30),
152 ))
153
154 # Fork record
155 db.add(MusehubFork(
156 fork_id=_uid("narr-fork-neo-baroque-marcus"),
157 source_repo_id=REPO_NEO_BAROQUE,
158 fork_repo_id=REPO_NEO_BAROQUE_FORK,
159 forked_by="marcus",
160 created_at=_now(days=30),
161 ))
162
163 # Commits on original repo
164 orig_commits: list[dict[str, Any]] = [
165 dict(message="init: D minor counterpoint skeleton at 72 BPM", author="gabriel", days=60),
166 dict(message="feat(soprano): Bach-style cantus firmus — whole notes", author="gabriel", days=58),
167 dict(message="feat(alto): first species counterpoint against cantus", author="gabriel", days=56),
168 dict(message="feat(tenor): second species — half-note counterpoint", author="gabriel", days=54),
169 dict(message="feat(bass): third species — quarter-note passing motion", author="gabriel", days=52),
170 dict(message="refactor(harmony): correct parallel-fifth error in bar 6", author="gabriel", days=50),
171 dict(message="feat(harpsichord): continuo realisation — figured bass", author="gabriel", days=48),
172 ]
173 prev_id: str | None = None
174 for i, c in enumerate(orig_commits):
175 cid = _sha(f"narr-neo-baroque-orig-{i}")
176 db.add(MusehubCommit(
177 commit_id=cid,
178 repo_id=REPO_NEO_BAROQUE,
179 branch="main",
180 parent_ids=[prev_id] if prev_id else [],
181 message=c["message"],
182 author=c["author"],
183 timestamp=_now(days=c["days"]),
184 snapshot_id=_sha(f"snap-narr-neo-baroque-{i}"),
185 ))
186 prev_id = cid
187 db.add(MusehubBranch(
188 repo_id=REPO_NEO_BAROQUE,
189 name="main",
190 head_commit_id=_sha("narr-neo-baroque-orig-6"),
191 ))
192
193 # Commits on fork (marcus adds trap elements)
194 fork_commits: list[dict[str, Any]] = [
195 dict(message="init: fork from gabriel/neo-baroque-counterpoint", author="marcus", days=30),
196 dict(message="feat(808): sub-bass 808 kick on beats 1 and 3", author="marcus", days=28),
197 dict(message="feat(hihat): hi-hat rolls between counterpoint lines", author="marcus", days=27),
198 dict(message="feat(808): pitched 808 bass following cantus firmus notes", author="marcus", days=26),
199 dict(message="refactor(808): tune 808 to D2 — matches bass line root", author="marcus", days=25),
200 ]
201 fork_prev: str | None = None
202 for i, c in enumerate(fork_commits):
203 cid = _sha(f"narr-neo-baroque-fork-{i}")
204 db.add(MusehubCommit(
205 commit_id=cid,
206 repo_id=REPO_NEO_BAROQUE_FORK,
207 branch="main",
208 parent_ids=[fork_prev] if fork_prev else [],
209 message=c["message"],
210 author=c["author"],
211 timestamp=_now(days=c["days"]),
212 snapshot_id=_sha(f"snap-narr-neo-baroque-fork-{i}"),
213 ))
214 fork_prev = cid
215 db.add(MusehubBranch(
216 repo_id=REPO_NEO_BAROQUE_FORK,
217 name="main",
218 head_commit_id=_sha("narr-neo-baroque-fork-4"),
219 ))
220 db.add(MusehubBranch(
221 repo_id=REPO_NEO_BAROQUE_FORK,
222 name="feat/808-bass-layer",
223 head_commit_id=_sha("narr-neo-baroque-fork-4"),
224 ))
225
226 # The contested PR — closed after 15-comment debate
227 pr_id = _uid("narr-pr-neo-baroque-808")
228 db.add(MusehubPullRequest(
229 pr_id=pr_id,
230 repo_id=REPO_NEO_BAROQUE,
231 title="Feat: 808 sub-bass layer — trap baroque fusion",
232 body=(
233 "## Summary\n\nAdds a Roland TR-808 sub-bass layer beneath the counterpoint.\n\n"
234 "The 808 follows the cantus firmus pitch contour but occupies the sub-register "
235 "(20-80 Hz), creating physical impact without obscuring the counterpoint.\n\n"
236 "## Why\n\nBaroque structures work beautifully under modern production — "
237 "the formal rigour of counterpoint gives trap beats a melodic backbone they lack.\n\n"
238 "## Test\n- [ ] Counterpoint lines still audible at -3 dB monitor level\n"
239 "- [ ] No parallel 5ths introduced by 808 root motion"
240 ),
241 state="closed",
242 from_branch="feat/808-bass-layer",
243 to_branch="main",
244 author="marcus",
245 created_at=_now(days=24),
246 ))
247
248 # 15-comment debate on the PR
249 pr_comment_thread: list[tuple[str, str]] = [
250 ("gabriel", "Marcus, this is interesting technically, but the 808 completely undermines the contrapuntal texture. Bach's counterpoint is meant to be heard as independent voices — the sub-bass collapses everything into a single bass layer."),
251 ("marcus", "That's a fair point on voice independence, but listeners who'd never touch harpsichord music are discovering Bach through this. Isn't expanded reach worth a small compromise?"),
252 ("pierre", "I have to side with Gabriel. Counterpoint is a living tradition, not a museum piece — but 808 trap beats are a fundamentally different aesthetic. The two don't coexist musically."),
253 ("fatou", "Disagree. West African djembe tradition has always layered percussion over melodic lines. The idea that sub-bass destroys voice independence is a purely Western conservatory assumption."),
254 ("chen", "The spectral argument is valid — 808s around 50 Hz produce intermodulation products that muddy mid-range clarity. You'd need a steep high-pass on the counterpoint lines to compensate."),
255 ("aaliya", "Afrobeat composers have been layering bass-heavy production over complex polyphony for decades. This isn't a compromise — it's a new genre."),
256 ("gabriel", "I hear you, Fatou and Aaliya, and I respect those traditions. But the *intent* of this repo is strict counterpoint study. If Marcus releases this as a fork-project, I'll follow it. Just not here."),
257 ("marcus", "What if I bring the 808 down 12 dB and pitch it an octave below the bass line? It becomes sub-perceptual texture rather than a competing voice."),
258 ("pierre", "Sub-perceptual by definition doesn't contribute musically. Why add it at all?"),
259 ("fatou", "Because it's *felt*, not heard. That's the whole point of 808 production — physical resonance."),
260 ("yuki", "From a granular synthesis perspective, the transient of an 808 kick is a broadband impulse that *does* interfere with upper partials. Chen's intermod point is technically accurate."),
261 ("marcus", "Yuki, what if we high-pass the counterpoint tracks at 80 Hz? Each voice stays clean in its register."),
262 ("chen", "An 80 Hz high-pass on a harpsichord loses the warmth of the lower-register strings. The instrument's character lives in 60-200 Hz."),
263 ("aaliya", "I think the real disagreement is curatorial, not acoustic. Gabriel has a vision for this repo and the 808 doesn't fit it. The fork is the right call. Marcus should ship v1.0.0-trap from his fork."),
264 ("gabriel", "Aaliya said it better than I could. Closing this PR — not because the idea is bad, but because it belongs in a different project. Marcus, please ship that fork. I'll be the first to star it."),
265 ]
266 parent: str | None = None
267 for i, (author, body) in enumerate(pr_comment_thread):
268 cid = _uid(f"narr-pr-comment-bach-{i}")
269 db.add(MusehubPRComment(
270 comment_id=cid,
271 pr_id=pr_id,
272 repo_id=REPO_NEO_BAROQUE,
273 author=author,
274 body=body,
275 target_type="general",
276 parent_comment_id=parent if i > 0 else None,
277 created_at=_now(days=24 - i),
278 ))
279 parent = cid if i == 0 else parent # thread from first comment
280
281 # Consolation release on the fork
282 db.add(MusehubRelease(
283 repo_id=REPO_NEO_BAROQUE_FORK,
284 tag="v1.0.0-trap",
285 title="Trap Baroque — v1.0.0",
286 body=(
287 "## v1.0.0-trap — Trap Baroque Fusion\n\n"
288 "The PR may have been closed, but the music lives here.\n\n"
289 "### What's in this release\n"
290 "- D minor counterpoint (Bach-style, 4 voices)\n"
291 "- 808 sub-bass following cantus firmus contour\n"
292 "- Hi-hat rolls between phrase endings\n"
293 "- High-pass at 80 Hz on harpsichord tracks\n\n"
294 "### Philosophy\n"
295 "Bach wrote for the instruments of his time. He'd have used 808s.\n\n"
296 "Thanks gabriel for the counterpoint foundation and for keeping it civil."
297 ),
298 commit_id=_sha("narr-neo-baroque-fork-4"),
299 download_urls={
300 "midi_bundle": f"/releases/{REPO_NEO_BAROQUE_FORK}-v1.0.0-trap.zip",
301 "mp3": f"/releases/{REPO_NEO_BAROQUE_FORK}-v1.0.0-trap.mp3",
302 },
303 author="marcus",
304 created_at=_now(days=22),
305 ))
306
307 print(" ✅ Scenario 1: Bach Remix War — 15-comment PR debate, trap release")
308
309
310 # ---------------------------------------------------------------------------
311 # Scenario 2 — Chopin+Coltrane (3-way merge)
312 # ---------------------------------------------------------------------------
313
314 async def _seed_chopin_coltrane(db: AsyncSession) -> None:
315 """Chopin+Coltrane: pierre owns Nocturne Op.9 No.2 repo. yuki adds jazz
316 harmony, fatou adds Afro-Cuban rhythmic reinterpretation. A 3-way merge
317 conflict arises — yuki's chord voicings clash with fatou's rhythm track.
318 20-comment resolution debate. Joint authorship merge commit. Jazz release.
319 """
320 db.add(MusehubRepo(
321 repo_id=REPO_NOCTURNE,
322 name="Nocturne Op.9 No.2",
323 owner="pierre",
324 slug="nocturne-op9-no2",
325 owner_user_id=PIERRE,
326 visibility="public",
327 description="Chopin's Nocturne Op.9 No.2 — reimagined as a jazz fusion triptych.",
328 tags=["Chopin", "Coltrane", "nocturne", "jazz", "fusion", "piano"],
329 key_signature="Eb major",
330 tempo_bpm=66,
331 created_at=_now(days=55),
332 ))
333
334 # Original commits by pierre
335 nocturne_base: list[dict[str, Any]] = [
336 dict(message="init: Eb major nocturne skeleton — cantilena melody", author="pierre", days=55),
337 dict(message="feat(piano): ornamental triplets in RH — bars 1-4", author="pierre", days=53),
338 dict(message="feat(piano): LH arpeggiated accompaniment — 6/4 feel", author="pierre", days=51),
339 dict(message="feat(cello): added cello doubling melody in lower 8va", author="pierre", days=49),
340 ]
341 prev: str | None = None
342 for i, c in enumerate(nocturne_base):
343 cid = _sha(f"narr-nocturne-main-{i}")
344 db.add(MusehubCommit(
345 commit_id=cid,
346 repo_id=REPO_NOCTURNE,
347 branch="main",
348 parent_ids=[prev] if prev else [],
349 message=c["message"],
350 author=c["author"],
351 timestamp=_now(days=c["days"]),
352 snapshot_id=_sha(f"snap-narr-nocturne-{i}"),
353 ))
354 prev = cid
355 base_commit = _sha("narr-nocturne-main-3")
356
357 # yuki's jazz harmony branch
358 yuki_commits: list[dict[str, Any]] = [
359 dict(message="feat(harmony): Coltrane substitutions — ii-V-I into Eb7#11", author="yuki", days=45),
360 dict(message="feat(piano): quartal voicings over Chopin melody — McCoy Tyner style", author="yuki", days=44),
361 dict(message="feat(harmony): tritone sub on bar 4 turnaround — A7 → Eb7", author="yuki", days=43),
362 ]
363 yuki_prev = base_commit
364 for i, c in enumerate(yuki_commits):
365 cid = _sha(f"narr-nocturne-yuki-{i}")
366 db.add(MusehubCommit(
367 commit_id=cid,
368 repo_id=REPO_NOCTURNE,
369 branch="feat/coltrane-harmony",
370 parent_ids=[yuki_prev],
371 message=c["message"],
372 author=c["author"],
373 timestamp=_now(days=c["days"]),
374 snapshot_id=_sha(f"snap-narr-nocturne-yuki-{i}"),
375 ))
376 yuki_prev = cid
377 yuki_head = _sha("narr-nocturne-yuki-2")
378
379 # fatou's rhythm branch
380 fatou_commits: list[dict[str, Any]] = [
381 dict(message="feat(percussion): Afro-Cuban clave pattern under nocturne", author="fatou", days=45),
382 dict(message="feat(bass): Fender bass line — Afrobeat bass register", author="fatou", days=44),
383 dict(message="feat(percussion): bata drum call-and-response in bridge", author="fatou", days=43),
384 ]
385 fatou_prev = base_commit
386 for i, c in enumerate(fatou_commits):
387 cid = _sha(f"narr-nocturne-fatou-{i}")
388 db.add(MusehubCommit(
389 commit_id=cid,
390 repo_id=REPO_NOCTURNE,
391 branch="feat/afro-rhythm",
392 parent_ids=[fatou_prev],
393 message=c["message"],
394 author=c["author"],
395 timestamp=_now(days=c["days"]),
396 snapshot_id=_sha(f"snap-narr-nocturne-fatou-{i}"),
397 ))
398 fatou_prev = cid
399 fatou_head = _sha("narr-nocturne-fatou-2")
400
401 # Branches
402 db.add(MusehubBranch(repo_id=REPO_NOCTURNE, name="main", head_commit_id=base_commit))
403 db.add(MusehubBranch(repo_id=REPO_NOCTURNE, name="feat/coltrane-harmony", head_commit_id=yuki_head))
404 db.add(MusehubBranch(repo_id=REPO_NOCTURNE, name="feat/afro-rhythm", head_commit_id=fatou_head))
405
406 # PRs — both open, conflict in piano.mid
407 pr_yuki_id = _uid("narr-pr-nocturne-yuki")
408 pr_fatou_id = _uid("narr-pr-nocturne-fatou")
409
410 db.add(MusehubPullRequest(
411 pr_id=pr_yuki_id,
412 repo_id=REPO_NOCTURNE,
413 title="Feat: Coltrane jazz harmony layer — quartal voicings + subs",
414 body=(
415 "Layers Coltrane-inspired reharmonisation beneath Chopin's cantilena.\n\n"
416 "Key changes:\n"
417 "- Piano voicings → quartal stacks (McCoy Tyner style)\n"
418 "- Tritone sub on bar 4 turnaround\n"
419 "- ii-V-I substitution into Eb7#11\n\n"
420 "⚠️ Conflict with feat/afro-rhythm on `tracks/piano.mid` — "
421 "both branches modified the piano track. Needs resolution discussion."
422 ),
423 state="merged",
424 from_branch="feat/coltrane-harmony",
425 to_branch="main",
426 merge_commit_id=_sha("narr-nocturne-merge"),
427 merged_at=_now(days=38),
428 author="yuki",
429 created_at=_now(days=42),
430 ))
431
432 db.add(MusehubPullRequest(
433 pr_id=pr_fatou_id,
434 repo_id=REPO_NOCTURNE,
435 title="Feat: Afro-Cuban rhythmic reinterpretation — clave + bata",
436 body=(
437 "Adds Afro-Cuban percussion and bass under the nocturne.\n\n"
438 "Key changes:\n"
439 "- Clave pattern (3-2 son clave) under Chopin melody\n"
440 "- Fender bass groove following harmonic rhythm\n"
441 "- Bata drum call-and-response in bridge section\n\n"
442 "⚠️ Conflict with feat/coltrane-harmony on `tracks/piano.mid` — "
443 "we both touch the piano accompaniment register."
444 ),
445 state="merged",
446 from_branch="feat/afro-rhythm",
447 to_branch="main",
448 merge_commit_id=_sha("narr-nocturne-merge"),
449 merged_at=_now(days=38),
450 author="fatou",
451 created_at=_now(days=42),
452 ))
453
454 # Merge commit (joint authorship)
455 db.add(MusehubCommit(
456 commit_id=_sha("narr-nocturne-merge"),
457 repo_id=REPO_NOCTURNE,
458 branch="main",
459 parent_ids=[yuki_head, fatou_head],
460 message=(
461 "merge: resolve piano.mid conflict — quartal voicings + clave coexist\n\n"
462 "Co-authored-by: yuki <yuki@muse.app>\n"
463 "Co-authored-by: fatou <fatou@muse.app>\n\n"
464 "Resolution: yuki's quartal voicings moved to octave above middle C, "
465 "fatou's bass register kept below C3. No spectral overlap."
466 ),
467 author="pierre",
468 timestamp=_now(days=38),
469 snapshot_id=_sha("snap-narr-nocturne-merge"),
470 ))
471
472 # 20-comment resolution debate (on yuki's PR as the primary thread)
473 resolution_debate: list[tuple[str, str]] = [
474 ("pierre", "Both PRs touch `tracks/piano.mid`. We have a conflict. Let's figure out the musical resolution before forcing a merge."),
475 ("yuki", "My quartal voicings sit in the mid-register piano — bar 2, beats 1-3. Fatou, where exactly does your bass line land?"),
476 ("fatou", "Fender bass is C2-E3. The clave lives on a separate track. The conflict is actually just the piano LH accompaniment — I moved the arpeggios to staccato comping."),
477 ("yuki", "Staccato comping could work. But if you changed the LH *rhythm*, my tritone sub on bar 4 might clash harmonically — I'm expecting the 6/4 swing from pierre's original."),
478 ("pierre", "Fatou, can you share the specific beats you're hitting on the piano comping?"),
479 ("fatou", "Beats 1, 2.5, 3.5 — anticipating the clave pattern. Think of it as the piano agreeing with the clave rather than fighting it."),
480 ("yuki", "That's actually beautiful. My Coltrane sub lands on beat 4 — we don't collide at all if your comping leaves beat 4 open."),
481 ("fatou", "I can leave beat 4 open. That gives your tritone sub space to breathe."),
482 ("pierre", "I'm starting to hear this. The piano is playing two roles at once — jazz harmony AND Afro-Cuban accompaniment. That's the whole concept of the piece."),
483 ("aaliya", "Following this thread and I love where it's going. Chopin's nocturne melody stays pure, but underneath it's having a conversation between two entirely different rhythmic traditions."),
484 ("yuki", "Pierre, are you comfortable with us both modifying your LH part? I want to make sure the cantilena stays yours."),
485 ("pierre", "The melody is the nocturne. The accompaniment can transform. Bach's continuo players improvised freely — this is the same spirit."),
486 ("fatou", "That's a beautiful way to frame it. OK — I'll rebase onto yuki's branch, adjust my piano comping to leave beat 4 open, and we resolve the conflict manually."),
487 ("yuki", "Perfect. I'll pull fatou's rhythm track as-is and just make sure my voicings don't occupy the bass register below C3."),
488 ("pierre", "This is what open-source composition should look like. Three people, two traditions, one piece of music."),
489 ("sofia", "I've been watching this. The resolution you've found is genuinely innovative — quartal jazz harmony + clave + Chopin cantilena. That's a new genre."),
490 ("yuki", "sofia — we're calling it 'Nocturn-é' for now. A nocturne that refuses to stay in one century."),
491 ("fatou", "I added a comment to the merge commit explaining the register split. pierre, can you do the final merge? Joint authorship commit."),
492 ("pierre", "Done. Merged with joint authorship. Both of you are credited in the commit message. Release coming."),
493 ("aaliya", "🔥 This is the most interesting thing on MuseHub right now."),
494 ]
495 pr_parent: str | None = None
496 for i, comment in enumerate(resolution_debate):
497 author, body = comment
498 cid = _uid(f"narr-pr-comment-nocturne-{i}")
499 db.add(MusehubPRComment(
500 comment_id=cid,
501 pr_id=pr_yuki_id,
502 repo_id=REPO_NOCTURNE,
503 author=author,
504 body=body,
505 target_type="general",
506 parent_comment_id=None,
507 created_at=_now(days=42 - i // 2),
508 ))
509
510 # Release
511 db.add(MusehubRelease(
512 repo_id=REPO_NOCTURNE,
513 tag="v1.0.0",
514 title="Nocturne Op.9 No.2 (Jazz Edition)",
515 body=(
516 "## Nocturne Op.9 No.2 (Jazz Edition) — v1.0.0\n\n"
517 "Chopin meets Coltrane, mediated by Afro-Cuban rhythm.\n\n"
518 "### Musicians\n"
519 "- **pierre** — Chopin melody (cantilena), piano LH framework\n"
520 "- **yuki** — Jazz harmony (Coltrane substitutions, quartal voicings)\n"
521 "- **fatou** — Afro-Cuban rhythm (clave, bata, Fender bass)\n\n"
522 "### Technical resolution\n"
523 "3-way merge conflict resolved by register split: "
524 "yuki's voicings above C4, fatou's bass below C3, beat 4 left open "
525 "for the tritone substitution.\n\n"
526 "### Downloads\nMIDI bundle, MP3 stereo mix, stems by instrument family"
527 ),
528 commit_id=_sha("narr-nocturne-merge"),
529 download_urls={
530 "midi_bundle": f"/releases/{REPO_NOCTURNE}-v1.0.0.zip",
531 "mp3": f"/releases/{REPO_NOCTURNE}-v1.0.0.mp3",
532 "stems": f"/releases/{REPO_NOCTURNE}-v1.0.0-stems.zip",
533 },
534 author="pierre",
535 created_at=_now(days=37),
536 ))
537
538 print(" ✅ Scenario 2: Chopin+Coltrane — 20-comment 3-way merge resolution, jazz release")
539
540
541 # ---------------------------------------------------------------------------
542 # Scenario 3 — Ragtime EDM Collab
543 # ---------------------------------------------------------------------------
544
545 async def _seed_ragtime_edm(db: AsyncSession) -> None:
546 """Ragtime EDM Collab: marcus, fatou, aaliya co-author a ragtime-EDM fusion.
547 8 commits across 3 participants. fatou's polyrhythm commit gets 🔥👏😢 reactions.
548 'Maple Leaf Drops' release.
549 """
550 db.add(MusehubRepo(
551 repo_id=REPO_RAGTIME_EDM,
552 name="Maple Leaf Drops",
553 owner="marcus",
554 slug="maple-leaf-drops",
555 owner_user_id=MARCUS,
556 visibility="public",
557 description="Joplin meets Berghain. Ragtime piano structures over 4/4 techno pulse.",
558 tags=["ragtime", "EDM", "Joplin", "techno", "fusion", "piano"],
559 key_signature="Ab major",
560 tempo_bpm=130,
561 created_at=_now(days=40),
562 ))
563
564 # 8 commits, 3 authors
565 ragtime_commits: list[dict[str, Any]] = [
566 dict(message="init: Maple Leaf Rag skeleton at 130 BPM — piano only", author="marcus", days=40),
567 dict(message="feat(synth): techno bass pulse — four-on-the-floor under ragtime", author="marcus", days=38),
568 dict(message="feat(drums): 909 kick + clap replacing ragtime march snare", author="aaliya", days=36),
569 dict(message="feat(piano): Joplin B section — 4-bar strain over synth bass", author="marcus", days=34),
570 dict(message="feat(perc): West African polyrhythm layer — fatou's surprise", author="fatou", days=32),
571 dict(message="feat(synth): acid 303 line weaving through ragtime changes", author="aaliya", days=30),
572 dict(message="refactor(drums): sidechained kick — duck the piano on beat 1", author="marcus", days=28),
573 dict(message="feat(breakdown): 8-bar ragtime-only breakdown before drop", author="fatou", days=26),
574 ]
575 prev_cid: str | None = None
576 for i, c in enumerate(ragtime_commits):
577 cid = _sha(f"narr-ragtime-{i}")
578 db.add(MusehubCommit(
579 commit_id=cid,
580 repo_id=REPO_RAGTIME_EDM,
581 branch="main",
582 parent_ids=[prev_cid] if prev_cid else [],
583 message=c["message"],
584 author=c["author"],
585 timestamp=_now(days=c["days"]),
586 snapshot_id=_sha(f"snap-narr-ragtime-{i}"),
587 ))
588 prev_cid = cid
589
590 db.add(MusehubBranch(
591 repo_id=REPO_RAGTIME_EDM,
592 name="main",
593 head_commit_id=_sha("narr-ragtime-7"),
594 ))
595
596 # Session — 3-participant collab
597 db.add(MusehubSession(
598 session_id=_uid("narr-session-ragtime-1"),
599 repo_id=REPO_RAGTIME_EDM,
600 started_at=_now(days=40),
601 ended_at=_now(days=40, hours=-4),
602 participants=["marcus", "fatou", "aaliya"],
603 location="Electric Lady Studios (remote async)",
604 intent="Ragtime EDM fusion — lay down the structural skeleton",
605 commits=[_sha("narr-ragtime-0"), _sha("narr-ragtime-1")],
606 notes="Decided on 130 BPM — fast enough for floor, slow enough for ragtime syncopation.",
607 is_active=False,
608 created_at=_now(days=40),
609 ))
610 db.add(MusehubSession(
611 session_id=_uid("narr-session-ragtime-2"),
612 repo_id=REPO_RAGTIME_EDM,
613 started_at=_now(days=32),
614 ended_at=_now(days=32, hours=-5),
615 participants=["marcus", "fatou", "aaliya"],
616 location="Remote",
617 intent="fatou's polyrhythm layer + aaliya's acid line",
618 commits=[
619 _sha("narr-ragtime-4"),
620 _sha("narr-ragtime-5"),
621 _sha("narr-ragtime-6"),
622 ],
623 notes="fatou's polyrhythm commit caused spontaneous celebration in the call.",
624 is_active=False,
625 created_at=_now(days=32),
626 ))
627
628 # Reactions on fatou's polyrhythm commit (commit index 4)
629 fatou_commit_id = _sha("narr-ragtime-4")
630 fatou_reactions = [
631 (MARCUS, "🔥"),
632 (AALIYA, "🔥"),
633 (GABRIEL, "🔥"),
634 (SOFIA, "🔥"),
635 (MARCUS, "👏"),
636 (AALIYA, "👏"),
637 (YUKI, "👏"),
638 (PIERRE, "👏"),
639 (CHEN, "😢"), # chen weeps for the death of pure ragtime
640 (FATOU, "😢"), # fatou weeps tears of joy
641 ]
642 for user_id, emoji in fatou_reactions:
643 try:
644 db.add(MusehubReaction(
645 reaction_id=_uid(f"narr-reaction-ragtime-{fatou_commit_id[:8]}-{user_id}-{emoji}"),
646 repo_id=REPO_RAGTIME_EDM,
647 target_type="commit",
648 target_id=fatou_commit_id,
649 user_id=user_id,
650 emoji=emoji,
651 created_at=_now(days=32),
652 ))
653 except Exception:
654 pass
655
656 # Comments on fatou's commit
657 fatou_comments: list[tuple[str, str]] = [
658 ("marcus", "Fatou WHAT. I was not expecting this. This is everything."),
659 ("aaliya", "This polyrhythm layer turns the whole track into something else entirely. Ragtime was just the scaffolding — this is the music."),
660 ("gabriel", "The way the West African pattern locks into the ragtime's inherent syncopation is... I need to sit with this."),
661 ("yuki", "The spectral relationship between the 130 BPM techno grid and the polyrhythm's implied triple meter creates a beautiful tension. I hear 3-against-4 every 8 bars."),
662 ("chen", "For the record — I think this might be too much. Joplin's architecture is already polyrhythmic. Adding another layer might obscure rather than enhance. (Still reacting with 😢 because I'm moved either way.)"),
663 ("fatou", "Chen — fair criticism. The polyrhythm is deliberately *in front* of the mix here. I can pull it back -4 dB in the final mix so it's felt rather than heard."),
664 ]
665 for i, (author, body) in enumerate(fatou_comments):
666 db.add(MusehubComment(
667 comment_id=_uid(f"narr-comment-ragtime-fatou-{i}"),
668 repo_id=REPO_RAGTIME_EDM,
669 target_type="commit",
670 target_id=fatou_commit_id,
671 author=author,
672 body=body,
673 created_at=_now(days=32, hours=-i),
674 ))
675
676 # Release
677 db.add(MusehubRelease(
678 repo_id=REPO_RAGTIME_EDM,
679 tag="v1.0.0",
680 title="Maple Leaf Drops",
681 body=(
682 "## Maple Leaf Drops — v1.0.0\n\n"
683 "Scott Joplin didn't live to see techno. We fixed that.\n\n"
684 "### Musicians\n"
685 "- **marcus** — Piano (Joplin arrangements), production, 909 drums\n"
686 "- **fatou** — West African polyrhythm layer, breakdown arrangement\n"
687 "- **aaliya** — Acid 303 line, 909 arrangement, sidechain design\n\n"
688 "### Highlights\n"
689 "- Joplin's Maple Leaf Rag chord structures at 130 BPM\n"
690 "- 4-on-the-floor kick beneath Joplin's inherent 3-against-4\n"
691 "- fatou's polyrhythm layer — the spiritual centre of the track\n"
692 "- 8-bar ragtime-only breakdown before the final drop\n\n"
693 "### Formats\nMIDI bundle, MP3 club master (-1 LUFS), stems"
694 ),
695 commit_id=_sha("narr-ragtime-7"),
696 download_urls={
697 "midi_bundle": f"/releases/{REPO_RAGTIME_EDM}-v1.0.0.zip",
698 "mp3": f"/releases/{REPO_RAGTIME_EDM}-v1.0.0.mp3",
699 "stems": f"/releases/{REPO_RAGTIME_EDM}-v1.0.0-stems.zip",
700 },
701 author="marcus",
702 created_at=_now(days=24),
703 ))
704
705 print(" ✅ Scenario 3: Ragtime EDM Collab — 8 commits, fire reactions, Maple Leaf Drops release")
706
707
708 # ---------------------------------------------------------------------------
709 # Scenario 4 — Community Chaos
710 # ---------------------------------------------------------------------------
711
712 async def _seed_community_chaos(db: AsyncSession) -> None:
713 """Community Chaos: 5 simultaneous open PRs on 'community-jam', a 25-comment
714 key signature debate on a central issue, 3 PRs in conflict state, and a
715 milestone at 70% completion.
716 """
717 db.add(MusehubRepo(
718 repo_id=REPO_COMMUNITY_JAM,
719 name="Community Jam Vol. 1",
720 owner="gabriel",
721 slug="community-jam-vol-1",
722 owner_user_id=GABRIEL,
723 visibility="public",
724 description="Open contribution jam — everyone adds something. Organised chaos.",
725 tags=["community", "collab", "open", "jam", "experimental"],
726 key_signature="C major", # the disputed key
727 tempo_bpm=95,
728 created_at=_now(days=50),
729 ))
730
731 # Base commits
732 base_commits: list[dict[str, Any]] = [
733 dict(message="init: community jam template — C major at 95 BPM", author="gabriel", days=50),
734 dict(message="feat(piano): opening vamp — community starting point", author="gabriel", days=48),
735 dict(message="feat(bass): walking bass line — open for all to build on", author="marcus", days=46),
736 dict(message="feat(drums): groove template — pocket at 95 BPM", author="fatou", days=44),
737 ]
738 jam_prev: str | None = None
739 for i, c in enumerate(base_commits):
740 cid = _sha(f"narr-community-{i}")
741 db.add(MusehubCommit(
742 commit_id=cid,
743 repo_id=REPO_COMMUNITY_JAM,
744 branch="main",
745 parent_ids=[jam_prev] if jam_prev else [],
746 message=c["message"],
747 author=c["author"],
748 timestamp=_now(days=c["days"]),
749 snapshot_id=_sha(f"snap-narr-community-{i}"),
750 ))
751 jam_prev = cid
752 db.add(MusehubBranch(
753 repo_id=REPO_COMMUNITY_JAM,
754 name="main",
755 head_commit_id=_sha("narr-community-3"),
756 ))
757
758 # 5 simultaneous open PRs
759 pr_configs: list[dict[str, Any]] = [
760 dict(n=0, title="Feat: modulate to A minor — more emotional depth",
761 body="C major is too bright for this jam. Relative minor gives it soul.",
762 author="yuki", branch="feat/a-minor-modal"),
763 dict(n=1, title="Feat: add jazz reharmonisation — ii-V-I substitutions",
764 body="The vamp needs harmonic movement. Jazz subs every 4 bars.",
765 author="marcus", branch="feat/jazz-reharmony"),
766 dict(n=2, title="Feat: Afrobeat key shift — G major groove",
767 body="G major sits better with the highlife guitar pattern I'm adding.",
768 author="aaliya", branch="feat/g-major-afrobeat"),
769 dict(n=3, title="Feat: microtonal drift — 31-TET temperament",
770 body="C major in equal temperament is compromised. 31-TET gives pure intervals.",
771 author="chen", branch="feat/31-tet"),
772 dict(n=4, title="Feat: stay in C major — add pedal point for tension",
773 body="The key is fine. Add a B pedal under the vamp for maximum dissonance.",
774 author="pierre", branch="feat/c-pedal-point"),
775 ]
776 pr_ids: list[str] = []
777 for pc in pr_configs:
778 pr_id = _uid(f"narr-community-pr-{pc['n']}")
779 pr_ids.append(pr_id)
780 # PRs 1, 2, 3 are in conflict
781 db.add(MusehubPullRequest(
782 pr_id=pr_id,
783 repo_id=REPO_COMMUNITY_JAM,
784 title=pc["title"],
785 body=pc["body"] + (
786 "\n\n⚠️ **CONFLICT**: Multiple PRs modify `tracks/piano.mid` and "
787 "`tracks/bass.mid`. Needs key signature resolution before merge."
788 if pc["n"] in (1, 2, 3) else ""
789 ),
790 state="open",
791 from_branch=pc["branch"],
792 to_branch="main",
793 author=pc["author"],
794 created_at=_now(days=40 - pc["n"] * 2),
795 ))
796
797 # The key signature debate issue (25 comments)
798 key_debate_issue_id = _uid("narr-community-issue-key-sig")
799 db.add(MusehubIssue(
800 issue_id=key_debate_issue_id,
801 repo_id=REPO_COMMUNITY_JAM,
802 number=1,
803 title="[DEBATE] Which key should Community Jam Vol. 1 be in?",
804 body=(
805 "We have 5 open PRs proposing 5 different keys. "
806 "This issue is the canonical place to resolve it. "
807 "Please make your case. Voting closes when we reach consensus or "
808 "gabriel makes a unilateral decision.\n\n"
809 "Current proposals:\n"
810 "- C major (original)\n"
811 "- A minor (relative minor)\n"
812 "- G major (Afrobeat fit)\n"
813 "- 31-TET C major (microtonal)\n"
814 "- C major + B pedal (stay, add tension)\n\n"
815 "Make. Your. Case."
816 ),
817 state="open",
818 labels=["key-signature", "debate", "community", "blocker"],
819 author="gabriel",
820 created_at=_now(days=38),
821 ))
822
823 # 25-comment key signature debate
824 key_debate: list[tuple[str, str]] = [
825 ("gabriel", "I opened C major as a neutral starting point — maximum accessibility. But I'm genuinely open to being overruled."),
826 ("yuki", "C major is a compositional blank slate — it's fine for exercises but emotionally empty. A minor gives us minor seconds, tritones, modal possibilities."),
827 ("marcus", "A minor is fine but the walking bass I wrote assumes C major changes. Any key shift means rewriting the bass line."),
828 ("aaliya", "G major suits the highlife pattern I'm adding. The guitar's open G string rings sympathetically with the root. It's not just theory — it's physics."),
829 ("chen", "Can we pause and acknowledge that 'key' in equal temperament is already a compromise? 31-TET gives us pure major thirds (386 cents vs 400 cents ET). The difference is audible."),
830 ("pierre", "Chen, I respect the microtonal argument but this is a community jam, not a tuning experiment. Most contributors can't work in 31-TET."),
831 ("chen", "That's a valid practical constraint. I'll withdraw 31-TET if we at least acknowledge it as the theoretically superior option."),
832 ("fatou", "Practically speaking — I've been writing drums. Drums don't care what key you're in. But the bass line does. Marcus, can you adapt the walking bass to G major?"),
833 ("marcus", "Walking bass in G major works. But then yuki's jazz subs don't resolve correctly — they're written for C major ii-V-I."),
834 ("yuki", "My jazz subs work in any key if you transpose. The *structure* is the same. But I'd need to redo the voice leading."),
835 ("aaliya", "This is the problem — every key choice benefits some contributors and costs others. We need a decision principle, not a debate."),
836 ("gabriel", "Decision principle: the key that requires the least total rework across all active PRs. Can everyone estimate their rework in hours?"),
837 ("marcus", "C major → no rework. G major → 2 hours. A minor → 1 hour."),
838 ("yuki", "C major → no rework (jazz subs already written). A minor → 30 min transpose. G major → 1 hour."),
839 ("aaliya", "C major → 3 hours (re-record guitar in sympathetic key). G major → 0 rework. A minor → 2 hours."),
840 ("chen", "C major → 0 rework (I'm withdrawing 31-TET). G major → 0 rework (I have no key-specific content yet)."),
841 ("pierre", "C major → 0. G major → 1 hour. A minor → 30 min."),
842 ("fatou", "C major → 0. G major → 0. A minor → 0."),
843 ("gabriel", "Total rework: C major = 3h, G major = 3h, A minor = 4h. It's a tie between C and G."),
844 ("aaliya", "Then G major wins on musical grounds — it gives the Afrobeat guitar its natural resonance AND ties with C on total effort."),
845 ("marcus", "I'll support G major if we accept that the walking bass rewrite is part of the scope."),
846 ("yuki", "G major. Fine. I'll transpose my jazz subs tonight."),
847 ("pierre", "G major it is. Though I still think the B pedal idea works better in C. I'll adapt."),
848 ("chen", "G major. I'll note for the record that G major in 31-TET is 19 steps of the 31-tone octave. Just saying."),
849 ("gabriel", "We have consensus: G major, 95 BPM. Aaliya's PR (#3) is the base. All other PRs should rebase onto her branch. Closing the debate. Thanks everyone — this is what community composition looks like."),
850 ]
851 issue_parent: str | None = None
852 for i, comment in enumerate(key_debate):
853 author, body = comment
854 cid = _uid(f"narr-community-debate-{i}")
855 db.add(MusehubIssueComment(
856 comment_id=cid,
857 issue_id=key_debate_issue_id,
858 repo_id=REPO_COMMUNITY_JAM,
859 author=author,
860 body=body,
861 parent_id=None,
862 musical_refs=[],
863 created_at=_now(days=38 - i // 2),
864 ))
865
866 # Milestone at 70% completion (14/20 tasks done)
867 milestone_id = _uid("narr-community-milestone-1")
868 db.add(MusehubMilestone(
869 milestone_id=milestone_id,
870 repo_id=REPO_COMMUNITY_JAM,
871 number=1,
872 title="Community Jam Vol. 1 — Full Release",
873 description="All tracks recorded, key signature resolved, final mix complete.",
874 state="open",
875 author="gabriel",
876 due_on=_now(days=-14), # 2 weeks from now
877 created_at=_now(days=50),
878 ))
879
880 # Issues tracking milestone tasks (14 closed = 70%, 6 open = 30%)
881 milestone_tasks: list[dict[str, Any]] = [
882 # Closed (done)
883 dict(n=2, title="Set tempo at 95 BPM", state="closed", labels=["done"]),
884 dict(n=3, title="Piano vamp template", state="closed", labels=["done"]),
885 dict(n=4, title="Walking bass template", state="closed", labels=["done"]),
886 dict(n=5, title="Drums groove template", state="closed", labels=["done"]),
887 dict(n=6, title="Define song structure (AABA)", state="closed", labels=["done"]),
888 dict(n=7, title="Record piano intro 4 bars", state="closed", labels=["done"]),
889 dict(n=8, title="Record piano A section", state="closed", labels=["done"]),
890 dict(n=9, title="Record piano B section", state="closed", labels=["done"]),
891 dict(n=10, title="Record piano outro", state="closed", labels=["done"]),
892 dict(n=11, title="Drum arrangement complete", state="closed", labels=["done"]),
893 dict(n=12, title="Bass line complete", state="closed", labels=["done"]),
894 dict(n=13, title="Afrobeat guitar layer", state="closed", labels=["done"]),
895 dict(n=14, title="Acid 303 layer", state="closed", labels=["done"]),
896 dict(n=15, title="Resolve key signature debate", state="closed", labels=["done"]),
897 # Open (30% remaining)
898 dict(n=16, title="Rebase all PRs onto G major", state="open", labels=["blocked"]),
899 dict(n=17, title="Jazz reharmonisation (yuki)", state="open", labels=["in-progress"]),
900 dict(n=18, title="Microtonal spice layer (chen)", state="open", labels=["in-progress"]),
901 dict(n=19, title="Final mix and master", state="open", labels=["todo"]),
902 dict(n=20, title="Release v1.0.0", state="open", labels=["todo"]),
903 dict(n=21, title="Write liner notes", state="open", labels=["todo"]),
904 ]
905 for task in milestone_tasks:
906 db.add(MusehubIssue(
907 repo_id=REPO_COMMUNITY_JAM,
908 number=task["n"],
909 title=task["title"],
910 body=f"Milestone task: {task['title']}",
911 state=task["state"],
912 labels=task["labels"],
913 author="gabriel",
914 milestone_id=milestone_id,
915 created_at=_now(days=50 - task["n"]),
916 ))
917
918 print(" ✅ Scenario 4: Community Chaos — 5 open PRs, 25-comment debate, 70% milestone")
919
920
921 # ---------------------------------------------------------------------------
922 # Scenario 5 — Goldberg Milestone
923 # ---------------------------------------------------------------------------
924
925 async def _seed_goldberg_milestone(db: AsyncSession) -> None:
926 """Goldberg Milestone: gabriel's 30-variation Goldberg project. 28/30 done.
927 Variation 25 has an 18-comment debate (slow vs fast). Variation 29 PR
928 with yuki requesting 'more ornamentation'.
929 """
930 db.add(MusehubRepo(
931 repo_id=REPO_GOLDBERG,
932 name="Goldberg Variations (Complete)",
933 owner="gabriel",
934 slug="goldberg-variations",
935 owner_user_id=GABRIEL,
936 visibility="public",
937 description=(
938 "Bach's Goldberg Variations — all 30. "
939 "Aria + 30 variations + Aria da capo. "
940 "Each variation is a separate branch, PR, and closed issue."
941 ),
942 tags=["Bach", "Goldberg", "variations", "harpsichord", "G major", "classical"],
943 key_signature="G major",
944 tempo_bpm=80,
945 created_at=_now(days=120),
946 ))
947
948 # Base commit — the Aria
949 db.add(MusehubCommit(
950 commit_id=_sha("narr-goldberg-aria"),
951 repo_id=REPO_GOLDBERG,
952 branch="main",
953 parent_ids=[],
954 message="init: Aria — G major sarabande, 32-bar binary form",
955 author="gabriel",
956 timestamp=_now(days=120),
957 snapshot_id=_sha("snap-narr-goldberg-aria"),
958 ))
959 db.add(MusehubBranch(
960 repo_id=REPO_GOLDBERG,
961 name="main",
962 head_commit_id=_sha("narr-goldberg-aria"),
963 ))
964
965 # Milestone for all 30 variations
966 milestone_id = _uid("narr-goldberg-milestone-30")
967 db.add(MusehubMilestone(
968 milestone_id=milestone_id,
969 repo_id=REPO_GOLDBERG,
970 number=1,
971 title="All 30 Variations Complete",
972 description=(
973 "Track progress through all 30 Goldberg variations. "
974 "Each variation gets its own issue, branch, commits, and PR. "
975 "28/30 complete."
976 ),
977 state="open",
978 author="gabriel",
979 due_on=_now(days=-30),
980 created_at=_now(days=120),
981 ))
982
983 # 28 closed variation issues + Var 25 issue with 18-comment debate
984 # + Var 29 issue (open) + Var 30 issue (open)
985 variation_metadata: dict[int, dict[str, str]] = {
986 1: dict(style="canon at the octave", key="G major", character="simple two-voice canon"),
987 2: dict(style="free composition", key="G major", character="crisp toccata-like passages"),
988 3: dict(style="canon at the ninth", key="G major", character="flowing melodic lines"),
989 4: dict(style="passepied", key="G major", character="brisk dance feel"),
990 5: dict(style="free with hand-crossing", key="G major", character="brilliant hand-crossing"),
991 6: dict(style="canon at the seventh", key="G major", character="lyrical canon"),
992 7: dict(style="gigue", key="G major", character="jig rhythm, 6/8"),
993 8: dict(style="free with hand-crossing", key="G major", character="energetic, sparkling"),
994 9: dict(style="canon at the fifth", key="E minor", character="tender, intimate"),
995 10: dict(style="fughetta", key="G major", character="four-voice fugue sketch"),
996 11: dict(style="free with hand-crossing", key="G major", character="rapid hand-crossing"),
997 12: dict(style="canon at the fourth", key="G major", character="inverted canon"),
998 13: dict(style="free", key="G major", character="gentle, flowing ornaments"),
999 14: dict(style="free with hand-crossing", key="G major", character="brilliant two-voice"),
1000 15: dict(style="canon at the fifth (inverted)", key="G minor", character="profound, slow"),
1001 16: dict(style="overture", key="G major", character="French overture style"),
1002 17: dict(style="free", key="G major", character="syncopated, off-beat accents"),
1003 18: dict(style="canon at the sixth", key="G major", character="stately canon"),
1004 19: dict(style="minuet", key="G major", character="elegant dance"),
1005 20: dict(style="free with hand-crossing", key="G major", character="percussive virtuosity"),
1006 21: dict(style="canon at the seventh", key="G minor", character="contemplative, modal"),
1007 22: dict(style="alla breve", key="G major", character="learned counterpoint"),
1008 23: dict(style="free", key="G major", character="brilliant, showy"),
1009 24: dict(style="canon at the octave", key="G major", character="graceful, symmetrical"),
1010 25: dict(style="adagio (siciliana)", key="G minor", character="profoundly expressive, slow"),
1011 26: dict(style="free with hand-crossing", key="G major", character="rapid parallel thirds"),
1012 27: dict(style="canon at the ninth", key="G major", character="clear, clean"),
1013 28: dict(style="free with trills", key="G major", character="trill-saturated virtuosity"),
1014 29: dict(style="quodlibet", key="G major", character="folk songs woven into counterpoint"),
1015 30: dict(style="aria da capo", key="G major", character="return of the opening Aria"),
1016 }
1017
1018 for n in range(1, 31):
1019 meta = variation_metadata[n]
1020 is_done = n <= 28 # 28/30 complete
1021 issue_id = _uid(f"narr-goldberg-issue-var-{n}")
1022
1023 db.add(MusehubIssue(
1024 issue_id=issue_id,
1025 repo_id=REPO_GOLDBERG,
1026 number=n,
1027 title=f"Variation {n} — {meta['style']}",
1028 body=(
1029 f"**Style:** {meta['style']}\n"
1030 f"**Key:** {meta['key']}\n"
1031 f"**Character:** {meta['character']}\n\n"
1032 f"Acceptance criteria:\n"
1033 f"- [ ] Correct ornaments (trills, mordents, turns)\n"
1034 f"- [ ] Correct articulation (slurs, staccati)\n"
1035 f"- [ ] Voice leading reviewed\n"
1036 f"- [ ] Tempo marking verified against Urtext"
1037 ),
1038 state="closed" if is_done else "open",
1039 labels=["variation", f"variation-{n}", meta["key"].replace(" ", "-").lower()],
1040 author="gabriel",
1041 milestone_id=milestone_id,
1042 created_at=_now(days=120 - n * 3),
1043 ))
1044
1045 # Add commits for closed variations
1046 if is_done:
1047 commit_id = _sha(f"narr-goldberg-var-{n}")
1048 db.add(MusehubCommit(
1049 commit_id=commit_id,
1050 repo_id=REPO_GOLDBERG,
1051 branch="main",
1052 parent_ids=[_sha(f"narr-goldberg-var-{n-1}") if n > 1 else _sha("narr-goldberg-aria")],
1053 message=f"feat(var{n}): Variation {n} — {meta['style']} — {meta['character']}",
1054 author="gabriel",
1055 timestamp=_now(days=120 - n * 3 + 1),
1056 snapshot_id=_sha(f"snap-narr-goldberg-var-{n}"),
1057 ))
1058
1059 # Variation 25 debate — 18 comments (slow vs fast tempo)
1060 var25_issue_id = _uid("narr-goldberg-issue-var-25")
1061 var25_debate: list[tuple[str, str]] = [
1062 ("gabriel", "Variation 25 is the emotional heart of the Goldberg. G minor, adagio. I'm recording at ♩=44. Is this too slow?"),
1063 ("pierre", "Glenn Gould's 1981 recording is ♩=40. Rosalyn Tureck goes as slow as ♩=36. ♩=44 is actually on the fast side for this variation."),
1064 ("sofia", "♩=44 is where I'd want it too. Below ♩=40 and the ornaments lose their shape — the trills become sludge."),
1065 ("yuki", "The ornaments at ♩=44 have to be measured very precisely or they rush. Have you tried recording with a click and then humanizing?"),
1066 ("gabriel", "I'm recording against a click at ♩=44 but the 32nd-note ornaments in bar 13 feel mechanical. Maybe ♩=40 gives them more breathing room?"),
1067 ("pierre", "♩=40 is where the silence *between* ornament notes starts to breathe. That's where this variation lives — in the silence."),
1068 ("sofia", "But the melodic line can't be too fragmentary. There's a balance between ornament breathing room and melodic continuity."),
1069 ("marcus", "From a production perspective — at ♩=40 the decay of each harpsichord note is long enough that adjacent notes overlap in the reverb. That creates a natural legato even on a plucked instrument."),
1070 ("gabriel", "That's a point I hadn't considered. The harpsichord's decay effectively determines the minimum tempo for legato character."),
1071 ("chen", "The acoustics of the instrument being used matter here. What's the decay time on your harpsichord model? At what tempo does the decay of beat 1 reach -60 dB before beat 2 hits?"),
1072 ("gabriel", "Good question — with the harpsichord sample I'm using, decay reaches -60 dB in about 1.2 seconds. At ♩=40, a quarter note = 1.5 seconds. So there IS overlap."),
1073 ("marcus", "At ♩=44, quarter note = 1.36 seconds, so barely any overlap. ♩=40 gives you legato for free."),
1074 ("pierre", "And the ornamentation question answers itself — at ♩=40, the ornaments land in the natural decay tail of the preceding note. They're not rushed because they're riding the resonance."),
1075 ("yuki", "This is the right analysis. ♩=40. The ornaments will feel inevitable rather than imposed."),
1076 ("sofia", "Agreed. ♩=40 and let the harpsichord's physics make the artistic decision."),
1077 ("gabriel", "OK — ♩=40 it is. I'll re-record bar 13 and the coda. Thank you for this. The physics argument is the one that convinced me."),
1078 ("pierre", "Document the tempo decision in the commit message. Future contributors will wonder why ♩=40 and not ♩=44."),
1079 ("gabriel", "Already done: 'tempo: adagio at q=40 — harpsichord decay overlap creates natural legato; ornaments ride resonance tail (see issue #25)'"),
1080 ]
1081 for i, comment in enumerate(var25_debate):
1082 author, body = comment
1083 db.add(MusehubIssueComment(
1084 comment_id=_uid(f"narr-goldberg-var25-comment-{i}"),
1085 issue_id=var25_issue_id,
1086 repo_id=REPO_GOLDBERG,
1087 author=author,
1088 body=body,
1089 parent_id=None,
1090 musical_refs=[],
1091 created_at=_now(days=120 - 25 * 3 + i // 3),
1092 ))
1093
1094 # Variation 29 PR — open, yuki requests more ornamentation
1095 var29_pr_id = _uid("narr-goldberg-pr-var-29")
1096 db.add(MusehubCommit(
1097 commit_id=_sha("narr-goldberg-var-29-draft"),
1098 repo_id=REPO_GOLDBERG,
1099 branch="feat/var-29-quodlibet",
1100 parent_ids=[_sha("narr-goldberg-var-28")],
1101 message="feat(var29): Variation 29 draft — quodlibet with folk songs",
1102 author="gabriel",
1103 timestamp=_now(days=8),
1104 snapshot_id=_sha("snap-narr-goldberg-var-29-draft"),
1105 ))
1106 db.add(MusehubBranch(
1107 repo_id=REPO_GOLDBERG,
1108 name="feat/var-29-quodlibet",
1109 head_commit_id=_sha("narr-goldberg-var-29-draft"),
1110 ))
1111 db.add(MusehubPullRequest(
1112 pr_id=var29_pr_id,
1113 repo_id=REPO_GOLDBERG,
1114 title="Feat: Variation 29 — Quodlibet (folk songs in counterpoint)",
1115 body=(
1116 "## Variation 29 — Quodlibet\n\n"
1117 "Bach's joke variation — two folk songs ('I've been so long away from you' "
1118 "and 'Cabbage and turnips') woven into strict four-voice counterpoint.\n\n"
1119 "## What's here\n"
1120 "- Folk song 1: soprano voice, bars 1-8\n"
1121 "- Folk song 2: alto voice, bars 1-8\n"
1122 "- Bass line: walking quodlibet bass\n"
1123 "- Tenor: free counterpoint bridging folk tunes\n\n"
1124 "## What needs work\n"
1125 "- Ornamentation is sparse — I've not yet added the trills and mordents\n"
1126 " that appear in the Urtext\n"
1127 "- Need a review from yuki on ornament placement\n\n"
1128 "Closes #29"
1129 ),
1130 state="open",
1131 from_branch="feat/var-29-quodlibet",
1132 to_branch="main",
1133 author="gabriel",
1134 created_at=_now(days=7),
1135 ))
1136
1137 # yuki's review — requesting more ornamentation
1138 yuki_review_id = _uid("narr-goldberg-review-yuki-var29")
1139 db.add(MusehubPRReview(
1140 id=yuki_review_id,
1141 pr_id=var29_pr_id,
1142 reviewer_username="yuki",
1143 state="changes_requested",
1144 body=(
1145 "The counterpoint structure is clean and the folk song placements are musically correct. "
1146 "However, the ornamentation is *significantly* under-specified. "
1147 "This is the penultimate variation — listeners have been through 28 variations of "
1148 "increasing complexity. They expect maximum ornamental density here before the Aria returns.\n\n"
1149 "**Specific requests:**\n\n"
1150 "1. Bar 3, soprano, beat 2: add a double mordent (pralltriller) on the C\n"
1151 "2. Bar 5, alto, beat 1: the folk melody needs a trill on the leading tone\n"
1152 "3. Bar 7-8: the authentic cadence needs an ornamental turn (Doppelschlag) "
1153 "on the penultimate note — this is standard Baroque practice at cadences\n"
1154 "4. General: consider adding short appoggiaturas throughout to soften the "
1155 "counterpoint into something more human\n\n"
1156 "**Reference:** Look at Variation 13 — similar character, full ornamental treatment. "
1157 "That's the standard for this project.\n\n"
1158 "More ornamentation, please. This is Bach's joke variation — it should feel lavish, "
1159 "not ascetic."
1160 ),
1161 submitted_at=_now(days=6),
1162 created_at=_now(days=6),
1163 ))
1164
1165 # gabriel's response comment
1166 db.add(MusehubPRComment(
1167 comment_id=_uid("narr-goldberg-var29-gabriel-response"),
1168 pr_id=var29_pr_id,
1169 repo_id=REPO_GOLDBERG,
1170 author="gabriel",
1171 body=(
1172 "yuki — thank you for the detailed feedback. You're right that I was too conservative.\n\n"
1173 "I'll add:\n"
1174 "- Double mordent on bar 3 soprano C ✓\n"
1175 "- Trill on bar 5 alto leading tone ✓\n"
1176 "- Doppelschlag at bar 7-8 cadence ✓\n"
1177 "- Appoggiaturas throughout — going to listen to Var 13 again and match the density\n\n"
1178 "Give me 24 hours. The quodlibet deserves to arrive well-dressed."
1179 ),
1180 target_type="general",
1181 created_at=_now(days=5),
1182 ))
1183
1184 print(" ✅ Scenario 5: Goldberg Milestone — 28/30 done, Var 25 debate, Var 29 ornament review")
1185
1186
1187 # ---------------------------------------------------------------------------
1188 # Main orchestrator
1189 # ---------------------------------------------------------------------------
1190
1191 async def seed_narratives(db: AsyncSession, force: bool = False) -> None:
1192 """Seed all 5 narrative scenarios into the database.
1193
1194 Idempotent — checks for the sentinel repo before inserting. Pass ``force``
1195 to wipe existing narrative data and re-seed from scratch.
1196 """
1197 print("🎭 Seeding MuseHub narrative scenarios…")
1198
1199 result = await db.execute(
1200 text("SELECT COUNT(*) FROM musehub_repos WHERE repo_id = :rid"),
1201 {"rid": SENTINEL_REPO_ID},
1202 )
1203 already_seeded = (result.scalar() or 0) > 0
1204
1205 if already_seeded and not force:
1206 print(" ⚠️ Narrative scenarios already seeded — skipping. Pass --force to reseed.")
1207 return
1208
1209 if already_seeded and force:
1210 print(" 🗑 --force: clearing existing narrative data…")
1211 narrative_repos = [
1212 REPO_NEO_BAROQUE, REPO_NEO_BAROQUE_FORK,
1213 REPO_NOCTURNE,
1214 REPO_RAGTIME_EDM,
1215 REPO_COMMUNITY_JAM,
1216 REPO_GOLDBERG,
1217 ]
1218 # Delete dependent records first, then repos (cascade handles the rest)
1219 for rid in narrative_repos:
1220 await db.execute(text("DELETE FROM musehub_repos WHERE repo_id = :rid"), {"rid": rid})
1221 await db.flush()
1222
1223 await _seed_bach_remix_war(db)
1224 await db.flush()
1225
1226 await _seed_chopin_coltrane(db)
1227 await db.flush()
1228
1229 await _seed_ragtime_edm(db)
1230 await db.flush()
1231
1232 await _seed_community_chaos(db)
1233 await db.flush()
1234
1235 await _seed_goldberg_milestone(db)
1236 await db.flush()
1237
1238 await db.commit()
1239
1240 print()
1241 print("=" * 72)
1242 print("🎭 NARRATIVE SCENARIOS — SEEDED")
1243 print("=" * 72)
1244 BASE = "http://localhost:10001/musehub/ui"
1245 print(f"\n1. Bach Remix War: {BASE}/gabriel/neo-baroque-counterpoint")
1246 print(f" Fork (trap): {BASE}/marcus/neo-baroque-counterpoint")
1247 print(f"\n2. Chopin+Coltrane: {BASE}/pierre/nocturne-op9-no2")
1248 print(f"\n3. Ragtime EDM: {BASE}/marcus/maple-leaf-drops")
1249 print(f"\n4. Community Chaos: {BASE}/gabriel/community-jam-vol-1")
1250 print(f"\n5. Goldberg Milestone: {BASE}/gabriel/goldberg-variations")
1251 print()
1252 print("=" * 72)
1253 print("✅ Narrative seed complete.")
1254 print("=" * 72)
1255
1256
1257 async def main() -> None:
1258 """Entry point — run all narrative scenarios."""
1259 force = "--force" in sys.argv
1260 db_url: str = settings.database_url or ""
1261 engine = create_async_engine(db_url, echo=False)
1262 async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) # type: ignore[call-overload] # SQLAlchemy: sessionmaker with class_=AsyncSession triggers call-overload false positive
1263 async with async_session() as db:
1264 await seed_narratives(db, force=force)
1265 await engine.dispose()
1266
1267
1268 if __name__ == "__main__":
1269 asyncio.run(main())