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,158 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Pending memory store — zero-loss async remember (Option C).
|
|
6
|
+
|
|
7
|
+
Problem: v3.3.20 async `slm remember` spawns a detached subprocess with
|
|
8
|
+
stderr=DEVNULL. If the embedding worker crashes, the user's data is silently
|
|
9
|
+
lost — they see "Queued for background processing" but the memory never stores.
|
|
10
|
+
|
|
11
|
+
Solution: Store-first, embed-later (Netflix pattern).
|
|
12
|
+
1. INSERT raw content into pending_memories (synchronous, 0.1s, no engine init)
|
|
13
|
+
2. Spawn background subprocess to process (extract facts, embed, build graph)
|
|
14
|
+
3. If background crashes, content survives in pending table
|
|
15
|
+
4. Next engine.initialize() auto-retries pending items
|
|
16
|
+
|
|
17
|
+
Uses a separate `pending.db` file — never touches memory.db directly.
|
|
18
|
+
Stdlib only — no SLM imports (must be fast).
|
|
19
|
+
|
|
20
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
21
|
+
License: MIT
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import sqlite3
|
|
28
|
+
import time
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
_DEFAULT_DIR = Path.home() / ".superlocalmemory"
|
|
32
|
+
_PENDING_DB = "pending.db"
|
|
33
|
+
|
|
34
|
+
_SCHEMA = """
|
|
35
|
+
CREATE TABLE IF NOT EXISTS pending_memories (
|
|
36
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
37
|
+
content TEXT NOT NULL,
|
|
38
|
+
tags TEXT DEFAULT '',
|
|
39
|
+
metadata TEXT DEFAULT '{}',
|
|
40
|
+
created_at TEXT NOT NULL,
|
|
41
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
42
|
+
error TEXT DEFAULT NULL,
|
|
43
|
+
retry_count INTEGER DEFAULT 0
|
|
44
|
+
);
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _get_db(base_dir: Path | None = None) -> sqlite3.Connection:
|
|
49
|
+
"""Open pending.db with WAL mode. Creates if needed."""
|
|
50
|
+
d = base_dir or _DEFAULT_DIR
|
|
51
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
db_path = d / _PENDING_DB
|
|
53
|
+
conn = sqlite3.connect(str(db_path), timeout=5)
|
|
54
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
55
|
+
conn.execute(_SCHEMA)
|
|
56
|
+
return conn
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def store_pending(
|
|
60
|
+
content: str,
|
|
61
|
+
tags: str = "",
|
|
62
|
+
metadata: dict | None = None,
|
|
63
|
+
base_dir: Path | None = None,
|
|
64
|
+
) -> int:
|
|
65
|
+
"""Store content in pending table. Returns the row ID.
|
|
66
|
+
|
|
67
|
+
This is intentionally FAST — no engine init, no embedding, no model loading.
|
|
68
|
+
Just a raw SQLite INSERT (~0.1s).
|
|
69
|
+
"""
|
|
70
|
+
conn = _get_db(base_dir)
|
|
71
|
+
try:
|
|
72
|
+
cur = conn.execute(
|
|
73
|
+
"INSERT INTO pending_memories (content, tags, metadata, created_at, status) "
|
|
74
|
+
"VALUES (?, ?, ?, ?, 'pending')",
|
|
75
|
+
(content, tags, json.dumps(metadata or {}), time.strftime("%Y-%m-%dT%H:%M:%S")),
|
|
76
|
+
)
|
|
77
|
+
conn.commit()
|
|
78
|
+
return cur.lastrowid or 0
|
|
79
|
+
finally:
|
|
80
|
+
conn.close()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_pending(base_dir: Path | None = None, limit: int = 50) -> list[dict]:
|
|
84
|
+
"""Get unprocessed pending memories."""
|
|
85
|
+
conn = _get_db(base_dir)
|
|
86
|
+
try:
|
|
87
|
+
rows = conn.execute(
|
|
88
|
+
"SELECT id, content, tags, metadata, created_at, retry_count "
|
|
89
|
+
"FROM pending_memories WHERE status = 'pending' "
|
|
90
|
+
"ORDER BY id ASC LIMIT ?",
|
|
91
|
+
(limit,),
|
|
92
|
+
).fetchall()
|
|
93
|
+
return [
|
|
94
|
+
{"id": r[0], "content": r[1], "tags": r[2], "metadata": r[3],
|
|
95
|
+
"created_at": r[4], "retry_count": r[5]}
|
|
96
|
+
for r in rows
|
|
97
|
+
]
|
|
98
|
+
finally:
|
|
99
|
+
conn.close()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def mark_done(row_id: int, base_dir: Path | None = None) -> None:
|
|
103
|
+
"""Mark a pending memory as successfully processed."""
|
|
104
|
+
conn = _get_db(base_dir)
|
|
105
|
+
try:
|
|
106
|
+
conn.execute(
|
|
107
|
+
"UPDATE pending_memories SET status = 'done' WHERE id = ?",
|
|
108
|
+
(row_id,),
|
|
109
|
+
)
|
|
110
|
+
conn.commit()
|
|
111
|
+
finally:
|
|
112
|
+
conn.close()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def mark_failed(row_id: int, error: str, base_dir: Path | None = None) -> None:
|
|
116
|
+
"""Mark a pending memory as failed with error message."""
|
|
117
|
+
conn = _get_db(base_dir)
|
|
118
|
+
try:
|
|
119
|
+
conn.execute(
|
|
120
|
+
"UPDATE pending_memories SET status = 'failed', error = ?, "
|
|
121
|
+
"retry_count = retry_count + 1 WHERE id = ?",
|
|
122
|
+
(error, row_id),
|
|
123
|
+
)
|
|
124
|
+
conn.commit()
|
|
125
|
+
finally:
|
|
126
|
+
conn.close()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def pending_count(base_dir: Path | None = None) -> int:
|
|
130
|
+
"""Count unprocessed pending memories."""
|
|
131
|
+
d = base_dir or _DEFAULT_DIR
|
|
132
|
+
db_path = d / _PENDING_DB
|
|
133
|
+
if not db_path.exists():
|
|
134
|
+
return 0
|
|
135
|
+
conn = sqlite3.connect(str(db_path), timeout=5)
|
|
136
|
+
try:
|
|
137
|
+
return conn.execute(
|
|
138
|
+
"SELECT COUNT(*) FROM pending_memories WHERE status = 'pending'"
|
|
139
|
+
).fetchone()[0]
|
|
140
|
+
except sqlite3.OperationalError:
|
|
141
|
+
return 0
|
|
142
|
+
finally:
|
|
143
|
+
conn.close()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def cleanup_done(days: int = 7, base_dir: Path | None = None) -> int:
|
|
147
|
+
"""Remove processed entries older than N days."""
|
|
148
|
+
conn = _get_db(base_dir)
|
|
149
|
+
try:
|
|
150
|
+
cur = conn.execute(
|
|
151
|
+
"DELETE FROM pending_memories WHERE status = 'done' "
|
|
152
|
+
"AND created_at < datetime('now', ?)",
|
|
153
|
+
(f"-{days} days",),
|
|
154
|
+
)
|
|
155
|
+
conn.commit()
|
|
156
|
+
return cur.rowcount
|
|
157
|
+
finally:
|
|
158
|
+
conn.close()
|
|
@@ -265,7 +265,7 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
265
265
|
print()
|
|
266
266
|
|
|
267
267
|
# -- Step 1: System check --
|
|
268
|
-
print("─── Step 1/
|
|
268
|
+
print("─── Step 1/6: System Check ───")
|
|
269
269
|
print()
|
|
270
270
|
py_ver = platform.python_version()
|
|
271
271
|
py_ok = sys.version_info >= (3, 11)
|
|
@@ -293,7 +293,7 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
293
293
|
|
|
294
294
|
# -- Step 2: Mode selection --
|
|
295
295
|
print()
|
|
296
|
-
print("─── Step 2/
|
|
296
|
+
print("─── Step 2/6: Choose Operating Mode ───")
|
|
297
297
|
print()
|
|
298
298
|
print(" [A] Local Guardian (recommended)")
|
|
299
299
|
print(" Zero cloud. Zero LLM. Full privacy.")
|
|
@@ -340,9 +340,42 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
340
340
|
mode_names = {"a": "Local Guardian", "b": "Smart Local", "c": "Full Power"}
|
|
341
341
|
print(f"\n ✓ Mode {choice.upper()} ({mode_names[choice]}) configured")
|
|
342
342
|
|
|
343
|
-
# -- Step 3:
|
|
343
|
+
# -- Step 3: Code Knowledge Graph --
|
|
344
344
|
print()
|
|
345
|
-
print("─── Step 3/
|
|
345
|
+
print("─── Step 3/6: Code Knowledge Graph ───")
|
|
346
|
+
print()
|
|
347
|
+
print(" CodeGraph builds a structural map of your codebase using Tree-sitter.")
|
|
348
|
+
print(" It gives your AI assistant blast-radius analysis, call graphs,")
|
|
349
|
+
print(" and connects code structure to your session memories.")
|
|
350
|
+
print()
|
|
351
|
+
print(" [Y] Enable CodeGraph (recommended for developers)")
|
|
352
|
+
print(" [N] Disable CodeGraph (can enable later via config)")
|
|
353
|
+
print()
|
|
354
|
+
|
|
355
|
+
if interactive:
|
|
356
|
+
cg_choice = _prompt(" Enable Code Knowledge Graph? [Y/n] (default: Y): ", "y").lower()
|
|
357
|
+
else:
|
|
358
|
+
cg_choice = "y"
|
|
359
|
+
print(" Auto-enabling CodeGraph (non-interactive)")
|
|
360
|
+
|
|
361
|
+
code_graph_enabled = cg_choice in ("", "y", "yes")
|
|
362
|
+
|
|
363
|
+
# Write code graph config
|
|
364
|
+
_SLM_HOME.mkdir(parents=True, exist_ok=True)
|
|
365
|
+
cg_config_path = _SLM_HOME / "code_graph_config.json"
|
|
366
|
+
import json
|
|
367
|
+
cg_config_data = {"enabled": code_graph_enabled, "bridge_enabled": code_graph_enabled}
|
|
368
|
+
cg_config_path.write_text(json.dumps(cg_config_data, indent=2))
|
|
369
|
+
|
|
370
|
+
if code_graph_enabled:
|
|
371
|
+
print(f"\n ✓ CodeGraph enabled")
|
|
372
|
+
print(f" Run `slm code-graph build` in any repo to index it")
|
|
373
|
+
else:
|
|
374
|
+
print(f"\n ✓ CodeGraph disabled (enable later in {cg_config_path})")
|
|
375
|
+
|
|
376
|
+
# -- Step 4: Download embedding model --
|
|
377
|
+
print()
|
|
378
|
+
print("─── Step 4/6: Download Embedding Model ───")
|
|
346
379
|
|
|
347
380
|
if not st_ok:
|
|
348
381
|
print(" ⚠ Skipped (sentence-transformers not installed)")
|
|
@@ -354,7 +387,7 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
354
387
|
|
|
355
388
|
# -- Step 4: Download reranker model --
|
|
356
389
|
print()
|
|
357
|
-
print("─── Step
|
|
390
|
+
print("─── Step 5/6: Download Reranker Model ───")
|
|
358
391
|
|
|
359
392
|
if not st_ok:
|
|
360
393
|
print(" ⚠ Skipped (sentence-transformers not installed)")
|
|
@@ -363,7 +396,7 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
363
396
|
|
|
364
397
|
# -- Step 5: Verification --
|
|
365
398
|
print()
|
|
366
|
-
print("─── Step
|
|
399
|
+
print("─── Step 6/6: Verification ───")
|
|
367
400
|
|
|
368
401
|
if st_ok:
|
|
369
402
|
verified = _verify_installation()
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
"""Code Knowledge Graph for SuperLocalMemory.
|
|
6
|
+
|
|
7
|
+
Unifies AST-derived code structure with SLM's semantic memory.
|
|
8
|
+
Separate code_graph.db — does not touch memory.db.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from superlocalmemory.code_graph import CodeGraphService, CodeGraphConfig
|
|
12
|
+
|
|
13
|
+
config = CodeGraphConfig(enabled=True, repo_root=Path("/my/repo"))
|
|
14
|
+
service = CodeGraphService(config)
|
|
15
|
+
stats = service.get_stats()
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from superlocalmemory.code_graph.config import CodeGraphConfig
|
|
19
|
+
from superlocalmemory.code_graph.models import (
|
|
20
|
+
CodeMemoryLink,
|
|
21
|
+
EdgeKind,
|
|
22
|
+
FileRecord,
|
|
23
|
+
GraphEdge,
|
|
24
|
+
GraphNode,
|
|
25
|
+
LinkType,
|
|
26
|
+
NodeKind,
|
|
27
|
+
ParseResult,
|
|
28
|
+
)
|
|
29
|
+
from superlocalmemory.code_graph.service import (
|
|
30
|
+
CodeGraphNotEnabledError,
|
|
31
|
+
CodeGraphService,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"CodeGraphConfig",
|
|
36
|
+
"CodeGraphService",
|
|
37
|
+
"CodeGraphNotEnabledError",
|
|
38
|
+
"GraphNode",
|
|
39
|
+
"GraphEdge",
|
|
40
|
+
"FileRecord",
|
|
41
|
+
"CodeMemoryLink",
|
|
42
|
+
"ParseResult",
|
|
43
|
+
"NodeKind",
|
|
44
|
+
"EdgeKind",
|
|
45
|
+
"LinkType",
|
|
46
|
+
]
|
|
@@ -0,0 +1,177 @@
|
|
|
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
|
+
"""BlastRadius — bidirectional BFS impact analysis.
|
|
6
|
+
|
|
7
|
+
Given changed files or node IDs, computes the transitive impact
|
|
8
|
+
radius on the rustworkx graph with configurable depth and node caps.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from superlocalmemory.code_graph.graph_engine import GraphEngine
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Result dataclass
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class BlastRadiusResult:
|
|
28
|
+
"""Impact analysis result."""
|
|
29
|
+
|
|
30
|
+
changed_nodes: frozenset[str] = field(default_factory=frozenset)
|
|
31
|
+
impacted_nodes: frozenset[str] = field(default_factory=frozenset)
|
|
32
|
+
impacted_files: frozenset[str] = field(default_factory=frozenset)
|
|
33
|
+
edges: tuple[tuple[str, str, dict[str, Any]], ...] = ()
|
|
34
|
+
depth_reached: int = 0
|
|
35
|
+
truncated: bool = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# BlastRadius computer
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
class BlastRadius:
|
|
43
|
+
"""Computes blast radius via bidirectional BFS on the in-memory graph.
|
|
44
|
+
|
|
45
|
+
Usage::
|
|
46
|
+
|
|
47
|
+
br = BlastRadius(engine)
|
|
48
|
+
result = br.compute(changed_files=["src/foo.py"], max_depth=2)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, engine: GraphEngine) -> None:
|
|
52
|
+
self._engine = engine
|
|
53
|
+
|
|
54
|
+
def compute(
|
|
55
|
+
self,
|
|
56
|
+
changed_files: list[str] | None = None,
|
|
57
|
+
seed_node_ids: list[str] | None = None,
|
|
58
|
+
max_depth: int = 2,
|
|
59
|
+
max_nodes: int = 500,
|
|
60
|
+
edge_kinds: set[str] | None = None,
|
|
61
|
+
direction: str = "both",
|
|
62
|
+
) -> BlastRadiusResult:
|
|
63
|
+
"""Compute the blast radius from changed files or explicit seed nodes.
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
changed_files : list of relative file paths whose nodes are seeds
|
|
68
|
+
seed_node_ids : explicit node IDs to use as seeds (additive)
|
|
69
|
+
max_depth : maximum BFS hops
|
|
70
|
+
max_nodes : cap on total visited nodes (seeds + impacted)
|
|
71
|
+
edge_kinds : restrict traversal to these edge kinds (None = all)
|
|
72
|
+
direction : "forward" (out-edges), "reverse" (in-edges), "both"
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
BlastRadiusResult with changed_nodes, impacted_nodes, etc.
|
|
77
|
+
"""
|
|
78
|
+
graph = self._engine.graph
|
|
79
|
+
index = self._engine.index
|
|
80
|
+
|
|
81
|
+
# Collect seed rx indices
|
|
82
|
+
seed_rx: set[int] = set()
|
|
83
|
+
|
|
84
|
+
if changed_files:
|
|
85
|
+
for fp in changed_files:
|
|
86
|
+
for nid, rx_idx in index.id_to_rx.items():
|
|
87
|
+
node_data = graph[rx_idx]
|
|
88
|
+
if node_data["file_path"] == fp:
|
|
89
|
+
seed_rx.add(rx_idx)
|
|
90
|
+
|
|
91
|
+
if seed_node_ids:
|
|
92
|
+
for nid in seed_node_ids:
|
|
93
|
+
rx_idx = index.id_to_rx.get(nid)
|
|
94
|
+
if rx_idx is not None:
|
|
95
|
+
seed_rx.add(rx_idx)
|
|
96
|
+
|
|
97
|
+
if not seed_rx:
|
|
98
|
+
return BlastRadiusResult()
|
|
99
|
+
|
|
100
|
+
# BFS
|
|
101
|
+
visited: set[int] = set(seed_rx)
|
|
102
|
+
frontier: set[int] = set(seed_rx)
|
|
103
|
+
impacted_rx: set[int] = set()
|
|
104
|
+
collected_edges: list[tuple[str, str, dict[str, Any]]] = []
|
|
105
|
+
depth = 0
|
|
106
|
+
truncated = False
|
|
107
|
+
|
|
108
|
+
while frontier and depth < max_depth:
|
|
109
|
+
next_frontier: set[int] = set()
|
|
110
|
+
should_break = False
|
|
111
|
+
|
|
112
|
+
for rx_idx in frontier:
|
|
113
|
+
if should_break:
|
|
114
|
+
break
|
|
115
|
+
|
|
116
|
+
# Forward (outgoing)
|
|
117
|
+
if direction in ("forward", "both"):
|
|
118
|
+
for src, tgt, edge_data in graph.out_edges(rx_idx):
|
|
119
|
+
if edge_kinds and edge_data["kind"] not in edge_kinds:
|
|
120
|
+
continue
|
|
121
|
+
if tgt not in visited:
|
|
122
|
+
if len(visited) + len(next_frontier) >= max_nodes:
|
|
123
|
+
truncated = True
|
|
124
|
+
should_break = True
|
|
125
|
+
break
|
|
126
|
+
next_frontier.add(tgt)
|
|
127
|
+
collected_edges.append((
|
|
128
|
+
index.rx_to_id[src],
|
|
129
|
+
index.rx_to_id[tgt],
|
|
130
|
+
dict(edge_data),
|
|
131
|
+
))
|
|
132
|
+
|
|
133
|
+
if should_break:
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
# Reverse (incoming)
|
|
137
|
+
if direction in ("reverse", "both"):
|
|
138
|
+
for src, tgt, edge_data in graph.in_edges(rx_idx):
|
|
139
|
+
if edge_kinds and edge_data["kind"] not in edge_kinds:
|
|
140
|
+
continue
|
|
141
|
+
if src not in visited:
|
|
142
|
+
if len(visited) + len(next_frontier) >= max_nodes:
|
|
143
|
+
truncated = True
|
|
144
|
+
should_break = True
|
|
145
|
+
break
|
|
146
|
+
next_frontier.add(src)
|
|
147
|
+
collected_edges.append((
|
|
148
|
+
index.rx_to_id[src],
|
|
149
|
+
index.rx_to_id[tgt],
|
|
150
|
+
dict(edge_data),
|
|
151
|
+
))
|
|
152
|
+
|
|
153
|
+
impacted_rx.update(next_frontier)
|
|
154
|
+
visited.update(next_frontier)
|
|
155
|
+
frontier = next_frontier
|
|
156
|
+
depth += 1
|
|
157
|
+
|
|
158
|
+
if truncated:
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
# Convert to node IDs
|
|
162
|
+
changed_ids = frozenset(index.rx_to_id[rx] for rx in seed_rx)
|
|
163
|
+
impacted_ids = frozenset(index.rx_to_id[rx] for rx in impacted_rx)
|
|
164
|
+
|
|
165
|
+
# Compute impacted file paths
|
|
166
|
+
impacted_files = frozenset(
|
|
167
|
+
graph[rx]["file_path"] for rx in impacted_rx
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return BlastRadiusResult(
|
|
171
|
+
changed_nodes=changed_ids,
|
|
172
|
+
impacted_nodes=impacted_ids,
|
|
173
|
+
impacted_files=impacted_files,
|
|
174
|
+
edges=tuple(collected_edges),
|
|
175
|
+
depth_reached=depth,
|
|
176
|
+
truncated=truncated,
|
|
177
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
"""Bridge between code_graph.db and SLM memory.db.
|
|
6
|
+
|
|
7
|
+
Five mechanisms cross-pollinate code structure with semantic memory:
|
|
8
|
+
|
|
9
|
+
1. **Entity Resolver** — Matches code mentions in fact text against
|
|
10
|
+
code graph nodes. Creates `code_memory_links` entries.
|
|
11
|
+
2. **Fact Enricher** — Appends file/function metadata to fact
|
|
12
|
+
descriptions for better auto-invoke precision.
|
|
13
|
+
3. **Hebbian Linker** — Creates/strengthens association edges when
|
|
14
|
+
two facts reference code nodes in the same call subgraph.
|
|
15
|
+
4. **Event Listeners** — Bidirectional event bus listeners bridging
|
|
16
|
+
`memory.stored` → code linking and `code_graph.node_*` → staleness.
|
|
17
|
+
5. **Temporal Checker** — Marks memories about deleted/renamed code
|
|
18
|
+
as temporally stale.
|
|
19
|
+
|
|
20
|
+
**Hard Rule (HR-1):** The bridge NEVER modifies memory.db schema.
|
|
21
|
+
All bridge data lives in code_graph.db via the `code_memory_links` table.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from superlocalmemory.code_graph.bridge.entity_resolver import EntityResolver
|
|
25
|
+
from superlocalmemory.code_graph.bridge.fact_enricher import FactEnricher
|
|
26
|
+
from superlocalmemory.code_graph.bridge.hebbian_linker import HebbianLinker
|
|
27
|
+
from superlocalmemory.code_graph.bridge.event_listeners import BridgeEventListeners
|
|
28
|
+
from superlocalmemory.code_graph.bridge.temporal_checker import TemporalChecker
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"EntityResolver",
|
|
32
|
+
"FactEnricher",
|
|
33
|
+
"HebbianLinker",
|
|
34
|
+
"BridgeEventListeners",
|
|
35
|
+
"TemporalChecker",
|
|
36
|
+
]
|