claude-mpm 4.4.0__py3-none-any.whl → 4.4.4__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/WORKFLOW.md +2 -14
- claude_mpm/agents/agent_loader.py +3 -2
- claude_mpm/agents/agent_loader_integration.py +2 -1
- claude_mpm/agents/async_agent_loader.py +2 -2
- claude_mpm/agents/base_agent_loader.py +2 -2
- claude_mpm/agents/frontmatter_validator.py +1 -0
- claude_mpm/agents/system_agent_config.py +2 -1
- claude_mpm/cli/commands/configure.py +2 -29
- claude_mpm/cli/commands/doctor.py +44 -5
- claude_mpm/cli/commands/mpm_init.py +117 -63
- claude_mpm/cli/parsers/configure_parser.py +6 -15
- claude_mpm/cli/startup_logging.py +1 -3
- claude_mpm/config/agent_config.py +1 -1
- claude_mpm/config/paths.py +2 -1
- claude_mpm/core/agent_name_normalizer.py +1 -0
- claude_mpm/core/config.py +2 -1
- claude_mpm/core/config_aliases.py +2 -1
- claude_mpm/core/file_utils.py +0 -1
- claude_mpm/core/framework/__init__.py +38 -0
- claude_mpm/core/framework/formatters/__init__.py +11 -0
- claude_mpm/core/framework/formatters/capability_generator.py +367 -0
- claude_mpm/core/framework/formatters/content_formatter.py +288 -0
- claude_mpm/core/framework/formatters/context_generator.py +184 -0
- claude_mpm/core/framework/loaders/__init__.py +13 -0
- claude_mpm/core/framework/loaders/agent_loader.py +206 -0
- claude_mpm/core/framework/loaders/file_loader.py +223 -0
- claude_mpm/core/framework/loaders/instruction_loader.py +161 -0
- claude_mpm/core/framework/loaders/packaged_loader.py +232 -0
- claude_mpm/core/framework/processors/__init__.py +11 -0
- claude_mpm/core/framework/processors/memory_processor.py +230 -0
- claude_mpm/core/framework/processors/metadata_processor.py +146 -0
- claude_mpm/core/framework/processors/template_processor.py +244 -0
- claude_mpm/core/framework_loader.py +298 -1795
- claude_mpm/core/log_manager.py +2 -1
- claude_mpm/core/tool_access_control.py +1 -0
- claude_mpm/core/unified_agent_registry.py +2 -1
- claude_mpm/core/unified_paths.py +1 -0
- claude_mpm/experimental/cli_enhancements.py +1 -0
- claude_mpm/hooks/__init__.py +9 -1
- claude_mpm/hooks/base_hook.py +1 -0
- claude_mpm/hooks/instruction_reinforcement.py +1 -0
- claude_mpm/hooks/kuzu_memory_hook.py +359 -0
- claude_mpm/hooks/validation_hooks.py +1 -1
- claude_mpm/scripts/mpm_doctor.py +1 -0
- claude_mpm/services/agents/loading/agent_profile_loader.py +1 -1
- claude_mpm/services/agents/loading/base_agent_manager.py +1 -1
- claude_mpm/services/agents/loading/framework_agent_loader.py +1 -1
- claude_mpm/services/agents/management/agent_capabilities_generator.py +1 -0
- claude_mpm/services/agents/management/agent_management_service.py +1 -1
- claude_mpm/services/agents/memory/memory_categorization_service.py +0 -1
- claude_mpm/services/agents/memory/memory_file_service.py +6 -2
- claude_mpm/services/agents/memory/memory_format_service.py +0 -1
- claude_mpm/services/agents/registry/deployed_agent_discovery.py +1 -1
- claude_mpm/services/async_session_logger.py +1 -1
- claude_mpm/services/claude_session_logger.py +1 -0
- claude_mpm/services/core/path_resolver.py +2 -0
- claude_mpm/services/diagnostics/checks/__init__.py +2 -0
- claude_mpm/services/diagnostics/checks/installation_check.py +126 -25
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +399 -0
- claude_mpm/services/diagnostics/diagnostic_runner.py +4 -0
- claude_mpm/services/diagnostics/doctor_reporter.py +259 -32
- claude_mpm/services/event_bus/direct_relay.py +2 -1
- claude_mpm/services/event_bus/event_bus.py +1 -0
- claude_mpm/services/event_bus/relay.py +3 -2
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +1 -1
- claude_mpm/services/infrastructure/daemon_manager.py +1 -1
- claude_mpm/services/mcp_config_manager.py +67 -4
- claude_mpm/services/mcp_gateway/core/process_pool.py +320 -0
- claude_mpm/services/mcp_gateway/core/startup_verification.py +2 -2
- claude_mpm/services/mcp_gateway/main.py +3 -13
- claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -10
- claude_mpm/services/mcp_gateway/tools/__init__.py +14 -2
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +38 -6
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +527 -0
- claude_mpm/services/memory/cache/simple_cache.py +1 -1
- claude_mpm/services/project/archive_manager.py +159 -96
- claude_mpm/services/project/documentation_manager.py +64 -45
- claude_mpm/services/project/enhanced_analyzer.py +132 -89
- claude_mpm/services/project/project_organizer.py +225 -131
- claude_mpm/services/response_tracker.py +1 -1
- claude_mpm/services/shared/__init__.py +2 -1
- claude_mpm/services/shared/service_factory.py +8 -5
- claude_mpm/services/socketio/server/eventbus_integration.py +1 -1
- claude_mpm/services/unified/__init__.py +1 -1
- claude_mpm/services/unified/analyzer_strategies/__init__.py +3 -3
- claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +97 -53
- claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +81 -40
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +277 -178
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +196 -112
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +83 -49
- claude_mpm/services/unified/config_strategies/__init__.py +175 -0
- claude_mpm/services/unified/config_strategies/config_schema.py +735 -0
- claude_mpm/services/unified/config_strategies/context_strategy.py +750 -0
- claude_mpm/services/unified/config_strategies/error_handling_strategy.py +1009 -0
- claude_mpm/services/unified/config_strategies/file_loader_strategy.py +879 -0
- claude_mpm/services/unified/config_strategies/unified_config_service.py +814 -0
- claude_mpm/services/unified/config_strategies/validation_strategy.py +1144 -0
- claude_mpm/services/unified/deployment_strategies/__init__.py +7 -7
- claude_mpm/services/unified/deployment_strategies/base.py +24 -28
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +168 -88
- claude_mpm/services/unified/deployment_strategies/local.py +49 -34
- claude_mpm/services/unified/deployment_strategies/utils.py +39 -43
- claude_mpm/services/unified/deployment_strategies/vercel.py +30 -24
- claude_mpm/services/unified/interfaces.py +0 -26
- claude_mpm/services/unified/migration.py +17 -40
- claude_mpm/services/unified/strategies.py +9 -26
- claude_mpm/services/unified/unified_analyzer.py +48 -44
- claude_mpm/services/unified/unified_config.py +21 -19
- claude_mpm/services/unified/unified_deployment.py +21 -26
- claude_mpm/storage/state_storage.py +1 -0
- claude_mpm/utils/agent_dependency_loader.py +18 -6
- claude_mpm/utils/common.py +14 -12
- claude_mpm/utils/database_connector.py +15 -12
- claude_mpm/utils/error_handler.py +1 -0
- claude_mpm/utils/log_cleanup.py +1 -0
- claude_mpm/utils/path_operations.py +1 -0
- claude_mpm/utils/session_logging.py +1 -1
- claude_mpm/utils/subprocess_utils.py +1 -0
- claude_mpm/validation/agent_validator.py +1 -1
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/METADATA +23 -17
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/RECORD +126 -105
- claude_mpm/cli/commands/configure_tui.py +0 -1927
- claude_mpm/services/mcp_gateway/tools/ticket_tools.py +0 -645
- claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +0 -602
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/WHEEL +0 -0
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/top_level.txt +0 -0
@@ -22,9 +22,9 @@ from datetime import datetime, timezone
|
|
22
22
|
from typing import Any, Dict, Optional
|
23
23
|
|
24
24
|
from claude_mpm.core.config import Config
|
25
|
+
from claude_mpm.core.logging_utils import get_logger
|
25
26
|
from claude_mpm.core.shared.config_loader import ConfigLoader
|
26
27
|
|
27
|
-
from claude_mpm.core.logging_utils import get_logger
|
28
28
|
logger = get_logger(__name__)
|
29
29
|
|
30
30
|
|
@@ -9,7 +9,7 @@ from .async_service_base import AsyncServiceBase
|
|
9
9
|
from .config_service_base import ConfigServiceBase
|
10
10
|
from .lifecycle_service_base import LifecycleServiceBase
|
11
11
|
from .manager_base import ManagerBase
|
12
|
-
from .service_factory import ServiceFactory
|
12
|
+
from .service_factory import ServiceFactory, get_service_factory
|
13
13
|
|
14
14
|
__all__ = [
|
15
15
|
"AsyncServiceBase",
|
@@ -17,4 +17,5 @@ __all__ = [
|
|
17
17
|
"LifecycleServiceBase",
|
18
18
|
"ManagerBase",
|
19
19
|
"ServiceFactory",
|
20
|
+
"get_service_factory",
|
20
21
|
]
|
@@ -287,20 +287,23 @@ class ServiceFactory:
|
|
287
287
|
self.logger.debug("Cleared all service registrations")
|
288
288
|
|
289
289
|
|
290
|
-
# Global factory instance
|
291
|
-
_global_factory =
|
290
|
+
# Global factory instance (lazy initialization)
|
291
|
+
_global_factory: Optional[ServiceFactory] = None
|
292
292
|
|
293
293
|
|
294
294
|
def get_service_factory() -> ServiceFactory:
|
295
|
-
"""Get global service factory instance."""
|
295
|
+
"""Get global service factory instance (created on first use)."""
|
296
|
+
global _global_factory
|
297
|
+
if _global_factory is None:
|
298
|
+
_global_factory = ServiceFactory()
|
296
299
|
return _global_factory
|
297
300
|
|
298
301
|
|
299
302
|
def create_service(service_class: Type[T], **kwargs) -> T:
|
300
303
|
"""Convenience function to create service using global factory."""
|
301
|
-
return
|
304
|
+
return get_service_factory().create_service(service_class, **kwargs)
|
302
305
|
|
303
306
|
|
304
307
|
def register_service(service_name: str, service_class: Type) -> None:
|
305
308
|
"""Convenience function to register service with global factory."""
|
306
|
-
|
309
|
+
get_service_factory().register_service(service_name, service_class)
|
@@ -10,11 +10,11 @@ WHY this integration module:
|
|
10
10
|
from datetime import datetime, timezone
|
11
11
|
from typing import Optional
|
12
12
|
|
13
|
+
from claude_mpm.core.logging_utils import get_logger
|
13
14
|
from claude_mpm.services.event_bus import EventBus
|
14
15
|
from claude_mpm.services.event_bus.config import get_config
|
15
16
|
from claude_mpm.services.event_bus.direct_relay import DirectSocketIORelay
|
16
17
|
|
17
|
-
from claude_mpm.core.logging_utils import get_logger
|
18
18
|
logger = get_logger(__name__)
|
19
19
|
|
20
20
|
|
@@ -29,9 +29,9 @@ from .structure_analyzer import StructureAnalyzerStrategy
|
|
29
29
|
__all__ = [
|
30
30
|
"CodeAnalyzerStrategy",
|
31
31
|
"DependencyAnalyzerStrategy",
|
32
|
-
"StructureAnalyzerStrategy",
|
33
|
-
"SecurityAnalyzerStrategy",
|
34
32
|
"PerformanceAnalyzerStrategy",
|
33
|
+
"SecurityAnalyzerStrategy",
|
34
|
+
"StructureAnalyzerStrategy",
|
35
35
|
]
|
36
36
|
|
37
37
|
# Strategy registry for automatic discovery
|
@@ -41,4 +41,4 @@ ANALYZER_STRATEGIES = {
|
|
41
41
|
"structure": StructureAnalyzerStrategy,
|
42
42
|
"security": SecurityAnalyzerStrategy,
|
43
43
|
"performance": PerformanceAnalyzerStrategy,
|
44
|
-
}
|
44
|
+
}
|
@@ -12,11 +12,16 @@ Created: 2025-01-26
|
|
12
12
|
import ast
|
13
13
|
import re
|
14
14
|
from pathlib import Path
|
15
|
-
from typing import Any, Dict, List, Optional
|
15
|
+
from typing import Any, Dict, List, Optional
|
16
16
|
|
17
17
|
from claude_mpm.core.logging_utils import get_logger
|
18
18
|
|
19
|
-
from ..strategies import
|
19
|
+
from ..strategies import (
|
20
|
+
AnalyzerStrategy,
|
21
|
+
StrategyContext,
|
22
|
+
StrategyMetadata,
|
23
|
+
StrategyPriority,
|
24
|
+
)
|
20
25
|
|
21
26
|
logger = get_logger(__name__)
|
22
27
|
|
@@ -109,7 +114,7 @@ class CodeAnalyzerStrategy(AnalyzerStrategy):
|
|
109
114
|
target_path = Path(target)
|
110
115
|
if target_path.is_file():
|
111
116
|
return self._analyze_file(target_path, options)
|
112
|
-
|
117
|
+
if target_path.is_dir():
|
113
118
|
return self._analyze_directory(target_path, options)
|
114
119
|
elif isinstance(target, ast.AST):
|
115
120
|
return self._analyze_ast(target, options)
|
@@ -143,7 +148,9 @@ class CodeAnalyzerStrategy(AnalyzerStrategy):
|
|
143
148
|
metrics.update(self._analyze_python_code(content, file_path))
|
144
149
|
|
145
150
|
# Calculate complexity metrics
|
146
|
-
metrics["complexity"] = self._calculate_complexity_metrics(
|
151
|
+
metrics["complexity"] = self._calculate_complexity_metrics(
|
152
|
+
content, language
|
153
|
+
)
|
147
154
|
|
148
155
|
# Detect code smells
|
149
156
|
metrics["code_smells"] = self._detect_code_smells(content, metrics)
|
@@ -166,7 +173,9 @@ class CodeAnalyzerStrategy(AnalyzerStrategy):
|
|
166
173
|
"error": str(e),
|
167
174
|
}
|
168
175
|
|
169
|
-
def _analyze_directory(
|
176
|
+
def _analyze_directory(
|
177
|
+
self, dir_path: Path, options: Dict[str, Any]
|
178
|
+
) -> Dict[str, Any]:
|
170
179
|
"""Analyze all code files in a directory."""
|
171
180
|
results = {
|
172
181
|
"status": "success",
|
@@ -198,7 +207,10 @@ class CodeAnalyzerStrategy(AnalyzerStrategy):
|
|
198
207
|
results["summary"] = {
|
199
208
|
"total_files": len(results["files"]),
|
200
209
|
"total_lines": total_metrics.get("lines_of_code", 0),
|
201
|
-
"average_complexity": total_metrics.get("complexity", {}).get(
|
210
|
+
"average_complexity": total_metrics.get("complexity", {}).get(
|
211
|
+
"cyclomatic", 0
|
212
|
+
)
|
213
|
+
/ max(len(results["files"]), 1),
|
202
214
|
"code_smells_count": sum(
|
203
215
|
len(f.get("metrics", {}).get("code_smells", []))
|
204
216
|
for f in results["files"]
|
@@ -219,7 +231,9 @@ class CodeAnalyzerStrategy(AnalyzerStrategy):
|
|
219
231
|
|
220
232
|
for node in ast.walk(tree):
|
221
233
|
if isinstance(node, ast.FunctionDef):
|
222
|
-
if any(
|
234
|
+
if any(
|
235
|
+
isinstance(parent, ast.ClassDef) for parent in ast.walk(tree)
|
236
|
+
):
|
223
237
|
methods.append(node.name)
|
224
238
|
else:
|
225
239
|
functions.append(node.name)
|
@@ -248,17 +262,27 @@ class CodeAnalyzerStrategy(AnalyzerStrategy):
|
|
248
262
|
|
249
263
|
# Analyze specific node types
|
250
264
|
if isinstance(node, ast.FunctionDef):
|
251
|
-
metrics.update(
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
265
|
+
metrics.update(
|
266
|
+
{
|
267
|
+
"name": node.name,
|
268
|
+
"parameters": len(node.args.args),
|
269
|
+
"lines": (
|
270
|
+
node.end_lineno - node.lineno + 1
|
271
|
+
if hasattr(node, "end_lineno")
|
272
|
+
else 0
|
273
|
+
),
|
274
|
+
}
|
275
|
+
)
|
256
276
|
elif isinstance(node, ast.ClassDef):
|
257
|
-
metrics.update(
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
277
|
+
metrics.update(
|
278
|
+
{
|
279
|
+
"name": node.name,
|
280
|
+
"methods": sum(
|
281
|
+
1 for n in node.body if isinstance(n, ast.FunctionDef)
|
282
|
+
),
|
283
|
+
"bases": len(node.bases),
|
284
|
+
}
|
285
|
+
)
|
262
286
|
|
263
287
|
return {
|
264
288
|
"status": "success",
|
@@ -266,7 +290,9 @@ class CodeAnalyzerStrategy(AnalyzerStrategy):
|
|
266
290
|
"metrics": metrics,
|
267
291
|
}
|
268
292
|
|
269
|
-
def _calculate_complexity_metrics(
|
293
|
+
def _calculate_complexity_metrics(
|
294
|
+
self, content: str, language: str
|
295
|
+
) -> Dict[str, Any]:
|
270
296
|
"""Calculate various complexity metrics."""
|
271
297
|
complexity = {
|
272
298
|
"cyclomatic": 1, # Base complexity
|
@@ -315,26 +341,35 @@ class CodeAnalyzerStrategy(AnalyzerStrategy):
|
|
315
341
|
"""Calculate complexity for a single AST node."""
|
316
342
|
return self._calculate_cyclomatic_complexity(node)
|
317
343
|
|
318
|
-
def _detect_code_smells(
|
344
|
+
def _detect_code_smells(
|
345
|
+
self, content: str, metrics: Dict[str, Any]
|
346
|
+
) -> List[Dict[str, Any]]:
|
319
347
|
"""Detect common code smells."""
|
320
348
|
smells = []
|
321
349
|
|
322
350
|
# Long method/function
|
323
|
-
if
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
351
|
+
if (
|
352
|
+
metrics.get("lines_of_code", 0)
|
353
|
+
> self.code_smell_patterns["long_method"]["threshold"]
|
354
|
+
):
|
355
|
+
smells.append(
|
356
|
+
{
|
357
|
+
"type": "long_method",
|
358
|
+
"severity": "medium",
|
359
|
+
"message": f"Method/function has {metrics['lines_of_code']} lines (threshold: {self.code_smell_patterns['long_method']['threshold']})",
|
360
|
+
}
|
361
|
+
)
|
329
362
|
|
330
363
|
# High complexity
|
331
364
|
complexity = metrics.get("complexity", {}).get("cyclomatic", 0)
|
332
365
|
if complexity > 10:
|
333
|
-
smells.append(
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
366
|
+
smells.append(
|
367
|
+
{
|
368
|
+
"type": "high_complexity",
|
369
|
+
"severity": "high",
|
370
|
+
"message": f"High cyclomatic complexity: {complexity}",
|
371
|
+
}
|
372
|
+
)
|
338
373
|
|
339
374
|
return smells
|
340
375
|
|
@@ -398,25 +433,35 @@ class CodeAnalyzerStrategy(AnalyzerStrategy):
|
|
398
433
|
if "metrics" in analysis_result:
|
399
434
|
raw_metrics = analysis_result["metrics"]
|
400
435
|
|
401
|
-
metrics.update(
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
436
|
+
metrics.update(
|
437
|
+
{
|
438
|
+
"lines_of_code": raw_metrics.get("lines_of_code", 0),
|
439
|
+
"cyclomatic_complexity": raw_metrics.get("complexity", {}).get(
|
440
|
+
"cyclomatic", 0
|
441
|
+
),
|
442
|
+
"cognitive_complexity": raw_metrics.get("complexity", {}).get(
|
443
|
+
"cognitive", 0
|
444
|
+
),
|
445
|
+
"maintainability_index": raw_metrics.get(
|
446
|
+
"maintainability_index", 0
|
447
|
+
),
|
448
|
+
"code_smells": len(raw_metrics.get("code_smells", [])),
|
449
|
+
"function_count": raw_metrics.get("function_count", 0),
|
450
|
+
"class_count": raw_metrics.get("class_count", 0),
|
451
|
+
}
|
452
|
+
)
|
410
453
|
|
411
454
|
# Extract summary metrics for directory analysis
|
412
455
|
if "summary" in analysis_result:
|
413
456
|
summary = analysis_result["summary"]
|
414
|
-
metrics.update(
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
457
|
+
metrics.update(
|
458
|
+
{
|
459
|
+
"total_files": summary.get("total_files", 0),
|
460
|
+
"total_lines": summary.get("total_lines", 0),
|
461
|
+
"average_complexity": summary.get("average_complexity", 0),
|
462
|
+
"total_code_smells": summary.get("code_smells_count", 0),
|
463
|
+
}
|
464
|
+
)
|
420
465
|
|
421
466
|
return metrics
|
422
467
|
|
@@ -461,13 +506,12 @@ class CodeAnalyzerStrategy(AnalyzerStrategy):
|
|
461
506
|
comparison["degraded"].append(result)
|
462
507
|
else:
|
463
508
|
comparison["unchanged"].append(result)
|
509
|
+
# Lower is better (complexity, code smells, etc.)
|
510
|
+
elif diff < 0:
|
511
|
+
comparison["improved"].append(result)
|
512
|
+
elif diff > 0:
|
513
|
+
comparison["degraded"].append(result)
|
464
514
|
else:
|
465
|
-
|
466
|
-
if diff < 0:
|
467
|
-
comparison["improved"].append(result)
|
468
|
-
elif diff > 0:
|
469
|
-
comparison["degraded"].append(result)
|
470
|
-
else:
|
471
|
-
comparison["unchanged"].append(result)
|
515
|
+
comparison["unchanged"].append(result)
|
472
516
|
|
473
|
-
return comparison
|
517
|
+
return comparison
|
@@ -11,13 +11,17 @@ Created: 2025-01-26
|
|
11
11
|
|
12
12
|
import json
|
13
13
|
import re
|
14
|
-
import subprocess
|
15
14
|
from pathlib import Path
|
16
|
-
from typing import Any, Dict, List, Optional
|
15
|
+
from typing import Any, Dict, List, Optional
|
17
16
|
|
18
17
|
from claude_mpm.core.logging_utils import get_logger
|
19
18
|
|
20
|
-
from ..strategies import
|
19
|
+
from ..strategies import (
|
20
|
+
AnalyzerStrategy,
|
21
|
+
StrategyContext,
|
22
|
+
StrategyMetadata,
|
23
|
+
StrategyPriority,
|
24
|
+
)
|
21
25
|
|
22
26
|
logger = get_logger(__name__)
|
23
27
|
|
@@ -69,7 +73,15 @@ class DependencyAnalyzerStrategy(AnalyzerStrategy):
|
|
69
73
|
# Testing framework packages
|
70
74
|
TESTING_PACKAGES = {
|
71
75
|
"python": ["pytest", "unittest", "nose", "nose2", "tox", "coverage"],
|
72
|
-
"javascript": [
|
76
|
+
"javascript": [
|
77
|
+
"jest",
|
78
|
+
"mocha",
|
79
|
+
"chai",
|
80
|
+
"jasmine",
|
81
|
+
"cypress",
|
82
|
+
"playwright",
|
83
|
+
"vitest",
|
84
|
+
],
|
73
85
|
"java": ["junit", "testng", "mockito", "assertj"],
|
74
86
|
"ruby": ["rspec", "minitest", "cucumber"],
|
75
87
|
"go": ["testify", "ginkgo", "gomega"],
|
@@ -79,7 +91,16 @@ class DependencyAnalyzerStrategy(AnalyzerStrategy):
|
|
79
91
|
# Web framework packages
|
80
92
|
FRAMEWORK_PACKAGES = {
|
81
93
|
"python": ["django", "flask", "fastapi", "pyramid", "tornado", "aiohttp"],
|
82
|
-
"javascript": [
|
94
|
+
"javascript": [
|
95
|
+
"express",
|
96
|
+
"koa",
|
97
|
+
"fastify",
|
98
|
+
"hapi",
|
99
|
+
"nestjs",
|
100
|
+
"next",
|
101
|
+
"nuxt",
|
102
|
+
"gatsby",
|
103
|
+
],
|
83
104
|
"ruby": ["rails", "sinatra", "hanami"],
|
84
105
|
"java": ["spring", "spring-boot", "struts", "play"],
|
85
106
|
"php": ["laravel", "symfony", "slim", "lumen"],
|
@@ -141,7 +162,7 @@ class DependencyAnalyzerStrategy(AnalyzerStrategy):
|
|
141
162
|
|
142
163
|
if target_path.is_dir():
|
143
164
|
return self._analyze_project(target_path, options)
|
144
|
-
|
165
|
+
if target_path.is_file():
|
145
166
|
return self._analyze_manifest(target_path, options)
|
146
167
|
|
147
168
|
return {
|
@@ -149,7 +170,9 @@ class DependencyAnalyzerStrategy(AnalyzerStrategy):
|
|
149
170
|
"message": f"Unsupported target type: {type(target).__name__}",
|
150
171
|
}
|
151
172
|
|
152
|
-
def _analyze_project(
|
173
|
+
def _analyze_project(
|
174
|
+
self, project_path: Path, options: Dict[str, Any]
|
175
|
+
) -> Dict[str, Any]:
|
153
176
|
"""Analyze dependencies in a project directory."""
|
154
177
|
results = {
|
155
178
|
"status": "success",
|
@@ -172,7 +195,9 @@ class DependencyAnalyzerStrategy(AnalyzerStrategy):
|
|
172
195
|
manager_deps = self._analyze_package_manager(project_path, manager, options)
|
173
196
|
if manager_deps:
|
174
197
|
results["dependencies"][manager] = manager_deps.get("dependencies", {})
|
175
|
-
results["dev_dependencies"][manager] = manager_deps.get(
|
198
|
+
results["dev_dependencies"][manager] = manager_deps.get(
|
199
|
+
"dev_dependencies", {}
|
200
|
+
)
|
176
201
|
|
177
202
|
# Detect frameworks, databases, and testing tools
|
178
203
|
all_deps = self._flatten_dependencies(results["dependencies"])
|
@@ -191,7 +216,9 @@ class DependencyAnalyzerStrategy(AnalyzerStrategy):
|
|
191
216
|
|
192
217
|
return results
|
193
218
|
|
194
|
-
def _analyze_manifest(
|
219
|
+
def _analyze_manifest(
|
220
|
+
self, manifest_path: Path, options: Dict[str, Any]
|
221
|
+
) -> Dict[str, Any]:
|
195
222
|
"""Analyze a specific package manifest file."""
|
196
223
|
results = {
|
197
224
|
"status": "success",
|
@@ -277,20 +304,21 @@ class DependencyAnalyzerStrategy(AnalyzerStrategy):
|
|
277
304
|
try:
|
278
305
|
if manager in ["npm", "yarn", "pnpm"]:
|
279
306
|
return self._analyze_node_dependencies(project_path, manager)
|
280
|
-
|
307
|
+
if manager in ["pip", "pipenv", "poetry"]:
|
281
308
|
return self._analyze_python_dependencies(project_path, manager)
|
282
|
-
|
309
|
+
if manager == "cargo":
|
283
310
|
return self._analyze_cargo_dependencies(project_path)
|
284
|
-
|
311
|
+
if manager == "go":
|
285
312
|
return self._analyze_go_dependencies(project_path)
|
286
|
-
|
287
|
-
|
288
|
-
return None
|
313
|
+
logger.debug(f"Unsupported package manager for analysis: {manager}")
|
314
|
+
return None
|
289
315
|
except Exception as e:
|
290
316
|
logger.error(f"Error analyzing {manager} dependencies: {e}")
|
291
317
|
return None
|
292
318
|
|
293
|
-
def _analyze_node_dependencies(
|
319
|
+
def _analyze_node_dependencies(
|
320
|
+
self, project_path: Path, manager: str
|
321
|
+
) -> Dict[str, Any]:
|
294
322
|
"""Analyze Node.js dependencies."""
|
295
323
|
package_json_path = project_path / "package.json"
|
296
324
|
if not package_json_path.exists():
|
@@ -298,7 +326,9 @@ class DependencyAnalyzerStrategy(AnalyzerStrategy):
|
|
298
326
|
|
299
327
|
return self._parse_package_json(package_json_path)
|
300
328
|
|
301
|
-
def _analyze_python_dependencies(
|
329
|
+
def _analyze_python_dependencies(
|
330
|
+
self, project_path: Path, manager: str
|
331
|
+
) -> Dict[str, Any]:
|
302
332
|
"""Analyze Python dependencies."""
|
303
333
|
results = {"dependencies": {}, "dev_dependencies": {}}
|
304
334
|
|
@@ -345,7 +375,7 @@ class DependencyAnalyzerStrategy(AnalyzerStrategy):
|
|
345
375
|
def _parse_package_json(self, path: Path) -> Dict[str, Any]:
|
346
376
|
"""Parse package.json file."""
|
347
377
|
try:
|
348
|
-
with open(path
|
378
|
+
with open(path) as f:
|
349
379
|
data = json.load(f)
|
350
380
|
|
351
381
|
return {
|
@@ -479,7 +509,7 @@ class DependencyAnalyzerStrategy(AnalyzerStrategy):
|
|
479
509
|
if line.startswith("require ("):
|
480
510
|
in_require = True
|
481
511
|
continue
|
482
|
-
|
512
|
+
if line == ")":
|
483
513
|
in_require = False
|
484
514
|
continue
|
485
515
|
|
@@ -494,7 +524,9 @@ class DependencyAnalyzerStrategy(AnalyzerStrategy):
|
|
494
524
|
|
495
525
|
return {"dependencies": dependencies}
|
496
526
|
|
497
|
-
def _flatten_dependencies(
|
527
|
+
def _flatten_dependencies(
|
528
|
+
self, deps_dict: Dict[str, Dict[str, str]]
|
529
|
+
) -> Dict[str, str]:
|
498
530
|
"""Flatten nested dependency dictionaries."""
|
499
531
|
flattened = {}
|
500
532
|
for manager_deps in deps_dict.values():
|
@@ -581,11 +613,13 @@ class DependencyAnalyzerStrategy(AnalyzerStrategy):
|
|
581
613
|
# Extract vulnerability metrics
|
582
614
|
if "vulnerabilities" in analysis_result:
|
583
615
|
vuln = analysis_result["vulnerabilities"]
|
584
|
-
metrics.update(
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
616
|
+
metrics.update(
|
617
|
+
{
|
618
|
+
"vulnerability_total": vuln.get("total", 0),
|
619
|
+
"vulnerability_critical": vuln.get("critical", 0),
|
620
|
+
"vulnerability_high": vuln.get("high", 0),
|
621
|
+
}
|
622
|
+
)
|
589
623
|
|
590
624
|
return metrics
|
591
625
|
|
@@ -607,27 +641,33 @@ class DependencyAnalyzerStrategy(AnalyzerStrategy):
|
|
607
641
|
# Find added dependencies
|
608
642
|
for dep, version in current_deps.items():
|
609
643
|
if dep not in baseline_deps:
|
610
|
-
comparison["added_dependencies"].append(
|
611
|
-
|
612
|
-
|
613
|
-
|
644
|
+
comparison["added_dependencies"].append(
|
645
|
+
{
|
646
|
+
"name": dep,
|
647
|
+
"version": version,
|
648
|
+
}
|
649
|
+
)
|
614
650
|
|
615
651
|
# Find removed dependencies
|
616
652
|
for dep, version in baseline_deps.items():
|
617
653
|
if dep not in current_deps:
|
618
|
-
comparison["removed_dependencies"].append(
|
619
|
-
|
620
|
-
|
621
|
-
|
654
|
+
comparison["removed_dependencies"].append(
|
655
|
+
{
|
656
|
+
"name": dep,
|
657
|
+
"version": version,
|
658
|
+
}
|
659
|
+
)
|
622
660
|
|
623
661
|
# Find updated dependencies
|
624
662
|
for dep in baseline_deps:
|
625
663
|
if dep in current_deps and baseline_deps[dep] != current_deps[dep]:
|
626
|
-
comparison["updated_dependencies"].append(
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
664
|
+
comparison["updated_dependencies"].append(
|
665
|
+
{
|
666
|
+
"name": dep,
|
667
|
+
"old_version": baseline_deps[dep],
|
668
|
+
"new_version": current_deps[dep],
|
669
|
+
}
|
670
|
+
)
|
631
671
|
|
632
672
|
# Compare vulnerability counts
|
633
673
|
if "vulnerabilities" in baseline and "vulnerabilities" in current:
|
@@ -636,8 +676,9 @@ class DependencyAnalyzerStrategy(AnalyzerStrategy):
|
|
636
676
|
|
637
677
|
comparison["vulnerability_changes"] = {
|
638
678
|
"total": current_vuln.get("total", 0) - baseline_vuln.get("total", 0),
|
639
|
-
"critical": current_vuln.get("critical", 0)
|
679
|
+
"critical": current_vuln.get("critical", 0)
|
680
|
+
- baseline_vuln.get("critical", 0),
|
640
681
|
"high": current_vuln.get("high", 0) - baseline_vuln.get("high", 0),
|
641
682
|
}
|
642
683
|
|
643
|
-
return comparison
|
684
|
+
return comparison
|