interlinked-mapper 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 (25) hide show
  1. interlinked_mapper-0.1.0/PKG-INFO +26 -0
  2. interlinked_mapper-0.1.0/interlinked/__init__.py +3 -0
  3. interlinked_mapper-0.1.0/interlinked/analyzer/__init__.py +7 -0
  4. interlinked_mapper-0.1.0/interlinked/analyzer/dead_code.py +137 -0
  5. interlinked_mapper-0.1.0/interlinked/analyzer/graph.py +822 -0
  6. interlinked_mapper-0.1.0/interlinked/analyzer/parser.py +1141 -0
  7. interlinked_mapper-0.1.0/interlinked/analyzer/similarity.py +486 -0
  8. interlinked_mapper-0.1.0/interlinked/cli.py +136 -0
  9. interlinked_mapper-0.1.0/interlinked/commander/__init__.py +6 -0
  10. interlinked_mapper-0.1.0/interlinked/commander/llm.py +304 -0
  11. interlinked_mapper-0.1.0/interlinked/commander/query.py +966 -0
  12. interlinked_mapper-0.1.0/interlinked/commander/repl.py +50 -0
  13. interlinked_mapper-0.1.0/interlinked/mcp_server.py +324 -0
  14. interlinked_mapper-0.1.0/interlinked/models.py +107 -0
  15. interlinked_mapper-0.1.0/interlinked/visualizer/__init__.py +1 -0
  16. interlinked_mapper-0.1.0/interlinked/visualizer/layouts.py +181 -0
  17. interlinked_mapper-0.1.0/interlinked/visualizer/server.py +428 -0
  18. interlinked_mapper-0.1.0/interlinked_mapper.egg-info/PKG-INFO +26 -0
  19. interlinked_mapper-0.1.0/interlinked_mapper.egg-info/SOURCES.txt +23 -0
  20. interlinked_mapper-0.1.0/interlinked_mapper.egg-info/dependency_links.txt +1 -0
  21. interlinked_mapper-0.1.0/interlinked_mapper.egg-info/entry_points.txt +2 -0
  22. interlinked_mapper-0.1.0/interlinked_mapper.egg-info/requires.txt +6 -0
  23. interlinked_mapper-0.1.0/interlinked_mapper.egg-info/top_level.txt +1 -0
  24. interlinked_mapper-0.1.0/pyproject.toml +43 -0
  25. interlinked_mapper-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: interlinked-mapper
