From f2c09d09e677d3cc0beb8af1f3235091a8b61a3c Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Tue, 24 Mar 2026 09:07:09 -0500 Subject: [PATCH 1/4] docs: add Refractor Phase 2 design validation spec Seven pre-implementation test cases covering: 108-sum invariant preservation under profile-based boosts, D20 probability shift magnitude at T4, pipeline collision risk between T4 rarity upgrade and live-series post_player_updates, HoF rarity cap (non-contiguous ID ladder), RP T1 achievability, SP/RP/batter T4 parity, and the cross-season stat accumulation design decision that must be confirmed before Phase 2 code is written. Co-Authored-By: Claude Sonnet 4.6 --- docs/REFRACTOR_PHASE2_VALIDATION_SPEC.md | 447 +++++++++++++++++++++++ 1 file changed, 447 insertions(+) create mode 100644 docs/REFRACTOR_PHASE2_VALIDATION_SPEC.md diff --git a/docs/REFRACTOR_PHASE2_VALIDATION_SPEC.md b/docs/REFRACTOR_PHASE2_VALIDATION_SPEC.md new file mode 100644 index 0000000..edb3799 --- /dev/null +++ b/docs/REFRACTOR_PHASE2_VALIDATION_SPEC.md @@ -0,0 +1,447 @@ +# Refractor Phase 2 — Design Validation Spec + +## Purpose + +This document captures the design validation test cases that must be verified before and during +Phase 2 (rating boosts) of the Refractor card progression system. Phase 1 — tracking, +milestone evaluation, and tier state persistence — is implemented. Phase 2 adds the rating boost +application logic (`apply_evolution_boosts`), rarity upgrade at T4, and variant hash creation. + +**When to reference this document:** + +- Before beginning Phase 2 implementation: review all cases to understand the design constraints + and edge cases the implementation must handle. +- During implementation: use each test case as an acceptance gate before the corresponding + feature is considered complete. +- During code review: each case documents the "risk if failed" so reviewers can assess whether + a proposed implementation correctly handles that scenario. +- After Phase 2 ships: run the cases as a regression checklist before any future change to the + boost logic, rarity assignment, or milestone evaluator. + +## Background: Rating Model + +Batter cards have 22 outcome columns summing to exactly 108 chances (derived from the D20 +probability system: 2d6 x 3 columns x 6 rows). Each Refractor tier (T1 through T4) awards a +1.0-chance budget — a flat shift from out columns to positive-outcome columns. The total +accumulated budget across all four tiers is 4.0 chances, equal to approximately 3.7% of the +108-chance total (4 / 108 ≈ 0.037). + +Rarity IDs in the codebase (from `rarity_thresholds.py`): + +| Rarity Name | ID | +|---|---| +| Common | 5 | +| Bronze | 4 | +| Silver | 3 | +| Gold | 2 | +| Diamond | 1 | +| Hall of Fame | 99 | + +The special value `99` for Hall of Fame means a naive `rarity_id + 1` increment is incorrect; +the upgrade logic must use an ordered rarity ladder, not arithmetic. + +--- + +## Test Cases + +--- + +### T4-1: 108-sum preservation under profile-based boosts + +**Status:** Pending — Phase 2 + +**Scenario:** + +`apply_evolution_boosts(card_ratings, boost_tier, player_profile)` redistributes 1.0 chance per +tier across outcome columns according to the player's detected profile (power hitter, contact +hitter, patient hitter, starting pitcher, relief pitcher). Every combination of profile and tier +must leave the 22-column sum exactly equal to 108 after the boost is applied. This must hold for +all four tier applications, cumulative as well as individual. + +The edge case: a batter card where `flyout_a = 0`. The power and contact hitter profiles draw +reductions from out columns including `flyout_a`. If the preferred reduction column is at zero, +the implementation must not produce a negative value and must not silently drop the remainder of +the budget. The 0-floor cap is enforced per column (see `05-rating-boosts.md` section 5.1: +"Truncated points are lost, not redistributed"). + +Verify: +- After each of T1, T2, T3, T4 boost applications, `sum(all outcome columns) == 108` exactly. +- A card with `flyout_a = 0` does not raise an error and does not produce a column below 0. +- When truncation occurs (column already at 0), the lost budget is discarded, not moved + elsewhere — the post-boost sum will be less than 108 + budget_added only in the case of + truncation, but must never exceed 108. + +**Expected Outcome:** + +Sum remains 108 after every boost under non-truncation conditions. Under truncation conditions +(a column hits 0), the sum is reduced by the truncated amount — the implementation discards the +excess rather than redistributing it. No column value falls below 0. + +**Risk If Failed:** + +A broken 108-sum produces invalid game probabilities. The D20 engine derives per-outcome +probabilities from `column / 108`. If the sum drifts above or below 108, every outcome +probability on that card is subtly wrong for every future game that uses it. This error silently +corrupts game results without any visible failure. + +**Files Involved:** + +- `docs/prd-evolution/05-rating-boosts.md` — boost budget, profile definitions, cap behavior +- Phase 2: `pd_cards/evo/boost_profiles.py` (to be created) — `apply_evolution_boosts` +- `batters/creation.py` — `battingcardratings` model column set (22 columns) +- `pitchers/creation.py` — `pitchingcardratings` model column set (18 columns + 9 x-checks) + +--- + +### T4-2: D20 probability shift at T4 + +**Status:** Pending — Phase 2 + +**Scenario:** + +Take a representative Bronze-rarity batter (e.g., a player with total OPS near 0.730, +`homerun` ≈ 1.2, `single_one` ≈ 4.0, `walk` ≈ 3.0 in the base ratings). Apply all four +tier boosts cumulatively, distributing the total 4.0-chance budget across positive-outcome +columns (HR, singles, walk) with equal reductions from out columns. Calculate the resulting +absolute and relative probability change per D20 roll outcome. + +Design target: the full T4 evolution shifts approximately 3.7% of all outcomes from outs to +positive results (4.0 / 108 = 0.037). The shift should be perceptible to a player reviewing +their card stats but should not fundamentally alter the card's tier or role. A Bronze batter +does not become a Gold batter through evolution — they become an evolved Bronze batter. + +Worked example for validation reference: +- Pre-evolution: `homerun = 1.2` → probability per D20 = 1.2 / 108 ≈ 1.11% +- Post T4 with +0.5 to homerun per tier (4 tiers × 0.5 = +2.0 total): `homerun = 3.2` + → probability per D20 = 3.2 / 108 ≈ 2.96% — an increase of ~1.85 percentage points +- Across all positive outcomes: total shift = 4.0 / 108 ≈ 3.7% + +**Expected Outcome:** + +The cumulative 4.0-chance shift produces a ~3.7% total movement from negative to positive +outcomes. No single outcome column increases by more than 2.5 chances across the full T4 +journey under any profile. The card remains recognizably Bronze — it does not cross the Gold +OPS threshold (0.700 for 2024/2025 thresholds) unless it was already near the boundary. + +**Risk If Failed:** + +If the shift is too large, evolution becomes a rarity bypass — players grind low-rarity cards +to simulate an upgrade they cannot earn through pack pulls. If the shift is too small, the +system feels unrewarding and players lose motivation to complete tiers. Either miscalibration +undermines the core design intent. + +**Files Involved:** + +- `docs/prd-evolution/05-rating-boosts.md` — section 5.2 (boost budgets), section 5.3 (profiles) +- `rarity_thresholds.py` — OPS boundary values used to assess whether evolution crosses a rarity + threshold as a side effect (it should not for mid-range cards) +- Phase 2: `pd_cards/evo/boost_profiles.py` — boost distribution logic + +--- + +### T4-3: T4 rarity upgrade — pipeline collision risk + +**Status:** Pending — Phase 2 + +**Scenario:** + +The Refractor T4 rarity upgrade (`player.rarity_id` incremented by one ladder step) and the +live-series `post_player_updates()` rarity assignment (OPS-threshold-based, in +`batters/creation.py`) both write to the same `rarity_id` field on the player record. A +collision occurs when both run against the same player: + +1. Player completes Refractor T4. Evolution system upgrades rarity: Bronze (4) → Silver (3). + `evolution_card_state.final_rarity_id = 3` is written as an audit record. +2. Live-series update runs two weeks later. `post_player_updates()` recalculates OPS → maps to + Bronze (4) → writes `rarity_id = 4` to the player record. +3. The T4 rarity upgrade is silently overwritten. The player's card reverts to Bronze. The + `evolution_card_state` record still shows `final_rarity_id = 3` but the live card is Bronze. + +This is a conflict between two independent systems both writing to the same field without +awareness of each other. The current live-series pipeline has no concept of evolution state. + +Proposed resolution strategies (document and evaluate; do not implement during Phase 2 spec): +- **Guard clause in `post_player_updates()`:** Before writing `rarity_id`, check + `evolution_card_state.final_rarity_id` for the player. If an evolution upgrade is on record, + apply `max(ops_rarity, final_rarity_id_ladder_position)` — never downgrade past the T4 result. +- **Separate evolution rarity field:** Add `evolution_rarity_bump` (int, default 0) to the + card model. The game engine resolves effective rarity as `base_rarity + bump`. Live-series + updates only touch `base_rarity`; the bump is immutable once T4 is reached. +- **Deferred rarity upgrade:** T4 does not write `rarity_id` immediately. Instead, it sets a + flag on `evolution_card_state`. `post_player_updates()` checks the flag and applies the bump + after its own rarity calculation, ensuring the evolution upgrade layers on top of the current + OPS-derived rarity rather than competing with it. + +**Expected Outcome:** + +Phase 2 must implement one of these strategies (or an alternative that provides equivalent +protection). The collision scenario must be explicitly tested: evolve a Bronze card to T4, +run a live-series update that maps the same player to Bronze, confirm the displayed rarity is +Silver or higher — not Bronze. + +**Risk If Failed:** + +Live-series updates silently revert T4 rarity upgrades. Players invest significant game time +reaching T4, receive the visual rarity upgrade, then lose it after the next live-series run +with no explanation. This is one of the highest-trust violations the system can produce — a +reward that disappears invisibly. + +**Files Involved:** + +- `batters/creation.py` — `post_player_updates()` (lines ~304–480) +- `pitchers/creation.py` — equivalent `post_player_updates()` for pitchers +- `docs/prd-evolution/05-rating-boosts.md` — section 5.4 (rarity upgrade at T4), note on live + series interaction +- Phase 2: `pd_cards/evo/tier_completion.py` (to be created) — T4 completion handler +- Database: `evolution_card_state` table, `final_rarity_id` column + +--- + +### T4-4: T4 rarity cap for HoF cards + +**Status:** Pending — Phase 2 + +**Scenario:** + +A player card currently at Hall of Fame rarity (`rarity_id = 99`) completes Refractor T4. The +design specifies: HoF cards receive the T4 rating boost deltas (1.0 chance shift) but do not +receive a rarity upgrade. The rarity stays at 99. + +The implementation must handle this without producing an invalid rarity value. The rarity ID +sequence in `rarity_thresholds.py` is non-contiguous — the IDs are: + +``` +5 (Common) → 4 (Bronze) → 3 (Silver) → 2 (Gold) → 1 (Diamond) → 99 (Hall of Fame) +``` + +A naive `rarity_id + 1` would produce `100`, which is not a valid rarity. A lookup-table +approach on the ordered ladder must be used instead. At `99` (HoF), the ladder returns `99` +(no-op). Additionally, Diamond (1) cards that complete T4 should upgrade to HoF (99), not to +`rarity_id = 0` or any other invalid value. + +**Expected Outcome:** + +- `rarity_id = 99` (HoF): T4 boost applied, rarity unchanged at 99. +- `rarity_id = 1` (Diamond): T4 boost applied, rarity upgrades to 99 (HoF). +- `rarity_id = 2` (Gold): T4 boost applied, rarity upgrades to 1 (Diamond). +- `rarity_id = 3` (Silver): T4 boost applied, rarity upgrades to 2 (Gold). +- `rarity_id = 4` (Bronze): T4 boost applied, rarity upgrades to 3 (Silver). +- `rarity_id = 5` (Common): T4 boost applied, rarity upgrades to 4 (Bronze). +- No card ever receives `rarity_id` outside the set {1, 2, 3, 4, 5, 99}. + +**Risk If Failed:** + +An invalid rarity ID (e.g., 0, 100, or None) propagates into the game engine and Discord bot +display layer. Cards with invalid rarities may render incorrectly, break sort/filter operations +in pack-opening UX, or cause exceptions in code paths that switch on rarity values. + +**Files Involved:** + +- `rarity_thresholds.py` — authoritative rarity ID definitions +- `docs/prd-evolution/05-rating-boosts.md` — section 5.4 (HoF cap behavior) +- Phase 2: `pd_cards/evo/tier_completion.py` — rarity ladder lookup, T4 completion handler +- Database: `evolution_card_state.final_rarity_id` + +--- + +### T4-5: RP T1 achievability in realistic timeframe + +**Status:** Pending — Phase 2 + +**Scenario:** + +The Relief Pitcher track formula is `IP + K` with a T1 threshold of 3. The design intent is +"almost any active reliever hits this" in approximately 2 appearances (from `04-milestones.md` +section 4.2). The scenario to validate: a reliever who throws 1.2 IP (4 outs) with 1 K in an +appearance scores `1.33 + 1 = 2.33` — below T1. This reliever needs another appearance before +reaching T1. + +The validation question is whether this is a blocking problem. If typical active RP usage +(5+ team game appearances) reliably produces T1 within a few sessions of play, the design is +sound. If a reliever can appear 4–5 times and still not reach T1 due to short, low-strikeout +outings (e.g., a pure groundball closer who throws 1.0 IP / 0 K per outing), the threshold +may be too high for the RP role to feel rewarding. + +Reference calibration data from Season 10 (via `evo_milestone_simulator.py`): ~94% of all +relievers reached T1 under the IP+K formula with the threshold of 3. However, this is based on +a full or near-full season of data. The question is whether early-season RP usage (first 3–5 +team games) produces T1 reliably. + +Worked example for a pure-groundball closer: +- 5 appearances × (1.0 IP + 0 K) = 5.0 — reaches T1 (threshold 3) after appearance 3 +- 5 appearances × (0.2 IP + 0 K) = 1.0 — does not reach T1 after 5 appearances + +The second case (mop-up reliever with minimal usage) is expected to not reach T1 quickly, and +the design accepts this. What is NOT acceptable: a dedicated closer or setup man with 2+ IP per +session failing to reach T1 after 5+ appearances. + +**Expected Outcome:** + +A reliever averaging 1.0+ IP per appearance reaches T1 after 3 appearances. A reliever +averaging 0.5+ IP per appearance reaches T1 after 5–6 appearances. A reliever with fewer than +3 total appearances in a season is not expected to reach T1 — this is acceptable. The ~94% +Season 10 T1 rate confirms the threshold is calibrated correctly for active relievers. + +**Risk If Failed:** + +If active relievers (regular bullpen roles) cannot reach T1 within 5–10 team games, the +Refractor system is effectively dead for RP cards from launch. Players who pick up RP cards +expecting progression will see no reward for multiple play sessions, creating a negative first +impression of the entire system. + +**Files Involved:** + +- `docs/prd-evolution/04-milestones.md` — section 4.2 (RP track thresholds and design intent), + section 4.3 (Season 10 calibration data) +- `scripts/evo_milestone_simulator.py` — `formula_rp_ip_k`, `simulate_tiers` — re-run against + current season data to validate T1 achievability in early-season usage windows +- Database: `evolution_track` table — threshold values (admin-tunable, no code change required + if recalibration is needed) + +--- + +### T4-6: SP/RP T4 parity with batters + +**Status:** Pending — Phase 2 + +**Scenario:** + +The T4 thresholds are: + +| Position | T4 Threshold | Formula | +|---|---|---| +| Batter | 896 | PA + (TB x 2) | +| Starting Pitcher | 240 | IP + K | +| Relief Pitcher | 70 | IP + K | + +These were calibrated against Season 10 production data using `evo_milestone_simulator.py`. +The calibration target was approximately 3% of active players reaching T4 over a full season +across all position types. The validation here is that this parity holds: one position type +does not trivially farm Superfractors while another cannot reach T2 without extraordinary +performance. + +The specific risk: SP T4 requires 240 IP+K across the full season. Top Season 10 SPs (Harang: +163, deGrom: 143) were on pace for T4 at the time of measurement but had not crossed 240 yet. +If the final-season data shows a spike (e.g., 10–15% of SPs reaching T4 vs. 3% of batters), +the SP threshold needs adjustment. Conversely, if no reliever reaches T4 in a full season +where 94% reach T1, the RP T4 threshold of 70 may be achievable only by top closers in +extreme usage scenarios. + +Validation requires re-running `evo_milestone_simulator.py --season ` with the final +season data for all three position types and comparing T4 reach percentages. Accepted tolerance: +T4 reach rate within 2x across position types (e.g., if batters are at 3%, SP and RP should be +between 1.5% and 6%). + +**Expected Outcome:** + +All three position types produce T4 rates between 1% and 6% over a full season of active play. +No position type produces T4 rates above 10% (trivially farmable) or below 0.5% (effectively +unachievable). SP and RP T4 rates should be comparable because their thresholds were designed +together with the same 3% target in mind. + +**Risk If Failed:** + +If SP is easy (T4 in half a season) while RP is hard (T4 only for elite closers), then SP card +owners extract disproportionate value from the system. The Refractor system's balance premise +— "same tier, same reward, regardless of position" — breaks down, undermining player confidence +in the fairness of the progression. + +**Files Involved:** + +- `docs/prd-evolution/04-milestones.md` — section 4.3 (Season 10 calibration table) +- `scripts/evo_milestone_simulator.py` — primary validation tool; run with `--all-formulas + --pitchers-only` and `--batters-only` flags against final season data +- Database: `evolution_track` table — thresholds are admin-tunable; recalibration does not + require a code deployment + +--- + +### T4-7: Cross-season stat accumulation — design confirmation + +**Status:** Pending — Phase 2 + +**Scenario:** + +The milestone evaluator (Phase 1, already implemented) queries `BattingSeasonStats` and +`PitchingSeasonStats` and SUMs the formula metric across all rows for a given +`(player_id, team_id)` pair, regardless of season number. This means a player's Refractor +progress is cumulative across seasons: if a player reaches 400 batter points in Season 10 and +another 400 in Season 11, their total is 800 — within range of T4 (threshold: 896). + +This design must be confirmed as intentional before Phase 2 is implemented, because it has +significant downstream implications: + +1. **Progress does not reset between seasons.** A player who earns a card across multiple + seasons continues progressing the same Refractor state. Season boundaries are invisible to + the evaluator. +2. **New teams start from zero.** If a player trades away a card and acquires a new copy of the + same player, the new card's `evolution_card_state` row starts at T0. The stat accumulation + query is scoped to `(player_id, team_id)`, so historical stats from the previous owner are + not inherited. +3. **Live-series stat updates do not retroactively change progress.** The evaluator reads + finalized season stat rows. If a player's Season 10 stats are adjusted via a data correction, + the evaluator will pick up the change on the next evaluation run — progress could shift + backward if a data correction removes a game's stats. +4. **The "full season" targets in the design docs (e.g., "T4 requires ~120 games") assume + cumulative multi-season play, not a single season.** At ~7.5 batter points per game, T4 of + 896 requires approximately 120 in-game appearances. A player who plays 40 games per season + across three seasons reaches T4 in their third season. + +This is the confirmed intended design per `04-milestones.md`: "Cumulative within a season — +progress never resets mid-season." The document does not explicitly state "cumulative across +seasons," but the evaluator implementation (SUM across all rows, no season filter) makes this +behavior implicit. This test case exists to surface that ambiguity and require an explicit +design decision before Phase 2 ships. + +**Expected Outcome:** + +Before Phase 2 implementation begins, the design intent must be explicitly confirmed in writing +(update `04-milestones.md` section 4.1 with a cross-season statement) or the evaluator query +must be updated to add a season boundary. The options are: + +- **Option A (current behavior — accumulate across seasons):** Document explicitly. The + Refractor journey can span multiple seasons. Long-term card holders are rewarded for loyalty. +- **Option B (reset per season):** Add a season filter to the evaluator query. Refractor + progress resets at season start. T4 is achievable within a single full season. Cards earned + mid-season have a natural catch-up disadvantage. + +This spec takes no position on which option is correct. It records that the choice exists, +that the current implementation defaults to Option A, and that Phase 2 must not be built on +an unexamined assumption about which option is in effect. + +**Risk If Failed:** + +If Option A is unintentional and players discover their Refractor progress carries over across +seasons before it is documented as a feature, they will optimize around it in ways the design +did not anticipate (e.g., holding cards across seasons purely to farm Refractor tiers). If +Option B is unintentional and progress resets each season without warning, players who invested +heavily in T3 at season end will be angry when their progress disappears. + +**Files Involved:** + +- `docs/prd-evolution/04-milestones.md` — section 4.1 (design principles) — **requires update + to state the cross-season policy explicitly** +- Phase 1 (implemented): `pd_cards/evo/evaluator.py` — stat accumulation query; inspect the + WHERE clause for any season filter +- Database: `BattingSeasonStats`, `PitchingSeasonStats` — confirm schema includes `season` + column and whether the evaluator query filters on it +- Database: `evolution_card_state` — confirm there is no season-reset logic in the state + management layer + +--- + +## Summary Status + +| ID | Title | Status | +|---|---|---| +| T4-1 | 108-sum preservation under profile-based boosts | Pending — Phase 2 | +| T4-2 | D20 probability shift at T4 | Pending — Phase 2 | +| T4-3 | T4 rarity upgrade — pipeline collision risk | Pending — Phase 2 | +| T4-4 | T4 rarity cap for HoF cards | Pending — Phase 2 | +| T4-5 | RP T1 achievability in realistic timeframe | Pending — Phase 2 | +| T4-6 | SP/RP T4 parity with batters | Pending — Phase 2 | +| T4-7 | Cross-season stat accumulation — design confirmation | Pending — Phase 2 | + +All cases are unblocked pending Phase 2 implementation. T4-7 requires a design decision before +any Phase 2 code is written. T4-3 requires a resolution strategy to be selected before the T4 +completion handler is implemented. From 6f67cfec9a8a90213b0282542ce98da0e573a3c2 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Tue, 24 Mar 2026 16:07:18 -0500 Subject: [PATCH 2/4] =?UTF-8?q?docs:=20address=20PR=20#51=20review=20?= =?UTF-8?q?=E2=80=94=20rarity=20naming,=20OPS=20threshold,=20truncation=20?= =?UTF-8?q?invariant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add rarity name cross-reference table in Background section mapping PRD display names (Replacement/Reserve/Starter/All-Star/MVP/Hall of Fame) to codebase names (Common/Bronze/Silver/Gold/Diamond/HoF) with IDs - Fix T4-2: correct Gold OPS threshold from 0.700 to 0.900 (confirmed in rarity_thresholds.py); add note that 0.700 is the Bronze floor - Fix T4-1: restate truncation invariant as a single precise assertion — sum(columns) == 108 - truncated_amount — instead of two independent checks that can both pass while the sum is wrong for unrelated reasons Co-Authored-By: Claude Sonnet 4.6 --- docs/REFRACTOR_PHASE2_VALIDATION_SPEC.md | 27 +++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/REFRACTOR_PHASE2_VALIDATION_SPEC.md b/docs/REFRACTOR_PHASE2_VALIDATION_SPEC.md index edb3799..bd03257 100644 --- a/docs/REFRACTOR_PHASE2_VALIDATION_SPEC.md +++ b/docs/REFRACTOR_PHASE2_VALIDATION_SPEC.md @@ -26,6 +26,21 @@ probability system: 2d6 x 3 columns x 6 rows). Each Refractor tier (T1 through T accumulated budget across all four tiers is 4.0 chances, equal to approximately 3.7% of the 108-chance total (4 / 108 ≈ 0.037). +**Rarity naming cross-reference:** The PRD chapters (`prd-evolution/`) use the player-facing +display names. The codebase and this spec use the internal names from `rarity_thresholds.py`. +They map as follows: + +| PRD / Display Name | Codebase Name | ID | +|---|---|---| +| Replacement | Common | 5 | +| Reserve | Bronze | 4 | +| Starter | Silver | 3 | +| All-Star | Gold | 2 | +| MVP | Diamond | 1 | +| Hall of Fame | HoF | 99 | + +All rarity references in this spec use the codebase names. + Rarity IDs in the codebase (from `rarity_thresholds.py`): | Rarity Name | ID | @@ -74,8 +89,12 @@ Verify: **Expected Outcome:** Sum remains 108 after every boost under non-truncation conditions. Under truncation conditions -(a column hits 0), the sum is reduced by the truncated amount — the implementation discards the -excess rather than redistributing it. No column value falls below 0. +(a column hits 0), the final column sum must equal exactly `108 - truncated_amount` — where +`truncated_amount` is the portion of the 1.0-chance budget that was dropped due to the 0-floor +cap. This is a single combined assertion: `sum(columns) == 108 - truncated_amount`. Checking +"sum <= 108" and "truncated amount was discarded" as two independent conditions is insufficient +— a test can pass both checks while the sum is wrong for an unrelated reason (e.g., a positive +column also lost value due to a bug). No column value falls below 0. **Risk If Failed:** @@ -121,7 +140,9 @@ Worked example for validation reference: The cumulative 4.0-chance shift produces a ~3.7% total movement from negative to positive outcomes. No single outcome column increases by more than 2.5 chances across the full T4 journey under any profile. The card remains recognizably Bronze — it does not cross the Gold -OPS threshold (0.700 for 2024/2025 thresholds) unless it was already near the boundary. +OPS threshold (0.900 for 2024/2025 thresholds; confirmed in `rarity_thresholds.py` +`BATTER_THRESHOLDS_2024.gold` and `BATTER_THRESHOLDS_2025.gold`) unless it was already near +the boundary. Note: 0.700 is the Bronze floor (`bronze` field), not the Gold threshold. **Risk If Failed:** From 43aff3568f975f5adbf8c7518ba75ab93949f4eb Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 30 Mar 2026 07:53:58 -0500 Subject: [PATCH 3/4] fix: increase API timeouts to prevent bulk query failures db_calls.py default timeouts raised from 3s to 30s across all methods (db_get, url_get, db_patch, db_post, db_put). scouting_batters.py fetch_data now passes timeout=120 for large card rating queries. Co-Authored-By: Claude Opus 4.6 (1M context) --- db_calls.py | 10 +++++----- scouting_batters.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/db_calls.py b/db_calls.py index ff31a53..faf2cc0 100644 --- a/db_calls.py +++ b/db_calls.py @@ -61,7 +61,7 @@ async def db_get( object_id: int = None, params: list = None, none_okay: bool = True, - timeout: int = 3, + timeout: int = 30, ) -> Optional[dict]: req_url = get_req_url(endpoint, api_ver=api_ver, object_id=object_id, params=params) log_string = f"get:\n{endpoint} id: {object_id} params: {params}" @@ -86,7 +86,7 @@ async def db_get( raise ValueError(f"DB: {e}") -async def url_get(url: str, timeout: int = 3) -> dict: +async def url_get(url: str, timeout: int = 30) -> dict: log_string = f"get:\n{url}" logger.info(log_string) if master_debug else logger.debug(log_string) @@ -105,7 +105,7 @@ async def url_get(url: str, timeout: int = 3) -> dict: async def db_patch( - endpoint: str, object_id: int, params: list, api_ver: int = 2, timeout: int = 3 + endpoint: str, object_id: int, params: list, api_ver: int = 2, timeout: int = 30 ) -> dict: req_url = get_req_url(endpoint, api_ver=api_ver, object_id=object_id, params=params) log_string = f"patch:\n{endpoint} {params}" @@ -126,7 +126,7 @@ async def db_patch( async def db_post( - endpoint: str, api_ver: int = 2, payload: dict = None, timeout: int = 3 + endpoint: str, api_ver: int = 2, payload: dict = None, timeout: int = 30 ) -> dict: req_url = get_req_url(endpoint, api_ver=api_ver) log_string = f"post:\n{endpoint} payload: {payload}\ntype: {type(payload)}" @@ -147,7 +147,7 @@ async def db_post( async def db_put( - endpoint: str, api_ver: int = 2, payload: dict = None, timeout: int = 3 + endpoint: str, api_ver: int = 2, payload: dict = None, timeout: int = 30 ) -> dict: req_url = get_req_url(endpoint, api_ver=api_ver) log_string = f"put:\n{endpoint} payload: {payload}\ntype: {type(payload)}" diff --git a/scouting_batters.py b/scouting_batters.py index ac9222e..7bb6e90 100644 --- a/scouting_batters.py +++ b/scouting_batters.py @@ -96,7 +96,7 @@ def build_c_throw(all_positions, pos_code): async def fetch_data(data): start_time = log_time("start", print_to_console=False) - this_query = await db_get(endpoint=data[0], params=data[1]) + this_query = await db_get(endpoint=data[0], params=data[1], timeout=120) log_time("end", print_to_console=False, start_time=start_time) return this_query From 94fd72344da6ed14e94c097aa9420b1e9e8e57e8 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Wed, 1 Apr 2026 11:58:58 -0500 Subject: [PATCH 4/4] fix: scrub exposed API token from docs Replace real API token in PD_CARDS_CLI_REFERENCE.md example command with placeholder value to prevent credential exposure in the repo. Closes #50 --- docs/PD_CARDS_CLI_REFERENCE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/PD_CARDS_CLI_REFERENCE.md b/docs/PD_CARDS_CLI_REFERENCE.md index 4ca65b3..bccff6b 100644 --- a/docs/PD_CARDS_CLI_REFERENCE.md +++ b/docs/PD_CARDS_CLI_REFERENCE.md @@ -418,7 +418,7 @@ pd-cards upload s3 --cardset [OPTIONS] cd /mnt/NV2/Development/paper-dynasty/database DATABASE_TYPE=postgresql POSTGRES_HOST=10.10.0.42 POSTGRES_DB=paperdynasty_dev \ POSTGRES_USER=sba_admin POSTGRES_PASSWORD= POSTGRES_PORT=5432 \ -API_TOKEN=Tp3aO3jhYve5NJF1IqOmJTmk \ +API_TOKEN=your-api-token-here \ uvicorn app.main:app --host 0.0.0.0 --port 8000 # Terminal 2: Upload with local rendering