codegraphcontext 0.4.7__py3-none-any.whl → 0.4.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. codegraphcontext/cli/cli_helpers.py +57 -6
  2. codegraphcontext/cli/config_manager.py +13 -6
  3. codegraphcontext/cli/main.py +101 -10
  4. codegraphcontext/core/__init__.py +3 -3
  5. codegraphcontext/core/database_falkordb.py +2 -2
  6. codegraphcontext/core/database_kuzu.py +101 -14
  7. codegraphcontext/server.py +45 -0
  8. codegraphcontext/tools/code_finder.py +204 -42
  9. codegraphcontext/tools/graph_builder.py +65 -12
  10. codegraphcontext/tools/handlers/analysis_handlers.py +5 -4
  11. codegraphcontext/tools/indexing/discovery.py +26 -2
  12. codegraphcontext/tools/indexing/persistence/writer.py +314 -214
  13. codegraphcontext/tools/indexing/pipeline.py +17 -4
  14. codegraphcontext/tools/indexing/resolution/calls.py +2291 -118
  15. codegraphcontext/tools/indexing/resolution/inheritance.py +34 -16
  16. codegraphcontext/tools/indexing/schema.py +75 -46
  17. codegraphcontext/tools/indexing/scip_pipeline.py +69 -5
  18. codegraphcontext/tools/languages/c.py +86 -4
  19. codegraphcontext/tools/languages/cpp.py +109 -53
  20. codegraphcontext/tools/languages/csharp.py +23 -4
  21. codegraphcontext/tools/languages/css.py +2 -3
  22. codegraphcontext/tools/languages/dart.py +138 -80
  23. codegraphcontext/tools/languages/elixir.py +54 -9
  24. codegraphcontext/tools/languages/go.py +63 -2
  25. codegraphcontext/tools/languages/html.py +8 -8
  26. codegraphcontext/tools/languages/java.py +178 -19
  27. codegraphcontext/tools/languages/javascript.py +1 -0
  28. codegraphcontext/tools/languages/kotlin.py +1694 -141
  29. codegraphcontext/tools/languages/perl.py +26 -4
  30. codegraphcontext/tools/languages/php.py +24 -14
  31. codegraphcontext/tools/languages/python.py +7 -1
  32. codegraphcontext/tools/languages/ruby.py +20 -1
  33. codegraphcontext/tools/languages/rust.py +58 -39
  34. codegraphcontext/tools/languages/scala.py +19 -7
  35. codegraphcontext/tools/languages/swift.py +57 -53
  36. codegraphcontext/tools/scip_indexer.py +506 -353
  37. codegraphcontext/tools/type_utils.py +12 -0
  38. codegraphcontext/utils/tree_sitter_manager.py +86 -35
  39. {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.9.dist-info}/METADATA +49 -87
  40. {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.9.dist-info}/RECORD +44 -43
  41. {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.9.dist-info}/WHEEL +0 -0
  42. {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.9.dist-info}/entry_points.txt +0 -0
  43. {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.9.dist-info}/licenses/LICENSE +0 -0
  44. {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.9.dist-info}/top_level.txt +0 -0
@@ -2,10 +2,12 @@ import asyncio
2
2
  import json
3
3
  import uuid
4
4
  import urllib.parse
5
+ from collections import Counter
5
6
  from pathlib import Path
6
7
  import time
7
8
  import os
8
9
  from typing import Optional, List, Dict, Any
10
+ import typer
9
11
  from rich.console import Console
10
12
  from rich.table import Table
