code-review-ai-cli 2.2.4__py3-none-any.whl → 2.2.6__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-review-ai-cli
3
- Version: 2.2.4
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"
@@ -0,0 +1,17 @@
1
+ src/__init__.py,sha256=BNfzb1HoMlgLc9MNEzc8Ne7USol7NHRUcMn7ISQ41sY,259
2
+ src/ai_review.py,sha256=Wfeo72uTsjuoKMW5ToqFi6edfVIWTMgqPIbv9_W8Z-Y,34482
3
+ src/config.py,sha256=GFqGfjnhjY0O6EiDTsFFCM3qvSCVZMPd3IbmWd4SOWk,16419
4
+ src/context_extractor.py,sha256=SyuTtau1eIVmjOkqmV_8ijawqDiGK_M7G9May99oBAo,20928
5
+ src/formatter.py,sha256=68kz1U6tMndLcmtJRImN4jAIpxXtog3cKtiJ-zc3S0M,19027
6
+ src/git_utils.py,sha256=sJPV-NpSx0up-KDvFK1Pn73mvGdSNiCqTjEj6I8kzpM,10218
7
+ src/llm_client.py,sha256=dVVkJ1glf1K-I_oWZ-qxMABaBLQ4gq4blOiM5lvFEp0,54393
8
+ src/prompt_utils.py,sha256=FnWgHQ7UW8dEKW-4R7ngpL6DNqbW7xFEKi5JsZ7348E,2045
9
+ src/tfs_client.py,sha256=Y7MIu5dutZ2J223yLJJPai3pbux5dnB-MoK_SQe-08I,37459
10
+ src/token_estimator.py,sha256=4_4TXhefaNi7KHomwhhWbt7GB6KfubCQVBEbD3YoK5Q,3192
11
+ src/prompts/config.yaml.template,sha256=p5ZolxDPmht_RXaS1iLcPqpK1zle20i5eAnWeKuKT2U,4878
12
+ src/prompts/review_prompt.md.template,sha256=OWlSqCIkJA6Q8fUuNNrI61B7YyfrRhDLy7nUfw46rJQ,2903
13
+ code_review_ai_cli-2.2.6.dist-info/METADATA,sha256=VQr3a3CZ9eIhkvjJ8Bt_Y3k3SNJRAoNlsIwvCkRiNYY,4966
14
+ code_review_ai_cli-2.2.6.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
15
+ code_review_ai_cli-2.2.6.dist-info/entry_points.txt,sha256=hzKqoXIQVpiCe4H-XV0DZcKwJY6Uj3M-WvBMEQtwr3o,49
16
+ code_review_ai_cli-2.2.6.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
17
+ code_review_ai_cli-2.2.6.dist-info/RECORD,,
src/config.py CHANGED
@@ -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 = 3 # Max changed blocks for Enclosing Scopes strategy
96
- max_scope_lines: int = 250 # Max lines in a single scope block before fallback to file_skeleton
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)
src/context_extractor.py CHANGED
@@ -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
- - SQL files → full SQL statement containing the changed line(s)
11
- - ≤ ``MAX_SCOPE_BLOCKS`` changed logical blocks → Enclosing Scopes (Scenario A)
12
- - > ``MAX_SCOPE_BLOCKS`` changed logical blocks → File Skeleton (Scenario B)
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 an *optional* dependency. If ``tree-sitter-languages`` is not
23
- installed the module automatically falls back to :func:`_build_adaptive_diff`.
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 that triggers Scenario A
72
- #: (Enclosing Scopes). When more blocks are detected Scenario B (File
73
- #: Skeleton) is used instead.
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 File Skeleton to avoid
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 _format_scope_block(scope: ScopeInfo) -> str:
374
- """Render a :class:`ScopeInfo` with absolute 1-based line numbers."""
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
- # File Skeleton helpers
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
- total = len(all_lines)
453
-
454
- if not all_lines:
455
- return ""
456
-
457
- # Build set of "always include" lines (context around changes)
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
- """Find the SQL statement(s) covering each changed line via Tree-sitter.
391
+ """Drop ancestor scopes whose changed lines are covered by child scopes.
501
392
 
502
- Args:
503
- content: Full SQL file content.
504
- changed_lines: 1-based line numbers that were changed.
505
- lang: Always ``"sql"`` for this path.
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
- Returns:
508
- List of :class:`ScopeInfo` for each affected SQL statement.
428
+ if not changes_covered_by_descendants:
429
+ retained.append(scope)
509
430
 
