sim-lib-music-serial 0.1.1

Immutable serial plans with stable row and event identity, explicit provenance, and validated partial temporal order.
Documentation
use std::collections::BTreeMap;

use sim_lib_pitch_core::PitchClass;
use sim_lib_pitch_serial::{DerivationCellRelation, DerivationMatch, RowForm};

use crate::{
    EventPlacement, PlannedSerialEvent, RowInstanceId, SerialDeployError, SerialEventId,
    SerialOrigin, SerialPlan, SerialRole, StructuralLicense, VoiceId,
};

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct DerivedCellPlanOccurrence {
    pub(crate) event_id: SerialEventId,
    pub(crate) voice: VoiceId,
    pub(crate) occurrence_index: usize,
    pub(crate) source_ordinals: Vec<u8>,
    pub(crate) generator_ordinals: Vec<u8>,
    pub(crate) generator_classes: Vec<PitchClass>,
    pub(crate) operation: sim_lib_pitch_serial::RowOperation,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct DerivedCellPlan {
    pub(crate) plan: SerialPlan,
    pub(crate) derivation: DerivationMatch,
    pub(crate) occurrences: Vec<DerivedCellPlanOccurrence>,
}

pub(crate) fn build_derived_cell_plan(
    row_id: RowInstanceId,
    row_form: RowForm,
    derivation: DerivationMatch,
    voices: Vec<VoiceId>,
    event_prefix: &str,
    rationale: &str,
    license: StructuralLicense,
) -> Result<DerivedCellPlan, SerialDeployError> {
    if derivation.cells.len() != voices.len() {
        return Err(SerialDeployError::VoiceCountMismatch {
            deployer: "derived-cells".to_owned(),
            expected: derivation.cells.len(),
            actual: voices.len(),
        });
    }

    let generator = generator_classes(&row_form, &derivation.cells[0]);
    let mut rows = BTreeMap::new();
    rows.insert(row_id.clone(), row_form);

    let mut events = BTreeMap::new();
    let mut precedence = Vec::new();
    let mut previous = None::<SerialEventId>;
    let mut occurrences = Vec::with_capacity(derivation.cells.len());

    for (occurrence_index, (cell, voice)) in derivation.cells.iter().zip(voices).enumerate() {
        let event_id = SerialEventId::new(format!("{event_prefix}/occurrence-{occurrence_index}"))
            .map_err(|error| SerialDeployError::Plan(error.to_string()))?;
        let event = PlannedSerialEvent {
            id: event_id.clone(),
            ordinals: cell
                .ordinals
                .iter()
                .copied()
                .map(|ordinal| crate::OrdinalRef::new(row_id.clone(), usize::from(ordinal)))
                .collect(),
            role: SerialRole::Structural,
            origin: SerialOrigin::Structural {
                rationale: format!(
                    "{rationale}; derived cell {occurrence_index} via {}",
                    cell.operation
                ),
            },
            voice: voice.clone(),
            placement: EventPlacement::independent(),
            parents: Vec::new(),
            licenses: vec![license.clone()],
        };
        events.insert(event_id.clone(), event);
        if let Some(previous_id) = previous.as_ref() {
            precedence.push((previous_id.clone(), event_id.clone()));
        }
        previous = Some(event_id.clone());
        occurrences.push(DerivedCellPlanOccurrence {
            event_id,
            voice,
            occurrence_index,
            source_ordinals: cell.ordinals.clone(),
            generator_ordinals: derivation.cells[0].ordinals.clone(),
            generator_classes: generator.clone(),
            operation: cell.operation,
        });
    }

    Ok(DerivedCellPlan {
        plan: SerialPlan::try_new(rows, events, precedence)
            .map_err(|error| SerialDeployError::Plan(error.to_string()))?,
        derivation,
        occurrences,
    })
}

fn generator_classes(row_form: &RowForm, cell: &DerivationCellRelation) -> Vec<PitchClass> {
    cell.ordinals
        .iter()
        .map(|ordinal| row_form.classes()[usize::from(*ordinal)])
        .collect()
}