ctxgraph-code 0.2.0__py3-none-any.whl → 0.3.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/cli.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ import time
4
5
  from pathlib import Path
5
6
  from typing import Optional
6
7
 
@@ -8,6 +9,12 @@ import typer
8
9
  from rich.console import Console
9
10
  from rich.table import Table
10
11
 
12
+ from ctxgraph_code.config.build_status import (
13
+ get_build_status,
14
+ get_status_message,
15
+ mark_build_complete,
16
+ mark_build_started,
17
+ )
11
18
  from ctxgraph_code.config.global_paths import get_global_claude_commands_dir
12
19
  from ctxgraph_code.config.init import init_project
13
20
  from ctxgraph_code.config.settings import Settings
@@ -66,19 +73,30 @@ def _resolve_storage(
66
73
  if dir_name:
67
74
  s = get_storage(path, dir_name=dir_name)
68
75
  if s:
76
+ msg = get_status_message(path)
77
+ if msg:
78
+ console.print(msg)
69
79
  return s
70
- console.print(f"[red]Error: No graph found for directory '[bold]{dir_name}[/bold]'.[/red]")
80
+ console.print("[red]Error: No graph found for directory '[bold]{dir_name}[/bold]'.[/red]")
71
81
  _print_avail(avail)
72
82
  raise typer.Exit(1)
73
83
 
74
84
  if avail["_combined"]:
75
- return get_storage(path)
85
+ s = get_storage(path)
86
+ if s:
87
+ msg = get_status_message(path)
88
+ if msg:
89
+ console.print(msg)
90
+ return s
76
91
 
77
92
  if file_arg:
78
93
  d = _resolve_dir(path, file_arg)
79
94
  if d and d in avail["dirs"]:
80
95
  s = get_storage(path, dir_name=d)
81
96
  if s:
97
+ msg = get_status_message(path)
98
+ if msg:
99
+ console.print(msg)
82
100
  return s
83
101
 
84
102
  if avail["dirs"]:
@@ -237,17 +255,28 @@ def init(
237
255
 
238
256
 
239
257
  def _print_build_hint(path: Path):
240
- console.print("\n[yellow]Run [bold]ctxgraph-code build[/bold] to scan files and build the graph.[/yellow]")
258
+ console.print(
259
+ "\n[yellow]Run [bold]ctxgraph-code build[/bold]"
260
+ " to scan files and build the graph.[/yellow]"
261
+ )
241
262
 
242
263
 
243
- def _build_single_graph(path, exts, user_patterns, db_path, label):
264
+ def _build_single_graph(path, exts, user_patterns, db_path, label,
265
+ incremental=False, verbose=False, no_summary=False):
266
+ ctx_dir = path / ".ctxgraph"
267
+ mark_build_started(ctx_dir, os.getpid())
268
+ start = time.time()
244
269
  with console.status(f"Scanning {', '.join(exts)} files for '{label}'..."):
245
270
  stats = build_graph(
246
271
  path,
247
272
  db_path=db_path,
248
273
  exclude_patterns=user_patterns,
249
274
  extensions=exts,
275
+ incremental=incremental,
276
+ verbose=verbose,
277
+ no_summary=no_summary,
250
278
  )
279
+ mark_build_complete(ctx_dir, time.time() - start)
251
280
 
252
281
  table = Table(title=f"Graph Build: {label}")
253
282
  table.add_column("Metric", style="cyan")
@@ -261,6 +290,88 @@ def _build_single_graph(path, exts, user_patterns, db_path, label):
261
290
  return stats
262
291
 
263
292
 
293
+ def _build_dir_worker(path, exts, user_patterns, db_path, label,
294
+ incremental=False, verbose=False, no_summary=False):
295
+ """Silent build worker for parallel execution. No console output."""
296
+ ctx_dir = path / ".ctxgraph"
297
+ mark_build_started(ctx_dir, os.getpid())
298
+ start = time.time()
299
+ stats = build_graph(
300
+ path,
301
+ db_path=db_path,
302
+ exclude_patterns=user_patterns,
303
+ extensions=exts,
304
+ incremental=incremental,
305
+ verbose=verbose,
306
+ no_summary=no_summary,
307
+ )
308
+ mark_build_complete(ctx_dir, time.time() - start)
309
+ return label, stats
310
+
311
+
312
+ def _build_dirs_parallel(path, exts, user_patterns, top_dirs, graphs_dir, jobs,
313
+ incremental=False, verbose=False, no_summary=False):
314
+ """Build per-directory graphs in parallel using a thread pool."""
315
+ from concurrent.futures import ThreadPoolExecutor, as_completed
316
+
317
+ n_workers = max(1, jobs) if jobs > 0 else os.cpu_count() or 1
318
+ console.print(f"[dim]Building {len(top_dirs)} graphs with {n_workers} workers...[/dim]")
319
+
320
+ futures = []
321
+ with ThreadPoolExecutor(max_workers=n_workers) as pool:
322
+ for d in top_dirs:
323
+ db_path = graphs_dir / f"{d.name}.db"
324
+ fut = pool.submit(
325
+ _build_dir_worker, path, exts, user_patterns, db_path, d.name,
326
+ incremental, verbose, no_summary,
327
+ )
328
+ futures.append(fut)
329
+
330
+ results = []
331
+ for f in as_completed(futures):
332
+ try:
333
+ label, stats = f.result()
334
+ results.append((label, stats))
335
+ except Exception as e:
336
+ results.append(("error", str(e)))
337
+
338
+ results.sort(key=lambda x: x[0] if isinstance(x[0], str) else "")
339
+
340
+ summary = Table(title="Build Summary")
341
+ summary.add_column("Directory", style="cyan")
342
+ summary.add_column("Files", style="green")
343
+ summary.add_column("Nodes", style="blue")
344
+ summary.add_column("Edges", style="yellow")
345
+ summary.add_column("Time", style="magenta")
346
+
347
+ total_files = 0
348
+ total_nodes = 0
349
+ total_edges = 0
350
+ total_time = 0.0
351
+
352
+ for label, stats in results:
353
+ if isinstance(stats, str):
354
+ summary.add_row(f"{label}/", "[red]ERROR[/red]", stats, "", "")
355
+ else:
356
+ files = stats.get("files_analyzed", 0)
357
+ nodes = stats.get("total_nodes", 0)
358
+ edges = stats.get("total_edges", 0)
359
+ t = stats.get("elapsed_seconds", 0)
360
+ summary.add_row(f"{label}/", str(files), str(nodes), str(edges), f"{t}s")
361
+ total_files += files
362
+ total_nodes += nodes
363
+ total_edges += edges
364
+ total_time = max(total_time, t)
365
+
366
+ console.print(summary)
367
+ console.print(
368
+ f"\n[green]Built {len(top_dirs)} graphs: "
369
+ f"{total_files} files, {total_nodes} nodes, {total_edges} edges "
370
+ f"in {total_time}s[/green]"
371
+ )
372
+ return results
373
+
374
+
264
375
  @app.command()
265
376
  def build(
266
377
  repo_path: Optional[str] = typer.Argument(
@@ -275,10 +386,22 @@ def build(
275
386
  all_graph: bool = typer.Option(
276
387
  False, "--all", "-a", help="Build a single combined graph instead of per-directory"
277
388
  ),
389
+ jobs: int = typer.Option(
390
+ 0, "--jobs", "-j", help="Number of parallel workers (0 = auto, default: CPU count)"
391
+ ),
392
+ incremental: bool = typer.Option(
393
+ False, "--incremental", "-i", help="Only rebuild files that changed since last build"
394
+ ),
395
+ verbose: bool = typer.Option(
396
+ False, "--verbose", "-v", help="Show per-file progress"
397
+ ),
398
+ no_summary: bool = typer.Option(
399
+ False, "--no-summary", help="Skip docstring extraction for faster builds"
400
+ ),
278
401
  ):
279
402
  """Build the knowledge graph from source files.
280
403
 
281
- Default: builds a separate graph per top-level directory.
404
+ Default: builds a separate graph per top-level directory in parallel.
282
405
  Use --all to build one combined graph instead.
283
406
  """
284
407
  path = Path(repo_path).resolve() if repo_path else Path.cwd()
@@ -297,9 +420,9 @@ def build(
297
420
 
298
421
  if all_graph:
299
422
  db_path = ctx_dir / "graph.db"
300
- _build_single_graph(path, exts, user_patterns, db_path, "combined")
423
+ _build_single_graph(path, exts, user_patterns, db_path, "combined",
424
+ incremental, verbose, no_summary)
301
425
  else:
302
- # Per-directory: find top-level dirs (skip excluded ones)
303
426
  from ctxgraph_code.exclude.patterns import should_exclude
304
427
  top_dirs = sorted([
305
428
  d for d in path.iterdir()
@@ -308,34 +431,18 @@ def build(
308
431
  ])
309
432
 
310
433
  if not top_dirs:
311
- console.print("[yellow]No top-level source directories found. Building combined graph.[/yellow]")
434
+ console.print(
435
+ "[yellow]No top-level source directories found."
436
+ " Building combined graph.[/yellow]"
437
+ )
312
438
  db_path = ctx_dir / "graph.db"
313
- _build_single_graph(path, exts, user_patterns, db_path, "combined")
439
+ _build_single_graph(path, exts, user_patterns, db_path, "combined",
440
+ incremental, verbose, no_summary)
314
441
  else:
315
442
  graphs_dir = ctx_dir / "graphs"
316
443
  graphs_dir.mkdir(parents=True, exist_ok=True)
317
-
318
- total_nodes = 0
319
- total_edges = 0
320
- for d in top_dirs:
321
- db_path = graphs_dir / f"{d.name}.db"
322
- s = _build_single_graph(path, exts, user_patterns, db_path, d.name)
323
- total_nodes += s.get("total_nodes", 0)
324
- total_edges += s.get("total_edges", 0)
325
-
326
- summary = Table(title="Build Summary")
327
- summary.add_column("Directory", style="cyan")
328
- for d in top_dirs:
329
- db = graphs_dir / f"{d.name}.db"
330
- if db.exists():
331
- from ctxgraph_code.graph.storage import Storage
332
- st = Storage(db)
333
- st.connect()
334
- nd = st.stats()["nodes"]
335
- ed = st.stats()["edges"]
336
- summary.add_row(f"{d.name}/: {nd} nodes, {ed} edges")
337
- console.print(summary)
338
- console.print(f"\n[green]Built {len(top_dirs)} graphs in {graphs_dir}[/green]")
444
+ _build_dirs_parallel(path, exts, user_patterns, top_dirs, graphs_dir, jobs,
445
+ incremental, verbose, no_summary)
339
446
 
340
447
 
341
448
  @app.command()
@@ -490,6 +597,21 @@ def setup(
490
597
  False, "--project-slash",
491
598
  help="Install slash command in project .claude/ instead of globally",
492
599
  ),
600
+ jobs: int = typer.Option(
601
+ 0, "--jobs", "-j", help="Number of parallel workers (0 = auto, default: CPU count)"
602
+ ),
603
+ incremental: bool = typer.Option(
604
+ False, "--incremental", "-i", help="Only rebuild files that changed since last build"
605
+ ),
606
+ verbose: bool = typer.Option(
607
+ False, "--verbose", "-v", help="Show per-file progress"
608
+ ),
609
+ no_summary: bool = typer.Option(
610
+ False, "--no-summary", help="Skip docstring extraction for faster builds"
611
+ ),
612
+ background: bool = typer.Option(
613
+ False, "--background", "-b", help="Launch build in background and exit immediately"
614
+ ),
493
615
  ):
494
616
  """Initialize config, build the graph, and install the Claude Code slash command.
495
617
 
@@ -529,7 +651,51 @@ def setup(
529
651
  init_project(path, extensions=exts, exclude_patterns=excl)
530
652
  console.print("[green][OK] Initialized .ctxgraph/[/green]")
531
653
 
532
- # Build per-directory graphs
654
+ # Install slash command FIRST (immediate)
655
+ if project_slash:
656
+ claude_dir = path / ".claude" / "commands"
657
+ else:
658
+ claude_dir = get_global_claude_commands_dir()
659
+ slash_path = claude_dir / "ctxgraph-code.md"
660
+ _write_slash_command(slash_path, path)
661
+
662
+ # Build in background via detached subprocess
663
+ if background:
664
+ import subprocess
665
+ import sys
666
+
667
+ build_args = [sys.executable, "-m", "ctxgraph_code", "build"]
668
+ if repo_path:
669
+ build_args.append(repo_path)
670
+ if extensions:
671
+ build_args.extend(["--extensions", extensions])
672
+ if exclude:
673
+ build_args.extend(["--exclude", exclude])
674
+ if jobs:
675
+ build_args.extend(["--jobs", str(jobs)])
676
+ if incremental:
677
+ build_args.append("--incremental")
678
+ if verbose:
679
+ build_args.append("--verbose")
680
+ if no_summary:
681
+ build_args.append("--no-summary")
682
+
683
+ subprocess.Popen(
684
+ build_args,
685
+ creationflags=subprocess.DETACHED_PROCESS,
686
+ stdout=subprocess.DEVNULL,
687
+ stderr=subprocess.DEVNULL,
688
+ )
689
+ console.print("[yellow]Build launched in background.[/yellow]")
690
+ console.print("Check status with: [bold]ctxgraph-code build-status[/bold]")
691
+ console.print()
692
+ console.print("[bold green]Setup complete! Slash command installed.[/bold green]")
693
+ console.print(
694
+ "Open any Claude Code project and type [bold]/ctxgraph-code[/bold] to get started."
695
+ )
696
+ return
697
+
698
+ # Build (synchronous)
533
699
  ctx_dir = path / ".ctxgraph"
534
700
  graphs_dir = ctx_dir / "graphs"
535
701
  graphs_dir.mkdir(parents=True, exist_ok=True)
@@ -539,22 +705,14 @@ def setup(
539
705
  ])
540
706
 
541
707
  if top_dirs:
542
- for d in top_dirs:
543
- db_path = graphs_dir / f"{d.name}.db"
544
- _build_single_graph(path, exts, excl, db_path, d.name)
545
- console.print(f"[green][OK] Built graphs for {len(top_dirs)} directories[/green]")
708
+ _build_dirs_parallel(path, exts, excl, top_dirs, graphs_dir, jobs,
709
+ incremental, verbose, no_summary)
546
710
  else:
547
711
  db_path = ctx_dir / "graph.db"
548
- _build_single_graph(path, exts, excl, db_path, "combined")
712
+ _build_single_graph(path, exts, excl, db_path, "combined",
713
+ incremental, verbose, no_summary)
549
714
  console.print("[green][OK] Built combined graph[/green]")
550
715
 
551
- if project_slash:
552
- claude_dir = path / ".claude" / "commands"
553
- else:
554
- claude_dir = get_global_claude_commands_dir()
555
- slash_path = claude_dir / "ctxgraph-code.md"
556
- _write_slash_command(slash_path, path)
557
-
558
716
  console.print()
559
717
  console.print("[bold green]Setup complete![/bold green]")
560
718
  console.print(
@@ -586,6 +744,30 @@ def install_slash(
586
744
  _write_slash_command(slash_path, path)
587
745
 
588
746
 
747
+ @app.command(name="build-status")
748
+ def build_status(
749
+ repo_path: Optional[str] = typer.Option(
750
+ None, "--repo", "-r", help="Repository path"
751
+ ),
752
+ ):
753
+ """Show the status of the last or current graph build."""
754
+ path = Path(repo_path).resolve() if repo_path else Path.cwd()
755
+ msg = get_status_message(path)
756
+ if msg:
757
+ console.print(msg)
758
+ else:
759
+ status = get_build_status(path)
760
+ if not status:
761
+ console.print("[yellow]No build history found.[/yellow]")
762
+ console.print("Run [bold]ctxgraph-code build[/bold] to start one.")
763
+ elif status["status"] == "complete":
764
+ dur = status.get("duration_s", "?")
765
+ console.print(f"[green]Last build completed[/green] (duration: {dur}s)")
766
+ elif status["status"] == "failed":
767
+ err = status.get("error", "unknown")
768
+ console.print(f"[red]Last build failed: {err}[/red]")
769
+
770
+
589
771
  @app.command()
590
772
  def view(
591
773
  repo_path: Optional[str] = typer.Option(
@@ -684,7 +866,7 @@ def version():
684
866
  try:
685
867
  ver = _v("ctxgraph-code")
686
868
  except Exception:
687
- ver = "0.2.0"
869
+ ver = "0.3.0"
688
870
  console.print(f"ctxgraph-code version [bold]{ver}[/bold]")
689
871
 
690
872
 
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+
10
+ BUILD_STATUS_FILE = "build_status.json"
11
+
12
+
13
+ def _status_path(repo_path: Path) -> Path:
14
+ return repo_path / ".ctxgraph" / BUILD_STATUS_FILE
15
+
16
+
17
+ def mark_build_started(repo_path: Path, pid: int) -> dict:
18
+ data = {
19
+ "status": "in_progress",
20
+ "pid": pid,
21
+ "started_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
22
+ }
23
+ p = _status_path(repo_path)
24
+ p.parent.mkdir(parents=True, exist_ok=True)
25
+ p.write_text(json.dumps(data), encoding="utf-8")
26
+ return data
27
+
28
+
29
+ def mark_build_complete(repo_path: Path, duration_s: float):
30
+ p = _status_path(repo_path)
31
+ data = {"status": "complete", "duration_s": round(duration_s, 1)}
32
+ p.write_text(json.dumps(data), encoding="utf-8")
33
+
34
+
35
+ def mark_build_failed(repo_path: Path, error: str):
36
+ p = _status_path(repo_path)
37
+ data = {"status": "failed", "error": error}
38
+ p.write_text(json.dumps(data), encoding="utf-8")
39
+
40
+
41
+ def get_build_status(repo_path: Path) -> Optional[dict]:
42
+ p = _status_path(repo_path)
43
+ if not p.exists():
44
+ return None
45
+ try:
46
+ data = json.loads(p.read_text(encoding="utf-8"))
47
+ return data
48
+ except (json.JSONDecodeError, OSError):
49
+ return None
50
+
51
+
52
+ def check_pid_running(pid: int) -> bool:
53
+ """Check if a process with the given PID is still running (Windows)."""
54
+ try:
55
+ import ctypes
56
+ kernel32 = ctypes.windll.kernel32
57
+ handle = kernel32.OpenProcess(0x1000, False, pid)
58
+ if not handle:
59
+ return False
60
+ kernel32.CloseHandle(handle)
61
+ return True
62
+ except Exception:
63
+ return False
64
+
65
+
66
+ def get_status_message(repo_path: Path) -> Optional[str]:
67
+ status = get_build_status(repo_path)
68
+ if not status:
69
+ return None
70
+
71
+ if status["status"] == "in_progress":
72
+ pid = status.get("pid", 0)
73
+ if pid and not check_pid_running(pid):
74
+ mark_build_failed(repo_path, "process exited unexpectedly")
75
+ return (
76
+ "[red]Previous build appears to have crashed. "
77
+ "Run [bold]ctxgraph-code build[/bold] to retry.[/red]"
78
+ )
79
+ started = status.get("started_at", "unknown")
80
+ return (
81
+ f"[yellow]Graph build in progress (PID {pid}, started {started})."
82
+ f" Results may be partial. Check [bold]ctxgraph-code info[/bold]"
83
+ f" for status.[/yellow]"
84
+ )
85
+
86
+ if status["status"] == "failed":
87
+ err = status.get("error", "unknown error")
88
+ return (
89
+ f"[red]Previous build failed: {err}."
90
+ f" Run [bold]ctxgraph-code build[/bold] to retry.[/red]"
91
+ )
92
+
93
+ return None
@@ -1,8 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
+ import json
4
5
  import os
5
6
  import time
7
+ from functools import lru_cache
6
8
  from pathlib import Path
7
9
  from typing import Optional
8
10
 
@@ -13,6 +15,28 @@ from ctxgraph_code.exclude.patterns import should_exclude
13
15
  from ctxgraph_code.graph.models import Graph
14
16
  from ctxgraph_code.graph.storage import Storage
15
17
 
18
+ MTIMES_FILE = "file_mtimes.json"
19
+
20
+
21
+ def _mtimes_path(repo_path: Path) -> Path:
22
+ return repo_path / ".ctxgraph" / MTIMES_FILE
23
+
24
+
25
+ def _load_mtimes(repo_path: Path) -> dict[str, float]:
26
+ p = _mtimes_path(repo_path)
27
+ if not p.exists():
28
+ return {}
29
+ try:
30
+ return json.loads(p.read_text(encoding="utf-8"))
31
+ except (json.JSONDecodeError, OSError):
32
+ return {}
33
+
34
+
35
+ def _save_mtimes(repo_path: Path, mtimes: dict[str, float]):
36
+ p = _mtimes_path(repo_path)
37
+ p.parent.mkdir(parents=True, exist_ok=True)
38
+ p.write_text(json.dumps(mtimes, indent=1), encoding="utf-8")
39
+
16
40
 
17
41
  def build_graph(
18
42
  repo_path: str | Path,
@@ -20,6 +44,9 @@ def build_graph(
20
44
  exclude_patterns: Optional[list[str]] = None,
21
45
  extensions: Optional[list[str]] = None,
22
46
  jobs: int = 0,
47
+ incremental: bool = False,
48
+ verbose: bool = False,
49
+ no_summary: bool = False,
23
50
  ) -> dict:
24
51
  repo_path = Path(repo_path).resolve()
25
52
  if db_path is None:
@@ -31,23 +58,32 @@ def build_graph(
31
58
  if jobs <= 0:
32
59
  jobs = (os.cpu_count() or 1)
33
60
 
34
- # Walk files with early-exclude pruning + build path_index
61
+ @lru_cache(maxsize=None)
62
+ def _should_exclude(path: Path) -> bool:
63
+ return should_exclude(path, repo_path, exclude_patterns)
64
+
65
+ # ── walk files + collect mtimes ────────────────────────────────────────
35
66
  path_index: set[str] = set()
36
67
  scan_files: list[Path] = []
68
+ current_mtimes: dict[str, float] = {}
37
69
 
38
70
  for dirpath, dirnames, filenames in os.walk(repo_path):
39
- rel = os.path.relpath(dirpath, repo_path).replace("\\", "/")
40
71
  dirnames[:] = [
41
72
  d for d in dirnames
42
- if not should_exclude(repo_path / d, repo_path, exclude_patterns)
73
+ if not _should_exclude(repo_path / d)
43
74
  ]
44
75
  for fn in filenames:
76
+ if Path(fn).suffix not in exts:
77
+ continue
45
78
  fp = Path(dirpath) / fn
46
- if fp.suffix in exts and fp.is_file():
47
- rp = str(fp.relative_to(repo_path)).replace("\\", "/")
48
- if not should_exclude(fp, repo_path, exclude_patterns):
49
- path_index.add(rp)
50
- scan_files.append(fp)
79
+ rp = str(fp.relative_to(repo_path)).replace("\\", "/")
80
+ if not _should_exclude(fp):
81
+ path_index.add(rp)
82
+ scan_files.append(fp)
83
+ try:
84
+ current_mtimes[rp] = os.path.getmtime(fp)
85
+ except OSError:
86
+ current_mtimes[rp] = 0.0
51
87
 
52
88
  if not scan_files:
53
89
  elapsed = time.time() - start
@@ -57,69 +93,160 @@ def build_graph(
57
93
  "total_nodes": 0, "total_edges": 0,
58
94
  }
59
95
 
60
- stats = {"files_analyzed": 0, "errors": 0}
61
-
62
- use_mp = jobs > 1 and len(scan_files) > 50
96
+ # ── incremental: filter to changed files only ─────────────────────────
97
+ changed_files: list[Path] = list(scan_files)
98
+ old_mtimes: dict[str, float] = {}
99
+
100
+ if incremental:
101
+ old_mtimes = _load_mtimes(repo_path)
102
+ filtered: list[Path] = []
103
+ for fp in scan_files:
104
+ rp = str(fp.relative_to(repo_path)).replace("\\", "/")
105
+ old_mtime = old_mtimes.get(rp)
106
+ current_mtime = current_mtimes.get(rp, 0)
107
+ if old_mtime is None or old_mtime != current_mtime:
108
+ filtered.append(fp)
109
+ changed_files = filtered
110
+
111
+ removed_paths = old_mtimes.keys() - current_mtimes.keys()
112
+
113
+ if not changed_files:
114
+ elapsed = time.time() - start
115
+ return {
116
+ "files_analyzed": 0, "files_skipped": len(scan_files), "errors": 0,
117
+ "elapsed_seconds": round(elapsed, 2),
118
+ "total_nodes": 0, "total_edges": 0,
119
+ }
120
+
121
+ stats: dict = {"files_analyzed": 0, "errors": 0}
63
122
  all_nodes: list[dict] = []
64
123
  all_edges: list[dict] = []
65
124
 
125
+ n_total = len(changed_files)
126
+ use_mp = jobs > 1 and n_total > 10
127
+
128
+ if verbose:
129
+ from rich.console import Console
130
+ vconsole = Console()
131
+ vconsole.log(f"Processing {n_total} files ({len(scan_files)} total) with {jobs} workers...")
132
+
133
+ # ── process files ─────────────────────────────────────────────────────
66
134
  if use_mp:
67
135
  import multiprocessing as mp
68
- chunk_size = max(1, len(scan_files) // jobs)
69
- chunks = [scan_files[i:i + chunk_size] for i in range(0, len(scan_files), chunk_size)]
136
+ chunk_size = max(1, n_total // jobs)
137
+ chunks = [changed_files[i:i + chunk_size] for i in range(0, n_total, chunk_size)]
70
138
 
71
139
  with mp.Pool(jobs) as pool:
72
140
  results = [
73
- pool.apply_async(_worker_batch, (chunk, repo_path, path_index))
141
+ pool.apply_async(_worker_batch, (chunk, repo_path, path_index, no_summary))
74
142
  for chunk in chunks
75
143
  ]
76
- for r in results:
144
+ for i, r in enumerate(results):
77
145
  try:
78
146
  nds, eds, ok, err = r.get(timeout=600)
79
147
  all_nodes.extend(nds)
80
148
  all_edges.extend(eds)
81
149
  stats["files_analyzed"] += ok
82
150
  stats["errors"] += err
151
+ if verbose:
152
+ done = min((i + 1) * chunk_size, n_total)
153
+ vconsole.log(f" [{done}/{n_total}] files done...")
83
154
  except Exception:
84
- pass
155
+ stats["errors"] += chunk_size
85
156
  else:
86
- for file_path in scan_files:
157
+ for i, file_path in enumerate(changed_files):
87
158
  try:
88
- nds, eds = _process_file(file_path, repo_path, path_index)
159
+ nds, eds = _process_file(file_path, repo_path, path_index, no_summary)
89
160
  all_nodes.extend(nds)
90
161
  all_edges.extend(eds)
91
162
  stats["files_analyzed"] += 1
92
163
  except Exception:
93
164
  stats["errors"] += 1
165
+ if verbose and (i + 1) % 50 == 0:
166
+ from rich.console import Console
167
+ vconsole = Console()
168
+ vconsole.log(f" [{i + 1}/{n_total}] files done...")
94
169
 
170
+ # ── save to DB ────────────────────────────────────────────────────────
95
171
  graph = Graph.from_batch(all_nodes, all_edges)
96
-
97
172
  storage = Storage(db_path)
98
173
  storage.connect()
174
+
175
+ if incremental:
176
+ for fp in changed_files:
177
+ rp = str(fp.relative_to(repo_path)).replace("\\", "/")
178
+ storage.delete_nodes_for_file(rp)
179
+ for rp in removed_paths:
180
+ storage.delete_nodes_for_file(rp)
181
+
99
182
  storage.save_graph(graph)
100
183
  storage.save_metadata("build_time", str(time.time()))
101
184
  storage.save_metadata("repo_path", str(repo_path))
102
185
  storage.save_metadata("file_count", str(stats["files_analyzed"]))
103
186
  storage.close()
104
187
 
188
+ # ── save mtimes for incremental ───────────────────────────────────────
189
+ if incremental:
190
+ _save_mtimes(repo_path, current_mtimes)
191
+
105
192
  elapsed = time.time() - start
106
193
  stats["elapsed_seconds"] = round(elapsed, 2)
107
194
  stats["total_nodes"] = len(graph.nodes)
108
195
  stats["total_edges"] = len(graph.edges)
109
196
 
197
+ if verbose:
198
+ from rich.console import Console
199
+ vconsole = Console()
200
+ vconsole.log(
201
+ f"Done: {stats['files_analyzed']} files, {stats['total_nodes']} nodes, "
202
+ f"{stats['total_edges']} edges in {elapsed:.1f}s"
203
+ )
204
+
110
205
  return stats
111
206
 
112
207
 
113
208
  # ── worker helpers ───────────────────────────────────────────────────────────
114
209
 
115
210
 
211
+ def _quick_scan(source: str) -> bool:
212
+ """Quick pre-check: does this file have imports, classes, functions,
213
+ or decorators? Returns True if full AST parsing is needed."""
214
+ for line in source.splitlines():
215
+ line = line.strip()
216
+ if not line or line.startswith("#"):
217
+ continue
218
+ if any(line.startswith(kw) for kw in ("import ", "from ", "class ", "def ", "@")):
219
+ return True
220
+ return False
221
+
222
+
116
223
  def _process_file(
117
224
  file_path: Path,
118
225
  root_path: Path,
119
226
  path_index: set[str],
227
+ no_summary: bool = False,
120
228
  ) -> tuple[list[dict], list[dict]]:
229
+ rel = str(file_path.relative_to(root_path)).replace("\\", "/")
230
+
121
231
  try:
122
232
  source = file_path.read_text(encoding="utf-8", errors="replace")
233
+ except OSError:
234
+ return [], []
235
+
236
+ if not _quick_scan(source):
237
+ return [{
238
+ "id": f"{root_path}:{rel}",
239
+ "type": "file",
240
+ "name": file_path.name,
241
+ "path": rel,
242
+ "parent_id": None,
243
+ "summary": None,
244
+ "importance": 0.5,
245
+ "size_bytes": len(source),
246
+ "lineno": 0,
247
+ }], []
248
+
249
+ try:
123
250
  tree = ast.parse(source)
124
251
  except SyntaxError:
125
252
  return [], []
@@ -139,19 +266,20 @@ def _process_file(
139
266
  for ed in sg.edges:
140
267
  edges.append(ed.to_dict())
141
268
 
142
- for nd in sg.nodes.values():
143
- if nd.summary is None:
144
- enriched = enrich_node_summary(nd, file_path, source=source, tree=tree)
269
+ if not no_summary:
270
+ for nd in sg.nodes.values():
271
+ if nd.summary is None:
272
+ enriched = enrich_node_summary(nd, file_path, source=source, tree=tree)
273
+ if enriched:
274
+ nd.summary = enriched
275
+ nodes.append(nd.to_dict())
276
+
277
+ file_node = next((nd for nd in ig.nodes.values() if nd.type == "file"), None)
278
+ if file_node and file_node.summary is None:
279
+ enriched = enrich_node_summary(file_node, file_path, source=source, tree=tree)
145
280
  if enriched:
146
- nd.summary = enriched
147
- nodes.append(nd.to_dict())
148
-
149
- file_node = next((nd for nd in ig.nodes.values() if nd.type == "file"), None)
150
- if file_node and file_node.summary is None:
151
- enriched = enrich_node_summary(file_node, file_path, source=source, tree=tree)
152
- if enriched:
153
- file_node.summary = enriched
154
- nodes.append(file_node.to_dict())
281
+ file_node.summary = enriched
282
+ nodes.append(file_node.to_dict())
155
283
 
156
284
  return nodes, edges
157
285
 
@@ -160,6 +288,7 @@ def _worker_batch(
160
288
  file_paths: list[Path],
161
289
  root_path: Path,
162
290
  path_index: set[str],
291
+ no_summary: bool = False,
163
292
  ) -> tuple[list[dict], list[dict], int, int]:
164
293
  nodes: list[dict] = []
165
294
  edges: list[dict] = []
@@ -167,7 +296,7 @@ def _worker_batch(
167
296
  err = 0
168
297
  for fp in file_paths:
169
298
  try:
170
- nds, eds = _process_file(fp, root_path, path_index)
299
+ nds, eds = _process_file(fp, root_path, path_index, no_summary)
171
300
  nodes.extend(nds)
172
301
  edges.extend(eds)
173
302
  ok += 1
@@ -75,37 +75,31 @@ class Storage:
75
75
  )
76
76
  self.conn.commit()
77
77
 
78
- def save_node(self, node: Node):
79
- self.conn.execute(
80
- """INSERT OR REPLACE INTO nodes
81
- (id, type, name, path, parent_id, summary, importance, size_bytes, lineno)
82
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
83
- (
84
- node.id,
85
- node.type,
86
- node.name,
87
- node.path,
88
- node.parent_id,
89
- node.summary,
90
- node.importance,
91
- node.size_bytes,
92
- node.lineno,
93
- ),
94
- )
95
-
96
- def save_edge(self, edge: Edge):
97
- self.conn.execute(
98
- """INSERT OR REPLACE INTO edges
99
- (source_id, target_id, relation, weight)
100
- VALUES (?, ?, ?, ?)""",
101
- (edge.source_id, edge.target_id, edge.relation, edge.weight),
102
- )
103
-
104
78
  def save_graph(self, graph: Graph):
105
- for node in graph.nodes.values():
106
- self.save_node(node)
107
- for edge in graph.edges:
108
- self.save_edge(edge)
79
+ nodes_data = [
80
+ (n.id, n.type, n.name, n.path, n.parent_id, n.summary,
81
+ n.importance, n.size_bytes, n.lineno)
82
+ for n in graph.nodes.values()
83
+ ]
84
+ if nodes_data:
85
+ self.conn.executemany(
86
+ """INSERT OR REPLACE INTO nodes
87
+ (id, type, name, path, parent_id, summary, importance, size_bytes, lineno)
88
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
89
+ nodes_data,
90
+ )
91
+
92
+ edges_data = [
93
+ (e.source_id, e.target_id, e.relation, e.weight)
94
+ for e in graph.edges
95
+ ]
96
+ if edges_data:
97
+ self.conn.executemany(
98
+ """INSERT OR REPLACE INTO edges
99
+ (source_id, target_id, relation, weight)
100
+ VALUES (?, ?, ?, ?)""",
101
+ edges_data,
102
+ )
109
103
  self.conn.commit()
110
104
 
111
105
  def get_node(self, node_id: str) -> Optional[Node]:
@@ -210,6 +204,26 @@ class Storage:
210
204
  "types": {r["type"]: r["cnt"] for r in type_counts},
211
205
  }
212
206
 
207
+ def delete_nodes_for_file(self, file_path: str):
208
+ """Remove all nodes and edges belonging to a file (for incremental rebuild)."""
209
+ cursor = self.conn.execute(
210
+ "SELECT id FROM nodes WHERE path = ?", (file_path,)
211
+ )
212
+ node_ids = [r[0] for r in cursor.fetchall()]
213
+ if not node_ids:
214
+ return
215
+ placeholders = ",".join("?" for _ in node_ids)
216
+ self.conn.execute(
217
+ "DELETE FROM edges WHERE source_id IN ({}) OR target_id IN ({})".format(
218
+ placeholders, placeholders
219
+ ),
220
+ node_ids + node_ids,
221
+ )
222
+ self.conn.execute(
223
+ f"DELETE FROM nodes WHERE id IN ({placeholders})", node_ids
224
+ )
225
+ self.conn.commit()
226
+
213
227
  def save_metadata(self, key: str, value: str):
214
228
  self.conn.execute(
215
229
  "INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctxgraph-code
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Code knowledge graph for Claude Code. Build a relationship graph of your Python codebase and query it during coding sessions.
5
5
  Author: ctxgraph-code contributors
6
6
  License: MIT
@@ -76,15 +76,23 @@ Interactive walkthrough — prompts for:
76
76
 
77
77
  Does everything in one step:
78
78
  1. Creates `.ctxgraph/config.toml` with your chosen extensions and excludes
79
- 2. Builds the knowledge graph from all matching files
80
- 3. Creates `.claude/commands/ctxgraph-code.md` with instructions for Claude Code
79
+ 2. Installs the `/ctxgraph-code` slash command globally (works in every Claude Code session)
80
+ 3. Builds the knowledge graph from all matching files
81
81
 
82
- Non-interactive mode (skip prompts):
82
+ Non-interactive mode:
83
83
  ```bash
84
84
  ctxgraph-code setup --extensions .py,.js,.ts --exclude tests/,examples/
85
85
  ctxgraph-code setup -y # all defaults
86
86
  ```
87
87
 
88
+ Options:
89
+ - `--project-slash` — install slash command in project's `.claude/` instead of globally
90
+ - `--background` / `-b` — launch build in background and exit immediately (check with `build-status`)
91
+ - `--jobs` / `-j` — number of parallel workers (default: CPU count)
92
+ - `--incremental` / `-i` — only rebuild files that changed since last build
93
+ - `--verbose` / `-v` — show per-file progress
94
+ - `--no-summary` — skip docstring extraction for faster builds
95
+
88
96
  ### `init`
89
97
 
90
98
  ```bash
@@ -103,24 +111,27 @@ ctxgraph-code build --exclude tests/ --exclude *.generated.py
103
111
 
104
112
  Scans all matching files in the project, runs AST analysis. Extensions are read from config (`.py` by default, or whatever was set in `setup`).
105
113
 
106
- - **Imports**: which files import other files
107
- - **Class definitions**: class names, base classes, methods
108
- - **Function definitions**: function names, arguments
109
- - **Function calls**: which functions call which (within the project)
110
- - **Docstrings**: extracted as node summaries
114
+ **By default, builds a separate graph per top-level directory** (e.g., `src/`, `api/`, `tests/`) in parallel. This keeps each graph small and fast to query. Use `--dir <name>` on query commands to select one, or let it auto-detect from file paths.
115
+
116
+ - `--all` / `-a` build a single combined graph instead of per-directory
117
+ - `--jobs` / `-j` number of parallel workers (default: CPU count)
118
+ - `--incremental` / `-i` only rebuild files that changed since last build
119
+ - `--verbose` / `-v` — show per-file progress
120
+ - `--no-summary` — skip docstring extraction for faster builds
111
121
 
112
- Stores the result in `.ctxgraph/graph.db`.
122
+ Stores graphs in `.ctxgraph/graphs/<dir>.db` (per-directory) or `.ctxgraph/graph.db` (combined).
113
123
 
114
- > The graph is a **static snapshot**. If code changes, run `ctxgraph-code build` again to refresh. Claude Code will also rebuild when it detects the graph is stale.
124
+ > The graph is a **static snapshot**. If code changes, run `ctxgraph-code build` again to refresh. Use `--incremental` to only reprocess changed files.
115
125
 
116
126
  ### `query`
117
127
 
118
128
  ```bash
119
129
  ctxgraph-code query "user authentication"
120
130
  ctxgraph-code query "database connection" --max 20
131
+ ctxgraph-code query "api routes" --dir src
121
132
  ```
122
133
 
123
- Searches the graph by relevance scoring (name matches > summary matches > path matches) and expands to neighboring nodes via BFS up to depth 2.
134
+ Searches the graph by relevance scoring (name matches > summary matches > path matches) and expands to neighboring nodes via BFS up to depth 2. Use `--dir` to scope to a specific per-directory graph.
124
135
 
125
136
  ### `deps`
126
137
 
@@ -128,7 +139,7 @@ Searches the graph by relevance scoring (name matches > summary matches > path m
128
139
  ctxgraph-code deps src/api/routes.py
129
140
  ```
130
141
 
131
- Shows all relationships for a file: imports, imported-by, function calls, class definitions.
142
+ Shows all relationships for a file: imports, imported-by, function calls, class definitions. Auto-detects the per-directory graph from the file path.
132
143
 
133
144
  ### `usedby`
134
145
 
@@ -142,9 +153,10 @@ Shows every file that imports or calls something in the given file. Useful to un
142
153
 
143
154
  ```bash
144
155
  ctxgraph-code overview
156
+ ctxgraph-code overview --dir src
145
157
  ```
146
158
 
147
- Prints the project structure: every file with its summary and top-level symbols.
159
+ Prints the project structure: every file with its summary and top-level symbols. Use `--dir` to scope to a per-directory graph.
148
160
 
149
161
  ### `symbols`
150
162
 
@@ -181,10 +193,28 @@ Use `--tree` for a terminal-friendly text view of the directory hierarchy with s
181
193
 
182
194
  ```bash
183
195
  ctxgraph-code info
196
+ ctxgraph-code info --dir src
184
197
  ```
185
198
 
186
199
  Shows graph statistics: node/edge counts, type distribution, build time.
187
200
 
201
+ ### `install-slash`
202
+
203
+ ```bash
204
+ ctxgraph-code install-slash
205
+ ctxgraph-code install-slash --project-slash # project-local instead of global
206
+ ```
207
+
208
+ Install or update the `/ctxgraph-code` slash command for Claude Code. By default installs globally so it works in every Claude Code session. Use `--project-slash` to install in the project's `.claude/commands/` directory instead.
209
+
210
+ ### `build-status`
211
+
212
+ ```bash
213
+ ctxgraph-code build-status
214
+ ```
215
+
216
+ Check whether a background build (`ctxgraph-code setup --background` or `ctxgraph-code build`) completed, failed, or is still running. Shows PID and start time for in-progress builds.
217
+
188
218
  ---
189
219
 
190
220
  ## How It Works
@@ -192,7 +222,7 @@ Shows graph statistics: node/edge counts, type distribution, build time.
192
222
  ```
193
223
  Python files ──AST──> Import/Symbol/Call analysis ──> SQLite graph.db
194
224
 
195
- Claude Code ──/ctxgraph-code──> CLI query/deps/overview <────┘
225
+ Claude Code ──/ctxgraph-code──> CLI query/deps/overview <───┘
196
226
  ```
197
227
 
198
228
  1. **Build phase**: `ctxgraph-code build` parses every `.py` file with Python's `ast` module. It extracts imports, class/function definitions, function calls, and docstrings. The result is a graph of **nodes** (files, classes, functions) and **edges** (imports, defines, extends, calls) stored in SQLite.
@@ -226,25 +256,43 @@ Edge weights: `imports=1.0`, `defines=1.0`, `extends=0.8`, `calls=0.7`
226
256
 
227
257
  ---
228
258
 
229
- ## Using with Claude Code
259
+ ## Performance
230
260
 
231
- After `ctxgraph-code setup`, Claude Code in the project directory will have the `/ctxgraph-code` slash command available.
261
+ `ctxgraph-code` includes several optimizations for large codebases:
232
262
 
233
- When you type `/ctxgraph-code`, Claude sees:
263
+ | Optimization | Details |
264
+ |---|---|
265
+ | **Parallel builds** | Per-directory graphs build concurrently via `ThreadPoolExecutor` |
266
+ | **Multiprocessing** | Combined graphs split files across CPU cores via `multiprocessing.Pool` |
267
+ | **`--jobs`** | Control parallelism level (default: CPU count) |
268
+ | **Incremental builds** | `--incremental` caches file mtimes, only reprocesses changed files |
269
+ | **Trivial file skip** | `_quick_scan()` pre-checks before AST parse for files with no imports/classes/functions |
270
+ | **Cached excludes** | `lru_cache` on `should_exclude()` during `os.walk` |
271
+ | **Batch SQLite inserts** | `executemany` instead of per-row `INSERT` statements |
272
+ | **`--no-summary`** | Skips docstring extraction (fastest rebuilds) |
273
+ | **`--background`** | Detach build process and continue working immediately |
274
+
275
+ ---
276
+
277
+ ## Using with Claude Code
278
+
279
+ After `ctxgraph-code setup`, the `/ctxgraph-code` slash command is installed globally by default — it works in every Claude Code session. Claude sees:
234
280
 
235
281
  ```
236
282
  # ctxgraph-code: Code Relationship Graph
237
283
 
238
- This project has a knowledge graph at `.ctxgraph/graph.db`.
239
- The graph knows about imports, class hierarchies, and function calls.
240
-
241
- Available commands:
242
- - ctxgraph-code query "search terms" -- Find relevant files, classes, and functions
243
- - ctxgraph-code deps <path> -- Show what a file imports and what calls it
244
- - ctxgraph-code usedby <path> -- Show what depends on a file
245
- - ctxgraph-code overview -- Show the full project structure
246
- - ctxgraph-code symbols <path> -- List classes/functions defined in a file
247
- - ctxgraph-code context "task" -- Generate a focused context summary
284
+ **First time in this project?** Tell the user to run `ctxgraph-code setup`.
285
+ **Graph needs refresh?** Tell the user to run `ctxgraph-code build`.
286
+ **Available graphs:** src api tests
287
+
288
+ **Commands:**
289
+ - `ctxgraph-code query "terms"` -- Find relevant files, classes, and functions
290
+ - `ctxgraph-code deps <path>` -- Show what a file imports and what calls it
291
+ - `ctxgraph-code usedby <path>` -- Show what depends on a file
292
+ - `ctxgraph-code overview --dir <name>` -- Show project structure for a specific graph
293
+ - `ctxgraph-code symbols <path>` -- List classes/functions defined in a file
294
+ - `ctxgraph-code context "task"` -- Generate a focused context summary
295
+ - `ctxgraph-code view --dir <name>` -- Visualize a graph interactively
248
296
  ```
249
297
 
250
298
  Claude then uses these commands as needed during the conversation.
@@ -287,7 +335,7 @@ Built-in default exclusion patterns (always applied): `__pycache__`, `*.pyc`, `.
287
335
 
288
336
  | Feature | ctxgraph | ctxgraph-code |
289
337
  |---------|----------|---------------|
290
- | CLI commands | 9 (build, capsule, query, view, serve, info, init, ask, chat, history, skill) | 9 (init, build, query, deps, usedby, overview, symbols, context, setup, view, info) |
338
+ | CLI commands | 9+ | 12 (init, build, query, deps, usedby, overview, symbols, context, setup, view, info, install-slash, build-status) |
291
339
  | LLM integration | Built-in (Ollama, Claude, OpenAI, Azure) | None (delegates to Claude Code) |
292
340
  | Chat sessions | Yes | No |
293
341
  | Visualizer | D3.js HTML + SVG | D3.js HTML (`view` opens in browser, `--tree` for text) |
@@ -1,6 +1,6 @@
1
1
  ctxgraph_code/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  ctxgraph_code/__main__.py,sha256=AfV3Onj_9FoL9W8B5jOWOeIyYF2MwX6S4-wyF9U90wM,41
3
- ctxgraph_code/cli.py,sha256=1D5PwRWp-GrVlfbBEsv2rngJpLVhB_WFap_OHDr_khw,23884
3
+ ctxgraph_code/cli.py,sha256=5ShvEYTi5JzN01_bPLhsds0oZfAnbQOOiAlyLz4OgxI,30322
4
4
  ctxgraph_code/render.py,sha256=zic5X5gKfuqZSL0sAxsAOCkZcHnac0eta2FXO_DNaHQ,12619
5
5
  ctxgraph_code/analyzers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  ctxgraph_code/analyzers/python/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -8,20 +8,21 @@ ctxgraph_code/analyzers/python/importer.py,sha256=_5WV-RHK9IzbjCaVi_-1rWCYSuJIu0
8
8
  ctxgraph_code/analyzers/python/semantic.py,sha256=gS_y2J6uZ5DoFdEz6fF1lp2cBte7hKOIBz2nrrA5db4,2489
9
9
  ctxgraph_code/analyzers/python/symbols.py,sha256=xKtpn-78sh8H_xf7TBs9bzIAO42NnuiY1X92d11nNJ4,6330
10
10
  ctxgraph_code/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ ctxgraph_code/config/build_status.py,sha256=e2tdNc34oJd-iDAi4aFEi-TkY9lIhkicCjmSj6VpvHY,2754
11
12
  ctxgraph_code/config/global_paths.py,sha256=Ot1YIzdTqOValsvN_oEstcg8dFcFGCtHo6OnwOp1IYE,511
12
13
  ctxgraph_code/config/init.py,sha256=tXVcDTPAJGVbzPavABRfnCXUsf9Pt-VzDUgV5YX09Tc,501
13
14
  ctxgraph_code/config/settings.py,sha256=t0Mu0U9T43ztDxxqj2NXGB3BbaY7Ll7e8UMhTLlY-Xw,4873
14
15
  ctxgraph_code/exclude/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
16
  ctxgraph_code/exclude/patterns.py,sha256=4jBbiVeMgMmK5c0Kucl5g2xiBtAQ1tVtH8dDXA7lz7g,1624
16
17
  ctxgraph_code/graph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- ctxgraph_code/graph/builder.py,sha256=VreInNG1BuDyCanBzXPe1Q3RQySxDZz563BlmUeFZcw,6759
18
+ ctxgraph_code/graph/builder.py,sha256=XgPfR7ssMaehS3-LwQ4ZGC5MpVJR4N-LPMPfPIyBh8c,11650
18
19
  ctxgraph_code/graph/models.py,sha256=0h3qVDHNUvE8zqBBSaTFlY_mBS1A-WrEW66nPU5oouA,2860
19
20
  ctxgraph_code/graph/query.py,sha256=qwGIdmTZ7-csL_FMkIdABfwpUCQ1ph9TbDeE3eY_YSU,3414
20
- ctxgraph_code/graph/storage.py,sha256=BMhcbybFP_hZv58WQTgR1SBldgFPWTuvk6im70MBMpU,7007
21
+ ctxgraph_code/graph/storage.py,sha256=9qy1OEjLozFZSaxaecrQKHNhyAJxIXYIhWxeagHyWDE,7648
21
22
  ctxgraph_code/view/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
23
  ctxgraph_code/view/visualizer.py,sha256=xvPn9eSDYAk_HICpL6Nt7CQ_5FOKX0EXpZGM6wwyv9s,10555
23
- ctxgraph_code-0.2.0.dist-info/METADATA,sha256=T3zcBQSKD4B7Sc2ZAO_JhnB4_4oIYaaVsNVGsOS_SDw,11129
24
- ctxgraph_code-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
25
- ctxgraph_code-0.2.0.dist-info/entry_points.txt,sha256=Q6poS8LApu1uGl_T9hjoZ7VMC8-iXZFCMslDx6OjlRo,56
26
- ctxgraph_code-0.2.0.dist-info/top_level.txt,sha256=ZOmow7lPtFHkcRJhLFOP5DVbt_SVaFrzsgXsawzMsq8,14
27
- ctxgraph_code-0.2.0.dist-info/RECORD,,
24
+ ctxgraph_code-0.3.0.dist-info/METADATA,sha256=MJ3lilCdCVHMn2aVtjVlkQYIf8okvXegFvE1LX3aRe4,13816
25
+ ctxgraph_code-0.3.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
26
+ ctxgraph_code-0.3.0.dist-info/entry_points.txt,sha256=Q6poS8LApu1uGl_T9hjoZ7VMC8-iXZFCMslDx6OjlRo,56
27
+ ctxgraph_code-0.3.0.dist-info/top_level.txt,sha256=ZOmow7lPtFHkcRJhLFOP5DVbt_SVaFrzsgXsawzMsq8,14
28
+ ctxgraph_code-0.3.0.dist-info/RECORD,,