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.
- skillpool/__init__.py +74 -0
- skillpool/__main__.py +6 -0
- skillpool/adapters/__init__.py +8 -0
- skillpool/adapters/base.py +41 -0
- skillpool/adapters/claude_adapter.py +36 -0
- skillpool/adapters/codex_adapter.py +92 -0
- skillpool/adapters/hermes_adapter.py +38 -0
- skillpool/audit/__init__.py +651 -0
- skillpool/bridge/__init__.py +16 -0
- skillpool/bridge/freeze_detector.py +134 -0
- skillpool/bridge/maintenance.py +119 -0
- skillpool/bridge/wal_manager.py +136 -0
- skillpool/clawmem_client.py +176 -0
- skillpool/cli.py +700 -0
- skillpool/combiner/__init__.py +31 -0
- skillpool/combiner/lifecycle.py +453 -0
- skillpool/combiner/models.py +99 -0
- skillpool/config.py +34 -0
- skillpool/cost/__init__.py +111 -0
- skillpool/cost/audit_hash.py +51 -0
- skillpool/cost/budget_tracker.py +66 -0
- skillpool/cost/dashboard.py +189 -0
- skillpool/cost/models.py +129 -0
- skillpool/cost/token_governor.py +264 -0
- skillpool/cost/trace_ceiling.py +38 -0
- skillpool/csdf.py +126 -0
- skillpool/evolver/__init__.py +978 -0
- skillpool/gain/__init__.py +285 -0
- skillpool/gate.py +282 -0
- skillpool/gate_policy/__init__.py +31 -0
- skillpool/gate_policy/incremental.py +157 -0
- skillpool/gate_policy/parser.py +258 -0
- skillpool/gate_policy/state_machine.py +432 -0
- skillpool/graph/__init__.py +14 -0
- skillpool/graph/ppr.py +279 -0
- skillpool/health/__init__.py +73 -0
- skillpool/health/check.py +85 -0
- skillpool/health/degradation.py +90 -0
- skillpool/health/models.py +43 -0
- skillpool/hooks/__init__.py +4 -0
- skillpool/hooks/security_scanner.py +288 -0
- skillpool/lifecycle.py +150 -0
- skillpool/materializer/__init__.py +124 -0
- skillpool/materializer/budget_cropper.py +178 -0
- skillpool/materializer/csdf_loader.py +114 -0
- skillpool/materializer/lazy_loader.py +265 -0
- skillpool/materializer/lifecycle_filter.py +93 -0
- skillpool/materializer/mapper.py +178 -0
- skillpool/materializer/models.py +66 -0
- skillpool/mcp_server.py +2005 -0
- skillpool/monitor/__init__.py +576 -0
- skillpool/monitor/bug_collector.py +392 -0
- skillpool/monitor/defect_classifier.py +218 -0
- skillpool/monitor/self_healing.py +530 -0
- skillpool/monitor/telemetry_bridge.py +197 -0
- skillpool/paradigm/__init__.py +312 -0
- skillpool/paradigm/override.py +285 -0
- skillpool/profile.py +94 -0
- skillpool/quality.py +254 -0
- skillpool/registry/__init__.py +509 -0
- skillpool/registry/models.py +98 -0
- skillpool/resolver/__init__.py +320 -0
- skillpool/resolver/cache.py +103 -0
- skillpool/resolver/circuit_breaker.py +103 -0
- skillpool/resolver/conflict_detector.py +111 -0
- skillpool/resolver/health_filter.py +38 -0
- skillpool/resolver/models.py +154 -0
- skillpool/resolver/rate_limiter.py +48 -0
- skillpool/resolver/skill_graph.py +183 -0
- skillpool/review/__init__.py +242 -0
- skillpool/review/async_queue.py +96 -0
- skillpool/review/checkpoint_runner.py +345 -0
- skillpool/review/models.py +164 -0
- skillpool/review/suspect_marker.py +39 -0
- skillpool/review/veto_evaluator.py +94 -0
- skillpool/router/__init__.py +481 -0
- skillpool/schemas.py +119 -0
- skillpool/synergy/__init__.py +240 -0
- skillpool/synergy/detector.py +5 -0
- skillpool/telemetry.py +126 -0
- skillpool/utils/__init__.py +21 -0
- skillpool/utils/changelog.py +218 -0
- skillpool/utils/logger.py +273 -0
- skillpool/utils/runtime_audit.py +163 -0
- skillpool/utils/time_utils.py +13 -0
- skillpool-4.3.0.dist-info/METADATA +21 -0
- skillpool-4.3.0.dist-info/RECORD +90 -0
- skillpool-4.3.0.dist-info/WHEEL +5 -0
- skillpool-4.3.0.dist-info/entry_points.txt +3 -0
- skillpool-4.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EmergencyOverride — 紧急降权协议。
|
|
3
|
+
|
|
4
|
+
当检测到安全事件、资源耗尽或不可恢复错误时,
|
|
5
|
+
EmergencyOverride 可以紧急降权 Agent 的 trust_level,
|
|
6
|
+
或完全禁止特定 Skill 的执行。
|
|
7
|
+
|
|
8
|
+
降权协议:
|
|
9
|
+
1. 检测触发条件(安全事件/资源耗尽/不可恢复错误)
|
|
10
|
+
2. 评估降权级别(trust_level 降低幅度)
|
|
11
|
+
3. 执行降权(修改 gate_file + 通知 TelemetryBridge)
|
|
12
|
+
4. 记录降权事件(审计日志)
|
|
13
|
+
5. 恢复机制(手动或超时后自动恢复)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from enum import StrEnum
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
from skillpool.config import get_data_dir
|
|
27
|
+
from skillpool.telemetry import TelemetryBridge
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class OverrideTrigger(StrEnum):
|
|
33
|
+
SECURITY_EVENT = "security_event"
|
|
34
|
+
RESOURCE_EXHAUSTION = "resource_exhaustion"
|
|
35
|
+
UNRECOVERABLE_ERROR = "unrecoverable_error"
|
|
36
|
+
MANUAL = "manual"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class OverrideLevel(StrEnum):
|
|
40
|
+
WARN = "warn" # 降低 trust_level 1 级
|
|
41
|
+
DEGRADE = "degrade" # 降低 trust_level 2 级
|
|
42
|
+
QUARANTINE = "quarantine" # trust_level → 0,禁止执行
|
|
43
|
+
KILL = "kill" # 完全禁止,需要人工恢复
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class OverrideEvent:
|
|
48
|
+
"""降权事件记录。"""
|
|
49
|
+
|
|
50
|
+
trigger: OverrideTrigger
|
|
51
|
+
level: OverrideLevel
|
|
52
|
+
target_skill: str = ""
|
|
53
|
+
target_agent: str = ""
|
|
54
|
+
original_trust: int = 3
|
|
55
|
+
new_trust: int = 0
|
|
56
|
+
reason: str = ""
|
|
57
|
+
timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
58
|
+
expires_at: Optional[str] = None # None = 需人工恢复
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class GateFile:
|
|
63
|
+
"""Gate file — 单个 Skill 的门禁状态文件。
|
|
64
|
+
|
|
65
|
+
格式:
|
|
66
|
+
{
|
|
67
|
+
"skill_id": "S05a",
|
|
68
|
+
"trust_level": 3,
|
|
69
|
+
"blocked": false,
|
|
70
|
+
"override_history": [...]
|
|
71
|
+
}
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
skill_id: str
|
|
75
|
+
trust_level: int = 3
|
|
76
|
+
blocked: bool = False
|
|
77
|
+
override_history: list[dict] = field(default_factory=list)
|
|
78
|
+
|
|
79
|
+
def to_dict(self) -> dict:
|
|
80
|
+
return {
|
|
81
|
+
"skill_id": self.skill_id,
|
|
82
|
+
"trust_level": self.trust_level,
|
|
83
|
+
"blocked": self.blocked,
|
|
84
|
+
"override_history": self.override_history,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_dict(cls, data: dict) -> GateFile:
|
|
89
|
+
return cls(
|
|
90
|
+
skill_id=data.get("skill_id", ""),
|
|
91
|
+
trust_level=data.get("trust_level", 3),
|
|
92
|
+
blocked=data.get("blocked", False),
|
|
93
|
+
override_history=data.get("override_history", []),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class EmergencyOverride:
|
|
98
|
+
"""紧急降权管理器。
|
|
99
|
+
|
|
100
|
+
Usage:
|
|
101
|
+
eo = EmergencyOverride(telemetry=bridge, gate_dir=Path("~/.skillpool/gates"))
|
|
102
|
+
event = eo.override(
|
|
103
|
+
trigger="security_event",
|
|
104
|
+
level="quarantine",
|
|
105
|
+
target_skill="S05a",
|
|
106
|
+
reason="SQL injection detected",
|
|
107
|
+
)
|
|
108
|
+
# 检查是否被降权
|
|
109
|
+
is_blocked = eo.is_blocked("S05a")
|
|
110
|
+
# 恢复
|
|
111
|
+
eo.restore("S05a")
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def __init__(
|
|
115
|
+
self,
|
|
116
|
+
telemetry: Optional[TelemetryBridge] = None,
|
|
117
|
+
gate_dir: Optional[Path] = None,
|
|
118
|
+
):
|
|
119
|
+
self.telemetry = telemetry
|
|
120
|
+
self.gate_dir = gate_dir or get_data_dir() / "gates"
|
|
121
|
+
self.gate_dir.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
self._overrides: dict[str, OverrideEvent] = {} # skill_id → latest event
|
|
123
|
+
|
|
124
|
+
def override(
|
|
125
|
+
self,
|
|
126
|
+
trigger: OverrideTrigger | str,
|
|
127
|
+
level: OverrideLevel | str,
|
|
128
|
+
target_skill: str,
|
|
129
|
+
target_agent: str = "",
|
|
130
|
+
reason: str = "",
|
|
131
|
+
current_trust: int = 3,
|
|
132
|
+
ttl_seconds: Optional[int] = None,
|
|
133
|
+
) -> OverrideEvent:
|
|
134
|
+
"""执行紧急降权。
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
trigger: 触发原因
|
|
138
|
+
level: 降权级别
|
|
139
|
+
target_skill: 目标 Skill ID
|
|
140
|
+
target_agent: 目标 Agent 名称
|
|
141
|
+
reason: 降权原因描述
|
|
142
|
+
current_trust: 当前 trust_level
|
|
143
|
+
ttl_seconds: 自动恢复时间(None = 需人工恢复)
|
|
144
|
+
"""
|
|
145
|
+
trig = OverrideTrigger(trigger) if isinstance(trigger, str) else trigger
|
|
146
|
+
lvl = OverrideLevel(level) if isinstance(level, str) else level
|
|
147
|
+
|
|
148
|
+
# 计算新的 trust_level
|
|
149
|
+
new_trust = self._compute_new_trust(current_trust, lvl)
|
|
150
|
+
|
|
151
|
+
# 计算过期时间
|
|
152
|
+
expires_at = None
|
|
153
|
+
if ttl_seconds:
|
|
154
|
+
from datetime import timedelta
|
|
155
|
+
|
|
156
|
+
expires_at = (datetime.now(timezone.utc) + timedelta(seconds=ttl_seconds)).isoformat()
|
|
157
|
+
|
|
158
|
+
event = OverrideEvent(
|
|
159
|
+
trigger=trig,
|
|
160
|
+
level=lvl,
|
|
161
|
+
target_skill=target_skill,
|
|
162
|
+
target_agent=target_agent,
|
|
163
|
+
original_trust=current_trust,
|
|
164
|
+
new_trust=new_trust,
|
|
165
|
+
reason=reason,
|
|
166
|
+
expires_at=expires_at,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# 更新 gate file — 只有 KILL 设 blocked=True, QUARANTINE 只降 trust
|
|
170
|
+
blocked = lvl == OverrideLevel.KILL
|
|
171
|
+
self._update_gate_file(target_skill, new_trust, blocked, event)
|
|
172
|
+
|
|
173
|
+
# 记录
|
|
174
|
+
self._overrides[target_skill] = event
|
|
175
|
+
|
|
176
|
+
# 发射遥测
|
|
177
|
+
if self.telemetry:
|
|
178
|
+
self.telemetry.emit(
|
|
179
|
+
event_type="emergency_override",
|
|
180
|
+
skill_id=target_skill,
|
|
181
|
+
channel="hook",
|
|
182
|
+
payload={
|
|
183
|
+
"trigger": str(trig),
|
|
184
|
+
"level": str(lvl),
|
|
185
|
+
"new_trust": new_trust,
|
|
186
|
+
"reason": reason,
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return event
|
|
191
|
+
|
|
192
|
+
def is_blocked(self, skill_id: str) -> bool:
|
|
193
|
+
"""检查 Skill 是否被降权/禁止。"""
|
|
194
|
+
gate = self._read_gate_file(skill_id)
|
|
195
|
+
if gate:
|
|
196
|
+
return gate.blocked or gate.trust_level == 0
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
def get_trust_level(self, skill_id: str) -> int:
|
|
200
|
+
"""获取 Skill 当前 trust_level(可能已被降权)。"""
|
|
201
|
+
gate = self._read_gate_file(skill_id)
|
|
202
|
+
if gate:
|
|
203
|
+
return gate.trust_level
|
|
204
|
+
return 3 # 默认
|
|
205
|
+
|
|
206
|
+
def restore(self, skill_id: str, trust_level: int = 3) -> bool:
|
|
207
|
+
"""恢复 Skill 的 trust_level。"""
|
|
208
|
+
gate = self._read_gate_file(skill_id)
|
|
209
|
+
if not gate:
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
gate.trust_level = trust_level
|
|
213
|
+
gate.blocked = False
|
|
214
|
+
self._write_gate_file(gate)
|
|
215
|
+
|
|
216
|
+
# 清除内存记录
|
|
217
|
+
self._overrides.pop(skill_id, None)
|
|
218
|
+
|
|
219
|
+
# 发射遥测
|
|
220
|
+
if self.telemetry:
|
|
221
|
+
self.telemetry.emit(
|
|
222
|
+
event_type="override_restored",
|
|
223
|
+
skill_id=skill_id,
|
|
224
|
+
channel="hook",
|
|
225
|
+
payload={"restored_trust": trust_level},
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
def check_expired(self) -> list[str]:
|
|
231
|
+
"""检查并自动恢复过期的降权。返回恢复的 skill_id 列表。"""
|
|
232
|
+
now = datetime.now(timezone.utc)
|
|
233
|
+
restored = []
|
|
234
|
+
|
|
235
|
+
for skill_id, event in list(self._overrides.items()):
|
|
236
|
+
if event.expires_at:
|
|
237
|
+
expires = datetime.fromisoformat(event.expires_at)
|
|
238
|
+
if now >= expires:
|
|
239
|
+
self.restore(skill_id, event.original_trust)
|
|
240
|
+
restored.append(skill_id)
|
|
241
|
+
|
|
242
|
+
return restored
|
|
243
|
+
|
|
244
|
+
def _compute_new_trust(self, current: int, level: OverrideLevel) -> int:
|
|
245
|
+
"""根据降权级别计算新的 trust_level。"""
|
|
246
|
+
if level == OverrideLevel.WARN:
|
|
247
|
+
return max(current - 1, 0)
|
|
248
|
+
elif level == OverrideLevel.DEGRADE:
|
|
249
|
+
return max(current - 2, 0)
|
|
250
|
+
elif level == OverrideLevel.QUARANTINE:
|
|
251
|
+
return 0
|
|
252
|
+
elif level == OverrideLevel.KILL:
|
|
253
|
+
return 0
|
|
254
|
+
return current
|
|
255
|
+
|
|
256
|
+
def _update_gate_file(self, skill_id: str, trust: int, blocked: bool, event: OverrideEvent) -> None:
|
|
257
|
+
"""更新 gate file。"""
|
|
258
|
+
gate = self._read_gate_file(skill_id) or GateFile(skill_id=skill_id)
|
|
259
|
+
gate.trust_level = trust
|
|
260
|
+
gate.blocked = blocked
|
|
261
|
+
gate.override_history.append(
|
|
262
|
+
{
|
|
263
|
+
"trigger": str(event.trigger),
|
|
264
|
+
"level": str(event.level),
|
|
265
|
+
"new_trust": event.new_trust,
|
|
266
|
+
"reason": event.reason,
|
|
267
|
+
"timestamp": event.timestamp,
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
self._write_gate_file(gate)
|
|
271
|
+
|
|
272
|
+
def _read_gate_file(self, skill_id: str) -> Optional[GateFile]:
|
|
273
|
+
"""读取 gate file。"""
|
|
274
|
+
path = self.gate_dir / f"{skill_id}.json"
|
|
275
|
+
if path.exists():
|
|
276
|
+
try:
|
|
277
|
+
return GateFile.from_dict(json.loads(path.read_text()))
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.warning("Failed to read gate file for %s: %s", skill_id, e)
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
def _write_gate_file(self, gate: GateFile) -> None:
|
|
283
|
+
"""写入 gate file。"""
|
|
284
|
+
path = self.gate_dir / f"{gate.skill_id}.json"
|
|
285
|
+
path.write_text(json.dumps(gate.to_dict(), indent=2, ensure_ascii=False))
|
skillpool/profile.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Agent capability profiles for skill matching and execution gating."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class AgentCapabilityProfile:
|
|
10
|
+
"""Describes an agent's capabilities used to determine skill compatibility.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
name: Unique identifier for the profile.
|
|
14
|
+
required_capabilities: Set of capability strings the agent possesses.
|
|
15
|
+
context_window: Maximum context window size in tokens.
|
|
16
|
+
trust_level: Trust level (1-3), higher = more privileged.
|
|
17
|
+
supported_paradigms: Set of paradigm strings the agent supports.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
name: str
|
|
21
|
+
required_capabilities: set[str] = field(default_factory=set)
|
|
22
|
+
context_window: int = 128000
|
|
23
|
+
trust_level: int = 1
|
|
24
|
+
supported_paradigms: set[str] = field(default_factory=set)
|
|
25
|
+
|
|
26
|
+
def can_execute(self, skill_requirements: dict) -> tuple[bool, str]:
|
|
27
|
+
"""Check whether this profile satisfies the given skill requirements.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
skill_requirements: Dict with optional keys:
|
|
31
|
+
- 'required_capabilities': set[str] of capabilities needed
|
|
32
|
+
- 'min_trust_level': int minimum trust level
|
|
33
|
+
- 'paradigm': str paradigm the skill belongs to
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
(True, 'ok') if all requirements are met,
|
|
37
|
+
(False, 'missing: <detail>') otherwise.
|
|
38
|
+
"""
|
|
39
|
+
# Check required capabilities
|
|
40
|
+
required_caps = skill_requirements.get("required_capabilities", set())
|
|
41
|
+
if isinstance(required_caps, (list, tuple)):
|
|
42
|
+
required_caps = set(required_caps)
|
|
43
|
+
missing_caps = required_caps - self.required_capabilities
|
|
44
|
+
if missing_caps:
|
|
45
|
+
return (False, f"missing: capabilities {sorted(missing_caps)}")
|
|
46
|
+
|
|
47
|
+
# Check minimum trust level
|
|
48
|
+
min_trust = skill_requirements.get("min_trust_level", 0)
|
|
49
|
+
if min_trust > self.trust_level:
|
|
50
|
+
return (False, f"missing: trust_level {min_trust} > {self.trust_level}")
|
|
51
|
+
|
|
52
|
+
# Check paradigm support
|
|
53
|
+
paradigm = skill_requirements.get("paradigm")
|
|
54
|
+
if paradigm and paradigm not in self.supported_paradigms:
|
|
55
|
+
return (False, f"missing: paradigm '{paradigm}'")
|
|
56
|
+
|
|
57
|
+
return (True, "ok")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Preset profiles
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
CLAUDE_CODE_PROFILE = AgentCapabilityProfile(
|
|
65
|
+
name="claude-code",
|
|
66
|
+
required_capabilities={"bash", "file_system", "python", "web_search"},
|
|
67
|
+
context_window=200000,
|
|
68
|
+
trust_level=3,
|
|
69
|
+
supported_paradigms={"review", "code", "planning", "4d", "docsdd", "sdd", "bdd", "tdd"},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
CODEX_PROFILE = AgentCapabilityProfile(
|
|
73
|
+
name="codex",
|
|
74
|
+
required_capabilities={"bash", "file_system", "python"},
|
|
75
|
+
context_window=128000,
|
|
76
|
+
trust_level=2,
|
|
77
|
+
supported_paradigms={"code", "test", "4d", "sdd", "bdd", "tdd"},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
HERMES_PROFILE = AgentCapabilityProfile(
|
|
81
|
+
name="hermes",
|
|
82
|
+
required_capabilities={"file_system", "web_search"},
|
|
83
|
+
context_window=32000,
|
|
84
|
+
trust_level=1,
|
|
85
|
+
supported_paradigms={"research", "planning", "4d", "docsdd"},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
OPENCLAW_PROFILE = AgentCapabilityProfile(
|
|
89
|
+
name="openclaw",
|
|
90
|
+
required_capabilities={"bash", "file_system", "python", "web_search"},
|
|
91
|
+
context_window=128000,
|
|
92
|
+
trust_level=2,
|
|
93
|
+
supported_paradigms={"review", "code", "test", "planning", "4d", "docsdd", "sdd", "bdd", "tdd"},
|
|
94
|
+
)
|
skillpool/quality.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""SkillPool Quality Profiler — 4-dimension scoring with auto-calculation and calibration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
from skillpool.csdf import CSDFDocument
|
|
9
|
+
|
|
10
|
+
# Default weights for overall score computation
|
|
11
|
+
DEFAULT_WEIGHTS: dict[str, float] = {
|
|
12
|
+
"completeness": 0.30,
|
|
13
|
+
"accuracy": 0.30,
|
|
14
|
+
"usability": 0.20,
|
|
15
|
+
"maintainability": 0.20,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Calibration offsets applied to raw dimension scores
|
|
19
|
+
CALIBRATION_OFFSETS: dict[str, float] = {
|
|
20
|
+
"completeness": 0.0,
|
|
21
|
+
"accuracy": -0.05,
|
|
22
|
+
"usability": 0.0,
|
|
23
|
+
"maintainability": 0.0,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _compute_overall(
|
|
28
|
+
completeness: float,
|
|
29
|
+
accuracy: float,
|
|
30
|
+
usability: float,
|
|
31
|
+
maintainability: float,
|
|
32
|
+
weights: dict[str, float],
|
|
33
|
+
) -> float:
|
|
34
|
+
"""Compute weighted overall score from dimension scores."""
|
|
35
|
+
return round(
|
|
36
|
+
completeness * weights.get("completeness", 0.0)
|
|
37
|
+
+ accuracy * weights.get("accuracy", 0.0)
|
|
38
|
+
+ usability * weights.get("usability", 0.0)
|
|
39
|
+
+ maintainability * weights.get("maintainability", 0.0),
|
|
40
|
+
4,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class QualityProfile:
|
|
46
|
+
"""Quality profile for a skill, containing 4 dimension scores and an overall score."""
|
|
47
|
+
|
|
48
|
+
name: str = ""
|
|
49
|
+
completeness: float = 0.0
|
|
50
|
+
accuracy: float = 0.0
|
|
51
|
+
usability: float = 0.0
|
|
52
|
+
maintainability: float = 0.0
|
|
53
|
+
overall: float = 0.0
|
|
54
|
+
weights: dict[str, float] = field(default_factory=lambda: dict(DEFAULT_WEIGHTS))
|
|
55
|
+
|
|
56
|
+
def __post_init__(self) -> None:
|
|
57
|
+
"""Auto-calculate overall score if not explicitly provided."""
|
|
58
|
+
# If overall is 0.0 and any dimension is non-zero, recalculate
|
|
59
|
+
if self.overall == 0.0 and any([self.completeness, self.accuracy, self.usability, self.maintainability]):
|
|
60
|
+
self.overall = _compute_overall(
|
|
61
|
+
self.completeness,
|
|
62
|
+
self.accuracy,
|
|
63
|
+
self.usability,
|
|
64
|
+
self.maintainability,
|
|
65
|
+
self.weights,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_document(
|
|
70
|
+
cls,
|
|
71
|
+
doc: CSDFDocument,
|
|
72
|
+
weights: dict[str, float] | None = None,
|
|
73
|
+
) -> QualityProfile:
|
|
74
|
+
"""Create a QualityProfile from a CSDFDocument, using its dimensions dict."""
|
|
75
|
+
w = weights or dict(DEFAULT_WEIGHTS)
|
|
76
|
+
dims = doc.dimensions
|
|
77
|
+
completeness = dims.get("completeness", 0.0)
|
|
78
|
+
accuracy = dims.get("accuracy", 0.0)
|
|
79
|
+
usability = dims.get("usability", 0.0)
|
|
80
|
+
maintainability = dims.get("maintainability", 0.0)
|
|
81
|
+
overall = _compute_overall(completeness, accuracy, usability, maintainability, w)
|
|
82
|
+
return cls(
|
|
83
|
+
name=doc.name,
|
|
84
|
+
completeness=completeness,
|
|
85
|
+
accuracy=accuracy,
|
|
86
|
+
usability=usability,
|
|
87
|
+
maintainability=maintainability,
|
|
88
|
+
overall=overall,
|
|
89
|
+
weights=w,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def from_scores(
|
|
94
|
+
cls,
|
|
95
|
+
name: str,
|
|
96
|
+
completeness: float = 0.0,
|
|
97
|
+
accuracy: float = 0.0,
|
|
98
|
+
usability: float = 0.0,
|
|
99
|
+
maintainability: float = 0.0,
|
|
100
|
+
weights: dict[str, float] | None = None,
|
|
101
|
+
) -> QualityProfile:
|
|
102
|
+
"""Create a QualityProfile from explicit dimension scores."""
|
|
103
|
+
w = weights or dict(DEFAULT_WEIGHTS)
|
|
104
|
+
overall = _compute_overall(completeness, accuracy, usability, maintainability, w)
|
|
105
|
+
return cls(
|
|
106
|
+
name=name,
|
|
107
|
+
completeness=completeness,
|
|
108
|
+
accuracy=accuracy,
|
|
109
|
+
usability=usability,
|
|
110
|
+
maintainability=maintainability,
|
|
111
|
+
overall=overall,
|
|
112
|
+
weights=w,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def compare(self, other: QualityProfile) -> dict[str, float]:
|
|
116
|
+
"""Compare this profile with another, returning dimension deltas."""
|
|
117
|
+
return {
|
|
118
|
+
"completeness": round(self.completeness - other.completeness, 4),
|
|
119
|
+
"accuracy": round(self.accuracy - other.accuracy, 4),
|
|
120
|
+
"usability": round(self.usability - other.usability, 4),
|
|
121
|
+
"maintainability": round(self.maintainability - other.maintainability, 4),
|
|
122
|
+
"overall": round(self.overall - other.overall, 4),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class QualityProfiler:
|
|
127
|
+
"""Profiles a CSDFDocument to produce a QualityProfile with auto-calculated scores."""
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
weights: dict[str, float] | None = None,
|
|
132
|
+
calibration_offsets: dict[str, float] | None = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
self.weights = weights or dict(DEFAULT_WEIGHTS)
|
|
135
|
+
self.calibration_offsets = calibration_offsets or dict(CALIBRATION_OFFSETS)
|
|
136
|
+
|
|
137
|
+
def profile(self, doc: CSDFDocument) -> QualityProfile:
|
|
138
|
+
"""Profile a CSDFDocument and return a QualityProfile with computed scores."""
|
|
139
|
+
# Use document dimensions if present, otherwise auto-calculate
|
|
140
|
+
dims = doc.dimensions
|
|
141
|
+
if dims:
|
|
142
|
+
completeness = dims.get("completeness", _score_completeness(doc))
|
|
143
|
+
accuracy = dims.get("accuracy", _score_accuracy(doc))
|
|
144
|
+
usability = dims.get("usability", _score_usability(doc))
|
|
145
|
+
maintainability = dims.get("maintainability", _score_maintainability(doc))
|
|
146
|
+
else:
|
|
147
|
+
completeness = _score_completeness(doc)
|
|
148
|
+
accuracy = _score_accuracy(doc)
|
|
149
|
+
usability = _score_usability(doc)
|
|
150
|
+
maintainability = _score_maintainability(doc)
|
|
151
|
+
|
|
152
|
+
# Apply calibration offsets
|
|
153
|
+
completeness = max(0.0, min(1.0, completeness + self.calibration_offsets.get("completeness", 0.0)))
|
|
154
|
+
accuracy = max(0.0, min(1.0, accuracy + self.calibration_offsets.get("accuracy", 0.0)))
|
|
155
|
+
usability = max(0.0, min(1.0, usability + self.calibration_offsets.get("usability", 0.0)))
|
|
156
|
+
maintainability = max(0.0, min(1.0, maintainability + self.calibration_offsets.get("maintainability", 0.0)))
|
|
157
|
+
|
|
158
|
+
overall = _compute_overall(
|
|
159
|
+
completeness,
|
|
160
|
+
accuracy,
|
|
161
|
+
usability,
|
|
162
|
+
maintainability,
|
|
163
|
+
self.weights,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
return QualityProfile(
|
|
167
|
+
name=doc.name,
|
|
168
|
+
completeness=round(completeness, 4),
|
|
169
|
+
accuracy=round(accuracy, 4),
|
|
170
|
+
usability=round(usability, 4),
|
|
171
|
+
maintainability=round(maintainability, 4),
|
|
172
|
+
overall=overall,
|
|
173
|
+
weights=self.weights,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
# Auto-scoring helpers — compute dimension scores from document content
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _score_completeness(doc: CSDFDocument) -> float:
|
|
183
|
+
"""Score completeness based on presence of key fields and content."""
|
|
184
|
+
score = 0.0
|
|
185
|
+
if doc.name:
|
|
186
|
+
score += 0.25
|
|
187
|
+
if doc.description:
|
|
188
|
+
score += 0.25
|
|
189
|
+
if doc.triggers:
|
|
190
|
+
score += 0.25
|
|
191
|
+
if doc.body:
|
|
192
|
+
score += 0.25
|
|
193
|
+
return round(min(score, 1.0), 4)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _score_accuracy(doc: CSDFDocument) -> float:
|
|
197
|
+
"""Score accuracy based on references and content indicators."""
|
|
198
|
+
score = 0.0
|
|
199
|
+
if doc.references:
|
|
200
|
+
score += min(len(doc.references) * 0.25, 0.5)
|
|
201
|
+
if doc.body:
|
|
202
|
+
code_indicators = len(re.findall(r"```", doc.body))
|
|
203
|
+
if code_indicators >= 2:
|
|
204
|
+
score += 0.3
|
|
205
|
+
elif code_indicators >= 1:
|
|
206
|
+
score += 0.15
|
|
207
|
+
tech_terms = len(re.findall(r"\b(API|function|class|method|parameter|return)\b", doc.body, re.IGNORECASE))
|
|
208
|
+
if tech_terms >= 3:
|
|
209
|
+
score += 0.2
|
|
210
|
+
elif tech_terms >= 1:
|
|
211
|
+
score += 0.1
|
|
212
|
+
return round(min(score, 1.0), 4)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _score_usability(doc: CSDFDocument) -> float:
|
|
216
|
+
"""Score usability based on structure and readability indicators."""
|
|
217
|
+
score = 0.0
|
|
218
|
+
if doc.body:
|
|
219
|
+
headers = len(re.findall(r"^#{1,6}\s+", doc.body, re.MULTILINE))
|
|
220
|
+
if headers >= 3:
|
|
221
|
+
score += 0.3
|
|
222
|
+
elif headers >= 1:
|
|
223
|
+
score += 0.15
|
|
224
|
+
lists = len(re.findall(r"^[\s]*[-*+]\s+", doc.body, re.MULTILINE))
|
|
225
|
+
if lists >= 3:
|
|
226
|
+
score += 0.2
|
|
227
|
+
elif lists >= 1:
|
|
228
|
+
score += 0.1
|
|
229
|
+
word_count = len(doc.body.split())
|
|
230
|
+
if 50 <= word_count <= 2000:
|
|
231
|
+
score += 0.3
|
|
232
|
+
elif word_count > 0:
|
|
233
|
+
score += 0.15
|
|
234
|
+
if doc.triggers:
|
|
235
|
+
score += 0.1
|
|
236
|
+
return round(min(score, 1.0), 4)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _score_maintainability(doc: CSDFDocument) -> float:
|
|
240
|
+
"""Score maintainability based on version info and content organization."""
|
|
241
|
+
score = 0.0
|
|
242
|
+
if doc.version and re.match(r"^\d+\.\d+\.\d+", doc.version):
|
|
243
|
+
score += 0.3
|
|
244
|
+
if doc.description:
|
|
245
|
+
score += 0.2
|
|
246
|
+
if doc.body:
|
|
247
|
+
paragraphs = [p for p in doc.body.split("\n\n") if p.strip()]
|
|
248
|
+
if paragraphs:
|
|
249
|
+
avg_len = sum(len(p) for p in paragraphs) / len(paragraphs)
|
|
250
|
+
if avg_len < 200:
|
|
251
|
+
score += 0.20
|
|
252
|
+
elif avg_len < 400:
|
|
253
|
+
score += 0.10
|
|
254
|
+
return round(min(score, 1.0), 4)
|