seed_narratives.py
python
| 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()) |