Navegador

chore: prep 0.1.0 release - Fix mypy: type-annotate lines[] in rust.py, _store/_loader in server.py - Run ruff format across all source files - Update README language support table (Go, Rust, Java are shipped, not planned) - Update CHANGELOG with full 0.1.0 feature list and quality metrics - Add pytest-asyncio to dev dependencies in pyproject.toml

lmata 2026-03-23 00:43 trunk
Commit 758c6b753ff9a637dbc3aa2d20d7c5e0a2104b2e9b6d7f6d968a29551b9c9df7
+18 -8
--- CHANGELOG.md
+++ CHANGELOG.md
@@ -1,12 +1,22 @@
11
# Changelog
22
33
## 0.1.0 — 2026-03-22
44
5
-Initial release scaffold.
6
-
7
-- AST ingestion pipeline (Python + TypeScript via tree-sitter)
8
-- Property graph storage via FalkorDB-lite (SQLite) or Redis
9
-- Context bundles: file, function, class context loading
10
-- MCP server with 7 tools for AI agent integration
11
-- CLI: `ingest`, `context`, `search`, `stats`, `mcp`
12
-- MkDocs documentation site (navegador.dev)
5
+First public release.
6
+
7
+### Features
8
+
9
+- **7-language AST ingestion** — Python, TypeScript, JavaScript, Go, Rust, Java via tree-sitter
10
+- **Property graph storage** — FalkorDB-lite (SQLite, zero-infra) or Redis-backed FalkorDB
11
+- **Context bundles** — file, function, class, concept, and explain context loading
12
+- **MCP server** — 7 tools for AI agent integration (`ingest_repo`, `load_file_context`, `load_function_context`, `load_class_context`, `search_symbols`, `query_graph`, `graph_stats`)
13
+- **CLI** — `ingest`, `context`, `function`, `class`, `explain`, `search`, `decorated`, `query`, `stats`, `add`, `annotate`, `domain`, `concept`, `wiki ingest`, `planopticon ingest`, `mcp`
14
+- **Knowledge ingestion** — concepts, rules, decisions, persons, domains, wiki pages, PlanOpticon video analysis outputs
15
+- **Wiki ingestion** — local Markdown directories, GitHub repo docs via API or git clone
16
+
17
+### Quality
18
+
19
+- 100% test coverage (426 tests)
20
+- mypy clean (`--ignore-missing-imports`)
21
+- ruff lint + format passing
22
+- CI matrix: Ubuntu + macOS, Python 3.12 / 3.13 / 3.14
1323
--- CHANGELOG.md
+++ CHANGELOG.md
@@ -1,12 +1,22 @@
1 # Changelog
2
3 ## 0.1.0 — 2026-03-22
4
5 Initial release scaffold.
6
7 - AST ingestion pipeline (Python + TypeScript via tree-sitter)
8 - Property graph storage via FalkorDB-lite (SQLite) or Redis
9 - Context bundles: file, function, class context loading
10 - MCP server with 7 tools for AI agent integration
11 - CLI: `ingest`, `context`, `search`, `stats`, `mcp`
12 - MkDocs documentation site (navegador.dev)
 
 
 
 
 
 
 
 
 
 
13
--- CHANGELOG.md
+++ CHANGELOG.md
@@ -1,12 +1,22 @@
1 # Changelog
2
3 ## 0.1.0 — 2026-03-22
4
5 First public release.
6
7 ### Features
8
9 - **7-language AST ingestion** — Python, TypeScript, JavaScript, Go, Rust, Java via tree-sitter
10 - **Property graph storage** — FalkorDB-lite (SQLite, zero-infra) or Redis-backed FalkorDB
11 - **Context bundles** — file, function, class, concept, and explain context loading
12 - **MCP server** — 7 tools for AI agent integration (`ingest_repo`, `load_file_context`, `load_function_context`, `load_class_context`, `search_symbols`, `query_graph`, `graph_stats`)
13 - **CLI** — `ingest`, `context`, `function`, `class`, `explain`, `search`, `decorated`, `query`, `stats`, `add`, `annotate`, `domain`, `concept`, `wiki ingest`, `planopticon ingest`, `mcp`
14 - **Knowledge ingestion** — concepts, rules, decisions, persons, domains, wiki pages, PlanOpticon video analysis outputs
15 - **Wiki ingestion** — local Markdown directories, GitHub repo docs via API or git clone
16
17 ### Quality
18
19 - 100% test coverage (426 tests)
20 - mypy clean (`--ignore-missing-imports`)
21 - ruff lint + format passing
22 - CI matrix: Ubuntu + macOS, Python 3.12 / 3.13 / 3.14
23
+3 -1
--- README.md
+++ README.md
@@ -101,11 +101,13 @@
101101
102102
| Language | Status |
103103
|----------|--------|
104104
| Python | ✅ |
105105
| TypeScript / JavaScript | ✅ |
106
-| Go, Rust, Java | Planned |
106
+| Go | ✅ |
107
+| Rust | ✅ |
108
+| Java | ✅ |
107109
108110
---
109111
110112
## License
111113
112114
--- README.md
+++ README.md
@@ -101,11 +101,13 @@
101
102 | Language | Status |
103 |----------|--------|
104 | Python | ✅ |
105 | TypeScript / JavaScript | ✅ |
106 | Go, Rust, Java | Planned |
 
 
107
108 ---
109
110 ## License
111
112
--- README.md
+++ README.md
@@ -101,11 +101,13 @@
101
102 | Language | Status |
103 |----------|--------|
104 | Python | ✅ |
105 | TypeScript / JavaScript | ✅ |
106 | Go | ✅ |
107 | Rust | ✅ |
108 | Java | ✅ |
109
110 ---
111
112 ## License
113
114
--- navegador/cli/commands.py
+++ navegador/cli/commands.py
@@ -18,19 +18,22 @@
1818
1919
DB_OPTION = click.option(
2020
"--db", default=".navegador/graph.db", show_default=True, help="Graph DB path."
2121
)
2222
FMT_OPTION = click.option(
23
- "--format", "fmt",
23
+ "--format",
24
+ "fmt",
2425
type=click.Choice(["markdown", "json"]),
25
- default="markdown", show_default=True,
26
+ default="markdown",
27
+ show_default=True,
2628
help="Output format. Use json for agent/pipe consumption.",
2729
)
2830
2931
3032
def _get_store(db: str):
3133
from navegador.config import DEFAULT_DB_PATH, get_store
34
+
3235
return get_store(db if db != DEFAULT_DB_PATH else None)
3336
3437
3538
def _emit(text: str, fmt: str) -> None:
3639
if fmt == "json":
@@ -38,10 +41,11 @@
3841
else:
3942
console.print(text)
4043
4144
4245
# ── Root group ────────────────────────────────────────────────────────────────
46
+
4347
4448
@click.group()
4549
@click.version_option(package_name="navegador")
4650
def main():
4751
"""Navegador — project knowledge graph for AI coding agents.
@@ -51,15 +55,20 @@
5155
"""
5256
logging.basicConfig(level=logging.WARNING)
5357
5458
5559
# ── Init ──────────────────────────────────────────────────────────────────────
60
+
5661
5762
@main.command()
5863
@click.argument("path", default=".", type=click.Path())
59
-@click.option("--redis", "redis_url", default="",
60
- help="Redis URL for centralized/production mode (e.g. redis://host:6379).")
64
+@click.option(
65
+ "--redis",
66
+ "redis_url",
67
+ default="",
68
+ help="Redis URL for centralized/production mode (e.g. redis://host:6379).",
69
+)
6170
def init(path: str, redis_url: str):
6271
"""Initialise navegador in a project directory.
6372
6473
Creates .navegador/ (gitignored), writes .env.example with storage options.
6574
@@ -89,10 +98,11 @@
8998
9099
console.print("\nNext: [bold]navegador ingest .[/bold]")
91100
92101
93102
# ── CODE: ingest ──────────────────────────────────────────────────────────────
103
+
94104
95105
@main.command()
96106
@click.argument("repo_path", type=click.Path(exists=True))
97107
@DB_OPTION
98108
@click.option("--clear", is_flag=True, help="Clear existing graph before ingesting.")
@@ -117,18 +127,20 @@
117127
table.add_row(k.capitalize(), str(v))
118128
console.print(table)
119129
120130
121131
# ── CODE: context / function / class ─────────────────────────────────────────
132
+
122133
123134
@main.command()
124135
@click.argument("file_path")
125136
@DB_OPTION
126137
@FMT_OPTION
127138
def context(file_path: str, db: str, fmt: str):
128139
"""Load context for a file — all symbols and their relationships."""
129140
from navegador.context import ContextLoader
141
+
130142
bundle = ContextLoader(_get_store(db)).load_file(file_path)
131143
_emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt)
132144
133145
134146
@main.command()
@@ -138,10 +150,11 @@
138150
@DB_OPTION
139151
@FMT_OPTION
140152
def function(name: str, file_path: str, db: str, depth: int, fmt: str):
141153
"""Load context for a function — callers, callees, decorators."""
142154
from navegador.context import ContextLoader
155
+
143156
bundle = ContextLoader(_get_store(db)).load_function(name, file_path=file_path, depth=depth)
144157
_emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt)
145158
146159
147160
@main.command("class")
@@ -150,41 +163,47 @@
150163
@DB_OPTION
151164
@FMT_OPTION
152165
def class_(name: str, file_path: str, db: str, fmt: str):
153166
"""Load context for a class — methods, inheritance, references."""
154167
from navegador.context import ContextLoader
168
+
155169
bundle = ContextLoader(_get_store(db)).load_class(name, file_path=file_path)
156170
_emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt)
157171
158172
159173
# ── UNIVERSAL: explain ────────────────────────────────────────────────────────
174
+
160175
161176
@main.command()
162177
@click.argument("name")
163178
@click.option("--file", "file_path", default="")
164179
@DB_OPTION
165180
@FMT_OPTION
166181
def explain(name: str, file_path: str, db: str, fmt: str):
167182
"""Full picture: all relationships in and out, code and knowledge layers."""
168183
from navegador.context import ContextLoader
184
+
169185
bundle = ContextLoader(_get_store(db)).explain(name, file_path=file_path)
170186
_emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt)
171187
172188
173189
# ── UNIVERSAL: search ─────────────────────────────────────────────────────────
190
+
174191
175192
@main.command()
176193
@click.argument("query")
177194
@DB_OPTION
178195
@click.option("--limit", default=20, show_default=True)
179
-@click.option("--all", "search_all", is_flag=True,
180
- help="Include knowledge layer (concepts, rules, wiki).")
196
+@click.option(
197
+ "--all", "search_all", is_flag=True, help="Include knowledge layer (concepts, rules, wiki)."
198
+)
181199
@click.option("--docs", "by_doc", is_flag=True, help="Search docstrings instead of names.")
182200
@FMT_OPTION
183201
def search(query: str, db: str, limit: int, search_all: bool, by_doc: bool, fmt: str):
184202
"""Search symbols, concepts, rules, and wiki pages."""
185203
from navegador.context import ContextLoader
204
+
186205
loader = ContextLoader(_get_store(db))
187206
188207
if by_doc:
189208
results = loader.search_by_docstring(query, limit=limit)
190209
elif search_all:
@@ -191,16 +210,26 @@
191210
results = loader.search_all(query, limit=limit)
192211
else:
193212
results = loader.search(query, limit=limit)
194213
195214
if fmt == "json":
196
- click.echo(json.dumps([
197
- {"type": r.type, "name": r.name, "file_path": r.file_path,
198
- "line_start": r.line_start, "docstring": r.docstring,
199
- "description": r.description}
200
- for r in results
201
- ], indent=2))
215
+ click.echo(
216
+ json.dumps(
217
+ [
218
+ {
219
+ "type": r.type,
220
+ "name": r.name,
221
+ "file_path": r.file_path,
222
+ "line_start": r.line_start,
223
+ "docstring": r.docstring,
224
+ "description": r.description,
225
+ }
226
+ for r in results
227
+ ],
228
+ indent=2,
229
+ )
230
+ )
202231
return
203232
204233
if not results:
205234
console.print("[yellow]No results.[/yellow]")
206235
return
@@ -215,25 +244,32 @@
215244
table.add_row(r.type, r.name, loc, str(r.line_start or ""))
216245
console.print(table)
217246
218247
219248
# ── CODE: decorator / query ───────────────────────────────────────────────────
249
+
220250
221251
@main.command()
222252
@click.argument("decorator_name")
223253
@DB_OPTION
224254
@FMT_OPTION
225255
def decorated(decorator_name: str, db: str, fmt: str):
226256
"""Find all functions/methods carrying a decorator."""
227257
from navegador.context import ContextLoader
258
+
228259
results = ContextLoader(_get_store(db)).decorated_by(decorator_name)
229260
230261
if fmt == "json":
231
- click.echo(json.dumps([
232
- {"type": r.type, "name": r.name, "file_path": r.file_path, "line": r.line_start}
233
- for r in results
234
- ], indent=2))
262
+ click.echo(
263
+ json.dumps(
264
+ [
265
+ {"type": r.type, "name": r.name, "file_path": r.file_path, "line": r.line_start}
266
+ for r in results
267
+ ],
268
+ indent=2,
269
+ )
270
+ )
235271
return
236272
237273
if not results:
238274
console.print(f"[yellow]No functions decorated with @{decorator_name}[/yellow]")
239275
return
@@ -256,10 +292,11 @@
256292
result = _get_store(db).query(cypher)
257293
click.echo(json.dumps(result.result_set or [], default=str, indent=2))
258294
259295
260296
# ── KNOWLEDGE: add group ──────────────────────────────────────────────────────
297
+
261298
262299
@main.group()
263300
def add():
264301
"""Add knowledge nodes — concepts, rules, decisions, people, domains."""
265302
@@ -273,13 +310,13 @@
273310
@click.option("--wiki", default="", help="Wiki URL or reference.")
274311
@DB_OPTION
275312
def add_concept(name: str, desc: str, domain: str, status: str, rules: str, wiki: str, db: str):
276313
"""Add a business concept to the knowledge graph."""
277314
from navegador.ingestion import KnowledgeIngester
315
+
278316
k = KnowledgeIngester(_get_store(db))
279
- k.add_concept(name, description=desc, domain=domain, status=status,
280
- rules=rules, wiki_refs=wiki)
317
+ k.add_concept(name, description=desc, domain=domain, status=status, rules=rules, wiki_refs=wiki)
281318
console.print(f"[green]Concept added:[/green] {name}")
282319
283320
284321
@add.command("rule")
285322
@click.argument("name")
@@ -289,10 +326,11 @@
289326
@click.option("--rationale", default="")
290327
@DB_OPTION
291328
def add_rule(name: str, desc: str, domain: str, severity: str, rationale: str, db: str):
292329
"""Add a business rule or constraint."""
293330
from navegador.ingestion import KnowledgeIngester
331
+
294332
k = KnowledgeIngester(_get_store(db))
295333
k.add_rule(name, description=desc, domain=domain, severity=severity, rationale=rationale)
296334
console.print(f"[green]Rule added:[/green] {name}")
297335
298336
@@ -301,19 +339,28 @@
301339
@click.option("--desc", default="")
302340
@click.option("--domain", default="")
303341
@click.option("--rationale", default="")
304342
@click.option("--alternatives", default="")
305343
@click.option("--date", default="")
306
-@click.option("--status", default="accepted",
307
- type=click.Choice(["proposed", "accepted", "deprecated"]))
344
+@click.option(
345
+ "--status", default="accepted", type=click.Choice(["proposed", "accepted", "deprecated"])
346
+)
308347
@DB_OPTION
309348
def add_decision(name, desc, domain, rationale, alternatives, date, status, db):
310349
"""Add an architectural or product decision."""
311350
from navegador.ingestion import KnowledgeIngester
351
+
312352
k = KnowledgeIngester(_get_store(db))
313
- k.add_decision(name, description=desc, domain=domain, status=status,
314
- rationale=rationale, alternatives=alternatives, date=date)
353
+ k.add_decision(
354
+ name,
355
+ description=desc,
356
+ domain=domain,
357
+ status=status,
358
+ rationale=rationale,
359
+ alternatives=alternatives,
360
+ date=date,
361
+ )
315362
console.print(f"[green]Decision added:[/green] {name}")
316363
317364
318365
@add.command("person")
319366
@click.argument("name")
@@ -322,10 +369,11 @@
322369
@click.option("--team", default="")
323370
@DB_OPTION
324371
def add_person(name: str, email: str, role: str, team: str, db: str):
325372
"""Add a person (contributor, owner, stakeholder)."""
326373
from navegador.ingestion import KnowledgeIngester
374
+
327375
k = KnowledgeIngester(_get_store(db))
328376
k.add_person(name, email=email, role=role, team=team)
329377
console.print(f"[green]Person added:[/green] {name}")
330378
331379
@@ -334,42 +382,50 @@
334382
@click.option("--desc", default="")
335383
@DB_OPTION
336384
def add_domain(name: str, desc: str, db: str):
337385
"""Add a business domain (auth, billing, notifications…)."""
338386
from navegador.ingestion import KnowledgeIngester
387
+
339388
k = KnowledgeIngester(_get_store(db))
340389
k.add_domain(name, description=desc)
341390
console.print(f"[green]Domain added:[/green] {name}")
342391
343392
344393
# ── KNOWLEDGE: annotate ───────────────────────────────────────────────────────
394
+
345395
346396
@main.command()
347397
@click.argument("code_name")
348
-@click.option("--type", "code_label", default="Function",
349
- type=click.Choice(["Function", "Class", "Method", "File", "Module"]))
398
+@click.option(
399
+ "--type",
400
+ "code_label",
401
+ default="Function",
402
+ type=click.Choice(["Function", "Class", "Method", "File", "Module"]),
403
+)
350404
@click.option("--concept", default="", help="Link to this concept.")
351405
@click.option("--rule", default="", help="Link to this rule.")
352406
@DB_OPTION
353407
def annotate(code_name: str, code_label: str, concept: str, rule: str, db: str):
354408
"""Link a code node to a concept or rule in the knowledge graph."""
355409
from navegador.ingestion import KnowledgeIngester
410
+
356411
k = KnowledgeIngester(_get_store(db))
357
- k.annotate_code(code_name, code_label,
358
- concept=concept or None, rule=rule or None)
412
+ k.annotate_code(code_name, code_label, concept=concept or None, rule=rule or None)
359413
console.print(f"[green]Annotated:[/green] {code_name}")
360414
361415
362416
# ── KNOWLEDGE: domain view ────────────────────────────────────────────────────
417
+
363418
364419
@main.command()
365420
@click.argument("name")
366421
@DB_OPTION
367422
@FMT_OPTION
368423
def domain(name: str, db: str, fmt: str):
369424
"""Show everything belonging to a domain — code and knowledge."""
370425
from navegador.context import ContextLoader
426
+
371427
bundle = ContextLoader(_get_store(db)).load_domain(name)
372428
_emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt)
373429
374430
375431
@main.command()
@@ -377,15 +433,17 @@
377433
@DB_OPTION
378434
@FMT_OPTION
379435
def concept(name: str, db: str, fmt: str):
380436
"""Load a business concept — rules, related concepts, implementing code, wiki."""
381437
from navegador.context import ContextLoader
438
+
382439
bundle = ContextLoader(_get_store(db)).load_concept(name)
383440
_emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt)
384441
385442
386443
# ── KNOWLEDGE: wiki ───────────────────────────────────────────────────────────
444
+
387445
388446
@main.group()
389447
def wiki():
390448
"""Ingest and manage wiki pages in the knowledge graph."""
391449
@@ -397,10 +455,11 @@
397455
@click.option("--api", is_flag=True, help="Use GitHub API instead of git clone.")
398456
@DB_OPTION
399457
def wiki_ingest(repo: str, wiki_dir: str, token: str, api: bool, db: str):
400458
"""Pull wiki pages into the knowledge graph."""
401459
from navegador.ingestion import WikiIngester
460
+
402461
w = WikiIngester(_get_store(db))
403462
404463
if wiki_dir:
405464
stats = w.ingest_local(wiki_dir)
406465
elif repo:
@@ -413,32 +472,39 @@
413472
414473
console.print(f"[green]Wiki ingested:[/green] {stats['pages']} pages, {stats['links']} links")
415474
416475
417476
# ── Stats ─────────────────────────────────────────────────────────────────────
477
+
418478
419479
@main.command()
420480
@DB_OPTION
421481
@click.option("--json", "as_json", is_flag=True)
422482
def stats(db: str, as_json: bool):
423483
"""Graph statistics broken down by node and edge type."""
424484
from navegador.graph import queries as q
485
+
425486
store = _get_store(db)
426487
427
- node_rows = (store.query(q.NODE_TYPE_COUNTS).result_set or [])
428
- edge_rows = (store.query(q.EDGE_TYPE_COUNTS).result_set or [])
488
+ node_rows = store.query(q.NODE_TYPE_COUNTS).result_set or []
489
+ edge_rows = store.query(q.EDGE_TYPE_COUNTS).result_set or []
429490
430491
total_nodes = sum(r[1] for r in node_rows)
431492
total_edges = sum(r[1] for r in edge_rows)
432493
433494
if as_json:
434
- click.echo(json.dumps({
435
- "total_nodes": total_nodes,
436
- "total_edges": total_edges,
437
- "nodes": {r[0]: r[1] for r in node_rows},
438
- "edges": {r[0]: r[1] for r in edge_rows},
439
- }, indent=2))
495
+ click.echo(
496
+ json.dumps(
497
+ {
498
+ "total_nodes": total_nodes,
499
+ "total_edges": total_edges,
500
+ "nodes": {r[0]: r[1] for r in node_rows},
501
+ "edges": {r[0]: r[1] for r in edge_rows},
502
+ },
503
+ indent=2,
504
+ )
505
+ )
440506
return
441507
442508
node_table = Table(title=f"Nodes ({total_nodes:,})")
443509
node_table.add_column("Type", style="cyan")
444510
node_table.add_column("Count", justify="right", style="green")
@@ -454,22 +520,27 @@
454520
console.print(node_table)
455521
console.print(edge_table)
456522
457523
458524
# ── PLANOPTICON ingestion ──────────────────────────────────────────────────────
525
+
459526
460527
@main.group()
461528
def planopticon():
462529
"""Ingest planopticon output (meetings, videos, docs) into the knowledge graph."""
463530
464531
465532
@planopticon.command("ingest")
466533
@click.argument("path", type=click.Path(exists=True))
467
-@click.option("--type", "input_type",
468
- type=click.Choice(["auto", "manifest", "kg", "interchange", "batch"]),
469
- default="auto", show_default=True,
470
- help="Input format. auto detects from filename.")
534
+@click.option(
535
+ "--type",
536
+ "input_type",
537
+ type=click.Choice(["auto", "manifest", "kg", "interchange", "batch"]),
538
+ default="auto",
539
+ show_default=True,
540
+ help="Input format. auto detects from filename.",
541
+)
471542
@click.option("--source", default="", help="Source label for provenance (e.g. 'Q4 planning').")
472543
@click.option("--json", "as_json", is_flag=True)
473544
@DB_OPTION
474545
def planopticon_ingest(path: str, input_type: str, source: str, as_json: bool, db: str):
475546
"""Load a planopticon output directory or file into the knowledge graph.
@@ -509,14 +580,14 @@
509580
input_type = "kg"
510581
511582
ing = PlanopticonIngester(_get_store(db), source_tag=source)
512583
513584
dispatch = {
514
- "manifest": ing.ingest_manifest,
515
- "kg": ing.ingest_kg,
585
+ "manifest": ing.ingest_manifest,
586
+ "kg": ing.ingest_kg,
516587
"interchange": ing.ingest_interchange,
517
- "batch": ing.ingest_batch,
588
+ "batch": ing.ingest_batch,
518589
}
519590
stats = dispatch[input_type](p)
520591
521592
if as_json:
522593
click.echo(json.dumps(stats, indent=2))
@@ -528,10 +599,11 @@
528599
table.add_row(k.capitalize(), str(v))
529600
console.print(table)
530601
531602
532603
# ── MCP ───────────────────────────────────────────────────────────────────────
604
+
533605
534606
@main.command()
535607
@DB_OPTION
536608
def mcp(db: str):
537609
"""Start the MCP server for AI agent integration (stdio)."""
538610
--- navegador/cli/commands.py
+++ navegador/cli/commands.py
@@ -18,19 +18,22 @@
18
19 DB_OPTION = click.option(
20 "--db", default=".navegador/graph.db", show_default=True, help="Graph DB path."
21 )
22 FMT_OPTION = click.option(
23 "--format", "fmt",
 
24 type=click.Choice(["markdown", "json"]),
25 default="markdown", show_default=True,
 
26 help="Output format. Use json for agent/pipe consumption.",
27 )
28
29
30 def _get_store(db: str):
31 from navegador.config import DEFAULT_DB_PATH, get_store
 
32 return get_store(db if db != DEFAULT_DB_PATH else None)
33
34
35 def _emit(text: str, fmt: str) -> None:
36 if fmt == "json":
@@ -38,10 +41,11 @@
38 else:
39 console.print(text)
40
41
42 # ── Root group ────────────────────────────────────────────────────────────────
 
43
44 @click.group()
45 @click.version_option(package_name="navegador")
46 def main():
47 """Navegador — project knowledge graph for AI coding agents.
@@ -51,15 +55,20 @@
51 """
52 logging.basicConfig(level=logging.WARNING)
53
54
55 # ── Init ──────────────────────────────────────────────────────────────────────
 
56
57 @main.command()
58 @click.argument("path", default=".", type=click.Path())
59 @click.option("--redis", "redis_url", default="",
60 help="Redis URL for centralized/production mode (e.g. redis://host:6379).")
 
 
 
 
61 def init(path: str, redis_url: str):
62 """Initialise navegador in a project directory.
63
64 Creates .navegador/ (gitignored), writes .env.example with storage options.
65
@@ -89,10 +98,11 @@
89
90 console.print("\nNext: [bold]navegador ingest .[/bold]")
91
92
93 # ── CODE: ingest ──────────────────────────────────────────────────────────────
 
94
95 @main.command()
96 @click.argument("repo_path", type=click.Path(exists=True))
97 @DB_OPTION
98 @click.option("--clear", is_flag=True, help="Clear existing graph before ingesting.")
@@ -117,18 +127,20 @@
117 table.add_row(k.capitalize(), str(v))
118 console.print(table)
119
120
121 # ── CODE: context / function / class ─────────────────────────────────────────
 
122
123 @main.command()
124 @click.argument("file_path")
125 @DB_OPTION
126 @FMT_OPTION
127 def context(file_path: str, db: str, fmt: str):
128 """Load context for a file — all symbols and their relationships."""
129 from navegador.context import ContextLoader
 
130 bundle = ContextLoader(_get_store(db)).load_file(file_path)
131 _emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt)
132
133
134 @main.command()
@@ -138,10 +150,11 @@
138 @DB_OPTION
139 @FMT_OPTION
140 def function(name: str, file_path: str, db: str, depth: int, fmt: str):
141 """Load context for a function — callers, callees, decorators."""
142 from navegador.context import ContextLoader
 
143 bundle = ContextLoader(_get_store(db)).load_function(name, file_path=file_path, depth=depth)
144 _emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt)
145
146
147 @main.command("class")
@@ -150,41 +163,47 @@
150 @DB_OPTION
151 @FMT_OPTION
152 def class_(name: str, file_path: str, db: str, fmt: str):
153 """Load context for a class — methods, inheritance, references."""
154 from navegador.context import ContextLoader
 
155 bundle = ContextLoader(_get_store(db)).load_class(name, file_path=file_path)
156 _emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt)
157
158
159 # ── UNIVERSAL: explain ────────────────────────────────────────────────────────
 
160
161 @main.command()
162 @click.argument("name")
163 @click.option("--file", "file_path", default="")
164 @DB_OPTION
165 @FMT_OPTION
166 def explain(name: str, file_path: str, db: str, fmt: str):
167 """Full picture: all relationships in and out, code and knowledge layers."""
168 from navegador.context import ContextLoader
 
169 bundle = ContextLoader(_get_store(db)).explain(name, file_path=file_path)
170 _emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt)
171
172
173 # ── UNIVERSAL: search ─────────────────────────────────────────────────────────
 
174
175 @main.command()
176 @click.argument("query")
177 @DB_OPTION
178 @click.option("--limit", default=20, show_default=True)
179 @click.option("--all", "search_all", is_flag=True,
180 help="Include knowledge layer (concepts, rules, wiki).")
 
181 @click.option("--docs", "by_doc", is_flag=True, help="Search docstrings instead of names.")
182 @FMT_OPTION
183 def search(query: str, db: str, limit: int, search_all: bool, by_doc: bool, fmt: str):
184 """Search symbols, concepts, rules, and wiki pages."""
185 from navegador.context import ContextLoader
 
186 loader = ContextLoader(_get_store(db))
187
188 if by_doc:
189 results = loader.search_by_docstring(query, limit=limit)
190 elif search_all:
@@ -191,16 +210,26 @@
191 results = loader.search_all(query, limit=limit)
192 else:
193 results = loader.search(query, limit=limit)
194
195 if fmt == "json":
196 click.echo(json.dumps([
197 {"type": r.type, "name": r.name, "file_path": r.file_path,
198 "line_start": r.line_start, "docstring": r.docstring,
199 "description": r.description}
200 for r in results
201 ], indent=2))
 
 
 
 
 
 
 
 
 
 
202 return
203
204 if not results:
205 console.print("[yellow]No results.[/yellow]")
206 return
@@ -215,25 +244,32 @@
215 table.add_row(r.type, r.name, loc, str(r.line_start or ""))
216 console.print(table)
217
218
219 # ── CODE: decorator / query ───────────────────────────────────────────────────
 
220
221 @main.command()
222 @click.argument("decorator_name")
223 @DB_OPTION
224 @FMT_OPTION
225 def decorated(decorator_name: str, db: str, fmt: str):
226 """Find all functions/methods carrying a decorator."""
227 from navegador.context import ContextLoader
 
228 results = ContextLoader(_get_store(db)).decorated_by(decorator_name)
229
230 if fmt == "json":
231 click.echo(json.dumps([
232 {"type": r.type, "name": r.name, "file_path": r.file_path, "line": r.line_start}
233 for r in results
234 ], indent=2))
 
 
 
 
 
235 return
236
237 if not results:
238 console.print(f"[yellow]No functions decorated with @{decorator_name}[/yellow]")
239 return
@@ -256,10 +292,11 @@
256 result = _get_store(db).query(cypher)
257 click.echo(json.dumps(result.result_set or [], default=str, indent=2))
258
259
260 # ── KNOWLEDGE: add group ──────────────────────────────────────────────────────
 
261
262 @main.group()
263 def add():
264 """Add knowledge nodes — concepts, rules, decisions, people, domains."""
265
@@ -273,13 +310,13 @@
273 @click.option("--wiki", default="", help="Wiki URL or reference.")
274 @DB_OPTION
275 def add_concept(name: str, desc: str, domain: str, status: str, rules: str, wiki: str, db: str):
276 """Add a business concept to the knowledge graph."""
277 from navegador.ingestion import KnowledgeIngester
 
278 k = KnowledgeIngester(_get_store(db))
279 k.add_concept(name, description=desc, domain=domain, status=status,
280 rules=rules, wiki_refs=wiki)
281 console.print(f"[green]Concept added:[/green] {name}")
282
283
284 @add.command("rule")
285 @click.argument("name")
@@ -289,10 +326,11 @@
289 @click.option("--rationale", default="")
290 @DB_OPTION
291 def add_rule(name: str, desc: str, domain: str, severity: str, rationale: str, db: str):
292 """Add a business rule or constraint."""
293 from navegador.ingestion import KnowledgeIngester
 
294 k = KnowledgeIngester(_get_store(db))
295 k.add_rule(name, description=desc, domain=domain, severity=severity, rationale=rationale)
296 console.print(f"[green]Rule added:[/green] {name}")
297
298
@@ -301,19 +339,28 @@
301 @click.option("--desc", default="")
302 @click.option("--domain", default="")
303 @click.option("--rationale", default="")
304 @click.option("--alternatives", default="")
305 @click.option("--date", default="")
306 @click.option("--status", default="accepted",
307 type=click.Choice(["proposed", "accepted", "deprecated"]))
 
308 @DB_OPTION
309 def add_decision(name, desc, domain, rationale, alternatives, date, status, db):
310 """Add an architectural or product decision."""
311 from navegador.ingestion import KnowledgeIngester
 
312 k = KnowledgeIngester(_get_store(db))
313 k.add_decision(name, description=desc, domain=domain, status=status,
314 rationale=rationale, alternatives=alternatives, date=date)
 
 
 
 
 
 
 
315 console.print(f"[green]Decision added:[/green] {name}")
316
317
318 @add.command("person")
319 @click.argument("name")
@@ -322,10 +369,11 @@
322 @click.option("--team", default="")
323 @DB_OPTION
324 def add_person(name: str, email: str, role: str, team: str, db: str):
325 """Add a person (contributor, owner, stakeholder)."""
326 from navegador.ingestion import KnowledgeIngester
 
327 k = KnowledgeIngester(_get_store(db))
328 k.add_person(name, email=email, role=role, team=team)
329 console.print(f"[green]Person added:[/green] {name}")
330
331
@@ -334,42 +382,50 @@
334 @click.option("--desc", default="")
335 @DB_OPTION
336 def add_domain(name: str, desc: str, db: str):
337 """Add a business domain (auth, billing, notifications…)."""
338 from navegador.ingestion import KnowledgeIngester
 
339 k = KnowledgeIngester(_get_store(db))
340 k.add_domain(name, description=desc)
341 console.print(f"[green]Domain added:[/green] {name}")
342
343
344 # ── KNOWLEDGE: annotate ───────────────────────────────────────────────────────
 
345
346 @main.command()
347 @click.argument("code_name")
348 @click.option("--type", "code_label", default="Function",
349 type=click.Choice(["Function", "Class", "Method", "File", "Module"]))
 
 
 
 
350 @click.option("--concept", default="", help="Link to this concept.")
351 @click.option("--rule", default="", help="Link to this rule.")
352 @DB_OPTION
353 def annotate(code_name: str, code_label: str, concept: str, rule: str, db: str):
354 """Link a code node to a concept or rule in the knowledge graph."""
355 from navegador.ingestion import KnowledgeIngester
 
356 k = KnowledgeIngester(_get_store(db))
357 k.annotate_code(code_name, code_label,
358 concept=concept or None, rule=rule or None)
359 console.print(f"[green]Annotated:[/green] {code_name}")
360
361
362 # ── KNOWLEDGE: domain view ────────────────────────────────────────────────────
 
363
364 @main.command()
365 @click.argument("name")
366 @DB_OPTION
367 @FMT_OPTION
368 def domain(name: str, db: str, fmt: str):
369 """Show everything belonging to a domain — code and knowledge."""
370 from navegador.context import ContextLoader
 
371 bundle = ContextLoader(_get_store(db)).load_domain(name)
372 _emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt)
373
374
375 @main.command()
@@ -377,15 +433,17 @@
377 @DB_OPTION
378 @FMT_OPTION
379 def concept(name: str, db: str, fmt: str):
380 """Load a business concept — rules, related concepts, implementing code, wiki."""
381 from navegador.context import ContextLoader
 
382 bundle = ContextLoader(_get_store(db)).load_concept(name)
383 _emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt)
384
385
386 # ── KNOWLEDGE: wiki ───────────────────────────────────────────────────────────
 
387
388 @main.group()
389 def wiki():
390 """Ingest and manage wiki pages in the knowledge graph."""
391
@@ -397,10 +455,11 @@
397 @click.option("--api", is_flag=True, help="Use GitHub API instead of git clone.")
398 @DB_OPTION
399 def wiki_ingest(repo: str, wiki_dir: str, token: str, api: bool, db: str):
400 """Pull wiki pages into the knowledge graph."""
401 from navegador.ingestion import WikiIngester
 
402 w = WikiIngester(_get_store(db))
403
404 if wiki_dir:
405 stats = w.ingest_local(wiki_dir)
406 elif repo:
@@ -413,32 +472,39 @@
413
414 console.print(f"[green]Wiki ingested:[/green] {stats['pages']} pages, {stats['links']} links")
415
416
417 # ── Stats ─────────────────────────────────────────────────────────────────────
 
418
419 @main.command()
420 @DB_OPTION
421 @click.option("--json", "as_json", is_flag=True)
422 def stats(db: str, as_json: bool):
423 """Graph statistics broken down by node and edge type."""
424 from navegador.graph import queries as q
 
425 store = _get_store(db)
426
427 node_rows = (store.query(q.NODE_TYPE_COUNTS).result_set or [])
428 edge_rows = (store.query(q.EDGE_TYPE_COUNTS).result_set or [])
429
430 total_nodes = sum(r[1] for r in node_rows)
431 total_edges = sum(r[1] for r in edge_rows)
432
433 if as_json:
434 click.echo(json.dumps({
435 "total_nodes": total_nodes,
436 "total_edges": total_edges,
437 "nodes": {r[0]: r[1] for r in node_rows},
438 "edges": {r[0]: r[1] for r in edge_rows},
439 }, indent=2))
 
 
 
 
 
440 return
441
442 node_table = Table(title=f"Nodes ({total_nodes:,})")
443 node_table.add_column("Type", style="cyan")
444 node_table.add_column("Count", justify="right", style="green")
@@ -454,22 +520,27 @@
454 console.print(node_table)
455 console.print(edge_table)
456
457
458 # ── PLANOPTICON ingestion ──────────────────────────────────────────────────────
 
459
460 @main.group()
461 def planopticon():
462 """Ingest planopticon output (meetings, videos, docs) into the knowledge graph."""
463
464
465 @planopticon.command("ingest")
466 @click.argument("path", type=click.Path(exists=True))
467 @click.option("--type", "input_type",
468 type=click.Choice(["auto", "manifest", "kg", "interchange", "batch"]),
469 default="auto", show_default=True,
470 help="Input format. auto detects from filename.")
 
 
 
 
471 @click.option("--source", default="", help="Source label for provenance (e.g. 'Q4 planning').")
472 @click.option("--json", "as_json", is_flag=True)
473 @DB_OPTION
474 def planopticon_ingest(path: str, input_type: str, source: str, as_json: bool, db: str):
475 """Load a planopticon output directory or file into the knowledge graph.
@@ -509,14 +580,14 @@
509 input_type = "kg"
510
511 ing = PlanopticonIngester(_get_store(db), source_tag=source)
512
513 dispatch = {
514 "manifest": ing.ingest_manifest,
515 "kg": ing.ingest_kg,
516 "interchange": ing.ingest_interchange,
517 "batch": ing.ingest_batch,
518 }
519 stats = dispatch[input_type](p)
520
521 if as_json:
522 click.echo(json.dumps(stats, indent=2))
@@ -528,10 +599,11 @@
528 table.add_row(k.capitalize(), str(v))
529 console.print(table)
530
531
532 # ── MCP ───────────────────────────────────────────────────────────────────────
 
