code-review-graph 2.2.3__tar.gz → 2.2.4__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.
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/PKG-INFO +2 -2
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/cli.py +2 -1
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/benchmarks/search_quality.py +3 -1
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/runner.py +1 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/graph.py +6 -2
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/incremental.py +46 -4
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/main.py +54 -22
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/migrations.py +1 -1
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/parser.py +184 -1
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/registry.py +2 -2
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/context.py +9 -5
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/query.py +10 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tsconfig_resolver.py +1 -1
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/visualization.py +11 -3
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/wiki.py +21 -3
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/pyproject.toml +5 -2
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/.gitignore +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/LICENSE +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/README.md +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code-review-graph-vscode/LICENSE +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code-review-graph-vscode/README.md +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/__init__.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/__main__.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/changes.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/communities.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/constants.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/embeddings.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/__init__.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/benchmarks/__init__.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/benchmarks/build_performance.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/benchmarks/flow_completeness.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/benchmarks/impact_accuracy.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/benchmarks/token_efficiency.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/configs/express.yaml +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/configs/fastapi.yaml +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/configs/flask.yaml +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/configs/gin.yaml +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/configs/httpx.yaml +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/configs/nextjs.yaml +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/reporter.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/scorer.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/token_benchmark.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/flows.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/hints.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/prompts.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/refactor.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/search.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/skills.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/__init__.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/_common.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/build.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/community_tools.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/docs.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/flows_tools.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/refactor_tools.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/registry_tools.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/review.py +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/COMMANDS.md +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/FEATURES.md +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/INDEX.md +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/LEGAL.md +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/LLM-OPTIMIZED-REFERENCE.md +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/ROADMAP.md +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/TROUBLESHOOTING.md +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/USAGE.md +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/architecture.md +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/schema.md +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/hooks/hooks.json +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/hooks/session-start.sh +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/skills/build-graph/SKILL.md +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/skills/review-delta/SKILL.md +0 -0
- {code_review_graph-2.2.3 → code_review_graph-2.2.4}/skills/review-pr/SKILL.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-review-graph
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.4
|
|
4
4
|
Summary: Persistent incremental knowledge graph for token-efficient, context-aware code reviews with Claude Code
|
|
5
5
|
Project-URL: Homepage, https://code-review-graph.com
|
|
6
6
|
Project-URL: Repository, https://github.com/tirth8205/code-review-graph
|
|
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
22
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
23
|
Requires-Python: >=3.10
|
|
24
|
-
Requires-Dist: fastmcp<2
|
|
24
|
+
Requires-Dist: fastmcp<3,>=2.14.0
|
|
25
25
|
Requires-Dist: mcp<2,>=1.0.0
|
|
26
26
|
Requires-Dist: networkx<4,>=3.2
|
|
27
27
|
Requires-Dist: tree-sitter-language-pack<1,>=0.3.0
|
|
@@ -32,6 +32,7 @@ import argparse
|
|
|
32
32
|
import json
|
|
33
33
|
import logging
|
|
34
34
|
import os
|
|
35
|
+
from importlib.metadata import PackageNotFoundError
|
|
35
36
|
from importlib.metadata import version as pkg_version
|
|
36
37
|
from pathlib import Path
|
|
37
38
|
|
|
@@ -40,7 +41,7 @@ def _get_version() -> str:
|
|
|
40
41
|
"""Get the installed package version."""
|
|
41
42
|
try:
|
|
42
43
|
return pkg_version("code-review-graph")
|
|
43
|
-
except
|
|
44
|
+
except PackageNotFoundError:
|
|
44
45
|
return "dev"
|
|
45
46
|
|
|
46
47
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
+
import sqlite3
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
logger = logging.getLogger(__name__)
|
|
@@ -18,7 +19,8 @@ def run(repo_path: Path, store, config: dict) -> list[dict]:
|
|
|
18
19
|
try:
|
|
19
20
|
from code_review_graph.search import hybrid_search
|
|
20
21
|
search_results = hybrid_search(store, query, limit=20)
|
|
21
|
-
except
|
|
22
|
+
except (ImportError, sqlite3.OperationalError) as exc:
|
|
23
|
+
logger.debug("hybrid_search unavailable, using fallback: %s", exc)
|
|
22
24
|
# Fallback to basic search
|
|
23
25
|
search_results = [
|
|
24
26
|
{"qualified_name": n.qualified_name}
|
|
@@ -769,7 +769,9 @@ class GraphStore:
|
|
|
769
769
|
r["qualified_name"]: r["community_id"]
|
|
770
770
|
for r in rows
|
|
771
771
|
}
|
|
772
|
-
except
|
|
772
|
+
except sqlite3.OperationalError as exc:
|
|
773
|
+
# community_id column may not exist yet on pre-v6 schemas
|
|
774
|
+
logger.debug("get_all_community_ids: schema not yet migrated (%s)", exc)
|
|
773
775
|
return {}
|
|
774
776
|
|
|
775
777
|
def get_node_ids_by_files(
|
|
@@ -844,7 +846,9 @@ class GraphStore:
|
|
|
844
846
|
return self._conn.execute(
|
|
845
847
|
"SELECT id, name FROM communities"
|
|
846
848
|
).fetchall()
|
|
847
|
-
except
|
|
849
|
+
except sqlite3.OperationalError as exc:
|
|
850
|
+
# communities table doesn't exist yet on pre-v4 schemas
|
|
851
|
+
logger.debug("get_communities_raw: table missing (%s)", exc)
|
|
848
852
|
return []
|
|
849
853
|
|
|
850
854
|
def get_community_member_qns(
|
|
@@ -14,7 +14,7 @@ import os
|
|
|
14
14
|
import re
|
|
15
15
|
import subprocess
|
|
16
16
|
import time
|
|
17
|
-
from pathlib import Path
|
|
17
|
+
from pathlib import Path, PurePosixPath
|
|
18
18
|
from typing import Optional
|
|
19
19
|
|
|
20
20
|
from .graph import GraphStore
|
|
@@ -26,7 +26,11 @@ _MAX_PARSE_WORKERS = int(os.environ.get(
|
|
|
26
26
|
|
|
27
27
|
logger = logging.getLogger(__name__)
|
|
28
28
|
|
|
29
|
-
# Default ignore patterns (in addition to .gitignore)
|
|
29
|
+
# Default ignore patterns (in addition to .gitignore).
|
|
30
|
+
#
|
|
31
|
+
# `<dir>/**` patterns are matched at any depth by _should_ignore, so
|
|
32
|
+
# `node_modules/**` also excludes `packages/app/node_modules/react/index.js`
|
|
33
|
+
# inside monorepos. See: #91
|
|
30
34
|
DEFAULT_IGNORE_PATTERNS = [
|
|
31
35
|
".code-review-graph/**",
|
|
32
36
|
"node_modules/**",
|
|
@@ -39,6 +43,21 @@ DEFAULT_IGNORE_PATTERNS = [
|
|
|
39
43
|
"build/**",
|
|
40
44
|
".next/**",
|
|
41
45
|
"target/**",
|
|
46
|
+
# PHP / Laravel / Composer
|
|
47
|
+
"vendor/**",
|
|
48
|
+
"bootstrap/cache/**",
|
|
49
|
+
"public/build/**",
|
|
50
|
+
# Ruby / Bundler
|
|
51
|
+
".bundle/**",
|
|
52
|
+
# Java / Kotlin / Gradle
|
|
53
|
+
".gradle/**",
|
|
54
|
+
"*.jar",
|
|
55
|
+
# Dart / Flutter
|
|
56
|
+
".dart_tool/**",
|
|
57
|
+
".pub-cache/**",
|
|
58
|
+
# General
|
|
59
|
+
"coverage/**",
|
|
60
|
+
".cache/**",
|
|
42
61
|
"*.min.js",
|
|
43
62
|
"*.min.css",
|
|
44
63
|
"*.map",
|
|
@@ -148,8 +167,31 @@ def _load_ignore_patterns(repo_root: Path) -> list[str]:
|
|
|
148
167
|
|
|
149
168
|
|
|
150
169
|
def _should_ignore(path: str, patterns: list[str]) -> bool:
|
|
151
|
-
"""Check if a path matches any ignore pattern.
|
|
152
|
-
|
|
170
|
+
"""Check if a path matches any ignore pattern.
|
|
171
|
+
|
|
172
|
+
Handles nested occurrences of ``<dir>/**`` patterns: for example,
|
|
173
|
+
``node_modules/**`` also matches ``packages/app/node_modules/foo.js``
|
|
174
|
+
inside monorepos. ``fnmatch`` alone treats ``*`` as not crossing ``/``
|
|
175
|
+
and only matches the prefix, so we additionally test each path segment
|
|
176
|
+
against the bare prefix of ``<dir>/**`` patterns. See: #91
|
|
177
|
+
"""
|
|
178
|
+
# Direct fnmatch first (cheap)
|
|
179
|
+
if any(fnmatch.fnmatch(path, p) for p in patterns):
|
|
180
|
+
return True
|
|
181
|
+
# Then: treat simple single-segment "dir/**" patterns as
|
|
182
|
+
# "this directory at any depth".
|
|
183
|
+
parts = PurePosixPath(path).parts
|
|
184
|
+
for p in patterns:
|
|
185
|
+
if not p.endswith("/**"):
|
|
186
|
+
continue
|
|
187
|
+
prefix = p[:-3]
|
|
188
|
+
# Only single-segment dir patterns (no "/" inside the prefix)
|
|
189
|
+
# qualify for nested matching.
|
|
190
|
+
if "/" in prefix or not prefix:
|
|
191
|
+
continue
|
|
192
|
+
if prefix in parts:
|
|
193
|
+
return True
|
|
194
|
+
return False
|
|
153
195
|
|
|
154
196
|
|
|
155
197
|
def _is_binary(path: Path) -> bool:
|
|
@@ -6,6 +6,7 @@ Communicates via stdio (standard MCP transport).
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import sys
|
|
9
10
|
from typing import Optional
|
|
10
11
|
|
|
11
12
|
from fastmcp import FastMCP
|
|
@@ -48,6 +49,23 @@ from .tools import (
|
|
|
48
49
|
# transport with concurrent requests, replace with contextvars.ContextVar.
|
|
49
50
|
_default_repo_root: str | None = None
|
|
50
51
|
|
|
52
|
+
|
|
53
|
+
def _resolve_repo_root(repo_root: Optional[str]) -> Optional[str]:
|
|
54
|
+
"""Resolve repo_root for a tool call.
|
|
55
|
+
|
|
56
|
+
Order of precedence:
|
|
57
|
+
1. Explicit ``repo_root`` passed by the MCP client (highest).
|
|
58
|
+
2. ``--repo`` CLI flag passed to ``code-review-graph serve``
|
|
59
|
+
(captured in ``_default_repo_root``).
|
|
60
|
+
3. None — the underlying impl will fall back to the server's cwd.
|
|
61
|
+
|
|
62
|
+
Previously, only ``get_docs_section_tool`` consulted ``_default_repo_root``,
|
|
63
|
+
so ``serve --repo <X>`` had no effect for the other 21 tools. See: #222
|
|
64
|
+
follow-up.
|
|
65
|
+
"""
|
|
66
|
+
return repo_root if repo_root else _default_repo_root
|
|
67
|
+
|
|
68
|
+
|
|
51
69
|
mcp = FastMCP(
|
|
52
70
|
"code-review-graph",
|
|
53
71
|
instructions=(
|
|
@@ -82,7 +100,7 @@ def build_or_update_graph_tool(
|
|
|
82
100
|
When None (default), falls back to CRG_RECURSE_SUBMODULES env var.
|
|
83
101
|
"""
|
|
84
102
|
return build_or_update_graph(
|
|
85
|
-
full_rebuild=full_rebuild, repo_root=repo_root, base=base,
|
|
103
|
+
full_rebuild=full_rebuild, repo_root=_resolve_repo_root(repo_root), base=base,
|
|
86
104
|
postprocess=postprocess, recurse_submodules=recurse_submodules,
|
|
87
105
|
)
|
|
88
106
|
|
|
@@ -106,7 +124,7 @@ def run_postprocess_tool(
|
|
|
106
124
|
repo_root: Repository root path. Auto-detected if omitted.
|
|
107
125
|
"""
|
|
108
126
|
return run_postprocess(
|
|
109
|
-
flows=flows, communities=communities, fts=fts, repo_root=repo_root,
|
|
127
|
+
flows=flows, communities=communities, fts=fts, repo_root=_resolve_repo_root(repo_root),
|
|
110
128
|
)
|
|
111
129
|
|
|
112
130
|
|
|
@@ -131,7 +149,7 @@ def get_minimal_context_tool(
|
|
|
131
149
|
"""
|
|
132
150
|
return get_minimal_context(
|
|
133
151
|
task=task, changed_files=changed_files,
|
|
134
|
-
repo_root=repo_root, base=base,
|
|
152
|
+
repo_root=_resolve_repo_root(repo_root), base=base,
|
|
135
153
|
)
|
|
136
154
|
|
|
137
155
|
|
|
@@ -157,7 +175,7 @@ def get_impact_radius_tool(
|
|
|
157
175
|
"""
|
|
158
176
|
return get_impact_radius(
|
|
159
177
|
changed_files=changed_files, max_depth=max_depth,
|
|
160
|
-
repo_root=repo_root, base=base, detail_level=detail_level,
|
|
178
|
+
repo_root=_resolve_repo_root(repo_root), base=base, detail_level=detail_level,
|
|
161
179
|
)
|
|
162
180
|
|
|
163
181
|
|
|
@@ -187,7 +205,7 @@ def query_graph_tool(
|
|
|
187
205
|
detail_level: "standard" for full output, "minimal" for compact summary. Default: standard.
|
|
188
206
|
"""
|
|
189
207
|
return query_graph(
|
|
190
|
-
pattern=pattern, target=target, repo_root=repo_root,
|
|
208
|
+
pattern=pattern, target=target, repo_root=_resolve_repo_root(repo_root),
|
|
191
209
|
detail_level=detail_level,
|
|
192
210
|
)
|
|
193
211
|
|
|
@@ -220,7 +238,7 @@ def get_review_context_tool(
|
|
|
220
238
|
return get_review_context(
|
|
221
239
|
changed_files=changed_files, max_depth=max_depth,
|
|
222
240
|
include_source=include_source, max_lines_per_file=max_lines_per_file,
|
|
223
|
-
repo_root=repo_root, base=base, detail_level=detail_level,
|
|
241
|
+
repo_root=_resolve_repo_root(repo_root), base=base, detail_level=detail_level,
|
|
224
242
|
)
|
|
225
243
|
|
|
226
244
|
|
|
@@ -249,7 +267,7 @@ def semantic_search_nodes_tool(
|
|
|
249
267
|
detail_level: "standard" for full output, "minimal" for compact summary. Default: standard.
|
|
250
268
|
"""
|
|
251
269
|
return semantic_search_nodes(
|
|
252
|
-
query=query, kind=kind, limit=limit, repo_root=repo_root, model=model,
|
|
270
|
+
query=query, kind=kind, limit=limit, repo_root=_resolve_repo_root(repo_root), model=model,
|
|
253
271
|
detail_level=detail_level,
|
|
254
272
|
)
|
|
255
273
|
|
|
@@ -274,7 +292,7 @@ def embed_graph_tool(
|
|
|
274
292
|
model: Embedding model name (HuggingFace ID or local path).
|
|
275
293
|
Falls back to CRG_EMBEDDING_MODEL env var, then all-MiniLM-L6-v2.
|
|
276
294
|
"""
|
|
277
|
-
return embed_graph(repo_root=repo_root, model=model)
|
|
295
|
+
return embed_graph(repo_root=_resolve_repo_root(repo_root), model=model)
|
|
278
296
|
|
|
279
297
|
|
|
280
298
|
@mcp.tool()
|
|
@@ -289,7 +307,7 @@ def list_graph_stats_tool(
|
|
|
289
307
|
Args:
|
|
290
308
|
repo_root: Repository root path. Auto-detected if omitted.
|
|
291
309
|
"""
|
|
292
|
-
return list_graph_stats(repo_root=repo_root)
|
|
310
|
+
return list_graph_stats(repo_root=_resolve_repo_root(repo_root))
|
|
293
311
|
|
|
294
312
|
|
|
295
313
|
@mcp.tool()
|
|
@@ -332,7 +350,7 @@ def find_large_functions_tool(
|
|
|
332
350
|
"""
|
|
333
351
|
return find_large_functions(
|
|
334
352
|
min_lines=min_lines, kind=kind, file_path_pattern=file_path_pattern,
|
|
335
|
-
limit=limit, repo_root=repo_root,
|
|
353
|
+
limit=limit, repo_root=_resolve_repo_root(repo_root),
|
|
336
354
|
)
|
|
337
355
|
|
|
338
356
|
|
|
@@ -359,7 +377,7 @@ def list_flows_tool(
|
|
|
359
377
|
repo_root: Repository root path. Auto-detected if omitted.
|
|
360
378
|
"""
|
|
361
379
|
return list_flows(
|
|
362
|
-
repo_root=repo_root, sort_by=sort_by, limit=limit, kind=kind,
|
|
380
|
+
repo_root=_resolve_repo_root(repo_root), sort_by=sort_by, limit=limit, kind=kind,
|
|
363
381
|
detail_level=detail_level,
|
|
364
382
|
)
|
|
365
383
|
|
|
@@ -386,7 +404,7 @@ def get_flow_tool(
|
|
|
386
404
|
"""
|
|
387
405
|
return get_flow(
|
|
388
406
|
flow_id=flow_id, flow_name=flow_name,
|
|
389
|
-
include_source=include_source, repo_root=repo_root,
|
|
407
|
+
include_source=include_source, repo_root=_resolve_repo_root(repo_root),
|
|
390
408
|
)
|
|
391
409
|
|
|
392
410
|
|
|
@@ -408,7 +426,7 @@ def get_affected_flows_tool(
|
|
|
408
426
|
repo_root: Repository root path. Auto-detected if omitted.
|
|
409
427
|
"""
|
|
410
428
|
return get_affected_flows_func(
|
|
411
|
-
changed_files=changed_files, base=base, repo_root=repo_root,
|
|
429
|
+
changed_files=changed_files, base=base, repo_root=_resolve_repo_root(repo_root),
|
|
412
430
|
)
|
|
413
431
|
|
|
414
432
|
|
|
@@ -434,7 +452,7 @@ def list_communities_tool(
|
|
|
434
452
|
repo_root: Repository root path. Auto-detected if omitted.
|
|
435
453
|
"""
|
|
436
454
|
return list_communities_func(
|
|
437
|
-
repo_root=repo_root, sort_by=sort_by, min_size=min_size,
|
|
455
|
+
repo_root=_resolve_repo_root(repo_root), sort_by=sort_by, min_size=min_size,
|
|
438
456
|
detail_level=detail_level,
|
|
439
457
|
)
|
|
440
458
|
|
|
@@ -462,7 +480,7 @@ def get_community_tool(
|
|
|
462
480
|
"""
|
|
463
481
|
return get_community_func(
|
|
464
482
|
community_name=community_name, community_id=community_id,
|
|
465
|
-
include_members=include_members, repo_root=repo_root,
|
|
483
|
+
include_members=include_members, repo_root=_resolve_repo_root(repo_root),
|
|
466
484
|
)
|
|
467
485
|
|
|
468
486
|
|
|
@@ -479,7 +497,7 @@ def get_architecture_overview_tool(
|
|
|
479
497
|
Args:
|
|
480
498
|
repo_root: Repository root path. Auto-detected if omitted.
|
|
481
499
|
"""
|
|
482
|
-
return get_architecture_overview_func(repo_root=repo_root)
|
|
500
|
+
return get_architecture_overview_func(repo_root=_resolve_repo_root(repo_root))
|
|
483
501
|
|
|
484
502
|
|
|
485
503
|
@mcp.tool()
|
|
@@ -509,7 +527,7 @@ def detect_changes_tool(
|
|
|
509
527
|
return detect_changes_func(
|
|
510
528
|
base=base, changed_files=changed_files,
|
|
511
529
|
include_source=include_source, max_depth=max_depth,
|
|
512
|
-
repo_root=repo_root, detail_level=detail_level,
|
|
530
|
+
repo_root=_resolve_repo_root(repo_root), detail_level=detail_level,
|
|
513
531
|
)
|
|
514
532
|
|
|
515
533
|
|
|
@@ -545,7 +563,7 @@ def refactor_tool(
|
|
|
545
563
|
"""
|
|
546
564
|
return refactor_func(
|
|
547
565
|
mode=mode, old_name=old_name, new_name=new_name,
|
|
548
|
-
kind=kind, file_pattern=file_pattern, repo_root=repo_root,
|
|
566
|
+
kind=kind, file_pattern=file_pattern, repo_root=_resolve_repo_root(repo_root),
|
|
549
567
|
)
|
|
550
568
|
|
|
551
569
|
|
|
@@ -568,7 +586,7 @@ def apply_refactor_tool(
|
|
|
568
586
|
repo_root: Repository root path. Auto-detected if omitted.
|
|
569
587
|
"""
|
|
570
588
|
return apply_refactor_func(
|
|
571
|
-
refactor_id=refactor_id, repo_root=repo_root,
|
|
589
|
+
refactor_id=refactor_id, repo_root=_resolve_repo_root(repo_root),
|
|
572
590
|
)
|
|
573
591
|
|
|
574
592
|
|
|
@@ -587,7 +605,7 @@ def generate_wiki_tool(
|
|
|
587
605
|
repo_root: Repository root path. Auto-detected if omitted.
|
|
588
606
|
force: If True, regenerate all pages even if content unchanged. Default: False.
|
|
589
607
|
"""
|
|
590
|
-
return generate_wiki_func(repo_root=repo_root, force=force)
|
|
608
|
+
return generate_wiki_func(repo_root=_resolve_repo_root(repo_root), force=force)
|
|
591
609
|
|
|
592
610
|
|
|
593
611
|
@mcp.tool()
|
|
@@ -604,7 +622,9 @@ def get_wiki_page_tool(
|
|
|
604
622
|
community_name: Community name to look up.
|
|
605
623
|
repo_root: Repository root path. Auto-detected if omitted.
|
|
606
624
|
"""
|
|
607
|
-
return get_wiki_page_func(
|
|
625
|
+
return get_wiki_page_func(
|
|
626
|
+
community_name=community_name, repo_root=_resolve_repo_root(repo_root),
|
|
627
|
+
)
|
|
608
628
|
|
|
609
629
|
|
|
610
630
|
@mcp.tool()
|
|
@@ -691,9 +711,21 @@ def pre_merge_check(base: str = "HEAD~1") -> list[dict]:
|
|
|
691
711
|
|
|
692
712
|
|
|
693
713
|
def main(repo_root: str | None = None) -> None:
|
|
694
|
-
"""Run the MCP server via stdio.
|
|
714
|
+
"""Run the MCP server via stdio.
|
|
715
|
+
|
|
716
|
+
On Windows, Python 3.8+ defaults to ``ProactorEventLoop``, which
|
|
717
|
+
interacts poorly with ``concurrent.futures.ProcessPoolExecutor``
|
|
718
|
+
(used by ``full_build``) over a stdio MCP transport — the combination
|
|
719
|
+
produces silent hangs on ``build_or_update_graph_tool`` and
|
|
720
|
+
``embed_graph_tool``. Switching to ``WindowsSelectorEventLoopPolicy``
|
|
721
|
+
before fastmcp starts its loop avoids the deadlock.
|
|
722
|
+
See: #46, #136
|
|
723
|
+
"""
|
|
695
724
|
global _default_repo_root
|
|
696
725
|
_default_repo_root = repo_root
|
|
726
|
+
if sys.platform == "win32":
|
|
727
|
+
import asyncio
|
|
728
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
697
729
|
mcp.run(transport="stdio")
|
|
698
730
|
|
|
699
731
|
|
|
@@ -238,7 +238,7 @@ def run_migrations(conn: sqlite3.Connection) -> None:
|
|
|
238
238
|
MIGRATIONS[version](conn)
|
|
239
239
|
_set_schema_version(conn, version)
|
|
240
240
|
conn.commit()
|
|
241
|
-
except
|
|
241
|
+
except sqlite3.Error:
|
|
242
242
|
conn.rollback()
|
|
243
243
|
logger.error("Migration v%d failed, rolling back", version, exc_info=True)
|
|
244
244
|
raise
|
|
@@ -304,12 +304,16 @@ class CodeParser:
|
|
|
304
304
|
self._module_file_cache: dict[str, Optional[str]] = {}
|
|
305
305
|
self._export_symbol_cache: dict[str, Optional[str]] = {}
|
|
306
306
|
self._tsconfig_resolver = TsconfigResolver()
|
|
307
|
+
# Per-parse cache of Dart pubspec root lookups; see #87
|
|
308
|
+
self._dart_pubspec_cache: dict[tuple[str, str], Optional[Path]] = {}
|
|
307
309
|
|
|
308
310
|
def _get_parser(self, language: str): # type: ignore[arg-type]
|
|
309
311
|
if language not in self._parsers:
|
|
310
312
|
try:
|
|
311
313
|
self._parsers[language] = tslp.get_parser(language) # type: ignore[arg-type]
|
|
312
|
-
except
|
|
314
|
+
except (LookupError, ValueError, ImportError) as exc:
|
|
315
|
+
# language not packaged, or grammar load failed
|
|
316
|
+
logger.debug("tree-sitter parser unavailable for %s: %s", language, exc)
|
|
313
317
|
return None
|
|
314
318
|
return self._parsers[language]
|
|
315
319
|
|
|
@@ -924,6 +928,18 @@ class CodeParser:
|
|
|
924
928
|
):
|
|
925
929
|
continue
|
|
926
930
|
|
|
931
|
+
# --- Dart call detection (see #87) ---
|
|
932
|
+
# tree-sitter-dart does not wrap calls in a single
|
|
933
|
+
# ``call_expression`` node; instead the pattern is
|
|
934
|
+
# ``identifier + selector > argument_part`` as siblings inside
|
|
935
|
+
# the parent. Scan child's children here and emit CALLS edges
|
|
936
|
+
# for any we find; nested calls are handled by the main recursion.
|
|
937
|
+
if language == "dart":
|
|
938
|
+
self._extract_dart_calls_from_children(
|
|
939
|
+
child, source, file_path, edges,
|
|
940
|
+
enclosing_class, enclosing_func,
|
|
941
|
+
)
|
|
942
|
+
|
|
927
943
|
# --- JS/TS variable-assigned functions (const foo = () => {}) ---
|
|
928
944
|
if (
|
|
929
945
|
language in ("javascript", "typescript", "tsx")
|
|
@@ -1014,6 +1030,84 @@ class CodeParser:
|
|
|
1014
1030
|
_depth=_depth + 1,
|
|
1015
1031
|
)
|
|
1016
1032
|
|
|
1033
|
+
def _extract_dart_calls_from_children(
|
|
1034
|
+
self,
|
|
1035
|
+
parent,
|
|
1036
|
+
source: bytes,
|
|
1037
|
+
file_path: str,
|
|
1038
|
+
edges: list[EdgeInfo],
|
|
1039
|
+
enclosing_class: Optional[str],
|
|
1040
|
+
enclosing_func: Optional[str],
|
|
1041
|
+
) -> None:
|
|
1042
|
+
"""Detect Dart call sites from a parent node's children (#87 bug 1).
|
|
1043
|
+
|
|
1044
|
+
tree-sitter-dart does not emit a single ``call_expression`` node for
|
|
1045
|
+
Dart calls. Instead it produces ``identifier`` / method-selector
|
|
1046
|
+
siblings followed by a ``selector`` whose child is ``argument_part``:
|
|
1047
|
+
|
|
1048
|
+
identifier "print"
|
|
1049
|
+
selector
|
|
1050
|
+
argument_part
|
|
1051
|
+
|
|
1052
|
+
And for method calls like ``obj.foo()`` the middle selector is a
|
|
1053
|
+
``unconditional_assignable_selector`` holding the method name:
|
|
1054
|
+
|
|
1055
|
+
identifier "obj"
|
|
1056
|
+
selector
|
|
1057
|
+
unconditional_assignable_selector "."
|
|
1058
|
+
identifier "foo"
|
|
1059
|
+
selector
|
|
1060
|
+
argument_part
|
|
1061
|
+
|
|
1062
|
+
This walker scans the immediate children of ``parent`` for either
|
|
1063
|
+
shape and emits a ``CALLS`` edge. Nested calls are picked up as
|
|
1064
|
+
``_extract_from_tree`` recurses into child nodes.
|
|
1065
|
+
"""
|
|
1066
|
+
call_name: Optional[str] = None
|
|
1067
|
+
for sub in parent.children:
|
|
1068
|
+
if sub.type == "identifier":
|
|
1069
|
+
call_name = sub.text.decode("utf-8", errors="replace")
|
|
1070
|
+
continue
|
|
1071
|
+
if sub.type == "selector":
|
|
1072
|
+
# Case A: selector > unconditional_assignable_selector > identifier
|
|
1073
|
+
# (updates call_name to the method name)
|
|
1074
|
+
method_name: Optional[str] = None
|
|
1075
|
+
has_arguments = False
|
|
1076
|
+
for ssub in sub.children:
|
|
1077
|
+
if ssub.type == "unconditional_assignable_selector":
|
|
1078
|
+
for ident in ssub.children:
|
|
1079
|
+
if ident.type == "identifier":
|
|
1080
|
+
method_name = ident.text.decode(
|
|
1081
|
+
"utf-8", errors="replace"
|
|
1082
|
+
)
|
|
1083
|
+
break
|
|
1084
|
+
elif ssub.type == "argument_part":
|
|
1085
|
+
has_arguments = True
|
|
1086
|
+
if method_name is not None:
|
|
1087
|
+
call_name = method_name
|
|
1088
|
+
if has_arguments and call_name:
|
|
1089
|
+
src_qn = (
|
|
1090
|
+
self._qualify(enclosing_func, file_path, enclosing_class)
|
|
1091
|
+
if enclosing_func else file_path
|
|
1092
|
+
)
|
|
1093
|
+
edges.append(EdgeInfo(
|
|
1094
|
+
kind="CALLS",
|
|
1095
|
+
source=src_qn,
|
|
1096
|
+
target=call_name,
|
|
1097
|
+
file_path=file_path,
|
|
1098
|
+
line=parent.start_point[0] + 1,
|
|
1099
|
+
))
|
|
1100
|
+
# After emitting for this call, clear call_name so we
|
|
1101
|
+
# don't re-emit on any trailing chained selector.
|
|
1102
|
+
call_name = None
|
|
1103
|
+
continue
|
|
1104
|
+
# Non-identifier, non-selector children don't change the
|
|
1105
|
+
# pending call name (``return``, ``await``, ``yield``, etc.)
|
|
1106
|
+
# but anything unexpected should reset it to avoid spurious
|
|
1107
|
+
# edges across unrelated siblings.
|
|
1108
|
+
if sub.type not in ("return", "await", "yield", "this", "const", "new"):
|
|
1109
|
+
call_name = None
|
|
1110
|
+
|
|
1017
1111
|
def _extract_r_constructs(
|
|
1018
1112
|
self,
|
|
1019
1113
|
child,
|
|
@@ -1577,6 +1671,14 @@ class CodeParser:
|
|
|
1577
1671
|
if not name:
|
|
1578
1672
|
return False
|
|
1579
1673
|
|
|
1674
|
+
# Go methods: attach to their receiver type as the enclosing class,
|
|
1675
|
+
# so `func (s *T) Foo()` becomes a member of T rather than a
|
|
1676
|
+
# top-level function. See: #190
|
|
1677
|
+
if language == "go" and child.type == "method_declaration":
|
|
1678
|
+
receiver_type = self._get_go_receiver_type(child)
|
|
1679
|
+
if receiver_type:
|
|
1680
|
+
enclosing_class = receiver_type
|
|
1681
|
+
|
|
1580
1682
|
# Extract annotations/decorators for test detection
|
|
1581
1683
|
decorators: tuple[str, ...] = ()
|
|
1582
1684
|
deco_list: list[str] = []
|
|
@@ -2469,9 +2571,60 @@ class CodeParser:
|
|
|
2469
2571
|
target = base.with_suffix(".dart")
|
|
2470
2572
|
if target.is_file():
|
|
2471
2573
|
return str(target.resolve())
|
|
2574
|
+
elif module.startswith("package:"):
|
|
2575
|
+
# ``package:<name>/<sub_path>`` — resolve to the current repo's
|
|
2576
|
+
# ``lib/<sub_path>`` iff a ``pubspec.yaml`` declaring that
|
|
2577
|
+
# package name is found in an ancestor directory. See: #87
|
|
2578
|
+
try:
|
|
2579
|
+
uri_body = module[len("package:"):]
|
|
2580
|
+
pkg_name, _, sub_path = uri_body.partition("/")
|
|
2581
|
+
if not sub_path:
|
|
2582
|
+
return None
|
|
2583
|
+
pubspec_root = self._find_dart_pubspec_root(
|
|
2584
|
+
caller_dir, pkg_name
|
|
2585
|
+
)
|
|
2586
|
+
if pubspec_root is not None:
|
|
2587
|
+
target = pubspec_root / "lib" / sub_path
|
|
2588
|
+
if target.is_file():
|
|
2589
|
+
return str(target.resolve())
|
|
2590
|
+
except (OSError, ValueError):
|
|
2591
|
+
return None
|
|
2592
|
+
# ``dart:core`` / ``dart:async`` etc. are SDK libraries we do
|
|
2593
|
+
# not track; fall through to return None.
|
|
2472
2594
|
|
|
2473
2595
|
return None
|
|
2474
2596
|
|
|
2597
|
+
def _find_dart_pubspec_root(
|
|
2598
|
+
self, start: Path, pkg_name: str,
|
|
2599
|
+
) -> Optional[Path]:
|
|
2600
|
+
"""Walk up from ``start`` to find a ``pubspec.yaml`` whose ``name:``
|
|
2601
|
+
matches ``pkg_name``. Returns the directory containing that pubspec,
|
|
2602
|
+
or None if no match is found. Result is cached per (start, pkg_name)
|
|
2603
|
+
pair so repeated lookups within one parse pass are cheap.
|
|
2604
|
+
"""
|
|
2605
|
+
cache_key = (str(start), pkg_name)
|
|
2606
|
+
cached = self._dart_pubspec_cache.get(cache_key)
|
|
2607
|
+
if cached is not None or cache_key in self._dart_pubspec_cache:
|
|
2608
|
+
return cached
|
|
2609
|
+
current = start
|
|
2610
|
+
# Avoid infinite loops on weird symlinks.
|
|
2611
|
+
for _ in range(20):
|
|
2612
|
+
pubspec = current / "pubspec.yaml"
|
|
2613
|
+
if pubspec.is_file():
|
|
2614
|
+
try:
|
|
2615
|
+
text = pubspec.read_text(encoding="utf-8", errors="replace")
|
|
2616
|
+
except OSError:
|
|
2617
|
+
text = ""
|
|
2618
|
+
m = re.search(r"^name:\s*([\w-]+)", text, re.MULTILINE)
|
|
2619
|
+
if m and m.group(1) == pkg_name:
|
|
2620
|
+
self._dart_pubspec_cache[cache_key] = current
|
|
2621
|
+
return current
|
|
2622
|
+
if current.parent == current:
|
|
2623
|
+
break
|
|
2624
|
+
current = current.parent
|
|
2625
|
+
self._dart_pubspec_cache[cache_key] = None
|
|
2626
|
+
return None
|
|
2627
|
+
|
|
2475
2628
|
def _resolve_call_target(
|
|
2476
2629
|
self,
|
|
2477
2630
|
call_name: str,
|
|
@@ -2684,6 +2837,36 @@ class CodeParser:
|
|
|
2684
2837
|
return self._get_name(child, language, kind)
|
|
2685
2838
|
return None
|
|
2686
2839
|
|
|
2840
|
+
def _get_go_receiver_type(self, node) -> Optional[str]:
|
|
2841
|
+
"""Extract the receiver type from a Go method_declaration.
|
|
2842
|
+
|
|
2843
|
+
For ``func (s *T) Foo() {...}`` returns ``"T"``. For ``func (T) Foo()``
|
|
2844
|
+
also returns ``"T"``. Returns None if no receiver is present.
|
|
2845
|
+
|
|
2846
|
+
The receiver is always the first ``parameter_list`` child of a
|
|
2847
|
+
Go ``method_declaration`` and contains a single ``parameter_declaration``
|
|
2848
|
+
whose type is either a ``type_identifier`` or a ``pointer_type``
|
|
2849
|
+
wrapping one. See: #190
|
|
2850
|
+
"""
|
|
2851
|
+
for child in node.children:
|
|
2852
|
+
if child.type != "parameter_list":
|
|
2853
|
+
continue
|
|
2854
|
+
for param in child.children:
|
|
2855
|
+
if param.type != "parameter_declaration":
|
|
2856
|
+
continue
|
|
2857
|
+
for sub in param.children:
|
|
2858
|
+
if sub.type == "type_identifier":
|
|
2859
|
+
return sub.text.decode("utf-8", errors="replace")
|
|
2860
|
+
if sub.type == "pointer_type":
|
|
2861
|
+
for ptr_child in sub.children:
|
|
2862
|
+
if ptr_child.type == "type_identifier":
|
|
2863
|
+
return ptr_child.text.decode(
|
|
2864
|
+
"utf-8", errors="replace"
|
|
2865
|
+
)
|
|
2866
|
+
# First parameter_list is always the receiver; stop searching.
|
|
2867
|
+
return None
|
|
2868
|
+
return None
|
|
2869
|
+
|
|
2687
2870
|
def _get_params(self, node, language: str, source: bytes) -> Optional[str]:
|
|
2688
2871
|
"""Extract parameter list as a string."""
|
|
2689
2872
|
for child in node.children:
|
|
@@ -192,7 +192,7 @@ class ConnectionPool:
|
|
|
192
192
|
evict_key, evict_conn = self._pool.popitem(last=False)
|
|
193
193
|
try:
|
|
194
194
|
evict_conn.close()
|
|
195
|
-
except
|
|
195
|
+
except sqlite3.Error:
|
|
196
196
|
logger.debug("Failed to close evicted connection: %s", evict_key)
|
|
197
197
|
logger.debug("Evicted connection: %s", evict_key)
|
|
198
198
|
|
|
@@ -212,7 +212,7 @@ class ConnectionPool:
|
|
|
212
212
|
for key, conn in self._pool.items():
|
|
213
213
|
try:
|
|
214
214
|
conn.close()
|
|
215
|
-
except
|
|
215
|
+
except sqlite3.Error:
|
|
216
216
|
logger.debug("Failed to close connection: %s", key)
|
|
217
217
|
self._pool.clear()
|
|
218
218
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
+
import sqlite3
|
|
6
7
|
import subprocess
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import Any
|
|
@@ -85,7 +86,10 @@ def get_minimal_context(
|
|
|
85
86
|
for f in analysis.get("changed_functions", [])[:5]
|
|
86
87
|
]
|
|
87
88
|
test_gap_count = len(analysis.get("test_gaps", []))
|
|
88
|
-
except
|
|
89
|
+
except (
|
|
90
|
+
ImportError, OSError, ValueError,
|
|
91
|
+
sqlite3.Error, subprocess.SubprocessError,
|
|
92
|
+
):
|
|
89
93
|
logger.debug("Risk analysis failed in get_minimal_context", exc_info=True)
|
|
90
94
|
|
|
91
95
|
# 3. Top 3 communities
|
|
@@ -95,8 +99,8 @@ def get_minimal_context(
|
|
|
95
99
|
"SELECT name FROM communities ORDER BY size DESC LIMIT 3"
|
|
96
100
|
).fetchall()
|
|
97
101
|
communities = [r[0] for r in rows]
|
|
98
|
-
except
|
|
99
|
-
|
|
102
|
+
except sqlite3.OperationalError: # nosec B110 — table may not exist yet
|
|
103
|
+
logger.debug("communities table not yet populated")
|
|
100
104
|
|
|
101
105
|
# 4. Top 3 critical flows
|
|
102
106
|
flows: list[str] = []
|
|
@@ -105,8 +109,8 @@ def get_minimal_context(
|
|
|
105
109
|
"SELECT name FROM flows ORDER BY criticality DESC LIMIT 3"
|
|
106
110
|
).fetchall()
|
|
107
111
|
flows = [r[0] for r in rows]
|
|
108
|
-
except
|
|
109
|
-
|
|
112
|
+
except sqlite3.OperationalError: # nosec B110 — table may not exist yet
|
|
113
|
+
logger.debug("flows table not yet populated")
|
|
110
114
|
|
|
111
115
|
# 5. Suggest next tools based on task keywords
|
|
112
116
|
task_lower = task.lower()
|
|
@@ -291,6 +291,16 @@ def query_graph(
|
|
|
291
291
|
if child:
|
|
292
292
|
results.append(node_to_dict(child))
|
|
293
293
|
edges_out.append(edge_to_dict(e))
|
|
294
|
+
# Fallback: INHERITS/IMPLEMENTS edges store unqualified base names
|
|
295
|
+
# (e.g. "Animal") while qn is fully qualified
|
|
296
|
+
# (e.g. "sample.dart::Animal"). Search by plain name too. See: #87
|
|
297
|
+
if not results and node:
|
|
298
|
+
for kind in ("INHERITS", "IMPLEMENTS"):
|
|
299
|
+
for e in store.search_edges_by_target_name(node.name, kind=kind):
|
|
300
|
+
child = store.get_node(e.source_qualified)
|
|
301
|
+
if child:
|
|
302
|
+
results.append(node_to_dict(child))
|
|
303
|
+
edges_out.append(edge_to_dict(e))
|
|
294
304
|
|
|
295
305
|
elif pattern == "file_summary":
|
|
296
306
|
abs_path = str(root / target)
|
|
@@ -52,7 +52,7 @@ class TsconfigResolver:
|
|
|
52
52
|
base_dir = Path(tsconfig_dir).resolve()
|
|
53
53
|
|
|
54
54
|
return self._match_and_probe(import_str, paths, base_dir)
|
|
55
|
-
except
|
|
55
|
+
except (OSError, ValueError, TypeError):
|
|
56
56
|
logger.debug(
|
|
57
57
|
"TsconfigResolver: unexpected error for %s", file_path, exc_info=True,
|
|
58
58
|
)
|
|
@@ -15,6 +15,7 @@ from __future__ import annotations
|
|
|
15
15
|
|
|
16
16
|
import json
|
|
17
17
|
import logging
|
|
18
|
+
import sqlite3
|
|
18
19
|
from collections import Counter, defaultdict
|
|
19
20
|
from dataclasses import asdict
|
|
20
21
|
from pathlib import Path
|
|
@@ -142,14 +143,16 @@ def export_graph_data(store: GraphStore) -> dict:
|
|
|
142
143
|
try:
|
|
143
144
|
from code_review_graph.flows import get_flows
|
|
144
145
|
flows = get_flows(store, limit=100)
|
|
145
|
-
except
|
|
146
|
+
except (ImportError, sqlite3.OperationalError) as exc:
|
|
147
|
+
logger.debug("flows unavailable for export: %s", exc)
|
|
146
148
|
flows = []
|
|
147
149
|
|
|
148
150
|
# Include communities (graceful fallback if table doesn't exist)
|
|
149
151
|
try:
|
|
150
152
|
from code_review_graph.communities import get_communities
|
|
151
153
|
communities = get_communities(store)
|
|
152
|
-
except
|
|
154
|
+
except (ImportError, sqlite3.OperationalError) as exc:
|
|
155
|
+
logger.debug("communities unavailable for export: %s", exc)
|
|
153
156
|
communities = []
|
|
154
157
|
|
|
155
158
|
return {
|
|
@@ -863,7 +866,12 @@ simulation.on("tick", function() {
|
|
|
863
866
|
.attr("x", function(d) { return d.x + (KIND_RADIUS[d.kind] || 6) + 5; })
|
|
864
867
|
.attr("y", function(d) { return d.y; });
|
|
865
868
|
});
|
|
866
|
-
nodes
|
|
869
|
+
// Only auto-collapse File nodes on very large graphs, otherwise all edges
|
|
870
|
+
// become invisible because they connect to Functions/Classes that are now
|
|
871
|
+
// hidden beneath collapsed Files. See: #132
|
|
872
|
+
if (N > 2000) {
|
|
873
|
+
nodes.forEach(function(n) { if (n.kind === "File") collapsedFiles.add(n.qualified_name); });
|
|
874
|
+
}
|
|
867
875
|
updateNodes();
|
|
868
876
|
function fitGraph() {
|
|
869
877
|
var b = gRoot.node().getBBox();
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import logging
|
|
10
10
|
import re
|
|
11
|
+
import sqlite3
|
|
11
12
|
from collections import Counter
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
from typing import Any
|
|
@@ -118,7 +119,8 @@ def _generate_community_page(store: GraphStore, community: dict[str, Any]) -> st
|
|
|
118
119
|
lines.append(f"- *... and {len(community_flows) - 10} more flows.*")
|
|
119
120
|
else:
|
|
120
121
|
lines.append("No execution flows pass through this community.")
|
|
121
|
-
except
|
|
122
|
+
except sqlite3.OperationalError as exc:
|
|
123
|
+
logger.debug("wiki: flows table unavailable: %s", exc)
|
|
122
124
|
lines.append("Execution flow data not available.")
|
|
123
125
|
lines.append("")
|
|
124
126
|
|
|
@@ -158,7 +160,8 @@ def _generate_community_page(store: GraphStore, community: dict[str, Any]) -> st
|
|
|
158
160
|
if not outgoing_targets and not incoming_sources:
|
|
159
161
|
lines.append("No cross-community dependencies detected.")
|
|
160
162
|
lines.append("")
|
|
161
|
-
except
|
|
163
|
+
except sqlite3.OperationalError as exc:
|
|
164
|
+
logger.debug("wiki: dependency edges unavailable: %s", exc)
|
|
162
165
|
lines.append("Dependency data not available.")
|
|
163
166
|
lines.append("")
|
|
164
167
|
|
|
@@ -194,9 +197,24 @@ def generate_wiki(
|
|
|
194
197
|
|
|
195
198
|
page_entries: list[tuple[str, str, int]] = [] # (slug, name, size)
|
|
196
199
|
|
|
200
|
+
# Track slugs we've already used in THIS run so two communities that
|
|
201
|
+
# slugify to the same filename don't overwrite each other (#222 follow-up).
|
|
202
|
+
# Previously "Data Processing" and "data processing" both became
|
|
203
|
+
# "data-processing.md", causing silent data loss and inflated "updated"
|
|
204
|
+
# counters (each collision was counted as an update while only one file
|
|
205
|
+
# made it to disk).
|
|
206
|
+
used_slugs: set[str] = set()
|
|
207
|
+
|
|
197
208
|
for comm in communities:
|
|
198
209
|
name = comm["name"]
|
|
199
|
-
|
|
210
|
+
base_slug = _slugify(name)
|
|
211
|
+
slug = base_slug
|
|
212
|
+
suffix = 2
|
|
213
|
+
while slug in used_slugs:
|
|
214
|
+
slug = f"{base_slug}-{suffix}"
|
|
215
|
+
suffix += 1
|
|
216
|
+
used_slugs.add(slug)
|
|
217
|
+
|
|
200
218
|
filename = f"{slug}.md"
|
|
201
219
|
filepath = wiki_path / filename
|
|
202
220
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "code-review-graph"
|
|
7
|
-
version = "2.2.
|
|
7
|
+
version = "2.2.4"
|
|
8
8
|
description = "Persistent incremental knowledge graph for token-efficient, context-aware code reviews with Claude Code"
|
|
9
9
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
10
10
|
license = "MIT"
|
|
@@ -26,7 +26,10 @@ classifiers = [
|
|
|
26
26
|
]
|
|
27
27
|
dependencies = [
|
|
28
28
|
"mcp>=1.0.0,<2",
|
|
29
|
-
|
|
29
|
+
# fastmcp 2.14+ fixes CVE-2025-62800/62801/66416 and drops the
|
|
30
|
+
# transitive fakeredis dep that was broken by a rename upstream.
|
|
31
|
+
# See: #139, #195
|
|
32
|
+
"fastmcp>=2.14.0,<3",
|
|
30
33
|
"tree-sitter>=0.23.0,<1",
|
|
31
34
|
"tree-sitter-language-pack>=0.3.0,<1",
|
|
32
35
|
"networkx>=3.2,<4",
|
|
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
|
{code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/benchmarks/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/configs/express.yaml
RENAMED
|
File without changes
|
{code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/configs/fastapi.yaml
RENAMED
|
File without changes
|
{code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/configs/flask.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/configs/httpx.yaml
RENAMED
|
File without changes
|
{code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/configs/nextjs.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/token_benchmark.py
RENAMED
|
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
|
{code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/community_tools.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/refactor_tools.py
RENAMED
|
File without changes
|
{code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/registry_tools.py
RENAMED
|
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
|