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,432 @@
|
|
|
1
|
+
"""Gate State Machine — 7-state FSM for 4D paradigm phase transitions.
|
|
2
|
+
|
|
3
|
+
Error Codes:
|
|
4
|
+
GP003: Illegal phase transition
|
|
5
|
+
GP004: Missing required artifact for gate
|
|
6
|
+
GP006: gate.json read/write failure
|
|
7
|
+
|
|
8
|
+
States: IDLE, ASSESSING, DOCSDD, SDD, BDD, TDD, REVIEW, COMPLETE
|
|
9
|
+
|
|
10
|
+
Contracts:
|
|
11
|
+
- gate.json persists across process restarts (B04)
|
|
12
|
+
- All transitions logged to phase_history (B13)
|
|
13
|
+
- Atomic writes via temp file + os.replace()
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import tempfile
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from pydantic import BaseModel, Field
|
|
25
|
+
|
|
26
|
+
from skillpool.gate_policy.parser import (
|
|
27
|
+
GatePolicyConfig,
|
|
28
|
+
GatePolicyError,
|
|
29
|
+
resolve_level_for_path,
|
|
30
|
+
)
|
|
31
|
+
from skillpool.utils.time_utils import utc_now
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Pydantic Models
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PhaseTransition(BaseModel):
|
|
42
|
+
"""Single phase transition record."""
|
|
43
|
+
|
|
44
|
+
from_phase: str
|
|
45
|
+
to_phase: str
|
|
46
|
+
timestamp: str
|
|
47
|
+
reason: str = ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ReviewCheckpoint(BaseModel):
|
|
51
|
+
"""Review checkpoint state."""
|
|
52
|
+
|
|
53
|
+
triggered: bool = False
|
|
54
|
+
checkpoint_level: str | None = None
|
|
55
|
+
review_result: str | None = None
|
|
56
|
+
veto_status: str | None = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class GateMetadata(BaseModel):
|
|
60
|
+
"""Gate state metadata."""
|
|
61
|
+
|
|
62
|
+
created_at: str | None = None
|
|
63
|
+
updated_at: str | None = None
|
|
64
|
+
session_id: str | None = None
|
|
65
|
+
task_description: str | None = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class GateStateFile(BaseModel):
|
|
69
|
+
"""gate.json runtime state."""
|
|
70
|
+
|
|
71
|
+
incremental_mode: bool = True
|
|
72
|
+
current_phase: str = "IDLE"
|
|
73
|
+
assessed_level: str | None = None
|
|
74
|
+
assessed_at: str | None = None
|
|
75
|
+
changed_files: list[str] = Field(default_factory=list)
|
|
76
|
+
phase_history: list[PhaseTransition] = Field(default_factory=list)
|
|
77
|
+
gate_checks: dict[str, str | None] = Field(default_factory=dict)
|
|
78
|
+
artifacts: dict[str, str | None] = Field(default_factory=dict)
|
|
79
|
+
review_checkpoint: ReviewCheckpoint = Field(default_factory=ReviewCheckpoint)
|
|
80
|
+
metadata: GateMetadata = Field(default_factory=GateMetadata)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class GateCheckResult(BaseModel):
|
|
84
|
+
"""Result of a gate check between phases."""
|
|
85
|
+
|
|
86
|
+
passed: bool
|
|
87
|
+
missing_artifacts: list[str] = Field(default_factory=list)
|
|
88
|
+
validation_message: str = ""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# Legal Transitions Table
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
_LEGAL_TRANSITIONS: dict[str, set[str]] = {
|
|
96
|
+
"IDLE": {"ASSESSING"},
|
|
97
|
+
"ASSESSING": {"COMPLETE", "DOCSDD", "SDD"},
|
|
98
|
+
"DOCSDD": {"SDD"},
|
|
99
|
+
"SDD": {"BDD", "TDD"},
|
|
100
|
+
"BDD": {"TDD"},
|
|
101
|
+
"TDD": {"REVIEW", "COMPLETE"},
|
|
102
|
+
"REVIEW": {"COMPLETE"},
|
|
103
|
+
"COMPLETE": set(),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Conditional transition rules: (from, to) → required assessed_level
|
|
107
|
+
_TRANSITION_LEVEL_CONDITIONS: dict[tuple[str, str], set[str]] = {
|
|
108
|
+
("ASSESSING", "COMPLETE"): {"L0"},
|
|
109
|
+
("ASSESSING", "DOCSDD"): {"L2", "L3+L2+"},
|
|
110
|
+
("ASSESSING", "SDD"): {"L1", "L2", "L3+L2+"},
|
|
111
|
+
("SDD", "BDD"): {"L2", "L3+L2+"},
|
|
112
|
+
("SDD", "TDD"): {"L1"},
|
|
113
|
+
("TDD", "REVIEW"): {"L3+L2+"},
|
|
114
|
+
("TDD", "COMPLETE"): {"L0", "L1", "L2"},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# GateStateMachine
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class GateStateMachine:
|
|
124
|
+
"""7-state FSM for 4D paradigm phase transitions.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
state_path: Path to gate.json file for persistence.
|
|
128
|
+
|
|
129
|
+
Contract:
|
|
130
|
+
- Loads state from gate.json on init (B04).
|
|
131
|
+
- If gate.json missing/corrupt, starts at IDLE.
|
|
132
|
+
- All transitions logged to phase_history (B13).
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def __init__(self, state_path: Path) -> None:
|
|
136
|
+
self._state_path = state_path
|
|
137
|
+
self._state = self._load_state()
|
|
138
|
+
self._policy: GatePolicyConfig | None = None
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def state(self) -> GateStateFile:
|
|
142
|
+
"""Current gate state."""
|
|
143
|
+
return self._state
|
|
144
|
+
|
|
145
|
+
def set_policy(self, policy: GatePolicyConfig) -> None:
|
|
146
|
+
"""Set policy for gate check validation."""
|
|
147
|
+
self._policy = policy
|
|
148
|
+
|
|
149
|
+
def assess(
|
|
150
|
+
self,
|
|
151
|
+
task_description: str,
|
|
152
|
+
changed_files: list[str],
|
|
153
|
+
policy: GatePolicyConfig | None = None,
|
|
154
|
+
) -> str:
|
|
155
|
+
"""Assess complexity and set assessed_level.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
task_description: Natural language task description.
|
|
159
|
+
changed_files: List of changed file paths.
|
|
160
|
+
policy: Optional policy for path-based level resolution.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Assessed complexity level string (L0/L1/L2/L3+L2+).
|
|
164
|
+
|
|
165
|
+
Side effects:
|
|
166
|
+
- Transitions IDLE → ASSESSING.
|
|
167
|
+
- Sets assessed_level in gate.json.
|
|
168
|
+
- If L0, immediately transitions to COMPLETE.
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
GatePolicyError: GP003 if not in IDLE state.
|
|
172
|
+
"""
|
|
173
|
+
if self._state.current_phase != "IDLE":
|
|
174
|
+
raise GatePolicyError(
|
|
175
|
+
"GP003",
|
|
176
|
+
f"assess() can only be called from IDLE, current: {self._state.current_phase}",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Transition IDLE → ASSESSING
|
|
180
|
+
self._do_transition("ASSESSING", reason="assess called")
|
|
181
|
+
|
|
182
|
+
# Determine level from changed files if policy provided
|
|
183
|
+
if policy and changed_files:
|
|
184
|
+
# Use highest level across all files
|
|
185
|
+
levels = set()
|
|
186
|
+
for f in changed_files:
|
|
187
|
+
resolution = resolve_level_for_path(f, policy)
|
|
188
|
+
levels.add(resolution.level)
|
|
189
|
+
# Pick highest
|
|
190
|
+
level_order = {"L0": 0, "L1": 1, "L2": 2, "L3+L2+": 3}
|
|
191
|
+
assessed = max(levels, key=lambda lvl: level_order.get(lvl, 0))
|
|
192
|
+
else:
|
|
193
|
+
# Simple keyword-based assessment as fallback
|
|
194
|
+
assessed = _keyword_assess(task_description)
|
|
195
|
+
|
|
196
|
+
self._state.assessed_level = assessed
|
|
197
|
+
self._state.assessed_at = utc_now().isoformat()
|
|
198
|
+
self._state.changed_files = list(changed_files)
|
|
199
|
+
self._state.metadata.task_description = task_description
|
|
200
|
+
self._persist()
|
|
201
|
+
|
|
202
|
+
# Auto-transition if L0
|
|
203
|
+
if assessed == "L0":
|
|
204
|
+
self._do_transition("COMPLETE", reason="L0 auto-complete")
|
|
205
|
+
|
|
206
|
+
return assessed
|
|
207
|
+
|
|
208
|
+
def transition(self, target_phase: str) -> GateStateFile:
|
|
209
|
+
"""Transition to target phase.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
target_phase: One of the 7 valid phase names.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Updated GateStateFile after transition.
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
GatePolicyError: GP003 if transition is illegal (strict mode).
|
|
219
|
+
"""
|
|
220
|
+
current = self._state.current_phase
|
|
221
|
+
|
|
222
|
+
# Determine enforcement mode
|
|
223
|
+
mode = "strict"
|
|
224
|
+
if self._policy:
|
|
225
|
+
mode = self._policy.enforcement.mode
|
|
226
|
+
|
|
227
|
+
# Check if transition is in legal table
|
|
228
|
+
illegal = target_phase not in _LEGAL_TRANSITIONS.get(current, set())
|
|
229
|
+
|
|
230
|
+
# Check level conditions
|
|
231
|
+
level_violation = False
|
|
232
|
+
key = (current, target_phase)
|
|
233
|
+
if key in _TRANSITION_LEVEL_CONDITIONS:
|
|
234
|
+
required_levels = _TRANSITION_LEVEL_CONDITIONS[key]
|
|
235
|
+
assessed = self._state.assessed_level
|
|
236
|
+
if assessed not in required_levels:
|
|
237
|
+
level_violation = True
|
|
238
|
+
|
|
239
|
+
if illegal or level_violation:
|
|
240
|
+
if mode == "strict":
|
|
241
|
+
reason = f"Illegal transition: {current} → {target_phase}"
|
|
242
|
+
if level_violation:
|
|
243
|
+
reason = (
|
|
244
|
+
f"Transition {current} → {target_phase} requires assessed_level "
|
|
245
|
+
f"in {_TRANSITION_LEVEL_CONDITIONS[key]}, got {self._state.assessed_level}"
|
|
246
|
+
)
|
|
247
|
+
raise GatePolicyError("GP003", reason)
|
|
248
|
+
elif mode == "permissive":
|
|
249
|
+
logger.warning(
|
|
250
|
+
"GP003 (permissive): Illegal transition %s → %s blocked but not raised",
|
|
251
|
+
current,
|
|
252
|
+
target_phase,
|
|
253
|
+
)
|
|
254
|
+
return self._state # No transition, no exception
|
|
255
|
+
# disabled mode: proceed regardless
|
|
256
|
+
logger.warning(
|
|
257
|
+
"GP003 (disabled): Illegal transition %s → %s allowed by disabled enforcement",
|
|
258
|
+
current,
|
|
259
|
+
target_phase,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
self._do_transition(target_phase, reason=f"transition to {target_phase}")
|
|
263
|
+
return self._state
|
|
264
|
+
|
|
265
|
+
def check_gate(
|
|
266
|
+
self,
|
|
267
|
+
from_phase: str,
|
|
268
|
+
to_phase: str,
|
|
269
|
+
artifacts: dict[str, str | None],
|
|
270
|
+
policy: GatePolicyConfig | None = None,
|
|
271
|
+
) -> GateCheckResult:
|
|
272
|
+
"""Check if gate transition is allowed given current artifacts.
|
|
273
|
+
|
|
274
|
+
Does NOT modify state. Pure check function.
|
|
275
|
+
"""
|
|
276
|
+
gate_key = f"{from_phase.lower()}_to_{to_phase.lower()}"
|
|
277
|
+
missing: list[str] = []
|
|
278
|
+
|
|
279
|
+
# Check policy phase_gates
|
|
280
|
+
effective_policy = policy or self._policy
|
|
281
|
+
if effective_policy and gate_key in effective_policy.phase_gates:
|
|
282
|
+
gate = effective_policy.phase_gates[gate_key]
|
|
283
|
+
for artifact_name in gate.required_artifacts:
|
|
284
|
+
if artifacts.get(artifact_name) is None:
|
|
285
|
+
missing.append(artifact_name)
|
|
286
|
+
|
|
287
|
+
# Check emergency bypass — only active if override file exists
|
|
288
|
+
if effective_policy and effective_policy.emergency_bypass.enabled:
|
|
289
|
+
bypass_path = self._state_path.parent / effective_policy.emergency_bypass.config_file
|
|
290
|
+
bypass_file_exists = bypass_path.exists()
|
|
291
|
+
bypass_expired = self._check_bypass_expiry(effective_policy) if bypass_file_exists else False
|
|
292
|
+
if bypass_file_exists and not bypass_expired:
|
|
293
|
+
# Bypass is active
|
|
294
|
+
if to_phase in effective_policy.emergency_bypass.allowed_phases:
|
|
295
|
+
return GateCheckResult(passed=True, validation_message="Emergency bypass active")
|
|
296
|
+
else:
|
|
297
|
+
missing.append(f"emergency_bypass:phase_{to_phase}_not_allowed")
|
|
298
|
+
|
|
299
|
+
return GateCheckResult(
|
|
300
|
+
passed=len(missing) == 0,
|
|
301
|
+
missing_artifacts=missing,
|
|
302
|
+
validation_message="All artifacts present" if not missing else f"Missing: {missing}",
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def update_artifact(self, name: str, value: str) -> None:
|
|
306
|
+
"""Update artifact status in gate.json."""
|
|
307
|
+
self._state.artifacts[name] = value
|
|
308
|
+
self._persist()
|
|
309
|
+
|
|
310
|
+
def reset(self) -> GateStateFile:
|
|
311
|
+
"""Reset state machine to IDLE.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
GateStateFile with current_phase="IDLE", cleared history.
|
|
315
|
+
|
|
316
|
+
Contract:
|
|
317
|
+
- Clears assessed_level, changed_files, phase_history, gate_checks, artifacts, review_checkpoint.
|
|
318
|
+
- Persists reset state to gate.json.
|
|
319
|
+
- Does NOT clear metadata.created_at.
|
|
320
|
+
- Gate key format: ``{from_phase.lower()}_to_{to_phase.lower()}``
|
|
321
|
+
(e.g. "sdd_to_bdd", "tdd_to_review"). After reset, all gate_checks are cleared.
|
|
322
|
+
"""
|
|
323
|
+
created_at = self._state.metadata.created_at
|
|
324
|
+
self._state = GateStateFile()
|
|
325
|
+
self._state.metadata.created_at = created_at
|
|
326
|
+
self._state.metadata.updated_at = utc_now().isoformat()
|
|
327
|
+
self._persist()
|
|
328
|
+
return self._state
|
|
329
|
+
|
|
330
|
+
# -----------------------------------------------------------------------
|
|
331
|
+
# Internal
|
|
332
|
+
# -----------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
def _check_bypass_expiry(self, policy: GatePolicyConfig) -> bool:
|
|
335
|
+
"""Check if emergency bypass has expired.
|
|
336
|
+
|
|
337
|
+
Returns True if bypass has expired (should enforce normal rules).
|
|
338
|
+
Returns False if bypass is still active or no override file exists.
|
|
339
|
+
"""
|
|
340
|
+
bypass_path = self._state_path.parent / policy.emergency_bypass.config_file
|
|
341
|
+
if not bypass_path.exists():
|
|
342
|
+
return False # No override file → bypass not activated
|
|
343
|
+
try:
|
|
344
|
+
data = json.loads(bypass_path.read_text())
|
|
345
|
+
expires_at = data.get("expires_at")
|
|
346
|
+
if not expires_at:
|
|
347
|
+
return False # No expiry set → bypass active indefinitely
|
|
348
|
+
from datetime import datetime
|
|
349
|
+
|
|
350
|
+
expiry = datetime.fromisoformat(expires_at)
|
|
351
|
+
now = utc_now()
|
|
352
|
+
if now >= expiry:
|
|
353
|
+
return True # Bypass expired
|
|
354
|
+
return False
|
|
355
|
+
except Exception:
|
|
356
|
+
return False # Corrupt file → treat as bypass active (safer default)
|
|
357
|
+
|
|
358
|
+
def _do_transition(self, target: str, reason: str = "") -> None:
|
|
359
|
+
"""Execute a transition and log it."""
|
|
360
|
+
transition = PhaseTransition(
|
|
361
|
+
from_phase=self._state.current_phase,
|
|
362
|
+
to_phase=target,
|
|
363
|
+
timestamp=utc_now().isoformat(),
|
|
364
|
+
reason=reason,
|
|
365
|
+
)
|
|
366
|
+
self._state.phase_history.append(transition)
|
|
367
|
+
self._state.current_phase = target
|
|
368
|
+
self._state.metadata.updated_at = utc_now().isoformat()
|
|
369
|
+
# Auto-set review_checkpoint when entering REVIEW
|
|
370
|
+
if target == "REVIEW":
|
|
371
|
+
self._state.review_checkpoint.triggered = True
|
|
372
|
+
self._state.review_checkpoint.checkpoint_level = self._state.assessed_level
|
|
373
|
+
self._persist()
|
|
374
|
+
|
|
375
|
+
def _load_state(self) -> GateStateFile:
|
|
376
|
+
"""Load state from gate.json. Returns IDLE on any failure (GP006)."""
|
|
377
|
+
if not self._state_path.exists():
|
|
378
|
+
state = GateStateFile()
|
|
379
|
+
state.metadata.created_at = utc_now().isoformat()
|
|
380
|
+
return state
|
|
381
|
+
try:
|
|
382
|
+
data = json.loads(self._state_path.read_text())
|
|
383
|
+
return GateStateFile.model_validate(data)
|
|
384
|
+
except Exception:
|
|
385
|
+
# GP006: corrupt file, reset to IDLE
|
|
386
|
+
state = GateStateFile()
|
|
387
|
+
state.metadata.created_at = utc_now().isoformat()
|
|
388
|
+
return state
|
|
389
|
+
|
|
390
|
+
def _persist(self) -> None:
|
|
391
|
+
"""Atomic write of gate.json via temp file + os.replace()."""
|
|
392
|
+
self._state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
393
|
+
data = self._state.model_dump(mode="json")
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
fd, tmp_path = tempfile.mkstemp(
|
|
397
|
+
dir=str(self._state_path.parent),
|
|
398
|
+
suffix=".tmp",
|
|
399
|
+
)
|
|
400
|
+
with os.fdopen(fd, "w") as f:
|
|
401
|
+
json.dump(data, f, indent=2)
|
|
402
|
+
os.replace(tmp_path, self._state_path)
|
|
403
|
+
except Exception:
|
|
404
|
+
# GP006: write failure, try to clean up temp file
|
|
405
|
+
try:
|
|
406
|
+
os.unlink(tmp_path)
|
|
407
|
+
except Exception:
|
|
408
|
+
pass
|
|
409
|
+
raise
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
# ---------------------------------------------------------------------------
|
|
413
|
+
# Helper: Keyword-based complexity assessment (fallback)
|
|
414
|
+
# ---------------------------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
_KEYWORD_LEVELS: dict[str, list[str]] = {
|
|
417
|
+
"L0": ["typo", "comment", "log", "style", "rename", "whitespace", "formatting"],
|
|
418
|
+
"L1": ["config", "flag", "param", "validation", "default", "simple fix", "parameter"],
|
|
419
|
+
"L2": ["feature", "module", "refactor", "api", "integration", "new feature"],
|
|
420
|
+
"L3+L2+": ["subsystem", "migration", "breaking", "security", "architecture", "performance"],
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _keyword_assess(task_description: str) -> str:
|
|
425
|
+
"""Assess complexity from task description keywords."""
|
|
426
|
+
desc_lower = task_description.lower()
|
|
427
|
+
# Check from highest to lowest
|
|
428
|
+
for level in ["L3+L2+", "L2", "L1", "L0"]:
|
|
429
|
+
for keyword in _KEYWORD_LEVELS[level]:
|
|
430
|
+
if keyword in desc_lower:
|
|
431
|
+
return level
|
|
432
|
+
return "L2" # Default
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Graph module — Skill graph algorithms."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = ["personalized_pagerank", "reverse_ppr"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def __getattr__(name: str):
|
|
9
|
+
"""Lazy import — numpy/scipy are optional dependencies."""
|
|
10
|
+
if name in ("personalized_pagerank", "reverse_ppr"):
|
|
11
|
+
from skillpool.graph.ppr import personalized_pagerank, reverse_ppr # noqa: F401
|
|
12
|
+
|
|
13
|
+
return locals().get(name)
|
|
14
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|