533
534 @main.command()
535 @DB_OPTION
536 def mcp(db: str):
537 """Start the MCP server for AI agent integration (stdio)."""
538
--- navegador/cli/commands.py
+++ navegador/cli/commands.py
@@ -18,19 +18,22 @@
18
19 DB_OPTION = click.option(
20 "--db", default=".navegador/graph.db", show_default=True, help="Graph DB path."
21 )
22 FMT_OPTION = click.option(
23 "--format",
24 "fmt",
25 type=click.Choice(["markdown", "json"]),
26 default="markdown",
27 show_default=True,
28 help="Output format. Use json for agent/pipe consumption.",
29 )
30
31
32 def _get_store(db: str):
33 from navegador.config import DEFAULT_DB_PATH, get_store
34
35 return get_store(db if db != DEFAULT_DB_PATH else None)
36
37
38 def _emit(text: str, fmt: str) -> None:
39 if fmt == "json":
@@ -38,10 +41,11 @@
41 else:
42 console.print(text)
43
44
45 # ── Root group ────────────────────────────────────────────────────────────────
46
47
48 @click.group()
49 @click.version_option(package_name="navegador")
50 def main():
51 """Navegador — project knowledge graph for AI coding agents.
@@ -51,15 +55,20 @@
55 """
56 logging.basicConfig(level=logging.WARNING)
57
58
59 # ── Init ──────────────────────────────────────────────────────────────────────
60
61
62 @main.command()
63 @click.argument("path", default=".", type=click.Path())
64 @click.option(
65 "--redis",
66 "redis_url",
67 default="",
68 help="Redis URL for centralized/production mode (e.g. redis://host:6379).",
69 )
70 def init(path: str, redis_url: str):
71 """Initialise navegador in a project directory.
72
73 Creates .navegador/ (gitignored), writes .env.example with storage options.
74
@@ -89,10 +98,11 @@
98
99 console.print("\nNext: [bold]navegador ingest .[/bold]")
100
101
102 # ── CODE: ingest ──────────────────────────────────────────────────────────────
103
104
105 @main.command()
106 @click.argument("repo_path", type=click.Path(exists=True))
107 @DB_OPTION
108 @click.option("--clear", is_flag=True, help="Clear existing graph before ingesting.")
@@ -117,18 +127,20 @@
127 table.add_row(k.capitalize(), str(v))
128 console.print(table)
129
130
131 # ── CODE: context / function / class ─────────────────────────────────────────
132
133
134 @main.command()
135 @click.argument("file_path")
136 @DB_OPTION
137 @FMT_OPTION
138 def context(file_path: str, db: str, fmt: str):
139 """Load context for a file — all symbols and their relationships."""
140 from navegador.context import ContextLoader
141
142 bundle = ContextLoader(_get_store(db)).load_file(file_path)
143 _emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt)
144
145
146 @main.command()
@@ -138,10 +150,11 @@
150 @DB_OPTION
151 @FMT_OPTION
152 def function(name: str, file_path: str, db: str, depth: int, fmt: str):
153 """Load context for a function — callers, callees, decorators."""
154 from navegador.context import ContextLoader
155
156 bundle = ContextLoader(_get_store(db)).load_function(name, file_path=file_path, depth=depth)
157 _emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt)
158
159
160 @main.command("class")
@@ -150,41 +163,47 @@
163 @DB_OPTION
164 @FMT_OPTION
165 def class_(name: str, file_path: str, db: str, fmt: str):
166 """Load context for a class — methods, inheritance, references."""
167 from navegador.context import ContextLoader
168
169 bundle = ContextLoader(_get_store(db)).load_class(name, file_path=file_path)
170 _emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt)
171
172
173 # ── UNIVERSAL: explain ────────────────────────────────────────────────────────
174
175
176 @main.command()
177 @click.argument("name")
178 @click.option("--file", "file_path", default="")
179 @DB_OPTION
180 @FMT_OPTION
181 def explain(name: str, file_path: str, db: str, fmt: str):
182 """Full picture: all relationships in and out, code and knowledge layers."""
183 from navegador.context import ContextLoader
184
185 bundle = ContextLoader(_get_store(db)).explain(name, file_path=file_path)
186 _emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt)
187
188
189 # ── UNIVERSAL: search ─────────────────────────────────────────────────────────
190
191
192 @main.command()
193 @click.argument("query")
194 @DB_OPTION
195 @click.option("--limit", default=20, show_default=True)
196 @click.option(
197 "--all", "search_all", is_flag=True, help="Include knowledge layer (concepts, rules, wiki)."
198 )
199 @click.option("--docs", "by_doc", is_flag=True, help="Search docstrings instead of names.")
200 @FMT_OPTION
201 def search(query: str, db: str, limit: int, search_all: bool, by_doc: bool, fmt: str):
202 """Search symbols, concepts, rules, and wiki pages."""
203 from navegador.context import ContextLoader
204
205 loader = ContextLoader(_get_store(db))
206
207 if by_doc:
208 results = loader.search_by_docstring(query, limit=limit)
209 elif search_all:
@@ -191,16 +210,26 @@
210 results = loader.search_all(query, limit=limit)
211 else:
212 results = loader.search(query, limit=limit)
213
214 if fmt == "json":
215 click.echo(
216 json.dumps(
217 [
218 {
219 "type": r.type,
220 "name": r.name,
221 "file_path": r.file_path,
222 "line_start": r.line_start,
223 "docstring": r.docstring,
224 "description": r.description,
225 }
226 for r in results
227 ],
228 indent=2,
229 )
230 )
231 return
232
233 if not results:
234 console.print("[yellow]No results.[/yellow]")
235 return
@@ -215,25 +244,32 @@
244 table.add_row(r.type, r.name, loc, str(r.line_start or ""))
245 console.print(table)
246
247
248 # ── CODE: decorator / query ───────────────────────────────────────────────────
249
250
251 @main.command()
252 @click.argument("decorator_name")
253 @DB_OPTION
254 @FMT_OPTION
255 def decorated(decorator_name: str, db: str, fmt: str):
256 """Find all functions/methods carrying a decorator."""
257 from navegador.context import ContextLoader
258
259 results = ContextLoader(_get_store(db)).decorated_by(decorator_name)
260
261 if fmt == "json":
262 click.echo(
263 json.dumps(
264 [
265 {"type": r.type, "name": r.name, "file_path": r.file_path, "line": r.line_start}
266 for r in results
267 ],
268 indent=2,
269 )
270 )
271 return
272
273 if not results:
274 console.print(f"[yellow]No functions decorated with @{decorator_name}[/yellow]")
275 return
@@ -256,10 +292,11 @@
292 result = _get_store(db).query(cypher)
293 click.echo(json.dumps(result.result_set or [], default=str, indent=2))
294
295
296 # ── KNOWLEDGE: add group ──────────────────────────────────────────────────────
297
298
299 @main.group()
300 def add():
301 """Add knowledge nodes — concepts, rules, decisions, people, domains."""
302
@@ -273,13 +310,13 @@
310 @click.option("--wiki", default="", help="Wiki URL or reference.")
311 @DB_OPTION
312 def add_concept(name: str, desc: str, domain: str, status: str, rules: str, wiki: str, db: str):
313 """Add a business concept to the knowledge graph."""
314 from navegador.ingestion import KnowledgeIngester
315
316 k = KnowledgeIngester(_get_store(db))
317 k.add_concept(name, description=desc, domain=domain, status=status, rules=rules, wiki_refs=wiki)
 
318 console.print(f"[green]Concept added:[/green] {name}")
319
320
321 @add.command("rule")
322 @click.argument("name")
@@ -289,10 +326,11 @@
326 @click.option("--rationale", default="")
327 @DB_OPTION
328 def add_rule(name: str, desc: str, domain: str, severity: str, rationale: str, db: str):
329 """Add a business rule or constraint."""
330 from navegador.ingestion import KnowledgeIngester
331
332 k = KnowledgeIngester(_get_store(db))
333 k.add_rule(name, description=desc, domain=domain, severity=severity, rationale=rationale)
334 console.print(f"[green]Rule added:[/green] {name}")
335
336
@@ -301,19 +339,28 @@
339 @click.option("--desc", default="")
340 @click.option("--domain", default="")
341 @click.option("--rationale", default="")
342 @click.option("--alternatives", default="")
343 @click.option("--date", default="")
344 @click.option(
345 "--status", default="accepted", type=click.Choice(["proposed", "accepted", "deprecated"])
346 )
347 @DB_OPTION
348 def add_decision(name, desc, domain, rationale, alternatives, date, status, db):
349 """Add an architectural or product decision."""
350 from navegador.ingestion import KnowledgeIngester
351
352 k = KnowledgeIngester(_get_store(db))
353 k.add_decision(
354 name,
355 description=desc,
356 domain=domain,
357 status=status,
358 rationale=rationale,
359 alternatives=alternatives,
360 date=date,
361 )
362 console.print(f"[green]Decision added:[/green] {name}")
363
364
365 @add.command("person")
366 @click.argument("name")
@@ -322,10 +369,11 @@
369 @click.option("--team", default="")
370 @DB_OPTION
371 def add_person(name: str, email: str, role: str, team: str, db: str):
372 """Add a person (contributor, owner, stakeholder)."""
373 from navegador.ingestion import KnowledgeIngester
374
375 k = KnowledgeIngester(_get_store(db))
376 k.add_person(name, email=email, role=role, team=team)
377 console.print(f"[green]Person added:[/green] {name}")
378
379
@@ -334,42 +382,50 @@
382 @click.option("--desc", default="")
383 @DB_OPTION
384 def add_domain(name: str, desc: str, db: str):
385 """Add a business domain (auth, billing, notifications…)."""
386 from navegador.ingestion import KnowledgeIngester
387
388 k = KnowledgeIngester(_get_store(db))
389 k.add_domain(name, description=desc)
390 console.print(f"[green]Domain added:[/green] {name}")
391
392
393 # ── KNOWLEDGE: annotate ───────────────────────────────────────────────────────
394
395
396 @main.command()
397 @click.argument("code_name")
398 @click.option(
399 "--type",
400 "code_label",
401 default="Function",
402 type=click.Choice(["Function", "Class", "Method", "File", "Module"]),
403 )
404 @click.option("--concept", default="", help="Link to this concept.")
405 @click.option("--rule", default="", help="Link to this rule.")
406 @DB_OPTION
407 def annotate(code_name: str, code_label: str, concept: str, rule: str, db: str):
408 """Link a code node to a concept or rule in the knowledge graph."""
409 from navegador.ingestion import KnowledgeIngester
410
411 k = KnowledgeIngester(_get_store(db))
412 k.annotate_code(code_name, code_label, concept=concept or None, rule=rule or None)
 
413 console.print(f"[green]Annotated:[/green] {code_name}")
414
415
416 # ── KNOWLEDGE: domain view ────────────────────────────────────────────────────
417
418
419 @main.command()
420 @click.argument("name")
421 @DB_OPTION
422 @FMT_OPTION
423 def domain(name: str, db: str, fmt: str):
424 """Show everything belonging to a domain — code and knowledge."""
425 from navegador.context import ContextLoader
426
427 bundle = ContextLoader(_get_store(db)).load_domain(name)
428 _emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt)
429
430
431 @main.command()
@@ -377,15 +433,17 @@
433 @DB_OPTION
434 @FMT_OPTION
435 def concept(name: str, db: str, fmt: str):
436 """Load a business concept — rules, related concepts, implementing code, wiki."""
437 from navegador.context import ContextLoader
438
439 bundle = ContextLoader(_get_store(db)).load_concept(name)
440 _emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt)
441
442
443 # ── KNOWLEDGE: wiki ───────────────────────────────────────────────────────────
444
445
446 @main.group()
447 def wiki():
448 """Ingest and manage wiki pages in the knowledge graph."""
449
@@ -397,10 +455,11 @@
455 @click.option("--api", is_flag=True, help="Use GitHub API instead of git clone.")
456 @DB_OPTION
457 def wiki_ingest(repo: str, wiki_dir: str, token: str, api: bool, db: str):
458 """Pull wiki pages into the knowledge graph."""
459 from navegador.ingestion import WikiIngester
460
461 w = WikiIngester(_get_store(db))
462
463 if wiki_dir:
464 stats = w.ingest_local(wiki_dir)
465 elif repo:
@@ -413,32 +472,39 @@
472
473 console.print(f"[green]Wiki ingested:[/green] {stats['pages']} pages, {stats['links']} links")
474
475
476 # ── Stats ─────────────────────────────────────────────────────────────────────
477
478
479 @main.command()
480 @DB_OPTION
481 @click.option("--json", "as_json", is_flag=True)
482 def stats(db: str, as_json: bool):
483 """Graph statistics broken down by node and edge type."""
484 from navegador.graph import queries as q
485
486 store = _get_store(db)
487
488 node_rows = store.query(q.NODE_TYPE_COUNTS).result_set or []
489 edge_rows = store.query(q.EDGE_TYPE_COUNTS).result_set or []
490
491 total_nodes = sum(r[1] for r in node_rows)
492 total_edges = sum(r[1] for r in edge_rows)
493
494 if as_json:
495 click.echo(
496 json.dumps(
497 {
498 "total_nodes": total_nodes,
499 "total_edges": total_edges,
500 "nodes": {r[0]: r[1] for r in node_rows},
501 "edges": {r[0]: r[1] for r in edge_rows},
502 },
503 indent=2,
504 )
505 )
506 return
507
508 node_table = Table(title=f"Nodes ({total_nodes:,})")
509 node_table.add_column("Type", style="cyan")
510 node_table.add_column("Count", justify="right", style="green")
@@ -454,22 +520,27 @@
520 console.print(node_table)
521 console.print(edge_table)
522
523
524 # ── PLANOPTICON ingestion ──────────────────────────────────────────────────────
525
526
527 @main.group()
528 def planopticon():
529 """Ingest planopticon output (meetings, videos, docs) into the knowledge graph."""
530
531
532 @planopticon.command("ingest")
533 @click.argument("path", type=click.Path(exists=True))
534 @click.option(
535 "--type",
536 "input_type",
537 type=click.Choice(["auto", "manifest", "kg", "interchange", "batch"]),
538 default="auto",
539 show_default=True,
540 help="Input format. auto detects from filename.",
541 )
542 @click.option("--source", default="", help="Source label for provenance (e.g. 'Q4 planning').")
543 @click.option("--json", "as_json", is_flag=True)
544 @DB_OPTION
545 def planopticon_ingest(path: str, input_type: str, source: str, as_json: bool, db: str):
546 """Load a planopticon output directory or file into the knowledge graph.
@@ -509,14 +580,14 @@
580 input_type = "kg"
581
582 ing = PlanopticonIngester(_get_store(db), source_tag=source)
583
584 dispatch = {
585 "manifest": ing.ingest_manifest,
586 "kg": ing.ingest_kg,
587 "interchange": ing.ingest_interchange,
588 "batch": ing.ingest_batch,
589 }
590 stats = dispatch[input_type](p)
591
592 if as_json:
593 click.echo(json.dumps(stats, indent=2))
@@ -528,10 +599,11 @@
599 table.add_row(k.capitalize(), str(v))
600 console.print(table)
601
602
603 # ── MCP ───────────────────────────────────────────────────────────────────────
604
605
606 @main.command()
607 @DB_OPTION
608 def mcp(db: str):
609 """Start the MCP server for AI agent integration (stdio)."""
610
--- navegador/context/loader.py
+++ navegador/context/loader.py
@@ -108,21 +108,22 @@
108108
def load_file(self, file_path: str) -> ContextBundle:
109109
"""All symbols in a file and their relationships."""
110110
result = self.store.query(queries.FILE_CONTENTS, {"path": file_path})
111111
target = ContextNode(type="File", name=Path(file_path).name, file_path=file_path)
112112
nodes = []
113
- for row in (result.result_set or []):
114
- nodes.append(ContextNode(
115
- type=row[0] or "Unknown",
116
- name=row[1] or "",
117
- file_path=file_path,
118
- line_start=row[2],
119
- docstring=row[3],
120
- signature=row[4],
121
- ))
122
- return ContextBundle(target=target, nodes=nodes,
123
- metadata={"query": "file_contents"})
113
+ for row in result.result_set or []:
114
+ nodes.append(
115
+ ContextNode(
116
+ type=row[0] or "Unknown",
117
+ name=row[1] or "",
118
+ file_path=file_path,
119
+ line_start=row[2],
120
+ docstring=row[3],
121
+ signature=row[4],
122
+ )
123
+ )
124
+ return ContextBundle(target=target, nodes=nodes, metadata={"query": "file_contents"})
124125
125126
# ── Code: function ────────────────────────────────────────────────────────
126127
127128
def load_function(self, name: str, file_path: str = "", depth: int = 2) -> ContextBundle:
128129
"""Callers, callees, decorators — everything touching this function."""
@@ -131,28 +132,32 @@
131132
edges: list[dict[str, str]] = []
132133
133134
params = {"name": name, "file_path": file_path, "depth": depth}
134135
135136
callees = self.store.query(queries.CALLEES, params)
136
- for row in (callees.result_set or []):
137
+ for row in callees.result_set or []:
137138
nodes.append(ContextNode(type=row[0], name=row[1], file_path=row[2], line_start=row[3]))
138139
edges.append({"from": name, "type": "CALLS", "to": row[1]})
139140
140141
callers = self.store.query(queries.CALLERS, params)
141
- for row in (callers.result_set or []):
142
+ for row in callers.result_set or []:
142143
nodes.append(ContextNode(type=row[0], name=row[1], file_path=row[2], line_start=row[3]))
143144
edges.append({"from": row[1], "type": "CALLS", "to": name})
144145
145146
decorators = self.store.query(
146147
queries.DECORATORS_FOR, {"name": name, "file_path": file_path}
147148
)
148
- for row in (decorators.result_set or []):
149
+ for row in decorators.result_set or []:
149150
nodes.append(ContextNode(type="Decorator", name=row[0], file_path=row[1]))
150151
edges.append({"from": row[0], "type": "DECORATES", "to": name})
151152
152
- return ContextBundle(target=target, nodes=nodes, edges=edges,
153
- metadata={"depth": depth, "query": "function_context"})
153
+ return ContextBundle(
154
+ target=target,
155
+ nodes=nodes,
156
+ edges=edges,
157
+ metadata={"depth": depth, "query": "function_context"},
158
+ )
154159
155160
# ── Code: class ───────────────────────────────────────────────────────────
156161
157162
def load_class(self, name: str, file_path: str = "") -> ContextBundle:
158163
"""Methods, parent classes, subclasses, references."""
@@ -159,26 +164,27 @@
159164
target = ContextNode(type="Class", name=name, file_path=file_path)
160165
nodes: list[ContextNode] = []
161166
edges: list[dict[str, str]] = []
162167
163168
parents = self.store.query(queries.CLASS_HIERARCHY, {"name": name})
164
- for row in (parents.result_set or []):
169
+ for row in parents.result_set or []:
165170
nodes.append(ContextNode(type="Class", name=row[0], file_path=row[1]))
166171
edges.append({"from": name, "type": "INHERITS", "to": row[0]})
167172
168173
subs = self.store.query(queries.SUBCLASSES, {"name": name})
169
- for row in (subs.result_set or []):
174
+ for row in subs.result_set or []:
170175
nodes.append(ContextNode(type="Class", name=row[0], file_path=row[1]))
171176
edges.append({"from": row[0], "type": "INHERITS", "to": name})
172177
173178
refs = self.store.query(queries.REFERENCES_TO, {"name": name, "file_path": ""})
174
- for row in (refs.result_set or []):
179
+ for row in refs.result_set or []:
175180
nodes.append(ContextNode(type=row[0], name=row[1], file_path=row[2], line_start=row[3]))
176181
edges.append({"from": row[1], "type": "REFERENCES", "to": name})
177182
178
- return ContextBundle(target=target, nodes=nodes, edges=edges,
179
- metadata={"query": "class_context"})
183
+ return ContextBundle(
184
+ target=target, nodes=nodes, edges=edges, metadata={"query": "class_context"}
185
+ )
180186
181187
# ── Universal: explain ────────────────────────────────────────────────────
182188
183189
def explain(self, name: str, file_path: str = "") -> ContextBundle:
184190
"""
@@ -189,105 +195,110 @@
189195
target = ContextNode(type="Node", name=name, file_path=file_path)
190196
nodes: list[ContextNode] = []
191197
edges: list[dict[str, str]] = []
192198
193199
outbound = self.store.query(queries.OUTBOUND, params)
194
- for row in (outbound.result_set or []):
200
+ for row in outbound.result_set or []:
195201
rel, ntype, nname, npath = row[0], row[1], row[2], row[3]
196202
nodes.append(ContextNode(type=ntype, name=nname, file_path=npath))
197203
edges.append({"from": name, "type": rel, "to": nname})
198204
199205
inbound = self.store.query(queries.INBOUND, params)
200
- for row in (inbound.result_set or []):
206
+ for row in inbound.result_set or []:
201207
rel, ntype, nname, npath = row[0], row[1], row[2], row[3]
202208
nodes.append(ContextNode(type=ntype, name=nname, file_path=npath))
203209
edges.append({"from": nname, "type": rel, "to": name})
204210
205
- return ContextBundle(target=target, nodes=nodes, edges=edges,
206
- metadata={"query": "explain"})
211
+ return ContextBundle(target=target, nodes=nodes, edges=edges, metadata={"query": "explain"})
207212
208213
# ── Knowledge: concept ────────────────────────────────────────────────────
209214
210215
def load_concept(self, name: str) -> ContextBundle:
211216
"""Concept + governing rules + related concepts + implementing code + wiki pages."""
212217
result = self.store.query(queries.CONCEPT_CONTEXT, {"name": name})
213218
rows = result.result_set or []
214219
215220
if not rows:
216
- return ContextBundle(target=ContextNode(type="Concept", name=name),
217
- metadata={"query": "concept_context", "found": False})
221
+ return ContextBundle(
222
+ target=ContextNode(type="Concept", name=name),
223
+ metadata={"query": "concept_context", "found": False},
224
+ )
218225
219226
row = rows[0]
220227
target = ContextNode(
221
- type="Concept", name=row[0],
222
- description=row[1], status=row[2], domain=row[3],
228
+ type="Concept",
229
+ name=row[0],
230
+ description=row[1],
231
+ status=row[2],
232
+ domain=row[3],
223233
)
224234
nodes: list[ContextNode] = []
225235
edges: list[dict[str, str]] = []
226236
227
- for cname in (row[4] or []):
237
+ for cname in row[4] or []:
228238
nodes.append(ContextNode(type="Concept", name=cname))
229239
edges.append({"from": name, "type": "RELATED_TO", "to": cname})
230
- for rname in (row[5] or []):
240
+ for rname in row[5] or []:
231241
nodes.append(ContextNode(type="Rule", name=rname))
232242
edges.append({"from": rname, "type": "GOVERNS", "to": name})
233
- for wname in (row[6] or []):
243
+ for wname in row[6] or []:
234244
nodes.append(ContextNode(type="WikiPage", name=wname))
235245
edges.append({"from": wname, "type": "DOCUMENTS", "to": name})
236
- for iname in (row[7] or []):
246
+ for iname in row[7] or []:
237247
nodes.append(ContextNode(type="Code", name=iname))
238248
edges.append({"from": iname, "type": "IMPLEMENTS", "to": name})
239249
240
- return ContextBundle(target=target, nodes=nodes, edges=edges,
241
- metadata={"query": "concept_context"})
250
+ return ContextBundle(
251
+ target=target, nodes=nodes, edges=edges, metadata={"query": "concept_context"}
252
+ )
242253
243254
# ── Knowledge: domain ─────────────────────────────────────────────────────
244255
245256
def load_domain(self, domain: str) -> ContextBundle:
246257
"""Everything belonging to a domain — code and knowledge."""
247258
result = self.store.query(queries.DOMAIN_CONTENTS, {"domain": domain})
248259
target = ContextNode(type="Domain", name=domain)
249260
nodes = [
250
- ContextNode(type=row[0], name=row[1], file_path=row[2],
251
- description=row[3] or None)
261
+ ContextNode(type=row[0], name=row[1], file_path=row[2], description=row[3] or None)
252262
for row in (result.result_set or [])
253263
]
254
- return ContextBundle(target=target, nodes=nodes,
255
- metadata={"query": "domain_contents"})
264
+ return ContextBundle(target=target, nodes=nodes, metadata={"query": "domain_contents"})
256265
257266
# ── Search ────────────────────────────────────────────────────────────────
258267
259268
def search(self, query: str, limit: int = 20) -> list[ContextNode]:
260269
"""Search code symbols by name."""
261270
result = self.store.query(queries.SYMBOL_SEARCH, {"query": query, "limit": limit})
262271
return [
263
- ContextNode(type=row[0], name=row[1], file_path=row[2],
264
- line_start=row[3], docstring=row[4])
272
+ ContextNode(
273
+ type=row[0], name=row[1], file_path=row[2], line_start=row[3], docstring=row[4]
274
+ )
265275
for row in (result.result_set or [])
266276
]
267277
268278
def search_all(self, query: str, limit: int = 20) -> list[ContextNode]:
269279
"""Search everything — code symbols, concepts, rules, decisions, wiki."""
270280
result = self.store.query(queries.GLOBAL_SEARCH, {"query": query, "limit": limit})
271281
return [
272
- ContextNode(type=row[0], name=row[1], file_path=row[2],
273
- docstring=row[3], line_start=row[4])
282
+ ContextNode(
283
+ type=row[0], name=row[1], file_path=row[2], docstring=row[3], line_start=row[4]
284
+ )
274285
for row in (result.result_set or [])
275286
]
276287
277288
def search_by_docstring(self, query: str, limit: int = 20) -> list[ContextNode]:
278289
"""Search functions/classes whose docstring contains the query."""
279290
result = self.store.query(queries.DOCSTRING_SEARCH, {"query": query, "limit": limit})
280291
return [
281
- ContextNode(type=row[0], name=row[1], file_path=row[2],
282
- line_start=row[3], docstring=row[4])
292
+ ContextNode(
293
+ type=row[0], name=row[1], file_path=row[2], line_start=row[3], docstring=row[4]
294
+ )
283295
for row in (result.result_set or [])
284296
]
285297
286298
def decorated_by(self, decorator_name: str) -> list[ContextNode]:
287299
"""All functions/methods carrying a given decorator."""
288
- result = self.store.query(queries.DECORATED_BY,
289
- {"decorator_name": decorator_name})
300
+ result = self.store.query(queries.DECORATED_BY, {"decorator_name": decorator_name})
290301
return [
291302
ContextNode(type=row[0], name=row[1], file_path=row[2], line_start=row[3])
292303
for row in (result.result_set or [])
293304
]
294305
--- navegador/context/loader.py
+++ navegador/context/loader.py
@@ -108,21 +108,22 @@
108 def load_file(self, file_path: str) -> ContextBundle:
109 """All symbols in a file and their relationships."""
110 result = self.store.query(queries.FILE_CONTENTS, {"path": file_path})
111 target = ContextNode(type="File", name=Path(file_path).name, file_path=file_path)
112 nodes = []
113 for row in (result.result_set or []):
114 nodes.append(ContextNode(
115 type=row[0] or "Unknown",
116 name=row[1] or "",
117 file_path=file_path,
118 line_start=row[2],
119 docstring=row[3],
120 signature=row[4],
121 ))
122 return ContextBundle(target=target, nodes=nodes,
123 metadata={"query": "file_contents"})
 
124
125 # ── Code: function ────────────────────────────────────────────────────────
126
127 def load_function(self, name: str, file_path: str = "", depth: int = 2) -> ContextBundle:
128 """Callers, callees, decorators — everything touching this function."""
@@ -131,28 +132,32 @@
131 edges: list[dict[str, str]] = []
132
133 params = {"name": name, "file_path": file_path, "depth": depth}
134
135 callees = self.store.query(queries.CALLEES, params)
136 for row in (callees.result_set or []):
137 nodes.append(ContextNode(type=row[0], name=row[1], file_path=row[2], line_start=row[3]))
138 edges.append({"from": name, "type": "CALLS", "to": row[1]})
139
140 callers = self.store.query(queries.CALLERS, params)
141 for row in (callers.result_set or []):
142 nodes.append(ContextNode(type=row[0], name=row[1], file_path=row[2], line_start=row[3]))
143 edges.append({"from": row[1], "type": "CALLS", "to": name})
144
145 decorators = self.store.query(
146 queries.DECORATORS_FOR, {"name": name, "file_path": file_path}
147 )
148 for row in (decorators.result_set or []):
149 nodes.append(ContextNode(type="Decorator", name=row[0], file_path=row[1]))
150 edges.append({"from": row[0], "type": "DECORATES", "to": name})
151
152 return ContextBundle(target=target, nodes=nodes, edges=edges,
153 metadata={"depth": depth, "query": "function_context"})
 
 
 
 
154
155 # ── Code: class ───────────────────────────────────────────────────────────
156
157 def load_class(self, name: str, file_path: str = "") -> ContextBundle:
158 """Methods, parent classes, subclasses, references."""
@@ -159,26 +164,27 @@
159 target = ContextNode(type="Class", name=name, file_path=file_path)
160 nodes: list[ContextNode] = []
161 edges: list[dict[str, str]] = []
162
163 parents = self.store.query(queries.CLASS_HIERARCHY, {"name": name})
164 for row in (parents.result_set or []):
165 nodes.append(ContextNode(type="Class", name=row[0], file_path=row[1]))
166 edges.append({"from": name, "type": "INHERITS", "to": row[0]})
167
168 subs = self.store.query(queries.SUBCLASSES, {"name": name})
169 for row in (subs.result_set or []):
170 nodes.append(ContextNode(type="Class", name=row[0], file_path=row[1]))
171 edges.append({"from": row[0], "type": "INHERITS", "to": name})
172
173 refs = self.store.query(queries.REFERENCES_TO, {"name": name, "file_path": ""})
174 for row in (refs.result_set or []):
175 nodes.append(ContextNode(type=row[0], name=row[1], file_path=row[2], line_start=row[3]))
176 edges.append({"from": row[1], "type": "REFERENCES", "to": name})
177
178 return ContextBundle(target=target, nodes=nodes, edges=edges,
179 metadata={"query": "class_context"})
 
180
181 # ── Universal: explain ────────────────────────────────────────────────────
182
183 def explain(self, name: str, file_path: str = "") -> ContextBundle:
184 """
@@ -189,105 +195,110 @@
189 target = ContextNode(type="Node", name=name, file_path=file_path)
190 nodes: list[ContextNode] = []
191 edges: list[dict[str, str]] = []
192
193 outbound = self.store.query(queries.OUTBOUND, params)
194 for row in (outbound.result_set or []):
195 rel, ntype, nname, npath = row[0], row[1], row[2], row[3]
196 nodes.append(ContextNode(type=ntype, name=nname, file_path=npath))
197 edges.append({"from": name, "type": rel, "to": nname})
198
199 inbound = self.store.query(queries.INBOUND, params)
200 for row in (inbound.result_set or []):
201 rel, ntype, nname, npath = row[0], row[1], row[2], row[3]
202 nodes.append(ContextNode(type=ntype, name=nname, file_path=npath))
203 edges.append({"from": nname, "type": rel, "to": name})
204
205 return ContextBundle(target=target, nodes=nodes, edges=edges,
206 metadata={"query": "explain"})
207
208 # ── Knowledge: concept ────────────────────────────────────────────────────
209
210 def load_concept(self, name: str) -> ContextBundle:
211 """Concept + governing rules + related concepts + implementing code + wiki pages."""
212 result = self.store.query(queries.CONCEPT_CONTEXT, {"name": name})
213 rows = result.result_set or []
214
215 if not rows:
216 return ContextBundle(target=ContextNode(type="Concept", name=name),
217 metadata={"query": "concept_context", "found": False})
 
 
218
219 row = rows[0]
220 target = ContextNode(
221 type="Concept", name=row[0],
222 description=row[1], status=row[2], domain=row[3],
 
 
 
223 )
224 nodes: list[ContextNode] = []
225 edges: list[dict[str, str]] = []
226
227 for cname in (row[4] or []):
228 nodes.append(ContextNode(type="Concept", name=cname))
229 edges.append({"from": name, "type": "RELATED_TO", "to": cname})
230 for rname in (row[5] or []):
231 nodes.append(ContextNode(type="Rule", name=rname))
232 edges.append({"from": rname, "type": "GOVERNS", "to": name})
233 for wname in (row[6] or []):
234 nodes.append(ContextNode(type="WikiPage", name=wname))
235 edges.append({"from": wname, "type": "DOCUMENTS", "to": name})
236 for iname in (row[7] or []):
237 nodes.append(ContextNode(type="Code", name=iname))
238 edges.append({"from": iname, "type": "IMPLEMENTS", "to": name})
239
240 return ContextBundle(target=target, nodes=nodes, edges=edges,
241 metadata={"query": "concept_context"})
 
