GET /api/v2/refractor/cards: add evaluated_only filter to exclude zero-value cards #174

Closed
opened 2026-03-25 22:05:14 +00:00 by cal · 1 comment
Owner

Problem

GET /api/v2/refractor/cards?team_id=31 currently returns all RefractorCardState rows for a team — including cards that have current_value = 0 and have never been evaluated against season stats. For team 31 this produces 2753 results when only ~14 cards have meaningful data.

This happens because RefractorCardState rows are created eagerly (one row per card when it enters a roster), but current_value only becomes non-zero after evaluate_card() runs for that player. Cards that have never appeared in a game will have current_value = 0.0 and last_evaluated_at = NULL.

The /refractor status Discord command fetches this endpoint for every invocation and pages through the full result set. At 2753 rows with most being zero-value placeholders, the command is fetching and paginating over data that is meaningless to display.


Design Recommendation

Add an evaluated_only query parameter (default True)

GET /api/v2/refractor/cards?team_id=31&evaluated_only=true   # default — excludes zero-value cards
GET /api/v2/refractor/cards?team_id=31&evaluated_only=false  # returns all rows including unevaluated

Default to True — the bot never needs to see unevaluated cards, and the admin/pipeline use case for seeing all rows is rare. A False opt-in is easier to reason about than a True opt-in.

Filter predicate

The cleanest signal for "has been evaluated" is last_evaluated_at IS NOT NULL, not current_value > 0.

Rationale:

  • A card evaluated after a legitimately scoreless or stat-less performance (e.g. a pitcher who threw but walked everyone, or a batter who reached only via walk in a PA-less edge case) would have current_value = 0.0 with last_evaluated_at populated. Filtering on current_value > 0 would silently drop that card.
  • last_evaluated_at IS NOT NULL precisely tracks the state transition "this card has gone through at least one evaluation cycle" — which is the actual question the bot needs answered.

In Peewee this is a one-line addition to the existing query:

if evaluated_only:
    query = query.where(RefractorCardState.last_evaluated_at.is_null(False))

No schema change required

This is a pure query-layer filter. No migration, no new index needed (the team index already covers the base query; the additional predicate adds negligible cost over a small evaluated set).


Edge Cases

Scenario current_value last_evaluated_at Correct behavior
Card created, never in a game 0.0 NULL Excluded by default — correct
Card evaluated, legit 0 stats 0.0 timestamp Included — correct (use last_evaluated_at filter, not value)
Card evaluated, has stats > 0 timestamp Included — correct
Fully evolved card any timestamp Included — correct

Discord Bot Impact

The bot sends evaluated_only implicitly via default behavior — no bot-side changes are needed if we default to True. The bot's existing params (card_type, tier, season, progress, limit, offset) all compose cleanly with this new filter.


Implementation Notes

  • File: database/app/routers_v2/refractor.py, list_card_states() function (~line 103)
  • Add evaluated_only: bool = Query(default=True) to the function signature
  • Apply query = query.where(RefractorCardState.last_evaluated_at.is_null(False)) when evaluated_only=True
  • Update the docstring to document the new parameter
  • Add a test covering the evaluated_only=False path (ensures unevaluated cards appear when explicitly requested)
