tonus 0.9.0 → 0.10.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 +124 -0
- package/README.md +3 -3
- package/dist/census.d.ts +4 -0
- package/dist/census.js +23 -0
- package/dist/corpus.d.ts +4 -0
- package/dist/corpus.js +17 -0
- package/dist/data/am.js +10220 -3006
- package/dist/data/attestation.js +38640 -4568
- package/dist/data/attestation.json +38639 -4567
- package/dist/data/census.d.ts +2 -2
- package/dist/data/census.js +4 -4
- package/dist/data/corpus-overlap.js +5 -118
- package/dist/data/gr.d.ts +29 -2
- package/dist/data/gr.js +2436 -6411
- package/dist/data/kyriale.d.ts +1 -1
- package/dist/data/kyriale.js +38 -38
- package/dist/data/la.js +12369 -1103
- package/dist/data/lh.js +3139 -158
- package/dist/data/lu.js +17332 -2379
- package/dist/data/nocturnale-romanum.js +10656 -1899
- package/dist/data/psm.js +463 -33
- package/dist/data/types.d.ts +1 -0
- package/dist/engines/chant/chant.js +60 -13
- package/dist/engines/chant/data/masses.js +18 -18
- package/dist/engines/chant/ordinary.js +12 -5
- package/dist/engines/chant/psalm.js +4 -0
- package/dist/engines/chant/types.d.ts +32 -4
- package/dist/engines/chant/types.js +47 -4
- package/dist/engines/score/emitters/moderna.js +150 -12
- package/dist/engines/score/infer.js +1 -1
- package/dist/engines/temper/gabc.d.ts +12 -0
- package/dist/engines/temper/gabc.js +51 -18
- package/dist/index.d.ts +5 -9
- package/dist/inscriptio.d.ts +5 -0
- package/dist/inscriptio.js +23 -0
- package/dist/score.d.ts +6 -0
- package/dist/score.js +20 -0
- package/docs/api/calendar.md +4 -4
- package/docs/api/census.md +31 -22
- package/docs/api/chant.md +102 -82
- package/docs/api/heavens.md +7 -7
- package/docs/api/index.md +40 -6
- package/docs/api/score.md +44 -44
- package/docs/api/tuning.md +14 -14
- package/package.json +22 -4
- package/dist/data/ams.d.ts +0 -5
- package/dist/data/ams.js +0 -122
- package/dist/data/cot.d.ts +0 -5
- package/dist/data/cot.js +0 -172
- package/dist/data/cse.d.ts +0 -5
- package/dist/data/cse.js +0 -122
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OFFICIA, MODI, CANTUS_QUERY_KEYS } from "./types.js";
|
|
1
|
+
import { OFFICIA, MODI, CANTUS_QUERY_KEYS, assertOrdinaryCodes } from "./types.js";
|
|
2
2
|
import { CORPUS_OVERLAP, CORPUS_FULL } from "../../data/corpus-overlap.js";
|
|
3
3
|
import { attestationCutoff, chantAdmissible } from "./attest.js";
|
|
4
4
|
import { GR_DATA, GR_SOURCE } from "../../data/gr.js";
|
|
@@ -9,11 +9,8 @@ import { AM_DATA, AM_SOURCE } from "../../data/am.js";
|
|
|
9
9
|
import { NR_DATA, NR_SOURCE } from "../../data/nocturnale-romanum.js";
|
|
10
10
|
// Office books — the antiphons and short responsories that fill weekday
|
|
11
11
|
// office slots; see ChantSource in types.ts for why.
|
|
12
|
-
import { AMS_DATA, AMS_SOURCE } from "../../data/ams.js";
|
|
13
12
|
import { PSM_DATA, PSM_SOURCE } from "../../data/psm.js";
|
|
14
13
|
// Further marked Solesmes books — see ChantSource in types.ts.
|
|
15
|
-
import { CSE_DATA, CSE_SOURCE } from "../../data/cse.js";
|
|
16
|
-
import { COT_DATA, COT_SOURCE } from "../../data/cot.js";
|
|
17
14
|
import { KYRIALE } from "../../data/kyriale.js";
|
|
18
15
|
import { entryToOrdinaryChant } from "./ordinary.js";
|
|
19
16
|
function modusOf(mode) {
|
|
@@ -64,11 +61,12 @@ function chantFromGABC(query) {
|
|
|
64
61
|
})();
|
|
65
62
|
const office = (query.office
|
|
66
63
|
? (Array.isArray(query.office) ? query.office[0] : query.office)
|
|
67
|
-
: officeFromHeader ?? "
|
|
64
|
+
: officeFromHeader ?? "va");
|
|
68
65
|
return [{
|
|
69
66
|
id: `gabc:${incipit.toLowerCase().replace(/\s+/g, "_") || "untitled"}`,
|
|
70
67
|
incipit,
|
|
71
68
|
gabc: body,
|
|
69
|
+
books: ["user"],
|
|
72
70
|
office,
|
|
73
71
|
genus: OFFICIA[office] ?? office,
|
|
74
72
|
mode: mode ?? null,
|
|
@@ -77,10 +75,53 @@ function chantFromGABC(query) {
|
|
|
77
75
|
source: { book: "User", year: null, editor: null, code: "user" },
|
|
78
76
|
}];
|
|
79
77
|
}
|
|
78
|
+
// Every printing's pages, by chant id then book code. Kept beside the corpus
|
|
79
|
+
// rather than on the Chant: a caller gets the pages for the book they ASKED
|
|
80
|
+
// about (see presentAs), and carrying all of them on every record would invite
|
|
81
|
+
// reading the wrong volume's citation.
|
|
82
|
+
const PAGES_BY_BOOK = new Map();
|
|
83
|
+
/**
|
|
84
|
+
* The same chant, presented as the book the caller asked for.
|
|
85
|
+
*
|
|
86
|
+
* A melody printed in several books is stored once, under one owner, so its
|
|
87
|
+
* `source` and `pages` are the owner's. Ask `cantus({ source: "gr" })` and you
|
|
88
|
+
* mean the Graduale: you want the Graduale's bibliographic record and the
|
|
89
|
+
* Graduale's page numbers, not the Liber Usualis's because that is where the
|
|
90
|
+
* record happens to be filed. `books` is unchanged — the shelf is the shelf.
|
|
91
|
+
*/
|
|
92
|
+
// The owner's pages, and the whole map remembered for presentAs. A generated
|
|
93
|
+
// record always carries the map (one key when one book prints it); a hand-built
|
|
94
|
+
// Chant may carry a bare array, which is taken as it stands.
|
|
95
|
+
function pagesFor(c, code) {
|
|
96
|
+
const byBook = c.pages;
|
|
97
|
+
if (Array.isArray(byBook))
|
|
98
|
+
return byBook;
|
|
99
|
+
const map = byBook;
|
|
100
|
+
PAGES_BY_BOOK.set(c.id, map);
|
|
101
|
+
return (code && map[code]) || Object.values(map)[0] || [];
|
|
102
|
+
}
|
|
103
|
+
function presentAs(chant, code) {
|
|
104
|
+
if (chant.source.code === code)
|
|
105
|
+
return chant;
|
|
106
|
+
const src = SOURCES[code];
|
|
107
|
+
if (!src)
|
|
108
|
+
return chant;
|
|
109
|
+
const pages = PAGES_BY_BOOK.get(chant.id)?.[code];
|
|
110
|
+
return { ...chant, source: src, pages: pages ?? chant.pages };
|
|
111
|
+
}
|
|
80
112
|
function withLabels(c, source) {
|
|
113
|
+
// `books` rides the generated data. The `??` is not defensive padding: the
|
|
114
|
+
// Kyriale is emitted by a different builder that never carried the field, and
|
|
115
|
+
// a chant printed by exactly one book is fully described by its own code. A
|
|
116
|
+
// record arriving without a listing is therefore listed under itself, which is
|
|
117
|
+
// true, rather than under nothing, which would make it unfindable by source.
|
|
81
118
|
return {
|
|
82
119
|
...c,
|
|
83
120
|
source,
|
|
121
|
+
// The one narrowing point: ChantData carries string[] so the generated
|
|
122
|
+
// literals do not hand tsc a 9-member union ~7,700 times over (TS2590).
|
|
123
|
+
books: (c.books?.length ? c.books : [source.code ?? "user"]),
|
|
124
|
+
pages: pagesFor(c, source.code),
|
|
84
125
|
genus: OFFICIA[c.office] ?? c.office,
|
|
85
126
|
modus: modusOf(c.mode),
|
|
86
127
|
};
|
|
@@ -92,10 +133,7 @@ const CORPUS = [
|
|
|
92
133
|
...LH_DATA.map((c) => withLabels(c, LH_SOURCE)),
|
|
93
134
|
...AM_DATA.map((c) => withLabels(c, AM_SOURCE)),
|
|
94
135
|
...NR_DATA.map((c) => withLabels(c, NR_SOURCE)),
|
|
95
|
-
...AMS_DATA.map((c) => withLabels(c, AMS_SOURCE)),
|
|
96
136
|
...PSM_DATA.map((c) => withLabels(c, PSM_SOURCE)),
|
|
97
|
-
...CSE_DATA.map((c) => withLabels(c, CSE_SOURCE)),
|
|
98
|
-
...COT_DATA.map((c) => withLabels(c, COT_SOURCE)),
|
|
99
137
|
];
|
|
100
138
|
// The Kyriale, addressable but not shelved — ruled 2026-08-04.
|
|
101
139
|
//
|
|
@@ -128,8 +166,7 @@ function byId() {
|
|
|
128
166
|
export const SOURCES = {
|
|
129
167
|
gr: GR_SOURCE, lu: LU_SOURCE, la: LA_SOURCE, lh: LH_SOURCE, am: AM_SOURCE, nr: NR_SOURCE,
|
|
130
168
|
// office books — provenance, not acquisition
|
|
131
|
-
|
|
132
|
-
cse: CSE_SOURCE, cot: COT_SOURCE,
|
|
169
|
+
psm: PSM_SOURCE,
|
|
133
170
|
};
|
|
134
171
|
// Tally a book's genre and mode distribution — computed once per code, cached.
|
|
135
172
|
const _corpusCache = new Map();
|
|
@@ -213,7 +250,7 @@ function corpusLedger() {
|
|
|
213
250
|
}
|
|
214
251
|
_ledger = {
|
|
215
252
|
count: byId().size,
|
|
216
|
-
listings: CORPUS.length,
|
|
253
|
+
listings: CORPUS.reduce((n, c) => n + c.books.filter((b) => b in SOURCES).length, 0),
|
|
217
254
|
// Sum only what was measured: a book outside GregoBase reports null rather
|
|
218
255
|
// than a false zero, and adding null in as 0 would understate the shelf
|
|
219
256
|
// while looking like a total.
|
|
@@ -231,7 +268,10 @@ function oneCorpus(code) {
|
|
|
231
268
|
const src = SOURCES[code];
|
|
232
269
|
if (!src)
|
|
233
270
|
throw new Error(`Unknown corpus code: "${code}" (expected ${Object.keys(SOURCES).join(", ")})`);
|
|
234
|
-
|
|
271
|
+
// What the book PRINTS. Reading `source.code` here would count only the
|
|
272
|
+
// records this book happens to store, which is why the Antiphonarius used to
|
|
273
|
+
// report 1,422 of a book it holds 2,501 of.
|
|
274
|
+
const chants = CORPUS.filter((c) => c.books.includes(code));
|
|
235
275
|
// Genre distribution — count by office code, descending by count.
|
|
236
276
|
const officeCounts = new Map();
|
|
237
277
|
// Mode distribution — count by mode 1–8; everything else (p/d/e, null) into one bucket.
|
|
@@ -332,6 +372,8 @@ export function getChants(query) {
|
|
|
332
372
|
// source and report an id from any book as belonging to that one.
|
|
333
373
|
const ids = toArray(query.id);
|
|
334
374
|
const ordinaries = toArray(query.ordinary);
|
|
375
|
+
if (ordinaries)
|
|
376
|
+
assertOrdinaryCodes(ordinaries, "cantus");
|
|
335
377
|
// The default pool is the SHELF. The Kyriale is not on it (see KYRIALE_CHANTS
|
|
336
378
|
// above), so it is reached deliberately: by `id`, which must resolve any
|
|
337
379
|
// chant tonus holds, or by `ordinary`, which asks for a part of the Mass and
|
|
@@ -350,7 +392,12 @@ export function getChants(query) {
|
|
|
350
392
|
const sources = toArray(query.source);
|
|
351
393
|
if (sources) {
|
|
352
394
|
const set = new Set(sources);
|
|
353
|
-
out = out
|
|
395
|
+
out = out
|
|
396
|
+
.filter((c) => c.books.some((b) => set.has(b)))
|
|
397
|
+
.map((c) => {
|
|
398
|
+
const asked = c.books.find((b) => set.has(b));
|
|
399
|
+
return asked ? presentAs(c, asked) : c;
|
|
400
|
+
});
|
|
354
401
|
}
|
|
355
402
|
const offices = toArray(query.office);
|
|
356
403
|
if (offices) {
|
|
@@ -309,123 +309,123 @@ export function massesForRubric(rubric) {
|
|
|
309
309
|
export const MASS_CENTURY = {
|
|
310
310
|
// Mass I — Lux et Origo
|
|
311
311
|
1: {
|
|
312
|
-
|
|
312
|
+
ke: { from: 10, to: 10, printed: "X. s." },
|
|
313
313
|
gl: { from: 10, to: 10, printed: "X. s." },
|
|
314
314
|
sa: { from: 10, to: 10, printed: "X. s." },
|
|
315
315
|
ag: { from: 10, to: 10, printed: "X. s." },
|
|
316
316
|
},
|
|
317
317
|
// Mass II — Kyrie fons bonitatis
|
|
318
318
|
2: {
|
|
319
|
-
|
|
319
|
+
ke: { from: 10, to: 10, printed: "X. s." },
|
|
320
320
|
gl: { from: 13, to: 13, printed: "XIII. s." },
|
|
321
321
|
ag: { from: 10, to: 10, printed: "X. s." },
|
|
322
322
|
},
|
|
323
323
|
// Mass III — Kyrie Deus sempiterne
|
|
324
324
|
3: {
|
|
325
|
-
|
|
325
|
+
ke: { from: 11, to: 11, inferred: true, printed: "XI. s." },
|
|
326
326
|
gl: { from: 11, to: 11, printed: "XI. s." },
|
|
327
327
|
sa: { from: 12, to: 12, alt: 11, printed: "(XI) XII. s." },
|
|
328
328
|
ag: { from: 11, to: 12, printed: "XI-XII. s." },
|
|
329
329
|
},
|
|
330
330
|
// Mass IV — Cunctipotens Genitor Deus
|
|
331
331
|
4: {
|
|
332
|
-
|
|
332
|
+
ke: { from: 10, to: 10, printed: "X. s." },
|
|
333
333
|
gl: { from: 10, to: 10, printed: "X. s." },
|
|
334
334
|
sa: { from: 11, to: 11, printed: "XI. s." },
|
|
335
335
|
ag: { from: 13, to: 13, alt: 12, printed: "(XII) XIII. s." },
|
|
336
336
|
},
|
|
337
337
|
// Mass V — Kyrie magnæ Deus potentiæ
|
|
338
338
|
5: {
|
|
339
|
-
|
|
339
|
+
ke: { from: 13, to: 13, printed: "XIII. s." },
|
|
340
340
|
gl: { from: 12, to: 12, printed: "XII. s." },
|
|
341
341
|
sa: { from: 12, to: 12, printed: "XII. s." },
|
|
342
342
|
ag: { from: 12, to: 12, printed: "XII. s." },
|
|
343
343
|
},
|
|
344
344
|
// Mass VI — Kyrie Rex Genitor
|
|
345
345
|
6: {
|
|
346
|
-
|
|
346
|
+
ke: { from: 10, to: 10, printed: "X. s." },
|
|
347
347
|
gl: { from: 10, to: 10, printed: "X. s." },
|
|
348
348
|
sa: { from: 11, to: 11, printed: "XI. s." },
|
|
349
349
|
ag: { from: 11, to: 11, printed: "XI. s." },
|
|
350
350
|
},
|
|
351
351
|
// Mass VII — Kyrie Rex splendes
|
|
352
352
|
7: {
|
|
353
|
-
|
|
353
|
+
ke: { from: 10, to: 10, printed: "X. s." },
|
|
354
354
|
gl: { from: 12, to: 12, printed: "XII. s." },
|
|
355
355
|
sa: { from: 11, to: 11, printed: "XI. s." },
|
|
356
356
|
ag: { from: 15, to: 15, printed: "XV. s." },
|
|
357
357
|
},
|
|
358
358
|
// Mass VIII — de Angelis
|
|
359
359
|
8: {
|
|
360
|
-
|
|
360
|
+
ke: { from: 15, to: 16, printed: "XV-XVI. s." },
|
|
361
361
|
gl: { from: 16, to: 16, printed: "XVI. s." },
|
|
362
362
|
sa: { from: 12, to: 12, alt: 11, printed: "(XI) XII. s." },
|
|
363
363
|
ag: { from: 15, to: 15, printed: "XV. s." },
|
|
364
364
|
},
|
|
365
365
|
// Mass IX — Cum Jubilo
|
|
366
366
|
9: {
|
|
367
|
-
|
|
367
|
+
ke: { from: 12, to: 12, printed: "XII. s." },
|
|
368
368
|
gl: { from: 11, to: 11, printed: "XI. s." },
|
|
369
369
|
sa: { from: 14, to: 14, printed: "XIV. s." },
|
|
370
370
|
ag: { from: 13, to: 13, alt: 10, printed: "(X) XIII. s." },
|
|
371
371
|
},
|
|
372
372
|
// Mass X — Alme Pater
|
|
373
373
|
10: {
|
|
374
|
-
|
|
374
|
+
ke: { from: 11, to: 11, printed: "XI. s." },
|
|
375
375
|
gl: { from: 15, to: 15, printed: "XV. s." },
|
|
376
376
|
sa: { from: null, to: null, printed: "?. s." },
|
|
377
377
|
ag: { from: 12, to: 12, printed: "XII. s." },
|
|
378
378
|
},
|
|
379
379
|
// Mass XI — Orbis factor
|
|
380
380
|
11: {
|
|
381
|
-
|
|
381
|
+
ke: { from: 14, to: 16, alt: 10, printed: "(X) XIV-XVI. s." },
|
|
382
382
|
gl: { from: 10, to: 10, printed: "X. s." },
|
|
383
383
|
sa: { from: 10, to: 10, printed: "X. s." },
|
|
384
384
|
ag: { from: 14, to: 14, printed: "XIV. s." },
|
|
385
385
|
},
|
|
386
386
|
// Mass XII — Pater cuncta
|
|
387
387
|
12: {
|
|
388
|
-
|
|
388
|
+
ke: { from: 12, to: 12, printed: "XII. s." },
|
|
389
389
|
gl: { from: 12, to: 12, inferred: true, printed: "XII. s." },
|
|
390
390
|
sa: { from: 13, to: 13, printed: "XIII. s." },
|
|
391
391
|
ag: { from: 11, to: 11, printed: "XI. s." },
|
|
392
392
|
},
|
|
393
393
|
// Mass XIII — Stelliferi Conditor orbis
|
|
394
394
|
13: {
|
|
395
|
-
|
|
395
|
+
ke: { from: 11, to: 11, printed: "XI. s." },
|
|
396
396
|
gl: { from: 12, to: 12, printed: "XII. s." },
|
|
397
397
|
sa: { from: 13, to: 13, printed: "XIII. s." },
|
|
398
398
|
ag: { from: null, to: null, printed: "? s." },
|
|
399
399
|
},
|
|
400
400
|
// Mass XIV — Jesu Redemptor
|
|
401
401
|
14: {
|
|
402
|
-
|
|
402
|
+
ke: { from: 10, to: 10, inferred: true, printed: "X. s." },
|
|
403
403
|
gl: { from: 10, to: 10, printed: "X. s." },
|
|
404
404
|
sa: { from: 12, to: 12, inferred: true, printed: "XII. s." },
|
|
405
405
|
ag: { from: 13, to: 13, printed: "XIII. s." },
|
|
406
406
|
},
|
|
407
407
|
// Mass XV — Dominator Deus
|
|
408
408
|
15: {
|
|
409
|
-
|
|
409
|
+
ke: { from: 11, to: 13, printed: "XI-XIII. s." },
|
|
410
410
|
gl: { from: 10, to: 10, printed: "X. s." },
|
|
411
411
|
sa: { from: 10, to: 10, printed: "X. s." },
|
|
412
412
|
ag: { from: 14, to: 14, alt: 12, printed: "(XII) XIV. s." },
|
|
413
413
|
},
|
|
414
414
|
// Mass XVI
|
|
415
415
|
16: {
|
|
416
|
-
|
|
416
|
+
ke: { from: 11, to: 13, printed: "XI-XIII. s." },
|
|
417
417
|
sa: { from: 13, to: 13, printed: "XIII. s." },
|
|
418
418
|
ag: { from: 10, to: 11, printed: "X-XI. s." },
|
|
419
419
|
},
|
|
420
420
|
// Mass XVII
|
|
421
421
|
17: {
|
|
422
|
-
|
|
422
|
+
ke: { from: 15, to: 17, alt: 10, printed: "(X) XV-XVII. s." },
|
|
423
423
|
sa: { from: 11, to: 11, printed: "XI. s." },
|
|
424
424
|
ag: { from: 13, to: 13, printed: "XIII. s." },
|
|
425
425
|
},
|
|
426
426
|
// Mass XVIII — Deus Genitor alme
|
|
427
427
|
18: {
|
|
428
|
-
|
|
428
|
+
ke: { from: 11, to: 11, printed: "XI. s." },
|
|
429
429
|
sa: { from: 13, to: 13, printed: "XIII. s." },
|
|
430
430
|
ag: { from: 12, to: 12, printed: "XII. s." },
|
|
431
431
|
},
|
|
@@ -5,7 +5,7 @@ import { MASSES, AD_LIB, WHOLE_MASS_RUBRICS, partWithinEra, } from "./data/masse
|
|
|
5
5
|
import { KYRIALE } from "../../data/kyriale.js";
|
|
6
6
|
import { attestationCutoff, eraCutoff, chantAdmissible } from "./attest.js";
|
|
7
7
|
import { CANTUS_QUERY_KEYS } from "./types.js";
|
|
8
|
-
import { KY_SOURCE, MODI, ORDINARIA, } from "./types.js";
|
|
8
|
+
import { KY_SOURCE, MODI, ORDINARIA, OFFICIA, assertOrdinaryCodes, } from "./types.js";
|
|
9
9
|
const ORDINARY_OFFICES = new Set(Object.keys(ORDINARIA));
|
|
10
10
|
const MODE_PAIRS = [[1, 2], [3, 4], [5, 6], [7, 8]];
|
|
11
11
|
const CREDO_PRIORITY = ["IV", "III", "I", "II", "V", "VI"];
|
|
@@ -279,13 +279,17 @@ function selectCredoCode(feast, allowed, year) {
|
|
|
279
279
|
export function entryToOrdinaryChant(entry) {
|
|
280
280
|
const ordinary = ORDINARY_OFFICES.has(entry.office)
|
|
281
281
|
? entry.office
|
|
282
|
-
: "
|
|
282
|
+
: "ke";
|
|
283
283
|
return {
|
|
284
284
|
id: entry.id,
|
|
285
285
|
incipit: entry.incipit,
|
|
286
286
|
gabc: entry.gabc,
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
books: ["ky"],
|
|
288
|
+
office: "ky",
|
|
289
|
+
// The genus follows the office code, as everywhere else — OFFICIA.ky.
|
|
290
|
+
// It read "Ordinarium" while the office read "or", which was tonus's own
|
|
291
|
+
// pairing; GregoBase files these under ky = Kyriale.
|
|
292
|
+
genus: OFFICIA.ky,
|
|
289
293
|
mode: entry.mode ? String(entry.mode) : null,
|
|
290
294
|
modus: entry.mode ? (MODI[String(entry.mode)] ?? null) : null,
|
|
291
295
|
pages: [],
|
|
@@ -353,7 +357,7 @@ function ordinaryForFeast(feast, pinMass, filterModes, admissible) {
|
|
|
353
357
|
return best ? entryToOrdinaryChant(best) : null;
|
|
354
358
|
};
|
|
355
359
|
const results = [];
|
|
356
|
-
const ky = pick("
|
|
360
|
+
const ky = pick("ke");
|
|
357
361
|
if (ky)
|
|
358
362
|
results.push(ky);
|
|
359
363
|
// Gloria is omitted under the gloria-less rubrics — penitential Sundays and
|
|
@@ -452,6 +456,9 @@ export function getOrdinary(query) {
|
|
|
452
456
|
throw new Error(`ordinarium: unknown query key(s) ${unknown.map((k) => `"${k}"`).join(", ")} ` +
|
|
453
457
|
`(expected ${[...ORDINARIUM_QUERY_KEYS].join(", ")}).`);
|
|
454
458
|
}
|
|
459
|
+
if (query.ordinary) {
|
|
460
|
+
assertOrdinaryCodes(Array.isArray(query.ordinary) ? query.ordinary : [query.ordinary], "ordinarium");
|
|
461
|
+
}
|
|
455
462
|
const feasts = toArray(query.feast);
|
|
456
463
|
assertFeasts(feasts, "ordinarium");
|
|
457
464
|
// `mode` accepts a scalar or an array, with cantus's semantics: match any.
|
|
@@ -36,6 +36,10 @@ function verseToChant(v, mode, differentia, intonation, inDirectum, solemn) {
|
|
|
36
36
|
id: `psalm:${v.psalm}:${v.verse}`,
|
|
37
37
|
incipit: v.half1.slice(0, 40),
|
|
38
38
|
gabc,
|
|
39
|
+
// Empty, and meaningfully so: a verse pointed to a tone at request time is
|
|
40
|
+
// not a printing of any book on the shelf, which is exactly why no
|
|
41
|
+
// `cantus({ source })` can ever return one.
|
|
42
|
+
books: [],
|
|
39
43
|
office: "ps",
|
|
40
44
|
genus: "Psalmus",
|
|
41
45
|
mode: String(mode),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Season, Grade, Feast } from "../cal/types.js";
|
|
2
2
|
export type { Season, Grade, Feast };
|
|
3
|
-
export type OfficeCode = "an" | "al" | "ca" | "co" | "gr" | "hy" | "in" | "of" | "ps" | "re" | "rb" | "se" | "tr" | "tp" | "or" | "im" | "pa" | "su";
|
|
4
|
-
export type OrdinaryCode = "
|
|
3
|
+
export type OfficeCode = "an" | "al" | "ca" | "co" | "gr" | "hy" | "in" | "of" | "ps" | "re" | "rb" | "se" | "tr" | "tp" | "or" | "ky" | "va" | "im" | "pa" | "su";
|
|
4
|
+
export type OrdinaryCode = "ke" | "gl" | "cr" | "sa" | "ag" | "be" | "it" | "as" | "va";
|
|
5
5
|
/**
|
|
6
6
|
* The books a chant can come from. `source` is PROVENANCE, not an acquisition
|
|
7
7
|
* unit: a book appears here because some chant the liturgy asks for is found in
|
|
@@ -24,7 +24,7 @@ export type OrdinaryCode = "ky" | "gl" | "cr" | "sa" | "ag" | "be" | "it" | "as"
|
|
|
24
24
|
* dies" — the Friday Vespers antiphon the widening was for. Every chant tonus
|
|
25
25
|
* now ships carries rhythmic notation.
|
|
26
26
|
*/
|
|
27
|
-
export type ChantSource = "gr" | "lu" | "la" | "lh" | "am" | "nr" | "
|
|
27
|
+
export type ChantSource = "gr" | "lu" | "la" | "lh" | "am" | "nr" | "psm";
|
|
28
28
|
export type CanonicalHour = "matutinum" | "laudes" | "prima" | "tertia" | "sexta" | "nona" | "vesperae" | "completorium";
|
|
29
29
|
/** The eight canonical hours in the order they are sung, Matins first. The
|
|
30
30
|
* order is the content: a day's office read out of sequence is not the day's
|
|
@@ -38,6 +38,18 @@ export declare const CANTUS_QUERY_KEYS: Set<string>;
|
|
|
38
38
|
export declare const MODI: Readonly<Record<string, string>>;
|
|
39
39
|
export declare const OFFICIA: Readonly<Record<OfficeCode, string>>;
|
|
40
40
|
export declare const ORDINARIA: Readonly<Record<string, string>>;
|
|
41
|
+
/**
|
|
42
|
+
* An ordinary code the vocabulary above does not carry is a MALFORMED query,
|
|
43
|
+
* not a no-match, and both doors that take one say so here.
|
|
44
|
+
*
|
|
45
|
+
* The query contract makes a no-match `[]` and a malformation a throw, and an
|
|
46
|
+
* unknown code cannot be a no-match: there is no chant it failed to find,
|
|
47
|
+
* because it never named a part of the Mass. The 0.10.0 rename is why this is
|
|
48
|
+
* a guard rather than a comment — `ky` was the Kyrie through 0.9.1 and is the
|
|
49
|
+
* Kyriale roll-up now, so the one code most likely to arrive stale is the one
|
|
50
|
+
* that would otherwise return an empty list and look like an answer.
|
|
51
|
+
*/
|
|
52
|
+
export declare function assertOrdinaryCodes(codes: readonly string[], method: string): void;
|
|
41
53
|
export declare const KY_SOURCE: Chant["source"];
|
|
42
54
|
export interface Chant {
|
|
43
55
|
id: string;
|
|
@@ -61,6 +73,22 @@ export interface Chant {
|
|
|
61
73
|
scanSource?: string | null;
|
|
62
74
|
code?: ChantSource | "user" | "ky";
|
|
63
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* Every book that PRINTS this chant, the one storing the record first.
|
|
78
|
+
*
|
|
79
|
+
* A melody printed in several books is stored once and listed under each, so
|
|
80
|
+
* `books` — not `source.code` — is what `cantus({ source })` and `corpus(code)`
|
|
81
|
+
* read. `source` names the book the record is filed under; `books` names the
|
|
82
|
+
* shelf. Until 2026-08-31 the two were conflated, which stored the 928 chants
|
|
83
|
+
* the Graduale and the Liber Usualis share twice over while hiding the 1,079
|
|
84
|
+
* the Usualis shares with the Antiphonarius from `corpus("la")` entirely.
|
|
85
|
+
*
|
|
86
|
+
* `books[0]` is the owner. A chant that IS a printing carries its own code
|
|
87
|
+
* even when no book on the shelf holds it — "user" for raw GABC, "ky" for a
|
|
88
|
+
* Kyriale printing. It is empty only for a chant that is not a printing at
|
|
89
|
+
* all: a psalm verse pointed to a tone at request time.
|
|
90
|
+
*/
|
|
91
|
+
books: (ChantSource | "user" | "ky")[];
|
|
64
92
|
ordinary?: OrdinaryCode;
|
|
65
93
|
ordinarium?: string;
|
|
66
94
|
mass?: number;
|
|
@@ -151,7 +179,7 @@ export interface CantusQuery {
|
|
|
151
179
|
office?: OfficeCode | OfficeCode[];
|
|
152
180
|
source?: ChantSource | ChantSource[];
|
|
153
181
|
/**
|
|
154
|
-
* A part of the Mass ordinary — `"
|
|
182
|
+
* A part of the Mass ordinary — `"ke"` for the Kyries, `"gl"` the Glorias,
|
|
155
183
|
* and so on. This is the door to the Kyriale, which is addressable but not
|
|
156
184
|
* shelved (it is a partition of the Graduale, not a book), so it is absent
|
|
157
185
|
* from `source` and from an unfiltered search. Asking for an ordinary can
|
|
@@ -33,14 +33,36 @@ export const OFFICIA = Object.freeze({
|
|
|
33
33
|
se: "Sequentia",
|
|
34
34
|
// Reported in the pre-cut tallies, not shipped — see OfficeCode above.
|
|
35
35
|
im: "Improperia", // the Good Friday Reproaches (Popule meus)
|
|
36
|
-
pa: "
|
|
36
|
+
pa: "Prosa",
|
|
37
37
|
su: "Supplicatio", // litanies and supplications (the Easter Vigil litany)
|
|
38
38
|
tr: "Tractus",
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// ── Corrected 2026-08-31 against GregoBase's own vocabulary ──────────────
|
|
40
|
+
// These three were read off the two-letter codes rather than off the source,
|
|
41
|
+
// and two of them named the wrong thing entirely. GregoBase's include/txt.php
|
|
42
|
+
// carries $txt['usage']: or = Toni Communes, tp = Tropa, pa = Prosa. `or` was
|
|
43
|
+
// the costly one — tonus stamped "Ordinarium" on it and used the code for the
|
|
44
|
+
// Mass ordinary, which meant a genus tonus invented was sitting on a genus the
|
|
45
|
+
// database already had, and 132 real Toni Communes chants were dropped at the
|
|
46
|
+
// extractor for want of a mapping.
|
|
47
|
+
tp: "Tropa",
|
|
48
|
+
or: "Toni Communes",
|
|
49
|
+
// The Mass ordinary's roll-up, and GregoBase's word for it: the database files
|
|
50
|
+
// every ordinary chant under office-part `ky`. The part itself rides
|
|
51
|
+
// `ordinary` (ke · gl · cr · sa · ag · …), which is the identity every query
|
|
52
|
+
// and the census bin on; this is the genus above them.
|
|
53
|
+
ky: "Kyriale",
|
|
54
|
+
// The catch-all, and GregoBase's own name for it. A chant whose office-part
|
|
55
|
+
// tonus does not recognise used to fall to `or`, which was harmless while `or`
|
|
56
|
+
// was a label tonus had invented and is not now that it means Toni Communes —
|
|
57
|
+
// an unknown chant would have been filed as a recitation formula, and the
|
|
58
|
+
// census excludes those. NOTE for review: `va` is also an ORDINARY code (Vidi
|
|
59
|
+
// aquam). Office codes and ordinary codes are separate vocabularies — `ky`
|
|
60
|
+
// lived in both until today — but if that overlap is unwanted, this is the
|
|
61
|
+
// one to rename, not the ordinary.
|
|
62
|
+
va: "Varia",
|
|
41
63
|
});
|
|
42
64
|
export const ORDINARIA = Object.freeze({
|
|
43
|
-
|
|
65
|
+
ke: "Kyrie eleison",
|
|
44
66
|
gl: "Gloria",
|
|
45
67
|
cr: "Credo",
|
|
46
68
|
sa: "Sanctus",
|
|
@@ -50,6 +72,27 @@ export const ORDINARIA = Object.freeze({
|
|
|
50
72
|
as: "Asperges",
|
|
51
73
|
va: "Vidi aquam",
|
|
52
74
|
});
|
|
75
|
+
/**
|
|
76
|
+
* An ordinary code the vocabulary above does not carry is a MALFORMED query,
|
|
77
|
+
* not a no-match, and both doors that take one say so here.
|
|
78
|
+
*
|
|
79
|
+
* The query contract makes a no-match `[]` and a malformation a throw, and an
|
|
80
|
+
* unknown code cannot be a no-match: there is no chant it failed to find,
|
|
81
|
+
* because it never named a part of the Mass. The 0.10.0 rename is why this is
|
|
82
|
+
* a guard rather than a comment — `ky` was the Kyrie through 0.9.1 and is the
|
|
83
|
+
* Kyriale roll-up now, so the one code most likely to arrive stale is the one
|
|
84
|
+
* that would otherwise return an empty list and look like an answer.
|
|
85
|
+
*/
|
|
86
|
+
export function assertOrdinaryCodes(codes, method) {
|
|
87
|
+
const unknown = codes.filter((c) => !(c in ORDINARIA));
|
|
88
|
+
if (unknown.length) {
|
|
89
|
+
throw new Error(`${method}: unknown ordinary code(s) ${unknown.map((c) => `"${c}"`).join(", ")} ` +
|
|
90
|
+
`(expected ${Object.keys(ORDINARIA).join(", ")}).` +
|
|
91
|
+
(unknown.includes("ky")
|
|
92
|
+
? ` The Kyrie is "ke" from 0.10.0 — "ky" is now the Kyriale genus (OFFICIA.ky).`
|
|
93
|
+
: ""));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
53
96
|
// The Kyriale's bibliographic identity. NOT a ChantSource: `ky` is a partition
|
|
54
97
|
// of the Graduale, not a book of its own (chant.ts, CORPUS), so it is not a
|
|
55
98
|
// value `cantus({ source })` takes and not a row in the shelf. This record
|