updating documentation

This commit is contained in:
lila 2026-04-11 21:32:13 +02:00
parent bc7977463e
commit dd6c2b0118
2 changed files with 39 additions and 21 deletions

View file

@ -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. 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 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 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). - 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. - 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. - 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) ## API Schemas (packages/shared)
### `GameRequestSchema` (implemented) ### `GameRequestSchema`
```typescript ```typescript
{ {
@ -169,14 +181,11 @@ as a potential constraint for the distractor algorithm at high difficulty.
} }
``` ```
### Planned schemas (not yet implemented) AnswerOption: { optionId: number (0-3), text: string }
GameQuestion: { questionId: uuid, prompt: string, gloss: string | null, options: AnswerOption[4] }
```text GameSession: { sessionId: uuid, questions: GameQuestion[] }
QuizQuestion — prompt, optional gloss, 4 options (no correct answer) AnswerSubmission: { sessionId: uuid, questionId: uuid, selectedOptionId: number (0-3) }
QuizOption — optionId + text AnswerResult: { questionId: uuid, isCorrect: boolean, correctOptionId: number (0-3), selectedOptionId: number (0-3) }
AnswerSubmission — questionId + selectedOptionId
AnswerResult — correct boolean, correctOptionId, selectedOptionId
```
--- ---
@ -236,27 +245,36 @@ packages/db/src/
- [x] Gloss left join implemented - [x] Gloss left join implemented
- [x] Model return type uses neutral field names (sourceText, targetText, sourceGloss) - [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] 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 ## Roadmap Ahead
### Step 1 — Learn SQL fundamentals (in progress) ### Step 1 — Learn SQL fundamentals - done
Concepts needed: SELECT, FROM, JOIN, WHERE, LIMIT. Concepts needed: SELECT, FROM, JOIN, WHERE, LIMIT.
Resources: sqlzoo.net or Khan Academy SQL section. Resources: sqlzoo.net or Khan Academy SQL section.
Required before: implementing the double join for source language prompt. 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) - Double join on `translations` — once for source language (prompt), once for target language (answer)
- `GlossModel.getGloss(termId, languageCode)` — fetch gloss if available - `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` - `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[]` - `QuizService.buildSession()` — assemble raw rows into `QuizQuestion[]`
- Generate `questionId` per question - 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 - Shuffle options so correct answer is not always in same position
- `QuizService.evaluateAnswer()` — validate correctness, return `AnswerResult` - `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 - `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 - **Distractor algorithm:** when Italian C2 has only 242 terms, should the difficulty
filter fall back gracefully or return an error? Decision needed before implementing 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). - **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 - **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 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 "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

View file

@ -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] 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:seed` → populates terms
[x] Run `pnpm db:build-deck` → creates curated decks [x] Run `pnpm db:build-deck` → creates curated decks
[ ] Define Zod response schemas in `packages/shared` [x] Define Zod response schemas in `packages/shared`
[ ] Implement `DeckRepository.getTerms(deckId, limit, offset)` [x] Implement `DeckRepository.getTerms(deckId, limit, offset)` => no decks needed anymore
[ ] Implement `QuizService.attachDistractors(terms)` — same POS, server-side, no duplicates [x] Implement `QuizService.attachDistractors(terms)` — same POS, server-side, no duplicates
[ ] Implement `GET /language-pairs`, `GET /decks`, `GET /decks/:id/terms` endpoints [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) [ ] Unit tests for `QuizService` (correct POS filtering, never includes the answer)
[ ] update decisions.md [ ] update decisions.md