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.
- soft_archmap-0.1.0/LICENSE +8 -0
- soft_archmap-0.1.0/PKG-INFO +91 -0
- soft_archmap-0.1.0/README.md +15 -0
- soft_archmap-0.1.0/pyproject.toml +92 -0
- soft_archmap-0.1.0/setup.cfg +4 -0
- soft_archmap-0.1.0/soft_archmap/__init__.py +0 -0
- soft_archmap-0.1.0/soft_archmap/adapters/__init__.py +0 -0
- soft_archmap-0.1.0/soft_archmap/adapters/python_adapter.py +198 -0
- soft_archmap-0.1.0/soft_archmap/analysis/__init__.py +0 -0
- soft_archmap-0.1.0/soft_archmap/analysis/cycles.py +13 -0
- soft_archmap-0.1.0/soft_archmap/analysis/health.py +15 -0
- soft_archmap-0.1.0/soft_archmap/analysis/impact.py +22 -0
- soft_archmap-0.1.0/soft_archmap/analysis/metrics.py +24 -0
- soft_archmap-0.1.0/soft_archmap/analysis/risk.py +33 -0
- soft_archmap-0.1.0/soft_archmap/analysis/top_risk.py +45 -0
- soft_archmap-0.1.0/soft_archmap/analysis/visualize.py +23 -0
- soft_archmap-0.1.0/soft_archmap/cli.py +225 -0
- soft_archmap-0.1.0/soft_archmap/core/__init__.py +0 -0
- soft_archmap-0.1.0/soft_archmap/core/graph.py +125 -0
- soft_archmap-0.1.0/soft_archmap/core/model.py +105 -0
- soft_archmap-0.1.0/soft_archmap/export/__init__.py +0 -0
- soft_archmap-0.1.0/soft_archmap/export/graphviz.py +92 -0
- soft_archmap-0.1.0/soft_archmap/export/json_export.py +31 -0
- soft_archmap-0.1.0/soft_archmap.egg-info/PKG-INFO +91 -0
- soft_archmap-0.1.0/soft_archmap.egg-info/SOURCES.txt +27 -0
- soft_archmap-0.1.0/soft_archmap.egg-info/dependency_links.txt +1 -0
- soft_archmap-0.1.0/soft_archmap.egg-info/entry_points.txt +2 -0
- soft_archmap-0.1.0/soft_archmap.egg-info/requires.txt +59 -0
- 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"
|
|
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}")
|