claude-mpm 4.1.7__py3-none-any.whl → 4.1.10__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 (109) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +26 -1
  3. claude_mpm/agents/OUTPUT_STYLE.md +73 -0
  4. claude_mpm/agents/agents_metadata.py +57 -0
  5. claude_mpm/agents/templates/.claude-mpm/memories/README.md +17 -0
  6. claude_mpm/agents/templates/.claude-mpm/memories/engineer_memories.md +3 -0
  7. claude_mpm/agents/templates/agent-manager.json +263 -17
  8. claude_mpm/agents/templates/agent-manager.md +248 -10
  9. claude_mpm/agents/templates/agentic_coder_optimizer.json +222 -0
  10. claude_mpm/agents/templates/code_analyzer.json +18 -8
  11. claude_mpm/agents/templates/engineer.json +1 -1
  12. claude_mpm/agents/templates/logs/prompts/agent_engineer_20250826_014258_728.md +39 -0
  13. claude_mpm/agents/templates/qa.json +1 -1
  14. claude_mpm/agents/templates/research.json +1 -1
  15. claude_mpm/cli/__init__.py +4 -0
  16. claude_mpm/cli/commands/__init__.py +6 -0
  17. claude_mpm/cli/commands/analyze.py +547 -0
  18. claude_mpm/cli/commands/analyze_code.py +524 -0
  19. claude_mpm/cli/commands/configure.py +223 -25
  20. claude_mpm/cli/commands/configure_tui.py +65 -61
  21. claude_mpm/cli/commands/debug.py +1387 -0
  22. claude_mpm/cli/parsers/analyze_code_parser.py +170 -0
  23. claude_mpm/cli/parsers/analyze_parser.py +135 -0
  24. claude_mpm/cli/parsers/base_parser.py +29 -0
  25. claude_mpm/cli/parsers/configure_parser.py +23 -0
  26. claude_mpm/cli/parsers/debug_parser.py +319 -0
  27. claude_mpm/config/socketio_config.py +21 -21
  28. claude_mpm/constants.py +3 -1
  29. claude_mpm/core/framework_loader.py +148 -6
  30. claude_mpm/core/log_manager.py +16 -13
  31. claude_mpm/core/logger.py +1 -1
  32. claude_mpm/core/unified_agent_registry.py +1 -1
  33. claude_mpm/dashboard/.claude-mpm/socketio-instances.json +1 -0
  34. claude_mpm/dashboard/analysis_runner.py +428 -0
  35. claude_mpm/dashboard/static/built/components/activity-tree.js +2 -0
  36. claude_mpm/dashboard/static/built/components/agent-inference.js +1 -1
  37. claude_mpm/dashboard/static/built/components/event-viewer.js +1 -1
  38. claude_mpm/dashboard/static/built/components/file-tool-tracker.js +1 -1
  39. claude_mpm/dashboard/static/built/components/module-viewer.js +1 -1
  40. claude_mpm/dashboard/static/built/components/session-manager.js +1 -1
  41. claude_mpm/dashboard/static/built/components/working-directory.js +1 -1
  42. claude_mpm/dashboard/static/built/dashboard.js +1 -1
  43. claude_mpm/dashboard/static/built/socket-client.js +1 -1
  44. claude_mpm/dashboard/static/css/activity.css +549 -0
  45. claude_mpm/dashboard/static/css/code-tree.css +846 -0
  46. claude_mpm/dashboard/static/css/dashboard.css +245 -0
  47. claude_mpm/dashboard/static/dist/components/activity-tree.js +2 -0
  48. claude_mpm/dashboard/static/dist/components/code-tree.js +2 -0
  49. claude_mpm/dashboard/static/dist/components/code-viewer.js +2 -0
  50. claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
  51. claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
  52. claude_mpm/dashboard/static/dist/components/working-directory.js +1 -1
  53. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  54. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  55. claude_mpm/dashboard/static/js/components/activity-tree.js +1139 -0
  56. claude_mpm/dashboard/static/js/components/code-tree.js +1357 -0
  57. claude_mpm/dashboard/static/js/components/code-viewer.js +480 -0
  58. claude_mpm/dashboard/static/js/components/event-viewer.js +11 -0
  59. claude_mpm/dashboard/static/js/components/session-manager.js +40 -4
  60. claude_mpm/dashboard/static/js/components/socket-manager.js +12 -0
  61. claude_mpm/dashboard/static/js/components/ui-state-manager.js +4 -0
  62. claude_mpm/dashboard/static/js/components/working-directory.js +17 -1
  63. claude_mpm/dashboard/static/js/dashboard.js +39 -0
  64. claude_mpm/dashboard/static/js/socket-client.js +414 -20
  65. claude_mpm/dashboard/templates/index.html +184 -4
  66. claude_mpm/hooks/claude_hooks/hook_handler.py +182 -5
  67. claude_mpm/hooks/claude_hooks/installer.py +728 -0
  68. claude_mpm/scripts/claude-hook-handler.sh +161 -0
  69. claude_mpm/scripts/socketio_daemon.py +121 -8
  70. claude_mpm/services/agents/deployment/agent_config_provider.py +127 -27
  71. claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +2 -2
  72. claude_mpm/services/agents/deployment/agent_record_service.py +1 -2
  73. claude_mpm/services/agents/memory/memory_format_service.py +1 -5
  74. claude_mpm/services/cli/agent_cleanup_service.py +1 -2
  75. claude_mpm/services/cli/agent_dependency_service.py +1 -1
  76. claude_mpm/services/cli/agent_validation_service.py +3 -4
  77. claude_mpm/services/cli/dashboard_launcher.py +2 -3
  78. claude_mpm/services/cli/startup_checker.py +0 -10
  79. claude_mpm/services/core/cache_manager.py +1 -2
  80. claude_mpm/services/core/path_resolver.py +1 -4
  81. claude_mpm/services/core/service_container.py +2 -2
  82. claude_mpm/services/diagnostics/checks/instructions_check.py +2 -5
  83. claude_mpm/services/event_bus/direct_relay.py +98 -20
  84. claude_mpm/services/infrastructure/monitoring/__init__.py +11 -11
  85. claude_mpm/services/infrastructure/monitoring.py +11 -11
  86. claude_mpm/services/project/architecture_analyzer.py +1 -1
  87. claude_mpm/services/project/dependency_analyzer.py +4 -4
  88. claude_mpm/services/project/language_analyzer.py +3 -3
  89. claude_mpm/services/project/metrics_collector.py +3 -6
  90. claude_mpm/services/socketio/handlers/__init__.py +2 -0
  91. claude_mpm/services/socketio/handlers/code_analysis.py +170 -0
  92. claude_mpm/services/socketio/handlers/registry.py +2 -0
  93. claude_mpm/services/socketio/server/connection_manager.py +95 -65
  94. claude_mpm/services/socketio/server/core.py +125 -17
  95. claude_mpm/services/socketio/server/main.py +44 -5
  96. claude_mpm/services/visualization/__init__.py +19 -0
  97. claude_mpm/services/visualization/mermaid_generator.py +938 -0
  98. claude_mpm/tools/__main__.py +208 -0
  99. claude_mpm/tools/code_tree_analyzer.py +778 -0
  100. claude_mpm/tools/code_tree_builder.py +632 -0
  101. claude_mpm/tools/code_tree_events.py +318 -0
  102. claude_mpm/tools/socketio_debug.py +671 -0
  103. {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/METADATA +1 -1
  104. {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/RECORD +108 -77
  105. claude_mpm/agents/schema/agent_schema.json +0 -314
  106. {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/WHEEL +0 -0
  107. {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/entry_points.txt +0 -0
  108. {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/licenses/LICENSE +0 -0
  109. {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,938 @@
1
+ """
2
+ Mermaid Diagram Generator Service for Claude MPM
3
+ ================================================
4
+
5
+ This service generates Mermaid diagrams for code visualization based on
6
+ analysis results from the Code Analyzer agent.
7
+
8
+ WHY: Visual representations of code structure help developers understand
9
+ complex codebases more quickly. Mermaid diagrams can be rendered in
10
+ documentation and provide interactive exploration capabilities.
11
+
12
+ DESIGN DECISION: We support multiple diagram types (entry points, module
13
+ dependencies, class hierarchies, and call graphs) to cover different
14
+ aspects of code structure analysis.
15
+ """
16
+
17
+ import re
18
+ from dataclasses import dataclass
19
+ from enum import Enum
20
+ from pathlib import Path
21
+ from typing import Any, Dict, List, Optional, Set, Tuple
22
+
23
+ from claude_mpm.services.core.base import SyncBaseService
24
+
25
+
26
+ class DiagramType(Enum):
27
+ """Supported Mermaid diagram types for code visualization."""
28
+
29
+ ENTRY_POINTS = "entry_points"
30
+ MODULE_DEPS = "module_deps"
31
+ CLASS_HIERARCHY = "class_hierarchy"
32
+ CALL_GRAPH = "call_graph"
33
+
34
+
35
+ @dataclass
36
+ class DiagramConfig:
37
+ """Configuration for diagram generation."""
38
+
39
+ title: Optional[str] = None
40
+ direction: str = "TB" # Top-Bottom by default
41
+ theme: str = "default"
42
+ max_depth: int = 5
43
+ include_external: bool = False
44
+ show_parameters: bool = True
45
+ show_return_types: bool = True
46
+
47
+
48
+ class MermaidGeneratorService(SyncBaseService):
49
+ """
50
+ Service for generating Mermaid diagrams from code analysis results.
51
+
52
+ This service provides methods to generate various types of diagrams
53
+ including entry points, module dependencies, class hierarchies, and
54
+ call graphs.
55
+ """
56
+
57
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
58
+ """Initialize the Mermaid generator service."""
59
+ super().__init__(service_name="MermaidGeneratorService", config=config)
60
+ self._node_id_counter = 0
61
+ self._node_id_cache: Dict[str, str] = {}
62
+ self._reserved_keywords = {
63
+ "graph",
64
+ "subgraph",
65
+ "end",
66
+ "class",
67
+ "classDef",
68
+ "click",
69
+ "style",
70
+ "linkStyle",
71
+ "interpolate",
72
+ "flowchart",
73
+ "pie",
74
+ "sequenceDiagram",
75
+ "gantt",
76
+ "stateDiagram",
77
+ "erDiagram",
78
+ "journey",
79
+ "gitGraph",
80
+ "mindmap",
81
+ "timeline",
82
+ "quadrantChart",
83
+ "sankey",
84
+ "xychart",
85
+ "block",
86
+ "start",
87
+ "stop",
88
+ "operation",
89
+ "subroutine",
90
+ "condition",
91
+ "inputoutput",
92
+ "parallel",
93
+ "database",
94
+ }
95
+
96
+ def initialize(self) -> bool:
97
+ """Initialize the service."""
98
+ try:
99
+ self.log_info("Initializing MermaidGeneratorService")
100
+ self._initialized = True
101
+ return True
102
+ except Exception as e:
103
+ self.log_error(f"Failed to initialize: {e}")
104
+ return False
105
+
106
+ def shutdown(self) -> None:
107
+ """Shutdown the service."""
108
+ self.log_info("Shutting down MermaidGeneratorService")
109
+ self._node_id_cache.clear()
110
+ self._shutdown = True
111
+
112
+ def generate_diagram(
113
+ self,
114
+ diagram_type: DiagramType,
115
+ analysis_results: Dict[str, Any],
116
+ config: Optional[DiagramConfig] = None,
117
+ ) -> str:
118
+ """
119
+ Generate a Mermaid diagram based on analysis results.
120
+
121
+ Args:
122
+ diagram_type: Type of diagram to generate
123
+ analysis_results: Code analysis results from Code Analyzer agent
124
+ config: Optional configuration for diagram generation
125
+
126
+ Returns:
127
+ Mermaid diagram syntax as string
128
+
129
+ Raises:
130
+ ValueError: If diagram type is not supported or data is invalid
131
+ """
132
+ if not self._initialized:
133
+ raise RuntimeError("Service not initialized")
134
+
135
+ config = config or DiagramConfig()
136
+
137
+ # Reset node ID cache for each new diagram
138
+ self._node_id_cache.clear()
139
+ self._node_id_counter = 0
140
+
141
+ try:
142
+ if diagram_type == DiagramType.ENTRY_POINTS:
143
+ return self._generate_entry_points_diagram(analysis_results, config)
144
+ if diagram_type == DiagramType.MODULE_DEPS:
145
+ return self._generate_module_deps_diagram(analysis_results, config)
146
+ if diagram_type == DiagramType.CLASS_HIERARCHY:
147
+ return self._generate_class_hierarchy_diagram(analysis_results, config)
148
+ if diagram_type == DiagramType.CALL_GRAPH:
149
+ return self._generate_call_graph_diagram(analysis_results, config)
150
+ raise ValueError(f"Unsupported diagram type: {diagram_type}")
151
+ except Exception as e:
152
+ # Handle both DiagramType enums and plain strings
153
+ type_name = (
154
+ diagram_type.value
155
+ if hasattr(diagram_type, "value")
156
+ else str(diagram_type)
157
+ )
158
+ self.log_error(f"Failed to generate {type_name} diagram: {e}")
159
+ raise
160
+
161
+ def _generate_entry_points_diagram(
162
+ self, analysis_results: Dict[str, Any], config: DiagramConfig
163
+ ) -> str:
164
+ """Generate entry points flow diagram."""
165
+ lines = []
166
+ title = config.title or "Application Entry Points"
167
+
168
+ # Start diagram
169
+ lines.append(f"flowchart {config.direction}")
170
+ lines.append(f" %% {title}")
171
+ lines.append("")
172
+
173
+ # Get entry points from analysis results
174
+ entry_points = analysis_results.get("entry_points", {})
175
+
176
+ if not entry_points:
177
+ lines.append(" NoEntryPoints[No entry points found]")
178
+ return "\n".join(lines)
179
+
180
+ # Create start node
181
+ lines.append(" Start([Application Start])")
182
+ lines.append("")
183
+
184
+ # Process each entry point
185
+ for entry_type, entries in entry_points.items():
186
+ if not entries:
187
+ continue
188
+
189
+ # Create subgraph for each entry type
190
+ subgraph_id = self._sanitize_node_id(f"subgraph_{entry_type}")
191
+ lines.append(
192
+ f" subgraph {subgraph_id}[{entry_type.replace('_', ' ').title()}]"
193
+ )
194
+
195
+ for entry in entries:
196
+ if isinstance(entry, dict):
197
+ file_path = entry.get("file", "")
198
+ func_name = entry.get("function", "main")
199
+ line_num = entry.get("line", 0)
200
+
201
+ # Handle None or invalid file paths
202
+ if not file_path or not isinstance(file_path, (str, Path)):
203
+ file_path = "unknown"
204
+
205
+ # Create node for entry point
206
+ node_id = self._get_node_id(f"{file_path}:{func_name}")
207
+ try:
208
+ path_name = (
209
+ Path(file_path).name
210
+ if file_path != "unknown"
211
+ else "unknown"
212
+ )
213
+ except (TypeError, OSError):
214
+ path_name = "unknown"
215
+ node_label = self._escape_label(f"{path_name}::{func_name}")
216
+
217
+ if line_num:
218
+ node_label += f" (L{line_num})"
219
+
220
+ lines.append(f" {node_id}[{node_label}]")
221
+
222
+ lines.append(" end")
223
+ lines.append("")
224
+
225
+ # Connect start to entry points
226
+ for entry_type, entries in entry_points.items():
227
+ if entries:
228
+ for entry in entries[:3]: # Limit connections to avoid clutter
229
+ if isinstance(entry, dict):
230
+ file_path = entry.get("file", "")
231
+ func_name = entry.get("function", "main")
232
+
233
+ # Handle None or invalid file paths
234
+ if not file_path or not isinstance(file_path, (str, Path)):
235
+ file_path = "unknown"
236
+
237
+ node_id = self._get_node_id(f"{file_path}:{func_name}")
238
+ lines.append(f" Start --> {node_id}")
239
+
240
+ # Add styling
241
+ lines.append("")
242
+ lines.append(
243
+ " classDef entryPoint fill:#90EE90,stroke:#333,stroke-width:2px"
244
+ )
245
+ lines.append(" classDef startNode fill:#FFD700,stroke:#333,stroke-width:3px")
246
+ lines.append(" class Start startNode")
247
+
248
+ return "\n".join(lines)
249
+
250
+ def _generate_module_deps_diagram(
251
+ self, analysis_results: Dict[str, Any], config: DiagramConfig
252
+ ) -> str:
253
+ """Generate module dependency diagram."""
254
+ lines = []
255
+ title = config.title or "Module Dependencies"
256
+
257
+ # Start diagram
258
+ lines.append(f"flowchart {config.direction}")
259
+ lines.append(f" %% {title}")
260
+ lines.append("")
261
+
262
+ # Get dependencies from analysis results
263
+ dependencies = analysis_results.get("dependencies", {})
264
+ imports_data = analysis_results.get("imports", {})
265
+
266
+ # Handle None values
267
+ if dependencies is None:
268
+ dependencies = {}
269
+ if imports_data is None:
270
+ imports_data = {}
271
+
272
+ # Check if we have any meaningful data
273
+ has_deps = bool(dependencies and any(v for v in dependencies.values() if v))
274
+ has_imports = bool(imports_data and any(v for v in imports_data.values() if v))
275
+
276
+ if not has_deps and not has_imports:
277
+ lines.append(" NoDeps[No dependencies found]")
278
+ return "\n".join(lines)
279
+
280
+ # Track all modules and their relationships
281
+ modules: Set[str] = set()
282
+ edges: List[Tuple[str, str, str]] = [] # (from, to, label)
283
+
284
+ # Process dependencies
285
+ for module, deps in dependencies.items():
286
+ module_name = self._extract_module_name(module)
287
+ modules.add(module_name)
288
+
289
+ if isinstance(deps, list):
290
+ for dep in deps:
291
+ dep_name = self._extract_module_name(str(dep))
292
+ if not config.include_external and self._is_external_module(
293
+ dep_name
294
+ ):
295
+ continue
296
+ modules.add(dep_name)
297
+ edges.append((module_name, dep_name, "depends"))
298
+
299
+ # Process imports
300
+ for file_path, imports in imports_data.items():
301
+ module_name = self._extract_module_name(file_path)
302
+ modules.add(module_name)
303
+
304
+ if isinstance(imports, list):
305
+ for imp in imports:
306
+ if isinstance(imp, dict):
307
+ import_from = imp.get("from", imp.get("module", ""))
308
+ if import_from:
309
+ import_name = self._extract_module_name(import_from)
310
+ if (
311
+ not config.include_external
312
+ and self._is_external_module(import_name)
313
+ ):
314
+ continue
315
+ modules.add(import_name)
316
+ edges.append((module_name, import_name, "imports"))
317
+
318
+ # Create nodes for all modules
319
+ external_modules = set()
320
+ internal_modules = set()
321
+
322
+ for module in modules:
323
+ node_id = self._get_node_id(module)
324
+ node_label = self._escape_label(module)
325
+
326
+ if self._is_external_module(module):
327
+ lines.append(f" {node_id}[({node_label})]")
328
+ external_modules.add(node_id)
329
+ else:
330
+ lines.append(f" {node_id}[{node_label}]")
331
+ internal_modules.add(node_id)
332
+
333
+ lines.append("")
334
+
335
+ # Create edges
336
+ for from_module, to_module, rel_type in edges:
337
+ from_id = self._get_node_id(from_module)
338
+ to_id = self._get_node_id(to_module)
339
+
340
+ if rel_type == "imports":
341
+ lines.append(f" {from_id} --> {to_id}")
342
+ else:
343
+ lines.append(f" {from_id} -.-> {to_id}")
344
+
345
+ # Add styling
346
+ lines.append("")
347
+ lines.append(" classDef internal fill:#87CEEB,stroke:#333,stroke-width:2px")
348
+ lines.append(" classDef external fill:#FFB6C1,stroke:#333,stroke-width:1px")
349
+
350
+ if internal_modules:
351
+ lines.append(f" class {','.join(internal_modules)} internal")
352
+ if external_modules:
353
+ lines.append(f" class {','.join(external_modules)} external")
354
+
355
+ return "\n".join(lines)
356
+
357
+ def _generate_class_hierarchy_diagram(
358
+ self, analysis_results: Dict[str, Any], config: DiagramConfig
359
+ ) -> str:
360
+ """Generate class hierarchy diagram."""
361
+ lines = []
362
+ title = config.title or "Class Hierarchy"
363
+
364
+ # Start diagram
365
+ lines.append("classDiagram")
366
+ lines.append(f" %% {title}")
367
+ lines.append("")
368
+
369
+ # Get classes from analysis results
370
+ classes = analysis_results.get("classes", {})
371
+
372
+ if not classes:
373
+ lines.append(" class NoClasses {")
374
+ lines.append(" <<placeholder>>")
375
+ lines.append(" No classes found")
376
+ lines.append(" }")
377
+ return "\n".join(lines)
378
+
379
+ # Process each class
380
+ for class_name, class_info in classes.items():
381
+ if not isinstance(class_info, dict):
382
+ continue
383
+
384
+ safe_name = self._sanitize_class_name(class_name)
385
+
386
+ # Define class
387
+ lines.append(f" class {safe_name} {{")
388
+
389
+ # Add class type annotation if it's special
390
+ if class_info.get("is_abstract"):
391
+ lines.append(" <<abstract>>")
392
+ elif class_info.get("is_interface"):
393
+ lines.append(" <<interface>>")
394
+ elif class_info.get("is_enum"):
395
+ lines.append(" <<enumeration>>")
396
+
397
+ # Add attributes
398
+ attributes = class_info.get("attributes", [])
399
+ if attributes:
400
+ for attr in attributes[:10]: # Limit to avoid clutter
401
+ if isinstance(attr, dict):
402
+ attr_name = attr.get("name", "")
403
+ attr_type = attr.get("type", "Any")
404
+ visibility = attr.get("visibility", "+")
405
+ if config.show_return_types:
406
+ lines.append(
407
+ f" {visibility}{attr_name}: {attr_type}"
408
+ )
409
+ else:
410
+ lines.append(f" {visibility}{attr_name}")
411
+ elif isinstance(attr, str):
412
+ lines.append(f" +{attr}")
413
+
414
+ # Add methods
415
+ methods = class_info.get("methods", [])
416
+ if methods:
417
+ for method in methods[:10]: # Limit to avoid clutter
418
+ if isinstance(method, dict):
419
+ method_name = method.get("name", "")
420
+ params = method.get("parameters", [])
421
+ return_type = method.get("return_type", "None")
422
+ visibility = method.get("visibility", "+")
423
+
424
+ if config.show_parameters and params:
425
+ param_str = (
426
+ ", ".join(params)
427
+ if isinstance(params, list)
428
+ else str(params)
429
+ )
430
+ method_sig = f"{method_name}({param_str})"
431
+ else:
432
+ method_sig = f"{method_name}()"
433
+
434
+ if config.show_return_types:
435
+ lines.append(
436
+ f" {visibility}{method_sig}: {return_type}"
437
+ )
438
+ else:
439
+ lines.append(f" {visibility}{method_sig}")
440
+ elif isinstance(method, str):
441
+ lines.append(f" +{method}()")
442
+
443
+ lines.append(" }")
444
+ lines.append("")
445
+
446
+ # Add relationships
447
+ for class_name, class_info in classes.items():
448
+ if not isinstance(class_info, dict):
449
+ continue
450
+
451
+ safe_name = self._sanitize_class_name(class_name)
452
+
453
+ # Inheritance relationships
454
+ bases = class_info.get("bases", [])
455
+ if bases:
456
+ for base in bases:
457
+ if isinstance(base, str):
458
+ safe_base = self._sanitize_class_name(base)
459
+ if safe_base in [self._sanitize_class_name(c) for c in classes]:
460
+ lines.append(f" {safe_base} <|-- {safe_name}")
461
+
462
+ # Composition relationships
463
+ compositions = class_info.get("compositions", [])
464
+ if compositions:
465
+ for comp in compositions:
466
+ if isinstance(comp, str):
467
+ safe_comp = self._sanitize_class_name(comp)
468
+ if safe_comp in [self._sanitize_class_name(c) for c in classes]:
469
+ lines.append(f" {safe_name} *-- {safe_comp}")
470
+
471
+ # Association relationships
472
+ associations = class_info.get("associations", [])
473
+ if associations:
474
+ for assoc in associations:
475
+ if isinstance(assoc, str):
476
+ safe_assoc = self._sanitize_class_name(assoc)
477
+ if safe_assoc in [
478
+ self._sanitize_class_name(c) for c in classes
479
+ ]:
480
+ lines.append(f" {safe_name} --> {safe_assoc}")
481
+
482
+ return "\n".join(lines)
483
+
484
+ def _generate_call_graph_diagram(
485
+ self, analysis_results: Dict[str, Any], config: DiagramConfig
486
+ ) -> str:
487
+ """Generate function call graph diagram."""
488
+ lines = []
489
+ title = config.title or "Function Call Graph"
490
+
491
+ # Start diagram
492
+ lines.append(f"flowchart {config.direction}")
493
+ lines.append(f" %% {title}")
494
+ lines.append("")
495
+
496
+ # Get functions and call relationships
497
+ functions = analysis_results.get("functions", {})
498
+ call_graph = analysis_results.get("call_graph", {})
499
+
500
+ if not functions and not call_graph:
501
+ lines.append(" NoFunctions[No functions found]")
502
+ return "\n".join(lines)
503
+
504
+ # Track all functions and their calls
505
+ all_functions: Set[str] = set()
506
+ calls: List[Tuple[str, str, Optional[str]]] = [] # (caller, callee, label)
507
+
508
+ # Process call graph
509
+ for caller, callees in call_graph.items():
510
+ all_functions.add(caller)
511
+
512
+ if isinstance(callees, list):
513
+ for callee in callees:
514
+ if isinstance(callee, dict):
515
+ func_name = callee.get("function", callee.get("name", ""))
516
+ if func_name:
517
+ all_functions.add(func_name)
518
+ call_count = callee.get("count", 0)
519
+ label = str(call_count) if call_count > 1 else None
520
+ calls.append((caller, func_name, label))
521
+ elif isinstance(callee, str):
522
+ all_functions.add(callee)
523
+ calls.append((caller, callee, None))
524
+
525
+ # Process functions to add those not in call graph
526
+ for func_name, func_info in functions.items():
527
+ all_functions.add(func_name)
528
+
529
+ # Check for calls within function
530
+ if isinstance(func_info, dict):
531
+ func_calls = func_info.get("calls", [])
532
+ for call in func_calls:
533
+ if isinstance(call, str):
534
+ all_functions.add(call)
535
+ calls.append((func_name, call, None))
536
+
537
+ # Create nodes for all functions
538
+ entry_functions = set()
539
+ regular_functions = set()
540
+
541
+ for func in all_functions:
542
+ node_id = self._get_node_id(func)
543
+ node_label = self._escape_label(func)
544
+
545
+ # Check if this is an entry point
546
+ is_entry = func in ["main", "__main__", "run", "start", "execute"]
547
+
548
+ if is_entry:
549
+ lines.append(f" {node_id}[/{node_label}/]")
550
+ entry_functions.add(node_id)
551
+ # Check if function has specific characteristics
552
+ elif func.startswith("_") and not func.startswith("__"):
553
+ lines.append(f" {node_id}[{node_label}]:::private")
554
+ elif func.startswith("__") and func.endswith("__"):
555
+ lines.append(f" {node_id}[{node_label}]:::magic")
556
+ else:
557
+ lines.append(f" {node_id}[{node_label}]")
558
+ regular_functions.add(node_id)
559
+
560
+ lines.append("")
561
+
562
+ # Create edges for calls
563
+ for caller, callee, label in calls:
564
+ caller_id = self._get_node_id(caller)
565
+ callee_id = self._get_node_id(callee)
566
+
567
+ if label:
568
+ lines.append(f" {caller_id} -->|{label}| {callee_id}")
569
+ else:
570
+ lines.append(f" {caller_id} --> {callee_id}")
571
+
572
+ # Add styling
573
+ lines.append("")
574
+ lines.append(" classDef entry fill:#90EE90,stroke:#333,stroke-width:3px")
575
+ lines.append(" classDef regular fill:#87CEEB,stroke:#333,stroke-width:2px")
576
+ lines.append(" classDef private fill:#FFE4B5,stroke:#333,stroke-width:1px")
577
+ lines.append(" classDef magic fill:#DDA0DD,stroke:#333,stroke-width:2px")
578
+
579
+ if entry_functions:
580
+ lines.append(f" class {','.join(entry_functions)} entry")
581
+ if regular_functions:
582
+ lines.append(f" class {','.join(regular_functions)} regular")
583
+
584
+ return "\n".join(lines)
585
+
586
+ def _sanitize_node_id(self, identifier: str) -> str:
587
+ """
588
+ Sanitize an identifier to be a valid Mermaid node ID.
589
+
590
+ Args:
591
+ identifier: Raw identifier string
592
+
593
+ Returns:
594
+ Sanitized identifier safe for use as node ID
595
+ """
596
+ # Replace common problematic characters
597
+ sanitized = identifier.replace(".", "_")
598
+ sanitized = sanitized.replace("/", "_")
599
+ sanitized = sanitized.replace("\\", "_")
600
+ sanitized = sanitized.replace("-", "_")
601
+ sanitized = sanitized.replace(" ", "_")
602
+ sanitized = sanitized.replace(":", "_")
603
+ sanitized = sanitized.replace("(", "_")
604
+ sanitized = sanitized.replace(")", "_")
605
+ sanitized = sanitized.replace("[", "_")
606
+ sanitized = sanitized.replace("]", "_")
607
+ sanitized = sanitized.replace("{", "_")
608
+ sanitized = sanitized.replace("}", "_")
609
+ sanitized = sanitized.replace("<", "_")
610
+ sanitized = sanitized.replace(">", "_")
611
+ sanitized = sanitized.replace(",", "_")
612
+ sanitized = sanitized.replace(";", "_")
613
+ sanitized = sanitized.replace("'", "_")
614
+ sanitized = sanitized.replace('"', "_")
615
+ sanitized = sanitized.replace("`", "_")
616
+ sanitized = sanitized.replace("@", "_")
617
+ sanitized = sanitized.replace("#", "_")
618
+ sanitized = sanitized.replace("$", "_")
619
+ sanitized = sanitized.replace("%", "_")
620
+ sanitized = sanitized.replace("^", "_")
621
+ sanitized = sanitized.replace("&", "_")
622
+ sanitized = sanitized.replace("*", "_")
623
+ sanitized = sanitized.replace("+", "_")
624
+ sanitized = sanitized.replace("=", "_")
625
+ sanitized = sanitized.replace("|", "_")
626
+ sanitized = sanitized.replace("~", "_")
627
+ sanitized = sanitized.replace("!", "_")
628
+ sanitized = sanitized.replace("?", "_")
629
+
630
+ # Remove consecutive underscores
631
+ sanitized = re.sub(r"_+", "_", sanitized)
632
+
633
+ # Remove leading/trailing underscores
634
+ sanitized = sanitized.strip("_")
635
+
636
+ # Ensure it doesn't start with a number
637
+ if sanitized and sanitized[0].isdigit():
638
+ sanitized = f"n_{sanitized}"
639
+
640
+ # Ensure it's not empty
641
+ if not sanitized:
642
+ sanitized = "node"
643
+
644
+ # Check against reserved keywords
645
+ if sanitized.lower() in self._reserved_keywords:
646
+ sanitized = f"{sanitized}_node"
647
+
648
+ return sanitized
649
+
650
+ def _sanitize_class_name(self, class_name: str) -> str:
651
+ """
652
+ Sanitize a class name for use in class diagrams.
653
+
654
+ Args:
655
+ class_name: Raw class name
656
+
657
+ Returns:
658
+ Sanitized class name
659
+ """
660
+ # For class diagrams, we need simpler names
661
+ sanitized = re.sub(r"[^a-zA-Z0-9_]", "", class_name)
662
+
663
+ # Ensure it doesn't start with a number
664
+ if sanitized and sanitized[0].isdigit():
665
+ sanitized = f"C_{sanitized}"
666
+
667
+ # Ensure it's not empty
668
+ if not sanitized:
669
+ sanitized = "Class"
670
+
671
+ return sanitized
672
+
673
+ def _escape_label(self, label: str) -> str:
674
+ """
675
+ Escape special characters in labels for Mermaid.
676
+
677
+ Args:
678
+ label: Raw label text
679
+
680
+ Returns:
681
+ Escaped label safe for Mermaid
682
+ """
683
+ # Escape ampersand first to avoid double-escaping
684
+ escaped = label.replace("&", "&amp;")
685
+ # Then escape other special characters
686
+ escaped = escaped.replace('"', '\\"')
687
+ escaped = escaped.replace("'", "\\'")
688
+ escaped = escaped.replace("`", "\\`")
689
+ escaped = escaped.replace("[", "&#91;")
690
+ escaped = escaped.replace("]", "&#93;")
691
+ escaped = escaped.replace("{", "&#123;")
692
+ escaped = escaped.replace("}", "&#125;")
693
+ escaped = escaped.replace("<", "&lt;")
694
+ escaped = escaped.replace(">", "&gt;")
695
+ escaped = escaped.replace("|", "&#124;")
696
+
697
+ # Limit length to avoid overly long labels
698
+ if len(escaped) > 50:
699
+ escaped = escaped[:47] + "..."
700
+
701
+ return escaped
702
+
703
+ def _get_node_id(self, identifier: str) -> str:
704
+ """
705
+ Get or create a unique node ID for an identifier.
706
+
707
+ Args:
708
+ identifier: Original identifier
709
+
710
+ Returns:
711
+ Unique node ID
712
+ """
713
+ # Always return the cached ID for the same identifier
714
+ if identifier in self._node_id_cache:
715
+ return self._node_id_cache[identifier]
716
+
717
+ # Generate a unique ID
718
+ sanitized = self._sanitize_node_id(identifier)
719
+
720
+ # Ensure uniqueness across all generated IDs
721
+ base_id = sanitized
722
+ counter = 1
723
+ used_ids = set(self._node_id_cache.values())
724
+ while sanitized in used_ids:
725
+ sanitized = f"{base_id}_{counter}"
726
+ counter += 1
727
+
728
+ self._node_id_cache[identifier] = sanitized
729
+ return sanitized
730
+
731
+ def _extract_module_name(self, path_or_module: str) -> str:
732
+ """
733
+ Extract a clean module name from a file path or module string.
734
+
735
+ Args:
736
+ path_or_module: File path or module name
737
+
738
+ Returns:
739
+ Clean module name
740
+ """
741
+ # Remove common file extensions
742
+ clean = re.sub(r"\.(py|js|ts|java|cpp|c|h|hpp)$", "", path_or_module)
743
+
744
+ # Convert path separators to dots
745
+ clean = clean.replace("/", ".")
746
+ clean = clean.replace("\\", ".")
747
+
748
+ # Remove leading dots
749
+ clean = clean.lstrip(".")
750
+
751
+ # Remove common prefixes
752
+ prefixes_to_remove = ["src.", "lib.", "pkg.", "app.", "modules."]
753
+ for prefix in prefixes_to_remove:
754
+ if clean.startswith(prefix):
755
+ clean = clean[len(prefix) :]
756
+
757
+ # Take last few components if too long
758
+ parts = clean.split(".")
759
+ if len(parts) > 3:
760
+ clean = ".".join(parts[-3:])
761
+
762
+ return clean or "module"
763
+
764
+ def _is_external_module(self, module_name: str) -> bool:
765
+ """
766
+ Check if a module is external (third-party).
767
+
768
+ Args:
769
+ module_name: Module name to check
770
+
771
+ Returns:
772
+ True if module appears to be external
773
+ """
774
+ # Common external/system module patterns
775
+ external_patterns = [
776
+ r"^(sys|os|re|json|typing|pathlib|datetime|collections|itertools)",
777
+ r"^(numpy|pandas|matplotlib|scipy|sklearn|tensorflow|torch|keras)",
778
+ r"^(requests|urllib|http|flask|django|fastapi|aiohttp)",
779
+ r"^(pytest|unittest|mock|coverage)",
780
+ r"^(logging|warnings|traceback|inspect)",
781
+ r"^(asyncio|threading|multiprocessing|concurrent)",
782
+ r"^(boto3|azure|google|aws)",
783
+ ]
784
+
785
+ for pattern in external_patterns:
786
+ if re.match(pattern, module_name):
787
+ return True
788
+
789
+ # Check for version numbers (common in external packages)
790
+ if re.search(r"\d+\.\d+", module_name):
791
+ return True
792
+
793
+ # Check for common external package naming patterns
794
+ # Single-level modules without dots are often external, but not always
795
+ if module_name.count(".") == 0:
796
+ # Check if it's a known stdlib module or starts with underscore (private)
797
+ if module_name.startswith("_") and not module_name.startswith("__"):
798
+ return True
799
+ # Don't assume all single-level modules are external
800
+ # Let specific pattern matching above handle known external modules
801
+
802
+ return False
803
+
804
+ def validate_mermaid_syntax(self, diagram: str) -> Tuple[bool, Optional[str]]:
805
+ """
806
+ Validate that the generated Mermaid syntax is correct.
807
+
808
+ Args:
809
+ diagram: Mermaid diagram syntax to validate
810
+
811
+ Returns:
812
+ Tuple of (is_valid, error_message)
813
+ """
814
+ try:
815
+ if not diagram or not diagram.strip():
816
+ return False, "Empty diagram"
817
+
818
+ lines = diagram.strip().split("\n")
819
+
820
+ if not lines:
821
+ return False, "Empty diagram"
822
+
823
+ # Find the first non-comment line for diagram type validation
824
+ first_content_line = None
825
+ for line in lines:
826
+ stripped = line.strip()
827
+ if stripped and not stripped.startswith("%%"):
828
+ first_content_line = stripped
829
+ break
830
+
831
+ if not first_content_line:
832
+ return False, "No content found after comments"
833
+
834
+ # Check for valid diagram type declaration
835
+ valid_starts = [
836
+ "graph",
837
+ "flowchart",
838
+ "sequenceDiagram",
839
+ "classDiagram",
840
+ "stateDiagram",
841
+ "erDiagram",
842
+ "gantt",
843
+ "pie",
844
+ "journey",
845
+ "gitGraph",
846
+ "mindmap",
847
+ "timeline",
848
+ "quadrantChart",
849
+ "sankey",
850
+ "xychart",
851
+ "block",
852
+ ]
853
+
854
+ if not any(first_content_line.startswith(start) for start in valid_starts):
855
+ return False, f"Invalid diagram type: {first_content_line}"
856
+
857
+ # Check for balanced brackets and quotes
858
+ open_brackets = diagram.count("[")
859
+ close_brackets = diagram.count("]")
860
+ if open_brackets != close_brackets:
861
+ return (
862
+ False,
863
+ f"Unbalanced brackets: {open_brackets} open, {close_brackets} close",
864
+ )
865
+
866
+ # Check for balanced parentheses
867
+ open_parens = diagram.count("(")
868
+ close_parens = diagram.count(")")
869
+ if open_parens != close_parens:
870
+ return (
871
+ False,
872
+ f"Unbalanced parentheses: {open_parens} open, {close_parens} close",
873
+ )
874
+
875
+ # Check for balanced braces in class diagrams
876
+ if "classDiagram" in first_content_line:
877
+ open_braces = diagram.count("{")
878
+ close_braces = diagram.count("}")
879
+ if open_braces != close_braces:
880
+ return (
881
+ False,
882
+ f"Unbalanced braces: {open_braces} open, {close_braces} close",
883
+ )
884
+
885
+ # Check for valid subgraph blocks
886
+ if "subgraph" in diagram:
887
+ # Only count subgraph declarations (with space after)
888
+ subgraph_pattern = r"\bsubgraph\s+"
889
+ subgraph_count = len(re.findall(subgraph_pattern, diagram))
890
+
891
+ # Count all 'end' statements that close blocks
892
+ end_pattern = r"^\s*end\s*$"
893
+ end_count = len(re.findall(end_pattern, diagram, re.MULTILINE))
894
+
895
+ if subgraph_count > end_count:
896
+ return (
897
+ False,
898
+ f"Unmatched subgraph blocks: {subgraph_count} subgraphs, {end_count} ends",
899
+ )
900
+
901
+ return True, None
902
+
903
+ except Exception as e:
904
+ return False, f"Validation error: {e!s}"
905
+
906
+ def format_diagram_with_metadata(
907
+ self, diagram: str, metadata: Optional[Dict[str, Any]] = None
908
+ ) -> str:
909
+ """
910
+ Format a diagram with metadata comments.
911
+
912
+ Args:
913
+ diagram: Mermaid diagram syntax
914
+ metadata: Optional metadata to include
915
+
916
+ Returns:
917
+ Formatted diagram with metadata
918
+ """
919
+ lines = []
920
+
921
+ # Add metadata as comments if provided
922
+ if metadata:
923
+ lines.append("%% Diagram Metadata")
924
+ lines.append(f"%% Generated: {metadata.get('timestamp', 'Unknown')}")
925
+ lines.append(f"%% Source: {metadata.get('source', 'Code Analysis')}")
926
+ lines.append(f"%% Type: {metadata.get('type', 'Unknown')}")
927
+
928
+ if "stats" in metadata:
929
+ lines.append("%% Statistics:")
930
+ for key, value in metadata["stats"].items():
931
+ lines.append(f"%% {key}: {value}")
932
+
933
+ lines.append("")
934
+
935
+ # Add the diagram
936
+ lines.append(diagram)
937
+
938
+ return "\n".join(lines)