code-review-ai-cli 2.4.0__tar.gz → 2.5.0__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.
Files changed (31) hide show
  1. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/PKG-INFO +4 -2
  2. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/README.md +3 -1
  3. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/code_review_ai_cli.egg-info/PKG-INFO +4 -2
  4. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/pyproject.toml +1 -1
  5. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/ai_review.py +176 -48
  6. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/config.py +0 -8
  7. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/context_extractor.py +50 -5
  8. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/formatter.py +29 -27
  9. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/git_utils.py +219 -75
  10. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/llm_client.py +36 -83
  11. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/prompts/config.yaml.template +2 -7
  12. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/tfs_client.py +28 -109
  13. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/tests/test_ai_review.py +84 -5
  14. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/tests/test_config.py +0 -4
  15. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/tests/test_git_utils.py +58 -19
  16. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/tests/test_llm_client.py +36 -51
  17. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/tests/test_tfs_client.py +6 -22
  18. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/code_review_ai_cli.egg-info/SOURCES.txt +0 -0
  19. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/code_review_ai_cli.egg-info/dependency_links.txt +0 -0
  20. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/code_review_ai_cli.egg-info/entry_points.txt +0 -0
  21. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/code_review_ai_cli.egg-info/requires.txt +0 -0
  22. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/code_review_ai_cli.egg-info/top_level.txt +0 -0
  23. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/setup.cfg +0 -0
  24. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/__init__.py +0 -0
  25. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/prompt_utils.py +0 -0
  26. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/prompts/review_prompt.md.template +0 -0
  27. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/token_estimator.py +0 -0
  28. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/tests/test_context_extractor.py +0 -0
  29. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/tests/test_formatter.py +0 -0
  30. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/tests/test_prompt_utils.py +0 -0
  31. {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/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.4.0
3
+ Version: 2.5.0
4
4
  Summary: Automated AI-powered code review CLI for Azure DevOps / TFS Pull Requests
5
5
  License: Apache-2.0
6
6
  Keywords: code-review,ai,azure-devops,tfs,pull-request,git,llm
@@ -38,6 +38,7 @@ AI Code Review is an automated AI-powered code review CLI, compatible with vario
38
38
 
39
39
  - AI Pull Request review (`pr-review`)
40
40
  - Local Git review without a PR (`local` / `local-review` — working tree, `--staged`, `--from <branch>`, `--commit <sha>`)
41
+ - **Full Code Scan (`full-code-scan`)**: on-demand static quality and security audit of files, folders, or glob patterns without requiring a Git diff or PR
41
42
  - **Smart File Bundling**: groups related files (interfaces, implementations, unit tests, i18n pairs, and domain folders) into cohesive bundles (`max_files_per_bundle`) so large changesets are reviewed completely without context degradation
42
43
  - **Deterministic Cross-File Symbol Resolution**: resolves compact signatures and type declarations (`<referenced_symbols>`) via Tree-sitter across PR/local changesets and relative/sibling imports (`max_referenced_symbols`)
43
44
  - Structured PR comments (inline + general summary)
@@ -113,12 +114,13 @@ tfs:
113
114
 
114
115
  review:
115
116
  verbosity: detailed
116
- scope: diff_only
117
117
  custom_prompt_file: review_prompt.md
118
118
  # maximum related files grouped per LLM review bundle (Smart File Bundling)
119
119
  max_files_per_bundle: 5
120
120
  # per-file limit
121
121
  max_diff_lines: 2000
122
+ # cross-file symbol declarations resolved per file (0 = disabled)
123
+ max_referenced_symbols: 10
122
124
  # extension allowlist (empty list = all files)
123
125
  file_extensions_filter: [".cs", ".ts", ".py"]
124
126
  # exclude path prefixes from the diff (applied before file_extensions_filter)
@@ -6,6 +6,7 @@ AI Code Review is an automated AI-powered code review CLI, compatible with vario
6
6
 
7
7
  - AI Pull Request review (`pr-review`)
8
8
  - Local Git review without a PR (`local` / `local-review` — working tree, `--staged`, `--from <branch>`, `--commit <sha>`)
9
+ - **Full Code Scan (`full-code-scan`)**: on-demand static quality and security audit of files, folders, or glob patterns without requiring a Git diff or PR
9
10
  - **Smart File Bundling**: groups related files (interfaces, implementations, unit tests, i18n pairs, and domain folders) into cohesive bundles (`max_files_per_bundle`) so large changesets are reviewed completely without context degradation
10
11
  - **Deterministic Cross-File Symbol Resolution**: resolves compact signatures and type declarations (`<referenced_symbols>`) via Tree-sitter across PR/local changesets and relative/sibling imports (`max_referenced_symbols`)
11
12
  - Structured PR comments (inline + general summary)
@@ -81,12 +82,13 @@ tfs:
81
82
 
82
83
  review:
83
84
  verbosity: detailed
84
- scope: diff_only
85
85
  custom_prompt_file: review_prompt.md
86
86
  # maximum related files grouped per LLM review bundle (Smart File Bundling)
87
87
  max_files_per_bundle: 5
88
88
  # per-file limit
89
89
  max_diff_lines: 2000
90
+ # cross-file symbol declarations resolved per file (0 = disabled)
91
+ max_referenced_symbols: 10
90
92
  # extension allowlist (empty list = all files)
91
93
  file_extensions_filter: [".cs", ".ts", ".py"]
92
94
  # exclude path prefixes from the diff (applied before file_extensions_filter)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-review-ai-cli
3
- Version: 2.4.0
3
+ Version: 2.5.0
4
4
  Summary: Automated AI-powered code review CLI for Azure DevOps / TFS Pull Requests
5
5
  License: Apache-2.0
6
6
  Keywords: code-review,ai,azure-devops,tfs,pull-request,git,llm
@@ -38,6 +38,7 @@ AI Code Review is an automated AI-powered code review CLI, compatible with vario
38
38
 
39
39
  - AI Pull Request review (`pr-review`)
40
40
  - Local Git review without a PR (`local` / `local-review` — working tree, `--staged`, `--from <branch>`, `--commit <sha>`)
41
+ - **Full Code Scan (`full-code-scan`)**: on-demand static quality and security audit of files, folders, or glob patterns without requiring a Git diff or PR
41
42
  - **Smart File Bundling**: groups related files (interfaces, implementations, unit tests, i18n pairs, and domain folders) into cohesive bundles (`max_files_per_bundle`) so large changesets are reviewed completely without context degradation
42
43
  - **Deterministic Cross-File Symbol Resolution**: resolves compact signatures and type declarations (`<referenced_symbols>`) via Tree-sitter across PR/local changesets and relative/sibling imports (`max_referenced_symbols`)
43
44
  - Structured PR comments (inline + general summary)
@@ -113,12 +114,13 @@ tfs:
113
114
 
114
115
  review:
115
116
  verbosity: detailed
116
- scope: diff_only
117
117
  custom_prompt_file: review_prompt.md
118
118
  # maximum related files grouped per LLM review bundle (Smart File Bundling)
119
119
  max_files_per_bundle: 5
120
120
  # per-file limit
121
121
  max_diff_lines: 2000
122
+ # cross-file symbol declarations resolved per file (0 = disabled)
123
+ max_referenced_symbols: 10
122
124
  # extension allowlist (empty list = all files)
123
125
  file_extensions_filter: [".cs", ".ts", ".py"]
124
126
  # exclude path prefixes from the diff (applied before file_extensions_filter)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code-review-ai-cli"
7
- version = "2.4.0"
7
+ version = "2.5.0"
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"
@@ -10,16 +10,16 @@ Main mode: Pull Request review on Azure DevOps/TFS with
10
10
  automatic comments posted directly to the PR.
11
11
 
12
12
  Usage:
13
- python ai_review.py # Interactive mode (main menu)
14
- python ai_review.py pr-review # List PRs and do interactive review
15
- python ai_review.py pr-review <id> # Direct review of a specific PR
16
- python ai_review.py local # Review local Git changes (uncommitted/staged/branch)
17
- python ai_review.py list-prs # List active Pull Requests
13
+ python ai_review.py # Interactive mode (main menu)
14
+ python ai_review.py pr-review # List PRs and do interactive review
15
+ python ai_review.py pr-review <id> # Direct review of a specific PR
16
+ python ai_review.py local # Review local Git changes (uncommitted/staged/branch)
17
+ python ai_review.py full-code-scan [paths] # Audit full files, folders, or glob patterns
18
+ python ai_review.py list-prs # List active Pull Requests
18
19
 
19
20
  Options:
20
21
  --detailed # Detailed review (default)
21
22
  --security # Security-focused review
22
- --review-scope <diff_only|full_code> # Review scope (default: diff_only)
23
23
  --max-files-per-bundle <n> # Max related files per LLM bundle (overrides config)
24
24
  --dry-run # Review without posting comments
25
25
  --auto-post # Post comments without confirmation
@@ -168,7 +168,9 @@ def build_parser() -> argparse.ArgumentParser:
168
168
  " %(prog)s local # Review local uncommitted changes\n"
169
169
  " %(prog)s local --staged # Review staged changes\n"
170
170
  " %(prog)s local --from main # Review branch diff against main\n"
171
- " %(prog)s list-prs --status active # List active PRs\n"
171
+ " %(prog)s full-code-scan src/ # Audit all eligible files in src/\n"
172
+ " %(prog)s full-code-scan \"src/**/*.cs\" # Audit files matching a glob\n"
173
+ " %(prog)s list-prs --status active # List active PRs\n"
172
174
  ),
