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,300 @@
1
+ """Project type detector — classifies workspaces and builds context profiles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from pathlib import Path
8
+
9
+
10
+ class ProjectType(Enum):
11
+ PYTHON_FASTAPI = "python-fastapi"
12
+ PYTHON_DJANGO = "python-django"
13
+ PYTHON_FLASK = "python-flask"
14
+ PYTHON_CLI = "python-cli"
15
+ PYTHON_GENERIC = "python"
16
+ NODE_REACT = "node-react"
17
+ NODE_NEXTJS = "node-nextjs"
18
+ NODE_EXPRESS = "node-express"
19
+ NODE_GENERIC = "node"
20
+ RUST = "rust"
21
+ GO = "go"
22
+ JAVA_SPRING = "java-spring"
23
+ JAVA_GENERIC = "java"
24
+ DOTNET = "dotnet"
25
+ FLUTTER = "flutter"
26
+ UNKNOWN = "unknown"
27
+
28
+
29
+ @dataclass
30
+ class ProjectProfile:
31
+ project_type: ProjectType
32
+ display_name: str
33
+ primary_language: str
34
+ detected_frameworks: list[str]
35
+ entry_points: list[str]
36
+ test_directories: list[str]
37
+ config_files: list[str]
38
+ suggested_model_skill: str # "coding" | "reasoning" | "balanced"
39
+ context_hints: list[str]
40
+ system_prompt_addon: str
41
+
42
+
43
+ PROJECT_SYSTEM_PROMPTS: dict[ProjectType, str] = {
44
+ ProjectType.PYTHON_FASTAPI: (
45
+ "This is a Python FastAPI project. When suggesting code changes, "
46
+ "prefer async/await patterns, Pydantic models for validation, "
47
+ "and dependency injection. Routes are in routers/. Models in models/."
48
+ ),
49
+ ProjectType.PYTHON_DJANGO: (
50
+ "This is a Django project. Follow Django conventions: fat models, "
51
+ "thin views, use Django ORM patterns, signals, and class-based views "
52
+ "where appropriate. Settings are in settings.py or settings/."
53
+ ),
54
+ ProjectType.PYTHON_FLASK: (
55
+ "This is a Flask project. Follow Flask patterns: blueprints for "
56
+ "routing, application factory pattern, SQLAlchemy for ORM if present."
57
+ ),
58
+ ProjectType.NODE_REACT: (
59
+ "This is a React frontend project. Prefer functional components "
60
+ "with hooks, TypeScript if tsconfig.json exists, and modern React "
61
+ "patterns. Components in src/components/."
62
+ ),
63
+ ProjectType.NODE_NEXTJS: (
64
+ "This is a Next.js project. Use App Router patterns if app/ exists, "
65
+ "Pages Router if pages/ exists. Server components by default, "
66
+ "client components only when needed."
67
+ ),
68
+ ProjectType.RUST: (
69
+ "This is a Rust project. Follow Rust idioms: ownership semantics, "
70
+ "Result/Option for error handling, no unwrap() in library code. "
71
+ "Check Cargo.toml for edition and features."
72
+ ),
73
+ ProjectType.GO: (
74
+ "This is a Go project. Follow Go conventions: simple interfaces, "
75
+ "error returns, goroutines and channels for concurrency. "
76
+ "Entry point is main.go or cmd/."
77
+ ),
78
+ }
79
+
80
+ _DISPLAY_NAMES: dict[ProjectType, str] = {
81
+ ProjectType.PYTHON_FASTAPI: "Python / FastAPI",
82
+ ProjectType.PYTHON_DJANGO: "Python / Django",
83
+ ProjectType.PYTHON_FLASK: "Python / Flask",
84
+ ProjectType.PYTHON_CLI: "Python / CLI",
85
+ ProjectType.PYTHON_GENERIC: "Python",
86
+ ProjectType.NODE_REACT: "Node.js / React",
87
+ ProjectType.NODE_NEXTJS: "Node.js / Next.js",
88
+ ProjectType.NODE_EXPRESS: "Node.js / Express",
89
+ ProjectType.NODE_GENERIC: "Node.js",
90
+ ProjectType.RUST: "Rust",
91
+ ProjectType.GO: "Go",
92
+ ProjectType.JAVA_SPRING: "Java / Spring",
93
+ ProjectType.JAVA_GENERIC: "Java",
94
+ ProjectType.DOTNET: ".NET",
95
+ ProjectType.FLUTTER: "Flutter / Dart",
96
+ ProjectType.UNKNOWN: "Unknown",
97
+ }
98
+
99
+ _LANGUAGE_MAP: dict[ProjectType, str] = {
100
+ ProjectType.PYTHON_FASTAPI: "python",
101
+ ProjectType.PYTHON_DJANGO: "python",
102
+ ProjectType.PYTHON_FLASK: "python",
103
+ ProjectType.PYTHON_CLI: "python",
104
+ ProjectType.PYTHON_GENERIC: "python",
105
+ ProjectType.NODE_REACT: "typescript",
106
+ ProjectType.NODE_NEXTJS: "typescript",
107
+ ProjectType.NODE_EXPRESS: "javascript",
108
+ ProjectType.NODE_GENERIC: "javascript",
109
+ ProjectType.RUST: "rust",
110
+ ProjectType.GO: "go",
111
+ ProjectType.JAVA_SPRING: "java",
112
+ ProjectType.JAVA_GENERIC: "java",
113
+ ProjectType.DOTNET: "csharp",
114
+ ProjectType.FLUTTER: "dart",
115
+ ProjectType.UNKNOWN: "unknown",
116
+ }
117
+
118
+
119
+ class ProjectTypeDetector:
120
+ """Detects the project type of a workspace by inspecting root-level files."""
121
+
122
+ def detect(self, workspace: Path) -> ProjectProfile:
123
+ files = self._list_root_files(workspace)
124
+ project_type, frameworks = self._classify(workspace, files)
125
+ entry_points = self._find_entry_points(workspace, project_type)
126
+ test_dirs = self._find_test_dirs(workspace)
127
+ config_files = self._find_config_files(workspace, files)
128
+ context_hints = self._build_context_hints(project_type)
129
+ system_prompt = PROJECT_SYSTEM_PROMPTS.get(project_type, "")
130
+
131
+ return ProjectProfile(
132
+ project_type=project_type,
133
+ display_name=_DISPLAY_NAMES.get(project_type, "Unknown"),
134
+ primary_language=_LANGUAGE_MAP.get(project_type, "unknown"),
135
+ detected_frameworks=frameworks,
136
+ entry_points=entry_points,
137
+ test_directories=test_dirs,
138
+ config_files=config_files,
139
+ suggested_model_skill="coding",
140
+ context_hints=context_hints,
141
+ system_prompt_addon=system_prompt,
142
+ )
143
+
144
+ def _list_root_files(self, workspace: Path) -> set[str]:
145
+ try:
146
+ return {f.name for f in workspace.iterdir() if not f.name.startswith(".")}
147
+ except Exception:
148
+ return set()
149
+
150
+ def _classify(self, workspace: Path, files: set[str]) -> tuple[ProjectType, list[str]]:
151
+ # ── Rust ──────────────────────────────────────────────────────
152
+ if "Cargo.toml" in files:
153
+ return ProjectType.RUST, ["cargo"]
154
+
155
+ # ── Go ────────────────────────────────────────────────────────
156
+ if "go.mod" in files:
157
+ return ProjectType.GO, ["go modules"]
158
+
159
+ # ── Flutter / Dart ────────────────────────────────────────────
160
+ if "pubspec.yaml" in files:
161
+ return ProjectType.FLUTTER, ["flutter", "dart"]
162
+
163
+ # ── .NET ──────────────────────────────────────────────────────
164
+ if any(f.endswith(".csproj") or f.endswith(".sln") for f in files):
165
+ return ProjectType.DOTNET, ["dotnet"]
166
+
167
+ # ── Node / JavaScript / TypeScript ────────────────────────────
168
+ if "package.json" in files:
169
+ try:
170
+ import json
171
+
172
+ pkg = json.loads((workspace / "package.json").read_text())
173
+ deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
174
+ if "next" in deps:
175
+ return ProjectType.NODE_NEXTJS, ["nextjs", "react"]
176
+ if "react" in deps or "react-dom" in deps:
177
+ frameworks = ["react"]
178
+ if "typescript" in deps or (workspace / "tsconfig.json").exists():
179
+ frameworks.append("typescript")
180
+ return ProjectType.NODE_REACT, frameworks
181
+ if "express" in deps:
182
+ return ProjectType.NODE_EXPRESS, ["express"]
183
+ except Exception:
184
+ pass
185
+ return ProjectType.NODE_GENERIC, []
186
+
187
+ # ── Python ────────────────────────────────────────────────────
188
+ has_python = (
189
+ "pyproject.toml" in files
190
+ or "setup.py" in files
191
+ or "requirements.txt" in files
192
+ or any(f.endswith(".py") for f in files)
193
+ )
194
+ if has_python:
195
+ combined = self._read_requirement_sources(workspace)
196
+ if "fastapi" in combined:
197
+ frameworks = ["fastapi", "uvicorn"]
198
+ if "sqlalchemy" in combined:
199
+ frameworks.append("sqlalchemy")
200
+ if "pydantic" in combined:
201
+ frameworks.append("pydantic")
202
+ return ProjectType.PYTHON_FASTAPI, frameworks
203
+ if "django" in combined:
204
+ frameworks: list[str] = ["django"]
205
+ if "drf" in combined or "rest_framework" in combined:
206
+ frameworks.append("drf")
207
+ return ProjectType.PYTHON_DJANGO, frameworks
208
+ if "flask" in combined:
209
+ return ProjectType.PYTHON_FLASK, ["flask"]
210
+ if "typer" in combined or "click" in combined or "argparse" in combined:
211
+ return ProjectType.PYTHON_CLI, ["cli"]
212
+ return ProjectType.PYTHON_GENERIC, []
213
+
214
+ # ── Java ──────────────────────────────────────────────────────
215
+ if "pom.xml" in files or "build.gradle" in files:
216
+ combined = ""
217
+ for fname in ("pom.xml", "build.gradle"):
218
+ fp = workspace / fname
219
+ if fp.exists():
220
+ try:
221
+ combined += fp.read_text().lower()
222
+ except Exception:
223
+ pass
224
+ if "spring" in combined:
225
+ return ProjectType.JAVA_SPRING, ["spring"]
226
+ return ProjectType.JAVA_GENERIC, []
227
+
228
+ return ProjectType.UNKNOWN, []
229
+
230
+ def _read_requirement_sources(self, workspace: Path) -> str:
231
+ parts: list[str] = []
232
+ for fname in ("requirements.txt", "requirements.in", "pyproject.toml", "setup.py"):
233
+ fp = workspace / fname
234
+ if fp.exists():
235
+ try:
236
+ parts.append(fp.read_text().lower())
237
+ except Exception:
238
+ pass
239
+ return " ".join(parts)
240
+
241
+ def _find_entry_points(self, workspace: Path, pt: ProjectType) -> list[str]:
242
+ candidates: dict[ProjectType, list[str]] = {
243
+ ProjectType.PYTHON_FASTAPI: ["main.py", "app/main.py", "src/main.py"],
244
+ ProjectType.PYTHON_DJANGO: ["manage.py"],
245
+ ProjectType.PYTHON_FLASK: ["app.py", "run.py", "main.py"],
246
+ ProjectType.PYTHON_CLI: ["main.py", "cli.py", "__main__.py"],
247
+ ProjectType.PYTHON_GENERIC: ["main.py", "__main__.py"],
248
+ ProjectType.NODE_REACT: ["src/main.tsx", "src/App.tsx", "src/index.tsx"],
249
+ ProjectType.NODE_NEXTJS: ["app/page.tsx", "pages/index.tsx"],
250
+ ProjectType.NODE_EXPRESS: ["index.js", "server.js", "app.js"],
251
+ ProjectType.RUST: ["src/main.rs", "src/lib.rs"],
252
+ ProjectType.GO: ["main.go", "cmd/main.go"],
253
+ }
254
+ found = []
255
+ for rel in candidates.get(pt, []):
256
+ if (workspace / rel).exists():
257
+ found.append(rel)
258
+ return found[:3]
259
+
260
+ def _find_test_dirs(self, workspace: Path) -> list[str]:
261
+ names = ["tests", "test", "__tests__", "spec", "specs", "e2e"]
262
+ return [d for d in names if (workspace / d).is_dir()]
263
+
264
+ def _find_config_files(self, workspace: Path, files: set[str]) -> list[str]:
265
+ candidates = [
266
+ "pyproject.toml",
267
+ "package.json",
268
+ "Cargo.toml",
269
+ "go.mod",
270
+ "tsconfig.json",
271
+ ".eslintrc.json",
272
+ "webpack.config.js",
273
+ "vite.config.ts",
274
+ "next.config.js",
275
+ "docker-compose.yml",
276
+ "Dockerfile",
277
+ ".env.example",
278
+ ]
279
+ return [f for f in candidates if f in files]
280
+
281
+ def _build_context_hints(self, pt: ProjectType) -> list[str]:
282
+ hints: list[str] = []
283
+ if pt in (
284
+ ProjectType.PYTHON_FASTAPI,
285
+ ProjectType.PYTHON_DJANGO,
286
+ ProjectType.PYTHON_FLASK,
287
+ ProjectType.PYTHON_GENERIC,
288
+ ):
289
+ hints.append("Always check imports and type hints in Python files")
290
+ hints.append("Prefer pathlib over os.path for file operations")
291
+ if pt == ProjectType.NODE_REACT:
292
+ hints.append("Check for TypeScript types in *.d.ts files")
293
+ hints.append("Components use named exports, not default where possible")
294
+ if pt == ProjectType.RUST:
295
+ hints.append("Check Cargo.toml features before suggesting dependencies")
296
+ hints.append("Use ? operator for error propagation")
297
+ if pt == ProjectType.GO:
298
+ hints.append("Follow Go error handling: always check returned errors")
299
+ hints.append("Check go.sum when suggesting new imports")
300
+ return hints
@@ -0,0 +1,287 @@
1
+ """Journal tracking symbol renames across the repository."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ import aiosqlite
11
+
12
+ logger = logging.getLogger("velune.repository.rename_journal")
13
+
14
+
15
+ @dataclass
16
+ class RenameRecord:
17
+ """Record of a symbol rename."""
18
+
19
+ symbol_id: str
20
+ old_name: str
21
+ new_name: str
22
+ file_path: str
23
+ old_line: int
24
+ new_line: int
25
+ timestamp: datetime
26
+
27
+
28
+ class RenameJournal:
29
+ """Persistent journal of symbol renames."""
30
+
31
+ def __init__(self, db_path: Path) -> None:
32
+ """Initialize rename journal.
33
+
34
+ Args:
35
+ db_path: Path to SQLite database file
36
+ """
37
+ self.db_path = db_path
38
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
39
+
40
+ async def initialize(self) -> None:
41
+ """Initialize database schema."""
42
+ async with aiosqlite.connect(str(self.db_path)) as db:
43
+ await db.executescript("""
44
+ CREATE TABLE IF NOT EXISTS renames (
45
+ symbol_id TEXT NOT NULL,
46
+ old_name TEXT NOT NULL,
47
+ new_name TEXT NOT NULL,
48
+ file_path TEXT NOT NULL,
49
+ old_line INTEGER NOT NULL,
50
+ new_line INTEGER NOT NULL,
51
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
52
+ PRIMARY KEY (symbol_id, timestamp)
53
+ );
54
+
55
+ CREATE INDEX IF NOT EXISTS idx_renames_symbol ON renames(symbol_id);
56
+ CREATE INDEX IF NOT EXISTS idx_renames_file ON renames(file_path);
57
+ CREATE INDEX IF NOT EXISTS idx_renames_old_name ON renames(old_name);
58
+ CREATE INDEX IF NOT EXISTS idx_renames_timestamp ON renames(timestamp);
59
+ """)
60
+ await db.commit()
61
+
62
+ async def record_rename(
63
+ self,
64
+ symbol_id: str,
65
+ old_name: str,
66
+ new_name: str,
67
+ file_path: str,
68
+ old_line: int,
69
+ new_line: int,
70
+ ) -> None:
71
+ """Record a symbol rename.
72
+
73
+ Args:
74
+ symbol_id: Stable symbol ID
75
+ old_name: Previous symbol name
76
+ new_name: New symbol name
77
+ file_path: File path where rename occurred
78
+ old_line: Previous line number
79
+ new_line: New line number
80
+ """
81
+ async with aiosqlite.connect(str(self.db_path)) as db:
82
+ await db.execute(
83
+ """
84
+ INSERT INTO renames (symbol_id, old_name, new_name, file_path, old_line, new_line)
85
+ VALUES (?, ?, ?, ?, ?, ?)
86
+ """,
87
+ (symbol_id, old_name, new_name, file_path, old_line, new_line),
88
+ )
89
+ await db.commit()
90
+
91
+ logger.debug(f"Recorded rename: {old_name} → {new_name} (id={symbol_id})")
92
+
93
+ async def get_renames_for_symbol(self, symbol_id: str) -> list[RenameRecord]:
94
+ """Get all rename records for a symbol.
95
+
96
+ Args:
97
+ symbol_id: Symbol to get renames for
98
+
99
+ Returns:
100
+ List of rename records in chronological order
101
+ """
102
+ records: list[RenameRecord] = []
103
+ async with aiosqlite.connect(str(self.db_path)) as db:
104
+ async with db.execute(
105
+ """
106
+ SELECT symbol_id, old_name, new_name, file_path, old_line, new_line, timestamp
107
+ FROM renames WHERE symbol_id = ? ORDER BY timestamp ASC
108
+ """,
109
+ (symbol_id,),
110
+ ) as cursor:
111
+ async for row in cursor:
112
+ record = RenameRecord(
113
+ symbol_id=row[0],
114
+ old_name=row[1],
115
+ new_name=row[2],
116
+ file_path=row[3],
117
+ old_line=row[4],
118
+ new_line=row[5],
119
+ timestamp=datetime.fromisoformat(row[6]),
120
+ )
121
+ records.append(record)
122
+
123
+ return records
124
+
125
+ async def resolve_name(self, old_name: str, file_path: str) -> str | None:
126
+ """Resolve an old symbol name to its current name.
127
+
128
+ Searches the most recent rename record for the given name and file.
129
+
130
+ Args:
131
+ old_name: Historical symbol name
132
+ file_path: File path
133
+
134
+ Returns:
135
+ Current name if rename exists, None otherwise
136
+ """
137
+ async with aiosqlite.connect(str(self.db_path)) as db:
138
+ async with db.execute(
139
+ """
140
+ SELECT new_name FROM renames
141
+ WHERE old_name = ? AND file_path = ?
142
+ ORDER BY timestamp DESC LIMIT 1
143
+ """,
144
+ (old_name, file_path),
145
+ ) as cursor:
146
+ row = await cursor.fetchone()
147
+
148
+ if row:
149
+ return row[0]
150
+
151
+ # Check if there's a chain of renames
152
+ # (i.e., A→B→C, resolve A to C)
153
+ current_name = old_name
154
+ depth = 0
155
+ max_depth = 10
156
+
157
+ while depth < max_depth:
158
+ next_name = await self.resolve_name(current_name, file_path)
159
+ if not next_name or next_name == current_name:
160
+ break
161
+ current_name = next_name
162
+ depth += 1
163
+
164
+ return current_name if current_name != old_name else None
165
+
166
+ async def get_renames_in_file(self, file_path: str) -> list[RenameRecord]:
167
+ """Get all renames in a specific file.
168
+
169
+ Args:
170
+ file_path: File path to get renames for
171
+
172
+ Returns:
173
+ List of rename records
174
+ """
175
+ records: list[RenameRecord] = []
176
+ async with aiosqlite.connect(str(self.db_path)) as db:
177
+ async with db.execute(
178
+ """
179
+ SELECT symbol_id, old_name, new_name, file_path, old_line, new_line, timestamp
180
+ FROM renames WHERE file_path = ? ORDER BY timestamp ASC
181
+ """,
182
+ (file_path,),
183
+ ) as cursor:
184
+ async for row in cursor:
185
+ record = RenameRecord(
186
+ symbol_id=row[0],
187
+ old_name=row[1],
188
+ new_name=row[2],
189
+ file_path=row[3],
190
+ old_line=row[4],
191
+ new_line=row[5],
192
+ timestamp=datetime.fromisoformat(row[6]),
193
+ )
194
+ records.append(record)
195
+
196
+ return records
197
+
198
+ async def detect_rename(
199
+ self,
200
+ old_symbols: dict[str, tuple[str, int]], # name -> (id, line)
201
+ new_symbols: dict[str, tuple[str, int]], # name -> (id, line)
202
+ file_path: str,
203
+ ) -> list[RenameRecord]:
204
+ """Detect renames by comparing old and new symbol lists.
205
+
206
+ Heuristic: symbols at approximately the same line in the same file
207
+ with the same ID are considered the same symbol, even if the name changed.
208
+
209
+ Args:
210
+ old_symbols: Map of name -> (symbol_id, line) from previous parse
211
+ new_symbols: Map of name -> (symbol_id, line) from current parse
212
+ file_path: File path being analyzed
213
+
214
+ Returns:
215
+ List of detected renames
216
+ """
217
+ detected_renames: list[RenameRecord] = []
218
+
219
+ # Build reverse map: id -> (name, line)
220
+ old_by_id = {sym_id: (name, line) for name, (sym_id, line) in old_symbols.items()}
221
+ new_by_id = {sym_id: (name, line) for name, (sym_id, line) in new_symbols.items()}
222
+
223
+ # Find renamed symbols: same ID, different name
224
+ for symbol_id, (new_name, new_line) in new_by_id.items():
225
+ if symbol_id in old_by_id:
226
+ old_name, old_line = old_by_id[symbol_id]
227
+ if old_name != new_name:
228
+ # Rename detected
229
+ await self.record_rename(
230
+ symbol_id=symbol_id,
231
+ old_name=old_name,
232
+ new_name=new_name,
233
+ file_path=file_path,
234
+ old_line=old_line,
235
+ new_line=new_line,
236
+ )
237
+
238
+ record = RenameRecord(
239
+ symbol_id=symbol_id,
240
+ old_name=old_name,
241
+ new_name=new_name,
242
+ file_path=file_path,
243
+ old_line=old_line,
244
+ new_line=new_line,
245
+ timestamp=datetime.now(),
246
+ )
247
+ detected_renames.append(record)
248
+
249
+ return detected_renames
250
+
251
+ async def get_all_renames(self) -> list[RenameRecord]:
252
+ """Get all rename records in the journal.
253
+
254
+ Returns:
255
+ List of all rename records
256
+ """
257
+ records: list[RenameRecord] = []
258
+ async with aiosqlite.connect(str(self.db_path)) as db:
259
+ async with db.execute(
260
+ """
261
+ SELECT symbol_id, old_name, new_name, file_path, old_line, new_line, timestamp
262
+ FROM renames ORDER BY timestamp ASC
263
+ """
264
+ ) as cursor:
265
+ async for row in cursor:
266
+ record = RenameRecord(
267
+ symbol_id=row[0],
268
+ old_name=row[1],
269
+ new_name=row[2],
270
+ file_path=row[3],
271
+ old_line=row[4],
272
+ new_line=row[5],
273
+ timestamp=datetime.fromisoformat(row[6]),
274
+ )
275
+ records.append(record)
276
+
277
+ return records
278
+
279
+ async def clear_renames_for_file(self, file_path: str) -> None:
280
+ """Clear all rename records for a file.
281
+
282
+ Args:
283
+ file_path: File path to clear renames for
284
+ """
285
+ async with aiosqlite.connect(str(self.db_path)) as db:
286
+ await db.execute("DELETE FROM renames WHERE file_path = ?", (file_path,))
287
+ await db.commit()