#!/usr/bin/env node const fs = require('fs') const path = require('path') const SRC = process.env.BSB_MD_PATH || path.join('bibles', 'bible-bsb.md') function canon() { const OT = [ ['Genesis', ['Genesis'], 50], ['Exodus', ['Exodus'], 40], ['Leviticus', ['Leviticus'], 27], ['Numbers', ['Numbers'], 36], ['Deuteronomy', ['Deuteronomy'], 34], ['Joshua', ['Joshua'], 24], ['Judges', ['Judges'], 21], ['Ruth', ['Ruth'], 4], ['1 Samuel', ['1 Samuel','1 Samuel'], 31], ['2 Samuel', ['2 Samuel','2 Samuel'], 24], ['1 Kings', ['1 Kings','1 Kings'], 22], ['2 Kings', ['2 Kings','2 Kings'], 25], ['1 Chronicles', ['1 Chronicles','1 Chronicles'], 29], ['2 Chronicles', ['2 Chronicles','2 Chronicles'], 36], ['Ezra', ['Ezra'], 10], ['Nehemiah', ['Nehemiah'], 13], ['Esther', ['Esther'], 10], ['Job', ['Job'], 42], ['Psalms', ['Psalms','Psalm'], 150], ['Proverbs', ['Proverbs'], 31], ['Ecclesiastes', ['Ecclesiastes'], 12], ['Song of Songs', ['Song of Songs','Song of Solomon'], 8], ['Isaiah', ['Isaiah'], 66], ['Jeremiah', ['Jeremiah'], 52], ['Lamentations', ['Lamentations'], 5], ['Ezekiel', ['Ezekiel'], 48], ['Daniel', ['Daniel'], 12], ['Hosea', ['Hosea'], 14], ['Joel', ['Joel'], 3], ['Amos', ['Amos'], 9], ['Obadiah', ['Obadiah'], 1], ['Jonah', ['Jonah'], 4], ['Micah', ['Micah'], 7], ['Nahum', ['Nahum'], 3], ['Habakkuk', ['Habakkuk'], 3], ['Zephaniah', ['Zephaniah'], 3], ['Haggai', ['Haggai'], 2], ['Zechariah', ['Zechariah'], 14], ['Malachi', ['Malachi'], 4] ] const NT = [ ['Matthew', ['Matthew'], 28], ['Mark', ['Mark'], 16], ['Luke', ['Luke'], 24], ['John', ['John'], 21], ['Acts', ['Acts'], 28], ['Romans', ['Romans'], 16], ['1 Corinthians', ['1 Corinthians','1 Corinthians'], 16], ['2 Corinthians', ['2 Corinthians','2 Corinthians'], 13], ['Galatians', ['Galatians'], 6], ['Ephesians', ['Ephesians'], 6], ['Philippians', ['Philippians'], 4], ['Colossians', ['Colossians'], 4], ['1 Thessalonians', ['1 Thessalonians','1 Thessalonians'], 5], ['2 Thessalonians', ['2 Thessalonians','2 Thessalonians'], 3], ['1 Timothy', ['1 Timothy','1 Timothy'], 6], ['2 Timothy', ['2 Timothy','2 Timothy'], 4], ['Titus', ['Titus'], 3], ['Philemon', ['Philemon'], 1], ['Hebrews', ['Hebrews'], 13], ['James', ['James'], 5], ['1 Peter', ['1 Peter','1 Peter'], 5], ['2 Peter', ['2 Peter','2 Peter'], 3], ['1 John', ['1 John','1 John'], 5], ['2 John', ['2 John','2 John'], 1], ['3 John', ['3 John','3 John'], 1], ['Jude', ['Jude'], 1], ['Revelation', ['Revelation'], 22] ] return [ ...OT.map(([n,v,c]) => ({ name:n, variants:v, expectedChapters:c, testament:'OT' })), ...NT.map(([n,v,c]) => ({ name:n, variants:v, expectedChapters:c, testament:'NT' })), ] } function main() { if (!fs.existsSync(SRC)) { console.error('Missing source file:', SRC) process.exit(1) } const md = fs.readFileSync(SRC, 'utf-8') const books = canon() const report = { file: SRC, totals: { versesTagged: 0 }, books: [] } for (const b of books) { const patterns = b.variants.map(v => v.replace(/\s+/g, '\\s+')) const names = patterns.join('|') const re = new RegExp(`(?:^|[\n\r\f\s\|\(])(?:${names})\\s+(\\d+):(\\d+)`, 'gi') const chapters = new Set() let m let verseCount = 0 while ((m = re.exec(md)) !== null) { const nums = m.slice(1).filter(Boolean) const ch = parseInt(nums[0] || '0', 10) const vs = parseInt(nums[1] || '0', 10) if (Number.isFinite(ch) && ch > 0) chapters.add(ch) if (Number.isFinite(vs) && vs > 0) verseCount++ } // Heuristic: some one-chapter books may lack inline verse references; accept header presence const oneChapterBooks = new Set(['Obadiah','Philemon','2 John','3 John','Jude']) if (chapters.size === 0 && oneChapterBooks.has(b.name)) { const headerRe = new RegExp(`[\f\n\r]\s*${b.variants.map(v=>v.replace(/\s+/g,'\\s+')).join('|')}\s*[\n\r]`, 'i') if (headerRe.test(md)) { chapters.add(1) } } report.totals.versesTagged += verseCount report.books.push({ name: b.name, testament: b.testament, expectedChapters: b.expectedChapters, detectedChapters: Array.from(chapters).sort((a,b)=>a-b), detectedCount: chapters.size, coverage: b.expectedChapters > 0 ? +(100 * chapters.size / b.expectedChapters).toFixed(2) : null, verseMarkers: verseCount }) } const missingBooks = report.books.filter(x => x.detectedCount === 0).map(x=>x.name) const partialBooks = report.books.filter(x => x.detectedCount > 0 && x.detectedCount < x.expectedChapters).map(x=>({name:x.name, det:x.detectedCount, exp:x.expectedChapters})) console.log('Validation summary for', SRC) console.log('Total verse markers found:', report.totals.versesTagged) console.log('Books missing markers:', missingBooks.length ? missingBooks.join(', ') : 'None') console.log('Books partially detected (chapters):', partialBooks.length ? JSON.stringify(partialBooks.slice(0,10)) : 'None') const outDir = path.join('data','en_bible','BSB_VALIDATION') fs.mkdirSync(outDir, { recursive: true }) fs.writeFileSync(path.join(outDir,'report.json'), JSON.stringify(report, null, 2), 'utf-8') console.log('Wrote detailed report to', path.join(outDir,'report.json')) } main()