gabriel / muse public
object_availability.py python
121 lines 4.3 KB
Raw
sha256:f9828efc523c2f8ccaee12ae76086a09a9a6a10d6dd20e53f0b13793f0fdcf50 docs: add symlog (#53) and reflog (#54) follow-up issue files Sonnet 4.6 18 days ago
1 """Object availability model for Muse repositories.
2
3 Every object referenced by a commit chain is in one of three states:
4
5 ``PRESENT``
6 The object's byte content exists in the local ``.muse/objects/`` store.
7 It can be read, hashed, and served without any network access.
8
9 ``PROMISED``
10 The object is absent from the local store, but at least one *promisor
11 remote* is configured. The remote is expected to have the object and
12 can serve it on demand. A promised object is **not** an integrity
13 failure — it is the normal state for a shallow or partial-clone repo.
14
15 ``MISSING``
16 The object is absent from the local store **and** no promisor remote is
17 configured. There is no known recovery path. This is a genuine
18 integrity failure.
19
20 Promisor remote convention
21 --------------------------
22 Any configured remote is implicitly a promisor unless it explicitly opts out
23 with ``promisor = false`` in ``.muse/config.toml``::
24
25 [remotes.mirror]
26 url = "http://mirror.internal/muse"
27 promisor = false # this remote is a dumb mirror, not authoritative
28
29 By contrast, all production remotes (``local``, ``staging``, etc.) are
30 promisors by default — they received the full object pack on every push and
31 can serve any object the local store lacks.
32
33 This mirrors git's promisor-remote concept (introduced for partial clones)
34 but uses a simpler default: *opted-in unless opted out*, which is the right
35 default for an agent-first platform where every push is to a trusted hub.
36
37 Integration with ``run_verify``
38 -------------------------------
39 ``run_verify`` calls ``load_promisor_remotes`` once at startup, then uses
40 ``object_state`` for every missing object it encounters:
41
42 - ``PRESENT`` → ``objects_checked`` counter incremented; hash verified.
43 - ``PROMISED`` → ``promised_objects`` counter incremented; not a failure.
44 - ``MISSING`` → appended to ``failures`` as ``kind="object"``.
45
46 In ``strict=True`` mode, ``PROMISED`` objects are treated as ``MISSING``.
47 """
48
49 import pathlib
50 from enum import Enum
51
52 from muse.core.object_store import _object_path_with_fallback
53
54 class ObjectState(str, Enum):
55 """Availability state of a single content-addressed object."""
56
57 PRESENT = "present"
58 """Bytes exist in the local ``.muse/objects/`` store."""
59
60 PROMISED = "promised"
61 """Absent locally but expected on at least one promisor remote."""
62
63 MISSING = "missing"
64 """Absent locally with no known recovery path."""
65
66 def object_state(
67 root: pathlib.Path,
68 obj_id: str,
69 promisor_remotes: list[str],
70 ) -> ObjectState:
71 """Return the availability state of *obj_id* in *root*.
72
73 Args:
74 root: Repository root (directory containing ``.muse/``).
75 obj_id: The ``sha256:``-prefixed content address to check.
76 promisor_remotes: Names of remotes that are promisors. Pass the
77 result of :func:`load_promisor_remotes`.
78
79 Returns:
80 ``PRESENT`` if the object file exists locally.
81 ``PROMISED`` if absent but at least one promisor remote is configured.
82 ``MISSING`` if absent and no promisor remote is configured.
83 """
84 if _object_path_with_fallback(root, obj_id).exists():
85 return ObjectState.PRESENT
86 if promisor_remotes:
87 return ObjectState.PROMISED
88 return ObjectState.MISSING
89
90 def load_promisor_remotes(root: pathlib.Path) -> list[str]:
91 """Return the names of all promisor remotes configured for *root*.
92
93 Reads ``.muse/config.toml``. Every configured remote is a promisor by
94 default; set ``promisor = false`` to opt a remote out.
95
96 Returns an empty list when no remotes are configured or when all
97 configured remotes have opted out.
98 """
99 try:
100 from muse.cli.config import _config_path, _load_config # type: ignore[attr-defined]
101 except ImportError:
102 return []
103
104 try:
105 config = _load_config(_config_path(root))
106 except Exception:
107 return []
108
109 remotes = config.get("remotes")
110 if not remotes:
111 return []
112
113 result: list[str] = []
114 for name, entry in remotes.items():
115 url = entry.get("url", "").strip()
116 if not url:
117 continue
118 # Default is promisor=True; opt out with promisor=false
119 if entry.get("promisor", True):
120 result.append(name)
121 return sorted(result)
File History 1 commit
sha256:f9828efc523c2f8ccaee12ae76086a09a9a6a10d6dd20e53f0b13793f0fdcf50 docs: add symlog (#53) and reflog (#54) follow-up issue files Sonnet 4.6 18 days ago