510
- Raises:
511
- Exception: On Tree-sitter parse failure (caller activates fallback).
512
- """
513
- return _extract_enclosing_scopes_ts(content, changed_lines, lang)
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: Threshold for Scenario A vs Scenario B selection.
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
- file_skeleton (avoids sending entire large classes to the LLM).
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 language is SQL → ``sql_statement`` (falls back on failure)
569
- 4. If ≤ ``max_scope_blocks`` distinct scopes found → ``enclosing_scopes``
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"``, ``"file_skeleton"``, ``"sql_statement"``,
580
- or ``"adaptive_diff"``, and *xml_block* is the XML-tagged string
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
- # SQL path
521
+ # Generic AST path
596
522
  # ------------------------------------------------------------------
597
- if lang == "sql":
598
- try:
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(new_content, changed_lines, lang)
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
- if len(scopes) <= self.max_scope_blocks:
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
- # Scenario B: File Skeleton
632
- try:
633
- skeleton = _build_file_skeleton(new_content, changed_lines, lang)
634
- except Exception:
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
- xml = (
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'<{tag} file="{file_path}">\n'
590
+ f'<enclosing_scopes file="{file_path}">\n'
685
591
  f"{body}\n"
686
- f"</{tag}>"
592
+ f"</enclosing_scopes>"
687
593
  )
688
- return strategy, xml
594
+ return "enclosing_scopes", xml
689
595
 
690
596
  # ------------------------------------------------------------------
691
597
  # Utility: extract changed lines from a diff hunk for one file
src/git_utils.py CHANGED
@@ -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>``, ``<file_skeleton>``, etc.)
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", "<file_skeleton", "<adaptive_diff", "<sql_statement")
118
- _xml_end_tags = ("</enclosing_scopes>", "</file_skeleton>", "</adaptive_diff>", "</sql_statement>")
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 the diff per file if it exceeds the maximum lines per section.
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
- if len(section) <= max_lines:
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(section) - max_lines
236
- part = "\n".join(section[:max_lines])
237
- part += (
238
- f"\n... [TRUNCATED IN THIS FILE: {omitted} lines omitted. "
239
- f"Original section: {len(section)} lines] ..."
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
- output_sections.append(part)
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
 
src/llm_client.py CHANGED
@@ -28,6 +28,26 @@ class LLMError(Exception):
28
28
  """Exception for LLM communication errors."""
29
29
  pass
30
30
 
31
+
32
+ def _normalize_prompt_spacing(text: str) -> str:
33
+ """Collapse repeated blank lines while preserving one between sections."""
34
+ normalized_lines: list[str] = []
35
+ blank_pending = False
36
+
37
+ for line in text.splitlines():
38
+ if not line.strip():
39
+ if normalized_lines:
40
+ blank_pending = True
41
+ continue
42
+
43
+ if blank_pending:
44
+ normalized_lines.append("")
45
+ blank_pending = False
46
+ normalized_lines.append(line)
47
+
48
+ return "\n".join(normalized_lines)
49
+
50
+
31
51
  # ---------------------------------------------------------------------------
32
52
  # System Prompts
33
53
  # ---------------------------------------------------------------------------
@@ -43,32 +63,17 @@ class LLMError(Exception):
43
63
  # fallback to "detailed" when the key is not recognised.
44
64
  SYSTEM_PROMPTS = {
45
65
  "detailed": (
46
- "You are an experienced Senior Code Reviewer. Analyze the provided code and return only inline comments.\n\n"
47
- "Output format — for each issue found, output exactly:\n"
48
- "- Line <line_number>: <issue description>\n\n"
49
- "Rules:\n"
50
- "- Report ONLY specific issues tied to a concrete line or block of changed code.\n"
51
- "- Do NOT produce summary sections (e.g. 'Potential Bugs', 'Security Overview').\n"
52
- "- Do NOT produce introductory or closing paragraphs.\n"
53
- "- If no issues are found, output only: No issues found.\n"
54
- "- Be specific, actionable, and concise."
66
+ "You are an AI Agente code reviewer. Identify concrete bugs, security risks, "
67
+ "performance problems, and maintainability issues introduced by the change. "
68
+ "Report only actionable findings tied to changed code; be specific and concise."
55
69
  ),
56
70
  "security": (
57
- "You are an application security. Analyze the "
58
- "provided code diff with EXCLUSIVE focus on security.\n\n"
59
- "Look for:\n"
60
- "- SQL Injection\n"
61
- "- Cross-Site Scripting (XSS)\n"
62
- "- Cross-Site Request Forgery (CSRF)\n"
63
- "- Hardcoded credentials or exposed secrets\n"
64
- "- Authentication/authorization vulnerabilities\n"
65
- "- Insecure deserialization\n"
66
- "- Path traversal\n"
67
- "- Command injection\n"
68
- "- Dependencies with known vulnerabilities\n"
69
- "- Logging of sensitive information\n"
70
- "- Insecure configurations\n\n"
71
- "Provide fix recommendations for each issue."
71
+ "You are an AI Agente application security reviewer. Identify actionable security vulnerabilities "
72
+ "introduced by the change, including injection, XSS, CSRF, exposed secrets, broken "
73
+ "authentication or authorization, insecure deserialization, path traversal, vulnerable "
74
+ "dependencies, sensitive-data logging, and insecure configuration. Consider other "
75
+ "security risks when relevant. Report only findings tied to changed code and include "
76
+ "a concise fix recommendation for each."
72
77
  ),
73
78
  }
74
79
 
@@ -91,15 +96,20 @@ PR_COMMENT_PROMPT = (
91
96
  'something like "No issues found."\n'
92
97
  '- "comments": an array of objects, one per issue found, with:\n'
93
98
  ' - "file": file path (e.g., "src/auth.py")\n'
94
- ' - "line": line number in diff (integer, or 0 if general)\n'
99
+ ' - "line": positive absolute line number in the new file (integer)\n'
95
100
  ' - "type": issue type ("bug", "security", "performance", "style", "suggestion", "praise")\n'
96
101
  ' - "comment": direct description of the issue, with no greetings and no emojis\n'
97
- ' - "suggestion": fix suggestion (optional, empty string if not applicable)\n'
98
- ' - "reference": source or reference for the issue (e.g., "OWASP Top 10", "PEP 8", documentation '
99
- 'URL, standard or principle). Important: ALWAYS include a relevant reference.\n\n'
100
- "In 'comment', use a short and objective tone. "
101
- "Do not include intros like 'Hello' or 'As a senior reviewer'.\n"
102
- "In 'reference', include a trusted source, standard or link to relevant documentation.\n\n"
102
+ ' - "suggestion": concise, concrete fix recommendation (string)\n'
103
+ ' - "reference": relevant trusted source, standard, or documentation URL; use an empty string '
104
+ 'when no reliable reference applies\n\n'
105
+ "The summary is a brief narrative of the overall review, not a heading or a duplicate list "
106
+ "of comments. Use an empty comments array when no issues are found. Each comment must refer "
107
+ "to a specific actionable issue in changed code, and its line must be a positive absolute "
108
+ "line number from the new file. Every comment must include a concise, concrete fix "
109
+ "recommendation in 'suggestion'; do not leave it empty for an actionable issue. Use a short, "
110
+ "objective tone with no greetings or emojis. "
111
+ "Include a relevant trusted source in 'reference' when one applies; otherwise use an empty "
112
+ "string. Never invent a citation.\n\n"
103
113
  "Respond ONLY with a valid JSON object. Example:\n"
104
114
  '{\n'
105
115
  ' "summary": "Short narrative summary of the review...",\n'
@@ -120,14 +130,12 @@ PR_COMMENT_PROMPT = (
120
130
  def get_system_prompt(verbosity: str) -> str:
121
131
  """Returns the system prompt for the given verbosity level.
