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.
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/PKG-INFO +4 -2
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/README.md +3 -1
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/code_review_ai_cli.egg-info/PKG-INFO +4 -2
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/pyproject.toml +1 -1
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/ai_review.py +176 -48
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/config.py +0 -8
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/context_extractor.py +50 -5
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/formatter.py +29 -27
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/git_utils.py +219 -75
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/llm_client.py +36 -83
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/prompts/config.yaml.template +2 -7
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/tfs_client.py +28 -109
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/tests/test_ai_review.py +84 -5
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/tests/test_config.py +0 -4
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/tests/test_git_utils.py +58 -19
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/tests/test_llm_client.py +36 -51
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/tests/test_tfs_client.py +6 -22
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/code_review_ai_cli.egg-info/SOURCES.txt +0 -0
- {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
- {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
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/code_review_ai_cli.egg-info/requires.txt +0 -0
- {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
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/setup.cfg +0 -0
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/__init__.py +0 -0
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/prompt_utils.py +0 -0
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/prompts/review_prompt.md.template +0 -0
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/src/token_estimator.py +0 -0
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/tests/test_context_extractor.py +0 -0
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/tests/test_formatter.py +0 -0
- {code_review_ai_cli-2.4.0 → code_review_ai_cli-2.5.0}/tests/test_prompt_utils.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
|
14
|
-
python ai_review.py pr-review
|
|
15
|
-
python ai_review.py pr-review <id>
|
|
16
|
-
python ai_review.py local
|
|
17
|
-
python ai_review.py
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
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}
|
|
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}
|
|
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-
|
|
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(
|
|
1140
|
+
return _interactive_list_prs()
|
|
1038
1141
|
|
|
1039
1142
|
if choice == "3":
|
|
1040
|
-
return _interactive_local_review(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|