tonus 0.11.0 → 0.12.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.
- package/CHANGELOG.md +33 -0
- package/dist/data/kyriale.d.ts +16 -0
- package/dist/data/kyriale.js +387 -26
- package/dist/engines/chant/chant.js +13 -1
- package/dist/engines/chant/ordinary.js +54 -20
- package/dist/engines/chant/types.d.ts +7 -0
- package/docs/api/chant.md +25 -6
- package/package.json +1 -1
|
@@ -149,7 +149,19 @@ const CORPUS = [
|
|
|
149
149
|
// still resolve to exactly one chant — the census contract says so in as many
|
|
150
150
|
// words. So they stay addressable by `id`, by `office: "or"`, and by
|
|
151
151
|
// `ordinary`, and stay out of the shelf's book list and row count.
|
|
152
|
-
|
|
152
|
+
//
|
|
153
|
+
// Not shelved, but CITED: each entry carries the books that print it (gr, lu)
|
|
154
|
+
// and their pages, so its by-book map is registered here the same way a corpus
|
|
155
|
+
// record's is. That is what lets `cantus({ id, source: "lu" })` answer with the
|
|
156
|
+
// Liber Usualis's leaf number instead of nothing.
|
|
157
|
+
const KYRIALE_CHANTS = KYRIALE.map((entry) => {
|
|
158
|
+
const chant = entryToOrdinaryChant(entry);
|
|
159
|
+
const byBook = entry.pages;
|
|
160
|
+
if (byBook && !Array.isArray(byBook)) {
|
|
161
|
+
PAGES_BY_BOOK.set(chant.id, byBook);
|
|
162
|
+
}
|
|
163
|
+
return chant;
|
|
164
|
+
});
|
|
153
165
|
/** Everything nameable: the shelf, plus the ordinary that no shelf holds. */
|
|
154
166
|
const ADDRESSABLE = [...CORPUS, ...KYRIALE_CHANTS];
|
|
155
167
|
let _byId = null;
|
|
@@ -21,16 +21,20 @@ function resolveMasses(feast) {
|
|
|
21
21
|
return resolved;
|
|
22
22
|
return [feast.marian ? AD_LIB.bvm : AD_LIB.standard];
|
|
23
23
|
}
|
|
24
|
-
//
|
|
25
|
-
// the
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
24
|
+
// Where the Kyriale prints an entry. Only the "mass" section carries a mass
|
|
25
|
+
// number; the others are addressed by section. The appendix used to carry its
|
|
26
|
+
// Graduale page as a "mass" (Clemens Rector was mass 75, the Requiem 94, 101
|
|
27
|
+
// and 102), and the two ad libitum Kyries whose incipits carry a numeral
|
|
28
|
+
// collided with Masses VI and X.
|
|
29
|
+
const KYRIALE_SECTIONS = new Set([
|
|
30
|
+
"mass", "credo", "sprinkling", "ad-libitum", "requiem",
|
|
31
|
+
]);
|
|
32
|
+
const SPRINKLING_OFFICES = new Set(["as", "va"]);
|
|
33
|
+
// The Cantus ad libitum, which no feast's `masses` list ever names: reachable
|
|
34
|
+
// on the appendix's solemnity turn, as a last-resort fallback here, or by a
|
|
35
|
+
// direct `ordinarium({ section: "ad-libitum" })` query.
|
|
29
36
|
function isAdLibitum(entry) {
|
|
30
|
-
|
|
31
|
-
// Kyries are numbered 6 and 10 in the source data, colliding with the
|
|
32
|
-
// numbered masses — for those the incipit is the only discriminator.
|
|
33
|
-
return (entry.mass ?? 0) > NUMBERED_MASS_MAX || /\(ad lib\./i.test(entry.incipit);
|
|
37
|
+
return entry.section === "ad-libitum";
|
|
34
38
|
}
|
|
35
39
|
/** Prefer a proper numbered setting over an appendix one at equal standing. */
|
|
36
40
|
function adLibLast(a, b) {
|
|
@@ -38,12 +42,13 @@ function adLibLast(a, b) {
|
|
|
38
42
|
}
|
|
39
43
|
// The Missa pro defunctis settings belong to the Requiem, not to the temporal or
|
|
40
44
|
// sanctoral day. They stay out of every calendar-driven pick and remain
|
|
41
|
-
// reachable only by direct `ordinarium({
|
|
42
|
-
// dismissal carries the bare incipit "Requiescant"
|
|
43
|
-
//
|
|
44
|
-
// last-resort appendix onto every feria whose mass prints no
|
|
45
|
+
// reachable only by direct `ordinarium({ section: "requiem" })` query. The
|
|
46
|
+
// Requiem's own dismissal carries the bare incipit "Requiescant", which is why
|
|
47
|
+
// the section is data and not an incipit test: left to the incipit it leaked
|
|
48
|
+
// through the last-resort appendix onto every feria whose mass prints no
|
|
49
|
+
// dismissal.
|
|
45
50
|
function isRequiem(entry) {
|
|
46
|
-
return
|
|
51
|
+
return entry.section === "requiem";
|
|
47
52
|
}
|
|
48
53
|
/**
|
|
49
54
|
* Deterministic rotation of a preference list by year.
|
|
@@ -104,8 +109,14 @@ function appendixLeadsThisYear(rubric, poolSize, year) {
|
|
|
104
109
|
const n = poolSize + 1;
|
|
105
110
|
return ((year % n) + n) % n === poolSize;
|
|
106
111
|
}
|
|
112
|
+
// The appendix in the book's own order. Its settings carry no mass number, so
|
|
113
|
+
// the Graduale's page orders them: Kyrie I to XI, then the Glorias, and so on.
|
|
114
|
+
function grPage(entry) {
|
|
115
|
+
const page = /\d+/.exec(entry.pages?.gr?.[0]?.page ?? "")?.[0];
|
|
116
|
+
return page == null ? Number.POSITIVE_INFINITY : Number(page);
|
|
117
|
+
}
|
|
107
118
|
function appendixFor(office, pool) {
|
|
108
|
-
return pool.filter((e) => e.office === office && isAdLibitum(e) && !isRequiem(e)).sort((a, b) => (a
|
|
119
|
+
return pool.filter((e) => e.office === office && isAdLibitum(e) && !isRequiem(e)).sort((a, b) => grPage(a) - grPage(b) || (a.id < b.id ? -1 : 1));
|
|
109
120
|
}
|
|
110
121
|
/**
|
|
111
122
|
* Candidate settings for one ordinary slot, most preferred first.
|
|
@@ -183,7 +194,7 @@ function entriesForOffice(office, massNumbers, year, appendixLeads, wholeMass =
|
|
|
183
194
|
// is the standing answer — its ad libitum variants must NOT rotate against it
|
|
184
195
|
// as equals, or the appendix would be sung more often than the rite itself.
|
|
185
196
|
// They arrive on the appendix turn like every other appendix setting.
|
|
186
|
-
const unnumbered = pool.filter((e) => e.office === office &&
|
|
197
|
+
const unnumbered = pool.filter((e) => e.office === office && SPRINKLING_OFFICES.has(office) && !isRequiem(e));
|
|
187
198
|
const plain = unnumbered.filter((e) => !isAdLibitum(e));
|
|
188
199
|
if (plain.length)
|
|
189
200
|
return rotate(plain, year);
|
|
@@ -280,11 +291,21 @@ export function entryToOrdinaryChant(entry) {
|
|
|
280
291
|
const ordinary = ORDINARY_OFFICES.has(entry.office)
|
|
281
292
|
? entry.office
|
|
282
293
|
: "ke";
|
|
294
|
+
// The Kyriale's citation. `ky` is a category, not a volume, so it can never
|
|
295
|
+
// be cited — which is why these chants shipped with books: ["ky"] and no page
|
|
296
|
+
// at all, and nothing downstream could name where to find them. But they ARE
|
|
297
|
+
// printed: the extractor now carries the Graduale's and the Liber Usualis's
|
|
298
|
+
// listings on every entry. `books` is those volumes, owner first; `pages` is
|
|
299
|
+
// the OWNER's, exactly as chant.ts's withLabels() does for a shelf record —
|
|
300
|
+
// presentAs() swaps in the asked-for book's when a caller names one. An entry
|
|
301
|
+
// with no citation still falls back to its own printing, so it stays findable.
|
|
302
|
+
const books = (entry.books?.length ? entry.books : ["ky"]);
|
|
303
|
+
const pages = entry.pages?.[books[0]] ?? [];
|
|
283
304
|
return {
|
|
284
305
|
id: entry.id,
|
|
285
306
|
incipit: entry.incipit,
|
|
286
307
|
gabc: entry.gabc,
|
|
287
|
-
books
|
|
308
|
+
books,
|
|
288
309
|
office: "ky",
|
|
289
310
|
// The genus follows the office code, as everywhere else — OFFICIA.ky.
|
|
290
311
|
// It read "Ordinarium" while the office read "or", which was tonus's own
|
|
@@ -292,11 +313,15 @@ export function entryToOrdinaryChant(entry) {
|
|
|
292
313
|
genus: OFFICIA.ky,
|
|
293
314
|
mode: entry.mode ? String(entry.mode) : null,
|
|
294
315
|
modus: entry.mode ? (MODI[String(entry.mode)] ?? null) : null,
|
|
295
|
-
pages
|
|
316
|
+
pages,
|
|
317
|
+
// Still KY_SOURCE: the bibliographic note says a singer is holding the
|
|
318
|
+
// Kyriale, which is true, and is a different claim from which shelved
|
|
319
|
+
// volumes print it. `books` carries that.
|
|
296
320
|
source: KY_SOURCE,
|
|
297
321
|
ordinary,
|
|
298
322
|
ordinarium: ORDINARIA[ordinary] ?? entry.incipit,
|
|
299
323
|
mass: entry.mass ?? 0,
|
|
324
|
+
section: entry.section,
|
|
300
325
|
};
|
|
301
326
|
}
|
|
302
327
|
// Maundy Thursday (In Cena Domini) is a Triduum exception: it retains a full
|
|
@@ -441,7 +466,7 @@ function assertFeasts(feasts, method) {
|
|
|
441
466
|
throw new Error(`${method}: feast must be a Feast (from tonus.festum) — got ${typeof f}`);
|
|
442
467
|
}
|
|
443
468
|
}
|
|
444
|
-
const ORDINARIUM_QUERY_KEYS = new Set([...CANTUS_QUERY_KEYS, "feast", "ordinary", "mass"]);
|
|
469
|
+
const ORDINARIUM_QUERY_KEYS = new Set([...CANTUS_QUERY_KEYS, "feast", "ordinary", "mass", "section"]);
|
|
445
470
|
/**
|
|
446
471
|
* Mass ordinary retrieval (`tonus.ordinarium`) from the Kyriale. A feast
|
|
447
472
|
* drives mass selection; `mass` pins a kyriale number directly.
|
|
@@ -459,6 +484,10 @@ export function getOrdinary(query) {
|
|
|
459
484
|
if (query.ordinary) {
|
|
460
485
|
assertOrdinaryCodes(Array.isArray(query.ordinary) ? query.ordinary : [query.ordinary], "ordinarium");
|
|
461
486
|
}
|
|
487
|
+
if (query.section != null && !KYRIALE_SECTIONS.has(query.section)) {
|
|
488
|
+
throw new Error(`ordinarium: unknown section "${query.section}" ` +
|
|
489
|
+
`(expected ${[...KYRIALE_SECTIONS].join(", ")}).`);
|
|
490
|
+
}
|
|
462
491
|
const feasts = toArray(query.feast);
|
|
463
492
|
assertFeasts(feasts, "ordinarium");
|
|
464
493
|
// `mode` accepts a scalar or an array, with cantus's semantics: match any.
|
|
@@ -480,7 +509,7 @@ export function getOrdinary(query) {
|
|
|
480
509
|
return ordinaryForFeast(f, query.mass, filterModes, adm);
|
|
481
510
|
});
|
|
482
511
|
}
|
|
483
|
-
else if (query.mass != null || query.ordinary) {
|
|
512
|
+
else if (query.mass != null || query.ordinary || query.section) {
|
|
484
513
|
// Direct kyriale query without feast context — same admissibility rule as
|
|
485
514
|
// cantus, so the two doors cannot disagree.
|
|
486
515
|
let entries = KYRIALE.slice();
|
|
@@ -490,6 +519,8 @@ export function getOrdinary(query) {
|
|
|
490
519
|
}
|
|
491
520
|
if (query.mass != null)
|
|
492
521
|
entries = entries.filter((e) => e.mass === query.mass);
|
|
522
|
+
if (query.section)
|
|
523
|
+
entries = entries.filter((e) => e.section === query.section);
|
|
493
524
|
if (query.ordinary)
|
|
494
525
|
entries = entries.filter((e) => e.office === query.ordinary);
|
|
495
526
|
if (filterModes)
|
|
@@ -502,6 +533,9 @@ export function getOrdinary(query) {
|
|
|
502
533
|
return [];
|
|
503
534
|
}
|
|
504
535
|
// Apply remaining CantusQuery filters
|
|
536
|
+
if (feasts && query.section) {
|
|
537
|
+
results = results.filter((c) => c.section === query.section);
|
|
538
|
+
}
|
|
505
539
|
if (query.incipit) {
|
|
506
540
|
const needle = query.incipit.toLowerCase();
|
|
507
541
|
results = results.filter((c) => c.incipit.toLowerCase().includes(needle));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Season, Grade, Feast } from "../cal/types.js";
|
|
2
|
+
import type { KyrialeSection } from "../../data/kyriale.js";
|
|
2
3
|
export type { Season, Grade, Feast };
|
|
3
4
|
export type OfficeCode = "an" | "al" | "ca" | "co" | "gr" | "hy" | "in" | "of" | "ps" | "re" | "rb" | "se" | "tr" | "tp" | "or" | "ky" | "va" | "im" | "pa" | "su";
|
|
4
5
|
export type OrdinaryCode = "ke" | "gl" | "cr" | "sa" | "ag" | "be" | "it" | "as" | "va";
|
|
@@ -92,11 +93,14 @@ export interface Chant {
|
|
|
92
93
|
ordinary?: OrdinaryCode;
|
|
93
94
|
ordinarium?: string;
|
|
94
95
|
mass?: number;
|
|
96
|
+
section?: KyrialeSection;
|
|
95
97
|
}
|
|
96
98
|
export interface OrdinaryChant extends Chant {
|
|
97
99
|
ordinary: OrdinaryCode;
|
|
98
100
|
ordinarium: string;
|
|
101
|
+
/** One of Masses I–XVIII; 0 outside the numbered Masses (see `section`). */
|
|
99
102
|
mass: number;
|
|
103
|
+
section: KyrialeSection;
|
|
100
104
|
}
|
|
101
105
|
export interface GenusCount {
|
|
102
106
|
office: OfficeCode;
|
|
@@ -219,7 +223,10 @@ export interface PropriumQuery extends CantusQuery {
|
|
|
219
223
|
export interface OrdinariumQuery extends CantusQuery {
|
|
220
224
|
feast?: Feast | Feast[];
|
|
221
225
|
ordinary?: OrdinaryCode;
|
|
226
|
+
/** One of Masses I–XVIII. */
|
|
222
227
|
mass?: number;
|
|
228
|
+
/** A section of the Kyriale: "mass", "credo", "sprinkling", "ad-libitum", "requiem". */
|
|
229
|
+
section?: KyrialeSection;
|
|
223
230
|
}
|
|
224
231
|
export interface OfficiumQuery extends CantusQuery {
|
|
225
232
|
feast?: Feast | Feast[];
|
package/docs/api/chant.md
CHANGED
|
@@ -311,6 +311,18 @@ The Kyriale is a **partition of the Graduale**, so it is not a `source` and not
|
|
|
311
311
|
a row in the shelf. It stays nameable by `id` and by the part of the Mass it
|
|
312
312
|
belongs to.
|
|
313
313
|
|
|
314
|
+
It is not a book, but it is **printed in** books, and it says which: every
|
|
315
|
+
ordinary chant carries `books: ["lu", "gr"]` and the Graduale's and the Liber
|
|
316
|
+
Usualis's pages, so it can be cited like anything else on the shelf.
|
|
317
|
+
|
|
318
|
+
```js
|
|
319
|
+
tonus.cantus({ id: "gregobase:31", source: "gr" })[0].pages; // [{ page: "28*", … }]
|
|
320
|
+
tonus.cantus({ id: "gregobase:31", source: "lu" })[0].pages; // [{ page: "37", … }]
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
`source` stays `ky` when no book is named — the note a singer holding the
|
|
324
|
+
Kyriale would want — and `ky` is still absent from the book registry.
|
|
325
|
+
|
|
314
326
|
A plain search does not sweep it in: `{ mode: 5 }` returns the shelf. Ask for a
|
|
315
327
|
Kyrie and you get Kyries.
|
|
316
328
|
|
|
@@ -393,8 +405,12 @@ interface PropriumQuery extends CantusQuery {
|
|
|
393
405
|
`ordinarium(query?)` retrieves the fixed chants of the Mass from the
|
|
394
406
|
Kyriale. A feast drives mass selection through its `masses` list, the
|
|
395
407
|
masses the day's Kyriale RUBRIC appoints, derived as described in
|
|
396
|
-
[calendar.md](calendar.md#the-days-feasts--festum); `mass` pins
|
|
397
|
-
|
|
408
|
+
[calendar.md](calendar.md#the-days-feasts--festum); `mass` pins one of the
|
|
409
|
+
eighteen numbered Masses directly, and `section` asks for a part of the book:
|
|
410
|
+
`mass` (Masses I–XVIII), `credo` (Credo I–VI), `sprinkling` (Asperges me,
|
|
411
|
+
Vidi aquam), `ad-libitum` (the Cantus ad libitum) or `requiem` (the Missa pro
|
|
412
|
+
defunctis). Only the `mass` section carries a mass number; every other entry
|
|
413
|
+
reads `mass: 0`. Where the rubric names several masses, the year rotates
|
|
398
414
|
through them (same feast, same year → same answer, every time), and sibling
|
|
399
415
|
printings under one number (Mass I prints two dismissals; Mass XVII prints
|
|
400
416
|
Kyrie A/B/C) rotate with it. Slots resolve independently, which the book
|
|
@@ -408,7 +424,7 @@ const [easter] = tonus.festum({ date: new Date("2026-04-05") });
|
|
|
408
424
|
tonus.ordinarium({ feast: easter });
|
|
409
425
|
// ky Kyrie I (mass 1) — Lux et origo, Paschal time, every year
|
|
410
426
|
// gl Gloria I (mass 1)
|
|
411
|
-
// cr Credo III (mass
|
|
427
|
+
// cr Credo III (mass 0) — the credo rotates on its own cycle
|
|
412
428
|
// sa Sanctus I (mass 1)
|
|
413
429
|
// ag Agnus Dei I (mass 1)
|
|
414
430
|
// it Ite Ia (mass 1)
|
|
@@ -425,7 +441,7 @@ sings Mass XVI whole with the Mass II Benedicamus. The ad libitum appendix
|
|
|
425
441
|
is a **solemnity boost**, reachable only under the festal rubrics (it takes
|
|
426
442
|
its turn in the rotation once every _n + 1_ years); it never reaches a
|
|
427
443
|
penitential day or a feria, and the Requiem settings stay out of every
|
|
428
|
-
calendar-driven pick (reachable by `ordinarium({
|
|
444
|
+
calendar-driven pick (reachable by `ordinarium({ section: "requiem" })` only). The
|
|
429
445
|
Triduum returns no ordinary at all: Good Friday has no Mass, and the
|
|
430
446
|
Vigil's ordinary belongs to Easter. A pinned `mass` overrides the Triduum
|
|
431
447
|
rule and the rotation both.
|
|
@@ -440,6 +456,7 @@ me** (`as`) otherwise.
|
|
|
440
456
|
```js
|
|
441
457
|
tonus.ordinarium({ ordinary: "ke" }); // every Kyrie
|
|
442
458
|
tonus.ordinarium({ mass: 9, ordinary: "gl" }); // Gloria of Cum jubilo
|
|
459
|
+
tonus.ordinarium({ section: "ad-libitum", ordinary: "ke" }); // the eleven ad libitum Kyries
|
|
443
460
|
```
|
|
444
461
|
|
|
445
462
|
| `ordinary` | `ordinarium` |
|
|
@@ -458,13 +475,15 @@ tonus.ordinarium({ mass: 9, ordinary: "gl" }); // Gloria of Cum jubilo
|
|
|
458
475
|
interface OrdinaryChant extends Chant {
|
|
459
476
|
ordinary: OrdinaryCode; // movement code
|
|
460
477
|
ordinarium: string; // Latin movement name, "Kyrie eleison" …
|
|
461
|
-
mass: number;
|
|
478
|
+
mass: number; // 1–18, or 0 outside the numbered Masses
|
|
479
|
+
section: "mass" | "credo" | "sprinkling" | "ad-libitum" | "requiem";
|
|
462
480
|
}
|
|
463
481
|
|
|
464
482
|
interface OrdinariumQuery extends CantusQuery {
|
|
465
483
|
feast?: Feast | Feast[];
|
|
466
484
|
ordinary?: OrdinaryCode;
|
|
467
|
-
mass?: number;
|
|
485
|
+
mass?: number; // 1–18
|
|
486
|
+
section?: "mass" | "credo" | "sprinkling" | "ad-libitum" | "requiem";
|
|
468
487
|
}
|
|
469
488
|
```
|
|
470
489
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tonus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Medieval music analysis: GABC plainchant exports, liturgical calendar, tuning systems, ephemeris, and the harmony of the spheres",
|
|
6
6
|
"author": "Jeffrey Pierce <jeffrey@jeffreypierce.net>",
|