tonus 0.1.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/BIBLIOGRAPHY.md +132 -108
  2. package/CHANGELOG.md +598 -1
  3. package/LICENSE +133 -29
  4. package/README.md +106 -83
  5. package/dist/data/am.js +2666 -11196
  6. package/dist/data/ams.d.ts +5 -0
  7. package/dist/data/ams.js +122 -0
  8. package/dist/data/attestation.d.ts +19 -0
  9. package/dist/data/attestation.js +15716 -0
  10. package/dist/data/attestation.json +15711 -0
  11. package/dist/data/cadentiae.d.ts +43 -0
  12. package/dist/data/cadentiae.js +174 -0
  13. package/dist/data/cal.js +48 -0
  14. package/dist/data/census.d.ts +12 -0
  15. package/dist/data/census.js +36 -0
  16. package/dist/data/commune-office.d.ts +4 -0
  17. package/dist/data/commune-office.js +2371 -0
  18. package/dist/data/commune-office.json +2365 -0
  19. package/dist/data/commune.js +181 -11
  20. package/dist/data/corpus-overlap.d.ts +17 -0
  21. package/dist/data/corpus-overlap.js +299 -5
  22. package/dist/data/cot.d.ts +5 -0
  23. package/dist/data/cot.js +172 -0
  24. package/dist/data/cse.d.ts +5 -0
  25. package/dist/data/cse.js +122 -0
  26. package/dist/data/gabc-glyphs.d.ts +45 -0
  27. package/dist/data/gabc-glyphs.js +122 -0
  28. package/dist/data/gr.js +754 -6394
  29. package/dist/data/kyriale.js +116 -116
  30. package/dist/data/la.js +799 -13419
  31. package/dist/data/lh.js +113 -3473
  32. package/dist/data/lu.js +931 -17631
  33. package/dist/data/nocturnale-romanum.js +1659 -10411
  34. package/dist/data/office-ferial.d.ts +4 -0
  35. package/dist/data/office-ferial.js +396 -0
  36. package/dist/data/office-ferial.json +391 -0
  37. package/dist/data/office-monastic.d.ts +17 -1
  38. package/dist/data/office-monastic.js +1403 -466
  39. package/dist/data/office-psalms-monastic.d.ts +13 -1
  40. package/dist/data/office-psalms-monastic.js +9 -0
  41. package/dist/data/propers.js +1 -1
  42. package/dist/data/psalms.js +22919 -5
  43. package/dist/data/psm.d.ts +5 -0
  44. package/dist/data/psm.js +122 -0
  45. package/dist/data/seasonal-respbreve.d.ts +5 -0
  46. package/dist/data/seasonal-respbreve.js +41 -0
  47. package/dist/data/seasonal-respbreve.json +35 -0
  48. package/dist/data/smufl-glyphs.d.ts +17 -0
  49. package/dist/data/smufl-glyphs.js +1546 -0
  50. package/dist/data/smufl-glyphs.json +1530 -0
  51. package/dist/engines/cal/calendar.d.ts +3 -2
  52. package/dist/engines/cal/calendar.js +105 -29
  53. package/dist/engines/cal/data/eras.d.ts +35 -0
  54. package/dist/engines/cal/data/eras.js +128 -0
  55. package/dist/engines/cal/date.js +44 -0
  56. package/dist/engines/cal/types.d.ts +15 -3
  57. package/dist/engines/cal/types.js +5 -5
  58. package/dist/engines/census/census.d.ts +7 -0
  59. package/dist/engines/census/census.js +179 -0
  60. package/dist/engines/census/types.d.ts +55 -0
  61. package/dist/engines/census/types.js +8 -0
  62. package/dist/engines/chant/attest.d.ts +39 -0
  63. package/dist/engines/chant/attest.js +90 -0
  64. package/dist/engines/chant/chant.d.ts +16 -4
  65. package/dist/engines/chant/chant.js +220 -30
  66. package/dist/engines/chant/data/compline.js +2 -1
  67. package/dist/engines/chant/data/masses.d.ts +56 -4
  68. package/dist/engines/chant/data/masses.js +305 -80
  69. package/dist/engines/chant/data/prime.js +1 -1
  70. package/dist/engines/chant/hour.js +279 -58
  71. package/dist/engines/chant/ordinary.d.ts +2 -0
  72. package/dist/engines/chant/ordinary.js +336 -56
  73. package/dist/engines/chant/propers.js +55 -5
  74. package/dist/engines/chant/psalm.d.ts +4 -4
  75. package/dist/engines/chant/psalm.js +25 -11
  76. package/dist/engines/chant/syllabify.d.ts +1 -0
  77. package/dist/engines/chant/syllabify.js +90 -17
  78. package/dist/engines/chant/types.d.ts +115 -12
  79. package/dist/engines/chant/types.js +38 -3
  80. package/dist/engines/harmonia/api.js +4 -0
  81. package/dist/engines/harmonia/data/doctrines.js +3 -1
  82. package/dist/engines/harmonia/tabula.d.ts +3 -0
  83. package/dist/engines/harmonia/tabula.js +1 -0
  84. package/dist/engines/harmonia/voice.d.ts +4 -0
  85. package/dist/engines/harmonia/voice.js +8 -4
  86. package/dist/engines/imprint.js +14 -1
  87. package/dist/engines/planet/orbital.js +4 -4
  88. package/dist/engines/planet/planet.d.ts +10 -0
  89. package/dist/engines/planet/planet.js +30 -3
  90. package/dist/engines/planet/position.js +13 -10
  91. package/dist/engines/planet/types.d.ts +1 -0
  92. package/dist/engines/score/api.d.ts +2 -13
  93. package/dist/engines/score/api.js +21 -8
  94. package/dist/engines/score/articulation.js +2 -2
  95. package/dist/engines/score/cadence.d.ts +76 -0
  96. package/dist/engines/score/cadence.js +96 -0
  97. package/dist/engines/score/emitters/accidentals.d.ts +21 -0
  98. package/dist/engines/score/emitters/accidentals.js +88 -0
  99. package/dist/engines/score/emitters/atramentum.d.ts +107 -0
  100. package/dist/engines/score/emitters/atramentum.js +239 -0
  101. package/dist/engines/score/emitters/breaking.d.ts +62 -0
  102. package/dist/engines/score/emitters/breaking.js +80 -0
  103. package/dist/engines/score/emitters/moderna.d.ts +38 -0
  104. package/dist/engines/score/emitters/moderna.js +612 -0
  105. package/dist/engines/score/emitters/svg.d.ts +143 -0
  106. package/dist/engines/score/emitters/svg.js +1328 -0
  107. package/dist/engines/score/emitters/tracks.d.ts +104 -0
  108. package/dist/engines/score/emitters/tracks.js +728 -0
  109. package/dist/engines/score/infer.d.ts +3 -3
  110. package/dist/engines/score/infer.js +2 -2
  111. package/dist/engines/score/inscriptio.d.ts +69 -0
  112. package/dist/engines/score/inscriptio.js +138 -0
  113. package/dist/engines/score/ir.d.ts +2 -2
  114. package/dist/engines/score/ir.js +59 -12
  115. package/dist/engines/score/lyric.d.ts +23 -0
  116. package/dist/engines/score/lyric.js +234 -0
  117. package/dist/engines/score/meta.d.ts +2 -2
  118. package/dist/engines/score/modulation.d.ts +12 -0
  119. package/dist/engines/score/modulation.js +49 -0
  120. package/dist/engines/score/neume.js +35 -4
  121. package/dist/engines/score/parse.js +150 -9
  122. package/dist/engines/score/phrasing.js +4 -3
  123. package/dist/engines/score/prosody.d.ts +38 -0
  124. package/dist/engines/score/prosody.js +70 -6
  125. package/dist/engines/score/tabula.d.ts +37 -5
  126. package/dist/engines/score/tabula.js +18 -0
  127. package/dist/engines/score/types.d.ts +88 -1
  128. package/dist/engines/temper/api.d.ts +4 -1
  129. package/dist/engines/temper/api.js +28 -5
  130. package/dist/engines/temper/data/guido.js +6 -2
  131. package/dist/engines/temper/data/modes.d.ts +6 -0
  132. package/dist/engines/temper/data/modes.js +42 -0
  133. package/dist/engines/temper/data/tones.d.ts +1 -1
  134. package/dist/engines/temper/data/tones.js +20 -11
  135. package/dist/engines/temper/interval.js +4 -3
  136. package/dist/engines/temper/modality.d.ts +11 -2
  137. package/dist/engines/temper/modality.js +74 -2
  138. package/dist/engines/temper/modes.d.ts +1 -1
  139. package/dist/engines/temper/pitch.d.ts +1 -1
  140. package/dist/engines/temper/pitch.js +12 -2
  141. package/dist/engines/temper/scale.d.ts +53 -0
  142. package/dist/engines/temper/scale.js +107 -8
  143. package/dist/index.d.ts +26 -8
  144. package/dist/index.js +37 -4
  145. package/docs/api/calendar.md +279 -0
  146. package/docs/api/census.md +288 -0
  147. package/docs/api/chant.md +657 -0
  148. package/docs/api/heavens.md +346 -0
  149. package/docs/api/index.md +263 -0
  150. package/docs/api/score.md +873 -0
  151. package/docs/api/tuning.md +619 -0
  152. package/package.json +11 -5
  153. package/dist/data/office-matins-roman.d.ts +0 -19
  154. package/dist/data/office-matins-roman.js +0 -4383
  155. package/dist/data/office-psalms-roman.d.ts +0 -15
  156. package/dist/data/office-psalms-roman.js +0 -28
  157. package/dist/data/office-roman.d.ts +0 -19
  158. package/dist/data/office-roman.js +0 -13792
  159. package/dist/engines/chant/matutinum.d.ts +0 -33
  160. package/dist/engines/chant/matutinum.js +0 -81
  161. package/dist/engines/score/emitters/midi.d.ts +0 -65
  162. package/dist/engines/score/emitters/midi.js +0 -162
  163. package/dist/engines/score/emitters/musicxml.d.ts +0 -18
  164. package/dist/engines/score/emitters/musicxml.js +0 -166
@@ -14,8 +14,9 @@ export declare function buildCalendar(year: number): Map<string, CalEntry[]>;
14
14
  * Calendar lookup (`tonus.festum`). Returns matching feasts sorted
15
15
  * `day asc, rank desc` — for a date, the primary feast plus concurrent
16
16
  * feasts; for a `from`/`to` range, every day flattened; with no query,
17
- * the current liturgical year. Dates are UTC-canonical: build them from
18
- * ISO strings or `Date.UTC`.
17
+ * the default-epoch day (Guido d'Arezzo's era); for a filter-only query,
18
+ * the liturgical year containing that epoch. Dates are UTC-canonical:
19
+ * build them from ISO strings or `Date.UTC`.
19
20
  */
20
21
  export declare function getFeast(query?: FeastQuery): Feast[];
21
22
  //# sourceMappingURL=calendar.d.ts.map
@@ -17,15 +17,12 @@
17
17
  // noted as future work.) The rank system this data carries is documented at
18
18
  // `ritus`/`Grade` in ./types.ts; Easter reckoning at pascha() in ./date.ts.
19
19
  import { CAL } from "../../data/cal.js";
20
- import { MASSES } from "../chant/data/masses.js";
20
+ import { feastKeptBy } from "./data/eras.js";
21
+ import { massesForRubric, CLASS_I_GRADES, CLASS_II_GRADES, CLASS_III_GRADES, } from "../chant/data/masses.js";
21
22
  import { isoDate, startOfDay, addDays, subDays, firstSundayOnOrAfter, nextSunday, pascha, resolveEntryId, DEFAULT_EPOCH, } from "./date.js";
