gitpr-cli 1.2.0__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. {gitpr_cli-1.2.0.dist-info → gitpr_cli-1.3.0.dist-info}/METADATA +66 -12
  2. gitpr_cli-1.3.0.dist-info/RECORD +108 -0
  3. src/application/__init__.py +2 -0
  4. src/application/use_cases/__init__.py +8 -0
  5. src/application/use_cases/generate_pr_explanation.py +73 -0
  6. src/application/use_cases/generate_test_file.py +227 -0
  7. src/branding/__init__.py +1 -0
  8. src/branding/badge_builder.py +139 -0
  9. src/branding/badge_data.py +53 -0
  10. src/changelog_builder.py +88 -15
  11. src/config.py +165 -1
  12. src/config_schema.py +107 -0
  13. src/core.py +75 -0
  14. src/demo/__init__.py +7 -0
  15. src/demo/demo_runner.py +336 -0
  16. src/demo/fake_ai_provider.py +112 -0
  17. src/demo/scenarios/__init__.py +128 -0
  18. src/demo/scenarios/laravel_bug_fix.py +443 -0
  19. src/demo/scenarios/security_issue.py +511 -0
  20. src/domain/linter/__init__.py +8 -0
  21. src/domain/linter/sast_finding_mapper.py +79 -0
  22. src/domain/pr/__init__.py +14 -0
  23. src/domain/pr/explain_section_builder.py +124 -0
  24. src/domain/tests_generation/__init__.py +14 -0
  25. src/domain/tests_generation/framework_detector.py +120 -0
  26. src/domain/tests_generation/test_content_types.py +43 -0
  27. src/domain/tests_generation/test_scaffold_builder.py +92 -0
  28. src/fix/__init__.py +1 -1
  29. src/fix/patch_applier.py +38 -168
  30. src/i18n.py +5 -1
  31. src/infrastructure/git/__init__.py +1 -0
  32. src/infrastructure/git/patch_applier.py +209 -0
  33. src/infrastructure/git/selective_stager.py +198 -0
  34. src/infrastructure/linter/external/__init__.py +9 -0
  35. src/infrastructure/linter/external/bandit_bridge.py +127 -0
  36. src/infrastructure/linter/external/base_bridge.py +105 -0
  37. src/infrastructure/linter/external/gitleaks_bridge.py +168 -0
  38. src/infrastructure/linter/external/semgrep_bridge.py +139 -0
  39. src/infrastructure/scm/__init__.py +11 -0
  40. src/infrastructure/scm/web_links.py +103 -0
  41. src/linter_engine.py +125 -6
  42. src/main.py +636 -2
  43. src/mcp_server.py +17 -1
  44. src/release_engine.py +358 -22
  45. src/security_ruleset.py +108 -0
  46. src/split/__init__.py +17 -0
  47. src/split/apply_split_plan.py +251 -0
  48. src/split/generate_split_plan.py +222 -0
  49. src/split/hunk_grouper.py +347 -0
  50. src/split/hunk_parser.py +256 -0
  51. src/split/split_plan.py +153 -0
  52. src/ui/chat_app.py +35 -9
  53. src/ui/demo/__init__.py +1 -0
  54. src/ui/demo/demo_app.py +91 -0
  55. src/ui/demo/demo_help_screen.py +52 -0
  56. src/ui/demo/demo_screens.py +60 -0
  57. src/updater.py +2 -2
  58. gitpr_cli-1.2.0.dist-info/RECORD +0 -67
  59. {gitpr_cli-1.2.0.dist-info → gitpr_cli-1.3.0.dist-info}/WHEEL +0 -0
  60. {gitpr_cli-1.2.0.dist-info → gitpr_cli-1.3.0.dist-info}/entry_points.txt +0 -0
  61. {gitpr_cli-1.2.0.dist-info → gitpr_cli-1.3.0.dist-info}/licenses/LICENSE +0 -0
  62. {gitpr_cli-1.2.0.dist-info → gitpr_cli-1.3.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitpr-cli
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: AI-powered PR automation, commit messages, and code review (Gemini, DeepSeek, and Ollama)
5
5
  Author-email: Natan Fiuza <contato@natanfiuza.dev.br>
6
6
  Requires-Python: >=3.10
@@ -24,6 +24,10 @@ Dynamic: license-file
24
24
  <img src="https://raw.githubusercontent.com/natanfiuza/gitpr/main/docs/logo.png" alt="GitPR Logo" width="150">
25
25
  </p>
26
26
 
27
+ <p align="center">
28
+ <a href="https://gitpr.natanfiuza.dev.br/"><img src="https://img.shields.io/badge/GitPR-quality--checked-blue" alt="GitPR"></a>
29
+ </p>
30
+
27
31
  GitPR CLI is a command-line automation tool that uses **Google Gemini**, **DeepSeek**, and **Ollama** artificial intelligence to analyze your code changes (git diff) or entire files. The tool automatically generates commit messages in the *Conventional Commits* standard, detailed Pull Request descriptions, and deep Code Reviews aimed at reducing technical debt.
28
32
 
29
33
  🌐 **Website:** [gitpr.natanfiuza.dev.br](https://gitpr.natanfiuza.dev.br/) · 📂 **Repository:** [https://github.com/gitpr-cli/gitpr.git](https://github.com/gitpr-cli/gitpr.git)
@@ -40,7 +44,18 @@ Install GitPR CLI using `pip`:
40
44
  pip install gitpr-cli
41
45
  ```
42
46
 
43
- ### **2. Initializing in a Repository**
47
+ ### **2. Seeing It Work (No Configuration)**
48
+
49
+ GitPR ships a guided tour over an example diff. It needs no API key, no Git repository and no connection:
50
+
51
+ ```bash
52
+ gitpr demo
53
+ ```
54
+
55
+ > **Guided Tour:** Six screens walking through a real example — the diff, the commit message, the code review and the pull request description — with the answers recorded once and replayed. Add `--no-tui` for plain text, `--scenario security-issue` for the other example.
56
+ > 📖 **Full Documentation:** [https://gitpr.natanfiuza.dev.br/docs/demo?lang=en_us](https://gitpr.natanfiuza.dev.br/docs/demo?lang=en_us)
57
+
58
+ ### **3. Initializing in a Repository**
44
59
 
45
60
  To set up GitPR in a folder of a new repository, run:
46
61
 
@@ -113,15 +128,19 @@ Pytest will automatically detect files inside the `tests/` folder and display a
113
128
 
114
129
  ### From Source Code
115
130
 
116
- 1. Clone the repository: `git clone https://github.com/gitpr-cli/gitpr.git.git`
131
+ 1. Clone the repository: `git clone https://github.com/gitpr-cli/gitpr.git`
117
132
 
118
133
  2. Enter the folder: `cd gitpr`
119
134
 
120
- 3. Set up the environment:
135
+ 3. Install in editable mode:
121
136
  ```bash
122
- pipenv install google-genai openai python-dotenv click cryptography
137
+ pip install -e .
123
138
  ```
124
- 4. Run: pipenv run python src/main.py
139
+ 4. Run: `gitpr`
140
+
141
+ > Prefer an isolated environment? Install the dependencies with `pipenv install google-genai openai python-dotenv click cryptography` and run `pipenv run python src/main.py` instead.
142
+ >
143
+ > **Note on the update gate:** a source install reports the version found in your checkout, so a tree behind the published release is stopped at startup. See [Installing GitPR from Source](docs/tutorial/install-from-source.md).
125
144
 
126
145
  ## **💻 How to Use**
127
146
 
@@ -137,6 +156,15 @@ The tool will sync with the remote (`git fetch`), compare your changes with the
137
156
  ### **Advanced Options and Commands**
138
157
  You can pass the following *flags* for specific actions:
139
158
 
159
+ * `gitpr demo`: **Guided tour** over an example diff that ships with the tool — the commit message, the code review and the pull request description, produced by the real pipeline on a recorded example. Needs no API key, no Git repository and no connection, which makes it the first thing to run on a new machine. Use `--scenario <name>` for another example, `--lang <code>` for another language, `--no-tui` for plain text. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/demo.md)
160
+ * `gitpr badge`: **Adoption badge for your README.** Prints the Markdown snippet — `--readme` prints only the line, `--style` picks the shields.io style (`flat`, `flat-square`, `for-the-badge`). Nothing is written to your README. Published pull requests carry a badge of their own, with the linter counts of the diff; `GITPR_BADGE=false` turns it off. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/badge.md)
161
+ * `gitpr config`: **Interactive configuration screen.** Opens a master-detail TUI to view, edit, search, and validate all settings stored in `~/.gitpr/.env` (with secret encryption and live validation). 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/config-tui.md)
162
+ * `gitpr release`: **Changelog and Release Notes generator.** Scans commits since the previous version, categorizes them according to Conventional Commits, suggests a semantic version bump, generates an AI executive summary, and optionally publishes the release to your forge. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/release-notes.md)
163
+ * `gitpr fix [<finding_id>]`: **Review findings patch application & rollback.** Turns findings from the last code review (`gitpr -r`) into safe, reviewable unified diffs. Includes safety classification (`safe`, `review_required`, `experimental`), `--all-safe` batching, `--force` validation, and `--rollback <patch-id>`. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/fix-command.md)
164
+ * `gitpr split`: **Atomic commit splitter.** Reads uncommitted working tree changes, groups hunks by intent using AI, and proposes ordered atomic commits without altering your working tree files (`--dry-run`, `--apply`). 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/split-command.md)
165
+ * `gitpr tests generate`: **AI test suite generation & scaffolding.** Analyzes your diff, a target file (`--file`), or a review finding (`--finding`) and generates complete, executable test suites (supporting pytest, jest, vitest, phpunit, pest). Use `--apply` to write to disk.
166
+ * `gitpr explain`: **Reviewer Guide generator.** Generates a concise summary explaining what changed, why it changed, where reviewers should focus, and potential regression risks. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/pull-request-publication.md)
167
+ * `gitpr review-pr <pr_number>`: **Remote Pull Request review.** Fetches and reviews an open pull request directly from the forge (GitHub, GitLab, Bitbucket, Azure DevOps) without checking out the branch locally. Use `--post-comment` to post the review directly to the PR. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/review-pr.md)
140
168
  * `-c` or `--commit`: Runs a local `git diff` and displays **only the suggested commit message**.
