sim-lib-music-serial 0.1.0

Immutable serial plans with stable row and event identity, explicit provenance, and validated partial temporal order.
Documentation
use sim_lib_pitch_scale::Scale;
use sim_lib_pitch_serial::RowForm;

use crate::{
    ReferentialClaim, ReferentialContextReport, ReferentialEvidence, ReferentialReport,
    ReferentialRequest,
    referential::{ratio_summary, roman_claim, scale_degrees},
};

pub(crate) fn build_referential_report(
    row_form: &RowForm,
    request: ReferentialRequest,
) -> Result<ReferentialReport, String> {
    if request.ordinals.is_empty() {
        return Err("referential ordinals cannot be empty".to_owned());
    }
    let segment = row_form
        .row()
        .indexed_segment(&request.ordinals)
        .map_err(|error| error.to_string())?;
    let subset_classes = segment.classes().to_vec();
    let subset_mask = segment.mask();

    let (claim, harmonic_claim, scale_degrees, ratio_summary) = match &request.evidence {
        ReferentialEvidence::Chord { label, chord } => {
            let target = chord.pitch_set().map_err(|error| error.to_string())?;
            let claim = if target == subset_mask {
                ReferentialClaim::EmbeddedSubset
            } else {
                ReferentialClaim::NoReferentialSubset
            };
            (claim, Some(label.clone()), Vec::new(), None)
        }
        ReferentialEvidence::Scale { label, scale } => {
            let claim = if subset_classes
                .iter()
                .all(|class| scale.mask().pitch_classes().contains(class))
            {
                ReferentialClaim::EmbeddedSubset
            } else {
                ReferentialClaim::NoReferentialSubset
            };
            (
                claim,
                Some(format!("{} {}", label, scale.mode.name())),
                scale_degrees(*scale, &subset_classes),
                None,
            )
        }
        ReferentialEvidence::KeyRegion {
            label,
            key,
            preferred_root,
        } => {
            let scale = Scale::new(key.tonic, key.mode);
            let claim = if subset_classes
                .iter()
                .all(|class| scale.mask().pitch_classes().contains(class))
            {
                ReferentialClaim::EmbeddedSubset
            } else {
                ReferentialClaim::NoReferentialSubset
            };
            (
                claim,
                roman_claim(subset_mask, *key, *preferred_root).or_else(|| Some(label.clone())),
                scale_degrees(scale, &subset_classes),
                None,
            )
        }
        ReferentialEvidence::PitchRatio {
            ratios,
            policy,
            label,
        } => {
            let summary = ratio_summary(ratios, *policy)?;
            let claim = if ratios.len() == subset_classes.len() {
                ReferentialClaim::EmbeddedSubset
            } else {
                ReferentialClaim::NoReferentialSubset
            };
            (claim, Some(label.clone()), Vec::new(), Some(summary))
        }
        ReferentialEvidence::Mask { label, mask } => {
            let claim = if *mask == subset_mask {
                ReferentialClaim::EmbeddedSubset
            } else {
                ReferentialClaim::NoReferentialSubset
            };
            (claim, Some(label.clone()), Vec::new(), None)
        }
    };

    Ok(ReferentialReport {
        claim,
        ordinals: request.ordinals,
        context: ReferentialContextReport {
            evidence_kind: request.evidence.kind(),
            evidence_label: request.evidence.label().to_owned(),
            harmonic_claim,
            subset_classes,
            subset_mask,
            scale_degrees,
            ratio_summary,
        },
        emphasis: request.emphasis,
        claims_whole_passage_tonality: false,
    })
}