Skip to content

Queue redesign: unified question feed + attribute chips - #21

Merged
WilfordGrimley merged 5 commits into
masterfrom
worktree-queue-question-feed
Jul 15, 2026
Merged

Queue redesign: unified question feed + attribute chips#21
WilfordGrimley merged 5 commits into
masterfrom
worktree-queue-question-feed

Conversation

@WilfordGrimley

Copy link
Copy Markdown

Summary

Replaces the printing/artist/tag/moderation tab switcher with a single
unified question feed (GET 2/questionFeed/), one question at a time,
typed per a 4-tier ranked union (AI-suggested printing → contested →
moderation → fresh unresolved). Adds tri-state attribute chips with
confidence-fill and exclusion groups, live candidate filtering, no-match
gating, and auto-tag-on-selection. Full design writeup in
docs/features/printing-tags.md's new Stage 7 section.

Companion piece, explicitly out of scope here: backend derivation of
canonical attributes from resolved printings (flagged, not attempted).
ML/scoring schedulers beyond the ranked union, federation, and voteQueue/
consumers are also untouched, per the original brief.

Deliverables (held for review)

Layout

Starburst/card panel LEFT, candidate grid RIGHT on desktop — flipped from
the original brief's candidates-left/card-right per direct follow-up
instruction. Mobile stacks card+chips first, grid second, in plain DOM
order (no CSS order trick).

Screenshots attached to this PR conversation (desktop 1280px, mobile 375px,
plus the tag and moderation question types).

Chip taxonomy proposal + data grounding

Queried live production data before proposing anything (see Stage 7 for
the full census). 11 chips total:

  • Standalone toggles: Full Art, Borderless, Showcase, Extended Art,
    Etched — the three frame_effects values common enough (849–4165
    occurrences) to read as a distinct visual treatment; legendary/
    inverted had higher raw counts but were excluded as a judgment call
    (card-type marker / one narrow product line, not a printing-variant
    signal) — flagged as a call worth a second look, not asserted as
    data-forced.
  • Border Color (exclusion group): Black / White / Silver — gold/yellow
    omitted (small, ambiguous with Showcase overlap).
  • Frame Style (exclusion group): Old / Modern / Future — bucketing
    Scryfall's four raw frame years into three, since finer distinctions are
    hard to reliably eyeball.
  • promo_types excluded entirely — mostly production/marketing
    provenance, not something a viewer can identify by looking at a card
    image.

Six of the eleven tags need seeding before this ships:
manage.py seed_attribute_tags (idempotent, same pattern as
seed_sensitive_tags) — not yet run in production, part of this PR's
own deploy step.

questionFeed API shape

GET 2/questionFeed/?anonymousId=<id>

{
  item: {
    type: "confirm_suggestion" | "identify_printing" | "artist" | "tag" | "moderation",
    card: Card,
    // per-type payload fields:
    suggestedPrinting?: PrintingCandidate,   // confirm_suggestion
    candidates?: PrintingCandidate[],        // confirm_suggestion, identify_printing
    tagConfidence?: Record<string, number>,  // confirm_suggestion, identify_printing (netPolarity per attribute tag)
    confidentlyKnownArtistName?: string|null,// artist
    tagName?: string,                        // tag, moderation
    reportCount?: number,                    // moderation
    reportExcerpts?: string[],               // moderation
  } | null,  // null = fully caught up
  remainingEstimate: number,
}

Own-vote exclusion at (card, tag) granularity (not just card) so a
voter's own prior vote on one attribute doesn't hide a different,
still-open attribute on the same card.

Starvation risk, stated concretely: tier 1 alone is 28,112 cards (the
full AI deductive-backfill set). At current volume, a voter working only
this feed will not see a single contested or moderation item until all
28,112 confirm_suggestion questions are exhausted. Implemented per spec
("dumb ranked union" v1) anyway; an interleaved/weighted union is the
likely v2 fix and the first item on the follow-up list below.

Retraction-support finding

CardTagVote only ever supported apply/not-applicable — no delete path,
so the chip's untouched-cycle-back had nothing to call. Minimal addition:
post_submit_tag_vote now accepts polarity=0 as a retract sentinel
(never persisted — the two real VotePolarity choices are unchanged),
deleting the existing row instead of upserting.

A bug this stage found, not introduced

