codegraphcontext 0.4.7__py3-none-any.whl → 0.4.8__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 (27) hide show
  1. codegraphcontext/cli/cli_helpers.py +54 -4
  2. codegraphcontext/cli/config_manager.py +3 -3
  3. codegraphcontext/cli/main.py +90 -9
  4. codegraphcontext/core/__init__.py +3 -3
  5. codegraphcontext/core/database_kuzu.py +57 -9
  6. codegraphcontext/tools/code_finder.py +172 -28
  7. codegraphcontext/tools/graph_builder.py +61 -12
  8. codegraphcontext/tools/handlers/analysis_handlers.py +2 -2
  9. codegraphcontext/tools/indexing/discovery.py +26 -2
  10. codegraphcontext/tools/indexing/persistence/writer.py +154 -154
  11. codegraphcontext/tools/indexing/pipeline.py +16 -3
  12. codegraphcontext/tools/indexing/resolution/calls.py +2248 -118
  13. codegraphcontext/tools/indexing/resolution/inheritance.py +26 -15
  14. codegraphcontext/tools/indexing/schema.py +22 -0
  15. codegraphcontext/tools/indexing/scip_pipeline.py +69 -5
  16. codegraphcontext/tools/languages/java.py +143 -8
  17. codegraphcontext/tools/languages/kotlin.py +1653 -135
  18. codegraphcontext/tools/languages/php.py +18 -14
  19. codegraphcontext/tools/scip_indexer.py +506 -353
  20. codegraphcontext/tools/type_utils.py +12 -0
  21. codegraphcontext/utils/tree_sitter_manager.py +86 -35
  22. {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.8.dist-info}/METADATA +49 -87
  23. {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.8.dist-info}/RECORD +27 -26
  24. {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.8.dist-info}/WHEEL +0 -0
  25. {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.8.dist-info}/entry_points.txt +0 -0
  26. {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.8.dist-info}/licenses/LICENSE +0 -0
  27. {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.8.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.
@@ -243,6 +270,7 @@ def index_helper(path: str, context: Optional[str] = None):
243
270
  asyncio.run(_run_index_with_progress(graph_builder, path_obj, is_dependency=False, cgcignore_path=ctx.cgcignore_path))
244
271
  time_end = time.time()
245
272
  elapsed = time_end - time_start
273
+ _print_call_resolution_diagnostics(graph_builder)
246
274
  console.print(f"[green]Successfully finished indexing: {path} in {elapsed:.2f} seconds[/green]")
247
275
 
248
276
  # Check if auto-watch is enabled
@@ -259,6 +287,7 @@ def index_helper(path: str, context: Optional[str] = None):
259
287
 
260
288
  except Exception as e:
261
289
  console.print(f"[bold red]An error occurred during indexing:[/bold red] {e}")
290
+ raise typer.Exit(code=1)
262
291
  finally:
263
292
  db_manager.close_driver()
264
293
 
@@ -289,6 +318,7 @@ def add_package_helper(package_name: str, language: str, context: Optional[str]
289
318
 
290
319
  try:
291
320
  asyncio.run(_run_index_with_progress(graph_builder, package_path, is_dependency=True, cgcignore_path=ctx.cgcignore_path))
321
+ _print_call_resolution_diagnostics(graph_builder)
292
322
  console.print(f"[green]Successfully finished indexing package: {package_name}[/green]")
293
323
  except Exception as e:
294
324
  console.print(f"[bold red]An error occurred during package indexing:[/bold red] {e}")
@@ -354,9 +384,11 @@ def cypher_helper(query: str, context: Optional[str] = None):
354
384
 
355
385
  db_manager, _, _, ctx = services
356
386
 
357
- # Replicating safety checks from MCPServer
387
+ # Replicating safety checks from MCPServer (using word boundaries to avoid false positives like 'createEmail')
388
+ import re
358
389
  forbidden_keywords = ['CREATE', 'MERGE', 'DELETE', 'SET', 'REMOVE', 'DROP', 'CALL apoc']
359
- if any(keyword in query.upper() for keyword in forbidden_keywords):
390
+ pattern = r'\b(' + '|'.join(forbidden_keywords) + r')\b'
391
+ if re.search(pattern, query, re.IGNORECASE):
360
392
  console.print("[bold red]Error: This command only supports read-only queries.[/bold red]")
361
393
  db_manager.close_driver()
362
394
  return
@@ -382,9 +414,11 @@ def cypher_helper_visual(query: str, context: Optional[str] = None):
382
414
 
383
415
  db_manager, _, _, ctx = services
384
416
 
385
- # Replicating safety checks from MCPServer
417
+ # Replicating safety checks from MCPServer (using word boundaries to avoid false positives like 'createEmail')
418
+ import re
386
419
  forbidden_keywords = ['CREATE', 'MERGE', 'DELETE', 'SET', 'REMOVE', 'DROP', 'CALL apoc']
387
- if any(keyword in query.upper() for keyword in forbidden_keywords):
420
+ pattern = r'\b(' + '|'.join(forbidden_keywords) + r')\b'
421
+ if re.search(pattern, query, re.IGNORECASE):
388
422
  console.print("[bold red]Error: This command only supports read-only queries.[/bold red]")
389
423
  db_manager.close_driver()
390
424
  return
@@ -540,9 +574,11 @@ def reindex_helper(path: str, context: Optional[str] = None):
540
574
  asyncio.run(_run_index_with_progress(graph_builder, path_obj, is_dependency=False, cgcignore_path=ctx.cgcignore_path))
541
575
  time_end = time.time()
542
576
  elapsed = time_end - time_start
577
+ _print_call_resolution_diagnostics(graph_builder)
543
578
  console.print(f"[green]Successfully re-indexed: {path} in {elapsed:.2f} seconds[/green]")
544
579
  except Exception as e:
545
580
  console.print(f"[bold red]An error occurred during re-indexing:[/bold red] {e}")
581
+ raise typer.Exit(code=1)
546
582
  finally:
547
583
  db_manager.close_driver()
548
584
 
@@ -669,6 +705,12 @@ def stats_helper(path: str = None, context: Optional[str] = None):
669
705
  class_count = session.run("MATCH (c:Class) RETURN count(c) as c").single()["c"]
670
706
  module_count = session.run("MATCH (m:Module) RETURN count(m) as c").single()["c"]
671
707
 
708
+ # Extended node types (PHP, Rust, Go, etc.)
709
+ interface_count = session.run("MATCH (i:Interface) RETURN count(i) as c").single()["c"]
710
+ trait_count = session.run("MATCH (t:Trait) RETURN count(t) as c").single()["c"]
711
+ struct_count = session.run("MATCH (s:Struct) RETURN count(s) as c").single()["c"]
712
+ enum_count = session.run("MATCH (e:Enum) RETURN count(e) as c").single()["c"]
713
+
672
714
  table = Table(show_header=True, header_style="bold magenta")
673
715
  table.add_column("Metric", style="cyan")
674
716
  table.add_column("Count", style="green", justify="right")
@@ -677,6 +719,14 @@ def stats_helper(path: str = None, context: Optional[str] = None):
677
719
  table.add_row("Files", str(file_count))
678
720
  table.add_row("Functions", str(func_count))
679
721
  table.add_row("Classes", str(class_count))
722
+ if interface_count > 0:
723
+ table.add_row("Interfaces", str(interface_count))
724
+ if trait_count > 0:
725
+ table.add_row("Traits", str(trait_count))
726
+ if struct_count > 0:
727
+ table.add_row("Structs", str(struct_count))
728
+ if enum_count > 0:
729
+ table.add_row("Enums", str(enum_count))
680
730
  table.add_row("Modules", str(module_count))
681
731
 
682
732
  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.",
@@ -859,7 +859,7 @@ def resolve_context(
859
859
  )
860
860
 
861
861
  # --- 4. Global fallback ---
862
- db = load_config().get("DEFAULT_DATABASE", "falkordb")
862
+ db = os.getenv("CGC_RUNTIME_DB_TYPE") or load_config().get("DEFAULT_DATABASE", "falkordb")
863
863
  return ResolvedContext(
864
864
  mode="global",
865
865
  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,
@@ -2654,4 +2735,4 @@ def _write_datasource_graph(ingested: dict) -> None:
2654
2735
 
2655
2736
 
2656
2737
  if __name__ == "__main__":
2657
- app()
2738
+ 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"
@@ -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:
@@ -108,8 +108,8 @@ class KuzuDBManager:
108
108
  ("Directory", "path STRING, name STRING, PRIMARY KEY (path)"),
109
109
  ("Module", "name STRING, lang STRING, full_import_name STRING, 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)"),
@@ -132,9 +132,21 @@ class KuzuDBManager:
132
132
  # or the rel table creation will fail silently, leading to runtime
133
133
  # "Binder exception: Table CONTAINS does not exist".
134
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),
135
+ ("CALLS", """
136
+ FROM Function TO Function, FROM Function TO Class, FROM Function TO Interface, FROM Function TO Trait, FROM Function TO Struct, FROM Function TO Enum, FROM Function TO `Record`, FROM Function TO `Union`,
137
+ FROM Class TO Function, FROM Class TO Class, FROM Class TO Interface, FROM Class TO Trait, FROM Class TO Struct, FROM Class TO Enum, FROM Class TO `Record`, FROM Class TO `Union`,
138
+ FROM Interface TO Function, FROM Interface TO Class, FROM Interface TO Interface,
139
+ FROM File TO Function, FROM File TO Class, FROM File TO Interface, FROM File TO Trait, FROM File TO Struct, FROM File TO Enum, FROM File TO `Record`, FROM File TO `Union`,
140
+ 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
141
+ """, True),
136
142
  ("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),
143
+ ("INHERITS", """
144
+ FROM Class TO Class, FROM Class TO Interface, FROM Class TO Trait,
145
+ FROM `Record` TO `Record`, FROM `Record` TO Interface,
146
+ FROM Interface TO Interface, FROM Interface TO Trait,
147
+ FROM Struct TO Interface, FROM Struct TO Trait,
148
+ confidence_label STRING
149
+ """, True),
138
150
  ("HAS_PARAMETER", "FROM Function TO Parameter", False),
139
151
  ("INCLUDES", "FROM Class TO Module", False),
140
152
  ("IMPLEMENTS", "FROM Class TO Interface, FROM Struct TO Interface, FROM Record TO Interface", True),
@@ -171,12 +183,15 @@ class KuzuDBManager:
171
183
  ("Module", "full_import_name", "STRING"),
172
184
  ("IMPORTS", "full_import_name", "STRING"),
173
185
  ("IMPORTS", "imported_name", "STRING"),
174
- # Freshness properties added to Repository in 0.4.7
186
+ # Freshness properties added to Repository in 0.4.8
175
187
  ("Repository", "indexed_at", "STRING"),
176
188
  ("Repository", "commit_hash", "STRING"),
177
189
  # Spring endpoint properties on Function
178
190
  ("Function", "http_method", "STRING"),
179
191
  ("Function", "http_path", "STRING"),
192
+ # Kotlin/JVM precision improvements
193
+ ("Function", "class_context_line", "INT64"),
194
+ ("Class", "node_type", "STRING"),
180
195
  ]
181
196
 
182
197
  # REL TABLE GROUP migrations: KuzuDB creates sub-tables named
@@ -222,6 +237,7 @@ class KuzuDBManager:
222
237
  continue
223
238
  warning_logger(f"Kuzu Schema Migration Error ({table_name}.{column_name}): {e}")
224
239
  debug_log(f"Kuzu Schema Migration Error ({table_name}.{column_name}): {e}")
240
+ raise RuntimeError("Kuzu Schema Migration Failed") from e
225
241
 
226
242
  def close_driver(self):
227
243
  """Closes the connection."""
@@ -259,7 +275,7 @@ class KuzuDBManager:
259
275
  import kuzu
260
276
  return True, None
261
277
  except ImportError:
262
- return False, "KùzuDB is not installed. Run 'pip install real_ladybug'"
278
+ return False, "KùzuDB is not installed. Run 'pip install kuzu'"
263
279
 
264
280
  class KuzuDriverWrapper:
265
281
  def __init__(self, conn, query_lock: Optional[threading.RLock] = None):
@@ -470,8 +486,8 @@ class KuzuSessionWrapper:
470
486
  'File': {'path', 'name', 'relative_path', 'package_name', 'is_dependency'},
471
487
  'Directory': {'path', 'name'},
472
488
  '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'},
489
+ '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'},
490
+ 'Class': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'node_type', 'is_dependency', 'decorators'},
475
491
  'Variable': {'uid', 'name', 'path', 'line_number', 'source', 'docstring', 'lang', 'value', 'context', 'is_dependency'},