## Problem `GET /api/v2/refractor/cards?team_id=31` currently returns all `RefractorCardState` rows for a team — including cards that have `current_value = 0` and have never been evaluated against season stats. For team 31 this produces 2753 results when only ~14 cards have meaningful data. This happens because `RefractorCardState` rows are created eagerly (one row per card when it enters a roster), but `current_value` only becomes non-zero after `evaluate_card()` runs for that player. Cards that have never appeared in a game will have `current_value = 0.0` and `last_evaluated_at = NULL`. The `/refractor status` Discord command fetches this endpoint for every invocation and pages through the full result set. At 2753 rows with most being zero-value placeholders, the command is fetching and paginating over data that is meaningless to display. --- ## Design Recommendation ### Add an `evaluated_only` query parameter (default `True`) ``` GET /api/v2/refractor/cards?team_id=31&evaluated_only=true # default — excludes zero-value cards GET /api/v2/refractor/cards?team_id=31&evaluated_only=false # returns all rows including unevaluated ``` **Default to `True`** — the bot never needs to see unevaluated cards, and the admin/pipeline use case for seeing all rows is rare. A `False` opt-in is easier to reason about than a `True` opt-in. ### Filter predicate The cleanest signal for "has been evaluated" is **`last_evaluated_at IS NOT NULL`**, not `current_value > 0`. Rationale: - A card evaluated after a legitimately scoreless or stat-less performance (e.g. a pitcher who threw but walked everyone, or a batter who reached only via walk in a PA-less edge case) would have `current_value = 0.0` with `last_evaluated_at` populated. Filtering on `current_value > 0` would silently drop that card. - `last_evaluated_at IS NOT NULL` precisely tracks the state transition "this card has gone through at least one evaluation cycle" — which is the actual question the bot needs answered. In Peewee this is a one-line addition to the existing query: ```python if evaluated_only: query = query.where(RefractorCardState.last_evaluated_at.is_null(False)) ``` ### No schema change required This is a pure query-layer filter. No migration, no new index needed (the `team` index already covers the base query; the additional predicate adds negligible cost over a small evaluated set). --- ## Edge Cases | Scenario | `current_value` | `last_evaluated_at` | Correct behavior | |---|---|---|---| | Card created, never in a game | 0.0 | NULL | Excluded by default — correct | | Card evaluated, legit 0 stats | 0.0 | timestamp | **Included** — correct (use `last_evaluated_at` filter, not value) | | Card evaluated, has stats | > 0 | timestamp | Included — correct | | Fully evolved card | any | timestamp | Included — correct | --- ## Discord Bot Impact The bot sends `evaluated_only` implicitly via default behavior — no bot-side changes are needed if we default to `True`. The bot's existing params (`card_type`, `tier`, `season`, `progress`, `limit`, `offset`) all compose cleanly with this new filter. --- ## Implementation Notes - File: `database/app/routers_v2/refractor.py`, `list_card_states()` function (~line 103) - Add `evaluated_only: bool = Query(default=True)` to the function signature - Apply `query = query.where(RefractorCardState.last_evaluated_at.is_null(False))` when `evaluated_only=True` - Update the docstring to document the new parameter - Add a test covering the `evaluated_only=False` path (ensures unevaluated cards appear when explicitly requested)
cal added the
enhancement
label 2026-03-25 22:05:22 +00:00
Claude added the
ai-working
label 2026-03-25 22:31:09 +00:00
Claude removed the
ai-working
label 2026-03-25 22:33:22 +00:00
Collaborator

PR #175 opened: #175

Added evaluated_only: bool = Query(default=True) to list_card_states(). When true (default), applies WHERE last_evaluated_at IS NOT NULL — filters out the ~2739 placeholder rows created at pack-open time. Bot behaviour is unchanged (gets the filtered view by default); pass evaluated_only=false for admin/pipeline access to all rows. No migration needed.

PR #175 opened: https://git.manticorum.com/cal/paper-dynasty-database/pulls/175 Added `evaluated_only: bool = Query(default=True)` to `list_card_states()`. When true (default), applies `WHERE last_evaluated_at IS NOT NULL` — filters out the ~2739 placeholder rows created at pack-open time. Bot behaviour is unchanged (gets the filtered view by default); pass `evaluated_only=false` for admin/pipeline access to all rows. No migration needed.
Claude added the
ai-pr-opened
label 2026-03-25 22:33:28 +00:00
cal closed this issue 2026-03-25 22:53:08 +00:00
Sign in to join this conversation.
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: cal/paper-dynasty-database#174
No description provided.