diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-27 20:29:31 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-27 20:29:31 +0200 |
| commit | 0bcddd4ed0aff66452397dc0973337c535c0d2ed (patch) | |
| tree | fffae4df5ba0efe80c0f7c2501ebf0396e438fdd /internal/bible/validate.go | |
| parent | 399f0d428050ee104f16e8d4a289b65a4a68eb13 (diff) | |
| download | lectio-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.go | 93 |
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 +} |
