code-review-graph-codeblackwell 2.3.6.post1__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.
Files changed (74) hide show
  1. code_review_graph/__init__.py +20 -0
  2. code_review_graph/__main__.py +4 -0
  3. code_review_graph/analysis.py +410 -0
  4. code_review_graph/changes.py +409 -0
  5. code_review_graph/cli.py +1255 -0
  6. code_review_graph/communities.py +874 -0
  7. code_review_graph/constants.py +23 -0
  8. code_review_graph/context_savings.py +317 -0
  9. code_review_graph/custom_languages.py +322 -0
  10. code_review_graph/daemon.py +1009 -0
  11. code_review_graph/daemon_cli.py +320 -0
  12. code_review_graph/docs/LLM-OPTIMIZED-REFERENCE.md +71 -0
  13. code_review_graph/embeddings.py +1006 -0
  14. code_review_graph/enrich.py +303 -0
  15. code_review_graph/eval/__init__.py +33 -0
  16. code_review_graph/eval/benchmarks/__init__.py +1 -0
  17. code_review_graph/eval/benchmarks/agent_baseline.py +193 -0
  18. code_review_graph/eval/benchmarks/build_performance.py +60 -0
  19. code_review_graph/eval/benchmarks/flow_completeness.py +36 -0
  20. code_review_graph/eval/benchmarks/impact_accuracy.py +220 -0
  21. code_review_graph/eval/benchmarks/multi_hop_retrieval.py +125 -0
  22. code_review_graph/eval/benchmarks/search_quality.py +59 -0
  23. code_review_graph/eval/benchmarks/token_efficiency.py +143 -0
  24. code_review_graph/eval/configs/code-review-graph.yaml +50 -0
  25. code_review_graph/eval/configs/express.yaml +45 -0
  26. code_review_graph/eval/configs/fastapi.yaml +48 -0
  27. code_review_graph/eval/configs/flask.yaml +50 -0
  28. code_review_graph/eval/configs/gin.yaml +51 -0
  29. code_review_graph/eval/configs/httpx.yaml +48 -0
  30. code_review_graph/eval/reporter.py +301 -0
  31. code_review_graph/eval/runner.py +211 -0
  32. code_review_graph/eval/scorer.py +85 -0
  33. code_review_graph/eval/token_benchmark.py +182 -0
  34. code_review_graph/exports.py +409 -0
  35. code_review_graph/flows.py +698 -0
  36. code_review_graph/graph.py +1427 -0
  37. code_review_graph/graph_diff.py +122 -0
  38. code_review_graph/hints.py +384 -0
  39. code_review_graph/incremental.py +1245 -0
  40. code_review_graph/jedi_resolver.py +303 -0
  41. code_review_graph/main.py +1079 -0
  42. code_review_graph/memory.py +142 -0
  43. code_review_graph/migrations.py +284 -0
  44. code_review_graph/parser.py +6957 -0
  45. code_review_graph/postprocessing.py +134 -0
  46. code_review_graph/prompts.py +159 -0
  47. code_review_graph/refactor.py +852 -0
  48. code_review_graph/registry.py +319 -0
  49. code_review_graph/rescript_resolver.py +206 -0
  50. code_review_graph/search.py +447 -0
  51. code_review_graph/skills.py +1481 -0
  52. code_review_graph/spring_resolver.py +200 -0
  53. code_review_graph/temporal_resolver.py +199 -0
  54. code_review_graph/token_benchmark.py +125 -0
  55. code_review_graph/tools/__init__.py +156 -0
  56. code_review_graph/tools/_common.py +176 -0
  57. code_review_graph/tools/analysis_tools.py +184 -0
  58. code_review_graph/tools/build.py +541 -0
  59. code_review_graph/tools/community_tools.py +246 -0
  60. code_review_graph/tools/context.py +152 -0
  61. code_review_graph/tools/docs.py +274 -0
  62. code_review_graph/tools/flows_tools.py +176 -0
  63. code_review_graph/tools/query.py +692 -0
  64. code_review_graph/tools/refactor_tools.py +168 -0
  65. code_review_graph/tools/registry_tools.py +125 -0
  66. code_review_graph/tools/review.py +477 -0
  67. code_review_graph/tsconfig_resolver.py +257 -0
  68. code_review_graph/visualization.py +2184 -0
  69. code_review_graph/wiki.py +305 -0
  70. code_review_graph_codeblackwell-2.3.6.post1.dist-info/METADATA +718 -0
  71. code_review_graph_codeblackwell-2.3.6.post1.dist-info/RECORD +74 -0
  72. code_review_graph_codeblackwell-2.3.6.post1.dist-info/WHEEL +4 -0
  73. code_review_graph_codeblackwell-2.3.6.post1.dist-info/entry_points.txt +3 -0
  74. code_review_graph_codeblackwell-2.3.6.post1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,176 @@
