codecompass-mcp 3.1.1__tar.gz → 3.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/PKG-INFO +1 -3
  2. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/codecompass_mcp.egg-info/PKG-INFO +1 -3
  3. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/codecompass_mcp.egg-info/SOURCES.txt +1 -11
  4. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/codecompass_mcp.egg-info/entry_points.txt +1 -1
  5. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/codecompass_mcp.egg-info/requires.txt +0 -2
  6. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/codecompass_mcp.egg-info/top_level.txt +0 -1
  7. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/graph/code_graph_client.py +1 -1
  8. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/graph/code_query_cli.py +15 -89
  9. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/ingestion/code_normalizer.py +2 -8
  10. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/ingestion/code_parser.py +2 -18
  11. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/ingestion/file_watcher.py +2 -4
  12. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/ingestion/hierarchy_builder.py +3 -31
  13. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/main.py +91 -82
  14. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/mcp_server.py +20 -53
  15. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/pyproject.toml +3 -5
  16. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/tests/test_accuracy_fixes.py +0 -2
  17. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/tests/test_batch_impact.py +4 -4
  18. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/tests/test_blast_radius.py +2 -2
  19. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/tests/test_code_parser.py +0 -2
  20. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/tests/test_description_enricher.py +1 -1
  21. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/tests/test_lit_css_parser.py +0 -2
  22. codecompass_mcp-3.1.1/graph/cli.py +0 -13
  23. codecompass_mcp-3.1.1/graph/setup.py +0 -36
  24. codecompass_mcp-3.1.1/ingestion/chunker.py +0 -61
  25. codecompass_mcp-3.1.1/ingestion/entity_resolver.py +0 -98
  26. codecompass_mcp-3.1.1/ingestion/graph_writer.py +0 -16
  27. codecompass_mcp-3.1.1/ingestion/reader_agent.py +0 -136
  28. codecompass_mcp-3.1.1/models/types.py +0 -45
  29. codecompass_mcp-3.1.1/tests/test_ingestion.py +0 -93
  30. codecompass_mcp-3.1.1/utils/__init__.py +0 -0
  31. codecompass_mcp-3.1.1/utils/formatting.py +0 -24
  32. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/LICENSE +0 -0
  33. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/README.md +0 -0
  34. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/codecompass_mcp.egg-info/dependency_links.txt +0 -0
  35. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/config.py +0 -0
  36. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/graph/__init__.py +0 -0
  37. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/ingestion/__init__.py +0 -0
  38. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/ingestion/description_enricher.py +0 -0
  39. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/ingestion/source_context.py +0 -0
  40. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/models/__init__.py +0 -0
  41. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/models/code_types.py +0 -0
  42. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/setup.cfg +0 -0
  43. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/tests/test_mcp_server.py +0 -0
  44. {codecompass_mcp-3.1.1 → codecompass_mcp-3.2.1}/tests/test_php_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecompass-mcp
3
- Version: 3.1.1
3
+ Version: 3.2.1
4
4
  Summary: Structural code context for AI coding agents — a local code knowledge graph for blast radius, impact, deps, dead code, and flow tracing
5
5
  Author: CodeCompass contributors
6
6
  License: MIT
@@ -20,10 +20,8 @@ License-File: LICENSE
20
20
  Requires-Dist: anthropic>=0.40.0
21
21
  Requires-Dist: fastmcp>=2.0.0
22
22
  Requires-Dist: networkx>=3.0
23
- Requires-Dist: pypdf2>=3.0.0
24
23
  Requires-Dist: python-dotenv>=1.0.0
25
24
  Requires-Dist: rich>=13.0.0
26
- Requires-Dist: tqdm>=4.66.0
27
25
  Requires-Dist: tree-sitter>=0.21.0
28
26
  Requires-Dist: tree-sitter-python>=0.21.0
29
27
  Requires-Dist: tree-sitter-javascript>=0.21.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecompass-mcp
