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,305 @@
|
|
|
1
|
+
"""Wiki generation from community structure.
|
|
2
|
+
|
|
3
|
+
Generates markdown pages for each detected community and an index page,
|
|
4
|
+
providing a navigable documentation wiki for the codebase architecture.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
import sqlite3
|
|
12
|
+
from collections import Counter
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .communities import get_communities
|
|
17
|
+
from .flows import get_flows
|
|
18
|
+
from .graph import GraphStore, _sanitize_name
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _slugify(name: str) -> str:
|
|
24
|
+
"""Convert a community name to a safe filename slug."""
|
|
25
|
+
slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
|
|
26
|
+
return slug[:80] or "unnamed"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _generate_community_page(store: GraphStore, community: dict[str, Any]) -> str:
|
|
30
|
+
"""Build markdown content for a single community.
|
|
31
|
+
|
|
32
|
+
Includes: heading, overview (size, cohesion, language), members table
|
|
33
|
+
(top 50), execution flows through the community, and dependencies.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
store: The graph store.
|
|
37
|
+
community: Community dict from get_communities().
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Markdown string for the community page.
|
|
41
|
+
"""
|
|
42
|
+
name = community["name"]
|
|
43
|
+
size = community["size"]
|
|
44
|
+
cohesion = community.get("cohesion", 0.0)
|
|
45
|
+
lang = community.get("dominant_language", "")
|
|
46
|
+
description = community.get("description", "")
|
|
47
|
+
|
|
48
|
+
lines: list[str] = []
|
|
49
|
+
lines.append(f"# {name}")
|
|
50
|
+
lines.append("")
|
|
51
|
+
|
|
52
|
+
# Overview section
|
|
53
|
+
lines.append("## Overview")
|
|
54
|
+
lines.append("")
|
|
55
|
+
if description:
|
|
56
|
+
lines.append(f"{description}")
|
|
57
|
+
lines.append("")
|
|
58
|
+
lines.append(f"- **Size**: {size} nodes")
|
|
59
|
+
lines.append(f"- **Cohesion**: {cohesion:.4f}")
|
|
60
|
+
if lang:
|
|
61
|
+
lines.append(f"- **Dominant Language**: {lang}")
|
|
62
|
+
lines.append("")
|
|
63
|
+
|
|
64
|
+
# Members table (top 50)
|
|
65
|
+
member_qns = community.get("members", [])
|
|
66
|
+
lines.append("## Members")
|
|
67
|
+
lines.append("")
|
|
68
|
+
if member_qns:
|
|
69
|
+
lines.append("| Name | Kind | File | Lines |")
|
|
70
|
+
lines.append("|------|------|------|-------|")
|
|
71
|
+
|
|
72
|
+
# Fetch node details for members (limit to 50)
|
|
73
|
+
member_count = 0
|
|
74
|
+
for qn in member_qns[:50]:
|
|
75
|
+
node = store.get_node(qn)
|
|
76
|
+
if node and node.kind != "File":
|
|
77
|
+
node_name = _sanitize_name(node.name)
|
|
78
|
+
lines.append(
|
|
79
|
+
f"| {node_name} | {node.kind} | {node.file_path} "
|
|
80
|
+
f"| {node.line_start}-{node.line_end} |"
|
|
81
|
+
)
|
|
82
|
+
member_count += 1
|
|
83
|
+
|
|
84
|
+
if not member_count:
|
|
85
|
+
# Remove the table headers if no members were added
|
|
86
|
+
lines.pop() # header separator
|
|
87
|
+
lines.pop() # header
|
|
88
|
+
lines.append("No non-file members found.")
|
|
89
|
+
|
|
90
|
+
if len(member_qns) > 50:
|
|
91
|
+
lines.append("")
|
|
92
|
+
lines.append(f"*... and {len(member_qns) - 50} more members.*")
|
|
93
|
+
else:
|
|
94
|
+
lines.append("No members found.")
|
|
95
|
+
lines.append("")
|
|
96
|
+
|
|
97
|
+
# Execution flows through community
|
|
98
|
+
lines.append("## Execution Flows")
|
|
99
|
+
lines.append("")
|
|
100
|
+
member_set = set(member_qns)
|
|
101
|
+
try:
|
|
102
|
+
all_flows = get_flows(store, sort_by="criticality", limit=200)
|
|
103
|
+
community_flows: list[dict] = []
|
|
104
|
+
for flow in all_flows:
|
|
105
|
+
# Check if this flow passes through any community member
|
|
106
|
+
flow_qns = store.get_flow_qualified_names(flow["id"])
|
|
107
|
+
if flow_qns & member_set:
|
|
108
|
+
community_flows.append(flow)
|
|
109
|
+
|
|
110
|
+
if community_flows:
|
|
111
|
+
for flow in community_flows[:10]:
|
|
112
|
+
flow_name = _sanitize_name(flow.get("name", "unnamed"))
|
|
113
|
+
criticality = flow.get("criticality", 0.0)
|
|
114
|
+
depth = flow.get("depth", 0)
|
|
115
|
+
lines.append(
|
|
116
|
+
f"- **{flow_name}** (criticality: {criticality:.2f}, depth: {depth})"
|
|
117
|
+
)
|
|
118
|
+
if len(community_flows) > 10:
|
|
119
|
+
lines.append(f"- *... and {len(community_flows) - 10} more flows.*")
|
|
120
|
+
else:
|
|
121
|
+
lines.append("No execution flows pass through this community.")
|
|
122
|
+
except sqlite3.OperationalError as exc:
|
|
123
|
+
logger.debug("wiki: flows table unavailable: %s", exc)
|
|
124
|
+
lines.append("Execution flow data not available.")
|
|
125
|
+
lines.append("")
|
|
126
|
+
|
|
127
|
+
# Dependencies (cross-community edges)
|
|
128
|
+
lines.append("## Dependencies")
|
|
129
|
+
lines.append("")
|
|
130
|
+
try:
|
|
131
|
+
outgoing_targets: Counter[str] = Counter()
|
|
132
|
+
incoming_sources: Counter[str] = Counter()
|
|
133
|
+
if member_qns:
|
|
134
|
+
qns = list(member_qns)
|
|
135
|
+
|
|
136
|
+
# Outgoing: source is a member
|
|
137
|
+
for t in store.get_outgoing_targets(qns):
|
|
138
|
+
if t not in member_set:
|
|
139
|
+
outgoing_targets[t] += 1
|
|
140
|
+
|
|
141
|
+
# Incoming: target is a member
|
|
142
|
+
for s in store.get_incoming_sources(qns):
|
|
143
|
+
if s not in member_set:
|
|
144
|
+
incoming_sources[s] += 1
|
|
145
|
+
|
|
146
|
+
if outgoing_targets:
|
|
147
|
+
lines.append("### Outgoing")
|
|
148
|
+
lines.append("")
|
|
149
|
+
for target, count in outgoing_targets.most_common(15):
|
|
150
|
+
lines.append(f"- `{_sanitize_name(target)}` ({count} edge(s))")
|
|
151
|
+
lines.append("")
|
|
152
|
+
|
|
153
|
+
if incoming_sources:
|
|
154
|
+
lines.append("### Incoming")
|
|
155
|
+
lines.append("")
|
|
156
|
+
for source, count in incoming_sources.most_common(15):
|
|
157
|
+
lines.append(f"- `{_sanitize_name(source)}` ({count} edge(s))")
|
|
158
|
+
lines.append("")
|
|
159
|
+
|
|
160
|
+
if not outgoing_targets and not incoming_sources:
|
|
161
|
+
lines.append("No cross-community dependencies detected.")
|
|
162
|
+
lines.append("")
|
|
163
|
+
except sqlite3.OperationalError as exc:
|
|
164
|
+
logger.debug("wiki: dependency edges unavailable: %s", exc)
|
|
165
|
+
lines.append("Dependency data not available.")
|
|
166
|
+
lines.append("")
|
|
167
|
+
|
|
168
|
+
return "\n".join(lines)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def generate_wiki(
|
|
172
|
+
store: GraphStore,
|
|
173
|
+
wiki_dir: str | Path,
|
|
174
|
+
force: bool = False,
|
|
175
|
+
) -> dict[str, Any]:
|
|
176
|
+
"""Generate a markdown wiki from the community structure.
|
|
177
|
+
|
|
178
|
+
For each community, generates a markdown page. Also generates an
|
|
179
|
+
index.md with links to all community pages.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
store: The graph store.
|
|
183
|
+
wiki_dir: Directory to write wiki pages into.
|
|
184
|
+
force: If True, regenerate all pages even if content unchanged.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Dict with pages_generated, pages_updated, pages_unchanged counts.
|
|
188
|
+
"""
|
|
189
|
+
wiki_path = Path(wiki_dir)
|
|
190
|
+
wiki_path.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
|
|
192
|
+
communities = get_communities(store)
|
|
193
|
+
|
|
194
|
+
pages_generated = 0
|
|
195
|
+
pages_updated = 0
|
|
196
|
+
pages_unchanged = 0
|
|
197
|
+
|
|
198
|
+
page_entries: list[tuple[str, str, int]] = [] # (slug, name, size)
|
|
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
|
+
|
|
208
|
+
for comm in communities:
|
|
209
|
+
name = comm["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
|
+
|
|
218
|
+
filename = f"{slug}.md"
|
|
219
|
+
filepath = wiki_path / filename
|
|
220
|
+
|
|
221
|
+
content = _generate_community_page(store, comm)
|
|
222
|
+
|
|
223
|
+
if filepath.exists() and not force:
|
|
224
|
+
existing = filepath.read_text(encoding="utf-8", errors="replace")
|
|
225
|
+
if existing == content:
|
|
226
|
+
pages_unchanged += 1
|
|
227
|
+
page_entries.append((slug, name, comm["size"]))
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
already_existed = filepath.exists()
|
|
231
|
+
filepath.write_text(content, encoding="utf-8")
|
|
232
|
+
if already_existed:
|
|
233
|
+
pages_updated += 1
|
|
234
|
+
else:
|
|
235
|
+
pages_generated += 1
|
|
236
|
+
page_entries.append((slug, name, comm["size"]))
|
|
237
|
+
|
|
238
|
+
# Generate index.md
|
|
239
|
+
index_lines: list[str] = []
|
|
240
|
+
index_lines.append("# Code Wiki")
|
|
241
|
+
index_lines.append("")
|
|
242
|
+
index_lines.append(
|
|
243
|
+
"Auto-generated documentation from the code knowledge graph community structure."
|
|
244
|
+
)
|
|
245
|
+
index_lines.append("")
|
|
246
|
+
index_lines.append(f"**Total communities**: {len(communities)}")
|
|
247
|
+
index_lines.append("")
|
|
248
|
+
index_lines.append("## Communities")
|
|
249
|
+
index_lines.append("")
|
|
250
|
+
index_lines.append("| Community | Size | Link |")
|
|
251
|
+
index_lines.append("|-----------|------|------|")
|
|
252
|
+
for slug, name, size in sorted(page_entries, key=lambda x: x[1]):
|
|
253
|
+
index_lines.append(f"| {name} | {size} | [{slug}.md]({slug}.md) |")
|
|
254
|
+
index_lines.append("")
|
|
255
|
+
|
|
256
|
+
index_content = "\n".join(index_lines)
|
|
257
|
+
index_path = wiki_path / "index.md"
|
|
258
|
+
|
|
259
|
+
if index_path.exists() and not force:
|
|
260
|
+
existing_index = index_path.read_text(encoding="utf-8", errors="replace")
|
|
261
|
+
if existing_index == index_content:
|
|
262
|
+
pages_unchanged += 1
|
|
263
|
+
else:
|
|
264
|
+
index_path.write_text(index_content, encoding="utf-8")
|
|
265
|
+
pages_updated += 1
|
|
266
|
+
else:
|
|
267
|
+
index_path.write_text(index_content, encoding="utf-8")
|
|
268
|
+
pages_generated += 1
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
"pages_generated": pages_generated,
|
|
272
|
+
"pages_updated": pages_updated,
|
|
273
|
+
"pages_unchanged": pages_unchanged,
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def get_wiki_page(wiki_dir: str | Path, page_name: str) -> str | None:
|
|
278
|
+
"""Retrieve a specific wiki page by community name.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
wiki_dir: Directory containing wiki pages.
|
|
282
|
+
page_name: Community name (will be slugified for filename lookup).
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Page content as a string, or None if the page does not exist.
|
|
286
|
+
"""
|
|
287
|
+
wiki_path = Path(wiki_dir)
|
|
288
|
+
slug = _slugify(page_name)
|
|
289
|
+
filepath = wiki_path / f"{slug}.md"
|
|
290
|
+
|
|
291
|
+
if filepath.is_file():
|
|
292
|
+
return filepath.read_text(encoding="utf-8", errors="replace")
|
|
293
|
+
|
|
294
|
+
# Fallback: try exact filename match — with path traversal protection
|
|
295
|
+
exact_path = (wiki_path / page_name).resolve()
|
|
296
|
+
if exact_path.is_file() and exact_path.is_relative_to(wiki_path.resolve()):
|
|
297
|
+
return exact_path.read_text(encoding="utf-8", errors="replace")
|
|
298
|
+
|
|
299
|
+
# Fallback: search for partial match
|
|
300
|
+
if wiki_path.is_dir():
|
|
301
|
+
for p in wiki_path.iterdir():
|
|
302
|
+
if p.suffix == ".md" and slug in p.stem:
|
|
303
|
+
return p.read_text(encoding="utf-8", errors="replace")
|
|
304
|
+
|
|
305
|
+
return None
|