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,154 @@
|
|
|
1
|
+
"""Resolver models — Pydantic schemas for skill resolution.
|
|
2
|
+
|
|
3
|
+
Aligned with contracts/schemas/skill_resolve_request.v1.json
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ResolveStrategy(StrEnum):
|
|
15
|
+
STRICT = "strict"
|
|
16
|
+
BEST_EFFORT = "best_effort"
|
|
17
|
+
FUZZY = "fuzzy"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ResolveStatus(StrEnum):
|
|
21
|
+
"""Three-state resolution status (per schema)."""
|
|
22
|
+
|
|
23
|
+
RESOLVED = "resolved"
|
|
24
|
+
PARTIAL = "partial"
|
|
25
|
+
UNRESOLVED = "unresolved"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConflictSeverity(StrEnum):
|
|
29
|
+
NONE = "none"
|
|
30
|
+
LOW = "low"
|
|
31
|
+
MEDIUM = "medium"
|
|
32
|
+
HIGH = "high"
|
|
33
|
+
CRITICAL = "critical"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ConflictType(StrEnum):
|
|
37
|
+
"""Conflict type classification (per schema)."""
|
|
38
|
+
|
|
39
|
+
NAMESPACE_OVERLAP = "namespace_overlap"
|
|
40
|
+
SEMANTIC_CONFLICT = "semantic_conflict"
|
|
41
|
+
RESOURCE_CONTENTION = "resource_contention"
|
|
42
|
+
VERSION_MISMATCH = "version_mismatch"
|
|
43
|
+
SECURITY_POLICY = "security_policy"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DagEdgeType(StrEnum):
|
|
47
|
+
"""Edge type classification (per schema)."""
|
|
48
|
+
|
|
49
|
+
DEPENDS_ON = "depends_on"
|
|
50
|
+
ENHANCES = "enhances"
|
|
51
|
+
CONFLICTS_WITH = "conflicts_with"
|
|
52
|
+
REPLACES = "replaces"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Domain(StrEnum):
|
|
56
|
+
"""Task domain classification (per schema)."""
|
|
57
|
+
|
|
58
|
+
CODE_REFACTOR = "code_refactor"
|
|
59
|
+
SECURITY_FIX = "security_fix"
|
|
60
|
+
CODE_ANALYSIS = "code_analysis"
|
|
61
|
+
ARCHITECTURE_MIGRATION = "architecture_migration"
|
|
62
|
+
DOCUMENTATION = "documentation"
|
|
63
|
+
DEPENDENCY_UPGRADE = "dependency_upgrade"
|
|
64
|
+
TESTING = "testing"
|
|
65
|
+
DATA_MIGRATION = "data_migration"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class DagEdge(BaseModel):
|
|
69
|
+
"""Directed edge in the skill dependency graph."""
|
|
70
|
+
|
|
71
|
+
source: str = Field(description="Upstream skill ID (schema: 'from')")
|
|
72
|
+
target: str = Field(description="Downstream skill ID (schema: 'to')")
|
|
73
|
+
weight: float = Field(default=1.0, description="Edge weight (0-1)")
|
|
74
|
+
type: DagEdgeType = Field(default=DagEdgeType.DEPENDS_ON, description="Edge type")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ResolvedSkill(BaseModel):
|
|
78
|
+
"""A single resolved skill with metadata."""
|
|
79
|
+
|
|
80
|
+
skill_id: str
|
|
81
|
+
name: str = ""
|
|
82
|
+
version: str = Field(default="1.0.0", description="Semantic version")
|
|
83
|
+
dimension: str = ""
|
|
84
|
+
domain: str = Field(default="", description="Task domain")
|
|
85
|
+
weight: float = 0.0
|
|
86
|
+
health_score: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
87
|
+
trust_level: int = Field(default=3, ge=0, le=3)
|
|
88
|
+
dependencies: list[str] = Field(default_factory=list)
|
|
89
|
+
estimated_tokens: int = Field(default=0, description="Estimated token usage")
|
|
90
|
+
provides: list[str] = Field(default_factory=list, description="Capability tags this skill provides")
|
|
91
|
+
conflict: Optional[ConflictSeverity] = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Conflict(BaseModel):
|
|
95
|
+
"""Detected conflict between skills."""
|
|
96
|
+
|
|
97
|
+
skill_a: str
|
|
98
|
+
skill_b: str
|
|
99
|
+
jaccard_score: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
100
|
+
severity: ConflictSeverity
|
|
101
|
+
conflict_type: ConflictType = Field(default=ConflictType.NAMESPACE_OVERLAP)
|
|
102
|
+
overlapping_namespaces: list[str] = Field(default_factory=list)
|
|
103
|
+
recommendation: str = Field(default="", description="Recommended resolution")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class SkillResolveRequest(BaseModel):
|
|
107
|
+
"""Request to resolve a skill chain.
|
|
108
|
+
|
|
109
|
+
Aligned with contracts/schemas/skill_resolve_request.v1.json:
|
|
110
|
+
- trace_id: W3C TraceContext (32 hex chars)
|
|
111
|
+
- task_description: Natural language task description
|
|
112
|
+
- domain: Task domain classification
|
|
113
|
+
- plan_id: Associated plan ID for audit
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
skill_ids: list[str] = Field(min_length=1, description="Root skill IDs to resolve (schema: available_skills)")
|
|
117
|
+
task_description: str = Field(default="", description="Natural language task description")
|
|
118
|
+
domain: Optional[Domain] = Field(default=None, description="Task domain classification")
|
|
119
|
+
trace_id: str = Field(default="", description="W3C TraceContext trace_id (32 hex chars)")
|
|
120
|
+
plan_id: str = Field(default="", description="Associated plan ID for audit tracing")
|
|
121
|
+
strategy: ResolveStrategy = ResolveStrategy.BEST_EFFORT
|
|
122
|
+
max_skills: int = Field(default=50, ge=1, le=200)
|
|
123
|
+
exclude_skills: list[str] = Field(default_factory=list)
|
|
124
|
+
require_independent: bool = False
|
|
125
|
+
min_health_score: float = Field(default=0.6, ge=0.0, le=1.0)
|
|
126
|
+
context: str = Field(default="", description="Resolution context for telemetry")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class SkillResolveResponse(BaseModel):
|
|
130
|
+
"""Response from skill resolution.
|
|
131
|
+
|
|
132
|
+
Aligned with contracts/schemas/skill_resolve_request.v1.json:
|
|
133
|
+
- status: three-state (resolved/partial/unresolved)
|
|
134
|
+
- health_scores: per-skill health score mapping
|
|
135
|
+
- feasibility_score: composite feasibility rating
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
resolved: list[ResolvedSkill] = Field(default_factory=list)
|
|
139
|
+
conflicts: list[Conflict] = Field(default_factory=list)
|
|
140
|
+
excluded: list[str] = Field(default_factory=list, description="Skills excluded by filters")
|
|
141
|
+
dag_edges: list[DagEdge] = Field(default_factory=list)
|
|
142
|
+
total_skills: int = 0
|
|
143
|
+
resolution_time_ms: float = 0.0
|
|
144
|
+
from_cache: bool = False
|
|
145
|
+
degraded: bool = False
|
|
146
|
+
error: Optional[str] = None
|
|
147
|
+
# Schema-aligned fields
|
|
148
|
+
status: ResolveStatus = Field(default=ResolveStatus.RESOLVED, description="Three-state resolution status")
|
|
149
|
+
health_scores: dict[str, float] = Field(
|
|
150
|
+
default_factory=dict, description="Per-skill health scores {skill_id: score}"
|
|
151
|
+
)
|
|
152
|
+
feasibility_score: float = Field(
|
|
153
|
+
default=1.0, ge=0.0, le=1.0, description="Composite feasibility = f(health_scores, conflicts)"
|
|
154
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""RateLimiter — sliding window rate limiter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from collections import deque
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RateLimiter:
|
|
10
|
+
"""Sliding window rate limiter.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
limiter = RateLimiter(max_requests=100, window_seconds=1.0)
|
|
14
|
+
if limiter.allow():
|
|
15
|
+
process_request()
|
|
16
|
+
else:
|
|
17
|
+
reject_with_429()
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, max_requests: int = 100, window_seconds: float = 1.0) -> None:
|
|
21
|
+
self.max_requests = max_requests
|
|
22
|
+
self.window_seconds = window_seconds
|
|
23
|
+
self._timestamps: deque[float] = deque()
|
|
24
|
+
|
|
25
|
+
def allow(self) -> bool:
|
|
26
|
+
"""Check if a request is allowed under the rate limit."""
|
|
27
|
+
now = time.monotonic()
|
|
28
|
+
cutoff = now - self.window_seconds
|
|
29
|
+
# Remove expired timestamps
|
|
30
|
+
while self._timestamps and self._timestamps[0] <= cutoff:
|
|
31
|
+
self._timestamps.popleft()
|
|
32
|
+
if len(self._timestamps) < self.max_requests:
|
|
33
|
+
self._timestamps.append(now)
|
|
34
|
+
return True
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def current_count(self) -> int:
|
|
39
|
+
"""Number of requests in the current window."""
|
|
40
|
+
now = time.monotonic()
|
|
41
|
+
cutoff = now - self.window_seconds
|
|
42
|
+
while self._timestamps and self._timestamps[0] <= cutoff:
|
|
43
|
+
self._timestamps.popleft()
|
|
44
|
+
return len(self._timestamps)
|
|
45
|
+
|
|
46
|
+
def reset(self) -> None:
|
|
47
|
+
"""Reset the rate limiter."""
|
|
48
|
+
self._timestamps.clear()
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""SkillGraph — DAG construction, topological sort, cycle detection, PPR ranking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
import scipy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CycleDetected(Exception):
|
|
13
|
+
"""Raised when a cycle is detected in the skill dependency graph."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, cycle_nodes: list[str]):
|
|
16
|
+
self.cycle_nodes = cycle_nodes
|
|
17
|
+
super().__init__(f"Cycle detected: {' → '.join(cycle_nodes)}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SkillGraph:
|
|
21
|
+
"""Directed Acyclic Graph for skill dependencies.
|
|
22
|
+
|
|
23
|
+
Supports topological sort, cycle detection, and PPR-based importance ranking.
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
graph = SkillGraph()
|
|
27
|
+
graph.add_edge("S01", "S05a", weight=0.8)
|
|
28
|
+
order = graph.topological_sort() # ["S01", "S05a"]
|
|
29
|
+
scores = graph.pagerank(["S01"]) # PPR importance scores
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
self._adj: dict[str, list[tuple[str, float]]] = defaultdict(list)
|
|
34
|
+
self._nodes: set[str] = set()
|
|
35
|
+
self._in_degree: dict[str, int] = defaultdict(int)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def nodes(self) -> set[str]:
|
|
39
|
+
return set(self._nodes)
|
|
40
|
+
|
|
41
|
+
def add_node(self, node: str) -> None:
|
|
42
|
+
"""Add a node without edges."""
|
|
43
|
+
self._nodes.add(node)
|
|
44
|
+
if node not in self._in_degree:
|
|
45
|
+
self._in_degree[node] = 0
|
|
46
|
+
|
|
47
|
+
def add_edge(self, source: str, target: str, weight: float = 1.0) -> None:
|
|
48
|
+
"""Add a directed edge source → target."""
|
|
49
|
+
self.add_node(source)
|
|
50
|
+
self.add_node(target)
|
|
51
|
+
self._adj[source].append((target, weight))
|
|
52
|
+
self._in_degree[target] += 1
|
|
53
|
+
|
|
54
|
+
def get_edges(self) -> list[tuple[str, str, float]]:
|
|
55
|
+
"""Return all edges as (source, target, weight) tuples."""
|
|
56
|
+
edges = []
|
|
57
|
+
for src, targets in self._adj.items():
|
|
58
|
+
for tgt, w in targets:
|
|
59
|
+
edges.append((src, tgt, w))
|
|
60
|
+
return edges
|
|
61
|
+
|
|
62
|
+
def get_dependencies(self, node: str) -> list[str]:
|
|
63
|
+
"""Get nodes that `node` depends on (upstream)."""
|
|
64
|
+
deps = []
|
|
65
|
+
for src, targets in self._adj.items():
|
|
66
|
+
for tgt, _ in targets:
|
|
67
|
+
if tgt == node:
|
|
68
|
+
deps.append(src)
|
|
69
|
+
return deps
|
|
70
|
+
|
|
71
|
+
def get_dependents(self, node: str) -> list[str]:
|
|
72
|
+
"""Get nodes that depend on `node` (downstream)."""
|
|
73
|
+
return [tgt for tgt, _ in self._adj.get(node, [])]
|
|
74
|
+
|
|
75
|
+
def topological_sort(self) -> list[str]:
|
|
76
|
+
"""Kahn's algorithm for topological sort. Raises CycleDetected if cycle exists."""
|
|
77
|
+
in_deg = dict(self._in_degree)
|
|
78
|
+
queue = [n for n in self._nodes if in_deg.get(n, 0) == 0]
|
|
79
|
+
result: list[str] = []
|
|
80
|
+
|
|
81
|
+
while queue:
|
|
82
|
+
node = queue.pop(0)
|
|
83
|
+
result.append(node)
|
|
84
|
+
for tgt, _ in self._adj.get(node, []):
|
|
85
|
+
in_deg[tgt] -= 1
|
|
86
|
+
if in_deg[tgt] == 0:
|
|
87
|
+
queue.append(tgt)
|
|
88
|
+
|
|
89
|
+
if len(result) != len(self._nodes):
|
|
90
|
+
# Find cycle nodes
|
|
91
|
+
remaining = self._nodes - set(result)
|
|
92
|
+
raise CycleDetected(list(remaining))
|
|
93
|
+
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
def has_cycle(self) -> bool:
|
|
97
|
+
"""Check if the graph contains a cycle."""
|
|
98
|
+
try:
|
|
99
|
+
self.topological_sort()
|
|
100
|
+
return False
|
|
101
|
+
except CycleDetected:
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
def subgraph(self, nodes: set[str]) -> SkillGraph:
|
|
105
|
+
"""Extract a subgraph containing only the specified nodes."""
|
|
106
|
+
g = SkillGraph()
|
|
107
|
+
for node in nodes:
|
|
108
|
+
if node in self._nodes:
|
|
109
|
+
g.add_node(node)
|
|
110
|
+
for src, targets in self._adj.items():
|
|
111
|
+
if src in nodes:
|
|
112
|
+
for tgt, w in targets:
|
|
113
|
+
if tgt in nodes:
|
|
114
|
+
g.add_edge(src, tgt, w)
|
|
115
|
+
return g
|
|
116
|
+
|
|
117
|
+
def to_sparse_matrix(self) -> tuple["scipy.sparse.csr_matrix", dict[str, int]]:
|
|
118
|
+
"""Convert graph to scipy sparse adjacency matrix.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Tuple of (adjacency_matrix, node_to_index_mapping)
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
ImportError if scipy/numpy not available
|
|
125
|
+
"""
|
|
126
|
+
import numpy as np
|
|
127
|
+
from scipy import sparse as sp
|
|
128
|
+
|
|
129
|
+
node_list = sorted(self._nodes)
|
|
130
|
+
node_to_idx = {n: i for i, n in enumerate(node_list)}
|
|
131
|
+
n = len(node_list)
|
|
132
|
+
|
|
133
|
+
rows, cols, weights = [], [], []
|
|
134
|
+
for src, targets in self._adj.items():
|
|
135
|
+
for tgt, w in targets:
|
|
136
|
+
rows.append(node_to_idx[src])
|
|
137
|
+
cols.append(node_to_idx[tgt])
|
|
138
|
+
weights.append(w)
|
|
139
|
+
|
|
140
|
+
if rows:
|
|
141
|
+
adj = sp.csr_matrix(
|
|
142
|
+
(np.array(weights, dtype=np.float64), (rows, cols)),
|
|
143
|
+
shape=(n, n),
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
adj = sp.csr_matrix((n, n))
|
|
147
|
+
|
|
148
|
+
return adj, node_to_idx
|
|
149
|
+
|
|
150
|
+
def pagerank(
|
|
151
|
+
self,
|
|
152
|
+
seeds: list[str],
|
|
153
|
+
alpha: float = 0.85,
|
|
154
|
+
method: str = "auto",
|
|
155
|
+
) -> dict[str, float]:
|
|
156
|
+
"""Compute Personalized PageRank scores for seed nodes.
|
|
157
|
+
|
|
158
|
+
Uses the 3-layer PPR implementation from skillpool.graph.ppr.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
seeds: Seed skill IDs for personalization
|
|
162
|
+
alpha: Damping factor (default 0.85)
|
|
163
|
+
method: "python", "csr", "sknetwork", or "auto"
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Dict mapping skill_id → PPR score
|
|
167
|
+
"""
|
|
168
|
+
from skillpool.graph.ppr import personalized_pagerank
|
|
169
|
+
|
|
170
|
+
adj, node_to_idx = self.to_sparse_matrix()
|
|
171
|
+
|
|
172
|
+
# Convert seed IDs to indices
|
|
173
|
+
seed_indices = [node_to_idx[s] for s in seeds if s in node_to_idx]
|
|
174
|
+
if not seed_indices:
|
|
175
|
+
# No valid seeds — return uniform scores
|
|
176
|
+
n = len(self._nodes)
|
|
177
|
+
return {node: 1.0 / n for node in self._nodes} if n > 0 else {}
|
|
178
|
+
|
|
179
|
+
scores_vec = personalized_pagerank(adj, seed_indices, alpha=alpha, method=method)
|
|
180
|
+
|
|
181
|
+
# Map back to skill IDs
|
|
182
|
+
idx_to_node = {i: n for n, i in node_to_idx.items()}
|
|
183
|
+
return {idx_to_node[i]: float(scores_vec[i]) for i in range(len(scores_vec))}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""ReviewManager — orchestrates the multi-dimension review pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
from skillpool.review.models import (
|
|
10
|
+
CheckpointLevel,
|
|
11
|
+
ReviewStatus,
|
|
12
|
+
ReviewTriggerRequest,
|
|
13
|
+
ReviewTriggerResponse,
|
|
14
|
+
SuspectSkill,
|
|
15
|
+
UpgradeRecommendation,
|
|
16
|
+
)
|
|
17
|
+
from skillpool.review.veto_evaluator import VetoEvaluator
|
|
18
|
+
from skillpool.review.checkpoint_runner import CheckpointRunner
|
|
19
|
+
from skillpool.review.suspect_marker import SuspectMarker
|
|
20
|
+
from skillpool.review.async_queue import AsyncReviewQueue
|
|
21
|
+
from skillpool.telemetry import TelemetryBridge
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ReviewManager:
|
|
27
|
+
"""Orchestrates the full review pipeline.
|
|
28
|
+
|
|
29
|
+
Pipeline: validate request → check cooldown → run checkpoint →
|
|
30
|
+
evaluate veto → mark suspects → feed evolver → emit telemetry
|
|
31
|
+
|
|
32
|
+
When an EvolverLayer is provided, veto results feed into the defect
|
|
33
|
+
accumulation system, and evolution proposals are generated automatically.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
telemetry: TelemetryBridge | None = None,
|
|
39
|
+
checkpoint_runner: CheckpointRunner | None = None,
|
|
40
|
+
veto_evaluator: VetoEvaluator | None = None,
|
|
41
|
+
suspect_marker: SuspectMarker | None = None,
|
|
42
|
+
queue: AsyncReviewQueue | None = None,
|
|
43
|
+
evolver=None,
|
|
44
|
+
) -> None:
|
|
45
|
+
self._telemetry = telemetry or TelemetryBridge()
|
|
46
|
+
self._runner = checkpoint_runner or CheckpointRunner()
|
|
47
|
+
self._evaluator = veto_evaluator or VetoEvaluator()
|
|
48
|
+
self._marker = suspect_marker or SuspectMarker()
|
|
49
|
+
self._queue = queue or AsyncReviewQueue()
|
|
50
|
+
self._evolver = evolver
|
|
51
|
+
|
|
52
|
+
def trigger(self, request: ReviewTriggerRequest) -> ReviewTriggerResponse:
|
|
53
|
+
"""Execute the full review pipeline for a trigger request."""
|
|
54
|
+
start_ms = time.time() * 1000
|
|
55
|
+
|
|
56
|
+
# Step 1: Check cooldown via queue (returns the review_id)
|
|
57
|
+
try:
|
|
58
|
+
review_id = self._queue.submit(request)
|
|
59
|
+
except ValueError:
|
|
60
|
+
# Cooldown violation — return queued status
|
|
61
|
+
return ReviewTriggerResponse(
|
|
62
|
+
review_id=uuid.uuid4().hex[:16],
|
|
63
|
+
status=ReviewStatus.QUEUED,
|
|
64
|
+
checkpoint=request.checkpoint,
|
|
65
|
+
scores={},
|
|
66
|
+
veto_triggered=False,
|
|
67
|
+
veto_details=[],
|
|
68
|
+
suspect_skills=[],
|
|
69
|
+
recommendation=UpgradeRecommendation.NONE,
|
|
70
|
+
duration_ms=round(time.time() * 1000 - start_ms, 2),
|
|
71
|
+
)
|
|
72
|
+
except RuntimeError:
|
|
73
|
+
# Max concurrent reached — return queued
|
|
74
|
+
return ReviewTriggerResponse(
|
|
75
|
+
review_id=uuid.uuid4().hex[:16],
|
|
76
|
+
status=ReviewStatus.QUEUED,
|
|
77
|
+
checkpoint=request.checkpoint,
|
|
78
|
+
scores={},
|
|
79
|
+
veto_triggered=False,
|
|
80
|
+
veto_details=[],
|
|
81
|
+
suspect_skills=[],
|
|
82
|
+
recommendation=UpgradeRecommendation.NONE,
|
|
83
|
+
duration_ms=round(time.time() * 1000 - start_ms, 2),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Mark as processing
|
|
87
|
+
self._queue.set_status(review_id, ReviewStatus.PROCESSING)
|
|
88
|
+
|
|
89
|
+
# Step 2: Run checkpoint
|
|
90
|
+
try:
|
|
91
|
+
scores = self._runner.run_checkpoint(
|
|
92
|
+
level=request.checkpoint,
|
|
93
|
+
skills=request.affected_skills,
|
|
94
|
+
)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.error("Checkpoint run failed for review %s: %s", review_id, e)
|
|
97
|
+
self._queue.set_status(review_id, ReviewStatus.FAILED)
|
|
98
|
+
return ReviewTriggerResponse(
|
|
99
|
+
review_id=review_id,
|
|
100
|
+
status=ReviewStatus.FAILED,
|
|
101
|
+
checkpoint=request.checkpoint,
|
|
102
|
+
scores={},
|
|
103
|
+
veto_triggered=False,
|
|
104
|
+
veto_details=[],
|
|
105
|
+
suspect_skills=[],
|
|
106
|
+
recommendation=UpgradeRecommendation.NONE,
|
|
107
|
+
duration_ms=round(time.time() * 1000 - start_ms, 2),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Step 3: Evaluate veto (only for L2, L3, L4)
|
|
111
|
+
veto_details = []
|
|
112
|
+
veto_triggered = False
|
|
113
|
+
if request.checkpoint in (CheckpointLevel.L2, CheckpointLevel.L3, CheckpointLevel.L4):
|
|
114
|
+
veto_details, veto_triggered = self._evaluator.evaluate(scores)
|
|
115
|
+
|
|
116
|
+
# Step 4: Mark suspect skills
|
|
117
|
+
suspect_skills: list[SuspectSkill] = []
|
|
118
|
+
for detail in veto_details:
|
|
119
|
+
if detail.blocks:
|
|
120
|
+
for skill_id in request.affected_skills:
|
|
121
|
+
self._marker.mark(
|
|
122
|
+
skill_id=skill_id,
|
|
123
|
+
reason=f"Veto {detail.rule.value}: {detail.recommendation}",
|
|
124
|
+
suspected_dimension=detail.dimension,
|
|
125
|
+
)
|
|
126
|
+
suspect_skills = self._marker.list_suspects()
|
|
127
|
+
|
|
128
|
+
# Step 5: Feed evolver (if available) and determine recommendation
|
|
129
|
+
recommendation = self._determine_recommendation(veto_details)
|
|
130
|
+
self._feed_evolver(veto_details, request, recommendation)
|
|
131
|
+
|
|
132
|
+
# Step 6: Determine final status
|
|
133
|
+
if veto_triggered:
|
|
134
|
+
status = ReviewStatus.PARTIAL
|
|
135
|
+
else:
|
|
136
|
+
status = ReviewStatus.COMPLETED
|
|
137
|
+
|
|
138
|
+
self._queue.set_status(review_id, status)
|
|
139
|
+
|
|
140
|
+
# Step 7: Emit telemetry (propagate trace_id from request)
|
|
141
|
+
for skill_id in request.affected_skills:
|
|
142
|
+
self._telemetry.emit(
|
|
143
|
+
event_type="review_completed",
|
|
144
|
+
skill_id=skill_id,
|
|
145
|
+
payload={
|
|
146
|
+
"review_id": review_id,
|
|
147
|
+
"checkpoint": request.checkpoint.value,
|
|
148
|
+
"trigger": request.trigger.value,
|
|
149
|
+
"veto_triggered": veto_triggered,
|
|
150
|
+
"recommendation": recommendation.value,
|
|
151
|
+
"scores": scores,
|
|
152
|
+
},
|
|
153
|
+
trace_id=request.trace_id,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
duration_ms = round(time.time() * 1000 - start_ms, 2)
|
|
157
|
+
return ReviewTriggerResponse(
|
|
158
|
+
review_id=review_id,
|
|
159
|
+
status=status,
|
|
160
|
+
checkpoint=request.checkpoint,
|
|
161
|
+
scores=scores,
|
|
162
|
+
veto_triggered=veto_triggered,
|
|
163
|
+
veto_details=veto_details,
|
|
164
|
+
suspect_skills=suspect_skills,
|
|
165
|
+
recommendation=recommendation,
|
|
166
|
+
duration_ms=duration_ms,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def _determine_recommendation(veto_details: list) -> UpgradeRecommendation:
|
|
171
|
+
"""Determine upgrade recommendation from veto results.
|
|
172
|
+
|
|
173
|
+
- MAJOR: any P0-level veto (V1 security block)
|
|
174
|
+
- MINOR: any P1-level veto (V2, V3, V4)
|
|
175
|
+
- PATCH: only V5 risk notice or V6 explanation
|
|
176
|
+
- NONE: no vetoes at all
|
|
177
|
+
"""
|
|
178
|
+
if not veto_details:
|
|
179
|
+
return UpgradeRecommendation.NONE
|
|
180
|
+
|
|
181
|
+
blocking = [d for d in veto_details if d.blocks]
|
|
182
|
+
if not blocking:
|
|
183
|
+
# Only non-blocking (V5) → PATCH
|
|
184
|
+
return UpgradeRecommendation.PATCH
|
|
185
|
+
|
|
186
|
+
# Check for MAJOR triggers (security = V1)
|
|
187
|
+
for d in blocking:
|
|
188
|
+
if d.rule.value in ("V1",):
|
|
189
|
+
return UpgradeRecommendation.MAJOR
|
|
190
|
+
|
|
191
|
+
# Check for MINOR triggers (V2, V3, V4)
|
|
192
|
+
for d in blocking:
|
|
193
|
+
if d.rule.value in ("V2", "V3", "V4"):
|
|
194
|
+
return UpgradeRecommendation.MINOR
|
|
195
|
+
|
|
196
|
+
# V6 only → PATCH
|
|
197
|
+
return UpgradeRecommendation.PATCH
|
|
198
|
+
|
|
199
|
+
def _feed_evolver(self, veto_details, request, recommendation) -> None:
|
|
200
|
+
"""Feed veto results into EvolverLayer for defect accumulation.
|
|
201
|
+
|
|
202
|
+
When evolver is available, each blocking veto is recorded as a defect.
|
|
203
|
+
Non-blocking vetoes are recorded as MINOR defects for tracking.
|
|
204
|
+
Evolution proposals are created when defect thresholds are reached.
|
|
205
|
+
"""
|
|
206
|
+
if self._evolver is None:
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
from skillpool.evolver import DefectSeverity
|
|
210
|
+
|
|
211
|
+
for detail in veto_details:
|
|
212
|
+
# Map veto severity to defect severity
|
|
213
|
+
if detail.blocks:
|
|
214
|
+
if detail.rule.value == "V1":
|
|
215
|
+
severity = DefectSeverity.CRITICAL
|
|
216
|
+
else:
|
|
217
|
+
severity = DefectSeverity.MAJOR
|
|
218
|
+
else:
|
|
219
|
+
severity = DefectSeverity.MINOR
|
|
220
|
+
|
|
221
|
+
# Record one defect per affected skill per veto
|
|
222
|
+
for skill_id in request.affected_skills:
|
|
223
|
+
self._evolver.record_defect(
|
|
224
|
+
skill_id=skill_id,
|
|
225
|
+
version="current",
|
|
226
|
+
severity=severity,
|
|
227
|
+
description=f"Veto {detail.rule.value}: {detail.recommendation}",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# If recommendation is MAJOR/MINOR, create an evolution proposal
|
|
231
|
+
if recommendation in (UpgradeRecommendation.MAJOR, UpgradeRecommendation.MINOR):
|
|
232
|
+
context = {
|
|
233
|
+
"trigger": "review_veto",
|
|
234
|
+
"recommendation": recommendation.value,
|
|
235
|
+
"checkpoint": request.checkpoint.value,
|
|
236
|
+
"affected_skills": request.affected_skills,
|
|
237
|
+
"veto_count": len(veto_details),
|
|
238
|
+
}
|
|
239
|
+
self._evolver.create_proposal(
|
|
240
|
+
context=context,
|
|
241
|
+
risk="high" if recommendation == UpgradeRecommendation.MAJOR else "medium",
|
|
242
|
+
)
|