diff options
| -rw-r--r-- | bin/main.ml | 62 | ||||
| -rw-r--r-- | data/dune | 23 | ||||
| -rw-r--r-- | test/cli.t | 20 |
3 files changed, 99 insertions, 6 deletions
diff --git a/bin/main.ml b/bin/main.ml index 01cdbf8..ca89a12 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -74,12 +74,62 @@ let temporal_report y = _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" + RESOLVED: the "known limitation" this comment used to end on -- that a + `dune install`-style deployment (executable copied to a prefix with no + adjacent _build/default/data/) had no resolution strategy, and would exit 2 + unable to find sanctoral.sexp -- is now handled by probing candidates in + order rather than computing one path and hoping. data/dune installs the + four runtime files into <prefix>/share/colitur/ef/. + + Two layouts are probed, and one override short-circuits both: + + [COLITUR_DATA_DIR], when set and non-blank -- an explicit override. It + NEVER falls through: if it is set and does not contain the data, that is + an error naming the directory, not a reason to quietly use different + data. A packager or operator who names a directory has stated an + intent, and silently calendaring off some other copy because theirs was + wrong is precisely the silent substitution this project refuses + everywhere else (CLAUDE.md's first binding decision: divergence is + flagged LOUDLY, never silently swallowed). Getting this wrong is not + hypothetical -- the first version of this function did fall through, and + a deliberately bogus COLITUR_DATA_DIR produced a full, plausible, + entirely un-flagged year off the build tree's data. + + Otherwise, in order: + 1. <exedir>/../share/colitur/ef -- the INSTALLED layout, from an opam or + `dune install` prefix where the binary sits at <prefix>/bin/colitur. + data/dune puts the four runtime files there. + 2. <exedir>/../data/ef -- the BUILD TREE, which is what `dune exec` and + the cram tests use. + + A candidate is accepted only if sanctoral.sexp is actually readable inside + it, not merely because the directory exists: an empty or half-populated + share/colitur/ef (a failed install, a partially removed package) falls + through to a working build tree rather than shadowing it and then failing + at load time with a confusing per-file error. Verified by simulation, not + assumed. + + Environment reads are fine HERE and only here: this is bin/, not the + kernel, whose contract forbids them (CLAUDE.md, "Kernel is total & + deterministic: no wall-clock, randomness, or environment reads"). Nothing + below the CLI ever learns where the data came from -- the loaders take a + path. *) +let data_dir () = + let has_data d = Sys.file_exists (Filename.concat d "sanctoral.sexp") in + let prefix = Filename.dirname (Filename.dirname Sys.executable_name) in + let installed = List.fold_left Filename.concat prefix [ "share"; "colitur"; "ef" ] in + let build_tree = Filename.concat prefix (Filename.concat "data" "ef") in + match Sys.getenv_opt "COLITUR_DATA_DIR" with + | Some d when String.trim d <> "" -> + if has_data d then d + else begin + Printf.eprintf + "colitur: COLITUR_DATA_DIR is set to %s, which contains no sanctoral.sexp\n\ + colitur: refusing to fall back to another data directory -- unset it, or point it at one\n" + d; + exit 2 + end + | _ -> if has_data installed then installed else build_tree (* Loads the universal sanctoral layer and applies the one hand-authored overlay over it (data/ef/adjustments.sexp -- see that file's own header): diff --git a/data/dune b/data/dune new file mode 100644 index 0000000..07e5307 --- /dev/null +++ b/data/dune @@ -0,0 +1,23 @@ +; The four RUNTIME data files, installed into <prefix>/share/colitur/ef/ so a +; `dune install`ed / opam-installed `colitur` can find them (bin/main.ml's own +; [data_dir], which probes the installed layout before the build-tree one). +; +; Deliberately NOT the two allow-lists (expected-divergences.sexp, +; expected-divergences-missalemeum.sexp): those are test fixtures describing +; where colitur and a comparison oracle disagree, read only by +; test_differential.ml / test_oracle.ml, and are meaningless to a running +; `colitur`. Installing them would ship an assertion about lectio's and +; missalemeum's behaviour as though it were calendar data. +; +; If a new runtime data file is ever added, it must be added HERE too -- there +; is no glob, on purpose: a glob would silently start shipping the allow-lists +; the moment someone renamed one, and an omission here fails loudly at startup +; (exit 2, naming the missing file) rather than silently degrading. +(install + (section share) + (package colitur) + (files + (ef/sanctoral.sexp as ef/sanctoral.sexp) + (ef/adjustments.sexp as ef/adjustments.sexp) + (ef/lectionary.sexp as ef/lectionary.sexp) + (ef/commons.sexp as ef/commons.sexp))) @@ -191,3 +191,23 @@ A year outside the supported domain is rejected (exit 2): $ colitur readings 1000 colitur: year 1000 out of range 1583..9999 [2] + +An explicit COLITUR_DATA_DIR that does not hold the data is an error (exit 2), +never a silent fall-back to some other copy. A packager or operator who names +a directory has stated an intent; quietly calendaring off a different one +because theirs was wrong is the silent substitution this project refuses +everywhere else. The first version of this resolution DID fall through, and a +deliberately bogus value produced a full, plausible, entirely un-flagged year: + + $ COLITUR_DATA_DIR=/nonexistent/xyz colitur readings 2026 + colitur: COLITUR_DATA_DIR is set to /nonexistent/xyz, which contains no sanctoral.sexp + colitur: refusing to fall back to another data directory -- unset it, or point it at one + [2] + +An empty or half-populated installed directory is a different case and DOES +fall through to the build tree: nobody stated an intent there, and a failed or +partially removed install must not shadow a working tree and then fail later +with a confusing per-file error. (Exercised directly in the task, by creating +_build/default/share/colitur/ef and confirming the year still resolves; not +reproduced here because the cram sandbox's own exe path makes the layout +awkward to stage without asserting on dune internals.) |
