velune-cli 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. velune/__init__.py +5 -0
  2. velune/__main__.py +6 -0
  3. velune/cli/__init__.py +5 -0
  4. velune/cli/app.py +208 -0
  5. velune/cli/autocomplete.py +80 -0
  6. velune/cli/banner.py +60 -0
  7. velune/cli/commands/__init__.py +32 -0
  8. velune/cli/commands/ask.py +175 -0
  9. velune/cli/commands/base.py +16 -0
  10. velune/cli/commands/chat.py +228 -0
  11. velune/cli/commands/config.py +224 -0
  12. velune/cli/commands/daemon.py +88 -0
  13. velune/cli/commands/doctor.py +721 -0
  14. velune/cli/commands/init.py +170 -0
  15. velune/cli/commands/mcp.py +82 -0
  16. velune/cli/commands/memory.py +293 -0
  17. velune/cli/commands/models.py +683 -0
  18. velune/cli/commands/preflight.py +95 -0
  19. velune/cli/commands/run.py +270 -0
  20. velune/cli/commands/setup.py +184 -0
  21. velune/cli/commands/workspace.py +249 -0
  22. velune/cli/context.py +36 -0
  23. velune/cli/councilmodel_ui.py +199 -0
  24. velune/cli/display/council_view.py +254 -0
  25. velune/cli/display/memory_view.py +126 -0
  26. velune/cli/display/panels.py +35 -0
  27. velune/cli/display/progress.py +25 -0
  28. velune/cli/display/themes.py +25 -0
  29. velune/cli/main.py +15 -0
  30. velune/cli/model_selector.py +51 -0
  31. velune/cli/modes.py +86 -0
  32. velune/cli/pull_ui.py +123 -0
  33. velune/cli/registry.py +80 -0
  34. velune/cli/rendering/__init__.py +5 -0
  35. velune/cli/rendering/error_panel.py +79 -0
  36. velune/cli/rendering/markdown.py +63 -0
  37. velune/cli/repl.py +1855 -0
  38. velune/cli/session_manager.py +71 -0
  39. velune/cli/slash_commands.py +37 -0
  40. velune/cli/theme.py +8 -0
  41. velune/cognition/__init__.py +23 -0
  42. velune/cognition/agents/__init__.py +7 -0
  43. velune/cognition/agents/coder.py +209 -0
  44. velune/cognition/agents/planner.py +156 -0
  45. velune/cognition/agents/reviewer.py +195 -0
  46. velune/cognition/arbitrator.py +220 -0
  47. velune/cognition/architecture.py +415 -0
  48. velune/cognition/budget.py +65 -0
  49. velune/cognition/council/__init__.py +47 -0
  50. velune/cognition/council/base.py +217 -0
  51. velune/cognition/council/challenger.py +74 -0
  52. velune/cognition/council/coder.py +79 -0
  53. velune/cognition/council/critic_agent.py +43 -0
  54. velune/cognition/council/critic_configs.py +111 -0
  55. velune/cognition/council/critics.py +41 -0
  56. velune/cognition/council/debate.py +46 -0
  57. velune/cognition/council/factory.py +140 -0
  58. velune/cognition/council/messages.py +56 -0
  59. velune/cognition/council/planner.py +124 -0
  60. velune/cognition/council/reviewer.py +74 -0
  61. velune/cognition/council/synthesizer.py +67 -0
  62. velune/cognition/council/tiers.py +188 -0
  63. velune/cognition/council_orchestrator.py +282 -0
  64. velune/cognition/firewall.py +354 -0
  65. velune/cognition/module.py +46 -0
  66. velune/cognition/orchestrator.py +1205 -0
  67. velune/cognition/personality.py +238 -0
  68. velune/cognition/state.py +104 -0
  69. velune/cognition/style_resolver.py +64 -0
  70. velune/cognition/verification.py +205 -0
  71. velune/context/__init__.py +28 -0
  72. velune/context/assembler.py +240 -0
  73. velune/context/budget.py +97 -0
  74. velune/context/extractive.py +95 -0
  75. velune/context/prompt_adaptation.py +480 -0
  76. velune/context/sections.py +99 -0
  77. velune/context/token_counter.py +134 -0
  78. velune/context/utilization.py +33 -0
  79. velune/context/window.py +63 -0
  80. velune/core/__init__.py +89 -0
  81. velune/core/background.py +5 -0
  82. velune/core/config/__init__.py +37 -0
  83. velune/core/errors/__init__.py +90 -0
  84. velune/core/errors/catalog.py +188 -0
  85. velune/core/errors/execution.py +31 -0
  86. velune/core/errors/memory.py +25 -0
  87. velune/core/errors/orchestration.py +31 -0
  88. velune/core/errors/provider.py +37 -0
  89. velune/core/event_loop.py +35 -0
  90. velune/core/logging.py +83 -0
  91. velune/core/paths.py +165 -0
  92. velune/core/runtime.py +113 -0
  93. velune/core/startup_profiler.py +56 -0
  94. velune/core/task_registry.py +117 -0
  95. velune/core/trace.py +83 -0
  96. velune/core/types/__init__.py +48 -0
  97. velune/core/types/agent.py +53 -0
  98. velune/core/types/context.py +42 -0
  99. velune/core/types/inference.py +38 -0
  100. velune/core/types/memory.py +42 -0
  101. velune/core/types/model.py +70 -0
  102. velune/core/types/provider.py +62 -0
  103. velune/core/types/repository.py +38 -0
  104. velune/core/types/task.py +61 -0
  105. velune/core/types/workspace.py +28 -0
  106. velune/daemon/client.py +13 -0
  107. velune/daemon/server.py +127 -0
  108. velune/daemon/transport.py +179 -0
  109. velune/events.py +204 -0
  110. velune/execution/__init__.py +22 -0
  111. velune/execution/benchmarker.py +315 -0
  112. velune/execution/cancellation.py +53 -0
  113. velune/execution/checkpointer.py +130 -0
  114. velune/execution/command_spec.py +165 -0
  115. velune/execution/diff_preview.py +197 -0
  116. velune/execution/executor.py +181 -0
  117. velune/execution/module.py +18 -0
  118. velune/execution/multi_diff.py +67 -0
  119. velune/execution/path_guard.py +74 -0
  120. velune/execution/planner.py +91 -0
  121. velune/execution/rollback.py +89 -0
  122. velune/execution/sandbox.py +268 -0
  123. velune/execution/validator.py +115 -0
  124. velune/hardware/__init__.py +1 -0
  125. velune/hardware/detector.py +192 -0
  126. velune/kernel/__init__.py +55 -0
  127. velune/kernel/bootstrap.py +125 -0
  128. velune/kernel/config.py +426 -0
  129. velune/kernel/entrypoint.py +78 -0
  130. velune/kernel/health.py +54 -0
  131. velune/kernel/lifecycle.py +143 -0
  132. velune/kernel/module.py +17 -0
  133. velune/kernel/modules.py +23 -0
  134. velune/kernel/registry.py +96 -0
  135. velune/kernel/schemas.py +28 -0
  136. velune/main.py +9 -0
  137. velune/mcp/__init__.py +9 -0
  138. velune/mcp/client.py +115 -0
  139. velune/mcp/config.py +19 -0
  140. velune/mcp/server.py +624 -0
  141. velune/memory/__init__.py +32 -0
  142. velune/memory/compaction.py +506 -0
  143. velune/memory/embedding_pipeline.py +241 -0
  144. velune/memory/lifecycle.py +680 -0
  145. velune/memory/module.py +218 -0
  146. velune/memory/prioritizer.py +67 -0
  147. velune/memory/storage/episodic_schema.sql +53 -0
  148. velune/memory/storage/lancedb_store.py +282 -0
  149. velune/memory/storage/sqlite_manager.py +369 -0
  150. velune/memory/storage/sqlite_pool.py +149 -0
  151. velune/memory/tiers/episodic.py +588 -0
  152. velune/memory/tiers/graph.py +378 -0
  153. velune/memory/tiers/lineage.py +416 -0
  154. velune/memory/tiers/semantic.py +475 -0
  155. velune/memory/tiers/working.py +168 -0
  156. velune/memory/vitality.py +132 -0
  157. velune/models/__init__.py +15 -0
  158. velune/models/family.py +76 -0
  159. velune/models/module.py +20 -0
  160. velune/models/probes.py +192 -0
  161. velune/models/profile_cache.py +84 -0
  162. velune/models/profiler.py +108 -0
  163. velune/models/registry.py +251 -0
  164. velune/models/scorer.py +233 -0
  165. velune/models/specializations.py +205 -0
  166. velune/orchestration/__init__.py +19 -0
  167. velune/orchestration/engine.py +239 -0
  168. velune/orchestration/module.py +15 -0
  169. velune/orchestration/role_assignments.py +82 -0
  170. velune/orchestration/schemas.py +98 -0
  171. velune/plugins/__init__.py +20 -0
  172. velune/plugins/hooks.py +50 -0
  173. velune/plugins/loader.py +161 -0
  174. velune/plugins/registry.py +56 -0
  175. velune/plugins/schemas.py +21 -0
  176. velune/providers/__init__.py +23 -0
  177. velune/providers/adapters/anthropic.py +257 -0
  178. velune/providers/adapters/fireworks.py +115 -0
  179. velune/providers/adapters/google.py +234 -0
  180. velune/providers/adapters/groq.py +151 -0
  181. velune/providers/adapters/huggingface.py +210 -0
  182. velune/providers/adapters/llamacpp.py +208 -0
  183. velune/providers/adapters/lmstudio.py +175 -0
  184. velune/providers/adapters/ollama.py +233 -0
  185. velune/providers/adapters/openai.py +213 -0
  186. velune/providers/adapters/openrouter.py +81 -0
  187. velune/providers/adapters/together.py +134 -0
  188. velune/providers/adapters/xai.py +60 -0
  189. velune/providers/base.py +86 -0
  190. velune/providers/benchmarker.py +138 -0
  191. velune/providers/discovery/__init__.py +33 -0
  192. velune/providers/discovery/anthropic.py +79 -0
  193. velune/providers/discovery/benchmarks.py +44 -0
  194. velune/providers/discovery/classifier.py +69 -0
  195. velune/providers/discovery/fireworks.py +95 -0
  196. velune/providers/discovery/gguf.py +88 -0
  197. velune/providers/discovery/google.py +95 -0
  198. velune/providers/discovery/gpu.py +117 -0
  199. velune/providers/discovery/groq.py +21 -0
  200. velune/providers/discovery/huggingface.py +67 -0
  201. velune/providers/discovery/lmstudio.py +80 -0
  202. velune/providers/discovery/ollama.py +162 -0
  203. velune/providers/discovery/openai.py +96 -0
  204. velune/providers/discovery/openrouter.py +113 -0
  205. velune/providers/discovery/scanner.py +115 -0
  206. velune/providers/discovery/together.py +114 -0
  207. velune/providers/discovery/xai.py +57 -0
  208. velune/providers/health.py +67 -0
  209. velune/providers/health_monitor.py +169 -0
  210. velune/providers/keystore.py +142 -0
  211. velune/providers/local_paths.py +49 -0
  212. velune/providers/local_resolver.py +229 -0
  213. velune/providers/module.py +51 -0
  214. velune/providers/ollama_manager.py +193 -0
  215. velune/providers/registry.py +220 -0
  216. velune/providers/router.py +255 -0
  217. velune/providers/task_classifier.py +288 -0
  218. velune/py.typed +0 -0
  219. velune/repository/__init__.py +33 -0
  220. velune/repository/analyzer.py +127 -0
  221. velune/repository/ast_parser.py +822 -0
  222. velune/repository/blast_radius.py +298 -0
  223. velune/repository/boundary_classifier.py +295 -0
  224. velune/repository/cognition.py +316 -0
  225. velune/repository/grapher.py +179 -0
  226. velune/repository/import_graph.py +263 -0
  227. velune/repository/incremental_indexer.py +275 -0
  228. velune/repository/index_state.py +96 -0
  229. velune/repository/indexer.py +243 -0
  230. velune/repository/module.py +17 -0
  231. velune/repository/parser.py +474 -0
  232. velune/repository/project_type.py +300 -0
  233. velune/repository/rename_journal.py +287 -0
  234. velune/repository/scanner.py +193 -0
  235. velune/repository/schemas.py +102 -0
  236. velune/repository/symbol_registry.py +365 -0
  237. velune/repository/tracker.py +252 -0
  238. velune/retrieval/__init__.py +27 -0
  239. velune/retrieval/cache.py +110 -0
  240. velune/retrieval/fast_path.py +391 -0
  241. velune/retrieval/graph.py +124 -0
  242. velune/retrieval/hybrid.py +271 -0
  243. velune/retrieval/keyword.py +131 -0
  244. velune/retrieval/module.py +26 -0
  245. velune/retrieval/pipeline.py +303 -0
  246. velune/retrieval/reranker.py +102 -0
  247. velune/retrieval/schemas.py +59 -0
  248. velune/retrieval/slow_path.py +364 -0
  249. velune/retrieval/vector.py +203 -0
  250. velune/telemetry/__init__.py +59 -0
  251. velune/telemetry/cognition.py +267 -0
  252. velune/telemetry/cost_estimator.py +92 -0
  253. velune/telemetry/debug.py +304 -0
  254. velune/telemetry/doctor.py +244 -0
  255. velune/telemetry/logging.py +286 -0
  256. velune/telemetry/spans.py +277 -0
  257. velune/telemetry/token_tracker.py +140 -0
  258. velune/telemetry/usage_tracker.py +340 -0
  259. velune/tools/__init__.py +41 -0
  260. velune/tools/base/registry.py +87 -0
  261. velune/tools/base/tool.py +63 -0
  262. velune/tools/code/navigate.py +116 -0
  263. velune/tools/code/search.py +123 -0
  264. velune/tools/filesystem/read.py +75 -0
  265. velune/tools/filesystem/search.py +136 -0
  266. velune/tools/filesystem/write.py +163 -0
  267. velune/tools/git/history.py +177 -0
  268. velune/tools/git/operations.py +122 -0
  269. velune/tools/git/state.py +121 -0
  270. velune/tools/module.py +81 -0
  271. velune/tools/terminal/execute.py +72 -0
  272. velune/tools/terminal/history.py +47 -0
  273. velune/tools/web/fetch.py +55 -0
  274. velune/tools/web/validator.py +122 -0
  275. velune_cli-0.9.0.dist-info/METADATA +518 -0
  276. velune_cli-0.9.0.dist-info/RECORD +279 -0
  277. velune_cli-0.9.0.dist-info/WHEEL +4 -0
  278. velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
  279. velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,474 @@