CardPanel's z-index: -1 (unchanged since the original
PrintingTagQueue.tsx, needed so the starburst bleed doesn't cover the
page heading) was never actually contained to its own column. Harmless
as long as nothing inside CardPanel needed to be clicked — true until
this stage's attribute chips. The escape made the whole panel unclickable
at the browser's hit-testing layer; fixed by giving the wrapping Col its
own local stacking context (position: relative + an explicit non-auto
z-indexposition: relative alone doesn't establish one). Full story
in Stage 7.

Server follow-up checklist

  • Run manage.py seed_attribute_tags (six new chip tags 400 without it)
  • Live tier-1 volume against real feed usage — 28,112 confirmed via
    direct query; whether it actually swamps tiers 2-4 in practice
    needs live traffic, not just the raw candidate-set size
  • Verify netPolarity's optimistic client update never visibly
    diverges from the server's real value (not linear in vote count once
    AI/admin weights are involved — can't fully check against MSW mocks)
  • Revisit the Border Color / frame_effects v1 chip-set omissions with
    real moderator/voter feedback
  • Starburst visual bleed is more dominant in the new narrower card
    column than in the original layout — cosmetic polish, not a
    functional bug (verified clickable/functional under Playwright)
  • No mobile-viewport-specific functional test coverage — Playwright
    specs all run at the default 800×600 desktop viewport; only a
    static 375px screenshot was captured, not verified interaction

Test plan

  • Backend: cardpicker/tests/test_question_feed.py (18 tests: tier
    ordering, own-vote exclusion incl. the (card,tag)-scoping regression
    case, moderator gating), test_tag_votes.py (retraction,
    get_tag_net_polarity), test_attribute_tags.py,
    test_printing_tags_views.py (extended PrintingCandidate fields)
  • Full backend suite: 573 passed (only the 4 known pre-existing
    network-dependent baseline failures)
  • Frontend unit (Jest): attributeChips.test.ts,
    AttributeChipPanel.test.tsx (tri-state cycle, no-sibling-votes),
    QuestionFeed.test.tsx (no-match gating, auto-tag payloads)
  • Frontend Playwright: one flow per question type
    (QuestionFeedConfirmSuggestion, QuestionFeedArtistAndTag,
    NoMatchReasonStrip rewritten for the new gating flow,
    ModerationQueue rewritten for the unified feed) — full suite:
    164 passed, 0 failed

🤖 Generated with Claude Code

https://claude.ai/code/session_016i9S7LQsCL3FGaih3ZTRBJ

WilfordGrimley and others added 2 commits July 15, 2026 00:23
…etraction, confidence

New GET 2/questionFeed/ (cardpicker/question_feed.py) - unified single-question
feed replacing the printing/artist/tag tab split, ranked union across four
tiers (AI-suggested printing, contested, moderation, fresh unresolved), with
per-voter own-vote exclusion at (card, tag) granularity.

Supporting pieces: CardTagVote retraction via a polarity=0 sentinel (was
apply/not_applicable only), a weighted net-polarity scalar on
TagConsensusEntry for chip confidence fill, PrintingCandidate extended with
borderColor/isShowcase/isExtendedArt/isEtched, and the new attribute-chip tag
taxonomy (cardpicker/attribute_tags.py + seed_attribute_tags command).

Design writeup: journal/2026-07-14-queue-question-feed-design.md (gitignored,
local only).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016i9S7LQsCL3FGaih3ZTRBJ
Replaces the printing/artist/tag/moderation tab switcher (PrintingTagQueue,
GenericVoteQueue, ModerationQueue, PrintingConfirmStrip - all deleted) with
QuestionFeed.tsx, driven by 2/questionFeed/. Sticky starburst/reveal/
candidate-grid mechanics extracted verbatim into cardPanel.tsx and reused
directly.

New attribute-chip panel (tri-state, confidence-fill, exclusion groups) with
live candidate filtering and no-match gating. Selecting a candidate auto-casts
positive tag votes for its own standalone attributes, making PrintingConfirmStrip
redundant. Route renamed printingQueue -> whatsthat (matches the page's own
"What's That Card?" branding; every other route is a single word).

Fixed a latent CardPanel z-index: -1 bug this stage's chips exposed - its
negative z-index was never contained to its own column, silently making
its whole subtree unclickable at the hit-testing layer the moment it hosted
real interactive content for the first time. Fixed via a locally-contained
stacking context (position: relative + explicit z-index on the wrapping Col).

Full design writeup: docs/features/printing-tags.md's new Stage 7 section.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016i9S7LQsCL3FGaih3ZTRBJ
WilfordGrimley and others added 2 commits July 15, 2026 00:28
Uncontained negative z-index on a sticky element silently makes its whole
subtree unclickable once it hosts interactive content; a call-count-based
MSW mock breaks under React 18 Strict Mode's dev-time double-invoke.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016i9S7LQsCL3FGaih3ZTRBJ
…chip ring

Card panel + candidate grid columns swapped back (candidates left, starburst
anchored right) and intro text right-aligned to match. The starburst is now
contained to the card's own box (moved inside AttributeChipPanel's card slot,
sized off CardArea instead of the whole ring+chips panel) rather than
bleeding across the full page and covering the heading/candidate grid.

AttributeChipPanel restructured to a CSS grid ring (standalone chips top,
the two exclusion groups left/right, card dead center) instead of stacking
chips above the card image.

