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.
Files changed (72) hide show
  1. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/PKG-INFO +2 -2
  2. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/cli.py +2 -1
  3. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/benchmarks/search_quality.py +3 -1
  4. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/runner.py +1 -0
  5. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/graph.py +6 -2
  6. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/incremental.py +46 -4
  7. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/main.py +54 -22
  8. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/migrations.py +1 -1
  9. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/parser.py +184 -1
  10. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/registry.py +2 -2
  11. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/context.py +9 -5
  12. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/query.py +10 -0
  13. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tsconfig_resolver.py +1 -1
  14. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/visualization.py +11 -3
  15. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/wiki.py +21 -3
  16. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/pyproject.toml +5 -2
  17. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/.gitignore +0 -0
  18. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/LICENSE +0 -0
  19. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/README.md +0 -0
  20. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code-review-graph-vscode/LICENSE +0 -0
  21. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code-review-graph-vscode/README.md +0 -0
  22. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/__init__.py +0 -0
  23. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/__main__.py +0 -0
  24. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/changes.py +0 -0
  25. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/communities.py +0 -0
  26. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/constants.py +0 -0
  27. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/embeddings.py +0 -0
  28. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/__init__.py +0 -0
  29. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/benchmarks/__init__.py +0 -0
  30. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/benchmarks/build_performance.py +0 -0
  31. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/benchmarks/flow_completeness.py +0 -0
  32. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/benchmarks/impact_accuracy.py +0 -0
  33. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/benchmarks/token_efficiency.py +0 -0
  34. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/configs/express.yaml +0 -0
  35. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/configs/fastapi.yaml +0 -0
  36. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/configs/flask.yaml +0 -0
  37. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/configs/gin.yaml +0 -0
  38. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/configs/httpx.yaml +0 -0
  39. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/configs/nextjs.yaml +0 -0
  40. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/reporter.py +0 -0
  41. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/scorer.py +0 -0
  42. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/eval/token_benchmark.py +0 -0
  43. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/flows.py +0 -0
  44. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/hints.py +0 -0
  45. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/prompts.py +0 -0
  46. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/refactor.py +0 -0
  47. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/search.py +0 -0
  48. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/skills.py +0 -0
  49. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/__init__.py +0 -0
  50. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/_common.py +0 -0
  51. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/build.py +0 -0
  52. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/community_tools.py +0 -0
  53. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/docs.py +0 -0
  54. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/flows_tools.py +0 -0
  55. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/refactor_tools.py +0 -0
  56. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/registry_tools.py +0 -0
  57. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/code_review_graph/tools/review.py +0 -0
  58. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/COMMANDS.md +0 -0
  59. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/FEATURES.md +0 -0
  60. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/INDEX.md +0 -0
  61. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/LEGAL.md +0 -0
  62. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/LLM-OPTIMIZED-REFERENCE.md +0 -0
  63. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/ROADMAP.md +0 -0
  64. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/TROUBLESHOOTING.md +0 -0
  65. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/USAGE.md +0 -0
  66. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/architecture.md +0 -0
  67. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/docs/schema.md +0 -0
  68. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/hooks/hooks.json +0 -0
  69. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/hooks/session-start.sh +0 -0
  70. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/skills/build-graph/SKILL.md +0 -0
  71. {code_review_graph-2.2.3 → code_review_graph-2.2.4}/skills/review-delta/SKILL.md +0 -0
  72. {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
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,>=0.1.0
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 Exception:
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 Exception:
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}
@@ -51,6 +51,7 @@ def load_config(name: str) -> dict:
51
51
 
52
52
  def load_all_configs() -> list[dict]:
53
53
  """Load all benchmark configs from the configs directory."""
54
+ _require_yaml()
54
55
  configs = []
55
56
  for p in sorted(CONFIGS_DIR.glob("*.yaml")):
56
57
  with open(p) as f:
@@ -769,7 +769,9 @@ class GraphStore:
769
769
  r["qualified_name"]: r["community_id"]
770
770
  for r in rows
771
771
  }
772
- except Exception:
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 Exception:
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
- return any(fnmatch.fnmatch(path, p) for p in patterns)
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(community_name=community_name, repo_root=repo_root)
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 Exception:
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 Exception:
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 Exception:
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 Exception:
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 Exception:
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 Exception: # nosec B110 — table may not exist yet
99
- pass
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 Exception: # nosec B110 — table may not exist yet
109
- pass
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 Exception:
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 Exception:
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 Exception:
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.forEach(function(n) { if (n.kind === "File") collapsedFiles.add(n.qualified_name); });
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 Exception:
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 Exception:
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
- slug = _slugify(name)
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.3"
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
- "fastmcp>=0.1.0,<2",
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",