From c46729f3656c6536eaf8a75d3a9555dcf168946b Mon Sep 17 00:00:00 2001 From: lila Date: Tue, 28 Apr 2026 12:32:44 +0200 Subject: [PATCH 1/3] formatting --- documentation/tickets/blueprint.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/documentation/tickets/blueprint.md b/documentation/tickets/blueprint.md index 7e612bb..8a3c065 100644 --- a/documentation/tickets/blueprint.md +++ b/documentation/tickets/blueprint.md @@ -6,6 +6,7 @@ decision between options was made. --- ## Format A — ADR (architectural/infrastructural decisions) + Use when: you chose between options with long-term consequences. Prefix: `adr-` @@ -14,45 +15,56 @@ Prefix: `adr-` # ADR: ## 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-` @@ -61,17 +73,23 @@ 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>` From 2ff7d1759e9a6bc1378f9a9f00b231b61ea04aac Mon Sep 17 00:00:00 2001 From: lila <beiweitemderbeste@protonmail.com> Date: Tue, 28 Apr 2026 13:17:24 +0200 Subject: [PATCH 2/3] refactor: extract shuffleArray to lib/utils, rename correctAnswers to terms --- apps/api/src/lib/utils.ts | 10 ++++++++ apps/api/src/services/gameService.ts | 30 ++++++++-------------- documentation/backlog.md | 4 +-- documentation/roasts/gameService.md | 38 ---------------------------- documentation/tickets/t00003.md | 37 +++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 60 deletions(-) create mode 100644 apps/api/src/lib/utils.ts create mode 100644 documentation/tickets/t00003.md diff --git a/apps/api/src/lib/utils.ts b/apps/api/src/lib/utils.ts new file mode 100644 index 0000000..4912c8c --- /dev/null +++ b/apps/api/src/lib/utils.ts @@ -0,0 +1,10 @@ +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; +}; diff --git a/apps/api/src/services/gameService.ts b/apps/api/src/services/gameService.ts index d0f0781..64f90f6 100644 --- a/apps/api/src/services/gameService.ts +++ b/apps/api/src/services/gameService.ts @@ -10,13 +10,14 @@ 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 correctAnswers = await getGameTerms( + const terms = await getGameTerms( request.source_language, request.target_language, request.pos, @@ -27,19 +28,19 @@ export const createGameSession = async ( const answerKey = new Map<string, number>(); const questions: GameQuestion[] = await Promise.all( - correctAnswers.map(async (correctAnswer) => { + terms.map(async (term) => { const distractorTexts = await getDistractors( - correctAnswer.termId, - correctAnswer.targetText, + term.termId, + term.targetText, request.target_language, request.pos, request.difficulty, 3, ); - const optionTexts = [correctAnswer.targetText, ...distractorTexts]; - const shuffledTexts = shuffle(optionTexts); - const correctOptionId = shuffledTexts.indexOf(correctAnswer.targetText); + const optionTexts = [term.targetText, ...distractorTexts]; + const shuffledTexts = shuffleArray(optionTexts); + const correctOptionId = shuffledTexts.indexOf(term.targetText); const options: AnswerOption[] = shuffledTexts.map((text, index) => ({ optionId: index, @@ -51,8 +52,8 @@ export const createGameSession = async ( return { questionId, - prompt: correctAnswer.sourceText, - gloss: correctAnswer.sourceGloss, + prompt: term.sourceText, + gloss: term.sourceGloss, options, }; }), @@ -64,17 +65,6 @@ 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> => { diff --git a/documentation/backlog.md b/documentation/backlog.md index 4e6af84..d0cb202 100644 --- a/documentation/backlog.md +++ b/documentation/backlog.md @@ -103,10 +103,10 @@ Directionally right, timing is unclear. Revisit when the next/now work is done. Shipped milestones, newest first. - **04 - 2026 - t00001 - Docker credential helper** -- **04 - 2026 - Pin dependencies in package.json** - Unpinned deps in a CI/CD pipeline are a real risk. +- **04 - 2026 - Pin dependencies in package.json** - Unpinned deps in a CI/CD pipeline are a real risk. - **04 - 2026 - React error boundaries** - Catch and display runtime errors gracefully instead of crashing the entire app. - **04 - 2026 - 404 and redirect handling** - Unknown routes return raw errors. Add a catch-all route on the frontend for client-side 404s. -- **04 - 2026 - Multiplayer GameService unit tests** - round evaluation, scoring, tie-breaking, timeout handling +- **04 - 2026 - Multiplayer GameService unit tests** - round evaluation, scoring, tie-breaking, timeout handling - **04 - 2026 - Security headers with helmet** - Add helmet middleware to set secure HTTP response headers. - **04 - 2026 - Rate limiting on API endpoints** - At minimum: auth endpoints (brute force prevention) and game endpoints (spam prevention) - **04 - 2026 — Migrations in deploy pipeline** — Drizzle migrate runs as a CI/CD step before the API container restarts diff --git a/documentation/roasts/gameService.md b/documentation/roasts/gameService.md index e5663b5..de8f968 100644 --- a/documentation/roasts/gameService.md +++ b/documentation/roasts/gameService.md @@ -115,42 +115,6 @@ 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** @@ -181,8 +145,6 @@ 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. diff --git a/documentation/tickets/t00003.md b/documentation/tickets/t00003.md new file mode 100644 index 0000000..774fe42 --- /dev/null +++ b/documentation/tickets/t00003.md @@ -0,0 +1,37 @@ +# 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` From 4f59f3bc1488e072b9e58e3978c16a443223235b Mon Sep 17 00:00:00 2001 From: lila <beiweitemderbeste@protonmail.com> Date: Tue, 28 Apr 2026 13:18:18 +0200 Subject: [PATCH 3/3] formatting --- README.md | 50 +- apps/web/src/components/game/QuestionCard.tsx | 22 +- .../multiplayer/MultiplayerScoreScreen.tsx | 4 +- apps/web/src/components/ui/ConfettiBurst.tsx | 10 +- apps/web/src/routes/multiplayer/index.tsx | 4 +- data-pipeline/test/output/sample.json | 3268 ++++------------- data-pipeline/tsconfig.json | 4 +- documentation/data-pipeline.md | 91 +- documentation/deployment.md | 14 +- documentation/llm-setup.md | 88 +- documentation/notes.md | 9 +- documentation/roasts/gameService.md | 41 +- documentation/spec.md | 73 +- documentation/tickets/blueprint.md | 2 +- documentation/tickets/t00001.md | 4 +- documentation/tickets/t00002.md | 2 +- packages/db/drizzle/meta/0007_snapshot.json | 148 +- packages/db/drizzle/meta/0008_snapshot.json | 162 +- packages/db/drizzle/meta/0009_snapshot.json | 162 +- packages/db/drizzle/meta/0010_snapshot.json | 162 +- packages/db/drizzle/meta/_journal.json | 2 +- packages/db/tsconfig.json | 6 +- tsconfig.json | 4 +- 23 files changed, 994 insertions(+), 3338 deletions(-) diff --git a/README.md b/README.md index 32af038..e212a55 100644 --- a/README.md +++ b/README.md @@ -10,21 +10,21 @@ Live at [lilastudy.com](https://lilastudy.com). ## Stack -| Layer | Technology | -|---|---| -| Monorepo | pnpm workspaces | -| Frontend | React 18, Vite, TypeScript | -| Routing | TanStack Router | -| Server state | TanStack Query | -| Styling | Tailwind CSS | -| Backend | Node.js, Express, TypeScript | -| Database | PostgreSQL + Drizzle ORM | -| Validation | Zod (shared schemas) | -| Auth | Better Auth (Google + GitHub) | -| Realtime | WebSockets (`ws` library) | -| Testing | Vitest, supertest | -| Deployment | Docker Compose, Caddy, Hetzner VPS | -| CI/CD | Forgejo Actions | +| Layer | Technology | +| ------------ | ---------------------------------- | +| Monorepo | pnpm workspaces | +| Frontend | React 18, Vite, TypeScript | +| Routing | TanStack Router | +| Server state | TanStack Query | +| Styling | Tailwind CSS | +| Backend | Node.js, Express, TypeScript | +| Database | PostgreSQL + Drizzle ORM | +| Validation | Zod (shared schemas) | +| Auth | Better Auth (Google + GitHub) | +| Realtime | WebSockets (`ws` library) | +| Testing | Vitest, supertest | +| Deployment | Docker Compose, Caddy, Hetzner VPS | +| CI/CD | Forgejo Actions | --- @@ -156,15 +156,15 @@ pnpm --filter web test ## Roadmap -| Phase | Description | Status | -|---|---|---| -| 0 | Foundation — monorepo, tooling, dev environment | ✅ | -| 1 | Vocabulary data pipeline + REST API | ✅ | -| 2 | Singleplayer quiz UI | ✅ | -| 3 | Auth (Google + GitHub) | ✅ | -| 4 | Multiplayer lobby (WebSockets) | ✅ | -| 5 | Multiplayer game (real-time, server timer) | ✅ | -| 6 | Production deployment + CI/CD | ✅ | -| 7 | Hardening (rate limiting, error boundaries, monitoring, accessibility) | 🔄 | +| Phase | Description | Status | +| ----- | ---------------------------------------------------------------------- | ------ | +| 0 | Foundation — monorepo, tooling, dev environment | ✅ | +| 1 | Vocabulary data pipeline + REST API | ✅ | +| 2 | Singleplayer quiz UI | ✅ | +| 3 | Auth (Google + GitHub) | ✅ | +| 4 | Multiplayer lobby (WebSockets) | ✅ | +| 5 | Multiplayer game (real-time, server timer) | ✅ | +| 6 | Production deployment + CI/CD | ✅ | +| 7 | Hardening (rate limiting, error boundaries, monitoring, accessibility) | 🔄 | See `documentation/roadmap.md` for task-level detail. diff --git a/apps/web/src/components/game/QuestionCard.tsx b/apps/web/src/components/game/QuestionCard.tsx index 7878a5b..2d4c633 100644 --- a/apps/web/src/components/game/QuestionCard.tsx +++ b/apps/web/src/components/game/QuestionCard.tsx @@ -53,7 +53,11 @@ 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> @@ -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="flex flex-col gap-3"> - {question.options.map((option) => ( - <OptionButton - key={option.optionId} - text={option.text} - state={getOptionState(option.optionId)} - onSelect={() => handleSelect(option.optionId)} - /> - ))} + {question.options.map((option) => ( + <OptionButton + key={option.optionId} + text={option.text} + state={getOptionState(option.optionId)} + onSelect={() => handleSelect(option.optionId)} + /> + ))} </div> </div> diff --git a/apps/web/src/components/multiplayer/MultiplayerScoreScreen.tsx b/apps/web/src/components/multiplayer/MultiplayerScoreScreen.tsx index f530db8..8b82f1a 100644 --- a/apps/web/src/components/multiplayer/MultiplayerScoreScreen.tsx +++ b/apps/web/src/components/multiplayer/MultiplayerScoreScreen.tsx @@ -69,7 +69,9 @@ 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} diff --git a/apps/web/src/components/ui/ConfettiBurst.tsx b/apps/web/src/components/ui/ConfettiBurst.tsx index 66285d4..ee1fcfd 100644 --- a/apps/web/src/components/ui/ConfettiBurst.tsx +++ b/apps/web/src/components/ui/ConfettiBurst.tsx @@ -6,10 +6,7 @@ type ConfettiBurstProps = { count?: number; }; -type Piece = { - id: number; - style: React.CSSProperties & ConfettiVars; -}; +type Piece = { id: number; style: React.CSSProperties & ConfettiVars }; type ConfettiVars = { ["--x0"]: string; @@ -56,7 +53,9 @@ 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); @@ -100,4 +99,3 @@ export const ConfettiBurst = ({ </div> ); }; - diff --git a/apps/web/src/routes/multiplayer/index.tsx b/apps/web/src/routes/multiplayer/index.tsx index ee757f4..2780eab 100644 --- a/apps/web/src/routes/multiplayer/index.tsx +++ b/apps/web/src/routes/multiplayer/index.tsx @@ -108,7 +108,9 @@ 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> diff --git a/data-pipeline/test/output/sample.json b/data-pipeline/test/output/sample.json index 5dd774f..3177e22 100644 --- a/data-pipeline/test/output/sample.json +++ b/data-pipeline/test/output/sample.json @@ -3,12 +3,8 @@ "source_id": "ili:i90862", "pos": "noun", "translations": { - "en": [ - "kinsman" - ], - "es": [ - "pariente" - ], + "en": ["kinsman"], + "es": ["pariente"], "de": [ "Gevatter", "Anverwandter", @@ -18,17 +14,11 @@ "Angehöriger", "Verwandte" ], - "fr": [ - "parent" - ] + "fr": ["parent"] }, "glosses": { - "en": [ - "a male relative" - ], - "de": [ - "ein männlicher Verwandter" - ] + "en": ["a male relative"], + "de": ["ein männlicher Verwandter"] }, "examples": { "de": [ @@ -44,16 +34,10 @@ "text": "Alle Familienangehörigen kamen zum Treffen.", "source": "cefr" }, - { - "text": "Er ist ein Angehöriger der Familie.", - "source": "cefr" - } + { "text": "Er ist ein Angehöriger der Familie.", "source": "cefr" } ], "fr": [ - { - "text": "Ses parents sont très fiers de lui.", - "source": "cefr" - } + { "text": "Ses parents sont très fiers de lui.", "source": "cefr" } ], "es": [ { @@ -63,35 +47,15 @@ ] }, "votes": { - "en": { - "kinsman": { - "cefr_source": "C1" - } - }, + "en": { "kinsman": { "cefr_source": "C1" } }, "de": { - "Familienmitglied": { - "cefr_source": "A2" - }, - "Verwandter": { - "cefr_source": "B1" - }, - "Familienangehöriger": { - "cefr_source": "B1" - }, - "Angehöriger": { - "cefr_source": "B2" - } + "Familienmitglied": { "cefr_source": "A2" }, + "Verwandter": { "cefr_source": "B1" }, + "Familienangehöriger": { "cefr_source": "B1" }, + "Angehöriger": { "cefr_source": "B2" } }, - "fr": { - "parent": { - "cefr_source": "A1" - } - }, - "es": { - "pariente": { - "cefr_source": "A2" - } - } + "fr": { "parent": { "cefr_source": "A1" } }, + "es": { "pariente": { "cefr_source": "A2" } } }, "_sample_bucket": "has_cefr_vote" }, @@ -99,96 +63,41 @@ "source_id": "ili:i23087", "pos": "verb", "translations": { - "en": [ - "teach" - ], - "it": [ - "addestrare", - "ammaestrare", - "insegnare" - ], - "es": [ - "enseñar" - ], - "fr": [ - "enseigner", - "apprendre", - "guider" - ] - }, - "glosses": { - "en": [ - "accustom gradually to some action or attitude" - ] + "en": ["teach"], + "it": ["addestrare", "ammaestrare", "insegnare"], + "es": ["enseñar"], + "fr": ["enseigner", "apprendre", "guider"] }, + "glosses": { "en": ["accustom gradually to some action or attitude"] }, "examples": { "en": [ - { - "text": "The child is taught to obey her parents", - "source": "omw" - } + { "text": "The child is taught to obey her parents", "source": "omw" } ], "it": [ - { - "text": "Stiamo addestrando il nostro cane.", - "source": "cefr" - }, - { - "text": "Lei insegna italiano ai bambini.", - "source": "cefr" - } + { "text": "Stiamo addestrando il nostro cane.", "source": "cefr" }, + { "text": "Lei insegna italiano ai bambini.", "source": "cefr" } ], "fr": [ - { - "text": "Elle enseigne le français au lycée.", - "source": "cefr" - }, - { - "text": "J'apprends le français.", - "source": "cefr" - }, - { - "text": "Il va nous guider à travers la forêt.", - "source": "cefr" - } + { "text": "Elle enseigne le français au lycée.", "source": "cefr" }, + { "text": "J'apprends le français.", "source": "cefr" }, + { "text": "Il va nous guider à travers la forêt.", "source": "cefr" } ], "es": [ - { - "text": "Ella enseña español en la universidad.", - "source": "cefr" - } + { "text": "Ella enseña español en la universidad.", "source": "cefr" } ] }, "votes": { - "en": { - "teach": { - "cefr_source": "A1" - } - }, + "en": { "teach": { "cefr_source": "A1" } }, "it": { - "addestrare": { - "cefr_source": "B1" - }, - "insegnare": { - "cefr_source": "A1" - } + "addestrare": { "cefr_source": "B1" }, + "insegnare": { "cefr_source": "A1" } }, "fr": { - "enseigner": { - "cefr_source": "A2" - }, - "apprendre": { - "cefr_source": "A1" - }, - "guider": { - "cefr_source": "A2" - } + "enseigner": { "cefr_source": "A2" }, + "apprendre": { "cefr_source": "A1" }, + "guider": { "cefr_source": "A2" } }, - "es": { - "enseñar": { - "cefr_source": "A1" - } - } + "es": { "enseñar": { "cefr_source": "A1" } } }, "_sample_bucket": "has_cefr_vote" }, @@ -196,39 +105,19 @@ "source_id": "ili:i26718", "pos": "verb", "translations": { - "en": [ - "dub", - "nickname" - ], - "it": [ - "battezzare", - "cognominare", - "doppiare", - "soprannominare" - ], - "es": [ - "apodar" - ], - "fr": [ - "surnom", - "baptiser" - ] - }, - "glosses": { - "en": [ - "give a nickname to" - ] + "en": ["dub", "nickname"], + "it": ["battezzare", "cognominare", "doppiare", "soprannominare"], + "es": ["apodar"], + "fr": ["surnom", "baptiser"] }, + "glosses": { "en": ["give a nickname to"] }, "examples": { "it": [ { "text": "Hanno deciso di battezzare il loro figlio la prossima primavera.", "source": "cefr" }, - { - "text": "Lo hanno soprannominato 'il Professore'.", - "source": "cefr" - } + { "text": "Lo hanno soprannominato 'il Professore'.", "source": "cefr" } ], "fr": [ { @@ -238,24 +127,12 @@ ] }, "votes": { - "en": { - "dub": { - "cefr_source": "B2" - } - }, + "en": { "dub": { "cefr_source": "B2" } }, "it": { - "battezzare": { - "cefr_source": "B1" - }, - "soprannominare": { - "cefr_source": "B2" - } + "battezzare": { "cefr_source": "B1" }, + "soprannominare": { "cefr_source": "B2" } }, - "fr": { - "baptiser": { - "cefr_source": "B1" - } - } + "fr": { "baptiser": { "cefr_source": "B1" } } }, "_sample_bucket": "has_cefr_vote" }, @@ -263,92 +140,46 @@ "source_id": "ili:i4448", "pos": "adjective", "translations": { - "en": [ - "drab", - "dreary" - ], - "es": [ - "igual", - "rutinario" - ], - "fr": [ - "morne", - "maussade", - "sombre" - ] - }, - "glosses": { - "en": [ - "lacking in liveliness or charm or surprise" - ] + "en": ["drab", "dreary"], + "es": ["igual", "rutinario"], + "fr": ["morne", "maussade", "sombre"] }, + "glosses": { "en": ["lacking in liveliness or charm or surprise"] }, "examples": { "en": [ - { - "text": "her drab personality", - "source": "omw" - }, + { "text": "her drab personality", "source": "omw" }, { "text": "life was drab compared with the more exciting life style overseas", "source": "omw" }, - { - "text": "a series of dreary dinner parties", - "source": "omw" - } + { "text": "a series of dreary dinner parties", "source": "omw" } ], "fr": [ - { - "text": "Le temps était morne et pluvieux.", - "source": "cefr" - }, + { "text": "Le temps était morne et pluvieux.", "source": "cefr" }, { "text": "Le temps était maussade toute la journée.", "source": "cefr" }, - { - "text": "La pièce était sombre sans lumière.", - "source": "cefr" - } + { "text": "La pièce était sombre sans lumière.", "source": "cefr" } ], "es": [ - { - "text": "Todos somos iguales.", - "source": "cefr" - }, - { - "text": "Su trabajo se ha vuelto muy rutinario.", - "source": "cefr" - } + { "text": "Todos somos iguales.", "source": "cefr" }, + { "text": "Su trabajo se ha vuelto muy rutinario.", "source": "cefr" } ] }, "votes": { "en": { - "drab": { - "cefr_source": "B2" - }, - "dreary": { - "cefr_source": "B2" - } + "drab": { "cefr_source": "B2" }, + "dreary": { "cefr_source": "B2" } }, "fr": { - "morne": { - "cefr_source": "B2" - }, - "maussade": { - "cefr_source": "B2" - }, - "sombre": { - "cefr_source": "B1" - } + "morne": { "cefr_source": "B2" }, + "maussade": { "cefr_source": "B2" }, + "sombre": { "cefr_source": "B1" } }, "es": { - "igual": { - "cefr_source": "A2" - }, - "rutinario": { - "cefr_source": "B1" - } + "igual": { "cefr_source": "A2" }, + "rutinario": { "cefr_source": "B1" } } }, "_sample_bucket": "has_cefr_vote" @@ -357,75 +188,30 @@ "source_id": "ili:i85845", "pos": "noun", "translations": { - "en": [ - "natural depression", - "depression" - ], - "it": [ - "avvallamento" - ], - "es": [ - "depresión", - "depresión natural" - ], - "fr": [ - "dépression" - ] - }, - "glosses": { - "en": [ - "a sunken or depressed geological formation" - ] + "en": ["natural depression", "depression"], + "it": ["avvallamento"], + "es": ["depresión", "depresión natural"], + "fr": ["dépression"] }, + "glosses": { "en": ["a sunken or depressed geological formation"] }, "examples": { - "fr": [ - { - "text": "Elle souffre de dépression.", - "source": "cefr" - } - ], + "fr": [{ "text": "Elle souffre de dépression.", "source": "cefr" }], "es": [ - { - "text": "La depresión es una enfermedad grave.", - "source": "cefr" - } + { "text": "La depresión es una enfermedad grave.", "source": "cefr" } ] }, "votes": { - "en": { - "depression": { - "cefr_source": "B2" - } - }, - "fr": { - "dépression": { - "cefr_source": "B2" - } - }, - "es": { - "depresión": { - "cefr_source": "B1" - } - } + "en": { "depression": { "cefr_source": "B2" } }, + "fr": { "dépression": { "cefr_source": "B2" } }, + "es": { "depresión": { "cefr_source": "B1" } } }, "_sample_bucket": "has_cefr_vote" }, { "source_id": "ili:i27202", "pos": "verb", - "translations": { - "en": [ - "jump" - ], - "fr": [ - "sauter" - ] - }, - "glosses": { - "en": [ - "make a sudden physical attack on" - ] - }, + "translations": { "en": ["jump"], "fr": ["sauter"] }, + "glosses": { "en": ["make a sudden physical attack on"] }, "examples": { "en": [ { @@ -441,16 +227,8 @@ ] }, "votes": { - "en": { - "jump": { - "cefr_source": "A1" - } - }, - "fr": { - "sauter": { - "cefr_source": "A2" - } - } + "en": { "jump": { "cefr_source": "A1" } }, + "fr": { "sauter": { "cefr_source": "A2" } } }, "_sample_bucket": "has_cefr_vote" }, @@ -465,15 +243,8 @@ "butt against", "knock against" ], - "it": [ - "urtare" - ], - "es": [ - "chocar", - "colisionar", - "golpearse contra", - "topar" - ], + "it": ["urtare"], + "es": ["chocar", "colisionar", "golpearse contra", "topar"], "de": [ "anraunzen", "anfahren", @@ -488,68 +259,32 @@ ] }, "glosses": { - "en": [ - "collide violently with an obstacle" - ], - "de": [ - "heftig mit einem Hindernis zusammenstoßen" - ] + "en": ["collide violently with an obstacle"], + "de": ["heftig mit einem Hindernis zusammenstoßen"] }, "examples": { - "en": [ - { - "text": "I ran into the telephone pole", - "source": "omw" - } - ], + "en": [{ "text": "I ran into the telephone pole", "source": "omw" }], "it": [ - { - "text": "Ho urtato il tavolo con il gomito.", - "source": "cefr" - } + { "text": "Ho urtato il tavolo con il gomito.", "source": "cefr" } ], "de": [ - { - "text": "Der Bus fuhr an die Haltestelle an.", - "source": "cefr" - }, - { - "text": "Er hat mich ohne Grund angeschrien.", - "source": "cefr" - } + { "text": "Der Bus fuhr an die Haltestelle an.", "source": "cefr" }, + { "text": "Er hat mich ohne Grund angeschrien.", "source": "cefr" } ], "es": [ - { - "text": "El coche chocó contra un árbol.", - "source": "cefr" - }, - { - "text": "Me topé con un viejo amigo en la calle.", - "source": "cefr" - } + { "text": "El coche chocó contra un árbol.", "source": "cefr" }, + { "text": "Me topé con un viejo amigo en la calle.", "source": "cefr" } ] }, "votes": { - "it": { - "urtare": { - "cefr_source": "B1" - } - }, + "it": { "urtare": { "cefr_source": "B1" } }, "de": { - "anfahren": { - "cefr_source": "B1" - }, - "anschreien": { - "cefr_source": "B1" - } + "anfahren": { "cefr_source": "B1" }, + "anschreien": { "cefr_source": "B1" } }, "es": { - "chocar": { - "cefr_source": "A2" - }, - "topar": { - "cefr_source": "B1" - } + "chocar": { "cefr_source": "A2" }, + "topar": { "cefr_source": "B1" } } }, "_sample_bucket": "has_cefr_vote" @@ -557,42 +292,19 @@ { "source_id": "ili:i27676", "pos": "verb", - "translations": { - "en": [ - "fumble" - ] - }, - "glosses": { - "en": [ - "handle clumsily" - ] - }, + "translations": { "en": ["fumble"] }, + "glosses": { "en": ["handle clumsily"] }, "examples": {}, - "votes": { - "en": { - "fumble": { - "cefr_source": "B2" - } - } - }, + "votes": { "en": { "fumble": { "cefr_source": "B2" } } }, "_sample_bucket": "has_cefr_vote" }, { "source_id": "ili:i30768", "pos": "verb", "translations": { - "en": [ - "attract", - "appeal" - ], - "it": [ - "allettare", - "attirare", - "attrarre" - ], - "es": [ - "atraer" - ], + "en": ["attract", "appeal"], + "it": ["allettare", "attirare", "attrarre"], + "es": ["atraer"], "de": [ "anziehen", "etwas überziehen", @@ -604,98 +316,51 @@ "ankleiden", "Kleidung anlegen" ], - "fr": [ - "allécher", - "attirer" - ] + "fr": ["allécher", "attirer"] }, "glosses": { - "en": [ - "be attractive to" - ], + "en": ["be attractive to"], "de": [ "ein Kleidungsstück in der dafür vorgesehenen Weise auf den Körper bringen" ] }, "examples": { "en": [ - { - "text": "The idea of a vacation appeals to me", - "source": "omw" - }, + { "text": "The idea of a vacation appeals to me", "source": "omw" }, { "text": "The beautiful garden attracted many people", "source": "omw" } ], - "de": [ - { - "text": "Sie zog sich das Kleid an.", - "source": "omw" - } - ], + "de": [{ "text": "Sie zog sich das Kleid an.", "source": "omw" }], "it": [ - { - "text": "Il nuovo negozio attira molti clienti.", - "source": "cefr" - }, - { - "text": "Il magnete attrae il metallo.", - "source": "cefr" - } + { "text": "Il nuovo negozio attira molti clienti.", "source": "cefr" }, + { "text": "Il magnete attrae il metallo.", "source": "cefr" } ], "fr": [ { "text": "La promesse d'un salaire élevé a alléché de nombreux candidats.", "source": "cefr" }, - { - "text": "Cette publicité attire l'attention.", - "source": "cefr" - } + { "text": "Cette publicité attire l'attention.", "source": "cefr" } ], - "es": [ - { - "text": "El imán atrae el metal.", - "source": "cefr" - } - ] + "es": [{ "text": "El imán atrae el metal.", "source": "cefr" }] }, "votes": { - "en": { - "attract": { - "cefr_source": "B1" - } - }, + "en": { "attract": { "cefr_source": "B1" } }, "it": { - "attirare": { - "cefr_source": "B2" - }, - "attrarre": { - "cefr_source": "B1" - } + "attirare": { "cefr_source": "B2" }, + "attrarre": { "cefr_source": "B1" } }, "de": { - "anziehen": { - "cefr_source": "A2" - }, - "bekleiden": { - "cefr_source": "B2" - } + "anziehen": { "cefr_source": "A2" }, + "bekleiden": { "cefr_source": "B2" } }, "fr": { - "allécher": { - "cefr_source": "C1" - }, - "attirer": { - "cefr_source": "B1" - } + "allécher": { "cefr_source": "C1" }, + "attirer": { "cefr_source": "B1" } }, - "es": { - "atraer": { - "cefr_source": "B2" - } - } + "es": { "atraer": { "cefr_source": "B2" } } }, "_sample_bucket": "has_cefr_vote" }, @@ -703,23 +368,11 @@ "source_id": "ili:i112909", "pos": "noun", "translations": { - "en": [ - "regulation" - ], - "es": [ - "reglamento" - ], - "fr": [ - "réglementation", - "gouvernement", - "tenue" - ] - }, - "glosses": { - "en": [ - "the state of being controlled or governed" - ] + "en": ["regulation"], + "es": ["reglamento"], + "fr": ["réglementation", "gouvernement", "tenue"] }, + "glosses": { "en": ["the state of being controlled or governed"] }, "examples": { "fr": [ { @@ -735,35 +388,16 @@ "source": "cefr" } ], - "es": [ - { - "text": "Debemos seguir el reglamento.", - "source": "cefr" - } - ] + "es": [{ "text": "Debemos seguir el reglamento.", "source": "cefr" }] }, "votes": { - "en": { - "regulation": { - "cefr_source": "B2" - } - }, + "en": { "regulation": { "cefr_source": "B2" } }, "fr": { - "réglementation": { - "cefr_source": "B2" - }, - "gouvernement": { - "cefr_source": "B1" - }, - "tenue": { - "cefr_source": "B1" - } + "réglementation": { "cefr_source": "B2" }, + "gouvernement": { "cefr_source": "B1" }, + "tenue": { "cefr_source": "B1" } }, - "es": { - "reglamento": { - "cefr_source": "B2" - } - } + "es": { "reglamento": { "cefr_source": "B2" } } }, "_sample_bucket": "has_cefr_vote" }, @@ -778,12 +412,8 @@ "ladybird", "ladybird beetle" ], - "it": [ - "coccinella" - ], - "fr": [ - "coccinelle" - ] + "it": ["coccinella"], + "fr": ["coccinelle"] }, "glosses": { "en": [ @@ -792,23 +422,12 @@ }, "examples": { "fr": [ - { - "text": "Une coccinelle s'est posée sur ma main.", - "source": "cefr" - } + { "text": "Une coccinelle s'est posée sur ma main.", "source": "cefr" } ] }, "votes": { - "en": { - "ladybug": { - "cefr_source": "A2" - } - }, - "fr": { - "coccinelle": { - "cefr_source": "A2" - } - } + "en": { "ladybug": { "cefr_source": "A2" } }, + "fr": { "coccinelle": { "cefr_source": "A2" } } }, "_sample_bucket": "has_cefr_vote" }, @@ -816,56 +435,32 @@ "source_id": "ili:i15517", "pos": "adjective", "translations": { - "en": [ - "judicial" - ], - "it": [ - "giudiziale", - "giudiziario" - ], - "es": [ - "judicial" - ], + "en": ["judicial"], + "it": ["giudiziale", "giudiziario"], + "es": ["judicial"], "de": [ "durch einen Richter", "durch ein Gericht", "durch den Richter", "richterlich" ], - "fr": [ - "judiciaire" - ] + "fr": ["judiciaire"] }, "glosses": { - "en": [ - "belonging or appropriate to the office of a judge" - ], - "de": [ - "zum Amt eines Richters gehörend oder diesem zugehörig" - ] + "en": ["belonging or appropriate to the office of a judge"], + "de": ["zum Amt eines Richters gehörend oder diesem zugehörig"] }, "examples": { - "en": [ - { - "text": "judicial robes", - "source": "omw" - } - ], + "en": [{ "text": "judicial robes", "source": "omw" }], "it": [ - { - "text": "Hanno avviato un'azione giudiziale.", - "source": "cefr" - }, + { "text": "Hanno avviato un'azione giudiziale.", "source": "cefr" }, { "text": "Il sistema giudiziario italiano è complesso.", "source": "cefr" } ], "de": [ - { - "text": "Es bedarf einer richterlichen Anordnung.", - "source": "cefr" - } + { "text": "Es bedarf einer richterlichen Anordnung.", "source": "cefr" } ], "fr": [ { @@ -873,104 +468,37 @@ "source": "cefr" } ], - "es": [ - { - "text": "El proceso judicial fue largo.", - "source": "cefr" - } - ] + "es": [{ "text": "El proceso judicial fue largo.", "source": "cefr" }] }, "votes": { - "en": { - "judicial": { - "cefr_source": "C1" - } - }, + "en": { "judicial": { "cefr_source": "C1" } }, "it": { - "giudiziale": { - "cefr_source": "C1" - }, - "giudiziario": { - "cefr_source": "C1" - } + "giudiziale": { "cefr_source": "C1" }, + "giudiziario": { "cefr_source": "C1" } }, - "de": { - "richterlich": { - "cefr_source": "C1" - } - }, - "fr": { - "judiciaire": { - "cefr_source": "B2" - } - }, - "es": { - "judicial": { - "cefr_source": "C1" - } - } + "de": { "richterlich": { "cefr_source": "C1" } }, + "fr": { "judiciaire": { "cefr_source": "B2" } }, + "es": { "judicial": { "cefr_source": "C1" } } }, "_sample_bucket": "has_cefr_vote" }, { "source_id": "ili:i11095", "pos": "adjective", - "translations": { - "en": [ - "poor" - ], - "es": [ - "pobre" - ], - "fr": [ - "pauvre" - ] - }, - "glosses": { - "en": [ - "characterized by or indicating poverty" - ] - }, + "translations": { "en": ["poor"], "es": ["pobre"], "fr": ["pauvre"] }, + "glosses": { "en": ["characterized by or indicating poverty"] }, "examples": { "en": [ - { - "text": "the country had a poor economy", - "source": "omw" - }, - { - "text": "they lived in the poor section of town", - "source": "omw" - } + { "text": "the country had a poor economy", "source": "omw" }, + { "text": "they lived in the poor section of town", "source": "omw" } ], - "fr": [ - { - "text": "Il est très pauvre.", - "source": "cefr" - } - ], - "es": [ - { - "text": "Es un hombre muy pobre.", - "source": "cefr" - } - ] + "fr": [{ "text": "Il est très pauvre.", "source": "cefr" }], + "es": [{ "text": "Es un hombre muy pobre.", "source": "cefr" }] }, "votes": { - "en": { - "poor": { - "cefr_source": "A2" - } - }, - "fr": { - "pauvre": { - "cefr_source": "A1" - } - }, - "es": { - "pobre": { - "cefr_source": "A1" - } - } + "en": { "poor": { "cefr_source": "A2" } }, + "fr": { "pauvre": { "cefr_source": "A1" } }, + "es": { "pobre": { "cefr_source": "A1" } } }, "_sample_bucket": "has_cefr_vote" }, @@ -988,10 +516,7 @@ "tawdriness", "glitz" ], - "it": [ - "pacchianeria", - "vistosità" - ], + "it": ["pacchianeria", "vistosità"], "es": [ "astracanada", "chabacanería", @@ -1001,22 +526,12 @@ "ordinariez", "zafiedad" ], - "de": [ - "Aufdringlichkeit", - "Zudringlichkeit", - "Penetranz" - ], - "fr": [ - "culot" - ] + "de": ["Aufdringlichkeit", "Zudringlichkeit", "Penetranz"], + "fr": ["culot"] }, "glosses": { - "en": [ - "tasteless showiness" - ], - "de": [ - "geschmacklose Aufdringlichkeit" - ] + "en": ["tasteless showiness"], + "de": ["geschmacklose Aufdringlichkeit"] }, "examples": { "fr": [ @@ -1028,18 +543,10 @@ }, "votes": { "en": { - "loudness": { - "cefr_source": "B2" - }, - "glitz": { - "cefr_source": "B2" - } + "loudness": { "cefr_source": "B2" }, + "glitz": { "cefr_source": "B2" } }, - "fr": { - "culot": { - "cefr_source": "B2" - } - } + "fr": { "culot": { "cefr_source": "B2" } } }, "_sample_bucket": "has_cefr_vote" }, @@ -1047,52 +554,19 @@ "source_id": "ili:i22613", "pos": "verb", "translations": { - "en": [ - "scavenge", - "clean" - ], - "es": [ - "limpiar" - ], - "fr": [ - "nettoyer" - ] - }, - "glosses": { - "en": [ - "remove unwanted substances from" - ] + "en": ["scavenge", "clean"], + "es": ["limpiar"], + "fr": ["nettoyer"] }, + "glosses": { "en": ["remove unwanted substances from"] }, "examples": { - "fr": [ - { - "text": "Je dois nettoyer ma chambre.", - "source": "cefr" - } - ], - "es": [ - { - "text": "Necesito limpiar mi habitación.", - "source": "cefr" - } - ] + "fr": [{ "text": "Je dois nettoyer ma chambre.", "source": "cefr" }], + "es": [{ "text": "Necesito limpiar mi habitación.", "source": "cefr" }] }, "votes": { - "en": { - "scavenge": { - "cefr_source": "B2" - } - }, - "fr": { - "nettoyer": { - "cefr_source": "A1" - } - }, - "es": { - "limpiar": { - "cefr_source": "A1" - } - } + "en": { "scavenge": { "cefr_source": "B2" } }, + "fr": { "nettoyer": { "cefr_source": "A1" } }, + "es": { "limpiar": { "cefr_source": "A1" } } }, "_sample_bucket": "has_cefr_vote" }, @@ -1100,35 +574,15 @@ "source_id": "ili:i4857", "pos": "adjective", "translations": { - "en": [ - "enthusiastic" - ], - "it": [ - "caloroso", - "entusiastico", - "fervido", - "entusiasta" - ], - "fr": [ - "courageux", - "enthousiaste" - ] - }, - "glosses": { - "en": [ - "having or showing great excitement and interest" - ] + "en": ["enthusiastic"], + "it": ["caloroso", "entusiastico", "fervido", "entusiasta"], + "fr": ["courageux", "enthousiaste"] }, + "glosses": { "en": ["having or showing great excitement and interest"] }, "examples": { "en": [ - { - "text": "enthusiastic crowds filled the streets", - "source": "omw" - }, - { - "text": "an enthusiastic response", - "source": "omw" - }, + { "text": "enthusiastic crowds filled the streets", "source": "omw" }, + { "text": "an enthusiastic response", "source": "omw" }, { "text": "was enthusiastic about taking ballet lessons", "source": "omw" @@ -1143,16 +597,10 @@ "text": "Ha espresso un fervido desiderio di pace.", "source": "cefr" }, - { - "text": "Era molto entusiasta del nuovo progetto.", - "source": "cefr" - } + { "text": "Era molto entusiasta del nuovo progetto.", "source": "cefr" } ], "fr": [ - { - "text": "C'est une personne très courageuse.", - "source": "cefr" - }, + { "text": "C'est une personne très courageuse.", "source": "cefr" }, { "text": "Elle est très enthousiaste à l'idée de ce voyage.", "source": "cefr" @@ -1160,29 +608,15 @@ ] }, "votes": { - "en": { - "enthusiastic": { - "cefr_source": "B1" - } - }, + "en": { "enthusiastic": { "cefr_source": "B1" } }, "it": { - "caloroso": { - "cefr_source": "B1" - }, - "fervido": { - "cefr_source": "C1" - }, - "entusiasta": { - "cefr_source": "B1" - } + "caloroso": { "cefr_source": "B1" }, + "fervido": { "cefr_source": "C1" }, + "entusiasta": { "cefr_source": "B1" } }, "fr": { - "courageux": { - "cefr_source": "A2" - }, - "enthousiaste": { - "cefr_source": "B1" - } + "courageux": { "cefr_source": "A2" }, + "enthousiaste": { "cefr_source": "B1" } } }, "_sample_bucket": "has_cefr_vote" @@ -1191,13 +625,8 @@ "source_id": "ili:i104521", "pos": "noun", "translations": { - "en": [ - "veronica", - "speedwell" - ], - "it": [ - "veronica" - ], + "en": ["veronica", "speedwell"], + "it": ["veronica"], "de": [ "Allerweltsheil", "Grundheil", @@ -1206,18 +635,11 @@ "Köhlerkraut", "Schlangenkraut" ], - "fr": [ - "veronica", - "véronique" - ] + "fr": ["veronica", "véronique"] }, "glosses": { - "en": [ - "any plant of the genus Veronica" - ], - "de": [ - "jede Pflanze der Gattung Veronica" - ] + "en": ["any plant of the genus Veronica"], + "de": ["jede Pflanze der Gattung Veronica"] }, "examples": { "de": [ @@ -1227,56 +649,21 @@ } ] }, - "votes": { - "de": { - "Ehrenpreis": { - "cefr_source": "C1" - } - } - }, + "votes": { "de": { "Ehrenpreis": { "cefr_source": "C1" } } }, "_sample_bucket": "has_cefr_vote" }, { "source_id": "ili:i958", "pos": "adjective", - "translations": { - "en": [ - "gracious" - ], - "es": [ - "amable" - ] - }, - "glosses": { - "en": [ - "disposed to bestow favors" - ] - }, + "translations": { "en": ["gracious"], "es": ["amable"] }, + "glosses": { "en": ["disposed to bestow favors"] }, "examples": { - "en": [ - { - "text": "thanks to the gracious gods", - "source": "omw" - } - ], - "es": [ - { - "text": "Siempre es muy amable con todos.", - "source": "cefr" - } - ] + "en": [{ "text": "thanks to the gracious gods", "source": "omw" }], + "es": [{ "text": "Siempre es muy amable con todos.", "source": "cefr" }] }, "votes": { - "en": { - "gracious": { - "cefr_source": "B2" - } - }, - "es": { - "amable": { - "cefr_source": "A2" - } - } + "en": { "gracious": { "cefr_source": "B2" } }, + "es": { "amable": { "cefr_source": "A2" } } }, "_sample_bucket": "has_cefr_vote" }, @@ -1284,23 +671,11 @@ "source_id": "ili:i109447", "pos": "noun", "translations": { - "en": [ - "declension" - ], - "it": [ - "declinazione" - ], - "es": [ - "declinación" - ], - "de": [ - "Deklination", - "Ortsmissweisung", - "Missweisung" - ], - "fr": [ - "déclinaison" - ] + "en": ["declension"], + "it": ["declinazione"], + "es": ["declinación"], + "de": ["Deklination", "Ortsmissweisung", "Missweisung"], + "fr": ["déclinaison"] }, "glosses": { "en": [ @@ -1318,23 +693,12 @@ } ], "fr": [ - { - "text": "En latin, les noms ont des déclinaisons.", - "source": "cefr" - } + { "text": "En latin, les noms ont des déclinaisons.", "source": "cefr" } ] }, "votes": { - "it": { - "declinazione": { - "cefr_source": "B2" - } - }, - "fr": { - "déclinaison": { - "cefr_source": "C1" - } - } + "it": { "declinazione": { "cefr_source": "B2" } }, + "fr": { "déclinaison": { "cefr_source": "C1" } } }, "_sample_bucket": "has_cefr_vote" }, @@ -1342,29 +706,14 @@ "source_id": "ili:i18812", "pos": "adverb", "translations": { - "en": [ - "fairly", - "fair", - "evenhandedly" - ], - "es": [ - "con justicia", - "imparcialmente", - "justamente" - ] + "en": ["fairly", "fair", "evenhandedly"], + "es": ["con justicia", "imparcialmente", "justamente"] }, "glosses": { - "en": [ - "without favoring one party, in a fair evenhanded manner" - ] + "en": ["without favoring one party, in a fair evenhanded manner"] }, "examples": { - "en": [ - { - "text": "deal fairly with one another", - "source": "omw" - } - ], + "en": [{ "text": "deal fairly with one another", "source": "omw" }], "es": [ { "text": "Llegó justamente a tiempo para la reunión.", @@ -1373,16 +722,8 @@ ] }, "votes": { - "en": { - "fairly": { - "cefr_source": "B1" - } - }, - "es": { - "justamente": { - "cefr_source": "B2" - } - } + "en": { "fairly": { "cefr_source": "B1" } }, + "es": { "justamente": { "cefr_source": "B2" } } }, "_sample_bucket": "has_cefr_vote" }, @@ -1390,23 +731,11 @@ "source_id": "ili:i44747", "pos": "noun", "translations": { - "en": [ - "Centrocercus", - "genus Centrocercus" - ], - "es": [ - "Centrocercus", - "género Centrocercus" - ], - "fr": [ - "centrocercus" - ] - }, - "glosses": { - "en": [ - "sage grouse" - ] + "en": ["Centrocercus", "genus Centrocercus"], + "es": ["Centrocercus", "género Centrocercus"], + "fr": ["centrocercus"] }, + "glosses": { "en": ["sage grouse"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_cefr_vote" @@ -1414,16 +743,8 @@ { "source_id": "ili:i20736", "pos": "adverb", - "translations": { - "en": [ - "insinuatingly" - ] - }, - "glosses": { - "en": [ - "in an insinuating manner" - ] - }, + "translations": { "en": ["insinuatingly"] }, + "glosses": { "en": ["in an insinuating manner"] }, "examples": { "en": [ { @@ -1438,16 +759,8 @@ { "source_id": "ili:i25017", "pos": "verb", - "translations": { - "en": [ - "superordinate" - ] - }, - "glosses": { - "en": [ - "place in a superior order or rank" - ] - }, + "translations": { "en": ["superordinate"] }, + "glosses": { "en": ["place in a superior order or rank"] }, "examples": { "en": [ { @@ -1463,9 +776,7 @@ "source_id": "ili:i46616", "pos": "noun", "translations": { - "en": [ - "sand cat" - ], + "en": ["sand cat"], "fr": [ "chat de marguerite", "chat du désert", @@ -1473,11 +784,7 @@ "chat des sables" ] }, - "glosses": { - "en": [ - "a desert wildcat" - ] - }, + "glosses": { "en": ["a desert wildcat"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_cefr_vote" @@ -1485,15 +792,9 @@ { "source_id": "ili:i83491", "pos": "noun", - "translations": { - "en": [ - "Bangor" - ] - }, + "translations": { "en": ["Bangor"] }, "glosses": { - "en": [ - "a university town in northwestern Wales on the Menai Strait" - ] + "en": ["a university town in northwestern Wales on the Menai Strait"] }, "examples": {}, "votes": {}, @@ -1503,19 +804,11 @@ "source_id": "ili:i72819", "pos": "noun", "translations": { - "en": [ - "Missouri" - ], - "fr": [ - "Saint Peters", - "Joplin", - "Missouri" - ] + "en": ["Missouri"], + "fr": ["Saint Peters", "Joplin", "Missouri"] }, "glosses": { - "en": [ - "a dialect of the Chiwere language spoken by the Missouri" - ] + "en": ["a dialect of the Chiwere language spoken by the Missouri"] }, "examples": {}, "votes": {}, @@ -1525,18 +818,9 @@ "source_id": "ili:i99797", "pos": "noun", "translations": { - "en": [ - "prickly poppy", - "argemone", - "white thistle", - "devil's fig" - ], - "es": [ - "argemone" - ], - "fr": [ - "argemone" - ] + "en": ["prickly poppy", "argemone", "white thistle", "devil's fig"], + "es": ["argemone"], + "fr": ["argemone"] }, "glosses": { "en": [ @@ -1551,26 +835,12 @@ "source_id": "ili:i90317", "pos": "noun", "translations": { - "en": [ - "great-uncle", - "granduncle" - ], - "it": [ - "protio", - "prozio" - ], - "es": [ - "tío abuelo" - ], - "fr": [ - "grand-oncle" - ] - }, - "glosses": { - "en": [ - "an uncle of your father or mother" - ] + "en": ["great-uncle", "granduncle"], + "it": ["protio", "prozio"], + "es": ["tío abuelo"], + "fr": ["grand-oncle"] }, + "glosses": { "en": ["an uncle of your father or mother"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_cefr_vote" @@ -1579,19 +849,10 @@ "source_id": "ili:i53881", "pos": "noun", "translations": { - "en": [ - "flour bin" - ], - "es": [ - "frasco de harina", - "tarro de harina" - ] - }, - "glosses": { - "en": [ - "a bin for holding flour" - ] + "en": ["flour bin"], + "es": ["frasco de harina", "tarro de harina"] }, + "glosses": { "en": ["a bin for holding flour"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_cefr_vote" @@ -1599,19 +860,8 @@ { "source_id": "ili:i58210", "pos": "noun", - "translations": { - "en": [ - "road map" - ], - "fr": [ - "carte routière" - ] - }, - "glosses": { - "en": [ - "a map showing roads (for automobile travel)" - ] - }, + "translations": { "en": ["road map"], "fr": ["carte routière"] }, + "glosses": { "en": ["a map showing roads (for automobile travel)"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_cefr_vote" @@ -1620,15 +870,10 @@ "source_id": "ili:i82638", "pos": "noun", "translations": { - "en": [ - "South American country", - "South American nation" - ] + "en": ["South American country", "South American nation"] }, "glosses": { - "en": [ - "any one of the countries occupying the South American continent" - ] + "en": ["any one of the countries occupying the South American continent"] }, "examples": {}, "votes": {}, @@ -1638,17 +883,9 @@ "source_id": "ili:i71111", "pos": "noun", "translations": { - "en": [ - "weekly" - ], - "it": [ - "ebdomadario", - "eddomadario", - "settimanale" - ], - "fr": [ - "hebdomadaire" - ] + "en": ["weekly"], + "it": ["ebdomadario", "eddomadario", "settimanale"], + "fr": ["hebdomadaire"] }, "glosses": { "en": [ @@ -1662,47 +899,17 @@ { "source_id": "ili:i10131", "pos": "adjective", - "translations": { - "en": [ - "embattled" - ], - "it": [ - "GAP!", - "in difficoltà" - ] - }, - "glosses": { - "en": [ - "prepared for battle" - ] - }, - "examples": { - "en": [ - { - "text": "an embattled city", - "source": "omw" - } - ] - }, + "translations": { "en": ["embattled"], "it": ["GAP!", "in difficoltà"] }, + "glosses": { "en": ["prepared for battle"] }, + "examples": { "en": [{ "text": "an embattled city", "source": "omw" }] }, "votes": {}, "_sample_bucket": "no_cefr_vote" }, { "source_id": "ili:i108195", "pos": "noun", - "translations": { - "en": [ - "mass unit" - ], - "es": [ - "unidad de masa" - ] - }, - "glosses": { - "en": [ - "a unit of measurement for mass" - ] - }, + "translations": { "en": ["mass unit"], "es": ["unidad de masa"] }, + "glosses": { "en": ["a unit of measurement for mass"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_cefr_vote" @@ -1710,11 +917,7 @@ { "source_id": "ili:i82225", "pos": "noun", - "translations": { - "en": [ - "Wrangell-St. Elias National Park" - ] - }, + "translations": { "en": ["Wrangell-St. Elias National Park"] }, "glosses": { "en": [ "the largest national park of the United States; located in Alaska" @@ -1728,20 +931,10 @@ "source_id": "ili:i47159", "pos": "noun", "translations": { - "en": [ - "Fenusa", - "genus-Fenusa" - ], - "es": [ - "Fenusa", - "género Fenusa" - ] - }, - "glosses": { - "en": [ - "birch leaf miner" - ] + "en": ["Fenusa", "genus-Fenusa"], + "es": ["Fenusa", "género Fenusa"] }, + "glosses": { "en": ["birch leaf miner"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_cefr_vote" @@ -1749,16 +942,8 @@ { "source_id": "ili:i106504", "pos": "noun", - "translations": { - "en": [ - "entail" - ] - }, - "glosses": { - "en": [ - "land received by fee tail" - ] - }, + "translations": { "en": ["entail"] }, + "glosses": { "en": ["land received by fee tail"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_cefr_vote" @@ -1767,19 +952,10 @@ "source_id": "ili:i46047", "pos": "noun", "translations": { - "en": [ - "Polynesian tattler", - "Heteroscelus incanus" - ], - "fr": [ - "heteroscelus incanus" - ] - }, - "glosses": { - "en": [ - "tattler of Pacific coastal regions" - ] + "en": ["Polynesian tattler", "Heteroscelus incanus"], + "fr": ["heteroscelus incanus"] }, + "glosses": { "en": ["tattler of Pacific coastal regions"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_cefr_vote" @@ -1787,11 +963,7 @@ { "source_id": "ili:i71598", "pos": "noun", - "translations": { - "en": [ - "market letter" - ] - }, + "translations": { "en": ["market letter"] }, "glosses": { "en": [ "a newsletter written by an analyst of the stock market and sold to subscribers" @@ -1805,29 +977,11 @@ "source_id": "ili:i115719", "pos": "noun", "translations": { - "en": [ - "monosaccharide", - "monosaccharose", - "simple sugar" - ], - "it": [ - "manosio", - "monosaccaride", - "monosio", - "monoso" - ], - "es": [ - "monosacárido" - ], - "de": [ - "Monosaccharid", - "Einfachzucker" - ], - "fr": [ - "ose", - "Ose", - "monosaccharide" - ] + "en": ["monosaccharide", "monosaccharose", "simple sugar"], + "it": ["manosio", "monosaccaride", "monosio", "monoso"], + "es": ["monosacárido"], + "de": ["Monosaccharid", "Einfachzucker"], + "fr": ["ose", "Ose", "monosaccharide"] }, "glosses": { "en": [ @@ -1845,11 +999,7 @@ "source_id": "ili:i74228", "pos": "noun", "translations": { - "en": [ - "negotiation", - "dialogue", - "talks" - ], + "en": ["negotiation", "dialogue", "talks"], "it": [ "contrattazione", "deal", @@ -1858,27 +1008,13 @@ "negoziazione", "trattativa" ], - "es": [ - "gestión", - "negociación", - "tramitación" - ], - "de": [ - "Besprechung", - "Verhandlung" - ], - "fr": [ - "dialogue", - "négociation" - ] + "es": ["gestión", "negociación", "tramitación"], + "de": ["Besprechung", "Verhandlung"], + "fr": ["dialogue", "négociation"] }, "glosses": { - "en": [ - "a discussion intended to produce an agreement" - ], - "de": [ - "Diskussion zur Ausarbeitung eines Abkommens" - ] + "en": ["a discussion intended to produce an agreement"], + "de": ["Diskussion zur Ausarbeitung eines Abkommens"] }, "examples": { "en": [ @@ -1886,24 +1022,15 @@ "text": "the buyout negotiation lasted several days", "source": "omw" }, - { - "text": "they disagreed but kept an open dialogue", - "source": "omw" - }, - { - "text": "talks between Israelis and Palestinians", - "source": "omw" - } + { "text": "they disagreed but kept an open dialogue", "source": "omw" }, + { "text": "talks between Israelis and Palestinians", "source": "omw" } ], "it": [ { "text": "La contrattazione collettiva è importante per i lavoratori.", "source": "cefr" }, - { - "text": "Abbiamo chiuso un buon deal.", - "source": "cefr" - }, + { "text": "Abbiamo chiuso un buon deal.", "source": "cefr" }, { "text": "È importante mantenere un dialogo aperto.", "source": "cefr" @@ -1916,10 +1043,7 @@ "text": "Le negoziazioni per il nuovo contratto sono state lunghe e complesse.", "source": "cefr" }, - { - "text": "Le trattative sono in corso.", - "source": "cefr" - } + { "text": "Le trattative sono in corso.", "source": "cefr" } ], "de": [ { @@ -1942,14 +1066,8 @@ } ], "es": [ - { - "text": "La gestión del proyecto fue excelente.", - "source": "cefr" - }, - { - "text": "Las negociaciones fueron difíciles.", - "source": "cefr" - }, + { "text": "La gestión del proyecto fue excelente.", "source": "cefr" }, + { "text": "Las negociaciones fueron difíciles.", "source": "cefr" }, { "text": "La tramitación de los documentos puede llevar tiempo.", "source": "cefr" @@ -1958,59 +1076,29 @@ }, "votes": { "en": { - "negotiation": { - "cefr_source": "B2" - }, - "dialogue": { - "cefr_source": "B2" - } + "negotiation": { "cefr_source": "B2" }, + "dialogue": { "cefr_source": "B2" } }, "it": { - "contrattazione": { - "cefr_source": "B2" - }, - "deal": { - "cefr_source": "B1" - }, - "dialogo": { - "cefr_source": "B1" - }, - "negoziato": { - "cefr_source": "B2" - }, - "negoziazione": { - "cefr_source": "B2" - }, - "trattativa": { - "cefr_source": "B2" - } + "contrattazione": { "cefr_source": "B2" }, + "deal": { "cefr_source": "B1" }, + "dialogo": { "cefr_source": "B1" }, + "negoziato": { "cefr_source": "B2" }, + "negoziazione": { "cefr_source": "B2" }, + "trattativa": { "cefr_source": "B2" } }, "de": { - "Besprechung": { - "cefr_source": "B1" - }, - "Verhandlung": { - "cefr_source": "B2" - } + "Besprechung": { "cefr_source": "B1" }, + "Verhandlung": { "cefr_source": "B2" } }, "fr": { - "dialogue": { - "cefr_source": "B1" - }, - "négociation": { - "cefr_source": "B2" - } + "dialogue": { "cefr_source": "B1" }, + "négociation": { "cefr_source": "B2" } }, "es": { - "gestión": { - "cefr_source": "B2" - }, - "negociación": { - "cefr_source": "B2" - }, - "tramitación": { - "cefr_source": "B2" - } + "gestión": { "cefr_source": "B2" }, + "negociación": { "cefr_source": "B2" }, + "tramitación": { "cefr_source": "B2" } } }, "_sample_bucket": "has_glosses_and_examples" @@ -2019,13 +1107,8 @@ "source_id": "ili:i408", "pos": "adjective", "translations": { - "en": [ - "aground" - ], - "es": [ - "encallado", - "varado" - ], + "en": ["aground"], + "es": ["encallado", "varado"], "de": [ "aufgrund", "dank", @@ -2037,48 +1120,28 @@ ] }, "glosses": { - "en": [ - "stuck in a place where a ship can no longer float" - ], + "en": ["stuck in a place where a ship can no longer float"], "de": [ "an einer Stelle feststecken, an der ein Schiff nicht mehr schwimmen kann" ] }, "examples": { "en": [ - { - "text": "a ship aground offshore", - "source": "omw" - }, + { "text": "a ship aground offshore", "source": "omw" }, { "text": "a boat aground on the beach waiting for the tide to lift it", "source": "omw" } ], - "es": [ - { - "text": "El barco quedó varado en la arena.", - "source": "cefr" - } - ] - }, - "votes": { - "es": { - "varado": { - "cefr_source": "B2" - } - } + "es": [{ "text": "El barco quedó varado en la arena.", "source": "cefr" }] }, + "votes": { "es": { "varado": { "cefr_source": "B2" } } }, "_sample_bucket": "has_glosses_and_examples" }, { "source_id": "ili:i41575", "pos": "noun", - "translations": { - "en": [ - "walkout" - ] - }, + "translations": { "en": ["walkout"] }, "glosses": { "en": [ "the act of walking out (of a meeting or organization) as a sign of protest" @@ -2092,31 +1155,14 @@ } ] }, - "votes": { - "en": { - "walkout": { - "cefr_source": "B2" - } - } - }, + "votes": { "en": { "walkout": { "cefr_source": "B2" } } }, "_sample_bucket": "has_glosses_and_examples" }, { "source_id": "ili:i67480", "pos": "noun", - "translations": { - "en": [ - "tasting" - ], - "fr": [ - "dégustation" - ] - }, - "glosses": { - "en": [ - "a small amount (especially of food or wine)" - ] - }, + "translations": { "en": ["tasting"], "fr": ["dégustation"] }, + "glosses": { "en": ["a small amount (especially of food or wine)"] }, "examples": { "fr": [ { @@ -2126,40 +1172,19 @@ ] }, "votes": { - "en": { - "tasting": { - "cefr_source": "B1" - } - }, - "fr": { - "dégustation": { - "cefr_source": "B1" - } - } + "en": { "tasting": { "cefr_source": "B1" } }, + "fr": { "dégustation": { "cefr_source": "B1" } } }, "_sample_bucket": "has_glosses_and_examples" }, { "source_id": "ili:i11256", "pos": "adjective", - "translations": { - "en": [ - "hobnailed" - ] - }, + "translations": { "en": ["hobnailed"] }, "glosses": { - "en": [ - "marked by the wearing of heavy boots studded with hobnails" - ] - }, - "examples": { - "en": [ - { - "text": "hobnailed laborers", - "source": "omw" - } - ] + "en": ["marked by the wearing of heavy boots studded with hobnails"] }, + "examples": { "en": [{ "text": "hobnailed laborers", "source": "omw" }] }, "votes": {}, "_sample_bucket": "has_glosses_and_examples" }, @@ -2167,145 +1192,74 @@ "source_id": "ili:i86151", "pos": "noun", "translations": { - "en": [ - "sediment", - "deposit" - ], - "it": [ - "deposito", - "posatura", - "sedimento" - ], - "es": [ - "depósito", - "sedimento" - ], + "en": ["sediment", "deposit"], + "it": ["deposito", "posatura", "sedimento"], + "es": ["depósito", "sedimento"], "de": [ "Ablagerung", "Sedimentation", "Sedimentierung", "Sedimentbildung" ], - "fr": [ - "sédiment", - "dépôt" - ] + "fr": ["sédiment", "dépôt"] }, "glosses": { - "en": [ - "matter that has been deposited by some natural process" - ], - "de": [ - "Materie, die durch einen natürlichen Prozess abgelagert wurde" - ] + "en": ["matter that has been deposited by some natural process"], + "de": ["Materie, die durch einen natürlichen Prozess abgelagert wurde"] }, "examples": { "it": [ - { - "text": "Ho lasciato i bagagli al deposito.", - "source": "cefr" - }, + { "text": "Ho lasciato i bagagli al deposito.", "source": "cefr" }, { "text": "C'era un sedimento sul fondo della bottiglia.", "source": "cefr" } ], "de": [ - { - "text": "Es gab Ablagerungen in den Rohren.", - "source": "cefr" - } + { "text": "Es gab Ablagerungen in den Rohren.", "source": "cefr" } ], "fr": [ { "text": "Le sédiment au fond du lac est très fin.", "source": "cefr" }, - { - "text": "J'ai fait un dépôt à la banque.", - "source": "cefr" - } + { "text": "J'ai fait un dépôt à la banque.", "source": "cefr" } ], - "es": [ - { - "text": "Hice un depósito en el banco.", - "source": "cefr" - } - ] + "es": [{ "text": "Hice un depósito en el banco.", "source": "cefr" }] }, "votes": { "en": { - "sediment": { - "cefr_source": "C1" - }, - "deposit": { - "cefr_source": "B1" - } + "sediment": { "cefr_source": "C1" }, + "deposit": { "cefr_source": "B1" } }, "it": { - "deposito": { - "cefr_source": "B1" - }, - "sedimento": { - "cefr_source": "B2" - } - }, - "de": { - "Ablagerung": { - "cefr_source": "B2" - } + "deposito": { "cefr_source": "B1" }, + "sedimento": { "cefr_source": "B2" } }, + "de": { "Ablagerung": { "cefr_source": "B2" } }, "fr": { - "sédiment": { - "cefr_source": "B2" - }, - "dépôt": { - "cefr_source": "B1" - } + "sédiment": { "cefr_source": "B2" }, + "dépôt": { "cefr_source": "B1" } }, - "es": { - "depósito": { - "cefr_source": "B1" - } - } + "es": { "depósito": { "cefr_source": "B1" } } }, "_sample_bucket": "has_glosses_and_examples" }, { "source_id": "ili:i45550", "pos": "noun", - "translations": { - "en": [ - "conch" - ], - "fr": [ - "conque" - ] - }, + "translations": { "en": ["conch"], "fr": ["conque"] }, "glosses": { "en": [ "any of various edible tropical marine gastropods of the genus Strombus having a brightly-colored spiral shell with large outer lip" ] }, "examples": { - "fr": [ - { - "text": "On entend la mer dans une conque.", - "source": "cefr" - } - ] + "fr": [{ "text": "On entend la mer dans une conque.", "source": "cefr" }] }, "votes": { - "en": { - "conch": { - "cefr_source": "B1" - } - }, - "fr": { - "conque": { - "cefr_source": "B2" - } - } + "en": { "conch": { "cefr_source": "B1" } }, + "fr": { "conque": { "cefr_source": "B2" } } }, "_sample_bucket": "has_glosses_and_examples" }, @@ -2313,15 +1267,9 @@ "source_id": "ili:i117521", "pos": "noun", "translations": { - "en": [ - "moratorium" - ], - "it": [ - "moratoria" - ], - "fr": [ - "moratoire" - ] + "en": ["moratorium"], + "it": ["moratoria"], + "fr": ["moratoire"] }, "glosses": { "en": [ @@ -2343,21 +1291,9 @@ ] }, "votes": { - "en": { - "moratorium": { - "cefr_source": "C1" - } - }, - "it": { - "moratoria": { - "cefr_source": "C1" - } - }, - "fr": { - "moratoire": { - "cefr_source": "C1" - } - } + "en": { "moratorium": { "cefr_source": "C1" } }, + "it": { "moratoria": { "cefr_source": "C1" } }, + "fr": { "moratoire": { "cefr_source": "C1" } } }, "_sample_bucket": "has_glosses_and_examples" }, @@ -2365,20 +1301,10 @@ "source_id": "ili:i31764", "pos": "verb", "translations": { - "en": [ - "return" - ], - "fr": [ - "rendre", - "retourner", - "revenir" - ] - }, - "glosses": { - "en": [ - "return to a previous position; in mathematics" - ] + "en": ["return"], + "fr": ["rendre", "retourner", "revenir"] }, + "glosses": { "en": ["return to a previous position; in mathematics"] }, "examples": { "en": [ { @@ -2395,23 +1321,14 @@ "text": "Je dois retourner ce livre à la bibliothèque.", "source": "cefr" }, - { - "text": "Je dois revenir demain.", - "source": "cefr" - } + { "text": "Je dois revenir demain.", "source": "cefr" } ] }, "votes": { "fr": { - "rendre": { - "cefr_source": "A2" - }, - "retourner": { - "cefr_source": "A2" - }, - "revenir": { - "cefr_source": "A1" - } + "rendre": { "cefr_source": "A2" }, + "retourner": { "cefr_source": "A2" }, + "revenir": { "cefr_source": "A1" } } }, "_sample_bucket": "has_glosses_and_examples" @@ -2420,17 +1337,9 @@ "source_id": "ili:i48149", "pos": "noun", "translations": { - "en": [ - "post horse", - "post-horse", - "poster" - ], - "it": [ - "cavallo di posta" - ], - "fr": [ - "affiche" - ] + "en": ["post horse", "post-horse", "poster"], + "it": ["cavallo di posta"], + "fr": ["affiche"] }, "glosses": { "en": [ @@ -2439,23 +1348,12 @@ }, "examples": { "fr": [ - { - "text": "L'affiche du concert est très colorée.", - "source": "cefr" - } + { "text": "L'affiche du concert est très colorée.", "source": "cefr" } ] }, "votes": { - "en": { - "poster": { - "cefr_source": "A2" - } - }, - "fr": { - "affiche": { - "cefr_source": "A2" - } - } + "en": { "poster": { "cefr_source": "A2" } }, + "fr": { "affiche": { "cefr_source": "A2" } } }, "_sample_bucket": "has_glosses_and_examples" }, @@ -2463,47 +1361,20 @@ "source_id": "ili:i51126", "pos": "noun", "translations": { - "en": [ - "brickwork" - ], - "it": [ - "GAP!", - "muratura in mattoni" - ], - "es": [ - "aparejo", - "calicanto", - "enladrillado", - "mampostería" - ], - "fr": [ - "appareil" - ] - }, - "glosses": { - "en": [ - "masonry done with bricks and mortar" - ] + "en": ["brickwork"], + "it": ["GAP!", "muratura in mattoni"], + "es": ["aparejo", "calicanto", "enladrillado", "mampostería"], + "fr": ["appareil"] }, + "glosses": { "en": ["masonry done with bricks and mortar"] }, "examples": { "fr": [ - { - "text": "J'ai acheté un nouvel appareil photo.", - "source": "cefr" - } + { "text": "J'ai acheté un nouvel appareil photo.", "source": "cefr" } ] }, "votes": { - "en": { - "brickwork": { - "cefr_source": "B2" - } - }, - "fr": { - "appareil": { - "cefr_source": "B1" - } - } + "en": { "brickwork": { "cefr_source": "B2" } }, + "fr": { "appareil": { "cefr_source": "B1" } } }, "_sample_bucket": "has_glosses_and_examples" }, @@ -2511,41 +1382,26 @@ "source_id": "ili:i17542", "pos": "adjective", "translations": { - "en": [ - "interdisciplinary" - ], - "it": [ - "interdisciplinare", - "multidisciplinare" - ], + "en": ["interdisciplinary"], + "it": ["interdisciplinare", "multidisciplinare"], "de": [ "multidisziplinär", "fachübergreifend", "interdisziplinär", "fächerübergreifend" ], - "fr": [ - "interdisciplinaire" - ] + "fr": ["interdisciplinaire"] }, "glosses": { "en": [ "drawing from or characterized by participation of two or more fields of study" ], - "de": [ - "die Zusammenarbeit mehrerer Disziplinen betreffend\">" - ] + "de": ["die Zusammenarbeit mehrerer Disziplinen betreffend\">"] }, "examples": { "en": [ - { - "text": "interdisciplinary studies", - "source": "omw" - }, - { - "text": "an interdisciplinary conference", - "source": "omw" - } + { "text": "interdisciplinary studies", "source": "omw" }, + { "text": "an interdisciplinary conference", "source": "omw" } ], "it": [ { @@ -2567,26 +1423,10 @@ ] }, "votes": { - "en": { - "interdisciplinary": { - "cefr_source": "C1" - } - }, - "it": { - "interdisciplinare": { - "cefr_source": "C1" - } - }, - "de": { - "interdisziplinär": { - "cefr_source": "C1" - } - }, - "fr": { - "interdisciplinaire": { - "cefr_source": "C1" - } - } + "en": { "interdisciplinary": { "cefr_source": "C1" } }, + "it": { "interdisciplinare": { "cefr_source": "C1" } }, + "de": { "interdisziplinär": { "cefr_source": "C1" } }, + "fr": { "interdisciplinaire": { "cefr_source": "C1" } } }, "_sample_bucket": "has_glosses_and_examples" }, @@ -2594,15 +1434,9 @@ "source_id": "ili:i69459", "pos": "noun", "translations": { - "en": [ - "new edition" - ], - "it": [ - "riedizione" - ], - "fr": [ - "new edition" - ] + "en": ["new edition"], + "it": ["riedizione"], + "fr": ["new edition"] }, "glosses": { "en": [ @@ -2617,34 +1451,19 @@ } ] }, - "votes": { - "it": { - "riedizione": { - "cefr_source": "C1" - } - } - }, + "votes": { "it": { "riedizione": { "cefr_source": "C1" } } }, "_sample_bucket": "has_glosses_and_examples" }, { "source_id": "ili:i75841", "pos": "noun", "translations": { - "en": [ - "stampede" - ], - "de": [ - "Stampede", - "Herdenpanik" - ], - "fr": [ - "débandade" - ] + "en": ["stampede"], + "de": ["Stampede", "Herdenpanik"], + "fr": ["débandade"] }, "glosses": { - "en": [ - "a wild headlong rush of frightened animals (horses or cattle)" - ], + "en": ["a wild headlong rush of frightened animals (horses or cattle)"], "de": [ "eine wilde, kopfüber laufende Flucht von verängstigten Tieren (Pferden oder Rindern)" ] @@ -2658,16 +1477,8 @@ ] }, "votes": { - "en": { - "stampede": { - "cefr_source": "B2" - } - }, - "fr": { - "débandade": { - "cefr_source": "C1" - } - } + "en": { "stampede": { "cefr_source": "B2" } }, + "fr": { "débandade": { "cefr_source": "C1" } } }, "_sample_bucket": "has_glosses_and_examples" }, @@ -2675,22 +1486,11 @@ "source_id": "ili:i67108", "pos": "noun", "translations": { - "en": [ - "stocktaking", - "stock-taking" - ], - "it": [ - "inventario" - ], - "es": [ - "balance" - ] - }, - "glosses": { - "en": [ - "reappraisal of a situation or position or outlook" - ] + "en": ["stocktaking", "stock-taking"], + "it": ["inventario"], + "es": ["balance"] }, + "glosses": { "en": ["reappraisal of a situation or position or outlook"] }, "examples": { "it": [ { @@ -2706,16 +1506,8 @@ ] }, "votes": { - "it": { - "inventario": { - "cefr_source": "B2" - } - }, - "es": { - "balance": { - "cefr_source": "B1" - } - } + "it": { "inventario": { "cefr_source": "B2" } }, + "es": { "balance": { "cefr_source": "B1" } } }, "_sample_bucket": "has_glosses_and_examples" }, @@ -2733,9 +1525,7 @@ "whacky", "zany" ], - "es": [ - "tonto" - ], + "es": ["tonto"], "de": [ "albern", "naiv", @@ -2749,164 +1539,69 @@ "infantil", "puerilistisch" ], - "fr": [ - "déraisonnable", - "fou", - "drôle", - "aberrant" - ] - }, - "glosses": { - "en": [ - "ludicrous, foolish" - ], - "de": [ - "lächerlich, töricht" - ] + "fr": ["déraisonnable", "fou", "drôle", "aberrant"] }, + "glosses": { "en": ["ludicrous, foolish"], "de": ["lächerlich, töricht"] }, "examples": { "en": [ { "text": "gave me a cockamamie reason for not going", "source": "omw" }, - { - "text": "wore a goofy hat", - "source": "omw" - }, - { - "text": "a silly idea", - "source": "omw" - }, - { - "text": "some wacky plan for selling more books", - "source": "omw" - } + { "text": "wore a goofy hat", "source": "omw" }, + { "text": "a silly idea", "source": "omw" }, + { "text": "some wacky plan for selling more books", "source": "omw" } ], "de": [ - { - "text": "Hör auf, so albern zu sein!", - "source": "cefr" - }, - { - "text": "Sie ist manchmal etwas naiv.", - "source": "cefr" - }, - { - "text": "Die Früchte sind noch unreif.", - "source": "cefr" - }, - { - "text": "Sie hat eine sehr kindliche Freude.", - "source": "cefr" - }, - { - "text": "Sein Verhalten war ziemlich kindisch.", - "source": "cefr" - } + { "text": "Hör auf, so albern zu sein!", "source": "cefr" }, + { "text": "Sie ist manchmal etwas naiv.", "source": "cefr" }, + { "text": "Die Früchte sind noch unreif.", "source": "cefr" }, + { "text": "Sie hat eine sehr kindliche Freude.", "source": "cefr" }, + { "text": "Sein Verhalten war ziemlich kindisch.", "source": "cefr" } ], "fr": [ - { - "text": "Ses exigences sont déraisonnables.", - "source": "cefr" - }, - { - "text": "C'est une idée folle.", - "source": "cefr" - }, - { - "text": "C'est une histoire drôle.", - "source": "cefr" - }, + { "text": "Ses exigences sont déraisonnables.", "source": "cefr" }, + { "text": "C'est une idée folle.", "source": "cefr" }, + { "text": "C'est une histoire drôle.", "source": "cefr" }, { "text": "Son comportement était aberrant et choquant.", "source": "cefr" } ], - "es": [ - { - "text": "No seas tonto, eso no es verdad.", - "source": "cefr" - } - ] + "es": [{ "text": "No seas tonto, eso no es verdad.", "source": "cefr" }] }, "votes": { "en": { - "goofy": { - "cefr_source": "B1" - }, - "sappy": { - "cefr_source": "B2" - }, - "silly": { - "cefr_source": "A2" - }, - "wacky": { - "cefr_source": "B2" - }, - "zany": { - "cefr_source": "B2" - } + "goofy": { "cefr_source": "B1" }, + "sappy": { "cefr_source": "B2" }, + "silly": { "cefr_source": "A2" }, + "wacky": { "cefr_source": "B2" }, + "zany": { "cefr_source": "B2" } }, "de": { - "albern": { - "cefr_source": "B1" - }, - "naiv": { - "cefr_source": "B1" - }, - "unreif": { - "cefr_source": "B1" - }, - "kindlich": { - "cefr_source": "B1" - }, - "kindisch": { - "cefr_source": "B1" - } + "albern": { "cefr_source": "B1" }, + "naiv": { "cefr_source": "B1" }, + "unreif": { "cefr_source": "B1" }, + "kindlich": { "cefr_source": "B1" }, + "kindisch": { "cefr_source": "B1" } }, "fr": { - "déraisonnable": { - "cefr_source": "B2" - }, - "fou": { - "cefr_source": "B1" - }, - "drôle": { - "cefr_source": "A2" - }, - "aberrant": { - "cefr_source": "C1" - } + "déraisonnable": { "cefr_source": "B2" }, + "fou": { "cefr_source": "B1" }, + "drôle": { "cefr_source": "A2" }, + "aberrant": { "cefr_source": "C1" } }, - "es": { - "tonto": { - "cefr_source": "A2" - } - } + "es": { "tonto": { "cefr_source": "A2" } } }, "_sample_bucket": "has_glosses_and_examples" }, { "source_id": "ili:i1291", "pos": "adjective", - "translations": { - "en": [ - "unifacial" - ] - }, - "glosses": { - "en": [ - "having but one principal or specialized surface" - ] - }, + "translations": { "en": ["unifacial"] }, + "glosses": { "en": ["having but one principal or specialized surface"] }, "examples": { - "en": [ - { - "text": "a primitive unifacial flint tool", - "source": "omw" - } - ] + "en": [{ "text": "a primitive unifacial flint tool", "source": "omw" }] }, "votes": {}, "_sample_bucket": "has_glosses_and_examples" @@ -2915,26 +1610,11 @@ "source_id": "ili:i73668", "pos": "noun", "translations": { - "en": [ - "cantata", - "oratorio" - ], - "it": [ - "cantata", - "oratorio" - ], - "es": [ - "oratorio" - ], - "de": [ - "Andachtsraum", - "Oratorium", - "Gebetsraum" - ], - "fr": [ - "oratorio", - "cantate" - ] + "en": ["cantata", "oratorio"], + "it": ["cantata", "oratorio"], + "es": ["oratorio"], + "de": ["Andachtsraum", "Oratorium", "Gebetsraum"], + "fr": ["oratorio", "cantate"] }, "glosses": { "en": [ @@ -2965,91 +1645,39 @@ ] }, "votes": { - "it": { - "oratorio": { - "cefr_source": "B1" - } - }, - "de": { - "Oratorium": { - "cefr_source": "C1" - } - }, - "es": { - "oratorio": { - "cefr_source": "C1" - } - } + "it": { "oratorio": { "cefr_source": "B1" } }, + "de": { "Oratorium": { "cefr_source": "C1" } }, + "es": { "oratorio": { "cefr_source": "C1" } } }, "_sample_bucket": "has_glosses_and_examples" }, { "source_id": "ili:i39774", "pos": "noun", - "translations": { - "en": [ - "respiration" - ], - "es": [ - "respiración" - ] - }, - "glosses": { - "en": [ - "a single complete act of breathing in and out" - ] - }, + "translations": { "en": ["respiration"], "es": ["respiración"] }, + "glosses": { "en": ["a single complete act of breathing in and out"] }, "examples": { - "en": [ - { - "text": "thirty respirations per minute", - "source": "omw" - } - ], + "en": [{ "text": "thirty respirations per minute", "source": "omw" }], "es": [ - { - "text": "Su respiración era lenta y profunda.", - "source": "cefr" - } + { "text": "Su respiración era lenta y profunda.", "source": "cefr" } ] }, "votes": { - "en": { - "respiration": { - "cefr_source": "B2" - } - }, - "es": { - "respiración": { - "cefr_source": "B1" - } - } + "en": { "respiration": { "cefr_source": "B2" } }, + "es": { "respiración": { "cefr_source": "B1" } } }, "_sample_bucket": "has_glosses_and_examples" }, { "source_id": "ili:i28838", "pos": "verb", - "translations": { - "en": [ - "unplug", - "disconnect" - ], - "fr": [ - "débrancher" - ] - }, + "translations": { "en": ["unplug", "disconnect"], "fr": ["débrancher"] }, "glosses": { - "en": [ - "pull the plug of (electrical appliances) and render inoperable" - ] + "en": ["pull the plug of (electrical appliances) and render inoperable"] }, "examples": { "en": [ - { - "text": "unplug the hair dryer after using it", - "source": "omw" - } + { "text": "unplug the hair dryer after using it", "source": "omw" } ], "fr": [ { @@ -3059,16 +1687,8 @@ ] }, "votes": { - "en": { - "unplug": { - "cefr_source": "A2" - } - }, - "fr": { - "débrancher": { - "cefr_source": "A2" - } - } + "en": { "unplug": { "cefr_source": "A2" } }, + "fr": { "débrancher": { "cefr_source": "A2" } } }, "_sample_bucket": "has_glosses_and_examples" }, @@ -3076,20 +1696,10 @@ "source_id": "ili:i85884", "pos": "noun", "translations": { - "en": [ - "North Sea" - ], - "es": [ - "Mar del Norte" - ], - "de": [ - "Nordsee", - "Deutsches Meer" - ], - "fr": [ - "mer du Nord", - "Mer du Nord" - ] + "en": ["North Sea"], + "es": ["Mar del Norte"], + "de": ["Nordsee", "Deutsches Meer"], + "fr": ["mer du Nord", "Mer du Nord"] }, "glosses": { "en": [ @@ -3101,31 +1711,18 @@ }, "examples": { "de": [ - { - "text": "Wir fahren im Sommer an die Nordsee.", - "source": "cefr" - } + { "text": "Wir fahren im Sommer an die Nordsee.", "source": "cefr" } ] }, - "votes": { - "de": { - "Nordsee": { - "cefr_source": "A2" - } - } - }, + "votes": { "de": { "Nordsee": { "cefr_source": "A2" } } }, "_sample_bucket": "no_glosses_no_examples" }, { "source_id": "ili:i57058", "pos": "noun", "translations": { - "en": [ - "patriarchal cross" - ], - "es": [ - "cruz patriarcal" - ], + "en": ["patriarchal cross"], + "es": ["cruz patriarcal"], "de": [ "Erzbischofskreuz", "Spanisches Kreuz", @@ -3135,12 +1732,8 @@ ] }, "glosses": { - "en": [ - "a cross with two crossbars" - ], - "de": [ - "ein Kreuz mit zwei Querbalken" - ] + "en": ["a cross with two crossbars"], + "de": ["ein Kreuz mit zwei Querbalken"] }, "examples": {}, "votes": {}, @@ -3150,19 +1743,10 @@ "source_id": "ili:i14067", "pos": "adjective", "translations": { - "en": [ - "maximizing", - "maximising" - ], - "fr": [ - "maximaliste" - ] - }, - "glosses": { - "en": [ - "making as great as possible" - ] + "en": ["maximizing", "maximising"], + "fr": ["maximaliste"] }, + "glosses": { "en": ["making as great as possible"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_glosses_no_examples" @@ -3171,27 +1755,14 @@ "source_id": "ili:i57206", "pos": "noun", "translations": { - "en": [ - "photocathode" - ], - "es": [ - "fotocátodo" - ], - "de": [ - "Photokathode", - "Fotokathode" - ], - "fr": [ - "photocathode" - ] + "en": ["photocathode"], + "es": ["fotocátodo"], + "de": ["Photokathode", "Fotokathode"], + "fr": ["photocathode"] }, "glosses": { - "en": [ - "a cathode that emits electrons when illuminated" - ], - "de": [ - "eine Kathode, die bei Beleuchtung Elektronen abgibt" - ] + "en": ["a cathode that emits electrons when illuminated"], + "de": ["eine Kathode, die bei Beleuchtung Elektronen abgibt"] }, "examples": {}, "votes": {}, @@ -3201,25 +1772,11 @@ "source_id": "ili:i97025", "pos": "noun", "translations": { - "en": [ - "Stockton", - "Frank Stockton", - "Francis Richard Stockton" - ], - "es": [ - "Francis Richard Stockton", - "Frank Stockton", - "Stockton" - ], - "fr": [ - "Stockton" - ] - }, - "glosses": { - "en": [ - "United States writer (1834-1902)" - ] + "en": ["Stockton", "Frank Stockton", "Francis Richard Stockton"], + "es": ["Francis Richard Stockton", "Frank Stockton", "Stockton"], + "fr": ["Stockton"] }, + "glosses": { "en": ["United States writer (1834-1902)"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_glosses_no_examples" @@ -3227,11 +1784,7 @@ { "source_id": "ili:i101248", "pos": "noun", - "translations": { - "en": [ - "obeche" - ] - }, + "translations": { "en": ["obeche"] }, "glosses": { "en": [ "the wood of an African obeche tree; used especially for veneering" @@ -3245,13 +1798,8 @@ "source_id": "ili:i94985", "pos": "noun", "translations": { - "en": [ - "Eames", - "Charles Eames" - ], - "es": [ - "Charles Eames" - ] + "en": ["Eames", "Charles Eames"], + "es": ["Charles Eames"] }, "glosses": { "en": [ @@ -3266,20 +1814,10 @@ "source_id": "ili:i16699", "pos": "adjective", "translations": { - "en": [ - "mensural", - "measured", - "mensurable" - ], - "es": [ - "mensural" - ] - }, - "glosses": { - "en": [ - "having notes of fixed rhythmic value" - ] + "en": ["mensural", "measured", "mensurable"], + "es": ["mensural"] }, + "glosses": { "en": ["having notes of fixed rhythmic value"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_glosses_no_examples" @@ -3288,13 +1826,8 @@ "source_id": "ili:i99999", "pos": "noun", "translations": { - "en": [ - "China aster", - "Callistephus chinensis" - ], - "fr": [ - "callistephus chinensis" - ] + "en": ["China aster", "Callistephus chinensis"], + "fr": ["callistephus chinensis"] }, "glosses": { "en": [ @@ -3308,19 +1841,8 @@ { "source_id": "ili:i75135", "pos": "noun", - "translations": { - "en": [ - "kiss of death" - ], - "fr": [ - "baiser de la mort" - ] - }, - "glosses": { - "en": [ - "something that is ruinous" - ] - }, + "translations": { "en": ["kiss of death"], "fr": ["baiser de la mort"] }, + "glosses": { "en": ["something that is ruinous"] }, "examples": { "en": [ { @@ -3335,11 +1857,7 @@ { "source_id": "ili:i36428", "pos": "noun", - "translations": { - "en": [ - "dark adaptation" - ] - }, + "translations": { "en": ["dark adaptation"] }, "glosses": { "en": [ "the process of adjusting the eyes to low levels of illumination; cones adapt first; rods continue to adapt for up to four hours" @@ -3353,16 +1871,10 @@ "source_id": "ili:i103092", "pos": "noun", "translations": { - "en": [ - "saw palmetto", - "scrub palmetto", - "Serenoa repens" - ] + "en": ["saw palmetto", "scrub palmetto", "Serenoa repens"] }, "glosses": { - "en": [ - "small hardy clump-forming spiny palm of southern United States" - ] + "en": ["small hardy clump-forming spiny palm of southern United States"] }, "examples": {}, "votes": {}, @@ -3371,16 +1883,8 @@ { "source_id": "ili:i14834", "pos": "adjective", - "translations": { - "en": [ - "zoic" - ] - }, - "glosses": { - "en": [ - "pertaining to animals or animal life or action" - ] - }, + "translations": { "en": ["zoic"] }, + "glosses": { "en": ["pertaining to animals or animal life or action"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_glosses_no_examples" @@ -3388,19 +1892,8 @@ { "source_id": "ili:i25953", "pos": "verb", - "translations": { - "en": [ - "blog" - ], - "es": [ - "blogear" - ] - }, - "glosses": { - "en": [ - "read, write, or edit a shared on-line journal" - ] - }, + "translations": { "en": ["blog"], "es": ["blogear"] }, + "glosses": { "en": ["read, write, or edit a shared on-line journal"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_glosses_no_examples" @@ -3408,27 +1901,9 @@ { "source_id": "ili:i24441", "pos": "verb", - "translations": { - "en": [ - "ream" - ], - "es": [ - "taladrar" - ] - }, - "glosses": { - "en": [ - "enlarge with a reamer" - ] - }, - "examples": { - "en": [ - { - "text": "ream a hole", - "source": "omw" - } - ] - }, + "translations": { "en": ["ream"], "es": ["taladrar"] }, + "glosses": { "en": ["enlarge with a reamer"] }, + "examples": { "en": [{ "text": "ream a hole", "source": "omw" }] }, "votes": {}, "_sample_bucket": "no_glosses_no_examples" }, @@ -3436,19 +1911,10 @@ "source_id": "ili:i60874", "pos": "noun", "translations": { - "en": [ - "virtual memory", - "virtual storage" - ], - "it": [ - "memoria virtuale" - ], - "es": [ - "memoria virtual" - ], - "fr": [ - "mémoire virtuelle" - ] + "en": ["virtual memory", "virtual storage"], + "it": ["memoria virtuale"], + "es": ["memoria virtual"], + "fr": ["mémoire virtuelle"] }, "glosses": { "en": [ @@ -3463,14 +1929,8 @@ "source_id": "ili:i105979", "pos": "noun", "translations": { - "en": [ - "Dryopteris", - "genus Dryopteris" - ], - "fr": [ - "Dryopteris", - "dryopteris" - ] + "en": ["Dryopteris", "genus Dryopteris"], + "fr": ["Dryopteris", "dryopteris"] }, "glosses": { "en": [ @@ -3485,19 +1945,10 @@ "source_id": "ili:i44411", "pos": "noun", "translations": { - "en": [ - "blue racer", - "Coluber constrictor flaviventris" - ], - "fr": [ - "coluber constrictor" - ] - }, - "glosses": { - "en": [ - "bluish-green blacksnake found from Ohio to Texas" - ] + "en": ["blue racer", "Coluber constrictor flaviventris"], + "fr": ["coluber constrictor"] }, + "glosses": { "en": ["bluish-green blacksnake found from Ohio to Texas"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_glosses_no_examples" @@ -3506,18 +1957,11 @@ "source_id": "ili:i14592", "pos": "adjective", "translations": { - "en": [ - "anagrammatic", - "anagrammatical" - ], - "it": [ - "anagrammatico" - ] + "en": ["anagrammatic", "anagrammatical"], + "it": ["anagrammatico"] }, "glosses": { - "en": [ - "related to anagrams or containing or making an anagram" - ] + "en": ["related to anagrams or containing or making an anagram"] }, "examples": {}, "votes": {}, @@ -3527,19 +1971,10 @@ "source_id": "ili:i5174", "pos": "adjective", "translations": { - "en": [ - "protrusile", - "protrusible" - ], - "fr": [ - "protrusible" - ] - }, - "glosses": { - "en": [ - "capable of being thrust forward, as the tongue" - ] + "en": ["protrusile", "protrusible"], + "fr": ["protrusible"] }, + "glosses": { "en": ["capable of being thrust forward, as the tongue"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_glosses_no_examples" @@ -3547,17 +1982,8 @@ { "source_id": "ili:i99278", "pos": "noun", - "translations": { - "en": [ - "pink calla", - "Zantedeschia rehmanii" - ] - }, - "glosses": { - "en": [ - "calla having a rose-colored spathe" - ] - }, + "translations": { "en": ["pink calla", "Zantedeschia rehmanii"] }, + "glosses": { "en": ["calla having a rose-colored spathe"] }, "examples": {}, "votes": {}, "_sample_bucket": "pos_spread" @@ -3566,16 +1992,9 @@ "source_id": "ili:i97983", "pos": "noun", "translations": { - "en": [ - "phosphorescence" - ], - "it": [ - "fosforescenza", - "fotoluminescenza" - ], - "fr": [ - "phosphorescence" - ] + "en": ["phosphorescence"], + "it": ["fosforescenza", "fotoluminescenza"], + "fr": ["phosphorescence"] }, "glosses": { "en": [ @@ -3589,16 +2008,9 @@ { "source_id": "ili:i54194", "pos": "noun", - "translations": { - "en": [ - "garrison cap", - "overseas cap" - ] - }, + "translations": { "en": ["garrison cap", "overseas cap"] }, "glosses": { - "en": [ - "a wedge-shaped wool or cotton cap; worn as part of a uniform" - ] + "en": ["a wedge-shaped wool or cotton cap; worn as part of a uniform"] }, "examples": {}, "votes": {}, @@ -3607,20 +2019,8 @@ { "source_id": "ili:i102972", "pos": "noun", - "translations": { - "en": [ - "Tipuana", - "genus Tipuana" - ], - "fr": [ - "tipuana" - ] - }, - "glosses": { - "en": [ - "one species: South American tree: tipu tree" - ] - }, + "translations": { "en": ["Tipuana", "genus Tipuana"], "fr": ["tipuana"] }, + "glosses": { "en": ["one species: South American tree: tipu tree"] }, "examples": {}, "votes": {}, "_sample_bucket": "pos_spread" @@ -3628,38 +2028,18 @@ { "source_id": "ili:i55386", "pos": "noun", - "translations": { - "en": [ - "king" - ], - "fr": [ - "roi" - ] - }, + "translations": { "en": ["king"], "fr": ["roi"] }, "glosses": { "en": [ "a checker that has been moved to the opponent's first row where it is promoted to a piece that is free to move either forward or backward" ] }, "examples": { - "fr": [ - { - "text": "Le roi a visité la ville.", - "source": "cefr" - } - ] + "fr": [{ "text": "Le roi a visité la ville.", "source": "cefr" }] }, "votes": { - "en": { - "king": { - "cefr_source": "A2" - } - }, - "fr": { - "roi": { - "cefr_source": "B1" - } - } + "en": { "king": { "cefr_source": "A2" } }, + "fr": { "roi": { "cefr_source": "B1" } } }, "_sample_bucket": "pos_spread" }, @@ -3667,47 +2047,19 @@ "source_id": "ili:i26482", "pos": "verb", "translations": { - "en": [ - "articulate", - "enunciate", - "vocalize", - "vocalise" - ], - "it": [ - "articolare", - "enunciare", - "enunziare", - "scandire" - ], - "es": [ - "articular" - ], - "de": [ - "ausdrücken", - "artikulieren" - ], - "fr": [ - "articuler", - "exprimer", - "énoncer", - "formuler", - "vocaliser" - ] + "en": ["articulate", "enunciate", "vocalize", "vocalise"], + "it": ["articolare", "enunciare", "enunziare", "scandire"], + "es": ["articular"], + "de": ["ausdrücken", "artikulieren"], + "fr": ["articuler", "exprimer", "énoncer", "formuler", "vocaliser"] }, "glosses": { - "en": [ - "express or state clearly" - ], - "de": [ - "klar ausdrücken oder erklären" - ] + "en": ["express or state clearly"], + "de": ["klar ausdrücken oder erklären"] }, "examples": { "it": [ - { - "text": "È importante articolare bene le parole.", - "source": "cefr" - } + { "text": "È importante articolare bene le parole.", "source": "cefr" } ], "de": [ { @@ -3745,105 +2097,44 @@ ] }, "votes": { - "en": { - "articulate": { - "cefr_source": "B2" - } - }, - "it": { - "articolare": { - "cefr_source": "B2" - } - }, + "en": { "articulate": { "cefr_source": "B2" } }, + "it": { "articolare": { "cefr_source": "B2" } }, "de": { - "ausdrücken": { - "cefr_source": "B1" - }, - "artikulieren": { - "cefr_source": "B2" - } + "ausdrücken": { "cefr_source": "B1" }, + "artikulieren": { "cefr_source": "B2" } }, "fr": { - "articuler": { - "cefr_source": "B1" - }, - "exprimer": { - "cefr_source": "B1" - }, - "énoncer": { - "cefr_source": "B2" - }, - "formuler": { - "cefr_source": "B2" - } + "articuler": { "cefr_source": "B1" }, + "exprimer": { "cefr_source": "B1" }, + "énoncer": { "cefr_source": "B2" }, + "formuler": { "cefr_source": "B2" } }, - "es": { - "articular": { - "cefr_source": "B2" - } - } + "es": { "articular": { "cefr_source": "B2" } } }, "_sample_bucket": "pos_spread" }, { "source_id": "ili:i22492", "pos": "verb", - "translations": { - "en": [ - "spike" - ] - }, - "glosses": { - "en": [ - "manifest a sharp increase" - ] - }, - "examples": { - "en": [ - { - "text": "the voltage spiked", - "source": "omw" - } - ] - }, + "translations": { "en": ["spike"] }, + "glosses": { "en": ["manifest a sharp increase"] }, + "examples": { "en": [{ "text": "the voltage spiked", "source": "omw" }] }, "votes": {}, "_sample_bucket": "pos_spread" }, { "source_id": "ili:i26383", "pos": "verb", - "translations": { - "en": [ - "redefine" - ], - "fr": [ - "redéfinir" - ] - }, - "glosses": { - "en": [ - "give a new or different definition of (a word)" - ] - }, + "translations": { "en": ["redefine"], "fr": ["redéfinir"] }, + "glosses": { "en": ["give a new or different definition of (a word)"] }, "examples": { "fr": [ - { - "text": "Il est temps de redéfinir nos objectifs.", - "source": "cefr" - } + { "text": "Il est temps de redéfinir nos objectifs.", "source": "cefr" } ] }, "votes": { - "en": { - "redefine": { - "cefr_source": "B2" - } - }, - "fr": { - "redéfinir": { - "cefr_source": "B2" - } - } + "en": { "redefine": { "cefr_source": "B2" } }, + "fr": { "redéfinir": { "cefr_source": "B2" } } }, "_sample_bucket": "pos_spread" }, @@ -3851,15 +2142,8 @@ "source_id": "ili:i22943", "pos": "verb", "translations": { - "en": [ - "slake", - "abate", - "slack" - ], - "es": [ - "aflojar", - "reducir" - ], + "en": ["slake", "abate", "slack"], + "es": ["aflojar", "reducir"], "fr": [ "descendre", "cesser", @@ -3870,47 +2154,25 @@ "supprimer" ] }, - "glosses": { - "en": [ - "make less active or intense" - ] - }, + "glosses": { "en": ["make less active or intense"] }, "examples": { "fr": [ { "text": "Nous allons descendre au rez-de-chaussée.", "source": "cefr" }, - { - "text": "La pluie a cessé de tomber.", - "source": "cefr" - }, - { - "text": "Nous devons réduire nos dépenses.", - "source": "cefr" - }, - { - "text": "Il faut ralentir avant le virage.", - "source": "cefr" - }, + { "text": "La pluie a cessé de tomber.", "source": "cefr" }, + { "text": "Nous devons réduire nos dépenses.", "source": "cefr" }, + { "text": "Il faut ralentir avant le virage.", "source": "cefr" }, { "text": "Ces mesures visent à amoindrir l'impact de la crise.", "source": "cefr" }, - { - "text": "Les prix ont commencé à diminuer.", - "source": "cefr" - }, - { - "text": "Il faut supprimer les fichiers inutiles.", - "source": "cefr" - } + { "text": "Les prix ont commencé à diminuer.", "source": "cefr" }, + { "text": "Il faut supprimer les fichiers inutiles.", "source": "cefr" } ], "es": [ - { - "text": "Tienes que aflojar el nudo.", - "source": "cefr" - }, + { "text": "Tienes que aflojar el nudo.", "source": "cefr" }, { "text": "Necesitamos reducir el consumo de energía.", "source": "cefr" @@ -3918,41 +2180,19 @@ ] }, "votes": { - "en": { - "abate": { - "cefr_source": "C1" - } - }, + "en": { "abate": { "cefr_source": "C1" } }, "fr": { - "descendre": { - "cefr_source": "A2" - }, - "cesser": { - "cefr_source": "B1" - }, - "réduire": { - "cefr_source": "B1" - }, - "ralentir": { - "cefr_source": "B1" - }, - "amoindrir": { - "cefr_source": "C1" - }, - "diminuer": { - "cefr_source": "B1" - }, - "supprimer": { - "cefr_source": "B2" - } + "descendre": { "cefr_source": "A2" }, + "cesser": { "cefr_source": "B1" }, + "réduire": { "cefr_source": "B1" }, + "ralentir": { "cefr_source": "B1" }, + "amoindrir": { "cefr_source": "C1" }, + "diminuer": { "cefr_source": "B1" }, + "supprimer": { "cefr_source": "B2" } }, "es": { - "aflojar": { - "cefr_source": "B1" - }, - "reducir": { - "cefr_source": "B1" - } + "aflojar": { "cefr_source": "B1" }, + "reducir": { "cefr_source": "B1" } } }, "_sample_bucket": "pos_spread" @@ -3960,50 +2200,22 @@ { "source_id": "ili:i31348", "pos": "verb", - "translations": { - "en": [ - "romp" - ] - }, - "glosses": { - "en": [ - "run easily and fairly fast" - ] - }, + "translations": { "en": ["romp"] }, + "glosses": { "en": ["run easily and fairly fast"] }, "examples": {}, - "votes": { - "en": { - "romp": { - "cefr_source": "B2" - } - } - }, + "votes": { "en": { "romp": { "cefr_source": "B2" } } }, "_sample_bucket": "pos_spread" }, { "source_id": "ili:i10413", "pos": "adjective", "translations": { - "en": [ - "imprudent" - ], - "it": [ - "imprudente", - "incauto" - ], - "es": [ - "imprudente", - "insensato" - ], - "fr": [ - "imprudent" - ] - }, - "glosses": { - "en": [ - "not prudent or wise" - ] + "en": ["imprudent"], + "it": ["imprudente", "incauto"], + "es": ["imprudente", "insensato"], + "fr": ["imprudent"] }, + "glosses": { "en": ["not prudent or wise"] }, "examples": { "en": [ { @@ -4032,30 +2244,15 @@ "text": "Fue una decisión imprudente conducir tan rápido.", "source": "cefr" }, - { - "text": "Fue una decisión insensata.", - "source": "cefr" - } + { "text": "Fue una decisión insensata.", "source": "cefr" } ] }, "votes": { - "it": { - "imprudente": { - "cefr_source": "B2" - } - }, - "fr": { - "imprudent": { - "cefr_source": "B2" - } - }, + "it": { "imprudente": { "cefr_source": "B2" } }, + "fr": { "imprudent": { "cefr_source": "B2" } }, "es": { - "imprudente": { - "cefr_source": "B2" - }, - "insensato": { - "cefr_source": "B2" - } + "imprudente": { "cefr_source": "B2" }, + "insensato": { "cefr_source": "B2" } } }, "_sample_bucket": "pos_spread" @@ -4064,36 +2261,13 @@ "source_id": "ili:i8645", "pos": "adjective", "translations": { - "en": [ - "metaphysical" - ], - "es": [ - "metafísico" - ], - "fr": [ - "métaphysique" - ] - }, - "glosses": { - "en": [ - "without material form or substance" - ] - }, - "examples": { - "en": [ - { - "text": "metaphysical forces", - "source": "omw" - } - ] - }, - "votes": { - "en": { - "metaphysical": { - "cefr_source": "C1" - } - } + "en": ["metaphysical"], + "es": ["metafísico"], + "fr": ["métaphysique"] }, + "glosses": { "en": ["without material form or substance"] }, + "examples": { "en": [{ "text": "metaphysical forces", "source": "omw" }] }, + "votes": { "en": { "metaphysical": { "cefr_source": "C1" } } }, "_sample_bucket": "pos_spread" }, { @@ -4107,13 +2281,8 @@ "essential", "of the essence" ], - "it": [ - "essenziale" - ], - "es": [ - "crucial", - "esencial" - ], + "it": ["essenziale"], + "es": ["crucial", "esencial"], "de": [ "bedeutsam", "wesentlich", @@ -4124,52 +2293,26 @@ "aussagekräftig", "signifikant" ], - "fr": [ - "essentiel" - ] + "fr": ["essentiel"] }, "glosses": { - "en": [ - "of the greatest importance" - ], - "de": [ - "von allergrößter Bedeutung" - ] + "en": ["of the greatest importance"], + "de": ["von allergrößter Bedeutung"] }, "examples": { "en": [ - { - "text": "the all-important subject of disarmament", - "source": "omw" - }, - { - "text": "crucial information", - "source": "omw" - }, - { - "text": "in chess cool nerves are of the essence", - "source": "omw" - } - ], - "it": [ - { - "text": "L'acqua è essenziale per la vita.", - "source": "cefr" - } + { "text": "the all-important subject of disarmament", "source": "omw" }, + { "text": "crucial information", "source": "omw" }, + { "text": "in chess cool nerves are of the essence", "source": "omw" } ], + "it": [{ "text": "L'acqua è essenziale per la vita.", "source": "cefr" }], "de": [ { "text": "Das war ein bedeutsamer Moment in der Geschichte.", "source": "cefr" }, - { - "text": "Das ist ein wesentlicher Unterschied.", - "source": "cefr" - }, - { - "text": "Das ist eine wichtige Information.", - "source": "cefr" - }, + { "text": "Das ist ein wesentlicher Unterschied.", "source": "cefr" }, + { "text": "Das ist eine wichtige Information.", "source": "cefr" }, { "text": "Er formulierte seine Gedanken sehr prägnant.", "source": "cefr" @@ -4178,10 +2321,7 @@ "text": "Die Studie lieferte aussagekräftige Ergebnisse.", "source": "cefr" }, - { - "text": "Es gab eine signifikante Veränderung.", - "source": "cefr" - } + { "text": "Es gab eine signifikante Veränderung.", "source": "cefr" } ], "fr": [ { @@ -4190,62 +2330,28 @@ } ], "es": [ - { - "text": "Es crucial que lleguemos a tiempo.", - "source": "cefr" - }, - { - "text": "El agua es esencial para la vida.", - "source": "cefr" - } + { "text": "Es crucial que lleguemos a tiempo.", "source": "cefr" }, + { "text": "El agua es esencial para la vida.", "source": "cefr" } ] }, "votes": { "en": { - "crucial": { - "cefr_source": "B2" - }, - "essential": { - "cefr_source": "B1" - } - }, - "it": { - "essenziale": { - "cefr_source": "B1" - } + "crucial": { "cefr_source": "B2" }, + "essential": { "cefr_source": "B1" } }, + "it": { "essenziale": { "cefr_source": "B1" } }, "de": { - "bedeutsam": { - "cefr_source": "B2" - }, - "wesentlich": { - "cefr_source": "B1" - }, - "wichtig": { - "cefr_source": "A1" - }, - "prägnant": { - "cefr_source": "B2" - }, - "aussagekräftig": { - "cefr_source": "B2" - }, - "signifikant": { - "cefr_source": "C1" - } - }, - "fr": { - "essentiel": { - "cefr_source": "B1" - } + "bedeutsam": { "cefr_source": "B2" }, + "wesentlich": { "cefr_source": "B1" }, + "wichtig": { "cefr_source": "A1" }, + "prägnant": { "cefr_source": "B2" }, + "aussagekräftig": { "cefr_source": "B2" }, + "signifikant": { "cefr_source": "C1" } }, + "fr": { "essentiel": { "cefr_source": "B1" } }, "es": { - "crucial": { - "cefr_source": "B2" - }, - "esencial": { - "cefr_source": "B1" - } + "crucial": { "cefr_source": "B2" }, + "esencial": { "cefr_source": "B1" } } }, "_sample_bucket": "pos_spread" @@ -4253,24 +2359,9 @@ { "source_id": "ili:i13690", "pos": "adjective", - "translations": { - "en": [ - "round-arm" - ] - }, - "glosses": { - "en": [ - "with the arm swung round at shoulder height" - ] - }, - "examples": { - "en": [ - { - "text": "round-arm bowling", - "source": "omw" - } - ] - }, + "translations": { "en": ["round-arm"] }, + "glosses": { "en": ["with the arm swung round at shoulder height"] }, + "examples": { "en": [{ "text": "round-arm bowling", "source": "omw" }] }, "votes": {}, "_sample_bucket": "pos_spread" }, @@ -4278,134 +2369,53 @@ "source_id": "ili:i16993", "pos": "adjective", "translations": { - "en": [ - "Monacan", - "Monegasque" - ], - "it": [ - "monegasco" - ], - "fr": [ - "monégasque" - ] + "en": ["Monacan", "Monegasque"], + "it": ["monegasco"], + "fr": ["monégasque"] }, "glosses": { - "en": [ - "of or relating to or characteristic of Monaco or its people" - ] + "en": ["of or relating to or characteristic of Monaco or its people"] }, "examples": { - "fr": [ - { - "text": "Il est de nationalité monégasque.", - "source": "cefr" - } - ] - }, - "votes": { - "fr": { - "monégasque": { - "cefr_source": "B1" - } - } + "fr": [{ "text": "Il est de nationalité monégasque.", "source": "cefr" }] }, + "votes": { "fr": { "monégasque": { "cefr_source": "B1" } } }, "_sample_bucket": "pos_spread" }, { "source_id": "ili:i18824", "pos": "adverb", "translations": { - "en": [ - "here", - "hither" - ], - "it": [ - "qua", - "qui" - ], - "fr": [ - "ici", - "çà", - "par ici" - ] - }, - "glosses": { - "en": [ - "to this place (especially toward the speaker)" - ] + "en": ["here", "hither"], + "it": ["qua", "qui"], + "fr": ["ici", "çà", "par ici"] }, + "glosses": { "en": ["to this place (especially toward the speaker)"] }, "examples": { - "en": [ - { - "text": "come here, please", - "source": "omw" - } - ], + "en": [{ "text": "come here, please", "source": "omw" }], "it": [ - { - "text": "Vieni qua, per favore.", - "source": "cefr" - }, - { - "text": "Vieni qui!", - "source": "cefr" - } + { "text": "Vieni qua, per favore.", "source": "cefr" }, + { "text": "Vieni qui!", "source": "cefr" } ], - "fr": [ - { - "text": "Venez ici !", - "source": "cefr" - } - ] + "fr": [{ "text": "Venez ici !", "source": "cefr" }] }, "votes": { "en": { - "here": { - "cefr_source": "A1" - }, - "hither": { - "cefr_source": "C2" - } + "here": { "cefr_source": "A1" }, + "hither": { "cefr_source": "C2" } }, - "it": { - "qua": { - "cefr_source": "A1" - }, - "qui": { - "cefr_source": "A1" - } - }, - "fr": { - "ici": { - "cefr_source": "A1" - } - } + "it": { "qua": { "cefr_source": "A1" }, "qui": { "cefr_source": "A1" } }, + "fr": { "ici": { "cefr_source": "A1" } } }, "_sample_bucket": "pos_spread" }, { "source_id": "ili:i19641", "pos": "adverb", - "translations": { - "en": [ - "head-on" - ], - "es": [ - "de frente" - ] - }, - "glosses": { - "en": [ - "with the front foremost" - ] - }, + "translations": { "en": ["head-on"], "es": ["de frente"] }, + "glosses": { "en": ["with the front foremost"] }, "examples": { - "en": [ - { - "text": "the cars collided head-on", - "source": "omw" - } - ] + "en": [{ "text": "the cars collided head-on", "source": "omw" }] }, "votes": {}, "_sample_bucket": "pos_spread" @@ -4413,16 +2423,8 @@ { "source_id": "ili:i21417", "pos": "adverb", - "translations": { - "en": [ - "sweepingly" - ] - }, - "glosses": { - "en": [ - "in a sweeping manner" - ] - }, + "translations": { "en": ["sweepingly"] }, + "glosses": { "en": ["in a sweeping manner"] }, "examples": { "en": [ { @@ -4438,28 +2440,14 @@ "source_id": "ili:i20131", "pos": "adverb", "translations": { - "en": [ - "gallantly", - "chivalrously" - ], - "it": [ - "galantemente" - ], - "fr": [ - "chevaleresquement" - ] - }, - "glosses": { - "en": [ - "in a gallant manner" - ] + "en": ["gallantly", "chivalrously"], + "it": ["galantemente"], + "fr": ["chevaleresquement"] }, + "glosses": { "en": ["in a gallant manner"] }, "examples": { "en": [ - { - "text": "he gallantly offered to take her home", - "source": "omw" - } + { "text": "he gallantly offered to take her home", "source": "omw" } ] }, "votes": {}, @@ -4468,16 +2456,8 @@ { "source_id": "ili:i20516", "pos": "adverb", - "translations": { - "en": [ - "fractiously" - ] - }, - "glosses": { - "en": [ - "in a fractious manner" - ] - }, + "translations": { "en": ["fractiously"] }, + "glosses": { "en": ["in a fractious manner"] }, "examples": { "en": [ { @@ -4489,4 +2469,4 @@ "votes": {}, "_sample_bucket": "pos_spread" } -] \ No newline at end of file +] diff --git a/data-pipeline/tsconfig.json b/data-pipeline/tsconfig.json index 83c3053..7752b6c 100644 --- a/data-pipeline/tsconfig.json +++ b/data-pipeline/tsconfig.json @@ -5,8 +5,8 @@ "moduleResolution": "NodeNext", "outDir": "dist", "rootDir": ".", - "types": ["node"], + "types": ["node"] }, "references": [{ "path": "../packages/shared" }], - "include": ["./**/*"], + "include": ["./**/*"] } diff --git a/documentation/data-pipeline.md b/documentation/data-pipeline.md index 56285b9..4d1bbc7 100644 --- a/documentation/data-pipeline.md +++ b/documentation/data-pipeline.md @@ -55,13 +55,13 @@ See **Setup** for download instructions. Per-language JSON files in `sources/cefr/` provide the initial CEFR level annotations. These files do not cover the full vocabulary extracted from OMW — coverage varies by language. Gaps and disagreements are handled by the enrich stage. -| Language | File | -|---|---| -| English | `sources/cefr/en.json` | -| Italian | `sources/cefr/it.json` | -| Spanish | `sources/cefr/es.json` | -| German | `sources/cefr/de.json` | -| French | `sources/cefr/fr.json` | +| Language | File | +| -------- | ---------------------- | +| English | `sources/cefr/en.json` | +| Italian | `sources/cefr/it.json` | +| Spanish | `sources/cefr/es.json` | +| German | `sources/cefr/de.json` | +| French | `sources/cefr/fr.json` | These files are committed to git. For per-language coverage detail see `COVERAGE.md`. @@ -102,13 +102,13 @@ See `LLM-SETUP.md`. The pipeline runs in five stages. Each stage is independent and can be re-run without affecting the others. -| Stage | What it does | -|---|---| -| 1. Extract | Reads OMW SQLite database, outputs normalized JSON per language | +| Stage | What it does | +| ----------- | -------------------------------------------------------------------- | +| 1. Extract | Reads OMW SQLite database, outputs normalized JSON per language | | 2. Annotate | Merges CEFR source files into extracted data, adds source file votes | -| 3. Enrich | Runs local LLMs in two rounds — generation then voting | -| 4. Merge | Resolves votes, derives difficulty, splits into final and flagged | -| 5. Compare | Generates COVERAGE.md with detailed quality report | +| 3. Enrich | Runs local LLMs in two rounds — generation then voting | +| 4. Merge | Resolves votes, derives difficulty, splits into final and flagged | +| 5. Compare | Generates COVERAGE.md with detailed quality report | ### 1. Extract @@ -137,11 +137,11 @@ Each record in the output looks like this: "fr": ["comptable"] }, "glosses": { - "en": ["(usually followed by 'to') having the necessary means or skill or know-how or authority to do something"] + "en": [ + "(usually followed by 'to') having the necessary means or skill or know-how or authority to do something" + ] }, - "examples": { - "en": ["able to swim", "she was able to program her computer"] - } + "examples": { "en": ["able to swim", "she was able to program her computer"] } } ``` @@ -158,6 +158,7 @@ Words appearing in the CEFR source file multiple times with different CEFR level **Input:** `stage-1-extract/output/omw.json` + `stage-2-annotate/sources/cefr/{lang}.json` **Output:** + - `stage-2-annotate/output/{lang}.json` — one per language - `stage-2-annotate/output/conflicts.json` — cross-language conflicts for review @@ -177,20 +178,14 @@ Each record in the output extends the OMW record with a `votes` field and any ad "es": ["capaz"], "fr": ["comptable"] }, - "glosses": { - "en": ["having the necessary means or skill to do something"] - }, + "glosses": { "en": ["having the necessary means or skill to do something"] }, "examples": { "en": [ { "text": "able to swim", "source": "omw" }, { "text": "She was able to finish the task.", "source": "cefr" } ] }, - "votes": { - "en": { - "able": { "cefr_source": "B1" } - } - } + "votes": { "en": { "able": { "cefr_source": "B1" } } } } ``` @@ -297,9 +292,7 @@ Each record in the votes file looks like this: } }, "examples": { - "en": [ - { "text": "the dog barked at the stranger", "source": "omw" } - ], + "en": [{ "text": "the dog barked at the stranger", "source": "omw" }], "fr": { "candidates": [ { "text": "le chien a aboyé", "source": "model_1" }, @@ -311,8 +304,14 @@ Each record in the votes file looks like this: "descriptions": { "en": { "candidates": [ - { "text": "a common household pet known for loyalty", "source": "model_1" }, - { "text": "a domesticated animal and loyal companion", "source": "model_2" } + { + "text": "a common household pet known for loyalty", + "source": "model_1" + }, + { + "text": "a domesticated animal and loyal companion", + "source": "model_2" + } ], "votes": { "model_1": 2, "model_2": 1 } } @@ -334,14 +333,15 @@ Reads the votes file per language and resolves the final value for every field. **Difficulty mapping:** -| CEFR | Difficulty | -|---|---| -| A1, A2 | easy | +| CEFR | Difficulty | +| ------ | ------------ | +| A1, A2 | easy | | B1, B2 | intermediate | -| C1, C2 | hard | +| C1, C2 | hard | **Input:** `stage-3-enrich/output/votes/{lang}_votes.json` **Output:** + - `stage-4-merge/output/final/{lang}.json` — fully resolved, ready for seeding - `stage-4-merge/output/flagged/{lang}.json` — CEFR majority not reached, needs manual review before seeding @@ -360,21 +360,15 @@ Each record in `final/{lang}.json` looks like this: { "text": "dog", "cefr_level": "A1", "difficulty": "easy" }, { "text": "canine", "cefr_level": "B2", "difficulty": "intermediate" } ], - "it": [ - { "text": "cane", "cefr_level": "A1", "difficulty": "easy" } - ] + "it": [{ "text": "cane", "cefr_level": "A1", "difficulty": "easy" }] }, "glosses": { "en": { "text": "a domesticated carnivorous mammal", "source": "omw" }, "fr": { "text": "un mammifère carnivore domestiqué", "source": "model_1" } }, "examples": { - "en": [ - { "text": "the dog barked at the stranger", "source": "omw" } - ], - "fr": [ - { "text": "le chien a aboyé", "source": "model_1" } - ] + "en": [{ "text": "the dog barked at the stranger", "source": "omw" }], + "fr": [{ "text": "le chien a aboyé", "source": "model_1" }] }, "descriptions": { "en": { @@ -400,6 +394,7 @@ output quality per language. Run this after merge to verify output before seeding the database. **Input:** + - `stage-4-merge/output/final/{lang}.json` - `stage-4-merge/output/flagged/{lang}.json` @@ -436,12 +431,12 @@ pnpm --filter @lila/pipeline compare These values are defined in `packages/shared/src/constants.ts` and enforced by database check constraints. The pipeline filters out any entries that violate them. -| Constant | Values | -|---|---| -| Languages | `en`, `it`, `de`, `es`, `fr` | +| Constant | Values | +| --------------- | ------------------------------------- | +| Languages | `en`, `it`, `de`, `es`, `fr` | | Parts of speech | `noun`, `verb`, `adjective`, `adverb` | -| CEFR levels | `A1`, `A2`, `B1`, `B2`, `C1`, `C2` | -| Difficulty | `easy`, `intermediate`, `hard` | +| CEFR levels | `A1`, `A2`, `B1`, `B2`, `C1`, `C2` | +| Difficulty | `easy`, `intermediate`, `hard` | Adding a new value to any of these requires a constants update and a database migration before re-running the pipeline. See **Adding a new language** for the full steps — the same process applies for new parts of speech. diff --git a/documentation/deployment.md b/documentation/deployment.md index de1d3a0..66d97e8 100644 --- a/documentation/deployment.md +++ b/documentation/deployment.md @@ -243,13 +243,13 @@ Automated build and deploy via Forgejo Actions. On every push to `main`, the pip ### Secrets (stored in Forgejo repo settings → Actions → Secrets) -| Secret | Value | -|---|---| -| REGISTRY_USER | Forgejo username | -| REGISTRY_PASSWORD | Forgejo password | -| SSH_PRIVATE_KEY | Contents of `~/.ssh/ci-runner` on the VPS | -| SSH_HOST | VPS IP address | -| SSH_USER | `lila` | +| Secret | Value | +| ----------------- | ----------------------------------------- | +| REGISTRY_USER | Forgejo username | +| REGISTRY_PASSWORD | Forgejo password | +| SSH_PRIVATE_KEY | Contents of `~/.ssh/ci-runner` on the VPS | +| SSH_HOST | VPS IP address | +| SSH_USER | `lila` | ### Runner Configuration diff --git a/documentation/llm-setup.md b/documentation/llm-setup.md index 6cc1f91..23a2ba8 100644 --- a/documentation/llm-setup.md +++ b/documentation/llm-setup.md @@ -9,12 +9,12 @@ and production scripts. ## Hardware (dev machine) -| Component | Spec | -|---|---| -| CPU | Intel Core i7-6500U (2 cores / 4 threads @ 3.10 GHz) | -| RAM | 8 GB | -| GPU | NVIDIA GeForce GTX 950M — 4 GB VRAM (Maxwell, CUDA compute 5.0) | -| OS | Debian GNU/Linux 13 (trixie) x86_64 | +| Component | Spec | +| --------- | --------------------------------------------------------------- | +| CPU | Intel Core i7-6500U (2 cores / 4 threads @ 3.10 GHz) | +| RAM | 8 GB | +| GPU | NVIDIA GeForce GTX 950M — 4 GB VRAM (Maxwell, CUDA compute 5.0) | +| OS | Debian GNU/Linux 13 (trixie) x86_64 | **Local inference verdict:** viable for small/quantized models, not for production runs. See the [Local inference](#local-inference-llamacpp) section @@ -28,12 +28,12 @@ The enrich script uses a single, swappable provider config. All providers except Anthropic expose an OpenAI-compatible API, so the same client code works across all of them — only `baseURL`, `apiKey`, and `model` change. -| Provider | Use case | Cost | Rate limits | -|---|---|---|---| -| llama.cpp (local) | Quality testing, overnight dev runs | Free (electricity) | None | -| OpenRouter (free tier) | Quality comparison, multi-model evaluation | Free | 50 req/day, 20 req/min | -| OpenRouter (paid) | Production runs if local quality insufficient | Pay-per-token | None | -| Anthropic API | Quality baseline / reference | Pay-per-token | Standard | +| Provider | Use case | Cost | Rate limits | +| ---------------------- | --------------------------------------------- | ------------------ | ---------------------- | +| llama.cpp (local) | Quality testing, overnight dev runs | Free (electricity) | None | +| OpenRouter (free tier) | Quality comparison, multi-model evaluation | Free | 50 req/day, 20 req/min | +| OpenRouter (paid) | Production runs if local quality insufficient | Pay-per-token | None | +| Anthropic API | Quality baseline / reference | Pay-per-token | Standard | --- @@ -58,12 +58,12 @@ in hybrid mode, slower than full-GPU but much faster than pure CPU. Practical estimates for this hardware (~3.5 GB VRAM usable after drivers): -| Model size | Q4 VRAM | Mode | Est. speed | -|---|---|---|---| -| 3B | ~2.0 GB | Full GPU | ~15–20 tok/s | -| 4B | ~2.5 GB | Full GPU | ~12–18 tok/s | -| 7B | ~4.5 GB | Hybrid (~26/32 layers on GPU) | ~8–12 tok/s | -| 13B+ | ~8 GB+ | CPU-heavy hybrid | too slow | +| Model size | Q4 VRAM | Mode | Est. speed | +| ---------- | ------- | ----------------------------- | ------------ | +| 3B | ~2.0 GB | Full GPU | ~15–20 tok/s | +| 4B | ~2.5 GB | Full GPU | ~12–18 tok/s | +| 7B | ~4.5 GB | Hybrid (~26/32 layers on GPU) | ~8–12 tok/s | +| 13B+ | ~8 GB+ | CPU-heavy hybrid | too slow | ### Recommended local models @@ -71,6 +71,7 @@ Two candidates worth testing, covering different points on the size/quality tradeoff: **Gemma 4 E4B Instruct (Q4 / UD-Q4_K_XL)** + - GGUF file: `gemma-4-E4B-it-UD-Q4_K_XL.gguf` (~2.5 GB) - Source: https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF - Runs fully on GPU. Brand new (April 2025), built for edge hardware, 140+ @@ -78,6 +79,7 @@ tradeoff: to test. **Qwen2.5 7B Instruct (Q4_K_M)** + - GGUF file: `Qwen2.5-7B-Instruct-Q4_K_M.gguf` (~4.5 GB) - Source: https://huggingface.co/Qwen/Qwen2.5-7B-Instruct-GGUF - Runs in hybrid mode (~26 of 32 layers on GPU, rest on CPU), ~8–12 tok/s. @@ -107,6 +109,7 @@ wget -O models/qwen2.5-3b-instruct-q4_k_m.gguf \ ### Starting the server **Gemma 4 E4B** (full GPU): + ```bash ./build/bin/llama-server \ --model models/gemma-4-e4b-it-ud-q4_k_xl.gguf \ @@ -117,6 +120,7 @@ wget -O models/qwen2.5-3b-instruct-q4_k_m.gguf \ ``` **Qwen2.5 7B** (hybrid — tune `--n-gpu-layers` to fit your VRAM): + ```bash ./build/bin/llama-server \ --model models/qwen2.5-7b-instruct-q4_k_m.gguf \ @@ -163,15 +167,16 @@ object changes. Ranked by expected multilingual generation quality for en/it/de/fr/es: -| Model ID | Params | Notes | -|---|---|---| -| `qwen/qwen3-coder:free` | 480B MoE (35B active) | Best free option. Strong multilingual despite "coder" label. Use as quality ceiling. | -| `qwen/qwen3-next-80b-a3b-instruct:free` | 80B MoE (3B active) | Smaller Qwen, useful comparison point. | -| `nvidia/nemotron-3-super-120b-a12b:free` | 120B MoE (12B active) | 262K context, supports structured output. | -| `google/gemma-4-31b-it:free` | 31B | 140+ language support, good European language coverage. | -| `zhipuai/glm-4.5-air:free` | MoE | Multilingual-focused. | +| Model ID | Params | Notes | +| ---------------------------------------- | --------------------- | ------------------------------------------------------------------------------------ | +| `qwen/qwen3-coder:free` | 480B MoE (35B active) | Best free option. Strong multilingual despite "coder" label. Use as quality ceiling. | +| `qwen/qwen3-next-80b-a3b-instruct:free` | 80B MoE (3B active) | Smaller Qwen, useful comparison point. | +| `nvidia/nemotron-3-super-120b-a12b:free` | 120B MoE (12B active) | 262K context, supports structured output. | +| `google/gemma-4-31b-it:free` | 31B | 140+ language support, good European language coverage. | +| `zhipuai/glm-4.5-air:free` | MoE | Multilingual-focused. | **Skip for this pipeline:** + - Llama models — weaker European language generation than Qwen/Gemma - Mistral free tier — requests may be used for model training @@ -194,7 +199,7 @@ change this object and re-run. // config.ts export type ProviderConfig = { - name: string; // used for output folder naming + name: string; // used for output folder naming baseURL: string; apiKey: string; model: string; @@ -205,8 +210,8 @@ export type ProviderConfig = { export const LOCAL_QWEN3B: ProviderConfig = { name: "local-qwen2.5-3b", baseURL: "http://127.0.0.1:8080/v1", - apiKey: "none", // llama.cpp ignores this - model: "qwen2.5-3b", // llama.cpp ignores model name, uses loaded model + apiKey: "none", // llama.cpp ignores this + model: "qwen2.5-3b", // llama.cpp ignores model name, uses loaded model maxTokens: 512, }; @@ -231,7 +236,7 @@ export const OR_GEMMA4_31B: ProviderConfig = { // Anthropic (reference baseline — different adapter required) export const ANTHROPIC_SONNET: ProviderConfig = { name: "anthropic-sonnet", - baseURL: "https://api.anthropic.com/v1", // adapter handles format difference + baseURL: "https://api.anthropic.com/v1", // adapter handles format difference apiKey: process.env.ANTHROPIC_API_KEY!, model: "claude-sonnet-4-6", maxTokens: 512, @@ -239,6 +244,7 @@ export const ANTHROPIC_SONNET: ProviderConfig = { ``` Output from each run lands in: + ``` stage-3-enrich/test/output/{provider.name}/results.json stage-3-enrich/test/output/{provider.name}/metrics.json @@ -252,21 +258,21 @@ The evaluate script compares all `metrics.json` files side by side. The test script measures the following per provider run: -| Metric | What it measures | -|---|---| -| **JSON parse rate** | % of responses that are valid, schema-compliant JSON. Critical — a failed parse is a wasted call. Target: >97% | -| **Field coverage** | % of records where all required fields are present (cefr votes for all translations, descriptions for all languages, glosses/examples for fr/es) | -| **CEFR agreement** | For records that have a `cefr_source` vote, % where the model agrees. Measures calibration. | -| **Language correctness** | Manual spot-check only — automated detection not reliable enough | -| **Tokens/second** | Local only. Indicates overnight run feasibility | +| Metric | What it measures | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| **JSON parse rate** | % of responses that are valid, schema-compliant JSON. Critical — a failed parse is a wasted call. Target: >97% | +| **Field coverage** | % of records where all required fields are present (cefr votes for all translations, descriptions for all languages, glosses/examples for fr/es) | +| **CEFR agreement** | For records that have a `cefr_source` vote, % where the model agrees. Measures calibration. | +| **Language correctness** | Manual spot-check only — automated detection not reliable enough | +| **Tokens/second** | Local only. Indicates overnight run feasibility | ### Decision thresholds -| Metric | Threshold | Action if below | -|---|---|---| -| JSON parse rate | < 97% | Do not use this model for production | -| Field coverage | < 95% | Prompt needs revision before production | -| CEFR agreement | < 70% | Model lacks vocabulary knowledge for this task | +| Metric | Threshold | Action if below | +| --------------- | --------- | ---------------------------------------------- | +| JSON parse rate | < 97% | Do not use this model for production | +| Field coverage | < 95% | Prompt needs revision before production | +| CEFR agreement | < 70% | Model lacks vocabulary knowledge for this task | --- diff --git a/documentation/notes.md b/documentation/notes.md index 8a8d414..4391d87 100644 --- a/documentation/notes.md +++ b/documentation/notes.md @@ -1,6 +1,5 @@ # notes - ## prompt ive attached the readme of my project. this is my current task: @@ -46,7 +45,7 @@ laptop: verify if docker containers run on startup (they shouldnt) ### vps setup - monitoring and logging (eg via chrootkit or rkhunter, logwatch/monit => mails daily with summary) -<<<<<<< HEAD + <<<<<<< HEAD - ~~keep the vps clean (e.g. old docker images/containers)~~ ✅ CI/CD pipeline runs `docker image prune -f` after deploy ### ~~cd/ci pipeline~~ ✅ RESOLVED @@ -55,9 +54,9 @@ Forgejo Actions with runner on VPS, Forgejo built-in container registry. See `de ### ~~postgres backups~~ ✅ RESOLVED -Daily pg_dump cron job, 7-day retention, dev laptop auto-sync via rsync. See `deployment.md`. -======= ->>>>>>> dev +# Daily pg_dump cron job, 7-day retention, dev laptop auto-sync via rsync. See `deployment.md`. + +> > > > > > > dev ### try now option diff --git a/documentation/roasts/gameService.md b/documentation/roasts/gameService.md index de8f968..0db3559 100644 --- a/documentation/roasts/gameService.md +++ b/documentation/roasts/gameService.md @@ -61,10 +61,12 @@ export const evaluateAnswer = async ( store: GameSessionStore, ): Promise<AnswerResult> => { const session = await store.get(submission.sessionId); - if (!session) throw new NotFoundError(`Game session not found: ${submission.sessionId}`); + if (!session) + throw new NotFoundError(`Game session not found: ${submission.sessionId}`); const correctOptionId = session.answers.get(submission.questionId); - if (correctOptionId === undefined) throw new NotFoundError(`Question not found: ${submission.questionId}`); + if (correctOptionId === undefined) + throw new NotFoundError(`Question not found: ${submission.questionId}`); // delete answered question; delete session when all questions are answered session.answers.delete(submission.questionId); @@ -84,10 +86,14 @@ export const evaluateAnswer = async ( ```ts // ✅ option B — TTL in InMemoryGameSessionStore export class InMemoryGameSessionStore implements GameSessionStore { - private sessions = new Map<string, { data: GameSessionData; expiresAt: number }>(); + private sessions = new Map< + string, + { data: GameSessionData; expiresAt: number } + >(); private readonly ttlMs: number; - constructor(ttlMs = 30 * 60 * 1000) { // 30 minutes default + constructor(ttlMs = 30 * 60 * 1000) { + // 30 minutes default this.ttlMs = ttlMs; } @@ -115,15 +121,13 @@ export class InMemoryGameSessionStore implements GameSessionStore { --- - - **Problem** `GameRequest.rounds` is typed as `string` in `@lila/shared`, forcing the service to cast it every time: ```ts // ❌ why is a round count a string? -Number(request.rounds) +Number(request.rounds); ``` **Fix — fix the schema in `@lila/shared`** @@ -204,7 +208,10 @@ it("correct answer appears exactly once in options even if distractor matches", // simulate getDistractors returning the correct answer as one of the distractors mockGetDistractors.mockResolvedValueOnce(["cane", "wrong2", "wrong3"]); - const session = await createGameSession(validRequest, new InMemoryGameSessionStore()); + const session = await createGameSession( + validRequest, + new InMemoryGameSessionStore(), + ); const question = session.questions[0]!; const optionTexts = question.options.map((o) => o.text); @@ -285,16 +292,14 @@ his `sessionId`. ```ts // GameSessionStore.ts -export type GameSessionData = { - answers: Map<string, number>; - userId: string; -}; +export type GameSessionData = { answers: Map<string, number>; userId: string }; // evaluateAnswer const session = await store.get(submission.sessionId); if (!session) throw new NotFoundError(`Game session not found`); -if (session.userId !== requestingUserId) throw new NotFoundError(`Game session not found`); +if (session.userId !== requestingUserId) + throw new NotFoundError(`Game session not found`); // ^^^ same error — don't confirm the session exists to the wrong user ``` @@ -326,8 +331,9 @@ if (terms.length === 0) { it("throws when getGameTerms returns no terms", async () => { mockGetGameTerms.mockResolvedValue([]); - await expect(createGameSession(validRequest, new InMemoryGameSessionStore())) - .rejects.toThrow("No terms found"); + await expect( + createGameSession(validRequest, new InMemoryGameSessionStore()), + ).rejects.toThrow("No terms found"); }); ``` @@ -349,8 +355,9 @@ it("throws when getGameTerms returns no terms", async () => { it("propagates getDistractors failure", async () => { mockGetDistractors.mockRejectedValue(new Error("db timeout")); - await expect(createGameSession(validRequest, new InMemoryGameSessionStore())) - .rejects.toThrow("db timeout"); + await expect( + createGameSession(validRequest, new InMemoryGameSessionStore()), + ).rejects.toThrow("db timeout"); }); ``` diff --git a/documentation/spec.md b/documentation/spec.md index b16fbc0..29938da 100644 --- a/documentation/spec.md +++ b/documentation/spec.md @@ -51,9 +51,9 @@ This is the full vision. The current implementation already covers most of it; r ### What is CUT from the MVP -| Feature | Why cut | -| ------------------------------- | -------------------------------------- | -| User stats / profiles | Needs auth | +| Feature | Why cut | +| --------------------- | ---------- | +| User stats / profiles | Needs auth | These are not deleted from the plan — they are deferred. The architecture is already designed to support them. See Section 11 (Post-MVP Ladder). @@ -63,22 +63,22 @@ These are not deleted from the plan — they are deferred. The architecture is a The monorepo structure and tooling are already set up. This is the full stack. -| Layer | Technology | Status | -| ------------ | ------------------------------ | ----------- | -| Monorepo | pnpm workspaces | ✅ | -| Frontend | React 18, Vite, TypeScript | ✅ | -| Routing | TanStack Router | ✅ | -| Server state | TanStack Query | ✅ | -| Client state | Zustand | ✅ | -| Styling | Tailwind CSS + shadcn/ui | ✅ | -| Backend | Node.js, Express, TypeScript | ✅ | -| Database | PostgreSQL + Drizzle ORM | ✅ | -| Validation | Zod (shared schemas) | ✅ | -| Testing | Vitest, supertest | ✅ | -| Auth | Better Auth (Google + GitHub) | ✅ | -| Deployment | Docker Compose, Caddy, Hetzner | ✅ | -| CI/CD | Forgejo Actions | ✅ | -| Realtime | WebSockets (`ws` library) | ✅ | +| Layer | Technology | Status | +| ------------ | ------------------------------ | ------------------------------------------------------ | +| Monorepo | pnpm workspaces | ✅ | +| Frontend | React 18, Vite, TypeScript | ✅ | +| Routing | TanStack Router | ✅ | +| Server state | TanStack Query | ✅ | +| Client state | Zustand | ✅ | +| Styling | Tailwind CSS + shadcn/ui | ✅ | +| Backend | Node.js, Express, TypeScript | ✅ | +| Database | PostgreSQL + Drizzle ORM | ✅ | +| Validation | Zod (shared schemas) | ✅ | +| Testing | Vitest, supertest | ✅ | +| Auth | Better Auth (Google + GitHub) | ✅ | +| Deployment | Docker Compose, Caddy, Hetzner | ✅ | +| CI/CD | Forgejo Actions | ✅ | +| Realtime | WebSockets (`ws` library) | ✅ | | Cache | Valkey | ⚠️ optional (used locally; production/state hardening) | --- @@ -288,26 +288,27 @@ After completing a task: share the code, ask what to refactor and why. The LLM s ## 11. Post-MVP Ladder <<<<<<< HEAD -| Phase | What it adds | Status | +| Phase | What it adds | Status | | ----------------- | ------------------------------------------------------------------------------- | ------ | -| Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ | -| Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ | -| Hardening (partial) | CI/CD pipeline, DB backups | ✅ | -| User Stats | Games played, score history, profile page | ❌ | -| Multiplayer Lobby | Room creation, join by code, WebSocket connection | ❌ | -| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ❌ | -| Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ | +| Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ | +| Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ | +| Hardening (partial) | CI/CD pipeline, DB backups | ✅ | +| User Stats | Games played, score history, profile page | ❌ | +| Multiplayer Lobby | Room creation, join by code, WebSocket connection | ❌ | +| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ❌ | +| Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ | ======= -| Phase | What it adds | Status | +| Phase | What it adds | Status | | ------------------- | ----------------------------------------------------------------------- | ------ | -| Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ | -| Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ | -| Hardening (partial) | CI/CD pipeline, DB backups | ✅ | -| User Stats | Games played, score history, profile page | ❌ | -| Multiplayer Lobby | Room creation, join by code, WebSocket connection | ✅ | -| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ✅ | -| Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ | ->>>>>>> dev +| Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ | +| Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ | +| Hardening (partial) | CI/CD pipeline, DB backups | ✅ | +| User Stats | Games played, score history, profile page | ❌ | +| Multiplayer Lobby | Room creation, join by code, WebSocket connection | ✅ | +| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ✅ | +| Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ | + +> > > > > > > dev ### Future Data Model Extensions (deferred, additive) diff --git a/documentation/tickets/blueprint.md b/documentation/tickets/blueprint.md index 8a3c065..c5fdf8a 100644 --- a/documentation/tickets/blueprint.md +++ b/documentation/tickets/blueprint.md @@ -1,6 +1,6 @@ # Ticket Blueprint -Two formats depending on task type. Choose based on whether a meaningful +Two formats depending on task type. Choose based on whether a meaningful decision between options was made. --- diff --git a/documentation/tickets/t00001.md b/documentation/tickets/t00001.md index 4fffaec..f7f1a09 100644 --- a/documentation/tickets/t00001.md +++ b/documentation/tickets/t00001.md @@ -87,9 +87,7 @@ pass init <your-key-id> Replace the entire file contents with: ```json -{ - "credsStore": "pass" -} +{ "credsStore": "pass" } ``` ### 6. Re-login to registries diff --git a/documentation/tickets/t00002.md b/documentation/tickets/t00002.md index dc93605..4f7fbb5 100644 --- a/documentation/tickets/t00002.md +++ b/documentation/tickets/t00002.md @@ -136,7 +136,7 @@ Rejected because: coercion is for untrusted or uncontrolled inputs (form fields, 6. In `apps/web/src/components/game/GameSetup.tsx`: - Update `SettingGroup` props to accept `string | number`: - + ```ts type SettingGroupProps = { options: readonly (string | number)[]; diff --git a/packages/db/drizzle/meta/0007_snapshot.json b/packages/db/drizzle/meta/0007_snapshot.json index e5ee0b5..051e87a 100644 --- a/packages/db/drizzle/meta/0007_snapshot.json +++ b/packages/db/drizzle/meta/0007_snapshot.json @@ -110,12 +110,8 @@ "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" } @@ -149,12 +145,8 @@ "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" }, @@ -162,12 +154,8 @@ "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" } @@ -175,10 +163,7 @@ "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": {}, @@ -265,10 +250,7 @@ "unique_deck_name": { "name": "unique_deck_name", "nullsNotDistinct": false, - "columns": [ - "name", - "source_language" - ] + "columns": ["name", "source_language"] } }, "policies": {}, @@ -336,12 +318,8 @@ "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" } @@ -351,9 +329,7 @@ "lobbies_code_unique": { "name": "lobbies_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -402,12 +378,8 @@ "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" }, @@ -415,12 +387,8 @@ "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" } @@ -428,10 +396,7 @@ "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": {}, @@ -515,12 +480,8 @@ "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" } @@ -530,9 +491,7 @@ "session_token_unique": { "name": "session_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -588,12 +547,8 @@ "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" } @@ -603,10 +558,7 @@ "unique_term_gloss": { "name": "unique_term_gloss", "nullsNotDistinct": false, - "columns": [ - "term_id", - "language_code" - ] + "columns": ["term_id", "language_code"] } }, "policies": {}, @@ -641,12 +593,8 @@ "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" }, @@ -654,12 +602,8 @@ "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" } @@ -667,10 +611,7 @@ "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": {}, @@ -744,10 +685,7 @@ "unique_source_id": { "name": "unique_source_id", "nullsNotDistinct": false, - "columns": [ - "source", - "source_id" - ] + "columns": ["source", "source_id"] } }, "policies": {}, @@ -803,9 +741,7 @@ "topics_slug_unique": { "name": "topics_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -901,12 +837,8 @@ "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" } @@ -916,11 +848,7 @@ "unique_translations": { "name": "unique_translations", "nullsNotDistinct": false, - "columns": [ - "term_id", - "language_code", - "text" - ] + "columns": ["term_id", "language_code", "text"] } }, "policies": {}, @@ -997,9 +925,7 @@ "user_email_unique": { "name": "user_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -1080,9 +1006,5 @@ "roles": {}, "policies": {}, "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file + "_meta": { "columns": {}, "schemas": {}, "tables": {} } +} diff --git a/packages/db/drizzle/meta/0008_snapshot.json b/packages/db/drizzle/meta/0008_snapshot.json index aab8d46..ebeb2b1 100644 --- a/packages/db/drizzle/meta/0008_snapshot.json +++ b/packages/db/drizzle/meta/0008_snapshot.json @@ -110,12 +110,8 @@ "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" } @@ -149,12 +145,8 @@ "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" }, @@ -162,12 +154,8 @@ "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" } @@ -175,10 +163,7 @@ "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": {}, @@ -265,10 +250,7 @@ "unique_deck_name": { "name": "unique_deck_name", "nullsNotDistinct": false, - "columns": [ - "name", - "source_language" - ] + "columns": ["name", "source_language"] } }, "policies": {}, @@ -336,12 +318,8 @@ "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" } @@ -351,9 +329,7 @@ "lobbies_code_unique": { "name": "lobbies_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -402,12 +378,8 @@ "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" }, @@ -415,12 +387,8 @@ "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" } @@ -428,10 +396,7 @@ "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": {}, @@ -515,12 +480,8 @@ "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" } @@ -530,9 +491,7 @@ "session_token_unique": { "name": "session_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -604,12 +563,8 @@ "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" } @@ -619,11 +574,7 @@ "unique_term_example": { "name": "unique_term_example", "nullsNotDistinct": false, - "columns": [ - "term_id", - "language_code", - "text" - ] + "columns": ["term_id", "language_code", "text"] } }, "policies": {}, @@ -684,12 +635,8 @@ "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" } @@ -699,10 +646,7 @@ "unique_term_gloss": { "name": "unique_term_gloss", "nullsNotDistinct": false, - "columns": [ - "term_id", - "language_code" - ] + "columns": ["term_id", "language_code"] } }, "policies": {}, @@ -737,12 +681,8 @@ "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" }, @@ -750,12 +690,8 @@ "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" } @@ -763,10 +699,7 @@ "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": {}, @@ -840,10 +773,7 @@ "unique_source_id": { "name": "unique_source_id", "nullsNotDistinct": false, - "columns": [ - "source", - "source_id" - ] + "columns": ["source", "source_id"] } }, "policies": {}, @@ -899,9 +829,7 @@ "topics_slug_unique": { "name": "topics_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -997,12 +925,8 @@ "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" } @@ -1012,11 +936,7 @@ "unique_translations": { "name": "unique_translations", "nullsNotDistinct": false, - "columns": [ - "term_id", - "language_code", - "text" - ] + "columns": ["term_id", "language_code", "text"] } }, "policies": {}, @@ -1093,9 +1013,7 @@ "user_email_unique": { "name": "user_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -1176,9 +1094,5 @@ "roles": {}, "policies": {}, "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file + "_meta": { "columns": {}, "schemas": {}, "tables": {} } +} diff --git a/packages/db/drizzle/meta/0009_snapshot.json b/packages/db/drizzle/meta/0009_snapshot.json index 8274112..6082664 100644 --- a/packages/db/drizzle/meta/0009_snapshot.json +++ b/packages/db/drizzle/meta/0009_snapshot.json @@ -110,12 +110,8 @@ "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" } @@ -149,12 +145,8 @@ "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" }, @@ -162,12 +154,8 @@ "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" } @@ -175,10 +163,7 @@ "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": {}, @@ -265,10 +250,7 @@ "unique_deck_name": { "name": "unique_deck_name", "nullsNotDistinct": false, - "columns": [ - "name", - "source_language" - ] + "columns": ["name", "source_language"] } }, "policies": {}, @@ -355,12 +337,8 @@ "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" } @@ -370,9 +348,7 @@ "lobbies_code_unique": { "name": "lobbies_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -421,12 +397,8 @@ "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" }, @@ -434,12 +406,8 @@ "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" } @@ -447,10 +415,7 @@ "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": {}, @@ -534,12 +499,8 @@ "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" } @@ -549,9 +510,7 @@ "session_token_unique": { "name": "session_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -623,12 +582,8 @@ "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" } @@ -638,11 +593,7 @@ "unique_term_example": { "name": "unique_term_example", "nullsNotDistinct": false, - "columns": [ - "term_id", - "language_code", - "text" - ] + "columns": ["term_id", "language_code", "text"] } }, "policies": {}, @@ -703,12 +654,8 @@ "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" } @@ -718,10 +665,7 @@ "unique_term_gloss": { "name": "unique_term_gloss", "nullsNotDistinct": false, - "columns": [ - "term_id", - "language_code" - ] + "columns": ["term_id", "language_code"] } }, "policies": {}, @@ -756,12 +700,8 @@ "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" }, @@ -769,12 +709,8 @@ "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" } @@ -782,10 +718,7 @@ "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": {}, @@ -859,10 +792,7 @@ "unique_source_id": { "name": "unique_source_id", "nullsNotDistinct": false, - "columns": [ - "source", - "source_id" - ] + "columns": ["source", "source_id"] } }, "policies": {}, @@ -918,9 +848,7 @@ "topics_slug_unique": { "name": "topics_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -1016,12 +944,8 @@ "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" } @@ -1031,11 +955,7 @@ "unique_translations": { "name": "unique_translations", "nullsNotDistinct": false, - "columns": [ - "term_id", - "language_code", - "text" - ] + "columns": ["term_id", "language_code", "text"] } }, "policies": {}, @@ -1112,9 +1032,7 @@ "user_email_unique": { "name": "user_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -1195,9 +1113,5 @@ "roles": {}, "policies": {}, "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file + "_meta": { "columns": {}, "schemas": {}, "tables": {} } +} diff --git a/packages/db/drizzle/meta/0010_snapshot.json b/packages/db/drizzle/meta/0010_snapshot.json index 720a585..0f0603f 100644 --- a/packages/db/drizzle/meta/0010_snapshot.json +++ b/packages/db/drizzle/meta/0010_snapshot.json @@ -110,12 +110,8 @@ "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" } @@ -149,12 +145,8 @@ "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" }, @@ -162,12 +154,8 @@ "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" } @@ -175,10 +163,7 @@ "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": {}, @@ -265,10 +250,7 @@ "unique_deck_name": { "name": "unique_deck_name", "nullsNotDistinct": false, - "columns": [ - "name", - "source_language" - ] + "columns": ["name", "source_language"] } }, "policies": {}, @@ -336,12 +318,8 @@ "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" } @@ -351,9 +329,7 @@ "lobbies_code_unique": { "name": "lobbies_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -402,12 +378,8 @@ "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" }, @@ -415,12 +387,8 @@ "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" } @@ -428,10 +396,7 @@ "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": {}, @@ -515,12 +480,8 @@ "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" } @@ -530,9 +491,7 @@ "session_token_unique": { "name": "session_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -604,12 +563,8 @@ "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" } @@ -619,11 +574,7 @@ "unique_term_example": { "name": "unique_term_example", "nullsNotDistinct": false, - "columns": [ - "term_id", - "language_code", - "text" - ] + "columns": ["term_id", "language_code", "text"] } }, "policies": {}, @@ -684,12 +635,8 @@ "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" } @@ -699,10 +646,7 @@ "unique_term_gloss": { "name": "unique_term_gloss", "nullsNotDistinct": false, - "columns": [ - "term_id", - "language_code" - ] + "columns": ["term_id", "language_code"] } }, "policies": {}, @@ -737,12 +681,8 @@ "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" }, @@ -750,12 +690,8 @@ "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" } @@ -763,10 +699,7 @@ "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": {}, @@ -840,10 +773,7 @@ "unique_source_id": { "name": "unique_source_id", "nullsNotDistinct": false, - "columns": [ - "source", - "source_id" - ] + "columns": ["source", "source_id"] } }, "policies": {}, @@ -899,9 +829,7 @@ "topics_slug_unique": { "name": "topics_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -997,12 +925,8 @@ "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" } @@ -1012,11 +936,7 @@ "unique_translations": { "name": "unique_translations", "nullsNotDistinct": false, - "columns": [ - "term_id", - "language_code", - "text" - ] + "columns": ["term_id", "language_code", "text"] } }, "policies": {}, @@ -1093,9 +1013,7 @@ "user_email_unique": { "name": "user_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -1176,9 +1094,5 @@ "roles": {}, "policies": {}, "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file + "_meta": { "columns": {}, "schemas": {}, "tables": {} } +} diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 512887d..65dc2f0 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -80,4 +80,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json index 1c40fbd..af1fba6 100644 --- a/packages/db/tsconfig.json +++ b/packages/db/tsconfig.json @@ -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" + ] } diff --git a/tsconfig.json b/tsconfig.json index 8b0de56..9e79e86 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ { "path": "./packages/db" }, { "path": "./apps/web" }, { "path": "./apps/api" }, - { "path": "./data-pipeline" }, + { "path": "./data-pipeline" } ], - "files": [], + "files": [] }