1
+ """Tree-sitter and AST multi-language parser with regex fallbacks.
2
+
3
+ This module provides :class:`RepositorySnapshotParser` — the synchronous
4
+ parser that returns :class:`~velune.repository.schemas.RepositorySymbol` /
5
+ :class:`~velune.repository.schemas.RepositoryEdge` objects for use by the
6
+ indexer, incremental indexer, and tools. It is intentionally distinct from
7
+ :class:`~velune.repository.ast_parser.ASTParser`, which is the async parser
8
+ used by :class:`~velune.repository.symbol_registry.SymbolRegistry` and
9
+ :class:`~velune.repository.rename_journal.RenameJournal`.
10
+
11
+ The lazy tree-sitter import pattern is preserved here: tree-sitter DLLs are
12
+ not loaded until the first actual ``parse()`` call, avoiding Windows Defender
13
+ real-time-scan startup costs.
14
+
15
+ .. deprecated alias:
16
+ ``ASTParser`` is kept as a backwards-compatible alias for
17
+ ``RepositorySnapshotParser`` so existing callers continue to work while
18
+ they are updated to use the canonical name.
19
+ """
20
+
21
+ import ast
22
+ import re
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ from velune.repository.schemas import (
27
+ RepositoryEdge,
28
+ RepositoryLanguage,
29
+ RepositorySymbol,
30
+ RepositorySymbolKind,
31
+ )
32
+
33
+ # Tree-sitter ships compiled C extensions (.pyd/.so). Importing them at module
34
+ # load time forces Windows to load several DLLs synchronously — each triggering
35
+ # a Defender real-time scan — adding seconds to *every* startup, even when no
36
+ # parsing is requested. We therefore defer all tree-sitter imports until the
37
+ # first actual parse via ``_ensure_tree_sitter``.
38
+ #
39
+ # ``HAS_TREE_SITTER`` starts ``None`` (unknown) and becomes ``True``/``False``
40
+ # after the first lazy load attempt. It remains importable for callers/tests
41
+ # that reference it, but no import cost is paid until parsing happens.
42
+ HAS_TREE_SITTER: bool | None = None
43
+ _TS_LANGUAGES: dict[str, Any] = {}
44
+ _TS_PARSER_CLS: Any = None
45
+
46
+
47
+ def _ensure_tree_sitter() -> bool:
48
+ """Lazily import tree-sitter grammars on first use. Returns availability."""
49
+ global HAS_TREE_SITTER, _TS_PARSER_CLS
50
+ if HAS_TREE_SITTER is not None:
51
+ return HAS_TREE_SITTER
52
+ try:
53
+ import tree_sitter_go
54
+ import tree_sitter_python
55
+ import tree_sitter_rust
56
+ import tree_sitter_typescript
57
+ from tree_sitter import Language, Parser
58
+
59
+ _TS_PARSER_CLS = Parser
60
+ for name, factory in (
61
+ ("python", tree_sitter_python.language),
62
+ ("typescript", tree_sitter_typescript.language_typescript),
63
+ ("javascript", tree_sitter_typescript.language_typescript),
64
+ ("go", tree_sitter_go.language),
65
+ ("rust", tree_sitter_rust.language),
66
+ ):
67
+ try:
68
+ _TS_LANGUAGES[name] = Language(factory())
69
+ except Exception:
70
+ pass
71
+ HAS_TREE_SITTER = True
72
+ except ImportError:
73
+ HAS_TREE_SITTER = False
74
+ return HAS_TREE_SITTER
75
+
76
+
77
+ class RepositorySnapshotParser:
78
+ """Multi-language AST and symbol parser with comprehensive fallbacks.
79
+
80
+ Returns :class:`~velune.repository.schemas.RepositorySymbol` /
81
+ :class:`~velune.repository.schemas.RepositoryEdge` pairs suitable for
82
+ building a :class:`~velune.repository.schemas.RepositorySnapshot`.
83
+
84
+ Uses tree-sitter when available (loaded lazily to avoid DLL startup cost),
85
+ falls back to Python's built-in ``ast`` module for Python files, and
86
+ finally falls back to regex for other languages.
87
+ """
88
+
89
+ def __init__(self) -> None:
90
+ # Languages are populated lazily on first parse — see _ensure_loaded.
91
+ self.languages: dict[str, Any] = {}
92
+ self._parsers: dict[str, Any] = {}
93
+ self._loaded = False
94
+
95
+ def _ensure_loaded(self) -> bool:
96
+ """Populate ``self.languages`` from the lazily-loaded grammar cache."""
97
+ if self._loaded:
98
+ return bool(self.languages)
99
+ self._loaded = True
100
+ if _ensure_tree_sitter():
101
+ self.languages = dict(_TS_LANGUAGES)
102
+ return bool(self.languages)
103
+
104
+ def parse(
105
+ self, file_path: Path, code: str
106
+ ) -> tuple[list[RepositorySymbol], list[RepositoryEdge]]:
107
+ """Parses source code from file_path, leveraging tree-sitter or fallbacks."""
108
+ lang = self._detect_language(file_path)
109
+
110
+ # Try tree-sitter if available (loaded lazily on first parse)
111
+ if self._ensure_loaded() and lang.value in self.languages:
112
+ try:
113
+ return self._parse_tree_sitter(file_path, code, lang)
114
+ except Exception:
115
+ # Fail silently and let fallbacks handle it
116
+ pass
117
+
118
+ # Fallbacks
119
+ if lang == RepositoryLanguage.PYTHON:
120
+ return self._parse_python_ast(file_path, code)
121
+
122
+ return self._parse_regex(file_path, code, lang)
123
+
124
+ def _detect_language(self, file_path: Path) -> RepositoryLanguage:
125
+ """Detect language from file path extension."""
126
+ suffix = file_path.suffix.lower()
127
+ mapping = {
128
+ ".py": RepositoryLanguage.PYTHON,
129
+ ".js": RepositoryLanguage.JAVASCRIPT,
130
+ ".jsx": RepositoryLanguage.JAVASCRIPT,
131
+ ".ts": RepositoryLanguage.TYPESCRIPT,
132
+ ".tsx": RepositoryLanguage.TYPESCRIPT,
133
+ ".go": RepositoryLanguage.GO,
134
+ ".rs": RepositoryLanguage.RUST,
135
+ }
136
+ return mapping.get(suffix, RepositoryLanguage.UNKNOWN)
137
+
138
+ def _parse_tree_sitter(
139
+ self, file_path: Path, code: str, lang: RepositoryLanguage
140
+ ) -> tuple[list[RepositorySymbol], list[RepositoryEdge]]:
141
+ """Uses tree-sitter to parse the code and extract symbols and imports."""
142
+ if lang.value not in self._parsers:
143
+ self._parsers[lang.value] = _TS_PARSER_CLS(self.languages[lang.value])
144
+ parser = self._parsers[lang.value]
145
+ tree = parser.parse(bytes(code, "utf8"))
146
+
147
+ symbols: list[RepositorySymbol] = []
148
+ edges: list[RepositoryEdge] = []
149
+ file_path_str = str(file_path)
150
+
151
+ def walk(node: Any, parent_class: str | None = None) -> None:
152
+ node_type = node.type
153
+ name = ""
154
+ kind = RepositorySymbolKind.UNKNOWN
155
+ current_class = parent_class
156
+
157
+ # Python types
158
+ if lang == RepositoryLanguage.PYTHON:
159
+ if node_type == "class_definition":
160
+ name_node = node.child_by_field_name("name")
161
+ if name_node:
162
+ name = code[name_node.start_byte : name_node.end_byte]
163
+ kind = RepositorySymbolKind.CLASS
164
+ current_class = name
165
+ elif node_type == "function_definition":
166
+ name_node = node.child_by_field_name("name")
167
+ if name_node:
168
+ name = code[name_node.start_byte : name_node.end_byte]
169
+ kind = (
170
+ RepositorySymbolKind.METHOD
171
+ if parent_class
172
+ else RepositorySymbolKind.FUNCTION
173
+ )
174
+ elif node_type in ("import_statement", "import_from_statement"):
175
+ text = code[node.start_byte : node.end_byte]
176
+ for match in re.finditer(r"(?:import|from)\s+([\w.]+)", text):
177
+ target = match.group(1)
178
+ symbols.append(
179
+ RepositorySymbol(
180
+ name=target,
181
+ kind=RepositorySymbolKind.IMPORT,
182
+ file_path=file_path_str,
183
+ line_start=node.start_point[0] + 1,
184
+ line_end=node.end_point[0] + 1,
185
+ )
186
+ )
187
+ edges.append(
188
+ RepositoryEdge(source=file_path_str, target=target, edge_type="imports")
189
+ )
190
+
191
+ # JS/TS types
192
+ elif lang in (RepositoryLanguage.JAVASCRIPT, RepositoryLanguage.TYPESCRIPT):
193
+ if node_type == "class_declaration":
194
+ name_node = node.child_by_field_name("name")
195
+ if name_node:
196
+ name = code[name_node.start_byte : name_node.end_byte]
197
+ kind = RepositorySymbolKind.CLASS
198
+ current_class = name
199
+ elif node_type in ("function_declaration", "method_definition"):
200
+ name_node = node.child_by_field_name("name")
201
+ if name_node:
202
+ name = code[name_node.start_byte : name_node.end_byte]
203
+ kind = (
204
+ RepositorySymbolKind.METHOD
205
+ if parent_class
206
+ else RepositorySymbolKind.FUNCTION
207
+ )
208
+ elif node_type == "import_statement":
209
+ text = code[node.start_byte : node.end_byte]
210
+ match = re.search(r"from\s+['\"]([^'\"]+)['\"]", text)
211
+ if match:
212
+ target = match.group(1)
213
+ symbols.append(
214
+ RepositorySymbol(
215
+ name=target,
216
+ kind=RepositorySymbolKind.IMPORT,
217
+ file_path=file_path_str,
218
+ line_start=node.start_point[0] + 1,
219
+ line_end=node.end_point[0] + 1,
220
+ )
221
+ )
222
+ edges.append(
223
+ RepositoryEdge(source=file_path_str, target=target, edge_type="imports")
224
+ )
225
+
226
+ # Go types
227
+ elif lang == RepositoryLanguage.GO:
228
+ if node_type == "type_declaration":
229
+ text = code[node.start_byte : node.end_byte]
230
+ match = re.search(r"type\s+(\w+)\s+(?:struct|interface)", text)
231
+ if match:
232
+ name = match.group(1)
233
+ kind = RepositorySymbolKind.CLASS
234
+ current_class = name
235
+ elif node_type == "function_declaration":
236
+ name_node = node.child_by_field_name("name")
237
+ if name_node:
238
+ name = code[name_node.start_byte : name_node.end_byte]
239
+ kind = RepositorySymbolKind.FUNCTION
240
+ elif node_type == "method_declaration":
241
+ name_node = node.child_by_field_name("name")
242
+ if name_node:
243
+ name = code[name_node.start_byte : name_node.end_byte]
244
+ kind = RepositorySymbolKind.METHOD
245
+ elif node_type == "import_spec":
246
+ text = code[node.start_byte : node.end_byte]
247
+ match = re.search(r"['\"]([^'\"]+)['\"]", text)
248
+ if match:
249
+ target = match.group(1)
250
+ symbols.append(
251
+ RepositorySymbol(
252
+ name=target,
253
+ kind=RepositorySymbolKind.IMPORT,
254
+ file_path=file_path_str,
255
+ line_start=node.start_point[0] + 1,
256
+ line_end=node.end_point[0] + 1,
257
+ )
258
+ )
259
+ edges.append(
260
+ RepositoryEdge(source=file_path_str, target=target, edge_type="imports")
261
+ )
262
+
263
+ # Rust types
264
+ elif lang == RepositoryLanguage.RUST:
265
+ if node_type in ("struct_item", "impl_item"):
266
+ name_node = node.child_by_field_name("name")
267
+ if name_node:
268
+ name = code[name_node.start_byte : name_node.end_byte]
269
+ kind = RepositorySymbolKind.CLASS
270
+ current_class = name
271
+ elif node_type == "function_item":
272
+ name_node = node.child_by_field_name("name")
273
+ if name_node:
274
+ name = code[name_node.start_byte : name_node.end_byte]
275
+ kind = (
276
+ RepositorySymbolKind.METHOD
277
+ if parent_class
278
+ else RepositorySymbolKind.FUNCTION
279
+ )
280
+ elif node_type == "use_declaration":
281
+ text = code[node.start_byte : node.end_byte]
282
+ match = re.search(r"use\s+([^;]+);", text)
283
+ if match:
284
+ target = match.group(1).strip()
285
+ symbols.append(
286
+ RepositorySymbol(
287
+ name=target,
288
+ kind=RepositorySymbolKind.IMPORT,
289
+ file_path=file_path_str,
290
+ line_start=node.start_point[0] + 1,
291
+ line_end=node.end_point[0] + 1,
292
+ )
293
+ )
294
+ edges.append(
295
+ RepositoryEdge(source=file_path_str, target=target, edge_type="imports")
296
+ )
297
+
298
+ # Append structured symbol if matched
299
+ if name and kind != RepositorySymbolKind.UNKNOWN:
300
+ symbols.append(
301
+ RepositorySymbol(
302
+ name=name,
303
+ kind=kind,
304
+ file_path=file_path_str,
305
+ line_start=node.start_point[0] + 1,
306
+ line_end=node.end_point[0] + 1,
307
+ parent=parent_class,
308
+ )
309
+ )
310
+
311
+ # Recurse children
312
+ for child in node.children:
313
+ walk(child, current_class)
314
+
315
+ walk(tree.root_node)
316
+ return symbols, edges
317
+
318
+ def _parse_python_ast(
319
+ self, file_path: Path, code: str
320
+ ) -> tuple[list[RepositorySymbol], list[RepositoryEdge]]:
321
+ """Standard Python AST library fallback."""
322
+ try:
323
+ tree = ast.parse(code)
324
+ except SyntaxError:
325
+ return [], []
326
+
327
+ symbols: list[RepositorySymbol] = []
328
+ edges: list[RepositoryEdge] = []
329
+ file_path_str = str(file_path)
330
+
331
+ class PythonVisitor(ast.NodeVisitor):
332
+ def __init__(self) -> None:
333
+ self.class_stack: list[str] = []
334
+
335
+ def visit_ClassDef(self, node: ast.ClassDef) -> None:
336
+ doc = ast.get_docstring(node)
337
+ symbols.append(
338
+ RepositorySymbol(
339
+ name=node.name,
340
+ kind=RepositorySymbolKind.CLASS,
341
+ file_path=file_path_str,
342
+ line_start=getattr(node, "lineno", 1),
343
+ line_end=getattr(node, "end_lineno", getattr(node, "lineno", 1)),
344
+ docstring=doc,
345
+ )
346
+ )
347
+ self.class_stack.append(node.name)
348
+ self.generic_visit(node)
349
+ self.class_stack.pop()
350
+
351
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
352
+ self._visit_function(node)
353
+
354
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
355
+ self._visit_function(node)
356
+
357
+ def visit_Import(self, node: ast.Import) -> None:
358
+ for alias in node.names:
359
+ target = alias.name
360
+ edges.append(
361
+ RepositoryEdge(source=file_path_str, target=target, edge_type="imports")
362
+ )
363
+ symbols.append(
364
+ RepositorySymbol(
365
+ name=target,
366
+ kind=RepositorySymbolKind.IMPORT,
367
+ file_path=file_path_str,
368
+ line_start=node.lineno,
369
+ line_end=getattr(node, "end_lineno", node.lineno),
370
+ )
371
+ )
372
+
373
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
374
+ mod = node.module or ""
375
+ for alias in node.names:
376
+ target = f"{mod}.{alias.name}" if mod else alias.name
377
+ edges.append(
378
+ RepositoryEdge(source=file_path_str, target=target, edge_type="imports")
379
+ )
380
+ symbols.append(
381
+ RepositorySymbol(
382
+ name=alias.name,
383
+ kind=RepositorySymbolKind.IMPORT,
384
+ file_path=file_path_str,
385
+ line_start=node.lineno,
386
+ line_end=getattr(node, "end_lineno", node.lineno),
387
+ metadata={"module": mod},
388
+ )
389
+ )
390
+
391
+ def _visit_function(self, node: Any) -> None:
392
+ doc = ast.get_docstring(node)
393
+ kind = (
394
+ RepositorySymbolKind.METHOD
395
+ if self.class_stack
396
+ else RepositorySymbolKind.FUNCTION
397
+ )
398
+ symbols.append(
399
+ RepositorySymbol(
400
+ name=node.name,
401
+ kind=kind,
402
+ file_path=file_path_str,
403
+ line_start=node.lineno,
404
+ line_end=getattr(node, "end_lineno", node.lineno),
405
+ docstring=doc,
406
+ parent=self.class_stack[-1] if self.class_stack else None,
407
+ )
408
+ )
409
+ self.generic_visit(node)
410
+
411
+ PythonVisitor().visit(tree)
412
+ return symbols, edges
413
+
414
+ def _parse_regex(
415
+ self, file_path: Path, code: str, lang: RepositoryLanguage
416
+ ) -> tuple[list[RepositorySymbol], list[RepositoryEdge]]:
417
+ """Universal regex symbol extractor fallback."""
418
+ patterns = {
419
+ RepositoryLanguage.JAVASCRIPT: [
420
+ (r"(?:export\s+)?class\s+(\w+)", RepositorySymbolKind.CLASS),
421
+ (r"(?:export\s+)?function\s+(\w+)", RepositorySymbolKind.FUNCTION),
422
+ (r"import\s+.*?from\s+['\"]([^'\"]+)['\"]", RepositorySymbolKind.IMPORT),
423
+ ],
424
+ RepositoryLanguage.TYPESCRIPT: [
425
+ (r"(?:export\s+)?class\s+(\w+)", RepositorySymbolKind.CLASS),
426
+ (r"(?:export\s+)?(?:async\s+)?function\s+(\w+)", RepositorySymbolKind.FUNCTION),
427
+ (r"import\s+.*?from\s+['\"]([^'\"]+)['\"]", RepositorySymbolKind.IMPORT),
428
+ ],
429
+ RepositoryLanguage.GO: [
430
+ (r"type\s+(\w+)\s+struct", RepositorySymbolKind.CLASS),
431
+ (r"func\s+(\w+)", RepositorySymbolKind.FUNCTION),
432
+ (r"import\s+['\"]([^'\"]+)['\"]", RepositorySymbolKind.IMPORT),
433
+ ],
434
+ RepositoryLanguage.RUST: [
435
+ (r"(?:pub\s+)?struct\s+(\w+)", RepositorySymbolKind.CLASS),
436
+ (r"(?:pub\s+)?(?:async\s+)?fn\s+(\w+)", RepositorySymbolKind.FUNCTION),
437
+ (r"use\s+([^;]+);", RepositorySymbolKind.IMPORT),
438
+ ],
439
+ }
440
+
441
+ symbols: list[RepositorySymbol] = []
442
+ edges: list[RepositoryEdge] = []
443
+ file_path_str = str(file_path)
444
+
445
+ for pattern, kind in patterns.get(lang, []):
446
+ for match in re.finditer(pattern, code):
447
+ value = match.group(1).strip()
448
+ start_char = match.start()
449
+ line_no = code[:start_char].count("\n") + 1
450
+
451
+ if kind == RepositorySymbolKind.IMPORT:
452
+ edges.append(
453
+ RepositoryEdge(source=file_path_str, target=value, edge_type="imports")
454
+ )
455
+
456
+ symbols.append(
457
+ RepositorySymbol(
458
+ name=value,
459
+ kind=kind,
460
+ file_path=file_path_str,
461
+ line_start=line_no,
462
+ line_end=line_no,
463
+ )
464
+ )
465
+
466
+ return symbols, edges
467
+
468
+
469
+ # ---------------------------------------------------------------------------
470
+ # Backwards-compatibility alias
471
+ # Callers that import ``from velune.repository.parser import ASTParser`` will
472
+ # continue to work while they migrate to ``RepositorySnapshotParser``.
473
+ # ---------------------------------------------------------------------------
474
+ ASTParser = RepositorySnapshotParser