interlinked-mapper 0.3.11__tar.gz → 0.3.12__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.3.11 → interlinked_mapper-0.3.12}/PKG-INFO +1 -1
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/mcp_server.py +14 -6
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/server.py +131 -70
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked_mapper.egg-info/PKG-INFO +1 -1
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked_mapper.egg-info/SOURCES.txt +2 -1
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/pyproject.toml +1 -1
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/tests/test_accuracy.py +398 -1
- interlinked_mapper-0.3.12/tests/test_watcher.py +636 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/__init__.py +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/analyzer/__init__.py +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/analyzer/dead_code.py +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/analyzer/embeddings.py +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/analyzer/graph.py +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/analyzer/parser.py +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/analyzer/similarity.py +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/cli.py +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/commander/__init__.py +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/commander/llm.py +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/commander/query.py +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/commander/repl.py +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/models.py +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/__init__.py +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/dist/assets/index-CyhrxsQU.css +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/dist/assets/index-Dh01aXoE.js +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/dist/index.html +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/index.html +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/index.html.d3-legacy +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/package-lock.json +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/package.json +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/src/App.tsx +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/src/graph/GraphCanvas.tsx +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/src/graph/nodePrograms.ts +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/src/index.css +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/src/main.tsx +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/src/state/graphStore.ts +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/src/state/sseClient.ts +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/src/theme.ts +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/src/types.ts +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/src/vite-env.d.ts +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/tsconfig.json +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/frontend/vite.config.ts +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked/visualizer/layouts.py +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked_mapper.egg-info/dependency_links.txt +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked_mapper.egg-info/entry_points.txt +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked_mapper.egg-info/requires.txt +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/interlinked_mapper.egg-info/top_level.txt +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/setup.cfg +0 -0
- {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.12}/tests/test_query_completeness.py +0 -0
|
@@ -26,6 +26,7 @@ from __future__ import annotations
|
|
|
26
26
|
import json
|
|
27
27
|
import os
|
|
28
28
|
import sys
|
|
29
|
+
import threading
|
|
29
30
|
from pathlib import Path
|
|
30
31
|
from typing import Any
|
|
31
32
|
|
|
@@ -48,8 +49,6 @@ def _deferred_background_work(graph: CodeGraph, engine: QueryEngine, project_pat
|
|
|
48
49
|
fight over the GIL with the main event loop. Everything heavy runs
|
|
49
50
|
sequentially in one thread.
|
|
50
51
|
"""
|
|
51
|
-
import threading
|
|
52
|
-
|
|
53
52
|
def _work():
|
|
54
53
|
# 1. Similarity fingerprinting (CPU-bound)
|
|
55
54
|
try:
|
|
@@ -379,7 +378,11 @@ def create_mcp_server(project_path: str) -> Server:
|
|
|
379
378
|
loop = asyncio.get_running_loop()
|
|
380
379
|
|
|
381
380
|
if name == "interlinked_switch_project":
|
|
382
|
-
from interlinked.visualizer.server import
|
|
381
|
+
from interlinked.visualizer.server import (
|
|
382
|
+
_rebuild_graph, start_file_watcher, stop_file_watcher,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
stop_file_watcher() # stop old watcher before rebuild
|
|
383
386
|
|
|
384
387
|
def _do_switch():
|
|
385
388
|
g = _state.get("graph")
|
|
@@ -395,8 +398,11 @@ def create_mcp_server(project_path: str) -> Server:
|
|
|
395
398
|
_state["graph"] = graph
|
|
396
399
|
_state["engine"] = engine
|
|
397
400
|
_state["ready"] = True
|
|
401
|
+
_state["project_path"] = arguments["path"]
|
|
398
402
|
# Deferred: single background thread AFTER response sent
|
|
399
403
|
_deferred_background_work(graph, engine, arguments["path"])
|
|
404
|
+
# Start file watcher so graph stays fresh between queries
|
|
405
|
+
start_file_watcher(graph, arguments["path"])
|
|
400
406
|
return [TextContent(type="text", text=json.dumps(result_dict, indent=2))]
|
|
401
407
|
|
|
402
408
|
# All other tools: ensure graph is built first
|
|
@@ -735,12 +741,14 @@ def _dispatch_tool(
|
|
|
735
741
|
return str(result)
|
|
736
742
|
|
|
737
743
|
elif name == "interlinked_switch_project":
|
|
738
|
-
from interlinked.visualizer.server import
|
|
744
|
+
from interlinked.visualizer.server import (
|
|
745
|
+
_rebuild_graph, start_file_watcher, stop_file_watcher,
|
|
746
|
+
)
|
|
747
|
+
stop_file_watcher()
|
|
739
748
|
result = _rebuild_graph(args["path"], graph, run_similarity=False)
|
|
740
749
|
engine.reset_filter()
|
|
741
|
-
# Deferred: similarity + embeddings in a single background thread
|
|
742
|
-
# (matches REST API: never run competing CPU threads)
|
|
743
750
|
_deferred_background_work(graph, engine, args["path"])
|
|
751
|
+
start_file_watcher(graph, args["path"])
|
|
744
752
|
return json.dumps(result, indent=2)
|
|
745
753
|
|
|
746
754
|
elif name == "interlinked_edges_between":
|
|
@@ -21,6 +21,121 @@ from interlinked.models import ViewState, NodeData, EdgeData, GraphDelta
|
|
|
21
21
|
FRONTEND_DIR = Path(__file__).parent / "frontend" / "dist"
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def apply_file_changes(
|
|
25
|
+
graph: CodeGraph, root: Path, changes_list: list[tuple[Any, str]]
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Apply incremental file changes to the graph.
|
|
28
|
+
|
|
29
|
+
This is the core logic used by the file watcher. Extracted as a
|
|
30
|
+
top-level function so it can be tested directly.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
graph: The live CodeGraph instance.
|
|
34
|
+
root: Resolved project root path.
|
|
35
|
+
changes_list: List of (change_type, file_path_str) tuples from watchfiles.
|
|
36
|
+
"""
|
|
37
|
+
from interlinked.analyzer.parser import parse_file, path_to_module
|
|
38
|
+
from interlinked.analyzer.dead_code import detect_dead_code
|
|
39
|
+
|
|
40
|
+
for change_type, file_path_str in changes_list:
|
|
41
|
+
file_path = Path(file_path_str)
|
|
42
|
+
try:
|
|
43
|
+
rel_path = file_path.relative_to(root)
|
|
44
|
+
except ValueError:
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
module_qname = path_to_module(rel_path)
|
|
48
|
+
|
|
49
|
+
# Use string comparison for change type to avoid importing watchfiles
|
|
50
|
+
# at module level (it's an optional dependency).
|
|
51
|
+
change_name = getattr(change_type, "name", str(change_type))
|
|
52
|
+
if change_name == "deleted":
|
|
53
|
+
graph.remove_file(module_qname)
|
|
54
|
+
else:
|
|
55
|
+
existing_ids = {n.id for n in graph.all_nodes()}
|
|
56
|
+
type_idx: dict[str, str] = {}
|
|
57
|
+
for n in graph.all_nodes():
|
|
58
|
+
if n.symbol_type.value in ("module", "class"):
|
|
59
|
+
type_idx[n.name] = n.id
|
|
60
|
+
new_nodes, new_edges = parse_file(
|
|
61
|
+
file_path, module_qname,
|
|
62
|
+
existing_node_ids=existing_ids,
|
|
63
|
+
existing_type_index=type_idx,
|
|
64
|
+
)
|
|
65
|
+
graph.update_file(module_qname, new_nodes, new_edges)
|
|
66
|
+
|
|
67
|
+
detect_dead_code(graph)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
from interlinked.analyzer.similarity import analyze_similarity
|
|
71
|
+
analyze_similarity(graph)
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ── Unified file watcher ──────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
import threading as _threading
|
|
79
|
+
|
|
80
|
+
_watcher_stop: _threading.Event | None = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def start_file_watcher(
|
|
84
|
+
graph: CodeGraph,
|
|
85
|
+
project_path: str,
|
|
86
|
+
on_change: Any = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Start a background file watcher. One implementation for all interfaces.
|
|
89
|
+
|
|
90
|
+
Watches for .py changes, applies incremental updates (edges, dead code,
|
|
91
|
+
similarity), then calls on_change(changes) if provided.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
graph: The live CodeGraph instance.
|
|
95
|
+
project_path: Project root path.
|
|
96
|
+
on_change: Optional callback(changes_list) for interface-specific
|
|
97
|
+
work (embedding updates, SSE push, etc).
|
|
98
|
+
"""
|
|
99
|
+
stop_file_watcher()
|
|
100
|
+
|
|
101
|
+
global _watcher_stop
|
|
102
|
+
stop_event = _threading.Event()
|
|
103
|
+
_watcher_stop = stop_event
|
|
104
|
+
root = Path(project_path).resolve()
|
|
105
|
+
|
|
106
|
+
def _watch():
|
|
107
|
+
try:
|
|
108
|
+
from watchfiles import watch
|
|
109
|
+
except ImportError:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
for changes in watch(
|
|
113
|
+
root,
|
|
114
|
+
watch_filter=lambda change, path: path.endswith(".py"),
|
|
115
|
+
debounce=500,
|
|
116
|
+
stop_event=stop_event,
|
|
117
|
+
):
|
|
118
|
+
if stop_event.is_set():
|
|
119
|
+
break
|
|
120
|
+
changes_list = list(changes)
|
|
121
|
+
apply_file_changes(graph, root, changes_list)
|
|
122
|
+
if on_change is not None:
|
|
123
|
+
try:
|
|
124
|
+
on_change(changes_list)
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
_threading.Thread(target=_watch, daemon=True, name="interlinked-watcher").start()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def stop_file_watcher() -> None:
|
|
132
|
+
"""Stop the file watcher if running."""
|
|
133
|
+
global _watcher_stop
|
|
134
|
+
if _watcher_stop is not None:
|
|
135
|
+
_watcher_stop.set()
|
|
136
|
+
_watcher_stop = None
|
|
137
|
+
|
|
138
|
+
|
|
24
139
|
def _rebuild_graph(project_path: str, graph: CodeGraph, run_similarity: bool = True) -> dict:
|
|
25
140
|
"""Re-parse a project and rebuild the graph in-place. Returns stats.
|
|
26
141
|
|
|
@@ -633,52 +748,16 @@ def create_app(graph: CodeGraph, initial_path: str | None = None) -> FastAPI:
|
|
|
633
748
|
|
|
634
749
|
# ── File watcher for live graph updates ──────────────────────
|
|
635
750
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
async def _watch_project(project_path: str) -> None:
|
|
639
|
-
"""Watch for .py file changes and incrementally update the graph."""
|
|
640
|
-
from watchfiles import awatch, Change
|
|
641
|
-
from interlinked.analyzer.parser import parse_file, path_to_module
|
|
642
|
-
from interlinked.analyzer.dead_code import detect_dead_code
|
|
751
|
+
def _start_watcher() -> None:
|
|
752
|
+
"""Start (or restart) the unified file watcher."""
|
|
643
753
|
from interlinked.models import ViewContext
|
|
644
754
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
def _apply_changes(changes_list):
|
|
649
|
-
"""Synchronous graph mutations — runs in executor thread."""
|
|
650
|
-
for change_type, file_path_str in changes_list:
|
|
651
|
-
file_path = Path(file_path_str)
|
|
652
|
-
try:
|
|
653
|
-
rel_path = file_path.relative_to(root)
|
|
654
|
-
except ValueError:
|
|
655
|
-
continue
|
|
656
|
-
|
|
657
|
-
module_qname = path_to_module(rel_path)
|
|
658
|
-
|
|
659
|
-
if change_type == Change.deleted:
|
|
660
|
-
graph.remove_file(module_qname)
|
|
661
|
-
else:
|
|
662
|
-
existing_ids = {n.id for n in graph.all_nodes()}
|
|
663
|
-
type_idx: dict[str, str] = {}
|
|
664
|
-
for n in graph.all_nodes():
|
|
665
|
-
if n.symbol_type.value in ("module", "class"):
|
|
666
|
-
type_idx[n.name] = n.id
|
|
667
|
-
new_nodes, new_edges = parse_file(
|
|
668
|
-
file_path, module_qname,
|
|
669
|
-
existing_node_ids=existing_ids,
|
|
670
|
-
existing_type_index=type_idx,
|
|
671
|
-
)
|
|
672
|
-
graph.update_file(module_qname, new_nodes, new_edges)
|
|
673
|
-
|
|
674
|
-
detect_dead_code(graph)
|
|
675
|
-
|
|
676
|
-
try:
|
|
677
|
-
from interlinked.analyzer.similarity import analyze_similarity
|
|
678
|
-
analyze_similarity(graph)
|
|
679
|
-
except Exception:
|
|
680
|
-
pass
|
|
755
|
+
project_path = app_state.get("project_path", "")
|
|
756
|
+
if not project_path:
|
|
757
|
+
return
|
|
681
758
|
|
|
759
|
+
def _on_change(changes_list):
|
|
760
|
+
# Embedding delta update
|
|
682
761
|
emb = app_state.get("embedding_index")
|
|
683
762
|
if emb and emb.status == "ready":
|
|
684
763
|
try:
|
|
@@ -686,34 +765,16 @@ def create_app(graph: CodeGraph, initial_path: str | None = None) -> FastAPI:
|
|
|
686
765
|
emb.update_functions(changed_funcs)
|
|
687
766
|
except Exception:
|
|
688
767
|
pass
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
):
|
|
696
|
-
await loop.run_in_executor(None, _apply_changes, list(changes))
|
|
697
|
-
|
|
698
|
-
engine.state.context = ViewContext(
|
|
699
|
-
what="Live update: files changed on disk",
|
|
700
|
-
why=f"{len(changes)} file(s) modified",
|
|
701
|
-
where=app_state["project_path"],
|
|
702
|
-
source="system",
|
|
703
|
-
)
|
|
704
|
-
engine._notify()
|
|
705
|
-
except asyncio.CancelledError:
|
|
706
|
-
pass
|
|
707
|
-
|
|
708
|
-
def _start_watcher() -> None:
|
|
709
|
-
"""Start (or restart) the file watcher background task."""
|
|
710
|
-
if _watcher_task["task"] is not None:
|
|
711
|
-
_watcher_task["task"].cancel()
|
|
712
|
-
project_path = app_state.get("project_path", "")
|
|
713
|
-
if project_path:
|
|
714
|
-
_watcher_task["task"] = asyncio.create_task(
|
|
715
|
-
_watch_project(project_path)
|
|
768
|
+
# SSE notify
|
|
769
|
+
engine.state.context = ViewContext(
|
|
770
|
+
what="Live update: files changed on disk",
|
|
771
|
+
why=f"{len(changes_list)} file(s) modified",
|
|
772
|
+
where=project_path,
|
|
773
|
+
source="system",
|
|
716
774
|
)
|
|
775
|
+
engine._notify()
|
|
776
|
+
|
|
777
|
+
start_file_watcher(graph, project_path, on_change=_on_change)
|
|
717
778
|
|
|
718
779
|
@app.on_event("startup")
|
|
719
780
|
async def _on_startup() -> None:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "interlinked-mapper"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.12"
|
|
8
8
|
description = "A Python program topology explorer — visualize the shape of your codebase"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|