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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/INSTRUCTIONS.md +26 -1
- claude_mpm/agents/OUTPUT_STYLE.md +73 -0
- claude_mpm/agents/agents_metadata.py +57 -0
- claude_mpm/agents/templates/.claude-mpm/memories/README.md +17 -0
- claude_mpm/agents/templates/.claude-mpm/memories/engineer_memories.md +3 -0
- claude_mpm/agents/templates/agent-manager.json +263 -17
- claude_mpm/agents/templates/agent-manager.md +248 -10
- claude_mpm/agents/templates/agentic_coder_optimizer.json +222 -0
- claude_mpm/agents/templates/code_analyzer.json +18 -8
- claude_mpm/agents/templates/engineer.json +1 -1
- claude_mpm/agents/templates/logs/prompts/agent_engineer_20250826_014258_728.md +39 -0
- claude_mpm/agents/templates/qa.json +1 -1
- claude_mpm/agents/templates/research.json +1 -1
- claude_mpm/cli/__init__.py +4 -0
- claude_mpm/cli/commands/__init__.py +6 -0
- claude_mpm/cli/commands/analyze.py +547 -0
- claude_mpm/cli/commands/analyze_code.py +524 -0
- claude_mpm/cli/commands/configure.py +223 -25
- claude_mpm/cli/commands/configure_tui.py +65 -61
- claude_mpm/cli/commands/debug.py +1387 -0
- claude_mpm/cli/parsers/analyze_code_parser.py +170 -0
- claude_mpm/cli/parsers/analyze_parser.py +135 -0
- claude_mpm/cli/parsers/base_parser.py +29 -0
- claude_mpm/cli/parsers/configure_parser.py +23 -0
- claude_mpm/cli/parsers/debug_parser.py +319 -0
- claude_mpm/config/socketio_config.py +21 -21
- claude_mpm/constants.py +3 -1
- claude_mpm/core/framework_loader.py +148 -6
- claude_mpm/core/log_manager.py +16 -13
- claude_mpm/core/logger.py +1 -1
- claude_mpm/core/unified_agent_registry.py +1 -1
- claude_mpm/dashboard/.claude-mpm/socketio-instances.json +1 -0
- claude_mpm/dashboard/analysis_runner.py +428 -0
- claude_mpm/dashboard/static/built/components/activity-tree.js +2 -0
- claude_mpm/dashboard/static/built/components/agent-inference.js +1 -1
- claude_mpm/dashboard/static/built/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/built/components/file-tool-tracker.js +1 -1
- claude_mpm/dashboard/static/built/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/built/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/built/components/working-directory.js +1 -1
- claude_mpm/dashboard/static/built/dashboard.js +1 -1
- claude_mpm/dashboard/static/built/socket-client.js +1 -1
- claude_mpm/dashboard/static/css/activity.css +549 -0
- claude_mpm/dashboard/static/css/code-tree.css +846 -0
- claude_mpm/dashboard/static/css/dashboard.css +245 -0
- claude_mpm/dashboard/static/dist/components/activity-tree.js +2 -0
- claude_mpm/dashboard/static/dist/components/code-tree.js +2 -0
- claude_mpm/dashboard/static/dist/components/code-viewer.js +2 -0
- claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/dist/components/working-directory.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/activity-tree.js +1139 -0
- claude_mpm/dashboard/static/js/components/code-tree.js +1357 -0
- claude_mpm/dashboard/static/js/components/code-viewer.js +480 -0
- claude_mpm/dashboard/static/js/components/event-viewer.js +11 -0
- claude_mpm/dashboard/static/js/components/session-manager.js +40 -4
- claude_mpm/dashboard/static/js/components/socket-manager.js +12 -0
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +4 -0
- claude_mpm/dashboard/static/js/components/working-directory.js +17 -1
- claude_mpm/dashboard/static/js/dashboard.js +39 -0
- claude_mpm/dashboard/static/js/socket-client.js +414 -20
- claude_mpm/dashboard/templates/index.html +184 -4
- claude_mpm/hooks/claude_hooks/hook_handler.py +182 -5
- claude_mpm/hooks/claude_hooks/installer.py +728 -0
- claude_mpm/scripts/claude-hook-handler.sh +161 -0
- claude_mpm/scripts/socketio_daemon.py +121 -8
- claude_mpm/services/agents/deployment/agent_config_provider.py +127 -27
- claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +2 -2
- claude_mpm/services/agents/deployment/agent_record_service.py +1 -2
- claude_mpm/services/agents/memory/memory_format_service.py +1 -5
- claude_mpm/services/cli/agent_cleanup_service.py +1 -2
- claude_mpm/services/cli/agent_dependency_service.py +1 -1
- claude_mpm/services/cli/agent_validation_service.py +3 -4
- claude_mpm/services/cli/dashboard_launcher.py +2 -3
- claude_mpm/services/cli/startup_checker.py +0 -10
- claude_mpm/services/core/cache_manager.py +1 -2
- claude_mpm/services/core/path_resolver.py +1 -4
- claude_mpm/services/core/service_container.py +2 -2
- claude_mpm/services/diagnostics/checks/instructions_check.py +2 -5
- claude_mpm/services/event_bus/direct_relay.py +98 -20
- claude_mpm/services/infrastructure/monitoring/__init__.py +11 -11
- claude_mpm/services/infrastructure/monitoring.py +11 -11
- claude_mpm/services/project/architecture_analyzer.py +1 -1
- claude_mpm/services/project/dependency_analyzer.py +4 -4
- claude_mpm/services/project/language_analyzer.py +3 -3
- claude_mpm/services/project/metrics_collector.py +3 -6
- claude_mpm/services/socketio/handlers/__init__.py +2 -0
- claude_mpm/services/socketio/handlers/code_analysis.py +170 -0
- claude_mpm/services/socketio/handlers/registry.py +2 -0
- claude_mpm/services/socketio/server/connection_manager.py +95 -65
- claude_mpm/services/socketio/server/core.py +125 -17
- claude_mpm/services/socketio/server/main.py +44 -5
- claude_mpm/services/visualization/__init__.py +19 -0
- claude_mpm/services/visualization/mermaid_generator.py +938 -0
- claude_mpm/tools/__main__.py +208 -0
- claude_mpm/tools/code_tree_analyzer.py +778 -0
- claude_mpm/tools/code_tree_builder.py +632 -0
- claude_mpm/tools/code_tree_events.py +318 -0
- claude_mpm/tools/socketio_debug.py +671 -0
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/METADATA +1 -1
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/RECORD +108 -77
- claude_mpm/agents/schema/agent_schema.json +0 -314
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/licenses/LICENSE +0 -0
- {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("&", "&")
|
|
685
|
+
# Then escape other special characters
|
|
686
|
+
escaped = escaped.replace('"', '\\"')
|
|
687
|
+
escaped = escaped.replace("'", "\\'")
|
|
688
|
+
escaped = escaped.replace("`", "\\`")
|
|
689
|
+
escaped = escaped.replace("[", "[")
|
|
690
|
+
escaped = escaped.replace("]", "]")
|
|
691
|
+
escaped = escaped.replace("{", "{")
|
|
692
|
+
escaped = escaped.replace("}", "}")
|
|
693
|
+
escaped = escaped.replace("<", "<")
|
|
694
|
+
escaped = escaped.replace(">", ">")
|
|
695
|
+
escaped = escaped.replace("|", "|")
|
|
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)
|