fplkit 2.7.1__tar.gz → 2.7.3__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.
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/TOOLS.md +1 -1
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/gw-prep/references/rules.md +1 -1
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/release/SKILL.md +1 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/squad-builder/references/rules.md +1 -1
- {fplkit-2.7.1 → fplkit-2.7.3}/.claude/output-styles/fpl-mate.md +1 -1
- {fplkit-2.7.1 → fplkit-2.7.3}/CHANGELOG.md +15 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/PKG-INFO +7 -1
- {fplkit-2.7.1 → fplkit-2.7.3}/README.md +5 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/docs/architecture.md +7 -6
- {fplkit-2.7.1 → fplkit-2.7.3}/docs/command-reference.md +42 -9
- {fplkit-2.7.1 → fplkit-2.7.3}/docs/custom-analysis.md +1 -1
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/_version.py +2 -2
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/agents/orchestration/report.py +43 -27
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/api/providers/anthropic.py +1 -1
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/_fines.py +2 -2
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/_helpers.py +45 -1
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/_league_recap_data.py +149 -15
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/_league_recap_types.py +45 -1
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/_review_classic.py +160 -150
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/_review_draft.py +173 -145
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/_review_summarisation.py +16 -15
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/allocate.py +2 -2
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/league_recap.py +114 -5
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/waivers.py +2 -1
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/config/defaults.yaml +1 -1
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/models/league_history.py +17 -7
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/prompts/league_recap.py +205 -13
- fplkit-2.7.3/fpl_cli/prompts/league_recap_checks.py +923 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/league_history_counters.py +145 -62
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/league_history_notes.py +90 -38
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/scoring/constants.py +7 -2
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/scoring/single_gw.py +1 -1
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/templates/gw_league_recap.md.j2 +13 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/templates/gw_review.md.j2 +38 -14
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/utils/markdown.py +25 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/utils/text.py +21 -6
- {fplkit-2.7.1 → fplkit-2.7.3}/pyproject.toml +3 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_league_recap.py +409 -3
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_review.py +490 -3
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_waivers.py +9 -0
- fplkit-2.7.3/tests/test_formation_guidance_sync.py +114 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_league_history_counters.py +171 -26
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_league_history_notes.py +148 -9
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_league_recap.py +1174 -27
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_preseason.py +6 -1
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_providers.py +8 -8
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_report_agent.py +177 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_review.py +68 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_scoring.py +28 -3
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_utils_markdown.py +34 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/README.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/build-test-plan/SKILL.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/build-test-plan/references/plan-template.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/build-test-plan/references/workspace-env.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/create-pr/SKILL.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/gw-prep/SKILL.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/gw-prep/references/entity-normalisation.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/gw-prep/references/output-template.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/gw-prep/scripts/_bootstrap.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/gw-prep/scripts/bench_order.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/gw-prep/scripts/extract_classic_squad.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/gw-prep/scripts/normalise_entities.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/gw-prep/scripts/starting_xi.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/gw-prep/scripts/transfer_eval.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/gw-prep/scripts/validate_draft_waivers.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/preview-ingest/SKILL.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/release-notes/SKILL.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/squad-builder/SKILL.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/squad-builder/references/output-template.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.agents/skills/update-gw-prep/SKILL.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.claude/settings.json +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.env.example +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.github/workflows/ci.yml +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.github/workflows/pr-title.yml +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.github/workflows/release.yml +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/.gitignore +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/AGENTS.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/CLAUDE.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/CONTRIBUTING.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/LICENSE +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/cliff.toml +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/config/team_ratings_overrides.yaml +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/docs/fpl-rules.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/docs/images/fpl-player-demo-v1-0.png +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/agents/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/agents/action/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/agents/action/waiver.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/agents/analysis/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/agents/analysis/bench_order.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/agents/analysis/captain.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/agents/analysis/squad_analyzer.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/agents/analysis/starting_xi.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/agents/analysis/stats.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/agents/analysis/transfer_eval.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/agents/base.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/agents/common.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/agents/data/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/agents/data/fixture.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/agents/data/price.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/agents/data/scout.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/agents/orchestration/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/api/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/api/contract.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/api/core_insights.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/api/dataset_fetcher.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/api/football_data.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/api/fpl.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/api/fpl_draft.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/api/historical.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/api/historical_types.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/api/providers/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/api/providers/_http.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/api/providers/_models.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/api/providers/openai_compat.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/api/providers/perplexity.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/api/understat.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/api/vaastav.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/_banner.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/_context.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/_fines_config.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/_json.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/_league_recap_history.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/_plan_grid.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/_review_analysis.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/captain.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/chips.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/credentials.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/differentials.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/doctor.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/doctor_providers.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/fdr.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/fixtures.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/history.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/init.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/intel.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/league.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/league_fines.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/player.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/preview.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/price_changes.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/price_history.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/ratings.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/returnees.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/review.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/sell_prices.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/squad.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/stats.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/status.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/targets.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/transfer_eval.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/cli/xg.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/config/fixture_predictions.yaml +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/config/previews/EXAMPLE.yaml +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/config/team_managers.yaml +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/constants.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/models/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/models/chip_plan.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/models/fixture.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/models/player.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/models/team.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/models/types.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/parsers/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/parsers/recommendations.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/paths.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/prompts/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/prompts/returnees.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/prompts/review.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/prompts/scout.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/scraper/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/scraper/fpl_prices.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/season.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/fixture_predictions.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/league_history.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/league_history_fines.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/matchup.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/player_clubs.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/player_prior.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/returnee_radar.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/scoring/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/scoring/data_prep.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/scoring/display.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/scoring/evaluation.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/scoring/ownership.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/scoring/shrinkage.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/scoring/signals.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/scoring/value_quality.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/season_previews.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/squad_allocator.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/team_form.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/team_ratings.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/services/team_ratings_prior.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/templates/gw_preview.md.j2 +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/utils/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/utils/files.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/utils/gameweek.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/utils/teams.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/fpl_cli/utils/time.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/pyrightconfig.json +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/scripts/backtest_early_season_prior_blend.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/scripts/calibrate_promoted_prior.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/scripts/calibrate_quality_ceilings.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/conftest.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/fixtures/classic_squad_fixture.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/fixtures/recommendations_fixture.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/fixtures/validate_draft_waivers/recs_clean.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/fixtures/validate_draft_waivers/squad_grid.json +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/fixtures/validate_draft_waivers/waivers.json +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/fixtures/waiver_ranked_golden.json +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_agents_action.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_agents_analysis.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_agents_analysis_squad.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_agents_base.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_agents_data.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_agents_data_price.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_agents_orchestration.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_allocate.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_api_client.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_api_core_insights.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_api_dataset_fetcher.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_api_draft.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_api_football_data.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_api_historical.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_api_understat.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_api_vaastav.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_bench_order_script.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_calibrate_quality_ceilings.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_classic_squad_extraction.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_allocate.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_banner.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_captain.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_chips.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_context_settings.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_credentials.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_custom_analysis.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_differentials.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_doctor.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_doctor_providers.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_failure_parity.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_failure_streams.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_fdr.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_fdr_blanks.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_fixtures.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_format.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_format_suppression.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_fpl_block.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_history.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_init.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_intel.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_json.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_json_contract.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_league.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_league_fines.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_plan_grid.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_player.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_preview.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_price_changes.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_price_history.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_returnees.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_sell_prices.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_settings_warning.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_squad.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_stats.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_status.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_targets.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_transfer_eval.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_warnings_stderr.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_cli_xg.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_common.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_common_scoring.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_data_prep_harmonisation.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_dependencies.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_doc_consistency.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_fines.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_fines_config.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_fixture_predictions.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_fpl_prices.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_gw_prep_script_bootstrap.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_init.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_init_fines.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_league_history_fines.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_league_history_store.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_matchup_service.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_models.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_normalise_entities.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_output_style_sync.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_paths.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_player_clubs.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_player_prior.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_player_resolution.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_prompt_tables.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_recommendations_parser.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_report_paths.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_returnee_radar.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_rolling_pts_per_m.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_scraper.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_season.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_season_awareness.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_season_previews.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_sell_prices.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_skill_fdr_quality_signals.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_skill_stats_thresholds.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_squad_allocator.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_starting_xi_agent.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_starting_xi_script.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_stats.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_team_ratings.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_team_ratings_prior.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_transfer_eval_agent.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_transfer_eval_script.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_utils_gameweek.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_utils_text.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_utils_time.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.3}/tests/test_validate_draft_waivers.py +0 -0
|
@@ -47,7 +47,7 @@ with `2>/dev/null` without losing the table, whichever command it runs.
|
|
|
47
47
|
| `returnees` | Track injured and suspended players due back soon, so a returnee can be claimed in draft before they are fit or planned around in classic. Reads the FPL availability news, filters to returns inside a window (`--window N`, 1-38) and through a source-aware quality bar, and diffs against the stored watchlist for week-over-week changes. Most flagged players carry no parseable return date, so date-unknown entries are the norm rather than the exception; `--enrich` searches the web for fresher timing on those and on stale-news entries (needs a Perplexity key; skips with a note without one) and shows it beside the FPL news, never over it. `--all` bypasses the quality bar and deliberately does not persist the snapshot. JSON `metadata` carries `window`, `escalation_window`, `stash_upgrade_margin`, `transitions_available`, `transitions_baseline_gameweek` (the gameweek `transition` is measured against — every run in a gameweek diffs against the last state stored before it, so a repeat run reports the same changes), `quality_bar_available`, `quality_bar_applied` and `enrichment_requested`/`_available`/`_note`/`_count`/`_rate_limited` (true when the search provider rate-limited the run even after retrying — the one failure a re-run is likely to clear); `data` is `{entries, departures}`, each entry carrying `quality.basis`/`quality.meets_stash`, `expected_return`/`return_gameweek`/`return_source`, `escalation_eligible`/`escalation_basis`, `transition`, and `enrichment` (null unless enriched) | Yes | General | No | direct-api |
|
|
48
48
|
| `preview` | Run full pre-gameweek analysis and generate report. `--save` writes to `<output dir>/<season>/gw{N}-preview.md`; `--scout` writes to `<research dir>/ai-scout-reports/<season>/gw{N}-scout-preview[-referenced].md` | No | General | No | via-agent |
|
|
49
49
|
| `review` | Review a completed gameweek - squad performance and standings. `--save` writes to `<output dir>/<season>/gw{N}-review.md`; `--compare-recs` reads `gw{N}-recommendations.md` from the same season directory. Under `--summarise` the personal analysis is handed next gameweek's fixtures — every club's opponent and venue plus each squad player's positional FDR (1-7 team ratings with custom analysis on, the FPL API's 1-5 difficulty with it off, carrying the same scale footer and unrated-club caveat the FDR tables print, plus any warning that the ratings cannot support difficulty) — so its "Next Week" calls are grounded in fixtures rather than in one week of points; with no next gameweek to look at, that section is narrowed to fixture-independent observations and points at `fpl gw-prep`, and a gameweek already played is marked retrospective. The answer is then checked back against that block — a cited FDR the block never printed, or any fixture claim made without one — and against the sections its prompt asked for: an answer missing a `## ` heading, ending mid-sentence, or that the provider says it stopped early on is retried once, and a still-incomplete one is warned about on stderr (detail in `data/debug/synthesis_corrections.txt` under `--debug`) and carries the same list as a callout in the saved report. The summary is an add-on the review does not depend on: the two provider roles resolve independently, and a role with no usable key is skipped with the reason on stderr (`Community narrative skipped: ...` / `Personal analysis skipped: ...`) while the review prints, saves and exits 0 — so one key produces that half rather than nothing, and a saved report names each skipped half in a warning callout of its own. A gameweek it cannot resolve - one still being played, an id the season does not have, a season with nothing finished yet - is refused with the specific reason on stderr and exit 1; stdout carries the review or nothing | No | General | No | via-agent |
|
|
50
|
-
| `league-recap` | Recap a completed gameweek - awards, standings, and banter. Saves to `<output dir>/<season>/gw{N}-league-recap[-draft].md`. Records every run into the league history ledger (`<data dir>/league_history/`) as a side effect, backfilling classic gaps at a coarse tier automatically; `--backfill-detail` rebuilds earlier gameweeks in full (one request per manager per gameweek). A rebuild keeps the name and position already recorded for each player instead of restamping them from today's bootstrap, and never drops a code the gameweek already resolved; it reads the earliest recorded row rather than the newest, so a gameweek an older version already restamped is repaired rather than re-copied. The club is not carried but *derived*, from the gameweek's own fixtures: FPL's live data records which fixtures each player's club played, so a double gameweek settles his club outright and a single narrows it to two, with one `element-summary` request spent only where today's club is absent from that pair (proof he has moved); today's club being one of the two is left unanswered rather than assumed, so a recorded club still wins there. That reaches the two cases a carry structurally cannot — a first capture and a coarse-to-detailed upgrade, neither of which has a recorded squad — and it supersedes a stale recorded club rather than preserving it, with `league_history_club_rederived` naming how many were replaced. A club that blanked has no fixture to intersect and falls back to the recorded club, then today's; name and position are not derivable from fixtures at all and stay carry-only. Everything the gameweek itself answers (points, cards, pick flags, blank status) is still re-derived, and a `league_history_identity_carried` warning names how many players were kept. A rebuild likewise never writes away a league position or cumulative total it cannot re-derive: draft has no per-manager history endpoint and its standings describe a later gameweek, so a replayed draft gameweek keeps the figures already recorded (`league_history_standings_carried`) instead of nulling them, which used to cost the ledger every position that gameweek held and collapse the streaks built on them. Where they were already lost, every run restores them from the ledger itself — the last superseded line that recorded them (a gameweek captured repeatedly while it was still settling holds a stale position in its first capture and the settled one in its last), or a cumulative total re-summed from earlier gameweeks — with no flag and no request (`league_history_standings_repaired`); a draft gameweek's table is derivable from `gross_points` alone at the league's start gameweek. A draft row also records the waiver claims its manager lost to a rival (`lost_claims`, kept apart from `transactions` because nothing moved — no points, no net, and never the denial that is the cascade of the manager's own accepted claim), so an outbid manager is not written into the permanent record as one who never tried; a gameweek captured before that field existed reads it back as unrecorded rather than as "lost none", is named by the coverage report, and is re-recorded by `--backfill-detail` while the league's transaction feed still serves it; a replay that finds fewer claims than the gameweek recorded keeps the recorded list (`league_history_claims_carried`). Those claims also make the waiver wire's races visible (issue #330): the editorial's waiver roster closes with every player more than one manager claimed, who won him and who was beaten to him at what priority, and a week where at least three managers claimed the same player (`MOST_CONTESTED_MIN_CLAIMANTS`) and one of them can be seen to have won him gets a Most Contested award beside Waiver Genius and Waiver Disaster — derived from the manager rows by `contested_draft_claims()`, never recorded, never a report section of its own, and absent from `--format json`, whose rows already carry the `lost_claims` and `transactions` they are regrouped from. Coverage gaps and capture warnings go to stderr; the command exits 0 even when the store is unreadable. `--format json` emits one row per manager (the captured ledger row shape) plus `metadata.coverage`, `season_phase`, `notes_pack`, `synthesis_summary`, `warnings` (always present, each entry a stable `code` plus rewritable `message` - codes listed in [Command Reference](../docs/command-reference.md#league-history)) and `first_capture_store_path` (always present; the partition directory on a first capture, `null` after). Fines are ruled *and recorded* against every captured gameweek, backfilled ones included: the detailed replay rules every configured rule, the coarse tier rules only what cohort points support (`last-place`, `below-threshold`) and records that `red-card` went unruled. `last-place` is ruled against every manager level on the gameweek's lowest score rather than whichever of them the cohort's arrival order reached first; because rulings are frozen at capture and the ledger is append-only, gameweeks recorded before that was true are swept on every run and the recorded ruling appended against the managers they left out, with no flag and no request (`league_history_tied_last_place_repaired`) — and where the row data cannot settle who was level, since the gameweek does not record whether it was ruled on gross or net points, the manager is named by `league_history_tied_last_place_ambiguous` rather than fined on a guess. Season totals print under `Season Fines` on console and as a `# Season Fines` report section, but only at the two milestone gameweeks (GW19 and the finale) - every other week those surfaces stay a this-week view and `league-fines` answers the season question on demand. The `## Season Fines` prompt section and `metadata.season_fines` are ungated: the editorial gets the running totals every week as optional colour (never a required beat), and a scripted consumer sees no calendar-driven appearance and disappearance. Bottom-half gameweeks and green-arrow droughts no longer surface as streaks at all (their run only drives the season count). Streak conditions also keep reset-surviving season occurrence counts ("their sixth gameweek win of the season"), each surfaced on its own rule (see [Command Reference](../docs/command-reference.md#league-recap) for the per-count table): wins and last places on multiples of 3 plus any second-half first, captain blanks and waiver hauls/backfires on multiples of 5, hits on multiples of 3, weeks on top on multiples of 5, bottom-half gameweeks on multiples of 10 in the second half only, and green-arrow droughts on an unbroken run of 5 or 10 (gameweeks spent leading the league never count towards a drought — the leader could not have climbed). Some counts carry same-week peers alongside the milestone — past a fixed floor for counts that stay small, or within a relative window for counts that climb all season. A firing count reaches the report's `## Season Counts` subsection and the editorial's League History section; at the two milestone gameweeks both surfaces carry every nonzero count as the season set-piece. Every line states its span and any not-judged gameweeks beside the number, and every nonzero count is emitted in `metadata.notes_pack.season_count_entries` regardless. A recap of the most recently finished gameweek keeps working once the next gameweek has started: the league table describes that later gameweek from its deadline onward, so the run takes the replay path (figures from each manager's own gameweek history, positions and cumulative totals derived rather than read off the table) and raises a `league_standings_moved_on` warning, instead of reconciling two different gameweeks' numbers against each other and exiting 1 with the ledger row unwritten. `--summarise` with no usable synthesis key no longer aborts the run: the recap, report and ledger capture all proceed, and a `synthesis_provider_unavailable` warning says the editorial was skipped; an editorial the provider stopped early on is written out as it stands with the reason on stderr and a `synthesis_stopped_early` warning in JSON. A gameweek that cannot be resolved at all exits 1 in both output formats - it used to exit 1 under `--format json` with a generic `"Could not resolve a gameweek to recap."` and 0 in table mode - and the resolver's own reason (`Gameweek 7 is not yet finished`) now reaches the error envelope and the stderr prose alike. On the league's opening gameweek only (GW1, or its own `start_event`), each classic manager's FPL-wide prior-season record — seasons played, last season's points/rank/top-% finish, best season — is fetched from the manager-history endpoint (one request per manager, that gameweek only, and only with `--save`/`--output` or `--summarise`; a league created after GW1 shares the fetch with its league-start offset) and rendered as a `# Prior Seasons` report section and a locked `## Prior Seasons` prompt section with an explicit count; tenure is phrased FPL-wide because entry and league IDs are reissued each season, and a manager with no record or a failed fetch is named as such rather than omitted. Nothing of it reaches the ledger or the JSON rows; draft has no such endpoint and no such section | Yes | General | No | via-agent |
|
|
50
|
+
| `league-recap` | Recap a completed gameweek - awards, standings, and banter. Saves to `<output dir>/<season>/gw{N}-league-recap[-draft].md`, opening on a fixed `# Gameweek N Recap: <league name>` title the report writes itself, editorial or not; the editorial is asked for a `## ` headline beneath it and never a title, and whatever heading it opens with anyway is demoted to that level or, when it only restates the title, dropped, so the model's heading is never the document's (issue #349). Records every run into the league history ledger (`<data dir>/league_history/`) as a side effect, backfilling classic gaps at a coarse tier automatically; `--backfill-detail` rebuilds earlier gameweeks in full (one request per manager per gameweek). A rebuild keeps the name and position already recorded for each player instead of restamping them from today's bootstrap, and never drops a code the gameweek already resolved; it reads the earliest recorded row rather than the newest, so a gameweek an older version already restamped is repaired rather than re-copied. The club is not carried but *derived*, from the gameweek's own fixtures: FPL's live data records which fixtures each player's club played, so a double gameweek settles his club outright and a single narrows it to two, with one `element-summary` request spent only where today's club is absent from that pair (proof he has moved); today's club being one of the two is left unanswered rather than assumed, so a recorded club still wins there. That reaches the two cases a carry structurally cannot — a first capture and a coarse-to-detailed upgrade, neither of which has a recorded squad — and it supersedes a stale recorded club rather than preserving it, with `league_history_club_rederived` naming how many were replaced. A club that blanked has no fixture to intersect and falls back to the recorded club, then today's; name and position are not derivable from fixtures at all and stay carry-only. Everything the gameweek itself answers (points, cards, pick flags, blank status) is still re-derived, and a `league_history_identity_carried` warning names how many players were kept. A rebuild likewise never writes away a league position or cumulative total it cannot re-derive: draft has no per-manager history endpoint and its standings describe a later gameweek, so a replayed draft gameweek keeps the figures already recorded (`league_history_standings_carried`) instead of nulling them, which used to cost the ledger every position that gameweek held and collapse the streaks built on them. Where they were already lost, every run restores them from the ledger itself — the last superseded line that recorded them (a gameweek captured repeatedly while it was still settling holds a stale position in its first capture and the settled one in its last), or a cumulative total re-summed from earlier gameweeks — with no flag and no request (`league_history_standings_repaired`); a draft gameweek's table is derivable from `gross_points` alone at the league's start gameweek. A draft row also records the waiver claims its manager lost to a rival (`lost_claims`, kept apart from `transactions` because nothing moved — no points, no net, and never the denial that is the cascade of the manager's own accepted claim), so an outbid manager is not written into the permanent record as one who never tried; a gameweek captured before that field existed reads it back as unrecorded rather than as "lost none", is named by the coverage report, and is re-recorded by `--backfill-detail` while the league's transaction feed still serves it; a replay that finds fewer claims than the gameweek recorded keeps the recorded list (`league_history_claims_carried`). Those claims also make the waiver wire's races visible (issue #330): the editorial's waiver roster closes with every player more than one manager claimed, who won him and who was beaten to him at what priority, and a week where at least three managers claimed the same player (`MOST_CONTESTED_MIN_CLAIMANTS`) and one of them can be seen to have won him gets a Most Contested award beside Waiver Genius and Waiver Disaster — derived from the manager rows by `contested_draft_claims()`, never recorded, never a report section of its own, and absent from `--format json`, whose rows already carry the `lost_claims` and `transactions` they are regrouped from. The editorial is told to write each race as its own clause and never fold two into one ("respectively"), and is checked against the races afterwards (issue #357): a manager it names as beaten to, or winning, a contested player in a role that player's line does not give them triggers one regeneration told which claims were wrong (a retry that errors keeps the first attempt), and a kept editorial that still fails is saved as written with the wrong claims named on stderr, in a `synthesis_contested_misattribution` JSON warning, and in a callout above the editorial in the report. The same retry and reporting cover two checks on either format (issue #359): a manager's net overall, pre-hit swing or another move's swing pinned on one named move (`synthesis_net_misattribution`), and a tie word ("joint-lowest", "tied for last", "joint-seventh") on a gameweek score or league position no two managers hold (`synthesis_unsupported_tie`). The prompt's GW Standings section names the week's highest and lowest scores, every shared score and position (Pos, and Prev where any is shared) and any season totals the league's tie-break placed apart, so "joint" has a line to be held to, and the roster lines call the manager's net "overall". Biggest Bench Haul (both formats) ranks on the best legal XI's gain over the XI that counted — one keeper, the eight legal formations, the doubled player held in place — never on the bench's raw sum, which counted a keeper who could not have played and outfielders no formation had room for (issue #350); its detail names the swap, a manager with bench points but no better XI is no candidate, and the raw sum stays on the row as `bench_points` (the API's `points_on_bench`) because the coarse tier can record nothing else. Coverage gaps and capture warnings go to stderr; the command exits 0 even when the store is unreadable. `--format json` emits one row per manager (the captured ledger row shape) plus `metadata.coverage`, `season_phase`, `notes_pack`, `synthesis_summary`, `warnings` (always present, each entry a stable `code` plus rewritable `message` - codes listed in [Command Reference](../docs/command-reference.md#league-history)) and `first_capture_store_path` (always present; the partition directory on a first capture, `null` after). Fines are ruled *and recorded* against every captured gameweek, backfilled ones included: the detailed replay rules every configured rule, the coarse tier rules only what cohort points support (`last-place`, `below-threshold`) and records that `red-card` went unruled. `last-place` is ruled against every manager level on the gameweek's lowest score rather than whichever of them the cohort's arrival order reached first; because rulings are frozen at capture and the ledger is append-only, gameweeks recorded before that was true are swept on every run and the recorded ruling appended against the managers they left out, with no flag and no request (`league_history_tied_last_place_repaired`) — and where the row data cannot settle who was level, since the gameweek does not record whether it was ruled on gross or net points, the manager is named by `league_history_tied_last_place_ambiguous` rather than fined on a guess. Season totals print under `Season Fines` on console and as a `# Season Fines` report section, but only at the two milestone gameweeks (GW19 and the finale) - every other week those surfaces stay a this-week view and `league-fines` answers the season question on demand. The `## Season Fines` prompt section and `metadata.season_fines` are ungated: the editorial gets the running totals every week as optional colour (never a required beat), and a scripted consumer sees no calendar-driven appearance and disappearance. Bottom-half gameweeks and green-arrow droughts no longer surface as streaks at all (their run only drives the season count). Streak conditions also keep reset-surviving season occurrence counts ("their sixth gameweek win of the season"), each surfaced on its own rule (see [Command Reference](../docs/command-reference.md#league-recap) for the per-count table): wins and last places on multiples of 3 plus any second-half first, captain blanks and waiver hauls/backfires on multiples of 5, hits on multiples of 3, weeks on top on multiples of 5, bottom-half gameweeks on multiples of 10 in the second half only, and green-arrow droughts on an unbroken run of 5 or 10 (gameweeks spent leading the league never count towards a drought — the leader could not have climbed). Some counts carry same-week peers alongside the milestone — past a fixed floor for counts that stay small, or within a relative window for counts that climb all season. A firing count reaches the report's `## Season Counts` subsection and the editorial's League History section; at the two milestone gameweeks both surfaces carry every nonzero count as the season set-piece. Every line states its span and any not-judged gameweeks beside the number, and every nonzero count is emitted in `metadata.notes_pack.season_count_entries` regardless. A recap of the most recently finished gameweek keeps working once the next gameweek has started: the league table describes that later gameweek from its deadline onward, so the run takes the replay path (figures from each manager's own gameweek history, positions and cumulative totals derived rather than read off the table) and raises a `league_standings_moved_on` warning, instead of reconciling two different gameweeks' numbers against each other and exiting 1 with the ledger row unwritten. `--summarise` with no usable synthesis key no longer aborts the run: the recap, report and ledger capture all proceed, and a `synthesis_provider_unavailable` warning says the editorial was skipped; an editorial the provider stopped early on is written out as it stands with the reason on stderr and a `synthesis_stopped_early` warning in JSON. A gameweek that cannot be resolved at all exits 1 in both output formats - it used to exit 1 under `--format json` with a generic `"Could not resolve a gameweek to recap."` and 0 in table mode - and the resolver's own reason (`Gameweek 7 is not yet finished`) now reaches the error envelope and the stderr prose alike. On the league's opening gameweek only (GW1, or its own `start_event`), each classic manager's FPL-wide prior-season record — seasons played, last season's points/rank/top-% finish, best season — is fetched from the manager-history endpoint (one request per manager, that gameweek only, and only with `--save`/`--output` or `--summarise`; a league created after GW1 shares the fetch with its league-start offset) and rendered as a `# Prior Seasons` report section and a locked `## Prior Seasons` prompt section with an explicit count; tenure is phrased FPL-wide because entry and league IDs are reissued each season, and a manager with no record or a failed fetch is named as such rather than omitted. Nothing of it reaches the ledger or the JSON rows; draft has no such endpoint and no such section | Yes | General | No | via-agent |
|
|
51
51
|
| `league-fines` | Show who has been fined this season and how much of it was actually ruled, folded out of the league history ledger. Reads local files only - no network - so it works for any season still on disk (`--season 2025-26`). Nothing is re-ruled: a fine is counted exactly as its gameweek recorded it — including one `league-recap` appended against a manager who tied for last and was left out at capture. Counts, never money (`penalty` is free text; settlement and escalation are not modelled). Every gameweek that could not be ruled is named beneath the table - never captured, unreadable, captured before rulings were recorded, captured with no rules configured, or captured at a fidelity that could not rule a given rule. `--format json` emits one entry per recorded manager, fined or not, each with `counts` (zero included), `total`, `fined_gameweeks`, `ruled_gameweeks`, `unruled_gameweeks`, `first_recorded_gameweek`, `last_recorded_gameweek` and `is_fully_ruled`; `metadata` carries `season`, `fpl_format`, `league_id`, `gameweek`, `start_gameweek`, `rule_types`, `total_fines` and `qualifiers`. Exits 1 with the shared error envelope when no league id is configured for the format | Yes | General | No | direct-api |
|
|
52
52
|
| `captain` | Analyse and rank captain options for next gameweek. Your squad is the default, so an absent `classic_entry_id` exits 1 with an `error` envelope naming `--global` rather than returning the global list as though it were your options; `metadata.my_squad_mode` says which list the payload holds, and `metadata.warnings` carries `captain_global_fallback` when there is no squad to rank yet (before the first deadline) or `captain_no_next_gameweek` when there is no next gameweek at all (finished season, or the next one unpublished). An entry ID that does not resolve is a failure, not a fallback, worded exactly as `squad` words it (`get_own_squad_picks`, `agents/common.py`). JSON candidates include `adjusted_npxg_per_90` and `raw_npxg_per_90` when fixture adjustment is active. Consistency tiebreaker (CV-xGI percentile) phased in GW6-10. Players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of the GW1-9 shrinkage rather than pulled toward the position mean | Yes | Classic | Yes | via-agent |
|
|
53
53
|
| `differentials` | Find differential picks - high potential, low ownership. Inverted consistency bonus (volatile players score higher, phased in GW6-10). Before GW10 the score's quality baseline carries the prior blend (last season's pedigree weighted by confidence) rather than position-mean shrinkage, so a quiet-starting elite is not ranked below a one-game wonder; the ownership, matchup and consistency bonuses stay pure observation; JSON `metadata.warnings` carries the early-season notice while the blend is live (`early_season_prior_informed`, or `early_season_small_sample` when history could not be loaded) plus `understat_team_unmatched` for any club Understat carried no players for, table mode prints them to stderr. Players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of the blend rather than handed last season's standing. The gameweek stamped into JSON `metadata` is fetched after the agent succeeds, not before it runs, so an unreachable API is reported as the agent's failure in both output formats rather than as a client error under `--format json` and an agent error in table mode | Yes | Classic | Yes | via-agent |
|
|
@@ -110,7 +110,7 @@ Every recommendation must include:
|
|
|
110
110
|
|
|
111
111
|
For starting XI selection:
|
|
112
112
|
- **11 players exactly:** 1 GK + 10 outfield
|
|
113
|
-
- **Valid formations:** 3-4-3, 3-5-2, 4-3-3, 4-4-2, 4-5-1, 5-3-2, 5-4-1
|
|
113
|
+
- **Valid formations:** 3-4-3, 3-5-2, 4-3-3, 4-4-2, 4-5-1, 5-2-3, 5-3-2, 5-4-1
|
|
114
114
|
- **Positions are fixed:** Each player's Pos must match their actual position. Never reassign a player to fit a formation.
|
|
115
115
|
- **Bench:** 4 players for both Classic and Draft
|
|
116
116
|
- **Bench ordering:** Must use the bench-order script output. Do not manually order bench players.
|
|
@@ -56,6 +56,7 @@ or stale build.
|
|
|
56
56
|
`head_sha` matches `origin/main` and `conclusion="success"`. For
|
|
57
57
|
per-job detail on a failing run, re-call with
|
|
58
58
|
`method="list_workflow_jobs"` and the run ID as `resource_id`.
|
|
59
|
+
- The default synthesis model is still current: `llm.synthesis.model` in `fpl_cli/config/defaults.yaml` and `DEFAULT_MODEL` in `fpl_cli/api/providers/anthropic.py` should name the newest Sonnet. If a newer one exists, flag it to the user (bumping needs a live-key run of the synthesis commands and a recheck of `max_tokens`, #316) rather than bumping silently.
|
|
59
60
|
- Local checks pass, mirroring CI (run in parallel; check each exit status
|
|
60
61
|
individually — a `| tail` pipe hides failures):
|
|
61
62
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
## Squad Constraints
|
|
2
2
|
- **15 players:** 2 GK, 5 DEF, 5 MID, 3 FWD
|
|
3
3
|
- **Max 3 from any team**
|
|
4
|
-
- **Valid formations:** 3-4-3, 3-5-2, 4-3-3, 4-4-2, 4-5-1, 5-3-2, 5-4-1
|
|
4
|
+
- **Valid formations:** 3-4-3, 3-5-2, 4-3-3, 4-4-2, 4-5-1, 5-2-3, 5-3-2, 5-4-1
|
|
5
5
|
- **Full stats required:** Run `fpl player "{name}" -f` for every player in the final squad and every serious alternative considered. Never recommend without data.
|
|
6
6
|
|
|
7
7
|
## pFDR (Positional FDR)
|
|
@@ -27,7 +27,7 @@ Never reason from a wrong rule. If you're unsure about a scoring or mechanics de
|
|
|
27
27
|
- **Bench Boost = all 15 score once (not 2x).** Floor-raiser via bench minutes, not a multiplier.
|
|
28
28
|
- **Triple Captain = captain scores 3x (not 2x).**
|
|
29
29
|
- **Chip availability:** each chip twice per season, split at GW19. Check `fpl chips` before assuming.
|
|
30
|
-
- **Formation is a variable.** Valid: 3-4-3, 3-5-2, 4-3-3, 4-4-2, 4-5-1, 5-3-2, 5-4-1. Evaluate transfers against the 15-man squad, not a fixed XI - a same-position swap can mean "both play via reshape", not "one replaces the other". Compare all candidate sells at that position before committing.
|
|
30
|
+
- **Formation is a variable.** Valid: 3-4-3, 3-5-2, 4-3-3, 4-4-2, 4-5-1, 5-2-3, 5-3-2, 5-4-1. Evaluate transfers against the 15-man squad, not a fixed XI - a same-position swap can mean "both play via reshape", not "one replaces the other". Compare all candidate sells at that position before committing.
|
|
31
31
|
|
|
32
32
|
## Draft Rules (load-bearing)
|
|
33
33
|
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
|
+
## [2.7.2] - 2026-09-16
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- open every league recap on a title the editorial cannot rewrite (#351)
|
|
9
|
+
- award Biggest Bench on the best XI's gain, not the raw bench sum (#352)
|
|
10
|
+
|
|
11
|
+
## [2.7.1] - 2026-09-09
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
- stop the review mismatching names over spacing or apostrophes (#346)
|
|
16
|
+
- give managers level on points the same position in `fpl league` (#345)
|
|
17
|
+
- stop a draft waiver a manager won reading as one he lost (#347)
|
|
18
|
+
|
|
4
19
|
## [2.7.0] - 2026-09-08
|
|
5
20
|
|
|
6
21
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fplkit
|
|
3
|
-
Version: 2.7.
|
|
3
|
+
Version: 2.7.3
|
|
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
|
|
@@ -21,6 +21,7 @@ Requires-Dist: rich>=13.7.0
|
|
|
21
21
|
Requires-Dist: ruamel-yaml>=0.18.0
|
|
22
22
|
Requires-Dist: tzdata>=2024.1; platform_system == 'Windows'
|
|
23
23
|
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: markdown-it-py>=2.2.0; extra == 'dev'
|
|
24
25
|
Requires-Dist: packaging>=23.0; extra == 'dev'
|
|
25
26
|
Requires-Dist: pyright>=1.1.0; extra == 'dev'
|
|
26
27
|
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
@@ -116,6 +117,11 @@ so `review --summarise` with a key for one provider prints that half, says on st
|
|
|
116
117
|
why the other was skipped, and still exits 0; a saved report names each skipped half
|
|
117
118
|
in its own warning callout. `league-recap --summarise` does the same for its editorial.
|
|
118
119
|
|
|
120
|
+
A saved recap opens with a fixed title — `# Gameweek 4 Recap: <league name>`, the league's name
|
|
121
|
+
as configured — whether or not it has an editorial. The editorial's own headline sits beneath it
|
|
122
|
+
as a `##` subheading, never as the title, so the first line you paste into the group chat reads
|
|
123
|
+
the same every week and the name on it is the league's rather than one the model restyled.
|
|
124
|
+
|
|
119
125
|
Fines are ruled per gameweek by `league-recap` and recorded against it, so `fpl
|
|
120
126
|
league-fines` reads them straight back off disk — no network, and any season still
|
|
121
127
|
on disk (`--season 2025-26`). Every gameweek that could not be ruled is named
|
|
@@ -81,6 +81,11 @@ so `review --summarise` with a key for one provider prints that half, says on st
|
|
|
81
81
|
why the other was skipped, and still exits 0; a saved report names each skipped half
|
|
82
82
|
in its own warning callout. `league-recap --summarise` does the same for its editorial.
|
|
83
83
|
|
|
84
|
+
A saved recap opens with a fixed title — `# Gameweek 4 Recap: <league name>`, the league's name
|
|
85
|
+
as configured — whether or not it has an editorial. The editorial's own headline sits beneath it
|
|
86
|
+
as a `##` subheading, never as the title, so the first line you paste into the group chat reads
|
|
87
|
+
the same every week and the name on it is the league's rather than one the model restyled.
|
|
88
|
+
|
|
84
89
|
Fines are ruled per gameweek by `league-recap` and recorded against it, so `fpl
|
|
85
90
|
league-fines` reads them straight back off disk — no network, and any season still
|
|
86
91
|
on disk (`--season 2025-26`). Every gameweek that could not be ruled is named
|
|
@@ -389,9 +389,9 @@ Services live in `fpl_cli/services/` and provide the computation layer between a
|
|
|
389
389
|
| `returnee_radar` | Injury returnee radar behind `fpl returnees`. Parses the FPL `news` grammar into a `ReturnSignal` (four shapes, only two of them dated), filters on a window and a **source-aware** quality bar, and diffs the result against a persisted snapshot for week-over-week transitions. The bar is source-aware because `player_prior` only assigns `source="history"` at 450+ minutes in the previous season and caps `prior_strength` at 0.5 on the `"price"` fallback, so one flat threshold would exclude exactly the players who missed most of last season through injury: history-sourced players are gated on `prior_strength`, price-sourced ones are scored through `value_quality` over their most recent season carrying real minutes, and the within-position price percentile is the last resort. That season supplies the DEF/GK weight terms (defensive contribution, saves, clean sheets, xGC per 90) from the `SeasonHistory` row when its source published them, and otherwise shrinks the ceiling via `ceiling_attainability()` rather than reading the gap as a measured zero — without which no keeper and effectively no defender could clear the bar (#132). `run_radar()` is the public entry point (snapshot I/O included); `build_radar()` is the pure core. The `SnapshotStore` behind it holds two states — the baseline every run in the current gameweek diffs against, and the current gameweek's own, promoted to baseline when the gameweek moves on — so a second run in one gameweek reports the same transitions as the first instead of diffing against what that run just stored (#225). **Fetches nothing** — the caller injects historical profiles and Understat season data, because `prepare_scoring_data` discards the profiles it built priors from and skips fetching them at all on a cache hit. Optional AI-search enrichment (`--enrich`) attaches an `EnrichedReturn` beside the FPL signal, never over it, and only a *cited* enriched date can decide an escalation |
|
|
390
390
|
| `team_form` | Rolling team form stats (last 6 matches, venue splits) |
|
|
391
391
|
| `league_history` | Durable league-history ledger: one NDJSON file per gameweek under `<data dir>/league_history/<season>/<format>-<league id>/`, written as a side effect of `league-recap`. Append-only — an identical row writes nothing, a differing one appends a superseding line, and readers resolve by highest fidelity tier then latest capture, with an unknown row ranking below every tier. Each row carries its own schema version: an older one is upgraded in memory on read (the line on disk is never rewritten, so a store shared with an older install stays readable by it), a newer one is skipped with a warning and preserved byte-for-byte — and every line the store writes is stamped with the writing install's version whatever version the row was read at, since a superseding line copied off a version-5 row but carrying a version-6 field would be validated by a version-5 install rather than skipped, and rejected as a corrupt file. Two deliberate inversions of house convention: loading **fails closed** (unlike `chip_plan`, which resets on a corrupt file) because the API cannot rebuild a past gameweek — scoped to that one gameweek, whose coverage entry carries the file path and the `mv` that retires it, so a caller reports it as `league_history_store_unreadable` rather than as a coverage gap `--backfill-detail` would refuse to fill, and reports it once per run however many of a recap's readers hit the same file — and season is a **partition key** (unlike `team_ratings`, which discards a previous season) because per-gameweek granularity is destroyed at the July rollover. The consequence of partitioning is that the store only ever grows — nothing prunes a prior season, by design — at roughly a few megabytes per league per season |
|
|
392
|
-
| `league_history_counters` | Declarative streak-condition registry (9 conditions: `weeks_on_top`, `bottom_half_run`, `gw_win_streak`, `gw_loss_streak`, `green_arrow_drought` shared; `captain_blank_run`, `hit_run` classic-only; `waiver_win_run`, `waiver_burn_run` draft-only) and the rebuildable counters projection it drives. Each entry declares the minimum run worth surfacing as a streak, `None` for the two conditions (bottom half, green-arrow drought) whose run only drives their season count's firing rule. Each predicate returns extend/reset/hold for one manager's row
|
|
392
|
+
| `league_history_counters` | Declarative streak-condition registry (9 conditions: `weeks_on_top`, `bottom_half_run`, `gw_win_streak`, `gw_loss_streak`, `green_arrow_drought` shared; `captain_blank_run`, `hit_run` classic-only; `waiver_win_run`, `waiver_burn_run` draft-only) and the rebuildable counters projection it drives. Each entry declares the minimum run worth surfacing as a streak, `None` for the two conditions (bottom half, green-arrow drought) whose run only drives their season count's firing rule. Each predicate returns extend/reset/hold/inapplicable for one manager's row, and the last two both leave the run untouched, so neither a capture gap nor an impossibility ever lies about a streak in either direction. They differ in what the record says: hold is a gameweek it cannot rule (an unknown row, a field the tier never captured), inapplicable one it rules completely where the condition had nothing to judge (a draft manager with no completed waiver moves, whether they made no claims or lost every one, a captain with no fixture, a cohort level on points, a green-arrow drought gameweek begun in first place). Each run counts them separately, and each condition that can return inapplicable names it in its own words (`inapplicable_label`), so a complete gameweek is never reported as missing (issue #358). Alongside each currently-open run, the state carries season-wide occurrence totals (issue #164): every extending gameweek counts once and survives a reset, holds (not inapplicable gameweeks, which were judged) are tallied separately as the count's "not judged" qualifier, and the last-occurrence and first-evaluated gameweeks bound what a consumer may claim — so "their sixth gameweek win of the season" is derivable, not just the open run. Each entry also declares a `CountSurfacePolicy`, the condition's own rule for when that total is worth rendering weekly (U9 applies it; the milestone set-pieces and `--format json` ignore it). The projection is a disposable cache (never a second source of truth) under `<data dir>/league_history_counters/<season>/<format>-<league id>/counters.json`: it advances one gameweek at a time when its stamp is exactly one behind, and **fails open** to a full rebuild from `league_history`'s rows otherwise — missing, unreadable, wrong-version, or a stamp that isn't stamp+1 (which is also what makes the season totals retroactive: the version bump that added them rebuilds silently from rows already on disk) |
|
|
393
393
|
| `league_history_fines` | Season fine tally (`build_season_fines_tally()`): a pure fold over `league_history`'s rows into per-manager, per-rule counts, with no cache and no second source of truth. Counts only what a row already recorded, so a settings change moves future rulings and leaves history alone — a guarantee `_freeze_recorded_fines` (`cli/_league_recap_history.py`) carries into backfill, where a repair would otherwise re-rule an unchanged cohort under whatever config is current at repair time. Every gameweek that could not be ruled is named as a qualifier rather than folded in as a zero — never captured, unreadable, captured but reaching nobody (every row unknown), holding no record of what was ruled (schema version < 4, or an evaluation that raised), recording a ruling on no rules at all, or captured at a fidelity that could not rule a given rule. Each qualifier states only what the ledger can prove: where two causes are indistinguishable from a row, neither is named. A mid-season joiner keeps their real, lower totals and is qualified; a manager who has since left keeps the fines already ruled against them, bounded to the gameweeks they were recorded for. Rulings are tracked per rule as well as per manager (`unruled_gameweeks_for()`), since a coarse gameweek rules `last-place` and structurally cannot rule `red-card` — the recap prompt asks that question before it will call a fine a manager's first of the season. `last-place` is the one cohort-relative rule, so it is the one a tie falls on: every manager level on the gameweek's lowest score is fined, and `_repair_tied_last_place` sweeps gameweeks recorded before that was true, appending the ruling the gameweek already wrote against the managers it left out rather than re-ruling anything (issue #336) |
|
|
394
|
-
| `league_history_notes` | Notes pack (`build_notes_pack()`), season-phase marker (`derive_season_phase()`, standalone) and the `is_season_milestone()` predicate the recap gates its *printed* season fines table on (its prompt section is ungated) - the phase's *first* gameweek and the finale, deliberately not the whole 13-gameweek midpoint phase, since a once-a-season set-piece fired thirteen times is wallpaper built from `league_history_counters`'s projection plus a bounded trailing window of raw ledger rows (`TRAILING_WINDOW_GAMEWEEKS = 6`, reused as the run-in phase's own length). Every open streak renders as an observed count over its true span rather than "in a row" once
|
|
394
|
+
| `league_history_notes` | Notes pack (`build_notes_pack()`), season-phase marker (`derive_season_phase()`, standalone) and the `is_season_milestone()` predicate the recap gates its *printed* season fines table on (its prompt section is ungated) - the phase's *first* gameweek and the finale, deliberately not the whole 13-gameweek midpoint phase, since a once-a-season set-piece fired thirteen times is wallpaper built from `league_history_counters`'s projection plus a bounded trailing window of raw ledger rows (`TRAILING_WINDOW_GAMEWEEKS = 6`, reused as the run-in phase's own length). Every open streak renders as an observed count over its true span rather than "in a row" once its window is wider than its run (e.g. "3 in the last 11, with 8 not recorded"), and the trailing clause separates the gameweeks the record lacks ("not recorded": unknown, uncaptured or absent) from the ones the condition had nothing to judge in, named by the condition ("with no completed moves in 1"); each entry declares which of console/report/prompt it reaches — a reportable streak reaches all three, coverage statements reach report+prompt, the season-phase marker is prompt-only (scene-setting for the editorial, never printed in the report — issue #187), and a below-minimum streak is retained (exposed via `--format json`) with no surfaces. Season-count entries (issue #164) render each condition's season occurrence total over the manager's own evaluated span, with holds stated as "not judged either way", surfaced per the registry's own declarative `CountSurfacePolicy` (a step the total must land on, run-length milestones read off the open run rather than the total, a first-occurrence rule gated on the season's half, a whole-condition second-half gate, and a ride-along rule for same-gameweek peers — an absolute floor for counts that stay small, a window relative to the nearest firing total for counts that climb all season). Evaluation is two-pass because firing is cross-manager: who fired their condition this gameweek, then which same-gameweek incrementers ride along beside them. Every nonzero count is retained for `--format json`, and at the two `is_season_milestone()` gameweeks the whole nonzero set carries report+prompt — the report's `## Season Counts` set-piece and the retrospective editorial's season-spanning facts. A "since GW X" qualifier is stated only when a partition's or a specific manager's own recorded coverage genuinely begins later than the league's start gameweek, never merely because a trailing window was read. Only the finale phase rescans every captured gameweek (via `rebuild_counters_through`, never the cached `compute_counters_through`) so weekly cost stays flat as the season progresses |
|
|
395
395
|
|
|
396
396
|
## LLM Provider Abstraction
|
|
397
397
|
|
|
@@ -421,7 +421,7 @@ classDiagram
|
|
|
421
421
|
}
|
|
422
422
|
|
|
423
423
|
class AnthropicProvider {
|
|
424
|
-
+DEFAULT_MODEL: claude-sonnet-5
|
|
424
|
+
+DEFAULT_MODEL: claude-sonnet-5-5
|
|
425
425
|
+query(prompt, system_prompt) LLMResponse
|
|
426
426
|
+post_process(content) str
|
|
427
427
|
+close()
|
|
@@ -531,7 +531,7 @@ fpl_cli/
|
|
|
531
531
|
│ ├── _banner.py # Startup banner
|
|
532
532
|
│ ├── _plan_grid.py # Fixture grid rendering
|
|
533
533
|
│ ├── _review_*.py # Review command helpers (analysis, classic, draft, summarisation)
|
|
534
|
-
│ ├── _league_recap_*.py # League recap helpers & types (`_league_recap_history.py` orchestrates capture: builds ledger rows, corrects previous league position from recorded rows, runs the two-tier backfill including the coarse tier's partial fine ruling, keeps the name/club/position a replayed gameweek already recorded rather than restamping it from today's bootstrap (`_carry_recorded_identity`, reading the earliest recorded line rather than the resolved winner so an already-restamped gameweek is repaired — a lost waiver claim's two players carried the same way, off the earliest line that recorded any claims), restates the players a stored fine names from that corrected squad, after the freeze so a ruling already written wrong is repaired rather than frozen wrong forever (`_repair_fine_identity`), keeps the league position and cumulative total the same way (`_carry_recorded_standings`, but reading the *last* recorded value rather than the earliest -- a table settles over a gameweek where a name does not) and reconstructs a draft gameweek's from the ledger where it can (`_fill_draft_standings`), sweeps already-damaged gameweeks back into shape without re-fetching anything (`_repair_recorded_standings`, and `_repair_tied_last_place` for a gameweek that fined one manager of a tie for last), and returns the notes pack and season fine tally the console, report, prompt and JSON payload all read; `_league_recap_data.py` also holds `fetch_manager_histories()`, the one manager-history fetch that `collect_classic_recap_data(with_prior_seasons=True)` makes at the league's opener and shares between the league-start offset and `collect_prior_seasons()` — asked for only when a report or editorial will read it, never by a replay, and never a ledger field — and `league_first_gameweek()`, the one definition of a league's first scored gameweek that both collectors' gates and this module's coverage, backfill, notes pack and fines tally read; `_league_recap_types.py` holds the TypedDicts and the pure helpers both the data module and the prompt builder read — the draft kind labels, `summarise_prior_seasons()`, and `contested_draft_claims()` / `format_contested_claim()`, which regroup a draft gameweek's lost claims by player, read the winner off the accepted move that brought him in, and print one sentence per race for the Most Contested award and the editorial alike (issue #330) — the award gated on `MOST_CONTESTED_MIN_CLAIMANTS` in `_league_recap_data.py`, the editorial handed every race)
|
|
534
|
+
│ ├── _league_recap_*.py # League recap helpers & types (`_league_recap_history.py` orchestrates capture: builds ledger rows, corrects previous league position from recorded rows, runs the two-tier backfill including the coarse tier's partial fine ruling, keeps the name/club/position a replayed gameweek already recorded rather than restamping it from today's bootstrap (`_carry_recorded_identity`, reading the earliest recorded line rather than the resolved winner so an already-restamped gameweek is repaired — a lost waiver claim's two players carried the same way, off the earliest line that recorded any claims), restates the players a stored fine names from that corrected squad, after the freeze so a ruling already written wrong is repaired rather than frozen wrong forever (`_repair_fine_identity`), keeps the league position and cumulative total the same way (`_carry_recorded_standings`, but reading the *last* recorded value rather than the earliest -- a table settles over a gameweek where a name does not) and reconstructs a draft gameweek's from the ledger where it can (`_fill_draft_standings`), sweeps already-damaged gameweeks back into shape without re-fetching anything (`_repair_recorded_standings`, and `_repair_tied_last_place` for a gameweek that fined one manager of a tie for last), and returns the notes pack and season fine tally the console, report, prompt and JSON payload all read; `_league_recap_data.py` also holds `fetch_manager_histories()`, the one manager-history fetch that `collect_classic_recap_data(with_prior_seasons=True)` makes at the league's opener and shares between the league-start offset and `collect_prior_seasons()` — asked for only when a report or editorial will read it, never by a replay, and never a ledger field — and `league_first_gameweek()`, the one definition of a league's first scored gameweek that both collectors' gates and this module's coverage, backfill, notes pack and fines tally read; `_league_recap_types.py` holds the TypedDicts and the pure helpers both the data module and the prompt builder read — the draft kind labels, `summarise_prior_seasons()`, and `contested_draft_claims()` / `format_contested_claim()`, which regroup a draft gameweek's lost claims by player, read the winner off the accepted move that brought him in, and print one sentence per race for the Most Contested award and the editorial alike (issue #330) — the award gated on `MOST_CONTESTED_MIN_CLAIMANTS` in `_league_recap_data.py`, the editorial handed every race; `_league_recap_data.py` also holds `best_xi_selection()`, the pure reselection of the best legal XI from a recorded 15 that both collectors stamp on the entry as `best_xi_gain` and the Biggest Bench award ranks on — one keeper, the eight `VALID_FORMATIONS`, the doubled player held in place, ties to the XI that played — where the bench's raw sum, kept as `bench_points` for the ledger's sake, overstated the award every week (issue #350))
|
|
535
535
|
│ ├── _fines.py / _fines_config.py # League fines system, including the cohort-only/needs-a-squad rule split the coarse ledger tier narrows by
|
|
536
536
|
│ ├── league_fines.py # `league-fines`: season fine table read straight off the ledger, no network
|
|
537
537
|
│ ├── doctor_providers.py # Live provider probes for `doctor --providers` (shape/volume/join checks; the Core-Insights per-GW probe runs the real parsers, so it cannot pass a file the runtime reads as zero records, and the Understat name-join probe runs the real matcher, so a club resolving cannot stand in for its players joining)
|
|
@@ -596,7 +596,8 @@ fpl_cli/
|
|
|
596
596
|
│ ├── scout.py # ScoutAgent system/user prompts
|
|
597
597
|
│ ├── review.py # Review research + synthesis prompts. Squad and transfer lines in the synthesis data block carry each player's full club name, and the hard constraints bind club affiliation and blanket scored/blanked claims to that supplied data — an LLM's own club knowledge goes stale every transfer window. The synthesis prompt also carries a `<next_gameweek>` block — every club's next fixture and each squad player's positional FDR, built by `_review_next_gameweek` / `_format_next_gameweek` — and the "Next Week" instruction switches on whether it is there: grounded advice when it is, fixture-independent observations pointing at `fpl gw-prep` when it is not, because a results-only review was issuing confident fixture-dependent calls with no fixture data behind them (#191), with `check_next_week_grounding` checking the answer back against that block -- a cited FDR figure the block never printed, or any fixture claim on a run that had no block -- so the grounding claim is not left resting on prompt prose alone, the way the research half's rules are backed by `validate_research_teams`, which folds both the supplied name and the model's cell through `normalise_name` before the allowlist search so a reformat is not read as an invention, and leaves a `[table trimmed: N unlisted row(s) removed]` line under a table it did shorten — the strip notice otherwise reaching stderr and `--debug` only, both gone by the time the markdown is read (#343). Also the synthesis half's post-generation guard (`check_synthesis_completeness`, `required_synthesis_sections`), which reads the required `## ` headings back off the prompt that was actually sent rather than restating them as a constant. The research half's counts are grounded the same way: `fixture_aggregates()` computes the fixture, goal, clean-sheet and goalless-draw totals once, `FixtureAggregates.summary_line()` pins them at the top of the GW Results block as the narrative's only permitted source, and `validate_research_counts` checks the narrative back against the same object — a count the prompt pins cannot drift from the count the check enforces. It rewrites a contradicting figure in place rather than scrubbing the sentence, and corrects one only when neither its sentence nor the one before it names anyone the count could belong to — a club, a nickname, a player, a pronoun standing in for one — because a round-wide total is exactly the sentence that names nobody. The guard errs towards leaving prose alone: a missed correction ships the model's own words, while a wrong one fabricates a claim in prose that was true. That blind spot is why the check was needed at all — the narrative that reported six clean sheets as four (#324) named no player, so all three name-based validators passed it
|
|
598
598
|
│ ├── returnees.py # Return-intel search prompt for `fpl returnees --enrich` (one player per query, so a citation list belongs to a single player)
|
|
599
|
-
│
|
|
599
|
+
│ ├── league_recap.py # League recap synthesis prompts, including the anchored League History section, its never-infer-history rule (emitted even when the pack is empty), and the season-phase framing instruction. The GW Standings section leads with an explicit "Previous gameweek's leader" fact (issue #189) so a "topped the table"/"fell from the top" claim is checkable against a stated name instead of inferred from the Prev column or the size of a fall, and the rules forbid merging two managers' separate League History entries into a shared record or "club". `collect_player_clubs()` regroups the club each squad/transfer row already resolved off its `team_id` by the name the prose uses, feeding both a `## Player Clubs` roster and the club printed inline on each captain group, so club claims bind to supplied data instead of recall; a name the data gives two clubs for is dropped rather than guessed at, since the prose can't tell the two players apart. Transfers get the same full-roster treatment as captains and chips (issue #71): `format_recap_transfers_context()` lists every manager who transferred with each move, the hit and the post-hit net, names who made none, and keeps a manager whose moves the collector could not capture among the movers with the moves marked unknown, so the rule pins transfer narration to that section rather than to the two award winners — the only transfer data the model used to see, which it read as the whole story. `format_recap_waivers_context()` is the draft half (issue #301): every waiver and free-agent move as it was made, never chain-contracted (that is the awards' display compression, not a record of activity), each tagged by kind through the shared `draft_transaction_kind_label()` / `draft_transaction_kind_counts()` in `_league_recap_types.py` so the roster and the waiver awards never describe one move in two vocabularies; the rule names that section as draft's source of truth and only falls back to "say nothing about moves" when neither roster is present. It states three groups rather than two (issue #329): movers, the managers who claimed a player and lost him to a rival, and the managers who did neither. A waiver is a competition, so an outbid manager ends the gameweek with no accepted move and used to be indistinguishable from one who submitted nothing -- the editorial read that absence as inactivity and assigned a motive for it, so the rule now forbids calling anyone in the outbid group idle. The section closes with the gameweek's contested players (issue #330) — one sentence per player more than one manager claimed, from `contested_draft_claims()` / `format_contested_claim()` in `_league_recap_types.py`, the same sentence the Most Contested award prints for a pile-up of `MOST_CONTESTED_MIN_CLAIMANTS` or more that a recapped manager won (a race nobody can be seen to have won is context, never the award), with a rule pinning who-wanted-whom to those lines and limiting what may be said of anyone's waiver order to the winner having stood higher in it. `format_recap_prior_seasons_context()` (issue #131) is the opener's one source of history: each classic manager's FPL-wide seasons before this one, folded once by `summarise_prior_seasons()` (`_league_recap_types.py`) so the saved report and the prompt read the same lines, with an explicit count, the no-record and unfetched managers named rather than omitted, and a rule pinning tenure to FPL-wide phrasing — entry and league IDs are reissued each season, so "Nth season in this league" is unknowable and the section says so. The report's title is not the model's (issue #349): `recap_title()` (`_league_recap_types.py`) is the one H1, read by the template that renders it and the prompt that names it as already written; the prompt asks for a `## ` headline beneath it and no title, and `normalise_recap_editorial()` holds the reply to that shape in one fence-aware pass over every heading — one that only restates the title is dropped wherever it sits, the opening one is kept at H2 with the title's parts taken off (emphasis unwrapped through `unwrap_emphasis()` in `utils/markdown.py`, balanced pairs only, so what is printed again stays well-formed), a later `# ` is demoted, and a title-shaped part is removed only behind a colon, pipe or spaced dash so a hook's own words are never edited — since an instruction is not a contract and the model had mirrored, restyled, renamed and skipped the heading in turn. For the same reason the finished editorial is checked against its own data afterwards, by `league_recap_checks.py` beside this module; what the checks hold "joint" to is stated here, in `_standings_tie_lines()`: the week's highest and lowest scores, every gameweek score and Pos position more than one manager holds, any shared Prev position, and any season totals the league's tie-break placed apart (a head-to-head draft table ranks managers level on league points by points scored)
|
|
600
|
+
│ └── league_recap_checks.py # Post-generation checks on the league-recap editorial, kept out of the prompt module so it stays about prompts. `EDITORIAL_CHECKS` is the one registry -- each check's `RECAP_WARNING_*` code, the lead of its JSON warning (`editorial_warning_message()`, one sentence per claim), its retry instruction and the checker -- so a new check is one record. `check_contested_attributions()` (issue #357) reads who a sentence says won or was beaten to a contested player; `check_net_attributions()` (issue #359) reads a "net" figure said of one named move and flags it when it is another figure on that manager's roster line, attaching it to whichever of the move and its manager is named nearer before it; `check_tie_claims()` (issue #359) reads a tie word fused to a rank ("joint-lowest", "tied for last", "joint-seventh") against the GW Pts column, or the table's Pos, Prev and Total columns. Each reads only the clauses it can pin down and lets the rest pass, so a problem is a sentence the data contradicts and a false alarm (which costs a retry and a warning above a correct editorial) is the failure it is built against. `check_recap_editorial()` runs them all; the command regenerates once with `get_recap_editorial_retry_prompt()` (the wrong claims plus an instruction per kind that failed), saves a still-failing editorial as written with one JSON warning per code and the claims in a callout above it, and ships the editorial unchecked if a check itself raises
|
|
600
601
|
├── parsers/
|
|
601
602
|
│ └── recommendations.py # Parse gw{N}-recommendations.md into structured decisions
|
|
602
603
|
├── scraper/
|
|
@@ -608,7 +609,7 @@ fpl_cli/
|
|
|
608
609
|
├── gameweek.py # is_opening_gameweek(gw) — shared GW1 check (transfers, waivers and league tables don't exist yet)
|
|
609
610
|
├── markdown.py # HeadingMatcher, find_section, section_body, leaf_body, fence_flags — fence-aware markdown section location tolerant of LLM heading drift; shared by the gw-prep validator scripts and fpl_cli.prompts.review. Also unescape_specials/find_entities — repair and reporting for markdown that arrived HTML-escaped
|
|
610
611
|
├── teams.py # describe_team_set_mismatch — diff a per-team config against the live team list (promotion/relegation drift)
|
|
611
|
-
├── text.py # strip_diacritics (name matching across sources). normalise_name folds the rest of what a retyped name varies by — the apostrophe family onto U+0027, whitespace runs (NFKC, so non-breaking spaces too) and the space after an initial ("B. Fernandes" → "B.Fernandes") — on top of the diacritic strip and lowercasing. Apply it to both sides of a comparison. Shared by everywhere the review matches an LLM-written name against an API one: `validate_research_teams` / `ensure_top_performer_first` in prompts/review.py, and `_normalise_name` in cli/_review_summarisation.py, which layers its parenthetical and leading-initial strips on top for `--compare-recs`. Neither a `\b`-anchored regex nor an equality test survives any of these differences: they cost a valid row in the review's Disappointments table, and would read a captain that was in fact fielded as a recommendation ignored (#343). Also ordinal_suffix/ordinal_word
|
|
612
|
+
├── text.py # strip_diacritics (name matching across sources). normalise_name folds the rest of what a retyped name varies by — the apostrophe family onto U+0027, whitespace runs (NFKC, so non-breaking spaces too) and the space after an initial ("B. Fernandes" → "B.Fernandes") — on top of the diacritic strip and lowercasing. Apply it to both sides of a comparison. Shared by everywhere the review matches an LLM-written name against an API one: `validate_research_teams` / `ensure_top_performer_first` in prompts/review.py, and `_normalise_name` in cli/_review_summarisation.py, which layers its parenthetical and leading-initial strips on top for `--compare-recs`. Neither a `\b`-anchored regex nor an equality test survives any of these differences: they cost a valid row in the review's Disappointments table, and would read a captain that was in fact fielded as a recommendation ignored (#343). Also ordinal_suffix/ordinal_word, and ORDINAL_WORDS (to the twentieth, for reading ordinals back out of prose; ordinal_word still spells out only to the tenth)
|
|
612
613
|
└── time.py # format_deadline/format_kickoff/format_generated_at — UK local (Europe/London, auto GMT↔BST). Canonical formatter for every user-facing timestamp.
|
|
613
614
|
|
|
614
615
|
platformdirs (user_config_dir / user_data_dir) # macOS: ~/Library/Application Support/fpl-cli/
|
|
@@ -292,7 +292,7 @@ fpl allocate --format json # JSON output for scripting / skill integrat
|
|
|
292
292
|
| `--sell-prices` | - | Path to sell-prices JSON file (from `fpl squad sell-prices --format json`). Solver uses sell prices for owned players in budget constraint. Budget auto-computed as `sum(sell_prices) + bank` unless `--budget` is explicitly set |
|
|
293
293
|
| `--format` | table | `table` or `json` |
|
|
294
294
|
|
|
295
|
-
Scores ~500 eligible players, adjusts for fixture difficulty over the planning horizon, then solves for the budget-constrained optimum across all
|
|
295
|
+
Scores ~500 eligible players, adjusts for fixture difficulty over the planning horizon, then solves for the budget-constrained optimum across all 8 valid formations. See [Squad Allocator](custom-analysis.md#squad-allocator) for scoring methodology, fixture coefficients, and solver detail.
|
|
296
296
|
|
|
297
297
|
**JSON output fields (horizon >= 2):** `id`, `web_name`, `team`, `position`, `price`, `quality_score` (0-100), `raw_quality` (float), `role` (starter/bench), `captain_gws`. Metadata includes `formation`, `budget_used`, `budget_remaining`, `captain_schedule`, `solver_status`, and `warnings` — the early-season quality notice before GW10 (`early_season_prior_informed`, or `early_season_small_sample` when last season's history could not be loaded and the solver ranked on pure observation), empty otherwise and at horizon 1; table mode prints it to stderr.
|
|
298
298
|
|
|
@@ -853,7 +853,33 @@ a `league_standings_moved_on` warning under `--format json`. The gameweek is rec
|
|
|
853
853
|
either way — an in-progress gameweek is never recappable, so the recap of the one before it
|
|
854
854
|
has to keep working while the next one plays.
|
|
855
855
|
|
|
856
|
+
**Report title:** a saved recap opens with `# Gameweek 4 Recap: <league name>` — the gameweek
|
|
857
|
+
spelled out and the league's name exactly as configured — whether or not it carries an editorial,
|
|
858
|
+
so the first line of every recap reads the same shape every week and in both formats
|
|
859
|
+
([#349](https://github.com/rossgroomio/fpl-cli/issues/349)). The title is written by the report,
|
|
860
|
+
never by the model: with `--summarise` the editorial is asked for one `##` headline beneath it — a
|
|
861
|
+
hook for the gameweek, never the gameweek number, the word "recap" or the league's name — and
|
|
862
|
+
every heading it writes anyway is held to that shape rather than trusted, wherever it sits. One
|
|
863
|
+
that only restates the title (`# GW4 Recap: <league name>`, in any order, spelling or emphasis)
|
|
864
|
+
is dropped, the opening one is kept as the `##` headline with the title's parts removed, a later
|
|
865
|
+
`#` heading is demoted, and a fenced code block is left alone. Only a title-shaped part joined by
|
|
866
|
+
a colon, pipe or spaced dash is removed — a gameweek callback inside a hook ("Bob Never Learns -
|
|
867
|
+
Gameweek 7") stays. The metadata lines sit between the title and the headline, so the editorial —
|
|
868
|
+
headline and prose — is one contiguous block to copy out.
|
|
869
|
+
|
|
856
870
|
**Awards:** GW winner/loser, biggest bench haul, best/worst captain, transfer/waiver genius and disaster.
|
|
871
|
+
Biggest Bench Haul ranks on what the bench actually cost: how many more points the best legal XI
|
|
872
|
+
from the same 15 would have scored than the XI that counted, with the player whose points were
|
|
873
|
+
doubled (the captain, or the vice when the captain did not play) held in place so the multiplier is
|
|
874
|
+
never quietly reassigned — not on the bench's raw points sum, which counts a keeper who could not
|
|
875
|
+
have played alongside the one who did and outfielders no legal formation had room for, and so
|
|
876
|
+
overstated every week and could hand the award to the wrong manager (issue #350). A manager with
|
|
877
|
+
points on the bench but no better XI is not a candidate, and the detail names the swap rather than
|
|
878
|
+
the bench ("Groß (17) and Gvardiol (11) benched while Rice (3) and Colwill (2) played"). Ties go
|
|
879
|
+
the way of the XI that played: a bench player level with a starter is never named, and of two
|
|
880
|
+
formations level on points the one that changes fewer players is the one described. The ledger row
|
|
881
|
+
and `--format json` keep `bench_points` as the raw sum — the API's own `points_on_bench`, the one
|
|
882
|
+
figure the coarse tier can also record — and the gain is re-derived from `squad` rather than stored.
|
|
857
883
|
The draft waiver awards count and label waiver claims and free-agent signings separately (e.g. "across
|
|
858
884
|
2 moves: 1 waiver, 1 free agent") rather than folding both into "waivers". A draft recap has a third
|
|
859
885
|
waiver award, **Most Contested**, on a week where at least three managers claimed the same player:
|
|
@@ -908,9 +934,9 @@ names the gap and forbids numbering the fine instead. `--format json` is
|
|
|
908
934
|
ungated too — `metadata.season_fines` is emitted every
|
|
909
935
|
week, so a scripted consumer never sees it appear and disappear on a calendar it cannot see.
|
|
910
936
|
|
|
911
|
-
**LLM editorial** (`--summarise`): Newsletter-style narrative via synthesis provider. Names names, calls out decisions. Captain picks, chips and transfers each reach it as a full per-manager roster with an explicit count, never through the awards alone — the awards name only the best and worst, so a manager whose pick or move was neither would otherwise be invented or miscounted. The transfer roster carries every move with its points swing, the hit and the post-hit net, names the managers who made none, and lists a manager whose moves could not be fetched as having moved with the moves unknown rather than as standing still. A draft recap gets the same roster for its waivers and free-agent signings: every move as it was made (a rebuild chain is listed move by move, not as the endpoint pair the award prints), each tagged `[waiver]` or `[free agent]` with the labels the waiver awards use, the net per manager, the count of movers, who claimed a player and lost him to a rival — with the priority they spent on him — and who did neither, then every contested player with who won him and who was beaten to him — every race, the two-way ones the Most Contested award never headlines included, in the sentence that award prints (the one thing it may say of anyone's waiver position is that the winner stood higher in it) — so the editorial can say who else wanted a player and no longer sees only the Waiver Genius and Waiver Disaster and is no longer told to say nothing about moves because draft has no `## Transfers` section. The editorial is an add-on: if the synthesis provider has no usable API key the recap still renders, still saves its report and still captures the ledger, with the reason on stderr and a `synthesis_provider_unavailable` warning in JSON. `synthesis_summary` is `null` on such a run — the warning is what distinguishes it from a run that never asked for an editorial. An editorial the provider stopped generating early (a token ceiling, a refusal) is written out as it stands, with the reason on stderr and a `synthesis_stopped_early` warning in JSON — a truncated editorial is otherwise indistinguishable from a whole one.
|
|
937
|
+
**LLM editorial** (`--summarise`): Newsletter-style narrative via synthesis provider. Names names, calls out decisions. Captain picks, chips and transfers each reach it as a full per-manager roster with an explicit count, never through the awards alone — the awards name only the best and worst, so a manager whose pick or move was neither would otherwise be invented or miscounted. The transfer roster carries every move with its points swing, the hit and the post-hit net, names the managers who made none, and lists a manager whose moves could not be fetched as having moved with the moves unknown rather than as standing still. A draft recap gets the same roster for its waivers and free-agent signings: every move as it was made (a rebuild chain is listed move by move, not as the endpoint pair the award prints), each tagged `[waiver]` or `[free agent]` with the labels the waiver awards use, the net per manager, the count of movers, who claimed a player and lost him to a rival — with the priority they spent on him — and who did neither, then every contested player with who won him and who was beaten to him — every race, the two-way ones the Most Contested award never headlines included, in the sentence that award prints (the one thing it may say of anyone's waiver position is that the winner stood higher in it) — so the editorial can say who else wanted a player and no longer sees only the Waiver Genius and Waiver Disaster and is no longer told to say nothing about moves because draft has no `## Transfers` section. The editorial is an add-on: if the synthesis provider has no usable API key the recap still renders, still saves its report and still captures the ledger, with the reason on stderr and a `synthesis_provider_unavailable` warning in JSON. `synthesis_summary` is `null` on such a run — the warning is what distinguishes it from a run that never asked for an editorial. An editorial the provider stopped generating early (a token ceiling, a refusal) is written out as it stands, with the reason on stderr and a `synthesis_stopped_early` warning in JSON — a truncated editorial is otherwise indistinguishable from a whole one. A draft editorial is also checked against the contested races it was given: wherever it says a manager was beaten to a contested player ("beaten to", "missed out on", "lost out on", "lost the race for", "pipped to", "was beat by … to") or beat a rival to him ("beat", "pipped", "edged out", "outbid"), that manager must hold that role on the player's line. The voice comes from the sentence — an auxiliary ("was", "got") or a "by" makes "pipped" and "beat" passive — and a list of managers set against a list of players is read position by position with "respectively" and as every pairing without it. The prompt forbids folding two races into one clause, because that is how a race's winner was once named as beaten in the next (issue #357). An editorial that fails the check is regenerated once, with the wrong claims and the lines they contradict appended to the prompt; the less damaged attempt is kept (a complete one over a cut-off one, then fewer wrong claims), and a retry that fails outright keeps the first attempt. If the kept editorial still fails, the wrong claims are named on stderr, in a `synthesis_contested_misattribution` warning in JSON, and in a warning callout above the editorial in the saved report. The check reads only clauses it can pin to roles — a race verb joined to the player by race phrasing ("to", "for", "on", "the race for") — so a negation, a possessive ("King's bonus"), a points swing ("lost 3 points on King") or a phrasing it does not recognise passes unchecked rather than raising a false alarm. Every editorial, classic or draft, is checked the same way for two more kinds of claim its data contradicts (issue #359). A roster line carries the manager's net overall beside each move's own swing, and a net figure ("-6 net", "net of -6") said of one named move — both its players in the clause — that is not that move's swing but another figure on the same line (the net overall, the swing before the hit, or another move's swing) is flagged, unless the clause makes it the manager's: a word that marks it ("overall", "across both", "his transfers"), the week or the hit beside the figure ("ended the week -6 net", "-6 net after the hit"), the manager named nearer before the figure than the move ("Gibbs-White for Wirtz went -4, and Alice was -6 net"), or the move's own swing stated in the same clause. And a tie word fused to a rank ("joint-lowest", "tied for last", "joint-seventh") is held to who holds that rank: a gameweek score is ranked on the GW Pts column, a league position on the Pos, Prev and Total columns (a head-to-head draft table places managers level on league points apart by points scored, and the Total column still shows them level), and a phrase that names neither (or both) passes if either reading does. To give that rule a line to point at, the prompt's GW Standings section now names the week's highest and lowest scores, says whether each was one manager's alone, and lists every gameweek score and Pos position more than one manager holds, any shared Prev position, and any season totals the league's tie-break placed apart; the roster lines call the manager's net "overall", the word the Transfer Disaster award already used. Both checks share the contested check's single retry (told only about the kinds of claim that failed) and its reporting — stderr, a `synthesis_net_misattribution` or `synthesis_unsupported_tie` warning in JSON, and a line in the report's callout — and both read only clauses they can pin down: a clause naming another manager, two of one manager's moves, or a figure that is nobody's on the line passes, as does a tie about a captain, a bench or a player, a negated one, a word that is not a rank where it stands ("tied last week", "shared first blood"), or a numbered rank among gameweek scores. The checks are advisory: one that raises is skipped with a note on stderr and the editorial saved unchecked, and each warning's message gives one sentence per claim.
|
|
912
938
|
|
|
913
|
-
**Streaks:** notable open streaks print under `Streaks:` on console — leaders only, so console stays a highlights view — and in full as a `# League History` section in the saved report. Each is reported as an observed count over its true span (e.g. "3 in the last 11, with 8 not recorded") rather than a bare "in a row" once any gameweek
|
|
939
|
+
**Streaks:** notable open streaks print under `Streaks:` on console — leaders only, so console stays a highlights view — and in full as a `# League History` section in the saved report. Each is reported as an observed count over its true span (e.g. "3 in the last 11, with 8 not recorded") rather than a bare "in a row" once any gameweek in it did not count towards the run. "Not recorded" is kept for gameweeks the history genuinely lacks; a fully recorded gameweek the streak had nothing to judge in is named for what it was instead — "with no completed moves in 1" for a draft manager whose gameweek moved nobody, whether they made no claims or lost every one, "with no captain fixture in 1" for a captain whose club did not play — and leaves the run open without breaking it. A streak surfaces once its run reaches the condition's own minimum: 2 gameweeks for weeks on top, gameweek wins, last-place finishes, captain blanks and transfer hits; 3 for waiver hauls and backfires. Bottom-half gameweeks and green-arrow droughts never surface as streaks at all — both restate where the table already shows a manager is, so their run exists to drive the season count's firing rule rather than to be read on its own.
|
|
914
940
|
|
|
915
941
|
**Prior seasons:** on the league's opening gameweek — GW1, or the league's own
|
|
916
942
|
`start_event` for one created later — the recap fetches each manager's FPL history
|
|
@@ -970,7 +996,8 @@ gameweek with a file on disk appears there with a status, a damaged one as
|
|
|
970
996
|
(every entry, including those below their reporting minimum and every nonzero season
|
|
971
997
|
count whether or not it grew this gameweek), `season_fines` (the whole
|
|
972
998
|
season tally, emitted every week regardless of the milestone gate the printed surfaces use), `synthesis_summary` (with
|
|
973
|
-
`--summarise
|
|
999
|
+
`--summarise`, as it reaches the saved report — the model's heading already demoted beneath the
|
|
1000
|
+
report's title or dropped, see [Report title](#league-recap)), `warnings`, and `first_capture_store_path` — always present, carrying the
|
|
974
1001
|
partition directory on its first capture and `null` on every run after that. Warning
|
|
975
1002
|
codes are listed under [Capture warnings](#capture-warnings).
|
|
976
1003
|
|
|
@@ -1128,8 +1155,10 @@ notes pack — an unreadable store, or no league id configured for the format.
|
|
|
1128
1155
|
|
|
1129
1156
|
Every capture problem is reported on stderr as prose and, under `--format json`, in
|
|
1130
1157
|
`metadata.warnings` as a `{"code", "message"}` pair. The prose is rewritable; the codes
|
|
1131
|
-
are stable, so scripts should key on those.
|
|
1132
|
-
they share the channel: `synthesis_provider_unavailable
|
|
1158
|
+
are stable, so scripts should key on those. Six non-capture codes share the list, since
|
|
1159
|
+
they share the channel: `synthesis_provider_unavailable`, `synthesis_stopped_early`,
|
|
1160
|
+
`synthesis_contested_misattribution`, `synthesis_net_misattribution`,
|
|
1161
|
+
`synthesis_unsupported_tie` and `league_standings_moved_on`.
|
|
1133
1162
|
|
|
1134
1163
|
| Code | Raised when |
|
|
1135
1164
|
|---|---|
|
|
@@ -1151,11 +1180,15 @@ they share the channel: `synthesis_provider_unavailable` and `league_standings_m
|
|
|
1151
1180
|
| `league_history_standings_repaired` | A gameweek whose recorded positions an *earlier* run had already erased was restored from the ledger itself — the last superseded line that recorded them, or, on draft, a cumulative total re-summed from earlier gameweeks. The *last* one, not the first: a gameweek captured several times while bonus and late results were still landing recorded a stale position early and the settled one last, and only the latest recorded value is the one the damage destroyed. Nothing is re-fetched, so this needs neither `--backfill-detail` nor a network call, and it stops once the damage is repaired. The count is the managers whose league position or cumulative total actually changed — a row rewritten only to restate a fine's player names is not one of them |
|
|
1152
1181
|
| `synthesis_provider_unavailable` | `--summarise` was asked for but the synthesis provider had no usable key; everything else in the recap, the capture included, ran normally |
|
|
1153
1182
|
| `synthesis_stopped_early` | The provider reported that the editorial stopped for a reason other than finishing (a token ceiling, a refusal), so `synthesis_summary` may be cut off mid-sentence. Everything else in the recap is unaffected |
|
|
1183
|
+
| `synthesis_contested_misattribution` | Draft only. The editorial still puts a manager in the wrong role of a contested waiver race after one retry — beaten to a player the race's line does not name them as beaten to, or winning one someone else won. The message quotes each wrong claim beside the contested line it contradicts. `synthesis_summary` is the editorial as written; the saved report carries the same lines in a warning callout above it |
|
|
1184
|
+
| `synthesis_net_misattribution` | Either format. The editorial still pins a figure from a manager's roster line on the wrong move after one retry — the net overall (or the swing before the hit, or another move's swing) said of one named move whose own swing differs. The message quotes each figure, says whose it is, and gives the move's own swing. Saved as written, with the same lines in the report's callout |
|
|
1185
|
+
| `synthesis_unsupported_tie` | Either format. The editorial still calls a gameweek score or league position "joint", "tied", "level" or "shared" after one retry where no two managers hold it. The message quotes the tie and says who holds the rank alone (or that nobody does). Saved as written, with the same lines in the report's callout |
|
|
1154
1186
|
| `league_standings_moved_on` | The recapped gameweek is the most recently finished one, but a later gameweek has started, so the league table no longer describes it. The gameweek is recapped and recorded from each manager's own gameweek history instead — see [When to run it](#league-recap) |
|
|
1155
1187
|
|
|
1156
1188
|
None of these change the exit code — `league-recap` exits 0 whenever the recap itself
|
|
1157
|
-
rendered, and a skipped or
|
|
1158
|
-
`synthesis_stopped_early`
|
|
1189
|
+
rendered, and a skipped, truncated or self-contradicting editorial (`synthesis_provider_unavailable`,
|
|
1190
|
+
`synthesis_stopped_early`, `synthesis_contested_misattribution`, `synthesis_net_misattribution`,
|
|
1191
|
+
`synthesis_unsupported_tie`) is no exception.
|
|
1159
1192
|
|
|
1160
1193
|
Four things do exit 1, all emitting the shared `{"command", "error"}` envelope on stdout
|
|
1161
1194
|
under `--format json` (see [JSON Output](#json-output)) and the same message as red prose
|
|
@@ -1491,7 +1524,7 @@ llm:
|
|
|
1491
1524
|
search_recency_filter: week
|
|
1492
1525
|
synthesis:
|
|
1493
1526
|
provider: anthropic
|
|
1494
|
-
model: claude-sonnet-5
|
|
1527
|
+
model: claude-sonnet-5-5
|
|
1495
1528
|
timeout: 60
|
|
1496
1529
|
query_defaults:
|
|
1497
1530
|
max_tokens: 8000
|
|
@@ -218,7 +218,7 @@ For horizon >= 2, per-player, per-GW fixture coefficients use position-variant s
|
|
|
218
218
|
|
|
219
219
|
### ILP Solver
|
|
220
220
|
|
|
221
|
-
Solves
|
|
221
|
+
Solves 8 independent ILPs (one per valid formation). The objective function maximises the weighted sum of expected contributions across all players and gameweeks:
|
|
222
222
|
|
|
223
223
|
```
|
|
224
224
|
max Σ(gw) Σ(p) discount[gw] × coeff[p][gw] × (starter[p] + bench_discount[p] × bench[p])
|
|
@@ -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.7.
|
|
22
|
-
__version_tuple__ = version_tuple = (2, 7,
|
|
21
|
+
__version__ = version = '2.7.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 7, 3)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
-
from collections.abc import Sequence
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
@@ -11,13 +11,28 @@ import jinja2
|
|
|
11
11
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
12
12
|
|
|
13
13
|
from fpl_cli.agents.base import Agent, AgentResult, AgentStatus
|
|
14
|
-
from fpl_cli.cli.
|
|
14
|
+
from fpl_cli.cli._helpers import performer_score, your_gw_rank_line
|
|
15
|
+
from fpl_cli.cli._league_recap_types import (
|
|
16
|
+
RecapManagerEntry,
|
|
17
|
+
draft_transaction_kind_label,
|
|
18
|
+
recap_title,
|
|
19
|
+
)
|
|
15
20
|
from fpl_cli.paths import TEMPLATE_DIR
|
|
16
21
|
from fpl_cli.services.team_ratings import fdr_columns_footer
|
|
17
|
-
from fpl_cli.utils.text import ordinal_suffix
|
|
22
|
+
from fpl_cli.utils.text import md_table_cell, ordinal_suffix
|
|
18
23
|
from fpl_cli.utils.time import format_generated_at
|
|
19
24
|
|
|
20
25
|
|
|
26
|
+
def _performers_table(performers: Sequence[Mapping[str, Any]]) -> list[str]:
|
|
27
|
+
"""Best/Worst GW Performers rows as a markdown table, blank line after."""
|
|
28
|
+
rows = [
|
|
29
|
+
f"| {p.get('rank_str', '?')} | {'You' if p.get('is_user') else md_table_cell(p.get('name', 'Unknown'))}"
|
|
30
|
+
f" | {performer_score(p)} |"
|
|
31
|
+
for p in performers
|
|
32
|
+
]
|
|
33
|
+
return ["| Rank | Manager | Pts |", "|------|---------|-----|", *rows, ""]
|
|
34
|
+
|
|
35
|
+
|
|
21
36
|
def build_report_environment() -> Environment:
|
|
22
37
|
"""The Jinja environment every report template is rendered through.
|
|
23
38
|
|
|
@@ -39,6 +54,14 @@ def build_report_environment() -> Environment:
|
|
|
39
54
|
# the label is derived at render -- here and in the prompt -- from the
|
|
40
55
|
# same mapping rather than from a second stored field that could drift
|
|
41
56
|
env.filters["kind_label"] = draft_transaction_kind_label
|
|
57
|
+
# Performer ranks render as a table cell, never as a list marker: markdown
|
|
58
|
+
# renumbers `19.` and cannot parse a tie's `3=.` at all (#360). The score
|
|
59
|
+
# and the user's rank note share their wording with the terminal and the
|
|
60
|
+
# synthesis prompt.
|
|
61
|
+
env.globals["performer_score"] = performer_score
|
|
62
|
+
env.globals["your_gw_rank_line"] = your_gw_rank_line
|
|
63
|
+
# Free-text names in a table cell: a `|` would split the row
|
|
64
|
+
env.filters["md_cell"] = md_table_cell
|
|
42
65
|
return env
|
|
43
66
|
|
|
44
67
|
|
|
@@ -508,29 +531,23 @@ class ReportAgent(Agent):
|
|
|
508
531
|
else:
|
|
509
532
|
diff = r.get("total", 0) - user_total
|
|
510
533
|
diff_str = f"+{diff}" if diff > 0 else str(diff)
|
|
511
|
-
name = r.get("manager_name", "Unknown")
|
|
534
|
+
name = md_table_cell(r.get("manager_name", "Unknown"))
|
|
512
535
|
lines.append(f"| {r.get('rank')} | {name} | {r.get('total'):,} | {diff_str} |")
|
|
513
536
|
if cl.get("nearby_rivals_omitted"):
|
|
514
|
-
lines.
|
|
537
|
+
lines.extend(["", f"*...and {cl['nearby_rivals_omitted']} more within 25*"])
|
|
538
|
+
lines.append("")
|
|
515
539
|
|
|
540
|
+
net_suffix = " (Net)" if cl.get("use_net_points") else ""
|
|
516
541
|
if cl.get("best_performers"):
|
|
517
|
-
lines.append("### Best GW Performers")
|
|
518
|
-
|
|
519
|
-
rank = p.get("rank_str", "?")
|
|
520
|
-
lines.append(f"{rank}. {p['name']} - {p['points']} pts")
|
|
542
|
+
lines.append(f"### Best GW Performers{net_suffix}")
|
|
543
|
+
lines.extend(_performers_table(cl["best_performers"]))
|
|
521
544
|
|
|
522
545
|
if cl.get("worst_performers"):
|
|
523
|
-
lines.append("### Worst GW Performers
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
cost = p.get("transfer_cost", 0)
|
|
529
|
-
net = p.get("net_points", gross)
|
|
530
|
-
if cost > 0:
|
|
531
|
-
lines.append(f"{rank}. {name} - {gross} gross, -{cost} hit = {net} net")
|
|
532
|
-
else:
|
|
533
|
-
lines.append(f"{rank}. {name} - {net} pts")
|
|
546
|
+
lines.append(f"### Worst GW Performers{net_suffix}")
|
|
547
|
+
lines.extend(_performers_table(cl["worst_performers"]))
|
|
548
|
+
if cl.get("user_context_row"):
|
|
549
|
+
note = your_gw_rank_line(cl.get("user_gw_rank"), cl.get("gw_field_size"), cl["user_context_row"])
|
|
550
|
+
lines.extend([f"*{note}*", ""])
|
|
534
551
|
if cl.get("transfer_impact"):
|
|
535
552
|
lines.append(f"\n⚠ {cl['transfer_impact']}")
|
|
536
553
|
lines.append("")
|
|
@@ -602,15 +619,11 @@ class ReportAgent(Agent):
|
|
|
602
619
|
|
|
603
620
|
if dl.get("best_performers"):
|
|
604
621
|
lines.append("### Best GW Performers")
|
|
605
|
-
|
|
606
|
-
rank = p.get("rank_str", "?")
|
|
607
|
-
lines.append(f"{rank}. {p['name']} - {p['points']} pts")
|
|
622
|
+
lines.extend(_performers_table(dl["best_performers"]))
|
|
608
623
|
|
|
609
624
|
if dl.get("worst_performers"):
|
|
610
625
|
lines.append("### Worst GW Performers")
|
|
611
|
-
|
|
612
|
-
rank = p.get("rank_str", "?")
|
|
613
|
-
lines.append(f"{rank}. {p['name']} - {p['points']} pts")
|
|
626
|
+
lines.extend(_performers_table(dl["worst_performers"]))
|
|
614
627
|
lines.append("")
|
|
615
628
|
|
|
616
629
|
# Results
|
|
@@ -620,7 +633,7 @@ class ReportAgent(Agent):
|
|
|
620
633
|
])
|
|
621
634
|
if data.get("fixtures"):
|
|
622
635
|
for f in data["fixtures"]:
|
|
623
|
-
lines.
|
|
636
|
+
lines.extend([f"**{f['home_team']} {f['home_score']}-{f['away_score']} {f['away_team']}**", ""])
|
|
624
637
|
if f.get("goals"):
|
|
625
638
|
lines.append(f"- Goals: {f['goals']}")
|
|
626
639
|
if f.get("assists"):
|
|
@@ -657,6 +670,9 @@ class ReportAgent(Agent):
|
|
|
657
670
|
def _generate_league_recap_report(self, gameweek: int, data: dict[str, Any]) -> str:
|
|
658
671
|
"""Generate a league recap report."""
|
|
659
672
|
template = self.jinja_env.get_template("gw_league_recap.md.j2")
|
|
673
|
+
# The H1 is the report's, never the editorial's (#349): the same
|
|
674
|
+
# string the prompt names as already written, so the two agree.
|
|
675
|
+
data["title"] = recap_title(gameweek, data.get("league_name", ""))
|
|
660
676
|
data.setdefault("generated_at", format_generated_at())
|
|
661
677
|
data["standings_block"] = _format_standings_block(data.get("managers", []))
|
|
662
678
|
return template.render(**data)
|
|
@@ -23,7 +23,7 @@ _PROVIDER_LABEL = "Anthropic"
|
|
|
23
23
|
class AnthropicProvider:
|
|
24
24
|
"""LLM provider for the Anthropic Messages API."""
|
|
25
25
|
|
|
26
|
-
DEFAULT_MODEL: ClassVar[str] = "claude-sonnet-5"
|
|
26
|
+
DEFAULT_MODEL: ClassVar[str] = "claude-sonnet-5-5"
|
|
27
27
|
DEFAULT_TIMEOUT: ClassVar[float] = 60.0
|
|
28
28
|
API_KEY_ENV_VAR: ClassVar[str] = "ANTHROPIC_API_KEY"
|
|
29
29
|
KEY_SETUP_URL: ClassVar[str] = "https://console.anthropic.com/"
|