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