interlinked-mapper 0.2.0__tar.gz → 0.3.1__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.
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/PKG-INFO +5 -1
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/analyzer/__init__.py +2 -2
- interlinked_mapper-0.3.1/interlinked/analyzer/dead_code.py +202 -0
- interlinked_mapper-0.3.1/interlinked/analyzer/embeddings.py +547 -0
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/analyzer/graph.py +179 -10
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/analyzer/parser.py +205 -7
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/analyzer/similarity.py +393 -60
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/commander/query.py +92 -9
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/mcp_server.py +183 -9
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/models.py +14 -1
- interlinked_mapper-0.3.1/interlinked/visualizer/frontend/index.html +13 -0
- interlinked_mapper-0.3.1/interlinked/visualizer/frontend/package-lock.json +1806 -0
- interlinked_mapper-0.3.1/interlinked/visualizer/frontend/package.json +27 -0
- interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/App.tsx +1094 -0
- interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/graph/GraphCanvas.tsx +570 -0
- interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/graph/nodePrograms.ts +242 -0
- interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/index.css +940 -0
- interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/main.tsx +10 -0
- interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/state/graphStore.ts +212 -0
- interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/state/sseClient.ts +37 -0
- interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/theme.ts +152 -0
- interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/types.ts +83 -0
- interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/vite-env.d.ts +1 -0
- interlinked_mapper-0.3.1/interlinked/visualizer/frontend/tsconfig.json +21 -0
- interlinked_mapper-0.3.1/interlinked/visualizer/frontend/vite.config.ts +15 -0
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/visualizer/layouts.py +23 -22
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/visualizer/server.py +355 -31
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked_mapper.egg-info/PKG-INFO +5 -1
- interlinked_mapper-0.3.1/interlinked_mapper.egg-info/SOURCES.txt +40 -0
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked_mapper.egg-info/requires.txt +5 -0
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/pyproject.toml +5 -1
- interlinked_mapper-0.2.0/interlinked/analyzer/dead_code.py +0 -137
- interlinked_mapper-0.2.0/interlinked_mapper.egg-info/SOURCES.txt +0 -24
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/__init__.py +0 -0
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/cli.py +0 -0
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/commander/__init__.py +0 -0
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/commander/llm.py +0 -0
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/commander/repl.py +0 -0
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/visualizer/__init__.py +0 -0
- /interlinked_mapper-0.2.0/interlinked/visualizer/frontend/index.html → /interlinked_mapper-0.3.1/interlinked/visualizer/frontend/index.html.d3-legacy +0 -0
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked_mapper.egg-info/dependency_links.txt +0 -0
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked_mapper.egg-info/entry_points.txt +0 -0
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked_mapper.egg-info/top_level.txt +0 -0
- {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: interlinked-mapper
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A Python program topology explorer — visualize the shape of your codebase
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/austerecryptid/interlinked
|
|
@@ -24,3 +24,7 @@ Requires-Dist: uvicorn[standard]>=0.29
|
|
|
24
24
|
Requires-Dist: pydantic>=2.0
|
|
25
25
|
Requires-Dist: httpx>=0.27
|
|
26
26
|
Requires-Dist: mcp>=1.0
|
|
27
|
+
Requires-Dist: watchfiles>=0.21
|
|
28
|
+
Provides-Extra: similarity
|
|
29
|
+
Requires-Dist: torch>=2.0; extra == "similarity"
|
|
30
|
+
Requires-Dist: transformers>=4.30; extra == "similarity"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Analyzer — static analysis and graph extraction from Python source."""
|
|
2
2
|
|
|
3
|
-
from interlinked.analyzer.parser import parse_project
|
|
3
|
+
from interlinked.analyzer.parser import parse_project, parse_file, path_to_module
|
|
4
4
|
from interlinked.analyzer.graph import CodeGraph
|
|
5
5
|
from interlinked.analyzer.dead_code import detect_dead_code
|
|
6
6
|
|
|
7
|
-
__all__ = ["parse_project", "CodeGraph", "detect_dead_code"]
|
|
7
|
+
__all__ = ["parse_project", "parse_file", "path_to_module", "CodeGraph", "detect_dead_code"]
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Dead code detection via forward reachability from production entry points.
|
|
2
|
+
|
|
3
|
+
A symbol is dead if no production execution path can reach it. This is
|
|
4
|
+
computed by building a calls-graph (including decorator registrations and
|
|
5
|
+
module/class-level calls captured by the parser) and doing a forward BFS
|
|
6
|
+
from every production entry point.
|
|
7
|
+
|
|
8
|
+
Entry points:
|
|
9
|
+
- Module nodes (their scope-level code runs on import)
|
|
10
|
+
- Dunder methods (called by the Python runtime)
|
|
11
|
+
- ``main``, ``setup``/``teardown`` and similar framework hooks
|
|
12
|
+
- Decorated functions (the parser emits calls edges from decorator → fn)
|
|
13
|
+
|
|
14
|
+
*Test-only reachability does not count.* If a symbol is reachable only from
|
|
15
|
+
``test_*`` functions, it is still flagged as dead — tests exercise code,
|
|
16
|
+
they don't make it production-live.
|
|
17
|
+
|
|
18
|
+
Additionally detects:
|
|
19
|
+
- Dead variables (written but never read)
|
|
20
|
+
- Dead return edges (return value never consumed)
|
|
21
|
+
- Dead imports (target not a project node and not a prefix of one)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from collections import deque
|
|
27
|
+
|
|
28
|
+
from interlinked.analyzer.graph import CodeGraph
|
|
29
|
+
from interlinked.models import EdgeType, SymbolType
|
|
30
|
+
|
|
31
|
+
_EXEMPT_NAMES: frozenset[str] = frozenset({
|
|
32
|
+
"__init__", "__new__", "__del__", "__repr__", "__str__",
|
|
33
|
+
"__enter__", "__exit__", "__aenter__", "__aexit__",
|
|
34
|
+
"__iter__", "__next__", "__len__", "__getitem__", "__setitem__",
|
|
35
|
+
"__contains__", "__hash__", "__eq__", "__ne__", "__lt__", "__gt__",
|
|
36
|
+
"__le__", "__ge__", "__bool__", "__call__", "__get__", "__set__",
|
|
37
|
+
"__post_init__", "main", "setup", "teardown",
|
|
38
|
+
"setUp", "tearDown", "setUpClass", "tearDownClass",
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def detect_dead_code(graph: CodeGraph) -> list[str]:
|
|
43
|
+
"""Mark dead nodes/edges using forward reachability. Returns dead node IDs."""
|
|
44
|
+
G = graph._g
|
|
45
|
+
all_nodes = graph.all_nodes(include_proposed=False)
|
|
46
|
+
node_ids = {n.id for n in all_nodes}
|
|
47
|
+
|
|
48
|
+
# ── Build adjacency maps ─────────────────────────────────────
|
|
49
|
+
# calls/reads: execution flow — "X invokes or references Y"
|
|
50
|
+
call_fwd: dict[str, set[str]] = {}
|
|
51
|
+
# contains: structural — "X's scope defines Y"
|
|
52
|
+
contains_fwd: dict[str, set[str]] = {}
|
|
53
|
+
# inherits: class → base class
|
|
54
|
+
inherits_targets: dict[str, set[str]] = {}
|
|
55
|
+
|
|
56
|
+
for u, v, d in G.edges(data=True):
|
|
57
|
+
etype = d.get("edge_type")
|
|
58
|
+
if etype in ("calls", "reads"):
|
|
59
|
+
call_fwd.setdefault(u, set()).add(v)
|
|
60
|
+
elif etype == "contains":
|
|
61
|
+
contains_fwd.setdefault(u, set()).add(v)
|
|
62
|
+
elif etype == "inherits":
|
|
63
|
+
inherits_targets.setdefault(u, set()).add(v)
|
|
64
|
+
|
|
65
|
+
# Which nodes are classes?
|
|
66
|
+
class_ids = {n.id for n in all_nodes if n.symbol_type == SymbolType.CLASS}
|
|
67
|
+
|
|
68
|
+
# Classes that inherit from serializable bases (Pydantic BaseModel,
|
|
69
|
+
# dataclasses, etc.). Their fields are implicitly read by framework
|
|
70
|
+
# serialization machinery — model_dump(), asdict(), etc.
|
|
71
|
+
_SERIALIZABLE_BASES = {"BaseModel", "Model", "Schema"}
|
|
72
|
+
serializable_class_ids: set[str] = set()
|
|
73
|
+
for cls_id in class_ids:
|
|
74
|
+
for base in inherits_targets.get(cls_id, ()):
|
|
75
|
+
base_short = base.rsplit(".", 1)[-1] if "." in base else base
|
|
76
|
+
if base_short in _SERIALIZABLE_BASES:
|
|
77
|
+
serializable_class_ids.add(cls_id)
|
|
78
|
+
|
|
79
|
+
# ── Identify production entry points ──────────────────────────
|
|
80
|
+
# Modules are roots — their scope-level code runs on import.
|
|
81
|
+
# Dunder methods and framework hooks are implicitly invoked.
|
|
82
|
+
entry_points: set[str] = set()
|
|
83
|
+
for n in all_nodes:
|
|
84
|
+
if n.symbol_type == SymbolType.MODULE:
|
|
85
|
+
entry_points.add(n.id)
|
|
86
|
+
elif n.name in _EXEMPT_NAMES:
|
|
87
|
+
entry_points.add(n.id)
|
|
88
|
+
|
|
89
|
+
# ── Forward BFS from production entry points ──────────────────
|
|
90
|
+
# When we reach a node, follow its calls/reads edges.
|
|
91
|
+
# When we reach a CLASS, also follow its contains edges — instantiating
|
|
92
|
+
# a class makes all its methods callable (handles visitor pattern,
|
|
93
|
+
# framework base classes, etc.).
|
|
94
|
+
reachable: set[str] = set()
|
|
95
|
+
queue: deque[str] = deque(entry_points)
|
|
96
|
+
while queue:
|
|
97
|
+
nid = queue.popleft()
|
|
98
|
+
if nid in reachable:
|
|
99
|
+
continue
|
|
100
|
+
reachable.add(nid)
|
|
101
|
+
# Follow execution edges
|
|
102
|
+
for target in call_fwd.get(nid, ()):
|
|
103
|
+
if target not in reachable:
|
|
104
|
+
queue.append(target)
|
|
105
|
+
# If this is a class, reaching it means its methods are callable
|
|
106
|
+
if nid in class_ids:
|
|
107
|
+
for child in contains_fwd.get(nid, ()):
|
|
108
|
+
if child not in reachable:
|
|
109
|
+
queue.append(child)
|
|
110
|
+
|
|
111
|
+
# ── Mark unreachable functions/methods as dead ─────────────────
|
|
112
|
+
dead: set[str] = set()
|
|
113
|
+
for n in all_nodes:
|
|
114
|
+
if n.symbol_type not in (SymbolType.FUNCTION, SymbolType.METHOD):
|
|
115
|
+
continue
|
|
116
|
+
# Test functions are not dead — they're tests
|
|
117
|
+
if n.name.startswith("test_"):
|
|
118
|
+
continue
|
|
119
|
+
# Exempt names are never dead
|
|
120
|
+
if n.name in _EXEMPT_NAMES:
|
|
121
|
+
continue
|
|
122
|
+
# If not reachable from any production entry point → dead
|
|
123
|
+
if n.id not in reachable:
|
|
124
|
+
n.is_dead = True
|
|
125
|
+
dead.add(n.id)
|
|
126
|
+
|
|
127
|
+
# ── Dead parameters (parent is dead, or never read) ───────────
|
|
128
|
+
for n in all_nodes:
|
|
129
|
+
if n.symbol_type != SymbolType.PARAMETER or n.name in ("self", "cls"):
|
|
130
|
+
continue
|
|
131
|
+
parent = n.id.rsplit(".", 1)[0] if "." in n.id else ""
|
|
132
|
+
if parent in dead:
|
|
133
|
+
n.is_dead = True
|
|
134
|
+
dead.add(n.id)
|
|
135
|
+
continue
|
|
136
|
+
# If the parent function has incoming 'reads' edges, it's being
|
|
137
|
+
# referenced as a value (passed as callback, stored in a variable,
|
|
138
|
+
# etc.). Its parameters are contract-obligated by whatever receives it.
|
|
139
|
+
if parent in G:
|
|
140
|
+
has_reads_edge = any(
|
|
141
|
+
d.get("edge_type") == "reads"
|
|
142
|
+
for _, _, d in G.in_edges(parent, data=True)
|
|
143
|
+
)
|
|
144
|
+
if has_reads_edge:
|
|
145
|
+
continue
|
|
146
|
+
if n.id not in G:
|
|
147
|
+
continue
|
|
148
|
+
has_reader = any(
|
|
149
|
+
d.get("edge_type") == "reads"
|
|
150
|
+
for _, _, d in G.in_edges(n.id, data=True)
|
|
151
|
+
)
|
|
152
|
+
if not has_reader:
|
|
153
|
+
n.is_dead = True
|
|
154
|
+
dead.add(n.id)
|
|
155
|
+
|
|
156
|
+
# ── Dead variables (written but never read) ───────────────────
|
|
157
|
+
for n in all_nodes:
|
|
158
|
+
if n.symbol_type != SymbolType.VARIABLE:
|
|
159
|
+
continue
|
|
160
|
+
if n.id not in G:
|
|
161
|
+
continue
|
|
162
|
+
# If this variable is a field on a serializable class (BaseModel etc.),
|
|
163
|
+
# it's implicitly read by framework serialization machinery.
|
|
164
|
+
parent = n.id.rsplit(".", 1)[0] if "." in n.id else ""
|
|
165
|
+
if parent in serializable_class_ids:
|
|
166
|
+
continue
|
|
167
|
+
has_writer = any(d.get("edge_type") == "writes" for _, _, d in G.in_edges(n.id, data=True))
|
|
168
|
+
has_reader = any(d.get("edge_type") == "reads" for _, _, d in G.in_edges(n.id, data=True))
|
|
169
|
+
if has_writer and not has_reader:
|
|
170
|
+
n.is_dead = True
|
|
171
|
+
dead.add(n.id)
|
|
172
|
+
|
|
173
|
+
# ── Dead returns (return value never read) ────────────────────
|
|
174
|
+
for n in all_nodes:
|
|
175
|
+
if n.symbol_type not in (SymbolType.FUNCTION, SymbolType.METHOD):
|
|
176
|
+
continue
|
|
177
|
+
if n.id in dead or n.id not in G:
|
|
178
|
+
continue
|
|
179
|
+
ret_targets = [
|
|
180
|
+
v for _, v, d in G.out_edges(n.id, data=True)
|
|
181
|
+
if d.get("edge_type") == "returns"
|
|
182
|
+
]
|
|
183
|
+
if not ret_targets:
|
|
184
|
+
continue
|
|
185
|
+
any_read = any(
|
|
186
|
+
any(d.get("edge_type") == "reads" for _, _, d in G.in_edges(rt, data=True))
|
|
187
|
+
for rt in ret_targets
|
|
188
|
+
)
|
|
189
|
+
if not any_read:
|
|
190
|
+
for e in graph.edges_from(n.id, EdgeType.RETURNS):
|
|
191
|
+
e.is_dead = True
|
|
192
|
+
|
|
193
|
+
# ── Dead imports ──────────────────────────────────────────────
|
|
194
|
+
for e in graph.all_edges(include_proposed=False):
|
|
195
|
+
if e.edge_type != EdgeType.IMPORTS:
|
|
196
|
+
continue
|
|
197
|
+
if e.target not in node_ids:
|
|
198
|
+
is_prefix = any(nid.startswith(e.target) for nid in node_ids)
|
|
199
|
+
if is_prefix:
|
|
200
|
+
e.is_dead = True
|
|
201
|
+
|
|
202
|
+
return list(dead)
|