soft-archmap 0.1.0__tar.gz

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 (29) hide show
  1. soft_archmap-0.1.0/LICENSE +8 -0
  2. soft_archmap-0.1.0/PKG-INFO +91 -0
  3. soft_archmap-0.1.0/README.md +15 -0
  4. soft_archmap-0.1.0/pyproject.toml +92 -0
  5. soft_archmap-0.1.0/setup.cfg +4 -0
  6. soft_archmap-0.1.0/soft_archmap/__init__.py +0 -0
  7. soft_archmap-0.1.0/soft_archmap/adapters/__init__.py +0 -0
  8. soft_archmap-0.1.0/soft_archmap/adapters/python_adapter.py +198 -0
  9. soft_archmap-0.1.0/soft_archmap/analysis/__init__.py +0 -0
  10. soft_archmap-0.1.0/soft_archmap/analysis/cycles.py +13 -0
  11. soft_archmap-0.1.0/soft_archmap/analysis/health.py +15 -0
  12. soft_archmap-0.1.0/soft_archmap/analysis/impact.py +22 -0
  13. soft_archmap-0.1.0/soft_archmap/analysis/metrics.py +24 -0
  14. soft_archmap-0.1.0/soft_archmap/analysis/risk.py +33 -0
  15. soft_archmap-0.1.0/soft_archmap/analysis/top_risk.py +45 -0
  16. soft_archmap-0.1.0/soft_archmap/analysis/visualize.py +23 -0
  17. soft_archmap-0.1.0/soft_archmap/cli.py +225 -0
  18. soft_archmap-0.1.0/soft_archmap/core/__init__.py +0 -0
  19. soft_archmap-0.1.0/soft_archmap/core/graph.py +125 -0
  20. soft_archmap-0.1.0/soft_archmap/core/model.py +105 -0
  21. soft_archmap-0.1.0/soft_archmap/export/__init__.py +0 -0
  22. soft_archmap-0.1.0/soft_archmap/export/graphviz.py +92 -0
  23. soft_archmap-0.1.0/soft_archmap/export/json_export.py +31 -0
  24. soft_archmap-0.1.0/soft_archmap.egg-info/PKG-INFO +91 -0
  25. soft_archmap-0.1.0/soft_archmap.egg-info/SOURCES.txt +27 -0
  26. soft_archmap-0.1.0/soft_archmap.egg-info/dependency_links.txt +1 -0
  27. soft_archmap-0.1.0/soft_archmap.egg-info/entry_points.txt +2 -0
  28. soft_archmap-0.1.0/soft_archmap.egg-info/requires.txt +59 -0
  29. soft_archmap-0.1.0/soft_archmap.egg-info/top_level.txt +1 -0
