Compare commits

..

No commits in common. "4f59f3bc1488e072b9e58e3978c16a443223235b" and "02ccc88d242d685385ff4032a732170bbfb37131" have entirely different histories.

27 changed files with 3416 additions and 1089 deletions

View file

@ -11,7 +11,7 @@ Live at [lilastudy.com](https://lilastudy.com).
## Stack
| Layer | Technology |
| ------------ | ---------------------------------- |
|---|---|
| Monorepo | pnpm workspaces |
| Frontend | React 18, Vite, TypeScript |
| Routing | TanStack Router |
@ -157,7 +157,7 @@ pnpm --filter web test
## Roadmap
| Phase | Description | Status |
| ----- | ---------------------------------------------------------------------- | ------ |
|---|---|---|
| 0 | Foundation — monorepo, tooling, dev environment | ✅ |
| 1 | Vocabulary data pipeline + REST API | ✅ |
| 2 | Singleplayer quiz UI | ✅ |

View file

@ -1,10 +0,0 @@
export const shuffleArray = <T>(array: T[]): T[] => {
const result = [...array];
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = result[i]!;
result[i] = result[j]!;
result[j] = temp;
}
return result;
};

View file

@ -10,14 +10,13 @@ import type {
} from "@lila/shared";
import { InMemoryGameSessionStore } from "../gameSessionStore/index.js";
import { NotFoundError } from "../errors/AppError.js";
import { shuffleArray } from "../lib/utils.js";
const gameSessionStore = new InMemoryGameSessionStore();
export const createGameSession = async (
request: GameRequest,
): Promise<GameSession> => {
const terms = await getGameTerms(
const correctAnswers = await getGameTerms(
request.source_language,
request.target_language,
request.pos,
@ -28,19 +27,19 @@ export const createGameSession = async (
const answerKey = new Map<string, number>();
const questions: GameQuestion[] = await Promise.all(
terms.map(async (term) => {
correctAnswers.map(async (correctAnswer) => {
const distractorTexts = await getDistractors(
term.termId,
term.targetText,
correctAnswer.termId,
correctAnswer.targetText,
request.target_language,
request.pos,
request.difficulty,
3,
);
const optionTexts = [term.targetText, ...distractorTexts];
const shuffledTexts = shuffleArray(optionTexts);
const correctOptionId = shuffledTexts.indexOf(term.targetText);
const optionTexts = [correctAnswer.targetText, ...distractorTexts];
const shuffledTexts = shuffle(optionTexts);
const correctOptionId = shuffledTexts.indexOf(correctAnswer.targetText);
const options: AnswerOption[] = shuffledTexts.map((text, index) => ({
optionId: index,
@ -52,8 +51,8 @@ export const createGameSession = async (
return {
questionId,
prompt: term.sourceText,
gloss: term.sourceGloss,
prompt: correctAnswer.sourceText,
gloss: correctAnswer.sourceGloss,
options,
};
}),
@ -65,6 +64,17 @@ export const createGameSession = async (
return { sessionId, questions };
};
const shuffle = <T>(array: T[]): T[] => {
const result = [...array];
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = result[i]!;
result[i] = result[j]!;
result[j] = temp;
}
return result;
};
export const evaluateAnswer = async (
submission: AnswerSubmission,
): Promise<AnswerResult> => {

View file

@ -53,11 +53,7 @@ export const QuestionCard = ({
Round {questionNumber}/{totalQuestions}
</div>
<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>

View file

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

View file

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

View file

@ -108,9 +108,7 @@ function MultiplayerPage() {
{/* Join lobby */}
<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)">
Enter the code shared by your host.
</p>

File diff suppressed because it is too large Load diff

View file

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

View file

@ -56,7 +56,7 @@ 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` |
@ -103,7 +103,7 @@ 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 |
| 2. Annotate | Merges CEFR source files into extracted data, adds source file votes |
| 3. Enrich | Runs local LLMs in two rounds — generation then voting |
@ -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,7 +158,6 @@ 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
@ -178,14 +177,20 @@ 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" }
}
}
}
```
@ -292,7 +297,9 @@ 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" },
@ -304,14 +311,8 @@ 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 +335,13 @@ Reads the votes file per language and resolves the final value for every field.
**Difficulty mapping:**
| CEFR | Difficulty |
| ------ | ------------ |
|---|---|
| A1, A2 | easy |
| B1, B2 | intermediate |
| 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,15 +360,21 @@ 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": {
@ -394,7 +400,6 @@ 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`
@ -432,7 +437,7 @@ 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` |
| Parts of speech | `noun`, `verb`, `adjective`, `adverb` |
| CEFR levels | `A1`, `A2`, `B1`, `B2`, `C1`, `C2` |

View file

@ -244,7 +244,7 @@ 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 |

View file

@ -10,7 +10,7 @@ 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) |
@ -29,7 +29,7 @@ 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 |
@ -59,7 +59,7 @@ 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 | ~1520 tok/s |
| 4B | ~2.5 GB | Full GPU | ~1218 tok/s |
| 7B | ~4.5 GB | Hybrid (~26/32 layers on GPU) | ~812 tok/s |
@ -71,7 +71,6 @@ 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+
@ -79,7 +78,6 @@ 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), ~812 tok/s.
@ -109,7 +107,6 @@ 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 \
@ -120,7 +117,6 @@ 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 \
@ -168,7 +164,7 @@ 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. |
@ -176,7 +172,6 @@ Ranked by expected multilingual generation quality for en/it/de/fr/es:
| `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
@ -244,7 +239,6 @@ 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
@ -259,7 +253,7 @@ 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. |
@ -269,7 +263,7 @@ The test script measures the following per provider run:
### 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 |

View file

@ -1,5 +1,6 @@
# notes
## prompt
ive attached the readme of my project. this is my current task:
@ -45,7 +46,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
@ -54,9 +55,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

View file

@ -61,12 +61,10 @@ 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);
@ -86,14 +84,10 @@ 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;
}
@ -121,13 +115,51 @@ export class InMemoryGameSessionStore implements GameSessionStore {
---
## 3. `shuffle` is defined after it's used
**Problem**
`shuffle` is called inside `createGameSession` but defined below it. It works at runtime (module evaluation order), but reads as if the file was written top-to-bottom without a plan.
```ts
// ❌ shuffle appears after the function that calls it
export const createGameSession = async (...) => {
const shuffledTexts = shuffle(optionTexts); // used here
};
const shuffle = <T>(array: T[]): T[] => { ... }; // defined down here
```
**Fix — move helpers to the top, exports to the bottom**
```ts
// ✅ utilities first, then exported functions
const shuffle = <T>(array: T[]): T[] => {
const result = [...array];
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = result[i]!;
result[i] = result[j]!;
result[j] = temp;
}
return result;
};
export const createGameSession = async (...) => { ... };
export const evaluateAnswer = async (...) => { ... };
```
---
**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`**
@ -149,6 +181,8 @@ The `z.coerce.number()` handles the case where the value arrives as a string fro
---
## 5. `correctAnswers` is a misleading variable name
**Problem**
The variable holds `terms` — word pairs fetched from the database. Calling them `correctAnswers` jumps ahead semantically; they only become "correct answers" once options are constructed around them.
@ -208,10 +242,7 @@ 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);
@ -292,14 +323,16 @@ 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
```
@ -331,9 +364,8 @@ 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");
});
```
@ -355,9 +387,8 @@ 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");
});
```

View file

@ -52,7 +52,7 @@ 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 |
These are not deleted from the plan — they are deferred. The architecture is already designed to support them. See Section 11 (Post-MVP Ladder).
@ -64,7 +64,7 @@ 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 | ✅ |
@ -307,8 +307,7 @@ After completing a task: share the code, ask what to refactor and why. The LLM s
| 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
>>>>>>> dev
### Future Data Model Extensions (deferred, additive)

View file

@ -6,7 +6,6 @@ decision between options was made.
---
## Format A — ADR (architectural/infrastructural decisions)
Use when: you chose between options with long-term consequences.
Prefix: `adr-`
@ -15,56 +14,45 @@ Prefix: `adr-`
# ADR: <title>
## Status
Accepted | Superseded by | Deprecated
## Date
YYYY-MM-DD
## Context
What is the problem? Why does it need to be solved?
## Decision
What was chosen and why in one or two sentences.
## Options considered
### Option A — <name>
Description. Why it was chosen.
### Option B — <name>
Description. Why it was rejected.
## Consequences
- What gets better
- What gets worse or more complex
- Operational implications
- What breaks if this needs to be redone
## Affected files / machines
- List files, servers, or systems touched
## References
- Links to relevant docs
---
## Setup guide / implementation notes
Step-by-step of what was actually done.
---
## Format B — Task (features, fixes, chores)
Use when: routine task with a clear solution.
Prefix: `feat-` / `fix-` / `chore-`
@ -73,23 +61,17 @@ Prefix: `feat-` / `fix-` / `chore-`
# <prefix>: <title>
## Problem
What was wrong or missing?
## Options considered
### Option A — <name>
### Option B — <name>
## Solution
What was done and why.
## Files changed
- `path/to/file.ts`
## Commit
`<type>: <message>`

View file

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

View file

@ -1,37 +0,0 @@
# refactor: extract shuffleArray to lib/utils, rename correctAnswers to terms
## Problem
Two readability issues in `gameService.ts`:
1. `shuffle` was defined as a private function at the bottom of `gameService.ts`, after the function that calls it. It is a pure generic utility with no dependency on game domain logic, so it had no business living there.
2. The variable holding terms fetched from the database was named `correctAnswers`. These are word pairs — they only become "correct answers" once options are built around them. The name was premature and misleading.
## Options considered
### Option A — Move `shuffle` up in the same file
Simple, no new files. Fixes the ordering issue but keeps a generic utility buried in domain code.
### Option B — Extract to `lib/utils.ts`
Move `shuffle` (renamed `shuffleArray`) to `apps/api/src/lib/utils.ts` and import it. Cleaner separation: domain logic stays in services, generic utilities live in `lib/`.
Chosen because `lib/` already exists, the function is reusable, and it gives future utilities a home.
## Solution
- Created `apps/api/src/lib/utils.ts` with `shuffleArray`
- Renamed `shuffle``shuffleArray` for clarity at the call site
- Removed the inline `shuffle` from `gameService.ts` and imported from `lib/utils.ts`
- Renamed `correctAnswers``terms` and `correctAnswer``term` throughout `gameService.ts`
## Files changed
- `apps/api/src/lib/utils.ts` — created
- `apps/api/src/services/gameService.ts` — removed `shuffle`, updated import, renamed variables
## Commit
`refactor: extract shuffleArray to lib/utils, rename correctAnswers to terms`

View file

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

View file

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

View file

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

View file

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

View file

@ -5,11 +5,11 @@
"moduleResolution": "NodeNext",
"outDir": "./dist",
"resolveJsonModule": true,
"types": ["vitest/globals"]
"types": ["vitest/globals"],
},
"include": [
"src",
"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": "./apps/web" },
{ "path": "./apps/api" },
{ "path": "./data-pipeline" }
{ "path": "./data-pipeline" },
],
"files": []
"files": [],
}