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
@@ -0,0 +1,279 @@
1
+ # Calendar
2
+
3
+ `tonus.festum` resolves a date to its place in the liturgical year. It
4
+ returns the feasts that fall on the day, ordered by precedence, each
5
+ carrying its rank in the rubrics' own vocabulary, its season, and the
6
+ kyriale masses appropriate to it. The calendar is the Tridentine
7
+ Roman rite (1570–1962), extracted from
8
+ [Divinum Officium](https://github.com/DivinumOfficium/divinum-officium):
9
+ 650 entries across the sanctorale (fixed feasts) and the temporale
10
+ (movable feasts), resolved against Easter computed by the Gregorian computus
11
+ from 1583 and the Julian computus before it.
12
+
13
+ - [Calendar](#calendar)
14
+ - [The day's feasts — `festum`](#the-days-feasts--festum)
15
+ - [The day as of a year — `before`](#the-day-as-of-a-year--before)
16
+ - [Rank — `ritus` and `grade`](#rank--ritus-and-grade)
17
+ - [Seasons — the temporale](#seasons--the-temporale)
18
+ - [The year's anchors — `pascha`](#the-years-anchors--pascha)
19
+ - [Theory \& Context](#theory--context)
20
+ - [The calendar's era](#the-calendars-era)
21
+
22
+ ## The day's feasts — `festum`
23
+
24
+ `festum(query?)` returns `Feast[]`. A date query returns every entry that
25
+ falls on the day: the primary feast first, concurrent feasts after it, in
26
+ order of dignity. A range query (`from`/`to`) walks each day and flattens
27
+ the results. A query with filters but no date scans the default liturgical
28
+ year, Advent to Advent. With no argument at all, the day resolves to
29
+ tonus's default epoch, **1 June 991**, the symbolic birthday of Guido
30
+ d'Arezzo (see [the conventions note](index.md#conventions)).
31
+ Empty matches return `[]`; an invalid range throws.
32
+
33
+ ```js
34
+ tonus.festum({ date: new Date("2026-12-25") });
35
+ ```
36
+
37
+ ```js
38
+ [
39
+ {
40
+ id: "12-25",
41
+ nomen: "In Nativitate Domini",
42
+ ritus: "Duplex I classis",
43
+ grade: "duplex-i",
44
+ season: "nat",
45
+ tempus: "Tempus Nativitatis",
46
+ seasonStart: "2026-12-25",
47
+ seasonEnd: "2027-01-10",
48
+ date: "2026-12-25",
49
+ weekday: 5,
50
+ masses: [2, 3],
51
+ marian: false,
52
+ apostolic: false,
53
+ },
54
+ {
55
+ id: "Adv4-5",
56
+ nomen: "Feria VI infra Hebdomadam IV Adventus",
57
+ ritus: "Feria major",
58
+ grade: "feria-major",
59
+ season: "nat",
60
+ tempus: "Tempus Nativitatis",
61
+ seasonStart: "2026-12-25",
62
+ seasonEnd: "2027-01-10",
63
+ date: "2026-12-25",
64
+ weekday: 5,
65
+ masses: [16],
66
+ marian: false,
67
+ apostolic: false,
68
+ },
69
+ ];
70
+ ```
71
+
72
+ The privileged feast leads; the concurrent Advent feria follows, carrying only
73
+ the ferial rubric's mass (`masses: [16]`).
74
+
75
+ Precedence decides what comes first when feasts collide. On November 30,
76
+ 2025, St. Andrew falls on the first Sunday of Advent; the privileged
77
+ Sunday wins the day and the Apostle follows it:
78
+
79
+ ```js
80
+ tonus.festum({ date: new Date("2025-11-30") });
81
+ // Dominica I Adventus [semiduplex-i]
82
+ // S. Andreæ Apostoli [duplex-ii]
83
+ ```
84
+
85
+ The other query forms:
86
+
87
+ ```js
88
+ tonus.festum({ from: advent1, to: epiphany }); // range, day by day
89
+ tonus.festum({ nomen: "Dominica I Adventus" }); // partial match, case-insensitive
90
+ tonus.festum({ season: "pasc" }); // liturgical-year scan, filtered
91
+ tonus.festum({ grade: "duplex-i", marian: true });
92
+ ```
93
+
94
+ ### The day as of a year — `before`
95
+
96
+ `before` resolves the day as it stood in a given year: the calendar holds
97
+ only the feasts instituted by then, and precedence runs over those. On most
98
+ days that returns the temporale or the feria in place of a modern feast.
99
+ Institution dates are in `cal/data/eras.ts`. A day with no feast yet
100
+ instituted returns `[]`.
101
+
102
+ ```js
103
+ tonus.festum({ date: new Date("2026-07-01"), before: 1100 });
104
+ // the feria — the Precious Blood was not instituted until 1849
105
+ ```
106
+
107
+ The feast returned **carries the view** (`feast.before`), and every chant
108
+ verb reads it back: `proprium`, `ordinarium`, and `officium`
109
+ serve only chants attested by the same year, without being told the year
110
+ twice. One `before` at the calendar door views the whole day. The chant side
111
+ — what "attested" means, and what a slot the view excludes does — is in
112
+ [chant.md](chant.md#the-repertoire-as-of-a-date--the-era-view).
113
+
114
+ ```ts
115
+ interface FeastQuery {
116
+ date?: Date;
117
+ from?: Date;
118
+ to?: Date;
119
+ nomen?: string; // partial match, case-insensitive
120
+ season?: Season;
121
+ grade?: Grade;
122
+ marian?: boolean;
123
+ apostolic?: boolean;
124
+ before?: number; // the era view: the day as of this year
125
+ }
126
+
127
+ interface Feast {
128
+ id: string; // "MM-DD" (sancti) or DO stem, e.g. "Adv1-0" (tempora)
129
+ nomen: string; // Latin feast name, "In Nativitate Domini"
130
+ ritus: string; // the Tridentine rank verbatim, incl. octave detail
131
+ grade: Grade; // canonical grade code; precedence via GRADE_ORDER
132
+ season: Season; // machine code (DO Tempora stem)
133
+ tempus: string; // Latin season name, "Tempus Adventus"
134
+ seasonStart: Date;
135
+ seasonEnd: Date;
136
+ date: Date;
137
+ weekday: number; // 0 = Sunday (UTC)
138
+ masses: number[]; // the masses the day's Kyriale rubric appoints
139
+ marian: boolean;
140
+ apostolic: boolean;
141
+ before?: number; // the era view this feast was resolved under, if any
142
+ }
143
+ ```
144
+
145
+ The `masses` list is derived from the Kyriale's own printed rubric — one
146
+ category per mass, by RANK: "In Paschal Time", "For feasts of the I class",
147
+ "For Sundays throughout the Year", "For ferias". A day resolves to exactly
148
+ one rubric (a BVM feast is "of the Blessed Virgin" even in Paschaltide),
149
+ and the masses carrying that rubric are the masses it may sing, in the
150
+ book's own numbering — where a rubric names several (II class 1–5), that
151
+ numbering is the book's invitation to choose, and `ordinarium` rotates
152
+ among them by year. The book's per-mass nicknames (_Orbis factor_ for
153
+ Sundays, and so on) record customary use, which disagrees with the rubric for 9
154
+ of the 18; `ordinarium` selects on the rubric.
155
+
156
+ ## Rank — `ritus` and `grade`
157
+
158
+ `ritus` is the rank string verbatim
159
+ from the Divinum Officium `[Rank]` line (`"Duplex majus"`, `"Semiduplex
160
+ II classis"`, `"Duplex I classis cum Octava privilegiata I ordinis"`).
161
+ It preserves the
162
+ octave detail, if present. `grade` is the canonical code the ritus reduces to for
163
+ sorting, filtering, and mass selection.
164
+
165
+ The order is classis-primary. A first-class day outranks any
166
+ non-first-class feast regardless of the duplex/semiduplex axis, so a plain
167
+ Duplex feast never displaces a Lent Sunday:
168
+
169
+ | # | `grade` | reduces from `ritus` | who it is |
170
+ | --- | -------------------- | ------------------------------------ | ----------------------------------------------------------- |
171
+ | 1 | `triduum` | Feria privilegiata Duplex I classis | Maundy Thu, Good Fri, Holy Sat |
172
+ | 2 | `duplex-i` | Duplex I classis (+ octave variants) | Christmas, Easter, Pentecost |
173
+ | 3 | `duplex-majus-i` | Duplex majus I classis | Low Sunday |
174
+ | 4 | `semiduplex-i` | Semiduplex I classis | Lent Sundays, Palm Sunday, Easter/Pentecost octave weekdays |
175
+ | 5 | `feria-privilegiata` | Feria privilegiata | Ash Wednesday, Holy Week Mon–Wed |
176
+ | 6 | `duplex-ii` | Duplex II classis (+ octave) | second-class feasts |
177
+ | 7 | `semiduplex-ii` | Semiduplex II classis | later Advent Sundays, octave days |
178
+ | 8 | `duplex-majus` | Duplex majus | |
179
+ | 9 | `duplex` | Duplex | |
180
+ | 10 | `semiduplex` | Semiduplex | Sundays throughout the year, semiduplex feasts |
181
+ | 11 | `simplex` | Simplex | |
182
+ | 12 | `feria-major` | Feria major | Advent/Lent ferias, Ember days |
183
+ | 13 | `vigilia` | Vigilia | |
184
+ | 14 | `feria` | Feria | weekdays with no feast |
185
+
186
+ Four Sundays are graded above their Divinum Officium rank, which marks Advent I
187
+ and the three Septuagesima-block Sundays plain `"Semiduplex"`. Advent I resolves
188
+ to `semiduplex-i`; Septuagesima, Sexagesima, and Quinquagesima to
189
+ `semiduplex-ii`, matching the classes DO gives the Lent and late-Advent Sundays.
190
+ `ritus` stays verbatim.
191
+
192
+ ## Seasons — the temporale
193
+
194
+ Each feast carries the pair `season` (code) and `tempus` (the Latin
195
+ season name). The codes are one-to-one with the Divinum Officium Tempora
196
+ stems, so a date's season and the stem of any Tempora feast on it agree by
197
+ construction.
198
+
199
+ | `season` | `tempus` | English | Span |
200
+ | -------- | ----------------------- | -------------------- | --------------------------------------------------- |
201
+ | `adv` | Tempus Adventus | Advent | Advent I Sunday → Christmas |
202
+ | `nat` | Tempus Nativitatis | Christmastide | Christmas → 1st Sunday after Epiphany |
203
+ | `epi` | Tempus post Epiphaniam | Time after Epiphany | there → Septuagesima |
204
+ | `quadp` | Tempus Septuagesimæ | Septuagesima | Septuagesima Sunday → Ash Wednesday |
205
+ | `quad` | Tempus Quadragesimæ | Lent | Ash Wednesday → Easter |
206
+ | `pasc` | Tempus Paschale | Paschaltide | Easter → Trinity Sunday (Pentecost octave included) |
207
+ | `pent` | Tempus post Pentecosten | Time after Pentecost | Trinity Sunday → next Advent |
208
+
209
+ A feast's `id` carries a nominal week number, but `season` is always
210
+ derived from the date; overflow entries, such as the Epiphany weeks
211
+ resumed before Septuagesima, take the season of the day they fall on.
212
+
213
+ Season drives real liturgy in the ordinary: in the penitential seasons
214
+ (`adv`, `quadp`, `quad`) the Gloria is omitted, and the Ite with it — the
215
+ Benedicamus dismissal appears only where the selected mass carries a
216
+ setting ([chant.md](chant.md#the-ordinary--ordinarium)).
217
+
218
+ ## The year's anchors — `pascha`
219
+
220
+ `pascha(year)` returns the movable anchors of one liturgical year as
221
+ UTC-midnight dates. Easter is computed by the Gregorian (Gauss/Butcher)
222
+ computus from 1583, and by the Julian computus with Julian→Gregorian
223
+ day-number conversion before that, so years reaching into the medieval
224
+ period stay correct. Everything else anchors to Easter, except Advent,
225
+ which anchors to the first Sunday on or after November 27, and the fixed
226
+ Christmas-cycle dates. A non-finite year throws.
227
+
228
+ ```js
229
+ tonus.pascha(2026);
230
+ ```
231
+
232
+ ```js
233
+ { year: 2026,
234
+ septuagesima: 2026-02-01, ashWednesday: 2026-02-18,
235
+ firstLentSunday: 2026-02-22, palmSunday: 2026-03-29,
236
+ goodFriday: 2026-04-03, easter: 2026-04-05,
237
+ ascension: 2026-05-14, pentecost: 2026-05-24,
238
+ trinitySunday: 2026-05-31, corpusChristi: 2026-06-04,
239
+ adventFirstSunday: 2026-11-29, gaudete: 2026-12-13,
240
+ christmas: 2026-12-25, epiphany: 2026-01-06,
241
+ baptism: 2026-01-11 }
242
+ ```
243
+
244
+ ```ts
245
+ interface Pascha {
246
+ year: number;
247
+ septuagesima: Date;
248
+ ashWednesday: Date;
249
+ firstLentSunday: Date;
250
+ palmSunday: Date;
251
+ goodFriday: Date;
252
+ easter: Date;
253
+ ascension: Date;
254
+ pentecost: Date;
255
+ trinitySunday: Date;
256
+ corpusChristi: Date;
257
+ adventFirstSunday: Date;
258
+ gaudete: Date;
259
+ christmas: Date;
260
+ epiphany: Date;
261
+ baptism: Date;
262
+ }
263
+ ```
264
+
265
+ ## Theory & Context
266
+
267
+ ### The calendar's era
268
+
269
+ The calendar's structure is medieval: the temporale from Advent through the
270
+ season after Pentecost (Septuagesima included), the eight-hour office cursus,
271
+ and the duplex/semiduplex/simplex dignity system. The data is the Tridentine
272
+ codification (1570–1962) via Divinum Officium, continuous with late-medieval
273
+ usage and carrying feasts as recent as the 1950s. tonus describes this calendar
274
+ as Tridentine Roman, continuous with medieval practice.
275
+
276
+ ## Sources
277
+
278
+ Sources for this page are in the central [bibliography](https://github.com/jeffreypierce/tonus/blob/main/BIBLIOGRAPHY.md):
279
+ `divinum-officium`, `computus`, `liber-usualis`.
@@ -0,0 +1,288 @@
1
+ # Census
2
+
3
+ `census` measures one chant against the corpus that holds it: how typical it
4
+ is, where it is unusual, and what it is near.
5
+
6
+ - [Census](#census)
7
+ - [The method](#the-method)
8
+ - [What a block holds](#what-a-block-holds)
9
+ - [How the measurement works](#how-the-measurement-works)
10
+ - [Distance is cosine per field group](#distance-is-cosine-per-field-group)
11
+ - [Profile and typicality](#profile-and-typicality)
12
+ - [Balance — distance and deviance](#balance--distance-and-deviance)
13
+ - [Neighbors, and `by`](#neighbors-and-by)
14
+ - [The era view](#the-era-view)
15
+ - [What the census is not](#what-the-census-is-not)
16
+
17
+ ## The method
18
+
19
+ ```js
20
+ tonus.census({ id: "gregobase:1210" });
21
+ ```
22
+
23
+ Everything comes back in one call — profile, balance, neighbors:
24
+
25
+ ```js
26
+ {
27
+ id: "gregobase:1210",
28
+ by: "all",
29
+ profile: {
30
+ modal: { values: [...12], typicality: 0.99 },
31
+ degreeHist: { values: [...15], typicality: … },
32
+ melodic: { values: [...121], typicality: 0.70 },
33
+ trigram: { values: [...16], typicality: … },
34
+ cadenceFinal: { values: [...16], typicality: … },
35
+ cadenceMedial: { values: [...16], typicality: … },
36
+ chironomy: { values: [...6], typicality: … },
37
+ textual: { values: [...7], typicality: … },
38
+ },
39
+ balance: {
40
+ distance: 0.091,
41
+ deviantGroups: ["degreeHist", "melodic"],
42
+ },
43
+ neighbors: [
44
+ { id: "gregobase:34", similarity: 0.999 },
45
+ …
46
+ ],
47
+ }
48
+ ```
49
+
50
+ ```ts
51
+ interface CensusQuery {
52
+ id: string; // the chant to census
53
+ k?: number; // how many neighbors, default 8; 0 returns none
54
+ by?: CensusBy; // which field group similarity is measured on, default "all"
55
+ before?: number; // restrict neighbors to chants attested by this year
56
+ }
57
+ ```
58
+
59
+ The census covers the **2,187 chants tonus ships** — the same population
60
+ `cantus({ id })` addresses, one block per chant. An id with no block throws
61
+ rather than returning an empty answer, because a silent nothing reads as "this
62
+ chant is unlike everything," which is a different claim.
63
+
64
+ ## What a block holds
65
+
66
+ The corpus pipeline censuses every shipped chant into 221 float32s, grouped by
67
+ what they describe:
68
+
69
+ | group | floats | what it measures |
70
+ | --------------- | -----: | --------------------------------------------------------------------------------- |
71
+ | `modal` | 12 | affinity to each of the eight modes, the final's and tenor's pitch-class, ambitus |
72
+ | `degreeHist` | 15 | how long the melody dwells on each scale degree, final-relative |
73
+ | `melodic` | 121 | the interval bigram table — which step follows which |
74
+ | `trigram` | 16 | three-note motifs, against the corpus's commonest |
75
+ | `cadenceFinal` | 16 | how the chant closes, keyed by cadence signature |
76
+ | `cadenceMedial` | 16 | how its interior phrases land |
77
+ | `chironomy` | 6 | the melodic arc in quarters, phrase length, melisma density |
78
+ | `textual` | 7 | vowel distribution by sung duration, accent rate, melisma mean |
79
+
80
+ Four more fields ride in the block and are **not** similarity dimensions:
81
+ `flags` (a bitfield), `attest` (dating — that is what `before` reads),
82
+ `extras`, and `reserve`. `by` will not accept them.
83
+
84
+ ## How the measurement works
85
+
86
+ Every number in a block reads off a single `notatio()` parse — the same parse
87
+ `score` gives you — so the census can never disagree with the library about
88
+ what a chant is.
89
+
90
+ Each float is a named measurement, not a learned one: time spent on the
91
+ subfinal, how often a rising second follows a falling third. When the census
92
+ calls two chants near, the profile says in what respect.
93
+
94
+ Most groups are normalized to sum to one, so a group holds a distribution —
95
+ where the melody's time goes, not how much of it there is; length is not a
96
+ similarity. The trigram and cadence groups count against dictionaries mined
97
+ from the corpus itself — its commonest motifs, its commonest closing gestures,
98
+ one bucket for the rest — so the corpus supplies the vocabulary and the chant
99
+ supplies the usage.
100
+
101
+ The reference is the mean block over all 2,187 chants, group by group. Because
102
+ blocks are sums of durations and counts, they add: a season's blocks, summed and
103
+ divided by their count, are the season's mean profile in the same 221 slots.
104
+
105
+ ## Distance is cosine per field group
106
+
107
+ **This is a contract, not an implementation note.** The census answers about
108
+ one chant at a time; grouping — "all Communions," "this season," "this
109
+ manuscript" — is yours to do. The moment you pool blocks yourself you are
110
+ computing a distance, and if you compute it differently from the rule below
111
+ your numbers will not agree with `census()`'s. Nothing will error.
112
+
113
+ The rule, in three lines:
114
+
115
+ 1. Cosine **per field group**, never over the flat 221.
116
+ 2. `by: "all"` is the **equal-weight mean** of the per-group cosines — every
117
+ dimension one vote, no tunable weights.
118
+ 3. Ties break to the lower id, so the same question always has the same answer.
119
+
120
+ Cosine on the whole vector is dominated by the 121-float `melodic` block and by
121
+ sheer magnitude, so a long Tract would neighbor other long chants for being
122
+ long. Per-group cosine asks about **shape within each dimension**.
123
+
124
+ [`CENSUS_GROUPS`](index.md#the-appendix) gives you the group names and their
125
+ field counts, and [`CENSUS_ORDER`](index.md#the-appendix) every censused id —
126
+ so you can pool a set without guessing at either.
127
+
128
+ ### Reading the numbers
129
+
130
+ **Similarity is not comparable across `by` values.** A 0.94 on `cadenceFinal`
131
+ and a 0.94 on `melodic` are not the same amount of alike: the groups have
132
+ different widths and different natural spreads. Rank within one `by`; never
133
+ threshold across two.
134
+
135
+ **A centroid must be pooled per group, then compared per group.** Averaging the
136
+ flat 221 and taking one cosine is the exact mistake rule 1 exists to prevent.
137
+ Pooling the 178 Communions both ways gives different winners, and the flat
138
+ version collapses the top of the field into a tie around 0.99 where the
139
+ per-group version spreads from about 0.85 down to 0.65. That compression comes
140
+ from one wide block outvoting the other eight.
141
+
142
+ **`before` filters before ranking.** It restricts the candidate pool, then
143
+ ranks — so `k` stays satisfiable, and a filtered list is *not* a subset of the
144
+ unfiltered one. Chants that were ranked out by later material rise into it.
145
+ Typicality is unaffected: it is always measured against the whole shipped
146
+ corpus (see [Profile and typicality](#profile-and-typicality)).
147
+
148
+ ### Worked example — pooling a genus
149
+
150
+ Reproducing the rule in full. This gives the same numbers `census()` gives,
151
+ which is the point of printing it:
152
+
153
+ ```js
154
+ import tonus, { CENSUS_GROUPS, CENSUS_ORDER } from "tonus";
155
+
156
+ const GROUPS = Object.keys(CENSUS_GROUPS);
157
+
158
+ const cosine = (a, b) => {
159
+ let dot = 0, na = 0, nb = 0;
160
+ for (let i = 0; i < a.length; i++) {
161
+ dot += a[i] * b[i]; na += a[i] ** 2; nb += b[i] ** 2;
162
+ }
163
+ return na && nb ? dot / Math.sqrt(na * nb) : 0;
164
+ };
165
+
166
+ // Pool a set of chants into a centroid — per group, never the flat 221.
167
+ function centroid(ids) {
168
+ const sums = Object.fromEntries(
169
+ GROUPS.map((g) => [g, new Array(CENSUS_GROUPS[g].count).fill(0)]),
170
+ );
171
+ for (const id of ids) {
172
+ const { profile } = tonus.census({ id, k: 0 }); // k: 0 — profile only
173
+ for (const g of GROUPS) profile[g].values.forEach((v, i) => { sums[g][i] += v; });
174
+ }
175
+ for (const g of GROUPS) sums[g] = sums[g].map((v) => v / ids.length);
176
+ return sums;
177
+ }
178
+
179
+ // Compare against it the same way: cosine per group, then the mean for `all`.
180
+ function similarity(id, c) {
181
+ const { profile } = tonus.census({ id, k: 0 });
182
+ const per = Object.fromEntries(GROUPS.map((g) => [g, cosine(profile[g].values, c[g])]));
183
+ per.all = GROUPS.reduce((s, g) => s + per[g], 0) / GROUPS.length;
184
+ return per;
185
+ }
186
+
187
+ // Every censused Communion, pooled — then: which Communion is most a Communion?
188
+ const ids = CENSUS_ORDER.filter((id) => tonus.cantus({ id })[0]?.office === "co");
189
+ const c = centroid(ids); // 178 chants
190
+ const ranked = ids
191
+ .map((id) => ({ id, s: similarity(id, c) }))
192
+ .sort((a, b) => b.s.all - a.s.all);
193
+
194
+ // 0.953 Quinque prudentes
195
+ // 0.951 Domus mea
196
+ // 0.944 Joseph fili David
197
+ // …
198
+ // 0.746 Exiit sermo
199
+ // 0.735 Tollite hostias
200
+ ```
201
+
202
+ The per-group breakdown is where the answer becomes legible. _Quinque
203
+ prudentes_ leads on `textual`, `cadenceMedial` and `trigram`, at about 0.99 on
204
+ each — it sets its text and turns its phrases the way Communions do — while its
205
+ `cadenceFinal` is only about 0.82, so the one thing it does unlike a typical
206
+ Communion is end. A chant is typical of its genus in some dimensions and not
207
+ others.
208
+
209
+ ## Profile and typicality
210
+
211
+ Each group's `typicality` is its cosine against the corpus mean for that group:
212
+ 1.0 is "uses this dimension exactly as the corpus does on average," lower means
213
+ "unlike the rest."
214
+
215
+ The two numbers above are a fair illustration. _Ab occultis meis_ is a mode-2
216
+ Gradual whose `modal` typicality is about 0.99 — modally it is a typical
217
+ mode-2 chant — while its `melodic` typicality is about 0.70, because its
218
+ interval
219
+ vocabulary is its own. One chant can be conventional in one dimension and
220
+ distinctive in another, which is the reason the groups are kept apart.
221
+
222
+ Typicality is always measured against the **whole shipped corpus**, never the
223
+ filtered pool: `before` restricts who may be a neighbor, it does not move
224
+ the mean.
225
+
226
+ ## Balance — distance and deviance
227
+
228
+ ```js
229
+ balance: { distance: 0.091, deviantGroups: ["degreeHist", "melodic"] }
230
+ ```
231
+
232
+ `distance` is 1 minus the mean typicality across all groups: 0 is a chant at
233
+ the corpus mean, 1 has nothing in common with it.
234
+
235
+ `deviantGroups` names where a chant is unusual **relative to its own mean**,
236
+ most deviant first — not against an absolute threshold. The question it answers
237
+ is "given how typical this chant is overall, where does it depart from
238
+ itself?", which is what makes the answer legible for a chant that is unusual
239
+ everywhere or nowhere.
240
+
241
+ ## Neighbors, and `by`
242
+
243
+ ```js
244
+ tonus.census({ id: "gregobase:1210", k: 3 });
245
+ // Ab occultis meis (Graduale, mode 2) →
246
+ // Justus ut palma Graduale, mode 2
247
+ // Requiem Graduale, mode 2
248
+ // Domine refugium Graduale, mode 2
249
+ ```
250
+
251
+ Nothing tells the census what genre or mode a chant is. It recovers them from
252
+ melodic shape alone.
253
+
254
+ `by` changes what _near_ means:
255
+
256
+ ```js
257
+ tonus.census({ id: "gregobase:1210", k: 3, by: "cadenceFinal" });
258
+ // chants that CLOSE the same way — crossing genre and mode freely
259
+ ```
260
+
261
+ Asked on `all`, a mode-2 Gradual finds mode-2 Graduals. Asked on
262
+ `cadenceFinal`, it finds an Alleluia, an Introit and a Communion in modes 1
263
+ and 4 that happen to end with the same gesture. Both answers are correct; they
264
+ are answers to different questions.
265
+
266
+ `k` bounds the result (default 8, `0` returns none, larger than the corpus
267
+ returns all 2,186 others).
268
+
269
+ ## The era view
270
+
271
+ ```js
272
+ tonus.census({ id: "gregobase:1210", before: 1100 });
273
+ ```
274
+
275
+ Restricts neighbors to chants a manuscript of the 11th century or earlier
276
+ already holds — 1,790 of the 2,186 candidates. This is the same rule as
277
+ [`cantus({ before })`](chant.md#the-repertoire-as-of-a-date--the-era-view),
278
+ through the same admissibility door: **evidence, not existence**, so a chant
279
+ with no dated witness is excluded rather than assumed old.
280
+
281
+ The seed chant itself is never filtered — you asked about it by name.
282
+
283
+ ## What the census is not
284
+
285
+ It is not a similarity search over Gregorian chant at large. The blocks
286
+ describe the chants tonus ships, which is the assignment-driven corpus: what
287
+ some day of the calendar calls for. A melody's neighbors are its neighbors
288
+ _within that repertoire_.