///|
pub(all) struct ExerciseSpec {
id : String
rule : RuleKind
species : Species
title : String
objective : String
prompt : String
target : String
level : String
notes : Int
} derive(Debug, Eq, ToJson)
///|
/// The curriculum is intentionally small and curated: each item names a distinct musical task.
pub fn exercise_catalog() -> Array[ExerciseSpec] {
[
{
id: "first-parallel-fifths",
rule: ParallelPerfect,
species: First,
title: "Separate consecutive fifths",
objective: "Recognize two adjacent perfect fifths in first species.",
prompt: "Keep the first sonority and rewrite only the second counterpoint pitch.",
target: "Preserve the contour while replacing the second perfect interval with a consonant third.",
level: "foundation",
notes: 5,
},
{
id: "second-parallel-octaves",
rule: ParallelPerfect,
species: Second,
title: "Audit repeated octaves at strong beats",
objective: "Distinguish a permitted consonance from a parallel octave.",
prompt: "Compare the aligned strong beats and identify the smallest edit that breaks the parallel motion.",
target: "Leave weak-beat passing material intact and remove the repeated strong-beat octave.",
level: "intermediate",
notes: 8,
},
{
id: "fifth-contrary-motion",
rule: ParallelPerfect,
species: Fifth,
title: "Design contrary motion into a perfect interval",
objective: "Use motion labels to repair a perfect-interval transition.",
prompt: "Generate two candidate pitches and select the one with contrary motion and a stable range.",
target: "Produce one ranked candidate that avoids consecutive perfect intervals.",
level: "advanced",
notes: 10,
},
{
id: "hidden-cadence-approach",
rule: HiddenPerfect,
species: First,
title: "Soften a hidden octave",
objective: "Locate similar motion into a final perfect sonority.",
prompt: "Keep the final note fixed and change the penultimate upper note by step.",
target: "Approach the final octave without a large upper-voice leap.",
level: "intermediate",
notes: 6,
},
{
id: "hidden-fifth-step",
rule: HiddenPerfect,
species: Second,
title: "Approach a fifth by step",
objective: "Compare a leap approach with a stepwise approach to a fifth.",
prompt: "Render both reports and identify which upper-voice transition removes the warning.",
target: "Retain the rhythmic pattern while making the approach stepwise.",
level: "intermediate",
notes: 8,
},
{
id: "hidden-profile-review",
rule: HiddenPerfect,
species: Fifth,
title: "Review hidden intervals by profile",
objective: "Explain why a profile may allow or reject a hidden perfect interval.",
prompt: "Run the same phrase through classroom and strict profiles.",
target: "Report the profile-dependent difference without changing the source phrase.",
level: "advanced",
notes: 12,
},
{
id: "crossing-register",
rule: VoiceCrossing,
species: First,
title: "Restore registral order",
objective: "Find the first point where the counterpoint crosses the cantus.",
prompt: "Use the finding index to edit the crossing event rather than the entire phrase.",
target: "Keep every aligned pair in the intended lower-to-upper order.",
level: "foundation",
notes: 5,
},
{
id: "crossing-neighbor",
rule: VoiceCrossing,
species: Second,
title: "Repair a weak-beat crossing",
objective: "Repair a crossing caused by a passing note.",
prompt: "Move the passing note by one scale step and compare the contour summary.",
target: "Remove the crossing while preserving the weak-beat direction.",
level: "intermediate",
notes: 9,
},
{
id: "crossing-range-candidate",
rule: VoiceCrossing,
species: Fifth,
title: "Rank register-safe candidates",
objective: "Use candidate generation to keep a dense phrase inside its voice range.",
prompt: "Filter candidates by range before scoring their harmonic motion.",
target: "Return a best candidate that is registrally ordered and consonant.",
level: "advanced",
notes: 14,
},
{
id: "weak-beat-dissonance",
rule: DissonancePlacement,
species: Second,
title: "Place a passing dissonance",
objective: "Tell a prepared weak-beat dissonance from an unsupported clash.",
prompt: "Inspect the beat strength and neighboring intervals around the dissonant event.",
target: "Keep the passing dissonance on a weak beat with stepwise preparation and resolution.",
level: "intermediate",
notes: 8,
},
{
id: "suspension-resolution",
rule: DissonancePlacement,
species: Fourth,
title: "Resolve a suspension",
objective: "Trace a tied suspension from preparation through resolution.",
prompt: "Mark the tied event and edit only its resolution direction.",
target: "Produce a suspension that resolves by step into a consonance.",
level: "advanced",
notes: 10,
},
{
id: "dissonance-profile",
rule: DissonancePlacement,
species: Fifth,
title: "Compare dissonance policy",
objective: "Measure how profile policy changes weak-beat dissonance handling.",
prompt: "Run both classroom and strict configurations over the same phrase.",
target: "Explain the changed severity and preserve the same note data.",
level: "advanced",
notes: 12,
},
{
id: "range-foundation",
rule: MelodyRange,
species: First,
title: "Keep a singable span",
objective: "Measure the range of a short first-species line.",
prompt: "Find the outlying pitch and move it toward the phrase center.",
target: "Meet the configured span without changing the cadence note.",
level: "foundation",
notes: 5,
},
{
id: "range-contour",
rule: MelodyRange,
species: Second,
title: "Constrain a decorated line",
objective: "Balance range and passing motion in a second-species phrase.",
prompt: "Compare pitch statistics before and after a local range edit.",
target: "Reduce the span while retaining the passing-note contour.",
level: "intermediate",
notes: 9,
},
{
id: "range-voice-profile",
rule: MelodyRange,
species: Fifth,
title: "Fit a line to a voice profile",
objective: "Use a voice assessment to detect a phrase outside its register.",
prompt: "Evaluate the phrase against the selected voice range and inspect its score.",
target: "Return a register-safe phrase with the same harmonic outline.",
level: "advanced",
notes: 14,
},
{
id: "leap-size",
rule: LargeLeap,
species: First,
title: "Reduce an unsupported leap",
objective: "Identify a melodic leap that exceeds the analysis profile.",
prompt: "Replace the leap with a consonant stepwise bridge and rerun the analyzer.",
target: "Remove the large-leap finding without introducing a crossing.",
level: "foundation",
notes: 6,
},
{
id: "leap-phrase-point",
rule: LargeLeap,
species: Second,
title: "Place a leap at a phrase point",
objective: "Distinguish a structural leap from an accidental interior leap.",
prompt: "Use turning points and beat strength to choose the better location.",
target: "Keep one intentional leap at a structural point and smooth the interior motion.",
level: "intermediate",
notes: 10,
},
{
id: "leap-candidate-filter",
rule: LargeLeap,
species: Fifth,
title: "Filter large-leap candidates",
objective: "Combine candidate generation with a maximum leap policy.",
prompt: "Generate a bounded candidate set and reject pitches exceeding the profile limit.",
target: "Select the highest-scoring candidate that satisfies the leap bound.",
level: "advanced",
notes: 14,
},
{
id: "recovery-step",
rule: LeapRecovery,
species: First,
title: "Recover after an upward leap",
objective: "Detect whether the following event reverses a large leap by step.",
prompt: "Change the immediate successor while leaving the leap itself unchanged.",
target: "Create a clear descending step after the upward leap.",
level: "foundation",
notes: 6,
},
{
id: "recovery-direction",
rule: LeapRecovery,
species: Second,
title: "Repair a same-direction recovery",
objective: "Compare same-direction and contrary-direction recovery choices.",
prompt: "Use contour steps to choose the candidate that restores balance.",
target: "Make the recovery direction explicit without breaking the species rhythm.",
level: "intermediate",
notes: 9,
},
{
id: "recovery-cadence",
rule: LeapRecovery,
species: Fifth,
title: "Recover before a cadence",
objective: "Plan recovery while preserving the final cadence shape.",
prompt: "Rank recovery candidates by contour, range, and cadence distance.",
target: "Improve melodic balance without altering the final sonority.",
level: "advanced",
notes: 15,
},
{
id: "species-count",
rule: SpeciesRhythm,
species: Second,
title: "Match second-species density",
objective: "Validate the expected two-to-one event relationship.",
prompt: "Inspect the species finding and add or remove only one counterpoint event.",
target: "Match the cantus event count while retaining ordered onsets.",
level: "foundation",
notes: 8,
},
{
id: "species-suspension",
rule: SpeciesRhythm,
species: Fourth,
title: "Mark suspension ties",
objective: "Use tie metadata to distinguish a suspension from a new attack.",
prompt: "Add the missing tie to each non-initial suspension event.",
target: "Pass the fourth-species tie check with no duration changes.",
level: "intermediate",
notes: 10,
},
{
id: "species-mixed",
rule: SpeciesRhythm,
species: Fifth,
title: "Audit mixed-species density",
objective: "Review a flexible fifth-species phrase with varied durations.",
prompt: "Use the rhythm summary to locate gaps, overlaps, and syncopations.",
target: "Keep intentional variation while eliminating accidental timeline errors.",
level: "advanced",
notes: 16,
},
{
id: "cadence-final",
rule: CadenceShape,
species: First,
title: "Validate the final sonority",
objective: "Check the final aligned interval and its approach.",
prompt: "Inspect only the last two events and compare the cadence finding.",
target: "End on a valid sonority with a controlled approach.",
level: "intermediate",
notes: 6,
},
{
id: "cadence-report",
rule: CadenceShape,
species: Second,
title: "Explain a cadence in a report",
objective: "Turn the cadence finding into a concise report section.",
prompt: "Export the analysis and identify the final approach in Markdown and CSV.",
target: "Produce matching machine-readable and human-readable cadence evidence.",
level: "advanced",
notes: 10,
},
{
id: "cadence-planner",
rule: CadenceShape,
species: Fifth,
title: "Plan a cadence candidate",
objective: "Use phrase planning to select a legal final approach.",
prompt: "Generate cadence candidates and rank them against the active profile.",
target: "Return a cadence plan whose final interval and approach both pass validation.",
level: "advanced",
notes: 15,
},
]
}
///|
pub fn exercise_count() -> Int {
exercise_catalog().length()
}
///|
pub fn exercises_for_rule(rule : RuleKind) -> Array[ExerciseSpec] {
let result : Array[ExerciseSpec] = []
for item in exercise_catalog() {
if item.rule == rule {
result.push(item)
}
}
result
}
///|
pub fn exercises_for_species(species : Species) -> Array[ExerciseSpec] {
let result : Array[ExerciseSpec] = []
for item in exercise_catalog() {
if item.species == species {
result.push(item)
}
}
result
}
///|
pub fn exercises_for_level(level : String) -> Array[ExerciseSpec] {
let result : Array[ExerciseSpec] = []
for item in exercise_catalog() {
if item.level == level {
result.push(item)
}
}
result
}
///|
pub fn exercise(id : String) -> ExerciseSpec? {
for item in exercise_catalog() {
if item.id == id {
return Some(item)
}
}
None
}
///|
pub fn next_exercise(
rule : RuleKind,
completed : Array[String],
) -> ExerciseSpec? {
for item in exercises_for_rule(rule) {
let mut found = false
for id in completed {
if id == item.id {
found = true
}
}
if !found {
return Some(item)
}
}
None
}
///|
pub fn exercise_prompt(item : ExerciseSpec) -> String {
"\{item.title}: \{item.prompt}"
}
///|
pub fn exercise_objective(item : ExerciseSpec) -> String {
item.objective
}
///|
pub fn exercise_target(item : ExerciseSpec) -> String {
item.target
}
///|
pub fn exercise_minutes(item : ExerciseSpec) -> Int {
5 + item.notes / 2
}
///|
pub fn exercise_summary(item : ExerciseSpec) -> String {
"\{item.id} / \{item.level} / \{item.notes} notes / \{rule_family(item.rule)}"
}
///|
pub fn exercise_catalog_markdown(rule : RuleKind) -> String {
let lines : Array[String] = ["## Exercises for \{rule_family(rule)}", ""]
for item in exercises_for_rule(rule) {
lines.push("- \{item.id}: \{item.title} (\{item.level})")
}
lines.join("\n")
}