Skip to main content

tenferro_runtime/runtime/
policy.rs

1use std::sync::Arc;
2
3use super::identity::validate_identifier;
4use super::{EngineId, HardwareClassId, IdentityError, IdentityKind, PlacementConstraintError};
5
6/// Selects the planning and execution reproducibility policy.
7///
8/// # Examples
9///
10/// ```
11/// use tenferro_runtime::Determinism;
12///
13/// assert_eq!(Determinism::Fast, Determinism::Fast);
14/// ```
15#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
16pub enum Determinism {
17    /// Prefer fast backend choices when multiple valid plans exist.
18    Fast,
19    /// Prefer reproducible backend choices when multiple valid plans exist.
20    Reproducible,
21}
22
23/// Validated namespaced storage-class identity.
24///
25/// # Examples
26///
27/// ```
28/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
29/// use tenferro_runtime::StorageClass;
30///
31/// assert_eq!(
32///     StorageClass::new("tenferro.storage.host")?.as_str(),
33///     "tenferro.storage.host"
34/// );
35/// # Ok(())
36/// # }
37/// ```
38#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
39pub struct StorageClass(Arc<str>);
40
41impl StorageClass {
42    /// Validate a lowercase ASCII namespaced storage-class identifier.
43    ///
44    /// # Examples
45    ///
46    /// ```
47    /// use tenferro_runtime::StorageClass;
48    ///
49    /// assert!(StorageClass::new("tenferro.storage.host").is_ok());
50    /// ```
51    ///
52    /// # Errors
53    ///
54    /// Returns [`IdentityError`] with [`IdentityKind::StorageClass`] when
55    /// `value` does not match the lowercase ASCII namespaced identifier grammar.
56    pub fn new(value: impl Into<Arc<str>>) -> Result<Self, IdentityError> {
57        validate_identifier(value.into(), IdentityKind::StorageClass).map(Self)
58    }
59
60    /// Borrow the validated identifier text.
61    ///
62    /// # Examples
63    ///
64    /// ```
65    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
66    /// use tenferro_runtime::StorageClass;
67    ///
68    /// assert_eq!(StorageClass::new("tenferro.storage.host")?.as_str(), "tenferro.storage.host");
69    /// # Ok(())
70    /// # }
71    /// ```
72    pub fn as_str(&self) -> &str {
73        &self.0
74    }
75
76    pub(crate) fn runtime_created(value: impl Into<Arc<str>>) -> Self {
77        Self(value.into())
78    }
79}
80
81/// Immutable execution endpoint used to identify a transfer route.
82///
83/// An endpoint combines the logical runtime engine with the storage class that
84/// the engine exposes. Equality, ordering, and hashing are based on both
85/// values, so two engines that expose the same storage class remain distinct
86/// route endpoints.
87///
88/// # Examples
89///
90/// ```
91/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
92/// use tenferro_runtime::{EngineId, StorageClass, TransferEndpoint};
93///
94/// let endpoint = TransferEndpoint::new(
95///     EngineId::new("tenferro.cpu")?,
96///     StorageClass::new("tenferro.storage.host")?,
97/// );
98/// assert_eq!(endpoint.engine_id().as_str(), "tenferro.cpu");
99/// assert_eq!(endpoint.storage_class().as_str(), "tenferro.storage.host");
100/// # Ok(())
101/// # }
102/// ```
103#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
104pub struct TransferEndpoint {
105    engine_id: EngineId,
106    storage_class: StorageClass,
107}
108
109impl TransferEndpoint {
110    /// Construct an endpoint from an engine and one of its storage classes.
111    ///
112    /// The runtime validates that the engine is registered and supports the
113    /// storage class when a transfer-provider candidate is frozen.
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
119    /// use tenferro_runtime::{EngineId, StorageClass, TransferEndpoint};
120    ///
121    /// let endpoint = TransferEndpoint::new(
122    ///     EngineId::new("tenferro.cpu")?,
123    ///     StorageClass::new("tenferro.storage.host")?,
124    /// );
125    /// assert_eq!(endpoint.engine_id().as_str(), "tenferro.cpu");
126    /// # Ok(())
127    /// # }
128    /// ```
129    pub fn new(engine_id: EngineId, storage_class: StorageClass) -> Self {
130        Self {
131            engine_id,
132            storage_class,
133        }
134    }
135
136    /// Return the logical engine that owns this endpoint.
137    ///
138    /// # Examples
139    ///
140    /// ```
141    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
142    /// use tenferro_runtime::{EngineId, StorageClass, TransferEndpoint};
143    ///
144    /// let engine = EngineId::new("tenferro.cpu")?;
145    /// let endpoint = TransferEndpoint::new(
146    ///     engine.clone(),
147    ///     StorageClass::new("tenferro.storage.host")?,
148    /// );
149    /// assert_eq!(endpoint.engine_id(), &engine);
150    /// # Ok(())
151    /// # }
152    /// ```
153    pub fn engine_id(&self) -> &EngineId {
154        &self.engine_id
155    }
156
157    /// Return the storage class exposed at this endpoint.
158    ///
159    /// # Examples
160    ///
161    /// ```
162    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
163    /// use tenferro_runtime::{EngineId, StorageClass, TransferEndpoint};
164    ///
165    /// let storage = StorageClass::new("tenferro.storage.host")?;
166    /// let endpoint = TransferEndpoint::new(EngineId::new("tenferro.cpu")?, storage.clone());
167    /// assert_eq!(endpoint.storage_class(), &storage);
168    /// # Ok(())
169    /// # }
170    /// ```
171    pub fn storage_class(&self) -> &StorageClass {
172        &self.storage_class
173    }
174}
175
176/// Validated namespaced layout-class identity.
177///
178/// # Examples
179///
180/// ```
181/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
182/// use tenferro_runtime::LayoutClass;
183///
184/// assert_eq!(
185///     LayoutClass::new("tenferro.layout.col-major")?.as_str(),
186///     "tenferro.layout.col-major"
187/// );
188/// # Ok(())
189/// # }
190/// ```
191#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
192pub struct LayoutClass(Arc<str>);
193
194impl LayoutClass {
195    /// Validate a lowercase ASCII namespaced layout-class identifier.
196    ///
197    /// # Examples
198    ///
199    /// ```
200    /// use tenferro_runtime::LayoutClass;
201    ///
202    /// assert!(LayoutClass::new("tenferro.layout.col-major").is_ok());
203    /// ```
204    ///
205    /// # Errors
206    ///
207    /// Returns [`IdentityError`] with [`IdentityKind::LayoutClass`] when
208    /// `value` does not match the lowercase ASCII namespaced identifier grammar.
209    pub fn new(value: impl Into<Arc<str>>) -> Result<Self, IdentityError> {
210        validate_identifier(value.into(), IdentityKind::LayoutClass).map(Self)
211    }
212
213    /// Borrow the validated identifier text.
214    ///
215    /// # Examples
216    ///
217    /// ```
218    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
219    /// use tenferro_runtime::LayoutClass;
220    ///
221    /// assert_eq!(LayoutClass::new("tenferro.layout.col-major")?.as_str(), "tenferro.layout.col-major");
222    /// # Ok(())
223    /// # }
224    /// ```
225    pub fn as_str(&self) -> &str {
226        &self.0
227    }
228
229    pub(crate) fn runtime_created(value: impl Into<Arc<str>>) -> Self {
230        Self(value.into())
231    }
232}
233
234/// Requested program placement constraints.
235///
236/// An empty engine list means any engine is allowed.
237///
238/// # Examples
239///
240/// ```
241/// use tenferro_runtime::ProgramPlacementConstraint;
242///
243/// assert!(ProgramPlacementConstraint::any().allowed_engines().is_empty());
244/// ```
245#[derive(Clone, Debug, Eq, Hash, PartialEq)]
246pub struct ProgramPlacementConstraint {
247    allowed_engines: Arc<[EngineId]>,
248    storage_class: Option<StorageClass>,
249}
250
251impl ProgramPlacementConstraint {
252    /// Return an unconstrained placement request.
253    ///
254    /// # Examples
255    ///
256    /// ```
257    /// use tenferro_runtime::ProgramPlacementConstraint;
258    ///
259    /// let any = ProgramPlacementConstraint::any();
260    /// assert!(any.allowed_engines().is_empty());
261    /// assert!(any.storage_class().is_none());
262    /// ```
263    pub fn any() -> Self {
264        Self {
265            allowed_engines: Arc::from(Vec::<EngineId>::new()),
266            storage_class: None,
267        }
268    }
269
270    /// Build a placement constraint from engine preferences and storage class.
271    ///
272    /// # Examples
273    ///
274    /// ```
275    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
276    /// use tenferro_runtime::{EngineId, ProgramPlacementConstraint, StorageClass};
277    ///
278    /// let engine = EngineId::new("tenferro.cpu")?;
279    /// let storage = StorageClass::new("tenferro.storage.host")?;
280    /// let constraint = ProgramPlacementConstraint::new(vec![engine.clone()], Some(storage.clone()))?;
281    /// assert_eq!(constraint.allowed_engines(), &[engine]);
282    /// assert_eq!(constraint.storage_class(), Some(&storage));
283    /// # Ok(())
284    /// # }
285    /// ```
286    ///
287    /// # Errors
288    ///
289    /// Returns [`PlacementConstraintError::DuplicateEngine`] when the same
290    /// engine appears more than once.
291    pub fn new(
292        allowed_engines: impl Into<Arc<[EngineId]>>,
293        storage_class: Option<StorageClass>,
294    ) -> Result<Self, PlacementConstraintError> {
295        let allowed_engines = allowed_engines.into();
296        for duplicate_index in 0..allowed_engines.len() {
297            if let Some(first_index) = (0..duplicate_index).find(|&first_index| {
298                allowed_engines[first_index] == allowed_engines[duplicate_index]
299            }) {
300                return Err(PlacementConstraintError::DuplicateEngine {
301                    engine_id: allowed_engines[duplicate_index].clone(),
302                    first_index,
303                    duplicate_index,
304                });
305            }
306        }
307        Ok(Self {
308            allowed_engines,
309            storage_class,
310        })
311    }
312
313    /// Return the allowed engines in preference order.
314    ///
315    /// # Examples
316    ///
317    /// ```
318    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
319    /// use tenferro_runtime::{EngineId, ProgramPlacementConstraint};
320    ///
321    /// let engine = EngineId::new("tenferro.cpu")?;
322    /// let constraint = ProgramPlacementConstraint::new(vec![engine.clone()], None)?;
323    /// assert_eq!(constraint.allowed_engines(), &[engine]);
324    /// # Ok(())
325    /// # }
326    /// ```
327    pub fn allowed_engines(&self) -> &[EngineId] {
328        &self.allowed_engines
329    }
330
331    /// Return the requested storage class, if one was supplied.
332    ///
333    /// # Examples
334    ///
335    /// ```
336    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
337    /// use tenferro_runtime::{ProgramPlacementConstraint, StorageClass};
338    ///
339    /// let storage = StorageClass::new("tenferro.storage.host")?;
340    /// let constraint = ProgramPlacementConstraint::new(Vec::new(), Some(storage.clone()))?;
341    /// assert_eq!(constraint.storage_class(), Some(&storage));
342    /// # Ok(())
343    /// # }
344    /// ```
345    pub fn storage_class(&self) -> Option<&StorageClass> {
346        self.storage_class.as_ref()
347    }
348}
349
350/// Runtime-resolved program placement.
351///
352/// A0 exposes no public constructor for this runtime-created value; B0 supplies
353/// its runtime creation path.
354///
355/// # Examples
356///
357/// ```
358/// use std::fmt::Debug;
359/// use tenferro_runtime::ResolvedProgramPlacement;
360///
361/// fn requires_debug<T: Debug>() {}
362/// requires_debug::<ResolvedProgramPlacement>();
363/// ```
364#[derive(Clone, Debug, Eq, Hash, PartialEq)]
365pub struct ResolvedProgramPlacement {
366    engine_id: EngineId,
367    storage_class: StorageClass,
368}
369
370impl ResolvedProgramPlacement {
371    // INVARIANT: A0 module-local tests create resolved placements until B0 owns runtime resolution.
372    #[allow(dead_code)]
373    pub(crate) fn new(engine_id: EngineId, storage_class: StorageClass) -> Self {
374        Self {
375            engine_id,
376            storage_class,
377        }
378    }
379
380    /// Return the resolved execution engine.
381    ///
382    /// # Examples
383    ///
384    /// ```
385    /// use std::fmt::Debug;
386    /// use tenferro_runtime::ResolvedProgramPlacement;
387    ///
388    /// fn requires_debug<T: Debug>() {}
389    /// requires_debug::<ResolvedProgramPlacement>();
390    /// ```
391    pub fn engine_id(&self) -> &EngineId {
392        &self.engine_id
393    }
394
395    /// Return the resolved storage class.
396    ///
397    /// # Examples
398    ///
399    /// ```
400    /// use std::fmt::Debug;
401    /// use tenferro_runtime::ResolvedProgramPlacement;
402    ///
403    /// fn requires_debug<T: Debug>() {}
404    /// requires_debug::<ResolvedProgramPlacement>();
405    /// ```
406    pub fn storage_class(&self) -> &StorageClass {
407        &self.storage_class
408    }
409}
410
411/// Controls how a caller reacts to a preparation already in flight.
412///
413/// The default is [`CacheInFlightBehavior::Wait`].
414///
415/// # Examples
416///
417/// ```
418/// use tenferro_runtime::CacheInFlightBehavior;
419///
420/// assert_eq!(CacheInFlightBehavior::default(), CacheInFlightBehavior::Wait);
421/// ```
422#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
423pub enum CacheInFlightBehavior {
424    /// Wait for the in-flight preparation.
425    #[default]
426    Wait,
427    /// Refuse to join the in-flight preparation.
428    Refuse,
429}
430
431/// Process-wide execution planning policy.
432///
433/// # Examples
434///
435/// ```
436/// use tenferro_runtime::{Determinism, ExecutionPolicy};
437///
438/// let policy = ExecutionPolicy::new(Determinism::Fast, Some(1024), 7);
439/// assert_eq!(policy.hard_workspace_limit_bytes(), Some(1024));
440/// ```
441#[derive(Clone, Debug, Eq, Hash, PartialEq)]
442pub struct ExecutionPolicy {
443    determinism: Determinism,
444    hard_workspace_limit_bytes: Option<usize>,
445    planning_seed: u64,
446}
447
448impl ExecutionPolicy {
449    /// Build an execution policy.
450    ///
451    /// # Examples
452    ///
453    /// ```
454    /// use tenferro_runtime::{Determinism, ExecutionPolicy};
455    ///
456    /// let policy = ExecutionPolicy::new(Determinism::Reproducible, None, 0);
457    /// assert_eq!(policy.determinism(), Determinism::Reproducible);
458    /// ```
459    pub fn new(
460        determinism: Determinism,
461        hard_workspace_limit_bytes: Option<usize>,
462        planning_seed: u64,
463    ) -> Self {
464        Self {
465            determinism,
466            hard_workspace_limit_bytes,
467            planning_seed,
468        }
469    }
470
471    /// Return the determinism mode.
472    ///
473    /// # Examples
474    ///
475    /// ```
476    /// use tenferro_runtime::{Determinism, ExecutionPolicy};
477    ///
478    /// assert_eq!(
479    ///     ExecutionPolicy::new(Determinism::Fast, None, 0).determinism(),
480    ///     Determinism::Fast
481    /// );
482    /// ```
483    pub fn determinism(&self) -> Determinism {
484        self.determinism
485    }
486
487    /// Return the hard workspace limit in bytes, if one is configured.
488    ///
489    /// # Examples
490    ///
491    /// ```
492    /// use tenferro_runtime::{Determinism, ExecutionPolicy};
493    ///
494    /// assert_eq!(
495    ///     ExecutionPolicy::new(Determinism::Fast, Some(8), 0).hard_workspace_limit_bytes(),
496    ///     Some(8)
497    /// );
498    /// ```
499    pub fn hard_workspace_limit_bytes(&self) -> Option<usize> {
500        self.hard_workspace_limit_bytes
501    }
502
503    /// Return the planning seed.
504    ///
505    /// # Examples
506    ///
507    /// ```
508    /// use tenferro_runtime::{Determinism, ExecutionPolicy};
509    ///
510    /// assert_eq!(ExecutionPolicy::new(Determinism::Fast, None, 42).planning_seed(), 42);
511    /// ```
512    pub fn planning_seed(&self) -> u64 {
513        self.planning_seed
514    }
515}
516
517/// Per-prepare request options before runtime normalization.
518///
519/// Raw options intentionally do not implement `Hash`; normalized keys are
520/// created only after placement and planning resolution.
521///
522/// # Examples
523///
524/// ```
525/// use tenferro_runtime::PrepareOptions;
526///
527/// assert!(PrepareOptions::new().planning_seed().is_none());
528/// ```
529#[derive(Clone, Debug, Default, Eq, PartialEq)]
530pub struct PrepareOptions {
531    placement: Option<ProgramPlacementConstraint>,
532    hard_workspace_limit_bytes: Option<usize>,
533    planning_seed: Option<u64>,
534    cache_in_flight: CacheInFlightBehavior,
535}
536
537impl PrepareOptions {
538    /// Return default prepare options.
539    ///
540    /// # Examples
541    ///
542    /// ```
543    /// use tenferro_runtime::{CacheInFlightBehavior, PrepareOptions};
544    ///
545    /// let options = PrepareOptions::new();
546    /// assert_eq!(options.cache_in_flight(), CacheInFlightBehavior::Wait);
547    /// ```
548    pub fn new() -> Self {
549        Self::default()
550    }
551
552    /// Set the placement constraint.
553    ///
554    /// # Examples
555    ///
556    /// ```
557    /// use tenferro_runtime::{PrepareOptions, ProgramPlacementConstraint};
558    ///
559    /// let placement = ProgramPlacementConstraint::any();
560    /// let options = PrepareOptions::new().with_placement(placement.clone());
561    /// assert_eq!(options.placement(), Some(&placement));
562    /// ```
563    pub fn with_placement(mut self, placement: ProgramPlacementConstraint) -> Self {
564        self.placement = Some(placement);
565        self
566    }
567
568    /// Set or reset the per-call workspace override.
569    ///
570    /// Passing `None` resets the field to inherit the policy value.
571    ///
572    /// # Examples
573    ///
574    /// ```
575    /// use tenferro_runtime::PrepareOptions;
576    ///
577    /// let options = PrepareOptions::new().with_hard_workspace_limit_bytes(Some(0));
578    /// assert_eq!(options.hard_workspace_limit_bytes(), Some(0));
579    /// assert_eq!(options.with_hard_workspace_limit_bytes(None).hard_workspace_limit_bytes(), None);
580    /// ```
581    pub fn with_hard_workspace_limit_bytes(mut self, limit: Option<usize>) -> Self {
582        self.hard_workspace_limit_bytes = limit;
583        self
584    }
585
586    /// Set the per-call planning seed override.
587    ///
588    /// # Examples
589    ///
590    /// ```
591    /// use tenferro_runtime::PrepareOptions;
592    ///
593    /// assert_eq!(PrepareOptions::new().with_planning_seed(3).planning_seed(), Some(3));
594    /// ```
595    pub fn with_planning_seed(mut self, seed: u64) -> Self {
596        self.planning_seed = Some(seed);
597        self
598    }
599
600    /// Set the in-flight cache behavior for this request.
601    ///
602    /// # Examples
603    ///
604    /// ```
605    /// use tenferro_runtime::{CacheInFlightBehavior, PrepareOptions};
606    ///
607    /// let options = PrepareOptions::new().with_cache_in_flight(CacheInFlightBehavior::Refuse);
608    /// assert_eq!(options.cache_in_flight(), CacheInFlightBehavior::Refuse);
609    /// ```
610    pub fn with_cache_in_flight(mut self, behavior: CacheInFlightBehavior) -> Self {
611        self.cache_in_flight = behavior;
612        self
613    }
614
615    /// Return the raw placement constraint, if supplied.
616    ///
617    /// # Examples
618    ///
619    /// ```
620    /// use tenferro_runtime::{PrepareOptions, ProgramPlacementConstraint};
621    ///
622    /// let placement = ProgramPlacementConstraint::any();
623    /// let options = PrepareOptions::new().with_placement(placement.clone());
624    /// assert_eq!(options.placement(), Some(&placement));
625    /// ```
626    pub fn placement(&self) -> Option<&ProgramPlacementConstraint> {
627        self.placement.as_ref()
628    }
629
630    /// Return the raw per-call workspace override.
631    ///
632    /// # Examples
633    ///
634    /// ```
635    /// use tenferro_runtime::PrepareOptions;
636    ///
637    /// assert_eq!(
638    ///     PrepareOptions::new().with_hard_workspace_limit_bytes(Some(16)).hard_workspace_limit_bytes(),
639    ///     Some(16)
640    /// );
641    /// ```
642    pub fn hard_workspace_limit_bytes(&self) -> Option<usize> {
643        self.hard_workspace_limit_bytes
644    }
645
646    /// Return the raw per-call planning seed override.
647    ///
648    /// # Examples
649    ///
650    /// ```
651    /// use tenferro_runtime::PrepareOptions;
652    ///
653    /// assert_eq!(PrepareOptions::new().with_planning_seed(9).planning_seed(), Some(9));
654    /// ```
655    pub fn planning_seed(&self) -> Option<u64> {
656        self.planning_seed
657    }
658
659    /// Return the request's in-flight cache behavior.
660    ///
661    /// # Examples
662    ///
663    /// ```
664    /// use tenferro_runtime::{CacheInFlightBehavior, PrepareOptions};
665    ///
666    /// assert_eq!(PrepareOptions::new().cache_in_flight(), CacheInFlightBehavior::Wait);
667    /// ```
668    pub fn cache_in_flight(&self) -> CacheInFlightBehavior {
669        self.cache_in_flight
670    }
671}
672
673/// Normalized prepare-options cache key.
674///
675/// A0 exposes no public constructor for this runtime-created value; B0 supplies
676/// its runtime creation path.
677///
678/// # Examples
679///
680/// ```
681/// use std::fmt::Debug;
682/// use tenferro_runtime::PrepareOptionsKey;
683///
684/// fn requires_debug<T: Debug>() {}
685/// requires_debug::<PrepareOptionsKey>();
686/// ```
687#[derive(Clone, Debug, Eq, Hash, PartialEq)]
688pub struct PrepareOptionsKey {
689    resolved_placement: ResolvedProgramPlacement,
690    hard_workspace_limit_bytes: Option<usize>,
691    planning_seed: u64,
692}
693
694impl PrepareOptionsKey {
695    // INVARIANT: A0 module-local tests create normalized keys until B0 owns prepare-key resolution.
696    #[allow(dead_code)]
697    pub(crate) fn from_resolved(
698        resolved_placement: ResolvedProgramPlacement,
699        hard_workspace_limit_bytes: Option<usize>,
700        planning_seed: u64,
701    ) -> Self {
702        Self {
703            resolved_placement,
704            hard_workspace_limit_bytes,
705            planning_seed,
706        }
707    }
708
709    /// Return the resolved placement carried by this key.
710    ///
711    /// # Examples
712    ///
713    /// ```
714    /// use std::fmt::Debug;
715    /// use tenferro_runtime::PrepareOptionsKey;
716    ///
717    /// fn requires_debug<T: Debug>() {}
718    /// requires_debug::<PrepareOptionsKey>();
719    /// ```
720    pub fn resolved_placement(&self) -> &ResolvedProgramPlacement {
721        &self.resolved_placement
722    }
723
724    /// Return the resolved hard workspace limit.
725    ///
726    /// # Examples
727    ///
728    /// ```
729    /// use std::fmt::Debug;
730    /// use tenferro_runtime::PrepareOptionsKey;
731    ///
732    /// fn requires_debug<T: Debug>() {}
733    /// requires_debug::<PrepareOptionsKey>();
734    /// ```
735    pub fn hard_workspace_limit_bytes(&self) -> Option<usize> {
736        self.hard_workspace_limit_bytes
737    }
738
739    /// Return the resolved planning seed.
740    ///
741    /// # Examples
742    ///
743    /// ```
744    /// use std::fmt::Debug;
745    /// use tenferro_runtime::PrepareOptionsKey;
746    ///
747    /// fn requires_debug<T: Debug>() {}
748    /// requires_debug::<PrepareOptionsKey>();
749    /// ```
750    pub fn planning_seed(&self) -> u64 {
751        self.planning_seed
752    }
753}
754
755/// Fully resolved planning configuration.
756///
757/// # Examples
758///
759/// ```
760/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
761/// use tenferro_runtime::{
762///     Determinism, ExecutionPolicy, HardwareClassId, PrepareOptions, ResolvedPlanningConfig,
763/// };
764///
765/// let policy = ExecutionPolicy::new(Determinism::Fast, Some(32), 5);
766/// let config = ResolvedPlanningConfig::resolve(
767///     &policy,
768///     &PrepareOptions::new(),
769///     HardwareClassId::new("tenferro.cpu")?,
770/// );
771/// assert_eq!(config.hard_workspace_limit_bytes(), Some(32));
772/// # Ok(())
773/// # }
774/// ```
775#[derive(Clone, Debug, Eq, Hash, PartialEq)]
776pub struct ResolvedPlanningConfig {
777    determinism: Determinism,
778    hard_workspace_limit_bytes: Option<usize>,
779    planning_seed: u64,
780    hardware_class: HardwareClassId,
781}
782
783impl ResolvedPlanningConfig {
784    /// Resolve raw policy and per-call options into concrete planning config.
785    ///
786    /// # Examples
787    ///
788    /// ```
789    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
790    /// use tenferro_runtime::{
791    ///     Determinism, ExecutionPolicy, HardwareClassId, PrepareOptions, ResolvedPlanningConfig,
792    /// };
793    ///
794    /// let policy = ExecutionPolicy::new(Determinism::Fast, Some(32), 5);
795    /// let options = PrepareOptions::new().with_hard_workspace_limit_bytes(Some(0));
796    /// let config = ResolvedPlanningConfig::resolve(
797    ///     &policy,
798    ///     &options,
799    ///     HardwareClassId::new("tenferro.cpu")?,
800    /// );
801    /// assert_eq!(config.hard_workspace_limit_bytes(), Some(0));
802    /// # Ok(())
803    /// # }
804    /// ```
805    pub fn resolve(
806        policy: &ExecutionPolicy,
807        options: &PrepareOptions,
808        hardware_class: HardwareClassId,
809    ) -> Self {
810        Self {
811            determinism: policy.determinism(),
812            hard_workspace_limit_bytes: options
813                .hard_workspace_limit_bytes()
814                .or(policy.hard_workspace_limit_bytes()),
815            planning_seed: options.planning_seed().unwrap_or(policy.planning_seed()),
816            hardware_class,
817        }
818    }
819
820    /// Return the resolved determinism mode.
821    ///
822    /// # Examples
823    ///
824    /// ```
825    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
826    /// use tenferro_runtime::{
827    ///     Determinism, ExecutionPolicy, HardwareClassId, PrepareOptions, ResolvedPlanningConfig,
828    /// };
829    ///
830    /// let config = ResolvedPlanningConfig::resolve(
831    ///     &ExecutionPolicy::new(Determinism::Reproducible, None, 0),
832    ///     &PrepareOptions::new(),
833    ///     HardwareClassId::new("tenferro.cpu")?,
834    /// );
835    /// assert_eq!(config.determinism(), Determinism::Reproducible);
836    /// # Ok(())
837    /// # }
838    /// ```
839    pub fn determinism(&self) -> Determinism {
840        self.determinism
841    }
842
843    /// Return the resolved hard workspace limit.
844    ///
845    /// # Examples
846    ///
847    /// ```
848    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
849    /// use tenferro_runtime::{
850    ///     Determinism, ExecutionPolicy, HardwareClassId, PrepareOptions, ResolvedPlanningConfig,
851    /// };
852    ///
853    /// let config = ResolvedPlanningConfig::resolve(
854    ///     &ExecutionPolicy::new(Determinism::Fast, Some(7), 0),
855    ///     &PrepareOptions::new(),
856    ///     HardwareClassId::new("tenferro.cpu")?,
857    /// );
858    /// assert_eq!(config.hard_workspace_limit_bytes(), Some(7));
859    /// # Ok(())
860    /// # }
861    /// ```
862    pub fn hard_workspace_limit_bytes(&self) -> Option<usize> {
863        self.hard_workspace_limit_bytes
864    }
865
866    /// Return the resolved planning seed.
867    ///
868    /// # Examples
869    ///
870    /// ```
871    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
872    /// use tenferro_runtime::{
873    ///     Determinism, ExecutionPolicy, HardwareClassId, PrepareOptions, ResolvedPlanningConfig,
874    /// };
875    ///
876    /// let config = ResolvedPlanningConfig::resolve(
877    ///     &ExecutionPolicy::new(Determinism::Fast, None, 19),
878    ///     &PrepareOptions::new(),
879    ///     HardwareClassId::new("tenferro.cpu")?,
880    /// );
881    /// assert_eq!(config.planning_seed(), 19);
882    /// # Ok(())
883    /// # }
884    /// ```
885    pub fn planning_seed(&self) -> u64 {
886        self.planning_seed
887    }
888
889    /// Return the resolved hardware class.
890    ///
891    /// # Examples
892    ///
893    /// ```
894    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
895    /// use tenferro_runtime::{
896    ///     Determinism, ExecutionPolicy, HardwareClassId, PrepareOptions, ResolvedPlanningConfig,
897    /// };
898    ///
899    /// let hardware = HardwareClassId::new("tenferro.cpu")?;
900    /// let config = ResolvedPlanningConfig::resolve(
901    ///     &ExecutionPolicy::new(Determinism::Fast, None, 0),
902    ///     &PrepareOptions::new(),
903    ///     hardware.clone(),
904    /// );
905    /// assert_eq!(config.hardware_class(), &hardware);
906    /// # Ok(())
907    /// # }
908    /// ```
909    pub fn hardware_class(&self) -> &HardwareClassId {
910        &self.hardware_class
911    }
912}
913
914/// Normalized planning cache key.
915///
916/// A0 exposes no public constructor for this runtime-created value; B0 supplies
917/// its runtime creation path.
918///
919/// # Examples
920///
921/// ```
922/// use std::fmt::Debug;
923/// use tenferro_runtime::ResolvedPlanningKey;
924///
925/// fn requires_debug<T: Debug>() {}
926/// requires_debug::<ResolvedPlanningKey>();
927/// ```
928#[derive(Clone, Debug, Eq, Hash, PartialEq)]
929pub struct ResolvedPlanningKey {
930    determinism: Determinism,
931    hard_workspace_limit_bytes: Option<usize>,
932    planning_seed: u64,
933    hardware_class: HardwareClassId,
934}
935
936impl ResolvedPlanningKey {
937    // INVARIANT: A0 module-local tests create normalized keys until B0 owns planning-key resolution.
938    #[allow(dead_code)]
939    pub(crate) fn from_config(config: &ResolvedPlanningConfig) -> Self {
940        Self {
941            determinism: config.determinism(),
942            hard_workspace_limit_bytes: config.hard_workspace_limit_bytes(),
943            planning_seed: config.planning_seed(),
944            hardware_class: config.hardware_class().clone(),
945        }
946    }
947
948    /// Return the key's determinism component.
949    ///
950    /// # Examples
951    ///
952    /// ```
953    /// use std::fmt::Debug;
954    /// use tenferro_runtime::ResolvedPlanningKey;
955    ///
956    /// fn requires_debug<T: Debug>() {}
957    /// requires_debug::<ResolvedPlanningKey>();
958    /// ```
959    pub fn determinism(&self) -> Determinism {
960        self.determinism
961    }
962
963    /// Return the key's workspace component.
964    ///
965    /// # Examples
966    ///
967    /// ```
968    /// use std::fmt::Debug;
969    /// use tenferro_runtime::ResolvedPlanningKey;
970    ///
971    /// fn requires_debug<T: Debug>() {}
972    /// requires_debug::<ResolvedPlanningKey>();
973    /// ```
974    pub fn hard_workspace_limit_bytes(&self) -> Option<usize> {
975        self.hard_workspace_limit_bytes
976    }
977
978    /// Return the key's planning seed component.
979    ///
980    /// # Examples
981    ///
982    /// ```
983    /// use std::fmt::Debug;
984    /// use tenferro_runtime::ResolvedPlanningKey;
985    ///
986    /// fn requires_debug<T: Debug>() {}
987    /// requires_debug::<ResolvedPlanningKey>();
988    /// ```
989    pub fn planning_seed(&self) -> u64 {
990        self.planning_seed
991    }
992
993    /// Return the key's hardware-class component.
994    ///
995    /// # Examples
996    ///
997    /// ```
998    /// use std::fmt::Debug;
999    /// use tenferro_runtime::ResolvedPlanningKey;
1000    ///
1001    /// fn requires_debug<T: Debug>() {}
1002    /// requires_debug::<ResolvedPlanningKey>();
1003    /// ```
1004    pub fn hardware_class(&self) -> &HardwareClassId {
1005        &self.hardware_class
1006    }
1007}