graph-dependency-analyzer 0.2.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.
- graph_dependency_analyzer-0.2.0.dist-info/METADATA +158 -0
- graph_dependency_analyzer-0.2.0.dist-info/RECORD +86 -0
- graph_dependency_analyzer-0.2.0.dist-info/WHEEL +5 -0
- graph_dependency_analyzer-0.2.0.dist-info/entry_points.txt +2 -0
- graph_dependency_analyzer-0.2.0.dist-info/top_level.txt +1 -0
- src/__init__.py +0 -0
- src/application/__init__.py +0 -0
- src/application/dtos/__init__.py +0 -0
- src/application/dtos/affected_component.py +59 -0
- src/application/dtos/batch_processing_result.py +17 -0
- src/application/dtos/impact_analysis_response.py +56 -0
- src/application/dtos/impact_query_request.py +38 -0
- src/application/dtos/index_batch_request.py +53 -0
- src/application/dtos/index_batch_response.py +72 -0
- src/application/dtos/index_result.py +20 -0
- src/application/dtos/repository_index_result.py +79 -0
- src/application/exceptions.py +41 -0
- src/application/services/__init__.py +0 -0
- src/application/services/file_scanner_service.py +258 -0
- src/application/services/progress_tracker.py +132 -0
- src/application/use_cases/__init__.py +0 -0
- src/application/use_cases/clear_all_data_use_case.py +180 -0
- src/application/use_cases/index_batch_use_case.py +153 -0
- src/application/use_cases/index_repository_use_case.py +683 -0
- src/application/use_cases/indexing_orchestrator.py +181 -0
- src/application/use_cases/list_repositories_use_case.py +84 -0
- src/cli.py +512 -0
- src/config/__init__.py +5 -0
- src/config/container.py +211 -0
- src/config/logging_config.py +123 -0
- src/config/settings.py +165 -0
- src/domain/__init__.py +0 -0
- src/domain/entities/__init__.py +20 -0
- src/domain/entities/ast_node.py +32 -0
- src/domain/entities/code_chunk.py +45 -0
- src/domain/entities/code_file.py +44 -0
- src/domain/entities/dependency_graph.py +110 -0
- src/domain/entities/repository.py +46 -0
- src/domain/exceptions.py +36 -0
- src/domain/repositories/__init__.py +22 -0
- src/domain/repositories/i_code_parser.py +50 -0
- src/domain/repositories/i_dependency_extractor.py +35 -0
- src/domain/repositories/i_embedding_provider.py +31 -0
- src/domain/repositories/i_graph_repository.py +78 -0
- src/domain/repositories/i_vector_repository.py +55 -0
- src/domain/services/__init__.py +8 -0
- src/domain/services/dependency_extractor.py +962 -0
- src/domain/services/semantic_chunk_builder.py +719 -0
- src/domain/value_objects/__init__.py +12 -0
- src/domain/value_objects/chunk_type.py +24 -0
- src/domain/value_objects/dependency_type.py +22 -0
- src/domain/value_objects/node_type.py +22 -0
- src/domain/value_objects/qualified_name.py +78 -0
- src/infrastructure/__init__.py +0 -0
- src/infrastructure/exceptions.py +41 -0
- src/infrastructure/external_apis/__init__.py +0 -0
- src/infrastructure/external_apis/ollama_embedding_provider.py +134 -0
- src/infrastructure/external_apis/openai_embedding_provider.py +231 -0
- src/infrastructure/parsing/__init__.py +20 -0
- src/infrastructure/parsing/config_file_parser.py +347 -0
- src/infrastructure/parsing/gradle_dependency_parser.py +159 -0
- src/infrastructure/parsing/groovy_parser.py +198 -0
- src/infrastructure/parsing/maven_dependency_parser.py +147 -0
- src/infrastructure/parsing/microfrontend_config_parser.py +242 -0
- src/infrastructure/parsing/npm_package_dependency_parser.py +255 -0
- src/infrastructure/parsing/python_parser.py +211 -0
- src/infrastructure/parsing/sql_schema_parser.py +658 -0
- src/infrastructure/parsing/tree_sitter_java_parser.py +434 -0
- src/infrastructure/parsing/tree_sitter_typescript_parser.py +600 -0
- src/infrastructure/persistence/__init__.py +0 -0
- src/infrastructure/persistence/graph_export_repository.py +443 -0
- src/infrastructure/persistence/neo4j_dependency_repository.py +657 -0
- src/infrastructure/persistence/qdrant_vector_repository.py +357 -0
- src/infrastructure/persistence/repository_relationship_repository.py +1312 -0
- src/presentation/__init__.py +0 -0
- src/presentation/api/__init__.py +0 -0
- src/presentation/api/rest/__init__.py +0 -0
- src/presentation/api/rest/exception_handlers.py +174 -0
- src/presentation/api/rest/main.py +80 -0
- src/presentation/api/rest/routers/__init__.py +5 -0
- src/presentation/api/rest/routers/admin.py +103 -0
- src/presentation/api/rest/routers/analysis.py +980 -0
- src/presentation/api/rest/routers/export.py +442 -0
- src/presentation/api/rest/routers/health.py +117 -0
- src/presentation/api/rest/routers/indexing.py +148 -0
- src/presentation/api/rest/routers/relationships.py +1089 -0
src/cli.py
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
"""
|
|
2
|
+
graph-explorer CLI ā Knowledge Graph Dependency Analyzer
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
graph-explorer init Setup project (guides through .env, Docker, Ollama)
|
|
6
|
+
graph-explorer start Start the API server
|
|
7
|
+
graph-explorer index <repos> Index repositories into the graph
|
|
8
|
+
graph-explorer link Connect cross-repo dependencies
|
|
9
|
+
graph-explorer status Show indexing status
|
|
10
|
+
graph-explorer open Open the graph in browser
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import click
|
|
22
|
+
import httpx
|
|
23
|
+
|
|
24
|
+
# āā Constants āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
25
|
+
|
|
26
|
+
API_URL = "http://localhost:8000"
|
|
27
|
+
PACKAGE_ROOT = Path(__file__).parent.parent # project root when installed
|
|
28
|
+
|
|
29
|
+
_BANNER = """
|
|
30
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
31
|
+
ā Knowledge Graph ā Dependency Analyzer ā
|
|
32
|
+
ā graph-explorer CLI ā
|
|
33
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# āā Helpers āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
38
|
+
|
|
39
|
+
def _api_running() -> bool:
|
|
40
|
+
try:
|
|
41
|
+
r = httpx.get(f"{API_URL}/health", timeout=3)
|
|
42
|
+
return r.status_code == 200
|
|
43
|
+
except Exception:
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _docker_running() -> bool:
|
|
48
|
+
try:
|
|
49
|
+
result = subprocess.run(
|
|
50
|
+
["docker", "info"], capture_output=True, timeout=5
|
|
51
|
+
)
|
|
52
|
+
return result.returncode == 0
|
|
53
|
+
except Exception:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _ollama_running() -> bool:
|
|
58
|
+
try:
|
|
59
|
+
r = httpx.get("http://localhost:11434/api/tags", timeout=3)
|
|
60
|
+
return r.status_code == 200
|
|
61
|
+
except Exception:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _ollama_model_installed(model: str) -> bool:
|
|
66
|
+
try:
|
|
67
|
+
r = httpx.get("http://localhost:11434/api/tags", timeout=3)
|
|
68
|
+
if r.status_code == 200:
|
|
69
|
+
models = [m["name"] for m in r.json().get("models", [])]
|
|
70
|
+
return any(model in m for m in models)
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _find_project_root() -> Path:
|
|
77
|
+
"""Find the project root ā works both in dev and when pip-installed."""
|
|
78
|
+
# When pip installed, look for docker-compose.yml in CWD or parent
|
|
79
|
+
cwd = Path.cwd()
|
|
80
|
+
for p in [cwd, *cwd.parents]:
|
|
81
|
+
if (p / "docker-compose.yml").exists() and (p / ".env").exists():
|
|
82
|
+
return p
|
|
83
|
+
# Fallback: package root
|
|
84
|
+
return PACKAGE_ROOT
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _check_env_file(project_root: Path) -> bool:
|
|
88
|
+
return (project_root / ".env").exists()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# āā CLI Group āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
92
|
+
|
|
93
|
+
@click.group()
|
|
94
|
+
def cli():
|
|
95
|
+
"""Knowledge Graph Dependency Analyzer ā explore cross-repo dependencies."""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# āā init āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
100
|
+
|
|
101
|
+
@cli.command()
|
|
102
|
+
@click.option("--skip-docker", is_flag=True, help="Skip Docker setup check")
|
|
103
|
+
@click.option("--skip-ollama", is_flag=True, help="Skip Ollama setup check")
|
|
104
|
+
def init(skip_docker: bool, skip_ollama: bool):
|
|
105
|
+
"""
|
|
106
|
+
First-time setup wizard.
|
|
107
|
+
|
|
108
|
+
Guides through:\n
|
|
109
|
+
1. Creating .env from template\n
|
|
110
|
+
2. Starting Docker infrastructure (Neo4j + Qdrant)\n
|
|
111
|
+
3. Setting up Ollama (optional, for local embeddings)\n
|
|
112
|
+
4. Installing Claude Code skills
|
|
113
|
+
"""
|
|
114
|
+
click.echo(_BANNER)
|
|
115
|
+
click.echo("Welcome! Let's get you set up.\n")
|
|
116
|
+
|
|
117
|
+
project_root = _find_project_root()
|
|
118
|
+
env_file = project_root / ".env"
|
|
119
|
+
env_example = project_root / ".env.example"
|
|
120
|
+
|
|
121
|
+
# āā Step 1: .env āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
122
|
+
click.echo("š Step 1: Environment configuration")
|
|
123
|
+
|
|
124
|
+
if env_file.exists():
|
|
125
|
+
click.echo(" ā
.env already exists")
|
|
126
|
+
else:
|
|
127
|
+
if env_example.exists():
|
|
128
|
+
shutil.copy(env_example, env_file)
|
|
129
|
+
click.echo(" ā
Created .env from .env.example")
|
|
130
|
+
else:
|
|
131
|
+
click.echo(" ā ļø .env.example not found ā creating minimal .env")
|
|
132
|
+
env_file.write_text(_MINIMAL_ENV)
|
|
133
|
+
|
|
134
|
+
click.echo("\n ā ļø Please edit .env before continuing:")
|
|
135
|
+
click.echo(f" {env_file}")
|
|
136
|
+
click.echo("\n Required settings:")
|
|
137
|
+
click.echo(" REPOSITORIES_PATH=/path/to/your/repos")
|
|
138
|
+
click.echo(" EMBEDDING_PROVIDER=ollama (or 'flow' if using CI&T Flow)")
|
|
139
|
+
click.echo()
|
|
140
|
+
if not click.confirm(" Have you configured .env?", default=False):
|
|
141
|
+
click.echo("\n Please edit .env and run 'graph-explorer init' again.")
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
# āā Step 2: Docker āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
145
|
+
click.echo("\nš³ Step 2: Docker infrastructure (Neo4j + Qdrant)")
|
|
146
|
+
|
|
147
|
+
if skip_docker:
|
|
148
|
+
click.echo(" āļø Skipped (--skip-docker)")
|
|
149
|
+
elif not _docker_running():
|
|
150
|
+
click.echo(" ā Docker is not running.")
|
|
151
|
+
click.echo(" Please start Docker Desktop and run 'graph-explorer init' again.")
|
|
152
|
+
click.echo(" Download: https://docs.docker.com/get-docker/")
|
|
153
|
+
return
|
|
154
|
+
else:
|
|
155
|
+
compose_file = project_root / "docker-compose.yml"
|
|
156
|
+
if not compose_file.exists():
|
|
157
|
+
click.echo(" ā docker-compose.yml not found")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
click.echo(" š Starting containers...")
|
|
161
|
+
result = subprocess.run(
|
|
162
|
+
["docker", "compose", "up", "-d"],
|
|
163
|
+
cwd=str(project_root),
|
|
164
|
+
capture_output=True, text=True
|
|
165
|
+
)
|
|
166
|
+
if result.returncode != 0:
|
|
167
|
+
click.echo(f" ā Docker failed:\n{result.stderr}")
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
click.echo(" ā³ Waiting for Neo4j and Qdrant to be ready (~15s)...")
|
|
171
|
+
time.sleep(15)
|
|
172
|
+
click.echo(" ā
Containers running")
|
|
173
|
+
click.echo(" Neo4j browser: http://localhost:7474")
|
|
174
|
+
click.echo(" Qdrant: http://localhost:6333")
|
|
175
|
+
|
|
176
|
+
# āā Step 3: Ollama āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
177
|
+
click.echo("\nš¦ Step 3: Ollama (local AI embeddings ā no rate limits)")
|
|
178
|
+
|
|
179
|
+
if skip_ollama:
|
|
180
|
+
click.echo(" āļø Skipped (--skip-ollama)")
|
|
181
|
+
else:
|
|
182
|
+
# Check if .env uses ollama
|
|
183
|
+
env_content = env_file.read_text()
|
|
184
|
+
uses_ollama = "EMBEDDING_PROVIDER=ollama" in env_content
|
|
185
|
+
|
|
186
|
+
if not uses_ollama:
|
|
187
|
+
click.echo(" ā¹ļø EMBEDDING_PROVIDER is not 'ollama' in .env ā skipping")
|
|
188
|
+
click.echo(" (Using Flow/OpenAI for embeddings)")
|
|
189
|
+
elif not shutil.which("ollama"):
|
|
190
|
+
click.echo(" ā Ollama is not installed.")
|
|
191
|
+
click.echo("\n Install options:")
|
|
192
|
+
click.echo(" Mac: brew install ollama")
|
|
193
|
+
click.echo(" Other: https://ollama.com/download")
|
|
194
|
+
click.echo("\n After installing:")
|
|
195
|
+
click.echo(" ollama serve &")
|
|
196
|
+
click.echo(" ollama pull nomic-embed-text")
|
|
197
|
+
click.echo("\n Then run 'graph-explorer init' again.")
|
|
198
|
+
return
|
|
199
|
+
elif not _ollama_running():
|
|
200
|
+
click.echo(" ā ļø Ollama is installed but not running.")
|
|
201
|
+
click.echo(" Start it with: ollama serve")
|
|
202
|
+
if click.confirm(" Start Ollama now?", default=True):
|
|
203
|
+
subprocess.Popen(["ollama", "serve"],
|
|
204
|
+
stdout=subprocess.DEVNULL,
|
|
205
|
+
stderr=subprocess.DEVNULL)
|
|
206
|
+
time.sleep(3)
|
|
207
|
+
click.echo(" ā
Ollama started")
|
|
208
|
+
else:
|
|
209
|
+
click.echo(" ā ļø Ollama not running ā embeddings will fail")
|
|
210
|
+
else:
|
|
211
|
+
click.echo(" ā
Ollama is running")
|
|
212
|
+
|
|
213
|
+
if _ollama_running():
|
|
214
|
+
model = "nomic-embed-text"
|
|
215
|
+
if _ollama_model_installed(model):
|
|
216
|
+
click.echo(f" ā
Model '{model}' already installed")
|
|
217
|
+
else:
|
|
218
|
+
click.echo(f" š„ Pulling model '{model}' (~274MB)...")
|
|
219
|
+
subprocess.run(["ollama", "pull", model])
|
|
220
|
+
click.echo(f" ā
Model '{model}' ready")
|
|
221
|
+
|
|
222
|
+
# āā Step 4: Claude skills āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
223
|
+
click.echo("\nš¤ Step 4: Claude Code skills")
|
|
224
|
+
install_script = project_root / "install-graph-skills.sh"
|
|
225
|
+
if install_script.exists():
|
|
226
|
+
skills_dir = Path.home() / ".claude" / "skills" / "graph-index"
|
|
227
|
+
if skills_dir.exists():
|
|
228
|
+
click.echo(" ā
Skills already installed globally")
|
|
229
|
+
else:
|
|
230
|
+
if click.confirm(" Install /graph-index, /graph-link, /graph-impact skills globally?", default=True):
|
|
231
|
+
result = subprocess.run(
|
|
232
|
+
["bash", str(install_script)],
|
|
233
|
+
capture_output=True, text=True
|
|
234
|
+
)
|
|
235
|
+
if result.returncode == 0:
|
|
236
|
+
click.echo(" ā
Skills installed ā use /graph-index in Claude Code")
|
|
237
|
+
else:
|
|
238
|
+
click.echo(f" ā ļø {result.stderr[:100]}")
|
|
239
|
+
else:
|
|
240
|
+
click.echo(" āļø install-graph-skills.sh not found ā skipping")
|
|
241
|
+
|
|
242
|
+
# āā Done āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
243
|
+
click.echo("\n" + "ā" * 58)
|
|
244
|
+
click.echo("ā
Setup complete! Next steps:")
|
|
245
|
+
click.echo()
|
|
246
|
+
click.echo(" 1. Start the API: graph-explorer start")
|
|
247
|
+
click.echo(" 2. Index your repos: graph-explorer index <repo1> <repo2>")
|
|
248
|
+
click.echo(" 3. Connect links: graph-explorer link")
|
|
249
|
+
click.echo(" 4. Open the graph: graph-explorer open")
|
|
250
|
+
click.echo()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# āā start āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
254
|
+
|
|
255
|
+
@cli.command()
|
|
256
|
+
@click.option("--port", default=8000, show_default=True, help="API port")
|
|
257
|
+
@click.option("--host", default="0.0.0.0", show_default=True, help="API host")
|
|
258
|
+
@click.option("--no-reload", is_flag=True, help="Disable auto-reload")
|
|
259
|
+
def start(port: int, host: str, no_reload: bool):
|
|
260
|
+
"""Start the Knowledge Graph API server."""
|
|
261
|
+
click.echo(_BANNER)
|
|
262
|
+
|
|
263
|
+
if _api_running():
|
|
264
|
+
click.echo(f"ā
API already running at http://localhost:{port}")
|
|
265
|
+
click.echo(f" Docs: http://localhost:{port}/docs")
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
# Verify Docker infra
|
|
269
|
+
if not _api_running():
|
|
270
|
+
click.echo("š Checking infrastructure...")
|
|
271
|
+
|
|
272
|
+
reload_flag = [] if no_reload else ["--reload"]
|
|
273
|
+
cmd = [
|
|
274
|
+
sys.executable, "-m", "uvicorn",
|
|
275
|
+
"src.presentation.api.rest.main:app",
|
|
276
|
+
"--host", host,
|
|
277
|
+
"--port", str(port),
|
|
278
|
+
*reload_flag,
|
|
279
|
+
]
|
|
280
|
+
|
|
281
|
+
click.echo(f"š Starting API on http://{host}:{port}")
|
|
282
|
+
click.echo(f" Docs: http://localhost:{port}/docs")
|
|
283
|
+
click.echo(f" Graph: http://localhost:{port}/api/analysis/repository-relationships/graph.html")
|
|
284
|
+
click.echo("\n Press Ctrl+C to stop\n")
|
|
285
|
+
|
|
286
|
+
project_root = _find_project_root()
|
|
287
|
+
os.chdir(str(project_root))
|
|
288
|
+
os.execvp(sys.executable, cmd)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# āā index āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
292
|
+
|
|
293
|
+
@cli.command()
|
|
294
|
+
@click.argument("repos", nargs=-1, required=True)
|
|
295
|
+
@click.option("--parallel", "-p", default=None, type=int,
|
|
296
|
+
help="Parallel limit (auto-detected if not set)")
|
|
297
|
+
@click.option("--api-url", default=API_URL, show_default=True)
|
|
298
|
+
def index(repos: tuple, parallel: int | None, api_url: str):
|
|
299
|
+
"""
|
|
300
|
+
Index repositories into the Knowledge Graph.
|
|
301
|
+
|
|
302
|
+
Examples:\n
|
|
303
|
+
graph-explorer index wdc-packing-list wdc-ui\n
|
|
304
|
+
graph-explorer index wms-oracle --parallel 1
|
|
305
|
+
"""
|
|
306
|
+
if not _api_running():
|
|
307
|
+
click.echo("ā API is not running. Start it first:")
|
|
308
|
+
click.echo(" graph-explorer start")
|
|
309
|
+
raise SystemExit(1)
|
|
310
|
+
|
|
311
|
+
# Auto-detect parallel limit
|
|
312
|
+
large_repos = {"wmsportal", "wms-oracle", "wms-oracle-fork"}
|
|
313
|
+
if parallel is None:
|
|
314
|
+
parallel = 1 if any(r in large_repos for r in repos) else 3
|
|
315
|
+
|
|
316
|
+
click.echo(f"š¦ Indexing {len(repos)} repo(s) with parallel={parallel}...\n")
|
|
317
|
+
|
|
318
|
+
payload = {
|
|
319
|
+
"repositories": list(repos),
|
|
320
|
+
"parallel": parallel,
|
|
321
|
+
"background": False,
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
with httpx.Client(timeout=3600) as client:
|
|
326
|
+
r = client.post(f"{api_url}/api/indexing/index/batch", json=payload)
|
|
327
|
+
r.raise_for_status()
|
|
328
|
+
data = r.json()
|
|
329
|
+
except httpx.ConnectError:
|
|
330
|
+
click.echo("ā Cannot connect to API. Is it running?")
|
|
331
|
+
click.echo(" graph-explorer start")
|
|
332
|
+
raise SystemExit(1)
|
|
333
|
+
except Exception as e:
|
|
334
|
+
click.echo(f"ā Error: {e}")
|
|
335
|
+
raise SystemExit(1)
|
|
336
|
+
|
|
337
|
+
results = data.get("results", [])
|
|
338
|
+
success = data.get("success_count", 0)
|
|
339
|
+
total = data.get("total_processed", 0)
|
|
340
|
+
|
|
341
|
+
# Table
|
|
342
|
+
click.echo(f"{'Repository':<40} {'Status':<8} {'Files':>6} {'Nodes':>7} {'Chunks':>7}")
|
|
343
|
+
click.echo("ā" * 72)
|
|
344
|
+
for r in results:
|
|
345
|
+
name = (r.get("repository_name") or "")[:38]
|
|
346
|
+
ok = r.get("status") == "success"
|
|
347
|
+
status = "ā
ok" if ok else "ā err"
|
|
348
|
+
files = r.get("files_processed", 0) or 0
|
|
349
|
+
nodes = r.get("nodes_created", 0) or 0
|
|
350
|
+
chunks = r.get("chunks_created", 0) or 0
|
|
351
|
+
err = (r.get("error_message") or "")[:40]
|
|
352
|
+
click.echo(f"{name:<40} {status:<8} {files:>6} {nodes:>7} {chunks:>7} {err}")
|
|
353
|
+
|
|
354
|
+
click.echo("ā" * 72)
|
|
355
|
+
click.echo(f"{'TOTAL':<40} {success}/{total} ok\n")
|
|
356
|
+
|
|
357
|
+
# Has SQL repos?
|
|
358
|
+
sql_repos = {"wms-oracle", "wms-oracle-fork"}
|
|
359
|
+
if any(r in sql_repos for r in repos):
|
|
360
|
+
click.echo("š” SQL/Oracle repos indexed ā run: graph-explorer link --db-only")
|
|
361
|
+
else:
|
|
362
|
+
click.echo("š” Run 'graph-explorer link' to connect cross-repo dependencies")
|
|
363
|
+
|
|
364
|
+
click.echo(f"\nš View: {api_url}/api/analysis/repository-relationships/graph.html")
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# āā link āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
368
|
+
|
|
369
|
+
@cli.command()
|
|
370
|
+
@click.option("--http-only", is_flag=True, help="Only link HTTP calls")
|
|
371
|
+
@click.option("--db-only", is_flag=True, help="Only resolve DB refs")
|
|
372
|
+
@click.option("--api-url", default=API_URL, show_default=True)
|
|
373
|
+
def link(http_only: bool, db_only: bool, api_url: str):
|
|
374
|
+
"""
|
|
375
|
+
Connect cross-repository dependencies.
|
|
376
|
+
|
|
377
|
+
Runs:\n
|
|
378
|
+
1. cross-language-link ā matches HTTP calls to backend endpoints\n
|
|
379
|
+
2. resolve-db-refs ā connects Oracle/SQL procedures to tables
|
|
380
|
+
"""
|
|
381
|
+
if not _api_running():
|
|
382
|
+
click.echo("ā API is not running. Start it first: graph-explorer start")
|
|
383
|
+
raise SystemExit(1)
|
|
384
|
+
|
|
385
|
+
with httpx.Client(timeout=120) as client:
|
|
386
|
+
|
|
387
|
+
if not db_only:
|
|
388
|
+
click.echo("š Linking HTTP calls (frontend ā backend)...")
|
|
389
|
+
r = client.post(f"{api_url}/api/analysis/impact/cross-language-link")
|
|
390
|
+
d = r.json()
|
|
391
|
+
created = d.get("created", 0)
|
|
392
|
+
click.echo(f" ā
{created} CALLS_BACKEND edges created")
|
|
393
|
+
|
|
394
|
+
if not http_only:
|
|
395
|
+
click.echo("šļø Resolving DB cross-references (~30s)...")
|
|
396
|
+
r = client.post(f"{api_url}/api/analysis/impact/resolve-db-refs",
|
|
397
|
+
timeout=120)
|
|
398
|
+
d = r.json()
|
|
399
|
+
new_edges = d.get("total_new_edges", 0)
|
|
400
|
+
total = d.get("total_resolved_edges_in_graph", 0)
|
|
401
|
+
click.echo(f" ā
{new_edges} new DB edges ā {total} total resolved")
|
|
402
|
+
|
|
403
|
+
click.echo(f"\nš View: {api_url}/api/analysis/repository-relationships/graph.html")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
# āā status āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
407
|
+
|
|
408
|
+
@cli.command()
|
|
409
|
+
@click.option("--api-url", default=API_URL, show_default=True)
|
|
410
|
+
def status(api_url: str):
|
|
411
|
+
"""Show current indexing status and graph stats."""
|
|
412
|
+
click.echo("š Knowledge Graph Status\n")
|
|
413
|
+
|
|
414
|
+
# API
|
|
415
|
+
if _api_running():
|
|
416
|
+
click.echo(" ā
API running")
|
|
417
|
+
try:
|
|
418
|
+
r = httpx.get(f"{api_url}/health", timeout=3)
|
|
419
|
+
for check in r.json().get("checks", []):
|
|
420
|
+
icon = "ā
" if check.get("status") == "healthy" else "ā"
|
|
421
|
+
click.echo(f" {icon} {check['service']}: {check.get('message','')}")
|
|
422
|
+
except Exception:
|
|
423
|
+
pass
|
|
424
|
+
else:
|
|
425
|
+
click.echo(" ā API not running ā graph-explorer start")
|
|
426
|
+
|
|
427
|
+
# Docker
|
|
428
|
+
click.echo(f" {'ā
' if _docker_running() else 'ā'} Docker")
|
|
429
|
+
|
|
430
|
+
# Ollama
|
|
431
|
+
if _ollama_running():
|
|
432
|
+
click.echo(" ā
Ollama running")
|
|
433
|
+
if _ollama_model_installed("nomic-embed-text"):
|
|
434
|
+
click.echo(" ā
nomic-embed-text installed")
|
|
435
|
+
else:
|
|
436
|
+
click.echo(" ⬠Ollama not running (ok if using Flow/OpenAI)")
|
|
437
|
+
|
|
438
|
+
# Graph stats
|
|
439
|
+
if _api_running():
|
|
440
|
+
try:
|
|
441
|
+
r = httpx.get(f"{api_url}/api/analysis/repository-relationships", timeout=10)
|
|
442
|
+
d = r.json()
|
|
443
|
+
repos = d.get("repositories", [])
|
|
444
|
+
rels = d.get("relationships", [])
|
|
445
|
+
meta = d.get("meta", {})
|
|
446
|
+
click.echo(f"\n š Graph: {len(repos)} repos | {meta.get('total_after_dedup',len(rels))} relationships")
|
|
447
|
+
if repos:
|
|
448
|
+
click.echo(f" Indexed repos: {', '.join(repos[:10])}")
|
|
449
|
+
if len(repos) > 10:
|
|
450
|
+
click.echo(f" ... and {len(repos)-10} more")
|
|
451
|
+
except Exception:
|
|
452
|
+
pass
|
|
453
|
+
|
|
454
|
+
click.echo()
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# āā open āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
458
|
+
|
|
459
|
+
@cli.command("open")
|
|
460
|
+
@click.option("--repo", "-r", default=None, help="Open specific repo graph")
|
|
461
|
+
@click.option("--api-url", default=API_URL, show_default=True)
|
|
462
|
+
def open_graph(repo: str | None, api_url: str):
|
|
463
|
+
"""Open the dependency graph in your browser."""
|
|
464
|
+
import webbrowser
|
|
465
|
+
|
|
466
|
+
if repo:
|
|
467
|
+
url = f"{api_url}/api/export/graph.html?repository={repo}"
|
|
468
|
+
else:
|
|
469
|
+
url = f"{api_url}/api/analysis/repository-relationships/graph.html"
|
|
470
|
+
|
|
471
|
+
click.echo(f"š Opening: {url}")
|
|
472
|
+
webbrowser.open(url)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
# āā Minimal .env template āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
476
|
+
|
|
477
|
+
_MINIMAL_ENV = """\
|
|
478
|
+
# āā Neo4j āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
479
|
+
NEO4J_URI=bolt://localhost:7687
|
|
480
|
+
NEO4J_USER=neo4j
|
|
481
|
+
NEO4J_PASSWORD=medline2024
|
|
482
|
+
|
|
483
|
+
# āā Qdrant āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
484
|
+
QDRANT_URL=http://localhost:6333
|
|
485
|
+
QDRANT_COLLECTION=code_chunks
|
|
486
|
+
QDRANT_VECTOR_SIZE=768
|
|
487
|
+
|
|
488
|
+
# āā Repositories āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
489
|
+
REPOSITORIES_PATH=/path/to/your/repositories
|
|
490
|
+
|
|
491
|
+
# āā Embedding (ollama | flow | openai) āāāāāāāāāāāāāāāāāāā
|
|
492
|
+
EMBEDDING_PROVIDER=ollama
|
|
493
|
+
OLLAMA_BASE_URL=http://localhost:11434
|
|
494
|
+
OLLAMA_EMBEDDING_MODEL=nomic-embed-text
|
|
495
|
+
OLLAMA_EMBEDDING_DIM=768
|
|
496
|
+
|
|
497
|
+
# Flow (CI&T) ā set EMBEDDING_PROVIDER=flow to use
|
|
498
|
+
# OPENAI_API_KEY=your-flow-token
|
|
499
|
+
# OPENAI_BASE_URL=https://flow.ciandt.com/ai-orchestration-api
|
|
500
|
+
# OPENAI_EMBEDDING_MODEL=text-embedding-3-small
|
|
501
|
+
# QDRANT_VECTOR_SIZE=1536
|
|
502
|
+
"""
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# āā Entry point āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
506
|
+
|
|
507
|
+
def main():
|
|
508
|
+
cli()
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
if __name__ == "__main__":
|
|
512
|
+
main()
|