Files
christianandClaude Opus 5 0db5e9b07d Bible App 1.4.34 — full source
First commit on the local Gitea server, made after moving the tree off the
Windows machine. Build outputs, node_modules and the AI models stay out
(see .gitignore); everything the installer build needs is tracked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 13:05:20 -04:00

131 lines
6.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* gen-stats.cjs — rebuild interlinear/_strongs-stats.json from the tagged corpus.
*
* Powers the popup's "Used: 175× in Scripture — as “angel” (168×), “messenger” (7×)" line.
* Shape: { "G0032": { n: <total occurrences>, g: [[gloss, count], ...] } }
*
* The generator that produced the shipped file is not in the repo, so the normalisation below is
* reverse-engineered from it and then MEASURED against it: Greek numbers are validated exactly,
* because the NT corpus did not change in this fix, so any disagreement there is my algorithm
* differing rather than the data.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const DIR = process.argv[2] || 'C:/Users/media/Downloads/bible-app/interlinear';
const MODE = process.argv[3] || 'compare'; // 'compare' | 'write'
/*
* Fold the per-verse gloss into the form the card shows.
* Deliberately light: "create" and "created" stay DISTINCT (the shipped data keeps both), so this
* is not a stemmer. Only a final plural -s is folded, which is what turns "angels"/"angels,"
* into "angel" while leaving "brothers —" alone (its last character is the dash, not the s).
*/
function normGloss(raw) {
let s = String(raw || '').toLowerCase();
s = s.split('[').join('').split(']').join('')
.split('<').join('').split('>').join('');
s = s.replace(/["'“”]/g, '');
s = s.replace(/[.,;:?!]+/g, ' ');
s = s.replace(/\s+/g, ' ').trim();
/*
* Strip the leading words that come from GRAMMAR rather than from the choice of English word.
*
* A Hebrew verb's gloss carries its subject ("and he said", "am about to create") and a noun's
* carries its case ("of Paul", "in Israel"). Those are properties of the occurrence, not
* different translations of the lexeme, and leaving them in fragments the count: without this,
* H1254 reports «"he created" (4×)» instead of «"created" (21×)», which is the wrong answer to
* "how is this word translated?".
*
* The list is derived from the shipped file's own output: it folds and/in/of/to and subject
* pronouns ("said" 2912, "saying" 940 for H0559; "israel" 2357 absorbing "in israel") while
* keeping meaning-bearing prepositions distinct ("o israel", "against israel", "among israel").
* So `o`, `against`, `among`, `from`, `by`, `on`, `for`, `with`, `after`, `like` stay.
*
* The trailing space in the pattern is what makes this safe for the function words' OWN
* entries: "was" alone never matches, so H1961 still reports "was" and "were".
*/
const LEAD = /^(?:a|an|the|of|to|in|and|i|you|he|she|it|we|they|me|him|her|them|us|am|is|are|was|were|be|been|being|has|have|had|will|shall|do|does|did|let) /;
for (;;) {
const next = s.replace(LEAD, '');
if (next === s || !next) break;
s = next;
}
/*
* Fold a final plural -s, guarded on -ss/-us/-is/-as.
* The guard is the point: without it the shipped generator turned every "Jesus" into "jesu",
* so the card read «Used: 907× in Scripture — as "jesu" (899×)».
*/
if (/s$/.test(s) && s.length > 3 && !/(ss|us|is|as)$/.test(s)) s = s.slice(0, -1);
return s.trim();
}
const counts = new Map(); // strong -> { n, glosses: Map }
for (const f of fs.readdirSync(DIR).filter((x) => x.endsWith('.json') && !x.startsWith('_'))) {
const d = JSON.parse(fs.readFileSync(path.join(DIR, f), 'utf-8'));
for (const k of Object.keys(d)) {
for (const w of (d[k] || [])) {
const s = w && w.s;
if (!s) continue;
if (!counts.has(s)) counts.set(s, { n: 0, glosses: new Map() });
const rec = counts.get(s);
rec.n++;
const g = normGloss(w.g);
if (g) rec.glosses.set(g, (rec.glosses.get(g) || 0) + 1);
}
}
}
const out = {};
for (const [s, rec] of counts) {
const g = [...rec.glosses.entries()]
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
.slice(0, 4);
out[s] = { n: rec.n, g };
}
if (MODE === 'write') {
const p = path.join(DIR, '_strongs-stats.json');
fs.writeFileSync(p, JSON.stringify(out));
console.log('wrote', p, '| entries:', Object.keys(out).length);
} else {
const shipped = JSON.parse(fs.readFileSync(path.join(DIR, '_strongs-stats.json'), 'utf-8'));
let gTotal = 0, gExactN = 0, gExactAll = 0;
const bad = [];
for (const k of Object.keys(shipped)) {
if (k[0] !== 'G') continue; // Greek only: that corpus is unchanged
gTotal++;
const a = shipped[k], b = out[k];
if (!b) { bad.push([k, 'absent now']); continue; }
if (a.n === b.n) gExactN++;
if (JSON.stringify(a) === JSON.stringify(b)) gExactAll++;
else if (bad.length < 6) bad.push([k, 'shipped=' + JSON.stringify(a), 'mine=' + JSON.stringify(b)]);
}
console.log('Greek entries checked:', gTotal);
console.log(' same occurrence count n :', gExactN, '(' + (100 * gExactN / gTotal).toFixed(1) + '%)');
console.log(' identical n AND glosses :', gExactAll, '(' + (100 * gExactAll / gTotal).toFixed(1) + '%)');
/*
* The real quality signal: does the PRIMARY gloss — the one the card leads with — still agree?
* Counts must move (2,083 verses were added, and the shipped `n` matches neither word nor verse
* occurrences), but the headline answer to "how is this word translated?" should not.
*/
let topSame = 0, topTotal = 0, topDiff = [];
for (const k of Object.keys(shipped)) {
const a = shipped[k], b = out[k];
if (!a || !b || !a.g || !a.g.length || !b.g || !b.g.length) continue;
topTotal++;
if (a.g[0][0] === b.g[0][0]) topSame++;
else if (topDiff.length < 8) topDiff.push(' ' + k + ' shipped="' + a.g[0][0] + '" mine="' + b.g[0][0] + '"');
}
console.log('\nprimary gloss agreement (all', topTotal, 'entries):', topSame,
'(' + (100 * topSame / topTotal).toFixed(1) + '%)');
if (topDiff.length) console.log('sample primary-gloss changes:\n' + topDiff.join('\n'));
console.log('\n new total entries:', Object.keys(out).length, 'vs shipped', Object.keys(shipped).length);
if (bad.length) console.log('\nexamples of n/gloss disagreement:\n' + bad.map((x) => ' ' + x.join('\n ')).join('\n'));
}