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,316 @@
|
|
|
1
|
+
"""Repository cognition pipeline merging AST indices, Git history, and dependency graphs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from velune.repository.analyzer import CodebaseAnalyzer
|
|
10
|
+
from velune.repository.grapher import RepositoryGrapher
|
|
11
|
+
from velune.repository.indexer import RepositoryIndexer
|
|
12
|
+
from velune.repository.schemas import (
|
|
13
|
+
RepositoryEdge,
|
|
14
|
+
RepositorySnapshot,
|
|
15
|
+
)
|
|
16
|
+
from velune.repository.tracker import GitTracker
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("velune.repository.cognition")
|
|
19
|
+
|
|
20
|
+
_STATE_FILENAME = "index_state.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RepositoryCognitionService:
|
|
24
|
+
"""The unified cognitive entrypoint mapping a workspace's AST structure, Git details, and dependencies."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, root_path: Path) -> None:
|
|
27
|
+
self.root_path = root_path.resolve()
|
|
28
|
+
self.indexer = RepositoryIndexer(self.root_path)
|
|
29
|
+
self.grapher = RepositoryGrapher(self.root_path)
|
|
30
|
+
self.tracker = GitTracker(self.root_path)
|
|
31
|
+
self.analyzer = CodebaseAnalyzer(self.root_path)
|
|
32
|
+
self._state_path = self.root_path / ".velune" / _STATE_FILENAME
|
|
33
|
+
self._bg_index_task: asyncio.Task | None = None
|
|
34
|
+
|
|
35
|
+
# ------------------------------------------------------------------
|
|
36
|
+
# Lifecycle protocol (called by LifecycleCoordinator)
|
|
37
|
+
# ------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
async def initialize(self) -> None:
|
|
40
|
+
"""Start background incremental indexing — non-blocking for the REPL."""
|
|
41
|
+
from velune.repository.incremental_indexer import IncrementalIndexer
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
from rich.console import Console
|
|
45
|
+
|
|
46
|
+
console = Console(stderr=True)
|
|
47
|
+
except Exception:
|
|
48
|
+
console = None # type: ignore[assignment]
|
|
49
|
+
|
|
50
|
+
def _print(msg: str) -> None:
|
|
51
|
+
if console:
|
|
52
|
+
console.print(msg)
|
|
53
|
+
|
|
54
|
+
_print("[dim]Checking repository...[/dim]")
|
|
55
|
+
|
|
56
|
+
inc = IncrementalIndexer(self.workspace_root, self._state_path)
|
|
57
|
+
try:
|
|
58
|
+
delta = await inc.compute_delta()
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
logger.warning("Incremental delta check failed: %s", exc)
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
if delta.is_empty:
|
|
64
|
+
_print("[dim green]Repository index is up to date[/dim green]")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
n = delta.total
|
|
68
|
+
_print(f"[dim]Indexing {n} changed file(s)...[/dim]")
|
|
69
|
+
self._bg_index_task = asyncio.create_task(self._background_apply(inc, delta, n))
|
|
70
|
+
|
|
71
|
+
async def shutdown(self) -> None:
|
|
72
|
+
"""Cancel any pending background indexing task."""
|
|
73
|
+
if self._bg_index_task and not self._bg_index_task.done():
|
|
74
|
+
self._bg_index_task.cancel()
|
|
75
|
+
try:
|
|
76
|
+
await self._bg_index_task
|
|
77
|
+
except (asyncio.CancelledError, Exception):
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
# ------------------------------------------------------------------
|
|
81
|
+
# Primary indexing API
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def workspace_root(self) -> Path:
|
|
86
|
+
return self.root_path
|
|
87
|
+
|
|
88
|
+
def index(self, force: bool = False) -> RepositorySnapshot:
|
|
89
|
+
"""Index the repository, using a git-SHA fast path to skip unchanged repos.
|
|
90
|
+
|
|
91
|
+
On a cache hit (git HEAD SHA matches stored SHA AND working tree is clean),
|
|
92
|
+
the existing symbol + file cache is loaded and the full pipeline (grapher,
|
|
93
|
+
git metrics, architecture analysis) is run on top of it — skipping all file
|
|
94
|
+
I/O and SHA computation.
|
|
95
|
+
|
|
96
|
+
On a cache miss, the full ``RepositoryIndexer`` pipeline runs (incremental
|
|
97
|
+
at the file level via SHA256 comparison) and the ``IndexState`` is updated.
|
|
98
|
+
"""
|
|
99
|
+
from velune.repository.incremental_indexer import IncrementalIndexer
|
|
100
|
+
|
|
101
|
+
inc = IncrementalIndexer(self.root_path, self._state_path)
|
|
102
|
+
|
|
103
|
+
if not force and inc._get_git_sha() == self._stored_commit_sha():
|
|
104
|
+
clean = inc._working_tree_is_clean()
|
|
105
|
+
if clean:
|
|
106
|
+
cached = self.get_snapshot()
|
|
107
|
+
if cached:
|
|
108
|
+
logger.debug("Fast path: reusing cached snapshot (git SHA matches).")
|
|
109
|
+
return self._run_pipeline(cached, update_state=False)
|
|
110
|
+
|
|
111
|
+
# Slow path: file-level incremental index
|
|
112
|
+
snapshot = self.indexer.index(force=force)
|
|
113
|
+
|
|
114
|
+
# Persist updated IndexState so the next session benefits from the fast path
|
|
115
|
+
self._persist_index_state(inc, snapshot)
|
|
116
|
+
|
|
117
|
+
return self._run_pipeline(snapshot, update_state=False)
|
|
118
|
+
|
|
119
|
+
# ------------------------------------------------------------------
|
|
120
|
+
# Read-only snapshot accessor (no indexing)
|
|
121
|
+
# ------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def get_snapshot(self) -> RepositorySnapshot | None:
|
|
124
|
+
"""Return the last-computed snapshot from the on-disk cache, or None."""
|
|
125
|
+
import json
|
|
126
|
+
|
|
127
|
+
cache_path = self.indexer.cache_path
|
|
128
|
+
if not cache_path.exists():
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
with open(cache_path, encoding="utf-8") as f:
|
|
133
|
+
cache: dict = json.load(f)
|
|
134
|
+
except Exception:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
from velune.repository.schemas import (
|
|
138
|
+
RepositoryFile,
|
|
139
|
+
RepositoryLanguage,
|
|
140
|
+
RepositorySymbol,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
files: list[RepositoryFile] = []
|
|
144
|
+
all_symbols: list[RepositorySymbol] = []
|
|
145
|
+
|
|
146
|
+
for rel_path, entry in cache.items():
|
|
147
|
+
try:
|
|
148
|
+
language = RepositoryLanguage(entry.get("language", "unknown"))
|
|
149
|
+
symbols = [RepositorySymbol(**s) for s in entry.get("symbols", [])]
|
|
150
|
+
file_rec = RepositoryFile(
|
|
151
|
+
path=rel_path,
|
|
152
|
+
language=language,
|
|
153
|
+
size_bytes=entry.get("size_bytes", 0),
|
|
154
|
+
sha256=entry.get("sha256", ""),
|
|
155
|
+
symbols=symbols,
|
|
156
|
+
metadata=entry.get("metadata", {}),
|
|
157
|
+
)
|
|
158
|
+
files.append(file_rec)
|
|
159
|
+
all_symbols.extend(symbols)
|
|
160
|
+
except Exception:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
return RepositorySnapshot(
|
|
164
|
+
root_path=str(self.root_path),
|
|
165
|
+
files=files,
|
|
166
|
+
symbols=all_symbols,
|
|
167
|
+
edges=[],
|
|
168
|
+
summary={
|
|
169
|
+
"total_files": len(files),
|
|
170
|
+
"total_symbols": len(all_symbols),
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def traverse(self, node_id: str, depth: int = 2) -> list[str]:
|
|
175
|
+
"""BFS traversal from *node_id* through the dependency graph."""
|
|
176
|
+
return self.grapher.traverse(node_id, depth)
|
|
177
|
+
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
# Internal helpers
|
|
180
|
+
# ------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
def _run_pipeline(
|
|
183
|
+
self, snapshot: RepositorySnapshot, *, update_state: bool = False
|
|
184
|
+
) -> RepositorySnapshot:
|
|
185
|
+
"""Run grapher, git metrics, and architecture analysis on *snapshot*."""
|
|
186
|
+
# Reset grapher for this run (it is stateful and must be rebuilt each call)
|
|
187
|
+
self.grapher = RepositoryGrapher(self.root_path)
|
|
188
|
+
|
|
189
|
+
file_paths = [f.path for f in snapshot.files]
|
|
190
|
+
for f in snapshot.files:
|
|
191
|
+
self.grapher.add_file(f.path, f.language.value, f.size_bytes)
|
|
192
|
+
for sym in f.symbols:
|
|
193
|
+
self.grapher.add_symbol(sym)
|
|
194
|
+
|
|
195
|
+
self.grapher.resolve_import_dependencies(file_paths, snapshot.symbols)
|
|
196
|
+
|
|
197
|
+
edges: list[RepositoryEdge] = []
|
|
198
|
+
for src, tgt, _key, data in self.grapher.graph.edges(keys=True, data=True):
|
|
199
|
+
edges.append(
|
|
200
|
+
RepositoryEdge(
|
|
201
|
+
source=src,
|
|
202
|
+
target=tgt,
|
|
203
|
+
edge_type=data.get("edge_type", "depends"),
|
|
204
|
+
weight=data.get("weight", 1.0),
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
snapshot.edges = edges
|
|
208
|
+
|
|
209
|
+
# Git metrics (batched subprocesses)
|
|
210
|
+
branch = self.tracker.get_active_branch()
|
|
211
|
+
changes = self.tracker.get_uncommitted_changes()
|
|
212
|
+
recent_commits = self.tracker.get_recent_commits(limit=5)
|
|
213
|
+
all_volatility = self.tracker.get_all_file_volatility(days=90)
|
|
214
|
+
file_volatility: dict[str, int] = {}
|
|
215
|
+
for f in snapshot.files:
|
|
216
|
+
file_volatility[f.path] = all_volatility.get(f.path, 0) or all_volatility.get(
|
|
217
|
+
f.path.replace("/", "\\"), 0
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Architecture analysis (pure Python)
|
|
221
|
+
layers = self.analyzer.classify_architecture_layers(file_paths)
|
|
222
|
+
analyzer_edges = [(e.source, e.target) for e in edges]
|
|
223
|
+
violations = self.analyzer.detect_dependency_violations(layers, analyzer_edges)
|
|
224
|
+
|
|
225
|
+
code_files: dict[str, str] = {}
|
|
226
|
+
for f in snapshot.files:
|
|
227
|
+
if f.size_bytes < 100_000:
|
|
228
|
+
try:
|
|
229
|
+
code_files[f.path] = (self.root_path / f.path).read_text(
|
|
230
|
+
encoding="utf-8", errors="ignore"
|
|
231
|
+
)
|
|
232
|
+
except Exception:
|
|
233
|
+
pass
|
|
234
|
+
frameworks = self.analyzer.detect_framework_footprint(code_files)
|
|
235
|
+
|
|
236
|
+
snapshot.summary.update(
|
|
237
|
+
{
|
|
238
|
+
"git": {
|
|
239
|
+
"active_branch": branch,
|
|
240
|
+
"uncommitted_changes_count": len(changes),
|
|
241
|
+
"uncommitted_changes": changes[:10],
|
|
242
|
+
"recent_commits": recent_commits,
|
|
243
|
+
},
|
|
244
|
+
"architecture": {
|
|
245
|
+
"layers": {k: len(v) for k, v in layers.items()},
|
|
246
|
+
"violations_count": len(violations),
|
|
247
|
+
"violations": violations[:5],
|
|
248
|
+
"frameworks_detected": frameworks,
|
|
249
|
+
},
|
|
250
|
+
"metrics": {
|
|
251
|
+
"high_volatility_files": sorted(
|
|
252
|
+
file_volatility.items(), key=lambda x: x[1], reverse=True
|
|
253
|
+
)[:5],
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return snapshot
|
|
259
|
+
|
|
260
|
+
def _stored_commit_sha(self) -> str | None:
|
|
261
|
+
"""Return the git SHA stored in the last saved IndexState."""
|
|
262
|
+
from velune.repository.index_state import IndexState
|
|
263
|
+
|
|
264
|
+
state = IndexState.load(self._state_path)
|
|
265
|
+
return state.last_commit_sha if state else None
|
|
266
|
+
|
|
267
|
+
def _persist_index_state(self, inc: object, snapshot: RepositorySnapshot) -> None:
|
|
268
|
+
"""Update IndexState on disk after a full index run."""
|
|
269
|
+
import time
|
|
270
|
+
|
|
271
|
+
from velune.repository.index_state import IndexedFile, IndexState
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
git_sha = inc._get_git_sha() # type: ignore[attr-defined]
|
|
275
|
+
state = IndexState.load(self._state_path) or IndexState.empty(str(self.root_path))
|
|
276
|
+
now = time.time()
|
|
277
|
+
state.file_index = {
|
|
278
|
+
f.path: IndexedFile(
|
|
279
|
+
path=f.path,
|
|
280
|
+
content_hash=f.sha256,
|
|
281
|
+
language=f.language.value,
|
|
282
|
+
symbol_count=len(f.symbols),
|
|
283
|
+
indexed_at=now,
|
|
284
|
+
)
|
|
285
|
+
for f in snapshot.files
|
|
286
|
+
}
|
|
287
|
+
state.touch(git_sha)
|
|
288
|
+
state.workspace_root = str(self.root_path)
|
|
289
|
+
state.save(self._state_path)
|
|
290
|
+
except Exception as exc:
|
|
291
|
+
logger.debug("Could not persist IndexState: %s", exc)
|
|
292
|
+
|
|
293
|
+
async def _background_apply(
|
|
294
|
+
self,
|
|
295
|
+
inc: object,
|
|
296
|
+
delta: object,
|
|
297
|
+
n: int,
|
|
298
|
+
) -> None:
|
|
299
|
+
"""Apply a delta in the background and announce completion."""
|
|
300
|
+
try:
|
|
301
|
+
from rich.console import Console
|
|
302
|
+
|
|
303
|
+
console = Console(stderr=True)
|
|
304
|
+
except Exception:
|
|
305
|
+
console = None # type: ignore[assignment]
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
await inc.apply_delta(delta) # type: ignore[attr-defined]
|
|
309
|
+
msg = f"[dim green]Repository index updated ({n} file(s))[/dim green]"
|
|
310
|
+
if console:
|
|
311
|
+
console.print(msg)
|
|
312
|
+
logger.info("Background incremental index complete: %d file(s) processed.", n)
|
|
313
|
+
except asyncio.CancelledError:
|
|
314
|
+
raise
|
|
315
|
+
except Exception as exc:
|
|
316
|
+
logger.warning("Background incremental indexing failed: %s", exc)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Dependency and import grapher using networkx."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import networkx as nx
|
|
7
|
+
|
|
8
|
+
from velune.repository.schemas import RepositoryEdge, RepositorySymbol, RepositorySymbolKind
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RepositoryGrapher:
|
|
12
|
+
"""Builds and analyzes dependency graphs for repository files and symbols."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, root_path: Path) -> None:
|
|
15
|
+
self.root_path = root_path.resolve()
|
|
16
|
+
self.graph = nx.MultiDiGraph()
|
|
17
|
+
|
|
18
|
+
def add_file(self, file_path: str, language: str, size_bytes: int) -> None:
|
|
19
|
+
"""Adds a file node to the dependency graph."""
|
|
20
|
+
# Normalize paths relative to workspace
|
|
21
|
+
rel_path = self._to_rel_path(file_path)
|
|
22
|
+
self.graph.add_node(
|
|
23
|
+
rel_path, kind="file", language=language, size_bytes=size_bytes, type="file"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def add_symbol(self, symbol: RepositorySymbol) -> None:
|
|
27
|
+
"""Adds a symbol node and binds it to its containing file."""
|
|
28
|
+
file_rel = self._to_rel_path(symbol.file_path)
|
|
29
|
+
sym_id = symbol.symbol_id or symbol.name
|
|
30
|
+
|
|
31
|
+
# Add symbol node
|
|
32
|
+
self.graph.add_node(
|
|
33
|
+
sym_id,
|
|
34
|
+
name=symbol.name,
|
|
35
|
+
qualified_name=symbol.qualified_name or symbol.name,
|
|
36
|
+
kind=symbol.kind.value,
|
|
37
|
+
file_path=file_rel,
|
|
38
|
+
line_start=symbol.line_start,
|
|
39
|
+
line_end=symbol.line_end,
|
|
40
|
+
type="symbol",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Draw containing relationship
|
|
44
|
+
self.graph.add_edge(file_rel, sym_id, edge_type="contains", weight=1.0)
|
|
45
|
+
|
|
46
|
+
def add_edge(self, edge: RepositoryEdge) -> None:
|
|
47
|
+
"""Adds a relationship edge between files or symbols."""
|
|
48
|
+
source_rel = self._to_rel_path(edge.source)
|
|
49
|
+
target_rel = self._to_rel_path(edge.target)
|
|
50
|
+
self.graph.add_edge(source_rel, target_rel, edge_type=edge.edge_type, weight=edge.weight)
|
|
51
|
+
|
|
52
|
+
def resolve_import_dependencies(
|
|
53
|
+
self, files: list[str], symbols: list[RepositorySymbol]
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Resolves module imports to concrete files and draws file-to-file import edges."""
|
|
56
|
+
# Map module names and symbol names to files
|
|
57
|
+
file_by_module: dict[str, str] = {}
|
|
58
|
+
for f in files:
|
|
59
|
+
rel = self._to_rel_path(f)
|
|
60
|
+
# e.g., velune/kernel/bus.py -> velune.kernel.bus
|
|
61
|
+
mod_name = rel.replace(".py", "").replace("/", ".").replace("\\", ".")
|
|
62
|
+
file_by_module[mod_name] = rel
|
|
63
|
+
|
|
64
|
+
# Keep index.js -> index, index.ts -> index conversions
|
|
65
|
+
if rel.endswith(("__init__.py", "index.ts", "index.js")):
|
|
66
|
+
parent_mod = os.path.dirname(rel).replace("/", ".").replace("\\", ".")
|
|
67
|
+
file_by_module[parent_mod] = rel
|
|
68
|
+
|
|
69
|
+
# Map import symbols to files
|
|
70
|
+
for sym in symbols:
|
|
71
|
+
if sym.kind == RepositorySymbolKind.IMPORT:
|
|
72
|
+
source_file = self._to_rel_path(sym.file_path)
|
|
73
|
+
import_name = sym.name
|
|
74
|
+
|
|
75
|
+
# Check direct module name match
|
|
76
|
+
# e.g. from velune.kernel.bus import CognitiveBus -> import_name is "velune.kernel.bus"
|
|
77
|
+
matched_file = file_by_module.get(import_name)
|
|
78
|
+
|
|
79
|
+
# Fallback: check metadata module
|
|
80
|
+
if not matched_file and "module" in sym.metadata:
|
|
81
|
+
mod = sym.metadata["module"]
|
|
82
|
+
matched_file = file_by_module.get(mod)
|
|
83
|
+
|
|
84
|
+
# If still not found, check relative import resolution
|
|
85
|
+
if not matched_file and import_name.startswith("."):
|
|
86
|
+
source_dir = os.path.dirname(source_file)
|
|
87
|
+
# Resolve relative dot hierarchy
|
|
88
|
+
dots = len(import_name) - len(import_name.lstrip("."))
|
|
89
|
+
parts = source_dir.split(os.sep) if source_dir else []
|
|
90
|
+
if len(parts) >= dots - 1:
|
|
91
|
+
target_dir_parts = parts[: len(parts) - (dots - 1)]
|
|
92
|
+
sub_mod = import_name.lstrip(".")
|
|
93
|
+
target_mod = (
|
|
94
|
+
".".join(target_dir_parts + [sub_mod]) if target_dir_parts else sub_mod
|
|
95
|
+
)
|
|
96
|
+
matched_file = file_by_module.get(target_mod)
|
|
97
|
+
|
|
98
|
+
if matched_file and source_file != matched_file:
|
|
99
|
+
self.graph.add_edge(source_file, matched_file, edge_type="imports", weight=1.0)
|
|
100
|
+
|
|
101
|
+
def traverse(self, node_id: str, depth: int = 2) -> list[str]:
|
|
102
|
+
"""BFS traversal to discover connected file and symbol nodes up to specified depth."""
|
|
103
|
+
node_rel = self._to_rel_path(node_id)
|
|
104
|
+
|
|
105
|
+
# Determine starting nodes
|
|
106
|
+
start_nodes = []
|
|
107
|
+
if node_rel in self.graph:
|
|
108
|
+
start_nodes.append(node_rel)
|
|
109
|
+
elif node_id in self.graph:
|
|
110
|
+
start_nodes.append(node_id)
|
|
111
|
+
else:
|
|
112
|
+
# Search by name or qualified_name in node attributes
|
|
113
|
+
for n, data in self.graph.nodes(data=True):
|
|
114
|
+
if data.get("type") == "symbol":
|
|
115
|
+
if data.get("name") == node_id or data.get("qualified_name") == node_id:
|
|
116
|
+
start_nodes.append(n)
|
|
117
|
+
|
|
118
|
+
if not start_nodes:
|
|
119
|
+
return []
|
|
120
|
+
|
|
121
|
+
visited: set[str] = set(start_nodes)
|
|
122
|
+
queue = list(start_nodes)
|
|
123
|
+
|
|
124
|
+
for _ in range(depth):
|
|
125
|
+
next_queue = []
|
|
126
|
+
for node in queue:
|
|
127
|
+
# Add successors (outgoing links)
|
|
128
|
+
if node in self.graph:
|
|
129
|
+
for succ in self.graph.successors(node):
|
|
130
|
+
if succ not in visited:
|
|
131
|
+
visited.add(succ)
|
|
132
|
+
next_queue.append(succ)
|
|
133
|
+
# Add predecessors (incoming links)
|
|
134
|
+
for pred in self.graph.predecessors(node):
|
|
135
|
+
if pred not in visited:
|
|
136
|
+
visited.add(pred)
|
|
137
|
+
next_queue.append(pred)
|
|
138
|
+
queue = next_queue
|
|
139
|
+
|
|
140
|
+
return list(visited)
|
|
141
|
+
|
|
142
|
+
def get_dependencies(self, file_path: str) -> list[str]:
|
|
143
|
+
"""Returns files imported by the given file."""
|
|
144
|
+
rel = self._to_rel_path(file_path)
|
|
145
|
+
if rel not in self.graph:
|
|
146
|
+
return []
|
|
147
|
+
|
|
148
|
+
deps = []
|
|
149
|
+
for _, target, data in self.graph.out_edges(rel, data=True):
|
|
150
|
+
if data.get("edge_type") == "imports":
|
|
151
|
+
deps.append(target)
|
|
152
|
+
return deps
|
|
153
|
+
|
|
154
|
+
def get_dependents(self, file_path: str) -> list[str]:
|
|
155
|
+
"""Returns files that import the given file."""
|
|
156
|
+
rel = self._to_rel_path(file_path)
|
|
157
|
+
if rel not in self.graph:
|
|
158
|
+
return []
|
|
159
|
+
|
|
160
|
+
dependents = []
|
|
161
|
+
for source, _, data in self.graph.in_edges(rel, data=True):
|
|
162
|
+
if data.get("edge_type") == "imports":
|
|
163
|
+
dependents.append(source)
|
|
164
|
+
return dependents
|
|
165
|
+
|
|
166
|
+
def _to_rel_path(self, path_str: str) -> str:
|
|
167
|
+
"""Helper to ensure paths are represented as uniform, workspace-relative strings."""
|
|
168
|
+
if not path_str or not (
|
|
169
|
+
path_str.startswith("/") or path_str.startswith("\\") or ":" in path_str
|
|
170
|
+
):
|
|
171
|
+
# Already relative or a symbol name
|
|
172
|
+
return path_str.replace("\\", "/")
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
p = Path(path_str).resolve()
|
|
176
|
+
rel = p.relative_to(self.root_path)
|
|
177
|
+
return str(rel).replace("\\", "/")
|
|
178
|
+
except (ValueError, RuntimeError):
|
|
179
|
+
return path_str.replace("\\", "/")
|