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,477 @@
1
+ """Tools 4, 12, 16: review context, affected flows, detect changes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from ..changes import analyze_changes, parse_diff_ranges, parse_git_diff_ranges # noqa: F401
10
+ from ..context_savings import attach_context_savings, estimate_file_tokens
11
+ from ..flows import get_affected_flows as _get_affected_flows
12
+ from ..graph import edge_to_dict, node_to_dict
13
+ from ..hints import generate_hints, get_session
14
+ from ..incremental import get_changed_files, get_staged_and_unstaged
15
+ from ._common import _get_store, _resolve_graph_file_paths
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Tool 4: get_review_context
22
+ # ---------------------------------------------------------------------------
23
+
24
+
25
+ def get_review_context(
26
+ changed_files: list[str] | None = None,
27
+ max_depth: int = 2,
28
+ include_source: bool = True,
29
+ max_lines_per_file: int = 200,
30
+ repo_root: str | None = None,
31
+ base: str = "HEAD~1",
32
+ detail_level: str = "standard",
33
+ ) -> dict[str, Any]:
34
+ """Generate a focused review context from changed files.
35
+
36
+ Builds a token-optimized subgraph + source snippets for code review.
37
+
38
+ Args:
39
+ changed_files: Files to review (auto-detected from git diff if omitted).
40
+ max_depth: Impact radius depth (default: 2).
41
+ include_source: Whether to include source code snippets (default: True).
42
+ max_lines_per_file: Max source lines per file in output (default: 200).
43
+ repo_root: Repository root path. Auto-detected if omitted.
44
+ base: Git ref for change detection (default: HEAD~1).
45
+ detail_level: Output detail level. "standard" returns full context;
46
+ "minimal" returns summary, risk level, changed/impacted file counts,
47
+ top 5 key entity names, test gap count, and next tool suggestions.
48
+ Default: "standard".
49
+
50
+ Returns:
51
+ Structured review context with subgraph, source snippets, and
52
+ review guidance.
53
+ """
54
+ store, root = _get_store(repo_root)
55
+ try:
56
+ # Get impact radius first
57
+ if changed_files is None:
58
+ changed_files = get_changed_files(root, base)
59
+ if not changed_files:
60
+ changed_files = get_staged_and_unstaged(root)
61
+
62
+ if not changed_files:
63
+ return {
64
+ "status": "ok",
65
+ "summary": "No changes detected. Nothing to review.",
66
+ "context": {},
67
+ }
68
+
69
+ graph_files = _resolve_graph_file_paths(store, root, changed_files)
70
+ original_tokens = estimate_file_tokens(root, changed_files)
71
+ impact = store.get_impact_radius(graph_files, max_depth=max_depth)
72
+
73
+ if detail_level == "minimal":
74
+ impacted_count = len(impact["impacted_nodes"])
75
+ if impacted_count > 20:
76
+ risk = "high"
77
+ elif impacted_count > 5:
78
+ risk = "medium"
79
+ else:
80
+ risk = "low"
81
+
82
+ key_entities = [
83
+ n.name for n in impact["changed_nodes"][:5]
84
+ ]
85
+
86
+ # Count test gaps among changed functions.
87
+ changed_funcs = [
88
+ n for n in impact["changed_nodes"]
89
+ if n.kind == "Function" and not n.is_test
90
+ ]
91
+ test_edges = [
92
+ e for e in impact["edges"] if e.kind == "TESTED_BY"
93
+ ]
94
+ tested_qualified = {e.source_qualified for e in test_edges}
95
+ test_gap_count = sum(
96
+ 1 for f in changed_funcs
97
+ if f.qualified_name not in tested_qualified
98
+ )
99
+
100
+ summary_parts = [
101
+ f"Review context for {len(changed_files)} changed file(s):",
102
+ f" - Risk: {risk}",
103
+ f" - {len(impact['impacted_nodes'])} impacted nodes"
104
+ f" in {len(impact['impacted_files'])} files",
105
+ ]
106
+
107
+ result = {
108
+ "status": "ok",
109
+ "summary": "\n".join(summary_parts),
110
+ "risk": risk,
111
+ "changed_file_count": len(changed_files),
112
+ "impacted_file_count": len(impact["impacted_files"]),
113
+ "key_entities": key_entities,
114
+ "test_gaps": test_gap_count,
115
+ "next_tool_suggestions": [
116
+ "detect_changes",
117
+ "get_affected_flows",
118
+ "get_impact_radius",
119
+ ],
120
+ }
121
+ attach_context_savings(result, original_tokens=original_tokens)
122
+ return result
123
+
124
+ # Build review context
125
+ context: dict[str, Any] = {
126
+ "changed_files": changed_files,
127
+ "impacted_files": impact["impacted_files"],
128
+ "graph": {
129
+ "changed_nodes": [
130
+ node_to_dict(n) for n in impact["changed_nodes"]
131
+ ],
132
+ "impacted_nodes": [
133
+ node_to_dict(n) for n in impact["impacted_nodes"]
134
+ ],
135
+ "edges": [edge_to_dict(e) for e in impact["edges"]],
136
+ },
137
+ }
138
+
139
+ # Add source snippets for changed files
140
+ if include_source:
141
+ snippets = {}
142
+ for rel_path in changed_files:
143
+ full_path = root / rel_path
144
+ if full_path.is_file():
145
+ try:
146
+ lines = full_path.read_text(
147
+ errors="replace"
148
+ ).splitlines()
149
+ if len(lines) > max_lines_per_file:
150
+ # Include only the relevant functions/classes
151
+ relevant_lines = _extract_relevant_lines(
152
+ lines,
153
+ impact["changed_nodes"],
154
+ str(full_path),
155
+ )
156
+ snippets[rel_path] = relevant_lines
157
+ else:
158
+ snippets[rel_path] = "\n".join(
159
+ f"{i+1}: {line}"
160
+ for i, line in enumerate(lines)
161
+ )
162
+ except (OSError, UnicodeDecodeError):
163
+ snippets[rel_path] = "(could not read file)"
164
+ context["source_snippets"] = snippets
165
+
166
+ # Generate review guidance
167
+ guidance = _generate_review_guidance(impact, changed_files)
168
+ context["review_guidance"] = guidance
169
+
170
+ summary_parts = [
171
+ f"Review context for {len(changed_files)} changed file(s):",
172
+ f" - {len(impact['changed_nodes'])} directly changed nodes",
173
+ f" - {len(impact['impacted_nodes'])} impacted nodes"
174
+ f" in {len(impact['impacted_files'])} files",
175
+ "",
176
+ "Review guidance:",
177
+ guidance,
178
+ ]
179
+
180
+ result = {
181
+ "status": "ok",
182
+ "summary": "\n".join(summary_parts),
183
+ "context": context,
184
+ }
185
+ attach_context_savings(result, original_tokens=original_tokens)
186
+ return result
187
+ finally:
188
+ store.close()
189
+
190
+
191
+ def _extract_relevant_lines(
192
+ lines: list[str], nodes: list, file_path: str
193
+ ) -> str:
194
+ """Extract only the lines relevant to changed nodes."""
195
+ ranges = []
196
+ for n in nodes:
197
+ if n.file_path == file_path:
198
+ start = max(0, n.line_start - 3) # 2 lines context before
199
+ end = min(len(lines), n.line_end + 2) # 1 line context after
200
+ ranges.append((start, end))
201
+
202
+ if not ranges:
203
+ # Show first N lines as fallback
204
+ return "\n".join(
205
+ f"{i+1}: {line}" for i, line in enumerate(lines[:50])
206
+ )
207
+
208
+ # Merge overlapping ranges
209
+ ranges.sort()
210
+ merged = [ranges[0]]
211
+ for start, end in ranges[1:]:
212
+ if start <= merged[-1][1] + 1:
213
+ merged[-1] = (merged[-1][0], max(merged[-1][1], end))
214
+ else:
215
+ merged.append((start, end))
216
+
217
+ parts: list[str] = []
218
+ for start, end in merged:
219
+ if parts:
220
+ parts.append("...")
221
+ for i in range(start, end):
222
+ parts.append(f"{i+1}: {lines[i]}")
223
+
224
+ return "\n".join(parts)
225
+
226
+
227
+ def _generate_review_guidance(
228
+ impact: dict, changed_files: list[str]
229
+ ) -> str:
230
+ """Generate review guidance based on the impact analysis."""
231
+ guidance_parts = []
232
+
233
+ # Check for test coverage
234
+ changed_funcs = [
235
+ n for n in impact["changed_nodes"] if n.kind == "Function"
236
+ ]
237
+ test_edges = [e for e in impact["edges"] if e.kind == "TESTED_BY"]
238
+ tested_funcs = {e.source_qualified for e in test_edges}
239
+
240
+ untested = [
241
+ f for f in changed_funcs
242
+ if f.qualified_name not in tested_funcs and not f.is_test
243
+ ]
244
+ if untested:
245
+ guidance_parts.append(
246
+ f"- {len(untested)} changed function(s) lack test coverage: "
247
+ + ", ".join(n.name for n in untested[:5])
248
+ )
249
+
250
+ # Check for wide blast radius
251
+ if len(impact["impacted_nodes"]) > 20:
252
+ guidance_parts.append(
253
+ f"- Wide blast radius: {len(impact['impacted_nodes'])} "
254
+ "nodes impacted. "
255
+ "Review callers and dependents carefully."
256
+ )
257
+
258
+ # Check for inheritance changes
259
+ inheritance_edges = [
260
+ e for e in impact["edges"]
261
+ if e.kind in ("INHERITS", "IMPLEMENTS")
262
+ ]
263
+ if inheritance_edges:
264
+ guidance_parts.append(
265
+ f"- {len(inheritance_edges)} inheritance/implementation "
266
+ "relationship(s) affected. "
267
+ "Check for Liskov substitution violations."
268
+ )
269
+
270
+ # Check for cross-file impact
271
+ impacted_file_count = len(impact["impacted_files"])
272
+ if impacted_file_count > 3:
273
+ guidance_parts.append(
274
+ f"- Changes impact {impacted_file_count} other files."
275
+ " Consider splitting into smaller PRs."
276
+ )
277
+
278
+ if not guidance_parts:
279
+ guidance_parts.append(
280
+ "- Changes appear well-contained with minimal blast radius."
281
+ )
282
+
283
+ return "\n".join(guidance_parts)
284
+
285
+
286
+ # ---------------------------------------------------------------------------
287
+ # Tool 12: get_affected_flows [REVIEW]
288
+ # ---------------------------------------------------------------------------
289
+
290
+
291
+ def get_affected_flows_func(
292
+ changed_files: list[str] | None = None,
293
+ base: str = "HEAD~1",
294
+ repo_root: str | None = None,
295
+ ) -> dict[str, Any]:
296
+ """Find execution flows affected by changed files.
297
+
298
+ [REVIEW] Identifies which execution flows pass through nodes in the
299
+ changed files. Useful during code review to understand which user-facing
300
+ or critical paths are affected by a change.
301
+
302
+ Args:
303
+ changed_files: List of changed file paths (relative to repo root).
304
+ Auto-detected from git diff if omitted.
305
+ base: Git ref for auto-detecting changes (default: HEAD~1).
306
+ repo_root: Repository root path. Auto-detected if omitted.
307
+
308
+ Returns:
309
+ Affected flows sorted by criticality, with step details.
310
+ """
311
+ store, root = _get_store(repo_root)
312
+ try:
313
+ if changed_files is None:
314
+ changed_files = get_changed_files(root, base)
315
+ if not changed_files:
316
+ changed_files = get_staged_and_unstaged(root)
317
+
318
+ if not changed_files:
319
+ return {
320
+ "status": "ok",
321
+ "summary": "No changed files detected.",
322
+ "affected_flows": [],
323
+ "total": 0,
324
+ }
325
+
326
+ # Convert to absolute paths for graph lookup
327
+ abs_files = [str(root / f) for f in changed_files]
328
+ result = _get_affected_flows(store, abs_files)
329
+
330
+ total = result["total"]
331
+ out = {
332
+ "status": "ok",
333
+ "summary": (
334
+ f"{total} flow(s) affected by changes "
335
+ f"in {len(changed_files)} file(s)"
336
+ ),
337
+ "changed_files": changed_files,
338
+ "affected_flows": result["affected_flows"],
339
+ "total": total,
340
+ }
341
+ out["_hints"] = generate_hints(
342
+ "get_affected_flows", out, get_session()
343
+ )
344
+ return out
345
+ except Exception as exc:
346
+ return {"status": "error", "error": str(exc)}
347
+ finally:
348
+ store.close()
349
+
350
+
351
+ # ---------------------------------------------------------------------------
352
+ # Tool 16: detect_changes [REVIEW]
353
+ # ---------------------------------------------------------------------------
354
+
355
+
356
+ def detect_changes_func(
357
+ base: str = "HEAD~1",
358
+ changed_files: list[str] | None = None,
359
+ include_source: bool = False,
360
+ max_depth: int = 2,
361
+ repo_root: str | None = None,
362
+ detail_level: str = "standard",
363
+ ) -> dict[str, Any]:
364
+ """Detect changes and produce risk-scored review guidance.
365
+
366
+ [REVIEW] Primary tool for code review. Maps git diffs to affected
367
+ functions, flows, communities, and test coverage gaps. Returns
368
+ priority-ordered review guidance with risk scores.
369
+
370
+ Args:
371
+ base: Git ref to diff against (default: HEAD~1).
372
+ changed_files: Explicit list of changed file paths (relative to repo
373
+ root). Auto-detected from git diff if omitted.
374
+ include_source: If True, include source code snippets for changed
375
+ functions. Default: False.
376
+ max_depth: Impact radius depth for BFS traversal. Default: 2.
377
+ repo_root: Repository root path. Auto-detected if omitted.
378
+ detail_level: Output detail level. "standard" returns full analysis;
379
+ "minimal" returns only summary, risk_score, changed_file_count,
380
+ test_gap_count, and top 3 review priorities (text only).
381
+ Default: "standard".
382
+
383
+ Returns:
384
+ Risk-scored analysis with changed functions, affected flows,
385
+ test gaps, and review priorities.
386
+ """
387
+ store, root = _get_store(repo_root)
388
+ try:
389
+ # Detect changed files if not provided.
390
+ if changed_files is None:
391
+ changed_files = get_changed_files(root, base)
392
+ if not changed_files:
393
+ changed_files = get_staged_and_unstaged(root)
394
+
395
+ if not changed_files:
396
+ return {
397
+ "status": "ok",
398
+ "summary": "No changed files detected.",
399
+ "risk_score": 0.0,
400
+ "changed_functions": [],
401
+ "affected_flows": [],
402
+ "test_gaps": [],
403
+ "review_priorities": [],
404
+ }
405
+
406
+ original_tokens = estimate_file_tokens(root, changed_files)
407
+
408
+ # Convert to absolute paths for graph lookup.
409
+ abs_files = [str(root / f) for f in changed_files]
410
+
411
+ # Parse diff ranges for line-level mapping.
412
+ diff_ranges = parse_diff_ranges(str(root), base)
413
+ # Remap to absolute paths so they match graph file_paths.
414
+ abs_ranges: dict[str, list[tuple[int, int]]] = {}
415
+ for rel_path, ranges in diff_ranges.items():
416
+ abs_path = str(root / rel_path)
417
+ abs_ranges[abs_path] = ranges
418
+
419
+ analysis = analyze_changes(
420
+ store,
421
+ changed_files=abs_files,
422
+ changed_ranges=abs_ranges if abs_ranges else None,
423
+ repo_root=str(root),
424
+ base=base,
425
+ )
426
+
427
+ # Optionally include source snippets for changed functions.
428
+ if include_source:
429
+ for func in analysis.get("changed_functions", []):
430
+ fp = func.get("file_path")
431
+ ls = func.get("line_start")
432
+ le = func.get("line_end")
433
+ if fp and ls and le:
434
+ file_path = Path(fp)
435
+ if file_path.is_file():
436
+ try:
437
+ lines = file_path.read_text(
438
+ errors="replace"
439
+ ).splitlines()
440
+ start = max(0, ls - 1)
441
+ end = min(len(lines), le)
442
+ func["source"] = "\n".join(
443
+ f"{i + 1}: {lines[i]}"
444
+ for i in range(start, end)
445
+ )
446
+ except (OSError, UnicodeDecodeError):
447
+ func["source"] = "(could not read file)"
448
+
449
+ if detail_level == "minimal":
450
+ priorities = analysis.get("review_priorities", [])
451
+ top_priorities = [
452
+ p.get("name", p.get("qualified_name", ""))
453
+ for p in priorities[:3]
454
+ ]
455
+ result: dict[str, Any] = {
456
+ "status": "ok",
457
+ "summary": analysis.get("summary", ""),
458
+ "risk_score": analysis.get("risk_score", 0.0),
459
+ "changed_file_count": len(changed_files),
460
+ "test_gap_count": len(analysis.get("test_gaps", [])),
461
+ "review_priorities": top_priorities,
462
+ }
463
+ else:
464
+ result = {
465
+ "status": "ok",
466
+ "changed_files": changed_files,
467
+ **analysis,
468
+ }
469
+ result["_hints"] = generate_hints(
470
+ "detect_changes", result, get_session()
471
+ )
472
+ attach_context_savings(result, original_tokens=original_tokens)
473
+ return result
474
+ except Exception as exc:
475
+ return {"status": "error", "error": str(exc)}
476
+ finally:
477
+ store.close()