@@ -0,0 +1,8 @@
1
+ Copyright (c) 2026 Excited Nuclei Tech Labs
2
+ All rights reserved.
3
+
4
+ No part of this software may be reproduced, distributed, or transmitted in any form
5
+ or by any means, including photocopying, recording, or other electronic or mechanical
6
+ methods, without the prior written permission of Excited Nuclei Tech Labs.
7
+
8
+ For permission requests, contact: excitednuclei.techlabs@gmail.com
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: soft-archmap
3
+ Version: 0.1.0
4
+ Summary: Python tool to analyze architecture and dependencies in Python projects.
5
+ Author-email: Excited Nuclei Tech Labs <excitednuclei.techlabs@gmail.com>
6
+ Project-URL: Homepage, https://github.com/EN-Tech-Labs/ArchMap
7
+ Project-URL: Repository, https://github.com/EN-Tech-Labs/ArchMap
8
+ Project-URL: Documentation, https://github.com/EN-Tech-Labs/ArchMap
9
+ Keywords: architecture,dependency,python,analysis
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: License :: Other/Proprietary License
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: graphviz>=0.21
18
+ Requires-Dist: networkx>=3.6.1
19
+ Requires-Dist: pyvis>=0.3.2
20
+ Requires-Dist: rich>=15.0.0
21
+ Requires-Dist: requests>=2.33.1
22
+ Provides-Extra: dev
23
+ Requires-Dist: asttokens==3.0.1; extra == "dev"
24
+ Requires-Dist: build==1.5.0; extra == "dev"
25
+ Requires-Dist: certifi==2026.4.22; extra == "dev"
26
+ Requires-Dist: charset-normalizer==3.4.7; extra == "dev"
27
+ Requires-Dist: colorama==0.4.6; extra == "dev"
28
+ Requires-Dist: comm==0.2.3; extra == "dev"
29
+ Requires-Dist: debugpy==1.8.20; extra == "dev"
30
+ Requires-Dist: decorator==5.2.1; extra == "dev"
31
+ Requires-Dist: docutils==0.22.4; extra == "dev"
32
+ Requires-Dist: executing==2.2.1; extra == "dev"
33
+ Requires-Dist: id==1.6.1; extra == "dev"
34
+ Requires-Dist: idna==3.13; extra == "dev"
35
+ Requires-Dist: ipykernel==7.2.0; extra == "dev"
36
+ Requires-Dist: ipython==9.13.0; extra == "dev"
37
+ Requires-Dist: ipython_pygments_lexers==1.1.1; extra == "dev"
38
+ Requires-Dist: jaraco.classes==3.4.0; extra == "dev"
39
+ Requires-Dist: jaraco.context==6.1.2; extra == "dev"
40
+ Requires-Dist: jaraco.functools==4.4.0; extra == "dev"
41
+ Requires-Dist: jedi==0.19.2; extra == "dev"
42
+ Requires-Dist: Jinja2==3.1.6; extra == "dev"
43
+ Requires-Dist: jsonpickle==4.1.1; extra == "dev"
44
+ Requires-Dist: jupyter_client==8.8.0; extra == "dev"
45
+ Requires-Dist: jupyter_core==5.9.1; extra == "dev"
46
+ Requires-Dist: keyring==25.7.0; extra == "dev"
47
+ Requires-Dist: markdown-it-py==4.0.0; extra == "dev"
48
+ Requires-Dist: MarkupSafe==3.0.3; extra == "dev"
49
+ Requires-Dist: matplotlib-inline==0.2.1; extra == "dev"
50
+ Requires-Dist: mdurl==0.1.2; extra == "dev"
51
+ Requires-Dist: more-itertools==11.0.2; extra == "dev"
52
+ Requires-Dist: nest-asyncio==1.6.0; extra == "dev"
53
+ Requires-Dist: nh3==0.3.5; extra == "dev"
54
+ Requires-Dist: packaging==26.1; extra == "dev"
55
+ Requires-Dist: parso==0.8.6; extra == "dev"
56
+ Requires-Dist: platformdirs==4.9.6; extra == "dev"
57
+ Requires-Dist: prompt_toolkit==3.0.52; extra == "dev"
58
+ Requires-Dist: psutil==7.2.2; extra == "dev"
59
+ Requires-Dist: pure_eval==0.2.3; extra == "dev"
60
+ Requires-Dist: Pygments==2.20.0; extra == "dev"
61
+ Requires-Dist: pyproject_hooks==1.2.0; extra == "dev"
62
+ Requires-Dist: python-dateutil==2.9.0.post0; extra == "dev"
63
+ Requires-Dist: pywin32-ctypes==0.2.3; extra == "dev"
64
+ Requires-Dist: pyzmq==27.1.0; extra == "dev"
65
+ Requires-Dist: readme_renderer==44.0; extra == "dev"
66
+ Requires-Dist: requests-toolbelt==1.0.0; extra == "dev"
67
+ Requires-Dist: rfc3986==2.0.0; extra == "dev"
68
+ Requires-Dist: six==1.17.0; extra == "dev"
69
+ Requires-Dist: stack-data==0.6.3; extra == "dev"
70
+ Requires-Dist: tornado==6.5.5; extra == "dev"
71
+ Requires-Dist: traitlets==5.14.3; extra == "dev"
72
+ Requires-Dist: twine==6.2.0; extra == "dev"
73
+ Requires-Dist: urllib3==2.6.3; extra == "dev"
74
+ Requires-Dist: wcwidth==0.6.0; extra == "dev"
75
+ Dynamic: license-file
76
+
77
+ # ArchMap-Python
78
+
79
+ ArchMap-Python is a CLI tool by **Excited Nuclei Tech Labs** for analyzing Python software architecture. It helps developers understand dependencies, detect cycles, measure module health, compute risk, and visualize the architecture.
80
+
81
+ ## Features
82
+
83
+ - Dependency graph generation
84
+ - Cycle detection
85
+ - Health metrics
86
+ - Risk scoring of modules/functions
87
+ - Impact analysis
88
+ - Graphviz visualization and JSON export
89
+
90
+ ## Installation
91
+
@@ -0,0 +1,15 @@
1
+ # ArchMap-Python
2
+
3
+ ArchMap-Python is a CLI tool by **Excited Nuclei Tech Labs** for analyzing Python software architecture. It helps developers understand dependencies, detect cycles, measure module health, compute risk, and visualize the architecture.
4
+
5
+ ## Features
6
+
7
+ - Dependency graph generation
8
+ - Cycle detection
9
+ - Health metrics
10
+ - Risk scoring of modules/functions
11
+ - Impact analysis
12
+ - Graphviz visualization and JSON export
13
+
14
+ ## Installation
15
+
@@ -0,0 +1,92 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "soft-archmap" # must be lowercase, use '-' not '_'
7
+ version = "0.1.0"
8
+ description = "Python tool to analyze architecture and dependencies in Python projects."
9
+ readme = "README.md" # include README on PyPI
10
+ requires-python = ">=3.10"
11
+ authors = [
12
+ { name = "Excited Nuclei Tech Labs", email = "excitednuclei.techlabs@gmail.com" }
13
+ ]
14
+ license = { file = "LICENSE.txt" } # or use SPDX identifier if open source, e.g., "MIT"
15
+ keywords = ["architecture", "dependency", "python", "analysis"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Operating System :: OS Independent",
20
+ "License :: Other/Proprietary License"
21
+ ]
22
+ dependencies = [
23
+ "graphviz>=0.21",
24
+ "networkx>=3.6.1",
25
+ "pyvis>=0.3.2",
26
+ "rich>=15.0.0",
27
+ "requests>=2.33.1"
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ dev = [
32
+ "asttokens==3.0.1",
33
+ "build==1.5.0",
34
+ "certifi==2026.4.22",
35
+ "charset-normalizer==3.4.7",
36
+ "colorama==0.4.6",
37
+ "comm==0.2.3",
38
+ "debugpy==1.8.20",
39
+ "decorator==5.2.1",
40
+ "docutils==0.22.4",
41
+ "executing==2.2.1",
42
+ "id==1.6.1",
43
+ "idna==3.13",
44
+ "ipykernel==7.2.0",
45
+ "ipython==9.13.0",
46
+ "ipython_pygments_lexers==1.1.1",
47
+ "jaraco.classes==3.4.0",
48
+ "jaraco.context==6.1.2",
49
+ "jaraco.functools==4.4.0",
50
+ "jedi==0.19.2",
51
+ "Jinja2==3.1.6",
52
+ "jsonpickle==4.1.1",
53
+ "jupyter_client==8.8.0",
54
+ "jupyter_core==5.9.1",
55
+ "keyring==25.7.0",
56
+ "markdown-it-py==4.0.0",
57
+ "MarkupSafe==3.0.3",
58
+ "matplotlib-inline==0.2.1",
59
+ "mdurl==0.1.2",
60
+ "more-itertools==11.0.2",
61
+ "nest-asyncio==1.6.0",
62
+ "nh3==0.3.5",
63
+ "packaging==26.1",
64
+ "parso==0.8.6",
65
+ "platformdirs==4.9.6",
66
+ "prompt_toolkit==3.0.52",
67
+ "psutil==7.2.2",
68
+ "pure_eval==0.2.3",
69
+ "Pygments==2.20.0",
70
+ "pyproject_hooks==1.2.0",
71
+ "python-dateutil==2.9.0.post0",
72
+ "pywin32-ctypes==0.2.3",
73
+ "pyzmq==27.1.0",
74
+ "readme_renderer==44.0",
75
+ "requests-toolbelt==1.0.0",
76
+ "rfc3986==2.0.0",
77
+ "six==1.17.0",
78
+ "stack-data==0.6.3",
79
+ "tornado==6.5.5",
80
+ "traitlets==5.14.3",
81
+ "twine==6.2.0",
82
+ "urllib3==2.6.3",
83
+ "wcwidth==0.6.0"
84
+ ]
85
+
86
+ [project.scripts]
87
+ soft-archmap = "soft_archmap.cli:main"
88
+
89
+ [project.urls]
90
+ Homepage = "https://github.com/EN-Tech-Labs/ArchMap"
91
+ Repository = "https://github.com/EN-Tech-Labs/ArchMap"
92
+ Documentation = "https://github.com/EN-Tech-Labs/ArchMap"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
File without changes
@@ -0,0 +1,198 @@
1
+ import ast
2
+ import os
3
+ from soft_archmap.core.model import Entity, Relation
4
+
5
+ class PythonParser:
6
+ def __init__(self, repo_path: str, model):
7
+ self.repo_path = repo_path
8
+ self.model = model
9
+
10
+ self.current_module = None
11
+ self.current_class = None
12
+ self.current_function = None
13
+
14
+ # NEW: track import aliases
15
+ self.import_aliases = {}
16
+
17
+ # ----------------------------
18
+ # PUBLIC
19
+ # ----------------------------
20
+ def parse_file(self, file_path: str):
21
+ if any(part in {"venv", ".venv", "env", ".env"} for part in file_path.split(os.sep)):
22
+ return
23
+
24
+ with open(file_path, "r", encoding="utf-8") as f:
25
+ print(f"Parsing {file_path}...")
26
+ source = f.read()
27
+
28
+ try:
29
+ tree = ast.parse(source, filename=file_path)
30
+ except SyntaxError as e:
31
+ print(f"Skipping {file_path} due to syntax error: {e}")
32
+ return
33
+
34
+ module_name = os.path.relpath(file_path, self.repo_path) \
35
+ .replace(os.sep, ".") \
36
+ .replace(".py", "")
37
+
38
+ self.current_module = module_name
39
+ module_id = f"module:{module_name}"
40
+
41
+ # Ensure module entity exists
42
+ self._ensure_entity(module_id, "module", module_name, file_path)
43
+
44
+ for node in tree.body:
45
+
46
+ # -------- Imports --------
47
+ if isinstance(node, ast.Import):
48
+ for alias in node.names:
49
+ real_name = alias.name
50
+ alias_name = alias.asname or alias.name
51
+
52
+ self.import_aliases[alias_name] = real_name
53
+ self._add_import(module_id, real_name)
54
+
55
+ elif isinstance(node, ast.ImportFrom):
56
+ if node.module:
57
+ for alias in node.names:
58
+ alias_name = alias.asname or alias.name
59
+ full_name = f"{node.module}.{alias.name}"
60
+ self.import_aliases[alias_name] = full_name
61
+
62
+ self._add_import(module_id, node.module)
63
+
64
+ # -------- Classes --------
65
+ elif isinstance(node, ast.ClassDef):
66
+ self._handle_class(node, module_id, module_name, file_path)
67
+
68
+ # -------- Top-level Functions --------
69
+ elif isinstance(node, ast.FunctionDef):
70
+ self._handle_function(
71
+ node, module_id, module_name, file_path, parent_class=None
72
+ )
73
+
74
+ # ----------------------------
75
+ # ENTITY HELPERS
76
+ # ----------------------------
77
+ def _ensure_entity(self, entity_id, entity_type, name, file):
78
+ if entity_id not in self.model.entities:
79
+ self.model.add_entity(Entity(
80
+ id=entity_id,
81
+ type=entity_type,
82
+ name=name,
83
+ file=file
84
+ ))
85
+
86
+ def _ensure_external_entity(self, entity_id):
87
+ if entity_id not in self.model.entities:
88
+ self.model.add_entity(Entity(
89
+ id=entity_id,
90
+ type="external",
91
+ name=entity_id.split(":", 1)[-1],
92
+ file="external"
93
+ ))
94
+
95
+ def _add_relation(self, src, dst, rel_type):
96
+ self._ensure_external_entity(dst)
97
+
98
+ self.model.add_relation(Relation(
99
+ src=src,
100
+ dst=dst,
101
+ type=rel_type
102
+ ))
103
+
104
+ # ----------------------------
105
+ # IMPORTS
106
+ # ----------------------------
107
+ def _add_import(self, module_id: str, imported_module: str):
108
+ target_id = f"module:{imported_module}"
109
+ self._add_relation(module_id, target_id, "imports")
110
+
111
+ # ----------------------------
112
+ # CLASS HANDLER
113
+ # ----------------------------
114
+ def _handle_class(self, node, module_id, module_name, file_path):
115
+ class_name = node.name
116
+ qualname = f"{module_name}.{class_name}"
117
+ class_id = f"class:{qualname}"
118
+
119
+ self._ensure_entity(class_id, "class", qualname, file_path)
120
+
121
+ self._add_relation(module_id, class_id, "contains")
122
+
123
+ # -------- Inheritance --------
124
+ for base in node.bases:
125
+ if isinstance(base, ast.Name):
126
+ base_id = f"class:{base.id}"
127
+ self._add_relation(class_id, base_id, "inherits")
128
+
129
+ prev_class = self.current_class
130
+ self.current_class = class_name
131
+
132
+ for item in node.body:
133
+ if isinstance(item, ast.FunctionDef):
134
+ self._handle_function(
135
+ item, module_id, module_name, file_path, parent_class=class_name
136
+ )
137
+
138
+ self.current_class = prev_class
139
+
140
+ # ----------------------------
141
+ # FUNCTION / METHOD HANDLER
142
+ # ----------------------------
143
+ def _handle_function(self, node, module_id, module_name, file_path, parent_class):
144
+ func_name = node.name
145
+
146
+ if parent_class:
147
+ qualname = f"{module_name}.{parent_class}.{func_name}"
148
+ func_id = f"function:{qualname}"
149
+ parent_id = f"class:{module_name}.{parent_class}"
150
+ entity_type = "method"
151
+ else:
152
+ qualname = f"{module_name}.{func_name}"
153
+ func_id = f"function:{qualname}"
154
+ parent_id = module_id
155
+ entity_type = "function"
156
+
157
+ self._ensure_entity(func_id, entity_type, qualname, file_path)
158
+
159
+ self._add_relation(parent_id, func_id, "contains")
160
+
161
+ # -------- Calls --------
162
+ prev_function = self.current_function
163
+ self.current_function = func_id
164
+
165
+ for child in ast.walk(node):
166
+ if isinstance(child, ast.Call):
167
+ called_name = self._resolve_call(child.func)
168
+ if called_name:
169
+ target_id = f"function:{called_name}"
170
+ self._add_relation(func_id, target_id, "calls")
171
+
172
+ self.current_function = prev_function
173
+
174
+ # ----------------------------
175
+ # CALL RESOLUTION (IMPROVED)
176
+ # ----------------------------
177
+ def _resolve_call(self, node):
178
+ # foo()
179
+ if isinstance(node, ast.Name):
180
+ return self._resolve_alias(node.id)
181
+
182
+ # obj.method()
183
+ elif isinstance(node, ast.Attribute):
184
+ parts = []
185
+ while isinstance(node, ast.Attribute):
186
+ parts.append(node.attr)
187
+ node = node.value
188
+
189
+ if isinstance(node, ast.Name):
190
+ base = self._resolve_alias(node.id)
191
+ parts.append(base)
192
+
193
+ return ".".join(reversed(parts))
194
+
195
+ return None
196
+
197
+ def _resolve_alias(self, name):
198
+ return self.import_aliases.get(name, name)
File without changes
@@ -0,0 +1,13 @@
1
+ # analysis/cycles.py
2
+ from soft_archmap.core.graph import DependencyGraph
3
+
4
+ def detect_cycles(model):
5
+ """
6
+ Detect cycles in architecture.
7
+ Returns a list of cycles (list of entity IDs).
8
+ """
9
+ graph = DependencyGraph()
10
+ for r in model.relations:
11
+ graph.add_edge(r.src, r.dst)
12
+
13
+ return graph.find_cycles()
@@ -0,0 +1,15 @@
1
+ # analysis/health.py
2
+ from soft_archmap.core.graph import DependencyGraph
3
+
4
+ def compute_health(model):
5
+ """
6
+ Simple health metric: ratio of used entities to total entities.
7
+ """
8
+ graph = DependencyGraph()
9
+ for r in model.relations:
10
+ graph.add_edge(r.src, r.dst)
11
+
12
+ total = len(model.entities)
13
+ unused = len([e for e in model.entities.values() if not graph.dependents(e.id)])
14
+ score = (total - unused) / total if total else 1.0
15
+ return round(score, 2)
@@ -0,0 +1,22 @@
1
+ from collections import defaultdict, deque
2
+ from soft_archmap.core.graph import DependencyGraph
3
+
4
+ class ImpactAnalyzer:
5
+ def __init__(self, model):
6
+ # Build a dependency graph from the model
7
+ self.graph = DependencyGraph()
8
+ self.graph.build_from_model(model)
9
+
10
+ def analyze(self, target):
11
+ visited = set()
12
+ result = []
13
+
14
+ def dfs(node):
15
+ for dep in self.graph.dependents(node):
16
+ if dep not in visited:
17
+ visited.add(dep)
18
+ result.append(dep)
19
+ dfs(dep)
20
+
21
+ dfs(target)
22
+ return result
@@ -0,0 +1,24 @@
1
+ # analysis/metrics.py
2
+ from soft_archmap.core.graph import DependencyGraph
3
+
4
+ def compute_metrics(model):
5
+ """
6
+ Compute simple architecture metrics.
7
+ """
8
+ graph = DependencyGraph()
9
+ for r in model.relations:
10
+ graph.add_edge(r.src, r.dst)
11
+
12
+ num_modules = sum(1 for e in model.entities.values() if e.type == "module")
13
+ num_classes = sum(1 for e in model.entities.values() if e.type == "class")
14
+ num_functions = sum(1 for e in model.entities.values() if e.type == "function")
15
+ num_relations = len(model.relations)
16
+ num_cycles = len(graph.find_cycles())
17
+
18
+ return {
19
+ "modules": num_modules,
20
+ "classes": num_classes,
21
+ "functions": num_functions,
22
+ "relations": num_relations,
23
+ "cycles": num_cycles
24
+ }
@@ -0,0 +1,33 @@
1
+ class RiskEngine:
2
+ def __init__(self, graph):
3
+ self.graph = graph
4
+
5
+ # -------------------------
6
+ # CORE RISK SCORE
7
+ # -------------------------
8
+ def compute_risk(self, node_id):
9
+ direct = len(self.graph.dependents(node_id))
10
+
11
+ # indirect risk (2-hop approximation)
12
+ indirect = 0
13
+ for child in self.graph.dependents(node_id):
14
+ indirect += len(self.graph.dependents(child))
15
+
16
+ # cycle penalty
17
+ cycles = self._in_cycle(node_id)
18
+ cycle_penalty = 2 if cycles else 0
19
+
20
+ risk_score = (
21
+ direct * 0.6 +
22
+ indirect * 0.2 +
23
+ cycle_penalty * 0.2
24
+ )
25
+
26
+ return round(risk_score, 3)
27
+
28
+ # -------------------------
29
+ # CHECK IF NODE IN CYCLE
30
+ # -------------------------
31
+ def _in_cycle(self, node_id):
32
+ cycles = self.graph.find_cycles()
33
+ return any(node_id in cycle for cycle in cycles)
@@ -0,0 +1,45 @@
1
+ from soft_archmap.analysis.risk import RiskEngine
2
+
3
+
4
+ # class TopRiskAnalyzer:
5
+ # def __init__(self, graph):
6
+ # self.graph = graph
7
+ # self.risk_engine = RiskEngine(graph)
8
+
9
+ # def get_top_risk_modules(self, top_k=10):
10
+ # scores = []
11
+
12
+ # for node in self.graph.nodes():
13
+ # score = self.risk_engine.compute_risk(node)
14
+ # scores.append((node, score))
15
+
16
+ # # sort by risk descending
17
+ # scores.sort(key=lambda x: x[1], reverse=True)
18
+
19
+ # return {
20
+ # "top_risk": scores[:top_k]
21
+ # }
22
+
23
+ class TopRiskAnalyzer:
24
+ def __init__(self, model):
25
+ self.model = model
26
+ self.graph = model.graph
27
+ self.engine = RiskEngine(self.graph)
28
+
29
+ def get_top_risk_modules(self):
30
+ results = []
31
+
32
+ for node_id, entity in self.model.entities.items():
33
+
34
+ # 🚫 SKIP EXTERNAL CODE HERE
35
+ if entity.type == "external":
36
+ continue
37
+
38
+ score = self.engine.compute_risk(node_id)
39
+ results.append((node_id, score))
40
+
41
+ results.sort(key=lambda x: x[1], reverse=True)
42
+
43
+ return {
44
+ "top_risk": results[:10]
45
+ }
@@ -0,0 +1,23 @@
1
+ # analysis/visualize.py
2
+ import os
3
+ import subprocess
4
+ import shutil
5
+
6
+ def visualize_architecture(dot_file, output_file="architecture.png"):
7
+ """
8
+ Generate PNG from Graphviz DOT file.
9
+ Requires 'dot' (Graphviz) installed.
10
+ """
11
+ # png_path = os.path.join(output_dir, "architecture.png")
12
+ if not shutil.which("dot"):
13
+ print("Graphviz 'dot' not found. Install Graphviz to generate visualization.")
14
+ return
15
+
16
+ try:
17
+ subprocess.run(
18
+ ["dot", "-Tpng", dot_file, "-o", output_file],
19
+ check=True
20
+ )
21
+ print(f"✅ Architecture visualization saved to {output_file}")
22
+ except subprocess.CalledProcessError as e:
23
+ print(f"Error generating visualization: {e}")