gabriel / musehub public
musehub_repo_models.py python
824 lines 38.9 KB Cold
Raw
sha256:5dfc96524e3921eb9acb8372241b6bec70b5f3e6598f79099a0ead16ff7cbb75 feat(phase1): add musehub_fetch_mpack_cache table (issue #9… Sonnet 4.6 patch 131 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 # Exact, unnormalized ISO-8601 string as originally sent by the client
194 # (or generated server-side at write time), preserved byte-for-byte.
195 # `timestamp` above is a Postgres timestamptz and gets UTC-normalized on
196 # write -- reconstructing wire bytes from it produces a DIFFERENT string
197 # than the client originally hashed for non-UTC-authored commits, which
198 # breaks the commit's content-addressed id (musehub#195). NULL for rows
199 # written before this column existed; the serve path (_to_wire_commit)
200 # falls back to `timestamp.isoformat()` for those and verifies the
201 # result actually reproduces commit_id, failing closed if it doesn't.
202 committed_at_raw: Mapped[str | None] = mapped_column(Text, nullable=True, default=None)
203 # Provenance
204 agent_id: Mapped[str] = mapped_column(String(255), nullable=True, default="")
205 model_id: Mapped[str] = mapped_column(String(255), nullable=True, default="")
206 toolchain_id: Mapped[str] = mapped_column(String(255), nullable=True, default="")
207 # Original author branch (WireCommit.branch), distinct from push-target branch
208 commit_branch: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None)
209 # Signing
210 signature: Mapped[str] = mapped_column(Text, nullable=True, default="")
211 signer_public_key: Mapped[str] = mapped_column(Text, nullable=True, default="")
212 signer_key_id: Mapped[str] = mapped_column(String(255), nullable=True, default="")
213 # Semantic versioning
214 sem_ver_bump: Mapped[str] = mapped_column(String(10), nullable=True, default="none")
215 breaking_changes: Mapped[list[str]] = mapped_column(ARRAY(Text), nullable=True, default_factory=list)
216 # Review / CI
217 reviewed_by: Mapped[list[str]] = mapped_column(ARRAY(Text), nullable=True, default_factory=list)
218 test_runs: Mapped[int] = mapped_column(Integer, nullable=True, default=0)
219 prompt_hash: Mapped[str] = mapped_column(String(255), nullable=True, default="")
220 # Symbol-level delta blob sent by the Muse CLI on push.
221 structured_delta: Mapped[JSONObject | None] = mapped_column(JSONB, nullable=True, default=None)
222 # S3 URI of the canonical `commit <size>\0<json>` object. NULL until Phase 5 backfill.
223 storage_uri: Mapped[str | None] = mapped_column(String(2048), nullable=True, default=None)
224 created_at: Mapped[datetime] = mapped_column(
225 DateTime(timezone=True), nullable=False, default_factory=_utc_now, server_default=sa.func.now()
226 )
227
228
229 class MusehubCommitRef(Base):
230 """Materialized reachability index — which repos reference which commits.
231
232 Every repo that pushes (or pulls via fork) a commit gets one row here.
233 Mirrors MusehubObjectRef exactly: content-addressed objects are global;
234 per-repo membership is tracked separately for GC and listing.
235
236 Write rules:
237 - Push path: upsert (repo_id, commit_id) ON CONFLICT DO NOTHING.
238 - Fork path: copy ref rows from source repo to forked repo.
239 - GC path: delete ref rows for commits no longer reachable from any
240 branch head; then delete musehub_commits rows with no remaining refs.
241
242 The composite PK (repo_id, commit_id) makes all push upserts idempotent.
243 """
244
245 __tablename__ = "musehub_commit_refs"
246 __table_args__ = (
247 # List commits for a repo: WHERE repo_id = X
248 Index("ix_musehub_commit_refs_repo_id", "repo_id"),
249 )
250
251 repo_id: Mapped[str] = mapped_column(
252 String(128),
253 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
254 primary_key=True,
255 )
256 commit_id: Mapped[str] = mapped_column(
257 String(128),
258 ForeignKey("musehub_commits.commit_id", ondelete="CASCADE"),
259 primary_key=True,
260 )
261 created_at: Mapped[datetime] = mapped_column(
262 DateTime(timezone=True), nullable=False, default=_utc_now,
263 server_default=sa.text("now()"),
264 )
265
266 repo: Mapped[MusehubRepo] = relationship("MusehubRepo", back_populates="commit_refs")
267
268
269 class MusehubObject(Base):
270 """A globally content-addressed binary artifact stored in MuseHub.
271
272 Objects are owned by no single repo. ``object_id`` (bare SHA-256 hex) is
273 the primary key; identical bytes pushed by any number of repos share one
274 row. Per-repo membership is tracked in ``MusehubObjectRef`` (the
275 materialized reachability index).
276
277 ``storage_uri`` lifecycle:
278 ``"pending"`` — upload in progress; raw bytes in ``content_cache``.
279 ``"s3://…"`` — bytes live in R2/MinIO; ``content_cache`` is NULL.
280
281 ``content_cache`` is a transient BYTEA column written at push time and
282 cleared to NULL by the background upload task. It lets the fetch path
283 serve bytes immediately without waiting for the blob store.
284 """
285
286 __tablename__ = "musehub_objects"
287
288 # Content-addressed ID — bare SHA-256 hex, e.g. "abc123..."
289 object_id: Mapped[str] = mapped_column(String(128), primary_key=True)
290 # Relative path hint from the first repo to push this object.
291 # Not authoritative — per-repo path context lives in the snapshot manifest.
292 path: Mapped[str] = mapped_column(String(1024), nullable=False)
293 size_bytes: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
294 storage_uri: Mapped[str | None] = mapped_column(String(2048), nullable=True)
295 content_cache: Mapped[bytes | None] = mapped_column(
296 sa.LargeBinary, nullable=True, default=None
297 )
298 created_at: Mapped[datetime] = mapped_column(
299 DateTime(timezone=True), nullable=False, default=_utc_now
300 )
301 deleted_at: Mapped[datetime | None] = mapped_column(
302 DateTime(timezone=True), nullable=True, default=None
303 )
304
305 refs: Mapped[list[MusehubObjectRef]] = relationship(
306 "MusehubObjectRef", back_populates="object", cascade="all, delete-orphan"
307 )
308
309
310 class MusehubObjectRef(Base):
311 """Materialized reachability index — which repos reference which objects.
312
313 Every repo that pushes (or pulls via fork) an object gets one row here.
314 The table is the authoritative answer to "what objects does repo X own?"
315 and drives all per-repo operations:
316
317 - Storage quota: SUM(o.size_bytes) JOIN object_refs WHERE repo_id = X
318 - Targeted repair / decompress: JOIN object_refs WHERE repo_id = X
319 - GC eligibility: DELETE objects WHERE NOT EXISTS (SELECT 1 FROM object_refs)
320
321 Write rules:
322 - Push path: upsert (repo_id, object_id) ON CONFLICT DO NOTHING.
323 - Fork path: copy ref rows from source repo to forked repo.
324 - GC path: delete ref rows for objects no longer reachable from any
325 branch head; then delete musehub_objects rows with no remaining refs.
326
327 The composite PK (repo_id, object_id) makes all push upserts idempotent —
328 re-pushing the same object to the same repo is always O(1) and safe.
329 """
330
331 __tablename__ = "musehub_object_refs"
332
333 repo_id: Mapped[str] = mapped_column(
334 String(128),
335 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
336 primary_key=True,
337 )
338 object_id: Mapped[str] = mapped_column(
339 String(128),
340 ForeignKey("musehub_objects.object_id", ondelete="CASCADE"),
341 primary_key=True,
342 )
343
344 repo: Mapped[MusehubRepo] = relationship("MusehubRepo")
345 object: Mapped[MusehubObject] = relationship("MusehubObject", back_populates="refs")
346
347
348 class MusehubSnapshot(MappedAsDataclass, Base):
349 """Content-addressed file-tree record — globally shared across all repos.
350
351 A snapshot captures the full state of a repo at a point in time.
352 snapshot_id = sha256(manifest_bytes) — the hash IS the identity.
353 Snapshots do not belong to any specific repo; repo membership is tracked
354 in MusehubSnapshotRef (mirrors MusehubObjectRef).
355
356 SOURCE OF TRUTH (issue #63): the canonical object is stored in S3 as
357 ``snapshot <size>\\0<json>`` at the URI in ``storage_uri``. Wire fetch
358 reads from S3 first, falling back to DB only when storage_uri is null
359 (pre-backfill rows).
360
361 ``manifest_blob`` is a msgpack DB cache of the manifest for fast queries
362 (intel providers, GC, governance). It is NOT the source of truth.
363 ``delta_blob`` stores the push-receive delta for efficient reconstruction;
364 it is NOT used in the wire-serve path after Phase 4.
365
366 ``entry_count`` mirrors ``len(manifest)`` at write time for O(1) counts.
367 """
368
369 __tablename__ = "musehub_snapshots"
370
371 # --- Required fields ---
372 snapshot_id: Mapped[str] = mapped_column(String(128), primary_key=True)
373 # DB cache only — msgpack-serialized {path: object_id} manifest for fast reads
374 # (intel providers, GC, governance). S3 object at storage_uri is canonical.
375 # NULL for delta-only push snapshots; use _reconstruct_manifest() for those.
376 manifest_blob: Mapped[bytes | None] = mapped_column(sa.LargeBinary, nullable=True)
377
378 # Delta compression — populated at push time from the mpack wire format.
379 # parent_snapshot_id: the snapshot this delta was applied on top of.
380 # delta_blob: msgpack-serialized {path: object_id} of files added/changed.
381 # When set, wire_fetch_mpack sends the delta instead of the full manifest,
382 # reducing clone wire size from O(commits × files) to O(commits × delta).
383 parent_snapshot_id: Mapped[str | None] = mapped_column(String(128), nullable=True, default=None)
384 delta_blob: Mapped[bytes | None] = mapped_column(sa.LargeBinary, nullable=True, default=None)
385
386 # --- Optional fields with Python-side defaults ---
387 # Sorted list of workspace-relative directory paths tracked at snapshot time.
388 directories: Mapped[list[str]] = mapped_column(ARRAY(Text), nullable=False, default_factory=list)
389 # Number of tracked files in this snapshot. Denormalised from len(manifest).
390 entry_count: Mapped[int] = mapped_column(Integer(), nullable=False, default=0)
391 # S3 URI of the canonical `snapshot <size>\0<json>` object. NULL until Phase 5 backfill.
392 storage_uri: Mapped[str | None] = mapped_column(String(2048), nullable=True, default=None)
393 created_at: Mapped[datetime] = mapped_column(
394 DateTime(timezone=True), nullable=False, default_factory=_utc_now
395 )
396
397
398 class MusehubSnapshotRef(Base):
399 """Materialized reachability index — which repos reference which snapshots.
400
401 Mirrors MusehubObjectRef and MusehubCommitRef exactly. Snapshots are
402 content-addressed and globally shared; per-repo membership is tracked here.
403
404 Write rules:
405 - Push path: upsert (repo_id, snapshot_id) ON CONFLICT DO NOTHING.
406 - GC path: delete snapshots with no remaining refs.
407 """
408
409 __tablename__ = "musehub_snapshot_refs"
410 __table_args__ = (
411 Index("ix_musehub_snapshot_refs_repo_id", "repo_id"),
412 )
413
414 repo_id: Mapped[str] = mapped_column(
415 String(128),
416 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
417 primary_key=True,
418 )
419 snapshot_id: Mapped[str] = mapped_column(
420 String(128),
421 ForeignKey("musehub_snapshots.snapshot_id", ondelete="CASCADE"),
422 primary_key=True,
423 )
424 created_at: Mapped[datetime] = mapped_column(
425 DateTime(timezone=True), nullable=False, default=_utc_now,
426 server_default=sa.text("now()"),
427 )
428
429
430 class MusehubSnapshotEntry(Base):
431 """Normalized per-file row within a snapshot.
432
433 Stores the flattened file tree as individual rows keyed by
434 (snapshot_id, path) rather than as a msgpack blob. Enables
435 efficient diff queries without decoding the full manifest.
436 """
437
438 __tablename__ = "musehub_snapshot_entries"
439
440 snapshot_id: Mapped[str] = mapped_column(
441 String(128),
442 ForeignKey("musehub_snapshots.snapshot_id", ondelete="CASCADE"),
443 primary_key=True,
444 )
445 path: Mapped[str] = mapped_column(String(4096), primary_key=True)
446 object_id: Mapped[str] = mapped_column(String(128), nullable=False)
447 size_bytes: Mapped[int] = mapped_column(Integer(), nullable=False, default=0)
448
449
450 class MusehubSession(MappedAsDataclass, Base):
451 """A recording session record pushed to MuseHub from the CLI.
452
453 Sessions capture the creative context of a recording period: who was
454 present, where they recorded, what they intended to create, which commits
455 were made, and any closing notes. Maps to ``muse session show`` locally.
456
457 ``commits`` is a JSON list of Muse commit IDs associated with the session.
458 ``participants`` is a JSON list of participant name strings.
459 """
460
461 __tablename__ = "musehub_sessions"
462
463 # --- Required fields ---
464 session_id: Mapped[str] = mapped_column(String(128), primary_key=True)
465 repo_id: Mapped[str] = mapped_column(
466 String(128),
467 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
468 nullable=False,
469 index=True,
470 )
471 started_at: Mapped[datetime] = mapped_column(
472 DateTime(timezone=True), nullable=False, index=True
473 )
474
475 # --- Optional fields ---
476 schema_version: Mapped[str] = mapped_column(String(10), nullable=False, default="1", server_default="1")
477 ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, default=None)
478 participants: Mapped[list[str]] = mapped_column(ARRAY(Text), nullable=False, default_factory=list)
479 location: Mapped[str] = mapped_column(String(500), nullable=False, default="")
480 intent: Mapped[str] = mapped_column(Text, nullable=False, default="")
481 commits: Mapped[list[str]] = mapped_column(ARRAY(String(128)), nullable=False, default_factory=list)
482 notes: Mapped[str] = mapped_column(Text, nullable=False, default="")
483 is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, index=True, server_default=sa.false())
484 created_at: Mapped[datetime] = mapped_column(
485 DateTime(timezone=True), nullable=False, default_factory=_utc_now
486 )
487
488 repo: Mapped[MusehubRepo] = relationship("MusehubRepo", back_populates="sessions", init=False)
489
490
491 class MusehubWireTag(Base):
492 """A lightweight tag pushed from a Muse CLI client via the wire protocol.
493
494 Wire tags are distinct from version releases: they carry semantic labels
495 such as ``emotion:joyful`` or ``section:verse`` that annotate commits
496 without implying a versioned release. The ``tag`` field is the raw
497 string pushed by the client (e.g. ``emotion:joyful``).
498
499 The ``(repo_id, tag)`` pair is unique — a second push of the same tag
500 for the same commit is a no-op (upsert at the service layer).
501 """
502
503 __tablename__ = "musehub_wire_tags"
504 __table_args__ = (UniqueConstraint("repo_id", "tag", name="uq_musehub_wire_tags_repo_tag"),)
505
506 tag_id: Mapped[str] = mapped_column(String(128), primary_key=True)
507 repo_id: Mapped[str] = mapped_column(
508 String(128),
509 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
510 nullable=False,
511 index=True,
512 )
513 # The commit this tag points to.
514 commit_id: Mapped[str] = mapped_column(String(128), nullable=False)
515 # Raw tag label, e.g. "emotion:joyful", "section:verse", "v1.0-wip".
516 tag: Mapped[str] = mapped_column(String(500), nullable=False, index=True)
517 created_at: Mapped[datetime] = mapped_column(
518 DateTime(timezone=True), nullable=False, default=_utc_now
519 )
520
521 repo: Mapped[MusehubRepo] = relationship("MusehubRepo", back_populates="wire_tags")
522
523
524 class MusehubVersionTag(Base):
525 """A semantic-version tag pushed from a Muse CLI client via the wire protocol.
526
527 Version tags carry a semver string (e.g. ``v1.2.0``) that points to a
528 specific commit. They are distinct from wire tags: they always parse as
529 valid semver and are used to mark releases. The ``(repo_id, tag)`` pair is
530 unique — re-pushing the same tag updates the commit pointer (upsert).
531 """
532
533 __tablename__ = "musehub_version_tags"
534 __table_args__ = (UniqueConstraint("repo_id", "tag", name="uq_musehub_version_tags_repo_tag"),)
535
536 tag_id: Mapped[str] = mapped_column(String(128), primary_key=True)
537 repo_id: Mapped[str] = mapped_column(
538 String(128),
539 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
540 nullable=False,
541 index=True,
542 )
543 commit_id: Mapped[str] = mapped_column(String(128), nullable=False)
544 tag: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
545 semver_major: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
546 semver_minor: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
547 semver_patch: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
548 semver_pre: Mapped[str] = mapped_column(String(128), nullable=False, default="")
549 semver_build: Mapped[str] = mapped_column(String(128), nullable=False, default="")
550 author: Mapped[str] = mapped_column(String(255), nullable=False, default="")
551 message: Mapped[str] = mapped_column(Text, nullable=False, default="")
552 created_at: Mapped[datetime] = mapped_column(
553 DateTime(timezone=True), nullable=False, default=_utc_now
554 )
555 #: "ed25519:<base64url>" or "" when unsigned (muse#57 item 3).
556 signer_public_key: Mapped[str] = mapped_column(Text, nullable=False, default="")
557 signature: Mapped[str] = mapped_column(Text, nullable=False, default="")
558
559 repo: Mapped[MusehubRepo] = relationship("MusehubRepo", back_populates="version_tags")
560
561
562 class MusehubBridgeMirror(Base):
563 """A Git mirror registration for a MuseHub repo.
564
565 Each row links a MuseHub repo to a remote Git repository URL and records
566 the state of the last export (Muse→Git) and import (Git→Muse) operations.
567
568 ``direction`` controls which sync directions are active:
569 ``"export"`` — Muse commits are pushed to Git only
570 ``"import"`` — Git commits are pulled into Muse only
571 ``"bidirectional"`` — both directions are synced
572
573 The unique constraint on ``(repo_id, git_remote_url)`` means a given Git
574 URL can only be registered once per Muse repo.
575 """
576
577 __tablename__ = "musehub_bridge_mirrors"
578 __table_args__ = (
579 UniqueConstraint("repo_id", "git_remote_url", name="uq_bridge_mirror_repo_url"),
580 )
581
582 id: Mapped[str] = mapped_column(String(128), primary_key=True)
583 repo_id: Mapped[str] = mapped_column(
584 String(128),
585 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
586 nullable=False,
587 index=True,
588 )
589 git_remote_url: Mapped[str] = mapped_column(String(2048), nullable=False)
590 git_branch: Mapped[str] = mapped_column(String(255), nullable=False, default="muse-mirror", server_default="muse-mirror")
591 # "export" | "import" | "bidirectional"
592 direction: Mapped[str] = mapped_column(String(20), nullable=False)
593 last_export_muse_commit_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
594 last_export_git_sha: Mapped[str | None] = mapped_column(String(128), nullable=True)
595 last_export_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
596 last_import_git_sha: Mapped[str | None] = mapped_column(String(128), nullable=True)
597 last_import_muse_commit_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
598 last_import_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
599 auto_export: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=sa.false())
600 created_by: Mapped[str] = mapped_column(String(255), nullable=False)
601 created_at: Mapped[datetime] = mapped_column(
602 DateTime(timezone=True), nullable=False, default=_utc_now
603 )
604
605 repo: Mapped[MusehubRepo] = relationship("MusehubRepo")
606
607
608 class MusehubMist(MappedAsDataclass, Base):
609 """A Muse Mist — content-addressed, signed, forkable single-artifact share.
610
611 A Mist is the Muse answer to GitHub gists: a single artifact (code, MIDI,
612 JSON Schema, ABI, or any binary blob) stored in the Muse object
613 store, identified by a content-derived 12-character base-58 mist_id, signed
614 with the author's Ed25519 key, and version-controlled via a Muse repo with
615 ``domain="mist"``.
616
617 Unlike a gist, a Mist:
618 - Has a Muse repo behind it (full VCS: branches, commits, proposals).
619 - Is domain-typed via ``artifact_type`` — the hub knows whether the
620 content is code, MIDI, a JSON Schema, or a Solidity ABI.
621 - Carries agent provenance (``agent_id``, ``model_id``) for AI-authored mists.
622 - Has semantic intelligence for code mists (symbol anchors cached in
623 ``symbol_anchors`` JSON column).
624 - Is forkable via ``fork_parent_id`` self-referential FK.
625 - Is embeddable via ``/{owner}/mists/{mist_id}/embed``.
626 - Is MCP-accessible as ``muse:///{owner}/mists/{mist_id}``.
627
628 The ``mist_id`` is the first 12 characters of the base-58 encoding of the
629 SHA-256 digest of the initial artifact bytes — globally unique, stable
630 across renames, and collision-resistant (~70 bits of entropy).
631
632 Fork depth
633 ----------
634 ``fork_depth`` is 0 for originals and increments by 1 per fork tier.
635 The API enforces a hard limit of 5 fork tiers to prevent fork chains
636 from growing unboundedly.
637
638 Visibility
639 ----------
640 ``"public"`` mists appear in the explore feed and handle list.
641 ``"secret"`` mists are accessible only via direct URL — they are not
642 indexed and do not appear in any listing endpoint.
643
644 Counters
645 --------
646 ``view_count``, ``fork_count``, and ``embed_count`` are denormalized and
647 updated atomically (``UPDATE ... SET col = col + 1``) to avoid SELECT +
648 UPDATE races.
649 """
650
651 __tablename__ = "musehub_mists"
652 __table_args__ = (
653 # List page: owner's public mists, newest first
654 Index("ix_musehub_mists_owner_visibility", "owner", "visibility"),
655 # Pagination: owner's mists sorted by created_at
656 Index("ix_musehub_mists_owner_created_at", "owner", "created_at"),
657 # Fork graph: find all forks of a given mist
658 Index("ix_musehub_mists_fork_parent_id", "fork_parent_id"),
659 # Explore page: public mists by type, newest first
660 Index("ix_musehub_mists_artifact_type_created_at", "artifact_type", "created_at"),
661 )
662
663 # --- Required fields ---
664 # Primary key — content-addressed 12-character base-58 string (globally unique)
665 mist_id: Mapped[str] = mapped_column(String(128), primary_key=True)
666 repo_id: Mapped[str] = mapped_column(
667 String(128),
668 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
669 nullable=False,
670 index=True,
671 )
672 owner: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
673 filename: Mapped[str] = mapped_column(String(255), nullable=False)
674 content: Mapped[str] = mapped_column(Text, nullable=False)
675
676 # --- Optional fields ---
677 artifact_type: Mapped[str] = mapped_column(
678 String(20), nullable=False, default="unknown", index=True
679 )
680 language: Mapped[str] = mapped_column(String(64), nullable=False, default="")
681 title: Mapped[str] = mapped_column(String(500), nullable=False, default="")
682 description: Mapped[str] = mapped_column(Text, nullable=False, default="")
683 size_bytes: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
684 commit_id: Mapped[str | None] = mapped_column(String(128), nullable=True, default=None)
685 snapshot_id: Mapped[str | None] = mapped_column(String(128), nullable=True, default=None)
686 version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
687 agent_id: Mapped[str] = mapped_column(String(255), nullable=False, default="")
688 model_id: Mapped[str] = mapped_column(String(255), nullable=False, default="")
689 gpg_signature: Mapped[str | None] = mapped_column(Text, nullable=True, default=None)
690 fork_parent_id: Mapped[str | None] = mapped_column(
691 String(128),
692 ForeignKey("musehub_mists.mist_id", ondelete="SET NULL"),
693 nullable=True,
694 default=None,
695 )
696 fork_depth: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
697 fork_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
698 view_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
699 embed_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
700 visibility: Mapped[str] = mapped_column(String(10), nullable=False, default="public", server_default="public")
701 tags: Mapped[list[str]] = mapped_column(ARRAY(Text), nullable=False, default_factory=list)
702 symbol_anchors: Mapped[list[str]] = mapped_column(ARRAY(Text), nullable=False, default_factory=list)
703 created_at: Mapped[datetime] = mapped_column(
704 DateTime(timezone=True), nullable=False, default_factory=_utc_now
705 )
706 updated_at: Mapped[datetime] = mapped_column(
707 DateTime(timezone=True), nullable=False, default_factory=_utc_now, onupdate=_utc_now
708 )
709
710 fork_parent: Mapped["MusehubMist | None"] = relationship(
711 "MusehubMist",
712 remote_side="MusehubMist.mist_id",
713 foreign_keys=[fork_parent_id],
714 back_populates="forks",
715 init=False,
716 default=None,
717 )
718 forks: Mapped[list["MusehubMist"]] = relationship(
719 "MusehubMist",
720 foreign_keys=[fork_parent_id],
721 back_populates="fork_parent",
722 cascade="save-update, merge",
723 init=False,
724 default_factory=list,
725 )
726 repo: Mapped[MusehubRepo] = relationship("MusehubRepo", init=False)
727
728
729 class MusehubMPackIndex(Base):
730 """MPack index — maps every commit, snapshot, and object to the mpack containing it.
731
732 Written by ``process_mpack_index_job`` after every push. Enables the
733 fetch path to locate covering mpacks for any entity_id without O(N)
734 individual GET calls.
735
736 Entries are content-addressed globally — no repo_id.
737 Primary key is (entity_id, mpack_id). ``on_conflict_do_nothing`` is idempotent.
738 entity_type is one of: "object", "commit", "snapshot".
739 """
740
741 __tablename__ = "musehub_mpack_index"
742 __table_args__ = (
743 Index("ix_musehub_mpack_index_entity_id", "entity_id"),
744 )
745
746 entity_id: Mapped[str] = mapped_column(String(128), primary_key=True)
747 mpack_id: Mapped[str] = mapped_column(String(128), primary_key=True)
748 entity_type: Mapped[str] = mapped_column(
749 String(16), nullable=False, server_default="object"
750 )
751 created_at: Mapped[datetime] = mapped_column(
752 DateTime(timezone=True), nullable=False, default=_utc_now,
753 server_default=sa.text("now()"),
754 )
755 byte_offset: Mapped[int | None] = mapped_column(sa.BigInteger(), nullable=True)
756 byte_length: Mapped[int | None] = mapped_column(sa.Integer(), nullable=True)
757
758
759 # Backwards-compatible alias — remove once all call sites are updated.
760 MusehubPackIndex = MusehubMPackIndex
761
762
763 class MusehubFetchMPackCache(Base):
764 """Pre-built fetch mpack cache — one row per (repo, tip commit).
765
766 Populated by ``process_fetch_mpack_prebuild_job`` after every push.
767 ``wire_fetch_mpack`` checks this table first for fresh-clone requests
768 (want=[tip], have=[]) and returns a presigned URL immediately on hit,
769 bypassing the synchronous mpack build that causes Cloudflare 524s on
770 large repos.
771
772 ``expires_at`` defaults to 7 days after creation. The ``gc`` job
773 deletes expired rows and their corresponding R2 objects.
774 """
775
776 __tablename__ = "musehub_fetch_mpack_cache"
777 __table_args__ = (
778 UniqueConstraint("repo_id", "tip_commit_id", name="uq_fetch_mpack_cache_repo_tip"),
779 Index("ix_fetch_mpack_cache_expires_at", "expires_at"),
780 )
781
782 cache_id: Mapped[str] = mapped_column(String(128), primary_key=True)
783 repo_id: Mapped[str] = mapped_column(String(128), nullable=False)
784 tip_commit_id: Mapped[str] = mapped_column(String(128), nullable=False)
785 mpack_id: Mapped[str] = mapped_column(String(128), nullable=False)
786 created_at: Mapped[datetime] = mapped_column(
787 DateTime(timezone=True), nullable=False, default=_utc_now,
788 server_default=sa.text("now()"),
789 )
790 expires_at: Mapped[datetime] = mapped_column(
791 DateTime(timezone=True), nullable=False,
792 server_default=sa.text("now() + interval '7 days'"),
793 )
794
795
796 class MusehubCommitGraph(Base):
797 """Precomputed commit graph — enables O(frontier) BFS instead of O(N commits).
798
799 Commits are content-addressed (like snapshots) so this table is global —
800 no repo_id. ``_walk_commit_delta`` queries in bulk (one query per BFS
801 frontier) instead of one ``session.get(MusehubCommit, cid)`` per commit.
802
803 ``generation`` is the topological depth from the root: root=0, each commit
804 is max(parent generations) + 1. The index on generation lets GC and stats
805 queries run forward scans instead of full-table scans.
806 """
807
808 __tablename__ = "musehub_commit_graph"
809 __table_args__ = (
810 Index("ix_musehub_commit_graph_generation", "generation"),
811 )
812
813 commit_id: Mapped[str] = mapped_column(String(128), primary_key=True)
814 parent_ids: Mapped[list[str]] = mapped_column(
815 ARRAY(Text), nullable=False, default=list, server_default="{}"
816 )
817 generation: Mapped[int] = mapped_column(
818 sa.BigInteger, nullable=False, default=0, server_default="0"
819 )
820 snapshot_id: Mapped[str | None] = mapped_column(String(128), nullable=True, default=None)
821 created_at: Mapped[datetime] = mapped_column(
822 DateTime(timezone=True), nullable=False, default=_utc_now,
823 server_default=sa.text("now()"),
824 )
File History 5 commits
sha256:5dfc96524e3921eb9acb8372241b6bec70b5f3e6598f79099a0ead16ff7cbb75 feat(phase1): add musehub_fetch_mpack_cache table (issue #9… Sonnet 4.6 patch 131 days ago
sha256:400438cf8bc700a611f1ba798aa9def68290f487dc19f7dbf317985ad17050c9 chore: delete muse/prose domain — hallucinated, never existed Sonnet 4.6 minor ⚠ 132 days ago
sha256:e35be48854f182f7bf02dc6cc0f58d22b3de3a544b570c0e2bc53f9e75a3607d feat(phase6): remove delta_blob path, dead imports, add fal… Sonnet 4.6 minor ⚠ 145 days ago
sha256:e3296c39859814a0ae8b688be26ec1a24b3895fec467d9bdaefb7431e5ae3a93 test(phase1): failing tests + DB schema for object store in… Sonnet 4.6 minor ⚠ 145 days ago
sha256:302574ddba13c9a20694c0fb051176eef4896f943b63bc458df886633b1bfcd6 feat: mpack byte-range index — store byte_offset/byte_lengt… Sonnet 4.6 minor ⚠ 145 days ago