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