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.
Files changed (44) hide show
  1. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/PKG-INFO +5 -1
  2. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/analyzer/__init__.py +2 -2
  3. interlinked_mapper-0.3.1/interlinked/analyzer/dead_code.py +202 -0
  4. interlinked_mapper-0.3.1/interlinked/analyzer/embeddings.py +547 -0
  5. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/analyzer/graph.py +179 -10
  6. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/analyzer/parser.py +205 -7
  7. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/analyzer/similarity.py +393 -60
  8. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/commander/query.py +92 -9
  9. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/mcp_server.py +183 -9
  10. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/models.py +14 -1
  11. interlinked_mapper-0.3.1/interlinked/visualizer/frontend/index.html +13 -0
  12. interlinked_mapper-0.3.1/interlinked/visualizer/frontend/package-lock.json +1806 -0
  13. interlinked_mapper-0.3.1/interlinked/visualizer/frontend/package.json +27 -0
  14. interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/App.tsx +1094 -0
  15. interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/graph/GraphCanvas.tsx +570 -0
  16. interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/graph/nodePrograms.ts +242 -0
  17. interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/index.css +940 -0
  18. interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/main.tsx +10 -0
  19. interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/state/graphStore.ts +212 -0
  20. interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/state/sseClient.ts +37 -0
  21. interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/theme.ts +152 -0
  22. interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/types.ts +83 -0
  23. interlinked_mapper-0.3.1/interlinked/visualizer/frontend/src/vite-env.d.ts +1 -0
  24. interlinked_mapper-0.3.1/interlinked/visualizer/frontend/tsconfig.json +21 -0
  25. interlinked_mapper-0.3.1/interlinked/visualizer/frontend/vite.config.ts +15 -0
  26. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/visualizer/layouts.py +23 -22
  27. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/visualizer/server.py +355 -31
  28. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked_mapper.egg-info/PKG-INFO +5 -1
  29. interlinked_mapper-0.3.1/interlinked_mapper.egg-info/SOURCES.txt +40 -0
  30. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked_mapper.egg-info/requires.txt +5 -0
  31. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/pyproject.toml +5 -1
  32. interlinked_mapper-0.2.0/interlinked/analyzer/dead_code.py +0 -137
  33. interlinked_mapper-0.2.0/interlinked_mapper.egg-info/SOURCES.txt +0 -24
  34. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/__init__.py +0 -0
  35. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/cli.py +0 -0
  36. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/commander/__init__.py +0 -0
  37. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/commander/llm.py +0 -0
  38. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/commander/repl.py +0 -0
  39. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked/visualizer/__init__.py +0 -0
  40. /interlinked_mapper-0.2.0/interlinked/visualizer/frontend/index.html → /interlinked_mapper-0.3.1/interlinked/visualizer/frontend/index.html.d3-legacy +0 -0
  41. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked_mapper.egg-info/dependency_links.txt +0 -0
  42. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked_mapper.egg-info/entry_points.txt +0 -0
  43. {interlinked_mapper-0.2.0 → interlinked_mapper-0.3.1}/interlinked_mapper.egg-info/top_level.txt +0 -0
  44. {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.2.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)