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,320 @@
1
+ """SkillResolver — skill chain resolution engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ import time
7
+ from typing import Optional
8
+
9
+ from skillpool.resolver.models import (
10
+ Conflict,
11
+ ConflictSeverity,
12
+ ConflictType as ConflictType,
13
+ DagEdge,
14
+ DagEdgeType,
15
+ Domain as Domain,
16
+ ResolveStatus,
17
+ ResolveStrategy as ResolveStrategy,
18
+ ResolvedSkill,
19
+ SkillResolveRequest,
20
+ SkillResolveResponse,
21
+ )
22
+ from skillpool.resolver.skill_graph import CycleDetected, SkillGraph
23
+ from skillpool.resolver.conflict_detector import ConflictDetector
24
+ from skillpool.resolver.health_filter import HealthFilter
25
+ from skillpool.resolver.cache import LRUCache
26
+ from skillpool.resolver.circuit_breaker import CircuitBreaker, CircuitState
27
+ from skillpool.resolver.rate_limiter import RateLimiter
28
+
29
+
30
+ # Simulated skill registry for resolver (production: backed by Registry)
31
+ _SKILL_REGISTRY: dict[str, dict] = {}
32
+ _REGISTRY_LOCK = threading.Lock()
33
+
34
+
35
+ def register_skill(skill_id: str, data: dict) -> None:
36
+ """Register a skill in the resolver's local registry."""
37
+ with _REGISTRY_LOCK:
38
+ _SKILL_REGISTRY[skill_id] = data
39
+
40
+
41
+ def clear_registry() -> None:
42
+ """Clear the resolver's local registry."""
43
+ with _REGISTRY_LOCK:
44
+ _SKILL_REGISTRY.clear()
45
+
46
+
47
+ class SkillResolver:
48
+ """Resolve skill chains from CSDF definitions.
49
+
50
+ Pipeline:
51
+ 1. Fetch skills from registry
52
+ 2. Build dependency DAG
53
+ 3. Cycle detection
54
+ 4. Conflict detection (Jaccard)
55
+ 5. Health filter
56
+ 6. Topological sort
57
+ 7. Apply constraints (max_skills, exclude, require_independent)
58
+ 8. Return resolved chain
59
+
60
+ When a Registry is provided, skill metadata and lifecycle state are
61
+ sourced from the Registry instead of the in-memory dict. The Registry
62
+ provides proper 9-state lifecycle governance, supply chain evidence
63
+ verification, and persistent storage.
64
+
65
+ Usage:
66
+ resolver = SkillResolver()
67
+ response = resolver.resolve(SkillResolveRequest(skill_ids=["S01", "S05a"]))
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ skill_registry: Optional[dict[str, dict]] = None,
73
+ cache_max_size: int = 128,
74
+ cache_ttl: float = 3600.0,
75
+ circuit_failure_threshold: int = 5,
76
+ circuit_recovery_timeout: float = 30.0,
77
+ rate_max_requests: int = 100,
78
+ rate_window_seconds: float = 1.0,
79
+ conflict_threshold: float = 0.5,
80
+ min_health_score: float = 0.6,
81
+ registry=None,
82
+ ) -> None:
83
+ self._registry_store = skill_registry if skill_registry is not None else _SKILL_REGISTRY
84
+ self._cache = LRUCache(max_size=cache_max_size, ttl_seconds=cache_ttl)
85
+ self._circuit = CircuitBreaker(
86
+ failure_threshold=circuit_failure_threshold,
87
+ recovery_timeout=circuit_recovery_timeout,
88
+ )
89
+ self._limiter = RateLimiter(
90
+ max_requests=rate_max_requests,
91
+ window_seconds=rate_window_seconds,
92
+ )
93
+ self._conflict_threshold = conflict_threshold
94
+ self._min_health_score = min_health_score
95
+ self._registry = registry
96
+
97
+ @property
98
+ def circuit_state(self) -> CircuitState:
99
+ return self._circuit.state
100
+
101
+ def resolve(self, request: SkillResolveRequest) -> SkillResolveResponse:
102
+ """Resolve a skill chain from the request."""
103
+ start = time.monotonic()
104
+
105
+ # Rate limit check
106
+ if not self._limiter.allow():
107
+ return SkillResolveResponse(
108
+ error="rate_limit_exceeded",
109
+ degraded=True,
110
+ resolution_time_ms=(time.monotonic() - start) * 1000,
111
+ )
112
+
113
+ # Circuit breaker check
114
+ if not self._circuit.allow_request():
115
+ return SkillResolveResponse(
116
+ degraded=True,
117
+ error="circuit_open",
118
+ resolution_time_ms=(time.monotonic() - start) * 1000,
119
+ )
120
+
121
+ # Cache check
122
+ cache_key = LRUCache.make_key(
123
+ request.skill_ids,
124
+ strategy=request.strategy.value,
125
+ max_skills=request.max_skills,
126
+ exclude=request.exclude_skills,
127
+ )
128
+ cached = self._cache.get(cache_key)
129
+ if cached is not None:
130
+ cached.from_cache = True
131
+ cached.resolution_time_ms = (time.monotonic() - start) * 1000
132
+ return cached
133
+
134
+ # Resolve
135
+ try:
136
+ response = self._do_resolve(request)
137
+ self._circuit.record_success()
138
+ except CycleDetected as e:
139
+ self._circuit.record_failure()
140
+ return SkillResolveResponse(
141
+ error=str(e),
142
+ resolution_time_ms=(time.monotonic() - start) * 1000,
143
+ )
144
+ except Exception as e:
145
+ self._circuit.record_failure()
146
+ return SkillResolveResponse(
147
+ error=str(e),
148
+ degraded=True,
149
+ resolution_time_ms=(time.monotonic() - start) * 1000,
150
+ )
151
+
152
+ # Cache result
153
+ self._cache.put(cache_key, response)
154
+ response.resolution_time_ms = (time.monotonic() - start) * 1000
155
+ return response
156
+
157
+ def _do_resolve(self, request: SkillResolveRequest) -> SkillResolveResponse:
158
+ """Core resolution logic."""
159
+ # 1. Fetch skills
160
+ fetched = self._fetch_skills(request.skill_ids)
161
+ if not fetched:
162
+ return SkillResolveResponse(
163
+ error="no_skills_found",
164
+ status=ResolveStatus.UNRESOLVED,
165
+ )
166
+
167
+ # 2. Build DAG
168
+ graph = self._build_dag(fetched)
169
+
170
+ # 3. Cycle detection
171
+ graph.topological_sort() # raises CycleDetected
172
+
173
+ # 4. Conflict detection
174
+ conflict_detector = ConflictDetector(threshold=self._conflict_threshold)
175
+ for sid, sdata in fetched.items():
176
+ conflict_detector.register(
177
+ skill_id=sid,
178
+ name=sdata.get("name", ""),
179
+ dimension=sdata.get("dimension", ""),
180
+ namespaces=sdata.get("namespaces", []),
181
+ )
182
+ raw_conflicts = conflict_detector.detect()
183
+
184
+ # 5. Health filter
185
+ skills_list = [{"skill_id": sid, **sdata} for sid, sdata in fetched.items()]
186
+ hf = HealthFilter(min_score=request.min_health_score)
187
+ passed, excluded = hf.filter(skills_list)
188
+
189
+ # 6. Apply exclude filter
190
+ if request.exclude_skills:
191
+ passed = [s for s in passed if s["skill_id"] not in request.exclude_skills]
192
+ excluded.extend(request.exclude_skills)
193
+
194
+ # 7. Topological sort
195
+ passed_ids = {s["skill_id"] for s in passed}
196
+ subgraph = graph.subgraph(passed_ids)
197
+ sorted_ids = subgraph.topological_sort()
198
+
199
+ # 8. Apply max_skills constraint
200
+ sorted_ids = sorted_ids[: request.max_skills]
201
+
202
+ # 9. Build response
203
+ resolved = []
204
+ for sid in sorted_ids:
205
+ sdata = fetched.get(sid, {})
206
+ # Find conflict severity for this skill
207
+ conflict_sev = None
208
+ for c in raw_conflicts:
209
+ if c["skill_a"] == sid or c["skill_b"] == sid:
210
+ conflict_sev = ConflictSeverity(c["severity"])
211
+ break
212
+
213
+ resolved.append(
214
+ ResolvedSkill(
215
+ skill_id=sid,
216
+ name=sdata.get("name", ""),
217
+ version=sdata.get("version", "1.0.0"),
218
+ dimension=sdata.get("dimension", ""),
219
+ domain=sdata.get("domain", ""),
220
+ weight=sdata.get("weight", 0.0),
221
+ health_score=sdata.get("health_score", 1.0),
222
+ trust_level=sdata.get("trust_level", 3),
223
+ dependencies=sdata.get("dependencies", []),
224
+ estimated_tokens=sdata.get("estimated_tokens", 0),
225
+ provides=sdata.get("provides", []),
226
+ conflict=conflict_sev,
227
+ )
228
+ )
229
+
230
+ conflicts = [Conflict(**c) for c in raw_conflicts]
231
+
232
+ dag_edges = [
233
+ DagEdge(source=src, target=tgt, weight=w, type=DagEdgeType.DEPENDS_ON)
234
+ for src, tgt, w in graph.get_edges()
235
+ if src in passed_ids and tgt in passed_ids
236
+ ]
237
+
238
+ # Build health_scores mapping
239
+ health_scores = {s.skill_id: s.health_score for s in resolved}
240
+
241
+ # Calculate feasibility_score = f(health_scores, conflicts)
242
+ avg_health = sum(health_scores.values()) / len(health_scores) if health_scores else 0.0
243
+ conflict_penalty = len(conflicts) * 0.1 # Each conflict reduces feasibility by 0.1
244
+ feasibility_score = max(0.0, min(1.0, avg_health - conflict_penalty))
245
+
246
+ # Determine status
247
+ if len(resolved) == 0:
248
+ status = ResolveStatus.UNRESOLVED
249
+ elif len(resolved) < len(request.skill_ids):
250
+ status = ResolveStatus.PARTIAL
251
+ else:
252
+ status = ResolveStatus.RESOLVED
253
+
254
+ return SkillResolveResponse(
255
+ resolved=resolved,
256
+ conflicts=conflicts,
257
+ excluded=excluded,
258
+ dag_edges=dag_edges,
259
+ total_skills=len(resolved),
260
+ status=status,
261
+ health_scores=health_scores,
262
+ feasibility_score=feasibility_score,
263
+ )
264
+
265
+ def _fetch_skills(self, skill_ids: list[str]) -> dict[str, dict]:
266
+ """Fetch skills from registry, including transitive dependencies.
267
+
268
+ When a Registry is configured, reads skill metadata from the Registry
269
+ (the authoritative source) and skips skills that are not enabled.
270
+ Otherwise falls back to the in-memory dict store.
271
+ """
272
+ fetched: dict[str, dict] = {}
273
+ queue = list(skill_ids)
274
+
275
+ while queue:
276
+ sid = queue.pop(0)
277
+ if sid in fetched:
278
+ continue
279
+
280
+ # Try Registry first (authoritative source)
281
+ if self._registry is not None:
282
+ record = self._registry.get_skill(sid)
283
+ if record is None:
284
+ continue
285
+ # Skip non-enabled skills from Registry
286
+ if not self._registry.is_enabled(sid):
287
+ continue
288
+ meta = record.metadata
289
+ sdata = {
290
+ "name": meta.name,
291
+ "version": meta.version,
292
+ "dimension": "",
293
+ "namespaces": meta.tags,
294
+ "dependencies": meta.dependencies,
295
+ "weight": meta.quality_score,
296
+ "health_score": 1.0, # Enabled = healthy
297
+ "trust_level": 3,
298
+ }
299
+ else:
300
+ sdata = self._registry_store.get(sid)
301
+ if sdata is None:
302
+ continue
303
+
304
+ fetched[sid] = sdata
305
+ # Follow dependencies
306
+ for dep in sdata.get("dependencies", []):
307
+ if dep not in fetched:
308
+ queue.append(dep)
309
+
310
+ return fetched
311
+
312
+ def _build_dag(self, skills: dict[str, dict]) -> SkillGraph:
313
+ """Build a SkillGraph from skill definitions."""
314
+ graph = SkillGraph()
315
+ for sid, sdata in skills.items():
316
+ graph.add_node(sid)
317
+ for dep in sdata.get("dependencies", []):
318
+ if dep in skills:
319
+ graph.add_edge(dep, sid, weight=sdata.get("weight", 1.0))
320
+ return graph
@@ -0,0 +1,103 @@
1
+ """Resolver cache — LRU with TTL expiration and thread safety.
2
+
3
+ Each cache entry has a TTL (default 3600s). Entries past TTL are treated
4
+ as misses and lazily evicted on access.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import threading
12
+ import time
13
+ from collections import OrderedDict
14
+ from dataclasses import dataclass
15
+ from typing import Any, Optional
16
+
17
+
18
+ @dataclass
19
+ class CacheEntry:
20
+ """A cached value with expiration timestamp."""
21
+
22
+ value: Any
23
+ expires_at: float # time.monotonic() timestamp
24
+
25
+
26
+ class LRUCache:
27
+ """LRU cache with TTL expiration and thread-safe operations.
28
+
29
+ Args:
30
+ max_size: Maximum number of entries.
31
+ ttl_seconds: Time-to-live for each entry (default 3600s).
32
+ """
33
+
34
+ def __init__(self, max_size: int = 128, ttl_seconds: float = 3600.0) -> None:
35
+ self._max_size = max_size
36
+ self._ttl_seconds = ttl_seconds
37
+ self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
38
+ self._hits = 0
39
+ self._misses = 0
40
+ self._lock = threading.Lock()
41
+
42
+ @staticmethod
43
+ def make_key(skill_ids: list[str], **kwargs: object) -> str:
44
+ """Generate a deterministic cache key from skill_ids and kwargs."""
45
+ payload = json.dumps({"ids": sorted(skill_ids), **kwargs}, sort_keys=True, default=str)
46
+ return hashlib.sha256(payload.encode()).hexdigest()[:16]
47
+
48
+ def get(self, key: str) -> Optional[Any]:
49
+ """Get a cached value. Returns None on miss or TTL expiry."""
50
+ with self._lock:
51
+ entry = self._cache.get(key)
52
+ if entry is None:
53
+ self._misses += 1
54
+ return None
55
+ # Check TTL
56
+ if time.monotonic() > entry.expires_at:
57
+ del self._cache[key]
58
+ self._misses += 1
59
+ return None
60
+ # Move to end (most recently used)
61
+ self._cache.move_to_end(key)
62
+ self._hits += 1
63
+ return entry.value
64
+
65
+ def put(self, key: str, value: Any, ttl_seconds: Optional[float] = None) -> None:
66
+ """Store a value with optional per-entry TTL override."""
67
+ with self._lock:
68
+ ttl = ttl_seconds if ttl_seconds is not None else self._ttl_seconds
69
+ entry = CacheEntry(value=value, expires_at=time.monotonic() + ttl)
70
+ if key in self._cache:
71
+ self._cache.move_to_end(key)
72
+ self._cache[key] = entry
73
+ # Evict oldest if over capacity
74
+ while len(self._cache) > self._max_size:
75
+ self._cache.popitem(last=False)
76
+
77
+ def invalidate(self, key: str) -> bool:
78
+ """Remove a specific key. Returns True if key existed."""
79
+ with self._lock:
80
+ if key in self._cache:
81
+ del self._cache[key]
82
+ return True
83
+ return False
84
+
85
+ def clear(self) -> None:
86
+ """Clear all entries."""
87
+ with self._lock:
88
+ self._cache.clear()
89
+ self._hits = 0
90
+ self._misses = 0
91
+
92
+ def stats(self) -> dict[str, int]:
93
+ """Return cache hit/miss statistics."""
94
+ with self._lock:
95
+ return {"hits": self._hits, "misses": self._misses, "size": len(self._cache)}
96
+
97
+ def is_expired(self, key: str) -> bool:
98
+ """Check if a key exists but is expired (without evicting)."""
99
+ with self._lock:
100
+ entry = self._cache.get(key)
101
+ if entry is None:
102
+ return False
103
+ return time.monotonic() > entry.expires_at
@@ -0,0 +1,103 @@
1
+ """CircuitBreaker — 3-state circuit breaker for resolver resilience."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from enum import StrEnum
7
+ from typing import Optional
8
+
9
+
10
+ class CircuitState(StrEnum):
11
+ CLOSED = "closed"
12
+ OPEN = "open"
13
+ HALF_OPEN = "half_open"
14
+
15
+
16
+ class CircuitBreaker:
17
+ """3-state circuit breaker: CLOSED → OPEN → HALF_OPEN → CLOSED.
18
+
19
+ CLOSED: Normal operation, requests pass through.
20
+ OPEN: Failures exceeded threshold, requests are rejected.
21
+ HALF_OPEN: Probe mode, limited requests allowed to test recovery.
22
+
23
+ Usage:
24
+ cb = CircuitBreaker(failure_threshold=5, recovery_timeout=30.0)
25
+ if cb.allow_request():
26
+ try:
27
+ result = do_work()
28
+ cb.record_success()
29
+ except Exception:
30
+ cb.record_failure()
31
+ else:
32
+ result = fallback()
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ failure_threshold: int = 5,
38
+ recovery_timeout: float = 30.0,
39
+ half_open_max_calls: int = 3,
40
+ ) -> None:
41
+ self.failure_threshold = failure_threshold
42
+ self.recovery_timeout = recovery_timeout
43
+ self.half_open_max_calls = half_open_max_calls
44
+ self._state = CircuitState.CLOSED
45
+ self._failure_count = 0
46
+ self._success_count = 0
47
+ self._last_failure_time: Optional[float] = None
48
+ self._half_open_calls = 0
49
+
50
+ @property
51
+ def state(self) -> CircuitState:
52
+ """Current circuit state, with automatic transition from OPEN → HALF_OPEN."""
53
+ if self._state == CircuitState.OPEN and self._last_failure_time is not None:
54
+ if time.monotonic() - self._last_failure_time >= self.recovery_timeout:
55
+ self._state = CircuitState.HALF_OPEN
56
+ self._half_open_calls = 0
57
+ return self._state
58
+
59
+ def allow_request(self) -> bool:
60
+ """Check if a request should be allowed through."""
61
+ current = self.state
62
+ if current == CircuitState.CLOSED:
63
+ return True
64
+ if current == CircuitState.HALF_OPEN:
65
+ if self._half_open_calls < self.half_open_max_calls:
66
+ self._half_open_calls += 1
67
+ return True
68
+ return False
69
+ return False # OPEN
70
+
71
+ def record_success(self) -> None:
72
+ """Record a successful request."""
73
+ if self._state == CircuitState.HALF_OPEN:
74
+ self._success_count += 1
75
+ if self._success_count >= self.half_open_max_calls:
76
+ self._reset()
77
+ self._failure_count = 0
78
+
79
+ def record_failure(self) -> None:
80
+ """Record a failed request."""
81
+ self._failure_count += 1
82
+ self._last_failure_time = time.monotonic()
83
+ self._success_count = 0
84
+
85
+ if self._state == CircuitState.HALF_OPEN:
86
+ self._state = CircuitState.OPEN
87
+ elif self._failure_count >= self.failure_threshold:
88
+ self._state = CircuitState.OPEN
89
+
90
+ def _reset(self) -> None:
91
+ """Reset to CLOSED state."""
92
+ self._state = CircuitState.CLOSED
93
+ self._failure_count = 0
94
+ self._success_count = 0
95
+ self._half_open_calls = 0
96
+
97
+ @property
98
+ def failure_count(self) -> int:
99
+ return self._failure_count
100
+
101
+ @property
102
+ def success_count(self) -> int:
103
+ return self._success_count
@@ -0,0 +1,111 @@
1
+ """ConflictDetector — Jaccard similarity conflict detection between skills."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Optional
7
+
8
+
9
+ def _tokenize(text: str) -> set[str]:
10
+ """Tokenize text into lowercase word set."""
11
+ return set(re.findall(r"[a-z0-9_]+", text.lower()))
12
+
13
+
14
+ def jaccard_similarity(set_a: set[str], set_b: set[str]) -> float:
15
+ """Compute Jaccard similarity between two sets."""
16
+ if not set_a and not set_b:
17
+ return 1.0
18
+ if not set_a or not set_b:
19
+ return 0.0
20
+ intersection = set_a & set_b
21
+ union = set_a | set_b
22
+ return len(intersection) / len(union)
23
+
24
+
25
+ class ConflictDetector:
26
+ """Detect naming conflicts between skills using Jaccard similarity.
27
+
28
+ Usage:
29
+ cd = ConflictDetector()
30
+ cd.register("S01", "Requirement Coverage", "D1", namespaces=["req", "coverage"])
31
+ conflicts = cd.detect(threshold=0.5)
32
+ """
33
+
34
+ def __init__(self, threshold: float = 0.5) -> None:
35
+ self.threshold = threshold
36
+ self._skills: dict[str, dict] = {} # skill_id → {name, tokens, namespaces}
37
+
38
+ def register(
39
+ self,
40
+ skill_id: str,
41
+ name: str = "",
42
+ dimension: str = "",
43
+ namespaces: Optional[list[str]] = None,
44
+ ) -> None:
45
+ """Register a skill for conflict detection."""
46
+ tokens = _tokenize(f"{name} {dimension} {' '.join(namespaces or [])}")
47
+ self._skills[skill_id] = {
48
+ "name": name,
49
+ "tokens": tokens,
50
+ "namespaces": set(namespaces or []),
51
+ }
52
+
53
+ def detect(self, threshold: Optional[float] = None) -> list[dict]:
54
+ """Detect all pairwise conflicts above threshold.
55
+
56
+ Returns list of dicts: {skill_a, skill_b, jaccard_score, severity, overlapping_namespaces}
57
+ """
58
+ thresh = threshold if threshold is not None else self.threshold
59
+ conflicts = []
60
+ skill_ids = list(self._skills.keys())
61
+
62
+ for i in range(len(skill_ids)):
63
+ for j in range(i + 1, len(skill_ids)):
64
+ a_id, b_id = skill_ids[i], skill_ids[j]
65
+ a_info, b_info = self._skills[a_id], self._skills[b_id]
66
+
67
+ score = jaccard_similarity(a_info["tokens"], b_info["tokens"])
68
+ if score >= thresh:
69
+ overlapping = a_info["namespaces"] & b_info["namespaces"]
70
+ severity = self._classify_severity(score, overlapping)
71
+ conflicts.append(
72
+ {
73
+ "skill_a": a_id,
74
+ "skill_b": b_id,
75
+ "jaccard_score": round(score, 4),
76
+ "severity": severity,
77
+ "conflict_type": self._classify_conflict_type(overlapping, score),
78
+ "overlapping_namespaces": sorted(overlapping),
79
+ "recommendation": self._generate_recommendation(severity, overlapping),
80
+ }
81
+ )
82
+
83
+ return conflicts
84
+
85
+ def _classify_severity(self, score: float, overlapping: set[str]) -> str:
86
+ """Classify conflict severity based on Jaccard score and namespace overlap."""
87
+ if overlapping and score >= 0.7:
88
+ return "high"
89
+ if overlapping or score >= 0.7:
90
+ return "medium"
91
+ return "low"
92
+
93
+ def _classify_conflict_type(self, overlapping: set[str], score: float) -> str:
94
+ """Classify conflict type per schema enum."""
95
+ if overlapping:
96
+ return "namespace_overlap"
97
+ if score >= 0.8:
98
+ return "semantic_conflict"
99
+ return "namespace_overlap" # Default for Jaccard-based detection
100
+
101
+ def _generate_recommendation(self, severity: str, overlapping: set[str]) -> str:
102
+ """Generate a recommendation for resolving the conflict."""
103
+ if severity == "high":
104
+ return "Consider merging or removing one of the conflicting skills"
105
+ if severity == "medium":
106
+ return "Review skill boundaries and adjust namespaces"
107
+ return "Monitor for potential future conflicts"
108
+
109
+ def clear(self) -> None:
110
+ """Remove all registered skills."""
111
+ self._skills.clear()
@@ -0,0 +1,38 @@
1
+ """HealthFilter — filter skills by health score threshold."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class HealthFilter:
7
+ """Filter skills based on health_score.
8
+
9
+ Skills with health_score < min_score are excluded.
10
+
11
+ Usage:
12
+ hf = HealthFilter(min_score=0.6)
13
+ passed, excluded = hf.filter(skills)
14
+ """
15
+
16
+ def __init__(self, min_score: float = 0.6) -> None:
17
+ self.min_score = min_score
18
+
19
+ def filter(self, skills: list[dict]) -> tuple[list[dict], list[str]]:
20
+ """Filter skills by health_score.
21
+
22
+ Args:
23
+ skills: List of dicts with at least 'skill_id' and 'health_score' keys.
24
+
25
+ Returns:
26
+ Tuple of (passed_skills, excluded_skill_ids)
27
+ """
28
+ passed = []
29
+ excluded = []
30
+
31
+ for skill in skills:
32
+ health = skill.get("health_score", 1.0)
33
+ if health >= self.min_score:
34
+ passed.append(skill)
35
+ else:
36
+ excluded.append(skill.get("skill_id", "unknown"))
37
+
38
+ return passed, excluded