gabriel / musehub public
musehub_repo_models.py python
811 lines 38.0 KB
Raw
sha256:5dfc96524e3921eb9acb8372241b6bec70b5f3e6598f79099a0ead16ff7cbb75 feat(phase1): add musehub_fetch_mpack_cache table (issue #9… Sonnet 4.6 patch 34 days ago
1 """ORM models for core repo and VCS objects.
2
3 Tables:
4 - musehub_repos: Remote repos (one per project, across any Muse domain)
5 - musehub_branches: Named branch pointers inside a repo
6 - musehub_commits: Content-addressed commit records — globally shared, no repo ownership
7 - musehub_commit_refs: Materialized reachability index — (repo_id, commit_id) membership
8 - musehub_objects: Content-addressed binary artifact storage — globally shared
9 - musehub_object_refs: Materialized reachability index — (repo_id, object_id) membership
10 - musehub_snapshots: Content-addressed file-tree records — globally shared, no repo ownership
11 - musehub_snapshot_refs: Materialized reachability index — (repo_id, snapshot_id) membership
12 - musehub_snapshot_entries: Normalized per-file rows within a snapshot
13 - musehub_sessions: Recording session records pushed from the CLI
14 - musehub_wire_tags: Lightweight semantic tags pushed via wire protocol
15 - musehub_version_tags: Semantic-version tags pushed via wire protocol
16 - musehub_bridge_mirrors: Git mirror registrations for a MuseHub repo
17 - musehub_mists: Content-addressed, signed, forkable single-artifact shares
18 - musehub_mpack_index: MPack index — maps every pushed object to its mpack in MinIO
19 - musehub_fetch_mpack_cache: Pre-built fetch mpacks keyed by (repo, tip commit)
20 - musehub_commit_graph: Precomputed commit reachability for O(frontier) DAG walks
21 """
22
23 from __future__ import annotations
24
25 from datetime import datetime, timezone
26
27 import sqlalchemy as sa
28 from sqlalchemy import ARRAY, Boolean, DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint
29 from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column, relationship
30 from sqlalchemy.dialects.postgresql import JSONB
31
32 from musehub.db.database import Base
33 from musehub.types.json_types import JSONObject, JSONValue # JSONValue needed for ForwardRef resolution in Mapped[]
34
35
36 def _utc_now() -> datetime:
37 return datetime.now(tz=timezone.utc)
38
39
40 class MusehubRepo(MappedAsDataclass, Base):
41 """A remote Muse repository — the hub-side equivalent of a Git remote.
42
43 ``owner`` is the URL-visible username (e.g. "gabriel") and ``slug`` is the
44 URL-safe repo name auto-generated from ``name`` (e.g. "neo-soul-experiment").
45 Together they form the canonical /{owner}/{slug} URL scheme. The internal
46 ``repo_id`` sha256 genesis hash remains the primary key — external URLs never expose it.
47
48 ``domain_id`` links this repo to a registered Muse domain plugin
49 (e.g. ``@gabriel/code``). ``domain_meta`` is a free-form JSON object for
50 domain-specific metadata declared by that plugin.
51 Tags are free-form strings that make repos discoverable on the explore page.
52 """
53
54 __tablename__ = "musehub_repos"
55 __table_args__ = (
56 UniqueConstraint("owner", "slug", name="uq_musehub_repos_owner_slug"),
57 # Explore page: list public repos by owner
58 Index("ix_musehub_repos_owner_visibility", "owner", "visibility"),
59 )
60
61 # --- Required fields (no default) — must precede optional fields ---
62 repo_id: Mapped[str] = mapped_column(String(128), primary_key=True)
63 name: Mapped[str] = mapped_column(String(255), nullable=False)
64 # URL-visible owner username, e.g. "gabriel" — forms the /{owner}/{slug} path
65 owner: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
66 # URL-safe slug auto-generated from name, e.g. "neo-soul-experiment"
67 slug: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
68 owner_user_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
69
70 # --- Optional fields with Python-side defaults ---
71 visibility: Mapped[str] = mapped_column(String(20), nullable=False, default="public", server_default="public")
72 description: Mapped[str] = mapped_column(Text, nullable=False, default="")
73 # list of free-form tag strings for discovery
74 tags: Mapped[list[str]] = mapped_column(ARRAY(Text), nullable=False, default_factory=list)
75 # FK to musehub_domains — null means a legacy row predating this default.
76 # New repos always have domain_id set; "code" is the default when none is specified.
77 domain_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True, default="code")
78 # Domain-specific metadata blob declared by the domain plugin
79 domain_meta: Mapped[JSONObject] = mapped_column(JSONB, nullable=False, default_factory=dict)
80 # musehub#117 Phase 3 (DOM_12): real FK to a registered marketplace
81 # MusehubDomain row (sha256 domain_id), deliberately separate from the
82 # plain-string domain_id column above. domain_id is a VCS-plugin
83 # category label ("code"/"midi"/"mist") load-bearing for the profile
84 # heatmap, and existing code actively guards against a real marketplace
85 # ID leaking into it (see musehub_profile.py) — this column is the
86 # genuine, unambiguous link a repo uses to opt into a third-party
87 # domain's registered viewer, never conflated with that string field.
88 marketplace_domain_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True, default=None)
89 # Feature-flag settings not covered by dedicated columns (JSON blob).
90 settings: Mapped[JSONObject | None] = mapped_column(JSONB, nullable=True, default=None)
91 # Default branch name (updated on each push to match CLI intent)
92 default_branch: Mapped[str] = mapped_column(String(255), nullable=False, default="main", server_default="main")
93 # Last push timestamp — used for trending sort
94 pushed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, default=None)
95 created_at: Mapped[datetime] = mapped_column(
96 DateTime(timezone=True), nullable=False, default_factory=_utc_now, server_default=sa.func.now()
97 )
98 updated_at: Mapped[datetime] = mapped_column(
99 DateTime(timezone=True), nullable=False, default_factory=_utc_now, server_default=sa.func.now(), onupdate=_utc_now
100 )
101 # Compliance: opt-out of AI training data pipelines for this repo.
102 training_opt_out: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=sa.false())
103 # DMCA / legal hold — set True by POST /api/admin/takedown.
104 # When True, pushes to this repo are blocked and objects are quarantined.
105 dmca_hold: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=sa.false())
106
107 # --- Relationships — excluded from __init__ ---
108 branches: Mapped[list[MusehubBranch]] = relationship(
109 "MusehubBranch", back_populates="repo", cascade="all, delete-orphan",
110 init=False, default_factory=list,
111 )
112 commit_refs: Mapped[list["MusehubCommitRef"]] = relationship(
113 "MusehubCommitRef", back_populates="repo", cascade="all, delete-orphan",
114 init=False, default_factory=list,
115 )
116 issues: Mapped[list["MusehubIssue"]] = relationship(
117 "MusehubIssue", back_populates="repo", cascade="all, delete-orphan",
118 init=False, default_factory=list,
119 )
120 proposals: Mapped[list["MusehubProposal"]] = relationship(
121 "MusehubProposal", back_populates="repo", cascade="all, delete-orphan",
122 init=False, default_factory=list,
123 )
124 releases: Mapped[list["MusehubRelease"]] = relationship(
125 "MusehubRelease", back_populates="repo", cascade="all, delete-orphan",
126 init=False, default_factory=list,
127 )
128 sessions: Mapped[list[MusehubSession]] = relationship(
129 "MusehubSession", back_populates="repo", cascade="all, delete-orphan",
130 init=False, default_factory=list,
131 )
132 webhooks: Mapped[list["MusehubWebhook"]] = relationship(
133 "MusehubWebhook", back_populates="repo", cascade="all, delete-orphan",
134 init=False, default_factory=list,
135 )
136 wire_tags: Mapped[list[MusehubWireTag]] = relationship(
137 "MusehubWireTag", back_populates="repo", cascade="all, delete-orphan",
138 init=False, default_factory=list,
139 )
140 version_tags: Mapped[list["MusehubVersionTag"]] = relationship(
141 "MusehubVersionTag", back_populates="repo", cascade="all, delete-orphan",
142 init=False, default_factory=list,
143 )
144
145
146 class MusehubBranch(Base):
147 """A named branch pointer inside a MuseHub repo."""
148
149 __tablename__ = "musehub_branches"
150 __table_args__ = (
151 # Branch name lookup: WHERE repo_id = ? AND name = ? (get_branch_head_commit_id)
152 # Also covers list_branches_with_detail ORDER BY name — no in-memory sort needed.
153 Index("ix_musehub_branches_repo_name", "repo_id", "name"),
154 )
155
156 branch_id: Mapped[str] = mapped_column(String(128), primary_key=True)
157 repo_id: Mapped[str] = mapped_column(
158 String(128),
159 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
160 nullable=False,
161 )
162 name: Mapped[str] = mapped_column(String(255), nullable=False)
163 # Null until the first push sets the head.
164 head_commit_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
165
166 repo: Mapped[MusehubRepo] = relationship("MusehubRepo", back_populates="branches")
167
168
169 class MusehubCommit(MappedAsDataclass, Base):
170 """A content-addressed commit record — globally shared across all repos.
171
172 Commits are immutable objects identified solely by their SHA-256 hash.
173 They do not belong to any specific repo; repo membership is tracked in
174 MusehubCommitRef (the materialized reachability index, mirroring
175 MusehubObjectRef for blobs).
176
177 ``parent_ids`` is a JSON list so merge commits can carry two parents.
178 """
179
180 __tablename__ = "musehub_commits"
181
182 # --- Required fields ---
183 commit_id: Mapped[str] = mapped_column(String(128), primary_key=True)
184 branch: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
185 message: Mapped[str] = mapped_column(Text, nullable=False)
186 author: Mapped[str] = mapped_column(String(255), nullable=False)
187 timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
188
189 # --- Optional fields with Python-side defaults ---
190 # JSON list of parent commit IDs; two entries for merge commits.
191 parent_ids: Mapped[list[str]] = mapped_column(ARRAY(String(128)), nullable=False, default_factory=list)
192 snapshot_id: Mapped[str | None] = mapped_column(String(128), nullable=True, default=None)
193 # Provenance
194 agent_id: Mapped[str] = mapped_column(String(255), nullable=True, default="")
195 model_id: Mapped[str] = mapped_column(String(255), nullable=True, default="")
196 toolchain_id: Mapped[str] = mapped_column(String(255), nullable=True, default="")
197 # Original author branch (WireCommit.branch), distinct from push-target branch
198 commit_branch: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None)
199 # Signing
200 signature: Mapped[str] = mapped_column(Text, nullable=True, default="")
201 signer_public_key: Mapped[str] = mapped_column(Text, nullable=True, default="")
202 signer_key_id: Mapped[str] = mapped_column(String(255), nullable=True, default="")
203 # Semantic versioning
204 sem_ver_bump: Mapped[str] = mapped_column(String(10), nullable=True, default="none")
205 breaking_changes: Mapped[list[str]] = mapped_column(ARRAY(Text), nullable=True, default_factory=list)
206 # Review / CI
207 reviewed_by: Mapped[list[str]] = mapped_column(ARRAY(Text), nullable=True, default_factory=list)
208 test_runs: Mapped[int] = mapped_column(Integer, nullable=True, default=0)
209 prompt_hash: Mapped[str] = mapped_column(String(255), nullable=True, default="")
210 # Symbol-level delta blob sent by the Muse CLI on push.
211 structured_delta: Mapped[JSONObject | None] = mapped_column(JSONB, nullable=True, default=None)
212 # S3 URI of the canonical `commit <size>\0<json>` object. NULL until Phase 5 backfill.
213 storage_uri: Mapped[str | None] = mapped_column(String(2048), nullable=True, default=None)
214 created_at: Mapped[datetime] = mapped_column(
215 DateTime(timezone=True), nullable=False, default_factory=_utc_now, server_default=sa.func.now()
216 )
217
218
219 class MusehubCommitRef(Base):
220 """Materialized reachability index — which repos reference which commits.
221
222 Every repo that pushes (or pulls via fork) a commit gets one row here.
223 Mirrors MusehubObjectRef exactly: content-addressed objects are global;
224 per-repo membership is tracked separately for GC and listing.
225
226 Write rules:
227 - Push path: upsert (repo_id, commit_id) ON CONFLICT DO NOTHING.
228 - Fork path: copy ref rows from source repo to forked repo.
229 - GC path: delete ref rows for commits no longer reachable from any
230 branch head; then delete musehub_commits rows with no remaining refs.
231
232 The composite PK (repo_id, commit_id) makes all push upserts idempotent.
233 """
234
235 __tablename__ = "musehub_commit_refs"
236 __table_args__ = (
237 # List commits for a repo: WHERE repo_id = X
238 Index("ix_musehub_commit_refs_repo_id", "repo_id"),
239 )
240
241 repo_id: Mapped[str] = mapped_column(
242 String(128),
243 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
244 primary_key=True,
245 )
246 commit_id: Mapped[str] = mapped_column(
247 String(128),
248 ForeignKey("musehub_commits.commit_id", ondelete="CASCADE"),
249 primary_key=True,
250 )
251 created_at: Mapped[datetime] = mapped_column(
252 DateTime(timezone=True), nullable=False, default=_utc_now,
253 server_default=sa.text("now()"),
254 )
255
256 repo: Mapped[MusehubRepo] = relationship("MusehubRepo", back_populates="commit_refs")
257
258
259 class MusehubObject(Base):
260 """A globally content-addressed binary artifact stored in MuseHub.
261
262 Objects are owned by no single repo. ``object_id`` (bare SHA-256 hex) is
263 the primary key; identical bytes pushed by any number of repos share one
264 row. Per-repo membership is tracked in ``MusehubObjectRef`` (the
265 materialized reachability index).
266
267 ``storage_uri`` lifecycle:
268 ``"pending"`` — upload in progress; raw bytes in ``content_cache``.
269 ``"s3://…"`` — bytes live in R2/MinIO; ``content_cache`` is NULL.
270
271 ``content_cache`` is a transient BYTEA column written at push time and
272 cleared to NULL by the background upload task. It lets the fetch path
273 serve bytes immediately without waiting for the blob store.
274 """
275
276 __tablename__ = "musehub_objects"
277
278 # Content-addressed ID — bare SHA-256 hex, e.g. "abc123..."
279 object_id: Mapped[str] = mapped_column(String(128), primary_key=True)
280 # Relative path hint from the first repo to push this object.
281 # Not authoritative — per-repo path context lives in the snapshot manifest.
282 path: Mapped[str] = mapped_column(String(1024), nullable=False)
283 size_bytes: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
284 storage_uri: Mapped[str | None] = mapped_column(String(2048), nullable=True)
285 content_cache: Mapped[bytes | None] = mapped_column(
286 sa.LargeBinary, nullable=True, default=None
287 )
288 created_at: Mapped[datetime] = mapped_column(
289 DateTime(timezone=True), nullable=False, default=_utc_now
290 )
291 deleted_at: Mapped[datetime | None] = mapped_column(
292 DateTime(timezone=True), nullable=True, default=None
293 )
294
295 refs: Mapped[list[MusehubObjectRef]] = relationship(
296 "MusehubObjectRef", back_populates="object", cascade="all, delete-orphan"
297 )
298
299
300 class MusehubObjectRef(Base):
301 """Materialized reachability index — which repos reference which objects.
302
303 Every repo that pushes (or pulls via fork) an object gets one row here.
304 The table is the authoritative answer to "what objects does repo X own?"
305 and drives all per-repo operations:
306
307 - Storage quota: SUM(o.size_bytes) JOIN object_refs WHERE repo_id = X
308 - Targeted repair / decompress: JOIN object_refs WHERE repo_id = X
309 - GC eligibility: DELETE objects WHERE NOT EXISTS (SELECT 1 FROM object_refs)
310
311 Write rules:
312 - Push path: upsert (repo_id, object_id) ON CONFLICT DO NOTHING.
313 - Fork path: copy ref rows from source repo to forked repo.
314 - GC path: delete ref rows for objects no longer reachable from any
315 branch head; then delete musehub_objects rows with no remaining refs.
316
317 The composite PK (repo_id, object_id) makes all push upserts idempotent —
318 re-pushing the same object to the same repo is always O(1) and safe.
319 """
320
321 __tablename__ = "musehub_object_refs"
322
323 repo_id: Mapped[str] = mapped_column(
324 String(128),
325 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
326 primary_key=True,
327 )
328 object_id: Mapped[str] = mapped_column(
329 String(128),
330 ForeignKey("musehub_objects.object_id", ondelete="CASCADE"),
331 primary_key=True,
332 )
333
334 repo: Mapped[MusehubRepo] = relationship("MusehubRepo")
335 object: Mapped[MusehubObject] = relationship("MusehubObject", back_populates="refs")
336
337
338 class MusehubSnapshot(MappedAsDataclass, Base):
339 """Content-addressed file-tree record — globally shared across all repos.
340
341 A snapshot captures the full state of a repo at a point in time.
342 snapshot_id = sha256(manifest_bytes) — the hash IS the identity.
343 Snapshots do not belong to any specific repo; repo membership is tracked
344 in MusehubSnapshotRef (mirrors MusehubObjectRef).
345
346 SOURCE OF TRUTH (issue #63): the canonical object is stored in S3 as
347 ``snapshot <size>\\0<json>`` at the URI in ``storage_uri``. Wire fetch
348 reads from S3 first, falling back to DB only when storage_uri is null
349 (pre-backfill rows).
350
351 ``manifest_blob`` is a msgpack DB cache of the manifest for fast queries
352 (intel providers, GC, governance). It is NOT the source of truth.
353 ``delta_blob`` stores the push-receive delta for efficient reconstruction;
354 it is NOT used in the wire-serve path after Phase 4.
355
356 ``entry_count`` mirrors ``len(manifest)`` at write time for O(1) counts.
357 """
358
359 __tablename__ = "musehub_snapshots"
360
361 # --- Required fields ---
362 snapshot_id: Mapped[str] = mapped_column(String(128), primary_key=True)
363 # DB cache only — msgpack-serialized {path: object_id} manifest for fast reads
364 # (intel providers, GC, governance). S3 object at storage_uri is canonical.
365 # NULL for delta-only push snapshots; use _reconstruct_manifest() for those.
366 manifest_blob: Mapped[bytes | None] = mapped_column(sa.LargeBinary, nullable=True)
367
368 # Delta compression — populated at push time from the mpack wire format.
369 # parent_snapshot_id: the snapshot this delta was applied on top of.
370 # delta_blob: msgpack-serialized {path: object_id} of files added/changed.
371 # When set, wire_fetch_mpack sends the delta instead of the full manifest,
372 # reducing clone wire size from O(commits × files) to O(commits × delta).
373 parent_snapshot_id: Mapped[str | None] = mapped_column(String(128), nullable=True, default=None)
374 delta_blob: Mapped[bytes | None] = mapped_column(sa.LargeBinary, nullable=True, default=None)
375
376 # --- Optional fields with Python-side defaults ---
377 # Sorted list of workspace-relative directory paths tracked at snapshot time.
378 directories: Mapped[list[str]] = mapped_column(ARRAY(Text), nullable=False, default_factory=list)
379 # Number of tracked files in this snapshot. Denormalised from len(manifest).
380 entry_count: Mapped[int] = mapped_column(Integer(), nullable=False, default=0)
381 # S3 URI of the canonical `snapshot <size>\0<json>` object. NULL until Phase 5 backfill.
382 storage_uri: Mapped[str | None] = mapped_column(String(2048), nullable=True, default=None)
383 created_at: Mapped[datetime] = mapped_column(
384 DateTime(timezone=True), nullable=False, default_factory=_utc_now
385 )
386
387
388 class MusehubSnapshotRef(Base):
389 """Materialized reachability index — which repos reference which snapshots.
390
391 Mirrors MusehubObjectRef and MusehubCommitRef exactly. Snapshots are
392 content-addressed and globally shared; per-repo membership is tracked here.
393
394 Write rules:
395 - Push path: upsert (repo_id, snapshot_id) ON CONFLICT DO NOTHING.
396 - GC path: delete snapshots with no remaining refs.
397 """
398
399 __tablename__ = "musehub_snapshot_refs"
400 __table_args__ = (
401 Index("ix_musehub_snapshot_refs_repo_id", "repo_id"),
402 )
403
404 repo_id: Mapped[str] = mapped_column(
405 String(128),
406 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
407 primary_key=True,
408 )
409 snapshot_id: Mapped[str] = mapped_column(
410 String(128),
411 ForeignKey("musehub_snapshots.snapshot_id", ondelete="CASCADE"),
412 primary_key=True,
413 )
414 created_at: Mapped[datetime] = mapped_column(
415 DateTime(timezone=True), nullable=False, default=_utc_now,
416 server_default=sa.text("now()"),
417 )
418
419
420 class MusehubSnapshotEntry(Base):
421 """Normalized per-file row within a snapshot.
422
423 Stores the flattened file tree as individual rows keyed by
424 (snapshot_id, path) rather than as a msgpack blob. Enables
425 efficient diff queries without decoding the full manifest.
426 """
427
428 __tablename__ = "musehub_snapshot_entries"
429
430 snapshot_id: Mapped[str] = mapped_column(
431 String(128),
432 ForeignKey("musehub_snapshots.snapshot_id", ondelete="CASCADE"),
433 primary_key=True,
434 )
435 path: Mapped[str] = mapped_column(String(4096), primary_key=True)
436 object_id: Mapped[str] = mapped_column(String(128), nullable=False)
437 size_bytes: Mapped[int] = mapped_column(Integer(), nullable=False, default=0)
438
439
440 class MusehubSession(MappedAsDataclass, Base):
441 """A recording session record pushed to MuseHub from the CLI.
442
443 Sessions capture the creative context of a recording period: who was
444 present, where they recorded, what they intended to create, which commits
445 were made, and any closing notes. Maps to ``muse session show`` locally.
446
447 ``commits`` is a JSON list of Muse commit IDs associated with the session.
448 ``participants`` is a JSON list of participant name strings.
449 """
450
451 __tablename__ = "musehub_sessions"
452
453 # --- Required fields ---
454 session_id: Mapped[str] = mapped_column(String(128), primary_key=True)
455 repo_id: Mapped[str] = mapped_column(
456 String(128),
457 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
458 nullable=False,
459 index=True,
460 )
461 started_at: Mapped[datetime] = mapped_column(
462 DateTime(timezone=True), nullable=False, index=True
463 )
464
465 # --- Optional fields ---
466 schema_version: Mapped[str] = mapped_column(String(10), nullable=False, default="1", server_default="1")
467 ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, default=None)
468 participants: Mapped[list[str]] = mapped_column(ARRAY(Text), nullable=False, default_factory=list)
469 location: Mapped[str] = mapped_column(String(500), nullable=False, default="")
470 intent: Mapped[str] = mapped_column(Text, nullable=False, default="")
471 commits: Mapped[list[str]] = mapped_column(ARRAY(String(128)), nullable=False, default_factory=list)
472 notes: Mapped[str] = mapped_column(Text, nullable=False, default="")
473 is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, index=True, server_default=sa.false())
474 created_at: Mapped[datetime] = mapped_column(
475 DateTime(timezone=True), nullable=False, default_factory=_utc_now
476 )
477
478 repo: Mapped[MusehubRepo] = relationship("MusehubRepo", back_populates="sessions", init=False)
479
480
481 class MusehubWireTag(Base):
482 """A lightweight tag pushed from a Muse CLI client via the wire protocol.
483
484 Wire tags are distinct from version releases: they carry semantic labels
485 such as ``emotion:joyful`` or ``section:verse`` that annotate commits
486 without implying a versioned release. The ``tag`` field is the raw
487 string pushed by the client (e.g. ``emotion:joyful``).
488
489 The ``(repo_id, tag)`` pair is unique — a second push of the same tag
490 for the same commit is a no-op (upsert at the service layer).
491 """
492
493 __tablename__ = "musehub_wire_tags"
494 __table_args__ = (UniqueConstraint("repo_id", "tag", name="uq_musehub_wire_tags_repo_tag"),)
495
496 tag_id: Mapped[str] = mapped_column(String(128), primary_key=True)
497 repo_id: Mapped[str] = mapped_column(
498 String(128),
499 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
500 nullable=False,
501 index=True,
502 )
503 # The commit this tag points to.
504 commit_id: Mapped[str] = mapped_column(String(128), nullable=False)
505 # Raw tag label, e.g. "emotion:joyful", "section:verse", "v1.0-wip".
506 tag: Mapped[str] = mapped_column(String(500), nullable=False, index=True)
507 created_at: Mapped[datetime] = mapped_column(
508 DateTime(timezone=True), nullable=False, default=_utc_now
509 )
510
511 repo: Mapped[MusehubRepo] = relationship("MusehubRepo", back_populates="wire_tags")
512
513
514 class MusehubVersionTag(Base):
515 """A semantic-version tag pushed from a Muse CLI client via the wire protocol.
516
517 Version tags carry a semver string (e.g. ``v1.2.0``) that points to a
518 specific commit. They are distinct from wire tags: they always parse as
519 valid semver and are used to mark releases. The ``(repo_id, tag)`` pair is
520 unique — re-pushing the same tag updates the commit pointer (upsert).
521 """
522
523 __tablename__ = "musehub_version_tags"
524 __table_args__ = (UniqueConstraint("repo_id", "tag", name="uq_musehub_version_tags_repo_tag"),)
525
526 tag_id: Mapped[str] = mapped_column(String(128), primary_key=True)
527 repo_id: Mapped[str] = mapped_column(
528 String(128),
529 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
530 nullable=False,
531 index=True,
532 )
533 commit_id: Mapped[str] = mapped_column(String(128), nullable=False)
534 tag: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
535 semver_major: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
536 semver_minor: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
537 semver_patch: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
538 semver_pre: Mapped[str] = mapped_column(String(128), nullable=False, default="")
539 semver_build: Mapped[str] = mapped_column(String(128), nullable=False, default="")
540 author: Mapped[str] = mapped_column(String(255), nullable=False, default="")
541 message: Mapped[str] = mapped_column(Text, nullable=False, default="")
542 created_at: Mapped[datetime] = mapped_column(
543 DateTime(timezone=True), nullable=False, default=_utc_now
544 )
545
546 repo: Mapped[MusehubRepo] = relationship("MusehubRepo", back_populates="version_tags")
547
548
549 class MusehubBridgeMirror(Base):
550 """A Git mirror registration for a MuseHub repo.
551
552 Each row links a MuseHub repo to a remote Git repository URL and records
553 the state of the last export (Muse→Git) and import (Git→Muse) operations.
554
555 ``direction`` controls which sync directions are active:
556 ``"export"`` — Muse commits are pushed to Git only
557 ``"import"`` — Git commits are pulled into Muse only
558 ``"bidirectional"`` — both directions are synced
559
560 The unique constraint on ``(repo_id, git_remote_url)`` means a given Git
561 URL can only be registered once per Muse repo.
562 """
563
564 __tablename__ = "musehub_bridge_mirrors"
565 __table_args__ = (
566 UniqueConstraint("repo_id", "git_remote_url", name="uq_bridge_mirror_repo_url"),
567 )
568
569 id: Mapped[str] = mapped_column(String(128), primary_key=True)
570 repo_id: Mapped[str] = mapped_column(
571 String(128),
572 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
573 nullable=False,
574 index=True,
575 )
576 git_remote_url: Mapped[str] = mapped_column(String(2048), nullable=False)
577 git_branch: Mapped[str] = mapped_column(String(255), nullable=False, default="muse-mirror", server_default="muse-mirror")
578 # "export" | "import" | "bidirectional"
579 direction: Mapped[str] = mapped_column(String(20), nullable=False)
580 last_export_muse_commit_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
581 last_export_git_sha: Mapped[str | None] = mapped_column(String(128), nullable=True)
582 last_export_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
583 last_import_git_sha: Mapped[str | None] = mapped_column(String(128), nullable=True)
584 last_import_muse_commit_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
585 last_import_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
586 auto_export: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=sa.false())
587 created_by: Mapped[str] = mapped_column(String(255), nullable=False)
588 created_at: Mapped[datetime] = mapped_column(
589 DateTime(timezone=True), nullable=False, default=_utc_now
590 )
591
592 repo: Mapped[MusehubRepo] = relationship("MusehubRepo")
593
594
595 class MusehubMist(MappedAsDataclass, Base):
596 """A Muse Mist — content-addressed, signed, forkable single-artifact share.
597
598 A Mist is the Muse answer to GitHub gists: a single artifact (code, MIDI,
599 JSON Schema, ABI, or any binary blob) stored in the Muse object
600 store, identified by a content-derived 12-character base-58 mist_id, signed
601 with the author's Ed25519 key, and version-controlled via a Muse repo with
602 ``domain="mist"``.
603
604 Unlike a gist, a Mist:
605 - Has a Muse repo behind it (full VCS: branches, commits, proposals).
606 - Is domain-typed via ``artifact_type`` — the hub knows whether the
607 content is code, MIDI, a JSON Schema, or a Solidity ABI.
608 - Carries agent provenance (``agent_id``, ``model_id``) for AI-authored mists.
609 - Has semantic intelligence for code mists (symbol anchors cached in
610 ``symbol_anchors`` JSON column).
611 - Is forkable via ``fork_parent_id`` self-referential FK.
612 - Is embeddable via ``/{owner}/mists/{mist_id}/embed``.
613 - Is MCP-accessible as ``muse:///{owner}/mists/{mist_id}``.
614
615 The ``mist_id`` is the first 12 characters of the base-58 encoding of the
616 SHA-256 digest of the initial artifact bytes — globally unique, stable
617 across renames, and collision-resistant (~70 bits of entropy).
618
619 Fork depth
620 ----------
621 ``fork_depth`` is 0 for originals and increments by 1 per fork tier.
622 The API enforces a hard limit of 5 fork tiers to prevent fork chains
623 from growing unboundedly.
624
625 Visibility
626 ----------
627 ``"public"`` mists appear in the explore feed and handle list.
628 ``"secret"`` mists are accessible only via direct URL — they are not
629 indexed and do not appear in any listing endpoint.
630
631 Counters
632 --------
633 ``view_count``, ``fork_count``, and ``embed_count`` are denormalized and
634 updated atomically (``UPDATE ... SET col = col + 1``) to avoid SELECT +
635 UPDATE races.
636 """
637
638 __tablename__ = "musehub_mists"
639 __table_args__ = (
640 # List page: owner's public mists, newest first
641 Index("ix_musehub_mists_owner_visibility", "owner", "visibility"),
642 # Pagination: owner's mists sorted by created_at
643 Index("ix_musehub_mists_owner_created_at", "owner", "created_at"),
644 # Fork graph: find all forks of a given mist
645 Index("ix_musehub_mists_fork_parent_id", "fork_parent_id"),
646 # Explore page: public mists by type, newest first
647 Index("ix_musehub_mists_artifact_type_created_at", "artifact_type", "created_at"),
648 )
649
650 # --- Required fields ---
651 # Primary key — content-addressed 12-character base-58 string (globally unique)
652 mist_id: Mapped[str] = mapped_column(String(128), primary_key=True)
653 repo_id: Mapped[str] = mapped_column(
654 String(128),
655 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
656 nullable=False,
657 index=True,
658 )
659 owner: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
660 filename: Mapped[str] = mapped_column(String(255), nullable=False)
661 content: Mapped[str] = mapped_column(Text, nullable=False)
662
663 # --- Optional fields ---
664 artifact_type: Mapped[str] = mapped_column(
665 String(20), nullable=False, default="unknown", index=True
666 )
667 language: Mapped[str] = mapped_column(String(64), nullable=False, default="")
668 title: Mapped[str] = mapped_column(String(500), nullable=False, default="")
669 description: Mapped[str] = mapped_column(Text, nullable=False, default="")
670 size_bytes: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
671 commit_id: Mapped[str | None] = mapped_column(String(128), nullable=True, default=None)
672 snapshot_id: Mapped[str | None] = mapped_column(String(128), nullable=True, default=None)
673 version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
674 agent_id: Mapped[str] = mapped_column(String(255), nullable=False, default="")
675 model_id: Mapped[str] = mapped_column(String(255), nullable=False, default="")
676 gpg_signature: Mapped[str | None] = mapped_column(Text, nullable=True, default=None)
677 fork_parent_id: Mapped[str | None] = mapped_column(
678 String(128),
679 ForeignKey("musehub_mists.mist_id", ondelete="SET NULL"),
680 nullable=True,
681 default=None,
682 )
683 fork_depth: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
684 fork_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
685 view_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
686 embed_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
687 visibility: Mapped[str] = mapped_column(String(10), nullable=False, default="public", server_default="public")
688 tags: Mapped[list[str]] = mapped_column(ARRAY(Text), nullable=False, default_factory=list)
689 symbol_anchors: Mapped[list[str]] = mapped_column(ARRAY(Text), nullable=False, default_factory=list)
690 created_at: Mapped[datetime] = mapped_column(
691 DateTime(timezone=True), nullable=False, default_factory=_utc_now
692 )
693 updated_at: Mapped[datetime] = mapped_column(
694 DateTime(timezone=True), nullable=False, default_factory=_utc_now, onupdate=_utc_now
695 )
696
697 fork_parent: Mapped["MusehubMist | None"] = relationship(
698 "MusehubMist",
699 remote_side="MusehubMist.mist_id",
700 foreign_keys=[fork_parent_id],
701 back_populates="forks",
702 init=False,
703 default=None,
704 )
705 forks: Mapped[list["MusehubMist"]] = relationship(
706 "MusehubMist",
707 foreign_keys=[fork_parent_id],
708 back_populates="fork_parent",
709 cascade="save-update, merge",
710 init=False,
711 default_factory=list,
712 )
713 repo: Mapped[MusehubRepo] = relationship("MusehubRepo", init=False)
714
715
716 class MusehubMPackIndex(Base):
717 """MPack index — maps every commit, snapshot, and object to the mpack containing it.
718
719 Written by ``process_mpack_index_job`` after every push. Enables the
720 fetch path to locate covering mpacks for any entity_id without O(N)
721 individual GET calls.
722
723 Entries are content-addressed globally — no repo_id.
724 Primary key is (entity_id, mpack_id). ``on_conflict_do_nothing`` is idempotent.
725 entity_type is one of: "object", "commit", "snapshot".
726 """
727
728 __tablename__ = "musehub_mpack_index"
729 __table_args__ = (
730 Index("ix_musehub_mpack_index_entity_id", "entity_id"),
731 )
732
733 entity_id: Mapped[str] = mapped_column(String(128), primary_key=True)
734 mpack_id: Mapped[str] = mapped_column(String(128), primary_key=True)
735 entity_type: Mapped[str] = mapped_column(
736 String(16), nullable=False, server_default="object"
737 )
738 created_at: Mapped[datetime] = mapped_column(
739 DateTime(timezone=True), nullable=False, default=_utc_now,
740 server_default=sa.text("now()"),
741 )
742 byte_offset: Mapped[int | None] = mapped_column(sa.BigInteger(), nullable=True)
743 byte_length: Mapped[int | None] = mapped_column(sa.Integer(), nullable=True)
744
745
746 # Backwards-compatible alias — remove once all call sites are updated.
747 MusehubPackIndex = MusehubMPackIndex
748
749
750 class MusehubFetchMPackCache(Base):
751 """Pre-built fetch mpack cache — one row per (repo, tip commit).
752
753 Populated by ``process_fetch_mpack_prebuild_job`` after every push.
754 ``wire_fetch_mpack`` checks this table first for fresh-clone requests
755 (want=[tip], have=[]) and returns a presigned URL immediately on hit,
756 bypassing the synchronous mpack build that causes Cloudflare 524s on
757 large repos.
758
759 ``expires_at`` defaults to 7 days after creation. The ``gc`` job
760 deletes expired rows and their corresponding R2 objects.
761 """
762
763 __tablename__ = "musehub_fetch_mpack_cache"
764 __table_args__ = (
765 UniqueConstraint("repo_id", "tip_commit_id", name="uq_fetch_mpack_cache_repo_tip"),
766 Index("ix_fetch_mpack_cache_expires_at", "expires_at"),
767 )
768
769 cache_id: Mapped[str] = mapped_column(String(128), primary_key=True)
770 repo_id: Mapped[str] = mapped_column(String(128), nullable=False)
771 tip_commit_id: Mapped[str] = mapped_column(String(128), nullable=False)
772 mpack_id: Mapped[str] = mapped_column(String(128), nullable=False)
773 created_at: Mapped[datetime] = mapped_column(
774 DateTime(timezone=True), nullable=False, default=_utc_now,
775 server_default=sa.text("now()"),
776 )
777 expires_at: Mapped[datetime] = mapped_column(
778 DateTime(timezone=True), nullable=False,
779 server_default=sa.text("now() + interval '7 days'"),
780 )
781
782
783 class MusehubCommitGraph(Base):
784 """Precomputed commit graph — enables O(frontier) BFS instead of O(N commits).
785
786 Commits are content-addressed (like snapshots) so this table is global —
787 no repo_id. ``_walk_commit_delta`` queries in bulk (one query per BFS
788 frontier) instead of one ``session.get(MusehubCommit, cid)`` per commit.
789
790 ``generation`` is the topological depth from the root: root=0, each commit
791 is max(parent generations) + 1. The index on generation lets GC and stats
792 queries run forward scans instead of full-table scans.
793 """
794
795 __tablename__ = "musehub_commit_graph"
796 __table_args__ = (
797 Index("ix_musehub_commit_graph_generation", "generation"),
798 )
799
800 commit_id: Mapped[str] = mapped_column(String(128), primary_key=True)
801 parent_ids: Mapped[list[str]] = mapped_column(
802 ARRAY(Text), nullable=False, default=list, server_default="{}"
803 )
804 generation: Mapped[int] = mapped_column(
805 sa.BigInteger, nullable=False, default=0, server_default="0"
806 )
807 snapshot_id: Mapped[str | None] = mapped_column(String(128), nullable=True, default=None)
808 created_at: Mapped[datetime] = mapped_column(
809 DateTime(timezone=True), nullable=False, default=_utc_now,
810 server_default=sa.text("now()"),
811 )
File History 5 commits
sha256:5dfc96524e3921eb9acb8372241b6bec70b5f3e6598f79099a0ead16ff7cbb75 feat(phase1): add musehub_fetch_mpack_cache table (issue #9… Sonnet 4.6 patch 34 days ago
sha256:400438cf8bc700a611f1ba798aa9def68290f487dc19f7dbf317985ad17050c9 chore: delete muse/prose domain — hallucinated, never existed Sonnet 4.6 minor 35 days ago
sha256:e35be48854f182f7bf02dc6cc0f58d22b3de3a544b570c0e2bc53f9e75a3607d feat(phase6): remove delta_blob path, dead imports, add fal… Sonnet 4.6 minor 48 days ago
sha256:e3296c39859814a0ae8b688be26ec1a24b3895fec467d9bdaefb7431e5ae3a93 test(phase1): failing tests + DB schema for object store in… Sonnet 4.6 minor 48 days ago
sha256:302574ddba13c9a20694c0fb051176eef4896f943b63bc458df886633b1bfcd6 feat: mpack byte-range index — store byte_offset/byte_lengt… Sonnet 4.6 minor 48 days ago