code-review-ai-cli 2.2.5__tar.gz → 2.2.6__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.
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/PKG-INFO +3 -4
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/code_review_ai_cli.egg-info/PKG-INFO +3 -4
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/code_review_ai_cli.egg-info/requires.txt +2 -4
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/pyproject.toml +3 -3
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/src/config.py +2 -2
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/src/context_extractor.py +96 -190
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/src/git_utils.py +23 -11
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/src/llm_client.py +77 -76
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/src/prompts/config.yaml.template +3 -3
- code_review_ai_cli-2.2.6/src/prompts/review_prompt.md.template +21 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/src/tfs_client.py +9 -13
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/tests/test_context_extractor.py +94 -88
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/tests/test_git_utils.py +26 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/tests/test_llm_client.py +87 -17
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/tests/test_tfs_client.py +36 -5
- code_review_ai_cli-2.2.5/src/prompts/review_prompt.md.template +0 -85
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/README.md +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/code_review_ai_cli.egg-info/SOURCES.txt +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/code_review_ai_cli.egg-info/dependency_links.txt +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/code_review_ai_cli.egg-info/entry_points.txt +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/code_review_ai_cli.egg-info/top_level.txt +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/setup.cfg +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/src/__init__.py +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/src/ai_review.py +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/src/formatter.py +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/src/prompt_utils.py +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/src/token_estimator.py +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/tests/test_ai_review.py +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/tests/test_config.py +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/tests/test_formatter.py +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/tests/test_prompt_utils.py +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/tests/test_token_estimator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-review-ai-cli
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.6
|
|
4
4
|
Summary: Automated AI-powered code review CLI for Azure DevOps / TFS Pull Requests
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: code-review,ai,azure-devops,tfs,pull-request,llm
|
|
@@ -19,6 +19,8 @@ Requires-Dist: requests>=2.28.0
|
|
|
19
19
|
Requires-Dist: PyYAML>=6.0
|
|
20
20
|
Requires-Dist: litellm>=1.40.0
|
|
21
21
|
Requires-Dist: tiktoken>=0.7.0
|
|
22
|
+
Requires-Dist: tree-sitter<0.22,>=0.20
|
|
23
|
+
Requires-Dist: tree-sitter-languages>=1.10.2
|
|
22
24
|
Provides-Extra: bedrock
|
|
23
25
|
Requires-Dist: boto3>=1.34.0; extra == "bedrock"
|
|
24
26
|
Provides-Extra: openai
|
|
@@ -27,8 +29,6 @@ Provides-Extra: gemini
|
|
|
27
29
|
Requires-Dist: google-generativeai>=0.3.0; extra == "gemini"
|
|
28
30
|
Provides-Extra: claude
|
|
29
31
|
Requires-Dist: anthropic>=0.18.0; extra == "claude"
|
|
30
|
-
Provides-Extra: tree-sitter
|
|
31
|
-
Requires-Dist: tree-sitter-languages>=1.10.2; extra == "tree-sitter"
|
|
32
32
|
Provides-Extra: all
|
|
33
33
|
Requires-Dist: boto3>=1.34.0; extra == "all"
|
|
34
34
|
Requires-Dist: litellm>=1.40.0; extra == "all"
|
|
@@ -36,7 +36,6 @@ Requires-Dist: tiktoken>=0.7.0; extra == "all"
|
|
|
36
36
|
Requires-Dist: openai>=1.0.0; extra == "all"
|
|
37
37
|
Requires-Dist: google-generativeai>=0.3.0; extra == "all"
|
|
38
38
|
Requires-Dist: anthropic>=0.18.0; extra == "all"
|
|
39
|
-
Requires-Dist: tree-sitter-languages>=1.10.2; extra == "all"
|
|
40
39
|
Provides-Extra: dev
|
|
41
40
|
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
42
41
|
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-review-ai-cli
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.6
|
|
4
4
|
Summary: Automated AI-powered code review CLI for Azure DevOps / TFS Pull Requests
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: code-review,ai,azure-devops,tfs,pull-request,llm
|
|
@@ -19,6 +19,8 @@ Requires-Dist: requests>=2.28.0
|
|
|
19
19
|
Requires-Dist: PyYAML>=6.0
|
|
20
20
|
Requires-Dist: litellm>=1.40.0
|
|
21
21
|
Requires-Dist: tiktoken>=0.7.0
|
|
22
|
+
Requires-Dist: tree-sitter<0.22,>=0.20
|
|
23
|
+
Requires-Dist: tree-sitter-languages>=1.10.2
|
|
22
24
|
Provides-Extra: bedrock
|
|
23
25
|
Requires-Dist: boto3>=1.34.0; extra == "bedrock"
|
|
24
26
|
Provides-Extra: openai
|
|
@@ -27,8 +29,6 @@ Provides-Extra: gemini
|
|
|
27
29
|
Requires-Dist: google-generativeai>=0.3.0; extra == "gemini"
|
|
28
30
|
Provides-Extra: claude
|
|
29
31
|
Requires-Dist: anthropic>=0.18.0; extra == "claude"
|
|
30
|
-
Provides-Extra: tree-sitter
|
|
31
|
-
Requires-Dist: tree-sitter-languages>=1.10.2; extra == "tree-sitter"
|
|
32
32
|
Provides-Extra: all
|
|
33
33
|
Requires-Dist: boto3>=1.34.0; extra == "all"
|
|
34
34
|
Requires-Dist: litellm>=1.40.0; extra == "all"
|
|
@@ -36,7 +36,6 @@ Requires-Dist: tiktoken>=0.7.0; extra == "all"
|
|
|
36
36
|
Requires-Dist: openai>=1.0.0; extra == "all"
|
|
37
37
|
Requires-Dist: google-generativeai>=0.3.0; extra == "all"
|
|
38
38
|
Requires-Dist: anthropic>=0.18.0; extra == "all"
|
|
39
|
-
Requires-Dist: tree-sitter-languages>=1.10.2; extra == "all"
|
|
40
39
|
Provides-Extra: dev
|
|
41
40
|
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
42
41
|
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
{code_review_ai_cli-2.2.5 → code_review_ai_cli-2.2.6}/code_review_ai_cli.egg-info/requires.txt
RENAMED
|
@@ -2,6 +2,8 @@ requests>=2.28.0
|
|
|
2
2
|
PyYAML>=6.0
|
|
3
3
|
litellm>=1.40.0
|
|
4
4
|
tiktoken>=0.7.0
|
|
5
|
+
tree-sitter<0.22,>=0.20
|
|
6
|
+
tree-sitter-languages>=1.10.2
|
|
5
7
|
|
|
6
8
|
[all]
|
|
7
9
|
boto3>=1.34.0
|
|
@@ -10,7 +12,6 @@ tiktoken>=0.7.0
|
|
|
10
12
|
openai>=1.0.0
|
|
11
13
|
google-generativeai>=0.3.0
|
|
12
14
|
anthropic>=0.18.0
|
|
13
|
-
tree-sitter-languages>=1.10.2
|
|
14
15
|
|
|
15
16
|
[bedrock]
|
|
16
17
|
boto3>=1.34.0
|
|
@@ -28,6 +29,3 @@ google-generativeai>=0.3.0
|
|
|
28
29
|
|
|
29
30
|
[openai]
|
|
30
31
|
openai>=1.0.0
|
|
31
|
-
|
|
32
|
-
[tree-sitter]
|
|
33
|
-
tree-sitter-languages>=1.10.2
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "code-review-ai-cli"
|
|
7
|
-
version = "2.2.
|
|
7
|
+
version = "2.2.6"
|
|
8
8
|
description = "Automated AI-powered code review CLI for Azure DevOps / TFS Pull Requests"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -26,6 +26,8 @@ dependencies = [
|
|
|
26
26
|
"PyYAML>=6.0",
|
|
27
27
|
"litellm>=1.40.0",
|
|
28
28
|
"tiktoken>=0.7.0",
|
|
29
|
+
"tree-sitter>=0.20,<0.22",
|
|
30
|
+
"tree-sitter-languages>=1.10.2",
|
|
29
31
|
]
|
|
30
32
|
|
|
31
33
|
[project.optional-dependencies]
|
|
@@ -33,7 +35,6 @@ bedrock = ["boto3>=1.34.0"]
|
|
|
33
35
|
openai = ["openai>=1.0.0"]
|
|
34
36
|
gemini = ["google-generativeai>=0.3.0"]
|
|
35
37
|
claude = ["anthropic>=0.18.0"]
|
|
36
|
-
tree-sitter = ["tree-sitter-languages>=1.10.2"]
|
|
37
38
|
all = [
|
|
38
39
|
"boto3>=1.34.0",
|
|
39
40
|
"litellm>=1.40.0",
|
|
@@ -41,7 +42,6 @@ all = [
|
|
|
41
42
|
"openai>=1.0.0",
|
|
42
43
|
"google-generativeai>=0.3.0",
|
|
43
44
|
"anthropic>=0.18.0",
|
|
44
|
-
"tree-sitter-languages>=1.10.2",
|
|
45
45
|
]
|
|
46
46
|
dev = [
|
|
47
47
|
"pytest>=8.3.0",
|
|
@@ -92,8 +92,8 @@ class ReviewConfig:
|
|
|
92
92
|
review_scope: str = "diff_only" # "diff_only" | "full_code"
|
|
93
93
|
max_diff_files: int = 50 # Max diff files sent to LLM
|
|
94
94
|
max_diff_lines: int = 2000 # Max diff lines
|
|
95
|
-
max_scope_blocks: int =
|
|
96
|
-
max_scope_lines: int = 250 # Max lines in a
|
|
95
|
+
max_scope_blocks: int = 8 # Max changed blocks for Enclosing Scopes strategy
|
|
96
|
+
max_scope_lines: int = 250 # Max lines in a scope before Adaptive Diff fallback
|
|
97
97
|
adaptive_context_lines: int = 10 # Context lines for Adaptive Diff fallback
|
|
98
98
|
custom_prompt_file: str = "review_prompt.md" # Markdown file with extra rules/context
|
|
99
99
|
file_extensions_filter: list = field(default_factory=list) # File extensions to include (empty = all)
|
|
@@ -7,20 +7,19 @@ Context Extractor Module - AI Code Review
|
|
|
7
7
|
Provides AST-based context extraction for diff payloads sent to the LLM.
|
|
8
8
|
|
|
9
9
|
Strategy selection per file:
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
- Tree-sitter unavailable / language unsupported → Adaptive Diff fallback
|
|
10
|
+
- AST scopes within the configured limits → Enclosing Scopes
|
|
11
|
+
- Tree-sitter unavailable, unsupported language, parse failure, or scopes
|
|
12
|
+
outside the configured limits → Adaptive Diff fallback
|
|
14
13
|
|
|
15
14
|
Output is always wrapped in XML tags so the LLM can distinguish code context
|
|
16
15
|
from the actual diff:
|
|
17
16
|
|
|
18
17
|
``<enclosing_scopes file="src/auth.py">...</enclosing_scopes>``
|
|
19
|
-
``<file_skeleton file="src/module.py">...</file_skeleton>``
|
|
20
18
|
``<adaptive_diff file="src/other.py">...</adaptive_diff>``
|
|
21
19
|
|
|
22
|
-
Tree-sitter is
|
|
23
|
-
|
|
20
|
+
Tree-sitter is installed as a core dependency. The availability probe remains
|
|
21
|
+
defensive: reduced or broken environments fall back to
|
|
22
|
+
:func:`_build_adaptive_diff` instead of failing the review.
|
|
24
23
|
"""
|
|
25
24
|
|
|
26
25
|
from __future__ import annotations
|
|
@@ -68,13 +67,12 @@ except Exception: # pragma: no cover — optional dependency
|
|
|
68
67
|
# Constants
|
|
69
68
|
# ---------------------------------------------------------------------------
|
|
70
69
|
|
|
71
|
-
#: Maximum number of distinct logical blocks
|
|
72
|
-
#:
|
|
73
|
-
|
|
74
|
-
MAX_SCOPE_BLOCKS: int = 3
|
|
70
|
+
#: Maximum number of distinct logical blocks included as Enclosing Scopes.
|
|
71
|
+
#: When more blocks are detected the extractor uses Adaptive Diff instead.
|
|
72
|
+
MAX_SCOPE_BLOCKS: int = 8
|
|
75
73
|
|
|
76
74
|
#: Maximum number of lines in a single enclosing scope. When a scope
|
|
77
|
-
#: exceeds this limit the strategy falls back to
|
|
75
|
+
#: exceeds this limit the strategy falls back to Adaptive Diff to avoid
|
|
78
76
|
#: sending large class bodies to the LLM.
|
|
79
77
|
MAX_SCOPE_LINES: int = 250
|
|
80
78
|
|
|
@@ -370,147 +368,76 @@ def _extract_enclosing_scopes_ts(
|
|
|
370
368
|
return scopes
|
|
371
369
|
|
|
372
370
|
|
|
373
|
-
def
|
|
374
|
-
"""
|
|
375
|
-
lines: list[str] = []
|
|
376
|
-
for offset, src_line in enumerate(scope.source_lines):
|
|
377
|
-
abs_line = scope.start_line + offset
|
|
378
|
-
lines.append(f"{abs_line:4d}: {src_line}")
|
|
379
|
-
return "\n".join(lines)
|
|
380
|
-
|
|
371
|
+
def _non_whitespace_changed_lines(content: str, changed_lines: list[int]) -> list[int]:
|
|
372
|
+
"""Return changed lines that contain meaningful code in the new file.
|
|
381
373
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
#: Regex patterns used to identify structural lines in each language.
|
|
387
|
-
_SKELETON_PATTERNS: dict[str, list[str]] = {
|
|
388
|
-
"python": [
|
|
389
|
-
r"^\s*(?:import|from)\s+", # imports
|
|
390
|
-
r"^\s*(?:def|async\s+def|class)\s+", # definitions
|
|
391
|
-
r"^\s*@\w", # decorators
|
|
392
|
-
],
|
|
393
|
-
"javascript": [
|
|
394
|
-
r"^\s*(?:import|export|require)\s*",
|
|
395
|
-
r"^\s*(?:function|class|const|let|var)\s+\w+\s*(?:=\s*(?:function|\(|\()|\()",
|
|
396
|
-
r"^\s*(?:async\s+function|export\s+(?:default\s+)?(?:function|class))",
|
|
397
|
-
],
|
|
398
|
-
"typescript": [
|
|
399
|
-
r"^\s*(?:import|export|require)\s*",
|
|
400
|
-
r"^\s*(?:function|class|const|let|var|interface|type|enum|abstract)\s+\w+",
|
|
401
|
-
r"^\s*(?:async\s+function|export\s+(?:default\s+)?(?:function|class|interface))",
|
|
402
|
-
],
|
|
403
|
-
"tsx": [
|
|
404
|
-
r"^\s*(?:import|export|require)\s*",
|
|
405
|
-
r"^\s*(?:function|class|const|let|var|interface|type|enum|abstract)\s+\w+",
|
|
406
|
-
],
|
|
407
|
-
"c_sharp": [
|
|
408
|
-
r"^\s*(?:using)\s+",
|
|
409
|
-
r"^\s*(?:namespace|public|private|protected|internal|static|sealed|abstract|partial|override|virtual)\s+",
|
|
410
|
-
r"^\s*(?:\[[\w\.\(\)\"]+\])", # attributes
|
|
411
|
-
],
|
|
412
|
-
"java": [
|
|
413
|
-
r"^\s*(?:import|package)\s+",
|
|
414
|
-
r"^\s*(?:public|private|protected|static|final|abstract|synchronized|@)\s*",
|
|
415
|
-
],
|
|
416
|
-
"sql": [], # SQL skeleton falls back to individual statements
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
# Body-start patterns (lines after which we can insert "...")
|
|
420
|
-
_BODY_START: dict[str, list[str]] = {
|
|
421
|
-
"python": [r":\s*$"],
|
|
422
|
-
"javascript": [r"\{\s*$"],
|
|
423
|
-
"typescript": [r"\{\s*$"],
|
|
424
|
-
"tsx": [r"\{\s*$"],
|
|
425
|
-
"c_sharp": [r"\{\s*$"],
|
|
426
|
-
"java": [r"\{\s*$"],
|
|
427
|
-
"sql": [],
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
def _build_file_skeleton(
|
|
432
|
-
content: str,
|
|
433
|
-
changed_lines: list[int],
|
|
434
|
-
lang: str,
|
|
435
|
-
) -> str:
|
|
436
|
-
"""Build a file skeleton: structural lines + changed blocks + ``...`` placeholders.
|
|
437
|
-
|
|
438
|
-
The skeleton includes:
|
|
439
|
-
- All structural/declaration lines (imports, signatures, class headers)
|
|
440
|
-
- Full content of lines inside changed regions (± ADAPTIVE_CONTEXT_LINES)
|
|
441
|
-
- ``...`` for bodies of unchanged functions/methods
|
|
442
|
-
|
|
443
|
-
Args:
|
|
444
|
-
content: Full source file content.
|
|
445
|
-
changed_lines: 1-based line numbers that were changed.
|
|
446
|
-
lang: Tree-sitter language name for pattern selection.
|
|
447
|
-
|
|
448
|
-
Returns:
|
|
449
|
-
Formatted skeleton string with 1-based line numbers.
|
|
374
|
+
Whitespace-only additions must not influence AST scope selection. For
|
|
375
|
+
example, a blank line immediately after a method belongs to its containing
|
|
376
|
+
class and would otherwise cause the entire class to be selected.
|
|
450
377
|
"""
|
|
451
378
|
all_lines = content.splitlines()
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
always: set[int] = set()
|
|
459
|
-
for ln in changed_lines:
|
|
460
|
-
lo = max(1, ln - ADAPTIVE_CONTEXT_LINES)
|
|
461
|
-
hi = min(total, ln + ADAPTIVE_CONTEXT_LINES)
|
|
462
|
-
always.update(range(lo, hi + 1))
|
|
463
|
-
|
|
464
|
-
# Structural patterns for this language
|
|
465
|
-
struct_patterns = [re.compile(p) for p in _SKELETON_PATTERNS.get(lang, [])]
|
|
466
|
-
|
|
467
|
-
result_lines: list[str] = []
|
|
468
|
-
skipping = False
|
|
469
|
-
prev_shown: Optional[int] = None
|
|
470
|
-
|
|
471
|
-
for idx, src in enumerate(all_lines, start=1):
|
|
472
|
-
is_structural = any(p.search(src) for p in struct_patterns)
|
|
473
|
-
in_changed = idx in always
|
|
474
|
-
|
|
475
|
-
if in_changed or is_structural:
|
|
476
|
-
if skipping and prev_shown is not None:
|
|
477
|
-
result_lines.append(" ...")
|
|
478
|
-
skipping = False
|
|
479
|
-
result_lines.append(f"{idx:4d}: {src}")
|
|
480
|
-
prev_shown = idx
|
|
481
|
-
else:
|
|
482
|
-
skipping = True
|
|
483
|
-
|
|
484
|
-
if skipping:
|
|
485
|
-
result_lines.append(" ...")
|
|
379
|
+
return [
|
|
380
|
+
line_number
|
|
381
|
+
for line_number in changed_lines
|
|
382
|
+
if 1 <= line_number <= len(all_lines)
|
|
383
|
+
and all_lines[line_number - 1].strip()
|
|
384
|
+
]
|
|
486
385
|
|
|
487
|
-
return "\n".join(result_lines)
|
|
488
386
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
# SQL-specific extraction
|
|
492
|
-
# ---------------------------------------------------------------------------
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
def _extract_sql_statement(
|
|
496
|
-
content: str,
|
|
387
|
+
def _remove_redundant_ancestor_scopes(
|
|
388
|
+
scopes: list[ScopeInfo],
|
|
497
389
|
changed_lines: list[int],
|
|
498
|
-
lang: str,
|
|
499
390
|
) -> list[ScopeInfo]:
|
|
500
|
-
"""
|
|
391
|
+
"""Drop ancestor scopes whose changed lines are covered by child scopes.
|
|
501
392
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
393
|
+
An ancestor is retained when it contains at least one meaningful changed
|
|
394
|
+
line outside every selected descendant. This avoids duplicate class and
|
|
395
|
+
method bodies without losing class-level changes.
|
|
396
|
+
"""
|
|
397
|
+
retained: list[ScopeInfo] = []
|
|
398
|
+
|
|
399
|
+
for scope in scopes:
|
|
400
|
+
descendants = [
|
|
401
|
+
candidate
|
|
402
|
+
for candidate in scopes
|
|
403
|
+
if candidate is not scope
|
|
404
|
+
and scope.start_line <= candidate.start_line
|
|
405
|
+
and candidate.end_line <= scope.end_line
|
|
406
|
+
and (
|
|
407
|
+
scope.start_line < candidate.start_line
|
|
408
|
+
or candidate.end_line < scope.end_line
|
|
409
|
+
)
|
|
410
|
+
]
|
|
411
|
+
scope_changes = [
|
|
412
|
+
line_number
|
|
413
|
+
for line_number in changed_lines
|
|
414
|
+
if scope.start_line <= line_number <= scope.end_line
|
|
415
|
+
]
|
|
416
|
+
changes_covered_by_descendants = (
|
|
417
|
+
bool(scope_changes)
|
|
418
|
+
and bool(descendants)
|
|
419
|
+
and all(
|
|
420
|
+
any(
|
|
421
|
+
child.start_line <= line_number <= child.end_line
|
|
422
|
+
for child in descendants
|
|
423
|
+
)
|
|
424
|
+
for line_number in scope_changes
|
|
425
|
+
)
|
|
426
|
+
)
|
|
506
427
|
|
|
507
|
-
|
|
508
|
-
|
|
428
|
+
if not changes_covered_by_descendants:
|
|
429
|
+
retained.append(scope)
|
|
509
430
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
431
|
+
return retained
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _format_scope_block(scope: ScopeInfo) -> str:
|
|
435
|
+
"""Render a :class:`ScopeInfo` with absolute 1-based line numbers."""
|
|
436
|
+
lines: list[str] = []
|
|
437
|
+
for offset, src_line in enumerate(scope.source_lines):
|
|
438
|
+
abs_line = scope.start_line + offset
|
|
439
|
+
lines.append(f"{abs_line:4d}: {src_line}")
|
|
440
|
+
return "\n".join(lines)
|
|
514
441
|
|
|
515
442
|
|
|
516
443
|
# ---------------------------------------------------------------------------
|
|
@@ -533,9 +460,10 @@ class ContextExtractor:
|
|
|
533
460
|
The returned *xml_block* is ready to be embedded in the LLM user message.
|
|
534
461
|
|
|
535
462
|
Attributes:
|
|
536
|
-
max_scope_blocks:
|
|
463
|
+
max_scope_blocks: Maximum number of AST scopes before falling back to
|
|
464
|
+
adaptive_diff.
|
|
537
465
|
max_scope_lines: Maximum lines per scope block before falling back to
|
|
538
|
-
|
|
466
|
+
adaptive_diff (avoids sending entire large classes to the LLM).
|
|
539
467
|
adaptive_context_lines: Lines of context for the fallback strategy.
|
|
540
468
|
"""
|
|
541
469
|
|
|
@@ -565,9 +493,8 @@ class ContextExtractor:
|
|
|
565
493
|
|
|
566
494
|
1. If Tree-sitter is unavailable → ``adaptive_diff``
|
|
567
495
|
2. If the language is not supported → ``adaptive_diff``
|
|
568
|
-
3. If the
|
|
569
|
-
4.
|
|
570
|
-
5. Otherwise → ``file_skeleton``
|
|
496
|
+
3. If the extracted scopes fit the configured limits → ``enclosing_scopes``
|
|
497
|
+
4. Otherwise → ``adaptive_diff``
|
|
571
498
|
|
|
572
499
|
Args:
|
|
573
500
|
file_path: Path (or name) of the file being reviewed.
|
|
@@ -576,9 +503,8 @@ class ContextExtractor:
|
|
|
576
503
|
|
|
577
504
|
Returns:
|
|
578
505
|
A tuple ``(strategy, xml_block)`` where *strategy* is one of
|
|
579
|
-
``"enclosing_scopes"
|
|
580
|
-
|
|
581
|
-
ready for the LLM payload.
|
|
506
|
+
``"enclosing_scopes"`` or ``"adaptive_diff"``, and *xml_block*
|
|
507
|
+
is the XML-tagged string ready for the LLM payload.
|
|
582
508
|
"""
|
|
583
509
|
if not changed_lines:
|
|
584
510
|
return "adaptive_diff", ""
|
|
@@ -592,24 +518,18 @@ class ContextExtractor:
|
|
|
592
518
|
return self._adaptive(file_path, new_content, changed_lines)
|
|
593
519
|
|
|
594
520
|
# ------------------------------------------------------------------
|
|
595
|
-
#
|
|
521
|
+
# Generic AST path
|
|
596
522
|
# ------------------------------------------------------------------
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
scopes = _extract_sql_statement(new_content, changed_lines, lang)
|
|
600
|
-
if scopes:
|
|
601
|
-
return self._format_enclosing_scopes(
|
|
602
|
-
file_path, scopes, strategy="sql_statement"
|
|
603
|
-
)
|
|
604
|
-
except Exception:
|
|
605
|
-
pass
|
|
523
|
+
ast_changed_lines = _non_whitespace_changed_lines(new_content, changed_lines)
|
|
524
|
+
if not ast_changed_lines:
|
|
606
525
|
return self._adaptive(file_path, new_content, changed_lines)
|
|
607
526
|
|
|
608
|
-
# ------------------------------------------------------------------
|
|
609
|
-
# Generic AST path
|
|
610
|
-
# ------------------------------------------------------------------
|
|
611
527
|
try:
|
|
612
|
-
scopes = _extract_enclosing_scopes_ts(
|
|
528
|
+
scopes = _extract_enclosing_scopes_ts(
|
|
529
|
+
new_content,
|
|
530
|
+
ast_changed_lines,
|
|
531
|
+
lang,
|
|
532
|
+
)
|
|
613
533
|
except Exception:
|
|
614
534
|
return self._adaptive(file_path, new_content, changed_lines)
|
|
615
535
|
|
|
@@ -617,29 +537,17 @@ class ContextExtractor:
|
|
|
617
537
|
# No enclosing scope found (top-level code) → adaptive diff
|
|
618
538
|
return self._adaptive(file_path, new_content, changed_lines)
|
|
619
539
|
|
|
620
|
-
|
|
621
|
-
# Check if any single scope is too large — if so, fall back to
|
|
622
|
-
# file_skeleton to avoid sending a 900-line class body to the LLM.
|
|
623
|
-
oversized = any(
|
|
624
|
-
(s.end_line - s.start_line) > self.max_scope_lines
|
|
625
|
-
for s in scopes
|
|
626
|
-
)
|
|
627
|
-
if not oversized:
|
|
628
|
-
# Scenario A: Enclosing Scopes
|
|
629
|
-
return self._format_enclosing_scopes(file_path, scopes)
|
|
540
|
+
scopes = _remove_redundant_ancestor_scopes(scopes, ast_changed_lines)
|
|
630
541
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
542
|
+
too_many_scopes = len(scopes) > self.max_scope_blocks
|
|
543
|
+
oversized_scope = any(
|
|
544
|
+
(s.end_line - s.start_line + 1) > self.max_scope_lines
|
|
545
|
+
for s in scopes
|
|
546
|
+
)
|
|
547
|
+
if too_many_scopes or oversized_scope:
|
|
635
548
|
return self._adaptive(file_path, new_content, changed_lines)
|
|
636
549
|
|
|
637
|
-
|
|
638
|
-
f'<file_skeleton file="{file_path}">\n'
|
|
639
|
-
f"{skeleton}\n"
|
|
640
|
-
f"</file_skeleton>"
|
|
641
|
-
)
|
|
642
|
-
return "file_skeleton", xml
|
|
550
|
+
return self._format_enclosing_scopes(file_path, scopes)
|
|
643
551
|
|
|
644
552
|
# ------------------------------------------------------------------
|
|
645
553
|
# Internal formatters
|
|
@@ -666,7 +574,6 @@ class ContextExtractor:
|
|
|
666
574
|
self,
|
|
667
575
|
file_path: str,
|
|
668
576
|
scopes: list[ScopeInfo],
|
|
669
|
-
strategy: str = "enclosing_scopes",
|
|
670
577
|
) -> tuple[str, str]:
|
|
671
578
|
"""Render a list of :class:`ScopeInfo` into an XML block."""
|
|
672
579
|
parts: list[str] = []
|
|
@@ -679,13 +586,12 @@ class ContextExtractor:
|
|
|
679
586
|
parts.append(_format_scope_block(scope))
|
|
680
587
|
|
|
681
588
|
body = "\n".join(parts)
|
|
682
|
-
tag = "enclosing_scopes" if strategy == "enclosing_scopes" else strategy
|
|
683
589
|
xml = (
|
|
684
|
-
f'<
|
|
590
|
+
f'<enclosing_scopes file="{file_path}">\n'
|
|
685
591
|
f"{body}\n"
|
|
686
|
-
f"</
|
|
592
|
+
f"</enclosing_scopes>"
|
|
687
593
|
)
|
|
688
|
-
return
|
|
594
|
+
return "enclosing_scopes", xml
|
|
689
595
|
|
|
690
596
|
# ------------------------------------------------------------------
|
|
691
597
|
# Utility: extract changed lines from a diff hunk for one file
|
|
@@ -100,7 +100,7 @@ class GitUtils:
|
|
|
100
100
|
- ``@@ ... @@``
|
|
101
101
|
- ``+ <content>`` (added lines)
|
|
102
102
|
- `` <content>`` (context lines — unchanged surrounding code)
|
|
103
|
-
- Lines inside XML context blocks (``<enclosing_scopes
|
|
103
|
+
- Lines inside XML context blocks (``<enclosing_scopes>`` or ``<adaptive_diff>``)
|
|
104
104
|
|
|
105
105
|
Lines removed:
|
|
106
106
|
- ``- <content>`` (deleted lines)
|
|
@@ -114,8 +114,8 @@ class GitUtils:
|
|
|
114
114
|
"""
|
|
115
115
|
result = []
|
|
116
116
|
in_xml_block = False
|
|
117
|
-
_xml_start_tags = ("<enclosing_scopes", "<
|
|
118
|
-
_xml_end_tags = ("</enclosing_scopes>", "</
|
|
117
|
+
_xml_start_tags = ("<enclosing_scopes", "<adaptive_diff")
|
|
118
|
+
_xml_end_tags = ("</enclosing_scopes>", "</adaptive_diff>")
|
|
119
119
|
|
|
120
120
|
for line in diff.split("\n"):
|
|
121
121
|
if any(line.startswith(tag) for tag in _xml_start_tags):
|
|
@@ -204,7 +204,7 @@ class GitUtils:
|
|
|
204
204
|
|
|
205
205
|
def truncate_diff_per_file(self, diff: str, max_lines: int = 2000) -> tuple[str, bool]:
|
|
206
206
|
"""
|
|
207
|
-
Truncates
|
|
207
|
+
Truncates diff lines per file while preserving complete XML context blocks.
|
|
208
208
|
Falls back to global truncation if no file sections are present.
|
|
209
209
|
|
|
210
210
|
Returns:
|
|
@@ -227,18 +227,30 @@ class GitUtils:
|
|
|
227
227
|
truncated_any = False
|
|
228
228
|
output_sections: list[str] = []
|
|
229
229
|
for section in sections:
|
|
230
|
-
|
|
230
|
+
xml_start = next(
|
|
231
|
+
(
|
|
232
|
+
index
|
|
233
|
+
for index, line in enumerate(section)
|
|
234
|
+
if line.startswith(("<enclosing_scopes", "<adaptive_diff"))
|
|
235
|
+
),
|
|
236
|
+
len(section),
|
|
237
|
+
)
|
|
238
|
+
diff_lines = section[:xml_start]
|
|
239
|
+
context_lines = section[xml_start:]
|
|
240
|
+
|
|
241
|
+
if len(diff_lines) <= max_lines:
|
|
231
242
|
output_sections.append("\n".join(section))
|
|
232
243
|
continue
|
|
233
244
|
|
|
234
245
|
truncated_any = True
|
|
235
|
-
omitted = len(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
f"
|
|
239
|
-
f"Original section: {len(
|
|
246
|
+
omitted = len(diff_lines) - max_lines
|
|
247
|
+
truncated_section = diff_lines[:max_lines]
|
|
248
|
+
truncated_section.append(
|
|
249
|
+
f"... [TRUNCATED IN THIS FILE: {omitted} diff lines omitted. "
|
|
250
|
+
f"Original diff section: {len(diff_lines)} lines] ..."
|
|
240
251
|
)
|
|
241
|
-
|
|
252
|
+
truncated_section.extend(context_lines)
|
|
253
|
+
output_sections.append("\n".join(truncated_section))
|
|
242
254
|
|
|
243
255
|
return "\n".join(output_sections), truncated_any
|
|
244
256
|
|