Skip to main content

pleiades_data/
lookup.rs

1use std::fmt;
2use std::sync::OnceLock;
3
4#[cfg(feature = "packaged-artifact-path")]
5use std::path::Path;
6
7use pleiades_backend::{
8    Apparentness, CelestialBody, CoordinateFrame, EclipticCoordinates, EphemerisBackend,
9    EphemerisRequest, Instant, QualityAnnotation, TimeScale, ZodiacMode,
10};
11use pleiades_compression::{
12    join_display, ArtifactOutput, ArtifactProfile, ChannelKind, CompressedArtifact,
13};
14use pleiades_jpl::{reference_snapshot, SnapshotEntry};
15
16use crate::backend::PackagedDataBackend;
17use crate::coverage::packaged_artifact_profile_summary_details;
18use crate::data::{packaged_artifact, PackagedArtifactLoadError};
19use crate::regenerate::normalize_lookup_instant;
20use crate::{packaged_bodies, packaged_reference_entry_for_body};
21
22/// Structured policy for how packaged-data lookup epochs are handled.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum PackagedLookupEpochPolicy {
25    /// TDB-tagged lookups are re-tagged onto the TT grid without relativistic correction.
26    RetagToTtGridWithoutRelativisticCorrection,
27}
28
29impl PackagedLookupEpochPolicy {
30    /// Returns the compact label used in release-facing summaries.
31    pub const fn label(self) -> &'static str {
32        match self {
33            Self::RetagToTtGridWithoutRelativisticCorrection => {
34                "TT-grid retag without relativistic correction"
35            }
36        }
37    }
38
39    /// Returns the explanatory note used in release-facing summaries.
40    pub const fn note(self) -> &'static str {
41        match self {
42            Self::RetagToTtGridWithoutRelativisticCorrection => {
43                "TDB lookup epochs are re-tagged onto the TT grid without applying a relativistic correction"
44            }
45        }
46    }
47
48    /// Returns `Ok(())` when the policy still matches the current packaged-data posture.
49    pub fn validate(self) -> Result<(), PackagedLookupEpochPolicyValidationError> {
50        if self != Self::RetagToTtGridWithoutRelativisticCorrection {
51            return Err(PackagedLookupEpochPolicyValidationError::FieldOutOfSync {
52                field: "policy",
53            });
54        }
55
56        Ok(())
57    }
58
59    /// Returns the compact release-facing summary for the lookup-epoch policy.
60    pub fn summary_line(self) -> String {
61        format!("{}; {}", self.label(), self.note())
62    }
63}
64
65impl fmt::Display for PackagedLookupEpochPolicy {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        f.write_str(self.label())
68    }
69}
70
71/// Validation error for the packaged-data lookup-epoch policy.
72#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
73pub enum PackagedLookupEpochPolicyValidationError {
74    /// A policy field is out of sync with the current packaged-data posture.
75    FieldOutOfSync { field: &'static str },
76}
77
78impl fmt::Display for PackagedLookupEpochPolicyValidationError {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match self {
81            Self::FieldOutOfSync { field } => write!(
82                f,
83                "the packaged lookup-epoch policy field `{field}` is out of sync with the current posture"
84            ),
85        }
86    }
87}
88
89impl std::error::Error for PackagedLookupEpochPolicyValidationError {}
90
91/// Structured summary for the packaged-data lookup-epoch policy.
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub struct PackagedLookupEpochPolicySummary {
94    /// Policy describing how TDB-tagged lookups are handled.
95    pub policy: PackagedLookupEpochPolicy,
96}
97
98/// Validation error for the packaged-data lookup-epoch policy summary.
99#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
100pub enum PackagedLookupEpochPolicySummaryValidationError {
101    /// A summary field is out of sync with the current packaged-data posture.
102    FieldOutOfSync { field: &'static str },
103}
104
105impl fmt::Display for PackagedLookupEpochPolicySummaryValidationError {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        match self {
108            Self::FieldOutOfSync { field } => write!(
109                f,
110                "the packaged lookup-epoch policy summary field `{field}` is out of sync with the current posture"
111            ),
112        }
113    }
114}
115
116impl std::error::Error for PackagedLookupEpochPolicySummaryValidationError {}
117
118impl PackagedLookupEpochPolicySummary {
119    /// Returns the packaged lookup-epoch policy as a compact human-readable line.
120    pub fn summary_line(self) -> String {
121        self.policy.summary_line()
122    }
123
124    /// Returns `Ok(())` when the summary still matches the current packaged-data posture.
125    pub fn validate(&self) -> Result<(), PackagedLookupEpochPolicySummaryValidationError> {
126        self.policy.validate().map_err(|error| match error {
127            PackagedLookupEpochPolicyValidationError::FieldOutOfSync { field } => {
128                PackagedLookupEpochPolicySummaryValidationError::FieldOutOfSync { field }
129            }
130        })
131    }
132}
133
134impl fmt::Display for PackagedLookupEpochPolicySummary {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        f.write_str(&self.summary_line())
137    }
138}
139
140const PACKAGED_LOOKUP_EPOCH_POLICY_SUMMARY: PackagedLookupEpochPolicySummary =
141    PackagedLookupEpochPolicySummary {
142        policy: PackagedLookupEpochPolicy::RetagToTtGridWithoutRelativisticCorrection,
143    };
144
145/// Returns the current packaged-data lookup-epoch policy summary record.
146///
147/// # Examples
148///
149/// ```
150/// use pleiades_data::packaged_lookup_epoch_policy_summary_details;
151///
152/// let summary = packaged_lookup_epoch_policy_summary_details();
153/// assert_eq!(
154///     summary.summary_line(),
155///     "TT-grid retag without relativistic correction; TDB lookup epochs are re-tagged onto the TT grid without applying a relativistic correction",
156/// );
157/// assert!(summary.validate().is_ok());
158/// ```
159pub fn packaged_lookup_epoch_policy_summary_details() -> PackagedLookupEpochPolicySummary {
160    let summary = PACKAGED_LOOKUP_EPOCH_POLICY_SUMMARY;
161    debug_assert!(summary.validate().is_ok());
162    summary
163}
164
165/// Returns the current packaged-data lookup-epoch policy summary after validating the structured posture.
166pub fn packaged_lookup_epoch_policy_summary_for_report() -> String {
167    let summary = packaged_lookup_epoch_policy_summary_details();
168    match summary.validate() {
169        Ok(()) => summary.to_string(),
170        Err(error) => format!("Packaged lookup epoch policy: unavailable ({error})"),
171    }
172}
173
174/// Returns the current packaged-data lookup-epoch policy summary.
175pub fn packaged_lookup_epoch_policy_summary() -> &'static str {
176    static SUMMARY: OnceLock<String> = OnceLock::new();
177    SUMMARY
178        .get_or_init(|| {
179            let summary = packaged_lookup_epoch_policy_summary_details();
180            match summary.validate() {
181                Ok(()) => summary.to_string(),
182                Err(error) => format!("Packaged lookup epoch policy: unavailable ({error})"),
183            }
184        })
185        .as_str()
186}
187
188/// Structured request-policy summary for the packaged-data backend.
189#[derive(Clone, Copy, Debug, PartialEq)]
190pub struct PackagedRequestPolicySummary {
191    /// Whether the backend is geocentric only.
192    pub geocentric_only: bool,
193    /// Coordinate frames supported by the packaged backend.
194    pub supported_frames: &'static [CoordinateFrame],
195    /// Time scales supported by the packaged backend.
196    pub supported_time_scales: &'static [TimeScale],
197    /// Zodiac modes supported by the packaged backend.
198    pub supported_zodiac_modes: &'static [ZodiacMode],
199    /// Apparentness modes supported by the packaged backend.
200    pub supported_apparentness: &'static [Apparentness],
201    /// Whether the packaged backend accepts topocentric observer requests.
202    pub supports_topocentric_observer: bool,
203    /// Policy describing how TDB-tagged lookups are handled.
204    pub lookup_epoch_policy: PackagedLookupEpochPolicy,
205}
206
207/// Validation error for the packaged-data request-policy summary.
208#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
209pub enum PackagedRequestPolicySummaryValidationError {
210    /// A summary field is out of sync with the current packaged-data posture.
211    FieldOutOfSync { field: &'static str },
212}
213
214impl fmt::Display for PackagedRequestPolicySummaryValidationError {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        match self {
217            Self::FieldOutOfSync { field } => write!(
218                f,
219                "the packaged request-policy summary field `{field}` is out of sync with the current posture"
220            ),
221        }
222    }
223}
224
225impl std::error::Error for PackagedRequestPolicySummaryValidationError {}
226
227impl PackagedRequestPolicySummary {
228    /// Renders the packaged request policy into a release-facing summary line.
229    pub fn summary_line(&self) -> String {
230        format!(
231            "Packaged request policy: {}frames={}; time scales={}; zodiac modes={}; apparentness={}; topocentric observer={}; lookup epoch policy={}",
232            if self.geocentric_only {
233                "geocentric-only; "
234            } else {
235                ""
236            },
237            join_display(self.supported_frames),
238            join_display(self.supported_time_scales),
239            join_display(self.supported_zodiac_modes),
240            join_display(self.supported_apparentness),
241            self.supports_topocentric_observer,
242            self.lookup_epoch_policy.summary_line(),
243        )
244    }
245
246    /// Returns `Ok(())` when the packaged request-policy posture still matches the current backend metadata.
247    pub fn validate(&self) -> Result<(), PackagedRequestPolicySummaryValidationError> {
248        if !self.geocentric_only {
249            return Err(
250                PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
251                    field: "geocentric_only",
252                },
253            );
254        }
255        if self.supported_frames != [CoordinateFrame::Ecliptic, CoordinateFrame::Equatorial] {
256            return Err(
257                PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
258                    field: "supported_frames",
259                },
260            );
261        }
262        if self.supported_time_scales != [TimeScale::Tt, TimeScale::Tdb] {
263            return Err(
264                PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
265                    field: "supported_time_scales",
266                },
267            );
268        }
269        if self.supported_zodiac_modes != [ZodiacMode::Tropical] {
270            return Err(
271                PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
272                    field: "supported_zodiac_modes",
273                },
274            );
275        }
276        if self.supported_apparentness != [Apparentness::Mean] {
277            return Err(
278                PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
279                    field: "supported_apparentness",
280                },
281            );
282        }
283        if self.supports_topocentric_observer {
284            return Err(
285                PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
286                    field: "supports_topocentric_observer",
287                },
288            );
289        }
290        if self.lookup_epoch_policy
291            != PackagedLookupEpochPolicy::RetagToTtGridWithoutRelativisticCorrection
292        {
293            return Err(
294                PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
295                    field: "lookup_epoch_policy",
296                },
297            );
298        }
299
300        Ok(())
301    }
302}
303
304impl fmt::Display for PackagedRequestPolicySummary {
305    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306        f.write_str(&self.summary_line())
307    }
308}
309
310const PACKAGED_REQUEST_POLICY_SUMMARY: PackagedRequestPolicySummary =
311    PackagedRequestPolicySummary {
312        geocentric_only: true,
313        supported_frames: &[CoordinateFrame::Ecliptic, CoordinateFrame::Equatorial],
314        supported_time_scales: &[TimeScale::Tt, TimeScale::Tdb],
315        supported_zodiac_modes: &[ZodiacMode::Tropical],
316        supported_apparentness: &[Apparentness::Mean],
317        supports_topocentric_observer: false,
318        lookup_epoch_policy: PackagedLookupEpochPolicy::RetagToTtGridWithoutRelativisticCorrection,
319    };
320
321/// Returns the current packaged-data request-policy summary record.
322///
323/// # Examples
324///
325/// ```
326/// use pleiades_data::packaged_request_policy_summary_details;
327///
328/// let summary = packaged_request_policy_summary_details();
329/// assert_eq!(
330///     summary.summary_line(),
331///     "Packaged request policy: geocentric-only; frames=Ecliptic, Equatorial; time scales=TT, TDB; zodiac modes=Tropical; apparentness=Mean; topocentric observer=false; lookup epoch policy=TT-grid retag without relativistic correction; TDB lookup epochs are re-tagged onto the TT grid without applying a relativistic correction",
332/// );
333/// assert!(summary.validate().is_ok());
334/// ```
335pub fn packaged_request_policy_summary_details() -> PackagedRequestPolicySummary {
336    let summary = PACKAGED_REQUEST_POLICY_SUMMARY;
337    debug_assert!(summary.validate().is_ok());
338    summary
339}
340
341/// Returns the current packaged-data request policy summary after validating the structured posture.
342pub fn packaged_request_policy_summary_for_report() -> String {
343    let summary = packaged_request_policy_summary_details();
344    match summary.validate() {
345        Ok(()) => summary.to_string(),
346        Err(error) => format!("Packaged request policy: unavailable ({error})"),
347    }
348}
349
350/// Returns the current packaged-data request policy summary.
351pub fn packaged_request_policy_summary() -> &'static str {
352    static SUMMARY: OnceLock<String> = OnceLock::new();
353    SUMMARY
354        .get_or_init(|| {
355            let summary = packaged_request_policy_summary_details();
356            match summary.validate() {
357                Ok(()) => summary.to_string(),
358                Err(error) => format!("Packaged request policy: unavailable ({error})"),
359            }
360        })
361        .as_str()
362}
363
364/// Structured frame-treatment summary for the packaged artifact.
365#[derive(Clone, Copy, Debug, PartialEq, Eq)]
366pub struct PackagedFrameTreatmentSummary;
367
368/// Validation error for a packaged frame-treatment summary that drifted away from the compact posture line.
369#[derive(Clone, Copy, Debug, PartialEq, Eq)]
370pub enum PackagedFrameTreatmentSummaryValidationError {
371    /// The summary text is blank or whitespace-only.
372    BlankSummary,
373    /// The summary text has surrounding whitespace.
374    WhitespacePaddedSummary,
375}
376
377impl fmt::Display for PackagedFrameTreatmentSummaryValidationError {
378    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379        match self {
380            Self::BlankSummary => f.write_str("packaged frame-treatment summary is blank"),
381            Self::WhitespacePaddedSummary => {
382                f.write_str("packaged frame-treatment summary has surrounding whitespace")
383            }
384        }
385    }
386}
387
388impl std::error::Error for PackagedFrameTreatmentSummaryValidationError {}
389
390pub(crate) fn validate_packaged_frame_treatment_summary_line(
391    summary: &str,
392) -> Result<(), PackagedFrameTreatmentSummaryValidationError> {
393    if summary.trim().is_empty() {
394        Err(PackagedFrameTreatmentSummaryValidationError::BlankSummary)
395    } else if summary.trim() != summary {
396        Err(PackagedFrameTreatmentSummaryValidationError::WhitespacePaddedSummary)
397    } else {
398        Ok(())
399    }
400}
401
402impl PackagedFrameTreatmentSummary {
403    /// Returns the frame-treatment posture as a compact human-readable line.
404    pub const fn summary_line(self) -> &'static str {
405        "checked-in compressed artifact stores J2000 ecliptic coordinates directly; equatorial coordinates are reconstructed from the stored channels and mean-obliquity transform"
406    }
407
408    /// Returns `Ok(())` when the summary still contains a compact canonical line.
409    pub fn validate(&self) -> Result<(), PackagedFrameTreatmentSummaryValidationError> {
410        validate_packaged_frame_treatment_summary_line(self.summary_line())
411    }
412
413    /// Returns the compact one-line rendering after validation.
414    pub fn validated_summary_line(
415        &self,
416    ) -> Result<&'static str, PackagedFrameTreatmentSummaryValidationError> {
417        self.validate()?;
418        Ok(self.summary_line())
419    }
420}
421
422impl fmt::Display for PackagedFrameTreatmentSummary {
423    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
424        f.write_str(self.summary_line())
425    }
426}
427
428/// Returns the structured packaged-artifact frame-treatment summary.
429pub const fn packaged_frame_treatment_summary_details() -> PackagedFrameTreatmentSummary {
430    PackagedFrameTreatmentSummary
431}
432
433/// Returns the packaged-artifact frame-treatment summary for report rendering.
434pub fn packaged_frame_treatment_summary_for_report() -> String {
435    let summary = packaged_frame_treatment_summary_details();
436    match summary.validated_summary_line() {
437        Ok(summary_line) => summary_line.to_string(),
438        Err(error) => format!("Packaged frame treatment unavailable ({error})"),
439    }
440}
441
442/// Returns the packaged-artifact frame-treatment summary.
443pub fn packaged_frame_treatment_summary() -> &'static str {
444    static SUMMARY: OnceLock<String> = OnceLock::new();
445    SUMMARY
446        .get_or_init(packaged_frame_treatment_summary_for_report)
447        .as_str()
448}
449
450/// Structured storage/reconstruction summary for the packaged artifact.
451#[derive(Clone, Copy, Debug, PartialEq, Eq)]
452pub struct PackagedArtifactStorageSummary;
453
454/// Validation error for a packaged storage/reconstruction summary that drifted away from the compact posture line.
455#[derive(Clone, Copy, Debug, PartialEq, Eq)]
456pub enum PackagedArtifactStorageSummaryValidationError {
457    /// The summary text is blank or whitespace-only.
458    BlankSummary,
459    /// The summary text has surrounding whitespace.
460    WhitespacePaddedSummary,
461    /// The summary text no longer matches the current packaged-artifact profile posture.
462    ProfileOutOfSync { field: &'static str },
463}
464
465impl fmt::Display for PackagedArtifactStorageSummaryValidationError {
466    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
467        match self {
468            Self::BlankSummary => f.write_str("packaged artifact storage summary is blank"),
469            Self::WhitespacePaddedSummary => {
470                f.write_str("packaged artifact storage summary has surrounding whitespace")
471            }
472            Self::ProfileOutOfSync { field } => write!(
473                f,
474                "packaged artifact storage summary is out of sync with the bundled artifact profile field `{field}`"
475            ),
476        }
477    }
478}
479
480impl std::error::Error for PackagedArtifactStorageSummaryValidationError {}
481
482pub(crate) fn validate_packaged_artifact_storage_summary_line(
483    summary: &str,
484) -> Result<(), PackagedArtifactStorageSummaryValidationError> {
485    if summary.trim().is_empty() {
486        Err(PackagedArtifactStorageSummaryValidationError::BlankSummary)
487    } else if summary.trim() != summary {
488        Err(PackagedArtifactStorageSummaryValidationError::WhitespacePaddedSummary)
489    } else {
490        Ok(())
491    }
492}
493
494pub(crate) fn validate_packaged_artifact_storage_profile(
495    profile: &ArtifactProfile,
496) -> Result<(), PackagedArtifactStorageSummaryValidationError> {
497    const EXPECTED_STORED_CHANNELS: [ChannelKind; 3] = [
498        ChannelKind::Longitude,
499        ChannelKind::Latitude,
500        ChannelKind::DistanceAu,
501    ];
502    const EXPECTED_DERIVED_OUTPUTS: [ArtifactOutput; 3] = [
503        ArtifactOutput::EclipticCoordinates,
504        ArtifactOutput::EquatorialCoordinates,
505        ArtifactOutput::Motion,
506    ];
507    const EXPECTED_UNSUPPORTED_OUTPUTS: [ArtifactOutput; 3] = [
508        ArtifactOutput::ApparentCorrections,
509        ArtifactOutput::TopocentricCoordinates,
510        ArtifactOutput::SiderealCoordinates,
511    ];
512
513    if profile.stored_channels != EXPECTED_STORED_CHANNELS {
514        return Err(
515            PackagedArtifactStorageSummaryValidationError::ProfileOutOfSync {
516                field: "stored_channels",
517            },
518        );
519    }
520
521    if profile.derived_outputs.as_slice() != EXPECTED_DERIVED_OUTPUTS {
522        return Err(
523            PackagedArtifactStorageSummaryValidationError::ProfileOutOfSync {
524                field: "derived_outputs",
525            },
526        );
527    }
528
529    if profile.unsupported_outputs.as_slice() != EXPECTED_UNSUPPORTED_OUTPUTS {
530        return Err(
531            PackagedArtifactStorageSummaryValidationError::ProfileOutOfSync {
532                field: "unsupported_outputs",
533            },
534        );
535    }
536
537    Ok(())
538}
539
540impl PackagedArtifactStorageSummary {
541    /// Returns the storage and reconstruction posture as a compact human-readable line.
542    pub const fn summary_line(self) -> &'static str {
543        "Quantized linear segments stored in pleiades-compression artifact format; body-indexed segment tables support random access by body and lookup time across the advertised range; ecliptic and equatorial coordinates are reconstructed at runtime from stored channels; apparent, topocentric, and sidereal outputs remain unsupported; motion/speed is derived from fitted segment derivatives"
544    }
545
546    /// Returns `Ok(())` when the summary still contains a compact canonical line.
547    pub fn validate(&self) -> Result<(), PackagedArtifactStorageSummaryValidationError> {
548        validate_packaged_artifact_storage_summary_line(self.summary_line())?;
549        validate_packaged_artifact_storage_profile(
550            &packaged_artifact_profile_summary_details().profile,
551        )
552    }
553}
554
555impl fmt::Display for PackagedArtifactStorageSummary {
556    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
557        f.write_str(self.summary_line())
558    }
559}
560
561/// Returns the structured packaged-artifact storage/reconstruction summary.
562pub const fn packaged_artifact_storage_summary_details() -> PackagedArtifactStorageSummary {
563    PackagedArtifactStorageSummary
564}
565
566/// Returns the packaged-artifact storage/reconstruction summary.
567pub fn packaged_artifact_storage_summary() -> &'static str {
568    static SUMMARY: OnceLock<String> = OnceLock::new();
569    SUMMARY
570        .get_or_init(|| {
571            let summary = packaged_artifact_storage_summary_details();
572            match summary.validate() {
573                Ok(()) => summary.to_string(),
574                Err(error) => format!("Packaged artifact storage unavailable ({error})"),
575            }
576        })
577        .as_str()
578}
579
580/// Returns the packaged-artifact storage/reconstruction summary for reporting.
581pub fn packaged_artifact_storage_summary_for_report() -> String {
582    let summary = packaged_artifact_storage_summary_details();
583    match summary.validate() {
584        Ok(()) => summary.to_string(),
585        Err(error) => format!("Packaged artifact storage/reconstruction: unavailable ({error})"),
586    }
587}
588
589/// Structured packaged-artifact access summary.
590#[derive(Clone, Copy, Debug, PartialEq, Eq)]
591pub struct PackagedArtifactAccessSummary {
592    /// Whether explicit artifact-file loading is enabled.
593    pub explicit_path_loading: bool,
594}
595
596/// Validation error for a packaged artifact access summary that drifted away from the current build posture.
597#[derive(Clone, Copy, Debug, PartialEq, Eq)]
598pub enum PackagedArtifactAccessSummaryValidationError {
599    /// The summary text is blank or whitespace-only.
600    BlankSummary,
601    /// The summary text has surrounding whitespace.
602    WhitespacePaddedSummary,
603    /// The feature-flag state no longer matches the current build posture.
604    FeatureStateOutOfSync { field: &'static str },
605}
606
607impl fmt::Display for PackagedArtifactAccessSummaryValidationError {
608    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
609        match self {
610            Self::BlankSummary => f.write_str("packaged artifact access summary is blank"),
611            Self::WhitespacePaddedSummary => {
612                f.write_str("packaged artifact access summary has surrounding whitespace")
613            }
614            Self::FeatureStateOutOfSync { field } => write!(
615                f,
616                "packaged artifact access summary is out of sync with the build feature field `{field}`"
617            ),
618        }
619    }
620}
621
622impl std::error::Error for PackagedArtifactAccessSummaryValidationError {}
623
624pub(crate) fn validate_packaged_artifact_access_summary_line(
625    summary: &str,
626) -> Result<(), PackagedArtifactAccessSummaryValidationError> {
627    if summary.trim().is_empty() {
628        Err(PackagedArtifactAccessSummaryValidationError::BlankSummary)
629    } else if summary.trim() != summary {
630        Err(PackagedArtifactAccessSummaryValidationError::WhitespacePaddedSummary)
631    } else {
632        Ok(())
633    }
634}
635
636impl PackagedArtifactAccessSummary {
637    /// Returns the packaged artifact access posture as a compact human-readable line.
638    pub const fn summary_line(self) -> &'static str {
639        if self.explicit_path_loading {
640            "packaged artifact access: checked-in fixture plus explicit artifact-path loading via `packaged-artifact-path` feature"
641        } else {
642            "packaged artifact access: checked-in fixture only; explicit artifact-path loading disabled"
643        }
644    }
645
646    /// Returns the validated packaged artifact access posture as a compact line.
647    pub fn validated_summary_line(
648        &self,
649    ) -> Result<&'static str, PackagedArtifactAccessSummaryValidationError> {
650        self.validate()?;
651        Ok(self.summary_line())
652    }
653
654    /// Returns `Ok(())` when the summary still matches the current build posture.
655    pub fn validate(&self) -> Result<(), PackagedArtifactAccessSummaryValidationError> {
656        validate_packaged_artifact_access_summary_line(self.summary_line())?;
657        if self.explicit_path_loading != packaged_artifact_path_loading_enabled() {
658            return Err(
659                PackagedArtifactAccessSummaryValidationError::FeatureStateOutOfSync {
660                    field: "explicit_path_loading",
661                },
662            );
663        }
664        Ok(())
665    }
666}
667
668impl fmt::Display for PackagedArtifactAccessSummary {
669    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
670        f.write_str(self.summary_line())
671    }
672}
673
674/// Returns whether this build can load packaged artifacts from an explicit path.
675pub const fn packaged_artifact_path_loading_enabled() -> bool {
676    cfg!(feature = "packaged-artifact-path")
677}
678
679/// Returns the structured packaged-artifact access summary.
680///
681/// # Examples
682///
683/// ```
684/// use pleiades_data::{
685///     packaged_artifact_access_summary_details,
686///     packaged_artifact_access_summary_for_report,
687///     packaged_artifact_path_loading_enabled,
688/// };
689///
690/// let summary = packaged_artifact_access_summary_details();
691/// assert_eq!(summary.explicit_path_loading, packaged_artifact_path_loading_enabled());
692/// assert_eq!(summary.to_string(), packaged_artifact_access_summary_for_report());
693/// assert!(summary.validate().is_ok());
694/// ```
695pub const fn packaged_artifact_access_summary_details() -> PackagedArtifactAccessSummary {
696    PackagedArtifactAccessSummary {
697        explicit_path_loading: packaged_artifact_path_loading_enabled(),
698    }
699}
700
701/// Returns the packaged-artifact access summary.
702pub fn packaged_artifact_access_summary() -> &'static str {
703    static SUMMARY: OnceLock<String> = OnceLock::new();
704    SUMMARY
705        .get_or_init(|| {
706            let summary = packaged_artifact_access_summary_details();
707            match summary.validated_summary_line() {
708                Ok(rendered) => rendered.to_string(),
709                Err(error) => format!("Packaged artifact access unavailable ({error})"),
710            }
711        })
712        .as_str()
713}
714
715/// Returns the packaged-artifact access summary for reporting.
716pub fn packaged_artifact_access_summary_for_report() -> String {
717    let summary = packaged_artifact_access_summary_details();
718    match summary.validated_summary_line() {
719        Ok(rendered) => rendered.to_string(),
720        Err(error) => format!("Packaged artifact access: unavailable ({error})"),
721    }
722}
723
724/// Structured mixed batch-parity summary for the packaged artifact.
725#[derive(Clone, Debug, PartialEq)]
726pub struct PackagedBatchParitySummary {
727    /// Number of requests in the batch regression.
728    pub request_count: usize,
729    /// Number of bodies covered by the batch regression.
730    pub body_count: usize,
731    /// Number of requests using the ecliptic frame.
732    pub ecliptic_request_count: usize,
733    /// Number of requests using the equatorial frame.
734    pub equatorial_request_count: usize,
735    /// Number of exact-quality results observed in the batch regression.
736    pub exact_count: usize,
737    /// Number of interpolated-quality results observed in the batch regression.
738    pub interpolated_count: usize,
739    /// Number of approximate-quality results observed in the batch regression.
740    pub approximate_count: usize,
741    /// Number of unknown-quality results observed in the batch regression.
742    pub unknown_count: usize,
743    /// Whether the batch regression preserved request order.
744    pub order_preserved: bool,
745    /// Whether the batch regression preserved batch/single parity.
746    pub single_query_parity_preserved: bool,
747}
748
749/// Validation error for a packaged mixed-frame batch-parity summary.
750#[derive(Clone, Debug, Eq, PartialEq)]
751pub enum PackagedBatchParitySummaryValidationError {
752    /// A summary field is out of sync with the current packaged batch-parity posture.
753    FieldOutOfSync { field: &'static str },
754}
755
756impl fmt::Display for PackagedBatchParitySummaryValidationError {
757    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
758        match self {
759            Self::FieldOutOfSync { field } => write!(
760                f,
761                "the packaged mixed-frame batch-parity summary field `{field}` is out of sync with the current packaged posture"
762            ),
763        }
764    }
765}
766
767impl std::error::Error for PackagedBatchParitySummaryValidationError {}
768
769impl PackagedBatchParitySummary {
770    /// Returns `Ok(())` when the summary still matches the current packaged batch-parity posture.
771    pub fn validate(&self) -> Result<(), PackagedBatchParitySummaryValidationError> {
772        if self.request_count != self.body_count {
773            return Err(PackagedBatchParitySummaryValidationError::FieldOutOfSync {
774                field: "request_count/body_count",
775            });
776        }
777
778        if self.ecliptic_request_count + self.equatorial_request_count != self.request_count {
779            return Err(PackagedBatchParitySummaryValidationError::FieldOutOfSync {
780                field: "frame_counts",
781            });
782        }
783
784        if self.ecliptic_request_count == 0 || self.equatorial_request_count == 0 {
785            return Err(PackagedBatchParitySummaryValidationError::FieldOutOfSync {
786                field: "frame_mix",
787            });
788        }
789
790        if self.exact_count + self.interpolated_count + self.approximate_count + self.unknown_count
791            != self.request_count
792        {
793            return Err(PackagedBatchParitySummaryValidationError::FieldOutOfSync {
794                field: "quality_counts",
795            });
796        }
797
798        if !self.order_preserved {
799            return Err(PackagedBatchParitySummaryValidationError::FieldOutOfSync {
800                field: "order_preserved",
801            });
802        }
803
804        if !self.single_query_parity_preserved {
805            return Err(PackagedBatchParitySummaryValidationError::FieldOutOfSync {
806                field: "single_query_parity_preserved",
807            });
808        }
809
810        Ok(())
811    }
812
813    /// Returns the validated batch-parity summary line.
814    pub fn validated_summary_line(
815        &self,
816    ) -> Result<String, PackagedBatchParitySummaryValidationError> {
817        self.validate()?;
818        Ok(self.summary_line())
819    }
820}
821
822fn packaged_mixed_frame_batch_parity_request_entries(
823) -> Option<(Vec<EphemerisRequest>, Vec<SnapshotEntry>)> {
824    let snapshot = reference_snapshot();
825    let mut requests = Vec::with_capacity(packaged_bodies().len());
826    let mut entries = Vec::with_capacity(packaged_bodies().len());
827
828    for (index, body) in packaged_bodies().iter().cloned().enumerate() {
829        let entry = packaged_reference_entry_for_body(snapshot, &body)?;
830        entries.push(entry.clone());
831        requests.push(EphemerisRequest {
832            body,
833            instant: Instant::new(entry.epoch.julian_day, TimeScale::Tt),
834            observer: None,
835            frame: if index % 2 == 0 {
836                CoordinateFrame::Ecliptic
837            } else {
838                CoordinateFrame::Equatorial
839            },
840            zodiac_mode: ZodiacMode::Tropical,
841            apparent: Apparentness::Mean,
842        });
843    }
844
845    Some((requests, entries))
846}
847
848/// Returns the packaged mixed-frame batch-parity request corpus used by downstream tooling.
849pub fn packaged_mixed_frame_batch_parity_requests() -> Option<Vec<EphemerisRequest>> {
850    packaged_mixed_frame_batch_parity_request_entries().map(|(requests, _)| requests)
851}
852
853/// This is a compatibility alias for [`packaged_mixed_frame_batch_parity_requests`].
854#[doc(alias = "packaged_mixed_frame_batch_parity_requests")]
855pub fn packaged_mixed_frame_batch_parity_request_corpus() -> Option<Vec<EphemerisRequest>> {
856    packaged_mixed_frame_batch_parity_requests()
857}
858
859/// Returns a compact mixed-frame batch-parity summary for the packaged artifact.
860pub fn packaged_mixed_frame_batch_parity_summary() -> Option<PackagedBatchParitySummary> {
861    let backend = packaged_backend();
862    let (requests, entries) = packaged_mixed_frame_batch_parity_request_entries()?;
863
864    let results = backend.positions(&requests).ok()?;
865    if results.len() != requests.len() {
866        return None;
867    }
868
869    let mut ecliptic_request_count = 0usize;
870    let mut equatorial_request_count = 0usize;
871    let mut exact_count = 0usize;
872    let mut interpolated_count = 0usize;
873    let mut approximate_count = 0usize;
874    let mut unknown_count = 0usize;
875    let mut order_preserved = true;
876    let mut single_query_parity = true;
877
878    for ((request, result), entry) in requests.iter().zip(results.iter()).zip(entries.iter()) {
879        let single = backend.position(request).ok();
880        single_query_parity &= single.as_ref().is_some_and(|single| single == result);
881
882        order_preserved &= result.body == entry.body
883            && result.instant == request.instant
884            && result.frame == request.frame
885            && result.zodiac_mode == request.zodiac_mode
886            && result.apparent == request.apparent;
887
888        match request.frame {
889            CoordinateFrame::Ecliptic => ecliptic_request_count += 1,
890            CoordinateFrame::Equatorial => equatorial_request_count += 1,
891            _ => return None,
892        }
893
894        match result.quality {
895            QualityAnnotation::Exact => exact_count += 1,
896            QualityAnnotation::Interpolated => interpolated_count += 1,
897            QualityAnnotation::Approximate => approximate_count += 1,
898            QualityAnnotation::Unknown => unknown_count += 1,
899            _ => unknown_count += 1,
900        }
901    }
902
903    Some(PackagedBatchParitySummary {
904        request_count: requests.len(),
905        body_count: entries.len(),
906        ecliptic_request_count,
907        equatorial_request_count,
908        exact_count,
909        interpolated_count,
910        approximate_count,
911        unknown_count,
912        order_preserved,
913        single_query_parity_preserved: single_query_parity,
914    })
915}
916
917impl PackagedBatchParitySummary {
918    /// Returns a compact summary line used in release-facing reporting.
919    pub fn summary_line(&self) -> String {
920        let order = if self.order_preserved {
921            "preserved"
922        } else {
923            "needs attention"
924        };
925        let parity = if self.single_query_parity_preserved {
926            "preserved"
927        } else {
928            "needs attention"
929        };
930        format!(
931            "Packaged mixed frame batch parity: {} requests across {} bodies, ecliptic requests={}, equatorial requests={}; quality counts: Exact={}, Interpolated={}, Approximate={}, Unknown={}; order={}, single-query parity={}",
932            self.request_count,
933            self.body_count,
934            self.ecliptic_request_count,
935            self.equatorial_request_count,
936            self.exact_count,
937            self.interpolated_count,
938            self.approximate_count,
939            self.unknown_count,
940            order,
941            parity,
942        )
943    }
944}
945
946impl fmt::Display for PackagedBatchParitySummary {
947    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
948        f.write_str(&self.summary_line())
949    }
950}
951
952fn format_validated_packaged_mixed_frame_batch_parity_summary_for_report(
953    summary: &PackagedBatchParitySummary,
954) -> String {
955    match summary.validated_summary_line() {
956        Ok(line) => line,
957        Err(error) => format!("Packaged mixed frame batch parity: unavailable ({error})"),
958    }
959}
960
961/// Returns the packaged mixed-frame batch-parity summary.
962pub fn packaged_mixed_frame_batch_parity_summary_for_report() -> String {
963    packaged_mixed_frame_batch_parity_summary()
964        .as_ref()
965        .map(format_validated_packaged_mixed_frame_batch_parity_summary_for_report)
966        .unwrap_or_else(|| "Packaged mixed frame batch parity: unavailable".to_string())
967}
968
969/// Returns the packaged frame-parity summary.
970pub fn packaged_frame_parity_summary_for_report() -> String {
971    packaged_mixed_frame_batch_parity_summary_for_report()
972}
973
974/// Structured mixed TT/TDB batch-parity summary for the packaged artifact.
975#[derive(Clone, Debug, PartialEq)]
976pub struct PackagedTimeScaleBatchParitySummary {
977    /// Number of requests in the mixed-scale batch regression.
978    pub request_count: usize,
979    /// Number of bodies covered by the batch regression.
980    pub body_count: usize,
981    /// Number of TT requests in the batch regression.
982    pub tt_request_count: usize,
983    /// Number of TDB requests in the batch regression.
984    pub tdb_request_count: usize,
985    /// Number of exact-quality results observed in the batch regression.
986    pub exact_count: usize,
987    /// Number of interpolated-quality results observed in the batch regression.
988    pub interpolated_count: usize,
989    /// Number of approximate-quality results observed in the batch regression.
990    pub approximate_count: usize,
991    /// Number of unknown-quality results observed in the batch regression.
992    pub unknown_count: usize,
993    /// Whether the batch regression preserved request order.
994    pub order_preserved: bool,
995    /// Whether the batch regression preserved batch/single parity.
996    pub single_query_parity_preserved: bool,
997}
998
999/// Validation error for a packaged mixed TT/TDB batch-parity summary.
1000#[derive(Clone, Debug, Eq, PartialEq)]
1001pub enum PackagedTimeScaleBatchParitySummaryValidationError {
1002    /// A summary field is out of sync with the current packaged batch-parity posture.
1003    FieldOutOfSync { field: &'static str },
1004}
1005
1006impl fmt::Display for PackagedTimeScaleBatchParitySummaryValidationError {
1007    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1008        match self {
1009            Self::FieldOutOfSync { field } => write!(
1010                f,
1011                "the packaged mixed TT/TDB batch-parity summary field `{field}` is out of sync with the current packaged posture"
1012            ),
1013        }
1014    }
1015}
1016
1017impl std::error::Error for PackagedTimeScaleBatchParitySummaryValidationError {}
1018
1019fn packaged_mixed_tt_tdb_batch_parity_request_entries(
1020) -> Option<(Vec<EphemerisRequest>, Vec<SnapshotEntry>)> {
1021    let snapshot = reference_snapshot();
1022    let mut requests = Vec::with_capacity(packaged_bodies().len());
1023    let mut entries = Vec::with_capacity(packaged_bodies().len());
1024
1025    for (index, body) in packaged_bodies().iter().cloned().enumerate() {
1026        let entry = packaged_reference_entry_for_body(snapshot, &body)?;
1027        entries.push(entry.clone());
1028        requests.push(EphemerisRequest {
1029            body,
1030            instant: Instant::new(
1031                entry.epoch.julian_day,
1032                if index % 2 == 0 {
1033                    TimeScale::Tt
1034                } else {
1035                    TimeScale::Tdb
1036                },
1037            ),
1038            observer: None,
1039            frame: CoordinateFrame::Ecliptic,
1040            zodiac_mode: ZodiacMode::Tropical,
1041            apparent: Apparentness::Mean,
1042        });
1043    }
1044
1045    Some((requests, entries))
1046}
1047
1048/// Returns the packaged mixed TT/TDB batch-parity request corpus used by downstream tooling.
1049pub fn packaged_mixed_tt_tdb_batch_parity_requests() -> Option<Vec<EphemerisRequest>> {
1050    packaged_mixed_tt_tdb_batch_parity_request_entries().map(|(requests, _)| requests)
1051}
1052
1053/// This is a compatibility alias for [`packaged_mixed_tt_tdb_batch_parity_requests`].
1054#[doc(alias = "packaged_mixed_tt_tdb_batch_parity_requests")]
1055pub fn packaged_mixed_tt_tdb_batch_parity_request_corpus() -> Option<Vec<EphemerisRequest>> {
1056    packaged_mixed_tt_tdb_batch_parity_requests()
1057}
1058
1059/// Returns a compact mixed TT/TDB batch-parity summary for the packaged artifact.
1060pub fn packaged_mixed_tt_tdb_batch_parity_summary() -> Option<PackagedTimeScaleBatchParitySummary> {
1061    let backend = packaged_backend();
1062    let (requests, entries) = packaged_mixed_tt_tdb_batch_parity_request_entries()?;
1063
1064    let results = backend.positions(&requests).ok()?;
1065    if results.len() != requests.len() {
1066        return None;
1067    }
1068
1069    let mut tt_request_count = 0usize;
1070    let mut tdb_request_count = 0usize;
1071    let mut exact_count = 0usize;
1072    let mut interpolated_count = 0usize;
1073    let mut approximate_count = 0usize;
1074    let mut unknown_count = 0usize;
1075    let mut order_preserved = true;
1076    let mut single_query_parity = true;
1077
1078    for ((request, result), entry) in requests.iter().zip(results.iter()).zip(entries.iter()) {
1079        let single = backend.position(request).ok();
1080        single_query_parity &= single.as_ref().is_some_and(|single| single == result);
1081
1082        order_preserved &= result.body == entry.body
1083            && result.instant == request.instant
1084            && result.frame == request.frame
1085            && result.zodiac_mode == request.zodiac_mode
1086            && result.apparent == request.apparent;
1087
1088        match request.instant.scale {
1089            TimeScale::Tt => tt_request_count += 1,
1090            TimeScale::Tdb => tdb_request_count += 1,
1091            _ => return None,
1092        }
1093
1094        match result.quality {
1095            QualityAnnotation::Exact => exact_count += 1,
1096            QualityAnnotation::Interpolated => interpolated_count += 1,
1097            QualityAnnotation::Approximate => approximate_count += 1,
1098            QualityAnnotation::Unknown => unknown_count += 1,
1099            _ => unknown_count += 1,
1100        }
1101    }
1102
1103    Some(PackagedTimeScaleBatchParitySummary {
1104        request_count: requests.len(),
1105        body_count: entries.len(),
1106        tt_request_count,
1107        tdb_request_count,
1108        exact_count,
1109        interpolated_count,
1110        approximate_count,
1111        unknown_count,
1112        order_preserved,
1113        single_query_parity_preserved: single_query_parity,
1114    })
1115}
1116
1117impl PackagedTimeScaleBatchParitySummary {
1118    /// Returns `Ok(())` when the summary still matches the current packaged batch-parity posture.
1119    pub fn validate(&self) -> Result<(), PackagedTimeScaleBatchParitySummaryValidationError> {
1120        if self.request_count != self.body_count {
1121            return Err(
1122                PackagedTimeScaleBatchParitySummaryValidationError::FieldOutOfSync {
1123                    field: "request_count/body_count",
1124                },
1125            );
1126        }
1127
1128        if self.tt_request_count + self.tdb_request_count != self.request_count {
1129            return Err(
1130                PackagedTimeScaleBatchParitySummaryValidationError::FieldOutOfSync {
1131                    field: "time_scale_counts",
1132                },
1133            );
1134        }
1135
1136        if self.tt_request_count == 0 || self.tdb_request_count == 0 {
1137            return Err(
1138                PackagedTimeScaleBatchParitySummaryValidationError::FieldOutOfSync {
1139                    field: "time_scale_mix",
1140                },
1141            );
1142        }
1143
1144        if self.exact_count + self.interpolated_count + self.approximate_count + self.unknown_count
1145            != self.request_count
1146        {
1147            return Err(
1148                PackagedTimeScaleBatchParitySummaryValidationError::FieldOutOfSync {
1149                    field: "quality_counts",
1150                },
1151            );
1152        }
1153
1154        if !self.order_preserved {
1155            return Err(
1156                PackagedTimeScaleBatchParitySummaryValidationError::FieldOutOfSync {
1157                    field: "order_preserved",
1158                },
1159            );
1160        }
1161
1162        if !self.single_query_parity_preserved {
1163            return Err(
1164                PackagedTimeScaleBatchParitySummaryValidationError::FieldOutOfSync {
1165                    field: "single_query_parity_preserved",
1166                },
1167            );
1168        }
1169
1170        Ok(())
1171    }
1172
1173    /// Returns the validated batch-parity summary line.
1174    pub fn validated_summary_line(
1175        &self,
1176    ) -> Result<String, PackagedTimeScaleBatchParitySummaryValidationError> {
1177        self.validate()?;
1178        Ok(self.summary_line())
1179    }
1180
1181    /// Returns a compact summary line used in release-facing reporting.
1182    pub fn summary_line(&self) -> String {
1183        let order = if self.order_preserved {
1184            "preserved"
1185        } else {
1186            "needs attention"
1187        };
1188        let parity = if self.single_query_parity_preserved {
1189            "preserved"
1190        } else {
1191            "needs attention"
1192        };
1193        format!(
1194            "Packaged mixed TT/TDB batch parity: {} requests across {} bodies, TT requests={}, TDB requests={}; quality counts: Exact={}, Interpolated={}, Approximate={}, Unknown={}; order={}, single-query parity={}",
1195            self.request_count,
1196            self.body_count,
1197            self.tt_request_count,
1198            self.tdb_request_count,
1199            self.exact_count,
1200            self.interpolated_count,
1201            self.approximate_count,
1202            self.unknown_count,
1203            order,
1204            parity,
1205        )
1206    }
1207}
1208
1209impl fmt::Display for PackagedTimeScaleBatchParitySummary {
1210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1211        f.write_str(&self.summary_line())
1212    }
1213}
1214
1215fn format_validated_packaged_mixed_tt_tdb_batch_parity_summary_for_report(
1216    summary: &PackagedTimeScaleBatchParitySummary,
1217) -> String {
1218    match summary.validated_summary_line() {
1219        Ok(line) => line,
1220        Err(error) => format!("Packaged mixed TT/TDB batch parity: unavailable ({error})"),
1221    }
1222}
1223
1224/// Returns the packaged mixed TT/TDB batch-parity summary.
1225pub fn packaged_mixed_tt_tdb_batch_parity_summary_for_report() -> String {
1226    packaged_mixed_tt_tdb_batch_parity_summary()
1227        .as_ref()
1228        .map(format_validated_packaged_mixed_tt_tdb_batch_parity_summary_for_report)
1229        .unwrap_or_else(|| "Packaged mixed TT/TDB batch parity: unavailable".to_string())
1230}
1231
1232/// Returns a packaged lookup for a body and instant.
1233///
1234/// # Examples
1235///
1236/// ```
1237/// use pleiades_backend::{CelestialBody, Instant, JulianDay, TimeScale};
1238/// use pleiades_data::packaged_lookup;
1239///
1240/// let instant = Instant::new(JulianDay::from_days(2_451_545.0), TimeScale::Tt);
1241/// let sun = packaged_lookup(&CelestialBody::Sun, instant)
1242///     .expect("Sun should be present in the packaged artifact");
1243///
1244/// assert!(sun.distance_au.is_some());
1245/// ```
1246pub fn packaged_lookup(
1247    body: &CelestialBody,
1248    instant: Instant,
1249) -> Result<EclipticCoordinates, pleiades_compression::CompressionError> {
1250    packaged_artifact().lookup_ecliptic(body, normalize_lookup_instant(instant))
1251}
1252
1253/// Returns a packaged-data backend instance.
1254///
1255/// # Examples
1256///
1257/// ```
1258/// use pleiades_backend::{CoordinateFrame, EphemerisBackend};
1259/// use pleiades_data::packaged_backend;
1260///
1261/// let backend = packaged_backend();
1262/// let metadata = backend.metadata();
1263///
1264/// assert_eq!(metadata.id.as_str(), "pleiades-data");
1265/// assert!(metadata.offline);
1266/// assert!(metadata.deterministic);
1267/// assert!(metadata.supported_frames.contains(&CoordinateFrame::Equatorial));
1268/// ```
1269pub fn packaged_backend() -> PackagedDataBackend {
1270    PackagedDataBackend::new()
1271}
1272
1273/// Returns a packaged-data backend built from an explicit artifact.
1274pub fn packaged_backend_from_artifact(artifact: CompressedArtifact) -> PackagedDataBackend {
1275    PackagedDataBackend::from_artifact(artifact)
1276}
1277
1278/// Returns a packaged-data backend built from decoded artifact bytes.
1279///
1280/// # Examples
1281///
1282/// ```
1283/// use pleiades_backend::EphemerisBackend;
1284/// use pleiades_data::{packaged_artifact, packaged_backend_from_bytes};
1285///
1286/// let bytes = packaged_artifact()
1287///     .encode()
1288///     .expect("packaged artifact should encode");
1289/// let backend = packaged_backend_from_bytes(&bytes)
1290///     .expect("packaged artifact bytes should decode");
1291///
1292/// assert_eq!(backend.metadata().id.as_str(), "pleiades-data");
1293/// assert!(backend.metadata().offline);
1294/// ```
1295pub fn packaged_backend_from_bytes(
1296    bytes: &[u8],
1297) -> Result<PackagedDataBackend, PackagedArtifactLoadError> {
1298    PackagedDataBackend::from_bytes(bytes)
1299}
1300
1301#[cfg(feature = "packaged-artifact-path")]
1302/// Returns a packaged-data backend built from a decoded artifact file.
1303///
1304/// # Examples
1305///
1306/// ```
1307/// use std::fs;
1308/// use pleiades_backend::EphemerisBackend;
1309/// use pleiades_data::{packaged_artifact, packaged_backend_from_path};
1310///
1311/// let bytes = packaged_artifact()
1312///     .encode()
1313///     .expect("packaged artifact should encode");
1314/// let path = std::env::temp_dir().join(format!(
1315///     "pleiades-packaged-artifact-{}.bin",
1316///     std::process::id()
1317/// ));
1318/// fs::write(&path, &bytes).expect("artifact fixture should be writable");
1319/// let backend = packaged_backend_from_path(&path)
1320///     .expect("packaged artifact file should decode");
1321/// assert_eq!(backend.metadata().id.as_str(), "pleiades-data");
1322/// let _ = fs::remove_file(&path);
1323/// ```
1324pub fn packaged_backend_from_path(
1325    path: impl AsRef<Path>,
1326) -> Result<PackagedDataBackend, PackagedArtifactLoadError> {
1327    PackagedDataBackend::from_path(path)
1328}