tonus 0.1.6 → 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.
- package/BIBLIOGRAPHY.md +132 -108
- package/CHANGELOG.md +610 -1
- package/LICENSE +133 -29
- package/README.md +106 -83
- package/dist/data/am.js +2669 -11196
- package/dist/data/ams.d.ts +5 -0
- package/dist/data/ams.js +122 -0
- package/dist/data/attestation.d.ts +19 -0
- package/dist/data/attestation.js +15716 -0
- package/dist/data/attestation.json +15711 -0
- package/dist/data/cadentiae.d.ts +43 -0
- package/dist/data/cadentiae.js +174 -0
- package/dist/data/cal.js +48 -0
- package/dist/data/census.d.ts +12 -0
- package/dist/data/census.js +36 -0
- package/dist/data/commune-office.d.ts +4 -0
- package/dist/data/commune-office.js +2371 -0
- package/dist/data/commune-office.json +2365 -0
- package/dist/data/commune.js +181 -11
- package/dist/data/corpus-overlap.d.ts +24 -0
- package/dist/data/corpus-overlap.js +347 -0
- package/dist/data/cot.d.ts +5 -0
- package/dist/data/cot.js +172 -0
- package/dist/data/cse.d.ts +5 -0
- package/dist/data/cse.js +122 -0
- package/dist/data/gabc-glyphs.d.ts +45 -0
- package/dist/data/gabc-glyphs.js +122 -0
- package/dist/data/gr.js +757 -6394
- package/dist/data/kyriale.js +116 -116
- package/dist/data/la.js +802 -13419
- package/dist/data/lh.js +116 -3473
- package/dist/data/lu.js +935 -17632
- package/dist/data/nocturnale-romanum.d.ts +5 -0
- package/dist/data/nocturnale-romanum.js +3772 -0
- package/dist/data/office-ferial.d.ts +4 -0
- package/dist/data/office-ferial.js +396 -0
- package/dist/data/office-ferial.json +391 -0
- package/dist/data/office-monastic.d.ts +17 -1
- package/dist/data/office-monastic.js +1403 -466
- package/dist/data/office-psalms-monastic.d.ts +13 -1
- package/dist/data/office-psalms-monastic.js +9 -0
- package/dist/data/propers.js +1 -1
- package/dist/data/psalms.js +22919 -5
- package/dist/data/psm.d.ts +5 -0
- package/dist/data/psm.js +122 -0
- package/dist/data/seasonal-respbreve.d.ts +5 -0
- package/dist/data/seasonal-respbreve.js +41 -0
- package/dist/data/seasonal-respbreve.json +35 -0
- package/dist/data/smufl-glyphs.d.ts +17 -0
- package/dist/data/smufl-glyphs.js +1546 -0
- package/dist/data/smufl-glyphs.json +1530 -0
- package/dist/engines/cal/calendar.d.ts +3 -2
- package/dist/engines/cal/calendar.js +105 -29
- package/dist/engines/cal/data/eras.d.ts +35 -0
- package/dist/engines/cal/data/eras.js +128 -0
- package/dist/engines/cal/date.js +44 -0
- package/dist/engines/cal/types.d.ts +15 -3
- package/dist/engines/cal/types.js +5 -5
- package/dist/engines/census/census.d.ts +7 -0
- package/dist/engines/census/census.js +179 -0
- package/dist/engines/census/types.d.ts +55 -0
- package/dist/engines/census/types.js +8 -0
- package/dist/engines/chant/attest.d.ts +39 -0
- package/dist/engines/chant/attest.js +90 -0
- package/dist/engines/chant/chant.d.ts +22 -4
- package/dist/engines/chant/chant.js +274 -14
- package/dist/engines/chant/data/compline.js +2 -1
- package/dist/engines/chant/data/masses.d.ts +56 -4
- package/dist/engines/chant/data/masses.js +305 -80
- package/dist/engines/chant/data/prime.js +1 -1
- package/dist/engines/chant/hour.js +279 -58
- package/dist/engines/chant/ordinary.d.ts +2 -0
- package/dist/engines/chant/ordinary.js +336 -56
- package/dist/engines/chant/propers.js +55 -5
- package/dist/engines/chant/psalm.d.ts +4 -4
- package/dist/engines/chant/psalm.js +25 -11
- package/dist/engines/chant/syllabify.d.ts +1 -0
- package/dist/engines/chant/syllabify.js +90 -17
- package/dist/engines/chant/types.d.ts +145 -10
- package/dist/engines/chant/types.js +38 -3
- package/dist/engines/harmonia/api.js +4 -0
- package/dist/engines/harmonia/data/doctrines.js +3 -1
- package/dist/engines/harmonia/tabula.d.ts +3 -0
- package/dist/engines/harmonia/tabula.js +1 -0
- package/dist/engines/harmonia/voice.d.ts +4 -0
- package/dist/engines/harmonia/voice.js +8 -4
- package/dist/engines/imprint.js +14 -1
- package/dist/engines/planet/orbital.js +4 -4
- package/dist/engines/planet/planet.d.ts +10 -0
- package/dist/engines/planet/planet.js +30 -3
- package/dist/engines/planet/position.js +13 -10
- package/dist/engines/planet/types.d.ts +1 -0
- package/dist/engines/score/api.d.ts +2 -13
- package/dist/engines/score/api.js +21 -8
- package/dist/engines/score/articulation.js +2 -2
- package/dist/engines/score/cadence.d.ts +76 -0
- package/dist/engines/score/cadence.js +96 -0
- package/dist/engines/score/emitters/accidentals.d.ts +21 -0
- package/dist/engines/score/emitters/accidentals.js +88 -0
- package/dist/engines/score/emitters/atramentum.d.ts +107 -0
- package/dist/engines/score/emitters/atramentum.js +239 -0
- package/dist/engines/score/emitters/breaking.d.ts +62 -0
- package/dist/engines/score/emitters/breaking.js +80 -0
- package/dist/engines/score/emitters/moderna.d.ts +38 -0
- package/dist/engines/score/emitters/moderna.js +612 -0
- package/dist/engines/score/emitters/svg.d.ts +143 -0
- package/dist/engines/score/emitters/svg.js +1328 -0
- package/dist/engines/score/emitters/tracks.d.ts +104 -0
- package/dist/engines/score/emitters/tracks.js +728 -0
- package/dist/engines/score/infer.d.ts +3 -3
- package/dist/engines/score/infer.js +2 -2
- package/dist/engines/score/inscriptio.d.ts +69 -0
- package/dist/engines/score/inscriptio.js +138 -0
- package/dist/engines/score/ir.d.ts +2 -2
- package/dist/engines/score/ir.js +59 -12
- package/dist/engines/score/lyric.d.ts +23 -0
- package/dist/engines/score/lyric.js +234 -0
- package/dist/engines/score/meta.d.ts +2 -2
- package/dist/engines/score/modulation.d.ts +12 -0
- package/dist/engines/score/modulation.js +49 -0
- package/dist/engines/score/neume.js +35 -4
- package/dist/engines/score/parse.js +150 -9
- package/dist/engines/score/phrasing.js +4 -3
- package/dist/engines/score/prosody.d.ts +38 -0
- package/dist/engines/score/prosody.js +70 -6
- package/dist/engines/score/tabula.d.ts +37 -5
- package/dist/engines/score/tabula.js +18 -0
- package/dist/engines/score/types.d.ts +88 -1
- package/dist/engines/temper/api.d.ts +4 -1
- package/dist/engines/temper/api.js +28 -5
- package/dist/engines/temper/data/guido.js +6 -2
- package/dist/engines/temper/data/modes.d.ts +6 -0
- package/dist/engines/temper/data/modes.js +42 -0
- package/dist/engines/temper/data/tones.d.ts +1 -1
- package/dist/engines/temper/data/tones.js +20 -11
- package/dist/engines/temper/interval.js +4 -3
- package/dist/engines/temper/modality.d.ts +11 -2
- package/dist/engines/temper/modality.js +74 -2
- package/dist/engines/temper/modes.d.ts +1 -1
- package/dist/engines/temper/pitch.d.ts +1 -1
- package/dist/engines/temper/pitch.js +12 -2
- package/dist/engines/temper/scale.d.ts +53 -0
- package/dist/engines/temper/scale.js +107 -8
- package/dist/index.d.ts +28 -6
- package/dist/index.js +39 -3
- package/docs/api/calendar.md +279 -0
- package/docs/api/census.md +288 -0
- package/docs/api/chant.md +657 -0
- package/docs/api/heavens.md +346 -0
- package/docs/api/index.md +263 -0
- package/docs/api/score.md +873 -0
- package/docs/api/tuning.md +619 -0
- package/package.json +11 -5
- package/dist/data/office-psalms-roman.d.ts +0 -15
- package/dist/data/office-psalms-roman.js +0 -28
- package/dist/data/office-roman.d.ts +0 -19
- package/dist/data/office-roman.js +0 -13792
- package/dist/engines/score/emitters/midi.d.ts +0 -65
- package/dist/engines/score/emitters/midi.js +0 -162
- package/dist/engines/score/emitters/musicxml.d.ts +0 -18
- package/dist/engines/score/emitters/musicxml.js +0 -166
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// engines/census — where a chant sits against the corpus that holds it
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// The corpus repo censuses every shipped chant into a block of 221 float32s:
|
|
5
|
+
// modal behaviour, degree histogram, interval bigrams, trigram and cadence
|
|
6
|
+
// vocabulary, chironomy, text setting, formula hits. This engine reads those
|
|
7
|
+
// blocks and answers one question — how typical is this chant, and what is it
|
|
8
|
+
// near?
|
|
9
|
+
//
|
|
10
|
+
// DISTANCE IS COSINE PER FIELD GROUP, never over the flat 221. Cosine
|
|
11
|
+
// on the whole vector is dominated by the 121-float melodic block and by sheer
|
|
12
|
+
// magnitude, so a long Tract would neighbor other long chants for being long.
|
|
13
|
+
// Per-group cosine asks about SHAPE within each dimension, and `all` is the
|
|
14
|
+
// equal-weight mean of those — every dimension one vote, no tunable weights.
|
|
15
|
+
import { CENSUS_BLOCKS_B64, CENSUS_BLOCK_FLOATS, CENSUS_GROUPS, CENSUS_ORDER, } from "../../data/census.js";
|
|
16
|
+
import { attestationCutoff, chantAdmissible } from "../chant/attest.js";
|
|
17
|
+
const GROUP_NAMES = Object.keys(CENSUS_GROUPS);
|
|
18
|
+
// ── The blocks, decoded once ────────────────────────────────────────────────
|
|
19
|
+
// Base64 in a .ts is how the blocks ride in the package (no fs, no fetch, works
|
|
20
|
+
// in a browser). Decoding is deferred so importing tonus does not pay for a
|
|
21
|
+
// census nobody asked for.
|
|
22
|
+
let _blocks = null;
|
|
23
|
+
function blocks() {
|
|
24
|
+
if (_blocks)
|
|
25
|
+
return _blocks;
|
|
26
|
+
const bin = typeof Buffer !== "undefined"
|
|
27
|
+
? Buffer.from(CENSUS_BLOCKS_B64, "base64")
|
|
28
|
+
: Uint8Array.from(atob(CENSUS_BLOCKS_B64), (c) => c.charCodeAt(0));
|
|
29
|
+
// The bytes are float32 little-endian. A DataView read is used rather than a
|
|
30
|
+
// Float32Array view over the buffer because the base64 decode gives no
|
|
31
|
+
// alignment guarantee, and because a big-endian host would silently
|
|
32
|
+
// byte-swap an aligned view.
|
|
33
|
+
const view = new DataView(bin.buffer, bin.byteOffset, bin.byteLength);
|
|
34
|
+
const out = new Float32Array(bin.byteLength / 4);
|
|
35
|
+
for (let i = 0; i < out.length; i++)
|
|
36
|
+
out[i] = view.getFloat32(i * 4, true);
|
|
37
|
+
_blocks = out;
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
let _index = null;
|
|
41
|
+
function index() {
|
|
42
|
+
if (!_index)
|
|
43
|
+
_index = new Map(CENSUS_ORDER.map((id, i) => [id, i]));
|
|
44
|
+
return _index;
|
|
45
|
+
}
|
|
46
|
+
/** One group's slice of a chant's block. */
|
|
47
|
+
function slice(blockIndex, group) {
|
|
48
|
+
const { offset, count } = CENSUS_GROUPS[group];
|
|
49
|
+
const start = blockIndex * CENSUS_BLOCK_FLOATS + offset;
|
|
50
|
+
return blocks().subarray(start, start + count);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Cosine similarity, 0–1. Two all-zero vectors are IDENTICAL (1), not
|
|
54
|
+
* undefined: two chants that both sit silent in a dimension agree perfectly
|
|
55
|
+
* about it. One zero vector against a non-zero one shares nothing, so 0.
|
|
56
|
+
*/
|
|
57
|
+
function cosine(a, b) {
|
|
58
|
+
let dot = 0;
|
|
59
|
+
let na = 0;
|
|
60
|
+
let nb = 0;
|
|
61
|
+
for (let i = 0; i < a.length; i++) {
|
|
62
|
+
const x = a[i];
|
|
63
|
+
const y = b[i];
|
|
64
|
+
dot += x * y;
|
|
65
|
+
na += x * x;
|
|
66
|
+
nb += y * y;
|
|
67
|
+
}
|
|
68
|
+
if (na === 0 && nb === 0)
|
|
69
|
+
return 1;
|
|
70
|
+
if (na === 0 || nb === 0)
|
|
71
|
+
return 0;
|
|
72
|
+
const c = dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
73
|
+
// Guard the float edge: a chant against itself must be exactly 1.
|
|
74
|
+
return c > 1 ? 1 : c < -1 ? -1 : c;
|
|
75
|
+
}
|
|
76
|
+
// ── The corpus mean, per group, computed once ───────────────────────────────
|
|
77
|
+
// Typicality is measured against the whole SHIPPED corpus, not against the
|
|
78
|
+
// filtered pool: `before` restricts who may be a neighbor, it does not
|
|
79
|
+
// move the mean.
|
|
80
|
+
let _means = null;
|
|
81
|
+
function means() {
|
|
82
|
+
if (_means)
|
|
83
|
+
return _means;
|
|
84
|
+
const out = {};
|
|
85
|
+
const n = CENSUS_ORDER.length;
|
|
86
|
+
for (const g of GROUP_NAMES) {
|
|
87
|
+
const { count } = CENSUS_GROUPS[g];
|
|
88
|
+
const acc = new Float32Array(count);
|
|
89
|
+
for (let i = 0; i < n; i++) {
|
|
90
|
+
const s = slice(i, g);
|
|
91
|
+
for (let k = 0; k < count; k++)
|
|
92
|
+
acc[k] += s[k];
|
|
93
|
+
}
|
|
94
|
+
for (let k = 0; k < count; k++)
|
|
95
|
+
acc[k] /= n;
|
|
96
|
+
out[g] = acc;
|
|
97
|
+
}
|
|
98
|
+
_means = out;
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
const VALID_KEYS = new Set(["id", "k", "by", "before"]);
|
|
102
|
+
/**
|
|
103
|
+
* The census of one chant: its profile against the corpus, where it is
|
|
104
|
+
* unusual, and what it is near.
|
|
105
|
+
*/
|
|
106
|
+
export function getCensus(query) {
|
|
107
|
+
if (!query || typeof query !== "object") {
|
|
108
|
+
throw new Error('census requires a query: census({ id: "gregobase:1210" }).');
|
|
109
|
+
}
|
|
110
|
+
const unknown = Object.keys(query).filter((k) => !VALID_KEYS.has(k));
|
|
111
|
+
if (unknown.length) {
|
|
112
|
+
throw new Error(`census: unknown query key(s) ${unknown.map((k) => `"${k}"`).join(", ")} ` +
|
|
113
|
+
`(expected ${[...VALID_KEYS].join(", ")}).`);
|
|
114
|
+
}
|
|
115
|
+
const { id } = query;
|
|
116
|
+
if (typeof id !== "string" || !id) {
|
|
117
|
+
throw new Error("census requires an id (a string), e.g. census({ id: \"gregobase:1210\" }).");
|
|
118
|
+
}
|
|
119
|
+
const self = index().get(id);
|
|
120
|
+
if (self == null) {
|
|
121
|
+
throw new Error(`census: no block for "${id}". The census covers the ${CENSUS_ORDER.length} chants ` +
|
|
122
|
+
"tonus ships; an id from a catalogue at large will not be among them.");
|
|
123
|
+
}
|
|
124
|
+
const by = query.by ?? "all";
|
|
125
|
+
if (by !== "all" && !GROUP_NAMES.includes(by)) {
|
|
126
|
+
throw new Error(`census: unknown field group "${by}" ` +
|
|
127
|
+
`(expected all, ${GROUP_NAMES.join(", ")}).`);
|
|
128
|
+
}
|
|
129
|
+
const k = query.k ?? 8;
|
|
130
|
+
if (!Number.isInteger(k) || k < 0) {
|
|
131
|
+
throw new Error(`census: k must be a non-negative integer, got ${String(query.k)}.`);
|
|
132
|
+
}
|
|
133
|
+
// The shared rule, not a local one — same door as cantus and the day verbs,
|
|
134
|
+
// so `before: 1098` means the same thing here as everywhere else.
|
|
135
|
+
const cutoff = attestationCutoff({ before: query.before }, "census");
|
|
136
|
+
// ── profile: each group against the corpus mean ──────────────────────────
|
|
137
|
+
const mean = means();
|
|
138
|
+
const profile = {};
|
|
139
|
+
for (const g of GROUP_NAMES) {
|
|
140
|
+
const s = slice(self, g);
|
|
141
|
+
profile[g] = {
|
|
142
|
+
values: Array.from(s),
|
|
143
|
+
typicality: cosine(s, mean[g]),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// ── balance: how far from the corpus mean, and where ─────────────────────
|
|
147
|
+
const typicalities = GROUP_NAMES.map((g) => profile[g].typicality);
|
|
148
|
+
const meanTypicality = typicalities.reduce((a, b) => a + b, 0) / typicalities.length;
|
|
149
|
+
const distance = 1 - meanTypicality;
|
|
150
|
+
// "Deviant" is relative to this chant's OWN mean, not an absolute threshold:
|
|
151
|
+
// the question is where a chant is unlike itself elsewhere, which is what
|
|
152
|
+
// makes it legible. Ties → alphabetical, so the answer is deterministic.
|
|
153
|
+
const deviantGroups = GROUP_NAMES.filter((g) => profile[g].typicality < meanTypicality).sort((a, b) => profile[a].typicality - profile[b].typicality ||
|
|
154
|
+
(a < b ? -1 : a > b ? 1 : 0));
|
|
155
|
+
// ── neighbors ────────────────────────────────────────────────────────────
|
|
156
|
+
const neighbors = [];
|
|
157
|
+
if (k > 0) {
|
|
158
|
+
const groupsToUse = by === "all" ? GROUP_NAMES : [by];
|
|
159
|
+
const selfSlices = groupsToUse.map((g) => slice(self, g));
|
|
160
|
+
const scored = [];
|
|
161
|
+
for (let i = 0; i < CENSUS_ORDER.length; i++) {
|
|
162
|
+
if (i === self)
|
|
163
|
+
continue;
|
|
164
|
+
const otherId = CENSUS_ORDER[i];
|
|
165
|
+
if (cutoff != null && !chantAdmissible(otherId, cutoff))
|
|
166
|
+
continue;
|
|
167
|
+
let sum = 0;
|
|
168
|
+
for (let gi = 0; gi < groupsToUse.length; gi++) {
|
|
169
|
+
sum += cosine(selfSlices[gi], slice(i, groupsToUse[gi]));
|
|
170
|
+
}
|
|
171
|
+
scored.push({ id: otherId, similarity: sum / groupsToUse.length });
|
|
172
|
+
}
|
|
173
|
+
// Ties → lower id, so the same question always has the same answer.
|
|
174
|
+
scored.sort((a, b) => b.similarity - a.similarity || (a.id < b.id ? -1 : 1));
|
|
175
|
+
neighbors.push(...scored.slice(0, k));
|
|
176
|
+
}
|
|
177
|
+
return { id, profile, balance: { distance, deviantGroups }, neighbors, by };
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=census.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** A field group census() can measure similarity on. */
|
|
2
|
+
export type CensusGroup = "modal" | "degreeHist" | "melodic" | "trigram" | "cadenceFinal" | "cadenceMedial" | "chironomy" | "textual";
|
|
3
|
+
/** `all` = the equal-weight mean of every group's cosine. */
|
|
4
|
+
export type CensusBy = CensusGroup | "all";
|
|
5
|
+
export interface CensusQuery {
|
|
6
|
+
/** The chant to census. Exactly one block per id (blocks are deduped by id). */
|
|
7
|
+
id: string;
|
|
8
|
+
/** How many neighbors to return. Default 8; 0 returns none. */
|
|
9
|
+
k?: number;
|
|
10
|
+
/** Which field group similarity is measured on. Default "all". */
|
|
11
|
+
by?: CensusBy;
|
|
12
|
+
/**
|
|
13
|
+
* Restrict neighbors to chants attested by this year. Unattested chants are
|
|
14
|
+
* EXCLUDED, not assumed old — the same rule as `cantus({ before })`.
|
|
15
|
+
*/
|
|
16
|
+
before?: number;
|
|
17
|
+
}
|
|
18
|
+
/** One field group's reading for a chant, beside the corpus norm. */
|
|
19
|
+
export interface CensusGroupProfile {
|
|
20
|
+
/** The chant's values for this group. */
|
|
21
|
+
values: readonly number[];
|
|
22
|
+
/**
|
|
23
|
+
* Cosine of this group against the corpus mean, 0–1. Low means the chant
|
|
24
|
+
* uses this dimension unlike the rest of the corpus.
|
|
25
|
+
*/
|
|
26
|
+
typicality: number;
|
|
27
|
+
}
|
|
28
|
+
export interface CensusNeighbor {
|
|
29
|
+
id: string;
|
|
30
|
+
/** Cosine similarity on the chosen group(s), 0–1. Higher is nearer. */
|
|
31
|
+
similarity: number;
|
|
32
|
+
}
|
|
33
|
+
export interface Census {
|
|
34
|
+
/** The chant censused. */
|
|
35
|
+
id: string;
|
|
36
|
+
/** Per-group readings, keyed by group name. */
|
|
37
|
+
profile: Readonly<Record<CensusGroup, CensusGroupProfile>>;
|
|
38
|
+
balance: {
|
|
39
|
+
/**
|
|
40
|
+
* Mean cosine distance from the corpus centre across all groups, 0–1.
|
|
41
|
+
* 0 is a chant at the corpus mean; 1 has nothing in common with it.
|
|
42
|
+
*/
|
|
43
|
+
distance: number;
|
|
44
|
+
/**
|
|
45
|
+
* Groups whose typicality falls furthest below the chant's own mean —
|
|
46
|
+
* where this chant is unusual, named. Most deviant first.
|
|
47
|
+
*/
|
|
48
|
+
deviantGroups: readonly CensusGroup[];
|
|
49
|
+
};
|
|
50
|
+
/** Nearest chants on the chosen group, nearest first. Ties → lower id. */
|
|
51
|
+
neighbors: readonly CensusNeighbor[];
|
|
52
|
+
/** Which group the neighbor sweep measured on. */
|
|
53
|
+
by: CensusBy;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// engines/census/types — the census verb's shapes
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// The field names are English by design. The census is measurement, not
|
|
5
|
+
// repertoire: it says how a chant sits against the corpus it belongs to, and
|
|
6
|
+
// the vocabulary for that is arithmetic, not liturgy.
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A year → the latest century wholly attested by it, on the same scale as
|
|
3
|
+
* `Attestation.century` (10 = the 900s).
|
|
4
|
+
*
|
|
5
|
+
* CANTUS dates a manuscript only to its century, so `before: 1098` cannot mean
|
|
6
|
+
* "witnessed before 1098" — a book dated "11th century" may have been written
|
|
7
|
+
* in 1099. Admitting the whole 11th century would let a caller asking for 1098
|
|
8
|
+
* receive chants first written down after it, which is the one thing this
|
|
9
|
+
* filter exists to prevent. So a year admits only centuries that CLOSED before
|
|
10
|
+
* it: 1098 → 10 (through the 900s), 1100 → 11.
|
|
11
|
+
*/
|
|
12
|
+
export declare function centuryOf(year: number): number;
|
|
13
|
+
/**
|
|
14
|
+
* The attestation cutoff a query asks for, as a century number — or null when
|
|
15
|
+
* the query carries no era view.
|
|
16
|
+
*/
|
|
17
|
+
export declare function attestationCutoff(query: {
|
|
18
|
+
before?: number;
|
|
19
|
+
}, method: string): number | null;
|
|
20
|
+
/**
|
|
21
|
+
* The cutoff a DAY verb serves under: the query's own view wins; otherwise the
|
|
22
|
+
* view `festum({ before })` stamped on the Feast rides along, so one argument
|
|
23
|
+
* at the calendar door carries through the whole day. Feasts resolved together
|
|
24
|
+
* came from one festum call and share one view — the first stamp speaks for
|
|
25
|
+
* the set.
|
|
26
|
+
*/
|
|
27
|
+
export declare function eraCutoff(query: {
|
|
28
|
+
before?: number;
|
|
29
|
+
}, feasts: readonly {
|
|
30
|
+
before?: number;
|
|
31
|
+
}[] | null | undefined, method: string): number | null;
|
|
32
|
+
/**
|
|
33
|
+
* ONE admissibility rule for every door — the book (`cantus`) and the day
|
|
34
|
+
* verbs (`proprium`, `ordinarium`, `officium`) alike. Evidence, not
|
|
35
|
+
* existence: an undated chant is excluded under any cutoff rather than
|
|
36
|
+
* assumed old, and `both` satisfies either cursus.
|
|
37
|
+
*/
|
|
38
|
+
export declare function chantAdmissible(id: string, cutoff: number | null, cursus?: "monastic" | "secular"): boolean;
|
|
39
|
+
//# sourceMappingURL=attest.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// engines/chant/attest — the era view's one admissibility rule
|
|
3
|
+
//
|
|
4
|
+
// A LEAF module, importing data only: every chant verb (cantus, proprium,
|
|
5
|
+
// ordinarium, officium) draws its attestation filtering from here, so no verb
|
|
6
|
+
// needs to import another verb's module to share the rule — the chant ↔
|
|
7
|
+
// ordinary import cycle this replaces died of exactly that (a TDZ
|
|
8
|
+
// ReferenceError whenever ordinary.js loaded first).
|
|
9
|
+
//
|
|
10
|
+
// ── A FILTER THAT LEFT AND CAME BACK ───────────────────────────────────────
|
|
11
|
+
// This filter was once retired because its data had a genre-shaped hole: 31%
|
|
12
|
+
// of the corpus undated, 93% of RESPONSORIES among them, so `before: 1098`
|
|
13
|
+
// deleted the Hartker repertory from Epiphany Matins — wrong about the
|
|
14
|
+
// liturgy on the exact question the library exists to answer. The rule was
|
|
15
|
+
// never the problem; the coverage was. The corpus side closed it (crosswalk
|
|
16
|
+
// gap-fill + matcher v2: composite respond+verse, the incipit-entry rule,
|
|
17
|
+
// containment, genre disambiguation — all gold-gated): 85% of shipped records
|
|
18
|
+
// now carry a dated witness and responsories are 92% DATED. What remains
|
|
19
|
+
// undated is deliberate (psalter, formulas) or editorial territory (hymns,
|
|
20
|
+
// modern propers) — not a genre bias. So the evidence law stands: an undated
|
|
21
|
+
// chant is EXCLUDED under any cutoff. Silence is not evidence.
|
|
22
|
+
//
|
|
23
|
+
// A `century` option is deliberately absent. It was always `before: N * 100`
|
|
24
|
+
// in different clothes — one cutoff internally, two spellings at the door —
|
|
25
|
+
// so the two spellings converged on the one that is a year.
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
import { ATTESTATION } from "../../data/attestation.js";
|
|
28
|
+
/**
|
|
29
|
+
* A year → the latest century wholly attested by it, on the same scale as
|
|
30
|
+
* `Attestation.century` (10 = the 900s).
|
|
31
|
+
*
|
|
32
|
+
* CANTUS dates a manuscript only to its century, so `before: 1098` cannot mean
|
|
33
|
+
* "witnessed before 1098" — a book dated "11th century" may have been written
|
|
34
|
+
* in 1099. Admitting the whole 11th century would let a caller asking for 1098
|
|
35
|
+
* receive chants first written down after it, which is the one thing this
|
|
36
|
+
* filter exists to prevent. So a year admits only centuries that CLOSED before
|
|
37
|
+
* it: 1098 → 10 (through the 900s), 1100 → 11.
|
|
38
|
+
*/
|
|
39
|
+
export function centuryOf(year) {
|
|
40
|
+
// floor, not ceil−1: the two agree everywhere except exact century
|
|
41
|
+
// multiples, where ceil−1 wrongly excluded the century that had just CLOSED
|
|
42
|
+
// (1100 → 10, so `before: 1100` refused the 1000s) — and the round inputs
|
|
43
|
+
// callers actually reach for are precisely the multiples that were wrong.
|
|
44
|
+
return Math.floor(year / 100);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The attestation cutoff a query asks for, as a century number — or null when
|
|
48
|
+
* the query carries no era view.
|
|
49
|
+
*/
|
|
50
|
+
export function attestationCutoff(query, method) {
|
|
51
|
+
if (query.before == null)
|
|
52
|
+
return null;
|
|
53
|
+
const cutoff = centuryOf(query.before);
|
|
54
|
+
if (!Number.isFinite(cutoff)) {
|
|
55
|
+
throw new Error(`${method}: before must be a year — the cutoff is the latest century number ` +
|
|
56
|
+
`(10 = the 900s) wholly closed by it; e.g. ${method}({ before: 1098 })`);
|
|
57
|
+
}
|
|
58
|
+
return cutoff;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The cutoff a DAY verb serves under: the query's own view wins; otherwise the
|
|
62
|
+
* view `festum({ before })` stamped on the Feast rides along, so one argument
|
|
63
|
+
* at the calendar door carries through the whole day. Feasts resolved together
|
|
64
|
+
* came from one festum call and share one view — the first stamp speaks for
|
|
65
|
+
* the set.
|
|
66
|
+
*/
|
|
67
|
+
export function eraCutoff(query, feasts, method) {
|
|
68
|
+
const own = attestationCutoff(query, method);
|
|
69
|
+
if (own != null)
|
|
70
|
+
return own;
|
|
71
|
+
const stamped = feasts?.find((f) => f?.before != null);
|
|
72
|
+
return stamped ? centuryOf(stamped.before) : null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* ONE admissibility rule for every door — the book (`cantus`) and the day
|
|
76
|
+
* verbs (`proprium`, `ordinarium`, `officium`) alike. Evidence, not
|
|
77
|
+
* existence: an undated chant is excluded under any cutoff rather than
|
|
78
|
+
* assumed old, and `both` satisfies either cursus.
|
|
79
|
+
*/
|
|
80
|
+
export function chantAdmissible(id, cutoff, cursus) {
|
|
81
|
+
if (cutoff == null && !cursus)
|
|
82
|
+
return true;
|
|
83
|
+
const a = ATTESTATION[id];
|
|
84
|
+
if (cutoff != null && (a == null || a.century > cutoff))
|
|
85
|
+
return false;
|
|
86
|
+
if (cursus && !(a?.cursus === cursus || a?.cursus === "both"))
|
|
87
|
+
return false;
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=attest.js.map
|
|
@@ -1,10 +1,28 @@
|
|
|
1
|
-
import type { Chant, CantusQuery } from "./types.js";
|
|
1
|
+
import type { Chant, CantusQuery, ChantSource, Corpus, CorpusLedger, CorpusQuery } from "./types.js";
|
|
2
|
+
import { CANTUS_QUERY_KEYS } from "./types.js";
|
|
3
|
+
export declare const SOURCES: Record<ChantSource, Chant["source"]>;
|
|
4
|
+
/**
|
|
5
|
+
* Metadata and content breakdown for one corpus book (`tonus.corpus`). Pass a
|
|
6
|
+
* source code; get the book's bibliographic identity plus its genre and mode
|
|
7
|
+
* distributions. Computed on first access from the loaded corpus, then cached.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* One book's ledger, or the whole shelf's.
|
|
11
|
+
*
|
|
12
|
+
* `corpus("am")` and `corpus({ book: "am" })` are the same question — the bare
|
|
13
|
+
* code came first and keeps working, the object form matches every other verb.
|
|
14
|
+
* `corpus()` with no argument returns the rollup.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getCorpus(): CorpusLedger;
|
|
17
|
+
export declare function getCorpus(code: ChantSource): Corpus;
|
|
18
|
+
export declare function getCorpus(query: CorpusQuery): Corpus;
|
|
2
19
|
export declare function resolveChant(id: string | null): Chant | null;
|
|
3
20
|
export declare function resolveChants(ids: string[]): Chant[];
|
|
21
|
+
export { CANTUS_QUERY_KEYS };
|
|
4
22
|
/**
|
|
5
|
-
* Cross-corpus chant retrieval (`tonus.cantus`) over
|
|
6
|
-
* A `gabc` field bypasses the corpus and returns a
|
|
7
|
-
* chant parsed from raw GABC (body or full file with headers).
|
|
23
|
+
* Cross-corpus chant retrieval (`tonus.cantus`) over every corpus book
|
|
24
|
+
* (the codes in SOURCES). A `gabc` field bypasses the corpus and returns a
|
|
25
|
+
* single user chant parsed from raw GABC (body or full file with headers).
|
|
8
26
|
*/
|
|
9
27
|
export declare function getChants(query?: CantusQuery): Chant[];
|
|
10
28
|
//# sourceMappingURL=chant.d.ts.map
|