122
132
 
123
- Selects a prompt from ``SYSTEM_PROMPTS`` keyed by *verbosity*. If the
133
+ Selects review-focus guidance from ``SYSTEM_PROMPTS`` keyed by *verbosity*. If the
124
134
  requested verbosity is not found, the function falls back to
125
135
  ``"detailed"``.
126
136
 
127
- The ``"detailed"`` prompt instructs the LLM to return **only** inline
128
- comments in the format ``- Line <n>: <description>``,
129
- prohibiting summary sections and introductory/closing paragraphs.
130
- When no issues are found the expected output is ``No issues found.``
137
+ The selected prompt controls review focus only. The PR JSON output contract
138
+ is defined separately by ``PR_COMMENT_PROMPT``.
131
139
 
132
140
  Args:
133
141
  verbosity: Review depth key — one of ``"detailed"``
@@ -145,21 +153,17 @@ def get_scope_guidance(review_scope: str, structured: bool = False) -> str:
145
153
 
146
154
  For ``diff_only`` scope, the instructions inform the LLM that:
147
155
 
148
- * Added lines are prefixed with ``+`` and context lines (unchanged code)
149
- have no prefix. Deleted lines are not included.
150
- * An XML context block is provided for each changed file, containing
151
- either:
156
+ * Added lines are prefixed with ``+``. Each hunk includes up to five
157
+ unchanged context lines before and after the changes, prefixed with a
158
+ single space. Deleted lines are not included.
159
+ * When source content is available, a changed file may include an XML
160
+ context block containing either:
152
161
 
153
162
  - ``<enclosing_scopes file="...">`` — the complete function(s) or
154
- method(s) that enclose the changed lines (Scenario A, ≤3 blocks).
155
- - ``<file_skeleton file="...">`` — a structural skeleton of the file
156
- with unchanged function bodies replaced by ``...`` (Scenario B,
157
- >3 blocks).
158
- - ``<sql_statement file="...">`` — the full SQL statement containing
159
- the changed lines.
163
+ methods/statements that enclose the changed lines.
160
164
  - ``<adaptive_diff file="...">`` — N lines of context around the
161
- changed lines when Tree-sitter is unavailable or the language is
162
- not supported.
165
+ changed lines when AST extraction is unavailable or exceeds its
166
+ configured limits.
163
167
 
164
168
  * Line numbers inside the XML blocks are **1-based absolute** line
165
169
  numbers from the new version of the file and should be used as the
@@ -193,13 +197,13 @@ def get_scope_guidance(review_scope: str, structured: bool = False) -> str:
193
197
  if structured:
