tonus 0.1.1 → 0.1.2

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.
@@ -14,6 +14,17 @@ function romanMap() {
14
14
  _roman = new Map(OFFICE_ROMAN.map((d) => [d.feastId, d]));
15
15
  return _roman;
16
16
  }
17
+ // Hours whose result is an ordered sequence (an ordo) rather than a set of
18
+ // chants — they keep assembly order instead of being sorted by incipit.
19
+ const ORDERED_ORDO_HOURS = new Set([
20
+ "prima", "tertia", "sexta", "nona", "completorium",
21
+ ]);
22
+ // The purely seasonal/fixed hours — identical for every feast of a day, so
23
+ // concurrent feasts collapse to one and a no-feast query resolves the default
24
+ // epoch. (Terce/Sext/None are NOT here: their responsory breve is per-feast.)
25
+ const SEASONAL_ORDO_HOURS = new Set([
26
+ "prima", "completorium",
27
+ ]);
17
28
  // Compline is fixed and seasonal, not per-feast: it does not use the OfficeDay
18
29
  // tables at all. The ordo is assembled from the season (Te lucis, In manus
19
30
  // tuas), the fixed psalms (from the extracted DO scheme), the invariable spine
@@ -92,18 +103,21 @@ function chantsForFeastHour(feast, hour) {
92
103
  if (hy)
93
104
  results.push(hy);
94
105
  }
95
- else if (hour === "tertia") {
96
- const rb = resolveChant(day.respBreveTertia);
97
- if (rb)
98
- results.push(rb);
99
- }
100
- else if (hour === "sexta") {
101
- const rb = resolveChant(day.respBreveSexta);
102
- if (rb)
103
- results.push(rb);
104
- }
105
- else if (hour === "nona") {
106
- const rb = resolveChant(day.respBreveNona);
106
+ else if (hour === "tertia" || hour === "sexta" || hour === "nona") {
107
+ // The little hours: their portion of Ps 118 (Terce vv. 33–80, Sext 81–128,
108
+ // None 129–176, from the extracted DO scheme), then the responsory breve.
109
+ // The psalmody belongs to a specific day, so it is only included for a real
110
+ // feast query — not the all-days survey scan (which has no date and would
111
+ // repeat the psalms once per feast).
112
+ if (feast.date) {
113
+ const hourName = hour === "tertia" ? "Tertia" : hour === "sexta" ? "Sexta" : "Nona";
114
+ for (const p of officePsalmPortions(hourName, feast.weekday)) {
115
+ results.push(...intonePortion(p));
116
+ }
117
+ }
118
+ const rb = resolveChant(hour === "tertia" ? day.respBreveTertia
119
+ : hour === "sexta" ? day.respBreveSexta
120
+ : day.respBreveNona);
107
121
  if (rb)
108
122
  results.push(rb);
109
123
  }
@@ -138,10 +152,9 @@ export function getHour(query) {
138
152
  // Prime and Compline are seasonal/weekday ordos, identical for every feast
139
153
  // of the day — so concurrent feasts collapse to a single ordo rather than
140
154
  // repeating it. The other hours are genuinely per-feast.
141
- results =
142
- hour === "prima" || hour === "completorium"
143
- ? feasts[0] ? chantsForFeastHour(feasts[0], hour) : []
144
- : feasts.flatMap((f) => chantsForFeastHour(f, hour));
155
+ results = SEASONAL_ORDO_HOURS.has(hour)
156
+ ? feasts[0] ? chantsForFeastHour(feasts[0], hour) : []
157
+ : feasts.flatMap((f) => chantsForFeastHour(f, hour));
145
158
  }
146
159
  else if (feasts) {
147
160
  const hours = [
@@ -150,14 +163,15 @@ export function getHour(query) {
150
163
  ];
151
164
  results = feasts.flatMap((f) => hours.flatMap((h) => chantsForFeastHour(f, h)));
152
165
  }
153
- else if (hour === "prima" || hour === "completorium") {
166
+ else if (hour && SEASONAL_ORDO_HOURS.has(hour)) {
154
167
  // Prime and Compline are seasonal ordos, not per-feast. With no feast,
155
168
  // resolve for the default epoch (Guido d'Arezzo's era) — festum()'s anchor.
156
169
  const [feast] = getFeast();
157
170
  results = feast ? chantsForFeastHour(feast, hour) : [];
158
171
  }
159
172
  else if (hour) {
160
- // Hour without feast — scan all office entries
173
+ // Hour without feast — survey per-feast content across all office entries.
174
+ // mockFeast has no date, so the little hours return only their responsories.
161
175
  results = OFFICE_ROMAN.flatMap((day) => {
162
176
  const mockFeast = { id: day.feastId };
163
177
  return chantsForFeastHour(mockFeast, hour);
@@ -190,10 +204,10 @@ export function getHour(query) {
190
204
  const ids = new Set(toArray(query.id));
191
205
  results = results.filter((c) => ids.has(c.id));
192
206
  }
193
- // Prime and Compline are ordered ordos — their sequence IS the content — so
194
- // they keep assembly order unless the caller explicitly asks for a sort.
195
- // Every other hour returns a set of chants, sorted by incipit by default.
196
- const isOrderedOrdo = query.hora === "prima" || query.hora === "completorium";
207
+ // The little hours and Compline are ordered ordos — their sequence IS the
208
+ // content — so they keep assembly order unless the caller explicitly asks for
209
+ // a sort. The other hours return a set of chants, sorted by incipit.
210
+ const isOrderedOrdo = query.hora != null && ORDERED_ORDO_HOURS.has(query.hora);
197
211
  if (query.sort || !isOrderedOrdo) {
198
212
  const sort = query.sort ?? "incipit";
199
213
  results.sort((a, b) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tonus",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Medieval music analysis and performance: GABC plainchant exports, liturgical calendar, tuning systems, ephemeris, and the harmony of the spheres",