diff --git a/README.md b/README.md index e212a55..32af038 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/api/src/lib/utils.ts b/apps/api/src/lib/utils.ts deleted file mode 100644 index 4912c8c..0000000 --- a/apps/api/src/lib/utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const shuffleArray = (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 64f90f6..d0f0781 100644 --- a/apps/api/src/services/gameService.ts +++ b/apps/api/src/services/gameService.ts @@ -10,14 +10,13 @@ import type { } from "@lila/shared"; import { InMemoryGameSessionStore } from "../gameSessionStore/index.js"; import { NotFoundError } from "../errors/AppError.js"; -import { shuffleArray } from "../lib/utils.js"; const gameSessionStore = new InMemoryGameSessionStore(); export const createGameSession = async ( request: GameRequest, ): Promise => { - const terms = await getGameTerms( + const correctAnswers = await getGameTerms( request.source_language, request.target_language, request.pos, @@ -28,19 +27,19 @@ export const createGameSession = async ( const answerKey = new Map(); const questions: GameQuestion[] = await Promise.all( - terms.map(async (term) => { + correctAnswers.map(async (correctAnswer) => { const distractorTexts = await getDistractors( - term.termId, - term.targetText, + correctAnswer.termId, + correctAnswer.targetText, request.target_language, request.pos, request.difficulty, 3, ); - const optionTexts = [term.targetText, ...distractorTexts]; - const shuffledTexts = shuffleArray(optionTexts); - const correctOptionId = shuffledTexts.indexOf(term.targetText); + const optionTexts = [correctAnswer.targetText, ...distractorTexts]; + const shuffledTexts = shuffle(optionTexts); + const correctOptionId = shuffledTexts.indexOf(correctAnswer.targetText); const options: AnswerOption[] = shuffledTexts.map((text, index) => ({ optionId: index, @@ -52,8 +51,8 @@ export const createGameSession = async ( return { questionId, - prompt: term.sourceText, - gloss: term.sourceGloss, + prompt: correctAnswer.sourceText, + gloss: correctAnswer.sourceGloss, options, }; }), @@ -65,6 +64,17 @@ export const createGameSession = async ( return { sessionId, questions }; }; +const shuffle = (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 => { diff --git a/apps/web/src/components/game/QuestionCard.tsx b/apps/web/src/components/game/QuestionCard.tsx index 2d4c633..7878a5b 100644 --- a/apps/web/src/components/game/QuestionCard.tsx +++ b/apps/web/src/components/game/QuestionCard.tsx @@ -53,11 +53,7 @@ export const QuestionCard = ({ Round {questionNumber}/{totalQuestions}
- {currentResult - ? "Checked" - : selectedOptionId !== null - ? "Ready" - : "Pick one"} + {currentResult ? "Checked" : selectedOptionId !== null ? "Ready" : "Pick one"}
@@ -77,14 +73,14 @@ export const QuestionCard = ({
- {question.options.map((option) => ( - handleSelect(option.optionId)} - /> - ))} + {question.options.map((option) => ( + handleSelect(option.optionId)} + /> + ))}
diff --git a/apps/web/src/components/multiplayer/MultiplayerScoreScreen.tsx b/apps/web/src/components/multiplayer/MultiplayerScoreScreen.tsx index 8b82f1a..f530db8 100644 --- a/apps/web/src/components/multiplayer/MultiplayerScoreScreen.tsx +++ b/apps/web/src/components/multiplayer/MultiplayerScoreScreen.tsx @@ -69,9 +69,7 @@ export const MultiplayerScoreScreen = ({ {player.user.name} diff --git a/apps/web/src/components/ui/ConfettiBurst.tsx b/apps/web/src/components/ui/ConfettiBurst.tsx index ee1fcfd..66285d4 100644 --- a/apps/web/src/components/ui/ConfettiBurst.tsx +++ b/apps/web/src/components/ui/ConfettiBurst.tsx @@ -6,7 +6,10 @@ type ConfettiBurstProps = { count?: number; }; -type Piece = { id: number; style: React.CSSProperties & ConfettiVars }; +type Piece = { + id: number; + style: React.CSSProperties & ConfettiVars; +}; type ConfettiVars = { ["--x0"]: string; @@ -53,9 +56,7 @@ export const ConfettiBurst = ({ }, []); const pieces = useMemo(() => { - const seed = hashStringToUint32( - `${instanceId}:${count}:${colors.join(",")}`, - ); + const seed = hashStringToUint32(`${instanceId}:${count}:${colors.join(",")}`); const rand = mulberry32(seed); const rnd = (min: number, max: number) => min + rand() * (max - min); @@ -99,3 +100,4 @@ export const ConfettiBurst = ({ ); }; + diff --git a/apps/web/src/routes/multiplayer/index.tsx b/apps/web/src/routes/multiplayer/index.tsx index 2780eab..ee757f4 100644 --- a/apps/web/src/routes/multiplayer/index.tsx +++ b/apps/web/src/routes/multiplayer/index.tsx @@ -108,9 +108,7 @@ function MultiplayerPage() { {/* Join lobby */}
-

- Join a lobby -

+

Join a lobby

Enter the code shared by your host.

diff --git a/data-pipeline/test/output/sample.json b/data-pipeline/test/output/sample.json index 3177e22..5dd774f 100644 --- a/data-pipeline/test/output/sample.json +++ b/data-pipeline/test/output/sample.json @@ -3,8 +3,12 @@ "source_id": "ili:i90862", "pos": "noun", "translations": { - "en": ["kinsman"], - "es": ["pariente"], + "en": [ + "kinsman" + ], + "es": [ + "pariente" + ], "de": [ "Gevatter", "Anverwandter", @@ -14,11 +18,17 @@ "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": [ @@ -34,10 +44,16 @@ "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": [ { @@ -47,15 +63,35 @@ ] }, "votes": { - "en": { "kinsman": { "cefr_source": "C1" } }, - "de": { - "Familienmitglied": { "cefr_source": "A2" }, - "Verwandter": { "cefr_source": "B1" }, - "Familienangehöriger": { "cefr_source": "B1" }, - "Angehöriger": { "cefr_source": "B2" } + "en": { + "kinsman": { + "cefr_source": "C1" + } }, - "fr": { "parent": { "cefr_source": "A1" } }, - "es": { "pariente": { "cefr_source": "A2" } } + "de": { + "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" + } + } }, "_sample_bucket": "has_cefr_vote" }, @@ -63,41 +99,96 @@ "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"] }, - "examples": { "en": [ - { "text": "The child is taught to obey her parents", "source": "omw" } + "teach" ], "it": [ - { "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" } + "addestrare", + "ammaestrare", + "insegnare" ], "es": [ - { "text": "Ella enseña español en la universidad.", "source": "cefr" } + "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" + } + ], + "it": [ + { + "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" + } + ], + "es": [ + { + "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" }, @@ -105,19 +196,39 @@ "source_id": "ili:i26718", "pos": "verb", "translations": { - "en": ["dub", "nickname"], - "it": ["battezzare", "cognominare", "doppiare", "soprannominare"], - "es": ["apodar"], - "fr": ["surnom", "baptiser"] + "en": [ + "dub", + "nickname" + ], + "it": [ + "battezzare", + "cognominare", + "doppiare", + "soprannominare" + ], + "es": [ + "apodar" + ], + "fr": [ + "surnom", + "baptiser" + ] + }, + "glosses": { + "en": [ + "give a nickname to" + ] }, - "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": [ { @@ -127,12 +238,24 @@ ] }, "votes": { - "en": { "dub": { "cefr_source": "B2" } }, - "it": { - "battezzare": { "cefr_source": "B1" }, - "soprannominare": { "cefr_source": "B2" } + "en": { + "dub": { + "cefr_source": "B2" + } }, - "fr": { "baptiser": { "cefr_source": "B1" } } + "it": { + "battezzare": { + "cefr_source": "B1" + }, + "soprannominare": { + "cefr_source": "B2" + } + }, + "fr": { + "baptiser": { + "cefr_source": "B1" + } + } }, "_sample_bucket": "has_cefr_vote" }, @@ -140,46 +263,92 @@ "source_id": "ili:i4448", "pos": "adjective", "translations": { - "en": ["drab", "dreary"], - "es": ["igual", "rutinario"], - "fr": ["morne", "maussade", "sombre"] + "en": [ + "drab", + "dreary" + ], + "es": [ + "igual", + "rutinario" + ], + "fr": [ + "morne", + "maussade", + "sombre" + ] + }, + "glosses": { + "en": [ + "lacking in liveliness or charm or surprise" + ] }, - "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" @@ -188,30 +357,75 @@ "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"] }, - "examples": { - "fr": [{ "text": "Elle souffre de dépression.", "source": "cefr" }], + "en": [ + "natural depression", + "depression" + ], + "it": [ + "avvallamento" + ], "es": [ - { "text": "La depresión es una enfermedad grave.", "source": "cefr" } + "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" + } + ], + "es": [ + { + "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": [ { @@ -227,8 +441,16 @@ ] }, "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" }, @@ -243,8 +465,15 @@ "butt against", "knock against" ], - "it": ["urtare"], - "es": ["chocar", "colisionar", "golpearse contra", "topar"], + "it": [ + "urtare" + ], + "es": [ + "chocar", + "colisionar", + "golpearse contra", + "topar" + ], "de": [ "anraunzen", "anfahren", @@ -259,32 +488,68 @@ ] }, "glosses": { - "en": ["collide violently with an obstacle"], - "de": ["heftig mit einem Hindernis zusammenstoßen"] - }, - "examples": { - "en": [{ "text": "I ran into the telephone pole", "source": "omw" }], - "it": [ - { "text": "Ho urtato il tavolo con il gomito.", "source": "cefr" } + "en": [ + "collide violently with an obstacle" ], "de": [ - { "text": "Der Bus fuhr an die Haltestelle an.", "source": "cefr" }, - { "text": "Er hat mich ohne Grund angeschrien.", "source": "cefr" } + "heftig mit einem Hindernis zusammenstoßen" + ] + }, + "examples": { + "en": [ + { + "text": "I ran into the telephone pole", + "source": "omw" + } + ], + "it": [ + { + "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" + } ], "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" @@ -292,19 +557,42 @@ { "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", @@ -316,51 +604,98 @@ "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" }, @@ -368,11 +703,23 @@ "source_id": "ili:i112909", "pos": "noun", "translations": { - "en": ["regulation"], - "es": ["reglamento"], - "fr": ["réglementation", "gouvernement", "tenue"] + "en": [ + "regulation" + ], + "es": [ + "reglamento" + ], + "fr": [ + "réglementation", + "gouvernement", + "tenue" + ] + }, + "glosses": { + "en": [ + "the state of being controlled or governed" + ] }, - "glosses": { "en": ["the state of being controlled or governed"] }, "examples": { "fr": [ { @@ -388,16 +735,35 @@ "source": "cefr" } ], - "es": [{ "text": "Debemos seguir el reglamento.", "source": "cefr" }] + "es": [ + { + "text": "Debemos seguir el reglamento.", + "source": "cefr" + } + ] }, "votes": { - "en": { "regulation": { "cefr_source": "B2" } }, - "fr": { - "réglementation": { "cefr_source": "B2" }, - "gouvernement": { "cefr_source": "B1" }, - "tenue": { "cefr_source": "B1" } + "en": { + "regulation": { + "cefr_source": "B2" + } }, - "es": { "reglamento": { "cefr_source": "B2" } } + "fr": { + "réglementation": { + "cefr_source": "B2" + }, + "gouvernement": { + "cefr_source": "B1" + }, + "tenue": { + "cefr_source": "B1" + } + }, + "es": { + "reglamento": { + "cefr_source": "B2" + } + } }, "_sample_bucket": "has_cefr_vote" }, @@ -412,8 +778,12 @@ "ladybird", "ladybird beetle" ], - "it": ["coccinella"], - "fr": ["coccinelle"] + "it": [ + "coccinella" + ], + "fr": [ + "coccinelle" + ] }, "glosses": { "en": [ @@ -422,12 +792,23 @@ }, "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" }, @@ -435,32 +816,56 @@ "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": [ { @@ -468,37 +873,104 @@ "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" } }, - "it": { - "giudiziale": { "cefr_source": "C1" }, - "giudiziario": { "cefr_source": "C1" } + "en": { + "judicial": { + "cefr_source": "C1" + } }, - "de": { "richterlich": { "cefr_source": "C1" } }, - "fr": { "judiciaire": { "cefr_source": "B2" } }, - "es": { "judicial": { "cefr_source": "C1" } } + "it": { + "giudiziale": { + "cefr_source": "C1" + }, + "giudiziario": { + "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" }, @@ -516,7 +988,10 @@ "tawdriness", "glitz" ], - "it": ["pacchianeria", "vistosità"], + "it": [ + "pacchianeria", + "vistosità" + ], "es": [ "astracanada", "chabacanería", @@ -526,12 +1001,22 @@ "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": [ @@ -543,10 +1028,18 @@ }, "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" }, @@ -554,19 +1047,52 @@ "source_id": "ili:i22613", "pos": "verb", "translations": { - "en": ["scavenge", "clean"], - "es": ["limpiar"], - "fr": ["nettoyer"] + "en": [ + "scavenge", + "clean" + ], + "es": [ + "limpiar" + ], + "fr": [ + "nettoyer" + ] + }, + "glosses": { + "en": [ + "remove unwanted substances from" + ] }, - "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" }, @@ -574,15 +1100,35 @@ "source_id": "ili:i4857", "pos": "adjective", "translations": { - "en": ["enthusiastic"], - "it": ["caloroso", "entusiastico", "fervido", "entusiasta"], - "fr": ["courageux", "enthousiaste"] + "en": [ + "enthusiastic" + ], + "it": [ + "caloroso", + "entusiastico", + "fervido", + "entusiasta" + ], + "fr": [ + "courageux", + "enthousiaste" + ] + }, + "glosses": { + "en": [ + "having or showing great excitement and interest" + ] }, - "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" @@ -597,10 +1143,16 @@ "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" @@ -608,15 +1160,29 @@ ] }, "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" @@ -625,8 +1191,13 @@ "source_id": "ili:i104521", "pos": "noun", "translations": { - "en": ["veronica", "speedwell"], - "it": ["veronica"], + "en": [ + "veronica", + "speedwell" + ], + "it": [ + "veronica" + ], "de": [ "Allerweltsheil", "Grundheil", @@ -635,11 +1206,18 @@ "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": [ @@ -649,21 +1227,56 @@ } ] }, - "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" }, @@ -671,11 +1284,23 @@ "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": [ @@ -693,12 +1318,23 @@ } ], "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" }, @@ -706,14 +1342,29 @@ "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.", @@ -722,8 +1373,16 @@ ] }, "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" }, @@ -731,11 +1390,23 @@ "source_id": "ili:i44747", "pos": "noun", "translations": { - "en": ["Centrocercus", "genus Centrocercus"], - "es": ["Centrocercus", "género Centrocercus"], - "fr": ["centrocercus"] + "en": [ + "Centrocercus", + "genus Centrocercus" + ], + "es": [ + "Centrocercus", + "género Centrocercus" + ], + "fr": [ + "centrocercus" + ] + }, + "glosses": { + "en": [ + "sage grouse" + ] }, - "glosses": { "en": ["sage grouse"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_cefr_vote" @@ -743,8 +1414,16 @@ { "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": [ { @@ -759,8 +1438,16 @@ { "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": [ { @@ -776,7 +1463,9 @@ "source_id": "ili:i46616", "pos": "noun", "translations": { - "en": ["sand cat"], + "en": [ + "sand cat" + ], "fr": [ "chat de marguerite", "chat du désert", @@ -784,7 +1473,11 @@ "chat des sables" ] }, - "glosses": { "en": ["a desert wildcat"] }, + "glosses": { + "en": [ + "a desert wildcat" + ] + }, "examples": {}, "votes": {}, "_sample_bucket": "no_cefr_vote" @@ -792,9 +1485,15 @@ { "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": {}, @@ -804,11 +1503,19 @@ "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": {}, @@ -818,9 +1525,18 @@ "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": [ @@ -835,12 +1551,26 @@ "source_id": "ili:i90317", "pos": "noun", "translations": { - "en": ["great-uncle", "granduncle"], - "it": ["protio", "prozio"], - "es": ["tío abuelo"], - "fr": ["grand-oncle"] + "en": [ + "great-uncle", + "granduncle" + ], + "it": [ + "protio", + "prozio" + ], + "es": [ + "tío abuelo" + ], + "fr": [ + "grand-oncle" + ] + }, + "glosses": { + "en": [ + "an uncle of your father or mother" + ] }, - "glosses": { "en": ["an uncle of your father or mother"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_cefr_vote" @@ -849,10 +1579,19 @@ "source_id": "ili:i53881", "pos": "noun", "translations": { - "en": ["flour bin"], - "es": ["frasco de harina", "tarro de harina"] + "en": [ + "flour bin" + ], + "es": [ + "frasco de harina", + "tarro de harina" + ] + }, + "glosses": { + "en": [ + "a bin for holding flour" + ] }, - "glosses": { "en": ["a bin for holding flour"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_cefr_vote" @@ -860,8 +1599,19 @@ { "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" @@ -870,10 +1620,15 @@ "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": {}, @@ -883,9 +1638,17 @@ "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": [ @@ -899,17 +1662,47 @@ { "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" @@ -917,7 +1710,11 @@ { "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" @@ -931,10 +1728,20 @@ "source_id": "ili:i47159", "pos": "noun", "translations": { - "en": ["Fenusa", "genus-Fenusa"], - "es": ["Fenusa", "género Fenusa"] + "en": [ + "Fenusa", + "genus-Fenusa" + ], + "es": [ + "Fenusa", + "género Fenusa" + ] + }, + "glosses": { + "en": [ + "birch leaf miner" + ] }, - "glosses": { "en": ["birch leaf miner"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_cefr_vote" @@ -942,8 +1749,16 @@ { "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" @@ -952,10 +1767,19 @@ "source_id": "ili:i46047", "pos": "noun", "translations": { - "en": ["Polynesian tattler", "Heteroscelus incanus"], - "fr": ["heteroscelus incanus"] + "en": [ + "Polynesian tattler", + "Heteroscelus incanus" + ], + "fr": [ + "heteroscelus incanus" + ] + }, + "glosses": { + "en": [ + "tattler of Pacific coastal regions" + ] }, - "glosses": { "en": ["tattler of Pacific coastal regions"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_cefr_vote" @@ -963,7 +1787,11 @@ { "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" @@ -977,11 +1805,29 @@ "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": [ @@ -999,7 +1845,11 @@ "source_id": "ili:i74228", "pos": "noun", "translations": { - "en": ["negotiation", "dialogue", "talks"], + "en": [ + "negotiation", + "dialogue", + "talks" + ], "it": [ "contrattazione", "deal", @@ -1008,13 +1858,27 @@ "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": [ @@ -1022,15 +1886,24 @@ "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" @@ -1043,7 +1916,10 @@ "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": [ { @@ -1066,8 +1942,14 @@ } ], "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" @@ -1076,29 +1958,59 @@ }, "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" @@ -1107,8 +2019,13 @@ "source_id": "ili:i408", "pos": "adjective", "translations": { - "en": ["aground"], - "es": ["encallado", "varado"], + "en": [ + "aground" + ], + "es": [ + "encallado", + "varado" + ], "de": [ "aufgrund", "dank", @@ -1120,28 +2037,48 @@ ] }, "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" }] + "es": [ + { + "text": "El barco quedó varado en la arena.", + "source": "cefr" + } + ] + }, + "votes": { + "es": { + "varado": { + "cefr_source": "B2" + } + } }, - "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" @@ -1155,14 +2092,31 @@ } ] }, - "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": [ { @@ -1172,19 +2126,40 @@ ] }, "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"] }, - "glosses": { - "en": ["marked by the wearing of heavy boots studded with hobnails"] + "translations": { + "en": [ + "hobnailed" + ] + }, + "glosses": { + "en": [ + "marked by the wearing of heavy boots studded with hobnails" + ] + }, + "examples": { + "en": [ + { + "text": "hobnailed laborers", + "source": "omw" + } + ] }, - "examples": { "en": [{ "text": "hobnailed laborers", "source": "omw" }] }, "votes": {}, "_sample_bucket": "has_glosses_and_examples" }, @@ -1192,74 +2167,145 @@ "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" } + "deposito": { + "cefr_source": "B1" + }, + "sedimento": { + "cefr_source": "B2" + } + }, + "de": { + "Ablagerung": { + "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" }, @@ -1267,9 +2313,15 @@ "source_id": "ili:i117521", "pos": "noun", "translations": { - "en": ["moratorium"], - "it": ["moratoria"], - "fr": ["moratoire"] + "en": [ + "moratorium" + ], + "it": [ + "moratoria" + ], + "fr": [ + "moratoire" + ] }, "glosses": { "en": [ @@ -1291,9 +2343,21 @@ ] }, "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" }, @@ -1301,10 +2365,20 @@ "source_id": "ili:i31764", "pos": "verb", "translations": { - "en": ["return"], - "fr": ["rendre", "retourner", "revenir"] + "en": [ + "return" + ], + "fr": [ + "rendre", + "retourner", + "revenir" + ] + }, + "glosses": { + "en": [ + "return to a previous position; in mathematics" + ] }, - "glosses": { "en": ["return to a previous position; in mathematics"] }, "examples": { "en": [ { @@ -1321,14 +2395,23 @@ "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" @@ -1337,9 +2420,17 @@ "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": [ @@ -1348,12 +2439,23 @@ }, "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" }, @@ -1361,20 +2463,47 @@ "source_id": "ili:i51126", "pos": "noun", "translations": { - "en": ["brickwork"], - "it": ["GAP!", "muratura in mattoni"], - "es": ["aparejo", "calicanto", "enladrillado", "mampostería"], - "fr": ["appareil"] + "en": [ + "brickwork" + ], + "it": [ + "GAP!", + "muratura in mattoni" + ], + "es": [ + "aparejo", + "calicanto", + "enladrillado", + "mampostería" + ], + "fr": [ + "appareil" + ] + }, + "glosses": { + "en": [ + "masonry done with bricks and mortar" + ] }, - "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" }, @@ -1382,26 +2511,41 @@ "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": [ { @@ -1423,10 +2567,26 @@ ] }, "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" }, @@ -1434,9 +2594,15 @@ "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": [ @@ -1451,19 +2617,34 @@ } ] }, - "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)" ] @@ -1477,8 +2658,16 @@ ] }, "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" }, @@ -1486,11 +2675,22 @@ "source_id": "ili:i67108", "pos": "noun", "translations": { - "en": ["stocktaking", "stock-taking"], - "it": ["inventario"], - "es": ["balance"] + "en": [ + "stocktaking", + "stock-taking" + ], + "it": [ + "inventario" + ], + "es": [ + "balance" + ] + }, + "glosses": { + "en": [ + "reappraisal of a situation or position or outlook" + ] }, - "glosses": { "en": ["reappraisal of a situation or position or outlook"] }, "examples": { "it": [ { @@ -1506,8 +2706,16 @@ ] }, "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" }, @@ -1525,7 +2733,9 @@ "whacky", "zany" ], - "es": ["tonto"], + "es": [ + "tonto" + ], "de": [ "albern", "naiv", @@ -1539,69 +2749,164 @@ "infantil", "puerilistisch" ], - "fr": ["déraisonnable", "fou", "drôle", "aberrant"] + "fr": [ + "déraisonnable", + "fou", + "drôle", + "aberrant" + ] + }, + "glosses": { + "en": [ + "ludicrous, foolish" + ], + "de": [ + "lächerlich, töricht" + ] }, - "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" @@ -1610,11 +2915,26 @@ "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": [ @@ -1645,39 +2965,91 @@ ] }, "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"] }, - "examples": { - "en": [{ "text": "thirty respirations per minute", "source": "omw" }], + "translations": { + "en": [ + "respiration" + ], "es": [ - { "text": "Su respiración era lenta y profunda.", "source": "cefr" } + "respiración" + ] + }, + "glosses": { + "en": [ + "a single complete act of breathing in and out" + ] + }, + "examples": { + "en": [ + { + "text": "thirty respirations per minute", + "source": "omw" + } + ], + "es": [ + { + "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": [ { @@ -1687,8 +3059,16 @@ ] }, "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" }, @@ -1696,10 +3076,20 @@ "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": [ @@ -1711,18 +3101,31 @@ }, "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", @@ -1732,8 +3135,12 @@ ] }, "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": {}, @@ -1743,10 +3150,19 @@ "source_id": "ili:i14067", "pos": "adjective", "translations": { - "en": ["maximizing", "maximising"], - "fr": ["maximaliste"] + "en": [ + "maximizing", + "maximising" + ], + "fr": [ + "maximaliste" + ] + }, + "glosses": { + "en": [ + "making as great as possible" + ] }, - "glosses": { "en": ["making as great as possible"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_glosses_no_examples" @@ -1755,14 +3171,27 @@ "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": {}, @@ -1772,11 +3201,25 @@ "source_id": "ili:i97025", "pos": "noun", "translations": { - "en": ["Stockton", "Frank Stockton", "Francis Richard Stockton"], - "es": ["Francis Richard Stockton", "Frank Stockton", "Stockton"], - "fr": ["Stockton"] + "en": [ + "Stockton", + "Frank Stockton", + "Francis Richard Stockton" + ], + "es": [ + "Francis Richard Stockton", + "Frank Stockton", + "Stockton" + ], + "fr": [ + "Stockton" + ] + }, + "glosses": { + "en": [ + "United States writer (1834-1902)" + ] }, - "glosses": { "en": ["United States writer (1834-1902)"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_glosses_no_examples" @@ -1784,7 +3227,11 @@ { "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" @@ -1798,8 +3245,13 @@ "source_id": "ili:i94985", "pos": "noun", "translations": { - "en": ["Eames", "Charles Eames"], - "es": ["Charles Eames"] + "en": [ + "Eames", + "Charles Eames" + ], + "es": [ + "Charles Eames" + ] }, "glosses": { "en": [ @@ -1814,10 +3266,20 @@ "source_id": "ili:i16699", "pos": "adjective", "translations": { - "en": ["mensural", "measured", "mensurable"], - "es": ["mensural"] + "en": [ + "mensural", + "measured", + "mensurable" + ], + "es": [ + "mensural" + ] + }, + "glosses": { + "en": [ + "having notes of fixed rhythmic value" + ] }, - "glosses": { "en": ["having notes of fixed rhythmic value"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_glosses_no_examples" @@ -1826,8 +3288,13 @@ "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": [ @@ -1841,8 +3308,19 @@ { "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": [ { @@ -1857,7 +3335,11 @@ { "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" @@ -1871,10 +3353,16 @@ "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": {}, @@ -1883,8 +3371,16 @@ { "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" @@ -1892,8 +3388,19 @@ { "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" @@ -1901,9 +3408,27 @@ { "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" }, @@ -1911,10 +3436,19 @@ "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": [ @@ -1929,8 +3463,14 @@ "source_id": "ili:i105979", "pos": "noun", "translations": { - "en": ["Dryopteris", "genus Dryopteris"], - "fr": ["Dryopteris", "dryopteris"] + "en": [ + "Dryopteris", + "genus Dryopteris" + ], + "fr": [ + "Dryopteris", + "dryopteris" + ] }, "glosses": { "en": [ @@ -1945,10 +3485,19 @@ "source_id": "ili:i44411", "pos": "noun", "translations": { - "en": ["blue racer", "Coluber constrictor flaviventris"], - "fr": ["coluber constrictor"] + "en": [ + "blue racer", + "Coluber constrictor flaviventris" + ], + "fr": [ + "coluber constrictor" + ] + }, + "glosses": { + "en": [ + "bluish-green blacksnake found from Ohio to Texas" + ] }, - "glosses": { "en": ["bluish-green blacksnake found from Ohio to Texas"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_glosses_no_examples" @@ -1957,11 +3506,18 @@ "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": {}, @@ -1971,10 +3527,19 @@ "source_id": "ili:i5174", "pos": "adjective", "translations": { - "en": ["protrusile", "protrusible"], - "fr": ["protrusible"] + "en": [ + "protrusile", + "protrusible" + ], + "fr": [ + "protrusible" + ] + }, + "glosses": { + "en": [ + "capable of being thrust forward, as the tongue" + ] }, - "glosses": { "en": ["capable of being thrust forward, as the tongue"] }, "examples": {}, "votes": {}, "_sample_bucket": "no_glosses_no_examples" @@ -1982,8 +3547,17 @@ { "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" @@ -1992,9 +3566,16 @@ "source_id": "ili:i97983", "pos": "noun", "translations": { - "en": ["phosphorescence"], - "it": ["fosforescenza", "fotoluminescenza"], - "fr": ["phosphorescence"] + "en": [ + "phosphorescence" + ], + "it": [ + "fosforescenza", + "fotoluminescenza" + ], + "fr": [ + "phosphorescence" + ] }, "glosses": { "en": [ @@ -2008,9 +3589,16 @@ { "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": {}, @@ -2019,8 +3607,20 @@ { "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" @@ -2028,18 +3628,38 @@ { "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" }, @@ -2047,19 +3667,47 @@ "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": [ { @@ -2097,44 +3745,105 @@ ] }, "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" }, @@ -2142,8 +3851,15 @@ "source_id": "ili:i22943", "pos": "verb", "translations": { - "en": ["slake", "abate", "slack"], - "es": ["aflojar", "reducir"], + "en": [ + "slake", + "abate", + "slack" + ], + "es": [ + "aflojar", + "reducir" + ], "fr": [ "descendre", "cesser", @@ -2154,25 +3870,47 @@ "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" @@ -2180,19 +3918,41 @@ ] }, "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" @@ -2200,22 +3960,50 @@ { "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"] + "en": [ + "imprudent" + ], + "it": [ + "imprudente", + "incauto" + ], + "es": [ + "imprudente", + "insensato" + ], + "fr": [ + "imprudent" + ] + }, + "glosses": { + "en": [ + "not prudent or wise" + ] }, - "glosses": { "en": ["not prudent or wise"] }, "examples": { "en": [ { @@ -2244,15 +4032,30 @@ "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" @@ -2261,13 +4064,36 @@ "source_id": "ili:i8645", "pos": "adjective", "translations": { - "en": ["metaphysical"], - "es": ["metafísico"], - "fr": ["métaphysique"] + "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" + } + } }, - "glosses": { "en": ["without material form or substance"] }, - "examples": { "en": [{ "text": "metaphysical forces", "source": "omw" }] }, - "votes": { "en": { "metaphysical": { "cefr_source": "C1" } } }, "_sample_bucket": "pos_spread" }, { @@ -2281,8 +4107,13 @@ "essential", "of the essence" ], - "it": ["essenziale"], - "es": ["crucial", "esencial"], + "it": [ + "essenziale" + ], + "es": [ + "crucial", + "esencial" + ], "de": [ "bedeutsam", "wesentlich", @@ -2293,26 +4124,52 @@ "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" } + { + "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" + } ], - "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" @@ -2321,7 +4178,10 @@ "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": [ { @@ -2330,28 +4190,62 @@ } ], "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" } + "crucial": { + "cefr_source": "B2" + }, + "essential": { + "cefr_source": "B1" + } + }, + "it": { + "essenziale": { + "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" } + "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" + } }, - "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" @@ -2359,9 +4253,24 @@ { "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" }, @@ -2369,53 +4278,134 @@ "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" }] + "fr": [ + { + "text": "Il est de nationalité monégasque.", + "source": "cefr" + } + ] + }, + "votes": { + "fr": { + "monégasque": { + "cefr_source": "B1" + } + } }, - "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)"] }, - "examples": { - "en": [{ "text": "come here, please", "source": "omw" }], - "it": [ - { "text": "Vieni qua, per favore.", "source": "cefr" }, - { "text": "Vieni qui!", "source": "cefr" } + "en": [ + "here", + "hither" ], - "fr": [{ "text": "Venez ici !", "source": "cefr" }] + "it": [ + "qua", + "qui" + ], + "fr": [ + "ici", + "çà", + "par ici" + ] + }, + "glosses": { + "en": [ + "to this place (especially toward the speaker)" + ] + }, + "examples": { + "en": [ + { + "text": "come here, please", + "source": "omw" + } + ], + "it": [ + { + "text": "Vieni qua, per favore.", + "source": "cefr" + }, + { + "text": "Vieni qui!", + "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" @@ -2423,8 +4413,16 @@ { "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": [ { @@ -2440,14 +4438,28 @@ "source_id": "ili:i20131", "pos": "adverb", "translations": { - "en": ["gallantly", "chivalrously"], - "it": ["galantemente"], - "fr": ["chevaleresquement"] + "en": [ + "gallantly", + "chivalrously" + ], + "it": [ + "galantemente" + ], + "fr": [ + "chevaleresquement" + ] + }, + "glosses": { + "en": [ + "in a gallant manner" + ] }, - "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": {}, @@ -2456,8 +4468,16 @@ { "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": [ { @@ -2469,4 +4489,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 7752b6c..83c3053 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/backlog.md b/documentation/backlog.md index d0cb202..4e6af84 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/data-pipeline.md b/documentation/data-pipeline.md index 4d1bbc7..56285b9 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,7 +158,6 @@ Words appearing in the CEFR source file multiple times with different CEFR level **Input:** `stage-1-extract/output/omw.json` + `stage-2-annotate/sources/cefr/{lang}.json` **Output:** - - `stage-2-annotate/output/{lang}.json` — one per language - `stage-2-annotate/output/conflicts.json` — cross-language conflicts for review @@ -178,14 +177,20 @@ Each record in the output extends the OMW record with a `votes` field and any ad "es": ["capaz"], "fr": ["comptable"] }, - "glosses": { "en": ["having the necessary means or skill to do something"] }, + "glosses": { + "en": ["having the necessary means or skill to do something"] + }, "examples": { "en": [ { "text": "able to swim", "source": "omw" }, { "text": "She was able to finish the task.", "source": "cefr" } ] }, - "votes": { "en": { "able": { "cefr_source": "B1" } } } + "votes": { + "en": { + "able": { "cefr_source": "B1" } + } + } } ``` @@ -292,7 +297,9 @@ Each record in the votes file looks like this: } }, "examples": { - "en": [{ "text": "the dog barked at the stranger", "source": "omw" }], + "en": [ + { "text": "the dog barked at the stranger", "source": "omw" } + ], "fr": { "candidates": [ { "text": "le chien a aboyé", "source": "model_1" }, @@ -304,14 +311,8 @@ Each record in the votes file looks like this: "descriptions": { "en": { "candidates": [ - { - "text": "a common household pet known for loyalty", - "source": "model_1" - }, - { - "text": "a domesticated animal and loyal companion", - "source": "model_2" - } + { "text": "a common household pet known for loyalty", "source": "model_1" }, + { "text": "a domesticated animal and loyal companion", "source": "model_2" } ], "votes": { "model_1": 2, "model_2": 1 } } @@ -333,15 +334,14 @@ 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,15 +360,21 @@ Each record in `final/{lang}.json` looks like this: { "text": "dog", "cefr_level": "A1", "difficulty": "easy" }, { "text": "canine", "cefr_level": "B2", "difficulty": "intermediate" } ], - "it": [{ "text": "cane", "cefr_level": "A1", "difficulty": "easy" }] + "it": [ + { "text": "cane", "cefr_level": "A1", "difficulty": "easy" } + ] }, "glosses": { "en": { "text": "a domesticated carnivorous mammal", "source": "omw" }, "fr": { "text": "un mammifère carnivore domestiqué", "source": "model_1" } }, "examples": { - "en": [{ "text": "the dog barked at the stranger", "source": "omw" }], - "fr": [{ "text": "le chien a aboyé", "source": "model_1" }] + "en": [ + { "text": "the dog barked at the stranger", "source": "omw" } + ], + "fr": [ + { "text": "le chien a aboyé", "source": "model_1" } + ] }, "descriptions": { "en": { @@ -394,7 +400,6 @@ output quality per language. Run this after merge to verify output before seeding the database. **Input:** - - `stage-4-merge/output/final/{lang}.json` - `stage-4-merge/output/flagged/{lang}.json` @@ -431,12 +436,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 66d97e8..de1d3a0 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 23a2ba8..6cc1f91 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,7 +71,6 @@ Two candidates worth testing, covering different points on the size/quality tradeoff: **Gemma 4 E4B Instruct (Q4 / UD-Q4_K_XL)** - - GGUF file: `gemma-4-E4B-it-UD-Q4_K_XL.gguf` (~2.5 GB) - Source: https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF - Runs fully on GPU. Brand new (April 2025), built for edge hardware, 140+ @@ -79,7 +78,6 @@ tradeoff: to test. **Qwen2.5 7B Instruct (Q4_K_M)** - - GGUF file: `Qwen2.5-7B-Instruct-Q4_K_M.gguf` (~4.5 GB) - Source: https://huggingface.co/Qwen/Qwen2.5-7B-Instruct-GGUF - Runs in hybrid mode (~26 of 32 layers on GPU, rest on CPU), ~8–12 tok/s. @@ -109,7 +107,6 @@ wget -O models/qwen2.5-3b-instruct-q4_k_m.gguf \ ### Starting the server **Gemma 4 E4B** (full GPU): - ```bash ./build/bin/llama-server \ --model models/gemma-4-e4b-it-ud-q4_k_xl.gguf \ @@ -120,7 +117,6 @@ wget -O models/qwen2.5-3b-instruct-q4_k_m.gguf \ ``` **Qwen2.5 7B** (hybrid — tune `--n-gpu-layers` to fit your VRAM): - ```bash ./build/bin/llama-server \ --model models/qwen2.5-7b-instruct-q4_k_m.gguf \ @@ -167,16 +163,15 @@ 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 @@ -199,7 +194,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; @@ -210,8 +205,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, }; @@ -236,7 +231,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, @@ -244,7 +239,6 @@ export const ANTHROPIC_SONNET: ProviderConfig = { ``` Output from each run lands in: - ``` stage-3-enrich/test/output/{provider.name}/results.json stage-3-enrich/test/output/{provider.name}/metrics.json @@ -258,21 +252,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 4391d87..8a8d414 100644 --- a/documentation/notes.md +++ b/documentation/notes.md @@ -1,5 +1,6 @@ # notes + ## prompt ive attached the readme of my project. this is my current task: @@ -45,7 +46,7 @@ laptop: verify if docker containers run on startup (they shouldnt) ### vps setup - monitoring and logging (eg via chrootkit or rkhunter, logwatch/monit => mails daily with summary) - <<<<<<< HEAD +<<<<<<< HEAD - ~~keep the vps clean (e.g. old docker images/containers)~~ ✅ CI/CD pipeline runs `docker image prune -f` after deploy ### ~~cd/ci pipeline~~ ✅ RESOLVED @@ -54,9 +55,9 @@ Forgejo Actions with runner on VPS, Forgejo built-in container registry. See `de ### ~~postgres backups~~ ✅ RESOLVED -# Daily pg_dump cron job, 7-day retention, dev laptop auto-sync via rsync. See `deployment.md`. - -> > > > > > > dev +Daily pg_dump cron job, 7-day retention, dev laptop auto-sync via rsync. See `deployment.md`. +======= +>>>>>>> dev ### try now option diff --git a/documentation/roasts/gameService.md b/documentation/roasts/gameService.md index 0db3559..e5663b5 100644 --- a/documentation/roasts/gameService.md +++ b/documentation/roasts/gameService.md @@ -61,12 +61,10 @@ export const evaluateAnswer = async ( store: GameSessionStore, ): Promise => { const session = await store.get(submission.sessionId); - if (!session) - throw new NotFoundError(`Game session not found: ${submission.sessionId}`); + if (!session) throw new NotFoundError(`Game session not found: ${submission.sessionId}`); const correctOptionId = session.answers.get(submission.questionId); - if (correctOptionId === undefined) - throw new NotFoundError(`Question not found: ${submission.questionId}`); + if (correctOptionId === undefined) throw new NotFoundError(`Question not found: ${submission.questionId}`); // delete answered question; delete session when all questions are answered session.answers.delete(submission.questionId); @@ -86,14 +84,10 @@ export const evaluateAnswer = async ( ```ts // ✅ option B — TTL in InMemoryGameSessionStore export class InMemoryGameSessionStore implements GameSessionStore { - private sessions = new Map< - string, - { data: GameSessionData; expiresAt: number } - >(); + private sessions = new Map(); private readonly ttlMs: number; - constructor(ttlMs = 30 * 60 * 1000) { - // 30 minutes default + constructor(ttlMs = 30 * 60 * 1000) { // 30 minutes default this.ttlMs = ttlMs; } @@ -121,13 +115,51 @@ export class InMemoryGameSessionStore implements GameSessionStore { --- +## 3. `shuffle` is defined after it's used + +**Problem** + +`shuffle` is called inside `createGameSession` but defined below it. It works at runtime (module evaluation order), but reads as if the file was written top-to-bottom without a plan. + +```ts +// ❌ shuffle appears after the function that calls it +export const createGameSession = async (...) => { + const shuffledTexts = shuffle(optionTexts); // used here +}; + +const shuffle = (array: T[]): T[] => { ... }; // defined down here +``` + +**Fix — move helpers to the top, exports to the bottom** + +```ts +// ✅ utilities first, then exported functions +const shuffle = (array: T[]): T[] => { + const result = [...array]; + for (let i = result.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const temp = result[i]!; + result[i] = result[j]!; + result[j] = temp; + } + return result; +}; + +export const createGameSession = async (...) => { ... }; +export const evaluateAnswer = async (...) => { ... }; +``` + +--- + + + **Problem** `GameRequest.rounds` is typed as `string` in `@lila/shared`, forcing the service to cast it every time: ```ts // ❌ why is a round count a string? -Number(request.rounds); +Number(request.rounds) ``` **Fix — fix the schema in `@lila/shared`** @@ -149,6 +181,8 @@ The `z.coerce.number()` handles the case where the value arrives as a string fro --- +## 5. `correctAnswers` is a misleading variable name + **Problem** The variable holds `terms` — word pairs fetched from the database. Calling them `correctAnswers` jumps ahead semantically; they only become "correct answers" once options are constructed around them. @@ -208,10 +242,7 @@ it("correct answer appears exactly once in options even if distractor matches", // simulate getDistractors returning the correct answer as one of the distractors mockGetDistractors.mockResolvedValueOnce(["cane", "wrong2", "wrong3"]); - const session = await createGameSession( - validRequest, - new InMemoryGameSessionStore(), - ); + const session = await createGameSession(validRequest, new InMemoryGameSessionStore()); const question = session.questions[0]!; const optionTexts = question.options.map((o) => o.text); @@ -292,14 +323,16 @@ his `sessionId`. ```ts // GameSessionStore.ts -export type GameSessionData = { answers: Map; userId: string }; +export type GameSessionData = { + answers: Map; + userId: string; +}; // evaluateAnswer const session = await store.get(submission.sessionId); if (!session) throw new NotFoundError(`Game session not found`); -if (session.userId !== requestingUserId) - throw new NotFoundError(`Game session not found`); +if (session.userId !== requestingUserId) throw new NotFoundError(`Game session not found`); // ^^^ same error — don't confirm the session exists to the wrong user ``` @@ -331,9 +364,8 @@ if (terms.length === 0) { it("throws when getGameTerms returns no terms", async () => { mockGetGameTerms.mockResolvedValue([]); - await expect( - createGameSession(validRequest, new InMemoryGameSessionStore()), - ).rejects.toThrow("No terms found"); + await expect(createGameSession(validRequest, new InMemoryGameSessionStore())) + .rejects.toThrow("No terms found"); }); ``` @@ -355,9 +387,8 @@ it("throws when getGameTerms returns no terms", async () => { it("propagates getDistractors failure", async () => { mockGetDistractors.mockRejectedValue(new Error("db timeout")); - await expect( - createGameSession(validRequest, new InMemoryGameSessionStore()), - ).rejects.toThrow("db timeout"); + await expect(createGameSession(validRequest, new InMemoryGameSessionStore())) + .rejects.toThrow("db timeout"); }); ``` diff --git a/documentation/spec.md b/documentation/spec.md index 29938da..b16fbc0 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,27 +288,26 @@ 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 c5fdf8a..7e612bb 100644 --- a/documentation/tickets/blueprint.md +++ b/documentation/tickets/blueprint.md @@ -1,12 +1,11 @@ # 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. --- ## Format A — ADR (architectural/infrastructural decisions) - Use when: you chose between options with long-term consequences. Prefix: `adr-` @@ -15,56 +14,45 @@ Prefix: `adr-` # ADR: ## Status - Accepted | Superseded by | Deprecated ## Date - YYYY-MM-DD ## Context - What is the problem? Why does it need to be solved? ## Decision - What was chosen and why in one or two sentences. ## Options considered ### Option A — <name> ✅ - Description. Why it was chosen. ### Option B — <name> - Description. Why it was rejected. ## Consequences - - What gets better - What gets worse or more complex - Operational implications - What breaks if this needs to be redone ## Affected files / machines - - List files, servers, or systems touched ## References - - Links to relevant docs --- ## Setup guide / implementation notes - Step-by-step of what was actually done. --- ## Format B — Task (features, fixes, chores) - Use when: routine task with a clear solution. Prefix: `feat-` / `fix-` / `chore-` @@ -73,23 +61,17 @@ Prefix: `feat-` / `fix-` / `chore-` # <prefix>: <title> ## Problem - What was wrong or missing? ## Options considered - ### Option A — <name> ✅ - ### Option B — <name> ## Solution - What was done and why. ## Files changed - - `path/to/file.ts` ## Commit - `<type>: <message>` diff --git a/documentation/tickets/t00001.md b/documentation/tickets/t00001.md index f7f1a09..4fffaec 100644 --- a/documentation/tickets/t00001.md +++ b/documentation/tickets/t00001.md @@ -87,7 +87,9 @@ pass init <your-key-id> Replace the entire file contents with: ```json -{ "credsStore": "pass" } +{ + "credsStore": "pass" +} ``` ### 6. Re-login to registries diff --git a/documentation/tickets/t00002.md b/documentation/tickets/t00002.md index 4f7fbb5..dc93605 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/documentation/tickets/t00003.md b/documentation/tickets/t00003.md deleted file mode 100644 index 774fe42..0000000 --- a/documentation/tickets/t00003.md +++ /dev/null @@ -1,37 +0,0 @@ -# refactor: extract shuffleArray to lib/utils, rename correctAnswers to terms - -## Problem - -Two readability issues in `gameService.ts`: - -1. `shuffle` was defined as a private function at the bottom of `gameService.ts`, after the function that calls it. It is a pure generic utility with no dependency on game domain logic, so it had no business living there. - -2. The variable holding terms fetched from the database was named `correctAnswers`. These are word pairs — they only become "correct answers" once options are built around them. The name was premature and misleading. - -## Options considered - -### Option A — Move `shuffle` up in the same file - -Simple, no new files. Fixes the ordering issue but keeps a generic utility buried in domain code. - -### Option B — Extract to `lib/utils.ts` ✅ - -Move `shuffle` (renamed `shuffleArray`) to `apps/api/src/lib/utils.ts` and import it. Cleaner separation: domain logic stays in services, generic utilities live in `lib/`. - -Chosen because `lib/` already exists, the function is reusable, and it gives future utilities a home. - -## Solution - -- Created `apps/api/src/lib/utils.ts` with `shuffleArray` -- Renamed `shuffle` → `shuffleArray` for clarity at the call site -- Removed the inline `shuffle` from `gameService.ts` and imported from `lib/utils.ts` -- Renamed `correctAnswers` → `terms` and `correctAnswer` → `term` throughout `gameService.ts` - -## Files changed - -- `apps/api/src/lib/utils.ts` — created -- `apps/api/src/services/gameService.ts` — removed `shuffle`, updated import, renamed variables - -## Commit - -`refactor: extract shuffleArray to lib/utils, rename correctAnswers to terms` diff --git a/packages/db/drizzle/meta/0007_snapshot.json b/packages/db/drizzle/meta/0007_snapshot.json index 051e87a..e5ee0b5 100644 --- a/packages/db/drizzle/meta/0007_snapshot.json +++ b/packages/db/drizzle/meta/0007_snapshot.json @@ -110,8 +110,12 @@ "name": "account_user_id_user_id_fk", "tableFrom": "account", "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -145,8 +149,12 @@ "name": "deck_terms_deck_id_decks_id_fk", "tableFrom": "deck_terms", "tableTo": "decks", - "columnsFrom": ["deck_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "deck_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" }, @@ -154,8 +162,12 @@ "name": "deck_terms_term_id_terms_id_fk", "tableFrom": "deck_terms", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -163,7 +175,10 @@ "compositePrimaryKeys": { "deck_terms_deck_id_term_id_pk": { "name": "deck_terms_deck_id_term_id_pk", - "columns": ["deck_id", "term_id"] + "columns": [ + "deck_id", + "term_id" + ] } }, "uniqueConstraints": {}, @@ -250,7 +265,10 @@ "unique_deck_name": { "name": "unique_deck_name", "nullsNotDistinct": false, - "columns": ["name", "source_language"] + "columns": [ + "name", + "source_language" + ] } }, "policies": {}, @@ -318,8 +336,12 @@ "name": "lobbies_host_user_id_user_id_fk", "tableFrom": "lobbies", "tableTo": "user", - "columnsFrom": ["host_user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "host_user_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -329,7 +351,9 @@ "lobbies_code_unique": { "name": "lobbies_code_unique", "nullsNotDistinct": false, - "columns": ["code"] + "columns": [ + "code" + ] } }, "policies": {}, @@ -378,8 +402,12 @@ "name": "lobby_players_lobby_id_lobbies_id_fk", "tableFrom": "lobby_players", "tableTo": "lobbies", - "columnsFrom": ["lobby_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "lobby_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" }, @@ -387,8 +415,12 @@ "name": "lobby_players_user_id_user_id_fk", "tableFrom": "lobby_players", "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -396,7 +428,10 @@ "compositePrimaryKeys": { "lobby_players_lobby_id_user_id_pk": { "name": "lobby_players_lobby_id_user_id_pk", - "columns": ["lobby_id", "user_id"] + "columns": [ + "lobby_id", + "user_id" + ] } }, "uniqueConstraints": {}, @@ -480,8 +515,12 @@ "name": "session_user_id_user_id_fk", "tableFrom": "session", "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -491,7 +530,9 @@ "session_token_unique": { "name": "session_token_unique", "nullsNotDistinct": false, - "columns": ["token"] + "columns": [ + "token" + ] } }, "policies": {}, @@ -547,8 +588,12 @@ "name": "term_glosses_term_id_terms_id_fk", "tableFrom": "term_glosses", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -558,7 +603,10 @@ "unique_term_gloss": { "name": "unique_term_gloss", "nullsNotDistinct": false, - "columns": ["term_id", "language_code"] + "columns": [ + "term_id", + "language_code" + ] } }, "policies": {}, @@ -593,8 +641,12 @@ "name": "term_topics_term_id_terms_id_fk", "tableFrom": "term_topics", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" }, @@ -602,8 +654,12 @@ "name": "term_topics_topic_id_topics_id_fk", "tableFrom": "term_topics", "tableTo": "topics", - "columnsFrom": ["topic_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "topic_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -611,7 +667,10 @@ "compositePrimaryKeys": { "term_topics_term_id_topic_id_pk": { "name": "term_topics_term_id_topic_id_pk", - "columns": ["term_id", "topic_id"] + "columns": [ + "term_id", + "topic_id" + ] } }, "uniqueConstraints": {}, @@ -685,7 +744,10 @@ "unique_source_id": { "name": "unique_source_id", "nullsNotDistinct": false, - "columns": ["source", "source_id"] + "columns": [ + "source", + "source_id" + ] } }, "policies": {}, @@ -741,7 +803,9 @@ "topics_slug_unique": { "name": "topics_slug_unique", "nullsNotDistinct": false, - "columns": ["slug"] + "columns": [ + "slug" + ] } }, "policies": {}, @@ -837,8 +901,12 @@ "name": "translations_term_id_terms_id_fk", "tableFrom": "translations", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -848,7 +916,11 @@ "unique_translations": { "name": "unique_translations", "nullsNotDistinct": false, - "columns": ["term_id", "language_code", "text"] + "columns": [ + "term_id", + "language_code", + "text" + ] } }, "policies": {}, @@ -925,7 +997,9 @@ "user_email_unique": { "name": "user_email_unique", "nullsNotDistinct": false, - "columns": ["email"] + "columns": [ + "email" + ] } }, "policies": {}, @@ -1006,5 +1080,9 @@ "roles": {}, "policies": {}, "views": {}, - "_meta": { "columns": {}, "schemas": {}, "tables": {} } -} + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/0008_snapshot.json b/packages/db/drizzle/meta/0008_snapshot.json index ebeb2b1..aab8d46 100644 --- a/packages/db/drizzle/meta/0008_snapshot.json +++ b/packages/db/drizzle/meta/0008_snapshot.json @@ -110,8 +110,12 @@ "name": "account_user_id_user_id_fk", "tableFrom": "account", "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -145,8 +149,12 @@ "name": "deck_terms_deck_id_decks_id_fk", "tableFrom": "deck_terms", "tableTo": "decks", - "columnsFrom": ["deck_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "deck_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" }, @@ -154,8 +162,12 @@ "name": "deck_terms_term_id_terms_id_fk", "tableFrom": "deck_terms", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -163,7 +175,10 @@ "compositePrimaryKeys": { "deck_terms_deck_id_term_id_pk": { "name": "deck_terms_deck_id_term_id_pk", - "columns": ["deck_id", "term_id"] + "columns": [ + "deck_id", + "term_id" + ] } }, "uniqueConstraints": {}, @@ -250,7 +265,10 @@ "unique_deck_name": { "name": "unique_deck_name", "nullsNotDistinct": false, - "columns": ["name", "source_language"] + "columns": [ + "name", + "source_language" + ] } }, "policies": {}, @@ -318,8 +336,12 @@ "name": "lobbies_host_user_id_user_id_fk", "tableFrom": "lobbies", "tableTo": "user", - "columnsFrom": ["host_user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "host_user_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -329,7 +351,9 @@ "lobbies_code_unique": { "name": "lobbies_code_unique", "nullsNotDistinct": false, - "columns": ["code"] + "columns": [ + "code" + ] } }, "policies": {}, @@ -378,8 +402,12 @@ "name": "lobby_players_lobby_id_lobbies_id_fk", "tableFrom": "lobby_players", "tableTo": "lobbies", - "columnsFrom": ["lobby_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "lobby_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" }, @@ -387,8 +415,12 @@ "name": "lobby_players_user_id_user_id_fk", "tableFrom": "lobby_players", "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -396,7 +428,10 @@ "compositePrimaryKeys": { "lobby_players_lobby_id_user_id_pk": { "name": "lobby_players_lobby_id_user_id_pk", - "columns": ["lobby_id", "user_id"] + "columns": [ + "lobby_id", + "user_id" + ] } }, "uniqueConstraints": {}, @@ -480,8 +515,12 @@ "name": "session_user_id_user_id_fk", "tableFrom": "session", "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -491,7 +530,9 @@ "session_token_unique": { "name": "session_token_unique", "nullsNotDistinct": false, - "columns": ["token"] + "columns": [ + "token" + ] } }, "policies": {}, @@ -563,8 +604,12 @@ "name": "term_examples_term_id_terms_id_fk", "tableFrom": "term_examples", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -574,7 +619,11 @@ "unique_term_example": { "name": "unique_term_example", "nullsNotDistinct": false, - "columns": ["term_id", "language_code", "text"] + "columns": [ + "term_id", + "language_code", + "text" + ] } }, "policies": {}, @@ -635,8 +684,12 @@ "name": "term_glosses_term_id_terms_id_fk", "tableFrom": "term_glosses", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -646,7 +699,10 @@ "unique_term_gloss": { "name": "unique_term_gloss", "nullsNotDistinct": false, - "columns": ["term_id", "language_code"] + "columns": [ + "term_id", + "language_code" + ] } }, "policies": {}, @@ -681,8 +737,12 @@ "name": "term_topics_term_id_terms_id_fk", "tableFrom": "term_topics", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" }, @@ -690,8 +750,12 @@ "name": "term_topics_topic_id_topics_id_fk", "tableFrom": "term_topics", "tableTo": "topics", - "columnsFrom": ["topic_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "topic_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -699,7 +763,10 @@ "compositePrimaryKeys": { "term_topics_term_id_topic_id_pk": { "name": "term_topics_term_id_topic_id_pk", - "columns": ["term_id", "topic_id"] + "columns": [ + "term_id", + "topic_id" + ] } }, "uniqueConstraints": {}, @@ -773,7 +840,10 @@ "unique_source_id": { "name": "unique_source_id", "nullsNotDistinct": false, - "columns": ["source", "source_id"] + "columns": [ + "source", + "source_id" + ] } }, "policies": {}, @@ -829,7 +899,9 @@ "topics_slug_unique": { "name": "topics_slug_unique", "nullsNotDistinct": false, - "columns": ["slug"] + "columns": [ + "slug" + ] } }, "policies": {}, @@ -925,8 +997,12 @@ "name": "translations_term_id_terms_id_fk", "tableFrom": "translations", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -936,7 +1012,11 @@ "unique_translations": { "name": "unique_translations", "nullsNotDistinct": false, - "columns": ["term_id", "language_code", "text"] + "columns": [ + "term_id", + "language_code", + "text" + ] } }, "policies": {}, @@ -1013,7 +1093,9 @@ "user_email_unique": { "name": "user_email_unique", "nullsNotDistinct": false, - "columns": ["email"] + "columns": [ + "email" + ] } }, "policies": {}, @@ -1094,5 +1176,9 @@ "roles": {}, "policies": {}, "views": {}, - "_meta": { "columns": {}, "schemas": {}, "tables": {} } -} + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/0009_snapshot.json b/packages/db/drizzle/meta/0009_snapshot.json index 6082664..8274112 100644 --- a/packages/db/drizzle/meta/0009_snapshot.json +++ b/packages/db/drizzle/meta/0009_snapshot.json @@ -110,8 +110,12 @@ "name": "account_user_id_user_id_fk", "tableFrom": "account", "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -145,8 +149,12 @@ "name": "deck_terms_deck_id_decks_id_fk", "tableFrom": "deck_terms", "tableTo": "decks", - "columnsFrom": ["deck_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "deck_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" }, @@ -154,8 +162,12 @@ "name": "deck_terms_term_id_terms_id_fk", "tableFrom": "deck_terms", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -163,7 +175,10 @@ "compositePrimaryKeys": { "deck_terms_deck_id_term_id_pk": { "name": "deck_terms_deck_id_term_id_pk", - "columns": ["deck_id", "term_id"] + "columns": [ + "deck_id", + "term_id" + ] } }, "uniqueConstraints": {}, @@ -250,7 +265,10 @@ "unique_deck_name": { "name": "unique_deck_name", "nullsNotDistinct": false, - "columns": ["name", "source_language"] + "columns": [ + "name", + "source_language" + ] } }, "policies": {}, @@ -337,8 +355,12 @@ "name": "lobbies_host_user_id_user_id_fk", "tableFrom": "lobbies", "tableTo": "user", - "columnsFrom": ["host_user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "host_user_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -348,7 +370,9 @@ "lobbies_code_unique": { "name": "lobbies_code_unique", "nullsNotDistinct": false, - "columns": ["code"] + "columns": [ + "code" + ] } }, "policies": {}, @@ -397,8 +421,12 @@ "name": "lobby_players_lobby_id_lobbies_id_fk", "tableFrom": "lobby_players", "tableTo": "lobbies", - "columnsFrom": ["lobby_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "lobby_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" }, @@ -406,8 +434,12 @@ "name": "lobby_players_user_id_user_id_fk", "tableFrom": "lobby_players", "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -415,7 +447,10 @@ "compositePrimaryKeys": { "lobby_players_lobby_id_user_id_pk": { "name": "lobby_players_lobby_id_user_id_pk", - "columns": ["lobby_id", "user_id"] + "columns": [ + "lobby_id", + "user_id" + ] } }, "uniqueConstraints": {}, @@ -499,8 +534,12 @@ "name": "session_user_id_user_id_fk", "tableFrom": "session", "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -510,7 +549,9 @@ "session_token_unique": { "name": "session_token_unique", "nullsNotDistinct": false, - "columns": ["token"] + "columns": [ + "token" + ] } }, "policies": {}, @@ -582,8 +623,12 @@ "name": "term_examples_term_id_terms_id_fk", "tableFrom": "term_examples", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -593,7 +638,11 @@ "unique_term_example": { "name": "unique_term_example", "nullsNotDistinct": false, - "columns": ["term_id", "language_code", "text"] + "columns": [ + "term_id", + "language_code", + "text" + ] } }, "policies": {}, @@ -654,8 +703,12 @@ "name": "term_glosses_term_id_terms_id_fk", "tableFrom": "term_glosses", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -665,7 +718,10 @@ "unique_term_gloss": { "name": "unique_term_gloss", "nullsNotDistinct": false, - "columns": ["term_id", "language_code"] + "columns": [ + "term_id", + "language_code" + ] } }, "policies": {}, @@ -700,8 +756,12 @@ "name": "term_topics_term_id_terms_id_fk", "tableFrom": "term_topics", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" }, @@ -709,8 +769,12 @@ "name": "term_topics_topic_id_topics_id_fk", "tableFrom": "term_topics", "tableTo": "topics", - "columnsFrom": ["topic_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "topic_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -718,7 +782,10 @@ "compositePrimaryKeys": { "term_topics_term_id_topic_id_pk": { "name": "term_topics_term_id_topic_id_pk", - "columns": ["term_id", "topic_id"] + "columns": [ + "term_id", + "topic_id" + ] } }, "uniqueConstraints": {}, @@ -792,7 +859,10 @@ "unique_source_id": { "name": "unique_source_id", "nullsNotDistinct": false, - "columns": ["source", "source_id"] + "columns": [ + "source", + "source_id" + ] } }, "policies": {}, @@ -848,7 +918,9 @@ "topics_slug_unique": { "name": "topics_slug_unique", "nullsNotDistinct": false, - "columns": ["slug"] + "columns": [ + "slug" + ] } }, "policies": {}, @@ -944,8 +1016,12 @@ "name": "translations_term_id_terms_id_fk", "tableFrom": "translations", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -955,7 +1031,11 @@ "unique_translations": { "name": "unique_translations", "nullsNotDistinct": false, - "columns": ["term_id", "language_code", "text"] + "columns": [ + "term_id", + "language_code", + "text" + ] } }, "policies": {}, @@ -1032,7 +1112,9 @@ "user_email_unique": { "name": "user_email_unique", "nullsNotDistinct": false, - "columns": ["email"] + "columns": [ + "email" + ] } }, "policies": {}, @@ -1113,5 +1195,9 @@ "roles": {}, "policies": {}, "views": {}, - "_meta": { "columns": {}, "schemas": {}, "tables": {} } -} + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/0010_snapshot.json b/packages/db/drizzle/meta/0010_snapshot.json index 0f0603f..720a585 100644 --- a/packages/db/drizzle/meta/0010_snapshot.json +++ b/packages/db/drizzle/meta/0010_snapshot.json @@ -110,8 +110,12 @@ "name": "account_user_id_user_id_fk", "tableFrom": "account", "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -145,8 +149,12 @@ "name": "deck_terms_deck_id_decks_id_fk", "tableFrom": "deck_terms", "tableTo": "decks", - "columnsFrom": ["deck_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "deck_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" }, @@ -154,8 +162,12 @@ "name": "deck_terms_term_id_terms_id_fk", "tableFrom": "deck_terms", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -163,7 +175,10 @@ "compositePrimaryKeys": { "deck_terms_deck_id_term_id_pk": { "name": "deck_terms_deck_id_term_id_pk", - "columns": ["deck_id", "term_id"] + "columns": [ + "deck_id", + "term_id" + ] } }, "uniqueConstraints": {}, @@ -250,7 +265,10 @@ "unique_deck_name": { "name": "unique_deck_name", "nullsNotDistinct": false, - "columns": ["name", "source_language"] + "columns": [ + "name", + "source_language" + ] } }, "policies": {}, @@ -318,8 +336,12 @@ "name": "lobbies_host_user_id_user_id_fk", "tableFrom": "lobbies", "tableTo": "user", - "columnsFrom": ["host_user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "host_user_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -329,7 +351,9 @@ "lobbies_code_unique": { "name": "lobbies_code_unique", "nullsNotDistinct": false, - "columns": ["code"] + "columns": [ + "code" + ] } }, "policies": {}, @@ -378,8 +402,12 @@ "name": "lobby_players_lobby_id_lobbies_id_fk", "tableFrom": "lobby_players", "tableTo": "lobbies", - "columnsFrom": ["lobby_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "lobby_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" }, @@ -387,8 +415,12 @@ "name": "lobby_players_user_id_user_id_fk", "tableFrom": "lobby_players", "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -396,7 +428,10 @@ "compositePrimaryKeys": { "lobby_players_lobby_id_user_id_pk": { "name": "lobby_players_lobby_id_user_id_pk", - "columns": ["lobby_id", "user_id"] + "columns": [ + "lobby_id", + "user_id" + ] } }, "uniqueConstraints": {}, @@ -480,8 +515,12 @@ "name": "session_user_id_user_id_fk", "tableFrom": "session", "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -491,7 +530,9 @@ "session_token_unique": { "name": "session_token_unique", "nullsNotDistinct": false, - "columns": ["token"] + "columns": [ + "token" + ] } }, "policies": {}, @@ -563,8 +604,12 @@ "name": "term_examples_term_id_terms_id_fk", "tableFrom": "term_examples", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -574,7 +619,11 @@ "unique_term_example": { "name": "unique_term_example", "nullsNotDistinct": false, - "columns": ["term_id", "language_code", "text"] + "columns": [ + "term_id", + "language_code", + "text" + ] } }, "policies": {}, @@ -635,8 +684,12 @@ "name": "term_glosses_term_id_terms_id_fk", "tableFrom": "term_glosses", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -646,7 +699,10 @@ "unique_term_gloss": { "name": "unique_term_gloss", "nullsNotDistinct": false, - "columns": ["term_id", "language_code"] + "columns": [ + "term_id", + "language_code" + ] } }, "policies": {}, @@ -681,8 +737,12 @@ "name": "term_topics_term_id_terms_id_fk", "tableFrom": "term_topics", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" }, @@ -690,8 +750,12 @@ "name": "term_topics_topic_id_topics_id_fk", "tableFrom": "term_topics", "tableTo": "topics", - "columnsFrom": ["topic_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "topic_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -699,7 +763,10 @@ "compositePrimaryKeys": { "term_topics_term_id_topic_id_pk": { "name": "term_topics_term_id_topic_id_pk", - "columns": ["term_id", "topic_id"] + "columns": [ + "term_id", + "topic_id" + ] } }, "uniqueConstraints": {}, @@ -773,7 +840,10 @@ "unique_source_id": { "name": "unique_source_id", "nullsNotDistinct": false, - "columns": ["source", "source_id"] + "columns": [ + "source", + "source_id" + ] } }, "policies": {}, @@ -829,7 +899,9 @@ "topics_slug_unique": { "name": "topics_slug_unique", "nullsNotDistinct": false, - "columns": ["slug"] + "columns": [ + "slug" + ] } }, "policies": {}, @@ -925,8 +997,12 @@ "name": "translations_term_id_terms_id_fk", "tableFrom": "translations", "tableTo": "terms", - "columnsFrom": ["term_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "no action" } @@ -936,7 +1012,11 @@ "unique_translations": { "name": "unique_translations", "nullsNotDistinct": false, - "columns": ["term_id", "language_code", "text"] + "columns": [ + "term_id", + "language_code", + "text" + ] } }, "policies": {}, @@ -1013,7 +1093,9 @@ "user_email_unique": { "name": "user_email_unique", "nullsNotDistinct": false, - "columns": ["email"] + "columns": [ + "email" + ] } }, "policies": {}, @@ -1094,5 +1176,9 @@ "roles": {}, "policies": {}, "views": {}, - "_meta": { "columns": {}, "schemas": {}, "tables": {} } -} + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 65dc2f0..512887d 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 af1fba6..1c40fbd 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 9e79e86..8b0de56 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": [], }