Recommendation Experience Roadmap
Purpose
PopChoice started as a course-sized recommendation engine with a small embedded movie catalog and a guided quiz. That is useful for proving the end-to-end flow, but it is not enough for a real recommendation app. The next product direction is to make recommendations feel more adaptive, more movie-aware, and less dependent on a fixed local seed list.
The guiding shift is:
- from a static quiz to reusable taste signals
- from a small embedded catalog to TMDB-first discovery and enrichment
- from "answer questions, get one result" to an app that learns what the user has seen, liked, disliked, and wants tonight
This should be done in stages. A full rewrite is not the goal.
Backlog Hygiene
- Create a GitHub issue for each actionable roadmap ticket before or alongside adding it here.
- If the work is too large for one PR, keep the original issue as an epic/umbrella and split focused implementation issues underneath it.
- Prefer linked roadmap entries so completed work can be checked off without losing context.
Current Quiz Flow
The current quiz captures:
- solo or group mode
- group participant names for group mode
- favorite or reference movie
- preferred era: newer, classic, or both
- broad mood/genre labels
- tone: light, balanced, serious, or dark
- optional favorite actor
This creates a usable first recommendation, but the data is coarse. It asks users to describe taste abstractly, while users often know their taste better through concrete reactions to movies.
Current Limitations
- The favorite movie prompt is high friction and can over-anchor the result toward movies the user already watched.
- Mood options are partly genre labels, which makes them too broad for accurate ranking.
- The quiz does not capture enough negative intent, such as "not slow", "not horror", "not long", "not subtitles", or "not something obvious".
- The app has started using signed-in movie memory for exclusions, down-ranking, and exact liked-candidate boosts, but watched/liked/not-interested signals are still not unified behind a first-class recommendation signal model.
- Group mode currently collects individual preferences, but the recommendation model should eventually optimize for overlap and compromise explicitly.
- The embedded/local movie catalog is too small to produce consistently satisfying real-world results.
Recent Product QA Notes
- #680 replaced user-facing raw match percentages with calibrated confidence tiers. The exact experimental percentage remains available in hover/accessibility copy for debugging, but the primary UI should not imply false precision.
- #681 reordered quiz entry into three layers: match depth first (
Fast PickvsNormal Match), then audience (Solo,Duo,Group), then the corresponding short or normal question flow. - Normal Match is producing promising qualitative results for specific intent prompts. A Russian dystopian/post-apocalyptic survival prompt returned a strong
Blade Runner-style selection and explanation. - Duo and Group need more manual product QA after the entry-flow ordering is fixed, especially to verify whether compromise results feel meaningfully different from solo recommendations.
0.2.0 Better Taste Control
The Better Taste Control release is tracked by #826. It makes PopChoice more controllable without turning the quiz into a technical filter form. Users can say what they want, what they want to avoid, and how safe or surprising the pick should feel.
Completed scope:
- #827: add explicit avoids and constraints to Fast Pick and Normal Match.
- #828: make reference movie input optional and lower-friction.
- #829: add discovery appetite control for safe, balanced, and surprising recommendations.
- #830: improve the result feedback loop so feedback can guide follow-up recommendations.
- #831: expand deterministic recommendation eval coverage for the new taste-control signals.
- #832: keep product docs and release notes aligned as behavior lands.
Release notes: /docs/releases/v0.2.0.
Out of scope for v0.2.0:
- Taste Swipe MVP.
- Multi-device group rooms.
- A full canonical
TasteSignal[]backend rewrite.
Target Model: Taste Signals
Both quiz answers and swipe interactions should eventually map into a shared signal model.
Example signal types:
type TasteSignal =
| { type: 'liked_movie'; tmdbId?: number; title: string; year?: number; weight: number }
| { type: 'seen_movie'; tmdbId?: number; title: string; year?: number; weight: number }
| { type: 'not_interested_movie'; tmdbId?: number; title: string; year?: number; weight: number }
| { type: 'wrong_mood_movie'; tmdbId?: number; title: string; year?: number; weight: number }
| { type: 'desired_trait'; value: string; weight: number }
| { type: 'avoid_trait'; value: string; weight: number }
| { type: 'constraint'; value: string; weight: number };The important point is that the recommendation engine should not care whether a signal came from the quiz, a swipe card, account history, or feedback on a past result.
TMDB-First Catalog Direction
The small local movie database should become a cache and enrichment layer, not the primary catalog.
Target behavior:
- Use TMDB as the broad source of movie candidates.
- Keep local Postgres for:
- embeddings and generated descriptions
- normalized TMDB identity
- recommendation history
- user memory and feedback
- cached metadata for performance and consistency
- Expand local records just in time when TMDB returns promising candidates.
- Prefer stable TMDB ids for movie identity whenever available.
- Keep fallback identity matching by normalized title and release year for older seed records.
- Log ambiguous TMDB/local matches for future manual review and admin tooling.
- Add cast, directors, genres, and keywords to the catalog core before promising actor/director/genre search in the available-movies UI.
This avoids pretending that a few hundred embedded titles can power a real app, while still preserving the value of local ranking, memory, and generated explanations.
Candidate Source Strategy
Recommendation V2 should treat candidate source as a separate decision from audience size and match depth.
| Strategy | Primary use | Candidate mix |
|---|---|---|
| Curated showcase | Demo/showcase surfaces and safe regression fixtures. | curated local seed records only. |
| Hybrid fast | Fast Pick when latency matters. | local-cache first, then bounded tmdb-discover if local quality is low. |
| Memory-aware local | Signed-in fast/normal recommendations with history. | local-cache plus memory exclusions/down-ranks. |
| TMDB-first normal | Normal Match and later Taste Swipe. | tmdb-discover/tmdb-search, enriched by local cache and JIT seeding. |
| Compromise group/duo | Duo and group results where overlap matters. | Same source mix as selected depth, but scored against participant overlap. |
#612 tracks carrying these source decisions through code, persistence, logs, eval reports, and optional UI badges.
The current code now has a first policy module, persisted experienceMode, and route/job/pipeline metadata bridge for these defaults:
- Curated showcase ->
curated-showcase. - Fast Pick solo ->
hybrid-fast. - Normal Match and Taste Swipe solo ->
tmdb-first. - Duo and Group ->
compromise-hybrid, regardless of fast/normal entry point. - Signed-in solo Fast Pick with existing memory can temporarily use
memory-aware-localuntil TMDB-first memory reranking is implemented.
Experience Modes
1. Fast Pick
Short guided mode for users who want a result fast. The current flow asks for audience, intent, hard avoids, and discovery appetite, then sends experienceMode: fast-pick through the recommendation API.
Target questions:
- What kind of night is this: easy, funny, gripping, emotional, weird, cozy, dark?
- What should PopChoice avoid: horror, gore, slow pacing, subtitles, long runtime, already-seen movies?
- Discovery level: safe hit, balanced, surprise me?
Duo/group Fast Pick reuses the same short question set per participant after audience-specific setup, then runs the same hybrid-fast source strategy.
2. Taste Swipe
Tinder-style mode for movie-heavy users who do not want questions.
Show a stream of movie cards and collect simple reactions:
- Seen
- Loved it
- Not for me
- Maybe tonight
- Skip
After enough signal, usually 8-12 cards, the user can ask for a recommendation. For users who have watched many films, "seen" is useful data, not a failed recommendation.
Card sourcing should start broad:
- popular classics
- recent popular movies
- genre landmarks
- foreign/art-house picks
- niche or polarizing titles
- titles near prior likes
- titles far away from prior dislikes
3. Deep Match
Advanced mode for users who enjoy a more deliberate quiz.
This can keep parts of the current flow, but favorite/reference movie should become optional. It should also capture "why" and "what not to recommend" more explicitly.
4. Group Swipe
Group mode should eventually become more concrete:
- pass-the-phone swipe rounds, or
- shared vote cards where each person reacts to the same candidates
The model should optimize for overlap and acceptable compromise, not only average preferences.
5. Group Rooms
Room-backed group mode should become the larger milestone behind #359, separate from the current same-device group flow.
Implementation should be sequenced as:
- #467: room persistence, TTL, cleanup, and participant storage.
- #468: share links, participant join flow, and readiness state.
- #469: recommendation orchestration from completed room answers.
- #470: QR invite and projector mode.
This keeps the high-risk data and orchestration work ahead of visual polish. The existing same-device group mode should remain available until room mode is reliable.
Staged Implementation Plan
Stage 1: Document and stabilize current behavior
- Keep the existing quiz and recommendation flow working.
- Document the signal-model direction.
- Make sure current feedback and movie memory continue to avoid obvious repeats.
- Continue using the local catalog, but treat it as a cache plus ranking support.
Stage 2: Improve the guided quiz
- Replace broad genre wording with more concrete "tonight" language.
- Add explicit avoid/constraint choices.
- Make favorite/reference movie optional.
- Keep the API compatible by translating new answers into current recommendation input.
- Improve group-mode copy so users understand whether PopChoice is balancing or optimizing for one person.
Stage 3: Taste Swipe MVP
- Add an entry choice: "Answer a few questions" or "Swipe movies".
- Build a swipe screen backed by TMDB and local cached metadata.
- Store signed-in reactions in movie memory.
- For anonymous users, keep session-local reactions long enough to produce one recommendation.
- Translate swipe reactions into current recommendation inputs before introducing a deeper backend rewrite.
Stage 4: Signal-based recommendation backend
- Add a canonical recommendation request shape based on
TasteSignal[]. - Convert quiz answers, swipe reactions, and account memory into signals.
- First slice:
TasteSignalv1 now maps quiz answers plus feedback/movie-memory rows into shared signal types, and existing candidate filtering consumes feedback-derived movie signals through that adapter. - Update ranking to use positive signals, negative signals, constraints, and TMDB candidate expansion together.
- Keep generated explanations aware of which signals actually existed, so copy does not mention actors, genres, or constraints the user never provided.
Stage 5: TMDB-first candidate generation
- Move candidate sourcing toward TMDB discover/search as the broad first pass.
- Use local embeddings as enrichment and reranking, not as the complete universe of possible movies.
- Add JIT embedding/enrichment for strong TMDB candidates.
- Move TMDB discovery and backfill into rate-limited BullMQ catalog workers in #492 before increasing catalog expansion volume:
- Add a dedicated catalog-maintenance queue for discovery pages, movie detail enrichment, metadata refresh, and per-movie backfill jobs.
- Enforce one shared TMDB request budget across discovery, backfill, and worker-driven enrichment, with configurable concurrency and
429backoff. - Use deterministic BullMQ-safe
jobIdvalues such astmdb-details-{tmdbId}-{language}andbackfill-{movieId}so retries and duplicate triggers do not fan out duplicate TMDB calls. - Keep the existing one-shot services as enqueue/maintenance entrypoints while workers own API pacing, retries, observability, and Bull Board visibility.
- Add catalog metadata prerequisites for richer search:
- #471 schema/model for cast, directors, genres, and keywords.
- #472 TMDB backfill and refresh for that metadata.
- #473 available-movies partial
ILIKEsearch over titles, actors, directors, and genres, combined with exact/ranged catalog filters. - Metadata v1 quality contract: hot identity/quality/language columns, normalized watch providers for
US,FI, andRU, bounded TMDB details enrichment for top direct TMDB candidates, and catalog-health/eval visibility for low-quality metadata.
- Track TMDB API failures, timeout behavior, and fallback quality.
- Keep #493 as the epic for admin/back-office review of ambiguous title matches and catalog-health issues.
Stage 5.5: Deterministic e2e and eval foundations
- #474: add a full Playwright e2e harness with an isolated migrated test database.
- #475: add product smoke flows for auth, catalog, quiz, recommendation, and feedback.
- Extend recommendation browser smoke coverage across current Normal/Fast and Solo/Duo/Group entry paths in
apps/web/e2e/recommendation.spec.ts. - #476: add recommendation eval fixtures and scoring so AI behavior can be changed with more control.
- #490: add scheduled or manually triggered real-data recommendation evals for seeded DB and catalog-retrieval changes. The backoffice can now queue mock and real-data evals, persist run summaries/results, and expose guarded live eval enqueueing for explicit operator checks.
- Keep live-model evals optional. The default path should be deterministic, cheap, and safe for CI.
Stage 5.6: Backoffice eval operations
The eval stack should be layered instead of treated as one overloaded "real data" command:
| Eval layer | Purpose | Where it runs |
|---|---|---|
| Deterministic fixture eval | Prompt/output shape, repeat avoidance, explanation quality, and basic scenario scoring without external services. | Per-PR CI. |
| Seeded catalog retrieval eval | Migrations, seeded catalog connectivity, fixture candidate availability, and catalog search. | Scheduled/manual GitHub Actions. |
| Environment retrieval/source eval | Current environment DB health for recommendation scenarios: candidate counts, source distribution, metadata quality/provider gaps, and hard-constraint feasibility. | Backoffice-triggered BullMQ job. |
| Live provider eval | Full provider-sensitive check with OpenAI/TMDB calls for intentional pre-launch validation. | Manual, guarded, audited backoffice action only. |
Implementation should be split in this order:
- #618: classify and refactor the current seeded
--real-datachecks so the runner is importable by jobs and docs do not imply it is a full live recommendation eval. - #616: persist eval run and result history so reports are not trapped in CLI logs or GitHub artifacts.
- #617: add bounded BullMQ jobs for environment retrieval and source-strategy evals against the configured DB.
- #619: add a protected backoffice UI for safe eval runs, history, status, and report details.
- #620: add a guarded live-provider eval action only after safe non-live backoffice evals exist.
Stage 6: Long-term personalization
- Add an editable watched list and liked/not-interested memory.
- Add "rewatch mode" so watched movies can appear only when intentionally requested.
- Use feedback to learn the user's taste profile over time.
- Consider gamified taste history, achievements, and "taste map" views only after the core memory model is reliable.
Near-Term PR Candidates
Good next PRs, in order:
- #484: refactor the quiz submit/results handoff so navigation state is explicit and the quiz page does not need short-lived reset guards.
- #492: move TMDB discovery/backfill/enrichment into a shared rate-limited BullMQ catalog worker before scaling catalog volume.
- #618: classify/refactor current seeded real-data checks before adding backoffice eval execution.
- #616: persist eval run/result history for backoffice and worker reports.
- #617: add non-live environment retrieval/source-strategy eval jobs.
- #619: expose recommendation eval runs in backoffice.
- Replace the current quiz copy and options with a more "tonight" oriented flow while preserving existing API shape.
- Add a small taste-swipe prototype behind a feature flag or alternate quiz entry path.
- Add TMDB-backed candidate-card sourcing for swipe mode.
- Continue the
TasteSignaldomain model by adding swipe reaction adapters after the v1 quiz plus feedback/movie-memory slice. - #620: add guarded live-provider evals after safe backoffice evals exist.
- Start #612 with first-class candidate source provenance, source-strategy policy, route/job/pipeline metadata, and eval assertions for curated showcase, hybrid fast, and TMDB-first behavior.
- Connect #612 source strategy to initial retrieval behavior:
hybrid-fastandcompromise-hybriduse bounded TMDB fallback, while curated/local-only strategies block external lookup. - Add
experienceModerequest/job/pipeline metadata sofast-pickcan selecthybrid-fastwhile current requests default tonormal-match. - Add a first Fast Pick quiz intro entry that sends the
fast-pickwrapper into the existing recommendation API. - Add the first short solo Fast Pick guided flow for intent, hard avoids, and discovery appetite under #609.
- Extend Fast Pick to Duo/Group by adding an audience layer before the short question flow.
- Start #608 by adding an optional Normal-mode hard-avoids step and carrying those negative signals into the current recommendation payload.
- Make Duo first-class in the guided UI: separate Normal/Fast audience entry, two-person setup copy, Duo result copy, and deterministic e2e coverage.
- Connect the first
tmdb-firstretrieval slice: Normal solo now attempts TMDB discover as a primary candidate-discovery path, then score-ranks TMDB and strong local matches together. - Deepen the first
tmdb-firstquery-shaping slice: Normal/Fast hard avoids and discovery appetite now shape TMDB discover params, and eval fixtures include source/metadata quality thresholds. - Deepen
tmdb-firstJIT enrichment before making it the Normal quality default: fetch richer TMDB details for strong direct candidates, persist/cache useful runtime/rating/provider metadata, and calibrate backoffice real-data thresholds. - #655: add provider-aware result UI and user-facing availability copy once product behavior decides whether availability is informational, a soft preference, or a hard constraint.
- #656: evaluate a movie embedding text v2 contract before adding metadata such as genres, keywords, cast, director, language, popularity, or certification to retrieval embeddings.
- #680: calibrate recommendation match percentages and user-facing score copy before treating raw percentages as precise quality claims.
- #681: rework the quiz entry flow so users choose match depth first, then Solo/Duo/Group audience, then answer the matching short or normal question set.
- Add manual-review logging for ambiguous TMDB/local identity matches.
Non-Goals For Now
- No full recommendation rewrite in one PR.
- No removal of the current quiz until swipe mode proves useful.
- No hard dependency on user accounts for the first swipe MVP.
- No admin panel before the app has enough ambiguous-match volume to justify it.
- No attempt to make the local embedded catalog the complete movie universe.