The phrases, as phrasesFromTimeline returns them.
The cadences, heaviest first; ties keep the earlier one.
import { chordTimelineFromNotes, phrasesFromTimeline, structuralCadences } from '@libraz/libcantus';
const triad = (pitches: number[], startBeat: number) =>
pitches.map((pitch) => ({ pitch, startBeat, durationBeat: 4 }));
const notes = [
...triad([60, 64, 67], 0),
...triad([65, 69, 72], 4),
...triad([67, 71, 74], 8),
...triad([60, 64, 67], 12),
];
const { timeline } = chordTimelineFromNotes(notes);
const ranked = structuralCadences(phrasesFromTimeline(timeline, notes));
ranked[0]?.cadence.type; // 'authentic' — the cadence carrying the most weight
ranked[0]?.atBeat; // 12
Rank the cadences that close phrases by how much structure they close.
This is the question the phrase layer exists to answer. Two chords alone cannot say whether a V-to-I is the end of the piece or a passing confirmation halfway through a hyperbar; a phrase reading can, because it knows what the cadence closes and where that sits. Phrases no cadence closes are left out.