Navegador

navegador / navegador / context / loader.py
Blame History Raw 377 lines
1
"""
2
ContextLoader — builds structured context bundles from the navegador graph.
3
4
Operates across both layers:
5
CODE — files, functions, classes, call graphs, decorators, references
6
KNOWLEDGE — concepts, rules, decisions, wiki pages, domains
7
8
Output can be dict, JSON string, or markdown for direct paste into AI chat.
9
"""
10
11
import json
12
import logging
13
from dataclasses import dataclass, field
14
from pathlib import Path
15
from typing import Any
16
17
from navegador.graph import GraphStore, queries
18
19
logger = logging.getLogger(__name__)
20
21
22
@dataclass
23
class ContextNode:
24
type: str
25
name: str
26
file_path: str = ""
27
line_start: int | None = None
28
docstring: str | None = None
29
signature: str | None = None
30
source: str | None = None
31
description: str | None = None
32
domain: str | None = None
33
status: str | None = None
34
rationale: str | None = None
35
alternatives: str | None = None
36
date: str | None = None
37
38
39
@dataclass
40
class ContextBundle:
41
target: ContextNode
42
nodes: list[ContextNode] = field(default_factory=list)
43
edges: list[dict[str, str]] = field(default_factory=list)
44
metadata: dict[str, Any] = field(default_factory=dict)
45
46
def to_dict(self) -> dict[str, Any]:
47
return {
48
"target": vars(self.target),
49
"nodes": [vars(n) for n in self.nodes],
50
"edges": self.edges,
51
"metadata": self.metadata,
52
}
53
54
def to_json(self, indent: int = 2) -> str:
55
return json.dumps(self.to_dict(), indent=indent)
56
57
def to_markdown(self) -> str:
58
lines = [
59
f"# Context: `{self.target.name}`",
60
f"**Type:** {self.target.type}",
61
]
62
if self.target.file_path:
63
lines.append(f"**File:** `{self.target.file_path}`")
64
if self.target.domain:
65
lines.append(f"**Domain:** {self.target.domain}")
66
if self.target.status:
67
lines.append(f"**Status:** {self.target.status}")
68
if self.target.docstring or self.target.description:
69
lines += ["", f"> {self.target.docstring or self.target.description}"]
70
if self.target.signature:
71
lines += ["", f"```python\n{self.target.signature}\n```"]
72
73
if self.nodes:
74
lines += ["", "## Related nodes", ""]
75
for node in self.nodes:
76
loc = f"`{node.file_path}`" if node.file_path else ""
77
lines.append(f"- **{node.type}** `{node.name}` {loc}".strip())
78
summary = node.docstring or node.description
79
if summary:
80
lines.append(f" > {summary}")
81
82
if self.edges:
83
lines += ["", "## Relationships", ""]
84
for edge in self.edges:
85
lines.append(f"- `{edge['from']}` **{edge['type']}** `{edge['to']}`")
86
87
return "\n".join(lines)
88
89
90
class ContextLoader:
91
"""
92
Loads context bundles from the navegador graph — code and knowledge layers.
93
94
Usage:
95
store = GraphStore.sqlite()
96
loader = ContextLoader(store)
97
98
bundle = loader.load_file("src/auth.py")
99
bundle = loader.load_function("validate_token")
100
bundle = loader.load_class("AuthService")
101
bundle = loader.explain("validate_token") # full picture
102
bundle = loader.load_concept("JWT")
103
bundle = loader.load_domain("auth")
104
"""
105
106
def __init__(self, store: GraphStore) -> None:
107
self.store = store
108
109
# ── Code: file ────────────────────────────────────────────────────────────
110
111
def load_file(self, file_path: str) -> ContextBundle:
112
"""All symbols in a file and their relationships."""
113
result = self.store.query(queries.FILE_CONTENTS, {"path": file_path})
114
target = ContextNode(type="File", name=Path(file_path).name, file_path=file_path)
115
nodes = []
116
for row in result.result_set or []:
117
nodes.append(
118
ContextNode(
119
type=row[0] or "Unknown",
120
name=row[1] or "",
121
file_path=file_path,
122
line_start=row[2],
123
docstring=row[3],
124
signature=row[4],
125
)
126
)
127
return ContextBundle(target=target, nodes=nodes, metadata={"query": "file_contents"})
128
129
# ── Code: function ────────────────────────────────────────────────────────
130
131
def load_function(self, name: str, file_path: str = "", depth: int = 2) -> ContextBundle:
132
"""Callers, callees, decorators — everything touching this function."""
133
target = ContextNode(type="Function", name=name, file_path=file_path)
134
nodes: list[ContextNode] = []
135
edges: list[dict[str, str]] = []
136
137
params = {"name": name, "file_path": file_path, "depth": depth}
138
139
callees = self.store.query(queries.CALLEES, params)
140
for row in callees.result_set or []:
141
nodes.append(ContextNode(type=row[0], name=row[1], file_path=row[2], line_start=row[3]))
142
edges.append({"from": name, "type": "CALLS", "to": row[1]})
143
144
callers = self.store.query(queries.CALLERS, params)
145
for row in callers.result_set or []:
146
nodes.append(ContextNode(type=row[0], name=row[1], file_path=row[2], line_start=row[3]))
147
edges.append({"from": row[1], "type": "CALLS", "to": name})
148
149
decorators = self.store.query(
150
queries.DECORATORS_FOR, {"name": name, "file_path": file_path}
151
)
152
for row in decorators.result_set or []:
153
nodes.append(ContextNode(type="Decorator", name=row[0], file_path=row[1]))
154
edges.append({"from": row[0], "type": "DECORATES", "to": name})
155
156
return ContextBundle(
157
target=target,
158
nodes=nodes,
159
edges=edges,
160
metadata={"depth": depth, "query": "function_context"},
161
)
162
163
# ── Code: class ───────────────────────────────────────────────────────────
164
165
def load_class(self, name: str, file_path: str = "") -> ContextBundle:
166
"""Methods, parent classes, subclasses, references."""
167
target = ContextNode(type="Class", name=name, file_path=file_path)
168
nodes: list[ContextNode] = []
169
edges: list[dict[str, str]] = []
170
171
parents = self.store.query(queries.CLASS_HIERARCHY, {"name": name})
172
for row in parents.result_set or []:
173
nodes.append(ContextNode(type="Class", name=row[0], file_path=row[1]))
174
edges.append({"from": name, "type": "INHERITS", "to": row[0]})
175
176
subs = self.store.query(queries.SUBCLASSES, {"name": name})
177
for row in subs.result_set or []:
178
nodes.append(ContextNode(type="Class", name=row[0], file_path=row[1]))
179
edges.append({"from": row[0], "type": "INHERITS", "to": name})
180
181
refs = self.store.query(queries.REFERENCES_TO, {"name": name, "file_path": ""})
182
for row in refs.result_set or []:
183
nodes.append(ContextNode(type=row[0], name=row[1], file_path=row[2], line_start=row[3]))
184
edges.append({"from": row[1], "type": "REFERENCES", "to": name})
185
186
return ContextBundle(
187
target=target, nodes=nodes, edges=edges, metadata={"query": "class_context"}
188
)
189
190
# ── Universal: explain ────────────────────────────────────────────────────
191
192
def explain(self, name: str, file_path: str = "") -> ContextBundle:
193
"""
194
Full picture: all inbound and outbound relationships for any node,
195
across both code and knowledge layers.
196
"""
197
params = {"name": name, "file_path": file_path}
198
target = ContextNode(type="Node", name=name, file_path=file_path)
199
nodes: list[ContextNode] = []
200
edges: list[dict[str, str]] = []
201
202
outbound = self.store.query(queries.OUTBOUND, params)
203
for row in outbound.result_set or []:
204
rel, ntype, nname, npath = row[0], row[1], row[2], row[3]
205
nodes.append(ContextNode(type=ntype, name=nname, file_path=npath))
206
edges.append({"from": name, "type": rel, "to": nname})
207
208
inbound = self.store.query(queries.INBOUND, params)
209
for row in inbound.result_set or []:
210
rel, ntype, nname, npath = row[0], row[1], row[2], row[3]
211
nodes.append(ContextNode(type=ntype, name=nname, file_path=npath))
212
edges.append({"from": nname, "type": rel, "to": name})
213
214
return ContextBundle(target=target, nodes=nodes, edges=edges, metadata={"query": "explain"})
215
216
# ── Knowledge: concept ────────────────────────────────────────────────────
217
218
def load_concept(self, name: str) -> ContextBundle:
219
"""Concept + governing rules + related concepts + implementing code + wiki pages."""
220
result = self.store.query(queries.CONCEPT_CONTEXT, {"name": name})
221
rows = result.result_set or []
222
223
if not rows:
224
return ContextBundle(
225
target=ContextNode(type="Concept", name=name),
226
metadata={"query": "concept_context", "found": False},
227
)
228
229
row = rows[0]
230
target = ContextNode(
231
type="Concept",
232
name=row[0],
233
description=row[1],
234
status=row[2],
235
domain=row[3],
236
)
237
nodes: list[ContextNode] = []
238
edges: list[dict[str, str]] = []
239
240
for cname in row[4] or []:
241
nodes.append(ContextNode(type="Concept", name=cname))
242
edges.append({"from": name, "type": "RELATED_TO", "to": cname})
243
for rname in row[5] or []:
244
nodes.append(ContextNode(type="Rule", name=rname))
245
edges.append({"from": rname, "type": "GOVERNS", "to": name})
246
for wname in row[6] or []:
247
nodes.append(ContextNode(type="WikiPage", name=wname))
248
edges.append({"from": wname, "type": "DOCUMENTS", "to": name})
249
for iname in row[7] or []:
250
nodes.append(ContextNode(type="Code", name=iname))
251
edges.append({"from": iname, "type": "IMPLEMENTS", "to": name})
252
253
return ContextBundle(
254
target=target, nodes=nodes, edges=edges, metadata={"query": "concept_context"}
255
)
256
257
# ── Knowledge: domain ─────────────────────────────────────────────────────
258
259
def load_domain(self, domain: str) -> ContextBundle:
260
"""Everything belonging to a domain — code and knowledge."""
261
result = self.store.query(queries.DOMAIN_CONTENTS, {"domain": domain})
262
target = ContextNode(type="Domain", name=domain)
263
nodes = [
264
ContextNode(type=row[0], name=row[1], file_path=row[2], description=row[3] or None)
265
for row in (result.result_set or [])
266
]
267
return ContextBundle(target=target, nodes=nodes, metadata={"query": "domain_contents"})
268
269
# ── Search ────────────────────────────────────────────────────────────────
270
271
def search(self, query: str, limit: int = 20) -> list[ContextNode]:
272
"""Search code symbols by name."""
273
result = self.store.query(queries.SYMBOL_SEARCH, {"query": query, "limit": limit})
274
return [
275
ContextNode(
276
type=row[0], name=row[1], file_path=row[2], line_start=row[3], docstring=row[4]
277
)
278
for row in (result.result_set or [])
279
]
280
281
def search_all(self, query: str, limit: int = 20) -> list[ContextNode]:
282
"""Search everything — code symbols, concepts, rules, decisions, wiki."""
283
result = self.store.query(queries.GLOBAL_SEARCH, {"query": query, "limit": limit})
284
return [
285
ContextNode(
286
type=row[0], name=row[1], file_path=row[2], docstring=row[3], line_start=row[4]
287
)
288
for row in (result.result_set or [])
289
]
290
291
def search_by_docstring(self, query: str, limit: int = 20) -> list[ContextNode]:
292
"""Search functions/classes whose docstring contains the query."""
293
result = self.store.query(queries.DOCSTRING_SEARCH, {"query": query, "limit": limit})
294
return [
295
ContextNode(
296
type=row[0], name=row[1], file_path=row[2], line_start=row[3], docstring=row[4]
297
)
298
for row in (result.result_set or [])
299
]
300
301
# ── Knowledge: decision rationale ────────────────────────────────────────
302
303
def load_decision(self, name: str) -> ContextBundle:
304
"""Decision rationale, alternatives, status, and related nodes."""
305
result = self.store.query(queries.DECISION_RATIONALE, {"name": name})
306
rows = result.result_set or []
307
308
if not rows:
309
return ContextBundle(
310
target=ContextNode(type="Decision", name=name),
311
metadata={"query": "decision_rationale", "found": False},
312
)
313
314
row = rows[0]
315
target = ContextNode(
316
type="Decision",
317
name=row[0],
318
description=row[1],
319
status=row[4],
320
domain=row[6],
321
)
322
target.rationale = row[2]
323
target.alternatives = row[3]
324
target.date = row[5]
325
326
nodes: list[ContextNode] = []
327
edges: list[dict[str, str]] = []
328
329
for tname in row[7] or []:
330
nodes.append(ContextNode(type="Node", name=tname))
331
edges.append({"from": name, "type": "DOCUMENTS", "to": tname})
332
for pname in row[8] or []:
333
nodes.append(ContextNode(type="Person", name=pname))
334
edges.append({"from": name, "type": "DECIDED_BY", "to": pname})
335
336
return ContextBundle(
337
target=target, nodes=nodes, edges=edges, metadata={"query": "decision_rationale"}
338
)
339
340
# ── Knowledge: find owners ────────────────────────────────────────────────
341
342
def find_owners(self, name: str, file_path: str = "") -> list[ContextNode]:
343
"""Find people assigned to a named node."""
344
result = self.store.query(queries.FIND_OWNERS, {"name": name, "file_path": file_path})
345
return [
346
ContextNode(
347
type="Person",
348
name=row[2],
349
description=f"role={row[4]}, team={row[5]}",
350
)
351
for row in (result.result_set or [])
352
]
353
354
# ── Knowledge: search ────────────────────────────────────────────────────
355
356
def search_knowledge(self, query: str, limit: int = 20) -> list[ContextNode]:
357
"""Search concepts, rules, decisions, and wiki pages."""
358
result = self.store.query(queries.KNOWLEDGE_SEARCH, {"query": query, "limit": limit})
359
return [
360
ContextNode(
361
type=row[0],
362
name=row[1],
363
description=row[2],
364
domain=row[3],
365
status=row[4],
366
)
367
for row in (result.result_set or [])
368
]
369
370
def decorated_by(self, decorator_name: str) -> list[ContextNode]:
371
"""All functions/methods carrying a given decorator."""
372
result = self.store.query(queries.DECORATED_BY, {"decorator_name": decorator_name})
373
return [
374
ContextNode(type=row[0], name=row[1], file_path=row[2], line_start=row[3])
375
for row in (result.result_set or [])
376
]
377

Keyboard Shortcuts

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