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.
- code_review_graph/__init__.py +20 -0
- code_review_graph/__main__.py +4 -0
- code_review_graph/analysis.py +410 -0
- code_review_graph/changes.py +409 -0
- code_review_graph/cli.py +1255 -0
- code_review_graph/communities.py +874 -0
- code_review_graph/constants.py +23 -0
- code_review_graph/context_savings.py +317 -0
- code_review_graph/custom_languages.py +322 -0
- code_review_graph/daemon.py +1009 -0
- code_review_graph/daemon_cli.py +320 -0
- code_review_graph/docs/LLM-OPTIMIZED-REFERENCE.md +71 -0
- code_review_graph/embeddings.py +1006 -0
- code_review_graph/enrich.py +303 -0
- code_review_graph/eval/__init__.py +33 -0
- code_review_graph/eval/benchmarks/__init__.py +1 -0
- code_review_graph/eval/benchmarks/agent_baseline.py +193 -0
- code_review_graph/eval/benchmarks/build_performance.py +60 -0
- code_review_graph/eval/benchmarks/flow_completeness.py +36 -0
- code_review_graph/eval/benchmarks/impact_accuracy.py +220 -0
- code_review_graph/eval/benchmarks/multi_hop_retrieval.py +125 -0
- code_review_graph/eval/benchmarks/search_quality.py +59 -0
- code_review_graph/eval/benchmarks/token_efficiency.py +143 -0
- code_review_graph/eval/configs/code-review-graph.yaml +50 -0
- code_review_graph/eval/configs/express.yaml +45 -0
- code_review_graph/eval/configs/fastapi.yaml +48 -0
- code_review_graph/eval/configs/flask.yaml +50 -0
- code_review_graph/eval/configs/gin.yaml +51 -0
- code_review_graph/eval/configs/httpx.yaml +48 -0
- code_review_graph/eval/reporter.py +301 -0
- code_review_graph/eval/runner.py +211 -0
- code_review_graph/eval/scorer.py +85 -0
- code_review_graph/eval/token_benchmark.py +182 -0
- code_review_graph/exports.py +409 -0
- code_review_graph/flows.py +698 -0
- code_review_graph/graph.py +1427 -0
- code_review_graph/graph_diff.py +122 -0
- code_review_graph/hints.py +384 -0
- code_review_graph/incremental.py +1245 -0
- code_review_graph/jedi_resolver.py +303 -0
- code_review_graph/main.py +1079 -0
- code_review_graph/memory.py +142 -0
- code_review_graph/migrations.py +284 -0
- code_review_graph/parser.py +6957 -0
- code_review_graph/postprocessing.py +134 -0
- code_review_graph/prompts.py +159 -0
- code_review_graph/refactor.py +852 -0
- code_review_graph/registry.py +319 -0
- code_review_graph/rescript_resolver.py +206 -0
- code_review_graph/search.py +447 -0
- code_review_graph/skills.py +1481 -0
- code_review_graph/spring_resolver.py +200 -0
- code_review_graph/temporal_resolver.py +199 -0
- code_review_graph/token_benchmark.py +125 -0
- code_review_graph/tools/__init__.py +156 -0
- code_review_graph/tools/_common.py +176 -0
- code_review_graph/tools/analysis_tools.py +184 -0
- code_review_graph/tools/build.py +541 -0
- code_review_graph/tools/community_tools.py +246 -0
- code_review_graph/tools/context.py +152 -0
- code_review_graph/tools/docs.py +274 -0
- code_review_graph/tools/flows_tools.py +176 -0
- code_review_graph/tools/query.py +692 -0
- code_review_graph/tools/refactor_tools.py +168 -0
- code_review_graph/tools/registry_tools.py +125 -0
- code_review_graph/tools/review.py +477 -0
- code_review_graph/tsconfig_resolver.py +257 -0
- code_review_graph/visualization.py +2184 -0
- code_review_graph/wiki.py +305 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/METADATA +718 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/RECORD +74 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/WHEEL +4 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/entry_points.txt +3 -0
- 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()
|