superlocalmemory 3.3.19 → 3.3.21
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.
- package/package.json +1 -1
- package/pyproject.toml +9 -1
- package/src/superlocalmemory/cli/commands.py +140 -23
- package/src/superlocalmemory/cli/daemon.py +372 -0
- package/src/superlocalmemory/cli/main.py +10 -2
- package/src/superlocalmemory/cli/pending_store.py +158 -0
- package/src/superlocalmemory/cli/setup_wizard.py +39 -6
- package/src/superlocalmemory/code_graph/__init__.py +46 -0
- package/src/superlocalmemory/code_graph/blast_radius.py +177 -0
- package/src/superlocalmemory/code_graph/bridge/__init__.py +36 -0
- package/src/superlocalmemory/code_graph/bridge/entity_resolver.py +464 -0
- package/src/superlocalmemory/code_graph/bridge/event_listeners.py +195 -0
- package/src/superlocalmemory/code_graph/bridge/fact_enricher.py +159 -0
- package/src/superlocalmemory/code_graph/bridge/hebbian_linker.py +170 -0
- package/src/superlocalmemory/code_graph/bridge/temporal_checker.py +152 -0
- package/src/superlocalmemory/code_graph/changes.py +363 -0
- package/src/superlocalmemory/code_graph/communities.py +299 -0
- package/src/superlocalmemory/code_graph/config.py +88 -0
- package/src/superlocalmemory/code_graph/database.py +482 -0
- package/src/superlocalmemory/code_graph/extractors/__init__.py +78 -0
- package/src/superlocalmemory/code_graph/extractors/python.py +413 -0
- package/src/superlocalmemory/code_graph/extractors/typescript.py +556 -0
- package/src/superlocalmemory/code_graph/flows.py +350 -0
- package/src/superlocalmemory/code_graph/git_hooks.py +226 -0
- package/src/superlocalmemory/code_graph/graph_engine.py +295 -0
- package/src/superlocalmemory/code_graph/graph_store.py +158 -0
- package/src/superlocalmemory/code_graph/incremental.py +200 -0
- package/src/superlocalmemory/code_graph/models.py +130 -0
- package/src/superlocalmemory/code_graph/parser.py +507 -0
- package/src/superlocalmemory/code_graph/resolver.py +321 -0
- package/src/superlocalmemory/code_graph/search.py +460 -0
- package/src/superlocalmemory/code_graph/service.py +95 -0
- package/src/superlocalmemory/code_graph/watcher.py +207 -0
- package/src/superlocalmemory/core/config.py +4 -3
- package/src/superlocalmemory/core/embedding_worker.py +4 -2
- package/src/superlocalmemory/core/embeddings.py +8 -2
- package/src/superlocalmemory/core/engine.py +32 -0
- package/src/superlocalmemory/core/engine_wiring.py +5 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -3
- package/src/superlocalmemory/core/store_pipeline.py +23 -1
- package/src/superlocalmemory/encoding/fact_extractor.py +68 -7
- package/src/superlocalmemory/infra/event_bus.py +5 -0
- package/src/superlocalmemory/mcp/server.py +23 -0
- package/src/superlocalmemory/mcp/tools_code_graph.py +1592 -0
- package/src/superlocalmemory/retrieval/agentic.py +89 -17
- package/src/superlocalmemory/retrieval/engine.py +137 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +6 -2
- package/src/superlocalmemory/retrieval/spreading_activation.py +5 -3
- package/src/superlocalmemory/retrieval/strategy.py +16 -0
- package/src/superlocalmemory/server/api.py +4 -2
- package/src/superlocalmemory/server/ui.py +5 -2
- package/src/superlocalmemory/storage/schema_code_graph.py +239 -0
- package/src/superlocalmemory/ui/index.html +1879 -0
- package/src/superlocalmemory/ui/js/agents.js +192 -0
- package/src/superlocalmemory/ui/js/auto-settings.js +399 -0
- package/src/superlocalmemory/ui/js/behavioral.js +276 -0
- package/src/superlocalmemory/ui/js/clusters.js +206 -0
- package/src/superlocalmemory/ui/js/compliance.js +252 -0
- package/src/superlocalmemory/ui/js/core.js +246 -0
- package/src/superlocalmemory/ui/js/dashboard.js +110 -0
- package/src/superlocalmemory/ui/js/events.js +178 -0
- package/src/superlocalmemory/ui/js/fact-detail.js +92 -0
- package/src/superlocalmemory/ui/js/feedback.js +333 -0
- package/src/superlocalmemory/ui/js/graph-core.js +447 -0
- package/src/superlocalmemory/ui/js/graph-filters.js +220 -0
- package/src/superlocalmemory/ui/js/graph-interactions.js +351 -0
- package/src/superlocalmemory/ui/js/graph-ui.js +214 -0
- package/src/superlocalmemory/ui/js/ide-status.js +102 -0
- package/src/superlocalmemory/ui/js/init.js +45 -0
- package/src/superlocalmemory/ui/js/learning.js +435 -0
- package/src/superlocalmemory/ui/js/lifecycle.js +298 -0
- package/src/superlocalmemory/ui/js/math-health.js +98 -0
- package/src/superlocalmemory/ui/js/memories.js +264 -0
- package/src/superlocalmemory/ui/js/modal.js +357 -0
- package/src/superlocalmemory/ui/js/patterns.js +93 -0
- package/src/superlocalmemory/ui/js/profiles.js +236 -0
- package/src/superlocalmemory/ui/js/recall-lab.js +292 -0
- package/src/superlocalmemory/ui/js/search.js +59 -0
- package/src/superlocalmemory/ui/js/settings.js +224 -0
- package/src/superlocalmemory/ui/js/timeline.js +32 -0
- 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)
|