3
- Version: 3.1.1
3
+ Version: 3.2.1
4
4
  Summary: Structural code context for AI coding agents — a local code knowledge graph for blast radius, impact, deps, dead code, and flow tracing
5
5
  Author: CodeCompass contributors
6
6
  License: MIT
@@ -20,10 +20,8 @@ License-File: LICENSE
20
20
  Requires-Dist: anthropic>=0.40.0
21
21
  Requires-Dist: fastmcp>=2.0.0
22
22
  Requires-Dist: networkx>=3.0
23
- Requires-Dist: pypdf2>=3.0.0
24
23
  Requires-Dist: python-dotenv>=1.0.0
25
24
  Requires-Dist: rich>=13.0.0
26
- Requires-Dist: tqdm>=4.66.0
27
25
  Requires-Dist: tree-sitter>=0.21.0
28
26
  Requires-Dist: tree-sitter-python>=0.21.0
29
27
  Requires-Dist: tree-sitter-javascript>=0.21.0
@@ -11,32 +11,22 @@ codecompass_mcp.egg-info/entry_points.txt
11
11
  codecompass_mcp.egg-info/requires.txt
12
12
  codecompass_mcp.egg-info/top_level.txt
13
13
  graph/__init__.py
14
- graph/cli.py
15
14
  graph/code_graph_client.py
16
15
  graph/code_query_cli.py
17
- graph/setup.py
18
16
  ingestion/__init__.py
19
- ingestion/chunker.py
20
17
  ingestion/code_normalizer.py
21
18
  ingestion/code_parser.py
22
19
  ingestion/description_enricher.py
23
- ingestion/entity_resolver.py
24
20
  ingestion/file_watcher.py
25
- ingestion/graph_writer.py
26
21
  ingestion/hierarchy_builder.py
27
- ingestion/reader_agent.py
28
22
  ingestion/source_context.py
29
23
  models/__init__.py
30
24
  models/code_types.py
31
- models/types.py
32
25
  tests/test_accuracy_fixes.py
33
26
  tests/test_batch_impact.py
34
27
  tests/test_blast_radius.py
35
28
  tests/test_code_parser.py
36
29
  tests/test_description_enricher.py
37
- tests/test_ingestion.py
38
30
  tests/test_lit_css_parser.py
39
31
  tests/test_mcp_server.py
40
- tests/test_php_parser.py
41
- utils/__init__.py
42
- utils/formatting.py
32
+ tests/test_php_parser.py
@@ -1,3 +1,3 @@
1
1
  [console_scripts]
2
- codecompass = graph.cli:main
2
+ codecompass = main:main
3
3
  codecompass-mcp = mcp_server:main
@@ -1,10 +1,8 @@
1
1
  anthropic>=0.40.0
2
2
  fastmcp>=2.0.0
3
3
  networkx>=3.0
4
- pypdf2>=3.0.0
5
4
  python-dotenv>=1.0.0
6
5
  rich>=13.0.0
7
- tqdm>=4.66.0
8
6
  tree-sitter>=0.21.0
9
7
  tree-sitter-python>=0.21.0
10
8
  tree-sitter-javascript>=0.21.0
@@ -4,4 +4,3 @@ ingestion
4
4
  main
5
5
  mcp_server
6
6
  models
7
- utils
@@ -10,7 +10,7 @@ import os
10
10
  import re
11
11
  import networkx as nx
12
12
  from datetime import datetime, timezone
13
- from typing import Optional, Any
13
+ from typing import Optional
14
14
 
15
15
  from models.code_types import CodeTriple, FileNode, FolderNode
16
16
 
@@ -3,7 +3,7 @@
3
3
  python -m graph.code_query_cli --impact "login()" <repo_path>
4
4
  python -m graph.code_query_cli --deps src/auth/login.py <repo_path>
5
5
  python -m graph.code_query_cli --styles LoginForm <repo_path>
