codegraph-py 1.0.1__tar.gz → 1.1.0__tar.gz
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.
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/PKG-INFO +1 -1
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/__init__.py +1 -1
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/cli.py +135 -50
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/codegraph.py +75 -7
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/db/queries.py +30 -3
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/graph/__init__.py +24 -16
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/types.py +1 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph_py.egg-info/PKG-INFO +1 -1
- codegraph_py-1.1.0/codegraph_py.egg-info/top_level.txt +1 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/pyproject.toml +1 -1
- codegraph_py-1.0.1/codegraph_py.egg-info/top_level.txt +0 -2
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/README.md +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/__main__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/context/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/db/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/db/connection.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/db/schema.sql +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/directory.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/errors.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/extraction/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/extraction/languages/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/extraction/languages/base.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/extraction/languages/c_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/extraction/languages/cpp_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/extraction/languages/dart_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/extraction/languages/go_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/extraction/languages/java_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/extraction/languages/javascript_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/extraction/languages/kotlin_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/extraction/languages/lua_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/extraction/languages/python_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/extraction/languages/ruby_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/extraction/languages/rust_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/extraction/languages/scala_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/extraction/languages/swift_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/extraction/languages/typescript_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/extraction/tree_sitter_extractor.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/installer/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/mcp/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/project_config.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/resolution/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/search/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/sync/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/ui/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph/utils.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph_py.egg-info/SOURCES.txt +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph_py.egg-info/dependency_links.txt +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph_py.egg-info/entry_points.txt +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/codegraph_py.egg-info/requires.txt +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/setup.cfg +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/tests/test_codegraph.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.0}/tests/test_stress.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codegraph-py
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Supercharge AI coding agents with semantic code intelligence — surgical context, fewer tool calls, faster answers. 100% local.
|
|
5
5
|
Author-email: mading <martin98@bu.edu>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -106,13 +106,24 @@ def resolve_project_path(path_arg: Optional[str] = None) -> str:
|
|
|
106
106
|
# CLI Commands
|
|
107
107
|
# =============================================================================
|
|
108
108
|
|
|
109
|
-
@click.group()
|
|
109
|
+
@click.group(context_settings=dict(help_option_names=['-h', '--help']))
|
|
110
110
|
@click.version_option(version='1.0.1', prog_name='codegraph')
|
|
111
111
|
def main():
|
|
112
|
-
"""CodeGraph
|
|
113
|
-
|
|
114
|
-
Build a knowledge graph of your codebase for
|
|
115
|
-
|
|
112
|
+
"""CodeGraph — Semantic code intelligence for AI coding agents.
|
|
113
|
+
|
|
114
|
+
Build a knowledge graph of your codebase for instant symbol lookup,
|
|
115
|
+
call-chain analysis, and surgical context extraction.
|
|
116
|
+
|
|
117
|
+
\b
|
|
118
|
+
Common workflows:
|
|
119
|
+
codegraph init Initialize and index current project
|
|
120
|
+
codegraph status View index statistics
|
|
121
|
+
codegraph query "class" Search for symbols matching "class"
|
|
122
|
+
codegraph query --kind class List all classes in the project
|
|
123
|
+
codegraph explore "main" Show source code and call paths for "main"
|
|
124
|
+
codegraph callers "func" Show what calls the function "func"
|
|
125
|
+
codegraph callees "func" Show what "func" calls
|
|
126
|
+
codegraph files List all indexed files
|
|
116
127
|
"""
|
|
117
128
|
pass
|
|
118
129
|
|
|
@@ -308,7 +319,13 @@ def sync(path: Optional[str], quiet: bool):
|
|
|
308
319
|
@click.argument('path', required=False, default=None)
|
|
309
320
|
@click.option('-j', '--json', 'json_output', is_flag=True, help='Output as JSON')
|
|
310
321
|
def status(path: Optional[str], json_output: bool):
|
|
311
|
-
"""Show index status and
|
|
322
|
+
"""Show index status, statistics, and pending changes.
|
|
323
|
+
|
|
324
|
+
Displays file count, node/edge counts, node breakdown by kind,
|
|
325
|
+
and any pending changes since the last index.
|
|
326
|
+
|
|
327
|
+
Use --json/-j for machine-readable output.
|
|
328
|
+
"""
|
|
312
329
|
project_path = resolve_project_path(path)
|
|
313
330
|
|
|
314
331
|
if not is_initialized(project_path):
|
|
@@ -403,14 +420,21 @@ def status(path: Optional[str], json_output: bool):
|
|
|
403
420
|
|
|
404
421
|
|
|
405
422
|
@main.command()
|
|
406
|
-
@click.argument('search')
|
|
423
|
+
@click.argument('search', required=False, default='')
|
|
407
424
|
@click.option('-p', '--path', 'path_arg', help='Project path')
|
|
408
|
-
@click.option('-l', '--limit', default=
|
|
425
|
+
@click.option('-l', '--limit', default=20, type=int, help='Maximum results')
|
|
409
426
|
@click.option('-k', '--kind', help='Filter by node kind (function, class, etc.)')
|
|
427
|
+
@click.option('--exact', is_flag=True, help='Exact name match (instead of fuzzy/prefix)')
|
|
428
|
+
@click.option('--fuzzy', is_flag=True, help='Force fuzzy name matching (default)')
|
|
410
429
|
@click.option('-j', '--json', 'json_output', is_flag=True, help='Output as JSON')
|
|
411
430
|
def query(search: str, path_arg: Optional[str], limit: int,
|
|
412
|
-
kind: Optional[str],
|
|
413
|
-
|
|
431
|
+
kind: Optional[str], exact: bool, fuzzy: bool,
|
|
432
|
+
json_output: bool):
|
|
433
|
+
"""Search for symbols in the codebase.
|
|
434
|
+
|
|
435
|
+
If SEARCH is omitted and --kind is provided, lists all symbols of that kind.
|
|
436
|
+
Use --exact for precise symbol name lookup.
|
|
437
|
+
"""
|
|
414
438
|
project_path = resolve_project_path(path_arg)
|
|
415
439
|
|
|
416
440
|
if not is_initialized(project_path):
|
|
@@ -424,7 +448,15 @@ def query(search: str, path_arg: Optional[str], limit: int,
|
|
|
424
448
|
if kind:
|
|
425
449
|
opts.kinds = [kind]
|
|
426
450
|
|
|
427
|
-
|
|
451
|
+
if exact:
|
|
452
|
+
opts.exact_match = True
|
|
453
|
+
|
|
454
|
+
if not search.strip() and kind:
|
|
455
|
+
# No search term — list by kind only
|
|
456
|
+
nodes = cg._queries.get_nodes_by_kind(kind) if kind else []
|
|
457
|
+
results = [SearchResult(node=n, score=1.0) for n in nodes]
|
|
458
|
+
else:
|
|
459
|
+
results = cg.search_nodes(search, opts)
|
|
428
460
|
|
|
429
461
|
if json_output:
|
|
430
462
|
output = []
|
|
@@ -447,9 +479,15 @@ def query(search: str, path_arg: Optional[str], limit: int,
|
|
|
447
479
|
return
|
|
448
480
|
|
|
449
481
|
if not results:
|
|
450
|
-
|
|
482
|
+
if search:
|
|
483
|
+
click.echo(f'No results found for "{search}"')
|
|
484
|
+
elif kind:
|
|
485
|
+
click.echo(f'No symbols of kind "{kind}" found')
|
|
486
|
+
else:
|
|
487
|
+
click.echo('No search term provided. Use --kind to browse by type.')
|
|
451
488
|
else:
|
|
452
|
-
|
|
489
|
+
title = f'\nSearch Results for "{search}":' if search else f'\nAll {kind}s:' if kind else '\nResults:'
|
|
490
|
+
click.echo(bold(title + '\n'))
|
|
453
491
|
for r in results:
|
|
454
492
|
node = r.node
|
|
455
493
|
location = f'{node.file_path}:{node.start_line}'
|
|
@@ -473,6 +511,14 @@ def query(search: str, path_arg: Optional[str], limit: int,
|
|
|
473
511
|
def explore(query_parts: List[str], path_arg: Optional[str], max_files: int):
|
|
474
512
|
"""Explore code: relevant symbols' source + call paths in one shot.
|
|
475
513
|
|
|
514
|
+
QUERY_PARTS is one or more search terms for symbol lookup.
|
|
515
|
+
Combines search, callers/callees, and source display in a single view.
|
|
516
|
+
|
|
517
|
+
\b
|
|
518
|
+
Examples:
|
|
519
|
+
codegraph explore main Find "main" and show source + call context
|
|
520
|
+
codegraph explore ChatBackend Explore ChatBackend class and its relationships
|
|
521
|
+
|
|
476
522
|
Same output as the codegraph_explore MCP tool.
|
|
477
523
|
"""
|
|
478
524
|
query = ' '.join(query_parts)
|
|
@@ -524,9 +570,14 @@ def explore(query_parts: List[str], path_arg: Optional[str], max_files: int):
|
|
|
524
570
|
@main.command()
|
|
525
571
|
@click.argument('symbol')
|
|
526
572
|
@click.option('-p', '--path', 'path_arg', help='Project path')
|
|
527
|
-
@click.option('-d', '--depth', default=1, type=int, help='Traversal depth')
|
|
573
|
+
@click.option('-d', '--depth', default=1, type=int, help='Traversal depth (1=direct, 2=indirect)')
|
|
528
574
|
def callers(symbol: str, path_arg: Optional[str], depth: int):
|
|
529
|
-
"""Find what calls a function/method.
|
|
575
|
+
"""Find what calls a function/method.
|
|
576
|
+
|
|
577
|
+
SYMBOL is the symbol name to find callers for (e.g. "ChatBackend", "main").
|
|
578
|
+
|
|
579
|
+
Use -d 2 to include indirect callers (callers of callers).
|
|
580
|
+
"""
|
|
530
581
|
project_path = resolve_project_path(path_arg)
|
|
531
582
|
|
|
532
583
|
if not is_initialized(project_path):
|
|
@@ -569,9 +620,14 @@ def callers(symbol: str, path_arg: Optional[str], depth: int):
|
|
|
569
620
|
@main.command()
|
|
570
621
|
@click.argument('symbol')
|
|
571
622
|
@click.option('-p', '--path', 'path_arg', help='Project path')
|
|
572
|
-
@click.option('-d', '--depth', default=1, type=int, help='Traversal depth')
|
|
623
|
+
@click.option('-d', '--depth', default=1, type=int, help='Traversal depth (1=direct, 2=indirect)')
|
|
573
624
|
def callees(symbol: str, path_arg: Optional[str], depth: int):
|
|
574
|
-
"""Find what a function/method calls.
|
|
625
|
+
"""Find what a function/method calls.
|
|
626
|
+
|
|
627
|
+
SYMBOL is the symbol name to find callees for (e.g. "run", "process").
|
|
628
|
+
|
|
629
|
+
Use -d 2 to include indirect callees (callees of callees).
|
|
630
|
+
"""
|
|
575
631
|
project_path = resolve_project_path(path_arg)
|
|
576
632
|
|
|
577
633
|
if not is_initialized(project_path):
|
|
@@ -730,12 +786,14 @@ def node(name: str, path_arg: Optional[str]):
|
|
|
730
786
|
return
|
|
731
787
|
|
|
732
788
|
try:
|
|
733
|
-
cg = CodeGraph(project_path)
|
|
789
|
+
cg = CodeGraph.open_sync(project_path)
|
|
734
790
|
|
|
735
791
|
# Search for nodes by name
|
|
736
|
-
|
|
792
|
+
opts = SearchOptions(limit=20)
|
|
793
|
+
results = cg.search_nodes(name, opts)
|
|
737
794
|
if not results:
|
|
738
795
|
click.echo(yellow(f'No nodes found matching "{name}"'))
|
|
796
|
+
cg.destroy()
|
|
739
797
|
return
|
|
740
798
|
|
|
741
799
|
# If exact match, show details
|
|
@@ -758,18 +816,18 @@ def node(name: str, path_arg: Optional[str]):
|
|
|
758
816
|
click.echo(f' {dim("Exported:")} {green("yes") if target.is_exported else "no"}')
|
|
759
817
|
|
|
760
818
|
# Show callers
|
|
761
|
-
|
|
762
|
-
if
|
|
763
|
-
click.echo(bold(f'\n Callers ({len(
|
|
764
|
-
for
|
|
765
|
-
click.echo(f' ← {
|
|
819
|
+
caller_pairs = cg.get_callers(target.id)
|
|
820
|
+
if caller_pairs:
|
|
821
|
+
click.echo(bold(f'\n Callers ({len(caller_pairs)}):'))
|
|
822
|
+
for c_node, c_edge in caller_pairs:
|
|
823
|
+
click.echo(f' ← {c_node.name} ({c_node.file_path}:{c_node.start_line})')
|
|
766
824
|
|
|
767
825
|
# Show callees
|
|
768
|
-
|
|
769
|
-
if
|
|
770
|
-
click.echo(bold(f'\n Callees ({len(
|
|
771
|
-
for
|
|
772
|
-
click.echo(f' → {
|
|
826
|
+
callee_pairs = cg.get_callees(target.id)
|
|
827
|
+
if callee_pairs:
|
|
828
|
+
click.echo(bold(f'\n Callees ({len(callee_pairs)}):'))
|
|
829
|
+
for c_node, c_edge in callee_pairs:
|
|
830
|
+
click.echo(f' → {c_node.name} ({c_node.file_path}:{c_node.start_line})')
|
|
773
831
|
|
|
774
832
|
cg.destroy()
|
|
775
833
|
|
|
@@ -781,7 +839,9 @@ def node(name: str, path_arg: Optional[str]):
|
|
|
781
839
|
@main.command()
|
|
782
840
|
@click.option('--path', '-p', 'path_arg', default=None, help='Project path')
|
|
783
841
|
@click.option('--json-output', 'json_output', is_flag=True, help='Output as JSON')
|
|
784
|
-
|
|
842
|
+
@click.option('--by-directory', 'by_directory', is_flag=True, help='Group files by directory')
|
|
843
|
+
@click.option('--stats', 'show_stats', is_flag=True, help='Show per-file symbol stats (default when no flags)')
|
|
844
|
+
def files(path_arg: Optional[str], json_output: bool, by_directory: bool, show_stats: bool):
|
|
785
845
|
"""List indexed files with language and symbol statistics."""
|
|
786
846
|
project_path = resolve_project_path(path_arg)
|
|
787
847
|
|
|
@@ -791,44 +851,69 @@ def files(path_arg: Optional[str], json_output: bool):
|
|
|
791
851
|
return
|
|
792
852
|
|
|
793
853
|
try:
|
|
794
|
-
cg = CodeGraph(project_path)
|
|
854
|
+
cg = CodeGraph.open_sync(project_path)
|
|
795
855
|
stats = cg.get_stats()
|
|
796
|
-
all_files = cg.
|
|
856
|
+
all_files = cg._queries.get_all_file_paths()
|
|
797
857
|
|
|
798
858
|
if json_output:
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
'total_edges': stats.edge_count,
|
|
803
|
-
'files': [],
|
|
804
|
-
}
|
|
805
|
-
for f in all_files:
|
|
806
|
-
nodes = cg.db.get_nodes_by_file(f)
|
|
859
|
+
file_list = []
|
|
860
|
+
for f in sorted(all_files):
|
|
861
|
+
nodes = cg.get_nodes_by_file(f)
|
|
807
862
|
langs = set(n.language for n in nodes if n.language)
|
|
808
|
-
|
|
863
|
+
kinds = {}
|
|
864
|
+
for n in nodes:
|
|
865
|
+
kinds[n.kind] = kinds.get(n.kind, 0) + 1
|
|
866
|
+
file_list.append({
|
|
809
867
|
'path': f,
|
|
810
868
|
'node_count': len(nodes),
|
|
811
869
|
'languages': list(langs),
|
|
870
|
+
'node_kinds': kinds,
|
|
812
871
|
})
|
|
872
|
+
result = {
|
|
873
|
+
'total_files': len(all_files),
|
|
874
|
+
'total_nodes': stats.node_count,
|
|
875
|
+
'total_edges': stats.edge_count,
|
|
876
|
+
'files': file_list,
|
|
877
|
+
}
|
|
813
878
|
click.echo(json.dumps(result, indent=2))
|
|
879
|
+
cg.destroy()
|
|
880
|
+
return
|
|
881
|
+
|
|
882
|
+
show_stats = show_stats or not by_directory
|
|
883
|
+
|
|
884
|
+
if by_directory:
|
|
885
|
+
# Group files by directory
|
|
886
|
+
dirs: Dict[str, List[str]] = {}
|
|
887
|
+
for f in sorted(all_files):
|
|
888
|
+
d = os.path.dirname(f) or '.'
|
|
889
|
+
dirs.setdefault(d, []).append(f)
|
|
890
|
+
click.echo(bold(f'\n📁 Indexed Files ({len(all_files)} total, {len(dirs)} directories)\n'))
|
|
891
|
+
for directory in sorted(dirs):
|
|
892
|
+
click.echo(blue(f' {directory}/'))
|
|
893
|
+
for f in dirs[directory]:
|
|
894
|
+
click.echo(f' {os.path.basename(f)}')
|
|
895
|
+
click.echo()
|
|
814
896
|
else:
|
|
815
897
|
click.echo(bold(f'\n📁 Indexed Files ({len(all_files)} total)\n'))
|
|
816
898
|
for f in sorted(all_files):
|
|
817
|
-
nodes = cg.
|
|
899
|
+
nodes = cg.get_nodes_by_file(f)
|
|
818
900
|
lang = ''
|
|
819
901
|
for n in nodes:
|
|
820
902
|
if n.language and n.language != 'unknown':
|
|
821
903
|
lang = n.language
|
|
822
904
|
break
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
905
|
+
if show_stats:
|
|
906
|
+
kinds = {}
|
|
907
|
+
for n in nodes:
|
|
908
|
+
k = n.kind
|
|
909
|
+
kinds[k] = kinds.get(k, 0) + 1
|
|
910
|
+
kind_str = ', '.join(f'{v} {k}' for k, v in sorted(kinds.items()) if k != 'file')
|
|
911
|
+
if kind_str:
|
|
912
|
+
click.echo(f' {dim(lang+" ") if lang else ""}{f} {dim("("+kind_str+")")}')
|
|
913
|
+
else:
|
|
914
|
+
click.echo(f' {f}')
|
|
830
915
|
else:
|
|
831
|
-
click.echo(f' {f}')
|
|
916
|
+
click.echo(f' {dim(lang+" ") if lang else ""}{f}')
|
|
832
917
|
|
|
833
918
|
cg.destroy()
|
|
834
919
|
|
|
@@ -31,7 +31,7 @@ from codegraph.graph import GraphTraverser, GraphQueryManager
|
|
|
31
31
|
from codegraph.context import ContextBuilder, create_context_builder
|
|
32
32
|
from codegraph.sync import FileWatcher, WatchOptions, PendingFile
|
|
33
33
|
|
|
34
|
-
__version__ = "1.0
|
|
34
|
+
__version__ = "1.1.0"
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
@dataclass
|
|
@@ -357,25 +357,93 @@ class CodeGraph:
|
|
|
357
357
|
return self._queries.get_nodes_by_file(file_path)
|
|
358
358
|
|
|
359
359
|
def get_callers(self, node_id: str, max_depth: int = 1) -> List[Tuple[Node, Edge]]:
|
|
360
|
-
"""Find what calls a function/method.
|
|
361
|
-
|
|
362
|
-
|
|
360
|
+
"""Find what calls a function/method.
|
|
361
|
+
|
|
362
|
+
First tries edge-based lookup (resolved references), then falls back
|
|
363
|
+
to name-based matching on unresolved references for maximum coverage.
|
|
364
|
+
"""
|
|
365
|
+
result: List[Tuple[Node, Edge]] = []
|
|
366
|
+
|
|
367
|
+
# ── Phase 1: edge-based (resolved references) ──
|
|
368
|
+
nodes = self._traverser.getCallers(node_id, max_depth)
|
|
363
369
|
for n in nodes:
|
|
364
370
|
edges = self._queries.get_outgoing_edges(n.id, ['calls', 'references'])
|
|
365
371
|
for e in edges:
|
|
366
372
|
if e.target == node_id:
|
|
367
373
|
result.append((n, e))
|
|
374
|
+
|
|
375
|
+
if result:
|
|
376
|
+
return result
|
|
377
|
+
|
|
378
|
+
# ── Phase 2: fallback via UnresolvedReference name matching ──
|
|
379
|
+
target = self._queries.get_node_by_id(node_id)
|
|
380
|
+
if target:
|
|
381
|
+
target_name = target.name
|
|
382
|
+
# Collect all refs whose name matches the target name (or any dotted suffix)
|
|
383
|
+
refs = self._queries.get_unresolved_refs()
|
|
384
|
+
seen: Set[str] = set()
|
|
385
|
+
for ref in refs:
|
|
386
|
+
ref_name = ref.reference_name
|
|
387
|
+
# Exact match, or last component of dotted name matches
|
|
388
|
+
if ref_name == target_name or (
|
|
389
|
+
'.' in ref_name and ref_name.rsplit('.', 1)[-1] == target_name
|
|
390
|
+
):
|
|
391
|
+
caller = self._queries.get_node_by_id(ref.from_node_id)
|
|
392
|
+
if caller and caller.id not in seen:
|
|
393
|
+
seen.add(caller.id)
|
|
394
|
+
fake_edge = Edge(
|
|
395
|
+
source=caller.id, target=node_id,
|
|
396
|
+
kind=ref.reference_kind or 'references',
|
|
397
|
+
line=ref.line, column=ref.column,
|
|
398
|
+
provenance='unresolved',
|
|
399
|
+
)
|
|
400
|
+
result.append((caller, fake_edge))
|
|
401
|
+
|
|
368
402
|
return result
|
|
369
403
|
|
|
370
404
|
def get_callees(self, node_id: str, max_depth: int = 1) -> List[Tuple[Node, Edge]]:
|
|
371
|
-
"""Find what a function/method calls.
|
|
372
|
-
|
|
373
|
-
|
|
405
|
+
"""Find what a function/method calls.
|
|
406
|
+
|
|
407
|
+
First tries edge-based lookup (resolved references), then falls back
|
|
408
|
+
to name-based matching on unresolved references for maximum coverage.
|
|
409
|
+
"""
|
|
410
|
+
result: List[Tuple[Node, Edge]] = []
|
|
411
|
+
|
|
412
|
+
# ── Phase 1: edge-based (resolved references) ──
|
|
413
|
+
nodes = self._traverser.getCallees(node_id, max_depth)
|
|
374
414
|
for n in nodes:
|
|
375
415
|
edges = self._queries.get_incoming_edges(n.id, ['calls', 'references'])
|
|
376
416
|
for e in edges:
|
|
377
417
|
if e.source == node_id:
|
|
378
418
|
result.append((n, e))
|
|
419
|
+
|
|
420
|
+
if result:
|
|
421
|
+
return result
|
|
422
|
+
|
|
423
|
+
# ── Phase 2: fallback via UnresolvedReference name matching ──
|
|
424
|
+
source = self._queries.get_node_by_id(node_id)
|
|
425
|
+
if source:
|
|
426
|
+
refs = self._queries.get_unresolved_refs_by_from_node(node_id)
|
|
427
|
+
seen: Set[str] = set()
|
|
428
|
+
for ref in refs:
|
|
429
|
+
# Try to resolve the referenced name to a real node
|
|
430
|
+
callees_found = self._queries.get_nodes_by_name(ref.reference_name)
|
|
431
|
+
# Also try last component of dotted names
|
|
432
|
+
if not callees_found and '.' in ref.reference_name:
|
|
433
|
+
simple = ref.reference_name.rsplit('.', 1)[-1]
|
|
434
|
+
callees_found = self._queries.get_nodes_by_name(simple)
|
|
435
|
+
|
|
436
|
+
for callee in callees_found:
|
|
437
|
+
if callee.id not in seen:
|
|
438
|
+
seen.add(callee.id)
|
|
439
|
+
fake_edge = Edge(
|
|
440
|
+
source=node_id, target=callee.id,
|
|
441
|
+
kind=ref.reference_kind or 'references',
|
|
442
|
+
line=ref.line, column=ref.column,
|
|
443
|
+
provenance='unresolved',
|
|
444
|
+
)
|
|
445
|
+
result.append((callee, fake_edge))
|
|
446
|
+
|
|
379
447
|
return result
|
|
380
448
|
|
|
381
449
|
def get_impact_radius(self, node_id: str, max_depth: int = 3) -> Subgraph:
|
|
@@ -443,6 +443,13 @@ class QueryBuilder:
|
|
|
443
443
|
)
|
|
444
444
|
return [self._row_to_unresolved(row) for row in cur.fetchall()]
|
|
445
445
|
|
|
446
|
+
def get_unresolved_refs_by_from_node(self, from_node_id: str) -> List[UnresolvedReference]:
|
|
447
|
+
"""Get unresolved references by originating node ID."""
|
|
448
|
+
cur = self._db.execute(
|
|
449
|
+
'SELECT * FROM unresolved_refs WHERE from_node_id = ?', (from_node_id,)
|
|
450
|
+
)
|
|
451
|
+
return [self._row_to_unresolved(row) for row in cur.fetchall()]
|
|
452
|
+
|
|
446
453
|
def clear_unresolved_refs(self) -> None:
|
|
447
454
|
"""Clear all unresolved references."""
|
|
448
455
|
self._db.execute('DELETE FROM unresolved_refs')
|
|
@@ -453,12 +460,32 @@ class QueryBuilder:
|
|
|
453
460
|
|
|
454
461
|
def search_nodes(self, query: str,
|
|
455
462
|
options: Optional[SearchOptions] = None) -> List[SearchResult]:
|
|
456
|
-
"""Search nodes
|
|
463
|
+
"""Search nodes in the codebase.
|
|
464
|
+
|
|
465
|
+
Supports three modes:
|
|
466
|
+
- Exact match (opts.exact_match=True): precise name lookup via SQL
|
|
467
|
+
- FTS5 full-text: prefix-based ranking search
|
|
468
|
+
- LIKE fallback: substring matching when FTS returns nothing
|
|
469
|
+
"""
|
|
457
470
|
opts = options or SearchOptions()
|
|
458
471
|
results: List[SearchResult] = []
|
|
459
472
|
|
|
473
|
+
# ── Exact match mode ──
|
|
474
|
+
if opts.exact_match and query.strip():
|
|
475
|
+
cur = self._db.execute(
|
|
476
|
+
'''SELECT * FROM nodes WHERE name = ?
|
|
477
|
+
ORDER BY file_path, start_line
|
|
478
|
+
LIMIT ?''',
|
|
479
|
+
(query.strip(), opts.limit)
|
|
480
|
+
)
|
|
481
|
+
for row in cur.fetchall():
|
|
482
|
+
node = self._row_to_node(row)
|
|
483
|
+
if self._matches_filters(node, opts):
|
|
484
|
+
results.append(SearchResult(node=node, score=1.0))
|
|
485
|
+
return results
|
|
486
|
+
|
|
487
|
+
# ── FTS5 full-text search ──
|
|
460
488
|
try:
|
|
461
|
-
# FTS5 query with prefix matching
|
|
462
489
|
fts_query = ' OR '.join(
|
|
463
490
|
f'"{word}"*' if len(word) > 1 else word
|
|
464
491
|
for word in query.split()
|
|
@@ -484,7 +511,7 @@ class QueryBuilder:
|
|
|
484
511
|
except Exception:
|
|
485
512
|
pass
|
|
486
513
|
|
|
487
|
-
#
|
|
514
|
+
# ── LIKE fallback ──
|
|
488
515
|
if not results:
|
|
489
516
|
for word in query.split():
|
|
490
517
|
if not word.strip():
|
|
@@ -174,62 +174,70 @@ class GraphTraverser:
|
|
|
174
174
|
dfs(start_id, 0)
|
|
175
175
|
return result
|
|
176
176
|
|
|
177
|
-
def getCallers(self, node_id: str,
|
|
177
|
+
def getCallers(self, node_id: str, max_depth: int = 1) -> List[Node]:
|
|
178
178
|
"""Get nodes that call/ reference the given node.
|
|
179
|
-
|
|
179
|
+
|
|
180
180
|
Args:
|
|
181
181
|
node_id: Target node ID
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
max_depth: How many levels up to traverse.
|
|
183
|
+
1 = direct callers only,
|
|
184
|
+
2 = callers + their callers, etc.
|
|
185
|
+
|
|
184
186
|
Returns:
|
|
185
187
|
List of caller nodes
|
|
186
188
|
"""
|
|
187
189
|
callers: List[Node] = []
|
|
188
190
|
seen: Set[str] = set()
|
|
189
191
|
|
|
190
|
-
def collect_callers(nid: str) -> None:
|
|
192
|
+
def collect_callers(nid: str, depth: int) -> None:
|
|
191
193
|
if nid in seen:
|
|
192
194
|
return
|
|
193
195
|
seen.add(nid)
|
|
194
196
|
|
|
197
|
+
if depth >= max_depth:
|
|
198
|
+
return
|
|
199
|
+
|
|
195
200
|
edges = self._qb.get_incoming_edges(nid, [EdgeKind.CALLS, EdgeKind.REFERENCES])
|
|
196
201
|
for edge in edges:
|
|
197
202
|
caller_node = self._qb.get_node_by_id(edge.source)
|
|
198
203
|
if caller_node and edge.source not in seen:
|
|
199
204
|
callers.append(caller_node)
|
|
200
|
-
|
|
201
|
-
collect_callers(edge.source)
|
|
205
|
+
collect_callers(edge.source, depth + 1)
|
|
202
206
|
|
|
203
|
-
collect_callers(node_id)
|
|
207
|
+
collect_callers(node_id, 0)
|
|
204
208
|
return callers
|
|
205
209
|
|
|
206
|
-
def getCallees(self, node_id: str,
|
|
210
|
+
def getCallees(self, node_id: str, max_depth: int = 1) -> List[Node]:
|
|
207
211
|
"""Get nodes that the given node calls/ references.
|
|
208
|
-
|
|
212
|
+
|
|
209
213
|
Args:
|
|
210
214
|
node_id: Source node ID
|
|
211
|
-
|
|
212
|
-
|
|
215
|
+
max_depth: How many levels down to traverse.
|
|
216
|
+
1 = direct callees only,
|
|
217
|
+
2 = callees + their callees, etc.
|
|
218
|
+
|
|
213
219
|
Returns:
|
|
214
220
|
List of callee nodes
|
|
215
221
|
"""
|
|
216
222
|
callees: List[Node] = []
|
|
217
223
|
seen: Set[str] = set()
|
|
218
224
|
|
|
219
|
-
def collect_callees(nid: str) -> None:
|
|
225
|
+
def collect_callees(nid: str, depth: int) -> None:
|
|
220
226
|
if nid in seen:
|
|
221
227
|
return
|
|
222
228
|
seen.add(nid)
|
|
223
229
|
|
|
230
|
+
if depth >= max_depth:
|
|
231
|
+
return
|
|
232
|
+
|
|
224
233
|
edges = self._qb.get_outgoing_edges(nid, [EdgeKind.CALLS, EdgeKind.REFERENCES])
|
|
225
234
|
for edge in edges:
|
|
226
235
|
callee_node = self._qb.get_node_by_id(edge.target)
|
|
227
236
|
if callee_node and edge.target not in seen:
|
|
228
237
|
callees.append(callee_node)
|
|
229
|
-
|
|
230
|
-
collect_callees(edge.target)
|
|
238
|
+
collect_callees(edge.target, depth + 1)
|
|
231
239
|
|
|
232
|
-
collect_callees(node_id)
|
|
240
|
+
collect_callees(node_id, 0)
|
|
233
241
|
return callees
|
|
234
242
|
|
|
235
243
|
def getImpactRadius(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codegraph-py
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Supercharge AI coding agents with semantic code intelligence — surgical context, fewer tool calls, faster answers. 100% local.
|
|
5
5
|
Author-email: mading <martin98@bu.edu>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
codegraph
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codegraph-py"
|
|
7
|
-
version = "1.0
|
|
7
|
+
version = "1.1.0"
|
|
8
8
|
description = "Supercharge AI coding agents with semantic code intelligence — surgical context, fewer tool calls, faster answers. 100% local."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|