242
243 # ── Knowledge: domain ─────────────────────────────────────────────────────
244
245 def load_domain(self, domain: str) -> ContextBundle:
246 """Everything belonging to a domain — code and knowledge."""
247 result = self.store.query(queries.DOMAIN_CONTENTS, {"domain": domain})
248 target = ContextNode(type="Domain", name=domain)
249 nodes = [
250 ContextNode(type=row[0], name=row[1], file_path=row[2],
251 description=row[3] or None)
252 for row in (result.result_set or [])
253 ]
254 return ContextBundle(target=target, nodes=nodes,
255 metadata={"query": "domain_contents"})
256
257 # ── Search ────────────────────────────────────────────────────────────────
258
259 def search(self, query: str, limit: int = 20) -> list[ContextNode]:
260 """Search code symbols by name."""
261 result = self.store.query(queries.SYMBOL_SEARCH, {"query": query, "limit": limit})
262 return [
263 ContextNode(type=row[0], name=row[1], file_path=row[2],
264 line_start=row[3], docstring=row[4])
 
265 for row in (result.result_set or [])
266 ]
267
268 def search_all(self, query: str, limit: int = 20) -> list[ContextNode]:
269 """Search everything — code symbols, concepts, rules, decisions, wiki."""
270 result = self.store.query(queries.GLOBAL_SEARCH, {"query": query, "limit": limit})
271 return [
272 ContextNode(type=row[0], name=row[1], file_path=row[2],
273 docstring=row[3], line_start=row[4])
 
274 for row in (result.result_set or [])
275 ]
276
277 def search_by_docstring(self, query: str, limit: int = 20) -> list[ContextNode]:
278 """Search functions/classes whose docstring contains the query."""
279 result = self.store.query(queries.DOCSTRING_SEARCH, {"query": query, "limit": limit})
280 return [
281 ContextNode(type=row[0], name=row[1], file_path=row[2],
282 line_start=row[3], docstring=row[4])
 
283 for row in (result.result_set or [])
284 ]
285
286 def decorated_by(self, decorator_name: str) -> list[ContextNode]:
287 """All functions/methods carrying a given decorator."""
288 result = self.store.query(queries.DECORATED_BY,
289 {"decorator_name": decorator_name})
290 return [
291 ContextNode(type=row[0], name=row[1], file_path=row[2], line_start=row[3])
292 for row in (result.result_set or [])
293 ]
294
--- navegador/context/loader.py
+++ navegador/context/loader.py
@@ -108,21 +108,22 @@
108 def load_file(self, file_path: str) -> ContextBundle:
109 """All symbols in a file and their relationships."""
110 result = self.store.query(queries.FILE_CONTENTS, {"path": file_path})
111 target = ContextNode(type="File", name=Path(file_path).name, file_path=file_path)
112 nodes = []
113 for row in result.result_set or []:
114 nodes.append(
115 ContextNode(
116 type=row[0] or "Unknown",
117 name=row[1] or "",
118 file_path=file_path,
119 line_start=row[2],
120 docstring=row[3],
121 signature=row[4],
122 )
123 )
124 return ContextBundle(target=target, nodes=nodes, metadata={"query": "file_contents"})
125
126 # ── Code: function ────────────────────────────────────────────────────────
127
128 def load_function(self, name: str, file_path: str = "", depth: int = 2) -> ContextBundle:
129 """Callers, callees, decorators — everything touching this function."""
@@ -131,28 +132,32 @@
132 edges: list[dict[str, str]] = []
133
134 params = {"name": name, "file_path": file_path, "depth": depth}
135
136 callees = self.store.query(queries.CALLEES, params)
137 for row in callees.result_set or []:
138 nodes.append(ContextNode(type=row[0], name=row[1], file_path=row[2], line_start=row[3]))
139 edges.append({"from": name, "type": "CALLS", "to": row[1]})
140
141 callers = self.store.query(queries.CALLERS, params)
142 for row in callers.result_set or []:
143 nodes.append(ContextNode(type=row[0], name=row[1], file_path=row[2], line_start=row[3]))
144 edges.append({"from": row[1], "type": "CALLS", "to": name})
145
146 decorators = self.store.query(
147 queries.DECORATORS_FOR, {"name": name, "file_path": file_path}
148 )
149 for row in decorators.result_set or []:
150 nodes.append(ContextNode(type="Decorator", name=row[0], file_path=row[1]))
151 edges.append({"from": row[0], "type": "DECORATES", "to": name})
152
153 return ContextBundle(
154 target=target,
155 nodes=nodes,
156 edges=edges,
157 metadata={"depth": depth, "query": "function_context"},
158 )
159
160 # ── Code: class ───────────────────────────────────────────────────────────
161
162 def load_class(self, name: str, file_path: str = "") -> ContextBundle:
163 """Methods, parent classes, subclasses, references."""
@@ -159,26 +164,27 @@
164 target = ContextNode(type="Class", name=name, file_path=file_path)
165 nodes: list[ContextNode] = []
166 edges: list[dict[str, str]] = []
167
168 parents = self.store.query(queries.CLASS_HIERARCHY, {"name": name})
169 for row in parents.result_set or []:
170 nodes.append(ContextNode(type="Class", name=row[0], file_path=row[1]))
171 edges.append({"from": name, "type": "INHERITS", "to": row[0]})
172
173 subs = self.store.query(queries.SUBCLASSES, {"name": name})
174 for row in subs.result_set or []:
175 nodes.append(ContextNode(type="Class", name=row[0], file_path=row[1]))
176 edges.append({"from": row[0], "type": "INHERITS", "to": name})
177
178 refs = self.store.query(queries.REFERENCES_TO, {"name": name, "file_path": ""})
179 for row in refs.result_set or []:
180 nodes.append(ContextNode(type=row[0], name=row[1], file_path=row[2], line_start=row[3]))
181 edges.append({"from": row[1], "type": "REFERENCES", "to": name})
182
183 return ContextBundle(
184 target=target, nodes=nodes, edges=edges, metadata={"query": "class_context"}
185 )
186
187 # ── Universal: explain ────────────────────────────────────────────────────
188
189 def explain(self, name: str, file_path: str = "") -> ContextBundle:
190 """
@@ -189,105 +195,110 @@
195 target = ContextNode(type="Node", name=name, file_path=file_path)
196 nodes: list[ContextNode] = []
197 edges: list[dict[str, str]] = []
198
199 outbound = self.store.query(queries.OUTBOUND, params)
200 for row in outbound.result_set or []:
201 rel, ntype, nname, npath = row[0], row[1], row[2], row[3]
202 nodes.append(ContextNode(type=ntype, name=nname, file_path=npath))
203 edges.append({"from": name, "type": rel, "to": nname})
204
205 inbound = self.store.query(queries.INBOUND, params)
206 for row in inbound.result_set or []:
207 rel, ntype, nname, npath = row[0], row[1], row[2], row[3]
208 nodes.append(ContextNode(type=ntype, name=nname, file_path=npath))
209 edges.append({"from": nname, "type": rel, "to": name})
210
211 return ContextBundle(target=target, nodes=nodes, edges=edges, metadata={"query": "explain"})
 
212
213 # ── Knowledge: concept ────────────────────────────────────────────────────
214
215 def load_concept(self, name: str) -> ContextBundle:
216 """Concept + governing rules + related concepts + implementing code + wiki pages."""
217 result = self.store.query(queries.CONCEPT_CONTEXT, {"name": name})
218 rows = result.result_set or []
219
220 if not rows:
221 return ContextBundle(
222 target=ContextNode(type="Concept", name=name),
223 metadata={"query": "concept_context", "found": False},
224 )
225
226 row = rows[0]
227 target = ContextNode(
228 type="Concept",
229 name=row[0],
230 description=row[1],
231 status=row[2],
232 domain=row[3],
233 )
234 nodes: list[ContextNode] = []
235 edges: list[dict[str, str]] = []
236
237 for cname in row[4] or []:
238 nodes.append(ContextNode(type="Concept", name=cname))
239 edges.append({"from": name, "type": "RELATED_TO", "to": cname})
240 for rname in row[5] or []:
241 nodes.append(ContextNode(type="Rule", name=rname))
242 edges.append({"from": rname, "type": "GOVERNS", "to": name})
243 for wname in row[6] or []:
244 nodes.append(ContextNode(type="WikiPage", name=wname))
245 edges.append({"from": wname, "type": "DOCUMENTS", "to": name})
246 for iname in row[7] or []:
247 nodes.append(ContextNode(type="Code", name=iname))
248 edges.append({"from": iname, "type": "IMPLEMENTS", "to": name})
249
250 return ContextBundle(
251 target=target, nodes=nodes, edges=edges, metadata={"query": "concept_context"}
252 )
253
254 # ── Knowledge: domain ─────────────────────────────────────────────────────
255
256 def load_domain(self, domain: str) -> ContextBundle:
257 """Everything belonging to a domain — code and knowledge."""
258 result = self.store.query(queries.DOMAIN_CONTENTS, {"domain": domain})
259 target = ContextNode(type="Domain", name=domain)
260 nodes = [
261 ContextNode(type=row[0], name=row[1], file_path=row[2], description=row[3] or None)
 
262 for row in (result.result_set or [])
263 ]
264 return ContextBundle(target=target, nodes=nodes, metadata={"query": "domain_contents"})
 
265
266 # ── Search ────────────────────────────────────────────────────────────────
267
268 def search(self, query: str, limit: int = 20) -> list[ContextNode]:
269 """Search code symbols by name."""
270 result = self.store.query(queries.SYMBOL_SEARCH, {"query": query, "limit": limit})
271 return [
272 ContextNode(
273 type=row[0], name=row[1], file_path=row[2], line_start=row[3], docstring=row[4]
274 )
275 for row in (result.result_set or [])
276 ]
277
278 def search_all(self, query: str, limit: int = 20) -> list[ContextNode]:
279 """Search everything — code symbols, concepts, rules, decisions, wiki."""
280 result = self.store.query(queries.GLOBAL_SEARCH, {"query": query, "limit": limit})
281 return [
282 ContextNode(
283 type=row[0], name=row[1], file_path=row[2], docstring=row[3], line_start=row[4]
284 )
285 for row in (result.result_set or [])
286 ]
287
288 def search_by_docstring(self, query: str, limit: int = 20) -> list[ContextNode]:
289 """Search functions/classes whose docstring contains the query."""
290 result = self.store.query(queries.DOCSTRING_SEARCH, {"query": query, "limit": limit})
291 return [
292 ContextNode(
293 type=row[0], name=row[1], file_path=row[2], line_start=row[3], docstring=row[4]
294 )
295 for row in (result.result_set or [])
296 ]
297
298 def decorated_by(self, decorator_name: str) -> list[ContextNode]:
299 """All functions/methods carrying a given decorator."""
300 result = self.store.query(queries.DECORATED_BY, {"decorator_name": decorator_name})
 
301 return [
302 ContextNode(type=row[0], name=row[1], file_path=row[2], line_start=row[3])
303 for row in (result.result_set or [])
304 ]
305
--- navegador/graph/schema.py
+++ navegador/graph/schema.py
@@ -25,38 +25,38 @@
2525
Variable = "Variable"
2626
Import = "Import"
2727
Decorator = "Decorator"
2828
2929
# ── Knowledge layer ───────────────────────────────────────────────────────
30
- Domain = "Domain" # logical grouping (auth, billing, notifications…)
31
- Concept = "Concept" # a named business entity or idea
32
- Rule = "Rule" # a constraint, invariant, or business rule
33
- Decision = "Decision" # an architectural or product decision + rationale
34
- WikiPage = "WikiPage" # a page from the project wiki (GitHub, Confluence…)
35
- Person = "Person" # a contributor, owner, or stakeholder
30
+ Domain = "Domain" # logical grouping (auth, billing, notifications…)
31
+ Concept = "Concept" # a named business entity or idea
32
+ Rule = "Rule" # a constraint, invariant, or business rule
33
+ Decision = "Decision" # an architectural or product decision + rationale
34
+ WikiPage = "WikiPage" # a page from the project wiki (GitHub, Confluence…)
35
+ Person = "Person" # a contributor, owner, or stakeholder
3636
3737
3838
class EdgeType(StrEnum):
3939
# ── Code structural ───────────────────────────────────────────────────────
40
- CONTAINS = "CONTAINS" # File/Class -CONTAINS-> Function/Class/Variable
41
- DEFINES = "DEFINES" # Module -DEFINES-> Class/Function
42
- IMPORTS = "IMPORTS" # File -IMPORTS-> Module/File
43
- DEPENDS_ON = "DEPENDS_ON" # module/package-level dependency
44
- CALLS = "CALLS" # Function -CALLS-> Function
45
- REFERENCES = "REFERENCES" # Function/Class -REFERENCES-> Variable/Class
46
- INHERITS = "INHERITS" # Class -INHERITS-> Class
47
- IMPLEMENTS = "IMPLEMENTS" # Class/Function -IMPLEMENTS-> Concept/Rule
48
- DECORATES = "DECORATES" # Decorator -DECORATES-> Function/Class
40
+ CONTAINS = "CONTAINS" # File/Class -CONTAINS-> Function/Class/Variable
41
+ DEFINES = "DEFINES" # Module -DEFINES-> Class/Function
42
+ IMPORTS = "IMPORTS" # File -IMPORTS-> Module/File
43
+ DEPENDS_ON = "DEPENDS_ON" # module/package-level dependency
44
+ CALLS = "CALLS" # Function -CALLS-> Function
45
+ REFERENCES = "REFERENCES" # Function/Class -REFERENCES-> Variable/Class
46
+ INHERITS = "INHERITS" # Class -INHERITS-> Class
47
+ IMPLEMENTS = "IMPLEMENTS" # Class/Function -IMPLEMENTS-> Concept/Rule
48
+ DECORATES = "DECORATES" # Decorator -DECORATES-> Function/Class
4949
5050
# ── Knowledge structural ──────────────────────────────────────────────────
51
- BELONGS_TO = "BELONGS_TO" # any node -BELONGS_TO-> Domain
52
- RELATED_TO = "RELATED_TO" # Concept -RELATED_TO-> Concept (bidirectional intent)
53
- GOVERNS = "GOVERNS" # Rule -GOVERNS-> Concept/Function/Class
54
- DOCUMENTS = "DOCUMENTS" # WikiPage/Decision -DOCUMENTS-> any node
55
- ANNOTATES = "ANNOTATES" # Concept/Rule -ANNOTATES-> code node (lightweight link)
56
- ASSIGNED_TO = "ASSIGNED_TO" # any node -ASSIGNED_TO-> Person (ownership)
57
- DECIDED_BY = "DECIDED_BY" # Decision -DECIDED_BY-> Person
51
+ BELONGS_TO = "BELONGS_TO" # any node -BELONGS_TO-> Domain
52
+ RELATED_TO = "RELATED_TO" # Concept -RELATED_TO-> Concept (bidirectional intent)
53
+ GOVERNS = "GOVERNS" # Rule -GOVERNS-> Concept/Function/Class
54
+ DOCUMENTS = "DOCUMENTS" # WikiPage/Decision -DOCUMENTS-> any node
55
+ ANNOTATES = "ANNOTATES" # Concept/Rule -ANNOTATES-> code node (lightweight link)
56
+ ASSIGNED_TO = "ASSIGNED_TO" # any node -ASSIGNED_TO-> Person (ownership)
57
+ DECIDED_BY = "DECIDED_BY" # Decision -DECIDED_BY-> Person
5858
5959
6060
# ── Property keys per node label ──────────────────────────────────────────────
6161
6262
NODE_PROPS = {
@@ -64,37 +64,68 @@
6464
NodeLabel.Repository: ["name", "path", "language", "description"],
6565
NodeLabel.File: ["name", "path", "language", "size", "line_count"],
6666
NodeLabel.Module: ["name", "file_path", "docstring"],
6767
NodeLabel.Class: ["name", "file_path", "line_start", "line_end", "docstring", "source"],
6868
NodeLabel.Function: [
69
- "name", "file_path", "line_start", "line_end", "docstring", "source", "signature",
69
+ "name",
70
+ "file_path",
71
+ "line_start",
72
+ "line_end",
73
+ "docstring",
74
+ "source",
75
+ "signature",
7076
],
7177
NodeLabel.Method: [
72
- "name", "file_path", "line_start", "line_end",
73
- "docstring", "source", "signature", "class_name",
78
+ "name",
79
+ "file_path",
80
+ "line_start",
81
+ "line_end",
82
+ "docstring",
83
+ "source",
84
+ "signature",
85
+ "class_name",
7486
],
7587
NodeLabel.Variable: ["name", "file_path", "line_start", "type_annotation"],
7688
NodeLabel.Import: ["name", "file_path", "line_start", "module", "alias"],
7789
NodeLabel.Decorator: ["name", "file_path", "line_start"],
78
-
7990
# Knowledge layer
8091
NodeLabel.Domain: ["name", "description"],
8192
NodeLabel.Concept: [
82
- "name", "description", "domain", "status",
83
- "rules", "examples", "wiki_refs",
93
+ "name",
94
+ "description",
95
+ "domain",
96
+ "status",
97
+ "rules",
98
+ "examples",
99
+ "wiki_refs",
84100
],
85101
NodeLabel.Rule: [
86
- "name", "description", "domain", "severity", # info|warning|critical
87
- "rationale", "examples",
102
+ "name",
103
+ "description",
104
+ "domain",
105
+ "severity", # info|warning|critical
106
+ "rationale",
107
+ "examples",
88108
],
89109
NodeLabel.Decision: [
90
- "name", "description", "domain", "status", # proposed|accepted|deprecated
91
- "rationale", "alternatives", "date",
110
+ "name",
111
+ "description",
112
+ "domain",
113
+ "status", # proposed|accepted|deprecated
114
+ "rationale",
115
+ "alternatives",
116
+ "date",
92117
],
93118
NodeLabel.WikiPage: [
94
- "name", "url", "source", # github|confluence|notion|local
95
- "content", "updated_at",
119
+ "name",
120
+ "url",
121
+ "source", # github|confluence|notion|local
122
+ "content",
123
+ "updated_at",
96124
],
97125
NodeLabel.Person: [
98
- "name", "email", "role", "team",
126
+ "name",
127
+ "email",
128
+ "role",
129
+ "team",
99130
],
100131
}
101132
--- navegador/graph/schema.py
+++ navegador/graph/schema.py
@@ -25,38 +25,38 @@
25 Variable = "Variable"
26 Import = "Import"
27 Decorator = "Decorator"
28
29 # ── Knowledge layer ───────────────────────────────────────────────────────
30 Domain = "Domain" # logical grouping (auth, billing, notifications…)
31 Concept = "Concept" # a named business entity or idea
32 Rule = "Rule" # a constraint, invariant, or business rule
33 Decision = "Decision" # an architectural or product decision + rationale
34 WikiPage = "WikiPage" # a page from the project wiki (GitHub, Confluence…)
35 Person = "Person" # a contributor, owner, or stakeholder
36
37
38 class EdgeType(StrEnum):
39 # ── Code structural ───────────────────────────────────────────────────────
40 CONTAINS = "CONTAINS" # File/Class -CONTAINS-> Function/Class/Variable
41 DEFINES = "DEFINES" # Module -DEFINES-> Class/Function
42 IMPORTS = "IMPORTS" # File -IMPORTS-> Module/File
43 DEPENDS_ON = "DEPENDS_ON" # module/package-level dependency
44 CALLS = "CALLS" # Function -CALLS-> Function
45 REFERENCES = "REFERENCES" # Function/Class -REFERENCES-> Variable/Class
46 INHERITS = "INHERITS" # Class -INHERITS-> Class
47 IMPLEMENTS = "IMPLEMENTS" # Class/Function -IMPLEMENTS-> Concept/Rule
48 DECORATES = "DECORATES" # Decorator -DECORATES-> Function/Class
49
50 # ── Knowledge structural ──────────────────────────────────────────────────
51 BELONGS_TO = "BELONGS_TO" # any node -BELONGS_TO-> Domain
52 RELATED_TO = "RELATED_TO" # Concept -RELATED_TO-> Concept (bidirectional intent)
53 GOVERNS = "GOVERNS" # Rule -GOVERNS-> Concept/Function/Class
54 DOCUMENTS = "DOCUMENTS" # WikiPage/Decision -DOCUMENTS-> any node
55 ANNOTATES = "ANNOTATES" # Concept/Rule -ANNOTATES-> code node (lightweight link)
56 ASSIGNED_TO = "ASSIGNED_TO" # any node -ASSIGNED_TO-> Person (ownership)
57 DECIDED_BY = "DECIDED_BY" # Decision -DECIDED_BY-> Person
58
59
60 # ── Property keys per node label ──────────────────────────────────────────────
61
62 NODE_PROPS = {
@@ -64,37 +64,68 @@
64 NodeLabel.Repository: ["name", "path", "language", "description"],
65 NodeLabel.File: ["name", "path", "language", "size", "line_count"],
66 NodeLabel.Module: ["name", "file_path", "docstring"],
67 NodeLabel.Class: ["name", "file_path", "line_start", "line_end", "docstring", "source"],
68 NodeLabel.Function: [
69 "name", "file_path", "line_start", "line_end", "docstring", "source", "signature",
 
 
 
 
 
 
70 ],
71 NodeLabel.Method: [
72 "name", "file_path", "line_start", "line_end",
73 "docstring", "source", "signature", "class_name",
 
 
 
 
 
 
74 ],
75 NodeLabel.Variable: ["name", "file_path", "line_start", "type_annotation"],
76 NodeLabel.Import: ["name", "file_path", "line_start", "module", "alias"],
77 NodeLabel.Decorator: ["name", "file_path", "line_start"],
78
79 # Knowledge layer
80 NodeLabel.Domain: ["name", "description"],
81 NodeLabel.Concept: [
82 "name", "description", "domain", "status",
83 "rules", "examples", "wiki_refs",
 
 
 
 
 
84 ],
85 NodeLabel.Rule: [
86 "name", "description", "domain", "severity", # info|warning|critical
87 "rationale", "examples",
 
 
 
 
88 ],
89 NodeLabel.Decision: [
90 "name", "description", "domain", "status", # proposed|accepted|deprecated
91 "rationale", "alternatives", "date",
 
 
 
 
 
92 ],
93 NodeLabel.WikiPage: [
94 "name", "url", "source", # github|confluence|notion|local
95 "content", "updated_at",
 
 
 
96 ],
97 NodeLabel.Person: [
98 "name", "email", "role", "team",
 
 
 
99 ],
100 }
101
--- navegador/graph/schema.py
+++ navegador/graph/schema.py
@@ -25,38 +25,38 @@
25 Variable = "Variable"
26 Import = "Import"
27 Decorator = "Decorator"
28
29 # ── Knowledge layer ───────────────────────────────────────────────────────
30 Domain = "Domain" # logical grouping (auth, billing, notifications…)
31 Concept = "Concept" # a named business entity or idea
32 Rule = "Rule" # a constraint, invariant, or business rule
33 Decision = "Decision" # an architectural or product decision + rationale
34 WikiPage = "WikiPage" # a page from the project wiki (GitHub, Confluence…)
35 Person = "Person" # a contributor, owner, or stakeholder
36
37
38 class EdgeType(StrEnum):
39 # ── Code structural ───────────────────────────────────────────────────────
40 CONTAINS = "CONTAINS" # File/Class -CONTAINS-> Function/Class/Variable
41 DEFINES = "DEFINES" # Module -DEFINES-> Class/Function
42 IMPORTS = "IMPORTS" # File -IMPORTS-> Module/File
43 DEPENDS_ON = "DEPENDS_ON" # module/package-level dependency
44 CALLS = "CALLS" # Function -CALLS-> Function
45 REFERENCES = "REFERENCES" # Function/Class -REFERENCES-> Variable/Class
46 INHERITS = "INHERITS" # Class -INHERITS-> Class
47 IMPLEMENTS = "IMPLEMENTS" # Class/Function -IMPLEMENTS-> Concept/Rule
48 DECORATES = "DECORATES" # Decorator -DECORATES-> Function/Class
49
50 # ── Knowledge structural ──────────────────────────────────────────────────
51 BELONGS_TO = "BELONGS_TO" # any node -BELONGS_TO-> Domain
52 RELATED_TO = "RELATED_TO" # Concept -RELATED_TO-> Concept (bidirectional intent)
53 GOVERNS = "GOVERNS" # Rule -GOVERNS-> Concept/Function/Class
54 DOCUMENTS = "DOCUMENTS" # WikiPage/Decision -DOCUMENTS-> any node
55 ANNOTATES = "ANNOTATES" # Concept/Rule -ANNOTATES-> code node (lightweight link)
56 ASSIGNED_TO = "ASSIGNED_TO" # any node -ASSIGNED_TO-> Person (ownership)
57 DECIDED_BY = "DECIDED_BY" # Decision -DECIDED_BY-> Person
58
59
60 # ── Property keys per node label ──────────────────────────────────────────────
61
62 NODE_PROPS = {
@@ -64,37 +64,68 @@
64 NodeLabel.Repository: ["name", "path", "language", "description"],
65 NodeLabel.File: ["name", "path", "language", "size", "line_count"],
66 NodeLabel.Module: ["name", "file_path", "docstring"],
67 NodeLabel.Class: ["name", "file_path", "line_start", "line_end", "docstring", "source"],
68 NodeLabel.Function: [
69 "name",
70 "file_path",
71 "line_start",
72 "line_end",
73 "docstring",
74 "source",
75 "signature",
76 ],
77 NodeLabel.Method: [
78 "name",
79 "file_path",
80 "line_start",
81 "line_end",
82 "docstring",
83 "source",
84 "signature",
85 "class_name",
86 ],
87 NodeLabel.Variable: ["name", "file_path", "line_start", "type_annotation"],
88 NodeLabel.Import: ["name", "file_path", "line_start", "module", "alias"],
89 NodeLabel.Decorator: ["name", "file_path", "line_start"],
 
90 # Knowledge layer
91 NodeLabel.Domain: ["name", "description"],
92 NodeLabel.Concept: [
93 "name",
94 "description",
95 "domain",
96 "status",
97 "rules",
98 "examples",
99 "wiki_refs",
100 ],
101 NodeLabel.Rule: [
102 "name",
103 "description",
104 "domain",
105 "severity", # info|warning|critical
106 "rationale",
107 "examples",
108 ],
109 NodeLabel.Decision: [
110 "name",
111 "description",
112 "domain",
113 "status", # proposed|accepted|deprecated
114 "rationale",
115 "alternatives",
116 "date",
117 ],
118 NodeLabel.WikiPage: [
119 "name",
120 "url",
121 "source", # github|confluence|notion|local
122 "content",
123 "updated_at",
124 ],
125 NodeLabel.Person: [
126 "name",
127 "email",
128 "role",
129 "team",
130 ],
131 }
132
--- navegador/graph/store.py
+++ navegador/graph/store.py
@@ -74,14 +74,11 @@
7474
return self._graph.query(cypher, params or {})
7575
7676
def create_node(self, label: str, props: dict[str, Any]) -> None:
7777
"""Upsert a node by (label, name, file_path)."""
7878
prop_str = ", ".join(f"n.{k} = ${k}" for k in props)
79
- cypher = (
80
- f"MERGE (n:{label} {{name: $name, file_path: $file_path}}) "
81
- f"SET {prop_str}"
82
- )
79
+ cypher = f"MERGE (n:{label} {{name: $name, file_path: $file_path}}) SET {prop_str}"
8380
self.query(cypher, props)
8481
8582
def create_edge(
8683
self,
8784
from_label: str,
8885
--- navegador/graph/store.py
+++ navegador/graph/store.py
@@ -74,14 +74,11 @@
74 return self._graph.query(cypher, params or {})
75
76 def create_node(self, label: str, props: dict[str, Any]) -> None:
77 """Upsert a node by (label, name, file_path)."""
78 prop_str = ", ".join(f"n.{k} = ${k}" for k in props)
79 cypher = (
80 f"MERGE (n:{label} {{name: $name, file_path: $file_path}}) "
81 f"SET {prop_str}"
82 )
83 self.query(cypher, props)
84
85 def create_edge(
86 self,
87 from_label: str,
88
--- navegador/graph/store.py
+++ navegador/graph/store.py
@@ -74,14 +74,11 @@
74 return self._graph.query(cypher, params or {})
75
76 def create_node(self, label: str, props: dict[str, Any]) -> None:
77 """Upsert a node by (label, name, file_path)."""
78 prop_str = ", ".join(f"n.{k} = ${k}" for k in props)
79 cypher = f"MERGE (n:{label} {{name: $name, file_path: $file_path}}) SET {prop_str}"
 
 
 
80 self.query(cypher, props)
81
82 def create_edge(
83 self,
84 from_label: str,
85
--- navegador/ingestion/go.py
+++ navegador/ingestion/go.py
@@ -15,46 +15,50 @@
1515
1616
def _get_go_language():
1717
try:
1818
import tree_sitter_go as tsgo # type: ignore[import]
1919
from tree_sitter import Language
20
+
2021
return Language(tsgo.language())
2122
except ImportError as e:
2223
raise ImportError("Install tree-sitter-go: pip install tree-sitter-go") from e
2324
2425
2526
def _node_text(node, source: bytes) -> str:
26
- return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
27
+ return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
2728
2829
2930
class GoParser(LanguageParser):
3031
"""Parses Go source files into the navegador graph."""
3132
3233
def __init__(self) -> None:
3334
from tree_sitter import Parser # type: ignore[import]
35
+
3436
self._parser = Parser(_get_go_language())
3537
3638
def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
3739
source = path.read_bytes()
3840
tree = self._parser.parse(source)
3941
rel_path = str(path.relative_to(repo_root))
4042
41
- store.create_node(NodeLabel.File, {
42
- "name": path.name,
43
- "path": rel_path,
44
- "language": "go",
45
- "line_count": source.count(b"\n"),
46
- })
43
+ store.create_node(
44
+ NodeLabel.File,
45
+ {
46
+ "name": path.name,
47
+ "path": rel_path,
48
+ "language": "go",
49
+ "line_count": source.count(b"\n"),
50
+ },
51
+ )
4752
4853
stats = {"functions": 0, "classes": 0, "edges": 0}
4954
self._walk(tree.root_node, source, rel_path, store, stats)
5055
return stats
5156
5257
# ── AST walker ────────────────────────────────────────────────────────────
5358
54
- def _walk(self, node, source: bytes, file_path: str,
55
- store: GraphStore, stats: dict) -> None:
59
+ def _walk(self, node, source: bytes, file_path: str, store: GraphStore, stats: dict) -> None:
5660
if node.type == "function_declaration":
5761
self._handle_function(node, source, file_path, store, stats, receiver=None)
5862
return
5963
if node.type == "method_declaration":
6064
self._handle_method(node, source, file_path, store, stats)
@@ -68,59 +72,70 @@
6872
for child in node.children:
6973
self._walk(child, source, file_path, store, stats)
7074
7175
# ── Handlers ──────────────────────────────────────────────────────────────
7276
73
- def _handle_function(self, node, source: bytes, file_path: str,
74
- store: GraphStore, stats: dict,
75
- receiver: str | None) -> None:
77
+ def _handle_function(
78
+ self,
79
+ node,
80
+ source: bytes,
81
+ file_path: str,
82
+ store: GraphStore,
83
+ stats: dict,
84
+ receiver: str | None,
85
+ ) -> None:
7686
name_node = node.child_by_field_name("name")
7787
if not name_node:
7888
return
7989
name = _node_text(name_node, source)
8090
label = NodeLabel.Method if receiver else NodeLabel.Function
8191
82
- store.create_node(label, {
83
- "name": name,
84
- "file_path": file_path,
85
- "line_start": node.start_point[0] + 1,
86
- "line_end": node.end_point[0] + 1,
87
- "docstring": "",
88
- "class_name": receiver or "",
89
- })
92
+ store.create_node(
93
+ label,
94
+ {
95
+ "name": name,
96
+ "file_path": file_path,
97
+ "line_start": node.start_point[0] + 1,
98
+ "line_end": node.end_point[0] + 1,
99
+ "docstring": "",
100
+ "class_name": receiver or "",
101
+ },
102
+ )
90103
91104
container_label = NodeLabel.Class if receiver else NodeLabel.File
92105
container_key = (
93
- {"name": receiver, "file_path": file_path}
94
- if receiver else {"path": file_path}
106
+ {"name": receiver, "file_path": file_path} if receiver else {"path": file_path}
95107
)
96108
store.create_edge(
97
- container_label, container_key,
109
+ container_label,
110
+ container_key,
98111
EdgeType.CONTAINS,
99
- label, {"name": name, "file_path": file_path},
112
+ label,
113
+ {"name": name, "file_path": file_path},
100114
)
101115
stats["functions"] += 1
102116
stats["edges"] += 1
103117
104118
self._extract_calls(node, source, file_path, name, label, store, stats)
105119
106
- def _handle_method(self, node, source: bytes, file_path: str,
107
- store: GraphStore, stats: dict) -> None:
120
+ def _handle_method(
121
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
122
+ ) -> None:
108123
receiver_type = ""
109124
recv_node = node.child_by_field_name("receiver")
110125
if recv_node:
111126
for child in recv_node.children:
112127
if child.type == "parameter_declaration":
113128
for c in child.children:
114129
if c.type in ("type_identifier", "pointer_type"):
115130
receiver_type = _node_text(c, source).lstrip("*").strip()
116131
break
117
- self._handle_function(node, source, file_path, store, stats,
118
- receiver=receiver_type or None)
132
+ self._handle_function(node, source, file_path, store, stats, receiver=receiver_type or None)
119133
120
- def _handle_type(self, node, source: bytes, file_path: str,
121
- store: GraphStore, stats: dict) -> None:
134
+ def _handle_type(
135
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
136
+ ) -> None:
122137
for child in node.children:
123138
if child.type != "type_spec":
124139
continue
125140
name_node = child.child_by_field_name("name")
126141
type_node = child.child_by_field_name("type")
@@ -128,71 +143,91 @@
128143
continue
129144
if type_node.type not in ("struct_type", "interface_type"):
130145
continue
131146
name = _node_text(name_node, source)
132147
kind = "struct" if type_node.type == "struct_type" else "interface"
133
- store.create_node(NodeLabel.Class, {
134
- "name": name,
135
- "file_path": file_path,
136
- "line_start": node.start_point[0] + 1,
137
- "line_end": node.end_point[0] + 1,
138
- "docstring": kind,
139
- })
148
+ store.create_node(
149
+ NodeLabel.Class,
150
+ {
151
+ "name": name,
152
+ "file_path": file_path,
153
+ "line_start": node.start_point[0] + 1,
154
+ "line_end": node.end_point[0] + 1,
155
+ "docstring": kind,
156
+ },
157
+ )
140158
store.create_edge(
141
- NodeLabel.File, {"path": file_path},
159
+ NodeLabel.File,
160
+ {"path": file_path},
142161
EdgeType.CONTAINS,
143
- NodeLabel.Class, {"name": name, "file_path": file_path},
162
+ NodeLabel.Class,
163
+ {"name": name, "file_path": file_path},
144164
)
145165
stats["classes"] += 1
146166
stats["edges"] += 1
147167
148
- def _handle_import(self, node, source: bytes, file_path: str,
149
- store: GraphStore, stats: dict) -> None:
168
+ def _handle_import(
169
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
170
+ ) -> None:
150171
line_start = node.start_point[0] + 1
151172
for child in node.children:
152173
if child.type == "import_spec":
153174
self._ingest_import_spec(child, source, file_path, line_start, store, stats)
154175
elif child.type == "import_spec_list":
155176
for spec in child.children:
156177
if spec.type == "import_spec":
157
- self._ingest_import_spec(spec, source, file_path, line_start,
158
- store, stats)
178
+ self._ingest_import_spec(spec, source, file_path, line_start, store, stats)
159179
160
- def _ingest_import_spec(self, spec, source: bytes, file_path: str,
161
- line_start: int, store: GraphStore, stats: dict) -> None:
180
+ def _ingest_import_spec(
181
+ self, spec, source: bytes, file_path: str, line_start: int, store: GraphStore, stats: dict
182
+ ) -> None:
162183
path_node = spec.child_by_field_name("path")
163184
if not path_node:
164185
return
165186
module = _node_text(path_node, source).strip('"')
166
- store.create_node(NodeLabel.Import, {
167
- "name": module,
168
- "file_path": file_path,
169
- "line_start": line_start,
170
- "module": module,
171
- })
187
+ store.create_node(
188
+ NodeLabel.Import,
189
+ {
190
+ "name": module,
191
+ "file_path": file_path,
192
+ "line_start": line_start,
193
+ "module": module,
194
+ },
195
+ )
172196
store.create_edge(
173
- NodeLabel.File, {"path": file_path},
197
+ NodeLabel.File,
198
+ {"path": file_path},
174199
EdgeType.IMPORTS,
175
- NodeLabel.Import, {"name": module, "file_path": file_path},
200
+ NodeLabel.Import,
201
+ {"name": module, "file_path": file_path},
176202
)
177203
stats["edges"] += 1
178204
179
- def _extract_calls(self, fn_node, source: bytes, file_path: str,
180
- fn_name: str, fn_label: str,
181
- store: GraphStore, stats: dict) -> None:
205
+ def _extract_calls(
206
+ self,
207
+ fn_node,
208
+ source: bytes,
209
+ file_path: str,
210
+ fn_name: str,
211
+ fn_label: str,
212
+ store: GraphStore,
213
+ stats: dict,
214
+ ) -> None:
182215
def walk(node):
183216
if node.type == "call_expression":
184217
func = node.child_by_field_name("function")
185218
if func:
186219
callee = _node_text(func, source).split(".")[-1]
187220
store.create_edge(
188
- fn_label, {"name": fn_name, "file_path": file_path},
221
+ fn_label,
222
+ {"name": fn_name, "file_path": file_path},
189223
EdgeType.CALLS,
190
- NodeLabel.Function, {"name": callee, "file_path": file_path},
224
+ NodeLabel.Function,
225
+ {"name": callee, "file_path": file_path},
191226
)
192227
stats["edges"] += 1
193228
for child in node.children:
194229
walk(child)
195230
196231
body = fn_node.child_by_field_name("body")
197232
if body:
198233
walk(body)
199234
--- navegador/ingestion/go.py
+++ navegador/ingestion/go.py
@@ -15,46 +15,50 @@
15
16 def _get_go_language():
17 try:
18 import tree_sitter_go as tsgo # type: ignore[import]
19 from tree_sitter import Language
 
20 return Language(tsgo.language())
21 except ImportError as e:
22 raise ImportError("Install tree-sitter-go: pip install tree-sitter-go") from e
23
24
25 def _node_text(node, source: bytes) -> str:
26 return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
27
28
29 class GoParser(LanguageParser):
30 """Parses Go source files into the navegador graph."""
31
32 def __init__(self) -> None:
33 from tree_sitter import Parser # type: ignore[import]
 
34 self._parser = Parser(_get_go_language())
35
36 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
37 source = path.read_bytes()
38 tree = self._parser.parse(source)
39 rel_path = str(path.relative_to(repo_root))
40
41 store.create_node(NodeLabel.File, {
42 "name": path.name,
43 "path": rel_path,
44 "language": "go",
45 "line_count": source.count(b"\n"),
46 })
 
 
 
47
48 stats = {"functions": 0, "classes": 0, "edges": 0}
49 self._walk(tree.root_node, source, rel_path, store, stats)
50 return stats
51
52 # ── AST walker ────────────────────────────────────────────────────────────
53
54 def _walk(self, node, source: bytes, file_path: str,
55 store: GraphStore, stats: dict) -> None:
56 if node.type == "function_declaration":
57 self._handle_function(node, source, file_path, store, stats, receiver=None)
58 return
59 if node.type == "method_declaration":
60 self._handle_method(node, source, file_path, store, stats)
@@ -68,59 +72,70 @@
68 for child in node.children:
69 self._walk(child, source, file_path, store, stats)
70
71 # ── Handlers ──────────────────────────────────────────────────────────────
72
73 def _handle_function(self, node, source: bytes, file_path: str,
74 store: GraphStore, stats: dict,
75 receiver: str | None) -> None:
 
 
 
 
 
 
76 name_node = node.child_by_field_name("name")
77 if not name_node:
78 return
79 name = _node_text(name_node, source)
80 label = NodeLabel.Method if receiver else NodeLabel.Function
81
82 store.create_node(label, {
83 "name": name,
84 "file_path": file_path,
85 "line_start": node.start_point[0] + 1,
86 "line_end": node.end_point[0] + 1,
87 "docstring": "",
88 "class_name": receiver or "",
89 })
 
 
 
90
91 container_label = NodeLabel.Class if receiver else NodeLabel.File
92 container_key = (
93 {"name": receiver, "file_path": file_path}
94 if receiver else {"path": file_path}
95 )
96 store.create_edge(
97 container_label, container_key,
 
98 EdgeType.CONTAINS,
99 label, {"name": name, "file_path": file_path},
 
100 )
101 stats["functions"] += 1
102 stats["edges"] += 1
103
104 self._extract_calls(node, source, file_path, name, label, store, stats)
105
106 def _handle_method(self, node, source: bytes, file_path: str,
107 store: GraphStore, stats: dict) -> None:
 
108 receiver_type = ""
109 recv_node = node.child_by_field_name("receiver")
110 if recv_node:
111 for child in recv_node.children:
112 if child.type == "parameter_declaration":
113 for c in child.children:
114 if c.type in ("type_identifier", "pointer_type"):
115 receiver_type = _node_text(c, source).lstrip("*").strip()
116 break
117 self._handle_function(node, source, file_path, store, stats,
118 receiver=receiver_type or None)
119
120 def _handle_type(self, node, source: bytes, file_path: str,
121 store: GraphStore, stats: dict) -> None:
 
122 for child in node.children:
123 if child.type != "type_spec":
124 continue
125 name_node = child.child_by_field_name("name")
126 type_node = child.child_by_field_name("type")
@@ -128,71 +143,91 @@
128 continue
129 if type_node.type not in ("struct_type", "interface_type"):
130 continue
131 name = _node_text(name_node, source)
132 kind = "struct" if type_node.type == "struct_type" else "interface"
133 store.create_node(NodeLabel.Class, {
134 "name": name,
135 "file_path": file_path,
136 "line_start": node.start_point[0] + 1,
137 "line_end": node.end_point[0] + 1,
138 "docstring": kind,
139 })
 
 
 
140 store.create_edge(
141 NodeLabel.File, {"path": file_path},
 
142 EdgeType.CONTAINS,
143 NodeLabel.Class, {"name": name, "file_path": file_path},
 
144 )
145 stats["classes"] += 1
146 stats["edges"] += 1
147
148 def _handle_import(self, node, source: bytes, file_path: str,
149 store: GraphStore, stats: dict) -> None:
 
150 line_start = node.start_point[0] + 1
151 for child in node.children:
152 if child.type == "import_spec":
153 self._ingest_import_spec(child, source, file_path, line_start, store, stats)
154 elif child.type == "import_spec_list":
155 for spec in child.children:
156 if spec.type == "import_spec":
157 self._ingest_import_spec(spec, source, file_path, line_start,
158 store, stats)
159
160 def _ingest_import_spec(self, spec, source: bytes, file_path: str,
161 line_start: int, store: GraphStore, stats: dict) -> None:
 
162 path_node = spec.child_by_field_name("path")
163 if not path_node:
164 return
165 module = _node_text(path_node, source).strip('"')
166 store.create_node(NodeLabel.Import, {
167 "name": module,
168 "file_path": file_path,
169 "line_start": line_start,
170 "module": module,
171 })
 
 
 
172 store.create_edge(
173 NodeLabel.File, {"path": file_path},
 
174 EdgeType.IMPORTS,
175 NodeLabel.Import, {"name": module, "file_path": file_path},
 
176 )
177 stats["edges"] += 1
178
179 def _extract_calls(self, fn_node, source: bytes, file_path: str,
180 fn_name: str, fn_label: str,
181 store: GraphStore, stats: dict) -> None:
 
 
 
 
 
 
 
182 def walk(node):
183 if node.type == "call_expression":
184 func = node.child_by_field_name("function")
185 if func:
186 callee = _node_text(func, source).split(".")[-1]
187 store.create_edge(
188 fn_label, {"name": fn_name, "file_path": file_path},
 
189 EdgeType.CALLS,
190 NodeLabel.Function, {"name": callee, "file_path": file_path},
 
191 )
192 stats["edges"] += 1
193 for child in node.children:
194 walk(child)
195
196 body = fn_node.child_by_field_name("body")
197 if body:
198 walk(body)
199
--- navegador/ingestion/go.py
+++ navegador/ingestion/go.py
@@ -15,46 +15,50 @@
15
16 def _get_go_language():
17 try:
18 import tree_sitter_go as tsgo # type: ignore[import]
19 from tree_sitter import Language
20
21 return Language(tsgo.language())
22 except ImportError as e:
23 raise ImportError("Install tree-sitter-go: pip install tree-sitter-go") from e
24
25
26 def _node_text(node, source: bytes) -> str:
27 return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
28
29
30 class GoParser(LanguageParser):
31 """Parses Go source files into the navegador graph."""
32
33 def __init__(self) -> None:
34 from tree_sitter import Parser # type: ignore[import]
35
36 self._parser = Parser(_get_go_language())
37
38 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
39 source = path.read_bytes()
40 tree = self._parser.parse(source)
41 rel_path = str(path.relative_to(repo_root))
42
43 store.create_node(
44 NodeLabel.File,
45 {
46 "name": path.name,
47 "path": rel_path,
48 "language": "go",
49 "line_count": source.count(b"\n"),
50 },
51 )
52
53 stats = {"functions": 0, "classes": 0, "edges": 0}
54 self._walk(tree.root_node, source, rel_path, store, stats)
55 return stats
56
57 # ── AST walker ────────────────────────────────────────────────────────────
58
59 def _walk(self, node, source: bytes, file_path: str, store: GraphStore, stats: dict) -> None:
 
60 if node.type == "function_declaration":
61 self._handle_function(node, source, file_path, store, stats, receiver=None)
62 return
63 if node.type == "method_declaration":
64 self._handle_method(node, source, file_path, store, stats)
@@ -68,59 +72,70 @@
72 for child in node.children:
73 self._walk(child, source, file_path, store, stats)
74
75 # ── Handlers ──────────────────────────────────────────────────────────────
76
77 def _handle_function(
78 self,
79 node,
80 source: bytes,
81 file_path: str,
82 store: GraphStore,
83 stats: dict,
84 receiver: str | None,
85 ) -> None:
86 name_node = node.child_by_field_name("name")
87 if not name_node:
88 return
89 name = _node_text(name_node, source)
90 label = NodeLabel.Method if receiver else NodeLabel.Function
91
92 store.create_node(
93 label,
94 {
95 "name": name,
96 "file_path": file_path,
97 "line_start": node.start_point[0] + 1,
98 "line_end": node.end_point[0] + 1,
99 "docstring": "",
100 "class_name": receiver or "",
101 },
102 )
103
104 container_label = NodeLabel.Class if receiver else NodeLabel.File
105 container_key = (
106 {"name": receiver, "file_path": file_path} if receiver else {"path": file_path}
 
107 )
108 store.create_edge(
109 container_label,
110 container_key,
111 EdgeType.CONTAINS,
112 label,
113 {"name": name, "file_path": file_path},
114 )
115 stats["functions"] += 1
116 stats["edges"] += 1
117
118 self._extract_calls(node, source, file_path, name, label, store, stats)
119
120 def _handle_method(
121 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
122 ) -> None:
123 receiver_type = ""
124 recv_node = node.child_by_field_name("receiver")
125 if recv_node:
126 for child in recv_node.children:
127 if child.type == "parameter_declaration":
128 for c in child.children:
129 if c.type in ("type_identifier", "pointer_type"):
130 receiver_type = _node_text(c, source).lstrip("*").strip()
131 break
132 self._handle_function(node, source, file_path, store, stats, receiver=receiver_type or None)
 
133
134 def _handle_type(
135 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
136 ) -> None:
137 for child in node.children:
138 if child.type != "type_spec":
139 continue
140 name_node = child.child_by_field_name("name")
141 type_node = child.child_by_field_name("type")
@@ -128,71 +143,91 @@
143 continue
144 if type_node.type not in ("struct_type", "interface_type"):
145 continue
146 name = _node_text(name_node, source)
147 kind = "struct" if type_node.type == "struct_type" else "interface"
148 store.create_node(
149 NodeLabel.Class,
150 {
151 "name": name,
152 "file_path": file_path,
153 "line_start": node.start_point[0] + 1,
154 "line_end": node.end_point[0] + 1,
155 "docstring": kind,
156 },
157 )
158 store.create_edge(
159 NodeLabel.File,
160 {"path": file_path},
161 EdgeType.CONTAINS,
162 NodeLabel.Class,
163 {"name": name, "file_path": file_path},
164 )
165 stats["classes"] += 1
166 stats["edges"] += 1
167
168 def _handle_import(
169 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
170 ) -> None:
171 line_start = node.start_point[0] + 1
172 for child in node.children:
173 if child.type == "import_spec":
174 self._ingest_import_spec(child, source, file_path, line_start, store, stats)
175 elif child.type == "import_spec_list":
176 for spec in child.children:
177 if spec.type == "import_spec":
178 self._ingest_import_spec(spec, source, file_path, line_start, store, stats)
 
179
180 def _ingest_import_spec(
181 self, spec, source: bytes, file_path: str, line_start: int, store: GraphStore, stats: dict
182 ) -> None:
183 path_node = spec.child_by_field_name("path")
184 if not path_node:
185 return
186 module = _node_text(path_node, source).strip('"')
187 store.create_node(
188 NodeLabel.Import,
189 {
190 "name": module,
191 "file_path": file_path,
192 "line_start": line_start,
193 "module": module,
194 },
195 )
196 store.create_edge(
197 NodeLabel.File,
198 {"path": file_path},
199 EdgeType.IMPORTS,
200 NodeLabel.Import,
201 {"name": module, "file_path": file_path},
202 )
203 stats["edges"] += 1
204
205 def _extract_calls(
206 self,
207 fn_node,
208 source: bytes,
209 file_path: str,
210 fn_name: str,
211 fn_label: str,
212 store: GraphStore,
213 stats: dict,
214 ) -> None:
215 def walk(node):
216 if node.type == "call_expression":
217 func = node.child_by_field_name("function")
218 if func:
219 callee = _node_text(func, source).split(".")[-1]
220 store.create_edge(
221 fn_label,
222 {"name": fn_name, "file_path": file_path},
223 EdgeType.CALLS,
224 NodeLabel.Function,
225 {"name": callee, "file_path": file_path},
226 )
227 stats["edges"] += 1
228 for child in node.children:
229 walk(child)
230
231 body = fn_node.child_by_field_name("body")
232 if body:
233 walk(body)
234
--- navegador/ingestion/java.py
+++ navegador/ingestion/java.py
@@ -15,17 +15,18 @@
1515
1616
def _get_java_language():
1717
try:
1818
import tree_sitter_java as tsjava # type: ignore[import]
1919
from tree_sitter import Language
20
+
2021
return Language(tsjava.language())
2122
except ImportError as e:
2223
raise ImportError("Install tree-sitter-java: pip install tree-sitter-java") from e
2324
2425
2526
def _node_text(node, source: bytes) -> str:
26
- return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
27
+ return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
2728
2829
2930
def _javadoc(node, source: bytes) -> str:
3031
"""Return the Javadoc (/** ... */) comment preceding a node, if any."""
3132
parent = node.parent
@@ -46,32 +47,43 @@
4647
class JavaParser(LanguageParser):
4748
"""Parses Java source files into the navegador graph."""
4849
4950
def __init__(self) -> None:
5051
from tree_sitter import Parser # type: ignore[import]
52
+
5153
self._parser = Parser(_get_java_language())
5254
5355
def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
5456
source = path.read_bytes()
5557
tree = self._parser.parse(source)
5658
rel_path = str(path.relative_to(repo_root))
5759
58
- store.create_node(NodeLabel.File, {
59
- "name": path.name,
60
- "path": rel_path,
61
- "language": "java",
62
- "line_count": source.count(b"\n"),
63
- })
60
+ store.create_node(
61
+ NodeLabel.File,
62
+ {
63
+ "name": path.name,
64
+ "path": rel_path,
65
+ "language": "java",
66
+ "line_count": source.count(b"\n"),
67
+ },
68
+ )
6469
6570
stats = {"functions": 0, "classes": 0, "edges": 0}
6671
self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
6772
return stats
6873
6974
# ── AST walker ────────────────────────────────────────────────────────────
7075
71
- def _walk(self, node, source: bytes, file_path: str,
72
- store: GraphStore, stats: dict, class_name: str | None) -> None:
76
+ def _walk(
77
+ self,
78
+ node,
79
+ source: bytes,
80
+ file_path: str,
81
+ store: GraphStore,
82
+ stats: dict,
83
+ class_name: str | None,
84
+ ) -> None:
7385
if node.type in ("class_declaration", "record_declaration"):
7486
self._handle_class(node, source, file_path, store, stats)
7587
return
7688
if node.type == "interface_declaration":
7789
self._handle_interface(node, source, file_path, store, stats)
@@ -82,29 +94,35 @@
8294
for child in node.children:
8395
self._walk(child, source, file_path, store, stats, class_name)
8496
8597
# ── Handlers ──────────────────────────────────────────────────────────────
8698
87
- def _handle_class(self, node, source: bytes, file_path: str,
88
- store: GraphStore, stats: dict) -> None:
99
+ def _handle_class(
100
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
101
+ ) -> None:
89102
name_node = node.child_by_field_name("name")
90103
if not name_node:
91104
return
92105
name = _node_text(name_node, source)
93106
docstring = _javadoc(node, source)
94107
95
- store.create_node(NodeLabel.Class, {
96
- "name": name,
97
- "file_path": file_path,
98
- "line_start": node.start_point[0] + 1,
99
- "line_end": node.end_point[0] + 1,
100
- "docstring": docstring,
101
- })
108
+ store.create_node(
109
+ NodeLabel.Class,
110
+ {
111
+ "name": name,
112
+ "file_path": file_path,
113
+ "line_start": node.start_point[0] + 1,
114
+ "line_end": node.end_point[0] + 1,
115
+ "docstring": docstring,
116
+ },
117
+ )
102118
store.create_edge(
103
- NodeLabel.File, {"path": file_path},
119
+ NodeLabel.File,
120
+ {"path": file_path},
104121
EdgeType.CONTAINS,
105
- NodeLabel.Class, {"name": name, "file_path": file_path},
122
+ NodeLabel.Class,
123
+ {"name": name, "file_path": file_path},
106124
)
107125
stats["classes"] += 1
108126
stats["edges"] += 1
109127
110128
# Superclass → INHERITS edge
@@ -112,135 +130,169 @@
112130
if superclass:
113131
for child in superclass.children:
114132
if child.type == "type_identifier":
115133
parent_name = _node_text(child, source)
116134
store.create_edge(
117
- NodeLabel.Class, {"name": name, "file_path": file_path},
135
+ NodeLabel.Class,
136
+ {"name": name, "file_path": file_path},
118137
EdgeType.INHERITS,
119
- NodeLabel.Class, {"name": parent_name, "file_path": file_path},
138
+ NodeLabel.Class,
139
+ {"name": parent_name, "file_path": file_path},
120140
)
121141
stats["edges"] += 1
122142
break
123143
124144
# Walk class body for methods and constructors
125145
body = node.child_by_field_name("body")
126146
if body:
127147
for child in body.children:
128148
if child.type in ("method_declaration", "constructor_declaration"):
129
- self._handle_method(child, source, file_path, store, stats,
130
- class_name=name)
131
- elif child.type in ("class_declaration", "record_declaration",
132
- "interface_declaration"):
149
+ self._handle_method(child, source, file_path, store, stats, class_name=name)
150
+ elif child.type in (
151
+ "class_declaration",
152
+ "record_declaration",
153
+ "interface_declaration",
154
+ ):
133155
# Nested class — register but don't recurse into methods
134156
inner_name_node = child.child_by_field_name("name")
135157
if inner_name_node:
136158
inner_name = _node_text(inner_name_node, source)
137
- store.create_node(NodeLabel.Class, {
138
- "name": inner_name,
139
- "file_path": file_path,
140
- "line_start": child.start_point[0] + 1,
141
- "line_end": child.end_point[0] + 1,
142
- "docstring": "",
143
- })
159
+ store.create_node(
160
+ NodeLabel.Class,
161
+ {
162
+ "name": inner_name,
163
+ "file_path": file_path,
164
+ "line_start": child.start_point[0] + 1,
165
+ "line_end": child.end_point[0] + 1,
166
+ "docstring": "",
167
+ },
168
+ )
144169
store.create_edge(
145
- NodeLabel.Class, {"name": name, "file_path": file_path},
170
+ NodeLabel.Class,
171
+ {"name": name, "file_path": file_path},
146172
EdgeType.CONTAINS,
147
- NodeLabel.Class, {"name": inner_name, "file_path": file_path},
173
+ NodeLabel.Class,
174
+ {"name": inner_name, "file_path": file_path},
148175
)
149176
stats["classes"] += 1
150177
stats["edges"] += 1
151178
152
- def _handle_interface(self, node, source: bytes, file_path: str,
153
- store: GraphStore, stats: dict) -> None:
179
+ def _handle_interface(
180
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
181
+ ) -> None:
154182
name_node = node.child_by_field_name("name")
155183
if not name_node:
156184
return
157185
name = _node_text(name_node, source)
158186
docstring = _javadoc(node, source)
159187
160
- store.create_node(NodeLabel.Class, {
161
- "name": name,
162
- "file_path": file_path,
163
- "line_start": node.start_point[0] + 1,
164
- "line_end": node.end_point[0] + 1,
165
- "docstring": f"interface: {docstring}".strip(": "),
166
- })
188
+ store.create_node(
189
+ NodeLabel.Class,
190
+ {
191
+ "name": name,
192
+ "file_path": file_path,
193
+ "line_start": node.start_point[0] + 1,
194
+ "line_end": node.end_point[0] + 1,
195
+ "docstring": f"interface: {docstring}".strip(": "),
196
+ },
197
+ )
167198
store.create_edge(
168
- NodeLabel.File, {"path": file_path},
199
+ NodeLabel.File,
200
+ {"path": file_path},
169201
EdgeType.CONTAINS,
170
- NodeLabel.Class, {"name": name, "file_path": file_path},
202
+ NodeLabel.Class,
203
+ {"name": name, "file_path": file_path},
171204
)
172205
stats["classes"] += 1
173206
stats["edges"] += 1
174207
175208
# Walk interface body for method signatures
176209
body = node.child_by_field_name("body")
177210
if body:
178211
for child in body.children:
179212
if child.type == "method_declaration":
180
- self._handle_method(child, source, file_path, store, stats,
181
- class_name=name)
213
+ self._handle_method(child, source, file_path, store, stats, class_name=name)
182214
183
- def _handle_method(self, node, source: bytes, file_path: str,
184
- store: GraphStore, stats: dict, class_name: str) -> None:
215
+ def _handle_method(
216
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict, class_name: str
217
+ ) -> None:
185218
name_node = node.child_by_field_name("name")
186219
if not name_node:
187220
return
188221
name = _node_text(name_node, source)
189222
docstring = _javadoc(node, source)
190223
191
- store.create_node(NodeLabel.Method, {
192
- "name": name,
193
- "file_path": file_path,
194
- "line_start": node.start_point[0] + 1,
195
- "line_end": node.end_point[0] + 1,
196
- "docstring": docstring,
197
- "class_name": class_name,
198
- })
224
+ store.create_node(
225
+ NodeLabel.Method,
226
+ {
227
+ "name": name,
228
+ "file_path": file_path,
229
+ "line_start": node.start_point[0] + 1,
230
+ "line_end": node.end_point[0] + 1,
231
+ "docstring": docstring,
232
+ "class_name": class_name,
233
+ },
234
+ )
199235
store.create_edge(
200
- NodeLabel.Class, {"name": class_name, "file_path": file_path},
236
+ NodeLabel.Class,
237
+ {"name": class_name, "file_path": file_path},
201238
EdgeType.CONTAINS,
202
- NodeLabel.Method, {"name": name, "file_path": file_path},
239
+ NodeLabel.Method,
240
+ {"name": name, "file_path": file_path},
203241
)
204242
stats["functions"] += 1
205243
stats["edges"] += 1
206244
207245
self._extract_calls(node, source, file_path, name, store, stats)
208246
209
- def _handle_import(self, node, source: bytes, file_path: str,
210
- store: GraphStore, stats: dict) -> None:
247
+ def _handle_import(
248
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
249
+ ) -> None:
211250
# import java.util.List; → strip keyword + semicolon
212251
raw = _node_text(node, source)
213
- module = (raw.removeprefix("import").removeprefix(" static")
214
- .removesuffix(";").strip())
215
- store.create_node(NodeLabel.Import, {
216
- "name": module,
217
- "file_path": file_path,
218
- "line_start": node.start_point[0] + 1,
219
- "module": module,
220
- })
252
+ module = raw.removeprefix("import").removeprefix(" static").removesuffix(";").strip()
253
+ store.create_node(
254
+ NodeLabel.Import,
255
+ {
256
+ "name": module,
257
+ "file_path": file_path,
258
+ "line_start": node.start_point[0] + 1,
259
+ "module": module,
260
+ },
261
+ )
221262
store.create_edge(
222
- NodeLabel.File, {"path": file_path},
263
+ NodeLabel.File,
264
+ {"path": file_path},
223265
EdgeType.IMPORTS,
224
- NodeLabel.Import, {"name": module, "file_path": file_path},
266
+ NodeLabel.Import,
267
+ {"name": module, "file_path": file_path},
225268
)
226269
stats["edges"] += 1
227270
228
- def _extract_calls(self, method_node, source: bytes, file_path: str,
229
- method_name: str, store: GraphStore, stats: dict) -> None:
271
+ def _extract_calls(
272
+ self,
273
+ method_node,
274
+ source: bytes,
275
+ file_path: str,
276
+ method_name: str,
277
+ store: GraphStore,
278
+ stats: dict,
279
+ ) -> None:
230280
def walk(node):
231281
if node.type == "method_invocation":
232282
name_node = node.child_by_field_name("name")
233283
if name_node:
234284
callee = _node_text(name_node, source)
235285
store.create_edge(
236
- NodeLabel.Method, {"name": method_name, "file_path": file_path},
286
+ NodeLabel.Method,
287
+ {"name": method_name, "file_path": file_path},
237288
EdgeType.CALLS,
238
- NodeLabel.Function, {"name": callee, "file_path": file_path},
289
+ NodeLabel.Function,
290
+ {"name": callee, "file_path": file_path},
239291
)
240292
stats["edges"] += 1
241293
for child in node.children:
242294
walk(child)
243295
244296
body = method_node.child_by_field_name("body")
245297
if body:
246298
walk(body)
247299
--- navegador/ingestion/java.py
+++ navegador/ingestion/java.py
@@ -15,17 +15,18 @@
15
16 def _get_java_language():
17 try:
18 import tree_sitter_java as tsjava # type: ignore[import]
19 from tree_sitter import Language
 
20 return Language(tsjava.language())
21 except ImportError as e:
22 raise ImportError("Install tree-sitter-java: pip install tree-sitter-java") from e
23
24
25 def _node_text(node, source: bytes) -> str:
26 return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
27
28
29 def _javadoc(node, source: bytes) -> str:
30 """Return the Javadoc (/** ... */) comment preceding a node, if any."""
31 parent = node.parent
@@ -46,32 +47,43 @@
46 class JavaParser(LanguageParser):
47 """Parses Java source files into the navegador graph."""
48
49 def __init__(self) -> None:
50 from tree_sitter import Parser # type: ignore[import]
 
51 self._parser = Parser(_get_java_language())
52
53 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
54 source = path.read_bytes()
55 tree = self._parser.parse(source)
56 rel_path = str(path.relative_to(repo_root))
57
58 store.create_node(NodeLabel.File, {
59 "name": path.name,
60 "path": rel_path,
61 "language": "java",
62 "line_count": source.count(b"\n"),
63 })
 
 
 
64
65 stats = {"functions": 0, "classes": 0, "edges": 0}
66 self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
67 return stats
68
69 # ── AST walker ────────────────────────────────────────────────────────────
70
71 def _walk(self, node, source: bytes, file_path: str,
72 store: GraphStore, stats: dict, class_name: str | None) -> None:
 
 
 
 
 
 
 
73 if node.type in ("class_declaration", "record_declaration"):
74 self._handle_class(node, source, file_path, store, stats)
75 return
76 if node.type == "interface_declaration":
77 self._handle_interface(node, source, file_path, store, stats)
@@ -82,29 +94,35 @@
82 for child in node.children:
83 self._walk(child, source, file_path, store, stats, class_name)
84
85 # ── Handlers ──────────────────────────────────────────────────────────────
86
87 def _handle_class(self, node, source: bytes, file_path: str,
88 store: GraphStore, stats: dict) -> None:
 
89 name_node = node.child_by_field_name("name")
90 if not name_node:
91 return
92 name = _node_text(name_node, source)
93 docstring = _javadoc(node, source)
94
95 store.create_node(NodeLabel.Class, {
96 "name": name,
97 "file_path": file_path,
98 "line_start": node.start_point[0] + 1,
99 "line_end": node.end_point[0] + 1,
100 "docstring": docstring,
101 })
 
 
 
102 store.create_edge(
103 NodeLabel.File, {"path": file_path},
 
104 EdgeType.CONTAINS,
105 NodeLabel.Class, {"name": name, "file_path": file_path},
 
106 )
107 stats["classes"] += 1
108 stats["edges"] += 1
109
110 # Superclass → INHERITS edge
@@ -112,135 +130,169 @@
112 if superclass:
113 for child in superclass.children:
114 if child.type == "type_identifier":
115 parent_name = _node_text(child, source)
116 store.create_edge(
117 NodeLabel.Class, {"name": name, "file_path": file_path},
 
118 EdgeType.INHERITS,
119 NodeLabel.Class, {"name": parent_name, "file_path": file_path},
 
120 )
121 stats["edges"] += 1
122 break
123
124 # Walk class body for methods and constructors
125 body = node.child_by_field_name("body")
126 if body:
127 for child in body.children:
128 if child.type in ("method_declaration", "constructor_declaration"):
129 self._handle_method(child, source, file_path, store, stats,
130 class_name=name)
131 elif child.type in ("class_declaration", "record_declaration",
132 "interface_declaration"):
 
 
133 # Nested class — register but don't recurse into methods
134 inner_name_node = child.child_by_field_name("name")
135 if inner_name_node:
136 inner_name = _node_text(inner_name_node, source)
137 store.create_node(NodeLabel.Class, {
138 "name": inner_name,
139 "file_path": file_path,
140 "line_start": child.start_point[0] + 1,
141 "line_end": child.end_point[0] + 1,
142 "docstring": "",
143 })
 
 
 
144 store.create_edge(
145 NodeLabel.Class, {"name": name, "file_path": file_path},
 
146 EdgeType.CONTAINS,
147 NodeLabel.Class, {"name": inner_name, "file_path": file_path},
 
148 )
149 stats["classes"] += 1
150 stats["edges"] += 1
151
152 def _handle_interface(self, node, source: bytes, file_path: str,
153 store: GraphStore, stats: dict) -> None:
 
154 name_node = node.child_by_field_name("name")
155 if not name_node:
156 return
157 name = _node_text(name_node, source)
158 docstring = _javadoc(node, source)
159
160 store.create_node(NodeLabel.Class, {
161 "name": name,
162 "file_path": file_path,
163 "line_start": node.start_point[0] + 1,
164 "line_end": node.end_point[0] + 1,
165 "docstring": f"interface: {docstring}".strip(": "),
166 })
 
 
 
167 store.create_edge(
168 NodeLabel.File, {"path": file_path},
 
169 EdgeType.CONTAINS,
170 NodeLabel.Class, {"name": name, "file_path": file_path},
 
171 )
172 stats["classes"] += 1
173 stats["edges"] += 1
174
175 # Walk interface body for method signatures
176 body = node.child_by_field_name("body")
177 if body:
178 for child in body.children:
179 if child.type == "method_declaration":
180 self._handle_method(child, source, file_path, store, stats,
181 class_name=name)
182
183 def _handle_method(self, node, source: bytes, file_path: str,
184 store: GraphStore, stats: dict, class_name: str) -> None:
 
185 name_node = node.child_by_field_name("name")
186 if not name_node:
187 return
188 name = _node_text(name_node, source)
189 docstring = _javadoc(node, source)
190
191 store.create_node(NodeLabel.Method, {
192 "name": name,
193 "file_path": file_path,
194 "line_start": node.start_point[0] + 1,
195 "line_end": node.end_point[0] + 1,
196 "docstring": docstring,
197 "class_name": class_name,
198 })
 
 
 
199 store.create_edge(
200 NodeLabel.Class, {"name": class_name, "file_path": file_path},
 
201 EdgeType.CONTAINS,
202 NodeLabel.Method, {"name": name, "file_path": file_path},
 
203 )
204 stats["functions"] += 1
205 stats["edges"] += 1
206
207 self._extract_calls(node, source, file_path, name, store, stats)
208
209 def _handle_import(self, node, source: bytes, file_path: str,
210 store: GraphStore, stats: dict) -> None:
 
211 # import java.util.List; → strip keyword + semicolon
212 raw = _node_text(node, source)
213 module = (raw.removeprefix("import").removeprefix(" static")
214 .removesuffix(";").strip())
215 store.create_node(NodeLabel.Import, {
216 "name": module,
217 "file_path": file_path,
218 "line_start": node.start_point[0] + 1,
219 "module": module,
220 })
 
 
221 store.create_edge(
222 NodeLabel.File, {"path": file_path},
 
223 EdgeType.IMPORTS,
224 NodeLabel.Import, {"name": module, "file_path": file_path},
 
225 )
226 stats["edges"] += 1
227
228 def _extract_calls(self, method_node, source: bytes, file_path: str,
229 method_name: str, store: GraphStore, stats: dict) -> None:
 
 
 
 
 
 
 
230 def walk(node):
231 if node.type == "method_invocation":
232 name_node = node.child_by_field_name("name")
233 if name_node:
234 callee = _node_text(name_node, source)
235 store.create_edge(
236 NodeLabel.Method, {"name": method_name, "file_path": file_path},
 
237 EdgeType.CALLS,
238 NodeLabel.Function, {"name": callee, "file_path": file_path},
 
239 )
240 stats["edges"] += 1
241 for child in node.children:
242 walk(child)
243
244 body = method_node.child_by_field_name("body")
245 if body:
246 walk(body)
247
--- navegador/ingestion/java.py
+++ navegador/ingestion/java.py
@@ -15,17 +15,18 @@
15
16 def _get_java_language():
17 try:
18 import tree_sitter_java as tsjava # type: ignore[import]
19 from tree_sitter import Language
20
21 return Language(tsjava.language())
22 except ImportError as e:
23 raise ImportError("Install tree-sitter-java: pip install tree-sitter-java") from e
24
25
26 def _node_text(node, source: bytes) -> str:
27 return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
28
29
30 def _javadoc(node, source: bytes) -> str:
31 """Return the Javadoc (/** ... */) comment preceding a node, if any."""
32 parent = node.parent
@@ -46,32 +47,43 @@
47 class JavaParser(LanguageParser):
48 """Parses Java source files into the navegador graph."""
49
50 def __init__(self) -> None:
51 from tree_sitter import Parser # type: ignore[import]
52
53 self._parser = Parser(_get_java_language())
54
55 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
56 source = path.read_bytes()
57 tree = self._parser.parse(source)
58 rel_path = str(path.relative_to(repo_root))
59
60 store.create_node(
61 NodeLabel.File,
62 {
63 "name": path.name,
64 "path": rel_path,
65 "language": "java",
66 "line_count": source.count(b"\n"),
67 },
68 )
69
70 stats = {"functions": 0, "classes": 0, "edges": 0}
71 self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
72 return stats
73
74 # ── AST walker ────────────────────────────────────────────────────────────
75
76 def _walk(
77 self,
78 node,
79 source: bytes,
80 file_path: str,
81 store: GraphStore,
82 stats: dict,
83 class_name: str | None,
84 ) -> None:
85 if node.type in ("class_declaration", "record_declaration"):
86 self._handle_class(node, source, file_path, store, stats)
87 return
88 if node.type == "interface_declaration":
89 self._handle_interface(node, source, file_path, store, stats)
@@ -82,29 +94,35 @@
94 for child in node.children:
95 self._walk(child, source, file_path, store, stats, class_name)
96
97 # ── Handlers ──────────────────────────────────────────────────────────────
98
99 def _handle_class(
100 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
101 ) -> None:
102 name_node = node.child_by_field_name("name")
103 if not name_node:
104 return
105 name = _node_text(name_node, source)
106 docstring = _javadoc(node, source)
107
108 store.create_node(
109 NodeLabel.Class,
110 {
111 "name": name,
112 "file_path": file_path,
113 "line_start": node.start_point[0] + 1,
114 "line_end": node.end_point[0] + 1,
115 "docstring": docstring,
116 },
117 )
118 store.create_edge(
119 NodeLabel.File,
120 {"path": file_path},
121 EdgeType.CONTAINS,
122 NodeLabel.Class,
123 {"name": name, "file_path": file_path},
124 )
125 stats["classes"] += 1
126 stats["edges"] += 1
127
128 # Superclass → INHERITS edge
@@ -112,135 +130,169 @@
130 if superclass:
131 for child in superclass.children:
132 if child.type == "type_identifier":
133 parent_name = _node_text(child, source)
134 store.create_edge(
135 NodeLabel.Class,
136 {"name": name, "file_path": file_path},
137 EdgeType.INHERITS,
138 NodeLabel.Class,
139 {"name": parent_name, "file_path": file_path},
140 )
141 stats["edges"] += 1
142 break
143
144 # Walk class body for methods and constructors
145 body = node.child_by_field_name("body")
146 if body:
147 for child in body.children:
148 if child.type in ("method_declaration", "constructor_declaration"):
149 self._handle_method(child, source, file_path, store, stats, class_name=name)
150 elif child.type in (
151 "class_declaration",
152 "record_declaration",
153 "interface_declaration",
154 ):
155 # Nested class — register but don't recurse into methods
156 inner_name_node = child.child_by_field_name("name")
157 if inner_name_node:
158 inner_name = _node_text(inner_name_node, source)
159 store.create_node(
160 NodeLabel.Class,
161 {
162 "name": inner_name,
163 "file_path": file_path,
164 "line_start": child.start_point[0] + 1,
165 "line_end": child.end_point[0] + 1,
166 "docstring": "",
167 },
168 )
169 store.create_edge(
170 NodeLabel.Class,
171 {"name": name, "file_path": file_path},
172 EdgeType.CONTAINS,
173 NodeLabel.Class,
174 {"name": inner_name, "file_path": file_path},
175 )
176 stats["classes"] += 1
177 stats["edges"] += 1
178
179 def _handle_interface(
180 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
181 ) -> None:
182 name_node = node.child_by_field_name("name")
183 if not name_node:
184 return
185 name = _node_text(name_node, source)
186 docstring = _javadoc(node, source)
187
188 store.create_node(
189 NodeLabel.Class,
190 {
191 "name": name,
192 "file_path": file_path,
193 "line_start": node.start_point[0] + 1,
194 "line_end": node.end_point[0] + 1,
195 "docstring": f"interface: {docstring}".strip(": "),
196 },
197 )
198 store.create_edge(
199 NodeLabel.File,
200 {"path": file_path},
201 EdgeType.CONTAINS,
202 NodeLabel.Class,
203 {"name": name, "file_path": file_path},
204 )
205 stats["classes"] += 1
206 stats["edges"] += 1
207
208 # Walk interface body for method signatures
209 body = node.child_by_field_name("body")
210 if body:
211 for child in body.children:
212 if child.type == "method_declaration":
213 self._handle_method(child, source, file_path, store, stats, class_name=name)
 
214
215 def _handle_method(
216 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict, class_name: str
217 ) -> None:
218 name_node = node.child_by_field_name("name")
219 if not name_node:
220 return
221 name = _node_text(name_node, source)
222 docstring = _javadoc(node, source)
223
224 store.create_node(
225 NodeLabel.Method,
226 {
227 "name": name,
228 "file_path": file_path,
229 "line_start": node.start_point[0] + 1,
230 "line_end": node.end_point[0] + 1,
231 "docstring": docstring,
232 "class_name": class_name,
233 },
234 )
235 store.create_edge(
236 NodeLabel.Class,
237 {"name": class_name, "file_path": file_path},
238 EdgeType.CONTAINS,
239 NodeLabel.Method,
240 {"name": name, "file_path": file_path},
241 )
242 stats["functions"] += 1
243 stats["edges"] += 1
244
245 self._extract_calls(node, source, file_path, name, store, stats)
246
247 def _handle_import(
248 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
249 ) -> None:
250 # import java.util.List; → strip keyword + semicolon
251 raw = _node_text(node, source)
252 module = raw.removeprefix("import").removeprefix(" static").removesuffix(";").strip()
253 store.create_node(
254 NodeLabel.Import,
255 {
256 "name": module,
257 "file_path": file_path,
258 "line_start": node.start_point[0] + 1,
259 "module": module,
260 },
261 )
262 store.create_edge(
263 NodeLabel.File,
264 {"path": file_path},
265 EdgeType.IMPORTS,
266 NodeLabel.Import,
267 {"name": module, "file_path": file_path},
268 )
269 stats["edges"] += 1
270
271 def _extract_calls(
272 self,
273 method_node,
274 source: bytes,
275 file_path: str,
276 method_name: str,
277 store: GraphStore,
278 stats: dict,
279 ) -> None:
280 def walk(node):
281 if node.type == "method_invocation":
282 name_node = node.child_by_field_name("name")
283 if name_node:
284 callee = _node_text(name_node, source)
285 store.create_edge(
286 NodeLabel.Method,
287 {"name": method_name, "file_path": file_path},
288 EdgeType.CALLS,
289 NodeLabel.Function,
290 {"name": callee, "file_path": file_path},
291 )
292 stats["edges"] += 1
293 for child in node.children:
294 walk(child)
295
296 body = method_node.child_by_field_name("body")
297 if body:
298 walk(body)
299
--- navegador/ingestion/knowledge.py
+++ navegador/ingestion/knowledge.py
@@ -35,14 +35,17 @@
3535
self.store = store
3636
3737
# ── Domains ───────────────────────────────────────────────────────────────
3838
3939
def add_domain(self, name: str, description: str = "") -> None:
40
- self.store.create_node(NodeLabel.Domain, {
41
- "name": name,
42
- "description": description,
43
- })
40
+ self.store.create_node(
41
+ NodeLabel.Domain,
42
+ {
43
+ "name": name,
44
+ "description": description,
45
+ },
46
+ )
4447
logger.info("Domain: %s", name)
4548
4649
# ── Concepts ──────────────────────────────────────────────────────────────
4750
4851
def add_concept(
@@ -53,29 +56,34 @@
5356
status: str = "",
5457
rules: str = "",
5558
examples: str = "",
5659
wiki_refs: str = "",
5760
) -> None:
58
- self.store.create_node(NodeLabel.Concept, {
59
- "name": name,
60
- "description": description,
61
- "domain": domain,
62
- "status": status,
63
- "rules": rules,
64
- "examples": examples,
65
- "wiki_refs": wiki_refs,
66
- })
61
+ self.store.create_node(
62
+ NodeLabel.Concept,
63
+ {
64
+ "name": name,
65
+ "description": description,
66
+ "domain": domain,
67
+ "status": status,
68
+ "rules": rules,
69
+ "examples": examples,
70
+ "wiki_refs": wiki_refs,
71
+ },
72
+ )
6773
if domain:
6874
self._link_to_domain(name, NodeLabel.Concept, domain)
6975
logger.info("Concept: %s", name)
7076
7177
def relate_concepts(self, a: str, b: str) -> None:
7278
"""Mark two concepts as related (bidirectional intent)."""
7379
self.store.create_edge(
74
- NodeLabel.Concept, {"name": a},
80
+ NodeLabel.Concept,
81
+ {"name": a},
7582
EdgeType.RELATED_TO,
76
- NodeLabel.Concept, {"name": b},
83
+ NodeLabel.Concept,
84
+ {"name": b},
7785
)
7886
7987
# ── Rules ─────────────────────────────────────────────────────────────────
8088
8189
def add_rule(
@@ -85,27 +93,32 @@
8593
domain: str = "",
8694
severity: str = "info",
8795
rationale: str = "",
8896
examples: str = "",
8997
) -> None:
90
- self.store.create_node(NodeLabel.Rule, {
91
- "name": name,
92
- "description": description,
93
- "domain": domain,
94
- "severity": severity,
95
- "rationale": rationale,
96
- "examples": examples,
97
- })
98
+ self.store.create_node(
99
+ NodeLabel.Rule,
100
+ {
101
+ "name": name,
102
+ "description": description,
103
+ "domain": domain,
104
+ "severity": severity,
105
+ "rationale": rationale,
106
+ "examples": examples,
107
+ },
108
+ )
98109
if domain:
99110
self._link_to_domain(name, NodeLabel.Rule, domain)
100111
logger.info("Rule: %s", name)
101112
102113
def rule_governs(self, rule_name: str, target_name: str, target_label: NodeLabel) -> None:
103114
self.store.create_edge(
104
- NodeLabel.Rule, {"name": rule_name},
115
+ NodeLabel.Rule,
116
+ {"name": rule_name},
105117
EdgeType.GOVERNS,
106
- target_label, {"name": target_name},
118
+ target_label,
119
+ {"name": target_name},
107120
)
108121
109122
# ── Decisions ─────────────────────────────────────────────────────────────
110123
111124
def add_decision(
@@ -116,19 +129,22 @@
116129
status: str = "accepted",
117130
rationale: str = "",
118131
alternatives: str = "",
119132
date: str = "",
120133
) -> None:
121
- self.store.create_node(NodeLabel.Decision, {
122
- "name": name,
123
- "description": description,
124
- "domain": domain,
125
- "status": status,
126
- "rationale": rationale,
127
- "alternatives": alternatives,
128
- "date": date,
129
- })
134
+ self.store.create_node(
135
+ NodeLabel.Decision,
136
+ {
137
+ "name": name,
138
+ "description": description,
139
+ "domain": domain,
140
+ "status": status,
141
+ "rationale": rationale,
142
+ "alternatives": alternatives,
143
+ "date": date,
144
+ },
145
+ )
130146
if domain:
131147
self._link_to_domain(name, NodeLabel.Decision, domain)
132148
logger.info("Decision: %s", name)
133149
134150
# ── People ────────────────────────────────────────────────────────────────
@@ -138,24 +154,29 @@
138154
name: str,
139155
email: str = "",
140156
role: str = "",
141157
team: str = "",
142158
) -> None:
143
- self.store.create_node(NodeLabel.Person, {
144
- "name": name,
145
- "email": email,
146
- "role": role,
147
- "team": team,
148
- })
159
+ self.store.create_node(
160
+ NodeLabel.Person,
161
+ {
162
+ "name": name,
163
+ "email": email,
164
+ "role": role,
165
+ "team": team,
166
+ },
167
+ )
149168
logger.info("Person: %s", name)
150169
151170
def assign(self, target_name: str, target_label: NodeLabel, person_name: str) -> None:
152171
"""Assign a person as owner of any node."""
153172
self.store.create_edge(
154
- target_label, {"name": target_name},
173
+ target_label,
174
+ {"name": target_name},
155175
EdgeType.ASSIGNED_TO,
156
- NodeLabel.Person, {"name": person_name},
176
+ NodeLabel.Person,
177
+ {"name": person_name},
157178
)
158179
159180
# ── Wiki pages ────────────────────────────────────────────────────────────
160181
161182
def wiki_page(
@@ -164,25 +185,35 @@
164185
url: str = "",
165186
source: str = "github",
166187
content: str = "",
167188
updated_at: str = "",
168189
) -> None:
169
- self.store.create_node(NodeLabel.WikiPage, {
170
- "name": name,
171
- "url": url,
172
- "source": source,
173
- "content": content,
174
- "updated_at": updated_at,
175
- })
190
+ self.store.create_node(
191
+ NodeLabel.WikiPage,
192
+ {
193
+ "name": name,
194
+ "url": url,
195
+ "source": source,
196
+ "content": content,
197
+ "updated_at": updated_at,
198
+ },
199
+ )
176200
logger.info("WikiPage: %s", name)
177201
178
- def wiki_documents(self, wiki_page_name: str, target_name: str,
179
- target_props: dict[str, Any], target_label: NodeLabel) -> None:
202
+ def wiki_documents(
203
+ self,
204
+ wiki_page_name: str,
205
+ target_name: str,
206
+ target_props: dict[str, Any],
207
+ target_label: NodeLabel,
208
+ ) -> None:
180209
self.store.create_edge(
181
- NodeLabel.WikiPage, {"name": wiki_page_name},
210
+ NodeLabel.WikiPage,
211
+ {"name": wiki_page_name},
182212
EdgeType.DOCUMENTS,
183
- target_label, target_props,
213
+ target_label,
214
+ target_props,
184215
)
185216
186217
# ── Code ↔ Knowledge bridges ──────────────────────────────────────────────
187218
188219
def annotate_code(
@@ -197,35 +228,43 @@
197228
code_label should be a string matching a NodeLabel value.
198229
"""
199230
label = NodeLabel(code_label)
200231
if concept:
201232
self.store.create_edge(
202
- NodeLabel.Concept, {"name": concept},
233
+ NodeLabel.Concept,
234
+ {"name": concept},
203235
EdgeType.ANNOTATES,
204
- label, {"name": code_name},
236
+ label,
237
+ {"name": code_name},
205238
)
206239
if rule:
207240
self.store.create_edge(
208
- NodeLabel.Rule, {"name": rule},
241
+ NodeLabel.Rule,
242
+ {"name": rule},
209243
EdgeType.ANNOTATES,
210
- label, {"name": code_name},
244
+ label,
245
+ {"name": code_name},
211246
)
212247
213248
def code_implements(self, code_name: str, code_label: str, concept_name: str) -> None:
214249
"""Mark a function/class as implementing a concept."""
215250
label = NodeLabel(code_label)
216251
self.store.create_edge(
217
- label, {"name": code_name},
252
+ label,
253
+ {"name": code_name},
218254
EdgeType.IMPLEMENTS,
219
- NodeLabel.Concept, {"name": concept_name},
255
+ NodeLabel.Concept,
256
+ {"name": concept_name},
220257
)
221258
222259
# ── Helpers ───────────────────────────────────────────────────────────────
223260
224261
def _link_to_domain(self, name: str, label: NodeLabel, domain: str) -> None:
225262
# Ensure domain node exists
226263
self.store.create_node(NodeLabel.Domain, {"name": domain, "description": ""})
227264
self.store.create_edge(
228
- label, {"name": name},
265
+ label,
266
+ {"name": name},
229267
EdgeType.BELONGS_TO,
230
- NodeLabel.Domain, {"name": domain},
268
+ NodeLabel.Domain,
269
+ {"name": domain},
231270
)
232271
--- navegador/ingestion/knowledge.py
+++ navegador/ingestion/knowledge.py
@@ -35,14 +35,17 @@
35 self.store = store
36
37 # ── Domains ───────────────────────────────────────────────────────────────
38
39 def add_domain(self, name: str, description: str = "") -> None:
40 self.store.create_node(NodeLabel.Domain, {
41 "name": name,
42 "description": description,
43 })
 
 
 
