invarlock 0.2.0__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.
Files changed (132) hide show
  1. invarlock/__init__.py +33 -0
  2. invarlock/__main__.py +10 -0
  3. invarlock/_data/runtime/profiles/ci_cpu.yaml +15 -0
  4. invarlock/_data/runtime/profiles/release.yaml +23 -0
  5. invarlock/_data/runtime/tiers.yaml +76 -0
  6. invarlock/adapters/__init__.py +102 -0
  7. invarlock/adapters/_capabilities.py +45 -0
  8. invarlock/adapters/auto.py +99 -0
  9. invarlock/adapters/base.py +530 -0
  10. invarlock/adapters/base_types.py +85 -0
  11. invarlock/adapters/hf_bert.py +852 -0
  12. invarlock/adapters/hf_gpt2.py +403 -0
  13. invarlock/adapters/hf_llama.py +485 -0
  14. invarlock/adapters/hf_mixin.py +383 -0
  15. invarlock/adapters/hf_onnx.py +112 -0
  16. invarlock/adapters/hf_t5.py +137 -0
  17. invarlock/adapters/py.typed +1 -0
  18. invarlock/assurance/__init__.py +43 -0
  19. invarlock/cli/__init__.py +8 -0
  20. invarlock/cli/__main__.py +8 -0
  21. invarlock/cli/_evidence.py +25 -0
  22. invarlock/cli/_json.py +75 -0
  23. invarlock/cli/adapter_auto.py +162 -0
  24. invarlock/cli/app.py +287 -0
  25. invarlock/cli/commands/__init__.py +26 -0
  26. invarlock/cli/commands/certify.py +403 -0
  27. invarlock/cli/commands/doctor.py +1358 -0
  28. invarlock/cli/commands/explain_gates.py +151 -0
  29. invarlock/cli/commands/export_html.py +100 -0
  30. invarlock/cli/commands/plugins.py +1331 -0
  31. invarlock/cli/commands/report.py +354 -0
  32. invarlock/cli/commands/run.py +4146 -0
  33. invarlock/cli/commands/verify.py +1040 -0
  34. invarlock/cli/config.py +396 -0
  35. invarlock/cli/constants.py +68 -0
  36. invarlock/cli/device.py +92 -0
  37. invarlock/cli/doctor_helpers.py +74 -0
  38. invarlock/cli/errors.py +6 -0
  39. invarlock/cli/overhead_utils.py +60 -0
  40. invarlock/cli/provenance.py +66 -0
  41. invarlock/cli/utils.py +41 -0
  42. invarlock/config.py +56 -0
  43. invarlock/core/__init__.py +62 -0
  44. invarlock/core/abi.py +15 -0
  45. invarlock/core/api.py +274 -0
  46. invarlock/core/auto_tuning.py +317 -0
  47. invarlock/core/bootstrap.py +226 -0
  48. invarlock/core/checkpoint.py +221 -0
  49. invarlock/core/contracts.py +73 -0
  50. invarlock/core/error_utils.py +64 -0
  51. invarlock/core/events.py +298 -0
  52. invarlock/core/exceptions.py +95 -0
  53. invarlock/core/registry.py +481 -0
  54. invarlock/core/retry.py +146 -0
  55. invarlock/core/runner.py +2041 -0
  56. invarlock/core/types.py +154 -0
  57. invarlock/edits/__init__.py +12 -0
  58. invarlock/edits/_edit_utils.py +249 -0
  59. invarlock/edits/_external_utils.py +268 -0
  60. invarlock/edits/noop.py +47 -0
  61. invarlock/edits/py.typed +1 -0
  62. invarlock/edits/quant_rtn.py +801 -0
  63. invarlock/edits/registry.py +166 -0
  64. invarlock/eval/__init__.py +23 -0
  65. invarlock/eval/bench.py +1207 -0
  66. invarlock/eval/bootstrap.py +50 -0
  67. invarlock/eval/data.py +2052 -0
  68. invarlock/eval/metrics.py +2167 -0
  69. invarlock/eval/primary_metric.py +767 -0
  70. invarlock/eval/probes/__init__.py +24 -0
  71. invarlock/eval/probes/fft.py +139 -0
  72. invarlock/eval/probes/mi.py +213 -0
  73. invarlock/eval/probes/post_attention.py +323 -0
  74. invarlock/eval/providers/base.py +67 -0
  75. invarlock/eval/providers/seq2seq.py +111 -0
  76. invarlock/eval/providers/text_lm.py +113 -0
  77. invarlock/eval/providers/vision_text.py +93 -0
  78. invarlock/eval/py.typed +1 -0
  79. invarlock/guards/__init__.py +18 -0
  80. invarlock/guards/_contracts.py +9 -0
  81. invarlock/guards/invariants.py +640 -0
  82. invarlock/guards/policies.py +805 -0
  83. invarlock/guards/py.typed +1 -0
  84. invarlock/guards/rmt.py +2097 -0
  85. invarlock/guards/spectral.py +1419 -0
  86. invarlock/guards/tier_config.py +354 -0
  87. invarlock/guards/variance.py +3298 -0
  88. invarlock/guards_ref/__init__.py +15 -0
  89. invarlock/guards_ref/rmt_ref.py +40 -0
  90. invarlock/guards_ref/spectral_ref.py +135 -0
  91. invarlock/guards_ref/variance_ref.py +60 -0
  92. invarlock/model_profile.py +353 -0
  93. invarlock/model_utils.py +221 -0
  94. invarlock/observability/__init__.py +10 -0
  95. invarlock/observability/alerting.py +535 -0
  96. invarlock/observability/core.py +546 -0
  97. invarlock/observability/exporters.py +565 -0
  98. invarlock/observability/health.py +588 -0
  99. invarlock/observability/metrics.py +457 -0
  100. invarlock/observability/py.typed +1 -0
  101. invarlock/observability/utils.py +553 -0
  102. invarlock/plugins/__init__.py +12 -0
  103. invarlock/plugins/hello_guard.py +33 -0
  104. invarlock/plugins/hf_awq_adapter.py +82 -0
  105. invarlock/plugins/hf_bnb_adapter.py +79 -0
  106. invarlock/plugins/hf_gptq_adapter.py +78 -0
  107. invarlock/plugins/py.typed +1 -0
  108. invarlock/py.typed +1 -0
  109. invarlock/reporting/__init__.py +7 -0
  110. invarlock/reporting/certificate.py +3221 -0
  111. invarlock/reporting/certificate_schema.py +244 -0
  112. invarlock/reporting/dataset_hashing.py +215 -0
  113. invarlock/reporting/guards_analysis.py +948 -0
  114. invarlock/reporting/html.py +32 -0
  115. invarlock/reporting/normalizer.py +235 -0
  116. invarlock/reporting/policy_utils.py +517 -0
  117. invarlock/reporting/primary_metric_utils.py +265 -0
  118. invarlock/reporting/render.py +1442 -0
  119. invarlock/reporting/report.py +903 -0
  120. invarlock/reporting/report_types.py +278 -0
  121. invarlock/reporting/utils.py +175 -0
  122. invarlock/reporting/validate.py +631 -0
  123. invarlock/security.py +176 -0
  124. invarlock/sparsity_utils.py +323 -0
  125. invarlock/utils/__init__.py +150 -0
  126. invarlock/utils/digest.py +45 -0
  127. invarlock-0.2.0.dist-info/METADATA +586 -0
  128. invarlock-0.2.0.dist-info/RECORD +132 -0
  129. invarlock-0.2.0.dist-info/WHEEL +5 -0
  130. invarlock-0.2.0.dist-info/entry_points.txt +20 -0
  131. invarlock-0.2.0.dist-info/licenses/LICENSE +201 -0
  132. invarlock-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+