6
- python -m graph.code_query_cli --trace "main()" <repo_path>
6
+ python -m graph.code_query_cli --flow "main()" <repo_path>
7
7
  python -m graph.code_query_cli --tree <repo_path>
8
8
 
9
9
  Output is plain text by default (agent-friendly). Pass --rich for formatted tables.
@@ -12,16 +12,12 @@ Output is plain text by default (agent-friendly). Pass --rich for formatted tabl
12
12
  from __future__ import annotations
13
13
 
14
14
  import argparse
15
+ from contextlib import closing
15
16
  import json
16
17
  import os
17
18
  import sys
18
19
  import xml.etree.ElementTree as ET
19
20
  from datetime import datetime, timezone
20
- from pathlib import Path
21
-
22
- _project_root = Path(__file__).resolve().parent.parent
23
- if str(_project_root) not in sys.path:
24
- sys.path.insert(0, str(_project_root))
25
21
 
26
22
  from rich.console import Console
27
23
  from rich.table import Table
@@ -42,88 +38,64 @@ STALE_WARN_HOURS = 24
42
38
 
43
39
  def fetch_impact(entity_name: str, repo_path: str, project: str, max_hops: int = DEFAULT_HOPS) -> dict:
44
40
  """Return callers of entity_name as structured data."""
45
- client = get_client(repo_path)
46
- try:
41
+ with closing(get_client(repo_path)) as client:
47
42
  rows = client.find_callers(entity_name, project, max_hops)
48
43
  updated_at = _entity_updated_at(client, entity_name)
49
- finally:
50
- client.close()
51
44
  return {"entity": entity_name, "callers": rows, "updated_at": updated_at}
52
45
 
53
46
 
54
47
  def fetch_grep(pattern: str, repo_path: str, project: str, field: str = "all",
55
48
  ignore_case: bool = True, limit: int = 100) -> dict:
56
49
  """Regex-search the graph — 'grep' over indexed entities, not file lines."""
57
- client = get_client(repo_path)
58
- try:
50
+ with closing(get_client(repo_path)) as client:
59
51
  return client.grep_graph(pattern, project, field=field,
60
52
  ignore_case=ignore_case, limit=limit)
61
- finally:
62
- client.close()
63
53
 
64
54
 
65
55
  def fetch_search(query: str, repo_path: str, project: str, limit: int = 30,
66
56
  kind: str | None = None) -> dict:
67
57
  """Search the graph for entities matching a keyword — the discovery entry point."""
68
- client = get_client(repo_path)
69
- try:
58
+ with closing(get_client(repo_path)) as client:
70
59
  rows = client.search_entities(query, project, limit=limit, kind=kind)
71
- finally:
72
- client.close()
73
60
  return {"query": query, "matches": rows, "count": len(rows)}
74
61
 
75
62
 
76
63
  def fetch_map(repo_path: str, project: str, include_tests: bool = False) -> dict:
77
64
  """Compact `{file: [symbols]}` index for an agent to reason over during discovery."""
78
- client = get_client(repo_path)
79
- try:
65
+ with closing(get_client(repo_path)) as client:
80
66
  m = client.symbol_map(project, include_tests=include_tests)
81
- finally:
82
- client.close()
83
67
  return {"project": project, "files": m, "file_count": len(m)}
84
68
 
85
69
 
86
70
  def fetch_deps(file_path: str, repo_path: str, project: str, max_hops: int = DEFAULT_HOPS) -> dict:
87
71
  """Return what file_path imports as structured data."""
88
- client = get_client(repo_path)
89
- try:
72
+ with closing(get_client(repo_path)) as client:
90
73
  rows = client.find_dependencies(file_path, project, max_hops)
91
74
  updated_at = client.get_file_updated_at(file_path, project)
92
- finally:
93
- client.close()
94
75
  return {"file": file_path, "dependencies": rows, "updated_at": updated_at}
95
76
 
96
77
 
