interlinked-mapper 0.3.11__tar.gz → 0.3.13__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 (48) hide show
  1. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/PKG-INFO +1 -1
  2. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/analyzer/graph.py +11 -1
  3. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/mcp_server.py +14 -6
  4. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/server.py +131 -70
  5. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked_mapper.egg-info/PKG-INFO +1 -1
  6. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked_mapper.egg-info/SOURCES.txt +2 -1
  7. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/pyproject.toml +1 -1
  8. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/tests/test_accuracy.py +459 -1
  9. interlinked_mapper-0.3.13/tests/test_watcher.py +636 -0
  10. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/__init__.py +0 -0
  11. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/analyzer/__init__.py +0 -0
  12. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/analyzer/dead_code.py +0 -0
  13. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/analyzer/embeddings.py +0 -0
  14. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/analyzer/parser.py +0 -0
  15. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/analyzer/similarity.py +0 -0
  16. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/cli.py +0 -0
  17. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/commander/__init__.py +0 -0
  18. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/commander/llm.py +0 -0
  19. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/commander/query.py +0 -0
  20. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/commander/repl.py +0 -0
  21. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/models.py +0 -0
  22. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/__init__.py +0 -0
  23. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/dist/assets/index-CyhrxsQU.css +0 -0
  24. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/dist/assets/index-Dh01aXoE.js +0 -0
  25. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/dist/index.html +0 -0
  26. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/index.html +0 -0
  27. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/index.html.d3-legacy +0 -0
  28. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/package-lock.json +0 -0
  29. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/package.json +0 -0
  30. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/src/App.tsx +0 -0
  31. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/src/graph/GraphCanvas.tsx +0 -0
  32. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/src/graph/nodePrograms.ts +0 -0
  33. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/src/index.css +0 -0
  34. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/src/main.tsx +0 -0
  35. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/src/state/graphStore.ts +0 -0
  36. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/src/state/sseClient.ts +0 -0
  37. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/src/theme.ts +0 -0
  38. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/src/types.ts +0 -0
  39. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/src/vite-env.d.ts +0 -0
  40. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/tsconfig.json +0 -0
  41. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/frontend/vite.config.ts +0 -0
  42. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked/visualizer/layouts.py +0 -0
  43. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked_mapper.egg-info/dependency_links.txt +0 -0
  44. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked_mapper.egg-info/entry_points.txt +0 -0
  45. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked_mapper.egg-info/requires.txt +0 -0
  46. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/interlinked_mapper.egg-info/top_level.txt +0 -0
  47. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/setup.cfg +0 -0
  48. {interlinked_mapper-0.3.11 → interlinked_mapper-0.3.13}/tests/test_query_completeness.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: interlinked-mapper
3
- Version: 0.3.11
3
+ Version: 0.3.13
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
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from itertools import islice
5
6
  from typing import Any, Iterator
6
7
 
7
8
  import networkx as nx
@@ -445,17 +446,26 @@ class CodeGraph:
445
446
  if k not in ("contains", "inherits")]
446
447
  )
447
448
 
449
+ _MAX_PAIRS = 50
450
+ _MAX_PATHS_PER_PAIR = 10
451
+ pairs_checked = 0
448
452
  for w in writers:
449
453
  for r in readers:
450
454
  if w == r:
451
455
  continue
456
+ if pairs_checked >= _MAX_PAIRS:
457
+ break
452
458
  for src, tgt in [(w, r), (r, w)]:
453
459
  if src in flow_graph and tgt in flow_graph:
454
460
  try:
455
- for path in nx.all_simple_paths(flow_graph, src, tgt, cutoff=5):
461
+ for path in islice(nx.all_simple_paths(flow_graph, src, tgt, cutoff=5), _MAX_PATHS_PER_PAIR):
456
462
  path_nodes.update(path)
457
463
  except nx.NetworkXError:
458
464
  pass
465
+ pairs_checked += 1
466
+ else:
467
+ continue
468
+ break
459
469
 
460
470
  # Also add ancestors/descendants of each writer/reader within the trace
461
471
  for nid in list(trace_func_ids):
@@ -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 _rebuild_graph
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 _rebuild_graph
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
- _watcher_task: dict[str, Any] = {"task": None}
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
- root = Path(project_path).resolve()
646
- loop = asyncio.get_running_loop()
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
- try:
691
- async for changes in awatch(
692
- root,
693
- watch_filter=lambda change, path: path.endswith(".py"),
694
- debounce=500,
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: interlinked-mapper
3
- Version: 0.3.11
3
+ Version: 0.3.13
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
@@ -42,4 +42,5 @@ interlinked_mapper.egg-info/entry_points.txt
42
42
  interlinked_mapper.egg-info/requires.txt
43
43
  interlinked_mapper.egg-info/top_level.txt
44
44
  tests/test_accuracy.py
45
- tests/test_query_completeness.py
45
+ tests/test_query_completeness.py
46
+ tests/test_watcher.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "interlinked-mapper"
7
- version = "0.3.11"
7
+ version = "0.3.13"
8
8
  description = "A Python program topology explorer — visualize the shape of your codebase"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}