7
+ @dataclass
8
+ class InvarlockError(Exception):
9
+ code: str
10
+ message: str
11
+ details: dict[str, Any] | None = None
12
+ recoverable: bool = False
13
+
14
+ def __str__(self) -> str: # pragma: no cover - trivial
15
+ return f"[INVARLOCK:{self.code}] {self.message}"
16
+
17
+
18
+ class ConfigError(InvarlockError):
19
+ """Configuration parsing/validation errors."""
20
+
21
+
22
+ class ValidationError(InvarlockError):
23
+ """Domain validation errors for inputs/parameters."""
24
+
25
+
26
+ class DependencyError(InvarlockError):
27
+ """Missing/invalid external dependency (package, binary, model file)."""
28
+
29
+
30
+ class ResourceError(InvarlockError):
31
+ """Insufficient resources (CPU/GPU/RAM/Disk)."""
32
+
33
+
34
+ class TimeoutError(InvarlockError):
35
+ """Operation timed out."""
36
+
37
+
38
+ class DataError(InvarlockError):
39
+ """Dataset/provider errors (shape, availability, corruption)."""
40
+
41
+
42
+ class MetricsError(InvarlockError):
43
+ """Metric computation errors (non-finite, mismatch)."""
44
+
45
+
46
+ class ModelLoadError(InvarlockError):
47
+ """Model/weights loading failures."""
48
+
49
+
50
+ class AdapterError(InvarlockError):
51
+ """Adapter-specific errors (resolution, device mapping)."""
52
+
53
+
54
+ class EditError(InvarlockError):
55
+ """Model edit/transform failures."""
56
+
57
+
58
+ class GuardError(InvarlockError):
59
+ """Guard setup/execution failures."""
60
+
61
+
62
+ class PolicyViolationError(InvarlockError):
63
+ """Guard or policy violation (hard gate)."""
64
+
65
+
66
+ class PluginError(InvarlockError):
67
+ """Plugin resolution/entry-point/import errors."""
68
+
69
+
70
+ class ObservabilityError(InvarlockError):
71
+ """Observability/metrics/export issues."""
72
+
73
+
74
+ class VersionError(InvarlockError):
75
+ """Version/ABI compatibility issues."""
76
+
77
+
78
+ __all__ = [
79
+ "InvarlockError",
80
+ "ConfigError",
81
+ "ValidationError",
82
+ "DependencyError",
83
+ "ResourceError",
84
+ "TimeoutError",
85
+ "DataError",
86
+ "MetricsError",
87
+ "ModelLoadError",
88
+ "AdapterError",
89
+ "EditError",
90
+ "GuardError",
91
+ "PolicyViolationError",
92
+ "PluginError",
93
+ "ObservabilityError",
94
+ "VersionError",
95
+ ]
@@ -0,0 +1,481 @@
1
+ """
2
+ InvarLock Core Registry
3
+ ===================
4
+
5
+ Unified plugin registry using entry point discovery.
6
+ Provides centralized access to adapters, edits, and guards.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib
12
+ import warnings
13
+ from collections.abc import Iterable
14
+ from dataclasses import dataclass
15
+ from importlib.metadata import (
16
+ EntryPoint,
17
+ PackageNotFoundError,
18
+ entry_points,
19
+ )
20
+ from importlib.metadata import (
21
+ version as metadata_version,
22
+ )
23
+ from typing import Any, cast
24
+
25
+ from invarlock import __version__ as INVARLOCK_VERSION
26
+
27
+ from .abi import INVARLOCK_CORE_ABI
28
+ from .api import Guard, ModelAdapter, ModelEdit
29
+ from .exceptions import DependencyError, PluginError
30
+
31
+ __all__ = ["PluginInfo", "CoreRegistry", "get_registry"]
32
+
33
+
34
+ @dataclass
35
+ class PluginInfo:
36
+ """Plugin information from entry points."""
37
+
38
+ name: str
39
+ module: str
40
+ class_name: str
41
+ available: bool
42
+ status: str
43
+ package: str | None = None
44
+ version: str | None = None
45
+ entry_point: Any | None = None
46
+
47
+
48
+ def _select_entry_points(eps: Any, group: str) -> list[EntryPoint]:
49
+ """Return entry points for a given group across importlib versions."""
50
+
51
+ selected: Iterable[EntryPoint]
52
+ if hasattr(eps, "select"):
53
+ selected = cast("Iterable[EntryPoint]", eps.select(group=group))
54
+ else:
55
+ selected = cast("Iterable[EntryPoint]", eps.get(group, []))
56
+ return list(selected)
57
+
58
+
59
+ class CoreRegistry:
60
+ """
61
+ Central registry for InvarLock plugins using entry point discovery.
62
+
63
+ Discovers and manages adapters, edits, and guards through
64
+ setuptools entry points without requiring imports.
65
+ """
66
+
67
+ def __init__(self):
68
+ self._adapters: dict[str, PluginInfo] = {}
69
+ self._edits: dict[str, PluginInfo] = {}
70
+ self._guards: dict[str, PluginInfo] = {}
71
+ self._initialized = False
72
+
73
+ def _ensure_initialized(self) -> None:
74
+ """Lazy initialization of plugin discovery."""
75
+ if self._initialized:
76
+ return
77
+
78
+ self._discover_plugins()
79
+ self._initialized = True
80
+
81
+ def _discover_plugins(self) -> None:
82
+ """Discover all plugins through entry points with fallback registration."""
83
+ # Try entry points first
84
+ try:
85
+ eps = entry_points()
86
+
87
+ # Discover adapters
88
+ adapter_eps = _select_entry_points(eps, "invarlock.adapters")
89
+ for ep in adapter_eps:
90
+ info = self._create_plugin_info(ep, "adapters")
91
+ self._adapters[ep.name] = info
92
+
93
+ # Discover edits
94
+ edit_eps = _select_entry_points(eps, "invarlock.edits")
95
+ for ep in edit_eps:
96
+ info = self._create_plugin_info(ep, "edits")
97
+ self._edits[ep.name] = info
98
+
99
+ # Discover guards
100
+ guard_eps = _select_entry_points(eps, "invarlock.guards")
101
+ for ep in guard_eps:
102
+ info = self._create_plugin_info(ep, "guards")
103
+ self._guards[ep.name] = info
104
+
105
+ except Exception as e:
106
+ warnings.warn(f"Plugin discovery failed: {e}", stacklevel=2)
107
+
108
+ # Fallback registration for development
109
+ self._register_fallback_plugins()
110
+
111
+ def _register_fallback_plugins(self) -> None:
112
+ """Register fallback plugins when entry points are not available."""
113
+
114
+ def _fallback(
115
+ registry: dict[str, PluginInfo],
116
+ name: str,
117
+ module: str,
118
+ class_name: str,
119
+ status: str = "Available (fallback)",
120
+ ) -> None:
121
+ if name not in registry:
122
+ registry[name] = PluginInfo(
123
+ name=name,
124
+ module=module,
125
+ class_name=class_name,
126
+ available=True,
127
+ status=status,
128
+ package="invarlock",
129
+ version=INVARLOCK_VERSION,
130
+ )
131
+
132
+ # Register built-in adapters
133
+ _fallback(self._adapters, "hf_gpt2", "invarlock.adapters", "HF_GPT2_Adapter")
134
+ _fallback(self._adapters, "hf_bert", "invarlock.adapters", "HF_BERT_Adapter")
135
+ _fallback(self._adapters, "hf_llama", "invarlock.adapters", "HF_LLaMA_Adapter")
136
+ _fallback(self._adapters, "hf_t5", "invarlock.adapters", "HF_T5_Adapter")
137
+ _fallback(
138
+ self._adapters, "hf_onnx", "invarlock.adapters", "HF_ORT_CausalLM_Adapter"
139
+ )
140
+ # Convenience auto adapters (delegate to built-ins)
141
+ _fallback(
142
+ self._adapters,
143
+ "hf_causal_auto",
144
+ "invarlock.adapters",
145
+ "HF_Causal_Auto_Adapter",
146
+ )
147
+ _fallback(
148
+ self._adapters, "hf_mlm_auto", "invarlock.adapters", "HF_MLM_Auto_Adapter"
149
+ )
150
+ # Optional plugin adapters (available when modules present)
151
+ _fallback(
152
+ self._adapters,
153
+ "hf_gptq",
154
+ "invarlock.plugins.hf_gptq_adapter",
155
+ "HF_GPTQ_Adapter",
156
+ status="Available (fallback plugin)",
157
+ )
158
+ _fallback(
159
+ self._adapters,
160
+ "hf_awq",
161
+ "invarlock.plugins.hf_awq_adapter",
162
+ "HF_AWQ_Adapter",
163
+ status="Available (fallback plugin)",
164
+ )
165
+ _fallback(
166
+ self._adapters,
167
+ "hf_bnb",
168
+ "invarlock.plugins.hf_bnb_adapter",
169
+ "HF_BNB_Adapter",
170
+ status="Available (fallback plugin)",
171
+ )
172
+
173
+ # Register built-in edits (quant-only core) and internal no-op
174
+ _fallback(self._edits, "quant_rtn", "invarlock.edits", "RTNQuantEdit")
175
+ _fallback(self._edits, "noop", "invarlock.edits.noop", "NoopEdit")
176
+
177
+ # Register built-in guards
178
+ _fallback(self._guards, "invariants", "invarlock.guards", "InvariantsGuard")
179
+ _fallback(self._guards, "spectral", "invarlock.guards", "SpectralGuard")
180
+ _fallback(self._guards, "variance", "invarlock.guards", "VarianceGuard")
181
+ _fallback(self._guards, "rmt", "invarlock.guards", "RMTGuard")
182
+ _fallback(self._guards, "hello_guard", "invarlock.plugins", "HelloGuard")
183
+
184
+ def _create_plugin_info(
185
+ self, entry_point: EntryPoint, plugin_type: str
186
+ ) -> PluginInfo:
187
+ """Create plugin info from entry point."""
188
+ try:
189
+ # Parse module and class from entry point value
190
+ module_path, class_name = entry_point.value.split(":")
191
+
192
+ # Determine package/version metadata
193
+ package_name: str | None = None
194
+ version: str | None = None
195
+
196
+ dist = getattr(entry_point, "dist", None)
197
+ if dist is not None:
198
+ package_name = getattr(dist, "metadata", {}).get("Name") or getattr(
199
+ dist, "name", None
200
+ )
201
+ version = getattr(dist, "version", None)
202
+
203
+ if not package_name:
204
+ package_name = module_path.split(".")[0]
205
+ try:
206
+ version = metadata_version(package_name)
207
+ except PackageNotFoundError:
208
+ version = None
209
+
210
+ # Defer import to instantiation time to avoid heavy imports here
211
+ available = True
212
+ status = "Deferred load"
213
+
214
+ return PluginInfo(
215
+ name=entry_point.name,
216
+ module=module_path,
217
+ class_name=class_name,
218
+ available=available,
219
+ status=status,
220
+ package=package_name,
221
+ version=version,
222
+ entry_point=entry_point,
223
+ )
224
+
225
+ except Exception as e:
226
+ return PluginInfo(
227
+ name=entry_point.name,
228
+ module="unknown",
229
+ class_name="unknown",
230
+ available=False,
231
+ status=f"Parse error: {e}",
232
+ entry_point=entry_point,
233
+ )
234
+
235
+ def list_adapters(self) -> list[str]:
236
+ """List all registered adapter names."""
237
+ self._ensure_initialized()
238
+ return list(self._adapters.keys())
239
+
240
+ def list_edits(self) -> list[str]:
241
+ """List all registered edit names."""
242
+ self._ensure_initialized()
243
+ return list(self._edits.keys())
244
+
245
+ def list_guards(self) -> list[str]:
246
+ """List all registered guard names."""
247
+ self._ensure_initialized()
248
+ return list(self._guards.keys())
249
+
250
+ def get_adapter(self, name: str) -> ModelAdapter:
251
+ """Get an adapter instance by name."""
252
+ self._ensure_initialized()
253
+
254
+ if name not in self._adapters:
255
+ available = list(self._adapters.keys())
256
+ raise KeyError(f"Unknown adapter '{name}'. Available: {available}")
257
+
258
+ info = self._adapters[name]
259
+ if not info.available:
260
+ raise ImportError(f"Adapter '{name}' unavailable: {info.status}")
261
+
262
+ try:
263
+ if info.entry_point:
264
+ cls = info.entry_point.load()
265
+ else:
266
+ # Fallback loading
267
+ module = importlib.import_module(info.module)
268
+ cls = getattr(module, info.class_name)
269
+ # ABI compatibility check on the providing module
270
+ try: # pragma: no cover - simple guard
271
+ provider_mod = importlib.import_module(cls.__module__)
272
+ plugin_abi = getattr(provider_mod, "INVARLOCK_CORE_ABI", None)
273
+ if plugin_abi is not None and str(plugin_abi) != INVARLOCK_CORE_ABI:
274
+ raise ImportError(
275
+ f"ABI mismatch: plugin={plugin_abi} != core={INVARLOCK_CORE_ABI}"
276
+ )
277
+ except Exception as abi_exc:
278
+ raise ImportError(str(abi_exc)) from abi_exc
279
+ instance = cls()
280
+ if not isinstance(instance, ModelAdapter):
281
+ raise TypeError(f"Expected ModelAdapter, got {type(instance)}")
282
+ return instance
283
+ except Exception as e:
284
+ raise ImportError(f"Failed to load adapter '{name}': {e}") from e
285
+
286
+ def get_edit(self, name: str) -> ModelEdit:
287
+ """Get an edit instance by name."""
288
+ self._ensure_initialized()
289
+
290
+ if name not in self._edits:
291
+ available = list(self._edits.keys())
292
+ raise KeyError(f"Unknown edit '{name}'. Available: {available}")
293
+
294
+ info = self._edits[name]
295
+ if not info.available:
296
+ raise ImportError(f"Edit '{name}' unavailable: {info.status}")
297
+
298
+ try:
299
+ if info.entry_point:
300
+ cls = info.entry_point.load()
301
+ else:
302
+ # Fallback loading
303
+ module = importlib.import_module(info.module)
304
+ cls = getattr(module, info.class_name)
305
+ try: # ABI check
306
+ provider_mod = importlib.import_module(cls.__module__)
307
+ plugin_abi = getattr(provider_mod, "INVARLOCK_CORE_ABI", None)
308
+ if plugin_abi is not None and str(plugin_abi) != INVARLOCK_CORE_ABI:
309
+ raise ImportError(
310
+ f"ABI mismatch: plugin={plugin_abi} != core={INVARLOCK_CORE_ABI}"
311
+ )
312
+ except Exception as abi_exc:
313
+ raise ImportError(str(abi_exc)) from abi_exc
314
+ instance = cls()
315
+ if not isinstance(instance, ModelEdit):
316
+ raise TypeError(f"Expected ModelEdit, got {type(instance)}")
317
+ return instance
318
+ except Exception as e:
319
+ raise ImportError(f"Failed to load edit '{name}': {e}") from e
320
+
321
+ def get_guard(self, name: str) -> Guard:
322
+ """Get a guard instance by name."""
323
+ self._ensure_initialized()
324
+
325
+ if name not in self._guards:
326
+ available = list(self._guards.keys())
327
+ raise KeyError(f"Unknown guard '{name}'. Available: {available}")
328
+
329
+ info = self._guards[name]
330
+ if not info.available:
331
+ raise ImportError(f"Guard '{name}' unavailable: {info.status}")
332
+
333
+ try:
334
+ if info.entry_point:
335
+ cls = info.entry_point.load()
336
+ else:
337
+ # Fallback loading
338
+ module = importlib.import_module(info.module)
339
+ cls = getattr(module, info.class_name)
340
+ try: # ABI check
341
+ provider_mod = importlib.import_module(cls.__module__)
342
+ plugin_abi = getattr(provider_mod, "INVARLOCK_CORE_ABI", None)
343
+ if plugin_abi is not None and str(plugin_abi) != INVARLOCK_CORE_ABI:
344
+ raise ImportError(
345
+ f"ABI mismatch: plugin={plugin_abi} != core={INVARLOCK_CORE_ABI}"
346
+ )
347
+ except Exception as abi_exc:
348
+ raise ImportError(str(abi_exc)) from abi_exc
349
+ instance = cls()
350
+ if not isinstance(instance, Guard):
351
+ raise TypeError(f"Expected Guard, got {type(instance)}")
352
+ return instance
353
+ except Exception as e:
354
+ raise ImportError(f"Failed to load guard '{name}': {e}") from e
355
+
356
+ def get_plugin_info(self, name: str, plugin_type: str) -> dict[str, Any]:
357
+ """Get plugin information without instantiation."""
358
+ self._ensure_initialized()
359
+
360
+ if plugin_type == "adapters":
361
+ registry = self._adapters
362
+ entry_group = "invarlock.adapters"
363
+ elif plugin_type == "edits":
364
+ registry = self._edits
365
+ entry_group = "invarlock.edits"
366
+ elif plugin_type == "guards":
367
+ registry = self._guards
368
+ entry_group = "invarlock.guards"
369
+ else:
370
+ raise ValueError(f"Unknown plugin type: {plugin_type}")
371
+
372
+ if name not in registry:
373
+ return {"available": False, "status": "Not found", "module": "unknown"}
374
+
375
+ info = registry[name]
376
+ return {
377
+ "available": info.available,
378
+ "status": info.status,
379
+ "module": info.module,
380
+ "package": info.package,
381
+ "version": info.version,
382
+ "entry_point": info.entry_point.name if info.entry_point else None,
383
+ "entry_point_group": entry_group if info.entry_point else None,
384
+ }
385
+
386
+ def get_plugin_metadata(self, name: str, plugin_type: str) -> dict[str, Any]:
387
+ """Return comprehensive metadata for a plugin."""
388
+ metadata = self.get_plugin_info(name, plugin_type)
389
+ if metadata.get("module") == "unknown":
390
+ raise KeyError(f"Unknown {plugin_type.rstrip('s')} plugin '{name}'")
391
+
392
+ metadata.update(
393
+ {
394
+ "name": name,
395
+ "type": plugin_type,
396
+ }
397
+ )
398
+ return metadata
399
+
400
+ # Typed-error wrappers that preserve existing behavior for legacy methods
401
+ def get_adapter_typed(self, name: str) -> ModelAdapter:
402
+ try:
403
+ return self.get_adapter(name)
404
+ except Exception as e: # pragma: no cover - exercised in tests
405
+ details = {"name": name, "kind": "adapter", "reason": type(e).__name__}
406
+ if isinstance(e, ImportError | ModuleNotFoundError):
407
+ raise DependencyError(
408
+ code="E702", message="PLUGIN-DEPENDENCY-MISSING", details=details
409
+ ) from e
410
+ raise PluginError(
411
+ code="E701", message="PLUGIN-LOAD-FAILED", details=details
412
+ ) from e
413
+
414
+ def get_edit_typed(self, name: str) -> ModelEdit:
415
+ try:
416
+ return self.get_edit(name)
417
+ except Exception as e: # pragma: no cover - exercised in tests
418
+ details = {"name": name, "kind": "edit", "reason": type(e).__name__}
419
+ if isinstance(e, ImportError | ModuleNotFoundError):
420
+ raise DependencyError(
421
+ code="E702", message="PLUGIN-DEPENDENCY-MISSING", details=details
422
+ ) from e
423
+ raise PluginError(
424
+ code="E701", message="PLUGIN-LOAD-FAILED", details=details
425
+ ) from e
426
+
427
+ def get_guard_typed(self, name: str) -> Guard:
428
+ try:
429
+ return self.get_guard(name)
430
+ except Exception as e: # pragma: no cover - exercised in tests
431
+ details = {"name": name, "kind": "guard", "reason": type(e).__name__}
432
+ if isinstance(e, ImportError | ModuleNotFoundError):
433
+ raise DependencyError(
434
+ code="E702", message="PLUGIN-DEPENDENCY-MISSING", details=details
435
+ ) from e
436
+ raise PluginError(
437
+ code="E701", message="PLUGIN-LOAD-FAILED", details=details
438
+ ) from e
439
+
440
+ def validate_configuration(
441
+ self, adapter_name: str, edit_name: str, guard_names: list[str]
442
+ ) -> tuple[bool, str]:
443
+ """Validate that a configuration is available."""
444
+ self._ensure_initialized()
445
+
446
+ issues = []
447
+
448
+ # Check adapter
449
+ if adapter_name not in self._adapters:
450
+ issues.append(f"Unknown adapter: {adapter_name}")
451
+ elif not self._adapters[adapter_name].available:
452
+ issues.append(f"Adapter unavailable: {adapter_name}")
453
+
454
+ # Check edit
455
+ if edit_name not in self._edits:
456
+ issues.append(f"Unknown edit: {edit_name}")
457
+ elif not self._edits[edit_name].available:
458
+ issues.append(f"Edit unavailable: {edit_name}")
459
+
460
+ # Check guards
461
+ for guard_name in guard_names:
462
+ if guard_name == "noop":
463
+ continue # noop is always available
464
+ if guard_name not in self._guards:
465
+ issues.append(f"Unknown guard: {guard_name}")
466
+ elif not self._guards[guard_name].available:
467
+ issues.append(f"Guard unavailable: {guard_name}")
468
+
469
+ if issues:
470
+ return False, "; ".join(issues)
471
+
472
+ return True, "Configuration is valid"
473
+
474
+
475
+ # Global registry instance
476
+ _global_registry = CoreRegistry()
477
+
478
+
479
+ def get_registry() -> CoreRegistry:
480
+ """Get the global plugin registry instance."""
481
+ return _global_registry