97
78
  def fetch_styles(element_name: str, repo_path: str, project: str) -> dict:
98
79
  """Return CSS selectors that style element_name as structured data."""
99
- client = get_client(repo_path)
100
- try:
80
+ with closing(get_client(repo_path)) as client:
101
81
  rows = client.find_styles(element_name, project)
102
82
  updated_at = _entity_updated_at(client, element_name)
103
- finally:
104
- client.close()
105
83
  return {"element": element_name, "selectors": rows, "updated_at": updated_at}
106
84
 
107
85
 
108
86
  def fetch_trace(start_name: str, repo_path: str, project: str, max_hops: int = DEFAULT_HOPS) -> dict:
109
87
  """Return forward call chain from start_name as structured data."""
110
- client = get_client(repo_path)
111
- try:
88
+ with closing(get_client(repo_path)) as client:
112
89
  rows = client.trace_calls(start_name, project, max_hops)
113
90
  updated_at = _entity_updated_at(client, start_name)
114
- finally:
115
- client.close()
116
91
  return {"entity": start_name, "calls": rows, "updated_at": updated_at}
117
92
 
118
93
 
119
94
  def fetch_blast_radius(target: str, repo_path: str, project: str, max_hops: int = DEFAULT_HOPS) -> dict:
120
95
  """Return blast radius of target as structured data."""
121
- client = get_client(repo_path)
122
- try:
96
+ with closing(get_client(repo_path)) as client:
123
97
  rows, target_file = client.get_blast_radius(target, project, max_hops)
124
98
  updated_at = client.get_file_updated_at(target_file, project) if target_file else None
125
- finally:
126
- client.close()
127
99
 
128
100
  if target_file is None:
129
101
  return {"target": target, "found": False, "files": [], "updated_at": None}
