gabriel / muse public
registry.py python
113 lines 3.7 KB
73edf876 feat(bitcoin): add Bitcoin domain plugin — multidimensional VCS for on-… Gabriel Cardona <gabriel@tellurstori.com> 4d ago
1 """Plugin registry — maps domain names to :class:`~muse.domain.MuseDomainPlugin` instances.
2
3 Every CLI command that operates on domain state calls :func:`resolve_plugin`
4 once to obtain the active plugin for the current repository. Adding support
5 for a new domain requires only two changes:
6
7 1. Implement :class:`~muse.domain.MuseDomainPlugin` in a new module under
8 ``muse/plugins/<domain>/plugin.py``.
9 2. Register the plugin instance in ``_REGISTRY`` below.
10
11 The domain for a repository is stored in ``.muse/repo.json`` under the key
12 ``"domain"``. Repositories created before this key was introduced default to
13 ``'midi'``.
14 """
15
16 from __future__ import annotations
17
18 import json
19 import pathlib
20
21 from muse.core.errors import MuseCLIError
22 from muse.core.schema import DomainSchema
23 from muse.domain import MuseDomainPlugin
24 from muse.plugins.bitcoin.plugin import BitcoinPlugin
25 from muse.plugins.code.plugin import CodePlugin
26 from muse.plugins.midi.plugin import MidiPlugin
27 from muse.plugins.scaffold.plugin import ScaffoldPlugin
28
29 _REGISTRY: dict[str, MuseDomainPlugin] = {
30 "bitcoin": BitcoinPlugin(),
31 "code": CodePlugin(),
32 "midi": MidiPlugin(),
33 "scaffold": ScaffoldPlugin(),
34 }
35
36 _DEFAULT_DOMAIN = "midi"
37
38
39 def _read_domain(root: pathlib.Path) -> str:
40 """Return the domain name stored in ``.muse/repo.json``.
41
42 Falls back to ``'midi'`` for repos that pre-date the ``domain`` field.
43 """
44 repo_json = root / ".muse" / "repo.json"
45 try:
46 data = json.loads(repo_json.read_text())
47 domain = data.get("domain")
48 return str(domain) if domain else _DEFAULT_DOMAIN
49 except (OSError, json.JSONDecodeError):
50 return _DEFAULT_DOMAIN
51
52
53 def resolve_plugin(root: pathlib.Path) -> MuseDomainPlugin:
54 """Return the active domain plugin for the repository at *root*.
55
56 Reads the ``"domain"`` key from ``.muse/repo.json`` and looks it up in
57 the plugin registry. Raises :class:`~muse.core.errors.MuseCLIError` if
58 the domain is not registered.
59
60 Args:
61 root: Repository root directory (contains ``.muse/``).
62
63 Returns:
64 The :class:`~muse.domain.MuseDomainPlugin` instance for this repo.
65
66 Raises:
67 MuseCLIError: When the domain stored in ``repo.json`` is not in the
68 registry. This is a configuration error — either the plugin was
69 not installed or ``repo.json`` was edited manually.
70 """
71 domain = _read_domain(root)
72 plugin = _REGISTRY.get(domain)
73 if plugin is None:
74 registered = ", ".join(sorted(_REGISTRY))
75 raise MuseCLIError(
76 f"Unknown domain {domain!r}. Registered domains: {registered}"
77 )
78 return plugin
79
80
81 def read_domain(root: pathlib.Path) -> str:
82 """Return the domain name for the repository at *root*.
83
84 This is the same lookup used internally by :func:`resolve_plugin`.
85 Use it when you need the domain string to construct a
86 :class:`~muse.domain.SnapshotManifest` for a stored manifest.
87 """
88 return _read_domain(root)
89
90
91 def registered_domains() -> list[str]:
92 """Return the sorted list of registered domain names."""
93 return sorted(_REGISTRY)
94
95
96 def schema_for(domain: str) -> DomainSchema | None:
97 """Return the ``DomainSchema`` for *domain*, or ``None`` if not registered.
98
99 Allows the CLI and merge engine to look up a domain's schema without
100 holding a plugin instance. Returns ``None`` rather than raising so callers
101 can decide whether an unknown domain is an error or a soft miss.
102
103 Args:
104 domain: Domain name string (e.g. ``'midi'``).
105
106 Returns:
107 The :class:`~muse.core.schema.DomainSchema` declared by the plugin,
108 or ``None`` if *domain* is not in the registry.
109 """
110 plugin = _REGISTRY.get(domain)
111 if plugin is None:
112 return None
113 return plugin.schema()