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,159 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4 — CodeGraph Bridge Module
4
+
5
+ """Fact Enricher — append code metadata to fact descriptions.
6
+
7
+ For MVP: builds enrichment strings from matched nodes but does NOT
8
+ write to memory.db. Returns the enriched description so callers
9
+ (MCP tools, event listeners) can decide what to do with it.
10
+
11
+ This keeps the bridge completely isolated from memory.db writes
12
+ per the MVP constraint.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from dataclasses import dataclass
19
+ from typing import TYPE_CHECKING
20
+
21
+ from superlocalmemory.code_graph.models import EdgeKind
22
+
23
+ if TYPE_CHECKING:
24
+ from superlocalmemory.code_graph.database import CodeGraphDatabase
25
+ from superlocalmemory.code_graph.bridge.entity_resolver import MatchedNode
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ MAX_ENRICHMENT_LEN = 500
30
+ MAX_NODES_PER_ENRICHMENT = 3
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class EnrichmentResult:
35
+ """Result of enriching a fact description."""
36
+ fact_id: str
37
+ original_description: str
38
+ enriched_description: str
39
+ nodes_used: int
40
+
41
+
42
+ class FactEnricher:
43
+ """Enriches SLM fact descriptions with code graph metadata.
44
+
45
+ For MVP, this only constructs enrichment strings from code_graph.db.
46
+ It does NOT read from or write to memory.db.
47
+ """
48
+
49
+ def __init__(self, code_graph_db: CodeGraphDatabase) -> None:
50
+ self._db = code_graph_db
51
+
52
+ def enrich(
53
+ self,
54
+ fact_id: str,
55
+ matched_nodes: list[MatchedNode],
56
+ original_description: str = "",
57
+ ) -> str:
58
+ """Enrich a fact description with code metadata.
59
+
60
+ Args:
61
+ fact_id: SLM fact ID.
62
+ matched_nodes: Nodes matched by EntityResolver.
63
+ original_description: Original fact text to enrich.
64
+
65
+ Returns:
66
+ Enriched description string. If no matches, returns
67
+ original_description unchanged.
68
+ """
69
+ if not matched_nodes:
70
+ return original_description
71
+
72
+ # Sort by confidence descending, limit to top N
73
+ sorted_nodes = sorted(
74
+ matched_nodes,
75
+ key=lambda n: n.confidence,
76
+ reverse=True,
77
+ )[:MAX_NODES_PER_ENRICHMENT]
78
+
79
+ suffix_parts: list[str] = []
80
+ for node in sorted_nodes:
81
+ suffix_part = self._build_node_suffix(node)
82
+ if suffix_part:
83
+ suffix_parts.append(suffix_part)
84
+
85
+ if not suffix_parts:
86
+ return original_description
87
+
88
+ enrichment_suffix = " ".join(suffix_parts)
89
+
90
+ if original_description:
91
+ enriched = f"{original_description} {enrichment_suffix}"
92
+ else:
93
+ enriched = enrichment_suffix
94
+
95
+ # Truncate to MAX_ENRICHMENT_LEN
96
+ if len(enriched) > MAX_ENRICHMENT_LEN:
97
+ enriched = enriched[:MAX_ENRICHMENT_LEN - 3] + "..."
98
+
99
+ return enriched
100
+
101
+ def bulk_enrich(
102
+ self,
103
+ fact_node_pairs: list[tuple[str, list[MatchedNode], str]],
104
+ ) -> list[EnrichmentResult]:
105
+ """Enrich multiple facts. Returns list of EnrichmentResult."""
106
+ results: list[EnrichmentResult] = []
107
+ for fact_id, nodes, description in fact_node_pairs:
108
+ enriched = self.enrich(fact_id, nodes, description)
109
+ results.append(EnrichmentResult(
110
+ fact_id=fact_id,
111
+ original_description=description,
112
+ enriched_description=enriched,
113
+ nodes_used=min(len(nodes), MAX_NODES_PER_ENRICHMENT),
114
+ ))
115
+ return results
116
+
117
+ # ------------------------------------------------------------------
118
+ # Internals
119
+ # ------------------------------------------------------------------
120
+
121
+ def _build_node_suffix(self, node: MatchedNode) -> str:
122
+ """Build enrichment suffix for a single matched node."""
123
+ try:
124
+ # Get callers count
125
+ callers_count = self._count_edges_to(node.node_id, EdgeKind.CALLS)
126
+ # Get callees count
127
+ callees_count = self._count_edges_from(node.node_id, EdgeKind.CALLS)
128
+
129
+ suffix = f"[{node.kind}: {node.file_path}::{node.qualified_name.split('::')[-1] if '::' in node.qualified_name else node.qualified_name}"
130
+ if callers_count > 0:
131
+ suffix += f"; {callers_count} callers"
132
+ if callees_count > 0:
133
+ suffix += f"; calls {callees_count}"
134
+ suffix += "]"
135
+ return suffix
136
+ except Exception:
137
+ logger.debug(
138
+ "Failed to build suffix for node %s", node.node_id,
139
+ exc_info=True,
140
+ )
141
+ return ""
142
+
143
+ def _count_edges_to(self, node_id: str, kind: EdgeKind) -> int:
144
+ """Count incoming edges of a specific kind."""
145
+ rows = self._db.execute(
146
+ "SELECT COUNT(*) as cnt FROM graph_edges "
147
+ "WHERE target_node_id = ? AND kind = ?",
148
+ (node_id, kind.value),
149
+ )
150
+ return rows[0]["cnt"] if rows else 0
151
+
152
+ def _count_edges_from(self, node_id: str, kind: EdgeKind) -> int:
153
+ """Count outgoing edges of a specific kind."""
154
+ rows = self._db.execute(
155
+ "SELECT COUNT(*) as cnt FROM graph_edges "
156
+ "WHERE source_node_id = ? AND kind = ?",
157
+ (node_id, kind.value),
158
+ )
159
+ return rows[0]["cnt"] if rows else 0
@@ -0,0 +1,170 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4 — CodeGraph Bridge Module
4
+
5
+ """Hebbian Linker — code-aware association edge creation.
6
+
7
+ When two SLM facts mention functions in the same call subgraph,
8
+ creates code_memory_links entries to record the relationship.
9
+
10
+ For MVP: creates code_memory_links entries ONLY. Does NOT write
11
+ to memory.db association_edges — that's Phase 4b (future).
12
+ This keeps the bridge completely isolated from memory.db writes.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from dataclasses import dataclass
19
+ from typing import TYPE_CHECKING
20
+
21
+ from superlocalmemory.code_graph.models import EdgeKind
22
+
23
+ if TYPE_CHECKING:
24
+ from superlocalmemory.code_graph.database import CodeGraphDatabase
25
+ from superlocalmemory.code_graph.graph_engine import GraphEngine
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ WEIGHT_BASE = 0.3
30
+ WEIGHT_PER_SHARED = 0.1
31
+ WEIGHT_CAP = 0.8
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class HebbianEdge:
36
+ """A code-aware Hebbian association found between two facts."""
37
+ source_fact_id: str
38
+ target_fact_id: str
39
+ weight: float
40
+ shared_node_count: int
41
+
42
+
43
+ class HebbianLinker:
44
+ """Finds facts sharing code subgraphs via code_memory_links in code_graph.db.
45
+
46
+ For MVP: read-only analysis + returns results. Does NOT write
47
+ association_edges to memory.db.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ code_graph_db: CodeGraphDatabase,
53
+ graph_engine: GraphEngine,
54
+ ) -> None:
55
+ self._db = code_graph_db
56
+ self._engine = graph_engine
57
+
58
+ def link(
59
+ self,
60
+ fact_id: str,
61
+ linked_node_ids: list[str],
62
+ ) -> list[HebbianEdge]:
63
+ """Find facts sharing code subgraph and return Hebbian edges.
64
+
65
+ Args:
66
+ fact_id: The SLM fact that was just linked to code.
67
+ linked_node_ids: Code node IDs the fact was linked to.
68
+
69
+ Returns:
70
+ List of HebbianEdge objects describing discovered associations.
71
+ """
72
+ if not linked_node_ids:
73
+ return []
74
+
75
+ # Step 1: Compute 1-hop call neighborhood for all linked nodes
76
+ neighborhood_ids: set[str] = set(linked_node_ids)
77
+ for node_id in linked_node_ids:
78
+ neighbors = self._get_call_neighborhood(node_id)
79
+ neighborhood_ids.update(neighbors)
80
+
81
+ # Step 2: Find other facts linked to the same neighborhood
82
+ other_facts = self._find_facts_in_neighborhood(
83
+ neighborhood_ids, exclude_fact_id=fact_id,
84
+ )
85
+
86
+ if not other_facts:
87
+ return []
88
+
89
+ # Step 3: For each other fact, count shared nodes and compute weight
90
+ edges: list[HebbianEdge] = []
91
+ for other_fact_id, other_node_ids in other_facts.items():
92
+ shared_count = len(neighborhood_ids & other_node_ids)
93
+ if shared_count == 0:
94
+ continue
95
+
96
+ weight = min(
97
+ WEIGHT_BASE + (shared_count * WEIGHT_PER_SHARED),
98
+ WEIGHT_CAP,
99
+ )
100
+ edges.append(HebbianEdge(
101
+ source_fact_id=fact_id,
102
+ target_fact_id=other_fact_id,
103
+ weight=weight,
104
+ shared_node_count=shared_count,
105
+ ))
106
+
107
+ logger.debug(
108
+ "Found %d Hebbian associations for fact %s",
109
+ len(edges), fact_id,
110
+ )
111
+ return edges
112
+
113
+ # ------------------------------------------------------------------
114
+ # Internals
115
+ # ------------------------------------------------------------------
116
+
117
+ def _get_call_neighborhood(self, node_id: str) -> set[str]:
118
+ """Get 1-hop CALLS neighbors (both directions) for a node."""
119
+ neighbors: set[str] = set()
120
+ try:
121
+ # Outgoing CALLS
122
+ callees = self._engine.get_callees(
123
+ node_id, edge_kinds={EdgeKind.CALLS.value},
124
+ )
125
+ for item in callees:
126
+ nid = item["node"]["node_id"]
127
+ neighbors.add(nid)
128
+
129
+ # Incoming CALLS
130
+ callers = self._engine.get_callers(
131
+ node_id, edge_kinds={EdgeKind.CALLS.value},
132
+ )
133
+ for item in callers:
134
+ nid = item["node"]["node_id"]
135
+ neighbors.add(nid)
136
+ except KeyError:
137
+ # Node not in graph — skip
138
+ logger.debug("Node %s not in graph, skipping neighborhood", node_id)
139
+
140
+ return neighbors
141
+
142
+ def _find_facts_in_neighborhood(
143
+ self,
144
+ neighborhood_ids: set[str],
145
+ exclude_fact_id: str,
146
+ ) -> dict[str, set[str]]:
147
+ """Find other facts linked to nodes in the neighborhood.
148
+
149
+ Returns dict of fact_id -> set of linked node_ids.
150
+ """
151
+ if not neighborhood_ids:
152
+ return {}
153
+
154
+ placeholders = ",".join("?" for _ in neighborhood_ids)
155
+ rows = self._db.execute(
156
+ f"SELECT DISTINCT slm_fact_id, code_node_id "
157
+ f"FROM code_memory_links "
158
+ f"WHERE code_node_id IN ({placeholders}) "
159
+ f"AND slm_fact_id != ? "
160
+ f"AND is_stale = 0",
161
+ tuple(neighborhood_ids) + (exclude_fact_id,),
162
+ )
163
+
164
+ result: dict[str, set[str]] = {}
165
+ for row in rows:
166
+ fid = row["slm_fact_id"]
167
+ nid = row["code_node_id"]
168
+ result.setdefault(fid, set()).add(nid)
169
+
170
+ return result
@@ -0,0 +1,152 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4 — CodeGraph Bridge Module
4
+
5
+ """Temporal Checker — invalidate memories about deleted/renamed code.
6
+
7
+ For MVP: operates entirely on code_graph.db (code_memory_links table).
8
+ Does NOT write to memory.db fact_temporal_validity — that's Phase 4b.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from dataclasses import dataclass
15
+ from typing import TYPE_CHECKING
16
+
17
+ if TYPE_CHECKING:
18
+ from superlocalmemory.code_graph.database import CodeGraphDatabase
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class StaleLink:
25
+ """A code_memory_link that has been marked stale."""
26
+ link_id: str
27
+ code_node_id: str
28
+ slm_fact_id: str
29
+ qualified_name: str
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class DeletedCodeMemory:
34
+ """A fact linked to a code node that no longer exists in the graph."""
35
+ fact_id: str
36
+ node_id: str
37
+ node_qualified_name: str
38
+ link_id: str
39
+
40
+
41
+ class TemporalChecker:
42
+ """Checks and invalidates memories about deleted/renamed code.
43
+
44
+ Operates on code_graph.db only (MVP constraint).
45
+ """
46
+
47
+ def __init__(self, code_graph_db: CodeGraphDatabase) -> None:
48
+ self._db = code_graph_db
49
+
50
+ def mark_links_stale(self, node_id: str) -> int:
51
+ """Mark all non-stale links for a node as stale.
52
+
53
+ Returns the count of links marked stale.
54
+ """
55
+ count = self._db.execute_write(
56
+ "UPDATE code_memory_links SET is_stale = 1, "
57
+ "last_verified = datetime('now') "
58
+ "WHERE code_node_id = ? AND is_stale = 0",
59
+ (node_id,),
60
+ )
61
+ if count > 0:
62
+ logger.info(
63
+ "Marked %d links stale for node %s", count, node_id,
64
+ )
65
+ return count
66
+
67
+ def check_stale_links(self) -> list[StaleLink]:
68
+ """Get all stale code_memory_links.
69
+
70
+ Returns list of StaleLink with node metadata.
71
+ """
72
+ rows = self._db.execute(
73
+ "SELECT cml.link_id, cml.code_node_id, cml.slm_fact_id, "
74
+ "COALESCE(gn.qualified_name, 'deleted') as qualified_name "
75
+ "FROM code_memory_links cml "
76
+ "LEFT JOIN graph_nodes gn ON cml.code_node_id = gn.node_id "
77
+ "WHERE cml.is_stale = 1",
78
+ (),
79
+ )
80
+ return [
81
+ StaleLink(
82
+ link_id=row["link_id"],
83
+ code_node_id=row["code_node_id"],
84
+ slm_fact_id=row["slm_fact_id"],
85
+ qualified_name=row["qualified_name"],
86
+ )
87
+ for row in rows
88
+ ]
89
+
90
+ def get_memories_for_deleted_code(self) -> list[DeletedCodeMemory]:
91
+ """Find facts linked to code nodes that no longer exist in the graph.
92
+
93
+ These are code_memory_links whose code_node_id has no matching
94
+ graph_nodes row — the code was deleted.
95
+ """
96
+ rows = self._db.execute(
97
+ "SELECT cml.slm_fact_id, cml.code_node_id, cml.link_id "
98
+ "FROM code_memory_links cml "
99
+ "LEFT JOIN graph_nodes gn ON cml.code_node_id = gn.node_id "
100
+ "WHERE gn.node_id IS NULL",
101
+ (),
102
+ )
103
+ return [
104
+ DeletedCodeMemory(
105
+ fact_id=row["slm_fact_id"],
106
+ node_id=row["code_node_id"],
107
+ node_qualified_name="deleted",
108
+ link_id=row["link_id"],
109
+ )
110
+ for row in rows
111
+ ]
112
+
113
+ def bulk_verify(self) -> dict[str, int]:
114
+ """Re-verify ALL code_memory_links against current graph state.
115
+
116
+ - Links whose code_node_id still exists: mark verified
117
+ - Links whose code_node_id is gone: mark stale
118
+
119
+ Returns {"verified": int, "marked_stale": int, "already_stale": int}
120
+ """
121
+ # Count already stale
122
+ already_rows = self._db.execute(
123
+ "SELECT COUNT(*) as cnt FROM code_memory_links WHERE is_stale = 1",
124
+ (),
125
+ )
126
+ already_stale = already_rows[0]["cnt"] if already_rows else 0
127
+
128
+ # Mark stale: links to deleted nodes
129
+ marked_stale = self._db.execute_write(
130
+ "UPDATE code_memory_links SET is_stale = 1, "
131
+ "last_verified = datetime('now') "
132
+ "WHERE is_stale = 0 AND code_node_id NOT IN "
133
+ "(SELECT node_id FROM graph_nodes)",
134
+ (),
135
+ )
136
+
137
+ # Verify: links to existing nodes
138
+ verified = self._db.execute_write(
139
+ "UPDATE code_memory_links SET "
140
+ "last_verified = datetime('now') "
141
+ "WHERE is_stale = 0 AND code_node_id IN "
142
+ "(SELECT node_id FROM graph_nodes)",
143
+ (),
144
+ )
145
+
146
+ result = {
147
+ "verified": verified,
148
+ "marked_stale": marked_stale,
149
+ "already_stale": already_stale,
150
+ }
151
+ logger.info("Bulk verify complete: %s", result)
152
+ return result