ctxgraph-code 0.1.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.
- ctxgraph_code/__init__.py +0 -0
- ctxgraph_code/__main__.py +3 -0
- ctxgraph_code/analyzers/__init__.py +0 -0
- ctxgraph_code/analyzers/python/__init__.py +0 -0
- ctxgraph_code/analyzers/python/importer.py +140 -0
- ctxgraph_code/analyzers/python/semantic.py +75 -0
- ctxgraph_code/analyzers/python/symbols.py +221 -0
- ctxgraph_code/cli.py +337 -0
- ctxgraph_code/config/__init__.py +0 -0
- ctxgraph_code/config/init.py +14 -0
- ctxgraph_code/config/settings.py +121 -0
- ctxgraph_code/exclude/__init__.py +0 -0
- ctxgraph_code/exclude/patterns.py +75 -0
- ctxgraph_code/graph/__init__.py +0 -0
- ctxgraph_code/graph/builder.py +76 -0
- ctxgraph_code/graph/models.py +83 -0
- ctxgraph_code/graph/query.py +115 -0
- ctxgraph_code/graph/storage.py +224 -0
- ctxgraph_code/render.py +244 -0
- ctxgraph_code-0.1.0.dist-info/METADATA +279 -0
- ctxgraph_code-0.1.0.dist-info/RECORD +24 -0
- ctxgraph_code-0.1.0.dist-info/WHEEL +5 -0
- ctxgraph_code-0.1.0.dist-info/entry_points.txt +2 -0
- ctxgraph_code-0.1.0.dist-info/top_level.txt +1 -0
ctxgraph_code/cli.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from ctxgraph_code.config.init import init_project
|
|
12
|
+
from ctxgraph_code.config.settings import Settings
|
|
13
|
+
from ctxgraph_code.graph.builder import build_graph, get_storage
|
|
14
|
+
from ctxgraph_code.graph.query import search_relevant_nodes
|
|
15
|
+
from ctxgraph_code.render import (
|
|
16
|
+
render_context,
|
|
17
|
+
render_deps,
|
|
18
|
+
render_overview,
|
|
19
|
+
render_symbols,
|
|
20
|
+
render_usedby,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
app = typer.Typer(name="ctxgraph-code", help="Code knowledge graph for Claude Code")
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
SLASH_COMMAND_TEMPLATE = """# ctxgraph-code: Code Relationship Graph
|
|
28
|
+
|
|
29
|
+
This project has a knowledge graph at `.ctxgraph/graph.db`.
|
|
30
|
+
The graph knows about imports, class hierarchies, and function calls.
|
|
31
|
+
|
|
32
|
+
**Available commands** (run these as shell commands in the terminal):
|
|
33
|
+
|
|
34
|
+
- `ctxgraph-code query "search terms"` -- Find relevant files, classes, and functions
|
|
35
|
+
- `ctxgraph-code deps <path>` -- Show what a file imports and what calls it
|
|
36
|
+
- `ctxgraph-code usedby <path>` -- Show what depends on a file
|
|
37
|
+
- `ctxgraph-code overview` -- Show the full project structure
|
|
38
|
+
- `ctxgraph-code symbols <path>` -- List classes/functions defined in a file
|
|
39
|
+
- `ctxgraph-code context "task description"` -- Generate a focused context summary
|
|
40
|
+
|
|
41
|
+
**When to use:**
|
|
42
|
+
- Before modifying code, run `deps` and `usedby` to understand ripple effects.
|
|
43
|
+
- When exploring an unfamiliar area, run `query` to find relevant files, then read them.
|
|
44
|
+
- When asked about architecture, run `overview` for the big picture.
|
|
45
|
+
- For complex tasks, run `context "what I need to do"` for a focused summary.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.callback()
|
|
50
|
+
def callback():
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.command()
|
|
55
|
+
def init(
|
|
56
|
+
repo_path: Optional[str] = typer.Argument(
|
|
57
|
+
None, help="Path to repository (default: current directory)"
|
|
58
|
+
),
|
|
59
|
+
):
|
|
60
|
+
"""Scaffold .ctxgraph directory with default config."""
|
|
61
|
+
path = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
62
|
+
result = init_project(path)
|
|
63
|
+
config_path = result / "config.toml"
|
|
64
|
+
console.print(f"[green]Created {config_path}[/green]")
|
|
65
|
+
console.print(f"[green]Initialized .ctxgraph in: {result}[/green]")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@app.command()
|
|
69
|
+
def build(
|
|
70
|
+
repo_path: Optional[str] = typer.Argument(
|
|
71
|
+
None, help="Path to repository (default: current directory)"
|
|
72
|
+
),
|
|
73
|
+
exclude: Optional[list[str]] = typer.Option(
|
|
74
|
+
None, "--exclude", "-e", help="Additional exclude patterns"
|
|
75
|
+
),
|
|
76
|
+
):
|
|
77
|
+
"""Build the knowledge graph from Python source files."""
|
|
78
|
+
path = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
79
|
+
|
|
80
|
+
settings = Settings(path)
|
|
81
|
+
user_patterns = settings.exclude_patterns
|
|
82
|
+
if exclude:
|
|
83
|
+
user_patterns = list((user_patterns or []) + exclude)
|
|
84
|
+
|
|
85
|
+
if not (path / ".ctxgraph").exists():
|
|
86
|
+
(path / ".ctxgraph").mkdir(parents=True, exist_ok=True)
|
|
87
|
+
|
|
88
|
+
with console.status(f"Analyzing {path}..."):
|
|
89
|
+
stats = build_graph(path, exclude_patterns=user_patterns)
|
|
90
|
+
|
|
91
|
+
table = Table(title="Graph Build Complete")
|
|
92
|
+
table.add_column("Metric", style="cyan")
|
|
93
|
+
table.add_column("Value", style="green")
|
|
94
|
+
|
|
95
|
+
table.add_row("Files Analyzed", str(stats["files_analyzed"]))
|
|
96
|
+
table.add_row("Files Skipped", str(stats.get("files_skipped", 0)))
|
|
97
|
+
table.add_row("Errors", str(stats.get("errors", 0)))
|
|
98
|
+
table.add_row("Total Nodes", str(stats.get("total_nodes", 0)))
|
|
99
|
+
table.add_row("Total Edges", str(stats.get("total_edges", 0)))
|
|
100
|
+
table.add_row("Time", f"{stats.get('elapsed_seconds', 0)}s")
|
|
101
|
+
|
|
102
|
+
console.print(table)
|
|
103
|
+
console.print(f"\nGraph stored in: [bold]{path / '.ctxgraph' / 'graph.db'}[/bold]")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@app.command()
|
|
107
|
+
def query(
|
|
108
|
+
query: str = typer.Argument(..., help="Search query"),
|
|
109
|
+
repo_path: Optional[str] = typer.Option(
|
|
110
|
+
None, "--repo", "-r", help="Repository path"
|
|
111
|
+
),
|
|
112
|
+
max_results: int = typer.Option(
|
|
113
|
+
15, "--max", "-m", help="Maximum number of results"
|
|
114
|
+
),
|
|
115
|
+
):
|
|
116
|
+
"""Search the knowledge graph for relevant files, classes, and functions."""
|
|
117
|
+
path = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
118
|
+
|
|
119
|
+
storage = get_storage(path)
|
|
120
|
+
if storage is None:
|
|
121
|
+
console.print("[red]No graph found. Run [bold]ctxgraph-code build[/bold] first.[/red]")
|
|
122
|
+
raise typer.Exit(1)
|
|
123
|
+
|
|
124
|
+
results = search_relevant_nodes(storage, query, max_nodes=max_results)
|
|
125
|
+
|
|
126
|
+
if not results:
|
|
127
|
+
console.print("[yellow]No matches found.[/yellow]")
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
table = Table(title=f"Search Results: {query}")
|
|
131
|
+
table.add_column("Type", style="cyan")
|
|
132
|
+
table.add_column("Name", style="green")
|
|
133
|
+
table.add_column("Path", style="blue")
|
|
134
|
+
table.add_column("Score", style="yellow")
|
|
135
|
+
|
|
136
|
+
for node, score in results:
|
|
137
|
+
type_tag = {"file": "F", "class": "C", "function": "M"}
|
|
138
|
+
table.add_row(
|
|
139
|
+
type_tag.get(node.type, "?"),
|
|
140
|
+
node.name,
|
|
141
|
+
node.path or "-",
|
|
142
|
+
str(score),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
console.print(table)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.command()
|
|
149
|
+
def deps(
|
|
150
|
+
file_path: str = typer.Argument(..., help="Path to file (relative to repo root)"),
|
|
151
|
+
repo_path: Optional[str] = typer.Option(
|
|
152
|
+
None, "--repo", "-r", help="Repository path"
|
|
153
|
+
),
|
|
154
|
+
):
|
|
155
|
+
"""Show imports, dependents, and call relationships for a file."""
|
|
156
|
+
path = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
157
|
+
|
|
158
|
+
storage = get_storage(path)
|
|
159
|
+
if storage is None:
|
|
160
|
+
console.print("[red]No graph found. Run [bold]ctxgraph-code build[/bold] first.[/red]")
|
|
161
|
+
raise typer.Exit(1)
|
|
162
|
+
|
|
163
|
+
output = render_deps(storage, file_path)
|
|
164
|
+
console.print(output)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@app.command()
|
|
168
|
+
def usedby(
|
|
169
|
+
file_path: str = typer.Argument(..., help="Path to file (relative to repo root)"),
|
|
170
|
+
repo_path: Optional[str] = typer.Option(
|
|
171
|
+
None, "--repo", "-r", help="Repository path"
|
|
172
|
+
),
|
|
173
|
+
):
|
|
174
|
+
"""Show what files depend on a given file."""
|
|
175
|
+
path = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
176
|
+
|
|
177
|
+
storage = get_storage(path)
|
|
178
|
+
if storage is None:
|
|
179
|
+
console.print("[red]No graph found. Run [bold]ctxgraph-code build[/bold] first.[/red]")
|
|
180
|
+
raise typer.Exit(1)
|
|
181
|
+
|
|
182
|
+
output = render_usedby(storage, file_path)
|
|
183
|
+
console.print(output)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@app.command()
|
|
187
|
+
def overview(
|
|
188
|
+
repo_path: Optional[str] = typer.Option(
|
|
189
|
+
None, "--repo", "-r", help="Repository path"
|
|
190
|
+
),
|
|
191
|
+
):
|
|
192
|
+
"""Show the full project structure from the graph."""
|
|
193
|
+
path = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
194
|
+
|
|
195
|
+
storage = get_storage(path)
|
|
196
|
+
if storage is None:
|
|
197
|
+
console.print("[red]No graph found. Run [bold]ctxgraph-code build[/bold] first.[/red]")
|
|
198
|
+
raise typer.Exit(1)
|
|
199
|
+
|
|
200
|
+
output = render_overview(storage)
|
|
201
|
+
console.print(output)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@app.command()
|
|
205
|
+
def symbols(
|
|
206
|
+
file_path: str = typer.Argument(..., help="Path to file (relative to repo root)"),
|
|
207
|
+
repo_path: Optional[str] = typer.Option(
|
|
208
|
+
None, "--repo", "-r", help="Repository path"
|
|
209
|
+
),
|
|
210
|
+
):
|
|
211
|
+
"""List classes and functions defined in a file."""
|
|
212
|
+
path = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
213
|
+
|
|
214
|
+
storage = get_storage(path)
|
|
215
|
+
if storage is None:
|
|
216
|
+
console.print("[red]No graph found. Run [bold]ctxgraph-code build[/bold] first.[/red]")
|
|
217
|
+
raise typer.Exit(1)
|
|
218
|
+
|
|
219
|
+
output = render_symbols(storage, file_path)
|
|
220
|
+
console.print(output)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@app.command()
|
|
224
|
+
def context(
|
|
225
|
+
query: str = typer.Argument(..., help="Task description"),
|
|
226
|
+
repo_path: Optional[str] = typer.Option(
|
|
227
|
+
None, "--repo", "-r", help="Repository path"
|
|
228
|
+
),
|
|
229
|
+
max_nodes: int = typer.Option(
|
|
230
|
+
15, "--max-nodes", "-n", help="Maximum nodes to include"
|
|
231
|
+
),
|
|
232
|
+
):
|
|
233
|
+
"""Generate a focused context summary for a specific task."""
|
|
234
|
+
path = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
235
|
+
|
|
236
|
+
storage = get_storage(path)
|
|
237
|
+
if storage is None:
|
|
238
|
+
console.print("[red]No graph found. Run [bold]ctxgraph-code build[/bold] first.[/red]")
|
|
239
|
+
raise typer.Exit(1)
|
|
240
|
+
|
|
241
|
+
output = render_context(storage, query, max_nodes=max_nodes)
|
|
242
|
+
console.print(output)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@app.command()
|
|
246
|
+
def setup(
|
|
247
|
+
repo_path: Optional[str] = typer.Argument(
|
|
248
|
+
None, help="Path to repository (default: current directory)"
|
|
249
|
+
),
|
|
250
|
+
):
|
|
251
|
+
"""Initialize config, build the graph, and configure Claude Code integration."""
|
|
252
|
+
path = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
253
|
+
|
|
254
|
+
init_project(path)
|
|
255
|
+
console.print(f"[green][OK] Initialized .ctxgraph/[/green]")
|
|
256
|
+
|
|
257
|
+
settings = Settings(path)
|
|
258
|
+
with console.status(f"Building graph for {path}..."):
|
|
259
|
+
stats = build_graph(path, exclude_patterns=settings.exclude_patterns)
|
|
260
|
+
|
|
261
|
+
table = Table(title="Graph Build Complete")
|
|
262
|
+
table.add_column("Metric", style="cyan")
|
|
263
|
+
table.add_column("Value", style="green")
|
|
264
|
+
table.add_row("Files Analyzed", str(stats["files_analyzed"]))
|
|
265
|
+
table.add_row("Files Skipped", str(stats.get("files_skipped", 0)))
|
|
266
|
+
table.add_row("Errors", str(stats.get("errors", 0)))
|
|
267
|
+
table.add_row("Total Nodes", str(stats.get("total_nodes", 0)))
|
|
268
|
+
table.add_row("Total Edges", str(stats.get("total_edges", 0)))
|
|
269
|
+
table.add_row("Time", f"{stats.get('elapsed_seconds', 0)}s")
|
|
270
|
+
console.print(table)
|
|
271
|
+
console.print(f"[green][OK] Built graph[/green]")
|
|
272
|
+
|
|
273
|
+
claude_dir = path / ".claude" / "commands"
|
|
274
|
+
claude_dir.mkdir(parents=True, exist_ok=True)
|
|
275
|
+
slash_path = claude_dir / "ctxgraph-code.md"
|
|
276
|
+
if not slash_path.exists():
|
|
277
|
+
slash_path.write_text(SLASH_COMMAND_TEMPLATE, encoding="utf-8")
|
|
278
|
+
console.print(f"[green][OK] Created {slash_path}[/green]")
|
|
279
|
+
else:
|
|
280
|
+
console.print(f"[yellow] Skipped (already exists): {slash_path}[/yellow]")
|
|
281
|
+
|
|
282
|
+
console.print()
|
|
283
|
+
console.print("[bold green]Setup complete![/bold green]")
|
|
284
|
+
console.print("Open Claude Code in this project and type [bold]/ctxgraph-code[/bold] to get started.")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@app.command()
|
|
288
|
+
def info(
|
|
289
|
+
repo_path: Optional[str] = typer.Option(
|
|
290
|
+
None, "--repo", "-r", help="Repository path"
|
|
291
|
+
),
|
|
292
|
+
):
|
|
293
|
+
"""Show graph statistics."""
|
|
294
|
+
path = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
295
|
+
|
|
296
|
+
storage = get_storage(path)
|
|
297
|
+
if storage is None:
|
|
298
|
+
console.print("[red]No graph found. Run [bold]ctxgraph-code build[/bold] first.[/red]")
|
|
299
|
+
raise typer.Exit(1)
|
|
300
|
+
|
|
301
|
+
stats = storage.stats()
|
|
302
|
+
build_time = storage.get_metadata("build_time")
|
|
303
|
+
file_count = storage.get_metadata("file_count")
|
|
304
|
+
|
|
305
|
+
table = Table(title="Graph Info")
|
|
306
|
+
table.add_column("Metric", style="cyan")
|
|
307
|
+
table.add_column("Value", style="green")
|
|
308
|
+
|
|
309
|
+
table.add_row("Total Nodes", str(stats["nodes"]))
|
|
310
|
+
table.add_row("Total Edges", str(stats["edges"]))
|
|
311
|
+
|
|
312
|
+
plural_map = {"file": "files", "class": "classes", "function": "functions", "module": "modules"}
|
|
313
|
+
for t, cnt in stats.get("types", {}).items():
|
|
314
|
+
label = plural_map.get(t, t + "s")
|
|
315
|
+
table.add_row(f" {label}", str(cnt))
|
|
316
|
+
|
|
317
|
+
if file_count:
|
|
318
|
+
table.add_row("Files Analyzed", file_count)
|
|
319
|
+
if build_time:
|
|
320
|
+
table.add_row("Last Build", build_time)
|
|
321
|
+
|
|
322
|
+
console.print(table)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@app.command()
|
|
326
|
+
def version():
|
|
327
|
+
"""Show the version number."""
|
|
328
|
+
from importlib.metadata import version as _v
|
|
329
|
+
try:
|
|
330
|
+
ver = _v("ctxgraph-code")
|
|
331
|
+
except Exception:
|
|
332
|
+
ver = "0.1.0"
|
|
333
|
+
console.print(f"ctxgraph-code version [bold]{ver}[/bold]")
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
if __name__ == "__main__":
|
|
337
|
+
app()
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ctxgraph_code.config.settings import create_default_config
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def init_project(repo_path: Path) -> Path:
|
|
9
|
+
cfg_dir = repo_path / ".ctxgraph"
|
|
10
|
+
cfg_dir.mkdir(parents=True, exist_ok=True)
|
|
11
|
+
|
|
12
|
+
create_default_config(repo_path)
|
|
13
|
+
|
|
14
|
+
return cfg_dir
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
DEFAULT_CONFIG = {
|
|
10
|
+
"graph": {
|
|
11
|
+
"exclude": [],
|
|
12
|
+
"follow_symlinks": False,
|
|
13
|
+
"max_file_size_mb": 5,
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Settings:
|
|
19
|
+
def __init__(self, repo_path: Optional[Path] = None):
|
|
20
|
+
self.repo_path = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
21
|
+
self._data = dict(DEFAULT_CONFIG)
|
|
22
|
+
self._load()
|
|
23
|
+
|
|
24
|
+
def _load(self):
|
|
25
|
+
config_paths = [
|
|
26
|
+
self.repo_path / ".ctxgraph" / "config.toml",
|
|
27
|
+
self.repo_path / ".ctxgraph" / "config.json",
|
|
28
|
+
self.repo_path / "ctxgraph-code.toml",
|
|
29
|
+
self.repo_path / "ctxgraph-code.json",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
for path in config_paths:
|
|
33
|
+
if path.exists():
|
|
34
|
+
self._load_file(path)
|
|
35
|
+
break
|
|
36
|
+
|
|
37
|
+
def _load_file(self, path: Path):
|
|
38
|
+
text = path.read_text(encoding="utf-8")
|
|
39
|
+
if path.suffix == ".json":
|
|
40
|
+
parsed = json.loads(text)
|
|
41
|
+
self._deep_merge(self._data, parsed)
|
|
42
|
+
elif path.suffix == ".toml":
|
|
43
|
+
parsed = self._parse_toml(text)
|
|
44
|
+
self._deep_merge(self._data, parsed)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def exclude_patterns(self) -> list[str]:
|
|
48
|
+
return self._data["graph"].get("exclude", [])
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> dict:
|
|
51
|
+
return dict(self._data)
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def _parse_toml(text: str) -> dict:
|
|
55
|
+
result = {}
|
|
56
|
+
current_section = result
|
|
57
|
+
|
|
58
|
+
for line in text.split("\n"):
|
|
59
|
+
line = line.strip()
|
|
60
|
+
if not line or line.startswith("#"):
|
|
61
|
+
continue
|
|
62
|
+
if line.startswith("[") and line.endswith("]"):
|
|
63
|
+
section_name = line[1:-1].strip()
|
|
64
|
+
current_section = result.setdefault(section_name, {})
|
|
65
|
+
elif "=" in line:
|
|
66
|
+
key, _, value = line.partition("=")
|
|
67
|
+
key = key.strip()
|
|
68
|
+
value = value.strip()
|
|
69
|
+
|
|
70
|
+
if (value.startswith('"') and value.endswith('"')) or \
|
|
71
|
+
(value.startswith("'") and value.endswith("'")):
|
|
72
|
+
value = value[1:-1]
|
|
73
|
+
else:
|
|
74
|
+
value = Settings._parse_toml_value(value)
|
|
75
|
+
|
|
76
|
+
current_section[key] = value
|
|
77
|
+
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _parse_toml_value(value: str):
|
|
82
|
+
if value.lower() in ("true", "false"):
|
|
83
|
+
return value.lower() == "true"
|
|
84
|
+
try:
|
|
85
|
+
if "." in value:
|
|
86
|
+
return float(value)
|
|
87
|
+
return int(value)
|
|
88
|
+
except ValueError:
|
|
89
|
+
return value
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _deep_merge(base: dict, override: dict):
|
|
93
|
+
for key, value in override.items():
|
|
94
|
+
if key in base and isinstance(base[key], dict) and isinstance(value, dict):
|
|
95
|
+
Settings._deep_merge(base[key], value)
|
|
96
|
+
else:
|
|
97
|
+
base[key] = value
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def create_default_config(repo_path: Path):
|
|
101
|
+
config_dir = repo_path / ".ctxgraph"
|
|
102
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
|
|
104
|
+
config_path = config_dir / "config.toml"
|
|
105
|
+
if config_path.exists():
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
config_path.write_text(
|
|
109
|
+
"""# ctxgraph-code configuration
|
|
110
|
+
|
|
111
|
+
[graph]
|
|
112
|
+
# Additional exclude patterns (gitignore is used automatically)
|
|
113
|
+
exclude = []
|
|
114
|
+
# Follow symlinks when scanning files
|
|
115
|
+
follow_symlinks = false
|
|
116
|
+
# Skip files larger than this many MB
|
|
117
|
+
max_file_size_mb = 5
|
|
118
|
+
""",
|
|
119
|
+
encoding="utf-8",
|
|
120
|
+
)
|
|
121
|
+
return config_path
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
DEFAULT_EXCLUDE = [
|
|
8
|
+
"__pycache__",
|
|
9
|
+
"*.pyc",
|
|
10
|
+
".git",
|
|
11
|
+
".svn",
|
|
12
|
+
".hg",
|
|
13
|
+
"node_modules",
|
|
14
|
+
"venv",
|
|
15
|
+
".venv",
|
|
16
|
+
"env",
|
|
17
|
+
".env",
|
|
18
|
+
"dist",
|
|
19
|
+
"build",
|
|
20
|
+
"*.egg-info",
|
|
21
|
+
".pytest_cache",
|
|
22
|
+
".mypy_cache",
|
|
23
|
+
".ruff_cache",
|
|
24
|
+
".tox",
|
|
25
|
+
".nox",
|
|
26
|
+
"migrations",
|
|
27
|
+
"*.min.js",
|
|
28
|
+
"*.min.css",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def should_exclude(
|
|
33
|
+
file_path: Path,
|
|
34
|
+
root_path: Path,
|
|
35
|
+
user_patterns: Optional[list[str]] = None,
|
|
36
|
+
) -> bool:
|
|
37
|
+
patterns = list(DEFAULT_EXCLUDE)
|
|
38
|
+
if user_patterns:
|
|
39
|
+
patterns.extend(user_patterns)
|
|
40
|
+
|
|
41
|
+
rel_path = _relative_path(file_path, root_path)
|
|
42
|
+
|
|
43
|
+
for pattern in patterns:
|
|
44
|
+
if _matches_pattern(rel_path, pattern):
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _matches_pattern(path: str, pattern: str) -> bool:
|
|
51
|
+
if pattern.startswith("*."):
|
|
52
|
+
return path.endswith(pattern[1:])
|
|
53
|
+
|
|
54
|
+
if pattern.endswith("/"):
|
|
55
|
+
return pattern.rstrip("/") in path.split("/")
|
|
56
|
+
|
|
57
|
+
if "*" not in pattern:
|
|
58
|
+
return pattern in path.split("/")
|
|
59
|
+
|
|
60
|
+
if pattern.startswith("*") and pattern.endswith("*"):
|
|
61
|
+
mid = pattern[1:-1]
|
|
62
|
+
return mid in path
|
|
63
|
+
elif pattern.startswith("*"):
|
|
64
|
+
return path.endswith(pattern[1:])
|
|
65
|
+
elif pattern.endswith("*"):
|
|
66
|
+
return path.startswith(pattern[:-1])
|
|
67
|
+
|
|
68
|
+
return pattern in path
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _relative_path(file_path: Path, root_path: Path) -> str:
|
|
72
|
+
try:
|
|
73
|
+
return str(file_path.relative_to(root_path)).replace("\\", "/")
|
|
74
|
+
except ValueError:
|
|
75
|
+
return file_path.name
|
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ctxgraph_code.analyzers.python.importer import analyze_imports
|
|
8
|
+
from ctxgraph_code.analyzers.python.semantic import enrich_node_summary
|
|
9
|
+
from ctxgraph_code.analyzers.python.symbols import analyze_symbols
|
|
10
|
+
from ctxgraph_code.exclude.patterns import should_exclude
|
|
11
|
+
from ctxgraph_code.graph.models import Graph
|
|
12
|
+
from ctxgraph_code.graph.storage import Storage
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_graph(
|
|
16
|
+
repo_path: str | Path,
|
|
17
|
+
db_path: Optional[str | Path] = None,
|
|
18
|
+
exclude_patterns: Optional[list[str]] = None,
|
|
19
|
+
) -> dict:
|
|
20
|
+
repo_path = Path(repo_path).resolve()
|
|
21
|
+
if db_path is None:
|
|
22
|
+
db_path = repo_path / ".ctxgraph" / "graph.db"
|
|
23
|
+
|
|
24
|
+
db_path = Path(db_path)
|
|
25
|
+
start = time.time()
|
|
26
|
+
|
|
27
|
+
storage = Storage(db_path)
|
|
28
|
+
storage.connect()
|
|
29
|
+
combined = Graph()
|
|
30
|
+
stats = {"files_analyzed": 0, "files_skipped": 0, "errors": 0}
|
|
31
|
+
|
|
32
|
+
python_files = list(repo_path.rglob("*.py"))
|
|
33
|
+
for file_path in python_files:
|
|
34
|
+
if should_exclude(file_path, repo_path, exclude_patterns):
|
|
35
|
+
stats["files_skipped"] += 1
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
import_graph = analyze_imports(file_path, repo_path)
|
|
40
|
+
combined.merge(import_graph)
|
|
41
|
+
|
|
42
|
+
symbol_graph = analyze_symbols(file_path, repo_path)
|
|
43
|
+
combined.merge(symbol_graph)
|
|
44
|
+
|
|
45
|
+
for node in combined.nodes.values():
|
|
46
|
+
if node.path and node.path in str(file_path):
|
|
47
|
+
if not node.summary:
|
|
48
|
+
summary = enrich_node_summary(node, file_path)
|
|
49
|
+
if summary:
|
|
50
|
+
node.summary = summary
|
|
51
|
+
|
|
52
|
+
stats["files_analyzed"] += 1
|
|
53
|
+
except Exception:
|
|
54
|
+
stats["errors"] += 1
|
|
55
|
+
|
|
56
|
+
storage.save_graph(combined)
|
|
57
|
+
storage.save_metadata("build_time", str(time.time()))
|
|
58
|
+
storage.save_metadata("repo_path", str(repo_path))
|
|
59
|
+
storage.save_metadata("file_count", str(stats["files_analyzed"]))
|
|
60
|
+
storage.close()
|
|
61
|
+
|
|
62
|
+
elapsed = time.time() - start
|
|
63
|
+
stats["elapsed_seconds"] = round(elapsed, 2)
|
|
64
|
+
stats["total_nodes"] = len(combined.nodes)
|
|
65
|
+
stats["total_edges"] = len(combined.edges)
|
|
66
|
+
|
|
67
|
+
return stats
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_storage(repo_path: str | Path) -> Optional[Storage]:
|
|
71
|
+
db_path = Path(repo_path) / ".ctxgraph" / "graph.db"
|
|
72
|
+
if not db_path.exists():
|
|
73
|
+
return None
|
|
74
|
+
storage = Storage(db_path)
|
|
75
|
+
storage.connect()
|
|
76
|
+
return storage
|