interlinked-mapper 0.1.8__tar.gz → 0.2.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.
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/PKG-INFO +1 -1
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked/commander/llm.py +17 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked/commander/query.py +182 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked/mcp_server.py +54 -1
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked/visualizer/server.py +36 -49
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked_mapper.egg-info/PKG-INFO +1 -1
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/pyproject.toml +1 -1
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked/__init__.py +0 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked/analyzer/__init__.py +0 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked/analyzer/dead_code.py +0 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked/analyzer/graph.py +0 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked/analyzer/parser.py +0 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked/analyzer/similarity.py +0 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked/cli.py +0 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked/commander/__init__.py +0 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked/commander/repl.py +0 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked/models.py +0 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked/visualizer/__init__.py +0 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked/visualizer/frontend/index.html +0 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked/visualizer/layouts.py +0 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked_mapper.egg-info/SOURCES.txt +0 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked_mapper.egg-info/dependency_links.txt +0 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked_mapper.egg-info/entry_points.txt +0 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked_mapper.egg-info/requires.txt +0 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked_mapper.egg-info/top_level.txt +0 -0
- {interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/setup.cfg +0 -0
|
@@ -62,12 +62,16 @@ You control what they see by emitting Python commands against the `view` object.
|
|
|
62
62
|
|
|
63
63
|
### Queries
|
|
64
64
|
- `view.query("dead functions")` — Find dead/uncalled code (highlights results)
|
|
65
|
+
- `view.query("dead functions in <scope>")` — Dead functions scoped to a module prefix, e.g. `"dead functions in engine.rules.ir_exec"`
|
|
66
|
+
- `view.query("dead classes in <scope>")` — Dead classes in a scope
|
|
65
67
|
- `view.query("callers of <name>")` — Who calls this?
|
|
66
68
|
- `view.query("callees of <name>")` — What does this call?
|
|
67
69
|
- `view.query("parameters of <name>")` — Show function parameters
|
|
68
70
|
- `view.query("returns of <name>")` — Show what a function returns (project symbols)
|
|
69
71
|
- `view.query("external calls in <name>")` — Show external library calls from a module/class/function
|
|
70
72
|
- `view.query("external calls")` — All external calls in the project
|
|
73
|
+
- `view.query("functions in <scope>")` — List all functions/methods under a module prefix, e.g. `"functions in engine.rules"`
|
|
74
|
+
- `view.query("classes in <scope>")` — List all classes under a module prefix
|
|
71
75
|
- `view.query("modules")` / `view.query("classes")` / `view.query("functions")` / `view.query("parameters")` / `view.query("variables")`
|
|
72
76
|
- `view.query("<search_term>")` — Fuzzy name search
|
|
73
77
|
|
|
@@ -82,6 +86,19 @@ You control what they see by emitting Python commands against the `view` object.
|
|
|
82
86
|
- `view.path_between(source, target)` — Shortest dependency chain between two symbols.
|
|
83
87
|
- `view.all_paths(source, target, max_depth=8)` — Every route between two symbols.
|
|
84
88
|
|
|
89
|
+
### Cross-Module Edge Enumeration
|
|
90
|
+
- `view.edges_between(source_scope, target_scope=None, edge_types=None)` — List all edges leaving a module scope, grouped by target module. Essential for module isolation checks.
|
|
91
|
+
- `view.edges_between("engine.rules.resolver")` — ALL outgoing edges from resolver
|
|
92
|
+
- `view.edges_between("engine.rules.resolver", target_scope="engine.systems")` — Only edges going to engine.systems
|
|
93
|
+
- `view.edges_between("engine.rules", edge_types=["imports"])` — Only import edges
|
|
94
|
+
- `view.edges_between("engine.rules", edge_types=["calls", "imports"])` — Calls + imports
|
|
95
|
+
|
|
96
|
+
### Reachability (Purity / Isolation Checks)
|
|
97
|
+
- `view.reachable(source, target, edge_types=None, max_depth=20)` — Check if target is reachable from source via specific edge types. Default: follows only call edges.
|
|
98
|
+
- `view.reachable("resolve_effects", "world.set_component")` — Is there ANY call path? Returns path if yes.
|
|
99
|
+
- `view.reachable("resolver.resolve", "db.commit", edge_types=["calls", "imports"])` — Check calls + imports
|
|
100
|
+
- Returns JSON: `{{"reachable": true/false, "path": [...], "short_path": "A → B → C"}}`
|
|
101
|
+
|
|
85
102
|
### Architecture Health
|
|
86
103
|
- `view.find_cycles()` — Find and highlight circular dependencies (calls/imports).
|
|
87
104
|
- `view.critical_nodes(top_n=20)` — Most important symbols by PageRank.
|
|
@@ -305,9 +305,26 @@ class QueryEngine:
|
|
|
305
305
|
results = self.graph.functions_returning(type_hint)
|
|
306
306
|
|
|
307
307
|
elif "dead" in expr or "uncalled" in expr:
|
|
308
|
+
# Support scoped dead queries: "dead functions in engine.rules"
|
|
309
|
+
scope = None
|
|
310
|
+
scope_match = re.search(r'(?:dead|uncalled)\s+\w+\s+in\s+(\S+)', expression, re.IGNORECASE)
|
|
311
|
+
if scope_match:
|
|
312
|
+
scope = scope_match.group(1)
|
|
313
|
+
# Support type filter: "dead functions" vs "dead classes" vs just "dead"
|
|
314
|
+
type_filter: set[SymbolType] | None = None
|
|
315
|
+
if "function" in expr or "method" in expr:
|
|
316
|
+
type_filter = {SymbolType.FUNCTION, SymbolType.METHOD}
|
|
317
|
+
elif "class" in expr:
|
|
318
|
+
type_filter = {SymbolType.CLASS}
|
|
319
|
+
elif "module" in expr:
|
|
320
|
+
type_filter = {SymbolType.MODULE}
|
|
321
|
+
elif "variable" in expr:
|
|
322
|
+
type_filter = {SymbolType.VARIABLE}
|
|
308
323
|
results = [
|
|
309
324
|
n for n in self.graph.all_nodes()
|
|
310
325
|
if n.is_dead
|
|
326
|
+
and (type_filter is None or n.symbol_type in type_filter)
|
|
327
|
+
and (scope is None or n.qualified_name.startswith(scope))
|
|
311
328
|
]
|
|
312
329
|
|
|
313
330
|
elif expr.startswith("imports of"):
|
|
@@ -316,22 +333,34 @@ class QueryEngine:
|
|
|
316
333
|
return [e.model_dump() for e in edges]
|
|
317
334
|
|
|
318
335
|
elif expr.startswith("modules"):
|
|
336
|
+
scope = self._parse_scope(expression, "modules")
|
|
319
337
|
results = self.graph.nodes_by_type(SymbolType.MODULE)
|
|
338
|
+
if scope:
|
|
339
|
+
results = [n for n in results if n.qualified_name.startswith(scope)]
|
|
320
340
|
|
|
321
341
|
elif expr.startswith("classes"):
|
|
342
|
+
scope = self._parse_scope(expression, "classes")
|
|
322
343
|
results = self.graph.nodes_by_type(SymbolType.CLASS)
|
|
344
|
+
if scope:
|
|
345
|
+
results = [n for n in results if n.qualified_name.startswith(scope)]
|
|
323
346
|
|
|
324
347
|
elif expr.startswith("functions") or expr.startswith("methods"):
|
|
348
|
+
scope = self._parse_scope(expression, "functions") or self._parse_scope(expression, "methods")
|
|
325
349
|
results = (
|
|
326
350
|
self.graph.nodes_by_type(SymbolType.FUNCTION)
|
|
327
351
|
+ self.graph.nodes_by_type(SymbolType.METHOD)
|
|
328
352
|
)
|
|
353
|
+
if scope:
|
|
354
|
+
results = [n for n in results if n.qualified_name.startswith(scope)]
|
|
329
355
|
|
|
330
356
|
elif expr.startswith("parameters") or expr.startswith("variables"):
|
|
357
|
+
scope = self._parse_scope(expression, "parameters") or self._parse_scope(expression, "variables")
|
|
331
358
|
results = [
|
|
332
359
|
n for n in self.graph.all_nodes()
|
|
333
360
|
if n.symbol_type == (SymbolType.PARAMETER if "param" in expr else SymbolType.VARIABLE)
|
|
334
361
|
]
|
|
362
|
+
if scope:
|
|
363
|
+
results = [n for n in results if n.qualified_name.startswith(scope)]
|
|
335
364
|
|
|
336
365
|
else:
|
|
337
366
|
# Fuzzy name search
|
|
@@ -652,6 +681,11 @@ class QueryEngine:
|
|
|
652
681
|
|
|
653
682
|
# ── Helper ───────────────────────────────────────────────────────
|
|
654
683
|
|
|
684
|
+
def _parse_scope(self, expression: str, keyword: str) -> str | None:
|
|
685
|
+
"""Extract scope from 'keyword in scope' pattern, e.g. 'functions in engine.rules'."""
|
|
686
|
+
m = re.search(rf'{keyword}\s+in\s+(\S+)', expression, re.IGNORECASE)
|
|
687
|
+
return m.group(1) if m else None
|
|
688
|
+
|
|
655
689
|
def _resolve_node(self, name: str) -> NodeData | None:
|
|
656
690
|
"""Resolve a name to a node, with fuzzy matching."""
|
|
657
691
|
node = self.graph.get_node(name)
|
|
@@ -776,6 +810,154 @@ class QueryEngine:
|
|
|
776
810
|
context = get_rich_context(self.graph, node)
|
|
777
811
|
return json.dumps(context, indent=2, default=str)
|
|
778
812
|
|
|
813
|
+
# ── Cross-module edge enumeration ────────────────────────────────
|
|
814
|
+
|
|
815
|
+
def edges_between(
|
|
816
|
+
self,
|
|
817
|
+
source_scope: str,
|
|
818
|
+
target_scope: str | None = None,
|
|
819
|
+
edge_types: list[str] | None = None,
|
|
820
|
+
) -> str:
|
|
821
|
+
"""List all edges from one module scope to another (or to everything external).
|
|
822
|
+
|
|
823
|
+
Args:
|
|
824
|
+
source_scope: Qualified name prefix of the source module, e.g. "engine.rules.resolver".
|
|
825
|
+
target_scope: Optional target prefix. If omitted, shows all edges leaving source_scope.
|
|
826
|
+
edge_types: Optional filter, e.g. ["calls", "imports"]. Default: all types.
|
|
827
|
+
|
|
828
|
+
Returns JSON with edges grouped by target module.
|
|
829
|
+
"""
|
|
830
|
+
et_filter = None
|
|
831
|
+
if edge_types:
|
|
832
|
+
et_filter = set()
|
|
833
|
+
for et_str in edge_types:
|
|
834
|
+
try:
|
|
835
|
+
et_filter.add(EdgeType(et_str))
|
|
836
|
+
except ValueError:
|
|
837
|
+
pass
|
|
838
|
+
|
|
839
|
+
all_edges = self.graph.all_edges(include_proposed=False)
|
|
840
|
+
source_nodes = {n.id for n in self.graph.all_nodes() if n.qualified_name.startswith(source_scope)}
|
|
841
|
+
|
|
842
|
+
edges_out: list[dict] = []
|
|
843
|
+
for e in all_edges:
|
|
844
|
+
if e.source not in source_nodes:
|
|
845
|
+
continue
|
|
846
|
+
if et_filter and e.edge_type not in et_filter:
|
|
847
|
+
continue
|
|
848
|
+
# Skip edges within the source scope (internal)
|
|
849
|
+
target_node = self.graph.get_node(e.target)
|
|
850
|
+
target_qname = target_node.qualified_name if target_node else e.target
|
|
851
|
+
if target_qname.startswith(source_scope):
|
|
852
|
+
continue
|
|
853
|
+
# If target_scope specified, filter to it
|
|
854
|
+
if target_scope and not target_qname.startswith(target_scope):
|
|
855
|
+
continue
|
|
856
|
+
edges_out.append({
|
|
857
|
+
"source": e.source,
|
|
858
|
+
"target": e.target,
|
|
859
|
+
"edge_type": e.edge_type.value,
|
|
860
|
+
"line": e.line,
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
# Group by target module (first two dotted components of target)
|
|
864
|
+
by_module: dict[str, list[dict]] = {}
|
|
865
|
+
for edge in edges_out:
|
|
866
|
+
tgt = edge["target"]
|
|
867
|
+
parts = tgt.split(".")
|
|
868
|
+
mod = ".".join(parts[:2]) if len(parts) > 1 else parts[0]
|
|
869
|
+
by_module.setdefault(mod, []).append(edge)
|
|
870
|
+
|
|
871
|
+
result = {
|
|
872
|
+
"source_scope": source_scope,
|
|
873
|
+
"target_scope": target_scope or "(all external)",
|
|
874
|
+
"total_edges": len(edges_out),
|
|
875
|
+
"by_target_module": {
|
|
876
|
+
mod: {"count": len(edges), "edges": edges}
|
|
877
|
+
for mod, edges in sorted(by_module.items(), key=lambda x: -len(x[1]))
|
|
878
|
+
},
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
# Highlight source nodes in view
|
|
882
|
+
self.state.highlighted_node_ids = list({e["source"] for e in edges_out})
|
|
883
|
+
self._notify()
|
|
884
|
+
|
|
885
|
+
return json.dumps(result, indent=2)
|
|
886
|
+
|
|
887
|
+
# ── Reachability ─────────────────────────────────────────────────
|
|
888
|
+
|
|
889
|
+
def reachable(
|
|
890
|
+
self,
|
|
891
|
+
source: str,
|
|
892
|
+
target: str,
|
|
893
|
+
edge_types: list[str] | None = None,
|
|
894
|
+
max_depth: int = 20,
|
|
895
|
+
) -> str:
|
|
896
|
+
"""Check if target is reachable from source via specific edge types.
|
|
897
|
+
|
|
898
|
+
By default follows only 'calls' edges — use for purity/isolation checks.
|
|
899
|
+
Returns the shortest path if reachable, or a clear "not reachable" message.
|
|
900
|
+
|
|
901
|
+
Args:
|
|
902
|
+
source: Qualified name of the source symbol.
|
|
903
|
+
target: Qualified name (or partial) of the target symbol.
|
|
904
|
+
edge_types: Edge types to traverse (default: ["calls"]).
|
|
905
|
+
max_depth: Maximum path length to search.
|
|
906
|
+
"""
|
|
907
|
+
import networkx as nx
|
|
908
|
+
|
|
909
|
+
src_node = self._resolve_node(source)
|
|
910
|
+
tgt_node = self._resolve_node(target)
|
|
911
|
+
if not src_node:
|
|
912
|
+
return json.dumps({"reachable": False, "error": f"Source '{source}' not found."})
|
|
913
|
+
if not tgt_node:
|
|
914
|
+
return json.dumps({"reachable": False, "error": f"Target '{target}' not found."})
|
|
915
|
+
|
|
916
|
+
# Build edge-type-filtered subgraph
|
|
917
|
+
et_filter = set()
|
|
918
|
+
for et_str in (edge_types or ["calls"]):
|
|
919
|
+
try:
|
|
920
|
+
et_filter.add(EdgeType(et_str).value)
|
|
921
|
+
except ValueError:
|
|
922
|
+
pass
|
|
923
|
+
if not et_filter:
|
|
924
|
+
et_filter = {"calls"}
|
|
925
|
+
|
|
926
|
+
sub_edges = [
|
|
927
|
+
(u, v) for u, v, d in self.graph._g.edges(data=True)
|
|
928
|
+
if d.get("edge_type") in et_filter
|
|
929
|
+
]
|
|
930
|
+
sub_g = nx.DiGraph()
|
|
931
|
+
sub_g.add_edges_from(sub_edges)
|
|
932
|
+
|
|
933
|
+
try:
|
|
934
|
+
path = nx.shortest_path(sub_g, src_node.id, tgt_node.id)
|
|
935
|
+
if len(path) - 1 > max_depth:
|
|
936
|
+
return json.dumps({"reachable": False, "reason": f"Path exists but exceeds max_depth={max_depth}."})
|
|
937
|
+
|
|
938
|
+
# Highlight the path
|
|
939
|
+
self.state.highlighted_node_ids = list(path)
|
|
940
|
+
self.state.trace_node_roles = {path[0]: "origin", path[-1]: "destination"}
|
|
941
|
+
for nid in path[1:-1]:
|
|
942
|
+
self.state.trace_node_roles[nid] = "passthrough"
|
|
943
|
+
self.state.trace_edge_roles = {}
|
|
944
|
+
self._notify()
|
|
945
|
+
|
|
946
|
+
short_path = " → ".join(n.split(".")[-1] for n in path)
|
|
947
|
+
return json.dumps({
|
|
948
|
+
"reachable": True,
|
|
949
|
+
"path_length": len(path) - 1,
|
|
950
|
+
"path": list(path),
|
|
951
|
+
"short_path": short_path,
|
|
952
|
+
}, indent=2)
|
|
953
|
+
except (nx.NetworkXNoPath, nx.NodeNotFound):
|
|
954
|
+
return json.dumps({
|
|
955
|
+
"reachable": False,
|
|
956
|
+
"source": src_node.qualified_name,
|
|
957
|
+
"target": tgt_node.qualified_name,
|
|
958
|
+
"edge_types": list(et_filter),
|
|
959
|
+
})
|
|
960
|
+
|
|
779
961
|
# ── Display settings ─────────────────────────────────────────────
|
|
780
962
|
|
|
781
963
|
@property
|
|
@@ -141,7 +141,7 @@ def create_mcp_server(project_path: str) -> Server:
|
|
|
141
141
|
),
|
|
142
142
|
Tool(
|
|
143
143
|
name="interlinked_query",
|
|
144
|
-
description="Run a structured query against the codebase graph. Supports: '
|
|
144
|
+
description="Run a structured query against the codebase graph. Supports: 'callers of X', 'callees of X', 'dead functions', 'dead functions in <scope>', 'functions in <scope>', 'classes in <scope>', 'modules in <scope>', 'external calls in <scope>', 'imports of X', 'functions returning <type>', or any search term for fuzzy name matching. The 'in <scope>' suffix filters by qualified name prefix.",
|
|
145
145
|
inputSchema={
|
|
146
146
|
"type": "object",
|
|
147
147
|
"properties": {
|
|
@@ -233,6 +233,33 @@ def create_mcp_server(project_path: str) -> Server:
|
|
|
233
233
|
"required": ["path"],
|
|
234
234
|
},
|
|
235
235
|
),
|
|
236
|
+
Tool(
|
|
237
|
+
name="interlinked_edges_between",
|
|
238
|
+
description="List all edges (calls, imports, etc.) from one module scope to another, or to everything outside the scope. Returns edges grouped by target module. Essential for module isolation checks.",
|
|
239
|
+
inputSchema={
|
|
240
|
+
"type": "object",
|
|
241
|
+
"properties": {
|
|
242
|
+
"source_scope": {"type": "string", "description": "Qualified name prefix of source, e.g. 'engine.rules.resolver'"},
|
|
243
|
+
"target_scope": {"type": "string", "description": "Optional: qualified name prefix of target to filter to, e.g. 'engine.systems'. Omit to see ALL outgoing edges."},
|
|
244
|
+
"edge_types": {"type": "array", "items": {"type": "string"}, "description": "Optional: edge types to include (calls, imports, inherits, reads, writes). Default: all."},
|
|
245
|
+
},
|
|
246
|
+
"required": ["source_scope"],
|
|
247
|
+
},
|
|
248
|
+
),
|
|
249
|
+
Tool(
|
|
250
|
+
name="interlinked_reachable",
|
|
251
|
+
description="Check if there is any path from source to target following specific edge types (default: calls only). Use for purity contracts and isolation verification. Returns the shortest path if reachable.",
|
|
252
|
+
inputSchema={
|
|
253
|
+
"type": "object",
|
|
254
|
+
"properties": {
|
|
255
|
+
"source": {"type": "string", "description": "Qualified name of the source symbol"},
|
|
256
|
+
"target": {"type": "string", "description": "Qualified name (or partial) of the target symbol"},
|
|
257
|
+
"edge_types": {"type": "array", "items": {"type": "string"}, "description": "Edge types to traverse (default: ['calls'])"},
|
|
258
|
+
"max_depth": {"type": "integer", "description": "Maximum path length (default: 20)", "default": 20},
|
|
259
|
+
},
|
|
260
|
+
"required": ["source", "target"],
|
|
261
|
+
},
|
|
262
|
+
),
|
|
236
263
|
Tool(
|
|
237
264
|
name="interlinked_reset",
|
|
238
265
|
description="Reset all filters, focus, and highlights back to the default full-graph view.",
|
|
@@ -320,6 +347,17 @@ def _dispatch_via_server(name: str, args: dict[str, Any], server_url: str) -> st
|
|
|
320
347
|
"interlinked_get_context": ("POST", "/api/get_context", {"target": args.get("target", "")}),
|
|
321
348
|
"interlinked_command": ("POST", "/api/command", {"command": args.get("command", "")}),
|
|
322
349
|
"interlinked_switch_project": ("POST", "/api/switch_project", {"path": args.get("path", "")}),
|
|
350
|
+
"interlinked_edges_between": ("POST", "/api/edges_between", {
|
|
351
|
+
"source_scope": args.get("source_scope", ""),
|
|
352
|
+
"target_scope": args.get("target_scope"),
|
|
353
|
+
"edge_types": args.get("edge_types"),
|
|
354
|
+
}),
|
|
355
|
+
"interlinked_reachable": ("POST", "/api/reachable", {
|
|
356
|
+
"source": args.get("source", ""),
|
|
357
|
+
"target": args.get("target", ""),
|
|
358
|
+
"edge_types": args.get("edge_types"),
|
|
359
|
+
"max_depth": args.get("max_depth", 20),
|
|
360
|
+
}),
|
|
323
361
|
"interlinked_reset": ("POST", "/api/reset", {}),
|
|
324
362
|
}
|
|
325
363
|
|
|
@@ -424,6 +462,21 @@ def _dispatch_tool(
|
|
|
424
462
|
engine.reset_filter()
|
|
425
463
|
return json.dumps(result, indent=2)
|
|
426
464
|
|
|
465
|
+
elif name == "interlinked_edges_between":
|
|
466
|
+
return engine.edges_between(
|
|
467
|
+
source_scope=args["source_scope"],
|
|
468
|
+
target_scope=args.get("target_scope"),
|
|
469
|
+
edge_types=args.get("edge_types"),
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
elif name == "interlinked_reachable":
|
|
473
|
+
return engine.reachable(
|
|
474
|
+
source=args["source"],
|
|
475
|
+
target=args["target"],
|
|
476
|
+
edge_types=args.get("edge_types"),
|
|
477
|
+
max_depth=args.get("max_depth", 20),
|
|
478
|
+
)
|
|
479
|
+
|
|
427
480
|
elif name == "interlinked_reset":
|
|
428
481
|
return engine.reset_filter()
|
|
429
482
|
|
|
@@ -16,7 +16,7 @@ from interlinked.analyzer.graph import CodeGraph
|
|
|
16
16
|
from interlinked.commander.query import QueryEngine
|
|
17
17
|
from interlinked.commander.llm import LLMAdapter, get_system_prompt
|
|
18
18
|
from interlinked.visualizer.layouts import compute_layout
|
|
19
|
-
from interlinked.models import ViewState
|
|
19
|
+
from interlinked.models import ViewState, NodeData, EdgeData
|
|
20
20
|
|
|
21
21
|
FRONTEND_DIR = Path(__file__).parent / "frontend" / "dist"
|
|
22
22
|
|
|
@@ -59,19 +59,19 @@ def create_app(graph: CodeGraph, initial_path: str | None = None) -> FastAPI:
|
|
|
59
59
|
|
|
60
60
|
# ── Broadcast view changes to all SSE clients ────────────────
|
|
61
61
|
|
|
62
|
+
def _layout_from_snap(snap: dict) -> dict:
|
|
63
|
+
"""Compute layout from the snapshot's visible nodes/edges only."""
|
|
64
|
+
snap_nodes = [NodeData(**n) for n in snap.get("nodes", [])]
|
|
65
|
+
snap_edges = [EdgeData(**e) for e in snap.get("edges", [])]
|
|
66
|
+
return compute_layout(snap_nodes, snap_edges)
|
|
67
|
+
|
|
62
68
|
def _snapshot_with_layout() -> dict:
|
|
63
69
|
snap = engine.snapshot()
|
|
64
|
-
snap["layout"] =
|
|
65
|
-
[n for n in graph.all_nodes()],
|
|
66
|
-
graph.all_edges(),
|
|
67
|
-
)
|
|
70
|
+
snap["layout"] = _layout_from_snap(snap)
|
|
68
71
|
return snap
|
|
69
72
|
|
|
70
73
|
def on_view_change(snapshot: dict) -> None:
|
|
71
|
-
snapshot["layout"] =
|
|
72
|
-
[n for n in graph.all_nodes()],
|
|
73
|
-
graph.all_edges(),
|
|
74
|
-
)
|
|
74
|
+
snapshot["layout"] = _layout_from_snap(snapshot)
|
|
75
75
|
msg = json.dumps({"type": "snapshot", "data": snapshot})
|
|
76
76
|
dead: list[asyncio.Queue] = []
|
|
77
77
|
for q in sse_queues:
|
|
@@ -115,11 +115,7 @@ def create_app(graph: CodeGraph, initial_path: str | None = None) -> FastAPI:
|
|
|
115
115
|
@app.get("/api/snapshot")
|
|
116
116
|
async def get_snapshot() -> JSONResponse:
|
|
117
117
|
snap = engine.snapshot()
|
|
118
|
-
layout =
|
|
119
|
-
[n for n in graph.all_nodes()],
|
|
120
|
-
graph.all_edges(),
|
|
121
|
-
)
|
|
122
|
-
snap["layout"] = layout
|
|
118
|
+
snap["layout"] = _layout_from_snap(snap)
|
|
123
119
|
return JSONResponse(content=snap)
|
|
124
120
|
|
|
125
121
|
@app.get("/api/stats")
|
|
@@ -168,11 +164,7 @@ def create_app(graph: CodeGraph, initial_path: str | None = None) -> FastAPI:
|
|
|
168
164
|
return JSONResponse({"error": "No text provided"}, status_code=400)
|
|
169
165
|
result = engine.nl(text)
|
|
170
166
|
snap = engine.snapshot()
|
|
171
|
-
layout =
|
|
172
|
-
[n for n in graph.all_nodes()],
|
|
173
|
-
graph.all_edges(),
|
|
174
|
-
)
|
|
175
|
-
snap["layout"] = layout
|
|
167
|
+
snap["layout"] = _layout_from_snap(snap)
|
|
176
168
|
return JSONResponse({"result": result, "snapshot": snap})
|
|
177
169
|
|
|
178
170
|
@app.post("/api/zoom")
|
|
@@ -199,11 +191,7 @@ def create_app(graph: CodeGraph, initial_path: str | None = None) -> FastAPI:
|
|
|
199
191
|
expr = body.get("expression", "")
|
|
200
192
|
results = engine.query(expr)
|
|
201
193
|
snap = engine.snapshot()
|
|
202
|
-
layout =
|
|
203
|
-
[n for n in graph.all_nodes()],
|
|
204
|
-
graph.all_edges(),
|
|
205
|
-
)
|
|
206
|
-
snap["layout"] = layout
|
|
194
|
+
snap["layout"] = _layout_from_snap(snap)
|
|
207
195
|
return JSONResponse({"results": results, "snapshot": snap})
|
|
208
196
|
|
|
209
197
|
@app.post("/api/propose")
|
|
@@ -231,11 +219,7 @@ def create_app(graph: CodeGraph, initial_path: str | None = None) -> FastAPI:
|
|
|
231
219
|
edge_types = body.get("edge_types")
|
|
232
220
|
result = engine.isolate(target, level=level, depth=depth, edge_types=edge_types)
|
|
233
221
|
snap = engine.snapshot()
|
|
234
|
-
layout =
|
|
235
|
-
[n for n in graph.all_nodes()],
|
|
236
|
-
graph.all_edges(),
|
|
237
|
-
)
|
|
238
|
-
snap["layout"] = layout
|
|
222
|
+
snap["layout"] = _layout_from_snap(snap)
|
|
239
223
|
return JSONResponse({"result": result, "snapshot": snap})
|
|
240
224
|
|
|
241
225
|
@app.post("/api/find_duplicates")
|
|
@@ -244,11 +228,7 @@ def create_app(graph: CodeGraph, initial_path: str | None = None) -> FastAPI:
|
|
|
244
228
|
scope = body.get("scope")
|
|
245
229
|
result = engine.find_duplicates(threshold=threshold, scope=scope)
|
|
246
230
|
snap = engine.snapshot()
|
|
247
|
-
layout =
|
|
248
|
-
[n for n in graph.all_nodes()],
|
|
249
|
-
graph.all_edges(),
|
|
250
|
-
)
|
|
251
|
-
snap["layout"] = layout
|
|
231
|
+
snap["layout"] = _layout_from_snap(snap)
|
|
252
232
|
return JSONResponse({"result": result, "snapshot": snap})
|
|
253
233
|
|
|
254
234
|
@app.post("/api/similar_to")
|
|
@@ -257,11 +237,7 @@ def create_app(graph: CodeGraph, initial_path: str | None = None) -> FastAPI:
|
|
|
257
237
|
threshold = body.get("threshold", 0.5)
|
|
258
238
|
result = engine.similar_to(target, threshold=threshold)
|
|
259
239
|
snap = engine.snapshot()
|
|
260
|
-
layout =
|
|
261
|
-
[n for n in graph.all_nodes()],
|
|
262
|
-
graph.all_edges(),
|
|
263
|
-
)
|
|
264
|
-
snap["layout"] = layout
|
|
240
|
+
snap["layout"] = _layout_from_snap(snap)
|
|
265
241
|
return JSONResponse({"result": result, "snapshot": snap})
|
|
266
242
|
|
|
267
243
|
@app.post("/api/get_context")
|
|
@@ -275,17 +251,32 @@ def create_app(graph: CodeGraph, initial_path: str | None = None) -> FastAPI:
|
|
|
275
251
|
result = engine.reset_filter()
|
|
276
252
|
return JSONResponse({"result": result})
|
|
277
253
|
|
|
254
|
+
@app.post("/api/edges_between")
|
|
255
|
+
async def edges_between(body: dict) -> JSONResponse:
|
|
256
|
+
result = engine.edges_between(
|
|
257
|
+
source_scope=body.get("source_scope", ""),
|
|
258
|
+
target_scope=body.get("target_scope"),
|
|
259
|
+
edge_types=body.get("edge_types"),
|
|
260
|
+
)
|
|
261
|
+
return JSONResponse(content=json.loads(result))
|
|
262
|
+
|
|
263
|
+
@app.post("/api/reachable")
|
|
264
|
+
async def reachable(body: dict) -> JSONResponse:
|
|
265
|
+
result = engine.reachable(
|
|
266
|
+
source=body.get("source", ""),
|
|
267
|
+
target=body.get("target", ""),
|
|
268
|
+
edge_types=body.get("edge_types"),
|
|
269
|
+
max_depth=body.get("max_depth", 20),
|
|
270
|
+
)
|
|
271
|
+
return JSONResponse(content=json.loads(result))
|
|
272
|
+
|
|
278
273
|
@app.post("/api/trace_variable")
|
|
279
274
|
async def trace_variable(body: dict) -> JSONResponse:
|
|
280
275
|
var_name = body.get("variable", "")
|
|
281
276
|
origin = body.get("origin")
|
|
282
277
|
result = engine.trace_variable(var_name, origin)
|
|
283
278
|
snap = engine.snapshot()
|
|
284
|
-
layout =
|
|
285
|
-
[n for n in graph.all_nodes()],
|
|
286
|
-
graph.all_edges(),
|
|
287
|
-
)
|
|
288
|
-
snap["layout"] = layout
|
|
279
|
+
snap["layout"] = _layout_from_snap(snap)
|
|
289
280
|
return JSONResponse({"result": result, "snapshot": snap})
|
|
290
281
|
|
|
291
282
|
# ── LLM chat + settings ──────────────────────────────────────
|
|
@@ -333,11 +324,7 @@ def create_app(graph: CodeGraph, initial_path: str | None = None) -> FastAPI:
|
|
|
333
324
|
|
|
334
325
|
# Always return fresh snapshot after commands have executed
|
|
335
326
|
snap = engine.snapshot()
|
|
336
|
-
layout =
|
|
337
|
-
[n for n in graph.all_nodes()],
|
|
338
|
-
graph.all_edges(),
|
|
339
|
-
)
|
|
340
|
-
snap["layout"] = layout
|
|
327
|
+
snap["layout"] = _layout_from_snap(snap)
|
|
341
328
|
result["snapshot"] = snap
|
|
342
329
|
|
|
343
330
|
return JSONResponse(result)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked/visualizer/frontend/index.html
RENAMED
|
File without changes
|
|
File without changes
|
{interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked_mapper.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked_mapper.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked_mapper.egg-info/requires.txt
RENAMED
|
File without changes
|
{interlinked_mapper-0.1.8 → interlinked_mapper-0.2.0}/interlinked_mapper.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|