Skip to main content

solverforge_core/domain/
dynamic.rs

1//! Dynamic model contracts for host-language integrations.
2
3use std::fmt;
4use std::sync::Arc;
5
6use crate::domain::{EntityClassId, SolutionDescriptor, VariableId};
7use crate::score::Score;
8
9mod backend;
10mod resolution;
11
12use backend::{BackendListAccess, BackendScalarAccess};
13use resolution::{resolve_dynamic_descriptor_indexes, DynamicVariableKind};
14
15mod assignment;
16
17pub use assignment::{
18    DynamicScalarAssignmentMetadata, DynamicScalarAssignmentMetadataCapabilities,
19};
20
21/// Rust-owned dynamic planning model backend.
22///
23/// Binding crates implement this trait on their concrete dynamic solution
24/// state. The trait is expressed in logical entity and variable IDs rather
25/// than Rust `TypeId`s.
26pub trait DynamicModelBackend: Clone + Send + Sync + 'static {
27    type Score: Score;
28
29    fn entity_count(&self, entity: EntityClassId) -> usize;
30
31    fn get_scalar(&self, entity: EntityClassId, row: usize, variable: VariableId) -> Option<usize>;
32
33    fn set_scalar(
34        &mut self,
35        entity: EntityClassId,
36        row: usize,
37        variable: VariableId,
38        value: Option<usize>,
39    );
40
41    fn list_len(&self, entity: EntityClassId, row: usize, variable: VariableId) -> usize;
42
43    fn list_get(
44        &self,
45        entity: EntityClassId,
46        row: usize,
47        variable: VariableId,
48        pos: usize,
49    ) -> Option<usize>;
50
51    fn list_insert(
52        &mut self,
53        entity: EntityClassId,
54        row: usize,
55        variable: VariableId,
56        pos: usize,
57        value: usize,
58    );
59
60    fn list_remove(
61        &mut self,
62        entity: EntityClassId,
63        row: usize,
64        variable: VariableId,
65        pos: usize,
66    ) -> Option<usize>;
67
68    fn candidate_values(&self, entity: EntityClassId, row: usize, variable: VariableId)
69        -> &[usize];
70
71    fn scalar_value_is_legal(
72        &self,
73        entity: EntityClassId,
74        row: usize,
75        variable: VariableId,
76        value: usize,
77    ) -> bool;
78
79    fn list_element_count(&self, _entity: EntityClassId, _variable: VariableId) -> usize {
80        0
81    }
82
83    fn list_element(
84        &self,
85        _entity: EntityClassId,
86        _variable: VariableId,
87        element_index: usize,
88    ) -> Option<usize> {
89        Some(element_index)
90    }
91
92    fn list_assigned_elements(&self, _entity: EntityClassId, _variable: VariableId) -> Vec<usize> {
93        Vec::new()
94    }
95}
96
97/// Object-safe dynamic scalar variable access.
98pub trait DynamicScalarAccess<S>: Send + Sync {
99    fn entity_class(&self) -> EntityClassId;
100    fn variable(&self) -> VariableId;
101    fn entity_count(&self, solution: &S) -> usize;
102    fn get(&self, solution: &S, row: usize) -> Option<usize>;
103    fn set(&self, solution: &mut S, row: usize, value: Option<usize>);
104    fn candidate_values<'a>(&self, solution: &'a S, row: usize) -> &'a [usize];
105    fn value_is_legal(&self, solution: &S, row: usize, value: usize) -> bool;
106
107    /// Whether this slot structurally supplies an ordered nearby-value source.
108    ///
109    /// This reports schema capability rather than a row result. The runtime
110    /// still calls [`Self::visit_nearby_value_candidates`] for each row, since
111    /// a row-local source or callback may intentionally be absent for one row.
112    fn has_nearby_value_candidates(&self) -> bool {
113        false
114    }
115
116    /// Visits the ordered nearby-value source for one row.
117    ///
118    /// `limit` is a source-consumption limit, not a result limit. Implementors
119    /// that bridge lazy host-language iterables must not consume values after
120    /// that limit. Return `true` when this row supplied a source, including an
121    /// empty source. Return `false` when no row source is available so the
122    /// solver can use the ordinary candidate-value fallback.
123    fn visit_nearby_value_candidates(
124        &self,
125        _solution: &S,
126        _row: usize,
127        _limit: usize,
128        _visit: &mut dyn FnMut(usize),
129    ) -> bool {
130        false
131    }
132
133    /// Returns the nearby-value distance for a source candidate when one is
134    /// available. `None` preserves source order as the distance fallback.
135    fn nearby_value_distance(&self, _solution: &S, _row: usize, _candidate: usize) -> Option<f64> {
136        None
137    }
138
139    /// Whether this slot structurally supplies an ordered nearby-entity source.
140    fn has_nearby_entity_candidates(&self) -> bool {
141        false
142    }
143
144    /// Visits the ordered nearby-entity source for one left-hand row.
145    ///
146    /// `limit` has the same source-consumption contract as
147    /// [`Self::visit_nearby_value_candidates`]. Return `true` when a row
148    /// source was supplied, including an empty source; otherwise return
149    /// `false` to request the all-entity fallback.
150    fn visit_nearby_entity_candidates(
151        &self,
152        _solution: &S,
153        _left_row: usize,
154        _limit: usize,
155        _visit: &mut dyn FnMut(usize),
156    ) -> bool {
157        false
158    }
159
160    /// Returns the nearby-entity distance for a source candidate when one is
161    /// available. `None` preserves source order as the distance fallback.
162    fn nearby_entity_distance(
163        &self,
164        _solution: &S,
165        _left_row: usize,
166        _right_row: usize,
167    ) -> Option<f64> {
168        None
169    }
170}
171
172/// Object-safe dynamic list variable access.
173///
174/// Basic list mutation capabilities are explicit. `false`/`None` from the
175/// optional range operations means the access implementation did not bind that
176/// operation; it is not permission for a phase to emulate it piecemeal. A
177/// canonical list kernel must validate these bits before creating a reverse,
178/// sublist, permute, swap, ruin, or route phase.
179#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
180pub struct DynamicListAccessCapabilities {
181    pub set: bool,
182    pub replace: bool,
183    pub reverse: bool,
184    pub sublist: bool,
185}
186
187pub trait DynamicListAccess<S>: Send + Sync {
188    fn entity_class(&self) -> EntityClassId;
189    fn variable(&self) -> VariableId;
190    fn entity_count(&self, solution: &S) -> usize;
191    fn element_count(&self, solution: &S) -> usize;
192    fn element(&self, solution: &S, element_index: usize) -> Option<usize>;
193    fn assigned_elements(&self, solution: &S) -> Vec<usize>;
194    fn len(&self, solution: &S, row: usize) -> usize;
195    fn get(&self, solution: &S, row: usize, pos: usize) -> Option<usize>;
196    fn insert(&self, solution: &mut S, row: usize, pos: usize, value: usize);
197    fn remove(&self, solution: &mut S, row: usize, pos: usize) -> Option<usize>;
198
199    fn capabilities(&self) -> DynamicListAccessCapabilities {
200        DynamicListAccessCapabilities::default()
201    }
202
203    fn set(&self, _solution: &mut S, _row: usize, _pos: usize, _value: usize) -> bool {
204        false
205    }
206
207    /// Replaces one complete row list as one logical mutation.
208    fn replace(&self, _solution: &mut S, _row: usize, _values: Vec<usize>) -> bool {
209        false
210    }
211
212    fn reverse(&self, _solution: &mut S, _row: usize, _start: usize, _end: usize) -> bool {
213        false
214    }
215
216    fn sublist_remove(
217        &self,
218        _solution: &mut S,
219        _row: usize,
220        _start: usize,
221        _end: usize,
222    ) -> Option<Vec<usize>> {
223        None
224    }
225
226    fn sublist_insert(
227        &self,
228        _solution: &mut S,
229        _row: usize,
230        _pos: usize,
231        _values: Vec<usize>,
232    ) -> bool {
233        false
234    }
235}
236
237/// Immutable capabilities bound to one dynamic planning-list slot.
238///
239/// A `true` bit means the corresponding method on
240/// [`DynamicListMetadata`] is structurally available. It does *not* install a
241/// semantic fallback: phase assembly must reject a selected heuristic when a
242/// required capability is absent. Optional owner restriction remains distinct
243/// from a missing owner capability: when `element_owner` is available, an
244/// individual element may still return `None` to mean unrestricted.
245#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
246pub struct DynamicListMetadataCapabilities {
247    pub element_owner: bool,
248    pub construction_order_key: bool,
249    /// Duration metadata. Full precedence-aware construction requires this
250    /// together with `precedence_successors`; successor-only metadata remains
251    /// a valid ListRuin contract.
252    pub precedence_duration: bool,
253    /// Ordered successor metadata. This is intentionally independent from
254    /// duration so dynamic bindings preserve the same successor-only policy
255    /// as native list slots.
256    pub precedence_successors: bool,
257    pub cross_position_distance: bool,
258    pub intra_position_distance: bool,
259    pub route: bool,
260    pub savings: bool,
261}
262
263/// Slot-bound immutable metadata used by canonical dynamic-list phases.
264///
265/// This deliberately contains no discovery API, current-phase lookup, or
266/// global/TLS state. Binding crates compile schema fields and callbacks once
267/// into one implementation for a specific `(entity_class, variable)` pair,
268/// then attach it to the corresponding [`DynamicListVariableSlot`]. Runtime
269/// callers validate [`Self::capabilities`] before invoking a selected family;
270/// an implementation must not synthesize depot, metric, distance, feasibility,
271/// precedence, or ownership defaults.
272pub trait DynamicListMetadata<S>: Send + Sync {
273    fn entity_class(&self) -> EntityClassId;
274    fn variable(&self) -> VariableId;
275    fn capabilities(&self) -> DynamicListMetadataCapabilities;
276
277    /// Fixed owner for one element when owner restriction is declared.
278    /// `None` means that element is unrestricted; callers must consult
279    /// `capabilities().element_owner` to distinguish an unavailable owner
280    /// source from an unrestricted element.
281    fn element_owner(&self, solution: &S, element: usize) -> Option<usize>;
282
283    /// Deterministic construction ordering key for one element.
284    fn construction_order_key(&self, solution: &S, element: usize) -> Option<i64>;
285
286    /// Duration used by full precedence-aware construction and scheduling.
287    /// It may be absent when this slot deliberately supplies successor-only
288    /// metadata for ListRuin.
289    fn precedence_duration(&self, solution: &S, element: usize) -> Option<usize>;
290
291    /// Extends `successors` with fixed successors in declared order. Returns
292    /// `false` only when successor metadata is unavailable for this slot.
293    ///
294    /// The concrete output buffer preserves the object-safe dynamic metadata
295    /// seam without putting a `dyn FnMut` dispatch in a precedence candidate
296    /// loop.
297    fn extend_precedence_successors(
298        &self,
299        solution: &S,
300        element: usize,
301        successors: &mut Vec<usize>,
302    ) -> bool;
303
304    /// Explicit cross-route position metric for nearby list neighborhoods.
305    fn cross_position_distance(
306        &self,
307        solution: &S,
308        from_entity: usize,
309        from_position: usize,
310        to_entity: usize,
311        to_position: usize,
312    ) -> Option<f64>;
313
314    /// Explicit within-route position metric for nearby K-opt/neighborhoods.
315    fn intra_position_distance(
316        &self,
317        solution: &S,
318        entity: usize,
319        from_position: usize,
320        to_position: usize,
321    ) -> Option<f64>;
322
323    /// Strict route-local bundle used by K-opt and other route phases.
324    ///
325    /// Reading and replacing a route are deliberately *not* metadata
326    /// callbacks. They come from the slot's [`DynamicListAccess`] (`get` and
327    /// one logical `replace` operation) so every route mutation follows the
328    /// same dirty-row/revision path as every other list mutation. A canonical
329    /// phase must require both that base access and this complete metadata
330    /// bundle: depot, distance, and feasibility.
331    fn route_depot(&self, solution: &S, entity: usize) -> Option<usize>;
332    fn route_distance(&self, solution: &S, entity: usize, from: usize, to: usize) -> Option<i64>;
333    fn route_feasible(&self, solution: &S, entity: usize, route: &[usize]) -> Option<bool>;
334
335    /// Savings-specific bundle used only by Clarke-Wright construction.
336    fn savings_depot(&self, solution: &S, entity: usize) -> Option<usize>;
337    fn savings_metric_class(&self, solution: &S, entity: usize) -> Option<usize>;
338    fn savings_distance(&self, solution: &S, entity: usize, from: usize, to: usize) -> Option<i64>;
339    fn savings_feasible(&self, solution: &S, entity: usize, route: &[usize]) -> Option<bool>;
340}
341
342/// Public dynamic scalar variable slot.
343pub struct DynamicScalarVariableSlot<S> {
344    pub entity: EntityClassId,
345    pub variable: VariableId,
346    pub entity_type_name: &'static str,
347    pub variable_name: &'static str,
348    pub allows_unassigned: bool,
349    descriptor_index: Option<usize>,
350    descriptor_variable_index: Option<usize>,
351    access: Arc<dyn DynamicScalarAccess<S>>,
352}
353
354impl<S> Clone for DynamicScalarVariableSlot<S> {
355    fn clone(&self) -> Self {
356        Self {
357            entity: self.entity,
358            variable: self.variable,
359            entity_type_name: self.entity_type_name,
360            variable_name: self.variable_name,
361            allows_unassigned: self.allows_unassigned,
362            descriptor_index: self.descriptor_index,
363            descriptor_variable_index: self.descriptor_variable_index,
364            access: Arc::clone(&self.access),
365        }
366    }
367}
368
369impl<S> fmt::Debug for DynamicScalarVariableSlot<S> {
370    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371        f.debug_struct("DynamicScalarVariableSlot")
372            .field("entity", &self.entity)
373            .field("variable", &self.variable)
374            .field("entity_type_name", &self.entity_type_name)
375            .field("variable_name", &self.variable_name)
376            .field("allows_unassigned", &self.allows_unassigned)
377            .field("descriptor_index", &self.descriptor_index)
378            .field("descriptor_variable_index", &self.descriptor_variable_index)
379            .finish()
380    }
381}
382
383impl<S> PartialEq for DynamicScalarVariableSlot<S> {
384    fn eq(&self, other: &Self) -> bool {
385        self.entity == other.entity
386            && self.variable == other.variable
387            && self.entity_type_name == other.entity_type_name
388            && self.variable_name == other.variable_name
389            && self.allows_unassigned == other.allows_unassigned
390            && self.descriptor_index == other.descriptor_index
391            && self.descriptor_variable_index == other.descriptor_variable_index
392    }
393}
394
395impl<S> Eq for DynamicScalarVariableSlot<S> {}
396
397impl<S> DynamicScalarVariableSlot<S> {
398    pub fn with_access(
399        entity: EntityClassId,
400        variable: VariableId,
401        entity_type_name: &'static str,
402        variable_name: &'static str,
403        allows_unassigned: bool,
404        access: Arc<dyn DynamicScalarAccess<S>>,
405    ) -> Self {
406        Self {
407            entity,
408            variable,
409            entity_type_name,
410            variable_name,
411            allows_unassigned,
412            descriptor_index: None,
413            descriptor_variable_index: None,
414            access,
415        }
416    }
417
418    pub fn resolve_descriptor_index(
419        &mut self,
420        descriptor: &SolutionDescriptor,
421    ) -> Result<(), String> {
422        let indexes = resolve_dynamic_descriptor_indexes(
423            descriptor,
424            self.entity,
425            self.variable,
426            self.entity_type_name,
427            self.variable_name,
428            DynamicVariableKind::Scalar,
429        )?;
430        if let Some(existing) = self.descriptor_index {
431            if existing != indexes.descriptor_index {
432                return Err(format!(
433                    "dynamic scalar variable {}.{} was pre-bound to descriptor index {existing}, but logical entity ID {} resolves to descriptor index {}",
434                    self.entity_type_name,
435                    self.variable_name,
436                    self.entity.0,
437                    indexes.descriptor_index
438                ));
439            }
440        }
441        self.descriptor_index = Some(indexes.descriptor_index);
442        self.descriptor_variable_index = Some(indexes.variable_index);
443        Ok(())
444    }
445
446    pub fn resolved_against(mut self, descriptor: &SolutionDescriptor) -> Result<Self, String> {
447        self.resolve_descriptor_index(descriptor)?;
448        Ok(self)
449    }
450
451    pub fn is_descriptor_resolved(&self) -> bool {
452        self.descriptor_index.is_some() && self.descriptor_variable_index.is_some()
453    }
454
455    pub fn descriptor_index(&self) -> usize {
456        self.descriptor_index.unwrap_or_else(|| {
457            panic!(
458                "dynamic scalar variable {}.{} has not been resolved against a SolutionDescriptor",
459                self.entity_type_name, self.variable_name
460            )
461        })
462    }
463
464    pub fn descriptor_variable_index(&self) -> usize {
465        self.descriptor_variable_index.unwrap_or_else(|| {
466            panic!(
467                "dynamic scalar variable {}.{} has not been resolved against a SolutionDescriptor",
468                self.entity_type_name, self.variable_name
469            )
470        })
471    }
472
473    pub fn matches_target(&self, entity_class: Option<&str>, variable_name: Option<&str>) -> bool {
474        entity_class.is_none_or(|entity| entity == self.entity_type_name)
475            && variable_name.is_none_or(|variable| variable == self.variable_name)
476    }
477
478    pub fn entity_count(&self, solution: &S) -> usize {
479        self.access.entity_count(solution)
480    }
481
482    pub fn current_value(&self, solution: &S, row: usize) -> Option<usize> {
483        self.access.get(solution, row)
484    }
485
486    pub fn set_value(&self, solution: &mut S, row: usize, value: Option<usize>) {
487        self.access.set(solution, row, value);
488    }
489
490    pub fn candidate_values<'a>(&self, solution: &'a S, row: usize) -> &'a [usize] {
491        self.access.candidate_values(solution, row)
492    }
493
494    pub fn has_nearby_value_candidates(&self) -> bool {
495        self.access.has_nearby_value_candidates()
496    }
497
498    pub fn visit_nearby_value_candidates(
499        &self,
500        solution: &S,
501        row: usize,
502        limit: usize,
503        visit: &mut dyn FnMut(usize),
504    ) -> bool {
505        self.access
506            .visit_nearby_value_candidates(solution, row, limit, visit)
507    }
508
509    pub fn nearby_value_distance(&self, solution: &S, row: usize, candidate: usize) -> Option<f64> {
510        self.access.nearby_value_distance(solution, row, candidate)
511    }
512
513    pub fn has_nearby_entity_candidates(&self) -> bool {
514        self.access.has_nearby_entity_candidates()
515    }
516
517    pub fn visit_nearby_entity_candidates(
518        &self,
519        solution: &S,
520        left_row: usize,
521        limit: usize,
522        visit: &mut dyn FnMut(usize),
523    ) -> bool {
524        self.access
525            .visit_nearby_entity_candidates(solution, left_row, limit, visit)
526    }
527
528    pub fn nearby_entity_distance(
529        &self,
530        solution: &S,
531        left_row: usize,
532        right_row: usize,
533    ) -> Option<f64> {
534        self.access
535            .nearby_entity_distance(solution, left_row, right_row)
536    }
537
538    pub fn value_is_legal(&self, solution: &S, row: usize, value: Option<usize>) -> bool {
539        let Some(value) = value else {
540            return self.allows_unassigned;
541        };
542        self.access.value_is_legal(solution, row, value)
543    }
544}
545
546impl<S> DynamicScalarVariableSlot<S>
547where
548    S: DynamicModelBackend,
549{
550    pub fn new(
551        entity: EntityClassId,
552        variable: VariableId,
553        entity_type_name: &'static str,
554        variable_name: &'static str,
555        allows_unassigned: bool,
556    ) -> Self {
557        Self::with_access(
558            entity,
559            variable,
560            entity_type_name,
561            variable_name,
562            allows_unassigned,
563            Arc::new(BackendScalarAccess { entity, variable }),
564        )
565    }
566}
567
568/// Public dynamic list variable slot.
569pub struct DynamicListVariableSlot<S> {
570    pub entity: EntityClassId,
571    pub variable: VariableId,
572    pub entity_type_name: &'static str,
573    pub variable_name: &'static str,
574    descriptor_index: Option<usize>,
575    descriptor_variable_index: Option<usize>,
576    access: Arc<dyn DynamicListAccess<S>>,
577    metadata: Option<Arc<dyn DynamicListMetadata<S>>>,
578}
579
580impl<S> Clone for DynamicListVariableSlot<S> {
581    fn clone(&self) -> Self {
582        Self {
583            entity: self.entity,
584            variable: self.variable,
585            entity_type_name: self.entity_type_name,
586            variable_name: self.variable_name,
587            descriptor_index: self.descriptor_index,
588            descriptor_variable_index: self.descriptor_variable_index,
589            access: Arc::clone(&self.access),
590            metadata: self.metadata.as_ref().map(Arc::clone),
591        }
592    }
593}
594
595impl<S> fmt::Debug for DynamicListVariableSlot<S> {
596    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
597        f.debug_struct("DynamicListVariableSlot")
598            .field("entity", &self.entity)
599            .field("variable", &self.variable)
600            .field("entity_type_name", &self.entity_type_name)
601            .field("variable_name", &self.variable_name)
602            .field("descriptor_index", &self.descriptor_index)
603            .field("descriptor_variable_index", &self.descriptor_variable_index)
604            .field(
605                "metadata_capabilities",
606                &self
607                    .metadata
608                    .as_ref()
609                    .map(|metadata| metadata.capabilities()),
610            )
611            .finish()
612    }
613}
614
615impl<S> PartialEq for DynamicListVariableSlot<S> {
616    fn eq(&self, other: &Self) -> bool {
617        self.entity == other.entity
618            && self.variable == other.variable
619            && self.entity_type_name == other.entity_type_name
620            && self.variable_name == other.variable_name
621            && self.descriptor_index == other.descriptor_index
622            && self.descriptor_variable_index == other.descriptor_variable_index
623    }
624}
625
626impl<S> Eq for DynamicListVariableSlot<S> {}
627
628impl<S> DynamicListVariableSlot<S> {
629    /// Fallible access constructor for integrations that compile a slot from
630    /// runtime schema. This verifies that the object supplying mutations is
631    /// bound to exactly the same logical entity and variable as the slot.
632    ///
633    /// Integrations requiring immutable metadata can use
634    /// [`Self::with_access_and_metadata`] to validate both bindings together.
635    pub fn try_with_access(
636        entity: EntityClassId,
637        variable: VariableId,
638        entity_type_name: &'static str,
639        variable_name: &'static str,
640        access: Arc<dyn DynamicListAccess<S>>,
641    ) -> Result<Self, String> {
642        if access.entity_class() != entity || access.variable() != variable {
643            return Err(format!(
644                "dynamic list access is bound to entity ID {} variable ID {}, but slot {}.{} is entity ID {} variable ID {}",
645                access.entity_class().0,
646                access.variable().0,
647                entity_type_name,
648                variable_name,
649                entity.0,
650                variable.0,
651            ));
652        }
653        Ok(Self {
654            entity,
655            variable,
656            entity_type_name,
657            variable_name,
658            descriptor_index: None,
659            descriptor_variable_index: None,
660            access,
661            metadata: None,
662        })
663    }
664
665    /// Constructs a slot with its immutable, structurally bound metadata.
666    ///
667    /// Metadata identity is checked immediately; a binding implementation
668    /// cannot accidentally be reused for a same-named variable in another
669    /// entity class. Phase assembly remains responsible for rejecting a
670    /// selected family whose specific capability bit is absent.
671    pub fn with_access_and_metadata(
672        entity: EntityClassId,
673        variable: VariableId,
674        entity_type_name: &'static str,
675        variable_name: &'static str,
676        access: Arc<dyn DynamicListAccess<S>>,
677        metadata: Arc<dyn DynamicListMetadata<S>>,
678    ) -> Result<Self, String> {
679        Self::try_with_access(entity, variable, entity_type_name, variable_name, access)?
680            .with_metadata(metadata)
681    }
682
683    /// Attaches immutable metadata after access construction while preserving
684    /// the same structural identity validation as
685    /// [`Self::with_access_and_metadata`].
686    pub fn with_metadata(
687        mut self,
688        metadata: Arc<dyn DynamicListMetadata<S>>,
689    ) -> Result<Self, String> {
690        if metadata.entity_class() != self.entity || metadata.variable() != self.variable {
691            return Err(format!(
692                "dynamic list metadata is bound to entity ID {} variable ID {}, but slot {}.{} is entity ID {} variable ID {}",
693                metadata.entity_class().0,
694                metadata.variable().0,
695                self.entity_type_name,
696                self.variable_name,
697                self.entity.0,
698                self.variable.0,
699            ));
700        }
701        self.metadata = Some(metadata);
702        Ok(self)
703    }
704
705    pub fn resolve_descriptor_index(
706        &mut self,
707        descriptor: &SolutionDescriptor,
708    ) -> Result<(), String> {
709        let indexes = resolve_dynamic_descriptor_indexes(
710            descriptor,
711            self.entity,
712            self.variable,
713            self.entity_type_name,
714            self.variable_name,
715            DynamicVariableKind::List,
716        )?;
717        if let Some(existing) = self.descriptor_index {
718            if existing != indexes.descriptor_index {
719                return Err(format!(
720                    "dynamic list variable {}.{} was pre-bound to descriptor index {existing}, but logical entity ID {} resolves to descriptor index {}",
721                    self.entity_type_name,
722                    self.variable_name,
723                    self.entity.0,
724                    indexes.descriptor_index
725                ));
726            }
727        }
728        self.descriptor_index = Some(indexes.descriptor_index);
729        self.descriptor_variable_index = Some(indexes.variable_index);
730        Ok(())
731    }
732
733    pub fn resolved_against(mut self, descriptor: &SolutionDescriptor) -> Result<Self, String> {
734        self.resolve_descriptor_index(descriptor)?;
735        Ok(self)
736    }
737
738    pub fn is_descriptor_resolved(&self) -> bool {
739        self.descriptor_index.is_some() && self.descriptor_variable_index.is_some()
740    }
741
742    pub fn descriptor_index(&self) -> usize {
743        self.descriptor_index.unwrap_or_else(|| {
744            panic!(
745                "dynamic list variable {}.{} has not been resolved against a SolutionDescriptor",
746                self.entity_type_name, self.variable_name
747            )
748        })
749    }
750
751    pub fn descriptor_variable_index(&self) -> usize {
752        self.descriptor_variable_index.unwrap_or_else(|| {
753            panic!(
754                "dynamic list variable {}.{} has not been resolved against a SolutionDescriptor",
755                self.entity_type_name, self.variable_name
756            )
757        })
758    }
759
760    pub fn matches_target(&self, entity_class: Option<&str>, variable_name: Option<&str>) -> bool {
761        entity_class.is_none_or(|entity| entity == self.entity_type_name)
762            && variable_name.is_none_or(|variable| variable == self.variable_name)
763    }
764
765    pub fn entity_count(&self, solution: &S) -> usize {
766        self.access.entity_count(solution)
767    }
768
769    pub fn element_count(&self, solution: &S) -> usize {
770        self.access.element_count(solution)
771    }
772
773    pub fn element(&self, solution: &S, element_index: usize) -> Option<usize> {
774        self.access.element(solution, element_index)
775    }
776
777    pub fn assigned_elements(&self, solution: &S) -> Vec<usize> {
778        self.access.assigned_elements(solution)
779    }
780
781    pub fn list_len(&self, solution: &S, row: usize) -> usize {
782        self.access.len(solution, row)
783    }
784
785    pub fn list_get(&self, solution: &S, row: usize, pos: usize) -> Option<usize> {
786        self.access.get(solution, row, pos)
787    }
788
789    pub fn list_insert(&self, solution: &mut S, row: usize, pos: usize, value: usize) {
790        self.access.insert(solution, row, pos, value);
791    }
792
793    pub fn list_remove(&self, solution: &mut S, row: usize, pos: usize) -> Option<usize> {
794        self.access.remove(solution, row, pos)
795    }
796
797    pub fn access_capabilities(&self) -> DynamicListAccessCapabilities {
798        self.access.capabilities()
799    }
800
801    pub fn list_set(&self, solution: &mut S, row: usize, pos: usize, value: usize) -> bool {
802        self.access.set(solution, row, pos, value)
803    }
804
805    /// Replaces a complete list row as one logical mutation. Dynamic kernels
806    /// use this for route replacement rather than composing remove/insert and
807    /// accidentally publishing multiple dirty/revision transitions.
808    pub fn list_replace(&self, solution: &mut S, row: usize, values: Vec<usize>) -> bool {
809        self.access.replace(solution, row, values)
810    }
811
812    pub fn list_reverse(&self, solution: &mut S, row: usize, start: usize, end: usize) -> bool {
813        self.access.reverse(solution, row, start, end)
814    }
815
816    pub fn sublist_remove(
817        &self,
818        solution: &mut S,
819        row: usize,
820        start: usize,
821        end: usize,
822    ) -> Option<Vec<usize>> {
823        self.access.sublist_remove(solution, row, start, end)
824    }
825
826    pub fn sublist_insert(
827        &self,
828        solution: &mut S,
829        row: usize,
830        pos: usize,
831        values: Vec<usize>,
832    ) -> bool {
833        self.access.sublist_insert(solution, row, pos, values)
834    }
835
836    /// Returns slot-bound metadata when the integration compiled it.
837    ///
838    /// Absence is intentional and must cause a selected metadata-dependent
839    /// phase to fail validation; it is never a request to synthesize defaults.
840    pub fn metadata(&self) -> Option<&dyn DynamicListMetadata<S>> {
841        self.metadata.as_deref()
842    }
843
844    pub fn metadata_capabilities(&self) -> Option<DynamicListMetadataCapabilities> {
845        self.metadata
846            .as_ref()
847            .map(|metadata| metadata.capabilities())
848    }
849
850    pub fn require_metadata(&self) -> Result<&dyn DynamicListMetadata<S>, String> {
851        self.metadata().ok_or_else(|| {
852            format!(
853                "dynamic list variable {}.{} has no immutable list metadata; selected list phases must declare and bind their required capability bundle before solve",
854                self.entity_type_name, self.variable_name,
855            )
856        })
857    }
858}
859
860impl<S> DynamicListVariableSlot<S>
861where
862    S: DynamicModelBackend,
863{
864    pub fn new(
865        entity: EntityClassId,
866        variable: VariableId,
867        entity_type_name: &'static str,
868        variable_name: &'static str,
869    ) -> Self {
870        Self {
871            entity,
872            variable,
873            entity_type_name,
874            variable_name,
875            descriptor_index: None,
876            descriptor_variable_index: None,
877            access: Arc::new(BackendListAccess { entity, variable }),
878            metadata: None,
879        }
880    }
881
882    /// Backend-access convenience constructor with immutable metadata.
883    pub fn new_with_metadata(
884        entity: EntityClassId,
885        variable: VariableId,
886        entity_type_name: &'static str,
887        variable_name: &'static str,
888        metadata: Arc<dyn DynamicListMetadata<S>>,
889    ) -> Result<Self, String> {
890        Self::with_access_and_metadata(
891            entity,
892            variable,
893            entity_type_name,
894            variable_name,
895            Arc::new(BackendListAccess { entity, variable }),
896            metadata,
897        )
898    }
899}