Wire the Matins psalmody-override category tier into real resolution
Deploy / deploy (push) Successful in 1m5s
Deploy / deploy (push) Successful in 1m5s
getMatinsPsalmodyOverride is now a genuine 3-tier lookup: proper (saints/) -> Common (categories/, joined via the winner's own SaintRecord.common) -> the plain ferial fallback, reversing the earlier "category files are documentation-only" decision now that two categories are trusted. Category files gain an `aliases` field; common-of-a-martyr.yml lists common-of-a-martyr-bishop, since the reference engine's own Matins psalmody redirects that Common outright to plain Martyr — so its three Duplex+ saints (Ignatius, Callistus, Boniface, none with proper text of his own) get real Common-tier content with no new authoring. tests/hours/matins.test.ts's old ferial-fallback proof (St. Ignatius) is rewritten as a Common-tier proof; a fresh ferial-fallback proof (St. John Bosco, an unauthored Confessor category) replaces it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1842,6 +1842,30 @@ Confessor-Bishop, Virgin, Doctor, etc. — the largest population, per the earli
|
||||
each needs its own category file (one live `prayMatutinum` query against a no-proper-antiphon
|
||||
saint) before any of its own saints' proper antiphons can be authored against it.
|
||||
|
||||
### Matins psalmody overrides — category tier wired in as a real fallback (2026-08)
|
||||
|
||||
Reversed the "category files are documentation-only" decision above now that two categories
|
||||
are trusted: `getMatinsPsalmodyOverride` is now a real three-tier lookup — **proper**
|
||||
(`saints/*.yml`) → **Common** (`categories/*.yml`, joined via the winner's own
|
||||
`SaintRecord.common` through a new `getSaintRecord` import) → **ferial fallback**
|
||||
(`matins.ts`'s own plain weekday table, unchanged, now genuinely the last resort rather than
|
||||
the default for every unauthored saint). This is "prefer real text over missing" applied one
|
||||
tier further, once the tier itself had proof behind it. `MatinsPsalmodyOverride` and
|
||||
`MatinsPsalmodyCategory` now share a `MatinsPsalmodyScheme` base (`nocturn1/2/3`) so the lookup
|
||||
can return either without a cast.
|
||||
|
||||
Category files gained an `aliases` field: `common-of-a-martyr.yml` lists
|
||||
`common-of-a-martyr-bishop`, so all three of its Duplex+ saints (Ignatius, Callistus, Boniface —
|
||||
confirmed none has proper text of his own) now resolve to real, verified Common psalmody instead
|
||||
of the ferial table, with zero new authoring.
|
||||
|
||||
`tests/hours/matins.test.ts`'s old "falls back to ferial" case (St. Ignatius, 2029-02-01) is
|
||||
rewritten as a Common-tier proof (his Nocturn 2 correctly using the Common default's Ps 64, not
|
||||
Lawrence's own proper Ps 16); a fresh genuine ferial-fallback proof was added (St. John Bosco,
|
||||
Common of a Confessor — no category authored yet for that Common — 2026-01-31, confirmed a
|
||||
clean Saturday with no Sunday collision). `npm test` (375 tests), `npm run build`, and
|
||||
`tsc --noEmit` all pass.
|
||||
|
||||
### Calendar precedence: a tie against an octave's own elevated closing day now favors the octave (2026-08)
|
||||
|
||||
Real bug found and fixed, same session as the Matins psalmody fix above, discovered while
|
||||
|
||||
@@ -9,19 +9,23 @@
|
||||
# this same Common-of-a-Martyr file -- the two categories share one
|
||||
# scheme, not two.
|
||||
#
|
||||
# This file is documentation/reference content only, not yet wired into
|
||||
# `hours/matins-psalmody-overrides.ts`'s resolution -- per this project's
|
||||
# "stay on the ferial fallback until proper antiphon text is authored
|
||||
# per-saint" decision (2026-08), a saint with no entry under
|
||||
# matins-psalmody-overrides/saints/ still falls all the way back to the
|
||||
# plain ferial weekday table, never to this generic Common text. Recorded
|
||||
# here so the next Common-of-a-Martyr(-Bishop) saint's own proper file
|
||||
# doesn't have to re-derive/re-verify these same shared psalm numbers and
|
||||
# versicles from scratch, and as the reference point for what "differs
|
||||
# from Common" means when authoring one (see
|
||||
# `aliases` registers `common-of-a-martyr-bishop` under this same scheme
|
||||
# per the redirect confirmed above, rather than duplicating this file --
|
||||
# checked all 3 Duplex+ Common-of-a-Martyr-Bishop saints on the calendar
|
||||
# (Ignatius, Callistus I, Boniface of Mainz) and none has a proper
|
||||
# antiphon of his own, so this alias is how each of them gets real
|
||||
# psalmody at all (`hours/matins-psalmody-overrides.ts`'s tiered
|
||||
# lookup falls through proper -> Common -> ferial; this is their Common
|
||||
# tier).
|
||||
#
|
||||
# Recorded so the next Common-of-a-Martyr(-Bishop) saint's own proper
|
||||
# file doesn't have to re-derive/re-verify these same shared psalm
|
||||
# numbers and versicles from scratch, and as the reference point for
|
||||
# what "differs from Common" means when authoring one (see
|
||||
# matins-psalmody-overrides/saints/st-lawrence.yml's own note on its one
|
||||
# genuine deviation, Ps 16 vs this file's Ps 64 in Nocturn 2).
|
||||
id: common-of-a-martyr
|
||||
aliases: [common-of-a-martyr-bishop]
|
||||
nocturn1:
|
||||
groups:
|
||||
- psalms: [1]
|
||||
|
||||
@@ -10,11 +10,6 @@
|
||||
# pattern already established for Common-of-a-Martyr (see that category
|
||||
# file's own doc comment).
|
||||
#
|
||||
# Reference/documentation content only, not wired into
|
||||
# `hours/matins-psalmody-overrides.ts`'s resolution -- see
|
||||
# common-of-a-martyr.yml's own doc comment for why (the "stay on the
|
||||
# ferial fallback until proper antiphon text is authored per-saint"
|
||||
# decision, 2026-08).
|
||||
id: common-of-an-apostle
|
||||
nocturn1:
|
||||
groups:
|
||||
|
||||
@@ -1,31 +1,41 @@
|
||||
import { getSaintRecord } from '../calendar/feasts';
|
||||
|
||||
/** Matins' own analogue of hours/lauds-psalmody-overrides.ts. Two data
|
||||
* stores, both keyed by id:
|
||||
* stores, both keyed by id, consulted in a three-tier fallback by
|
||||
* `getMatinsPsalmodyOverride` below (assumes the caller has already
|
||||
* gated on rank — this module is only ever consulted for a Sunday/
|
||||
* Duplex+ three-nocturn day, never a plain ferial one):
|
||||
*
|
||||
* - `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.
|
||||
* 1. **Proper** — `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 below,
|
||||
* 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.
|
||||
* 2. **Common** — `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. A category file may declare
|
||||
* `aliases` for a second `SaintRecord.common` id that shares its
|
||||
* exact scheme (e.g. Common-of-a-Martyr-Bishop's own Matins
|
||||
* psalmody redirects outright to Common-of-a-Martyr in the reference
|
||||
* engine — one scheme, not two — so `common-of-a-martyr.yml` lists
|
||||
* `common-of-a-martyr-bishop` as an alias rather than duplicating the
|
||||
* file).
|
||||
* 3. **Ferial fallback** — no entry in either store yet; `matins.ts`'s
|
||||
* own `ferialPsalmodyThreeNocturns`, redistributed into 3 nocturns.
|
||||
* This is genuinely a stand-in (the plain weekday table, not this
|
||||
* saint's or even this Common's own text) and is expected to shrink
|
||||
* over time as more categories get authored, not as a permanent third
|
||||
* class of content.
|
||||
*
|
||||
* `id` is looked up against `hours/resolve-common.ts`'s
|
||||
* `resolveOfficeWinner(day).id` when it's sanctoral, exactly like every
|
||||
@@ -45,18 +55,18 @@ export interface MatinsPsalmodyNocturn {
|
||||
antiphon?: Partial<Record<string, string>>;
|
||||
versicle: { v: Partial<Record<string, string>>; r: Partial<Record<string, string>> };
|
||||
}
|
||||
export interface MatinsPsalmodyOverride {
|
||||
id: string;
|
||||
category?: string;
|
||||
export interface MatinsPsalmodyScheme {
|
||||
nocturn1: MatinsPsalmodyNocturn;
|
||||
nocturn2: MatinsPsalmodyNocturn;
|
||||
nocturn3: MatinsPsalmodyNocturn;
|
||||
}
|
||||
export interface MatinsPsalmodyCategory {
|
||||
export interface MatinsPsalmodyOverride extends MatinsPsalmodyScheme {
|
||||
id: string;
|
||||
nocturn1: MatinsPsalmodyNocturn;
|
||||
nocturn2: MatinsPsalmodyNocturn;
|
||||
nocturn3: MatinsPsalmodyNocturn;
|
||||
category?: string;
|
||||
}
|
||||
export interface MatinsPsalmodyCategory extends MatinsPsalmodyScheme {
|
||||
id: string;
|
||||
aliases?: string[];
|
||||
}
|
||||
|
||||
const saintModules = import.meta.glob<{ default: MatinsPsalmodyOverride }>(
|
||||
@@ -76,12 +86,12 @@ for (const mod of Object.values(saintModules)) {
|
||||
const categories = new Map<string, MatinsPsalmodyCategory>();
|
||||
for (const mod of Object.values(categoryModules)) {
|
||||
categories.set(mod.default.id, mod.default);
|
||||
for (const alias of mod.default.aliases ?? []) {
|
||||
categories.set(alias, 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;
|
||||
export function getMatinsPsalmodyOverride(id: string | undefined): MatinsPsalmodyScheme | undefined {
|
||||
if (!id) return undefined;
|
||||
return overrides.get(id) ?? categories.get(getSaintRecord(id)?.common ?? '');
|
||||
}
|
||||
|
||||
+6
-4
@@ -315,10 +315,12 @@ export function resolveOrdo(date: string): ResolvedOrdo {
|
||||
parts.push({ kind: 'te-deum', text: resolveCommon('te-deum') });
|
||||
} else if (threeNocturns) {
|
||||
// A Duplex+ weekday feast — genuinely different psalmody from a real
|
||||
// Sunday's (see hours/matins-psalmody-overrides.ts's own doc
|
||||
// comment), not the same content reused. Uses that saint's own
|
||||
// Common-category override when authored, falling back to the plain
|
||||
// ferial weekday table (redistributed into 3 nocturns) otherwise.
|
||||
// Sunday's, not the same content reused. getMatinsPsalmodyOverride
|
||||
// itself does the proper -> Common -> (nothing) tiering; only the
|
||||
// final "nothing authored at all" tier — the plain ferial weekday
|
||||
// table, redistributed into 3 nocturns — lives here (see
|
||||
// hours/matins-psalmody-overrides.ts's own doc comment for the full
|
||||
// three-tier picture).
|
||||
const override = winner.kind === 'sanctoral' ? getMatinsPsalmodyOverride(winner.id) : undefined;
|
||||
if (override) {
|
||||
parts.push(...sundayPsalmNocturn(override.nocturn1, day));
|
||||
|
||||
@@ -167,18 +167,44 @@ describe('resolveOrdo("matins", ...) Duplex+ weekday-feast (3-nocturn, non-Sunda
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to the plain ferial weekday table, redistributed into 3 nocturns, for a Duplex+ weekday winner with no override authored", () => {
|
||||
it("falls through to its Common category's own generic psalmody for a Duplex+ weekday winner with no proper antiphon authored", () => {
|
||||
// St. Ignatius of Antioch (Duplex, Common of a Martyr-Bishop, no
|
||||
// matins-psalmody-overrides entry authored) -- Feb 1 2029, confirmed
|
||||
// clean (no Sunday collision) per his own saint-file comment.
|
||||
const fallback = resolveOrdo('matins', '2029-02-01');
|
||||
// proper antiphon of his own) -- Feb 1 2029, confirmed clean (no
|
||||
// Sunday collision) per his own saint-file comment.
|
||||
// getMatinsPsalmodyOverride's Common tier resolves
|
||||
// common-of-a-martyr-bishop via common-of-a-martyr.yml's own
|
||||
// `aliases` (the reference engine's own Matins psalmody for
|
||||
// Martyr-Bishop redirects outright to plain Martyr) -- real,
|
||||
// verified Common text, not the plain ferial table.
|
||||
const common = resolveOrdo('matins', '2029-02-01');
|
||||
const psalms = common.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
||||
// Same Nocturn 1 as St. Lawrence's own (1,2,4,5,8,10), but Nocturn 2
|
||||
// uses the Common default's Ps 64, not Lawrence's own proper Ps 16.
|
||||
expect(psalms).toEqual([3, 94, 1, 2, 4, 5, 8, 10, 14, 20, 23, 63, 64, 91]);
|
||||
expect(common.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
||||
expect(common.parts.some((p) => p.kind === 'canticle')).toBe(true);
|
||||
|
||||
const psalmParts = common.parts.filter((p) => p.kind === 'psalm') as { psalmNumber: number; antiphon?: { status: Record<string, string> } }[];
|
||||
const nocturnPsalms = psalmParts.filter((p) => ![3, 94].includes(p.psalmNumber));
|
||||
// The generic Common antiphon text is real and verified, even though
|
||||
// it is not proper to Ignatius himself.
|
||||
for (const p of nocturnPsalms) {
|
||||
expect(p.antiphon?.status.la).toBe('verified');
|
||||
expect(p.antiphon?.status.en).toBe('verified');
|
||||
}
|
||||
});
|
||||
|
||||
it('falls all the way back to the plain ferial weekday table, redistributed into 3 nocturns, when neither a proper nor a Common entry is authored', () => {
|
||||
// St. John Bosco (Duplex, Common of a Confessor -- no
|
||||
// matins-psalmody-overrides category authored yet for either tier)
|
||||
// -- 2026-01-31, a Saturday, confirmed clean (no Sunday collision).
|
||||
const fallback = resolveOrdo('matins', '2026-01-31');
|
||||
const psalms = fallback.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
||||
// Ps 3 + 94, then Thursday's own 9-psalm ferial table (psalter-
|
||||
// distribution.yml), not St. Lawrence's proper psalmody and not the
|
||||
// Sunday scheme either.
|
||||
// Ps 3 + 94, then Saturday's own 9-psalm ferial table
|
||||
// (psalter-distribution.yml), not any Common-of-a-Confessor scheme.
|
||||
expect(psalms[0]).toBe(3);
|
||||
expect(psalms[1]).toBe(94);
|
||||
expect(psalms.slice(2)).toEqual(getPsalmsFor('matins', 'thursday').map((r) => r.number));
|
||||
expect(psalms.slice(2)).toEqual(getPsalmsFor('matins', 'saturday').map((r) => r.number));
|
||||
expect(fallback.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
||||
expect(fallback.parts.some((p) => p.kind === 'canticle')).toBe(false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user