gabriel / musehub public
seed_musehub.py python
4044 lines 218.9 KB
2789c9ef Rename Docker service from 'muse' to 'musehub' everywhere Gabriel Cardona <gabriel@tellurstori.com> 6d ago
1 """MuseHub stress-test seed script.
2
3 Inserts a rich, realistic dataset that exercises every implemented URL,
4 every sidebar section, every social feature, every analytics panel, and
5 every discovery/explore surface.
6
7 Scale:
8 - 8 users (gabriel, sofia, marcus, yuki, aaliya, chen, fatou, pierre)
9 - 12 repos across 5 genres
10 - 30-50 commits per repo (with realistic branch history)
11 - 10+ issues per repo, mix of open/closed
12 - 4+ PRs per repo (open, merged, closed)
13 - 2-4 releases per repo
14 - 3-8 sessions per repo
15 - Comments, reactions, follows, watches, notifications, forks, view events
16 - Commit objects (tracks) with real instrument roles for the breakdown bar
17
18 Run inside the container:
19 docker compose exec musehub python3 /app/scripts/seed_musehub.py
20
21 Idempotent: pass --force to wipe and re-insert.
22 """
23 from __future__ import annotations
24
25 import asyncio
26 import hashlib
27 import json
28 import sys
29 import uuid
30 from datetime import datetime, timedelta, timezone
31 from typing import Any
32
33 from sqlalchemy import text
34 from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
35 from sqlalchemy.orm import sessionmaker
36
37 from musehub.config import settings
38 from musehub.contracts.json_types import NoteDict
39 from musehub.db.models import (
40 AccessToken,
41 Conversation,
42 ConversationMessage,
43 MessageAction,
44 UsageLog,
45 User,
46 )
47 from musehub.db.muse_models import NoteChange, Phrase, Variation
48 from musehub.db.musehub_collaborator_models import MusehubCollaborator
49 from musehub.db.musehub_label_models import (
50 MusehubIssueLabel,
51 MusehubLabel,
52 MusehubPRLabel,
53 )
54 from musehub.db.musehub_models import (
55 MusehubBranch,
56 MusehubComment,
57 MusehubCommit,
58 MusehubDownloadEvent,
59 MusehubEvent,
60 MusehubFollow,
61 MusehubFork,
62 MusehubIssue,
63 MusehubIssueComment,
64 MusehubIssueMilestone,
65 MusehubMilestone,
66 MusehubNotification,
67 MusehubObject,
68 MusehubPRComment,
69 MusehubPRReview,
70 MusehubProfile,
71 MusehubPullRequest,
72 MusehubReaction,
73 MusehubRelease,
74 MusehubReleaseAsset,
75 MusehubRenderJob,
76 MusehubRepo,
77 MusehubSession,
78 MusehubStar,
79 MusehubViewEvent,
80 MusehubWatch,
81 MusehubWebhook,
82 MusehubWebhookDelivery,
83 )
84 from musehub.db.musehub_stash_models import MusehubStash, MusehubStashEntry
85
86
87 # ---------------------------------------------------------------------------
88 # Helpers
89 # ---------------------------------------------------------------------------
90
91 UTC = timezone.utc
92
93
94 def _now(days: int = 0, hours: int = 0) -> datetime:
95 return datetime.now(tz=UTC) - timedelta(days=days, hours=hours)
96
97
98 def _sha(seed: str) -> str:
99 return hashlib.sha256(seed.encode()).hexdigest()
100
101
102 def _uid(seed: str) -> str:
103 return str(uuid.UUID(bytes=hashlib.md5(seed.encode()).digest()))
104
105
106 # ---------------------------------------------------------------------------
107 # Constants — stable IDs so URLs never change between re-seeds
108 # ---------------------------------------------------------------------------
109
110 # Users — original community
111 GABRIEL = "user-gabriel-001"
112 SOFIA = "user-sofia-002"
113 MARCUS = "user-marcus-003"
114 YUKI = "user-yuki-004"
115 AALIYA = "user-aaliya-005"
116 CHEN = "user-chen-006"
117 FATOU = "user-fatou-007"
118 PIERRE = "user-pierre-008"
119
120 USERS = [
121 (GABRIEL, "gabriel", "Composer & producer. Neo-soul, modal jazz, ambient. All sounds generated by Muse."),
122 (SOFIA, "sofia", "Classical-meets-electronic. Algorithmic composition. Lover of Nils Frahm."),
123 (MARCUS, "marcus", "Session keys player turned digital composer. Jazz, funk, everything in between."),
124 (YUKI, "yuki", "Tokyo-based sound designer. Granular synthesis, algorithmic rhythms, and noise art."),
125 (AALIYA, "aaliya", "Afrobeat, highlife, and jazz fusion. Lagos → Berlin. Always in motion."),
126 (CHEN, "chen", "Microtonal explorer. Just intonation, spectral music, extended techniques."),
127 (FATOU, "fatou", "Griots meet Moog. West African rhythms with modular synthesis."),
128 (PIERRE, "pierre", "French chanson meets minimalism. Piano, cello, and long silences."),
129 ]
130
131 # Users — historical composers and licensed artists (batch-13)
132 BACH = "user-bach-000000009"
133 CHOPIN = "user-chopin-00000010"
134 SCOTT_JOPLIN = "user-sjoplin-0000011"
135 KEVIN_MACLEOD = "user-kmacleod-000012"
136 KAI_ENGEL = "user-kaiengl-000013"
137
138 COMPOSER_USERS = [
139 (BACH, "bach", "Johann Sebastian Bach (1685-1750). Baroque counterpoint, fugue, and harmony. Archive upload by Muse community."),
140 (CHOPIN, "chopin", "Frédéric Chopin (1810-1849). Romantic piano. Nocturnes, études, ballades. Archive upload by Muse community."),
141 (SCOTT_JOPLIN, "scott_joplin", "Scott Joplin (1868-1917). King of Ragtime. Maple Leaf Rag, The Entertainer. Archive upload by Muse community."),
142 (KEVIN_MACLEOD, "kevin_macleod", "Kevin MacLeod. Cinematic, orchestral, ambient. Thousands of royalty-free compositions. incompetech.com."),
143 (KAI_ENGEL, "kai_engel", "Kai Engel. Ambient, neoclassical, cinematic. Delicate textures and patient melodies. Free Music Archive."),
144 ]
145
146 # Rich profile metadata for all users — display names, locations, CC attribution, social links.
147 # Keyed by user_id (stable VARCHAR(36) identifier). Used when seeding musehub_profiles.
148 # CC attribution: "Public Domain" for composers expired >70 yrs; "CC BY 4.0" for explicitly licensed artists.
149 PROFILE_DATA: dict[str, dict[str, str | bool | None]] = {
150 GABRIEL: dict(
151 display_name="Gabriel",
152 bio="Building the infinite music machine. Neo-baroque meets modern production. Every session is a new fugue.",
153 location="San Francisco, CA",
154 website_url="https://muse.app",
155 twitter_handle="gabriel",
156 is_verified=False,
157 cc_license=None,
158 ),
159 SOFIA: dict(
160 display_name="Sofia",
161 bio="Counterpoint scholar and baroque revival composer. Polyphony enthusiast. Bach is the north star.",
162 location="Vienna, Austria",
163 website_url="https://muse.app/sofia",
164 twitter_handle="sofia_counterpoint",
165 is_verified=False,
166 cc_license=None,
167 ),
168 MARCUS: dict(
169 display_name="Marcus",
170 bio="EDM producer. Sampling the classics. 808s and Scarlatti. Ragtime breakbeats are a real genre now.",
171 location="Detroit, MI",
172 website_url="https://muse.app/marcus",
173 twitter_handle="marcus_808",
174 is_verified=False,
175 cc_license=None,
176 ),
177 YUKI: dict(
178 display_name="Yuki",
179 bio="Music theorist and Muse power user. Harmonic analysis obsessive. Every chord has a reason.",
180 location="Tokyo, Japan",
181 website_url="https://muse.app/yuki",
182 twitter_handle="yuki_harmony",
183 is_verified=False,
184 cc_license=None,
185 ),
186 AALIYA: dict(
187 display_name="Aaliya",
188 bio="Jazz fusion meets romantic piano. Coltrane changes on Chopin. Lagos-born, Berlin-based.",
189 location="Berlin, Germany",
190 website_url="https://muse.app/aaliya",
191 twitter_handle="aaliya_jazzpiano",
192 is_verified=False,
193 cc_license=None,
194 ),
195 CHEN: dict(
196 display_name="Chen",
197 bio="Film composer. Every emotion has a chord. Every scene has a theme. Microtonal when the script demands.",
198 location="Shanghai, China",
199 website_url="https://muse.app/chen",
200 twitter_handle="chen_filmscore",
201 is_verified=False,
202 cc_license=None,
203 ),
204 FATOU: dict(
205 display_name="Fatou",
206 bio="Afrobeats composer. Polyrhythm is natural. 7 over 4 makes sense to me. Griot traditions meet modular synthesis.",
207 location="Dakar, Senegal",
208 website_url="https://muse.app/fatou",
209 twitter_handle="fatou_polyrhythm",
210 is_verified=False,
211 cc_license=None,
212 ),
213 PIERRE: dict(
214 display_name="Pierre",
215 bio="French chanson meets minimalism. Piano, cello, and long silences. Satie would approve.",
216 location="Paris, France",
217 website_url="https://muse.app/pierre",
218 twitter_handle="pierre_chanson",
219 is_verified=False,
220 cc_license=None,
221 ),
222 BACH: dict(
223 display_name="Johann Sebastian Bach",
224 bio="Baroque composer. 48 preludes, 48 fugues. All 24 keys. Music is the arithmetic of the soul.",
225 location="Leipzig, Saxony (1723–1750)",
226 website_url="https://www.bach-digital.de",
227 twitter_handle=None,
228 is_verified=True,
229 cc_license="Public Domain",
230 ),
231 CHOPIN: dict(
232 display_name="Frédéric Chopin",
233 bio="Romantic pianist. Nocturnes, ballades, études. Expressive beyond measure. The piano speaks in my voice.",
234 location="Paris, France (1831–1849)",
235 website_url="https://chopin.nifc.pl",
236 twitter_handle=None,
237 is_verified=True,
238 cc_license="Public Domain",
239 ),
240 SCOTT_JOPLIN: dict(
241 display_name="Scott Joplin",
242 bio="King of Ragtime. Maple Leaf Rag. The Entertainer. Syncopation is poetry in motion.",
243 location="Sedalia, Missouri (1890s)",
244 website_url="https://www.scottjoplin.org",
245 twitter_handle=None,
246 is_verified=True,
247 cc_license="Public Domain",
248 ),
249 KEVIN_MACLEOD: dict(
250 display_name="Kevin MacLeod",
251 bio="Prolific composer. Every genre. Royalty-free forever. CC BY 4.0. If you use my music, just credit me.",
252 location="Sandpoint, Idaho",
253 website_url="https://incompetech.com",
254 twitter_handle="kmacleod",
255 is_verified=True,
256 cc_license="CC BY 4.0",
257 ),
258 KAI_ENGEL: dict(
259 display_name="Kai Engel",
260 bio="Ambient architect. Long-form textures. Silence is also music. Free Music Archive.",
261 location="Germany",
262 website_url="https://freemusicarchive.org/music/Kai_Engel",
263 twitter_handle=None,
264 is_verified=True,
265 cc_license="CC BY 4.0",
266 ),
267 }
268
269 # All contributors for community-collab cycling (8 existing users)
270 ALL_CONTRIBUTORS = [
271 "gabriel", "sofia", "marcus", "yuki", "aaliya", "chen", "fatou", "pierre",
272 ]
273
274 # Repos — original community projects
275 REPO_NEO_SOUL = "repo-neo-soul-00000001"
276 REPO_MODAL_JAZZ = "repo-modal-jazz-000001"
277 REPO_AMBIENT = "repo-ambient-textures-1"
278 REPO_AFROBEAT = "repo-afrobeat-grooves-1"
279 REPO_MICROTONAL = "repo-microtonal-etudes1"
280 REPO_DRUM_MACHINE = "repo-drum-machine-00001"
281 REPO_CHANSON = "repo-chanson-minimale-1"
282 REPO_GRANULAR = "repo-granular-studies-1"
283 REPO_FUNK_SUITE = "repo-funk-suite-0000001"
284 REPO_JAZZ_TRIO = "repo-jazz-trio-0000001"
285 REPO_NEO_SOUL_FORK = "repo-neo-soul-fork-0001"
286 REPO_AMBIENT_FORK = "repo-ambient-fork-0001"
287
288 # Repos — 12 genre archive repos (batch-13)
289 REPO_WTC = "repo-well-tempered-cl01" # bach/well-tempered-clavier
290 REPO_GOLDBERG = "repo-goldberg-vars00001" # bach/goldberg-variations
291 REPO_NOCTURNES = "repo-chopin-nocturnes01" # chopin/nocturnes
292 REPO_MAPLE_LEAF = "repo-maple-leaf-rag0001" # scott_joplin/maple-leaf-rag
293 REPO_CIN_STRINGS = "repo-cinematic-strngs01" # kevin_macleod/cinematic-strings
294 REPO_KAI_AMBIENT = "repo-kai-ambient-txtr01" # kai_engel/ambient-textures
295 REPO_NEO_BAROQUE = "repo-neo-baroque-000001" # gabriel/neo-baroque
296 REPO_JAZZ_CHOPIN = "repo-jazz-chopin-000001" # aaliya/jazz-chopin
297 REPO_RAGTIME_EDM = "repo-ragtime-edm-000001" # marcus/ragtime-edm
298 REPO_FILM_SCORE = "repo-film-score-000001" # chen/film-score
299 REPO_POLYRHYTHM = "repo-polyrhythm-000001" # fatou/polyrhythm
300 REPO_COMMUNITY = "repo-community-collab01" # gabriel/community-collab
301
302 REPOS: list[dict[str, Any]] = [
303 dict(repo_id=REPO_NEO_SOUL, name="Neo-Soul Experiment", owner="gabriel", slug="neo-soul-experiment",
304 owner_user_id=GABRIEL, visibility="public",
305 description="A funk-influenced neo-soul project exploring polyrhythmic grooves in F# minor.",
306 tags=["neo-soul", "funk", "F# minor", "polyrhythm", "bass-heavy"],
307 key_signature="F# minor", tempo_bpm=92, days_ago=90, star_count=24, fork_count=3),
308 dict(repo_id=REPO_MODAL_JAZZ, name="Modal Jazz Sketches", owner="gabriel", slug="modal-jazz-sketches",
309 owner_user_id=GABRIEL, visibility="public",
310 description="Exploring Dorian and Phrygian modes. Miles Davis and Coltrane are the north stars.",
311 tags=["jazz", "modal", "Dorian", "Phrygian", "piano", "trumpet"],
312 key_signature="D Dorian", tempo_bpm=120, days_ago=60, star_count=18, fork_count=2),
313 dict(repo_id=REPO_AMBIENT, name="Ambient Textures Vol. 1", owner="sofia", slug="ambient-textures-vol-1",
314 owner_user_id=SOFIA, visibility="public",
315 description="Slow-evolving pads and generative arpeggios. Inspired by Eno and Nils Frahm.",
316 tags=["ambient", "generative", "pads", "Eb major", "slow"],
317 key_signature="Eb major", tempo_bpm=60, days_ago=45, star_count=31, fork_count=5),
318 dict(repo_id=REPO_AFROBEAT, name="Afrobeat Grooves", owner="aaliya", slug="afrobeat-grooves",
319 owner_user_id=AALIYA, visibility="public",
320 description="High-life meets contemporary production. Polyrhythmic percussion layers.",
321 tags=["afrobeat", "highlife", "polyrhythm", "Lagos", "percussion"],
322 key_signature="G major", tempo_bpm=128, days_ago=30, star_count=42, fork_count=6),
323 dict(repo_id=REPO_MICROTONAL, name="Microtonal Études", owner="chen", slug="microtonal-etudes",
324 owner_user_id=CHEN, visibility="public",
325 description="31-TET explorations. Spectral harmony and just intonation studies.",
326 tags=["microtonal", "spectral", "31-TET", "just intonation", "experimental"],
327 key_signature="C (31-TET)", tempo_bpm=76, days_ago=25, star_count=9, fork_count=1),
328 dict(repo_id=REPO_DRUM_MACHINE, name="808 Variations", owner="fatou", slug="808-variations",
329 owner_user_id=FATOU, visibility="public",
330 description="West African polyrhythm patterns through an 808 and modular synthesis.",
331 tags=["drums", "808", "polyrhythm", "West Africa", "modular"],
332 key_signature="A minor", tempo_bpm=100, days_ago=20, star_count=15, fork_count=2),
333 dict(repo_id=REPO_CHANSON, name="Chanson Minimale", owner="pierre", slug="chanson-minimale",
334 owner_user_id=PIERRE, visibility="public",
335 description="French chanson miniatures. Piano and cello. Silence as a compositional element.",
336 tags=["chanson", "minimalism", "piano", "cello", "French"],
337 key_signature="A major", tempo_bpm=52, days_ago=15, star_count=7, fork_count=0),
338 dict(repo_id=REPO_GRANULAR, name="Granular Studies", owner="yuki", slug="granular-studies",
339 owner_user_id=YUKI, visibility="public",
340 description="Granular synthesis research — texture, density, scatter. Source material: found sounds.",
341 tags=["granular", "synthesis", "experimental", "Tokyo", "texture"],
342 key_signature="E minor", tempo_bpm=70, days_ago=10, star_count=12, fork_count=1),
343 dict(repo_id=REPO_FUNK_SUITE, name="Funk Suite No. 1", owner="marcus", slug="funk-suite-no-1",
344 owner_user_id=MARCUS, visibility="public",
345 description="A four-movement funk suite. Electric piano, clavinet, wah bass, and pocket drums.",
346 tags=["funk", "electric piano", "clavinet", "groove", "suite"],
347 key_signature="E minor", tempo_bpm=108, days_ago=50, star_count=28, fork_count=4),
348 dict(repo_id=REPO_JAZZ_TRIO, name="Jazz Trio Sessions", owner="marcus", slug="jazz-trio-sessions",
349 owner_user_id=MARCUS, visibility="public",
350 description="Live-feel jazz trio recordings. Piano, double bass, brushed snare. Standards reimagined.",
351 tags=["jazz", "trio", "piano", "bass", "standards"],
352 key_signature="Bb major", tempo_bpm=138, days_ago=35, star_count=19, fork_count=2),
353 # Forked repos (private — for the fork sidebar section)
354 dict(repo_id=REPO_NEO_SOUL_FORK, name="Neo-Soul Experiment", owner="marcus", slug="neo-soul-experiment",
355 owner_user_id=MARCUS, visibility="private",
356 description="Fork of gabriel/neo-soul-experiment — Marcus's arrangement experiments.",
357 tags=["neo-soul", "funk", "F# minor", "fork"],
358 key_signature="F# minor", tempo_bpm=92, days_ago=10, star_count=0, fork_count=0),
359 dict(repo_id=REPO_AMBIENT_FORK, name="Ambient Textures Vol. 1", owner="yuki", slug="ambient-textures-vol-1",
360 owner_user_id=YUKI, visibility="private",
361 description="Fork of sofia/ambient-textures-vol-1 — Yuki's granular re-imagining.",
362 tags=["ambient", "granular", "fork"],
363 key_signature="Eb major", tempo_bpm=60, days_ago=5, star_count=0, fork_count=0),
364 ]
365
366 # Genre archive repos — batch-13 additions with structured muse_tags
367 GENRE_REPOS: list[dict[str, Any]] = [
368 dict(repo_id=REPO_WTC, name="The Well-Tempered Clavier", owner="bach", slug="well-tempered-clavier",
369 owner_user_id=BACH, visibility="public",
370 description="Bach's 48 preludes and fugues — one in each major and minor key. The definitive study in tonal harmony.",
371 tags=["genre:baroque", "key:C", "key:Am", "key:G", "key:F", "key:Bb", "stage:released", "emotion:serene", "emotion:complex"],
372 key_signature="C major (all 24 keys)", tempo_bpm=72, days_ago=365, star_count=88, fork_count=12),
373 dict(repo_id=REPO_GOLDBERG, name="Goldberg Variations", owner="bach", slug="goldberg-variations",
374 owner_user_id=BACH, visibility="public",
375 description="Aria with 30 variations. Bach's monumental keyboard work — from simple canon to ornate arabesque.",
376 tags=["genre:baroque", "stage:released", "emotion:joyful", "emotion:melancholic", "key:G"],
377 key_signature="G major", tempo_bpm=60, days_ago=350, star_count=74, fork_count=9),
378 dict(repo_id=REPO_NOCTURNES, name="Nocturnes", owner="chopin", slug="nocturnes",
379 owner_user_id=CHOPIN, visibility="public",
380 description="21 nocturnes for solo piano — poetry in sound. Lyrical melodies over arpeggiated left-hand accompaniment.",
381 tags=["genre:romantic", "emotion:melancholic", "emotion:tender", "stage:released", "key:Bb", "key:Eb"],
382 key_signature="Bb minor", tempo_bpm=58, days_ago=300, star_count=62, fork_count=8),
383 dict(repo_id=REPO_MAPLE_LEAF, name="Maple Leaf Rag", owner="scott_joplin", slug="maple-leaf-rag",
384 owner_user_id=SCOTT_JOPLIN, visibility="public",
385 description="The rag that launched a revolution. Syncopated right-hand melody over an oom-pah bass. The birth of ragtime.",
386 tags=["genre:ragtime", "emotion:playful", "stage:released", "key:Ab", "tempo:march"],
387 key_signature="Ab major", tempo_bpm=100, days_ago=280, star_count=51, fork_count=7),
388 dict(repo_id=REPO_CIN_STRINGS, name="Cinematic Strings", owner="kevin_macleod", slug="cinematic-strings",
389 owner_user_id=KEVIN_MACLEOD, visibility="public",
390 description="Orchestral string textures for cinematic use. Builds from delicate pianissimo to full tutti climax.",
391 tags=["genre:cinematic", "emotion:triumphant", "stage:released", "tempo:adagio", "key:D"],
392 key_signature="D minor", tempo_bpm=64, days_ago=180, star_count=43, fork_count=5),
393 dict(repo_id=REPO_KAI_AMBIENT, name="Ambient Textures", owner="kai_engel", slug="ambient-textures",
394 owner_user_id=KAI_ENGEL, visibility="public",
395 description="Patient, breathing soundscapes. Piano, strings, and silence woven into evolving ambient fields.",
396 tags=["genre:ambient", "emotion:serene", "stage:released", "tempo:largo", "key:C"],
397 key_signature="C major", tempo_bpm=50, days_ago=150, star_count=38, fork_count=4),
398 dict(repo_id=REPO_NEO_BAROQUE, name="Neo-Baroque Studies", owner="gabriel", slug="neo-baroque",
399 owner_user_id=GABRIEL, visibility="public",
400 description="What if Bach wrote jazz? Modal harmony and quartal voicings dressed in baroque counterpoint.",
401 tags=["genre:baroque", "genre:jazz", "stage:rough-mix", "emotion:complex", "ref:bach", "key:D"],
402 key_signature="D Dorian", tempo_bpm=84, days_ago=120, star_count=29, fork_count=3),
403 dict(repo_id=REPO_JAZZ_CHOPIN, name="Jazz Chopin", owner="aaliya", slug="jazz-chopin",
404 owner_user_id=AALIYA, visibility="public",
405 description="Chopin nocturnes reharmonized through a Coltrane lens. Rootless voicings, tritone substitutions, and more.",
406 tags=["genre:jazz", "genre:romantic", "emotion:tender", "ref:chopin", "ref:coltrane", "stage:mixing"],
407 key_signature="Bb minor", tempo_bpm=68, days_ago=90, star_count=34, fork_count=4),
408 dict(repo_id=REPO_RAGTIME_EDM, name="Ragtime EDM", owner="marcus", slug="ragtime-edm",
409 owner_user_id=MARCUS, visibility="public",
410 description="Scott Joplin meets the dancefloor. Syncopated MIDI melodies over trap hi-hats and house kick patterns.",
411 tags=["genre:edm", "genre:ragtime", "stage:production", "emotion:playful", "tempo:dance"],
412 key_signature="Ab major", tempo_bpm=128, days_ago=70, star_count=26, fork_count=3),
413 dict(repo_id=REPO_FILM_SCORE, name="Film Score — Untitled", owner="chen", slug="film-score",
414 owner_user_id=CHEN, visibility="public",
415 description="Three-act cinematic score. Microtonal tension in Act I, spectral climax in Act II, resolution in Act III.",
416 tags=["genre:cinematic", "emotion:tense", "emotion:triumphant", "stage:mixing", "key:C"],
417 key_signature="C (31-TET)", tempo_bpm=72, days_ago=55, star_count=18, fork_count=2),
418 dict(repo_id=REPO_POLYRHYTHM, name="Polyrhythm Studies", owner="fatou", slug="polyrhythm",
419 owner_user_id=FATOU, visibility="public",
420 description="West African rhythmic philosophy through a modular lens. 7-over-4, 5-over-3, and beyond.",
421 tags=["genre:afrobeats", "emotion:playful", "emotion:energetic", "stage:rough-mix", "tempo:polyrhythm"],
422 key_signature="A minor", tempo_bpm=92, days_ago=35, star_count=21, fork_count=2),
423 dict(repo_id=REPO_COMMUNITY, name="Community Collab", owner="gabriel", slug="community-collab",
424 owner_user_id=GABRIEL, visibility="public",
425 description="An open canvas for all eight contributors. Every genre, every voice, one evolving composition.",
426 tags=["genre:baroque", "genre:jazz", "genre:romantic", "genre:ragtime", "genre:cinematic",
427 "genre:ambient", "genre:edm", "genre:afrobeats", "emotion:serene", "emotion:complex",
428 "emotion:joyful", "emotion:melancholic", "emotion:tender", "emotion:playful",
429 "emotion:energetic", "emotion:triumphant", "emotion:tense", "stage:rough-mix"],
430 key_signature="C major", tempo_bpm=90, days_ago=200, star_count=95, fork_count=15),
431 ]
432
433
434 # ---------------------------------------------------------------------------
435 # Commit templates per repo
436 # ---------------------------------------------------------------------------
437
438 def _make_commits(repo_id: str, repo_key: str, n: int) -> list[dict[str, Any]]:
439 """Generate n realistic commits for repo_key with a branching history."""
440 TEMPLATES = {
441 "neo-soul": [
442 ("init: establish F# minor groove template at 92 BPM", "gabriel"),
443 ("feat(bass): add polyrhythmic bass line — 3-against-4 pulse", "gabriel"),
444 ("feat(keys): Rhodes chord voicings with upper-structure triads", "gabriel"),
445 ("refactor(drums): humanize ghost notes, tighten hi-hat velocity", "gabriel"),
446 ("feat(strings): bridge string section — section:bridge track:strings", "gabriel"),
447 ("feat(horns): sketch trumpet + alto sax counter-melody", "gabriel"),
448 ("fix(keys): resolve voice-leading parallel fifths in bar 7", "gabriel"),
449 ("feat(guitar): add scratch guitar rhythm in chorus — track:guitar", "gabriel"),
450 ("refactor(bass): tighten sub-bass at bar 13 to avoid muddiness", "marcus"),
451 ("feat(perc): add shaker and tambourine for groove density", "gabriel"),
452 ("fix(timing): realign hi-hat to quantize grid after humanize", "gabriel"),
453 ("feat(choir): add background vocal pad — ooh/aah — section:chorus", "gabriel"),
454 ("refactor(mix): reduce Rhodes level -3dB, open hi-hat +2dB", "marcus"),
455 ("feat(bridge): call-and-response horn arrangement — bars 25-32", "gabriel"),
456 ("fix(harmony): correct augmented chord spelling in turnaround", "gabriel"),
457 ("feat(strings): counterpoint violin line against bass in verse", "gabriel"),
458 ("refactor(drums): add kick variation in bar 4 of each 8-bar phrase", "marcus"),
459 ("feat(organ): add organ swell in pre-chorus — track:organ", "gabriel"),
460 ("fix(keys): remove accidental octave doubling in Rhodes voicing", "gabriel"),
461 ("feat(bass): slap variation for funk breakdown — section:breakdown", "marcus"),
462 ("refactor(horns): rewrite alto sax response phrase — cleaner contour", "gabriel"),
463 ("feat(perc): cowbell accent on beat 3 of bar 2 — groove:funk", "gabriel"),
464 ("feat(strings): pizzicato countermelody — bars 17-24", "gabriel"),
465 ("fix(guitar): tighten wah envelope attack — reduce pre-delay", "gabriel"),
466 ("feat(voice): lead vocal melody sketch — section:verse track:vocals", "gabriel"),
467 ],
468 "modal-jazz": [
469 ("init: D Dorian vamp at 120 BPM — piano + bass", "gabriel"),
470 ("feat(melody): Coltrane-inspired pentatonic runs over IV chord", "gabriel"),
471 ("feat(drums): brush kit — swing factor 0.65", "gabriel"),
472 ("experiment: Phrygian dominant bridge — E Phrygian Dominant", "gabriel"),
473 ("feat(piano): add McCoy Tyner quartal voicings in A section", "gabriel"),
474 ("fix(bass): correct walking bass note on bar 9 beat 3", "gabriel"),
475 ("feat(trumpet): head melody sketch — 12-bar AABA form", "gabriel"),
476 ("refactor(drums): increase ride cymbal bell accent frequency", "gabriel"),
477 ("feat(piano): bebop left-hand comp pattern — bars 1-8", "marcus"),
478 ("fix(melody): resolve blue note to major 3rd at phrase end", "gabriel"),
479 ("feat(bass): pedal point through Phrygian section — E pedal", "gabriel"),
480 ("feat(drums): hi-hat splash on beat 4 of turnaround", "gabriel"),
481 ("refactor(piano): revoice III chord as tritone substitution", "gabriel"),
482 ("feat(guitar): Freddie Green-style chord stabs — 4-to-the-bar", "marcus"),
483 ("fix(trumpet): fix pitch of low C# — use Eb enharmonic", "gabriel"),
484 ("feat(piano): out-chorus with McCoy quartal clusters", "gabriel"),
485 ("feat(bass): counter-rhythm 2-bar fill after trumpet solo", "gabriel"),
486 ],
487 "ambient": [
488 ("init: Eb major pad foundation — slow attack 4s release 8s", "sofia"),
489 ("feat(arp): generative 16th-note arpeggiator — random seed 42", "sofia"),
490 ("feat(texture): granular string texture layer — section:intro", "yuki"),
491 ("fix(arp): reduce velocity variance — sounds too mechanical", "sofia"),
492 ("feat(pad): add sub-octave layer for warmth — section:middle", "sofia"),
493 ("feat(bells): wind chime texture — 7th partial harmonic series", "yuki"),
494 ("refactor(arp): increase note-length randomization range", "sofia"),
495 ("feat(drone): Eb pedal drone — bowed brass harmonic", "pierre"),
496 ("fix(texture): reduce granular density in intro — too busy", "yuki"),
497 ("feat(reverb): add convolution reverb impulse — Norwegian church", "sofia"),
498 ("feat(pad): second pad layer — inversion of root chord", "sofia"),
499 ("refactor(arp): change arpeggio direction — ascending + descending", "sofia"),
500 ("feat(texture): filtered noise texture — high shelf +6dB", "yuki"),
501 ("fix(drone): tune drone to equal temperament Eb", "sofia"),
502 ("feat(melody): sparse piano melody — whole notes — section:climax", "pierre"),
503 ("feat(fade): 3-minute fade out — linear to -80dB", "sofia"),
504 ("refactor(mix): reduce string texture -2dB to sit behind pad", "yuki"),
505 ("fix(arp): fix stuck note at bar 64 — midi note-off missing", "sofia"),
506 ],
507 "afrobeat": [
508 ("init: G major groove at 128 BPM — 12/8 polyrhythm", "aaliya"),
509 ("feat(perc): traditional talking drum pattern — track:tama", "aaliya"),
510 ("feat(guitar): highlife guitar pattern — interlocking rhythm", "aaliya"),
511 ("feat(bass): electric bass groove — root-fifth walking pattern", "aaliya"),
512 ("feat(horns): brass unison figure — bars 1-4", "aaliya"),
513 ("refactor(perc): tighten conga timing — reduce humanize variance", "fatou"),
514 ("feat(keys): Fender Rhodes stabs — track:keys", "aaliya"),
515 ("fix(guitar): fix choke on open string — add palm mute", "aaliya"),
516 ("feat(choir): call-and-response vocal arrangement", "aaliya"),
517 ("feat(bass): syncopated fills at section transitions", "aaliya"),
518 ("refactor(horns): split alto and tenor lines — 3rd apart", "aaliya"),
519 ("feat(perc): shekere layer — steady eighth-note pulse", "fatou"),
520 ("fix(mix): reduce vocal level in verse — instrumental focus", "aaliya"),
521 ("feat(guitar): second guitar — rhythmic scratches on offbeat", "aaliya"),
522 ("feat(bass): slap bass hook for chorus energy boost", "aaliya"),
523 ("refactor(drums): add more snare ghost notes — Questlove style", "fatou"),
524 ("feat(keys): organ swell into chorus — track:organ", "aaliya"),
525 ("fix(perc): fix timing drift on conga in bar 32", "fatou"),
526 ("feat(strings): string overdub — Fela-inspired octave line", "aaliya"),
527 ("feat(voice): lead vocal melody — Yoruba lyric sketch", "aaliya"),
528 ],
529 "microtonal": [
530 ("init: C (31-TET) drone exploration at 76 BPM", "chen"),
531 ("feat(harmony): otonal hexad — 4:5:6:7:9:11", "chen"),
532 ("feat(melody): quarter-tone scale ascending line", "chen"),
533 ("fix(tuning): correct 7th partial — was off by 3 cents", "chen"),
534 ("feat(rhythm): Messiaen mode 3 rhythm grid", "chen"),
535 ("feat(texture): spectral filtered noise — harmonic series", "chen"),
536 ("refactor(melody): retrograde inversion of opening motif", "chen"),
537 ("feat(bass): undertone series pedal — utonal foundation", "chen"),
538 ("fix(harmony): resolve voice-leading microtonal step error", "chen"),
539 ("feat(perc): non-retrogradable rhythm in timpani", "chen"),
540 ("feat(strings): col legno battuto technique — quarter-tone gliss", "chen"),
541 ("refactor(harmony): substitute Ptolemy's intense chromatic", "chen"),
542 ("feat(woodwinds): multiphonics — clarinet + flute", "chen"),
543 ("fix(tuning): recalibrate piano to equal 31-TET temperament", "chen"),
544 ],
545 "drums": [
546 ("init: A minor 808 foundation at 100 BPM", "fatou"),
547 ("feat(kick): four-on-the-floor with sub-frequency duck", "fatou"),
548 ("feat(snare): syncopated snare with flam accent", "fatou"),
549 ("feat(hihat): 16th-note hi-hat with velocity curve", "fatou"),
550 ("feat(perc): djembe pattern — traditional Mandinka rhythm", "fatou"),
551 ("feat(808): 808 bass note on root — 100ms decay", "fatou"),
552 ("refactor(kick): tune 808 kick to key center A — 110Hz", "fatou"),
553 ("fix(hihat): remove double-triggered hi-hat on beat 3", "fatou"),
554 ("feat(perc): shaker accent pattern — offbeat sixteenths", "fatou"),
555 ("feat(snare): ghost note velocity humanize — ±12 velocity", "fatou"),
556 ("feat(808): sub-bass movement — root to 5th fills", "fatou"),
557 ("refactor(perc): layer djembe with finger drum machine", "fatou"),
558 ("feat(crash): crash on bar 9 downbeat — section transition", "fatou"),
559 ],
560 "chanson": [
561 ("init: A major sketch — piano solo motif at 52 BPM", "pierre"),
562 ("feat(piano): left-hand ostinato — arpeggiated 9th chord", "pierre"),
563 ("feat(cello): pizzicato bass line — bars 1-8", "pierre"),
564 ("feat(piano): theme A — 8-bar melody in upper voice", "pierre"),
565 ("feat(cello): sustained cello counterpoint — bars 9-16", "pierre"),
566 ("refactor(piano): reduce left-hand density — let melody breathe", "pierre"),
567 ("feat(piano): theme B in relative minor — F# minor", "pierre"),
568 ("fix(cello): bowings — ensure smooth legato at bar 12", "pierre"),
569 ("feat(piano): coda — augmented theme A in parallel 10ths", "pierre"),
570 ("feat(silence): 4-bar rest before final chord — dynamic contrast", "pierre"),
571 ("feat(cello): col legno tremolo — extended technique", "pierre"),
572 ("refactor(harmony): substitute V7 with bVII for chanson flavour", "pierre"),
573 ],
574 "granular": [
575 ("init: E minor granular pad — source: rain recording", "yuki"),
576 ("feat(scatter): random scatter algorithm — grain size 20-80ms", "yuki"),
577 ("feat(density): grain density envelope — sparse to dense", "yuki"),
578 ("feat(pitch): pitch randomization ±0.3 semitones", "yuki"),
579 ("feat(texture): city ambience layer — Tokyo train station", "yuki"),
580 ("fix(phase): fix grain phase correlation — reduce flamming", "yuki"),
581 ("feat(filter): formant filter on granular output — vowel morph", "yuki"),
582 ("refactor(scatter): increase random seed variation per bar", "yuki"),
583 ("feat(rhythm): rhythmic granular — sync to 70 BPM sixteenths", "yuki"),
584 ("fix(tuning): retune pitch center to E — was detuned +0.5st", "yuki"),
585 ("feat(reverb): 8-second hall reverb tail — late reflections only", "yuki"),
586 ("feat(mod): LFO modulation on grain position — 0.3Hz triangle", "yuki"),
587 ],
588 "funk-suite": [
589 ("init: E minor funk groove at 108 BPM — Mvt. I", "marcus"),
590 ("feat(bass): wah-wah bass hook — bars 1-4 — track:bass", "marcus"),
591 ("feat(keys): electric piano chord voicings — tight stabs", "marcus"),
592 ("feat(clavinet): clavinet riff — bars 5-8", "marcus"),
593 ("feat(drums): pocket drum groove — ghost notes on snare", "marcus"),
594 ("feat(guitar): rhythm guitar — interlocking with clavinet", "marcus"),
595 ("feat(horns): brass hits on the upbeat — track:horns", "marcus"),
596 ("refactor(bass): tighten wah envelope attack for more snap", "marcus"),
597 ("feat(keys): Rhodes solo in Mvt. II — Dorian mode", "marcus"),
598 ("fix(guitar): remove string buzz on open D", "marcus"),
599 ("feat(bass): slap funk breakdown — Mvt. II outro", "marcus"),
600 ("feat(perc): add congas — Afro-Cuban polyrhythm layer", "marcus"),
601 ("feat(keys): B3 organ swell — Mvt. III transition", "marcus"),
602 ("refactor(drums): accent hi-hat on the e's — open 16th feel", "marcus"),
603 ("fix(horns): retune brass — flat by 8 cents on high notes", "marcus"),
604 ("feat(bass): octave bass walk into chorus — track:bass", "marcus"),
605 ("feat(clavinet): filtered clavinet — muted pickstyle", "marcus"),
606 ("fix(keys): fix missed chord on beat 4 bar 22", "marcus"),
607 ("feat(drums): Mvt. IV — double-time feel — hi-hat 16th groove", "marcus"),
608 ("feat(bass): fretless bass for Mvt. IV — floating groove", "marcus"),
609 ],
610 "jazz-trio": [
611 ("init: Bb major vamp — piano trio at 138 BPM", "marcus"),
612 ("feat(piano): comping pattern — shell voicings 3-7", "marcus"),
613 ("feat(bass): walking bass — Bb major standard changes", "marcus"),
614 ("feat(drums): brushed snare pattern — triplet feel", "marcus"),
615 ("feat(piano): solo chorus 1 — pentatonic approach", "marcus"),
616 ("feat(bass): bass solo feature — rubato", "marcus"),
617 ("feat(drums): trading 4s — kit break response", "marcus"),
618 ("refactor(piano): reharmonize bridge — tritone subs", "marcus"),
619 ("feat(piano): stride left hand in final chorus", "marcus"),
620 ("fix(bass): fix intonation on F# — adjust finger placement", "marcus"),
621 ("feat(drums): add brushed cymbal roll into solo sections", "marcus"),
622 ("feat(piano): ballad tempo reduction for outro — ♩=72", "marcus"),
623 ("refactor(bass): add counterpoint line during piano comping", "marcus"),
624 ("fix(drums): remove extraneous kick note on bar 9", "marcus"),
625 ("feat(piano): final cadenza — rubato", "marcus"),
626 ],
627 # Genre archive repos — batch-13
628 "wtc": [
629 ("init: Book I — Prelude No.1 in C major — arpeggiated harmony", "gabriel"),
630 ("feat(fugue): Fugue No.1 in C major — 4-voice exposition", "gabriel"),
631 ("feat(prelude): Prelude No.2 in C minor — perpetual motion 16ths", "sofia"),
632 ("feat(fugue): Fugue No.2 in C minor — chromatic subject", "gabriel"),
633 ("feat(prelude): Prelude No.3 in C# major — arpeggiated texture", "chen"),
634 ("feat(fugue): Fugue No.3 in C# major — 3-voice stretto", "gabriel"),
635 ("refactor(harmony): correct spelling of diminished 7th in bar 8", "pierre"),
636 ("feat(prelude): Prelude No.4 in C# minor — lyrical melody", "gabriel"),
637 ("feat(fugue): Fugue No.4 in C# minor — 5-voice exposition", "sofia"),
638 ("feat(prelude): Prelude No.5 in D major — driving 16th notes", "gabriel"),
639 ("feat(fugue): Fugue No.5 in D major — invertible counterpoint", "chen"),
640 ("fix(voice-leading): resolve parallel 5ths in C major fugue bar 14", "gabriel"),
641 ("feat(prelude): Prelude No.6 in D minor — expressive chromatics", "pierre"),
642 ("feat(fugue): Fugue No.6 in D minor — augmentation in bass", "gabriel"),
643 ("feat(prelude): Prelude No.7 in Eb major — ornate passagework", "sofia"),
644 ("feat(fugue): Fugue No.7 in Eb major — inversion of subject", "gabriel"),
645 ("refactor(ornamentation): add trills per Baroque convention — bars 1-4", "chen"),
646 ("feat(prelude): Prelude No.8 in Eb minor — chromatic descent", "gabriel"),
647 ("feat(fugue): Fugue No.8 in Eb minor — 3-voice with episode", "pierre"),
648 ("feat(prelude): Prelude No.9 in E major — binary form", "gabriel"),
649 ("feat(fugue): Fugue No.9 in E major — motivic development", "sofia"),
650 ("fix(tuning): retune to equal temperament from well-temperament", "chen"),
651 ("feat(prelude): Prelude No.10 in E minor — two-part invention style", "gabriel"),
652 ("feat(fugue): Fugue No.10 in E minor — rhythmic diminution", "gabriel"),
653 ("feat(book2): Book II — Prelude No.1 in C major — extended version", "sofia"),
654 ("feat(book2): Fugue No.1 BK2 in C major — 4-voice with tonal answer", "gabriel"),
655 ("feat(book2): Prelude No.2 BK2 in C minor — turbulent arpeggios", "pierre"),
656 ("feat(book2): Fugue No.2 BK2 — chromatic subject, 4 voices", "gabriel"),
657 ("refactor(dynamics): add hairpin dynamics per Urtext edition", "sofia"),
658 ("feat(book2): Prelude No.3 BK2 in C# major — serene cantabile", "gabriel"),
659 ],
660 "goldberg": [
661 ("init: Goldberg Aria — sarabande in G major, ornate upper voice", "gabriel"),
662 ("feat(var1): Variation 1 — two-voice in parallel 3rds", "gabriel"),
663 ("feat(var2): Variation 2 — one voice per hand, canonic imitation", "sofia"),
664 ("feat(var3): Variation 3 — canon at the unison", "gabriel"),
665 ("feat(var4): Variation 4 — robust 4-voice passepied", "pierre"),
666 ("feat(var5): Variation 5 — hand-crossing, one voice each hand", "gabriel"),
667 ("feat(var6): Variation 6 — canon at the second", "sofia"),
668 ("feat(var7): Variation 7 — gigue in 6/8, dance character", "gabriel"),
669 ("feat(var8): Variation 8 — two-voice inversion in 3rds and 6ths", "chen"),
670 ("feat(var13): Variation 13 — lyrical aria-like melody, experimental rubato", "gabriel"),
671 ("fix(ornaments): correct trill resolution in Variation 13 bar 9", "sofia"),
672 ("feat(var25): Variation 25 — chromatic aria, the emotional heart", "gabriel"),
673 ("feat(var30): Variation 30 — Quodlibet, quotes folk songs", "gabriel"),
674 ("feat(aria-reprise): Aria da capo — return of opening theme", "pierre"),
675 ("refactor(voicing): ensure aria melody sits above all inner voices", "gabriel"),
676 ("fix(voice-leading): remove parallel octaves in Variation 4 bar 12", "sofia"),
677 ("refactor(tempo): apply consistent note values in 3/4 Variations", "gabriel"),
678 ("feat(var21): Variation 21 — chromatic canon at the 7th", "chen"),
679 ],
680 "nocturnes": [
681 ("init: Op.9 No.1 in Bb minor — gentle arpeggiated bass, yearning melody", "aaliya"),
682 ("feat(op9-2): Op.9 No.2 in Eb major — the iconic theme, ornate reprise", "aaliya"),
683 ("feat(op9-3): Op.9 No.3 in B major — agitated middle section", "gabriel"),
684 ("feat(op15-1): Op.15 No.1 in F major — pastoral melody, stormy development", "aaliya"),
685 ("feat(op15-2): Op.15 No.2 in F# major — murmuring bass, cantabile melody", "sofia"),
686 ("feat(op15-3): Op.15 No.3 in G minor — solemn choral opening", "aaliya"),
687 ("feat(op27-1): Op.27 No.1 in C# minor — tragic opening, ecstatic climax", "gabriel"),
688 ("feat(op27-2): Op.27 No.2 in Db major — sustained melody, ornate inner voice", "aaliya"),
689 ("refactor(rubato): add tempo fluctuation markings per Chopin's own notation", "pierre"),
690 ("fix(pedaling): correct sustain pedal placement in Op.9 No.2 bar 5", "aaliya"),
691 ("feat(op32-1): Op.32 No.1 in B major — introspective, questioning end", "sofia"),
692 ("feat(op32-2): Op.32 No.2 in Ab major — gentle but harmonically complex", "aaliya"),
693 ("feat(op37-1): Op.37 No.1 in G minor — chorale-like, organistic", "gabriel"),
694 ("feat(op37-2): Op.37 No.2 in G major — barcarolle-style 6/8", "aaliya"),
695 ("refactor(ornamentation): add mordents and grace notes per autograph", "sofia"),
696 ("fix(voice-leading): eliminate voice crossing in Op.15 No.3 bar 7", "aaliya"),
697 ("feat(op48-1): Op.48 No.1 in C minor — grand and tragic", "gabriel"),
698 ("feat(op48-2): Op.48 No.2 in F# minor — agitated and restless", "aaliya"),
699 ("feat(op55-1): Op.55 No.1 in F minor — melancholic cantabile", "sofia"),
700 ("feat(op55-2): Op.55 No.2 in Eb major — flowing, conversational", "aaliya"),
701 ("feat(op62-1): Op.62 No.1 in B major — late style, fragmented ornament", "gabriel"),
702 ("feat(op62-2): Op.62 No.2 in E major — tender farewell, inner voices", "aaliya"),
703 ],
704 "maple-leaf": [
705 ("init: Maple Leaf Rag in Ab major — 4/4 at 100 BPM", "marcus"),
706 ("feat(A): Section A — syncopated melody over oom-pah bass, bars 1-16", "marcus"),
707 ("feat(A-repeat): Section A repeat with octave doubling in melody", "gabriel"),
708 ("feat(B): Section B — contrast, moves to Eb major", "marcus"),
709 ("feat(B-repeat): Section B repeat — velocity humanized", "marcus"),
710 ("feat(C): Section C (trio) — moves to Db major, more lyrical", "marcus"),
711 ("feat(C-repeat): Section C repeat with improvised embellishment", "gabriel"),
712 ("feat(D): Section D — returns to Ab, triumphant restatement", "marcus"),
713 ("refactor(bass): tighten oom-pah bass timing — was 8ms ahead", "marcus"),
714 ("fix(melody): correct grace note in bar 9 — was wrong pitch Eb not D", "gabriel"),
715 ("feat(slow): slow-version — halftime feel, rubato allowed", "marcus"),
716 ("feat(slow): slow-version extended ornaments in melody", "gabriel"),
717 ("feat(edm): marcus-edm-remix — trap hi-hats under ragtime melody", "marcus"),
718 ("feat(edm): marcus-edm-remix — 808 bass replacing oom-pah pattern", "marcus"),
719 ],
720 "cinematic-strings": [
721 ("init: Cinematic Strings in D minor — string orchestra at 64 BPM", "gabriel"),
722 ("feat(intro): solo cello theme — bars 1-8 — pp, col arco", "chen"),
723 ("feat(build): violas enter — pizzicato counter-rhythm, bars 9-16", "gabriel"),
724 ("feat(build): second violins add sustained harmonic pad", "sofia"),
725 ("feat(climax): full orchestra tutti — bars 33-40 — ff", "gabriel"),
726 ("feat(climax): timpani and brass reinforcement at climax peak", "chen"),
727 ("feat(resolution): strings return to solo cello — reprise of theme", "gabriel"),
728 ("refactor(dynamics): smooth crescendo from pp to ff over 32 bars", "sofia"),
729 ("fix(intonation): retune violin II section — was sharp by 5 cents", "gabriel"),
730 ("feat(orchestral): orchestral branch — add oboe and clarinet doubling", "chen"),
731 ("feat(orchestral): French horn countermelody in orchestral version", "gabriel"),
732 ("feat(piano): stripped-piano branch — piano reduction of string score", "pierre"),
733 ("feat(piano): add pedal markings to piano reduction", "sofia"),
734 ("refactor(tempo): add ritardando at bar 38 for dramatic pause", "gabriel"),
735 ("fix(articulation): add sul ponticello marking to Variation 2 strings", "chen"),
736 ],
737 "kai-ambient": [
738 ("init: Kai Engel ambient field — C major, slow morphing pad", "pierre"),
739 ("feat(pad1): first layer — high strings, ppp, infinite sustain", "pierre"),
740 ("feat(pad2): second pad — piano harmonics, prepared technique", "sofia"),
741 ("feat(piano): sparse piano melody — whole notes, bars 9-24", "pierre"),
742 ("feat(v1): v1 branch — original release version, 8-minute version", "pierre"),
743 ("refactor(v1): v1 — master level adjusted to -14 LUFS", "sofia"),
744 ("feat(v2): v2-extended — added 4-minute drone coda", "pierre"),
745 ("feat(v2): v2-extended — new string layer in coda, sul tasto", "sofia"),
746 ("fix(phase): reduce stereo width in pad2 to avoid phase cancellation", "pierre"),
747 ("refactor(mix): filter low end on pad1 — HPF at 80Hz", "pierre"),
748 ],
749 "neo-baroque": [
750 ("init: Neo-Baroque in D Dorian — harpsichord + electric bass at 84 BPM", "gabriel"),
751 ("feat(counterpoint): two-voice invention — right hand melody, left hand bass", "gabriel"),
752 ("feat(jazz): jazz-voicings branch — quartal harmony replaces triads", "gabriel"),
753 ("feat(jazz): tritone substitution in bar 8 turnaround — jazz-voicings", "marcus"),
754 ("feat(jazz): rootless 9th voicings in right hand — jazz-voicings", "gabriel"),
755 ("feat(harmony): harmonic sequence — descending 5ths in bass", "gabriel"),
756 ("feat(rhythm): syncopated baroque rhythm — quarter-note displacement", "gabriel"),
757 ("feat(edm): edm-bassline branch — 808 sub bass under baroque melody", "marcus"),
758 ("feat(edm): four-on-the-floor kick added — edm-bassline", "gabriel"),
759 ("feat(edm): filter sweep into bridge — edm-bassline", "marcus"),
760 ("feat(harpsichord): feature/add-harpsichord — harpsichord replaces piano", "gabriel"),
761 ("feat(harpsichord): double manual technique — feature/add-harpsichord", "pierre"),
762 ("fix(voice-leading): parallel 5ths in bar 5 inner voices corrected", "gabriel"),
763 ("refactor(form): add da capo repeat — bars 1-8 return at end", "gabriel"),
764 ("feat(improv): jazz improvisation section — 8 bars over baroque changes", "marcus"),
765 ("fix(timing): realign baroque ornaments to 16th grid", "gabriel"),
766 ("feat(strings): add pizzicato baroque strings — bars 17-32", "sofia"),
767 ("refactor(harmony): rewrite cadence — Phrygian half cadence, bar 16", "gabriel"),
768 ("feat(coda): extended coda with fugal stretto — all voices", "gabriel"),
769 ("fix(harpsichord): velocity normalization — harpsichord lacks dynamics", "gabriel"),
770 ("feat(modal): modal interchange — borrow from D minor in bridge", "marcus"),
771 ("refactor(mix): balance harpsichord vs bass — HF boost on harpsichord", "gabriel"),
772 ("feat(ornament): mordent on beat 1 of each 4-bar phrase", "pierre"),
773 ("feat(jazz2): jazz-voicings v2 — add upper-structure triads", "gabriel"),
774 ("refactor(form): restructure to AABBA form — stronger contrast", "gabriel"),
775 ("feat(bass): walking bass line for jazz-voicings bridge section", "marcus"),
776 ("fix(modal): correct Dorian vs natural minor in bar 12", "gabriel"),
777 ("feat(fugue): mini fugue in coda — 3-voice, 8 bars", "gabriel"),
778 ],
779 "jazz-chopin": [
780 ("init: Jazz Chopin — Op.9 No.2 reharmonized, Bb minor at 68 BPM", "aaliya"),
781 ("feat(reharmonize): tritone sub on V7 chord — bar 4", "aaliya"),
782 ("feat(reharmonize): minor ii-V-I substitution in bridge", "gabriel"),
783 ("feat(voicing): rootless 9th chord voicings — left hand", "aaliya"),
784 ("feat(reharmonize): Coltrane substitution pattern in climax — reharmonized", "aaliya"),
785 ("feat(reharmonize): add chromatic approach chords — reharmonized", "marcus"),
786 ("feat(trio): trio-arrangement — add bass and drums", "aaliya"),
787 ("feat(trio): walking bass added under reharmonized changes", "marcus"),
788 ("feat(trio): brushed snare — light jazz feel, trio-arrangement", "gabriel"),
789 ("fix(voice-leading): parallel 5ths in reharmonized bridge, bar 9", "aaliya"),
790 ("refactor(melody): add bebop ornaments to Chopin melody line", "aaliya"),
791 ("feat(reharmonize): backdoor ii-V substitution in outro", "aaliya"),
792 ("fix(bass): fix intonation issue on low Bb — walking bass", "marcus"),
793 ("feat(trio): piano solo chorus over jazz changes — trio-arrangement", "aaliya"),
794 ("refactor(tempo): add ritardando at 4-bar phrase ends", "aaliya"),
795 ("feat(reharmonize): modal interchange — iv chord from parallel minor", "gabriel"),
796 ("fix(drums): remove unintentional kick on beat 3 — trio", "aaliya"),
797 ("feat(coda): free improvisation coda — all three voices", "aaliya"),
798 ("refactor(harmony): ensure all substitutions maintain melodic identity", "aaliya"),
799 ("feat(reharmonize): full reharmonized version complete — all 3 sections", "aaliya"),
800 ],
801 "ragtime-edm": [
802 ("init: Ragtime EDM — Maple Leaf Rag MIDI over trap beat at 128 BPM", "marcus"),
803 ("feat(trap): trap hi-hat grid — 16th triplets with velocity variation", "marcus"),
804 ("feat(trap): 808 kick on 1 and 3 — trap-version", "marcus"),
805 ("feat(trap): snare on 2 and 4 with ghosted 16ths — trap-version", "gabriel"),
806 ("feat(ragtime): ragtime melody quantized to EDM grid — bars 1-16", "marcus"),
807 ("feat(house): house-version — 4-on-floor kick, Chicago-style", "marcus"),
808 ("feat(house): sidechain compression on ragtime bass — house-version", "gabriel"),
809 ("feat(house): filter sweep on ragtime melody — house-version", "marcus"),
810 ("feat(swing): electro-swing branch — shuffle quantize 16ths to swing", "marcus"),
811 ("feat(swing): brass sample layer on ragtime melody — electro-swing", "gabriel"),
812 ("fix(pitch): transpose ragtime melody up one semitone to Ab for EDM mix", "marcus"),
813 ("refactor(mix): sidechain bass to kick for pumping effect — all versions", "marcus"),
814 ("feat(drop): big drop transition — silence then tutti return", "marcus"),
815 ("fix(timing): tighten ragtime melody to EDM grid — was 10ms behind", "gabriel"),
816 ("refactor(master): normalize to -8 LUFS for streaming platforms", "marcus"),
817 ("feat(bridge): 8-bar bridge — minimal, just kick and ragtime melody fragment", "marcus"),
818 ("feat(outro): outro — gradual filter close on all elements", "marcus"),
819 ],
820 "film-score": [
821 ("init: Film Score — Act I, C (31-TET) — establishing motif, tense, pp", "chen"),
822 ("feat(act1): act1 — microtonal string cluster, bars 1-8", "chen"),
823 ("feat(act1): act1 — ascending quarter-tone figure in winds", "chen"),
824 ("feat(act1): act1 — timpani accent on beat 3 — instability motif", "gabriel"),
825 ("feat(act1): act1 — brass pedal — low brass drone, bars 9-16", "chen"),
826 ("feat(act2): act2 branch — spectral climax, full orchestra tutti", "chen"),
827 ("feat(act2): act2 — strings in high register, ff, sul ponticello", "sofia"),
828 ("feat(act2): act2 — timpani rolls and brass fanfare — climax peak", "chen"),
829 ("feat(act2): act2 — dissonant chord cluster, all 31-TET pitches", "gabriel"),
830 ("feat(act3): act3 branch — resolution, return to simple C major", "chen"),
831 ("feat(act3): act3 — solo violin melody, simple diatonic, pp", "sofia"),
832 ("feat(act3): act3 — gradual orchestral return from silence", "chen"),
833 ("feat(act3): act3 — final chord — C major, fff, held for 8 bars", "gabriel"),
834 ("fix(tuning): recalibrate all instruments to 31-TET in Act I", "chen"),
835 ("refactor(dynamics): smooth transition from Act I pp to Act II ff", "chen"),
836 ("fix(voice-leading): remove dissonance clash in Act III resolution", "sofia"),
837 ("refactor(score): add rehearsal letter marks every 8 bars", "chen"),
838 ("feat(leitmotif): recurring motif appears in each act — unifying thread", "gabriel"),
839 ],
840 "polyrhythm": [
841 ("init: Polyrhythm Studies — A minor at 92 BPM — 7-over-4 base", "fatou"),
842 ("feat(7-4): 7-over-4 — djembe in 7, talking drum in 4", "fatou"),
843 ("feat(7-4): 7-over-4 — bass guitar anchors common pulse", "fatou"),
844 ("feat(7-4): 7-over-4 — listener orientation — hi-hat on beat 1 only", "aaliya"),
845 ("refactor(7-4): humanize djembe timing — ±8ms variance", "fatou"),
846 ("feat(5-3): 5-over-3-experiment — conga in 5, shekere in 3", "fatou"),
847 ("feat(5-3): 5-over-3-experiment — bass anchors on shared beat", "aaliya"),
848 ("feat(5-3): 5-over-3-experiment — add melody on shared downbeats only", "fatou"),
849 ("fix(phase): fix drifting phase in 7-over-4 at bar 32 — MIDI timing", "fatou"),
850 ("feat(groove): add cross-stick snare to bridge the two rhythmic worlds", "aaliya"),
851 ("refactor(mix): bring up djembe attack — was buried under bass", "fatou"),
852 ],
853 "community": [
854 ("init: Community Collab — open canvas — C major, 90 BPM", "gabriel"),
855 ("feat(counterpoint): sofia's counterpoint — Bach-inspired 2-voice invention", "sofia"),
856 ("feat(ornament): yuki's ornaments — granular delay on all voices", "yuki"),
857 ("feat(analysis): pierre's analysis annotations — harmonic function labels", "pierre"),
858 ("feat(bass): marcus's bassline — funk groove under baroque counterpoint", "marcus"),
859 ("feat(rhythm): fatou's polyrhythm layer — 5-over-3 pattern over 4/4", "fatou"),
860 ("feat(reharmonize): aaliya's jazz reharmonization of C major progression", "aaliya"),
861 ("feat(microtonal): chen's microtonal ornaments — quarter-tone glissandi", "chen"),
862 ("refactor(structure): gabriel rebalances all layers — new mix", "gabriel"),
863 ("feat(baroque): sofia adds fugal episode — subject and answer", "sofia"),
864 ("feat(texture): yuki adds granular texture layer — sparse grain scatter", "yuki"),
865 ("fix(voice-leading): pierre fixes parallel 5ths — bars 12-13", "pierre"),
866 ("feat(groove): marcus adds clavinet stabs — funk energy", "marcus"),
867 ("feat(perc): fatou adds shekere pulse — holds everything together", "fatou"),
868 ("feat(jazz): aaliya adds blue notes to melody — jazzy feel", "aaliya"),
869 ("feat(tuning): chen corrects micro-tuning in ornament layer", "chen"),
870 ("feat(improv): gabriel improvises bridge over new changes", "gabriel"),
871 ("feat(strings): sofia adds lush string pad — bars 33-48", "sofia"),
872 ("feat(reverb): yuki adds cathedral reverb to string layer", "yuki"),
873 ("feat(harmony): pierre adds 9th and 11th extensions to all chords", "pierre"),
874 ("feat(bass2): marcus doubles bassline at octave — thicker low end", "marcus"),
875 ("feat(perc2): fatou adds djembe solo — bars 49-56", "fatou"),
876 ("feat(modal): aaliya introduces Dorian mode shift in bridge", "aaliya"),
877 ("feat(spectral): chen adds spectral filter sweep — act of transformation", "chen"),
878 ("feat(motif): gabriel introduces 4-note motif — appears in all layers", "gabriel"),
879 ("refactor(mix): sofia adjusts balance — counterpoint more prominent", "sofia"),
880 ("feat(scatter): yuki reduces grain density for introspective section", "yuki"),
881 ("feat(chorale): pierre writes 4-voice chorale climax — bars 57-64", "pierre"),
882 ("feat(solo): marcus piano solo over baroque changes", "marcus"),
883 ("feat(perc3): fatou polyrhythm climax — all layers simultaneously", "fatou"),
884 ("feat(reprise): aaliya leads reprise of opening theme — reharmonized", "aaliya"),
885 ("feat(finale): chen's finale motif — microtonal glissando into last chord", "chen"),
886 ("feat(coda): gabriel's coda — reduces to solo piano, pp", "gabriel"),
887 ("refactor(final-mix): sofia final mix pass — all dynamics balanced", "sofia"),
888 ("feat(outro): yuki granular outro — voices dissolve into texture", "yuki"),
889 ("feat(credits): pierre adds annotation — credits all contributors", "pierre"),
890 ],
891 }
892
893 key = repo_key
894 templates = TEMPLATES.get(key, TEMPLATES["neo-soul"])
895 commits: list[dict[str, Any]] = []
896 prev_id: str | None = None
897 branch = "main"
898 t = templates * ((n // len(templates)) + 1)
899
900 for i in range(n):
901 cid = _sha(f"{repo_id}-commit-{i}")
902 msg = t[i % len(templates)][0]
903 author = t[i % len(templates)][1]
904 days = (n - i) * 2 # older commits further back
905 commits.append(dict(
906 commit_id=cid,
907 repo_id=repo_id,
908 branch=branch,
909 parent_ids=[prev_id] if prev_id else [],
910 message=msg,
911 author=author,
912 timestamp=_now(days=days),
913 snapshot_id=_sha(f"snap-{repo_id}-{i}"),
914 ))
915 prev_id = cid
916 # Sprinkle in a feature branch every ~8 commits
917 if i > 0 and i % 8 == 0:
918 branch = "main"
919
920 return commits
921
922
923 # Track roles per repo key for the instrument breakdown bar
924 REPO_TRACKS: dict[str, list[tuple[str, str]]] = {
925 "neo-soul": [("bass", "tracks/bass.mid"), ("keys", "tracks/rhodes.mid"),
926 ("drums", "tracks/drums.mid"), ("strings", "tracks/strings.mid"),
927 ("horns", "tracks/trumpet.mid"), ("horns", "tracks/alto_sax.mid"),
928 ("guitar", "tracks/guitar.mid"), ("vocals", "tracks/vocals.mid")],
929 "modal-jazz": [("piano", "tracks/piano.mid"), ("bass", "tracks/bass.mid"),
930 ("drums", "tracks/drums.mid"), ("trumpet", "tracks/trumpet.mid"),
931 ("guitar", "tracks/guitar.mid")],
932 "ambient": [("pad", "tracks/pad.mid"), ("arp", "tracks/arpeggiator.mid"),
933 ("strings", "tracks/strings.mid"), ("bells", "tracks/bells.mid"),
934 ("drone", "tracks/drone.mid")],
935 "afrobeat": [("perc", "tracks/talking_drum.mid"), ("guitar", "tracks/guitar.mid"),
936 ("bass", "tracks/bass.mid"), ("horns", "tracks/horns.mid"),
937 ("keys", "tracks/rhodes.mid"), ("perc", "tracks/shekere.mid"),
938 ("vocals", "tracks/vocals.mid")],
939 "microtonal": [("piano", "tracks/piano.mid"), ("strings", "tracks/strings.mid"),
940 ("woodwinds", "tracks/woodwinds.mid"), ("perc", "tracks/percussion.mid")],
941 "drums": [("kick", "tracks/kick.mid"), ("snare", "tracks/snare.mid"),
942 ("hihat", "tracks/hihat.mid"), ("perc", "tracks/djembe.mid"),
943 ("808", "tracks/808.mid")],
944 "chanson": [("piano", "tracks/piano.mid"), ("cello", "tracks/cello.mid")],
945 "granular": [("pad", "tracks/granular_pad.mid"), ("texture", "tracks/texture.mid"),
946 ("rhythm", "tracks/rhythmic.mid")],
947 "funk-suite": [("bass", "tracks/bass.mid"), ("keys", "tracks/electric_piano.mid"),
948 ("clavinet", "tracks/clavinet.mid"), ("drums", "tracks/drums.mid"),
949 ("guitar", "tracks/guitar.mid"), ("horns", "tracks/horns.mid"),
950 ("perc", "tracks/congas.mid")],
951 "jazz-trio": [("piano", "tracks/piano.mid"), ("bass", "tracks/bass.mid"),
952 ("drums", "tracks/drums.mid")],
953 # Genre archive repos — batch-13
954 "wtc": [("piano", "tracks/piano.mid"), ("harpsichord", "tracks/harpsichord.mid")],
955 "goldberg": [("piano", "tracks/piano.mid"), ("harpsichord", "tracks/harpsichord.mid")],
956 "nocturnes": [("piano", "tracks/piano.mid")],
957 "maple-leaf": [("piano", "tracks/piano.mid"), ("bass", "tracks/bass.mid")],
958 "cinematic-strings":[("violin1", "tracks/violin1.mid"), ("violin2", "tracks/violin2.mid"),
959 ("viola", "tracks/viola.mid"), ("cello", "tracks/cello.mid"),
960 ("bass", "tracks/double_bass.mid"), ("timp", "tracks/timpani.mid")],
961 "kai-ambient": [("pad", "tracks/pad.mid"), ("piano", "tracks/piano.mid"),
962 ("strings", "tracks/strings.mid")],
963 "neo-baroque": [("harpsichord", "tracks/harpsichord.mid"), ("bass", "tracks/bass.mid"),
964 ("strings", "tracks/strings.mid")],
965 "jazz-chopin": [("piano", "tracks/piano.mid"), ("bass", "tracks/bass.mid"),
966 ("drums", "tracks/drums.mid")],
967 "ragtime-edm": [("piano", "tracks/piano.mid"), ("kick", "tracks/kick.mid"),
968 ("snare", "tracks/snare.mid"), ("hihat", "tracks/hihat.mid"),
969 ("808", "tracks/808.mid")],
970 "film-score": [("strings", "tracks/strings.mid"), ("brass", "tracks/brass.mid"),
971 ("woodwinds", "tracks/woodwinds.mid"), ("timp", "tracks/timpani.mid")],
972 "polyrhythm": [("djembe", "tracks/djembe.mid"), ("tama", "tracks/talking_drum.mid"),
973 ("shekere", "tracks/shekere.mid"), ("bass", "tracks/bass.mid")],
974 "community": [("piano", "tracks/piano.mid"), ("bass", "tracks/bass.mid"),
975 ("strings", "tracks/strings.mid"), ("perc", "tracks/djembe.mid"),
976 ("harpsichord", "tracks/harpsichord.mid"), ("pad", "tracks/granular_pad.mid")],
977 }
978
979 REPO_KEY_MAP = {
980 REPO_NEO_SOUL: "neo-soul",
981 REPO_MODAL_JAZZ: "modal-jazz",
982 REPO_AMBIENT: "ambient",
983 REPO_AFROBEAT: "afrobeat",
984 REPO_MICROTONAL: "microtonal",
985 REPO_DRUM_MACHINE: "drums",
986 REPO_CHANSON: "chanson",
987 REPO_GRANULAR: "granular",
988 REPO_FUNK_SUITE: "funk-suite",
989 REPO_JAZZ_TRIO: "jazz-trio",
990 REPO_NEO_SOUL_FORK: "neo-soul",
991 REPO_AMBIENT_FORK: "ambient",
992 # Genre archive repos — batch-13
993 REPO_WTC: "wtc",
994 REPO_GOLDBERG: "goldberg",
995 REPO_NOCTURNES: "nocturnes",
996 REPO_MAPLE_LEAF: "maple-leaf",
997 REPO_CIN_STRINGS: "cinematic-strings",
998 REPO_KAI_AMBIENT: "kai-ambient",
999 REPO_NEO_BAROQUE: "neo-baroque",
1000 REPO_JAZZ_CHOPIN: "jazz-chopin",
1001 REPO_RAGTIME_EDM: "ragtime-edm",
1002 REPO_FILM_SCORE: "film-score",
1003 REPO_POLYRHYTHM: "polyrhythm",
1004 REPO_COMMUNITY: "community",
1005 }
1006
1007 COMMIT_COUNTS = {
1008 REPO_NEO_SOUL: 40,
1009 REPO_MODAL_JAZZ: 30,
1010 REPO_AMBIENT: 35,
1011 REPO_AFROBEAT: 38,
1012 REPO_MICROTONAL: 25,
1013 REPO_DRUM_MACHINE: 28,
1014 REPO_CHANSON: 22,
1015 REPO_GRANULAR: 24,
1016 REPO_FUNK_SUITE: 42,
1017 REPO_JAZZ_TRIO: 32,
1018 REPO_NEO_SOUL_FORK: 8,
1019 REPO_AMBIENT_FORK: 5,
1020 # Genre archive repos — batch-13
1021 REPO_WTC: 60,
1022 REPO_GOLDBERG: 35,
1023 REPO_NOCTURNES: 45,
1024 REPO_MAPLE_LEAF: 25,
1025 REPO_CIN_STRINGS: 30,
1026 REPO_KAI_AMBIENT: 20,
1027 REPO_NEO_BAROQUE: 55,
1028 REPO_JAZZ_CHOPIN: 40,
1029 REPO_RAGTIME_EDM: 35,
1030 REPO_FILM_SCORE: 28,
1031 REPO_POLYRHYTHM: 22,
1032 REPO_COMMUNITY: 70,
1033 }
1034
1035 # Specific branch configurations for genre archive repos (batch-13).
1036 # Each entry: list of (branch_name, commit_offset_from_end) — offset 0 = HEAD.
1037 GENRE_REPO_BRANCHES: dict[str, list[tuple[str, int]]] = {
1038 REPO_WTC: [("prelude-bk1", 50), ("fugue-bk1", 42), ("prelude-bk2", 20), ("fugue-bk2", 10)],
1039 REPO_GOLDBERG: [("aria-only", 30), ("variation-13-experimental", 15)],
1040 REPO_NOCTURNES: [("op9", 38), ("op15", 25), ("op27", 12)],
1041 REPO_MAPLE_LEAF: [("slow-version", 10), ("marcus-edm-remix", 5)],
1042 REPO_CIN_STRINGS: [("orchestral", 20), ("stripped-piano", 8)],
1043 REPO_KAI_AMBIENT: [("v1", 14), ("v2-extended", 6)],
1044 REPO_NEO_BAROQUE: [("experiment/jazz-voicings", 45), ("experiment/edm-bassline", 30), ("feature/add-harpsichord", 15)],
1045 REPO_JAZZ_CHOPIN: [("reharmonized", 30), ("trio-arrangement", 15)],
1046 REPO_RAGTIME_EDM: [("trap-version", 28), ("house-version", 18), ("electro-swing", 8)],
1047 REPO_FILM_SCORE: [("act1", 22), ("act2", 14), ("act3", 6)],
1048 REPO_POLYRHYTHM: [("7-over-4", 16), ("5-over-3-experiment", 8)],
1049 REPO_COMMUNITY: [("sofias-counterpoint", 60), ("yukis-ornaments", 50), ("pierres-analysis", 40), ("marcuss-bassline", 25)],
1050 }
1051
1052 # ---------------------------------------------------------------------------
1053 # Muse VCS — content-addressed MIDI objects, snapshots, commits, tags
1054 # ---------------------------------------------------------------------------
1055
1056 # Track files per repo for Muse VCS — realistic MIDI instrument names and sizes.
1057 # Piano solo: 8KB–40KB; ensemble: 50KB–200KB (task spec).
1058 # Each tuple is (filename, base_size_bytes).
1059 MUSE_VCS_FILES: dict[str, list[tuple[str, int]]] = {
1060 REPO_NEO_SOUL: [("piano.mid", 24576), ("bass.mid", 12288), ("drums.mid", 16384),
1061 ("violin.mid", 18432), ("trumpet.mid", 13312)],
1062 REPO_FUNK_SUITE: [("piano.mid", 22528), ("bass.mid", 13312), ("drums.mid", 16384),
1063 ("trumpet.mid", 12288), ("flute.mid", 10240)],
1064 REPO_AFROBEAT: [("bass.mid", 14336), ("drums.mid", 18432), ("violin.mid", 15360),
1065 ("cello.mid", 14336), ("trumpet.mid", 12288)],
1066 REPO_AMBIENT: [("piano.mid", 32768), ("violin.mid", 20480), ("cello.mid", 17408),
1067 ("viola.mid", 15360), ("flute.mid", 11264)],
1068 REPO_MODAL_JAZZ: [("piano.mid", 28672), ("bass.mid", 10240), ("drums.mid", 14336),
1069 ("trumpet.mid", 11264)],
1070 REPO_JAZZ_TRIO: [("piano.mid", 26624), ("bass.mid", 11264), ("drums.mid", 13312)],
1071 REPO_MICROTONAL: [("piano.mid", 20480), ("violin.mid", 16384), ("cello.mid", 14336)],
1072 REPO_DRUM_MACHINE: [("drums.mid", 18432), ("bass.mid", 12288)],
1073 REPO_CHANSON: [("piano.mid", 36864), ("cello.mid", 17408)],
1074 REPO_GRANULAR: [("piano.mid", 15360), ("violin.mid", 12288), ("flute.mid", 9216)],
1075 REPO_NEO_SOUL_FORK:[("piano.mid", 24576), ("bass.mid", 12288), ("drums.mid", 16384)],
1076 REPO_AMBIENT_FORK: [("piano.mid", 32768), ("violin.mid", 20480), ("cello.mid", 17408)],
1077 }
1078
1079 # Metadata per repo for muse_commits.metadata JSON field.
1080 MUSE_COMMIT_META: dict[str, dict[str, object]] = {
1081 REPO_NEO_SOUL: {"tempo_bpm": 92.0, "key": "F# minor", "time_signature": "4/4", "instrument_count": 5},
1082 REPO_FUNK_SUITE: {"tempo_bpm": 108.0, "key": "E minor", "time_signature": "4/4", "instrument_count": 5},
1083 REPO_AFROBEAT: {"tempo_bpm": 128.0, "key": "G major", "time_signature": "12/8","instrument_count": 5},
1084 REPO_AMBIENT: {"tempo_bpm": 60.0, "key": "Eb major", "time_signature": "4/4", "instrument_count": 5},
1085 REPO_MODAL_JAZZ: {"tempo_bpm": 120.0, "key": "D Dorian", "time_signature": "4/4", "instrument_count": 4},
1086 REPO_JAZZ_TRIO: {"tempo_bpm": 138.0, "key": "Bb major", "time_signature": "3/4", "instrument_count": 3},
1087 REPO_MICROTONAL: {"tempo_bpm": 76.0, "key": "C (31-TET)","time_signature":"4/4", "instrument_count": 3},
1088 REPO_DRUM_MACHINE: {"tempo_bpm": 100.0, "key": "A minor", "time_signature": "4/4", "instrument_count": 2},
1089 REPO_CHANSON: {"tempo_bpm": 52.0, "key": "A major", "time_signature": "4/4", "instrument_count": 2},
1090 REPO_GRANULAR: {"tempo_bpm": 70.0, "key": "E minor", "time_signature": "4/4", "instrument_count": 3},
1091 REPO_NEO_SOUL_FORK:{"tempo_bpm": 92.0, "key": "F# minor", "time_signature": "4/4", "instrument_count": 3},
1092 REPO_AMBIENT_FORK: {"tempo_bpm": 60.0, "key": "Eb major", "time_signature": "4/4", "instrument_count": 3},
1093 }
1094
1095 # Muse tag taxonomy — ALL values from the task spec must appear in the seed.
1096 MUSE_EMOTION_TAGS = [
1097 "melancholic", "joyful", "tense", "serene", "triumphant",
1098 "mysterious", "playful", "tender", "energetic", "complex",
1099 ]
1100 MUSE_STAGE_TAGS = [
1101 "sketch", "rough-mix", "arrangement", "production", "mixing", "mastering", "released",
1102 ]
1103 MUSE_KEY_TAGS = [
1104 "C", "Am", "G", "Em", "Bb", "F#", "Db", "Abm", "D", "Bm", "A", "F", "Eb", "Cm",
1105 ]
1106 MUSE_TEMPO_TAGS = [
1107 "60bpm", "72bpm", "80bpm", "96bpm", "120bpm", "132bpm", "140bpm", "160bpm",
1108 ]
1109 MUSE_GENRE_TAGS = [
1110 "baroque", "romantic", "ragtime", "edm", "ambient", "cinematic",
1111 "jazz", "afrobeats", "classical", "fusion",
1112 ]
1113 MUSE_REF_TAGS = [
1114 "bach", "chopin", "debussy", "coltrane", "daft-punk", "beethoven", "joplin", "monk",
1115 ]
1116
1117 # Full flat list of all taxonomy tags — used when cycling through commits.
1118 _ALL_MUSE_TAGS: list[str] = (
1119 MUSE_EMOTION_TAGS
1120 + MUSE_STAGE_TAGS
1121 + MUSE_KEY_TAGS
1122 + MUSE_TEMPO_TAGS
1123 + MUSE_GENRE_TAGS
1124 + MUSE_REF_TAGS
1125 )
1126
1127 # Repos that get the full rich tag taxonomy (most active, richest history).
1128 MUSE_RICH_TAG_REPOS = {REPO_NEO_SOUL, REPO_FUNK_SUITE}
1129
1130
1131 # ---------------------------------------------------------------------------
1132 # Muse variation history — DAW project constants
1133 # ---------------------------------------------------------------------------
1134
1135 # Two most active DAW projects used for variation seeding.
1136 # project_id values are deterministic UUIDs so they survive re-seeds.
1137 PROJECT_NEO_BAROQUE = _uid("project-gabriel-neo-baroque")
1138 PROJECT_COMMUNITY_COLLAB = _uid("project-gabriel-community-collab")
1139
1140 PHRASE_TYPES = ["melody", "harmony", "bass", "rhythm", "pad", "lead"]
1141
1142 VARIATION_INTENTS_NEO_BAROQUE = [
1143 "Add counterpoint line above the baroque theme in bars 9-16",
1144 "Reharmonize the continuo with IV-V-I instead of I-IV-V",
1145 "Double the melody at the upper octave in the A section",
1146 "Reduce note density in the ornament layer — too busy",
1147 "Add a fermata on the penultimate chord for dramatic pause",
1148 "Transpose the inner voice down a third for smoother voice-leading",
1149 "Replace the parallel motion with contrary motion in bars 5-8",
1150 "Add suspensions (4-3, 7-6) on the strong beats of the progression",
1151 "Introduce a sequence pattern (descending thirds) in the episode",
1152 "Thicken the bass line with octave doubling",
1153 "Add a ritardando in the final four bars",
1154 "Experiment with a Neapolitan chord before the final cadence",
1155 "Restructure the ornamentation — trills only on structural beats",
1156 "Add an inner pedal point on the dominant during the development",
1157 "Rewrite the melodic leap (octave) as stepwise with passing tones",
1158 "Apply tierce de Picardie on the final chord",
1159 "Compress the sequence pattern to fit 2-bar phrases",
1160 "Add imitation between soprano and bass at the interval of a 4th",
1161 "Slow harmonic rhythm in the development section",
1162 "Introduce a chromatic passing tone in bar 12 inner voice",
1163 ]
1164
1165 VARIATION_INTENTS_COMMUNITY_COLLAB = [
1166 "Blend the neo-soul groove with the afrobeat polyrhythm layer",
1167 "Merge gabriel's chord voicings with aaliya's bass pattern",
1168 "Add marcus's funk stabs over the ambient pad foundation",
1169 "Cross-fade between yuki's granular texture and sofia's arp",
1170 "Combine pierre's chanson melody with the modal jazz harmony",
1171 "Overlay chen's microtonal texture on the funk groove",
1172 "Mix fatou's djembe pattern with the electronic kick drum",
1173 "Harmonise gabriel's melody with aaliya's Yoruba vocal line",
1174 "Layer marcus's Rhodes over the afrobeat rhythm section",
1175 "Merge the granular scatter with the orchestral strings",
1176 "Add a call-and-response between the jazz trio and the afrobeat horns",
1177 "Blend microtonal pitch-bends with the neo-soul Rhodes voicing",
1178 "Cross-fade the ambient pad into the funk breakdown",
1179 "Combine pierre's cello with fatou's 808 bass for a hybrid outro",
1180 "Layer gabriel's polyrhythm with yuki's rhythmic granular engine",
1181 "Blend chen's otonal hexad with sofia's generative arpeggio",
1182 "Mix aaliya's talking drum with marcus's brushed snare",
1183 "Overlay the modal jazz walking bass under the afrobeat groove",
1184 "Merge the Chanson ostinato with the funk electric piano stabs",
1185 "Cross-fade the microtonal étude into the neo-baroque continuo",
1186 ]
1187
1188 TRACK_IDS_NEO_BAROQUE = [
1189 _uid("track-nb-soprano"),
1190 _uid("track-nb-alto"),
1191 _uid("track-nb-tenor"),
1192 _uid("track-nb-bass"),
1193 _uid("track-nb-continuo"),
1194 _uid("track-nb-violin"),
1195 ]
1196
1197 TRACK_IDS_COMMUNITY = [
1198 _uid("track-cc-lead"),
1199 _uid("track-cc-harmony"),
1200 _uid("track-cc-bass"),
1201 _uid("track-cc-drums"),
1202 _uid("track-cc-pad"),
1203 _uid("track-cc-horns"),
1204 ]
1205
1206 REGION_IDS_NEO_BAROQUE = [_uid(f"region-nb-{i}") for i in range(8)]
1207 REGION_IDS_COMMUNITY = [_uid(f"region-cc-{i}") for i in range(8)]
1208
1209
1210 def _make_note_dict(
1211 pitch: int,
1212 velocity: int,
1213 start_beat: float,
1214 duration_beats: float,
1215 track_id: str,
1216 region_id: str,
1217 ) -> NoteDict:
1218 """Build a NoteDict payload for before_json / after_json."""
1219 return NoteDict(
1220 pitch=pitch,
1221 velocity=velocity,
1222 start_beat=start_beat,
1223 duration_beats=duration_beats,
1224 track_id=track_id,
1225 region_id=region_id,
1226 )
1227
1228
1229 def _make_variation_section(
1230 project_id: str,
1231 intents: list[str],
1232 track_ids: list[str],
1233 region_ids: list[str],
1234 base_commit_hashes: list[str],
1235 seed_prefix: str,
1236 ) -> tuple[list[Variation], list[Phrase], list[NoteChange]]:
1237 """Generate 30 variations (20 accepted, 5 discarded, 5 pending) with
1238 realistic phrase and note-change children for a single DAW project.
1239
1240 Parent chains (draft → refined → final) are formed in groups of 3-5.
1241 Three variations are merge variations with parent2_variation_id set.
1242 """
1243 variations: list[Variation] = []
1244 phrases: list[Phrase] = []
1245 note_changes: list[NoteChange] = []
1246
1247 # Build 30 variations. Status distribution:
1248 # [0..19] accepted, [20..24] discarded, [25..29] pending
1249 STATUS_MAP = (
1250 ["accepted"] * 20
1251 + ["discarded"] * 5
1252 + ["pending"] * 5
1253 )
1254
1255 var_ids: list[str] = [
1256 _uid(f"{seed_prefix}-var-{i}") for i in range(30)
1257 ]
1258
1259 # Form parent chains in groups: [0-3], [4-7], [8-11], [12-15], [16-19],
1260 # [20-22], [23-24], [25-27], [28-29]
1261 chain_groups = [
1262 [0, 1, 2, 3], # accepted chain of 4
1263 [4, 5, 6, 7], # accepted chain of 4
1264 [8, 9, 10, 11], # accepted chain of 4
1265 [12, 13, 14], # accepted chain of 3
1266 [15, 16, 17, 18, 19], # accepted chain of 5
1267 [20, 21, 22], # discarded chain of 3
1268 [23, 24], # discarded chain of 2
1269 [25, 26, 27], # pending chain of 3
1270 [28, 29], # pending chain of 2
1271 ]
1272
1273 # Merge variations at indices 7, 11, 15 — they get parent2_variation_id.
1274 # All parent2 references point BACKWARD (earlier in the sequence) so the
1275 # entire batch can be flushed in one call without FK violations.
1276 merge_indices = {7, 11, 15}
1277 merge_parent2_map = {
1278 7: var_ids[0], # end of chain-2 merges with start of chain-1
1279 11: var_ids[3], # end of chain-3 merges with end of chain-1
1280 15: var_ids[7], # start of chain-5 merges with end of chain-2
1281 }
1282
1283 # Build parent_variation_id mapping
1284 parent_map: dict[int, str | None] = {}
1285 for chain in chain_groups:
1286 for pos, idx in enumerate(chain):
1287 parent_map[idx] = var_ids[chain[pos - 1]] if pos > 0 else None
1288
1289 now = _now()
1290
1291 for i in range(30):
1292 status = STATUS_MAP[i]
1293 intent = intents[i % len(intents)]
1294 base_hash = base_commit_hashes[i % len(base_commit_hashes)]
1295 # muse_variations.base_state_id / commit_state_id are VARCHAR(36) — derive
1296 # a UUID from the 64-char SHA-256 commit hash so it fits the column.
1297 base_state_uuid = _uid(base_hash)
1298 parent_vid = parent_map.get(i)
1299 parent2_vid = merge_parent2_map.get(i) if i in merge_indices else None
1300 is_head = status == "accepted" and i == 19
1301
1302 var = Variation(
1303 variation_id=var_ids[i],
1304 project_id=project_id,
1305 base_state_id=base_state_uuid,
1306 conversation_id=_uid(f"{seed_prefix}-conv-{i // 5}"),
1307 intent=intent,
1308 explanation=f"Variation {i+1}: {intent[:60]}",
1309 status=status,
1310 affected_tracks=[track_ids[i % len(track_ids)]],
1311 affected_regions=[region_ids[i % len(region_ids)]],
1312 beat_range_start=float((i % 8) * 8),
1313 beat_range_end=float((i % 8) * 8 + 16),
1314 parent_variation_id=parent_vid,
1315 parent2_variation_id=parent2_vid,
1316 commit_state_id=base_state_uuid if status == "accepted" else None,
1317 is_head=is_head,
1318 created_at=_now(days=30 - i),
1319 updated_at=_now(days=30 - i),
1320 )
1321 variations.append(var)
1322
1323 # 2-5 phrases per variation
1324 phrase_count = 2 + (i % 4)
1325 for p in range(phrase_count):
1326 start_beat = float(((i % 8) * 8 + p * 4) % 64)
1327 end_beat = start_beat + 4.0 + float((p % 3) * 4)
1328 phrase_type = PHRASE_TYPES[p % len(PHRASE_TYPES)]
1329 tid = track_ids[(i + p) % len(track_ids)]
1330 rid = region_ids[(i + p) % len(region_ids)]
1331
1332 # CC events attached to phrases with sustain/expression/modulation/volume
1333 cc_data = [
1334 {"cc": 64, "beat": start_beat + 0.5, "value": 127},
1335 {"cc": 11, "beat": start_beat + 1.0, "value": 90},
1336 ] if p % 2 == 0 else [
1337 {"cc": 1, "beat": start_beat + 0.5, "value": 50},
1338 {"cc": 7, "beat": start_beat + 1.0, "value": 100},
1339 ]
1340
1341 # Pitch bend on every third phrase
1342 pitch_bends_data = (
1343 [{"beat": start_beat + 2.5, "value": 4096}]
1344 if p % 3 == 0 else None
1345 )
1346
1347 phrase = Phrase(
1348 phrase_id=_uid(f"{seed_prefix}-phrase-{i}-{p}"),
1349 variation_id=var_ids[i],
1350 sequence=p,
1351 track_id=tid,
1352 region_id=rid,
1353 start_beat=start_beat,
1354 end_beat=end_beat,
1355 label=phrase_type,
1356 tags=[phrase_type, "seed"],
1357 explanation=f"Phrase {p+1} ({phrase_type}) of variation {i+1}",
1358 cc_events=cc_data,
1359 pitch_bends=pitch_bends_data,
1360 aftertouch=None,
1361 region_start_beat=start_beat,
1362 region_duration_beats=end_beat - start_beat,
1363 region_name=f"Region-{rid[:8]}",
1364 )
1365 phrases.append(phrase)
1366
1367 # 4-20 note changes per phrase
1368 note_count = 4 + ((i * 3 + p * 7) % 17)
1369 for n in range(note_count):
1370 pitch_base = 48 + (n * 4) % 60 # MIDI 48-108
1371 vel = 30 + (n * 7) % 98 # velocity 30-127
1372 nb = start_beat + float(n) * 0.5
1373 dur = 0.25 + float(n % 4) * 0.25 + float((n // 4) % 4) * 0.5
1374
1375 # Cycle through change types — canonical values from contracts/json_types.py
1376 if n % 3 == 0:
1377 change_type = "added"
1378 before_j = None
1379 after_j = _make_note_dict(pitch_base, vel, nb, dur, tid, rid)
1380 elif n % 3 == 1:
1381 change_type = "removed"
1382 before_j = _make_note_dict(pitch_base, vel, nb, dur, tid, rid)
1383 after_j = None
1384 else:
1385 change_type = "modified"
1386 orig_pitch = pitch_base - 2
1387 orig_vel = max(30, vel - 12)
1388 orig_beat = nb - 0.25
1389 before_j = _make_note_dict(orig_pitch, orig_vel, orig_beat, dur, tid, rid)
1390 after_j = _make_note_dict(pitch_base, vel, nb, dur, tid, rid)
1391
1392 nc = NoteChange(
1393 id=_uid(f"{seed_prefix}-nc-{i}-{p}-{n}"),
1394 phrase_id=_uid(f"{seed_prefix}-phrase-{i}-{p}"),
1395 change_type=change_type,
1396 before_json=before_j,
1397 after_json=after_j,
1398 )
1399 note_changes.append(nc)
1400
1401 return variations, phrases, note_changes
1402
1403
1404 # ---------------------------------------------------------------------------
1405 # Issue templates
1406 # ---------------------------------------------------------------------------
1407
1408 ISSUE_TEMPLATES: dict[str, list[dict[str, Any]]] = {
1409 "neo-soul": [
1410 dict(n=1, state="open", title="Bass line loses tension in bar 9",
1411 body="3-against-4 pulse drifts. Ghost note on beat 2.5 recommended.", labels=["groove", "bass"]),
1412 dict(n=2, state="open", title="Add guitar scratch rhythm track",
1413 body="Arrangement too sparse. Scratch guitar would complement Rhodes.", labels=["arrangement"]),
1414 dict(n=3, state="closed", title="Tempo fluctuates bars 4-8",
1415 body="Resolved by re-quantizing with tight humanization.", labels=["tempo", "drums"]),
1416 dict(n=4, state="open", title="Choir voicing too wide in chorus",
1417 body="Soprano and bass parts are 2+ octaves apart — muddy on small speakers.", labels=["harmony"]),
1418 dict(n=5, state="open", title="Organ swell clashes with Rhodes",
1419 body="Both sit in mid-range 400-800Hz. Pan or EQ to separate.", labels=["mix"]),
1420 dict(n=6, state="closed", title="String pizzicato timing off",
1421 body="Fixed — re-quantized to 16th grid with 10ms humanize.", labels=["strings", "timing"]),
1422 dict(n=7, state="open", title="Bridge needs more harmonic tension",
1423 body="The IV-I cadence in the bridge is too resolved. Try IV-bVII.", labels=["harmony", "bridge"]),
1424 dict(n=8, state="open", title="Trumpet counter-melody too high",
1425 body="Goes above high C. Alto sax range would be more idiomatic.", labels=["horns"]),
1426 dict(n=9, state="closed", title="Bass note collision on beat 1",
1427 body="Fixed — root changed from F# to C# (5th) to reduce mud.", labels=["bass", "harmony"]),
1428 dict(n=10, state="open", title="Add breakdown section before final chorus",
1429 body="Energy needs to drop before the big finish. 4-bar bass+drums only.", labels=["arrangement"]),
1430 dict(n=11, state="open", title="Vocals too bright — needs de-essing",
1431 body="Sibilance prominent on headphones. High shelf cut above 10kHz.", labels=["mix", "vocals"]),
1432 dict(n=12, state="open", title="Consider key change to A minor for outro",
1433 body="A modulation to relative major would give a brighter feel at the end.", labels=["harmony"]),
1434 dict(n=13, state="closed", title="Rhodes voicing clashes in bar 12",
1435 body="Fixed — upper structure triad replaced with shell voicing (root + 7th).", labels=["piano", "harmony"]),
1436 dict(n=14, state="open", title="Add shaker for groove density in pre-chorus",
1437 body="The pre-chorus feels lighter than the verse. A 16th-note shaker would tie the pulse together.",
1438 labels=["groove", "perc"]),
1439 dict(n=15, state="open", title="Vocal compression artifacts on sustained notes",
1440 body="Long vowels show pumping at attack. Reduce ratio from 8:1 to 4:1 and increase attack to 10ms.",
1441 labels=["mix", "vocals"]),
1442 ],
1443 "modal-jazz": [
1444 dict(n=1, state="open", title="Phrygian bridge needs ii-V turnaround",
1445 body="Jump from D Dorian to E Phrygian is abrupt. Add Am7b5 → D7alt.", labels=["harmony"]),
1446 dict(n=2, state="open", title="Swing factor inconsistent piano vs bass",
1447 body="Piano at 0.65 swing, bass at 0.55. Should match.", labels=["groove", "timing"]),
1448 dict(n=3, state="closed", title="Piano pedaling too heavy in changes",
1449 body="Fixed — reduced sustain pedal range.", labels=["piano"]),
1450 dict(n=4, state="open", title="Guitar chord stabs too loud",
1451 body="Freddie Green stabs should sit under the piano. Lower -3dB.", labels=["mix", "guitar"]),
1452 dict(n=5, state="open", title="Head melody needs resolution note",
1453 body="The A section ends on 6th scale degree — unresolved. Add scale degree 1.", labels=["melody"]),
1454 dict(n=6, state="open", title="Tritone sub reharmonization too frequent",
1455 body="Using sub every 2 bars sounds formulaic. Reserve for 8-bar phrase end.", labels=["harmony"]),
1456 dict(n=7, state="closed", title="Bass solo too long — loses listener",
1457 body="Trimmed to 16 bars. Better pacing.", labels=["bass"]),
1458 dict(n=8, state="open", title="Drummer needs to lay back on trumpet solo",
1459 body="Ride accent too prominent during solo. Comp more sparsely.", labels=["drums"]),
1460 dict(n=9, state="open", title="Piano comping too busy in A section",
1461 body="Left-hand comp obscures walking bass line. Simplify to 2-feel.", labels=["piano", "arrangement"]),
1462 dict(n=10, state="closed", title="Trumpet range error — written vs concert pitch",
1463 body="Fixed — all trumpet parts transposed down a major 2nd to concert pitch.", labels=["horns"]),
1464 dict(n=11, state="open", title="Add lydian mode variation in B section",
1465 body="The B section stays strictly Dorian. A Lydian passage would add color.", labels=["harmony"]),
1466 dict(n=12, state="open", title="Bass register too low in chorus",
1467 body="Walking bass drops below E1 — inaudible on most systems. Transpose up an octave.", labels=["bass"]),
1468 dict(n=13, state="open", title="Snare ghost notes need velocity curve",
1469 body="All ghosts at velocity 40 — too uniform. Use 20-50 range with slight randomization.", labels=["drums"]),
1470 dict(n=14, state="closed", title="Key center ambiguous in intro",
1471 body="Fixed — added a clear D Dorian vamp at the start before the head.", labels=["harmony"]),
1472 dict(n=15, state="open", title="Outro needs ritardando",
1473 body="The piece ends abruptly at tempo. Gradual slow-down over last 4 bars would give closure.",
1474 labels=["arrangement"]),
1475 ],
1476 "ambient": [
1477 dict(n=1, state="open", title="Arpeggiator repeats — needs more variation",
1478 body="After 32 bars the pattern becomes predictable. Modulate seed every 8 bars.", labels=["generative"]),
1479 dict(n=2, state="open", title="Pad too washy — needs more definition",
1480 body="Attack of 4s is too slow. Try 2s with a short sustain plateau.", labels=["pad"]),
1481 dict(n=3, state="closed", title="Stuck note in arp at bar 64",
1482 body="Fixed — MIDI note-off added. Was a gate issue.", labels=["bug", "midi"]),
1483 dict(n=4, state="open", title="Add harmonic movement after bar 48",
1484 body="The Eb pedal has been static for 3 minutes. Move to Ab for 8 bars.", labels=["harmony"]),
1485 dict(n=5, state="open", title="Norwegian church reverb is too bright",
1486 body="High frequency content in reverb tail is distracting. EQ pre-send.", labels=["mix"]),
1487 dict(n=6, state="open", title="Granular density too high in intro",
1488 body="Start sparser and build. Currently too dense from bar 1.", labels=["texture"]),
1489 dict(n=7, state="closed", title="Phase correlation issues in stereo pad",
1490 body="Resolved by setting stereo width to 80% (was 120%).", labels=["mix"]),
1491 dict(n=8, state="open", title="Piano melody needs more dynamic variation",
1492 body="All notes at same velocity. Add cresc/dim on each 4-bar phrase.", labels=["piano", "dynamics"]),
1493 dict(n=9, state="open", title="Wind chimes pitched too high",
1494 body="7th partial sits above 8kHz on most speakers. Lower source pitch.", labels=["texture"]),
1495 dict(n=10, state="open", title="Generative seed produces repeated rhythmic clusters",
1496 body="Seed 42 has a bias toward beat 1 and 3. Rotate seed every 16 bars.",
1497 labels=["generative", "bug"]),
1498 dict(n=11, state="closed", title="Cello sustain too long — blurs transitions",
1499 body="Fixed — reduced release to 2s from 6s. Now transitions are audible.", labels=["strings"]),
1500 dict(n=12, state="open", title="Add breath sounds between sections",
1501 body="Silence between sections is too abrupt. A subtle room tone or breath sample would ease transitions.",
1502 labels=["texture", "arrangement"]),
1503 dict(n=13, state="open", title="LFO rate too fast on pad filter",
1504 body="0.1Hz LFO creates audible tremolo. Slow to 0.02Hz for imperceptible movement.",
1505 labels=["pad", "generative"]),
1506 dict(n=14, state="open", title="Mono bass under stereo pad causes phase issues",
1507 body="Bass is mono center, pad is 120° wide. Below 200Hz the combination cancels. HPF pad below 250Hz.",
1508 labels=["mix"]),
1509 dict(n=15, state="closed", title="Arp note lengths too uniform",
1510 body="Fixed — gate time now varies from 50% to 90% per note.", labels=["generative"]),
1511 ],
1512 "afrobeat": [
1513 dict(n=1, state="open", title="Talking drum pattern needs more swing",
1514 body="Djembe is perfectly quantized — needs human timing ±5ms.", labels=["groove", "perc"]),
1515 dict(n=2, state="open", title="Highlife guitar pattern clash with bass",
1516 body="Both emphasise beat 1. Guitar should accent beats 2 and 4.", labels=["arrangement"]),
1517 dict(n=3, state="closed", title="Conga timing drift at bar 32",
1518 body="Fixed — re-quantized to 8th note grid.", labels=["perc", "timing"]),
1519 dict(n=4, state="open", title="Brass unison too thick — needs harmony",
1520 body="Four instruments in unison is thin. Split into 3-part harmony.", labels=["horns"]),
1521 dict(n=5, state="open", title="Vocal call-and-response timing off",
1522 body="Response phrases enter 1 beat early. Needs 4-beat gap.", labels=["vocals"]),
1523 dict(n=6, state="open", title="Add agogo bell pattern",
1524 body="The timeline/bell pattern is missing. Essential for afrobeat structure.", labels=["perc"]),
1525 dict(n=7, state="open", title="Bass slap too clicky at high velocity",
1526 body="Velocities above 100 produce unwanted transient click.", labels=["bass"]),
1527 dict(n=8, state="closed", title="Organ swell level too high",
1528 body="Reduced by -4dB. Now sits correctly behind guitar.", labels=["mix"]),
1529 dict(n=9, state="open", title="Yoruba lyric timing — stress on wrong syllable",
1530 body="Need input from native speaker on placement of tonal accent.", labels=["vocals", "cultural"]),
1531 dict(n=10, state="open", title="Add Talking Heads-style guitar texture",
1532 body="Open-string plucked guitar arpeggio on top of the rhythm section.", labels=["guitar"]),
1533 dict(n=11, state="open", title="Shekere part clashes with hi-hat",
1534 body="Both playing 16th pattern in the same register. Pan shekere hard right, hi-hat left.",
1535 labels=["perc", "mix"]),
1536 dict(n=12, state="closed", title="Bass register too muddy below 80Hz",
1537 body="Fixed — high-pass filter at 60Hz with 6dB/oct slope applied.", labels=["mix", "bass"]),
1538 dict(n=13, state="open", title="Trumpet solo needs call-and-response with guitar",
1539 body="Current solo is solo instrument only. Adding guitar responses every 2 bars would honor the tradition.",
1540 labels=["horns", "guitar", "arrangement"]),
1541 dict(n=14, state="open", title="Polyrhythm section needs tempo anchor",
1542 body="The 3-over-2 polyrhythm section lacks a clear pulse anchor. A kick on beat 1 every bar would help.",
1543 labels=["groove", "perc"]),
1544 dict(n=15, state="closed", title="Intro too long — listener disengages",
1545 body="Fixed — trimmed from 16 bars to 8 bars. Groove now enters at bar 9.", labels=["arrangement"]),
1546 ],
1547 "microtonal": [ # REPO_KEY_MAP key: "microtonal"
1548 dict(n=1, state="open", title="31-TET tuning table not loading on export",
1549 body="MIDI export falls back to 12-TET. Need to embed the tuning table in SysEx.", labels=["bug", "midi"]),
1550 dict(n=2, state="open", title="Neutral third interval sounds jarring in context",
1551 body="The 11/9 neutral third in bar 7 needs a resolving phrase. It hangs unresolved.", labels=["harmony"]),
1552 dict(n=3, state="closed", title="Playback pitch drift after bar 48",
1553 body="Fixed — DAW clock sync issue. Resolved by enabling MIDI clock.", labels=["bug"]),
1554 dict(n=4, state="open", title="Add justly-tuned overtone drone",
1555 body="A drone on the 5th partial (5/4 above root) would anchor the spectral harmony.", labels=["texture", "harmony"]),
1556 dict(n=5, state="open", title="Spectral voice leading too disjunct",
1557 body="Leaps of more than 7 steps in 31-TET feel chromatic. Stepwise motion preferred.", labels=["melody"]),
1558 dict(n=6, state="open", title="Cello bow speed inconsistency",
1559 body="Bow speed changes mid-phrase cause unintended dynamics. Normalize velocity curve.", labels=["strings"]),
1560 dict(n=7, state="closed", title="Score notation doesn't reflect microtonal accidentals",
1561 body="Fixed — using Helmholtz-Ellis notation for all quarter-tones.", labels=["notation"]),
1562 dict(n=8, state="open", title="Overtone series segment 8-16 missing",
1563 body="Partials 8-16 not included in the harmonic texture. Add soft flute tones for those partials.",
1564 labels=["harmony", "texture"]),
1565 dict(n=9, state="open", title="Attack transients too sharp in 31-TET scale runs",
1566 body="Fast runs in 31-TET sound percussive. Soften attack to 20ms.", labels=["dynamics"]),
1567 dict(n=10, state="closed", title="Tuning reference pitch wrong",
1568 body="Fixed — set A=432Hz as agreed for this piece.", labels=["tuning"]),
1569 dict(n=11, state="open", title="Add quarter-tone trill in cadential passage",
1570 body="The cadence (bars 22-24) lacks ornament. A quarter-tone trill on the leading tone would help.",
1571 labels=["melody", "ornament"]),
1572 dict(n=12, state="open", title="Sustain pedal creates pitch smear in 31-TET",
1573 body="Held notes at different 31-TET pitches ring together creating beating. Reduce pedal depth.",
1574 labels=["piano", "tuning"]),
1575 dict(n=13, state="open", title="Section 3 needs dynamic arc",
1576 body="Section 3 stays at mf throughout. Build from pp to ff over 16 bars.", labels=["dynamics"]),
1577 dict(n=14, state="closed", title="MIDI velocity map doesn't match 31-TET dynamics",
1578 body="Fixed — remapped velocity curve to match the intended dynamic nuance.", labels=["midi"]),
1579 dict(n=15, state="open", title="Missing rest in bar 19 causes overlap",
1580 body="Violin and cello overlap by one beat in bar 19. Insert an 8th rest.", labels=["notation", "bug"]),
1581 ],
1582 "drums": [ # REPO_KEY_MAP key: "drums" (REPO_DRUM_MACHINE)
1583 dict(n=1, state="open", title="808 kick too short — needs longer decay",
1584 body="Kick envelope decay at 0.1s sounds punchy but loses sub presence. Try 0.4s.", labels=["808", "drums"]),
1585 dict(n=2, state="open", title="Hi-hat pattern too rigid — needs humanize",
1586 body="All hats at 16th grid. Add ±8ms timing offset and velocity 60-90 range.", labels=["groove", "drums"]),
1587 dict(n=3, state="closed", title="Clap reverb tail too long",
1588 body="Fixed — reduced reverb to 0.8s. Clap now sits in the groove.", labels=["mix"]),
1589 dict(n=4, state="open", title="Add polyrhythmic hi-hat ostinato",
1590 body="Current pattern is 4/4 grid. Add a 3-against-4 hi-hat line as a layer.", labels=["groove", "drums"]),
1591 dict(n=5, state="open", title="Modular snare too bright at 4kHz",
1592 body="High transient spike at 4kHz sounds harsh. EQ notch at 4kHz, -4dB, Q=2.", labels=["mix", "drums"]),
1593 dict(n=6, state="closed", title="Kick/bass frequency masking",
1594 body="Fixed — sidechain compression on bass triggered by kick.", labels=["mix"]),
1595 dict(n=7, state="open", title="Pattern variation needed at bar 17",
1596 body="The pattern repeats unmodified for 16 bars. Add a fill at bar 17.", labels=["arrangement", "drums"]),
1597 dict(n=8, state="open", title="808 tuning: needs pitch envelope",
1598 body="Kick pitch stays flat. A fast downward pitch sweep (1 octave, 50ms) would be more musical.",
1599 labels=["808"]),
1600 dict(n=9, state="open", title="Tom fills too frequent",
1601 body="Tom fills every 4 bars interrupt the groove flow. Reduce to every 8 bars.", labels=["drums"]),
1602 dict(n=10, state="closed", title="Crash cymbal sample too long",
1603 body="Fixed — trimmed to 2s with fade-out.", labels=["drums", "mix"]),
1604 dict(n=11, state="open", title="Polyrhythm section: 5-against-4 too abrupt",
1605 body="The shift to 5-against-4 at bar 33 needs 2 bars of transition.", labels=["groove", "arrangement"]),
1606 dict(n=12, state="open", title="Missing ghost note pattern in verse",
1607 body="Verse section lacks ghost notes — pattern sounds flat. Add 16th ghosts at velocity 25-35.",
1608 labels=["drums", "groove"]),
1609 dict(n=13, state="closed", title="Open hi-hat not choked by closed hat",
1610 body="Fixed — added hat-choke controller message at each closed hat hit.", labels=["drums", "midi"]),
1611 dict(n=14, state="open", title="Rimshot too loud relative to snare",
1612 body="Rimshot peaks 3dB above snare. Level down or use snare for fills.", labels=["mix", "drums"]),
1613 dict(n=15, state="open", title="Add shaker for 16th-note pulse reference",
1614 body="The groove loses its feel at slower tempo passages. A shaker pulse would anchor the listener.",
1615 labels=["perc", "groove"]),
1616 ],
1617 "chanson": [
1618 dict(n=1, state="open", title="Piano left hand too busy in verse",
1619 body="Alberti bass pattern is too active for chanson miniature style. Try sparse block chords.",
1620 labels=["piano", "arrangement"]),
1621 dict(n=2, state="open", title="Cello pizzicato needs more resonance",
1622 body="Pizzicato sounds thin at 52 BPM. Add short room reverb to give note length.",
1623 labels=["strings"]),
1624 dict(n=3, state="closed", title="Piano sustain pedal creates blur in slow passage",
1625 body="Fixed — split pedaling technique applied to maintain harmonic clarity.", labels=["piano"]),
1626 dict(n=4, state="open", title="Melody too narrow — stays in A4-E5 range",
1627 body="Expand downward to A3. The lower register gives a more intimate chanson character.",
1628 labels=["melody"]),
1629 dict(n=5, state="open", title="Final cadence needs ritardando",
1630 body="The piece ends metrically. A gradual slow-down over 2 bars would give weight to the ending.",
1631 labels=["arrangement", "dynamics"]),
1632 dict(n=6, state="open", title="Add optional accordion doubling",
1633 body="Chanson tradition supports musette accordion. A soft doubling of the piano melody would be idiomatic.",
1634 labels=["arrangement"]),
1635 dict(n=7, state="closed", title="Cello bowing direction markers missing",
1636 body="Fixed — down-bows on beats 1 and 3, up-bows on 2 and 4.", labels=["strings", "notation"]),
1637 dict(n=8, state="open", title="Bridge modulation to C# minor too abrupt",
1638 body="The pivot chord (E major = shared dominant) should be held for 2 bars before modulating.",
1639 labels=["harmony"]),
1640 dict(n=9, state="open", title="Silence sections too short",
1641 body="Pierre's style uses 4-bar silences. Current rests are only 2 bars — double them.",
1642 labels=["arrangement", "dynamics"]),
1643 dict(n=10, state="closed", title="Notation: accidentals not consistent",
1644 body="Fixed — standardized to sharps throughout (A major context).", labels=["notation"]),
1645 dict(n=11, state="open", title="Piano voicing too wide in left hand",
1646 body="Bass notes below C2 sound muddy on a grand piano. Raise left hand by an octave.",
1647 labels=["piano"]),
1648 dict(n=12, state="open", title="Add pedal marking for the coda",
1649 body="Coda (bars 28-32) has no pedal indication. The color should be hazy — add una corda.",
1650 labels=["piano", "dynamics"]),
1651 dict(n=13, state="open", title="Tempo too fast for lyric melancholy",
1652 body="52 BPM feels hurried for this material. Try 44 BPM — aligns with Pierre's reference recordings.",
1653 labels=["arrangement"]),
1654 dict(n=14, state="closed", title="Cello enters too early in bar 5",
1655 body="Fixed — shifted cello entrance to bar 6 beat 1.", labels=["strings", "timing"]),
1656 dict(n=15, state="open", title="Middle section lacks harmonic tension",
1657 body="The A major tonality is too stable for 8 bars. Introduce a borrowed chord (mode mixture) at bar 20.",
1658 labels=["harmony"]),
1659 ],
1660 "granular": [
1661 dict(n=1, state="open", title="Grain density parameter too uniform",
1662 body="30 grains/sec is constant throughout. Modulate between 5 and 80 for organic feel.",
1663 labels=["generative", "texture"]),
1664 dict(n=2, state="open", title="Source sample quality too clean",
1665 body="Found sounds should be degraded. Add vinyl noise and room tone before granulating.",
1666 labels=["texture"]),
1667 dict(n=3, state="closed", title="Grain size too small — produces clicks",
1668 body="Fixed — minimum grain size set to 40ms (was 5ms). Click-free.", labels=["bug", "generative"]),
1669 dict(n=4, state="open", title="Pitch randomization range too wide",
1670 body="±1 octave pitch spread sounds noisy, not musical. Constrain to ±major 3rd.",
1671 labels=["generative", "pitch"]),
1672 dict(n=5, state="open", title="Add grain position automation over time",
1673 body="Reading from fixed position 0.3 in the source. Automate position 0.0→1.0 over 4 minutes.",
1674 labels=["generative"]),
1675 dict(n=6, state="closed", title="Stereo pan spread too narrow",
1676 body="Fixed — grain pan randomization set to ±45° (was ±10°).", labels=["mix"]),
1677 dict(n=7, state="open", title="Texture layer too loud vs piano layer",
1678 body="Granular texture sits 6dB above the piano melody. Attenuate texture by -6dB.", labels=["mix"]),
1679 dict(n=8, state="open", title="Add resonant filter sweep through granular cloud",
1680 body="A slow bandpass filter sweep (0.05Hz) through the grain cloud would create hypnotic movement.",
1681 labels=["texture", "generative"]),
1682 dict(n=9, state="open", title="Grain envelope too flat — no transients",
1683 body="All grains use linear envelope. Add a fast attack (2ms) + slow decay for percussion-like texture.",
1684 labels=["generative"]),
1685 dict(n=10, state="closed", title="Found sound source too recognizable",
1686 body="Fixed — source pitch-shifted and time-stretched until original is unrecognizable.", labels=["texture"]),
1687 dict(n=11, state="open", title="Feedback loop creates unwanted oscillation",
1688 body="Grain output fed back into input causes 12Hz oscillation at high density. Add DC blocker.",
1689 labels=["bug", "generative"]),
1690 dict(n=12, state="open", title="Density automation ramp too abrupt",
1691 body="The jump from 5 to 80 grains/sec happens over 1 bar. Needs 4-bar ramp for smooth transition.",
1692 labels=["generative", "arrangement"]),
1693 dict(n=13, state="closed", title="Grain position quantized to beat grid",
1694 body="Fixed — position now continuous with subtle clock jitter (±10ms).", labels=["generative"]),
1695 dict(n=14, state="open", title="Violin source needs more bow noise character",
1696 body="Current sample is too pure. A sul ponticello bowing noise layer would add grit.", labels=["texture"]),
1697 dict(n=15, state="open", title="Outro needs silence interruption",
1698 body="The granular outro should be punctuated by 500ms silences every 8 bars — gaps in the cloud.",
1699 labels=["arrangement", "texture"]),
1700 ],
1701 "funk-suite": [ # REPO_KEY_MAP key: "funk-suite" (REPO_FUNK_SUITE)
1702 dict(n=1, state="open", title="Electric piano comping too dense in verse",
1703 body="Clavinet and Rhodes both comp simultaneously. Pick one per section.", labels=["arrangement"]),
1704 dict(n=2, state="open", title="Wah bass envelope too slow",
1705 body="Wah envelope follows ADSR but attack is 80ms — loses the click. Set to 10ms.", labels=["bass"]),
1706 dict(n=3, state="closed", title="Hi-hat and shaker doubling causes flamming",
1707 body="Fixed — hi-hat quantized to 16th grid, shaker humanized separately.", labels=["drums", "timing"]),
1708 dict(n=4, state="open", title="Horns need staccato articulation in bars 9-16",
1709 body="Horn stabs are held too long. 16th-note staccato would give the funk punch.", labels=["horns"]),
1710 dict(n=5, state="open", title="Clavinet tone too trebly",
1711 body="Clavinet without a low-pass filter sounds harsh. A gentle 3kHz shelf would smooth it.",
1712 labels=["mix"]),
1713 dict(n=6, state="closed", title="Groove falls apart at bar 25",
1714 body="Fixed — kick pattern re-programmed with 16th anticipation on beat 3.", labels=["groove", "drums"]),
1715 dict(n=7, state="open", title="Movement IV needs a climactic peak",
1716 body="Movement IV builds but never peaks before the final cadence. Add a unison hit at bar 48.",
1717 labels=["arrangement", "dynamics"]),
1718 dict(n=8, state="open", title="Bass slap velocity too uniform",
1719 body="All slap notes at velocity 110. Alternate strong (120) and weak (90) for dynamic groove.",
1720 labels=["bass", "groove"]),
1721 dict(n=9, state="open", title="Pocket drum fills too predictable at phrase ends",
1722 body="Fill every 4 bars, always a snare run. Vary: sometimes a kick + tom, sometimes silence.",
1723 labels=["drums", "arrangement"]),
1724 dict(n=10, state="closed", title="Chord voicings too thick in bridge",
1725 body="Fixed — reduced to 3-voice drop-2 voicing in brass section.", labels=["harmony"]),
1726 dict(n=11, state="open", title="Add octave unison in horn section for climax",
1727 body="The climax in Movement III (bar 56) lacks weight. Add trombones an octave below trumpets.",
1728 labels=["horns", "arrangement"]),
1729 dict(n=12, state="open", title="Movement II transition too abrupt",
1730 body="Movement I ends, Movement II starts without transition. Add 2-bar breakdown.",
1731 labels=["arrangement"]),
1732 dict(n=13, state="closed", title="Clavinet out of tune with Rhodes",
1733 body="Fixed — both set to A=440 reference. Clavinet detuned by +12 cents.", labels=["tuning"]),
1734 dict(n=14, state="open", title="Shaker too loud in mix",
1735 body="Shaker sits 4dB above hi-hat in the mid-range. Reduce shaker -4dB.", labels=["mix"]),
1736 dict(n=15, state="open", title="Final movement needs a proper ending",
1737 body="Suite ends with a fade-out which is too passive. Write a 4-bar coda with a unison hit.",
1738 labels=["arrangement"]),
1739 ],
1740 "jazz-trio": [ # REPO_KEY_MAP key: "jazz-trio" (REPO_JAZZ_TRIO)
1741 dict(n=1, state="open", title="Piano left hand too busy during bass solo",
1742 body="Left-hand comp fills every bar during the bass solo — player needs space.", labels=["piano", "arrangement"]),
1743 dict(n=2, state="open", title="Brushed snare too loud in A section",
1744 body="Brushes are competing with piano in the same frequency range. Reduce snare by -3dB.",
1745 labels=["drums", "mix"]),
1746 dict(n=3, state="closed", title="Walking bass accidentally doubles piano left hand",
1747 body="Fixed — bass transposed up a 10th in bars 5-8 to avoid doubling.", labels=["bass"]),
1748 dict(n=4, state="open", title="Tempo rushes during piano solo",
1749 body="Drummer accelerates during piano solo — common problem. Add a click track reference.",
1750 labels=["tempo", "groove"]),
1751 dict(n=5, state="open", title="Add ritardando at end of each chorus",
1752 body="Each 32-bar chorus ends metrically. A slight rit in the last 2 bars would honor the standard.",
1753 labels=["arrangement", "dynamics"]),
1754 dict(n=6, state="closed", title="Bass pizzicato too short — sounds staccato",
1755 body="Fixed — gate time extended to 90% of note duration.", labels=["bass"]),
1756 dict(n=7, state="open", title="Cymbal swell missing at the top of each chorus",
1757 body="A ride cymbal swell on beat 4 of bar 32 would signal the chorus repeat elegantly.",
1758 labels=["drums", "arrangement"]),
1759 dict(n=8, state="open", title="Piano voicing too sparse in outer choruses",
1760 body="Shell voicings (root + 7th only) are too thin in the outer A sections. Add 3rds.",
1761 labels=["piano", "harmony"]),
1762 dict(n=9, state="open", title="Standards melody not centered in mix",
1763 body="Piano melody sits behind the rhythm section. Bump piano +2dB during head.", labels=["mix"]),
1764 dict(n=10, state="closed", title="Bass note durations overlap chord changes",
1765 body="Fixed — note-offs now aligned to beat boundaries before chord changes.", labels=["bass"]),
1766 dict(n=11, state="open", title="Add sus chord before final turnaround",
1767 body="The final turnaround (bars 29-32) lacks a sus chord to build tension before the resolution.",
1768 labels=["harmony"]),
1769 dict(n=12, state="open", title="Ride cymbal bell too prominent",
1770 body="Bell accent on beat 2 and 4 cuts through the texture. Use shoulder of stick instead.",
1771 labels=["drums"]),
1772 dict(n=13, state="closed", title="Piano octaves in outro too thick",
1773 body="Fixed — reduced to single melody line in the final 4 bars.", labels=["piano"]),
1774 dict(n=14, state="open", title="Tag repeat needs extra bar",
1775 body="Standard convention adds 1 extra bar at the final tag. Currently not present.",
1776 labels=["arrangement"]),
1777 dict(n=15, state="open", title="Double-time feel section needs hi-hat switch",
1778 body="During double-time feel (bars 17-24), hi-hat should move to 2-beat pattern. Currently stays on 4.",
1779 labels=["drums", "groove"]),
1780 ],
1781 }
1782
1783 # Use a generic template for repos without specific issue templates (fork repos)
1784 GENERIC_ISSUES = [
1785 dict(n=1, state="open", title="Energy drops in the middle section",
1786 body="The arrangement loses momentum around bar 24-32. Add element to sustain interest.",
1787 labels=["arrangement", "energy"]),
1788 dict(n=2, state="open", title="Dynamics too compressed",
1789 body="The quietest and loudest moments are within 3dB. Needs more dynamic range.",
1790 labels=["mix", "dynamics"]),
1791 dict(n=3, state="closed", title="Tempo inconsistency between sections",
1792 body="Fixed by applying strict quantize to all MIDI.", labels=["timing"]),
1793 dict(n=4, state="open", title="Add a counter-melody",
1794 body="The main melody is unaccompanied for too long. Add a secondary voice.",
1795 labels=["arrangement"]),
1796 dict(n=5, state="open", title="Harmonic rhythm too fast in verse",
1797 body="Chord changes every 2 beats feels rushed. Try 4-beat chord duration.",
1798 labels=["harmony"]),
1799 dict(n=6, state="closed", title="Mix: low end muddy",
1800 body="Resolved — high-pass filter below 80Hz on all non-bass instruments.",
1801 labels=["mix"]),
1802 dict(n=7, state="open", title="Transition between sections too abrupt",
1803 body="The jump from section A to B lacks a linking phrase. Add a 2-bar turnaround.",
1804 labels=["arrangement"]),
1805 dict(n=8, state="open", title="Lead instrument too forward in mix",
1806 body="The melody sits 6dB above the supporting texture. Reduce by 3dB and add subtle delay.",
1807 labels=["mix"]),
1808 dict(n=9, state="closed", title="Reverb tail bleeds into silence",
1809 body="Fixed — reduced reverb pre-delay to 20ms, decay to 1.5s.", labels=["mix"]),
1810 dict(n=10, state="open", title="Add introduction before main theme",
1811 body="The piece starts on the main theme with no setup. A 4-bar intro would establish context.",
1812 labels=["arrangement"]),
1813 dict(n=11, state="open", title="Velocity variation too narrow",
1814 body="All MIDI velocities within 90-110 range. Expand to 60-127 for natural expression.",
1815 labels=["dynamics"]),
1816 dict(n=12, state="open", title="Stereo field too narrow",
1817 body="Mix is mostly center-panned. Pan secondary voices hard left/right for width.",
1818 labels=["mix"]),
1819 dict(n=13, state="closed", title="Quantization too tight — sounds mechanical",
1820 body="Fixed — applied 75% quantize (humanize 25%).", labels=["groove", "timing"]),
1821 dict(n=14, state="open", title="Ending lacks finality",
1822 body="The piece fades out rather than closing with a defined cadence. Write a 2-bar coda.",
1823 labels=["arrangement"]),
1824 dict(n=15, state="open", title="High frequency content harsh on headphones",
1825 body="Content above 8kHz is piercing. A gentle high shelf cut -3dB above 8kHz would help.",
1826 labels=["mix"]),
1827 ]
1828
1829
1830 # ---------------------------------------------------------------------------
1831 # Milestone templates per repo-key — title, description, state, due offset
1832 # ---------------------------------------------------------------------------
1833
1834 MILESTONE_TEMPLATES: dict[str, list[dict[str, Any]]] = {
1835 "neo-soul": [
1836 dict(n=1, title="Album v1.0", state="open",
1837 description="Full release of Neo-Soul Experiment Vol. 1. All tracks mixed and mastered.",
1838 due_days=60),
1839 dict(n=2, title="Mixing Complete", state="closed",
1840 description="All tracks signed off by mixing engineer. Ready for mastering.",
1841 due_days=None),
1842 ],
1843 "modal-jazz": [
1844 dict(n=1, title="Session Complete", state="open",
1845 description="All Modal Jazz Sketches tracks recorded and approved.",
1846 due_days=30),
1847 ],
1848 "ambient": [
1849 dict(n=1, title="Vol. 1 Complete", state="open",
1850 description="All Ambient Textures Vol. 1 compositions finalised.",
1851 due_days=45),
1852 dict(n=2, title="Mastering Done", state="closed",
1853 description="Mastering session at Abbey Road complete.",
1854 due_days=None),
1855 ],
1856 "afrobeat": [
1857 dict(n=1, title="Album Launch", state="open",
1858 description="Afrobeat Grooves album release. All 12 tracks production-ready.",
1859 due_days=30),
1860 dict(n=2, title="v1.0 Recording", state="closed",
1861 description="All live tracking sessions completed.",
1862 due_days=None),
1863 ],
1864 "microtonal": [
1865 dict(n=1, title="Études Complete", state="open",
1866 description="All 10 microtonal études composed, engraved, and recorded.",
1867 due_days=90),
1868 ],
1869 "drums": [
1870 dict(n=1, title="808 Variations v1.0", state="open",
1871 description="All drum variations composed and exported as stems.",
1872 due_days=20),
1873 ],
1874 "chanson": [
1875 dict(n=1, title="Score Publication", state="open",
1876 description="Score submitted to publisher for Chanson Minimale edition.",
1877 due_days=45),
1878 ],
1879 "granular": [
1880 dict(n=1, title="Research Complete", state="open",
1881 description="All granular synthesis research documented and recordings exported.",
1882 due_days=60),
1883 ],
1884 "funk-suite": [
1885 dict(n=1, title="Suite Release", state="open",
1886 description="Funk Suite No. 1 — all four movements completed and sequenced.",
1887 due_days=25),
1888 dict(n=2, title="Mvt. I–II Done", state="closed",
1889 description="Movements I and II approved by the full ensemble.",
1890 due_days=None),
1891 ],
1892 "jazz-trio": [
1893 dict(n=1, title="Album Complete", state="open",
1894 description="Jazz Trio Sessions album — all takes selected and arranged.",
1895 due_days=40),
1896 ],
1897 }
1898
1899 # Issues in each milestone (by issue number n) — controls milestone_id assignment
1900 MILESTONE_ISSUE_ASSIGNMENTS: dict[str, dict[int, list[int]]] = {
1901 # key: repo_key → {milestone_n: [issue_n, ...]}
1902 "neo-soul": {1: [1, 2, 4, 5, 7, 8, 10, 11, 12, 14, 15],
1903 2: [3, 6, 9, 13]},
1904 "modal-jazz": {1: [1, 2, 4, 5, 6, 8, 9, 11, 12, 13, 15]},
1905 "ambient": {1: [1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 15],
1906 2: [3, 7, 11]},
1907 "afrobeat": {1: [1, 2, 4, 5, 6, 7, 9, 10, 11, 13, 14, 15],
1908 2: [3, 8, 12]},
1909 "microtonal": {1: [1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 15]},
1910 "drums": {1: [1, 2, 4, 5, 7, 8, 9, 10, 12, 13, 14, 15]},
1911 "chanson": {1: [1, 2, 4, 5, 6, 8, 9, 10, 11, 12, 13, 15]},
1912 "granular": {1: [1, 2, 4, 5, 8, 9, 10, 12, 14, 15]},
1913 "funk-suite": {1: [1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 15],
1914 2: [3, 7, 11]},
1915 "jazz-trio": {1: [1, 2, 4, 5, 7, 8, 9, 10, 12, 13, 14, 15]},
1916 }
1917
1918
1919 # ---------------------------------------------------------------------------
1920 # Issue comment templates
1921 # ---------------------------------------------------------------------------
1922
1923 ISSUE_COMMENT_BODIES: list[str] = [
1924 "Agreed — I noticed this too during the last session. @{mention} have you tried adjusting the velocity curve?",
1925 "Good catch. The `{track}` track in `section:{section}` is definitely the culprit here.",
1926 "I think we can fix this with:\n```python\n# Adjust humanization range\nhumanize_ms = 12 # was 5\nvelocity_range = (40, 90) # was (70, 80)\n```",
1927 "This has been bothering me since the first mix. The `beats:{beats}` region needs attention.",
1928 "@{mention} — can you take a look? This is blocking the v1.0 milestone.",
1929 "Fixed in my local branch. The root cause was the `{track}` MIDI channel assignment. Will open a PR.",
1930 "I ran an analysis on the affected region:\n```\nFreq: {freq}Hz Peak: -6dBFS Phase: +12°\n```\nNeeds a notch filter.",
1931 "Confirmed on my system. Happens consistently at bar {bar}. The {track} seems off.",
1932 "Not sure this is the right approach. @{mention} what do you think about using a different technique?",
1933 "This is now tracked in the v1.0 milestone. Should be resolved before release.",
1934 "After further listening, the issue is more subtle than I initially thought. The `section:{section}` transition is the real problem.",
1935 "Tested the fix — sounds much better now. The `track:{track}` now sits properly in the mix.",
1936 "Adding context: this is related to #3 which had the same root cause in `section:{section}`.",
1937 "I think we should prioritize this. The groove feels off and it's the first thing listeners will notice.",
1938 "Will take a pass at this during the next session. @{mention} — can you prepare a reference recording?",
1939 ]
1940
1941 ISSUE_COMMENT_MENTIONS = ["gabriel", "sofia", "marcus", "yuki", "aaliya", "chen", "fatou", "pierre"]
1942 ISSUE_COMMENT_TRACKS = ["bass", "keys", "drums", "strings", "horns", "guitar", "vocals", "pad"]
1943 ISSUE_COMMENT_SECTIONS = ["intro", "verse", "chorus", "bridge", "breakdown", "coda", "outro"]
1944 ISSUE_COMMENT_FREQS = ["80", "200", "400", "800", "2000", "4000", "8000"]
1945
1946
1947 def _make_issue_comment_body(seed: int) -> str:
1948 """Generate a realistic issue comment body with @mention, track refs, and code blocks."""
1949 template = ISSUE_COMMENT_BODIES[seed % len(ISSUE_COMMENT_BODIES)]
1950 mention = ISSUE_COMMENT_MENTIONS[(seed + 1) % len(ISSUE_COMMENT_MENTIONS)]
1951 track = ISSUE_COMMENT_TRACKS[seed % len(ISSUE_COMMENT_TRACKS)]
1952 section = ISSUE_COMMENT_SECTIONS[(seed + 2) % len(ISSUE_COMMENT_SECTIONS)]
1953 bar = (seed % 32) + 1
1954 freq = ISSUE_COMMENT_FREQS[seed % len(ISSUE_COMMENT_FREQS)]
1955 return (template
1956 .replace("{mention}", mention)
1957 .replace("{track}", track)
1958 .replace("{section}", section)
1959 .replace("{bar}", str(bar))
1960 .replace("{beats}", f"{bar}-{bar+4}")
1961 .replace("{freq}", freq))
1962
1963
1964 def _make_issue_musical_refs(body: str) -> list[dict[str, str]]:
1965 """Extract musical context references from a comment body."""
1966 import re
1967 refs: list[dict[str, str]] = []
1968 for m in re.finditer(r"track:(\w+)", body):
1969 refs.append({"type": "track", "value": m.group(1)})
1970 for m in re.finditer(r"section:(\w+)", body):
1971 refs.append({"type": "section", "value": m.group(1)})
1972 for m in re.finditer(r"beats:(\d+-\d+)", body):
1973 refs.append({"type": "beats", "value": m.group(1)})
1974 return refs
1975
1976
1977 # ---------------------------------------------------------------------------
1978 # PR templates
1979 # ---------------------------------------------------------------------------
1980
1981 def _make_prs(repo_id: str, commits: list[dict[str, Any]], owner: str) -> list[dict[str, Any]]:
1982 """Generate 4 template pull requests (open, merged, open, closed) for a repo."""
1983 if len(commits) < 4:
1984 return []
1985 c = commits
1986 return [
1987 dict(pr_id=_uid(f"pr-{repo_id}-1"), repo_id=repo_id,
1988 title="Feat: add counter-melody layer",
1989 body="## Changes\nAdds secondary melodic voice.\n\n## Analysis\nHarmonic tension +0.08.",
1990 state="open", from_branch="feat/counter-melody", to_branch="main",
1991 author=owner,
1992 created_at=_now(days=6)),
1993 dict(pr_id=_uid(f"pr-{repo_id}-2"), repo_id=repo_id,
1994 title="Refactor: humanize all MIDI timing",
1995 body="Applied `muse humanize --natural` to all tracks. Groove score +0.12.",
1996 state="merged", from_branch="fix/humanize-midi", to_branch="main",
1997 merge_commit_id=c[-3]["commit_id"],
1998 author=owner,
1999 created_at=_now(days=14)),
2000 dict(pr_id=_uid(f"pr-{repo_id}-3"), repo_id=repo_id,
2001 title="Experiment: alternate bridge harmony",
2002 body="Trying a tritone substitution approach for the bridge section.",
2003 state="open", from_branch="experiment/bridge-harmony", to_branch="main",
2004 author=owner,
2005 created_at=_now(days=3)),
2006 dict(pr_id=_uid(f"pr-{repo_id}-4"), repo_id=repo_id,
2007 title="Fix: resolve voice-leading errors",
2008 body="Parallel 5ths in bars 7-8 and parallel octaves in bars 15-16 corrected.",
2009 state="closed", from_branch="fix/voice-leading", to_branch="main",
2010 author=owner,
2011 created_at=_now(days=20)),
2012 ]
2013
2014
2015 # ---------------------------------------------------------------------------
2016 # Release templates
2017 # ---------------------------------------------------------------------------
2018
2019 def _make_releases(repo_id: str, commits: list[dict[str, Any]], repo_name: str, owner: str) -> list[dict[str, Any]]:
2020 """Generate 3 releases (v0.1.0 draft, v0.2.0 arrangement, v1.0.0 full) for a repo.
2021
2022 Each release dict includes a deterministic ``release_id`` so downstream
2023 code (release assets seeding) can reference it without a separate DB query.
2024 """
2025 if not commits:
2026 return []
2027 return [
2028 dict(release_id=_uid(f"release-{repo_id}-v0.1.0"),
2029 repo_id=repo_id, tag="v0.1.0", title="Early Draft",
2030 body=f"## v0.1.0 — Early Draft\n\nFirst checkpoint. Basic groove locked in.\n\n### Tracks\n- Main groove\n- Bass foundation\n\n### Technical\nInitial BPM and key established.",
2031 commit_id=commits[min(4, len(commits)-1)]["commit_id"],
2032 download_urls={"midi_bundle": f"/releases/{repo_id}-v0.1.0.zip"},
2033 author=owner,
2034 created_at=_now(days=45)),
2035 dict(release_id=_uid(f"release-{repo_id}-v0.2.0"),
2036 repo_id=repo_id, tag="v0.2.0", title="Arrangement Draft",
2037 body=f"## v0.2.0 — Arrangement Draft\n\nAll major sections sketched.\n\n### What's new\n- Additional instrument layers\n- Section transitions defined\n- Dynamic arc mapped",
2038 commit_id=commits[min(12, len(commits)-1)]["commit_id"],
2039 download_urls={"midi_bundle": f"/releases/{repo_id}-v0.2.0.zip", "mp3": f"/releases/{repo_id}-v0.2.0.mp3"},
2040 author=owner,
2041 created_at=_now(days=25)),
2042 dict(release_id=_uid(f"release-{repo_id}-v1.0.0"),
2043 repo_id=repo_id, tag="v1.0.0", title=f"{repo_name} — Full Release",
2044 body=f"## v1.0.0 — Full Release\n\nProduction-ready state.\n\n### Highlights\n- Complete arrangement with all instruments\n- Mixed and mastered\n- Stems included\n\n### Downloads\nMIDI bundle, MP3 stereo mix, individual stems",
2045 commit_id=commits[-1]["commit_id"],
2046 download_urls={"midi_bundle": f"/releases/{repo_id}-v1.0.0.zip",
2047 "mp3": f"/releases/{repo_id}-v1.0.0.mp3",
2048 "stems": f"/releases/{repo_id}-v1.0.0-stems.zip"},
2049 author=owner,
2050 created_at=_now(days=5)),
2051 ]
2052
2053
2054 # ---------------------------------------------------------------------------
2055 # Session templates
2056 # ---------------------------------------------------------------------------
2057
2058 def _make_sessions(repo_id: str, owner: str, commits: list[dict[str, Any]]) -> list[dict[str, Any]]:
2059 """Generate 6 collaboration sessions per repo; adds a live session for high-traffic repos."""
2060 if len(commits) < 2:
2061 return []
2062 sess = []
2063 collab_map: dict[str, list[tuple[str, ...]]] = {
2064 REPO_NEO_SOUL: [("gabriel", "marcus"), ("gabriel",), ("gabriel", "marcus", "aaliya")],
2065 REPO_MODAL_JAZZ: [("gabriel", "marcus"), ("gabriel",)],
2066 REPO_AMBIENT: [("sofia", "yuki"), ("sofia",), ("sofia", "pierre")],
2067 REPO_AFROBEAT: [("aaliya", "fatou"), ("aaliya",), ("aaliya", "marcus")],
2068 REPO_FUNK_SUITE: [("marcus", "gabriel"), ("marcus",)],
2069 REPO_JAZZ_TRIO: [("marcus",), ("marcus", "gabriel")],
2070 REPO_DRUM_MACHINE: [("fatou",), ("fatou", "aaliya")],
2071 REPO_CHANSON: [("pierre",), ("pierre", "sofia")],
2072 REPO_GRANULAR: [("yuki",), ("yuki", "sofia")],
2073 REPO_MICROTONAL: [("chen",)],
2074 }
2075 collab_groups: list[tuple[str, ...]] = collab_map.get(repo_id) or [(owner,)]
2076 locations = [
2077 "Studio A, São Paulo", "Home studio", "Remote",
2078 "Abbey Road Studio 3", "Electric Lady Studios", "Remote (async)",
2079 "Bedroom studio, Tokyo", "La Fabrique, Marseille",
2080 ]
2081 for i, group in enumerate((collab_groups * 3)[:6]):
2082 start_days = 60 - i * 8
2083 dur_hours = [3, 4, 2, 5, 2, 3][i % 6]
2084 commit_slice = commits[i * 4:i * 4 + 3] if len(commits) > i * 4 + 3 else commits[-2:]
2085 sess.append(dict(
2086 session_id=_uid(f"sess-{repo_id}-{i}"),
2087 repo_id=repo_id,
2088 started_at=_now(days=start_days),
2089 ended_at=_now(days=start_days, hours=-dur_hours),
2090 participants=list(group),
2091 location=locations[i % len(locations)],
2092 intent=f"Session {i+1}: extend arrangement and refine mix",
2093 commits=[c["commit_id"] for c in commit_slice],
2094 notes=f"Productive session. Focused on {'groove' if i % 2 == 0 else 'arrangement'}.",
2095 is_active=False,
2096 created_at=_now(days=start_days),
2097 ))
2098 # Add one live/active session for the first big repo
2099 if repo_id in (REPO_NEO_SOUL, REPO_AFROBEAT):
2100 sess.append(dict(
2101 session_id=_uid(f"sess-{repo_id}-live"),
2102 repo_id=repo_id,
2103 started_at=_now(hours=1),
2104 ended_at=None,
2105 participants=[owner, "marcus"],
2106 location="Studio A — Live",
2107 intent="Live recording session — tracking final takes",
2108 commits=[],
2109 notes="",
2110 is_active=True,
2111 created_at=_now(hours=1),
2112 ))
2113 return sess
2114
2115
2116 # ---------------------------------------------------------------------------
2117 # Webhook + delivery templates
2118 # ---------------------------------------------------------------------------
2119
2120 def _make_webhooks(
2121 repo_id: str, owner: str
2122 ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
2123 """Generate 3 webhook subscriptions and 10–15 deliveries per webhook.
2124
2125 Delivery outcomes cycle through 200 (success), 500 (server error), and
2126 0/timeout patterns so every status is represented in the dataset.
2127 Returns (webhooks, deliveries).
2128 """
2129 webhooks: list[dict[str, Any]] = []
2130 deliveries: list[dict[str, Any]] = []
2131
2132 wh_configs = [
2133 dict(suffix="push", url=f"https://hooks.example.com/{owner}/push",
2134 events=["push"], active=True),
2135 dict(suffix="pr", url=f"https://hooks.example.com/{owner}/pr",
2136 events=["pull_request", "issue"], active=True),
2137 dict(suffix="release", url=f"https://hooks.example.com/{owner}/release",
2138 events=["release"], active=False),
2139 ]
2140
2141 for wh_spec in wh_configs:
2142 wh_suffix: str = wh_spec["suffix"] # type: ignore[assignment]
2143 wh_url: str = wh_spec["url"] # type: ignore[assignment]
2144 wh_events: list[str] = wh_spec["events"] # type: ignore[assignment]
2145 wh_active: bool = wh_spec["active"] # type: ignore[assignment]
2146 wh_id = _uid(f"wh-{repo_id}-{wh_suffix}")
2147 webhooks.append(dict(
2148 webhook_id=wh_id,
2149 repo_id=repo_id,
2150 url=wh_url,
2151 events=wh_events,
2152 secret=_sha(f"secret-{repo_id}-{wh_suffix}")[:32],
2153 active=wh_active,
2154 created_at=_now(days=60),
2155 ))
2156
2157 # 10–15 deliveries per webhook; cycle through status patterns.
2158 n_deliveries = 10 + (int(_sha(f"nd-{repo_id}-{wh_suffix}")[:2], 16) % 6)
2159 for j in range(n_deliveries):
2160 # Pattern (period 7): 200, 200, 500, 200, 200, timeout, 200
2161 pattern = j % 7
2162 if pattern in (0, 1, 3, 4, 6):
2163 success, status, resp_body = True, 200, '{"ok": true}'
2164 elif pattern == 2:
2165 success, status, resp_body = False, 500, '{"error": "Internal Server Error"}'
2166 else: # pattern == 5 → timeout
2167 success, status, resp_body = False, 0, ""
2168
2169 event_type = wh_events[j % len(wh_events)]
2170 payload_data = (
2171 f'{{"event": "{event_type}", "repo": "{repo_id}", '
2172 f'"attempt": {(j % 3) + 1}, "ts": "{_now(days=j).isoformat()}"}}'
2173 )
2174 deliveries.append(dict(
2175 delivery_id=_uid(f"del-{wh_id}-{j}"),
2176 webhook_id=wh_id,
2177 event_type=event_type,
2178 payload=payload_data,
2179 attempt=(j % 3) + 1,
2180 success=success,
2181 response_status=status,
2182 response_body=resp_body,
2183 delivered_at=_now(days=j),
2184 ))
2185
2186 return webhooks, deliveries
2187
2188
2189 # ---------------------------------------------------------------------------
2190 # PR comment templates
2191 # ---------------------------------------------------------------------------
2192
2193 _PR_COMMENT_POOL: list[dict[str, Any]] = [
2194 dict(target_type="general", target_track=None, target_beat_start=None,
2195 target_beat_end=None, target_note_pitch=None,
2196 body="Overall approach looks good. The counter-melody adds the harmonic tension the arrangement was missing. LGTM with minor comments below."),
2197 dict(target_type="track", target_track="bass", target_beat_start=None,
2198 target_beat_end=None, target_note_pitch=None,
2199 body="The bass track humanization is much improved. Ghost notes at beats 2.5 and 3.5 are well-placed.\n\n> Suggest reducing velocity on the ghost at beat 3.5 by 10 units."),
2200 dict(target_type="region", target_track="keys", target_beat_start=9.0,
2201 target_beat_end=17.0, target_note_pitch=None,
2202 body="Bars 9-16: the Rhodes voicing here feels crowded. Try removing the 5th — root + 3rd + 7th is cleaner."),
2203 dict(target_type="note", target_track="trumpet", target_beat_start=24.0,
2204 target_beat_end=None, target_note_pitch=84,
2205 body="This high C (MIDI 84) at beat 24 is above idiomatic range. Transpose down an octave to C5 (MIDI 72)."),
2206 dict(target_type="general", target_track=None, target_beat_start=None,
2207 target_beat_end=None, target_note_pitch=None,
2208 body="The tritone substitution is elegant. I'd approve as-is but @gabriel should confirm alignment with the arrangement plan."),
2209 dict(target_type="track", target_track="drums", target_beat_start=None,
2210 target_beat_end=None, target_note_pitch=None,
2211 body="Drum humanization is a big improvement. Hi-hat timing now feels natural.\n\nOne nit: the ride bell at beat 4 is slightly too loud — try velocity 85 instead of 100."),
2212 dict(target_type="region", target_track="strings", target_beat_start=25.0,
2213 target_beat_end=33.0, target_note_pitch=None,
2214 body="Bars 25-32: pizzicato countermelody is beautifully voiced. Staggered entries work well. No changes needed."),
2215 dict(target_type="note", target_track="bass", target_beat_start=13.0,
2216 target_beat_end=None, target_note_pitch=42,
2217 body="This F# (MIDI 42) at beat 13 creates a very dark sub. If intentional — great. If not, try B1 (MIDI 47)."),
2218 dict(target_type="general", target_track=None, target_beat_start=None,
2219 target_beat_end=None, target_note_pitch=None,
2220 body="Reviewed all four parallel-5th instances. Bars 7-8 and 15-16 are fixed. Bars 23-24 still have a parallel octave between violin and cello — please fix before merging."),
2221 dict(target_type="track", target_track="vocals", target_beat_start=None,
2222 target_beat_end=None, target_note_pitch=None,
2223 body="Vocal sibilance is still present on sustained S sounds. De-esser threshold needs to be 3dB lower. Otherwise the PR is ready."),
2224 ]
2225
2226
2227 def _make_pr_comments(
2228 pr_id: str, repo_id: str, pr_n: int, owner: str, days_ago: int
2229 ) -> list[dict[str, Any]]:
2230 """Generate 3–8 inline review comments for a single pull request.
2231
2232 Uses the rotating _PR_COMMENT_POOL with author cycling across the user roster.
2233 Some comments thread as replies via parent_comment_id.
2234 """
2235 pool_size = len(_PR_COMMENT_POOL)
2236 authors = ["gabriel", "sofia", "marcus", "yuki", "aaliya", "chen", "fatou", "pierre"]
2237 n_comments = 3 + (pr_n % 6) # yields 3–8 per PR
2238 comments: list[dict[str, Any]] = []
2239 first_comment_id: str | None = None
2240
2241 for i in range(n_comments):
2242 pool_entry = _PR_COMMENT_POOL[(pr_n * 5 + i * 3) % pool_size]
2243 author = authors[(pr_n + i + 2) % len(authors)]
2244 if author == owner and i == 0:
2245 author = authors[(pr_n + i + 3) % len(authors)]
2246
2247 comment_id = _uid(f"pr-cmt-{repo_id}-{pr_n}-{i}")
2248 parent_id: str | None = None
2249 # Comments 3+ thread under the first comment to simulate replies.
2250 if i >= 3 and first_comment_id is not None:
2251 parent_id = first_comment_id
2252
2253 comments.append(dict(
2254 comment_id=comment_id,
2255 pr_id=pr_id,
2256 repo_id=repo_id,
2257 author=author,
2258 body=pool_entry["body"],
2259 target_type=pool_entry["target_type"],
2260 target_track=pool_entry["target_track"],
2261 target_beat_start=pool_entry["target_beat_start"],
2262 target_beat_end=pool_entry["target_beat_end"],
2263 target_note_pitch=pool_entry["target_note_pitch"],
2264 parent_comment_id=parent_id,
2265 created_at=_now(days=days_ago - i),
2266 ))
2267 if i == 0:
2268 first_comment_id = comment_id
2269
2270 return comments
2271
2272
2273 # ---------------------------------------------------------------------------
2274 # Main seed function
2275 # ---------------------------------------------------------------------------
2276
2277 async def seed(db: AsyncSession, force: bool = False) -> None:
2278 """Populate all MuseHub tables with a realistic stress-test dataset.
2279
2280 Inserts users, repos, commits, branches, issues, PRs, releases, sessions,
2281 social graph (stars, follows, watches, comments, reactions, notifications,
2282 forks, view/download events), and the full Muse VCS layer (objects,
2283 snapshots, commits, tags). Pass force=True to wipe and re-seed existing data.
2284 """
2285 print("🌱 Seeding MuseHub stress-test dataset…")
2286
2287 result = await db.execute(text("SELECT COUNT(*) FROM musehub_repos"))
2288 existing = result.scalar() or 0
2289
2290 if existing > 0 and not force:
2291 print(f" ⚠️ {existing} repo(s) already exist — skipping. Pass --force to wipe and reseed.")
2292 _print_urls()
2293 return
2294
2295 if existing > 0 and force:
2296 print(" 🗑 --force: clearing existing seed data…")
2297 for tbl in [
2298 # Conversation children first
2299 "muse_message_actions", "muse_conversation_messages",
2300 "muse_conversations", "muse_usage_logs", "muse_access_tokens",
2301 # Muse variation children first (FK order)
2302 "muse_note_changes", "muse_phrases", "muse_variations",
2303 # Muse VCS — innermost first (tags depend on commits, commits depend on snapshots)
2304 "muse_tags", "muse_commits", "muse_snapshots", "muse_objects",
2305 # MuseHub — children before parents (FK order)
2306 "musehub_download_events", "musehub_view_events", "musehub_forks",
2307 "musehub_notifications", "musehub_watches", "musehub_follows",
2308 "musehub_reactions", "musehub_comments",
2309 "musehub_render_jobs",
2310 "musehub_events",
2311 # Stash children before stash (FK to muse_users + repos)
2312 "musehub_stash_entries", "musehub_stash",
2313 # Collaborators (FK to muse_users + repos)
2314 "musehub_collaborators",
2315 "musehub_stars", "musehub_sessions",
2316 # Release assets before releases
2317 "musehub_release_assets", "musehub_releases",
2318 "musehub_webhook_deliveries", "musehub_webhooks",
2319 # PR children before pull_requests
2320 "musehub_pr_labels", "musehub_pr_comments", "musehub_pr_reviews",
2321 "musehub_pull_requests",
2322 # Issue children before issues; milestones after (SET NULL FK)
2323 "musehub_issue_labels", "musehub_issue_milestones",
2324 "musehub_issue_comments",
2325 "musehub_issues", "musehub_milestones",
2326 # Labels after issues/PRs
2327 "musehub_labels",
2328 "musehub_branches",
2329 "musehub_objects", "musehub_commits", "musehub_repos",
2330 "musehub_profiles",
2331 # muse_users last (other tables FK to it)
2332 "muse_users",
2333 ]:
2334 await db.execute(text(f"DELETE FROM {tbl}"))
2335 await db.flush()
2336
2337 # ── 1. muse_users (required FK for collaborators + stash) ─────────────
2338 # Mirrors the same user IDs used in musehub_profiles so the FK chain is
2339 # consistent across the whole schema.
2340 all_user_ids_and_names = list(USERS) + list(COMPOSER_USERS)
2341 for uid, _uname, _bio in all_user_ids_and_names:
2342 db.add(User(
2343 id=uid,
2344 budget_cents=2500,
2345 budget_limit_cents=5000,
2346 created_at=_now(days=120),
2347 updated_at=_now(days=1),
2348 ))
2349 print(f" ✅ muse_users: {len(all_user_ids_and_names)}")
2350
2351 await db.flush()
2352
2353 # ── 1b. User profiles (musehub_profiles) ──────────────────────
2354 # Pinned repos show the owner's most prominent repos on their profile page.
2355 _PROFILE_PINS: dict[str, list[str]] = {
2356 GABRIEL: [REPO_NEO_SOUL, REPO_MODAL_JAZZ, REPO_NEO_BAROQUE, REPO_COMMUNITY],
2357 SOFIA: [REPO_AMBIENT],
2358 MARCUS: [REPO_FUNK_SUITE, REPO_JAZZ_TRIO, REPO_RAGTIME_EDM],
2359 YUKI: [REPO_GRANULAR],
2360 AALIYA: [REPO_AFROBEAT, REPO_JAZZ_CHOPIN],
2361 CHEN: [REPO_MICROTONAL, REPO_FILM_SCORE],
2362 FATOU: [REPO_DRUM_MACHINE, REPO_POLYRHYTHM],
2363 PIERRE: [REPO_CHANSON],
2364 BACH: [REPO_WTC, REPO_GOLDBERG],
2365 CHOPIN: [REPO_NOCTURNES],
2366 SCOTT_JOPLIN: [REPO_MAPLE_LEAF],
2367 KEVIN_MACLEOD: [REPO_CIN_STRINGS],
2368 KAI_ENGEL: [REPO_KAI_AMBIENT],
2369 }
2370 all_user_profiles = list(USERS) + list(COMPOSER_USERS)
2371 for uid, uname, _bio in all_user_profiles:
2372 p = PROFILE_DATA.get(uid, {})
2373 db.add(MusehubProfile(
2374 user_id=uid,
2375 username=uname,
2376 display_name=str(p["display_name"]) if p.get("display_name") else uname,
2377 bio=str(p["bio"]) if p.get("bio") else _bio,
2378 avatar_url=f"https://api.dicebear.com/7.x/avataaars/svg?seed={uname}",
2379 location=str(p["location"]) if p.get("location") else None,
2380 website_url=str(p["website_url"]) if p.get("website_url") else None,
2381 twitter_handle=str(p["twitter_handle"]) if p.get("twitter_handle") else None,
2382 is_verified=bool(p.get("is_verified", False)),
2383 cc_license=str(p["cc_license"]) if p.get("cc_license") else None,
2384 pinned_repo_ids=_PROFILE_PINS.get(uid, []),
2385 ))
2386 verified_count = sum(1 for uid, _, __ in all_user_profiles if PROFILE_DATA.get(uid, {}).get("is_verified"))
2387 print(f" ✅ Profiles: {len(all_user_profiles)} users ({len(USERS)} community + {len(COMPOSER_USERS)} composer/archive, {verified_count} verified CC)")
2388
2389 # ── 2. Repos ──────────────────────────────────────────────────
2390 all_repos = list(REPOS) + list(GENRE_REPOS)
2391 for r in all_repos:
2392 db.add(MusehubRepo(
2393 repo_id=r["repo_id"],
2394 name=r["name"],
2395 owner=r["owner"],
2396 slug=r["slug"],
2397 owner_user_id=r["owner_user_id"],
2398 visibility=r["visibility"],
2399 description=r["description"],
2400 tags=r["tags"],
2401 key_signature=r["key_signature"],
2402 tempo_bpm=r["tempo_bpm"],
2403 created_at=_now(days=r["days_ago"]),
2404 ))
2405 print(f" ✅ Repos: {len(all_repos)} ({len(REPOS)} original + {len(GENRE_REPOS)} genre archive)")
2406
2407 await db.flush()
2408
2409 # ── 3. Commits + Branches ─────────────────────────────────────
2410 all_commits: dict[str, list[dict[str, Any]]] = {}
2411 total_commits = 0
2412 for r in all_repos:
2413 repo_id = r["repo_id"]
2414 rkey = REPO_KEY_MAP.get(repo_id, "neo-soul")
2415 n = COMMIT_COUNTS.get(repo_id, 20)
2416 commits = _make_commits(repo_id, rkey, n)
2417 all_commits[repo_id] = commits
2418 total_commits += len(commits)
2419 for c in commits:
2420 db.add(MusehubCommit(**c))
2421 # main branch always points to HEAD
2422 db.add(MusehubBranch(repo_id=repo_id, name="main",
2423 head_commit_id=commits[-1]["commit_id"]))
2424 if repo_id in GENRE_REPO_BRANCHES:
2425 # Genre archive repos: use specific named branches
2426 for branch_name, offset in GENRE_REPO_BRANCHES[repo_id]:
2427 idx = max(0, len(commits) - 1 - offset)
2428 db.add(MusehubBranch(
2429 repo_id=repo_id,
2430 name=branch_name,
2431 head_commit_id=commits[idx]["commit_id"],
2432 ))
2433 else:
2434 # Original repos: generic feature branches
2435 if len(commits) > 10:
2436 db.add(MusehubBranch(repo_id=repo_id, name="feat/develop",
2437 head_commit_id=commits[-4]["commit_id"]))
2438 if len(commits) > 20:
2439 db.add(MusehubBranch(repo_id=repo_id, name="experiment/alternate-harmony",
2440 head_commit_id=commits[-8]["commit_id"]))
2441 print(f" ✅ Commits: {total_commits} across {len(all_repos)} repos")
2442
2443 await db.flush()
2444
2445 # ── 4. Objects (track breakdown bar) ──────────────────────────
2446 obj_count = 0
2447 for r in all_repos:
2448 repo_id = r["repo_id"]
2449 rkey = REPO_KEY_MAP.get(repo_id, "neo-soul")
2450 tracks = REPO_TRACKS.get(rkey, REPO_TRACKS["neo-soul"])
2451 commits = all_commits.get(repo_id, [])
2452 if not commits:
2453 continue
2454 # Attach objects to the last 3 commits
2455 for commit in commits[-3:]:
2456 cid = commit["commit_id"]
2457 for role, path in tracks:
2458 obj_id = f"sha256:{_sha(f'{cid}-{path}')}"
2459 db.add(MusehubObject(
2460 object_id=obj_id,
2461 repo_id=repo_id,
2462 path=path,
2463 size_bytes=len(path) * 1024,
2464 disk_path=f"/app/objects/{repo_id}/{obj_id[7:15]}.mid",
2465 created_at=commit["timestamp"],
2466 ))
2467 obj_count += 1
2468 print(f" ✅ Objects: {obj_count} track files")
2469
2470 await db.flush()
2471
2472 # ── 4b. Labels (scoped to each repo — seeded before issues/PRs) ───────────
2473 _LABEL_DEFS: list[tuple[str, str, str]] = [
2474 # (name, color, description)
2475 ("bug", "#d73a4a", "Something isn't working correctly"),
2476 ("enhancement", "#a2eeef", "New feature or improvement request"),
2477 ("documentation","#0075ca", "Documentation update or correction"),
2478 ("question", "#d876e3", "Further information requested"),
2479 ("wontfix", "#ffffff", "This will not be addressed"),
2480 ("good first issue", "#7057ff", "Good for newcomers to the project"),
2481 ("help wanted", "#008672", "Extra attention needed"),
2482 ("in progress", "#e4e669", "Currently being worked on"),
2483 ("blocked", "#e11d48", "Blocked by another issue or dependency"),
2484 ("harmony", "#fbbf24", "Harmonic or tonal issue"),
2485 ("timing", "#6366f1", "Timing, groove or quantization issue"),
2486 ("mixing", "#10b981", "Mix balance, levels or EQ"),
2487 ("arrangement", "#f97316", "Arrangement or structure feedback"),
2488 ]
2489 # Structure: repo_id → {label_name → label_id}
2490 label_id_map: dict[str, dict[str, str]] = {}
2491 label_count = 0
2492 for r in all_repos:
2493 repo_id = r["repo_id"]
2494 label_id_map[repo_id] = {}
2495 for lname, lcolor, ldesc in _LABEL_DEFS:
2496 lid = _uid(f"label-{repo_id}-{lname}")
2497 db.add(MusehubLabel(
2498 id=lid,
2499 repo_id=repo_id,
2500 name=lname,
2501 color=lcolor,
2502 description=ldesc,
2503 created_at=_now(days=r["days_ago"]),
2504 ))
2505 label_id_map[repo_id][lname] = lid
2506 label_count += 1
2507 print(f" ✅ Labels: {label_count} ({len(_LABEL_DEFS)} per repo × {len(all_repos)} repos)")
2508
2509 await db.flush()
2510
2511 # ── 5a. Milestones (seed before issues so milestone_id can be referenced) ──
2512 # Structure: repo_id → {milestone_n → milestone_id}
2513 milestone_id_map: dict[str, dict[int, str]] = {}
2514 milestone_count = 0
2515 for r in REPOS:
2516 repo_id = r["repo_id"]
2517 rkey = REPO_KEY_MAP.get(repo_id, "neo-soul")
2518 ms_list = MILESTONE_TEMPLATES.get(rkey, [])
2519 if not ms_list:
2520 continue
2521 milestone_id_map[repo_id] = {}
2522 for ms in ms_list:
2523 mid = _uid(f"milestone-{repo_id}-{ms['n']}")
2524 due = _now(days=-ms["due_days"]) if ms.get("due_days") else None
2525 db.add(MusehubMilestone(
2526 milestone_id=mid,
2527 repo_id=repo_id,
2528 number=ms["n"],
2529 title=ms["title"],
2530 description=ms["description"],
2531 state=ms["state"],
2532 author=r["owner"],
2533 due_on=due,
2534 created_at=_now(days=r["days_ago"]),
2535 ))
2536 milestone_id_map[repo_id][ms["n"]] = mid
2537 milestone_count += 1
2538 print(f" ✅ Milestones: {milestone_count}")
2539
2540 await db.flush()
2541
2542 # ── 5b. Issues (track IDs for comment and milestone-link seeding) ──────────
2543 issue_count = 0
2544 # Structure: repo_id → {issue_n → issue_id}
2545 issue_id_map: dict[str, dict[int, str]] = {}
2546 for r in all_repos:
2547 repo_id = r["repo_id"]
2548 rkey = REPO_KEY_MAP.get(repo_id, "neo-soul")
2549 issue_list = ISSUE_TEMPLATES.get(rkey, GENERIC_ISSUES)
2550 days_base = r["days_ago"]
2551 # Determine milestone assignment map for this repo: issue_n → milestone_n
2552 ms_assignments = MILESTONE_ISSUE_ASSIGNMENTS.get(rkey, {})
2553 issue_to_ms: dict[int, int] = {}
2554 for ms_number, issue_ns in ms_assignments.items():
2555 for iss_n in issue_ns:
2556 issue_to_ms[iss_n] = ms_number
2557
2558 issue_id_map[repo_id] = {}
2559 for iss in issue_list:
2560 iid = _uid(f"issue-{repo_id}-{iss['n']}")
2561 assigned_ms_n: int | None = issue_to_ms.get(iss["n"])
2562 ms_id: str | None = milestone_id_map.get(repo_id, {}).get(assigned_ms_n) if assigned_ms_n else None
2563 db.add(MusehubIssue(
2564 issue_id=iid,
2565 repo_id=repo_id,
2566 number=iss["n"],
2567 state=iss["state"],
2568 title=iss["title"],
2569 body=iss["body"],
2570 labels=iss["labels"],
2571 author=r["owner"],
2572 milestone_id=ms_id,
2573 created_at=_now(days=days_base - iss["n"] * 2),
2574 ))
2575 issue_id_map[repo_id][iss["n"]] = iid
2576 # Also populate MusehubIssueMilestone join table for repos with milestones
2577 if ms_id:
2578 db.add(MusehubIssueMilestone(issue_id=iid, milestone_id=ms_id))
2579 issue_count += 1
2580 print(f" ✅ Issues: {issue_count}")
2581
2582 await db.flush()
2583
2584 # ── 5b-ii. Issue labels (many-to-many join) ────────────────────────────────
2585 _ISSUE_LABEL_PICKS: list[list[str]] = [
2586 # Cycling pattern of label combos assigned to issues by index
2587 ["bug"],
2588 ["enhancement"],
2589 ["bug", "in progress"],
2590 ["question"],
2591 ["harmony"],
2592 ["timing"],
2593 ["mixing"],
2594 ["arrangement"],
2595 ["enhancement", "help wanted"],
2596 ["bug", "blocked"],
2597 ["documentation"],
2598 ["good first issue"],
2599 ["wontfix"],
2600 ["enhancement", "in progress"],
2601 ["harmony", "help wanted"],
2602 ]
2603 issue_label_count = 0
2604 for r in all_repos:
2605 repo_id = r["repo_id"]
2606 rkey = REPO_KEY_MAP.get(repo_id, "neo-soul")
2607 issue_list = ISSUE_TEMPLATES.get(rkey, GENERIC_ISSUES)
2608 repo_labels = label_id_map.get(repo_id, {})
2609 for iss in issue_list:
2610 il_iid: str | None = issue_id_map.get(repo_id, {}).get(iss["n"])
2611 if not il_iid:
2612 continue
2613 picks = _ISSUE_LABEL_PICKS[iss["n"] % len(_ISSUE_LABEL_PICKS)]
2614 for lname in picks:
2615 il_lid: str | None = repo_labels.get(lname)
2616 if il_lid:
2617 db.add(MusehubIssueLabel(issue_id=il_iid, label_id=il_lid))
2618 issue_label_count += 1
2619 print(f" ✅ Issue labels: {issue_label_count}")
2620
2621 await db.flush()
2622
2623 # ── 5c. Issue comments (5-10 per issue, with @mentions and code blocks) ────
2624 issue_comment_count = 0
2625 users_list = [u[1] for u in USERS]
2626 for r in REPOS[:10]: # Comments on all non-fork repos
2627 repo_id = r["repo_id"]
2628 rkey = REPO_KEY_MAP.get(repo_id, "neo-soul")
2629 issue_list = ISSUE_TEMPLATES.get(rkey, GENERIC_ISSUES)
2630 for iss in issue_list:
2631 iss_iid = issue_id_map.get(repo_id, {}).get(iss["n"])
2632 if not iss_iid:
2633 continue
2634 # 5-10 comments per issue (varies by issue number parity)
2635 n_comments = 5 + (iss["n"] % 6)
2636 iss_cmt_parent: str | None = None
2637 for j in range(n_comments):
2638 cmt_seed = hash(repo_id + str(iss["n"]) + str(j)) % 1000
2639 body = _make_issue_comment_body(cmt_seed)
2640 musical_refs = _make_issue_musical_refs(body)
2641 author_idx = (abs(hash(repo_id)) + iss["n"] + j) % len(users_list)
2642 author = users_list[author_idx]
2643 cid = _uid(f"iss-comment-{repo_id}-{iss['n']}-{j}")
2644 db.add(MusehubIssueComment(
2645 comment_id=cid,
2646 issue_id=iss_iid,
2647 repo_id=repo_id,
2648 author=author,
2649 body=body,
2650 parent_id=iss_cmt_parent if j > 0 and j % 3 == 0 else None,
2651 musical_refs=musical_refs,
2652 created_at=_now(days=r["days_ago"] - iss["n"] * 2, hours=j * 2),
2653 ))
2654 issue_comment_count += 1
2655 # First comment becomes parent for threaded replies
2656 if j == 0:
2657 iss_cmt_parent = cid
2658 print(f" ✅ Issue comments: {issue_comment_count}")
2659
2660 await db.flush()
2661
2662 # ── 6. Pull Requests ──────────────────────────────────────────
2663 pr_count = 0
2664 pr_ids: dict[str, list[str]] = {}
2665 for r in all_repos:
2666 repo_id = r["repo_id"]
2667 commits = all_commits.get(repo_id, [])
2668 prs = _make_prs(repo_id, commits, r["owner"])
2669 pr_ids[repo_id] = [p["pr_id"] for p in prs]
2670 for pr in prs:
2671 db.add(MusehubPullRequest(**pr))
2672 pr_count += 1
2673 print(f" ✅ Pull Requests: {pr_count}")
2674
2675 await db.flush()
2676
2677 # ── 6b. PR comments (3-8 per PR with target_type variety) ─────────────────
2678 PR_COMMENT_BODIES: list[tuple[str, str, str | None, float | None, float | None, int | None]] = [
2679 # (body, target_type, target_track, beat_start, beat_end, note_pitch)
2680 ("General: this PR looks good overall. The groove change is an improvement.",
2681 "general", None, None, None, None),
2682 ("The `bass` track changes in this PR need review — the wah envelope is still too slow.",
2683 "track", "bass", None, None, None),
2684 ("This region (beats 16-24) sounds much better with the humanized timing.",
2685 "region", "keys", 16.0, 24.0, None),
2686 ("The C#4 (MIDI 61) note in the Rhodes feels misplaced — should be D4 for the chord.",
2687 "note", "keys", None, None, 61),
2688 ("Great improvement in the horns section. The harmony is now correct.",
2689 "track", "horns", None, None, None),
2690 ("The beat 1-8 region on the bass now locks properly with the kick.",
2691 "region", "bass", 1.0, 8.0, None),
2692 ("The G3 (MIDI 55) in bar 7 creates an unwanted clash. Remove or lower octave.",
2693 "note", "strings", None, None, 55),
2694 ("Overall this PR solves the main issue. LGTM with minor nits.",
2695 "general", None, None, None, None),
2696 ("The `drums` ghost notes are much improved — much more human now.",
2697 "track", "drums", None, None, None),
2698 ("Beats 32-40 on the guitar feel slightly rushed. Did you check the quantize grid?",
2699 "region", "guitar", 32.0, 40.0, None),
2700 ]
2701
2702 pr_comment_count = 0
2703 for r in REPOS[:10]:
2704 repo_id = r["repo_id"]
2705 for pr_id_str in pr_ids.get(repo_id, []):
2706 # 3-8 comments per PR
2707 n_pr_comments = 3 + (abs(hash(pr_id_str)) % 6)
2708 parent_pr_cid: str | None = None
2709 for k in range(n_pr_comments):
2710 tmpl_idx = (abs(hash(pr_id_str)) + k) % len(PR_COMMENT_BODIES)
2711 body, ttype, ttrack, tbs, tbe, tnp = PR_COMMENT_BODIES[tmpl_idx]
2712 author_idx = (abs(hash(repo_id)) + k + 1) % len(users_list)
2713 pr_cid = _uid(f"pr-comment-{pr_id_str}-{k}")
2714 db.add(MusehubPRComment(
2715 comment_id=pr_cid,
2716 pr_id=pr_id_str,
2717 repo_id=repo_id,
2718 author=users_list[author_idx],
2719 body=body,
2720 target_type=ttype,
2721 target_track=ttrack,
2722 target_beat_start=tbs,
2723 target_beat_end=tbe,
2724 target_note_pitch=tnp,
2725 parent_comment_id=parent_pr_cid if k > 0 and k % 4 == 0 else None,
2726 created_at=_now(days=7 - k),
2727 ))
2728 pr_comment_count += 1
2729 if k == 0:
2730 parent_pr_cid = pr_cid
2731 print(f" ✅ PR comments: {pr_comment_count}")
2732
2733 await db.flush()
2734
2735 # ── 6c. PR Reviews (reviewer assignment + approved/changes_requested) ──────
2736 _PR_REVIEW_STATES = ["approved", "approved", "changes_requested", "pending", "dismissed"]
2737 _PR_REVIEW_BODIES = [
2738 "LGTM — the harmonic changes are solid and the voice-leading is now clean.",
2739 "Approved. The groove is much tighter after the timing adjustments.",
2740 "Changes requested: the bass still feels muddy in bars 9-16. Please reduce low-mids.",
2741 "Pending review — I'll listen through the changes this weekend.",
2742 "Approved with nits: the coda could be shorter, but the core change is correct.",
2743 "Changes requested: parallel fifths still present in bar 7 on the strings voice.",
2744 "LGTM — the new arrangement section is exactly what the composition needed.",
2745 ]
2746 pr_review_count = 0
2747 for r in REPOS[:10]:
2748 repo_id = r["repo_id"]
2749 owner = r["owner"]
2750 for pr_id_str in pr_ids.get(repo_id, []):
2751 # 1-2 reviewers per PR, drawn from the community pool (not the PR author)
2752 n_reviewers = 1 + (abs(hash(pr_id_str)) % 2)
2753 reviewers_pool = [u for u in users_list if u != owner]
2754 for ri in range(n_reviewers):
2755 reviewer = reviewers_pool[(abs(hash(pr_id_str)) + ri) % len(reviewers_pool)]
2756 state = _PR_REVIEW_STATES[(abs(hash(pr_id_str)) + ri) % len(_PR_REVIEW_STATES)]
2757 body = _PR_REVIEW_BODIES[(abs(hash(pr_id_str)) + ri) % len(_PR_REVIEW_BODIES)]
2758 submitted = _now(days=5 - ri) if state != "pending" else None
2759 db.add(MusehubPRReview(
2760 id=_uid(f"pr-review-{pr_id_str}-{ri}"),
2761 pr_id=pr_id_str,
2762 reviewer_username=reviewer,
2763 state=state,
2764 body=body if state != "pending" else None,
2765 submitted_at=submitted,
2766 created_at=_now(days=7 - ri),
2767 ))
2768 pr_review_count += 1
2769 print(f" ✅ PR reviews: {pr_review_count}")
2770
2771 await db.flush()
2772
2773 # ── 6d. PR Labels (label tags on pull requests) ────────────────────────────
2774 _PR_LABEL_PICKS: list[list[str]] = [
2775 ["enhancement"],
2776 ["bug"],
2777 ["enhancement", "in progress"],
2778 ["wontfix"],
2779 ]
2780 pr_label_count = 0
2781 for r in all_repos:
2782 repo_id = r["repo_id"]
2783 repo_labels = label_id_map.get(repo_id, {})
2784 for pi, pr_id_str in enumerate(pr_ids.get(repo_id, [])):
2785 picks = _PR_LABEL_PICKS[pi % len(_PR_LABEL_PICKS)]
2786 for lname in picks:
2787 prl_lid: str | None = repo_labels.get(lname)
2788 if prl_lid:
2789 db.add(MusehubPRLabel(pr_id=pr_id_str, label_id=prl_lid))
2790 pr_label_count += 1
2791 print(f" ✅ PR labels: {pr_label_count}")
2792
2793 await db.flush()
2794
2795 # ── 7. Releases ───────────────────────────────────────────────
2796 release_count = 0
2797 release_tags: dict[str, list[str]] = {}
2798 for r in all_repos:
2799 repo_id = r["repo_id"]
2800 commits = all_commits.get(repo_id, [])
2801 releases = _make_releases(repo_id, commits, r["name"], r["owner"])
2802 release_tags[repo_id] = [rel["tag"] for rel in releases]
2803 for rel in releases:
2804 db.add(MusehubRelease(**rel))
2805 release_count += 1
2806 print(f" ✅ Releases: {release_count}")
2807
2808 await db.flush()
2809
2810 # ── 7b. Release Assets (downloadable file attachments per release) ─────────
2811 _ASSET_TYPES: list[tuple[str, str, str, int]] = [
2812 # (name_suffix, label, content_type, approx_size_bytes)
2813 ("-midi-bundle.zip", "MIDI Bundle", "application/zip", 2_400_000),
2814 ("-stereo-mix.mp3", "Stereo Mix", "audio/mpeg", 8_200_000),
2815 ("-stems.zip", "Stems Archive", "application/zip", 42_000_000),
2816 ("-score.pdf", "Score PDF", "application/pdf", 1_100_000),
2817 ("-metadata.json", "Metadata", "application/json", 18_000),
2818 ]
2819 # Only full releases (v1.0.0) get all 5 assets; earlier releases get 2.
2820 release_asset_count = 0
2821 for r in all_repos:
2822 repo_id = r["repo_id"]
2823 tags = release_tags.get(repo_id, [])
2824 for ti, tag in enumerate(tags):
2825 # release_id is deterministic — matches what _make_releases sets
2826 rel_id = _uid(f"release-{repo_id}-{tag}")
2827 # Full release gets all asset types; earlier releases get 2
2828 n_assets = len(_ASSET_TYPES) if ti == len(tags) - 1 else 2
2829 base_slug = r["slug"]
2830 for ai, (sfx, label, ctype, base_size) in enumerate(_ASSET_TYPES[:n_assets]):
2831 dl_count = max(0, (50 - ti * 15) - ai * 5 + abs(hash(repo_id + tag)) % 20)
2832 db.add(MusehubReleaseAsset(
2833 asset_id=_uid(f"asset-{rel_id}-{ai}"),
2834 release_id=rel_id,
2835 repo_id=repo_id,
2836 name=f"{base_slug}-{tag}{sfx}",
2837 label=label,
2838 content_type=ctype,
2839 size=base_size + abs(hash(repo_id)) % 500_000,
2840 download_url=f"/releases/{repo_id}/{tag}{sfx}",
2841 download_count=dl_count,
2842 created_at=_now(days=max(1, 45 - ti * 20)),
2843 ))
2844 release_asset_count += 1
2845 print(f" ✅ Release assets: {release_asset_count}")
2846
2847 await db.flush()
2848
2849 # ── 8. Sessions ───────────────────────────────────────────────
2850 session_count = 0
2851 session_ids: dict[str, list[str]] = {}
2852 for r in all_repos:
2853 repo_id = r["repo_id"]
2854 commits = all_commits.get(repo_id, [])
2855 sessions = _make_sessions(repo_id, r["owner"], commits)
2856 session_ids[repo_id] = [s["session_id"] for s in sessions]
2857 for sess in sessions:
2858 db.add(MusehubSession(**sess))
2859 session_count += 1
2860 print(f" ✅ Sessions: {session_count}")
2861
2862 await db.flush()
2863
2864 # ── 8b. Collaborators (repo access beyond owner) ───────────────────────────
2865 # Each active repo gets 1-3 collaborators from the community pool.
2866 # Collaborators have write permission; the most active repos get an admin.
2867 _COLLAB_CONFIGS: list[tuple[str, str, list[tuple[str, str]]]] = [
2868 # (repo_id, owner_user_id, [(collab_user_id, permission)])
2869 (REPO_NEO_SOUL, GABRIEL, [(MARCUS, "write"), (SOFIA, "write"), (AALIYA, "admin")]),
2870 (REPO_MODAL_JAZZ, GABRIEL, [(MARCUS, "write"), (CHEN, "write")]),
2871 (REPO_AMBIENT, SOFIA, [(YUKI, "write"), (PIERRE, "write"), (GABRIEL, "admin")]),
2872 (REPO_AFROBEAT, AALIYA, [(FATOU, "write"), (GABRIEL, "write")]),
2873 (REPO_MICROTONAL, CHEN, [(PIERRE, "write")]),
2874 (REPO_DRUM_MACHINE, FATOU, [(AALIYA, "write"), (MARCUS, "write")]),
2875 (REPO_CHANSON, PIERRE, [(SOFIA, "write")]),
2876 (REPO_GRANULAR, YUKI, [(SOFIA, "write"), (CHEN, "write")]),
2877 (REPO_FUNK_SUITE, MARCUS, [(GABRIEL, "write"), (AALIYA, "admin")]),
2878 (REPO_JAZZ_TRIO, MARCUS, [(GABRIEL, "write")]),
2879 (REPO_NEO_BAROQUE, GABRIEL, [(PIERRE, "write"), (CHEN, "write")]),
2880 (REPO_JAZZ_CHOPIN, AALIYA, [(GABRIEL, "write"), (MARCUS, "write")]),
2881 (REPO_COMMUNITY, GABRIEL, [(SOFIA, "admin"), (MARCUS, "write"), (YUKI, "write"),
2882 (AALIYA, "write"), (CHEN, "write"), (FATOU, "write"), (PIERRE, "write")]),
2883 ]
2884 collab_count = 0
2885 for repo_id, owner_uid, collab_list in _COLLAB_CONFIGS:
2886 for collab_uid, perm in collab_list:
2887 accepted = _now(days=abs(hash(collab_uid + repo_id)) % 20 + 1)
2888 db.add(MusehubCollaborator(
2889 id=_uid(f"collab-{repo_id}-{collab_uid}"),
2890 repo_id=repo_id,
2891 user_id=collab_uid,
2892 permission=perm,
2893 invited_by=owner_uid,
2894 invited_at=_now(days=abs(hash(collab_uid + repo_id)) % 20 + 3),
2895 accepted_at=accepted,
2896 ))
2897 collab_count += 1
2898 print(f" ✅ Collaborators: {collab_count}")
2899
2900 await db.flush()
2901
2902 # ── 8c. Stash (shelved in-progress work per user/repo) ────────────────────
2903 _STASH_MESSAGES: list[str] = [
2904 "WIP: Rhodes chord voicings — not ready to commit",
2905 "Experiment: tritone sub on IV chord — might revert",
2906 "Sketching counter-melody — needs more work",
2907 "Bass line draft — 3-against-4 groove not locked yet",
2908 "Drum fills experiment — comparing two approaches",
2909 "Harmony sketch: parallel 10ths — maybe too classical?",
2910 "Tempo map idea: ritardando at bar 36 — not sure yet",
2911 ]
2912 _STASH_CONFIGS: list[tuple[str, str, str]] = [
2913 # (repo_id, user_id, branch)
2914 (REPO_NEO_SOUL, GABRIEL, "feat/counter-melody"),
2915 (REPO_AMBIENT, SOFIA, "experiment/drone-layer"),
2916 (REPO_AFROBEAT, AALIYA, "feat/brass-section"),
2917 (REPO_FUNK_SUITE, MARCUS, "experiment/fretless-bass"),
2918 (REPO_MICROTONAL, CHEN, "feat/spectral-harmony"),
2919 (REPO_GRANULAR, YUKI, "experiment/formant-filter"),
2920 (REPO_CHANSON, PIERRE, "feat/coda-extension"),
2921 (REPO_COMMUNITY, GABRIEL, "collab/all-genre-finale"),
2922 (REPO_MODAL_JAZZ, GABRIEL, "feat/mcoy-voicings"),
2923 (REPO_NEO_BAROQUE, GABRIEL, "experiment/fugue-development"),
2924 ]
2925 stash_count = 0
2926 stash_entry_count = 0
2927 for si, (repo_id, user_id, branch) in enumerate(_STASH_CONFIGS):
2928 stash_id = _uid(f"stash-{repo_id}-{user_id}-{si}")
2929 msg = _STASH_MESSAGES[si % len(_STASH_MESSAGES)]
2930 is_applied = si % 5 == 0 # Every 5th stash has been popped
2931 applied_at = _now(days=1) if is_applied else None
2932 db.add(MusehubStash(
2933 id=stash_id,
2934 repo_id=repo_id,
2935 user_id=user_id,
2936 branch=branch,
2937 message=msg,
2938 is_applied=is_applied,
2939 created_at=_now(days=si + 2),
2940 applied_at=applied_at,
2941 ))
2942 stash_count += 1
2943 # 2-4 MIDI file entries per stash
2944 rkey = REPO_KEY_MAP.get(repo_id, "neo-soul")
2945 stash_tracks = REPO_TRACKS.get(rkey, REPO_TRACKS["neo-soul"])
2946 n_entries = 2 + (si % 3)
2947 for ei, (role, fpath) in enumerate(stash_tracks[:n_entries]):
2948 obj_id = f"sha256:{_sha(f'stash-obj-{stash_id}-{role}')}"
2949 db.add(MusehubStashEntry(
2950 id=_uid(f"stash-entry-{stash_id}-{ei}"),
2951 stash_id=stash_id,
2952 path=f"tracks/{fpath}",
2953 object_id=obj_id,
2954 position=ei,
2955 ))
2956 stash_entry_count += 1
2957 print(f" ✅ Stash: {stash_count} stashes, {stash_entry_count} entries")
2958
2959 await db.flush()
2960
2961 # ── 9. Stars (cross-repo) ─────────────────────────────────────
2962 # Every community user stars 5-10 repos; genre/composer archives also get
2963 # starred by community users who inspired or forked from them. 50+ total.
2964 star_pairs = [
2965 # Community repos — broad community engagement
2966 (SOFIA, REPO_NEO_SOUL, 20), (MARCUS, REPO_NEO_SOUL, 18),
2967 (YUKI, REPO_NEO_SOUL, 15), (AALIYA, REPO_NEO_SOUL, 12),
2968 (CHEN, REPO_NEO_SOUL, 10), (FATOU, REPO_NEO_SOUL, 8),
2969 (PIERRE, REPO_NEO_SOUL, 6),
2970
2971 (GABRIEL, REPO_AMBIENT, 14), (MARCUS, REPO_AMBIENT, 12),
2972 (YUKI, REPO_AMBIENT, 10), (AALIYA, REPO_AMBIENT, 9),
2973 (CHEN, REPO_AMBIENT, 7), (FATOU, REPO_AMBIENT, 5),
2974 (PIERRE, REPO_AMBIENT, 3),
2975
2976 (GABRIEL, REPO_AFROBEAT, 8), (SOFIA, REPO_AFROBEAT, 7),
2977 (MARCUS, REPO_AFROBEAT, 6), (YUKI, REPO_AFROBEAT, 5),
2978 (CHEN, REPO_AFROBEAT, 4), (PIERRE, REPO_AFROBEAT, 3),
2979
2980 (GABRIEL, REPO_FUNK_SUITE, 12), (SOFIA, REPO_FUNK_SUITE, 10),
2981 (AALIYA, REPO_FUNK_SUITE, 9), (CHEN, REPO_FUNK_SUITE, 7),
2982 (FATOU, REPO_FUNK_SUITE, 5),
2983
2984 (GABRIEL, REPO_MODAL_JAZZ, 11), (SOFIA, REPO_MODAL_JAZZ, 8),
2985 (MARCUS, REPO_MODAL_JAZZ, 6), (AALIYA, REPO_MODAL_JAZZ, 4),
2986
2987 (GABRIEL, REPO_JAZZ_TRIO, 9), (SOFIA, REPO_JAZZ_TRIO, 7),
2988 (AALIYA, REPO_JAZZ_TRIO, 5), (CHEN, REPO_JAZZ_TRIO, 3),
2989
2990 (GABRIEL, REPO_DRUM_MACHINE, 6), (MARCUS, REPO_DRUM_MACHINE, 4),
2991 (GABRIEL, REPO_GRANULAR, 5), (MARCUS, REPO_GRANULAR, 3),
2992 (GABRIEL, REPO_CHANSON, 4), (SOFIA, REPO_CHANSON, 3),
2993 (GABRIEL, REPO_MICROTONAL, 3), (SOFIA, REPO_MICROTONAL, 2),
2994
2995 # Genre/composer archive repos starred by community users who draw
2996 # inspiration from or fork into them
2997 (GABRIEL, REPO_GOLDBERG, 25), (SOFIA, REPO_GOLDBERG, 22),
2998 (MARCUS, REPO_GOLDBERG, 18), (AALIYA, REPO_GOLDBERG, 15),
2999 (CHEN, REPO_GOLDBERG, 12), (FATOU, REPO_GOLDBERG, 9),
3000 (PIERRE, REPO_GOLDBERG, 7), (YUKI, REPO_GOLDBERG, 5),
3001
3002 (GABRIEL, REPO_WTC, 24), (SOFIA, REPO_WTC, 20),
3003 (CHEN, REPO_WTC, 16), (MARCUS, REPO_WTC, 13),
3004 (YUKI, REPO_WTC, 10), (PIERRE, REPO_WTC, 6),
3005
3006 (GABRIEL, REPO_NOCTURNES, 22), (AALIYA, REPO_NOCTURNES, 18),
3007 (SOFIA, REPO_NOCTURNES, 15), (CHEN, REPO_NOCTURNES, 11),
3008 (MARCUS, REPO_NOCTURNES, 8), (YUKI, REPO_NOCTURNES, 5),
3009
3010 (MARCUS, REPO_MAPLE_LEAF, 20), (GABRIEL, REPO_MAPLE_LEAF, 17),
3011 (AALIYA, REPO_MAPLE_LEAF, 13), (FATOU, REPO_MAPLE_LEAF, 9),
3012 (PIERRE, REPO_MAPLE_LEAF, 6),
3013
3014 (CHEN, REPO_CIN_STRINGS, 19), (GABRIEL, REPO_CIN_STRINGS, 15),
3015 (SOFIA, REPO_CIN_STRINGS, 12), (YUKI, REPO_CIN_STRINGS, 8),
3016 (MARCUS, REPO_CIN_STRINGS, 5),
3017
3018 # Community genre-fusion repos (created as forks of composer archives)
3019 (SOFIA, REPO_NEO_BAROQUE, 14), (MARCUS, REPO_NEO_BAROQUE, 11),
3020 (CHEN, REPO_NEO_BAROQUE, 8), (YUKI, REPO_NEO_BAROQUE, 5),
3021 (PIERRE, REPO_NEO_BAROQUE, 3),
3022
3023 (GABRIEL, REPO_JAZZ_CHOPIN, 12), (MARCUS, REPO_JAZZ_CHOPIN, 9),
3024 (SOFIA, REPO_JAZZ_CHOPIN, 7), (CHEN, REPO_JAZZ_CHOPIN, 4),
3025
3026 (GABRIEL, REPO_RAGTIME_EDM, 10), (AALIYA, REPO_RAGTIME_EDM, 8),
3027 (FATOU, REPO_RAGTIME_EDM, 6), (YUKI, REPO_RAGTIME_EDM, 4),
3028
3029 (GABRIEL, REPO_FILM_SCORE, 9), (SOFIA, REPO_FILM_SCORE, 7),
3030 (MARCUS, REPO_FILM_SCORE, 5), (AALIYA, REPO_FILM_SCORE, 3),
3031
3032 (GABRIEL, REPO_COMMUNITY, 8), (SOFIA, REPO_COMMUNITY, 6),
3033 (MARCUS, REPO_COMMUNITY, 5), (AALIYA, REPO_COMMUNITY, 4),
3034 (CHEN, REPO_COMMUNITY, 3), (FATOU, REPO_COMMUNITY, 2),
3035 (PIERRE, REPO_COMMUNITY, 1),
3036 ]
3037 for user_id, repo_id, days in star_pairs:
3038 db.add(MusehubStar(repo_id=repo_id, user_id=user_id,
3039 created_at=_now(days=days)))
3040 print(f" ✅ Stars: {len(star_pairs)}")
3041
3042 await db.flush()
3043
3044 # ── 10. Comments ─────────────────────────────────────────────
3045 COMMENT_BODIES = [
3046 "This groove is incredible — the 3-against-4 polyrhythm is exactly what this track needed.",
3047 "Love the Rhodes voicings here. Upper-structure triads give it that sophisticated neo-soul feel.",
3048 "The humanization really helped. Feels much more like a live performance now.",
3049 "I think the bridge needs more harmonic tension. The IV-I resolution is too settled.",
3050 "That trumpet counter-melody is stunning. It perfectly answers the Rhodes line.",
3051 "Could we push the bass a bit more? It's sitting a little behind the kick drum.",
3052 "The string pizzicato in the verse is a beautiful subtle touch.",
3053 "I'm not sure about the guitar scratch — feels a bit busy with the Rhodes.",
3054 "This is really coming together. The dynamic arc from intro to chorus is perfect.",
3055 "The call-and-response between horns and strings is very Quincy Jones.",
3056 ]
3057
3058 comment_count = 0
3059 for r in REPOS[:6]: # Comments on first 6 repos
3060 repo_id = r["repo_id"]
3061 commits = all_commits.get(repo_id, [])
3062 if not commits:
3063 continue
3064 # Comments on latest 3 commits
3065 for i, commit in enumerate(commits[-3:]):
3066 for j in range(3):
3067 body = COMMENT_BODIES[(i * 3 + j) % len(COMMENT_BODIES)]
3068 author_ids = [GABRIEL, SOFIA, MARCUS, YUKI, AALIYA, CHEN, FATOU, PIERRE]
3069 author = [u[1] for u in USERS if u[0] == author_ids[(i + j + hash(repo_id)) % len(author_ids)]][0]
3070 comment_id = _uid(f"comment-{repo_id}-{i}-{j}")
3071 db.add(MusehubComment(
3072 comment_id=comment_id,
3073 repo_id=repo_id,
3074 target_type="commit",
3075 target_id=commit["commit_id"],
3076 author=author,
3077 body=body,
3078 parent_id=None,
3079 created_at=_now(days=2, hours=i * 3 + j),
3080 ))
3081 comment_count += 1
3082 # Add a reply to first comment on each commit
3083 if j == 0:
3084 reply_author = [u[1] for u in USERS if u[1] != author][0]
3085 db.add(MusehubComment(
3086 comment_id=_uid(f"reply-{repo_id}-{i}-{j}"),
3087 repo_id=repo_id,
3088 target_type="commit",
3089 target_id=commit["commit_id"],
3090 author=reply_author,
3091 body="Totally agree — this part really elevates the whole track.",
3092 parent_id=comment_id,
3093 created_at=_now(days=1, hours=i * 2),
3094 ))
3095 comment_count += 1
3096 print(f" ✅ Comments: {comment_count}")
3097
3098 await db.flush()
3099
3100 # ── 11. Reactions ─────────────────────────────────────────────
3101 # All 8 emoji types from the spec: 👍❤️🎵🔥🎹👏🤔😢. 200+ total across
3102 # both community and genre-archive repos.
3103 EMOJIS = ["👍", "❤️", "🎵", "🔥", "🎹", "👏", "🤔", "😢"]
3104 reaction_count = 0
3105 all_community_users = [GABRIEL, SOFIA, MARCUS, YUKI, AALIYA, CHEN, FATOU, PIERRE]
3106 # Community repos — all 8 users react to last 5 commits on first 6 repos
3107 for r in REPOS[:6]:
3108 repo_id = r["repo_id"]
3109 commits = all_commits.get(repo_id, [])
3110 if not commits:
3111 continue
3112 for commit in commits[-5:]:
3113 for i, uid in enumerate(all_community_users):
3114 emoji = EMOJIS[i % len(EMOJIS)]
3115 try:
3116 db.add(MusehubReaction(
3117 reaction_id=_uid(f"reaction-{repo_id}-{commit['commit_id'][:8]}-{uid}"),
3118 repo_id=repo_id,
3119 target_type="commit",
3120 target_id=commit["commit_id"],
3121 user_id=uid,
3122 emoji=emoji,
3123 created_at=_now(days=1),
3124 ))
3125 reaction_count += 1
3126 except Exception:
3127 pass
3128 # Genre-archive repos — community users react to last 3 commits, rotating
3129 # through all emoji types to ensure full coverage
3130 genre_reaction_repos = [
3131 REPO_GOLDBERG, REPO_WTC, REPO_NOCTURNES, REPO_MAPLE_LEAF, REPO_CIN_STRINGS,
3132 ]
3133 for repo_id in genre_reaction_repos:
3134 commits = all_commits.get(repo_id, [])
3135 if not commits:
3136 continue
3137 for ci, commit in enumerate(commits[-3:]):
3138 for ui, uid in enumerate(all_community_users):
3139 emoji = EMOJIS[(ci * len(all_community_users) + ui) % len(EMOJIS)]
3140 try:
3141 db.add(MusehubReaction(
3142 reaction_id=_uid(f"reaction-g-{repo_id[:12]}-{commit['commit_id'][:8]}-{uid}"),
3143 repo_id=repo_id,
3144 target_type="commit",
3145 target_id=commit["commit_id"],
3146 user_id=uid,
3147 emoji=emoji,
3148 created_at=_now(days=2),
3149 ))
3150 reaction_count += 1
3151 except Exception:
3152 pass
3153 print(f" ✅ Reactions: {reaction_count}")
3154
3155 await db.flush()
3156
3157 # ── 12. Follows (social graph) ────────────────────────────────
3158 # Rich bidirectional follow graph: gabriel↔sofia, marcus↔fatou,
3159 # yuki↔chen, aaliya↔pierre, plus composer-to-community asymmetric follows.
3160 # 60+ total, preserving all prior pairs.
3161 follow_pairs = [
3162 # Original follows — gabriel is the hub (everyone follows him)
3163 (SOFIA, GABRIEL), (MARCUS, GABRIEL), (YUKI, GABRIEL),
3164 (AALIYA, GABRIEL), (CHEN, GABRIEL), (FATOU, GABRIEL),
3165 (PIERRE, GABRIEL),
3166
3167 # Bidirectional close collaborator pairs (symmetric)
3168 (GABRIEL, SOFIA), (SOFIA, GABRIEL), # already added above, deduped by _uid
3169 (MARCUS, FATOU), (FATOU, MARCUS),
3170 (YUKI, CHEN), (CHEN, YUKI),
3171 (AALIYA, PIERRE), (PIERRE, AALIYA),
3172
3173 # Cross-community follows
3174 (GABRIEL, MARCUS), (SOFIA, MARCUS), (AALIYA, MARCUS),
3175 (GABRIEL, YUKI), (SOFIA, YUKI),
3176 (GABRIEL, AALIYA), (MARCUS, AALIYA),
3177 (GABRIEL, CHEN),
3178 (GABRIEL, FATOU), (AALIYA, FATOU),
3179 (GABRIEL, PIERRE), (SOFIA, PIERRE),
3180 (MARCUS, SOFIA), (PIERRE, SOFIA),
3181 (MARCUS, YUKI), (FATOU, YUKI),
3182 (PIERRE, MARCUS), (YUKI, MARCUS),
3183 (CHEN, MARCUS), (FATOU, SOFIA),
3184 (YUKI, AALIYA), (CHEN, AALIYA), (FATOU, AALIYA),
3185 (MARCUS, CHEN), (AALIYA, CHEN), (PIERRE, CHEN),
3186 (SOFIA, FATOU), (CHEN, FATOU), (PIERRE, FATOU),
3187 (YUKI, PIERRE), (MARCUS, PIERRE), (CHEN, PIERRE),
3188 (SOFIA, CHEN), (AALIYA, YUKI), (FATOU, CHEN),
3189
3190 # Community users follow composer archive maintainers
3191 (GABRIEL, BACH), (SOFIA, BACH), (MARCUS, BACH),
3192 (CHEN, BACH), (AALIYA, BACH), (YUKI, BACH),
3193 (GABRIEL, CHOPIN), (AALIYA, CHOPIN), (SOFIA, CHOPIN),
3194 (MARCUS, SCOTT_JOPLIN), (GABRIEL, SCOTT_JOPLIN),
3195 (FATOU, SCOTT_JOPLIN),
3196 (CHEN, KEVIN_MACLEOD), (GABRIEL, KEVIN_MACLEOD),
3197 ]
3198 # Deduplicate pairs so _uid collisions never cause constraint errors
3199 seen_follows: set[tuple[str, str]] = set()
3200 deduped_follows = []
3201 for pair in follow_pairs:
3202 if pair not in seen_follows:
3203 seen_follows.add(pair)
3204 deduped_follows.append(pair)
3205 for follower, followee in deduped_follows:
3206 db.add(MusehubFollow(
3207 follow_id=_uid(f"follow-{follower}-{followee}"),
3208 follower_id=follower,
3209 followee_id=followee,
3210 created_at=_now(days=15),
3211 ))
3212 print(f" ✅ Follows: {len(deduped_follows)}")
3213
3214 await db.flush()
3215
3216 # ── 13. Watches ───────────────────────────────────────────────
3217 # 60+ total: community users watch each other's repos and the composer
3218 # archive repos they draw inspiration from.
3219 watch_pairs = [
3220 # Community repo watches
3221 (GABRIEL, REPO_AMBIENT), (GABRIEL, REPO_AFROBEAT), (GABRIEL, REPO_FUNK_SUITE),
3222 (GABRIEL, REPO_JAZZ_TRIO), (GABRIEL, REPO_GRANULAR), (GABRIEL, REPO_CHANSON),
3223 (GABRIEL, REPO_MICROTONAL), (GABRIEL, REPO_DRUM_MACHINE),
3224
3225 (SOFIA, REPO_NEO_SOUL), (SOFIA, REPO_FUNK_SUITE), (SOFIA, REPO_CHANSON),
3226 (SOFIA, REPO_MODAL_JAZZ), (SOFIA, REPO_AFROBEAT), (SOFIA, REPO_MICROTONAL),
3227
3228 (MARCUS, REPO_NEO_SOUL), (MARCUS, REPO_AMBIENT), (MARCUS, REPO_AFROBEAT),
3229 (MARCUS, REPO_MODAL_JAZZ), (MARCUS, REPO_JAZZ_TRIO), (MARCUS, REPO_FUNK_SUITE),
3230
3231 (YUKI, REPO_AMBIENT), (YUKI, REPO_GRANULAR), (YUKI, REPO_MICROTONAL),
3232 (YUKI, REPO_NEO_SOUL), (YUKI, REPO_DRUM_MACHINE),
3233
3234 (AALIYA, REPO_NEO_SOUL), (AALIYA, REPO_AFROBEAT), (AALIYA, REPO_FUNK_SUITE),
3235 (AALIYA, REPO_MODAL_JAZZ), (AALIYA, REPO_JAZZ_TRIO), (AALIYA, REPO_CHANSON),
3236
3237 (CHEN, REPO_MICROTONAL), (CHEN, REPO_AMBIENT), (CHEN, REPO_GRANULAR),
3238 (CHEN, REPO_MODAL_JAZZ), (CHEN, REPO_NEO_SOUL), (CHEN, REPO_DRUM_MACHINE),
3239
3240 (FATOU, REPO_AFROBEAT), (FATOU, REPO_DRUM_MACHINE),(FATOU, REPO_FUNK_SUITE),
3241 (FATOU, REPO_NEO_SOUL), (FATOU, REPO_MODAL_JAZZ), (FATOU, REPO_GRANULAR),
3242
3243 (PIERRE, REPO_CHANSON), (PIERRE, REPO_AMBIENT), (PIERRE, REPO_NEO_SOUL),
3244 (PIERRE, REPO_MODAL_JAZZ), (PIERRE, REPO_MICROTONAL),
3245
3246 # Composer archive repo watches — community users watching source material
3247 (GABRIEL, REPO_GOLDBERG), (GABRIEL, REPO_WTC), (GABRIEL, REPO_NOCTURNES),
3248 (GABRIEL, REPO_MAPLE_LEAF), (GABRIEL, REPO_CIN_STRINGS),
3249
3250 (SOFIA, REPO_GOLDBERG), (SOFIA, REPO_WTC), (SOFIA, REPO_NOCTURNES),
3251 (MARCUS, REPO_MAPLE_LEAF), (MARCUS, REPO_GOLDBERG), (MARCUS, REPO_WTC),
3252 (AALIYA, REPO_NOCTURNES), (AALIYA, REPO_MAPLE_LEAF),
3253 (CHEN, REPO_CIN_STRINGS), (CHEN, REPO_GOLDBERG),
3254 (YUKI, REPO_WTC), (FATOU, REPO_MAPLE_LEAF), (PIERRE, REPO_NOCTURNES),
3255
3256 # Genre-fusion community repos
3257 (SOFIA, REPO_NEO_BAROQUE), (MARCUS, REPO_NEO_BAROQUE),
3258 (GABRIEL, REPO_JAZZ_CHOPIN), (MARCUS, REPO_RAGTIME_EDM),
3259 (GABRIEL, REPO_FILM_SCORE), (SOFIA, REPO_FILM_SCORE),
3260 (GABRIEL, REPO_COMMUNITY), (AALIYA, REPO_COMMUNITY),
3261 ]
3262 for user_id, repo_id in watch_pairs:
3263 db.add(MusehubWatch(
3264 watch_id=_uid(f"watch-{user_id}-{repo_id}"),
3265 user_id=user_id,
3266 repo_id=repo_id,
3267 created_at=_now(days=10),
3268 ))
3269 print(f" ✅ Watches: {len(watch_pairs)}")
3270
3271 await db.flush()
3272
3273 # ── 14. Notifications ─────────────────────────────────────────
3274 # 15-25 unread per user, all event types, all 8 users. 15-25 unread means
3275 # we create 20 notifications per user with the first 5 marked as read and
3276 # the remaining 15 unread — satisfying the "15-25 unread" spec.
3277 EVENT_TYPES = [
3278 "comment", "pr_opened", "pr_merged", "issue_opened", "new_commit",
3279 "new_follower", "star", "fork", "release", "watch",
3280 ]
3281 NOTIFS_PER_USER = 20 # 5 read + 15 unread = 15 unread per user
3282 READ_THRESHOLD = 5 # first N are read
3283 all_repos_flat = [r["repo_id"] for r in (list(REPOS) + list(GENRE_REPOS))]
3284 notif_count = 0
3285 for i, (uid, uname, _) in enumerate(USERS):
3286 for j in range(NOTIFS_PER_USER):
3287 actor_user = USERS[(i + j + 1) % len(USERS)]
3288 db.add(MusehubNotification(
3289 notif_id=_uid(f"notif2-{uid}-{j}"),
3290 recipient_id=uid,
3291 event_type=EVENT_TYPES[j % len(EVENT_TYPES)],
3292 repo_id=all_repos_flat[(i + j) % len(all_repos_flat)],
3293 actor=actor_user[1],
3294 payload={"message": f"Notification {j + 1} for {uname}"},
3295 is_read=j < READ_THRESHOLD,
3296 created_at=_now(days=j),
3297 ))
3298 notif_count += 1
3299 print(f" ✅ Notifications: {notif_count} ({NOTIFS_PER_USER - READ_THRESHOLD} unread per user)")
3300
3301 await db.flush()
3302
3303 # ── 15. Forks ─────────────────────────────────────────────────
3304 # Original community-to-community forks
3305 db.add(MusehubFork(
3306 fork_id=_uid("fork-neo-soul-marcus"),
3307 source_repo_id=REPO_NEO_SOUL,
3308 fork_repo_id=REPO_NEO_SOUL_FORK,
3309 forked_by="marcus",
3310 created_at=_now(days=10),
3311 ))
3312 db.add(MusehubFork(
3313 fork_id=_uid("fork-ambient-yuki"),
3314 source_repo_id=REPO_AMBIENT,
3315 fork_repo_id=REPO_AMBIENT_FORK,
3316 forked_by="yuki",
3317 created_at=_now(days=5),
3318 ))
3319 # Genre-archive → community forks: each community repo that remixes a
3320 # composer's archive is modelled as a fork of the canonical archive repo.
3321 # marcus/ragtime-edm ← scott_joplin/maple-leaf-rag
3322 db.add(MusehubFork(
3323 fork_id=_uid("fork-maple-leaf-marcus"),
3324 source_repo_id=REPO_MAPLE_LEAF,
3325 fork_repo_id=REPO_RAGTIME_EDM,
3326 forked_by="marcus",
3327 created_at=_now(days=30),
3328 ))
3329 # aaliya/jazz-chopin ← chopin/nocturnes
3330 db.add(MusehubFork(
3331 fork_id=_uid("fork-nocturnes-aaliya"),
3332 source_repo_id=REPO_NOCTURNES,
3333 fork_repo_id=REPO_JAZZ_CHOPIN,
3334 forked_by="aaliya",
3335 created_at=_now(days=28),
3336 ))
3337 # gabriel/neo-baroque ← bach/goldberg-variations
3338 db.add(MusehubFork(
3339 fork_id=_uid("fork-goldberg-gabriel-neobaroque"),
3340 source_repo_id=REPO_GOLDBERG,
3341 fork_repo_id=REPO_NEO_BAROQUE,
3342 forked_by="gabriel",
3343 created_at=_now(days=25),
3344 ))
3345 # chen/film-score ← kevin_macleod/cinematic-strings
3346 db.add(MusehubFork(
3347 fork_id=_uid("fork-cinstrings-chen"),
3348 source_repo_id=REPO_CIN_STRINGS,
3349 fork_repo_id=REPO_FILM_SCORE,
3350 forked_by="chen",
3351 created_at=_now(days=22),
3352 ))
3353 # gabriel/community-collab ← bach/goldberg-variations
3354 db.add(MusehubFork(
3355 fork_id=_uid("fork-goldberg-gabriel-community"),
3356 source_repo_id=REPO_GOLDBERG,
3357 fork_repo_id=REPO_COMMUNITY,
3358 forked_by="gabriel",
3359 created_at=_now(days=20),
3360 ))
3361 print(" ✅ Forks: 7")
3362
3363 await db.flush()
3364
3365 # ── 16. View events (analytics) ───────────────────────────────
3366 # 1000+ total across all repos (community + genre archives). Each repo
3367 # receives 30 days of daily view fingerprints; active repos get up to 10
3368 # unique viewers per day, quieter repos get fewer.
3369 view_count = 0
3370 view_repos = list(REPOS) + list(GENRE_REPOS)
3371 for r in view_repos:
3372 repo_id = r["repo_id"]
3373 star_count = r.get("star_count", 5)
3374 for day_offset in range(30):
3375 date_str = (_now(days=day_offset)).strftime("%Y-%m-%d")
3376 viewers = max(star_count // 3 + 1, 3) # at least 3 unique viewers/day
3377 for v in range(min(viewers, 10)):
3378 try:
3379 db.add(MusehubViewEvent(
3380 view_id=_uid(f"view-{repo_id}-{day_offset}-{v}"),
3381 repo_id=repo_id,
3382 viewer_fingerprint=_sha(f"viewer-{repo_id}-{v}"),
3383 event_date=date_str,
3384 created_at=_now(days=day_offset),
3385 ))
3386 view_count += 1
3387 except Exception:
3388 pass
3389 print(f" ✅ View events: {view_count}")
3390
3391 await db.flush()
3392
3393 # ── 17. Download events ───────────────────────────────────────
3394 # 5-25 downloads per release tag across all repos. Using release_tags
3395 # (built in step 7) so every release gets at least 5 unique downloaders.
3396 all_user_ids = [u[0] for u in USERS]
3397 dl_count = 0
3398 dl_repos = list(REPOS) + list(GENRE_REPOS)
3399 for r in dl_repos:
3400 repo_id = r["repo_id"]
3401 tags = release_tags.get(repo_id, ["main"])
3402 if not tags:
3403 tags = ["main"]
3404 for ti, tag in enumerate(tags):
3405 # 5-15 downloads per release depending on tag index (older = more)
3406 n_downloads = max(5, 15 - ti * 2)
3407 for i in range(n_downloads):
3408 db.add(MusehubDownloadEvent(
3409 dl_id=_uid(f"dl2-{repo_id}-{tag}-{i}"),
3410 repo_id=repo_id,
3411 ref=tag,
3412 downloader_id=all_user_ids[i % len(all_user_ids)],
3413 created_at=_now(days=ti * 7 + i),
3414 ))
3415 dl_count += 1
3416 print(f" ✅ Download events: {dl_count}")
3417
3418 # ── 17b. Webhooks + deliveries (1-3/repo, 10-20 deliveries each) ──────────
3419 WEBHOOK_CONFIGS: list[tuple[str, list[str]]] = [
3420 # (url_suffix, events)
3421 ("push", ["push"]),
3422 ("pr", ["pull_request", "push"]),
3423 ("release", ["release", "push", "pull_request"]),
3424 ]
3425 # Delivery outcome patterns: (response_status, success)
3426 # Mix of 200 OK, 500 server error, and 0 timeout
3427 DELIVERY_OUTCOMES: list[tuple[int, bool, str]] = [
3428 (200, True, '{"status": "ok"}'),
3429 (200, True, '{"accepted": true}'),
3430 (200, True, '{"queued": true}'),
3431 (500, False, '{"error": "Internal Server Error"}'),
3432 (500, False, '{"error": "Service unavailable"}'),
3433 (0, False, ""), # timeout — no response
3434 (200, True, '{"ok": 1}'),
3435 (404, False, '{"error": "Not Found"}'),
3436 (200, True, '{"received": true}'),
3437 (0, False, ""), # timeout
3438 ]
3439 WEBHOOK_EVENT_TYPES = ["push", "pull_request", "release", "issue", "comment"]
3440
3441 webhook_count = 0
3442 delivery_count = 0
3443 for r in REPOS[:10]:
3444 repo_id = r["repo_id"]
3445 # 1-3 webhooks per repo based on repo index
3446 repo_idx = REPOS.index(r)
3447 n_webhooks = 1 + (repo_idx % 3)
3448 for wh_i in range(n_webhooks):
3449 url_suffix, events = WEBHOOK_CONFIGS[wh_i % len(WEBHOOK_CONFIGS)]
3450 wh_id = _uid(f"webhook-{repo_id}-{wh_i}")
3451 db.add(MusehubWebhook(
3452 webhook_id=wh_id,
3453 repo_id=repo_id,
3454 url=f"https://hooks.example.com/{r['owner']}/{r['slug']}/{url_suffix}",
3455 events=events,
3456 secret=_sha(f"secret-{repo_id}-{wh_i}")[:32],
3457 active=True,
3458 created_at=_now(days=r["days_ago"] - 5),
3459 ))
3460 webhook_count += 1
3461 # 10-20 deliveries per webhook
3462 n_deliveries = 10 + (abs(hash(wh_id)) % 11)
3463 for d_i in range(n_deliveries):
3464 outcome_idx = (abs(hash(wh_id)) + d_i) % len(DELIVERY_OUTCOMES)
3465 status, success, resp_body = DELIVERY_OUTCOMES[outcome_idx]
3466 evt = WEBHOOK_EVENT_TYPES[d_i % len(WEBHOOK_EVENT_TYPES)]
3467 db.add(MusehubWebhookDelivery(
3468 delivery_id=_uid(f"wh-delivery-{wh_id}-{d_i}"),
3469 webhook_id=wh_id,
3470 event_type=evt,
3471 payload=f'{{"event": "{evt}", "repo": "{repo_id}", "seq": {d_i}}}',
3472 attempt=1 + (d_i % 3),
3473 success=success,
3474 response_status=status,
3475 response_body=resp_body,
3476 delivered_at=_now(days=r["days_ago"] - d_i, hours=d_i % 24),
3477 ))
3478 delivery_count += 1
3479 print(f" ✅ Webhooks: {webhook_count} Deliveries: {delivery_count}")
3480
3481 await db.flush()
3482
3483 # ── 17c. Render Jobs (async audio render pipeline) ─────────────────────────
3484 # Creates completed, processing, and failed render jobs across repos.
3485 # Each job ties a commit to a set of generated MP3/piano-roll artifact IDs.
3486 _RENDER_STATUSES = ["completed", "completed", "completed", "processing", "failed"]
3487 render_job_count = 0
3488 for r in all_repos[:16]: # Community + most popular genre archive repos
3489 repo_id = r["repo_id"]
3490 commits = all_commits.get(repo_id, [])
3491 if not commits:
3492 continue
3493 # 2-3 render jobs per repo (latest commits)
3494 target_commits = commits[-3:]
3495 for ji, c in enumerate(target_commits):
3496 rj_cid: str = c["commit_id"]
3497 rj_status: str = _RENDER_STATUSES[(abs(hash(repo_id)) + ji) % len(_RENDER_STATUSES)]
3498 err_msg = "Storpheus timeout after 30s" if rj_status == "failed" else None
3499 mp3_ids = (
3500 [f"sha256:{_sha(f'mp3-{rj_cid}-{i}')}" for i in range(3)]
3501 if rj_status == "completed" else []
3502 )
3503 img_ids = (
3504 [f"sha256:{_sha(f'img-{rj_cid}-{i}')}" for i in range(2)]
3505 if rj_status == "completed" else []
3506 )
3507 try:
3508 db.add(MusehubRenderJob(
3509 render_job_id=_uid(f"rjob-{repo_id}-{rj_cid[:12]}"),
3510 repo_id=repo_id,
3511 commit_id=rj_cid,
3512 status=rj_status,
3513 error_message=err_msg,
3514 midi_count=len(REPO_TRACKS.get(REPO_KEY_MAP.get(repo_id, "neo-soul"),
3515 REPO_TRACKS["neo-soul"])),
3516 mp3_object_ids=mp3_ids,
3517 image_object_ids=img_ids,
3518 created_at=_now(days=ji + 1),
3519 updated_at=_now(days=ji),
3520 ))
3521 render_job_count += 1
3522 except Exception:
3523 pass # unique constraint on (repo_id, commit_id) — skip dupes
3524 print(f" ✅ Render jobs: {render_job_count}")
3525
3526 await db.flush()
3527
3528 # ── 17d. Activity Event Stream (musehub_events) ────────────────────────────
3529 # Captures the most recent activity on each repo: push, pr_open, pr_merge,
3530 # issue_open, issue_close, release, tag, session, fork, star events.
3531 _EVENT_TEMPLATES: list[tuple[str, str]] = [
3532 ("push", "pushed {n} commits to {branch}"),
3533 ("push", "force-pushed to {branch} (rebase)"),
3534 ("pr_open", "opened pull request: {title}"),
3535 ("pr_merge", "merged pull request into {branch}"),
3536 ("pr_close", "closed pull request without merging"),
3537 ("issue_open", "opened issue: {title}"),
3538 ("issue_close", "closed issue #{n}"),
3539 ("release", "published release {tag}"),
3540 ("tag", "created tag {tag} at {branch}"),
3541 ("session_start", "started recording session"),
3542 ("session_end", "ended recording session — {n} commits"),
3543 ("fork", "forked this repo"),
3544 ("star", "starred this repo"),
3545 ("branch_create", "created branch {branch}"),
3546 ]
3547 _EVENT_ACTORS_POOL = list(ALL_CONTRIBUTORS)
3548 event_count = 0
3549 for r in all_repos:
3550 repo_id = r["repo_id"]
3551 commits = all_commits.get(repo_id, [])
3552 owner = r["owner"]
3553 actor_pool = [owner] * 3 + _EVENT_ACTORS_POOL # Weight owner higher
3554 # Generate 8-15 events per repo spread over the last 60 days
3555 n_events = 8 + abs(hash(repo_id)) % 8
3556 for ei in range(n_events):
3557 tmpl_type, tmpl_body = _EVENT_TEMPLATES[ei % len(_EVENT_TEMPLATES)]
3558 actor = actor_pool[(abs(hash(repo_id)) + ei) % len(actor_pool)]
3559 n_val = (ei % 10) + 1
3560 branch = "main" if ei % 3 == 0 else f"feat/{r['slug'][:20]}-{ei}"
3561 tag = f"v{ei // 3 + 1}.0.{'0' if ei % 2 == 0 else '1'}"
3562 description = (
3563 tmpl_body
3564 .replace("{n}", str(n_val))
3565 .replace("{branch}", branch)
3566 .replace("{title}", f"Issue/PR title for event {ei}")
3567 .replace("{tag}", tag)
3568 )
3569 meta: dict[str, object] = {
3570 "actor": actor,
3571 "branch": branch,
3572 "commit_count": n_val,
3573 }
3574 if commits and ei < len(commits):
3575 meta["commit_id"] = commits[ei]["commit_id"]
3576 db.add(MusehubEvent(
3577 event_id=_uid(f"event-{repo_id}-{ei}"),
3578 repo_id=repo_id,
3579 event_type=tmpl_type,
3580 actor=actor,
3581 description=description,
3582 event_metadata=meta,
3583 created_at=_now(days=60 - ei * 4, hours=ei * 3 % 24),
3584 ))
3585 event_count += 1
3586 print(f" ✅ Events: {event_count}")
3587
3588 await db.flush()
3589
3590 # ── 18. Muse variations, phrases, note changes ────────────────
3591 # Collect stable commit hashes from the two most active repos as base
3592 # state IDs. muse_variations.base_state_id links a variation to the DAW
3593 # snapshot it was proposed against — we reuse musehub commit hashes as a
3594 # realistic stand-in for DAW project state hashes.
3595 neo_commits = all_commits.get(REPO_NEO_SOUL, [])
3596 jazz_commits = all_commits.get(REPO_MODAL_JAZZ, [])
3597 neo_hashes = [c["commit_id"] for c in neo_commits[-15:]] or [_sha("nb-fallback")]
3598 jazz_hashes = [c["commit_id"] for c in jazz_commits[-15:]] or [_sha("cc-fallback")]
3599
3600 var_nb, phrase_nb, nc_nb = _make_variation_section(
3601 project_id=PROJECT_NEO_BAROQUE,
3602 intents=VARIATION_INTENTS_NEO_BAROQUE,
3603 track_ids=TRACK_IDS_NEO_BAROQUE,
3604 region_ids=REGION_IDS_NEO_BAROQUE,
3605 base_commit_hashes=neo_hashes,
3606 seed_prefix="nb",
3607 )
3608 var_cc, phrase_cc, nc_cc = _make_variation_section(
3609 project_id=PROJECT_COMMUNITY_COLLAB,
3610 intents=VARIATION_INTENTS_COMMUNITY_COLLAB,
3611 track_ids=TRACK_IDS_COMMUNITY,
3612 region_ids=REGION_IDS_COMMUNITY,
3613 base_commit_hashes=jazz_hashes,
3614 seed_prefix="cc",
3615 )
3616
3617 all_variations = var_nb + var_cc
3618 all_phrases = phrase_nb + phrase_cc
3619 all_note_changes = nc_nb + nc_cc
3620
3621 for var in all_variations:
3622 db.add(var)
3623 await db.flush()
3624
3625 for ph in all_phrases:
3626 db.add(ph)
3627 await db.flush()
3628
3629 for nc in all_note_changes:
3630 db.add(nc)
3631 await db.flush()
3632
3633 print(f" ✅ Variations: {len(all_variations)} ({len(var_nb)} neo-baroque, {len(var_cc)} community-collab)")
3634 print(f" ✅ Phrases: {len(all_phrases)}")
3635 print(f" ✅ Note changes: {len(all_note_changes)}")
3636 # ── 19. Muse VCS — muse_objects, muse_snapshots, muse_commits, muse_tags ─
3637 #
3638 # Inserts content-addressed MIDI blobs, snapshot manifests, a proper DAG
3639 # of Muse commits (including merge commits), and the full tag taxonomy.
3640 #
3641 # Insertion order respects FK constraints:
3642 # muse_objects → muse_snapshots → muse_commits → muse_tags
3643 #
3644 muse_obj_count = 0
3645 muse_snap_count = 0
3646 muse_commit_count = 0
3647 muse_tag_count = 0
3648
3649 # Running objects pool so the same content can be deduplicated across
3650 # snapshots (object_ids that haven't changed reuse the same sha256).
3651 # Structure: repo_id → {filename: object_id}
3652 _prev_objects: dict[str, dict[str, str]] = {}
3653
3654 for r in REPOS:
3655 repo_id = r["repo_id"]
3656 hub_commits = all_commits.get(repo_id, [])
3657 if not hub_commits:
3658 continue
3659
3660 track_files = MUSE_VCS_FILES.get(repo_id, MUSE_VCS_FILES[REPO_AMBIENT])
3661 meta = MUSE_COMMIT_META.get(repo_id, MUSE_COMMIT_META[REPO_AMBIENT])
3662 is_rich = repo_id in MUSE_RICH_TAG_REPOS
3663
3664 prev_objects: dict[str, str] = {} # filename → object_id for this repo
3665 muse_commit_ids: list[str] = [] # ordered muse commit_ids for this repo
3666
3667 for i, hub_c in enumerate(hub_commits):
3668 snap_seed = f"snap-muse-{repo_id}-{i}"
3669 committed_at = hub_c["timestamp"]
3670
3671 # Build this commit's object set.
3672 # Every commit, ~2 files "change" (get fresh object_ids).
3673 # The rest reuse from the previous commit — simulating deduplication.
3674 changed_indices = {i % len(track_files), (i + 2) % len(track_files)}
3675 commit_objects: dict[str, str] = {}
3676
3677 for fi, (fname, base_size) in enumerate(track_files):
3678 if fi in changed_indices or fname not in prev_objects:
3679 # New or modified file → fresh content-addressed blob.
3680 obj_id = _sha(f"midi-{repo_id}-{fname}-v{i}")
3681 size = base_size + (i * 128) % 4096
3682 await db.execute(
3683 text(
3684 "INSERT INTO muse_objects (object_id, size_bytes, created_at)"
3685 " VALUES (:oid, :sz, :ca)"
3686 " ON CONFLICT (object_id) DO NOTHING"
3687 ),
3688 {"oid": obj_id, "sz": size, "ca": committed_at},
3689 )
3690 muse_obj_count += 1
3691 else:
3692 # Unchanged file → reuse previous object_id (deduplication).
3693 obj_id = prev_objects[fname]
3694 commit_objects[fname] = obj_id
3695
3696 prev_objects = commit_objects
3697
3698 # Snapshot — manifest maps track paths to object_ids.
3699 snapshot_id = _sha(snap_seed)
3700 manifest: dict[str, str] = {f"tracks/{fname}": oid for fname, oid in commit_objects.items()}
3701 await db.execute(
3702 text(
3703 "INSERT INTO muse_snapshots (snapshot_id, manifest, created_at)"
3704 " VALUES (:sid, :manifest, :ca)"
3705 " ON CONFLICT (snapshot_id) DO NOTHING"
3706 ),
3707 {"sid": snapshot_id, "manifest": json.dumps(manifest), "ca": committed_at},
3708 )
3709 muse_snap_count += 1
3710
3711 # Muse commit — derives its ID from snapshot + parent + message.
3712 parent_id: str | None = muse_commit_ids[-1] if muse_commit_ids else None
3713 # Merge commit every 7 commits (from commit 7 onward) — parent2 is the
3714 # commit from 5 positions back, simulating a merged feature branch.
3715 # Interval of 7 guarantees ≥5 merges per repo for repos with ≥35 commits.
3716 parent2_id: str | None = None
3717 if i >= 7 and i % 7 == 0 and len(muse_commit_ids) >= 6:
3718 parent2_id = muse_commit_ids[-6]
3719
3720 commit_id = _sha(f"muse-c-{snapshot_id}-{parent_id or ''}-{hub_c['message']}")
3721 await db.execute(
3722 text(
3723 "INSERT INTO muse_commits"
3724 " (commit_id, repo_id, branch, parent_commit_id, parent2_commit_id,"
3725 " snapshot_id, message, author, committed_at, created_at, metadata)"
3726 " VALUES"
3727 " (:cid, :rid, :branch, :pid, :p2id,"
3728 " :sid, :msg, :author, :cat, :cat, :meta)"
3729 " ON CONFLICT (commit_id) DO NOTHING"
3730 ),
3731 {
3732 "cid": commit_id,
3733 "rid": repo_id,
3734 "branch": hub_c["branch"],
3735 "pid": parent_id,
3736 "p2id": parent2_id,
3737 "sid": snapshot_id,
3738 "msg": hub_c["message"],
3739 "author": hub_c["author"],
3740 "cat": committed_at,
3741 "meta": json.dumps(meta),
3742 },
3743 )
3744 muse_commit_ids.append(commit_id)
3745 muse_commit_count += 1
3746
3747 # Tags: apply cycling taxonomy to every commit.
3748 # Rich repos get ALL taxonomy values; others get a representative subset.
3749 if is_rich:
3750 # Cycle through all 57 tag values across commits so every value appears.
3751 tag_val = _ALL_MUSE_TAGS[i % len(_ALL_MUSE_TAGS)]
3752 tag_vals = [tag_val]
3753 # Also add a second tag from a different category group.
3754 second_idx = (i + len(MUSE_EMOTION_TAGS)) % len(_ALL_MUSE_TAGS)
3755 if second_idx != i % len(_ALL_MUSE_TAGS):
3756 tag_vals.append(_ALL_MUSE_TAGS[second_idx])
3757 else:
3758 # Non-rich repos get one tag per commit drawn from a trimmed pool.
3759 _trimmed = MUSE_EMOTION_TAGS + MUSE_STAGE_TAGS + MUSE_GENRE_TAGS
3760 tag_vals = [_trimmed[i % len(_trimmed)]]
3761
3762 for tag_val in tag_vals:
3763 tag_id = _uid(f"muse-tag-{commit_id}-{tag_val}")
3764 await db.execute(
3765 text(
3766 "INSERT INTO muse_tags (tag_id, repo_id, commit_id, tag, created_at)"
3767 " VALUES (:tid, :rid, :cid, :tag, :ca)"
3768 " ON CONFLICT (tag_id) DO NOTHING"
3769 ),
3770 {"tid": tag_id, "rid": repo_id, "cid": commit_id,
3771 "tag": tag_val, "ca": committed_at},
3772 )
3773 muse_tag_count += 1
3774
3775 _prev_objects[repo_id] = prev_objects
3776
3777 # Ensure every tag taxonomy value appears at least once in REPO_NEO_SOUL.
3778 # Walk through ALL values and seed any that haven't been covered yet.
3779 if all_commits.get(REPO_NEO_SOUL):
3780 hub_commits_ns = all_commits[REPO_NEO_SOUL]
3781 muse_ids_ns: list[str] = []
3782 for i, hub_c in enumerate(hub_commits_ns):
3783 snap_seed = f"snap-muse-{REPO_NEO_SOUL}-{i}"
3784 snapshot_id = _sha(snap_seed)
3785 parent_id_ns: str | None = muse_ids_ns[-1] if muse_ids_ns else None
3786 commit_id_ns = _sha(f"muse-c-{snapshot_id}-{parent_id_ns or ''}-{hub_c['message']}")
3787 muse_ids_ns.append(commit_id_ns)
3788
3789 # Fetch existing tags for REPO_NEO_SOUL.
3790 result = await db.execute(
3791 text("SELECT tag FROM muse_tags WHERE repo_id = :rid"),
3792 {"rid": REPO_NEO_SOUL},
3793 )
3794 existing_tags: set[str] = {row[0] for row in result.fetchall()}
3795 missing_tags = [t for t in _ALL_MUSE_TAGS if t not in existing_tags]
3796
3797 for j, missing_tag in enumerate(missing_tags):
3798 commit_id_ns = muse_ids_ns[j % len(muse_ids_ns)]
3799 committed_at_ns = hub_commits_ns[j % len(hub_commits_ns)]["timestamp"]
3800 tag_id = _uid(f"muse-tag-fill-{REPO_NEO_SOUL}-{missing_tag}")
3801 await db.execute(
3802 text(
3803 "INSERT INTO muse_tags (tag_id, repo_id, commit_id, tag, created_at)"
3804 " VALUES (:tid, :rid, :cid, :tag, :ca)"
3805 " ON CONFLICT (tag_id) DO NOTHING"
3806 ),
3807 {"tid": tag_id, "rid": REPO_NEO_SOUL, "cid": commit_id_ns,
3808 "tag": missing_tag, "ca": committed_at_ns},
3809 )
3810 muse_tag_count += 1
3811
3812 await db.flush()
3813 print(f" ✅ Muse objects: {muse_obj_count} blobs")
3814 print(f" ✅ Muse snapshots: {muse_snap_count} manifests")
3815 print(f" ✅ Muse commits: {muse_commit_count} (DAG; includes merge commits)")
3816 print(f" ✅ Muse tags: {muse_tag_count} (full taxonomy)")
3817
3818 await db.flush()
3819
3820 # ── 20. Usage Logs (billing history per user) ─────────────────────────────
3821 _USAGE_MODELS = ["anthropic/claude-sonnet-4.6", "anthropic/claude-opus-4.6"]
3822 _USAGE_PROMPTS = [
3823 "Add a counter-melody above the baroque theme",
3824 "Reharmonize the bridge using tritone substitutions",
3825 "Humanize the drum timing across all tracks",
3826 "Add string pad layer to the ambient section",
3827 "Transpose bass line down a perfect fifth",
3828 "Generate a jazz piano voicing for the IV chord",
3829 "Thicken the horns arrangement with parallel 3rds",
3830 "Reduce note density in bars 9-16 on the keys track",
3831 ]
3832 usage_count = 0
3833 for uid, uname, _ in USERS: # Only community users generate usage
3834 n_logs = 8 + abs(hash(uid)) % 12 # 8-20 usage logs per user
3835 for li in range(n_logs):
3836 model = _USAGE_MODELS[li % len(_USAGE_MODELS)]
3837 prompt = _USAGE_PROMPTS[li % len(_USAGE_PROMPTS)]
3838 prompt_tok = 800 + abs(hash(uid + str(li))) % 1200
3839 compl_tok = 300 + abs(hash(uid + str(li) + "c")) % 800
3840 cost = (prompt_tok * 3 + compl_tok * 15) // 100 # ~cents approximation
3841 db.add(UsageLog(
3842 id=_uid(f"usage-{uid}-{li}"),
3843 user_id=uid,
3844 prompt=f"[{uname}] {prompt} (iteration {li + 1})",
3845 model=model,
3846 prompt_tokens=prompt_tok,
3847 completion_tokens=compl_tok,
3848 cost_cents=cost,
3849 created_at=_now(days=60 - li * 3),
3850 ))
3851 usage_count += 1
3852 print(f" ✅ Usage logs: {usage_count}")
3853
3854 await db.flush()
3855
3856 # ── 21. Access Tokens (API key tracking) ──────────────────────────────────
3857 token_count = 0
3858 for uid, uname, _ in USERS:
3859 # 1-3 access tokens per user (active, revoked, expired)
3860 n_tokens = 1 + abs(hash(uid)) % 3
3861 for ti in range(n_tokens):
3862 is_revoked = ti > 0 and ti % 2 == 0
3863 db.add(AccessToken(
3864 id=_uid(f"token-{uid}-{ti}"),
3865 user_id=uid,
3866 token_hash=_sha(f"tok-{uid}-{ti}-secret"),
3867 expires_at=_now(days=-30 * ti) if is_revoked else _now(days=-365),
3868 revoked=is_revoked,
3869 created_at=_now(days=90 - ti * 15),
3870 ))
3871 token_count += 1
3872 print(f" ✅ Access tokens: {token_count}")
3873
3874 await db.flush()
3875
3876 # ── 22. Conversations + Messages + Actions ────────────────────────────────
3877 _CONV_TITLES = [
3878 "Neo-Soul chord voicing session",
3879 "Ambient texture generation",
3880 "Jazz reharmonization experiment",
3881 "Drum groove exploration",
3882 "Counter-melody development",
3883 "Bridge arrangement help",
3884 "Mix balance discussion",
3885 "Modal jazz improvisation sketch",
3886 ]
3887 _MSG_CONTENTS: list[tuple[str, str]] = [
3888 ("user", "Can you add a counter-melody above the baroque theme in bars 9-16?"),
3889 ("assistant", "I'll add a counter-melody that follows the harmonic rhythm of the baroque theme. The new voice will move in contrary motion to create interest."),
3890 ("user", "Make the counter-melody more chromatic — use passing tones"),
3891 ("assistant", "Adding chromatic passing tones between the structural chord tones. The counter-melody now has richer harmonic color while maintaining the baroque style."),
3892 ("user", "What key is this section in?"),
3893 ("assistant", "The section is in D Dorian — a natural minor mode built on D. The characteristic raised 6th (B natural) gives it the distinctive minor-but-not-sad quality."),
3894 ("user", "Add a jazz-influenced harmony to the bridge"),
3895 ("assistant", "I'll reharmonize the bridge using tritone substitutions on the dominant chords and quartal voicings on the tonic. This will give it a Coltrane-influenced quality."),
3896 ]
3897 _ACTION_TYPES = ["generate_notes", "reharmonize", "transpose", "humanize", "analyze"]
3898 conv_count = 0
3899 msg_count = 0
3900 action_count = 0
3901 for uid, uname, _ in USERS:
3902 n_convs = 3 + abs(hash(uid)) % 4 # 3-7 conversations per user
3903 for ci in range(n_convs):
3904 conv_id = _uid(f"conv-{uid}-{ci}")
3905 title = _CONV_TITLES[(abs(hash(uid)) + ci) % len(_CONV_TITLES)]
3906 project_repo = all_repos[(abs(hash(uid)) + ci) % len(all_repos)]
3907 db.add(Conversation(
3908 id=conv_id,
3909 user_id=uid,
3910 project_id=project_repo["repo_id"],
3911 title=title,
3912 is_archived=ci == 0 and abs(hash(uid)) % 3 == 0,
3913 project_context={
3914 "repo_id": project_repo["repo_id"],
3915 "key_signature": project_repo.get("key_signature"),
3916 "tempo_bpm": project_repo.get("tempo_bpm"),
3917 },
3918 created_at=_now(days=30 - ci * 5),
3919 updated_at=_now(days=30 - ci * 5, hours=ci * 2),
3920 ))
3921 conv_count += 1
3922 # 4-8 messages per conversation (user/assistant alternating)
3923 n_msgs = 4 + (abs(hash(conv_id)) % 5)
3924 prev_msg_id: str | None = None
3925 for mi in range(min(n_msgs, len(_MSG_CONTENTS))):
3926 role, content = _MSG_CONTENTS[mi]
3927 msg_id = _uid(f"msg-{conv_id}-{mi}")
3928 model_used = _USAGE_MODELS[mi % len(_USAGE_MODELS)] if role == "assistant" else None
3929 tok = {"prompt": 800 + mi * 50, "completion": 300 + mi * 30} if role == "assistant" else None
3930 db.add(ConversationMessage(
3931 id=msg_id,
3932 conversation_id=conv_id,
3933 role=role,
3934 content=content,
3935 model_used=model_used,
3936 tokens_used=tok,
3937 cost_cents=((tok["prompt"] * 3 + tok["completion"] * 15) // 100) if tok else 0,
3938 timestamp=_now(days=30 - ci * 5, hours=mi),
3939 ))
3940 msg_count += 1
3941 # Add an action for assistant messages
3942 if role == "assistant" and mi > 0:
3943 action_type = _ACTION_TYPES[mi % len(_ACTION_TYPES)]
3944 db.add(MessageAction(
3945 id=_uid(f"action-{msg_id}"),
3946 message_id=msg_id,
3947 action_type=action_type,
3948 description=f"{action_type.replace('_', ' ').title()} performed on track",
3949 success=mi % 5 != 4, # Mostly successful
3950 error_message=None if mi % 5 != 4 else "Tool execution timeout",
3951 extra_metadata={"track": "keys", "bars": f"{mi * 8}-{mi * 8 + 8}"},
3952 timestamp=_now(days=30 - ci * 5, hours=mi),
3953 ))
3954 action_count += 1
3955 prev_msg_id = msg_id
3956 print(f" ✅ Conversations: {conv_count} Messages: {msg_count} Actions: {action_count}")
3957
3958 await db.flush()
3959
3960 await db.commit()
3961 print()
3962 _print_urls(all_commits, session_ids, pr_ids, release_tags)
3963
3964
3965 def _print_urls(
3966 all_commits: dict[str, list[dict[str, Any]]] | None = None,
3967 session_ids: dict[str, list[str]] | None = None,
3968 pr_ids: dict[str, list[str]] | None = None,
3969 release_tags: dict[str, list[str]] | None = None,
3970 ) -> None:
3971 """Print all seeded MuseHub URLs to stdout for manual browser verification."""
3972 BASE = "http://localhost:10001/musehub/ui"
3973 print()
3974 print("=" * 72)
3975 print("🎵 MUSEHUB — ALL URLs (localhost:10001)")
3976 print("=" * 72)
3977
3978 print("\n── User profiles ────────────────────────────────────────────────")
3979 for _, uname, _ in list(USERS) + list(COMPOSER_USERS):
3980 print(f" {BASE}/users/{uname}")
3981 print(f" {BASE}/{uname} (redirects → above)")
3982
3983 print("\n── Explore / discover ───────────────────────────────────────────")
3984 print(f" {BASE}/explore")
3985 print(f" {BASE}/trending")
3986 print(f" {BASE}/search")
3987 print(f" {BASE}/feed")
3988
3989 all_repos_for_urls = list(REPOS[:8]) + list(GENRE_REPOS)
3990 for r in all_repos_for_urls: # Skip fork repos from URL dump
3991 owner, slug = r["owner"], r["slug"]
3992 repo_id = r["repo_id"]
3993 rbase = f"{BASE}/{owner}/{slug}"
3994
3995 print(f"\n── {owner}/{slug} ─────────────────────────────────────")
3996 print(f" Repo: {rbase}")
3997 print(f" Graph: {rbase}/graph")
3998 print(f" Timeline: {rbase}/timeline")
3999 print(f" Insights: {rbase}/insights")
4000 print(f" Credits: {rbase}/credits")
4001 print(f" Search: {rbase}/search")
4002
4003 if all_commits and repo_id in all_commits:
4004 commits = all_commits[repo_id]
4005 for c in commits[-3:]:
4006 print(f" Commit: {rbase}/commits/{c['commit_id'][:12]}")
4007 if commits:
4008 print(f" Diff: {rbase}/commits/{commits[-1]['commit_id'][:12]}/diff")
4009
4010 print(f" Issues: {rbase}/issues")
4011 print(f" PRs: {rbase}/pulls")
4012 print(f" Releases: {rbase}/releases")
4013 if release_tags and repo_id in release_tags:
4014 for tag in release_tags[repo_id]:
4015 print(f" {rbase}/releases/{tag}")
4016 print(f" Sessions: {rbase}/sessions")
4017 if session_ids and repo_id in session_ids:
4018 for sid in session_ids[repo_id][:2]:
4019 print(f" {rbase}/sessions/{sid}")
4020 print(f" Divergence:{rbase}/divergence")
4021 print(f" Context: {rbase}/context/main")
4022 print(f" Analysis: {rbase}/analysis/main/contour")
4023 print(f" {rbase}/analysis/main/tempo")
4024 print(f" {rbase}/analysis/main/dynamics")
4025
4026 print()
4027 print("=" * 72)
4028 print("✅ Seed complete.")
4029 print("=" * 72)
4030
4031
4032 async def main() -> None:
4033 """CLI entry point. Pass --force to wipe existing seed data before re-seeding."""
4034 force = "--force" in sys.argv
4035 db_url: str = settings.database_url or ""
4036 engine = create_async_engine(db_url, echo=False)
4037 async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) # type: ignore[call-overload] # SQLAlchemy typing: sessionmaker + class_=AsyncSession overload not reflected in stubs
4038 async with async_session() as db:
4039 await seed(db, force=force)
4040 await engine.dispose()
4041
4042
4043 if __name__ == "__main__":
4044 asyncio.run(main())