superlocalmemory 3.3.20 → 3.3.22

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 (78) hide show
  1. package/package.json +1 -1
  2. package/pyproject.toml +9 -1
  3. package/src/superlocalmemory/cli/commands.py +138 -22
  4. package/src/superlocalmemory/cli/daemon.py +372 -0
  5. package/src/superlocalmemory/cli/main.py +8 -0
  6. package/src/superlocalmemory/cli/pending_store.py +158 -0
  7. package/src/superlocalmemory/cli/setup_wizard.py +39 -6
  8. package/src/superlocalmemory/code_graph/__init__.py +46 -0
  9. package/src/superlocalmemory/code_graph/blast_radius.py +177 -0
  10. package/src/superlocalmemory/code_graph/bridge/__init__.py +36 -0
  11. package/src/superlocalmemory/code_graph/bridge/entity_resolver.py +464 -0
  12. package/src/superlocalmemory/code_graph/bridge/event_listeners.py +195 -0
  13. package/src/superlocalmemory/code_graph/bridge/fact_enricher.py +159 -0
  14. package/src/superlocalmemory/code_graph/bridge/hebbian_linker.py +170 -0
  15. package/src/superlocalmemory/code_graph/bridge/temporal_checker.py +152 -0
  16. package/src/superlocalmemory/code_graph/changes.py +363 -0
  17. package/src/superlocalmemory/code_graph/communities.py +299 -0
  18. package/src/superlocalmemory/code_graph/config.py +88 -0
  19. package/src/superlocalmemory/code_graph/database.py +482 -0
  20. package/src/superlocalmemory/code_graph/extractors/__init__.py +78 -0
  21. package/src/superlocalmemory/code_graph/extractors/python.py +413 -0
  22. package/src/superlocalmemory/code_graph/extractors/typescript.py +556 -0
  23. package/src/superlocalmemory/code_graph/flows.py +350 -0
  24. package/src/superlocalmemory/code_graph/git_hooks.py +226 -0
  25. package/src/superlocalmemory/code_graph/graph_engine.py +295 -0
  26. package/src/superlocalmemory/code_graph/graph_store.py +158 -0
  27. package/src/superlocalmemory/code_graph/incremental.py +200 -0
  28. package/src/superlocalmemory/code_graph/models.py +130 -0
  29. package/src/superlocalmemory/code_graph/parser.py +507 -0
  30. package/src/superlocalmemory/code_graph/resolver.py +321 -0
  31. package/src/superlocalmemory/code_graph/search.py +460 -0
  32. package/src/superlocalmemory/code_graph/service.py +95 -0
  33. package/src/superlocalmemory/code_graph/watcher.py +207 -0
  34. package/src/superlocalmemory/core/embedding_worker.py +4 -2
  35. package/src/superlocalmemory/core/embeddings.py +8 -2
  36. package/src/superlocalmemory/core/engine.py +32 -0
  37. package/src/superlocalmemory/core/engine_wiring.py +5 -0
  38. package/src/superlocalmemory/core/store_pipeline.py +23 -1
  39. package/src/superlocalmemory/encoding/fact_extractor.py +68 -7
  40. package/src/superlocalmemory/infra/event_bus.py +5 -0
  41. package/src/superlocalmemory/mcp/server.py +23 -0
  42. package/src/superlocalmemory/mcp/tools_code_graph.py +1592 -0
  43. package/src/superlocalmemory/retrieval/engine.py +137 -2
  44. package/src/superlocalmemory/retrieval/semantic_channel.py +6 -2
  45. package/src/superlocalmemory/retrieval/spreading_activation.py +5 -3
  46. package/src/superlocalmemory/retrieval/strategy.py +16 -0
  47. package/src/superlocalmemory/server/api.py +4 -2
  48. package/src/superlocalmemory/server/ui.py +5 -2
  49. package/src/superlocalmemory/storage/schema_code_graph.py +239 -0
  50. package/src/superlocalmemory/ui/index.html +1879 -0
  51. package/src/superlocalmemory/ui/js/agents.js +192 -0
  52. package/src/superlocalmemory/ui/js/auto-settings.js +399 -0
  53. package/src/superlocalmemory/ui/js/behavioral.js +276 -0
  54. package/src/superlocalmemory/ui/js/clusters.js +206 -0
  55. package/src/superlocalmemory/ui/js/compliance.js +252 -0
  56. package/src/superlocalmemory/ui/js/core.js +246 -0
  57. package/src/superlocalmemory/ui/js/dashboard.js +110 -0
  58. package/src/superlocalmemory/ui/js/events.js +178 -0
  59. package/src/superlocalmemory/ui/js/fact-detail.js +92 -0
  60. package/src/superlocalmemory/ui/js/feedback.js +333 -0
  61. package/src/superlocalmemory/ui/js/graph-core.js +447 -0
  62. package/src/superlocalmemory/ui/js/graph-filters.js +220 -0
  63. package/src/superlocalmemory/ui/js/graph-interactions.js +351 -0
  64. package/src/superlocalmemory/ui/js/graph-ui.js +214 -0
  65. package/src/superlocalmemory/ui/js/ide-status.js +102 -0
  66. package/src/superlocalmemory/ui/js/init.js +45 -0
  67. package/src/superlocalmemory/ui/js/learning.js +435 -0
  68. package/src/superlocalmemory/ui/js/lifecycle.js +298 -0
  69. package/src/superlocalmemory/ui/js/math-health.js +98 -0
  70. package/src/superlocalmemory/ui/js/memories.js +264 -0
  71. package/src/superlocalmemory/ui/js/modal.js +357 -0
  72. package/src/superlocalmemory/ui/js/patterns.js +93 -0
  73. package/src/superlocalmemory/ui/js/profiles.js +236 -0
  74. package/src/superlocalmemory/ui/js/recall-lab.js +292 -0
  75. package/src/superlocalmemory/ui/js/search.js +59 -0
  76. package/src/superlocalmemory/ui/js/settings.js +224 -0
  77. package/src/superlocalmemory/ui/js/timeline.js +32 -0
  78. package/src/superlocalmemory/ui/js/trust-dashboard.js +73 -0
