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,193 @@
1
+ """Gitignore-aware file scanner for workspace discovery."""
2
+
3
+ import fnmatch
4
+ from pathlib import Path
5
+
6
+ DEFAULT_VELUNEIGNORE = """\
7
+ # Velune index exclusions
8
+ # Secrets and credentials
9
+ .env
10
+ .env.*
11
+ *.pem
12
+ *.key
13
+ *.p12
14
+ *.pfx
15
+ *.crt
16
+ *.jks
17
+ *.keystore
18
+ id_rsa
19
+ id_dsa
20
+ id_ed25519
21
+ id_ecdsa
22
+ .netrc
23
+ .npmrc
24
+ .pypirc
25
+ .aws/
26
+ credentials.json
27
+ service-account.json
28
+ gcp-credentials.json
29
+ secrets/
30
+ credentials/
31
+
32
+ # Large generated files
33
+ *.min.js
34
+ *.min.css
35
+ dist/
36
+ build/
37
+ __pycache__/
38
+ *.pyc
39
+ .mypy_cache/
40
+ .ruff_cache/
41
+
42
+ # Data and media
43
+ *.sqlite
44
+ *.db
45
+ *.csv
46
+ *.parquet
47
+ data/
48
+ datasets/
49
+ *.jpg
50
+ *.jpeg
51
+ *.png
52
+ *.gif
53
+ *.mp4
54
+ *.zip
55
+ *.tar.gz
56
+
57
+ # IDE
58
+ .idea/
59
+ .vscode/settings.json
60
+ *.swp
61
+ """
62
+
63
+
64
+ class FilesystemScanner:
65
+ """Discovers source files inside a workspace, strictly adhering to .gitignore rules."""
66
+
67
+ def __init__(self, root_path: Path) -> None:
68
+ self.root_path = root_path.resolve()
69
+ self.gitignore_patterns = self._load_gitignore() + self._load_veluneignore()
70
+
71
+ def _load_gitignore(self) -> list[str]:
72
+ """Loads and parses .gitignore rules along with core default exclusions."""
73
+ patterns = [
74
+ # Default exclusions
75
+ ".git",
76
+ ".github",
77
+ "__pycache__",
78
+ "*.pyc",
79
+ "*.pyo",
80
+ "*.pyd",
81
+ ".venv",
82
+ "venv",
83
+ "env",
84
+ "node_modules",
85
+ "dist",
86
+ "build",
87
+ ".velune",
88
+ "*.egg-info",
89
+ ".pytest_cache",
90
+ ".mypy_cache",
91
+ "CVS",
92
+ ".DS_Store",
93
+ ]
94
+
95
+ gitignore_path = self.root_path / ".gitignore"
96
+ if gitignore_path.exists():
97
+ try:
98
+ with open(gitignore_path, encoding="utf-8", errors="ignore") as f:
99
+ for line in f:
100
+ line = line.strip()
101
+ if line and not line.startswith("#"):
102
+ # Normalize trailing slash to match standard glob behavior
103
+ if line.endswith("/"):
104
+ line = line[:-1]
105
+ patterns.append(line)
106
+ except Exception:
107
+ pass
108
+
109
+ return patterns
110
+
111
+ def _load_veluneignore(self) -> list[str]:
112
+ """Loads and parses .veluneignore rules from the workspace root."""
113
+ patterns: list[str] = []
114
+ veluneignore_path = self.root_path / ".veluneignore"
115
+ if not veluneignore_path.exists():
116
+ return patterns
117
+ try:
118
+ with open(veluneignore_path, encoding="utf-8", errors="ignore") as f:
119
+ for line in f:
120
+ line = line.strip()
121
+ if line and not line.startswith("#"):
122
+ if line.endswith("/"):
123
+ line = line[:-1]
124
+ patterns.append(line)
125
+ except Exception:
126
+ pass
127
+ return patterns
128
+
129
+ def is_ignored(self, path: Path) -> bool:
130
+ """Determines if a path is excluded by .gitignore or default ignore rules."""
131
+ try:
132
+ rel_path = path.resolve().relative_to(self.root_path)
133
+ except ValueError:
134
+ # Not under root
135
+ return True
136
+
137
+ path_parts = rel_path.parts
138
+ path_str = str(rel_path).replace("\\", "/")
139
+
140
+ for pattern in self.gitignore_patterns:
141
+ # Check direct match or directory component match
142
+ if fnmatch.fnmatch(path_str, pattern) or fnmatch.fnmatch(path_str, f"*/{pattern}"):
143
+ return True
144
+ for part in path_parts:
145
+ if fnmatch.fnmatch(part, pattern):
146
+ return True
147
+
148
+ return False
149
+
150
+ def scan(self, extensions: list[str] | None = None) -> list[Path]:
151
+ """Scans the repository recursively and lists all valid files."""
152
+ files: list[Path] = []
153
+ self._recursive_scan(self.root_path, extensions, files)
154
+ return files
155
+
156
+ def _recursive_scan(
157
+ self, current_dir: Path, extensions: list[str] | None, accumulator: list[Path]
158
+ ) -> None:
159
+ """Recurses through directory structure, skipping ignored directories entirely to optimize speed."""
160
+ try:
161
+ for item in current_dir.iterdir():
162
+ if self.is_ignored(item):
163
+ continue
164
+
165
+ if item.is_dir():
166
+ self._recursive_scan(item, extensions, accumulator)
167
+ elif item.is_file():
168
+ if extensions is None or item.suffix.lower() in extensions:
169
+ accumulator.append(item)
170
+ except PermissionError:
171
+ pass # Fail silently for protected folders
172
+
173
+ def scan_code_files(self) -> list[Path]:
174
+ """Convenience method to scan for common source code extensions."""
175
+ code_extensions = [
176
+ ".py",
177
+ ".js",
178
+ ".ts",
179
+ ".jsx",
180
+ ".tsx",
181
+ ".go",
182
+ ".rs",
183
+ ".java",
184
+ ".c",
185
+ ".cpp",
186
+ ".h",
187
+ ".cs",
188
+ ".php",
189
+ ".rb",
190
+ ".swift",
191
+ ".kt",
192
+ ]
193
+ return self.scan(code_extensions)
@@ -0,0 +1,102 @@
1
+ """Strictly-typed schemas for repository cognition."""
2
+
3
+ import hashlib
4
+ from enum import StrEnum
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field, model_validator
8
+
9
+
10
+ def build_qualified_name(file_path: str, name: str, parent: str | None = None) -> str:
11
+ """Builds a dotted qualified name for a symbol from its file path and parent scope."""
12
+ p = file_path.replace("\\", "/")
13
+
14
+ # Extract package path relative to 'velune/' if absolute
15
+ if "/velune/" in p:
16
+ p = p.split("/velune/", 1)[1]
17
+ p = "velune/" + p
18
+ elif p.startswith("c:") or p.startswith("C:") or ":" in p:
19
+ p = p.split(":", 1)[1].lstrip("/")
20
+
21
+ p = p.rsplit(".", 1)[0]
22
+ dotted = p.replace("/", ".").strip(".")
23
+
24
+ if parent:
25
+ return f"{dotted}.{parent}.{name}"
26
+ return f"{dotted}.{name}"
27
+
28
+
29
+ def compute_symbol_id(file_path: str, qualified_name: str, kind: str) -> str:
30
+ """Computes a stable, deterministic, line-independent SHA256 identity for a symbol."""
31
+ p = file_path.replace("\\", "/")
32
+ if "/velune/" in p:
33
+ p = p.split("/velune/", 1)[1]
34
+ p = "velune/" + p
35
+ elif p.startswith("c:") or p.startswith("C:") or ":" in p:
36
+ p = p.split(":", 1)[1].lstrip("/")
37
+
38
+ payload = f"{p}:{qualified_name}:{kind.lower()}"
39
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()
40
+
41
+
42
+ class RepositoryLanguage(StrEnum):
43
+ PYTHON = "python"
44
+ JAVASCRIPT = "javascript"
45
+ TYPESCRIPT = "typescript"
46
+ GO = "go"
47
+ RUST = "rust"
48
+ UNKNOWN = "unknown"
49
+
50
+
51
+ class RepositorySymbolKind(StrEnum):
52
+ CLASS = "class"
53
+ FUNCTION = "function"
54
+ METHOD = "method"
55
+ IMPORT = "import"
56
+ UNKNOWN = "unknown"
57
+
58
+
59
+ class RepositorySymbol(BaseModel):
60
+ name: str
61
+ kind: RepositorySymbolKind
62
+ file_path: str
63
+ line_start: int = 1
64
+ line_end: int = 1
65
+ docstring: str | None = None
66
+ parent: str | None = None
67
+ metadata: dict[str, Any] = Field(default_factory=dict)
68
+ symbol_id: str | None = None
69
+ qualified_name: str | None = None
70
+
71
+ @model_validator(mode="after")
72
+ def populate_identity(self) -> "RepositorySymbol":
73
+ """Ensures stable symbol_id and qualified_name are automatically calculated if missing."""
74
+ if not self.qualified_name:
75
+ self.qualified_name = build_qualified_name(self.file_path, self.name, self.parent)
76
+ if not self.symbol_id:
77
+ self.symbol_id = compute_symbol_id(self.file_path, self.qualified_name, self.kind.value)
78
+ return self
79
+
80
+
81
+ class RepositoryFile(BaseModel):
82
+ path: str
83
+ language: RepositoryLanguage
84
+ size_bytes: int
85
+ sha256: str
86
+ symbols: list[RepositorySymbol] = Field(default_factory=list)
87
+ metadata: dict[str, Any] = Field(default_factory=dict)
88
+
89
+
90
+ class RepositoryEdge(BaseModel):
91
+ source: str
92
+ target: str
93
+ edge_type: str # e.g., "imports", "calls", "contains"
94
+ weight: float = 1.0
95
+
96
+
97
+ class RepositorySnapshot(BaseModel):
98
+ root_path: str
99
+ files: list[RepositoryFile] = Field(default_factory=list)
100
+ symbols: list[RepositorySymbol] = Field(default_factory=list)
101
+ edges: list[RepositoryEdge] = Field(default_factory=list)
102
+ summary: dict[str, Any] = Field(default_factory=dict)
@@ -0,0 +1,365 @@
1
+ """SQLite-backed symbol registry for tracking code symbols."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+
8
+ import aiosqlite
9
+
10
+ from velune.repository.ast_parser import Symbol, SymbolKind
11
+
12
+ logger = logging.getLogger("velune.repository.symbol_registry")
13
+
14
+
15
+ class SymbolRegistry:
16
+ """Persistent symbol registry using SQLite."""
17
+
18
+ def __init__(self, db_path: Path) -> None:
19
+ """Initialize symbol registry.
20
+
21
+ Args:
22
+ db_path: Path to SQLite database file
23
+ """
24
+ self.db_path = db_path
25
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
26
+
27
+ async def initialize(self) -> None:
28
+ """Initialize database schema."""
29
+ async with aiosqlite.connect(str(self.db_path)) as db:
30
+ await db.executescript("""
31
+ CREATE TABLE IF NOT EXISTS symbols (
32
+ id TEXT PRIMARY KEY,
33
+ name TEXT NOT NULL,
34
+ kind TEXT NOT NULL,
35
+ file_path TEXT NOT NULL,
36
+ line_start INTEGER NOT NULL,
37
+ line_end INTEGER NOT NULL,
38
+ docstring TEXT,
39
+ parameters TEXT, -- JSON array
40
+ return_type TEXT,
41
+ is_exported BOOLEAN DEFAULT 0,
42
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
43
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
44
+ UNIQUE(file_path, name, line_start)
45
+ );
46
+
47
+ CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_path);
48
+ CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
49
+ CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);
50
+ """)
51
+ await db.commit()
52
+
53
+ async def upsert_symbols(self, file_path: str, symbols: list[Symbol]) -> None:
54
+ """Store or update symbols from a file.
55
+
56
+ Updates existing symbols by (file_path, name, line_start).
57
+ Preserves stable IDs when possible.
58
+
59
+ Args:
60
+ file_path: Relative path to file
61
+ symbols: List of symbols to store
62
+ """
63
+ import json
64
+
65
+ async with aiosqlite.connect(str(self.db_path)) as db:
66
+ db.row_factory = aiosqlite.Row
67
+ for symbol in symbols:
68
+ params = json.dumps(symbol.parameters) if symbol.parameters else None
69
+
70
+ # 1. Check if ID exists
71
+ async with db.execute("SELECT 1 FROM symbols WHERE id = ?", (symbol.id,)) as cursor:
72
+ exists_by_id = await cursor.fetchone()
73
+
74
+ if exists_by_id:
75
+ # Update by ID (rename or modification)
76
+ await db.execute(
77
+ """
78
+ UPDATE symbols SET
79
+ name = ?,
80
+ kind = ?,
81
+ file_path = ?,
82
+ line_start = ?,
83
+ line_end = ?,
84
+ docstring = ?,
85
+ parameters = ?,
86
+ return_type = ?,
87
+ is_exported = ?,
88
+ updated_at = CURRENT_TIMESTAMP
89
+ WHERE id = ?
90
+ """,
91
+ (
92
+ symbol.name,
93
+ symbol.kind.value,
94
+ file_path,
95
+ symbol.line_start,
96
+ symbol.line_end,
97
+ symbol.docstring,
98
+ params,
99
+ symbol.return_type,
100
+ 1 if symbol.is_exported else 0,
101
+ symbol.id,
102
+ ),
103
+ )
104
+ else:
105
+ # 2. Check if file_path, name, line_start exists
106
+ async with db.execute(
107
+ "SELECT id FROM symbols WHERE file_path = ? AND name = ? AND line_start = ?",
108
+ (file_path, symbol.name, symbol.line_start),
109
+ ) as cursor:
110
+ existing_row = await cursor.fetchone()
111
+
112
+ if existing_row:
113
+ # Update by file_path/name/line_start, preserving existing ID
114
+ existing_id = existing_row[0]
115
+ await db.execute(
116
+ """
117
+ UPDATE symbols SET
118
+ kind = ?,
119
+ line_end = ?,
120
+ docstring = ?,
121
+ parameters = ?,
122
+ return_type = ?,
123
+ is_exported = ?,
124
+ updated_at = CURRENT_TIMESTAMP
125
+ WHERE id = ?
126
+ """,
127
+ (
128
+ symbol.kind.value,
129
+ symbol.line_end,
130
+ symbol.docstring,
131
+ params,
132
+ symbol.return_type,
133
+ 1 if symbol.is_exported else 0,
134
+ existing_id,
135
+ ),
136
+ )
137
+ else:
138
+ # 3. Insert new row
139
+ await db.execute(
140
+ """
141
+ INSERT INTO symbols (
142
+ id, name, kind, file_path, line_start, line_end,
143
+ docstring, parameters, return_type, is_exported
144
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
145
+ """,
146
+ (
147
+ symbol.id,
148
+ symbol.name,
149
+ symbol.kind.value,
150
+ file_path,
151
+ symbol.line_start,
152
+ symbol.line_end,
153
+ symbol.docstring,
154
+ params,
155
+ symbol.return_type,
156
+ 1 if symbol.is_exported else 0,
157
+ ),
158
+ )
159
+
160
+ await db.commit()
161
+
162
+ async def get_symbols(self, file_path: str) -> list[Symbol]:
163
+ """Retrieve all symbols from a file.
164
+
165
+ Args:
166
+ file_path: Relative path to file
167
+
168
+ Returns:
169
+ List of symbols in the file
170
+ """
171
+ import json
172
+
173
+ symbols: list[Symbol] = []
174
+ async with aiosqlite.connect(str(self.db_path)) as db:
175
+ async with db.execute(
176
+ "SELECT id, name, kind, file_path, line_start, line_end, "
177
+ "docstring, parameters, return_type, is_exported FROM symbols "
178
+ "WHERE file_path = ? ORDER BY line_start",
179
+ (file_path,),
180
+ ) as cursor:
181
+ async for row in cursor:
182
+ params = json.loads(row[7]) if row[7] else []
183
+ symbol = Symbol(
184
+ id=row[0],
185
+ name=row[1],
186
+ kind=SymbolKind(row[2]),
187
+ file_path=row[3],
188
+ line_start=row[4],
189
+ line_end=row[5],
190
+ docstring=row[6],
191
+ parameters=params,
192
+ return_type=row[8],
193
+ is_exported=bool(row[9]),
194
+ )
195
+ symbols.append(symbol)
196
+
197
+ return symbols
198
+
199
+ async def search_symbols(self, name_pattern: str) -> list[Symbol]:
200
+ """Search for symbols by name pattern (LIKE search).
201
+
202
+ Args:
203
+ name_pattern: SQL LIKE pattern (e.g., 'validate%')
204
+
205
+ Returns:
206
+ List of matching symbols
207
+ """
208
+ import json
209
+
210
+ symbols: list[Symbol] = []
211
+ async with aiosqlite.connect(str(self.db_path)) as db:
212
+ async with db.execute(
213
+ "SELECT id, name, kind, file_path, line_start, line_end, "
214
+ "docstring, parameters, return_type, is_exported FROM symbols "
215
+ "WHERE name LIKE ? ORDER BY file_path, line_start",
216
+ (name_pattern,),
217
+ ) as cursor:
218
+ async for row in cursor:
219
+ params = json.loads(row[7]) if row[7] else []
220
+ symbol = Symbol(
221
+ id=row[0],
222
+ name=row[1],
223
+ kind=SymbolKind(row[2]),
224
+ file_path=row[3],
225
+ line_start=row[4],
226
+ line_end=row[5],
227
+ docstring=row[6],
228
+ parameters=params,
229
+ return_type=row[8],
230
+ is_exported=bool(row[9]),
231
+ )
232
+ symbols.append(symbol)
233
+
234
+ return symbols
235
+
236
+ async def get_symbol_by_id(self, symbol_id: str) -> Symbol | None:
237
+ """Retrieve a symbol by its stable ID.
238
+
239
+ Args:
240
+ symbol_id: Unique symbol ID
241
+
242
+ Returns:
243
+ Symbol if found, None otherwise
244
+ """
245
+ import json
246
+
247
+ async with aiosqlite.connect(str(self.db_path)) as db:
248
+ async with db.execute(
249
+ "SELECT id, name, kind, file_path, line_start, line_end, "
250
+ "docstring, parameters, return_type, is_exported FROM symbols "
251
+ "WHERE id = ?",
252
+ (symbol_id,),
253
+ ) as cursor:
254
+ row = await cursor.fetchone()
255
+
256
+ if not row:
257
+ return None
258
+
259
+ params = json.loads(row[7]) if row[7] else []
260
+ return Symbol(
261
+ id=row[0],
262
+ name=row[1],
263
+ kind=SymbolKind(row[2]),
264
+ file_path=row[3],
265
+ line_start=row[4],
266
+ line_end=row[5],
267
+ docstring=row[6],
268
+ parameters=params,
269
+ return_type=row[8],
270
+ is_exported=bool(row[9]),
271
+ )
272
+
273
+ async def remove_file_symbols(self, file_path: str) -> None:
274
+ """Remove all symbols from a file.
275
+
276
+ Called when a file is deleted or removed from tracking.
277
+
278
+ Args:
279
+ file_path: Relative path to file
280
+ """
281
+ async with aiosqlite.connect(str(self.db_path)) as db:
282
+ await db.execute("DELETE FROM symbols WHERE file_path = ?", (file_path,))
283
+ await db.commit()
284
+
285
+ async def get_all_symbols(self) -> list[Symbol]:
286
+ """Retrieve all symbols in registry.
287
+
288
+ Returns:
289
+ List of all symbols
290
+ """
291
+ import json
292
+
293
+ symbols: list[Symbol] = []
294
+ async with aiosqlite.connect(str(self.db_path)) as db:
295
+ async with db.execute(
296
+ "SELECT id, name, kind, file_path, line_start, line_end, "
297
+ "docstring, parameters, return_type, is_exported FROM symbols "
298
+ "ORDER BY file_path, line_start"
299
+ ) as cursor:
300
+ async for row in cursor:
301
+ params = json.loads(row[7]) if row[7] else []
302
+ symbol = Symbol(
303
+ id=row[0],
304
+ name=row[1],
305
+ kind=SymbolKind(row[2]),
306
+ file_path=row[3],
307
+ line_start=row[4],
308
+ line_end=row[5],
309
+ docstring=row[6],
310
+ parameters=params,
311
+ return_type=row[8],
312
+ is_exported=bool(row[9]),
313
+ )
314
+ symbols.append(symbol)
315
+
316
+ return symbols
317
+
318
+ async def get_symbols_by_kind(self, kind: SymbolKind) -> list[Symbol]:
319
+ """Retrieve all symbols of a specific kind.
320
+
321
+ Args:
322
+ kind: Symbol kind to filter by
323
+
324
+ Returns:
325
+ List of symbols of specified kind
326
+ """
327
+ import json
328
+
329
+ symbols: list[Symbol] = []
330
+ async with aiosqlite.connect(str(self.db_path)) as db:
331
+ async with db.execute(
332
+ "SELECT id, name, kind, file_path, line_start, line_end, "
333
+ "docstring, parameters, return_type, is_exported FROM symbols "
334
+ "WHERE kind = ? ORDER BY file_path, line_start",
335
+ (kind.value,),
336
+ ) as cursor:
337
+ async for row in cursor:
338
+ params = json.loads(row[7]) if row[7] else []
339
+ symbol = Symbol(
340
+ id=row[0],
341
+ name=row[1],
342
+ kind=SymbolKind(row[2]),
343
+ file_path=row[3],
344
+ line_start=row[4],
345
+ line_end=row[5],
346
+ docstring=row[6],
347
+ parameters=params,
348
+ return_type=row[8],
349
+ is_exported=bool(row[9]),
350
+ )
351
+ symbols.append(symbol)
352
+
353
+ return symbols
354
+
355
+ async def count_symbols(self) -> int:
356
+ """Count total symbols in registry.
357
+
358
+ Returns:
359
+ Total number of symbols
360
+ """
361
+ async with aiosqlite.connect(str(self.db_path)) as db:
362
+ async with db.execute("SELECT COUNT(*) FROM symbols") as cursor:
363
+ row = await cursor.fetchone()
364
+
365
+ return row[0] if row else 0