claude-mpm 4.3.20__py3-none-any.whl → 4.4.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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/agent_loader.py +2 -2
- claude_mpm/agents/agent_loader_integration.py +2 -2
- claude_mpm/agents/async_agent_loader.py +2 -2
- claude_mpm/agents/base_agent_loader.py +2 -2
- claude_mpm/agents/frontmatter_validator.py +2 -2
- claude_mpm/agents/system_agent_config.py +2 -2
- claude_mpm/agents/templates/data_engineer.json +1 -2
- claude_mpm/cli/commands/doctor.py +2 -2
- claude_mpm/cli/commands/mpm_init.py +560 -47
- claude_mpm/cli/commands/mpm_init_handler.py +6 -0
- claude_mpm/cli/parsers/mpm_init_parser.py +39 -1
- claude_mpm/cli/startup_logging.py +11 -9
- claude_mpm/commands/mpm-init.md +76 -12
- claude_mpm/config/agent_config.py +2 -2
- claude_mpm/config/paths.py +2 -2
- claude_mpm/core/agent_name_normalizer.py +2 -2
- claude_mpm/core/config.py +2 -1
- claude_mpm/core/config_aliases.py +2 -2
- claude_mpm/core/file_utils.py +1 -0
- claude_mpm/core/log_manager.py +2 -2
- claude_mpm/core/tool_access_control.py +2 -2
- claude_mpm/core/unified_agent_registry.py +2 -2
- claude_mpm/core/unified_paths.py +2 -2
- claude_mpm/experimental/cli_enhancements.py +3 -2
- claude_mpm/hooks/base_hook.py +2 -2
- claude_mpm/hooks/instruction_reinforcement.py +2 -2
- claude_mpm/hooks/memory_integration_hook.py +1 -1
- claude_mpm/hooks/validation_hooks.py +2 -2
- claude_mpm/scripts/mpm_doctor.py +2 -2
- claude_mpm/services/agents/loading/agent_profile_loader.py +2 -2
- claude_mpm/services/agents/loading/base_agent_manager.py +2 -2
- claude_mpm/services/agents/loading/framework_agent_loader.py +2 -2
- claude_mpm/services/agents/management/agent_capabilities_generator.py +2 -2
- claude_mpm/services/agents/management/agent_management_service.py +2 -2
- claude_mpm/services/agents/memory/content_manager.py +5 -2
- claude_mpm/services/agents/memory/memory_categorization_service.py +5 -2
- claude_mpm/services/agents/memory/memory_file_service.py +28 -6
- claude_mpm/services/agents/memory/memory_format_service.py +5 -2
- claude_mpm/services/agents/memory/memory_limits_service.py +4 -2
- claude_mpm/services/agents/registry/deployed_agent_discovery.py +2 -2
- claude_mpm/services/agents/registry/modification_tracker.py +4 -4
- claude_mpm/services/async_session_logger.py +2 -1
- claude_mpm/services/claude_session_logger.py +2 -2
- claude_mpm/services/core/path_resolver.py +3 -2
- claude_mpm/services/diagnostics/diagnostic_runner.py +4 -3
- claude_mpm/services/event_bus/direct_relay.py +2 -1
- claude_mpm/services/event_bus/event_bus.py +2 -1
- claude_mpm/services/event_bus/relay.py +2 -2
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +2 -2
- claude_mpm/services/infrastructure/daemon_manager.py +2 -2
- claude_mpm/services/memory/cache/simple_cache.py +2 -2
- claude_mpm/services/project/archive_manager.py +981 -0
- claude_mpm/services/project/documentation_manager.py +536 -0
- claude_mpm/services/project/enhanced_analyzer.py +491 -0
- claude_mpm/services/project/project_organizer.py +904 -0
- claude_mpm/services/response_tracker.py +2 -2
- claude_mpm/services/socketio/handlers/connection.py +14 -33
- claude_mpm/services/socketio/server/eventbus_integration.py +2 -2
- claude_mpm/services/unified/__init__.py +65 -0
- claude_mpm/services/unified/analyzer_strategies/__init__.py +44 -0
- claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +473 -0
- claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +643 -0
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +804 -0
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +661 -0
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +696 -0
- claude_mpm/services/unified/deployment_strategies/__init__.py +97 -0
- claude_mpm/services/unified/deployment_strategies/base.py +557 -0
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +486 -0
- claude_mpm/services/unified/deployment_strategies/local.py +594 -0
- claude_mpm/services/unified/deployment_strategies/utils.py +672 -0
- claude_mpm/services/unified/deployment_strategies/vercel.py +471 -0
- claude_mpm/services/unified/interfaces.py +499 -0
- claude_mpm/services/unified/migration.py +532 -0
- claude_mpm/services/unified/strategies.py +551 -0
- claude_mpm/services/unified/unified_analyzer.py +534 -0
- claude_mpm/services/unified/unified_config.py +688 -0
- claude_mpm/services/unified/unified_deployment.py +470 -0
- claude_mpm/services/version_control/version_parser.py +5 -4
- claude_mpm/storage/state_storage.py +2 -2
- claude_mpm/utils/agent_dependency_loader.py +49 -0
- claude_mpm/utils/common.py +542 -0
- claude_mpm/utils/database_connector.py +298 -0
- claude_mpm/utils/error_handler.py +2 -1
- claude_mpm/utils/log_cleanup.py +2 -2
- claude_mpm/utils/path_operations.py +2 -2
- claude_mpm/utils/robust_installer.py +56 -0
- claude_mpm/utils/session_logging.py +2 -2
- claude_mpm/utils/subprocess_utils.py +2 -2
- claude_mpm/validation/agent_validator.py +2 -2
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.4.0.dist-info}/METADATA +1 -1
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.4.0.dist-info}/RECORD +96 -71
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.4.0.dist-info}/WHEEL +0 -0
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.4.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.4.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,696 @@
|
|
1
|
+
"""
|
2
|
+
Structure Analyzer Strategy Implementation
|
3
|
+
==========================================
|
4
|
+
|
5
|
+
Analyzes project structure, organization, and architectural patterns.
|
6
|
+
Consolidates structure analysis functionality from multiple services.
|
7
|
+
|
8
|
+
Author: Claude MPM Development Team
|
9
|
+
Created: 2025-01-26
|
10
|
+
"""
|
11
|
+
|
12
|
+
import fnmatch
|
13
|
+
import os
|
14
|
+
from pathlib import Path
|
15
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
16
|
+
|
17
|
+
from claude_mpm.core.logging_utils import get_logger
|
18
|
+
|
19
|
+
from ..strategies import AnalyzerStrategy, StrategyContext, StrategyMetadata, StrategyPriority
|
20
|
+
|
21
|
+
logger = get_logger(__name__)
|
22
|
+
|
23
|
+
|
24
|
+
class StructureAnalyzerStrategy(AnalyzerStrategy):
|
25
|
+
"""
|
26
|
+
Strategy for analyzing project structure and organization.
|
27
|
+
|
28
|
+
Consolidates:
|
29
|
+
- Directory structure analysis
|
30
|
+
- File organization patterns
|
31
|
+
- Module/package detection
|
32
|
+
- Architecture pattern recognition
|
33
|
+
- Naming convention analysis
|
34
|
+
"""
|
35
|
+
|
36
|
+
# Common project patterns
|
37
|
+
PROJECT_PATTERNS = {
|
38
|
+
"mvc": {
|
39
|
+
"dirs": ["models", "views", "controllers"],
|
40
|
+
"confidence": 0.8,
|
41
|
+
},
|
42
|
+
"layered": {
|
43
|
+
"dirs": ["presentation", "business", "data", "domain"],
|
44
|
+
"confidence": 0.7,
|
45
|
+
},
|
46
|
+
"hexagonal": {
|
47
|
+
"dirs": ["domain", "application", "infrastructure", "adapters"],
|
48
|
+
"confidence": 0.8,
|
49
|
+
},
|
50
|
+
"clean": {
|
51
|
+
"dirs": ["entities", "usecases", "interfaces", "frameworks"],
|
52
|
+
"confidence": 0.8,
|
53
|
+
},
|
54
|
+
"microservice": {
|
55
|
+
"dirs": ["services", "api-gateway", "common", "shared"],
|
56
|
+
"confidence": 0.7,
|
57
|
+
},
|
58
|
+
}
|
59
|
+
|
60
|
+
# Language-specific structure patterns
|
61
|
+
LANGUAGE_STRUCTURES = {
|
62
|
+
"python": {
|
63
|
+
"src_patterns": ["src", "lib", "app"],
|
64
|
+
"test_patterns": ["tests", "test", "spec"],
|
65
|
+
"config_files": ["setup.py", "setup.cfg", "pyproject.toml"],
|
66
|
+
"module_indicator": "__init__.py",
|
67
|
+
},
|
68
|
+
"javascript": {
|
69
|
+
"src_patterns": ["src", "lib", "app", "client", "server"],
|
70
|
+
"test_patterns": ["tests", "test", "__tests__", "spec"],
|
71
|
+
"config_files": ["package.json", "tsconfig.json", "webpack.config.js"],
|
72
|
+
"module_indicator": "index.js",
|
73
|
+
},
|
74
|
+
"java": {
|
75
|
+
"src_patterns": ["src/main/java", "src"],
|
76
|
+
"test_patterns": ["src/test/java", "test"],
|
77
|
+
"config_files": ["pom.xml", "build.gradle"],
|
78
|
+
"module_indicator": None,
|
79
|
+
},
|
80
|
+
"go": {
|
81
|
+
"src_patterns": ["cmd", "internal", "pkg"],
|
82
|
+
"test_patterns": ["test"],
|
83
|
+
"config_files": ["go.mod"],
|
84
|
+
"module_indicator": "go.mod",
|
85
|
+
},
|
86
|
+
}
|
87
|
+
|
88
|
+
# Common ignore patterns
|
89
|
+
IGNORE_PATTERNS = [
|
90
|
+
"*.pyc", "__pycache__", ".git", ".svn", ".hg",
|
91
|
+
"node_modules", "venv", ".venv", "env",
|
92
|
+
"dist", "build", "target", "bin", "obj",
|
93
|
+
".idea", ".vscode", "*.egg-info",
|
94
|
+
".pytest_cache", ".coverage", ".tox",
|
95
|
+
]
|
96
|
+
|
97
|
+
def __init__(self):
|
98
|
+
"""Initialize structure analyzer strategy."""
|
99
|
+
metadata = StrategyMetadata(
|
100
|
+
name="StructureAnalyzer",
|
101
|
+
description="Analyzes project structure and organization patterns",
|
102
|
+
supported_types=["project", "directory", "module"],
|
103
|
+
supported_operations=["analyze", "structure", "patterns", "metrics"],
|
104
|
+
priority=StrategyPriority.HIGH,
|
105
|
+
tags={"structure", "organization", "architecture", "patterns"},
|
106
|
+
)
|
107
|
+
super().__init__(metadata)
|
108
|
+
|
109
|
+
self._file_cache = {}
|
110
|
+
self._dir_cache = {}
|
111
|
+
|
112
|
+
def can_handle(self, context: StrategyContext) -> bool:
|
113
|
+
"""Check if strategy can handle the given context."""
|
114
|
+
return (
|
115
|
+
context.target_type in self.metadata.supported_types
|
116
|
+
and context.operation in self.metadata.supported_operations
|
117
|
+
)
|
118
|
+
|
119
|
+
def validate_input(self, input_data: Any) -> List[str]:
|
120
|
+
"""Validate input data for strategy."""
|
121
|
+
errors = []
|
122
|
+
|
123
|
+
if not input_data:
|
124
|
+
errors.append("Input data is required")
|
125
|
+
return errors
|
126
|
+
|
127
|
+
if isinstance(input_data, (str, Path)):
|
128
|
+
path = Path(input_data)
|
129
|
+
if not path.exists():
|
130
|
+
errors.append(f"Path does not exist: {path}")
|
131
|
+
elif not path.is_dir():
|
132
|
+
errors.append(f"Path is not a directory: {path}")
|
133
|
+
else:
|
134
|
+
errors.append(f"Invalid input type: {type(input_data).__name__}")
|
135
|
+
|
136
|
+
return errors
|
137
|
+
|
138
|
+
def analyze(
|
139
|
+
self, target: Any, options: Optional[Dict[str, Any]] = None
|
140
|
+
) -> Dict[str, Any]:
|
141
|
+
"""
|
142
|
+
Execute structure analysis on target.
|
143
|
+
|
144
|
+
Args:
|
145
|
+
target: Project directory to analyze
|
146
|
+
options: Analysis options (max_depth, ignore_patterns, etc.)
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
Analysis results with structure information
|
150
|
+
"""
|
151
|
+
options = options or {}
|
152
|
+
|
153
|
+
if isinstance(target, (str, Path)):
|
154
|
+
target_path = Path(target)
|
155
|
+
|
156
|
+
if not target_path.is_dir():
|
157
|
+
return {
|
158
|
+
"status": "error",
|
159
|
+
"message": "Target must be a directory",
|
160
|
+
}
|
161
|
+
|
162
|
+
return self._analyze_structure(target_path, options)
|
163
|
+
|
164
|
+
return {
|
165
|
+
"status": "error",
|
166
|
+
"message": f"Unsupported target type: {type(target).__name__}",
|
167
|
+
}
|
168
|
+
|
169
|
+
def _analyze_structure(self, root_path: Path, options: Dict[str, Any]) -> Dict[str, Any]:
|
170
|
+
"""Analyze the structure of a project directory."""
|
171
|
+
results = {
|
172
|
+
"status": "success",
|
173
|
+
"type": "structure",
|
174
|
+
"path": str(root_path),
|
175
|
+
"tree": {},
|
176
|
+
"statistics": {},
|
177
|
+
"patterns": {},
|
178
|
+
"organization": {},
|
179
|
+
}
|
180
|
+
|
181
|
+
# Build directory tree
|
182
|
+
max_depth = options.get("max_depth", 5)
|
183
|
+
ignore_patterns = options.get("ignore_patterns", self.IGNORE_PATTERNS)
|
184
|
+
|
185
|
+
tree, stats = self._build_tree(root_path, max_depth, ignore_patterns)
|
186
|
+
results["tree"] = tree
|
187
|
+
results["statistics"] = stats
|
188
|
+
|
189
|
+
# Detect project patterns
|
190
|
+
results["patterns"] = self._detect_patterns(root_path, tree)
|
191
|
+
|
192
|
+
# Analyze organization
|
193
|
+
results["organization"] = self._analyze_organization(root_path, tree, stats)
|
194
|
+
|
195
|
+
# Detect architecture style
|
196
|
+
results["architecture"] = self._detect_architecture(tree)
|
197
|
+
|
198
|
+
# Calculate complexity metrics
|
199
|
+
results["complexity"] = self._calculate_structure_complexity(tree, stats)
|
200
|
+
|
201
|
+
# Detect language and framework
|
202
|
+
results["language"] = self._detect_language(root_path)
|
203
|
+
results["framework"] = self._detect_framework(root_path, results["language"])
|
204
|
+
|
205
|
+
return results
|
206
|
+
|
207
|
+
def _build_tree(
|
208
|
+
self, root_path: Path, max_depth: int, ignore_patterns: List[str]
|
209
|
+
) -> Tuple[Dict[str, Any], Dict[str, int]]:
|
210
|
+
"""Build directory tree structure."""
|
211
|
+
tree = {
|
212
|
+
"name": root_path.name,
|
213
|
+
"type": "directory",
|
214
|
+
"path": str(root_path),
|
215
|
+
"children": [],
|
216
|
+
}
|
217
|
+
|
218
|
+
statistics = {
|
219
|
+
"total_files": 0,
|
220
|
+
"total_dirs": 0,
|
221
|
+
"max_depth": 0,
|
222
|
+
"file_types": {},
|
223
|
+
}
|
224
|
+
|
225
|
+
def should_ignore(path: Path) -> bool:
|
226
|
+
"""Check if path should be ignored."""
|
227
|
+
for pattern in ignore_patterns:
|
228
|
+
if fnmatch.fnmatch(path.name, pattern):
|
229
|
+
return True
|
230
|
+
return False
|
231
|
+
|
232
|
+
def walk_directory(
|
233
|
+
current_path: Path, current_node: Dict[str, Any], depth: int
|
234
|
+
) -> None:
|
235
|
+
"""Recursively walk directory tree."""
|
236
|
+
if depth > max_depth:
|
237
|
+
return
|
238
|
+
|
239
|
+
statistics["max_depth"] = max(statistics["max_depth"], depth)
|
240
|
+
|
241
|
+
try:
|
242
|
+
items = sorted(current_path.iterdir())
|
243
|
+
except PermissionError:
|
244
|
+
return
|
245
|
+
|
246
|
+
for item in items:
|
247
|
+
if should_ignore(item):
|
248
|
+
continue
|
249
|
+
|
250
|
+
if item.is_dir():
|
251
|
+
statistics["total_dirs"] += 1
|
252
|
+
child_node = {
|
253
|
+
"name": item.name,
|
254
|
+
"type": "directory",
|
255
|
+
"path": str(item),
|
256
|
+
"children": [],
|
257
|
+
}
|
258
|
+
current_node["children"].append(child_node)
|
259
|
+
walk_directory(item, child_node, depth + 1)
|
260
|
+
|
261
|
+
elif item.is_file():
|
262
|
+
statistics["total_files"] += 1
|
263
|
+
|
264
|
+
# Track file types
|
265
|
+
ext = item.suffix.lower()
|
266
|
+
if ext:
|
267
|
+
statistics["file_types"][ext] = statistics["file_types"].get(ext, 0) + 1
|
268
|
+
|
269
|
+
# Get file info
|
270
|
+
try:
|
271
|
+
size = item.stat().st_size
|
272
|
+
except:
|
273
|
+
size = 0
|
274
|
+
|
275
|
+
child_node = {
|
276
|
+
"name": item.name,
|
277
|
+
"type": "file",
|
278
|
+
"path": str(item),
|
279
|
+
"size": size,
|
280
|
+
"extension": ext,
|
281
|
+
}
|
282
|
+
current_node["children"].append(child_node)
|
283
|
+
|
284
|
+
walk_directory(root_path, tree, 0)
|
285
|
+
|
286
|
+
return tree, statistics
|
287
|
+
|
288
|
+
def _detect_patterns(self, root_path: Path, tree: Dict[str, Any]) -> Dict[str, Any]:
|
289
|
+
"""Detect common project patterns."""
|
290
|
+
patterns = {
|
291
|
+
"has_src": False,
|
292
|
+
"has_tests": False,
|
293
|
+
"has_docs": False,
|
294
|
+
"has_ci": False,
|
295
|
+
"has_config": False,
|
296
|
+
"has_examples": False,
|
297
|
+
"naming_convention": None,
|
298
|
+
}
|
299
|
+
|
300
|
+
# Get top-level directories
|
301
|
+
top_level_dirs = {
|
302
|
+
child["name"].lower()
|
303
|
+
for child in tree.get("children", [])
|
304
|
+
if child["type"] == "directory"
|
305
|
+
}
|
306
|
+
|
307
|
+
# Check for common directories
|
308
|
+
src_dirs = {"src", "lib", "app", "source", "main"}
|
309
|
+
test_dirs = {"tests", "test", "spec", "__tests__"}
|
310
|
+
doc_dirs = {"docs", "doc", "documentation"}
|
311
|
+
ci_dirs = {".github", ".gitlab", ".circleci"}
|
312
|
+
example_dirs = {"examples", "example", "samples", "demo"}
|
313
|
+
|
314
|
+
patterns["has_src"] = bool(src_dirs & top_level_dirs)
|
315
|
+
patterns["has_tests"] = bool(test_dirs & top_level_dirs)
|
316
|
+
patterns["has_docs"] = bool(doc_dirs & top_level_dirs)
|
317
|
+
patterns["has_ci"] = bool(ci_dirs & top_level_dirs)
|
318
|
+
patterns["has_examples"] = bool(example_dirs & top_level_dirs)
|
319
|
+
|
320
|
+
# Check for config files
|
321
|
+
config_files = {
|
322
|
+
child["name"].lower()
|
323
|
+
for child in tree.get("children", [])
|
324
|
+
if child["type"] == "file"
|
325
|
+
and child["name"].startswith(".") or child["name"].endswith(".config.js")
|
326
|
+
}
|
327
|
+
patterns["has_config"] = len(config_files) > 0
|
328
|
+
|
329
|
+
# Detect naming convention
|
330
|
+
patterns["naming_convention"] = self._detect_naming_convention(tree)
|
331
|
+
|
332
|
+
return patterns
|
333
|
+
|
334
|
+
def _detect_naming_convention(self, tree: Dict[str, Any]) -> str:
|
335
|
+
"""Detect naming convention used in the project."""
|
336
|
+
file_names = []
|
337
|
+
|
338
|
+
def collect_names(node: Dict[str, Any]) -> None:
|
339
|
+
"""Collect all file and directory names."""
|
340
|
+
if node["type"] == "file":
|
341
|
+
name = node["name"].rsplit(".", 1)[0] if "." in node["name"] else node["name"]
|
342
|
+
file_names.append(name)
|
343
|
+
elif node["type"] == "directory":
|
344
|
+
file_names.append(node["name"])
|
345
|
+
for child in node.get("children", []):
|
346
|
+
collect_names(child)
|
347
|
+
|
348
|
+
collect_names(tree)
|
349
|
+
|
350
|
+
if not file_names:
|
351
|
+
return "unknown"
|
352
|
+
|
353
|
+
# Count naming patterns
|
354
|
+
snake_case = sum(1 for n in file_names if "_" in n and n.islower())
|
355
|
+
kebab_case = sum(1 for n in file_names if "-" in n)
|
356
|
+
camel_case = sum(1 for n in file_names if n[0].islower() and any(c.isupper() for c in n))
|
357
|
+
pascal_case = sum(1 for n in file_names if n[0].isupper() and any(c.islower() for c in n))
|
358
|
+
|
359
|
+
# Determine dominant pattern
|
360
|
+
patterns = {
|
361
|
+
"snake_case": snake_case,
|
362
|
+
"kebab-case": kebab_case,
|
363
|
+
"camelCase": camel_case,
|
364
|
+
"PascalCase": pascal_case,
|
365
|
+
}
|
366
|
+
|
367
|
+
if max(patterns.values()) == 0:
|
368
|
+
return "mixed"
|
369
|
+
|
370
|
+
return max(patterns, key=patterns.get)
|
371
|
+
|
372
|
+
def _analyze_organization(
|
373
|
+
self, root_path: Path, tree: Dict[str, Any], stats: Dict[str, int]
|
374
|
+
) -> Dict[str, Any]:
|
375
|
+
"""Analyze project organization quality."""
|
376
|
+
organization = {
|
377
|
+
"structure_score": 0,
|
378
|
+
"file_organization": {},
|
379
|
+
"depth_analysis": {},
|
380
|
+
"recommendations": [],
|
381
|
+
}
|
382
|
+
|
383
|
+
# Calculate structure score (0-100)
|
384
|
+
score = 100
|
385
|
+
|
386
|
+
# Check for proper separation
|
387
|
+
patterns = self._detect_patterns(root_path, tree)
|
388
|
+
if not patterns["has_src"]:
|
389
|
+
score -= 10
|
390
|
+
organization["recommendations"].append(
|
391
|
+
"Consider organizing source code in a dedicated directory (src, lib, or app)"
|
392
|
+
)
|
393
|
+
|
394
|
+
if not patterns["has_tests"]:
|
395
|
+
score -= 15
|
396
|
+
organization["recommendations"].append(
|
397
|
+
"Add a dedicated test directory for better organization"
|
398
|
+
)
|
399
|
+
|
400
|
+
# Check depth balance
|
401
|
+
max_depth = stats.get("max_depth", 0)
|
402
|
+
if max_depth > 7:
|
403
|
+
score -= 10
|
404
|
+
organization["recommendations"].append(
|
405
|
+
f"Directory structure is too deep ({max_depth} levels). Consider flattening"
|
406
|
+
)
|
407
|
+
elif max_depth < 2:
|
408
|
+
score -= 5
|
409
|
+
organization["recommendations"].append(
|
410
|
+
"Directory structure is too flat. Consider better organization"
|
411
|
+
)
|
412
|
+
|
413
|
+
# Check file distribution
|
414
|
+
avg_files_per_dir = (
|
415
|
+
stats["total_files"] / max(stats["total_dirs"], 1)
|
416
|
+
)
|
417
|
+
if avg_files_per_dir > 20:
|
418
|
+
score -= 10
|
419
|
+
organization["recommendations"].append(
|
420
|
+
"Too many files per directory. Consider better organization"
|
421
|
+
)
|
422
|
+
|
423
|
+
organization["structure_score"] = max(0, score)
|
424
|
+
|
425
|
+
# Analyze file organization by type
|
426
|
+
file_types = stats.get("file_types", {})
|
427
|
+
total_files = stats.get("total_files", 0)
|
428
|
+
|
429
|
+
if total_files > 0:
|
430
|
+
organization["file_organization"] = {
|
431
|
+
ext: {"count": count, "percentage": (count / total_files) * 100}
|
432
|
+
for ext, count in sorted(
|
433
|
+
file_types.items(), key=lambda x: x[1], reverse=True
|
434
|
+
)[:10]
|
435
|
+
}
|
436
|
+
|
437
|
+
# Depth analysis
|
438
|
+
organization["depth_analysis"] = {
|
439
|
+
"max_depth": max_depth,
|
440
|
+
"average_files_per_directory": avg_files_per_dir,
|
441
|
+
"total_directories": stats["total_dirs"],
|
442
|
+
"total_files": stats["total_files"],
|
443
|
+
}
|
444
|
+
|
445
|
+
return organization
|
446
|
+
|
447
|
+
def _detect_architecture(self, tree: Dict[str, Any]) -> Dict[str, Any]:
|
448
|
+
"""Detect architectural patterns in the project structure."""
|
449
|
+
architecture = {
|
450
|
+
"pattern": "unknown",
|
451
|
+
"confidence": 0.0,
|
452
|
+
"detected_patterns": [],
|
453
|
+
}
|
454
|
+
|
455
|
+
# Get all directory names at various levels
|
456
|
+
dir_names = set()
|
457
|
+
|
458
|
+
def collect_dir_names(node: Dict[str, Any], depth: int = 0) -> None:
|
459
|
+
"""Collect directory names up to depth 3."""
|
460
|
+
if depth > 3 or node["type"] != "directory":
|
461
|
+
return
|
462
|
+
|
463
|
+
dir_names.add(node["name"].lower())
|
464
|
+
for child in node.get("children", []):
|
465
|
+
collect_dir_names(child, depth + 1)
|
466
|
+
|
467
|
+
collect_dir_names(tree)
|
468
|
+
|
469
|
+
# Check for architectural patterns
|
470
|
+
for pattern_name, pattern_info in self.PROJECT_PATTERNS.items():
|
471
|
+
required_dirs = set(pattern_info["dirs"])
|
472
|
+
found_dirs = required_dirs & dir_names
|
473
|
+
|
474
|
+
if len(found_dirs) >= len(required_dirs) * pattern_info["confidence"]:
|
475
|
+
architecture["detected_patterns"].append({
|
476
|
+
"name": pattern_name,
|
477
|
+
"confidence": len(found_dirs) / len(required_dirs),
|
478
|
+
"matched_dirs": list(found_dirs),
|
479
|
+
})
|
480
|
+
|
481
|
+
# Select the pattern with highest confidence
|
482
|
+
if architecture["detected_patterns"]:
|
483
|
+
best_pattern = max(
|
484
|
+
architecture["detected_patterns"],
|
485
|
+
key=lambda x: x["confidence"]
|
486
|
+
)
|
487
|
+
architecture["pattern"] = best_pattern["name"]
|
488
|
+
architecture["confidence"] = best_pattern["confidence"]
|
489
|
+
|
490
|
+
return architecture
|
491
|
+
|
492
|
+
def _calculate_structure_complexity(
|
493
|
+
self, tree: Dict[str, Any], stats: Dict[str, int]
|
494
|
+
) -> Dict[str, Any]:
|
495
|
+
"""Calculate structural complexity metrics."""
|
496
|
+
complexity = {
|
497
|
+
"structural_complexity": 0,
|
498
|
+
"nesting_complexity": 0,
|
499
|
+
"file_dispersion": 0,
|
500
|
+
"coupling_indicator": 0,
|
501
|
+
}
|
502
|
+
|
503
|
+
# Structural complexity based on file and directory count
|
504
|
+
total_nodes = stats["total_files"] + stats["total_dirs"]
|
505
|
+
complexity["structural_complexity"] = (
|
506
|
+
(total_nodes / 100) * (stats["max_depth"] / 3)
|
507
|
+
)
|
508
|
+
|
509
|
+
# Nesting complexity
|
510
|
+
complexity["nesting_complexity"] = min(
|
511
|
+
100, (stats["max_depth"] / 7) * 100
|
512
|
+
)
|
513
|
+
|
514
|
+
# File dispersion (how spread out files are)
|
515
|
+
if stats["total_dirs"] > 0:
|
516
|
+
avg_files = stats["total_files"] / stats["total_dirs"]
|
517
|
+
# Ideal is around 5-10 files per directory
|
518
|
+
if avg_files < 5:
|
519
|
+
complexity["file_dispersion"] = (5 - avg_files) * 10
|
520
|
+
elif avg_files > 10:
|
521
|
+
complexity["file_dispersion"] = (avg_files - 10) * 5
|
522
|
+
else:
|
523
|
+
complexity["file_dispersion"] = 0
|
524
|
+
|
525
|
+
# Coupling indicator (based on common cross-cutting directories)
|
526
|
+
cross_cutting_dirs = {"common", "shared", "utils", "helpers", "core"}
|
527
|
+
dir_names = set()
|
528
|
+
|
529
|
+
def collect_dir_names(node: Dict[str, Any]) -> None:
|
530
|
+
if node["type"] == "directory":
|
531
|
+
dir_names.add(node["name"].lower())
|
532
|
+
for child in node.get("children", []):
|
533
|
+
collect_dir_names(child)
|
534
|
+
|
535
|
+
collect_dir_names(tree)
|
536
|
+
coupling_count = len(cross_cutting_dirs & dir_names)
|
537
|
+
complexity["coupling_indicator"] = min(100, coupling_count * 20)
|
538
|
+
|
539
|
+
return complexity
|
540
|
+
|
541
|
+
def _detect_language(self, root_path: Path) -> str:
|
542
|
+
"""Detect primary programming language."""
|
543
|
+
# Check for language-specific config files
|
544
|
+
for language, structure in self.LANGUAGE_STRUCTURES.items():
|
545
|
+
for config_file in structure["config_files"]:
|
546
|
+
if (root_path / config_file).exists():
|
547
|
+
return language
|
548
|
+
|
549
|
+
# Check for common file extensions
|
550
|
+
language_extensions = {
|
551
|
+
"python": [".py"],
|
552
|
+
"javascript": [".js", ".jsx", ".ts", ".tsx"],
|
553
|
+
"java": [".java"],
|
554
|
+
"go": [".go"],
|
555
|
+
"rust": [".rs"],
|
556
|
+
"c": [".c", ".h"],
|
557
|
+
"cpp": [".cpp", ".hpp", ".cc"],
|
558
|
+
"csharp": [".cs"],
|
559
|
+
"ruby": [".rb"],
|
560
|
+
"php": [".php"],
|
561
|
+
}
|
562
|
+
|
563
|
+
file_counts = {}
|
564
|
+
for language, extensions in language_extensions.items():
|
565
|
+
count = sum(
|
566
|
+
len(list(root_path.rglob(f"*{ext}")))
|
567
|
+
for ext in extensions
|
568
|
+
)
|
569
|
+
if count > 0:
|
570
|
+
file_counts[language] = count
|
571
|
+
|
572
|
+
if file_counts:
|
573
|
+
return max(file_counts, key=file_counts.get)
|
574
|
+
|
575
|
+
return "unknown"
|
576
|
+
|
577
|
+
def _detect_framework(self, root_path: Path, language: str) -> Optional[str]:
|
578
|
+
"""Detect framework based on language and files."""
|
579
|
+
framework_indicators = {
|
580
|
+
"python": {
|
581
|
+
"django": ["manage.py", "settings.py"],
|
582
|
+
"flask": ["app.py", "flask"],
|
583
|
+
"fastapi": ["main.py", "fastapi"],
|
584
|
+
},
|
585
|
+
"javascript": {
|
586
|
+
"react": ["package.json", "react"],
|
587
|
+
"angular": ["angular.json"],
|
588
|
+
"vue": ["vue.config.js"],
|
589
|
+
"express": ["app.js", "express"],
|
590
|
+
"next": ["next.config.js"],
|
591
|
+
},
|
592
|
+
"java": {
|
593
|
+
"spring": ["pom.xml", "spring"],
|
594
|
+
"springboot": ["application.properties", "application.yml"],
|
595
|
+
},
|
596
|
+
}
|
597
|
+
|
598
|
+
if language in framework_indicators:
|
599
|
+
for framework, indicators in framework_indicators[language].items():
|
600
|
+
for indicator in indicators:
|
601
|
+
if (root_path / indicator).exists():
|
602
|
+
return framework
|
603
|
+
|
604
|
+
# Check in package files
|
605
|
+
if language == "javascript" and indicator != "package.json":
|
606
|
+
package_json = root_path / "package.json"
|
607
|
+
if package_json.exists():
|
608
|
+
content = package_json.read_text()
|
609
|
+
if indicator in content:
|
610
|
+
return framework
|
611
|
+
|
612
|
+
return None
|
613
|
+
|
614
|
+
def extract_metrics(self, analysis_result: Dict[str, Any]) -> Dict[str, Any]:
|
615
|
+
"""Extract key metrics from analysis results."""
|
616
|
+
metrics = {}
|
617
|
+
|
618
|
+
if analysis_result.get("status") != "success":
|
619
|
+
return metrics
|
620
|
+
|
621
|
+
# Extract structure statistics
|
622
|
+
if "statistics" in analysis_result:
|
623
|
+
stats = analysis_result["statistics"]
|
624
|
+
metrics.update({
|
625
|
+
"total_files": stats.get("total_files", 0),
|
626
|
+
"total_directories": stats.get("total_dirs", 0),
|
627
|
+
"max_depth": stats.get("max_depth", 0),
|
628
|
+
"unique_file_types": len(stats.get("file_types", {})),
|
629
|
+
})
|
630
|
+
|
631
|
+
# Extract organization metrics
|
632
|
+
if "organization" in analysis_result:
|
633
|
+
org = analysis_result["organization"]
|
634
|
+
metrics["structure_score"] = org.get("structure_score", 0)
|
635
|
+
|
636
|
+
# Extract complexity metrics
|
637
|
+
if "complexity" in analysis_result:
|
638
|
+
complexity = analysis_result["complexity"]
|
639
|
+
metrics.update({
|
640
|
+
"structural_complexity": complexity.get("structural_complexity", 0),
|
641
|
+
"nesting_complexity": complexity.get("nesting_complexity", 0),
|
642
|
+
"file_dispersion": complexity.get("file_dispersion", 0),
|
643
|
+
"coupling_indicator": complexity.get("coupling_indicator", 0),
|
644
|
+
})
|
645
|
+
|
646
|
+
return metrics
|
647
|
+
|
648
|
+
def compare_results(
|
649
|
+
self, baseline: Dict[str, Any], current: Dict[str, Any]
|
650
|
+
) -> Dict[str, Any]:
|
651
|
+
"""Compare two structure analysis results."""
|
652
|
+
comparison = {
|
653
|
+
"structure_changes": {},
|
654
|
+
"metric_changes": {},
|
655
|
+
"pattern_changes": {},
|
656
|
+
}
|
657
|
+
|
658
|
+
# Compare metrics
|
659
|
+
baseline_metrics = self.extract_metrics(baseline)
|
660
|
+
current_metrics = self.extract_metrics(current)
|
661
|
+
|
662
|
+
for key in baseline_metrics:
|
663
|
+
if key in current_metrics:
|
664
|
+
diff = current_metrics[key] - baseline_metrics[key]
|
665
|
+
comparison["metric_changes"][key] = {
|
666
|
+
"baseline": baseline_metrics[key],
|
667
|
+
"current": current_metrics[key],
|
668
|
+
"change": diff,
|
669
|
+
"percent_change": (
|
670
|
+
(diff / baseline_metrics[key] * 100)
|
671
|
+
if baseline_metrics[key] else 0
|
672
|
+
),
|
673
|
+
}
|
674
|
+
|
675
|
+
# Compare patterns
|
676
|
+
if "patterns" in baseline and "patterns" in current:
|
677
|
+
baseline_patterns = baseline["patterns"]
|
678
|
+
current_patterns = current["patterns"]
|
679
|
+
|
680
|
+
for key in baseline_patterns:
|
681
|
+
if key in current_patterns:
|
682
|
+
if baseline_patterns[key] != current_patterns[key]:
|
683
|
+
comparison["pattern_changes"][key] = {
|
684
|
+
"baseline": baseline_patterns[key],
|
685
|
+
"current": current_patterns[key],
|
686
|
+
}
|
687
|
+
|
688
|
+
# Compare architecture
|
689
|
+
if "architecture" in baseline and "architecture" in current:
|
690
|
+
if baseline["architecture"]["pattern"] != current["architecture"]["pattern"]:
|
691
|
+
comparison["architecture_change"] = {
|
692
|
+
"baseline": baseline["architecture"]["pattern"],
|
693
|
+
"current": current["architecture"]["pattern"],
|
694
|
+
}
|
695
|
+
|
696
|
+
return comparison
|