gitpr-cli 1.6.0__tar.gz → 1.8.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 (254) hide show
  1. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/PKG-INFO +2 -2
  2. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/README.md +1 -1
  3. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/gitpr_cli.egg-info/PKG-INFO +2 -2
  4. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/gitpr_cli.egg-info/SOURCES.txt +3 -0
  5. gitpr_cli-1.8.0/src/ascii_chart.py +248 -0
  6. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/config.py +55 -0
  7. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/config_schema.py +176 -0
  8. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/core.py +201 -9
  9. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/scm/factory.py +9 -6
  10. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/issue_engine.py +6 -1
  11. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/review/remote_pr.py +7 -1
  12. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/scm_settings.py +12 -0
  13. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/ui/config_app.py +1 -0
  14. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/ui/metrics_app.py +118 -21
  15. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/updater.py +2 -2
  16. gitpr_cli-1.8.0/tests/test_ascii_chart.py +166 -0
  17. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_commit_app.py +6 -0
  18. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_config_schema.py +33 -0
  19. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_full_diff_encoding.py +24 -0
  20. gitpr_cli-1.8.0/tests/test_map_reduce_chunking.py +162 -0
  21. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_metrics.py +13 -0
  22. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_smart_excludes.py +1 -1
  23. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/LICENSE +0 -0
  24. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/gitpr_cli.egg-info/dependency_links.txt +0 -0
  25. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/gitpr_cli.egg-info/entry_points.txt +0 -0
  26. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/gitpr_cli.egg-info/requires.txt +0 -0
  27. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/gitpr_cli.egg-info/top_level.txt +0 -0
  28. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/pyproject.toml +0 -0
  29. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/setup.cfg +0 -0
  30. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/__init__.py +0 -0
  31. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/ai_providers.py +0 -0
  32. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/application/__init__.py +0 -0
  33. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/application/use_cases/__init__.py +0 -0
  34. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/application/use_cases/activate_policy_pack.py +0 -0
  35. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/application/use_cases/baseline_gate.py +0 -0
  36. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/application/use_cases/baseline_summary.py +0 -0
  37. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/application/use_cases/calculate_risk.py +0 -0
  38. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/application/use_cases/compare_against_baseline.py +0 -0
  39. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/application/use_cases/create_baseline.py +0 -0
  40. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/application/use_cases/generate_mentor_explanation.py +0 -0
  41. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/application/use_cases/generate_pr_explanation.py +0 -0
  42. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/application/use_cases/generate_test_file.py +0 -0
  43. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/application/use_cases/install_policy_pack.py +0 -0
  44. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/application/use_cases/publish_pull_request.py +0 -0
  45. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/application/use_cases/resolve_effective_policy.py +0 -0
  46. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/application/use_cases/run_check.py +0 -0
  47. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/application/use_cases/suppress_finding.py +0 -0
  48. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/application/use_cases/validate_policy_pack.py +0 -0
  49. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/blame_engine.py +0 -0
  50. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/branding/__init__.py +0 -0
  51. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/branding/badge_builder.py +0 -0
  52. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/branding/badge_data.py +0 -0
  53. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/cache.py +0 -0
  54. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/changelog_builder.py +0 -0
  55. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/chat_memory.py +0 -0
  56. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/commit_classifier.py +0 -0
  57. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/demo/__init__.py +0 -0
  58. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/demo/demo_runner.py +0 -0
  59. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/demo/fake_ai_provider.py +0 -0
  60. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/demo/scenarios/__init__.py +0 -0
  61. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/demo/scenarios/laravel_bug_fix.py +0 -0
  62. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/demo/scenarios/security_issue.py +0 -0
  63. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/diff_parser.py +0 -0
  64. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/doc_links.py +0 -0
  65. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/baseline/__init__.py +0 -0
  66. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/baseline/baseline_comparator.py +0 -0
  67. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/baseline/baseline_fingerprint.py +0 -0
  68. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/baseline/baseline_manifest.py +0 -0
  69. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/baseline/baseline_types.py +0 -0
  70. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/baseline/suppression_policy.py +0 -0
  71. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/check/__init__.py +0 -0
  72. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/check/check_config.py +0 -0
  73. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/check/check_output.py +0 -0
  74. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/check/check_types.py +0 -0
  75. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/finding/__init__.py +0 -0
  76. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/finding/finding_types.py +0 -0
  77. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/linter/__init__.py +0 -0
  78. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/linter/sast_finding_mapper.py +0 -0
  79. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/mentor/__init__.py +0 -0
  80. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/mentor/mentor_explanation_builder.py +0 -0
  81. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/policy/__init__.py +0 -0
  82. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/policy/policy_checksum.py +0 -0
  83. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/policy/policy_compatibility.py +0 -0
  84. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/policy/policy_manifest.py +0 -0
  85. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/policy/policy_resolver.py +0 -0
  86. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/policy/policy_types.py +0 -0
  87. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/pr/__init__.py +0 -0
  88. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/pr/explain_section_builder.py +0 -0
  89. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/risk/__init__.py +0 -0
  90. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/risk/risk_calculator.py +0 -0
  91. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/risk/risk_explanation.py +0 -0
  92. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/risk/risk_rules.py +0 -0
  93. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/risk/risk_types.py +0 -0
  94. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/tests_generation/__init__.py +0 -0
  95. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/tests_generation/framework_detector.py +0 -0
  96. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/tests_generation/test_content_types.py +0 -0
  97. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/domain/tests_generation/test_scaffold_builder.py +0 -0
  98. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/fix/__init__.py +0 -0
  99. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/fix/apply_fix.py +0 -0
  100. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/fix/fix_history.py +0 -0
  101. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/fix/patch_applier.py +0 -0
  102. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/fix/patch_extractor.py +0 -0
  103. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/fix/patch_provenance.py +0 -0
  104. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/fix/patch_safety_classifier.py +0 -0
  105. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/fix/rollback_fix.py +0 -0
  106. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/github_api.py +0 -0
  107. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/i18n.py +0 -0
  108. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/__init__.py +0 -0
  109. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/baseline/__init__.py +0 -0
  110. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/baseline/local_baseline_repository.py +0 -0
  111. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/ci/__init__.py +0 -0
  112. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/ci/junit_builder.py +0 -0
  113. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/ci/masking.py +0 -0
  114. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/ci/sarif_builder.py +0 -0
  115. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/git/__init__.py +0 -0
  116. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/git/identity.py +0 -0
  117. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/git/patch_applier.py +0 -0
  118. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/git/risk_history_reader.py +0 -0
  119. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/git/selective_stager.py +0 -0
  120. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/git/test_matcher.py +0 -0
  121. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/linter/external/__init__.py +0 -0
  122. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/linter/external/bandit_bridge.py +0 -0
  123. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/linter/external/base_bridge.py +0 -0
  124. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/linter/external/gitleaks_bridge.py +0 -0
  125. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/linter/external/semgrep_bridge.py +0 -0
  126. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/policy/__init__.py +0 -0
  127. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/policy/local_policy_repository.py +0 -0
  128. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/policy/policy_pack_loader.py +0 -0
  129. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/scm/__init__.py +0 -0
  130. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/scm/azure_devops_provider.py +0 -0
  131. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/scm/base.py +0 -0
  132. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/scm/bitbucket_provider.py +0 -0
  133. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/scm/github_provider.py +0 -0
  134. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/scm/gitlab_provider.py +0 -0
  135. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/infrastructure/scm/web_links.py +0 -0
  136. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/issue_autoclose/__init__.py +0 -0
  137. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/issue_autoclose/insert_close_line.py +0 -0
  138. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/issue_autoclose/issue_registry.py +0 -0
  139. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/issue_autoclose/register_created_issue.py +0 -0
  140. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/issue_autoclose/resolve_close_issue.py +0 -0
  141. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/ledger.py +0 -0
  142. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/linter_engine.py +0 -0
  143. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/linter_wizard.py +0 -0
  144. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/main.py +0 -0
  145. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/mcp_server.py +0 -0
  146. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/metrics.py +0 -0
  147. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/net.py +0 -0
  148. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/policy_packs/laravel-quality/CHANGELOG.md +0 -0
  149. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/policy_packs/laravel-quality/README.md +0 -0
  150. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/policy_packs/laravel-quality/linter.yml +0 -0
  151. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/policy_packs/laravel-quality/policy.yml +0 -0
  152. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/policy_packs/node-quality/README.md +0 -0
  153. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/policy_packs/node-quality/linter.yml +0 -0
  154. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/policy_packs/node-quality/policy.yml +0 -0
  155. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/policy_packs/php-security/README.md +0 -0
  156. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/policy_packs/php-security/linter.yml +0 -0
  157. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/policy_packs/php-security/policy.yml +0 -0
  158. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/policy_packs/vue-quality/README.md +0 -0
  159. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/policy_packs/vue-quality/linter.yml +0 -0
  160. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/policy_packs/vue-quality/policy.yml +0 -0
  161. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/project_config.py +0 -0
  162. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/release_engine.py +0 -0
  163. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/review/__init__.py +0 -0
  164. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/review/diff_normalizer.py +0 -0
  165. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/review/diff_source.py +0 -0
  166. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/review/render.py +0 -0
  167. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/reviewer_resolution.py +0 -0
  168. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/reviewer_suggestion.py +0 -0
  169. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/security.py +0 -0
  170. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/security_ruleset.py +0 -0
  171. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/skill_frontmatter.py +0 -0
  172. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/spinner.py +0 -0
  173. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/split/__init__.py +0 -0
  174. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/split/apply_split_plan.py +0 -0
  175. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/split/generate_split_plan.py +0 -0
  176. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/split/hunk_grouper.py +0 -0
  177. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/split/hunk_parser.py +0 -0
  178. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/split/split_plan.py +0 -0
  179. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/suggest_reviewers.py +0 -0
  180. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/tui_issue.py +0 -0
  181. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/ui/__init__.py +0 -0
  182. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/ui/chat_app.py +0 -0
  183. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/ui/commit_app.py +0 -0
  184. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/ui/demo/__init__.py +0 -0
  185. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/ui/demo/demo_app.py +0 -0
  186. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/ui/demo/demo_help_screen.py +0 -0
  187. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/ui/demo/demo_screens.py +0 -0
  188. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/ui/help_screen.py +0 -0
  189. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/ui/issue_app.py +0 -0
  190. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/ui/linter_app.py +0 -0
  191. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/ui/metrics_migration_app.py +0 -0
  192. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/ui/pr_base_app.py +0 -0
  193. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/ui/pr_publish_app.py +0 -0
  194. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/ui/pr_publish_help.py +0 -0
  195. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/usage_log.py +0 -0
  196. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/src/version_bump.py +0 -0
  197. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_baseline_cli.py +0 -0
  198. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_blame_engine_ranges.py +0 -0
  199. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_blame_metrics.py +0 -0
  200. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_changelog_builder.py +0 -0
  201. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_chat_backend.py +0 -0
  202. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_check_cli.py +0 -0
  203. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_cli_help.py +0 -0
  204. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_commit_classifier.py +0 -0
  205. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_config_app.py +0 -0
  206. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_config_cli.py +0 -0
  207. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_config_issue_autoclose.py +0 -0
  208. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_config_store.py +0 -0
  209. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_config_suggest_reviewers.py +0 -0
  210. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_config_validation.py +0 -0
  211. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_core.py +0 -0
  212. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_diff_failure_cli.py +0 -0
  213. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_diff_parser.py +0 -0
  214. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_doc_links.py +0 -0
  215. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_explain_command.py +0 -0
  216. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_external_linters.py +0 -0
  217. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_i18n.py +0 -0
  218. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_install_wizard.py +0 -0
  219. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_issue_engine.py +0 -0
  220. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_linter_metrics.py +0 -0
  221. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_linter_presets.py +0 -0
  222. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_main_suggest_reviewers.py +0 -0
  223. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_mcp_prompts.py +0 -0
  224. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_mcp_server.py +0 -0
  225. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_mcp_server_e2e.py +0 -0
  226. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_mentor_cli.py +0 -0
  227. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_net_timeouts.py +0 -0
  228. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_plugins.py +0 -0
  229. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_policy_cli.py +0 -0
  230. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_pr_base_app.py +0 -0
  231. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_pr_publish_app.py +0 -0
  232. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_pr_publish_linter_modal.py +0 -0
  233. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_pre_save.py +0 -0
  234. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_project_config.py +0 -0
  235. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_project_config_cli.py +0 -0
  236. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_release_cli.py +0 -0
  237. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_release_engine.py +0 -0
  238. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_reviewer_resolution.py +0 -0
  239. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_reviewer_suggestion.py +0 -0
  240. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_risk_cli.py +0 -0
  241. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_security_ruleset.py +0 -0
  242. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_seed_command.py +0 -0
  243. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_skill_command.py +0 -0
  244. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_skill_context.py +0 -0
  245. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_skill_frontmatter.py +0 -0
  246. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_skill_templates.py +0 -0
  247. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_suggest_reviewers.py +0 -0
  248. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_tests_command.py +0 -0
  249. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_thinking_words.py +0 -0
  250. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_tui_issue_token.py +0 -0
  251. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_updater.py +0 -0
  252. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_usage_log.py +0 -0
  253. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_version_bump.py +0 -0
  254. {gitpr_cli-1.6.0 → gitpr_cli-1.8.0}/tests/test_web_links.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitpr-cli