173
175
  )
174
176
 
@@ -219,6 +221,23 @@ def build_parser() -> argparse.ArgumentParser:
219
221
  help="Path to the local Git repository (default: current directory)",
220
222
  )
221
223
 
224
+ # --- full-code-scan (FULL FILE AUDIT MODE) ---
225
+ sub_scan = subparsers.add_parser(
226
+ "full-code-scan",
227
+ help="🔎 Full Code Scan (audit local files, directories, or glob patterns without a diff)",
228
+ )
229
+ sub_scan.add_argument(
230
+ "paths",
231
+ nargs="*",
232
+ default=["."],
233
+ metavar="PATH",
234
+ help="Files, directories, or glob patterns to scan (default: current directory)",
235
+ )
236
+ sub_scan.add_argument(
237
+ "--repo-path", default=None, metavar="PATH",
238
+ help="Base directory for relative paths and filters (default: current directory)",
239
+ )
240
+
222
241
  # --- init ---
223
242
  subparsers.add_parser(
224
243
  "init", help="Create a config.yaml template in the current directory"
@@ -236,7 +255,7 @@ def build_parser() -> argparse.ArgumentParser:
236
255
  help="Filter by author")
237
256
 
238
257
  # --- Global options ---
239
- all_subs = [sub_pr_review, sub_local, sub_list_prs]
258
+ all_subs = [sub_pr_review, sub_local, sub_scan, sub_list_prs]
240
259
  for sub in all_subs:
