aboutsummaryrefslogtreecommitdiff
path: root/internal/bible/validate.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-27 20:29:31 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-27 20:29:31 +0200
commit0bcddd4ed0aff66452397dc0973337c535c0d2ed (patch)
treefffae4df5ba0efe80c0f7c2501ebf0396e438fdd /internal/bible/validate.go
parent399f0d428050ee104f16e8d4a289b65a4a68eb13 (diff)
downloadlectio-0bcddd4ed0aff66452397dc0973337c535c0d2ed.tar.gz
lectio-0bcddd4ed0aff66452397dc0973337c535c0d2ed.zip
feat(cli): lectio --corpus-check validator
Diffstat (limited to 'internal/bible/validate.go')
-rw-r--r--internal/bible/validate.go93
1 files changed, 93 insertions, 0 deletions
diff --git a/internal/bible/validate.go b/internal/bible/validate.go
new file mode 100644
index 0000000..27ec519
--- /dev/null
+++ b/internal/bible/validate.go
@@ -0,0 +1,93 @@
+package bible
+
+import (
+ "fmt"
+ "sort"
+)
+
+// 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.
+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 {
+ 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))
+ }
+ }
+ if refChaps, ok := ref.books[book]; ok {
+ for ch := range refChaps {
+ 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
+}