/** * Migrates the 13 Factfulness quiz cards from type=basic (A/B/C embedded in * front text) to type=multiple-choice with proper field structure. * Also sets the deck to visibility=public. * * Run from repo root: bun --cwd apps/api run ../../scripts/migrate-factfulness-to-mc.ts */ import postgres from 'postgres'; const DECK_ID = '01KR96NH892HBP1TV5M3XJTB39'; const DB_URL = 'postgresql://cards:cards@localhost:5435/cards'; const sql = postgres(DB_URL); interface CardRow { id: string; fields: { front?: string; back?: string }; } function parseQuizCard(front: string, back: string): { question: string; answer: string; distractorPool: string; explanation: string; } | null { // front: "\n\nA: opt1\nB: opt2\nC: opt3" const splitIdx = front.indexOf('\n\nA:'); if (splitIdx === -1) return null; const question = front.slice(0, splitIdx).trim(); const optionsBlock = front.slice(splitIdx + 2).trim(); // starts with "A: …" const options: Record = {}; for (const line of optionsBlock.split('\n')) { const m = line.match(/^([A-D]):\s*(.+)$/); if (m) options[m[1]] = m[2].trim(); } // back: "LETTER — answer_text\n\nexplanation" const backMatch = back.match(/^([A-D])\s*[—–-]+\s*(.+?)(?:\n\n([\s\S]*))?$/); if (!backMatch) return null; const correctLetter = backMatch[1]; const answer = options[correctLetter] ?? backMatch[2].trim(); const explanation = (backMatch[3] ?? '').trim(); const distractorPool = Object.entries(options) .filter(([l]) => l !== correctLetter) .map(([, v]) => v) .join('\n'); return { question, answer, distractorPool, explanation }; } async function run() { console.log('Fetching basic cards from Factfulness deck…\n'); const rows = await sql` SELECT id, fields FROM cards.cards WHERE deck_id = ${DECK_ID} AND type = 'basic' `; let migrated = 0; let skipped = 0; for (const row of rows) { const front = row.fields.front ?? ''; const back = row.fields.back ?? ''; if (!front.includes('\n\nA:')) { skipped++; continue; } const parsed = parseQuizCard(front, back); if (!parsed) { console.warn(` ⚠ Cannot parse ${row.id.slice(-8)}: "${front.slice(0, 60)}"`); skipped++; continue; } const { question, answer, distractorPool, explanation } = parsed; const newFields: Record = { front: question, answer }; if (distractorPool) newFields.distractor_pool = distractorPool; if (explanation) newFields.explanation = explanation; await sql` UPDATE cards.cards SET type = 'multiple-choice', fields = ${sql.json(newFields)} WHERE id = ${row.id} `; console.log(` ✓ ${row.id.slice(-8)} Q: "${question.slice(0, 55)}…"`); migrated++; } await sql` UPDATE cards.decks SET visibility = 'public' WHERE id = ${DECK_ID} `; console.log(`\n ✓ Deck → visibility=public`); console.log(`\nDone: ${migrated} migrated, ${skipped} basic cards kept as-is.`); await sql.end(); } run().catch((e) => { console.error(e); process.exit(1); });