241
260
  _add_global_options(sub)
242
261
 
@@ -259,11 +278,6 @@ def _add_global_options(parser: argparse.ArgumentParser) -> None:
259
278
  "--security", "-S", action="store_const", dest="verbosity",
260
279
  const="security", help="Security-focused review"
261
280
  )
262
- group_review.add_argument(
263
- "--review-scope", default=None,
264
- choices=["diff_only", "full_code"],
265
- help="Review scope: diff_only (default) or full_code"
266
- )
267
281
  group_review.add_argument(
268
282
  "--max-files-per-bundle", default=None, type=int, metavar="N",
269
283
  help="Max related files grouped per LLM bundle (overrides review.max_files_per_bundle)"
@@ -353,8 +367,6 @@ def run_review(args: argparse.Namespace) -> int:
353
367
  config.model = args.model
354
368
  if getattr(args, "provider", None):
355
369
  config.llm_provider = args.provider
356
- if getattr(args, "review_scope", None):
357
- config.review_scope = args.review_scope
358
370
  if getattr(args, "max_files_per_bundle", None) is not None:
359
371
  config.max_files_per_bundle = args.max_files_per_bundle
360
372
  if getattr(args, "output_format", None):
@@ -393,6 +405,8 @@ def run_review(args: argparse.Namespace) -> int:
393
405
  return run_pr_review_workflow(args, config, formatter)
394
406
  elif command in ("local", "local-review"):
395
407
  return run_local_review_workflow(args, config, formatter)
408
+ elif command == "full-code-scan":
409
+ return run_full_code_scan_workflow(args, config, formatter)
396
410
  elif command == "list-prs":
397
411
  return run_list_prs(args, config, formatter)
398
412
 
@@ -406,7 +420,7 @@ def _execute_bundled_review(
406
420
  llm: LLMClient,
407
421
  bundles: list[dict],
408
422
  context: str,
409
- review_scope: str,
423
+ full_code_scan: bool = False,
410
424
  ) -> tuple[str, list[dict]]:
411
425
  """Executes LLM review across one or more cohesive file bundles and merges results."""
412
426
  if not bundles:
@@ -418,7 +432,7 @@ def _execute_bundled_review(
418
432
  diff=bundle["diff"],
419
433
  files_summary=bundle["files_summary"],
420
434
  context=context,
421
- review_scope=review_scope,
435
+ full_code_scan=full_code_scan,
422
436
  )
423
437
 
424
438
  summaries: list[str] = []
@@ -432,7 +446,7 @@ def _execute_bundled_review(
432
446
  diff=bundle["diff"],
433
447
  files_summary=bundle["files_summary"],
434
448
  context=context,
435
- review_scope=review_scope,
449
+ full_code_scan=full_code_scan,
436
450
  )
437
451
  if summary_text and summary_text.strip():
438
452
  summaries.append(f"**Bundle {idx}/{len(bundles)}:** {summary_text.strip()}")
@@ -457,6 +471,19 @@ def _execute_bundled_review(
457
471
  return combined_summary, all_comments
458
472
 
459
473
 
474
+ def _init_debug_dump(
475
+ config: ReviewConfig,
476
+ formatter: ReviewFormatter,
477
+ prefix: str,
478
+ ) -> None:
479
+ """Initializes a timestamped debug dump log path when debug_dump is enabled."""
480
+ if config.debug_dump and not config.debug_dump_file:
481
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
482
+ config.debug_dump_file = os.path.join("logs", f"{prefix}_{timestamp}_debug.log")
483
+ os.makedirs(os.path.dirname(config.debug_dump_file), exist_ok=True)
484
+ print(formatter.format_info(f"🐛 Debug dump enabled: {config.debug_dump_file}"))
485
+
486
+
460
487
  def _prepare_and_execute_review(
461
488
  diff: str,
462
489
  args: argparse.Namespace,
@@ -464,6 +491,7 @@ def _prepare_and_execute_review(
464
491
  formatter: ReviewFormatter,
465
492
  git_utils: GitUtils,
466
493
  progress_message: str,
494
+ full_code_scan: bool = False,
467
495
  ) -> tuple[int, Optional[tuple[str, list[dict], bool]]]:
468
496
  """Filters, truncates, bundles *diff*, executes LLM review, and logs token stats."""
469
497
  diff = git_utils.filter_diff_additions_only(diff)
@@ -503,7 +531,7 @@ def _prepare_and_execute_review(
503
531
  llm=llm,
504
532
  bundles=bundles,
505
533
  context=getattr(args, "context", ""),
506
- review_scope=config.review_scope,
534
+ full_code_scan=full_code_scan,
507
535
  )
508
536
  except LLMError as exc:
509
537
  progress.stop()
@@ -584,11 +612,7 @@ def run_pr_review_workflow(args: argparse.Namespace, config: ReviewConfig,
584
612
  # Show PR details
585
613
  print(formatter.format_pr_details(pr_details))
586
614
 
587
- if config.debug_dump and not config.debug_dump_file:
588
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
589
- config.debug_dump_file = os.path.join("logs", f"pr_{pr_id}_{timestamp}_debug.log")
590
- os.makedirs(os.path.dirname(config.debug_dump_file), exist_ok=True)
591
- print(formatter.format_info(f"🐛 Debug dump enabled: {config.debug_dump_file}"))
615
+ _init_debug_dump(config, formatter, f"pr_{pr_id}")
592
616
 
593
617
  # --- Get PR diff ---
594
618
  print(formatter.format_progress("Getting Pull Request diff"))
@@ -597,7 +621,6 @@ def run_pr_review_workflow(args: argparse.Namespace, config: ReviewConfig,
597
621
  diff = tfs.get_pull_request_diff(
598
622
  repo_name,
599
623
  pr_id,
600
- review_scope=config.review_scope,
601
624
  excluded_paths=config.excluded_paths or None,
602
625
  file_extensions_filter=config.file_extensions_filter or None,
603
626
  )
@@ -612,7 +635,7 @@ def run_pr_review_workflow(args: argparse.Namespace, config: ReviewConfig,
612
635
  # --- AI Analysis ---
613
636
  print(formatter.format_info(
614
637
  f"Provider: {config.llm_provider} | Model: {config.model} | "
615
- f"Mode: {config.verbosity} | Scope: {config.review_scope}"
638
+ f"Mode: {config.verbosity}"
616
639
  ))
617
640
 
618
641
  dry_run = config.dry_run or getattr(args, "dry_run", False)
@@ -693,7 +716,6 @@ def run_pr_review_workflow(args: argparse.Namespace, config: ReviewConfig,
693
716
  repo_name,
694
717
  pr_id,
695
718
  comments_to_post,
696
- review_scope=config.review_scope,
697
719
  comment_mode=config.pr_comment_mode,
698
720
  )
699
721
  print(formatter.format_post_results(results))
@@ -888,24 +910,14 @@ def run_local_review_workflow(args: argparse.Namespace, config: ReviewConfig,
888
910
  }
889
911
  print(formatter.format_local_details(local_details))
890
912
 
891
- if config.debug_dump and not config.debug_dump_file:
892
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
893
- config.debug_dump_file = os.path.join("logs", f"local_{timestamp}_debug.log")
894
- os.makedirs(os.path.dirname(config.debug_dump_file), exist_ok=True)
895
- print(formatter.format_info(f"🐛 Debug dump enabled: {config.debug_dump_file}"))
913
+ _init_debug_dump(config, formatter, "local")
896
914
 
897
915
  print(formatter.format_progress("Building local diff and AST context"))
898
916
 
899
- extractor = ContextExtractor(
900
- max_scope_blocks=config.max_scope_blocks,
901
- max_scope_lines=config.max_scope_lines,
902
- adaptive_context_lines=config.adaptive_context_lines,
903
- max_referenced_symbols=config.max_referenced_symbols,
904
- )
917
+ extractor = ContextExtractor.from_config(config)
905
918
 
906
919
  try:
907
920
  diff = git_utils.get_local_diff(
908
- review_scope=config.review_scope,
909
921
  excluded_paths=config.excluded_paths or None,
910
922
  file_extensions_filter=config.file_extensions_filter or None,
911
923
  staged=staged,
@@ -924,7 +936,7 @@ def run_local_review_workflow(args: argparse.Namespace, config: ReviewConfig,
924
936
 
925
937
  print(formatter.format_info(
926
938
  f"Provider: {config.llm_provider} | Model: {config.model} | "
927
- f"Mode: {config.verbosity} | Scope: {config.review_scope}"
939
+ f"Mode: {config.verbosity}"
928
940
  ))
929
941
 
930
942
  status_code, review_outcome = _prepare_and_execute_review(
@@ -959,6 +971,96 @@ def run_local_review_workflow(args: argparse.Namespace, config: ReviewConfig,
959
971
  return 0
960
972
 
961
973
 
974
+ # ---------------------------------------------------------------------------
975
+ # Full Code Scan (static audit of local files / folders / globs)
976
+ # ---------------------------------------------------------------------------
977
+ def run_full_code_scan_workflow(args: argparse.Namespace, config: ReviewConfig,
978
+ formatter: ReviewFormatter) -> int:
979
+ """
980
+ Full code static audit workflow for files, folders, or glob patterns.
981
+ Does not require a Git diff or TFS/Azure DevOps configuration.
982
+ """
983
+ repo_path = getattr(args, "repo_path", None)
984
+ git_utils = GitUtils(repo_path=repo_path, validate=False)
985
+ targets = list(getattr(args, "paths", None) or ["."])
986
+ target_label = ", ".join(targets)
987
+
988
+ base_abs = os.path.abspath(git_utils.repo_path)
989
+ repo_name = os.path.basename(base_abs.rstrip("/\\")) or "local-scan"
990
+ branch = ""
991
+ try:
992
+ git_utils._validate_repo()
993
+ repo_name = git_utils.get_repo_name()
994
+ branch = git_utils.get_current_branch()
995
+ except GitError:
996
+ pass
997
+
998
+ print(formatter.format_progress(f"Scanning files matching ({target_label})"))
999
+
1000
+ extractor = ContextExtractor.from_config(config)
1001
+
1002
+ diff, discovered_files = git_utils.get_scan_diff(
1003
+ targets=targets,
1004
+ excluded_paths=config.excluded_paths or None,
1005
+ file_extensions_filter=config.file_extensions_filter or None,
1006
+ context_extractor=extractor,
1007
+ )
1008
+
1009
+ if not discovered_files or not diff.strip():
1010
+ print(formatter.format_warning("No eligible files found to scan."))
1011
+ return 0
1012
+
1013
+ scan_details = {
1014
+ "title": "🔎 Full Code Scan",
1015
+ "files_label": "Scanned Files",
1016
+ "repository": repo_name,
1017
+ "branch": branch,
1018
+ "target": target_label,
1019
+ "changed_files": discovered_files,
1020
+ }
1021
+ print(formatter.format_local_details(scan_details))
1022
+
1023
+ _init_debug_dump(config, formatter, "scan")
1024
+
1025
+ print(formatter.format_info(
1026
+ f"Provider: {config.llm_provider} | Model: {config.model} | "
1027
+ f"Mode: {config.verbosity}"
1028
+ ))
1029
+
1030
+ status_code, review_outcome = _prepare_and_execute_review(
1031
+ diff=diff,
1032
+ args=args,
1033
+ config=config,
1034
+ formatter=formatter,
1035
+ git_utils=git_utils,
1036
+ progress_message="Scanning code with AI (may take 30-60s)",
1037
+ full_code_scan=True,
1038
+ )
1039
+ if review_outcome is None:
1040
+ return status_code
1041
+
1042
+ review_text, structured_comments, was_truncated = review_outcome
1043
+
1044
+ print(formatter.format_structured_comments(
1045
+ structured_comments,
1046
+ summary=review_text,
1047
+ ))
1048
+
1049
+ output_file = config.output_file or getattr(args, "output", "")
1050
+ branch_header = f"{branch} ({target_label})" if branch else target_label
1051
+ _save_review_output(
1052
+ output_file=output_file,
1053
+ review_type="Full Code Scan",
1054
+ repo_name=repo_name,
1055
+ branch=branch_header,
1056
+ review_text=review_text,
1057
+ structured_comments=structured_comments,
1058
+ was_truncated=was_truncated,
1059
+ )
1060
+
1061
+ return 0
1062
+
1063
+
962
1064
  # ---------------------------------------------------------------------------
963
1065
  # List PRs
964
1066
  # ---------------------------------------------------------------------------
@@ -1014,15 +1116,16 @@ def interactive_mode() -> int:
1014
1116
  print(f" {c.CYAN}1{c.RESET}) 🌟 Pull Request Review (list PRs and select)")
1015
1117
  print(f" {c.CYAN}2{c.RESET}) 📋 List active Pull Requests")
1016
1118
  print("")
1017
- print(f" {c.CYAN}{c.BOLD}── Local Git ──{c.RESET}")
1119
+ print(f" {c.CYAN}{c.BOLD}── Local Git & Static Audit ──{c.RESET}")
1018
1120
  print(f" {c.CYAN}3{c.RESET}) 💻 Local Git Review (working tree, staged, or branch diff)")
1121
+ print(f" {c.CYAN}4{c.RESET}) 🔎 Full Code Scan (audit files, folders, or glob patterns)")
1019
1122
  print("")
1020
1123
  print(f" {c.CYAN}{c.BOLD}── Other ──{c.RESET}")
1021
- print(f" {c.CYAN}4{c.RESET}) ⚙️ Current configuration")
1124
+ print(f" {c.CYAN}5{c.RESET}) ⚙️ Current configuration")
1022
1125
  print(f" {c.CYAN}0{c.RESET}) Exit")
1023
1126
 
1024
1127
  try:
1025
- choice = input(f"\n{c.BOLD}Choose [0-4]: {c.RESET}").strip()
1128
+ choice = input(f"\n{c.BOLD}Choose [0-5]: {c.RESET}").strip()
1026
1129
  except (KeyboardInterrupt, EOFError):
1027
1130
  print("\n")
1028
1131
  return 0
@@ -1034,12 +1137,15 @@ def interactive_mode() -> int:
1034
1137
  return _interactive_pr_review(config)
1035
1138
 
1036
1139
  if choice == "2":
1037
- return _interactive_list_prs(config)
1140
+ return _interactive_list_prs()
1038
1141
 
1039
1142
  if choice == "3":
1040
- return _interactive_local_review(config)
1143
+ return _interactive_local_review()
1041
1144
 
1042
1145
  if choice == "4":
1146
+ return _interactive_full_code_scan()
1147
+
1148
+ if choice == "5":
1043
1149
  _show_config(config)
1044
1150
  return 0
1045
1151
 
@@ -1047,7 +1153,7 @@ def interactive_mode() -> int:
1047
1153
  return 1
1048
1154
 
1049
1155
 
1050
- def _interactive_pr_review(config: ReviewConfig) -> int:
1156
+ def _interactive_pr_review(config: Optional[ReviewConfig] = None) -> int:
1051
1157
  """Interactive PR review workflow."""
1052
1158
  c = Colors
1053
1159
 
@@ -1079,7 +1185,7 @@ def _interactive_pr_review(config: ReviewConfig) -> int:
1079
1185
  return run_review(parsed)
1080
1186
 
1081
1187
 
1082
- def _interactive_list_prs(config: ReviewConfig) -> int:
1188
+ def _interactive_list_prs() -> int:
1083
1189
  """Lists PRs interactively."""
1084
1190
  argv = ["list-prs"]
1085
1191
  parser = build_parser()
@@ -1087,7 +1193,7 @@ def _interactive_list_prs(config: ReviewConfig) -> int:
1087
1193
  return run_review(parsed)
1088
1194
 
1089
1195
 
1090
- def _interactive_local_review(config: ReviewConfig) -> int:
1196
+ def _interactive_local_review() -> int:
1091
1197
  """Interactive local Git review workflow."""
1092
1198
  c = Colors
1093
1199
 
@@ -1122,6 +1228,28 @@ def _interactive_local_review(config: ReviewConfig) -> int:
1122
1228
  return run_review(parsed)
1123
1229
 
1124
1230
 
1231
+ def _interactive_full_code_scan() -> int:
1232
+ """Interactive full-code-scan workflow."""
1233
+ c = Colors
1234
+
1235
+ try:
1236
+ raw_target = input(
1237
+ f"\n{c.BOLD}Path or glob pattern to scan (default=.): {c.RESET}"
1238
+ ).strip() or "."
1239
+ except (KeyboardInterrupt, EOFError):
1240
+ print("\n")
1241
+ return 0
1242
+
1243
+ argv = ["full-code-scan", raw_target]
1244
+ verbosity = _ask_verbosity()
1245
+ if verbosity:
1246
+ argv.append(f"--{verbosity}")
1247
+
1248
+ parser = build_parser()
1249
+ parsed = parser.parse_args(argv)
1250
+ return run_review(parsed)
1251
+
1252
+
1125
1253
  def _ask_verbosity() -> str:
1126
1254
  """Asks for the verbosity mode."""
1127
1255
  c = Colors
@@ -89,7 +89,6 @@ class ReviewConfig:
89
89
 
90
90
  # --- Review -------------------------------------------------------
91
91
  verbosity: str = "detailed" # "detailed" | "security"
92
- review_scope: str = "diff_only" # "diff_only" | "full_code"
93
92
  max_files_per_bundle: int = 5 # Max related files grouped per LLM bundle
94
93
  max_diff_lines: int = 2000 # Max diff lines
95
94
  max_scope_blocks: int = 8 # Max changed blocks for Enclosing Scopes strategy
@@ -219,7 +218,6 @@ class ReviewConfig:
219
218
  "tfs_repository": ("tfs", "repository"),
220
219
  # Review
221
220
  "verbosity": ("review", "verbosity"),
222
- "review_scope": ("review", "scope"),
223
221
  "max_files_per_bundle": ("review", "max_files_per_bundle"),
224
222
  "max_diff_lines": ("review", "max_diff_lines"),
225
223
  "max_scope_blocks": ("review", "max_scope_blocks"),
@@ -321,12 +319,6 @@ class ReviewConfig:
321
319
  "Use 'detailed' or 'security'."
322
320
  )
323
321
 
324
- if self.review_scope not in ("diff_only", "full_code"):
325
- issues.append(
326
- f"Invalid review scope: '{self.review_scope}'. "
327
- "Use 'diff_only' or 'full_code'."
328
- )
329
-
330
322
  if self.max_files_per_bundle <= 0:
331
323
  issues.append(
332
324
  f"Invalid max_files_per_bundle: '{self.max_files_per_bundle}'. "
@@ -1019,6 +1019,20 @@ class ContextExtractor:
1019
1019
  self.max_referenced_symbols = max_referenced_symbols
1020
1020
  self._symbol_cache: dict[tuple[str, int], list[SymbolInfo]] = {}
1021
1021
 
1022
+ @classmethod
1023
+ def from_config(cls, config: object) -> "ContextExtractor":
1024
+ """Build a :class:`ContextExtractor` from a :class:`~src.config.ReviewConfig`."""
1025
+ return cls(
1026
+ max_scope_blocks=getattr(config, "max_scope_blocks", MAX_SCOPE_BLOCKS),
1027
+ max_scope_lines=getattr(config, "max_scope_lines", MAX_SCOPE_LINES),
1028
+ adaptive_context_lines=getattr(
1029
+ config, "adaptive_context_lines", ADAPTIVE_CONTEXT_LINES
1030
+ ),
1031
+ max_referenced_symbols=getattr(
1032
+ config, "max_referenced_symbols", MAX_REFERENCED_SYMBOLS
1033
+ ),
1034
+ )
1035
+
1022
1036
  # ------------------------------------------------------------------
1023
1037
  # Public entry point
1024
1038
  # ------------------------------------------------------------------
@@ -1617,24 +1631,27 @@ def should_include_file(
1617
1631
  return True
1618
1632
 
1619
1633
 
1620
- def format_full_code_diff(
1634
+ def format_scan_file_diff(
1621
1635
  file_path: str,
1622
- change_type: str,
1623
1636
  content: Optional[str],
1624
1637
  unavailable_error: bool = False,
1625
1638
  ) -> list[str]:
1626
- """Build a ``full_code`` pseudo-diff payload for a single changed file."""
1639
+ """Build a pseudo-diff payload for a full-file static scan (``full-code-scan``)."""
1627
1640
  parts = [
1628
1641
  f"diff --git a{file_path} b{file_path}",
1629
1642
  f"--- a{file_path}",
1630
1643
  f"+++ b{file_path}",
1631
- f"@@ Change type: {change_type} @@",
1632
1644
  ]
1633
1645
  if unavailable_error:
1646
+ parts.append("@@ -0,0 +1,1 @@")
1634
1647
  parts.append(f"+[Content not available for {file_path}]")
1635
1648
  elif content:
1636
- for line in content.split("\n"):
1649
+ lines = content.splitlines()
1650
+ parts.append(f"@@ -0,0 +1,{len(lines)} @@")
1651
+ for line in lines:
1637
1652
  parts.append(f"+{line}")
1653
+ else:
1654
+ parts.append("@@ -0,0 +0,0 @@")
1638
1655
  return parts
1639
1656
 
1640
1657
 
@@ -1683,3 +1700,31 @@ def format_unified_diff_with_context(
1683
1700
  result.append(xml_block)
1684
1701
 
1685
1702
  return result
1703
+
1704
+
1705
+ def assemble_diff_with_referenced_symbols(
1706
+ built_entries: list[tuple[str, list[str]]],
1707
+ file_records: dict[str, tuple[str, list[int]]],
1708
+ candidate_files: dict[str, str],
1709
+ extractor: ContextExtractor,
1710
+ ) -> str:
1711
+ """Append ``<referenced_symbols>`` blocks to *built_entries* and join into a unified diff string."""
1712
+ if not built_entries:
1713
+ return ""
1714
+
1715
+ diff_parts: list[str] = []
1716
+ for entry_path, entry_lines in built_entries:
1717
+ if extractor.max_referenced_symbols > 0 and entry_path in file_records:
1718
+ new_txt, ch_lines = file_records[entry_path]
1719
+ ref_xml = extractor.extract_referenced_symbols(
1720
+ file_path=entry_path,
1721
+ new_content=new_txt,
1722
+ changed_lines=ch_lines,
1723
+ candidate_files=candidate_files,
1724
+ )
1725
+ if ref_xml:
1726
+ entry_lines.append(ref_xml)
1727
+ diff_parts.extend(entry_lines)
1728
+ diff_parts.append("")
1729
+
1730
+ return "\n".join(diff_parts)