3
+ Version: 0.1.0
4
+ Summary: A Python program topology explorer — visualize the shape of your codebase
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/austerecryptid/interlinked
7
+ Project-URL: Repository, https://github.com/austerecryptid/interlinked
8
+ Keywords: ast,code-analysis,topology,graph,visualization,mcp,networkx
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Software Development :: Quality Assurance
12
+ Classifier: Topic :: Software Development :: Documentation
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Framework :: FastAPI
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: networkx>=3.1
22
+ Requires-Dist: fastapi>=0.110
23
+ Requires-Dist: uvicorn[standard]>=0.29
24
+ Requires-Dist: pydantic>=2.0
25
+ Requires-Dist: httpx>=0.27
26
+ Requires-Dist: mcp>=1.0
@@ -0,0 +1,3 @@
1
+ """Interlinked — A Python program topology explorer."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,7 @@
1
+ """Analyzer — static analysis and graph extraction from Python source."""
2
+
3
+ from interlinked.analyzer.parser import parse_project
4
+ from interlinked.analyzer.graph import CodeGraph
5
+ from interlinked.analyzer.dead_code import detect_dead_code
6
+
7
+ __all__ = ["parse_project", "CodeGraph", "detect_dead_code"]
@@ -0,0 +1,137 @@
1
+ """Dead code detection via NetworkX graph queries.
2
+
3
+ All detection is done through native NetworkX degree/reachability queries
4
+ on the typed MultiDiGraph. No manual graph traversal.
5
+
6
+ Detects:
7
+ - Uncalled functions/methods (zero in-degree on 'calls' + 'reads' keys)
8
+ - Dead parameters (zero in-degree on 'reads' key)
9
+ - Dead variables (has 'writes' in-edges but zero 'reads' in-edges)
10
+ - Dead returns (RETURNS edges whose targets have zero 'reads' in-edges)
11
+ - Dead imports (target not a project node and not a prefix of one)
12
+ - Transitive dead (all ancestors in calls graph are already dead)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import networkx as nx
18
+
19
+ from interlinked.analyzer.graph import CodeGraph
20
+ from interlinked.models import EdgeType, SymbolType
21
+
22
+ _EXEMPT: frozenset[str] = frozenset({
23
+ "__init__", "__new__", "__del__", "__repr__", "__str__",
24
+ "__enter__", "__exit__", "__aenter__", "__aexit__",
25
+ "__iter__", "__next__", "__len__", "__getitem__", "__setitem__",
26
+ "__contains__", "__hash__", "__eq__", "__ne__", "__lt__", "__gt__",
27
+ "__le__", "__ge__", "__bool__", "__call__", "__get__", "__set__",
28
+ "__post_init__", "main", "setup", "teardown",
29
+ "setUp", "tearDown", "setUpClass", "tearDownClass",
30
+ })
31
+
32
+
33
+ def detect_dead_code(graph: CodeGraph) -> list[str]:
34
+ """Mark dead nodes/edges using NetworkX queries. Returns dead node IDs."""
35
+ G = graph._g
36
+ dead: set[str] = set()
37
+ node_ids = {n.id for n in graph.all_nodes(include_proposed=False)}
38
+
39
+ # ── Uncalled functions ────────────────────────────────────────
40
+ for n in graph.all_nodes(include_proposed=False):
41
+ if n.symbol_type not in (SymbolType.FUNCTION, SymbolType.METHOD):
42
+ continue
43
+ if n.name in _EXEMPT or n.name.startswith("test_"):
44
+ continue
45
+ if n.id not in G:
46
+ continue
47
+ # No incoming calls AND no incoming reads (callback references)
48
+ has_caller = any(
49
+ d.get("edge_type") in ("calls", "reads")
50
+ for _, _, d in G.in_edges(n.id, data=True)
51
+ )
52
+ if not has_caller:
53
+ n.is_dead = True
54
+ dead.add(n.id)
55
+
56
+ # ── Dead parameters (never read) ─────────────────────────────
57
+ for n in graph.all_nodes(include_proposed=False):
58
+ if n.symbol_type != SymbolType.PARAMETER or n.name in ("self", "cls"):
59
+ continue
60
+ # Skip params of already-dead functions
61
+ parent = n.id.rsplit(".", 1)[0] if "." in n.id else ""
62
+ if parent in dead:
63
+ continue
64
+ if n.id not in G:
65
+ continue
66
+ has_reader = any(
67
+ d.get("edge_type") == "reads"
68
+ for _, _, d in G.in_edges(n.id, data=True)
69
+ )
70
+ if not has_reader:
71
+ n.is_dead = True
72
+ dead.add(n.id)
73
+
74
+ # ── Dead variables (written but never read) ──────────────────
75
+ for n in graph.all_nodes(include_proposed=False):
76
+ if n.symbol_type != SymbolType.VARIABLE:
77
+ continue
78
+ if n.id not in G:
79
+ continue
80
+ has_writer = any(d.get("edge_type") == "writes" for _, _, d in G.in_edges(n.id, data=True))
81
+ has_reader = any(d.get("edge_type") == "reads" for _, _, d in G.in_edges(n.id, data=True))
82
+ if has_writer and not has_reader:
83
+ n.is_dead = True
84
+ dead.add(n.id)
85
+
86
+ # ── Dead returns (return value never read) ───────────────────
87
+ for n in graph.all_nodes(include_proposed=False):
88
+ if n.symbol_type not in (SymbolType.FUNCTION, SymbolType.METHOD):
89
+ continue
90
+ if n.id in dead or n.id not in G:
91
+ continue
92
+ ret_targets = [
93
+ v for _, v, d in G.out_edges(n.id, data=True)
94
+ if d.get("edge_type") == "returns"
95
+ ]
96
+ if not ret_targets:
97
+ continue
98
+ any_read = any(
99
+ any(d.get("edge_type") == "reads" for _, _, d in G.in_edges(rt, data=True))
100
+ for rt in ret_targets
101
+ )
102
+ if not any_read:
103
+ # Mark the return edges as dead, not the function
104
+ for e in graph.edges_from(n.id, EdgeType.RETURNS):
105
+ e.is_dead = True
106
+
107
+ # ── Dead imports ─────────────────────────────────────────────
108
+ for e in graph.all_edges(include_proposed=False):
109
+ if e.edge_type != EdgeType.IMPORTS:
110
+ continue
111
+ if e.target not in node_ids:
112
+ is_prefix = any(nid.startswith(e.target) for nid in node_ids)
113
+ if is_prefix:
114
+ e.is_dead = True
115
+
116
+ # ── Transitive dead ──────────────────────────────────────────
117
+ # If ALL ancestors of a function in the calls graph are dead, it's dead too
118
+ calls_graph = G.edge_subgraph(
119
+ [(u, v, k) for u, v, k in G.edges(keys=True) if k == "calls"]
120
+ )
121
+ changed = True
122
+ while changed:
123
+ changed = False
124
+ for n in graph.all_nodes(include_proposed=False):
125
+ if n.is_dead or n.id not in calls_graph:
126
+ continue
127
+ if n.symbol_type not in (SymbolType.FUNCTION, SymbolType.METHOD):
128
+ continue
129
+ if n.name in _EXEMPT or n.name.startswith("test_"):
130
+ continue
131
+ callers = set(calls_graph.predecessors(n.id))
132
+ if callers and callers.issubset(dead):
133
+ n.is_dead = True
134
+ dead.add(n.id)
135
+ changed = True
136
+
137
+ return list(dead)