44 logger.info("Domain: %s", name)
45
46 # ── Concepts ──────────────────────────────────────────────────────────────
47
48 def add_concept(
@@ -53,29 +56,34 @@
53 status: str = "",
54 rules: str = "",
55 examples: str = "",
56 wiki_refs: str = "",
57 ) -> None:
58 self.store.create_node(NodeLabel.Concept, {
59 "name": name,
60 "description": description,
61 "domain": domain,
62 "status": status,
63 "rules": rules,
64 "examples": examples,
65 "wiki_refs": wiki_refs,
66 })
 
 
 
67 if domain:
68 self._link_to_domain(name, NodeLabel.Concept, domain)
69 logger.info("Concept: %s", name)
70
71 def relate_concepts(self, a: str, b: str) -> None:
72 """Mark two concepts as related (bidirectional intent)."""
73 self.store.create_edge(
74 NodeLabel.Concept, {"name": a},
 
75 EdgeType.RELATED_TO,
76 NodeLabel.Concept, {"name": b},
 
77 )
78
79 # ── Rules ─────────────────────────────────────────────────────────────────
80
81 def add_rule(
@@ -85,27 +93,32 @@
85 domain: str = "",
86 severity: str = "info",
87 rationale: str = "",
88 examples: str = "",
89 ) -> None:
90 self.store.create_node(NodeLabel.Rule, {
91 "name": name,
92 "description": description,
93 "domain": domain,
94 "severity": severity,
95 "rationale": rationale,
96 "examples": examples,
97 })
 
 
 
98 if domain:
99 self._link_to_domain(name, NodeLabel.Rule, domain)
100 logger.info("Rule: %s", name)
101
102 def rule_governs(self, rule_name: str, target_name: str, target_label: NodeLabel) -> None:
103 self.store.create_edge(
104 NodeLabel.Rule, {"name": rule_name},
 
105 EdgeType.GOVERNS,
106 target_label, {"name": target_name},
 
107 )
108
109 # ── Decisions ─────────────────────────────────────────────────────────────
110
111 def add_decision(
@@ -116,19 +129,22 @@
116 status: str = "accepted",
117 rationale: str = "",
118 alternatives: str = "",
119 date: str = "",
120 ) -> None:
121 self.store.create_node(NodeLabel.Decision, {
122 "name": name,
123 "description": description,
124 "domain": domain,
125 "status": status,
126 "rationale": rationale,
127 "alternatives": alternatives,
128 "date": date,
129 })
 
 
 
130 if domain:
131 self._link_to_domain(name, NodeLabel.Decision, domain)
132 logger.info("Decision: %s", name)
133
134 # ── People ────────────────────────────────────────────────────────────────
@@ -138,24 +154,29 @@
138 name: str,
139 email: str = "",
140 role: str = "",
141 team: str = "",
142 ) -> None:
143 self.store.create_node(NodeLabel.Person, {
144 "name": name,
145 "email": email,
146 "role": role,
147 "team": team,
148 })
 
 
 
149 logger.info("Person: %s", name)
150
151 def assign(self, target_name: str, target_label: NodeLabel, person_name: str) -> None:
152 """Assign a person as owner of any node."""
153 self.store.create_edge(
154 target_label, {"name": target_name},
 
155 EdgeType.ASSIGNED_TO,
156 NodeLabel.Person, {"name": person_name},
 
157 )
158
159 # ── Wiki pages ────────────────────────────────────────────────────────────
160
161 def wiki_page(
@@ -164,25 +185,35 @@
164 url: str = "",
165 source: str = "github",
166 content: str = "",
167 updated_at: str = "",
168 ) -> None:
169 self.store.create_node(NodeLabel.WikiPage, {
170 "name": name,
171 "url": url,
172 "source": source,
173 "content": content,
174 "updated_at": updated_at,
175 })
 
 
 
176 logger.info("WikiPage: %s", name)
177
178 def wiki_documents(self, wiki_page_name: str, target_name: str,
179 target_props: dict[str, Any], target_label: NodeLabel) -> None:
 
 
 
 
 
