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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,603 @@
2
2
 
3
3
  All notable changes to tonus. Newest first.
4
4
 
5
+ ## Unreleased
6
+
7
+ ## 0.5.0 — 2026-08-12
8
+
9
+ Rendering, mostly — a day of looking at real chant on a real page and fixing
10
+ what the page showed.
11
+
12
+ ### Added
13
+
14
+ - **A third analysis track: `"prosodia"`** — how the melody treats the word.
15
+ One tent per word (the hairpin's top edge), its apex over the accented
16
+ syllable, the landing at the peak in the liturgical red — filled for arsic
17
+ (struck), an open ring for thetic (deferred). Below it a fence on a rail:
18
+ a stem per spoken syllable, a flat dash for a syllable recited on the tenor,
19
+ and a block per melisma — as wide as its real extent, as tall as its note
20
+ count, connected melismas joining into one ridge. It rides first in the
21
+ stack, directly under the lyric line it reads, and the site's vestigia strip
22
+ and tracks key carry it. Shaped across eight lab rounds
23
+ (`working/review/diagram-word-track-01…08.html`).
24
+ - **The rubrica reservation, amended.** Ruled 2026-07-29 as the mode line's
25
+ alone, the liturgical red now belongs to the claims: the tonarium's mode
26
+ line and the prosodia's accent dots. The precedent is the score's own text
27
+ apparatus — the dropcap and annotations were always rubricated, and in the
28
+ books red is the word's colour.
29
+
30
+ ### Changed — breaking
31
+
32
+ - **The layout options are `width` and `scale`.** `padding`, `noteScale`,
33
+ `systemGap`, and `custos` are gone. Nothing ever set them — not the docs site,
34
+ not the 28 lab plates, not the 13 stress pieces — so they were surface without
35
+ use. They are now constants chosen to look right at every scale, and the
36
+ custos appears whenever a system wraps, which is what a chant book does.
37
+
38
+ `scale` replaces `staffHeight`: `"small"`, `"normal"` (default), `"large"`, or
39
+ a staff height in px for fitting a known column. A caller decides how big the
40
+ chant should be, not how tall its staff is in pixels.
41
+
42
+ ```js
43
+ inscriptio(score, { width: 900, scale: "large" })
44
+ ```
45
+
46
+ The page margin deliberately does NOT scale with it — a margin belongs to the
47
+ page, not the notation, and scaling it gave a large chant *less* usable width
48
+ than a small one (89% of a 900px canvas against 93%). The air between systems
49
+ does scale, since flat 24px held the system pitch at 135px whether the staff
50
+ was 30 or 56.
51
+
52
+ - **`inscriptio`'s look options are one `theme` object.** `fonts`, `noteColor`,
53
+ `staffLineColor`, and `rubricaColor` are replaced by
54
+ `theme: { fonts, colors }`. These travel
55
+ together — a caller setting a lyric face is usually setting a whole look — and
56
+ a house style is worth naming once and passing everywhere.
57
+
58
+ ```js
59
+ // before
60
+ inscriptio(score, { fonts: { lyric: "Junicode" }, rubricaColor: "#801", staffHeight: 48 })
61
+ // after
62
+ inscriptio(score, { scale: 48, theme: {
63
+ fonts: { lyric: "Junicode" }, colors: { rubrica: "#801" },
64
+ } })
65
+ ```
66
+
67
+ `fonts` keeps its four roles unchanged, `dropcap` among them: a book's
68
+ initial is very often not its lyric face, and the two stay separate.
69
+
70
+ ### Added
71
+
72
+ - **A note knows the diphthong it belongs to.** `Context.diphthong` and
73
+ `ChantTabulaRow.diphthong` report the pair a sung vowel is part of — `ae` ·
74
+ `oe` · `au`, and `ui` in the cui/hui stems — or null. `vowel` is unchanged: it
75
+ stays the NUCLEUS, the single vowel a singer sustains, because that is what
76
+ the analysis passes key on. The pair rides beside it for anyone rendering the
77
+ off-glide.
78
+
79
+ It has to come from here, because **only the syllabifier can tell a diphthong
80
+ from a hiatus: the accent decides.** `cae` is one syllable — nucleus `a`,
81
+ gliding toward `e`; `sa-é` is two, the same letters with the accent on the
82
+ second and no glide at all. Downstream, given only the lyric, the two are
83
+ indistinguishable. A scan for the letter pair also misreads `quae`, where `qu`
84
+ is a consonantal glide: the nucleus is `u`, and the `ae` is not the sung pair.
85
+ Over the Graduale that is 71 distinct syllables — 1,526 sung notes — a naive
86
+ reading gets wrong, and 192 syllables over the whole corpus. The detection
87
+ reuses the syllabifier's own `DIPHTHONGS` set, so the word means the same
88
+ thing here as it does when a word is split.
89
+
90
+ - **The ink is themable from CSS.** Colours now reach the SVG as custom
91
+ properties with the render's own value as the fallback —
92
+ `fill="var(--tonus-note, #111)"` — so a host stylesheet can retheme a drawn
93
+ chant without re-rendering it, while a file opened on its own still shows the
94
+ ink it was drawn with. Three properties: `--tonus-note`, `--tonus-staff-line`,
95
+ `--tonus-rubrica`. The emitter already carried semantic classes (`note`,
96
+ `lyric`, `dropcap`, `custos`, `episema`, `divisio`, `clef`, `mora`, `ictus`)
97
+ but an inline `fill` beats any stylesheet rule, so none of them could be
98
+ styled. `scale` deliberately stays outside the theme for the same reason it
99
+ cannot be a CSS property: line breaking consumes it long before a stylesheet
100
+ sees the output.
101
+
102
+ ### Fixed
103
+
104
+ - **An accidental was emitted as a sounding note.** GABC's `fx` means "F is flat
105
+ from here" — a mark drawn on the staff, not a pitch to sing. The parser set the
106
+ state correctly and then fell through and pushed a note anyway, so `A(fxfg)`
107
+ returned three notes for two and duplicated the pitch. 1337 markers across 426
108
+ Graduale chants: 0.97% of every note in the book was a phantom, at a wrong
109
+ pitch, inventing a unison before each one and inflating every count, interval
110
+ and analysis downstream.
111
+
112
+ The SIGN still draws, and now on the note that follows it — where the books
113
+ print it. That needed a new field: `accidentalSign` is what to draw, separate
114
+ from `accidental`, which is the note's own alteration. In `fe(jx)cit(ih)` the
115
+ flat is printed before the I while it governs J; the emitter had been reading
116
+ the alteration and so drew nothing once the phantom was gone.
117
+
118
+ - **A written flat could come back spelled as a sharp.** Pitch spelling was
119
+ derived from the pitch class alone, against a preferred-flat set that omitted
120
+ pc 1 and pc 6 — so a GABC `x` (a flat) landing on those degrees returned
121
+ `C#`/`F#` rather than `Db`/`Gb`, and reported `accidental: +1` for a source
122
+ that wrote a flat. The pitch was always right; only the spelling and its sign
123
+ were wrong. `toPitch` now takes an optional spelling preference and `notatio`
124
+ passes what the source wrote. The Graduale writes only the bmolle (1821 B-flats
125
+ and no sharps at all), so the corpus never reached the gap.
126
+
127
+ - **Moderna honoured no note colour at all.** It hardcoded `#111` in seventeen
128
+ places while quadrata threaded the option, so a caller theming the ink saw one
129
+ species change and the other not.
130
+
131
+ - **`staffHeight` means the same thing in both species.** Moderna's staff was
132
+ a hardcoded constant, so the option moved quadrata and did nothing here: a
133
+ request of 30, 40 or 60 left moderna at 7.4px every time. Every geometric
134
+ constant now derives from it through one metrics factory.
135
+ - **The canvas is the width you asked for.** It was `max(systemMaxX)` —
136
+ whatever the widest system happened to reach — so a requested 900 came back
137
+ 915, 986, 1074, 1203 by chant, and a host applying `max-width` shrank each
138
+ differently. That is why the same page showed one chant's notation a third
139
+ smaller than the next's.
140
+ - **Lyrics count toward a system's extent.** A syllable is centred on its
141
+ note, so half of a final wide syllable always hung past the canvas and was
142
+ cut off. The layout already tracked the lyric's right edge for its collision
143
+ check; the width calculation never asked.
144
+ - **The custos is a custos.** It was drawn as a shrunken punctum on the
145
+ authority of a comment saying no custos glyph was baked — Bravura's have
146
+ been baked all along. The real glyph is a hooked note whose stem points
147
+ toward the pitch it announces. It also floated 41px past the last note,
148
+ having been placed after the divisio's trailing air; and it is now
149
+ suppressed after a full stop, where drawing it put two marks in one place
150
+ and read as a heavy double barline.
151
+ - **Analysis tracks scale with the staff in moderna**, and a track that SPANS
152
+ notes now reaches their ink rather than their anchors — the emitters had
153
+ measured the ink all along and the mapping dropped it.
154
+ - **Capitals are measured as capitals.** The lyric width estimate was a flat
155
+ per-character average, and chant sets its opening word in capitals; "CAn"
156
+ was estimated at 22.9px against a real 27.9, so opening syllables collided
157
+ with what followed.
158
+
159
+ ### Changed
160
+
161
+ - **More air.** Syllable and word spacing widened in both species, the
162
+ staff-to-lyric gap from 21px to 28, and moderna's note advance retuned so a
163
+ chant takes about one system more than quadrata rather than twice as many.
164
+
165
+ ## 0.4.5 — 2026-08-04
166
+
167
+ The rubric true-up, the era view, and a long pass of making the library say
168
+ what it means. The 2026-07-27 review found the Kyriale selection chain leaking
169
+ past its own gates; the fixes landed with regression tests, the corpus pipeline
170
+ was corrected and regenerated behind them, and the calendar's and the corpus's
171
+ `before` arguments were reconciled into one composable view. The shelf widened,
172
+ the office settled on one cursus, the cadence catalogue was re-mined over the
173
+ sung corpus, and a new verb — `census` — measures every chant against the
174
+ corpus that holds it.
175
+
176
+ Then the tidying, most of it prompted by things that turned out to be measured
177
+ rather than assumed. The salicus took Cardine's correction and, in the process,
178
+ stopped being confused with the Solesmes ictus — two nearly disjoint sets under
179
+ one name. The cadence catalogue learned its own denominator and joined itself
180
+ once instead of per consumer. The appendix roughly doubled and was renamed to
181
+ follow the register rule. The corpus ledger stopped needing three numbers to
182
+ say how much chant there is. And the Apel formula matcher came out: what
183
+ shipped could not use the catalogue it shipped with, and saying so was better
184
+ than leaving a public field that answered `null`.
185
+
186
+ `docs/` is a published site now, which is where 0.5 is headed.
187
+
188
+ ### Removed
189
+
190
+ - **BREAKING — `Score.formulas` and the Apel centonization matcher are out.**
191
+ The catalogue of standard phrases, the per-phrase matcher, `FormulaMatch`,
192
+ `Formula`, `FormulaSlot`, and `formulaeFor` are gone from the library. What
193
+ shipped was one mode's alphabet (mode-5 Graduals) behind a matcher whose
194
+ per-phrase window structurally could not fire on 40 of its 53 formulae —
195
+ Apel tabulates a chant as a sequence across a whole respond, and matching at
196
+ that scale is a different algorithm that was never built. The honest state
197
+ was a public field that answered `null` for nearly every chant, and an
198
+ appendix of research the library could not yet use.
199
+ The census keeps its `formulas` field group: those four floats are baked
200
+ upstream in tonus-corpus and describe the corpus as measured, whatever tonus
201
+ does with them. The transcriptions, photographs, tooling and verification
202
+ reports are kept in the workshop, not deleted — the reading of Apel stands on
203
+ its own, and the pipeline is there if the matcher is ever built properly.
204
+
205
+ ### Changed
206
+
207
+ - **BREAKING — the shelf reports ONE number, and the Kyriale leaves it.**
208
+ `corpus()` reported `count` (book listings, a chant in two books counted
209
+ twice) beside `distinct` (chants), which made the headline figure depend on
210
+ how many books happened to print the same melody — a reader had to know that
211
+ to read either. Now `count` is **how many chants tonus holds**, each counted
212
+ once, and `listings` is the shelf's length; `listings - count` is the
213
+ overlap. `genera` and `modes` sum to `count`, describing the same population
214
+ the headline does.
215
+ The Kyriale is no longer an eleventh book. There is no Kyriale in GregoBase:
216
+ `ky` and `gr` are the same source, partitioned by the extractor so the
217
+ ordinary can be routed to per-ordinary codes, so listing it beside its parent
218
+ counted the Graduale twice and its `total: null` read as "unmeasured" when
219
+ there was no separate book to measure. `ky` is not a `ChantSource` and not a
220
+ row in the shelf — which is 10 books, 2,187 chants, 2,767 listings.
221
+ Nothing became unreachable. `ordinarium({ feast })` is untouched, and the
222
+ ordinary is now queryable directly: **`cantus({ ordinary: "ky" })`** returns
223
+ all 31 Kyries, composes with the other filters, and the Kyriale's
224
+ bibliographic record still rides every one of its chants. A plain search does
225
+ not sweep the ordinary in — you ask for a Kyrie rather than stumbling onto
226
+ one.
227
+ - **BEHAVIOUR — the salicus takes Cardine's correction, and stops being
228
+ confused with the Solesmes ictus.** Two changes, each ruled and each
229
+ measured separately over the sung corpus (2,887 chants, 28,498 phrases).
230
+ The second — redefining what counts as a salicus at all — was ratified on
231
+ its own terms, since it decides what the word denotes everywhere in tonus,
232
+ not just how one note is weighted.
233
+ **Detection.** A salicus is "at least three ascending notes in which the
234
+ next-to-last is an oriscus" [biblio: cardine-semiology, ch. 16]. tonus had
235
+ been calling any ascending run with the editorial ictus a salicus — 2,795
236
+ groups, of which **36 (1.3%) actually carried an oriscus** — while missing
237
+ 188 of the corpus's 226 real salici, which classified as `scandicus`. Two
238
+ nearly disjoint sets under one name. Now: `salicus` 2,795 → **226**, exactly
239
+ the chants that meet Cardine's definition; `scandicus` 498 → 2,501, and the
240
+ 4-and-5-note ictus ascents (566) join `compound`. An ictus-marked ascent
241
+ with no oriscus is a scandicus that Solesmes marked for rhythm, and the mark
242
+ is still readable on `context.ictus`.
243
+ **Weighting.** The printed editions lengthen the oriscus; the manuscripts
244
+ show the principal note is the one immediately following it. The 1.3×
245
+ prolongation moves from the next-to-last note to the **summit** — which is
246
+ the last note at any length, since the oriscus is next-to-last by
247
+ definition, so the 4- and 5-note forms need no separate rule. The salicus
248
+ stays arsic: tension toward the summit is the arsic gesture, so Cardine
249
+ strengthens that rule rather than contradicting it.
250
+ **Downstream:** 208 of 28,498 phrases (0.73%) change `rhythmicType`,
251
+ overwhelmingly toward VIII. This is the one point where tonus's rhythmic
252
+ layer departs from Mocquereau and Suñol, and `ir.ts` now says so at its
253
+ header.
254
+ **The limit, stated plainly:** tonus sees only what the transcription marks.
255
+ Bevenot's own example — the mode-6 _Requiem_ introit's fa-sol-la — carries
256
+ the ictus and no oriscus in GregoBase, so it still reads as a scandicus. He
257
+ is reading the manuscripts; the corpus is a printed edition that resolved
258
+ the oriscus away. Recovering those wants the sources, not a looser rule.
259
+ - **The tonarium's cadence label says how typical a close is, not which
260
+ family it belongs to.** The bracket read `"2,0,-2 @0"` — the family's name,
261
+ which a reader could not weigh. It now reads `"×2.1"`: the family's share
262
+ within this chant's mode over its share of the corpus at large. Measured
263
+ across four books the figure spans ×0.46 to ×10.56, median ×2.22; a lift
264
+ below 1.0 prints too, since an atypical close is information. A mode-less
265
+ chant, or a family with fewer than ten occurrences in the chant's mode,
266
+ falls back to the plain corpus share. The key is not lost — each cadence
267
+ now draws inside a group carrying `data-cadentia`, the join back to
268
+ `CADENTIAE` and the provenance a margin gloss can print.
269
+ - **BREAKING — five appendix tables renamed to match the register rule.**
270
+ `TEMPUS_NAME` → `TEMPORA`, `GRADE_NAME` → `GRADUS`, and the internal
271
+ `OFFICE_LABELS` / `ORDINARY_LABELS` / `MODE_LABELS` → `OFFICIA` /
272
+ `ORDINARIA` / `MODI`, the last three now public. The house rule is that
273
+ Latin names carry Latin content and English names carry codes or English;
274
+ the appendix had three spellings of "code → display string" and five tables
275
+ whose Latin values sat under English names. Names now follow the register of
276
+ their values, so `SEASON_LABEL` ("Advent") and `TEMPORA` ("Tempus
277
+ Adventus") are distinguishable by name rather than by memory. Only the two
278
+ `cal` tables were public before; the rest is new surface.
279
+ - **`officium` throws on an unrecognised `hora`.** It returned `[]`, which
280
+ read as "no chants at this hour" rather than "there is no such hour" — the
281
+ same silent-nothing the unknown-query-key guard beside it already refused.
282
+
283
+ ### Added
284
+
285
+ - **Every shelved book reports what it HOLDS.** The Nocturnale was the last
286
+ one answering `full: null`, which read as "not yet measured" — but the tally
287
+ had existed all along in its own extract. It holds **1,564** chants against
288
+ the 470 tonus ships, and it shares nothing: `unique` is all of them. That is
289
+ a measurement, not a gap. The crosswalk pairing a nocturnale chant with a
290
+ GregoBase twin is enrichment, a route to metadata, not a claim that two books
291
+ print the same chant, so counting those as shared would invent a
292
+ relationship. The shelf now holds 10,156 chants before the cut.
293
+ - **Three genera that are reported but not shipped now have names.**
294
+ `Improperia` (the Good Friday Reproaches), `Antiphona Mariana` (Marian
295
+ antiphons outside the office cycle) and `Supplicatio` (litanies) appear in a
296
+ book's pre-cut `full` tally, where they had been printing as bare codes —
297
+ `genus: "su"` — while every other row carried a Latin genus. Being outside
298
+ the cut is not a reason to be nameless in a table tonus publishes.
299
+ - **The cadence catalogue carries its own denominator, and joins itself.**
300
+ Every `CadentiaFamilia` gains `share` — its occurrences over ALL 28,481
301
+ phrase-ends, not over the 58.7% that cleared the table's floor, which would
302
+ have flattered every family in it. `CADENTIAE_POPULATION` ships beside it
303
+ with the same total per mode digit, so a family's **lift** in a mode is one
304
+ division: `(modes[m] / byMode[m]) / share`. The ratio itself is not baked —
305
+ export the vocabulary, not the arithmetic.
306
+ A `Cadence` now carries `finality`, the share of ITS family's corpus
307
+ occurrences that land at a final close, joined once in `notatio` instead of
308
+ by every caller rebuilding the index. It rides the cadence while `familia`
309
+ still does not, because the two differ in kind: the signature already IS the
310
+ family's name, but how often that family closes cannot be read off it — of
311
+ the 55 families landing on the final, 31 do not close, and their finality
312
+ spans 0.054 to 1.000. Detection stays pure; the corpus table meets detected
313
+ data in the score builder, where `MODES` already does.
314
+ Both cadence catalogues are now documented as what they are — _tradita_
315
+ (the treatises' figures, final cadences only) and _inventa_ (the corpus
316
+ tally, any target, and so the only account of medial closes) — with the
317
+ measured coverage of each and a worked lift example that was run before it
318
+ was printed.
319
+ - **The appendix widened, so callers stop transcribing the library's own
320
+ vocabulary.** `HORAE` (the eight canonical hours, Matins first — the order
321
+ is the content, and `officium`'s validation reads the same list, so the two
322
+ cannot drift), `OFFICIA`, `ORDINARIA`, `MODI`, `SOURCES` (the book codes
323
+ `cantus({ source })` takes, with their bibliographic records), and
324
+ `CENSUS_GROUPS` / `CENSUS_ORDER` — the census field groups and the block
325
+ index, so asking whether a chant is censused stops needing a `try/catch`.
326
+ A table is admitted when a caller would otherwise type it out, because a
327
+ transcribed copy drifts and fails as wrong answers rather than as an error.
328
+ The appendix is now grouped by engine.
329
+ - **The census distance rule is documented as a contract.** Similarity is
330
+ cosine per field group, never over the flat 225; `by: "all"` is the
331
+ equal-weight mean. Grouping is userland, so a caller pooling blocks is
332
+ computing a distance and must reproduce the rule or silently disagree with
333
+ `census()`. `census.md` now states it for callers with the three ways to get
334
+ a plausible wrong answer, and a worked example — pooling the 178 Communions
335
+ — that was run before it was printed, and reproduces `census()` exactly.
336
+ - **The analysis tracks ship with `inscriptio`.** `tracks: ["chironomia"]`
337
+ draws the conducting hand's wave — arsic crests, thetic
338
+ troughs, pick-up loops, Pierik letters. `tracks: ["tonarium"]`
339
+ draws the melodic lane: the four maneriae rails, the melody
340
+ compressed to its ambitus, the mode line in rubrica (solid inflection,
341
+ dashed transposition). A cadence is the melody's own ending re-inked
342
+ at full strength — the same curve at the same width — landing on a
343
+ terminal node (filled when the family's measured finality in CADENTIAE
344
+ closes, open when it suspends), labelled beneath by its signature on a
345
+ light end-ticked bracket spanning the figure. One governing ink system
346
+ runs through both tracks: one black graded by stratum opacity (rubrica
347
+ is the mode line's alone), and one nib law — velocity as stroke
348
+ width — for every pressure-bearing line. Either track rides either
349
+ species, and both may ride one score: they stack in a fixed order, the
350
+ chironomia above the tonarium, whichever order they are asked for. The
351
+ two-register principle — the rhythmic band under the square notation,
352
+ the melodic band under the transcription — is the house default, not a
353
+ rule the renderer enforces. Confidence is
354
+ opacity; claims under 0.45 draw nothing. The tracks consume the same
355
+ note anchors the geometry contract exports — the score's own ink is
356
+ untouched, and the geometry is unchanged with tracks on.
357
+ - **The duae species share one lyric setting and one opening.**
358
+ Quadrata's staff→lyric gap now equals moderna's (21px at the default
359
+ staff height, scaling with it), and both species default the lyric
360
+ weight to 518; an explicit `fonts.lyric.weight` still overrides.
361
+ Moderna now honours the front matter (`title`, `rubric`,
362
+ `annotation: "auto"`) as quadrata does; the official opening of a
363
+ tonus score is the incipit as `title` plus the auto genus/mode mark,
364
+ with no dropcap.
365
+ - **The era view composes end to end.** `festum({ date, before })` stamps
366
+ the view year on the returned `Feast` (`feast.before`), and every day
367
+ verb — `proprium`, `ordinarium`, `officium` — serves the
368
+ same view without being told the year twice; an own `before`
369
+ overrides. Previously the day verbs' types promised `before`/`century`/
370
+ `cursus` (they extend `CantusQuery`) while the implementations diverged
371
+ three ways: `proprium` threw "unknown query key," `officium` and
372
+ `ordinarium` silently ignored them.
373
+ - One admissibility rule for every door: `engines/chant/attest.ts`, a leaf
374
+ module shared by `cantus`, the day verbs, and the census. One spelling
375
+ at the door — `before`, a year.
376
+ - Under a view, `ordinarium` **re-picks** over the admissible pool (the
377
+ Kyriale offers ranked alternatives by design); `proprium` and `officium`
378
+ degrade to silence — the corpus cut's evidence law.
379
+ - **The shelf widens to eleven books.** Four more marked Solesmes office
380
+ books join — Antiphonale Monasticum Solesmense (`ams`), Psalterium
381
+ Monasticum (`psm`), Cantus selecti (`cse`), Chants of the Church
382
+ (`cot`) — admitted by the stated rule: Solesmes and rhythmically marked.
383
+ The shipped corpus is 2,887 rows over 2,187 distinct chants.
384
+ - **`tonus.census({ id })`** — one chant measured against the corpus that
385
+ holds it: a per-group profile with typicality, `balance` (distance and
386
+ deviant groups), and nearest neighbours by per-group cosine (`k`, `by`,
387
+ `before`). One block of 225 float32s per shipped chant; see
388
+ `docs/census.md`.
389
+ - **`corpus()` with no argument returns the whole shelf** — the rollup
390
+ plus every book's ledger — and every book carries `full`, the pre-cut
391
+ tally: the ledger of the cut, auditable rather than asserted.
392
+ - **`CADENTIAE` joins the appendix** (with `CadentiaFamilia`): the
393
+ corpus-grounded cadence catalogue, mined from every phrase-end in the
394
+ sung corpus — the same population the census counts — keyed by shape
395
+ and arrival.
396
+ - Test suites: `tests/ordinarium.test.mjs` (the selection chain end to
397
+ end) and `tests/era-view.test.mjs` (the view's whole contract).
398
+
399
+ ### Removed
400
+
401
+ - **The `rite` option.** tonus assembles one cursus, the Benedictine: the
402
+ Roman office table carried no chant at all on 63.4% of its days, and its
403
+ little-hours psalmody had no other consumer, so `rite: "romanum"`
404
+ returned a cursus nobody sang. A stale `rite` key now throws
405
+ (`officium` rejects unknown keys) rather than being silently ignored.
406
+ - **`matutinum` as a separate verb.** Matins is an hour of `officium`
407
+ (`hora: "matutinum"`), returned flat like every other hour; the
408
+ three-nocturn ordo shape remains unmodelled.
409
+ - **`century`.** It was `before: N * 100` in different clothes; the two
410
+ spellings converged on the one that is a year.
411
+
412
+ ### Fixed
413
+
414
+ - **"Requiescant in pace" was the dismissal on every green feria** (~20
415
+ days/year): `isRequiem` missed the bare "Requiescant" incipit AND the
416
+ last-resort appendix return was ungated. The same hole handed ferias an
417
+ ad libitum Gloria. The appendix now never reaches a day whose rubric
418
+ does not admit it; the Requiem stays reachable via
419
+ `ordinarium({ mass: 102 })`.
420
+ - **The Gloria follows the day's rank rubric, not its season**: ferias
421
+ print none; a I-class feast inside Advent (Immaculate Conception) keeps
422
+ hers. The "Benedicamus Domino as in Mass II" borrow the book directs is
423
+ now real — ferias and penitential Sundays sing Mass II's dismissal.
424
+ - **The paschal rubric is a time, not a rank**: removed from the solemn
425
+ set (an Eastertide Tuesday no longer leads with an appendix Kyrie;
426
+ Easter keeps Lux et origo every year), and BVM outranks paschal in
427
+ `rubricForDay`, so Cum jubilo is reachable in Eastertide.
428
+ - **Credo V was never sung**: the off-year rotation's parity was coupled
429
+ to the two-year bias (2 divides 6). The off-years now advance their own
430
+ cycle — all six credos are heard across twelve years.
431
+ - The leftover masses-1–9 preference on high feasts (precedence measuring
432
+ what the rubric should) — deleted; it defeated the appendix solemnity
433
+ boost on the class feasts it was built for and pinned high BVM feasts to
434
+ mass IX.
435
+ - `centuryOf` off-by-one at exact century multiples: `before: 1100` now
436
+ admits the closed 11th century, as its own doc always claimed.
437
+ - "Kyrie XVII C" refiled from a synthetic mass 100 to mass 17 (corpus-side
438
+ incipit parsing read the variant letter C as roman 100) — it now rotates
439
+ as Mass XVII's sibling printing.
440
+
441
+ ### Data
442
+
443
+ - Regenerated from the corrected pipeline: `office-roman` (canticle
444
+ antiphons split positionally — the last SURVIVOR of matching is no
445
+ longer promoted to the Benedictus/Magnificat slot), `office-monastic`
446
+ (464 days; rubric-year variant files excluded from the merge; stub
447
+ redirects followed to their actual targets), `office-ferial` (honestly
448
+ monastic rows only; the tier-then-book tiebreak; Matins 94 → 98 of 128),
449
+ `commune-office` (rubric-variant sections no longer blended; the Introit
450
+ genre-cap hole closed), `kyriale`.
451
+
452
+ ## 0.3.0
453
+
454
+ The vox-ectomy. The voice engine (`vox`, `chorus`, the formant tables,
455
+ personae, consortia, liquescentia coda tables, spectrum, accordatio) leaves
456
+ the public library for orreliquum, the private instrument it was always
457
+ being built toward. tonus returns to what it is: theory, calendar, chant,
458
+ notation, and the heavens — the algorithms, publishable and citable.
459
+
460
+ ### Added
461
+
462
+ - **`accent` on `ChantTabulaRow`** — a boolean marking whether a note's
463
+ syllable bears the Latin tonic word-accent. The value already existed inside
464
+ the parse (`detectVowelAccent` drives articulation stress); it is now surfaced
465
+ as data on the tabula, so downstream analysis can read accent placement
466
+ without re-deriving it. Threaded `ParsedNote → Context → ChantTabulaRow`.
467
+
468
+ ### Removed
469
+
470
+ - **`tonus.vox()` and `tonus.chorus()`** and every voice type
471
+ (`Vox`, `VoxInput`, `VoxParams`, `Vowel`, `Formant`, `Locus`, `Latinitas`,
472
+ `Persona`, `PersonaName`, `Coda`, `TuningLike`, `Chorus`, `ChorusOpts`,
473
+ `Dispersio`, `ConsortiumName`, `Voces`). The namespace holds fourteen
474
+ verbs again. `docs/voice.md` emigrates with the code.
475
+ - Voice test suites (`voice-vox`, `voice-chorus`, `voice-features`,
476
+ `voice-spectrum`) move with their subject.
477
+
478
+ ### Unchanged
479
+
480
+ - `harmonia`'s planetary voices (`VoicedPitch`, `VoicedBody`, the Greek
481
+ vowel tables) are its own and remain.
482
+ - Every other engine, table, and contract.
483
+
484
+ ## 0.2.0
485
+
486
+ Rendering becomes a standalone engine (`inscriptio`), the per-chant analysis
487
+ chart is completed, a voice engine is added, and the emitter surface settles on
488
+ one format (SVG).
489
+
490
+ ### Added
491
+
492
+ - **`inscriptio(score, opts?)`** — the standalone SVG renderer. Draws a `Score`
493
+ and returns `{ svg, geometry }`. Two notation species, each with its own
494
+ spacing pass: `"quadrata"` (square-note, SMuFL glyphs baked inline) and
495
+ `"moderna"` (modern round-note transcription — treble-8 clef, engraved slurs,
496
+ after the Lomer practice). A multi-system **layout engine** (`width`,
497
+ `systemGap`, `custos`) and **front matter** (`title`, `rubric` /
498
+ `annotation: "auto"`, `dropcap`, `rubricaColor`).
499
+ - **The geometry contract** — `geometry: NoteGeometry[]`, one entry per note in
500
+ tabula order (system, x, y, systemY). A public API: downstream analysis tracks
501
+ build on it instead of scraping the SVG.
502
+ - **The intonation channel** — `accidentals: "standard" | "heji" | "cents"`, a
503
+ **moderna** overlay. HEJI comma accidentals are baselined on the Pythagorean
504
+ chain (tonus's default tuning), so a Pythagorean chant renders clean and
505
+ syntonic-comma arrows bloom only under a just preset; meantone is not just, so
506
+ `heji` throws under it. `cents` labels signed deviations against `"pythagorean"`
507
+ (default) or `"et"`. Square notation (`quadrata`) carries only the accidentals
508
+ GABC itself expresses (flat / natural / sharp) — the HEJI and cents overlays are
509
+ modern analytical marks, so quadrata throws when asked for them.
510
+ - **Score metrics** on `prosody`: interval statistics (histogram, maxLeap,
511
+ leapRate, step/skip/leap `motus`), `tessitura`, the melodic `arcus`
512
+ (initial/peak/final + arch index), `melismaCadential`; and conveniences
513
+ `phrase.noteCount` / `phrase.syllableCount` / `syllable.melisma`.
514
+ - **`vox` and `chorus`** — a singing voice modelled as formant and spectrum
515
+ data, and seeded ensembles. `formantes` tunes to a temperament directly:
516
+ `tenor.formantes("a", temper, vis?)` — `vis` weights the pull, 0 (phonetic
517
+ truth) to 1 (fully tuned, the default).
518
+ - **The appendix.** The export law is settled: verbs live on the namespace,
519
+ return values are plain data, and the named exports are canonical constant
520
+ tables only — `SEASON_LABEL`, `TEMPUS_NAME`, `GRADE_ORDER`, `GRADE_NAME`,
521
+ `MODES`, `TONES` (types `PsalmTone`/`Differentia` ride with their table).
522
+ The grade helper functions (`gradeOrder`, `compareGrade`, `ritusToGrade`)
523
+ retire from the surface: `gradeOrder(g)` is `GRADE_ORDER.indexOf(g)`.
524
+ - **`docs/` ships in the package** — the documentation renders from the
525
+ installed tarball, pinned to the version it describes.
526
+ - **GABC lyric markup decodes.** The angle-bracket text tags — excluded since
527
+ the MIDI-only days — are parsed at intake: `<sp>` specials become real
528
+ characters (℣ ℟ † ǽ œ, the raised *), style tags (`<i>`, `<b>`, `<sc>`,
529
+ `<c>` rubric color, `<e>` elision) survive as styled `runs` on `Syllable`
530
+ and the tabula row, and both notation species draw them as `<tspan>`s.
531
+ Layout hints (`<clear>`, `<nlba>`, centering braces) and `\pageref`
532
+ cross-references vanish. Before this, quadrata printed `<sp>V/</sp>` as
533
+ literal lyric text and syllable widths were measured tags-and-all.
534
+ - **Per-role text faces** — `inscriptio`'s `fonts` option assigns a face to
535
+ the `dropcap`, `title`, `annotation`, and `lyric` roles (family, optional
536
+ weight, optional size scale). By default the SVG carries references and
537
+ the host page supplies the face; a slot may instead `embed` the caller's
538
+ own font bytes (base64) into the SVG's `<style>` for a self-contained
539
+ file. tonus bundles no font files either way.
540
+
541
+ ### Changed
542
+
543
+ - **License: PolyForm Noncommercial 1.0.0** (was MIT through 0.1.x, which
544
+ remain MIT). Free for any noncommercial purpose; commercial use by
545
+ arrangement with the author.
546
+ - **Corpus double-escape fixed.** Every `gabc` field stored its non-ASCII as a
547
+ literal `\uXXXX` escape, which had silently disabled accent detection across
548
+ the whole corpus — so note weights, prosody, rhythm, and imprint were computed
549
+ accent-blind. The extractor now decodes correctly; **computed accent weights
550
+ shift corpus-wide** as a result. A guard test asserts no gabc carries an escape.
551
+ - **`cantus({})` throws.** An empty or unknown-key chant query is a caller bug,
552
+ not an empty result; it throws with guidance (matching the `festum` contract).
553
+ - NABC pipes stripped from the corpus (`(notes|nabc)` → `(notes)`), with a
554
+ corpus-wide guard test.
555
+
556
+ ### Fixed (pre-release review)
557
+
558
+ - **The Pythagorean chain is the medieval dodecachord (E♭–G♯).** The chain had
559
+ stacked twelve fifths ascending from C, spelling F as E♯ — a 521.5¢ wolf
560
+ ut–fa — and b molle as A♯, so `temperamentum` disagreed with `harmonia`'s own
561
+ pure F. The naturals now sit F–B with b molle and E♭ on the flat side, the
562
+ ficta sharps on the sharp side; the heji/cents baseline derives from the same
563
+ chain constant, so a flatted chant under the default tuning renders clean
564
+ (it had thrown).
565
+ - **Quadrata figure grouping is phrase-aware.** A single-syllable phrase
566
+ followed by another phrase merged figures across the divisio, silently
567
+ dropping the second lyric and the divisio itself. An accidental inflecting
568
+ any note of a ligature now prints before the whole figure (Solesmes
569
+ practice) instead of vanishing on non-initial notes.
570
+ - **Canticles by name resolve their own texts.** `magnificat` and
571
+ `nunc dimittis` pointed at the wrong psalter rows (one returned the Symbolum
572
+ Athanasium); `te deum` is withdrawn — the Te Deum is not psalmody.
573
+ - **Formula step-skeletons count Apel's degrees.** Octave-aware relative to the
574
+ final's register: the mode-5 tenor reads +4, where the old ±half-octave fold
575
+ read −3. (Cadence figures keep their deliberate local-contour fold.)
576
+ - **The register doctrine at the seams.** `Matins.nomen` / `Matins.ritus`
577
+ (were `name` / `rank`), `PsalmTone.nomen`, and `rubricaColor` (was `rubrica`,
578
+ a Latin key that took a CSS color string).
579
+ - **The error contract enforced across the surface.** `notatio`, `festum`,
580
+ `caelum`, `matutinum`, `harmonia`, `proprium`, `temperamentum`, and `vox`
581
+ throw with guidance on junk input instead of raw TypeErrors or silent
582
+ fallbacks; a bare `chorus()` sings (the schola is the default); `psalmus`
583
+ no longer stamps the wall-clock year into `source.year` (determinism);
584
+ `iter` honours the `latinitas` shift; a user-GABC `office-part` header
585
+ normalizes to its OfficeCode.
586
+
587
+ ### Removed
588
+
589
+ - **The MusicXML and MIDI emitters** (`score.musicxml()`, `score.midi()`).
590
+ tonus emits one format now: SVG. Microtuning still lives on the tabula
591
+ (`bend`/`hz`/`offset`) for a Web-Audio player to read directly — microtonally
592
+ exact, which MIDI never was.
593
+
594
+ ## 0.1.8
595
+
596
+ - **Roman Matins.** `matutinum({ feast })` assembles the structured Roman night
597
+ office — the nocturns with their great responsories — from the community
598
+ *Nocturnale Romanum* (new `nr` chant source, 1,564 chants). Sanctorale feasts
599
+ draw Matins from their commune; coverage is the sanctorale and Advent today
600
+ (see COVERAGE.md). A separate accessor; the flat `officium` path is unchanged.
601
+
5
602
  ## 0.1.7
6
603
 
7
604
  - **`corpus(code)`** — metadata and analytics for a corpus book. Returns its
@@ -44,7 +641,7 @@ All notable changes to tonus. Newest first.
44
641
 
45
642
  - **Modulation detection** (`score.modulations`) — where the tonal centre leans
46
643
  away from the home mode, calibrated against Suñol.
47
- - **Richer modal affinity** — degree-, ictus-, and cadence-weighted, with ranked
644
+ - **Modal affinity** — degree-, ictus-, and cadence-weighted, with ranked
48
645
  initials after Rockstro.
49
646
  - **`modus()`** tunes its finalis, tenor, and ambitus through the temperamentum.
50
647