{"repo": "abanobmikaeel/coptic.io", "files": {"packages/data/scripts/llm-match-synaxarium.ts": {"fdd01f736097205869dbfc9a5c1616fd7c25c7ec": {"t": 1785745596.704331, "n": 7019, "h": "92713cb1b1176971", "c": "/**\n * Script to match English and Arabic synaxarium entries using DeepSeek LLM.\n *\n * Setup:\n * 1. Copy scripts/.env.example to scripts/.env\n * 2. Add your DEEPSEEK_API_KEY\n *\n * Run with: pnpm match:synaxarium\n */\n\nimport { existsSync, readFileSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\n// Load .env file if exists\nconst envPath = join(import.meta.dirname, '.env')\nif (existsSync(envPath)) {\n\tconst envContent = readFileSync(envPath, 'utf-8')\n\tfor (const line of envContent.split('\\n')) {\n\t\tconst trimmed = line.trim()\n\t\tif (trimmed && !trimmed.startsWith('#')) {\n\t\t\tconst [key, ...valueParts] = trimmed.split('=')\n\t\t\tconst value = valueParts.join('=')\n\t\t\tif (key && value) {\n\t\t\t\tprocess.env[key] = value\n\t\t\t}\n\t\t}\n\t}\n}\n\ninterface SynaxariumEntry {\n\turl?: string\n\tname: string\n\ttext?: string\n\tid?: string\n}\n\ntype SynaxariumData = Record\n\nconst DATA_DIR = join(import.meta.dirname, '../src')\nconst DEEPSEEK_API_URL = 'https://api.deepseek.com/v1/chat/completions'\n\nasync function callDeepSeek(prompt: string): Promise {\n\tconst apiKey = process.env.DEEPSEEK_API_KEY\n\tif (!apiKey) {\n\t\tthrow new Error('DEEPSEEK_API_KEY environment variable is required')\n\t}\n\n\tconst response = await fetch(DEEPSEEK_API_URL, {\n\t\tmethod: 'POST',\n\t\theaders: {\n\t\t\t'Content-Type': 'application/json',\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t},\n\t\tbody: JSON.stringify({\n\t\t\tmodel: 'deepseek-chat',\n\t\t\tmessages: [\n\t\t\t\t{\n\t\t\t\t\trole: 'system',\n\t\t\t\t\tcontent: `You are an expert in Coptic Orthodox saints and Arabic-English transliteration.\nYour task is to match English saint names with their Arabic equivalents.\nRespond ONLY with a JSON array of matches, no explanation.\nFormat: [{\"en_idx\": 0, \"ar_idx\": 2}, {\"en_idx\": 1, \"ar_idx\": 0}, ...]\nIf an entry has no match, omit it from the array.`,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\trole: 'user',\n\t\t\t\t\tcontent: prompt,\n\t\t\t\t},\n\t\t\t],\n\t\t\ttemperature: 0.1,\n\t\t}),\n\t})\n\n\tif (!response.ok) {\n\t\tconst error = await response.text()\n\t\tthrow new Error(`DeepSeek API error: ${response.status} - ${error}`)\n\t}\n\n\tconst data = await response.json()\n\treturn data.choices[0].message.content\n}\n\nfunction buildPrompt(\n\tdateKey: string,\n\tenEntries: SynaxariumEntry[],\n\tarEntries: SynaxariumEntry[],\n): string {\n\tconst enList = enEntries.map((e, i) => `${i}: ${e.name}`).join('\\n')\n\tconst arList = arEntries.map((e, i) => `${i}: ${e.name}`).join('\\n')\n\n\treturn `Match these English Coptic saint commemorations with their Arabic equivalents for ${dateKey}.\n\nENGLISH ENTRIES:\n${enList}\n\nARABIC ENTRIES:\n${arList}\n\nReturn JSON array of index matches: [{\"en_idx\": 0, \"ar_idx\": 1}, ...]`\n}\n\ninterface Match {\n\ten_idx: number\n\tar_idx: number\n}\n\nfunction parseMatches(response: string): Match[] {\n\ttry {\n\t\t// Extract JSON from response (in case there's extra text)\n\t\tconst jsonMatch = response.match(/\\[[\\s\\S]*\\]/)\n\t\tif (!jsonMatch) return []\n\t\treturn JSON.parse(jsonMatch[0])\n\t} catch {\n\t\tconsole.error('Failed to parse response:', response)\n\t\treturn []\n\t}\n}\n\nasync function matchEntriesForDate(\n\tdateKey: string,\n\tenEntries: SynaxariumEntry[],\n\tarEntries: SynaxariumEntry[],\n): Promise> {\n\tconst prompt = buildPrompt(dateKey, enEntries, arEntries)\n\n\ttry {\n\t\tconst response = await callDeepSeek(prompt)\n\t\tconst matches = parseMatches(response)\n\n\t\tconst matchMap = new Map()\n\t\tfor (const match of matches) {\n\t\t\tif (\n\t\t\t\tmatch.en_idx >= 0 &&\n\t\t\t\tmatch.en_idx < enEntries.length &&\n\t\t\t\tmatch.ar_idx >= 0 &&\n\t\t\t\tmatch.ar_idx < arEntries.length\n\t\t\t) {\n\t\t\t\tmatchMap.set(match.en_idx, match.ar_idx)\n\t\t\t}\n\t\t}\n\t\treturn matchMap\n\t} catch (error) {\n\t\tconsole.error(`Error matching ${dateKey}:`, error)\n\t\treturn new Map()\n\t}\n}\n\nfunction generateSemanticId(dateKey: string, name: string): string {\n\tconst slug = name\n\t\t.toLowerCase()\n\t\t.replace(/['']/g, '')\n\t\t.replace(/st\\.\\s*/g, 'st-')\n\t\t.replace(/[^\\w\\s-]/g, '')\n\t\t.replace(/\\s+/g, '-')\n\t\t.replace(/-+/g, '-')\n\t\t.replace(/^-|-$/g, '')\n\t\t.slice(0, 50)\n\n\treturn `${dateKey.toLowerCase().replace(/\\s+/g, '-')}-${slug}`\n}\n\nasync function main() {\n\tconst enPath = join(DATA_DIR, 'en/synaxarium/canonical.json')\n\tconst arPath = join(DATA_DIR, 'ar/synaxarium/canonical.json')\n\n\tconst enData: SynaxariumData = JSON.parse(readFileSync(enPath, 'utf-8'))\n\tconst arData: SynaxariumData = JSON.parse(readFileSync(arPath, 'utf-8'))\n\n\tconst allDateKeys = [...new Set([...Object.keys(enData), ...Object.keys(arData)])]\n\n\tconst log = (msg: string) => {\n\t\tprocess.stdout.write(`${msg}\\n`)\n\t}\n\n\tlog(`Processing ${allDateKeys.length} dates...\\n`)\n\n\tlet totalMatched = 0\n\tlet totalEn = 0\n\tlet totalAr = 0\n\n\t// Process in batches to avoid rate limits\n\tconst batchSize = 5\n\tfor (let i = 0; i < allDateKeys.length; i += batchSize) {\n\t\tconst batch = allDateKeys.slice(i, i + batchSize)\n\n\t\tawait Promise.all(\n\t\t\tbatch.map(async (dateKey) => {\n\t\t\t\tconst enEntries = enData[dateKey] || []\n\t\t\t\tconst arEntries = arData[dateKey] || []\n\n\t\t\t\ttotalEn += enEntries.length\n\t\t\t\ttotalAr += arEntries.length\n\n\t\t\t\tif (enEntries.length === 0 || arEntries.length === 0) {\n\t\t\t\t\t// No matching possible, assign unique IDs\n\t\t\t\t\tfor (let j = 0; j < enEntries.length; j++) {\n\t\t\t\t\t\tenEntries[j].id = generateSemanticId(dateKey, enEntries[j].name)\n\t\t\t\t\t}\n\t\t\t\t\tfor (let j = 0; j < arEntries.length; j++) {\n\t\t\t\t\t\tarEntries[j].id = `${dateKey.toLowerCase().replace(/\\s+/g, '-')}-ar-${j + 1}`\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tlog(`Matching: ${dateKey} (${enEntries.length} EN, ${arEntries.length} AR)`)\n\n\t\t\t\tconst matches = await matchEntriesForDate(dateKey, enEntries, arEntries)\n\t\t\t\tconst usedArIndices = new Set()\n\n\t\t\t\t// Assign IDs to matched entries\n\t\t\t\tfor (const [enIdx, arIdx] of matches) {\n\t\t\t\t\tconst semanticId = generateSemanticId(dateKey, enEntries[enIdx].name)\n\t\t\t\t\tenEntries[enIdx].id = semanticId\n\t\t\t\t\tarEntries[arIdx].id = semanticId\n\t\t\t\t\tusedArIndices.add(arIdx)\n\t\t\t\t\ttotalMatched++\n\t\t\t\t}\n\n\t\t\t\t// Assign unique IDs to unmatched English entries\n\t\t\t\tfor (let j = 0; j < enEntries.length; j++) {\n\t\t\t\t\tif (!enEntries[j].id) {\n\t\t\t\t\t\tenEntries[j].id = generateSemanticId(dateKey, enEntries[j].name)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Assign unique IDs to unmatched Arabic entries\n\t\t\t\tfor (let j = 0; j < arEntries.length; j++) {\n\t\t\t\t\tif (!arEntries[j].id) {\n\t\t\t\t\t\tarEntries[j].id = `${dateKey.toLowerCase().replace(/\\s+/g, '-')}-ar-${j + 1}`\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tlog(` ✓ Matched ${matches.size}/${Math.min(enEntries.length, arEntries.length)} entries`)\n\t\t\t}),\n\t\t)\n\n\t\t// Small delay between batches to respect rate limits\n\t\tif (i + batchSize < allDateKeys.length) {\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, 1000))\n\t\t}\n\t}\n\n\t// Write updated data\n\twriteFileSync(enPath, JSON.stringify(enData, null, '\\t'))\n\twriteFileSync(arPath, JSON.stringify(arData, null, '\\t'))\n\n\tlog(`\\n${'='.repeat(50)}`)\n\tlog('COMPLETE!')\n\tlog(`Total English entries: ${totalEn}`)\n\tlog(`Total Arabic entries: ${totalAr}`)\n\tlog(`Total matched: ${totalMatched}`)\n\tlog(`Match rate: ${((totalMatched / Math.min(totalEn, totalAr)) * 100).toFixed(1)}%`)\n}\n\nmain().catch(console.error)\n"}, "a71ec5e01efc9db4edac74e7585add4f2576553a": {"t": 1785754621.6405222, "n": 7019, "h": "92713cb1b1176971", "c": "/**\n * Script to match English and Arabic synaxarium entries using DeepSeek LLM.\n *\n * Setup:\n * 1. Copy scripts/.env.example to scripts/.env\n * 2. Add your DEEPSEEK_API_KEY\n *\n * Run with: pnpm match:synaxarium\n */\n\nimport { existsSync, readFileSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\n// Load .env file if exists\nconst envPath = join(import.meta.dirname, '.env')\nif (existsSync(envPath)) {\n\tconst envContent = readFileSync(envPath, 'utf-8')\n\tfor (const line of envContent.split('\\n')) {\n\t\tconst trimmed = line.trim()\n\t\tif (trimmed && !trimmed.startsWith('#')) {\n\t\t\tconst [key, ...valueParts] = trimmed.split('=')\n\t\t\tconst value = valueParts.join('=')\n\t\t\tif (key && value) {\n\t\t\t\tprocess.env[key] = value\n\t\t\t}\n\t\t}\n\t}\n}\n\ninterface SynaxariumEntry {\n\turl?: string\n\tname: string\n\ttext?: string\n\tid?: string\n}\n\ntype SynaxariumData = Record\n\nconst DATA_DIR = join(import.meta.dirname, '../src')\nconst DEEPSEEK_API_URL = 'https://api.deepseek.com/v1/chat/completions'\n\nasync function callDeepSeek(prompt: string): Promise {\n\tconst apiKey = process.env.DEEPSEEK_API_KEY\n\tif (!apiKey) {\n\t\tthrow new Error('DEEPSEEK_API_KEY environment variable is required')\n\t}\n\n\tconst response = await fetch(DEEPSEEK_API_URL, {\n\t\tmethod: 'POST',\n\t\theaders: {\n\t\t\t'Content-Type': 'application/json',\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t},\n\t\tbody: JSON.stringify({\n\t\t\tmodel: 'deepseek-chat',\n\t\t\tmessages: [\n\t\t\t\t{\n\t\t\t\t\trole: 'system',\n\t\t\t\t\tcontent: `You are an expert in Coptic Orthodox saints and Arabic-English transliteration.\nYour task is to match English saint names with their Arabic equivalents.\nRespond ONLY with a JSON array of matches, no explanation.\nFormat: [{\"en_idx\": 0, \"ar_idx\": 2}, {\"en_idx\": 1, \"ar_idx\": 0}, ...]\nIf an entry has no match, omit it from the array.`,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\trole: 'user',\n\t\t\t\t\tcontent: prompt,\n\t\t\t\t},\n\t\t\t],\n\t\t\ttemperature: 0.1,\n\t\t}),\n\t})\n\n\tif (!response.ok) {\n\t\tconst error = await response.text()\n\t\tthrow new Error(`DeepSeek API error: ${response.status} - ${error}`)\n\t}\n\n\tconst data = await response.json()\n\treturn data.choices[0].message.content\n}\n\nfunction buildPrompt(\n\tdateKey: string,\n\tenEntries: SynaxariumEntry[],\n\tarEntries: SynaxariumEntry[],\n): string {\n\tconst enList = enEntries.map((e, i) => `${i}: ${e.name}`).join('\\n')\n\tconst arList = arEntries.map((e, i) => `${i}: ${e.name}`).join('\\n')\n\n\treturn `Match these English Coptic saint commemorations with their Arabic equivalents for ${dateKey}.\n\nENGLISH ENTRIES:\n${enList}\n\nARABIC ENTRIES:\n${arList}\n\nReturn JSON array of index matches: [{\"en_idx\": 0, \"ar_idx\": 1}, ...]`\n}\n\ninterface Match {\n\ten_idx: number\n\tar_idx: number\n}\n\nfunction parseMatches(response: string): Match[] {\n\ttry {\n\t\t// Extract JSON from response (in case there's extra text)\n\t\tconst jsonMatch = response.match(/\\[[\\s\\S]*\\]/)\n\t\tif (!jsonMatch) return []\n\t\treturn JSON.parse(jsonMatch[0])\n\t} catch {\n\t\tconsole.error('Failed to parse response:', response)\n\t\treturn []\n\t}\n}\n\nasync function matchEntriesForDate(\n\tdateKey: string,\n\tenEntries: SynaxariumEntry[],\n\tarEntries: SynaxariumEntry[],\n): Promise> {\n\tconst prompt = buildPrompt(dateKey, enEntries, arEntries)\n\n\ttry {\n\t\tconst response = await callDeepSeek(prompt)\n\t\tconst matches = parseMatches(response)\n\n\t\tconst matchMap = new Map()\n\t\tfor (const match of matches) {\n\t\t\tif (\n\t\t\t\tmatch.en_idx >= 0 &&\n\t\t\t\tmatch.en_idx < enEntries.length &&\n\t\t\t\tmatch.ar_idx >= 0 &&\n\t\t\t\tmatch.ar_idx < arEntries.length\n\t\t\t) {\n\t\t\t\tmatchMap.set(match.en_idx, match.ar_idx)\n\t\t\t}\n\t\t}\n\t\treturn matchMap\n\t} catch (error) {\n\t\tconsole.error(`Error matching ${dateKey}:`, error)\n\t\treturn new Map()\n\t}\n}\n\nfunction generateSemanticId(dateKey: string, name: string): string {\n\tconst slug = name\n\t\t.toLowerCase()\n\t\t.replace(/['']/g, '')\n\t\t.replace(/st\\.\\s*/g, 'st-')\n\t\t.replace(/[^\\w\\s-]/g, '')\n\t\t.replace(/\\s+/g, '-')\n\t\t.replace(/-+/g, '-')\n\t\t.replace(/^-|-$/g, '')\n\t\t.slice(0, 50)\n\n\treturn `${dateKey.toLowerCase().replace(/\\s+/g, '-')}-${slug}`\n}\n\nasync function main() {\n\tconst enPath = join(DATA_DIR, 'en/synaxarium/canonical.json')\n\tconst arPath = join(DATA_DIR, 'ar/synaxarium/canonical.json')\n\n\tconst enData: SynaxariumData = JSON.parse(readFileSync(enPath, 'utf-8'))\n\tconst arData: SynaxariumData = JSON.parse(readFileSync(arPath, 'utf-8'))\n\n\tconst allDateKeys = [...new Set([...Object.keys(enData), ...Object.keys(arData)])]\n\n\tconst log = (msg: string) => {\n\t\tprocess.stdout.write(`${msg}\\n`)\n\t}\n\n\tlog(`Processing ${allDateKeys.length} dates...\\n`)\n\n\tlet totalMatched = 0\n\tlet totalEn = 0\n\tlet totalAr = 0\n\n\t// Process in batches to avoid rate limits\n\tconst batchSize = 5\n\tfor (let i = 0; i < allDateKeys.length; i += batchSize) {\n\t\tconst batch = allDateKeys.slice(i, i + batchSize)\n\n\t\tawait Promise.all(\n\t\t\tbatch.map(async (dateKey) => {\n\t\t\t\tconst enEntries = enData[dateKey] || []\n\t\t\t\tconst arEntries = arData[dateKey] || []\n\n\t\t\t\ttotalEn += enEntries.length\n\t\t\t\ttotalAr += arEntries.length\n\n\t\t\t\tif (enEntries.length === 0 || arEntries.length === 0) {\n\t\t\t\t\t// No matching possible, assign unique IDs\n\t\t\t\t\tfor (let j = 0; j < enEntries.length; j++) {\n\t\t\t\t\t\tenEntries[j].id = generateSemanticId(dateKey, enEntries[j].name)\n\t\t\t\t\t}\n\t\t\t\t\tfor (let j = 0; j < arEntries.length; j++) {\n\t\t\t\t\t\tarEntries[j].id = `${dateKey.toLowerCase().replace(/\\s+/g, '-')}-ar-${j + 1}`\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tlog(`Matching: ${dateKey} (${enEntries.length} EN, ${arEntries.length} AR)`)\n\n\t\t\t\tconst matches = await matchEntriesForDate(dateKey, enEntries, arEntries)\n\t\t\t\tconst usedArIndices = new Set()\n\n\t\t\t\t// Assign IDs to matched entries\n\t\t\t\tfor (const [enIdx, arIdx] of matches) {\n\t\t\t\t\tconst semanticId = generateSemanticId(dateKey, enEntries[enIdx].name)\n\t\t\t\t\tenEntries[enIdx].id = semanticId\n\t\t\t\t\tarEntries[arIdx].id = semanticId\n\t\t\t\t\tusedArIndices.add(arIdx)\n\t\t\t\t\ttotalMatched++\n\t\t\t\t}\n\n\t\t\t\t// Assign unique IDs to unmatched English entries\n\t\t\t\tfor (let j = 0; j < enEntries.length; j++) {\n\t\t\t\t\tif (!enEntries[j].id) {\n\t\t\t\t\t\tenEntries[j].id = generateSemanticId(dateKey, enEntries[j].name)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Assign unique IDs to unmatched Arabic entries\n\t\t\t\tfor (let j = 0; j < arEntries.length; j++) {\n\t\t\t\t\tif (!arEntries[j].id) {\n\t\t\t\t\t\tarEntries[j].id = `${dateKey.toLowerCase().replace(/\\s+/g, '-')}-ar-${j + 1}`\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tlog(` ✓ Matched ${matches.size}/${Math.min(enEntries.length, arEntries.length)} entries`)\n\t\t\t}),\n\t\t)\n\n\t\t// Small delay between batches to respect rate limits\n\t\tif (i + batchSize < allDateKeys.length) {\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, 1000))\n\t\t}\n\t}\n\n\t// Write updated data\n\twriteFileSync(enPath, JSON.stringify(enData, null, '\\t'))\n\twriteFileSync(arPath, JSON.stringify(arData, null, '\\t'))\n\n\tlog(`\\n${'='.repeat(50)}`)\n\tlog('COMPLETE!')\n\tlog(`Total English entries: ${totalEn}`)\n\tlog(`Total Arabic entries: ${totalAr}`)\n\tlog(`Total matched: ${totalMatched}`)\n\tlog(`Match rate: ${((totalMatched / Math.min(totalEn, totalAr)) * 100).toFixed(1)}%`)\n}\n\nmain().catch(console.error)\n"}, "89ca7982cfbd001022fc5bd817a9bf45a11f73bc": {"t": 1785754623.3113027, "n": 6945, "h": "ac5a4649bdcd6851", "c": "/**\n * Script to match English and Arabic synaxarium entries using DeepSeek LLM.\n *\n * Setup:\n * 1. Copy scripts/.env.example to scripts/.env\n * 2. Add your DEEPSEEK_API_KEY\n *\n * Run with: pnpm match:synaxarium\n */\n\nimport { readFileSync, writeFileSync, existsSync } from 'node:fs'\nimport { join } from 'node:path'\n\n// Load .env file if exists\nconst envPath = join(import.meta.dirname, '.env')\nif (existsSync(envPath)) {\n\tconst envContent = readFileSync(envPath, 'utf-8')\n\tfor (const line of envContent.split('\\n')) {\n\t\tconst trimmed = line.trim()\n\t\tif (trimmed && !trimmed.startsWith('#')) {\n\t\t\tconst [key, ...valueParts] = trimmed.split('=')\n\t\t\tconst value = valueParts.join('=')\n\t\t\tif (key && value) {\n\t\t\t\tprocess.env[key] = value\n\t\t\t}\n\t\t}\n\t}\n}\n\ninterface SynaxariumEntry {\n\turl?: string\n\tname: string\n\ttext?: string\n\tid?: string\n}\n\ntype SynaxariumData = Record\n\nconst DATA_DIR = join(import.meta.dirname, '../src')\nconst DEEPSEEK_API_URL = 'https://api.deepseek.com/v1/chat/completions'\n\nasync function callDeepSeek(prompt: string): Promise {\n\tconst apiKey = process.env.DEEPSEEK_API_KEY\n\tif (!apiKey) {\n\t\tthrow new Error('DEEPSEEK_API_KEY environment variable is required')\n\t}\n\n\tconst response = await fetch(DEEPSEEK_API_URL, {\n\t\tmethod: 'POST',\n\t\theaders: {\n\t\t\t'Content-Type': 'application/json',\n\t\t\t'Authorization': `Bearer ${apiKey}`,\n\t\t},\n\t\tbody: JSON.stringify({\n\t\t\tmodel: 'deepseek-chat',\n\t\t\tmessages: [\n\t\t\t\t{\n\t\t\t\t\trole: 'system',\n\t\t\t\t\tcontent: `You are an expert in Coptic Orthodox saints and Arabic-English transliteration.\nYour task is to match English saint names with their Arabic equivalents.\nRespond ONLY with a JSON array of matches, no explanation.\nFormat: [{\"en_idx\": 0, \"ar_idx\": 2}, {\"en_idx\": 1, \"ar_idx\": 0}, ...]\nIf an entry has no match, omit it from the array.`\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\trole: 'user',\n\t\t\t\t\tcontent: prompt\n\t\t\t\t}\n\t\t\t],\n\t\t\ttemperature: 0.1,\n\t\t}),\n\t})\n\n\tif (!response.ok) {\n\t\tconst error = await response.text()\n\t\tthrow new Error(`DeepSeek API error: ${response.status} - ${error}`)\n\t}\n\n\tconst data = await response.json()\n\treturn data.choices[0].message.content\n}\n\nfunction buildPrompt(dateKey: string, enEntries: SynaxariumEntry[], arEntries: SynaxariumEntry[]): string {\n\tconst enList = enEntries.map((e, i) => `${i}: ${e.name}`).join('\\n')\n\tconst arList = arEntries.map((e, i) => `${i}: ${e.name}`).join('\\n')\n\n\treturn `Match these English Coptic saint commemorations with their Arabic equivalents for ${dateKey}.\n\nENGLISH ENTRIES:\n${enList}\n\nARABIC ENTRIES:\n${arList}\n\nReturn JSON array of index matches: [{\"en_idx\": 0, \"ar_idx\": 1}, ...]`\n}\n\ninterface Match {\n\ten_idx: number\n\tar_idx: number\n}\n\nfunction parseMatches(response: string): Match[] {\n\ttry {\n\t\t// Extract JSON from response (in case there's extra text)\n\t\tconst jsonMatch = response.match(/\\[[\\s\\S]*\\]/)\n\t\tif (!jsonMatch) return []\n\t\treturn JSON.parse(jsonMatch[0])\n\t} catch {\n\t\tconsole.error('Failed to parse response:', response)\n\t\treturn []\n\t}\n}\n\nasync function matchEntriesForDate(\n\tdateKey: string,\n\tenEntries: SynaxariumEntry[],\n\tarEntries: SynaxariumEntry[]\n): Promise> {\n\tconst prompt = buildPrompt(dateKey, enEntries, arEntries)\n\n\ttry {\n\t\tconst response = await callDeepSeek(prompt)\n\t\tconst matches = parseMatches(response)\n\n\t\tconst matchMap = new Map()\n\t\tfor (const match of matches) {\n\t\t\tif (match.en_idx >= 0 && match.en_idx < enEntries.length &&\n\t\t\t\tmatch.ar_idx >= 0 && match.ar_idx < arEntries.length) {\n\t\t\t\tmatchMap.set(match.en_idx, match.ar_idx)\n\t\t\t}\n\t\t}\n\t\treturn matchMap\n\t} catch (error) {\n\t\tconsole.error(`Error matching ${dateKey}:`, error)\n\t\treturn new Map()\n\t}\n}\n\nfunction generateSemanticId(dateKey: string, name: string): string {\n\tconst slug = name\n\t\t.toLowerCase()\n\t\t.replace(/['']/g, '')\n\t\t.replace(/st\\.\\s*/g, 'st-')\n\t\t.replace(/[^\\w\\s-]/g, '')\n\t\t.replace(/\\s+/g, '-')\n\t\t.replace(/-+/g, '-')\n\t\t.replace(/^-|-$/g, '')\n\t\t.slice(0, 50)\n\n\treturn `${dateKey.toLowerCase().replace(/\\s+/g, '-')}-${slug}`\n}\n\nasync function main() {\n\tconst enPath = join(DATA_DIR, 'en/synaxarium/canonical.json')\n\tconst arPath = join(DATA_DIR, 'ar/synaxarium/canonical.json')\n\n\tconst enData: SynaxariumData = JSON.parse(readFileSync(enPath, 'utf-8'))\n\tconst arData: SynaxariumData = JSON.parse(readFileSync(arPath, 'utf-8'))\n\n\tconst allDateKeys = [...new Set([...Object.keys(enData), ...Object.keys(arData)])]\n\n\tconst log = (msg: string) => {\n\t\tprocess.stdout.write(msg + '\\n')\n\t}\n\n\tlog(`Processing ${allDateKeys.length} dates...\\n`)\n\n\tlet totalMatched = 0\n\tlet totalEn = 0\n\tlet totalAr = 0\n\n\t// Process in batches to avoid rate limits\n\tconst batchSize = 5\n\tfor (let i = 0; i < allDateKeys.length; i += batchSize) {\n\t\tconst batch = allDateKeys.slice(i, i + batchSize)\n\n\t\tawait Promise.all(batch.map(async (dateKey) => {\n\t\t\tconst enEntries = enData[dateKey] || []\n\t\t\tconst arEntries = arData[dateKey] || []\n\n\t\t\ttotalEn += enEntries.length\n\t\t\ttotalAr += arEntries.length\n\n\t\t\tif (enEntries.length === 0 || arEntries.length === 0) {\n\t\t\t\t// No matching possible, assign unique IDs\n\t\t\t\tfor (let j = 0; j < enEntries.length; j++) {\n\t\t\t\t\tenEntries[j].id = generateSemanticId(dateKey, enEntries[j].name)\n\t\t\t\t}\n\t\t\t\tfor (let j = 0; j < arEntries.length; j++) {\n\t\t\t\t\tarEntries[j].id = `${dateKey.toLowerCase().replace(/\\s+/g, '-')}-ar-${j + 1}`\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tlog(`Matching: ${dateKey} (${enEntries.length} EN, ${arEntries.length} AR)`)\n\n\t\t\tconst matches = await matchEntriesForDate(dateKey, enEntries, arEntries)\n\t\t\tconst usedArIndices = new Set()\n\n\t\t\t// Assign IDs to matched entries\n\t\t\tfor (const [enIdx, arIdx] of matches) {\n\t\t\t\tconst semanticId = generateSemanticId(dateKey, enEntries[enIdx].name)\n\t\t\t\tenEntries[enIdx].id = semanticId\n\t\t\t\tarEntries[arIdx].id = semanticId\n\t\t\t\tusedArIndices.add(arIdx)\n\t\t\t\ttotalMatched++\n\t\t\t}\n\n\t\t\t// Assign unique IDs to unmatched English entries\n\t\t\tfor (let j = 0; j < enEntries.length; j++) {\n\t\t\t\tif (!enEntries[j].id) {\n\t\t\t\t\tenEntries[j].id = generateSemanticId(dateKey, enEntries[j].name)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Assign unique IDs to unmatched Arabic entries\n\t\t\tfor (let j = 0; j < arEntries.length; j++) {\n\t\t\t\tif (!arEntries[j].id) {\n\t\t\t\t\tarEntries[j].id = `${dateKey.toLowerCase().replace(/\\s+/g, '-')}-ar-${j + 1}`\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlog(` ✓ Matched ${matches.size}/${Math.min(enEntries.length, arEntries.length)} entries`)\n\t\t}))\n\n\t\t// Small delay between batches to respect rate limits\n\t\tif (i + batchSize < allDateKeys.length) {\n\t\t\tawait new Promise(resolve => setTimeout(resolve, 1000))\n\t\t}\n\t}\n\n\t// Write updated data\n\twriteFileSync(enPath, JSON.stringify(enData, null, '\\t'))\n\twriteFileSync(arPath, JSON.stringify(arData, null, '\\t'))\n\n\tlog('\\n' + '='.repeat(50))\n\tlog('COMPLETE!')\n\tlog(`Total English entries: ${totalEn}`)\n\tlog(`Total Arabic entries: ${totalAr}`)\n\tlog(`Total matched: ${totalMatched}`)\n\tlog(`Match rate: ${((totalMatched / Math.min(totalEn, totalAr)) * 100).toFixed(1)}%`)\n}\n\nmain().catch(console.error)\n"}}}}