194
198
  return (
195
199
  "Review scope: diff_only. "
196
- "The <diff> block contains added lines (marked +) and surrounding context lines "
197
- "(unchanged code, no prefix). Deleted lines were removed. "
198
- "An XML context block is provided for each changed file: "
199
- "<enclosing_scopes> contains the complete function(s)/method(s) enclosing the changes; "
200
- "<file_skeleton> contains a structural skeleton for files with many spread changes; "
201
- "<sql_statement> contains the full SQL statement for SQL files; "
202
- "<adaptive_diff> contains surrounding context lines when AST parsing is unavailable. "
200
+ "The <diff> block contains added lines (prefixed with +) and up to five unchanged "
201
+ "context lines before and after each hunk (prefixed with a single space). "
202
+ "Deleted lines were removed. When source content is available, a changed file may "
203
+ "include an XML context block: "
204
+ "<enclosing_scopes> contains the complete AST scope(s) enclosing the changes; "
205
+ "<adaptive_diff> contains surrounding context lines when AST extraction is unavailable "
206
+ "or exceeds its configured limits. "
203
207
  "Line numbers inside XML context blocks are 1-based absolute line numbers from the new file. "
204
208
  "Use those line numbers as the authoritative reference when reporting issue positions. "
205
209
  "Focus your review EXCLUSIVELY on the changed lines (marked + in <diff>). "
@@ -210,10 +214,11 @@ def get_scope_guidance(review_scope: str, structured: bool = False) -> str:
210
214
 
211
215
  return (
212
216
  "Review scope: diff_only. "
213
- "The <diff> block contains added lines (marked +) and surrounding context lines "
214
- "(unchanged code, no prefix). Deleted lines were removed. "
215
- "An XML context block is provided for each changed file "
216
- "(<enclosing_scopes>, <file_skeleton>, <sql_statement>, or <adaptive_diff>) "
217
+ "The <diff> block contains added lines (prefixed with +) and up to five unchanged "
218
+ "context lines before and after each hunk (prefixed with a single space). "
219
+ "Deleted lines were removed. When source content is available, a changed file may "
220
+ "include an XML context block "
221
+ "(<enclosing_scopes> or <adaptive_diff>) "
217
222
  "as read-only structural background. "
218
223
  "Line numbers inside XML context blocks are 1-based absolute line numbers from the new file. "
219
224
  "Use those line numbers as the authoritative reference when reporting issue positions. "
@@ -263,7 +268,7 @@ def build_user_message(diff: str, files_summary: list[dict], context: str = "")
263
268
  parts.append("<context>\n" + "\n".join(context_inner) + "\n</context>")
264
269
 
265
270
  # --- Split diff lines from XML context blocks ---
266
- # XML context blocks (enclosing_scopes, file_skeleton, etc.) are already
271
+ # XML context blocks (enclosing_scopes or adaptive_diff) are already
267
272
  # embedded inside the diff string by TFSClient._build_unified_diff_part.
268
273
  # We extract them here and render them separately so the LLM sees:
269
274
  # <diff> ... pure unified diff ...</diff>
@@ -275,14 +280,10 @@ def build_user_message(diff: str, files_summary: list[dict], context: str = "")
275
280
  _in_xml = False
276
281
  _xml_tags = (
277
282
  "<enclosing_scopes ",
278
- "<file_skeleton ",
279
- "<sql_statement ",
280
283
  "<adaptive_diff ",
281
284
  )
282
285
  _xml_close = (
283
286
  "</enclosing_scopes>",
284
- "</file_skeleton>",
285
- "</sql_statement>",
286
287
  "</adaptive_diff>",
287
288
  )
288
289
 
@@ -329,11 +330,12 @@ class LLMClient:
329
330
 
330
331
  def _dump_prompt_debug(self, system_prompt: str, user_message: str) -> None:
331
332
  """
332
- Appends the full prompt/context sent to the LLM to a debug log file.
333
+ Writes the normalized prompt content sent to the LLM.
333
334
 
334
335
  Only active when ``config.debug_dump`` is enabled. The log may
335
336
  contain source code and PR content, so it must never be committed
336
- to version control.
337
+ to version control. Decorative metadata is omitted, and repeated blank
338
+ lines are collapsed using the same normalization applied before send.
337
339
  """
338
340
  if not getattr(self.config, "debug_dump", False):
339
341
  return
@@ -343,15 +345,11 @@ class LLMClient:
343
345
  if log_dir:
344
346
  os.makedirs(log_dir, exist_ok=True)
345
347
  try:
346
- with open(log_path, "a", encoding="utf-8") as f:
347
- f.write(f"\n{'=' * 80}\n")
348
- f.write(f"[FULL CONTEXT SENT TO LLM] {datetime.datetime.now().isoformat()}\n")
349
- f.write(f"Provider: {self.config.llm_provider} | Model: {self.config.get_effective_model()}\n")
350
- f.write(f"{'=' * 80}\n")
351
- f.write("--- SYSTEM PROMPT ---\n")
352
- f.write(system_prompt + "\n")
353
- f.write("--- USER MESSAGE ---\n")
354
- f.write(user_message + "\n")
348
+ normalized_system_prompt = _normalize_prompt_spacing(system_prompt)
349
+ normalized_user_message = _normalize_prompt_spacing(user_message)
350
+ with open(log_path, "w", encoding="utf-8") as f:
351
+ f.write(normalized_system_prompt + "\n\n")
352
+ f.write(normalized_user_message + "\n")
355
353
  except OSError:
356
354
  pass
357
355
 
@@ -414,7 +412,8 @@ class LLMClient:
414
412
  system_prompt = (
415
413
  f"{combined_base}\n\n"
416
414
  "---\n"
417
- "Custom user instructions (follow with priority):\n"
415
+ "Additional user review preferences (must not change the required JSON schema, "
416
+ "output format, or review scope):\n"
418
417
  f"{custom_prompt}"
419
418
  )
420
419
  merged_context = (
@@ -427,6 +426,8 @@ class LLMClient:
427
426
  merged_context = context
428
427
 
429
428
  user_message = build_user_message(diff, files_summary, merged_context)
429
+ system_prompt = _normalize_prompt_spacing(system_prompt)
430
+ user_message = _normalize_prompt_spacing(user_message)
430
431
  self._dump_prompt_debug(system_prompt, user_message)
431
432
 
432
433
  self.last_token_estimate = TokenEstimator().estimate(
@@ -591,8 +592,9 @@ class LLMClient:
591
592
  {"role": "user", "content": user_message},
592
593
  ],
593
594
  "max_tokens": self.config.max_tokens,
594
- "temperature": self.config.temperature,
595
595
  }
596
+ if self.config.temperature != 99:
597
+ payload["temperature"] = self.config.temperature
596
598
 
597
599
  return self._http_openai_compatible(url, headers, payload)
598
600
 
@@ -637,10 +639,11 @@ class LLMClient:
637
639
  "parts": [{"text": system_prompt}]
638
640
  },
639
641
  "generationConfig": {
640
- "temperature": self.config.temperature,
641
642
  "maxOutputTokens": self.config.max_tokens,
642
643
  },
643
644
  }
