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