updating documentation
This commit is contained in:
parent
bc7977463e
commit
dd6c2b0118
2 changed files with 39 additions and 21 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue