formatting

This commit is contained in:
lila 2026-04-28 13:18:18 +02:00
parent 2ff7d1759e
commit 4f59f3bc14
23 changed files with 994 additions and 3338 deletions

View file

@ -10,21 +10,21 @@ Live at [lilastudy.com](https://lilastudy.com).
## Stack ## Stack
| Layer | Technology | | Layer | Technology |
|---|---| | ------------ | ---------------------------------- |
| Monorepo | pnpm workspaces | | Monorepo | pnpm workspaces |
| Frontend | React 18, Vite, TypeScript | | Frontend | React 18, Vite, TypeScript |
| Routing | TanStack Router | | Routing | TanStack Router |
| Server state | TanStack Query | | Server state | TanStack Query |
| Styling | Tailwind CSS | | Styling | Tailwind CSS |
| Backend | Node.js, Express, TypeScript | | Backend | Node.js, Express, TypeScript |
| Database | PostgreSQL + Drizzle ORM | | Database | PostgreSQL + Drizzle ORM |
| Validation | Zod (shared schemas) | | Validation | Zod (shared schemas) |
| Auth | Better Auth (Google + GitHub) | | Auth | Better Auth (Google + GitHub) |
| Realtime | WebSockets (`ws` library) | | Realtime | WebSockets (`ws` library) |
| Testing | Vitest, supertest | | Testing | Vitest, supertest |
| Deployment | Docker Compose, Caddy, Hetzner VPS | | Deployment | Docker Compose, Caddy, Hetzner VPS |
| CI/CD | Forgejo Actions | | CI/CD | Forgejo Actions |
--- ---
@ -156,15 +156,15 @@ pnpm --filter web test
## Roadmap ## Roadmap
| Phase | Description | Status | | Phase | Description | Status |
|---|---|---| | ----- | ---------------------------------------------------------------------- | ------ |
| 0 | Foundation — monorepo, tooling, dev environment | ✅ | | 0 | Foundation — monorepo, tooling, dev environment | ✅ |
| 1 | Vocabulary data pipeline + REST API | ✅ | | 1 | Vocabulary data pipeline + REST API | ✅ |
| 2 | Singleplayer quiz UI | ✅ | | 2 | Singleplayer quiz UI | ✅ |
| 3 | Auth (Google + GitHub) | ✅ | | 3 | Auth (Google + GitHub) | ✅ |
| 4 | Multiplayer lobby (WebSockets) | ✅ | | 4 | Multiplayer lobby (WebSockets) | ✅ |
| 5 | Multiplayer game (real-time, server timer) | ✅ | | 5 | Multiplayer game (real-time, server timer) | ✅ |
| 6 | Production deployment + CI/CD | ✅ | | 6 | Production deployment + CI/CD | ✅ |
| 7 | Hardening (rate limiting, error boundaries, monitoring, accessibility) | 🔄 | | 7 | Hardening (rate limiting, error boundaries, monitoring, accessibility) | 🔄 |
See `documentation/roadmap.md` for task-level detail. See `documentation/roadmap.md` for task-level detail.

View file

@ -53,7 +53,11 @@ export const QuestionCard = ({
Round {questionNumber}/{totalQuestions} Round {questionNumber}/{totalQuestions}
</div> </div>
<div className="text-xs font-semibold text-(--color-text-muted)"> <div className="text-xs font-semibold text-(--color-text-muted)">
{currentResult ? "Checked" : selectedOptionId !== null ? "Ready" : "Pick one"} {currentResult
? "Checked"
: selectedOptionId !== null
? "Ready"
: "Pick one"}
</div> </div>
</div> </div>
@ -73,14 +77,14 @@ export const QuestionCard = ({
<div className="w-full rounded-3xl border border-(--color-primary-light) bg-white/55 dark:bg-black/10 backdrop-blur shadow-sm p-4"> <div className="w-full rounded-3xl border border-(--color-primary-light) bg-white/55 dark:bg-black/10 backdrop-blur shadow-sm p-4">
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{question.options.map((option) => ( {question.options.map((option) => (
<OptionButton <OptionButton
key={option.optionId} key={option.optionId}
text={option.text} text={option.text}
state={getOptionState(option.optionId)} state={getOptionState(option.optionId)}
onSelect={() => handleSelect(option.optionId)} onSelect={() => handleSelect(option.optionId)}
/> />
))} ))}
</div> </div>
</div> </div>

View file

@ -69,7 +69,9 @@ export const MultiplayerScoreScreen = ({
</span> </span>
<span <span
className={`text-sm font-semibold ${ className={`text-sm font-semibold ${
isCurrentUser ? "text-(--color-text)" : "text-(--color-text)" isCurrentUser
? "text-(--color-text)"
: "text-(--color-text)"
}`} }`}
> >
{player.user.name} {player.user.name}

View file

@ -6,10 +6,7 @@ type ConfettiBurstProps = {
count?: number; count?: number;
}; };
type Piece = { type Piece = { id: number; style: React.CSSProperties & ConfettiVars };
id: number;
style: React.CSSProperties & ConfettiVars;
};
type ConfettiVars = { type ConfettiVars = {
["--x0"]: string; ["--x0"]: string;
@ -56,7 +53,9 @@ export const ConfettiBurst = ({
}, []); }, []);
const pieces = useMemo<Piece[]>(() => { const pieces = useMemo<Piece[]>(() => {
const seed = hashStringToUint32(`${instanceId}:${count}:${colors.join(",")}`); const seed = hashStringToUint32(
`${instanceId}:${count}:${colors.join(",")}`,
);
const rand = mulberry32(seed); const rand = mulberry32(seed);
const rnd = (min: number, max: number) => min + rand() * (max - min); const rnd = (min: number, max: number) => min + rand() * (max - min);
@ -100,4 +99,3 @@ export const ConfettiBurst = ({
</div> </div>
); );
}; };

View file

@ -108,7 +108,9 @@ function MultiplayerPage() {
{/* Join lobby */} {/* Join lobby */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h2 className="text-lg font-bold text-(--color-text)">Join a lobby</h2> <h2 className="text-lg font-bold text-(--color-text)">
Join a lobby
</h2>
<p className="text-sm text-(--color-text-muted)"> <p className="text-sm text-(--color-text-muted)">
Enter the code shared by your host. Enter the code shared by your host.
</p> </p>

File diff suppressed because it is too large Load diff

View file

@ -5,8 +5,8 @@
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"outDir": "dist", "outDir": "dist",
"rootDir": ".", "rootDir": ".",
"types": ["node"], "types": ["node"]
}, },
"references": [{ "path": "../packages/shared" }], "references": [{ "path": "../packages/shared" }],
"include": ["./**/*"], "include": ["./**/*"]
} }

View file

@ -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. 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 | | Language | File |
|---|---| | -------- | ---------------------- |
| English | `sources/cefr/en.json` | | English | `sources/cefr/en.json` |
| Italian | `sources/cefr/it.json` | | Italian | `sources/cefr/it.json` |
| Spanish | `sources/cefr/es.json` | | Spanish | `sources/cefr/es.json` |
| German | `sources/cefr/de.json` | | German | `sources/cefr/de.json` |
| French | `sources/cefr/fr.json` | | French | `sources/cefr/fr.json` |
These files are committed to git. For per-language coverage detail see `COVERAGE.md`. 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. The pipeline runs in five stages. Each stage is independent and can be re-run without affecting the others.
| Stage | What it does | | Stage | What it does |
|---|---| | ----------- | -------------------------------------------------------------------- |
| 1. Extract | Reads OMW SQLite database, outputs normalized JSON per language | | 1. Extract | Reads OMW SQLite database, outputs normalized JSON per language |
| 2. Annotate | Merges CEFR source files into extracted data, adds source file votes | | 2. Annotate | Merges CEFR source files into extracted data, adds source file votes |
| 3. Enrich | Runs local LLMs in two rounds — generation then voting | | 3. Enrich | Runs local LLMs in two rounds — generation then voting |
| 4. Merge | Resolves votes, derives difficulty, splits into final and flagged | | 4. Merge | Resolves votes, derives difficulty, splits into final and flagged |
| 5. Compare | Generates COVERAGE.md with detailed quality report | | 5. Compare | Generates COVERAGE.md with detailed quality report |
### 1. Extract ### 1. Extract
@ -137,11 +137,11 @@ Each record in the output looks like this:
"fr": ["comptable"] "fr": ["comptable"]
}, },
"glosses": { "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": { "examples": { "en": ["able to swim", "she was able to program her computer"] }
"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` **Input:** `stage-1-extract/output/omw.json` + `stage-2-annotate/sources/cefr/{lang}.json`
**Output:** **Output:**
- `stage-2-annotate/output/{lang}.json` — one per language - `stage-2-annotate/output/{lang}.json` — one per language
- `stage-2-annotate/output/conflicts.json` — cross-language conflicts for review - `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"], "es": ["capaz"],
"fr": ["comptable"] "fr": ["comptable"]
}, },
"glosses": { "glosses": { "en": ["having the necessary means or skill to do something"] },
"en": ["having the necessary means or skill to do something"]
},
"examples": { "examples": {
"en": [ "en": [
{ "text": "able to swim", "source": "omw" }, { "text": "able to swim", "source": "omw" },
{ "text": "She was able to finish the task.", "source": "cefr" } { "text": "She was able to finish the task.", "source": "cefr" }
] ]
}, },
"votes": { "votes": { "en": { "able": { "cefr_source": "B1" } } }
"en": {
"able": { "cefr_source": "B1" }
}
}
} }
``` ```
@ -297,9 +292,7 @@ Each record in the votes file looks like this:
} }
}, },
"examples": { "examples": {
"en": [ "en": [{ "text": "the dog barked at the stranger", "source": "omw" }],
{ "text": "the dog barked at the stranger", "source": "omw" }
],
"fr": { "fr": {
"candidates": [ "candidates": [
{ "text": "le chien a aboyé", "source": "model_1" }, { "text": "le chien a aboyé", "source": "model_1" },
@ -311,8 +304,14 @@ Each record in the votes file looks like this:
"descriptions": { "descriptions": {
"en": { "en": {
"candidates": [ "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 } "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:** **Difficulty mapping:**
| CEFR | Difficulty | | CEFR | Difficulty |
|---|---| | ------ | ------------ |
| A1, A2 | easy | | A1, A2 | easy |
| B1, B2 | intermediate | | B1, B2 | intermediate |
| C1, C2 | hard | | C1, C2 | hard |
**Input:** `stage-3-enrich/output/votes/{lang}_votes.json` **Input:** `stage-3-enrich/output/votes/{lang}_votes.json`
**Output:** **Output:**
- `stage-4-merge/output/final/{lang}.json` — fully resolved, ready for seeding - `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 - `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": "dog", "cefr_level": "A1", "difficulty": "easy" },
{ "text": "canine", "cefr_level": "B2", "difficulty": "intermediate" } { "text": "canine", "cefr_level": "B2", "difficulty": "intermediate" }
], ],
"it": [ "it": [{ "text": "cane", "cefr_level": "A1", "difficulty": "easy" }]
{ "text": "cane", "cefr_level": "A1", "difficulty": "easy" }
]
}, },
"glosses": { "glosses": {
"en": { "text": "a domesticated carnivorous mammal", "source": "omw" }, "en": { "text": "a domesticated carnivorous mammal", "source": "omw" },
"fr": { "text": "un mammifère carnivore domestiqué", "source": "model_1" } "fr": { "text": "un mammifère carnivore domestiqué", "source": "model_1" }
}, },
"examples": { "examples": {
"en": [ "en": [{ "text": "the dog barked at the stranger", "source": "omw" }],
{ "text": "the dog barked at the stranger", "source": "omw" } "fr": [{ "text": "le chien a aboyé", "source": "model_1" }]
],
"fr": [
{ "text": "le chien a aboyé", "source": "model_1" }
]
}, },
"descriptions": { "descriptions": {
"en": { "en": {
@ -400,6 +394,7 @@ output quality per language. Run this after merge to verify output before
seeding the database. seeding the database.
**Input:** **Input:**
- `stage-4-merge/output/final/{lang}.json` - `stage-4-merge/output/final/{lang}.json`
- `stage-4-merge/output/flagged/{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. 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 | | Constant | Values |
|---|---| | --------------- | ------------------------------------- |
| Languages | `en`, `it`, `de`, `es`, `fr` | | Languages | `en`, `it`, `de`, `es`, `fr` |
| Parts of speech | `noun`, `verb`, `adjective`, `adverb` | | Parts of speech | `noun`, `verb`, `adjective`, `adverb` |
| CEFR levels | `A1`, `A2`, `B1`, `B2`, `C1`, `C2` | | CEFR levels | `A1`, `A2`, `B1`, `B2`, `C1`, `C2` |
| Difficulty | `easy`, `intermediate`, `hard` | | 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. 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.

View file

@ -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) ### Secrets (stored in Forgejo repo settings → Actions → Secrets)
| Secret | Value | | Secret | Value |
|---|---| | ----------------- | ----------------------------------------- |
| REGISTRY_USER | Forgejo username | | REGISTRY_USER | Forgejo username |
| REGISTRY_PASSWORD | Forgejo password | | REGISTRY_PASSWORD | Forgejo password |
| SSH_PRIVATE_KEY | Contents of `~/.ssh/ci-runner` on the VPS | | SSH_PRIVATE_KEY | Contents of `~/.ssh/ci-runner` on the VPS |
| SSH_HOST | VPS IP address | | SSH_HOST | VPS IP address |
| SSH_USER | `lila` | | SSH_USER | `lila` |
### Runner Configuration ### Runner Configuration

View file

@ -9,12 +9,12 @@ and production scripts.
## Hardware (dev machine) ## Hardware (dev machine)
| Component | Spec | | Component | Spec |
|---|---| | --------- | --------------------------------------------------------------- |
| CPU | Intel Core i7-6500U (2 cores / 4 threads @ 3.10 GHz) | | CPU | Intel Core i7-6500U (2 cores / 4 threads @ 3.10 GHz) |
| RAM | 8 GB | | RAM | 8 GB |
| GPU | NVIDIA GeForce GTX 950M — 4 GB VRAM (Maxwell, CUDA compute 5.0) | | GPU | NVIDIA GeForce GTX 950M — 4 GB VRAM (Maxwell, CUDA compute 5.0) |
| OS | Debian GNU/Linux 13 (trixie) x86_64 | | OS | Debian GNU/Linux 13 (trixie) x86_64 |
**Local inference verdict:** viable for small/quantized models, not for **Local inference verdict:** viable for small/quantized models, not for
production runs. See the [Local inference](#local-inference-llamacpp) section 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 except Anthropic expose an OpenAI-compatible API, so the same client code
works across all of them — only `baseURL`, `apiKey`, and `model` change. works across all of them — only `baseURL`, `apiKey`, and `model` change.
| Provider | Use case | Cost | Rate limits | | Provider | Use case | Cost | Rate limits |
|---|---|---|---| | ---------------------- | --------------------------------------------- | ------------------ | ---------------------- |
| llama.cpp (local) | Quality testing, overnight dev runs | Free (electricity) | None | | 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 (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 | | OpenRouter (paid) | Production runs if local quality insufficient | Pay-per-token | None |
| Anthropic API | Quality baseline / reference | Pay-per-token | Standard | | 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): Practical estimates for this hardware (~3.5 GB VRAM usable after drivers):
| Model size | Q4 VRAM | Mode | Est. speed | | Model size | Q4 VRAM | Mode | Est. speed |
|---|---|---|---| | ---------- | ------- | ----------------------------- | ------------ |
| 3B | ~2.0 GB | Full GPU | ~1520 tok/s | | 3B | ~2.0 GB | Full GPU | ~1520 tok/s |
| 4B | ~2.5 GB | Full GPU | ~1218 tok/s | | 4B | ~2.5 GB | Full GPU | ~1218 tok/s |
| 7B | ~4.5 GB | Hybrid (~26/32 layers on GPU) | ~812 tok/s | | 7B | ~4.5 GB | Hybrid (~26/32 layers on GPU) | ~812 tok/s |
| 13B+ | ~8 GB+ | CPU-heavy hybrid | too slow | | 13B+ | ~8 GB+ | CPU-heavy hybrid | too slow |
### Recommended local models ### Recommended local models
@ -71,6 +71,7 @@ Two candidates worth testing, covering different points on the size/quality
tradeoff: tradeoff:
**Gemma 4 E4B Instruct (Q4 / UD-Q4_K_XL)** **Gemma 4 E4B Instruct (Q4 / UD-Q4_K_XL)**
- GGUF file: `gemma-4-E4B-it-UD-Q4_K_XL.gguf` (~2.5 GB) - GGUF file: `gemma-4-E4B-it-UD-Q4_K_XL.gguf` (~2.5 GB)
- Source: https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF - Source: https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF
- Runs fully on GPU. Brand new (April 2025), built for edge hardware, 140+ - Runs fully on GPU. Brand new (April 2025), built for edge hardware, 140+
@ -78,6 +79,7 @@ tradeoff:
to test. to test.
**Qwen2.5 7B Instruct (Q4_K_M)** **Qwen2.5 7B Instruct (Q4_K_M)**
- GGUF file: `Qwen2.5-7B-Instruct-Q4_K_M.gguf` (~4.5 GB) - GGUF file: `Qwen2.5-7B-Instruct-Q4_K_M.gguf` (~4.5 GB)
- Source: https://huggingface.co/Qwen/Qwen2.5-7B-Instruct-GGUF - Source: https://huggingface.co/Qwen/Qwen2.5-7B-Instruct-GGUF
- Runs in hybrid mode (~26 of 32 layers on GPU, rest on CPU), ~812 tok/s. - Runs in hybrid mode (~26 of 32 layers on GPU, rest on CPU), ~812 tok/s.
@ -107,6 +109,7 @@ wget -O models/qwen2.5-3b-instruct-q4_k_m.gguf \
### Starting the server ### Starting the server
**Gemma 4 E4B** (full GPU): **Gemma 4 E4B** (full GPU):
```bash ```bash
./build/bin/llama-server \ ./build/bin/llama-server \
--model models/gemma-4-e4b-it-ud-q4_k_xl.gguf \ --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): **Qwen2.5 7B** (hybrid — tune `--n-gpu-layers` to fit your VRAM):
```bash ```bash
./build/bin/llama-server \ ./build/bin/llama-server \
--model models/qwen2.5-7b-instruct-q4_k_m.gguf \ --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: Ranked by expected multilingual generation quality for en/it/de/fr/es:
| Model ID | Params | Notes | | 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-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. | | `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. | | `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. | | `google/gemma-4-31b-it:free` | 31B | 140+ language support, good European language coverage. |
| `zhipuai/glm-4.5-air:free` | MoE | Multilingual-focused. | | `zhipuai/glm-4.5-air:free` | MoE | Multilingual-focused. |
**Skip for this pipeline:** **Skip for this pipeline:**
- Llama models — weaker European language generation than Qwen/Gemma - Llama models — weaker European language generation than Qwen/Gemma
- Mistral free tier — requests may be used for model training - Mistral free tier — requests may be used for model training
@ -194,7 +199,7 @@ change this object and re-run.
// config.ts // config.ts
export type ProviderConfig = { export type ProviderConfig = {
name: string; // used for output folder naming name: string; // used for output folder naming
baseURL: string; baseURL: string;
apiKey: string; apiKey: string;
model: string; model: string;
@ -205,8 +210,8 @@ export type ProviderConfig = {
export const LOCAL_QWEN3B: ProviderConfig = { export const LOCAL_QWEN3B: ProviderConfig = {
name: "local-qwen2.5-3b", name: "local-qwen2.5-3b",
baseURL: "http://127.0.0.1:8080/v1", baseURL: "http://127.0.0.1:8080/v1",
apiKey: "none", // llama.cpp ignores this apiKey: "none", // llama.cpp ignores this
model: "qwen2.5-3b", // llama.cpp ignores model name, uses loaded model model: "qwen2.5-3b", // llama.cpp ignores model name, uses loaded model
maxTokens: 512, maxTokens: 512,
}; };
@ -231,7 +236,7 @@ export const OR_GEMMA4_31B: ProviderConfig = {
// Anthropic (reference baseline — different adapter required) // Anthropic (reference baseline — different adapter required)
export const ANTHROPIC_SONNET: ProviderConfig = { export const ANTHROPIC_SONNET: ProviderConfig = {
name: "anthropic-sonnet", 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!, apiKey: process.env.ANTHROPIC_API_KEY!,
model: "claude-sonnet-4-6", model: "claude-sonnet-4-6",
maxTokens: 512, maxTokens: 512,
@ -239,6 +244,7 @@ export const ANTHROPIC_SONNET: ProviderConfig = {
``` ```
Output from each run lands in: Output from each run lands in:
``` ```
stage-3-enrich/test/output/{provider.name}/results.json stage-3-enrich/test/output/{provider.name}/results.json
stage-3-enrich/test/output/{provider.name}/metrics.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: The test script measures the following per provider run:
| Metric | What it measures | | 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% | | **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) | | **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. | | **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 | | **Language correctness** | Manual spot-check only — automated detection not reliable enough |
| **Tokens/second** | Local only. Indicates overnight run feasibility | | **Tokens/second** | Local only. Indicates overnight run feasibility |
### Decision thresholds ### Decision thresholds
| Metric | Threshold | Action if below | | Metric | Threshold | Action if below |
|---|---|---| | --------------- | --------- | ---------------------------------------------- |
| JSON parse rate | < 97% | Do not use this model for production | | JSON parse rate | < 97% | Do not use this model for production |
| Field coverage | < 95% | Prompt needs revision before production | | Field coverage | < 95% | Prompt needs revision before production |
| CEFR agreement | < 70% | Model lacks vocabulary knowledge for this task | | CEFR agreement | < 70% | Model lacks vocabulary knowledge for this task |
--- ---

View file

@ -1,6 +1,5 @@
# notes # notes
## prompt ## prompt
ive attached the readme of my project. this is my current task: 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 ### vps setup
- monitoring and logging (eg via chrootkit or rkhunter, logwatch/monit => mails daily with summary) - 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 - ~~keep the vps clean (e.g. old docker images/containers)~~ ✅ CI/CD pipeline runs `docker image prune -f` after deploy
### ~~cd/ci pipeline~~ ✅ RESOLVED ### ~~cd/ci pipeline~~ ✅ RESOLVED
@ -55,9 +54,9 @@ Forgejo Actions with runner on VPS, Forgejo built-in container registry. See `de
### ~~postgres backups~~ ✅ RESOLVED ### ~~postgres backups~~ ✅ RESOLVED
Daily pg_dump cron job, 7-day retention, dev laptop auto-sync via rsync. See `deployment.md`. # Daily pg_dump cron job, 7-day retention, dev laptop auto-sync via rsync. See `deployment.md`.
=======
>>>>>>> dev > > > > > > > dev
### try now option ### try now option

View file

@ -61,10 +61,12 @@ export const evaluateAnswer = async (
store: GameSessionStore, store: GameSessionStore,
): Promise<AnswerResult> => { ): Promise<AnswerResult> => {
const session = await store.get(submission.sessionId); 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); 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 // delete answered question; delete session when all questions are answered
session.answers.delete(submission.questionId); session.answers.delete(submission.questionId);
@ -84,10 +86,14 @@ export const evaluateAnswer = async (
```ts ```ts
// ✅ option B — TTL in InMemoryGameSessionStore // ✅ option B — TTL in InMemoryGameSessionStore
export class InMemoryGameSessionStore implements GameSessionStore { 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; private readonly ttlMs: number;
constructor(ttlMs = 30 * 60 * 1000) { // 30 minutes default constructor(ttlMs = 30 * 60 * 1000) {
// 30 minutes default
this.ttlMs = ttlMs; this.ttlMs = ttlMs;
} }
@ -115,15 +121,13 @@ export class InMemoryGameSessionStore implements GameSessionStore {
--- ---
**Problem** **Problem**
`GameRequest.rounds` is typed as `string` in `@lila/shared`, forcing the service to cast it every time: `GameRequest.rounds` is typed as `string` in `@lila/shared`, forcing the service to cast it every time:
```ts ```ts
// ❌ why is a round count a string? // ❌ why is a round count a string?
Number(request.rounds) Number(request.rounds);
``` ```
**Fix — fix the schema in `@lila/shared`** **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 // simulate getDistractors returning the correct answer as one of the distractors
mockGetDistractors.mockResolvedValueOnce(["cane", "wrong2", "wrong3"]); 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 question = session.questions[0]!;
const optionTexts = question.options.map((o) => o.text); const optionTexts = question.options.map((o) => o.text);
@ -285,16 +292,14 @@ his `sessionId`.
```ts ```ts
// GameSessionStore.ts // GameSessionStore.ts
export type GameSessionData = { export type GameSessionData = { answers: Map<string, number>; userId: string };
answers: Map<string, number>;
userId: string;
};
// evaluateAnswer // evaluateAnswer
const session = await store.get(submission.sessionId); const session = await store.get(submission.sessionId);
if (!session) throw new NotFoundError(`Game session not found`); 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 // ^^^ 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 () => { it("throws when getGameTerms returns no terms", async () => {
mockGetGameTerms.mockResolvedValue([]); mockGetGameTerms.mockResolvedValue([]);
await expect(createGameSession(validRequest, new InMemoryGameSessionStore())) await expect(
.rejects.toThrow("No terms found"); 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 () => { it("propagates getDistractors failure", async () => {
mockGetDistractors.mockRejectedValue(new Error("db timeout")); mockGetDistractors.mockRejectedValue(new Error("db timeout"));
await expect(createGameSession(validRequest, new InMemoryGameSessionStore())) await expect(
.rejects.toThrow("db timeout"); createGameSession(validRequest, new InMemoryGameSessionStore()),
).rejects.toThrow("db timeout");
}); });
``` ```

View file

@ -51,9 +51,9 @@ This is the full vision. The current implementation already covers most of it; r
### What is CUT from the MVP ### What is CUT from the MVP
| Feature | Why cut | | Feature | Why cut |
| ------------------------------- | -------------------------------------- | | --------------------- | ---------- |
| User stats / profiles | Needs auth | | 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). 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. The monorepo structure and tooling are already set up. This is the full stack.
| Layer | Technology | Status | | Layer | Technology | Status |
| ------------ | ------------------------------ | ----------- | | ------------ | ------------------------------ | ------------------------------------------------------ |
| Monorepo | pnpm workspaces | ✅ | | Monorepo | pnpm workspaces | ✅ |
| Frontend | React 18, Vite, TypeScript | ✅ | | Frontend | React 18, Vite, TypeScript | ✅ |
| Routing | TanStack Router | ✅ | | Routing | TanStack Router | ✅ |
| Server state | TanStack Query | ✅ | | Server state | TanStack Query | ✅ |
| Client state | Zustand | ✅ | | Client state | Zustand | ✅ |
| Styling | Tailwind CSS + shadcn/ui | ✅ | | Styling | Tailwind CSS + shadcn/ui | ✅ |
| Backend | Node.js, Express, TypeScript | ✅ | | Backend | Node.js, Express, TypeScript | ✅ |
| Database | PostgreSQL + Drizzle ORM | ✅ | | Database | PostgreSQL + Drizzle ORM | ✅ |
| Validation | Zod (shared schemas) | ✅ | | Validation | Zod (shared schemas) | ✅ |
| Testing | Vitest, supertest | ✅ | | Testing | Vitest, supertest | ✅ |
| Auth | Better Auth (Google + GitHub) | ✅ | | Auth | Better Auth (Google + GitHub) | ✅ |
| Deployment | Docker Compose, Caddy, Hetzner | ✅ | | Deployment | Docker Compose, Caddy, Hetzner | ✅ |
| CI/CD | Forgejo Actions | ✅ | | CI/CD | Forgejo Actions | ✅ |
| Realtime | WebSockets (`ws` library) | ✅ | | Realtime | WebSockets (`ws` library) | ✅ |
| Cache | Valkey | ⚠️ optional (used locally; production/state hardening) | | 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 ## 11. Post-MVP Ladder
<<<<<<< HEAD <<<<<<< HEAD
| Phase | What it adds | Status | | Phase | What it adds | Status |
| ----------------- | ------------------------------------------------------------------------------- | ------ | | ----------------- | ------------------------------------------------------------------------------- | ------ |
| Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ | | Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ |
| Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ | | Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ |
| Hardening (partial) | CI/CD pipeline, DB backups | ✅ | | Hardening (partial) | CI/CD pipeline, DB backups | ✅ |
| User Stats | Games played, score history, profile page | ❌ | | User Stats | Games played, score history, profile page | ❌ |
| Multiplayer Lobby | Room creation, join by code, WebSocket connection | ❌ | | Multiplayer Lobby | Room creation, join by code, WebSocket connection | ❌ |
| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ❌ | | Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ❌ |
| Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ | | 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 | ✅ | | Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ |
| Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ | | Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ |
| Hardening (partial) | CI/CD pipeline, DB backups | ✅ | | Hardening (partial) | CI/CD pipeline, DB backups | ✅ |
| User Stats | Games played, score history, profile page | ❌ | | User Stats | Games played, score history, profile page | ❌ |
| Multiplayer Lobby | Room creation, join by code, WebSocket connection | ✅ | | Multiplayer Lobby | Room creation, join by code, WebSocket connection | ✅ |
| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ✅ | | Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ✅ |
| Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ | | Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ |
>>>>>>> dev
> > > > > > > dev
### Future Data Model Extensions (deferred, additive) ### Future Data Model Extensions (deferred, additive)

View file

@ -1,6 +1,6 @@
# Ticket Blueprint # 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. decision between options was made.
--- ---

View file

@ -87,9 +87,7 @@ pass init <your-key-id>
Replace the entire file contents with: Replace the entire file contents with:
```json ```json
{ { "credsStore": "pass" }
"credsStore": "pass"
}
``` ```
### 6. Re-login to registries ### 6. Re-login to registries

View file

@ -136,7 +136,7 @@ Rejected because: coercion is for untrusted or uncontrolled inputs (form fields,
6. In `apps/web/src/components/game/GameSetup.tsx`: 6. In `apps/web/src/components/game/GameSetup.tsx`:
- Update `SettingGroup` props to accept `string | number`: - Update `SettingGroup` props to accept `string | number`:
```ts ```ts
type SettingGroupProps = { type SettingGroupProps = {
options: readonly (string | number)[]; options: readonly (string | number)[];

View file

@ -110,12 +110,8 @@
"name": "account_user_id_user_id_fk", "name": "account_user_id_user_id_fk",
"tableFrom": "account", "tableFrom": "account",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -149,12 +145,8 @@
"name": "deck_terms_deck_id_decks_id_fk", "name": "deck_terms_deck_id_decks_id_fk",
"tableFrom": "deck_terms", "tableFrom": "deck_terms",
"tableTo": "decks", "tableTo": "decks",
"columnsFrom": [ "columnsFrom": ["deck_id"],
"deck_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@ -162,12 +154,8 @@
"name": "deck_terms_term_id_terms_id_fk", "name": "deck_terms_term_id_terms_id_fk",
"tableFrom": "deck_terms", "tableFrom": "deck_terms",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -175,10 +163,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"deck_terms_deck_id_term_id_pk": { "deck_terms_deck_id_term_id_pk": {
"name": "deck_terms_deck_id_term_id_pk", "name": "deck_terms_deck_id_term_id_pk",
"columns": [ "columns": ["deck_id", "term_id"]
"deck_id",
"term_id"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@ -265,10 +250,7 @@
"unique_deck_name": { "unique_deck_name": {
"name": "unique_deck_name", "name": "unique_deck_name",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["name", "source_language"]
"name",
"source_language"
]
} }
}, },
"policies": {}, "policies": {},
@ -336,12 +318,8 @@
"name": "lobbies_host_user_id_user_id_fk", "name": "lobbies_host_user_id_user_id_fk",
"tableFrom": "lobbies", "tableFrom": "lobbies",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["host_user_id"],
"host_user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -351,9 +329,7 @@
"lobbies_code_unique": { "lobbies_code_unique": {
"name": "lobbies_code_unique", "name": "lobbies_code_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["code"]
"code"
]
} }
}, },
"policies": {}, "policies": {},
@ -402,12 +378,8 @@
"name": "lobby_players_lobby_id_lobbies_id_fk", "name": "lobby_players_lobby_id_lobbies_id_fk",
"tableFrom": "lobby_players", "tableFrom": "lobby_players",
"tableTo": "lobbies", "tableTo": "lobbies",
"columnsFrom": [ "columnsFrom": ["lobby_id"],
"lobby_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@ -415,12 +387,8 @@
"name": "lobby_players_user_id_user_id_fk", "name": "lobby_players_user_id_user_id_fk",
"tableFrom": "lobby_players", "tableFrom": "lobby_players",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -428,10 +396,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"lobby_players_lobby_id_user_id_pk": { "lobby_players_lobby_id_user_id_pk": {
"name": "lobby_players_lobby_id_user_id_pk", "name": "lobby_players_lobby_id_user_id_pk",
"columns": [ "columns": ["lobby_id", "user_id"]
"lobby_id",
"user_id"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@ -515,12 +480,8 @@
"name": "session_user_id_user_id_fk", "name": "session_user_id_user_id_fk",
"tableFrom": "session", "tableFrom": "session",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -530,9 +491,7 @@
"session_token_unique": { "session_token_unique": {
"name": "session_token_unique", "name": "session_token_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["token"]
"token"
]
} }
}, },
"policies": {}, "policies": {},
@ -588,12 +547,8 @@
"name": "term_glosses_term_id_terms_id_fk", "name": "term_glosses_term_id_terms_id_fk",
"tableFrom": "term_glosses", "tableFrom": "term_glosses",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -603,10 +558,7 @@
"unique_term_gloss": { "unique_term_gloss": {
"name": "unique_term_gloss", "name": "unique_term_gloss",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["term_id", "language_code"]
"term_id",
"language_code"
]
} }
}, },
"policies": {}, "policies": {},
@ -641,12 +593,8 @@
"name": "term_topics_term_id_terms_id_fk", "name": "term_topics_term_id_terms_id_fk",
"tableFrom": "term_topics", "tableFrom": "term_topics",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@ -654,12 +602,8 @@
"name": "term_topics_topic_id_topics_id_fk", "name": "term_topics_topic_id_topics_id_fk",
"tableFrom": "term_topics", "tableFrom": "term_topics",
"tableTo": "topics", "tableTo": "topics",
"columnsFrom": [ "columnsFrom": ["topic_id"],
"topic_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -667,10 +611,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"term_topics_term_id_topic_id_pk": { "term_topics_term_id_topic_id_pk": {
"name": "term_topics_term_id_topic_id_pk", "name": "term_topics_term_id_topic_id_pk",
"columns": [ "columns": ["term_id", "topic_id"]
"term_id",
"topic_id"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@ -744,10 +685,7 @@
"unique_source_id": { "unique_source_id": {
"name": "unique_source_id", "name": "unique_source_id",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["source", "source_id"]
"source",
"source_id"
]
} }
}, },
"policies": {}, "policies": {},
@ -803,9 +741,7 @@
"topics_slug_unique": { "topics_slug_unique": {
"name": "topics_slug_unique", "name": "topics_slug_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["slug"]
"slug"
]
} }
}, },
"policies": {}, "policies": {},
@ -901,12 +837,8 @@
"name": "translations_term_id_terms_id_fk", "name": "translations_term_id_terms_id_fk",
"tableFrom": "translations", "tableFrom": "translations",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -916,11 +848,7 @@
"unique_translations": { "unique_translations": {
"name": "unique_translations", "name": "unique_translations",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["term_id", "language_code", "text"]
"term_id",
"language_code",
"text"
]
} }
}, },
"policies": {}, "policies": {},
@ -997,9 +925,7 @@
"user_email_unique": { "user_email_unique": {
"name": "user_email_unique", "name": "user_email_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["email"]
"email"
]
} }
}, },
"policies": {}, "policies": {},
@ -1080,9 +1006,5 @@
"roles": {}, "roles": {},
"policies": {}, "policies": {},
"views": {}, "views": {},
"_meta": { "_meta": { "columns": {}, "schemas": {}, "tables": {} }
"columns": {}, }
"schemas": {},
"tables": {}
}
}

View file

@ -110,12 +110,8 @@
"name": "account_user_id_user_id_fk", "name": "account_user_id_user_id_fk",
"tableFrom": "account", "tableFrom": "account",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -149,12 +145,8 @@
"name": "deck_terms_deck_id_decks_id_fk", "name": "deck_terms_deck_id_decks_id_fk",
"tableFrom": "deck_terms", "tableFrom": "deck_terms",
"tableTo": "decks", "tableTo": "decks",
"columnsFrom": [ "columnsFrom": ["deck_id"],
"deck_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@ -162,12 +154,8 @@
"name": "deck_terms_term_id_terms_id_fk", "name": "deck_terms_term_id_terms_id_fk",
"tableFrom": "deck_terms", "tableFrom": "deck_terms",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -175,10 +163,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"deck_terms_deck_id_term_id_pk": { "deck_terms_deck_id_term_id_pk": {
"name": "deck_terms_deck_id_term_id_pk", "name": "deck_terms_deck_id_term_id_pk",
"columns": [ "columns": ["deck_id", "term_id"]
"deck_id",
"term_id"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@ -265,10 +250,7 @@
"unique_deck_name": { "unique_deck_name": {
"name": "unique_deck_name", "name": "unique_deck_name",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["name", "source_language"]
"name",
"source_language"
]
} }
}, },
"policies": {}, "policies": {},
@ -336,12 +318,8 @@
"name": "lobbies_host_user_id_user_id_fk", "name": "lobbies_host_user_id_user_id_fk",
"tableFrom": "lobbies", "tableFrom": "lobbies",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["host_user_id"],
"host_user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -351,9 +329,7 @@
"lobbies_code_unique": { "lobbies_code_unique": {
"name": "lobbies_code_unique", "name": "lobbies_code_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["code"]
"code"
]
} }
}, },
"policies": {}, "policies": {},
@ -402,12 +378,8 @@
"name": "lobby_players_lobby_id_lobbies_id_fk", "name": "lobby_players_lobby_id_lobbies_id_fk",
"tableFrom": "lobby_players", "tableFrom": "lobby_players",
"tableTo": "lobbies", "tableTo": "lobbies",
"columnsFrom": [ "columnsFrom": ["lobby_id"],
"lobby_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@ -415,12 +387,8 @@
"name": "lobby_players_user_id_user_id_fk", "name": "lobby_players_user_id_user_id_fk",
"tableFrom": "lobby_players", "tableFrom": "lobby_players",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -428,10 +396,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"lobby_players_lobby_id_user_id_pk": { "lobby_players_lobby_id_user_id_pk": {
"name": "lobby_players_lobby_id_user_id_pk", "name": "lobby_players_lobby_id_user_id_pk",
"columns": [ "columns": ["lobby_id", "user_id"]
"lobby_id",
"user_id"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@ -515,12 +480,8 @@
"name": "session_user_id_user_id_fk", "name": "session_user_id_user_id_fk",
"tableFrom": "session", "tableFrom": "session",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -530,9 +491,7 @@
"session_token_unique": { "session_token_unique": {
"name": "session_token_unique", "name": "session_token_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["token"]
"token"
]
} }
}, },
"policies": {}, "policies": {},
@ -604,12 +563,8 @@
"name": "term_examples_term_id_terms_id_fk", "name": "term_examples_term_id_terms_id_fk",
"tableFrom": "term_examples", "tableFrom": "term_examples",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -619,11 +574,7 @@
"unique_term_example": { "unique_term_example": {
"name": "unique_term_example", "name": "unique_term_example",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["term_id", "language_code", "text"]
"term_id",
"language_code",
"text"
]
} }
}, },
"policies": {}, "policies": {},
@ -684,12 +635,8 @@
"name": "term_glosses_term_id_terms_id_fk", "name": "term_glosses_term_id_terms_id_fk",
"tableFrom": "term_glosses", "tableFrom": "term_glosses",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -699,10 +646,7 @@
"unique_term_gloss": { "unique_term_gloss": {
"name": "unique_term_gloss", "name": "unique_term_gloss",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["term_id", "language_code"]
"term_id",
"language_code"
]
} }
}, },
"policies": {}, "policies": {},
@ -737,12 +681,8 @@
"name": "term_topics_term_id_terms_id_fk", "name": "term_topics_term_id_terms_id_fk",
"tableFrom": "term_topics", "tableFrom": "term_topics",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@ -750,12 +690,8 @@
"name": "term_topics_topic_id_topics_id_fk", "name": "term_topics_topic_id_topics_id_fk",
"tableFrom": "term_topics", "tableFrom": "term_topics",
"tableTo": "topics", "tableTo": "topics",
"columnsFrom": [ "columnsFrom": ["topic_id"],
"topic_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -763,10 +699,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"term_topics_term_id_topic_id_pk": { "term_topics_term_id_topic_id_pk": {
"name": "term_topics_term_id_topic_id_pk", "name": "term_topics_term_id_topic_id_pk",
"columns": [ "columns": ["term_id", "topic_id"]
"term_id",
"topic_id"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@ -840,10 +773,7 @@
"unique_source_id": { "unique_source_id": {
"name": "unique_source_id", "name": "unique_source_id",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["source", "source_id"]
"source",
"source_id"
]
} }
}, },
"policies": {}, "policies": {},
@ -899,9 +829,7 @@
"topics_slug_unique": { "topics_slug_unique": {
"name": "topics_slug_unique", "name": "topics_slug_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["slug"]
"slug"
]
} }
}, },
"policies": {}, "policies": {},
@ -997,12 +925,8 @@
"name": "translations_term_id_terms_id_fk", "name": "translations_term_id_terms_id_fk",
"tableFrom": "translations", "tableFrom": "translations",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -1012,11 +936,7 @@
"unique_translations": { "unique_translations": {
"name": "unique_translations", "name": "unique_translations",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["term_id", "language_code", "text"]
"term_id",
"language_code",
"text"
]
} }
}, },
"policies": {}, "policies": {},
@ -1093,9 +1013,7 @@
"user_email_unique": { "user_email_unique": {
"name": "user_email_unique", "name": "user_email_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["email"]
"email"
]
} }
}, },
"policies": {}, "policies": {},
@ -1176,9 +1094,5 @@
"roles": {}, "roles": {},
"policies": {}, "policies": {},
"views": {}, "views": {},
"_meta": { "_meta": { "columns": {}, "schemas": {}, "tables": {} }
"columns": {}, }
"schemas": {},
"tables": {}
}
}

View file

@ -110,12 +110,8 @@
"name": "account_user_id_user_id_fk", "name": "account_user_id_user_id_fk",
"tableFrom": "account", "tableFrom": "account",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -149,12 +145,8 @@
"name": "deck_terms_deck_id_decks_id_fk", "name": "deck_terms_deck_id_decks_id_fk",
"tableFrom": "deck_terms", "tableFrom": "deck_terms",
"tableTo": "decks", "tableTo": "decks",
"columnsFrom": [ "columnsFrom": ["deck_id"],
"deck_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@ -162,12 +154,8 @@
"name": "deck_terms_term_id_terms_id_fk", "name": "deck_terms_term_id_terms_id_fk",
"tableFrom": "deck_terms", "tableFrom": "deck_terms",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -175,10 +163,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"deck_terms_deck_id_term_id_pk": { "deck_terms_deck_id_term_id_pk": {
"name": "deck_terms_deck_id_term_id_pk", "name": "deck_terms_deck_id_term_id_pk",
"columns": [ "columns": ["deck_id", "term_id"]
"deck_id",
"term_id"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@ -265,10 +250,7 @@
"unique_deck_name": { "unique_deck_name": {
"name": "unique_deck_name", "name": "unique_deck_name",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["name", "source_language"]
"name",
"source_language"
]
} }
}, },
"policies": {}, "policies": {},
@ -355,12 +337,8 @@
"name": "lobbies_host_user_id_user_id_fk", "name": "lobbies_host_user_id_user_id_fk",
"tableFrom": "lobbies", "tableFrom": "lobbies",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["host_user_id"],
"host_user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -370,9 +348,7 @@
"lobbies_code_unique": { "lobbies_code_unique": {
"name": "lobbies_code_unique", "name": "lobbies_code_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["code"]
"code"
]
} }
}, },
"policies": {}, "policies": {},
@ -421,12 +397,8 @@
"name": "lobby_players_lobby_id_lobbies_id_fk", "name": "lobby_players_lobby_id_lobbies_id_fk",
"tableFrom": "lobby_players", "tableFrom": "lobby_players",
"tableTo": "lobbies", "tableTo": "lobbies",
"columnsFrom": [ "columnsFrom": ["lobby_id"],
"lobby_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@ -434,12 +406,8 @@
"name": "lobby_players_user_id_user_id_fk", "name": "lobby_players_user_id_user_id_fk",
"tableFrom": "lobby_players", "tableFrom": "lobby_players",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -447,10 +415,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"lobby_players_lobby_id_user_id_pk": { "lobby_players_lobby_id_user_id_pk": {
"name": "lobby_players_lobby_id_user_id_pk", "name": "lobby_players_lobby_id_user_id_pk",
"columns": [ "columns": ["lobby_id", "user_id"]
"lobby_id",
"user_id"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@ -534,12 +499,8 @@
"name": "session_user_id_user_id_fk", "name": "session_user_id_user_id_fk",
"tableFrom": "session", "tableFrom": "session",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -549,9 +510,7 @@
"session_token_unique": { "session_token_unique": {
"name": "session_token_unique", "name": "session_token_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["token"]
"token"
]
} }
}, },
"policies": {}, "policies": {},
@ -623,12 +582,8 @@
"name": "term_examples_term_id_terms_id_fk", "name": "term_examples_term_id_terms_id_fk",
"tableFrom": "term_examples", "tableFrom": "term_examples",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -638,11 +593,7 @@
"unique_term_example": { "unique_term_example": {
"name": "unique_term_example", "name": "unique_term_example",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["term_id", "language_code", "text"]
"term_id",
"language_code",
"text"
]
} }
}, },
"policies": {}, "policies": {},
@ -703,12 +654,8 @@
"name": "term_glosses_term_id_terms_id_fk", "name": "term_glosses_term_id_terms_id_fk",
"tableFrom": "term_glosses", "tableFrom": "term_glosses",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -718,10 +665,7 @@
"unique_term_gloss": { "unique_term_gloss": {
"name": "unique_term_gloss", "name": "unique_term_gloss",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["term_id", "language_code"]
"term_id",
"language_code"
]
} }
}, },
"policies": {}, "policies": {},
@ -756,12 +700,8 @@
"name": "term_topics_term_id_terms_id_fk", "name": "term_topics_term_id_terms_id_fk",
"tableFrom": "term_topics", "tableFrom": "term_topics",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@ -769,12 +709,8 @@
"name": "term_topics_topic_id_topics_id_fk", "name": "term_topics_topic_id_topics_id_fk",
"tableFrom": "term_topics", "tableFrom": "term_topics",
"tableTo": "topics", "tableTo": "topics",
"columnsFrom": [ "columnsFrom": ["topic_id"],
"topic_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -782,10 +718,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"term_topics_term_id_topic_id_pk": { "term_topics_term_id_topic_id_pk": {
"name": "term_topics_term_id_topic_id_pk", "name": "term_topics_term_id_topic_id_pk",
"columns": [ "columns": ["term_id", "topic_id"]
"term_id",
"topic_id"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@ -859,10 +792,7 @@
"unique_source_id": { "unique_source_id": {
"name": "unique_source_id", "name": "unique_source_id",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["source", "source_id"]
"source",
"source_id"
]
} }
}, },
"policies": {}, "policies": {},
@ -918,9 +848,7 @@
"topics_slug_unique": { "topics_slug_unique": {
"name": "topics_slug_unique", "name": "topics_slug_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["slug"]
"slug"
]
} }
}, },
"policies": {}, "policies": {},
@ -1016,12 +944,8 @@
"name": "translations_term_id_terms_id_fk", "name": "translations_term_id_terms_id_fk",
"tableFrom": "translations", "tableFrom": "translations",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -1031,11 +955,7 @@
"unique_translations": { "unique_translations": {
"name": "unique_translations", "name": "unique_translations",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["term_id", "language_code", "text"]
"term_id",
"language_code",
"text"
]
} }
}, },
"policies": {}, "policies": {},
@ -1112,9 +1032,7 @@
"user_email_unique": { "user_email_unique": {
"name": "user_email_unique", "name": "user_email_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["email"]
"email"
]
} }
}, },
"policies": {}, "policies": {},
@ -1195,9 +1113,5 @@
"roles": {}, "roles": {},
"policies": {}, "policies": {},
"views": {}, "views": {},
"_meta": { "_meta": { "columns": {}, "schemas": {}, "tables": {} }
"columns": {}, }
"schemas": {},
"tables": {}
}
}

View file

@ -110,12 +110,8 @@
"name": "account_user_id_user_id_fk", "name": "account_user_id_user_id_fk",
"tableFrom": "account", "tableFrom": "account",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -149,12 +145,8 @@
"name": "deck_terms_deck_id_decks_id_fk", "name": "deck_terms_deck_id_decks_id_fk",
"tableFrom": "deck_terms", "tableFrom": "deck_terms",
"tableTo": "decks", "tableTo": "decks",
"columnsFrom": [ "columnsFrom": ["deck_id"],
"deck_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@ -162,12 +154,8 @@
"name": "deck_terms_term_id_terms_id_fk", "name": "deck_terms_term_id_terms_id_fk",
"tableFrom": "deck_terms", "tableFrom": "deck_terms",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -175,10 +163,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"deck_terms_deck_id_term_id_pk": { "deck_terms_deck_id_term_id_pk": {
"name": "deck_terms_deck_id_term_id_pk", "name": "deck_terms_deck_id_term_id_pk",
"columns": [ "columns": ["deck_id", "term_id"]
"deck_id",
"term_id"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@ -265,10 +250,7 @@
"unique_deck_name": { "unique_deck_name": {
"name": "unique_deck_name", "name": "unique_deck_name",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["name", "source_language"]
"name",
"source_language"
]
} }
}, },
"policies": {}, "policies": {},
@ -336,12 +318,8 @@
"name": "lobbies_host_user_id_user_id_fk", "name": "lobbies_host_user_id_user_id_fk",
"tableFrom": "lobbies", "tableFrom": "lobbies",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["host_user_id"],
"host_user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -351,9 +329,7 @@
"lobbies_code_unique": { "lobbies_code_unique": {
"name": "lobbies_code_unique", "name": "lobbies_code_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["code"]
"code"
]
} }
}, },
"policies": {}, "policies": {},
@ -402,12 +378,8 @@
"name": "lobby_players_lobby_id_lobbies_id_fk", "name": "lobby_players_lobby_id_lobbies_id_fk",
"tableFrom": "lobby_players", "tableFrom": "lobby_players",
"tableTo": "lobbies", "tableTo": "lobbies",
"columnsFrom": [ "columnsFrom": ["lobby_id"],
"lobby_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@ -415,12 +387,8 @@
"name": "lobby_players_user_id_user_id_fk", "name": "lobby_players_user_id_user_id_fk",
"tableFrom": "lobby_players", "tableFrom": "lobby_players",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -428,10 +396,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"lobby_players_lobby_id_user_id_pk": { "lobby_players_lobby_id_user_id_pk": {
"name": "lobby_players_lobby_id_user_id_pk", "name": "lobby_players_lobby_id_user_id_pk",
"columns": [ "columns": ["lobby_id", "user_id"]
"lobby_id",
"user_id"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@ -515,12 +480,8 @@
"name": "session_user_id_user_id_fk", "name": "session_user_id_user_id_fk",
"tableFrom": "session", "tableFrom": "session",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -530,9 +491,7 @@
"session_token_unique": { "session_token_unique": {
"name": "session_token_unique", "name": "session_token_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["token"]
"token"
]
} }
}, },
"policies": {}, "policies": {},
@ -604,12 +563,8 @@
"name": "term_examples_term_id_terms_id_fk", "name": "term_examples_term_id_terms_id_fk",
"tableFrom": "term_examples", "tableFrom": "term_examples",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -619,11 +574,7 @@
"unique_term_example": { "unique_term_example": {
"name": "unique_term_example", "name": "unique_term_example",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["term_id", "language_code", "text"]
"term_id",
"language_code",
"text"
]
} }
}, },
"policies": {}, "policies": {},
@ -684,12 +635,8 @@
"name": "term_glosses_term_id_terms_id_fk", "name": "term_glosses_term_id_terms_id_fk",
"tableFrom": "term_glosses", "tableFrom": "term_glosses",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -699,10 +646,7 @@
"unique_term_gloss": { "unique_term_gloss": {
"name": "unique_term_gloss", "name": "unique_term_gloss",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["term_id", "language_code"]
"term_id",
"language_code"
]
} }
}, },
"policies": {}, "policies": {},
@ -737,12 +681,8 @@
"name": "term_topics_term_id_terms_id_fk", "name": "term_topics_term_id_terms_id_fk",
"tableFrom": "term_topics", "tableFrom": "term_topics",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@ -750,12 +690,8 @@
"name": "term_topics_topic_id_topics_id_fk", "name": "term_topics_topic_id_topics_id_fk",
"tableFrom": "term_topics", "tableFrom": "term_topics",
"tableTo": "topics", "tableTo": "topics",
"columnsFrom": [ "columnsFrom": ["topic_id"],
"topic_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -763,10 +699,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"term_topics_term_id_topic_id_pk": { "term_topics_term_id_topic_id_pk": {
"name": "term_topics_term_id_topic_id_pk", "name": "term_topics_term_id_topic_id_pk",
"columns": [ "columns": ["term_id", "topic_id"]
"term_id",
"topic_id"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@ -840,10 +773,7 @@
"unique_source_id": { "unique_source_id": {
"name": "unique_source_id", "name": "unique_source_id",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["source", "source_id"]
"source",
"source_id"
]
} }
}, },
"policies": {}, "policies": {},
@ -899,9 +829,7 @@
"topics_slug_unique": { "topics_slug_unique": {
"name": "topics_slug_unique", "name": "topics_slug_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["slug"]
"slug"
]
} }
}, },
"policies": {}, "policies": {},
@ -997,12 +925,8 @@
"name": "translations_term_id_terms_id_fk", "name": "translations_term_id_terms_id_fk",
"tableFrom": "translations", "tableFrom": "translations",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -1012,11 +936,7 @@
"unique_translations": { "unique_translations": {
"name": "unique_translations", "name": "unique_translations",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["term_id", "language_code", "text"]
"term_id",
"language_code",
"text"
]
} }
}, },
"policies": {}, "policies": {},
@ -1093,9 +1013,7 @@
"user_email_unique": { "user_email_unique": {
"name": "user_email_unique", "name": "user_email_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["email"]
"email"
]
} }
}, },
"policies": {}, "policies": {},
@ -1176,9 +1094,5 @@
"roles": {}, "roles": {},
"policies": {}, "policies": {},
"views": {}, "views": {},
"_meta": { "_meta": { "columns": {}, "schemas": {}, "tables": {} }
"columns": {}, }
"schemas": {},
"tables": {}
}
}

View file

@ -80,4 +80,4 @@
"breakpoints": true "breakpoints": true
} }
] ]
} }

View file

@ -5,11 +5,11 @@
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"outDir": "./dist", "outDir": "./dist",
"resolveJsonModule": true, "resolveJsonModule": true,
"types": ["vitest/globals"], "types": ["vitest/globals"]
}, },
"include": [ "include": [
"src", "src",
"vitest.config.ts", "vitest.config.ts",
"../../data-pipeline/archive/packages-db-src-old-seeding-scripts/data", "../../data-pipeline/archive/packages-db-src-old-seeding-scripts/data"
], ]
} }

View file

@ -4,7 +4,7 @@
{ "path": "./packages/db" }, { "path": "./packages/db" },
{ "path": "./apps/web" }, { "path": "./apps/web" },
{ "path": "./apps/api" }, { "path": "./apps/api" },
{ "path": "./data-pipeline" }, { "path": "./data-pipeline" }
], ],
"files": [], "files": []
} }