180 self.store.create_edge(
181 NodeLabel.WikiPage, {"name": wiki_page_name},
 
182 EdgeType.DOCUMENTS,
183 target_label, target_props,
 
184 )
185
186 # ── Code ↔ Knowledge bridges ──────────────────────────────────────────────
187
188 def annotate_code(
@@ -197,35 +228,43 @@
197 code_label should be a string matching a NodeLabel value.
198 """
199 label = NodeLabel(code_label)
200 if concept:
201 self.store.create_edge(
202 NodeLabel.Concept, {"name": concept},
 
203 EdgeType.ANNOTATES,
204 label, {"name": code_name},
 
205 )
206 if rule:
207 self.store.create_edge(
208 NodeLabel.Rule, {"name": rule},
 
209 EdgeType.ANNOTATES,
210 label, {"name": code_name},
 
211 )
212
213 def code_implements(self, code_name: str, code_label: str, concept_name: str) -> None:
214 """Mark a function/class as implementing a concept."""
215 label = NodeLabel(code_label)
216 self.store.create_edge(
217 label, {"name": code_name},
 
218 EdgeType.IMPLEMENTS,
219 NodeLabel.Concept, {"name": concept_name},
 
220 )
221
222 # ── Helpers ───────────────────────────────────────────────────────────────
223
224 def _link_to_domain(self, name: str, label: NodeLabel, domain: str) -> None:
225 # Ensure domain node exists
226 self.store.create_node(NodeLabel.Domain, {"name": domain, "description": ""})
227 self.store.create_edge(
228 label, {"name": name},
 
229 EdgeType.BELONGS_TO,
230 NodeLabel.Domain, {"name": domain},
 
231 )
232
--- navegador/ingestion/knowledge.py
+++ navegador/ingestion/knowledge.py
@@ -35,14 +35,17 @@
35 self.store = store
36
37 # ── Domains ───────────────────────────────────────────────────────────────
38
39 def add_domain(self, name: str, description: str = "") -> None:
40 self.store.create_node(
41 NodeLabel.Domain,
42 {
43 "name": name,
44 "description": description,
45 },
46 )
47 logger.info("Domain: %s", name)
48
49 # ── Concepts ──────────────────────────────────────────────────────────────
50
51 def add_concept(
@@ -53,29 +56,34 @@
56 status: str = "",
57 rules: str = "",
58 examples: str = "",
59 wiki_refs: str = "",
60 ) -> None:
61 self.store.create_node(
62 NodeLabel.Concept,
63 {
64 "name": name,
65 "description": description,
66 "domain": domain,
67 "status": status,
68 "rules": rules,
69 "examples": examples,
70 "wiki_refs": wiki_refs,
71 },
72 )
73 if domain:
74 self._link_to_domain(name, NodeLabel.Concept, domain)
75 logger.info("Concept: %s", name)
76
77 def relate_concepts(self, a: str, b: str) -> None:
78 """Mark two concepts as related (bidirectional intent)."""
79 self.store.create_edge(
80 NodeLabel.Concept,
81 {"name": a},
82 EdgeType.RELATED_TO,
83 NodeLabel.Concept,
84 {"name": b},
85 )
86
87 # ── Rules ─────────────────────────────────────────────────────────────────
88
89 def add_rule(
@@ -85,27 +93,32 @@
93 domain: str = "",
94 severity: str = "info",
95 rationale: str = "",
96 examples: str = "",
97 ) -> None:
98 self.store.create_node(
99 NodeLabel.Rule,
100 {
101 "name": name,
102 "description": description,
103 "domain": domain,
104 "severity": severity,
105 "rationale": rationale,
106 "examples": examples,
107 },
108 )
109 if domain:
110 self._link_to_domain(name, NodeLabel.Rule, domain)
111 logger.info("Rule: %s", name)
112
113 def rule_governs(self, rule_name: str, target_name: str, target_label: NodeLabel) -> None:
114 self.store.create_edge(
115 NodeLabel.Rule,
116 {"name": rule_name},
117 EdgeType.GOVERNS,
118 target_label,
119 {"name": target_name},
120 )
121
122 # ── Decisions ─────────────────────────────────────────────────────────────
123
124 def add_decision(
@@ -116,19 +129,22 @@
129 status: str = "accepted",
130 rationale: str = "",
131 alternatives: str = "",
132 date: str = "",
133 ) -> None:
134 self.store.create_node(
135 NodeLabel.Decision,
136 {
137 "name": name,
138 "description": description,
139 "domain": domain,
140 "status": status,
141 "rationale": rationale,
142 "alternatives": alternatives,
143 "date": date,
144 },
145 )
146 if domain:
147 self._link_to_domain(name, NodeLabel.Decision, domain)
148 logger.info("Decision: %s", name)
149
150 # ── People ────────────────────────────────────────────────────────────────
@@ -138,24 +154,29 @@
154 name: str,
155 email: str = "",
156 role: str = "",
157 team: str = "",
158 ) -> None:
159 self.store.create_node(
160 NodeLabel.Person,
161 {
162 "name": name,
163 "email": email,
164 "role": role,
165 "team": team,
166 },
167 )
168 logger.info("Person: %s", name)
169
170 def assign(self, target_name: str, target_label: NodeLabel, person_name: str) -> None:
171 """Assign a person as owner of any node."""
172 self.store.create_edge(
173 target_label,
174 {"name": target_name},
175 EdgeType.ASSIGNED_TO,
176 NodeLabel.Person,
177 {"name": person_name},
178 )
179
180 # ── Wiki pages ────────────────────────────────────────────────────────────
181
182 def wiki_page(
@@ -164,25 +185,35 @@
185 url: str = "",
186 source: str = "github",
187 content: str = "",
188 updated_at: str = "",
189 ) -> None:
190 self.store.create_node(
191 NodeLabel.WikiPage,
192 {
193 "name": name,
194 "url": url,
195 "source": source,
196 "content": content,
197 "updated_at": updated_at,
198 },
199 )
200 logger.info("WikiPage: %s", name)
201
202 def wiki_documents(
203 self,
204 wiki_page_name: str,
205 target_name: str,
206 target_props: dict[str, Any],
207 target_label: NodeLabel,
208 ) -> None:
209 self.store.create_edge(
210 NodeLabel.WikiPage,
211 {"name": wiki_page_name},
212 EdgeType.DOCUMENTS,
213 target_label,
214 target_props,
215 )
216
217 # ── Code ↔ Knowledge bridges ──────────────────────────────────────────────
218
219 def annotate_code(
@@ -197,35 +228,43 @@
228 code_label should be a string matching a NodeLabel value.
229 """
230 label = NodeLabel(code_label)
231 if concept:
232 self.store.create_edge(
233 NodeLabel.Concept,
234 {"name": concept},
235 EdgeType.ANNOTATES,
236 label,
237 {"name": code_name},
238 )
239 if rule:
240 self.store.create_edge(
241 NodeLabel.Rule,
242 {"name": rule},
243 EdgeType.ANNOTATES,
244 label,
245 {"name": code_name},
246 )
247
248 def code_implements(self, code_name: str, code_label: str, concept_name: str) -> None:
249 """Mark a function/class as implementing a concept."""
250 label = NodeLabel(code_label)
251 self.store.create_edge(
252 label,
253 {"name": code_name},
254 EdgeType.IMPLEMENTS,
255 NodeLabel.Concept,
256 {"name": concept_name},
257 )
258
259 # ── Helpers ───────────────────────────────────────────────────────────────
260
261 def _link_to_domain(self, name: str, label: NodeLabel, domain: str) -> None:
262 # Ensure domain node exists
263 self.store.create_node(NodeLabel.Domain, {"name": domain, "description": ""})
264 self.store.create_edge(
265 label,
266 {"name": name},
267 EdgeType.BELONGS_TO,
268 NodeLabel.Domain,
269 {"name": domain},
270 )
271
--- navegador/ingestion/parser.py
+++ navegador/ingestion/parser.py
@@ -19,17 +19,17 @@
1919
2020
logger = logging.getLogger(__name__)
2121
2222
# File extensions → language key
2323
LANGUAGE_MAP: dict[str, str] = {
24
- ".py": "python",
25
- ".ts": "typescript",
26
- ".tsx": "typescript",
27
- ".js": "javascript",
28
- ".jsx": "javascript",
29
- ".go": "go",
30
- ".rs": "rust",
24
+ ".py": "python",
25
+ ".ts": "typescript",
26
+ ".tsx": "typescript",
27
+ ".js": "javascript",
28
+ ".jsx": "javascript",
29
+ ".go": "go",
30
+ ".rs": "rust",
3131
".java": "java",
3232
}
3333
3434
3535
class RepoIngester:
@@ -63,14 +63,17 @@
6363
6464
if clear:
6565
self.store.clear()
6666
6767
# Create repository node
68
- self.store.create_node(NodeLabel.Repository, {
69
- "name": repo_path.name,
70
- "path": str(repo_path),
71
- })
68
+ self.store.create_node(
69
+ NodeLabel.Repository,
70
+ {
71
+ "name": repo_path.name,
72
+ "path": str(repo_path),
73
+ },
74
+ )
7275
7376
stats: dict[str, int] = {"files": 0, "functions": 0, "classes": 0, "edges": 0}
7477
7578
for source_file in self._iter_source_files(repo_path):
7679
language = LANGUAGE_MAP.get(source_file.suffix)
@@ -86,21 +89,30 @@
8689
except Exception:
8790
logger.exception("Failed to parse %s", source_file)
8891
8992
logger.info(
9093
"Ingested %s: %d files, %d functions, %d classes",
91
- repo_path.name, stats["files"], stats["functions"], stats["classes"],
94
+ repo_path.name,
95
+ stats["files"],
96
+ stats["functions"],
97
+ stats["classes"],
9298
)
9399
return stats
94100
95101
def _iter_source_files(self, repo_path: Path):
96102
skip_dirs = {
97
- ".git", ".venv", "venv", "node_modules", "__pycache__",
98
- "dist", "build", ".next",
99
- "target", # Rust / Java (Maven/Gradle)
100
- "vendor", # Go modules cache
101
- ".gradle", # Gradle cache
103
+ ".git",
104
+ ".venv",
105
+ "venv",
106
+ "node_modules",
107
+ "__pycache__",
108
+ "dist",
109
+ "build",
110
+ ".next",
111
+ "target", # Rust / Java (Maven/Gradle)
112
+ "vendor", # Go modules cache
113
+ ".gradle", # Gradle cache
102114
}
103115
for path in repo_path.rglob("*"):
104116
if path.is_file() and path.suffix in LANGUAGE_MAP:
105117
if not any(part in skip_dirs for part in path.parts):
106118
yield path
@@ -107,22 +119,27 @@
107119
108120
def _get_parser(self, language: str) -> "LanguageParser":
109121
if language not in self._parsers:
110122
if language == "python":
111123
from navegador.ingestion.python import PythonParser
124
+
112125
self._parsers[language] = PythonParser()
113126
elif language in ("typescript", "javascript"):
114127
from navegador.ingestion.typescript import TypeScriptParser
128
+
115129
self._parsers[language] = TypeScriptParser(language)
116130
elif language == "go":
117131
from navegador.ingestion.go import GoParser
132
+
118133
self._parsers[language] = GoParser()
119134
elif language == "rust":
120135
from navegador.ingestion.rust import RustParser
136
+
121137
self._parsers[language] = RustParser()
122138
elif language == "java":
123139
from navegador.ingestion.java import JavaParser
140
+
124141
self._parsers[language] = JavaParser()
125142
else:
126143
raise ValueError(f"Unsupported language: {language}")
127144
return self._parsers[language]
128145
129146
--- navegador/ingestion/parser.py
+++ navegador/ingestion/parser.py
@@ -19,17 +19,17 @@
19
20 logger = logging.getLogger(__name__)
21
22 # File extensions → language key
23 LANGUAGE_MAP: dict[str, str] = {
24 ".py": "python",
25 ".ts": "typescript",
26 ".tsx": "typescript",
27 ".js": "javascript",
28 ".jsx": "javascript",
29 ".go": "go",
30 ".rs": "rust",
31 ".java": "java",
32 }
33
34
35 class RepoIngester:
@@ -63,14 +63,17 @@
63
64 if clear:
65 self.store.clear()
66
67 # Create repository node
68 self.store.create_node(NodeLabel.Repository, {
69 "name": repo_path.name,
70 "path": str(repo_path),
71 })
 
 
 
72
73 stats: dict[str, int] = {"files": 0, "functions": 0, "classes": 0, "edges": 0}
74
75 for source_file in self._iter_source_files(repo_path):
76 language = LANGUAGE_MAP.get(source_file.suffix)
@@ -86,21 +89,30 @@
86 except Exception:
87 logger.exception("Failed to parse %s", source_file)
88
89 logger.info(
90 "Ingested %s: %d files, %d functions, %d classes",
91 repo_path.name, stats["files"], stats["functions"], stats["classes"],
 
 
 
92 )
93 return stats
94
95 def _iter_source_files(self, repo_path: Path):
96 skip_dirs = {
97 ".git", ".venv", "venv", "node_modules", "__pycache__",
98 "dist", "build", ".next",
99 "target", # Rust / Java (Maven/Gradle)
100 "vendor", # Go modules cache
101 ".gradle", # Gradle cache
 
 
 
 
 
 
102 }
103 for path in repo_path.rglob("*"):
104 if path.is_file() and path.suffix in LANGUAGE_MAP:
105 if not any(part in skip_dirs for part in path.parts):
106 yield path
@@ -107,22 +119,27 @@
107
108 def _get_parser(self, language: str) -> "LanguageParser":
109 if language not in self._parsers:
110 if language == "python":
111 from navegador.ingestion.python import PythonParser
 
112 self._parsers[language] = PythonParser()
113 elif language in ("typescript", "javascript"):
114 from navegador.ingestion.typescript import TypeScriptParser
 
115 self._parsers[language] = TypeScriptParser(language)
116 elif language == "go":
117 from navegador.ingestion.go import GoParser
 
118 self._parsers[language] = GoParser()
119 elif language == "rust":
120 from navegador.ingestion.rust import RustParser
 
121 self._parsers[language] = RustParser()
122 elif language == "java":
123 from navegador.ingestion.java import JavaParser
 
124 self._parsers[language] = JavaParser()
125 else:
126 raise ValueError(f"Unsupported language: {language}")
127 return self._parsers[language]
128
129
--- navegador/ingestion/parser.py
+++ navegador/ingestion/parser.py
@@ -19,17 +19,17 @@
19
20 logger = logging.getLogger(__name__)
21
22 # File extensions → language key
23 LANGUAGE_MAP: dict[str, str] = {
24 ".py": "python",
25 ".ts": "typescript",
26 ".tsx": "typescript",
27 ".js": "javascript",
28 ".jsx": "javascript",
29 ".go": "go",
30 ".rs": "rust",
31 ".java": "java",
32 }
33
34
35 class RepoIngester:
@@ -63,14 +63,17 @@
63
64 if clear:
65 self.store.clear()
66
67 # Create repository node
68 self.store.create_node(
69 NodeLabel.Repository,
70 {
71 "name": repo_path.name,
72 "path": str(repo_path),
73 },
74 )
75
76 stats: dict[str, int] = {"files": 0, "functions": 0, "classes": 0, "edges": 0}
77
78 for source_file in self._iter_source_files(repo_path):
79 language = LANGUAGE_MAP.get(source_file.suffix)
@@ -86,21 +89,30 @@
89 except Exception:
90 logger.exception("Failed to parse %s", source_file)
91
92 logger.info(
93 "Ingested %s: %d files, %d functions, %d classes",
94 repo_path.name,
95 stats["files"],
96 stats["functions"],
97 stats["classes"],
98 )
99 return stats
100
101 def _iter_source_files(self, repo_path: Path):
102 skip_dirs = {
103 ".git",
104 ".venv",
105 "venv",
106 "node_modules",
107 "__pycache__",
108 "dist",
109 "build",
110 ".next",
111 "target", # Rust / Java (Maven/Gradle)
112 "vendor", # Go modules cache
113 ".gradle", # Gradle cache
114 }
115 for path in repo_path.rglob("*"):
116 if path.is_file() and path.suffix in LANGUAGE_MAP:
117 if not any(part in skip_dirs for part in path.parts):
118 yield path
@@ -107,22 +119,27 @@
119
120 def _get_parser(self, language: str) -> "LanguageParser":
121 if language not in self._parsers:
122 if language == "python":
123 from navegador.ingestion.python import PythonParser
124
125 self._parsers[language] = PythonParser()
126 elif language in ("typescript", "javascript"):
127 from navegador.ingestion.typescript import TypeScriptParser
128
129 self._parsers[language] = TypeScriptParser(language)
130 elif language == "go":
131 from navegador.ingestion.go import GoParser
132
133 self._parsers[language] = GoParser()
134 elif language == "rust":
135 from navegador.ingestion.rust import RustParser
136
137 self._parsers[language] = RustParser()
138 elif language == "java":
139 from navegador.ingestion.java import JavaParser
140
141 self._parsers[language] = JavaParser()
142 else:
143 raise ValueError(f"Unsupported language: {language}")
144 return self._parsers[language]
145
146
--- navegador/ingestion/planopticon.py
+++ navegador/ingestion/planopticon.py
@@ -45,53 +45,53 @@
4545
logger = logging.getLogger(__name__)
4646
4747
# ── Relationship type mapping ─────────────────────────────────────────────────
4848
4949
EDGE_MAP: dict[str, EdgeType] = {
50
- "related_to": EdgeType.RELATED_TO,
51
- "uses": EdgeType.DEPENDS_ON,
52
- "depends_on": EdgeType.DEPENDS_ON,
53
- "built_on": EdgeType.DEPENDS_ON,
54
- "implements": EdgeType.IMPLEMENTS,
55
- "requires": EdgeType.DEPENDS_ON,
56
- "blocked_by": EdgeType.DEPENDS_ON,
57
- "has_risk": EdgeType.RELATED_TO,
58
- "addresses": EdgeType.RELATED_TO,
50
+ "related_to": EdgeType.RELATED_TO,
51
+ "uses": EdgeType.DEPENDS_ON,
52
+ "depends_on": EdgeType.DEPENDS_ON,
53
+ "built_on": EdgeType.DEPENDS_ON,
54
+ "implements": EdgeType.IMPLEMENTS,
55
+ "requires": EdgeType.DEPENDS_ON,
56
+ "blocked_by": EdgeType.DEPENDS_ON,
57
+ "has_risk": EdgeType.RELATED_TO,
58
+ "addresses": EdgeType.RELATED_TO,
5959
"has_tradeoff": EdgeType.RELATED_TO,
60
- "delivers": EdgeType.IMPLEMENTS,
61
- "parent_of": EdgeType.CONTAINS,
62
- "assigned_to": EdgeType.ASSIGNED_TO,
63
- "owned_by": EdgeType.ASSIGNED_TO,
64
- "owns": EdgeType.ASSIGNED_TO,
65
- "employed_by": EdgeType.ASSIGNED_TO,
66
- "works_with": EdgeType.RELATED_TO,
67
- "governs": EdgeType.GOVERNS,
68
- "documents": EdgeType.DOCUMENTS,
60
+ "delivers": EdgeType.IMPLEMENTS,
61
+ "parent_of": EdgeType.CONTAINS,
62
+ "assigned_to": EdgeType.ASSIGNED_TO,
63
+ "owned_by": EdgeType.ASSIGNED_TO,
64
+ "owns": EdgeType.ASSIGNED_TO,
65
+ "employed_by": EdgeType.ASSIGNED_TO,
66
+ "works_with": EdgeType.RELATED_TO,
67
+ "governs": EdgeType.GOVERNS,
68
+ "documents": EdgeType.DOCUMENTS,
6969
}
7070
7171
# planopticon node type → navegador NodeLabel
7272
NODE_TYPE_MAP: dict[str, NodeLabel] = {
73
- "concept": NodeLabel.Concept,
74
- "technology": NodeLabel.Concept,
73
+ "concept": NodeLabel.Concept,
74
+ "technology": NodeLabel.Concept,
7575
"organization": NodeLabel.Concept,
76
- "diagram": NodeLabel.WikiPage,
77
- "time": NodeLabel.Concept,
78
- "person": NodeLabel.Person,
76
+ "diagram": NodeLabel.WikiPage,
77
+ "time": NodeLabel.Concept,
78
+ "person": NodeLabel.Person,
7979
}
8080
8181
# planning_type → navegador NodeLabel
8282
PLANNING_TYPE_MAP: dict[str, NodeLabel] = {
83
- "decision": NodeLabel.Decision,
83
+ "decision": NodeLabel.Decision,
8484
"requirement": NodeLabel.Rule,
85
- "constraint": NodeLabel.Rule,
86
- "risk": NodeLabel.Rule,
87
- "goal": NodeLabel.Concept,
88
- "assumption": NodeLabel.Concept,
89
- "feature": NodeLabel.Concept,
90
- "milestone": NodeLabel.Concept,
91
- "task": NodeLabel.Concept,
92
- "dependency": NodeLabel.Concept,
85
+ "constraint": NodeLabel.Rule,
86
+ "risk": NodeLabel.Rule,
87
+ "goal": NodeLabel.Concept,
88
+ "assumption": NodeLabel.Concept,
89
+ "feature": NodeLabel.Concept,
90
+ "milestone": NodeLabel.Concept,
91
+ "task": NodeLabel.Concept,
92
+ "dependency": NodeLabel.Concept,
9393
}
9494
9595
9696
class PlanopticonIngester:
9797
"""
@@ -144,11 +144,13 @@
144144
for diagram in manifest.get("diagrams", []):
145145
self._ingest_diagram(diagram, base_dir, title)
146146
147147
logger.info(
148148
"PlanopticonIngester (%s): nodes=%d edges=%d",
149
- title, stats.get("nodes", 0), stats.get("edges", 0),
149
+ title,
150
+ stats.get("nodes", 0),
151
+ stats.get("edges", 0),
150152
)
151153
return stats
152154
153155
def ingest_kg(self, kg_path: str | Path) -> dict[str, int]:
154156
"""
@@ -247,37 +249,48 @@
247249
248250
descriptions = node.get("descriptions", [])
249251
description = descriptions[0] if descriptions else node.get("description", "")
250252
251253
if label == NodeLabel.Person:
252
- self.store.create_node(NodeLabel.Person, {
253
- "name": name,
254
- "email": "",
255
- "role": node.get("role", ""),
256
- "team": node.get("organization", ""),
257
- })
254
+ self.store.create_node(
255
+ NodeLabel.Person,
256
+ {
257
+ "name": name,
258
+ "email": "",
259
+ "role": node.get("role", ""),
260
+ "team": node.get("organization", ""),
261
+ },
262
+ )
258263
elif label == NodeLabel.WikiPage:
259
- self.store.create_node(NodeLabel.WikiPage, {
260
- "name": name,
261
- "url": node.get("source", ""),
262
- "source": self.source_tag,
263
- "content": description[:4000],
264
- })
264
+ self.store.create_node(
265
+ NodeLabel.WikiPage,
266
+ {
267
+ "name": name,
268
+ "url": node.get("source", ""),
269
+ "source": self.source_tag,
270
+ "content": description[:4000],
271
+ },
272
+ )
265273
else:
266274
domain = "organization" if raw_type == "organization" else node.get("domain", "")
267
- self.store.create_node(NodeLabel.Concept, {
268
- "name": name,
269
- "description": description,
270
- "domain": domain,
271
- "status": node.get("status", ""),
272
- })
275
+ self.store.create_node(
276
+ NodeLabel.Concept,
277
+ {
278
+ "name": name,
279
+ "description": description,
280
+ "domain": domain,
281
+ "status": node.get("status", ""),
282
+ },
283
+ )
273284
if domain:
274285
self._ensure_domain(domain)
275286
self.store.create_edge(
276
- NodeLabel.Concept, {"name": name},
287
+ NodeLabel.Concept,
288
+ {"name": name},
277289
EdgeType.BELONGS_TO,
278
- NodeLabel.Domain, {"name": domain},
290
+ NodeLabel.Domain,
291
+ {"name": domain},
279292
)
280293
281294
self._stats["nodes"] = self._stats.get("nodes", 0) + 1
282295
283296
# Provenance: link to source WikiPage if present
@@ -296,39 +309,50 @@
296309
domain = entity.get("domain", "")
297310
status = entity.get("status", "")
298311
priority = entity.get("priority", "")
299312
300313
if label == NodeLabel.Decision:
301
- self.store.create_node(NodeLabel.Decision, {
302
- "name": name,
303
- "description": description,
304
- "domain": domain,
305
- "status": status or "accepted",
306
- "rationale": entity.get("rationale", ""),
307
- })
314
+ self.store.create_node(
315
+ NodeLabel.Decision,
316
+ {
317
+ "name": name,
318
+ "description": description,
319
+ "domain": domain,
320
+ "status": status or "accepted",
321
+ "rationale": entity.get("rationale", ""),
322
+ },
323
+ )
308324
elif label == NodeLabel.Rule:
309
- self.store.create_node(NodeLabel.Rule, {
310
- "name": name,
311
- "description": description,
312
- "domain": domain,
313
- "severity": "critical" if priority == "high" else "info",
314
- "rationale": entity.get("rationale", ""),
315
- })
325
+ self.store.create_node(
326
+ NodeLabel.Rule,
327
+ {
328
+ "name": name,
329
+ "description": description,
330
+ "domain": domain,
331
+ "severity": "critical" if priority == "high" else "info",
332
+ "rationale": entity.get("rationale", ""),
333
+ },
334
+ )
316335
else:
317
- self.store.create_node(NodeLabel.Concept, {
318
- "name": name,
319
- "description": description,
320
- "domain": domain,
321
- "status": status,
322
- })
336
+ self.store.create_node(
337
+ NodeLabel.Concept,
338
+ {
339
+ "name": name,
340
+ "description": description,
341
+ "domain": domain,
342
+ "status": status,
343
+ },
344
+ )
323345
324346
if domain:
325347
self._ensure_domain(domain)
326348
self.store.create_edge(
327
- label, {"name": name},
349
+ label,
350
+ {"name": name},
328351
EdgeType.BELONGS_TO,
329
- NodeLabel.Domain, {"name": domain},
352
+ NodeLabel.Domain,
353
+ {"name": domain},
330354
)
331355
332356
self._stats["nodes"] = self._stats.get("nodes", 0) + 1
333357
334358
def _ingest_kg_relationship(self, rel: dict[str, Any]) -> None:
@@ -340,15 +364,19 @@
340364
return
341365
342366
edge_type = EDGE_MAP.get(rel_type, EdgeType.RELATED_TO)
343367
344368
# We don't know the exact label of each node — use a label-agnostic match
345
- cypher = """
369
+ cypher = (
370
+ """
346371
MATCH (a), (b)
347372
WHERE a.name = $src AND b.name = $tgt
348
- MERGE (a)-[r:""" + edge_type + """]->(b)
373
+ MERGE (a)-[r:"""
374
+ + edge_type
375
+ + """]->(b)
349376
"""
377
+ )
350378
try:
351379
self.store.query(cypher, {"src": src, "tgt": tgt})
352380
self._stats["edges"] = self._stats.get("edges", 0) + 1
353381
except Exception:
354382
logger.warning("Could not create edge %s -[%s]-> %s", src, edge_type, tgt)
@@ -358,22 +386,27 @@
358386
point = (kp.get("point") or "").strip()
359387
if not point:
360388
continue
361389
topic = kp.get("topic") or ""
362390
name = point[:120] # use the point text as the concept name
363
- self.store.create_node(NodeLabel.Concept, {
364
- "name": name,
365
- "description": kp.get("details", ""),
366
- "domain": topic,
367
- "status": "key_point",
368
- })
391
+ self.store.create_node(
392
+ NodeLabel.Concept,
393
+ {
394
+ "name": name,
395
+ "description": kp.get("details", ""),
396
+ "domain": topic,
397
+ "status": "key_point",
398
+ },
399
+ )
369400
if topic:
370401
self._ensure_domain(topic)
371402
self.store.create_edge(
372
- NodeLabel.Concept, {"name": name},
403
+ NodeLabel.Concept,
404
+ {"name": name},
373405
EdgeType.BELONGS_TO,
374
- NodeLabel.Domain, {"name": topic},
406
+ NodeLabel.Domain,
407
+ {"name": topic},
375408
)
376409
self._stats["nodes"] = self._stats.get("nodes", 0) + 1
377410
378411
def _ingest_action_items(self, action_items: list[dict], source: str) -> None:
379412
for item in action_items:
@@ -381,27 +414,38 @@
381414
assignee = (item.get("assignee") or "").strip()
382415
if not action:
383416
continue
384417
385418
# Action → Rule (it's a commitment / obligation)
386
- self.store.create_node(NodeLabel.Rule, {
387
- "name": action[:120],
388
- "description": item.get("context", ""),
389
- "domain": source,
390
- "severity": item.get("priority", "info"),
391
- "rationale": f"Action item from {source}",
392
- })
419
+ self.store.create_node(
420
+ NodeLabel.Rule,
421
+ {
422
+ "name": action[:120],
423
+ "description": item.get("context", ""),
424
+ "domain": source,
425
+ "severity": item.get("priority", "info"),
426
+ "rationale": f"Action item from {source}",
427
+ },
428
+ )
393429
self._stats["nodes"] = self._stats.get("nodes", 0) + 1
394430
395431
if assignee:
396
- self.store.create_node(NodeLabel.Person, {
397
- "name": assignee, "email": "", "role": "", "team": "",
398
- })
432
+ self.store.create_node(
433
+ NodeLabel.Person,
434
+ {
435
+ "name": assignee,
436
+ "email": "",
437
+ "role": "",
438
+ "team": "",
439
+ },
440
+ )
399441
self.store.create_edge(
400
- NodeLabel.Rule, {"name": action[:120]},
442
+ NodeLabel.Rule,
443
+ {"name": action[:120]},
401444
EdgeType.ASSIGNED_TO,
402
- NodeLabel.Person, {"name": assignee},
445
+ NodeLabel.Person,
446
+ {"name": assignee},
403447
)
404448
self._stats["edges"] = self._stats.get("edges", 0) + 1
405449
406450
def _ingest_diagram(self, diagram: dict[str, Any], base_dir: Path, source: str) -> None:
407451
dtype = diagram.get("diagram_type", "diagram")
@@ -409,57 +453,74 @@
409453
mermaid = diagram.get("mermaid", "")
410454
ts = diagram.get("timestamp")
411455
name = f"{dtype.capitalize()} @ {ts:.0f}s" if ts is not None else f"{dtype.capitalize()}"
412456
413457
content = mermaid or desc
414
- self.store.create_node(NodeLabel.WikiPage, {
415
- "name": name,
416
- "url": diagram.get("image_path", ""),
417
- "source": source,
418
- "content": content[:4000],
419
- })
458
+ self.store.create_node(
459
+ NodeLabel.WikiPage,
460
+ {
461
+ "name": name,
462
+ "url": diagram.get("image_path", ""),
463
+ "source": source,
464
+ "content": content[:4000],
465
+ },
466
+ )
420467
self._stats["nodes"] = self._stats.get("nodes", 0) + 1
421468
422469
# Link diagram elements as concepts
423470
for element in diagram.get("elements", []):
424471
element = element.strip()
425472
if not element:
426473
continue
427
- self.store.create_node(NodeLabel.Concept, {
428
- "name": element, "description": "", "domain": source, "status": "",
429
- })
474
+ self.store.create_node(
475
+ NodeLabel.Concept,
476
+ {
477
+ "name": element,
478
+ "description": "",
479
+ "domain": source,
480
+ "status": "",
481
+ },
482
+ )
430483
self.store.create_edge(
431
- NodeLabel.WikiPage, {"name": name},
484
+ NodeLabel.WikiPage,
485
+ {"name": name},
432486
EdgeType.DOCUMENTS,
433
- NodeLabel.Concept, {"name": element},
487
+ NodeLabel.Concept,
488
+ {"name": element},
434489
)
435490
self._stats["edges"] = self._stats.get("edges", 0) + 1
436491
437492
def _ingest_source(self, source: dict[str, Any]) -> None:
438493
name = (source.get("title") or source.get("source_id") or "").strip()
439494
if not name:
440495
return
441
- self.store.create_node(NodeLabel.WikiPage, {
442
- "name": name,
443
- "url": source.get("url") or source.get("path") or "",
444
- "source": source.get("source_type", ""),
445
- "content": "",
446
- "updated_at": source.get("ingested_at", ""),
447
- })
496
+ self.store.create_node(
497
+ NodeLabel.WikiPage,
498
+ {
499
+ "name": name,
500
+ "url": source.get("url") or source.get("path") or "",
501
+ "source": source.get("source_type", ""),
502
+ "content": "",
503
+ "updated_at": source.get("ingested_at", ""),
504
+ },
505
+ )
448506
self._stats["nodes"] = self._stats.get("nodes", 0) + 1
449507
450508
def _ingest_artifact(self, artifact: dict[str, Any], project_name: str) -> None:
451509
name = (artifact.get("name") or "").strip()
452510
if not name:
453511
return
454512
content = artifact.get("content", "")
455
- self.store.create_node(NodeLabel.WikiPage, {
456
- "name": name,
457
- "url": "",
458
- "source": project_name,
459
- "content": content[:4000],
460
- })
513
+ self.store.create_node(
514
+ NodeLabel.WikiPage,
515
+ {
516
+ "name": name,
517
+ "url": "",
518
+ "source": project_name,
519
+ "content": content[:4000],
520
+ },
521
+ )
461522
self._stats["nodes"] = self._stats.get("nodes", 0) + 1
462523
463524
# ── Helpers ───────────────────────────────────────────────────────────────
464525
465526
def _ensure_domain(self, name: str) -> None:
@@ -467,13 +528,15 @@
467528
468529
def _lazy_wiki_link(self, name: str, label: NodeLabel, source_id: str) -> None:
469530
"""Create a DOCUMENTS edge from a WikiPage to this node if the page exists."""
470531
try:
471532
self.store.create_edge(
472
- NodeLabel.WikiPage, {"name": source_id},
533
+ NodeLabel.WikiPage,
534
+ {"name": source_id},
473535
EdgeType.DOCUMENTS,
474
- label, {"name": name},
536
+ label,
537
+ {"name": name},
475538
)
476539
except Exception:
477540
logger.debug("Could not link %s to wiki page %s", name, source_id)
478541
479542
def _load_json(self, path: Path) -> Any:
480543
--- navegador/ingestion/planopticon.py
+++ navegador/ingestion/planopticon.py
@@ -45,53 +45,53 @@
45 logger = logging.getLogger(__name__)
46
47 # ── Relationship type mapping ─────────────────────────────────────────────────
48
49 EDGE_MAP: dict[str, EdgeType] = {
50 "related_to": EdgeType.RELATED_TO,
51 "uses": EdgeType.DEPENDS_ON,
52 "depends_on": EdgeType.DEPENDS_ON,
53 "built_on": EdgeType.DEPENDS_ON,
54 "implements": EdgeType.IMPLEMENTS,
55 "requires": EdgeType.DEPENDS_ON,
56 "blocked_by": EdgeType.DEPENDS_ON,
57 "has_risk": EdgeType.RELATED_TO,
58 "addresses": EdgeType.RELATED_TO,
59 "has_tradeoff": EdgeType.RELATED_TO,
60 "delivers": EdgeType.IMPLEMENTS,
61 "parent_of": EdgeType.CONTAINS,
62 "assigned_to": EdgeType.ASSIGNED_TO,
63 "owned_by": EdgeType.ASSIGNED_TO,
64 "owns": EdgeType.ASSIGNED_TO,
65 "employed_by": EdgeType.ASSIGNED_TO,
66 "works_with": EdgeType.RELATED_TO,
67 "governs": EdgeType.GOVERNS,
68 "documents": EdgeType.DOCUMENTS,
69 }
70
71 # planopticon node type → navegador NodeLabel
72 NODE_TYPE_MAP: dict[str, NodeLabel] = {
73 "concept": NodeLabel.Concept,
74 "technology": NodeLabel.Concept,
75 "organization": NodeLabel.Concept,
76 "diagram": NodeLabel.WikiPage,
77 "time": NodeLabel.Concept,
78 "person": NodeLabel.Person,
79 }
80
81 # planning_type → navegador NodeLabel
82 PLANNING_TYPE_MAP: dict[str, NodeLabel] = {
83 "decision": NodeLabel.Decision,
84 "requirement": NodeLabel.Rule,
85 "constraint": NodeLabel.Rule,
86 "risk": NodeLabel.Rule,
87 "goal": NodeLabel.Concept,
88 "assumption": NodeLabel.Concept,
89 "feature": NodeLabel.Concept,
90 "milestone": NodeLabel.Concept,
91 "task": NodeLabel.Concept,
92 "dependency": NodeLabel.Concept,
93 }
94
95
96 class PlanopticonIngester:
97 """
@@ -144,11 +144,13 @@
144 for diagram in manifest.get("diagrams", []):
145 self._ingest_diagram(diagram, base_dir, title)
146
147 logger.info(
148 "PlanopticonIngester (%s): nodes=%d edges=%d",
149 title, stats.get("nodes", 0), stats.get("edges", 0),
 
 
150 )
151 return stats
152
153 def ingest_kg(self, kg_path: str | Path) -> dict[str, int]:
154 """
@@ -247,37 +249,48 @@
247
248 descriptions = node.get("descriptions", [])
249 description = descriptions[0] if descriptions else node.get("description", "")
250
251 if label == NodeLabel.Person:
252 self.store.create_node(NodeLabel.Person, {
253 "name": name,
254 "email": "",
255 "role": node.get("role", ""),
256 "team": node.get("organization", ""),
257 })
 
 
 
258 elif label == NodeLabel.WikiPage:
259 self.store.create_node(NodeLabel.WikiPage, {
260 "name": name,
261 "url": node.get("source", ""),
262 "source": self.source_tag,
263 "content": description[:4000],
264 })
 
 
 
265 else:
266 domain = "organization" if raw_type == "organization" else node.get("domain", "")
267 self.store.create_node(NodeLabel.Concept, {
268 "name": name,
269 "description": description,
270 "domain": domain,
271 "status": node.get("status", ""),
272 })
 
 
 
273 if domain:
274 self._ensure_domain(domain)
275 self.store.create_edge(
276 NodeLabel.Concept, {"name": name},
 
277 EdgeType.BELONGS_TO,
278 NodeLabel.Domain, {"name": domain},
 
279 )
280
281 self._stats["nodes"] = self._stats.get("nodes", 0) + 1
282
283 # Provenance: link to source WikiPage if present
@@ -296,39 +309,50 @@
296 domain = entity.get("domain", "")
297 status = entity.get("status", "")
298 priority = entity.get("priority", "")
299
300 if label == NodeLabel.Decision:
301 self.store.create_node(NodeLabel.Decision, {
302 "name": name,
303 "description": description,
304 "domain": domain,
305 "status": status or "accepted",
306 "rationale": entity.get("rationale", ""),
307 })
 
 
 
308 elif label == NodeLabel.Rule:
309 self.store.create_node(NodeLabel.Rule, {
310 "name": name,
311 "description": description,
312 "domain": domain,
313 "severity": "critical" if priority == "high" else "info",
314 "rationale": entity.get("rationale", ""),
315 })
 
 
 
316 else:
317 self.store.create_node(NodeLabel.Concept, {
318 "name": name,
319 "description": description,
320 "domain": domain,
321 "status": status,
322 })
 
 
 
323
324 if domain:
325 self._ensure_domain(domain)
326 self.store.create_edge(
327 label, {"name": name},
 
328 EdgeType.BELONGS_TO,
329 NodeLabel.Domain, {"name": domain},
 
330 )
331
332 self._stats["nodes"] = self._stats.get("nodes", 0) + 1
333
334 def _ingest_kg_relationship(self, rel: dict[str, Any]) -> None:
@@ -340,15 +364,19 @@
340 return
341
342 edge_type = EDGE_MAP.get(rel_type, EdgeType.RELATED_TO)
343
344 # We don't know the exact label of each node — use a label-agnostic match
345 cypher = """
 
346 MATCH (a), (b)
347 WHERE a.name = $src AND b.name = $tgt
348 MERGE (a)-[r:""" + edge_type + """]->(b)
 
 
349 """
 
350 try:
351 self.store.query(cypher, {"src": src, "tgt": tgt})
352 self._stats["edges"] = self._stats.get("edges", 0) + 1
353 except Exception:
354 logger.warning("Could not create edge %s -[%s]-> %s", src, edge_type, tgt)
@@ -358,22 +386,27 @@
358 point = (kp.get("point") or "").strip()
359 if not point:
360 continue
361 topic = kp.get("topic") or ""
362 name = point[:120] # use the point text as the concept name
363 self.store.create_node(NodeLabel.Concept, {
364 "name": name,
365 "description": kp.get("details", ""),
366 "domain": topic,
367 "status": "key_point",
368 })
 
 
 
369 if topic:
370 self._ensure_domain(topic)
371 self.store.create_edge(
372 NodeLabel.Concept, {"name": name},
 
373 EdgeType.BELONGS_TO,
374 NodeLabel.Domain, {"name": topic},
 
375 )
376 self._stats["nodes"] = self._stats.get("nodes", 0) + 1
377
378 def _ingest_action_items(self, action_items: list[dict], source: str) -> None:
379 for item in action_items:
@@ -381,27 +414,38 @@
381 assignee = (item.get("assignee") or "").strip()
382 if not action:
383 continue
384
385 # Action → Rule (it's a commitment / obligation)
386 self.store.create_node(NodeLabel.Rule, {
387 "name": action[:120],
388 "description": item.get("context", ""),
389 "domain": source,
390 "severity": item.get("priority", "info"),
391 "rationale": f"Action item from {source}",
392 })
 
 
 
393 self._stats["nodes"] = self._stats.get("nodes", 0) + 1
394
395 if assignee:
396 self.store.create_node(NodeLabel.Person, {
397 "name": assignee, "email": "", "role": "", "team": "",
398 })
 
 
 
 
 
 
399 self.store.create_edge(
400 NodeLabel.Rule, {"name": action[:120]},
 
401 EdgeType.ASSIGNED_TO,
402 NodeLabel.Person, {"name": assignee},
 
403 )
404 self._stats["edges"] = self._stats.get("edges", 0) + 1
405
406 def _ingest_diagram(self, diagram: dict[str, Any], base_dir: Path, source: str) -> None:
407 dtype = diagram.get("diagram_type", "diagram")
@@ -409,57 +453,74 @@
409 mermaid = diagram.get("mermaid", "")
410 ts = diagram.get("timestamp")
411 name = f"{dtype.capitalize()} @ {ts:.0f}s" if ts is not None else f"{dtype.capitalize()}"
412
413 content = mermaid or desc
414 self.store.create_node(NodeLabel.WikiPage, {
415 "name": name,
416 "url": diagram.get("image_path", ""),
417 "source": source,
418 "content": content[:4000],
419 })
 
 
 
420 self._stats["nodes"] = self._stats.get("nodes", 0) + 1
421
422 # Link diagram elements as concepts
423 for element in diagram.get("elements", []):
424 element = element.strip()
425 if not element:
426 continue
427 self.store.create_node(NodeLabel.Concept, {
428 "name": element, "description": "", "domain": source, "status": "",
429 })
 
 
 
 
 
 
430 self.store.create_edge(
431 NodeLabel.WikiPage, {"name": name},
 
432 EdgeType.DOCUMENTS,
433 NodeLabel.Concept, {"name": element},
 
434 )
435 self._stats["edges"] = self._stats.get("edges", 0) + 1
436
437 def _ingest_source(self, source: dict[str, Any]) -> None:
438 name = (source.get("title") or source.get("source_id") or "").strip()
439 if not name:
440 return
441 self.store.create_node(NodeLabel.WikiPage, {
442 "name": name,
443 "url": source.get("url") or source.get("path") or "",
444 "source": source.get("source_type", ""),
445 "content": "",
446 "updated_at": source.get("ingested_at", ""),
447 })
 
 
 
448 self._stats["nodes"] = self._stats.get("nodes", 0) + 1
449
450 def _ingest_artifact(self, artifact: dict[str, Any], project_name: str) -> None:
451 name = (artifact.get("name") or "").strip()
452 if not name:
453 return
454 content = artifact.get("content", "")
455 self.store.create_node(NodeLabel.WikiPage, {
456 "name": name,
457 "url": "",
458 "source": project_name,
459 "content": content[:4000],
460 })
 
 
 
461 self._stats["nodes"] = self._stats.get("nodes", 0) + 1
462
463 # ── Helpers ───────────────────────────────────────────────────────────────
464
465 def _ensure_domain(self, name: str) -> None:
@@ -467,13 +528,15 @@
467
468 def _lazy_wiki_link(self, name: str, label: NodeLabel, source_id: str) -> None:
469 """Create a DOCUMENTS edge from a WikiPage to this node if the page exists."""
470 try:
471 self.store.create_edge(
472 NodeLabel.WikiPage, {"name": source_id},
 
473 EdgeType.DOCUMENTS,
474 label, {"name": name},
 
475 )
476 except Exception:
477 logger.debug("Could not link %s to wiki page %s", name, source_id)
478
479 def _load_json(self, path: Path) -> Any:
480
--- navegador/ingestion/planopticon.py
+++ navegador/ingestion/planopticon.py
@@ -45,53 +45,53 @@
45 logger = logging.getLogger(__name__)
46
47 # ── Relationship type mapping ─────────────────────────────────────────────────
48
49 EDGE_MAP: dict[str, EdgeType] = {
50 "related_to": EdgeType.RELATED_TO,
51 "uses": EdgeType.DEPENDS_ON,
52 "depends_on": EdgeType.DEPENDS_ON,
53 "built_on": EdgeType.DEPENDS_ON,
54 "implements": EdgeType.IMPLEMENTS,
55 "requires": EdgeType.DEPENDS_ON,
56 "blocked_by": EdgeType.DEPENDS_ON,
57 "has_risk": EdgeType.RELATED_TO,
58 "addresses": EdgeType.RELATED_TO,
59 "has_tradeoff": EdgeType.RELATED_TO,
60 "delivers": EdgeType.IMPLEMENTS,
61 "parent_of": EdgeType.CONTAINS,
62 "assigned_to": EdgeType.ASSIGNED_TO,
63 "owned_by": EdgeType.ASSIGNED_TO,
64 "owns": EdgeType.ASSIGNED_TO,
65 "employed_by": EdgeType.ASSIGNED_TO,
66 "works_with": EdgeType.RELATED_TO,
67 "governs": EdgeType.GOVERNS,
68 "documents": EdgeType.DOCUMENTS,
69 }
70
71 # planopticon node type → navegador NodeLabel
72 NODE_TYPE_MAP: dict[str, NodeLabel] = {
73 "concept": NodeLabel.Concept,
74 "technology": NodeLabel.Concept,
75 "organization": NodeLabel.Concept,
76 "diagram": NodeLabel.WikiPage,
77 "time": NodeLabel.Concept,
78 "person": NodeLabel.Person,
79 }
80
81 # planning_type → navegador NodeLabel
82 PLANNING_TYPE_MAP: dict[str, NodeLabel] = {
83 "decision": NodeLabel.Decision,
84 "requirement": NodeLabel.Rule,
85 "constraint": NodeLabel.Rule,
86 "risk": NodeLabel.Rule,
87 "goal": NodeLabel.Concept,
88 "assumption": NodeLabel.Concept,
89 "feature": NodeLabel.Concept,
90 "milestone": NodeLabel.Concept,
91 "task": NodeLabel.Concept,
92 "dependency": NodeLabel.Concept,
93 }
94
95
96 class PlanopticonIngester:
97 """
@@ -144,11 +144,13 @@
144 for diagram in manifest.get("diagrams", []):
145 self._ingest_diagram(diagram, base_dir, title)
146
147 logger.info(
148 "PlanopticonIngester (%s): nodes=%d edges=%d",
149 title,
150 stats.get("nodes", 0),
151 stats.get("edges", 0),
152 )
153 return stats
154
155 def ingest_kg(self, kg_path: str | Path) -> dict[str, int]:
156 """
@@ -247,37 +249,48 @@
249
250 descriptions = node.get("descriptions", [])
251 description = descriptions[0] if descriptions else node.get("description", "")
252
253 if label == NodeLabel.Person:
254 self.store.create_node(
255 NodeLabel.Person,
256 {
257 "name": name,
258 "email": "",
259 "role": node.get("role", ""),
260 "team": node.get("organization", ""),
261 },
262 )
263 elif label == NodeLabel.WikiPage:
264 self.store.create_node(
265 NodeLabel.WikiPage,
266 {
267 "name": name,
268 "url": node.get("source", ""),
269 "source": self.source_tag,
270 "content": description[:4000],
271 },
272 )
273 else:
274 domain = "organization" if raw_type == "organization" else node.get("domain", "")
275 self.store.create_node(
276 NodeLabel.Concept,
277 {
278 "name": name,
279 "description": description,
280 "domain": domain,
281 "status": node.get("status", ""),
282 },
283 )
284 if domain:
285 self._ensure_domain(domain)
286 self.store.create_edge(
287 NodeLabel.Concept,
288 {"name": name},
289 EdgeType.BELONGS_TO,
290 NodeLabel.Domain,
291 {"name": domain},
292 )
293
294 self._stats["nodes"] = self._stats.get("nodes", 0) + 1
295
296 # Provenance: link to source WikiPage if present
@@ -296,39 +309,50 @@
309 domain = entity.get("domain", "")
310 status = entity.get("status", "")
311 priority = entity.get("priority", "")
312
313 if label == NodeLabel.Decision:
314 self.store.create_node(
315 NodeLabel.Decision,
316 {
317 "name": name,
318 "description": description,
319 "domain": domain,
320 "status": status or "accepted",
321 "rationale": entity.get("rationale", ""),
322 },
323 )
324 elif label == NodeLabel.Rule:
325 self.store.create_node(
326 NodeLabel.Rule,
327 {
328 "name": name,
329 "description": description,
330 "domain": domain,
331 "severity": "critical" if priority == "high" else "info",
332 "rationale": entity.get("rationale", ""),
333 },
334 )
335 else:
336 self.store.create_node(
337 NodeLabel.Concept,
338 {
339 "name": name,
340 "description": description,
341 "domain": domain,
342 "status": status,
343 },
344 )
345
346 if domain:
347 self._ensure_domain(domain)
348 self.store.create_edge(
349 label,
350 {"name": name},
351 EdgeType.BELONGS_TO,
352 NodeLabel.Domain,
353 {"name": domain},
354 )
355
356 self._stats["nodes"] = self._stats.get("nodes", 0) + 1
357
358 def _ingest_kg_relationship(self, rel: dict[str, Any]) -> None:
@@ -340,15 +364,19 @@
364 return
365
366 edge_type = EDGE_MAP.get(rel_type, EdgeType.RELATED_TO)
367
368 # We don't know the exact label of each node — use a label-agnostic match
369 cypher = (
370 """
371 MATCH (a), (b)
372 WHERE a.name = $src AND b.name = $tgt
373 MERGE (a)-[r:"""
374 + edge_type
375 + """]->(b)
376 """
377 )
378 try:
379 self.store.query(cypher, {"src": src, "tgt": tgt})
380 self._stats["edges"] = self._stats.get("edges", 0) + 1
381 except Exception:
382 logger.warning("Could not create edge %s -[%s]-> %s", src, edge_type, tgt)
@@ -358,22 +386,27 @@
386 point = (kp.get("point") or "").strip()
387 if not point:
388 continue
389 topic = kp.get("topic") or ""
390 name = point[:120] # use the point text as the concept name
391 self.store.create_node(
392 NodeLabel.Concept,
393 {
394 "name": name,
395 "description": kp.get("details", ""),
396 "domain": topic,
397 "status": "key_point",
398 },
399 )
400 if topic:
401 self._ensure_domain(topic)
402 self.store.create_edge(
403 NodeLabel.Concept,
404 {"name": name},
405 EdgeType.BELONGS_TO,
406 NodeLabel.Domain,
407 {"name": topic},
408 )
409 self._stats["nodes"] = self._stats.get("nodes", 0) + 1
410
411 def _ingest_action_items(self, action_items: list[dict], source: str) -> None:
412 for item in action_items:
@@ -381,27 +414,38 @@
414 assignee = (item.get("assignee") or "").strip()
415 if not action:
416 continue
417
418 # Action → Rule (it's a commitment / obligation)
419 self.store.create_node(
420 NodeLabel.Rule,
421 {
422 "name": action[:120],
423 "description": item.get("context", ""),
424 "domain": source,
425 "severity": item.get("priority", "info"),
426 "rationale": f"Action item from {source}",
427 },
428 )
429 self._stats["nodes"] = self._stats.get("nodes", 0) + 1
430
431 if assignee:
432 self.store.create_node(
433 NodeLabel.Person,
434 {
435 "name": assignee,
436 "email": "",
437 "role": "",
438 "team": "",
439 },
440 )
441 self.store.create_edge(
442 NodeLabel.Rule,
443 {"name": action[:120]},
444 EdgeType.ASSIGNED_TO,
445 NodeLabel.Person,
446 {"name": assignee},
447 )
448 self._stats["edges"] = self._stats.get("edges", 0) + 1
449
450 def _ingest_diagram(self, diagram: dict[str, Any], base_dir: Path, source: str) -> None:
451 dtype = diagram.get("diagram_type", "diagram")
@@ -409,57 +453,74 @@
453 mermaid = diagram.get("mermaid", "")
454 ts = diagram.get("timestamp")
455 name = f"{dtype.capitalize()} @ {ts:.0f}s" if ts is not None else f"{dtype.capitalize()}"
456
457 content = mermaid or desc
458 self.store.create_node(
459 NodeLabel.WikiPage,
460 {
461 "name": name,
462 "url": diagram.get("image_path", ""),
463 "source": source,
464 "content": content[:4000],
465 },
466 )
467 self._stats["nodes"] = self._stats.get("nodes", 0) + 1
468
469 # Link diagram elements as concepts
470 for element in diagram.get("elements", []):
471 element = element.strip()
472 if not element:
473 continue
474 self.store.create_node(
475 NodeLabel.Concept,
476 {
477 "name": element,
478 "description": "",
479 "domain": source,
480 "status": "",
481 },
482 )
483 self.store.create_edge(
484 NodeLabel.WikiPage,
485 {"name": name},
486 EdgeType.DOCUMENTS,
487 NodeLabel.Concept,
488 {"name": element},
489 )
490 self._stats["edges"] = self._stats.get("edges", 0) + 1
491
492 def _ingest_source(self, source: dict[str, Any]) -> None:
493 name = (source.get("title") or source.get("source_id") or "").strip()
494 if not name:
495 return
496 self.store.create_node(
497 NodeLabel.WikiPage,
498 {
499 "name": name,
500 "url": source.get("url") or source.get("path") or "",
501 "source": source.get("source_type", ""),
502 "content": "",
503 "updated_at": source.get("ingested_at", ""),
504 },
505 )
506 self._stats["nodes"] = self._stats.get("nodes", 0) + 1
507
508 def _ingest_artifact(self, artifact: dict[str, Any], project_name: str) -> None:
509 name = (artifact.get("name") or "").strip()
510 if not name:
511 return
512 content = artifact.get("content", "")
513 self.store.create_node(
514 NodeLabel.WikiPage,
515 {
516 "name": name,
517 "url": "",
518 "source": project_name,
519 "content": content[:4000],
520 },
521 )
522 self._stats["nodes"] = self._stats.get("nodes", 0) + 1
523
524 # ── Helpers ───────────────────────────────────────────────────────────────
525
526 def _ensure_domain(self, name: str) -> None:
@@ -467,13 +528,15 @@
528
529 def _lazy_wiki_link(self, name: str, label: NodeLabel, source_id: str) -> None:
530 """Create a DOCUMENTS edge from a WikiPage to this node if the page exists."""
531 try:
532 self.store.create_edge(
533 NodeLabel.WikiPage,
534 {"name": source_id},
535 EdgeType.DOCUMENTS,
536 label,
537 {"name": name},
538 )
539 except Exception:
540 logger.debug("Could not link %s to wiki page %s", name, source_id)
541
542 def _load_json(self, path: Path) -> Any:
543
--- navegador/ingestion/python.py
+++ navegador/ingestion/python.py
@@ -15,25 +15,25 @@
1515
1616
def _get_python_language():
1717
try:
1818
import tree_sitter_python as tspython # type: ignore[import]
1919
from tree_sitter import Language
20
+
2021
return Language(tspython.language())
2122
except ImportError as e:
22
- raise ImportError(
23
- "Install tree-sitter-python: pip install tree-sitter-python"
24
- ) from e
23
+ raise ImportError("Install tree-sitter-python: pip install tree-sitter-python") from e
2524
2625
2726
def _get_parser():
2827
from tree_sitter import Parser # type: ignore[import]
28
+
2929
parser = Parser(_get_python_language())
3030
return parser
3131
3232
3333
def _node_text(node, source: bytes) -> str:
34
- return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
34
+ return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
3535
3636
3737
def _get_docstring(node, source: bytes) -> str | None:
3838
"""Extract the first string literal from a function/class body as docstring."""
3939
body = next((c for c in node.children if c.type == "block"), None)
@@ -61,22 +61,32 @@
6161
rel_path = str(path.relative_to(repo_root))
6262
6363
stats = {"functions": 0, "classes": 0, "edges": 0}
6464
6565
# File node
66
- store.create_node(NodeLabel.File, {
67
- "name": path.name,
68
- "path": rel_path,
69
- "language": "python",
70
- "line_count": source.count(b"\n"),
71
- })
66
+ store.create_node(
67
+ NodeLabel.File,
68
+ {
69
+ "name": path.name,
70
+ "path": rel_path,
71
+ "language": "python",
72
+ "line_count": source.count(b"\n"),
73
+ },
74
+ )
7275
7376
self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
7477
return stats
7578
76
- def _walk(self, node, source: bytes, file_path: str, store: GraphStore,
77
- stats: dict, class_name: str | None) -> None:
79
+ def _walk(
80
+ self,
81
+ node,
82
+ source: bytes,
83
+ file_path: str,
84
+ store: GraphStore,
85
+ stats: dict,
86
+ class_name: str | None,
87
+ ) -> None:
7888
if node.type == "import_statement":
7989
self._handle_import(node, source, file_path, store, stats)
8090
8191
elif node.type == "import_from_statement":
8292
self._handle_import_from(node, source, file_path, store, stats)
@@ -90,70 +100,88 @@
90100
return # function walker handles children
91101
92102
for child in node.children:
93103
self._walk(child, source, file_path, store, stats, class_name)
94104
95
- def _handle_import(self, node, source: bytes, file_path: str,
96
- store: GraphStore, stats: dict) -> None:
105
+ def _handle_import(
106
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
107
+ ) -> None:
97108
for child in node.children:
98109
if child.type == "dotted_name":
99110
name = _node_text(child, source)
100
- store.create_node(NodeLabel.Import, {
101
- "name": name,
102
- "file_path": file_path,
103
- "line_start": node.start_point[0] + 1,
104
- "module": name,
105
- })
111
+ store.create_node(
112
+ NodeLabel.Import,
113
+ {
114
+ "name": name,
115
+ "file_path": file_path,
116
+ "line_start": node.start_point[0] + 1,
117
+ "module": name,
118
+ },
119
+ )
106120
store.create_edge(
107
- NodeLabel.File, {"path": file_path},
121
+ NodeLabel.File,
122
+ {"path": file_path},
108123
EdgeType.IMPORTS,
109
- NodeLabel.Import, {"name": name, "file_path": file_path},
124
+ NodeLabel.Import,
125
+ {"name": name, "file_path": file_path},
110126
)
111127
stats["edges"] += 1
112128
113
- def _handle_import_from(self, node, source: bytes, file_path: str,
114
- store: GraphStore, stats: dict) -> None:
129
+ def _handle_import_from(
130
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
131
+ ) -> None:
115132
module = ""
116133
for child in node.children:
117134
if child.type in ("dotted_name", "relative_import"):
118135
module = _node_text(child, source)
119136
break
120137
for child in node.children:
121138
if child.type == "import_from_member":
122139
name = _node_text(child, source)
123
- store.create_node(NodeLabel.Import, {
124
- "name": name,
125
- "file_path": file_path,
126
- "line_start": node.start_point[0] + 1,
127
- "module": module,
128
- })
140
+ store.create_node(
141
+ NodeLabel.Import,
142
+ {
143
+ "name": name,
144
+ "file_path": file_path,
145
+ "line_start": node.start_point[0] + 1,
146
+ "module": module,
147
+ },
148
+ )
129149
store.create_edge(
130
- NodeLabel.File, {"path": file_path},
150
+ NodeLabel.File,
151
+ {"path": file_path},
131152
EdgeType.IMPORTS,
132
- NodeLabel.Import, {"name": name, "file_path": file_path},
153
+ NodeLabel.Import,
154
+ {"name": name, "file_path": file_path},
133155
)
134156
stats["edges"] += 1
135157
136
- def _handle_class(self, node, source: bytes, file_path: str,
137
- store: GraphStore, stats: dict) -> None:
158
+ def _handle_class(
159
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
160
+ ) -> None:
138161
name_node = next((c for c in node.children if c.type == "identifier"), None)
139162
if not name_node:
140163
return
141164
name = _node_text(name_node, source)
142165
docstring = _get_docstring(node, source)
143166
144
- store.create_node(NodeLabel.Class, {
145
- "name": name,
146
- "file_path": file_path,
147
- "line_start": node.start_point[0] + 1,
148
- "line_end": node.end_point[0] + 1,
149
- "docstring": docstring or "",
150
- })
167
+ store.create_node(
168
+ NodeLabel.Class,
169
+ {
170
+ "name": name,
171
+ "file_path": file_path,
172
+ "line_start": node.start_point[0] + 1,
173
+ "line_end": node.end_point[0] + 1,
174
+ "docstring": docstring or "",
175
+ },
176
+ )
151177
store.create_edge(
152
- NodeLabel.File, {"path": file_path},
178
+ NodeLabel.File,
179
+ {"path": file_path},
153180
EdgeType.CONTAINS,
154
- NodeLabel.Class, {"name": name, "file_path": file_path},
181
+ NodeLabel.Class,
182
+ {"name": name, "file_path": file_path},
155183
)
156184
stats["classes"] += 1
157185
stats["edges"] += 1
158186
159187
# Inheritance
@@ -161,13 +189,15 @@
161189
if child.type == "argument_list":
162190
for arg in child.children:
163191
if arg.type == "identifier":
164192
parent_name = _node_text(arg, source)
165193
store.create_edge(
166
- NodeLabel.Class, {"name": name, "file_path": file_path},
194
+ NodeLabel.Class,
195
+ {"name": name, "file_path": file_path},
167196
EdgeType.INHERITS,
168
- NodeLabel.Class, {"name": parent_name, "file_path": file_path},
197
+ NodeLabel.Class,
198
+ {"name": parent_name, "file_path": file_path},
169199
)
170200
stats["edges"] += 1
171201
172202
# Walk class body for methods
173203
body = next((c for c in node.children if c.type == "block"), None)
@@ -174,12 +204,19 @@
174204
if body:
175205
for child in body.children:
176206
if child.type == "function_definition":
177207
self._handle_function(child, source, file_path, store, stats, class_name=name)
178208
179
- def _handle_function(self, node, source: bytes, file_path: str,
180
- store: GraphStore, stats: dict, class_name: str | None) -> None:
209
+ def _handle_function(
210
+ self,
211
+ node,
212
+ source: bytes,
213
+ file_path: str,
214
+ store: GraphStore,
215
+ stats: dict,
216
+ class_name: str | None,
217
+ ) -> None:
181218
name_node = next((c for c in node.children if c.type == "identifier"), None)
182219
if not name_node:
183220
return
184221
name = _node_text(name_node, source)
185222
docstring = _get_docstring(node, source)
@@ -195,40 +232,51 @@
195232
}
196233
store.create_node(label, props)
197234
198235
container_label = NodeLabel.Class if class_name else NodeLabel.File
199236
container_key = (
200
- {"name": class_name, "file_path": file_path}
201
- if class_name
202
- else {"path": file_path}
237
+ {"name": class_name, "file_path": file_path} if class_name else {"path": file_path}
203238
)
204239
store.create_edge(
205
- container_label, container_key, EdgeType.CONTAINS, label,
240
+ container_label,
241
+ container_key,
242
+ EdgeType.CONTAINS,
243
+ label,
206244
{"name": name, "file_path": file_path},
207245
)
208246
stats["functions"] += 1
209247
stats["edges"] += 1
210248
211249
# Call edges — find all call expressions in the body
212250
self._extract_calls(node, source, file_path, name, label, store, stats)
213251
214
- def _extract_calls(self, fn_node, source: bytes, file_path: str, fn_name: str,
215
- fn_label: str, store: GraphStore, stats: dict) -> None:
252
+ def _extract_calls(
253
+ self,
254
+ fn_node,
255
+ source: bytes,
256
+ file_path: str,
257
+ fn_name: str,
258
+ fn_label: str,
259
+ store: GraphStore,
260
+ stats: dict,
261
+ ) -> None:
216262
def walk_calls(node):
217263
if node.type == "call":
218264
func = next(
219265
(c for c in node.children if c.type in ("identifier", "attribute")), None
220266
)
221267
if func:
222268
callee_name = _node_text(func, source).split(".")[-1]
223269
store.create_edge(
224
- fn_label, {"name": fn_name, "file_path": file_path},
270
+ fn_label,
271
+ {"name": fn_name, "file_path": file_path},
225272
EdgeType.CALLS,
226
- NodeLabel.Function, {"name": callee_name, "file_path": file_path},
273
+ NodeLabel.Function,
274
+ {"name": callee_name, "file_path": file_path},
227275
)
228276
stats["edges"] += 1
229277
for child in node.children:
230278
walk_calls(child)
231279
232280
body = next((c for c in fn_node.children if c.type == "block"), None)
233281
if body:
234282
walk_calls(body)
235283
--- navegador/ingestion/python.py
+++ navegador/ingestion/python.py
@@ -15,25 +15,25 @@
15
16 def _get_python_language():
17 try:
18 import tree_sitter_python as tspython # type: ignore[import]
19 from tree_sitter import Language
 
20 return Language(tspython.language())
21 except ImportError as e:
22 raise ImportError(
23 "Install tree-sitter-python: pip install tree-sitter-python"
24 ) from e
25
26
27 def _get_parser():
28 from tree_sitter import Parser # type: ignore[import]
 
29 parser = Parser(_get_python_language())
30 return parser
31
32
33 def _node_text(node, source: bytes) -> str:
34 return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
35
36
37 def _get_docstring(node, source: bytes) -> str | None:
38 """Extract the first string literal from a function/class body as docstring."""
39 body = next((c for c in node.children if c.type == "block"), None)
@@ -61,22 +61,32 @@
61 rel_path = str(path.relative_to(repo_root))
62
63 stats = {"functions": 0, "classes": 0, "edges": 0}
64
65 # File node
66 store.create_node(NodeLabel.File, {
67 "name": path.name,
68 "path": rel_path,
69 "language": "python",
70 "line_count": source.count(b"\n"),
71 })
 
 
 
72
73 self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
74 return stats
75
76 def _walk(self, node, source: bytes, file_path: str, store: GraphStore,
77 stats: dict, class_name: str | None) -> None:
 
 
 
 
 
 
 
78 if node.type == "import_statement":
79 self._handle_import(node, source, file_path, store, stats)
80
81 elif node.type == "import_from_statement":
82 self._handle_import_from(node, source, file_path, store, stats)
@@ -90,70 +100,88 @@
90 return # function walker handles children
91
92 for child in node.children:
93 self._walk(child, source, file_path, store, stats, class_name)
94
95 def _handle_import(self, node, source: bytes, file_path: str,
96 store: GraphStore, stats: dict) -> None:
 
97 for child in node.children:
98 if child.type == "dotted_name":
99 name = _node_text(child, source)
100 store.create_node(NodeLabel.Import, {
101 "name": name,
102 "file_path": file_path,
103 "line_start": node.start_point[0] + 1,
104 "module": name,
105 })
 
 
 
106 store.create_edge(
107 NodeLabel.File, {"path": file_path},
 
108 EdgeType.IMPORTS,
109 NodeLabel.Import, {"name": name, "file_path": file_path},
 
110 )
111 stats["edges"] += 1
112
113 def _handle_import_from(self, node, source: bytes, file_path: str,
114 store: GraphStore, stats: dict) -> None:
 
115 module = ""
116 for child in node.children:
117 if child.type in ("dotted_name", "relative_import"):
118 module = _node_text(child, source)
119 break
120 for child in node.children:
121 if child.type == "import_from_member":
122 name = _node_text(child, source)
123 store.create_node(NodeLabel.Import, {
124 "name": name,
125 "file_path": file_path,
126 "line_start": node.start_point[0] + 1,
127 "module": module,
128 })
 
 
 
129 store.create_edge(
130 NodeLabel.File, {"path": file_path},
 
131 EdgeType.IMPORTS,
132 NodeLabel.Import, {"name": name, "file_path": file_path},
 
133 )
134 stats["edges"] += 1
135
136 def _handle_class(self, node, source: bytes, file_path: str,
137 store: GraphStore, stats: dict) -> None:
 
138 name_node = next((c for c in node.children if c.type == "identifier"), None)
139 if not name_node:
140 return
141 name = _node_text(name_node, source)
142 docstring = _get_docstring(node, source)
143
144 store.create_node(NodeLabel.Class, {
145 "name": name,
146 "file_path": file_path,
147 "line_start": node.start_point[0] + 1,
148 "line_end": node.end_point[0] + 1,
149 "docstring": docstring or "",
150 })
 
 
 
151 store.create_edge(
152 NodeLabel.File, {"path": file_path},
 
153 EdgeType.CONTAINS,
154 NodeLabel.Class, {"name": name, "file_path": file_path},
 
155 )
156 stats["classes"] += 1
157 stats["edges"] += 1
158
159 # Inheritance
@@ -161,13 +189,15 @@
161 if child.type == "argument_list":
162 for arg in child.children:
163 if arg.type == "identifier":
164 parent_name = _node_text(arg, source)
165 store.create_edge(
166 NodeLabel.Class, {"name": name, "file_path": file_path},
 
167 EdgeType.INHERITS,
168 NodeLabel.Class, {"name": parent_name, "file_path": file_path},
 
169 )
170 stats["edges"] += 1
171
172 # Walk class body for methods
173 body = next((c for c in node.children if c.type == "block"), None)
@@ -174,12 +204,19 @@
174 if body:
175 for child in body.children:
176 if child.type == "function_definition":
177 self._handle_function(child, source, file_path, store, stats, class_name=name)
178
179 def _handle_function(self, node, source: bytes, file_path: str,
180 store: GraphStore, stats: dict, class_name: str | None) -> None:
 
 
 
 
 
 
 
181 name_node = next((c for c in node.children if c.type == "identifier"), None)
182 if not name_node:
183 return
184 name = _node_text(name_node, source)
185 docstring = _get_docstring(node, source)
@@ -195,40 +232,51 @@
195 }
196 store.create_node(label, props)
197
198 container_label = NodeLabel.Class if class_name else NodeLabel.File
199 container_key = (
200 {"name": class_name, "file_path": file_path}
201 if class_name
202 else {"path": file_path}
203 )
204 store.create_edge(
205 container_label, container_key, EdgeType.CONTAINS, label,
 
 
 
206 {"name": name, "file_path": file_path},
207 )
208 stats["functions"] += 1
209 stats["edges"] += 1
210
211 # Call edges — find all call expressions in the body
212 self._extract_calls(node, source, file_path, name, label, store, stats)
213
214 def _extract_calls(self, fn_node, source: bytes, file_path: str, fn_name: str,
215 fn_label: str, store: GraphStore, stats: dict) -> None:
 
 
 
 
 
 
 
 
216 def walk_calls(node):
217 if node.type == "call":
218 func = next(
219 (c for c in node.children if c.type in ("identifier", "attribute")), None
220 )
221 if func:
222 callee_name = _node_text(func, source).split(".")[-1]
223 store.create_edge(
224 fn_label, {"name": fn_name, "file_path": file_path},
 
225 EdgeType.CALLS,
226 NodeLabel.Function, {"name": callee_name, "file_path": file_path},
 
227 )
228 stats["edges"] += 1
229 for child in node.children:
230 walk_calls(child)
231
232 body = next((c for c in fn_node.children if c.type == "block"), None)
233 if body:
234 walk_calls(body)
235
--- navegador/ingestion/python.py
+++ navegador/ingestion/python.py
@@ -15,25 +15,25 @@
15
16 def _get_python_language():
17 try:
18 import tree_sitter_python as tspython # type: ignore[import]
19 from tree_sitter import Language
20
21 return Language(tspython.language())
22 except ImportError as e:
23 raise ImportError("Install tree-sitter-python: pip install tree-sitter-python") from e
 
 
24
25
26 def _get_parser():
27 from tree_sitter import Parser # type: ignore[import]
28
29 parser = Parser(_get_python_language())
30 return parser
31
32
33 def _node_text(node, source: bytes) -> str:
34 return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
35
36
37 def _get_docstring(node, source: bytes) -> str | None:
38 """Extract the first string literal from a function/class body as docstring."""
39 body = next((c for c in node.children if c.type == "block"), None)
@@ -61,22 +61,32 @@
61 rel_path = str(path.relative_to(repo_root))
62
63 stats = {"functions": 0, "classes": 0, "edges": 0}
64
65 # File node
66 store.create_node(
67 NodeLabel.File,
68 {
69 "name": path.name,
70 "path": rel_path,
71 "language": "python",
72 "line_count": source.count(b"\n"),
73 },
74 )
75
76 self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
77 return stats
78
79 def _walk(
80 self,
81 node,
82 source: bytes,
83 file_path: str,
84 store: GraphStore,
85 stats: dict,
86 class_name: str | None,
87 ) -> None:
88 if node.type == "import_statement":
89 self._handle_import(node, source, file_path, store, stats)
90
91 elif node.type == "import_from_statement":
92 self._handle_import_from(node, source, file_path, store, stats)
@@ -90,70 +100,88 @@
100 return # function walker handles children
101
102 for child in node.children:
103 self._walk(child, source, file_path, store, stats, class_name)
104
105 def _handle_import(
106 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
107 ) -> None:
108 for child in node.children:
109 if child.type == "dotted_name":
110 name = _node_text(child, source)
111 store.create_node(
112 NodeLabel.Import,
113 {
114 "name": name,
115 "file_path": file_path,
116 "line_start": node.start_point[0] + 1,
117 "module": name,
118 },
119 )
120 store.create_edge(
121 NodeLabel.File,
122 {"path": file_path},
123 EdgeType.IMPORTS,
124 NodeLabel.Import,
125 {"name": name, "file_path": file_path},
126 )
127 stats["edges"] += 1
128
129 def _handle_import_from(
130 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
131 ) -> None:
132 module = ""
133 for child in node.children:
134 if child.type in ("dotted_name", "relative_import"):
135 module = _node_text(child, source)
136 break
137 for child in node.children:
138 if child.type == "import_from_member":
139 name = _node_text(child, source)
140 store.create_node(
141 NodeLabel.Import,
142 {
143 "name": name,
144 "file_path": file_path,
145 "line_start": node.start_point[0] + 1,
146 "module": module,
147 },
148 )
149 store.create_edge(
150 NodeLabel.File,
151 {"path": file_path},
152 EdgeType.IMPORTS,
153 NodeLabel.Import,
154 {"name": name, "file_path": file_path},
155 )
156 stats["edges"] += 1
157
158 def _handle_class(
159 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
160 ) -> None:
161 name_node = next((c for c in node.children if c.type == "identifier"), None)
162 if not name_node:
163 return
164 name = _node_text(name_node, source)
165 docstring = _get_docstring(node, source)
166
167 store.create_node(
168 NodeLabel.Class,
169 {
170 "name": name,
171 "file_path": file_path,
172 "line_start": node.start_point[0] + 1,
173 "line_end": node.end_point[0] + 1,
174 "docstring": docstring or "",
175 },
176 )
177 store.create_edge(
178 NodeLabel.File,
179 {"path": file_path},
180 EdgeType.CONTAINS,
181 NodeLabel.Class,
182 {"name": name, "file_path": file_path},
183 )
184 stats["classes"] += 1
185 stats["edges"] += 1
186
187 # Inheritance
@@ -161,13 +189,15 @@
189 if child.type == "argument_list":
190 for arg in child.children:
191 if arg.type == "identifier":
192 parent_name = _node_text(arg, source)
193 store.create_edge(
194 NodeLabel.Class,
195 {"name": name, "file_path": file_path},
196 EdgeType.INHERITS,
197 NodeLabel.Class,
198 {"name": parent_name, "file_path": file_path},
199 )
200 stats["edges"] += 1
201
202 # Walk class body for methods
203 body = next((c for c in node.children if c.type == "block"), None)
@@ -174,12 +204,19 @@
204 if body:
205 for child in body.children:
206 if child.type == "function_definition":
207 self._handle_function(child, source, file_path, store, stats, class_name=name)
208
209 def _handle_function(
210 self,
211 node,
212 source: bytes,
213 file_path: str,
214 store: GraphStore,
215 stats: dict,
216 class_name: str | None,
217 ) -> None:
218 name_node = next((c for c in node.children if c.type == "identifier"), None)
219 if not name_node:
220 return
221 name = _node_text(name_node, source)
222 docstring = _get_docstring(node, source)
@@ -195,40 +232,51 @@
232 }
233 store.create_node(label, props)
234
235 container_label = NodeLabel.Class if class_name else NodeLabel.File
236 container_key = (
237 {"name": class_name, "file_path": file_path} if class_name else {"path": file_path}
 
 
238 )
239 store.create_edge(
240 container_label,
241 container_key,
242 EdgeType.CONTAINS,
243 label,
244 {"name": name, "file_path": file_path},
245 )
246 stats["functions"] += 1
247 stats["edges"] += 1
248
249 # Call edges — find all call expressions in the body
250 self._extract_calls(node, source, file_path, name, label, store, stats)
251
252 def _extract_calls(
253 self,
254 fn_node,
255 source: bytes,
256 file_path: str,
257 fn_name: str,
258 fn_label: str,
259 store: GraphStore,
260 stats: dict,
261 ) -> None:
262 def walk_calls(node):
263 if node.type == "call":
264 func = next(
265 (c for c in node.children if c.type in ("identifier", "attribute")), None
266 )
267 if func:
268 callee_name = _node_text(func, source).split(".")[-1]
269 store.create_edge(
270 fn_label,
271 {"name": fn_name, "file_path": file_path},
272 EdgeType.CALLS,
273 NodeLabel.Function,
274 {"name": callee_name, "file_path": file_path},
275 )
276 stats["edges"] += 1
277 for child in node.children:
278 walk_calls(child)
279
280 body = next((c for c in fn_node.children if c.type == "block"), None)
281 if body:
282 walk_calls(body)
283
--- navegador/ingestion/rust.py
+++ navegador/ingestion/rust.py
@@ -15,17 +15,18 @@
1515
1616
def _get_rust_language():
1717
try:
1818
import tree_sitter_rust as tsrust # type: ignore[import]
1919
from tree_sitter import Language
20
+
2021
return Language(tsrust.language())
2122
except ImportError as e:
2223
raise ImportError("Install tree-sitter-rust: pip install tree-sitter-rust") from e
2324
2425
2526
def _node_text(node, source: bytes) -> str:
26
- return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
27
+ return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
2728
2829
2930
def _doc_comment(node, source: bytes) -> str:
3031
"""Collect preceding /// doc-comment lines from siblings."""
3132
parent = node.parent
@@ -33,11 +34,11 @@
3334
return ""
3435
siblings = list(parent.children)
3536
idx = next((i for i, c in enumerate(siblings) if c.id == node.id), -1)
3637
if idx <= 0:
3738
return ""
38
- lines = []
39
+ lines: list[str] = []
3940
for i in range(idx - 1, -1, -1):
4041
sib = siblings[i]
4142
raw = _node_text(sib, source)
4243
if sib.type == "line_comment" and raw.startswith("///"):
4344
lines.insert(0, raw.lstrip("/").strip())
@@ -49,32 +50,43 @@
4950
class RustParser(LanguageParser):
5051
"""Parses Rust source files into the navegador graph."""
5152
5253
def __init__(self) -> None:
5354
from tree_sitter import Parser # type: ignore[import]
55
+
5456
self._parser = Parser(_get_rust_language())
5557
5658
def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
5759
source = path.read_bytes()
5860
tree = self._parser.parse(source)
5961
rel_path = str(path.relative_to(repo_root))
6062
61
- store.create_node(NodeLabel.File, {
62
- "name": path.name,
63
- "path": rel_path,
64
- "language": "rust",
65
- "line_count": source.count(b"\n"),
66
- })
63
+ store.create_node(
64
+ NodeLabel.File,
65
+ {
66
+ "name": path.name,
67
+ "path": rel_path,
68
+ "language": "rust",
69
+ "line_count": source.count(b"\n"),
70
+ },
71
+ )
6772
6873
stats = {"functions": 0, "classes": 0, "edges": 0}
6974
self._walk(tree.root_node, source, rel_path, store, stats, impl_type=None)
7075
return stats
7176
7277
# ── AST walker ────────────────────────────────────────────────────────────
7378
74
- def _walk(self, node, source: bytes, file_path: str,
75
- store: GraphStore, stats: dict, impl_type: str | None) -> None:
79
+ def _walk(
80
+ self,
81
+ node,
82
+ source: bytes,
83
+ file_path: str,
84
+ store: GraphStore,
85
+ stats: dict,
86
+ impl_type: str | None,
87
+ ) -> None:
7688
if node.type == "function_item":
7789
self._handle_function(node, source, file_path, store, stats, impl_type)
7890
return
7991
if node.type == "impl_item":
8092
self._handle_impl(node, source, file_path, store, stats)
@@ -88,115 +100,149 @@
88100
for child in node.children:
89101
self._walk(child, source, file_path, store, stats, impl_type)
90102
91103
# ── Handlers ──────────────────────────────────────────────────────────────
92104
93
- def _handle_function(self, node, source: bytes, file_path: str,
94
- store: GraphStore, stats: dict,
95
- impl_type: str | None) -> None:
105
+ def _handle_function(
106
+ self,
107
+ node,
108
+ source: bytes,
109
+ file_path: str,
110
+ store: GraphStore,
111
+ stats: dict,
112
+ impl_type: str | None,
113
+ ) -> None:
96114
name_node = node.child_by_field_name("name")
97115
if not name_node:
98116
return
99117
name = _node_text(name_node, source)
100118
docstring = _doc_comment(node, source)
101119
label = NodeLabel.Method if impl_type else NodeLabel.Function
102120
103
- store.create_node(label, {
104
- "name": name,
105
- "file_path": file_path,
106
- "line_start": node.start_point[0] + 1,
107
- "line_end": node.end_point[0] + 1,
108
- "docstring": docstring,
109
- "class_name": impl_type or "",
110
- })
121
+ store.create_node(
122
+ label,
123
+ {
124
+ "name": name,
125
+ "file_path": file_path,
126
+ "line_start": node.start_point[0] + 1,
127
+ "line_end": node.end_point[0] + 1,
128
+ "docstring": docstring,
129
+ "class_name": impl_type or "",
130
+ },
131
+ )
111132
112133
container_label = NodeLabel.Class if impl_type else NodeLabel.File
113134
container_key = (
114
- {"name": impl_type, "file_path": file_path}
115
- if impl_type else {"path": file_path}
135
+ {"name": impl_type, "file_path": file_path} if impl_type else {"path": file_path}
116136
)
117137
store.create_edge(
118
- container_label, container_key,
138
+ container_label,
139
+ container_key,
119140
EdgeType.CONTAINS,
120
- label, {"name": name, "file_path": file_path},
141
+ label,
142
+ {"name": name, "file_path": file_path},
121143
)
122144
stats["functions"] += 1
123145
stats["edges"] += 1
124146
125147
self._extract_calls(node, source, file_path, name, label, store, stats)
126148
127
- def _handle_impl(self, node, source: bytes, file_path: str,
128
- store: GraphStore, stats: dict) -> None:
149
+ def _handle_impl(
150
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
151
+ ) -> None:
129152
type_node = node.child_by_field_name("type")
130153
impl_type_name = _node_text(type_node, source) if type_node else ""
131154
132155
body = node.child_by_field_name("body")
133156
if body:
134157
for child in body.children:
135158
if child.type == "function_item":
136
- self._handle_function(child, source, file_path, store, stats,
137
- impl_type=impl_type_name or None)
159
+ self._handle_function(
160
+ child, source, file_path, store, stats, impl_type=impl_type_name or None
161
+ )
138162
139
- def _handle_type(self, node, source: bytes, file_path: str,
140
- store: GraphStore, stats: dict) -> None:
163
+ def _handle_type(
164
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
165
+ ) -> None:
141166
name_node = node.child_by_field_name("name")
142167
if not name_node:
143168
return
144169
name = _node_text(name_node, source)
145
- kind = {"struct_item": "struct", "enum_item": "enum",
146
- "trait_item": "trait"}.get(node.type, "")
170
+ kind = {"struct_item": "struct", "enum_item": "enum", "trait_item": "trait"}.get(
171
+ node.type, ""
172
+ )
147173
docstring = _doc_comment(node, source)
148174
149
- store.create_node(NodeLabel.Class, {
150
- "name": name,
151
- "file_path": file_path,
152
- "line_start": node.start_point[0] + 1,
153
- "line_end": node.end_point[0] + 1,
154
- "docstring": f"{kind}: {docstring}".strip(": ") if kind else docstring,
155
- })
175
+ store.create_node(
176
+ NodeLabel.Class,
177
+ {
178
+ "name": name,
179
+ "file_path": file_path,
180
+ "line_start": node.start_point[0] + 1,
181
+ "line_end": node.end_point[0] + 1,
182
+ "docstring": f"{kind}: {docstring}".strip(": ") if kind else docstring,
183
+ },
184
+ )
156185
store.create_edge(
157
- NodeLabel.File, {"path": file_path},
186
+ NodeLabel.File,
187
+ {"path": file_path},
158188
EdgeType.CONTAINS,
159
- NodeLabel.Class, {"name": name, "file_path": file_path},
189
+ NodeLabel.Class,
190
+ {"name": name, "file_path": file_path},
160191
)
161192
stats["classes"] += 1
162193
stats["edges"] += 1
163194
164
- def _handle_use(self, node, source: bytes, file_path: str,
165
- store: GraphStore, stats: dict) -> None:
195
+ def _handle_use(
196
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
197
+ ) -> None:
166198
raw = _node_text(node, source)
167199
module = raw.removeprefix("use ").removesuffix(";").strip()
168
- store.create_node(NodeLabel.Import, {
169
- "name": module,
170
- "file_path": file_path,
171
- "line_start": node.start_point[0] + 1,
172
- "module": module,
173
- })
200
+ store.create_node(
201
+ NodeLabel.Import,
202
+ {
203
+ "name": module,
204
+ "file_path": file_path,
205
+ "line_start": node.start_point[0] + 1,
206
+ "module": module,
207
+ },
208
+ )
174209
store.create_edge(
175
- NodeLabel.File, {"path": file_path},
210
+ NodeLabel.File,
211
+ {"path": file_path},
176212
EdgeType.IMPORTS,
177
- NodeLabel.Import, {"name": module, "file_path": file_path},
213
+ NodeLabel.Import,
214
+ {"name": module, "file_path": file_path},
178215
)
179216
stats["edges"] += 1
180217
181
- def _extract_calls(self, fn_node, source: bytes, file_path: str,
182
- fn_name: str, fn_label: str,
183
- store: GraphStore, stats: dict) -> None:
218
+ def _extract_calls(
219
+ self,
220
+ fn_node,
221
+ source: bytes,
222
+ file_path: str,
223
+ fn_name: str,
224
+ fn_label: str,
225
+ store: GraphStore,
226
+ stats: dict,
227
+ ) -> None:
184228
def walk(node):
185229
if node.type == "call_expression":
186230
func = node.child_by_field_name("function")
187231
if func:
188232
text = _node_text(func, source)
189233
# Handle Foo::bar() and obj.method()
190234
callee = text.replace("::", ".").split(".")[-1]
191235
store.create_edge(
192
- fn_label, {"name": fn_name, "file_path": file_path},
236
+ fn_label,
237
+ {"name": fn_name, "file_path": file_path},
193238
EdgeType.CALLS,
194
- NodeLabel.Function, {"name": callee, "file_path": file_path},
239
+ NodeLabel.Function,
240
+ {"name": callee, "file_path": file_path},
195241
)
196242
stats["edges"] += 1
197243
for child in node.children:
198244
walk(child)
199245
200246
body = fn_node.child_by_field_name("body")
201247
if body:
202248
walk(body)
203249
--- navegador/ingestion/rust.py
+++ navegador/ingestion/rust.py
@@ -15,17 +15,18 @@
15
16 def _get_rust_language():
17 try:
18 import tree_sitter_rust as tsrust # type: ignore[import]
19 from tree_sitter import Language
 
20 return Language(tsrust.language())
21 except ImportError as e:
22 raise ImportError("Install tree-sitter-rust: pip install tree-sitter-rust") from e
23
24
25 def _node_text(node, source: bytes) -> str:
26 return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
27
28
29 def _doc_comment(node, source: bytes) -> str:
30 """Collect preceding /// doc-comment lines from siblings."""
31 parent = node.parent
@@ -33,11 +34,11 @@
33 return ""
34 siblings = list(parent.children)
35 idx = next((i for i, c in enumerate(siblings) if c.id == node.id), -1)
36 if idx <= 0:
37 return ""
38 lines = []
39 for i in range(idx - 1, -1, -1):
40 sib = siblings[i]
41 raw = _node_text(sib, source)
42 if sib.type == "line_comment" and raw.startswith("///"):
43 lines.insert(0, raw.lstrip("/").strip())
@@ -49,32 +50,43 @@
49 class RustParser(LanguageParser):
50 """Parses Rust source files into the navegador graph."""
51
52 def __init__(self) -> None:
53 from tree_sitter import Parser # type: ignore[import]
 
54 self._parser = Parser(_get_rust_language())
55
56 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
57 source = path.read_bytes()
58 tree = self._parser.parse(source)
59 rel_path = str(path.relative_to(repo_root))
60
61 store.create_node(NodeLabel.File, {
62 "name": path.name,
63 "path": rel_path,
64 "language": "rust",
65 "line_count": source.count(b"\n"),
66 })
 
 
 
67
68 stats = {"functions": 0, "classes": 0, "edges": 0}
69 self._walk(tree.root_node, source, rel_path, store, stats, impl_type=None)
70 return stats
71
72 # ── AST walker ────────────────────────────────────────────────────────────
73
74 def _walk(self, node, source: bytes, file_path: str,
75 store: GraphStore, stats: dict, impl_type: str | None) -> None:
 
 
 
 
 
 
 
76 if node.type == "function_item":
77 self._handle_function(node, source, file_path, store, stats, impl_type)
78 return
79 if node.type == "impl_item":
80 self._handle_impl(node, source, file_path, store, stats)
@@ -88,115 +100,149 @@
88 for child in node.children:
89 self._walk(child, source, file_path, store, stats, impl_type)
90
91 # ── Handlers ──────────────────────────────────────────────────────────────
92
93 def _handle_function(self, node, source: bytes, file_path: str,
94 store: GraphStore, stats: dict,
95 impl_type: str | None) -> None:
 
 
 
 
 
 
96 name_node = node.child_by_field_name("name")
97 if not name_node:
98 return
99 name = _node_text(name_node, source)
100 docstring = _doc_comment(node, source)
101 label = NodeLabel.Method if impl_type else NodeLabel.Function
102
103 store.create_node(label, {
104 "name": name,
105 "file_path": file_path,
106 "line_start": node.start_point[0] + 1,
107 "line_end": node.end_point[0] + 1,
108 "docstring": docstring,
109 "class_name": impl_type or "",
110 })
 
 
 
111
112 container_label = NodeLabel.Class if impl_type else NodeLabel.File
113 container_key = (
114 {"name": impl_type, "file_path": file_path}
115 if impl_type else {"path": file_path}
116 )
117 store.create_edge(
118 container_label, container_key,
 
119 EdgeType.CONTAINS,
120 label, {"name": name, "file_path": file_path},
 
121 )
122 stats["functions"] += 1
123 stats["edges"] += 1
124
125 self._extract_calls(node, source, file_path, name, label, store, stats)
126
127 def _handle_impl(self, node, source: bytes, file_path: str,
128 store: GraphStore, stats: dict) -> None:
 
129 type_node = node.child_by_field_name("type")
130 impl_type_name = _node_text(type_node, source) if type_node else ""
131
132 body = node.child_by_field_name("body")
133 if body:
134 for child in body.children:
135 if child.type == "function_item":
136 self._handle_function(child, source, file_path, store, stats,
137 impl_type=impl_type_name or None)
 
138
139 def _handle_type(self, node, source: bytes, file_path: str,
140 store: GraphStore, stats: dict) -> None:
 
141 name_node = node.child_by_field_name("name")
142 if not name_node:
143 return
144 name = _node_text(name_node, source)
145 kind = {"struct_item": "struct", "enum_item": "enum",
146 "trait_item": "trait"}.get(node.type, "")
 
147 docstring = _doc_comment(node, source)
148
149 store.create_node(NodeLabel.Class, {
150 "name": name,
151 "file_path": file_path,
152 "line_start": node.start_point[0] + 1,
153 "line_end": node.end_point[0] + 1,
154 "docstring": f"{kind}: {docstring}".strip(": ") if kind else docstring,
155 })
 
 
 
156 store.create_edge(
157 NodeLabel.File, {"path": file_path},
 
158 EdgeType.CONTAINS,
159 NodeLabel.Class, {"name": name, "file_path": file_path},
 
160 )
161 stats["classes"] += 1
162 stats["edges"] += 1
163
164 def _handle_use(self, node, source: bytes, file_path: str,
165 store: GraphStore, stats: dict) -> None:
 
166 raw = _node_text(node, source)
167 module = raw.removeprefix("use ").removesuffix(";").strip()
168 store.create_node(NodeLabel.Import, {
169 "name": module,
170 "file_path": file_path,
171 "line_start": node.start_point[0] + 1,
172 "module": module,
173 })
 
 
 
174 store.create_edge(
175 NodeLabel.File, {"path": file_path},
 
176 EdgeType.IMPORTS,
177 NodeLabel.Import, {"name": module, "file_path": file_path},
 
178 )
179 stats["edges"] += 1
180
181 def _extract_calls(self, fn_node, source: bytes, file_path: str,
182 fn_name: str, fn_label: str,
183 store: GraphStore, stats: dict) -> None:
 
 
 
 
 
 
 
184 def walk(node):
185 if node.type == "call_expression":
186 func = node.child_by_field_name("function")
187 if func:
188 text = _node_text(func, source)
189 # Handle Foo::bar() and obj.method()
190 callee = text.replace("::", ".").split(".")[-1]
191 store.create_edge(
192 fn_label, {"name": fn_name, "file_path": file_path},
 
193 EdgeType.CALLS,
194 NodeLabel.Function, {"name": callee, "file_path": file_path},
 
195 )
196 stats["edges"] += 1
197 for child in node.children:
198 walk(child)
199
200 body = fn_node.child_by_field_name("body")
201 if body:
202 walk(body)
203
--- navegador/ingestion/rust.py
+++ navegador/ingestion/rust.py
@@ -15,17 +15,18 @@
15
16 def _get_rust_language():
17 try:
18 import tree_sitter_rust as tsrust # type: ignore[import]
19 from tree_sitter import Language
20
21 return Language(tsrust.language())
22 except ImportError as e:
23 raise ImportError("Install tree-sitter-rust: pip install tree-sitter-rust") from e
24
25
26 def _node_text(node, source: bytes) -> str:
27 return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
28
29
30 def _doc_comment(node, source: bytes) -> str:
31 """Collect preceding /// doc-comment lines from siblings."""
32 parent = node.parent
@@ -33,11 +34,11 @@
34 return ""
35 siblings = list(parent.children)
36 idx = next((i for i, c in enumerate(siblings) if c.id == node.id), -1)
37 if idx <= 0:
38 return ""
39 lines: list[str] = []
40 for i in range(idx - 1, -1, -1):
41 sib = siblings[i]
42 raw = _node_text(sib, source)
43 if sib.type == "line_comment" and raw.startswith("///"):
44 lines.insert(0, raw.lstrip("/").strip())
@@ -49,32 +50,43 @@
50 class RustParser(LanguageParser):
51 """Parses Rust source files into the navegador graph."""
52
53 def __init__(self) -> None:
54 from tree_sitter import Parser # type: ignore[import]
55
56 self._parser = Parser(_get_rust_language())
57
58 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
59 source = path.read_bytes()
60 tree = self._parser.parse(source)
61 rel_path = str(path.relative_to(repo_root))
62
63 store.create_node(
64 NodeLabel.File,
65 {
66 "name": path.name,
67 "path": rel_path,
68 "language": "rust",
69 "line_count": source.count(b"\n"),
70 },
71 )
72
73 stats = {"functions": 0, "classes": 0, "edges": 0}
74 self._walk(tree.root_node, source, rel_path, store, stats, impl_type=None)
75 return stats
76
77 # ── AST walker ────────────────────────────────────────────────────────────
78
79 def _walk(
80 self,
81 node,
82 source: bytes,
83 file_path: str,
84 store: GraphStore,
85 stats: dict,
86 impl_type: str | None,
87 ) -> None:
88 if node.type == "function_item":
89 self._handle_function(node, source, file_path, store, stats, impl_type)
90 return
91 if node.type == "impl_item":
92 self._handle_impl(node, source, file_path, store, stats)
@@ -88,115 +100,149 @@
100 for child in node.children:
101 self._walk(child, source, file_path, store, stats, impl_type)
102
103 # ── Handlers ──────────────────────────────────────────────────────────────
104
105 def _handle_function(
106 self,
107 node,
108 source: bytes,
109 file_path: str,
110 store: GraphStore,
111 stats: dict,
112 impl_type: str | None,
113 ) -> None:
114 name_node = node.child_by_field_name("name")
115 if not name_node:
116 return
117 name = _node_text(name_node, source)
118 docstring = _doc_comment(node, source)
119 label = NodeLabel.Method if impl_type else NodeLabel.Function
120
121 store.create_node(
122 label,
123 {
124 "name": name,
125 "file_path": file_path,
126 "line_start": node.start_point[0] + 1,
127 "line_end": node.end_point[0] + 1,
128 "docstring": docstring,
129 "class_name": impl_type or "",
130 },
131 )
132
133 container_label = NodeLabel.Class if impl_type else NodeLabel.File
134 container_key = (
135 {"name": impl_type, "file_path": file_path} if impl_type else {"path": file_path}
 
136 )
137 store.create_edge(
138 container_label,
139 container_key,
140 EdgeType.CONTAINS,
141 label,
142 {"name": name, "file_path": file_path},
143 )
144 stats["functions"] += 1
145 stats["edges"] += 1
146
147 self._extract_calls(node, source, file_path, name, label, store, stats)
148
149 def _handle_impl(
150 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
151 ) -> None:
152 type_node = node.child_by_field_name("type")
153 impl_type_name = _node_text(type_node, source) if type_node else ""
154
155 body = node.child_by_field_name("body")
156 if body:
157 for child in body.children:
158 if child.type == "function_item":
159 self._handle_function(
160 child, source, file_path, store, stats, impl_type=impl_type_name or None
161 )
162
163 def _handle_type(
164 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
165 ) -> None:
166 name_node = node.child_by_field_name("name")
167 if not name_node:
168 return
169 name = _node_text(name_node, source)
170 kind = {"struct_item": "struct", "enum_item": "enum", "trait_item": "trait"}.get(
171 node.type, ""
172 )
173 docstring = _doc_comment(node, source)
174
175 store.create_node(
176 NodeLabel.Class,
177 {
178 "name": name,
179 "file_path": file_path,
180 "line_start": node.start_point[0] + 1,
181 "line_end": node.end_point[0] + 1,
182 "docstring": f"{kind}: {docstring}".strip(": ") if kind else docstring,
183 },
184 )
185 store.create_edge(
186 NodeLabel.File,
187 {"path": file_path},
188 EdgeType.CONTAINS,
189 NodeLabel.Class,
190 {"name": name, "file_path": file_path},
191 )
192 stats["classes"] += 1
193 stats["edges"] += 1
194
195 def _handle_use(
196 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
197 ) -> None:
198 raw = _node_text(node, source)
199 module = raw.removeprefix("use ").removesuffix(";").strip()
200 store.create_node(
201 NodeLabel.Import,
202 {
203 "name": module,
204 "file_path": file_path,
205 "line_start": node.start_point[0] + 1,
206 "module": module,
207 },
208 )
209 store.create_edge(
210 NodeLabel.File,
211 {"path": file_path},
212 EdgeType.IMPORTS,
213 NodeLabel.Import,
214 {"name": module, "file_path": file_path},
215 )
216 stats["edges"] += 1
217
218 def _extract_calls(
219 self,
220 fn_node,
221 source: bytes,
222 file_path: str,
223 fn_name: str,
224 fn_label: str,
225 store: GraphStore,
226 stats: dict,
227 ) -> None:
228 def walk(node):
229 if node.type == "call_expression":
230 func = node.child_by_field_name("function")
231 if func:
232 text = _node_text(func, source)
233 # Handle Foo::bar() and obj.method()
234 callee = text.replace("::", ".").split(".")[-1]
235 store.create_edge(
236 fn_label,
237 {"name": fn_name, "file_path": file_path},
238 EdgeType.CALLS,
239 NodeLabel.Function,
240 {"name": callee, "file_path": file_path},
241 )
242 stats["edges"] += 1
243 for child in node.children:
244 walk(child)
245
246 body = fn_node.child_by_field_name("body")
247 if body:
248 walk(body)
249
--- navegador/ingestion/typescript.py
+++ navegador/ingestion/typescript.py
@@ -20,23 +20,25 @@
2020
def _get_ts_language(language: str):
2121
try:
2222
if language == "typescript":
2323
import tree_sitter_typescript as tsts # type: ignore[import]
2424
from tree_sitter import Language
25
+
2526
return Language(tsts.language_typescript())
2627
else:
2728
import tree_sitter_javascript as tsjs # type: ignore[import]
2829
from tree_sitter import Language
30
+
2931
return Language(tsjs.language())
3032
except ImportError as e:
3133
raise ImportError(
3234
f"Install tree-sitter-{language}: pip install tree-sitter-{language}"
3335
) from e
3436
3537
3638
def _node_text(node, source: bytes) -> str:
37
- return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
39
+ return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
3840
3941
4042
def _jsdoc(node, source: bytes) -> str:
4143
"""Return the JSDoc comment (/** ... */) preceding a node, if any."""
4244
parent = node.parent
@@ -57,33 +59,44 @@
5759
class TypeScriptParser(LanguageParser):
5860
"""Parses TypeScript/JavaScript source files into the navegador graph."""
5961
6062
def __init__(self, language: str = "typescript") -> None:
6163
from tree_sitter import Parser # type: ignore[import]
64
+
6265
self._parser = Parser(_get_ts_language(language))
6366
self._language = language
6467
6568
def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
6669
source = path.read_bytes()
6770
tree = self._parser.parse(source)
6871
rel_path = str(path.relative_to(repo_root))
6972
70
- store.create_node(NodeLabel.File, {
71
- "name": path.name,
72
- "path": rel_path,
73
- "language": self._language,
74
- "line_count": source.count(b"\n"),
75
- })
73
+ store.create_node(
74
+ NodeLabel.File,
75
+ {
76
+ "name": path.name,
77
+ "path": rel_path,
78
+ "language": self._language,
79
+ "line_count": source.count(b"\n"),
80
+ },
81
+ )
7682
7783
stats = {"functions": 0, "classes": 0, "edges": 0}
7884
self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
7985
return stats
8086
8187
# ── AST walker ────────────────────────────────────────────────────────────
8288
83
- def _walk(self, node, source: bytes, file_path: str,
84
- store: GraphStore, stats: dict, class_name: str | None) -> None:
89
+ def _walk(
90
+ self,
91
+ node,
92
+ source: bytes,
93
+ file_path: str,
94
+ store: GraphStore,
95
+ stats: dict,
96
+ class_name: str | None,
97
+ ) -> None:
8598
if node.type in ("class_declaration", "abstract_class_declaration"):
8699
self._handle_class(node, source, file_path, store, stats)
87100
return
88101
if node.type in ("interface_declaration", "type_alias_declaration"):
89102
self._handle_interface(node, source, file_path, store, stats)
@@ -110,31 +123,35 @@
110123
for child in node.children:
111124
self._walk(child, source, file_path, store, stats, class_name)
112125
113126
# ── Handlers ──────────────────────────────────────────────────────────────
114127
115
- def _handle_class(self, node, source: bytes, file_path: str,
116
- store: GraphStore, stats: dict) -> None:
117
- name_node = next(
118
- (c for c in node.children if c.type == "type_identifier"), None
119
- )
128
+ def _handle_class(
129
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
130
+ ) -> None:
131
+ name_node = next((c for c in node.children if c.type == "type_identifier"), None)
120132
if not name_node:
121133
return
122134
name = _node_text(name_node, source)
123135
docstring = _jsdoc(node, source)
124136
125
- store.create_node(NodeLabel.Class, {
126
- "name": name,
127
- "file_path": file_path,
128
- "line_start": node.start_point[0] + 1,
129
- "line_end": node.end_point[0] + 1,
130
- "docstring": docstring,
131
- })
137
+ store.create_node(
138
+ NodeLabel.Class,
139
+ {
140
+ "name": name,
141
+ "file_path": file_path,
142
+ "line_start": node.start_point[0] + 1,
143
+ "line_end": node.end_point[0] + 1,
144
+ "docstring": docstring,
145
+ },
146
+ )
132147
store.create_edge(
133
- NodeLabel.File, {"path": file_path},
148
+ NodeLabel.File,
149
+ {"path": file_path},
134150
EdgeType.CONTAINS,
135
- NodeLabel.Class, {"name": name, "file_path": file_path},
151
+ NodeLabel.Class,
152
+ {"name": name, "file_path": file_path},
136153
)
137154
stats["classes"] += 1
138155
stats["edges"] += 1
139156
140157
# Inheritance: extends clause
@@ -144,172 +161,209 @@
144161
if child.type == "extends_clause":
145162
for c in child.children:
146163
if c.type == "identifier":
147164
parent_name = _node_text(c, source)
148165
store.create_edge(
149
- NodeLabel.Class, {"name": name, "file_path": file_path},
166
+ NodeLabel.Class,
167
+ {"name": name, "file_path": file_path},
150168
EdgeType.INHERITS,
151
- NodeLabel.Class, {"name": parent_name, "file_path": file_path},
169
+ NodeLabel.Class,
170
+ {"name": parent_name, "file_path": file_path},
152171
)
153172
stats["edges"] += 1
154173
155174
body = next((c for c in node.children if c.type == "class_body"), None)
156175
if body:
157176
for child in body.children:
158177
if child.type == "method_definition":
159
- self._handle_function(child, source, file_path, store, stats,
160
- class_name=name)
161
-
162
- def _handle_interface(self, node, source: bytes, file_path: str,
163
- store: GraphStore, stats: dict) -> None:
164
- name_node = next(
165
- (c for c in node.children if c.type == "type_identifier"), None
166
- )
178
+ self._handle_function(child, source, file_path, store, stats, class_name=name)
179
+
180
+ def _handle_interface(
181
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
182
+ ) -> None:
183
+ name_node = next((c for c in node.children if c.type == "type_identifier"), None)
167184
if not name_node:
168185
return
169186
name = _node_text(name_node, source)
170187
docstring = _jsdoc(node, source)
171188
kind = "interface" if node.type == "interface_declaration" else "type"
172189
173
- store.create_node(NodeLabel.Class, {
174
- "name": name,
175
- "file_path": file_path,
176
- "line_start": node.start_point[0] + 1,
177
- "line_end": node.end_point[0] + 1,
178
- "docstring": f"{kind}: {docstring}".strip(": "),
179
- })
190
+ store.create_node(
191
+ NodeLabel.Class,
192
+ {
193
+ "name": name,
194
+ "file_path": file_path,
195
+ "line_start": node.start_point[0] + 1,
196
+ "line_end": node.end_point[0] + 1,
197
+ "docstring": f"{kind}: {docstring}".strip(": "),
198
+ },
199
+ )
180200
store.create_edge(
181
- NodeLabel.File, {"path": file_path},
201
+ NodeLabel.File,
202
+ {"path": file_path},
182203
EdgeType.CONTAINS,
183
- NodeLabel.Class, {"name": name, "file_path": file_path},
204
+ NodeLabel.Class,
205
+ {"name": name, "file_path": file_path},
184206
)
185207
stats["classes"] += 1
186208
stats["edges"] += 1
187209
188
- def _handle_function(self, node, source: bytes, file_path: str,
189
- store: GraphStore, stats: dict,
190
- class_name: str | None) -> None:
210
+ def _handle_function(
211
+ self,
212
+ node,
213
+ source: bytes,
214
+ file_path: str,
215
+ store: GraphStore,
216
+ stats: dict,
217
+ class_name: str | None,
218
+ ) -> None:
191219
name_node = next(
192
- (c for c in node.children
193
- if c.type in ("identifier", "property_identifier")), None
220
+ (c for c in node.children if c.type in ("identifier", "property_identifier")), None
194221
)
195222
if not name_node:
196223
return
197224
name = _node_text(name_node, source)
198225
if name in ("constructor", "get", "set", "static", "async"):
199226
# These are keywords, not useful names — look for next identifier
200227
name_node = next(
201
- (c for c in node.children
202
- if c.type in ("identifier", "property_identifier") and c != name_node),
228
+ (
229
+ c
230
+ for c in node.children
231
+ if c.type in ("identifier", "property_identifier") and c != name_node
232
+ ),
203233
None,
204234
)
205235
if not name_node:
206236
return
207237
name = _node_text(name_node, source)
208238
209239
docstring = _jsdoc(node, source)
210240
label = NodeLabel.Method if class_name else NodeLabel.Function
211241
212
- store.create_node(label, {
213
- "name": name,
214
- "file_path": file_path,
215
- "line_start": node.start_point[0] + 1,
216
- "line_end": node.end_point[0] + 1,
217
- "docstring": docstring,
218
- "class_name": class_name or "",
219
- })
242
+ store.create_node(
243
+ label,
244
+ {
245
+ "name": name,
246
+ "file_path": file_path,
247
+ "line_start": node.start_point[0] + 1,
248
+ "line_end": node.end_point[0] + 1,
249
+ "docstring": docstring,
250
+ "class_name": class_name or "",
251
+ },
252
+ )
220253
221254
container_label = NodeLabel.Class if class_name else NodeLabel.File
222255
container_key = (
223
- {"name": class_name, "file_path": file_path}
224
- if class_name else {"path": file_path}
256
+ {"name": class_name, "file_path": file_path} if class_name else {"path": file_path}
225257
)
226258
store.create_edge(
227
- container_label, container_key,
259
+ container_label,
260
+ container_key,
228261
EdgeType.CONTAINS,
229
- label, {"name": name, "file_path": file_path},
262
+ label,
263
+ {"name": name, "file_path": file_path},
230264
)
231265
stats["functions"] += 1
232266
stats["edges"] += 1
233267
234268
self._extract_calls(node, source, file_path, name, label, store, stats)
235269
236
- def _handle_lexical(self, node, source: bytes, file_path: str,
237
- store: GraphStore, stats: dict) -> None:
270
+ def _handle_lexical(
271
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
272
+ ) -> None:
238273
"""Handle: const foo = () => {} and const bar = function() {}"""
239274
for child in node.children:
240275
if child.type != "variable_declarator":
241276
continue
242277
name_node = child.child_by_field_name("name")
243278
value_node = child.child_by_field_name("value")
244279
if not name_node or not value_node:
245280
continue
246
- if value_node.type not in ("arrow_function", "function_expression",
247
- "function"):
281
+ if value_node.type not in ("arrow_function", "function_expression", "function"):
248282
continue
249283
name = _node_text(name_node, source)
250284
docstring = _jsdoc(node, source)
251285
252
- store.create_node(NodeLabel.Function, {
253
- "name": name,
254
- "file_path": file_path,
255
- "line_start": node.start_point[0] + 1,
256
- "line_end": node.end_point[0] + 1,
257
- "docstring": docstring,
258
- "class_name": "",
259
- })
286
+ store.create_node(
287
+ NodeLabel.Function,
288
+ {
289
+ "name": name,
290
+ "file_path": file_path,
291
+ "line_start": node.start_point[0] + 1,
292
+ "line_end": node.end_point[0] + 1,
293
+ "docstring": docstring,
294
+ "class_name": "",
295
+ },
296
+ )
260297
store.create_edge(
261
- NodeLabel.File, {"path": file_path},
298
+ NodeLabel.File,
299
+ {"path": file_path},
262300
EdgeType.CONTAINS,
263
- NodeLabel.Function, {"name": name, "file_path": file_path},
301
+ NodeLabel.Function,
302
+ {"name": name, "file_path": file_path},
264303
)
265304
stats["functions"] += 1
266305
stats["edges"] += 1
267306
268
- self._extract_calls(value_node, source, file_path, name,
269
- NodeLabel.Function, store, stats)
307
+ self._extract_calls(
308
+ value_node, source, file_path, name, NodeLabel.Function, store, stats
309
+ )
270310
271
- def _handle_import(self, node, source: bytes, file_path: str,
272
- store: GraphStore, stats: dict) -> None:
311
+ def _handle_import(
312
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
313
+ ) -> None:
273314
for child in node.children:
274315
if child.type == "string":
275316
module = _node_text(child, source).strip("'\"")
276
- store.create_node(NodeLabel.Import, {
277
- "name": module,
278
- "file_path": file_path,
279
- "line_start": node.start_point[0] + 1,
280
- "module": module,
281
- })
317
+ store.create_node(
318
+ NodeLabel.Import,
319
+ {
320
+ "name": module,
321
+ "file_path": file_path,
322
+ "line_start": node.start_point[0] + 1,
323
+ "module": module,
324
+ },
325
+ )
282326
store.create_edge(
283
- NodeLabel.File, {"path": file_path},
327
+ NodeLabel.File,
328
+ {"path": file_path},
284329
EdgeType.IMPORTS,
285
- NodeLabel.Import, {"name": module, "file_path": file_path},
330
+ NodeLabel.Import,
331
+ {"name": module, "file_path": file_path},
286332
)
287333
stats["edges"] += 1
288334
break
289335
290
- def _extract_calls(self, fn_node, source: bytes, file_path: str,
291
- fn_name: str, fn_label: str,
292
- store: GraphStore, stats: dict) -> None:
336
+ def _extract_calls(
337
+ self,
338
+ fn_node,
339
+ source: bytes,
340
+ file_path: str,
341
+ fn_name: str,
342
+ fn_label: str,
343
+ store: GraphStore,
344
+ stats: dict,
345
+ ) -> None:
293346
def walk(node):
294347
if node.type == "call_expression":
295348
func = node.child_by_field_name("function")
296349
if func:
297350
text = _node_text(func, source)
298351
callee = text.split(".")[-1]
299352
store.create_edge(
300
- fn_label, {"name": fn_name, "file_path": file_path},
353
+ fn_label,
354
+ {"name": fn_name, "file_path": file_path},
301355
EdgeType.CALLS,
302
- NodeLabel.Function, {"name": callee, "file_path": file_path},
356
+ NodeLabel.Function,
357
+ {"name": callee, "file_path": file_path},
303358
)
304359
stats["edges"] += 1
305360
for child in node.children:
306361
walk(child)
307362
308363
# Body is statement_block for functions, expression for arrow fns
309364
body = next(
310
- (c for c in fn_node.children
311
- if c.type in ("statement_block", "expression_statement")),
365
+ (c for c in fn_node.children if c.type in ("statement_block", "expression_statement")),
312366
None,
313367
)
314368
if body:
315369
walk(body)
316370
--- navegador/ingestion/typescript.py
+++ navegador/ingestion/typescript.py
@@ -20,23 +20,25 @@
20 def _get_ts_language(language: str):
21 try:
22 if language == "typescript":
23 import tree_sitter_typescript as tsts # type: ignore[import]
24 from tree_sitter import Language
 
25 return Language(tsts.language_typescript())
26 else:
27 import tree_sitter_javascript as tsjs # type: ignore[import]
28 from tree_sitter import Language
 
29 return Language(tsjs.language())
30 except ImportError as e:
31 raise ImportError(
32 f"Install tree-sitter-{language}: pip install tree-sitter-{language}"
33 ) from e
34
35
36 def _node_text(node, source: bytes) -> str:
37 return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
38
39
40 def _jsdoc(node, source: bytes) -> str:
41 """Return the JSDoc comment (/** ... */) preceding a node, if any."""
42 parent = node.parent
@@ -57,33 +59,44 @@
57 class TypeScriptParser(LanguageParser):
58 """Parses TypeScript/JavaScript source files into the navegador graph."""
59
60 def __init__(self, language: str = "typescript") -> None:
61 from tree_sitter import Parser # type: ignore[import]
 
62 self._parser = Parser(_get_ts_language(language))
63 self._language = language
64
65 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
66 source = path.read_bytes()
67 tree = self._parser.parse(source)
68 rel_path = str(path.relative_to(repo_root))
69
70 store.create_node(NodeLabel.File, {
71 "name": path.name,
72 "path": rel_path,
73 "language": self._language,
74 "line_count": source.count(b"\n"),
75 })
 
 
 
76
77 stats = {"functions": 0, "classes": 0, "edges": 0}
78 self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
79 return stats
80
81 # ── AST walker ────────────────────────────────────────────────────────────
82
83 def _walk(self, node, source: bytes, file_path: str,
84 store: GraphStore, stats: dict, class_name: str | None) -> None:
 
 
 
 
 
 
 
85 if node.type in ("class_declaration", "abstract_class_declaration"):
86 self._handle_class(node, source, file_path, store, stats)
87 return
88 if node.type in ("interface_declaration", "type_alias_declaration"):
89 self._handle_interface(node, source, file_path, store, stats)
@@ -110,31 +123,35 @@
110 for child in node.children:
111 self._walk(child, source, file_path, store, stats, class_name)
112
113 # ── Handlers ──────────────────────────────────────────────────────────────
114
115 def _handle_class(self, node, source: bytes, file_path: str,
116 store: GraphStore, stats: dict) -> None:
117 name_node = next(
118 (c for c in node.children if c.type == "type_identifier"), None
119 )
120 if not name_node:
121 return
122 name = _node_text(name_node, source)
123 docstring = _jsdoc(node, source)
124
125 store.create_node(NodeLabel.Class, {
126 "name": name,
127 "file_path": file_path,
128 "line_start": node.start_point[0] + 1,
129 "line_end": node.end_point[0] + 1,
130 "docstring": docstring,
131 })
 
 
 
132 store.create_edge(
133 NodeLabel.File, {"path": file_path},
 
134 EdgeType.CONTAINS,
135 NodeLabel.Class, {"name": name, "file_path": file_path},
 
136 )
137 stats["classes"] += 1
138 stats["edges"] += 1
139
140 # Inheritance: extends clause
@@ -144,172 +161,209 @@
144 if child.type == "extends_clause":
145 for c in child.children:
146 if c.type == "identifier":
147 parent_name = _node_text(c, source)
148 store.create_edge(
149 NodeLabel.Class, {"name": name, "file_path": file_path},
 
150 EdgeType.INHERITS,
151 NodeLabel.Class, {"name": parent_name, "file_path": file_path},
 
152 )
153 stats["edges"] += 1
154
155 body = next((c for c in node.children if c.type == "class_body"), None)
156 if body:
157 for child in body.children:
158 if child.type == "method_definition":
159 self._handle_function(child, source, file_path, store, stats,
160 class_name=name)
161
162 def _handle_interface(self, node, source: bytes, file_path: str,
163 store: GraphStore, stats: dict) -> None:
164 name_node = next(
165 (c for c in node.children if c.type == "type_identifier"), None
166 )
167 if not name_node:
168 return
169 name = _node_text(name_node, source)
170 docstring = _jsdoc(node, source)
171 kind = "interface" if node.type == "interface_declaration" else "type"
172
173 store.create_node(NodeLabel.Class, {
174 "name": name,
175 "file_path": file_path,
176 "line_start": node.start_point[0] + 1,
177 "line_end": node.end_point[0] + 1,
178 "docstring": f"{kind}: {docstring}".strip(": "),
179 })
 
 
 
180 store.create_edge(
181 NodeLabel.File, {"path": file_path},
 
182 EdgeType.CONTAINS,
183 NodeLabel.Class, {"name": name, "file_path": file_path},
 
184 )
185 stats["classes"] += 1
186 stats["edges"] += 1
187
188 def _handle_function(self, node, source: bytes, file_path: str,
189 store: GraphStore, stats: dict,
190 class_name: str | None) -> None:
 
 
 
 
 
 
191 name_node = next(
192 (c for c in node.children
193 if c.type in ("identifier", "property_identifier")), None
194 )
195 if not name_node:
196 return
197 name = _node_text(name_node, source)
198 if name in ("constructor", "get", "set", "static", "async"):
199 # These are keywords, not useful names — look for next identifier
200 name_node = next(
201 (c for c in node.children
202 if c.type in ("identifier", "property_identifier") and c != name_node),
 
 
 
203 None,
204 )
205 if not name_node:
206 return
207 name = _node_text(name_node, source)
208
209 docstring = _jsdoc(node, source)
210 label = NodeLabel.Method if class_name else NodeLabel.Function
211
212 store.create_node(label, {
213 "name": name,
214 "file_path": file_path,
215 "line_start": node.start_point[0] + 1,
216 "line_end": node.end_point[0] + 1,
217 "docstring": docstring,
218 "class_name": class_name or "",
219 })
 
 
 
220
221 container_label = NodeLabel.Class if class_name else NodeLabel.File
222 container_key = (
223 {"name": class_name, "file_path": file_path}
224 if class_name else {"path": file_path}
225 )
226 store.create_edge(
227 container_label, container_key,
 
228 EdgeType.CONTAINS,
229 label, {"name": name, "file_path": file_path},
 
230 )
231 stats["functions"] += 1
232 stats["edges"] += 1
233
234 self._extract_calls(node, source, file_path, name, label, store, stats)
235
236 def _handle_lexical(self, node, source: bytes, file_path: str,
237 store: GraphStore, stats: dict) -> None:
 
238 """Handle: const foo = () => {} and const bar = function() {}"""
239 for child in node.children:
240 if child.type != "variable_declarator":
241 continue
242 name_node = child.child_by_field_name("name")
243 value_node = child.child_by_field_name("value")
244 if not name_node or not value_node:
245 continue
246 if value_node.type not in ("arrow_function", "function_expression",
247 "function"):
248 continue
249 name = _node_text(name_node, source)
250 docstring = _jsdoc(node, source)
251
252 store.create_node(NodeLabel.Function, {
253 "name": name,
254 "file_path": file_path,
255 "line_start": node.start_point[0] + 1,
256 "line_end": node.end_point[0] + 1,
257 "docstring": docstring,
258 "class_name": "",
259 })
 
 
 
260 store.create_edge(
261 NodeLabel.File, {"path": file_path},
 
262 EdgeType.CONTAINS,
263 NodeLabel.Function, {"name": name, "file_path": file_path},
 
264 )
265 stats["functions"] += 1
266 stats["edges"] += 1
267
268 self._extract_calls(value_node, source, file_path, name,
269 NodeLabel.Function, store, stats)
 
270
271 def _handle_import(self, node, source: bytes, file_path: str,
272 store: GraphStore, stats: dict) -> None:
 
273 for child in node.children:
274 if child.type == "string":
275 module = _node_text(child, source).strip("'\"")
276 store.create_node(NodeLabel.Import, {
277 "name": module,
278 "file_path": file_path,
279 "line_start": node.start_point[0] + 1,
280 "module": module,
281 })
 
 
 
282 store.create_edge(
283 NodeLabel.File, {"path": file_path},
 
284 EdgeType.IMPORTS,
285 NodeLabel.Import, {"name": module, "file_path": file_path},
 
286 )
287 stats["edges"] += 1
288 break
289
290 def _extract_calls(self, fn_node, source: bytes, file_path: str,
291 fn_name: str, fn_label: str,
292 store: GraphStore, stats: dict) -> None:
 
 
 
 
 
 
 
293 def walk(node):
294 if node.type == "call_expression":
295 func = node.child_by_field_name("function")
296 if func:
297 text = _node_text(func, source)
298 callee = text.split(".")[-1]
299 store.create_edge(
300 fn_label, {"name": fn_name, "file_path": file_path},
 
301 EdgeType.CALLS,
302 NodeLabel.Function, {"name": callee, "file_path": file_path},
 
303 )
304 stats["edges"] += 1
305 for child in node.children:
306 walk(child)
307
308 # Body is statement_block for functions, expression for arrow fns
309 body = next(
310 (c for c in fn_node.children
311 if c.type in ("statement_block", "expression_statement")),
312 None,
313 )
314 if body:
315 walk(body)
316
--- navegador/ingestion/typescript.py
+++ navegador/ingestion/typescript.py
@@ -20,23 +20,25 @@
20 def _get_ts_language(language: str):
21 try:
22 if language == "typescript":
23 import tree_sitter_typescript as tsts # type: ignore[import]
24 from tree_sitter import Language
25
26 return Language(tsts.language_typescript())
27 else:
28 import tree_sitter_javascript as tsjs # type: ignore[import]
29 from tree_sitter import Language
30
31 return Language(tsjs.language())
32 except ImportError as e:
33 raise ImportError(
34 f"Install tree-sitter-{language}: pip install tree-sitter-{language}"
35 ) from e
36
37
38 def _node_text(node, source: bytes) -> str:
39 return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
40
41
42 def _jsdoc(node, source: bytes) -> str:
43 """Return the JSDoc comment (/** ... */) preceding a node, if any."""
44 parent = node.parent
@@ -57,33 +59,44 @@
59 class TypeScriptParser(LanguageParser):
60 """Parses TypeScript/JavaScript source files into the navegador graph."""
61
62 def __init__(self, language: str = "typescript") -> None:
63 from tree_sitter import Parser # type: ignore[import]
64
65 self._parser = Parser(_get_ts_language(language))
66 self._language = language
67
68 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
69 source = path.read_bytes()
70 tree = self._parser.parse(source)
71 rel_path = str(path.relative_to(repo_root))
72
73 store.create_node(
74 NodeLabel.File,
75 {
76 "name": path.name,
77 "path": rel_path,
78 "language": self._language,
79 "line_count": source.count(b"\n"),
80 },
81 )
82
83 stats = {"functions": 0, "classes": 0, "edges": 0}
84 self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
85 return stats
86
87 # ── AST walker ────────────────────────────────────────────────────────────
88
89 def _walk(
90 self,
91 node,
92 source: bytes,
93 file_path: str,
94 store: GraphStore,
95 stats: dict,
96 class_name: str | None,
97 ) -> None:
98 if node.type in ("class_declaration", "abstract_class_declaration"):
99 self._handle_class(node, source, file_path, store, stats)
100 return
101 if node.type in ("interface_declaration", "type_alias_declaration"):
102 self._handle_interface(node, source, file_path, store, stats)
@@ -110,31 +123,35 @@
123 for child in node.children:
124 self._walk(child, source, file_path, store, stats, class_name)
125
126 # ── Handlers ──────────────────────────────────────────────────────────────
127
128 def _handle_class(
129 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
130 ) -> None:
131 name_node = next((c for c in node.children if c.type == "type_identifier"), None)
 
132 if not name_node:
133 return
134 name = _node_text(name_node, source)
135 docstring = _jsdoc(node, source)
136
137 store.create_node(
138 NodeLabel.Class,
139 {
140 "name": name,
141 "file_path": file_path,
142 "line_start": node.start_point[0] + 1,
143 "line_end": node.end_point[0] + 1,
144 "docstring": docstring,
145 },
146 )
147 store.create_edge(
148 NodeLabel.File,
149 {"path": file_path},
150 EdgeType.CONTAINS,
151 NodeLabel.Class,
152 {"name": name, "file_path": file_path},
153 )
154 stats["classes"] += 1
155 stats["edges"] += 1
156
157 # Inheritance: extends clause
@@ -144,172 +161,209 @@
161 if child.type == "extends_clause":
162 for c in child.children:
163 if c.type == "identifier":
164 parent_name = _node_text(c, source)
165 store.create_edge(
166 NodeLabel.Class,
167 {"name": name, "file_path": file_path},
168 EdgeType.INHERITS,
169 NodeLabel.Class,
170 {"name": parent_name, "file_path": file_path},
171 )
172 stats["edges"] += 1
173
174 body = next((c for c in node.children if c.type == "class_body"), None)
175 if body:
176 for child in body.children:
177 if child.type == "method_definition":
178 self._handle_function(child, source, file_path, store, stats, class_name=name)
179
180 def _handle_interface(
181 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
182 ) -> None:
183 name_node = next((c for c in node.children if c.type == "type_identifier"), None)
 
 
184 if not name_node:
185 return
186 name = _node_text(name_node, source)
187 docstring = _jsdoc(node, source)
188 kind = "interface" if node.type == "interface_declaration" else "type"
189
190 store.create_node(
191 NodeLabel.Class,
192 {
193 "name": name,
194 "file_path": file_path,
195 "line_start": node.start_point[0] + 1,
196 "line_end": node.end_point[0] + 1,
197 "docstring": f"{kind}: {docstring}".strip(": "),
198 },
199 )
200 store.create_edge(
201 NodeLabel.File,
202 {"path": file_path},
203 EdgeType.CONTAINS,
204 NodeLabel.Class,
205 {"name": name, "file_path": file_path},
206 )
207 stats["classes"] += 1
208 stats["edges"] += 1
209
210 def _handle_function(
211 self,
212 node,
213 source: bytes,
214 file_path: str,
215 store: GraphStore,
216 stats: dict,
217 class_name: str | None,
218 ) -> None:
219 name_node = next(
220 (c for c in node.children if c.type in ("identifier", "property_identifier")), None
 
221 )
222 if not name_node:
223 return
224 name = _node_text(name_node, source)
225 if name in ("constructor", "get", "set", "static", "async"):
226 # These are keywords, not useful names — look for next identifier
227 name_node = next(
228 (
229 c
230 for c in node.children
231 if c.type in ("identifier", "property_identifier") and c != name_node
232 ),
233 None,
234 )
235 if not name_node:
236 return
237 name = _node_text(name_node, source)
238
239 docstring = _jsdoc(node, source)
240 label = NodeLabel.Method if class_name else NodeLabel.Function
241
242 store.create_node(
243 label,
244 {
245 "name": name,
246 "file_path": file_path,
247 "line_start": node.start_point[0] + 1,
248 "line_end": node.end_point[0] + 1,
249 "docstring": docstring,
250 "class_name": class_name or "",
251 },
252 )
253
254 container_label = NodeLabel.Class if class_name else NodeLabel.File
255 container_key = (
256 {"name": class_name, "file_path": file_path} if class_name else {"path": file_path}
 
257 )
258 store.create_edge(
259 container_label,
260 container_key,
261 EdgeType.CONTAINS,
262 label,
263 {"name": name, "file_path": file_path},
264 )
265 stats["functions"] += 1
266 stats["edges"] += 1
267
268 self._extract_calls(node, source, file_path, name, label, store, stats)
269
270 def _handle_lexical(
271 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
272 ) -> None:
273 """Handle: const foo = () => {} and const bar = function() {}"""
274 for child in node.children:
275 if child.type != "variable_declarator":
276 continue
277 name_node = child.child_by_field_name("name")
278 value_node = child.child_by_field_name("value")
279 if not name_node or not value_node:
280 continue
281 if value_node.type not in ("arrow_function", "function_expression", "function"):
 
282 continue
283 name = _node_text(name_node, source)
284 docstring = _jsdoc(node, source)
285
286 store.create_node(
287 NodeLabel.Function,
288 {
289 "name": name,
290 "file_path": file_path,
291 "line_start": node.start_point[0] + 1,
292 "line_end": node.end_point[0] + 1,
293 "docstring": docstring,
294 "class_name": "",
295 },
296 )
297 store.create_edge(
298 NodeLabel.File,
299 {"path": file_path},
300 EdgeType.CONTAINS,
301 NodeLabel.Function,
302 {"name": name, "file_path": file_path},
303 )
304 stats["functions"] += 1
305 stats["edges"] += 1
306
307 self._extract_calls(
308 value_node, source, file_path, name, NodeLabel.Function, store, stats
309 )
310
311 def _handle_import(
312 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
313 ) -> None:
314 for child in node.children:
315 if child.type == "string":
316 module = _node_text(child, source).strip("'\"")
317 store.create_node(
318 NodeLabel.Import,
319 {
320 "name": module,
321 "file_path": file_path,
322 "line_start": node.start_point[0] + 1,
323 "module": module,
324 },
325 )
326 store.create_edge(
327 NodeLabel.File,
328 {"path": file_path},
329 EdgeType.IMPORTS,
330 NodeLabel.Import,
331 {"name": module, "file_path": file_path},
332 )
333 stats["edges"] += 1
334 break
335
336 def _extract_calls(
337 self,
338 fn_node,
339 source: bytes,
340 file_path: str,
341 fn_name: str,
342 fn_label: str,
343 store: GraphStore,
344 stats: dict,
345 ) -> None:
346 def walk(node):
347 if node.type == "call_expression":
348 func = node.child_by_field_name("function")
349 if func:
350 text = _node_text(func, source)
351 callee = text.split(".")[-1]
352 store.create_edge(
353 fn_label,
354 {"name": fn_name, "file_path": file_path},
355 EdgeType.CALLS,
356 NodeLabel.Function,
357 {"name": callee, "file_path": file_path},
358 )
359 stats["edges"] += 1
360 for child in node.children:
361 walk(child)
362
363 # Body is statement_block for functions, expression for arrow fns
364 body = next(
365 (c for c in fn_node.children if c.type in ("statement_block", "expression_statement")),
 
366 None,
367 )
368 if body:
369 walk(body)
370
--- navegador/ingestion/wiki.py
+++ navegador/ingestion/wiki.py
@@ -103,11 +103,12 @@
103103
clone_dir = Path(clone_dir)
104104
105105
logger.info("Cloning wiki %s → %s", wiki_url, clone_dir)
106106
result = subprocess.run(
107107
["git", "clone", "--depth=1", wiki_url, str(clone_dir)],
108
- capture_output=True, text=True,
108
+ capture_output=True,
109
+ text=True,
109110
)
110111
if result.returncode != 0:
111112
# Wiki may not exist yet — treat as empty
112113
logger.warning("Wiki clone failed: %s", result.stderr.strip())
113114
return {"pages": 0, "links": 0}
@@ -137,10 +138,11 @@
137138
try:
138139
req = urllib.request.Request(url, headers=headers)
139140
with urllib.request.urlopen(req, timeout=10) as resp:
140141
import base64
141142
import json as _json
143
+
142144
data = _json.loads(resp.read().decode())
143145
content = base64.b64decode(data["content"]).decode("utf-8", errors="replace") # noqa: E501
144146
page_name = Path(path).stem.replace("-", " ").replace("_", " ")
145147
html_url = data.get("html_url", "")
146148
links = self._ingest_page(page_name, content, source="github", url=html_url)
@@ -160,16 +162,19 @@
160162
content: str,
161163
source: str,
162164
url: str,
163165
) -> int:
164166
"""Store one wiki page and return the number of DOCUMENTS links created."""
165
- self.store.create_node(NodeLabel.WikiPage, {
166
- "name": name,
167
- "url": url,
168
- "source": source,
169
- "content": content[:4000], # cap stored content
170
- })
167
+ self.store.create_node(
168
+ NodeLabel.WikiPage,
169
+ {
170
+ "name": name,
171
+ "url": url,
172
+ "source": source,
173
+ "content": content[:4000], # cap stored content
174
+ },
175
+ )
171176
172177
links = 0
173178
for term in _extract_terms(content):
174179
links += self._try_link(name, term)
175180
@@ -195,12 +200,14 @@
195200
196201
label_str, node_name = rows[0][0], rows[0][1]
197202
try:
198203
label = NodeLabel(label_str)
199204
self.store.create_edge(
200
- NodeLabel.WikiPage, {"name": wiki_page_name},
205
+ NodeLabel.WikiPage,
206
+ {"name": wiki_page_name},
201207
EdgeType.DOCUMENTS,
202
- label, {"name": node_name},
208
+ label,
209
+ {"name": node_name},
203210
)
204211
return 1
205212
except ValueError:
206213
return 0
207214
--- navegador/ingestion/wiki.py
+++ navegador/ingestion/wiki.py
@@ -103,11 +103,12 @@
103 clone_dir = Path(clone_dir)
104
105 logger.info("Cloning wiki %s → %s", wiki_url, clone_dir)
106 result = subprocess.run(
107 ["git", "clone", "--depth=1", wiki_url, str(clone_dir)],
108 capture_output=True, text=True,
 
109 )
110 if result.returncode != 0:
111 # Wiki may not exist yet — treat as empty
112 logger.warning("Wiki clone failed: %s", result.stderr.strip())
113 return {"pages": 0, "links": 0}
@@ -137,10 +138,11 @@
137 try:
138 req = urllib.request.Request(url, headers=headers)
139 with urllib.request.urlopen(req, timeout=10) as resp:
140 import base64
141 import json as _json
 
