tscode-kg 0.2.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.
- tscode_kg/__init__.py +42 -0
- tscode_kg/__main__.py +6 -0
- tscode_kg/analysis.py +1829 -0
- tscode_kg/app.py +1355 -0
- tscode_kg/bridge.py +114 -0
- tscode_kg/centrality.py +434 -0
- tscode_kg/cli/__init__.py +1 -0
- tscode_kg/cli/cmd_analyze.py +69 -0
- tscode_kg/cli/cmd_bridges.py +38 -0
- tscode_kg/cli/cmd_build.py +86 -0
- tscode_kg/cli/cmd_centrality.py +124 -0
- tscode_kg/cli/cmd_explain.py +58 -0
- tscode_kg/cli/cmd_framework_nodes.py +43 -0
- tscode_kg/cli/cmd_hooks.py +125 -0
- tscode_kg/cli/cmd_init.py +234 -0
- tscode_kg/cli/cmd_mcp.py +35 -0
- tscode_kg/cli/cmd_model.py +52 -0
- tscode_kg/cli/cmd_query.py +75 -0
- tscode_kg/cli/cmd_snapshot.py +431 -0
- tscode_kg/cli/cmd_viz.py +175 -0
- tscode_kg/cli/main.py +56 -0
- tscode_kg/coderank.py +564 -0
- tscode_kg/config.py +36 -0
- tscode_kg/explain.py +270 -0
- tscode_kg/extractor.py +827 -0
- tscode_kg/framework_detector.py +106 -0
- tscode_kg/kg.py +193 -0
- tscode_kg/layout3d.py +492 -0
- tscode_kg/mcp_server.py +1412 -0
- tscode_kg/snapshots.py +64 -0
- tscode_kg/viz3d.py +1457 -0
- tscode_kg/viz3d_timeline.py +369 -0
- tscode_kg-0.2.0.dist-info/METADATA +196 -0
- tscode_kg-0.2.0.dist-info/RECORD +37 -0
- tscode_kg-0.2.0.dist-info/WHEEL +4 -0
- tscode_kg-0.2.0.dist-info/entry_points.txt +15 -0
- tscode_kg-0.2.0.dist-info/licenses/LICENSE +24 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cli/cmd_build.py — tscodekg build command.
|
|
3
|
+
|
|
4
|
+
Builds the SQLite graph and sqlite-vec vector index from a TypeScript/JS repo.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.command("build")
|
|
18
|
+
@click.option("--repo", default=".", show_default=True, help="Repository root directory.")
|
|
19
|
+
@click.option(
|
|
20
|
+
"--db",
|
|
21
|
+
default=None,
|
|
22
|
+
help="SQLite database path (default: <repo>/.tscodekg/graph.sqlite).",
|
|
23
|
+
)
|
|
24
|
+
@click.option(
|
|
25
|
+
"--vectors",
|
|
26
|
+
default=None,
|
|
27
|
+
help="sqlite-vec store path (default: <repo>/.tscodekg/vectors.sqlite).",
|
|
28
|
+
)
|
|
29
|
+
@click.option("--wipe", is_flag=True, default=False, help="Clear existing data before building.")
|
|
30
|
+
@click.option(
|
|
31
|
+
"--graph-only",
|
|
32
|
+
is_flag=True,
|
|
33
|
+
default=False,
|
|
34
|
+
help="Build SQLite graph only; skip vector index.",
|
|
35
|
+
)
|
|
36
|
+
@click.option(
|
|
37
|
+
"--index-only",
|
|
38
|
+
is_flag=True,
|
|
39
|
+
default=False,
|
|
40
|
+
help="Build vector index only; graph must already exist.",
|
|
41
|
+
)
|
|
42
|
+
def build(
|
|
43
|
+
repo: str,
|
|
44
|
+
db: str | None,
|
|
45
|
+
vectors: str | None,
|
|
46
|
+
wipe: bool,
|
|
47
|
+
graph_only: bool,
|
|
48
|
+
index_only: bool,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Build the TypeScript/JS knowledge graph for a repository."""
|
|
51
|
+
from tscode_kg.kg import TypeScriptKG # pylint: disable=import-outside-toplevel
|
|
52
|
+
|
|
53
|
+
repo_path = Path(repo).resolve()
|
|
54
|
+
if not repo_path.is_dir():
|
|
55
|
+
console.print(f"[red]Error:[/red] Repository not found: {repo_path}")
|
|
56
|
+
raise SystemExit(1)
|
|
57
|
+
|
|
58
|
+
kg = TypeScriptKG(
|
|
59
|
+
repo_root=repo_path,
|
|
60
|
+
db_path=db,
|
|
61
|
+
vectors_path=vectors,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
console.print("[bold]TypeScriptKG build[/bold]")
|
|
65
|
+
console.print(f" repo : {repo_path}")
|
|
66
|
+
console.print(f" db : {kg.db_path}")
|
|
67
|
+
console.print(f" vectors : {kg.vectors_path}")
|
|
68
|
+
console.print(f" wipe : {wipe}")
|
|
69
|
+
console.print()
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
if index_only:
|
|
73
|
+
console.print("[cyan]Building vector index...[/cyan]")
|
|
74
|
+
stats = kg.build_index(wipe=wipe)
|
|
75
|
+
elif graph_only:
|
|
76
|
+
console.print("[cyan]Building SQLite graph...[/cyan]")
|
|
77
|
+
stats = kg.build_graph(wipe=wipe)
|
|
78
|
+
else:
|
|
79
|
+
console.print("[cyan]Building graph + vector index...[/cyan]")
|
|
80
|
+
stats = kg.build(wipe=wipe)
|
|
81
|
+
|
|
82
|
+
console.print("[green]Done.[/green]")
|
|
83
|
+
console.print(str(stats))
|
|
84
|
+
except Exception as exc: # pylint: disable=broad-except
|
|
85
|
+
console.print(f"[red]Build failed:[/red] {exc}")
|
|
86
|
+
raise SystemExit(1) from exc
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cli/cmd_centrality.py — tscodekg centrality command.
|
|
3
|
+
|
|
4
|
+
Structural Importance Ranking (SIR) over the TypeScriptKG graph.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.command("centrality")
|
|
16
|
+
@click.option(
|
|
17
|
+
"--db",
|
|
18
|
+
type=click.Path(path_type=Path, dir_okay=False),
|
|
19
|
+
default=Path(".tscodekg/graph.sqlite"),
|
|
20
|
+
show_default=True,
|
|
21
|
+
help="Path to the TypeScriptKG SQLite graph.",
|
|
22
|
+
)
|
|
23
|
+
@click.option(
|
|
24
|
+
"--kind",
|
|
25
|
+
"kinds",
|
|
26
|
+
multiple=True,
|
|
27
|
+
type=click.Choice(["module", "class", "interface", "function", "method"], case_sensitive=False),
|
|
28
|
+
help="Restrict output to one or more node kinds.",
|
|
29
|
+
)
|
|
30
|
+
@click.option("--top", type=int, default=25, show_default=True, help="Maximum rows to show.")
|
|
31
|
+
@click.option(
|
|
32
|
+
"--group-by",
|
|
33
|
+
type=click.Choice(["node", "module"], case_sensitive=False),
|
|
34
|
+
default="node",
|
|
35
|
+
show_default=True,
|
|
36
|
+
help="Return node-level or module-level rankings.",
|
|
37
|
+
)
|
|
38
|
+
@click.option("--json", "as_json", is_flag=True, help="Emit JSON instead of a table.")
|
|
39
|
+
@click.option("--write-db", is_flag=True, help="Persist node-level scores into centrality_scores.")
|
|
40
|
+
def centrality(
|
|
41
|
+
db: Path,
|
|
42
|
+
kinds: tuple[str, ...],
|
|
43
|
+
top: int,
|
|
44
|
+
group_by: str,
|
|
45
|
+
as_json: bool,
|
|
46
|
+
write_db: bool,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Compute Structural Importance Ranking over the resolved TypeScriptKG graph."""
|
|
49
|
+
from tscode_kg.centrality import ( # pylint: disable=import-outside-toplevel
|
|
50
|
+
StructuralImportanceRanker,
|
|
51
|
+
aggregate_module_scores,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
ranker = StructuralImportanceRanker(db)
|
|
55
|
+
records = ranker.compute(kinds=set(kinds) if kinds else None, top=None)
|
|
56
|
+
|
|
57
|
+
if write_db:
|
|
58
|
+
ranker.write_scores(records)
|
|
59
|
+
|
|
60
|
+
if group_by == "module":
|
|
61
|
+
payload = aggregate_module_scores(records)[:top]
|
|
62
|
+
if as_json:
|
|
63
|
+
click.echo(json.dumps(payload, indent=2))
|
|
64
|
+
return
|
|
65
|
+
_print_module_table(payload)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
node_payload = records[:top]
|
|
69
|
+
if as_json:
|
|
70
|
+
click.echo(json.dumps([_record_to_dict(r) for r in node_payload], indent=2))
|
|
71
|
+
return
|
|
72
|
+
_print_node_table(node_payload)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _record_to_dict(record) -> dict:
|
|
76
|
+
return {
|
|
77
|
+
"node_id": record.node_id,
|
|
78
|
+
"kind": record.kind,
|
|
79
|
+
"name": record.name,
|
|
80
|
+
"module_path": record.module_path,
|
|
81
|
+
"score": record.score,
|
|
82
|
+
"rank": record.rank,
|
|
83
|
+
"inbound_count": record.inbound_count,
|
|
84
|
+
"cross_module_inbound": record.cross_module_inbound,
|
|
85
|
+
"rel_breakdown": record.rel_breakdown,
|
|
86
|
+
"top_contributors": record.top_contributors,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _print_node_table(records) -> None:
|
|
91
|
+
headers = ("Rank", "Score", "Kind", "Name", "Inbound", "XMod", "Module")
|
|
92
|
+
rows = [
|
|
93
|
+
(
|
|
94
|
+
r.rank,
|
|
95
|
+
f"{r.score:.6f}",
|
|
96
|
+
r.kind,
|
|
97
|
+
r.name,
|
|
98
|
+
r.inbound_count,
|
|
99
|
+
r.cross_module_inbound,
|
|
100
|
+
r.module_path or "",
|
|
101
|
+
)
|
|
102
|
+
for r in records
|
|
103
|
+
]
|
|
104
|
+
click.echo(_format_table(headers, rows))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _print_module_table(payload) -> None:
|
|
108
|
+
headers = ("Rank", "Score", "Members", "Module")
|
|
109
|
+
rows = [
|
|
110
|
+
(row["rank"], f"{row['score']:.6f}", row["member_count"], row["module_path"])
|
|
111
|
+
for row in payload
|
|
112
|
+
]
|
|
113
|
+
click.echo(_format_table(headers, rows))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _format_table(headers, rows) -> str:
|
|
117
|
+
widths = [len(str(h)) for h in headers]
|
|
118
|
+
for row in rows:
|
|
119
|
+
for i, value in enumerate(row):
|
|
120
|
+
widths[i] = max(widths[i], len(str(value)))
|
|
121
|
+
fmt = " ".join(f"{{:{w}}}" for w in widths)
|
|
122
|
+
lines = [fmt.format(*headers), fmt.format(*["-" * w for w in widths])]
|
|
123
|
+
lines.extend(fmt.format(*[str(v) for v in row]) for row in rows)
|
|
124
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cli/cmd_explain.py — tscodekg explain command.
|
|
3
|
+
|
|
4
|
+
explain — get a natural-language explanation of a code node by its ID
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command("explain")
|
|
15
|
+
@click.argument("node_id", metavar="NODE_ID")
|
|
16
|
+
@click.option("--repo", default=".", show_default=True, help="Repository root.")
|
|
17
|
+
@click.option(
|
|
18
|
+
"--db",
|
|
19
|
+
default=None,
|
|
20
|
+
type=click.Path(),
|
|
21
|
+
help="SQLite knowledge graph path (default: <repo>/.tscodekg/graph.sqlite).",
|
|
22
|
+
)
|
|
23
|
+
@click.option(
|
|
24
|
+
"--out",
|
|
25
|
+
type=click.Path(),
|
|
26
|
+
default=None,
|
|
27
|
+
help="Output file path (default: stdout).",
|
|
28
|
+
)
|
|
29
|
+
def explain(node_id: str, repo: str, db: str | None, out: str | None) -> None:
|
|
30
|
+
"""Get a natural-language explanation of a code node.
|
|
31
|
+
|
|
32
|
+
NODE_ID is the stable identifier of a node, e.g.:
|
|
33
|
+
fn:src/utils/helpers.ts:formatDate
|
|
34
|
+
"""
|
|
35
|
+
from tscode_kg.explain import render_explain # pylint: disable=import-outside-toplevel
|
|
36
|
+
from tscode_kg.kg import TypeScriptKG # pylint: disable=import-outside-toplevel
|
|
37
|
+
|
|
38
|
+
kg = TypeScriptKG(repo_root=Path(repo).resolve(), db_path=db)
|
|
39
|
+
|
|
40
|
+
# Fail fast for scripting: distinguish missing-node from rendered output.
|
|
41
|
+
if kg.node(node_id) is None:
|
|
42
|
+
click.echo(f"[ERROR] Node not found: {node_id}", err=True)
|
|
43
|
+
kg.close()
|
|
44
|
+
raise SystemExit(1)
|
|
45
|
+
|
|
46
|
+
markdown_output = render_explain(
|
|
47
|
+
kg,
|
|
48
|
+
node_id,
|
|
49
|
+
snippets_hint="tscodekg pack",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if out:
|
|
53
|
+
Path(out).write_text(markdown_output)
|
|
54
|
+
click.echo(f"[OK] Explanation written to {out}")
|
|
55
|
+
else:
|
|
56
|
+
click.echo(markdown_output)
|
|
57
|
+
|
|
58
|
+
kg.close()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cli/cmd_framework_nodes.py — tscodekg framework-nodes command.
|
|
3
|
+
|
|
4
|
+
Framework-like hub module detection over the TypeScriptKG graph.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command("framework-nodes")
|
|
15
|
+
@click.option(
|
|
16
|
+
"--db",
|
|
17
|
+
type=click.Path(path_type=Path, dir_okay=False),
|
|
18
|
+
default=Path(".tscodekg/graph.sqlite"),
|
|
19
|
+
show_default=True,
|
|
20
|
+
help="Path to the TypeScriptKG SQLite graph.",
|
|
21
|
+
)
|
|
22
|
+
@click.option("--top", type=int, default=25, show_default=True, help="Number of top modules.")
|
|
23
|
+
def framework_nodes(db: Path, top: int) -> None:
|
|
24
|
+
"""Show top framework-like modules (high SIR + high connectivity)."""
|
|
25
|
+
from tscode_kg.bridge import ( # pylint: disable=import-outside-toplevel
|
|
26
|
+
compute_bridge_centrality,
|
|
27
|
+
)
|
|
28
|
+
from tscode_kg.centrality import ( # pylint: disable=import-outside-toplevel
|
|
29
|
+
StructuralImportanceRanker,
|
|
30
|
+
)
|
|
31
|
+
from tscode_kg.framework_detector import ( # pylint: disable=import-outside-toplevel
|
|
32
|
+
detect_framework_nodes,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Both metrics must be persisted before detection can combine them.
|
|
36
|
+
ranker = StructuralImportanceRanker(str(db))
|
|
37
|
+
ranker.write_scores(ranker.compute(), metric="sir_pagerank")
|
|
38
|
+
compute_bridge_centrality(kind="module", include_imports=True, top=top, db_path=str(db))
|
|
39
|
+
|
|
40
|
+
nodes = detect_framework_nodes(limit=top, db_path=str(db))
|
|
41
|
+
click.echo(f"Top {top} framework-like modules:")
|
|
42
|
+
for node_id, score, label in nodes:
|
|
43
|
+
click.echo(f"{label:50s} {score:.5f} ({node_id})")
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cli/cmd_hooks.py — tscodekg install-hooks command.
|
|
3
|
+
|
|
4
|
+
install-hooks — install the pre-commit snapshot hook into .git/hooks/
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import stat
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Hook script content (embedded so this module is self-contained when
|
|
16
|
+
# installed as a package in any repo, not just tscode_kg itself)
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
_PRE_COMMIT_HOOK = """\
|
|
20
|
+
#!/usr/bin/env bash
|
|
21
|
+
# TypeScriptKG pre-commit hook — keeps the local index in sync and captures
|
|
22
|
+
# metrics snapshots BEFORE quality checks run.
|
|
23
|
+
# Installed by: tscodekg install-hooks
|
|
24
|
+
# Skip with: TSCODEKG_SKIP_SNAPSHOT=1 git commit ...
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
|
|
27
|
+
[ "${TSCODEKG_SKIP_SNAPSHOT:-0}" = "1" ] && exit 0
|
|
28
|
+
|
|
29
|
+
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
30
|
+
cd "$REPO_ROOT"
|
|
31
|
+
|
|
32
|
+
# Resolve the tscodekg binary: prefer the repo's .venv, fall back to PATH.
|
|
33
|
+
if [ -x "$REPO_ROOT/.venv/bin/tscodekg" ]; then
|
|
34
|
+
TSCODEKG="$REPO_ROOT/.venv/bin/tscodekg"
|
|
35
|
+
elif command -v tscodekg &>/dev/null; then
|
|
36
|
+
TSCODEKG="tscodekg"
|
|
37
|
+
else
|
|
38
|
+
echo "[tscodekg] binary not found — skipping snapshot hook" >&2
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Capture the tree hash of the staged index NOW — before any tool modifies files.
|
|
43
|
+
TREE_HASH=$(git write-tree)
|
|
44
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
45
|
+
|
|
46
|
+
# Rebuild the local index to keep it in sync with staged content.
|
|
47
|
+
"$TSCODEKG" build --repo "$REPO_ROOT" || exit 1
|
|
48
|
+
|
|
49
|
+
# Snapshot TypeScriptKG (version auto-detected from installed package).
|
|
50
|
+
"$TSCODEKG" snapshot save \\
|
|
51
|
+
--repo . \\
|
|
52
|
+
--tree-hash "$TREE_HASH" \\
|
|
53
|
+
--branch "$BRANCH" \\
|
|
54
|
+
|| { echo "[tscodekg] snapshot skipped (run 'tscodekg build' to initialize)" >&2; }
|
|
55
|
+
|
|
56
|
+
# Stage the snapshot directory so it is included in the commit.
|
|
57
|
+
git add .tscodekg/snapshots/ 2>/dev/null || true
|
|
58
|
+
|
|
59
|
+
# Run pre-commit framework checks (ruff, ty, detect-secrets, etc.) AFTER
|
|
60
|
+
# snapshots are captured and staged. Delegates to .pre-commit-config.yaml so
|
|
61
|
+
# quality checks stay in one place.
|
|
62
|
+
PRECOMMIT="$REPO_ROOT/.venv/bin/pre-commit"
|
|
63
|
+
if [ -x "$PRECOMMIT" ]; then
|
|
64
|
+
"$PRECOMMIT" run || exit 1
|
|
65
|
+
elif command -v pre-commit &>/dev/null; then
|
|
66
|
+
pre-commit run || exit 1
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
exit 0
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@click.command("install-hooks")
|
|
74
|
+
@click.option(
|
|
75
|
+
"--repo",
|
|
76
|
+
default=".",
|
|
77
|
+
type=click.Path(exists=True),
|
|
78
|
+
show_default=True,
|
|
79
|
+
help="Repository root.",
|
|
80
|
+
)
|
|
81
|
+
@click.option(
|
|
82
|
+
"--force",
|
|
83
|
+
is_flag=True,
|
|
84
|
+
help="Overwrite an existing pre-commit hook.",
|
|
85
|
+
)
|
|
86
|
+
def install_hooks(repo: str, force: bool) -> None:
|
|
87
|
+
"""Install the TypeScriptKG pre-commit git hook.
|
|
88
|
+
|
|
89
|
+
After installation, before each commit:
|
|
90
|
+
|
|
91
|
+
\b
|
|
92
|
+
1. Rebuilds the local TypeScriptKG index (full wipe)
|
|
93
|
+
2. Captures a metrics snapshot keyed by tree hash
|
|
94
|
+
3. Stages the snapshot directory atomically
|
|
95
|
+
4. Runs the pre-commit framework checks
|
|
96
|
+
|
|
97
|
+
This keeps the index in sync and ensures snapshots reflect the state of
|
|
98
|
+
the knowledge graph at commit time.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
tscodekg install-hooks --repo .
|
|
102
|
+
"""
|
|
103
|
+
repo_root = Path(repo).resolve()
|
|
104
|
+
git_dir = repo_root / ".git"
|
|
105
|
+
|
|
106
|
+
if not git_dir.is_dir():
|
|
107
|
+
click.echo(f"Error: {repo_root} is not a git repository.", err=True)
|
|
108
|
+
raise SystemExit(1)
|
|
109
|
+
|
|
110
|
+
hooks_dir = git_dir / "hooks"
|
|
111
|
+
hooks_dir.mkdir(exist_ok=True)
|
|
112
|
+
hook_path = hooks_dir / "pre-commit"
|
|
113
|
+
|
|
114
|
+
if hook_path.exists() and not force:
|
|
115
|
+
click.echo(f"Hook already exists: {hook_path}")
|
|
116
|
+
click.echo("Use --force to overwrite.")
|
|
117
|
+
raise SystemExit(1)
|
|
118
|
+
|
|
119
|
+
hook_path.write_text(_PRE_COMMIT_HOOK)
|
|
120
|
+
mode = hook_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
|
121
|
+
hook_path.chmod(mode)
|
|
122
|
+
|
|
123
|
+
click.echo(f"OK Installed pre-commit hook: {hook_path}")
|
|
124
|
+
click.echo(" Snapshots will be captured automatically before each commit.")
|
|
125
|
+
click.echo(" Run 'tscodekg build' first if you haven't built the graph yet.")
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cli/cmd_init.py — tscodekg init command.
|
|
3
|
+
|
|
4
|
+
init — download model, build graph, install hooks, capture snapshot
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import stat
|
|
10
|
+
import subprocess
|
|
11
|
+
import time
|
|
12
|
+
import tomllib
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
|
|
17
|
+
from tscode_kg.cli.cmd_hooks import _PRE_COMMIT_HOOK
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _has_tscodekg_config(repo_root: Path) -> bool:
|
|
21
|
+
"""Check whether pyproject.toml already has a [tool.tscodekg] section.
|
|
22
|
+
|
|
23
|
+
:param repo_root: Repository root directory.
|
|
24
|
+
:return: True when the section exists.
|
|
25
|
+
"""
|
|
26
|
+
pyproject = repo_root / "pyproject.toml"
|
|
27
|
+
if not pyproject.exists():
|
|
28
|
+
return False
|
|
29
|
+
try:
|
|
30
|
+
with open(pyproject, "rb") as f:
|
|
31
|
+
data = tomllib.load(f)
|
|
32
|
+
return "tscodekg" in data.get("tool", {})
|
|
33
|
+
except (OSError, ValueError):
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _scaffold_tscodekg_config(repo_root: Path) -> bool:
|
|
38
|
+
"""Append a minimal [tool.tscodekg] section to pyproject.toml if missing.
|
|
39
|
+
|
|
40
|
+
Detects the most likely source directory (``src``, ``lib``, or ``app``)
|
|
41
|
+
and sets it as the include list.
|
|
42
|
+
|
|
43
|
+
:param repo_root: Repository root directory.
|
|
44
|
+
:return: True if the section was added, False if skipped.
|
|
45
|
+
"""
|
|
46
|
+
pyproject = repo_root / "pyproject.toml"
|
|
47
|
+
if not pyproject.exists():
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
candidates = ["src", "lib", "app"]
|
|
51
|
+
include_dirs: list[str] = [d for d in candidates if (repo_root / d).is_dir()]
|
|
52
|
+
|
|
53
|
+
lines = [
|
|
54
|
+
"",
|
|
55
|
+
"[tool.tscodekg]",
|
|
56
|
+
"# Directories to include in the knowledge graph build and analysis.",
|
|
57
|
+
"# When unset, all directories are indexed.",
|
|
58
|
+
]
|
|
59
|
+
if include_dirs:
|
|
60
|
+
include_str = ", ".join(f'"{d}"' for d in include_dirs)
|
|
61
|
+
lines.append(f"include = [{include_str}]")
|
|
62
|
+
else:
|
|
63
|
+
lines.append("# include = []")
|
|
64
|
+
|
|
65
|
+
pyproject.open("a").write("\n".join(lines) + "\n")
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@click.command("init")
|
|
70
|
+
@click.option(
|
|
71
|
+
"--repo",
|
|
72
|
+
default=".",
|
|
73
|
+
type=click.Path(exists=True),
|
|
74
|
+
show_default=True,
|
|
75
|
+
help="Repository root.",
|
|
76
|
+
)
|
|
77
|
+
@click.option(
|
|
78
|
+
"--model",
|
|
79
|
+
default=None,
|
|
80
|
+
help="SentenceTransformer model name (default: shared kg_utils default).",
|
|
81
|
+
)
|
|
82
|
+
@click.option("--skip-hooks", is_flag=True, help="Don't install the pre-commit git hook.")
|
|
83
|
+
@click.option("--skip-snapshot", is_flag=True, help="Don't capture an initial snapshot.")
|
|
84
|
+
@click.option("--force", is_flag=True, help="Overwrite existing graph data and hook.")
|
|
85
|
+
def init(
|
|
86
|
+
repo: str,
|
|
87
|
+
model: str | None,
|
|
88
|
+
skip_hooks: bool,
|
|
89
|
+
skip_snapshot: bool,
|
|
90
|
+
force: bool,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Initialize TypeScriptKG in a repository.
|
|
93
|
+
|
|
94
|
+
Downloads the embedding model, builds the knowledge graph (SQLite +
|
|
95
|
+
sqlite-vec), optionally installs the pre-commit hook, and captures an
|
|
96
|
+
initial snapshot. Designed to be idempotent — safe to run more than once.
|
|
97
|
+
|
|
98
|
+
Example::
|
|
99
|
+
|
|
100
|
+
tscodekg init --repo .
|
|
101
|
+
"""
|
|
102
|
+
from kg_utils.semantic import ( # pylint: disable=import-outside-toplevel
|
|
103
|
+
DEFAULT_MODEL,
|
|
104
|
+
_local_model_path,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
from tscode_kg.kg import TypeScriptKG # pylint: disable=import-outside-toplevel
|
|
108
|
+
|
|
109
|
+
repo_root = Path(repo).resolve()
|
|
110
|
+
model = model or DEFAULT_MODEL
|
|
111
|
+
t_total = time.monotonic()
|
|
112
|
+
|
|
113
|
+
click.echo()
|
|
114
|
+
click.echo(" TypeScriptKG Init")
|
|
115
|
+
click.echo(f" repo {repo_root}")
|
|
116
|
+
click.echo()
|
|
117
|
+
|
|
118
|
+
# ------------------------------------------------------------------
|
|
119
|
+
# Step 0: Scaffold [tool.tscodekg] in pyproject.toml if missing
|
|
120
|
+
# ------------------------------------------------------------------
|
|
121
|
+
if not _has_tscodekg_config(repo_root):
|
|
122
|
+
if _scaffold_tscodekg_config(repo_root):
|
|
123
|
+
click.echo(" [0/4] Added [tool.tscodekg] section to pyproject.toml")
|
|
124
|
+
else:
|
|
125
|
+
click.echo(" [0/4] No pyproject.toml found — skipping config scaffold")
|
|
126
|
+
else:
|
|
127
|
+
click.echo(" [0/4] [tool.tscodekg] config already present")
|
|
128
|
+
|
|
129
|
+
# ------------------------------------------------------------------
|
|
130
|
+
# Step 1: Download the embedding model
|
|
131
|
+
# ------------------------------------------------------------------
|
|
132
|
+
click.echo()
|
|
133
|
+
local_path = _local_model_path(model)
|
|
134
|
+
|
|
135
|
+
if local_path.exists() and not force:
|
|
136
|
+
click.echo(f" [1/4] Model already cached at {local_path}")
|
|
137
|
+
else:
|
|
138
|
+
click.echo(f" [1/4] Downloading embedding model '{model}'...")
|
|
139
|
+
from sentence_transformers import ( # pylint: disable=import-outside-toplevel
|
|
140
|
+
SentenceTransformer,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
st_model = SentenceTransformer(model)
|
|
144
|
+
local_path.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
st_model.save(str(local_path))
|
|
146
|
+
click.echo(f" OK: model saved to {local_path}")
|
|
147
|
+
|
|
148
|
+
# ------------------------------------------------------------------
|
|
149
|
+
# Step 2: Build the knowledge graph (full wipe)
|
|
150
|
+
# ------------------------------------------------------------------
|
|
151
|
+
click.echo()
|
|
152
|
+
click.echo(" [2/4] Building knowledge graph...")
|
|
153
|
+
kg = TypeScriptKG(repo_root=repo_root, model=model)
|
|
154
|
+
try:
|
|
155
|
+
stats = kg.build(wipe=True)
|
|
156
|
+
click.echo(f" OK: {stats}")
|
|
157
|
+
finally:
|
|
158
|
+
kg.close()
|
|
159
|
+
|
|
160
|
+
# ------------------------------------------------------------------
|
|
161
|
+
# Step 3: Install pre-commit hook
|
|
162
|
+
# ------------------------------------------------------------------
|
|
163
|
+
click.echo()
|
|
164
|
+
if skip_hooks:
|
|
165
|
+
click.echo(" [3/4] Skipping hook installation (--skip-hooks)")
|
|
166
|
+
else:
|
|
167
|
+
git_dir = repo_root / ".git"
|
|
168
|
+
if not git_dir.is_dir():
|
|
169
|
+
click.echo(" [3/4] Not a git repository — skipping hook installation")
|
|
170
|
+
else:
|
|
171
|
+
hooks_dir = git_dir / "hooks"
|
|
172
|
+
hooks_dir.mkdir(exist_ok=True)
|
|
173
|
+
hook_path = hooks_dir / "pre-commit"
|
|
174
|
+
|
|
175
|
+
if hook_path.exists() and not force:
|
|
176
|
+
click.echo(f" [3/4] Hook already exists: {hook_path}")
|
|
177
|
+
click.echo(" Use --force to overwrite.")
|
|
178
|
+
else:
|
|
179
|
+
hook_path.write_text(_PRE_COMMIT_HOOK)
|
|
180
|
+
mode = hook_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
|
181
|
+
hook_path.chmod(mode)
|
|
182
|
+
click.echo(f" [3/4] OK: installed pre-commit hook at {hook_path}")
|
|
183
|
+
|
|
184
|
+
# ------------------------------------------------------------------
|
|
185
|
+
# Step 4: Capture initial snapshot
|
|
186
|
+
# ------------------------------------------------------------------
|
|
187
|
+
click.echo()
|
|
188
|
+
if skip_snapshot:
|
|
189
|
+
click.echo(" [4/4] Skipping initial snapshot (--skip-snapshot)")
|
|
190
|
+
else:
|
|
191
|
+
try:
|
|
192
|
+
tree_hash = (
|
|
193
|
+
subprocess.check_output(
|
|
194
|
+
["git", "rev-parse", "HEAD"],
|
|
195
|
+
cwd=str(repo_root),
|
|
196
|
+
stderr=subprocess.DEVNULL,
|
|
197
|
+
)
|
|
198
|
+
.decode()
|
|
199
|
+
.strip()
|
|
200
|
+
)
|
|
201
|
+
branch = (
|
|
202
|
+
subprocess.check_output(
|
|
203
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
204
|
+
cwd=str(repo_root),
|
|
205
|
+
stderr=subprocess.DEVNULL,
|
|
206
|
+
)
|
|
207
|
+
.decode()
|
|
208
|
+
.strip()
|
|
209
|
+
)
|
|
210
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
211
|
+
tree_hash = ""
|
|
212
|
+
branch = None
|
|
213
|
+
|
|
214
|
+
from tscode_kg.cli.cmd_snapshot import ( # pylint: disable=import-outside-toplevel
|
|
215
|
+
capture_snapshot,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
capture_snapshot(
|
|
220
|
+
version="",
|
|
221
|
+
repo=str(repo_root),
|
|
222
|
+
db=None,
|
|
223
|
+
snapshots_dir=None,
|
|
224
|
+
branch=branch,
|
|
225
|
+
tree_hash=tree_hash,
|
|
226
|
+
)
|
|
227
|
+
click.echo(" [4/4] OK: initial snapshot captured")
|
|
228
|
+
except Exception as exc: # noqa: BLE001
|
|
229
|
+
click.echo(f" [4/4] Snapshot skipped: {exc}")
|
|
230
|
+
|
|
231
|
+
elapsed = time.monotonic() - t_total
|
|
232
|
+
click.echo()
|
|
233
|
+
click.echo(f" Done in {elapsed:.1f}s — TypeScriptKG is ready.")
|
|
234
|
+
click.echo(' Try: tscodekg query "authentication middleware"')
|