fplkit 2.5.0__tar.gz → 2.6.1__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 (315) hide show
  1. {fplkit-2.5.0 → fplkit-2.6.1}/CHANGELOG.md +50 -0
  2. {fplkit-2.5.0 → fplkit-2.6.1}/PKG-INFO +1 -1
  3. {fplkit-2.5.0 → fplkit-2.6.1}/docs/architecture.md +4 -4
  4. {fplkit-2.5.0 → fplkit-2.6.1}/docs/command-reference.md +31 -11
  5. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/_version.py +2 -2
  6. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/providers/_models.py +25 -0
  7. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/providers/anthropic.py +19 -1
  8. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/understat.py +228 -62
  9. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_context.py +80 -2
  10. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_review_summarisation.py +52 -12
  11. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/doctor.py +71 -27
  12. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/doctor_providers.py +11 -3
  13. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/ratings.py +1 -1
  14. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/config/defaults.yaml +1 -1
  15. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/prompts/review.py +35 -12
  16. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/season.py +24 -0
  17. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/returnee_radar.py +7 -3
  18. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/team_ratings.py +61 -6
  19. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/templates/gw_review.md.j2 +9 -4
  20. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_api_understat.py +235 -0
  21. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_doctor.py +196 -9
  22. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_doctor_providers.py +15 -0
  23. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_format.py +85 -1
  24. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_review.py +123 -7
  25. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_paths.py +88 -0
  26. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_providers.py +107 -1
  27. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_report_agent.py +62 -3
  28. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_returnee_radar.py +41 -0
  29. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_review.py +43 -1
  30. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_season.py +21 -0
  31. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_team_ratings.py +127 -1
  32. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/README.md +0 -0
  33. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/TOOLS.md +0 -0
  34. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/build-test-plan/SKILL.md +0 -0
  35. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/build-test-plan/references/plan-template.md +0 -0
  36. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/build-test-plan/references/workspace-env.md +0 -0
  37. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/create-pr/SKILL.md +0 -0
  38. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/SKILL.md +0 -0
  39. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/references/entity-normalisation.md +0 -0
  40. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/references/output-template.md +0 -0
  41. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/references/rules.md +0 -0
  42. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/scripts/_bootstrap.py +0 -0
  43. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/scripts/bench_order.py +0 -0
  44. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/scripts/extract_classic_squad.py +0 -0
  45. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/scripts/normalise_entities.py +0 -0
  46. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/scripts/starting_xi.py +0 -0
  47. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/scripts/transfer_eval.py +0 -0
  48. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/scripts/validate_draft_waivers.py +0 -0
  49. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/preview-ingest/SKILL.md +0 -0
  50. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/release/SKILL.md +0 -0
  51. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/release-notes/SKILL.md +0 -0
  52. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/squad-builder/SKILL.md +0 -0
  53. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/squad-builder/references/output-template.md +0 -0
  54. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/squad-builder/references/rules.md +0 -0
  55. {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/update-gw-prep/SKILL.md +0 -0
  56. {fplkit-2.5.0 → fplkit-2.6.1}/.claude/output-styles/fpl-mate.md +0 -0
  57. {fplkit-2.5.0 → fplkit-2.6.1}/.claude/settings.json +0 -0
  58. {fplkit-2.5.0 → fplkit-2.6.1}/.env.example +0 -0
  59. {fplkit-2.5.0 → fplkit-2.6.1}/.github/workflows/ci.yml +0 -0
  60. {fplkit-2.5.0 → fplkit-2.6.1}/.github/workflows/pr-title.yml +0 -0
  61. {fplkit-2.5.0 → fplkit-2.6.1}/.github/workflows/release.yml +0 -0
  62. {fplkit-2.5.0 → fplkit-2.6.1}/.gitignore +0 -0
  63. {fplkit-2.5.0 → fplkit-2.6.1}/AGENTS.md +0 -0
  64. {fplkit-2.5.0 → fplkit-2.6.1}/CLAUDE.md +0 -0
  65. {fplkit-2.5.0 → fplkit-2.6.1}/CONTRIBUTING.md +0 -0
  66. {fplkit-2.5.0 → fplkit-2.6.1}/LICENSE +0 -0
  67. {fplkit-2.5.0 → fplkit-2.6.1}/README.md +0 -0
  68. {fplkit-2.5.0 → fplkit-2.6.1}/cliff.toml +0 -0
  69. {fplkit-2.5.0 → fplkit-2.6.1}/config/team_ratings_overrides.yaml +0 -0
  70. {fplkit-2.5.0 → fplkit-2.6.1}/docs/custom-analysis.md +0 -0
  71. {fplkit-2.5.0 → fplkit-2.6.1}/docs/fpl-rules.md +0 -0
  72. {fplkit-2.5.0 → fplkit-2.6.1}/docs/images/fpl-player-demo-v1-0.png +0 -0
  73. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/__init__.py +0 -0
  74. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/__init__.py +0 -0
  75. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/action/__init__.py +0 -0
  76. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/action/waiver.py +0 -0
  77. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/analysis/__init__.py +0 -0
  78. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/analysis/bench_order.py +0 -0
  79. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/analysis/captain.py +0 -0
  80. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/analysis/squad_analyzer.py +0 -0
  81. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/analysis/starting_xi.py +0 -0
  82. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/analysis/stats.py +0 -0
  83. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/analysis/transfer_eval.py +0 -0
  84. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/base.py +0 -0
  85. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/common.py +0 -0
  86. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/data/__init__.py +0 -0
  87. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/data/fixture.py +0 -0
  88. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/data/price.py +0 -0
  89. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/data/scout.py +0 -0
  90. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/orchestration/__init__.py +0 -0
  91. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/orchestration/report.py +0 -0
  92. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/__init__.py +0 -0
  93. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/contract.py +0 -0
  94. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/core_insights.py +0 -0
  95. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/dataset_fetcher.py +0 -0
  96. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/football_data.py +0 -0
  97. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/fpl.py +0 -0
  98. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/fpl_draft.py +0 -0
  99. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/historical.py +0 -0
  100. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/historical_types.py +0 -0
  101. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/providers/__init__.py +0 -0
  102. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/providers/_http.py +0 -0
  103. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/providers/openai_compat.py +0 -0
  104. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/providers/perplexity.py +0 -0
  105. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/vaastav.py +0 -0
  106. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/__init__.py +0 -0
  107. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_banner.py +0 -0
  108. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_fines.py +0 -0
  109. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_fines_config.py +0 -0
  110. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_helpers.py +0 -0
  111. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_json.py +0 -0
  112. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_league_recap_data.py +0 -0
  113. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_league_recap_history.py +0 -0
  114. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_league_recap_types.py +0 -0
  115. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_plan_grid.py +0 -0
  116. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_review_analysis.py +0 -0
  117. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_review_classic.py +0 -0
  118. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_review_draft.py +0 -0
  119. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/allocate.py +0 -0
  120. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/captain.py +0 -0
  121. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/chips.py +0 -0
  122. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/credentials.py +0 -0
  123. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/differentials.py +0 -0
  124. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/fdr.py +0 -0
  125. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/fixtures.py +0 -0
  126. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/history.py +0 -0
  127. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/init.py +0 -0
  128. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/intel.py +0 -0
  129. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/league.py +0 -0
  130. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/league_fines.py +0 -0
  131. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/league_recap.py +0 -0
  132. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/player.py +0 -0
  133. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/preview.py +0 -0
  134. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/price_changes.py +0 -0
  135. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/price_history.py +0 -0
  136. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/returnees.py +0 -0
  137. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/review.py +0 -0
  138. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/sell_prices.py +0 -0
  139. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/squad.py +0 -0
  140. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/stats.py +0 -0
  141. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/status.py +0 -0
  142. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/targets.py +0 -0
  143. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/transfer_eval.py +0 -0
  144. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/waivers.py +0 -0
  145. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/xg.py +0 -0
  146. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/config/fixture_predictions.yaml +0 -0
  147. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/config/previews/EXAMPLE.yaml +0 -0
  148. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/config/team_managers.yaml +0 -0
  149. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/constants.py +0 -0
  150. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/models/__init__.py +0 -0
  151. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/models/chip_plan.py +0 -0
  152. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/models/fixture.py +0 -0
  153. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/models/league_history.py +0 -0
  154. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/models/player.py +0 -0
  155. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/models/team.py +0 -0
  156. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/models/types.py +0 -0
  157. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/parsers/__init__.py +0 -0
  158. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/parsers/recommendations.py +0 -0
  159. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/paths.py +0 -0
  160. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/prompts/__init__.py +0 -0
  161. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/prompts/league_recap.py +0 -0
  162. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/prompts/returnees.py +0 -0
  163. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/prompts/scout.py +0 -0
  164. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/scraper/__init__.py +0 -0
  165. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/scraper/fpl_prices.py +0 -0
  166. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/__init__.py +0 -0
  167. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/fixture_predictions.py +0 -0
  168. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/league_history.py +0 -0
  169. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/league_history_counters.py +0 -0
  170. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/league_history_fines.py +0 -0
  171. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/league_history_notes.py +0 -0
  172. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/matchup.py +0 -0
  173. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/player_clubs.py +0 -0
  174. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/player_prior.py +0 -0
  175. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/__init__.py +0 -0
  176. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/constants.py +0 -0
  177. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/data_prep.py +0 -0
  178. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/display.py +0 -0
  179. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/evaluation.py +0 -0
  180. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/ownership.py +0 -0
  181. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/shrinkage.py +0 -0
  182. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/signals.py +0 -0
  183. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/single_gw.py +0 -0
  184. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/value_quality.py +0 -0
  185. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/season_previews.py +0 -0
  186. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/squad_allocator.py +0 -0
  187. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/team_form.py +0 -0
  188. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/team_ratings_prior.py +0 -0
  189. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/templates/gw_league_recap.md.j2 +0 -0
  190. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/templates/gw_preview.md.j2 +0 -0
  191. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/utils/__init__.py +0 -0
  192. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/utils/files.py +0 -0
  193. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/utils/gameweek.py +0 -0
  194. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/utils/markdown.py +0 -0
  195. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/utils/teams.py +0 -0
  196. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/utils/text.py +0 -0
  197. {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/utils/time.py +0 -0
  198. {fplkit-2.5.0 → fplkit-2.6.1}/pyproject.toml +0 -0
  199. {fplkit-2.5.0 → fplkit-2.6.1}/pyrightconfig.json +0 -0
  200. {fplkit-2.5.0 → fplkit-2.6.1}/scripts/backtest_early_season_prior_blend.py +0 -0
  201. {fplkit-2.5.0 → fplkit-2.6.1}/scripts/calibrate_promoted_prior.py +0 -0
  202. {fplkit-2.5.0 → fplkit-2.6.1}/scripts/calibrate_quality_ceilings.py +0 -0
  203. {fplkit-2.5.0 → fplkit-2.6.1}/tests/__init__.py +0 -0
  204. {fplkit-2.5.0 → fplkit-2.6.1}/tests/conftest.py +0 -0
  205. {fplkit-2.5.0 → fplkit-2.6.1}/tests/fixtures/classic_squad_fixture.md +0 -0
  206. {fplkit-2.5.0 → fplkit-2.6.1}/tests/fixtures/recommendations_fixture.md +0 -0
  207. {fplkit-2.5.0 → fplkit-2.6.1}/tests/fixtures/validate_draft_waivers/recs_clean.md +0 -0
  208. {fplkit-2.5.0 → fplkit-2.6.1}/tests/fixtures/validate_draft_waivers/squad_grid.json +0 -0
  209. {fplkit-2.5.0 → fplkit-2.6.1}/tests/fixtures/validate_draft_waivers/waivers.json +0 -0
  210. {fplkit-2.5.0 → fplkit-2.6.1}/tests/fixtures/waiver_ranked_golden.json +0 -0
  211. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_agents_action.py +0 -0
  212. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_agents_analysis.py +0 -0
  213. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_agents_analysis_squad.py +0 -0
  214. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_agents_base.py +0 -0
  215. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_agents_data.py +0 -0
  216. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_agents_data_price.py +0 -0
  217. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_agents_orchestration.py +0 -0
  218. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_allocate.py +0 -0
  219. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_api_client.py +0 -0
  220. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_api_core_insights.py +0 -0
  221. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_api_dataset_fetcher.py +0 -0
  222. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_api_draft.py +0 -0
  223. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_api_football_data.py +0 -0
  224. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_api_historical.py +0 -0
  225. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_api_vaastav.py +0 -0
  226. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_bench_order_script.py +0 -0
  227. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_calibrate_quality_ceilings.py +0 -0
  228. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_classic_squad_extraction.py +0 -0
  229. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_allocate.py +0 -0
  230. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_banner.py +0 -0
  231. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_captain.py +0 -0
  232. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_chips.py +0 -0
  233. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_context_settings.py +0 -0
  234. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_credentials.py +0 -0
  235. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_custom_analysis.py +0 -0
  236. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_differentials.py +0 -0
  237. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_failure_parity.py +0 -0
  238. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_failure_streams.py +0 -0
  239. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_fdr.py +0 -0
  240. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_fdr_blanks.py +0 -0
  241. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_fixtures.py +0 -0
  242. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_format_suppression.py +0 -0
  243. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_fpl_block.py +0 -0
  244. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_history.py +0 -0
  245. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_init.py +0 -0
  246. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_intel.py +0 -0
  247. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_json.py +0 -0
  248. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_json_contract.py +0 -0
  249. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_league.py +0 -0
  250. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_league_fines.py +0 -0
  251. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_league_recap.py +0 -0
  252. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_plan_grid.py +0 -0
  253. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_player.py +0 -0
  254. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_preview.py +0 -0
  255. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_price_changes.py +0 -0
  256. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_price_history.py +0 -0
  257. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_returnees.py +0 -0
  258. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_sell_prices.py +0 -0
  259. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_settings_warning.py +0 -0
  260. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_squad.py +0 -0
  261. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_stats.py +0 -0
  262. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_status.py +0 -0
  263. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_targets.py +0 -0
  264. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_transfer_eval.py +0 -0
  265. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_waivers.py +0 -0
  266. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_warnings_stderr.py +0 -0
  267. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_xg.py +0 -0
  268. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_common.py +0 -0
  269. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_common_scoring.py +0 -0
  270. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_data_prep_harmonisation.py +0 -0
  271. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_dependencies.py +0 -0
  272. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_doc_consistency.py +0 -0
  273. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_fines.py +0 -0
  274. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_fines_config.py +0 -0
  275. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_fixture_predictions.py +0 -0
  276. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_fpl_prices.py +0 -0
  277. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_gw_prep_script_bootstrap.py +0 -0
  278. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_init.py +0 -0
  279. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_init_fines.py +0 -0
  280. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_league_history_counters.py +0 -0
  281. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_league_history_fines.py +0 -0
  282. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_league_history_notes.py +0 -0
  283. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_league_history_store.py +0 -0
  284. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_league_recap.py +0 -0
  285. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_matchup_service.py +0 -0
  286. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_models.py +0 -0
  287. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_normalise_entities.py +0 -0
  288. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_output_style_sync.py +0 -0
  289. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_player_clubs.py +0 -0
  290. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_player_prior.py +0 -0
  291. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_player_resolution.py +0 -0
  292. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_preseason.py +0 -0
  293. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_prompt_tables.py +0 -0
  294. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_recommendations_parser.py +0 -0
  295. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_report_paths.py +0 -0
  296. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_rolling_pts_per_m.py +0 -0
  297. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_scoring.py +0 -0
  298. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_scraper.py +0 -0
  299. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_season_awareness.py +0 -0
  300. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_season_previews.py +0 -0
  301. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_sell_prices.py +0 -0
  302. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_skill_fdr_quality_signals.py +0 -0
  303. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_skill_stats_thresholds.py +0 -0
  304. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_squad_allocator.py +0 -0
  305. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_starting_xi_agent.py +0 -0
  306. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_starting_xi_script.py +0 -0
  307. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_stats.py +0 -0
  308. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_team_ratings_prior.py +0 -0
  309. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_transfer_eval_agent.py +0 -0
  310. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_transfer_eval_script.py +0 -0
  311. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_utils_gameweek.py +0 -0
  312. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_utils_markdown.py +0 -0
  313. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_utils_text.py +0 -0
  314. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_utils_time.py +0 -0
  315. {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_validate_draft_waivers.py +0 -0
@@ -1,6 +1,56 @@
1
1
  # Changelog
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
+ ## [2.6.0] - 2026-09-07
5
+
6
+ ### Bug Fixes
7
+
8
+ - drop ratings update's dead -v hint for a kept prior (#311)
9
+ - carry the --format json envelope through a relative FPL_CLI_* rejection (#312)
10
+ - derive `fpl doctor`'s season from GW1's deadline like `fpl status` (#314)
11
+ - warn in the saved review when the summary came back empty (#313)
12
+
13
+ ### Features
14
+
15
+ - join Understat rows for players the two sources spell differently (#315)
16
+
17
+ ## [2.5.0] - 2026-09-07
18
+
19
+ ### Bug Fixes
20
+
21
+ - stop the fpl-mate output style calling ep_next prior-informed (#261)
22
+ - restore xG enrichment for players with an apostrophe in their name (#268)
23
+ - keep coverage readable when one ledger file is damaged (#271)
24
+ - stop the review scrubber deleting sentences naming full names (#270)
25
+ - recap the last completed gameweek once the next one has kicked off (#274)
26
+ - stop fpl xg table output crashing when custom analysis is off (#275)
27
+ - cap httpx, pydantic and click below their next majors (#278)
28
+ - stop a truncated LLM summary being saved without a warning (#276)
29
+ - stop a draft replay erasing the league positions it recorded (#280)
30
+ - validate FPL_CLI_DATA_DIR and FPL_CLI_CACHE_DIR eagerly like FPL_CLI_CONFIG_DIR (#281)
31
+ - raise the click floor to 8.2 so the test suite can capture stderr (#283)
32
+ - collapse a league-wide captain tie into one recap line (#282)
33
+ - fill the blank Source column in a review's Standout Performers (#284)
34
+ - exit 1 with the real reason when review or recap cannot run (#285)
35
+ - point doctor's draft ID fix hints at `fpl init` (#288)
36
+ - keep the current table's points out of a replayed gameweek's recap (#293)
37
+ - report a failure the same way whatever the output format (#292)
38
+ - keep league positions when one manager's replay data is partial (#295)
39
+ - stop a replay renaming the player a stored red-card fine names (#290)
40
+ - ground the review's "Next Week" advice in next gameweek's fixtures (#291)
41
+ - rebuild the ratings prior when a football-data key appears (#289)
42
+ - record the club a player played for in a replayed gameweek (#296)
43
+ - keep `review --summarise` running when an LLM key is missing (#297)
44
+ - derive season labels from GW1's deadline instead of the clock (#299)
45
+ - correct award move direction and free-agent mislabelling (#302)
46
+
47
+ ### Features
48
+
49
+ - flag stale quality-score calibration in `fpl doctor` (#298)
50
+ - hand the league-recap editorial every manager's transfers (#300)
51
+ - give the draft recap editorial every waiver and free-agent move (#303)
52
+ - give the season-opener recap each manager's prior FPL seasons (#304)
53
+
4
54
  ## [2.4.1] - 2026-09-05
5
55
 
6
56
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fplkit
3
- Version: 2.5.0
3
+ Version: 2.6.1
4
4
  Summary: CLI tool for Fantasy Premier League analysis - classic and draft formats
5
5
  Project-URL: Homepage, https://github.com/rossgroomio/fpl-cli
6
6
  Project-URL: Repository, https://github.com/rossgroomio/fpl-cli
@@ -434,7 +434,7 @@ classDiagram
434
434
 
435
435
  All providers share the `LLMResponse` contract. `OpenAICompatProvider` supports OpenAI, Groq, Together, Ollama via configurable `base_url`. Provider selection configured in settings.
436
436
 
437
- `stop_reason` carries the provider's own verdict on why generation ended, verbatim in whichever vocabulary it used — Anthropic's `stop_reason`, an OpenAI-compatible API's `finish_reason`. `stopped_early` reads it against `NORMAL_STOP_REASONS`, a single set covering both dialects (the vocabularies do not collide), and every provider announces an abnormal one at WARNING via `log_abnormal_stop` — the same reason `_http` announces its retries there, so a truncation is visible on any command that talks to an LLM. `None` means the provider said nothing, which is never treated as evidence of truncation. Without this a cut-off response and a complete one were indistinguishable downstream, which is how a saved gameweek review shipped missing a whole format's verdict at exit 0 (#266).
437
+ `stop_reason` carries the provider's own verdict on why generation ended, verbatim in whichever vocabulary it used — Anthropic's `stop_reason`, an OpenAI-compatible API's `finish_reason`. `stopped_early` reads it against `NORMAL_STOP_REASONS`, a single set covering both dialects (the vocabularies do not collide), and every provider announces an abnormal one at WARNING via `log_abnormal_stop` — the same reason `_http` announces its retries there, so a truncation is visible on any command that talks to an LLM. `None` means the provider said nothing, which is never treated as evidence of truncation. Without this a cut-off response and a complete one were indistinguishable downstream, which is how a saved gameweek review shipped missing a whole format's verdict at exit 0 (#266). `log_textless_response` is its companion for the other half of that failure: a reasoning model that reaches the ceiling mid-thought returns content with no text block in it at all, so the concatenated prose is `""` — honest, but indistinguishable from a model with nothing to say until the non-text blocks that did arrive are named (#306). Unlike `log_abnormal_stop` this one is Anthropic-only, because only the Messages API returns typed content blocks — an OpenAI-compatible reply carries a plain `message.content` string, so there is nothing to name when it comes back empty. What catches an empty answer whatever produced it is `check_synthesis_completeness`'s `empty` flag, one layer along, which reads the string the report would carry rather than the envelope it arrived in.
438
438
 
439
439
  Both HTTP providers post through `_http.post_json_with_retry`, the one place their error handling lives: it retries an HTTP 429 with `RetryPolicy` (exponential backoff with jitter, `Retry-After` honoured, the whole wait bounded by one budget, each retry announced at WARNING) and raises `RateLimitError` — a `ProviderError` subclass carrying the server's `retry_after` — once the attempts are spent, and turns every other error status, a timeout, or a non-JSON body into a sanitised `ProviderError`. That is the same 429-is-transient distinction `DatasetFetcher` draws for the historical datasets. The module also holds `QueryPacer`, which keeps request starts a configured interval apart for a caller that fires several queries at once; `fpl returnees --enrich` pairs it with an in-flight cap, reads the typed error to re-query only the rate-limited subset of its shortlist once, and reports a still-refused player as rate-limited rather than unanswered.
440
440
 
@@ -555,7 +555,7 @@ fpl_cli/
555
555
  │ ├── historical.py # HistoricalDataProvider (composition: vaastav + Core-Insights) + historical_season_windows() (the disjoint source allocation) + merge_season_histories()
556
556
  │ ├── football_data.py # FootballDataClient (standings, match results)
557
557
  │ └── providers/ # LLM provider abstraction
558
- │ ├── _models.py # LLMResponse (incl. stop_reason/stopped_early), TokenUsage, NORMAL_STOP_REASONS, log_abnormal_stop, ProviderError, RateLimitError
558
+ │ ├── _models.py # LLMResponse (incl. stop_reason/stopped_early), TokenUsage, NORMAL_STOP_REASONS, log_abnormal_stop, log_textless_response, ProviderError, RateLimitError
559
559
  │ ├── _http.py # Shared HTTP plumbing: 429 backoff (RetryPolicy, post_with_retry), the providers' one error path (post_json_with_retry), QueryPacer for batching callers
560
560
  │ ├── anthropic.py # AnthropicProvider
561
561
  │ ├── openai_compat.py # OpenAICompatProvider (OpenAI, Groq, Together, Ollama)
@@ -572,7 +572,7 @@ fpl_cli/
572
572
  │ │ ├── shrinkage.py # Single-GW early-season shrinkage + the shared hold-out helpers
573
573
  │ │ └── data_prep.py # ScoringContext / ScoringData + prepare_scoring_data()
574
574
  │ ├── player_prior.py # Player prior (Bayesian early-season confidence)
575
- │ ├── team_ratings.py # TeamRatingsService + Calculator (1-7 scale)
575
+ │ ├── team_ratings.py # TeamRatingsService + Calculator (1-7 scale). The service judges the file's `metadata.season` stamp against the season it is told is in progress, discarding a previous season's ratings outright (they rate relegated clubs, and serving them is worse than serving none) — which means the season it uses has to be the resolved one, not the clock: an overrunning season had its own live ratings thrown away from 1 July, dropping every fixture to a neutral 4.0 mid-season (#318). `ensure_fresh(client)` resolves it from GW1's deadline before anything else and re-reads if a property access already forced a load on the clock; `fpl doctor` passes the label it resolved; writes are stamped with the same season, so a file written during an overrun is not stamped into the future
576
576
  │ ├── team_ratings_prior.py # Previous-season prior + Bayesian blending (cutoff GW12); every pool it reads (the PL pool from either source, the Championship division) takes full-season records only, and every club's basis is written to the cache as `inputs`. The cache is invalidated by the methodology version, the league's club list, and a one-directional provenance check (`_inputs_have_improved`) that rebuilds only when an input the cached run lacked is available now — so setting `FOOTBALL_DATA_API_KEY` reaches the promoted sides without the file being deleted (#112). A rebuild is then held to `_is_worse_than_cache` (source rank and count of clubs on an estimate) before it may replace the cached prior, and `_record_attempt` stamps the file either way so a down provider costs one retry, not one per command
577
577
  │ ├── matchup.py # Fixture matchup scoring (0-10)
578
578
  │ ├── fixture_predictions.py # BGW/DGW predictions from YAML + live detection, plus the point-in-time "did his club have a fixture, and how many" read a past gameweek needs
@@ -602,7 +602,7 @@ fpl_cli/
602
602
  ├── scraper/
603
603
  │ └── fpl_prices.py # FPLPriceScraper (needs FPL_EMAIL/FPL_PASSWORD; behind TLS-inspecting proxies: FPL_BROWSER_IGNORE_CERTS=1 for cert MITM, or FPL_BROWSER_EXECUTABLE/CHANNEL/ARGS to swap the browser when the ClientHello itself is rejected)
604
604
  ├── paths.py # SHIPPED_CONFIG_DIR, TEMPLATE_DIR, user_config_dir(), user_data_dir(), user_cache_dir() — each user_* dir overridable via FPL_CLI_CONFIG_DIR / FPL_CLI_DATA_DIR / FPL_CLI_CACHE_DIR (absolute paths only; a relative one raises UserDirError rather than resolving against the cwd)
605
- ├── season.py # season_label() (+ vaastav_season() alias), previous_season_year()/previous_season_label() (the newest *completed* season, one expression for the player prior, the calibration script's default and doctor's frozen-anchor check), understat_season(), core_insights_season(), TOTAL_GAMEWEEKS, CHIP_SPLIT_GW, PROMOTED_CLUBS_PER_SEASON. season_year_from_gameweeks() derives the year from GW1's deadline rather than the July-cutover clock (#91); resolve_season_year() composes it with the clock — GW1's year while its season is still live, the clock's once that season has fully concluded and the calendar has moved past it (the close-season gap before the next one's fixtures publish, when bootstrap-static keeps serving the finished season's events untouched). FPLClient.get_season_year() wraps resolve_season_year(), and review/league-recap/preview/status pass the result through explicitly so a season overrunning the cutover (2019-20, delayed into July 2020) still resolves to the year it started; league-fines, which reads the ledger with no network, instead defaults to services/league_history.py's most_recent_captured_season() so its default agrees with whatever league-recap actually wrote
605
+ ├── season.py # season_label() (+ vaastav_season() alias), previous_season_year()/previous_season_label() (the newest *completed* season, one expression for the player prior, the calibration script's default and doctor's frozen-anchor check), understat_season(), core_insights_season(), TOTAL_GAMEWEEKS, CHIP_SPLIT_GW, PROMOTED_CLUBS_PER_SEASON. season_year_from_gameweeks() derives the year from GW1's deadline rather than the July-cutover clock (#91); resolve_season_year() composes it with the clock — GW1's year while its season is still live, the clock's once that season has fully concluded and the calendar has moved past it (the close-season gap before the next one's fixtures publish, when bootstrap-static keeps serving the finished season's events untouched). FPLClient.get_season_year() wraps resolve_season_year(), and review/league-recap/preview/status/doctor pass the result through explicitly so a season overrunning the cutover (2019-20, delayed into July 2020) still resolves to the year it started -- doctor falls back to the clock only when the API is unreachable, and `fpl doctor --providers` reads the season off the bootstrap its FPL probe already fetched rather than downloading it twice. is_previous_season_year() is the ordered counterpart the staleness checks compare a timestamp's cutover year with: during an overrun a July timestamp lands a year *ahead* of the season it was written in, so exact comparison would call live data a previous season's (#308); league-fines, which reads the ledger with no network, instead defaults to services/league_history.py's most_recent_captured_season() so its default agrees with whatever league-recap actually wrote
606
606
  ├── constants.py # MIN_MINUTES_FOR_PER90
607
607
  └── utils/
608
608
  ├── gameweek.py # is_opening_gameweek(gw) — shared GW1 check (transfers, waivers and league tables don't exist yet)
@@ -96,6 +96,15 @@ about: the block is read on nearly every command, so the envelope naming it can
96
96
  from `fpl status` or `fpl league-recap` as readily as from `fpl league-fines`, and the
97
97
  message names the rule and the valid set.
98
98
 
99
+ A relative `FPL_CLI_CONFIG_DIR` / `FPL_CLI_DATA_DIR` / `FPL_CLI_CACHE_DIR` override is
100
+ rejected before any command runs, so this one case has no parsed `--format` to read when it
101
+ fails — the CLI scans the raw command line for `--format json` instead. The envelope's
102
+ `command` names your subcommand where that is unambiguous (`fpl status`, or a bare group like
103
+ `fpl chips`), and falls back to `"fpl"` for a subgroup's own subcommand (`fpl chips timing`,
104
+ `fpl squad grid`, `fpl intel show`) or a line that was nothing but options — those commands
105
+ don't name their own envelope after either word you typed, and guessing wrong would be worse
106
+ than saying so.
107
+
99
108
  **Table mode splits the same way.** Without `--format json` the output you asked for goes
100
109
  to stdout and everything else goes to stderr — warnings, progress notices, and the reason
101
110
  a command exited nonzero. `fpl squad grid 2>/dev/null` prints a grid or prints nothing;
@@ -403,7 +412,7 @@ A club that has played only one venue is rated on it. Once GW1 finishes, every c
403
412
 
404
413
  Early-season results are shrunk toward that previous-season prior, by the automatic refresh and by `fpl ratings update` alike: a one-gameweek sample carries 1/7 of the weight, six gameweeks half, and the prior drops out entirely once GW12 has completed. Shrinkage is gated on how far the season has run, so a narrow window late on is not blended -- `--since-gw 30` at GW34 saves recent form alone. Inside the early-season window the weight follows the size of that window rather than the gameweek number, so `--since-gw 8` at GW10 is weighted as three gameweeks of evidence. Blended files are stamped `calculated_blended` / `understat_xg_blended` / `auto_calculated_blended` so `fpl ratings` shows that shrinkage was applied. While the sample is shorter than six gameweeks the prior still carries most of the weight, and every fixture-difficulty view says so - `Ratings are mostly last season's prior - 1 gameweek of results carries 14% of the weight`. That replaces the pre-season estimate warning, which stops applying the moment a gameweek completes.
405
414
 
406
- Ratings are tied to the season that produced them. A file carried across a season boundary is ignored rather than served, because it rates the three relegated clubs and knows nothing about the three promoted ones. Ratings that cover the wrong set of clubs are called out by name — `team_ratings.yaml is missing COV, HUL, IPS and still rates BUR, WHU, WOL` — which catches a rollover that a "days old" check cannot: a file rebuilt in early August is new by date and still describes last season's league.
415
+ Ratings are tied to the season that produced them. A file carried across a season boundary is ignored rather than served, because it rates the three relegated clubs and knows nothing about the three promoted ones. Which season is "in progress" comes from GW1's deadline, not the July clock, so a season overrunning 1 July keeps its own ratings instead of having them discarded with a third of the run-in still to play. Ratings that cover the wrong set of clubs are called out by name — `team_ratings.yaml is missing COV, HUL, IPS and still rates BUR, WHU, WOL` — which catches a rollover that a "days old" check cannot: a file rebuilt in early August is new by date and still describes last season's league.
407
416
 
408
417
  See [Team Ratings](custom-analysis.md#team-ratings) for calculation methodology, axes, early-season blending, pre-season estimates, and manual overrides.
409
418
 
@@ -556,7 +565,7 @@ These metrics appear in `fpl player`, `fpl xg`, and gameweek reports. If Underst
556
565
 
557
566
  **When a whole club fails to join.** FPL and Understat spell some clubs differently, so a `TEAM_NAME_MAP` gap costs one club's entire squad its npxG, xGChain and every score built on them, while the other 19 look fine. The commands that enrich from the live league payload — `fpl stats --value`, `fpl player`, `fpl allocate`, `fpl xg`, `fpl targets`, `fpl differentials` — report it once per club: on stderr in table mode, and as an `understat_team_unmatched` entry in JSON `metadata.warnings`, one per club, naming both the FPL club and the Understat name it was mapped to. `fpl doctor --providers` checks the same thing ahead of time. A club missing from a *past* season's payload is not this: `fpl returnees` scores a player's current club against the season they played, so the three clubs promoted since are absent from it every year by definition, and that stays a debug line rather than a warning about a map that is fine.
558
567
 
559
- **When one player fails to join.** A player whose club Understat *does* carry can still fail to match on name, and that is not always a defect: Understat has not ingested the gameweek in progress, spells a transliterated name differently, or simply has no row for the player. Those read as `null` quality and value scores rather than as zeros, so the player is ranked as "no data" rather than as bad. A few percent of a club's players missing this way is normal and stays a debug line naming each one with a running rate (`No Understat row matched ... — 3 of 351 FPL players with minutes unmatched so far this run`); the rate is what to watch, since a jump in it means the two sources have stopped agreeing about names rather than that one more cameo went un-ingested. `fpl doctor --providers` reports the same rate as an `Understat name join` row, which is where to look for it without turning on debug logging. Understat serves apostrophes HTML-escaped (`O'Shea`), which used to fail every apostrophe name silently; the payload is now decoded when it is fetched.
568
+ **When one player fails to join.** A player whose club Understat *does* carry can still fail to match on name, and that is not always a defect: Understat has not ingested the gameweek in progress, names the player outright differently (FPL's `A.Becker` is Understat's `Alisson`), or simply has no row for them. Those read as `null` quality and value scores rather than as zeros, so the player is ranked as "no data" rather than as bad. A name the two sources merely *spell* differently does join, through a last-resort pass that stays inside the player's club and needs their minutes to agree and no second candidate: a transliteration variant (`Yarmoliuk` and `Yehor Yarmolyuk`), a double-barrelled or middle name one source drops (`Gannon-Doak` and `Ben Doak`, `Walle Egeli` and `Sindre Egeli`), or a mononym inside a longer name (`Jair Cunha` and `Jair`). A shared word alone is never enough — `João Pedro` and `Pedro Neto` share one — so a player whose row is genuinely missing stays `null` rather than wearing a club-mate's numbers. A few percent of a club's players missing this way is normal and stays a debug line naming each one with a running rate (`No Understat row matched ... — 3 of 351 FPL players with minutes unmatched so far this run`); the rate is what to watch, since a jump in it means the two sources have stopped agreeing about names rather than that one more cameo went un-ingested. `fpl doctor --providers` reports the same rate as an `Understat name join` row, which is where to look for it without turning on debug logging. Understat serves apostrophes HTML-escaped (`O'Shea`), which used to fail every apostrophe name silently; the payload is now decoded when it is fetched.
560
569
 
561
570
  #### Quality and Value Scores
562
571
 
@@ -769,15 +778,24 @@ on a run with no fixture data, is reported alongside the completeness findings b
769
778
  stderr, and as a warning callout in the saved report.
770
779
 
771
780
  **Incomplete summaries:** the personal analysis is checked against the sections its own prompt
772
- asked for before anything is written. A response that is missing a `## ` heading, ends without
773
- terminal punctuation, or that the provider reports it stopped early on is retried **once** — and
774
- if the second attempt is no better, the first one stands and the run says so on stderr, naming
775
- what is missing. Under `--debug` the detail lands in `data/debug/synthesis_corrections.txt`
776
- alongside `research_corrections.txt`. A saved report generated from such a response carries the
777
- same list as a warning callout above the summary, so a verdict the model dropped never reads as
778
- one deliberately omitted. A verdict for a format the run has no squad data for is not counted as
781
+ asked for before anything is written. A response that comes back empty, is missing a `## `
782
+ heading, ends without terminal punctuation, or that the provider reports it stopped early on is
783
+ retried **once** — and if the second attempt is no better, the less damaged of the two stands
784
+ and the run says so on stderr, naming what is missing. Under `--debug` the detail lands in
785
+ `data/debug/synthesis_corrections.txt` alongside `research_corrections.txt`. A saved report
786
+ generated from such a response carries the same list as a warning callout above the summary, so
787
+ a verdict the model dropped never reads as one deliberately omitted. The callout does not depend
788
+ on there being a summary to qualify: a response that returned no text at all — the shape a hard
789
+ `max_tokens` truncation takes when the model was still thinking at the ceiling — is the one that
790
+ most needs explaining, and is reported as a single "the response is empty" finding rather than
791
+ as every section it happens to be short of. A verdict for a format the run has no squad data for is not counted as
779
792
  missing — the prompt tells the model to analyse only the format it was given.
780
793
 
794
+ A call that never lands at all — a timeout, a 500, a 429 that outlasted its retries, a revoked
795
+ key — is reported through the same callout, naming the failure. The run still exits **0** with
796
+ the rest of the review intact, but the saved report says the analysis was attempted and lost
797
+ rather than leaving a reader to wonder whether the template ever had one.
798
+
781
799
  **Blanks and doubles:** a zero from a player whose club had no fixture is marked `[BGW]` rather
782
800
  than read as a choice that failed, and such a player is kept off the Blankers list entirely; a
783
801
  player whose club played twice is marked `[DGW]`. Which clubs those were is read off the
@@ -1278,7 +1296,7 @@ Rolling a setup into a new season silently invalidates IDs and per-team files: a
1278
1296
 
1279
1297
  **Data files:**
1280
1298
 
1281
- - `team_ratings.yaml` — season stamp and team set vs the live league (problems here are stale, not broken: the ratings service already ignores or rebuilds bad files)
1299
+ - `team_ratings.yaml` — season stamp and team set vs the live league, judged against the same resolved season the report names (problems here are stale, not broken: the ratings service already ignores or rebuilds bad files)
1282
1300
  - `team_managers.yaml` — merged shipped + user copy covers exactly the current twenty clubs
1283
1301
  - `previews/` — optional season preview intel: a file for a club not in the current league is flagged (it loads and inflates the coverage gate), and files the loader skipped (previous season, malformed) are surfaced with `fpl intel` as the follow-up
1284
1302
  - `team_finances.json` — `scraped_at` falls within the current season
@@ -1291,6 +1309,8 @@ Rolling a setup into a new season silently invalidates IDs and per-team files: a
1291
1309
 
1292
1310
  **Environment** — which directory each of config/data/cache resolved to and whether an `FPL_CLI_*` override is in effect, plus whether `settings.yaml` exists.
1293
1311
 
1312
+ **Which season it is** — the season doctor reports in `metadata.season`, and judges "a previous season's" against, comes from GW1's `deadline_time` where the API answers and the July cutover only where it does not — the same rule `fpl status` uses, so the two commands cannot name different seasons for the same moment. A season that overruns 1 July (2019-20, COVID-delayed) therefore keeps its own label in both, and a `team_finances.json` scraped during that overrun reads as current rather than as a previous season's.
1313
+
1294
1314
  Each finding is classified as **broken** (wrong answers now — fix it today), **stale** (self-corrects or needs one routine refresh), **skipped** (not configured/present), or **unchecked** (API unreachable). Exits non-zero when anything is broken, so it can gate scripts.
1295
1315
 
1296
1316
  **`--providers`** checks the external data sources instead of the local setup. None of them version anything, and the tool degrades gracefully everywhere, so upstream drift otherwise surfaces as plausible but wrong output — a renamed stat field zeroes every player's xG without an error. Each probe asserts shape and volume, not just reachability, and where a column check is not the contract it runs the parser itself:
@@ -1398,7 +1418,7 @@ llm:
1398
1418
  model: claude-sonnet-5
1399
1419
  timeout: 60
1400
1420
  query_defaults:
1401
- max_tokens: 4096
1421
+ max_tokens: 8000
1402
1422
  ```
1403
1423
 
1404
1424
  ### LLM Providers
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.5.0'
22
- __version_tuple__ = version_tuple = (2, 5, 0)
21
+ __version__ = version = '2.6.1'
22
+ __version_tuple__ = version_tuple = (2, 6, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ from collections.abc import Sequence
6
7
  from dataclasses import dataclass, field
7
8
 
8
9
  logger = logging.getLogger(__name__)
@@ -97,3 +98,27 @@ def log_abnormal_stop(response: LLMResponse, label: str) -> None:
97
98
  "the response may be cut off",
98
99
  label, response.stop_reason, response.usage.output_tokens,
99
100
  )
101
+
102
+
103
+ def log_textless_response(
104
+ response: LLMResponse, label: str, non_text_blocks: Sequence[str],
105
+ ) -> None:
106
+ """Announce a response whose content was blocks, none of them text.
107
+
108
+ A model that reasons before it answers emits its thinking first, so a
109
+ ceiling reached mid-thought returns content with no prose block in it at
110
+ all. The concatenated text is then `""` -- honest, but indistinguishable
111
+ downstream from a model that had nothing to say. Naming the blocks that
112
+ did arrive is what separates the two for anyone reading stderr (#306).
113
+
114
+ `non_text_blocks` is the types of the blocks that were *not* text, so an
115
+ answer of nothing but an empty text block stays silent: that is a model
116
+ with nothing to say, which is the case this has nothing to add to.
117
+ `log_abnormal_stop` has already spoken if it was cut off.
118
+ """
119
+ if response.content or not non_text_blocks:
120
+ return
121
+ logger.warning(
122
+ "%s returned no text content -- the response carried only %s block(s)",
123
+ label, ", ".join(sorted(set(non_text_blocks))),
124
+ )
@@ -8,7 +8,13 @@ from typing import Any, ClassVar, Self
8
8
  import httpx
9
9
 
10
10
  from fpl_cli.api.providers._http import RetryPolicy, post_json_with_retry
11
- from fpl_cli.api.providers._models import LLMResponse, ProviderError, TokenUsage, log_abnormal_stop
11
+ from fpl_cli.api.providers._models import (
12
+ LLMResponse,
13
+ ProviderError,
14
+ TokenUsage,
15
+ log_abnormal_stop,
16
+ log_textless_response,
17
+ )
12
18
 
13
19
  _BASE_URL = "https://api.anthropic.com/v1"
14
20
  _PROVIDER_LABEL = "Anthropic"
@@ -81,10 +87,21 @@ class AnthropicProvider:
81
87
  label=_PROVIDER_LABEL, policy=self.RETRY_POLICY,
82
88
  )
83
89
 
90
+ # Only text blocks carry prose, but the types that were *not* text are
91
+ # worth keeping: a ceiling reached while the model was still thinking
92
+ # returns a lone `thinking` block, and the empty string that falls out
93
+ # of this loop is otherwise indistinguishable from a model with
94
+ # nothing to say (#306). Text blocks are deliberately not collected --
95
+ # a response of nothing but an empty text block *is* a model with
96
+ # nothing to say, and naming "text" as the reason there was no text
97
+ # would contradict itself.
84
98
  content = ""
99
+ non_text_blocks: list[str] = []
85
100
  for block in data.get("content", []):
86
101
  if block.get("type") == "text":
87
102
  content += block.get("text", "")
103
+ else:
104
+ non_text_blocks.append(str(block.get("type")))
88
105
 
89
106
  # Normalise usage: Anthropic returns input_tokens/output_tokens directly
90
107
  raw_usage = data.get("usage", {})
@@ -105,6 +122,7 @@ class AnthropicProvider:
105
122
  stop_reason=data.get("stop_reason") or None,
106
123
  )
107
124
  log_abnormal_stop(response, _PROVIDER_LABEL)
125
+ log_textless_response(response, _PROVIDER_LABEL, non_text_blocks)
108
126
  return response
109
127
 
110
128
  def post_process(self, content: str) -> str: