The service now tracks the correct optionId for each question and
stores the answer key in the GameSessionStore after building the
session. The client response is unchanged — the store is invisible
to the outside.
- Build answerKey (questionId → correctOptionId) during question
assembly by finding the correct answer's position after shuffle
- Store the answer key via gameSessionStore.create() before returning
- Add excludeText parameter to getDistractors to prevent a distractor
from having identical text to the correct answer (different term,
same translation). Solved at the query level, not with post-filtering.
- Module-level InMemoryGameSessionStore singleton in the service
Add the session storage infrastructure for tracking correct answers
during a game. Designed for easy swap to Valkey in Phase 4.
- GameSessionStore interface with create/get/delete methods, all async
to match the eventual Valkey implementation
- InMemoryGameSessionStore backed by a Map
- GameSessionData holds only the answer key (questionId → correctOptionId)
- Also fix root build script to build packages in dependency order
Extend the game flow from raw term rows to a complete GameSession
matching the shared schema:
- Add getDistractors model query: fetches N same-POS, same-difficulty,
same-target-language terms excluding a given termId. Returns bare
strings since distractors only need their display text.
- Fix getGameTerms select clause to use the neutral field names
(sourceText, targetText, sourceGloss) that the type already declared.
- Rename gameService entry point to createGameSession; signature now
takes a GameRequest and returns a GameSession.
- Per question: fetch 3 distractors, combine with the correct answer,
shuffle (Fisher-Yates), assign optionIds 0-3 by post-shuffle index,
and assemble into a GameQuestion with a fresh UUID.
- Wrap the questions in a GameSession with its own UUID.
- Run per-question distractor fetches in parallel via Promise.all.
Known gap: the correct option is not yet remembered server-side, so
/game/answer cannot evaluate submissions. SessionStore lands next.
Add the shared schemas for the quiz request/response cycle, defining
the contract between the API and the frontend.
- Reorganise packages/shared: move schemas into a schemas/ subdirectory
grouped by domain. Delete the old flat schema.ts.
- Add AnswerOption, GameQuestion, GameSession, AnswerSubmission, and
AnswerResult alongside the existing GameRequest.
- optionId is an integer 0-3 (positional, shuffled at session-build
time so position carries no information).
- questionId and sessionId are UUIDs (globally unique, opaque, natural
keys for Valkey storage later).
- gloss is rather than optional, for a predictable
shape on the frontend.
- options array enforced to exactly 4 elements at the schema level.
- Add double join on translations for source/target languages
- Left join term_glosses for optional source-language glosses
- Filter difficulty on target side only (intentionally asymmetric:
a word's difficulty can differ between languages, and what matters
is the difficulty of the word being learned)
- Return neutral field names (sourceText, targetText, sourceGloss)
instead of quiz semantics; service layer maps to prompt/answer
- Tighten term_glosses unique constraint to (term_id, language_code)
to prevent the left join from multiplying question rows
- Add TODO for ORDER BY RANDOM() scaling post-MVP
- Add GameRequestSchema and derived types to packages/shared
- Add SupportedLanguageCode, SupportedPos, DifficultyLevel type exports
- Add getGameTerms() model to packages/db with pos/language/difficulty/limit filters
- Add prepareGameQuestions() service skeleton in apps/api
- Add createGame controller with Zod safeParse validation
- Wire POST /api/v1/game/start route
- Add scripts/gametest/test-game.ts for manual end-to-end testing
- add deck_terms to schema imports
- add addTermsToDeck — diffs source term IDs against existing deck_terms,
inserts only new ones, returns count of inserted terms
- add updateValidatedLanguages — recalculates and persists validated_languages
on every run so coverage stays accurate as translation data grows
- wire both functions into main with isNewDeck guard to avoid redundant
validated_languages update on deck creation
- add final summary report
- fix possible undefined on result[0] in createDeck
- tick off remaining roadmap items
Implements packages/db/src/seed.ts — reads all JSON files from
scripts/datafiles/, validates filenames against supported language
codes and POS, and upserts synsets into and
via onConflictDoNothing. Safe to re-run; produces 0 writes on
a duplicate run.
- terms, translations, term_glosses with cascade deletes and pos check constraint
- language_pairs with source/target language check constraints and no-self-pair guard
- users with openauth_sub as identity provider key
- decks and deck_terms with composite PK and position ordering
- indexes on all hot query paths (distractor generation, deck lookups, FK joins)
- SUPPORTED_POS and SUPPORTED_LANGUAGE_CODES as single source of truth in @glossa/shared
- Configure PostgreSQL 18 and Valkey 9.1 services
- Create multi-stage Dockerfiles for API and Web apps
- Set up pnpm workspace support in container builds
- Configure hot reload via volume mounts for both services
- Add healthchecks for service orchestration
- Support dev/production stage targets (tsx watch vs compiled)