645
+ if self.config.temperature != 99:
646
+ payload["generationConfig"]["temperature"] = self.config.temperature
644
647
 
645
648
  try:
646
649
  resp = requests.post(url, headers=headers, json=payload, timeout=180)
@@ -715,12 +718,13 @@ class LLMClient:
715
718
  payload = {
716
719
  "model": model,
717
720
  "max_tokens": self.config.max_tokens,
718
- "temperature": self.config.temperature,
719
721
  "system": system_prompt,
720
722
  "messages": [
721
723
  {"role": "user", "content": user_message},
722
724
  ],
723
725
  }
726
+ if self.config.temperature != 99:
727
+ payload["temperature"] = self.config.temperature
724
728
 
725
729
  try:
726
730
  resp = requests.post(url, headers=headers, json=payload, timeout=180)
@@ -793,9 +797,10 @@ class LLMClient:
793
797
  {"role": "system", "content": system_prompt},
794
798
  {"role": "user", "content": user_message},
795
799
  ],
796
- "temperature": self.config.temperature,
797
800
  "stream": False,
798
801
  }
802
+ if self.config.temperature != 99:
803
+ payload["temperature"] = self.config.temperature
799
804
 
800
805
  # Ollama does not require an API key, but we add max_tokens if configured
801
806
  if self.config.max_tokens:
@@ -912,8 +917,9 @@ class LLMClient:
912
917
  {"role": "system", "content": system_prompt},
913
918
  {"role": "user", "content": user_message},
914
919
  ],
915
- "temperature": self.config.temperature,
916
920
  }
921
+ if self.config.temperature != 99:
922
+ payload["temperature"] = self.config.temperature
917
923
 
918
924
  # Add max_tokens if configured (some Copilot models
919
925
  # may not support this parameter)
@@ -1028,7 +1034,7 @@ class LLMClient:
1028
1034
  system_prompt: str,
1029
1035
  user_message: str,
1030
1036
  ) -> str:
1031
- """Calls Bedrock InvokeModel using an HTTP Bearer token (long-term API key).
1037
+ """Calls Bedrock Converse using an HTTP Bearer token (long-term API key).
1032
1038
 
1033
1039
  Args:
1034
1040
  region: AWS region, e.g. ``us-east-1``.
@@ -1051,14 +1057,19 @@ class LLMClient:
1051
1057
 
1052
1058
  # The model ARN contains ':' and '/' that must be URL-encoded in the path
1053
1059
  model_encoded = urllib.parse.quote(self.config.model, safe="")
1054
- url = f"https://bedrock-runtime.{region}.amazonaws.com/model/{model_encoded}/invoke"
1060
+ url = f"https://bedrock-runtime.{region}.amazonaws.com/model/{model_encoded}/converse"
1061
+
1062
+ inference_config = {"maxTokens": self.config.max_tokens}
1063
+ if self.config.temperature != 99:
1064
+ inference_config["temperature"] = self.config.temperature
1055
1065
 
1056
1066
  payload = json.dumps({
1057
- "anthropic_version": "bedrock-2023-05-31",
1058
- "max_tokens": self.config.max_tokens,
1059
- "temperature": self.config.temperature,
1060
- "system": system_prompt,
1061
- "messages": [{"role": "user", "content": user_message}],
1067
+ "system": [{"text": system_prompt}],
1068
+ "messages": [{
1069
+ "role": "user",
1070
+ "content": [{"text": user_message}],
1071
+ }],
1072
+ "inferenceConfig": inference_config,
1062
1073
  }, separators=(",", ":"))
1063
1074
 
1064
1075
  headers = {
@@ -1084,10 +1095,9 @@ class LLMClient:
1084
1095
  except ValueError as exc:
1085
1096
  raise LLMError(f"Invalid JSON from Bedrock: {resp.text[:500]}") from exc
1086
1097
 
1087
- content = data.get("content", [])
1098
+ content = data.get("output", {}).get("message", {}).get("content", [])
1088
1099
  text_parts = [
1089
- item["text"] for item in content
1090
- if item.get("type") == "text" and "text" in item
1100
+ item["text"] for item in content if "text" in item
1091
1101
  ]
1092
1102
  text = "\n".join(part for part in text_parts if part).strip()
1093
1103
  if not text:
@@ -1103,7 +1113,7 @@ class LLMClient:
1103
1113
  system_prompt: str,
1104
1114
  user_message: str,
1105
1115
  ) -> str:
1106
- """Calls Bedrock InvokeModel with manual AWS SigV4 HMAC-SHA256 signing.
1116
+ """Calls Bedrock Converse with manual AWS SigV4 HMAC-SHA256 signing.
1107
1117
 
1108
1118
  Equivalent to the C# BedrockLlmClient implementation.
1109
1119
 
@@ -1135,15 +1145,20 @@ class LLMClient:
1135
1145
 
1136
1146
  host = f"bedrock-runtime.{region}.amazonaws.com"
1137
1147
  model_encoded = urllib.parse.quote(self.config.model, safe="")
1138
- endpoint = f"https://{host}/model/{model_encoded}/invoke"
1148
+ endpoint = f"https://{host}/model/{model_encoded}/converse"
1139
1149
  service = "bedrock"
1140
1150
 
