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,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)