summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-23 23:01:42 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-23 23:01:42 +0200
commit8faf89f89bd6783a518b6fc0b48c6ee452ba2e94 (patch)
tree6aa160722b0c8cf0cc29df7e7ee5cce6082e4429 /tools
parent0806fe65e388a9502035bc9d4f551528ac74b26d (diff)
downloadcolitur-8faf89f89bd6783a518b6fc0b48c6ee452ba2e94.tar.gz
colitur-8faf89f89bd6783a518b6fc0b48c6ee452ba2e94.zip
feat(ordo): wire the LMS preface column into the witness suite
Extends tools/extract_lms_ordo.ml's row type with a praef field (the first "Pr of ..." / "Common Pr" match in each day's universal block, same first-in-reading-order discipline already used for Gl/Cr) and regenerates all three LMS fixtures. Also brings lms-ordo-2024-2025.sexp under the generalised, probe-verified provenance template the Witnesses task previously added only for the two newer editions -- it had kept Task 6's original hand-written header. Characterisation finding: this Ordo prints OPTION LISTS ("Pr of X or Pr of Y or Common Pr"), not a single value per day, and several named options (Martyrs, All Saints and Patron Saints, the Dedication of a Church, the Most Holy Sacrament, St John the Baptist, the Angels) are genuinely absent from RG 484-497's own fourteen named prefaces. Every instance found in the universal block pairs such an extra with a genuine RG-482 answer, so test/test_lms_ordo.ml compares by membership (classify_praef_options, a 15-entry prefix table split on the standalone token "or"), not string equality. Opposite-prediction probes (Nativity/Apostles/Requiem) are hard- verified inside the extractor itself before any fixture is trusted, the same discipline already used for the Creed column. Result: 396/396, 399/399, 398/398 comparable rows agree across the three editions (Good Friday excluded per edition, no Mass that day), zero allow-list entries needed. extraordinaryform.org's own three editions were characterised but deliberately not wired in this pass -- see .superpowers/sdd/2026-08-21-colitur-celebrant-rubrics-phase1/preface-witnesses-report.md for the full account (the field is genuinely per-day, contrary to a naive header-count read, but carries its own classification anomaly during Advent and reconfirms the corpus's known St Lawrence vigil defect, and building a full extractor for it is a separate, comparably large task not needed to decide the RG 497 question this same report settles). Claude-Session: https://claude.ai/code/session_017ZBxCCRM2ojnBupp3SBxV9
Diffstat (limited to 'tools')
-rw-r--r--tools/extract_lms_ordo.ml198
1 files changed, 189 insertions, 9 deletions
diff --git a/tools/extract_lms_ordo.ml b/tools/extract_lms_ordo.ml
index 8680a91..582fb6e 100644
--- a/tools/extract_lms_ordo.ml
+++ b/tools/extract_lms_ordo.ml
@@ -62,6 +62,24 @@ type row = {
matched as a substring of "IV Mass of BVM"). *)
gloria : bool option; (** [None] only if genuinely not found -- reported, not silently dropped *)
creed : bool option;
+ praef : string option;
+ (** RAW trailing text from the FIRST "Pr of ..." / "Common Pr" match
+ in the universal block's own reading order -- CAPTURED, NOT
+ CLASSIFIED here, the same discipline
+ tools/extract_fiuv_ordo.ml's own [row.praef] documents for a
+ different publisher's format. See the provenance header's own
+ "Pr of" characterisation bullet for what this column can and
+ cannot show: this Ordo prints OPTION LISTS ("Pr of X or Pr of Y
+ or Common Pr"), some of whose named options ("Martyrs", "All
+ Saints and Patron Saints", "the Dedication of a Church", "the
+ Most Holy Sacrament", "St John the Baptist", "the Angels") are
+ NOT among the fourteen the 1962 Missale Romanum's own RG 484-497
+ enumerate -- ad libitum extras this publisher includes alongside
+ a genuine RG 482 answer, never the sole option on any row found
+ in this corpus. Classification into {!Colitur_kernel.Preface.t}
+ happens test-side (test/test_lms_ordo.ml), by MEMBERSHIP in the
+ parsed option set, not string equality -- see that file's own
+ [classify_praef] citation for the full account. *)
has_diocesan_variant : bool;
(** whether a diocese-specific block followed the universal one
anywhere in this day's raw text (informational only -- see the
@@ -126,10 +144,10 @@ let bvm_numeral_of_line line = List.assoc_opt line (List.map (fun (n, s) -> (s,
(* [lines]: this day's own raw continuation lines, marker-stripped and
trimmed, title line NOT included (the title never carries Gl/Cr/"Mass
of" itself in this source). Returns (formulary_override, bvm_numeral,
- gloria, creed, has_diocesan_variant), each derived from the portion
- BEFORE the first colon-terminated (diocesan) line -- see the module
- header for why this is belt-and-suspenders, not load-bearing on its
- own. *)
+ gloria, creed, praef, has_diocesan_variant), each derived from the
+ portion BEFORE the first colon-terminated (diocesan) line -- see the
+ module header for why this is belt-and-suspenders, not load-bearing on
+ its own. *)
let scan_block lines =
let rec universal_prefix = function
| [] -> []
@@ -161,7 +179,36 @@ let scan_block lines =
reads as [true], the same as any other non-"No" predecessor would). *)
let gloria = find_flag "Gl" ("" :: all_tokens) in
let creed = find_flag "Cr" ("" :: all_tokens) in
- (formulary_override, bvm_numeral, gloria, creed, has_diocesan)
+ (* [praef]: LINE-SCOPED (not [all_tokens]'s flattened stream), because
+ the captured text runs to the END of whichever physical line carries
+ it -- flattening first would glue an unrelated following line's own
+ prose onto it. "Pr" always abbreviates "Preface" in this source's own
+ body text (checked directly: its only OTHER appearance is the front-
+ matter Abbreviations glossary entry "Pr Preface", outside the Ordo
+ body this tool scans), so a whole-token ("Pr","of") pair, or a bare
+ ("Common","Pr") pair when no proper/de-Tempore option is offered, is
+ the sole anchor needed -- no reject-list, unlike the BVM roman
+ numeral's own substring trap, because nothing else in this corpus's
+ body ever produces either two-token sequence. *)
+ let find_praef_in_line line =
+ let toks = Array.of_list (split_ws line) in
+ let n = Array.length toks in
+ let rec go i =
+ if i + 1 >= n then None
+ else if (toks.(i) = "Pr" && toks.(i + 1) = "of") || (toks.(i) = "Common" && toks.(i + 1) = "Pr") then
+ Some (String.concat " " (Array.to_list (Array.sub toks i (n - i))))
+ else go (i + 1)
+ in
+ go 0
+ in
+ let praef =
+ let rec find = function
+ | [] -> None
+ | l :: rest -> ( match find_praef_in_line l with Some s -> Some s | None -> find rest)
+ in
+ find universal
+ in
+ (formulary_override, bvm_numeral, gloria, creed, praef, has_diocesan)
(* --- the top-level line scan --------------------------------------------- *)
@@ -334,25 +381,29 @@ let () =
die "%s: printed weekday %S does not match computed weekday %S -- date resolution bug"
(Date.to_iso8601 date) b.b_weekday (Date.weekday_to_string (Date.weekday date))
| None -> die "%s: unrecognised weekday abbreviation %S" (Date.to_iso8601 date) b.b_weekday);
- let formulary_override, bvm_numeral, gloria, creed, has_diocesan_variant = scan_block b.b_lines in
+ let formulary_override, bvm_numeral, gloria, creed, praef, has_diocesan_variant = scan_block b.b_lines in
{ date = Date.to_iso8601 date; weekday = b.b_weekday; title = b.b_title; formulary_override; bvm_numeral;
- gloria; creed; has_diocesan_variant })
+ gloria; creed; praef; has_diocesan_variant })
blocks
in
let missing_gloria = List.filter (fun r -> r.gloria = None) rows in
let missing_creed = List.filter (fun r -> r.creed = None) rows in
+ let missing_praef = List.filter (fun r -> r.praef = None) rows in
if missing_gloria <> [] then
Printf.printf "extract_lms_ordo: WARNING %d rows with no Gloria found: %s\n" (List.length missing_gloria)
(String.concat ", " (List.map (fun r -> r.date) missing_gloria));
if missing_creed <> [] then
Printf.printf "extract_lms_ordo: WARNING %d rows with no Creed found: %s\n" (List.length missing_creed)
(String.concat ", " (List.map (fun r -> r.date) missing_creed));
+ if missing_praef <> [] then
+ Printf.printf "extract_lms_ordo: WARNING %d rows with no Pr-of/Common-Pr found: %s\n" (List.length missing_praef)
+ (String.concat ", " (List.map (fun r -> r.date) missing_praef));
let with_variant = List.length (List.filter (fun r -> r.has_diocesan_variant) rows) in
let with_override = List.length (List.filter (fun r -> r.formulary_override <> None) rows) in
let with_bvm = List.length (List.filter (fun r -> r.bvm_numeral <> None) rows) in
Printf.printf "extract_lms_ordo: %d rows; %d with a diocesan variant present; %d with a formulary override; \
- %d with a BVM roman numeral\n"
- (List.length rows) with_variant with_override with_bvm;
+ %d with a BVM roman numeral; %d with no Pr-of/Common-Pr text\n"
+ (List.length rows) with_variant with_override with_bvm (List.length missing_praef);
(* --- provenance header + write ----------------------------------------- *)
let sha256_of_file path =
let cmd = Printf.sprintf "sha256sum %s" (Filename.quote path) in
@@ -401,6 +452,24 @@ let () =
"NOT found by URL-pattern web search this session (unlike the 2023-2024 and 2024-2025 editions, no exact download link matching this PDF's own filename/version turned up under lms.org.uk/sites/default/files/u5374/; several near-miss candidates were checked and rejected -- none matched this PDF's own metadata) -- left honestly blank rather than guessed; the file was supplied locally to this session, not fetched by this tool",
{|28 November 2025 ("End of November 2025", the same shape as the 2024-2025 edition -- Advent Sunday 2025 fell on 30 November, so only 3 tail days remained; note this excludes 27 November 2025, which the PRECEDING edition's own window (2024-2025, through 2025-12-31) already covers -- a one-day-short overlap between consecutive editions' own windows, not a gap or a bug in either extraction|}
)
+ | "lms-ordo-2024-2025.sexp" ->
+ (* Preface-witnesses task (2026-08-23): brought into this same
+ auto-generated, probe-verified header template so all three
+ editions carry the identical characterisation discipline --
+ Task 6's own original HAND-WRITTEN header (never regenerated by
+ the Witnesses task, which only added table entries for the two
+ NEW editions) is superseded by this run; every fact below is
+ re-derived from this PDF's own text/metadata, not copied
+ blind from that older header (though it agrees with it in
+ every particular checked). *)
+ ( "2024-2025",
+ "9781739209636",
+ "2024",
+ {|Author "Peter Day-Milne", Producer "LibreOffice 7.4", CreationDate 2024-11-06, 140 pages|},
+ "https://lms.org.uk/sites/default/files/u5374/ordo_2.21_2024-2025.pdf",
+ "identified via web search matching the PDF's own version number and filename to the publisher's own hosting path (product page: https://lms.org.uk/product/latin-mass-society-ordo-2024-25); NOT independently re-downloaded and byte-compared in this session -- recorded honestly rather than presented as verified",
+ {|27 November 2024 ("End of November 2024", the same shape as the 2025-2026 edition -- Advent Sunday 2024 fell on 1 December, so 4 tail days remained (27-30 November))|}
+ )
| _ -> die "no provenance/characterisation entry for %s -- add one to extract_lms_ordo.ml's own per-edition table before generating this fixture (the tool refuses to emit an under-characterised header)" base
in
let creed_of_date rows d = List.find_map (fun r -> if String.equal r.date d then r.creed else None) rows in
@@ -420,6 +489,12 @@ let () =
"St Laurence, II class martyr, no Lord/BVM/apostle clause applies, RG 476(b) -- St Joachim (16 August) is IMPEDED by a Sunday in this edition's own calendar (12th Sunday after Pentecost), so this edition needed a DIFFERENT sharp non-octave II-class pairing than the 2023-2024/2024-2025 editions used; a real, edition-specific finding, not an extraction quirk"
);
("2026-08-15", true, "the Assumption, I class, RG 475(b)") ]
+ | "lms-ordo-2024-2025.sexp" ->
+ [ ("2024-12-01", true, "Advent Sunday, RG 475(a)");
+ ("2024-12-02", false, "ordinary Advent feria, RG 476(b)/(d)");
+ ("2024-12-26", true, "St Stephen, II class, inside the Nativity octave, RG 475(d)");
+ ("2025-08-15", true, "the Assumption, I class, RG 475(b)");
+ ("2025-08-16", false, "St Joachim, II class, no Lord/BVM/apostle clause applies, RG 476(b)") ]
| _ -> die "no probe list for %s" base
in
List.iter
@@ -430,6 +505,54 @@ let () =
| None -> die "characterisation probe FAILED: %s (%s) -- no row extracted for this date at all" d desc)
probes;
Printf.printf "extract_lms_ordo: all %d characterisation probes confirmed for %s\n" (List.length probes) edition_label;
+ (* Preface-witnesses task (2026-08-23): a SECOND opposite-prediction
+ probe set, over [praef] rather than [creed] -- the same discipline,
+ applied to the new column this task adds, per the task brief's own
+ "probe dates where the rule predicts OPPOSITE answers before
+ adjudicating anything" instruction. Three universal, unambiguous
+ fixed-date feasts per edition, each landing on a DIFFERENT named
+ preface (RG 484 Nativity, RG 497 Apostles, RG 499 Requiem) -- checked
+ directly against this run's own [rows] before this header is
+ written, a hard [die] on failure, not a printed claim. All Souls'
+ own date is edition-specific (2 November, TRANSFERRED to 3 November
+ in the one edition -- 2024-2025 -- where 2 November falls on a
+ Sunday; verified directly against each PDF's own text, not assumed
+ from RG 96 alone). *)
+ let praef_of_date rows d = List.find_map (fun r -> if String.equal r.date d then r.praef else None) rows in
+ let praef_probes =
+ match base with
+ | "lms-ordo-2023-2024.sexp" ->
+ [ ("2023-12-25", "Pr of the Nativity", "the Nativity of Our Lord, I class, RG 484(a)");
+ ("2024-06-29", "Pr of the Apostles", "SS Peter & Paul, I class, RG 497");
+ ("2024-11-02", "Pr of the Dead", "All Souls' Day, I class, RG 499") ]
+ | "lms-ordo-2024-2025.sexp" ->
+ [ ("2024-12-25", "Pr of the Nativity", "the Nativity of Our Lord, I class, RG 484(a)");
+ ("2025-06-29", "Pr of the Apostles", "SS Peter & Paul, I class, RG 497");
+ ( "2025-11-03", "Pr of the Dead",
+ "All Souls' Day, I class, RG 499 -- TRANSFERRED from 2 November, a Sunday in this edition's own calendar" )
+ ]
+ | "lms-ordo-2025-2026.sexp" ->
+ [ ("2025-12-25", "Pr of the Nativity", "the Nativity of Our Lord, I class, RG 484(a)");
+ ("2026-06-29", "Pr of the Apostles", "SS Peter & Paul, I class, RG 497");
+ ("2026-11-02", "Pr of the Dead", "All Souls' Day, I class, RG 499") ]
+ | _ -> die "no praef probe list for %s" base
+ in
+ List.iter
+ (fun (d, expected_prefix, desc) ->
+ match praef_of_date rows d with
+ | Some got when has_prefix ~prefix:expected_prefix got -> ()
+ | Some got ->
+ die "praef characterisation probe FAILED: %s (%s) -- expected prefix %S, extracted %S" d desc
+ expected_prefix got
+ | None -> die "praef characterisation probe FAILED: %s (%s) -- no praef extracted for this date at all" d desc)
+ praef_probes;
+ Printf.printf "extract_lms_ordo: all %d praef characterisation probes confirmed for %s\n" (List.length praef_probes)
+ edition_label;
+ let praef_probe_lines =
+ String.concat "\n"
+ (List.map (fun (d, expected_prefix, desc) -> Printf.sprintf "; %s (%s) -> %S" d desc expected_prefix) praef_probes)
+ in
+ let missing_praef_count = List.length missing_praef in
let probe_lines =
String.concat "\n"
(List.map
@@ -529,6 +652,62 @@ let () =
; and the "When may I say...?"/Appendix tail are never scanned at
; all (the extractor's own start/stop markers bound it to the Ordo
; proper).
+;
+; 5. THE "Pr of" (PREFACE) COLUMN (Preface-witnesses task, 2026-08-23) --
+; [row.praef], the RAW text from the FIRST "Pr of ..." / "Common Pr"
+; match in the universal block's own reading order (the same
+; FIRST-in-reading-order discipline finding 1 above already
+; establishes for Gl/Cr). CAPTURED, NOT CLASSIFIED by this tool --
+; classification into {!Colitur_kernel.Preface.t} and the comparison
+; against {!Rite_ef.Rubrics_ef.preface} both happen test-side
+; (test/test_lms_ordo.ml's own [classify_praef]).
+;
+; CHARACTERISATION FINDING, non-negotiable per the same discipline as
+; finding 2: this Ordo prints OPTION LISTS ("Pr of X or Pr of Y or
+; Common Pr"), not always a single value -- and several of the named
+; options ("Martyrs", "All Saints and Patron Saints" [almost always
+; diocesan -- a Patronal feast's own preface -- but checked directly to
+; occur ONCE in THIS edition's own UNIVERSAL block too, All Saints' Day
+; itself, 1 November, paired with "or Common Pr"/"or Pr of the
+; Trinity" depending on whether 1 November falls on a Sunday that
+; edition, exactly matching {!Rite_ef.Rubrics_ef.preface}'s own RG
+; 494(b) Sunday-gated Trinity fallback either way -- see that
+; function's own citation], "the Dedication of a Church", "the Most Holy
+; Sacrament" [Corpus Christi's own universal entry], "St John the
+; Baptist", "the Angels") are genuinely NOT among the fourteen the
+; 1962 Missale Romanum's own RG 484-497 enumerate (docs/research/LT.txt,
+; lines 3936-4020, checked directly: no "de Martyribus"/"de Angelis"/
+; "de Dedicatione"/"de Ss.mo Sacramento"/"de S. Ioanne Baptista" clause
+; exists anywhere in 482-499). Every one of these extras, in every
+; instance found in this edition's UNIVERSAL block, is printed
+; ALONGSIDE a genuine RG 482 answer (most often "or Common Pr"; on 24
+; March-shaped days, "or Pr of Lent", the live RG 486(b) de-Tempore
+; answer) -- never as the row's OWN AND ONLY option. This is why the
+; test-side comparison checks colitur's single computed answer is a
+; MEMBER of this row's own parsed option set (dropping any unmapped
+; extra name from that set first), not string equality against
+; whichever option happens to print first -- the same "capture, don't
+; force a match invented at extraction time" discipline
+; tools/extract_fiuv_ordo.ml's own [row.praef] doc comment already
+; states for a different publisher's format.
+;
+; OPPOSITE-PREDICTION PROBES against Rite_ef.Rubrics_ef.preface (RG
+; 482-499), the SAME discipline as finding 2, applied to this new
+; column -- RE-CHECKED BY THIS TOOL RUN before this header was
+; written; a failed probe is a hard [die]:
+%s
+; All %d predictions confirmed -- this column, too, is discriminating,
+; not a constant, across three different RG-cited outcomes (Nativity/
+; Apostles/Requiem).
+;
+; COVERAGE: %d of %d rows have no "Pr of"/"Common Pr" text found at
+; all -- checked directly against this window's own Good Friday
+; (Easter-2): its block carries NO Gl/Cr/Pr line whatsoever in this
+; source (the 1955-restored Holy Week's own liturgical action has no
+; Mass that day at all -- the SAME structural fact
+; {!test_creed_coverage}'s own [window_good_fridays] already asserts
+; for Gl/Cr), so [praef] shares that single expected gap rather than
+; having a coverage gap of its own.
|}
dest edition_label isbn copyright_year pdf_meta url_note url pdf_base pdf_base pdf_sha (today ()) pdf_base
pdf_base dest
@@ -537,6 +716,7 @@ let () =
(match List.rev rows with r :: _ -> r.date | [] -> "?")
window_open_desc probe_lines (List.length probes) with_variant (List.length rows)
(Printf.sprintf "%.1f" (100.0 *. float_of_int with_variant /. float_of_int (List.length rows)))
+ praef_probe_lines (List.length praef_probes) missing_praef_count (List.length rows)
in
let body = Sexplib.Sexp.to_string_hum ~indent:2 (sexp_of_list sexp_of_row rows) in
let oc = open_out dest in