1151
+ inference_config = {"maxTokens": self.config.max_tokens}
1152
+ if self.config.temperature != 99:
1153
+ inference_config["temperature"] = self.config.temperature
1154
+
1141
1155
  payload = json.dumps({
1142
- "anthropic_version": "bedrock-2023-05-31",
1143
- "max_tokens": self.config.max_tokens,
1144
- "temperature": self.config.temperature,
1145
- "system": system_prompt,
1146
- "messages": [{"role": "user", "content": user_message}],
1156
+ "system": [{"text": system_prompt}],
1157
+ "messages": [{
1158
+ "role": "user",
1159
+ "content": [{"text": user_message}],
1160
+ }],
1161
+ "inferenceConfig": inference_config,
1147
1162
  }, separators=(",", ":"))
1148
1163
 
1149
1164
  now = datetime.datetime.now(datetime.timezone.utc)
@@ -1157,7 +1172,7 @@ class LLMClient:
1157
1172
  # Splitting by '/' and encoding each segment mirrors the C# SigV4 implementation.
1158
1173
  canonical_uri = "/".join(
1159
1174
  urllib.parse.quote(seg, safe="")
1160
- for seg in f"/model/{self.config.model}/invoke".split("/")
1175
+ for seg in f"/model/{self.config.model}/converse".split("/")
1161
1176
  )
1162
1177
 
1163
1178
  headers_to_sign = {
@@ -1227,10 +1242,9 @@ class LLMClient:
1227
1242
  except ValueError as exc:
1228
1243
  raise LLMError(f"Invalid JSON from Bedrock: {resp.text[:500]}") from exc
1229
1244
 
1230
- content = data.get("content", [])
1245
+ content = data.get("output", {}).get("message", {}).get("content", [])
1231
1246
  text_parts = [
1232
- item["text"] for item in content
1233
- if item.get("type") == "text" and "text" in item
1247
+ item["text"] for item in content if "text" in item
1234
1248
  ]
1235
1249
  text = "\n".join(part for part in text_parts if part).strip()
1236
1250
  if not text:
@@ -1277,6 +1291,10 @@ class LLMClient:
1277
1291
  session = boto3.Session(**session_kwargs)
1278
1292
  client = session.client("bedrock-runtime", region_name=region)
1279
1293
 
1294
+ inference_config = {"maxTokens": self.config.max_tokens}
1295
+ if self.config.temperature != 99:
1296
+ inference_config["temperature"] = self.config.temperature
1297
+
1280
1298
  response = client.converse(
1281
1299
  modelId=self.config.model,
1282
1300
  system=[{"text": system_prompt}],
@@ -1286,10 +1304,7 @@ class LLMClient:
1286
1304
  "content": [{"text": user_message}],
1287
1305
  }
1288
1306
  ],
1289
- inferenceConfig={
1290
- "temperature": self.config.temperature,
1291
- "maxTokens": self.config.max_tokens,
1292
- },
1307
+ inferenceConfig=inference_config,
1293
1308
  )
1294
1309
 
