package bible import ( "fmt" "sort" "strings" "unicode/utf8" ) // CorpusReport is the outcome of validating one corpus's sidecar + text (see // CheckCorpus). Errors mean the corpus is unusable/malformed and fail // --corpus-check (exit 1); Warnings flag coverage gaps against the reference // Vulgate ("vul") and never fail the check -- a corpus may legitimately be // incomplete (the built-in wuj is) and still be a valid drop-in. type CorpusReport struct { Code string Errors []string Warnings []string } // OK reports whether the corpus is usable: no errors. Warnings never affect it. func (r CorpusReport) OK() bool { return len(r.Errors) == 0 } var validPsalmSystems = map[string]bool{"vulgate": true, "hebrew": true, "drb": true} // CheckCorpus validates code's sidecar and text and reports coverage gaps vs // "vul" as warnings. // // Sidecar: lang, name and psalm_system are required (psalm_system must be // vulgate/hebrew/drb); sigla and autoselect are optional -- their absence, or // autoselect=false, is never flagged (the built-in wuj sets autoselect=false // and must pass clean). // // Text: every Book column value must be one of CanonicalBooks' 73 keys // (error). A chapter missing entirely versus vul, a verse-number gap within a // chapter, or a repeated verse number within a chapter, is a warning only -- // real source texts legitimately do this (e.g. the LXX's lettered doublet // verses in 3 Kingdoms, or a scanned translation's occasional merged verse), // and it must never turn a corpus that otherwise loads fine into a failure. A // verse that IS present but whose text is a stub next to the aligned Vulgate // verse (a likely truncation or lost line -- the one defect the count-based // checks cannot see) is likewise a warning. func CheckCorpus(code string) CorpusReport { r := CorpusReport{Code: code} m, ok := Meta(code) if !ok || m.Lang == "" || m.Name == "" || m.PsalmSystem == "" { r.Errors = append(r.Errors, "missing or incomplete sidecar (need lang, name, psalm_system)") } if m.PsalmSystem != "" && !validPsalmSystems[m.PsalmSystem] { r.Errors = append(r.Errors, fmt.Sprintf("invalid psalm_system %q (want vulgate|hebrew|drb)", m.PsalmSystem)) } c := load(code) if len(c.books) == 0 { r.Errors = append(r.Errors, "no verses parsed (empty or malformed .tsv)") sort.Strings(r.Errors) return r } canon := CanonicalBooks() for book := range c.books { if !canon[book] { r.Errors = append(r.Errors, "unknown book name: "+book) } } ref := load("vul") for book, chaps := range c.books { refBook := ref.books[book] // nil if vul lacks the book (e.g. an EF-only name) for ch, verses := range chaps { seen := map[int]bool{} maxV := 0 for _, v := range verses { if seen[v.Verse] { r.Warnings = append(r.Warnings, fmt.Sprintf("%s %d: duplicate verse %d", book, ch, v.Verse)) } seen[v.Verse] = true if v.Verse > maxV { maxV = v.Verse } } if maxV > len(seen) { r.Warnings = append(r.Warnings, fmt.Sprintf("%s %d: verse gap (have %d verse(s), highest numbered %d)", book, ch, len(seen), maxV)) } // Suspiciously-short verse: present, but a stub next to the SAME verse // in the reference Vulgate -- a likely truncation or lost line (an // opening dropped in a scrape, a merge artifact) that the // presence-only checks cannot see, since the verse IS there. The // comparison is cross-corpus, not within the chapter, precisely so a // genuinely terse verse ("Non occides", "Jesus wept") is NOT flagged: // the Vulgate's own copy is short too, so the guard below fails. It // fires only when the corpus verse is a stub AND the aligned Vulgate // verse is substantial. Skipped on the Psalms when the corpus // renumbers them (psalm_system != vulgate), where the per-verse // alignment with the Vulgate does not hold. if refBook != nil && !(book == "Psalms" && m.PsalmSystem != "vulgate") { if refCh, ok := refBook[ch]; ok { refLen := make(map[int]int, len(refCh)) for _, rv := range refCh { refLen[rv.Verse] = utf8.RuneCountInString(strings.TrimSpace(rv.Text)) } for _, v := range verses { n := utf8.RuneCountInString(strings.TrimSpace(v.Text)) if rl := refLen[v.Verse]; rl >= 40 && n < 12 && n*4 < rl { r.Warnings = append(r.Warnings, fmt.Sprintf("%s %d:%d: text suspiciously short (%d chars vs vul %d)", book, ch, v.Verse, n, rl)) } } } } } if refBook != nil { for ch := range refBook { if _, have := chaps[ch]; !have { r.Warnings = append(r.Warnings, fmt.Sprintf("%s: missing chapter %d (present in vul)", book, ch)) } } } } sort.Strings(r.Errors) sort.Strings(r.Warnings) return r }