142 data = _json.loads(resp.read().decode())
143 content = base64.b64decode(data["content"]).decode("utf-8", errors="replace") # noqa: E501
144 page_name = Path(path).stem.replace("-", " ").replace("_", " ")
145 html_url = data.get("html_url", "")
146 links = self._ingest_page(page_name, content, source="github", url=html_url)
@@ -160,16 +162,19 @@
160 content: str,
161 source: str,
162 url: str,
163 ) -> int:
164 """Store one wiki page and return the number of DOCUMENTS links created."""
165 self.store.create_node(NodeLabel.WikiPage, {
166 "name": name,
167 "url": url,
168 "source": source,
169 "content": content[:4000], # cap stored content
170 })
 
 
 
171
172 links = 0
173 for term in _extract_terms(content):
174 links += self._try_link(name, term)
175
@@ -195,12 +200,14 @@
195
196 label_str, node_name = rows[0][0], rows[0][1]
197 try:
198 label = NodeLabel(label_str)
199 self.store.create_edge(
200 NodeLabel.WikiPage, {"name": wiki_page_name},
 
201 EdgeType.DOCUMENTS,
202 label, {"name": node_name},
 
203 )
204 return 1
205 except ValueError:
206 return 0
207
--- navegador/ingestion/wiki.py
+++ navegador/ingestion/wiki.py
@@ -103,11 +103,12 @@
103 clone_dir = Path(clone_dir)
104
105 logger.info("Cloning wiki %s → %s", wiki_url, clone_dir)
106 result = subprocess.run(
107 ["git", "clone", "--depth=1", wiki_url, str(clone_dir)],
108 capture_output=True,
109 text=True,
110 )
111 if result.returncode != 0:
112 # Wiki may not exist yet — treat as empty
113 logger.warning("Wiki clone failed: %s", result.stderr.strip())
114 return {"pages": 0, "links": 0}
@@ -137,10 +138,11 @@
138 try:
139 req = urllib.request.Request(url, headers=headers)
140 with urllib.request.urlopen(req, timeout=10) as resp:
141 import base64
142 import json as _json
143
144 data = _json.loads(resp.read().decode())
145 content = base64.b64decode(data["content"]).decode("utf-8", errors="replace") # noqa: E501
146 page_name = Path(path).stem.replace("-", " ").replace("_", " ")
147 html_url = data.get("html_url", "")
148 links = self._ingest_page(page_name, content, source="github", url=html_url)
@@ -160,16 +162,19 @@
162 content: str,
163 source: str,
164 url: str,
165 ) -> int:
166 """Store one wiki page and return the number of DOCUMENTS links created."""
167 self.store.create_node(
168 NodeLabel.WikiPage,
169 {
170 "name": name,
171 "url": url,
172 "source": source,
173 "content": content[:4000], # cap stored content
174 },
175 )
176
177 links = 0
178 for term in _extract_terms(content):
179 links += self._try_link(name, term)
180
@@ -195,12 +200,14 @@
200
201 label_str, node_name = rows[0][0], rows[0][1]
202 try:
203 label = NodeLabel(label_str)
204 self.store.create_edge(
205 NodeLabel.WikiPage,
206 {"name": wiki_page_name},
207 EdgeType.DOCUMENTS,
208 label,
209 {"name": node_name},
210 )
211 return 1
212 except ValueError:
213 return 0
214
--- navegador/mcp/server.py
+++ navegador/mcp/server.py
@@ -5,10 +5,11 @@
55
navegador mcp --db .navegador/graph.db
66
"""
77
88
import json
99
import logging
10
+from typing import Any
1011
1112
logger = logging.getLogger(__name__)
1213
1314
1415
def create_mcp_server(store_factory):
@@ -25,12 +26,12 @@
2526
raise ImportError("Install mcp: pip install mcp") from e
2627
2728
from navegador.context import ContextLoader
2829
2930
server = Server("navegador")
30
- _store = None
31
- _loader = None
31
+ _store: Any = None
32
+ _loader: ContextLoader | None = None
3233
3334
def _get_loader() -> ContextLoader:
3435
nonlocal _store, _loader
3536
if _loader is None:
3637
_store = store_factory()
@@ -65,11 +66,13 @@
6566
"file_path": {
6667
"type": "string",
6768
"description": "Relative file path within the ingested repo.",
6869
},
6970
"format": {
70
- "type": "string", "enum": ["json", "markdown"], "default": "markdown",
71
+ "type": "string",
72
+ "enum": ["json", "markdown"],
73
+ "default": "markdown",
7174
},
7275
},
7376
"required": ["file_path"],
7477
},
7578
),
@@ -81,11 +84,13 @@
8184
"properties": {
8285
"name": {"type": "string", "description": "Function name."},
8386
"file_path": {"type": "string", "description": "Relative file path."},
8487
"depth": {"type": "integer", "default": 2},
8588
"format": {
86
- "type": "string", "enum": ["json", "markdown"], "default": "markdown",
89
+ "type": "string",
90
+ "enum": ["json", "markdown"],
91
+ "default": "markdown",
8792
},
8893
},
8994
"required": ["name", "file_path"],
9095
},
9196
),
@@ -96,11 +101,13 @@
96101
"type": "object",
97102
"properties": {
98103
"name": {"type": "string", "description": "Class name."},
99104
"file_path": {"type": "string", "description": "Relative file path."},
100105
"format": {
101
- "type": "string", "enum": ["json", "markdown"], "default": "markdown",
106
+ "type": "string",
107
+ "enum": ["json", "markdown"],
108
+ "default": "markdown",
102109
},
103110
},
104111
"required": ["name", "file_path"],
105112
},
106113
),
@@ -138,10 +145,11 @@
138145
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
139146
loader = _get_loader()
140147
141148
if name == "ingest_repo":
142149
from navegador.ingestion import RepoIngester
150
+
143151
ingester = RepoIngester(loader.store)
144152
stats = ingester.ingest(arguments["path"], clear=arguments.get("clear", False))
145153
return [TextContent(type="text", text=json.dumps(stats, indent=2))]
146154
147155
elif name == "load_file_context":
148156
--- navegador/mcp/server.py
+++ navegador/mcp/server.py
@@ -5,10 +5,11 @@
5 navegador mcp --db .navegador/graph.db
6 """
7
8 import json
9 import logging
 
