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,157 @@
1
+ """Gate Policy Incremental Assessor — Detect changed files and assess complexity.
2
+
3
+ Error Codes:
4
+ GP005: git diff execution failure
5
+
6
+ Contracts:
7
+ - git diff failure returns empty list, never crashes (B18)
8
+ - Timeout enforced via subprocess timeout (B12)
9
+ - Per-file level resolution with highest-level aggregation
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import re
16
+ import subprocess
17
+ from pathlib import Path
18
+
19
+ from pydantic import BaseModel, Field
20
+
21
+ from skillpool.gate_policy.parser import (
22
+ GatePolicyConfig,
23
+ resolve_level_for_path,
24
+ )
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Models
31
+ # ---------------------------------------------------------------------------
32
+
33
+
34
+ class ComplexityAssessment(BaseModel):
35
+ """Result of incremental complexity assessment."""
36
+
37
+ level: str | None = None
38
+ changed_files: list[str] = Field(default_factory=list)
39
+ per_file_levels: dict[str, str] = Field(default_factory=dict)
40
+ override_applied: str | None = None
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Level ordering
45
+ # ---------------------------------------------------------------------------
46
+
47
+ _LEVEL_ORDER = {"L0": 0, "L1": 1, "L2": 2, "L3+L2+": 3}
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # IncrementalAssessor
52
+ # ---------------------------------------------------------------------------
53
+
54
+
55
+ class IncrementalAssessor:
56
+ """Detect changed files and assess per-file complexity.
57
+
58
+ Args:
59
+ policy: GatePolicyConfig for path-based level resolution.
60
+ git_timeout: Max seconds for git diff command (default: 5).
61
+
62
+ Contract:
63
+ - git diff failure returns empty list, never crashes (B18).
64
+ - Timeout enforced via subprocess timeout (B12).
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ policy: GatePolicyConfig,
70
+ git_timeout: int = 5,
71
+ ) -> None:
72
+ self._policy = policy
73
+ self._git_timeout = git_timeout
74
+
75
+ def detect_changed_files(
76
+ self,
77
+ base_ref: str = "HEAD",
78
+ cwd: Path | None = None,
79
+ ) -> list[str]:
80
+ """Detect files changed since base_ref.
81
+
82
+ Args:
83
+ base_ref: Git ref to compare against (default: "HEAD").
84
+ cwd: Working directory for git command.
85
+
86
+ Returns:
87
+ List of relative file paths.
88
+
89
+ Contract:
90
+ - Uses `git diff --name-only <base_ref>`.
91
+ - base_ref validated: alphanumeric, dots, dashes, slashes only.
92
+ - Timeout: git_timeout seconds (B12).
93
+ - Fallback: empty list on any error (B18).
94
+ """
95
+ # Validate base_ref to prevent injection
96
+ if not re.match(r"^[a-zA-Z0-9._/\-]+$", base_ref):
97
+ return []
98
+
99
+ try:
100
+ result = subprocess.run(
101
+ ["git", "diff", "--name-only", base_ref],
102
+ capture_output=True,
103
+ text=True,
104
+ timeout=self._git_timeout,
105
+ cwd=cwd,
106
+ )
107
+ if result.returncode != 0:
108
+ logger.warning("GP005: git diff returned non-zero: %s", result.stderr.strip())
109
+ return []
110
+ files = [f for f in result.stdout.strip().split("\n") if f]
111
+ return files
112
+ except subprocess.TimeoutExpired:
113
+ logger.warning("GP005: git diff timed out after %ds", self._git_timeout)
114
+ return []
115
+ except (FileNotFoundError, OSError) as e:
116
+ logger.warning("GP005: git diff execution failed: %s", e)
117
+ return []
118
+
119
+ def assess_complexity(
120
+ self,
121
+ files: list[str],
122
+ ) -> ComplexityAssessment:
123
+ """Assess complexity for a list of changed files.
124
+
125
+ Args:
126
+ files: List of relative file paths.
127
+
128
+ Returns:
129
+ ComplexityAssessment with level, per_file_levels, override_applied.
130
+
131
+ Contract:
132
+ - Empty files list → level=None (AC08).
133
+ - Per-file level via resolve_level_for_path().
134
+ - Aggregated level = highest across all files (AC10).
135
+ - skip_phases = union of all files' skip_phases.
136
+ """
137
+ if not files:
138
+ return ComplexityAssessment(changed_files=[], per_file_levels={})
139
+
140
+ per_file: dict[str, str] = {}
141
+ highest_level = "L0"
142
+ override_applied: str | None = None
143
+
144
+ for f in files:
145
+ resolution = resolve_level_for_path(f, self._policy)
146
+ per_file[f] = resolution.level
147
+ if _LEVEL_ORDER.get(resolution.level, 0) > _LEVEL_ORDER.get(highest_level, 0):
148
+ highest_level = resolution.level
149
+ if resolution.matched_rules:
150
+ override_applied = resolution.matched_rules[0]
151
+
152
+ return ComplexityAssessment(
153
+ level=highest_level,
154
+ changed_files=files,
155
+ per_file_levels=per_file,
156
+ override_applied=override_applied,
157
+ )
@@ -0,0 +1,258 @@
1
+ """Gate Policy Parser — Parse gate.policy YAML and resolve path-based overrides.
2
+
3
+ Error Codes:
4
+ GP001: gate.policy file not found
5
+ GP002: YAML parse or validation error
6
+
7
+ Contracts:
8
+ - load_gate_policy(): Parse YAML → frozen GatePolicyConfig
9
+ - resolve_level_for_path(): Path → LevelResolution with overrides applied
10
+ - Deterministic: same inputs → same outputs (B10)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from fnmatch import fnmatch
16
+ from pathlib import Path
17
+
18
+ import yaml
19
+ from pydantic import BaseModel, Field
20
+
21
+ from typing import Literal
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Error Handling
26
+ # ---------------------------------------------------------------------------
27
+
28
+
29
+ class GatePolicyError(Exception):
30
+ """Base exception for gate policy errors.
31
+
32
+ Attributes:
33
+ error_code: One of GP001-GP006
34
+ detail: Human-readable description
35
+ """
36
+
37
+ def __init__(self, error_code: str, detail: str):
38
+ self.error_code = error_code
39
+ self.detail = detail
40
+ super().__init__(f"[{error_code}] {detail}")
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Pydantic Models (frozen for B11)
45
+ # ---------------------------------------------------------------------------
46
+
47
+
48
+ class DirectoryOverride(BaseModel):
49
+ """Single directory override rule."""
50
+
51
+ path: str
52
+ minimum_level: str | None = None
53
+ maximum_level: str | None = None
54
+ skip_phases: list[str] = Field(default_factory=list)
55
+ skip_all: bool = False
56
+ reason: str = ""
57
+
58
+
59
+ class FilePattern(BaseModel):
60
+ """File pattern override rule."""
61
+
62
+ pattern: str
63
+ skip_phases: list[str] = Field(default_factory=list)
64
+ skip_all: bool = False
65
+ maximum_level: str | None = None
66
+ reason: str = ""
67
+
68
+
69
+ class PhaseGate(BaseModel):
70
+ """Gate check rule for a phase transition."""
71
+
72
+ required_artifacts: list[str] = Field(default_factory=list)
73
+ validation: str = ""
74
+ level_condition: str | None = None
75
+
76
+
77
+ class EmergencyBypass(BaseModel):
78
+ """Emergency bypass configuration."""
79
+
80
+ enabled: bool = False
81
+ config_file: str = "emergency_overrides.json"
82
+ allowed_phases: list[str] = Field(default_factory=lambda: ["SDD", "TDD"])
83
+ max_duration_hours: int = 24
84
+ require_retrospective: bool = True
85
+
86
+
87
+ class ReviewTrigger(BaseModel):
88
+ """Review checkpoint trigger condition."""
89
+
90
+ condition: str
91
+ checkpoint: str = "L4"
92
+ required: bool = True
93
+ reason: str = ""
94
+
95
+
96
+ class EnforcementConfig(BaseModel):
97
+ """Gate enforcement mode configuration."""
98
+
99
+ mode: Literal["strict", "permissive", "disabled"] = "strict"
100
+ hook_integration: bool = True
101
+ log_all_transitions: bool = True
102
+ audit_trail: bool = True
103
+
104
+
105
+ class GatePolicyConfig(BaseModel):
106
+ """Complete gate.policy configuration. Frozen after creation (B11)."""
107
+
108
+ model_config = {"frozen": True}
109
+
110
+ version: str = "1.0"
111
+ default_level: str = "L2"
112
+ phase_gates: dict[str, PhaseGate] = Field(default_factory=dict)
113
+ directory_overrides: list[DirectoryOverride] = Field(default_factory=list)
114
+ file_patterns: list[FilePattern] = Field(default_factory=list)
115
+ emergency_bypass: EmergencyBypass = Field(default_factory=EmergencyBypass)
116
+ review_triggers: list[ReviewTrigger] = Field(default_factory=list)
117
+ enforcement: EnforcementConfig = Field(default_factory=EnforcementConfig)
118
+
119
+
120
+ class LevelResolution(BaseModel):
121
+ """Result of resolving complexity level for a path."""
122
+
123
+ level: str
124
+ skip_phases: list[str] = Field(default_factory=list)
125
+ skip_all: bool = False
126
+ matched_rules: list[str] = Field(default_factory=list)
127
+
128
+
129
+ # ---------------------------------------------------------------------------
130
+ # Level ordering for min/max comparisons
131
+ # ---------------------------------------------------------------------------
132
+
133
+ _LEVEL_ORDER = {"L0": 0, "L1": 1, "L2": 2, "L3+L2+": 3}
134
+
135
+
136
+ def _level_ge(a: str, b: str) -> bool:
137
+ """Return True if level a >= level b."""
138
+ return _LEVEL_ORDER.get(a, 0) >= _LEVEL_ORDER.get(b, 0)
139
+
140
+
141
+ def _max_level(a: str, b: str) -> str:
142
+ """Return the higher of two levels."""
143
+ return a if _LEVEL_ORDER.get(a, 0) >= _LEVEL_ORDER.get(b, 0) else b
144
+
145
+
146
+ def _min_level(a: str, b: str) -> str:
147
+ """Return the lower of two levels."""
148
+ return a if _LEVEL_ORDER.get(a, 0) <= _LEVEL_ORDER.get(b, 0) else b
149
+
150
+
151
+ # ---------------------------------------------------------------------------
152
+ # Core Functions
153
+ # ---------------------------------------------------------------------------
154
+
155
+
156
+ def load_gate_policy(policy_path: Path) -> GatePolicyConfig:
157
+ """Parse gate.policy YAML file into validated config.
158
+
159
+ Args:
160
+ policy_path: Path to gate.policy YAML file.
161
+
162
+ Returns:
163
+ GatePolicyConfig: Frozen, validated configuration object.
164
+
165
+ Raises:
166
+ GatePolicyError: GP001 if file not found.
167
+ GatePolicyError: GP002 if YAML parse or validation error.
168
+
169
+ Contract:
170
+ - Same path always returns structurally identical config (B10).
171
+ - Config is frozen after creation (B11).
172
+ - All 6 sections populated or use defaults.
173
+ """
174
+ if not policy_path.exists():
175
+ raise GatePolicyError("GP001", f"gate.policy not found: {policy_path}")
176
+
177
+ try:
178
+ raw = yaml.safe_load(policy_path.read_text())
179
+ if raw is None:
180
+ raw = {}
181
+ return GatePolicyConfig.model_validate(raw)
182
+ except yaml.YAMLError as e:
183
+ raise GatePolicyError("GP002", f"YAML parse error: {e}") from e
184
+ except Exception as e:
185
+ raise GatePolicyError("GP002", f"Validation error: {e}") from e
186
+
187
+
188
+ def resolve_level_for_path(
189
+ file_path: str,
190
+ policy: GatePolicyConfig,
191
+ ) -> LevelResolution:
192
+ """Resolve effective complexity level for a file path.
193
+
194
+ Args:
195
+ file_path: Relative file path (e.g., "src/core/engine.py").
196
+ policy: Loaded gate policy configuration.
197
+
198
+ Returns:
199
+ LevelResolution with level, skip_phases, skip_all, matched_rules.
200
+
201
+ Contract:
202
+ - Applies directory_overrides first (longest-prefix match).
203
+ - Then applies file_patterns (glob match).
204
+ - minimum_level upgrades, maximum_level downgrades.
205
+ - skip_phases merged (union).
206
+ - Deterministic: same inputs → same outputs (B10).
207
+ """
208
+ level = policy.default_level
209
+ skip_phases: set[str] = set()
210
+ skip_all = False
211
+ matched_rules: list[str] = []
212
+
213
+ # Normalize path (remove leading ./ or /)
214
+ norm_path = file_path.lstrip("./")
215
+
216
+ # Step 1: Find longest-prefix matching directory override
217
+ best_dir_match: DirectoryOverride | None = None
218
+ best_dir_len = 0
219
+ for override in policy.directory_overrides:
220
+ opath = override.path.rstrip("/")
221
+ if norm_path.startswith(opath + "/") or norm_path == opath:
222
+ if len(opath) > best_dir_len:
223
+ best_dir_match = override
224
+ best_dir_len = len(opath)
225
+
226
+ if best_dir_match:
227
+ matched_rules.append(best_dir_match.path)
228
+ if best_dir_match.minimum_level:
229
+ if _level_ge(best_dir_match.minimum_level, level):
230
+ level = best_dir_match.minimum_level
231
+ if best_dir_match.maximum_level:
232
+ # Cap level at maximum_level (level must not exceed maximum)
233
+ if _LEVEL_ORDER.get(level, 0) > _LEVEL_ORDER.get(best_dir_match.maximum_level, 0):
234
+ level = best_dir_match.maximum_level
235
+ skip_phases.update(best_dir_match.skip_phases)
236
+ if best_dir_match.skip_all:
237
+ skip_all = True
238
+
239
+ # Step 2: Apply file pattern matches
240
+ for fp in policy.file_patterns:
241
+ # Match against both full path and basename
242
+ basename = norm_path.rsplit("/", 1)[-1] if "/" in norm_path else norm_path
243
+ if fnmatch(norm_path, fp.pattern) or fnmatch(basename, fp.pattern):
244
+ matched_rules.append(fp.pattern)
245
+ if fp.maximum_level:
246
+ # Cap level at maximum_level (level must not exceed maximum)
247
+ if _LEVEL_ORDER.get(level, 0) > _LEVEL_ORDER.get(fp.maximum_level, 0):
248
+ level = fp.maximum_level
249
+ skip_phases.update(fp.skip_phases)
250
+ if fp.skip_all:
251
+ skip_all = True
252
+
253
+ return LevelResolution(
254
+ level=level,
255
+ skip_phases=sorted(skip_phases),
256
+ skip_all=skip_all,
257
+ matched_rules=matched_rules,
258
+ )