ffaa797940
Deploy / deploy (push) Successful in 56s
Continues the 94-saint batch this collects pass unblocked. Raw Commune/*.txt fields don't reliably match what the engine actually renders per hour -- same lesson as the collects pass, confirmed again: no CommuneM/*.txt file defines explicit per-hour antiphon fields for any category this batch needed, and the already-authored common-of-a-confessor-bishop content (from the original 29-saint pass) doesn't match Commune/C4.txt's own raw [Ant 1] either, since it was live-query-sourced rather than read from the file directly. Fixed by querying web/cgi-bin/horas/officium.pl directly (local Perl, not a network fetch) for one representative saint per Common category, with every representative's title verified against an accent-insensitive name-stem match before its content is trusted -- caught a stale-date bug this way: St. Emerentiana's stored clean-year comment actually rendered St. Hilary of Poitiers's office that year. Cross-querying multiple saints per nominal SaintRecord.common category found the real liturgical groupings are coarser (Martyr-Bishop and Martyr; Abbot, Confessor-not-Bishop, and Confessor; Virgin-Martyr, Virgin, and Holy Woman all render identical minor-hour text) but not uniformly so (Pope-Martyrs render distinct allelúja-toned text from plain Several-Martyrs; the Dedication of the Basilicas of Ss. Peter and Paul draws from the Dedication Common, not its nominal common-of-an-apostle at all). Net: 7 new shared Common categories plus the pre-existing common-of-a-confessor-bishop (which also needed a new Benedictus-antiphon file of its own) cover 66 of the 94 saints. New mechanism: SaintRecord.benedictusCommon, with getBenedictusAntiphon falling back to it the same way getDayCollect already falls back to collectCommon -- no name-substitution needed here, since every Common category in current use renders name-free Benedictus-antiphon text. Also fixes a real bug this surfaced: getMinorHourOverrideId gated all Prime/Terce/Sext/None lookup behind the same duplex-majus+ threshold Lauds' own psalmody override uses, on the assumption that only a strong feast carries its own minor-hour content -- live-querying proved that wrong (a winning Semiduplex saint still renders its own antiphons, not the ferial default). Lauds' psalmody override itself stays majus+-gated correctly, since it's a much bigger per-saint commitment this batch doesn't add a Common fallback for. Ss. Ursula and Companions deliberately left without a minor-hour Common: her collect template has no other calendar member to verify a shared text against, and she can never be queried directly (a genuinely unwinnable stub) -- left honestly unauthored rather than guessed. The other 26 of the 94 (own proper collect, from the collects batch) still need their own individually-authored Benedictus antiphon, same shape as the original 109-saint pass -- not done in this pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
103 lines
5.1 KiB
TypeScript
103 lines
5.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { resolveDay } from '../../src/calendar';
|
|
import {
|
|
getDayCollect,
|
|
getBenedictusAntiphon,
|
|
resolveMinorHourAntiphon,
|
|
resolveMinorHourChapter,
|
|
splitNamedAntiphon,
|
|
} from '../../src/hours/resolve-common';
|
|
import type { LiturgicalDay } from '../../src/calendar/types';
|
|
|
|
describe('getDayCollect', () => {
|
|
it('resolves a real, verified collect for an ordinary Sunday', () => {
|
|
const day = resolveDay('2026-08-09'); // post-pentecost-11
|
|
const collect = getDayCollect(day);
|
|
expect(collect.status.la).toBe('verified');
|
|
expect(collect.status.en).toBe('verified');
|
|
expect(collect.text.en).toContain('Let us pray.');
|
|
});
|
|
|
|
it('resolves a real, verified collect for a ferial day via its governing Sunday', () => {
|
|
const day = resolveDay('2026-06-14'); // post-pentecost-03's Sunday
|
|
const collect = getDayCollect(day);
|
|
expect(collect.status.la).toBe('verified');
|
|
});
|
|
|
|
it('resolves as honestly missing for a saint with neither a proper collect nor a Common fallback authored', () => {
|
|
// Every real saint in data/calendar/saints/*.yml now has either a
|
|
// proper collect or a collectCommon fallback (see TODO.md's
|
|
// Kalendaria-table pull, 2026-08) -- so this exercises the "missing"
|
|
// path with a synthetic winner rather than hunting for a real
|
|
// example that no longer exists.
|
|
const day: LiturgicalDay = {
|
|
date: '2026-01-01',
|
|
weekday: 'thursday',
|
|
season: 'trinitytide',
|
|
temporalCategory: 'ordinary-feria',
|
|
winner: { kind: 'sanctoral', id: 'example-confessor', name: 'Example Confessor (placeholder)', rank: 'simplex' },
|
|
commemorations: [],
|
|
};
|
|
const collect = getDayCollect(day);
|
|
expect(collect.status.la).toBe('missing');
|
|
expect(collect.status.en).toBe('missing');
|
|
});
|
|
});
|
|
|
|
describe('minorHoursCommon/benedictusCommon fallback (2026-08, the 94 propers:null saints)', () => {
|
|
it('a below-duplex-majus saint still gets a real Benedictus antiphon and P/T/S/N content via its Common category', () => {
|
|
// St. Anacletus, Semiduplex -- below the duplex-majus+ threshold that
|
|
// gates a *proper* psalmody override (hours/lauds.ts's
|
|
// getPsalmodyOverrideFor), but getMinorHourOverrideId no longer
|
|
// requires that threshold for the shared-Common fallback (see its own
|
|
// doc comment for why: live-querying confirmed even a winning Simplex
|
|
// saint keeps proper/Common minor-hour content, not the ferial
|
|
// default).
|
|
const day = resolveDay('2028-07-13');
|
|
expect(day.winner).toMatchObject({ kind: 'sanctoral', id: 'st-anacletus' });
|
|
expect(getBenedictusAntiphon(day).status).toEqual({ la: 'verified', en: 'verified' });
|
|
expect(resolveMinorHourAntiphon('prime', day, {}).status.la).toBe('verified');
|
|
expect(resolveMinorHourChapter('terce', day, 'terce-capitulum-ferial').status.la).toBe('verified');
|
|
});
|
|
|
|
it('a Duplex-II-classis saint with no proper minor-hour content of its own still resolves via the Common, not just below-majus saints', () => {
|
|
// Ss. Placid and Companions -- confirms the fallback isn't only
|
|
// reached by low ranks; a real major feast with no *proper*
|
|
// Prime/Terce/Sext/None file authored falls through to its
|
|
// minorHoursCommon (common-of-several-martyrs) exactly the same way.
|
|
const day = resolveDay('2025-10-05');
|
|
expect(day.winner).toMatchObject({ kind: 'sanctoral', id: 'ss-placid-and-companions' });
|
|
expect(resolveMinorHourAntiphon('none', day, {}).status.la).toBe('verified');
|
|
});
|
|
});
|
|
|
|
describe('splitNamedAntiphon', () => {
|
|
it('carries a verified status through to both the incipit and full forms', () => {
|
|
const { incipit, full } = splitNamedAntiphon({
|
|
text: { la: 'Foo * bar baz.', en: 'Foo * bar baz.' },
|
|
status: { la: 'verified', en: 'verified' },
|
|
});
|
|
expect(incipit.status).toEqual({ la: 'verified', en: 'verified' });
|
|
expect(full.status).toEqual({ la: 'verified', en: 'verified' });
|
|
});
|
|
|
|
it('carries a draft status through to both forms, per language -- the bug this fixes: it used to hardcode verified on everything regardless of the source status, silently dropping the "unverified draft text" UI marker (src/ui/styles.css .text-draft) from every draft antiphon', () => {
|
|
const { incipit, full } = splitNamedAntiphon({
|
|
text: { la: 'Foo * bar baz.', en: 'Foo * bar baz.' },
|
|
status: { la: 'verified', en: 'draft' },
|
|
});
|
|
expect(incipit.status).toEqual({ la: 'verified', en: 'draft' });
|
|
expect(full.status).toEqual({ la: 'verified', en: 'draft' });
|
|
});
|
|
|
|
it("reflects St. Scholastica's own real draft-English antiphon end to end, through Lauds", async () => {
|
|
const { resolveOrdo } = await import('../../src/hours/lauds');
|
|
const ordo = resolveOrdo('2029-02-10'); // St. Scholastica, a clean year
|
|
const idx = ordo.parts.findIndex((p) => p.kind === 'canticle' && p.canticleId === 'benedictus');
|
|
const canticle = ordo.parts[idx];
|
|
const repeat = ordo.parts[idx + 1];
|
|
expect(canticle?.kind === 'canticle' ? canticle.antiphon?.status.en : undefined).toBe('draft');
|
|
expect(repeat?.kind === 'antiphon' ? repeat.text.status.en : undefined).toBe('draft');
|
|
});
|
|
});
|