claude-mpm 4.3.20__py3-none-any.whl → 4.3.22__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/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/memory_categorization_service.py +5 -2
- claude_mpm/services/agents/memory/memory_file_service.py +27 -6
- claude_mpm/services/agents/memory/memory_format_service.py +5 -2
- claude_mpm/services/agents/memory/memory_limits_service.py +3 -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/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.3.22.dist-info}/METADATA +1 -1
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.3.22.dist-info}/RECORD +75 -69
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.3.22.dist-info}/WHEEL +0 -0
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.3.22.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.3.22.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.3.22.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,491 @@
|
|
1
|
+
"""
|
2
|
+
Enhanced Project Analyzer with Git History Support
|
3
|
+
=================================================
|
4
|
+
|
5
|
+
This service extends project analysis with git history analysis,
|
6
|
+
recent changes tracking, and intelligent project state detection.
|
7
|
+
|
8
|
+
Key Features:
|
9
|
+
- Git history analysis and recent commits
|
10
|
+
- Change frequency detection
|
11
|
+
- Author contribution analysis
|
12
|
+
- File modification tracking
|
13
|
+
- Project lifecycle detection
|
14
|
+
|
15
|
+
Author: Claude MPM Development Team
|
16
|
+
Created: 2025-01-26
|
17
|
+
"""
|
18
|
+
|
19
|
+
import json
|
20
|
+
import subprocess
|
21
|
+
from datetime import datetime, timedelta
|
22
|
+
from pathlib import Path
|
23
|
+
from typing import Dict, List, Optional, Set, Tuple
|
24
|
+
|
25
|
+
from rich.console import Console
|
26
|
+
|
27
|
+
from claude_mpm.core.logging_utils import get_logger
|
28
|
+
logger = get_logger(__name__)
|
29
|
+
console = Console()
|
30
|
+
|
31
|
+
|
32
|
+
class EnhancedProjectAnalyzer:
|
33
|
+
"""Enhanced project analyzer with git history support."""
|
34
|
+
|
35
|
+
def __init__(self, project_path: Path):
|
36
|
+
"""Initialize the enhanced analyzer."""
|
37
|
+
self.project_path = project_path
|
38
|
+
self.is_git_repo = (project_path / ".git").exists()
|
39
|
+
|
40
|
+
def analyze_git_history(self, days_back: int = 30) -> Dict:
|
41
|
+
"""Analyze git history for recent changes and patterns."""
|
42
|
+
if not self.is_git_repo:
|
43
|
+
return {"git_available": False, "message": "Not a git repository"}
|
44
|
+
|
45
|
+
analysis = {
|
46
|
+
"git_available": True,
|
47
|
+
"recent_commits": self._get_recent_commits(days_back),
|
48
|
+
"changed_files": self._get_changed_files(days_back),
|
49
|
+
"authors": self._get_author_stats(days_back),
|
50
|
+
"branch_info": self._get_branch_info(),
|
51
|
+
"documentation_changes": self._get_documentation_changes(days_back),
|
52
|
+
}
|
53
|
+
|
54
|
+
# Analyze patterns
|
55
|
+
analysis["patterns"] = self._analyze_commit_patterns(analysis["recent_commits"])
|
56
|
+
analysis["hot_spots"] = self._identify_hot_spots(analysis["changed_files"])
|
57
|
+
|
58
|
+
return analysis
|
59
|
+
|
60
|
+
def _run_git_command(self, args: List[str]) -> Optional[str]:
|
61
|
+
"""Run a git command and return output."""
|
62
|
+
try:
|
63
|
+
result = subprocess.run(
|
64
|
+
["git"] + args,
|
65
|
+
cwd=self.project_path,
|
66
|
+
capture_output=True,
|
67
|
+
text=True,
|
68
|
+
check=True,
|
69
|
+
)
|
70
|
+
return result.stdout.strip()
|
71
|
+
except subprocess.CalledProcessError as e:
|
72
|
+
logger.debug(f"Git command failed: {e}")
|
73
|
+
return None
|
74
|
+
|
75
|
+
def _get_recent_commits(self, days: int) -> List[Dict]:
|
76
|
+
"""Get recent commits within specified days."""
|
77
|
+
since_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
78
|
+
|
79
|
+
# Get commit log with structured format
|
80
|
+
output = self._run_git_command([
|
81
|
+
"log",
|
82
|
+
f"--since={since_date}",
|
83
|
+
"--pretty=format:%H|%an|%ae|%at|%s",
|
84
|
+
"--no-merges",
|
85
|
+
])
|
86
|
+
|
87
|
+
if not output:
|
88
|
+
return []
|
89
|
+
|
90
|
+
commits = []
|
91
|
+
for line in output.splitlines():
|
92
|
+
parts = line.split("|", 4)
|
93
|
+
if len(parts) == 5:
|
94
|
+
commits.append({
|
95
|
+
"hash": parts[0][:8],
|
96
|
+
"author": parts[1],
|
97
|
+
"email": parts[2],
|
98
|
+
"timestamp": datetime.fromtimestamp(int(parts[3])).isoformat(),
|
99
|
+
"message": parts[4],
|
100
|
+
})
|
101
|
+
|
102
|
+
return commits[:50] # Limit to 50 most recent
|
103
|
+
|
104
|
+
def _get_changed_files(self, days: int) -> Dict:
|
105
|
+
"""Get files changed in recent commits."""
|
106
|
+
since_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
107
|
+
|
108
|
+
output = self._run_git_command([
|
109
|
+
"log",
|
110
|
+
f"--since={since_date}",
|
111
|
+
"--pretty=format:",
|
112
|
+
"--name-only",
|
113
|
+
"--no-merges",
|
114
|
+
])
|
115
|
+
|
116
|
+
if not output:
|
117
|
+
return {}
|
118
|
+
|
119
|
+
file_changes = {}
|
120
|
+
for line in output.splitlines():
|
121
|
+
if line:
|
122
|
+
file_changes[line] = file_changes.get(line, 0) + 1
|
123
|
+
|
124
|
+
# Sort by change frequency
|
125
|
+
sorted_files = sorted(file_changes.items(), key=lambda x: x[1], reverse=True)
|
126
|
+
|
127
|
+
return {
|
128
|
+
"total_files": len(file_changes),
|
129
|
+
"most_changed": dict(sorted_files[:20]), # Top 20 most changed
|
130
|
+
"recently_added": self._get_recently_added_files(days),
|
131
|
+
}
|
132
|
+
|
133
|
+
def _get_recently_added_files(self, days: int) -> List[str]:
|
134
|
+
"""Get files added in recent commits."""
|
135
|
+
since_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
136
|
+
|
137
|
+
output = self._run_git_command([
|
138
|
+
"log",
|
139
|
+
f"--since={since_date}",
|
140
|
+
"--pretty=format:",
|
141
|
+
"--name-status",
|
142
|
+
"--diff-filter=A", # Added files only
|
143
|
+
])
|
144
|
+
|
145
|
+
if not output:
|
146
|
+
return []
|
147
|
+
|
148
|
+
added_files = []
|
149
|
+
for line in output.splitlines():
|
150
|
+
if line.startswith("A\t"):
|
151
|
+
added_files.append(line[2:])
|
152
|
+
|
153
|
+
return list(set(added_files))[:20] # Unique files, max 20
|
154
|
+
|
155
|
+
def _get_author_stats(self, days: int) -> Dict:
|
156
|
+
"""Get author contribution statistics."""
|
157
|
+
since_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
158
|
+
|
159
|
+
output = self._run_git_command([
|
160
|
+
"shortlog",
|
161
|
+
"-sne",
|
162
|
+
f"--since={since_date}",
|
163
|
+
"--no-merges",
|
164
|
+
])
|
165
|
+
|
166
|
+
if not output:
|
167
|
+
return {}
|
168
|
+
|
169
|
+
authors = {}
|
170
|
+
for line in output.splitlines():
|
171
|
+
parts = line.strip().split("\t", 1)
|
172
|
+
if len(parts) == 2:
|
173
|
+
count = int(parts[0])
|
174
|
+
author_info = parts[1]
|
175
|
+
authors[author_info] = count
|
176
|
+
|
177
|
+
return {
|
178
|
+
"total_authors": len(authors),
|
179
|
+
"contributors": dict(list(authors.items())[:10]), # Top 10 contributors
|
180
|
+
}
|
181
|
+
|
182
|
+
def _get_branch_info(self) -> Dict:
|
183
|
+
"""Get current branch and remote information."""
|
184
|
+
info = {}
|
185
|
+
|
186
|
+
# Current branch
|
187
|
+
branch = self._run_git_command(["branch", "--show-current"])
|
188
|
+
info["current_branch"] = branch or "unknown"
|
189
|
+
|
190
|
+
# All branches
|
191
|
+
branches = self._run_git_command(["branch", "-a"])
|
192
|
+
if branches:
|
193
|
+
info["branches"] = [
|
194
|
+
b.strip().replace("* ", "") for b in branches.splitlines()
|
195
|
+
]
|
196
|
+
|
197
|
+
# Remote info
|
198
|
+
remotes = self._run_git_command(["remote", "-v"])
|
199
|
+
if remotes:
|
200
|
+
info["remotes"] = []
|
201
|
+
for line in remotes.splitlines():
|
202
|
+
parts = line.split()
|
203
|
+
if len(parts) >= 2:
|
204
|
+
info["remotes"].append({
|
205
|
+
"name": parts[0],
|
206
|
+
"url": parts[1],
|
207
|
+
})
|
208
|
+
|
209
|
+
# Check for uncommitted changes
|
210
|
+
status = self._run_git_command(["status", "--porcelain"])
|
211
|
+
info["has_uncommitted_changes"] = bool(status)
|
212
|
+
if status:
|
213
|
+
info["uncommitted_files"] = len(status.splitlines())
|
214
|
+
|
215
|
+
return info
|
216
|
+
|
217
|
+
def _get_documentation_changes(self, days: int) -> Dict:
|
218
|
+
"""Track changes to documentation files."""
|
219
|
+
since_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
220
|
+
|
221
|
+
# Get changes to documentation files
|
222
|
+
doc_patterns = ["*.md", "*.rst", "*.txt", "docs/*", "README*", "CLAUDE*"]
|
223
|
+
|
224
|
+
doc_changes = {}
|
225
|
+
for pattern in doc_patterns:
|
226
|
+
output = self._run_git_command([
|
227
|
+
"log",
|
228
|
+
f"--since={since_date}",
|
229
|
+
"--pretty=format:%H|%s",
|
230
|
+
"--", pattern,
|
231
|
+
])
|
232
|
+
|
233
|
+
if output:
|
234
|
+
for line in output.splitlines():
|
235
|
+
parts = line.split("|", 1)
|
236
|
+
if len(parts) == 2:
|
237
|
+
if pattern not in doc_changes:
|
238
|
+
doc_changes[pattern] = []
|
239
|
+
doc_changes[pattern].append({
|
240
|
+
"commit": parts[0][:8],
|
241
|
+
"message": parts[1],
|
242
|
+
})
|
243
|
+
|
244
|
+
# Check CLAUDE.md specifically
|
245
|
+
claude_history = self._run_git_command([
|
246
|
+
"log",
|
247
|
+
f"--since={since_date}",
|
248
|
+
"--pretty=format:%H|%at|%s",
|
249
|
+
"--", "CLAUDE.md",
|
250
|
+
])
|
251
|
+
|
252
|
+
claude_updates = []
|
253
|
+
if claude_history:
|
254
|
+
for line in claude_history.splitlines():
|
255
|
+
parts = line.split("|", 2)
|
256
|
+
if len(parts) == 3:
|
257
|
+
claude_updates.append({
|
258
|
+
"commit": parts[0][:8],
|
259
|
+
"timestamp": datetime.fromtimestamp(int(parts[1])).isoformat(),
|
260
|
+
"message": parts[2],
|
261
|
+
})
|
262
|
+
|
263
|
+
return {
|
264
|
+
"documentation_commits": doc_changes,
|
265
|
+
"claude_md_updates": claude_updates,
|
266
|
+
"has_recent_doc_changes": bool(doc_changes or claude_updates),
|
267
|
+
}
|
268
|
+
|
269
|
+
def _analyze_commit_patterns(self, commits: List[Dict]) -> Dict:
|
270
|
+
"""Analyze patterns in commit messages."""
|
271
|
+
patterns = {
|
272
|
+
"features": [],
|
273
|
+
"fixes": [],
|
274
|
+
"refactoring": [],
|
275
|
+
"documentation": [],
|
276
|
+
"tests": [],
|
277
|
+
"chores": [],
|
278
|
+
}
|
279
|
+
|
280
|
+
for commit in commits:
|
281
|
+
msg_lower = commit["message"].lower()
|
282
|
+
|
283
|
+
if any(kw in msg_lower for kw in ["feat", "feature", "add", "implement"]):
|
284
|
+
patterns["features"].append(commit["message"][:100])
|
285
|
+
elif any(kw in msg_lower for kw in ["fix", "bug", "resolve", "patch"]):
|
286
|
+
patterns["fixes"].append(commit["message"][:100])
|
287
|
+
elif any(kw in msg_lower for kw in ["refactor", "restructure", "reorganize"]):
|
288
|
+
patterns["refactoring"].append(commit["message"][:100])
|
289
|
+
elif any(kw in msg_lower for kw in ["doc", "readme", "comment"]):
|
290
|
+
patterns["documentation"].append(commit["message"][:100])
|
291
|
+
elif any(kw in msg_lower for kw in ["test", "spec", "coverage"]):
|
292
|
+
patterns["tests"].append(commit["message"][:100])
|
293
|
+
elif any(kw in msg_lower for kw in ["chore", "build", "ci", "deps"]):
|
294
|
+
patterns["chores"].append(commit["message"][:100])
|
295
|
+
|
296
|
+
# Limit each category to 5 items
|
297
|
+
for key in patterns:
|
298
|
+
patterns[key] = patterns[key][:5]
|
299
|
+
|
300
|
+
# Add summary
|
301
|
+
patterns["summary"] = {
|
302
|
+
"total_commits": len(commits),
|
303
|
+
"feature_commits": len(patterns["features"]),
|
304
|
+
"bug_fixes": len(patterns["fixes"]),
|
305
|
+
"most_active_type": max(
|
306
|
+
patterns.keys(),
|
307
|
+
key=lambda k: len(patterns[k]) if k != "summary" else 0,
|
308
|
+
),
|
309
|
+
}
|
310
|
+
|
311
|
+
return patterns
|
312
|
+
|
313
|
+
def _identify_hot_spots(self, changed_files: Dict) -> List[Dict]:
|
314
|
+
"""Identify hot spots (frequently changed files)."""
|
315
|
+
if not changed_files.get("most_changed"):
|
316
|
+
return []
|
317
|
+
|
318
|
+
hot_spots = []
|
319
|
+
for file_path, change_count in list(changed_files["most_changed"].items())[:10]:
|
320
|
+
file_type = Path(file_path).suffix
|
321
|
+
hot_spots.append({
|
322
|
+
"file": file_path,
|
323
|
+
"changes": change_count,
|
324
|
+
"type": file_type,
|
325
|
+
"category": self._categorize_file(file_path),
|
326
|
+
})
|
327
|
+
|
328
|
+
return hot_spots
|
329
|
+
|
330
|
+
def _categorize_file(self, file_path: str) -> str:
|
331
|
+
"""Categorize file based on path and extension."""
|
332
|
+
path = Path(file_path)
|
333
|
+
|
334
|
+
# Check directory
|
335
|
+
if "test" in str(path).lower():
|
336
|
+
return "test"
|
337
|
+
elif "docs" in str(path).lower():
|
338
|
+
return "documentation"
|
339
|
+
elif "src" in str(path) or "lib" in str(path):
|
340
|
+
return "source"
|
341
|
+
elif "scripts" in str(path):
|
342
|
+
return "scripts"
|
343
|
+
elif path.suffix in [".yml", ".yaml", ".json", ".toml", ".ini"]:
|
344
|
+
return "configuration"
|
345
|
+
elif path.suffix in [".md", ".rst", ".txt"]:
|
346
|
+
return "documentation"
|
347
|
+
else:
|
348
|
+
return "other"
|
349
|
+
|
350
|
+
def detect_project_state(self) -> Dict:
|
351
|
+
"""Detect the current state and lifecycle phase of the project."""
|
352
|
+
state = {
|
353
|
+
"phase": "unknown",
|
354
|
+
"indicators": [],
|
355
|
+
"recommendations": [],
|
356
|
+
}
|
357
|
+
|
358
|
+
# Check various indicators
|
359
|
+
indicators = []
|
360
|
+
|
361
|
+
# Check for version files
|
362
|
+
version_files = ["VERSION", "version.txt", "package.json", "pyproject.toml"]
|
363
|
+
for vf in version_files:
|
364
|
+
if (self.project_path / vf).exists():
|
365
|
+
indicators.append(f"Has {vf} file")
|
366
|
+
|
367
|
+
# Check for CI/CD
|
368
|
+
if (self.project_path / ".github" / "workflows").exists():
|
369
|
+
indicators.append("Has GitHub Actions")
|
370
|
+
if (self.project_path / ".gitlab-ci.yml").exists():
|
371
|
+
indicators.append("Has GitLab CI")
|
372
|
+
|
373
|
+
# Check for tests
|
374
|
+
if (self.project_path / "tests").exists() or (self.project_path / "test").exists():
|
375
|
+
indicators.append("Has test directory")
|
376
|
+
|
377
|
+
# Check for documentation
|
378
|
+
if (self.project_path / "docs").exists():
|
379
|
+
indicators.append("Has documentation directory")
|
380
|
+
if (self.project_path / "CLAUDE.md").exists():
|
381
|
+
indicators.append("Has CLAUDE.md")
|
382
|
+
|
383
|
+
# Check git history if available
|
384
|
+
if self.is_git_repo:
|
385
|
+
# Count total commits
|
386
|
+
commit_count = self._run_git_command(["rev-list", "--count", "HEAD"])
|
387
|
+
if commit_count:
|
388
|
+
count = int(commit_count)
|
389
|
+
indicators.append(f"{count} total commits")
|
390
|
+
|
391
|
+
# Determine phase based on commit count
|
392
|
+
if count < 10:
|
393
|
+
state["phase"] = "initial"
|
394
|
+
state["recommendations"].append("Focus on establishing core structure")
|
395
|
+
elif count < 50:
|
396
|
+
state["phase"] = "early_development"
|
397
|
+
state["recommendations"].append("Consider adding tests and documentation")
|
398
|
+
elif count < 200:
|
399
|
+
state["phase"] = "active_development"
|
400
|
+
state["recommendations"].append("Ensure CI/CD and testing are in place")
|
401
|
+
elif count < 1000:
|
402
|
+
state["phase"] = "maturing"
|
403
|
+
state["recommendations"].append("Focus on optimization and documentation")
|
404
|
+
else:
|
405
|
+
state["phase"] = "mature"
|
406
|
+
state["recommendations"].append("Maintain backward compatibility")
|
407
|
+
|
408
|
+
# Check age
|
409
|
+
first_commit = self._run_git_command([
|
410
|
+
"log", "--reverse", "--format=%at", "-1"
|
411
|
+
])
|
412
|
+
if first_commit:
|
413
|
+
age_days = (datetime.now() - datetime.fromtimestamp(int(first_commit))).days
|
414
|
+
indicators.append(f"{age_days} days old")
|
415
|
+
|
416
|
+
state["indicators"] = indicators
|
417
|
+
|
418
|
+
# Add phase-specific recommendations
|
419
|
+
if not (self.project_path / "CLAUDE.md").exists():
|
420
|
+
state["recommendations"].append("Create CLAUDE.md for AI agent documentation")
|
421
|
+
if not (self.project_path / "tests").exists():
|
422
|
+
state["recommendations"].append("Add tests directory for test organization")
|
423
|
+
if not (self.project_path / ".gitignore").exists():
|
424
|
+
state["recommendations"].append("Create .gitignore file")
|
425
|
+
|
426
|
+
return state
|
427
|
+
|
428
|
+
def generate_analysis_report(self, include_git: bool = True) -> Dict:
|
429
|
+
"""Generate comprehensive project analysis report."""
|
430
|
+
report = {
|
431
|
+
"project_path": str(self.project_path),
|
432
|
+
"timestamp": datetime.now().isoformat(),
|
433
|
+
}
|
434
|
+
|
435
|
+
# Basic project info
|
436
|
+
report["project_info"] = {
|
437
|
+
"name": self.project_path.name,
|
438
|
+
"is_git_repo": self.is_git_repo,
|
439
|
+
}
|
440
|
+
|
441
|
+
# Project state
|
442
|
+
report["state"] = self.detect_project_state()
|
443
|
+
|
444
|
+
# Git analysis if available
|
445
|
+
if include_git and self.is_git_repo:
|
446
|
+
report["git_analysis"] = self.analyze_git_history()
|
447
|
+
|
448
|
+
# File statistics
|
449
|
+
report["statistics"] = self._get_project_statistics()
|
450
|
+
|
451
|
+
return report
|
452
|
+
|
453
|
+
def _get_project_statistics(self) -> Dict:
|
454
|
+
"""Get basic project statistics."""
|
455
|
+
stats = {
|
456
|
+
"total_files": 0,
|
457
|
+
"total_directories": 0,
|
458
|
+
"file_types": {},
|
459
|
+
"largest_files": [],
|
460
|
+
}
|
461
|
+
|
462
|
+
# Count files and directories
|
463
|
+
for path in self.project_path.rglob("*"):
|
464
|
+
# Skip hidden and git files
|
465
|
+
if any(part.startswith(".") for part in path.parts):
|
466
|
+
continue
|
467
|
+
|
468
|
+
if path.is_file():
|
469
|
+
stats["total_files"] += 1
|
470
|
+
ext = path.suffix or "no_extension"
|
471
|
+
stats["file_types"][ext] = stats["file_types"].get(ext, 0) + 1
|
472
|
+
|
473
|
+
# Track large files
|
474
|
+
try:
|
475
|
+
size = path.stat().st_size
|
476
|
+
if size > 1024 * 1024: # Files over 1MB
|
477
|
+
stats["largest_files"].append({
|
478
|
+
"path": str(path.relative_to(self.project_path)),
|
479
|
+
"size_mb": round(size / (1024 * 1024), 2),
|
480
|
+
})
|
481
|
+
except (OSError, PermissionError):
|
482
|
+
pass
|
483
|
+
|
484
|
+
elif path.is_dir():
|
485
|
+
stats["total_directories"] += 1
|
486
|
+
|
487
|
+
# Sort largest files
|
488
|
+
stats["largest_files"].sort(key=lambda x: x["size_mb"], reverse=True)
|
489
|
+
stats["largest_files"] = stats["largest_files"][:10] # Top 10
|
490
|
+
|
491
|
+
return stats
|