11
13
  from rich.progress import (
@@ -31,6 +33,31 @@ from .config_manager import resolve_context, ResolvedContext, register_repo_in_c
31
33
  console = Console()
32
34
 
33
35
 
36
+ def _print_call_resolution_diagnostics(graph_builder: GraphBuilder, limit: int = 5) -> None:
37
+ diagnostics = getattr(graph_builder, "last_call_resolution_diagnostics", [])
38
+ if not diagnostics:
39
+ return
40
+
41
+ reason_counts = Counter(d.get("reason", "unknown") for d in diagnostics)
42
+ summary = ", ".join(
43
+ f"{reason}={count}" for reason, count in reason_counts.most_common()
44
+ )
45
+ console.print(
46
+ f"[yellow]Skipped {len(diagnostics)} unresolved call relationship(s): {summary}[/yellow]"
47
+ )
48
+ table = Table(show_header=True, header_style="bold magenta")
49
+ table.add_column("Call", style="cyan", overflow="fold")
50
+ table.add_column("Reason", style="yellow")
51
+ table.add_column("Location", style="dim", overflow="fold")
52
+ for diagnostic in diagnostics[:limit]:
53
+ table.add_row(
54
+ str(diagnostic.get("full_call_name") or ""),
55
+ str(diagnostic.get("reason") or ""),
56
+ f"{diagnostic.get('caller_file_path')}:{diagnostic.get('line_number')}",
57
+ )
58
+ console.print(table)
59
+
60
+
34
61
  def _initialize_services(cli_context_flag: Optional[str] = None) -> tuple[Any, Any, Any, ResolvedContext]:
35
62
  """
36
63
  Initializes and returns core service managers based on the resolved context.
@@ -60,8 +87,9 @@ def _initialize_services(cli_context_flag: Optional[str] = None) -> tuple[Any, A
60
87
  ):
61
88
  os.environ["DEFAULT_DATABASE"] = ctx.database
62
89
 
63
- # Pass the exact DB path resolved from the context
64
- db_manager = get_database_manager(db_path=ctx.db_path)
90
+ # Pass the exact DB path resolved from the context, or the runtime override
91
+ runtime_path = os.getenv("CGC_RUNTIME_DB_PATH")
92
+ db_manager = get_database_manager(db_path=runtime_path or ctx.db_path)
65
93
  except ValueError as e:
66
94
  console.print(f"[bold red]Database Configuration Error:[/bold red] {e}")
67
95
  return None, None, None, ctx
@@ -243,6 +271,7 @@ def index_helper(path: str, context: Optional[str] = None):
243
271
  asyncio.run(_run_index_with_progress(graph_builder, path_obj, is_dependency=False, cgcignore_path=ctx.cgcignore_path))
244
272
  time_end = time.time()
245
273
  elapsed = time_end - time_start
274
+ _print_call_resolution_diagnostics(graph_builder)
246
275
  console.print(f"[green]Successfully finished indexing: {path} in {elapsed:.2f} seconds[/green]")
247
276
 
248
277
  # Check if auto-watch is enabled
@@ -259,6 +288,7 @@ def index_helper(path: str, context: Optional[str] = None):
259
288
 
260
289
  except Exception as e:
261
290
  console.print(f"[bold red]An error occurred during indexing:[/bold red] {e}")
291
+ raise typer.Exit(code=1)
262
292
  finally:
263
293
  db_manager.close_driver()
264
294
 
@@ -289,6 +319,7 @@ def add_package_helper(package_name: str, language: str, context: Optional[str]
289
319
 
290
320
  try:
291
321
  asyncio.run(_run_index_with_progress(graph_builder, package_path, is_dependency=True, cgcignore_path=ctx.cgcignore_path))
322
+ _print_call_resolution_diagnostics(graph_builder)
292
323
  console.print(f"[green]Successfully finished indexing package: {package_name}[/green]")
293
324
  except Exception as e:
294
325
  console.print(f"[bold red]An error occurred during package indexing:[/bold red] {e}")
@@ -354,9 +385,11 @@ def cypher_helper(query: str, context: Optional[str] = None):
354
385
 
355
386
  db_manager, _, _, ctx = services
356
387
 
357
- # Replicating safety checks from MCPServer
388
+ # Replicating safety checks from MCPServer (using word boundaries to avoid false positives like 'createEmail')
389
+ import re
358
390
  forbidden_keywords = ['CREATE', 'MERGE', 'DELETE', 'SET', 'REMOVE', 'DROP', 'CALL apoc']
359
- if any(keyword in query.upper() for keyword in forbidden_keywords):
391
+ pattern = r'\b(' + '|'.join(forbidden_keywords) + r')\b'
392
+ if re.search(pattern, query, re.IGNORECASE):
360
393
  console.print("[bold red]Error: This command only supports read-only queries.[/bold red]")
361
394
  db_manager.close_driver()
362
395
  return
@@ -382,9 +415,11 @@ def cypher_helper_visual(query: str, context: Optional[str] = None):
382
415
 
383
416
  db_manager, _, _, ctx = services
384
417
 
385
- # Replicating safety checks from MCPServer
418
+ # Replicating safety checks from MCPServer (using word boundaries to avoid false positives like 'createEmail')
419
+ import re
386
420
  forbidden_keywords = ['CREATE', 'MERGE', 'DELETE', 'SET', 'REMOVE', 'DROP', 'CALL apoc']
387
- if any(keyword in query.upper() for keyword in forbidden_keywords):
421
+ pattern = r'\b(' + '|'.join(forbidden_keywords) + r')\b'
422
+ if re.search(pattern, query, re.IGNORECASE):
388
423
  console.print("[bold red]Error: This command only supports read-only queries.[/bold red]")
389
424
  db_manager.close_driver()
390
425
  return
@@ -540,9 +575,11 @@ def reindex_helper(path: str, context: Optional[str] = None):
540
575
  asyncio.run(_run_index_with_progress(graph_builder, path_obj, is_dependency=False, cgcignore_path=ctx.cgcignore_path))
541
576
  time_end = time.time()
542
577
  elapsed = time_end - time_start
578
+ _print_call_resolution_diagnostics(graph_builder)
543
579
  console.print(f"[green]Successfully re-indexed: {path} in {elapsed:.2f} seconds[/green]")
544
580
  except Exception as e:
545
581
  console.print(f"[bold red]An error occurred during re-indexing:[/bold red] {e}")
582
+ raise typer.Exit(code=1)
546
583
  finally:
547
584
  db_manager.close_driver()
548
585
 
@@ -669,6 +706,12 @@ def stats_helper(path: str = None, context: Optional[str] = None):
669
706
  class_count = session.run("MATCH (c:Class) RETURN count(c) as c").single()["c"]
670
707
  module_count = session.run("MATCH (m:Module) RETURN count(m) as c").single()["c"]
671
708
 
709
+ # Extended node types (PHP, Rust, Go, etc.)
710
+ interface_count = session.run("MATCH (i:Interface) RETURN count(i) as c").single()["c"]
711
+ trait_count = session.run("MATCH (t:Trait) RETURN count(t) as c").single()["c"]
712
+ struct_count = session.run("MATCH (s:Struct) RETURN count(s) as c").single()["c"]
713
+ enum_count = session.run("MATCH (e:Enum) RETURN count(e) as c").single()["c"]
714
+
672
715
  table = Table(show_header=True, header_style="bold magenta")
673
716
  table.add_column("Metric", style="cyan")
674
717
  table.add_column("Count", style="green", justify="right")
@@ -677,6 +720,14 @@ def stats_helper(path: str = None, context: Optional[str] = None):
677
720
  table.add_row("Files", str(file_count))
678
721
  table.add_row("Functions", str(func_count))
679
722
  table.add_row("Classes", str(class_count))
723
+ if interface_count > 0:
724
+ table.add_row("Interfaces", str(interface_count))
725
+ if trait_count > 0:
726
+ table.add_row("Traits", str(trait_count))
727
+ if struct_count > 0:
728
+ table.add_row("Structs", str(struct_count))
729
+ if enum_count > 0:
730
+ table.add_row("Enums", str(enum_count))
680
731
  table.add_row("Modules", str(module_count))
681
732
 
682
733
  console.print(table)
@@ -48,7 +48,7 @@ DEFAULT_CONFIG = {
48
48
  "INDEX_SOURCE": "true",
49
49
  # SCIP indexer feature flag (default off — existing Tree-sitter behaviour unchanged)
50
50
  "SCIP_INDEXER": "false",
51
- "SCIP_LANGUAGES": "python,typescript,go,rust,java",
51
+ "SCIP_LANGUAGES": "python,typescript,javascript,go,rust,java,dart,cpp,c,csharp",
52
52
  "SKIP_EXTERNAL_RESOLUTION": "false",
53
53
  # 0 = unlimited; any positive integer caps MCP tool response size.
54
54
  "MAX_TOOL_RESPONSE_TOKENS": "0",
@@ -85,7 +85,7 @@ CONFIG_DESCRIPTIONS = {
85
85
  "IGNORE_DIRS": "Comma-separated list of directory names to ignore during indexing",
86
86
  "INDEX_SOURCE": "Store full source code in graph database (for faster indexing use false, for better performance use true)",
87
87
  "SCIP_INDEXER": "Use SCIP-based indexing for higher accuracy call/inheritance resolution (requires scip-<lang> tools installed)",
88
- "SCIP_LANGUAGES": "Comma-separated languages to index via SCIP when SCIP_INDEXER=true (python,typescript,go,rust,java)",
88
+ "SCIP_LANGUAGES": "Comma-separated languages to index via SCIP when SCIP_INDEXER=true (python,typescript,javascript,go,rust,java,dart,cpp,c,csharp)",
89
89
  "SKIP_EXTERNAL_RESOLUTION": "Skip resolution attempts for external library method calls (recommended for enterprise large Java/Spring codebases)",
90
90
  "MAX_TOOL_RESPONSE_TOKENS": "Maximum tokens per MCP tool response (0 = unlimited). Truncates oversized payloads and appends a notice.",
91
91
  "TOOL_RESULT_LIMITS": "JSON object mapping tool names to max result counts, e.g. {\"find_code\": 20, \"analyze_code_relationships\": 10}. Missing keys use built-in defaults.",
@@ -634,10 +634,17 @@ def _default_global_db_path(database: str) -> str:
634
634
  """Return the canonical DB path for the global context.
635
635
 
636
636
  New layout: ``~/.codegraphcontext/global/db/<backend>/``
637
- For backward-compat, if the legacy flat path exists we keep using it.
637
+ For backward-compat, we check:
638
+ 1. FALKORDB_PATH in config (if database is falkordb)
639
+ 2. Legacy flat path
640
+ 3. New layout default
638
641
  """
639
- if database == "falkordb" and _LEGACY_FALKORDB_PATH.exists():
640
- return str(_LEGACY_FALKORDB_PATH)
642
+ if database == "falkordb":
643
+ custom_path = load_config().get("FALKORDB_PATH")
644
+ if custom_path:
645
+ return str(Path(custom_path).resolve())
646
+ if _LEGACY_FALKORDB_PATH.exists():
647
+ return str(_LEGACY_FALKORDB_PATH)
641
648
  return str(CONFIG_DIR / "global" / "db" / database)
642
649
 
643
650
 
@@ -859,7 +866,7 @@ def resolve_context(
859
866
  )
860
867
 
861
868
  # --- 4. Global fallback ---
862
- db = load_config().get("DEFAULT_DATABASE", "falkordb")
869
+ db = os.getenv("CGC_RUNTIME_DB_TYPE") or load_config().get("DEFAULT_DATABASE", "falkordb")
863
870
  return ResolvedContext(
864
871
  mode="global",
865
872
  context_name="",
@@ -300,7 +300,7 @@ def _load_credentials():
300
300
  Step 2 skips duplicate loading when that file is the same path as the global file.
301
301
  Arbitrary repo-root `.env` files are not loaded—only CodeGraphContext config paths.
302
302
  """
303
- from dotenv import dotenv_values
303
+ from dotenv import dotenv_values, find_dotenv
304
304
  from codegraphcontext.cli.config_manager import (
305
305
  ensure_config_dir,
306
306
  codegraphcontext_dotenv_at_cwd,
@@ -586,7 +586,7 @@ def bundle_export(
586
586
  services = _initialize_services(context)
587
587
  if not all(services[:3]):
588
588
  return
589
- db_manager, graph_builder, code_finder = services[:3]
589
+ db_manager, _, code_finder = services[:3]
590
590
 
591
591
  try:
592
592
  output_path = Path(output)
@@ -753,9 +753,19 @@ def load_shortcut(
753
753
  # REGISTRY COMMAND GROUP - Browse and Download Bundles
754
754
  # ============================================================================
755
755
 
756
- registry_app = typer.Typer(help="Browse and download bundles from the registry")
756
+ registry_app = typer.Typer(
757
+ help="Browse and download bundles from the registry",
758
+ invoke_without_command=True,
759
+ )
757
760
  app.add_typer(registry_app, name="registry")
758
761
 
762
+
763
+ @registry_app.callback()
764
+ def registry_callback(ctx: typer.Context):
765
+ """Browse and download bundles from the registry."""
766
+ if ctx.invoked_subcommand is None:
767
+ typer.echo(ctx.get_help())
768
+
759
769
  @registry_app.command("list")
760
770
  def registry_list(
761
771
  verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed information including download URLs"),
@@ -940,11 +950,11 @@ def doctor():
940
950
  elif default_db == "kuzudb":
941
951
  from importlib.util import find_spec
942
952
 
943
- if find_spec("real_ladybug") is not None:
953
+ if find_spec("kuzu") is not None:
944
954
  console.print(f" [green]✓[/green] KuzuDB is installed")
945
955
  else:
946
956
  console.print(f" [red]✗[/red] KuzuDB is not installed")
947
- console.print(f" Run: pip install real_ladybug")
957
+ console.print(f" Run: pip install kuzu")
948
958
  all_checks_passed = False
949
959
  else:
950
960
  # FalkorDB
@@ -1199,7 +1209,7 @@ def report(
1199
1209
  """
1200
1210
  _load_credentials()
1201
1211
  output_path = Path(output) if output else Path.cwd() / "CGC_REPORT.md"
1202
- db_manager, _, _ = _initialize_services(context)
1212
+ db_manager, _, _, _ = _initialize_services(context)
1203
1213
  try:
1204
1214
  from codegraphcontext.tools.report_generator import generate_report
1205
1215
  report_text = generate_report(db_manager, output_path=output_path, include_java=java)
@@ -1374,6 +1384,18 @@ def find_by_name(
1374
1384
  results.extend(variables)
1375
1385
  results.extend(modules)
1376
1386
  results.extend(imports)
1387
+
1388
+ # Also search Interface, Trait, Struct, Enum nodes (PHP, Rust, Go, etc.)
1389
+ with db_manager.get_driver().session() as session:
1390
+ for label in ['Interface', 'Trait', 'Struct', 'Enum']:
1391
+ res = session.run(
1392
+ f"MATCH (n:{label}) WHERE n.name = $name RETURN n.name as name, n.path as path, n.line_number as line_number",
1393
+ name=name
1394
+ )
1395
+ for record in res:
1396
+ row = dict(record)
1397
+ row['type'] = label
1398
+ results.append(row)
1377
1399
 
1378
1400
  elif type.lower() == 'function':
1379
1401
  results = code_finder.find_by_function_name(name, fuzzy_search=False)
@@ -1460,7 +1482,7 @@ def find_by_pattern(
1460
1482
  if not case_sensitive:
1461
1483
  query = """
1462
1484
  MATCH (n)
1463
- WHERE (n:Function OR n:Class OR n:Module OR n:Variable) AND toLower(n.name) CONTAINS toLower($pattern)
1485
+ WHERE (n:Function OR n:Class OR n:Module OR n:Variable OR n:Interface OR n:Trait OR n:Struct OR n:Enum) AND toLower(n.name) CONTAINS toLower($pattern)
1464
1486
  RETURN
1465
1487
  labels(n)[0] as type,
1466
1488
  n.name as name,
@@ -1473,7 +1495,7 @@ def find_by_pattern(
1473
1495
  else:
1474
1496
  query = """
1475
1497
  MATCH (n)
1476
- WHERE (n:Function OR n:Class OR n:Module OR n:Variable) AND n.name CONTAINS $pattern
1498
+ WHERE (n:Function OR n:Class OR n:Module OR n:Variable OR n:Interface OR n:Trait OR n:Struct OR n:Enum) AND n.name CONTAINS $pattern
1477
1499
  RETURN
1478
1500
  labels(n)[0] as type,
1479
1501
  n.name as name,
@@ -1978,6 +2000,65 @@ def analyze_chain(
1978
2000
  finally:
1979
2001
  db_manager.close_driver()
1980
2002
 
2003
+ @analyze_app.command("kotlin-call-audit")
2004
+ def analyze_kotlin_call_audit(
2005
+ repo_path: Optional[str] = typer.Option(None, "--repo-path", "-r", help="Limit audit to paths under this repository root"),
2006
+ limit: int = typer.Option(20, "--limit", "-n", help="Maximum examples/top names to show"),
2007
+ json_output: bool = typer.Option(False, "--json", help="Print machine-readable JSON"),
2008
+ fail_on_ambiguity: bool = typer.Option(False, "--fail-on-ambiguity", help="Exit non-zero if any ambiguous Kotlin call groups are found"),
2009
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
2010
+ ):
2011
+ """
2012
+ Audit Kotlin function call edges for multi-target callsite ambiguity.
2013
+
2014
+ Example:
2015
+ cgc analyze kotlin-call-audit --context elrond-stable --fail-on-ambiguity
2016
+ cgc analyze kotlin-call-audit --json
2017
+ """
2018
+ _load_credentials()
2019
+ services = _initialize_services(context)
2020
+ if not all(services[:3]):
2021
+ return
2022
+ db_manager, _, code_finder = services[:3]
2023
+
2024
+ try:
2025
+ result = code_finder.audit_kotlin_call_ambiguity(repo_path=repo_path, limit=limit)
2026
+ if json_output:
2027
+ console.print_json(json.dumps(result))
2028
+ else:
2029
+ console.print("\n[bold cyan]Kotlin CALLS ambiguity audit[/bold cyan]")
2030
+ summary = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
2031
+ summary.add_column("Metric", style="cyan")
2032
+ summary.add_column("Value", style="green")
2033
+ summary.add_row("Kotlin fn→fn CALLS edges", str(result["kotlin_fn_to_fn_edges"]))
2034
+ summary.add_row("Ambiguous groups", str(result["ambiguous_groups"]))
2035
+ summary.add_row("Ambiguous edges", str(result["ambiguous_edges"]))
2036
+ console.print(summary)
2037
+
2038
+ if result["examples"]:
2039
+ examples = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
2040
+ examples.add_column("Callsite", style="cyan", overflow="fold")
2041
+ examples.add_column("Call", style="yellow", overflow="fold")
2042
+ examples.add_column("Targets", style="green", overflow="fold")
2043
+ for example in result["examples"]:
2044
+ targets = "\n".join(
2045
+ f"{target['context'] or ''}:{target['line_number']} {target['path']}"
2046
+ for target in example["targets"]
2047
+ )
2048
+ examples.add_row(
2049
+ f"{example.get('caller_name')} {example.get('caller_path')}:{example.get('call_line')}",
2050
+ str(example.get("full_call_name") or ""),
2051
+ targets,
2052
+ )
2053
+ console.print(examples)
2054
+ else:
2055
+ console.print("[green]No ambiguous Kotlin call groups found.[/green]")
2056
+
2057
+ if fail_on_ambiguity and result["ambiguous_groups"]:
2058
+ raise typer.Exit(1)
2059
+ finally:
2060
+ db_manager.close_driver()
2061
+
1981
2062
  @analyze_app.command("deps")
1982
2063
  def analyze_dependencies(
1983
2064
  ctx: typer.Context,
@@ -2471,12 +2552,22 @@ def main(
2471
2552
  "-h",
2472
2553
  help="[Root-level only] Show help and exit",
2473
2554
  is_eager=True,
2474
- ),
2555
+ ),
2556
+ db_path: Optional[str] = typer.Option(
2557
+ None,
2558
+ "--path",
2559
+ "--db-path",
2560
+ help="[Global] Temporarily override database path (for local DBs like KuzuDB)"
2561
+ ),
2475
2562
  ):
2476
2563
  """
2477
2564
  Main entry point for the cgc CLI application.
2478
2565
  If no subcommand is provided, it displays a welcome message with instructions.
2479
2566
  """
2567
+ if db_path:
2568
+ os.environ["CGC_RUNTIME_DB_PATH"] = db_path
2569
+ if database:
2570
+ os.environ["CGC_RUNTIME_DB_TYPE"] = database
2480
2571
  # Initialize context object for sharing state with subcommands
2481
2572
  ctx.ensure_object(dict)
2482
2573
 
@@ -2654,4 +2745,4 @@ def _write_datasource_graph(ingested: dict) -> None:
2654
2745
 
2655
2746
 
2656
2747
  if __name__ == "__main__":
2657
- app()
2748
+ app()
@@ -22,7 +22,7 @@ import importlib.util
22
22
  def _is_kuzudb_available() -> bool:
23
23
  """Check if KùzuDB is installed."""
24
24
  try:
25
- return importlib.util.find_spec("real_ladybug") is not None
25
+ return importlib.util.find_spec("kuzu") is not None
26
26
  except ImportError:
27
27
  return False
28
28
 
@@ -78,7 +78,7 @@ def get_database_manager(db_path: Optional[str] = None) -> Union['DatabaseManage
78
78
  db_type = db_type.lower()
79
79
  if db_type == 'kuzudb':
80
80
  if not _is_kuzudb_available():
81
- raise ValueError("Database set to 'kuzudb' but Kùzu is not installed.\nRun 'pip install real_ladybug'")
81
+ raise ValueError("Database set to 'kuzudb' but Kùzu is not installed.\nRun 'pip install kuzu'")
82
82
  from .database_kuzu import KuzuDBManager
83
83
  info_logger(f"Using KùzuDB (explicit) at {db_path or 'default path'}")
84
84
  return KuzuDBManager(db_path=db_path)
@@ -168,7 +168,7 @@ def get_database_manager(db_path: Optional[str] = None) -> Union['DatabaseManage
168
168
  return NornicDBManager()
169
169
 
170
170
  error_msg = "No database backend available.\n"
171
- error_msg += "Recommended: Install KùzuDB for zero-config ('pip install real_ladybug')\n"
171
+ error_msg += "Recommended: Install KùzuDB for zero-config ('pip install kuzu')\n"
172
172
 
173
173
  if platform.system() != "Windows":
174
174
  error_msg += "Alternative: Install FalkorDB Lite ('pip install falkordblite')\n"
@@ -335,7 +335,7 @@ class FalkorDBDriverWrapper:
335
335
  def __init__(self, graph):
336
336
  self.graph = graph
337
337
 
338
- def session(self):
338
+ def session(self, **kwargs):
339
339
  """Returns a session-like object for FalkorDB."""
340
340
  return FalkorDBSessionWrapper(self.graph)
341
341
 
@@ -377,7 +377,7 @@ class FalkorDBSessionWrapper:
377
377
  except Exception as e:
378
378
  # Ignore errors about existing constraints/indexes
379
379
  error_msg = str(e).lower()
380
- if "already exists" in error_msg or "already created" in error_msg:
380
+ if "already exists" in error_msg or "already created" in error_msg or "already indexed" in error_msg:
381
381
  return FalkorDBResultWrapper(None)
382
382
 
383
383
  error_logger(f"FalkorDB query failed: {query[:100]}... Error: {e}")
@@ -81,7 +81,7 @@ class KuzuDBManager:
81
81
  info_logger("KùzuDB connection established and schema verified")
82
82
  break
83
83
  except ImportError:
84
- error_logger("KùzuDB is not installed. Run 'pip install real_ladybug'")
84
+ error_logger("KùzuDB is not installed. Run 'pip install kuzu'")
85
85
  raise ValueError("KùzuDB missing.")
86
86
  except Exception as e:
87
87
  if "lock" in str(e).lower() and attempt < max_retries - 1:
@@ -106,10 +106,10 @@ class KuzuDBManager:
106
106
  ("Repository", "path STRING, name STRING, is_dependency BOOLEAN, indexed_at STRING, commit_hash STRING, PRIMARY KEY (path)"),
107
107
  ("File", "path STRING, name STRING, relative_path STRING, package_name STRING, is_dependency BOOLEAN, PRIMARY KEY (path)"),
108
108
  ("Directory", "path STRING, name STRING, PRIMARY KEY (path)"),
109
- ("Module", "name STRING, lang STRING, full_import_name STRING, PRIMARY KEY (name)"),
109
+ ("Module", "name STRING, lang STRING, full_import_name STRING, path STRING, line_number INT64, PRIMARY KEY (name)"),
110
110
  # For types with composite keys (name, path, line_number), we use a 'uid'
111
- ("Function", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, cyclomatic_complexity INT64, context STRING, context_type STRING, class_context STRING, is_dependency BOOLEAN, decorators STRING[], args STRING[], http_method STRING, http_path STRING, PRIMARY KEY (uid)"),
112
- ("Class", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, decorators STRING[], PRIMARY KEY (uid)"),
111
+ ("Function", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, cyclomatic_complexity INT64, context STRING, context_type STRING, class_context STRING, class_context_line INT64, is_dependency BOOLEAN, decorators STRING[], args STRING[], http_method STRING, http_path STRING, PRIMARY KEY (uid)"),
112
+ ("Class", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, node_type STRING, is_dependency BOOLEAN, decorators STRING[], PRIMARY KEY (uid)"),
113
113
  ("Variable", "uid STRING, name STRING, path STRING, line_number INT64, source STRING, docstring STRING, lang STRING, value STRING, context STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
114
114
  ("Trait", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
115
115
  ("Interface", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
@@ -120,7 +120,12 @@ class KuzuDBManager:
120
120
  ("Annotation", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
121
121
  ("Record", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
122
122
  ("Property", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
123
- ("Parameter", "uid STRING, name STRING, path STRING, function_line_number INT64, PRIMARY KEY (uid)")
123
+ ("Parameter", "uid STRING, name STRING, path STRING, function_line_number INT64, PRIMARY KEY (uid)"),
124
+ ("Mixin", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
125
+ ("Extension", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
126
+ ("Object", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
127
+ ("DbTable", "name STRING, fqn STRING, datasource_name STRING, path STRING, PRIMARY KEY (name)"),
128
+ ("ExternalClass", "name STRING, path STRING, PRIMARY KEY (name)")
124
129
  ]
125
130
 
126
131
  # rel_tables: list of (table_name, schema, use_group)
@@ -131,14 +136,57 @@ class KuzuDBManager:
131
136
  # keywords in CREATE REL TABLE statements. We must escape them with backticks
132
137
  # or the rel table creation will fail silently, leading to runtime
133
138
  # "Binder exception: Table CONTAINS does not exist".
134
- ("CONTAINS", "FROM File TO Function, FROM File TO Class, FROM File TO Variable, FROM File TO Trait, FROM File TO Interface, FROM `Macro` TO `Macro`, FROM File TO `Macro`, FROM File TO Struct, FROM File TO Enum, FROM File TO `Union`, FROM File TO Annotation, FROM File TO Record, FROM File TO `Property`, FROM Repository TO Directory, FROM Directory TO Directory, FROM Directory TO File, FROM Repository TO File, FROM Class TO Function, FROM Function TO Function", True),
135
- ("CALLS", "FROM Function TO Function, FROM Function TO Class, FROM File TO Function, FROM File TO Class, FROM Class TO Function, FROM Class TO Class, line_number INT64, args STRING[], full_call_name STRING, confidence DOUBLE, resolution_tier INT64, confidence_label STRING, source STRING, resolution_method STRING, called_name STRING", True),
139
+ ("CONTAINS", """
140
+ FROM File TO Function, FROM File TO Class, FROM File TO Variable, FROM File TO Trait, FROM File TO Interface,
141
+ FROM File TO `Macro`, FROM File TO Struct, FROM File TO Enum, FROM File TO `Union`, FROM File TO Annotation,
142
+ FROM File TO Record, FROM File TO `Property`, FROM File TO Mixin, FROM File TO Extension, FROM File TO Module,
143
+ FROM File TO Object,
144
+ FROM Repository TO Directory, FROM Directory TO Directory, FROM Directory TO File, FROM Repository TO File,
145
+ FROM Class TO Function, FROM Module TO Function, FROM Interface TO Function, FROM Struct TO Function,
146
+ FROM Record TO Function, FROM Trait TO Function, FROM Object TO Function, FROM Mixin TO Function,
147
+ FROM Extension TO Function, FROM Class TO Class, FROM Class TO Interface, FROM Class TO Struct,
148
+ FROM Class TO Variable, FROM Module TO Class, FROM Module TO Module, FROM `Macro` TO `Macro`, FROM Function TO Function
149
+ """, True),
150
+ ("CALLS", """
151
+ FROM Function TO Function, FROM Function TO Class, FROM Function TO Interface, FROM Function TO Trait,
152
+ FROM Function TO Struct, FROM Function TO Enum, FROM Function TO Record, FROM Function TO `Union`,
153
+ FROM Function TO Mixin, FROM Function TO Extension, FROM Function TO Object,
154
+ FROM Class TO Function, FROM Class TO Class, FROM Class TO Interface, FROM Class TO Trait,
155
+ FROM Class TO Struct, FROM Class TO Enum, FROM Class TO Record, FROM Class TO `Union`,
156
+ FROM Interface TO Function, FROM Interface TO Class, FROM Interface TO Interface,
157
+ FROM Trait TO Function, FROM Trait TO Class, FROM Trait TO Interface,
158
+ FROM Mixin TO Function, FROM Mixin TO Class, FROM Mixin TO Interface,
159
+ FROM Extension TO Function, FROM Extension TO Class, FROM Extension TO Interface,
160
+ FROM Object TO Function, FROM Object TO Class, FROM Object TO Interface,
161
+ FROM `Union` TO Function, FROM `Union` TO Class, FROM `Union` TO Interface,
162
+ FROM `Macro` TO Function, FROM `Macro` TO Class, FROM `Macro` TO Interface,
163
+ FROM File TO Function, FROM File TO Class, FROM File TO Interface, FROM File TO Trait,
164
+ FROM File TO Struct, FROM File TO Enum, FROM File TO Record, FROM File TO `Union`,
165
+ FROM Variable TO Function, FROM Variable TO Class, FROM Variable TO Interface,
166
+ line_number INT64, args STRING[], full_call_name STRING, confidence DOUBLE, resolution_tier INT64,
167
+ confidence_label STRING, source STRING, resolution_method STRING, called_name STRING
168
+ """, True),
136
169
  ("IMPORTS", "FROM File TO Module, alias STRING, full_import_name STRING, imported_name STRING, line_number INT64", False),
137
- ("INHERITS", "FROM Class TO Class, FROM Record TO Record, FROM Interface TO Interface, confidence_label STRING", True),
170
+ ("INHERITS", """
171
+ FROM Class TO Class, FROM Class TO Interface, FROM Class TO Trait, FROM Class TO Mixin, FROM Class TO Extension, FROM Class TO ExternalClass, FROM Class TO Struct, FROM Class TO Enum, FROM Class TO `Union`, FROM Class TO Record, FROM Class TO Object,
172
+ FROM Trait TO Trait, FROM Trait TO Interface, FROM Trait TO ExternalClass, FROM Trait TO Class,
173
+ FROM Interface TO Interface, FROM Interface TO Trait, FROM Interface TO ExternalClass, FROM Interface TO Class,
174
+ FROM Struct TO Interface, FROM Struct TO Trait, FROM Struct TO ExternalClass, FROM Struct TO Struct, FROM Struct TO Class,
175
+ FROM Record TO Record, FROM Record TO Interface, FROM Record TO ExternalClass, FROM Record TO Class, FROM Record TO Struct,
176
+ FROM Mixin TO Mixin, FROM Mixin TO Interface, FROM Mixin TO ExternalClass, FROM Mixin TO Class,
177
+ FROM Extension TO Extension, FROM Extension TO Interface, FROM Extension TO ExternalClass, FROM Extension TO Class,
178
+ FROM Enum TO Class, FROM Enum TO Interface, FROM Enum TO ExternalClass, FROM Enum TO Enum,
179
+ FROM `Union` TO Class, FROM `Union` TO Interface, FROM `Union` TO ExternalClass, FROM `Union` TO `Union`,
180
+ FROM Module TO Module, FROM Module TO ExternalClass, FROM Object TO Object, FROM Object TO ExternalClass, FROM Object TO Class,
181
+ confidence_label STRING
182
+ """, True),
138
183
  ("HAS_PARAMETER", "FROM Function TO Parameter", False),
139
184
  ("INCLUDES", "FROM Class TO Module", False),
140
- ("IMPLEMENTS", "FROM Class TO Interface, FROM Struct TO Interface, FROM Record TO Interface", True),
141
- ("INJECTS", "FROM Class TO Class, field_name STRING, inject_line INT64, confidence_label STRING", False)
185
+ ("IMPLEMENTS", "FROM Class TO Interface, FROM Struct TO Interface, FROM Record TO Interface, FROM Mixin TO Interface, FROM Extension TO Interface, FROM Enum TO Interface, FROM Object TO Interface, FROM `Union` TO Interface, FROM Trait TO Interface", True),
186
+ ("INJECTS", "FROM Class TO Class, field_name STRING, inject_line INT64, confidence_label STRING", False),
187
+ ("MAPS_TO", "FROM Class TO DbTable, datastore STRING, line_number INT64", False),
188
+ ("READS", "FROM Function TO DbTable, line_number INT64", False),
189
+ ("WRITES", "FROM Function TO DbTable, line_number INT64", False)
142
190
  ]
143
191
 
144
192
  for table_name, schema in node_tables:
@@ -169,14 +217,20 @@ class KuzuDBManager:
169
217
  simple_migrations = [
170
218
  ("File", "package_name", "STRING"),
171
219
  ("Module", "full_import_name", "STRING"),
220
+ ("Module", "path", "STRING"),
221
+ ("Module", "line_number", "INT64"),
222
+ ("DbTable", "path", "STRING"),
223
+ ("ExternalClass", "path", "STRING"),
172
224
  ("IMPORTS", "full_import_name", "STRING"),
173
225
  ("IMPORTS", "imported_name", "STRING"),
174
- # Freshness properties added to Repository in 0.4.7
175
226
  ("Repository", "indexed_at", "STRING"),
176
227
  ("Repository", "commit_hash", "STRING"),
177
228
  # Spring endpoint properties on Function
178
229
  ("Function", "http_method", "STRING"),
179
230
  ("Function", "http_path", "STRING"),
231
+ # Kotlin/JVM precision improvements
232
+ ("Function", "class_context_line", "INT64"),
233
+ ("Class", "node_type", "STRING"),
180
234
  ]
181
235
 
182
236
  # REL TABLE GROUP migrations: KuzuDB creates sub-tables named
@@ -222,6 +276,7 @@ class KuzuDBManager:
222
276
  continue
223
277
  warning_logger(f"Kuzu Schema Migration Error ({table_name}.{column_name}): {e}")
224
278
  debug_log(f"Kuzu Schema Migration Error ({table_name}.{column_name}): {e}")
279
+ raise RuntimeError("Kuzu Schema Migration Failed") from e
225
280
 
226
281
  def close_driver(self):
227
282
  """Closes the connection."""
@@ -259,7 +314,7 @@ class KuzuDBManager:
259
314
  import kuzu
260
315
  return True, None
261
316
  except ImportError:
262
- return False, "KùzuDB is not installed. Run 'pip install real_ladybug'"
317
+ return False, "KùzuDB is not installed. Run 'pip install kuzu'"
263
318
 
264
319
  class KuzuDriverWrapper:
265
320
  def __init__(self, conn, query_lock: Optional[threading.RLock] = None):
@@ -470,8 +525,8 @@ class KuzuSessionWrapper:
470
525
  'File': {'path', 'name', 'relative_path', 'package_name', 'is_dependency'},
471
526
  'Directory': {'path', 'name'},
472
527
  'Module': {'name', 'lang', 'full_import_name'},
473
- 'Function': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'cyclomatic_complexity', 'context', 'context_type', 'class_context', 'is_dependency', 'decorators', 'args', 'http_method', 'http_path'},
474
- 'Class': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency', 'decorators'},
528
+ 'Function': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'cyclomatic_complexity', 'context', 'context_type', 'class_context', 'class_context_line', 'is_dependency', 'decorators', 'args', 'http_method', 'http_path'},
529
+ 'Class': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'node_type', 'is_dependency', 'decorators'},
475
530
  'Variable': {'uid', 'name', 'path', 'line_number', 'source', 'docstring', 'lang', 'value', 'context', 'is_dependency'},
476
531
  'Trait': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency'},
477
532
  'Interface': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency'},
@@ -701,6 +756,38 @@ class KuzuSessionWrapper:
701
756
  for label in labels_to_escape:
702
757
  query = re.sub(rf':{label}\b', f':`{label}`', query)
703
758
 
759
+ # Translate (n:Label1 OR n:Label2 ...) to label(n) IN ['Label1', 'Label2', ...]
760
+ def poly_replacer(match):
761
+ full_match = match.group(0)
762
+ var_name = match.group(1)
763
+ # Find all labels associated with this variable in the OR chain
764
+ labels = re.findall(rf'{var_name}:([a-zA-Z0-9_`]+)', full_match)
765
+ # Strip backticks from labels
766
+ labels = [l.strip('`') for l in labels]
767
+ return f"label({var_name}) IN {json.dumps(labels)}"
768
+
769
+ # Regex to match (n:Label1 OR n:Label2 OR n:Label3)
770
+ query = re.sub(r'\((\w+):[a-zA-Z0-9_`]+(?:\s+OR\s+\1:[a-zA-Z0-9_`]+)+\)', poly_replacer, query)
771
+
772
+ # Translate single WHERE n:Label to label(n) = 'Label'
773
+ # This is more complex because we don't want to match MATCH/MERGE
774
+ # For now, we only target where it appears after WHERE or AND/OR
775
+ def single_label_replacer(match):
776
+ prefix = match.group(1)
777
+ var_name = match.group(2)
778
+ label = match.group(3).strip('`')
779
+ return f"{prefix}label({var_name}) = '{label}'"
780
+
781
+ query = re.sub(r'(WHERE\s+|AND\s+|OR\s+|WHEN\s+)(\w+):([a-zA-Z0-9_`]+)', single_label_replacer, query, flags=re.IGNORECASE)
782
+
783
+ # Handle NOT n:Label → NOT label(n) = 'Label'
784
+ def not_label_replacer(match):
785
+ prefix = match.group(1)
786
+ var_name = match.group(2)
787
+ label_name = match.group(3).strip('`')
788
+ return f"{prefix}NOT label({var_name}) = '{label_name}'"
789
+ query = re.sub(r'(WHERE\s+|AND\s+|OR\s+)NOT\s+(\w+):([a-zA-Z0-9_`]+)', not_label_replacer, query, flags=re.IGNORECASE)
790
+
704
791
  # 4. Polymorphic matches and label access
705
792
  query = query.replace("labels(n)[0]", "label(n)")
706
793