22
- import { TEMPUS_NAMES, entryGrade, gradeOrder, BVM_FEAST_IDS, APOSTOLIC_FEAST_IDS, } from "./types.js";
23
+ import { TEMPORA, entryGrade, gradeOrder, BVM_FEAST_IDS, APOSTOLIC_FEAST_IDS, PENITENTIAL_SEASONS, } from "./types.js";
23
24
  const _calCache = new Map();
24
25
  const _anchorCache = new Map();
25
- // Preferred mass order; ad libitum (mass 0) is handled separately in ordinary.ts.
26
- const DEFAULT_MASSES = [
27
- 8, 9, 11, 1, 2, 3, 4, 5, 6, 7, 10, 12, 13, 14, 15, 16, 17, 18,
28
- ];
29
26
  export function getAnchors(year) {
30
27
  if (_anchorCache.has(year))
31
28
  return _anchorCache.get(year);
@@ -142,29 +139,68 @@ function findSeason(date) {
142
139
  return s("quad", a.ashWednesday, a.easter);
143
140
  if (date >= a.easter && date < trinitySunday(a))
144
141
  return s("pasc", a.easter, trinitySunday(a));
145
- if (date >= trinitySunday(a) && date < next.adventFirstSunday)
146
- return s("pent", trinitySunday(a), next.adventFirstSunday);
142
+ // Advent of THIS year closes the season after Pentecost — the `adv` branch
143
+ // above has already claimed those days, so reaching for next year's Advent
144
+ // put the end date sixteen months out and made the season 547 days long.
145
+ if (date >= trinitySunday(a) && date < a.adventFirstSunday)
146
+ return s("pent", trinitySunday(a), a.adventFirstSunday);
147
147
  return s("epi", epiphanySunday(a), a.septuagesima);
148
148
  }
149
- function selectMasses(id, grade, season, date) {
150
- const dowCode = date.getUTCDay() === 0 ? "dominica" : "feria";
151
- const requireBvm = BVM_FEAST_IDS.has(id);
152
- const matches = [];
153
- for (const num of DEFAULT_MASSES) {
154
- const mass = MASSES.get(num);
155
- if (!mass)
156
- continue;
157
- if (requireBvm !== mass.bvm)
158
- continue;
159
- if (!mass.seasons.includes(season))
160
- continue;
161
- if (!mass.grades.includes(grade))
162
- continue;
163
- if (!mass.days.includes(dowCode))
164
- continue;
165
- matches.push(num);
149
+ // The Sunday-as-such grades. Per the canonical grade table, an ordinary Sunday is
150
+ // `semiduplex`, an Advent Sunday `semiduplex-ii`, a Lent Sunday `semiduplex-i` —
151
+ // so a day that is merely a Sunday is recognised by carrying one of these, while
152
+ // a feast that outranks the Sunday carries a duplex grade and takes its class.
153
+ const SUNDAY_GRADES = [
154
+ "semiduplex-i",
155
+ "semiduplex-ii",
156
+ "semiduplex",
157
+ ];
158
+ /**
159
+ * The Kyriale rubric this day falls under — the book classifies by RANK and
160
+ * appoints one category per day [biblio: liber-usualis, Kyriale].
161
+ *
162
+ * Order matters. "In Paschal Time" is a season rubric and governs inside
163
+ * Paschaltide. The Sunday categories must be tested before the class tiers,
164
+ * because the Sunday grades are the semiduplex variants — which would otherwise
165
+ * be claimed by the I, II and III class tiers and hand a Lent Sunday a
166
+ * first-class mass.
167
+ *
168
+ * An open editorial question: should a I class feast inside Paschaltide
169
+ * (Ascension, Pentecost) reach for the I-class pair II/III instead of Lux et
170
+ * Origo? The book's heading is unqualified, so Paschaltide wins here.
171
+ */
172
+ function rubricForDay(id, grade, season, date) {
173
+ const isSunday = date.getUTCDay() === 0;
174
+ const penitential = PENITENTIAL_SEASONS.has(season);
175
+ // The book's own subject category outranks the season: a BVM feast in
176
+ // Paschaltide is "For feasts of the Blessed Virgin", not "In Paschal Time" —
177
+ // paschal-first left Cum jubilo unreachable for the whole of Eastertide.
178
+ if (BVM_FEAST_IDS.has(id))
179
+ return "bvm";
180
+ if (season === "pasc")
181
+ return "paschal";
182
+ if (isSunday && SUNDAY_GRADES.includes(grade)) {
183
+ return penitential ? "sunday-penitential" : "sunday";
166
184
  }
167
- return matches;
185
+ if (CLASS_I_GRADES.includes(grade))
186
+ return "class-i";
187
+ if (CLASS_II_GRADES.includes(grade))
188
+ return "class-ii";
189
+ if (CLASS_III_GRADES.includes(grade))
190
+ return "class-iii";
191
+ if (grade === "simplex")
192
+ return "commemoration";
193
+ return penitential ? "feria-penitential" : "feria";
194
+ }
195
+ // The masses the day may sing: those the Kyriale appoints under its rubric, in
196
+ // the book's own numbering. Where a rubric names several — II class 1–5 — that
197
+ // numbering IS the invitation to choose, and ordinary.ts rotates among them.
198
+ //
199
+ // This replaced a `seasons ∩ grades ∩ days` intersection that returned every
200
+ // mass not positively excluded, which is how Easter came to be offered masses IV
201
+ // and V (appointed for the II class). See the header of chant/data/masses.ts.
202
+ function selectMasses(id, grade, season, date) {
203
+ return massesForRubric(rubricForDay(id, grade, season, date)).map((m) => m.mass);
168
204
  }
169
205
  function calEntryToFeast(entry, season, d) {
170
206
  const id = entry.id ?? "";
@@ -177,7 +213,7 @@ function calEntryToFeast(entry, season, d) {
177
213
  ritus,
178
214
  grade,
179
215
  season: season.code,
180
- tempus: TEMPUS_NAMES[season.code],
216
+ tempus: TEMPORA[season.code],
181
217
  seasonStart: season.start,
182
218
  seasonEnd: season.end,
183
219
  date: d,
@@ -208,19 +244,41 @@ function feastsForDate(date) {
208
244
  const season = findSeason(d);
209
245
  return entries.map((e) => calEntryToFeast(e, season, d));
210
246
  }
247
+ const FEAST_QUERY_KEYS = new Set([
248
+ "date", "from", "to", "nomen", "season", "grade", "marian", "apostolic",
249
+ "before",
250
+ ]);
211
251
  /**
212
252
  * Calendar lookup (`tonus.festum`). Returns matching feasts sorted
213
253
  * `day asc, rank desc` — for a date, the primary feast plus concurrent
214
254
  * feasts; for a `from`/`to` range, every day flattened; with no query,
215
- * the current liturgical year. Dates are UTC-canonical: build them from
216
- * ISO strings or `Date.UTC`.
255
+ * the default-epoch day (Guido d'Arezzo's era); for a filter-only query,
256
+ * the liturgical year containing that epoch. Dates are UTC-canonical:
257
+ * build them from ISO strings or `Date.UTC`.
217
258
  */
218
259
  export function getFeast(query) {
260
+ // A bare Date is the natural guess, and it has no own enumerable keys — so
261
+ // without this it would read as "no query" and quietly return the default
262
+ // epoch, the same plausible-looking wrong answer the unknown-key guard below
263
+ // exists to prevent. Say what was meant instead.
264
+ if (query instanceof Date) {
265
+ throw new Error(`festum: pass the date as a query — festum({ date }), not festum(date)`);
266
+ }
219
267
  if (!query || Object.keys(query).length === 0) {
220
268
  return feastsForDate(DEFAULT_EPOCH);
221
269
  }
270
+ // Reject unknown keys rather than silently falling through to the default
271
+ // epoch — `festum({ month: 12, day: 25 })` (a natural guess) would otherwise
272
+ // return a plausible-looking wrong answer. Fail loudly on the typo instead.
273
+ const unknown = Object.keys(query).filter((k) => !FEAST_QUERY_KEYS.has(k));
274
+ if (unknown.length > 0) {
275
+ throw new Error(`festum: unknown query key(s) ${unknown.map((k) => `"${k}"`).join(", ")} ` +
276
+ `(expected ${[...FEAST_QUERY_KEYS].join(", ")})`);
277
+ }
222
278
  let results;
223
279
  if (query.date) {
280
+ if (!(query.date instanceof Date) || Number.isNaN(query.date.getTime()))
281
+ throw new Error(`festum: date must be a Date — e.g. new Date("2026-12-25") (UTC-canonical)`);
224
282
  results = feastsForDate(query.date);
225
283
  }
226
284
  else if (query.from != null || query.to != null) {
@@ -256,6 +314,24 @@ export function getFeast(query) {
256
314
  d = addDays(d, 1);
257
315
  }
258
316
  }
317
+ // `before` resolves the day AS OF a year: feasts instituted later step aside,
318
+ // and whatever ranked behind them — usually the temporale or the feria — wins
319
+ // instead. The calendar DATA is untouched; this is a view over it, so a caller
320
+ // asking for 1350 and a caller asking for 1962 read the same shipped table.
321
+ // A day whose every candidate is later than `before` returns empty, which is
322
+ // the honest answer: that day had no feast yet.
323
+ if (query.before != null) {
324
+ if (!Number.isFinite(query.before)) {
325
+ throw new Error(`festum: before must be a year — e.g. festum({ date, before: 1350 })`);
326
+ }
327
+ results = results
328
+ .filter((f) => feastKeptBy(f.id, query.before))
329
+ // Stamp the view on the survivors. The chant verbs read it back, so one
330
+ // `before` at the calendar door carries through the whole day — the
331
+ // calendar as of 1100 serves the repertoire attested by 1100, without
332
+ // the caller saying the year twice. A `before` of their own overrides.
333
+ .map((f) => ({ ...f, before: query.before }));
334
+ }
259
335
  if (query.nomen) {
260
336
  const n = query.nomen.toLowerCase();
261
337
  results = results.filter((f) => f.nomen.toLowerCase().includes(n));
@@ -0,0 +1,35 @@
1
+ export interface FeastEra {
2
+ /** Year of universal Roman observance — the year it starts winning days. */
3
+ year: number;
4
+ /** Earlier local/regional observance, where one is well attested. */
5
+ local?: number;
6
+ /** Why the date is what it is, and where it is shaky. */
7
+ note?: string;
8
+ }
9
+ /**
10
+ * Feast id → when it entered the universal calendar. Ids are the calendar's own
11
+ * (`MM-DD` for the sanctorale, season-relative for the temporale).
12
+ *
13
+ * INTERNAL, ruled 2026-08-04. This table does not join the public appendix and
14
+ * is not re-exported from the index, though it is the last table that could
15
+ * plausibly have. The appendix carries measurements and vocabularies; these are
16
+ * forty-three liturgical-historical CLAIMS, and the distinction between `year`
17
+ * (universal Roman observance) and `local` is a judgement about what a caller
18
+ * is modelling rather than a fact about the calendar — see the banner above.
19
+ * Publishing it would invite callers to cite dates tonus is not in a position
20
+ * to defend. The behaviour it drives, `festum({ before })`, is public and
21
+ * documented; the reckoning behind it stays here.
22
+ */
23
+ export declare const FEAST_ERAS: Readonly<Record<string, FeastEra>>;
24
+ /**
25
+ * Was this feast kept by `year`? A feast the table does not mention is treated
26
+ * as old enough — the table lists arrivals, not the whole calendar.
27
+ */
28
+ export declare function feastKeptBy(feastId: string, year: number): boolean;
29
+ /**
30
+ * The same question asked of a LOCAL observance: a feast kept somewhere by
31
+ * `year`, even if Rome had not yet taken it up. Use this when modelling a house
32
+ * rather than the universal calendar.
33
+ */
34
+ export declare function feastKeptLocallyBy(feastId: string, year: number): boolean;
35
+ //# sourceMappingURL=eras.d.ts.map
@@ -0,0 +1,128 @@
1
+ // ---------------------------------------------------------------------------
2
+ // engines/cal/data/eras — when a feast entered the calendar
3
+ // ---------------------------------------------------------------------------
4
+ // The shipped calendar is Tridentine, so it carries feasts instituted centuries
5
+ // after the repertoire tonus models. This table dates the late arrivals so a
6
+ // caller can ask for the day AS OF a chosen year — `festum({ date, before })` —
7
+ // and let the temporale or the feria win where a later feast would have taken
8
+ // the day.
9
+ //
10
+ // tonus-corpus keeps shipping every calendar day; nothing is cut from the data.
11
+ // This is a VIEW over it. A feast absent from this table is treated as old
12
+ // enough to keep, so the table only ever needs the late arrivals.
13
+ //
14
+ // ── EVERY DATE HERE WANTS A HISTORIAN'S RED PEN ─────────────────────────────
15
+ // These are liturgical-historical claims, not measurements. `year` is the date
16
+ // of UNIVERSAL observance in the Roman calendar (the point at which the feast
17
+ // would displace a feria everywhere), because that is what a day-resolution
18
+ // question actually turns on. Where a local observance long predates the
19
+ // universal one, `local` records it — several feasts are genuinely medieval in
20
+ // one diocese and early-modern in Rome, and which one you want is a judgement
21
+ // about what you are modelling, not a fact.
22
+ //
23
+ // Deliberately NOT listed, though a name-pattern sweep flagged them:
24
+ // · 05-08 In Apparitione S. Michaëlis — the Monte Gargano apparition is 5th c.
25
+ // and the feast early medieval. Old; keep.
26
+ // · 02-01 S. Ignatii Episcopi et Martyris — Ignatius of ANTIOCH, d. c. 107.
27
+ // Not Ignatius Loyola. Keep.
28
+ // · 09-11 Ss. Proti et Hyacinthi — ancient Roman martyrs. Not Hyacinth of
29
+ // Poland (who is listed, at 08-17). Keep.
30
+ // Those three are the traps in dating this list by name.
31
+ //
32
+ // ── KEPT DELIBERATELY, AGAINST THE STRICT READING ───────────────────────────
33
+ // · 08-06 In Transfiguratione Domini — Callixtus III made it universal in 1457,
34
+ // but it is an ancient Eastern feast with local Western observance long
35
+ // before. Old school; keep.
36
+ // · 07-02 In Visitatione BMV — 1389 is Urban VI's UNIVERSAL extension (prayed
37
+ // for the end of the Great Schism). St Bonaventure had already instituted it
38
+ // for the Franciscans in 1263 — genuinely inside a pre-1350 line, so the
39
+ // keep is supported by the record, not only by instinct.
40
+ // · 02-08 S. Joannis de Matha and 11-20 S. Felicis de Valois — the Trinitarian
41
+ // founders, both dead c. 1212–13. Their cult was confirmed late, but the
42
+ // saints and their veneration are medieval. Keep.
43
+ // Each of these would come back under a stricter reading of "universal
44
+ // observance"; they are out because the feast, not the paperwork, is what a
45
+ // singing calendar is modelling.
46
+ /**
47
+ * Feast id → when it entered the universal calendar. Ids are the calendar's own
48
+ * (`MM-DD` for the sanctorale, season-relative for the temporale).
49
+ *
50
+ * INTERNAL, ruled 2026-08-04. This table does not join the public appendix and
51
+ * is not re-exported from the index, though it is the last table that could
52
+ * plausibly have. The appendix carries measurements and vocabularies; these are
53
+ * forty-three liturgical-historical CLAIMS, and the distinction between `year`
54
+ * (universal Roman observance) and `local` is a judgement about what a caller
55
+ * is modelling rather than a fact about the calendar — see the banner above.
56
+ * Publishing it would invite callers to cite dates tonus is not in a position
57
+ * to defend. The behaviour it drives, `festum({ before })`, is public and
58
+ * documented; the reckoning behind it stays here.
59
+ */
60
+ export const FEAST_ERAS = Object.freeze({
61
+ // ── Devotional feasts of the Lord and of Our Lady ──────────────────────────
62
+ "Nat2-0": { year: 1721, local: 1530, note: "Holy Name of Jesus; Franciscan/local 16th c." },
63
+ "Epi1-0": { year: 1921, local: 1893, note: "Holy Family; Leo XIII then Benedict XV" },
64
+ "02-11": { year: 1907, note: "Apparition of the Immaculate Virgin (Lourdes)" },
65
+ "Quad5-5": { year: 1727, local: 1423, note: "Seven Sorrows, Friday of Passion week; Cologne 1423" },
66
+ "Pent02-5": { year: 1856, local: 1765, note: "Sacred Heart; Clement XIII grant 1765, Pius IX universal" },
67
+ "07-01": { year: 1849, note: "Precious Blood (Pius IX)" },
68
+ "07-16": { year: 1726, local: 1376, note: "Our Lady of Mount Carmel; Carmelite from c. 1376" },
69
+ "08-22": { year: 1944, note: "Immaculate Heart (Pius XII)" },
70
+ "09-15": { year: 1727, local: 1423, note: "Seven Sorrows (September)" },
71
+ "09-24": { year: 1696, note: "Our Lady of Ransom / de Mercede" },
72
+ "10-02": { year: 1670, local: 1608, note: "Guardian Angels (Clement X)" },
73
+ "10-07": { year: 1716, local: 1573, note: "Rosary; Gregory XIII 1573 after Lepanto, Clement XI universal" },
74
+ "10-11": { year: 1931, note: "Divine Maternity (Pius XI, Ephesus centenary)" },
75
+ "11-21": { year: 1585, local: 1372, note: "Presentation of Our Lady; Avignon papal court 1372" },
76
+ "09-17": { year: 1585, note: "Stigmata of St Francis" },
77
+ // ── Saints venerated after ~1350 ───────────────────────────────────────────
78
+ // Dated by canonization or by entry into the universal calendar, whichever is
79
+ // the later — again, these want checking against the sources.
80
+ "03-04": { year: 1521, note: "Casimir of Poland, d. 1484" },
81
+ "08-17": { year: 1594, note: "Hyacinth of Poland, d. 1257 — cult approved late" },
82
+ "05-05": { year: 1712, note: "Pius V, d. 1572" },
83
+ "10-15": { year: 1622, note: "Teresa of Ávila, d. 1582" },
84
+ "11-24": { year: 1726, note: "John of the Cross, d. 1591" },
85
+ "07-31": { year: 1622, note: "Ignatius Loyola, d. 1556" },
86
+ "12-03": { year: 1622, note: "Francis Xavier, d. 1552" },
87
+ "05-26": { year: 1622, note: "Philip Neri, d. 1595 — id verified against the calendar" },
88
+ "01-29": { year: 1665, note: "Francis de Sales, d. 1622" },
89
+ "07-19": { year: 1737, note: "Vincent de Paul, d. 1660" },
90
+ "08-02": { year: 1839, note: "Alphonsus Liguori, d. 1787" },
91
+ "06-21": { year: 1726, note: "Aloysius Gonzaga, d. 1591" },
92
+ "05-13": { year: 1930, note: "Robert Bellarmine, d. 1621" },
93
+ "04-27": { year: 1925, note: "Peter Canisius, d. 1597" },
94
+ "08-07": { year: 1671, note: "Cajetan, d. 1547" },
95
+ "03-08": { year: 1690, note: "John of God, d. 1550" },
96
+ "07-18": { year: 1746, note: "Camillus de Lellis, d. 1614" },
97
+ "08-27": { year: 1767, note: "Joseph Calasanz, d. 1648" },
98
+ "08-19": { year: 1925, note: "John Eudes, d. 1680" },
99
+ "04-28": { year: 1867, note: "Paul of the Cross, d. 1775" },
100
+ "05-17": { year: 1690, note: "Paschal Baylon, d. 1592" },
101
+ "10-20": { year: 1767, note: "John Cantius, d. 1473" },
102
+ "11-14": { year: 1867, note: "Josaphat, d. 1623" },
103
+ "10-17": { year: 1920, note: "Margaret Mary Alacoque, d. 1690" },
104
+ "01-31": { year: 1934, note: "John Bosco, d. 1888" },
105
+ "02-27": { year: 1920, note: "Gabriel of Our Lady of Sorrows, d. 1862" },
106
+ "09-03": { year: 1954, note: "Pius X, d. 1914" },
107
+ "03-24": { year: 1921, note: "St Gabriel Archangel as a separate feast; the archangel is of course ancient" },
108
+ });
109
+ /**
110
+ * Was this feast kept by `year`? A feast the table does not mention is treated
111
+ * as old enough — the table lists arrivals, not the whole calendar.
112
+ */
113
+ export function feastKeptBy(feastId, year) {
114
+ const era = FEAST_ERAS[feastId];
115
+ return !era || era.year <= year;
116
+ }
117
+ /**
118
+ * The same question asked of a LOCAL observance: a feast kept somewhere by
119
+ * `year`, even if Rome had not yet taken it up. Use this when modelling a house
120
+ * rather than the universal calendar.
121
+ */
122
+ export function feastKeptLocallyBy(feastId, year) {
123
+ const era = FEAST_ERAS[feastId];
124
+ if (!era)
125
+ return true;
126
+ return Math.min(era.year, era.local ?? era.year) <= year;
127
+ }
128
+ //# sourceMappingURL=eras.js.map
@@ -91,6 +91,50 @@ export function resolveEntryId(id, year, anchors) {
91
91
  if (/^\d{2}-\d{2}$/.test(id)) {
92
92
  return [place(startOfDay(parseMonthDay(year, id)))];
93
93
  }
94
+ // The Christmas-octave days are DATE-stemmed, not week-stemmed:
95
+ // Nat26–Nat31 = Dec 26–31 ("Diei VI infra Octavam Nativitatis"). Dec 30
96
+ // has no saint, so without Nat30 the date resolved to nothing at all.
97
+ const nat = id.match(/^Nat(2[6-9]|3[01])$/);
98
+ if (nat) {
99
+ return [place(startOfDay(new Date(Date.UTC(year, 11, Number(nat[1])))))];
100
+ }
101
+ const m = id.match(/^([A-Za-z]+)(\d+)-(\d+)$/);
102
+ if (!m)
103
+ throw new Error(`Unrecognized tempora stem: ${id}`);
104
+ const prefix = m[1];
105
+ const week = parseInt(m[2], 10);
106
+ const weekday = parseInt(m[3], 10);
107
+ // The season after Pentecost is ELASTIC (23–28 Sundays), and the book's
108
+ // rule is not linear: DO's Pent24 is "Dominica XXIV et ultima" — ALWAYS
109
+ // the last Sunday before Advent — and the surplus Sundays between the 23rd
110
+ // and the last resume the post-Epiphany Sundays omitted in January, in
111
+ // their order, ending at the sixth. Linear placement stranded the November
112
+ // tail with no tempora at all (the 11-18 / 11-27 / 11-28 holes of 2026).
113
+ const lastSunday = addDays(anchors.adventFirstSunday, -7);
114
+ const sundaysAfterPentecost = Math.round((lastSunday.getTime() - anchors.pentecost.getTime()) / (7 * 86400000));
115
+ if (prefix === "Pent") {
116
+ // Dominica ultima: anchored to Advent, not to Pentecost.
117
+ if (week === 24)
118
+ return [place(addDays(lastSunday, weekday))];
119
+ // In a 23-Sunday year the ultima falls ON the 23rd slot and sings the
120
+ // 24th's mass — the 23rd steps aside rather than double-booking the day.
121
+ if (week >= sundaysAfterPentecost)
122
+ return [];
123
+ return [place(addDays(anchors.pentecost, week * 7 + weekday))];
124
+ }
125
+ if (prefix === "Epi") {
126
+ // Always the January placement…
127
+ const placements = [place(resolveTemporaStem(id, anchors))];
128
+ // …and, in a long year, the autumn resumption: with S Sundays after
129
+ // Pentecost, the surplus g = S − 24 weeks between the 23rd and the
130
+ // ultima take Epi(7−g) … Epi6, in their order.
131
+ const surplus = sundaysAfterPentecost - 24;
132
+ if (surplus > 0 && week >= 7 - surplus && week <= 6) {
133
+ const sundayIndex = 24 + (week - (7 - surplus));
134
+ placements.push(place(addDays(anchors.pentecost, sundayIndex * 7 + weekday)));
135
+ }
136
+ return placements;
137
+ }
94
138
  return [place(resolveTemporaStem(id, anchors))];
95
139
  }
96
140
  function resolveTemporaStem(stem, anchors) {
@@ -1,10 +1,10 @@
1
1
  export type Season = "adv" | "nat" | "epi" | "quadp" | "quad" | "pasc" | "pent";
2
- export declare const SEASON_LABELS: Readonly<Record<Season, string>>;
3
- export declare const TEMPUS_NAMES: Readonly<Record<Season, string>>;
2
+ export declare const SEASON_LABEL: Readonly<Record<Season, string>>;
3
+ export declare const TEMPORA: Readonly<Record<Season, string>>;
4
4
  export declare const PENITENTIAL_SEASONS: ReadonlySet<Season>;
5
5
  export type Grade = "triduum" | "duplex-i" | "duplex-majus-i" | "semiduplex-i" | "feria-privilegiata" | "duplex-ii" | "semiduplex-ii" | "duplex-majus" | "duplex" | "semiduplex" | "simplex" | "feria-major" | "vigilia" | "feria";
6
6
  export declare const GRADE_ORDER: readonly Grade[];
7
- export declare const GRADE_NAMES: Readonly<Record<Grade, string>>;
7
+ export declare const GRADUS: Readonly<Record<Grade, string>>;
8
8
  export declare const RITUS_TO_GRADE: Readonly<Record<string, Grade>>;
9
9
  /** Reduce a Tridentine ritus string to its canonical Grade. */
10
10
  export declare function ritusToGrade(ritus: string): Grade;
@@ -34,6 +34,11 @@ export interface Feast {
34
34
  masses: number[];
35
35
  marian: boolean;
36
36
  apostolic: boolean;
37
+ /** The era view this feast was resolved under — the `before` year given to
38
+ * festum. Carried on the feast so the chant verbs (proprium, ordinarium,
39
+ * officium) serve the SAME view without being told twice; a `before` on
40
+ * their own query overrides. Absent on a present-day resolution. */
41
+ before?: number;
37
42
  }
38
43
  export interface FeastQuery {
39
44
  date?: Date;
@@ -44,6 +49,13 @@ export interface FeastQuery {
44
49
  grade?: Grade;
45
50
  marian?: boolean;
46
51
  apostolic?: boolean;
52
+ /**
53
+ * Resolve the day AS OF this year: feasts instituted later step aside and
54
+ * whatever ranked behind them wins — usually the temporale or the feria. A
55
+ * view over the shipped calendar, which keeps every day either way. Dates
56
+ * live in cal/data/eras.ts.
57
+ */
58
+ before?: number;
47
59
  }
48
60
  export interface Pascha {
49
61
  year: number;
@@ -1,7 +1,7 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // engines/cal/types — calendar types and constants
3
3
  // ---------------------------------------------------------------------------
4
- export const SEASON_LABELS = Object.freeze({
4
+ export const SEASON_LABEL = Object.freeze({
5
5
  adv: "Advent",
6
6
  nat: "Christmastide",
7
7
  epi: "Time after Epiphany",
@@ -11,8 +11,8 @@ export const SEASON_LABELS = Object.freeze({
11
11
  pent: "Time after Pentecost",
12
12
  });
13
13
  // Authentic Latin season names (the books' own headings). Feast.tempus
14
- // carries these; SEASON_LABELS above is the English reference map.
15
- export const TEMPUS_NAMES = Object.freeze({
14
+ // carries these; SEASON_LABEL above is the English reference map.
15
+ export const TEMPORA = Object.freeze({
16
16
  adv: "Tempus Adventus",
17
17
  nat: "Tempus Nativitatis",
18
18
  epi: "Tempus post Epiphaniam",
@@ -46,12 +46,12 @@ export const GRADE_ORDER = [
46
46
  "vigilia",
47
47
  "feria",
48
48
  ];
49
- // Canonical Latin name per grade (display reference map, like SEASON_LABELS).
49
+ // Canonical Latin name per grade (display reference map, like SEASON_LABEL).
50
50
  // Feast objects don't carry this — `ritus` is the on-object Latin carrier;
51
51
  // use this map when the canonical grade name is wanted instead (it differs
52
52
  // from ritus only for octave compounds, the Triduum, and the privileged-
53
53
  // Sunday overrides).
54
- export const GRADE_NAMES = Object.freeze({
54
+ export const GRADUS = Object.freeze({
55
55
  triduum: "Triduum Sacrum",
56
56
  "duplex-i": "Duplex I classis",
57
57
  "duplex-majus-i": "Duplex majus I classis",
@@ -0,0 +1,7 @@
1
+ import type { Census, CensusQuery } from "./types.js";
2
+ /**
3
+ * The census of one chant: its profile against the corpus, where it is
4
+ * unusual, and what it is near.
5
+ */
6
+ export declare function getCensus(query: CensusQuery): Census;
7
+ //# sourceMappingURL=census.d.ts.map