fplkit 2.7.0__tar.gz → 2.7.2__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.0 → fplkit-2.7.2}/.agents/TOOLS.md +2 -2
- {fplkit-2.7.0 → fplkit-2.7.2}/CHANGELOG.md +31 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/PKG-INFO +6 -1
- {fplkit-2.7.0 → fplkit-2.7.2}/README.md +5 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/docs/architecture.md +6 -6
- {fplkit-2.7.0 → fplkit-2.7.2}/docs/command-reference.md +36 -6
- {fplkit-2.7.0 → fplkit-2.7.2}/docs/custom-analysis.md +1 -1
- {fplkit-2.7.0 → fplkit-2.7.2}/docs/fpl-rules.md +10 -4
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/_version.py +2 -2
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/agents/orchestration/report.py +8 -1
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/api/fpl_draft.py +97 -16
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/_league_recap_data.py +163 -22
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/_league_recap_types.py +49 -18
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/_review_classic.py +6 -1
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/_review_draft.py +5 -3
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/_review_summarisation.py +13 -5
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/allocate.py +2 -2
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/league.py +24 -2
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/league_recap.py +9 -1
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/prompts/league_recap.py +102 -1
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/prompts/review.py +46 -9
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/scoring/constants.py +7 -2
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/scoring/single_gw.py +1 -1
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/templates/gw_league_recap.md.j2 +2 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/utils/markdown.py +25 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/utils/text.py +42 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_api_draft.py +129 -6
- fplkit-2.7.2/tests/test_cli_league.py +161 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_league_recap.py +75 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_review.py +67 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_league_recap.py +657 -23
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_preseason.py +6 -1
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_review.py +114 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_scoring.py +5 -2
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_utils_markdown.py +34 -0
- fplkit-2.7.2/tests/test_utils_text.py +106 -0
- fplkit-2.7.0/tests/test_cli_league.py +0 -67
- fplkit-2.7.0/tests/test_utils_text.py +0 -54
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/README.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/build-test-plan/SKILL.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/build-test-plan/references/plan-template.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/build-test-plan/references/workspace-env.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/create-pr/SKILL.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/gw-prep/SKILL.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/gw-prep/references/entity-normalisation.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/gw-prep/references/output-template.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/gw-prep/references/rules.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/gw-prep/scripts/_bootstrap.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/gw-prep/scripts/bench_order.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/gw-prep/scripts/extract_classic_squad.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/gw-prep/scripts/normalise_entities.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/gw-prep/scripts/starting_xi.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/gw-prep/scripts/transfer_eval.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/gw-prep/scripts/validate_draft_waivers.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/preview-ingest/SKILL.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/release/SKILL.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/release-notes/SKILL.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/squad-builder/SKILL.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/squad-builder/references/output-template.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/squad-builder/references/rules.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.agents/skills/update-gw-prep/SKILL.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.claude/output-styles/fpl-mate.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.claude/settings.json +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.env.example +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.github/workflows/ci.yml +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.github/workflows/pr-title.yml +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.github/workflows/release.yml +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/.gitignore +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/AGENTS.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/CLAUDE.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/CONTRIBUTING.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/LICENSE +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/cliff.toml +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/config/team_ratings_overrides.yaml +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/docs/images/fpl-player-demo-v1-0.png +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/__init__.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/agents/__init__.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/agents/action/__init__.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/agents/action/waiver.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/agents/analysis/__init__.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/agents/analysis/bench_order.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/agents/analysis/captain.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/agents/analysis/squad_analyzer.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/agents/analysis/starting_xi.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/agents/analysis/stats.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/agents/analysis/transfer_eval.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/agents/base.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/agents/common.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/agents/data/__init__.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/agents/data/fixture.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/agents/data/price.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/agents/data/scout.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/agents/orchestration/__init__.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/api/__init__.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/api/contract.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/api/core_insights.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/api/dataset_fetcher.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/api/football_data.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/api/fpl.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/api/historical.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/api/historical_types.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/api/providers/__init__.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/api/providers/_http.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/api/providers/_models.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/api/providers/anthropic.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/api/providers/openai_compat.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/api/providers/perplexity.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/api/understat.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/api/vaastav.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/__init__.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/_banner.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/_context.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/_fines.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/_fines_config.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/_helpers.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/_json.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/_league_recap_history.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/_plan_grid.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/_review_analysis.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/captain.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/chips.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/credentials.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/differentials.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/doctor.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/doctor_providers.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/fdr.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/fixtures.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/history.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/init.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/intel.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/league_fines.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/player.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/preview.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/price_changes.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/price_history.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/ratings.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/returnees.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/review.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/sell_prices.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/squad.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/stats.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/status.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/targets.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/transfer_eval.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/waivers.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/cli/xg.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/config/defaults.yaml +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/config/fixture_predictions.yaml +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/config/previews/EXAMPLE.yaml +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/config/team_managers.yaml +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/constants.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/models/__init__.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/models/chip_plan.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/models/fixture.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/models/league_history.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/models/player.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/models/team.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/models/types.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/parsers/__init__.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/parsers/recommendations.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/paths.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/prompts/__init__.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/prompts/returnees.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/prompts/scout.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/scraper/__init__.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/scraper/fpl_prices.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/season.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/__init__.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/fixture_predictions.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/league_history.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/league_history_counters.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/league_history_fines.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/league_history_notes.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/matchup.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/player_clubs.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/player_prior.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/returnee_radar.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/scoring/__init__.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/scoring/data_prep.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/scoring/display.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/scoring/evaluation.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/scoring/ownership.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/scoring/shrinkage.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/scoring/signals.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/scoring/value_quality.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/season_previews.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/squad_allocator.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/team_form.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/team_ratings.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/services/team_ratings_prior.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/templates/gw_preview.md.j2 +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/templates/gw_review.md.j2 +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/utils/__init__.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/utils/files.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/utils/gameweek.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/utils/teams.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/fpl_cli/utils/time.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/pyproject.toml +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/pyrightconfig.json +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/scripts/backtest_early_season_prior_blend.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/scripts/calibrate_promoted_prior.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/scripts/calibrate_quality_ceilings.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/__init__.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/conftest.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/fixtures/classic_squad_fixture.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/fixtures/recommendations_fixture.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/fixtures/validate_draft_waivers/recs_clean.md +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/fixtures/validate_draft_waivers/squad_grid.json +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/fixtures/validate_draft_waivers/waivers.json +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/fixtures/waiver_ranked_golden.json +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_agents_action.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_agents_analysis.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_agents_analysis_squad.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_agents_base.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_agents_data.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_agents_data_price.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_agents_orchestration.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_allocate.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_api_client.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_api_core_insights.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_api_dataset_fetcher.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_api_football_data.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_api_historical.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_api_understat.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_api_vaastav.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_bench_order_script.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_calibrate_quality_ceilings.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_classic_squad_extraction.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_allocate.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_banner.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_captain.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_chips.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_context_settings.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_credentials.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_custom_analysis.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_differentials.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_doctor.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_doctor_providers.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_failure_parity.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_failure_streams.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_fdr.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_fdr_blanks.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_fixtures.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_format.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_format_suppression.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_fpl_block.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_history.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_init.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_intel.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_json.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_json_contract.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_league_fines.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_plan_grid.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_player.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_preview.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_price_changes.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_price_history.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_returnees.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_sell_prices.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_settings_warning.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_squad.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_stats.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_status.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_targets.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_transfer_eval.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_waivers.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_warnings_stderr.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_cli_xg.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_common.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_common_scoring.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_data_prep_harmonisation.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_dependencies.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_doc_consistency.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_fines.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_fines_config.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_fixture_predictions.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_fpl_prices.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_gw_prep_script_bootstrap.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_init.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_init_fines.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_league_history_counters.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_league_history_fines.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_league_history_notes.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_league_history_store.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_matchup_service.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_models.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_normalise_entities.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_output_style_sync.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_paths.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_player_clubs.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_player_prior.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_player_resolution.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_prompt_tables.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_providers.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_recommendations_parser.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_report_agent.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_report_paths.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_returnee_radar.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_rolling_pts_per_m.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_scraper.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_season.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_season_awareness.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_season_previews.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_sell_prices.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_skill_fdr_quality_signals.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_skill_stats_thresholds.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_squad_allocator.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_starting_xi_agent.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_starting_xi_script.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_stats.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_team_ratings.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_team_ratings_prior.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_transfer_eval_agent.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_transfer_eval_script.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_utils_gameweek.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_utils_time.py +0 -0
- {fplkit-2.7.0 → fplkit-2.7.2}/tests/test_validate_draft_waivers.py +0 -0
|
@@ -39,7 +39,7 @@ with `2>/dev/null` without losing the table, whichever command it runs.
|
|
|
39
39
|
| `player` | Look up a player's stats, xG, ownership and fixture run. Before GW10 `quality_score` blends last season's pedigree in and JSON `metadata.warnings` says so (`early_season_prior_informed`, or `early_season_small_sample` when history could not be loaded), same as `fpl stats --value`; `understat_team_unmatched` joins it for any club Understat carried no players for, whose whole squad loses xG enrichment. JSON includes `ep_next` (predicted pts next GW) and `ep_this` (current GW), emitting `null` when FPL has no projection; panel shows `xPts` (ep_next) when available, omits the segment when FPL has no projection. `--detail` (`-d`) shows GW-by-GW performance and, for FWD/MID with sufficient history, xGI sustainability (per-match GI-xGI divergence -> form modifier). With `custom_analysis`: JSON adds `info.adjusted_npxg_per_90` (fixture-adjusted) and `info.raw_npxg_per_90` (Understat season avg); panel shows `adj. npxG/90: X.XXX (raw: Y.YYY)` | Yes | General | No | direct-api |
|
|
40
40
|
| `stats` | List players with filtering and sorting. `--value` adds quality/value per £m columns; `--window N` sets rolling lookback (3-10) for `rolling_pts_per_m`. Before GW10, `--value` quality scores blend last season's pts/90 pedigree with this season's observation and the command says so (stderr notice quoting the observation's weight band; JSON `metadata.warnings` code `early_season_prior_informed`); when last season's history cannot be loaded they are pure observation and the notice says that instead (`early_season_small_sample`, before GW6). `understat_team_unmatched` is raised once per club Understat carried no players for, whose whole squad scores without xG. GK ceilings scale with the calendar-possible sample, reaching full scale at GW6. Sortable by `ep_next`/`ep_this` (FPL predicted points; `ep_this` is only meaningful before the gameweek's first kickoff — FPL rolls it forward to `ep_next` as each player's match finishes); both in JSON output (emit `null` when FPL has no projection; table renders `—`). Players with no projection sort to the bottom in either direction | Yes | General | No | direct-api |
|
|
41
41
|
| `history` | Show historical player performance across seasons. A provider that refuses exits 1 in both output formats, carrying the upstream's own reason: the error envelope used to say only `Failed to fetch historical data` while the cause went to stderr, and table mode printed the cause to stdout and exited 0 | Yes | General | No | direct-api |
|
|
42
|
-
| `league` | Show live league standings for Classic and Draft leagues | No | General | No | direct-api |
|
|
42
|
+
| `league` | Show live league standings for Classic and Draft leagues. Classic positions are competition-ranked from cumulative totals — managers level on points share a place and the next distinct total skips the places they consumed — matching `fpl review` and the league history ledger rather than the standings API's sequential rank, which splits such a tie on fewest transfers season-to-date, a figure no column shows. Draft keeps the league's own rank, which already applies the head-to-head points-for tie-break | No | General | No | direct-api |
|
|
43
43
|
| `fdr` | Analyse fixture difficulty - easy runs, blanks, doubles. Three payloads, not one: the default call runs the agent and returns the full analysis plus `data.ratings_warning` (the team-ratings quality verdict, and with `--my-squad` also `data.predictions_stale` / `data.prediction_warnings` / `data.squad_exposure`); with custom analysis off it returns raw 1-5 FPL API difficulty (`data.easy_fixture_runs` only, `metadata.custom_analysis: false`, no ratings and so no warning); `--blanks` bypasses the agent for a schedule-only payload carrying neither the FDR analysis nor `ratings_warning`. `--blanks` mixes confirmed fixtures with predictions; when those predictions are stale, table mode warns on stderr and JSON adds a `fixture_predictions_stale` entry to `metadata.warnings` | Yes | General | No | via-agent |
|
|
44
44
|
| `xg` | Analyse underlying stats: xG, xA, overperformers. The minutes floor and the window scale to the gameweeks played (`min(60 x window, 45 x gameweeks played)`, so 90 minutes over 2 GWs rather than an unreachable 360 over 6); the configured bar binds from GW9, or GW11 for `--all`. JSON `metadata` carries `window_label`, `gameweeks_played` and `min_minutes`, and `metadata.warnings` an `early_season_minutes_floor` notice while the bar is scaled, plus `understat_team_unmatched` for any club Understat carried no players for. When nothing qualifies, `data.empty_reason` says whether the floor (`below_minutes_floor`) or the data (`no_minutes_played`) caused it | Yes | General | No | via-agent |
|
|
45
45
|
| `price-changes` | Show price changes and transfer activity | No | General | No | via-agent |
|
|
@@ -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. 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 |
|
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
|
+
## [2.7.1] - 2026-09-09
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- stop the review mismatching names over spacing or apostrophes (#346)
|
|
9
|
+
- give managers level on points the same position in `fpl league` (#345)
|
|
10
|
+
- stop a draft waiver a manager won reading as one he lost (#347)
|
|
11
|
+
|
|
12
|
+
## [2.7.0] - 2026-09-08
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
- never record rank movement out of a league's first gameweek (#325)
|
|
17
|
+
- stop the gameweek review narrative inventing its own counts (#328)
|
|
18
|
+
- keep the settled league position, not a mid-settlement one (#331)
|
|
19
|
+
- keep players who have never appeared off the review's Blankers list (#333)
|
|
20
|
+
- stop draft recaps reading an outbid manager as one who never tried (#334)
|
|
21
|
+
- mark the reader's own row in the saved review's Nearby Rivals table (#335)
|
|
22
|
+
- give managers level on points the same position in the review (#338)
|
|
23
|
+
- record the waiver claims a draft manager lost in the league ledger (#339)
|
|
24
|
+
- fine every manager who ties for last, not just one of them (#340)
|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
|
|
28
|
+
- surface contested waiver claims in the draft recap (#341)
|
|
29
|
+
|
|
30
|
+
### Other
|
|
31
|
+
|
|
32
|
+
- No AFCON this season
|
|
33
|
+
- add back mention of AFCON transfers
|
|
34
|
+
|
|
4
35
|
## [2.6.1] - 2026-09-07
|
|
5
36
|
|
|
6
37
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fplkit
|
|
3
|
-
Version: 2.7.
|
|
3
|
+
Version: 2.7.2
|
|
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
|
|
@@ -116,6 +116,11 @@ so `review --summarise` with a key for one provider prints that half, says on st
|
|
|
116
116
|
why the other was skipped, and still exits 0; a saved report names each skipped half
|
|
117
117
|
in its own warning callout. `league-recap --summarise` does the same for its editorial.
|
|
118
118
|
|
|
119
|
+
A saved recap opens with a fixed title — `# Gameweek 4 Recap: <league name>`, the league's name
|
|
120
|
+
as configured — whether or not it has an editorial. The editorial's own headline sits beneath it
|
|
121
|
+
as a `##` subheading, never as the title, so the first line you paste into the group chat reads
|
|
122
|
+
the same every week and the name on it is the league's rather than one the model restyled.
|
|
123
|
+
|
|
119
124
|
Fines are ruled per gameweek by `league-recap` and recorded against it, so `fpl
|
|
120
125
|
league-fines` reads them straight back off disk — no network, and any season still
|
|
121
126
|
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
|
|
@@ -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)
|
|
@@ -547,7 +547,7 @@ fpl_cli/
|
|
|
547
547
|
│ ├── contract.py # Runtime tripwires: CSV header checks + empty-output warnings for dataset drift
|
|
548
548
|
│ ├── dataset_fetcher.py # DatasetFetcher (disk cache with ETag/TTL for GitHub CSVs)
|
|
549
549
|
│ ├── fpl.py # FPLClient (main API, caches bootstrap-static)
|
|
550
|
-
│ ├── fpl_draft.py # FPLDraftClient + match_draft_to_main() + the transaction `result` vocabulary (`is_accepted_transaction()` / `
|
|
550
|
+
│ ├── fpl_draft.py # FPLDraftClient + match_draft_to_main() + the transaction `result` vocabulary (`is_accepted_transaction()` / `is_denied_on_the_incoming_player()` / `resolve_lost_claims()`, issues #329, #342). The feed carries denied claims alongside accepted ones, and the two denial codes say which side of the claim was already gone rather than who took it: `di` is the incoming player, `do` the nominated drop. Neither is a defeat on its own -- the engine processes claims league-wide in `index` order, so a manager's own winning claim removes the incoming player exactly as a rival's does, and a manager who wins a player he also listed lower down gets a `di` for the player he just won. What a manager actually lost is therefore only answerable from his whole gameweek, which is `resolve_lost_claims()`: it drops a `di` only where a claim of that manager's had *already taken* the player -- ordering, not end-of-gameweek ownership, since free agency runs after the whole waiver batch and a pickup there cannot have caused an earlier denial -- and returns a player he claimed several times once, at the best priority he spent, the same two rules `contested_draft_claims` states. Consumers go through these rather than comparing `result` themselves, so a move ledger never counts a denied row, an activity question never misses one, and a race a manager won is never reported as one he lost
|
|
551
551
|
│ ├── understat.py # UnderstatClient + match_fpl_to_understat(), understat_club_rows() (the club gate the matcher and `fpl doctor --providers` share), understat_join_warnings(), understat_name_join_stats() (the name-level join rate, the signal a silently unmatched player had none of), decode_entities() (Understat escapes its payload for HTML, so it is decoded once at the fetch boundary), matches_in_season() (the season guard behind get_team())
|
|
552
552
|
│ ├── historical_types.py # Shared dataclasses (SeasonHistory — incl. the four optional per-90 DEF/GK rates only Core-Insights publishes, PlayerProfile, GwTrendProfile) + compute_trend/compute_acceleration/compute_reliability
|
|
553
553
|
│ ├── vaastav.py # VaastavClient (the two oldest seasons of the window via DatasetFetcher)
|
|
@@ -594,9 +594,9 @@ fpl_cli/
|
|
|
594
594
|
│ └── types.py # TypedDicts: CaptainCandidate, WaiverTarget, EnrichedPlayer, etc.
|
|
595
595
|
├── prompts/
|
|
596
596
|
│ ├── scout.py # ScoutAgent system/user prompts
|
|
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
|
|
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
|
-
│ └── 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
|
|
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
|
|
600
600
|
├── parsers/
|
|
601
601
|
│ └── recommendations.py # Parse gw{N}-recommendations.md into structured decisions
|
|
602
602
|
├── scraper/
|
|
@@ -608,7 +608,7 @@ fpl_cli/
|
|
|
608
608
|
├── gameweek.py # is_opening_gameweek(gw) — shared GW1 check (transfers, waivers and league tables don't exist yet)
|
|
609
609
|
├── 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
610
|
├── 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)
|
|
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
612
|
└── time.py # format_deadline/format_kickoff/format_generated_at — UK local (Europe/London, auto GMT↔BST). Canonical formatter for every user-facing timestamp.
|
|
613
613
|
|
|
614
614
|
platformdirs (user_config_dir / user_data_dir) # macOS: ~/Library/Application Support/fpl-cli/
|
|
@@ -712,5 +712,5 @@ A settings block present but empty — every line under it commented out — par
|
|
|
712
712
|
- **One allocation and one merge for the historical sources.** `historical_season_windows()` (`api/historical.py`) is the only place that decides which of the four seasons a profile spans come from Core-Insights (the newest two, all it publishes) and which from vaastav (the two before), and `make_historical_provider()` and `fpl doctor --providers` both read it. `merge_season_histories()` then keeps one row per `(element_code, season)` with Core-Insights outranking vaastav and announces any repeat, so provenance is a decision rather than two window expressions happening to be disjoint: before #101 a widened window would have doubled a season in every per-season mean and taken two of the reliability window's three slots without raising.
|
|
713
713
|
- **Season data frozen in code is surfaced, not guarded.** Every other season-derived artefact rolls or invalidates itself — the historical window rolls at the July cutover, a stale `player_prior.yaml` or `returnee_snapshot.json` is discarded and rebuilt. The calibrated quality ceilings cannot: they are measurements of a completed season's player cohort, and only re-running `scripts/calibrate_quality_ceilings.py --write` against a newly completed season can replace them. So the generated block records `CALIBRATION_SEASON` beside the fingerprint and `fpl doctor` compares it against the newest completed season (#128). It reports, rather than fails, on purpose: anchors from last season are correct for the whole season being played, so the mid-season state is not a defect, and a rollover changes the truth of the block with no commit behind it — a CI guard would have nothing to fire on until someone touched the code, which is exactly the case it is not for.
|
|
714
714
|
- **Who a player *was* is remembered; where he played is re-derived.** A replay resolves every pick against today's bootstrap, so a player transferred or renamed since lands on a past gameweek's row wearing his current club and current name (issue #169). `_carry_recorded_identity` answers that by keeping what the gameweek already recorded, which works only where something *was* recorded — and two cases have nothing: a first capture, and a coarse row being upgraded, since the coarse tier stores headline numbers and no squad at all. The second is the documented workflow, so a mid-season first run plus `--backfill-detail` writes today's clubs across a whole season (issue #177). `services/player_clubs.py` closes the club half from the gameweek itself instead of from a prior row: the live payload records which fixtures each player's club played, a fixture names its two clubs, and the intersection is where he was — settled outright by a double, narrowed to a pair by a single, and paid for with one `element-summary` request only where today's club is absent from that pair, which is proof he moved and the one case the pair cannot resolve. Because it is *derived* rather than remembered, it also supersedes a recorded club, where name and position are only ever carried: a row a first capture or a coarse upgrade wrote holds today's club as fact, so carrying it forward would preserve the mistake permanently — the same earliest-not-winner lesson as #169, applied to a value the gameweek can recompute. Which makes the *precision* of "derived" load-bearing in both directions. A derivation must supersede a record even when it agrees with today's club, or a player who left and came back keeps a stale row forever; and an assumption must never be called a derivation, or today's club overwrites a correctly recorded one — which is why the ambiguous single-fixture case answers nothing rather than answering "he has not moved". Name and position stay carry-forward-only because fixtures say nothing about them, and a blanking club has nothing to intersect; both fall back to the recorded value, then to today's. A lost waiver claim names two players the way a move does and a replay restamps both the same way, so it is carried by the same rule — read off the earliest line that recorded any claims rather than the earliest squad line, since a gameweek captured before schema version 6 holds its squad on a line that records nothing about them (issue #332).
|
|
715
|
-
- **Absence from the accepted set is not inactivity.** The draft transaction feed carries every claim the league processed, and a claim a rival won is evidence the manager was active — but the ledger held completed moves alone, so a manager whose whole gameweek was one lost claim was written into the permanent record as having sat it out, and the recap named real people as idle on that basis (issues #329, #332). The row now carries `lost_claims` beside `transactions` rather than a result code inside it: nothing moved, so a lost claim has no points and no net, and folding it into a list every consumer reads as completed moves — the waiver streaks sum `net` over it, the identity carry pairs it slot for slot — would have made each of them learn to skip entries. Only
|
|
715
|
+
- **Absence from the accepted set is not inactivity.** The draft transaction feed carries every claim the league processed, and a claim a rival won is evidence the manager was active — but the ledger held completed moves alone, so a manager whose whole gameweek was one lost claim was written into the permanent record as having sat it out, and the recap named real people as idle on that basis (issues #329, #332). The row now carries `lost_claims` beside `transactions` rather than a result code inside it: nothing moved, so a lost claim has no points and no net, and folding it into a list every consumer reads as completed moves — the waiver streaks sum `net` over it, the identity carry pairs it slot for slot — would have made each of them learn to skip entries. Only genuine defeats are recorded, which neither denial code identifies by itself: `do` is the cascade behind a success, and `di` covers one too, since the manager's own winning claim removes the incoming player just as a rival's does (issue #342). `resolve_lost_claims()` settles both from the manager's whole gameweek, on the order the league processed it rather than on who owned the player by the end — so a waiver lost to a rival who then dropped him, and signed in free agency afterwards, stays the defeat it was — and dedupes a target he offered against several drops down to the one player he wanted. The field is three-state on purpose: a list is what the feed held, `[]` included, and `None` says nothing was recorded — a classic row, an unknown row, or a row written before the field existed. That last state is what makes the backfill work without a migration: `coverage()` counts such rows on draft partitions, `_gaps` hands those gameweeks to the detailed tier behind `--backfill-detail`, the replay re-records them from the feed while it still serves the season, and because `[]` differs in content from `None` the superseding line lands once and the target converges. Presence is carried as well as identity: the feed is a log, so a finished gameweek's claims are all in it and a replay that finds fewer has lost some — a player it can no longer place, or a gameweek the feed has stopped serving — and `_carry_recorded_claims` keeps the longest recorded list rather than letting the remnant supersede it, while a longer replayed list still lands as the correction it is.
|
|
716
716
|
- **A row that knows less never supersedes one that knows more.** The ledger is append-only and reads take the newest row per manager, so a capture that writes a null over a recorded value destroys it as surely as an edit would — and permanently. Draft is where that bites: it has no per-manager history endpoint and its standings always describe the current gameweek, so a *replayed* draft gameweek derives no league position and no cumulative total at all. Writing those rows out cost the ledger every position that gameweek held, which is the one input the streak and season-count projections have, and each replay left a full set of dead lines behind (issue #223). Three things stop it. `_carry_recorded_standings` fills a null from the *latest-captured* line that recorded the field — read off the whole file rather than off `resolved_gameweek`'s winner, and ordered by `captured_at` rather than by position in it, since `append_rows` skips on tier rank alone and so does not guarantee the two agree — the same read-past-the-winner move `_carry_recorded_identity` makes, and for the same reason: by the time anyone notices, the degraded row is the winner. What keeps a degraded row out is the value being non-null, not the line being earliest, and that holds whichever way the pass runs; direction only settles which recorded value wins when several disagree, and there the later one is right — a gameweek captured repeatedly while bonus and late results were still landing holds a stale position in its first capture and the settled one in its last (issue #320). Identity stays earliest-first, because a name and a position do not settle over a gameweek the way a table does. `_fill_draft_standings` reconstructs what the ledger can supply on its own, summing a cumulative total from earlier gameweeks and ranking from it (at the league's start gameweek the total *is* the gameweek score, so `gross_points` alone settles that table), and derives a whole table or none of it. Every manager needs a total, because ranking the subset that summed cleanly would renumber everyone below whoever was left out; and no row may already carry a position, because `_assign_cohort_ranks` fills only the nulls, so a cohort part-carried and part-re-derived is ranked against two tables at once and lands two managers on the same place with nobody on the next. Draft breaks a head-to-head tie on points-for, which a cumulative total cannot reproduce, so those two rankings disagree in the ordinary case rather than only on a tie — and a null, which every streak condition holds across, beats a wrong position recorded as fact. `_repair_recorded_standings` then sweeps already-damaged gameweeks, ascending, on every run: it re-fetches nothing, so it needs neither `--backfill-detail` nor a request, and it is the only path that heals a ledger damaged before this existed. Filling only nulls is what makes all three idempotent — a genuine correction still lands, and a replay that reproduces a gameweek exactly now writes no line at all.
|
|
@@ -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
|
|
|
@@ -750,7 +750,7 @@ fpl review --dry-run # Build prompts without calling LLMs
|
|
|
750
750
|
|
|
751
751
|
**Classic:** team summary, player-by-player breakdown (captain doubled/tripled), transfer assessment, league standings with nearby rivals, best/worst performers.
|
|
752
752
|
|
|
753
|
-
**League position:** your own position and the `Pos` column of Nearby Rivals are competition-ranked from the cumulative totals, the same numbering `fpl league-recap` writes to the ledger: managers level on points share a place and the next distinct total skips the places they consumed (1, 2, 2, 4). The standings API hands out strictly sequential ranks instead, splitting a tie on fewest transfers season-to-date — a figure no column here shows — so the same gameweek used to place the same tie differently depending on which report you read.
|
|
753
|
+
**League position:** your own position and the `Pos` column of Nearby Rivals are competition-ranked from the cumulative totals, the same numbering `fpl league`'s table shows and `fpl league-recap` writes to the ledger: managers level on points share a place and the next distinct total skips the places they consumed (1, 2, 2, 4). The standings API hands out strictly sequential ranks instead, splitting a tie on fewest transfers season-to-date — a figure no column here shows — so the same gameweek used to place the same tie differently depending on which report you read.
|
|
754
754
|
|
|
755
755
|
**Draft:** squad breakdown, transaction assessment, league standings, best/worst performers.
|
|
756
756
|
|
|
@@ -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:
|
|
@@ -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
|
|
|
@@ -1043,9 +1070,12 @@ priority it was made at. They sit apart from `transactions`, which stays the lis
|
|
|
1043
1070
|
that happened — nothing moved here, so there are no points on either side and no net —
|
|
1044
1071
|
because a manager whose whole gameweek was one claim they lost has no completed move,
|
|
1045
1072
|
and a ledger holding moves alone wrote them into the permanent record as having sat the
|
|
1046
|
-
week out. Only claims
|
|
1047
|
-
own
|
|
1048
|
-
|
|
1073
|
+
week out. Only claims a rival won are recorded, which the feed's denial codes do not
|
|
1074
|
+
identify on their own: a claim denied because the manager's own earlier accepted claim
|
|
1075
|
+
had already dropped the nominated player is the cascade behind a success, and so is a
|
|
1076
|
+
claim denied because he had just won the incoming player himself — both are read against
|
|
1077
|
+
his whole gameweek and neither appears. A player he claimed several times against
|
|
1078
|
+
different drops is one claim, at the best priority he spent on him (see
|
|
1049
1079
|
[Conditional chains and outcomes](fpl-rules.md#waivers)). A replay that finds fewer
|
|
1050
1080
|
claims than the gameweek recorded keeps the recorded list and says so
|
|
1051
1081
|
(`league_history_claims_carried`): a claim whose player can no longer be placed is
|
|
@@ -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])
|
|
@@ -189,15 +189,21 @@ Processed **24 hours before the GW deadline**. After processing, **free agency**
|
|
|
189
189
|
|
|
190
190
|
**Locked players** (unavailable for waivers): players added to the game within 24 hours of the waiver deadline, or players recently dropped from another squad. Locked players become available at the next waiver deadline.
|
|
191
191
|
|
|
192
|
-
**Conditional chains and outcomes.** A manager submits several claims in their own priority order
|
|
192
|
+
**Conditional chains and outcomes.** A manager submits several claims in their own priority order. Claims may share a drop, and may name one target against several drops, so only one of a group is ever expected to land. Claims are processed league-wide in a single order, and each ends in one of three states:
|
|
193
193
|
|
|
194
194
|
| Outcome | Meaning |
|
|
195
195
|
|---|---|
|
|
196
196
|
| Accepted | The claim was processed; the incoming player joined the squad and the nominated player was dropped |
|
|
197
|
-
|
|
|
198
|
-
| Drop already used | An earlier accepted claim by the same manager had already dropped the nominated player, so this one could not be processed.
|
|
197
|
+
| Incoming player gone | Someone had already taken him by the time this claim processed, so it could not be filled |
|
|
198
|
+
| Drop already used | An earlier accepted claim by the same manager had already dropped the nominated player, so this one could not be processed. It cannot occur without that earlier accepted claim |
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
Neither denial says on its own whether the manager lost anything, because "someone" in the second row includes the manager himself. A manager who wins a player at one priority and listed him again lower down gets an "incoming player gone" for the player he just won — the same self-cascade the third row describes, arriving on the other side of the claim. Only a denial where a **different** manager took the player is a defeat.
|
|
201
|
+
|
|
202
|
+
So a claim's outcome is only readable against the manager's whole gameweek, never from the claim alone. Two rules follow: a denial for a player one of the manager's own claims had **already taken** is his own cascade, not a loss; and a player he claimed several times and lost was wanted once, not once per drop he offered.
|
|
203
|
+
|
|
204
|
+
The first rule turns on order, not on who ends the gameweek owning the player. Waivers process as one batch and free agency opens only afterwards, so a manager who loses a waiver to a rival, and signs the same player once that rival drops him, still lost the waiver.
|
|
205
|
+
|
|
206
|
+
Only accepted claims changed a squad, so only they belong in a record of moves. But a manager who submitted claims and genuinely lost them all was active, and reading their absence from the accepted set as inactivity misreports them.
|
|
201
207
|
|
|
202
208
|
### Free Agents
|
|
203
209
|
Unclaimed players can be picked up first-come, first-served in the **24-hour window before each GW deadline**. Free agent pickups don't affect waiver priority.
|
|
@@ -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.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 7, 2)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -11,7 +11,11 @@ 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._league_recap_types import
|
|
14
|
+
from fpl_cli.cli._league_recap_types import (
|
|
15
|
+
RecapManagerEntry,
|
|
16
|
+
draft_transaction_kind_label,
|
|
17
|
+
recap_title,
|
|
18
|
+
)
|
|
15
19
|
from fpl_cli.paths import TEMPLATE_DIR
|
|
16
20
|
from fpl_cli.services.team_ratings import fdr_columns_footer
|
|
17
21
|
from fpl_cli.utils.text import ordinal_suffix
|
|
@@ -657,6 +661,9 @@ class ReportAgent(Agent):
|
|
|
657
661
|
def _generate_league_recap_report(self, gameweek: int, data: dict[str, Any]) -> str:
|
|
658
662
|
"""Generate a league recap report."""
|
|
659
663
|
template = self.jinja_env.get_template("gw_league_recap.md.j2")
|
|
664
|
+
# The H1 is the report's, never the editorial's (#349): the same
|
|
665
|
+
# string the prompt names as already written, so the two agree.
|
|
666
|
+
data["title"] = recap_title(gameweek, data.get("league_name", ""))
|
|
660
667
|
data.setdefault("generated_at", format_generated_at())
|
|
661
668
|
data["standings_block"] = _format_standings_block(data.get("managers", []))
|
|
662
669
|
return template.render(**data)
|
|
@@ -17,22 +17,28 @@ logger = logging.getLogger(__name__)
|
|
|
17
17
|
BASE_URL = "https://draft.premierleague.com/api"
|
|
18
18
|
|
|
19
19
|
# The Draft API's transaction `result` codes. Every row the league processed
|
|
20
|
-
# carries one, and the two denial codes
|
|
21
|
-
#
|
|
22
|
-
# made" is wrong for one of them and right for the other (issue #329).
|
|
20
|
+
# carries one, and the two denial codes say which side of the claim was
|
|
21
|
+
# already gone -- not who took it (issues #329, #342).
|
|
23
22
|
#
|
|
24
23
|
# `a` -- the league processed the claim into a completed move.
|
|
25
|
-
# `di` -- denied
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
24
|
+
# `di` -- denied on the *incoming* side: the player was gone when this claim
|
|
25
|
+
# processed. Usually a rival won him, which is a genuine competitive
|
|
26
|
+
# loss. But the engine processes claims globally in `index` order and
|
|
27
|
+
# a manager's own earlier accepted claim removes him exactly as a
|
|
28
|
+
# rival's does, so a manager who wins a player and also listed him
|
|
29
|
+
# lower down gets a `di` for the player he just won.
|
|
30
|
+
# `do` -- denied on the *outgoing* side: the player nominated as the drop had
|
|
31
|
+
# already been used as the drop in an earlier accepted claim by the
|
|
32
|
+
# same manager. It cannot occur without that earlier accepted claim,
|
|
33
|
+
# so a `do` row never stands alone as a manager's only activity.
|
|
34
|
+
#
|
|
35
|
+
# Both denials therefore cover a self-cascade as well as a real defeat, which
|
|
36
|
+
# is why the code alone cannot say whether a manager lost anything: managers
|
|
37
|
+
# submit several claims sharing one drop slot, or naming one target against
|
|
38
|
+
# several drops, expecting only one to land. `resolve_lost_claims()` answers
|
|
39
|
+
# that from the manager's whole gameweek; a single row cannot.
|
|
34
40
|
DRAFT_TXN_ACCEPTED = "a"
|
|
35
|
-
|
|
41
|
+
DRAFT_TXN_DENIED_PLAYER_GONE = "di"
|
|
36
42
|
DRAFT_TXN_DROP_ALREADY_USED = "do"
|
|
37
43
|
|
|
38
44
|
|
|
@@ -45,14 +51,89 @@ def is_accepted_transaction(txn: Mapping[str, Any]) -> bool:
|
|
|
45
51
|
return txn.get("result") == DRAFT_TXN_ACCEPTED
|
|
46
52
|
|
|
47
53
|
|
|
48
|
-
def
|
|
49
|
-
"""Whether this row
|
|
54
|
+
def is_denied_on_the_incoming_player(txn: Mapping[str, Any]) -> bool:
|
|
55
|
+
"""Whether this row was denied because the incoming player was gone.
|
|
50
56
|
|
|
51
57
|
True for `di` alone. A `do` row is deliberately excluded: counting it as
|
|
52
58
|
a failed attempt would misreport the manager whose *winning* claim caused
|
|
53
59
|
it, which is the misattribution this classification exists to prevent.
|
|
60
|
+
|
|
61
|
+
Row-local, and so not the same question as "did this manager lose the
|
|
62
|
+
player" -- his own winning claim makes him gone too. Callers wanting that
|
|
63
|
+
want `resolve_lost_claims()`.
|
|
64
|
+
"""
|
|
65
|
+
return txn.get("result") == DRAFT_TXN_DENIED_PLAYER_GONE
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def draft_claim_priority_rank(priority: int | None) -> tuple[int, int]:
|
|
69
|
+
"""Sort key placing a numbered priority before none at all, 1 first."""
|
|
70
|
+
return (1, 0) if priority is None else (0, priority)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _processing_position(txn: Mapping[str, Any], arrival: int) -> tuple[int, int, int]:
|
|
74
|
+
"""Where one row sits in the order the league processed the gameweek.
|
|
75
|
+
|
|
76
|
+
Waivers run first, as a single batch the engine works through in `index`
|
|
77
|
+
order; free agency opens once that batch is done and its rows carry no
|
|
78
|
+
index at all. `arrival` is the row's place in the feed, which orders the
|
|
79
|
+
rows an index cannot separate.
|
|
80
|
+
"""
|
|
81
|
+
index = txn.get("index")
|
|
82
|
+
return (0, index, arrival) if isinstance(index, int) else (1, 0, arrival)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def resolve_lost_claims(txns: Sequence[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
86
|
+
"""The claims one manager genuinely lost, from all their rows for one
|
|
87
|
+
gameweek -- the question no single row can answer (issue #342).
|
|
88
|
+
|
|
89
|
+
Two things a row-local read gets wrong, both consequences of managers
|
|
90
|
+
submitting conditional chains rather than independent claims:
|
|
91
|
+
|
|
92
|
+
- A `di` for a player this manager had *already taken* is his own cascade,
|
|
93
|
+
not a defeat. He listed the same target twice with different drops, one
|
|
94
|
+
landed, and every later claim for him was then denied on the incoming
|
|
95
|
+
side. Reporting it inverts the outcome for the manager who won the race.
|
|
96
|
+
- A player he claimed several times and lost was wanted once, so he is
|
|
97
|
+
returned once, at the best priority he spent on him. Otherwise one
|
|
98
|
+
target across three drops reads as three separate defeats.
|
|
99
|
+
|
|
100
|
+
"Already taken" is a question about order, not merely about owning him by
|
|
101
|
+
the end of the gameweek: only a claim that landed *before* the denial can
|
|
102
|
+
have caused it. Free agency opens after the whole waiver batch, so a
|
|
103
|
+
manager who loses a waiver to a rival and signs the same player as a free
|
|
104
|
+
agent once the rival drops him still lost that waiver, and the loss is
|
|
105
|
+
still recorded -- clearing it on the later pickup would erase a real
|
|
106
|
+
defeat and contradict the case `contested_draft_claims` documents.
|
|
107
|
+
|
|
108
|
+
Both rules are the ones that function already states and works to; this
|
|
109
|
+
brings the stored claims into line with them, so the two layers cannot
|
|
110
|
+
disagree about what a manager lost. Order follows the feed, by the row
|
|
111
|
+
kept for each player.
|
|
54
112
|
"""
|
|
55
|
-
|
|
113
|
+
# The earliest point at which a claim of this manager's took each player.
|
|
114
|
+
taken_at: dict[Any, tuple[int, int, int]] = {}
|
|
115
|
+
for arrival, txn in enumerate(txns):
|
|
116
|
+
if not is_accepted_transaction(txn):
|
|
117
|
+
continue
|
|
118
|
+
player = txn.get("element_in")
|
|
119
|
+
at = _processing_position(txn, arrival)
|
|
120
|
+
if player not in taken_at or at < taken_at[player]:
|
|
121
|
+
taken_at[player] = at
|
|
122
|
+
|
|
123
|
+
best: dict[Any, dict[str, Any]] = {}
|
|
124
|
+
for arrival, txn in enumerate(txns):
|
|
125
|
+
player = txn.get("element_in")
|
|
126
|
+
if player is None or not is_denied_on_the_incoming_player(txn):
|
|
127
|
+
continue
|
|
128
|
+
taken = taken_at.get(player)
|
|
129
|
+
if taken is not None and taken < _processing_position(txn, arrival):
|
|
130
|
+
continue
|
|
131
|
+
known = best.get(player)
|
|
132
|
+
if known is None or draft_claim_priority_rank(txn.get("priority")) < draft_claim_priority_rank(
|
|
133
|
+
known.get("priority"),
|
|
134
|
+
):
|
|
135
|
+
best[player] = txn
|
|
136
|
+
return list(best.values())
|
|
56
137
|
|
|
57
138
|
|
|
58
139
|
class FPLDraftClient:
|