gitpr-cli 1.5.0__tar.gz → 1.7.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 (256) hide show
  1. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/PKG-INFO +2 -2
  2. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/README.md +1 -1
  3. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/gitpr_cli.egg-info/PKG-INFO +2 -2
  4. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/gitpr_cli.egg-info/SOURCES.txt +8 -0
  5. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/application/use_cases/create_baseline.py +2 -1
  6. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/application/use_cases/run_check.py +2 -1
  7. gitpr_cli-1.7.0/src/ascii_chart.py +248 -0
  8. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/config.py +1266 -1383
  9. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/config_schema.py +1730 -1357
  10. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/core.py +2769 -2328
  11. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/diff_parser.py +102 -0
  12. gitpr_cli-1.7.0/src/doc_links.py +53 -0
  13. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/i18n.py +142 -136
  14. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/scm/azure_devops_provider.py +491 -464
  15. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/scm/base.py +1 -0
  16. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/scm/bitbucket_provider.py +443 -402
  17. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/scm/factory.py +15 -3
  18. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/scm/gitlab_provider.py +469 -435
  19. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/issue_autoclose/issue_registry.py +285 -235
  20. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/issue_autoclose/register_created_issue.py +81 -80
  21. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/issue_autoclose/resolve_close_issue.py +236 -226
  22. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/ledger.py +7 -2
  23. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/main.py +6277 -45
  24. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/mcp_server.py +21 -5
  25. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/metrics.py +249 -2
  26. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/project_config.py +484 -426
  27. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/release_engine.py +134 -64
  28. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/review/diff_normalizer.py +84 -84
  29. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/reviewer_suggestion.py +2 -0
  30. gitpr_cli-1.7.0/src/scm_settings.py +441 -0
  31. gitpr_cli-1.7.0/src/skill_frontmatter.py +70 -0
  32. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/spinner.py +260 -257
  33. gitpr_cli-1.7.0/src/suggest_reviewers.py +155 -0
  34. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/tui_issue.py +44 -24
  35. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/ui/chat_app.py +4 -3
  36. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/ui/config_app.py +2332 -2043
  37. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/ui/help_screen.py +46 -46
  38. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/ui/issue_app.py +260 -258
  39. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/ui/linter_app.py +48 -46
  40. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/ui/metrics_app.py +129 -22
  41. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/updater.py +2 -2
  42. gitpr_cli-1.7.0/tests/test_ascii_chart.py +166 -0
  43. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_commit_app.py +44 -3
  44. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_config_app.py +2129 -1929
  45. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_config_schema.py +149 -36
  46. gitpr_cli-1.7.0/tests/test_doc_links.py +155 -0
  47. gitpr_cli-1.7.0/tests/test_full_diff_encoding.py +252 -0
  48. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_i18n.py +385 -384
  49. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_install_wizard.py +101 -98
  50. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_metrics.py +2424 -2205
  51. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_project_config.py +95 -0
  52. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_release_engine.py +1305 -1193
  53. gitpr_cli-1.7.0/tests/test_seed_command.py +186 -0
  54. gitpr_cli-1.7.0/tests/test_skill_frontmatter.py +60 -0
  55. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_smart_excludes.py +38 -0
  56. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_suggest_reviewers.py +429 -5
  57. gitpr_cli-1.7.0/tests/test_tui_issue_token.py +189 -0
  58. gitpr_cli-1.5.0/src/doc_links.py +0 -28
  59. gitpr_cli-1.5.0/src/suggest_reviewers.py +0 -146
  60. gitpr_cli-1.5.0/tests/test_full_diff_encoding.py +0 -83
  61. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/LICENSE +0 -0
  62. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/gitpr_cli.egg-info/dependency_links.txt +0 -0
  63. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/gitpr_cli.egg-info/entry_points.txt +0 -0
  64. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/gitpr_cli.egg-info/requires.txt +0 -0
  65. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/gitpr_cli.egg-info/top_level.txt +0 -0
  66. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/pyproject.toml +0 -0
  67. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/setup.cfg +0 -0
  68. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/__init__.py +0 -0
  69. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/ai_providers.py +0 -0
  70. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/application/__init__.py +0 -0
  71. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/application/use_cases/__init__.py +0 -0
  72. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/application/use_cases/activate_policy_pack.py +0 -0
  73. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/application/use_cases/baseline_gate.py +0 -0
  74. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/application/use_cases/baseline_summary.py +0 -0
  75. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/application/use_cases/calculate_risk.py +0 -0
  76. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/application/use_cases/compare_against_baseline.py +0 -0
  77. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/application/use_cases/generate_mentor_explanation.py +0 -0
  78. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/application/use_cases/generate_pr_explanation.py +0 -0
  79. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/application/use_cases/generate_test_file.py +0 -0
  80. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/application/use_cases/install_policy_pack.py +0 -0
  81. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/application/use_cases/publish_pull_request.py +0 -0
  82. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/application/use_cases/resolve_effective_policy.py +0 -0
  83. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/application/use_cases/suppress_finding.py +0 -0
  84. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/application/use_cases/validate_policy_pack.py +0 -0
  85. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/blame_engine.py +0 -0
  86. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/branding/__init__.py +0 -0
  87. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/branding/badge_builder.py +0 -0
  88. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/branding/badge_data.py +0 -0
  89. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/cache.py +0 -0
  90. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/changelog_builder.py +0 -0
  91. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/chat_memory.py +0 -0
  92. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/commit_classifier.py +0 -0
  93. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/demo/__init__.py +0 -0
  94. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/demo/demo_runner.py +0 -0
  95. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/demo/fake_ai_provider.py +0 -0
  96. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/demo/scenarios/__init__.py +0 -0
  97. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/demo/scenarios/laravel_bug_fix.py +0 -0
  98. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/demo/scenarios/security_issue.py +0 -0
  99. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/baseline/__init__.py +0 -0
  100. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/baseline/baseline_comparator.py +0 -0
  101. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/baseline/baseline_fingerprint.py +0 -0
  102. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/baseline/baseline_manifest.py +0 -0
  103. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/baseline/baseline_types.py +0 -0
  104. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/baseline/suppression_policy.py +0 -0
  105. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/check/__init__.py +0 -0
  106. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/check/check_config.py +0 -0
  107. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/check/check_output.py +0 -0
  108. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/check/check_types.py +0 -0
  109. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/finding/__init__.py +0 -0
  110. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/finding/finding_types.py +0 -0
  111. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/linter/__init__.py +0 -0
  112. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/linter/sast_finding_mapper.py +0 -0
  113. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/mentor/__init__.py +0 -0
  114. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/mentor/mentor_explanation_builder.py +0 -0
  115. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/policy/__init__.py +0 -0
  116. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/policy/policy_checksum.py +0 -0
  117. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/policy/policy_compatibility.py +0 -0
  118. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/policy/policy_manifest.py +0 -0
  119. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/policy/policy_resolver.py +0 -0
  120. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/policy/policy_types.py +0 -0
  121. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/pr/__init__.py +0 -0
  122. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/pr/explain_section_builder.py +0 -0
  123. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/risk/__init__.py +0 -0
  124. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/risk/risk_calculator.py +0 -0
  125. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/risk/risk_explanation.py +0 -0
  126. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/risk/risk_rules.py +0 -0
  127. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/risk/risk_types.py +0 -0
  128. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/tests_generation/__init__.py +0 -0
  129. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/tests_generation/framework_detector.py +0 -0
  130. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/tests_generation/test_content_types.py +0 -0
  131. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/domain/tests_generation/test_scaffold_builder.py +0 -0
  132. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/fix/__init__.py +0 -0
  133. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/fix/apply_fix.py +0 -0
  134. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/fix/fix_history.py +0 -0
  135. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/fix/patch_applier.py +0 -0
  136. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/fix/patch_extractor.py +0 -0
  137. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/fix/patch_provenance.py +0 -0
  138. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/fix/patch_safety_classifier.py +0 -0
  139. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/fix/rollback_fix.py +0 -0
  140. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/github_api.py +0 -0
  141. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/__init__.py +0 -0
  142. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/baseline/__init__.py +0 -0
  143. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/baseline/local_baseline_repository.py +0 -0
  144. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/ci/__init__.py +0 -0
  145. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/ci/junit_builder.py +0 -0
  146. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/ci/masking.py +0 -0
  147. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/ci/sarif_builder.py +0 -0
  148. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/git/__init__.py +0 -0
  149. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/git/identity.py +0 -0
  150. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/git/patch_applier.py +0 -0
  151. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/git/risk_history_reader.py +0 -0
  152. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/git/selective_stager.py +0 -0
  153. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/git/test_matcher.py +0 -0
  154. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/linter/external/__init__.py +0 -0
  155. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/linter/external/bandit_bridge.py +0 -0
  156. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/linter/external/base_bridge.py +0 -0
  157. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/linter/external/gitleaks_bridge.py +0 -0
  158. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/linter/external/semgrep_bridge.py +0 -0
  159. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/policy/__init__.py +0 -0
  160. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/policy/local_policy_repository.py +0 -0
  161. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/policy/policy_pack_loader.py +0 -0
  162. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/scm/__init__.py +0 -0
  163. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/scm/github_provider.py +0 -0
  164. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/infrastructure/scm/web_links.py +0 -0
  165. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/issue_autoclose/__init__.py +0 -0
  166. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/issue_autoclose/insert_close_line.py +0 -0
  167. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/issue_engine.py +0 -0
  168. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/linter_engine.py +0 -0
  169. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/linter_wizard.py +0 -0
  170. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/net.py +0 -0
  171. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/policy_packs/laravel-quality/CHANGELOG.md +0 -0
  172. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/policy_packs/laravel-quality/README.md +0 -0
  173. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/policy_packs/laravel-quality/linter.yml +0 -0
  174. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/policy_packs/laravel-quality/policy.yml +0 -0
  175. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/policy_packs/node-quality/README.md +0 -0
  176. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/policy_packs/node-quality/linter.yml +0 -0
  177. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/policy_packs/node-quality/policy.yml +0 -0
  178. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/policy_packs/php-security/README.md +0 -0
  179. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/policy_packs/php-security/linter.yml +0 -0
  180. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/policy_packs/php-security/policy.yml +0 -0
  181. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/policy_packs/vue-quality/README.md +0 -0
  182. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/policy_packs/vue-quality/linter.yml +0 -0
  183. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/policy_packs/vue-quality/policy.yml +0 -0
  184. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/review/__init__.py +0 -0
  185. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/review/diff_source.py +0 -0
  186. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/review/remote_pr.py +0 -0
  187. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/review/render.py +0 -0
  188. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/reviewer_resolution.py +0 -0
  189. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/security.py +0 -0
  190. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/security_ruleset.py +0 -0
  191. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/split/__init__.py +0 -0
  192. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/split/apply_split_plan.py +0 -0
  193. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/split/generate_split_plan.py +0 -0
  194. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/split/hunk_grouper.py +0 -0
  195. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/split/hunk_parser.py +0 -0
  196. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/split/split_plan.py +0 -0
  197. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/ui/__init__.py +0 -0
  198. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/ui/commit_app.py +0 -0
  199. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/ui/demo/__init__.py +0 -0
  200. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/ui/demo/demo_app.py +0 -0
  201. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/ui/demo/demo_help_screen.py +0 -0
  202. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/ui/demo/demo_screens.py +0 -0
  203. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/ui/metrics_migration_app.py +0 -0
  204. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/ui/pr_base_app.py +0 -0
  205. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/ui/pr_publish_app.py +0 -0
  206. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/ui/pr_publish_help.py +0 -0
  207. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/usage_log.py +0 -0
  208. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/src/version_bump.py +0 -0
  209. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_baseline_cli.py +0 -0
  210. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_blame_engine_ranges.py +0 -0
  211. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_blame_metrics.py +0 -0
  212. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_changelog_builder.py +0 -0
  213. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_chat_backend.py +0 -0
  214. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_check_cli.py +0 -0
  215. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_cli_help.py +0 -0
  216. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_commit_classifier.py +0 -0
  217. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_config_cli.py +0 -0
  218. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_config_issue_autoclose.py +0 -0
  219. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_config_store.py +0 -0
  220. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_config_suggest_reviewers.py +0 -0
  221. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_config_validation.py +0 -0
  222. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_core.py +0 -0
  223. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_diff_failure_cli.py +0 -0
  224. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_diff_parser.py +0 -0
  225. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_explain_command.py +0 -0
  226. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_external_linters.py +0 -0
  227. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_issue_engine.py +0 -0
  228. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_linter_metrics.py +0 -0
  229. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_linter_presets.py +0 -0
  230. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_main_suggest_reviewers.py +0 -0
  231. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_mcp_prompts.py +0 -0
  232. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_mcp_server.py +0 -0
  233. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_mcp_server_e2e.py +0 -0
  234. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_mentor_cli.py +0 -0
  235. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_net_timeouts.py +0 -0
  236. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_plugins.py +0 -0
  237. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_policy_cli.py +0 -0
  238. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_pr_base_app.py +0 -0
  239. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_pr_publish_app.py +0 -0
  240. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_pr_publish_linter_modal.py +0 -0
  241. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_pre_save.py +0 -0
  242. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_project_config_cli.py +0 -0
  243. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_release_cli.py +0 -0
  244. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_reviewer_resolution.py +0 -0
  245. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_reviewer_suggestion.py +0 -0
  246. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_risk_cli.py +0 -0
  247. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_security_ruleset.py +0 -0
  248. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_skill_command.py +0 -0
  249. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_skill_context.py +0 -0
  250. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_skill_templates.py +0 -0
  251. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_tests_command.py +0 -0
  252. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_thinking_words.py +0 -0
  253. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_updater.py +0 -0
  254. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_usage_log.py +0 -0
  255. {gitpr_cli-1.5.0 → gitpr_cli-1.7.0}/tests/test_version_bump.py +0 -0
  256. {gitpr_cli-1.5.0 → gitpr_cli-1.7.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.5.0
3
+ Version: 1.7.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.
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.
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.5.0
3
+ Version: 1.7.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.
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
@@ -33,8 +34,10 @@ src/project_config.py
33
34
  src/release_engine.py
34
35
  src/reviewer_resolution.py
35
36
  src/reviewer_suggestion.py
37
+ src/scm_settings.py
36
38
  src/security.py
37
39
  src/security_ruleset.py
40
+ src/skill_frontmatter.py
38
41
  src/spinner.py
39
42
  src/suggest_reviewers.py
40
43
  src/tui_issue.py
@@ -182,6 +185,7 @@ src/ui/demo/__init__.py
182
185
  src/ui/demo/demo_app.py
183
186
  src/ui/demo/demo_help_screen.py
184
187
  src/ui/demo/demo_screens.py
188
+ tests/test_ascii_chart.py
185
189
  tests/test_baseline_cli.py
186
190
  tests/test_blame_engine_ranges.py
187
191
  tests/test_blame_metrics.py
@@ -201,6 +205,7 @@ tests/test_config_validation.py
201
205
  tests/test_core.py
202
206
  tests/test_diff_failure_cli.py
203
207
  tests/test_diff_parser.py
208
+ tests/test_doc_links.py
204
209
  tests/test_explain_command.py
205
210
  tests/test_external_linters.py
206
211
  tests/test_full_diff_encoding.py
@@ -230,13 +235,16 @@ tests/test_reviewer_resolution.py
230
235
  tests/test_reviewer_suggestion.py
231
236
  tests/test_risk_cli.py
232
237
  tests/test_security_ruleset.py
238
+ tests/test_seed_command.py
233
239
  tests/test_skill_command.py
234
240
  tests/test_skill_context.py
241
+ tests/test_skill_frontmatter.py
235
242
  tests/test_skill_templates.py
236
243
  tests/test_smart_excludes.py
237
244
  tests/test_suggest_reviewers.py
238
245
  tests/test_tests_command.py
239
246
  tests/test_thinking_words.py
247
+ tests/test_tui_issue_token.py
240
248
  tests/test_updater.py
241
249
  tests/test_usage_log.py
242
250
  tests/test_version_bump.py
@@ -107,7 +107,8 @@ def current_diff(base: str | None = None, quiet: bool = True) -> str:
107
107
  encoding="utf-8",
108
108
  errors="replace",
109
109
  )
110
- return result.stdout if result.returncode == 0 else ""
110
+ from src.diff_parser import collapse_binary_hunks
111
+ return collapse_binary_hunks(result.stdout) if result.returncode == 0 else ""
111
112
 
112
113
 
113
114
  def diff_files(diff_text: str) -> list[str]:
@@ -108,7 +108,8 @@ def get_check_diff(
108
108
  cwd=repo_path or None,
109
109
  )
110
110
  if proc.returncode == 0:
111
- return proc.stdout or ""
111
+ from src.diff_parser import collapse_binary_hunks
112
+ return collapse_binary_hunks(proc.stdout or "")
112
113
  except OSError:
113
114
  pass
114
115
  return ""
@@ -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)