1295
1310
  content = (
@@ -26,7 +26,7 @@ llm:
26
26
  # Maximum tokens in response
27
27
  max_tokens: 4096
28
28
 
29
- # Temperature (0.0 = deterministic, 1.0 = creative)
29
+ # Temperature (0.0 = deterministic, 1.0 = creative, 99 omits it for all models that dont support this)
30
30
  temperature: 0.0
31
31
 
32
32
  # ---------------------------------------------------------------------------
@@ -91,9 +91,9 @@ review:
91
91
  max_diff_lines: 4500
92
92
 
93
93
  # Max changed logical blocks for Enclosing Scopes strategy (diff_only mode)
94
- max_scope_blocks: 10
94
+ max_scope_blocks: 8
95
95
 
96
- # Max lines in a single enclosing scope before falling back to file_skeleton
96
+ # Max lines in a single enclosing scope before falling back to Adaptive Diff
97
97
  # (prevents sending entire large classes to the LLM)
98
98
  max_scope_lines: 250
99
99
 
@@ -138,4 +138,4 @@ output:
138
138
 
139
139
  debug:
140
140
  dump: false
141
- dump_file: "logs/debug.log"
141
+ dump_file: "logs/debug.log"
@@ -4,82 +4,18 @@
4
4
  - Error handling: swallowed exceptions (empty catch), missing logging at critical points, generic error messages to end users.
5
5
  - Readability: methods too long or with too many responsibilities (SRP), poor naming, dead/commented-out code.
6
6
  - Duplication: repeated logic that should be extracted/reused.
7
- - Testability: hard-to-mock dependencies, business logic mixed with I/O.
8
7
  - Unused code: unused local variables, method parameters, private fields/methods, unreachable code, and unused imports/usings.
9
- - Unnecessary allocations in hot paths (loops, frequently-called requests).
10
-
11
- <!-- lang: cs,ts -->
12
- ## Performance
13
- - Use of `StringBuilder` vs string concatenation in loops.
14
- - Blocking synchronous operations where async should be used (I/O, network calls).
15
-
16
- <!-- lang: cs,ts,sql -->
17
- ## Concurrency / Thread-safety
18
- - Race conditions in shared code (static fields, singletons).
19
- - Correct use of locks/semaphores, avoid deadlocks.
20
- - Thread-safe collections where needed (`ConcurrentDictionary`, etc).
21
-
22
- <!-- lang: all -->
23
- ## Configuration & Secrets
24
- - Hardcoded values that should be in configuration (connection strings, URLs, timeouts).
25
- - Secrets/API keys accidentally committed.
26
-
27
- <!-- lang: cs,ts -->
28
- ## Dependencies & Compatibility
29
- - New dependencies added without clear justification - unnecessary or outdated packages.
30
- - Breaking changes when updating package versions.
8
+ - Mandatory language consistency: Newly added or modified identifiers, developer-facing comments, and documentation must use the same natural language already established by the surrounding code and project. Report mixed-language naming or documentation as a `style` finding on the identifier declaration or comment line. For example, a Portuguese method name in a codebase whose existing identifiers are English is a violation. Do not flag names imposed by external APIs, standard technical terms, or localized user-facing text.
31
9
 
32
10
  <!-- lang: cs -->
33
11
  ## C#
34
- - Naming Conventions & Casing: follow Microsoft's official C# naming guidelines (https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names).
35
- - XML docs: follow Microsoft's official documentation guidance (https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/).
12
+ - Mandatory naming conventions: Every newly added or modified C# identifier must follow Microsoft's official naming guidance. Report each violation as a `style` finding on the identifier's declaration line. Use PascalCase for types, namespaces, all methods and local functions, public or protected members, and constants; prefix interfaces with `I`; use camelCase for parameters and local variables; use `_camelCase` for private or internal instance fields and `s_camelCase` for private or internal static fields. Names must be meaningful and descriptive, and nonstandard abbreviations must be avoided. Reference: https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names
13
+ - Mandatory XML documentation: Every newly added or modified public or protected C# type and member must have valid XML documentation comments.
14
+ Report missing documentation as a `style` finding on the declaration line. Require `<summary>` and, when applicable, `<param>`, `<typeparam>` and
15
+ `<returns>` elements. Follow Microsoft's XML documentation guidance: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/
16
+ - Mandatory null safety: Report a `bug` whenever newly added or modified
17
+ code dereferences an externally supplied reference-type parameter without first validating it for null, unless non-nullability is explicitly
18
+ established by preceding code. Recommend `ArgumentNullException.ThrowIfNull(parameter)` and report the finding on the first unsafe dereference.
36
19
  - Async: avoid `async void` (except handlers), use `ConfigureAwait(false)` in libraries, don't block with `.Result`/`.Wait()`.
37
20
  - Nullable refs: correct `?` usage, avoid unjustified `!`.
38
- - LINQ: avoid redundant iterations, N+1 queries with EF Core.
39
- - Disposal: proper use of `IDisposable`/`using` for resources (streams, connections, etc).
40
21
  - Immutability: prefer `readonly`, records, immutable types.
41
-
42
- <!-- lang: cs,sql -->
43
- ## Logging & Observability
44
- - Appropriate log levels (Debug vs Info vs Warning vs Error).
45
- - Don't log sensitive data (PII, passwords, tokens).
46
-
47
- <!-- lang: sql -->
48
- ## Migrations / Database (given your SQL/EF stack)
49
- - Destructive changes to columns/tables with existing data (data loss risk).
50
- - Migrations that lock large tables in production.
51
-
52
- <!-- lang: sql -->
53
- ## SQL
54
- - Avoid `SELECT *`; specify columns explicitly.
55
- - Check for parameterized queries (avoid string concatenation → SQL injection).
56
- - Indexes: flag queries likely to cause table scans on large tables.
57
- - Transactions: ensure multi-step critical operations are wrapped in a transaction.
58
- - Naming conventions for tables/columns consistent with the rest of the schema.
59
-
60
- <!-- lang: html -->
61
- ## HTML
62
- - A11y: `alt`, `aria-*`, labels on inputs, semantic tags (`<button>` vs `<div onclick>`).
63
- - Semantic tags where they apply (`<button>`, `<label>`, `<ul>`/`<li>`) over generic `<div>`/`<span>`. Landmarks (`<header>`/`<nav>`/`<main>`) apply to shell/layout, not feature components.
64
- - No inline `style=""` for static styling - use the component's `.less` file. `[style.x]`/`[ngStyle]` ok for dynamic values.
65
-
66
- <!-- lang: ts,html -->
67
- ## TypeScript / Angular
68
- - Follow the Angular style guide (https://angular.dev/style-guide).
69
- - Avoid `any`, prefer explicit types.
70
- - RxJS: unsubscribe via async pipe, `takeUntil`, or `takeUntilDestroyed`.
71
- - Use `OnPush` where it makes sense.
72
- - Separate presentation (component) from business logic (services); avoid heavy template logic.
73
- - Files: kebab-case + type suffix (`select-maintenance-plan-step.component.ts/.html/.less`).
74
- - Classes: PascalCase matching file name, no type suffix (`SelectMaintenancePlanStep`, not `...Component`).
75
- - Selectors: kebab-case with project prefix (`app-select-maintenance-plan-step`).
76
- - `.less` files named like their component; use `:host`, avoid global style leaks.
77
-
78
- <!-- lang: all -->
79
- ## Review Process Notes (meta)
80
- - Distinguish blocking comments (must-fix) from suggestions (nice-to-have) - this matters a lot for an automated reviewer, so it doesn't hold up merges over trivial issues.
81
- - If the change is purely formatting/whitespace, do not comment on it.
82
- - Ignore auto-generated files (generated migrations, `*.designer.cs`, `node_modules`, etc.) unless they contain manually-added code.
83
- - Flag only issues relevant to the diff/change being reviewed, not the entire file, unless the change requires it.
84
- - Before flagging something as "missing", verify it isn't present in the surrounding unchanged context (lines without `+`/`-`). Only flag genuinely absent elements, not elements outside the shown diff hunk.
85
- - Priority order: Security > Functional bugs > Maintainability > Style.
src/tfs_client.py CHANGED
@@ -324,8 +324,7 @@ class TFSClient:
324
324
  For ``diff_only`` scope (default), builds a standard unified diff for
325
325
  each changed file and appends an XML context block produced by
326
326
  :class:`~src.context_extractor.ContextExtractor` (one of
327
- ``<enclosing_scopes>``, ``<file_skeleton>``, ``<sql_statement>``, or
328
- ``<adaptive_diff>``). The block is preserved verbatim by
327
+ ``<enclosing_scopes>`` or ``<adaptive_diff>``). The block is preserved verbatim by
329
328
  :py:meth:`GitUtils.filter_diff_additions_only` so the LLM receives
330
329
  structured scope context without consuming the entire file.
331
330
 
@@ -500,18 +499,15 @@ class TFSClient:
500
499
 
501
500
  Fetches the old version from the target branch and the new version
502
501
  from the source branch, then generates a standard unified diff with
503
- 3 lines of context using :py:mod:`difflib`.
502
+ 5 lines of context using :py:mod:`difflib`.
504
503
 
505
504
  After the diff lines an XML context block is appended, produced by
506
505
  :class:`~src.context_extractor.ContextExtractor`:
507
506
 
508
- * ``<enclosing_scopes>`` — the function(s)/method(s) enclosing
509
- the changed lines (Scenario A: ≤ max_scope_blocks distinct blocks)
510
- * ``<file_skeleton>`` — structural outline of the whole file
511
- (Scenario B: > max_scope_blocks blocks, or scope exceeds max_scope_lines)
512
- * ``<sql_statement>`` — the full SQL statement for SQL files
513
- * ``<adaptive_diff>`` — surrounding context lines when AST parsing
514
- is unavailable or the language is unsupported
507
+ * ``<enclosing_scopes>`` — the AST scope(s) enclosing the changed
508
+ lines when they fit the configured block and line limits
509
+ * ``<adaptive_diff>`` — surrounding context lines when AST extraction
510
+ is unavailable, unsupported, fails, or exceeds those limits
515
511
 
516
512
  The XML block is preserved verbatim by
517
513
  :py:meth:`GitUtils.filter_diff_additions_only`.
@@ -565,7 +561,7 @@ class TFSClient:
565
561
  fromfile=f"a{original_path}",
566
562
  tofile=f"b{file_path}",
567
563
  lineterm="",
568
- n=3,
564
+ n=5,
569
565
  ))
