Skip to main content

sim_lib_music_consonance/
completion.rs

1use std::cmp::Ordering;
2
3use sim_lib_discrete_search::{
4    SearchControl, SearchInterrupt, SearchProblem, SearchReceipt, SearchStep, solve,
5};
6use sim_lib_music_core::{ObjectId, Staff};
7use thiserror::Error;
8
9use crate::constraints::changed_spans;
10use crate::patch::addition_ids;
11use crate::{
12    Addition, CompletionConstraints, ConsonanceError, ConsonancePatch, ConsonancePolicy,
13    ConsonanceReport, ConstraintError, PatchError, TimeSpan, apply_patch, evaluate_staff,
14};
15
16/// Candidate additions and explicit acceptance policy for one completion run.
17#[derive(Clone, Debug, Default, PartialEq)]
18pub struct CompletionRequest {
19    /// Deterministically ordered typed candidates considered by the search.
20    pub candidates: Vec<Addition>,
21    /// Metric, preservation, range, and style constraints.
22    pub constraints: CompletionConstraints,
23}
24
25/// Provenance of the selected additive completion.
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct CompletionProvenance {
28    /// Candidate indexes selected in caller-supplied order.
29    pub selected_candidates: Vec<usize>,
30    /// Every source identity proven present and byte-for-byte unchanged.
31    pub preserved_ids: Vec<ObjectId>,
32    /// Every voice, note, and event identity introduced by the patch.
33    pub added_ids: Vec<ObjectId>,
34    /// Stable facts about acceptance and inverse verification.
35    pub facts: Vec<String>,
36}
37
38/// A reversible patch paired with analysis and bounded-search evidence.
39#[derive(Clone, Debug, PartialEq)]
40pub struct CompletionResult {
41    /// Strictly additive, content-bound patch.
42    pub patch: ConsonancePatch,
43    /// Consonance analysis before completion.
44    pub before: ConsonanceReport,
45    /// Consonance analysis after completion.
46    pub after: ConsonanceReport,
47    /// Exact after-report windows containing introduced events.
48    pub changed_windows: Vec<TimeSpan>,
49    /// Source, selection, identity, and inverse evidence.
50    pub provenance: CompletionProvenance,
51    /// Generic bounded-search termination receipt.
52    pub search: SearchReceipt,
53}
54
55/// Failure to run or satisfy additive completion.
56#[derive(Clone, Debug, Error, PartialEq)]
57pub enum CompletionError {
58    /// Initial consonance evaluation failed.
59    #[error(transparent)]
60    Consonance(#[from] ConsonanceError),
61    /// Patch construction or inversion failed.
62    #[error(transparent)]
63    Patch(#[from] PatchError),
64    /// The request's explicit constraints were malformed.
65    #[error(transparent)]
66    Constraint(#[from] ConstraintError),
67    /// Search terminated without a feasible completion.
68    #[error("bounded completion produced no feasible patch")]
69    NoCompletion {
70        /// Original report retained for diagnosis.
71        before: Box<ConsonanceReport>,
72        /// Honest complete, partial, cancelled, or infeasible receipt.
73        search: Box<SearchReceipt>,
74    },
75}
76
77/// Searches typed candidate subsets under explicit bounds and constraints.
78///
79/// The input staff is borrowed and never mutated. A partial search may return a
80/// result only when it actually emitted a feasible patch; its `SearchReceipt`
81/// remains `Partial` or `Cancelled` rather than being upgraded to complete.
82pub fn complete_staff(
83    source: &Staff,
84    policy: &ConsonancePolicy,
85    request: &CompletionRequest,
86    control: SearchControl,
87    interrupt: &dyn SearchInterrupt,
88) -> Result<CompletionResult, CompletionError> {
89    request.constraints.validate(source)?;
90    for candidate in &request.candidates {
91        ConsonancePatch::new(source, vec![candidate.clone()])?;
92    }
93    let before = evaluate_staff(source, policy)?;
94    let problem = CompletionProblem {
95        source,
96        policy,
97        request,
98    };
99    let run = solve(&problem, control, interrupt);
100    let receipt = run.receipt;
101    let Some(candidate) = run.outputs.into_iter().min_by(compare_outputs) else {
102        return Err(CompletionError::NoCompletion {
103            before: Box::new(before),
104            search: Box::new(receipt),
105        });
106    };
107    let restored = crate::remove_patch(&candidate.completed, &candidate.patch)?;
108    if restored != *source {
109        return Err(PatchError::InvalidInverse(
110            "remove(apply(source, patch), patch) changed source identities or values".to_owned(),
111        )
112        .into());
113    }
114    let preserved_ids = source.object_ids();
115    let added_ids = addition_ids(&candidate.patch.additions);
116    let changed_windows = changed_spans(&candidate.after, &candidate.patch.additions);
117    Ok(CompletionResult {
118        patch: candidate.patch,
119        before,
120        after: candidate.after,
121        changed_windows,
122        provenance: CompletionProvenance {
123            selected_candidates: candidate.selected,
124            preserved_ids,
125            added_ids,
126            facts: vec![
127                "source-material=immutable".to_owned(),
128                "patch-operation=additions-only".to_owned(),
129                "inverse=remove(apply(source,patch),patch)==source".to_owned(),
130                "metric-thresholds=checked-per-intersecting-window".to_owned(),
131            ],
132        },
133        search: receipt,
134    })
135}
136
137struct CompletionProblem<'a> {
138    source: &'a Staff,
139    policy: &'a ConsonancePolicy,
140    request: &'a CompletionRequest,
141}
142
143#[derive(Clone, Debug)]
144struct CompletionState {
145    cursor: usize,
146    selected: Vec<usize>,
147}
148
149#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
150enum CompletionChoice {
151    Skip,
152    Include,
153}
154
155#[derive(Clone, Debug)]
156struct CompletionOutput {
157    selected: Vec<usize>,
158    patch: ConsonancePatch,
159    completed: Staff,
160    after: ConsonanceReport,
161}
162
163impl SearchProblem for CompletionProblem<'_> {
164    type State = CompletionState;
165    type Choice = CompletionChoice;
166    type Output = CompletionOutput;
167
168    fn initial_state(&self) -> Self::State {
169        CompletionState {
170            cursor: 0,
171            selected: Vec::new(),
172        }
173    }
174
175    fn expand(&self, state: &Self::State, out: &mut Vec<Self::Choice>) {
176        if state.cursor < self.request.candidates.len() {
177            out.extend([CompletionChoice::Skip, CompletionChoice::Include]);
178        }
179    }
180
181    fn apply(&self, state: &Self::State, choice: &Self::Choice) -> SearchStep<Self::State> {
182        let mut next = state.clone();
183        if *choice == CompletionChoice::Include {
184            next.selected.push(state.cursor);
185        }
186        next.cursor += 1;
187        let additions = self.selected_additions(&next.selected);
188        match self
189            .request
190            .constraints
191            .accepts_partial(self.source, &additions)
192        {
193            Ok(true) => SearchStep::Continue(next),
194            Ok(false) => SearchStep::pruned("completion constraints rejected candidate prefix"),
195            Err(error) => SearchStep::pruned(error.to_string()),
196        }
197    }
198
199    fn finish(&self, state: &Self::State) -> Option<Self::Output> {
200        if state.cursor != self.request.candidates.len() {
201            return None;
202        }
203        let additions = self.selected_additions(&state.selected);
204        let patch = ConsonancePatch::new(self.source, additions.clone()).ok()?;
205        let completed = apply_patch(self.source, &patch).ok()?;
206        let after = evaluate_staff(&completed, self.policy).ok()?;
207        self.request
208            .constraints
209            .accepts_complete(self.source, &additions, &after)
210            .ok()?
211            .then(|| CompletionOutput {
212                selected: state.selected.clone(),
213                patch,
214                completed,
215                after,
216            })
217    }
218
219    fn score_state(&self, state: &Self::State) -> i64 {
220        i64::try_from(state.selected.len()).unwrap_or(i64::MAX)
221    }
222
223    fn bound(&self, state: &Self::State) -> Option<i64> {
224        Some(i64::try_from(state.selected.len()).unwrap_or(i64::MAX))
225    }
226
227    fn output_score(&self, output: &Self::Output) -> Option<i64> {
228        Some(i64::try_from(output.selected.len()).unwrap_or(i64::MAX))
229    }
230}
231
232impl CompletionProblem<'_> {
233    fn selected_additions(&self, selected: &[usize]) -> Vec<Addition> {
234        selected
235            .iter()
236            .map(|index| self.request.candidates[*index].clone())
237            .collect()
238    }
239}
240
241fn compare_outputs(left: &CompletionOutput, right: &CompletionOutput) -> Ordering {
242    left.selected
243        .len()
244        .cmp(&right.selected.len())
245        .then_with(|| left.selected.cmp(&right.selected))
246}