fplkit 2.9.0__tar.gz → 2.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/TOOLS.md +4 -4
- {fplkit-2.9.0 → fplkit-2.10.0}/AGENTS.md +2 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/CHANGELOG.md +36 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/CLAUDE.md +2 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/CONTRIBUTING.md +17 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/PKG-INFO +7 -1
- {fplkit-2.9.0 → fplkit-2.10.0}/README.md +6 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/docs/architecture.md +11 -8
- {fplkit-2.9.0 → fplkit-2.10.0}/docs/command-reference.md +108 -18
- {fplkit-2.9.0 → fplkit-2.10.0}/docs/custom-analysis.md +5 -1
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/_version.py +2 -2
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/agents/action/waiver.py +83 -28
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/agents/common.py +120 -20
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/agents/orchestration/report.py +8 -14
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/api/fpl_draft.py +14 -4
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/_context.py +5 -5
- fplkit-2.10.0/fpl_cli/cli/_draft_provenance.py +303 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/_league_recap_data.py +5 -2
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/_league_recap_history.py +1 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/_league_recap_types.py +75 -18
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/league_recap.py +60 -11
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/player.py +52 -27
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/preview.py +1 -3
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/review.py +3 -3
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/stats.py +46 -29
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/models/player.py +24 -8
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/models/types.py +2 -1
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/prompts/league_recap.py +72 -14
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/prompts/league_recap_checks.py +950 -68
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/draft_provenance.py +164 -20
- fplkit-2.10.0/fpl_cli/services/draft_rebuild_cache.py +113 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/league_history_notes.py +228 -6
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/scoring/__init__.py +0 -2
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/scoring/constants.py +0 -3
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/templates/gw_league_recap.md.j2 +0 -25
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/utils/text.py +17 -1
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/fixtures/waiver_ranked_golden.json +192 -159
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_agents_action.py +220 -17
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_api_draft.py +43 -0
- fplkit-2.10.0/tests/test_cli_draft_provenance.py +330 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_league_recap.py +338 -7
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_player.py +252 -3
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_stats.py +112 -1
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_common.py +171 -8
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_draft_provenance.py +292 -1
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_league_history_notes.py +266 -5
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_league_recap.py +943 -53
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_models.py +10 -1
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_utils_text.py +9 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/README.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/build-test-plan/SKILL.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/build-test-plan/references/plan-template.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/build-test-plan/references/workspace-env.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/create-pr/SKILL.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/gw-prep/SKILL.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/gw-prep/references/entity-normalisation.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/gw-prep/references/output-template.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/gw-prep/references/rules.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/gw-prep/scripts/_bootstrap.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/gw-prep/scripts/bench_order.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/gw-prep/scripts/claimable_returnees.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/gw-prep/scripts/extract_classic_squad.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/gw-prep/scripts/normalise_entities.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/gw-prep/scripts/starting_xi.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/gw-prep/scripts/transfer_eval.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/gw-prep/scripts/validate_draft_waivers.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/preview-ingest/SKILL.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/release/SKILL.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/release-notes/SKILL.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/squad-builder/SKILL.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/squad-builder/references/output-template.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/squad-builder/references/rules.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.agents/skills/update-gw-prep/SKILL.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.claude/output-styles/fpl-mate.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.claude/settings.json +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.env.example +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.github/workflows/ci.yml +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.github/workflows/pr-title.yml +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.github/workflows/release.yml +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/.gitignore +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/LICENSE +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/cliff.toml +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/config/team_ratings_overrides.yaml +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/docs/fpl-rules.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/docs/images/fpl-player-demo-v1-0.png +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/__init__.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/agents/__init__.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/agents/action/__init__.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/agents/analysis/__init__.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/agents/analysis/bench_order.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/agents/analysis/captain.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/agents/analysis/squad_analyzer.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/agents/analysis/starting_xi.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/agents/analysis/stats.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/agents/analysis/transfer_eval.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/agents/base.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/agents/data/__init__.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/agents/data/fixture.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/agents/data/price.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/agents/data/scout.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/agents/orchestration/__init__.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/api/__init__.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/api/contract.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/api/core_insights.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/api/dataset_fetcher.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/api/football_data.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/api/fpl.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/api/historical.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/api/historical_types.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/api/providers/__init__.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/api/providers/_http.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/api/providers/_models.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/api/providers/anthropic.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/api/providers/openai_compat.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/api/providers/perplexity.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/api/understat.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/api/vaastav.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/__init__.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/_banner.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/_fines.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/_fines_config.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/_gameweek_status.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/_helpers.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/_json.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/_plan_grid.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/_review_analysis.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/_review_classic.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/_review_draft.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/_review_summarisation.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/_synthesis_retry.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/allocate.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/captain.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/chips.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/credentials.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/differentials.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/doctor.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/doctor_providers.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/fdr.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/fixtures.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/history.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/init.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/intel.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/league.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/league_fines.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/price_changes.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/price_history.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/ratings.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/returnees.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/sell_prices.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/squad.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/status.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/targets.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/transfer_eval.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/waivers.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/cli/xg.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/config/defaults.yaml +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/config/fixture_predictions.yaml +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/config/previews/EXAMPLE.yaml +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/config/team_managers.yaml +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/constants.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/models/__init__.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/models/chip_plan.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/models/fixture.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/models/gameweek_status.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/models/league_history.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/models/team.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/parsers/__init__.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/parsers/recommendations.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/paths.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/prompts/__init__.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/prompts/returnees.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/prompts/review.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/prompts/scout.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/scraper/__init__.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/scraper/fpl_prices.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/season.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/__init__.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/fixture_predictions.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/league_history.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/league_history_counters.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/league_history_drafts.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/league_history_fines.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/matchup.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/player_clubs.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/player_prior.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/returnee_radar.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/scoring/coverage.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/scoring/data_prep.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/scoring/display.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/scoring/evaluation.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/scoring/ownership.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/scoring/shrinkage.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/scoring/signals.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/scoring/single_gw.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/scoring/value_quality.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/season_previews.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/sell_prices.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/squad_allocator.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/team_form.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/team_ratings.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/services/team_ratings_prior.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/templates/gw_preview.md.j2 +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/templates/gw_review.md.j2 +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/utils/__init__.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/utils/files.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/utils/gameweek.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/utils/markdown.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/utils/teams.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/fpl_cli/utils/time.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/pyproject.toml +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/pyrightconfig.json +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/scripts/backtest_early_season_prior_blend.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/scripts/calibrate_promoted_prior.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/scripts/calibrate_quality_ceilings.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/__init__.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/conftest.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/fixtures/classic_squad_fixture.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/fixtures/recommendations_fixture.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/fixtures/validate_draft_waivers/recs_clean.md +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/fixtures/validate_draft_waivers/squad_grid.json +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/fixtures/validate_draft_waivers/waivers.json +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_agents_analysis.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_agents_analysis_squad.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_agents_base.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_agents_data.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_agents_data_price.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_agents_orchestration.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_allocate.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_api_client.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_api_core_insights.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_api_dataset_fetcher.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_api_football_data.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_api_historical.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_api_understat.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_api_vaastav.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_bench_order_script.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_calibrate_quality_ceilings.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_claimable_returnees.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_classic_squad_extraction.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_allocate.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_banner.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_captain.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_chips.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_context_settings.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_credentials.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_custom_analysis.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_differentials.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_doctor.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_doctor_providers.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_failure_parity.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_failure_streams.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_fdr.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_fdr_blanks.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_fixtures.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_format.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_format_suppression.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_fpl_block.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_history.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_init.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_intel.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_json.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_json_contract.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_league.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_league_fines.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_notice_streams.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_plan_grid.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_preview.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_price_changes.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_price_history.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_returnees.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_review.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_sell_prices.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_settings_warning.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_squad.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_status.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_synthesis_retry.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_targets.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_transfer_eval.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_waivers.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_warnings_stderr.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_cli_xg.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_common_scoring.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_data_prep_harmonisation.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_dependencies.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_doc_consistency.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_fines.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_fines_config.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_fixture_predictions.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_formation_guidance_sync.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_fpl_prices.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_gameweek_status.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_gw_prep_script_bootstrap.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_init.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_init_fines.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_league_history_counters.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_league_history_drafts.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_league_history_fines.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_league_history_store.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_matchup_service.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_normalise_entities.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_output_style_sync.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_paths.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_player_clubs.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_player_prior.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_player_resolution.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_preseason.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_prompt_tables.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_providers.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_recommendations_parser.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_report_agent.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_report_paths.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_returnee_radar.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_review.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_rolling_pts_per_m.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_scoring.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_scoring_coverage.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_scraper.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_season.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_season_awareness.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_season_previews.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_sell_prices.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_sell_prices_service.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_skill_effort_levels.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_skill_fdr_quality_signals.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_skill_stats_thresholds.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_squad_allocator.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_starting_xi_agent.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_starting_xi_script.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_stats.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_team_ratings.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_team_ratings_prior.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_transfer_eval_agent.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_transfer_eval_script.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_utils_gameweek.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_utils_markdown.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_utils_time.py +0 -0
- {fplkit-2.9.0 → fplkit-2.10.0}/tests/test_validate_draft_waivers.py +0 -0
|
@@ -44,8 +44,8 @@ see [Feed coverage warnings](../docs/command-reference.md#json-output).
|
|
|
44
44
|
| `status` | Show FPL gameweek status and upcoming deadlines. JSON `metadata` carries `gameweek`, `format` and `season` (hyphenated label, e.g. `2026-27`) - `season` is the directory segment skills must use when writing alongside fpl-cli's own reports. `data.gameweek_info` carries `finished` beside `bonus_confirmed` and `league_tables_updated`, read from FPL's processing feed (`event-status`) for each game shown (the classic one alone when no format is configured): `true` only when every game's feed says so, `false` as soon as one says otherwise, and `null` otherwise - always `null` while the gameweek is in progress (the feed can report the days played so far as settled), and also whenever some game's feed could not answer and none said no, so with both formats `null` can mean one confirmed and the other was unreadable. `finished` comes hours before the league-table refresh (issue #424), so a skill gating a league recap should wait on `league_tables_updated` being `true`, and table mode prints both beside "Finished" | Yes | General | No | direct-api |
|
|
45
45
|
| `doctor` | Check the setup for dead IDs, stale data, and config problems: resolves each configured ID against the live API (reporting the team/league name back so a wrong-but-valid ID is visible), verifies per-team data files (team ratings, managers, season preview intel, finances, player prior) describe the current season's clubs, reports whether the calibrated quality ceilings were measured against the newest completed season (the one season-derived artefact frozen in code, so a rollover with no recalibration leaves the scoring scale anchored to a cohort that has turned over), and reports which config/data/cache directories are in use. With `--providers`, probes the external data sources instead: live shape-and-volume checks against the FPL and Draft APIs (including that the draft game's club list agrees with the main game's by club code, since every draft path translates club ids through it), the vaastav and Core-Insights datasets, Understat, and football-data.org, including that every club resolves across sources and that players actually join by name (both Understat checks call the enrichment's own gate and matcher on the payload the scoring commands scan, so probe and runtime cannot disagree; a club resolving does not mean its players' names line up) and that the Core-Insights per-gameweek files parse to records through the parsers the scoring commands use (the drift that otherwise surfaces as plausible but wrong output). Distinguishes broken (needs a fix today) from stale (self-corrects, or waits on an upstream fix the tool works around). Exits non-zero when something is broken | Yes | General | No | direct-api |
|
|
46
46
|
| `fixtures` | Show fixtures for a gameweek. With custom analysis on, FDR is the venue-aware ATK/DEF mean `fpl fdr` and `fpl preview` show, so one match reads the same on all three; `-m/--mode` picks `difference` (default) or `opponent`, an unrated club scores a neutral 4.0 rather than the FPL API's 1-5 difficulty, and when the ratings cannot support difficulty at all, table mode warns on stderr and JSON adds a `team_ratings_unusable` entry to `metadata.warnings` (always present, empty or not). With it off, the raw FPL API 1-5 difficulty, and an explicitly passed `-m` reports on stderr that it does not apply. JSON `metadata` carries `custom_analysis`, `fdr_scale` (`team_ratings_1_7` / `fpl_api_1_5`) and `fdr_mode` (null when it does not apply) | Yes | General | No | direct-api |
|
|
47
|
-
| `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 |
|
|
48
|
-
| `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 |
|
|
47
|
+
| `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)`. Under a draft league (format not classic) the `Draft:` line says how the player came to stand where he does, from a replay of the league's draft, waivers and trades (issue #460): `Owned by Alice (round-3 pick)`, `Owned by Bob (GW4 waiver pickup; Cam's round-5 pick)`, `Available (released by Dan in GW5; Dan's round-9 pick)`, `Available (undrafted)`, round unknown where the draft's picks are lost, with a `Draft history:` line beneath when the chain has a step the line does not name; JSON keeps `info.draft_ownership` and adds `info.draft` (owner, acquisition, gameweek, drafter, round/pick, counterparty, full `history`), null with a `draft_provenance_unavailable` warning when the history cannot be shown complete | Yes | General | No | direct-api |
|
|
48
|
+
| `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. Under a draft league (format not classic) the table adds a Draft column: the owner, tagged with how they came by him (`R3` round-3 pick, `GW4 W` waiver, `GW9 FA` free agent, `GW7 T` trade; issue #460), or `Available`; table only, JSON carries no draft column | Yes | General | No | direct-api |
|
|
49
49
|
| `history` | Show historical player performance across seasons. JSON `seasons[].team_code` is the permanent club code, not a season's 1-20 team id; `seasons[].team` carries the same number and is deprecated. 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 |
|
|
50
50
|
| `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. The Best/Worst GW Performers lists (top three; bottom five classic, bottom three draft) are the ones `fpl review` prints: managers level on the gameweek share a place (`3=`) and a tie at the cut keeps every manager in it, a hit reads `29 net pts (33 gross, -4 hit)`, and a classic user above the bottom five gets a `Your GW rank: R of N` line beside the list | No | General | No | direct-api |
|
|
51
51
|
| `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 |
|
|
@@ -55,14 +55,14 @@ see [Feed coverage warnings](../docs/command-reference.md#json-output).
|
|
|
55
55
|
| `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 |
|
|
56
56
|
| `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 |
|
|
57
57
|
| `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 and grades the captain, each transfer and each waiver or free-agent move against it; transfer rows that sell the same player are read as ranked alternatives for one sale and graded once, as followed if any candidate was bought and as one miss otherwise; waiver rows that share a drop are read as one conditional chain, each claim keeping its own row: the move made is credited to the claim it filled wherever that claim sits in the chain, a claim below the one that landed is marked not needed rather than not executed, a claim skipped above one that was made is a miss, and a chain abandoned part-way is one miss where it was left, the claims below that marked as fallbacks not reached; the captain line sets the recommended captain against the armband in captain points (raw score times that week's multiplier, Triple Captain included), the armband being the vice's when the captain played no minutes, and adds the change to the team total when the recommended captain was in the side that scored; one who was not in the squad or played no minutes is labelled so rather than priced at 0; on a wildcard or free hit week, where the recommendation is a squad, each player brought in is graded against the recommended 15 instead, and a recommended name that fits more than one player is left ungraded with a warning on stderr rather than matched to the first; a part of the file it could not read (the captain, the transfers or the waivers) is left ungraded the same way, the warning naming the file, rather than grading every move unadvised, and a file nothing could be read from is not compared at all. 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 cut off at its token ceiling is retried once, told what the last draft lacked (a refused answer is flagged but never retried, and a retry that errors keeps the first attempt), and a still-incomplete one is warned about on stderr (detail in `data/debug/synthesis_corrections.txt` under `--debug`, where `synthesis_meta.json` also records how many answers were judged, which was kept, and the tokens they spent between them) 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 |
|
|
58
|
-
| `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, for draft alongside each held draft's picks (`drafts/draft-{id}.json` in the draft partition, saved once while the API still serves them, since a scheduled redraft drops them for good; `league_history_draft_picks_conflict`, `_lost` (once per draft), `_unsaved` and `_unchecked` name a disagreement with the API, a draft held before anything saved it, a run that could not save, and a `league.drafts` list it could not read), backfilling classic gaps at a coarse tier automatically; `--backfill-detail` rebuilds earlier gameweeks in full (one request per manager per gameweek). A rebuild keeps the name and position already recorded for each player instead of restamping them from today's bootstrap, and never drops a code the gameweek already resolved; it reads the earliest recorded row rather than the newest, so a gameweek an older version already restamped is repaired rather than re-copied. The club is not carried but *derived*, from the gameweek's own fixtures: FPL's live data records which fixtures each player's club played, so a double gameweek settles his club outright and a single narrows it to two, with one `element-summary` request spent only where today's club is absent from that pair (proof he has moved); today's club being one of the two is left unanswered rather than assumed, so a recorded club still wins there. That reaches the two cases a carry structurally cannot — a first capture and a coarse-to-detailed upgrade, neither of which has a recorded squad — and it supersedes a stale recorded club rather than preserving it, with `league_history_club_rederived` naming how many were replaced. A club that blanked has no fixture to intersect and falls back to the recorded club, then today's; name and position are not derivable from fixtures at all and stay carry-only. Everything the gameweek itself answers (points, cards, pick flags, blank status) is still re-derived, and a `league_history_identity_carried` warning names how many players were kept. A rebuild likewise never writes away a league position or cumulative total it cannot re-derive: draft has no per-manager history endpoint and its standings describe a later gameweek, so a replayed draft gameweek keeps the figures already recorded (`league_history_standings_carried`) instead of nulling them, which used to cost the ledger every position that gameweek held and collapse the streaks built on them. Where they were already lost, every run restores them from the ledger itself — the last superseded line that recorded them (a gameweek captured repeatedly while it was still settling holds a stale position in its first capture and the settled one in its last), or a cumulative total re-summed from earlier gameweeks — with no flag and no request (`league_history_standings_repaired`); a draft gameweek's table is derivable from `gross_points` alone at the league's start gameweek. A draft row also records the waiver claims its manager lost to a rival (`lost_claims`, kept apart from `transactions` because nothing moved — no points, no net, and never the denial that is the cascade of the manager's own accepted claim), so an outbid manager is not written into the permanent record as one who never tried; a gameweek captured before that field existed reads it back as unrecorded rather than as "lost none", is named by the coverage report, and is re-recorded by `--backfill-detail` while the league's transaction feed still serves it; a replay that finds fewer claims than the gameweek recorded keeps the recorded list (`league_history_claims_carried`). Those claims also make the waiver wire's races visible (issue #330): the editorial's waiver roster closes with every player more than one manager claimed, who won him and who was beaten to him at what priority, and a week where at least three managers claimed the same player (`MOST_CONTESTED_MIN_CLAIMANTS`) and one of them can be seen to have won him gets a Most Contested award beside Waiver Genius and Waiver Disaster — derived from the manager rows by `contested_draft_claims()`, never recorded, never a report section of its own, and absent from `--format json`, whose rows already carry the `lost_claims` and `transactions` they are regrouped from. The editorial is told to write each race as its own clause and never fold two into one ("respectively"), and is checked against the races afterwards (issue #357): a manager it names as beaten to, or winning, a contested player in a role that player's line does not give them triggers one regeneration told which claims were wrong (a retry that errors keeps the first attempt), and a kept editorial that still fails is saved as written with the wrong claims named on stderr, in a `synthesis_contested_misattribution` JSON warning, and in a callout above the editorial in the report. The same retry and reporting cover two checks on either format (issue #359): a manager's net overall, pre-hit swing or another move's swing pinned on one named move (`synthesis_net_misattribution`), and a tie word ("joint-lowest", "tied for last", "joint-seventh") on a gameweek score or league position no two managers hold (`synthesis_unsupported_tie`); and a third on draft (issue #390): the net overall, or the other kind's figure, said of one kind of a manager's moves ("his waiver moves netted +3 overall" of a -1 waiver and a +4 free-agent pickup, `synthesis_kind_net_misattribution`), for which a roster line that mixes kinds now gives each kind's net beside the net overall. A fourth, on draft (issue #459), holds the editorial to not reciting a race as a list: a paragraph naming the race's player and three or more of its beaten managers (`synthesis_contested_roll_call`) earns the same retry and reporting, but as a broken brief rather than a contradiction of the data, since the sentence is true, so the retry, stderr and the report callout say that instead. The prompt's GW Standings section names the week's highest and lowest scores, every shared score and position (Pos, and Prev where any is shared) and any season totals the league's tie-break placed apart, so "joint" has a line to be held to, and the roster lines call the manager's net "overall". Biggest Bench Haul (both formats) ranks on the best legal XI's gain over the XI that counted — one keeper, the eight legal formations, the doubled player held in place — never on the bench's raw sum, which counted a keeper who could not have played and outfielders no formation had room for (issue #350); its detail names the swap, a manager with bench points but no better XI is no candidate, and the raw sum stays on the row as `bench_points` (the API's `points_on_bench`) because the coarse tier can record nothing else. Coverage gaps and capture warnings go to stderr; the command exits 0 even when the store is unreadable. `--format json` emits one row per manager (the captured ledger row shape) plus `metadata.coverage`, `season_phase`, `notes_pack`, `synthesis_summary`, `synthesis_attempts` (answers judged: 1, or 2 after a retry), `synthesis_shipped_attempt` (which of them shipped) and `synthesis_usage` (input and output tokens summed over every answer judged; all three `null` when no editorial came back), `warnings` (always present, each entry a stable `code` plus rewritable `message` - codes listed in [Command Reference](../docs/command-reference.md#league-history)) `first_capture_store_path` (always present; the partition directory on a first capture, `null` after) and `draft_picks_saved` (always present; the draft-picks snapshots this run wrote, empty on classic). 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 cut off at its token ceiling is regenerated once, told it was cut off (a refused editorial is never regenerated, even with a wrong claim in it), and one the provider stopped early on that is still the one kept is written out as it stands with the reason on stderr, a `synthesis_stopped_early` warning in JSON, and a line in the report's warning callout above the editorial (issue #372). 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. Every recap, classic and draft, also carries a Standout Players section (issue #455), read off the squads and live points the bench haul already uses: the week's top scorers in any manager's squad and its costliest blanks (2 points or fewer; in classic the one that counted for the most managers first, in draft the best by FPL's pre-season draft rank and, on a live recap from GW19, by season points per game, since the rank is never updated; a draft player neither can rank is left off, and no line states the rank or average, which only the heading and JSON carry), each with the managers he counted for, how long a blank played when it was under the hour, and any who left him on the bench, as one sentence per player that the console, a `# Standout Players` report section and a `## Standout Players` prompt section all print; `--format json` carries it as `metadata.standout_players`, every player row with the main game's `id` (under `--draft` too) and `code` beside it. A draft recap's lines also say how each player came to the manager they name (issue #458): the draft pick, the waiver or free-agent move, or the trade, the latest as of the recapped gameweek (a `-g` replay shows its own week), from a replay of the league's draft, accepted transactions and processed trades that must end where FPL's own ownership list does and match the week's squads, with the draft's picks read from the choices feed or, once it serves another draft, the saved snapshot, or, where neither has them, who drafted whom rebuilt from each manager's squad at the draft's gameweek with the earlier moves undone (the line then says "who drafted him" with no round, `round_unknown` in the JSON, and a `draft_rounds_unknown` notice replaces the warning). If the history cannot be shown complete (or a read fails, a failed trades read included once a trade has happened) the lines read without it and a `draft_provenance_unavailable` warning says why; draft rows carry `acquisition` in the JSON, classic rows do not. Before a live capture collects, it reads the recapped game's processing feed (`event-status/` on `FPLClient`, `pl/event-status` on `FPLDraftClient`, both `get_gameweek_status()`): when the feed's match days for the recapped gameweek show bonus unconfirmed or the league tables still "Updating" (`GameweekStatus.still_processing()`), it warns on stderr, in a `league_tables_updating` JSON warning and in a callout at the top of a saved report that positions and totals may be stale, and records the gameweek anyway, since a later run supersedes it and a missed draft gameweek cannot be recaptured; a reconciliation failure in that window keeps its own diagnosis and adds that it may be timing. Only a definite signal warns: an unreadable, slow (5s), reshaped or unrecognised feed is a debug log, and a replay never reads it (issue #424) | Yes | General | No | via-agent |
|
|
58
|
+
| `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, for draft alongside each held draft's picks (`drafts/draft-{id}.json` in the draft partition, saved once while the API still serves them, since a scheduled redraft drops them for good; `league_history_draft_picks_conflict`, `_lost` (once per draft), `_unsaved` and `_unchecked` name a disagreement with the API, a draft held before anything saved it, a run that could not save, and a `league.drafts` list it could not read), backfilling classic gaps at a coarse tier automatically; `--backfill-detail` rebuilds earlier gameweeks in full (one request per manager per gameweek). A rebuild keeps the name and position already recorded for each player instead of restamping them from today's bootstrap, and never drops a code the gameweek already resolved; it reads the earliest recorded row rather than the newest, so a gameweek an older version already restamped is repaired rather than re-copied. The club is not carried but *derived*, from the gameweek's own fixtures: FPL's live data records which fixtures each player's club played, so a double gameweek settles his club outright and a single narrows it to two, with one `element-summary` request spent only where today's club is absent from that pair (proof he has moved); today's club being one of the two is left unanswered rather than assumed, so a recorded club still wins there. That reaches the two cases a carry structurally cannot — a first capture and a coarse-to-detailed upgrade, neither of which has a recorded squad — and it supersedes a stale recorded club rather than preserving it, with `league_history_club_rederived` naming how many were replaced. A club that blanked has no fixture to intersect and falls back to the recorded club, then today's; name and position are not derivable from fixtures at all and stay carry-only. Everything the gameweek itself answers (points, cards, pick flags, blank status) is still re-derived, and a `league_history_identity_carried` warning names how many players were kept. A rebuild likewise never writes away a league position or cumulative total it cannot re-derive: draft has no per-manager history endpoint and its standings describe a later gameweek, so a replayed draft gameweek keeps the figures already recorded (`league_history_standings_carried`) instead of nulling them, which used to cost the ledger every position that gameweek held and collapse the streaks built on them. Where they were already lost, every run restores them from the ledger itself — the last superseded line that recorded them (a gameweek captured repeatedly while it was still settling holds a stale position in its first capture and the settled one in its last), or a cumulative total re-summed from earlier gameweeks — with no flag and no request (`league_history_standings_repaired`); a draft gameweek's table is derivable from `gross_points` alone at the league's start gameweek. A draft row also records the waiver claims its manager lost to a rival (`lost_claims`, kept apart from `transactions` because nothing moved — no points, no net, and never the denial that is the cascade of the manager's own accepted claim), so an outbid manager is not written into the permanent record as one who never tried; a gameweek captured before that field existed reads it back as unrecorded rather than as "lost none", is named by the coverage report, and is re-recorded by `--backfill-detail` while the league's transaction feed still serves it; a replay that finds fewer claims than the gameweek recorded keeps the recorded list (`league_history_claims_carried`). Those claims also make the waiver wire's races visible (issue #330): the editorial's waiver roster closes with every player more than one manager claimed, who won him and who was beaten to him at what priority, and a week where at least three managers claimed the same player (`MOST_CONTESTED_MIN_CLAIMANTS`) and one of them can be seen to have won him gets a Most Contested award beside Waiver Genius and Waiver Disaster — derived from the manager rows by `contested_draft_claims()`, never recorded, never a report section of its own, and absent from `--format json`, whose rows already carry the `lost_claims` and `transactions` they are regrouped from. The editorial is told to write each race as its own clause and never fold two into one ("respectively"), and is checked against the races afterwards (issue #357): a manager it names as beaten to, or winning, a contested player in a role that player's line does not give them triggers one regeneration told which claims were wrong (a retry that errors keeps the first attempt), and a kept editorial that still fails is saved as written with the wrong claims named on stderr, in a `synthesis_contested_misattribution` JSON warning, and in a callout above the editorial in the report. The same retry and reporting cover four checks on either format: a manager's net overall, pre-hit swing or another move's swing pinned on one named move (`synthesis_net_misattribution`, issue #359), a tie word ("joint-lowest", "tied for last", "joint-seventh") on a gameweek score or league position no two managers hold, or on the season's highest or lowest score where its season record was scored only once (`synthesis_unsupported_tie`, issues #359 and #494), a placing on the week the manager's gameweek score does not earn, said of the week ("third on the week") or set beside that score ("finished eighth on 31", a league position read as the week's finish) (`synthesis_wrong_gameweek_placing`, issue #486), and an escape from last place given to a manager who finished there ("He escaped the wooden spoon" of the week's lowest scorer, or "clear of the foot of the table" of the manager bottom of it) (`synthesis_escape_misattribution`, issue #488); and a fourth on draft (issue #390): the net overall, or the other kind's figure, said of one kind of a manager's moves ("his waiver moves netted +3 overall" of a -1 waiver and a +4 free-agent pickup, `synthesis_kind_net_misattribution`), for which a roster line that mixes kinds now gives each kind's net beside the net overall. A fifth, on draft (issue #459), holds the editorial to not reciting a race as a list: a paragraph naming the race's player and three or more of its beaten managers (`synthesis_contested_roll_call`) earns the same retry and reporting, but as a broken brief rather than a contradiction of the data, since the sentence is true, so the retry, stderr and the report callout say that instead. The prompt's GW Standings section names the week's highest and lowest scores, every shared score and position (Pos, and Prev where any is shared) and any season totals the league's tie-break placed apart, so "joint" has a line to be held to, and the roster lines call the manager's net "overall". Biggest Bench Haul (both formats) ranks on the best legal XI's gain over the XI that counted — one keeper, the eight legal formations, the doubled player held in place — never on the bench's raw sum, which counted a keeper who could not have played and outfielders no formation had room for (issue #350); its detail names the swap, a manager with bench points but no better XI is no candidate, and the raw sum stays on the row as `bench_points` (the API's `points_on_bench`) because the coarse tier can record nothing else. Coverage gaps and capture warnings go to stderr; the command exits 0 even when the store is unreadable. A `--save` whose report cannot be written exits 1 with `Failed to save report: <reason>` on stderr; under `--format json` that is the `{command, error}` envelope, with the codes of any capture warnings named in the message and any editorial printed on stderr first (issue #474). `--format json` emits one row per manager (the captured ledger row shape) plus `metadata.coverage`, `season_phase`, `notes_pack`, `synthesis_summary`, `synthesis_attempts` (answers judged: 1, or 2 after a retry), `synthesis_shipped_attempt` (which of them shipped) and `synthesis_usage` (input and output tokens summed over every answer judged; all three `null` when no editorial came back), `warnings` (always present, each entry a stable `code` plus rewritable `message` - codes listed in [Command Reference](../docs/command-reference.md#league-history)) `first_capture_store_path` (always present; the partition directory on a first capture, `null` after) and `draft_picks_saved` (always present; the draft-picks snapshots this run wrote, empty on classic). 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. The editorial's League History section also states the league's highest and lowest gameweek score this season with who scored each and when, counted as the standings count the week (gross, or net under `use_net_points`) and qualified by any gameweek not every score of which was recorded, and the system prompt allows a season superlative ("the week of the season") only where that line names it (issue #494); prompt-only, absent on the league's first gameweek, and in `metadata.notes_pack.season_record_entries` with `points` and `holders`. 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 cut off at its token ceiling is regenerated once, told it was cut off (a refused editorial is never regenerated, even with a wrong claim in it), and one the provider stopped early on that is still the one kept is written out as it stands with the reason on stderr, a `synthesis_stopped_early` warning in JSON, and a line in the report's warning callout above the editorial (issue #372). 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. Every recap, classic and draft, also carries a Standout Players section (issue #455), read off the squads and live points the bench haul already uses: the week's top scorers in any manager's squad and its costliest blanks (2 points or fewer; in classic the one that counted for the most managers first, in draft the best by FPL's pre-season draft rank and, on a live recap from GW19, by season points per game, since the rank is never updated; a draft player neither can rank is left off, and no line states the rank or average, which only the heading and JSON carry), each with the managers he counted for, how long a blank played when it was under the hour, and any who left him on the bench, as one sentence per player that the console and, with `--summarise`, a `## Standout Players` prompt section print, never the saved report, where the editorial already tells the same week in prose (issue #482); `--format json` carries it as `metadata.standout_players`, every player row with the main game's `id` (under `--draft` too) and `code` beside it. A draft recap's lines also say how each player came to the manager they name (issue #458): the draft pick, the waiver or free-agent move, or the trade, the latest as of the recapped gameweek (a `-g` replay shows its own week), from a replay of the league's draft, accepted transactions and processed trades that must end where FPL's own ownership list does and match the week's squads, with the draft's picks read from the choices feed or, once it serves another draft, the saved snapshot, or, where neither has them, who drafted whom rebuilt from each manager's squad at the draft's gameweek with the earlier moves undone (the line then says "who drafted him" with no round, `round_unknown` in the JSON, and a `draft_rounds_unknown` notice replaces the warning). If the history cannot be shown complete (or a read fails, a failed trades read included once a trade has happened) the lines read without it and a `draft_provenance_unavailable` warning says why; draft rows carry `acquisition` in the JSON, classic rows do not. Before a live capture collects, it reads the recapped game's processing feed (`event-status/` on `FPLClient`, `pl/event-status` on `FPLDraftClient`, both `get_gameweek_status()`): when the feed's match days for the recapped gameweek show bonus unconfirmed or the league tables still "Updating" (`GameweekStatus.still_processing()`), it warns on stderr, in a `league_tables_updating` JSON warning and in a callout at the top of a saved report that positions and totals may be stale, and records the gameweek anyway, since a later run supersedes it and a missed draft gameweek cannot be recaptured; a reconciliation failure in that window keeps its own diagnosis and adds that it may be timing. Only a definite signal warns: an unreadable, slow (5s), reshaped or unrecognised feed is a debug log, and a replay never reads it (issue #424) | Yes | General | No | via-agent |
|
|
59
59
|
| `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 |
|
|
60
60
|
| `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 |
|
|
61
61
|
| `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 |
|
|
62
62
|
| `targets` | Find transfer targets - high performers across all ownership. Consistency bonus (CV-xGI percentile, 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 matchup and consistency bonuses stay pure observation, and the score agrees with `fpl transfer-eval`'s Outlook column; 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 | Yes | Classic | Yes | via-agent |
|
|
63
63
|
| `transfer-eval` | Compare transfer OUT player against IN candidates. JSON includes `adjusted_npxg_per_90` and `raw_npxg_per_90` per player when fixture adjustment is active. Before GW10 `quality_score` carries the value family's prior blend (matching `fpl player` and `fpl stats --value`) and `target_score` the target family's (matching `fpl targets`); the single-GW `lineup_score` keeps position-mean shrinkage. JSON `metadata.warnings` carries the early-season notice naming both blended columns, or only `target_score` when no Understat match produced a quality score. Players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of whichever device applies, rather than pulled toward the position mean or handed last season's standing. A name two players answer to exactly (`Henderson`) exits 1 with an `error` envelope naming both, rather than resolving to whichever the API lists first; `Name (TEAM)` or a player ID picks one | Yes | General | Yes | via-agent |
|
|
64
64
|
| `allocate` | Select mathematically optimal 15-player squad within budget. At `--horizon >= 2` the solver ranks on the value family's GW1-9 prior blend (last season's pedigree weighted by confidence); players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of it rather than handed last season's standing, and stay in the solver pool; JSON `metadata.warnings` carries the early-season notice at `--horizon >= 2` (`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, whose squad therefore scored without xG | Yes | Classic | Yes | direct-api |
|
|
65
|
-
| `waivers` | Show waiver recommendations for your draft league. JSON `data` carries `top_targets` and `targets_by_position` (ranked claims) plus `pool` — the full unowned roster, one `{id, code, player_name, position, team_short}` row each, unranked and untruncated, so a consumer can answer "is this player claimable at all" without inferring it from the ranked lists. On every player row the command emits `id` is the draft game's element id and `code` the cross-game key: the gw-prep skill joins the returnee radar's main-game rows to the pool on `code`, since a mid-season addition holds a different id in each game (#418). Keeper scores carry the GK signal block (saves/90, defensive quality, clean-sheet rate), read from the main-game player each draft element is joined to on `code` because the draft bootstrap publishes none of it, so draft keepers rank on what separates them rather than on form and ppg alone. Defender scores carry DC/90 through the same join, since the draft bootstrap has defensive contribution only as a season total; every row's `dc_per_90` (pool, `current_squad`, `recent_releases`) is the main-game rate, weighted only in the defender score and display-only for other positions, and a defender the join misses stays at 0.
|
|
65
|
+
| `waivers` | Show waiver recommendations for your draft league. JSON `data` carries `top_targets` and `targets_by_position` (ranked claims) plus `pool` — the full unowned roster, one `{id, code, player_name, position, team_short}` row each, unranked and untruncated, so a consumer can answer "is this player claimable at all" without inferring it from the ranked lists. On every player row the command emits `id` is the draft game's element id and `code` the cross-game key: the gw-prep skill joins the returnee radar's main-game rows to the pool on `code`, since a mid-season addition holds a different id in each game (#418). Keeper scores carry the GK signal block (saves/90, defensive quality, clean-sheet rate), read from the main-game player each draft element is joined to on `code` because the draft bootstrap publishes none of it, so draft keepers rank on what separates them rather than on form and ppg alone. Defender scores carry DC/90 through the same join, since the draft bootstrap has defensive contribution only as a season total; every row's `dc_per_90` (pool, `current_squad`, `recent_releases`) is the main-game rate, weighted only in the defender score and display-only for other positions, and a defender the join misses stays at 0. The join also carries every player's history, prior, adjusted npxG and consistency signals, so a player in the unowned pool that the join misses is reported as one `draft_player_unjoined` entry in `metadata.warnings` (stderr in table mode) naming the terms live that gameweek, with the count per position and the first three names in ranked order, since his score is built from a different mix of terms (keepers and defenders also lose their position-specific term, and count even when no shared term is live) and can read high or low; an empty main-game player list is one sentence instead of counts. Before GW10 the score's quality baseline carries the prior blend (last season's pedigree weighted by confidence) rather than position-mean shrinkage; the matchup, position-need, team-stacking and consistency terms stay pure observation; JSON `metadata.warnings` carries the early-season notice while the blend is live, and reports `early_season_small_sample` both when history could not be loaded and when no draft element found a main-game counterpart to take a prior from. 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. Every row outside `pool` carries `appearances`, derived from total points over ppg as the main game derives it, which the minutes factor reads: from GW6 a player with none loses his per-90 and matchup terms. Each row's `team_id` is the main game's team id, translated from the draft game's own club numbering by club code (null for a club the main game does not list, which `metadata.warnings` reports as `draft_club_untranslated`; the row keeps the club's draft name), since fixtures, form and FDR are all keyed by it. No `draft_league_id` configured exits 1 in both output formats — table mode used to warn and exit 0 | Yes | Draft | Yes | via-agent |
|
|
66
66
|
| `squad` | Analyse your FPL squad health and fixtures. Exits 1 with an `error` envelope when no entry ID is configured for the active format. `--classic` / `--draft` (mutually exclusive) pin the format instead of inferring it from the configured IDs, so a caller that means classic gets the missing-ID error rather than the draft roster; with neither, single-format configs still auto-select. A picks 404 is checked against `entry/<id>/` before it is reported as "no squad submitted yet", so a reissued or wrong entry ID says so; `squad grid` and `captain` share that diagnosis through `agents/common.py` | Yes | General | No | via-agent |
|
|
67
67
|
| `squad grid` | Show squad fixture difficulty grid. Exits 1 with an `error` envelope when no entry ID is configured for the active format, or when the squad fetch fails. Takes the same `--classic` / `--draft` pinning as `squad`, before or after the subcommand, and the same stale-entry diagnosis. A `--watch` name that does not resolve is non-fatal either way: unmatched, or matching two players exactly, it warns on stderr and is skipped, and the grid still renders | Yes | General | No | via-agent |
|
|
68
68
|
| `squad sell-prices` | Show squad sell prices and financial breakdown. A cached run recomputes each sell price at today's price, applying FPL's rule only to a player whose cached sell price it reproduces at the price stored with the scrape; JSON rows carry `scraped_sell_price` beside `sell_price`, and a player it cannot check keeps the scraped price under a `sell_prices_as_scraped` warning, on stderr in both modes (also given, with exit 0, when today's prices cannot be fetched). `transfer-eval` budgets on the same recomputed price and warns when the OUT player's is unchecked or not cached; `allocate --sell-prices` repeats the file's warning. Nothing cached and no `--refresh`, a failed scrape, draft format, or a scrape the extraction heuristics distrust each exit 1 with an `error` envelope under `--format json`; table mode still renders a suspect scrape under a `Squad Budget (Suspect)` panel, since a reader can judge it and `fpl allocate --sell-prices` cannot | Yes | Classic | No | direct-api |
|
|
@@ -21,6 +21,8 @@ API clients in `fpl_cli/api/`: FPLClient (main API, caches `bootstrap-static/`),
|
|
|
21
21
|
|
|
22
22
|
LLM access is not an API client: `fpl_cli/api/providers/` is a registry of `perplexity` / `anthropic` / `openai` (an OpenAI-compatible base covering Groq, Together, Ollama via `base_url`), resolved per role by `get_llm_provider(role, settings)` and all returning `LLMResponse`. Roles are `research` and `synthesis`; resolution is env > `settings.yaml` > `defaults.yaml`, with `FPL_{ROLE}_PROVIDER` / `_MODEL` / `_BASE_URL` overriding and keys in `PERPLEXITY_API_KEY` / `ANTHROPIC_API_KEY` / `OPENAI_API_KEY`.
|
|
23
23
|
|
|
24
|
+
Changing a prompt, or a data section the model is shown, is verified before it ships by an A/B in the fpl-workspace vault's harness (`.claude/scripts/recap_ab/` there): both arms run from pinned commits, a dry run first so the prompts differ only where intended, graded per format with draft as the gate, and the result goes into the PR body as numbers with no names. A change to an editorial check (`fpl_cli/prompts/league_recap_checks.py`) is verified by replaying the saved drafts corpus through both builds' checks, never by new model runs. The harness's `triage.sh <base> <head>` says which of the two a change needs, or both, and names any check whose retry wording changed, which only fresh runs show the model reading; the corpus lives in the vault at `.claude/scripts/recap_ab/corpus/`, each round's drafts beside the recap data they were judged against, so a replay needs neither a model call nor the FPL API. The October 2026 prompt audit and its A/B record live in the private `fpl-cli-docs` repo under `docs/audits/`, with the method distilled in its `docs/solutions/best-practices/`; re-run that audit at the next change of the default synthesis model, beside the release skill's model-pin preflight.
|
|
25
|
+
|
|
24
26
|
Services in `fpl_cli/services/` are the computation layer between agents and API clients: scoring engine, player priors, team ratings and form, matchup scoring, fixture predictions, season previews, the ILP squad allocator, and the league-history ledger with its counters projection and notes pack. Also load-bearing and easy to miss: `fpl_cli/paths.py` (every path resolution), `fpl_cli/season.py` (season labels and partitioning), `fpl_cli/prompts/` (LLM prompt builders), `fpl_cli/parsers/` (recommendation parsing), `fpl_cli/constants.py`.
|
|
25
27
|
|
|
26
28
|
### Models (non-obvious aliases)
|
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
|
+
## [2.9.0] - 2026-10-18
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- review --compare-recs reads captains whose rationale has a dash (#407)
|
|
9
|
+
- stop fpl review printing an empty Draft heading on stdout (#412)
|
|
10
|
+
- say why a report failed to save and stop calling refusals cut off (#413)
|
|
11
|
+
- gw-prep stops when the Draft waiver table cannot be read (#414)
|
|
12
|
+
- review --compare-recs stops marking ranked alternatives as missed (#415)
|
|
13
|
+
- review --compare-recs stops marking waiver fallbacks as missed (#417)
|
|
14
|
+
- waivers JSON carries `code` so gw-prep joins returnees to the pool (#420)
|
|
15
|
+
- cached sell prices follow price changes since the scrape (#427)
|
|
16
|
+
- gw-prep draft Returning Soon flags an unreadable returnee radar (#435)
|
|
17
|
+
- fpl waivers drop advice accounts for injured players again (#440)
|
|
18
|
+
- draft commands read each club in the main game's numbering (#430)
|
|
19
|
+
- fixture-adjusted npxG scales xG down against weak sides, not up (#443)
|
|
20
|
+
- fpl waivers credits draft defenders for defensive contributions (#444)
|
|
21
|
+
- scoring keeps Core-Insights matches with blank Elo or xG (#447)
|
|
22
|
+
- fpl waivers reports unmatched keepers and defenders in JSON (#449)
|
|
23
|
+
- league-recap --draft saves draft picks when the API sends a completion time (#467)
|
|
24
|
+
- league-recap stops flagging a week's net said after a waiver aside (#468)
|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
|
|
28
|
+
- JSON player rows carry `code` beside `id` on every command (#421)
|
|
29
|
+
- league-recap and status say when FPL is still updating leagues (#428)
|
|
30
|
+
- history JSON names each season's club code `team_code` (#441)
|
|
31
|
+
- fpl doctor --providers checks the draft and main club lists agree (#442)
|
|
32
|
+
- --summarise records attempts and token usage, and pins effort (#451)
|
|
33
|
+
- shorter league-recap editorials that tell the week's best stories (#453)
|
|
34
|
+
- scoring commands warn when a feed reached only part of the pool (#454)
|
|
35
|
+
- league-recap names the week's top scorers and costliest blanks (#457)
|
|
36
|
+
- league-recap --draft saves draft picks a redraft would erase (#464)
|
|
37
|
+
- league-recap retries editorials that list a race's beaten managers (#462)
|
|
38
|
+
- league-recap says where each standout draft player came from (#463)
|
|
39
|
+
|
|
4
40
|
## [2.8.0] - 2026-10-14
|
|
5
41
|
|
|
6
42
|
### Bug Fixes
|
|
@@ -21,6 +21,8 @@ API clients in `fpl_cli/api/`: FPLClient (main API, caches `bootstrap-static/`),
|
|
|
21
21
|
|
|
22
22
|
LLM access is not an API client: `fpl_cli/api/providers/` is a registry of `perplexity` / `anthropic` / `openai` (an OpenAI-compatible base covering Groq, Together, Ollama via `base_url`), resolved per role by `get_llm_provider(role, settings)` and all returning `LLMResponse`. Roles are `research` and `synthesis`; resolution is env > `settings.yaml` > `defaults.yaml`, with `FPL_{ROLE}_PROVIDER` / `_MODEL` / `_BASE_URL` overriding and keys in `PERPLEXITY_API_KEY` / `ANTHROPIC_API_KEY` / `OPENAI_API_KEY`.
|
|
23
23
|
|
|
24
|
+
Changing a prompt, or a data section the model is shown, is verified before it ships by an A/B in the fpl-workspace vault's harness (`.claude/scripts/recap_ab/` there): both arms run from pinned commits, a dry run first so the prompts differ only where intended, graded per format with draft as the gate, and the result goes into the PR body as numbers with no names. A change to an editorial check (`fpl_cli/prompts/league_recap_checks.py`) is verified by replaying the saved drafts corpus through both builds' checks, never by new model runs. The harness's `triage.sh <base> <head>` says which of the two a change needs, or both, and names any check whose retry wording changed, which only fresh runs show the model reading; the corpus lives in the vault at `.claude/scripts/recap_ab/corpus/`, each round's drafts beside the recap data they were judged against, so a replay needs neither a model call nor the FPL API. The October 2026 prompt audit and its A/B record live in the private `fpl-cli-docs` repo under `docs/audits/`, with the method distilled in its `docs/solutions/best-practices/`; re-run that audit at the next change of the default synthesis model, beside the release skill's model-pin preflight.
|
|
25
|
+
|
|
24
26
|
Services in `fpl_cli/services/` are the computation layer between agents and API clients: scoring engine, player priors, team ratings and form, matchup scoring, fixture predictions, season previews, the ILP squad allocator, and the league-history ledger with its counters projection and notes pack. Also load-bearing and easy to miss: `fpl_cli/paths.py` (every path resolution), `fpl_cli/season.py` (season labels and partitioning), `fpl_cli/prompts/` (LLM prompt builders), `fpl_cli/parsers/` (recommendation parsing), `fpl_cli/constants.py`.
|
|
25
27
|
|
|
26
28
|
### Models (non-obvious aliases)
|
|
@@ -145,6 +145,23 @@ via trusted publishing, and prepends the git-cliff section to
|
|
|
145
145
|
agent sessions run it as the `release` skill, and can preview the next
|
|
146
146
|
version's notes any time with the read-only `release-notes` skill.
|
|
147
147
|
|
|
148
|
+
## Prompts and editorial checks
|
|
149
|
+
|
|
150
|
+
A change to a prompt the CLI sends to a model, or to a data section the model is shown,
|
|
151
|
+
is verified before it ships by an A/B in the fpl-workspace vault's harness
|
|
152
|
+
(`.claude/scripts/recap_ab/` there): both arms run from pinned commits, a dry run first so
|
|
153
|
+
the prompts differ only where intended, graded per format with draft as the gate, and the
|
|
154
|
+
result is written into the PR body as numbers with no names. A change to an editorial
|
|
155
|
+
check (`fpl_cli/prompts/league_recap_checks.py`) is verified by replaying the saved drafts
|
|
156
|
+
corpus through both builds' checks, never by new model runs. The harness's
|
|
157
|
+
`triage.sh <base> <head>` says which of the two a change needs, or both, and names any
|
|
158
|
+
check whose retry wording changed, which only fresh runs show the model reading; the
|
|
159
|
+
corpus lives in the vault at `.claude/scripts/recap_ab/corpus/`, each round's drafts beside
|
|
160
|
+
the recap data they were judged against, so a replay needs neither a model call nor the
|
|
161
|
+
FPL API. The October 2026 prompt audit and its A/B record live in the private
|
|
162
|
+
`fpl-cli-docs` repo under `docs/audits/`; re-run it at the next change of the default
|
|
163
|
+
synthesis model.
|
|
164
|
+
|
|
148
165
|
## Agent sessions
|
|
149
166
|
|
|
150
167
|
The operative copy of these conventions for agents is in
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fplkit
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.10.0
|
|
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
|
|
@@ -181,6 +181,10 @@ prints twice a season, at GW19 and the finale, alongside the fines table. Unjudg
|
|
|
181
181
|
gameweeks are always stated beside the number — see
|
|
182
182
|
[Season counts](docs/command-reference.md#league-recap) for the per-count rules.
|
|
183
183
|
|
|
184
|
+
With `--summarise` the editorial is also told the league's highest and lowest gameweek
|
|
185
|
+
score of the season, who scored each and when, and may call a week the season's best or
|
|
186
|
+
worst only where that record says so.
|
|
187
|
+
|
|
184
188
|
On the league's first gameweek the recap also looks up each manager's FPL record from
|
|
185
189
|
previous seasons — how many they have played, how they finished last season, their best
|
|
186
190
|
season — and puts it in the saved report and, with `--summarise`, in front of the
|
|
@@ -213,6 +217,8 @@ $ fpl returnees # Injured and suspended players due ba
|
|
|
213
217
|
$ fpl returnees --enrich # Add searched return timing where FPL says nothing
|
|
214
218
|
```
|
|
215
219
|
|
|
220
|
+
With a draft league configured, `fpl player` also says how a player came to stand where he does: "Owned by Bob (GW4 waiver pickup; Cam's round-5 pick)", "Available (released by Dan in GW5; Dan's round-9 pick)", "Available (undrafted)", with his whole history on a line beneath it, worked out by replaying the league's draft, waivers and trades. `fpl stats` tags each owner in its Draft column the short way (`R3`, `GW4 W`). If the history cannot be shown complete the lines read as before and a warning says why. See [Draft ownership and provenance](docs/command-reference.md#draft-ownership-and-provenance).
|
|
221
|
+
|
|
216
222
|
`fpl xg` scales its minutes floor to the gameweeks played, so an early-season run analyses the football that has actually happened rather than filtering everyone out against a bar the calendar has not allowed yet; it names the window and floor it used, and says which of the two caused an empty result. See [Underlying Stats](docs/command-reference.md#underlying-stats-xg).
|
|
217
223
|
|
|
218
224
|
`fpl returnees` reads the availability news attached to each player, works out who is due back inside the next few gameweeks, and keeps the list short by filtering on past performance — the players worth claiming in draft before they are fit again, and worth planning around in classic. FPL states a return date for only a small minority of flagged players, so `--enrich` searches the web for fresher timing on the ones it is quiet or stale about and shows what it finds alongside the FPL news rather than in place of it. Each run remembers what it showed, so the next one can say whose outlook moved.
|
|
@@ -145,6 +145,10 @@ prints twice a season, at GW19 and the finale, alongside the fines table. Unjudg
|
|
|
145
145
|
gameweeks are always stated beside the number — see
|
|
146
146
|
[Season counts](docs/command-reference.md#league-recap) for the per-count rules.
|
|
147
147
|
|
|
148
|
+
With `--summarise` the editorial is also told the league's highest and lowest gameweek
|
|
149
|
+
score of the season, who scored each and when, and may call a week the season's best or
|
|
150
|
+
worst only where that record says so.
|
|
151
|
+
|
|
148
152
|
On the league's first gameweek the recap also looks up each manager's FPL record from
|
|
149
153
|
previous seasons — how many they have played, how they finished last season, their best
|
|
150
154
|
season — and puts it in the saved report and, with `--summarise`, in front of the
|
|
@@ -177,6 +181,8 @@ $ fpl returnees # Injured and suspended players due ba
|
|
|
177
181
|
$ fpl returnees --enrich # Add searched return timing where FPL says nothing
|
|
178
182
|
```
|
|
179
183
|
|
|
184
|
+
With a draft league configured, `fpl player` also says how a player came to stand where he does: "Owned by Bob (GW4 waiver pickup; Cam's round-5 pick)", "Available (released by Dan in GW5; Dan's round-9 pick)", "Available (undrafted)", with his whole history on a line beneath it, worked out by replaying the league's draft, waivers and trades. `fpl stats` tags each owner in its Draft column the short way (`R3`, `GW4 W`). If the history cannot be shown complete the lines read as before and a warning says why. See [Draft ownership and provenance](docs/command-reference.md#draft-ownership-and-provenance).
|
|
185
|
+
|
|
180
186
|
`fpl xg` scales its minutes floor to the gameweeks played, so an early-season run analyses the football that has actually happened rather than filtering everyone out against a bar the calendar has not allowed yet; it names the window and floor it used, and says which of the two caused an empty result. See [Underlying Stats](docs/command-reference.md#underlying-stats-xg).
|
|
181
187
|
|
|
182
188
|
`fpl returnees` reads the availability news attached to each player, works out who is due back inside the next few gameweeks, and keeps the list short by filtering on past performance — the players worth claiming in draft before they are fit again, and worth planning around in classic. FPL states a return date for only a small minority of flagged players, so `--enrich` searches the web for fresher timing on the ones it is quiet or stale about and shows what it finds alongside the FPL news rather than in place of it. Each run remembers what it showed, so the next one can say whose outlook moved.
|
|
@@ -391,8 +391,9 @@ Services live in `fpl_cli/services/` and provide the computation layer between a
|
|
|
391
391
|
| `league_history` | Durable league-history ledger: one NDJSON file per gameweek under `<data dir>/league_history/<season>/<format>-<league id>/`, written as a side effect of `league-recap`. Append-only — an identical row writes nothing, a differing one appends a superseding line, and readers resolve by highest fidelity tier then latest capture, with an unknown row ranking below every tier. Each row carries its own schema version: an older one is upgraded in memory on read (the line on disk is never rewritten, so a store shared with an older install stays readable by it), a newer one is skipped with a warning and preserved byte-for-byte — and every line the store writes is stamped with the writing install's version whatever version the row was read at, since a superseding line copied off a version-5 row but carrying a version-6 field would be validated by a version-5 install rather than skipped, and rejected as a corrupt file. Two deliberate inversions of house convention: loading **fails closed** (unlike `chip_plan`, which resets on a corrupt file) because the API cannot rebuild a past gameweek — scoped to that one gameweek, whose coverage entry carries the file path and the `mv` that retires it, so a caller reports it as `league_history_store_unreadable` rather than as a coverage gap `--backfill-detail` would refuse to fill, and reports it once per run however many of a recap's readers hit the same file — and season is a **partition key** (unlike `team_ratings`, which discards a previous season) because per-gameweek granularity is destroyed at the July rollover. The consequence of partitioning is that the store only ever grows — nothing prunes a prior season, by design — at roughly a few megabytes per league per season |
|
|
392
392
|
| `league_history_counters` | Declarative streak-condition registry (9 conditions: `weeks_on_top`, `bottom_half_run`, `gw_win_streak`, `gw_loss_streak`, `green_arrow_drought` shared; `captain_blank_run`, `hit_run` classic-only; `waiver_win_run`, `waiver_burn_run` draft-only) and the rebuildable counters projection it drives. Each entry declares the minimum run worth surfacing as a streak, `None` for the two conditions (bottom half, green-arrow drought) whose run only drives their season count's firing rule. Each predicate returns extend/reset/hold/inapplicable for one manager's row, and the last two both leave the run untouched, so neither a capture gap nor an impossibility ever lies about a streak in either direction. They differ in what the record says: hold is a gameweek it cannot rule (an unknown row, a field the tier never captured), inapplicable one it rules completely where the condition had nothing to judge (a draft manager with no completed waiver moves, whether they made no claims or lost every one, a captain with no fixture, a cohort level on points, a green-arrow drought gameweek begun in first place). Each run counts them separately, and each condition that can return inapplicable names it in its own words (`inapplicable_label`), so a complete gameweek is never reported as missing (issue #358). Alongside each currently-open run, the state carries season-wide occurrence totals (issue #164): every extending gameweek counts once and survives a reset, holds (not inapplicable gameweeks, which were judged) are tallied separately as the count's "not judged" qualifier, and the last-occurrence and first-evaluated gameweeks bound what a consumer may claim — so "their sixth gameweek win of the season" is derivable, not just the open run. Each entry also declares a `CountSurfacePolicy`, the condition's own rule for when that total is worth rendering weekly (U9 applies it; the milestone set-pieces and `--format json` ignore it). The projection is a disposable cache (never a second source of truth) under `<data dir>/league_history_counters/<season>/<format>-<league id>/counters.json`: it advances one gameweek at a time when its stamp is exactly one behind, and **fails open** to a full rebuild from `league_history`'s rows otherwise — missing, unreadable, wrong-version, or a stamp that isn't stamp+1 (which is also what makes the season totals retroactive: the version bump that added them rebuilds silently from rows already on disk) |
|
|
393
393
|
| `league_history_fines` | Season fine tally (`build_season_fines_tally()`): a pure fold over `league_history`'s rows into per-manager, per-rule counts, with no cache and no second source of truth. Counts only what a row already recorded, so a settings change moves future rulings and leaves history alone — a guarantee `_freeze_recorded_fines` (`cli/_league_recap_history.py`) carries into backfill, where a repair would otherwise re-rule an unchanged cohort under whatever config is current at repair time. Every gameweek that could not be ruled is named as a qualifier rather than folded in as a zero — never captured, unreadable, captured but reaching nobody (every row unknown), holding no record of what was ruled (schema version < 4, or an evaluation that raised), recording a ruling on no rules at all, or captured at a fidelity that could not rule a given rule. Each qualifier states only what the ledger can prove: where two causes are indistinguishable from a row, neither is named. A mid-season joiner keeps their real, lower totals and is qualified; a manager who has since left keeps the fines already ruled against them, bounded to the gameweeks they were recorded for. Rulings are tracked per rule as well as per manager (`unruled_gameweeks_for()`), since a coarse gameweek rules `last-place` and structurally cannot rule `red-card` — the recap prompt asks that question before it will call a fine a manager's first of the season. `last-place` is the one cohort-relative rule, so it is the one a tie falls on: every manager level on the gameweek's lowest score is fined, and `_repair_tied_last_place` sweeps gameweeks recorded before that was true, appending the ruling the gameweek already wrote against the managers it left out rather than re-ruling anything (issue #336) |
|
|
394
|
-
| `league_history_notes` | Notes pack (`build_notes_pack()`), season-phase marker (`derive_season_phase()`, standalone) and the `is_season_milestone()` predicate the recap gates its *printed* season fines table on (its prompt section is ungated) - the phase's *first* gameweek and the finale, deliberately not the whole 13-gameweek midpoint phase, since a once-a-season set-piece fired thirteen times is wallpaper built from `league_history_counters`'s projection plus a bounded trailing window of raw ledger rows (`TRAILING_WINDOW_GAMEWEEKS = 6`, reused as the run-in phase's own length). Every open streak renders as an observed count over its true span rather than "in a row" once its window is wider than its run (e.g. "3 in the last 11, with 8 not recorded"), and the trailing clause separates the gameweeks the record lacks ("not recorded": unknown, uncaptured or absent) from the ones the condition had nothing to judge in, named by the condition ("with no completed moves in 1"); each entry declares which of console/report/prompt it reaches — a reportable streak reaches all three, coverage statements reach report+prompt, the season-phase marker is prompt-only (scene-setting for the editorial, never printed in the report — issue #187), and a below-minimum streak is retained (exposed via `--format json`) with no surfaces. Streak and season-count entries also carry `prompt_text`, the same fact as a sentence for the editorial prompt ("Alice has 3 captain blanks in the last 6 (GW1-GW6). Of the rest, 2 were not recorded."), which `format_recap_league_history_context()` reads through `text_for(NoteSurface.PROMPT)` while console, report and `--format json` keep the ledger line: the model quotes whatever it is handed, and the ledger row is not a sentence a columnist would write (issue #452). Season-count entries (issue #164) render each condition's season occurrence total over the manager's own evaluated span, with holds stated as "not judged either way", surfaced per the registry's own declarative `CountSurfacePolicy` (a step the total must land on, run-length milestones read off the open run rather than the total, a first-occurrence rule gated on the season's half, a whole-condition second-half gate, and a ride-along rule for same-gameweek peers — an absolute floor for counts that stay small, a window relative to the nearest firing total for counts that climb all season). Evaluation is two-pass because firing is cross-manager: who fired their condition this gameweek, then which same-gameweek incrementers ride along beside them. Every nonzero count is retained for `--format json`, and at the two `is_season_milestone()` gameweeks the whole nonzero set carries report+prompt — the report's `## Season Counts` set-piece and the retrospective editorial's season-spanning facts. A "since GW X" qualifier is stated only when a partition's or a specific manager's own recorded coverage genuinely begins later than the league's start gameweek, never merely because a trailing window was read. Only the finale phase rescans every captured gameweek (via `rebuild_counters_through`, never the cached `compute_counters_through`) so weekly cost stays flat as the season progresses |
|
|
395
|
-
| `draft_provenance` | How each owned draft player came to his manager (#458), replayed from the league's own history: the draft in force (the latest completed entry in `league.drafts` at or before the gameweek, its picks from the choices feed or, when the feed has none for that draft id, from the `league_history_drafts` snapshot), then every accepted transaction by `added` and every processed trade (`is_processed_trade()`: `p`, not the vetoed `v`) by `response_time`, in one time order, keeping each player's full chain (`DraftStep`: draft, waiver, free_agent, trade, release). `replay_draft_history()` trusts the replay only when it ends exactly where `element_status` says everyone is now and, stopped at the gameweek, matches the squads the caller observed; any disagreement, a move that does not fit the squad it applies to, or a log that could not be read (`read_draft_logs()` degrades a failed read to a missing log, never an exception; the trades log is one of them, since a trade and its reversal leave no mark on either check) returns no players and a `problem` the recap reports as `draft_provenance_unavailable`. Where neither source has a held draft's picks (a redraft was scheduled before the first snapshot) the start is rebuilt instead: each manager's squad at the draft's gameweek (`read_draft_logs(entry_ids=...)`, one request each, only then), with the moves made before that gameweek undone newest first, so the chains begin at a draft step with no pick (`DraftInForce.rounds_known` False) and run on under the same two checks; because a start derived from the logs cannot reveal a move they lack, a pre-gameweek trade made since the draft in an unseen state, an unreadable squad or an undone move that does not fit all refuse it. No squad is read when another log is missing, since the replay refuses without them. `DraftLogs.choices_payload` is the choices response itself, which the recap hands to the picks snapshot so the endpoint is read once. One shared helper (`get_draft_provenance()`) so the recap's Standout Players section and #460'
|
|
394
|
+
| `league_history_notes` | Notes pack (`build_notes_pack()`), season-phase marker (`derive_season_phase()`, standalone) and the `is_season_milestone()` predicate the recap gates its *printed* season fines table on (its prompt section is ungated) - the phase's *first* gameweek and the finale, deliberately not the whole 13-gameweek midpoint phase, since a once-a-season set-piece fired thirteen times is wallpaper built from `league_history_counters`'s projection plus a bounded trailing window of raw ledger rows (`TRAILING_WINDOW_GAMEWEEKS = 6`, reused as the run-in phase's own length). Every open streak renders as an observed count over its true span rather than "in a row" once its window is wider than its run (e.g. "3 in the last 11, with 8 not recorded"), and the trailing clause separates the gameweeks the record lacks ("not recorded": unknown, uncaptured or absent) from the ones the condition had nothing to judge in, named by the condition ("with no completed moves in 1"); each entry declares which of console/report/prompt it reaches — a reportable streak reaches all three, coverage statements reach report+prompt, the season-phase marker is prompt-only (scene-setting for the editorial, never printed in the report — issue #187), and a below-minimum streak is retained (exposed via `--format json`) with no surfaces. Streak and season-count entries also carry `prompt_text`, the same fact as a sentence for the editorial prompt ("Alice has 3 captain blanks in the last 6 (GW1-GW6). Of the rest, 2 were not recorded."), which `format_recap_league_history_context()` reads through `text_for(NoteSurface.PROMPT)` while console, report and `--format json` keep the ledger line: the model quotes whatever it is handed, and the ledger row is not a sentence a columnist would write (issue #452). Season-count entries (issue #164) render each condition's season occurrence total over the manager's own evaluated span, with holds stated as "not judged either way", surfaced per the registry's own declarative `CountSurfacePolicy` (a step the total must land on, run-length milestones read off the open run rather than the total, a first-occurrence rule gated on the season's half, a whole-condition second-half gate, and a ride-along rule for same-gameweek peers — an absolute floor for counts that stay small, a window relative to the nearest firing total for counts that climb all season). Evaluation is two-pass because firing is cross-manager: who fired their condition this gameweek, then which same-gameweek incrementers ride along beside them. Every nonzero count is retained for `--format json`, and at the two `is_season_milestone()` gameweeks the whole nonzero set carries report+prompt — the report's `## Season Counts` set-piece and the retrospective editorial's season-spanning facts. A "since GW X" qualifier is stated only when a partition's or a specific manager's own recorded coverage genuinely begins later than the league's start gameweek, never merely because a trailing window was read. Only the finale phase rescans every captured gameweek (via `rebuild_counters_through`, never the cached `compute_counters_through`) so weekly cost stays flat as the season progresses. The one weekly exception is the season-record entries (issue #494): the league's highest and lowest gameweek score this season, each naming every score that reached it with its manager and gameweek ("this gameweek" for the recapped one, plus "No score this gameweek equalled it" where it fell short), swept over every gameweek from the league's start the way `league_history_fines` sweeps them — the store memoizes each read, so the two share it. They are scored as the GW Standings section scores the week (gross, or net of the hit under `use_net_points`, passed in since the row does not record it); only a score the row proves counts (not an unknown row's, nor under `use_net_points` a classic row's that never recorded its hit), and a gameweek with such a row, no row for a manager in the recapped gameweek's league, an unreadable file or no capture is named in the line's qualifier ("not every score was recorded in GW2"). The recap stashes both records on `LeagueRecapData` (`season_highest_score` / `season_lowest_score`) so `check_tie_claims` can read a record scored more than once as a shared one. Prompt-only, and absent on the league's first gameweek, where they would only restate that week's extremes |
|
|
395
|
+
| `draft_provenance` | How each owned draft player came to his manager (#458), replayed from the league's own history: the draft in force (the latest completed entry in `league.drafts` at or before the gameweek, its picks from the choices feed or, when the feed has none for that draft id, from the `league_history_drafts` snapshot), then every accepted transaction by `added` and every processed trade (`is_processed_trade()`: `p`, not the vetoed `v`) by `response_time`, in one time order, keeping each player's full chain (`DraftStep`: draft, waiver, free_agent, trade, release). `replay_draft_history()` trusts the replay only when it ends exactly where `element_status` says everyone is now and, stopped at the gameweek, matches the squads the caller observed; any disagreement, a move that does not fit the squad it applies to, or a log that could not be read (`read_draft_logs()` degrades a failed read to a missing log, never an exception; the trades log is one of them, since a trade and its reversal leave no mark on either check) returns no players and a `problem` the recap reports as `draft_provenance_unavailable`. Where neither source has a held draft's picks (a redraft was scheduled before the first snapshot) the start is rebuilt instead: each manager's squad at the draft's gameweek (`read_draft_logs(entry_ids=...)`, one request each, only then), with the moves made before that gameweek undone newest first, so the chains begin at a draft step with no pick (`DraftInForce.rounds_known` False) and run on under the same two checks; because a start derived from the logs cannot reveal a move they lack, a pre-gameweek trade made since the draft in an unseen state, an unreadable squad or an undone move that does not fit all refuse it. No squad is read when another log is missing, since the replay refuses without them. The squads cost those requests once (#472): the replay records the drafts it rebuilt (`DraftProvenance.rebuilt_drafts`) and any whose squads did not fit the moves undone (`misfit_drafts`), `keep_rebuild_squads()` keeps the squads read from the API for each draft a passing replay rebuilt once the draft game's `current_event` has reached its gameweek, and `read_draft_logs()` reads that copy third, after the feed's picks and the snapshot's and before asking the API (`DraftLogs.kept_squads` names the drafts it served); only a misfit drops a copy. `DraftLogs.choices_payload` is the choices response itself, which the recap hands to the picks snapshot so the endpoint is read once. One shared helper (`get_draft_provenance()`) so the recap's Standout Players section and the draft lines of `fpl player` and `fpl stats` (#460) read the draft one way. Beside the owned players' chains, `DraftProvenance.released` keeps the chain of every player the replay held and let go (`DraftFreeAgentHistory`, ending on his release), so a lookup can say a free agent's last exit and whether he was drafted; a player in neither was never held. The lookups reach it through `get_draft_ownership_mapping(with_provenance=True)` (`agents/common.py`), which reads the history alongside the squads, reusing the league details it already fetched, and replays it to the coming gameweek (`next_event`, else `current_event`) with no observed squads: that is the history checked against `element_status`, so its owners are FPL's, and a waiver run processed for the coming gameweek is in it even while the current squads lag. An enrichment failure of any kind becomes a `problem`, never an exception that would cost the ownership read beside it. `cli/_draft_provenance.py` turns a player's history into the line, the `fpl stats` tag and `info.draft` through one pick formatter, describing a player only where the replay's owner is the one the command shows, so a player whose move the squads have not caught up with, or whose squad could not be read, gets the plain line. |
|
|
396
|
+
| `draft_rebuild_cache` | The squads a lost draft's picks were rebuilt from (#472), one file per draft at `<cache dir>/draft_rebuilds/draft-<league id>/draft-<draft id>.json`, stamped with the season and the draft's gameweek. Kept only by `draft_provenance.keep_rebuild_squads()`, for a draft a replay rebuilt from squads read from the API and passed on, once the draft game's `current_event` (memoised game state, read only when there is something to keep) has reached the draft's gameweek. What that replay checked depends on the caller: the undone moves always fit the squads, a draft the whole history runs from also ends on `element_status`, and the recap also holds the gameweek to its squads; an older draft in force on the lookup path has the fit alone, as a live read there does. That is enough because a gameweek's squads are locked from its deadline, so the copy is the squads any later read would return. The squads rather than the set rebuilt from them, so a run reading the copy undoes the pre-gameweek moves and applies every check it would apply to a live read. A copy is dropped only when its squads do not fit the moves undone (`DraftProvenance.misfit_drafts`), the one refusal it can be blamed for (one kept before the deadline, say); a refusal on the ownership list after a waiver run leaves it. Cache-class, not ledger-class like the snapshots: the endpoint serves the squads all season. A file that cannot be read, has the wrong shape, is stamped with another season or gameweek, or holds other managers than the league's is no file, and a failed write costs only the next run's reads |
|
|
396
397
|
| `league_history_drafts` | Draft-picks snapshots (`snapshot_draft_choices()`), one file per held draft under the draft ledger partition's `drafts/` (`<data dir>/league_history/<season>/draft-<league id>/drafts/draft-<draft id>.json`), written by `league-recap --draft` through `capture_draft_picks()` (`cli/_league_recap_history.py`) after the gameweek's rows. Ledger-class data rather than a cache: `FPLDraftClient.get_draft_choices()` serves only the league's latest draft, so a scheduled redraft drops the previous draft's picks from the API for good (#461). Each file is the endpoint's response as served plus the draft's `league.drafts` entry under `league_draft`, so a hand-saved response is a valid snapshot too. Written once and never rewritten: a later run compares the picks alone (`PICK_FIELDS`), because the response's `element_status` is today's ownership and moves every waiver round, and a difference leaves the file untouched and is reported (`league_history_draft_picks_conflict`). A held draft no longer served counts as kept only when its file holds at least one of its picks, so an empty capture or an error page cannot stand in for a copy; otherwise the loss is reported once (`league_history_draft_picks_lost`) and recorded in `lost-draft-<draft id>.json` so later runs stay quiet. A response with no `choices` list is a failed read, never "no picks served", and a `league.drafts` entry without a numeric `id` and a `draft_completed` (the API sends a completion timestamp, or `null` before the draft is held; a bool is accepted too) is reported (`league_history_draft_picks_unchecked`) rather than skipped, since quietly skipping it would lose a held draft's picks with no sign. Every outcome is a value rather than an exception, reads included (no `exists()` probe that a permission error could raise through), so a file problem costs the snapshot and never the recap. `LeagueHistoryStore.partition_exists()` asks for gameweek files rather than the directory, so a snapshot landing first cannot swallow the first-capture notice. The `draft_provenance` replay reads them back (`read_saved_choices()`, `is_draft_lost()`) when the feed has moved on, and the recap hands `capture_draft_picks()` the choices response the replay already read so the endpoint is requested once per run; one loader (`_load_snapshot()`) decides what a readable snapshot is for the writer and the readers |
|
|
397
398
|
|
|
398
399
|
## LLM Provider Abstraction
|
|
@@ -533,18 +534,19 @@ fpl_cli/
|
|
|
533
534
|
│ ├── _helpers.py # Shared display utilities
|
|
534
535
|
│ ├── _gameweek_status.py # Reads FPL's post-gameweek processing feed for `league-recap` and `status` (#424): a fetch that degrades to None (unreachable, reshaped, or slower than a 5s advisory timeout) and `settled_flags()` (status's per-game three-valued AND). The still-processing decision itself is `GameweekStatus.still_processing()`, on the model
|
|
535
536
|
│ ├── _json.py # JSON output serialisation
|
|
537
|
+
│ ├── _draft_provenance.py # How a draft player came to his owner, in the words of `fpl player` and `fpl stats` (#460): `player_standing()` (described only where the replay agrees with the owner shown), the draft line's brackets, the `Draft history:` chain, the stats tag and `info.draft`
|
|
536
538
|
│ ├── _banner.py # Startup banner
|
|
537
539
|
│ ├── _plan_grid.py # Fixture grid rendering
|
|
538
540
|
│ ├── _review_*.py # Review command helpers (analysis, classic, draft, summarisation)
|
|
539
541
|
│ ├── _synthesis_retry.py # The one query-judge-retry loop `review --summarise` and `league-recap --summarise` share (#397): the attempt cap, which stops earn a retry on their own (a token ceiling) and which rule one out (a refusal, whatever else is wrong), the retry prompt told what was wrong, and a failed retry keeping the attempt in hand. Each command supplies its own judge and damage ranking
|
|
540
|
-
│ ├── _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); and `find_standout_players()`, which reads the same squads and live points for the week's top scorers and costliest blanks with the managers each counted for (issue #455), ordering draft blanks by `standout_blanks_order()`: the draft game's pre-season `draft_rank` before `STANDOUT_PPG_FROM_GW` (GW19, its own constant rather than the chip split) and on any replay, and the matched main-game player's points per game from it on a live recap, both carried on the squad rows beside minutes and never written to the ledger, built by the command for the gameweek it recaps (`_standout_players_or_none()` in `league_recap.py`, never by the collectors, so a backfill's replays skip it, and best-effort, so a row it cannot read costs only the section) and printed through `format_standout_player()` / `standout_players_lines()` in `_league_recap_types.py`, one sentence per player for the console, the
|
|
542
|
+
│ ├── _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); and `find_standout_players()`, which reads the same squads and live points for the week's top scorers and costliest blanks with the managers each counted for (issue #455), ordering draft blanks by `standout_blanks_order()`: the draft game's pre-season `draft_rank` before `STANDOUT_PPG_FROM_GW` (GW19, its own constant rather than the chip split) and on any replay, and the matched main-game player's points per game from it on a live recap, both carried on the squad rows beside minutes and never written to the ledger, built by the command for the gameweek it recaps (`_standout_players_or_none()` in `league_recap.py`, never by the collectors, so a backfill's replays skip it, and best-effort, so a row it cannot read costs only the section) and printed through `format_standout_player()` / `standout_players_lines()` in `_league_recap_types.py`, one sentence per player for the console, the prompt and the JSON `text`; the saved report leaves the section out, #482)
|
|
541
543
|
│ ├── _fines.py / _fines_config.py # League fines system, including the cohort-only/needs-a-squad rule split the coarse ledger tier narrows by
|
|
542
544
|
│ ├── league_fines.py # `league-fines`: season fine table read straight off the ledger, no network
|
|
543
545
|
│ ├── doctor_providers.py # Live provider probes for `doctor --providers` (shape/volume/join checks, including the draft club list against the main game's by code; 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)
|
|
544
546
|
│ └── [command files] # One file per command/group
|
|
545
547
|
├── agents/
|
|
546
548
|
│ ├── base.py # Agent ABC, AgentResult, AgentStatus
|
|
547
|
-
│ ├── common.py # Shared: enrich_player, fetch_understat_lookup, draft helpers
|
|
549
|
+
│ ├── common.py # Shared: enrich_player, fetch_understat_lookup, draft helpers (`get_draft_ownership_mapping` returns a `DraftOwnership`, with `owner_of()` and the replayed history when asked for, #460)
|
|
548
550
|
│ ├── data/ # FixtureAgent, PriceAgent, ScoutAgent
|
|
549
551
|
│ ├── analysis/ # StatsAgent, CaptainAgent, SquadAnalyzerAgent, BenchOrderAgent, StartingXIAgent, TransferEvalAgent
|
|
550
552
|
│ ├── action/ # WaiverAgent
|
|
@@ -592,9 +594,10 @@ fpl_cli/
|
|
|
592
594
|
│ ├── league_history.py # League history ledger store: per-gameweek NDJSON, fail-closed load, supersession, coverage query. most_recent_captured_season() -- the network-free read side's answer to "which season" (used by `league-fines`), preferring the season the ledger was actually last written under over a clock-derived guess that could disagree with it (#91)
|
|
593
595
|
│ ├── league_history_counters.py # Streak-condition registry (9 conditions, extend/reset/hold predicates) + rebuildable counters projection: own version + computed-through-gameweek stamp, fails open to a full rebuild from league_history. State carries both the open run and reset-surviving season occurrence totals with a held-gameweek tally
|
|
594
596
|
│ ├── league_history_fines.py # Season fine tally (build_season_fines_tally): per-manager, per-rule counts folded straight off the ledger, plus honest coverage qualifiers. No cache, no re-ruling
|
|
595
|
-
│ ├── draft_provenance.py # How each owned draft player came to his manager: the replay of the draft, transactions and processed trades in time order, checked against `element_status` and the observed squads, with each player's chain (#458). Reads picks from the choices feed, then the `league_history_drafts` snapshot
|
|
597
|
+
│ ├── draft_provenance.py # How each owned draft player came to his manager: the replay of the draft, transactions and processed trades in time order, checked against `element_status` and the observed squads, with each player's chain (#458). Reads picks from the choices feed, then the `league_history_drafts` snapshot, else rebuilds who drafted whom from the squads at the draft's gameweek
|
|
598
|
+
│ ├── draft_rebuild_cache.py # The squads a lost draft was rebuilt from, kept in the cache dir once a replay passes on them so later runs read none (#472). Season- and gameweek-stamped, checked against the league's managers; a file that is wrong in any way is no file
|
|
596
599
|
│ ├── league_history_drafts.py # Draft-picks snapshots: each held draft's choices response, saved once beside the ledger's gameweek files because the API stops serving it at a redraft (#461). Compared on the picks alone, never rewritten, a difference reported rather than written
|
|
597
|
-
│ └── league_history_notes.py # Notes pack (build_notes_pack) + derive_season_phase() + is_season_milestone(): per-manager streak factoids in observed-count phrasing, season-count factoids (surfaced per each condition's own CountSurfacePolicy — who fired it this gameweek plus qualifying ride-alongs; full set at the two milestones), season-phase marker, coverage/"since GW X" statements. Counters projection + a bounded trailing window of rows; full season only at the finale
|
|
600
|
+
│ └── league_history_notes.py # Notes pack (build_notes_pack) + derive_season_phase() + is_season_milestone(): per-manager streak factoids in observed-count phrasing, season-count factoids (surfaced per each condition's own CountSurfacePolicy — who fired it this gameweek plus qualifying ride-alongs; full set at the two milestones), the season's highest and lowest gameweek score (swept weekly, like the fines tally), season-phase marker, coverage/"since GW X" statements. Counters projection + a bounded trailing window of rows; full season only at the finale
|
|
598
601
|
├── models/
|
|
599
602
|
│ ├── player.py # Player, PlayerStatus, PlayerPosition, POSITION_MAP, BLANK_POINTS_THRESHOLD, name resolution (`resolve_players` / `resolve_player`, `AmbiguousPlayerError`, and the `_or_report` pair every name-taking command and script shares)
|
|
600
603
|
│ ├── team.py # Team
|
|
@@ -607,8 +610,8 @@ fpl_cli/
|
|
|
607
610
|
│ ├── scout.py # ScoutAgent system/user prompts
|
|
608
611
|
│ ├── 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
|
|
609
612
|
│ ├── returnees.py # Return-intel search prompt for `fpl returnees --enrich` (one player per query, so a citation list belongs to a single player)
|
|
610
|
-
│ ├── 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, and a manager whose moves mix kinds gets each kind's net from `draft_transaction_kind_nets()` beside the net overall (issue #390), so the editorial has a figure for their waivers alone instead of saying the combined one of them; 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_standout_players_context()` (issue #455) hands the editorial the week's top scorers and costliest blanks as the sentences the report
|
|
611
|
-
│ └── league_recap_checks.py # Post-generation checks on the league-recap editorial, kept out of the prompt module so it stays about prompts. `EDITORIAL_CHECKS` is the one registry -- each check's `RECAP_WARNING_*` code, the lead of its JSON warning (`editorial_warning_message()`, one sentence per claim), its retry instruction and the checker -- so a new check is one record. `check_contested_attributions()` (issue #357) reads who a sentence says won or was beaten to a contested player; `check_net_attributions()` (issue #359) reads a "net" figure (or a signed one after "netted"/"nets", issue #390) said of one named move and flags it when it is another figure on that manager's roster line, a kind's net on a mixed draft line included, attaching it to whichever of the move and its manager is named nearer before it; `check_kind_net_scopes()` (issue #390) reads a figure a kind word scopes ("his waiver moves netted +3") on a draft line that mixes kinds and flags it when it is the net overall, the other kind's net or one of its moves' swings — the per-kind nets read from the line's `kind_nets`, built by the same `draft_transaction_kind_nets()` the roster prints with, so the check never holds the model to a figure it was not given — taking the manager from the clause or, for a bare "his", from the nearest earlier clause of the paragraph that names exactly one, and leaving alone a figure after a kind that is only an aside, one already carrying its own figure with a new subject opening the gap to the next ("with Dunk's +1 on waivers he finished +7", issue #465); `check_tie_claims()` (issue #359) reads a tie word fused to a rank ("joint-lowest", "tied for last", "joint-seventh") against the GW Pts column, or the table's Pos, Prev and Total columns; `check_contested_roll_calls()` (issue #459) counts, paragraph by paragraph (blank-line separated, a heading its own paragraph) and only in a paragraph that names the race's player, the distinct beaten managers of one contested race that the paragraph names and flags three or more — the one check that fires on a true sentence, so it is a count of known names and never a judgement of tone, its `EditorialCheck` is marked `broken_brief`, and `split_recap_problems()` / `recap_editorial_flaw()` let the retry prompt's headers, the stderr notices and the report callout (the report agent hands the template the two lists) say the draft broke its brief instead of that the data contradicts it. All
|
|
613
|
+
│ ├── 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 its table states each manager's gameweek placing in a GW Rank column (issue #483), since its rows run in league order and a Pos read beside a gameweek score became a wrong "finished Nth", and its highest and lowest scores each carry their gap to the nearest score, naming who holds it (issue #484), since the week's runner-up is rarely the winner's neighbour in that order and a margin found by scanning the column came out a point wrong, while no line states a gap between two season totals, so the margin rule forbids working one out of the Total column (issue #493, a 55-point gap given as 50), 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, and a manager whose moves mix kinds gets each kind's net from `draft_transaction_kind_nets()` beside the net overall (issue #390), so the editorial has a figure for their waivers alone instead of saying the combined one of them; 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_standout_players_context()` (issue #455) hands the editorial the week's top scorers and costliest blanks as the sentences the console prints (the saved report never carries them, issue #482, since the editorial weaves them into its prose), each tied to the managers he counted for, with one line saying what the figures are and no rule of its own, every line a fact of this gameweek alone (a draft blank's rank or average orders the list and names the heading, but no line states it), so it has player performances to write about instead of reaching for the waiver race. `format_recap_prior_seasons_context()` (issue #131) is the opener's one source of history: each classic manager's FPL-wide seasons before this one, folded once by `summarise_prior_seasons()` (`_league_recap_types.py`) so the saved report and the prompt read the same lines, with an explicit count, the no-record and unfetched managers named rather than omitted, and a rule pinning tenure to FPL-wide phrasing — entry and league IDs are reissued each season, so "Nth season in this league" is unknowable and the section says so. The report's title is not the model's (issue #349): `recap_title()` (`_league_recap_types.py`) is the one H1, read by the template that renders it and the prompt that names it as already written; the prompt asks for a `## ` headline beneath it and no title, and `normalise_recap_editorial()` holds the reply to that shape in one fence-aware pass over every heading — one that only restates the title is dropped wherever it sits, the opening one is kept at H2 with the title's parts taken off (emphasis unwrapped through `unwrap_emphasis()` in `utils/markdown.py`, balanced pairs only, so what is printed again stays well-formed), a later `# ` is demoted, and a title-shaped part is removed only behind a colon, pipe or spaced dash so a hook's own words are never edited — since an instruction is not a contract and the model had mirrored, restyled, renamed and skipped the heading in turn. For the same reason the finished editorial is checked against its own data afterwards, by `league_recap_checks.py` beside this module; what the checks hold "joint" to is stated here, in `_standings_tie_lines()`: the week's highest and lowest scores, every gameweek score and Pos position more than one manager holds, any shared Prev position, and any season totals the league's tie-break placed apart (a head-to-head draft table ranks managers level on league points by points scored)
|
|
614
|
+
│ └── league_recap_checks.py # Post-generation checks on the league-recap editorial, kept out of the prompt module so it stays about prompts. `EDITORIAL_CHECKS` is the one registry -- each check's `RECAP_WARNING_*` code, the lead of its JSON warning (`editorial_warning_message()`, one sentence per claim), its retry instruction and the checker -- so a new check is one record. `check_contested_attributions()` (issue #357) reads who a sentence says won or was beaten to a contested player; `check_net_attributions()` (issue #359) reads a "net" figure (or a signed one after "netted"/"nets", issue #390) said of one named move and flags it when it is another figure on that manager's roster line, a kind's net on a mixed draft line included, attaching it to whichever of the move and its manager is named nearer before it; `check_kind_net_scopes()` (issue #390) reads a figure a kind word scopes ("his waiver moves netted +3") on a draft line that mixes kinds and flags it when it is the net overall, the other kind's net or one of its moves' swings — the per-kind nets read from the line's `kind_nets`, built by the same `draft_transaction_kind_nets()` the roster prints with, so the check never holds the model to a figure it was not given — taking the manager from the clause or, for a bare "his", from the nearest earlier clause of the paragraph that names exactly one, and leaving alone a figure after a kind that is only an aside, one already carrying its own figure with a new subject opening the gap to the next ("with Dunk's +1 on waivers he finished +7", issue #465); `check_tie_claims()` (issue #359) reads a tie word fused to a rank ("joint-lowest", "tied for last", "joint-seventh") against the GW Pts column, or the table's Pos, Prev and Total columns; `check_gameweek_placings()` (issue #486) reads a placing ("finished seventh", "third on the week", "came last", "the third-best score of the week") that its own stretch of the sentence makes the week's, or that the manager's own gameweek score is attached to ("finished eighth on 31", issue #483's league position read as the week's finish) or a finishing verb leads into in a clause that gives that score ("scored 31 and finished eighth"), and flags it when it is not the manager's `gameweek_rank()`, leaving alone any placing beside a word for the table, a season, a span longer than the week or a move up it (unless the word comes before the placing and its score and the week straight after it: "sits 5th on 39 for the week"), and taking the manager from the name nearest before it (only the one its own stretch names, where the clause names two) or, for a pronoun with nobody named before it, from the clause's antecedent; `check_escape_claims()` (issue #488) reads an escape from last place ("escaped the wooden spoon", "avoided last place", "missed the dreaded last place", "clear of the foot of the table") and flags it when the manager whose escape it is was on the week's lowest score, or, where the words naming where from name the table (or, alongside the week, where a holding verb leads into it), is bottom of the table, leaving alone an escape that says nothing happened (a hedge, a base form, an infinitive nothing says was achieved, a gerund after a preposition) -- the escape the GW Standings section's lowest line states (issue #487) in the one wording `last_place_escape()` (`_league_recap_types.py`) gives the prompt and the retry alike, which once went to the manager who finished last through a pronoun, so the margin rule no longer forces names on every sentence that uses a gap; it and the placing check take their subject from the one `_claim_subject()` (the escape check, with `opener=True`, carrying a pronoun to the first manager the clause before named, the subject a reader takes it for) and their stretch and negation from `_claim_stretch()` / `_claim_negated()`; the tie and placing checks rank with `competition_rank()` (`_league_recap_types.py`), which the GW Rank column and the report's standings block count with too, so a change to how a tie ranks there reaches all four at once (league positions come from `derive_point_in_time_positions()`, a separate competition ranking); `_read_clauses()` is the one reader of a clause's names, antecedent -- the one manager the nearest earlier clause of the paragraph to name anyone named -- and opener, the first manager that clause named -- shared by `check_kind_net_scopes()`, `check_gameweek_placings()` and `check_escape_claims()`, so a fix to how a pronoun is carried lands in all three; `check_contested_roll_calls()` (issue #459) counts, paragraph by paragraph (blank-line separated, a heading its own paragraph) and only in a paragraph that names the race's player, the distinct beaten managers of one contested race that the paragraph names and flags three or more — the one check that fires on a true sentence, so it is a count of known names and never a judgement of tone, its `EditorialCheck` is marked `broken_brief`, and `split_recap_problems()` / `recap_editorial_flaw()` let the retry prompt's headers, the stderr notices and the report callout (the report agent hands the template the two lists) say the draft broke its brief instead of that the data contradicts it. All seven read names the same way: `check_recap_editorial()` builds one `RecapNames` per editorial -- a longest-first alternation over every name the data carries (every squad, captain, move and lost claim's players, the managers and their unique short forms) -- and each check takes it as a required argument and keeps only the matches it is after, so a manager's surname is never read inside a hyphenated player ("White" in "Gibbs-White") nor a player inside a manager's full name (issue #379). Each reads only the clauses it can pin down and lets the rest pass, so a problem is a sentence the data contradicts and a false alarm (which costs a retry and a warning above a correct editorial) is the failure it is built against. `check_recap_editorial()` runs them all; the command regenerates once with `get_recap_editorial_retry_prompt()` (the wrong claims plus an instruction per kind that failed, a contradiction and a broken brief each under a header that fits); the less damaged attempt ships, ranking a complete one over a cut-off one, then fewer contradicted claims, then fewer broken briefs, saves a still-failing editorial as written with one JSON warning per code and the claims in a callout above it, and ships the editorial unchecked if a check itself raises
|
|
612
615
|
├── parsers/
|
|
613
616
|
│ └── recommendations.py # Parse gw{N}-recommendations.md into structured decisions for `fpl review --compare-recs`. Reads the gw-prep output template's tables by column name, not position (the transfer table is Out-then-In, the waiver table Drop-then-Claim, and the free-agent table a post-waiver file carries instead is `# | Drop | Pick up`), the `**Recommended captain:**` line, and on a wildcard or free hit week the embedded Classic Squad's Starting XI and Bench tables, which come back as `classic.squad` instead of transfers. Tables and player cells are read through `utils/markdown.py`'s shared table reader. Names keep their initial and hand their club code over separately (`in_team`/`out_team`, read only from a bracket that holds nothing but codes, so a rationale like "(home vs WHU)" is not taken for a club), because stripping the initial merged `B.Fernandes` with `J.Fernandes` (#354). The Classic and Draft sections each end at the next `## ` heading, so a `## GW Update` that update-gw-prep appends is not read as more of the baseline. The heading format the template replaced is still read as a fallback for older files. A part the file writes a section for (one with a sub-heading or a table, not a one-line stub) but that yields nothing (the captain, the transfers, the waivers, each judged on its own) is named in `unread`, because finding nothing is a parse that failed, not advice to do nothing: only a roll or a "no waivers" said in words, each under its own Recommendations heading, counts as the latter, and the review leaves an unread part's moves ungraded with a warning on stderr rather than grading every one unadvised, and skips the comparison when nothing was read at all (#355). `tests/test_recommendations_parser.py` parses both skill templates themselves, so a template change the parser does not follow fails there
|
|
614
617
|
├── scraper/
|