gabriel / muse public
registry.py python
138 lines 4.9 KB
Raw
sha256:ac29b8ba9021514a03ab2782d92bf67671f0efa5b3b70d46f7598c5d4e923378 docs: record muse#74 Phase 4 live verification (SCR_13, SCR_14) Sonnet 5 13 days 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 import pathlib
17
18 from muse.core.types import load_json_file
19 from muse.core.paths import repo_json_path as _repo_json_path
20 from muse.core.errors import MuseCLIError
21 from muse.core.schema import DomainSchema
22 from muse.domain import MuseDomainPlugin
23 from muse.plugins.code.plugin import CodePlugin
24 from muse.plugins.identity.plugin import IdentityPlugin
25 from muse.plugins.mist.plugin import MistPlugin
26 from muse.plugins.scaffold.plugin import ScaffoldPlugin
27 from muse.plugins.social.plugin import SocialPlugin
28
29 type _PluginRegistry = dict[str, "MuseDomainPlugin"]
30 # MIDI domain is temporarily suspended pending its own security and
31 # performance audit. Re-enable by restoring the MidiPlugin import and
32 # the "midi" entry in _REGISTRY below.
33 # from muse.plugins.midi.plugin import MidiPlugin
34
35 _REGISTRY: _PluginRegistry = {
36 "code": CodePlugin(),
37 "identity": IdentityPlugin(),
38 "mist": MistPlugin(),
39 "social": SocialPlugin(),
40 # "midi": MidiPlugin(), # suspended — see comment above
41 "scaffold": ScaffoldPlugin(),
42 }
43
44 _DEFAULT_DOMAIN = "code"
45
46 def _read_domain(root: pathlib.Path) -> str:
47 """Return the domain name stored in ``.muse/repo.json``.
48
49 Falls back to ``'midi'`` for repos that pre-date the ``domain`` field.
50 """
51 data = load_json_file(_repo_json_path(root))
52 if data is None:
53 return _DEFAULT_DOMAIN
54 domain = data.get("domain")
55 return str(domain) if domain else _DEFAULT_DOMAIN
56
57 def resolve_plugin(root: pathlib.Path) -> MuseDomainPlugin:
58 """Return the active domain plugin for the repository at *root*.
59
60 Reads the ``"domain"`` key from ``.muse/repo.json`` and looks it up in
61 the plugin registry. Raises :class:`~muse.core.errors.MuseCLIError` if
62 the domain is not registered.
63
64 Args:
65 root: Repository root directory (contains ``.muse/``).
66
67 Returns:
68 The :class:`~muse.domain.MuseDomainPlugin` instance for this repo.
69
70 Raises:
71 MuseCLIError: When the domain stored in ``repo.json`` is not in the
72 registry. This is a configuration error — either the plugin was
73 not installed or ``repo.json`` was edited manually.
74 """
75 domain = _read_domain(root)
76 plugin = _REGISTRY.get(domain)
77 if plugin is None:
78 registered = ", ".join(sorted(_REGISTRY))
79 raise MuseCLIError(
80 f"Unknown domain {domain!r}. Registered domains: {registered}"
81 )
82 return plugin
83
84 def read_domain(root: pathlib.Path) -> str:
85 """Return the domain name for the repository at *root*.
86
87 This is the same lookup used internally by :func:`resolve_plugin`.
88 Use it when you need the domain string to construct a
89 :class:`~muse.domain.SnapshotManifest` for a stored manifest.
90 """
91 return _read_domain(root)
92
93 def resolve_plugin_by_domain(domain: str) -> MuseDomainPlugin:
94 """Return the plugin for *domain* without reading the filesystem.
95
96 Use this when the caller has already read ``repo.json`` and only needs
97 the plugin instance — avoids a redundant ``repo.json`` read compared to
98 :func:`resolve_plugin`.
99
100 Args:
101 domain: Domain name string (e.g. ``'code'``).
102
103 Returns:
104 The :class:`~muse.domain.MuseDomainPlugin` instance for *domain*.
105
106 Raises:
107 MuseCLIError: When *domain* is not in the registry.
108 """
109 plugin = _REGISTRY.get(domain)
110 if plugin is None:
111 registered = ", ".join(sorted(_REGISTRY))
112 raise MuseCLIError(
113 f"Unknown domain {domain!r}. Registered domains: {registered}"
114 )
115 return plugin
116
117 def registered_domains() -> list[str]:
118 """Return the sorted list of registered domain names."""
119 return sorted(_REGISTRY)
120
121 def schema_for(domain: str) -> DomainSchema | None:
122 """Return the ``DomainSchema`` for *domain*, or ``None`` if not registered.
123
124 Allows the CLI and merge engine to look up a domain's schema without
125 holding a plugin instance. Returns ``None`` rather than raising so callers
126 can decide whether an unknown domain is an error or a soft miss.
127
128 Args:
129 domain: Domain name string (e.g. ``'midi'``).
130
131 Returns:
132 The :class:`~muse.core.schema.DomainSchema` declared by the plugin,
133 or ``None`` if *domain* is not in the registry.
134 """
135 plugin = _REGISTRY.get(domain)
136 if plugin is None:
137 return None
138 return plugin.schema()
File History 2 commits
sha256:ac29b8ba9021514a03ab2782d92bf67671f0efa5b3b70d46f7598c5d4e923378 docs: record muse#74 Phase 4 live verification (SCR_13, SCR_14) Sonnet 5 13 days ago
sha256:2562dffa0a0822ac1bdea854f9b267843c6ce95b497a9dc5c55837c80a3ebd0a feat: domain_command_registry — Phase 1 of muse#74 (SCR_01-03) Sonnet 4.6 patch 13 days ago