gabriel / musehub public
musehub.py python
2675 lines 106.8 KB
cd448303 Initial extraction of MuseHub from maestro monorepo. Gabriel Cardona <gabriel@tellurstori.com> 7d ago
1 """Pydantic v2 request/response models for the Muse Hub API.
2
3 All wire-format fields use camelCase via CamelModel. Python code uses
4 snake_case throughout; only serialisation to JSON uses camelCase.
5 """
6 from __future__ import annotations
7
8 from datetime import datetime
9 from typing import NotRequired, TypedDict
10
11 from pydantic import Field
12
13 from musehub.models.base import CamelModel
14
15
16 # ── Sync protocol models ──────────────────────────────────────────────────────
17
18
19 class CommitInput(CamelModel):
20 """A single commit record transferred in a push payload."""
21
22 commit_id: str = Field(
23 ...,
24 description="Content-addressed commit ID (e.g. SHA-256 hex)",
25 examples=["a3f8c1d2e4b5"],
26 )
27 parent_ids: list[str] = Field(
28 default_factory=list,
29 description="Parent commit IDs; empty for the initial commit",
30 examples=[["b2a7d9e1c3f4"]],
31 )
32 message: str = Field(
33 ...,
34 description="Musical commit message describing the compositional change",
35 examples=["Add dominant 7th chord progression in the bridge — Fm7→Bb7→EbMaj7"],
36 )
37 snapshot_id: str | None = Field(
38 default=None,
39 description="Optional snapshot ID linking this commit to a stored MIDI artifact",
40 )
41 timestamp: datetime = Field(..., description="Commit creation time (ISO-8601 UTC)")
42 # Optional -- falls back to the JWT ``sub`` when absent
43 author: str | None = Field(
44 default=None,
45 description="Commit author identifier; defaults to the JWT sub claim when absent",
46 examples=["composer@stori.com"],
47 )
48
49
50 class ObjectInput(CamelModel):
51 """A binary object transferred in a push payload.
52
53 Content is base64-encoded. For MVP, objects up to ~1 MB are fine; larger
54 files will require pre-signed URL upload in a future release.
55 """
56
57 object_id: str = Field(..., description="Content-addressed ID, e.g. 'sha256:abc...'")
58 path: str = Field(..., description="Relative path hint, e.g. 'tracks/jazz_4b.mid'")
59 content_b64: str = Field(..., description="Base64-encoded binary content")
60
61
62 class PushRequest(CamelModel):
63 """Body for POST /musehub/repos/{repo_id}/push."""
64
65 branch: str = Field(
66 ...,
67 description="Branch name to push to (e.g. 'main', 'feat/jazz-bridge')",
68 examples=["feat/jazz-bridge"],
69 )
70 head_commit_id: str = Field(
71 ...,
72 description="The commit ID that becomes the new branch head after push",
73 examples=["a3f8c1d2e4b5"],
74 )
75 commits: list[CommitInput] = Field(default_factory=list, description="New commits to push")
76 objects: list[ObjectInput] = Field(default_factory=list, description="Binary artifacts to upload")
77 # Set true to allow non-fast-forward updates (overwrites remote head)
78 force: bool = Field(False, description="Allow non-fast-forward push (overwrites remote head)")
79
80
81 class PushResponse(CamelModel):
82 """Response for POST /musehub/repos/{repo_id}/push."""
83
84 ok: bool = Field(..., description="True when the push succeeded", examples=[True])
85 remote_head: str = Field(
86 ...,
87 description="The new branch head commit ID on the remote after push",
88 examples=["a3f8c1d2e4b5"],
89 )
90
91
92 class PullRequest(CamelModel):
93 """Body for POST /musehub/repos/{repo_id}/pull."""
94
95 branch: str
96 # Commit IDs the client already has -- missing ones will be returned
97 have_commits: list[str] = Field(default_factory=list)
98 # Object IDs the client already has -- missing ones will be returned
99 have_objects: list[str] = Field(default_factory=list)
100
101
102 class ObjectResponse(CamelModel):
103 """A binary object returned in a pull response."""
104
105 object_id: str
106 path: str
107 content_b64: str
108
109
110 class PullResponse(CamelModel):
111 """Response for POST /musehub/repos/{repo_id}/pull."""
112
113 commits: list[CommitResponse]
114 objects: list[ObjectResponse]
115 remote_head: str | None
116
117
118 # ── Request models ────────────────────────────────────────────────────────────
119
120
121 class CreateRepoRequest(CamelModel):
122 """Body for POST /musehub/repos — creation wizard.
123
124 ``owner`` is the URL-visible username that appears in /{owner}/{slug} paths.
125 ``slug`` is auto-generated from ``name`` — lowercase, hyphens, 1–64 chars.
126
127 Wizard fields:
128 - ``initialize``: when True, an empty "Initial commit" + default branch are
129 created immediately so the repo is browsable right away.
130 - ``default_branch``: branch name used when ``initialize=True``.
131 - ``template_repo_id``: if set, topics/description are copied from that
132 public repo before creation.
133 - ``license``: SPDX identifier or common shorthand (e.g. "CC BY 4.0").
134 - ``topics``: genre/mood labels analogous to GitHub topics; merged with
135 ``tags`` into a single tag list on the server.
136 """
137
138 name: str = Field(..., min_length=1, max_length=255, description="Repo name")
139 owner: str = Field(
140 ...,
141 min_length=1,
142 max_length=64,
143 pattern=r"^[a-z0-9]([a-z0-9\-]{0,62}[a-z0-9])?$",
144 description="URL-safe owner username (lowercase alphanumeric + hyphens, no leading/trailing hyphens)",
145 )
146 visibility: str = Field("private", pattern="^(public|private)$")
147 description: str = Field("", description="Short description shown on the explore page")
148 tags: list[str] = Field(
149 default_factory=list,
150 description="Free-form tags -- genre, key, instrumentation (e.g. 'jazz', 'F# minor', 'bass')",
151 )
152 key_signature: str | None = Field(None, max_length=50, description="Musical key (e.g. 'C major', 'F# minor')")
153 tempo_bpm: int | None = Field(None, ge=20, le=300, description="Tempo in BPM")
154 # ── Wizard extensions ────────────────────────────────────────
155 license: str | None = Field(None, max_length=100, description="License identifier (e.g. 'CC BY 4.0', 'MIT')")
156 topics: list[str] = Field(
157 default_factory=list,
158 description="Genre/mood topic labels merged with tags (e.g. 'classical', 'piano')",
159 )
160 initialize: bool = Field(
161 True,
162 description="When true, create an initial empty commit + default branch so the repo is immediately browsable",
163 )
164 default_branch: str = Field(
165 "main",
166 min_length=1,
167 max_length=255,
168 description="Name of the default branch created when initialize=true",
169 )
170 template_repo_id: str | None = Field(
171 None,
172 description="UUID of a public repo to copy topics/description/labels from; must be public",
173 )
174
175
176 # ── Response models ───────────────────────────────────────────────────────────
177
178
179 class RepoResponse(CamelModel):
180 """Wire representation of a Muse Hub repo.
181
182 ``owner`` and ``slug`` together form the canonical /{owner}/{slug} URL scheme.
183 ``repo_id`` is the internal UUID primary key — never exposed in external URLs.
184 """
185
186 repo_id: str = Field(..., description="Internal UUID primary key for this repo", examples=["e3b0c44298fc"])
187 name: str = Field(..., description="Human-readable repo name", examples=["jazz-standards-2024"])
188 owner: str = Field(..., description="URL-visible owner username", examples=["miles_davis"])
189 slug: str = Field(..., description="URL-safe slug auto-generated from name", examples=["jazz-standards-2024"])
190 visibility: str = Field(..., description="'public' or 'private'", examples=["public"])
191 owner_user_id: str = Field(..., description="UUID of the owning user account")
192 clone_url: str = Field(..., description="URL used by the CLI for push/pull", examples=["https://musehub.stori.com/api/v1/musehub/repos/e3b0c44298fc"])
193 description: str = Field("", description="Short description shown on the explore page", examples=["Classic jazz standards arranged for quartet"])
194 tags: list[str] = Field(default_factory=list, description="Free-form tags (genre, key, instrumentation)", examples=[["jazz", "F# minor", "bass"]])
195 key_signature: str | None = Field(None, description="Musical key (e.g. 'C major', 'F# minor')", examples=["F# minor"])
196 tempo_bpm: int | None = Field(None, description="Tempo in BPM", examples=[120])
197 created_at: datetime = Field(..., description="Repo creation timestamp (ISO-8601 UTC)")
198
199
200 class TransferOwnershipRequest(CamelModel):
201 """Request body for transferring repo ownership to another user."""
202
203 new_owner_user_id: str = Field(
204 ..., description="User ID of the new repo owner", examples=["a1b2c3d4-e5f6-7890-abcd-ef1234567890"]
205 )
206
207
208 class RepoListResponse(CamelModel):
209 """Paginated list of repos for the authenticated user.
210
211 Covers repos they own plus repos they collaborate on. The ``next_cursor``
212 opaque string is passed back as ``?cursor=`` to retrieve the next page;
213 a null value means there are no more results.
214 """
215
216 repos: list[RepoResponse] = Field(..., description="Repos on this page (up to 20)")
217 next_cursor: str | None = Field(None, description="Pagination cursor — pass as ?cursor= to get the next page")
218 total: int = Field(..., description="Total number of repos across all pages")
219
220
221 class BranchResponse(CamelModel):
222 """Wire representation of a branch pointer."""
223
224 branch_id: str = Field(..., description="Internal UUID for this branch")
225 name: str = Field(..., description="Branch name", examples=["main", "feat/jazz-bridge"])
226 head_commit_id: str | None = Field(None, description="HEAD commit ID; null for an empty branch", examples=["a3f8c1d2e4b5"])
227
228
229 class CommitResponse(CamelModel):
230 """Wire representation of a pushed commit."""
231
232 commit_id: str = Field(..., description="Content-addressed commit ID", examples=["a3f8c1d2e4b5"])
233 branch: str = Field(..., description="Branch this commit was pushed to", examples=["main"])
234 parent_ids: list[str] = Field(..., description="Parent commit IDs", examples=[["b2a7d9e1c3f4"]])
235 message: str = Field(
236 ...,
237 description="Musical commit message",
238 examples=["Increase tempo from 120→132 BPM in the chorus for more energy"],
239 )
240 author: str = Field(..., description="Commit author identifier", examples=["composer@stori.com"])
241 timestamp: datetime = Field(..., description="Commit creation time (ISO-8601 UTC)")
242 snapshot_id: str | None = Field(default=None, description="Optional snapshot artifact ID")
243
244
245 class BranchListResponse(CamelModel):
246 """Paginated list of branches."""
247
248 branches: list[BranchResponse]
249
250
251 class BranchDivergenceScores(CamelModel):
252 """Placeholder musical divergence scores between a branch and the default branch.
253
254 These five dimensions mirror the ``muse divergence`` command output. Values
255 are floats in [0.0, 1.0] where 0 = identical and 1 = maximally different.
256 All fields are ``None`` when divergence cannot yet be computed server-side
257 (e.g. no audio snapshots attached to commits).
258 """
259
260 melodic: float | None = Field(None, description="Melodic divergence (0–1)")
261 harmonic: float | None = Field(None, description="Harmonic divergence (0–1)")
262 rhythmic: float | None = Field(None, description="Rhythmic divergence (0–1)")
263 structural: float | None = Field(None, description="Structural divergence (0–1)")
264 dynamic: float | None = Field(None, description="Dynamic divergence (0–1)")
265
266
267 class BranchDetailResponse(CamelModel):
268 """Branch pointer enriched with ahead/behind counts and musical divergence.
269
270 Used by the branch list page (``GET /{owner}/{repo}/branches``) to give
271 musicians a quick overview of how each branch relates to the default branch.
272 """
273
274 branch_id: str = Field(..., description="Internal UUID for this branch")
275 name: str = Field(..., description="Branch name", examples=["main", "feat/jazz-bridge"])
276 head_commit_id: str | None = Field(None, description="HEAD commit ID; null for an empty branch")
277 is_default: bool = Field(False, description="True when this is the repo's default branch")
278 ahead_count: int = Field(0, ge=0, description="Commits on this branch not yet on the default branch")
279 behind_count: int = Field(0, ge=0, description="Commits on the default branch not yet on this branch")
280 divergence: BranchDivergenceScores = Field(
281 default_factory=lambda: BranchDivergenceScores(
282 melodic=None, harmonic=None, rhythmic=None, structural=None, dynamic=None
283 ),
284 description="Musical divergence scores vs the default branch (placeholder until computable)",
285 )
286
287
288 class BranchDetailListResponse(CamelModel):
289 """List of branches with detail — used by the branch list page and its JSON variant."""
290
291 branches: list[BranchDetailResponse]
292 default_branch: str = Field("main", description="Name of the repo's default branch")
293
294
295 class TagResponse(CamelModel):
296 """A single tag entry for the tag browser page.
297
298 Tags are sourced from ``musehub_releases``. The ``namespace`` field is
299 derived from the tag name: ``emotion:happy`` → namespace ``emotion``,
300 ``v1.0`` → namespace ``version``.
301 """
302
303 tag: str = Field(..., description="Full tag string (e.g. 'emotion:happy', 'v1.0')")
304 namespace: str = Field(..., description="Namespace prefix (e.g. 'emotion', 'genre', 'version')")
305 commit_id: str | None = Field(None, description="Commit this tag is pinned to")
306 message: str = Field("", description="Release title / description")
307 created_at: datetime = Field(..., description="Tag creation timestamp (ISO-8601 UTC)")
308
309
310 class TagListResponse(CamelModel):
311 """All tags for a repo, grouped by namespace.
312
313 ``namespaces`` is an ordered list of distinct namespace strings present in
314 the repo. ``tags`` is the flat list; clients should filter/group client-side
315 using the ``namespace`` field.
316 """
317
318 tags: list[TagResponse]
319 namespaces: list[str] = Field(default_factory=list, description="Distinct namespaces present in this repo")
320
321
322 class CommitListResponse(CamelModel):
323 """Paginated list of commits (newest first)."""
324
325 commits: list[CommitResponse]
326 total: int
327
328
329 class RepoStatsResponse(CamelModel):
330 """Aggregated counts for the repo home page stats bar.
331
332 Returned by ``GET /api/v1/musehub/repos/{repo_id}/stats``.
333 All counts are non-negative integers; 0 when the repo has no data yet.
334 """
335
336 commit_count: int = Field(0, ge=0, description="Total number of commits across all branches")
337 branch_count: int = Field(0, ge=0, description="Number of branches (including default)")
338 release_count: int = Field(0, ge=0, description="Number of published releases / tags")
339
340
341 # ── Issue models ───────────────────────────────────────────────────────────────
342
343
344 class IssueCreate(CamelModel):
345 """Body for POST /musehub/repos/{repo_id}/issues."""
346
347 title: str = Field(
348 ...,
349 min_length=1,
350 max_length=500,
351 description="Issue title",
352 examples=["Verse chord progression feels unresolved — needs perfect cadence at bar 16"],
353 )
354 body: str = Field(
355 "",
356 description="Issue description (Markdown)",
357 examples=["The Dm→Am→E7→Am progression in the verse doesn't resolve — suggest Dm→G7→CMaj7."],
358 )
359 labels: list[str] = Field(
360 default_factory=list,
361 description="Free-form label strings",
362 examples=[["harmony", "needs-review"]],
363 )
364
365
366 class IssueUpdate(CamelModel):
367 """Body for PATCH /musehub/repos/{repo_id}/issues/{number} — partial update.
368
369 All fields are optional; only non-None fields are applied.
370 """
371
372 title: str | None = Field(None, min_length=1, max_length=500, description="Updated issue title")
373 body: str | None = Field(None, description="Updated issue body (Markdown)")
374 labels: list[str] | None = Field(None, description="Replacement label list")
375
376
377 class IssueResponse(CamelModel):
378 """Wire representation of a Muse Hub issue."""
379
380 issue_id: str = Field(..., description="Internal UUID for this issue")
381 number: int = Field(..., description="Per-repo sequential issue number", examples=[42])
382 title: str = Field(..., description="Issue title", examples=["Verse chord progression feels unresolved"])
383 body: str = Field(..., description="Issue description (Markdown)")
384 state: str = Field(..., description="'open' or 'closed'", examples=["open"])
385 labels: list[str] = Field(..., description="Labels attached to this issue", examples=[["harmony"]])
386 author: str = ""
387 # Collaborator assigned to resolve this issue; null when unassigned
388 assignee: str | None = Field(None, description="Display name of the assigned collaborator")
389 # Milestone this issue belongs to; null when not assigned to a milestone
390 milestone_id: str | None = Field(None, description="Milestone UUID; null when not assigned")
391 milestone_title: str | None = Field(None, description="Milestone title for display; null when not assigned")
392 created_at: datetime = Field(..., description="Issue creation timestamp (ISO-8601 UTC)")
393 updated_at: datetime | None = Field(None, description="Last update timestamp (ISO-8601 UTC)")
394 comment_count: int = Field(0, description="Number of non-deleted comments on this issue")
395
396
397 class IssueListResponse(CamelModel):
398 """Paginated list of issues for a repo.
399
400 ``total`` reflects the total number of matching issues before pagination.
401 Clients should use the RFC 8288 ``Link`` response header to navigate pages.
402 """
403
404 issues: list[IssueResponse]
405 total: int = Field(0, ge=0, description="Total matching issues across all pages")
406
407
408 # ── Musical context reference models ──────────────────────────────────────────
409
410
411 class MusicalRef(CamelModel):
412 """A parsed musical context reference extracted from a comment body.
413
414 Examples from user text:
415 - ``track:bass`` → type="track", value="bass"
416 - ``section:chorus`` → type="section", value="chorus"
417 - ``beats:16-24`` → type="beats", value="16-24"
418
419 These are parsed at write time and stored alongside the comment so that
420 the UI can render them as clickable links without re-parsing on every read.
421 """
422
423 type: str = Field(..., description="Reference type: 'track' | 'section' | 'beats'")
424 value: str = Field(..., description="The referenced value, e.g. 'bass', 'chorus', '16-24'")
425 raw: str = Field(..., description="Original raw token from the comment body, e.g. 'track:bass'")
426
427
428 # ── Issue comment models ───────────────────────────────────────────────────────
429
430
431 class IssueCommentCreate(CamelModel):
432 """Body for POST /musehub/repos/{repo_id}/issues/{number}/comments."""
433
434 body: str = Field(
435 ...,
436 min_length=1,
437 description="Comment body (Markdown). Use track:bass, section:chorus, beats:16-24 for musical refs.",
438 examples=["The bass in section:chorus beats:16-24 clashes with the chord progression."],
439 )
440 parent_id: str | None = Field(
441 None,
442 description="Parent comment UUID for threaded replies; omit for top-level comments",
443 )
444
445
446 class IssueCommentResponse(CamelModel):
447 """Wire representation of a single issue comment."""
448
449 comment_id: str = Field(..., description="Internal UUID for this comment")
450 issue_id: str = Field(..., description="UUID of the issue this comment belongs to")
451 author: str = Field(..., description="Display name of the comment author")
452 body: str = Field(..., description="Comment body (Markdown)")
453 parent_id: str | None = Field(None, description="Parent comment UUID; null for top-level comments")
454 musical_refs: list[MusicalRef] = Field(
455 default_factory=list,
456 description="Parsed musical context references extracted from the comment body",
457 )
458 is_deleted: bool = Field(False, description="True when the comment has been soft-deleted")
459 created_at: datetime = Field(..., description="Comment creation timestamp (ISO-8601 UTC)")
460 updated_at: datetime = Field(..., description="Last edit timestamp (ISO-8601 UTC)")
461
462
463 class IssueCommentListResponse(CamelModel):
464 """Threaded discussion on a single issue.
465
466 Comments are returned in chronological order (oldest first). Top-level
467 comments have ``parent_id=None``; replies reference their parent via
468 ``parent_id``. Clients build the thread tree client-side.
469 """
470
471 comments: list[IssueCommentResponse]
472 total: int
473
474
475 # ── Milestone models ────────────────────────────────────────────────────────────
476
477
478 class MilestoneCreate(CamelModel):
479 """Body for POST /musehub/repos/{repo_id}/milestones."""
480
481 title: str = Field(
482 ...,
483 min_length=1,
484 max_length=255,
485 description="Milestone title",
486 examples=["Album v1.0", "Mix Revision 2"],
487 )
488 description: str = Field(
489 "",
490 description="Milestone description (Markdown)",
491 examples=["All tracks balanced and mastered for the first release cut."],
492 )
493 due_on: datetime | None = Field(None, description="Optional due date (ISO-8601 UTC)")
494
495
496 class MilestoneResponse(CamelModel):
497 """Wire representation of a Muse Hub milestone."""
498
499 milestone_id: str = Field(..., description="Internal UUID for this milestone")
500 number: int = Field(..., description="Per-repo sequential milestone number", examples=[1])
501 title: str = Field(..., description="Milestone title", examples=["Album v1.0"])
502 description: str = Field("", description="Milestone description (Markdown)")
503 state: str = Field(..., description="'open' or 'closed'", examples=["open"])
504 author: str = ""
505 due_on: datetime | None = Field(None, description="Optional due date; null when not set")
506 open_issues: int = Field(0, description="Number of open issues assigned to this milestone")
507 closed_issues: int = Field(0, description="Number of closed issues assigned to this milestone")
508 created_at: datetime = Field(..., description="Milestone creation timestamp (ISO-8601 UTC)")
509
510
511 class MilestoneListResponse(CamelModel):
512 """List of milestones for a repo."""
513
514 milestones: list[MilestoneResponse]
515
516
517 # ── Issue assignee models ─────────────────────────────────────────────────────
518
519
520 class IssueAssignRequest(CamelModel):
521 """Body for POST /musehub/repos/{repo_id}/issues/{number}/assign."""
522
523 assignee: str | None = Field(
524 None,
525 description="Display name or user ID to assign; null to unassign",
526 examples=["miles_davis"],
527 )
528
529
530 class IssueLabelAssignRequest(CamelModel):
531 """Body for POST /musehub/repos/{repo_id}/issues/{number}/labels.
532
533 Replaces the entire label list on the issue. To append labels, fetch the
534 current list first, merge client-side, and post the merged result.
535 """
536
537 labels: list[str] = Field(
538 ...,
539 description="Replacement label list for the issue",
540 examples=[["harmony", "needs-review"]],
541 )
542
543
544 # ── Pull request models ────────────────────────────────────────────────────────
545
546
547 class PRCreate(CamelModel):
548 """Body for POST /musehub/repos/{repo_id}/pull-requests."""
549
550 title: str = Field(
551 ...,
552 min_length=1,
553 max_length=500,
554 description="PR title",
555 examples=["Add bossa nova bridge section with 5/4 time signature"],
556 )
557 from_branch: str = Field(
558 ...,
559 min_length=1,
560 max_length=255,
561 description="Source branch name",
562 examples=["feat/bossa-nova-bridge"],
563 )
564 to_branch: str = Field(
565 ...,
566 min_length=1,
567 max_length=255,
568 description="Target branch name",
569 examples=["main"],
570 )
571 body: str = Field(
572 "",
573 description="PR description (Markdown)",
574 examples=["This branch adds an 8-bar bossa nova bridge in 5/4 with guitar and upright bass."],
575 )
576
577
578 class PRResponse(CamelModel):
579 """Wire representation of a Muse Hub pull request."""
580
581 pr_id: str = Field(..., description="Internal UUID for this pull request")
582 title: str = Field(..., description="PR title", examples=["Add bossa nova bridge section"])
583 body: str = Field(..., description="PR description (Markdown)")
584 state: str = Field(..., description="'open', 'merged', or 'closed'", examples=["open"])
585 from_branch: str = Field(..., description="Source branch name", examples=["feat/bossa-nova-bridge"])
586 to_branch: str = Field(..., description="Target branch name", examples=["main"])
587 merge_commit_id: str | None = Field(default=None, description="Merge commit ID; only set after merge")
588 merged_at: datetime | None = Field(default=None, description="UTC timestamp when the PR was merged; None while open or closed")
589 author: str = ""
590 created_at: datetime = Field(..., description="PR creation timestamp (ISO-8601 UTC)")
591
592
593 class PRListResponse(CamelModel):
594 """Paginated list of pull requests for a repo.
595
596 ``total`` reflects the total number of matching PRs before pagination.
597 Clients should use the RFC 8288 ``Link`` response header to navigate pages.
598 """
599
600 pull_requests: list[PRResponse]
601 total: int = Field(0, ge=0, description="Total matching pull requests across all pages")
602
603
604 class PRMergeRequest(CamelModel):
605 """Body for POST /musehub/repos/{repo_id}/pull-requests/{pr_id}/merge."""
606
607 merge_strategy: str = Field(
608 "merge_commit",
609 pattern="^(merge_commit|squash|rebase)$",
610 description="Merge strategy: 'merge_commit' (default), 'squash', or 'rebase'",
611 )
612
613
614 class PRDiffDimensionScore(CamelModel):
615 """Per-dimension musical change score between the from_branch and to_branch of a PR.
616
617 Used by agents to determine which musical dimensions changed most significantly
618 in a PR before deciding whether to approve or request changes.
619 Scores are Jaccard divergence in [0.0, 1.0]: 0 = identical, 1 = completely different.
620 """
621
622 dimension: str = Field(
623 ...,
624 description="Musical dimension: harmonic | rhythmic | melodic | structural | dynamic",
625 examples=["harmonic"],
626 )
627 score: float = Field(..., ge=0.0, le=1.0, description="Divergence magnitude [0.0, 1.0]")
628 level: str = Field(..., description="Human-readable level: NONE | LOW | MED | HIGH")
629 delta_label: str = Field(
630 ...,
631 description="Formatted delta label for diff badge, e.g. '+2.3' or 'unchanged'",
632 )
633 description: str = Field(..., description="Human-readable summary of what changed in this dimension")
634 from_branch_commits: int = Field(..., description="Commits in from_branch touching this dimension")
635 to_branch_commits: int = Field(..., description="Commits in to_branch touching this dimension")
636
637
638 class PRDiffResponse(CamelModel):
639 """Musical diff between the from_branch and to_branch of a pull request.
640
641 Returned by ``GET /api/v1/musehub/repos/{repo_id}/pull-requests/{pr_id}/diff``.
642 Consumed by the PR detail page to render the radar chart, piano roll diff,
643 audio A/B toggle, and dimension badges. Also consumed by AI agents to
644 reason about musical impact before merging.
645
646 ``overall_score`` is in [0.0, 1.0]; multiply by 100 for a percentage.
647 ``common_ancestor`` is the merge-base commit ID, or None if histories diverged.
648 """
649
650 pr_id: str = Field(..., description="The pull request being inspected")
651 repo_id: str = Field(..., description="The repository containing the PR")
652 from_branch: str = Field(..., description="Source branch name")
653 to_branch: str = Field(..., description="Target branch name")
654 dimensions: list[PRDiffDimensionScore] = Field(
655 ..., description="Per-dimension divergence scores (always five entries)"
656 )
657 overall_score: float = Field(..., ge=0.0, le=1.0, description="Mean of all five dimension scores")
658 common_ancestor: str | None = Field(
659 None, description="Merge-base commit ID; None if no common ancestor"
660 )
661 affected_sections: list[str] = Field(
662 default_factory=list,
663 description="List of section/track names that changed (derived from commit messages)",
664 )
665
666
667 class PRMergeResponse(CamelModel):
668 """Confirmation that a PR was merged."""
669
670 merged: bool = Field(..., description="True when the merge succeeded", examples=[True])
671 merge_commit_id: str = Field(..., description="The new merge commit ID", examples=["c9d8e7f6a5b4"])
672
673
674 # ── PR review comment models ───────────────────────────────────────────────────
675
676
677 class PRCommentCreate(CamelModel):
678 """Body for POST /musehub/repos/{repo_id}/pull-requests/{pr_id}/comments.
679
680 ``target_type`` selects the granularity of the musical annotation:
681 - ``general`` — whole PR, no positional context
682 - ``track`` — a named instrument track (supply ``target_track``)
683 - ``region`` — beat range within a track (supply track + beat_start/end)
684 - ``note`` — single note event (supply track + beat_start + note_pitch)
685
686 ``body`` supports Markdown so reviewers can format code-fence chord charts,
687 lists of suggested edits, etc.
688 """
689
690 body: str = Field(
691 ...,
692 min_length=1,
693 description="Review comment body (Markdown)",
694 examples=["The bass line in beats 16-24 feels rhythmically stiff — try adding some swing."],
695 )
696 target_type: str = Field(
697 "general",
698 pattern="^(general|track|region|note)$",
699 description="Comment target granularity",
700 examples=["region"],
701 )
702 target_track: str | None = Field(
703 None,
704 max_length=255,
705 description="Instrument track name for track/region/note targets",
706 examples=["bass"],
707 )
708 target_beat_start: float | None = Field(
709 None,
710 ge=0,
711 description="First beat of the targeted region (inclusive)",
712 examples=[16.0],
713 )
714 target_beat_end: float | None = Field(
715 None,
716 ge=0,
717 description="Last beat of the targeted region (exclusive)",
718 examples=[24.0],
719 )
720 target_note_pitch: int | None = Field(
721 None,
722 ge=0,
723 le=127,
724 description="MIDI pitch (0-127) for note-level targets",
725 examples=[46],
726 )
727 parent_comment_id: str | None = Field(
728 None,
729 description="ID of the parent comment when creating a threaded reply",
730 examples=["a1b2c3d4-e5f6-7890-abcd-ef1234567890"],
731 )
732
733
734 class PRCommentResponse(CamelModel):
735 """Wire representation of a single PR review comment."""
736
737 comment_id: str = Field(..., description="Internal UUID for this comment")
738 pr_id: str = Field(..., description="Pull request this comment belongs to")
739 author: str = Field(..., description="Display name / JWT sub of the comment author")
740 body: str = Field(..., description="Review body (Markdown)")
741 target_type: str = Field(..., description="'general', 'track', 'region', or 'note'")
742 target_track: str | None = Field(None, description="Instrument track name when targeted")
743 target_beat_start: float | None = Field(None, description="Region start beat (inclusive)")
744 target_beat_end: float | None = Field(None, description="Region end beat (exclusive)")
745 target_note_pitch: int | None = Field(None, description="MIDI pitch for note-level targets")
746 parent_comment_id: str | None = Field(None, description="Parent comment ID for threaded replies")
747 created_at: datetime = Field(..., description="Comment creation timestamp (ISO-8601 UTC)")
748 replies: list[PRCommentResponse] = Field(
749 default_factory=list,
750 description="Nested replies to this comment (only populated on top-level comments)",
751 )
752
753
754 class PRCommentListResponse(CamelModel):
755 """Threaded list of review comments for a PR.
756
757 ``comments`` contains only top-level comments; each carries a ``replies``
758 list with its direct children, sorted chronologically. This two-level
759 structure covers all current threading requirements without recursive fetches.
760 """
761
762 comments: list[PRCommentResponse] = Field(
763 default_factory=list,
764 description="Top-level review comments with nested replies",
765 )
766 total: int = Field(0, ge=0, description="Total number of comments (all levels)")
767
768
769 # Rebuild the model to resolve the forward reference in PRCommentResponse.replies
770 PRCommentResponse.model_rebuild()
771
772
773 # ── PR reviewer / review models ───────────────────────────────────────────────
774
775
776 class PRReviewerRequest(CamelModel):
777 """Body for POST /musehub/repos/{repo_id}/pull-requests/{pr_id}/reviewers.
778
779 Requests a review from one or more users. Each username is added as a
780 ``pending`` review row. Duplicate requests for the same reviewer are
781 idempotent — the state is not reset if the reviewer already submitted.
782 """
783
784 reviewers: list[str] = Field(
785 ...,
786 min_length=1,
787 description="List of usernames to request reviews from",
788 examples=[["alice", "bob"]],
789 )
790
791
792 class PRReviewResponse(CamelModel):
793 """Wire representation of a single PR review.
794
795 ``state`` reflects the current disposition of the reviewer:
796 - ``pending`` — review requested, not yet submitted
797 - ``approved`` — reviewer approved the changes
798 - ``changes_requested`` — reviewer blocked the merge pending fixes
799 - ``dismissed`` — a previous review was dismissed by the PR author
800
801 ``submitted_at`` is ``None`` while the review is in ``pending`` state.
802 """
803
804 id: str = Field(..., description="Internal UUID for this review row")
805 pr_id: str = Field(..., description="Pull request this review belongs to")
806 reviewer_username: str = Field(..., description="Username of the reviewer")
807 state: str = Field(
808 ...,
809 description="Review state: pending | approved | changes_requested | dismissed",
810 examples=["approved"],
811 )
812 body: str | None = Field(None, description="Review comment body (Markdown); null for bare assignments")
813 submitted_at: datetime | None = Field(None, description="UTC timestamp when the review was submitted")
814 created_at: datetime = Field(..., description="Row creation timestamp (ISO-8601 UTC)")
815
816
817 class PRReviewListResponse(CamelModel):
818 """List of reviews for a pull request.
819
820 Used by the PR detail page review panel and by AI agents evaluating
821 merge readiness. Includes both pending assignments and submitted reviews.
822 """
823
824 reviews: list[PRReviewResponse] = Field(
825 default_factory=list,
826 description="All review rows for this PR (pending and submitted)",
827 )
828 total: int = Field(0, ge=0, description="Total number of review rows")
829
830
831 class PRReviewCreate(CamelModel):
832 """Body for POST /musehub/repos/{repo_id}/pull-requests/{pr_id}/reviews.
833
834 Submits a formal review for the authenticated user. If the user was
835 previously assigned as a reviewer, the existing ``pending`` row is updated
836 in-place. If no prior row exists, a new one is created.
837
838 ``event`` governs the new review state:
839 - ``approve`` → state = approved
840 - ``request_changes`` → state = changes_requested
841 - ``comment`` → state = pending (body-only feedback, no verdict)
842 """
843
844 event: str = Field(
845 ...,
846 pattern="^(approve|request_changes|comment)$",
847 description="Review event: approve | request_changes | comment",
848 examples=["approve"],
849 )
850 body: str = Field(
851 "",
852 description="Review body (Markdown). Required when event='request_changes'.",
853 examples=["Sounds great — the harmonic transitions in the bridge are exactly right."],
854 )
855
856
857 # ── Release models ────────────────────────────────────────────────────────────
858
859
860 class ReleaseCreate(CamelModel):
861 """Body for POST /musehub/repos/{repo_id}/releases.
862
863 ``tag`` must be unique per repo (e.g. "v1.0", "v2.3.1").
864 ``commit_id`` pins the release to a specific commit snapshot.
865 """
866
867 tag: str = Field(
868 ..., min_length=1, max_length=100, description="Version tag, e.g. 'v1.0'", examples=["v1.0"]
869 )
870 title: str = Field(
871 ..., min_length=1, max_length=500, description="Release title", examples=["Summer Sessions 2024 — Final Mix"]
872 )
873 body: str = Field(
874 "",
875 description="Release notes (Markdown)",
876 examples=["## Summer Sessions 2024\n\nFinal arrangement with full brass section and 132 BPM tempo."],
877 )
878 commit_id: str | None = Field(
879 None, description="Commit to pin this release to", examples=["a3f8c1d2e4b5"]
880 )
881 is_prerelease: bool = Field(False, description="Mark as a pre-release (beta, rc, alpha)")
882 is_draft: bool = Field(False, description="Save as draft — not yet publicly visible")
883 gpg_signature: str | None = Field(
884 None,
885 description="ASCII-armoured GPG signature for the tag object; omit when unsigned",
886 )
887
888
889 class ReleaseDownloadUrls(CamelModel):
890 """Structured download package URLs for a release.
891
892 Each field is either a URL string or None if the package is not available.
893 ``midi_bundle`` is the full MIDI export (all tracks as a single .mid).
894 ``stems`` is a zip of per-track MIDI stems.
895 ``mp3`` is the full mix audio render.
896 ``musicxml`` is the notation export in MusicXML format.
897 ``metadata`` is a JSON file with tempo, key, and arrangement info.
898 """
899
900 midi_bundle: str | None = None
901 stems: str | None = None
902 mp3: str | None = None
903 musicxml: str | None = None
904 metadata: str | None = None
905
906
907 class ReleaseResponse(CamelModel):
908 """Wire representation of a Muse Hub release.
909
910 is_prerelease and is_draft drive the UI badges on the release detail page.
911 gpg_signature is None when the tag was not GPG-signed; a non-empty string
912 indicates the release carries a verifiable signature and the UI renders a verified badge.
913 """
914
915 release_id: str
916 tag: str
917 title: str
918 body: str
919 commit_id: str | None = None
920 download_urls: ReleaseDownloadUrls
921 author: str = ""
922 is_prerelease: bool = False
923 is_draft: bool = False
924 gpg_signature: str | None = None
925 created_at: datetime
926
927
928 class ReleaseListResponse(CamelModel):
929 """List of releases for a repo (newest first)."""
930
931 releases: list[ReleaseResponse]
932
933
934 # ── Release asset models ───────────────────────────────────────────────────
935
936
937 class ReleaseAssetCreate(CamelModel):
938 """Body for POST /musehub/repos/{repo_id}/releases/{tag}/assets.
939
940 ``name`` is the filename shown in the UI (e.g. "summer-v1.0.mid").
941 ``download_url`` is the pre-signed or CDN URL from which clients
942 download the artifact; Maestro stores it verbatim.
943 """
944
945 name: str = Field(
946 ..., min_length=1, max_length=500, description="Filename shown in the UI"
947 )
948 label: str = Field(
949 "",
950 max_length=255,
951 description="Optional human-readable label, e.g. 'MIDI Bundle'",
952 )
953 content_type: str = Field(
954 "",
955 max_length=128,
956 description="MIME type, e.g. 'audio/midi', 'application/zip'",
957 )
958 size: int = Field(
959 0, ge=0, description="File size in bytes; 0 when unknown"
960 )
961 download_url: str = Field(
962 ..., min_length=1, max_length=2048, description="Direct download URL for the artifact"
963 )
964
965
966 class ReleaseAssetResponse(CamelModel):
967 """Wire representation of a single release asset."""
968
969 asset_id: str = Field(..., description="Internal UUID for this asset")
970 release_id: str = Field(..., description="UUID of the owning release")
971 name: str = Field(..., description="Filename shown in the UI")
972 label: str = Field("", description="Optional human-readable label")
973 content_type: str = Field("", description="MIME type of the artifact")
974 size: int = Field(0, ge=0, description="File size in bytes; 0 when unknown")
975 download_url: str = Field(..., description="Direct download URL")
976 download_count: int = Field(0, ge=0, description="Number of times the asset has been downloaded")
977 created_at: datetime = Field(..., description="Asset creation timestamp (ISO-8601 UTC)")
978
979
980 class ReleaseAssetListResponse(CamelModel):
981 """List of assets attached to a release, returned by GET .../releases/{tag}/assets.
982
983 Agents use this to surface per-asset download counts and direct download
984 URLs on the release detail page without re-fetching the full release.
985 """
986
987 release_id: str
988 tag: str
989 assets: list[ReleaseAssetResponse]
990
991
992 class ReleaseAssetDownloadCount(CamelModel):
993 """Per-asset download count entry in a release download stats response."""
994
995 asset_id: str = Field(..., description="Internal UUID for the asset")
996 name: str = Field(..., description="Filename shown in the UI")
997 label: str = Field("", description="Optional human-readable label")
998 download_count: int = Field(0, ge=0, description="Number of times this asset has been downloaded")
999
1000
1001 class ReleaseDownloadStatsResponse(CamelModel):
1002 """Download counts per asset for a single release.
1003
1004 Returned by ``GET /repos/{repo_id}/releases/{tag}/downloads``.
1005 ``total_downloads`` is the sum of ``download_count`` across all assets,
1006 providing a quick headline metric without client-side aggregation.
1007 """
1008
1009 release_id: str = Field(..., description="UUID of the release")
1010 tag: str = Field(..., description="Version tag of the release")
1011 assets: list[ReleaseAssetDownloadCount] = Field(
1012 default_factory=list,
1013 description="Per-asset download counts; empty when no assets have been attached",
1014 )
1015 total_downloads: int = Field(
1016 0, ge=0, description="Sum of download_count across all assets"
1017 )
1018
1019
1020 # ── Credits models ────────────────────────────────────────────────────────────
1021
1022
1023 class ContributorCredits(CamelModel):
1024 """Wire representation of a single contributor's credit record.
1025
1026 Aggregated from commit history -- one record per unique author string.
1027 Contribution types are inferred from commit message keywords so that an
1028 agent or a human can understand each collaborator's role at a glance.
1029 """
1030
1031 author: str
1032 session_count: int
1033 contribution_types: list[str]
1034 first_active: datetime
1035 last_active: datetime
1036
1037
1038 class CreditsResponse(CamelModel):
1039 """Wire representation of the full credits roll for a repo.
1040
1041 Returned by ``GET /api/v1/musehub/repos/{repo_id}/credits``.
1042 The ``sort`` field echoes back the sort order applied to the list.
1043 An empty ``contributors`` list means no commits have been pushed yet.
1044 """
1045
1046 repo_id: str
1047 contributors: list[ContributorCredits]
1048 sort: str
1049 total_contributors: int
1050
1051
1052 # ── Object metadata model ─────────────────────────────────────────────────────
1053
1054
1055 class ObjectMetaResponse(CamelModel):
1056 """Wire representation of a stored artifact -- metadata only, no content bytes.
1057
1058 Returned by GET /musehub/repos/{repo_id}/objects. Use the ``/content``
1059 sub-resource to download the raw bytes. The ``path`` field retains the
1060 client-supplied relative path hint (e.g. "tracks/jazz_4b.mid") and is
1061 the primary signal for choosing display treatment (.webp → img, .mid /
1062 .mp3 → audio/download).
1063 """
1064
1065 object_id: str
1066 path: str
1067 size_bytes: int
1068 created_at: datetime
1069
1070
1071 class ObjectMetaListResponse(CamelModel):
1072 """List of artifact metadata for a repo."""
1073
1074 objects: list[ObjectMetaResponse]
1075
1076
1077 # ── Timeline models ───────────────────────────────────────────────────────────
1078
1079
1080 class TimelineCommitEvent(CamelModel):
1081 """A commit plotted as a point on the timeline.
1082
1083 Every pushed commit becomes a commit event regardless of its message content.
1084 The ``commit_id`` is the canonical identifier for audio-preview lookup and
1085 deep-linking to the commit detail page.
1086 """
1087
1088 event_type: str = "commit"
1089 commit_id: str
1090 branch: str
1091 message: str
1092 author: str
1093 timestamp: datetime
1094 parent_ids: list[str]
1095
1096
1097 class TimelineEmotionEvent(CamelModel):
1098 """An emotion-vector data point overlaid on the timeline as a line chart.
1099
1100 Emotion values are derived deterministically from the commit SHA so the
1101 timeline is always reproducible without external inference. Each field is
1102 in the range [0.0, 1.0]. Agents use these values to understand how the
1103 emotional character of the composition shifted over time.
1104 """
1105
1106 event_type: str = "emotion"
1107 commit_id: str
1108 timestamp: datetime
1109 valence: float
1110 energy: float
1111 tension: float
1112
1113
1114 class TimelineSectionEvent(CamelModel):
1115 """A detected section change plotted as a marker on the timeline.
1116
1117 Section names are extracted from commit messages using keyword heuristics
1118 (e.g. "added chorus", "intro complete", "bridge removed"). The ``action``
1119 field is either ``"added"`` or ``"removed"``.
1120 """
1121
1122 event_type: str = "section"
1123 commit_id: str
1124 timestamp: datetime
1125 section_name: str
1126 action: str
1127
1128
1129 class TimelineTrackEvent(CamelModel):
1130 """A detected track addition or removal plotted as a marker on the timeline.
1131
1132 Track changes are extracted from commit messages using keyword heuristics
1133 (e.g. "added bass", "removed keys", "new drums track"). The ``action``
1134 field is either ``"added"`` or ``"removed"``.
1135 """
1136
1137 event_type: str = "track"
1138 commit_id: str
1139 timestamp: datetime
1140 track_name: str
1141 action: str
1142
1143
1144 class TimelineResponse(CamelModel):
1145 """Chronological timeline of musical evolution for a repo.
1146
1147 Contains four parallel event streams that the client renders as
1148 independently toggleable layers:
1149 - ``commits``: every pushed commit (always present)
1150 - ``emotion``: emotion-vector data points per commit (always present)
1151 - ``sections``: section change events derived from commit messages
1152 - ``tracks``: track add/remove events derived from commit messages
1153
1154 Agent use case: call this endpoint to understand how a project evolved --
1155 when sections were introduced, when the emotional character shifted, and
1156 which instruments were added or removed over time.
1157 """
1158
1159 commits: list[TimelineCommitEvent]
1160 emotion: list[TimelineEmotionEvent]
1161 sections: list[TimelineSectionEvent]
1162 tracks: list[TimelineTrackEvent]
1163 total_commits: int
1164
1165
1166 # ── Divergence visualization models ───────────────────────────────────────────
1167
1168
1169 class DivergenceDimensionResponse(CamelModel):
1170 """Wire representation of divergence scores for a single musical dimension.
1171
1172 Mirrors :class:`maestro.services.musehub_divergence.MuseHubDimensionDivergence`
1173 for JSON serialization. AI agents consume this to decide which dimension
1174 of a branch needs creative attention before merging.
1175 """
1176
1177 dimension: str
1178 level: str
1179 score: float
1180 description: str
1181 branch_a_commits: int
1182 branch_b_commits: int
1183
1184
1185 class DivergenceResponse(CamelModel):
1186 """Full musical divergence report between two Muse Hub branches.
1187
1188 Returned by ``GET /musehub/repos/{repo_id}/divergence``. Contains five
1189 per-dimension scores (melodic, harmonic, rhythmic, structural, dynamic)
1190 and an overall score computed as the mean of those five scores.
1191
1192 The ``overall_score`` is in [0.0, 1.0]; multiply by 100 for a percentage.
1193 A score of 0.0 means identical, 1.0 means completely diverged.
1194 """
1195
1196 repo_id: str
1197 branch_a: str
1198 branch_b: str
1199 common_ancestor: str | None
1200 dimensions: list[DivergenceDimensionResponse]
1201 overall_score: float
1202
1203
1204 # ── Commit diff summary models ─────────────────────────────────────────────────
1205
1206
1207 class CommitDiffDimensionScore(CamelModel):
1208 """Per-dimension change score between a commit and its parent.
1209
1210 Scores are heuristic estimates derived from the commit message and metadata.
1211 They indicate *how much* each musical dimension changed in this commit.
1212 """
1213
1214 dimension: str = Field(
1215 ...,
1216 description="Musical dimension: harmonic | rhythmic | melodic | structural | dynamic",
1217 examples=["harmonic"],
1218 )
1219 score: float = Field(..., ge=0.0, le=1.0, description="Change magnitude [0.0, 1.0]")
1220 label: str = Field(..., description="Human-readable level: none | low | medium | high")
1221 color: str = Field(
1222 ...,
1223 description="CSS class hint for badge colour: dim-none | dim-low | dim-medium | dim-high",
1224 )
1225
1226
1227 class CommitDiffSummaryResponse(CamelModel):
1228 """Multi-dimensional diff summary between a commit and its parent.
1229
1230 Returned by ``GET /api/v1/musehub/repos/{repo_id}/commits/{commit_id}/diff-summary``.
1231 Consumed by the commit detail page to render dimension-change badges that help
1232 musicians understand *what* changed musically between two pushes.
1233 """
1234
1235 commit_id: str = Field(..., description="The commit being inspected")
1236 parent_id: str | None = Field(None, description="Parent commit ID; None for root commits")
1237 dimensions: list[CommitDiffDimensionScore] = Field(
1238 ..., description="Per-dimension change scores (always five entries)"
1239 )
1240 overall_score: float = Field(
1241 ..., ge=0.0, le=1.0, description="Mean across all five dimension scores"
1242 )
1243
1244
1245 # ── Explore / Discover models ──────────────────────────────────────────────────
1246
1247
1248 class ExploreRepoResult(CamelModel):
1249 """A public repo card shown on the explore/discover page.
1250
1251 Extends RepoResponse with aggregated counts (star_count, commit_count)
1252 that are computed at query time for efficient pagination and sorting.
1253 These counts are read-only signals -- they are never persisted directly on
1254 the repo row to avoid write amplification on every push/star.
1255
1256 ``owner`` and ``slug`` together form the /{owner}/{slug} canonical URL.
1257 """
1258
1259 repo_id: str
1260 name: str
1261 owner: str
1262 slug: str
1263 owner_user_id: str
1264 description: str
1265 tags: list[str]
1266 key_signature: str | None
1267 tempo_bpm: int | None
1268 star_count: int
1269 commit_count: int
1270 created_at: datetime
1271
1272
1273 # ── Profile models ────────────────────────────────────────────────────────────
1274
1275
1276 class ProfileUpdateRequest(CamelModel):
1277 """Body for PUT /api/v1/musehub/users/{username}.
1278
1279 All fields are optional -- send only the ones to change.
1280 ``is_verified`` and ``cc_license`` are intentionally excluded: they are
1281 set by the platform (not self-reported) when an archive upload is approved.
1282 """
1283
1284 display_name: str | None = Field(None, max_length=255, description="Human-readable display name")
1285 bio: str | None = Field(None, max_length=500, description="Short bio (Markdown supported)")
1286 avatar_url: str | None = Field(None, max_length=2048, description="Avatar image URL")
1287 location: str | None = Field(None, max_length=255, description="City or region")
1288 website_url: str | None = Field(None, max_length=2048, description="Personal website or project URL")
1289 twitter_handle: str | None = Field(None, max_length=64, description="Twitter/X handle without leading @")
1290 pinned_repo_ids: list[str] | None = Field(
1291 None, max_length=6, description="Up to 6 repo_ids to pin on the profile page"
1292 )
1293
1294
1295 class ProfileRepoSummary(CamelModel):
1296 """Compact repo summary shown on a user's profile page.
1297
1298 Includes the last-activity timestamp derived from the most recent commit
1299 and a stub star_count (always 0 at MVP -- no star mechanism yet).
1300 ``owner`` and ``slug`` form the /{owner}/{slug} canonical URL for the repo card.
1301 """
1302
1303 repo_id: str
1304 name: str
1305 owner: str
1306 slug: str
1307 visibility: str
1308 star_count: int
1309 last_activity_at: datetime | None
1310 created_at: datetime
1311
1312
1313 class ExploreResponse(CamelModel):
1314 """Paginated response from GET /api/v1/musehub/discover/repos.
1315
1316 ``total`` reflects the full filtered result set size -- not just the current
1317 page -- so clients can render pagination controls without a second query.
1318 """
1319
1320 repos: list[ExploreRepoResult]
1321 total: int
1322 page: int
1323 page_size: int
1324
1325
1326 class StarResponse(CamelModel):
1327 """Confirmation that a star was added or removed."""
1328
1329 starred: bool
1330 star_count: int
1331
1332
1333 class ContributionDay(CamelModel):
1334 """A single day in the contribution heatmap.
1335
1336 ``date`` is ISO-8601 (YYYY-MM-DD). ``count`` is the number of commits
1337 authored on that day across all of the user's repos.
1338 """
1339
1340 date: str
1341 count: int
1342
1343
1344 class ProfileResponse(CamelModel):
1345 """Full wire representation of a Muse Hub user profile.
1346
1347 Returned by GET /api/v1/musehub/users/{username}.
1348 ``repos`` contains only public repos when the caller is not the owner.
1349 ``contribution_graph`` is the last 52 weeks of daily commit activity.
1350 ``session_credits`` is the total number of commits across all repos
1351 (a proxy for creative session activity).
1352
1353 CC attribution fields added:
1354 ``is_verified`` is True for Public Domain / Creative Commons artists.
1355 ``cc_license`` is the SPDX-style license string (e.g. "CC BY 4.0") or
1356 None for community users who retain all rights.
1357 """
1358
1359 user_id: str
1360 username: str
1361 display_name: str | None = None
1362 bio: str | None = None
1363 avatar_url: str | None = None
1364 location: str | None = None
1365 website_url: str | None = None
1366 twitter_handle: str | None = None
1367 is_verified: bool = False
1368 cc_license: str | None = None
1369 pinned_repo_ids: list[str]
1370 repos: list[ProfileRepoSummary]
1371 contribution_graph: list[ContributionDay]
1372 session_credits: int
1373 created_at: datetime
1374 updated_at: datetime
1375
1376 # ── Cross-repo search models ───────────────────────────────────────────────────
1377
1378
1379 class GlobalSearchCommitMatch(CamelModel):
1380 """A single commit that matched the search query in a cross-repo search.
1381
1382 Consumers display ``repo_id`` / ``repo_name`` as the group header, then
1383 render ``commit_id``, ``message``, and ``author`` as the match row.
1384 Audio preview is surfaced via ``audio_object_id`` when an .mp3 or .ogg
1385 artifact is attached to the same repo.
1386 """
1387
1388 commit_id: str
1389 message: str
1390 author: str
1391 branch: str
1392 timestamp: datetime
1393 repo_id: str
1394 repo_name: str
1395 repo_owner: str
1396 repo_visibility: str
1397 audio_object_id: str | None = None
1398 # ── Webhook models ────────────────────────────────────────────────────────────
1399
1400 # Valid event types a subscriber may register for.
1401 WEBHOOK_EVENT_TYPES: frozenset[str] = frozenset(
1402 [
1403 "push",
1404 "pull_request",
1405 "issue",
1406 "release",
1407 "branch",
1408 "tag",
1409 "session",
1410 "analysis",
1411 ]
1412 )
1413
1414
1415 class WebhookCreate(CamelModel):
1416 """Body for POST /musehub/repos/{repo_id}/webhooks.
1417
1418 ``events`` must be a non-empty subset of the valid event-type strings
1419 (push, pull_request, issue, release, branch, tag, session, analysis).
1420 ``secret`` is optional; when provided it is used to sign every delivery
1421 with HMAC-SHA256 in the ``X-MuseHub-Signature`` header.
1422 """
1423
1424 url: str = Field(..., min_length=1, max_length=2048, description="HTTPS endpoint to deliver events to")
1425 events: list[str] = Field(..., min_length=1, description="Event types to subscribe to")
1426 secret: str = Field("", description="Optional HMAC-SHA256 signing secret")
1427
1428
1429 class WebhookResponse(CamelModel):
1430 """Wire representation of a registered webhook subscription."""
1431
1432 webhook_id: str
1433 repo_id: str
1434 url: str
1435 events: list[str]
1436 active: bool
1437 created_at: datetime
1438
1439
1440 class WebhookListResponse(CamelModel):
1441 """List of webhook subscriptions for a repo."""
1442
1443 webhooks: list[WebhookResponse]
1444
1445
1446 class WebhookDeliveryResponse(CamelModel):
1447 """Wire representation of a single webhook delivery attempt.
1448
1449 ``payload`` is the JSON body that was (or will be) sent to the subscriber.
1450 It is stored verbatim so that operators can inspect the exact bytes delivered
1451 and so the redeliver endpoint can replay the original payload without guessing.
1452 """
1453
1454 delivery_id: str
1455 webhook_id: str
1456 event_type: str
1457 payload: str = Field("", description="JSON body sent to the subscriber URL")
1458 attempt: int
1459 success: bool
1460 response_status: int
1461 response_body: str
1462 delivered_at: datetime
1463
1464
1465 class WebhookDeliveryListResponse(CamelModel):
1466 """Paginated list of delivery attempts for a webhook."""
1467
1468 deliveries: list[WebhookDeliveryResponse]
1469
1470
1471 class WebhookRedeliverResponse(CamelModel):
1472 """Confirmation that a delivery reattempt was executed.
1473
1474 ``success`` reflects the final outcome after all retry attempts.
1475 ``original_delivery_id`` links back to the delivery row that was replayed.
1476 """
1477
1478 original_delivery_id: str = Field(..., description="ID of the original delivery row that was retried")
1479 webhook_id: str = Field(..., description="Webhook the payload was redelivered to")
1480 event_type: str = Field(..., description="Event type of the redelivered payload")
1481 success: bool = Field(..., description="True when the redeliver attempt received a 2xx response")
1482 response_status: int = Field(..., description="HTTP status code from the final attempt (0 for network errors)")
1483 response_body: str = Field("", description="Response body snippet from the final attempt (≤512 chars)")
1484
1485
1486 # ── Webhook event payload TypedDicts ─────────────────────────────────────────
1487 # These typed dicts are used as the payload argument to dispatch_event /
1488 # dispatch_event_background, replacing dict[str, Any] at the service boundary.
1489
1490
1491 class PushEventPayload(TypedDict):
1492 """Payload emitted when commits are pushed to a MuseHub repo.
1493
1494 Used with event_type="push".
1495 """
1496
1497 repoId: str
1498 branch: str
1499 headCommitId: str
1500 pushedBy: str
1501 commitCount: int
1502
1503
1504 class IssueEventPayload(TypedDict):
1505 """Payload emitted when an issue is opened or closed.
1506
1507 ``action`` is either ``"opened"`` or ``"closed"``.
1508 Used with event_type="issue".
1509 """
1510
1511 repoId: str
1512 action: str
1513 issueId: str
1514 number: int
1515 title: str
1516 state: str
1517
1518
1519 class PullRequestEventPayload(TypedDict):
1520 """Payload emitted when a PR is opened or merged.
1521
1522 ``action`` is either ``"opened"`` or ``"merged"``.
1523 ``mergeCommitId`` is only present on the "merged" action.
1524 Used with event_type="pull_request".
1525 """
1526
1527 repoId: str
1528 action: str
1529 prId: str
1530 title: str
1531 fromBranch: str
1532 toBranch: str
1533 state: str
1534 mergeCommitId: NotRequired[str]
1535
1536
1537 # Union of all typed webhook event payloads. The dispatcher accepts any of
1538 # these; callers pass the specific TypedDict for their event type.
1539 WebhookEventPayload = PushEventPayload | IssueEventPayload | PullRequestEventPayload
1540
1541 # ── Context models ────────────────────────────────────────────────────────────
1542
1543
1544 class MuseHubContextCommitInfo(CamelModel):
1545 """Minimal commit metadata included in a MuseHub context document."""
1546
1547 commit_id: str
1548 message: str
1549 author: str
1550 branch: str
1551 timestamp: datetime
1552
1553
1554 class GlobalSearchRepoGroup(CamelModel):
1555 """All matching commits for a single repo, with repo-level metadata.
1556
1557 Results are grouped by repo so consumers can render a collapsible section
1558 per repo (name, owner) and paginate within each group.
1559
1560 ``repo_owner`` + ``repo_slug`` form the canonical /{owner}/{slug} UI URL.
1561 """
1562
1563 repo_id: str
1564 repo_name: str
1565 repo_owner: str
1566 repo_slug: str
1567 repo_visibility: str
1568 matches: list[GlobalSearchCommitMatch]
1569 total_matches: int
1570
1571
1572 class GlobalSearchResult(CamelModel):
1573 """Top-level response for GET /search?q={query}.
1574
1575 ``groups`` contains one entry per public repo that had at least one
1576 matching commit. ``total_repos`` is the count of repos searched, not just
1577 the repos with matches. ``page`` / ``page_size`` enable offset pagination
1578 across groups.
1579 """
1580
1581 query: str
1582 mode: str
1583 groups: list[GlobalSearchRepoGroup]
1584 total_repos_searched: int
1585 page: int
1586 page_size: int
1587
1588
1589 class MuseHubContextHistoryEntry(CamelModel):
1590 """A single ancestor commit in the evolutionary history of the composition.
1591
1592 History is built by walking parent_ids from the target commit.
1593 Entries are returned newest-first and limited to the last 5 ancestors.
1594 """
1595
1596 commit_id: str
1597 message: str
1598 author: str
1599 timestamp: datetime
1600 active_tracks: list[str]
1601
1602
1603 class MuseHubContextMusicalState(CamelModel):
1604 """Musical state at the target commit, derived from stored artifact paths.
1605
1606 ``active_tracks`` is populated from object paths in the repo.
1607 All analytical fields (key, tempo, etc.) are None until Storpheus MIDI
1608 analysis is integrated -- agents should treat None as "unknown."
1609 """
1610
1611 active_tracks: list[str]
1612 key: str | None = None
1613 mode: str | None = None
1614 tempo_bpm: int | None = None
1615 time_signature: str | None = None
1616 form: str | None = None
1617 emotion: str | None = None
1618
1619
1620 class MuseHubContextResponse(CamelModel):
1621 """Human-readable and agent-consumable musical context document for a commit.
1622
1623 Returned by ``GET /api/v1/musehub/repos/{repo_id}/context/{ref}``.
1624
1625 This is the MuseHub equivalent of ``MuseContextResult`` -- built from
1626 the remote repo's commit graph and stored objects rather than the local
1627 ``.muse`` filesystem. The structure deliberately mirrors ``MuseContextResult``
1628 so that agents consuming either source see the same schema.
1629
1630 Fields:
1631 repo_id: The hub repo identifier.
1632 current_branch: Branch name for the target commit.
1633 head_commit: Metadata for the resolved commit (ref).
1634 musical_state: Active tracks and any available musical dimensions.
1635 history: Up to 5 ancestor commits, newest-first.
1636 missing_elements: Dimensions that could not be determined from stored data.
1637 suggestions: Composer-facing hints about what to work on next.
1638 """
1639
1640 repo_id: str
1641 current_branch: str
1642 head_commit: MuseHubContextCommitInfo
1643 musical_state: MuseHubContextMusicalState
1644 history: list[MuseHubContextHistoryEntry]
1645 missing_elements: list[str]
1646 suggestions: dict[str, str]
1647
1648
1649 # ── In-repo search models ─────────────────────────────────────────────────────
1650
1651
1652 class SearchCommitMatch(CamelModel):
1653 """A single commit returned by a search query.
1654
1655 Carries enough metadata to render a result row and launch an audio preview.
1656 The ``score`` field is populated by keyword/recall modes (0–1 overlap ratio);
1657 property and grep modes always return 1.0.
1658 """
1659
1660 commit_id: str
1661 branch: str
1662 message: str
1663 author: str
1664 timestamp: datetime
1665 score: float = Field(1.0, ge=0.0, le=1.0, description="Match score (0–1); always 1.0 for exact-match modes")
1666 match_source: str = Field("message", description="Where the match was found: 'message', 'branch', or 'property'")
1667
1668
1669 class SearchResponse(CamelModel):
1670 """Response envelope for all four in-repo search modes.
1671
1672 ``mode`` echoes back the requested search mode so clients can render
1673 mode-appropriate headers. ``total_scanned`` is the number of commits
1674 examined before limit was applied; useful for indicating search depth.
1675 """
1676
1677 mode: str
1678 query: str
1679 matches: list[SearchCommitMatch]
1680 total_scanned: int
1681 limit: int
1682
1683
1684 # ── DAG graph models ───────────────────────────────────────────────────────────
1685
1686
1687 class DagNode(CamelModel):
1688 """A single commit node in the repo's directed acyclic graph.
1689
1690 Designed for consumption by interactive graph renderers. The ``is_head``
1691 flag marks the current HEAD commit across all branches. ``branch_labels``
1692 and ``tag_labels`` list all ref names pointing at this commit.
1693 """
1694
1695 commit_id: str
1696 message: str
1697 author: str
1698 timestamp: datetime
1699 branch: str
1700 parent_ids: list[str]
1701 is_head: bool = False
1702 branch_labels: list[str] = Field(default_factory=list)
1703 tag_labels: list[str] = Field(default_factory=list)
1704
1705
1706 class DagEdge(CamelModel):
1707 """A directed edge in the commit DAG.
1708
1709 ``source`` is the child commit (the one that has the parent).
1710 ``target`` is the parent commit. This follows standard graph convention:
1711 edge flows from child → parent (newest to oldest).
1712 """
1713
1714 source: str
1715 target: str
1716
1717
1718 class DagGraphResponse(CamelModel):
1719 """Topologically sorted commit graph for a Muse Hub repo.
1720
1721 ``nodes`` are ordered from oldest ancestor to newest commit (Kahn's
1722 algorithm). ``edges`` enumerate every parent→child relationship.
1723 Consumers can render this directly as a directed acyclic graph without
1724 further processing.
1725
1726 Agent use case: an AI music agent can use this to identify which branches
1727 diverged from a common ancestor, find merge points, and reason about the
1728 project's compositional history.
1729 """
1730
1731 nodes: list[DagNode]
1732 edges: list[DagEdge]
1733 head_commit_id: str | None = None
1734
1735
1736 # ── Session models ─────────────────────────────────────────────────────────────
1737
1738
1739 class SessionCreate(CamelModel):
1740 """Body for POST /musehub/repos/{repo_id}/sessions.
1741
1742 Sent by the CLI on ``muse session start`` to register a new session.
1743 ``started_at`` defaults to the server's current time when absent.
1744 """
1745
1746 started_at: datetime | None = Field(default=None, description="Session start time; defaults to server time when absent")
1747 participants: list[str] = Field(
1748 default_factory=list,
1749 description="Participant identifiers or display names",
1750 examples=[["miles_davis", "john_coltrane"]],
1751 )
1752 intent: str = Field(
1753 "",
1754 description="Free-text creative goal for this session",
1755 examples=["Finish the bossa nova bridge — add percussion and finalize the chord changes"],
1756 )
1757 location: str = Field(
1758 "",
1759 max_length=255,
1760 description="Studio or location label",
1761 examples=["Blue Note Studio, NYC"],
1762 )
1763 is_active: bool = Field(True, description="True if the session is currently live")
1764
1765
1766 class SessionStop(CamelModel):
1767 """Body for POST /musehub/repos/{repo_id}/sessions/{session_id}/stop.
1768
1769 Sent by the CLI on ``muse session stop`` to mark a session as ended.
1770 """
1771
1772 ended_at: datetime | None = None
1773
1774
1775 class SessionResponse(CamelModel):
1776 """Wire representation of a single recording session.
1777
1778 ``duration_seconds`` is derived from ``started_at`` and ``ended_at``;
1779 None when the session is still active (``ended_at`` is null).
1780 ``is_active`` is True while the session is open -- used by the Hub UI to
1781 render a live indicator.
1782 ``commits`` is the ordered list of Muse commit IDs associated with this session;
1783 the UI uses ``len(commits)`` as the commit count badge and the graph page
1784 uses it to apply session markers on commit nodes.
1785 ``notes`` contains closing markdown notes authored after the session ends.
1786 """
1787
1788 session_id: str
1789 started_at: datetime
1790 ended_at: datetime | None = None
1791 duration_seconds: float | None = None
1792 participants: list[str]
1793 commits: list[str] = Field(default_factory=list, description="Muse commit IDs recorded during this session")
1794 notes: str = Field("", description="Closing notes for the session (markdown)")
1795 intent: str
1796 location: str
1797 is_active: bool
1798 created_at: datetime
1799
1800
1801 class SessionListResponse(CamelModel):
1802 """Paginated list of sessions for a repo (newest first)."""
1803
1804 sessions: list[SessionResponse]
1805 total: int
1806
1807
1808 class ActivityEventResponse(CamelModel):
1809 """Wire representation of a single repo-level activity event.
1810
1811 ``event_type`` is one of:
1812 "commit_pushed" | "pr_opened" | "pr_merged" | "pr_closed" |
1813 "issue_opened" | "issue_closed" | "branch_created" | "branch_deleted" |
1814 "tag_pushed" | "session_started" | "session_ended"
1815
1816 ``metadata`` carries event-specific structured data for deep-link rendering
1817 (e.g. ``{"sha": "abc123", "message": "Add groove baseline"}`` for commit_pushed).
1818 """
1819
1820 event_id: str
1821 repo_id: str
1822 event_type: str
1823 actor: str
1824 description: str
1825 metadata: dict[str, object] = Field(default_factory=dict)
1826 created_at: datetime
1827
1828
1829 class ActivityFeedResponse(CamelModel):
1830 """Paginated activity event feed for a repo (newest-first).
1831
1832 ``page`` and ``page_size`` echo the request parameters.
1833 ``total`` is the total number of events matching the filter (ignoring pagination).
1834 ``event_type_filter`` is the active filter value, or None when showing all types.
1835 """
1836
1837 events: list[ActivityEventResponse]
1838 total: int
1839 page: int
1840 page_size: int
1841 event_type_filter: str | None = None
1842
1843
1844 # ── User public activity feed models ─────────────────────────────────────────
1845
1846
1847 class UserActivityEventItem(CamelModel):
1848 """A single event in a user's public activity feed.
1849
1850 Uses the public API type vocabulary (push, pull_request, issue, release)
1851 rather than the internal DB event_type vocabulary (commit_pushed, pr_opened, …).
1852 ``repo`` is the human-readable "{owner}/{slug}" identifier for deep-linking
1853 to the repo page without exposing internal repo_id UUIDs.
1854 ``payload`` carries event-specific structured data (e.g. branch name and
1855 head commit message for push events, PR number and title for pull_request events).
1856 """
1857
1858 id: str = Field(..., description="Internal UUID for this event")
1859 type: str = Field(
1860 ...,
1861 description="Public event type: push | pull_request | issue | release | push",
1862 )
1863 actor: str = Field(..., description="Username who triggered the event")
1864 repo: str = Field(..., description="Repo identifier as '{owner}/{slug}'")
1865 payload: dict[str, object] = Field(
1866 default_factory=dict,
1867 description="Event-specific structured data for deep-link rendering",
1868 )
1869 created_at: datetime = Field(..., description="Event creation timestamp (ISO-8601 UTC)")
1870
1871
1872 class UserActivityFeedResponse(CamelModel):
1873 """Cursor-paginated public activity feed for a Muse Hub user (newest-first).
1874
1875 ``events`` contains up to ``limit`` events for the given user, filtered to
1876 public repos only (or all repos when the caller is the profile owner).
1877 ``next_cursor`` is the event UUID to pass as ``before_id`` in the next
1878 request to fetch the subsequent page; None when there are no more events.
1879 ``type_filter`` echoes back the ``type`` query param, or None when all types
1880 are shown.
1881
1882 Agent use case: stream this feed to build a real-time view of what a
1883 collaborator has been working on across all their public repos.
1884 """
1885
1886 events: list[UserActivityEventItem]
1887 next_cursor: str | None = Field(
1888 None,
1889 description="Pass as before_id to fetch the next page; None on the last page",
1890 )
1891 type_filter: str | None = Field(
1892 None,
1893 description="Active type filter value, or None when all types are shown",
1894 )
1895
1896
1897 class SimilarCommitResponse(CamelModel):
1898 """A single result from a MuseHub semantic similarity search.
1899
1900 The score is cosine similarity in [0.0, 1.0] -- higher is more similar.
1901 Results are pre-sorted descending by score.
1902 """
1903
1904 commit_id: str = Field(..., description="Commit SHA of the matching commit")
1905 repo_id: str = Field(..., description="UUID of the repo containing this commit")
1906 score: float = Field(..., ge=0.0, le=1.0, description="Cosine similarity score")
1907 branch: str = Field(..., description="Branch the commit lives on")
1908 author: str = Field(..., description="Commit author identifier")
1909
1910
1911 class SimilarSearchResponse(CamelModel):
1912 """Response for GET /musehub/search/similar.
1913
1914 Contains the query commit SHA and a ranked list of musically similar commits.
1915 Only public repos appear in results -- enforced server-side by Qdrant filter.
1916 """
1917
1918 query_commit: str = Field(..., description="The commit SHA used as the search query")
1919 results: list[SimilarCommitResponse] = Field(
1920 default_factory=list,
1921 description="Ranked results, most similar first",
1922 )
1923
1924
1925 # ── Tree browser models ───────────────────────────────────────────────────────
1926
1927
1928 class TreeEntryResponse(CamelModel):
1929 """A single entry (file or directory) in the Muse tree browser.
1930
1931 Returned by GET /musehub/repos/{repo_id}/tree/{ref} and
1932 GET /musehub/repos/{repo_id}/tree/{ref}/{path}.
1933
1934 Consumers should use ``type`` to render the appropriate icon:
1935 - "dir" → folder icon, clickable to navigate deeper
1936 - "file" → file-type icon based on ``name`` extension
1937 (.mid → piano, .mp3/.wav → waveform, .json → braces, .webp/.png → photo)
1938
1939 ``size_bytes`` is None for directories (size is the sum of its contents,
1940 which the server does not compute at list time).
1941 """
1942
1943 type: str = Field(..., description="'file' or 'dir'")
1944 name: str = Field(..., description="Entry filename or directory name")
1945 path: str = Field(..., description="Full relative path from repo root, e.g. 'tracks/bass.mid'")
1946 size_bytes: int | None = Field(None, description="File size in bytes; None for directories")
1947
1948
1949 class TreeListResponse(CamelModel):
1950 """Directory listing for the Muse tree browser.
1951
1952 Returned by GET /musehub/repos/{repo_id}/tree/{ref} and
1953 GET /musehub/repos/{repo_id}/tree/{ref}/{path}.
1954
1955 Directories are listed before files within the same level. Within each
1956 group, entries are sorted alphabetically by name.
1957
1958 Agent use case: use this to enumerate files at a known ref without
1959 downloading any content. Combine with ``/objects/{object_id}/content``
1960 to read individual files.
1961 """
1962
1963 owner: str
1964 repo_slug: str
1965 ref: str = Field(..., description="The branch name or commit SHA used to resolve the tree")
1966 dir_path: str = Field(
1967 ..., description="Current directory path being listed; empty string for repo root"
1968 )
1969 entries: list[TreeEntryResponse] = Field(default_factory=list)
1970
1971
1972 # ── Groove Check models ───────────────────────────────────────────────────────
1973
1974
1975 class GrooveCommitEntry(CamelModel):
1976 """Per-commit groove metrics within a groove-check analysis window.
1977
1978 groove_score — average note-onset deviation from the quantization grid,
1979 measured in beats (lower = tighter to the grid).
1980 drift_delta — absolute change in groove_score relative to the prior
1981 commit. The oldest commit in the window always has 0.0.
1982 status — OK / WARN / FAIL classification against the threshold.
1983 """
1984
1985 commit: str = Field(..., description="Short commit reference (8 hex chars)")
1986 groove_score: float = Field(
1987 ..., description="Average onset deviation from quantization grid, in beats"
1988 )
1989 drift_delta: float = Field(
1990 ..., description="Absolute change in groove_score vs prior commit"
1991 )
1992 status: str = Field(..., description="OK / WARN / FAIL classification")
1993 track: str = Field(..., description="Track scope analysed, or 'all'")
1994 section: str = Field(..., description="Section scope analysed, or 'all'")
1995 midi_files: int = Field(..., description="Number of MIDI snapshots analysed")
1996
1997
1998 class ArrangementCellData(CamelModel):
1999 """Data for a single cell in the arrangement matrix (instrument × section).
2000
2001 Encodes whether an instrument plays in a given section, how dense its part is,
2002 and enough detail for a tooltip (note count, beat range, pitch range).
2003 """
2004
2005 instrument: str = Field(..., description="Instrument/track name (e.g. 'bass', 'keys')")
2006 section: str = Field(..., description="Section label (e.g. 'intro', 'chorus')")
2007 note_count: int = Field(..., description="Total notes played by this instrument in this section")
2008 note_density: float = Field(
2009 ...,
2010 ge=0.0,
2011 le=1.0,
2012 description="Normalised note density in [0, 1]; 0 = silent, 1 = densest cell",
2013 )
2014 beat_start: float = Field(..., description="Beat position where this section starts")
2015 beat_end: float = Field(..., description="Beat position where this section ends")
2016 pitch_low: int = Field(..., description="Lowest MIDI pitch played (0-127)")
2017 pitch_high: int = Field(..., description="Highest MIDI pitch played (0-127)")
2018 active: bool = Field(..., description="True when the instrument has at least one note in this section")
2019
2020
2021 class ArrangementRowSummary(CamelModel):
2022 """Aggregated stats for one instrument row across all sections."""
2023
2024 instrument: str = Field(..., description="Instrument/track name")
2025 total_notes: int = Field(..., description="Total note count across all sections")
2026 active_sections: int = Field(..., description="Number of sections where the instrument plays")
2027 mean_density: float = Field(..., description="Mean note density across all sections")
2028
2029
2030 class ArrangementColumnSummary(CamelModel):
2031 """Aggregated stats for one section column across all instruments."""
2032
2033 section: str = Field(..., description="Section label")
2034 total_notes: int = Field(..., description="Total note count across all instruments")
2035 active_instruments: int = Field(..., description="Number of instruments that play in this section")
2036 beat_start: float = Field(..., description="Beat position where this section starts")
2037 beat_end: float = Field(..., description="Beat position where this section ends")
2038
2039
2040 class ArrangementMatrixResponse(CamelModel):
2041 """Full arrangement matrix for a Muse commit ref.
2042
2043 Provides a bird's-eye view of which instruments play in which sections
2044 so producers can evaluate orchestration density without downloading tracks.
2045
2046 The ``cells`` list is a flat row-major enumeration of (instrument, section)
2047 pairs. Consumers should index by (instrument, section) for O(1) lookup.
2048 Row/column summaries pre-aggregate totals so the UI can draw marginal bars
2049 without re-summing the cell list.
2050 """
2051
2052 repo_id: str = Field(..., description="Internal repo UUID")
2053 ref: str = Field(..., description="Commit ref (full SHA or branch name)")
2054 instruments: list[str] = Field(..., description="Ordered instrument names (Y-axis)")
2055 sections: list[str] = Field(..., description="Ordered section labels (X-axis)")
2056 cells: list[ArrangementCellData] = Field(
2057 default_factory=list,
2058 description="Flat list of (instrument × section) cells, row-major order",
2059 )
2060 row_summaries: list[ArrangementRowSummary] = Field(
2061 default_factory=list,
2062 description="Per-instrument aggregates, same order as instruments list",
2063 )
2064 column_summaries: list[ArrangementColumnSummary] = Field(
2065 default_factory=list,
2066 description="Per-section aggregates, same order as sections list",
2067 )
2068 total_beats: float = Field(..., description="Total beat length of the arrangement")
2069
2070
2071 class BlobMetaResponse(CamelModel):
2072 """Wire representation of a single file (blob) in the Muse tree browser.
2073
2074 Returned by GET /musehub/repos/{repo_id}/blob/{ref}/{path}.
2075 Consumers use ``file_type`` to choose the appropriate rendering mode
2076 (piano roll for MIDI, audio player for MP3/WAV, inline img for images,
2077 syntax-highlighted text for JSON/XML, hex dump for unknown binaries).
2078 ``content_text`` is populated only for text files up to 256 KB; binary
2079 files should use ``raw_url`` to stream content.
2080 """
2081
2082 object_id: str = Field(..., description="Content-addressed ID, e.g. 'sha256:abc123...'")
2083 path: str = Field(..., description="Relative path from repo root, e.g. 'tracks/bass.mid'")
2084 filename: str = Field(..., description="Basename of the file, e.g. 'bass.mid'")
2085 size_bytes: int = Field(..., description="File size in bytes")
2086 sha: str = Field(..., description="Content-addressed SHA identifier")
2087 created_at: datetime = Field(..., description="Timestamp when this object was pushed")
2088 raw_url: str = Field(..., description="URL to download the raw file bytes")
2089 file_type: str = Field(
2090 ...,
2091 description="Rendering hint: 'midi' | 'audio' | 'json' | 'image' | 'xml' | 'other'",
2092 )
2093 content_text: str | None = Field(
2094 None,
2095 description="UTF-8 content for JSON/XML files up to 256 KB; None for binary or oversized files",
2096 )
2097
2098
2099 class GrooveCheckResponse(CamelModel):
2100 """Rhythmic consistency dashboard data for a commit range in a Muse Hub repo.
2101
2102 Aggregates timing deviation, swing ratio, and quantization tightness
2103 metrics derived from MIDI snapshots across a window of commits. The
2104 ``entries`` list is ordered oldest-first so consumers can plot groove
2105 evolution over time.
2106 """
2107
2108 commit_range: str = Field(..., description="Commit range string that was analysed")
2109 threshold: float = Field(
2110 ..., description="Drift threshold in beats used for WARN/FAIL classification"
2111 )
2112 total_commits: int = Field(..., description="Total commits in the analysis window")
2113 flagged_commits: int = Field(
2114 ..., description="Number of commits with WARN or FAIL status"
2115 )
2116 worst_commit: str = Field(
2117 ..., description="Commit ref with the highest drift_delta, or empty string"
2118 )
2119 entries: list[GrooveCommitEntry] = Field(
2120 default_factory=list,
2121 description="Per-commit metrics, oldest-first",
2122 )
2123
2124
2125 # ── Listen page models ────────────────────────────────────────────────────────
2126
2127
2128 class AudioTrackEntry(CamelModel):
2129 """A single audio artifact surfaced on the listen page.
2130
2131 Represents one stem or full-mix file at a given commit ref. The
2132 ``audio_url`` is the canonical download path served by the objects
2133 endpoint. ``piano_roll_url`` is non-None only when a matching .webp
2134 piano-roll image exists at the same path prefix.
2135 """
2136
2137 name: str = Field(..., description="Display name derived from the file path (basename without extension)")
2138 path: str = Field(..., description="Relative artifact path, e.g. 'tracks/bass.mp3'")
2139 object_id: str = Field(..., description="Content-addressed object ID")
2140 audio_url: str = Field(
2141 ..., description="Absolute URL to stream or download this artifact"
2142 )
2143 piano_roll_url: str | None = Field(
2144 default=None, description="Absolute URL to the matching piano-roll image, if available"
2145 )
2146 size_bytes: int = Field(..., description="File size in bytes")
2147
2148
2149 class TrackListingResponse(CamelModel):
2150 """Full-mix and per-track audio listing for a repo at a given ref.
2151
2152 Powers the listen page's dual-view UX: the full-mix player at the top
2153 and the per-track listing below. The ``has_renders`` flag lets the
2154 client differentiate between a repo with no audio at all and one that
2155 has audio but no explicit full-mix file.
2156 """
2157
2158 repo_id: str = Field(..., description="Internal UUID of the repo")
2159 ref: str = Field(..., description="Commit ref or branch name resolved by this listing")
2160 full_mix_url: str | None = Field(
2161 default=None,
2162 description="Audio URL for the first full-mix file found, or None if absent",
2163 )
2164 tracks: list[AudioTrackEntry] = Field(
2165 default_factory=list,
2166 description="All audio artifacts at this ref, sorted by path",
2167 )
2168 has_renders: bool = Field(
2169 ...,
2170 description="True when at least one audio artifact exists at this ref",
2171 )
2172
2173
2174 # ── Compare view models ────────────────────────────────────────────────────────
2175
2176
2177 class EmotionDiffResponse(CamelModel):
2178 """Delta between the emotional character of base and head refs.
2179
2180 Each field is ``head_value − base_value`` in [−1.0, 1.0]. Positive
2181 means head is more energetic/positive/tense/dark than base; negative
2182 means the opposite. Values are derived deterministically from commit
2183 SHA hashes so they are always reproducible.
2184
2185 Agents use this to answer "how did the mood shift between these two
2186 refs?" without running external ML inference.
2187 """
2188
2189 energy_delta: float = Field(
2190 ..., description="Δenergy (head − base), in [−1.0, 1.0]"
2191 )
2192 valence_delta: float = Field(
2193 ..., description="Δvalence (head − base), in [−1.0, 1.0]"
2194 )
2195 tension_delta: float = Field(
2196 ..., description="Δtension (head − base), in [−1.0, 1.0]"
2197 )
2198 darkness_delta: float = Field(
2199 ..., description="Δdarkness (head − base), in [−1.0, 1.0]"
2200 )
2201 base_energy: float = Field(..., description="Mean energy score for the base ref")
2202 base_valence: float = Field(..., description="Mean valence score for the base ref")
2203 base_tension: float = Field(..., description="Mean tension score for the base ref")
2204 base_darkness: float = Field(..., description="Mean darkness score for the base ref")
2205 head_energy: float = Field(..., description="Mean energy score for the head ref")
2206 head_valence: float = Field(..., description="Mean valence score for the head ref")
2207 head_tension: float = Field(..., description="Mean tension score for the head ref")
2208 head_darkness: float = Field(..., description="Mean darkness score for the head ref")
2209
2210
2211 class CompareResponse(CamelModel):
2212 """Multi-dimensional musical comparison between two refs in a Muse Hub repo.
2213
2214 Returned by ``GET /musehub/repos/{repo_id}/compare?base=X&head=Y``.
2215 Combines divergence scores, unique commits, and emotion diff into a single
2216 payload that powers the compare page UI.
2217
2218 The ``commits`` list contains only commits that are reachable from ``head``
2219 but not from ``base`` (i.e. commits unique to head), newest first. This
2220 mirrors GitHub's compare view: "commits you'd be adding to base."
2221
2222 Agents use this to decide whether to open a pull request and what the
2223 musical impact of merging would be.
2224 """
2225
2226 repo_id: str = Field(..., description="Repository identifier")
2227 base_ref: str = Field(..., description="Base ref (branch name, tag, or commit SHA)")
2228 head_ref: str = Field(..., description="Head ref (branch name, tag, or commit SHA)")
2229 common_ancestor: str | None = Field(
2230 default=None,
2231 description="Most recent common ancestor commit ID, or null if histories are disjoint",
2232 )
2233 dimensions: list[DivergenceDimensionResponse] = Field(
2234 ..., description="Five per-dimension divergence scores (melodic/harmonic/rhythmic/structural/dynamic)"
2235 )
2236 overall_score: float = Field(
2237 ..., description="Mean of all five dimension scores in [0.0, 1.0]"
2238 )
2239 commits: list[CommitResponse] = Field(
2240 ..., description="Commits in head not in base (newest first)"
2241 )
2242 emotion_diff: EmotionDiffResponse = Field(
2243 ..., description="Emotional character delta between base and head"
2244 )
2245 create_pr_url: str = Field(
2246 ..., description="URL to create a pull request from this comparison"
2247 )
2248
2249
2250
2251 # ── Star / Fork models ─────────────────────────────────────────────────────
2252
2253
2254 class StargazerEntry(CamelModel):
2255 """A single user who has starred a repo.
2256
2257 Returned as items in ``StargazerListResponse``. ``user_id`` is the JWT
2258 ``sub`` of the starring user; ``starred_at`` is when the star was created.
2259 """
2260
2261 user_id: str = Field(..., description="User ID (JWT sub) of the starring user")
2262 starred_at: datetime = Field(..., description="Timestamp when the star was created (ISO-8601 UTC)")
2263
2264
2265 class StargazerListResponse(CamelModel):
2266 """Paginated list of users who have starred a repo.
2267
2268 Returned by ``GET /api/v1/musehub/repos/{repo_id}/stargazers``.
2269 ``total`` is the full count, not just the current page, so clients can
2270 display "N stargazers" without a second query.
2271 """
2272
2273 stargazers: list[StargazerEntry] = Field(..., description="Users who starred this repo")
2274 total: int = Field(..., description="Total number of stargazers")
2275
2276
2277 class ForkEntry(CamelModel):
2278 """A single fork of a repo.
2279
2280 Carries both the fork's repo metadata and the lineage link back to the
2281 source repo. Returned as items in ``ForkListResponse``.
2282 """
2283
2284 fork_id: str = Field(..., description="Internal UUID of the fork relationship record")
2285 fork_repo_id: str = Field(..., description="Repo ID of the forked repo")
2286 source_repo_id: str = Field(..., description="Repo ID of the source (original) repo")
2287 forked_by: str = Field(..., description="User ID who created the fork")
2288 fork_owner: str = Field(..., description="Owner username of the fork repo")
2289 fork_slug: str = Field(..., description="Slug of the fork repo")
2290 created_at: datetime = Field(..., description="Timestamp when the fork was created (ISO-8601 UTC)")
2291
2292
2293 class ForkListResponse(CamelModel):
2294 """Paginated list of forks of a repo.
2295
2296 Returned by ``GET /api/v1/musehub/repos/{repo_id}/forks``.
2297 """
2298
2299 forks: list[ForkEntry] = Field(..., description="Forks of this repo")
2300 total: int = Field(..., description="Total number of forks")
2301
2302
2303 class ForkCreateResponse(CamelModel):
2304 """Confirmation that a fork was created.
2305
2306 Returned by ``POST /api/v1/musehub/repos/{repo_id}/fork``.
2307 ``fork_repo`` is the newly created repo under the authenticated user's
2308 namespace. ``source_repo_id`` is the original repo's ID for lineage
2309 display on the fork's home page.
2310 """
2311
2312 fork_repo: RepoResponse = Field(..., description="Newly created fork repo metadata")
2313 source_repo_id: str = Field(..., description="ID of the original source repo")
2314 source_owner: str = Field(..., description="Owner username of the source repo")
2315 source_slug: str = Field(..., description="Slug of the source repo")
2316
2317
2318 class UserForkedRepoEntry(CamelModel):
2319 """A single forked repo entry shown on a user's profile Forked tab.
2320
2321 Combines the fork repo's full metadata with source attribution so the
2322 profile page can render "forked from {source_owner}/{source_slug}" under
2323 each card.
2324 """
2325
2326 fork_id: str = Field(..., description="Internal UUID of the fork relationship record")
2327 fork_repo: RepoResponse = Field(..., description="Full metadata of the forked (child) repo")
2328 source_owner: str = Field(..., description="Owner username of the original source repo")
2329 source_slug: str = Field(..., description="Slug of the original source repo")
2330 forked_at: datetime = Field(..., description="Timestamp when the fork was created (ISO-8601 UTC)")
2331
2332
2333 class UserForksResponse(CamelModel):
2334 """Paginated list of repos forked by a user.
2335
2336 Returned by ``GET /api/v1/musehub/users/{username}/forks``.
2337 """
2338
2339 forks: list[UserForkedRepoEntry] = Field(..., description="Repos forked by this user")
2340 total: int = Field(..., description="Total number of forked repos")
2341
2342
2343 class ForkNetworkNode(CamelModel):
2344 """A single node in the fork network tree.
2345
2346 Represents one repo (root or fork) with its owner/slug identity,
2347 the number of commits it has diverged from its immediate parent,
2348 and its own children in the tree.
2349
2350 Used by ``GET /musehub/ui/{owner}/{repo_slug}/forks`` (JSON path)
2351 to surface the full network graph for programmatic traversal.
2352 """
2353
2354 owner: str = Field(..., description="Owner username of this repo")
2355 repo_slug: str = Field(..., description="Slug of this repo")
2356 repo_id: str = Field(..., description="Internal UUID of this repo")
2357 divergence_commits: int = Field(
2358 ...,
2359 description="Commits this fork has ahead of its immediate parent (0 for root)",
2360 )
2361 forked_by: str = Field(
2362 ..., description="User ID who created the fork (empty string for root repo)"
2363 )
2364 forked_at: datetime | None = Field(
2365 None, description="Timestamp when the fork was created (None for root repo)"
2366 )
2367 children: list["ForkNetworkNode"] = Field(
2368 default_factory=list,
2369 description="Direct forks of this repo, each recursively carrying their own children",
2370 )
2371
2372
2373 class ForkNetworkResponse(CamelModel):
2374 """Fork network graph for a repo — root with recursive children.
2375
2376 Returned by ``GET /musehub/ui/{owner}/{repo_slug}/forks?format=json``.
2377
2378 The ``root`` node represents the canonical upstream repo. Each
2379 ``ForkNetworkNode`` in ``root.children`` is a direct fork; their
2380 own ``children`` lists contain second-level forks, and so on.
2381
2382 ``total_forks`` is the flat count of all fork nodes in the tree
2383 (excluding the root), so callers can display "N forks" without
2384 walking the tree.
2385
2386 Agent use case: determine how many downstream forks exist, identify
2387 the most-diverged fork before proposing a merge-back PR, or decide
2388 which fork to merge into the root.
2389 """
2390
2391 root: ForkNetworkNode = Field(..., description="Root repo (the upstream source)")
2392 total_forks: int = Field(..., description="Total number of fork nodes in the network")
2393
2394
2395 # Resolve forward reference in self-referential ForkNetworkNode.children
2396 ForkNetworkNode.model_rebuild()
2397
2398
2399 class UserStarredRepoEntry(CamelModel):
2400 """A single starred-repo entry shown on a user's profile Starred tab.
2401
2402 Combines the starred repo's full metadata with the star timestamp so the
2403 profile page can render the repo card with owner/slug linked and
2404 "starred at {timestamp}" context.
2405 """
2406
2407 star_id: str = Field(..., description="Internal UUID of the star relationship record")
2408 repo: RepoResponse = Field(..., description="Full metadata of the starred repo")
2409 starred_at: datetime = Field(..., description="Timestamp when the user starred the repo (ISO-8601 UTC)")
2410
2411
2412 class UserStarredResponse(CamelModel):
2413 """Paginated list of repos starred by a user.
2414
2415 Returned by ``GET /api/v1/musehub/users/{username}/starred``.
2416 """
2417
2418 starred: list[UserStarredRepoEntry] = Field(..., description="Repos starred by this user")
2419 total: int = Field(..., description="Total number of starred repos")
2420
2421
2422 class UserWatchedRepoEntry(CamelModel):
2423 """A single watched-repo entry shown on a user's profile Watching tab.
2424
2425 Combines the watched repo's full metadata with the watch timestamp so the
2426 profile page can render the repo card with owner/slug linked and
2427 "watching since {timestamp}" context.
2428 """
2429
2430 watch_id: str = Field(..., description="Internal UUID of the watch relationship record")
2431 repo: RepoResponse = Field(..., description="Full metadata of the watched repo")
2432 watched_at: datetime = Field(..., description="Timestamp when the user started watching the repo (ISO-8601 UTC)")
2433
2434
2435 class UserWatchedResponse(CamelModel):
2436 """Paginated list of repos watched by a user.
2437
2438 Returned by ``GET /api/v1/musehub/users/{username}/watched``.
2439 """
2440
2441 watched: list[UserWatchedRepoEntry] = Field(..., description="Repos watched by this user")
2442 total: int = Field(..., description="Total number of watched repos")
2443
2444
2445 # ── Render pipeline ────────────────────────────────────────────────────────
2446
2447
2448 class RepoSettingsResponse(CamelModel):
2449 """Mutable settings for a Muse Hub repo.
2450
2451 Returned by ``GET /api/v1/musehub/repos/{repo_id}/settings``.
2452
2453 Fields map to GitHub-style repo settings. ``name``, ``description``,
2454 ``visibility``, and ``topics`` are stored in dedicated repo columns;
2455 all remaining flags are stored in the ``settings`` JSON blob.
2456
2457 Agent use case: read before updating project metadata, toggling features,
2458 or configuring merge strategy for a repo's PR workflow.
2459 """
2460
2461 name: str = Field(..., description="Human-readable repo name")
2462 description: str = Field("", description="Short description shown on the explore page")
2463 visibility: str = Field(..., description="'public' or 'private'")
2464 default_branch: str = Field("main", description="Default branch name (used for clone and PRs)")
2465 has_issues: bool = Field(True, description="Whether the issues tracker is enabled")
2466 has_projects: bool = Field(False, description="Whether the projects board is enabled")
2467 has_wiki: bool = Field(False, description="Whether the wiki is enabled")
2468 topics: list[str] = Field(default_factory=list, description="Free-form topic tags")
2469 license: str | None = Field(None, description="SPDX license identifier or display name, e.g. 'CC BY 4.0'")
2470 homepage_url: str | None = Field(None, description="Project homepage URL")
2471 allow_merge_commit: bool = Field(True, description="Allow merge commits on PRs")
2472 allow_squash_merge: bool = Field(True, description="Allow squash merges on PRs")
2473 allow_rebase_merge: bool = Field(False, description="Allow rebase merges on PRs")
2474 delete_branch_on_merge: bool = Field(True, description="Auto-delete head branch after PR merge")
2475
2476
2477 class RepoSettingsPatch(CamelModel):
2478 """Partial update body for ``PATCH /api/v1/musehub/repos/{repo_id}/settings``.
2479
2480 All fields are optional — only provided fields are updated.
2481 ``visibility`` must be ``'public'`` or ``'private'`` when supplied.
2482 Caller must hold owner or admin collaborator permission; otherwise 403 is returned.
2483
2484 Agent use case: update repo visibility, merge strategy, or homepage URL
2485 without knowing the full settings object.
2486 """
2487
2488 name: str | None = Field(None, description="New repo name")
2489 description: str | None = Field(None, description="New description")
2490 visibility: str | None = Field(
2491 None,
2492 pattern="^(public|private)$",
2493 description="'public' or 'private'",
2494 )
2495 default_branch: str | None = Field(None, description="New default branch name")
2496 has_issues: bool | None = Field(None, description="Enable/disable issues tracker")
2497 has_projects: bool | None = Field(None, description="Enable/disable projects board")
2498 has_wiki: bool | None = Field(None, description="Enable/disable wiki")
2499 topics: list[str] | None = Field(None, description="Replace topic tags (full list)")
2500 license: str | None = Field(None, description="SPDX license identifier or display name")
2501 homepage_url: str | None = Field(None, description="Project homepage URL")
2502 allow_merge_commit: bool | None = Field(None, description="Allow merge commits on PRs")
2503 allow_squash_merge: bool | None = Field(None, description="Allow squash merges on PRs")
2504 allow_rebase_merge: bool | None = Field(None, description="Allow rebase merges on PRs")
2505 delete_branch_on_merge: bool | None = Field(None, description="Auto-delete head branch after PR merge")
2506
2507
2508 class RenderStatusResponse(CamelModel):
2509 """Render job status for a single commit's auto-generated artifacts.
2510
2511 Returned by ``GET /api/v1/musehub/repos/{repo_id}/commits/{sha}/render-status``.
2512
2513 ``status`` lifecycle: ``pending`` → ``rendering`` → ``complete`` | ``failed``.
2514 ``mp3_object_ids`` and ``image_object_ids`` are populated only when
2515 status is ``complete``; both lists may be empty when no MIDI files were
2516 pushed with the commit.
2517
2518 When no render job exists for the given commit SHA, the endpoint returns
2519 ``status="not_found"`` with empty artifact lists rather than a 404, so
2520 callers do not need to distinguish between "never pushed" and "not yet
2521 rendered".
2522 """
2523
2524 commit_id: str = Field(..., description="Muse commit SHA")
2525 status: str = Field(
2526 ...,
2527 description="Render job status: pending | rendering | complete | failed | not_found",
2528 )
2529 midi_count: int = Field(
2530 default=0,
2531 description="Number of MIDI objects found in the commit",
2532 )
2533 mp3_object_ids: list[str] = Field(
2534 default_factory=list,
2535 description="Object IDs of generated MP3 (or stub) artifacts",
2536 )
2537 image_object_ids: list[str] = Field(
2538 default_factory=list,
2539 description="Object IDs of generated piano-roll PNG artifacts",
2540 )
2541 error_message: str | None = Field(
2542 default=None,
2543 description="Error details when status is 'failed'; null otherwise",
2544 )
2545
2546
2547 # ── Blame models ────────────────────────────────────────────────────────────
2548
2549
2550 class BlameEntry(CamelModel):
2551 """A single blame annotation entry attributing a note event to a commit.
2552
2553 Each entry maps a note (identified by pitch, track, and beat range) to the
2554 commit that last introduced or modified it. When filtering by ``track`` or
2555 ``beat_start``/``beat_end``, only entries within the specified scope are
2556 returned.
2557
2558 Consumers (e.g. the blame UI page) use ``commit_id`` to deep-link to the
2559 commit detail view and ``author`` / ``timestamp`` to display inline
2560 attribution labels on the piano roll.
2561 """
2562
2563 commit_id: str = Field(..., description="ID of the commit that last modified this note")
2564 commit_message: str = Field(..., description="Commit message from the attributing commit")
2565 author: str = Field(..., description="Display name or identifier of the commit author")
2566 timestamp: datetime = Field(..., description="UTC timestamp of the attributing commit")
2567 beat_start: float = Field(..., description="Start position of the note in quarter-note beats")
2568 beat_end: float = Field(..., description="End position of the note in quarter-note beats")
2569 track: str = Field(..., description="Instrument track name this note belongs to")
2570 note_pitch: int = Field(..., description="MIDI pitch value (0–127)")
2571 note_velocity: int = Field(..., description="MIDI velocity (0–127)")
2572 note_duration_beats: float = Field(..., description="Duration of the note in quarter-note beats")
2573
2574
2575 class BlameResponse(CamelModel):
2576 """Response envelope for the blame API.
2577
2578 ``entries`` is the list of blame annotations, each attributing a note to the
2579 commit that last modified it. ``total_entries`` reflects the total number of
2580 matching entries before any client-side pagination.
2581
2582 When no matching notes are found (e.g. the path does not exist at ``ref``
2583 or the track/beat filters exclude all notes), ``entries`` is empty and
2584 ``total_entries`` is 0 — the endpoint never returns 404 for an empty result.
2585 """
2586
2587 entries: list[BlameEntry] = Field(
2588 default_factory=list,
2589 description="Blame annotations, each attributing a note to its last-modifying commit",
2590 )
2591 total_entries: int = Field(
2592 default=0,
2593 description="Total number of blame entries in the response",
2594 )
2595
2596
2597 # ── Collaborator access-check model ─────────────────────────────────────────
2598
2599
2600 class CollaboratorAccessResponse(CamelModel):
2601 """Response for the collaborator access-check endpoint.
2602
2603 Returns the effective permission level for a given username on a repo.
2604 The owner's effective permission is always ``"owner"``. Non-collaborators
2605 are reported as 404 rather than returning a ``"none"`` permission value,
2606 so callers can distinguish a known absence (404) from a positive result.
2607
2608 ``accepted_at`` is ``null`` for the repo owner (ownership is immediate)
2609 and for collaborators whose invitation is still pending acceptance.
2610 """
2611
2612 username: str = Field(..., description="User identifier supplied in the request path")
2613 permission: str = Field(
2614 ...,
2615 description="Effective permission level: 'read' | 'write' | 'admin' | 'owner'",
2616 )
2617 accepted_at: datetime | None = Field(
2618 None,
2619 description="UTC timestamp when the collaborator accepted the invitation; null for owners",
2620 )
2621
2622
2623 # ── Canvas SSR scaffolding models ─────────────────────────────────────────────
2624
2625
2626 class InstrumentInfo(CamelModel):
2627 """Metadata for a single instrument lane in the piano roll sidebar.
2628
2629 Derived server-side from stored MIDI object paths so the instrument
2630 sidebar is rendered in SSR without requiring a client fetch cycle.
2631 ``channel`` is the zero-based index of this instrument among all MIDI
2632 objects in the repo; ``gm_program`` is None until MIDI parsing is available.
2633 """
2634
2635 name: str = Field(..., description="Human-readable instrument name, e.g. 'bass'")
2636 channel: int = Field(..., description="Zero-based lane index (render order)")
2637 gm_program: int | None = Field(
2638 None, description="General MIDI program number when known; null otherwise"
2639 )
2640
2641
2642 class TrackInfo(CamelModel):
2643 """SSR metadata for a single MIDI track shown in the piano roll header.
2644
2645 Populated from the ``musehub_objects`` row matching the requested path.
2646 ``duration_sec`` and ``track_count`` are None until server-side MIDI
2647 parsing is implemented; the template renders them conditionally.
2648 """
2649
2650 name: str = Field(..., description="Display name derived from the object path")
2651 size_bytes: int = Field(..., description="File size in bytes")
2652 duration_sec: float | None = Field(
2653 None, description="Track duration in seconds; null until MIDI parsing is available"
2654 )
2655 track_count: int | None = Field(
2656 None, description="Number of MIDI tracks; null until MIDI parsing is available"
2657 )
2658
2659
2660 class ScoreMetaInfo(CamelModel):
2661 """SSR metadata displayed in the score page header before JS renders notation.
2662
2663 Fields are derived from stored object metadata; ``key``, ``meter``,
2664 ``composer``, and ``instrument_count`` are None until server-side MIDI
2665 parsing is implemented. The template renders them conditionally so the
2666 page is useful even with partial data.
2667 """
2668
2669 title: str = Field(..., description="Score title derived from the file path")
2670 composer: str | None = Field(None, description="Composer name when known")
2671 key: str | None = Field(None, description="Key signature when known, e.g. 'C major'")
2672 meter: str | None = Field(None, description="Time signature when known, e.g. '4/4'")
2673 instrument_count: int | None = Field(
2674 None, description="Number of instrument parts when known"
2675 )