skillpool 4.3.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 (90) hide show
  1. skillpool/__init__.py +74 -0
  2. skillpool/__main__.py +6 -0
  3. skillpool/adapters/__init__.py +8 -0
  4. skillpool/adapters/base.py +41 -0
  5. skillpool/adapters/claude_adapter.py +36 -0
  6. skillpool/adapters/codex_adapter.py +92 -0
  7. skillpool/adapters/hermes_adapter.py +38 -0
  8. skillpool/audit/__init__.py +651 -0
  9. skillpool/bridge/__init__.py +16 -0
  10. skillpool/bridge/freeze_detector.py +134 -0
  11. skillpool/bridge/maintenance.py +119 -0
  12. skillpool/bridge/wal_manager.py +136 -0
  13. skillpool/clawmem_client.py +176 -0
  14. skillpool/cli.py +700 -0
  15. skillpool/combiner/__init__.py +31 -0
  16. skillpool/combiner/lifecycle.py +453 -0
  17. skillpool/combiner/models.py +99 -0
  18. skillpool/config.py +34 -0
  19. skillpool/cost/__init__.py +111 -0
  20. skillpool/cost/audit_hash.py +51 -0
  21. skillpool/cost/budget_tracker.py +66 -0
  22. skillpool/cost/dashboard.py +189 -0
  23. skillpool/cost/models.py +129 -0
  24. skillpool/cost/token_governor.py +264 -0
  25. skillpool/cost/trace_ceiling.py +38 -0
  26. skillpool/csdf.py +126 -0
  27. skillpool/evolver/__init__.py +978 -0
  28. skillpool/gain/__init__.py +285 -0
  29. skillpool/gate.py +282 -0
  30. skillpool/gate_policy/__init__.py +31 -0
  31. skillpool/gate_policy/incremental.py +157 -0
  32. skillpool/gate_policy/parser.py +258 -0
  33. skillpool/gate_policy/state_machine.py +432 -0
  34. skillpool/graph/__init__.py +14 -0
  35. skillpool/graph/ppr.py +279 -0
  36. skillpool/health/__init__.py +73 -0
  37. skillpool/health/check.py +85 -0
  38. skillpool/health/degradation.py +90 -0
  39. skillpool/health/models.py +43 -0
  40. skillpool/hooks/__init__.py +4 -0
  41. skillpool/hooks/security_scanner.py +288 -0
  42. skillpool/lifecycle.py +150 -0
  43. skillpool/materializer/__init__.py +124 -0
  44. skillpool/materializer/budget_cropper.py +178 -0
  45. skillpool/materializer/csdf_loader.py +114 -0
  46. skillpool/materializer/lazy_loader.py +265 -0
  47. skillpool/materializer/lifecycle_filter.py +93 -0
  48. skillpool/materializer/mapper.py +178 -0
  49. skillpool/materializer/models.py +66 -0
  50. skillpool/mcp_server.py +2005 -0
  51. skillpool/monitor/__init__.py +576 -0
  52. skillpool/monitor/bug_collector.py +392 -0
  53. skillpool/monitor/defect_classifier.py +218 -0
  54. skillpool/monitor/self_healing.py +530 -0
  55. skillpool/monitor/telemetry_bridge.py +197 -0
  56. skillpool/paradigm/__init__.py +312 -0
  57. skillpool/paradigm/override.py +285 -0
  58. skillpool/profile.py +94 -0
  59. skillpool/quality.py +254 -0
  60. skillpool/registry/__init__.py +509 -0
  61. skillpool/registry/models.py +98 -0
  62. skillpool/resolver/__init__.py +320 -0
  63. skillpool/resolver/cache.py +103 -0
  64. skillpool/resolver/circuit_breaker.py +103 -0
  65. skillpool/resolver/conflict_detector.py +111 -0
  66. skillpool/resolver/health_filter.py +38 -0
  67. skillpool/resolver/models.py +154 -0
  68. skillpool/resolver/rate_limiter.py +48 -0
  69. skillpool/resolver/skill_graph.py +183 -0
  70. skillpool/review/__init__.py +242 -0
  71. skillpool/review/async_queue.py +96 -0
  72. skillpool/review/checkpoint_runner.py +345 -0
  73. skillpool/review/models.py +164 -0
  74. skillpool/review/suspect_marker.py +39 -0
  75. skillpool/review/veto_evaluator.py +94 -0
  76. skillpool/router/__init__.py +481 -0
  77. skillpool/schemas.py +119 -0
  78. skillpool/synergy/__init__.py +240 -0
  79. skillpool/synergy/detector.py +5 -0
  80. skillpool/telemetry.py +126 -0
  81. skillpool/utils/__init__.py +21 -0
  82. skillpool/utils/changelog.py +218 -0
  83. skillpool/utils/logger.py +273 -0
  84. skillpool/utils/runtime_audit.py +163 -0
  85. skillpool/utils/time_utils.py +13 -0
  86. skillpool-4.3.0.dist-info/METADATA +21 -0
  87. skillpool-4.3.0.dist-info/RECORD +90 -0
  88. skillpool-4.3.0.dist-info/WHEEL +5 -0
  89. skillpool-4.3.0.dist-info/entry_points.txt +3 -0
  90. skillpool-4.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,392 @@
