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,378 @@
1
+ """Graph Memory Tier (Tier 4).
2
+
3
+ Lightweight SQLite-backed Knowledge Graph store for indexing entities
4
+ (files, functions, concepts) and their semantic edge relationships.
5
+
6
+ All I/O is async and routed through
7
+ :class:`~velune.memory.storage.sqlite_pool.SQLiteConnectionPool`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import logging
14
+ import time
15
+ from typing import Any
16
+
17
+ from pydantic import BaseModel, Field
18
+
19
+ from velune.memory.storage.sqlite_pool import SQLiteConnectionPool
20
+
21
+ logger = logging.getLogger("velune.memory.tiers.graph")
22
+
23
+ _SCHEMA_SQL = """
24
+ CREATE TABLE IF NOT EXISTS graph_nodes (
25
+ id TEXT PRIMARY KEY,
26
+ node_type TEXT NOT NULL,
27
+ properties TEXT NOT NULL
28
+ );
29
+
30
+ CREATE TABLE IF NOT EXISTS graph_edges (
31
+ source TEXT NOT NULL,
32
+ target TEXT NOT NULL,
33
+ relation_type TEXT NOT NULL,
34
+ properties TEXT NOT NULL,
35
+ PRIMARY KEY (source, target, relation_type),
36
+ FOREIGN KEY (source) REFERENCES graph_nodes(id) ON DELETE CASCADE,
37
+ FOREIGN KEY (target) REFERENCES graph_nodes(id) ON DELETE CASCADE
38
+ );
39
+
40
+ CREATE TABLE IF NOT EXISTS execution_nodes (
41
+ id TEXT PRIMARY KEY,
42
+ task_id TEXT NOT NULL,
43
+ node_type TEXT NOT NULL,
44
+ status TEXT NOT NULL,
45
+ parameters TEXT NOT NULL,
46
+ outcome TEXT,
47
+ timestamp REAL NOT NULL
48
+ );
49
+
50
+ CREATE TABLE IF NOT EXISTS execution_edges (
51
+ source TEXT NOT NULL,
52
+ target TEXT NOT NULL,
53
+ relation_type TEXT NOT NULL,
54
+ PRIMARY KEY (source, target, relation_type),
55
+ FOREIGN KEY (source) REFERENCES execution_nodes(id) ON DELETE CASCADE,
56
+ FOREIGN KEY (target) REFERENCES execution_nodes(id) ON DELETE CASCADE
57
+ );
58
+
59
+ CREATE INDEX IF NOT EXISTS idx_edges_source ON graph_edges(source);
60
+ CREATE INDEX IF NOT EXISTS idx_edges_target ON graph_edges(target);
61
+ CREATE INDEX IF NOT EXISTS idx_exec_nodes_task ON execution_nodes(task_id);
62
+ """
63
+
64
+
65
+ class GraphNode(BaseModel):
66
+ """A single entity node in the Knowledge Graph."""
67
+
68
+ id: str
69
+ node_type: str
70
+ properties: dict[str, Any] = Field(default_factory=dict)
71
+
72
+
73
+ class GraphEdge(BaseModel):
74
+ """A directed edge connecting two entities in the Knowledge Graph."""
75
+
76
+ source: str
77
+ target: str
78
+ relation_type: str
79
+ properties: dict[str, Any] = Field(default_factory=dict)
80
+
81
+
82
+ class GraphMemoryTier:
83
+ """Tier 4: Structured entity-relationship store for codebase and cognitive dependencies."""
84
+
85
+ def __init__(self, pool: SQLiteConnectionPool) -> None:
86
+ self._pool = pool
87
+
88
+ # ------------------------------------------------------------------
89
+ # Lifecycle
90
+ # ------------------------------------------------------------------
91
+
92
+ async def initialize(self) -> None:
93
+ """Create tables and indexes if they do not yet exist."""
94
+ await self._init_db()
95
+
96
+ async def _init_db(self) -> None:
97
+ async with self._pool.write() as conn:
98
+ for stmt in _SCHEMA_SQL.split(";"):
99
+ stmt = stmt.strip()
100
+ if stmt:
101
+ await conn.execute(stmt)
102
+ logger.debug("GraphMemoryTier schema initialised.")
103
+
104
+ # ------------------------------------------------------------------
105
+ # Write operations
106
+ # ------------------------------------------------------------------
107
+
108
+ async def add_node(
109
+ self,
110
+ node_id: str,
111
+ node_type: str,
112
+ properties: dict[str, Any] | None = None,
113
+ ) -> None:
114
+ """Insert or upsert a node."""
115
+ props_str = json.dumps(properties or {})
116
+ try:
117
+ async with self._pool.write() as conn:
118
+ await conn.execute(
119
+ """
120
+ INSERT INTO graph_nodes (id, node_type, properties)
121
+ VALUES (?, ?, ?)
122
+ ON CONFLICT(id) DO UPDATE SET
123
+ node_type=excluded.node_type,
124
+ properties=excluded.properties
125
+ """,
126
+ (node_id, node_type, props_str),
127
+ )
128
+ except Exception as exc:
129
+ logger.error("Failed to add node %s: %s", node_id, exc)
130
+
131
+ async def add_edge(
132
+ self,
133
+ source_id: str,
134
+ target_id: str,
135
+ relation_type: str,
136
+ properties: dict[str, Any] | None = None,
137
+ ) -> None:
138
+ """Create or update a directed edge."""
139
+ props_str = json.dumps(properties or {})
140
+ try:
141
+ async with self._pool.write() as conn:
142
+ await conn.execute(
143
+ """
144
+ INSERT INTO graph_edges (source, target, relation_type, properties)
145
+ VALUES (?, ?, ?, ?)
146
+ ON CONFLICT(source, target, relation_type) DO UPDATE SET
147
+ properties=excluded.properties
148
+ """,
149
+ (source_id, target_id, relation_type, props_str),
150
+ )
151
+ except Exception as exc:
152
+ logger.error("Failed to add edge %s→%s: %s", source_id, target_id, exc)
153
+
154
+ async def upsert_entity(self, entity_id: str, entity_type: str, **properties: Any) -> None:
155
+ """Upsert a node (entity) in the knowledge graph."""
156
+ await self.add_node(node_id=entity_id, node_type=entity_type, properties=properties)
157
+
158
+ async def upsert_relationship(
159
+ self,
160
+ source_id: str,
161
+ target_id: str,
162
+ relation_type: str,
163
+ **properties: Any,
164
+ ) -> None:
165
+ """Upsert a directed edge (relationship) between two existing nodes."""
166
+ await self.add_edge(
167
+ source_id=source_id,
168
+ target_id=target_id,
169
+ relation_type=relation_type,
170
+ properties=properties,
171
+ )
172
+
173
+ async def record_execution_node(
174
+ self,
175
+ node_id: str,
176
+ task_id: str,
177
+ node_type: str,
178
+ status: str,
179
+ parameters: dict[str, Any],
180
+ outcome: str | None = None,
181
+ ) -> None:
182
+ """Record a step in the execution lineage graph."""
183
+ params_str = json.dumps(parameters)
184
+ try:
185
+ async with self._pool.write() as conn:
186
+ await conn.execute(
187
+ """
188
+ INSERT INTO execution_nodes
189
+ (id, task_id, node_type, status, parameters, outcome, timestamp)
190
+ VALUES (?, ?, ?, ?, ?, ?, ?)
191
+ ON CONFLICT(id) DO UPDATE SET
192
+ status=excluded.status,
193
+ parameters=excluded.parameters,
194
+ outcome=excluded.outcome,
195
+ timestamp=excluded.timestamp
196
+ """,
197
+ (node_id, task_id, node_type, status, params_str, outcome, time.time()),
198
+ )
199
+ except Exception as exc:
200
+ logger.error("Failed to record execution node %s: %s", node_id, exc)
201
+
202
+ async def record_execution_edge(
203
+ self,
204
+ source_id: str,
205
+ target_id: str,
206
+ relation_type: str,
207
+ ) -> None:
208
+ """Record a transition edge between execution steps."""
209
+ try:
210
+ async with self._pool.write() as conn:
211
+ await conn.execute(
212
+ """
213
+ INSERT INTO execution_edges (source, target, relation_type)
214
+ VALUES (?, ?, ?)
215
+ ON CONFLICT(source, target, relation_type) DO NOTHING
216
+ """,
217
+ (source_id, target_id, relation_type),
218
+ )
219
+ except Exception as exc:
220
+ logger.error("Failed to record execution edge %s→%s: %s", source_id, target_id, exc)
221
+
222
+ # ------------------------------------------------------------------
223
+ # Read operations
224
+ # ------------------------------------------------------------------
225
+
226
+ async def get_all_nodes(self) -> list[GraphNode]:
227
+ """Retrieve all nodes from the knowledge graph."""
228
+ nodes = []
229
+ try:
230
+ async with self._pool.read() as conn:
231
+ cursor = await conn.execute("SELECT id, node_type, properties FROM graph_nodes")
232
+ rows = await cursor.fetchall()
233
+ for row in rows:
234
+ nodes.append(
235
+ GraphNode(
236
+ id=row["id"],
237
+ node_type=row["node_type"],
238
+ properties=json.loads(row["properties"]),
239
+ )
240
+ )
241
+ except Exception as exc:
242
+ logger.error("Failed to retrieve all graph nodes: %s", exc)
243
+ return nodes
244
+
245
+ async def get_all_edges(self) -> list[GraphEdge]:
246
+ """Retrieve all edges from the knowledge graph."""
247
+ edges = []
248
+ try:
249
+ async with self._pool.read() as conn:
250
+ cursor = await conn.execute(
251
+ "SELECT source, target, relation_type, properties FROM graph_edges"
252
+ )
253
+ rows = await cursor.fetchall()
254
+ for row in rows:
255
+ edges.append(
256
+ GraphEdge(
257
+ source=row["source"],
258
+ target=row["target"],
259
+ relation_type=row["relation_type"],
260
+ properties=json.loads(row["properties"]),
261
+ )
262
+ )
263
+ except Exception as exc:
264
+ logger.error("Failed to retrieve all graph edges: %s", exc)
265
+ return edges
266
+
267
+ async def get_node(self, node_id: str) -> GraphNode | None:
268
+ """Fetch a specific node by its identifier."""
269
+ try:
270
+ async with self._pool.read() as conn:
271
+ cursor = await conn.execute(
272
+ "SELECT id, node_type, properties FROM graph_nodes WHERE id = ?",
273
+ (node_id,),
274
+ )
275
+ row = await cursor.fetchone()
276
+ if row:
277
+ return GraphNode(
278
+ id=row["id"],
279
+ node_type=row["node_type"],
280
+ properties=json.loads(row["properties"]),
281
+ )
282
+ except Exception as exc:
283
+ logger.error("Failed to query node %s: %s", node_id, exc)
284
+ return None
285
+
286
+ async def get_neighbors(self, node_id: str) -> list[tuple[GraphNode, str, GraphEdge]]:
287
+ """Find all neighbouring nodes and their edge relations."""
288
+ neighbors: list[tuple[GraphNode, str, GraphEdge]] = []
289
+ try:
290
+ async with self._pool.read() as conn:
291
+ cursor = await conn.execute(
292
+ """
293
+ SELECT e.relation_type, e.properties as edge_props,
294
+ n.id, n.node_type, n.properties as node_props
295
+ FROM graph_edges e
296
+ JOIN graph_nodes n ON e.target = n.id
297
+ WHERE e.source = ?
298
+ """,
299
+ (node_id,),
300
+ )
301
+ rows = await cursor.fetchall()
302
+ for row in rows:
303
+ target_node = GraphNode(
304
+ id=row["id"],
305
+ node_type=row["node_type"],
306
+ properties=json.loads(row["node_props"]),
307
+ )
308
+ edge = GraphEdge(
309
+ source=node_id,
310
+ target=row["id"],
311
+ relation_type=row["relation_type"],
312
+ properties=json.loads(row["edge_props"]),
313
+ )
314
+ neighbors.append((target_node, "outgoing", edge))
315
+ except Exception as exc:
316
+ logger.error("Failed to query graph neighbours for %s: %s", node_id, exc)
317
+ return neighbors
318
+
319
+ async def find_shortest_path(
320
+ self,
321
+ start_id: str,
322
+ end_id: str,
323
+ max_depth: int = 4,
324
+ ) -> list[str] | None:
325
+ """BFS to find the shortest relationship path between two nodes."""
326
+ if start_id == end_id:
327
+ return [start_id]
328
+
329
+ queue: list[list[str]] = [[start_id]]
330
+ visited = {start_id}
331
+
332
+ while queue:
333
+ path = queue.pop(0)
334
+ node = path[-1]
335
+
336
+ if len(path) > max_depth:
337
+ continue
338
+
339
+ for neighbor_node, _, _ in await self.get_neighbors(node):
340
+ n_id = neighbor_node.id
341
+ if n_id == end_id:
342
+ return path + [end_id]
343
+ if n_id not in visited:
344
+ visited.add(n_id)
345
+ queue.append(path + [n_id])
346
+
347
+ return None
348
+
349
+ async def query_execution_lineage(self, file_path: str) -> list[dict[str, Any]]:
350
+ """Query historical execution lineage for a file path."""
351
+ attempts = []
352
+ try:
353
+ async with self._pool.read() as conn:
354
+ cursor = await conn.execute(
355
+ """
356
+ SELECT id, task_id, node_type, status, parameters, outcome, timestamp
357
+ FROM execution_nodes
358
+ WHERE parameters LIKE ?
359
+ ORDER BY timestamp DESC
360
+ """,
361
+ (f"%{file_path}%",),
362
+ )
363
+ rows = await cursor.fetchall()
364
+ for row in rows:
365
+ attempts.append(
366
+ {
367
+ "id": row["id"],
368
+ "task_id": row["task_id"],
369
+ "node_type": row["node_type"],
370
+ "status": row["status"],
371
+ "parameters": json.loads(row["parameters"]),
372
+ "outcome": row["outcome"],
373
+ "timestamp": row["timestamp"],
374
+ }
375
+ )
376
+ except Exception as exc:
377
+ logger.error("Failed to query execution lineage for %s: %s", file_path, exc)
378
+ return attempts