570
566
 
571
567
  if not diff:
@@ -581,8 +577,8 @@ class TFSClient:
581
577
  raw_diff = list(diff)
582
578
  result = [f"diff --git a{original_path} b{file_path}"] + raw_diff
583
579
 
584
- # Build the AST-based context block (Enclosing Scopes / File Skeleton /
585
- # Adaptive Diff fallback) and append it after the diff section.
580
+ # Build the AST-based context block (Enclosing Scopes or Adaptive Diff
581
+ # fallback) and append it after the diff section.
586
582
  if new_lines:
587
583
  new_content = "\n".join(new_lines)
588
584
  file_diff_text = "\n".join(result)
@@ -1,17 +0,0 @@
1
- src/__init__.py,sha256=BNfzb1HoMlgLc9MNEzc8Ne7USol7NHRUcMn7ISQ41sY,259
2
- src/ai_review.py,sha256=Wfeo72uTsjuoKMW5ToqFi6edfVIWTMgqPIbv9_W8Z-Y,34482
3
- src/config.py,sha256=_k0XPY-XVO6xjgwT2zWfox1AQztbk6fkqeqmiSFcGBA,16435
4
- src/context_extractor.py,sha256=qyiAUjznPfWyZw9VN34nf7OYcZrPUp92pUvR_Y7uSi0,24466
5
- src/formatter.py,sha256=68kz1U6tMndLcmtJRImN4jAIpxXtog3cKtiJ-zc3S0M,19027
6
- src/git_utils.py,sha256=yq31qkIXEbuv4zVGNwqW92N-X66SfyldSPCe5i0cX30,9812
7
- src/llm_client.py,sha256=4WMDs23FxHnrjRDfsbCvedtILYZA00Slss0XrSVvtLE,53672
8
- src/prompt_utils.py,sha256=FnWgHQ7UW8dEKW-4R7ngpL6DNqbW7xFEKi5JsZ7348E,2045
9
- src/tfs_client.py,sha256=tZ7-UyNmayPWoptDntefUWDb2By98WkJr4v5MtEM80I,37744
10
- src/token_estimator.py,sha256=4_4TXhefaNi7KHomwhhWbt7GB6KfubCQVBEbD3YoK5Q,3192
11
- src/prompts/config.yaml.template,sha256=JlUSywkr0w2K94SzkbHJkkPuxM04gckfjyTJY5yPiHg,4827
12
- src/prompts/review_prompt.md.template,sha256=hoGP4C6hQQpqDPVzwRTWuS5HCbaUyOgFllciKO8R61I,5019
13
- code_review_ai_cli-2.2.4.dist-info/METADATA,sha256=3fRvsCtLrA_LsCmxsQEXFcF9Dit6HUW0kk8aRsuke6s,5040
14
- code_review_ai_cli-2.2.4.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
15
- code_review_ai_cli-2.2.4.dist-info/entry_points.txt,sha256=hzKqoXIQVpiCe4H-XV0DZcKwJY6Uj3M-WvBMEQtwr3o,49
16
- code_review_ai_cli-2.2.4.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
17
- code_review_ai_cli-2.2.4.dist-info/RECORD,,