formatting
This commit is contained in:
parent
2ff7d1759e
commit
4f59f3bc14
23 changed files with 994 additions and 3338 deletions
|
|
@ -55,13 +55,13 @@ See **Setup** for download instructions.
|
|||
|
||||
Per-language JSON files in `sources/cefr/` provide the initial CEFR level annotations. These files do not cover the full vocabulary extracted from OMW — coverage varies by language. Gaps and disagreements are handled by the enrich stage.
|
||||
|
||||
| Language | File |
|
||||
|---|---|
|
||||
| English | `sources/cefr/en.json` |
|
||||
| Italian | `sources/cefr/it.json` |
|
||||
| Spanish | `sources/cefr/es.json` |
|
||||
| German | `sources/cefr/de.json` |
|
||||
| French | `sources/cefr/fr.json` |
|
||||
| Language | File |
|
||||
| -------- | ---------------------- |
|
||||
| English | `sources/cefr/en.json` |
|
||||
| Italian | `sources/cefr/it.json` |
|
||||
| Spanish | `sources/cefr/es.json` |
|
||||
| German | `sources/cefr/de.json` |
|
||||
| French | `sources/cefr/fr.json` |
|
||||
|
||||
These files are committed to git. For per-language coverage detail see `COVERAGE.md`.
|
||||
|
||||
|
|
@ -102,13 +102,13 @@ See `LLM-SETUP.md`.
|
|||
|
||||
The pipeline runs in five stages. Each stage is independent and can be re-run without affecting the others.
|
||||
|
||||
| Stage | What it does |
|
||||
|---|---|
|
||||
| 1. Extract | Reads OMW SQLite database, outputs normalized JSON per language |
|
||||
| Stage | What it does |
|
||||
| ----------- | -------------------------------------------------------------------- |
|
||||
| 1. Extract | Reads OMW SQLite database, outputs normalized JSON per language |
|
||||
| 2. Annotate | Merges CEFR source files into extracted data, adds source file votes |
|
||||
| 3. Enrich | Runs local LLMs in two rounds — generation then voting |
|
||||
| 4. Merge | Resolves votes, derives difficulty, splits into final and flagged |
|
||||
| 5. Compare | Generates COVERAGE.md with detailed quality report |
|
||||
| 3. Enrich | Runs local LLMs in two rounds — generation then voting |
|
||||
| 4. Merge | Resolves votes, derives difficulty, splits into final and flagged |
|
||||
| 5. Compare | Generates COVERAGE.md with detailed quality report |
|
||||
|
||||
### 1. Extract
|
||||
|
||||
|
|
@ -137,11 +137,11 @@ Each record in the output looks like this:
|
|||
"fr": ["comptable"]
|
||||
},
|
||||
"glosses": {
|
||||
"en": ["(usually followed by 'to') having the necessary means or skill or know-how or authority to do something"]
|
||||
"en": [
|
||||
"(usually followed by 'to') having the necessary means or skill or know-how or authority to do something"
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"en": ["able to swim", "she was able to program her computer"]
|
||||
}
|
||||
"examples": { "en": ["able to swim", "she was able to program her computer"] }
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -158,6 +158,7 @@ Words appearing in the CEFR source file multiple times with different CEFR level
|
|||
|
||||
**Input:** `stage-1-extract/output/omw.json` + `stage-2-annotate/sources/cefr/{lang}.json`
|
||||
**Output:**
|
||||
|
||||
- `stage-2-annotate/output/{lang}.json` — one per language
|
||||
- `stage-2-annotate/output/conflicts.json` — cross-language conflicts for review
|
||||
|
||||
|
|
@ -177,20 +178,14 @@ Each record in the output extends the OMW record with a `votes` field and any ad
|
|||
"es": ["capaz"],
|
||||
"fr": ["comptable"]
|
||||
},
|
||||
"glosses": {
|
||||
"en": ["having the necessary means or skill to do something"]
|
||||
},
|
||||
"glosses": { "en": ["having the necessary means or skill to do something"] },
|
||||
"examples": {
|
||||
"en": [
|
||||
{ "text": "able to swim", "source": "omw" },
|
||||
{ "text": "She was able to finish the task.", "source": "cefr" }
|
||||
]
|
||||
},
|
||||
"votes": {
|
||||
"en": {
|
||||
"able": { "cefr_source": "B1" }
|
||||
}
|
||||
}
|
||||
"votes": { "en": { "able": { "cefr_source": "B1" } } }
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -297,9 +292,7 @@ Each record in the votes file looks like this:
|
|||
}
|
||||
},
|
||||
"examples": {
|
||||
"en": [
|
||||
{ "text": "the dog barked at the stranger", "source": "omw" }
|
||||
],
|
||||
"en": [{ "text": "the dog barked at the stranger", "source": "omw" }],
|
||||
"fr": {
|
||||
"candidates": [
|
||||
{ "text": "le chien a aboyé", "source": "model_1" },
|
||||
|
|
@ -311,8 +304,14 @@ Each record in the votes file looks like this:
|
|||
"descriptions": {
|
||||
"en": {
|
||||
"candidates": [
|
||||
{ "text": "a common household pet known for loyalty", "source": "model_1" },
|
||||
{ "text": "a domesticated animal and loyal companion", "source": "model_2" }
|
||||
{
|
||||
"text": "a common household pet known for loyalty",
|
||||
"source": "model_1"
|
||||
},
|
||||
{
|
||||
"text": "a domesticated animal and loyal companion",
|
||||
"source": "model_2"
|
||||
}
|
||||
],
|
||||
"votes": { "model_1": 2, "model_2": 1 }
|
||||
}
|
||||
|
|
@ -334,14 +333,15 @@ Reads the votes file per language and resolves the final value for every field.
|
|||
|
||||
**Difficulty mapping:**
|
||||
|
||||
| CEFR | Difficulty |
|
||||
|---|---|
|
||||
| A1, A2 | easy |
|
||||
| CEFR | Difficulty |
|
||||
| ------ | ------------ |
|
||||
| A1, A2 | easy |
|
||||
| B1, B2 | intermediate |
|
||||
| C1, C2 | hard |
|
||||
| C1, C2 | hard |
|
||||
|
||||
**Input:** `stage-3-enrich/output/votes/{lang}_votes.json`
|
||||
**Output:**
|
||||
|
||||
- `stage-4-merge/output/final/{lang}.json` — fully resolved, ready for seeding
|
||||
- `stage-4-merge/output/flagged/{lang}.json` — CEFR majority not reached, needs manual review before seeding
|
||||
|
||||
|
|
@ -360,21 +360,15 @@ Each record in `final/{lang}.json` looks like this:
|
|||
{ "text": "dog", "cefr_level": "A1", "difficulty": "easy" },
|
||||
{ "text": "canine", "cefr_level": "B2", "difficulty": "intermediate" }
|
||||
],
|
||||
"it": [
|
||||
{ "text": "cane", "cefr_level": "A1", "difficulty": "easy" }
|
||||
]
|
||||
"it": [{ "text": "cane", "cefr_level": "A1", "difficulty": "easy" }]
|
||||
},
|
||||
"glosses": {
|
||||
"en": { "text": "a domesticated carnivorous mammal", "source": "omw" },
|
||||
"fr": { "text": "un mammifère carnivore domestiqué", "source": "model_1" }
|
||||
},
|
||||
"examples": {
|
||||
"en": [
|
||||
{ "text": "the dog barked at the stranger", "source": "omw" }
|
||||
],
|
||||
"fr": [
|
||||
{ "text": "le chien a aboyé", "source": "model_1" }
|
||||
]
|
||||
"en": [{ "text": "the dog barked at the stranger", "source": "omw" }],
|
||||
"fr": [{ "text": "le chien a aboyé", "source": "model_1" }]
|
||||
},
|
||||
"descriptions": {
|
||||
"en": {
|
||||
|
|
@ -400,6 +394,7 @@ output quality per language. Run this after merge to verify output before
|
|||
seeding the database.
|
||||
|
||||
**Input:**
|
||||
|
||||
- `stage-4-merge/output/final/{lang}.json`
|
||||
- `stage-4-merge/output/flagged/{lang}.json`
|
||||
|
||||
|
|
@ -436,12 +431,12 @@ pnpm --filter @lila/pipeline compare
|
|||
|
||||
These values are defined in `packages/shared/src/constants.ts` and enforced by database check constraints. The pipeline filters out any entries that violate them.
|
||||
|
||||
| Constant | Values |
|
||||
|---|---|
|
||||
| Languages | `en`, `it`, `de`, `es`, `fr` |
|
||||
| Constant | Values |
|
||||
| --------------- | ------------------------------------- |
|
||||
| Languages | `en`, `it`, `de`, `es`, `fr` |
|
||||
| Parts of speech | `noun`, `verb`, `adjective`, `adverb` |
|
||||
| CEFR levels | `A1`, `A2`, `B1`, `B2`, `C1`, `C2` |
|
||||
| Difficulty | `easy`, `intermediate`, `hard` |
|
||||
| CEFR levels | `A1`, `A2`, `B1`, `B2`, `C1`, `C2` |
|
||||
| Difficulty | `easy`, `intermediate`, `hard` |
|
||||
|
||||
Adding a new value to any of these requires a constants update and a database migration before re-running the pipeline. See **Adding a new language** for the full steps — the same process applies for new parts of speech.
|
||||
|
||||
|
|
|
|||
|
|
@ -243,13 +243,13 @@ Automated build and deploy via Forgejo Actions. On every push to `main`, the pip
|
|||
|
||||
### Secrets (stored in Forgejo repo settings → Actions → Secrets)
|
||||
|
||||
| Secret | Value |
|
||||
|---|---|
|
||||
| REGISTRY_USER | Forgejo username |
|
||||
| REGISTRY_PASSWORD | Forgejo password |
|
||||
| SSH_PRIVATE_KEY | Contents of `~/.ssh/ci-runner` on the VPS |
|
||||
| SSH_HOST | VPS IP address |
|
||||
| SSH_USER | `lila` |
|
||||
| Secret | Value |
|
||||
| ----------------- | ----------------------------------------- |
|
||||
| REGISTRY_USER | Forgejo username |
|
||||
| REGISTRY_PASSWORD | Forgejo password |
|
||||
| SSH_PRIVATE_KEY | Contents of `~/.ssh/ci-runner` on the VPS |
|
||||
| SSH_HOST | VPS IP address |
|
||||
| SSH_USER | `lila` |
|
||||
|
||||
### Runner Configuration
|
||||
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ and production scripts.
|
|||
|
||||
## Hardware (dev machine)
|
||||
|
||||
| Component | Spec |
|
||||
|---|---|
|
||||
| CPU | Intel Core i7-6500U (2 cores / 4 threads @ 3.10 GHz) |
|
||||
| RAM | 8 GB |
|
||||
| GPU | NVIDIA GeForce GTX 950M — 4 GB VRAM (Maxwell, CUDA compute 5.0) |
|
||||
| OS | Debian GNU/Linux 13 (trixie) x86_64 |
|
||||
| Component | Spec |
|
||||
| --------- | --------------------------------------------------------------- |
|
||||
| CPU | Intel Core i7-6500U (2 cores / 4 threads @ 3.10 GHz) |
|
||||
| RAM | 8 GB |
|
||||
| GPU | NVIDIA GeForce GTX 950M — 4 GB VRAM (Maxwell, CUDA compute 5.0) |
|
||||
| OS | Debian GNU/Linux 13 (trixie) x86_64 |
|
||||
|
||||
**Local inference verdict:** viable for small/quantized models, not for
|
||||
production runs. See the [Local inference](#local-inference-llamacpp) section
|
||||
|
|
@ -28,12 +28,12 @@ The enrich script uses a single, swappable provider config. All providers
|
|||
except Anthropic expose an OpenAI-compatible API, so the same client code
|
||||
works across all of them — only `baseURL`, `apiKey`, and `model` change.
|
||||
|
||||
| Provider | Use case | Cost | Rate limits |
|
||||
|---|---|---|---|
|
||||
| llama.cpp (local) | Quality testing, overnight dev runs | Free (electricity) | None |
|
||||
| OpenRouter (free tier) | Quality comparison, multi-model evaluation | Free | 50 req/day, 20 req/min |
|
||||
| OpenRouter (paid) | Production runs if local quality insufficient | Pay-per-token | None |
|
||||
| Anthropic API | Quality baseline / reference | Pay-per-token | Standard |
|
||||
| Provider | Use case | Cost | Rate limits |
|
||||
| ---------------------- | --------------------------------------------- | ------------------ | ---------------------- |
|
||||
| llama.cpp (local) | Quality testing, overnight dev runs | Free (electricity) | None |
|
||||
| OpenRouter (free tier) | Quality comparison, multi-model evaluation | Free | 50 req/day, 20 req/min |
|
||||
| OpenRouter (paid) | Production runs if local quality insufficient | Pay-per-token | None |
|
||||
| Anthropic API | Quality baseline / reference | Pay-per-token | Standard |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -58,12 +58,12 @@ in hybrid mode, slower than full-GPU but much faster than pure CPU.
|
|||
|
||||
Practical estimates for this hardware (~3.5 GB VRAM usable after drivers):
|
||||
|
||||
| Model size | Q4 VRAM | Mode | Est. speed |
|
||||
|---|---|---|---|
|
||||
| 3B | ~2.0 GB | Full GPU | ~15–20 tok/s |
|
||||
| 4B | ~2.5 GB | Full GPU | ~12–18 tok/s |
|
||||
| 7B | ~4.5 GB | Hybrid (~26/32 layers on GPU) | ~8–12 tok/s |
|
||||
| 13B+ | ~8 GB+ | CPU-heavy hybrid | too slow |
|
||||
| Model size | Q4 VRAM | Mode | Est. speed |
|
||||
| ---------- | ------- | ----------------------------- | ------------ |
|
||||
| 3B | ~2.0 GB | Full GPU | ~15–20 tok/s |
|
||||
| 4B | ~2.5 GB | Full GPU | ~12–18 tok/s |
|
||||
| 7B | ~4.5 GB | Hybrid (~26/32 layers on GPU) | ~8–12 tok/s |
|
||||
| 13B+ | ~8 GB+ | CPU-heavy hybrid | too slow |
|
||||
|
||||
### Recommended local models
|
||||
|
||||
|
|
@ -71,6 +71,7 @@ Two candidates worth testing, covering different points on the size/quality
|
|||
tradeoff:
|
||||
|
||||
**Gemma 4 E4B Instruct (Q4 / UD-Q4_K_XL)**
|
||||
|
||||
- GGUF file: `gemma-4-E4B-it-UD-Q4_K_XL.gguf` (~2.5 GB)
|
||||
- Source: https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF
|
||||
- Runs fully on GPU. Brand new (April 2025), built for edge hardware, 140+
|
||||
|
|
@ -78,6 +79,7 @@ tradeoff:
|
|||
to test.
|
||||
|
||||
**Qwen2.5 7B Instruct (Q4_K_M)**
|
||||
|
||||
- GGUF file: `Qwen2.5-7B-Instruct-Q4_K_M.gguf` (~4.5 GB)
|
||||
- Source: https://huggingface.co/Qwen/Qwen2.5-7B-Instruct-GGUF
|
||||
- Runs in hybrid mode (~26 of 32 layers on GPU, rest on CPU), ~8–12 tok/s.
|
||||
|
|
@ -107,6 +109,7 @@ wget -O models/qwen2.5-3b-instruct-q4_k_m.gguf \
|
|||
### Starting the server
|
||||
|
||||
**Gemma 4 E4B** (full GPU):
|
||||
|
||||
```bash
|
||||
./build/bin/llama-server \
|
||||
--model models/gemma-4-e4b-it-ud-q4_k_xl.gguf \
|
||||
|
|
@ -117,6 +120,7 @@ wget -O models/qwen2.5-3b-instruct-q4_k_m.gguf \
|
|||
```
|
||||
|
||||
**Qwen2.5 7B** (hybrid — tune `--n-gpu-layers` to fit your VRAM):
|
||||
|
||||
```bash
|
||||
./build/bin/llama-server \
|
||||
--model models/qwen2.5-7b-instruct-q4_k_m.gguf \
|
||||
|
|
@ -163,15 +167,16 @@ object changes.
|
|||
|
||||
Ranked by expected multilingual generation quality for en/it/de/fr/es:
|
||||
|
||||
| Model ID | Params | Notes |
|
||||
|---|---|---|
|
||||
| `qwen/qwen3-coder:free` | 480B MoE (35B active) | Best free option. Strong multilingual despite "coder" label. Use as quality ceiling. |
|
||||
| `qwen/qwen3-next-80b-a3b-instruct:free` | 80B MoE (3B active) | Smaller Qwen, useful comparison point. |
|
||||
| `nvidia/nemotron-3-super-120b-a12b:free` | 120B MoE (12B active) | 262K context, supports structured output. |
|
||||
| `google/gemma-4-31b-it:free` | 31B | 140+ language support, good European language coverage. |
|
||||
| `zhipuai/glm-4.5-air:free` | MoE | Multilingual-focused. |
|
||||
| Model ID | Params | Notes |
|
||||
| ---------------------------------------- | --------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `qwen/qwen3-coder:free` | 480B MoE (35B active) | Best free option. Strong multilingual despite "coder" label. Use as quality ceiling. |
|
||||
| `qwen/qwen3-next-80b-a3b-instruct:free` | 80B MoE (3B active) | Smaller Qwen, useful comparison point. |
|
||||
| `nvidia/nemotron-3-super-120b-a12b:free` | 120B MoE (12B active) | 262K context, supports structured output. |
|
||||
| `google/gemma-4-31b-it:free` | 31B | 140+ language support, good European language coverage. |
|
||||
| `zhipuai/glm-4.5-air:free` | MoE | Multilingual-focused. |
|
||||
|
||||
**Skip for this pipeline:**
|
||||
|
||||
- Llama models — weaker European language generation than Qwen/Gemma
|
||||
- Mistral free tier — requests may be used for model training
|
||||
|
||||
|
|
@ -194,7 +199,7 @@ change this object and re-run.
|
|||
// config.ts
|
||||
|
||||
export type ProviderConfig = {
|
||||
name: string; // used for output folder naming
|
||||
name: string; // used for output folder naming
|
||||
baseURL: string;
|
||||
apiKey: string;
|
||||
model: string;
|
||||
|
|
@ -205,8 +210,8 @@ export type ProviderConfig = {
|
|||
export const LOCAL_QWEN3B: ProviderConfig = {
|
||||
name: "local-qwen2.5-3b",
|
||||
baseURL: "http://127.0.0.1:8080/v1",
|
||||
apiKey: "none", // llama.cpp ignores this
|
||||
model: "qwen2.5-3b", // llama.cpp ignores model name, uses loaded model
|
||||
apiKey: "none", // llama.cpp ignores this
|
||||
model: "qwen2.5-3b", // llama.cpp ignores model name, uses loaded model
|
||||
maxTokens: 512,
|
||||
};
|
||||
|
||||
|
|
@ -231,7 +236,7 @@ export const OR_GEMMA4_31B: ProviderConfig = {
|
|||
// Anthropic (reference baseline — different adapter required)
|
||||
export const ANTHROPIC_SONNET: ProviderConfig = {
|
||||
name: "anthropic-sonnet",
|
||||
baseURL: "https://api.anthropic.com/v1", // adapter handles format difference
|
||||
baseURL: "https://api.anthropic.com/v1", // adapter handles format difference
|
||||
apiKey: process.env.ANTHROPIC_API_KEY!,
|
||||
model: "claude-sonnet-4-6",
|
||||
maxTokens: 512,
|
||||
|
|
@ -239,6 +244,7 @@ export const ANTHROPIC_SONNET: ProviderConfig = {
|
|||
```
|
||||
|
||||
Output from each run lands in:
|
||||
|
||||
```
|
||||
stage-3-enrich/test/output/{provider.name}/results.json
|
||||
stage-3-enrich/test/output/{provider.name}/metrics.json
|
||||
|
|
@ -252,21 +258,21 @@ The evaluate script compares all `metrics.json` files side by side.
|
|||
|
||||
The test script measures the following per provider run:
|
||||
|
||||
| Metric | What it measures |
|
||||
|---|---|
|
||||
| **JSON parse rate** | % of responses that are valid, schema-compliant JSON. Critical — a failed parse is a wasted call. Target: >97% |
|
||||
| **Field coverage** | % of records where all required fields are present (cefr votes for all translations, descriptions for all languages, glosses/examples for fr/es) |
|
||||
| **CEFR agreement** | For records that have a `cefr_source` vote, % where the model agrees. Measures calibration. |
|
||||
| **Language correctness** | Manual spot-check only — automated detection not reliable enough |
|
||||
| **Tokens/second** | Local only. Indicates overnight run feasibility |
|
||||
| Metric | What it measures |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **JSON parse rate** | % of responses that are valid, schema-compliant JSON. Critical — a failed parse is a wasted call. Target: >97% |
|
||||
| **Field coverage** | % of records where all required fields are present (cefr votes for all translations, descriptions for all languages, glosses/examples for fr/es) |
|
||||
| **CEFR agreement** | For records that have a `cefr_source` vote, % where the model agrees. Measures calibration. |
|
||||
| **Language correctness** | Manual spot-check only — automated detection not reliable enough |
|
||||
| **Tokens/second** | Local only. Indicates overnight run feasibility |
|
||||
|
||||
### Decision thresholds
|
||||
|
||||
| Metric | Threshold | Action if below |
|
||||
|---|---|---|
|
||||
| JSON parse rate | < 97% | Do not use this model for production |
|
||||
| Field coverage | < 95% | Prompt needs revision before production |
|
||||
| CEFR agreement | < 70% | Model lacks vocabulary knowledge for this task |
|
||||
| Metric | Threshold | Action if below |
|
||||
| --------------- | --------- | ---------------------------------------------- |
|
||||
| JSON parse rate | < 97% | Do not use this model for production |
|
||||
| Field coverage | < 95% | Prompt needs revision before production |
|
||||
| CEFR agreement | < 70% | Model lacks vocabulary knowledge for this task |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
# notes
|
||||
|
||||
|
||||
## prompt
|
||||
|
||||
ive attached the readme of my project. this is my current task:
|
||||
|
|
@ -46,7 +45,7 @@ laptop: verify if docker containers run on startup (they shouldnt)
|
|||
### vps setup
|
||||
|
||||
- monitoring and logging (eg via chrootkit or rkhunter, logwatch/monit => mails daily with summary)
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
- ~~keep the vps clean (e.g. old docker images/containers)~~ ✅ CI/CD pipeline runs `docker image prune -f` after deploy
|
||||
|
||||
### ~~cd/ci pipeline~~ ✅ RESOLVED
|
||||
|
|
@ -55,9 +54,9 @@ Forgejo Actions with runner on VPS, Forgejo built-in container registry. See `de
|
|||
|
||||
### ~~postgres backups~~ ✅ RESOLVED
|
||||
|
||||
Daily pg_dump cron job, 7-day retention, dev laptop auto-sync via rsync. See `deployment.md`.
|
||||
=======
|
||||
>>>>>>> dev
|
||||
# Daily pg_dump cron job, 7-day retention, dev laptop auto-sync via rsync. See `deployment.md`.
|
||||
|
||||
> > > > > > > dev
|
||||
|
||||
### try now option
|
||||
|
||||
|
|
|
|||
|
|
@ -61,10 +61,12 @@ export const evaluateAnswer = async (
|
|||
store: GameSessionStore,
|
||||
): Promise<AnswerResult> => {
|
||||
const session = await store.get(submission.sessionId);
|
||||
if (!session) throw new NotFoundError(`Game session not found: ${submission.sessionId}`);
|
||||
if (!session)
|
||||
throw new NotFoundError(`Game session not found: ${submission.sessionId}`);
|
||||
|
||||
const correctOptionId = session.answers.get(submission.questionId);
|
||||
if (correctOptionId === undefined) throw new NotFoundError(`Question not found: ${submission.questionId}`);
|
||||
if (correctOptionId === undefined)
|
||||
throw new NotFoundError(`Question not found: ${submission.questionId}`);
|
||||
|
||||
// delete answered question; delete session when all questions are answered
|
||||
session.answers.delete(submission.questionId);
|
||||
|
|
@ -84,10 +86,14 @@ export const evaluateAnswer = async (
|
|||
```ts
|
||||
// ✅ option B — TTL in InMemoryGameSessionStore
|
||||
export class InMemoryGameSessionStore implements GameSessionStore {
|
||||
private sessions = new Map<string, { data: GameSessionData; expiresAt: number }>();
|
||||
private sessions = new Map<
|
||||
string,
|
||||
{ data: GameSessionData; expiresAt: number }
|
||||
>();
|
||||
private readonly ttlMs: number;
|
||||
|
||||
constructor(ttlMs = 30 * 60 * 1000) { // 30 minutes default
|
||||
constructor(ttlMs = 30 * 60 * 1000) {
|
||||
// 30 minutes default
|
||||
this.ttlMs = ttlMs;
|
||||
}
|
||||
|
||||
|
|
@ -115,15 +121,13 @@ export class InMemoryGameSessionStore implements GameSessionStore {
|
|||
|
||||
---
|
||||
|
||||
|
||||
|
||||
**Problem**
|
||||
|
||||
`GameRequest.rounds` is typed as `string` in `@lila/shared`, forcing the service to cast it every time:
|
||||
|
||||
```ts
|
||||
// ❌ why is a round count a string?
|
||||
Number(request.rounds)
|
||||
Number(request.rounds);
|
||||
```
|
||||
|
||||
**Fix — fix the schema in `@lila/shared`**
|
||||
|
|
@ -204,7 +208,10 @@ it("correct answer appears exactly once in options even if distractor matches",
|
|||
// simulate getDistractors returning the correct answer as one of the distractors
|
||||
mockGetDistractors.mockResolvedValueOnce(["cane", "wrong2", "wrong3"]);
|
||||
|
||||
const session = await createGameSession(validRequest, new InMemoryGameSessionStore());
|
||||
const session = await createGameSession(
|
||||
validRequest,
|
||||
new InMemoryGameSessionStore(),
|
||||
);
|
||||
const question = session.questions[0]!;
|
||||
const optionTexts = question.options.map((o) => o.text);
|
||||
|
||||
|
|
@ -285,16 +292,14 @@ his `sessionId`.
|
|||
|
||||
```ts
|
||||
// GameSessionStore.ts
|
||||
export type GameSessionData = {
|
||||
answers: Map<string, number>;
|
||||
userId: string;
|
||||
};
|
||||
export type GameSessionData = { answers: Map<string, number>; userId: string };
|
||||
|
||||
// evaluateAnswer
|
||||
const session = await store.get(submission.sessionId);
|
||||
|
||||
if (!session) throw new NotFoundError(`Game session not found`);
|
||||
if (session.userId !== requestingUserId) throw new NotFoundError(`Game session not found`);
|
||||
if (session.userId !== requestingUserId)
|
||||
throw new NotFoundError(`Game session not found`);
|
||||
// ^^^ same error — don't confirm the session exists to the wrong user
|
||||
```
|
||||
|
||||
|
|
@ -326,8 +331,9 @@ if (terms.length === 0) {
|
|||
it("throws when getGameTerms returns no terms", async () => {
|
||||
mockGetGameTerms.mockResolvedValue([]);
|
||||
|
||||
await expect(createGameSession(validRequest, new InMemoryGameSessionStore()))
|
||||
.rejects.toThrow("No terms found");
|
||||
await expect(
|
||||
createGameSession(validRequest, new InMemoryGameSessionStore()),
|
||||
).rejects.toThrow("No terms found");
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -349,8 +355,9 @@ it("throws when getGameTerms returns no terms", async () => {
|
|||
it("propagates getDistractors failure", async () => {
|
||||
mockGetDistractors.mockRejectedValue(new Error("db timeout"));
|
||||
|
||||
await expect(createGameSession(validRequest, new InMemoryGameSessionStore()))
|
||||
.rejects.toThrow("db timeout");
|
||||
await expect(
|
||||
createGameSession(validRequest, new InMemoryGameSessionStore()),
|
||||
).rejects.toThrow("db timeout");
|
||||
});
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -51,9 +51,9 @@ This is the full vision. The current implementation already covers most of it; r
|
|||
|
||||
### What is CUT from the MVP
|
||||
|
||||
| Feature | Why cut |
|
||||
| ------------------------------- | -------------------------------------- |
|
||||
| User stats / profiles | Needs auth |
|
||||
| Feature | Why cut |
|
||||
| --------------------- | ---------- |
|
||||
| User stats / profiles | Needs auth |
|
||||
|
||||
These are not deleted from the plan — they are deferred. The architecture is already designed to support them. See Section 11 (Post-MVP Ladder).
|
||||
|
||||
|
|
@ -63,22 +63,22 @@ These are not deleted from the plan — they are deferred. The architecture is a
|
|||
|
||||
The monorepo structure and tooling are already set up. This is the full stack.
|
||||
|
||||
| Layer | Technology | Status |
|
||||
| ------------ | ------------------------------ | ----------- |
|
||||
| Monorepo | pnpm workspaces | ✅ |
|
||||
| Frontend | React 18, Vite, TypeScript | ✅ |
|
||||
| Routing | TanStack Router | ✅ |
|
||||
| Server state | TanStack Query | ✅ |
|
||||
| Client state | Zustand | ✅ |
|
||||
| Styling | Tailwind CSS + shadcn/ui | ✅ |
|
||||
| Backend | Node.js, Express, TypeScript | ✅ |
|
||||
| Database | PostgreSQL + Drizzle ORM | ✅ |
|
||||
| Validation | Zod (shared schemas) | ✅ |
|
||||
| Testing | Vitest, supertest | ✅ |
|
||||
| Auth | Better Auth (Google + GitHub) | ✅ |
|
||||
| Deployment | Docker Compose, Caddy, Hetzner | ✅ |
|
||||
| CI/CD | Forgejo Actions | ✅ |
|
||||
| Realtime | WebSockets (`ws` library) | ✅ |
|
||||
| Layer | Technology | Status |
|
||||
| ------------ | ------------------------------ | ------------------------------------------------------ |
|
||||
| Monorepo | pnpm workspaces | ✅ |
|
||||
| Frontend | React 18, Vite, TypeScript | ✅ |
|
||||
| Routing | TanStack Router | ✅ |
|
||||
| Server state | TanStack Query | ✅ |
|
||||
| Client state | Zustand | ✅ |
|
||||
| Styling | Tailwind CSS + shadcn/ui | ✅ |
|
||||
| Backend | Node.js, Express, TypeScript | ✅ |
|
||||
| Database | PostgreSQL + Drizzle ORM | ✅ |
|
||||
| Validation | Zod (shared schemas) | ✅ |
|
||||
| Testing | Vitest, supertest | ✅ |
|
||||
| Auth | Better Auth (Google + GitHub) | ✅ |
|
||||
| Deployment | Docker Compose, Caddy, Hetzner | ✅ |
|
||||
| CI/CD | Forgejo Actions | ✅ |
|
||||
| Realtime | WebSockets (`ws` library) | ✅ |
|
||||
| Cache | Valkey | ⚠️ optional (used locally; production/state hardening) |
|
||||
|
||||
---
|
||||
|
|
@ -288,26 +288,27 @@ After completing a task: share the code, ask what to refactor and why. The LLM s
|
|||
## 11. Post-MVP Ladder
|
||||
|
||||
<<<<<<< HEAD
|
||||
| Phase | What it adds | Status |
|
||||
| Phase | What it adds | Status |
|
||||
| ----------------- | ------------------------------------------------------------------------------- | ------ |
|
||||
| Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ |
|
||||
| Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ |
|
||||
| Hardening (partial) | CI/CD pipeline, DB backups | ✅ |
|
||||
| User Stats | Games played, score history, profile page | ❌ |
|
||||
| Multiplayer Lobby | Room creation, join by code, WebSocket connection | ❌ |
|
||||
| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ❌ |
|
||||
| Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ |
|
||||
| Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ |
|
||||
| Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ |
|
||||
| Hardening (partial) | CI/CD pipeline, DB backups | ✅ |
|
||||
| User Stats | Games played, score history, profile page | ❌ |
|
||||
| Multiplayer Lobby | Room creation, join by code, WebSocket connection | ❌ |
|
||||
| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ❌ |
|
||||
| Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ |
|
||||
=======
|
||||
| Phase | What it adds | Status |
|
||||
| Phase | What it adds | Status |
|
||||
| ------------------- | ----------------------------------------------------------------------- | ------ |
|
||||
| Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ |
|
||||
| Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ |
|
||||
| Hardening (partial) | CI/CD pipeline, DB backups | ✅ |
|
||||
| User Stats | Games played, score history, profile page | ❌ |
|
||||
| Multiplayer Lobby | Room creation, join by code, WebSocket connection | ✅ |
|
||||
| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ✅ |
|
||||
| Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ |
|
||||
>>>>>>> dev
|
||||
| Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ |
|
||||
| Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ |
|
||||
| Hardening (partial) | CI/CD pipeline, DB backups | ✅ |
|
||||
| User Stats | Games played, score history, profile page | ❌ |
|
||||
| Multiplayer Lobby | Room creation, join by code, WebSocket connection | ✅ |
|
||||
| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ✅ |
|
||||
| Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ |
|
||||
|
||||
> > > > > > > dev
|
||||
|
||||
### Future Data Model Extensions (deferred, additive)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Ticket Blueprint
|
||||
|
||||
Two formats depending on task type. Choose based on whether a meaningful
|
||||
Two formats depending on task type. Choose based on whether a meaningful
|
||||
decision between options was made.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -87,9 +87,7 @@ pass init <your-key-id>
|
|||
Replace the entire file contents with:
|
||||
|
||||
```json
|
||||
{
|
||||
"credsStore": "pass"
|
||||
}
|
||||
{ "credsStore": "pass" }
|
||||
```
|
||||
|
||||
### 6. Re-login to registries
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ Rejected because: coercion is for untrusted or uncontrolled inputs (form fields,
|
|||
|
||||
6. In `apps/web/src/components/game/GameSetup.tsx`:
|
||||
- Update `SettingGroup` props to accept `string | number`:
|
||||
|
||||
|
||||
```ts
|
||||
type SettingGroupProps = {
|
||||
options: readonly (string | number)[];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue