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