10
11 logger = logging.getLogger(__name__)
12
13
14 def create_mcp_server(store_factory):
@@ -25,12 +26,12 @@
25 raise ImportError("Install mcp: pip install mcp") from e
26
27 from navegador.context import ContextLoader
28
29 server = Server("navegador")
30 _store = None
31 _loader = None
32
33 def _get_loader() -> ContextLoader:
34 nonlocal _store, _loader
35 if _loader is None:
36 _store = store_factory()
@@ -65,11 +66,13 @@
65 "file_path": {
66 "type": "string",
67 "description": "Relative file path within the ingested repo.",
68 },
69 "format": {
70 "type": "string", "enum": ["json", "markdown"], "default": "markdown",
 
 
71 },
72 },
73 "required": ["file_path"],
74 },
75 ),
@@ -81,11 +84,13 @@
81 "properties": {
82 "name": {"type": "string", "description": "Function name."},
83 "file_path": {"type": "string", "description": "Relative file path."},
84 "depth": {"type": "integer", "default": 2},
85 "format": {
86 "type": "string", "enum": ["json", "markdown"], "default": "markdown",
 
 
87 },
88 },
89 "required": ["name", "file_path"],
90 },
91 ),
@@ -96,11 +101,13 @@
96 "type": "object",
97 "properties": {
98 "name": {"type": "string", "description": "Class name."},
99 "file_path": {"type": "string", "description": "Relative file path."},
100 "format": {
101 "type": "string", "enum": ["json", "markdown"], "default": "markdown",
 
 
102 },
103 },
104 "required": ["name", "file_path"],
105 },
106 ),
@@ -138,10 +145,11 @@
138 async def call_tool(name: str, arguments: dict) -> list[TextContent]:
139 loader = _get_loader()
140
141 if name == "ingest_repo":
142 from navegador.ingestion import RepoIngester
 
143 ingester = RepoIngester(loader.store)
144 stats = ingester.ingest(arguments["path"], clear=arguments.get("clear", False))
145 return [TextContent(type="text", text=json.dumps(stats, indent=2))]
146
147 elif name == "load_file_context":
148
--- navegador/mcp/server.py
+++ navegador/mcp/server.py
@@ -5,10 +5,11 @@
5 navegador mcp --db .navegador/graph.db
6 """
7
8 import json
9 import logging
10 from typing import Any
11
12 logger = logging.getLogger(__name__)
13
14
15 def create_mcp_server(store_factory):
@@ -25,12 +26,12 @@
26 raise ImportError("Install mcp: pip install mcp") from e
27
28 from navegador.context import ContextLoader
29
30 server = Server("navegador")
31 _store: Any = None
32 _loader: ContextLoader | None = None
33
34 def _get_loader() -> ContextLoader:
35 nonlocal _store, _loader
36 if _loader is None:
37 _store = store_factory()
@@ -65,11 +66,13 @@
66 "file_path": {
67 "type": "string",
68 "description": "Relative file path within the ingested repo.",
69 },
70 "format": {
71 "type": "string",
72 "enum": ["json", "markdown"],
73 "default": "markdown",
74 },
75 },
76 "required": ["file_path"],
77 },
78 ),
@@ -81,11 +84,13 @@
84 "properties": {
85 "name": {"type": "string", "description": "Function name."},
86 "file_path": {"type": "string", "description": "Relative file path."},
87 "depth": {"type": "integer", "default": 2},
88 "format": {
89 "type": "string",
90 "enum": ["json", "markdown"],
91 "default": "markdown",
92 },
93 },
94 "required": ["name", "file_path"],
95 },
96 ),
@@ -96,11 +101,13 @@
101 "type": "object",
102 "properties": {
103 "name": {"type": "string", "description": "Class name."},
104 "file_path": {"type": "string", "description": "Relative file path."},
105 "format": {
106 "type": "string",
107 "enum": ["json", "markdown"],
108 "default": "markdown",
109 },
110 },
111 "required": ["name", "file_path"],
112 },
113 ),
@@ -138,10 +145,11 @@
145 async def call_tool(name: str, arguments: dict) -> list[TextContent]:
146 loader = _get_loader()
147
148 if name == "ingest_repo":
149 from navegador.ingestion import RepoIngester
150
151 ingester = RepoIngester(loader.store)
152 stats = ingester.ingest(arguments["path"], clear=arguments.get("clear", False))
153 return [TextContent(type="text", text=json.dumps(stats, indent=2))]
154
155 elif name == "load_file_context":
156

Keyboard Shortcuts

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