/** * Enrich ru/lives bundles with Chetyi-Minei texts. * * Pipeline: * 1. Load scripts/output/minei.json (source lives by MM-DD, from convert-minei.ts). * 2. Load static/data/cu/menaion-bundle.json (authoritative date -> CId mapping). * 3. Load ru/lives bundles (names per CId; target for enrichment). * 4. For each date, fuzzy-match source life titles to candidate CIds' names. * 5. Fill life.text (only entries that currently lack it) + write report. * * Usage: * npx tsx scripts/enrich-lives.ts # dry run -> report only * npx tsx scripts/enrich-lives.ts --write # apply changes * npx tsx scripts/enrich-lives.ts --month 01 # limit to one month (report) * npx tsx scripts/enrich-lives.ts --write --month 01 */ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs'; import { join, dirname } from 'path'; const ROOT = join(new URL('.', import.meta.url).pathname, '..'); const MINEI_PATH = join(ROOT, 'scripts', 'output', 'minei.json'); const BULGAKOV_PATH = join(ROOT, 'scripts', 'output', 'bulgakov.json'); const MENAION_BUNDLE_PATH = join(ROOT, 'static', 'data', 'cu', 'menaion-bundle.json'); const LIVES_DIR = join(ROOT, 'static', 'data', 'ru', 'lives'); const REPORT_PATH = join(ROOT, 'scripts', 'output', 'lives-match-report.json'); const COPYRIGHT_MINEI = 'Четьи-Минеи свт. Димитрия Ростовского'; const COPYRIGHT_BULGAKOV = 'Протоиерей Сергий Булгаков. Настольная Книга.'; interface SourceLife { title: string; html: string; } interface Candidate { cid: string; name: string; file: string; hasLife: boolean; score: number; } interface ReportEntry { date: string; sourceTitle: string; source: 'minei' | 'bulgakov' | 'unknown'; matchedCid: string | null; matchedName: string | null; score: number; status: 'auto' | 'review' | 'unmatched' | 'already-filled' | 'no-name'; } const ROLE_WORDS = new Set([ 'святого', 'святой', 'святая', 'святых', 'святому', 'святою', 'святыя', 'свт', 'святитель', 'святителя', 'святителю', 'преподобного', 'преподобный', 'преподобной', 'преподобных', 'прп', 'прпп', 'мученика', 'мученик', 'мучеников', 'мученицы', 'мучениц', 'мученице', 'мч', 'мчч', 'мц', 'мцц', 'сщмч', 'сщмчч', 'прпмч', 'прпмц', 'прмч', 'блаженного', 'блаженный', 'блаженной', 'блаж', 'праведного', 'праведный', 'праведной', 'прав', 'пророка', 'пророк', 'пророков', 'апостола', 'апостол', 'апостолов', 'апостолы', 'ап', 'священномученика', 'священномучеников', 'отца', 'отец', 'отцов', 'нашего', 'наших', 'наш', 'его', 'ея', 'ей', 'их', 'житие', 'жития', 'страдание', 'страданиев', 'страдания', 'память', 'слово', 'богоносного', 'вселенского', 'собора', 'собор', 'иже', 'во', 'в', 'на', 'по', 'за', 'от', 'о', 'об', 'к', 'и', 'с', 'вмч', 'вмц', 'великомученика', 'великомученик', 'еп', 'епископа', 'епископ', 'архиеп', 'архиепископа', 'архиепископ', 'митр', 'митрополита', 'патриарха', 'патриарх', 'игумена', 'игумении', 'пресвитера', 'пресвитер', 'диакона', 'диакон', 'князя', 'князь', 'кн', 'царя', 'царь', 'императора', 'император', 'девы', 'дева', 'девица', 'сестры', 'сестер', 'брата', 'братий', 'чад', 'чада', 'сына', 'сын', 'воинов', 'отрока', 'отрок', 'младенцев', 'авраам', 'аврамия', 'ок', 'иже', 'св', 'блгв', 'блж', 'прмц', 'прав', 'прор', ]); /** * Manual overrides for cases the fuzzy matcher cannot resolve * (near-ties, token-subset collisions, name homonyms on the same date). * Key: `${date}|${distinctive lowercase substring of the source title}`. */ const MANUAL_FIXES: Record = { '01-02|серафима саровского': '010202', '01-04|семидесяти апостолов': '146', '01-06|слово святого иоанна златоустого на богоявление': '163', '01-07|десной руке': '1007', '01-08|илии пустынника': '175', '01-08|иулиана и василиссы': '174', '01-09|петра, епископа севастийского': '010905', '01-12|петра авесаламита': '202', '01-14|преподобного стефана': '219', '01-26|иосифа солунского': '31701', '01-30|ипполита, кенсорина': '339', '01-30|преподобного зинона': '341', '02-09|никифора': '1621', '02-11|память святого благоверного князя всеволода, нареченного во святом крещении гавриилом': '438', '02-11|память святой царицы феодоры': '431', '02-14|память преподобного марона': '1485', '02-14|память святого авраама, епископа каррийского': '448', '02-16|память святого маруфа епископа и святых мучеников, пострадавших в мартирополе': '021602', '02-21|память преподобного отца нашего тимофея, пустынника в символах': '484', '02-22|память преподобного афанасия исповедника': '302', '02-28|память святого священномученика нестора, епископа магидийского': '527', '02-29|память преподобного иоанна, нареченного варсонофия': '532', '03-05|прозванию огородника': '570', '03-06|страдание святого преподономученика конона и сына его конона, в иконии пострадавших': '582', '03-07|павла исповедника': '589', '03-09|память святого кесария': '601', '03-10|кодрата и прочих': '604', '03-15|память святых мучеников агапия, пуплия, тимолая, ромила, александра, александра, дионисия и дионисия': '646', '03-17|житие преподобного отца нашего макария колязинского': '1404', '03-18|житие преподобного отца нашего анина чудотворца': '66801', '03-20|память святого никиты исповедника': '682', '03-20|страдание святых мучениц александры и клавдии': '681', '03-21|память преподобного отца нашего иакова, епископа и исповедника': '685', '03-23|память святых мучеников филита и лидии': '696', '03-23|священномученика никона': '694', '03-24|память преподобного отца нашего иакова исповедника': '68501', '03-29|страдание преподобного отца нашего марка, епископа арефусийского': '729', '03-31|память преподобного аполлония': '746', '04-04|зосимы': '754', '04-04|страдание святой мученицы фервуфы': '780', '04-05|память преподобного отца нашего платона исповедника': '791', '04-12|житие преподобной матери нашей афанасии': '838', '04-13|страдание святого мученика крискента': '850', '04-14|страдание святых мучеников 1000 и азата': '84', '04-16|святой мученицы ирины': '2367', '04-21|мученика феодора': '909', '05-04|страдание святого мученика сильвана, епископа кесийского [1] и с ним сорока мучеников': '1990', '05-06|память святого варвара, бывшего прежде разбойником': '1003', '05-06|страдание святого мученика варвара воина и других с ним': '1002', '05-10|страдание святых мучеников алфия, филадельфа, киприана [1] и других, с ними пострадавших': '1031', '05-12|савина': '1045', '05-14|житие преподобного отца нашего серапиона синдонита': '1064', '05-17|память святого мученика солохона': '1089', '05-20|страдание святого мученика фалалея': '110801', '05-23|михаила исповедника': '1130', '05-24|страдание святого мученика серапиона и с ним каллиника, феодора и фавста': '1135', '05-27|перенесение честных мощей святителей киприана, фотия и ионы, киевских и всея россии чудотворцев': '1209', '05-27|ферапонта, белозерского': '2275', '05-29|память святой мученицы феодосии': '271401', '05-29|преподобномученицы феодосии': '1169', '06-04|житие преподобного отца нашего зосимы, епископа вавилонского': '1228', '06-05|память преподобного отца нашего феодора пустынника': '1239', '06-05|повесть о кончине блаженного константина, митрополита киевского': '06050200', '06-05|преставление преподобного отца нашего анувия исповедника и пустынножителя египетского': '123802', '06-06|страдание святых преподобномучениц архелаи, феклы и сусанны': '1249', '06-07|маркелла, папы': '1253', '06-08|перенесение мощей святого великомученика феодора стратилата': '220101', '06-10|житие святого отца нашего вассиана, епископа лодийского': '1278', '06-13|житие святого отца нашего трифиллия, епископа левкусийского': '1296', '06-20|житие во святых отца наш его левкия исповедника': '1355', '07-02|память святого отца нашего ювеналия, патриарха иерусалимского': '4140', '07-02|сказание о положении честной ризы пречистой девы богородицы во влахернской церкви': '4138', '07-06|страдание святой мученицы лукии девицы и с нею рикса и других': '4169', '07-09|память преподобного коприя': '4201', '07-09|память преподобного патермуфия': '4201', '07-10|память преподобного отца нашего антония печерского': '21801', '07-11|воспоминание о чуде святой великомученицы евфимии всехвальной': '9479', '07-11|преставление блаженной княгини ольги, во святом крещении елены': '2634', '07-12|страдание святой мученицы голиндухи': '4224', '07-14|память преподобного еллия': '4238', '07-25|житие святой олимпиады диакониссы': '4301', '07-27|память преподобной игумении анфисы': '4316', '07-28|память святого мученика акакия': '4325', '08-02|перенесение мощей святого первомученика и архидиакона стефана': '2051', '08-07|преставление преподобного ора черноризца': '4393', '08-14|о создании церкви честного успения пресвятой богородицы в киево-печерской обители': '444411', '08-22|тот же день': '4494', '08-26|сретения владимирской иконы': '281201', '08-27|пимена (палестинского)': '4533', '08-30|житие святого благоверного князя александра невского': '114', '08-30|память преподобного отца нашего фантина': '4551', '09-03|житие и страдание святого священномученика анфима, епископа никомидийского, и с ним многих': '1439', '09-04|память святой мученицы ермионии': '1449', '09-04|священномученика вавилы': '1446', '09-05|память святых мучеников урвана, феодора и медимна, и многих с ними': '1458', '09-05|убиение святого князя глеба': '51801', '09-06|страдание святого мученика евдоксия': '1462', '09-09|воспоминание святого третьего вселенского собора в ефесе': '1495', '09-09|память преподобного иосифа волоколамского': '1055', '09-09|память преподобного феофана, постника и исповедника': '1493', '09-10|память святых апостолов апеллия, луки и климента': '1501', '09-13|память преподобного петра в атрое': '152502', '09-15|память святого филофея, пресвитера и чудотворца': '1534', '09-19|память святого благоверного князя феодора, смоленского и ярославского чудотворца, и чад его давида и константина': '219701', '09-20|житие и страдание святого великомученика евстафия плакиды, его супруги и чад': '156901', '09-20|страдание святых мучеников михаила, князя черниговского и боярина его феодора, от нечестивого батыя пострадавших': '155001', '09-21|житие святителя димитрия, митрополита ростовского, чудотворца': '60102', '09-21|память святых епископов исакия и мелетия': '09210100', '09-22|вертоградар': '1582', '09-22|житие святого петра, прежде бывшего мытарем': '1583', '09-22|ионы пресвитера': '1581', '09-22|синопского': '232101', '09-23|житие преподобной поликсении': '1588', '09-24|никандра псковского': '1595', '09-25|житие преподобной матери нашей евфросинии': '160601', '09-27|житие и страдание святого мученика каллистрата': '1611', '09-30|житие и страдание святого священномученика григория, епископа великой армении, и с ним тридцати семи дев': '1632', '09-30|память святого михаила, митрополита киевского и всея россии чудотворца': '1553', '10-04|память святого мученика давикта и дочери его каллисфении': '10040200', '10-07|житие преподобного отца нашего сергия нуромского, чудотворца вологодского и обнорского': '1971', '10-08|пелагии девы': '10080000', '10-08|преподобной матери нашей пелагии': '2641', '10-09|житие святого праведного авраама': '1697', '10-09|память святой поплии': '1699', '10-11|воспоминание седьмого вселенского собора': '9914', '10-11|житие святого апостола филиппа': '2292', '10-16|житие и страдание святого мученика лонгина сотника': '136801', '10-17|память святого праведного лазаря четверодневного': '1760', '10-22|память святого равноапостольного аверкия, епископа иерапольского': '4571', '10-23|боровицкого': '868', '10-26|страдание и чудеса святого славного великомученика димитрия': '60401', '10-27|память святых мучениц капетолины и еротииды': '6661', '10-28|память святых мучеников терентия и неониллы': '10280000', '10-28|страдание святой великомученицы параскевы': '14172', '11-05|житие и страдание святых преподобномучеников галактиона и епистимы': '248801', '11-10|страдание священномученика милия': '2516', '11-22|михаила воина': '2585', '11-23|память святого мученика сисиния': '3214', '11-23|память святого мученика феодора': '3214', '11-25|житие и страдание святого священномученика петра, архиепископа александрийского': '6969', '11-26|воспоминание освящения храма великомученика георгия в киеве': '49702', '11-27|память святого палладия, епископа еленопольского': '11270000', '11-28|память святого мученика иринарха': '3242', '11-29|память святого мученика парамона': '3246', '12-04|житие и страдание святой великомученицы варвары': '3061', '12-07|память преподобного павла повинника': '3080', '12-29|житие преподобных марка пещерника и феофила плачливого': '146402', '12-29|память преподобного фаддея исповедника': '2156', '12-30|память преподобного зотика сиротопитателя': '3199', '12-30|цареградской': '3203', '02-02|сказание о сретении': '373', '02-04|епископа арвильскаго': '388', '02-05|феодулии': '393', '02-07|тысячи и трех': '411', '02-15|онисима, одного из лика': '450', '02-22|варадата': '493', '02-22|фалассия': '493', '03-06|42 мучеников': '57901', '03-09|сорока мучеников, в армянской севастии': '1238', '03-10|переименованной': '607', '03-14|евcхимона': '642', '03-19|хрисанфа и дарии': '673', '03-20|фотины': '680', '03-22|дросиды': '690', '03-24|воспоминание о чуде': '822', '03-24|артемона': '6673', '03-26|26-ти мучеников готских': '712', '03-28|стефана исповедника': '723', '03-28|ионы и варахисия': '726', '04-03|никиты исповедника': '767', '04-29|кизике': '09951', '05-04|алвиана': '992', '05-10|taиcии': '2685', '05-13|георгия исповедника': '106001', '05-15|евфросина, псковского': '74701', '05-24|мелетия стратилата': '1135', '06-03|лукиана': '1220', '06-07|маркеллина': '1252', '06-26|лидской': '1399', '07-08|казанской': '2865', '07-25|евпраксии': '4301', '07-30|полихрония': '4352', '07-30|валентина': '336', '08-05|фавия': '08051300', '08-08|явления ее пречестной': '4404', '08-09|иулиана, маркиана': '4410', '08-11|сосанны': '4420', '08-13|ипполита': '4432', '08-16|херимона': '4449', '08-20|севир и мемнон': '4484', '08-25|в едессе от ариан': '4517', '08-27|осии исповедника': '4531', '08-28|святой праведной анны': '2396', '09-01|индикта': '09426', '09-02|3618 мучеников': '143601', '09-03|аристиона': '09030000', '09-03|василисы': '1440', '09-05|захарии': '1453', '09-09|тайного угодника': '149401', '09-09|севириана': '1492', '09-11|мужском образе': '1506', '09-12|автонома': '1512', '09-16|киприана, митрополита киевского': '120901', '09-18|евмений': '1559', '09-21|кодрат': '1256', '09-23|ираиды': '2656', '09-24|равноапостольной феклы': '1594', '09-25|пафнутия': '1605', '10-05|дамиана': '1674', '10-05|иеремии': '1674', '10-05|матфея': '1674', '10-05|мамелхфы': '1678', '10-05|московским': '1779', '10-12|мальтийских': '1722', '11-20|саверия': '257401', '11-20|нирсы': '257401', '11-28|стефаном новым': '3244', // Bulgakov fixes (feast-related entries) '02-22|обретение честных мощей': '491', '11-20|во храм входа пресвятой': '257510', '11-25|отда́нїе праздника входа': '257512', '12-20|по плоти рождества господа бога и спаса': '09001', '12-22|из церковных песнопений этого дня': '09001', '12-23|из церковных песнопений этого дня': '09001', '12-24|из церковных песнопений этого дня': '09006', '12-26|собо́р прест҃ы́ѧ бг҃оро́дицы': '12260098', '12-31|отда́нїе праздника рождества̀ христо́ва': '123101', }; /** True if a token is a year, year-range, or Roman numeral (e.g. 309, 1238, V, IX). */ function isYearish(tok: string): boolean { if (/^\d+$/.test(tok)) return true; if (/^[ivxlc]+$/.test(tok)) return true; return false; } /** Normalize pre-revolutionary Russian orthography to modern. */ function normalizePreRev(s: string): string { return s .replace(/ѣ/g, 'е').replace(/Ѣ/g, 'Е') .replace(/і/g, 'и').replace(/І/g, 'И') .replace(/ѳ/g, 'ф').replace(/Ѳ/g, 'Ф') .replace(/ѵ/g, 'и').replace(/Ѵ/g, 'И') .replace(/ѧ/g, 'я').replace(/Ѧ/g, 'Я') .replace(/ѫ/g, 'у').replace(/Ѫ/g, 'У'); } function normalizeToken(tok: string): string { return normalizePreRev(tok).toLowerCase().replace(/ё/g, 'е').replace(/[^\p{L}]/gu, ''); } function tokenize(s: string): string[] { return s .replace(/\([^)]*\)/g, ' ') .replace(/[()[\].,;:—–-]/g, ' ') .split(/\s+/) .map(normalizeToken) .filter(t => t.length > 1 && !ROLE_WORDS.has(t) && !isYearish(t)); } /** Normalize a title for MANUAL_FIXES substring lookup. */ function normKey(s: string): string { return normalizePreRev(s).toLowerCase().replace(/ё/g, 'е').replace(/\s+/g, ' ').trim(); } /** Look up a manual override: date + distinctive normalized substring. */ function findManualFix(date: string, title: string): string | null { const t = normKey(title); for (const [key, cid] of Object.entries(MANUAL_FIXES)) { const [d, sub] = key.split('|'); if (d === date && t.includes(sub)) return cid; } return null; } /** Dice coefficient over significant tokens. */ function scoreMatch(source: string, name: string): number { const a = tokenize(source); const b = tokenize(name); if (a.length === 0 || b.length === 0) return 0; const setA = new Set(a); let inter = 0; for (const t of b) if (setA.has(t)) inter++; return (2 * inter) / (a.length + b.length); } interface Args { write: boolean; month: string | null; } function parseArgs(argv: string[]): Args { return { write: argv.includes('--write'), month: argv.includes('--month') ? argv[argv.indexOf('--month') + 1] ?? null : null, }; } export async function enrichLives(args: Args): Promise { if (!existsSync(MINEI_PATH)) { console.error('Missing scripts/output/minei.json — run convert-minei.ts first.'); process.exit(1); } const minei = JSON.parse(readFileSync(MINEI_PATH, 'utf-8')) as Record; // Optional second source: Bulgakov let bulgakov: Record = {}; if (existsSync(BULGAKOV_PATH)) { bulgakov = JSON.parse(readFileSync(BULGAKOV_PATH, 'utf-8')); console.log(`Loaded Bulgakov source: ${Object.keys(bulgakov).length} days, ${Object.values(bulgakov).flat().length} entries`); } const bundle = JSON.parse(readFileSync(MENAION_BUNDLE_PATH, 'utf-8')) as Record; // Load all ru/lives bundles into one map: cid -> {name, life, file} const livesByCid: Record; life?: { text?: string }; file: string }> = {}; function loadBundleDir(dir: string, prefix: string): void { for (const f of readdirSync(dir)) { const fullPath = join(dir, f); if (f.endsWith('.json')) { const bundleData = JSON.parse(readFileSync(fullPath, 'utf-8')) as Record; life?: { text?: string } }>; const fileRef = prefix ? `${prefix}/${f}` : f; for (const [cid, entry] of Object.entries(bundleData)) { livesByCid[cid] = { name: entry.name || {}, life: entry.life, file: fileRef }; } } else if (f !== '..' && f !== '.' && !f.includes('.')) { // Recurse into subdirectory loadBundleDir(fullPath, prefix ? `${prefix}/${f}` : f); } } } loadBundleDir(LIVES_DIR, ''); const report: ReportEntry[] = []; const pendingWrites: Record> = {}; let autoApplied = 0; let needReview = 0; let unmatched = 0; const filledByMinei = new Set(); // Process a single source (minei or bulgakov) for a given date function processSource( date: string, entries: SourceLife[], source: 'minei' | 'bulgakov', copyright: string, idPrefix: string, candidates: Candidate[], skipCids: Set, ): void { for (const life of entries) { let best: Candidate | null = null; const manualCid = findManualFix(date, life.title); if (manualCid) { const entry = livesByCid[manualCid]; if (entry) { best = { cid: manualCid, name: entry.name.nominative || entry.name.short || '', file: entry.file, hasLife: !!entry.life?.text, score: 1, }; } } else { for (const cand of candidates) { const score = scoreMatch(life.title, cand.name); if (score > (best?.score ?? 0)) { best = { ...cand, score }; } } } if (!best || best.score === 0) { report.push({ date, sourceTitle: life.title, source, matchedCid: null, matchedName: null, score: 0, status: 'unmatched' }); unmatched++; continue; } // For Bulgakov: skip if this CId was already filled by Minei if (source === 'bulgakov' && skipCids.has(best.cid)) { report.push({ date, sourceTitle: life.title, source, matchedCid: best.cid, matchedName: best.name, score: best.score, status: 'already-filled' }); continue; } const status = best.hasLife ? 'already-filled' : best.score >= 0.6 ? 'auto' : best.score >= 0.35 ? 'review' : 'unmatched'; report.push({ date, sourceTitle: life.title, source, matchedCid: best.score >= 0.35 ? best.cid : null, matchedName: best.score >= 0.35 ? best.name : null, score: best.score, status, }); if (status === 'auto') { autoApplied++; const targetFile = best.file; const key = targetFile.replace('.json', ''); if (!pendingWrites[key]) pendingWrites[key] = JSON.parse(readFileSync(join(LIVES_DIR, targetFile), 'utf-8')); const entry = pendingWrites[key][best.cid] as Record; const existingLife = entry.life as { id?: string; text?: string } | undefined; if (existingLife?.text) { entry.life = { id: existingLife.id!, copyright, text: `${existingLife.text}

