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