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.
- velune/__init__.py +5 -0
- velune/__main__.py +6 -0
- velune/cli/__init__.py +5 -0
- velune/cli/app.py +208 -0
- velune/cli/autocomplete.py +80 -0
- velune/cli/banner.py +60 -0
- velune/cli/commands/__init__.py +32 -0
- velune/cli/commands/ask.py +175 -0
- velune/cli/commands/base.py +16 -0
- velune/cli/commands/chat.py +228 -0
- velune/cli/commands/config.py +224 -0
- velune/cli/commands/daemon.py +88 -0
- velune/cli/commands/doctor.py +721 -0
- velune/cli/commands/init.py +170 -0
- velune/cli/commands/mcp.py +82 -0
- velune/cli/commands/memory.py +293 -0
- velune/cli/commands/models.py +683 -0
- velune/cli/commands/preflight.py +95 -0
- velune/cli/commands/run.py +270 -0
- velune/cli/commands/setup.py +184 -0
- velune/cli/commands/workspace.py +249 -0
- velune/cli/context.py +36 -0
- velune/cli/councilmodel_ui.py +199 -0
- velune/cli/display/council_view.py +254 -0
- velune/cli/display/memory_view.py +126 -0
- velune/cli/display/panels.py +35 -0
- velune/cli/display/progress.py +25 -0
- velune/cli/display/themes.py +25 -0
- velune/cli/main.py +15 -0
- velune/cli/model_selector.py +51 -0
- velune/cli/modes.py +86 -0
- velune/cli/pull_ui.py +123 -0
- velune/cli/registry.py +80 -0
- velune/cli/rendering/__init__.py +5 -0
- velune/cli/rendering/error_panel.py +79 -0
- velune/cli/rendering/markdown.py +63 -0
- velune/cli/repl.py +1855 -0
- velune/cli/session_manager.py +71 -0
- velune/cli/slash_commands.py +37 -0
- velune/cli/theme.py +8 -0
- velune/cognition/__init__.py +23 -0
- velune/cognition/agents/__init__.py +7 -0
- velune/cognition/agents/coder.py +209 -0
- velune/cognition/agents/planner.py +156 -0
- velune/cognition/agents/reviewer.py +195 -0
- velune/cognition/arbitrator.py +220 -0
- velune/cognition/architecture.py +415 -0
- velune/cognition/budget.py +65 -0
- velune/cognition/council/__init__.py +47 -0
- velune/cognition/council/base.py +217 -0
- velune/cognition/council/challenger.py +74 -0
- velune/cognition/council/coder.py +79 -0
- velune/cognition/council/critic_agent.py +43 -0
- velune/cognition/council/critic_configs.py +111 -0
- velune/cognition/council/critics.py +41 -0
- velune/cognition/council/debate.py +46 -0
- velune/cognition/council/factory.py +140 -0
- velune/cognition/council/messages.py +56 -0
- velune/cognition/council/planner.py +124 -0
- velune/cognition/council/reviewer.py +74 -0
- velune/cognition/council/synthesizer.py +67 -0
- velune/cognition/council/tiers.py +188 -0
- velune/cognition/council_orchestrator.py +282 -0
- velune/cognition/firewall.py +354 -0
- velune/cognition/module.py +46 -0
- velune/cognition/orchestrator.py +1205 -0
- velune/cognition/personality.py +238 -0
- velune/cognition/state.py +104 -0
- velune/cognition/style_resolver.py +64 -0
- velune/cognition/verification.py +205 -0
- velune/context/__init__.py +28 -0
- velune/context/assembler.py +240 -0
- velune/context/budget.py +97 -0
- velune/context/extractive.py +95 -0
- velune/context/prompt_adaptation.py +480 -0
- velune/context/sections.py +99 -0
- velune/context/token_counter.py +134 -0
- velune/context/utilization.py +33 -0
- velune/context/window.py +63 -0
- velune/core/__init__.py +89 -0
- velune/core/background.py +5 -0
- velune/core/config/__init__.py +37 -0
- velune/core/errors/__init__.py +90 -0
- velune/core/errors/catalog.py +188 -0
- velune/core/errors/execution.py +31 -0
- velune/core/errors/memory.py +25 -0
- velune/core/errors/orchestration.py +31 -0
- velune/core/errors/provider.py +37 -0
- velune/core/event_loop.py +35 -0
- velune/core/logging.py +83 -0
- velune/core/paths.py +165 -0
- velune/core/runtime.py +113 -0
- velune/core/startup_profiler.py +56 -0
- velune/core/task_registry.py +117 -0
- velune/core/trace.py +83 -0
- velune/core/types/__init__.py +48 -0
- velune/core/types/agent.py +53 -0
- velune/core/types/context.py +42 -0
- velune/core/types/inference.py +38 -0
- velune/core/types/memory.py +42 -0
- velune/core/types/model.py +70 -0
- velune/core/types/provider.py +62 -0
- velune/core/types/repository.py +38 -0
- velune/core/types/task.py +61 -0
- velune/core/types/workspace.py +28 -0
- velune/daemon/client.py +13 -0
- velune/daemon/server.py +127 -0
- velune/daemon/transport.py +179 -0
- velune/events.py +204 -0
- velune/execution/__init__.py +22 -0
- velune/execution/benchmarker.py +315 -0
- velune/execution/cancellation.py +53 -0
- velune/execution/checkpointer.py +130 -0
- velune/execution/command_spec.py +165 -0
- velune/execution/diff_preview.py +197 -0
- velune/execution/executor.py +181 -0
- velune/execution/module.py +18 -0
- velune/execution/multi_diff.py +67 -0
- velune/execution/path_guard.py +74 -0
- velune/execution/planner.py +91 -0
- velune/execution/rollback.py +89 -0
- velune/execution/sandbox.py +268 -0
- velune/execution/validator.py +115 -0
- velune/hardware/__init__.py +1 -0
- velune/hardware/detector.py +192 -0
- velune/kernel/__init__.py +55 -0
- velune/kernel/bootstrap.py +125 -0
- velune/kernel/config.py +426 -0
- velune/kernel/entrypoint.py +78 -0
- velune/kernel/health.py +54 -0
- velune/kernel/lifecycle.py +143 -0
- velune/kernel/module.py +17 -0
- velune/kernel/modules.py +23 -0
- velune/kernel/registry.py +96 -0
- velune/kernel/schemas.py +28 -0
- velune/main.py +9 -0
- velune/mcp/__init__.py +9 -0
- velune/mcp/client.py +115 -0
- velune/mcp/config.py +19 -0
- velune/mcp/server.py +624 -0
- velune/memory/__init__.py +32 -0
- velune/memory/compaction.py +506 -0
- velune/memory/embedding_pipeline.py +241 -0
- velune/memory/lifecycle.py +680 -0
- velune/memory/module.py +218 -0
- velune/memory/prioritizer.py +67 -0
- velune/memory/storage/episodic_schema.sql +53 -0
- velune/memory/storage/lancedb_store.py +282 -0
- velune/memory/storage/sqlite_manager.py +369 -0
- velune/memory/storage/sqlite_pool.py +149 -0
- velune/memory/tiers/episodic.py +588 -0
- velune/memory/tiers/graph.py +378 -0
- velune/memory/tiers/lineage.py +416 -0
- velune/memory/tiers/semantic.py +475 -0
- velune/memory/tiers/working.py +168 -0
- velune/memory/vitality.py +132 -0
- velune/models/__init__.py +15 -0
- velune/models/family.py +76 -0
- velune/models/module.py +20 -0
- velune/models/probes.py +192 -0
- velune/models/profile_cache.py +84 -0
- velune/models/profiler.py +108 -0
- velune/models/registry.py +251 -0
- velune/models/scorer.py +233 -0
- velune/models/specializations.py +205 -0
- velune/orchestration/__init__.py +19 -0
- velune/orchestration/engine.py +239 -0
- velune/orchestration/module.py +15 -0
- velune/orchestration/role_assignments.py +82 -0
- velune/orchestration/schemas.py +98 -0
- velune/plugins/__init__.py +20 -0
- velune/plugins/hooks.py +50 -0
- velune/plugins/loader.py +161 -0
- velune/plugins/registry.py +56 -0
- velune/plugins/schemas.py +21 -0
- velune/providers/__init__.py +23 -0
- velune/providers/adapters/anthropic.py +257 -0
- velune/providers/adapters/fireworks.py +115 -0
- velune/providers/adapters/google.py +234 -0
- velune/providers/adapters/groq.py +151 -0
- velune/providers/adapters/huggingface.py +210 -0
- velune/providers/adapters/llamacpp.py +208 -0
- velune/providers/adapters/lmstudio.py +175 -0
- velune/providers/adapters/ollama.py +233 -0
- velune/providers/adapters/openai.py +213 -0
- velune/providers/adapters/openrouter.py +81 -0
- velune/providers/adapters/together.py +134 -0
- velune/providers/adapters/xai.py +60 -0
- velune/providers/base.py +86 -0
- velune/providers/benchmarker.py +138 -0
- velune/providers/discovery/__init__.py +33 -0
- velune/providers/discovery/anthropic.py +79 -0
- velune/providers/discovery/benchmarks.py +44 -0
- velune/providers/discovery/classifier.py +69 -0
- velune/providers/discovery/fireworks.py +95 -0
- velune/providers/discovery/gguf.py +88 -0
- velune/providers/discovery/google.py +95 -0
- velune/providers/discovery/gpu.py +117 -0
- velune/providers/discovery/groq.py +21 -0
- velune/providers/discovery/huggingface.py +67 -0
- velune/providers/discovery/lmstudio.py +80 -0
- velune/providers/discovery/ollama.py +162 -0
- velune/providers/discovery/openai.py +96 -0
- velune/providers/discovery/openrouter.py +113 -0
- velune/providers/discovery/scanner.py +115 -0
- velune/providers/discovery/together.py +114 -0
- velune/providers/discovery/xai.py +57 -0
- velune/providers/health.py +67 -0
- velune/providers/health_monitor.py +169 -0
- velune/providers/keystore.py +142 -0
- velune/providers/local_paths.py +49 -0
- velune/providers/local_resolver.py +229 -0
- velune/providers/module.py +51 -0
- velune/providers/ollama_manager.py +193 -0
- velune/providers/registry.py +220 -0
- velune/providers/router.py +255 -0
- velune/providers/task_classifier.py +288 -0
- velune/py.typed +0 -0
- velune/repository/__init__.py +33 -0
- velune/repository/analyzer.py +127 -0
- velune/repository/ast_parser.py +822 -0
- velune/repository/blast_radius.py +298 -0
- velune/repository/boundary_classifier.py +295 -0
- velune/repository/cognition.py +316 -0
- velune/repository/grapher.py +179 -0
- velune/repository/import_graph.py +263 -0
- velune/repository/incremental_indexer.py +275 -0
- velune/repository/index_state.py +96 -0
- velune/repository/indexer.py +243 -0
- velune/repository/module.py +17 -0
- velune/repository/parser.py +474 -0
- velune/repository/project_type.py +300 -0
- velune/repository/rename_journal.py +287 -0
- velune/repository/scanner.py +193 -0
- velune/repository/schemas.py +102 -0
- velune/repository/symbol_registry.py +365 -0
- velune/repository/tracker.py +252 -0
- velune/retrieval/__init__.py +27 -0
- velune/retrieval/cache.py +110 -0
- velune/retrieval/fast_path.py +391 -0
- velune/retrieval/graph.py +124 -0
- velune/retrieval/hybrid.py +271 -0
- velune/retrieval/keyword.py +131 -0
- velune/retrieval/module.py +26 -0
- velune/retrieval/pipeline.py +303 -0
- velune/retrieval/reranker.py +102 -0
- velune/retrieval/schemas.py +59 -0
- velune/retrieval/slow_path.py +364 -0
- velune/retrieval/vector.py +203 -0
- velune/telemetry/__init__.py +59 -0
- velune/telemetry/cognition.py +267 -0
- velune/telemetry/cost_estimator.py +92 -0
- velune/telemetry/debug.py +304 -0
- velune/telemetry/doctor.py +244 -0
- velune/telemetry/logging.py +286 -0
- velune/telemetry/spans.py +277 -0
- velune/telemetry/token_tracker.py +140 -0
- velune/telemetry/usage_tracker.py +340 -0
- velune/tools/__init__.py +41 -0
- velune/tools/base/registry.py +87 -0
- velune/tools/base/tool.py +63 -0
- velune/tools/code/navigate.py +116 -0
- velune/tools/code/search.py +123 -0
- velune/tools/filesystem/read.py +75 -0
- velune/tools/filesystem/search.py +136 -0
- velune/tools/filesystem/write.py +163 -0
- velune/tools/git/history.py +177 -0
- velune/tools/git/operations.py +122 -0
- velune/tools/git/state.py +121 -0
- velune/tools/module.py +81 -0
- velune/tools/terminal/execute.py +72 -0
- velune/tools/terminal/history.py +47 -0
- velune/tools/web/fetch.py +55 -0
- velune/tools/web/validator.py +122 -0
- velune_cli-0.9.0.dist-info/METADATA +518 -0
- velune_cli-0.9.0.dist-info/RECORD +279 -0
- velune_cli-0.9.0.dist-info/WHEEL +4 -0
- velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
- 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
|