gabriel / muse public
paths.py python
446 lines 17.2 KB
Raw
sha256:61100cca63d948098d334e1b600d8fea514568a1c26ef357bf8b6380fbd8a217 chore(timeline): remove unused RationalRate import in entity.py Human minor ⚠ breaking 8 days ago
1 """Canonical path helpers for the Muse on-disk layout.
2
3 Every place in the codebase that constructs a path inside ``.muse/`` (or the
4 user-global ``~/.muse/``) must call one of these helpers. Inline path
5 construction — ``root / ".muse" / "refs" / "heads"`` — is banned; it
6 duplicates the layout knowledge and makes future restructuring impossible.
7
8 All repo-local helpers take ``root: pathlib.Path`` (the repository root, i.e.
9 the directory containing ``.muse/``). All user-global helpers take no
10 arguments and derive their base from ``pathlib.Path.home()``.
11
12 Composability rule: every helper calls a lower-level helper rather than
13 reconstructing path segments from scratch. ``ref_path`` calls ``heads_dir``;
14 ``heads_dir`` calls ``refs_dir``; ``refs_dir`` calls ``muse_dir``. Adding a
15 new layout concept means choosing the right parent helper to compose from.
16 """
17
18 import pathlib
19
20 from muse.core.types import MUSE_DIR, OBJECTS_DIR
21
22 # ---------------------------------------------------------------------------
23 # Repo-local helpers (all take root: pathlib.Path)
24 # ---------------------------------------------------------------------------
25
26 def muse_dir(root: pathlib.Path) -> pathlib.Path:
27 """Return the ``.muse/`` directory for the repository at *root*."""
28 return root / MUSE_DIR
29
30 def objects_dir(root: pathlib.Path) -> pathlib.Path:
31 """Return ``.muse/objects/`` — the content-addressed object store."""
32 return muse_dir(root) / OBJECTS_DIR
33
34 def packs_dir(root: pathlib.Path) -> pathlib.Path:
35 """Return ``.muse/objects/pack/sha256/`` — the MPack local store.
36
37 Algorithm is canonical in the path, mirroring the loose object layout
38 (``.muse/objects/sha256/<prefix>/<remainder>``). Pack files are stored as
39 ``<packs_dir>/<64hex>.mpack`` and indexed at ``<packs_dir>/<64hex>.idx``.
40 """
41 from muse.core.types import DEFAULT_HASH_ALGO
42 return objects_dir(root) / "pack" / DEFAULT_HASH_ALGO
43
44 def commits_dir(root: pathlib.Path) -> pathlib.Path:
45 """Return ``.muse/commits/``."""
46 return muse_dir(root) / "commits"
47
48 def snapshots_dir(root: pathlib.Path) -> pathlib.Path:
49 """Return ``.muse/snapshots/``."""
50 return muse_dir(root) / "snapshots"
51
52 def tags_dir(root: pathlib.Path) -> pathlib.Path:
53 """Return ``.muse/tags/``."""
54 return muse_dir(root) / "tags"
55
56 def releases_dir(root: pathlib.Path) -> pathlib.Path:
57 """Return ``.muse/releases/``."""
58 return muse_dir(root) / "releases"
59
60 def indices_dir(root: pathlib.Path) -> pathlib.Path:
61 """Return ``.muse/indices/``."""
62 return muse_dir(root) / "indices"
63
64 def coordination_dir(root: pathlib.Path) -> pathlib.Path:
65 """Return ``.muse/coordination/``."""
66 return muse_dir(root) / "coordination"
67
68 def harmony_dir(root: pathlib.Path) -> pathlib.Path:
69 """Return ``.muse/harmony/``."""
70 return muse_dir(root) / "harmony"
71
72 def logs_dir(root: pathlib.Path) -> pathlib.Path:
73 """Return ``.muse/logs/``."""
74 return muse_dir(root) / "logs"
75
76 def symlogs_dir(root: pathlib.Path) -> pathlib.Path:
77 """Return ``.muse/symlogs/`` — root of the per-symbol live journal."""
78 return muse_dir(root) / "symlogs"
79
80 def refs_dir(root: pathlib.Path) -> pathlib.Path:
81 """Return ``.muse/refs/``."""
82 return muse_dir(root) / "refs"
83
84 def heads_dir(root: pathlib.Path) -> pathlib.Path:
85 """Return ``.muse/refs/heads/``."""
86 return refs_dir(root) / "heads"
87
88 def remotes_dir(root: pathlib.Path) -> pathlib.Path:
89 """Return ``.muse/remotes/`` — remote tracking ref root."""
90 return muse_dir(root) / "remotes"
91
92 def remote_tracking_dir(root: pathlib.Path, remote: str) -> pathlib.Path:
93 """Return ``.muse/remotes/<remote>/`` — tracking refs for one remote."""
94 return remotes_dir(root) / remote
95
96 def ref_path(root: pathlib.Path, branch: str) -> pathlib.Path:
97 """Return the ref file path for *branch* under ``.muse/refs/heads/``."""
98 return heads_dir(root) / branch
99
100 def remote_ref_path(root: pathlib.Path, remote: str, branch: str) -> pathlib.Path:
101 """Return the ref file path for *branch* under ``.muse/remotes/<remote>/``."""
102 return remotes_dir(root) / remote / branch
103
104 def head_path(root: pathlib.Path) -> pathlib.Path:
105 """Return ``.muse/HEAD``."""
106 return muse_dir(root) / "HEAD"
107
108 def repo_json_path(root: pathlib.Path) -> pathlib.Path:
109 """Return ``.muse/repo.json``."""
110 return muse_dir(root) / "repo.json"
111
112 def config_toml_path(root: pathlib.Path) -> pathlib.Path:
113 """Return ``.muse/config.toml``."""
114 return muse_dir(root) / "config.toml"
115
116 def workspace_toml_path(root: pathlib.Path) -> pathlib.Path:
117 """Return ``.muse/workspace.toml``."""
118 return muse_dir(root) / "workspace.toml"
119
120 def shelf_dir(root: pathlib.Path) -> pathlib.Path:
121 """Return ``.muse/shelf/`` — root of the per-entry shelf layout.
122
123 Shelf entries are stored as ``.muse/shelf/<algo>/<hex>`` (no extension),
124 using git-object-style framing (``shelf <size>\\0<json>``). The algo
125 segment is derived from each entry's content-addressed ID prefix (e.g.
126 ``sha256``), making the layout forward-compatible with future hash algorithms.
127
128 This helper is the single source of truth for the shelf directory location.
129 Never construct ``.muse/shelf`` inline — call this helper.
130 """
131 return muse_dir(root) / "shelf"
132
133 def shelf_json_path(root: pathlib.Path) -> pathlib.Path:
134 """Return ``.muse/shelf.json``.
135
136 .. deprecated::
137 Retained only for GC migration detection. New code must use
138 :func:`shelf_dir` and the per-entry git-header+JSON layout.
139 """
140 return muse_dir(root) / "shelf.json"
141
142 def agent_md_path(root: pathlib.Path) -> pathlib.Path:
143 """Return ``.muse/agent.md``."""
144 return muse_dir(root) / "agent.md"
145
146 def shallow_path(root: pathlib.Path) -> pathlib.Path:
147 """Return ``.muse/shallow``."""
148 return muse_dir(root) / "shallow"
149
150 def bisect_state_path(root: pathlib.Path) -> pathlib.Path:
151 """Return ``.muse/BISECT_STATE.toml``."""
152 return muse_dir(root) / "BISECT_STATE.toml"
153
154 def merge_state_path(root: pathlib.Path) -> pathlib.Path:
155 """Return ``.muse/MERGE_STATE.json``."""
156 return muse_dir(root) / "MERGE_STATE.json"
157
158 def stability_toml_path(root: pathlib.Path) -> pathlib.Path:
159 """Return ``.muse/stability.toml``."""
160 return muse_dir(root) / "stability.toml"
161
162 def cache_dir(root: pathlib.Path) -> pathlib.Path:
163 """Return ``.muse/cache/`` — all recomputable JSON cache files live here."""
164 return muse_dir(root) / "cache"
165
166 def stat_cache_path(root: pathlib.Path) -> pathlib.Path:
167 """Return ``.muse/cache/stat.json``."""
168 return cache_dir(root) / "stat.json"
169
170 def symbol_cache_path(root: pathlib.Path) -> pathlib.Path:
171 """Return ``.muse/cache/symbols.json``."""
172 return cache_dir(root) / "symbols.json"
173
174 def callgraph_cache_path(root: pathlib.Path) -> pathlib.Path:
175 """Return ``.muse/cache/callgraph.json``."""
176 return cache_dir(root) / "callgraph.json"
177
178 def implicit_edge_cache_path(root: pathlib.Path) -> pathlib.Path:
179 """Return ``.muse/cache/implicit_edges.json``."""
180 return cache_dir(root) / "implicit_edges.json"
181
182 def invariants_cache_path(root: pathlib.Path) -> pathlib.Path:
183 """Return ``.muse/cache/invariants.json``."""
184 return cache_dir(root) / "invariants.json"
185
186 def midi_invariants_path(root: pathlib.Path) -> pathlib.Path:
187 """Return ``.muse/midi_invariants.toml``."""
188 return muse_dir(root) / "midi_invariants.toml"
189
190 def rebase_merge_dir(root: pathlib.Path) -> pathlib.Path:
191 """Return ``.muse/rebase-merge/`` — in-progress rebase state directory."""
192 return muse_dir(root) / "rebase-merge"
193
194 def test_history_path(root: pathlib.Path) -> pathlib.Path:
195 """Return ``.muse/cache/test_history.json``."""
196 return cache_dir(root) / "test_history.json"
197
198 def maintenance_json_path(root: pathlib.Path) -> pathlib.Path:
199 """Return ``.muse/maintenance.json``."""
200 return muse_dir(root) / "maintenance.json"
201
202 def reflog_heads_dir(root: pathlib.Path) -> pathlib.Path:
203 """Return ``.muse/logs/refs/heads/`` — reflog directory for local branches."""
204 return logs_dir(root) / "refs" / "heads"
205
206 def reflog_branch_path(root: pathlib.Path, branch: str) -> pathlib.Path:
207 """Return the reflog file for *branch* under ``.muse/logs/refs/heads/``."""
208 return reflog_heads_dir(root) / branch
209
210 def prev_branch_path(root: pathlib.Path) -> pathlib.Path:
211 """Return ``.muse/PREV_BRANCH`` — stores the previous branch for ``switch -``."""
212 return muse_dir(root) / "PREV_BRANCH"
213
214 def checkout_head_path(root: pathlib.Path) -> pathlib.Path:
215 """Return ``.muse/CHECKOUT_HEAD`` — sentinel written during in-progress checkouts."""
216 return muse_dir(root) / "CHECKOUT_HEAD"
217
218 def code_dir(root: pathlib.Path) -> pathlib.Path:
219 """Return ``.muse/code/`` — code-domain working files."""
220 return muse_dir(root) / "code"
221
222 def code_stage_path(root: pathlib.Path) -> pathlib.Path:
223 """Return ``.muse/code/stage.json``."""
224 return code_dir(root) / "stage.json"
225
226 def code_config_path(root: pathlib.Path) -> pathlib.Path:
227 """Return ``.muse/code_config.toml``."""
228 return muse_dir(root) / "code_config.toml"
229
230 def code_manifests_dir(root: pathlib.Path) -> pathlib.Path:
231 """Return ``.muse/code_manifests/``."""
232 return muse_dir(root) / "code_manifests"
233
234 def sparse_checkout_path(root: pathlib.Path) -> pathlib.Path:
235 """Return ``.muse/sparse-checkout``."""
236 return muse_dir(root) / "sparse-checkout"
237
238 def dead_allowlist_path(root: pathlib.Path) -> pathlib.Path:
239 """Return ``.muse/dead-allowlist.json``."""
240 return muse_dir(root) / "dead-allowlist.json"
241
242 def ci_toml_path(root: pathlib.Path) -> pathlib.Path:
243 """Return ``.muse/ci.toml``."""
244 return muse_dir(root) / "ci.toml"
245
246 def docs_toml_path(root: pathlib.Path) -> pathlib.Path:
247 """Return ``.muse/docs.toml``."""
248 return muse_dir(root) / "docs.toml"
249
250 def op_log_dir(root: pathlib.Path) -> pathlib.Path:
251 """Return ``.muse/op_log/``."""
252 return muse_dir(root) / "op_log"
253
254 def rebase_state_path(root: pathlib.Path) -> pathlib.Path:
255 """Return ``.muse/REBASE_STATE.json``."""
256 return muse_dir(root) / "REBASE_STATE.json"
257
258 def worktrees_dir(root: pathlib.Path) -> pathlib.Path:
259 """Return ``.muse/worktrees/``."""
260 return muse_dir(root) / "worktrees"
261
262 def code_invariants_path(root: pathlib.Path) -> pathlib.Path:
263 """Return ``.muse/code_invariants.toml``."""
264 return muse_dir(root) / "code_invariants.toml"
265
266 def entity_index_dir(root: pathlib.Path) -> pathlib.Path:
267 """Return ``.muse/entity_index/``."""
268 return muse_dir(root) / "entity_index"
269
270 def music_manifests_dir(root: pathlib.Path) -> pathlib.Path:
271 """Return ``.muse/music_manifests/``."""
272 return muse_dir(root) / "music_manifests"
273
274 def git_bridge_state_path(root: pathlib.Path) -> pathlib.Path:
275 """Return ``.muse/git-bridge.toml``."""
276 return muse_dir(root) / "git-bridge.toml"
277
278 def git_bridge_sidecar_path(root: pathlib.Path) -> pathlib.Path:
279 """Return ``.muse/git-bridge-p8.json``."""
280 return muse_dir(root) / "git-bridge-p8.json"
281
282 # ---------------------------------------------------------------------------
283 # User-global helpers (no root argument — based on ~/.muse/)
284 # ---------------------------------------------------------------------------
285
286 def user_muse_dir() -> pathlib.Path:
287 """Return ``~/.muse/`` — the user-global Muse directory."""
288 return pathlib.Path.home() / MUSE_DIR
289
290 def user_keys_dir() -> pathlib.Path:
291 """Return ``~/.muse/keys/``."""
292 return user_muse_dir() / "keys"
293
294 def user_hub_trust_path() -> pathlib.Path:
295 """Return ``~/.muse/hub_trust.toml``."""
296 return user_muse_dir() / "hub_trust.toml"
297
298 def user_agent_slots_path() -> pathlib.Path:
299 """Return ``~/.muse/agent-slots.toml``."""
300 return user_muse_dir() / "agent-slots.toml"
301
302 def user_config_toml_path() -> pathlib.Path:
303 """Return ``~/.muse/config.toml`` — user-global config (safe_dirs, etc.)."""
304 return user_muse_dir() / "config.toml"
305
306 def user_identity_toml_path() -> pathlib.Path:
307 """Return ``~/.muse/identity.toml``."""
308 return user_muse_dir() / "identity.toml"
309
310 def user_domain_registry_path() -> pathlib.Path:
311 """Return ``~/.muse/domain-registry.json``."""
312 return user_muse_dir() / "domain-registry.json"
313
314 # ---------------------------------------------------------------------------
315 # Server-side per-repo store helpers (MuseHub / remote server)
316 # ---------------------------------------------------------------------------
317
318 def server_repo_root(repos_dir: pathlib.Path, owner: str, slug: str) -> pathlib.Path:
319 """Return the canonical on-disk root for a server-side repo.
320
321 Layout: ``<repos_dir>/<owner>/<slug>/``
322
323 This mirrors the local ``.muse/`` layout convention — the *repo root* is the
324 directory that directly contains the ``objects/``, ``refs/``, and ``HEAD``
325 subdirectories. All server-side path helpers take the value returned here
326 as their ``repo_root`` argument.
327
328 Path traversal via *owner* or *slug* is rejected; both components must
329 resolve to a path strictly inside *repos_dir*.
330
331 Args:
332 repos_dir: Base directory for all server-side repos (e.g. ``/data/repos``).
333 owner: Repository owner handle.
334 slug: Repository slug.
335
336 Returns:
337 Absolute, unresolved path ``repos_dir / owner / slug``.
338
339 Raises:
340 ValueError: If the resolved path would escape *repos_dir*.
341 """
342 base = repos_dir.resolve()
343 candidate = (repos_dir / owner / slug).resolve()
344 if not str(candidate).startswith(f"{base}/") and candidate != base:
345 raise ValueError(
346 f"Path traversal detected: owner={owner!r} slug={slug!r} "
347 f"escapes repos_dir={repos_dir!r}"
348 )
349 return candidate
350
351 def server_objects_dir(repo_root: pathlib.Path) -> pathlib.Path:
352 """Return the object store directory for a server-side repo.
353
354 Layout: ``<repo_root>/objects/``
355
356 Mirrors :func:`objects_dir` for local repos (which returns
357 ``<root>/.muse/objects/``). The server omits the ``.muse/`` wrapper because
358 repos are bare — there is no working tree.
359 """
360 return repo_root / OBJECTS_DIR
361
362 def server_refs_dir(repo_root: pathlib.Path) -> pathlib.Path:
363 """Return ``<repo_root>/refs/`` for a server-side repo."""
364 return repo_root / "refs"
365
366 def server_heads_dir(repo_root: pathlib.Path) -> pathlib.Path:
367 """Return ``<repo_root>/refs/heads/`` for a server-side repo."""
368 return server_refs_dir(repo_root) / "heads"
369
370 def server_ref_path(repo_root: pathlib.Path, branch: str) -> pathlib.Path:
371 """Return the ref file path for *branch* in a server-side repo.
372
373 Layout: ``<repo_root>/refs/heads/<branch>``
374 """
375 return server_heads_dir(repo_root) / branch
376
377 def server_head_path(repo_root: pathlib.Path) -> pathlib.Path:
378 """Return ``<repo_root>/HEAD`` for a server-side repo."""
379 return repo_root / "HEAD"
380
381 def server_object_path(
382 repo_root: pathlib.Path,
383 object_id: str,
384 prefix_len: int = 2,
385 ) -> pathlib.Path:
386 """Return the canonical on-disk path for an object in a server-side bare repo.
387
388 Server-side repos are bare — there is no working tree or ``.muse/`` wrapper.
389 Objects are stored directly under ``<repo_root>/objects/``:
390
391 ``<repo_root>/objects/<algo>/<prefix>/<remainder>``
392
393 This mirrors the local ``object_path`` layout (``<root>/.muse/objects/…``)
394 in every respect *except* the leading ``.muse/`` — both use algo-namespaced
395 + N-char sharding so objects can be hardlinked or transferred between the two
396 layouts without re-hashing.
397
398 Args:
399 repo_root: Root of the server-side bare repo (e.g. ``/data/repos/alice/muse``).
400 object_id: Prefixed SHA-256 object ID (``sha256:<64hex>``).
401 prefix_len: Shard prefix length (default ``2``).
402
403 Returns:
404 Absolute path to the object file (may not yet exist).
405
406 Raises:
407 ValueError: If *object_id* is not a valid prefixed SHA-256 object ID.
408 """
409 from muse.core.types import DEFAULT_HASH_ALGO, split_id
410 from muse.core.validation import validate_object_id
411 validate_object_id(object_id)
412 _, hex_id = split_id(object_id)
413 return server_objects_dir(repo_root) / DEFAULT_HASH_ALGO / hex_id[:prefix_len] / hex_id[prefix_len:]
414
415 # ---------------------------------------------------------------------------
416 # Repo bootstrapping helper (testing + `muse init` internals)
417 # ---------------------------------------------------------------------------
418
419 def init_repo_dirs(root: pathlib.Path) -> pathlib.Path:
420 """Create the minimal ``.muse/`` directory tree under *root*.
421
422 Idempotent — safe to call on a repo that already has some or all of the
423 required directories. Does **not** write ``HEAD``, ``repo.json``, or any
424 other file; callers that need those must write them separately.
425
426 Use this in tests and in ``muse init`` internals rather than spelling out
427 ``(root / ".muse" / "refs" / "heads").mkdir(parents=True, exist_ok=True)``
428 inline — that duplicates layout knowledge.
429
430 Args:
431 root: Repository root directory (the directory that will contain
432 ``.muse/``). Created with ``parents=True`` if it does not exist.
433
434 Returns:
435 *root* — allows the common ``repo = init_repo_dirs(tmp_path)`` pattern.
436 """
437 for make_dir in (
438 muse_dir,
439 objects_dir,
440 heads_dir,
441 remotes_dir,
442 logs_dir,
443 shelf_dir,
444 ):
445 make_dir(root).mkdir(parents=True, exist_ok=True)
446 return root
File History 1 commit
sha256:61100cca63d948098d334e1b600d8fea514568a1c26ef357bf8b6380fbd8a217 chore(timeline): remove unused RationalRate import in entity.py Human minor 8 days ago