From dd6c2b0118c9df2143a029bcecb2a4d4aa473e8f Mon Sep 17 00:00:00 2001 From: lila Date: Sat, 11 Apr 2026 21:32:13 +0200 Subject: [PATCH] updating documentation --- documentation/api-development.md | 52 +++++++++++++++++++++----------- documentation/roadmap.md | 8 ++--- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/documentation/api-development.md b/documentation/api-development.md index a07f741..23ed459 100644 --- a/documentation/api-development.md +++ b/documentation/api-development.md @@ -83,6 +83,9 @@ touches the database. A service never reads `req.body`. A model never knows what model. Documented coupling acknowledged with a comment. - Problem 7: Gloss join could multiply question rows. Schema allowed multiple glosses per term per language, so the left join would duplicate rows. Fixed by tightening the unique constraint. - Problem 8: Model leaked quiz semantics. Return fields were named prompt / answer, baking HTTP-layer concepts into the database layer. Renamed to neutral field names. +- Problem 9: AnswerResult wasn't self-contained. Frontend needed selectedOptionId to render feedback but the schema didn't include it (reasoning was "client already knows"). Discovered during frontend work; added the field. +- Problem 10: Distractor could duplicate the correct answer text. Different terms can share the same translation. Fixed with ne(translations.text, excludeText) in the query. +- Problem 11: TypeScript strict mode flagged Fisher-Yates shuffle array access. noUncheckedIndexedAccess treats result[i] as T | undefined. Fixed with non-null assertion and temp variable pattern. --- @@ -116,6 +119,15 @@ touches the database. A service never reads `req.body`. A model never knows what - One gloss per term per language. The unique constraint on term_glosses was tightened from (term_id, language_code, text) to (term_id, language_code) to prevent the left join from multiplying question rows. Revisit if multiple glosses per language are ever needed (e.g. register or domain variants). - Model returns neutral field names, not quiz semantics. getGameTerms returns sourceText / targetText / sourceGloss rather than prompt / answer / gloss. Quiz semantics are applied in the service layer. Keeps the model reusable for non-quiz features. - Asymmetric difficulty filter. Difficulty is filtered on the target (answer) side only. A word can be A2 in Italian but B1 in English, and what matters is the difficulty of the word being learned. +- optionId as integer 0-3, not UUID. Options only need uniqueness within a single question; cheating prevented by shuffling, not opaque IDs. +- questionId and sessionId as UUIDs. Globally unique, opaque, natural Valkey keys when storage moves later. +- gloss is string | null rather than optional, for predictable shape on the frontend. +- GameSessionStore stores only the answer key (questionId → correctOptionId). Minimal payload for easy Valkey migration. +- All GameSessionStore methods are async even for the in-memory implementation, so the service layer is already written for Valkey. +- Distractors fetched per-question (N+1 queries). Correct shape for the problem; 10 queries on local Postgres is negligible latency. +- No fallback logic for insufficient distractors. Data volumes are sufficient; strict query throws if something is genuinely broken. +- Distractor query excludes both the correct term ID and the correct answer text, preventing duplicate options from different terms with the same translation. +- Submit-before-send flow on frontend: user selects, then confirms. Prevents misclicks. --- @@ -157,7 +169,7 @@ as a potential constraint for the distractor algorithm at high difficulty. ## API Schemas (packages/shared) -### `GameRequestSchema` (implemented) +### `GameRequestSchema` ```typescript { @@ -169,14 +181,11 @@ as a potential constraint for the distractor algorithm at high difficulty. } ``` -### Planned schemas (not yet implemented) - -```text -QuizQuestion — prompt, optional gloss, 4 options (no correct answer) -QuizOption — optionId + text -AnswerSubmission — questionId + selectedOptionId -AnswerResult — correct boolean, correctOptionId, selectedOptionId -``` +AnswerOption: { optionId: number (0-3), text: string } +GameQuestion: { questionId: uuid, prompt: string, gloss: string | null, options: AnswerOption[4] } +GameSession: { sessionId: uuid, questions: GameQuestion[] } +AnswerSubmission: { sessionId: uuid, questionId: uuid, selectedOptionId: number (0-3) } +AnswerResult: { questionId: uuid, isCorrect: boolean, correctOptionId: number (0-3), selectedOptionId: number (0-3) } --- @@ -236,27 +245,36 @@ packages/db/src/ - [x] Gloss left join implemented - [x] Model return type uses neutral field names (sourceText, targetText, sourceGloss) - [x] Schema: gloss unique constraint tightened to one gloss per term per language +- [x] Zod schemas defined: AnswerOption, GameQuestion, GameSession, AnswerSubmission, AnswerResult +- [x] getDistractors model implemented with POS/difficulty/language/excludeTermId/excludeText filters +- [x] createGameSession service: fetches terms, fetches distractors per question, shuffles options, stores session, returns GameSession +- [x] evaluateAnswer service: looks up session, compares submitted optionId to stored correct answer, returns AnswerResult +- [x] GameSessionStore interface + InMemoryGameSessionStore (Map-backed, swappable to Valkey) +- [x] POST /api/v1/game/answer endpoint wired (route, controller, service) +- [x] selectedOptionId added to AnswerResult (discovered during frontend work) +- [x] Minimal frontend: /play route with settings UI, QuestionCard, OptionButton, ScoreScreen +- [x] Vite proxy configured for dev --- ## Roadmap Ahead -### Step 1 — Learn SQL fundamentals (in progress) +### Step 1 — Learn SQL fundamentals - done Concepts needed: SELECT, FROM, JOIN, WHERE, LIMIT. Resources: sqlzoo.net or Khan Academy SQL section. Required before: implementing the double join for source language prompt. -### Step 2 — Complete the model layer +### Step 2 — Complete the model layer - done - Double join on `translations` — once for source language (prompt), once for target language (answer) - `GlossModel.getGloss(termId, languageCode)` — fetch gloss if available -### Step 3 — Define remaining Zod schemas +### Step 3 — Define remaining Zod schemas - done - `QuizQuestion`, `QuizOption`, `AnswerSubmission`, `AnswerResult` in `packages/shared` -### Step 4 — Complete the service layer +### Step 4 — Complete the service layer - done - `QuizService.buildSession()` — assemble raw rows into `QuizQuestion[]` - Generate `questionId` per question @@ -266,7 +284,7 @@ Required before: implementing the double join for source language prompt. - Shuffle options so correct answer is not always in same position - `QuizService.evaluateAnswer()` — validate correctness, return `AnswerResult` -### Step 5 — Implement answer endpoint +### Step 5 — Implement answer endpoint - done - `POST /api/v1/game/answer` route, controller, service method @@ -295,10 +313,10 @@ Required before: implementing the double join for source language prompt. - **Distractor algorithm:** when Italian C2 has only 242 terms, should the difficulty filter fall back gracefully or return an error? Decision needed before implementing - `buildSession()`. + `buildSession()`. => resolved - **Session statefulness:** game loop is currently stateless (fetch all questions upfront). - Confirm this is still the intended MVP approach before building `buildSession()`. + Confirm this is still the intended MVP approach before building `buildSession()`. => resolved - **Glosses can leak answers:** some WordNet glosses contain the target-language word in the definition text (e.g. "Padre" appearing in the English gloss for "father"). Address during the post-MVP data enrichment pass — either clean the - glosses, replace them with custom definitions, or filter at the service layer. + glosses, replace them with custom definitions, or filter at the service layer. => resolved diff --git a/documentation/roadmap.md b/documentation/roadmap.md index cf2ee6f..2a43140 100644 --- a/documentation/roadmap.md +++ b/documentation/roadmap.md @@ -32,10 +32,10 @@ Done when: `GET /api/decks/1/terms?limit=10` returns 10 terms from a specific de [x] Write `scripts/build_decks.ts` (reads external CEFR lists, matches to DB, creates decks) [x] Run `pnpm db:seed` → populates terms [x] Run `pnpm db:build-deck` → creates curated decks -[ ] Define Zod response schemas in `packages/shared` -[ ] Implement `DeckRepository.getTerms(deckId, limit, offset)` -[ ] Implement `QuizService.attachDistractors(terms)` — same POS, server-side, no duplicates -[ ] Implement `GET /language-pairs`, `GET /decks`, `GET /decks/:id/terms` endpoints +[x] Define Zod response schemas in `packages/shared` +[x] Implement `DeckRepository.getTerms(deckId, limit, offset)` => no decks needed anymore +[x] Implement `QuizService.attachDistractors(terms)` — same POS, server-side, no duplicates +[x] Implement `GET /language-pairs`, `GET /decks`, `GET /decks/:id/terms` endpoints => no language pairs, not needed anymore [ ] Unit tests for `QuizService` (correct POS filtering, never includes the answer) [ ] update decisions.md