1
+ """Shared utilities for tool sub-modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from ..graph import GraphStore
9
+ from ..incremental import find_project_root, get_db_path
10
+
11
+
12
+ def _error_response(
13
+ message: str, status: str = "error", **extra: Any,
14
+ ) -> dict[str, Any]:
15
+ """Build a standardised error response dict."""
16
+ return {"status": status, "error": message, "summary": message, **extra}
17
+
18
+ # Common JS/TS builtin method names filtered from callers_of results.
19
+ # "Who calls .map()?" returns hundreds of hits and is never useful.
20
+ # These are kept in the graph (callees_of still shows them) but excluded
21
+ # when doing reverse call tracing to reduce noise.
22
+ _BUILTIN_CALL_NAMES: set[str] = {
23
+ "map", "filter", "reduce", "reduceRight", "forEach", "find", "findIndex",
24
+ "some", "every", "includes", "indexOf", "lastIndexOf",
25
+ "push", "pop", "shift", "unshift", "splice", "slice",
26
+ "concat", "join", "flat", "flatMap", "sort", "reverse", "fill",
27
+ "keys", "values", "entries", "from", "isArray", "of", "at",
28
+ "trim", "trimStart", "trimEnd", "split", "replace", "replaceAll",
29
+ "match", "matchAll", "search", "substring", "substr",
30
+ "toLowerCase", "toUpperCase", "startsWith", "endsWith",
31
+ "padStart", "padEnd", "repeat", "charAt", "charCodeAt",
32
+ "assign", "freeze", "defineProperty", "getOwnPropertyNames",
33
+ "hasOwnProperty", "create", "is", "fromEntries",
34
+ "log", "warn", "error", "info", "debug", "trace", "dir", "table",
35
+ "time", "timeEnd", "assert", "clear", "count",
36
+ "then", "catch", "finally", "resolve", "reject", "all", "allSettled", "race", "any",
37
+ "parse", "stringify",
38
+ "floor", "ceil", "round", "random", "max", "min", "abs", "pow", "sqrt",
39
+ "addEventListener", "removeEventListener", "querySelector", "querySelectorAll",
40
+ "getElementById", "createElement", "appendChild", "removeChild",
41
+ "setAttribute", "getAttribute", "preventDefault", "stopPropagation",
42
+ "setTimeout", "clearTimeout", "setInterval", "clearInterval",
43
+ "toString", "valueOf", "toJSON", "toISOString",
44
+ "getTime", "getFullYear", "now",
45
+ "isNaN", "parseInt", "parseFloat", "toFixed",
46
+ "encodeURIComponent", "decodeURIComponent",
47
+ "call", "apply", "bind", "next",
48
+ "emit", "on", "off", "once",
49
+ "pipe", "write", "read", "end", "close", "destroy",
50
+ "send", "status", "json", "redirect",
51
+ "set", "get", "delete", "has",
52
+ "findUnique", "findFirst", "findMany", "createMany",
53
+ "update", "updateMany", "deleteMany", "upsert",
54
+ "aggregate", "groupBy", "transaction",
55
+ "describe", "it", "test", "expect", "beforeEach", "afterEach",
56
+ "beforeAll", "afterAll", "mock", "spyOn",
57
+ "require", "fetch",
58
+ }
59
+
60
+
61
+ def _validate_repo_root(path: "Path | str") -> Path:
62
+ """Validate that a path is a plausible project root.
63
+
64
+ Ensures the path is an existing directory that contains a ``.git``,
65
+ ``.svn``, or ``.code-review-graph`` directory, preventing arbitrary
66
+ file-system traversal via the ``repo_root`` parameter.
67
+ """
68
+ resolved = Path(path).resolve()
69
+ if not resolved.is_dir():
70
+ raise ValueError(
71
+ f"repo_root is not an existing directory: {resolved}"
72
+ )
73
+ has_vcs = (
74
+ (resolved / ".git").exists()
75
+ or (resolved / ".svn").exists()
76
+ or (resolved / ".code-review-graph").exists()
77
+ )
78
+ if not has_vcs:
79
+ raise ValueError(
80
+ f"repo_root does not look like a project root "
81
+ f"(no .git, .svn, or .code-review-graph directory found): "
82
+ f"{resolved}"
83
+ )
84
+ return resolved
85
+
86
+
87
+ def _resolve_root(repo_root: str | None = None) -> Path:
88
+ """Resolve and validate the repository root without opening a store."""
89
+ return _validate_repo_root(Path(repo_root)) if repo_root else find_project_root()
90
+
91
+
92
+ def _get_store(repo_root: str | None = None) -> tuple[GraphStore, Path]:
93
+ """Resolve repo root and open the graph store.
94
+
95
+ Callers own the returned store and must close it (try/finally or
96
+ context manager) to avoid leaking SQLite file descriptors.
97
+ """
98
+ root = _resolve_root(repo_root)
99
+ db_path = get_db_path(root)
100
+ return GraphStore(db_path), root
101
+
102
+
103
+ def _resolve_graph_file_paths(
104
+ store: GraphStore, root: Path, file_paths: list[str],
105
+ ) -> list[str]:
106
+ """Resolve user-facing file paths to the paths stored in the graph.
107
+
108
+ Graphs may contain absolute paths, repo-relative paths, or cwd-relative
109
+ paths depending on how they were built. Tool inputs are usually relative to
110
+ repo root, so exact matching alone can miss existing graph nodes.
111
+ """
112
+ resolved: list[str] = []
113
+ seen: set[str] = set()
114
+
115
+ def add(path: str) -> None:
116
+ if path not in seen:
117
+ resolved.append(path)
118
+ seen.add(path)
119
+
120
+ for file_path in file_paths:
121
+ raw = file_path.replace("\\", "/")
122
+ candidates = [raw]
123
+ path = Path(file_path)
124
+ if path.is_absolute():
125
+ try:
126
+ candidates.append(str(path.resolve().relative_to(root)).replace("\\", "/"))
127
+ except ValueError:
128
+ pass
129
+ else:
130
+ candidates.append(str(root / path))
131
+
132
+ for candidate in candidates:
133
+ if store.get_nodes_by_file(candidate):
134
+ add(candidate)
135
+
136
+ suffixes = []
137
+ for candidate in candidates:
138
+ normalized = candidate.replace("\\", "/")
139
+ if normalized not in suffixes:
140
+ suffixes.append(normalized)
141
+
142
+ for suffix in suffixes:
143
+ for matched_path in store.get_files_matching(suffix):
144
+ add(matched_path)
145
+
146
+ return resolved
147
+
148
+
149
+ def compact_response(
150
+ summary: str,
151
+ key_entities: list[str] | None = None,
152
+ risk: str = "unknown",
153
+ communities: list[str] | None = None,
154
+ flows_affected: list[str] | None = None,
155
+ next_tool_suggestions: list[str] | None = None,
156
+ data: dict[str, Any] | None = None,
157
+ detail_level: str = "minimal",
158
+ ) -> dict[str, Any]:
159
+ """Standard compact response format for token efficiency."""
160
+ resp: dict[str, Any] = {
161
+ "status": "ok",
162
+ "summary": summary,
163
+ }
164
+ if key_entities:
165
+ resp["key_entities"] = key_entities[:10]
166
+ if risk != "unknown":
167
+ resp["risk"] = risk
168
+ if communities:
169
+ resp["communities"] = communities[:5]
170
+ if flows_affected:
171
+ resp["flows_affected"] = flows_affected[:5]
172
+ if next_tool_suggestions:
173
+ resp["next_tool_suggestions"] = next_tool_suggestions[:3]
174
+ if detail_level != "minimal" and data:
175
+ resp["data"] = data
176
+ return resp
@@ -0,0 +1,184 @@
1
+ """MCP tool wrappers for graph analysis features."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..analysis import (
8
+ find_bridge_nodes,
9
+ find_hub_nodes,
10
+ find_knowledge_gaps,
11
+ find_surprising_connections,
12
+ generate_suggested_questions,
13
+ )
14
+ from ._common import _get_store
15
+
16
+
17
+ def get_hub_nodes_func(
18
+ repo_root: str | None = None,
19
+ top_n: int = 10,
20
+ ) -> dict[str, Any]:
21
+ """Find the most connected nodes in the codebase graph.
22
+
23
+ Hub nodes have the highest total degree (in + out edges).
24
+ These are architectural hotspots -- changes to them have
25
+ disproportionate blast radius.
26
+
27
+ Args:
28
+ repo_root: Repository root (auto-detected if omitted).
29
+ top_n: Number of top hubs to return (default 10).
30
+ """
31
+ store, _root = _get_store(repo_root or None)
32
+ try:
33
+ hubs = find_hub_nodes(store, top_n=top_n)
34
+ return {
35
+ "hub_nodes": hubs,
36
+ "count": len(hubs),
37
+ "next_tool_suggestions": [
38
+ "get_impact_radius -- check blast radius of a hub",
39
+ "query_graph callers_of -- see what calls a hub",
40
+ "get_bridge_nodes -- find architectural chokepoints",
41
+ ],
42
+ }
43
+ finally:
44
+ store.close()
45
+
46
+
47
+ def get_bridge_nodes_func(
48
+ repo_root: str | None = None,
49
+ top_n: int = 10,
50
+ ) -> dict[str, Any]:
51
+ """Find architectural chokepoints via betweenness centrality.
52
+
53
+ Bridge nodes sit on the shortest paths between many node
54
+ pairs. If they break, multiple code regions lose
55
+ connectivity.
56
+
57
+ Args:
58
+ repo_root: Repository root (auto-detected if omitted).
59
+ top_n: Number of top bridges to return (default 10).
60
+ """
61
+ store, _root = _get_store(repo_root or None)
62
+ try:
63
+ bridges = find_bridge_nodes(store, top_n=top_n)
64
+ return {
65
+ "bridge_nodes": bridges,
66
+ "count": len(bridges),
67
+ "next_tool_suggestions": [
68
+ "get_hub_nodes -- find most connected nodes",
69
+ "get_impact_radius -- check blast radius",
70
+ "detect_changes -- see if bridges are affected",
71
+ ],
72
+ }
73
+ finally:
74
+ store.close()
75
+
76
+
77
+ def get_knowledge_gaps_func(
78
+ repo_root: str | None = None,
79
+ ) -> dict[str, Any]:
80
+ """Identify structural weaknesses in the codebase.
81
+
82
+ Finds: isolated nodes (disconnected), thin communities
83
+ (< 3 members), untested hotspots (high-degree, no tests),
84
+ and single-file communities.
85
+
86
+ Args:
87
+ repo_root: Repository root (auto-detected if omitted).
88
+ """
89
+ store, _root = _get_store(repo_root or None)
90
+ try:
91
+ gaps = find_knowledge_gaps(store)
92
+ total = sum(len(v) for v in gaps.values())
93
+ return {
94
+ "gaps": gaps,
95
+ "total_gaps": total,
96
+ "summary": {
97
+ "isolated_nodes": len(gaps["isolated_nodes"]),
98
+ "thin_communities": len(
99
+ gaps["thin_communities"]
100
+ ),
101
+ "untested_hotspots": len(
102
+ gaps["untested_hotspots"]
103
+ ),
104
+ "single_file_communities": len(
105
+ gaps["single_file_communities"]
106
+ ),
107
+ },
108
+ "next_tool_suggestions": [
109
+ "refactor dead_code -- find unused symbols",
110
+ "get_hub_nodes -- find high-impact nodes",
111
+ "get_suggested_questions -- review prompts",
112
+ ],
113
+ }
114
+ finally:
115
+ store.close()
116
+
117
+
118
+ def get_surprising_connections_func(
119
+ repo_root: str | None = None,
120
+ top_n: int = 15,
121
+ ) -> dict[str, Any]:
122
+ """Find unexpected architectural coupling in the codebase.
123
+
124
+ Scores edges by surprise factors: cross-community,
125
+ cross-language, peripheral-to-hub, cross-test-boundary.
126
+
127
+ Args:
128
+ repo_root: Repository root (auto-detected if omitted).
129
+ top_n: Number of top surprises to return (default 15).
130
+ """
131
+ store, _root = _get_store(repo_root or None)
132
+ try:
133
+ surprises = find_surprising_connections(
134
+ store, top_n=top_n
135
+ )
136
+ return {
137
+ "surprising_connections": surprises,
138
+ "count": len(surprises),
139
+ "next_tool_suggestions": [
140
+ "get_architecture_overview -- community structure",
141
+ "query_graph callers_of -- trace the coupling",
142
+ "get_bridge_nodes -- find chokepoints",
143
+ ],
144
+ }
145
+ finally:
146
+ store.close()
147
+
148
+
149
+ def get_suggested_questions_func(
150
+ repo_root: str | None = None,
151
+ ) -> dict[str, Any]:
152
+ """Auto-generate review questions from graph analysis.
153
+
154
+ Produces questions about: bridge nodes, untested hubs,
155
+ surprising connections, thin communities, and untested
156
+ hotspots.
157
+
158
+ Args:
159
+ repo_root: Repository root (auto-detected if omitted).
160
+ """
161
+ store, _root = _get_store(repo_root or None)
162
+ try:
163
+ questions = generate_suggested_questions(store)
164
+ by_priority: dict[str, list[dict[str, Any]]] = {
165
+ "high": [], "medium": [], "low": [],
166
+ }
167
+ for q in questions:
168
+ prio = q.get("priority", "medium")
169
+ if prio in by_priority:
170
+ by_priority[prio].append(q)
171
+ return {
172
+ "questions": questions,
173
+ "count": len(questions),
174
+ "by_priority": {
175
+ k: len(v) for k, v in by_priority.items()
176
+ },
177
+ "next_tool_suggestions": [
178
+ "get_knowledge_gaps -- structural weaknesses",
179
+ "detect_changes -- risk-scored review",
180
+ "get_architecture_overview -- community map",
181
+ ],
182
+ }
183
+ finally:
184
+ store.close()