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