476
492
  'Trait': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency'},
477
493
  'Interface': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency'},
@@ -701,6 +717,38 @@ class KuzuSessionWrapper:
701
717
  for label in labels_to_escape:
702
718
  query = re.sub(rf':{label}\b', f':`{label}`', query)
703
719
 
720
+ # Translate (n:Label1 OR n:Label2 ...) to label(n) IN ['Label1', 'Label2', ...]
721
+ def poly_replacer(match):
722
+ full_match = match.group(0)
723
+ var_name = match.group(1)
724
+ # Find all labels associated with this variable in the OR chain
725
+ labels = re.findall(rf'{var_name}:([a-zA-Z0-9_`]+)', full_match)
726
+ # Strip backticks from labels
727
+ labels = [l.strip('`') for l in labels]
728
+ return f"label({var_name}) IN {json.dumps(labels)}"
729
+
730
+ # Regex to match (n:Label1 OR n:Label2 OR n:Label3)
731
+ query = re.sub(r'\((\w+):[a-zA-Z0-9_`]+(?:\s+OR\s+\1:[a-zA-Z0-9_`]+)+\)', poly_replacer, query)
732
+
733
+ # Translate single WHERE n:Label to label(n) = 'Label'
734
+ # This is more complex because we don't want to match MATCH/MERGE
735
+ # For now, we only target where it appears after WHERE or AND/OR
736
+ def single_label_replacer(match):
737
+ prefix = match.group(1)
738
+ var_name = match.group(2)
739
+ label = match.group(3).strip('`')
740
+ return f"{prefix}label({var_name}) = '{label}'"
741
+
742
+ query = re.sub(r'(WHERE\s+|AND\s+|OR\s+|WHEN\s+)(\w+):([a-zA-Z0-9_`]+)', single_label_replacer, query, flags=re.IGNORECASE)
743
+
744
+ # Handle NOT n:Label → NOT label(n) = 'Label'
745
+ def not_label_replacer(match):
746
+ prefix = match.group(1)
747
+ var_name = match.group(2)
748
+ label_name = match.group(3).strip('`')
749
+ return f"{prefix}NOT label({var_name}) = '{label_name}'"
750
+ query = re.sub(r'(WHERE\s+|AND\s+|OR\s+)NOT\s+(\w+):([a-zA-Z0-9_`]+)', not_label_replacer, query, flags=re.IGNORECASE)
751
+
704
752
  # 4. Polymorphic matches and label access
705
753
  query = query.replace("labels(n)[0]", "label(n)")
706
754