1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
|
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
}
|