141
169
  * `-r` or `--review`: Performs a detailed **Code Review** of local changes.
142
170
  * `-f` or `--fullreview`: Performs a **Full Code Review** analyzing all changes since the remote branch.
@@ -147,6 +175,9 @@ You can pass the following *flags* for specific actions:
147
175
  * `-l` or `--linter`: Runs **only the local static linter** (no AI calls). Ideal for use in CI/CD pipelines to block non-compliant code.
148
176
  * `--status`: Lists uncommitted file changes categorized as **new**, **modified**, and **deleted** — fast, no AI, no network. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/git-status.md)
149
177
  * `--no-unstaged-check`: Skips the unstaged files verification before AI processing for a single invocation. Equivalent to `GITPR_SKIP_UNSTAGED_CHECK=true` for one run. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/git-status.md)
178
+ * `--no-suggest-reviewers`: Disables the suggested reviewers computation in the interactive PR publisher (default ON). 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/suggested-reviewers.md)
179
+ * `--explain`: Includes the Reviewer Guide section ("Explain my PR") in the Pull Request description.
180
+ * `--init`: **Interactive SCM forge wizard.** Detects the repository's forge (GitHub, GitLab, Bitbucket Cloud, Azure DevOps), configures the provider, and stores the access token encrypted. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/scm-multiforge.md)
150
181
  * `--linter-setup`: **Interactive external linter wizard.** Guides you through installing and configuring external linters (ESLint, PHPCS, Stylelint) as a Checkstyle XML bridge. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/linter-regras-customizadas.md)
151
182
  * `--mcp`: Starts GitPR as an **MCP server** (Model Context Protocol) on stdio transport. Enables integration with VS Code, Cursor, Claude Desktop, and other MCP-compatible editors — exposing all GitPR AI capabilities as 14 annotated tools, 18 resources, and 7 pre-built prompts directly inside your IDE. Also available as the standalone `gitpr-mcp` command.
152
183
  * `--plugins`: Lists all **globally installed plugins** — custom linter packs from `~/.gitpr/plugins/linter/` and MCP prompt templates from `~/.gitpr/plugins/prompts/`. These plugins apply across all your projects without duplication. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/plugins-system.md)
@@ -157,7 +188,7 @@ You can pass the following *flags* for specific actions:
157
188
  * **New Code Issue (`gitpr -is`):** Reads the current `git diff`. **Why use:** Ideal for quickly documenting the task you just finished programming, before committing.
158
189
  * **Epic/Release Issue (`gitpr -is -ht`):** Reads the full history of the current branch (Git Log + PR Cache). **Why use:** Ideal for generating consolidated documentation of an entire release or a large *feature* that took several days/commits to complete.
159
190
  * **Archaeological/Technical Debt Issue (`gitpr -is -b file:lines`):** Reads the timeline of a specific business rule. **Why use:** Ideal for documenting technical debt, explaining how a legacy code block evolved and why it needs to be refactored.
