node-walk 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,285 @@
1
+ """
2
+ SQLite implementation of GraphStore.
3
+
4
+ All SQL is contained here. The rest of the codebase only depends on
5
+ the abstract GraphStore interface from node_walk.storage.base.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import sqlite3
12
+ from pathlib import Path
13
+
14
+ from node_walk.ir.enums import Language, RelationshipType, ResolutionStatus, SymbolKind
15
+ from node_walk.ir.models import (
16
+ AnalysisResult,
17
+ FileInfo,
18
+ Relationship,
19
+ SourceLocation,
20
+ Symbol,
21
+ )
22
+ from node_walk.storage import schema as _schema
23
+ from node_walk.storage.base import GraphStore
24
+
25
+
26
+ class SQLiteGraphStore(GraphStore):
27
+ """
28
+ SQLite-backed graph store.
29
+
30
+ Usage::
31
+
32
+ store = SQLiteGraphStore(".node_walk/graph.db")
33
+ store.store_results(analysis_results)
34
+ """
35
+
36
+ def __init__(self, db_path: str | Path) -> None:
37
+ self._db_path = Path(db_path)
38
+ self._db_path.parent.mkdir(parents=True, exist_ok=True)
39
+ self._conn = sqlite3.connect(str(self._db_path), check_same_thread=False)
40
+ self._conn.row_factory = sqlite3.Row
41
+ _schema.initialize(self._conn)
42
+
43
+ def close(self) -> None:
44
+ self._conn.close()
45
+
46
+ # ------------------------------------------------------------------
47
+ # Write operations
48
+ # ------------------------------------------------------------------
49
+
50
+ def store_result(self, result: AnalysisResult) -> None:
51
+ self.store_results([result])
52
+
53
+ def store_results(self, results: list[AnalysisResult]) -> None:
54
+ """Bulk-insert all files, symbols, and relationships in a single transaction."""
55
+ with self._conn:
56
+ for res in results:
57
+ self._upsert_file(res.file)
58
+ for sym in res.symbols:
59
+ self._insert_symbol(sym)
60
+ for rel in res.relationships:
61
+ self._insert_relationship(rel)
62
+
63
+ def clear(self) -> None:
64
+ with self._conn:
65
+ self._conn.execute("DELETE FROM relationships")
66
+ self._conn.execute("DELETE FROM symbols")
67
+ self._conn.execute("DELETE FROM files")
68
+
69
+ def update_relationship(
70
+ self, rel_id: str, target_id: str, resolution: ResolutionStatus
71
+ ) -> None:
72
+ with self._conn:
73
+ self._conn.execute(
74
+ "UPDATE relationships SET target_id = ?, resolution = ? WHERE id = ?",
75
+ (target_id, resolution.value, rel_id),
76
+ )
77
+
78
+ # ------------------------------------------------------------------
79
+ # Read operations
80
+ # ------------------------------------------------------------------
81
+
82
+ def get_file(self, file_id: str) -> FileInfo | None:
83
+ row = self._conn.execute(
84
+ "SELECT * FROM files WHERE id = ?", (file_id,)
85
+ ).fetchone()
86
+ return self._row_to_file(row) if row else None
87
+
88
+ def get_all_files(self) -> list[FileInfo]:
89
+ rows = self._conn.execute("SELECT * FROM files ORDER BY path").fetchall()
90
+ return [self._row_to_file(r) for r in rows]
91
+
92
+ def get_symbol(self, symbol_id: str) -> Symbol | None:
93
+ row = self._conn.execute(
94
+ "SELECT * FROM symbols WHERE id = ?", (symbol_id,)
95
+ ).fetchone()
96
+ return self._row_to_symbol(row) if row else None
97
+
98
+ def find_symbols_by_name(self, name: str, exact: bool = False) -> list[Symbol]:
99
+ if exact:
100
+ rows = self._conn.execute(
101
+ "SELECT * FROM symbols WHERE name = ? ORDER BY qualified_name",
102
+ (name,),
103
+ ).fetchall()
104
+ else:
105
+ rows = self._conn.execute(
106
+ "SELECT * FROM symbols WHERE name LIKE ? ORDER BY qualified_name",
107
+ (f"%{name}%",),
108
+ ).fetchall()
109
+ return [self._row_to_symbol(r) for r in rows]
110
+
111
+ def find_symbols_by_qualified_name(self, qname: str) -> list[Symbol]:
112
+ rows = self._conn.execute(
113
+ "SELECT * FROM symbols WHERE qualified_name = ? OR qualified_name LIKE ?",
114
+ (qname, f"%{qname}%"),
115
+ ).fetchall()
116
+ return [self._row_to_symbol(r) for r in rows]
117
+
118
+ def get_all_symbols(self) -> list[Symbol]:
119
+ rows = self._conn.execute(
120
+ "SELECT * FROM symbols ORDER BY qualified_name"
121
+ ).fetchall()
122
+ return [self._row_to_symbol(r) for r in rows]
123
+
124
+ def get_relationships_from(
125
+ self, symbol_id: str, rel_type: RelationshipType | None = None
126
+ ) -> list[Relationship]:
127
+ if rel_type:
128
+ rows = self._conn.execute(
129
+ "SELECT * FROM relationships WHERE source_id = ? AND type = ?",
130
+ (symbol_id, rel_type.value),
131
+ ).fetchall()
132
+ else:
133
+ rows = self._conn.execute(
134
+ "SELECT * FROM relationships WHERE source_id = ?", (symbol_id,)
135
+ ).fetchall()
136
+ return [self._row_to_rel(r) for r in rows]
137
+
138
+ def get_relationships_to(
139
+ self, symbol_id: str, rel_type: RelationshipType | None = None
140
+ ) -> list[Relationship]:
141
+ if rel_type:
142
+ rows = self._conn.execute(
143
+ "SELECT * FROM relationships WHERE target_id = ? AND type = ?",
144
+ (symbol_id, rel_type.value),
145
+ ).fetchall()
146
+ else:
147
+ rows = self._conn.execute(
148
+ "SELECT * FROM relationships WHERE target_id = ?", (symbol_id,)
149
+ ).fetchall()
150
+ return [self._row_to_rel(r) for r in rows]
151
+
152
+ def get_all_unresolved_relationships(self) -> list[Relationship]:
153
+ rows = self._conn.execute(
154
+ "SELECT * FROM relationships WHERE target_id = '' OR resolution = 'unresolved'"
155
+ ).fetchall()
156
+ return [self._row_to_rel(r) for r in rows]
157
+
158
+ def stats(self) -> dict[str, int]:
159
+ files = self._conn.execute("SELECT COUNT(*) FROM files").fetchone()[0]
160
+ symbols = self._conn.execute("SELECT COUNT(*) FROM symbols").fetchone()[0]
161
+ rels = self._conn.execute("SELECT COUNT(*) FROM relationships").fetchone()[0]
162
+ unresolved = self._conn.execute(
163
+ "SELECT COUNT(*) FROM relationships WHERE target_id = ''"
164
+ ).fetchone()[0]
165
+
166
+ kind_rows = self._conn.execute(
167
+ "SELECT kind, COUNT(*) as cnt FROM symbols GROUP BY kind"
168
+ ).fetchall()
169
+ kind_counts = {f"symbols_{r['kind'].lower()}": r["cnt"] for r in kind_rows}
170
+
171
+ rel_rows = self._conn.execute(
172
+ "SELECT type, COUNT(*) as cnt FROM relationships GROUP BY type"
173
+ ).fetchall()
174
+ rel_counts = {f"rel_{r['type'].lower()}": r["cnt"] for r in rel_rows}
175
+
176
+ return {
177
+ "files": files,
178
+ "symbols": symbols,
179
+ "relationships": rels,
180
+ "unresolved_relationships": unresolved,
181
+ **kind_counts,
182
+ **rel_counts,
183
+ }
184
+
185
+ # ------------------------------------------------------------------
186
+ # Private helpers — insert
187
+ # ------------------------------------------------------------------
188
+
189
+ def _upsert_file(self, file: FileInfo) -> None:
190
+ self._conn.execute(
191
+ """
192
+ INSERT INTO files (id, path, language, content_hash, size_bytes)
193
+ VALUES (?, ?, ?, ?, ?)
194
+ ON CONFLICT(path) DO UPDATE SET
195
+ language = excluded.language,
196
+ content_hash = excluded.content_hash,
197
+ size_bytes = excluded.size_bytes,
198
+ indexed_at = datetime('now')
199
+ """,
200
+ (file.id, file.path, file.language.value, file.content_hash, file.size_bytes),
201
+ )
202
+
203
+ def _insert_symbol(self, sym: Symbol) -> None:
204
+ self._conn.execute(
205
+ """
206
+ INSERT OR IGNORE INTO symbols
207
+ (id, name, qualified_name, kind, language, file_id,
208
+ start_line, end_line, signature, parent_id, docstring, is_async)
209
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
210
+ """,
211
+ (
212
+ sym.id, sym.name, sym.qualified_name, sym.kind.value,
213
+ sym.language.value, sym.file_id, sym.start_line, sym.end_line,
214
+ sym.signature, sym.parent_id, sym.docstring, int(sym.is_async),
215
+ ),
216
+ )
217
+
218
+ def _insert_relationship(self, rel: Relationship) -> None:
219
+ loc = rel.source_location
220
+ self._conn.execute(
221
+ """
222
+ INSERT OR IGNORE INTO relationships
223
+ (id, source_id, target_id, type, source_file_id, source_line,
224
+ source_col, resolution, metadata_json)
225
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
226
+ """,
227
+ (
228
+ rel.id, rel.source_id, rel.target_id, rel.type.value,
229
+ loc.file_id if loc else None,
230
+ loc.line if loc else None,
231
+ loc.col if loc else None,
232
+ rel.resolution.value,
233
+ json.dumps(rel.metadata),
234
+ ),
235
+ )
236
+
237
+ # ------------------------------------------------------------------
238
+ # Private helpers — row → model conversion
239
+ # ------------------------------------------------------------------
240
+
241
+ @staticmethod
242
+ def _row_to_file(row: sqlite3.Row) -> FileInfo:
243
+ return FileInfo(
244
+ id=row["id"],
245
+ path=row["path"],
246
+ language=Language(row["language"]),
247
+ content_hash=row["content_hash"],
248
+ size_bytes=row["size_bytes"],
249
+ )
250
+
251
+ @staticmethod
252
+ def _row_to_symbol(row: sqlite3.Row) -> Symbol:
253
+ return Symbol(
254
+ id=row["id"],
255
+ name=row["name"],
256
+ qualified_name=row["qualified_name"],
257
+ kind=SymbolKind(row["kind"]),
258
+ language=Language(row["language"]),
259
+ file_id=row["file_id"],
260
+ start_line=row["start_line"],
261
+ end_line=row["end_line"],
262
+ signature=row["signature"] or "",
263
+ parent_id=row["parent_id"],
264
+ docstring=row["docstring"] or "",
265
+ is_async=bool(row["is_async"]),
266
+ )
267
+
268
+ @staticmethod
269
+ def _row_to_rel(row: sqlite3.Row) -> Relationship:
270
+ loc: SourceLocation | None = None
271
+ if row["source_file_id"] and row["source_line"]:
272
+ loc = SourceLocation(
273
+ file_id=row["source_file_id"],
274
+ line=row["source_line"],
275
+ col=row["source_col"] or 0,
276
+ )
277
+ return Relationship(
278
+ id=row["id"],
279
+ source_id=row["source_id"],
280
+ target_id=row["target_id"] or "",
281
+ type=RelationshipType(row["type"]),
282
+ source_location=loc,
283
+ resolution=ResolutionStatus(row["resolution"]),
284
+ metadata=json.loads(row["metadata_json"]),
285
+ )
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.5
2
+ Name: node-walk
3
+ Version: 0.1.0
4
+ Summary: Semantic code intelligence — local-first graph of your codebase for humans and LLMs
5
+ Author: CodeGraph Contributors
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: code-analysis,developer-tools,llm,semantic-graph
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: pydantic>=2.0.0
16
+ Requires-Dist: rich>=13.0.0
17
+ Requires-Dist: tree-sitter-python>=0.23.0
18
+ Requires-Dist: tree-sitter>=0.23.0
19
+ Requires-Dist: typer>=0.12.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
22
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # CodeGraph
26
+
27
+ **Semantic code intelligence for humans and LLMs.**
28
+
29
+ Local-first · Lightweight · Python first · SQLite backed
30
+
31
+ ---
32
+
33
+ ## Quickstart
34
+
35
+ ### 1. Install & Setup
36
+ ```bash
37
+ # Clone and setup the environment
38
+ git clone <repo-url> CodeGraph
39
+ cd CodeGraph
40
+ python -m venv .venv
41
+ .venv\Scripts\activate # Windows (or source .venv/bin/activate on Unix)
42
+ pip install -e ".[dev]"
43
+ ```
44
+
45
+ ### 2. Indexing and Querying
46
+ All CLI commands discover the graph database by searching for a `.node_walk/` directory in the current directory or walking up parent directories.
47
+
48
+ **To index any directory:**
49
+ ```bash
50
+ # Activate CodeGraph venv, then go to the repository you want to index
51
+ cd /path/to/your/project
52
+ node_walk index .
53
+ ```
54
+
55
+ **Run queries within that project's directory:**
56
+ ```bash
57
+ # Find a symbol
58
+ node_walk find UserService
59
+
60
+ # See its definition
61
+ node_walk definition UserService
62
+
63
+ # Find callers (shows interactive picker if multiple symbols match)
64
+ node_walk callers UserService.create_user
65
+
66
+ # Trace outgoing dependencies (out-edges)
67
+ node_walk trace UserService.create_user --depth 5
68
+
69
+ # Blast radius (in-edges - what breaks if this changes?)
70
+ node_walk blast-radius UserService.create_user
71
+
72
+ # View source code of a symbol
73
+ node_walk source UserService.create_user
74
+
75
+ # Graph statistics
76
+ node_walk stats
77
+
78
+ # Export to JSON
79
+ node_walk export --output graph.json
80
+ ```
81
+
82
+
83
+ ## Architecture
84
+
85
+ ```
86
+ Repository → Language Analysis → Code IR → SQLite Graph → Query Engine → CLI
87
+ ```
88
+
89
+ - **Language analysis**: Tree-sitter based Python parser
90
+ - **Code IR**: Pydantic v2 models — Symbol, Relationship, FileInfo
91
+ - **Storage**: SQLite with 10 indexes, WAL mode
92
+ - **Query engine**: Pure SQLite including recursive CTEs for graph traversal
93
+ - **CLI**: Typer + Rich
94
+
95
+ ## Development
96
+
97
+ ```bash
98
+ python -m venv .venv
99
+ .venv\Scripts\activate # Windows
100
+ pip install -e ".[dev]"
101
+ pytest tests/ -v
102
+ ```
103
+
104
+ ## Graph data
105
+
106
+ Stored in `.node_walk/graph.db` inside the indexed repository.
107
+ Safe to delete and re-index at any time.
@@ -0,0 +1,26 @@
1
+ node_walk/__init__.py,sha256=lyJBhaMT5oq2AaZrboYhoK_55Mnk5C4zIUhMXP6LcgA,91
2
+ node_walk/indexer.py,sha256=RJuMeb2uMDUp4iShzOm9EaaRloFF0uwvWGbKrc9htws,6666
3
+ node_walk/analysis/__init__.py,sha256=mb15--gtv-wpXn4jHq8MH6Rgx9ZlifBdMgWvxz1bAgE,394
4
+ node_walk/analysis/base.py,sha256=4mYsZA7Vr6CEMIQtAzfE7kqjPg14bFWizoON_b9EI7Y,6440
5
+ node_walk/analysis/python_analyzer.py,sha256=2gfwkdrszY4GilV48SXpGQmsEBmDauHu33ppg6JjUoA,492
6
+ node_walk/analysis/python/__init__.py,sha256=IwtnjMD5df-Av70dT1MfCYvLWKU4ea3kz7jlGfbrr04,230
7
+ node_walk/analysis/python/analyzer.py,sha256=D230OhlRpImwuWVsM8JDgQP4Hb3R3QI4RCunM83qKeI,1435
8
+ node_walk/analysis/python/scope.py,sha256=8J62GeH1Ouy77xdJnj9Wo3XHyw92BI5g7yBJvg3Fbe0,1320
9
+ node_walk/analysis/python/visitor.py,sha256=0bMzgXUl5l9aCSyoFgpIEQL8QojLlSarTAHWI7WqaR4,21734
10
+ node_walk/cli/__init__.py,sha256=EuI73cd5fRn3eHKATNRerJCyomrPvDfvss_6PiResFA,50
11
+ node_walk/cli/main.py,sha256=C0sGKa7iLKYHm6imrGDq7xfnuvRHP8DrTiOsEkBeJR4,19718
12
+ node_walk/ir/__init__.py,sha256=0oQQSIpYwTRWHuJXv1IvDAeUwwh1PphRIhnpglqGpPM,741
13
+ node_walk/ir/enums.py,sha256=BvgDkxm7YKm6hAQjVVdST9HwZkYM-oJkBDEK1mWUduc,1764
14
+ node_walk/ir/models.py,sha256=ai1Up6Ke5vSnkLJkp1oye970tEmrS1DU5Ewj69lEIGs,3300
15
+ node_walk/query/__init__.py,sha256=XpqXvMPtbn_i382jxIKOYXQRPm7sus6YZv1ZCkf4ZJo,50
16
+ node_walk/query/engine.py,sha256=RJlY-zgGdcFrI6PZlM06yyTxJ9_2Y7HtcEMd4EjypR0,13887
17
+ node_walk/storage/__init__.py,sha256=2P4IgasCF48CXlewNakplrEZTx9G3jvrYbtE_N1Yqd8,383
18
+ node_walk/storage/base.py,sha256=FfErayseFMHuZynKoTnmGDNIloSIBiEjHDYx9LlwSdM,2419
19
+ node_walk/storage/repository.py,sha256=7t9GoJCe5aGeQX3lR3HjSOI8IYxwu0NusDIV-RFFykk,471
20
+ node_walk/storage/schema.py,sha256=l86Ojd3Biswzda_1N9flGXrZNL_gtHYnfjCfOTO6w0s,4461
21
+ node_walk/storage/sqlite_store.py,sha256=RK1-dN4PIM5iwh-2NhCkbMNlMQL_CA0tG2qmQtdaJVo,10599
22
+ node_walk-0.1.0.dist-info/METADATA,sha256=aH3NFkim2D7t2R3PVSiSuScYfoAme1Eqqb9J5OApTkE,2839
23
+ node_walk-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
24
+ node_walk-0.1.0.dist-info/entry_points.txt,sha256=GfEbFx9MiZz2i7__MlP93hHCPZT3Xw1rO7dniuZgoag,122
25
+ node_walk-0.1.0.dist-info/licenses/LICENSE,sha256=JALI9baeqOBYv5lOMAqMVUDxpuqHN0fex7dRuUfetO8,1069
26
+ node_walk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ node-walk = node_walk.cli.main:app
3
+ node_walk = node_walk.cli.main:app
4
+ nodewalk = node_walk.cli.main:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shriram Naik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.