claude-mpm 4.3.22__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.
Files changed (31) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/doctor.py +2 -2
  3. claude_mpm/hooks/memory_integration_hook.py +1 -1
  4. claude_mpm/services/agents/memory/content_manager.py +5 -2
  5. claude_mpm/services/agents/memory/memory_file_service.py +1 -0
  6. claude_mpm/services/agents/memory/memory_limits_service.py +1 -0
  7. claude_mpm/services/unified/__init__.py +65 -0
  8. claude_mpm/services/unified/analyzer_strategies/__init__.py +44 -0
  9. claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +473 -0
  10. claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +643 -0
  11. claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +804 -0
  12. claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +661 -0
  13. claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +696 -0
  14. claude_mpm/services/unified/deployment_strategies/__init__.py +97 -0
  15. claude_mpm/services/unified/deployment_strategies/base.py +557 -0
  16. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +486 -0
  17. claude_mpm/services/unified/deployment_strategies/local.py +594 -0
  18. claude_mpm/services/unified/deployment_strategies/utils.py +672 -0
  19. claude_mpm/services/unified/deployment_strategies/vercel.py +471 -0
  20. claude_mpm/services/unified/interfaces.py +499 -0
  21. claude_mpm/services/unified/migration.py +532 -0
  22. claude_mpm/services/unified/strategies.py +551 -0
  23. claude_mpm/services/unified/unified_analyzer.py +534 -0
  24. claude_mpm/services/unified/unified_config.py +688 -0
  25. claude_mpm/services/unified/unified_deployment.py +470 -0
  26. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.0.dist-info}/METADATA +1 -1
  27. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.0.dist-info}/RECORD +31 -12
  28. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.0.dist-info}/WHEEL +0 -0
  29. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.0.dist-info}/entry_points.txt +0 -0
  30. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.0.dist-info}/licenses/LICENSE +0 -0
  31. {claude_mpm-4.3.22.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