/* * merge.cjs — add the missing OT verses to interlinear/.json. * * ADDITIVE ONLY. Every verse already present keeps its existing value byte-for-byte; this only * fills keys that are absent or empty. That matters because 36 shipped verses disagree with a * fresh conversion (Qere/Ketiv variant picks, empty-word placeholders, and the verse-boundary * cases TAHOT's own header lists) — those are left exactly as they are rather than "corrected" * into a change nobody asked for. * * Keys are written in canonical chapter|verse order. The app looks verses up by key, but * server.js /api/interlinear iterates Object.keys() for whole-chapter requests, so a predictable * order is worth having. Output is minified with no trailing newline, matching the shipped files * (verified: JSON.stringify round-trips them byte-identically). */ 'use strict'; const fs = require('fs'); const path = require('path'); const { parseFile } = require('./tahot-convert.cjs'); const REPO = process.argv[2] || 'C:/Users/media/Downloads/bible-app'; const BACKUP = process.argv[3]; const DRY = process.argv.includes('--dry'); const DIR = __dirname + '/tahot'; if (!BACKUP && !DRY) { console.error('usage: merge.cjs [--dry]'); process.exit(1); } // ---- collect every TAHOT verse ---- const all = new Map(); // "Book|ch|v" -> words for (const f of fs.readdirSync(DIR).filter((x) => x.endsWith('.txt'))) { const { verses } = parseFile(path.join(DIR, f)); for (const [k, v] of verses) all.set(k, v); } console.log('TAHOT verses parsed:', all.size); const byBook = new Map(); for (const [k, v] of all) { const i = k.indexOf('|'); const book = k.slice(0, i); if (!byBook.has(book)) byBook.set(book, new Map()); byBook.get(book).set(k.slice(i + 1), v); // "ch|v" -> words } // ---- back up before touching anything ---- if (!DRY) { fs.mkdirSync(BACKUP, { recursive: true }); for (const f of fs.readdirSync(path.join(REPO, 'interlinear'))) { fs.copyFileSync(path.join(REPO, 'interlinear', f), path.join(BACKUP, f)); } console.log('backed up interlinear/ ->', BACKUP); } const sortKey = (k) => { const [c, v] = k.split('|').map(Number); return c * 10000 + v; }; let totalAdded = 0, totalWords = 0, booksChanged = 0; const report = []; for (const [book, tahotVerses] of byBook) { const p = path.join(REPO, 'interlinear', book + '.json'); if (!fs.existsSync(p)) { report.push([book, 'NO SHIPPED FILE', 0]); continue; } const original = fs.readFileSync(p, 'utf-8'); const shipped = JSON.parse(original); const merged = {}; let added = 0, words = 0; // Union of shipped keys and TAHOT keys, canonical order. const keys = new Set([...Object.keys(shipped), ...tahotVerses.keys()]); for (const k of [...keys].sort((a, b) => sortKey(a) - sortKey(b))) { const have = shipped[k]; if (have && have.length) { merged[k] = have; continue; } // untouched const fresh = tahotVerses.get(k); if (fresh && fresh.length) { merged[k] = fresh; added++; words += fresh.length; } else if (have) { merged[k] = have; } // preserve an empty array as-is } // Safety: no shipped verse may change. for (const k of Object.keys(shipped)) { if (shipped[k] && shipped[k].length && JSON.stringify(merged[k]) !== JSON.stringify(shipped[k])) { throw new Error('REFUSING TO WRITE: ' + book + ' ' + k + ' would change'); } } if (added) { booksChanged++; totalAdded += added; totalWords += words; report.push([book, added + ' verses', words + ' words']); if (!DRY) fs.writeFileSync(p, JSON.stringify(merged)); } } console.log('\nbooks changed:', booksChanged, '| verses added:', totalAdded, '| words added:', totalWords); report.sort((a, b) => parseInt(b[1]) - parseInt(a[1])); report.forEach((r) => console.log(' ' + String(r[0]).padEnd(18) + String(r[1]).padStart(12) + ' ' + r[2])); if (DRY) console.log('\n(dry run — nothing written)');