Navegador

feat: Python SDK — programmatic API for building on navegador Navegador class wraps all internal modules: ingestion, context loading, knowledge management, search, export/import. Fully documented. Closes #33

lmata 2026-03-23 04:59 trunk
Commit 0392cfc1ca953100084d05b45fe23f0f7d9da5aaedbfd0cee81d9e0cbde2634f
--- navegador/__init__.py
+++ navegador/__init__.py
@@ -2,5 +2,9 @@
22
Navegador — AST + knowledge graph context engine for AI coding agents.
33
"""
44
55
__version__ = "0.1.0"
66
__author__ = "CONFLICT LLC"
7
+
8
+from navegador.sdk import Navegador
9
+
10
+__all__ = ["Navegador"]
711
812
ADDED navegador/sdk.py
913
ADDED tests/test_sdk.py
--- navegador/__init__.py
+++ navegador/__init__.py
@@ -2,5 +2,9 @@
2 Navegador — AST + knowledge graph context engine for AI coding agents.
3 """
4
5 __version__ = "0.1.0"
6 __author__ = "CONFLICT LLC"
 
 
 
 
7
8 DDED navegador/sdk.py
9 DDED tests/test_sdk.py
--- navegador/__init__.py
+++ navegador/__init__.py
@@ -2,5 +2,9 @@
2 Navegador — AST + knowledge graph context engine for AI coding agents.
3 """
4
5 __version__ = "0.1.0"
6 __author__ = "CONFLICT LLC"
7
8 from navegador.sdk import Navegador
9
10 __all__ = ["Navegador"]
11
12 DDED navegador/sdk.py
13 DDED tests/test_sdk.py
--- a/navegador/sdk.py
+++ b/navegador/sdk.py
@@ -0,0 +1,350 @@
1
+"""
2
+Navegador Python SDK — high-level programmatic API.
3
+
4
+Wraps the internal graph, ingestion, and context modules into a single
5
+clean interface suitable for building tools on top of navegador.
6
+
7
+Usage::
8
+
9
+ from navegador import Navegador
10
+
11
+ # SQLite (local, zero-infra)
12
+ nav = Navegador.sqlite(".navegador/graph.db")
13
+
14
+ # Redis (production)
15
+ nav = Navegador.redis("redis://localhost:6379")
16
+
17
+ # Ingest a codebase
18
+ stats = nav.ingest("/path/to/repo")
19
+
20
+ # Query context
21
+ bundle = nav.function_context("validate_token")
22
+ bundle = nav.class_context("AuthService")
23
+ bundle = nav.file_context("src/auth.py")
24
+
25
+ # Knowledge graph
26
+ nav.add_domain("auth", description="Authentication layer")
27
+ nav.add_concept("JWT", domain="auth")
28
+ nav.annotate("validate_token", "Function", concept="JWT")
29
+
30
+ # Search
31
+ results = nav.search("validate")
32
+ results = nav.search_all("JWT")
33
+ results = nav.search_knowledge("token")
34
+
35
+ # Raw Cypher
36
+ result = nav.query("MATCH (n:Function) RETURN n.name LIMIT 10")
37
+
38
+ # Graph admin
39
+ counts = nav.stats()
40
+ nav.export(".navegador/graph.jsonl")
41
+ nav.import_graph(".navegador/graph.jsonl")
42
+ nav.clear()
43
+"""
44
+
45
+from __future__ import annotations
46
+
47
+from typing import Any
48
+
49
+
50
+class Navegador:
51
+ """High-level Python SDK for programmatic use of navegador."""
52
+
53
+ def __init__(self, store: Any) -> None:
54
+ """
55
+ Initialise
56
+ with an existing Gra
57
+ ) self._storthods :meth:`sqlite` and :meth:`redis` for
58
+ typical usage.
59
+ """
60
+ self._store = store
61
+
62
+ # ── Constructors ──────────────────────────────────────────────────────────
63
+
64
+ @classmethod
65
+ def sqlite(cls, db_path: str = ".navegador/graph.db") -> "Navegador":
66
+ """Create a Navegador instance backed by SQLite (zero-infra, local)."""
67
+ from navegador.graph.store import GraphStore
68
+
69
+ return cls(GraphStore.sqlite(db_path))
70
+
71
+ @classmethod
72
+ d
73
+ ef redis(cls, url: str =
74
+ def domain(self, name: str) -ked by Redis FalkorDB (production)."""
75
+ from navegador.graph.store import GraphStore
76
+
77
+ return cls(GraphStore.redis(url))
78
+
79
+ # ── Ingestion ─────────────────────────────────────────────────────────────
80
+
81
+ def ingest(
82
+ self,
83
+ repo_path: str,
84
+ clea
85
+ name, file_path=file_path, depth=depth
86
+ path=file_path, depth=depth)
87
+
88
+ def class_context(self, name: str, file_path: str = "") -> Any:
89
+ """
90
+ Return a ContextBundle for a class — methods, inheritance, references.
91
+
92
+ Args:
93
+ name: Class name.
94
+ file_path: Optional file path to narrow the match.
95
+
96
+ Returns:
97
+ :class:`~navegador.context.loader.ContextBundle`
98
+ """
99
+ from navegador.context.loader import ContextLoader
100
+
101
+ return ContextLoader(self._store).load_class(name, file_path=file_path)
102
+
103
+ def explain(self, name: str, file_path: str = "") -> Any:
104
+ """
105
+ Full picture: all inbound and outbound relationships for any node.
106
+
107
+ Spans both code and knowledge layers.
108
+
109
+ Args:
110
+ name: Node name.
111
+ file_path: Optional file path to narrow the match.
112
+
113
+ Returns:
114
+ :class:`~navegador.context.loader.ContextBundle`
115
+ """
116
+ from navegador.context.loader import ContextLoader
117
+
118
+ return ContextLoader(self._store).explain(name, file_path=file_path)
119
+
120
+ # ── Knowledge ─────────────────────────────────────────────────────────────
121
+
122
+ def add_concept(self, name: str, **kwargs: Any) -> None:
123
+ """
124
+ Add a business concept node to the knowledge graph.
125
+
126
+ Keyword arguments are forwarded to
127
+ :meth:`~navegador.ingestion.KnowledgeIngester.add_concept`.
128
+ Common kwargs: ``description``, ``domain``, ``status``.
129
+ """
130
+ from navegador.ingestion import KnowledgeIngester
131
+
132
+ KnowledgeIngester(self._store).add_concept(name, **kwargs)
133
+
134
+ def add_rule(self, name: str, **kwargs: Any) -> None:
135
+ """
136
+ Add a business rule node to the knowledge graph.
137
+
138
+ Common kwargs: ``description``, ``domain``, ``severity``, ``rationale``.
139
+ """
140
+ from navegador.ingestion import KnowledgeIngester
141
+
142
+ KnowledgeIngester(self._store).add_rule(name, **kwargs)
143
+
144
+ def add_decision(self, name: str, **kwargs: Any) -> None:
145
+ """
146
+ Add an architectural or product decision node.
147
+
148
+ Common kwargs: ``description``, ``domain``, ``status``,
149
+ ``rationale``, ``alternatives``, ``date``.
150
+ """
151
+ from navegador.ingestion import KnowledgeIngester
152
+
153
+ KnowledgeIngester(self._store).add_decision(name, **kwargs)
154
+
155
+ def add_person(self, name: str, **kwargs: Any) -> None:
156
+ """
157
+ Add a person (contributor, owner, stakeholder) node.
158
+
159
+ Common kwargs: ``email``, ``role``, ``team``.
160
+ """
161
+ from navegador.ingestion import KnowledgeIngester
162
+
163
+ KnowledgeIngester(self._store).add_person(name, **kwargs)
164
+
165
+ def add_domain(self, name: str, **kwargs: Any) -> None:
166
+ """
167
+ Add a business domain node (e.g. ``auth``, ``billing``).
168
+
169
+ Common kwargs: ``description``.
170
+ """
171
+ from navegador.ingestion import KnowledgeIngester
172
+
173
+ KnowledgeIngester(self._store).add_domain(name, **kwargs)
174
+
175
+ def annotate(
176
+ self,
177
+ code_name: str,
178
+ code_label: str,
179
+ concept: str | None = None,
180
+ rule: str | None = None,
181
+ ) -> None:
182
+ """
183
+ Link a code node to a concept or rule via ``ANNOTATES``.
184
+
185
+ Args:
186
+ code_name: Name of the code symbol.
187
+ code_label: Node label — one of ``Function``, ``Class``,
188
+ ``Method``, ``File``, ``Module``.
189
+ concept: Concept name to link to (optional).
190
+ rule: Rule name to link to (optional).
191
+ """
192
+ from navegador.ingestion import KnowledgeIngester
193
+
194
+ KnowledgeIngester(self._store).annotate_code(
195
+ code_name, code_label, concept=concept, rule=rule
196
+ )
197
+
198
+ def concept(self, name: str) -> Any:
199
+ """
200
+ Load a concept context bundle — rules, related concepts, code.
201
+
202
+ Returns:
203
+ :class:`~navegador.context.loader.ContextBundle`
204
+ """
205
+ from navegador.context.loader import ContextLoader
206
+
207
+ return ContextLoader(self._store).load_concept(name)
208
+
209
+ def domain(self, name: str) -> Any:
210
+ """
211
+ Load a domain context bundle — everything belonging to that domain.
212
+
213
+ Returns:
214
+ :class:`~navegador.context.loader.ContextBundle`
215
+ """
216
+ from navegador.context.loader import ContextLoader
217
+
218
+ return ContextLoader(self._store).load_domain(name)
219
+
220
+ def decision(self, name: str) -> Any:
221
+ """
222
+ Load a decision rationale bundle — alternatives, status, related nodes.
223
+
224
+ Returns:
225
+ :class:`~navegador.context.loader.ContextBundle`
226
+ """
227
+ from navegador.context.loader import ContextLoader
228
+
229
+ return ContextLoader(self._store).load_decision(name)
230
+
231
+ # ── Search ────────────────────────────────────────────────────────────────
232
+
233
+ def search(self, query: str, limit: int = 20) -> list[Any]:
234
+ """
235
+ Search code symbols by name.
236
+
237
+ Returns:
238
+ List of :class:`~navegador.context.loader.ContextNode`.
239
+ """
240
+ from navegador.context.loader import ContextLoader
241
+
242
+ return ContextLoader(self._store).search(query, limit=limit)
243
+
244
+ def search_all(self, query: str, limit: int = 20) -> list[Any]:
245
+ """
246
+ Search everything — code symbols, concepts, rules, decisions, wiki.
247
+
248
+ Returns:
249
+ List of :class:`~navegador.context.loader.ContextNode`.
250
+ """
251
+ from navegador.context.loader import ContextLoader
252
+
253
+ return ContextLoader(self._store).search_all(query, limit=limit)
254
+
255
+ def search_knowledge(self, query: str, limit: int = 20) -> list[Any]:
256
+ """
257
+ Search the knowledge layer — concepts, rules, decisions, wiki pages.
258
+
259
+ Returns:
260
+ List of :class:`~navegador.context.loader.ContextNode`.
261
+ """
262
+ from navegador.context.loader import ContextLoader
263
+
264
+ return ContextLoader(self._store).search_knowledge(query, limit=limit)
265
+
266
+ # ── Graph ─────────────────────────────────────────────────────────────────
267
+
268
+ def query(self, cypher: str, params: dict[str, Any] | None = None) -> Any:
269
+ """
270
+ Execute a raw Cypher query against the graph.
271
+
272
+ Args:
273
+ cypher: Cypher query string.
274
+ params: Optional parameter dict.
275
+
276
+ Returns:
277
+ FalkorDB result object (has ``.result_set`` attribute).
278
+ """
279
+ return self._store.query(cypher, params)
280
+
281
+ def stats(self) -> dict[str, Any]:
282
+ """
283
+ Return graph statistics broken down by node and edge type.
284
+
285
+ Returns:
286
+ Dict with keys ``total_nodes``, ``total_edges``, ``nodes``
287
+ (label → count), ``edges`` (type → count).
288
+ """
289
+ from navegador.graph import queries as q
290
+
291
+ node_rows = self._store.query(q.NODE_TYPE_COUNTS).result_set or []
292
+ edge_rows = self._store.query(q.EDGE_TYPE_COUNTS).result_set or []
293
+
294
+ return {
295
+ "total_nodes": sum(r[1] for r in node_rows),
296
+ "total_edges": sum(r[1] for r in edge_rows),
297
+ "nodes": {r[0]: r[1] for r in node_rows},
298
+ "edges": {r[0]: r[1] for r in edge_rows},
299
+ }
300
+
301
+ def export(self, output_path: str) -> dict[str, int]:
302
+ """
303
+ Export the full graph to a JSONL file.
304
+
305
+ Args:
306
+ output_path: Destination file path.
307
+
308
+ Returns:
309
+ Dict with counts: ``nodes``, ``edges``.
310
+ """
311
+ from navegador.graph.export import export_graph
312
+
313
+ return export_graph(self._store, output_path)
314
+
315
+ def import_graph(self, input_path: str, clear: bool = True) -> dict[str, int]:
316
+ """
317
+ Import a graph from a JSONL export file.
318
+
319
+ Args:
320
+ input_path: Source JSONL file path.
321
+ clear: Wipe the graph before importing (default ``True``).
322
+
323
+ Returns:
324
+ Dict with counts: ``nodes``, ``edges``.
325
+ """
326
+ from navegador.graph.export import import_graph
327
+
328
+ return import_graph(self._store, input_path, clear=clear)
329
+
330
+ def clear(self) -> None:
331
+ """Delete all nodes and edges in the graph."""
332
+ self._store.clear()
333
+
334
+ # ── Owners ────────────────────────────────────────────────────────────────
335
+
336
+ def find_owners(self, name: str, file_path: str = "") -> list[Any]:
337
+ """
338
+ Find people assigned as owners of a named node.
339
+
340
+ Args:
341
+ name: Node name.
342
+ file_path: Optional file path to narrow the match.
343
+
344
+ Returns:
345
+ List of :class:`~navegador.context.loader.ContextNode` with
346
+ ``type="Person"``.
347
+ """
348
+ from navegador.context.loader import ContextLoader
349
+
350
+ return ContextLoader(self._store).find_owners(
--- a/navegador/sdk.py
+++ b/navegador/sdk.py
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/navegador/sdk.py
+++ b/navegador/sdk.py
@@ -0,0 +1,350 @@
1 """
2 Navegador Python SDK — high-level programmatic API.
3
4 Wraps the internal graph, ingestion, and context modules into a single
5 clean interface suitable for building tools on top of navegador.
6
7 Usage::
8
9 from navegador import Navegador
10
11 # SQLite (local, zero-infra)
12 nav = Navegador.sqlite(".navegador/graph.db")
13
14 # Redis (production)
15 nav = Navegador.redis("redis://localhost:6379")
16
17 # Ingest a codebase
18 stats = nav.ingest("/path/to/repo")
19
20 # Query context
21 bundle = nav.function_context("validate_token")
22 bundle = nav.class_context("AuthService")
23 bundle = nav.file_context("src/auth.py")
24
25 # Knowledge graph
26 nav.add_domain("auth", description="Authentication layer")
27 nav.add_concept("JWT", domain="auth")
28 nav.annotate("validate_token", "Function", concept="JWT")
29
30 # Search
31 results = nav.search("validate")
32 results = nav.search_all("JWT")
33 results = nav.search_knowledge("token")
34
35 # Raw Cypher
36 result = nav.query("MATCH (n:Function) RETURN n.name LIMIT 10")
37
38 # Graph admin
39 counts = nav.stats()
40 nav.export(".navegador/graph.jsonl")
41 nav.import_graph(".navegador/graph.jsonl")
42 nav.clear()
43 """
44
45 from __future__ import annotations
46
47 from typing import Any
48
49
50 class Navegador:
51 """High-level Python SDK for programmatic use of navegador."""
52
53 def __init__(self, store: Any) -> None:
54 """
55 Initialise
56 with an existing Gra
57 ) self._storthods :meth:`sqlite` and :meth:`redis` for
58 typical usage.
59 """
60 self._store = store
61
62 # ── Constructors ──────────────────────────────────────────────────────────
63
64 @classmethod
65 def sqlite(cls, db_path: str = ".navegador/graph.db") -> "Navegador":
66 """Create a Navegador instance backed by SQLite (zero-infra, local)."""
67 from navegador.graph.store import GraphStore
68
69 return cls(GraphStore.sqlite(db_path))
70
71 @classmethod
72 d
73 ef redis(cls, url: str =
74 def domain(self, name: str) -ked by Redis FalkorDB (production)."""
75 from navegador.graph.store import GraphStore
76
77 return cls(GraphStore.redis(url))
78
79 # ── Ingestion ─────────────────────────────────────────────────────────────
80
81 def ingest(
82 self,
83 repo_path: str,
84 clea
85 name, file_path=file_path, depth=depth
86 path=file_path, depth=depth)
87
88 def class_context(self, name: str, file_path: str = "") -> Any:
89 """
90 Return a ContextBundle for a class — methods, inheritance, references.
91
92 Args:
93 name: Class name.
94 file_path: Optional file path to narrow the match.
95
96 Returns:
97 :class:`~navegador.context.loader.ContextBundle`
98 """
99 from navegador.context.loader import ContextLoader
100
101 return ContextLoader(self._store).load_class(name, file_path=file_path)
102
103 def explain(self, name: str, file_path: str = "") -> Any:
104 """
105 Full picture: all inbound and outbound relationships for any node.
106
107 Spans both code and knowledge layers.
108
109 Args:
110 name: Node name.
111 file_path: Optional file path to narrow the match.
112
113 Returns:
114 :class:`~navegador.context.loader.ContextBundle`
115 """
116 from navegador.context.loader import ContextLoader
117
118 return ContextLoader(self._store).explain(name, file_path=file_path)
119
120 # ── Knowledge ─────────────────────────────────────────────────────────────
121
122 def add_concept(self, name: str, **kwargs: Any) -> None:
123 """
124 Add a business concept node to the knowledge graph.
125
126 Keyword arguments are forwarded to
127 :meth:`~navegador.ingestion.KnowledgeIngester.add_concept`.
128 Common kwargs: ``description``, ``domain``, ``status``.
129 """
130 from navegador.ingestion import KnowledgeIngester
131
132 KnowledgeIngester(self._store).add_concept(name, **kwargs)
133
134 def add_rule(self, name: str, **kwargs: Any) -> None:
135 """
136 Add a business rule node to the knowledge graph.
137
138 Common kwargs: ``description``, ``domain``, ``severity``, ``rationale``.
139 """
140 from navegador.ingestion import KnowledgeIngester
141
142 KnowledgeIngester(self._store).add_rule(name, **kwargs)
143
144 def add_decision(self, name: str, **kwargs: Any) -> None:
145 """
146 Add an architectural or product decision node.
147
148 Common kwargs: ``description``, ``domain``, ``status``,
149 ``rationale``, ``alternatives``, ``date``.
150 """
151 from navegador.ingestion import KnowledgeIngester
152
153 KnowledgeIngester(self._store).add_decision(name, **kwargs)
154
155 def add_person(self, name: str, **kwargs: Any) -> None:
156 """
157 Add a person (contributor, owner, stakeholder) node.
158
159 Common kwargs: ``email``, ``role``, ``team``.
160 """
161 from navegador.ingestion import KnowledgeIngester
162
163 KnowledgeIngester(self._store).add_person(name, **kwargs)
164
165 def add_domain(self, name: str, **kwargs: Any) -> None:
166 """
167 Add a business domain node (e.g. ``auth``, ``billing``).
168
169 Common kwargs: ``description``.
170 """
171 from navegador.ingestion import KnowledgeIngester
172
173 KnowledgeIngester(self._store).add_domain(name, **kwargs)
174
175 def annotate(
176 self,
177 code_name: str,
178 code_label: str,
179 concept: str | None = None,
180 rule: str | None = None,
181 ) -> None:
182 """
183 Link a code node to a concept or rule via ``ANNOTATES``.
184
185 Args:
186 code_name: Name of the code symbol.
187 code_label: Node label — one of ``Function``, ``Class``,
188 ``Method``, ``File``, ``Module``.
189 concept: Concept name to link to (optional).
190 rule: Rule name to link to (optional).
191 """
192 from navegador.ingestion import KnowledgeIngester
193
194 KnowledgeIngester(self._store).annotate_code(
195 code_name, code_label, concept=concept, rule=rule
196 )
197
198 def concept(self, name: str) -> Any:
199 """
200 Load a concept context bundle — rules, related concepts, code.
201
202 Returns:
203 :class:`~navegador.context.loader.ContextBundle`
204 """
205 from navegador.context.loader import ContextLoader
206
207 return ContextLoader(self._store).load_concept(name)
208
209 def domain(self, name: str) -> Any:
210 """
211 Load a domain context bundle — everything belonging to that domain.
212
213 Returns:
214 :class:`~navegador.context.loader.ContextBundle`
215 """
216 from navegador.context.loader import ContextLoader
217
218 return ContextLoader(self._store).load_domain(name)
219
220 def decision(self, name: str) -> Any:
221 """
222 Load a decision rationale bundle — alternatives, status, related nodes.
223
224 Returns:
225 :class:`~navegador.context.loader.ContextBundle`
226 """
227 from navegador.context.loader import ContextLoader
228
229 return ContextLoader(self._store).load_decision(name)
230
231 # ── Search ────────────────────────────────────────────────────────────────
232
233 def search(self, query: str, limit: int = 20) -> list[Any]:
234 """
235 Search code symbols by name.
236
237 Returns:
238 List of :class:`~navegador.context.loader.ContextNode`.
239 """
240 from navegador.context.loader import ContextLoader
241
242 return ContextLoader(self._store).search(query, limit=limit)
243
244 def search_all(self, query: str, limit: int = 20) -> list[Any]:
245 """
246 Search everything — code symbols, concepts, rules, decisions, wiki.
247
248 Returns:
249 List of :class:`~navegador.context.loader.ContextNode`.
250 """
251 from navegador.context.loader import ContextLoader
252
253 return ContextLoader(self._store).search_all(query, limit=limit)
254
255 def search_knowledge(self, query: str, limit: int = 20) -> list[Any]:
256 """
257 Search the knowledge layer — concepts, rules, decisions, wiki pages.
258
259 Returns:
260 List of :class:`~navegador.context.loader.ContextNode`.
261 """
262 from navegador.context.loader import ContextLoader
263
264 return ContextLoader(self._store).search_knowledge(query, limit=limit)
265
266 # ── Graph ─────────────────────────────────────────────────────────────────
267
268 def query(self, cypher: str, params: dict[str, Any] | None = None) -> Any:
269 """
270 Execute a raw Cypher query against the graph.
271
272 Args:
273 cypher: Cypher query string.
274 params: Optional parameter dict.
275
276 Returns:
277 FalkorDB result object (has ``.result_set`` attribute).
278 """
279 return self._store.query(cypher, params)
280
281 def stats(self) -> dict[str, Any]:
282 """
283 Return graph statistics broken down by node and edge type.
284
285 Returns:
286 Dict with keys ``total_nodes``, ``total_edges``, ``nodes``
287 (label → count), ``edges`` (type → count).
288 """
289 from navegador.graph import queries as q
290
291 node_rows = self._store.query(q.NODE_TYPE_COUNTS).result_set or []
292 edge_rows = self._store.query(q.EDGE_TYPE_COUNTS).result_set or []
293
294 return {
295 "total_nodes": sum(r[1] for r in node_rows),
296 "total_edges": sum(r[1] for r in edge_rows),
297 "nodes": {r[0]: r[1] for r in node_rows},
298 "edges": {r[0]: r[1] for r in edge_rows},
299 }
300
301 def export(self, output_path: str) -> dict[str, int]:
302 """
303 Export the full graph to a JSONL file.
304
305 Args:
306 output_path: Destination file path.
307
308 Returns:
309 Dict with counts: ``nodes``, ``edges``.
310 """
311 from navegador.graph.export import export_graph
312
313 return export_graph(self._store, output_path)
314
315 def import_graph(self, input_path: str, clear: bool = True) -> dict[str, int]:
316 """
317 Import a graph from a JSONL export file.
318
319 Args:
320 input_path: Source JSONL file path.
321 clear: Wipe the graph before importing (default ``True``).
322
323 Returns:
324 Dict with counts: ``nodes``, ``edges``.
325 """
326 from navegador.graph.export import import_graph
327
328 return import_graph(self._store, input_path, clear=clear)
329
330 def clear(self) -> None:
331 """Delete all nodes and edges in the graph."""
332 self._store.clear()
333
334 # ── Owners ────────────────────────────────────────────────────────────────
335
336 def find_owners(self, name: str, file_path: str = "") -> list[Any]:
337 """
338 Find people assigned as owners of a named node.
339
340 Args:
341 name: Node name.
342 file_path: Optional file path to narrow the match.
343
344 Returns:
345 List of :class:`~navegador.context.loader.ContextNode` with
346 ``type="Person"``.
347 """
348 from navegador.context.loader import ContextLoader
349
350 return ContextLoader(self._store).find_owners(
--- a/tests/test_sdk.py
+++ b/tests/test_sdk.py
@@ -0,0 +1,549 @@
1
+"""
2
+Tests for the Navegador Python SDK (navegador/sdk.py).
3
+
4
+All tests use a mock GraphStore so no real database is required.
5
+"""
6
+
7
+from unittest.mock import MagicMock, patch
8
+
9
+import pytest
10
+
11
+from navegador.sdk import Navegador
12
+
13
+
14
+# ── Helpers ───────────────────────────────────────────────────────────────────
15
+
16
+
17
+def _mock_store(rows=None):
18
+ """Return a mock GraphStore whose .query() yields the given rows."""
19
+ store = MagicMock()
20
+ result = MagicMock()
21
+ result.result_set = rows or []
22
+ store.query.return_value = result
23
+ return store
24
+
25
+
26
+def _nav(rows=None):
27
+ """Return a Navegador instance wired to a mock store."""
28
+ return Navegador(_mock_store(rows))
29
+
30
+
31
+# ── Constructor tests ─────────────────────────────────────────────────────────
32
+
33
+
34
+class TestConstructors:
35
+ def test_direct_init_stores_store(self):
36
+ store = _mock_store()
37
+ nav = Navegador(store)
38
+ assert nav._store is store
39
+
40
+ def test_sqlite_classmethod(self):
41
+ fake_store = _mock_store()
42
+ with patch("navegador.graph.store.GraphStore.sqlite", return_value=fake_store) as mock_sqlite:
43
+ nav = Navegador.sqlite("/tmp/test.db")
44
+ mock_sqlite.assert_called_once_with("/tmp/test.db")
45
+ assert nav._store is fake_store
46
+
47
+ def test_sqlite_default_path(self):
48
+ fake_store = _mock_store()
49
+ with patch("navegador.graph.store.GraphStore.sqlite", return_value=fake_store) as mock_sqlite:
50
+ Navegador.sqlite()
51
+ mock_sqlite.assert_called_once_with(".navegador/graph.db")
52
+
53
+ def test_redis_classmethod(self):
54
+ fake_store = _mock_store()
55
+ with patch("navegador.graph.store.GraphStore.redis", return_value=fake_store) as mock_redis:
56
+ nav = Navegador.redis("redis://myhost:6379")
57
+ mock_redis.assert_called_once_with("redis://myhost:6379")
58
+ assert nav._store is fake_store
59
+
60
+ def test_redis_default_url(self):
61
+ fake_store = _mock_store()
62
+ with patch("navegador.graph.store.GraphStore.redis", return_value=fake_store) as mock_redis:
63
+ Navegador.redis()
64
+ mock_redis.assert_called_once_with("redis://localhost:6379")
65
+
66
+
67
+# ── Ingestion ─────────────────────────────────────────────────────────────────
68
+
69
+
70
+class TestIngest:
71
+ def test_ingest_delegates_to_repo_ingester(self):
72
+ store = _mock_store()
73
+ nav = Navegador(store)
74
+ expected = {"files": 3, "functions": 10, "classes": 2, "edges": 5, "skipped": 0}
75
+
76
+ with patch("navegador.ingestion.RepoIngester") as MockIngester:
77
+ mock_instance = MockIngester.return_value
78
+ mock_instance.ingest.return_value = expected
79
+
80
+ result = nav.ingest("/some/repo")
81
+
82
+ MockIngester.assert_called_once_with(store)
83
+ mock_instance.ingest.assert_called_once_with(
84
+ "/some/repo", clear=False, incremental=False
85
+ )
86
+ assert result == expected
87
+
88
+ def test_ingest_passes_clear_and_incremental(self):
89
+ store = _mock_store()
90
+ nav = Navegador(store)
91
+
92
+ with patch("navegador.ingestion.RepoIngester") as MockIngester:
93
+ mock_instance = MockIngester.return_value
94
+ mock_instance.ingest.return_value = {}
95
+
96
+ nav.ingest("/repo", clear=True, incremental=True)
97
+ mock_instance.ingest.assert_called_once_with(
98
+ "/repo", clear=True, incremental=True
99
+ )
100
+
101
+
102
+# ── Context loading ───────────────────────────────────────────────────────────
103
+
104
+
105
+class TestFileContext:
106
+ def test_returns_context_bundle(self):
107
+ from navegador.context.loader import ContextBundle, ContextNode
108
+
109
+ nav = _nav([])
110
+ bundle = nav.file_context("src/auth.py")
111
+ assert isinstance(bundle, ContextBundle)
112
+ assert bundle.target.type == "File"
113
+ assert bundle.target.name == "auth.py"
114
+
115
+ def test_passes_file_path(self):
116
+ store = _mock_store([])
117
+ nav = Navegador(store)
118
+ nav.file_context("src/auth.py")
119
+ # store.query must have been called with the file path param
120
+ call_args = store.query.call_args
121
+ assert call_args[0][1]["path"] == "src/auth.py"
122
+
123
+
124
+class TestFunctionContext:
125
+ def test_returns_context_bundle(self):
126
+ from navegador.context.loader import ContextBundle
127
+
128
+ nav = _nav([])
129
+ bundle = nav.function_context("validate_token")
130
+ assert isinstance(bundle, ContextBundle)
131
+ assert bundle.target.name == "validate_token"
132
+ assert bundle.target.type == "Function"
133
+
134
+ def test_passes_file_path_and_depth(self):
135
+ store = _mock_store([])
136
+ nav = Navegador(store)
137
+
138
+ with patch("navegador.context.loader.ContextLoader.load_function") as mock_load:
139
+ from navegador.context.loader import ContextBundle, ContextNode
140
+ mock_load.return_value = ContextBundle(
141
+ target=ContextNode(type="Function", name="fn")
142
+ )
143
+ nav.function_context("fn", file_path="src/x.py", depth=3)
144
+ mock_load.assert_called_once_with("fn", file_path="src/x.py", depth=3)
145
+
146
+ def test_default_depth(self):
147
+ store = _mock_store([])
148
+ nav = Navegador(store)
149
+
150
+ with patch("navegador.context.loader.ContextLoader.load_function") as mock_load:
151
+ from navegador.context.loader import ContextBundle, ContextNode
152
+ mock_load.return_value = ContextBundle(
153
+ target=ContextNode(type="Function", name="fn")
154
+ )
155
+ nav.function_context("fn")
156
+ mock_load.assert_called_once_with("fn", file_path="", depth=2)
157
+
158
+
159
+class TestClassContext:
160
+ def test_returns_context_bundle(self):
161
+ from navegador.context.loader import ContextBundle
162
+
163
+ nav = _nav([])
164
+ bundle = nav.class_context("AuthService")
165
+ assert isinstance(bundle, ContextBundle)
166
+ assert bundle.target.name == "AuthService"
167
+ assert bundle.target.type == "Class"
168
+
169
+ def test_passes_file_path(self):
170
+ store = _mock_store([])
171
+ nav = Navegador(store)
172
+
173
+ with patch("navegador.context.loader.ContextLoader.load_class") as mock_load:
174
+ from navegador.context.loader import ContextBundle, ContextNode
175
+ mock_load.return_value = ContextBundle(
176
+ target=ContextNode(type="Class", name="AuthService")
177
+ )
178
+ nav.class_context("AuthService", file_path="src/auth.py")
179
+ mock_load.assert_called_once_with("AuthService", file_path="src/auth.py")
180
+
181
+
182
+class TestExplain:
183
+ def test_returns_context_bundle(self):
184
+ from navegador.context.loader import ContextBundle
185
+
186
+ nav = _nav([])
187
+ bundle = nav.explain("validate_token")
188
+ assert isinstance(bundle, ContextBundle)
189
+ assert bundle.metadata["query"] == "explain"
190
+
191
+ def test_passes_file_path(self):
192
+ store = _mock_store([])
193
+ nav = Navegador(store)
194
+
195
+ with patch("navegador.context.loader.ContextLoader.explain") as mock_explain:
196
+ from navegador.context.loader import ContextBundle, ContextNode
197
+ mock_explain.return_value = ContextBundle(
198
+ target=ContextNode(type="Node", name="fn")
199
+ )
200
+ nav.explain("fn", file_path="src/x.py")
201
+ mock_explain.assert_called_once_with("fn", file_path="src/x.py")
202
+
203
+
204
+# ── Knowledge ─────────────────────────────────────────────────────────────────
205
+
206
+
207
+class TestAddConcept:
208
+ def test_delegates_to_knowledge_ingester(self):
209
+ store = _mock_store()
210
+ nav = Navegador(store)
211
+
212
+ with patch("navegador.ingestion.KnowledgeIngester") as MockK:
213
+ mock_k = MockK.return_value
214
+ nav.add_concept("JWT", description="Stateless token", domain="auth")
215
+ MockK.assert_called_once_with(store)
216
+ mock_k.add_concept.assert_called_once_with(
217
+ "JWT", description="Stateless token", domain="auth"
218
+ )
219
+
220
+
221
+class TestAddRule:
222
+ def test_delegates_to_knowledge_ingester(self):
223
+ store = _mock_store()
224
+ nav = Navegador(store)
225
+
226
+ with patch("navegador.ingestion.KnowledgeIngester") as MockK:
227
+ mock_k = MockK.return_value
228
+ nav.add_rule("tokens must expire", severity="critical")
229
+ mock_k.add_rule.assert_called_once_with(
230
+ "tokens must expire", severity="critical"
231
+ )
232
+
233
+
234
+class TestAddDecision:
235
+ def test_delegates_to_knowledge_ingester(self):
236
+ store = _mock_store()
237
+ nav = Navegador(store)
238
+
239
+ with patch("navegador.ingestion.KnowledgeIngester") as MockK:
240
+ mock_k = MockK.return_value
241
+ nav.add_decision("Use FalkorDB", rationale="Cypher + SQLite", status="accepted")
242
+ mock_k.add_decision.assert_called_once_with(
243
+ "Use FalkorDB", rationale="Cypher + SQLite", status="accepted"
244
+ )
245
+
246
+
247
+class TestAddPerson:
248
+ def test_delegates_to_knowledge_ingester(self):
249
+ store = _mock_store()
250
+ nav = Navegador(store)
251
+
252
+ with patch("navegador.ingestion.KnowledgeIngester") as MockK:
253
+ mock_k = MockK.return_value
254
+ nav.add_person("Alice", email="[email protected]", role="lead")
255
+ mock_k.add_person.assert_called_once_with(
256
+ "Alice", email="[email protected]", role="lead"
257
+ )
258
+
259
+
260
+class TestAddDomain:
261
+ def test_delegates_to_knowledge_ingester(self):
262
+ store = _mock_store()
263
+ nav = Navegador(store)
264
+
265
+ with patch("navegador.ingestion.KnowledgeIngester") as MockK:
266
+ mock_k = MockK.return_value
267
+ nav.add_domain("auth", description="Authentication layer")
268
+ mock_k.add_domain.assert_called_once_with(
269
+ "auth", description="Authentication layer"
270
+ )
271
+
272
+
273
+class TestAnnotate:
274
+ def test_delegates_to_knowledge_ingester(self):
275
+ store = _mock_store()
276
+ nav = Navegador(store)
277
+
278
+ with patch("navegador.ingestion.KnowledgeIngester") as MockK:
279
+ mock_k = MockK.return_value
280
+ nav.annotate("validate_token", "Function", concept="JWT")
281
+ mock_k.annotate_code.assert_called_once_with(
282
+ "validate_token", "Function", concept="JWT", rule=None
283
+ )
284
+
285
+ def test_passes_rule(self):
286
+ store = _mock_store()
287
+ nav = Navegador(store)
288
+
289
+ with patch("navegador.ingestion.KnowledgeIngester") as MockK:
290
+ mock_k = MockK.return_value
291
+ nav.annotate("validate_token", "Function", rule="tokens must expire")
292
+ mock_k.annotate_code.assert_called_once_with(
293
+ "validate_token", "Function", concept=None, rule="tokens must expire"
294
+ )
295
+
296
+
297
+class TestConceptLoad:
298
+ def test_delegates_to_context_loader(self):
299
+ rows = [["JWT", "Stateless token auth", "active", "auth", [], [], [], []]]
300
+ nav = _nav(rows)
301
+ bundle = nav.concept("JWT")
302
+ assert bundle.target.name == "JWT"
303
+ assert bundle.target.type == "Concept"
304
+
305
+ def test_not_found_returns_bundle_with_found_false(self):
306
+ nav = _nav([])
307
+ bundle = nav.concept("NonExistent")
308
+ assert bundle.metadata.get("found") is False
309
+
310
+
311
+class TestDomainLoad:
312
+ def test_delegates_to_context_loader(self):
313
+ rows = [["Function", "login", "src/auth.py", "Log in"]]
314
+ nav = _nav(rows)
315
+ bundle = nav.domain("auth")
316
+ assert bundle.target.name == "auth"
317
+ assert bundle.target.type == "Domain"
318
+
319
+
320
+class TestDecisionLoad:
321
+ def test_delegates_to_context_loader(self):
322
+ rows = [[
323
+ "Use FalkorDB",
324
+ "Graph DB",
325
+ "Cypher queries",
326
+ "Neo4j",
327
+ "accepted",
328
+ "2026-01-01",
329
+ "infrastructure",
330
+ [],
331
+ [],
332
+ ]]
333
+ nav = _nav(rows)
334
+ bundle = nav.decision("Use FalkorDB")
335
+ assert bundle.target.name == "Use FalkorDB"
336
+ assert bundle.target.type == "Decision"
337
+ assert bundle.target.rationale == "Cypher queries"
338
+
339
+ def test_not_found(self):
340
+ nav = _nav([])
341
+ bundle = nav.decision("Unknown")
342
+ assert bundle.metadata.get("found") is False
343
+
344
+
345
+# ── Search ────────────────────────────────────────────────────────────────────
346
+
347
+
348
+class TestSearch:
349
+ def test_search_returns_nodes(self):
350
+ from navegador.context.loader import ContextNode
351
+
352
+ rows = [["Function", "validate_token", "src/auth.py", 10, "Validate a token"]]
353
+ nav = _nav(rows)
354
+ results = nav.search("validate")
355
+ assert len(results) == 1
356
+ assert isinstance(results[0], ContextNode)
357
+ assert results[0].name == "validate_token"
358
+
359
+ def test_search_empty(self):
360
+ nav = _nav([])
361
+ assert nav.search("xyz") == []
362
+
363
+ def test_search_passes_limit(self):
364
+ store = _mock_store([])
365
+ nav = Navegador(store)
366
+
367
+ with patch("navegador.context.loader.ContextLoader.search") as mock_search:
368
+ mock_search.return_value = []
369
+ nav.search("auth", limit=5)
370
+ mock_search.assert_called_once_with("auth", limit=5)
371
+
372
+
373
+class TestSearchAll:
374
+ def test_search_all_returns_nodes(self):
375
+ from navegador.context.loader import ContextNode
376
+
377
+ rows = [["Concept", "JWT", "", None, "Stateless token auth"]]
378
+ nav = _nav(rows)
379
+ results = nav.search_all("JWT")
380
+ assert len(results) == 1
381
+ assert results[0].type == "Concept"
382
+
383
+ def test_search_all_passes_limit(self):
384
+ store = _mock_store([])
385
+ nav = Navegador(store)
386
+
387
+ with patch("navegador.context.loader.ContextLoader.search_all") as mock_sa:
388
+ mock_sa.return_value = []
389
+ nav.search_all("auth", limit=10)
390
+ mock_sa.assert_called_once_with("auth", limit=10)
391
+
392
+
393
+class TestSearchKnowledge:
394
+ def test_search_knowledge_returns_nodes(self):
395
+ from navegador.context.loader import ContextNode
396
+
397
+ rows = [["Concept", "JWT", "Stateless token auth", "auth", "active"]]
398
+ nav = _nav(rows)
399
+ results = nav.search_knowledge("JWT")
400
+ assert len(results) == 1
401
+ assert results[0].domain == "auth"
402
+
403
+ def test_search_knowledge_empty(self):
404
+ nav = _nav([])
405
+ assert nav.search_knowledge("missing") == []
406
+
407
+ def test_search_knowledge_passes_limit(self):
408
+ store = _mock_store([])
409
+ nav = Navegador(store)
410
+
411
+ with patch("navegador.context.loader.ContextLoader.search_knowledge") as mock_sk:
412
+ mock_sk.return_value = []
413
+ nav.search_knowledge("auth", limit=3)
414
+ mock_sk.assert_called_once_with("auth", limit=3)
415
+
416
+
417
+# ── Graph ─────────────────────────────────────────────────────────────────────
418
+
419
+
420
+class TestQuery:
421
+ def test_delegates_to_store(self):
422
+ store = _mock_store([[42]])
423
+ nav = Navegador(store)
424
+ result = nav.query("MATCH (n) RETURN count(n)")
425
+ store.query.assert_called_once_with("MATCH (n) RETURN count(n)", None)
426
+ assert result.result_set == [[42]]
427
+
428
+ def test_passes_params(self):
429
+ store = _mock_store([])
430
+ nav = Navegador(store)
431
+ nav.query("MATCH (n:Function {name: $name}) RETURN n", {"name": "foo"})
432
+ store.query.assert_called_once_with(
433
+ "MATCH (n:Function {name: $name}) RETURN n", {"name": "foo"}
434
+ )
435
+
436
+
437
+class TestStats:
438
+ def test_returns_dict_with_expected_keys(self):
439
+ node_result = MagicMock()
440
+ node_result.result_set = [["Function", 5], ["Class", 2]]
441
+ edge_result = MagicMock()
442
+ edge_result.result_set = [["CALLS", 8], ["INHERITS", 1]]
443
+
444
+ store = MagicMock()
445
+ store.query.side_effect = [node_result, edge_result]
446
+
447
+ nav = Navegador(store)
448
+ s = nav.stats()
449
+
450
+ assert s["total_nodes"] == 7
451
+ assert s["total_edges"] == 9
452
+ assert s["nodes"]["Function"] == 5
453
+ assert s["nodes"]["Class"] == 2
454
+ assert s["edges"]["CALLS"] == 8
455
+ assert s["edges"]["INHERITS"] == 1
456
+
457
+ def test_empty_graph(self):
458
+ store = _mock_store([])
459
+ nav = Navegador(store)
460
+ s = nav.stats()
461
+ assert s["total_nodes"] == 0
462
+ assert s["total_edges"] == 0
463
+ assert s["nodes"] == {}
464
+ assert s["edges"] == {}
465
+
466
+
467
+class TestExport:
468
+ def test_delegates_to_export_graph(self):
469
+ store = _mock_store()
470
+ nav = Navegador(store)
471
+ expected = {"nodes": 10, "edges": 5}
472
+
473
+ with patch("navegador.graph.export.export_graph", return_value=expected) as mock_export:
474
+ result = nav.export("/tmp/out.jsonl")
475
+ mock_export.assert_called_once_with(store, "/tmp/out.jsonl")
476
+ assert result == expected
477
+
478
+
479
+class TestImportGraph:
480
+ def test_delegates_to_import_graph(self):
481
+ store = _mock_store()
482
+ nav = Navegador(store)
483
+ expected = {"nodes": 10, "edges": 5}
484
+
485
+ with patch("navegador.graph.export.import_graph", return_value=expected) as mock_import:
486
+ result = nav.import_graph("/tmp/in.jsonl")
487
+ mock_import.assert_called_once_with(store, "/tmp/in.jsonl", clear=True)
488
+ assert result == expected
489
+
490
+ def test_passes_clear_false(self):
491
+ store = _mock_store()
492
+ nav = Navegador(store)
493
+
494
+ with patch("navegador.graph.export.import_graph", return_value={}) as mock_import:
495
+ nav.import_graph("/tmp/in.jsonl", clear=False)
496
+ mock_import.assert_called_once_with(store, "/tmp/in.jsonl", clear=False)
497
+
498
+
499
+class TestClear:
500
+ def test_delegates_to_store(self):
501
+ store = _mock_store()
502
+ nav = Navegador(store)
503
+ nav.clear()
504
+ store.clear.assert_called_once()
505
+
506
+
507
+# ── Owners ────────────────────────────────────────────────────────────────────
508
+
509
+
510
+class TestFindOwners:
511
+ def test_returns_person_nodes(self):
512
+ from navegador.context.loader import ContextNode
513
+
514
+ rows = [["Class", "AuthService", "Alice", "[email protected]", "lead", "auth"]]
515
+ nav = _nav(rows)
516
+ results = nav.find_owners("AuthService")
517
+ assert len(results) == 1
518
+ assert isinstance(results[0], ContextNode)
519
+ assert results[0].type == "Person"
520
+ assert results[0].name == "Alice"
521
+
522
+ def test_empty(self):
523
+ nav = _nav([])
524
+ assert nav.find_owners("nobody") == []
525
+
526
+ def test_passes_file_path(self):
527
+ store = _mock_store([])
528
+ nav = Navegador(store)
529
+
530
+ with patch("navegador.context.loader.ContextLoader.find_owners") as mock_fo:
531
+ mock_fo.return_value = []
532
+ nav.find_owners("AuthService", file_path="src/auth.py")
533
+ mock_fo.assert_called_once_with("AuthService", file_path="src/auth.py")
534
+
535
+
536
+# ── Top-level import ──────────────────────────────────────────────────────────
537
+
538
+
539
+class TestTopLevelImport:
540
+ def test_navegador_exported_from_package(self):
541
+ import navegador
542
+
543
+ assert hasattr(navegador, "Navegador")
544
+ assert navegador.Navegador is Navegador
545
+
546
+ def test_navegador_in_all(self):
547
+ import navegador
548
+
549
+ assert "Navegador" in navegador.__all__
--- a/tests/test_sdk.py
+++ b/tests/test_sdk.py
@@ -0,0 +1,549 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/tests/test_sdk.py
+++ b/tests/test_sdk.py
@@ -0,0 +1,549 @@
1 """
2 Tests for the Navegador Python SDK (navegador/sdk.py).
3
4 All tests use a mock GraphStore so no real database is required.
5 """
6
7 from unittest.mock import MagicMock, patch
8
9 import pytest
10
11 from navegador.sdk import Navegador
12
13
14 # ── Helpers ───────────────────────────────────────────────────────────────────
15
16
17 def _mock_store(rows=None):
18 """Return a mock GraphStore whose .query() yields the given rows."""
19 store = MagicMock()
20 result = MagicMock()
21 result.result_set = rows or []
22 store.query.return_value = result
23 return store
24
25
26 def _nav(rows=None):
27 """Return a Navegador instance wired to a mock store."""
28 return Navegador(_mock_store(rows))
29
30
31 # ── Constructor tests ─────────────────────────────────────────────────────────
32
33
34 class TestConstructors:
35 def test_direct_init_stores_store(self):
36 store = _mock_store()
37 nav = Navegador(store)
38 assert nav._store is store
39
40 def test_sqlite_classmethod(self):
41 fake_store = _mock_store()
42 with patch("navegador.graph.store.GraphStore.sqlite", return_value=fake_store) as mock_sqlite:
43 nav = Navegador.sqlite("/tmp/test.db")
44 mock_sqlite.assert_called_once_with("/tmp/test.db")
45 assert nav._store is fake_store
46
47 def test_sqlite_default_path(self):
48 fake_store = _mock_store()
49 with patch("navegador.graph.store.GraphStore.sqlite", return_value=fake_store) as mock_sqlite:
50 Navegador.sqlite()
51 mock_sqlite.assert_called_once_with(".navegador/graph.db")
52
53 def test_redis_classmethod(self):
54 fake_store = _mock_store()
55 with patch("navegador.graph.store.GraphStore.redis", return_value=fake_store) as mock_redis:
56 nav = Navegador.redis("redis://myhost:6379")
57 mock_redis.assert_called_once_with("redis://myhost:6379")
58 assert nav._store is fake_store
59
60 def test_redis_default_url(self):
61 fake_store = _mock_store()
62 with patch("navegador.graph.store.GraphStore.redis", return_value=fake_store) as mock_redis:
63 Navegador.redis()
64 mock_redis.assert_called_once_with("redis://localhost:6379")
65
66
67 # ── Ingestion ─────────────────────────────────────────────────────────────────
68
69
70 class TestIngest:
71 def test_ingest_delegates_to_repo_ingester(self):
72 store = _mock_store()
73 nav = Navegador(store)
74 expected = {"files": 3, "functions": 10, "classes": 2, "edges": 5, "skipped": 0}
75
76 with patch("navegador.ingestion.RepoIngester") as MockIngester:
77 mock_instance = MockIngester.return_value
78 mock_instance.ingest.return_value = expected
79
80 result = nav.ingest("/some/repo")
81
82 MockIngester.assert_called_once_with(store)
83 mock_instance.ingest.assert_called_once_with(
84 "/some/repo", clear=False, incremental=False
85 )
86 assert result == expected
87
88 def test_ingest_passes_clear_and_incremental(self):
89 store = _mock_store()
90 nav = Navegador(store)
91
92 with patch("navegador.ingestion.RepoIngester") as MockIngester:
93 mock_instance = MockIngester.return_value
94 mock_instance.ingest.return_value = {}
95
96 nav.ingest("/repo", clear=True, incremental=True)
97 mock_instance.ingest.assert_called_once_with(
98 "/repo", clear=True, incremental=True
99 )
100
101
102 # ── Context loading ───────────────────────────────────────────────────────────
103
104
105 class TestFileContext:
106 def test_returns_context_bundle(self):
107 from navegador.context.loader import ContextBundle, ContextNode
108
109 nav = _nav([])
110 bundle = nav.file_context("src/auth.py")
111 assert isinstance(bundle, ContextBundle)
112 assert bundle.target.type == "File"
113 assert bundle.target.name == "auth.py"
114
115 def test_passes_file_path(self):
116 store = _mock_store([])
117 nav = Navegador(store)
118 nav.file_context("src/auth.py")
119 # store.query must have been called with the file path param
120 call_args = store.query.call_args
121 assert call_args[0][1]["path"] == "src/auth.py"
122
123
124 class TestFunctionContext:
125 def test_returns_context_bundle(self):
126 from navegador.context.loader import ContextBundle
127
128 nav = _nav([])
129 bundle = nav.function_context("validate_token")
130 assert isinstance(bundle, ContextBundle)
131 assert bundle.target.name == "validate_token"
132 assert bundle.target.type == "Function"
133
134 def test_passes_file_path_and_depth(self):
135 store = _mock_store([])
136 nav = Navegador(store)
137
138 with patch("navegador.context.loader.ContextLoader.load_function") as mock_load:
139 from navegador.context.loader import ContextBundle, ContextNode
140 mock_load.return_value = ContextBundle(
141 target=ContextNode(type="Function", name="fn")
142 )
143 nav.function_context("fn", file_path="src/x.py", depth=3)
144 mock_load.assert_called_once_with("fn", file_path="src/x.py", depth=3)
145
146 def test_default_depth(self):
147 store = _mock_store([])
148 nav = Navegador(store)
149
150 with patch("navegador.context.loader.ContextLoader.load_function") as mock_load:
151 from navegador.context.loader import ContextBundle, ContextNode
152 mock_load.return_value = ContextBundle(
153 target=ContextNode(type="Function", name="fn")
154 )
155 nav.function_context("fn")
156 mock_load.assert_called_once_with("fn", file_path="", depth=2)
157
158
159 class TestClassContext:
160 def test_returns_context_bundle(self):
161 from navegador.context.loader import ContextBundle
162
163 nav = _nav([])
164 bundle = nav.class_context("AuthService")
165 assert isinstance(bundle, ContextBundle)
166 assert bundle.target.name == "AuthService"
167 assert bundle.target.type == "Class"
168
169 def test_passes_file_path(self):
170 store = _mock_store([])
171 nav = Navegador(store)
172
173 with patch("navegador.context.loader.ContextLoader.load_class") as mock_load:
174 from navegador.context.loader import ContextBundle, ContextNode
175 mock_load.return_value = ContextBundle(
176 target=ContextNode(type="Class", name="AuthService")
177 )
178 nav.class_context("AuthService", file_path="src/auth.py")
179 mock_load.assert_called_once_with("AuthService", file_path="src/auth.py")
180
181
182 class TestExplain:
183 def test_returns_context_bundle(self):
184 from navegador.context.loader import ContextBundle
185
186 nav = _nav([])
187 bundle = nav.explain("validate_token")
188 assert isinstance(bundle, ContextBundle)
189 assert bundle.metadata["query"] == "explain"
190
191 def test_passes_file_path(self):
192 store = _mock_store([])
193 nav = Navegador(store)
194
195 with patch("navegador.context.loader.ContextLoader.explain") as mock_explain:
196 from navegador.context.loader import ContextBundle, ContextNode
197 mock_explain.return_value = ContextBundle(
198 target=ContextNode(type="Node", name="fn")
199 )
200 nav.explain("fn", file_path="src/x.py")
201 mock_explain.assert_called_once_with("fn", file_path="src/x.py")
202
203
204 # ── Knowledge ─────────────────────────────────────────────────────────────────
205
206
207 class TestAddConcept:
208 def test_delegates_to_knowledge_ingester(self):
209 store = _mock_store()
210 nav = Navegador(store)
211
212 with patch("navegador.ingestion.KnowledgeIngester") as MockK:
213 mock_k = MockK.return_value
214 nav.add_concept("JWT", description="Stateless token", domain="auth")
215 MockK.assert_called_once_with(store)
216 mock_k.add_concept.assert_called_once_with(
217 "JWT", description="Stateless token", domain="auth"
218 )
219
220
221 class TestAddRule:
222 def test_delegates_to_knowledge_ingester(self):
223 store = _mock_store()
224 nav = Navegador(store)
225
226 with patch("navegador.ingestion.KnowledgeIngester") as MockK:
227 mock_k = MockK.return_value
228 nav.add_rule("tokens must expire", severity="critical")
229 mock_k.add_rule.assert_called_once_with(
230 "tokens must expire", severity="critical"
231 )
232
233
234 class TestAddDecision:
235 def test_delegates_to_knowledge_ingester(self):
236 store = _mock_store()
237 nav = Navegador(store)
238
239 with patch("navegador.ingestion.KnowledgeIngester") as MockK:
240 mock_k = MockK.return_value
241 nav.add_decision("Use FalkorDB", rationale="Cypher + SQLite", status="accepted")
242 mock_k.add_decision.assert_called_once_with(
243 "Use FalkorDB", rationale="Cypher + SQLite", status="accepted"
244 )
245
246
247 class TestAddPerson:
248 def test_delegates_to_knowledge_ingester(self):
249 store = _mock_store()
250 nav = Navegador(store)
251
252 with patch("navegador.ingestion.KnowledgeIngester") as MockK:
253 mock_k = MockK.return_value
254 nav.add_person("Alice", email="[email protected]", role="lead")
255 mock_k.add_person.assert_called_once_with(
256 "Alice", email="[email protected]", role="lead"
257 )
258
259
260 class TestAddDomain:
261 def test_delegates_to_knowledge_ingester(self):
262 store = _mock_store()
263 nav = Navegador(store)
264
265 with patch("navegador.ingestion.KnowledgeIngester") as MockK:
266 mock_k = MockK.return_value
267 nav.add_domain("auth", description="Authentication layer")
268 mock_k.add_domain.assert_called_once_with(
269 "auth", description="Authentication layer"
270 )
271
272
273 class TestAnnotate:
274 def test_delegates_to_knowledge_ingester(self):
275 store = _mock_store()
276 nav = Navegador(store)
277
278 with patch("navegador.ingestion.KnowledgeIngester") as MockK:
279 mock_k = MockK.return_value
280 nav.annotate("validate_token", "Function", concept="JWT")
281 mock_k.annotate_code.assert_called_once_with(
282 "validate_token", "Function", concept="JWT", rule=None
283 )
284
285 def test_passes_rule(self):
286 store = _mock_store()
287 nav = Navegador(store)
288
289 with patch("navegador.ingestion.KnowledgeIngester") as MockK:
290 mock_k = MockK.return_value
291 nav.annotate("validate_token", "Function", rule="tokens must expire")
292 mock_k.annotate_code.assert_called_once_with(
293 "validate_token", "Function", concept=None, rule="tokens must expire"
294 )
295
296
297 class TestConceptLoad:
298 def test_delegates_to_context_loader(self):
299 rows = [["JWT", "Stateless token auth", "active", "auth", [], [], [], []]]
300 nav = _nav(rows)
301 bundle = nav.concept("JWT")
302 assert bundle.target.name == "JWT"
303 assert bundle.target.type == "Concept"
304
305 def test_not_found_returns_bundle_with_found_false(self):
306 nav = _nav([])
307 bundle = nav.concept("NonExistent")
308 assert bundle.metadata.get("found") is False
309
310
311 class TestDomainLoad:
312 def test_delegates_to_context_loader(self):
313 rows = [["Function", "login", "src/auth.py", "Log in"]]
314 nav = _nav(rows)
315 bundle = nav.domain("auth")
316 assert bundle.target.name == "auth"
317 assert bundle.target.type == "Domain"
318
319
320 class TestDecisionLoad:
321 def test_delegates_to_context_loader(self):
322 rows = [[
323 "Use FalkorDB",
324 "Graph DB",
325 "Cypher queries",
326 "Neo4j",
327 "accepted",
328 "2026-01-01",
329 "infrastructure",
330 [],
331 [],
332 ]]
333 nav = _nav(rows)
334 bundle = nav.decision("Use FalkorDB")
335 assert bundle.target.name == "Use FalkorDB"
336 assert bundle.target.type == "Decision"
337 assert bundle.target.rationale == "Cypher queries"
338
339 def test_not_found(self):
340 nav = _nav([])
341 bundle = nav.decision("Unknown")
342 assert bundle.metadata.get("found") is False
343
344
345 # ── Search ────────────────────────────────────────────────────────────────────
346
347
348 class TestSearch:
349 def test_search_returns_nodes(self):
350 from navegador.context.loader import ContextNode
351
352 rows = [["Function", "validate_token", "src/auth.py", 10, "Validate a token"]]
353 nav = _nav(rows)
354 results = nav.search("validate")
355 assert len(results) == 1
356 assert isinstance(results[0], ContextNode)
357 assert results[0].name == "validate_token"
358
359 def test_search_empty(self):
360 nav = _nav([])
361 assert nav.search("xyz") == []
362
363 def test_search_passes_limit(self):
364 store = _mock_store([])
365 nav = Navegador(store)
366
367 with patch("navegador.context.loader.ContextLoader.search") as mock_search:
368 mock_search.return_value = []
369 nav.search("auth", limit=5)
370 mock_search.assert_called_once_with("auth", limit=5)
371
372
373 class TestSearchAll:
374 def test_search_all_returns_nodes(self):
375 from navegador.context.loader import ContextNode
376
377 rows = [["Concept", "JWT", "", None, "Stateless token auth"]]
378 nav = _nav(rows)
379 results = nav.search_all("JWT")
380 assert len(results) == 1
381 assert results[0].type == "Concept"
382
383 def test_search_all_passes_limit(self):
384 store = _mock_store([])
385 nav = Navegador(store)
386
387 with patch("navegador.context.loader.ContextLoader.search_all") as mock_sa:
388 mock_sa.return_value = []
389 nav.search_all("auth", limit=10)
390 mock_sa.assert_called_once_with("auth", limit=10)
391
392
393 class TestSearchKnowledge:
394 def test_search_knowledge_returns_nodes(self):
395 from navegador.context.loader import ContextNode
396
397 rows = [["Concept", "JWT", "Stateless token auth", "auth", "active"]]
398 nav = _nav(rows)
399 results = nav.search_knowledge("JWT")
400 assert len(results) == 1
401 assert results[0].domain == "auth"
402
403 def test_search_knowledge_empty(self):
404 nav = _nav([])
405 assert nav.search_knowledge("missing") == []
406
407 def test_search_knowledge_passes_limit(self):
408 store = _mock_store([])
409 nav = Navegador(store)
410
411 with patch("navegador.context.loader.ContextLoader.search_knowledge") as mock_sk:
412 mock_sk.return_value = []
413 nav.search_knowledge("auth", limit=3)
414 mock_sk.assert_called_once_with("auth", limit=3)
415
416
417 # ── Graph ─────────────────────────────────────────────────────────────────────
418
419
420 class TestQuery:
421 def test_delegates_to_store(self):
422 store = _mock_store([[42]])
423 nav = Navegador(store)
424 result = nav.query("MATCH (n) RETURN count(n)")
425 store.query.assert_called_once_with("MATCH (n) RETURN count(n)", None)
426 assert result.result_set == [[42]]
427
428 def test_passes_params(self):
429 store = _mock_store([])
430 nav = Navegador(store)
431 nav.query("MATCH (n:Function {name: $name}) RETURN n", {"name": "foo"})
432 store.query.assert_called_once_with(
433 "MATCH (n:Function {name: $name}) RETURN n", {"name": "foo"}
434 )
435
436
437 class TestStats:
438 def test_returns_dict_with_expected_keys(self):
439 node_result = MagicMock()
440 node_result.result_set = [["Function", 5], ["Class", 2]]
441 edge_result = MagicMock()
442 edge_result.result_set = [["CALLS", 8], ["INHERITS", 1]]
443
444 store = MagicMock()
445 store.query.side_effect = [node_result, edge_result]
446
447 nav = Navegador(store)
448 s = nav.stats()
449
450 assert s["total_nodes"] == 7
451 assert s["total_edges"] == 9
452 assert s["nodes"]["Function"] == 5
453 assert s["nodes"]["Class"] == 2
454 assert s["edges"]["CALLS"] == 8
455 assert s["edges"]["INHERITS"] == 1
456
457 def test_empty_graph(self):
458 store = _mock_store([])
459 nav = Navegador(store)
460 s = nav.stats()
461 assert s["total_nodes"] == 0
462 assert s["total_edges"] == 0
463 assert s["nodes"] == {}
464 assert s["edges"] == {}
465
466
467 class TestExport:
468 def test_delegates_to_export_graph(self):
469 store = _mock_store()
470 nav = Navegador(store)
471 expected = {"nodes": 10, "edges": 5}
472
473 with patch("navegador.graph.export.export_graph", return_value=expected) as mock_export:
474 result = nav.export("/tmp/out.jsonl")
475 mock_export.assert_called_once_with(store, "/tmp/out.jsonl")
476 assert result == expected
477
478
479 class TestImportGraph:
480 def test_delegates_to_import_graph(self):
481 store = _mock_store()
482 nav = Navegador(store)
483 expected = {"nodes": 10, "edges": 5}
484
485 with patch("navegador.graph.export.import_graph", return_value=expected) as mock_import:
486 result = nav.import_graph("/tmp/in.jsonl")
487 mock_import.assert_called_once_with(store, "/tmp/in.jsonl", clear=True)
488 assert result == expected
489
490 def test_passes_clear_false(self):
491 store = _mock_store()
492 nav = Navegador(store)
493
494 with patch("navegador.graph.export.import_graph", return_value={}) as mock_import:
495 nav.import_graph("/tmp/in.jsonl", clear=False)
496 mock_import.assert_called_once_with(store, "/tmp/in.jsonl", clear=False)
497
498
499 class TestClear:
500 def test_delegates_to_store(self):
501 store = _mock_store()
502 nav = Navegador(store)
503 nav.clear()
504 store.clear.assert_called_once()
505
506
507 # ── Owners ────────────────────────────────────────────────────────────────────
508
509
510 class TestFindOwners:
511 def test_returns_person_nodes(self):
512 from navegador.context.loader import ContextNode
513
514 rows = [["Class", "AuthService", "Alice", "[email protected]", "lead", "auth"]]
515 nav = _nav(rows)
516 results = nav.find_owners("AuthService")
517 assert len(results) == 1
518 assert isinstance(results[0], ContextNode)
519 assert results[0].type == "Person"
520 assert results[0].name == "Alice"
521
522 def test_empty(self):
523 nav = _nav([])
524 assert nav.find_owners("nobody") == []
525
526 def test_passes_file_path(self):
527 store = _mock_store([])
528 nav = Navegador(store)
529
530 with patch("navegador.context.loader.ContextLoader.find_owners") as mock_fo:
531 mock_fo.return_value = []
532 nav.find_owners("AuthService", file_path="src/auth.py")
533 mock_fo.assert_called_once_with("AuthService", file_path="src/auth.py")
534
535
536 # ── Top-level import ──────────────────────────────────────────────────────────
537
538
539 class TestTopLevelImport:
540 def test_navegador_exported_from_package(self):
541 import navegador
542
543 assert hasattr(navegador, "Navegador")
544 assert navegador.Navegador is Navegador
545
546 def test_navegador_in_all(self):
547 import navegador
548
549 assert "Navegador" in navegador.__all__

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button