@@ -0,0 +1,321 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4 — CodeGraph Module
4
+
5
+ """Cross-file import resolution with heuristic fallback.
6
+
7
+ Resolves Python dotted imports and TypeScript/JS relative imports to
8
+ actual file paths within the repo. Falls back to name-based heuristics
9
+ with lower confidence for unresolvable calls.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import logging
16
+ from pathlib import Path
17
+
18
+ from superlocalmemory.code_graph.config import CodeGraphConfig
19
+ from superlocalmemory.code_graph.models import (
20
+ EdgeKind,
21
+ GraphEdge,
22
+ GraphNode,
23
+ )
24
+ from superlocalmemory.storage.models import _new_id
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class UnsupportedLanguageError(Exception):
30
+ """Raised when a language is not supported for import resolution."""
31
+
32
+
33
+ class ImportResolver:
34
+ """Cross-file import resolution with heuristic fallback."""
35
+
36
+ def __init__(self, repo_root: Path, config: CodeGraphConfig) -> None:
37
+ self._repo_root = repo_root
38
+ self._config = config
39
+
40
+ def resolve(
41
+ self, import_path: str, importer_file: str, language: str
42
+ ) -> str | None:
43
+ """Resolve an import path to a relative file path.
44
+
45
+ Returns None for external packages.
46
+ Raises UnsupportedLanguageError for unknown languages.
47
+ """
48
+ if language == "python":
49
+ return self._resolve_python(import_path, importer_file)
50
+ if language in ("typescript", "tsx", "javascript", "jsx"):
51
+ return self._resolve_typescript(import_path, importer_file)
52
+ raise UnsupportedLanguageError(
53
+ f"Import resolution not supported for language: {language}"
54
+ )
55
+
56
+ def build_symbol_table(
57
+ self, all_nodes: list[GraphNode]
58
+ ) -> dict[str, list[GraphNode]]:
59
+ """Build global symbol table: {bare_name -> [matching_nodes]}.
60
+
61
+ Used for heuristic cross-file resolution.
62
+ """
63
+ table: dict[str, list[GraphNode]] = {}
64
+ for node in all_nodes:
65
+ if node.name:
66
+ table.setdefault(node.name, []).append(node)
67
+ return table
68
+
69
+ def resolve_call_targets(
70
+ self,
71
+ nodes: list[GraphNode],
72
+ edges: list[GraphEdge],
73
+ import_maps: dict[str, dict[str, tuple[str, str]]],
74
+ ) -> list[GraphEdge]:
75
+ """Resolve CALLS edges with placeholder targets.
76
+
77
+ Returns new list of edges with resolved target_node_ids.
78
+ Unresolvable edges (external calls) are dropped.
79
+ """
80
+ symbol_table = self.build_symbol_table(nodes)
81
+
82
+ # Index: (file_path, name) -> node
83
+ file_name_index: dict[tuple[str, str], GraphNode] = {}
84
+ for node in nodes:
85
+ file_name_index[(node.file_path, node.name)] = node
86
+
87
+ resolved: list[GraphEdge] = []
88
+
89
+ for edge in edges:
90
+ if edge.kind != EdgeKind.CALLS:
91
+ resolved.append(edge)
92
+ continue
93
+
94
+ if not edge.target_node_id.startswith("__call__"):
95
+ resolved.append(edge)
96
+ continue
97
+
98
+ call_name = edge.target_node_id.replace("__call__", "")
99
+ source_file = edge.file_path
100
+ file_import_map = import_maps.get(source_file, {})
101
+
102
+ # Strategy 1: Import-resolved
103
+ if call_name in file_import_map:
104
+ module_path, imported_name = file_import_map[call_name]
105
+ resolved_file = self.resolve(
106
+ module_path, source_file,
107
+ self._guess_language(source_file)
108
+ )
109
+ if resolved_file:
110
+ target = file_name_index.get((resolved_file, imported_name))
111
+ if target is None:
112
+ target = file_name_index.get((resolved_file, call_name))
113
+ if target:
114
+ resolved.append(GraphEdge(
115
+ edge_id=edge.edge_id,
116
+ kind=edge.kind,
117
+ source_node_id=edge.source_node_id,
118
+ target_node_id=target.node_id,
119
+ file_path=edge.file_path,
120
+ line=edge.line,
121
+ confidence=1.0,
122
+ extra_json=edge.extra_json,
123
+ ))
124
+ continue
125
+
126
+ # Strategy 2: Same-file match
127
+ same_file_target = file_name_index.get((source_file, call_name))
128
+ if same_file_target:
129
+ resolved.append(GraphEdge(
130
+ edge_id=edge.edge_id,
131
+ kind=edge.kind,
132
+ source_node_id=edge.source_node_id,
133
+ target_node_id=same_file_target.node_id,
134
+ file_path=edge.file_path,
135
+ line=edge.line,
136
+ confidence=1.0,
137
+ extra_json=edge.extra_json,
138
+ ))
139
+ continue
140
+
141
+ # Strategy 3: Global heuristic
142
+ candidates = symbol_table.get(call_name, [])
143
+ if len(candidates) == 1:
144
+ resolved.append(GraphEdge(
145
+ edge_id=edge.edge_id,
146
+ kind=edge.kind,
147
+ source_node_id=edge.source_node_id,
148
+ target_node_id=candidates[0].node_id,
149
+ file_path=edge.file_path,
150
+ line=edge.line,
151
+ confidence=self._config.heuristic_confidence,
152
+ extra_json=edge.extra_json,
153
+ ))
154
+ elif len(candidates) > 1:
155
+ # Pick closest by directory proximity
156
+ best = self._pick_closest(source_file, candidates)
157
+ resolved.append(GraphEdge(
158
+ edge_id=edge.edge_id,
159
+ kind=edge.kind,
160
+ source_node_id=edge.source_node_id,
161
+ target_node_id=best.node_id,
162
+ file_path=edge.file_path,
163
+ line=edge.line,
164
+ confidence=self._config.heuristic_confidence * 0.8,
165
+ extra_json=edge.extra_json,
166
+ ))
167
+ else:
168
+ # External call — drop
169
+ logger.debug(
170
+ "Dropping unresolvable call: %s in %s", call_name, source_file
171
+ )
172
+
173
+ return resolved
174
+
175
+ # ------------------------------------------------------------------
176
+ # Private: Python resolution
177
+ # ------------------------------------------------------------------
178
+
179
+ def _resolve_python(self, import_path: str, importer_file: str) -> str | None:
180
+ """Resolve a Python dotted import to a file path."""
181
+ # Convert dots to path separators
182
+ parts = import_path.replace(".", "/")
183
+
184
+ # Try direct file
185
+ candidate = self._repo_root / f"{parts}.py"
186
+ if candidate.exists():
187
+ return str(candidate.relative_to(self._repo_root))
188
+
189
+ # Try package __init__.py
190
+ candidate = self._repo_root / parts / "__init__.py"
191
+ if candidate.exists():
192
+ return str(candidate.relative_to(self._repo_root))
193
+
194
+ # Walk up from importer directory
195
+ importer_dir = (self._repo_root / importer_file).parent
196
+ current = importer_dir
197
+ while current >= self._repo_root:
198
+ candidate = current / f"{parts}.py"
199
+ if candidate.exists():
200
+ return str(candidate.relative_to(self._repo_root))
201
+ candidate = current / parts / "__init__.py"
202
+ if candidate.exists():
203
+ return str(candidate.relative_to(self._repo_root))
204
+ if current == self._repo_root:
205
+ break
206
+ current = current.parent
207
+
208
+ return None # External package
209
+
210
+ # ------------------------------------------------------------------
211
+ # Private: TypeScript resolution
212
+ # ------------------------------------------------------------------
213
+
214
+ def _resolve_typescript(self, import_path: str, importer_file: str) -> str | None:
215
+ """Resolve a TypeScript/JS import to a file path."""
216
+ # Bare imports (no ./ or ../) = external package
217
+ if not import_path.startswith(".") and not import_path.startswith("@/"):
218
+ if import_path.startswith("@") and "/" in import_path:
219
+ # Scoped package like @foo/bar — still external
220
+ return None
221
+ return None
222
+
223
+ # Handle @/ alias
224
+ if import_path.startswith("@/"):
225
+ return self._resolve_ts_alias(import_path, importer_file)
226
+
227
+ # Relative import
228
+ importer_dir = (self._repo_root / importer_file).parent
229
+ base = (importer_dir / import_path).resolve()
230
+
231
+ # Try extensions in order
232
+ extensions = [".ts", ".tsx", ".js", ".jsx"]
233
+ for ext in extensions:
234
+ candidate = base.with_suffix(ext)
235
+ if candidate.exists():
236
+ try:
237
+ return str(candidate.relative_to(self._repo_root))
238
+ except ValueError:
239
+ continue
240
+
241
+ # Try index files
242
+ if base.is_dir():
243
+ for ext in extensions:
244
+ candidate = base / f"index{ext}"
245
+ if candidate.exists():
246
+ try:
247
+ return str(candidate.relative_to(self._repo_root))
248
+ except ValueError:
249
+ continue
250
+
251
+ # Try as directory even if doesn't exist as dir
252
+ for ext in extensions:
253
+ candidate = Path(str(base)) / f"index{ext}"
254
+ if candidate.exists():
255
+ try:
256
+ return str(candidate.relative_to(self._repo_root))
257
+ except ValueError:
258
+ continue
259
+
260
+ return None
261
+
262
+ def _resolve_ts_alias(self, import_path: str, importer_file: str) -> str | None:
263
+ """Resolve @/ style path aliases from tsconfig.json."""
264
+ tsconfig_path = self._repo_root / "tsconfig.json"
265
+ if not tsconfig_path.exists():
266
+ return None
267
+
268
+ try:
269
+ tsconfig = json.loads(tsconfig_path.read_text())
270
+ except (json.JSONDecodeError, OSError):
271
+ return None
272
+
273
+ paths = tsconfig.get("compilerOptions", {}).get("paths", {})
274
+ for alias_pattern, targets in paths.items():
275
+ prefix = alias_pattern.rstrip("*")
276
+ if import_path.startswith(prefix):
277
+ remainder = import_path[len(prefix):]
278
+ for target in targets:
279
+ target_prefix = target.rstrip("*")
280
+ resolved_path = target_prefix + remainder
281
+ # Try with extensions
282
+ extensions = [".ts", ".tsx", ".js", ".jsx"]
283
+ for ext in extensions:
284
+ candidate = self._repo_root / f"{resolved_path}{ext}"
285
+ if candidate.exists():
286
+ return str(candidate.relative_to(self._repo_root))
287
+ # Try as-is
288
+ candidate = self._repo_root / resolved_path
289
+ if candidate.exists():
290
+ return str(candidate.relative_to(self._repo_root))
291
+
292
+ return None
293
+
294
+ # ------------------------------------------------------------------
295
+ # Private: helpers
296
+ # ------------------------------------------------------------------
297
+
298
+ def _guess_language(self, file_path: str) -> str:
299
+ """Guess language from file extension."""
300
+ ext_map = self._config.extension_map
301
+ for ext, lang in ext_map.items():
302
+ if file_path.endswith(ext):
303
+ return lang
304
+ return "python"
305
+
306
+ @staticmethod
307
+ def _pick_closest(source_file: str, candidates: list[GraphNode]) -> GraphNode:
308
+ """Pick the candidate closest to source_file by directory depth."""
309
+ source_parts = Path(source_file).parts
310
+
311
+ def _distance(node: GraphNode) -> int:
312
+ target_parts = Path(node.file_path).parts
313
+ common = 0
314
+ for s, t in zip(source_parts, target_parts):
315
+ if s == t:
316
+ common += 1
317
+ else:
318
+ break
319
+ return len(source_parts) + len(target_parts) - 2 * common
320
+
321
+ return min(candidates, key=_distance)