160
- * **PR Publisher (default):** Running `gitpr` generates the PR description with AI, saves the `.md` file to `.gitpr/reports/pr_desc/`, and opens an interactive terminal interface (TUI) to review, edit, and publish the Pull Request directly to GitHub via REST API. Before generation, it checks for unstaged files and offers a modal to manage them. Use `--no-publish` to save only the PR file locally without opening the publisher, or `--no-edit` to auto-commit pending changes (with lint validation), auto-push, and publish immediately — handling existing PR updates, optional auto-merge, and clear error feedback when merge conflicts occur. Use `--base <branch>` to change the target branch. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/pull-request-publication.md)
191
+ * **PR Publisher (default):** Running `gitpr` generates the PR description with AI, saves the `.md` file to `.gitpr/reports/pr_desc/`, and opens an interactive terminal interface (TUI) to review, edit, and publish the Pull Request directly to GitHub, GitLab, Bitbucket, or Azure DevOps via REST API. Before generation, it checks for unstaged files, calculates suggested reviewers, and offers a modal to manage them. Use `--no-publish` to save only the PR file locally without opening the publisher, or `--no-edit` to auto-commit pending changes (with lint validation), auto-push, and publish immediately — handling existing PR updates, optional auto-merge, and clear error feedback when merge conflicts occur. Use `--base <branch>` to change the target branch. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/pull-request-publication.md)
161
192
  * `-h` or `--help`: Shows the general help with all options. Use together with another flag for **contextual help** (e.g.: `gitpr -h --issue`, `gitpr -h --linter`) with a direct link to the detailed documentation of each feature.
162
193
  * `-u` or `--update`: Checks the latest version of GitPR on PyPI and shows how to update it (Auto-Updater).
163
194
 
@@ -171,7 +202,7 @@ When your diff is too large for a single AI call (over ~90k estimated tokens), G
171
202
 
172
203
  📚 Full documentation: [docs/map-reduce-diff.md](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/map-reduce-diff.md)
173
204
 
174
- ## 🛡️ Local Linter (Static Analysis)
205
+ ## 🛡️ Local Linter (Static Analysis & Security)
175
206
 
176
207
  GitPR CLI allows you to define strict rules that will be validated instantly during `--review` or `--fullreview`, without depending on AI. This is ideal for preventing common errors (like `console.log` or test IPs) from reaching the repository.
177
208
 
@@ -192,9 +223,9 @@ rules:
192
223
 
193
224
  The Linter analyzes only the **added lines** in your `git diff`, ensuring a focused and extremely fast execution. If there are violations, they will appear highlighted at the top of your review file.
194
225
 
195
- ### External Linters (Checkstyle Bridge)
226
+ ### External Linters & SAST Bridges (Checkstyle Bridge)
196
227
 
197
- If your project already uses tools like ESLint, PHP_CodeSniffer or Stylelint, GitPR can act as a bridge — running them in the background and filtering errors **only for the lines you changed** in your current diff. Any linter that emits reports in the `checkstyle` format is supported.
228
+ If your project already uses tools like ESLint, PHP_CodeSniffer, Stylelint, Semgrep, Gitleaks, or Bandit, GitPR can act as a bridge — running them in the background and filtering errors **only for the lines you changed** in your current diff. Any linter or SAST scanner that emits reports in the `checkstyle` format is supported. In addition, GitPR includes embedded secret scanning rules to catch leaked API keys, tokens, and credentials before they leave your machine.
198
229
 
199
230
  Instead of configuring the YAML manually, use the interactive wizard:
200
231
 
@@ -204,7 +235,7 @@ gitpr --linter-setup
204
235
 
205
236
  The wizard shows pre-configured presets (PHPCS, ESLint, Stylelint — controlled remotely via `templates/gitpr.linter-presets.json`), guides you through the native installation command (e.g.: `npm install --save-dev eslint`), and injects the correct `external_linters` block into your `.gitpr.linter.yml`.
206
237
 
207
- Every run — manual via `--linter` or automatic before commits — consolidates the Regex Rules and External Linters into a single Markdown report saved at `.gitpr/reports/linter/` (customizable via `OUTPUT_FILE_NAME_LINTER`). The report is generated only when violations are found — clean runs create no files.
238
+ Every run — manual via `--linter` or automatic before commits — consolidates the Regex Rules, Embedded Security Rules, and External Linters into a single Markdown report saved at `.gitpr/reports/linter/` (customizable via `OUTPUT_FILE_NAME_LINTER`). The report is generated only when violations are found — clean runs create no files.
208
239
 
209
240
  ## 🤝 Co-Author Signature
210
241
 
@@ -218,6 +249,22 @@ The trailer is appended programmatically (never by the AI) in all flows: console
218
249
 
219
250
  📖 **Full documentation:** [docs/commit-message-ia.md](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/commit-message-ia.md)
220
251
 
