pycode-kg 0.16.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pycode_kg/.DS_Store +0 -0
- pycode_kg/__init__.py +91 -0
- pycode_kg/__main__.py +11 -0
- pycode_kg/analysis/__init__.py +15 -0
- pycode_kg/analysis/bridge.py +108 -0
- pycode_kg/analysis/centrality.py +412 -0
- pycode_kg/analysis/framework_detector.py +103 -0
- pycode_kg/analysis/hybrid_rank.py +53 -0
- pycode_kg/app.py +1335 -0
- pycode_kg/architecture.py +624 -0
- pycode_kg/build_pycodekg_lancedb.py +15 -0
- pycode_kg/build_pycodekg_sqlite.py +15 -0
- pycode_kg/cli/__init__.py +29 -0
- pycode_kg/cli/cmd_analyze.py +96 -0
- pycode_kg/cli/cmd_architecture.py +150 -0
- pycode_kg/cli/cmd_bridges.py +26 -0
- pycode_kg/cli/cmd_build.py +154 -0
- pycode_kg/cli/cmd_build_full.py +242 -0
- pycode_kg/cli/cmd_centrality.py +131 -0
- pycode_kg/cli/cmd_explain.py +180 -0
- pycode_kg/cli/cmd_framework_nodes.py +18 -0
- pycode_kg/cli/cmd_hooks.py +137 -0
- pycode_kg/cli/cmd_init.py +312 -0
- pycode_kg/cli/cmd_mcp.py +71 -0
- pycode_kg/cli/cmd_model.py +53 -0
- pycode_kg/cli/cmd_query.py +211 -0
- pycode_kg/cli/cmd_snapshot.py +421 -0
- pycode_kg/cli/cmd_viz.py +180 -0
- pycode_kg/cli/main.py +23 -0
- pycode_kg/cli/options.py +63 -0
- pycode_kg/config.py +78 -0
- pycode_kg/graph.py +125 -0
- pycode_kg/index.py +542 -0
- pycode_kg/kg.py +220 -0
- pycode_kg/layout3d.py +470 -0
- pycode_kg/mcp/bridge_tools.py +19 -0
- pycode_kg/mcp/framework_tools.py +18 -0
- pycode_kg/mcp_server.py +1965 -0
- pycode_kg/module/__init__.py +83 -0
- pycode_kg/module/base.py +720 -0
- pycode_kg/module/extractor.py +276 -0
- pycode_kg/module/types.py +532 -0
- pycode_kg/pycodekg.py +543 -0
- pycode_kg/pycodekg_query.py +1 -0
- pycode_kg/pycodekg_snippet_packer.py +1 -0
- pycode_kg/pycodekg_thorough_analysis.py +2751 -0
- pycode_kg/pycodekg_viz.py +1 -0
- pycode_kg/pycodekg_viz3d.py +1 -0
- pycode_kg/ranking/__init__.py +1 -0
- pycode_kg/ranking/cli_rank.py +92 -0
- pycode_kg/ranking/coderank.py +555 -0
- pycode_kg/snapshots.py +612 -0
- pycode_kg/sql/004_add_centrality_table.sql +12 -0
- pycode_kg/store.py +766 -0
- pycode_kg/utils.py +39 -0
- pycode_kg/visitor.py +413 -0
- pycode_kg/viz3d.py +1353 -0
- pycode_kg/viz3d_timeline.py +364 -0
- pycode_kg-0.16.0.dist-info/METADATA +305 -0
- pycode_kg-0.16.0.dist-info/RECORD +63 -0
- pycode_kg-0.16.0.dist-info/WHEEL +4 -0
- pycode_kg-0.16.0.dist-info/entry_points.txt +19 -0
- pycode_kg-0.16.0.dist-info/licenses/LICENSE +94 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cmd_analyze.py
|
|
3
|
+
|
|
4
|
+
Click subcommand for running a thorough codebase analysis:
|
|
5
|
+
|
|
6
|
+
analyze — run PyCodeKGAnalyzer, emit a Markdown report (JSON optional via --json)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
from pycode_kg.cli.main import cli
|
|
16
|
+
from pycode_kg.cli.options import exclude_option, include_option
|
|
17
|
+
from pycode_kg.config import load_exclude_dirs, load_include_dirs
|
|
18
|
+
from pycode_kg.pycodekg_thorough_analysis import main as run_analysis
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@cli.command("analyze")
|
|
22
|
+
@click.argument("repo_root", default=".", required=False)
|
|
23
|
+
@click.option(
|
|
24
|
+
"--db",
|
|
25
|
+
default=None,
|
|
26
|
+
type=click.Path(),
|
|
27
|
+
help="SQLite knowledge graph path (default: <repo>/.pycodekg/graph.sqlite).",
|
|
28
|
+
)
|
|
29
|
+
@click.option(
|
|
30
|
+
"--lancedb",
|
|
31
|
+
default=None,
|
|
32
|
+
type=click.Path(),
|
|
33
|
+
help="LanceDB vector index directory (default: <repo>/.pycodekg/lancedb).",
|
|
34
|
+
)
|
|
35
|
+
@click.option(
|
|
36
|
+
"--output",
|
|
37
|
+
"-o",
|
|
38
|
+
default=None,
|
|
39
|
+
type=click.Path(),
|
|
40
|
+
help="Markdown report output path (default: <repo>_analysis_<YYYYMMDD>.md).",
|
|
41
|
+
)
|
|
42
|
+
@click.option(
|
|
43
|
+
"--json",
|
|
44
|
+
"-j",
|
|
45
|
+
"json_path",
|
|
46
|
+
default=None,
|
|
47
|
+
type=click.Path(),
|
|
48
|
+
help="Write JSON snapshot to this path (omit to skip JSON output).",
|
|
49
|
+
)
|
|
50
|
+
@click.option(
|
|
51
|
+
"--quiet",
|
|
52
|
+
"-q",
|
|
53
|
+
is_flag=True,
|
|
54
|
+
help="Suppress the Rich console summary table.",
|
|
55
|
+
)
|
|
56
|
+
@click.option(
|
|
57
|
+
"--write-centrality",
|
|
58
|
+
is_flag=True,
|
|
59
|
+
help="Persist SIR centrality scores to the centrality_scores table in the SQLite graph.",
|
|
60
|
+
)
|
|
61
|
+
@include_option
|
|
62
|
+
@exclude_option
|
|
63
|
+
def analyze(
|
|
64
|
+
repo_root: str,
|
|
65
|
+
db: str | None,
|
|
66
|
+
lancedb: str | None,
|
|
67
|
+
output: str | None,
|
|
68
|
+
json_path: str | None,
|
|
69
|
+
quiet: bool,
|
|
70
|
+
write_centrality: bool,
|
|
71
|
+
include_dir: tuple[str, ...],
|
|
72
|
+
exclude_dir: tuple[str, ...],
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Run a thorough analysis of a Python repository.
|
|
75
|
+
|
|
76
|
+
Analyzes complexity, hotspots, docstring coverage, circular dependencies,
|
|
77
|
+
and other health signals. Outputs a Markdown report. Pass --json PATH to
|
|
78
|
+
also write a JSON snapshot.
|
|
79
|
+
"""
|
|
80
|
+
# Merge pyproject.toml config with CLI flags
|
|
81
|
+
repo_path = Path(repo_root).resolve()
|
|
82
|
+
include = load_include_dirs(repo_path) | set(include_dir)
|
|
83
|
+
exclude = load_exclude_dirs(repo_path) | set(exclude_dir)
|
|
84
|
+
|
|
85
|
+
# Run thorough analysis
|
|
86
|
+
run_analysis(
|
|
87
|
+
repo_root=repo_root,
|
|
88
|
+
db_path=db,
|
|
89
|
+
lancedb_path=lancedb,
|
|
90
|
+
report_path=output,
|
|
91
|
+
json_path=json_path,
|
|
92
|
+
quiet=quiet,
|
|
93
|
+
include=include if include else None,
|
|
94
|
+
exclude=exclude if exclude else None,
|
|
95
|
+
persist_centrality=write_centrality,
|
|
96
|
+
)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cmd_architecture.py
|
|
3
|
+
|
|
4
|
+
Click subcommand for generating coherent architecture descriptions:
|
|
5
|
+
|
|
6
|
+
architecture — analyze codebase architecture, emit Markdown or JSON descriptions
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib.metadata
|
|
12
|
+
import subprocess
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
|
|
17
|
+
from pycode_kg.architecture import ArchitectureAnalyzer
|
|
18
|
+
from pycode_kg.cli.main import cli
|
|
19
|
+
from pycode_kg.store import GraphStore
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@cli.command("architecture")
|
|
23
|
+
@click.argument("repo_root", default=".", required=False)
|
|
24
|
+
@click.option(
|
|
25
|
+
"--db",
|
|
26
|
+
default=None,
|
|
27
|
+
type=click.Path(),
|
|
28
|
+
help="SQLite knowledge graph path (default: <repo>/.pycodekg/graph.sqlite).",
|
|
29
|
+
)
|
|
30
|
+
@click.option(
|
|
31
|
+
"--markdown",
|
|
32
|
+
"-m",
|
|
33
|
+
"markdown_path",
|
|
34
|
+
default=None,
|
|
35
|
+
type=click.Path(),
|
|
36
|
+
help="Save architecture description as Markdown.",
|
|
37
|
+
)
|
|
38
|
+
@click.option(
|
|
39
|
+
"--json",
|
|
40
|
+
"-j",
|
|
41
|
+
"json_path",
|
|
42
|
+
default=None,
|
|
43
|
+
type=click.Path(),
|
|
44
|
+
help="Save architecture description as JSON (infographic-ready).",
|
|
45
|
+
)
|
|
46
|
+
@click.option(
|
|
47
|
+
"--analysis",
|
|
48
|
+
type=click.Path(),
|
|
49
|
+
default=None,
|
|
50
|
+
help="Path to thorough analysis JSON (from 'pycodekg analyze') to incorporate.",
|
|
51
|
+
)
|
|
52
|
+
@click.option(
|
|
53
|
+
"--load-latest",
|
|
54
|
+
is_flag=True,
|
|
55
|
+
help="Auto-load latest thorough analysis from ~/.claude/pycodekg_analysis_latest.json.",
|
|
56
|
+
)
|
|
57
|
+
def architecture(
|
|
58
|
+
repo_root: str,
|
|
59
|
+
db: str | None,
|
|
60
|
+
markdown_path: str | None,
|
|
61
|
+
json_path: str | None,
|
|
62
|
+
analysis: str | None,
|
|
63
|
+
load_latest: bool,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Generate a coherent architecture description of a Python repository.
|
|
66
|
+
|
|
67
|
+
Produces human-readable Markdown and machine-friendly JSON suitable for
|
|
68
|
+
infographics. Can incorporate insights from 'pycodekg analyze' output.
|
|
69
|
+
|
|
70
|
+
If neither --markdown nor --json is specified, prints Markdown to console.
|
|
71
|
+
"""
|
|
72
|
+
repo_path = Path(repo_root).resolve()
|
|
73
|
+
db_path = Path(db or repo_path / ".pycodekg" / "graph.sqlite")
|
|
74
|
+
|
|
75
|
+
if not db_path.exists():
|
|
76
|
+
raise click.UsageError(
|
|
77
|
+
f"SQLite graph not found: {db_path}\nRun 'pycodekg build' first to index your repository."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
store = GraphStore(db_path)
|
|
81
|
+
try:
|
|
82
|
+
# Get version and commit for provenance
|
|
83
|
+
try:
|
|
84
|
+
commit = subprocess.check_output(
|
|
85
|
+
["git", "rev-parse", "HEAD"], text=True, stderr=subprocess.DEVNULL
|
|
86
|
+
).strip()[:10]
|
|
87
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
88
|
+
commit = "unknown"
|
|
89
|
+
|
|
90
|
+
# Try to get version from package
|
|
91
|
+
version = "unknown"
|
|
92
|
+
try:
|
|
93
|
+
version = importlib.metadata.version("pycode-kg")
|
|
94
|
+
except importlib.metadata.PackageNotFoundError:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
analyzer = ArchitectureAnalyzer(store, repo_path, version=version, commit=commit)
|
|
98
|
+
|
|
99
|
+
# Load thorough analysis - try in order: explicit path, auto-load latest, skip
|
|
100
|
+
import json # pylint: disable=import-outside-toplevel
|
|
101
|
+
|
|
102
|
+
analysis_loaded = False
|
|
103
|
+
if analysis:
|
|
104
|
+
try:
|
|
105
|
+
with open(analysis) as f:
|
|
106
|
+
analysis_data = json.load(f)
|
|
107
|
+
analyzer.incorporate_thorough_analysis(analysis_data)
|
|
108
|
+
click.echo(f"OK Loaded analysis: {analysis}")
|
|
109
|
+
analysis_loaded = True
|
|
110
|
+
except (FileNotFoundError, json.JSONDecodeError) as e:
|
|
111
|
+
click.echo(f"Warning: Could not load analysis file: {e}", err=True)
|
|
112
|
+
|
|
113
|
+
elif load_latest:
|
|
114
|
+
latest_path = Path.home() / ".claude" / "pycodekg_analysis_latest.json"
|
|
115
|
+
if latest_path.exists():
|
|
116
|
+
try:
|
|
117
|
+
with open(latest_path) as f:
|
|
118
|
+
analysis_data = json.load(f)
|
|
119
|
+
analyzer.incorporate_thorough_analysis(analysis_data)
|
|
120
|
+
click.echo("OK Auto-loaded latest analysis")
|
|
121
|
+
analysis_loaded = True
|
|
122
|
+
except json.JSONDecodeError as e:
|
|
123
|
+
click.echo(f"Warning: Could not parse analysis file: {e}", err=True)
|
|
124
|
+
|
|
125
|
+
if not analysis_loaded:
|
|
126
|
+
click.echo(
|
|
127
|
+
"Tip: Run 'pycodekg analyze --json' then 'pycodekg architecture --load-latest'"
|
|
128
|
+
)
|
|
129
|
+
click.echo(" for richer architectural insights.")
|
|
130
|
+
|
|
131
|
+
# Generate outputs
|
|
132
|
+
if markdown_path:
|
|
133
|
+
arch_md = analyzer.analyze_to_markdown()
|
|
134
|
+
with open(markdown_path, "w") as f:
|
|
135
|
+
f.write(arch_md)
|
|
136
|
+
click.echo(f"OK Architecture Markdown: {markdown_path}")
|
|
137
|
+
|
|
138
|
+
if json_path:
|
|
139
|
+
arch_json = analyzer.analyze_to_json()
|
|
140
|
+
with open(json_path, "w") as f:
|
|
141
|
+
f.write(arch_json)
|
|
142
|
+
click.echo(f"OK Architecture JSON: {json_path}")
|
|
143
|
+
|
|
144
|
+
if not markdown_path and not json_path:
|
|
145
|
+
# Print to console
|
|
146
|
+
arch_md = analyzer.analyze_to_markdown()
|
|
147
|
+
click.echo(arch_md)
|
|
148
|
+
|
|
149
|
+
finally:
|
|
150
|
+
store.close()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI command for bridge centrality in PyCodeKG.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from pycode_kg.analysis.bridge import compute_bridge_centrality
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
description="Show top bridge modules by betweenness centrality."
|
|
13
|
+
)
|
|
14
|
+
parser.add_argument("--db", default="pycodekg.sqlite", help="Path to PyCodeKG SQLite DB")
|
|
15
|
+
parser.add_argument("--top", type=int, default=25, help="Number of top bridges to show")
|
|
16
|
+
parser.add_argument("--no-imports", action="store_true", help="Ignore IMPORTS edges")
|
|
17
|
+
args = parser.parse_args()
|
|
18
|
+
bridges = compute_bridge_centrality(
|
|
19
|
+
kind="module",
|
|
20
|
+
include_imports=not args.no_imports,
|
|
21
|
+
top=args.top,
|
|
22
|
+
db_path=args.db,
|
|
23
|
+
)
|
|
24
|
+
print(f"Top {args.top} bridge modules:")
|
|
25
|
+
for mod, score in bridges:
|
|
26
|
+
print(f"{mod:50s} {score:.5f}")
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cmd_build.py
|
|
3
|
+
|
|
4
|
+
Click subcommands for building the PyCodeKG knowledge graph:
|
|
5
|
+
|
|
6
|
+
build-sqlite - repo -> AST -> graph store
|
|
7
|
+
build-lancedb - graph store -> vector index
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from pycode_kg.cli.main import cli
|
|
17
|
+
from pycode_kg.cli.options import exclude_option, include_option, repo_option
|
|
18
|
+
from pycode_kg.config import load_exclude_dirs, load_include_dirs
|
|
19
|
+
from pycode_kg.graph import CodeGraph
|
|
20
|
+
from pycode_kg.index import (
|
|
21
|
+
SemanticIndex,
|
|
22
|
+
SentenceTransformerEmbedder,
|
|
23
|
+
suppress_ingestion_logging,
|
|
24
|
+
)
|
|
25
|
+
from pycode_kg.pycodekg import DEFAULT_MODEL
|
|
26
|
+
from pycode_kg.store import GraphStore
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@cli.command("build-sqlite")
|
|
30
|
+
@repo_option
|
|
31
|
+
@click.option(
|
|
32
|
+
"--db",
|
|
33
|
+
default=None,
|
|
34
|
+
type=click.Path(),
|
|
35
|
+
show_default=False,
|
|
36
|
+
help="SQLite database path (default: <repo>/.pycodekg/graph.sqlite).",
|
|
37
|
+
)
|
|
38
|
+
@click.option("--wipe", is_flag=True, help="Delete existing graph first.")
|
|
39
|
+
@include_option
|
|
40
|
+
@exclude_option
|
|
41
|
+
def build_sqlite(
|
|
42
|
+
repo: str,
|
|
43
|
+
db: str | None,
|
|
44
|
+
wipe: bool,
|
|
45
|
+
include_dir: tuple[str, ...],
|
|
46
|
+
exclude_dir: tuple[str, ...],
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Extract a code knowledge graph from a Python repo and store it in SQLite."""
|
|
49
|
+
repo_root = Path(repo).resolve()
|
|
50
|
+
db_path = Path(db) if db else repo_root / ".pycodekg" / "graph.sqlite"
|
|
51
|
+
|
|
52
|
+
# Merge pyproject.toml config with CLI flags
|
|
53
|
+
include = load_include_dirs(repo_root) | set(include_dir)
|
|
54
|
+
exclude = load_exclude_dirs(repo_root) | set(exclude_dir)
|
|
55
|
+
|
|
56
|
+
graph = CodeGraph(
|
|
57
|
+
repo_root,
|
|
58
|
+
include=include if include else None,
|
|
59
|
+
exclude=exclude if exclude else None,
|
|
60
|
+
)
|
|
61
|
+
nodes, edges = graph.extract().result()
|
|
62
|
+
|
|
63
|
+
store = GraphStore(db_path)
|
|
64
|
+
store.write(nodes, edges, wipe=wipe)
|
|
65
|
+
resolved = store.resolve_symbols()
|
|
66
|
+
store.close()
|
|
67
|
+
|
|
68
|
+
print(f"OK: nodes={len(nodes)} edges={len(edges)} resolved={resolved} db={db_path}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@cli.command("build-lancedb")
|
|
72
|
+
@repo_option
|
|
73
|
+
@click.option(
|
|
74
|
+
"--sqlite",
|
|
75
|
+
default=None,
|
|
76
|
+
type=click.Path(),
|
|
77
|
+
show_default=False,
|
|
78
|
+
help="Path to graph.sqlite (default: <repo>/.pycodekg/graph.sqlite).",
|
|
79
|
+
)
|
|
80
|
+
@click.option(
|
|
81
|
+
"--lancedb",
|
|
82
|
+
default=None,
|
|
83
|
+
type=click.Path(),
|
|
84
|
+
show_default=False,
|
|
85
|
+
help="Directory for LanceDB (default: <repo>/.pycodekg/lancedb).",
|
|
86
|
+
)
|
|
87
|
+
@click.option(
|
|
88
|
+
"--table",
|
|
89
|
+
default="pycodekg_nodes",
|
|
90
|
+
show_default=True,
|
|
91
|
+
help="LanceDB table name.",
|
|
92
|
+
)
|
|
93
|
+
@click.option(
|
|
94
|
+
"--model",
|
|
95
|
+
default=DEFAULT_MODEL,
|
|
96
|
+
show_default=True,
|
|
97
|
+
help="SentenceTransformer model name.",
|
|
98
|
+
)
|
|
99
|
+
@click.option("--wipe", is_flag=True, help="Delete existing vectors first.")
|
|
100
|
+
@click.option("-v", "--verbose", is_flag=True, help="Show LanceDB and embedder progress output.")
|
|
101
|
+
@click.option(
|
|
102
|
+
"--kinds",
|
|
103
|
+
default="module,class,function,method",
|
|
104
|
+
show_default=True,
|
|
105
|
+
help="Comma-separated node kinds to index.",
|
|
106
|
+
)
|
|
107
|
+
@click.option(
|
|
108
|
+
"--batch",
|
|
109
|
+
type=int,
|
|
110
|
+
default=256,
|
|
111
|
+
show_default=True,
|
|
112
|
+
help="Embedding batch size.",
|
|
113
|
+
)
|
|
114
|
+
def build_lancedb(
|
|
115
|
+
repo: str,
|
|
116
|
+
sqlite: str | None,
|
|
117
|
+
lancedb: str | None,
|
|
118
|
+
table: str,
|
|
119
|
+
model: str,
|
|
120
|
+
wipe: bool,
|
|
121
|
+
verbose: bool,
|
|
122
|
+
kinds: str,
|
|
123
|
+
batch: int,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Build a LanceDB semantic index from an existing pycodekg SQLite database."""
|
|
126
|
+
repo_root = Path(repo).resolve()
|
|
127
|
+
sqlite_path = Path(sqlite) if sqlite else repo_root / ".pycodekg" / "graph.sqlite"
|
|
128
|
+
lancedb_dir = Path(lancedb) if lancedb else repo_root / ".pycodekg" / "lancedb"
|
|
129
|
+
|
|
130
|
+
if not verbose:
|
|
131
|
+
suppress_ingestion_logging()
|
|
132
|
+
|
|
133
|
+
kinds_tuple = tuple(k.strip() for k in kinds.split(",") if k.strip())
|
|
134
|
+
embedder = SentenceTransformerEmbedder(model)
|
|
135
|
+
|
|
136
|
+
store = GraphStore(sqlite_path)
|
|
137
|
+
idx = SemanticIndex(
|
|
138
|
+
lancedb_dir,
|
|
139
|
+
embedder=embedder,
|
|
140
|
+
table=table,
|
|
141
|
+
index_kinds=kinds_tuple,
|
|
142
|
+
)
|
|
143
|
+
stats = idx.build(store, wipe=wipe, batch_size=batch, quiet=not verbose)
|
|
144
|
+
store.close()
|
|
145
|
+
|
|
146
|
+
print(
|
|
147
|
+
"OK:",
|
|
148
|
+
f"indexed_rows={stats['indexed_rows']}",
|
|
149
|
+
f"embedder={model}",
|
|
150
|
+
f"dim={stats['dim']}",
|
|
151
|
+
f"table={stats['table']}",
|
|
152
|
+
f"lancedb_dir={stats['lancedb_dir']}",
|
|
153
|
+
f"kinds={','.join(stats['kinds'])}",
|
|
154
|
+
)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cmd_build_full.py
|
|
3
|
+
|
|
4
|
+
Click subcommand for building the full PyCodeKG pipeline in one step:
|
|
5
|
+
|
|
6
|
+
build - repo -> AST -> graph store -> vector index (full pipeline, always wipes)
|
|
7
|
+
update - repo -> AST -> graph store -> vector index (incremental upsert, no wipe)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
|
|
17
|
+
from pycode_kg.cli.main import cli
|
|
18
|
+
from pycode_kg.cli.options import (
|
|
19
|
+
exclude_option,
|
|
20
|
+
include_option,
|
|
21
|
+
model_option,
|
|
22
|
+
repo_option,
|
|
23
|
+
)
|
|
24
|
+
from pycode_kg.config import load_exclude_dirs, load_include_dirs
|
|
25
|
+
from pycode_kg.graph import CodeGraph
|
|
26
|
+
from pycode_kg.index import (
|
|
27
|
+
SemanticIndex,
|
|
28
|
+
SentenceTransformerEmbedder,
|
|
29
|
+
suppress_ingestion_logging,
|
|
30
|
+
)
|
|
31
|
+
from pycode_kg.store import GraphStore
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _banner(repo_root: Path, label: str) -> None:
|
|
35
|
+
"""Print the build header banner."""
|
|
36
|
+
click.echo()
|
|
37
|
+
click.echo(f" PyCodeKG {label}")
|
|
38
|
+
click.echo(" repo - AST - graph store - vector index")
|
|
39
|
+
click.echo(f" repo {repo_root}")
|
|
40
|
+
click.echo()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _step_result(n: int, label: str, pairs: list[tuple[str, str]], elapsed: float) -> None:
|
|
44
|
+
"""Print a single-line step result after the step completes."""
|
|
45
|
+
kv = " ".join(f"{k}={v}" for k, v in pairs)
|
|
46
|
+
click.echo(f" Step {n} {label} {kv} ({elapsed:.1f}s)")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _done(elapsed: float) -> None:
|
|
50
|
+
"""Print a compact build-complete summary."""
|
|
51
|
+
click.echo()
|
|
52
|
+
click.echo(f" done ({elapsed:.1f}s)")
|
|
53
|
+
click.echo()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _common_options(fn):
|
|
57
|
+
"""Shared Click options for build and update commands."""
|
|
58
|
+
fn = repo_option(fn)
|
|
59
|
+
fn = click.option(
|
|
60
|
+
"--db",
|
|
61
|
+
default=None,
|
|
62
|
+
type=click.Path(),
|
|
63
|
+
show_default=False,
|
|
64
|
+
help="SQLite database path (default: <repo>/.pycodekg/graph.sqlite).",
|
|
65
|
+
)(fn)
|
|
66
|
+
fn = click.option(
|
|
67
|
+
"--lancedb",
|
|
68
|
+
default=None,
|
|
69
|
+
type=click.Path(),
|
|
70
|
+
show_default=False,
|
|
71
|
+
help="LanceDB directory path (default: <repo>/.pycodekg/lancedb).",
|
|
72
|
+
)(fn)
|
|
73
|
+
fn = click.option(
|
|
74
|
+
"--table",
|
|
75
|
+
default="pycodekg_nodes",
|
|
76
|
+
show_default=True,
|
|
77
|
+
help="LanceDB table name.",
|
|
78
|
+
)(fn)
|
|
79
|
+
fn = model_option(fn)
|
|
80
|
+
fn = click.option(
|
|
81
|
+
"-v", "--verbose", is_flag=True, help="Show LanceDB and embedder progress output."
|
|
82
|
+
)(fn)
|
|
83
|
+
fn = click.option(
|
|
84
|
+
"--kinds",
|
|
85
|
+
default="module,class,function,method",
|
|
86
|
+
show_default=True,
|
|
87
|
+
help="Comma-separated node kinds to index.",
|
|
88
|
+
)(fn)
|
|
89
|
+
fn = click.option(
|
|
90
|
+
"--batch",
|
|
91
|
+
type=int,
|
|
92
|
+
default=256,
|
|
93
|
+
show_default=True,
|
|
94
|
+
help="Embedding batch size.",
|
|
95
|
+
)(fn)
|
|
96
|
+
fn = include_option(fn)
|
|
97
|
+
fn = exclude_option(fn)
|
|
98
|
+
return fn
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _run_pipeline(
|
|
102
|
+
repo: str,
|
|
103
|
+
db: str | None,
|
|
104
|
+
lancedb: str | None,
|
|
105
|
+
table: str,
|
|
106
|
+
model: str,
|
|
107
|
+
verbose: bool,
|
|
108
|
+
kinds: str,
|
|
109
|
+
batch: int,
|
|
110
|
+
include_dir: tuple[str, ...],
|
|
111
|
+
exclude_dir: tuple[str, ...],
|
|
112
|
+
*,
|
|
113
|
+
wipe: bool,
|
|
114
|
+
label: str,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Execute the full build pipeline (shared by build and update)."""
|
|
117
|
+
repo_root = Path(repo).resolve()
|
|
118
|
+
db_path = Path(db) if db else repo_root / ".pycodekg" / "graph.sqlite"
|
|
119
|
+
lancedb_dir = Path(lancedb) if lancedb else repo_root / ".pycodekg" / "lancedb"
|
|
120
|
+
|
|
121
|
+
include = load_include_dirs(repo_root) | set(include_dir)
|
|
122
|
+
exclude = load_exclude_dirs(repo_root) | set(exclude_dir)
|
|
123
|
+
|
|
124
|
+
_banner(repo_root, label)
|
|
125
|
+
t_total = time.monotonic()
|
|
126
|
+
|
|
127
|
+
# Step 1: Build graph store
|
|
128
|
+
t1 = time.monotonic()
|
|
129
|
+
graph = CodeGraph(
|
|
130
|
+
repo_root,
|
|
131
|
+
include=include if include else None,
|
|
132
|
+
exclude=exclude if exclude else None,
|
|
133
|
+
)
|
|
134
|
+
nodes, edges = graph.extract().result()
|
|
135
|
+
|
|
136
|
+
store = GraphStore(db_path)
|
|
137
|
+
store.write(nodes, edges, wipe=wipe)
|
|
138
|
+
resolved = store.resolve_symbols()
|
|
139
|
+
store.close()
|
|
140
|
+
|
|
141
|
+
_step_result(
|
|
142
|
+
1,
|
|
143
|
+
"graph store",
|
|
144
|
+
[
|
|
145
|
+
("nodes", str(len(nodes))),
|
|
146
|
+
("edges", str(len(edges))),
|
|
147
|
+
("resolved", str(resolved)),
|
|
148
|
+
],
|
|
149
|
+
elapsed=time.monotonic() - t1,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Step 2: Build vector index
|
|
153
|
+
if not verbose:
|
|
154
|
+
suppress_ingestion_logging()
|
|
155
|
+
|
|
156
|
+
t2 = time.monotonic()
|
|
157
|
+
kinds_tuple = tuple(k.strip() for k in kinds.split(",") if k.strip())
|
|
158
|
+
embedder = SentenceTransformerEmbedder(model)
|
|
159
|
+
|
|
160
|
+
store = GraphStore(db_path)
|
|
161
|
+
idx = SemanticIndex(
|
|
162
|
+
lancedb_dir,
|
|
163
|
+
embedder=embedder,
|
|
164
|
+
table=table,
|
|
165
|
+
index_kinds=kinds_tuple,
|
|
166
|
+
)
|
|
167
|
+
stats = idx.build(store, wipe=wipe, batch_size=batch, quiet=not verbose)
|
|
168
|
+
store.close()
|
|
169
|
+
|
|
170
|
+
_step_result(
|
|
171
|
+
2,
|
|
172
|
+
"vector index",
|
|
173
|
+
[
|
|
174
|
+
("indexed", str(stats["indexed_rows"])),
|
|
175
|
+
("model", Path(model).name),
|
|
176
|
+
],
|
|
177
|
+
elapsed=time.monotonic() - t2,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
_done(elapsed=time.monotonic() - t_total)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@cli.command("build")
|
|
184
|
+
@_common_options
|
|
185
|
+
def build(
|
|
186
|
+
repo: str,
|
|
187
|
+
db: str | None,
|
|
188
|
+
lancedb: str | None,
|
|
189
|
+
table: str,
|
|
190
|
+
model: str,
|
|
191
|
+
verbose: bool,
|
|
192
|
+
kinds: str,
|
|
193
|
+
batch: int,
|
|
194
|
+
include_dir: tuple[str, ...],
|
|
195
|
+
exclude_dir: tuple[str, ...],
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Build knowledge graph from scratch: wipes existing data, then extracts AST -> graph store -> vector index."""
|
|
198
|
+
_run_pipeline(
|
|
199
|
+
repo,
|
|
200
|
+
db,
|
|
201
|
+
lancedb,
|
|
202
|
+
table,
|
|
203
|
+
model,
|
|
204
|
+
verbose,
|
|
205
|
+
kinds,
|
|
206
|
+
batch,
|
|
207
|
+
include_dir,
|
|
208
|
+
exclude_dir,
|
|
209
|
+
wipe=True,
|
|
210
|
+
label="Full Build",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@cli.command("update")
|
|
215
|
+
@_common_options
|
|
216
|
+
def update(
|
|
217
|
+
repo: str,
|
|
218
|
+
db: str | None,
|
|
219
|
+
lancedb: str | None,
|
|
220
|
+
table: str,
|
|
221
|
+
model: str,
|
|
222
|
+
verbose: bool,
|
|
223
|
+
kinds: str,
|
|
224
|
+
batch: int,
|
|
225
|
+
include_dir: tuple[str, ...],
|
|
226
|
+
exclude_dir: tuple[str, ...],
|
|
227
|
+
) -> None:
|
|
228
|
+
"""Update knowledge graph incrementally: upserts changes without wiping existing data."""
|
|
229
|
+
_run_pipeline(
|
|
230
|
+
repo,
|
|
231
|
+
db,
|
|
232
|
+
lancedb,
|
|
233
|
+
table,
|
|
234
|
+
model,
|
|
235
|
+
verbose,
|
|
236
|
+
kinds,
|
|
237
|
+
batch,
|
|
238
|
+
include_dir,
|
|
239
|
+
exclude_dir,
|
|
240
|
+
wipe=False,
|
|
241
|
+
label="Update",
|
|
242
|
+
)
|