3
- Version: 1.6.0
3
+ Version: 1.8.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
@@ -499,7 +499,7 @@ This is useful for scripting, CI/CD pipelines, and one-off queries where you don
499
499
 
500
500
  📖 **Full documentation:** [gitpr.natanfiuza.dev.br/docs/mcp-integration](https://gitpr.natanfiuza.dev.br/docs/mcp-integration) — available in 5 languages (EN, PT-BR, PT-PT, ES, FR).
501
501
 
502
- > 🧩 **Official VS Code & Cursor Extension** — Prefer native editor commands, SCM toolbar buttons, and inline `Problems` diagnostics? Install the thin-client extension in [`extensions/vscode/`](https://gitpr.natanfiuza.dev.br/docs/editor-extension), powered by `gitpr-mcp`.
502
+ > 🧩 **Official VS Code & Cursor Extension** — Prefer native editor commands, SCM toolbar buttons, and inline `Problems` diagnostics? Download the ready-to-use `.vsix` from [GitHub Releases](https://github.com/natanfiuza/gitpr/releases) or see the [Editor Extension Guide](https://gitpr.natanfiuza.dev.br/docs/editor-extension) for 1-click install in VS Code and Cursor. Maintainers can run `scripts/marketplace-setup-wizard.sh` to configure and publish to Visual Studio Marketplace and Open VSX Registry.
503
503
 
504
504
  > 💬 **MCP Prompts** — GitPR also exposes 7 pre-defined message templates (prompts) for common flows like "Review PR", "Generate Commit Message", and "Create Issue from Diff". See the [MCP Prompts guide](https://gitpr.natanfiuza.dev.br/docs/mcp-prompts) for the full list.
505
505
 
@@ -479,7 +479,7 @@ This is useful for scripting, CI/CD pipelines, and one-off queries where you don
479
479
 
480
480
  📖 **Full documentation:** [gitpr.natanfiuza.dev.br/docs/mcp-integration](https://gitpr.natanfiuza.dev.br/docs/mcp-integration) — available in 5 languages (EN, PT-BR, PT-PT, ES, FR).
481
481
 
482
- > 🧩 **Official VS Code & Cursor Extension** — Prefer native editor commands, SCM toolbar buttons, and inline `Problems` diagnostics? Install the thin-client extension in [`extensions/vscode/`](https://gitpr.natanfiuza.dev.br/docs/editor-extension), powered by `gitpr-mcp`.
482
+ > 🧩 **Official VS Code & Cursor Extension** — Prefer native editor commands, SCM toolbar buttons, and inline `Problems` diagnostics? Download the ready-to-use `.vsix` from [GitHub Releases](https://github.com/natanfiuza/gitpr/releases) or see the [Editor Extension Guide](https://gitpr.natanfiuza.dev.br/docs/editor-extension) for 1-click install in VS Code and Cursor. Maintainers can run `scripts/marketplace-setup-wizard.sh` to configure and publish to Visual Studio Marketplace and Open VSX Registry.
483
483
 
484
484
  > 💬 **MCP Prompts** — GitPR also exposes 7 pre-defined message templates (prompts) for common flows like "Review PR", "Generate Commit Message", and "Create Issue from Diff". See the [MCP Prompts guide](https://gitpr.natanfiuza.dev.br/docs/mcp-prompts) for the full list.
485
485
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitpr-cli
3
- Version: 1.6.0
3
+ Version: 1.8.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
@@ -499,7 +499,7 @@ This is useful for scripting, CI/CD pipelines, and one-off queries where you don
499
499
 
500
500
  📖 **Full documentation:** [gitpr.natanfiuza.dev.br/docs/mcp-integration](https://gitpr.natanfiuza.dev.br/docs/mcp-integration) — available in 5 languages (EN, PT-BR, PT-PT, ES, FR).
501
501
 
502
- > 🧩 **Official VS Code & Cursor Extension** — Prefer native editor commands, SCM toolbar buttons, and inline `Problems` diagnostics? Install the thin-client extension in [`extensions/vscode/`](https://gitpr.natanfiuza.dev.br/docs/editor-extension), powered by `gitpr-mcp`.
502
+ > 🧩 **Official VS Code & Cursor Extension** — Prefer native editor commands, SCM toolbar buttons, and inline `Problems` diagnostics? Download the ready-to-use `.vsix` from [GitHub Releases](https://github.com/natanfiuza/gitpr/releases) or see the [Editor Extension Guide](https://gitpr.natanfiuza.dev.br/docs/editor-extension) for 1-click install in VS Code and Cursor. Maintainers can run `scripts/marketplace-setup-wizard.sh` to configure and publish to Visual Studio Marketplace and Open VSX Registry.
503
503
 
504
504
  > 💬 **MCP Prompts** — GitPR also exposes 7 pre-defined message templates (prompts) for common flows like "Review PR", "Generate Commit Message", and "Create Issue from Diff". See the [MCP Prompts guide](https://gitpr.natanfiuza.dev.br/docs/mcp-prompts) for the full list.
505
505
 
@@ -9,6 +9,7 @@ gitpr_cli.egg-info/requires.txt
9
9
  gitpr_cli.egg-info/top_level.txt
10
10
  src/__init__.py
11
11
  src/ai_providers.py
12
+ src/ascii_chart.py
12
13
  src/blame_engine.py
13
14
  src/cache.py
14
15
  src/changelog_builder.py
@@ -184,6 +185,7 @@ src/ui/demo/__init__.py
184
185
  src/ui/demo/demo_app.py
185
186
  src/ui/demo/demo_help_screen.py
186
187
  src/ui/demo/demo_screens.py
188
+ tests/test_ascii_chart.py
187
189
  tests/test_baseline_cli.py
188
190
  tests/test_blame_engine_ranges.py
189
191
  tests/test_blame_metrics.py
@@ -213,6 +215,7 @@ tests/test_issue_engine.py
213
215
  tests/test_linter_metrics.py
214
216
  tests/test_linter_presets.py
215
217
  tests/test_main_suggest_reviewers.py
218
+ tests/test_map_reduce_chunking.py
216
219
  tests/test_mcp_prompts.py
217
220
  tests/test_mcp_server.py
218
221
  tests/test_mcp_server_e2e.py
@@ -0,0 +1,248 @@
1
+ """
2
+ ASCII / Unicode 2D Chart Rendering Engine for GitPR Metrics.
3
+
4
+ Provides pure functions to render 2D terminal charts (vertical histograms,
5
+ time-series charts) without external dependencies or UI framework coupling.
6
+ """
7
+
8
+ import math
9
+ from typing import Any, Dict, List, Optional
10
+
11
+
12
+ def _fmt_tokens(val: float) -> str:
13
+ """Format token count in human-readable compact form (e.g. 1.2M, 45.0k, 500)."""
14
+ if val >= 1_000_000:
15
+ return f"{val / 1_000_000:.1f}M"
16
+ if val >= 1_000:
17
+ return f"{val / 1_000:.1f}k"
18
+ return str(int(round(val)))
19
+
20
+
21
+ def _duration_ms_label(ms: Optional[float]) -> str:
22
+ """Format duration in milliseconds into ms, s, or min."""
23
+ if ms is None:
24
+ return "0 ms"
25
+ if ms < 1000:
26
+ return f"{ms:.0f} ms"
27
+ seconds = ms / 1000.0
28
+ if seconds < 60:
29
+ return f"{seconds:.1f} s"
30
+ return f"{seconds / 60.0:.1f} min"
31
+
32
+
33
+ def render_empty_card(title: str, message: str, width: int = 60) -> str:
34
+ """Render a framed empty-state banner with informative text."""
35
+ w = max(40, width)
36
+ inner_w = w - 4
37
+ top_border = f"┌{'─' * (w - 2)}┐"
38
+ bottom_border = f"└{'─' * (w - 2)}┘"
39
+ empty_line = f"│ {' ' * inner_w} │"
40
+
41
+ title_line = f"│ [bold cyan]{title:<{inner_w}}[/bold cyan] │"
42
+ msg_line = f"│ [yellow]{message:<{inner_w}}[/yellow] │"
43
+
44
+ return "\n".join([top_border, title_line, empty_line, msg_line, bottom_border])
45
+
46
+
47
+ def render_vertical_histogram(
48
+ latency: Optional[Dict[str, Any]],
49
+ height: int = 7,
50
+ col_width: int = 7,
51
+ ) -> str:
52
+ """Render a 2D vertical ASCII histogram for latency distributions.
53
+
54
+ Args:
55
+ latency: Dict from latency_report() containing 'count', 'average_ms',
56
+ 'median_ms', 'p95_ms', and 'buckets'.
57
+ height: Height in lines for the plotting area.
58
+ col_width: Character width of each column bar.
59
+
60
+ Returns:
61
+ Formatted multi-line string containing the 2D chart.
62
+ """
63
+ if not latency or not latency.get("count") or not latency.get("buckets"):
64
+ return render_empty_card(
65
+ "📊 Latency Distribution",
66
+ "No timed executions recorded in the selected window.",
67
+ )
68
+
69
+ buckets: List[Dict[str, Any]] = latency["buckets"]
70
+ counts = [b["count"] for b in buckets]
71
+ max_count = max(counts) if counts else 0
72
+ if max_count <= 0:
73
+ max_count = 1
74
+
75
+ count_total = latency.get("count", 0)
76
+ avg_lbl = _duration_ms_label(latency.get("average_ms"))
77
+ med_lbl = _duration_ms_label(latency.get("median_ms"))
78
+ p95_lbl = _duration_ms_label(latency.get("p95_ms"))
79
+
80
+ lines: List[str] = []
81
+
82
+ # Title & Summary header
83
+ lines.append("[bold cyan]📊 Latency Histogram[/bold cyan]")
84
+ lines.append(
85
+ f" Timed runs: [bold]{count_total}[/bold] │ "
86
+ f"Average: [cyan]{avg_lbl}[/cyan] │ "
87
+ f"Median (P50): [green]{med_lbl}[/green] │ "
88
+ f"P95: [yellow]{p95_lbl}[/yellow]"
89
+ )
90
+ lines.append("")
91
+
92
+ # Y-axis label formatting width
93
+ y_label_width = max(len(str(max_count)), 3)
94
+
95
+ # Plot rows from top (height) down to 1
96
+ for r in range(height, 0, -1):
97
+ # Y-axis tick mark and label
98
+ ratio = r / height
99
+ level_val = round(ratio * max_count)
100
+ if r == height:
101
+ y_tick = f"{max_count:>{y_label_width}} ┤"
102
+ elif r == math.ceil(height / 2):
103
+ half_val = round(max_count / 2)
104
+ y_tick = f"{half_val:>{y_label_width}} ┤"
105
+ else:
106
+ y_tick = f"{' ' * y_label_width} │"
107
+
108
+ # Build column cells
109
+ cells = []
110
+ for b_count in counts:
111
+ # Thresholds for full block and half block
112
+ val_ratio = b_count / max_count
113
+ if val_ratio >= (r / height):
114
+ # Full block
115
+ bar_chars = "█" * col_width
116
+ elif val_ratio >= ((r - 0.5) / height):
117
+ # Half block
118
+ bar_chars = "▄" * col_width
119
+ else:
120
+ bar_chars = " " * col_width
121
+
122
+ cells.append(bar_chars)
123
+
124
+ row_content = " ".join(cells)
125
+ lines.append(f" {y_tick} {row_content}")
126
+
127
+ # X-axis Baseline
128
+ base_col = "─" * col_width
129
+ x_axis_line = "───".join([base_col] * len(buckets))
130
+ lines.append(f" {0:>{y_label_width}} ┴─{x_axis_line}─")
131
+
132
+ # Bucket Labels
133
+ label_cells = []
134
+ for b in buckets:
135
+ lbl = b.get("label", "")
136
+ label_cells.append(f"{lbl:^{col_width}}")
137
+ lines.append(f" {' ' * y_label_width} " + " ".join(label_cells))
138
+
139
+ # Counts and percentages below labels
140
+ stat_cells = []
141
+ for b in buckets:
142
+ c = b.get("count", 0)
143
+ rate = b.get("rate", 0.0)
144
+ txt = f"{c} ({rate:.0%})"
145
+ stat_cells.append(f"{txt:^{col_width}}")
146
+ lines.append(f" {' ' * y_label_width} " + " ".join(stat_cells))
147
+
148
+ return "\n".join(lines)
149
+
150
+
151
+ def render_token_timeseries(
152
+ trend: Optional[Dict[str, Any]],
153
+ height: int = 7,
154
+ col_width: int = 5,
155
+ ) -> str:
156
+ """Render a 2D continuous ASCII time-series chart for daily token trends.
157
+
158
+ Args:
159
+ trend: Dict from token_trend_report() containing 'days', 'total_tokens',
160
+ 'peak_tokens', 'average_tokens', and 'sparkline'.
161
+ height: Height in lines for the plotting area.
162
+ col_width: Character width allocated per day column.
163
+
164
+ Returns:
165
+ Formatted multi-line string containing the 2D chart.
166
+ """
167
+ if not trend or not trend.get("days") or not trend.get("total_tokens"):
168
+ return render_empty_card(
169
+ "📈 Token Trend & Consumption",
170
+ "No token consumption recorded in the selected window.",
171
+ )
172
+
173
+ days: List[Dict[str, Any]] = trend["days"]
174
+ peak_tokens = trend.get("peak_tokens", 0)
175
+ total_tokens = trend.get("total_tokens", 0)
176
+ avg_tokens = trend.get("average_tokens", 0.0)
177
+ sparkline = trend.get("sparkline", "")
178
+
179
+ if peak_tokens <= 0:
180
+ peak_tokens = 1
181
+
182
+ lines: List[str] = []
183
+
184
+ # Title & Summary header
185
+ lines.append("[bold cyan]📈 Token Consumption Trend[/bold cyan]")
186
+ lines.append(
187
+ f" Total: [bold]{total_tokens:,}[/bold] │ "
188
+ f"Peak: [yellow]{peak_tokens:,}[/yellow] │ "
189
+ f"Daily Avg: [green]{avg_tokens:,.0f}[/green] │ "
190
+ f"Sparkline: {sparkline}"
191
+ )
192
+ lines.append("")
193
+
194
+ y_label_top = _fmt_tokens(peak_tokens)
195
+ y_label_mid = _fmt_tokens(peak_tokens / 2)
196
+ y_label_width = max(len(y_label_top), len(y_label_mid), 4)
197
+
198
+ # Plot matrix from top down
199
+ for r in range(height, 0, -1):
200
+ if r == height:
201
+ y_tick = f"{y_label_top:>{y_label_width}} ┼"
202
+ grid_char = "┈"
203
+ elif r == math.ceil(height / 2):
204
+ y_tick = f"{y_label_mid:>{y_label_width}} ┼"
205
+ grid_char = "┈"
206
+ else:
207
+ y_tick = f"{' ' * y_label_width} │"
208
+ grid_char = " "
209
+
210
+ day_cells = []
211
+ for d in days:
212
+ tokens = d.get("tokens", 0)
213
+ ratio = tokens / peak_tokens
214
+ if ratio >= (r / height):
215
+ cell_str = "█" * col_width
216
+ elif ratio >= ((r - 0.5) / height):
217
+ cell_str = "▄" * col_width
218
+ elif grid_char != " ":
219
+ cell_str = grid_char * col_width
220
+ else:
221
+ cell_str = " " * col_width
222
+ day_cells.append(cell_str)
223
+
224
+ lines.append(f" {y_tick} " + " ".join(day_cells))
225
+
226
+ # X-axis Baseline
227
+ base_col = "─" * col_width
228
+ x_axis_line = "─".join([base_col] * len(days))
229
+ lines.append(f" {'0':>{y_label_width}} ┴─{x_axis_line}─")
230
+
231
+ # Date labels (MM-DD)
232
+ date_cells = []
233
+ for d in days:
234
+ raw_day = d.get("day", "")
235
+ # Format YYYY-MM-DD -> MM-DD
236
+ short_date = raw_day[5:] if len(raw_day) >= 10 else raw_day
237
+ date_cells.append(f"{short_date:^{col_width}}")
238
+ lines.append(f" {' ' * y_label_width} " + " ".join(date_cells))
239
+
240
+ # Compact token labels below dates
241
+ tok_cells = []
242
+ for d in days:
243
+ t = d.get("tokens", 0)
244
+ tok_str = _fmt_tokens(t) if t > 0 else "·"
245
+ tok_cells.append(f"{tok_str:^{col_width}}")
246
+ lines.append(f" {' ' * y_label_width} " + " ".join(tok_cells))
247
+
248
+ return "\n".join(lines)
@@ -249,6 +249,59 @@ def get_linter_timeout():
249
249
  return _positive_float(os.getenv("GITPR_LINTER_TIMEOUT"), _DEFAULT_LINTER_TIMEOUT)
250
250
 
251
251
 
252
+ DEFAULT_MAP_REDUCE_MAX_TOKENS = 90000
253
+ DEFAULT_MAP_REDUCE_MAX_PART_TOKENS = 50000
254
+
255
+
256
+ def _positive_int_from_raw(raw, fallback):
257
+ """Parses raw as a positive int, falling back on non-positive or invalid input."""
258
+ if raw is None:
259
+ return fallback
260
+ try:
261
+ val = int(str(raw).strip())
262
+ return val if val > 0 else fallback
263
+ except (TypeError, ValueError):
264
+ return fallback
265
+
266
+
267
+ def get_map_reduce_thresholds(provider=None):
268
+ """Returns (max_tokens, max_part_tokens) for Map-Reduce chunking.
269
+
270
+ Resolution hierarchy:
271
+ 1. Provider-specific: GITPR_MAP_REDUCE_MAX_TOKENS_<PROVIDER>
272
+ 2. Generic: GITPR_MAP_REDUCE_MAX_TOKENS
273
+ 3. Baseline default: 90,000 tokens
274
+
275
+ For part tokens:
276
+ 1. Provider-specific: GITPR_MAP_REDUCE_MAX_PART_TOKENS_<PROVIDER>
277
+ 2. Generic: GITPR_MAP_REDUCE_MAX_PART_TOKENS
278
+ 3. Baseline default: 50,000 tokens
279
+
280
+ Guarantees max_part_tokens <= max_tokens.
281
+ """
282
+ load_dotenv(ENV_FILE)
283
+ prov_key = (provider or "").strip().upper()
284
+
285
+ trigger_val = None
286
+ if prov_key:
287
+ trigger_val = os.getenv(f"GITPR_MAP_REDUCE_MAX_TOKENS_{prov_key}")
288
+ if trigger_val is None or not str(trigger_val).strip():
289
+ trigger_val = os.getenv("GITPR_MAP_REDUCE_MAX_TOKENS")
290
+ max_tokens = _positive_int_from_raw(trigger_val, DEFAULT_MAP_REDUCE_MAX_TOKENS)
291
+
292
+ part_val = None
293
+ if prov_key:
294
+ part_val = os.getenv(f"GITPR_MAP_REDUCE_MAX_PART_TOKENS_{prov_key}")
295
+ if part_val is None or not str(part_val).strip():
296
+ part_val = os.getenv("GITPR_MAP_REDUCE_MAX_PART_TOKENS")
297
+ max_part_tokens = _positive_int_from_raw(part_val, DEFAULT_MAP_REDUCE_MAX_PART_TOKENS)
298
+
299
+ if max_part_tokens > max_tokens:
300
+ max_part_tokens = max_tokens
301
+
302
+ return max_tokens, max_part_tokens
303
+
304
+
252
305
  # Metrics pricing.
253
306
  #
254
307
  # Two layers, the second overriding the first per model:
@@ -291,6 +344,8 @@ DEFAULT_MODEL_PRICES = {
291
344
  "deepseek-v4-pro": (0.435, 0.87),
292
345
  "gemini-2.5-pro": (1.25, 10.00),
293
346
  "gemini-2.5-flash-lite": (0.10, 0.40),
347
+ "gemini-pro-latest": (1.25, 10.00),
348
+ "gemini-flash-lite-latest": (0.10, 0.40),
294
349
  }
295
350
 
296
351
  # Providers that run on this machine. There is no invoice behind them, so their
@@ -22,6 +22,7 @@ from src.i18n import __
22
22
  # Widget kinds. Each one maps to a different control in ui/config_app.py.
23
23
  KIND_BOOL = "bool"
24
24
  KIND_INT = "int"
25
+ KIND_FLOAT = "float"
25
26
  KIND_STR = "str"
26
27
  KIND_ENUM = "enum"
27
28
  KIND_TEMPLATE = "template"
@@ -39,6 +40,7 @@ KIND_BRANCH = "branch"
39
40
  KINDS = (
40
41
  KIND_BOOL,
41
42
  KIND_INT,
43
+ KIND_FLOAT,
42
44
  KIND_STR,
43
45
  KIND_ENUM,
44
46
  KIND_TEMPLATE,
@@ -395,6 +397,16 @@ FIELDS = (
395
397
  default="180",
396
398
  seeded=True,
397
399
  ),
400
+ ConfigField(
401
+ key="GITPR_METRICS_CURRENCY",
402
+ label=__("Metrics Currency"),
403
+ description=__(
404
+ "Currency code used in telemetry and cost reports (default USD)."
405
+ ),
406
+ category="ai",
407
+ kind=KIND_STR,
408
+ default="",
409
+ ),
398
410
  ConfigField(
399
411
  key="GEMINI_API_MODEL_PRIMARY",
400
412
  label=__("Gemini Advanced Model"),
@@ -405,6 +417,28 @@ FIELDS = (
405
417
  show_if=(("DEFAULT_AI_PROVIDER", ("gemini",)),),
406
418
  seeded=True,
407
419
  ),
420
+ ConfigField(
421
+ key="GITPR_METRICS_PRICE_GEMINI_PRO_LATEST_INPUT",
422
+ label=__("Gemini Advanced Model - Input Price"),
423
+ description=__(
424
+ "Cost per million input tokens for the advanced model."
425
+ ),
426
+ category="ai",
427
+ kind=KIND_FLOAT,
428
+ default="",
429
+ show_if=(("DEFAULT_AI_PROVIDER", ("gemini",)),),
430
+ ),
431
+ ConfigField(
432
+ key="GITPR_METRICS_PRICE_GEMINI_PRO_LATEST_OUTPUT",
433
+ label=__("Gemini Advanced Model - Output Price"),
434
+ description=__(
435
+ "Cost per million completion tokens for the advanced model."
436
+ ),
437
+ category="ai",
438
+ kind=KIND_FLOAT,
439
+ default="",
440
+ show_if=(("DEFAULT_AI_PROVIDER", ("gemini",)),),
441
+ ),
408
442
  ConfigField(
409
443
  key="GEMINI_API_MODEL_SECONDARY",
410
444
  label=__("Gemini Fast Model"),
@@ -415,6 +449,28 @@ FIELDS = (
415
449
  show_if=(("DEFAULT_AI_PROVIDER", ("gemini",)),),
416
450
  seeded=True,
417
451
  ),
452
+ ConfigField(
453
+ key="GITPR_METRICS_PRICE_GEMINI_FLASH_LITE_LATEST_INPUT",
454
+ label=__("Gemini Fast Model - Input Price"),
455
+ description=__(
456
+ "Cost per million input tokens for the fast model."
457
+ ),
458
+ category="ai",
459
+ kind=KIND_FLOAT,
460
+ default="",
461
+ show_if=(("DEFAULT_AI_PROVIDER", ("gemini",)),),
462
+ ),
463
+ ConfigField(
464
+ key="GITPR_METRICS_PRICE_GEMINI_FLASH_LITE_LATEST_OUTPUT",
465
+ label=__("Gemini Fast Model - Output Price"),
466
+ description=__(
467
+ "Cost per million completion tokens for the fast model."
468
+ ),
469
+ category="ai",
470
+ kind=KIND_FLOAT,
471
+ default="",
472
+ show_if=(("DEFAULT_AI_PROVIDER", ("gemini",)),),
473
+ ),
418
474
  ConfigField(
419
475
  key="GEMINI_API_KEY_ENCRYPTED",
420
476
  label=__("Gemini API Key"),
@@ -427,6 +483,28 @@ FIELDS = (
427
483
  validator=VALIDATOR_AI_KEY,
428
484
  show_if=(("DEFAULT_AI_PROVIDER", ("gemini",)),),
429
485
  ),
486
+ ConfigField(
487
+ key="GITPR_MAP_REDUCE_MAX_TOKENS_GEMINI",
488
+ label=__("Gemini Map-Reduce Trigger Tokens"),
489
+ description=__(
490
+ "Maximum total diff tokens before triggering Map-Reduce processing for Gemini."
491
+ ),
492
+ category="ai",
493
+ kind=KIND_INT,
494
+ default="90000",
495
+ show_if=(("DEFAULT_AI_PROVIDER", ("gemini",)),),
496
+ ),
497
+ ConfigField(
498
+ key="GITPR_MAP_REDUCE_MAX_PART_TOKENS_GEMINI",
499
+ label=__("Gemini Map-Reduce Part Size"),
500
+ description=__(
501
+ "Maximum tokens per batch chunk during Gemini Map-Reduce processing."
502
+ ),
503
+ category="ai",
504
+ kind=KIND_INT,
505
+ default="50000",
506
+ show_if=(("DEFAULT_AI_PROVIDER", ("gemini",)),),
507
+ ),
430
508
  ConfigField(
431
509
  key="DEEPSEEK_API_MODEL_PRIMARY",
432
510
  label=__("DeepSeek Advanced Model"),
@@ -437,6 +515,28 @@ FIELDS = (
437
515
  show_if=(("DEFAULT_AI_PROVIDER", ("deepseek",)),),
438
516
  seeded=True,
439
517
  ),
518
+ ConfigField(
519
+ key="GITPR_METRICS_PRICE_DEEPSEEK_V4_PRO_INPUT",
520
+ label=__("DeepSeek Advanced Model - Input Price"),
521
+ description=__(
522
+ "Cost per million input tokens for the advanced model."
523
+ ),
524
+ category="ai",
525
+ kind=KIND_FLOAT,
526
+ default="",
527
+ show_if=(("DEFAULT_AI_PROVIDER", ("deepseek",)),),
528
+ ),
529
+ ConfigField(
530
+ key="GITPR_METRICS_PRICE_DEEPSEEK_V4_PRO_OUTPUT",
531
+ label=__("DeepSeek Advanced Model - Output Price"),
532
+ description=__(
533
+ "Cost per million completion tokens for the advanced model."
534
+ ),
535
+ category="ai",
536
+ kind=KIND_FLOAT,
537
+ default="",
538
+ show_if=(("DEFAULT_AI_PROVIDER", ("deepseek",)),),
539
+ ),
440
540
  ConfigField(
441
541
  key="DEEPSEEK_API_MODEL_SECONDARY",
442
542
  label=__("DeepSeek Fast Model"),
@@ -447,6 +547,28 @@ FIELDS = (
447
547
  show_if=(("DEFAULT_AI_PROVIDER", ("deepseek",)),),
448
548
  seeded=True,
449
549
  ),
550
+ ConfigField(
551
+ key="GITPR_METRICS_PRICE_DEEPSEEK_V4_FLASH_INPUT",
552
+ label=__("DeepSeek Fast Model - Input Price"),
553
+ description=__(
554
+ "Cost per million input tokens for the fast model."
555
+ ),
556
+ category="ai",
557
+ kind=KIND_FLOAT,
558
+ default="",
559
+ show_if=(("DEFAULT_AI_PROVIDER", ("deepseek",)),),
560
+ ),
561
+ ConfigField(
562
+ key="GITPR_METRICS_PRICE_DEEPSEEK_V4_FLASH_OUTPUT",
563
+ label=__("DeepSeek Fast Model - Output Price"),
564
+ description=__(
565
+ "Cost per million completion tokens for the fast model."
566
+ ),
567
+ category="ai",
568
+ kind=KIND_FLOAT,
569
+ default="",
570
+ show_if=(("DEFAULT_AI_PROVIDER", ("deepseek",)),),
571
+ ),
450
572
  ConfigField(
451
573
  key="DEEPSEEK_API_KEY_ENCRYPTED",
452
574
  label=__("DeepSeek API Key"),
@@ -459,6 +581,28 @@ FIELDS = (
459
581
  validator=VALIDATOR_AI_KEY,
460
582
  show_if=(("DEFAULT_AI_PROVIDER", ("deepseek",)),),
461
583
  ),
584
+ ConfigField(
585
+ key="GITPR_MAP_REDUCE_MAX_TOKENS_DEEPSEEK",
586
+ label=__("DeepSeek Map-Reduce Trigger Tokens"),
587
+ description=__(
588
+ "Maximum total diff tokens before triggering Map-Reduce processing for DeepSeek."
589
+ ),
590
+ category="ai",
591
+ kind=KIND_INT,
592
+ default="90000",
593
+ show_if=(("DEFAULT_AI_PROVIDER", ("deepseek",)),),
594
+ ),
595
+ ConfigField(
596
+ key="GITPR_MAP_REDUCE_MAX_PART_TOKENS_DEEPSEEK",
597
+ label=__("DeepSeek Map-Reduce Part Size"),
598
+ description=__(
599
+ "Maximum tokens per batch chunk during DeepSeek Map-Reduce processing."
600
+ ),
601
+ category="ai",
602
+ kind=KIND_INT,
603
+ default="50000",
604
+ show_if=(("DEFAULT_AI_PROVIDER", ("deepseek",)),),
605
+ ),
462
606
  ConfigField(
463
607
  key="OLLAMA_API_MODEL_PRIMARY",
464
608
  label=__("Ollama Advanced Model"),
@@ -479,6 +623,28 @@ FIELDS = (
479
623
  show_if=(("DEFAULT_AI_PROVIDER", ("ollama",)),),
480
624
  seeded=True,
481
625
  ),
626
+ ConfigField(
627
+ key="GITPR_MAP_REDUCE_MAX_TOKENS_OLLAMA",
628
+ label=__("Ollama Map-Reduce Trigger Tokens"),
629
+ description=__(
630
+ "Maximum total diff tokens before triggering Map-Reduce processing for Ollama."
631
+ ),
632
+ category="ai",
633
+ kind=KIND_INT,
634
+ default="90000",
635
+ show_if=(("DEFAULT_AI_PROVIDER", ("ollama",)),),
636
+ ),
637
+ ConfigField(
638
+ key="GITPR_MAP_REDUCE_MAX_PART_TOKENS_OLLAMA",
639
+ label=__("Ollama Map-Reduce Part Size"),
640
+ description=__(
641
+ "Maximum tokens per batch chunk during Ollama Map-Reduce processing."
642
+ ),
643
+ category="ai",
644
+ kind=KIND_INT,
645
+ default="50000",
646
+ show_if=(("DEFAULT_AI_PROVIDER", ("ollama",)),),
647
+ ),
482
648
  # ----------------------------------------------------------- Pull request
483
649
  ConfigField(
484
650
  key="OUTPUT_FILE_NAME",
@@ -1595,6 +1761,16 @@ def validate_field_value(field, value):
1595
1761
  if parsed <= 0:
1596
1762
  return __("Use a whole number greater than zero.")
1597
1763
 
1764
+ elif field.kind == KIND_FLOAT:
1765
+ if not value:
1766
+ return ""
1767
+ try:
1768
+ parsed = float(value)
1769
+ except ValueError:
1770
+ return __("Use a number greater than or equal to zero.")
1771
+ if parsed < 0:
1772
+ return __("Use a number greater than or equal to zero.")
1773
+
1598
1774
  elif field.kind == KIND_ENUM:
1599
1775
  if value not in field.choices:
1600
1776
  return __("Choose one of: {choices}", choices=", ".join(field.choices))