Skip to content

Commit db28a72

Browse files
authored
Merge pull request #191 from Wolfvin/fix/issue-188-semantic-query-graph-nodes
fix(semantic-query): read symbols from graph_nodes, not empty symbols table (closes #188)
2 parents ab0d789 + 6529aa8 commit db28a72

2 files changed

Lines changed: 108 additions & 43 deletions

File tree

scripts/semantic_search_engine.py

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313
or any ~80 MB embedding model. Fast to import, deterministic, no native
1414
deps. Good enough for the majority of agent "find the right file" queries.
1515
* **Reads from the existing SQLite registry.** Symbol text is built from
16-
fields already populated by ``persistent_registry`` — ``name``,
17-
``signature``, ``kind``, ``file_path``, ``language`` — so the index is
18-
always in sync with the last ``scan`` result. No separate index file to
19-
keep consistent.
16+
the ``graph_nodes`` table populated by ``scan`` via
17+
:func:`graph_model.populate_graph_tables` — fields ``name``,
18+
``node_type``, ``file``, ``line``, ``extra_json`` — so the index is
19+
always in sync with the last ``scan`` result. No separate index file
20+
to keep consistent. (Issue #188: previously read from the ``symbols``
21+
table, which is never populated by the scan flow — see
22+
:func:`_load_symbols_from_db` for details.)
2023
* **In-memory index, cached per (db_path, mtime).** Building the vocabulary
2124
is O(N_symbols) — for CodeLens's own 3000-node graph this is <100 ms.
2225
Cached so repeated ``semantic_query`` calls within a session are ~1 ms.
@@ -379,8 +382,34 @@ def clear_cache() -> None:
379382
def _load_symbols_from_db(db_path: str) -> List[Dict[str, Any]]:
380383
"""Load all symbols from the SQLite registry at ``db_path``.
381384
382-
Returns an empty list if the database doesn't exist, the ``symbols``
383-
table is missing, or SQLite is unavailable. Never raises — semantic
385+
Reads from the ``graph_nodes`` table populated by ``scan`` via
386+
:func:`graph_model.populate_graph_tables`. The legacy ``symbols``
387+
table declared by ``persistent_registry`` is never populated by the
388+
scan flow (issue #188) — only the graph tables are — so reading from
389+
``symbols`` returned an empty result set on every real workspace.
390+
391+
Column mapping (graph_nodes -> symbol dict consumed by this module):
392+
393+
node_id -> id (string node id, e.g. "auth/jwt.py:42")
394+
name -> name (symbol name)
395+
file -> file_path (relative file path)
396+
node_type -> kind (function|class|file|module|route|type|interface)
397+
line -> line_start (1-indexed line number)
398+
extra_json-> extra_json (preserves async/impl_for/status/...)
399+
400+
Fields that ``graph_nodes`` does not carry (``signature``,
401+
``language``, ``line_end``, ``hash``) are defaulted to empty/None so
402+
downstream consumers (:func:`_build_symbol_text`, :func:`semantic_query`
403+
result construction) keep working unchanged. Signatures and language
404+
are not part of the flat-registry node format produced by parsers
405+
(see ``scripts/parsers/python_parser.py`` for a representative node),
406+
so there is no signal loss in practice — the TF-IDF document is still
407+
built from name + file_path + kind + extra_json, which is enough for
408+
"find by meaning" queries.
409+
410+
Returns an empty list if the database doesn't exist, the
411+
``graph_nodes`` table is missing (e.g. workspace initialized but
412+
never scanned), or SQLite is unavailable. Never raises — semantic
384413
search is a non-breaking add-on and should degrade gracefully to
385414
"no results" rather than crash the host command.
386415
@@ -398,21 +427,47 @@ def _load_symbols_from_db(db_path: str) -> List[Dict[str, Any]]:
398427
conn = sqlite3.connect(db_path)
399428
try:
400429
conn.row_factory = sqlite3.Row
401-
# Sanity-check that the symbols table exists. If the workspace
402-
# has been initialized but never scanned, the db file exists
403-
# but has no tables.
430+
# Sanity-check that the graph_nodes table exists. If the
431+
# workspace has been initialized but never scanned, the db
432+
# file exists but has no graph tables.
404433
table = conn.execute(
405434
"SELECT name FROM sqlite_master "
406-
"WHERE type='table' AND name='symbols'"
435+
"WHERE type='table' AND name='graph_nodes'"
407436
).fetchone()
408437
if table is None:
409438
return []
410-
rows = conn.execute("SELECT * FROM symbols").fetchall()
411-
return [dict(r) for r in rows]
439+
# Map graph_nodes columns to the symbol-dict shape expected
440+
# by _build_symbol_text and semantic_query's result builder.
441+
# Columns not present in graph_nodes are filled with defaults
442+
# so the rest of the engine does not need to know about the
443+
# source table.
444+
rows = conn.execute(
445+
"""
446+
SELECT
447+
node_id AS id,
448+
name AS name,
449+
node_type AS kind,
450+
file AS file_path,
451+
line AS line_start,
452+
extra_json AS extra_json
453+
FROM graph_nodes
454+
"""
455+
).fetchall()
456+
symbols: List[Dict[str, Any]] = []
457+
for r in rows:
458+
d = dict(r)
459+
# Fill defaults for fields graph_nodes does not carry so
460+
# _build_symbol_text / result construction keep working.
461+
d.setdefault("line_end", None)
462+
d.setdefault("language", "")
463+
d.setdefault("signature", "")
464+
d.setdefault("hash", "")
465+
symbols.append(d)
466+
return symbols
412467
finally:
413468
conn.close()
414469
except sqlite3.Error as e:
415-
logger.debug(f"semantic_search_engine: failed to read symbols from {db_path}: {e}")
470+
logger.debug(f"semantic_search_engine: failed to read graph_nodes from {db_path}: {e}")
416471
return []
417472

418473

tests/test_semantic_search_engine.py

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
* IDF weighting causes rare, discriminative terms (e.g. ``jwt``) to rank
2121
higher than ubiquitous terms (e.g. ``function``).
2222
* The engine degrades gracefully — empty query, missing db, missing
23-
``symbols`` table, and ``top_k=0`` all return well-formed responses
23+
``graph_nodes`` table, and ``top_k=0`` all return well-formed responses
2424
rather than raising.
2525
* The cache invalidates on db mtime change, so a re-scan picks up new
2626
symbols.
@@ -59,50 +59,58 @@
5959
def _make_db(workspace: str, symbols):
6060
"""Create a fake ``.codelens/codelens.db`` with the given symbol rows.
6161
62-
``symbols`` is a list of dicts with keys: name, kind, file_path,
63-
line_start, language, signature, extra_json (dict, will be
64-
JSON-encoded).
62+
Mirrors what ``scan`` actually produces: rows in the ``graph_nodes``
63+
table (issue #188). The engine reads from ``graph_nodes``, not the
64+
legacy ``symbols`` table declared by ``persistent_registry`` (which
65+
the scan flow never populates).
66+
67+
``symbols`` is a list of dicts with keys: name, kind (maps to
68+
node_type), file_path (maps to file), line_start (maps to line),
69+
language, signature, extra_json (dict, will be JSON-encoded).
6570
"""
6671
codelens_dir = os.path.join(workspace, ".codelens")
6772
os.makedirs(codelens_dir, exist_ok=True)
6873
db_path = os.path.join(codelens_dir, "codelens.db")
6974
conn = sqlite3.connect(db_path)
7075
try:
76+
# Schema mirrors scripts/graph_model.py:_CREATE_GRAPH_NODES so the
77+
# engine's SELECT AS mapping has real columns to read from.
7178
conn.execute(
7279
"""
73-
CREATE TABLE symbols (
80+
CREATE TABLE graph_nodes (
7481
id INTEGER PRIMARY KEY AUTOINCREMENT,
82+
node_id TEXT NOT NULL UNIQUE,
83+
node_type TEXT NOT NULL DEFAULT 'function',
7584
name TEXT NOT NULL,
76-
kind TEXT NOT NULL DEFAULT 'function',
77-
file_path TEXT,
78-
line_start INTEGER,
79-
line_end INTEGER,
80-
language TEXT,
81-
signature TEXT,
82-
hash TEXT,
85+
file TEXT,
86+
line INTEGER,
8387
extra_json TEXT
8488
)
8589
"""
8690
)
8791
for i, s in enumerate(symbols, start=1):
8892
extra = s.get("extra_json") or {}
93+
name = s["name"]
94+
file_path = s.get("file_path", "")
95+
line = s.get("line_start") or 0
96+
# Synthesize a node_id matching the flat-registry convention
97+
# (file:line) — what populate_graph_tables would store. This
98+
# keeps tests realistic and would let future graph-traversal
99+
# integration reuse the same fixtures.
100+
node_id = s.get("id") or f"{file_path}:{line}:{name}"
89101
conn.execute(
90102
"""
91-
INSERT INTO symbols
92-
(id, name, kind, file_path, line_start, line_end,
93-
language, signature, hash, extra_json)
94-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
103+
INSERT INTO graph_nodes
104+
(id, node_id, node_type, name, file, line, extra_json)
105+
VALUES (?, ?, ?, ?, ?, ?, ?)
95106
""",
96107
(
97108
i,
98-
s["name"],
109+
node_id,
99110
s.get("kind", "function"),
100-
s.get("file_path", ""),
101-
s.get("line_start"),
102-
s.get("line_end"),
103-
s.get("language", ""),
104-
s.get("signature", ""),
105-
s.get("hash", ""),
111+
name,
112+
file_path,
113+
line,
106114
json.dumps(extra) if extra else None,
107115
),
108116
)
@@ -255,7 +263,7 @@ class TestSemanticQuery:
255263
def test_finds_auth_symbol_by_concept(self, auth_workspace):
256264
"""The core issue #11 use case: a query token that doesn't appear
257265
in any symbol NAME still surfaces relevant symbols because file
258-
paths, signatures, and kinds are all part of the TF-IDF document.
266+
paths and kinds are part of the TF-IDF document.
259267
260268
Here, the query ``"auth"`` doesn't appear in any symbol name
261269
(``verify_jwt_claims``, ``loginUser``, ``format_date``, etc.), but
@@ -380,13 +388,14 @@ def test_cache_invalidates_on_mtime_change(self, workspace):
380388
idx1 = build_index(db_path)
381389
assert len(idx1.symbols) == 1
382390

383-
# Force mtime change by sleeping then writing new symbols
391+
# Force mtime change by sleeping then writing a new graph_nodes
392+
# row (what a real re-scan would do).
384393
time.sleep(0.05)
385394
conn = sqlite3.connect(db_path)
386395
try:
387396
conn.execute(
388-
"INSERT INTO symbols (name, kind, file_path, line_start, language) "
389-
"VALUES ('bar', 'function', 'b.py', 2, 'python')"
397+
"INSERT INTO graph_nodes (node_id, node_type, name, file, line) "
398+
"VALUES ('b.py:2:bar', 'function', 'bar', 'b.py', 2)"
390399
)
391400
conn.commit()
392401
finally:
@@ -413,8 +422,9 @@ def test_missing_db_returns_empty_results(self, workspace):
413422
assert result["stats"]["total_symbols"] == 0
414423
assert result["stats"]["returned"] == 0
415424

416-
def test_db_without_symbols_table_returns_empty(self, workspace):
417-
# Create the db file but no `symbols` table
425+
def test_db_without_graph_nodes_table_returns_empty(self, workspace):
426+
# Create the db file but no `graph_nodes` table — mirrors a
427+
# workspace that has been `codelens init`-ed but never scanned.
418428
codelens_dir = os.path.join(workspace, ".codelens")
419429
os.makedirs(codelens_dir, exist_ok=True)
420430
db_path = os.path.join(codelens_dir, "codelens.db")
@@ -427,7 +437,7 @@ def test_db_without_symbols_table_returns_empty(self, workspace):
427437
assert result["status"] == "ok"
428438
assert result["results"] == []
429439

430-
def test_empty_symbols_table_returns_empty(self, workspace):
440+
def test_empty_graph_nodes_table_returns_empty(self, workspace):
431441
_make_db(workspace, [])
432442
result = semantic_query(workspace, "anything", top_k=10)
433443
assert result["status"] == "ok"

0 commit comments

Comments
 (0)