252
+ ## 🏷️ Pull Request Badge
253
+
254
+ Every pull request GitPR publishes carries a badge at the foot of the body, with what the local linter counted on that very diff:
255
+
256
+ ```text
257
+ GitPR | 0 errors · 2 warnings
258
+ ```
259
+
260
+ It is built from a real measurement, never from a claim: red when a blocking finding is there, yellow for advice, green when the rules found nothing — and **no badge at all when no linter rules are configured**, because a green badge over a diff nobody checked would say something GitPR cannot back. It is on by default; `GITPR_BADGE=false` turns it off, and `gitpr --skill` is what gives it rules to count.
261
+
262
+ For your own README there is a static badge. `gitpr badge --readme` prints the line and writes nothing — pasting it is your call:
263
+
264
+ [![GitPR](https://img.shields.io/badge/GitPR-quality--checked-blue)](https://gitpr.natanfiuza.dev.br/)
265
+
266
+ 📖 **Full documentation:** [docs/badge.md](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/badge.md)
267
+
221
268
  ## 🧠 Multi-Model Architecture (AI-Agnostic)
222
269
 
223
270
  GitPR is not tied to a single Artificial Intelligence. During initial setup, the user can choose their default engine. We currently support:
@@ -430,6 +477,8 @@ If you want to implement GitPR as an automated quality barrier in your team, che
430
477
 
431
478
  ### Core Features
432
479
 
480
+ * [**Guided Tour (gitpr demo)**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/demo.md) — How the `gitpr demo` subcommand walks through the commit message, the code review and the pull request description over a recorded example, with no API key, no repository and no network.
481
+ * [**Pull Request Badge**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/badge.md) — What the badge GitPR attaches to published pull requests says, where it is attached, when it is left out, and how to print the static one for your own README.
433
482
  * [**Pull Request (Default Mode)**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/pr-descricao-padrao.md) — Complete flow for generating PR descriptions without flags.
434
483
  * [**Pull Request Publisher TUI**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/pull-request-publication.md) — How to review and publish Pull Requests directly to GitHub from the terminal.
435
484
  * [**AI Code Review**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/code-review-ia.md) — Guide to review modes (`--review`, `--fullreview`), file auditing (`--input`) and remote pull request review (`gitpr review-pr`).
@@ -438,11 +487,16 @@ If you want to implement GitPR as an automated quality barrier in your team, che
438
487
  * [**Issue Generation and TUI Interface**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/issue-tui-help.md) — How to use the terminal graphical interface (TUI) and the 3 context engines to manage structured Issues.
439
488
  * [**Code Archaeologist (Git Blame)**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/blame-arqueologo.md) — How to trace the origin of business rules with `git blame` and AI.
440
489
  * [**Skills and Templates System**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/skill-template.md) — How to customize AI behavior with `.gitpr.*.md` files.
441
- * [**Release Notes & Changelog (gitpr release)**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/release-notes.md) — How the `gitpr release` subcommand generates the changelog / release notes of a repository, suggests the next semantic version and publishes releases on the forge.
490
+ * [**Release Notes & Changelog (gitpr release)**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/release-notes.md) — How the `gitpr release` subcommand generates the changelog / release notes of a repository from the delta of the previous version, links every commit, pull request and contributor, suggests the next semantic version and publishes releases on the forge.
442
491
  * [**Fix Command (gitpr fix)**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/fix-command.md) — How the `gitpr fix` subcommand turns the findings of the last review into patches you read before they touch your tree, classifies each one by safety, and undoes an applied patch on demand.
492
+ * [**Split Command (gitpr split)**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/split-command.md) — How the `gitpr split` subcommand reads a working tree holding several concerns, groups the hunks by intent with AI, and turns them into ordered atomic commits without ever writing to your files.
443
493
 
444
494
  ### Configuration & Infrastructure
445
495
 
496
+ * [**Interactive Configuration TUI (gitpr config)**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/config-tui.md) — Master-detail terminal interface to view, edit, and validate settings in `~/.gitpr/.env`.
497
+ * [**Multi-Forge SCM Integration**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/scm-multiforge.md) — Unified SCM provider layer for GitHub, GitLab, Bitbucket Cloud, and Azure DevOps with `--init` wizard.
498
+ * [**Suggested Reviewers**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/suggested-reviewers.md) — Automatic reviewer suggestion via Git blame of added lines mapped to forge logins.
499
+ * [**Usage Telemetry & Audit Log**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/usage-log.md) — Local invocation logging and audit history for tracking CLI executions.
446
500
  * [**Install Wizard**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/install-wizard.md) — Step-by-step guided setup for configuring GitPR in a new project.
447
501
  * [**AI Providers**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/providers-ia.md) — Configuration and selection between Google Gemini, DeepSeek, and Ollama.
448
502
  * [**Auto-Updater**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/auto-update.md) — How GitPR's automatic update and the mandatory update block work.
@@ -0,0 +1,108 @@
1
+ gitpr_cli-1.3.0.dist-info/licenses/LICENSE,sha256=UQzLC6mwqb-ZNDkBBAqOxC-SMQcEID0gaM6oFcu6u48,27030
2
+ src/__init__.py,sha256=WZW9x8P5CYnru29UiarFa8q1nQzuwYWyIodSOlruCWI,29
3
+ src/ai_providers.py,sha256=QyFEOsXyA59lZziO5bl-4nlYFJpxBcxU9aLhcTBnkWE,14633
4
+ src/blame_engine.py,sha256=Zov1hxrt7Dcmf56ovJKz3QmHIfzDKtSub2iItXeg9-A,17104
5
+ src/cache.py,sha256=GC2ZT0ThAtH22aRRZapIeauATv_nqBfiRsXjqeMhAiI,6368
6
+ src/changelog_builder.py,sha256=8GicvIhREWo3B8XhQugVi2_OWzhN_ik_2GxpxmiJVTg,9650
7
+ src/chat_memory.py,sha256=PcohnyspAfIwwwbsp9U2Wk_1AylJMFEMoWqRSJe_w30,8949
8
+ src/commit_classifier.py,sha256=8_LHQEzisWflV5ZsK31P3fwyu2QWacv8cdFErXYe030,4668
9
+ src/config.py,sha256=sY9qd-aNB00Z_QNefi_jguAl8GoZgi7XBv0S1n1mufM,40323
10
+ src/config_schema.py,sha256=pegWMWQ0ALZvipf90f_dGSsnjDfu8xxlzw6v_Mt71zM,38783
11
+ src/core.py,sha256=mmmFwcEGCVp3ZyrkqYn3v9HbYTAiD7CVLndpKKVGibY,79844
12
+ src/diff_parser.py,sha256=62X69ves4tlzNgVKXNsfWdOMwqn2h16RkUOPqszMU7k,11252
13
+ src/doc_links.py,sha256=DuovU-RF9kjbisBdEoGAsuavQ2y0MYDhqy6Sz6vbrGI,1157
14
+ src/github_api.py,sha256=ILm7e03UP80swc3XuJvIJJvhLp81-V_3kQQ1EuAcN8Q,3836
15
+ src/i18n.py,sha256=PdUmtZlH1zogbiDc3j6xiwtnvUd7XiNbiS-xKjYK4WM,4844
16
+ src/issue_engine.py,sha256=XwW9C5uX84eOWWZMerm0DvE8LpLRcjhrLsXtt7yftak,10872
17
+ src/linter_engine.py,sha256=QZ-mJz5amh3rafsKSxjTL4NyelJKLQo5IERz1k1SJZE,17280
18
+ src/linter_wizard.py,sha256=7J_nQFghVwCaKG6QI3K45KcOaxHSH1oTCAvP_6tnCQM,7424
19
+ src/main.py,sha256=ivcSlV3sGqUf7-P3pGpPW_bDuSYVZXLko6QlEwNUHME,120679
20
+ src/mcp_server.py,sha256=jHKUjhO225WbZVv8xkQXAXyS9ckP34cjTMwbrxJLNfE,91147
21
+ src/metrics.py,sha256=VC68t8xGUBFry-ANvEb_TjaRkboPi37__GxK0eHZOe4,21250
22
+ src/net.py,sha256=f0MOkXyIxi4HVetsybtWpV_yHjiC17T2JMWySBdoV2I,2774
23
+ src/release_engine.py,sha256=S9qMg3l4Vjx9gBDSX6cwaacYF-tDcQbj7Rj_7AMgkQk,34629
24
+ src/reviewer_resolution.py,sha256=lZ3csSJHN6Y5TJ3U3p9uuXikOgG7fH4qqx7X9-4F7pc,5754
25
+ src/reviewer_suggestion.py,sha256=jk9FAyp7WkkJbi-yepz7LDF1_tSHxJEEjcajMKF5HdY,8501
26
+ src/security.py,sha256=fkWy-r4tdISH0xOyWZyR8ZCFyLuYqu8fn9sKsIeNAso,1351
27
+ src/security_ruleset.py,sha256=L754S4RlVoZGWm0nuCoeli9-49mKydZpwi31V319DQ0,4337
28
+ src/spinner.py,sha256=63pEotiDa3TVwBr-sn0MJoDPQ_uWJTTwINXieJIDDKc,8964
29
+ src/suggest_reviewers.py,sha256=-wRe0HYksx4FWQZ-eDvU9Ghdk5gqtEt457KQP7sE31Y,5181
30
+ src/tui_issue.py,sha256=WEFxT4kzWQ_KbSDsOnS_iuYXasZySB-68jryGNm0peg,10331
31
+ src/updater.py,sha256=MZgHnlmKss0Vft-c0ys-fzt5ninPGnZ8F3Wq8QvvsTY,4771
32
+ src/usage_log.py,sha256=314sTT5Gl_uOXqWSIi-V42bnTHuMSYAZ6G6-_15uEGQ,5968
33
+ src/version_bump.py,sha256=OjqJe9OL1-1dT5od7-SgU5ZN_dTtR_aKX_oSjSY64YQ,2954
34
+ src/application/__init__.py,sha256=0GADzpCB3CdCzw-d3az0DVOtTvXfCppt-0hY-Js5a1U,29
35
+ src/application/use_cases/__init__.py,sha256=7rwBoDbGxnsjVe32j7yj9yCA9g2E-DWGeokOfDbl_O0,284
36
+ src/application/use_cases/generate_pr_explanation.py,sha256=5BYUPgGBdR29P-9zt75IcKO4KOwGhZnapws7Y6A4574,3202
37
+ src/application/use_cases/generate_test_file.py,sha256=WlHnhwCWHe-XBrL9m1xvJOkn1wqr8vnsE3s3x9wgUi0,8615
38
+ src/branding/__init__.py,sha256=6ORCsJHtd422jUiQNMCf6eL8kwW4QMf6dJ9Z4Gf9bEc,80
39
+ src/branding/badge_builder.py,sha256=ZO-rXMPoSPYodvILfY2wHufYs0ZQwgFxSs0SkHd6m4A,4899
40
+ src/branding/badge_data.py,sha256=eamUtEKeg3NwF9MCipb5EXFlwL8vgJnR6yUw2TrFlEw,1925
41
+ src/demo/__init__.py,sha256=lF-kdDOqylPXRiuSmT3XwqdghMlRIiOuzpNYs7ElGiE,370
42
+ src/demo/demo_runner.py,sha256=lqnJZB27GSJan7bwV3Gf1wnCBYn7Wws_DEOFdB5PADc,10721
43
+ src/demo/fake_ai_provider.py,sha256=9slMebDqKvHv6wmhms_YhEqPhQJBlxYwAiCzehpkR3A,4038
44
+ src/demo/scenarios/__init__.py,sha256=t8PRSdgNM9f0tb53lBSkehiQ_liI-azts7BdhMdUZrs,4041
45
+ src/demo/scenarios/laravel_bug_fix.py,sha256=lU3XoKaGvcDIlMmzZFeWvEI6tGjgz5bWGflBbUNvgHU,20461
46
+ src/demo/scenarios/security_issue.py,sha256=sI8YykXcYOkpgHygAOX6pYLHwSkHZoE5OjrYsRqOCXQ,23821
47
+ src/domain/linter/__init__.py,sha256=WoLci3LhRc7S2IlytrveRPbEQVrAwI4bqazVmGP08Ak,236
48
+ src/domain/linter/sast_finding_mapper.py,sha256=GaeRqyD9NOL_crJLV9qAbY6hrUknXZakLXBB3T_5FsU,3105
49
+ src/domain/pr/__init__.py,sha256=O9C2dQpbMnbF3mOXTMUvHPmIAF0G-QxhDlVXjB_wShs,274
50
+ src/domain/pr/explain_section_builder.py,sha256=RMPErD_X8nLhbfCwxHCBmI3AFLilO_eNPpEDZE6pyns,3799
51
+ src/domain/tests_generation/__init__.py,sha256=OcHvtEilwu_8z_PV_prq2x_GGBGxmlsCW2XM1xU8dl0,251
52
+ src/domain/tests_generation/framework_detector.py,sha256=DyJZcttvpRkZMEx8a9X3ksveJ-PNub49XXNzqERQSU4,4439
53
+ src/domain/tests_generation/test_content_types.py,sha256=jnPywEMAGtm340e8V1kuHbx9Nxa9nhC7mKnubduxEZM,1019
54
+ src/domain/tests_generation/test_scaffold_builder.py,sha256=NkTbYswWiuWikLfz6nRi34zU8e7HvzMzkbj3q2W__Ws,3800
55
+ src/fix/__init__.py,sha256=haZFtazOc4v9SmtqBtyPNUvYrEQXNaChTIZiOpZlNSc,937
56
+ src/fix/apply_fix.py,sha256=yMinMW50Fw62V-k7hRwN1lsikCJbz6xV5iX4IH0sdxY,17912
57
+ src/fix/fix_history.py,sha256=pHXHpt-SstitbLDmlPloDVKlH79FUD8hqKjVK5xKLMA,5626
58
+ src/fix/patch_applier.py,sha256=3tIXwp8yPwelngXvhmQhvCYVpEOPfvCUwrnG_B9Yf0c,1235
59
+ src/fix/patch_extractor.py,sha256=gEiwSvxGNHM74TnubTN22NBpbXDwvNpB2vMqgpF7hQQ,2514
60
+ src/fix/patch_provenance.py,sha256=sF934ntcZjJCTzzhtuR2Pi-vMUdXFElKcuw5riwPnUk,3195
61
+ src/fix/patch_safety_classifier.py,sha256=StZvh_0Y-ZCdf_LwFoVAdfhpi0b7v1TOxTRoG86_63M,4121
62
+ src/fix/rollback_fix.py,sha256=rMBYewuvlhJOFdo-Ke_SPfu1O-yd6NJzEYiO5o-LcKk,2501
63
+ src/infrastructure/__init__.py,sha256=zTIcY3cscsdkXogSGXoJbH0mVGFbKYTRK32fg26wr9U,81
64
+ src/infrastructure/git/__init__.py,sha256=j9zQUXKNp5NcOzTK-qHGNh75YkufGlw5rgH1wgRknX4,87
65
+ src/infrastructure/git/patch_applier.py,sha256=RgbrpAp-6x401Eydz6yLMTOXKn5E9LQzV9ZB_n7E90M,7988
66
+ src/infrastructure/git/selective_stager.py,sha256=sMRPZmq2EWipcabCCETeMu_0in66BFSzQAqZ7h1y2hc,8038
67
+ src/infrastructure/linter/external/__init__.py,sha256=2OvWKwC2AAtuMZ7KwZuqywwt181WoRcUuyzSCtTQLtM,277
68
+ src/infrastructure/linter/external/bandit_bridge.py,sha256=8poP9g0Fts0F7EpervYqfGvJpadreBGCJBOvVUghFhI,4290
69
+ src/infrastructure/linter/external/base_bridge.py,sha256=n9StBko-Cu86u0EzCooS_OKNCDT2cGwxSTBubxvgt4U,3214
70
+ src/infrastructure/linter/external/gitleaks_bridge.py,sha256=OMpARs53bSJuhA429mbAbRxUyvbmJ7ovha9wFjd6JY4,5841
71
+ src/infrastructure/linter/external/semgrep_bridge.py,sha256=N-mKeiBF0uQz4XrH6bR-sCwesgoiiFfiwnZtM7ndlSc,4464
72
+ src/infrastructure/scm/__init__.py,sha256=kNiYljRCJRwi5WoPckn8NbZt6qbEQZFMwaBY3-kepRo,1825
73
+ src/infrastructure/scm/azure_devops_provider.py,sha256=ezRS_O2450mfYEfqFOD4UBSVFhT3aJdx3DEVkZcJjac,14717
74
+ src/infrastructure/scm/base.py,sha256=L-7NYG2hqIWKzKCsQ5NurtgJJvvYvjTVOs0NJ2Aqx_M,9536
75
+ src/infrastructure/scm/bitbucket_provider.py,sha256=jv62MlOd0Iiu3VYAHUffJO2D95TE4k2lmukQL93bOd8,12105
76
+ src/infrastructure/scm/factory.py,sha256=Qeearq8eWnF_4boI68rVBkoGEiS-xRGD4V-O_FF0204,3856
77
+ src/infrastructure/scm/github_provider.py,sha256=9RXRAocYEVWS6NR2PsOg7wtqiF8989x1BxAqgBSoLOI,16438
78
+ src/infrastructure/scm/gitlab_provider.py,sha256=mznhtBhXtc4zRut_Jb0pM57RIXViNYYiKiM7VKDFhl8,13881
79
+ src/infrastructure/scm/web_links.py,sha256=9DHFCwYc6TVzO3INe0rvEicgoD2-kxQSrgq3vt_p5Ow,3621
80
+ src/review/__init__.py,sha256=XVyQZFf6UAqIKi8Gb1pQ4dKjcrC50soUvNuKu1DBspc,687
81
+ src/review/diff_normalizer.py,sha256=PRXQ7b81EBH1nx_8_Tm9G7Kjdax_6hIVtFgEixicT0E,3551
82
+ src/review/diff_source.py,sha256=O8bIlff0Zr7wPMzCKAVk_LF2pMDr3sc9KAuyA3OvHG0,2664
83
+ src/review/remote_pr.py,sha256=maPCQO5p7u9-_PA0FEBNgimfA6na2BdQGBPmqNj1_0c,11817
84
+ src/review/render.py,sha256=Hnkz0PvtYIaaikTS1inTzrneAJL4Xqjd-F8Lr6Yh6mQ,2691
85
+ src/split/__init__.py,sha256=JuXxPxoG1NrWtLAQARSBh59tsNXvrUoS-T-vHcHeDPM,912
86
+ src/split/apply_split_plan.py,sha256=-ESEEl973Ai9czEOVfS697Dy196QX8EKbVrNZlgrQhY,10678
87
+ src/split/generate_split_plan.py,sha256=I370yLCxqeZRemIIcLswYVI07fet5JJMRxxht21HlOA,8786
88
+ src/split/hunk_grouper.py,sha256=rNH64CJQ80ue6YjTTaVxwtQug1tY6yCjEEScjj8wL2o,14249
89
+ src/split/hunk_parser.py,sha256=Lvmpd8qPYT2a0pw-XCSvUJqm_b9P5tAw9Kj_DlbBWTk,10010
90
+ src/split/split_plan.py,sha256=rim_iUlyQO8EpqXjN-YzRFv5mbo2pwZlXdrkuZ7PGxM,6419
91
+ src/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
92
+ src/ui/chat_app.py,sha256=O21d4-_6u87pHeFKtBJbTpQxkBQWmaNa617CO0f4-0Q,26777
93
+ src/ui/config_app.py,sha256=xgppZbEBFrFijgLGo1b1PN1vTGCg8xpI8gpMYBv2KzU,73417
94
+ src/ui/help_screen.py,sha256=EvbP8kFZ6Sdehi7bJ6Wo_YcTTk5MTkgneeidWwefYrA,1890
95
+ src/ui/issue_app.py,sha256=eINMmamloeopbtwyS1rCPwosaHeZW5oLebnEV4TfamY,9281
96
+ src/ui/linter_app.py,sha256=Y5vwU7D5PQIR_HaY6t40jZy7Guc8z2w-auyosCQqoro,1566
97
+ src/ui/metrics_app.py,sha256=a3jsB_kjMusSL57A6LpJyb9LFN7JJWlOdUNClklwq-c,16016
98
+ src/ui/pr_publish_app.py,sha256=cZ9xr1TSZF6fjvQPlAdSjzSNykufHF1l7hsmo3Ib2j0,67311
99
+ src/ui/pr_publish_help.py,sha256=fBiUtD97ngxbSfAvPVGGXU19r5kbALCKyi4QeKqyQ2c,1891
100
+ src/ui/demo/__init__.py,sha256=PGXenIwpS1lVDE164CZ55q8_65XtJq5H300NIpnqOh0,58
101
+ src/ui/demo/demo_app.py,sha256=EDeuqxh_v9KrPhib49TyirmFlHZN7I5NQkg9AjDGhcs,3074
102
+ src/ui/demo/demo_help_screen.py,sha256=m8SuvhzGmkMsahUBA6Aq7ull4W65nS0KOX4hLx-GGps,2056
103
+ src/ui/demo/demo_screens.py,sha256=3Ntp-A7OB43yEtniRqV1UKWc-p5lHRyxeadpe8bxJpM,1937
104
+ gitpr_cli-1.3.0.dist-info/METADATA,sha256=uqEGURIaBMXUiX9a0FFUWuqBTNT_argqEcxvTTUAj_M,42671
105
+ gitpr_cli-1.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
106
+ gitpr_cli-1.3.0.dist-info/entry_points.txt,sha256=TstGnMUzdOI5qNmpM97EPDBPuMqOZxoo6EoLxHSOMSQ,71
107
+ gitpr_cli-1.3.0.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
108
+ gitpr_cli-1.3.0.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ # Application layer markers
2
+
@@ -0,0 +1,8 @@
1
+ from src.application.use_cases.generate_test_file import generate_test_file, validate_test_syntax
2
+ from src.application.use_cases.generate_pr_explanation import generate_pr_explanation
3
+
4
+ __all__ = [
5
+ "generate_test_file",
6
+ "validate_test_syntax",
7
+ "generate_pr_explanation",
8
+ ]
@@ -0,0 +1,73 @@
1
+ import json
2
+ from src.ai_providers import call_ai_model
3
+ from src.config import get_ai_provider, get_api_key, get_api_model
4
+ from src.core import get_skill_context
5
+ from src.domain.pr.explain_section_builder import PrExplanation, parse_explain_payload
6
+
7
+
8
+ def generate_pr_explanation(
9
+ diff_content: str,
10
+ ai_provider: str | None = None,
11
+ skill_context: str | None = None,
12
+ quiet: bool = False,
13
+ ) -> PrExplanation:
14
+ """Generates a structured reviewer explanation for the current PR diff."""
15
+ if not diff_content or not diff_content.strip():
16
+ return PrExplanation(
17
+ what_changes="",
18
+ why_it_changes="",
19
+ reviewer_focus_points=[],
20
+ regression_risk="",
21
+ has_sufficient_evidence=False,
22
+ markdown="",
23
+ )
24
+
25
+ provider = ai_provider or get_ai_provider()
26
+ api_key = get_api_key(provider)
27
+ if not api_key:
28
+ return PrExplanation(
29
+ what_changes="No API key configured.",
30
+ why_it_changes="No API key configured.",
31
+ reviewer_focus_points=[],
32
+ regression_risk="No API key configured.",
33
+ has_sufficient_evidence=False,
34
+ markdown="⚠️ No API key configured for AI provider.",
35
+ )
36
+
37
+ api_model = get_api_model(provider, task_complexity="advanced")
38
+
39
+ # Load skill context
40
+ skill_instruction = skill_context or get_skill_context("explain", quiet=True)
41
+ if not skill_instruction:
42
+ skill_instruction = (
43
+ "You are an expert Code Reviewer and Software Architect. "
44
+ "Explain this pull request diff from the reviewer's perspective: What changes, Why it changes, Where to focus review, and Regression risk. "
45
+ "You MUST ONLY return a valid JSON object matching this schema:\n"
46
+ '{"what_changes": "2-4 plain language sentences", "why_it_changes": "motivation or [FILL: question]", '
47
+ '"reviewer_focus_points": [{"description": "...", "file_path": "...", "related_line": 0}], '
48
+ '"regression_risk": "short assessment of potential regressions"}'
49
+ )
50
+
51
+ user_prompt = (
52
+ "Generate a reviewer guide for the following diff changes:\n\n"
53
+ f"Diff:\n```diff\n{diff_content}\n```\n\n"
54
+ "Instructions:\n"
55
+ "1. In 'what_changes', provide 2-4 plain sentences describing the functional changes without excessive jargon.\n"
56
+ "2. In 'why_it_changes', explain the inferred motivation. If motivation cannot be determined with certainty, use '[FILL: What is the primary business motivation for this change?]'.\n"
57
+ "3. In 'reviewer_focus_points', list 2-5 concrete points requiring specific review attention.\n"
58
+ "4. In 'regression_risk', state any objective risks (critical files, missing tests, auth changes).\n"
59
+ "5. Return strictly JSON with 'what_changes', 'why_it_changes', 'reviewer_focus_points', 'regression_risk'."
60
+ )
61
+
62
+ ai_raw = call_ai_model(
63
+ provider=provider,
64
+ api_key=api_key,
65
+ api_model=api_model,
66
+ prompt=user_prompt,
67
+ system_instruction=skill_instruction,
68
+ quiet=quiet,
69
+ action="explain",
70
+ )
71
+
72
+ return parse_explain_payload(ai_raw or "")
73
+
@@ -0,0 +1,227 @@
1
+ import json
2
+ import os
3
+ import shutil
4
+ import subprocess
5
+ import tempfile
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from src.ai_providers import call_ai_model
10
+ from src.config import get_ai_provider, get_api_key, get_api_model
11
+ from src.core import get_skill_context, resolve_skill_path
12
+ from src.domain.tests_generation.framework_detector import detect_test_framework
13
+ from src.domain.tests_generation.test_content_types import (
14
+ GeneratedTest,
15
+ TestFramework,
16
+ TestGenerationTarget,
17
+ TestScaffold,
18
+ )
19
+ from src.domain.tests_generation.test_scaffold_builder import build_test_scaffold
20
+ from src.i18n import __
21
+
22
+
23
+ def validate_test_syntax(content: str, framework: TestFramework) -> tuple[bool, str | None]:
24
+ """Validates the syntax of the generated test file using local tools if present.
25
+
26
+ Returns:
27
+ tuple[bool, str | None]: (is_valid, error_message)
28
+ """
29
+ if not content or not content.strip():
30
+ return False, "Generated test content is empty."
31
+
32
+ if framework in (TestFramework.PEST, TestFramework.PHPUNIT):
33
+ php_bin = shutil.which("php")
34
+ if php_bin:
35
+ try:
36
+ with tempfile.NamedTemporaryFile("w", suffix=".php", delete=False, encoding="utf-8") as tf:
37
+ tf.write(content)
38
+ temp_name = tf.name
39
+
40
+ try:
41
+ res = subprocess.run(
42
+ [php_bin, "-l", temp_name],
43
+ capture_output=True,
44
+ text=True,
45
+ encoding="utf-8",
46
+ errors="replace",
47
+ )
48
+ if res.returncode != 0:
49
+ return False, res.stderr.strip() or res.stdout.strip()
50
+ finally:
51
+ if os.path.exists(temp_name):
52
+ os.unlink(temp_name)
53
+ except Exception as e:
54
+ return True, f"Syntax validation skipped: {e}"
55
+
56
+ elif framework in (TestFramework.JEST, TestFramework.VITEST):
57
+ node_bin = shutil.which("node")
58
+ if node_bin:
59
+ try:
60
+ with tempfile.NamedTemporaryFile("w", suffix=".js", delete=False, encoding="utf-8") as tf:
61
+ tf.write(content)
62
+ temp_name = tf.name
63
+ try:
64
+ res = subprocess.run(
65
+ [node_bin, "--check", temp_name],
66
+ capture_output=True,
67
+ text=True,
68
+ encoding="utf-8",
69
+ errors="replace",
70
+ )
71
+ if res.returncode != 0:
72
+ return False, res.stderr.strip() or res.stdout.strip()
73
+ finally:
74
+ if os.path.exists(temp_name):
75
+ os.unlink(temp_name)
76
+ except Exception as e:
77
+ return True, f"Syntax validation skipped: {e}"
78
+
79
+ elif framework == TestFramework.PYTEST:
80
+ python_bin = shutil.which("python") or shutil.which("python3")
81
+ if python_bin:
82
+ try:
83
+ with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False, encoding="utf-8") as tf:
84
+ tf.write(content)
85
+ temp_name = tf.name
86
+ try:
87
+ res = subprocess.run(
88
+ [python_bin, "-m", "py_compile", temp_name],
89
+ capture_output=True,
90
+ text=True,
91
+ encoding="utf-8",
92
+ errors="replace",
93
+ )
94
+ if res.returncode != 0:
95
+ return False, res.stderr.strip() or res.stdout.strip()
96
+ finally:
97
+ if os.path.exists(temp_name):
98
+ os.unlink(temp_name)
99
+ except Exception as e:
100
+ return True, f"Syntax validation skipped: {e}"
101
+
102
+ return True, None
103
+
104
+
105
+ def generate_test_file(
106
+ target: TestGenerationTarget,
107
+ repo_path: str = ".",
108
+ ai_provider: str | None = None,
109
+ framework_override: TestFramework | str | None = None,
110
+ apply: bool = False,
111
+ quiet: bool = False,
112
+ ) -> GeneratedTest:
113
+ """Orchestrates test generation from a target diff/file/finding."""
114
+ warnings: list[str] = []
115
+
116
+ # 1. Detect Framework
117
+ detected_fw, fw_warnings = detect_test_framework(repo_path, framework_override)
118
+ warnings.extend(fw_warnings)
119
+
120
+ # 2. Build Scaffold
121
+ scaffold = build_test_scaffold(target.file_path, detected_fw, repo_path)
122
+
123
+ # 3. Setup AI provider & system instruction
124
+ provider = ai_provider or get_ai_provider()
125
+ api_key = get_api_key(provider)
126
+ if not api_key:
127
+ warnings.append(f"No API key configured for {provider}. Generate one with --install.")
128
+ return GeneratedTest(
129
+ scaffold=scaffold,
130
+ content="",
131
+ covered_scenarios=[],
132
+ warnings=warnings,
133
+ )
134
+
135
+ api_model = get_api_model(provider, task_complexity="advanced")
136
+
137
+ # Load skill context or use built-in instruction
138
+ skill_content = get_skill_context("tests", quiet=True)
139
+ if not skill_content:
140
+ skill_content = (
141
+ "You are an expert QA and Test Automation Engineer. "
142
+ "Generate a complete, executable, clean test file adhering strictly to the requested test framework and conventions. "
143
+ "You MUST ONLY return a valid JSON object matching this schema:\n"
144
+ '{"content": "complete test file source code as a string", "covered_scenarios": ["scenario 1", "scenario 2"], "warnings": []}'
145
+ )
146
+
147
+ # 4. Construct Prompt
148
+ target_info = f"Target Type: {target.source_type}\n"
149
+ if target.file_path:
150
+ target_info += f"Target File: {target.file_path}\n"
151
+ if target.finding_id:
152
+ target_info += f"Target Finding ID: {target.finding_id}\n"
153
+
154
+ user_prompt = (
155
+ f"Generate a comprehensive test file for the following changes:\n\n"
156
+ f"Framework: {detected_fw.value}\n"
157
+ f"Target Test Path: {scaffold.target_test_path}\n"
158
+ f"{target_info}\n"
159
+ f"Diff Context:\n```diff\n{target.diff_content}\n```\n\n"
160
+ "Requirements:\n"
161
+ "1. Write complete, ready-to-run test code (imports, test cases, assertions).\n"
162
+ "2. Include Happy Path scenarios covering main changes.\n"
163
+ "3. Include Edge Cases and Regression scenarios where applicable.\n"
164
+ "4. Follow the idiomatic conventions of the detected framework.\n"
165
+ "5. Return strictly JSON with 'content', 'covered_scenarios', and 'warnings'."
166
+ )
167
+
168
+ # 5. Call AI
169
+ ai_raw = call_ai_model(
170
+ provider=provider,
171
+ api_key=api_key,
172
+ api_model=api_model,
173
+ prompt=user_prompt,
174
+ system_instruction=skill_content,
175
+ quiet=quiet,
176
+ action="tests",
177
+ )
178
+
179
+ if not ai_raw:
180
+ warnings.append("AI model did not return a response.")
181
+ return GeneratedTest(
182
+ scaffold=scaffold,
183
+ content="",
184
+ covered_scenarios=[],
185
+ warnings=warnings,
186
+ )
187
+
188
+ # 6. Parse AI JSON
189
+ test_code = ""
190
+ covered_scenarios = []
191
+ try:
192
+ data = json.loads(ai_raw)
193
+ test_code = data.get("content", "")
194
+ covered_scenarios = data.get("covered_scenarios", [])
195
+ if "warnings" in data and isinstance(data["warnings"], list):
196
+ warnings.extend(data["warnings"])
197
+ except Exception:
198
+ # If response wasn't strict JSON, use clean raw text
199
+ test_code = ai_raw.strip()
200
+ if test_code.startswith("```"):
201
+ lines = test_code.split("\n")
202
+ if lines[0].startswith("```"):
203
+ lines = lines[1:]
204
+ if lines and lines[-1].startswith("```"):
205
+ lines = lines[:-1]
206
+ test_code = "\n".join(lines).strip()
207
+ covered_scenarios.append("Generated from raw model output")
208
+
209
+ # 7. Validate Syntax
210
+ is_valid, syn_err = validate_test_syntax(test_code, detected_fw)
211
+ if not is_valid:
212
+ warnings.append(f"Syntax validation failed ({syn_err}). Marked as EXPERIMENTAL / low confidence.")
213
+
214
+ # 8. Apply if requested
215
+ if apply and test_code:
216
+ full_dest_path = Path(repo_path) / scaffold.target_test_path
217
+ full_dest_path.parent.mkdir(parents=True, exist_ok=True)
218
+ with open(full_dest_path, "w", encoding="utf-8", errors="replace") as f:
219
+ f.write(test_code)
220
+
221
+ return GeneratedTest(
222
+ scaffold=scaffold,
223
+ content=test_code,
224
+ covered_scenarios=covered_scenarios,
225
+ warnings=warnings,
226
+ )
227
+
@@ -0,0 +1 @@
1
+ """Public badges: the automatic one on published PRs and the README snippet."""