cgcardona / muse public
models.py python
156 lines 5.8 KB
12901c5a Initial extraction from tellurstori/maestro cgcardona <gabriel@tellurstori.com> 4d ago
1 """SQLAlchemy ORM models for Muse commit history.
2
3 Tables:
4 - muse_objects: content-addressed file blobs (sha256 keyed)
5 - muse_snapshots: snapshot manifests mapping paths to object IDs
6 - muse_commits: commit history with parent linkage, branch tracking,
7 and an extensible ``extra_metadata`` JSON blob for annotations such as
8 meter (time signature), tempo, key, and other compositional metadata.
9 - muse_tags: music-semantic tags attached to commits
10
11 These tables are owned by the Muse CLI (``muse commit``) and are
12 distinct from the Muse variation tables (``muse_variations``, ``muse_phrases``,
13 ``muse_note_changes``) which track DAW-level note editing history.
14
15 ``muse_cli_commits.metadata`` is an extensible JSON blob for commit-level
16 annotations. Current keys:
17
18 - ``tempo_bpm`` (``float | None``): BPM set via ``muse tempo --set``.
19 - ``key`` (``str | None``): Key string (e.g. ``"Eb major"``) auto-updated by
20 ``muse transpose`` when transposing a commit that has this annotation.
21 """
22 from __future__ import annotations
23
24 from datetime import datetime, timezone
25 import uuid
26
27 from sqlalchemy import DateTime, ForeignKey, Integer, String, Text
28 from sqlalchemy.orm import Mapped, mapped_column
29 from sqlalchemy.types import JSON
30
31 from maestro.db.database import Base
32
33
34 def _utc_now() -> datetime:
35 return datetime.now(timezone.utc)
36
37
38 class MuseCliObject(Base):
39 """A content-addressed blob: sha256(file_bytes) → bytes on disk.
40
41 Objects are deduplicated across commits — the same file committed on
42 two different branches is stored exactly once.
43 """
44
45 __tablename__ = "muse_objects"
46
47 object_id: Mapped[str] = mapped_column(String(64), primary_key=True)
48 size_bytes: Mapped[int] = mapped_column(Integer, nullable=False)
49 created_at: Mapped[datetime] = mapped_column(
50 DateTime(timezone=True), nullable=False, default=_utc_now
51 )
52
53 def __repr__(self) -> str:
54 return f"<MuseCliObject {self.object_id[:8]} size={self.size_bytes}>"
55
56
57 class MuseCliSnapshot(Base):
58 """An immutable snapshot manifest: sha256(sorted(path:object_id pairs)).
59
60 The manifest JSON maps relative file paths to their object IDs.
61 Content-addressed: two identical working trees produce the same snapshot_id.
62 """
63
64 __tablename__ = "muse_snapshots"
65
66 snapshot_id: Mapped[str] = mapped_column(String(64), primary_key=True)
67 manifest: Mapped[dict[str, str]] = mapped_column(JSON, nullable=False)
68 created_at: Mapped[datetime] = mapped_column(
69 DateTime(timezone=True), nullable=False, default=_utc_now
70 )
71
72 def __repr__(self) -> str:
73 files = len(self.manifest) if self.manifest else 0
74 return f"<MuseCliSnapshot {self.snapshot_id[:8]} files={files}>"
75
76
77 class MuseCliCommit(Base):
78 """A versioned commit record pointing to a snapshot and its parent.
79
80 commit_id = sha256(sorted(parent_ids) | snapshot_id | message | committed_at_iso)
81
82 This derivation is deterministic: given the same working tree state,
83 message, and timestamp two machines produce identical commit IDs.
84 The ``committed_at`` field is the timestamp used in the hash; ``created_at``
85 is the wall-clock DB write time and is non-deterministic.
86 """
87
88 __tablename__ = "muse_commits"
89
90 commit_id: Mapped[str] = mapped_column(String(64), primary_key=True)
91 repo_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
92 branch: Mapped[str] = mapped_column(String(255), nullable=False)
93 parent_commit_id: Mapped[str | None] = mapped_column(
94 String(64), nullable=True, index=True
95 )
96 parent2_commit_id: Mapped[str | None] = mapped_column(
97 String(64), nullable=True, index=True
98 )
99 snapshot_id: Mapped[str] = mapped_column(
100 String(64),
101 ForeignKey("muse_snapshots.snapshot_id", ondelete="RESTRICT"),
102 nullable=False,
103 )
104 message: Mapped[str] = mapped_column(Text, nullable=False)
105 author: Mapped[str] = mapped_column(String(255), nullable=False, default="")
106 committed_at: Mapped[datetime] = mapped_column(
107 DateTime(timezone=True), nullable=False
108 )
109 created_at: Mapped[datetime] = mapped_column(
110 DateTime(timezone=True), nullable=False, default=_utc_now
111 )
112 commit_metadata: Mapped[dict[str, object] | None] = mapped_column(
113 "metadata", JSON, nullable=True, default=None
114 )
115
116 def __repr__(self) -> str:
117 return (
118 f"<MuseCliCommit {self.commit_id[:8]} branch={self.branch!r}"
119 f" msg={self.message[:30]!r}>"
120 )
121
122
123 class MuseCliTag(Base):
124 """A music-semantic tag attached to a Muse CLI commit.
125
126 Tags are free-form strings supporting namespaced conventions:
127 - ``emotion:*`` — emotional character (e.g. emotion:melancholic)
128 - ``stage:*`` — production stage (e.g. stage:rough-mix)
129 - ``ref:*`` — reference track or external source (e.g. ref:beatles)
130 - ``key:*`` — musical key (e.g. key:Am)
131 - ``tempo:*`` — tempo annotation (e.g. tempo:120bpm)
132 - free-form — any other descriptive label
133
134 Multiple tags can be attached to the same commit. Tags are scoped to a
135 repo so that different local repos can use independent tag spaces.
136 """
137
138 __tablename__ = "muse_tags"
139
140 tag_id: Mapped[str] = mapped_column(
141 String(36), primary_key=True, default=lambda: str(uuid.uuid4())
142 )
143 repo_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
144 commit_id: Mapped[str] = mapped_column(
145 String(64),
146 ForeignKey("muse_commits.commit_id", ondelete="CASCADE"),
147 nullable=False,
148 index=True,
149 )
150 tag: Mapped[str] = mapped_column(Text, nullable=False, index=True)
151 created_at: Mapped[datetime] = mapped_column(
152 DateTime(timezone=True), nullable=False, default=_utc_now
153 )
154
155 def __repr__(self) -> str:
156 return f"<MuseCliTag {self.tag!r} commit={self.commit_id[:8]}>"