${life.title}

${life.html}` }; } else { entry.life = { id: `${idPrefix}-${date.replace('-', '')}-${best.cid}`, copyright, text: life.html }; } if (source === 'minei') skipCids.add(best.cid); } else if (status === 'review') { needReview++; } else if (status === 'unmatched') { unmatched++; } } } // Build a set of all dates from both sources const allDates = new Set(Object.keys(minei)); if (Object.keys(bulgakov).length > 0) { for (const d of Object.keys(bulgakov)) allDates.add(d); } for (const date of [...allDates].sort()) { if (args.month && !date.startsWith(args.month)) continue; // Candidate CIds for this date from the menaion bundle const bundleIds = (bundle[date] || []).map(e => String(e.id)); const candidates: Candidate[] = bundleIds .map(cid => { const entry = livesByCid[cid]; if (!entry) return null; const name = entry.name.nominative || entry.name.short || ''; return { cid, name, file: entry.file, hasLife: !!entry.life?.text, score: 0, } as Candidate; }) .filter((c): c is Candidate => c !== null); // Process Minei entries first (primary source) const mineiLives = minei[date] || []; processSource(date, mineiLives, 'minei', COPYRIGHT_MINEI, 'minei', candidates, filledByMinei); // Process Bulgakov entries second (fills gaps only) if (Object.keys(bulgakov).length > 0) { const bulgakovLives = bulgakov[date] || []; if (bulgakovLives.length > 0) { processSource(date, bulgakovLives, 'bulgakov', COPYRIGHT_BULGAKOV, 'bulgakov', candidates, filledByMinei); } } } mkdirSync(dirname(REPORT_PATH), { recursive: true }); writeFileSync(REPORT_PATH, JSON.stringify(report, null, 0)); console.log(`Auto-applied: ${autoApplied}, needs review: ${needReview}, unmatched: ${unmatched}`); console.log(`Report: ${REPORT_PATH}`); if (args.write) { for (const [key, data] of Object.entries(pendingWrites)) { writeFileSync(join(LIVES_DIR, `${key}.json`), JSON.stringify(data)); } console.log(`Wrote ${Object.keys(pendingWrites).length} updated bundle(s).`); } else { const months = new Set(report.filter(r => r.status === 'auto').map(r => r.date.slice(0, 2))); console.log(`Dry run — no files changed. Auto-applied covers months: ${[...months].sort().join(', ')}`); console.log('Re-run with --write to apply.'); } } if (import.meta.url === `file://${process.argv[1]}`) { (async () => { const args = parseArgs(process.argv.slice(2)); await enrichLives(args); })(); }