@@ -153,9 +125,7 @@ def fetch_batch_impact(targets: list[str], repo_path: str, project: str, max_hop
153
125
  for t in targets:
154
126
  flat_targets.extend(s.strip() for s in t.split(",") if s.strip())
155
127
 
156
- input_set = set(flat_targets)
157
- client = get_client(repo_path)
158
- try:
128
+ with closing(get_client(repo_path)) as client:
159
129
  merged: dict[str, dict] = {}
160
130
  resolved: list[str] = []
161
131
  staleness_ts: str | None = None
@@ -181,8 +151,6 @@ def fetch_batch_impact(targets: list[str], repo_path: str, project: str, max_hop
181
151
  if h < merged[f]["hops"]:
182
152
  merged[f]["hops"] = h
183
153
  merged[f]["via"].add(target)
184
- finally:
185
- client.close()
186
154
 
187
155
  deduped = sorted(
188
156
  [{"file": f, "hops": v["hops"], "via": sorted(v["via"])} for f, v in merged.items()],
@@ -199,11 +167,8 @@ def fetch_batch_impact(targets: list[str], repo_path: str, project: str, max_hop
199
167
 
200
168
  def fetch_dead_code(repo_path: str, project: str, show_entrypoints: bool = False) -> dict:
201
169
  """Return dead-code candidates as structured data."""
202
- client = get_client(repo_path)
203
- try:
170
+ with closing(get_client(repo_path)) as client:
204
171
  result = client.find_dead_code(project)
205
- finally:
206
- client.close()
207
172
  return {
208
173
  "project": project,
209
174
  "dead": result["dead"],
@@ -214,12 +179,9 @@ def fetch_dead_code(repo_path: str, project: str, show_entrypoints: bool = False
214
179
 
215
180
  def fetch_tree(repo_path: str, project: str) -> dict:
216
181
  """Return project hierarchy as structured data."""
217
- client = get_client(repo_path)
218
- try:
182
+ with closing(get_client(repo_path)) as client:
219
183
  rows = client.get_project_tree(project)
220
184
  last_ingested = client.get_project_last_ingested(project)
221
- finally:
222
- client.close()
223
185
  return {"project": project, "tree": rows, "last_ingested": last_ingested}
224
186
 
225
187
 
@@ -231,11 +193,8 @@ def fetch_flow(start_name: str, repo_path: str, project: str, max_hops: int = DE
231
193
  line — NO embedded source, docstrings, narration, or rendered image. Use
232
194
  fetch_flow_summary for a human-facing walkthrough (mermaid + narration).
233
195
  """
234
- client = get_client(repo_path)
235
- try:
196
+ with closing(get_client(repo_path)) as client:
236
197
  data = client.trace_flow(start_name, project, max_hops, include_external=include_external)
237
- finally:
238
- client.close()
239
198
 
240
199
  nodes = data["nodes"]
241
200
  edges = data["edges"]
@@ -272,11 +231,8 @@ def fetch_flow_summary(start_name: str, repo_path: str, project: str, max_hops:
272
231
  "drawio" renders a diagram. Heavier than fetch_flow — use when a person needs
273
232
  to read the flow, not when an agent just needs the structure.
274
233
  """
275
- client = get_client(repo_path)
276
- try:
234
+ with closing(get_client(repo_path)) as client:
277
235
  data = client.trace_flow(start_name, project, max_hops, include_external=include_external)
278
- finally:
279
- client.close()
280
236
 
281
237
  nodes = data["nodes"]
282
238
  edges = data["edges"]
@@ -327,8 +283,6 @@ def main() -> None:
327
283
  run_deps(args.deps, repo_path, project, max_hops=args.hops, rich=rich)
328
284
  elif args.styles:
329
285
  run_styles(args.styles, repo_path, project, rich=rich)
330
- elif args.trace:
331
- run_trace(args.trace, repo_path, project, max_hops=args.hops, rich=rich)
332
286
  elif args.flow:
333
287
  run_flow(args.flow, repo_path, project, max_hops=args.hops, rich=rich,
334
288
  output=args.flow_output, include_external=args.include_external,
@@ -437,33 +391,6 @@ def run_styles(element_name: str, repo_path: str, project: str, rich: bool = Fal
437
391
  print(f" {row.get('selector','')} in {row.get('source_file','')} line {row.get('line','')}")
438
392
 
439
393
 
440
- def run_trace(start_name: str, repo_path: str, project: str, max_hops: int = DEFAULT_HOPS, rich: bool = False) -> None:
441
- """Trace the call chain forward from start_name."""
442
- data = fetch_trace(start_name, repo_path, project, max_hops)
443
- rows = data["calls"]
444
- updated_at = data["updated_at"]
445
-
446
- if not rows:
447
- print(f"'{start_name}' makes no tracked calls within {max_hops} hops.")
448
- return
449
-
450
- stamp = _staleness_line(updated_at, rich_mode=rich)
451
- if rich:
452
- console.print(f"\n[bold blue]Call trace from:[/] {start_name}")
453
- if stamp:
454
- console.print(stamp)
455
- table = _make_table(title=f"Call chain from '{start_name}'", columns=["Callee", "Type", "File", "Depth"])
456
- for row in rows:
457
- table.add_row(row.get("callee_name",""), row.get("callee_type",""), row.get("callee_file",""), str(row.get("depth","")))
458
- console.print(table)
459
- else:
460
- if stamp:
461
- print(stamp)
462
- print(f"Call chain from '{start_name}':")
463
- for row in rows:
464
- print(f" {row.get('callee_name','')} ({row.get('callee_type','')}) in {row.get('callee_file','')} [depth {row.get('depth','')}]")
465
-
466
-
467
394
  def run_blast_radius(target: str, repo_path: str, project: str, max_hops: int = DEFAULT_HOPS, rich: bool = False) -> None:
468
395
  """Show every file that DEPENDS ON the target (reverse: callers/importers/subclasses)."""
469
396
  data = fetch_blast_radius(target, repo_path, project, max_hops)
@@ -1018,7 +945,6 @@ def _parse_args() -> argparse.Namespace:
1018
945
  parser.add_argument("--impact", metavar="ENTITY")
1019
946
  parser.add_argument("--deps", metavar="FILE")
1020
947
  parser.add_argument("--styles", metavar="ELEMENT")
1021
- parser.add_argument("--trace", metavar="ENTITY")
1022
948
  parser.add_argument("--flow", metavar="ENTITY")
1023
949
  parser.add_argument("--flow-summary", metavar="ENTITY",
1024
950
  help="Flow trace + rendered narration (mermaid by default) for a human")
@@ -17,6 +17,7 @@ import json
17
17
  from typing import Any
18
18
 
19
19
  import anthropic
20
+ from rich.progress import track
20
21
 
21
22
  from config import anthropic_api_key
22
23
  from models.code_types import CodeTriple
@@ -67,14 +68,7 @@ def normalize_triples(triples: list[CodeTriple], progress: bool = False) -> list
67
68
  batches = _split_into_batches(triples, BATCH_SIZE)
68
69
  normalized: list[CodeTriple] = []
69
70
 
70
- if progress:
71
- try:
72
- from tqdm import tqdm
73
- batches_iter = tqdm(batches, desc="Normalizing batches", unit="batch")
74
- except ImportError:
75
- batches_iter = batches
76
- else:
77
- batches_iter = batches
71
+ batches_iter = track(batches, description="Normalizing batches") if progress else batches
78
72
 
79
73
  for batch in batches_iter:
80
74
  corrected = _normalize_batch(client, batch)
@@ -13,6 +13,7 @@ import re
13
13
  from pathlib import Path
14
14
  from typing import Callable
15
15
 
16
+ from rich.progress import track
16
17
  from tree_sitter import Language, Parser, Node
17
18
 
18
19
  from models.code_types import CodeTriple
@@ -316,11 +317,6 @@ def _extract_python(root: Node, source: bytes, file_path: str) -> list[CodeTripl
316
317
  return triples
317
318
 
318
319
 
319
- def _extract_python_callee(call_node: Node) -> str | None:
320
- """Back-compat shim: callee name only."""
321
- return _extract_python_call_parts(call_node)[0]
322
-
323
-
324
320
  def _extract_python_call_parts(call_node: Node) -> tuple[str | None, str | None, Node | None]:
325
321
  """Return (callee_name, receiver_text, receiver_object_node) for a `call`."""
326
322
  if not call_node.children:
@@ -1444,11 +1440,6 @@ def _resolve_receiver_type(receiver: str | None, obj: Node | None, call_node: No
1444
1440
  return None
1445
1441
 
1446
1442
 
1447
- def _extract_js_callee(call_node: Node) -> str | None:
1448
- """Back-compat shim: callee name only."""
1449
- return _extract_js_call(call_node)[0]
1450
-
1451
-
1452
1443
  def _extract_js_require_target(call_node: Node) -> str | None:
1453
1444
  """Module string of a `require('x')` or dynamic `import('x')` call, else None.
1454
1445
 
@@ -1785,14 +1776,7 @@ def parse_directory(
1785
1776
  if Path(filename).suffix.lower() in SUPPORTED_EXTENSIONS:
1786
1777
  source_files.append(os.path.join(dirpath, filename))
1787
1778
 
1788
- if progress:
1789
- try:
1790
- from tqdm import tqdm
1791
- source_files_iter = tqdm(source_files, desc="Parsing files", unit="file")
1792
- except ImportError:
1793
- source_files_iter = source_files
1794
- else:
1795
- source_files_iter = source_files
1779
+ source_files_iter = track(source_files, description="Parsing files") if progress else source_files
1796
1780
 
1797
1781
  triples: list[CodeTriple] = []
1798
1782
  for full_path in source_files_iter:
@@ -16,8 +16,6 @@ from __future__ import annotations
16
16
  import os
17
17
  import time
18
18
  from pathlib import Path
19
- from typing import Optional
20
-
21
19
  from watchdog.events import FileSystemEventHandler, FileSystemEvent
22
20
  from watchdog.observers import Observer
23
21
 
@@ -48,7 +46,7 @@ class _CodeFileEventHandler(FileSystemEventHandler):
48
46
  self._project_root = project_root
49
47
  self._project_name = project_name
50
48
  self._client = client
51
- self._file_id_map = file_id_map # {rel_path: neo4j_file_node_id}
49
+ self._file_id_map = file_id_map # {rel_path: file_node_id}
52
50
 
53
51
  def on_modified(self, event: FileSystemEvent) -> None:
54
52
  if event.is_directory:
@@ -81,7 +79,7 @@ class _CodeFileEventHandler(FileSystemEventHandler):
81
79
  # ------------------------------------------------------------------
82
80
 
83
81
  def _handle_change(self, abs_path: str) -> None:
84
- """Re-parse a changed file and apply only the delta to Neo4j."""
82
+ """Re-parse a changed file and apply only the delta to the local graph."""
85
83
  ext = Path(abs_path).suffix.lower()
86
84
  if ext not in SUPPORTED_EXTENSIONS:
87
85
  return
@@ -1,4 +1,4 @@
1
- """Hierarchy builder — walks a repo and writes the Project → Folder → File skeleton to Neo4j.
1
+ """Hierarchy builder — walks a repo and writes the Project → Folder → File skeleton to the local graph.
2
2
 
3
3
  This runs before code_parser so every file has a node to attach entities to.
4
4
  No API calls — purely local filesystem traversal.
@@ -24,9 +24,9 @@ _SOURCE_EXTENSIONS = {".py", ".js", ".jsx", ".ts", ".tsx", ".html", ".css", ".sc
24
24
 
25
25
 
26
26
  def build_hierarchy(project_root: str, project_name: str, client) -> dict[str, str]:
27
- """Walk project_root and write Project → Folder → File nodes to Neo4j.
27
+ """Walk project_root and write Project → Folder → File nodes to the local graph.
28
28
 
29
- Returns a mapping of {relative_file_path: neo4j_node_id} so the caller
29
+ Returns a mapping of {relative_file_path: node_id} so the caller
30
30
  can attach entity nodes to the correct File nodes.
31
31
 
32
32
  Args:
@@ -74,34 +74,6 @@ def build_hierarchy(project_root: str, project_name: str, client) -> dict[str, s
74
74
  return file_id_map
75
75
 
76
76
 
77
- def collect_file_nodes(project_root: str) -> list[FileNode]:
78
- """Return FileNode objects for every supported source file under project_root.
79
-
80
- Useful for dry-run inspection or passing file lists to other pipeline stages.
81
- """
82
- nodes: list[FileNode] = []
83
- for dirpath, dirnames, filenames in os.walk(project_root):
84
- dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
85
- for filename in filenames:
86
- ext = Path(filename).suffix.lower()
87
- if ext in _SOURCE_EXTENSIONS:
88
- full_path = os.path.join(dirpath, filename)
89
- rel_path = os.path.relpath(full_path, project_root)
90
- nodes.append(_make_file_node(rel_path, project_root))
91
- return nodes
92
-
93
-
94
- def collect_folder_nodes(project_root: str) -> list[FolderNode]:
95
- """Return FolderNode objects for every non-skipped directory under project_root."""
96
- nodes: list[FolderNode] = []
97
- for dirpath, dirnames, _ in os.walk(project_root):
98
- dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
99
- rel_dir = os.path.relpath(dirpath, project_root)
100
- if rel_dir != ".":
101
- nodes.append(_make_folder_node(rel_dir, project_root))
102
- return nodes
103
-
104
-
105
77
  # ---------------------------------------------------------------------------
106
78
  # Internal helpers
107
79
  # ---------------------------------------------------------------------------