From 2bac28d36c604bb7c0a15131312d63d1ea1a186f Mon Sep 17 00:00:00 2001 From: Will Estes Date: Tue, 18 Aug 2026 06:42:48 -0400 Subject: [PATCH] Refactor Matins psalmody overrides to category-keyed data Split the store into categories/*.yml (generic Common-category psalm- number/versicle/canticle scheme) and saints/*.yml (per-saint proper antiphon overrides, unchanged shape) per the mechanism's own flagged next step. St. Lawrence's file moves into saints/ with a category cross-reference; resolution behavior (per-saint lookup, ferial fallback when unauthored) is unchanged. Co-Authored-By: Claude Sonnet 5 --- .../{ => saints}/st-lawrence.yml | 17 ++++ src/hours/matins-psalmody-overrides.ts | 78 ++++++++++++------- 2 files changed, 69 insertions(+), 26 deletions(-) rename src/data/hours/matins-psalmody-overrides/{ => saints}/st-lawrence.yml (85%) diff --git a/src/data/hours/matins-psalmody-overrides/st-lawrence.yml b/src/data/hours/matins-psalmody-overrides/saints/st-lawrence.yml similarity index 85% rename from src/data/hours/matins-psalmody-overrides/st-lawrence.yml rename to src/data/hours/matins-psalmody-overrides/saints/st-lawrence.yml index 6d16707..5c97d93 100644 --- a/src/data/hours/matins-psalmody-overrides/st-lawrence.yml +++ b/src/data/hours/matins-psalmody-overrides/saints/st-lawrence.yml @@ -29,7 +29,24 @@ # chapters authored for this: data/scripture/{sir-14,sir-15,jer-17,sir-31}.yml # (all sparse, only the cited verses -- same convention as sir-36.yml's # own Sunday-canticle transcription). +# +# `category: common-of-a-martyr` links this to +# data/hours/matins-psalmody-overrides/categories/common-of-a-martyr.yml, +# the generic Common-of-a-Martyr scheme authored from St. Ignatius of +# Antioch's own live-verified Matins (2029-02-01 -- his own file has no +# proper antiphon, "vide C2" i.e. entirely from Common, confirming his +# psalmody *is* the category default). Lawrence's own file stays fully +# self-contained (own psalm numbers + antiphons, not merged with the +# category file at load time) -- the category link is documentation/ +# cross-reference only, not a runtime merge, per this project's "stay on +# the ferial fallback until proper antiphon text is authored per-saint" +# decision (2026-08): a saint without an entry here still gets no +# override, never the category's generic antiphon text. Note one real +# divergence from the category default worth remembering: Lawrence's own +# Nocturn 2 has Ps 16 in the slot where the plain Common default (and +# Ignatius) has Ps 64 -- his own proper psalm selection, not a typo. id: st-lawrence +category: common-of-a-martyr nocturn1: groups: - psalms: [1] diff --git a/src/hours/matins-psalmody-overrides.ts b/src/hours/matins-psalmody-overrides.ts index 9552c04..40d1cf1 100644 --- a/src/hours/matins-psalmody-overrides.ts +++ b/src/hours/matins-psalmody-overrides.ts @@ -1,29 +1,35 @@ -/** Matins' own analogue of hours/lauds-psalmody-overrides.ts, but keyed - * by Common category rather than per-saint: live-verified (see - * data/hours/matins-psalmody-overrides/st-lawrence.yml's own header) - * that the psalm *numbers* for Nocturns 1-2 are shared across saints of - * the same Common category (a plain-Duplex Martyr and a plain-Duplex - * Martyr-Bishop share all but one of the same twelve psalm numbers) — - * only the antiphon text varies, proper to the individual saint when - * authored, generic Common-of-* text otherwise. Deliberately the - * opposite indexing choice from Lauds' own per-feast override (see that - * file's own doc comment) — Lauds' 5-antiphon pool is shared across - * every Duplex-majus+ feast regardless of category, so per-feast is the - * natural key there; Matins' pool varies *by* category, so per-category - * is the natural key here. +/** Matins' own analogue of hours/lauds-psalmody-overrides.ts. Two data + * stores, both keyed by id: + * + * - `data/hours/matins-psalmody-overrides/categories/*.yml` — the + * generic Common-of-* Matins psalmody (psalm numbers, versicles, + * canticle refs, and the plain Common antiphon text), one file per + * `SaintRecord.common` category. Live-verified against a saint with no + * proper antiphon of his own (so his own output *is* the category + * default) — see e.g. `categories/common-of-a-martyr.yml`'s own doc + * comment. Reference/documentation content only: not consulted by + * `getMatinsPsalmodyOverride` below, per this project's "stay on the + * ferial fallback until proper antiphon text is authored per-saint" + * decision (2026-08) — a saint without their own entry in `saints/` + * still falls all the way back to matins.ts's own plain ferial table, + * never to a category's generic antiphon text. Recorded so the next + * saint authored in an already-proven category doesn't have to + * re-derive/re-verify the same shared psalm numbers and versicles. + * + * - `data/hours/matins-psalmody-overrides/saints/*.yml` — real proper + * antiphon text for one specific saint (only authored where a saint + * genuinely has proper text of their own, confirmed against the live + * engine, not merely inherited Common text). Each carries its own + * `category` field cross-referencing the file above, but is otherwise + * fully self-contained (own psalm numbers, not merged with the + * category file at load time) — a saint's proper psalmody occasionally + * swaps one slot's psalm number from the category default too (see + * `saints/st-lawrence.yml`'s own note), so per-saint files stay + * authoritative rather than being computed as a diff. * * `id` is looked up against `hours/resolve-common.ts`'s * `resolveOfficeWinner(day).id` when it's sanctoral, exactly like every - * other per-feast/per-category content lookup in this app — but the - * `id` values populated so far are individual saint ids (St. Lawrence - * proper), not Common-category ids, since only the live-verified proof - * has been authored; extending this to a real Common-category key - * (`common-of-a-martyr`, etc., matching `SaintRecord.common`) with - * per-saint antiphon overrides layered on top is the deferred bulk- - * content work (see TODO.md) — not built yet, so a saint without their - * own entry here falls back to the plain ferial weekday table - * (matins.ts's own fallback), not to some other saint's proper - * antiphons. */ + * other per-feast/per-category content lookup in this app. */ export interface MatinsPsalmodyGroup { psalms: number[]; antiphon: Partial>; @@ -40,22 +46,42 @@ export interface MatinsPsalmodyNocturn { versicle: { v: Partial>; r: Partial> }; } export interface MatinsPsalmodyOverride { + id: string; + category?: string; + nocturn1: MatinsPsalmodyNocturn; + nocturn2: MatinsPsalmodyNocturn; + nocturn3: MatinsPsalmodyNocturn; +} +export interface MatinsPsalmodyCategory { id: string; nocturn1: MatinsPsalmodyNocturn; nocturn2: MatinsPsalmodyNocturn; nocturn3: MatinsPsalmodyNocturn; } -const modules = import.meta.glob<{ default: MatinsPsalmodyOverride }>( - '../data/hours/matins-psalmody-overrides/*.yml', +const saintModules = import.meta.glob<{ default: MatinsPsalmodyOverride }>( + '../data/hours/matins-psalmody-overrides/saints/*.yml', + { eager: true }, +); +const categoryModules = import.meta.glob<{ default: MatinsPsalmodyCategory }>( + '../data/hours/matins-psalmody-overrides/categories/*.yml', { eager: true }, ); const overrides = new Map(); -for (const mod of Object.values(modules)) { +for (const mod of Object.values(saintModules)) { overrides.set(mod.default.id, mod.default); } +const categories = new Map(); +for (const mod of Object.values(categoryModules)) { + categories.set(mod.default.id, mod.default); +} + export function getMatinsPsalmodyOverride(id: string | undefined): MatinsPsalmodyOverride | undefined { return id ? overrides.get(id) : undefined; } + +export function getMatinsPsalmodyCategory(id: string | undefined): MatinsPsalmodyCategory | undefined { + return id ? categories.get(id) : undefined; +}