1
+ """BugCollector — Sentry-inspired 4-stage bug collection pipeline.
2
+
3
+ Stages: Capture -> Enrich -> Filter -> Persist
4
+
5
+ Capture: Intercept exceptions via sys.excepthook + manual record() calls
6
+ Enrich: Auto-attach skill_id, trace_id, checkpoint, gate_result, defect_type
7
+ Filter: Sample rate (test=1.0, prod=0.1), before_persist hook for noise filtering
8
+ Persist: Write to JSONL at ~/.skillpool/logs/bugs.jsonl (with fsync) + append to AuditLayer hash chain
9
+
10
+ Part of SkillPool — independent infrastructure, shared by all agents.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ __all__ = [
16
+ "BugCollector",
17
+ "BugRecord",
18
+ "BugSeverity",
19
+ "DefectType",
20
+ ]
21
+
22
+ import json
23
+ import logging
24
+ import os
25
+ import random
26
+ import sys
27
+ import traceback
28
+ import uuid
29
+ from collections.abc import Callable
30
+ from dataclasses import asdict, dataclass, field
31
+ from enum import StrEnum
32
+ from pathlib import Path
33
+ from typing import Any
34
+
35
+ from skillpool.config import get_data_dir
36
+ from skillpool.utils.time_utils import utc_now
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ class BugSeverity(StrEnum):
42
+ """Bug severity levels (Sentry-inspired)."""
43
+
44
+ P0 = "P0"
45
+ P1 = "P1"
46
+ P2 = "P2"
47
+
48
+
49
+ class DefectType(StrEnum):
50
+ """Defect classification from ProcCtrlBench — 11 types."""
51
+
52
+ PARAM_ERROR = "PARAM_ERROR"
53
+ PERMISSION_BREACH = "PERMISSION_BREACH"
54
+ TIMEOUT = "TIMEOUT"
55
+ DEPENDENCY_MISSING = "DEPENDENCY_MISSING"
56
+ EXECUTION_FAILURE = "EXECUTION_FAILURE"
57
+ OUTPUT_INVALID = "OUTPUT_INVALID"
58
+ STATE_CORRUPTION = "STATE_CORRUPTION"
59
+ RESOURCE_EXHAUSTION = "RESOURCE_EXHAUSTION"
60
+ GATE_DENIED = "GATE_DENIED"
61
+ PROTOCOL_ERROR = "PROTOCOL_ERROR"
62
+ UNKNOWN = "UNKNOWN"
63
+
64
+
65
+ @dataclass
66
+ class BugRecord:
67
+ """Immutable bug record produced by the 4-stage pipeline."""
68
+
69
+ bug_id: str
70
+ timestamp: str
71
+ severity: BugSeverity
72
+ defect_type: DefectType
73
+ message: str
74
+ skill_id: str = ""
75
+ trace_id: str = ""
76
+ traceback: str = ""
77
+ context: dict[str, Any] = field(default_factory=dict)
78
+
79
+ def to_dict(self) -> dict[str, Any]:
80
+ """Serialize to dict (JSON-safe)."""
81
+ d = asdict(self)
82
+ d["severity"] = self.severity.value
83
+ d["defect_type"] = self.defect_type.value
84
+ return d
85
+
86
+ def to_jsonl(self) -> str:
87
+ """Serialize to single JSON line for JSONL persistence."""
88
+ return json.dumps(self.to_dict(), sort_keys=True, ensure_ascii=False)
89
+
90
+
91
+ class BugCollector:
92
+ """4-stage bug collection pipeline: Capture -> Enrich -> Filter -> Persist.
93
+
94
+ Args:
95
+ audit_layer: Optional AuditLayer instance for hash chain persistence.
96
+ sample_rate: Fraction of bugs to persist (1.0 = all, 0.1 = 10%).
97
+ before_persist: Optional hook called before persist; return False to drop.
98
+ log_dir: Override for persistence directory (default: ~/.skillpool/logs).
99
+ """
100
+
101
+ def __init__(
102
+ self,
103
+ audit_layer: Any | None = None,
104
+ sample_rate: float = 1.0,
105
+ before_persist: Callable[[BugRecord], bool] | None = None,
106
+ log_dir: Path | None = None,
107
+ ) -> None:
108
+ self._audit = audit_layer
109
+ self._sample_rate = max(0.0, min(1.0, sample_rate))
110
+ self._before_persist = before_persist
111
+ self._log_dir = log_dir or get_data_dir() / "logs"
112
+ self._bugs: list[BugRecord] = []
113
+ self._original_excepthook: Any = None
114
+ self._rng = random.Random(42)
115
+
116
+ # ── Stage 1: Capture ──
117
+
118
+ def record(
119
+ self,
120
+ severity: BugSeverity,
121
+ defect_type: DefectType,
122
+ message: str,
123
+ skill_id: str | None = None,
124
+ context: dict[str, Any] | None = None,
125
+ ) -> BugRecord:
126
+ """Manually record a bug (Capture stage entry point).
127
+
128
+ Runs the full 4-stage pipeline and returns the BugRecord
129
+ (even if filtered out from persistence).
130
+ """
131
+ rec = self._create_record(
132
+ severity=severity,
133
+ defect_type=defect_type,
134
+ message=message,
135
+ skill_id=skill_id or "",
136
+ tb="",
137
+ context=context or {},
138
+ )
139
+ return self._pipeline(rec)
140
+
141
+ def capture_exception(
142
+ self,
143
+ exc: BaseException,
144
+ skill_id: str | None = None,
145
+ ) -> BugRecord:
146
+ """Capture an exception with auto-extracted traceback.
147
+
148
+ Maps exception type to DefectType heuristically.
149
+ """
150
+ tb_str = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
151
+ defect_type = self._classify_exception(exc)
152
+ severity = self._severity_from_exception(exc)
153
+
154
+ rec = self._create_record(
155
+ severity=severity,
156
+ defect_type=defect_type,
157
+ message=str(exc),
158
+ skill_id=skill_id or "",
159
+ tb=tb_str,
160
+ context={"exc_type": type(exc).__name__},
161
+ )
162
+ return self._pipeline(rec)
163
+
164
+ def install_excepthook(self) -> None:
165
+ """Install sys.excepthook to auto-capture unhandled exceptions."""
166
+ if self._original_excepthook is not None:
167
+ return # Already installed
168
+ self._original_excepthook = sys.excepthook
169
+ collector = self
170
+
171
+ def _hook(exc_type, exc_value, exc_tb): # type: ignore[no-untyped-def]
172
+ # KeyboardInterrupt/SystemExit are control flow signals, not bugs
173
+ if issubclass(exc_type, (KeyboardInterrupt, SystemExit)):
174
+ if collector._original_excepthook is not None:
175
+ collector._original_excepthook(exc_type, exc_value, exc_tb)
176
+ return
177
+ try:
178
+ tb_str = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
179
+ defect_type = collector._classify_exception(exc_value)
180
+ severity = collector._severity_from_exception(exc_value)
181
+ rec = collector._create_record(
182
+ severity=severity,
183
+ defect_type=defect_type,
184
+ message=str(exc_value),
185
+ skill_id="",
186
+ tb=tb_str,
187
+ context={"exc_type": exc_type.__name__, "source": "excepthook"},
188
+ )
189
+ collector._pipeline(rec)
190
+ except Exception as e:
191
+ logger.warning("BugCollector excepthook failed: %s", e) # Never let the hook itself crash
192
+ finally:
193
+ if collector._original_excepthook is not None:
194
+ collector._original_excepthook(exc_type, exc_value, exc_tb)
195
+
196
+ sys.excepthook = _hook
197
+
198
+ def uninstall_excepthook(self) -> None:
199
+ """Restore original sys.excepthook."""
200
+ if self._original_excepthook is not None:
201
+ sys.excepthook = self._original_excepthook
202
+ self._original_excepthook = None
203
+
204
+ # ── Query ──
205
+
206
+ def get_bugs(
207
+ self,
208
+ severity: BugSeverity | None = None,
209
+ defect_type: DefectType | None = None,
210
+ skill_id: str | None = None,
211
+ ) -> list[BugRecord]:
212
+ """Query collected bugs with optional filters."""
213
+ bugs = self._bugs
214
+ if severity is not None:
215
+ bugs = [b for b in bugs if b.severity == severity]
216
+ if defect_type is not None:
217
+ bugs = [b for b in bugs if b.defect_type == defect_type]
218
+ if skill_id is not None:
219
+ bugs = [b for b in bugs if b.skill_id == skill_id]
220
+ return bugs
221
+
222
+ def get_stats(self) -> dict[str, Any]:
223
+ """Return aggregate counts by severity and defect_type."""
224
+ by_severity: dict[str, int] = {}
225
+ by_defect: dict[str, int] = {}
226
+ for bug in self._bugs:
227
+ by_severity[bug.severity.value] = by_severity.get(bug.severity.value, 0) + 1
228
+ by_defect[bug.defect_type.value] = by_defect.get(bug.defect_type.value, 0) + 1
229
+ return {
230
+ "total": len(self._bugs),
231
+ "by_severity": by_severity,
232
+ "by_defect_type": by_defect,
233
+ }
234
+
235
+ # ── Internal: Pipeline ──
236
+
237
+ def _create_record(
238
+ self,
239
+ severity: BugSeverity,
240
+ defect_type: DefectType,
241
+ message: str,
242
+ skill_id: str,
243
+ tb: str,
244
+ context: dict[str, Any],
245
+ ) -> BugRecord:
246
+ """Create a BugRecord with auto-generated id and timestamp."""
247
+ return BugRecord(
248
+ bug_id=f"bug-{uuid.uuid4().hex[:12]}",
249
+ timestamp=utc_now().isoformat(),
250
+ severity=severity,
251
+ defect_type=defect_type,
252
+ message=message,
253
+ skill_id=skill_id,
254
+ trace_id="",
255
+ traceback=tb,
256
+ context=context,
257
+ )
258
+
259
+ def _pipeline(self, record: BugRecord) -> BugRecord:
260
+ """Run the 4-stage pipeline: Capture -> Enrich -> Filter -> Persist."""
261
+ # Stage 2: Enrich
262
+ record = self._enrich(record)
263
+ # Always store in-memory (regardless of filter outcome)
264
+ self._bugs.append(record)
265
+ # Stage 3: Filter
266
+ if not self._filter(record):
267
+ return record
268
+ # Stage 4: Persist
269
+ self._persist(record)
270
+ return record
271
+
272
+ # ── Stage 2: Enrich ──
273
+
274
+ def _enrich(self, record: BugRecord) -> BugRecord:
275
+ """Auto-attach trace_id and context from environment."""
276
+ if not record.trace_id:
277
+ record.trace_id = os.urandom(16).hex()
278
+
279
+ env_info: dict[str, str] = {}
280
+ if "SKILLPOOL_ENV" in os.environ:
281
+ env_info["env"] = os.environ["SKILLPOOL_ENV"]
282
+ if env_info:
283
+ record.context.update(env_info)
284
+
285
+ return record
286
+
287
+ # ── Stage 3: Filter ──
288
+
289
+ def _filter(self, record: BugRecord) -> bool:
290
+ """Determine if a record should be persisted.
291
+
292
+ Returns True if the record passes both sampling and the before_persist hook.
293
+ """
294
+ if self._sample_rate < 1.0 and self._rng.random() >= self._sample_rate:
295
+ return False
296
+
297
+ if self._before_persist is not None:
298
+ try:
299
+ return self._before_persist(record)
300
+ except Exception as e:
301
+ logger.warning("before_persist hook failed, persisting by default: %s", e)
302
+ return True # Hook error -> persist by default
303
+
304
+ return True
305
+
306
+ # ── Stage 4: Persist ──
307
+
308
+ def _persist(self, record: BugRecord) -> None:
309
+ """Write to JSONL file and append to AuditLayer hash chain.
310
+
311
+ Uses fsync for crash safety — partial lines from crashes are
312
+ truncated on recovery by downstream consumers.
313
+ """
314
+ # JSONL persistence
315
+ try:
316
+ self._log_dir.mkdir(parents=True, exist_ok=True)
317
+ log_path = self._log_dir / "bugs.jsonl"
318
+ line = record.to_jsonl() + "\n"
319
+ with open(log_path, "a", encoding="utf-8") as f:
320
+ f.write(line)
321
+ f.flush()
322
+ os.fsync(f.fileno())
323
+ except OSError:
324
+ pass # Filesystem errors should not crash the pipeline
325
+
326
+ # Audit chain persistence
327
+ if self._audit is not None:
328
+ try:
329
+ self._audit.append(
330
+ action="bug_collected",
331
+ object_id=record.bug_id,
332
+ result=record.severity.value,
333
+ reason=record.message[:200],
334
+ severity=self._map_severity(record.severity),
335
+ metadata=record.to_dict(),
336
+ trace_id=record.trace_id,
337
+ )
338
+ except Exception as e:
339
+ logger.warning(
340
+ "Audit record failed for bug %s: %s", record.bug_id, e
341
+ ) # Audit errors should not crash the pipeline
342
+
343
+ @staticmethod
344
+ def _map_severity(severity: BugSeverity) -> str:
345
+ """Map BugSeverity to AuditLayer severity string."""
346
+ mapping = {
347
+ BugSeverity.P0: "CRITICAL",
348
+ BugSeverity.P1: "ERROR",
349
+ BugSeverity.P2: "WARN",
350
+ }
351
+ return mapping.get(severity, "INFO")
352
+
353
+ # ── Exception classification ──
354
+
355
+ @staticmethod
356
+ def _classify_exception(exc: BaseException) -> DefectType:
357
+ """Heuristically map exception type to DefectType."""
358
+ exc_name = type(exc).__name__
359
+ mapping: dict[str, DefectType] = {
360
+ "TimeoutError": DefectType.TIMEOUT,
361
+ "ConnectionTimeoutError": DefectType.TIMEOUT,
362
+ "PermissionError": DefectType.PERMISSION_BREACH,
363
+ "PermissionDenied": DefectType.PERMISSION_BREACH,
364
+ "ImportError": DefectType.DEPENDENCY_MISSING,
365
+ "ModuleNotFoundError": DefectType.DEPENDENCY_MISSING,
366
+ "FileNotFoundError": DefectType.DEPENDENCY_MISSING,
367
+ "ValueError": DefectType.PARAM_ERROR,
368
+ "TypeError": DefectType.PARAM_ERROR,
369
+ "KeyError": DefectType.PARAM_ERROR,
370
+ "AssertionError": DefectType.OUTPUT_INVALID,
371
+ "RuntimeError": DefectType.EXECUTION_FAILURE,
372
+ "OSError": DefectType.RESOURCE_EXHAUSTION,
373
+ "MemoryError": DefectType.RESOURCE_EXHAUSTION,
374
+ "ConnectionRefusedError": DefectType.RESOURCE_EXHAUSTION,
375
+ }
376
+ return mapping.get(exc_name, DefectType.UNKNOWN)
377
+
378
+ @staticmethod
379
+ def _severity_from_exception(exc: BaseException) -> BugSeverity:
380
+ """Heuristically determine severity from exception type.
381
+
382
+ KeyboardInterrupt and SystemExit are excluded from bug collection
383
+ entirely (handled in the excepthook), so this method only maps
384
+ actual defect exceptions.
385
+ """
386
+ critical_types = (MemoryError,)
387
+ if isinstance(exc, critical_types):
388
+ return BugSeverity.P0
389
+ error_types = (PermissionError, ImportError, ModuleNotFoundError)
390
+ if isinstance(exc, error_types):
391
+ return BugSeverity.P1
392
+ return BugSeverity.P2
@@ -0,0 +1,218 @@
1
+ """Defect Classifier — ProcCtrlBench 11-type defect ontology.
2
+
3
+ Auto-classifies exceptions into structured defect types for monitoring
4
+ and self-healing feedback loops.
5
+
6
+ Uses MRO-based exception matching (more precise than string-based matching
7
+ in BugCollector) and context-aware severity escalation.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ __all__ = [
13
+ "DefectClassifier",
14
+ "DefectType",
15
+ ]
16
+
17
+ import asyncio
18
+ from enum import StrEnum
19
+
20
+ from skillpool.monitor.bug_collector import BugSeverity
21
+
22
+
23
+ class DefectType(StrEnum):
24
+ """11 defect types from ProcCtrlBench ontology.
25
+
26
+ Uses lowercase snake_case values (distinct from BugCollector's
27
+ UPPER_CASE DefectType for string-based matching). This enum is
28
+ for class-based MRO classification via DefectClassifier.
29
+ """
30
+
31
+ PARAM_ERROR = "param_error"
32
+ PERMISSION_BREACH = "permission_breach"
33
+ TIMEOUT = "timeout"
34
+ DEPENDENCY_MISSING = "dependency_missing"
35
+ EXECUTION_FAILURE = "execution_failure"
36
+ OUTPUT_INVALID = "output_invalid"
37
+ STATE_CORRUPTION = "state_corruption"
38
+ RESOURCE_EXHAUSTION = "resource_exhaustion"
39
+ GATE_DENIED = "gate_denied"
40
+ PROTOCOL_ERROR = "protocol_error"
41
+ UNKNOWN = "unknown"
42
+
43
+
44
+ # Lazy import for domain-specific exceptions that live in other modules.
45
+ # Avoids circular imports at module load time.
46
+ def _get_domain_exceptions() -> dict[type[Exception], DefectType]:
47
+ """Resolve domain-specific exception mappings lazily."""
48
+ mapping: dict[type[Exception], DefectType] = {}
49
+ try:
50
+ from skillpool.registry import (
51
+ SkillNotFoundError,
52
+ SupplyChainEvidenceMissingError,
53
+ IllegalStateTransitionError,
54
+ )
55
+
56
+ mapping[SkillNotFoundError] = DefectType.DEPENDENCY_MISSING
57
+ mapping[SupplyChainEvidenceMissingError] = DefectType.PERMISSION_BREACH
58
+ mapping[IllegalStateTransitionError] = DefectType.STATE_CORRUPTION
59
+ except ImportError:
60
+ pass
61
+
62
+ try:
63
+ from skillpool.audit import AuditUnavailableError
64
+
65
+ mapping[AuditUnavailableError] = DefectType.GATE_DENIED
66
+ except ImportError:
67
+ pass
68
+
69
+ return mapping
70
+
71
+
72
+ # Severity heuristics: defect type → default severity when no context
73
+ _DEFAULT_SEVERITY: dict[DefectType, BugSeverity] = {
74
+ DefectType.RESOURCE_EXHAUSTION: BugSeverity.P0,
75
+ DefectType.STATE_CORRUPTION: BugSeverity.P0,
76
+ DefectType.PERMISSION_BREACH: BugSeverity.P1,
77
+ DefectType.GATE_DENIED: BugSeverity.P1,
78
+ DefectType.TIMEOUT: BugSeverity.P2,
79
+ DefectType.EXECUTION_FAILURE: BugSeverity.P2,
80
+ DefectType.DEPENDENCY_MISSING: BugSeverity.P2,
81
+ DefectType.OUTPUT_INVALID: BugSeverity.P2,
82
+ DefectType.PARAM_ERROR: BugSeverity.P2,
83
+ DefectType.PROTOCOL_ERROR: BugSeverity.P2,
84
+ DefectType.UNKNOWN: BugSeverity.P2,
85
+ }
86
+
87
+ # One-line fix suggestions per defect type
88
+ _FIX_SUGGESTIONS: dict[DefectType, str] = {
89
+ DefectType.PARAM_ERROR: "Validate input parameters against the skill contract before execution.",
90
+ DefectType.PERMISSION_BREACH: "Check agent trust level and supply chain evidence before access.",
91
+ DefectType.TIMEOUT: "Increase timeout threshold or add async cancellation with fallback.",
92
+ DefectType.DEPENDENCY_MISSING: "Verify all skill dependencies are registered and in ACTIVE state.",
93
+ DefectType.EXECUTION_FAILURE: "Add defensive error handling and retry with exponential backoff.",
94
+ DefectType.OUTPUT_INVALID: "Add output contract validation (schema check) after execution.",
95
+ DefectType.STATE_CORRUPTION: "Reset skill lifecycle state and re-validate transition legality.",
96
+ DefectType.RESOURCE_EXHAUSTION: "Enforce resource budgets via Token Governor and rate limiting.",
97
+ DefectType.GATE_DENIED: "Review gate complexity score and escalate or adjust agent capabilities.",
98
+ DefectType.PROTOCOL_ERROR: "Verify MCP protocol version and message schema compliance.",
99
+ DefectType.UNKNOWN: "Add structured error context to enable classification on recurrence.",
100
+ }
101
+
102
+ # Context-based severity escalation rules
103
+ _SEVERITY_ESCALATION_KEYS: dict[str, BugSeverity] = {
104
+ "production": BugSeverity.P0,
105
+ "critical_path": BugSeverity.P0,
106
+ "data_loss": BugSeverity.P0,
107
+ "security_breach": BugSeverity.P0,
108
+ "user_facing": BugSeverity.P1,
109
+ "retry_exhausted": BugSeverity.P1,
110
+ "cascade": BugSeverity.P1,
111
+ }
112
+
113
+
114
+ class DefectClassifier:
115
+ """Auto-classify exceptions into defect types.
116
+
117
+ Uses a two-tier mapping: built-in Python exceptions (static) and
118
+ domain-specific SkillPool exceptions (lazy-loaded to avoid circular imports).
119
+
120
+ Unlike BugCollector's string-based _classify_exception, this classifier
121
+ walks the MRO for precise matching and supports context-aware severity
122
+ escalation.
123
+
124
+ Usage:
125
+ classifier = DefectClassifier()
126
+ defect = classifier.classify(ValueError("bad param"))
127
+ defect_type, severity = classifier.classify_with_context(
128
+ TimeoutError("slow"), {"production": True}
129
+ )
130
+ hint = classifier.suggest_fix(defect)
131
+ """
132
+
133
+ # Built-in exception → DefectType mapping (always available)
134
+ EXCEPTION_MAP: dict[type[Exception], DefectType] = {
135
+ TypeError: DefectType.PARAM_ERROR,
136
+ ValueError: DefectType.PARAM_ERROR,
137
+ KeyError: DefectType.PARAM_ERROR,
138
+ PermissionError: DefectType.PERMISSION_BREACH,
139
+ TimeoutError: DefectType.TIMEOUT,
140
+ asyncio.TimeoutError: DefectType.TIMEOUT,
141
+ ImportError: DefectType.DEPENDENCY_MISSING,
142
+ ModuleNotFoundError: DefectType.DEPENDENCY_MISSING,
143
+ FileNotFoundError: DefectType.DEPENDENCY_MISSING,
144
+ RuntimeError: DefectType.EXECUTION_FAILURE,
145
+ AssertionError: DefectType.OUTPUT_INVALID,
146
+ MemoryError: DefectType.RESOURCE_EXHAUSTION,
147
+ ConnectionError: DefectType.PROTOCOL_ERROR,
148
+ OSError: DefectType.EXECUTION_FAILURE,
149
+ }
150
+
151
+ def __init__(self) -> None:
152
+ self._domain_map: dict[type[Exception], DefectType] | None = None
153
+
154
+ def _full_map(self) -> dict[type[Exception], DefectType]:
155
+ """Return merged mapping (built-in + domain-specific)."""
156
+ if self._domain_map is None:
157
+ self._domain_map = {**self.EXCEPTION_MAP, **_get_domain_exceptions()}
158
+ return self._domain_map
159
+
160
+ def classify(self, exception: Exception) -> DefectType:
161
+ """Classify an exception into a DefectType.
162
+
163
+ Walks the MRO of the exception class to find the most specific
164
+ matching defect type.
165
+
166
+ Args:
167
+ exception: The exception to classify.
168
+
169
+ Returns:
170
+ DefectType enum value.
171
+ """
172
+ mapping = self._full_map()
173
+ for cls in type(exception).__mro__:
174
+ if cls in mapping:
175
+ return mapping[cls]
176
+ return DefectType.UNKNOWN
177
+
178
+ def classify_with_context(
179
+ self,
180
+ exception: Exception,
181
+ context: dict,
182
+ ) -> tuple[DefectType, BugSeverity]:
183
+ """Classify an exception and determine severity with context.
184
+
185
+ Context keys that escalate severity:
186
+ - production / critical_path / data_loss / security_breach → P0
187
+ - user_facing / retry_exhausted / cascade → P1
188
+
189
+ Args:
190
+ exception: The exception to classify.
191
+ context: Additional context for severity determination.
192
+
193
+ Returns:
194
+ Tuple of (DefectType, BugSeverity).
195
+ """
196
+ defect_type = self.classify(exception)
197
+ severity = _DEFAULT_SEVERITY[defect_type]
198
+
199
+ # Escalate based on context
200
+ for key, escalated in _SEVERITY_ESCALATION_KEYS.items():
201
+ if context.get(key):
202
+ # Only escalate, never de-escalate
203
+ if escalated.value < severity.value:
204
+ severity = escalated
205
+ break
206
+
207
+ return defect_type, severity
208
+
209
+ def suggest_fix(self, defect_type: DefectType) -> str:
210
+ """Return a one-line fix suggestion for a defect type.
211
+
212
+ Args:
213
+ defect_type: The classified defect type.
214
+
215
+ Returns:
216
+ Human-readable fix suggestion string.
217
+ """
218
+ return _FIX_SUGGESTIONS.get(defect_type, _FIX_SUGGESTIONS[DefectType.UNKNOWN])