diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-12 11:38:35 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-12 11:38:35 +0200 |
| commit | e19adb9c7ea369cafe76e7eff50e9248a4a954d0 (patch) | |
| tree | 57824e121606286ebdf8fff68439986dd3300eae /bin/main.ml | |
| parent | 0506388da160a15ceddb0cea1a697d737103d5c4 (diff) | |
| parent | 36df2bd0af9a555a09334e14b0d89118be1dbbc0 (diff) | |
| download | colitur-e19adb9c7ea369cafe76e7eff50e9248a4a954d0.tar.gz colitur-e19adb9c7ea369cafe76e7eff50e9248a4a954d0.zip | |
Merge branch 'ef-plan3': Plan 3, the EF resolution engine
Adds the rite-parameterised Precedence resolver, Liturgical_day, Rite,
and Calendar (year as the primitive, because transfers need whole-year
knowledge), the full EF precedence ruleset (RG 91's 28-entry table,
occurrence RG 92-95, commemorations RG 108-111, transfers RG 96-98),
322 bootstrapped sanctoral entries, colitur day <year>, and validation
layers 3-5.
Layer 3 diffs 16801 days against lectio; layer 4 diffs 730 days against
missalemeum; layer 5 pins ~30 dates on the known-tricky years. Both
comparison layers carry cited allow-lists that name the governing RG
paragraph and which engine is right.
The oracle layer earned its place immediately: Holy Thursday was violet
in colitur and lectio alike, because colitur's data was bootstrapped
from lectio and both carried the same error. Only an independent source
could see it. RG 128(b) and RG 122 name it white.
Diffstat (limited to 'bin/main.ml')
| -rw-r--r-- | bin/main.ml | 137 |
1 files changed, 136 insertions, 1 deletions
diff --git a/bin/main.ml b/bin/main.ml index 896a6bd..53cae87 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -41,8 +41,142 @@ let temporal_report y = d := D.add_days !d 1 done +(* Task 11: the fully resolved EF calendar (temporal AND sanctoral, + occurrence and transfers applied), one line per civil-year day -- + "YYYY-MM-DD weekday season week slug rank colour [+commemoration-slug]...". + [temporal_report] above only ever showed the temporal cycle in isolation + ([Rite_ef.Temporal_ef.temporal] directly, no sanctoral layer, no + [Precedence] contest); this is the first CLI path that runs every piece + Plan 3 built -- [Colitur_kernel.Layer], [Overlay], [Precedence_ef], + [Calendar] -- against real data. *) + +(* [data/ef/sanctoral.sexp] and [data/ef/adjustments.sexp] are located + relative to the BUILD TREE, not the process's own cwd: cwd varies with + how the binary is invoked (a user's shell for `dune exec colitur --`, a + dune cram test's own sandboxed temp directory for `test/cli.t`) and + nothing in this project's build pins it to the repository root. A + build-time constant substituted via dune's [%{workspace_root}] was tried + first and rejected: it is resolved RELATIVE TO THE BUILD ACTION'S OWN + directory (empirically "." here, not an absolute path -- dune keeps + build actions relocatable), so it silently reproduces the same + cwd-dependence this is trying to eliminate, just baked in at build time + instead of read at run time; confirmed by the resulting `colitur day` + failing to find its own data outside the exact directory the build + happened to run in. + + [Sys.executable_name] does not have that problem -- on Linux it resolves + through /proc/self/exe, which the kernel always reports as the + executable's own canonical absolute path, even when the process was + launched through a symlink (verified against dune's own cram sandbox, + which places exactly such a symlink; see the task report). dune's default + ("no [(sandbox ...)] declared") build context mirrors the ENTIRE source + tree under _build/default/, unconditionally, so climbing from + _build/default/bin/main.exe up two directories and back down into data/ + always finds both files, regardless of the caller's own cwd. + + Known limitation, not yet exercised by this project: a `dune install`- + style deployment (executable copied to a prefix with no adjacent _build/ + default/data/) would need a different resolution strategy; there is no + install story yet (README.md: `dune exec` only), so this is not a + regression against anything this project currently supports. *) +let data_dir () = Filename.dirname (Filename.dirname Sys.executable_name) ^ "/data/ef" + +(* Loads the universal sanctoral layer and applies the one hand-authored + overlay over it (data/ef/adjustments.sexp -- see that file's own header): + [Overlay.apply]'s diagnostics are never silently dropped (Overlay.mli), + so any that come back -- expected to be none in the committed data; see + the overlay file's own comment on when one WOULD fire -- are printed to + stderr, loudly, without aborting the run. *) +let load_ef_layer () = + let dir = data_dir () in + let sanctoral_path = Filename.concat dir "sanctoral.sexp" in + let adjustments_path = Filename.concat dir "adjustments.sexp" in + match Colitur_kernel.Layer.load Rite_ef.Vocab_ef.rank_of_sexp sanctoral_path with + | Error e -> Error (Printf.sprintf "failed to load %s: %s" sanctoral_path e) + | Ok layer -> ( + match Colitur_kernel.Overlay.load Rite_ef.Vocab_ef.rank_of_sexp adjustments_path with + | Error e -> Error (Printf.sprintf "failed to load %s: %s" adjustments_path e) + | Ok overlay -> + let layer, diagnostics = Colitur_kernel.Overlay.apply layer overlay in + List.iter + (fun d -> Printf.eprintf "colitur: %s\n" (Colitur_kernel.Overlay.diagnostic_to_string d)) + diagnostics; + Ok layer) + +let day_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) + = + let t = d.Colitur_kernel.Liturgical_day.temporal in + let cel = d.Colitur_kernel.Liturgical_day.observed in + let week = + match t.Colitur_kernel.Temporal.week with Some n -> string_of_int n | None -> "-" + in + let commemoration_suffix (c, _) = + " +" ^ Colitur_kernel.Slug.to_string c.Colitur_kernel.Celebration.slug + in + let commemorations = + String.concat "" (List.map commemoration_suffix d.Colitur_kernel.Liturgical_day.commemorations) + in + Printf.printf "%s %s %s %s %s %s %s%s\n" (D.to_iso8601 d.Colitur_kernel.Liturgical_day.date) + (D.weekday_to_string t.Colitur_kernel.Temporal.weekday) + (Rite_ef.Vocab_ef.season_to_string t.Colitur_kernel.Temporal.season) + week + (Colitur_kernel.Slug.to_string cel.Colitur_kernel.Celebration.slug) + (Rite_ef.Vocab_ef.rank_to_string cel.Colitur_kernel.Celebration.rank) + (Colitur_kernel.Colour.to_string cel.Colitur_kernel.Celebration.colour) + commemorations + +(* One civil year, Jan 1 - Dec 31, matching [temporal_report]'s own scan -- + NOT one liturgical year: [Colitur_kernel.Calendar.year] resolves a single + Advent-anchored liturgical year, which straddles two civil years, so a + civil year's worth of output needs the tail of the liturgical year that + opened the PREVIOUS civil year (covers roughly 1 Jan - 28 Nov) plus the + liturgical year that opens within this one (roughly 29 Nov - 31 Dec). + Both are computed once each -- not once per day via [Calendar.day], which + would recompute the whole (~365-day) placement pass up to 365 times over + for the days sharing one liturgical year (calendar.mli's own "pays it + once" cost model assumes exactly this usage: call [year], not [day] in a + loop). *) +let day_report y = + match load_ef_layer () with + | Error msg -> + Printf.eprintf "colitur: %s\n" msg; + exit 2 + | Ok layer -> + let module Cal = Colitur_kernel.Calendar in + let by_rata : (int, (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) Hashtbl.t = + Hashtbl.create 400 + in + let index days = + Array.iter + (fun (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) -> + Hashtbl.replace by_rata (D.to_rata d.Colitur_kernel.Liturgical_day.date) d) + days + in + index (Cal.year Rite_ef.context layer (y - 1)); + index (Cal.year Rite_ef.context layer y); + let jan1 = match D.make ~year:y ~month:1 ~day:1 with Ok t -> t | Error e -> failwith e in + let dec31 = match D.make ~year:y ~month:12 ~day:31 with Ok t -> t | Error e -> failwith e in + let d = ref jan1 in + while D.compare !d dec31 <= 0 do + (match Hashtbl.find_opt by_rata (D.to_rata !d) with + | Some day -> day_line day + | None -> + (* Unreachable for any [y] in 1583..9999: the two indexed + liturgical years jointly cover [year_start (y-1), year_start + (y+1)), which contains all of civil year [y] + (calendar.mli). Not a [failwith] -- an out-of-domain [d] + inside this loop is impossible by construction (jan1/dec31 + are themselves validated in range, and [add_days] only ever + advances within the same civil year here) -- but a silent + skip would violate the same "never silently dropped" + standard the kernel holds itself to, so a gap surfaces + loudly on stderr rather than as a quietly short year. *) + Printf.eprintf "colitur: internal error: no resolved day for %s\n" (D.to_iso8601 !d)); + d := D.add_days !d 1 + done + let usage () = - prerr_endline "colitur: usage: colitur easter <year> | colitur temporal <year>"; + prerr_endline "colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year>"; exit 2 let with_year ys f = @@ -57,4 +191,5 @@ let () = match Sys.argv with | [| _; "easter"; ys |] -> with_year ys easter_report | [| _; "temporal"; ys |] -> with_year ys temporal_report + | [| _; "day"; ys |] -> with_year ys day_report | _ -> usage () |