Also: tier 4 now prioritizes a card with one AI vote + one agreeing human
vote (one vote from crossing PRINTING_TAG_MIN_VOTES=2) over a totally fresh
zero-vote card - a small, concrete step toward "closest to resolving first"
without building a full scoring system.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016i9S7LQsCL3FGaih3ZTRBJ
@WilfordGrimley

Copy link
Copy Markdown
Author

Layout rework per review feedback

  • Starburst/card panel anchored right (card centered, all 11 chips forming a ring around it - top row for standalone toggles, left/right columns for the two exclusion groups); candidate grid left; intro text right-aligned to match.
  • Fixed the actual root cause of the earlier bleed complaint: the starburst was sized/centered against the whole card+chips panel (much wider than the card itself). Moved it inside the card's own box specifically - it's now contained and doesn't compete with the heading, instructions, or candidate grid for readability at any viewport tested (1280px desktop, 375px mobile).
  • Small, concrete "closest to resolving first" improvement: tier 4 now prioritizes a card that already has one AI vote + one agreeing human vote (one vote short of PRINTING_TAG_MIN_VOTES=2) over a totally fresh zero-vote card, rather than treating them identically. New regression test covers it.

Full suites re-verified after these changes: backend 574 passed (same 4 known pre-existing baseline failures), frontend Jest all green, full Playwright suite 164 passed, 0 failed.

Screenshots sent directly to the reviewer (desktop + 375px mobile).

Still open from the review thread, not yet actioned (flagged for a decision, not silently built or dropped):

  • Border/frame → persisted+resolved models: technically straightforward (same resolve_and_persist_* pattern as printing/artist), currently only client-side filtering. This is the "backend derivation of canonical attributes from resolved printings" companion piece already called out as out-of-scope for this PR.
  • atag:external-ip: confirmed real (Scryfall's separate art-tags system, distinct from the bulk-data fields this PR already uses) - bringing it in means a new data source/import, not a field addition to the current census.
  • Export-popularity tally for queue priority: technically sound (doesn't affect resolution quality, only ordering), but there's currently zero export-time backend call anywhere in the app to hang a tally on (XML/PDF export are fully client-side) - real new infrastructure, and worth a deliberate call given the zero-first-party-telemetry policy this fork otherwise holds to.
  • Deductive backfill (Stage 6) extensions: two additional deduction tiers look feasible from a fresh read of deductive_backfill.py - collector-number-narrowed D3, and extending D1/D2 to non-English cards via Scryfall's translated names (currently a total blind spot). Not started.

🤖 Generated with Claude Code

Current composition was hand-tuned via iterative screenshot review, not
built against a real design system.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016i9S7LQsCL3FGaih3ZTRBJ
@WilfordGrimley
WilfordGrimley marked this pull request as ready for review July 15, 2026 10:03
@WilfordGrimley

Copy link
Copy Markdown
Author

Marking ready for review — all requested layout fixes are in (starburst anchored/contained, card centered in the chip ring, right-aligned text) and the follow-up questions from review are answered in the earlier comment.

Backend tests CI (red X): confirmed this is the pre-existing baseline, not a regression — moxfield/moxfield_without_www and the test_sources.py snapshot/upsert tests fail identically on master today, all due to external network calls (Google Drive, Scryfall) being unreachable from the CI sandbox. Same 4 failures, same error signatures, unrelated to this branch's changes.

Ready to merge whenever you want to sequence it in.

@WilfordGrimley
WilfordGrimley merged commit fd6ea52 into master Jul 15, 2026
6 of 7 checks passed
WilfordGrimley added a commit that referenced this pull request Jul 18, 2026
Real-device breakage (labels/buttons banding across the card art,
large blank reserved regions) persisted after PR #55 because that fix
only addressed Level 1 - Level 2, the funnel's dominant default
screen, kept the identical sticky-plus-negative-z-index CardPanel
unchanged. CardPanel is now position: static below the md breakpoint
(768px), sticky only at md+, where desktop's side-by-side layout
genuinely benefits from it and never exhibited the bug.

Second, independent mechanism found in the same pass: AttributeChipPanel's
ChipRing (PR #21-era, reachable via Level 2's opt-in "Filter by
attribute" disclosure) had no responsive behavior - its flanking
left/right chip columns were always auto-sized to their own content
while the card's own column was the only flexible one, squeezing the
card narrower at mobile widths. Below sm (576px) the ring now
collapses to a single vertical stack instead of forming a ring.

Decision rule applied throughout: the funnel's document-flow design
wins wherever it conflicts with chip-ring-era positioning on mobile;
chip-ring visuals (starburst, ring arrangement) survive only where
they stay contained inside the card's own layout box.


Claude-Session: https://claude.ai/code/session_014dB7mGPQyoML2Ri5ZrprdK

Co-authored-by: Claude <noreply@anthropic.com>
@WilfordGrimley
WilfordGrimley deleted the worktree-queue-question-feed branch July 19, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant