monoco-toolkit 0.3.10__py3-none-any.whl → 0.3.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- monoco/__main__.py +8 -0
- monoco/core/artifacts/__init__.py +16 -0
- monoco/core/artifacts/manager.py +575 -0
- monoco/core/artifacts/models.py +161 -0
- monoco/core/automation/__init__.py +51 -0
- monoco/core/automation/config.py +338 -0
- monoco/core/automation/field_watcher.py +296 -0
- monoco/core/automation/handlers.py +723 -0
- monoco/core/config.py +31 -4
- monoco/core/executor/__init__.py +38 -0
- monoco/core/executor/agent_action.py +254 -0
- monoco/core/executor/git_action.py +303 -0
- monoco/core/executor/im_action.py +309 -0
- monoco/core/executor/pytest_action.py +218 -0
- monoco/core/git.py +38 -0
- monoco/core/hooks/context.py +74 -13
- monoco/core/ingestion/__init__.py +20 -0
- monoco/core/ingestion/discovery.py +248 -0
- monoco/core/ingestion/watcher.py +343 -0
- monoco/core/ingestion/worker.py +436 -0
- monoco/core/loader.py +633 -0
- monoco/core/registry.py +34 -25
- monoco/core/router/__init__.py +55 -0
- monoco/core/router/action.py +341 -0
- monoco/core/router/router.py +392 -0
- monoco/core/scheduler/__init__.py +63 -0
- monoco/core/scheduler/base.py +152 -0
- monoco/core/scheduler/engines.py +175 -0
- monoco/core/scheduler/events.py +171 -0
- monoco/core/scheduler/local.py +377 -0
- monoco/core/skills.py +119 -80
- monoco/core/watcher/__init__.py +57 -0
- monoco/core/watcher/base.py +365 -0
- monoco/core/watcher/dropzone.py +152 -0
- monoco/core/watcher/issue.py +303 -0
- monoco/core/watcher/memo.py +200 -0
- monoco/core/watcher/task.py +238 -0
- monoco/daemon/app.py +77 -1
- monoco/daemon/commands.py +10 -0
- monoco/daemon/events.py +34 -0
- monoco/daemon/mailroom_service.py +196 -0
- monoco/daemon/models.py +1 -0
- monoco/daemon/scheduler.py +207 -0
- monoco/daemon/services.py +27 -58
- monoco/daemon/triggers.py +55 -0
- monoco/features/agent/__init__.py +25 -7
- monoco/features/agent/adapter.py +17 -7
- monoco/features/agent/cli.py +91 -57
- monoco/features/agent/engines.py +31 -170
- monoco/{core/resources/en/skills/monoco_core → features/agent/resources/en/skills/monoco_atom_core}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
- monoco/features/agent/resources/{roles/role-engineer.yaml → zh/roles/monoco_role_engineer.yaml} +3 -3
- monoco/features/agent/resources/{roles/role-manager.yaml → zh/roles/monoco_role_manager.yaml} +8 -8
- monoco/features/agent/resources/{roles/role-planner.yaml → zh/roles/monoco_role_planner.yaml} +8 -8
- monoco/features/agent/resources/{roles/role-reviewer.yaml → zh/roles/monoco_role_reviewer.yaml} +8 -8
- monoco/{core/resources/zh/skills/monoco_core → features/agent/resources/zh/skills/monoco_atom_core}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
- monoco/features/agent/worker.py +1 -1
- monoco/features/artifact/__init__.py +0 -0
- monoco/features/artifact/adapter.py +33 -0
- monoco/features/artifact/resources/zh/AGENTS.md +14 -0
- monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +278 -0
- monoco/features/glossary/adapter.py +18 -7
- monoco/features/glossary/resources/en/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
- monoco/features/glossary/resources/zh/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
- monoco/features/hooks/__init__.py +11 -0
- monoco/features/hooks/adapter.py +67 -0
- monoco/features/hooks/commands.py +309 -0
- monoco/features/hooks/core.py +441 -0
- monoco/features/hooks/resources/ADDING_HOOKS.md +234 -0
- monoco/features/i18n/adapter.py +18 -5
- monoco/features/i18n/core.py +482 -17
- monoco/features/i18n/resources/en/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
- monoco/features/i18n/resources/en/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
- monoco/features/i18n/resources/zh/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
- monoco/features/i18n/resources/zh/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
- monoco/features/issue/adapter.py +19 -6
- monoco/features/issue/commands.py +352 -20
- monoco/features/issue/core.py +475 -16
- monoco/features/issue/engine/machine.py +114 -4
- monoco/features/issue/linter.py +60 -5
- monoco/features/issue/models.py +2 -2
- monoco/features/issue/resources/en/AGENTS.md +109 -0
- monoco/features/issue/resources/en/skills/{monoco_issue → monoco_atom_issue}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
- monoco/features/issue/resources/hooks/post-checkout.sh +39 -0
- monoco/features/issue/resources/hooks/pre-commit.sh +41 -0
- monoco/features/issue/resources/hooks/pre-push.sh +35 -0
- monoco/features/issue/resources/zh/AGENTS.md +109 -0
- monoco/features/issue/resources/zh/skills/{monoco_issue → monoco_atom_issue_lifecycle}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
- monoco/features/issue/validator.py +101 -1
- monoco/features/memo/adapter.py +21 -8
- monoco/features/memo/cli.py +103 -10
- monoco/features/memo/core.py +178 -92
- monoco/features/memo/models.py +53 -0
- monoco/features/memo/resources/en/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
- monoco/features/memo/resources/en/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
- monoco/features/memo/resources/zh/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
- monoco/features/memo/resources/zh/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
- monoco/features/spike/adapter.py +18 -5
- monoco/features/spike/commands.py +5 -3
- monoco/features/spike/resources/en/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
- monoco/features/spike/resources/en/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
- monoco/features/spike/resources/zh/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
- monoco/features/spike/resources/zh/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
- monoco/main.py +38 -1
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/METADATA +7 -1
- monoco_toolkit-0.3.12.dist-info/RECORD +202 -0
- monoco/features/agent/apoptosis.py +0 -44
- monoco/features/agent/manager.py +0 -91
- monoco/features/agent/session.py +0 -121
- monoco_toolkit-0.3.10.dist-info/RECORD +0 -156
- /monoco/{core → features/agent}/resources/en/AGENTS.md +0 -0
- /monoco/{core → features/agent}/resources/zh/AGENTS.md +0 -0
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/licenses/LICENSE +0 -0
monoco/core/loader.py
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified Module Loader and Lifecycle Management for Monoco Features.
|
|
3
|
+
|
|
4
|
+
This module provides dynamic discovery, loading, and lifecycle management
|
|
5
|
+
for Monoco feature modules. It implements the FeatureModule protocol with
|
|
6
|
+
standard lifecycle hooks (mount/unmount) and supports dependency injection
|
|
7
|
+
and lazy loading for improved startup performance.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import importlib
|
|
11
|
+
import importlib.util
|
|
12
|
+
import inspect
|
|
13
|
+
import pkgutil
|
|
14
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Type, TypeVar, Union
|
|
18
|
+
|
|
19
|
+
from monoco.core.feature import IntegrationData, MonocoFeature
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
T = TypeVar("T")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LifecycleState:
|
|
26
|
+
"""Lifecycle states for a feature module."""
|
|
27
|
+
|
|
28
|
+
UNLOADED = "unloaded"
|
|
29
|
+
LOADING = "loading"
|
|
30
|
+
LOADED = "loaded"
|
|
31
|
+
MOUNTING = "mounting"
|
|
32
|
+
MOUNTED = "mounted"
|
|
33
|
+
UNMOUNTING = "unmounting"
|
|
34
|
+
ERROR = "error"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class FeatureMetadata:
|
|
39
|
+
"""Metadata for a feature module."""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
version: str = "1.0.0"
|
|
43
|
+
description: str = ""
|
|
44
|
+
author: str = ""
|
|
45
|
+
dependencies: List[str] = field(default_factory=list)
|
|
46
|
+
optional_dependencies: List[str] = field(default_factory=list)
|
|
47
|
+
tags: List[str] = field(default_factory=list)
|
|
48
|
+
lazy: bool = False # Whether this feature supports lazy loading
|
|
49
|
+
priority: int = 100 # Lower values = higher priority for loading order
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class FeatureModule(MonocoFeature, ABC):
|
|
53
|
+
"""
|
|
54
|
+
Abstract base class for all Monoco feature modules.
|
|
55
|
+
|
|
56
|
+
Features must implement this protocol to participate in the unified
|
|
57
|
+
module loading system with full lifecycle management.
|
|
58
|
+
|
|
59
|
+
Lifecycle:
|
|
60
|
+
1. Discovery: Feature is discovered in monoco/features/
|
|
61
|
+
2. Load: Module is imported and class instantiated
|
|
62
|
+
3. Mount: Feature is mounted (initialized) with workspace context
|
|
63
|
+
4. Runtime: Feature is active and responding to commands
|
|
64
|
+
5. Unmount: Feature is gracefully shut down
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
class MyFeature(FeatureModule):
|
|
68
|
+
@property
|
|
69
|
+
def metadata(self) -> FeatureMetadata:
|
|
70
|
+
return FeatureMetadata(
|
|
71
|
+
name="myfeature",
|
|
72
|
+
version="1.0.0",
|
|
73
|
+
dependencies=["core"]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def mount(self, context: FeatureContext) -> None:
|
|
77
|
+
# Initialize feature with workspace context
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
def unmount(self) -> None:
|
|
81
|
+
# Cleanup resources
|
|
82
|
+
pass
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
_state: str = LifecycleState.UNLOADED
|
|
86
|
+
_context: Optional["FeatureContext"] = None
|
|
87
|
+
_instance_id: Optional[str] = None
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def metadata(self) -> FeatureMetadata:
|
|
92
|
+
"""Return feature metadata including dependencies."""
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def name(self) -> str:
|
|
97
|
+
"""Unique identifier for the feature (from metadata)."""
|
|
98
|
+
return self.metadata.name
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def state(self) -> str:
|
|
102
|
+
"""Current lifecycle state of the feature."""
|
|
103
|
+
return self._state
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def context(self) -> Optional["FeatureContext"]:
|
|
107
|
+
"""The feature context if mounted."""
|
|
108
|
+
return self._context
|
|
109
|
+
|
|
110
|
+
def mount(self, context: "FeatureContext") -> None:
|
|
111
|
+
"""
|
|
112
|
+
Lifecycle hook: Mount the feature with workspace context.
|
|
113
|
+
|
|
114
|
+
Called when the feature is being activated. The feature should
|
|
115
|
+
initialize any resources needed for operation.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
context: The feature context containing workspace and config.
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
FeatureMountError: If mounting fails.
|
|
122
|
+
"""
|
|
123
|
+
self._context = context
|
|
124
|
+
self._state = LifecycleState.MOUNTING
|
|
125
|
+
try:
|
|
126
|
+
self._on_mount(context)
|
|
127
|
+
self._state = LifecycleState.MOUNTED
|
|
128
|
+
except Exception as e:
|
|
129
|
+
self._state = LifecycleState.ERROR
|
|
130
|
+
raise FeatureMountError(f"Failed to mount feature '{self.name}': {e}") from e
|
|
131
|
+
|
|
132
|
+
def unmount(self) -> None:
|
|
133
|
+
"""
|
|
134
|
+
Lifecycle hook: Unmount the feature and cleanup resources.
|
|
135
|
+
|
|
136
|
+
Called when the feature is being deactivated or the application
|
|
137
|
+
is shutting down. The feature should release all resources.
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
FeatureUnmountError: If unmounting fails.
|
|
141
|
+
"""
|
|
142
|
+
self._state = LifecycleState.UNMOUNTING
|
|
143
|
+
try:
|
|
144
|
+
self._on_unmount()
|
|
145
|
+
self._state = LifecycleState.UNLOADED
|
|
146
|
+
except Exception as e:
|
|
147
|
+
self._state = LifecycleState.ERROR
|
|
148
|
+
raise FeatureUnmountError(f"Failed to unmount feature '{self.name}': {e}") from e
|
|
149
|
+
finally:
|
|
150
|
+
self._context = None
|
|
151
|
+
|
|
152
|
+
def _on_mount(self, context: "FeatureContext") -> None:
|
|
153
|
+
"""
|
|
154
|
+
Override this method to implement mount logic.
|
|
155
|
+
Default implementation delegates to legacy initialize().
|
|
156
|
+
"""
|
|
157
|
+
if hasattr(self, "initialize"):
|
|
158
|
+
# Legacy support: call initialize if defined
|
|
159
|
+
root = context.root if context else Path.cwd()
|
|
160
|
+
config = context.config if context else {}
|
|
161
|
+
self.initialize(root, config) # type: ignore
|
|
162
|
+
|
|
163
|
+
def _on_unmount(self) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Override this method to implement unmount logic.
|
|
166
|
+
"""
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
def initialize(self, root: Path, config: Dict) -> None:
|
|
170
|
+
"""
|
|
171
|
+
Legacy lifecycle hook: Physical Structure Initialization.
|
|
172
|
+
|
|
173
|
+
New features should override _on_mount() instead.
|
|
174
|
+
"""
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
def integrate(self, root: Path, config: Dict) -> IntegrationData:
|
|
178
|
+
"""
|
|
179
|
+
Lifecycle hook: Agent Environment Integration.
|
|
180
|
+
|
|
181
|
+
Called during sync to provide prompts and skills.
|
|
182
|
+
"""
|
|
183
|
+
return IntegrationData()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclass
|
|
187
|
+
class FeatureContext:
|
|
188
|
+
"""
|
|
189
|
+
Context provided to features during mount/unmount operations.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
root: Path
|
|
193
|
+
config: Dict[str, Any]
|
|
194
|
+
registry: "FeatureRegistry"
|
|
195
|
+
services: "ServiceContainer" = field(default_factory=lambda: ServiceContainer())
|
|
196
|
+
|
|
197
|
+
def get_service(self, interface: Type[T]) -> Optional[T]:
|
|
198
|
+
"""Get a service from the service container."""
|
|
199
|
+
return self.services.get(interface)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class ServiceContainer:
|
|
203
|
+
"""
|
|
204
|
+
Simple IoC container for dependency injection.
|
|
205
|
+
|
|
206
|
+
Supports registration of services by interface and implementation.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
def __init__(self):
|
|
210
|
+
self._services: Dict[Type, Any] = {}
|
|
211
|
+
self._factories: Dict[Type, Callable[[], Any]] = {}
|
|
212
|
+
|
|
213
|
+
def register(self, interface: Type[T], implementation: T) -> None:
|
|
214
|
+
"""Register a service instance for an interface."""
|
|
215
|
+
self._services[interface] = implementation
|
|
216
|
+
|
|
217
|
+
def register_factory(self, interface: Type[T], factory: Callable[[], T]) -> None:
|
|
218
|
+
"""Register a factory function for lazy instantiation."""
|
|
219
|
+
self._factories[interface] = factory
|
|
220
|
+
|
|
221
|
+
def get(self, interface: Type[T]) -> Optional[T]:
|
|
222
|
+
"""Get a service implementation by interface."""
|
|
223
|
+
if interface in self._services:
|
|
224
|
+
return self._services[interface]
|
|
225
|
+
if interface in self._factories:
|
|
226
|
+
instance = self._factories[interface]()
|
|
227
|
+
self._services[interface] = instance
|
|
228
|
+
return instance
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
def has(self, interface: Type) -> bool:
|
|
232
|
+
"""Check if a service is registered."""
|
|
233
|
+
return interface in self._services or interface in self._factories
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class FeatureError(Exception):
|
|
237
|
+
"""Base exception for feature-related errors."""
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class FeatureMountError(FeatureError):
|
|
242
|
+
"""Raised when feature mounting fails."""
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class FeatureUnmountError(FeatureError):
|
|
247
|
+
"""Raised when feature unmounting fails."""
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class FeatureLoadError(FeatureError):
|
|
252
|
+
"""Raised when feature loading fails."""
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class FeatureDependencyError(FeatureError):
|
|
257
|
+
"""Raised when feature dependencies cannot be resolved."""
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class FeatureRegistry:
|
|
262
|
+
"""
|
|
263
|
+
Registry for managing feature modules.
|
|
264
|
+
|
|
265
|
+
Maintains a collection of registered features and provides
|
|
266
|
+
lifecycle management operations.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
def __init__(self):
|
|
270
|
+
self._features: Dict[str, FeatureModule] = {}
|
|
271
|
+
self._states: Dict[str, str] = {}
|
|
272
|
+
self._context: Optional[FeatureContext] = None
|
|
273
|
+
|
|
274
|
+
def register(self, feature: FeatureModule) -> None:
|
|
275
|
+
"""Register a feature instance."""
|
|
276
|
+
self._features[feature.name] = feature
|
|
277
|
+
self._states[feature.name] = feature.state
|
|
278
|
+
|
|
279
|
+
def unregister(self, name: str) -> None:
|
|
280
|
+
"""Unregister a feature by name."""
|
|
281
|
+
if name in self._features:
|
|
282
|
+
del self._features[name]
|
|
283
|
+
del self._states[name]
|
|
284
|
+
|
|
285
|
+
def get(self, name: str) -> Optional[FeatureModule]:
|
|
286
|
+
"""Get a feature by name."""
|
|
287
|
+
return self._features.get(name)
|
|
288
|
+
|
|
289
|
+
def get_all(self) -> List[FeatureModule]:
|
|
290
|
+
"""Get all registered features."""
|
|
291
|
+
return list(self._features.values())
|
|
292
|
+
|
|
293
|
+
def get_mounted(self) -> List[FeatureModule]:
|
|
294
|
+
"""Get all mounted features."""
|
|
295
|
+
return [f for f in self._features.values() if f.state == LifecycleState.MOUNTED]
|
|
296
|
+
|
|
297
|
+
def is_registered(self, name: str) -> bool:
|
|
298
|
+
"""Check if a feature is registered."""
|
|
299
|
+
return name in self._features
|
|
300
|
+
|
|
301
|
+
def is_mounted(self, name: str) -> bool:
|
|
302
|
+
"""Check if a feature is mounted."""
|
|
303
|
+
return self._states.get(name) == LifecycleState.MOUNTED
|
|
304
|
+
|
|
305
|
+
def mount_all(self, context: FeatureContext) -> Dict[str, Exception]:
|
|
306
|
+
"""
|
|
307
|
+
Mount all registered features.
|
|
308
|
+
|
|
309
|
+
Returns a dictionary of errors keyed by feature name.
|
|
310
|
+
"""
|
|
311
|
+
self._context = context
|
|
312
|
+
errors = {}
|
|
313
|
+
|
|
314
|
+
# Sort features by priority (lower = higher priority)
|
|
315
|
+
sorted_features = sorted(
|
|
316
|
+
self._features.values(),
|
|
317
|
+
key=lambda f: f.metadata.priority if hasattr(f, "metadata") else 100
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
for feature in sorted_features:
|
|
321
|
+
try:
|
|
322
|
+
feature.mount(context)
|
|
323
|
+
self._states[feature.name] = LifecycleState.MOUNTED
|
|
324
|
+
except Exception as e:
|
|
325
|
+
errors[feature.name] = e
|
|
326
|
+
self._states[feature.name] = LifecycleState.ERROR
|
|
327
|
+
|
|
328
|
+
return errors
|
|
329
|
+
|
|
330
|
+
def unmount_all(self) -> Dict[str, Exception]:
|
|
331
|
+
"""
|
|
332
|
+
Unmount all mounted features.
|
|
333
|
+
|
|
334
|
+
Returns a dictionary of errors keyed by feature name.
|
|
335
|
+
"""
|
|
336
|
+
errors = {}
|
|
337
|
+
|
|
338
|
+
# Unmount in reverse priority order
|
|
339
|
+
sorted_features = sorted(
|
|
340
|
+
self._features.values(),
|
|
341
|
+
key=lambda f: f.metadata.priority if hasattr(f, "metadata") else 100,
|
|
342
|
+
reverse=True
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
for feature in sorted_features:
|
|
346
|
+
if feature.state == LifecycleState.MOUNTED:
|
|
347
|
+
try:
|
|
348
|
+
feature.unmount()
|
|
349
|
+
self._states[feature.name] = LifecycleState.UNLOADED
|
|
350
|
+
except Exception as e:
|
|
351
|
+
errors[feature.name] = e
|
|
352
|
+
|
|
353
|
+
self._context = None
|
|
354
|
+
return errors
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class FeatureLoader:
|
|
358
|
+
"""
|
|
359
|
+
Dynamic feature loader with discovery and lifecycle management.
|
|
360
|
+
|
|
361
|
+
Discovers features in the monoco.features package and provides
|
|
362
|
+
lazy loading capabilities for improved startup performance.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
FEATURES_PACKAGE = "monoco.features"
|
|
366
|
+
ADAPTER_MODULE = "adapter"
|
|
367
|
+
FEATURE_CLASS_NAME = "Feature"
|
|
368
|
+
|
|
369
|
+
def __init__(
|
|
370
|
+
self,
|
|
371
|
+
registry: Optional[FeatureRegistry] = None,
|
|
372
|
+
service_container: Optional[ServiceContainer] = None,
|
|
373
|
+
):
|
|
374
|
+
self.registry = registry or FeatureRegistry()
|
|
375
|
+
self.services = service_container or ServiceContainer()
|
|
376
|
+
self._discovered: Dict[str, Type[FeatureModule]] = {}
|
|
377
|
+
self._loaded: Dict[str, FeatureModule] = {}
|
|
378
|
+
self._lazy_queue: Set[str] = set()
|
|
379
|
+
self._load_hooks: List[Callable[[FeatureModule], None]] = []
|
|
380
|
+
self._mount_hooks: List[Callable[[FeatureModule], None]] = []
|
|
381
|
+
|
|
382
|
+
def add_load_hook(self, hook: Callable[[FeatureModule], None]) -> None:
|
|
383
|
+
"""Add a hook to be called after a feature is loaded."""
|
|
384
|
+
self._load_hooks.append(hook)
|
|
385
|
+
|
|
386
|
+
def add_mount_hook(self, hook: Callable[[FeatureModule], None]) -> None:
|
|
387
|
+
"""Add a hook to be called after a feature is mounted."""
|
|
388
|
+
self._mount_hooks.append(hook)
|
|
389
|
+
|
|
390
|
+
def discover(self, package: Optional[str] = None) -> List[str]:
|
|
391
|
+
"""
|
|
392
|
+
Discover available features in the features package.
|
|
393
|
+
|
|
394
|
+
Scans subpackages of monoco.features for adapter modules
|
|
395
|
+
containing FeatureModule implementations.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
package: Package to scan (defaults to monoco.features).
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
List of discovered feature names.
|
|
402
|
+
"""
|
|
403
|
+
package = package or self.FEATURES_PACKAGE
|
|
404
|
+
discovered = []
|
|
405
|
+
|
|
406
|
+
try:
|
|
407
|
+
import monoco.features as features_pkg
|
|
408
|
+
|
|
409
|
+
package_path = Path(features_pkg.__file__).parent
|
|
410
|
+
|
|
411
|
+
for item in package_path.iterdir():
|
|
412
|
+
if not item.is_dir() or item.name.startswith("_"):
|
|
413
|
+
continue
|
|
414
|
+
|
|
415
|
+
feature_name = item.name
|
|
416
|
+
adapter_path = item / f"{self.ADAPTER_MODULE}.py"
|
|
417
|
+
|
|
418
|
+
if adapter_path.exists():
|
|
419
|
+
self._discovered[feature_name] = None # Mark as discovered, not loaded
|
|
420
|
+
discovered.append(feature_name)
|
|
421
|
+
|
|
422
|
+
except ImportError:
|
|
423
|
+
pass
|
|
424
|
+
|
|
425
|
+
return discovered
|
|
426
|
+
|
|
427
|
+
def load(self, name: str, lazy: bool = False) -> Optional[FeatureModule]:
|
|
428
|
+
"""
|
|
429
|
+
Load a feature by name.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
name: Feature name (subpackage name in monoco.features).
|
|
433
|
+
lazy: If True, defer actual instantiation until mount.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
Loaded FeatureModule instance or None if loading failed.
|
|
437
|
+
|
|
438
|
+
Raises:
|
|
439
|
+
FeatureLoadError: If the feature cannot be loaded.
|
|
440
|
+
"""
|
|
441
|
+
if name in self._loaded:
|
|
442
|
+
return self._loaded[name]
|
|
443
|
+
|
|
444
|
+
if lazy:
|
|
445
|
+
self._lazy_queue.add(name)
|
|
446
|
+
return None
|
|
447
|
+
|
|
448
|
+
try:
|
|
449
|
+
module_path = f"{self.FEATURES_PACKAGE}.{name}.{self.ADAPTER_MODULE}"
|
|
450
|
+
module = importlib.import_module(module_path)
|
|
451
|
+
|
|
452
|
+
# Find FeatureModule subclass in the module
|
|
453
|
+
feature_class = None
|
|
454
|
+
for attr_name in dir(module):
|
|
455
|
+
attr = getattr(module, attr_name)
|
|
456
|
+
if (
|
|
457
|
+
inspect.isclass(attr)
|
|
458
|
+
and issubclass(attr, FeatureModule)
|
|
459
|
+
and attr is not FeatureModule
|
|
460
|
+
):
|
|
461
|
+
feature_class = attr
|
|
462
|
+
break
|
|
463
|
+
|
|
464
|
+
# Fallback: Check for legacy MonocoFeature implementations
|
|
465
|
+
if feature_class is None:
|
|
466
|
+
for attr_name in dir(module):
|
|
467
|
+
attr = getattr(module, attr_name)
|
|
468
|
+
if (
|
|
469
|
+
inspect.isclass(attr)
|
|
470
|
+
and issubclass(attr, MonocoFeature)
|
|
471
|
+
and attr is not MonocoFeature
|
|
472
|
+
and attr is not FeatureModule
|
|
473
|
+
):
|
|
474
|
+
# Wrap legacy feature in adapter
|
|
475
|
+
feature_class = self._wrap_legacy_feature(attr)
|
|
476
|
+
break
|
|
477
|
+
|
|
478
|
+
if feature_class is None:
|
|
479
|
+
raise FeatureLoadError(f"No FeatureModule found in {module_path}")
|
|
480
|
+
|
|
481
|
+
# Instantiate the feature
|
|
482
|
+
instance = feature_class()
|
|
483
|
+
instance._state = LifecycleState.LOADED
|
|
484
|
+
self._loaded[name] = instance
|
|
485
|
+
self.registry.register(instance)
|
|
486
|
+
|
|
487
|
+
# Call load hooks
|
|
488
|
+
for hook in self._load_hooks:
|
|
489
|
+
hook(instance)
|
|
490
|
+
|
|
491
|
+
return instance
|
|
492
|
+
|
|
493
|
+
except ImportError as e:
|
|
494
|
+
raise FeatureLoadError(f"Failed to import feature '{name}': {e}") from e
|
|
495
|
+
except Exception as e:
|
|
496
|
+
raise FeatureLoadError(f"Failed to load feature '{name}': {e}") from e
|
|
497
|
+
|
|
498
|
+
def load_all(self, lazy: bool = False) -> Dict[str, Optional[FeatureModule]]:
|
|
499
|
+
"""
|
|
500
|
+
Load all discovered features.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
lazy: If True, defer non-critical feature loading.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
Dictionary mapping feature names to instances (or None for lazy).
|
|
507
|
+
"""
|
|
508
|
+
if not self._discovered:
|
|
509
|
+
self.discover()
|
|
510
|
+
|
|
511
|
+
results = {}
|
|
512
|
+
for name in self._discovered:
|
|
513
|
+
# Check if feature supports lazy loading
|
|
514
|
+
should_lazy = lazy and self._is_lazy_feature(name)
|
|
515
|
+
try:
|
|
516
|
+
results[name] = self.load(name, lazy=should_lazy)
|
|
517
|
+
except FeatureLoadError as e:
|
|
518
|
+
results[name] = None
|
|
519
|
+
# Log error but continue loading other features
|
|
520
|
+
print(f"Warning: {e}")
|
|
521
|
+
|
|
522
|
+
return results
|
|
523
|
+
|
|
524
|
+
def mount(self, name: str, context: FeatureContext) -> None:
|
|
525
|
+
"""
|
|
526
|
+
Mount a specific feature.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
name: Feature name.
|
|
530
|
+
context: Feature context for mounting.
|
|
531
|
+
|
|
532
|
+
Raises:
|
|
533
|
+
FeatureError: If the feature is not loaded or cannot be mounted.
|
|
534
|
+
"""
|
|
535
|
+
if name in self._lazy_queue:
|
|
536
|
+
# Load now if it was deferred
|
|
537
|
+
self._lazy_queue.discard(name)
|
|
538
|
+
self.load(name, lazy=False)
|
|
539
|
+
|
|
540
|
+
feature = self.registry.get(name)
|
|
541
|
+
if feature is None:
|
|
542
|
+
raise FeatureError(f"Feature '{name}' is not loaded")
|
|
543
|
+
|
|
544
|
+
feature.mount(context)
|
|
545
|
+
|
|
546
|
+
for hook in self._mount_hooks:
|
|
547
|
+
hook(feature)
|
|
548
|
+
|
|
549
|
+
def mount_all(self, context: FeatureContext) -> Dict[str, Exception]:
|
|
550
|
+
"""
|
|
551
|
+
Mount all loaded features.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
context: Feature context for mounting.
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
Dictionary of errors keyed by feature name.
|
|
558
|
+
"""
|
|
559
|
+
# Load any remaining lazy features first
|
|
560
|
+
for name in list(self._lazy_queue):
|
|
561
|
+
self.load(name, lazy=False)
|
|
562
|
+
self._lazy_queue.clear()
|
|
563
|
+
|
|
564
|
+
return self.registry.mount_all(context)
|
|
565
|
+
|
|
566
|
+
def unmount(self, name: str) -> None:
|
|
567
|
+
"""Unmount a specific feature."""
|
|
568
|
+
feature = self.registry.get(name)
|
|
569
|
+
if feature:
|
|
570
|
+
feature.unmount()
|
|
571
|
+
|
|
572
|
+
def unmount_all(self) -> Dict[str, Exception]:
|
|
573
|
+
"""Unmount all mounted features."""
|
|
574
|
+
return self.registry.unmount_all()
|
|
575
|
+
|
|
576
|
+
def _is_lazy_feature(self, name: str) -> bool:
|
|
577
|
+
"""Check if a feature supports lazy loading."""
|
|
578
|
+
# For now, use a simple heuristic based on known non-critical features
|
|
579
|
+
# In the future, this could be determined from metadata
|
|
580
|
+
lazy_features = {"glossary", "i18n"}
|
|
581
|
+
return name in lazy_features
|
|
582
|
+
|
|
583
|
+
def _wrap_legacy_feature(self, legacy_class: Type[MonocoFeature]) -> Type[FeatureModule]:
|
|
584
|
+
"""
|
|
585
|
+
Wrap a legacy MonocoFeature class to support the new FeatureModule protocol.
|
|
586
|
+
|
|
587
|
+
This allows gradual migration of existing features.
|
|
588
|
+
"""
|
|
589
|
+
|
|
590
|
+
class LegacyFeatureAdapter(FeatureModule):
|
|
591
|
+
def __init__(self):
|
|
592
|
+
self._legacy = legacy_class()
|
|
593
|
+
self._state = LifecycleState.UNLOADED
|
|
594
|
+
|
|
595
|
+
@property
|
|
596
|
+
def metadata(self) -> FeatureMetadata:
|
|
597
|
+
return FeatureMetadata(
|
|
598
|
+
name=self._legacy.name,
|
|
599
|
+
version="1.0.0",
|
|
600
|
+
description=f"Legacy feature: {self._legacy.name}",
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
@property
|
|
604
|
+
def name(self) -> str:
|
|
605
|
+
return self._legacy.name
|
|
606
|
+
|
|
607
|
+
def _on_mount(self, context: FeatureContext) -> None:
|
|
608
|
+
self._legacy.initialize(context.root, context.config)
|
|
609
|
+
|
|
610
|
+
def integrate(self, root: Path, config: Dict) -> IntegrationData:
|
|
611
|
+
return self._legacy.integrate(root, config)
|
|
612
|
+
|
|
613
|
+
# Preserve the original class name
|
|
614
|
+
LegacyFeatureAdapter.__name__ = f"{legacy_class.__name__}Adapter"
|
|
615
|
+
return LegacyFeatureAdapter
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
# Global loader instance for convenience
|
|
619
|
+
_default_loader: Optional[FeatureLoader] = None
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def get_loader() -> FeatureLoader:
|
|
623
|
+
"""Get the default feature loader instance."""
|
|
624
|
+
global _default_loader
|
|
625
|
+
if _default_loader is None:
|
|
626
|
+
_default_loader = FeatureLoader()
|
|
627
|
+
return _default_loader
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def set_loader(loader: FeatureLoader) -> None:
|
|
631
|
+
"""Set the default feature loader instance."""
|
|
632
|
+
global _default_loader
|
|
633
|
+
_default_loader = loader
|