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.
Files changed (81) hide show
  1. package/package.json +1 -1
  2. package/pyproject.toml +9 -1
  3. package/src/superlocalmemory/cli/commands.py +140 -23
  4. package/src/superlocalmemory/cli/daemon.py +372 -0
  5. package/src/superlocalmemory/cli/main.py +10 -2
  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/config.py +4 -3
  35. package/src/superlocalmemory/core/embedding_worker.py +4 -2
  36. package/src/superlocalmemory/core/embeddings.py +8 -2
  37. package/src/superlocalmemory/core/engine.py +32 -0
  38. package/src/superlocalmemory/core/engine_wiring.py +5 -0
  39. package/src/superlocalmemory/core/recall_pipeline.py +7 -3
  40. package/src/superlocalmemory/core/store_pipeline.py +23 -1
  41. package/src/superlocalmemory/encoding/fact_extractor.py +68 -7
  42. package/src/superlocalmemory/infra/event_bus.py +5 -0
  43. package/src/superlocalmemory/mcp/server.py +23 -0
  44. package/src/superlocalmemory/mcp/tools_code_graph.py +1592 -0
  45. package/src/superlocalmemory/retrieval/agentic.py +89 -17
  46. package/src/superlocalmemory/retrieval/engine.py +137 -2
  47. package/src/superlocalmemory/retrieval/semantic_channel.py +6 -2
  48. package/src/superlocalmemory/retrieval/spreading_activation.py +5 -3
  49. package/src/superlocalmemory/retrieval/strategy.py +16 -0
  50. package/src/superlocalmemory/server/api.py +4 -2
  51. package/src/superlocalmemory/server/ui.py +5 -2
  52. package/src/superlocalmemory/storage/schema_code_graph.py +239 -0
  53. package/src/superlocalmemory/ui/index.html +1879 -0
  54. package/src/superlocalmemory/ui/js/agents.js +192 -0
  55. package/src/superlocalmemory/ui/js/auto-settings.js +399 -0
  56. package/src/superlocalmemory/ui/js/behavioral.js +276 -0
  57. package/src/superlocalmemory/ui/js/clusters.js +206 -0
  58. package/src/superlocalmemory/ui/js/compliance.js +252 -0
  59. package/src/superlocalmemory/ui/js/core.js +246 -0
  60. package/src/superlocalmemory/ui/js/dashboard.js +110 -0
  61. package/src/superlocalmemory/ui/js/events.js +178 -0
  62. package/src/superlocalmemory/ui/js/fact-detail.js +92 -0
  63. package/src/superlocalmemory/ui/js/feedback.js +333 -0
  64. package/src/superlocalmemory/ui/js/graph-core.js +447 -0
  65. package/src/superlocalmemory/ui/js/graph-filters.js +220 -0
  66. package/src/superlocalmemory/ui/js/graph-interactions.js +351 -0
  67. package/src/superlocalmemory/ui/js/graph-ui.js +214 -0
  68. package/src/superlocalmemory/ui/js/ide-status.js +102 -0
  69. package/src/superlocalmemory/ui/js/init.js +45 -0
  70. package/src/superlocalmemory/ui/js/learning.js +435 -0
  71. package/src/superlocalmemory/ui/js/lifecycle.js +298 -0
  72. package/src/superlocalmemory/ui/js/math-health.js +98 -0
  73. package/src/superlocalmemory/ui/js/memories.js +264 -0
  74. package/src/superlocalmemory/ui/js/modal.js +357 -0
  75. package/src/superlocalmemory/ui/js/patterns.js +93 -0
  76. package/src/superlocalmemory/ui/js/profiles.js +236 -0
  77. package/src/superlocalmemory/ui/js/recall-lab.js +292 -0
  78. package/src/superlocalmemory/ui/js/search.js +59 -0
  79. package/src/superlocalmemory/ui/js/settings.js +224 -0
  80. package/src/superlocalmemory/ui/js/timeline.js +32 -0
  81. package/src/superlocalmemory/ui/js/trust-dashboard.js +73 -0
@@ -0,0 +1,413 @@
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
+ """Python-specific AST extractor using tree-sitter S-expression queries.
6
+
7
+ Extracts: function_definition, class_definition, import_statement,
8
+ import_from_statement, call. Detects test functions and docstrings.
9
+
10
+ tree-sitter imports are lazy (HR-07) — only imported when this module
11
+ is actually used at parse time, never at package import time.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ from typing import Any
19
+
20
+ from superlocalmemory.code_graph.config import CodeGraphConfig
21
+ from superlocalmemory.code_graph.extractors import BaseExtractor
22
+ from superlocalmemory.code_graph.models import (
23
+ EdgeKind,
24
+ GraphEdge,
25
+ GraphNode,
26
+ NodeKind,
27
+ )
28
+ from superlocalmemory.storage.models import _new_id
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # S-expression queries (compiled once per class, not per instance)
34
+ # ---------------------------------------------------------------------------
35
+
36
+ FUNC_QUERY_SRC = (
37
+ "(function_definition"
38
+ " name: (identifier) @func.name"
39
+ " parameters: (parameters) @func.params"
40
+ ") @func.def"
41
+ )
42
+
43
+ CLASS_QUERY_SRC = (
44
+ "(class_definition"
45
+ " name: (identifier) @class.name"
46
+ ") @class.def"
47
+ )
48
+
49
+ IMPORT_QUERY_SRC = (
50
+ "(import_statement) @import.stmt"
51
+ )
52
+
53
+ IMPORT_FROM_QUERY_SRC = (
54
+ "(import_from_statement) @import.from_stmt"
55
+ )
56
+
57
+ CALL_QUERY_SRC = (
58
+ "(call"
59
+ " function: (identifier) @call.name"
60
+ ") @call.expr"
61
+ )
62
+
63
+ CALL_METHOD_QUERY_SRC = (
64
+ "(call"
65
+ " function: (attribute"
66
+ " attribute: (identifier) @call.method)"
67
+ ") @call.method_expr"
68
+ )
69
+
70
+
71
+ def _get_ts_modules() -> tuple[Any, Any, Any]:
72
+ """Lazy import of tree-sitter modules."""
73
+ try:
74
+ import tree_sitter
75
+ from tree_sitter_language_pack import get_language
76
+ except ImportError as exc:
77
+ raise ImportError(
78
+ "tree-sitter and tree-sitter-language-pack are required for "
79
+ "code graph parsing. Install with: "
80
+ "pip install 'superlocalmemory[code-graph]'"
81
+ ) from exc
82
+ return tree_sitter, get_language, None
83
+
84
+
85
+ def _node_text(node: Any, source: bytes) -> str:
86
+ """Extract text from a tree-sitter node."""
87
+ return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
88
+
89
+
90
+ def _find_parent_class(node: Any) -> str | None:
91
+ """Walk up the AST to find the enclosing class_definition name."""
92
+ current = node.parent
93
+ while current is not None:
94
+ if current.type == "class_definition":
95
+ name_node = current.child_by_field_name("name")
96
+ if name_node:
97
+ return name_node.text.decode("utf-8", errors="replace")
98
+ current = current.parent
99
+ return None
100
+
101
+
102
+ def _extract_docstring(func_node: Any, source: bytes) -> str | None:
103
+ """Extract the docstring from a function/class body."""
104
+ body = func_node.child_by_field_name("body")
105
+ if body is None or len(body.children) == 0:
106
+ return None
107
+ first_stmt = body.children[0]
108
+ # tree-sitter Python may represent docstrings as:
109
+ # 1. expression_statement > string (older grammars)
110
+ # 2. string directly in block (newer grammars)
111
+ string_node = None
112
+ if first_stmt.type == "expression_statement":
113
+ expr = first_stmt.children[0] if first_stmt.children else None
114
+ if expr and expr.type == "string":
115
+ string_node = expr
116
+ elif first_stmt.type == "string":
117
+ string_node = first_stmt
118
+
119
+ if string_node is not None:
120
+ raw = _node_text(string_node, source)
121
+ # Strip triple-quote markers
122
+ for quote in ('"""', "'''", '"', "'"):
123
+ if raw.startswith(quote) and raw.endswith(quote):
124
+ raw = raw[len(quote):-len(quote)]
125
+ break
126
+ return raw.strip()
127
+ return None
128
+
129
+
130
+ def _extract_return_type(func_node: Any, source: bytes) -> str | None:
131
+ """Extract return type annotation from function_definition."""
132
+ rt = func_node.child_by_field_name("return_type")
133
+ if rt:
134
+ return _node_text(rt, source)
135
+ return None
136
+
137
+
138
+ def _make_qualified_name(
139
+ file_path: str, name: str, parent_name: str | None
140
+ ) -> str:
141
+ if parent_name:
142
+ return f"{file_path}::{parent_name}.{name}"
143
+ return f"{file_path}::{name}"
144
+
145
+
146
+ def _run_query(
147
+ query_src: str, root_node: Any, language_name: str = "python"
148
+ ) -> list[tuple[int, dict[str, list[Any]]]]:
149
+ """Run a tree-sitter query and return matches."""
150
+ ts, get_language, _ = _get_ts_modules()
151
+ lang = get_language(language_name)
152
+ query = ts.Query(lang, query_src)
153
+ cursor = ts.QueryCursor(query)
154
+ return cursor.matches(root_node)
155
+
156
+
157
+ class PythonExtractor(BaseExtractor):
158
+ """Python-specific AST extractor using S-expression queries."""
159
+
160
+ def extract_functions(self) -> list[GraphNode]:
161
+ """Extract all function/method definitions."""
162
+ matches = _run_query(FUNC_QUERY_SRC, self._root)
163
+ nodes: list[GraphNode] = []
164
+
165
+ for _pattern_idx, captures in matches:
166
+ func_defs = captures.get("func.def", [])
167
+ func_names = captures.get("func.name", [])
168
+ func_params = captures.get("func.params", [])
169
+
170
+ for i, func_def in enumerate(func_defs):
171
+ name_node = func_names[i] if i < len(func_names) else None
172
+ params_node = func_params[i] if i < len(func_params) else None
173
+ if name_node is None:
174
+ continue
175
+
176
+ name = _node_text(name_node, self._source)
177
+ parent_name = _find_parent_class(func_def)
178
+ kind = NodeKind.METHOD if parent_name else NodeKind.FUNCTION
179
+ line_start = func_def.start_point[0]
180
+ line_end = func_def.end_point[0]
181
+
182
+ # Build signature
183
+ params_text = _node_text(params_node, self._source) if params_node else "()"
184
+ return_type = _extract_return_type(func_def, self._source)
185
+ sig = f"def {name}{params_text}"
186
+ if return_type:
187
+ sig += f" -> {return_type}"
188
+
189
+ # Docstring
190
+ docstring = _extract_docstring(func_def, self._source)
191
+
192
+ # Decorators
193
+ decorators: list[str] = []
194
+ for child in func_def.children:
195
+ if child.type == "decorator":
196
+ decorators.append(_node_text(child, self._source))
197
+
198
+ extra: dict[str, Any] = {}
199
+ if decorators:
200
+ extra["decorators"] = decorators
201
+
202
+ node = GraphNode(
203
+ node_id=_new_id(),
204
+ kind=kind,
205
+ name=name,
206
+ qualified_name=_make_qualified_name(
207
+ self._file_path, name, parent_name
208
+ ),
209
+ file_path=self._file_path,
210
+ line_start=line_start,
211
+ line_end=line_end,
212
+ language="python",
213
+ parent_name=parent_name,
214
+ signature=sig,
215
+ docstring=docstring,
216
+ extra_json=json.dumps(extra) if extra else "{}",
217
+ )
218
+ nodes.append(node)
219
+
220
+ return nodes
221
+
222
+ def extract_classes(self) -> list[GraphNode]:
223
+ """Extract all class definitions, including INHERITS edges."""
224
+ matches = _run_query(CLASS_QUERY_SRC, self._root)
225
+ nodes: list[GraphNode] = []
226
+ self._inherits_info: list[tuple[str, str, int]] = [] # (class_qname, base_name, line)
227
+
228
+ for _pattern_idx, captures in matches:
229
+ class_defs = captures.get("class.def", [])
230
+ class_names = captures.get("class.name", [])
231
+
232
+ for i, class_def in enumerate(class_defs):
233
+ name_node = class_names[i] if i < len(class_names) else None
234
+ if name_node is None:
235
+ continue
236
+
237
+ name = _node_text(name_node, self._source)
238
+ line_start = class_def.start_point[0]
239
+ line_end = class_def.end_point[0]
240
+
241
+ # Check for parent class in enclosing class
242
+ parent_class = _find_parent_class(class_def)
243
+
244
+ qualified_name = _make_qualified_name(
245
+ self._file_path, name, parent_class
246
+ )
247
+
248
+ # Extract superclasses
249
+ for child in class_def.children:
250
+ if child.type == "argument_list":
251
+ for arg in child.named_children:
252
+ base_name = _node_text(arg, self._source)
253
+ self._inherits_info.append(
254
+ (qualified_name, base_name, line_start)
255
+ )
256
+
257
+ # Docstring
258
+ docstring = _extract_docstring(class_def, self._source)
259
+
260
+ node = GraphNode(
261
+ node_id=_new_id(),
262
+ kind=NodeKind.CLASS,
263
+ name=name,
264
+ qualified_name=qualified_name,
265
+ file_path=self._file_path,
266
+ line_start=line_start,
267
+ line_end=line_end,
268
+ language="python",
269
+ parent_name=parent_class,
270
+ docstring=docstring,
271
+ )
272
+ nodes.append(node)
273
+
274
+ return nodes
275
+
276
+ def extract_imports(
277
+ self,
278
+ ) -> tuple[list[GraphEdge], dict[str, tuple[str, str]]]:
279
+ """Extract import statements and build import_map."""
280
+ edges: list[GraphEdge] = []
281
+ import_map: dict[str, tuple[str, str]] = {}
282
+
283
+ # Process "import X" statements
284
+ matches = _run_query(IMPORT_QUERY_SRC, self._root)
285
+ for _pat, captures in matches:
286
+ for stmt in captures.get("import.stmt", []):
287
+ for child in stmt.named_children:
288
+ if child.type == "dotted_name":
289
+ module = _node_text(child, self._source)
290
+ local_name = module.split(".")[-1]
291
+ import_map[local_name] = (module, local_name)
292
+ elif child.type == "aliased_import":
293
+ name_node = child.child_by_field_name("name")
294
+ alias_node = child.child_by_field_name("alias")
295
+ if name_node:
296
+ module = _node_text(name_node, self._source)
297
+ alias = (
298
+ _node_text(alias_node, self._source)
299
+ if alias_node
300
+ else module.split(".")[-1]
301
+ )
302
+ import_map[alias] = (module, module.split(".")[-1])
303
+
304
+ edges.append(GraphEdge(
305
+ edge_id=_new_id(),
306
+ kind=EdgeKind.IMPORTS,
307
+ source_node_id="__unresolved__",
308
+ target_node_id="__unresolved__",
309
+ file_path=self._file_path,
310
+ line=stmt.start_point[0],
311
+ ))
312
+
313
+ # Process "from X import Y" statements
314
+ from_matches = _run_query(IMPORT_FROM_QUERY_SRC, self._root)
315
+ for _pat, captures in from_matches:
316
+ for stmt in captures.get("import.from_stmt", []):
317
+ module_name = ""
318
+ # Find module name (dotted_name or relative_import)
319
+ for child in stmt.children:
320
+ if child.type == "dotted_name":
321
+ if not module_name:
322
+ module_name = _node_text(child, self._source)
323
+ continue
324
+ elif child.type == "relative_import":
325
+ module_name = _node_text(child, self._source)
326
+ continue
327
+
328
+ # Find imported names
329
+ imported_names: list[tuple[str, str]] = []
330
+ seen_import_keyword = False
331
+ for child in stmt.children:
332
+ if child.type == "import":
333
+ seen_import_keyword = True
334
+ continue
335
+ if not seen_import_keyword:
336
+ continue
337
+ if child.type == "dotted_name":
338
+ imp_name = _node_text(child, self._source)
339
+ imported_names.append((imp_name, imp_name))
340
+ elif child.type == "aliased_import":
341
+ name_n = child.child_by_field_name("name")
342
+ alias_n = child.child_by_field_name("alias")
343
+ if name_n:
344
+ orig = _node_text(name_n, self._source)
345
+ alias = (
346
+ _node_text(alias_n, self._source)
347
+ if alias_n
348
+ else orig
349
+ )
350
+ imported_names.append((alias, orig))
351
+ elif child.type == "wildcard_import":
352
+ imported_names.append(("*", "*"))
353
+
354
+ for local_name, orig_name in imported_names:
355
+ import_map[local_name] = (module_name, orig_name)
356
+
357
+ edges.append(GraphEdge(
358
+ edge_id=_new_id(),
359
+ kind=EdgeKind.IMPORTS,
360
+ source_node_id="__unresolved__",
361
+ target_node_id="__unresolved__",
362
+ file_path=self._file_path,
363
+ line=stmt.start_point[0],
364
+ ))
365
+
366
+ return edges, import_map
367
+
368
+ def extract_calls(
369
+ self, import_map: dict[str, tuple[str, str]]
370
+ ) -> list[GraphEdge]:
371
+ """Extract function/method calls."""
372
+ edges: list[GraphEdge] = []
373
+
374
+ # Simple calls: foo()
375
+ matches = _run_query(CALL_QUERY_SRC, self._root)
376
+ for _pat, captures in matches:
377
+ call_names = captures.get("call.name", [])
378
+ call_exprs = captures.get("call.expr", [])
379
+ for i, name_node in enumerate(call_names):
380
+ call_name = _node_text(name_node, self._source)
381
+ line = name_node.start_point[0]
382
+ # Find enclosing function for source context
383
+ confidence = 1.0 if call_name in import_map else self._config.heuristic_confidence
384
+ edges.append(GraphEdge(
385
+ edge_id=_new_id(),
386
+ kind=EdgeKind.CALLS,
387
+ source_node_id="__unresolved__",
388
+ target_node_id=f"__call__{call_name}",
389
+ file_path=self._file_path,
390
+ line=line,
391
+ confidence=confidence,
392
+ extra_json=json.dumps({"call_name": call_name}),
393
+ ))
394
+
395
+ # Method calls: obj.method()
396
+ method_matches = _run_query(CALL_METHOD_QUERY_SRC, self._root)
397
+ for _pat, captures in method_matches:
398
+ method_names = captures.get("call.method", [])
399
+ for name_node in method_names:
400
+ method_name = _node_text(name_node, self._source)
401
+ line = name_node.start_point[0]
402
+ edges.append(GraphEdge(
403
+ edge_id=_new_id(),
404
+ kind=EdgeKind.CALLS,
405
+ source_node_id="__unresolved__",
406
+ target_node_id=f"__call__{method_name}",
407
+ file_path=self._file_path,
408
+ line=line,
409
+ confidence=self._config.heuristic_confidence,
410
+ extra_json=json.dumps({"call_name": method_name, "is_method": True}),
411
+ ))
412
+
413
+ return edges