Skip to main content

pleiades_data/coverage/
profile.rs

1use super::*;
2
3/// Structured production-profile skeleton for the packaged artifact generator.
4#[derive(Clone, Debug, PartialEq)]
5pub struct PackagedArtifactProductionProfileSummary {
6    /// Stable identifier for the production-profile skeleton.
7    pub profile_id: &'static str,
8    /// Human-readable generation label.
9    pub label: &'static str,
10    /// Version of the packaged artifact format.
11    pub artifact_version: u16,
12    /// Covered time range for the packaged artifact.
13    pub time_range: TimeRange,
14    /// Provenance summary for the checked-in production-generation corpus.
15    pub source_provenance: String,
16    /// Bodies bundled into the packaged artifact.
17    pub body_coverage: PackagedBodyCoverageSummary,
18    /// Capability profile encoded by the packaged artifact.
19    pub artifact_profile: ArtifactProfile,
20    /// Output speed policy encoded by the packaged artifact.
21    pub speed_policy: pleiades_compression::SpeedPolicy,
22    /// Generation policy used to turn reference snapshots into segments.
23    pub generation_policy: PackagedArtifactGenerationPolicy,
24    /// Request policy encoded by the packaged artifact.
25    pub request_policy: PackagedRequestPolicySummary,
26    /// Lookup-epoch policy encoded by the packaged artifact.
27    pub lookup_epoch_policy: PackagedLookupEpochPolicy,
28    /// Frame-treatment policy encoded by the packaged artifact.
29    pub frame_treatment: PackagedFrameTreatmentSummary,
30    /// Storage/reconstruction policy encoded by the packaged artifact.
31    pub storage_summary: PackagedArtifactStorageSummary,
32    /// Release-facing statement about the packaged-artifact target thresholds.
33    pub target_thresholds: PackagedArtifactTargetThresholdSummary,
34}
35
36/// Validation error for a packaged artifact production-profile skeleton that drifted from the current posture.
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
38pub enum PackagedArtifactProductionProfileSummaryValidationError {
39    /// A summary field is out of sync with the current packaged-artifact posture.
40    FieldOutOfSync { field: &'static str },
41}
42
43impl fmt::Display for PackagedArtifactProductionProfileSummaryValidationError {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            Self::FieldOutOfSync { field } => write!(
47                f,
48                "the packaged artifact production profile summary field `{field}` is out of sync with the current posture"
49            ),
50        }
51    }
52}
53
54impl std::error::Error for PackagedArtifactProductionProfileSummaryValidationError {}
55
56impl PackagedArtifactProductionProfileSummary {
57    /// Returns the production-profile skeleton as a compact human-readable line.
58    pub fn summary_line(&self) -> String {
59        format!(
60            "Packaged artifact production profile draft: profile id={}; label={}; version={}; time range={}; source provenance={}; body coverage={}; artifact profile={}; output support={}; speed policy={}; generation policy={}; segment strategy={}; request policy={}; lookup epoch policy={}; frame treatment={}; storage/reconstruction={}; {}",
61            self.profile_id,
62            self.label,
63            self.artifact_version,
64            self.time_range,
65            self.source_provenance,
66            self.body_coverage,
67            self.artifact_profile,
68            self.artifact_profile.output_support_entries_summary_line(),
69            self.speed_policy,
70            self.generation_policy,
71            self.generation_policy.segment_strategy(),
72            self.request_policy,
73            self.lookup_epoch_policy.summary_line(),
74            self.frame_treatment,
75            self.storage_summary,
76            self.target_thresholds,
77        )
78    }
79
80    /// Returns `Ok(())` when the production-profile skeleton still matches the current packaged-artifact posture.
81    pub fn validate(&self) -> Result<(), PackagedArtifactProductionProfileSummaryValidationError> {
82        if self.profile_id != ARTIFACT_PROFILE_ID {
83            return Err(
84                PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
85                    field: "profile_id",
86                },
87            );
88        }
89        if self.label != ARTIFACT_LABEL {
90            return Err(
91                PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
92                    field: "label",
93                },
94            );
95        }
96        if self.artifact_version != packaged_artifact().header.version {
97            return Err(
98                PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
99                    field: "artifact_version",
100                },
101            );
102        }
103        if self.time_range != artifact_time_range(packaged_artifact()) {
104            return Err(
105                PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
106                    field: "time_range",
107                },
108            );
109        }
110        if self.source_provenance != production_generation_source_summary_for_report() {
111            return Err(
112                PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
113                    field: "source_provenance",
114                },
115            );
116        }
117        self.body_coverage.validate().map_err(|_| {
118            PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
119                field: "body_coverage",
120            }
121        })?;
122        if self.artifact_profile != packaged_artifact_profile_summary_details().profile {
123            return Err(
124                PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
125                    field: "artifact_profile",
126                },
127            );
128        }
129        if self.speed_policy != self.artifact_profile.speed_policy {
130            return Err(
131                PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
132                    field: "speed_policy",
133                },
134            );
135        }
136        self.generation_policy.validate().map_err(|_| {
137            PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
138                field: "generation_policy",
139            }
140        })?;
141        self.request_policy.validate().map_err(|_| {
142            PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
143                field: "request_policy",
144            }
145        })?;
146        if self.lookup_epoch_policy != packaged_lookup_epoch_policy_summary_details().policy {
147            return Err(
148                PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
149                    field: "lookup_epoch_policy",
150                },
151            );
152        }
153        self.frame_treatment.validate().map_err(|_| {
154            PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
155                field: "frame_treatment",
156            }
157        })?;
158        self.storage_summary.validate().map_err(|_| {
159            PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
160                field: "storage_summary",
161            }
162        })?;
163        self.target_thresholds.validate().map_err(|_| {
164            PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
165                field: "target_thresholds",
166            }
167        })?;
168
169        Ok(())
170    }
171
172    /// Returns the validated production-profile skeleton summary line.
173    pub fn validated_summary_line(
174        &self,
175    ) -> Result<String, PackagedArtifactProductionProfileSummaryValidationError> {
176        self.validate()?;
177        Ok(self.summary_line())
178    }
179}
180
181impl fmt::Display for PackagedArtifactProductionProfileSummary {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        f.write_str(&self.summary_line())
184    }
185}
186
187/// Returns the current packaged-artifact production-profile skeleton.
188pub fn packaged_artifact_production_profile_summary_details(
189) -> PackagedArtifactProductionProfileSummary {
190    let artifact = packaged_artifact();
191    let profile_summary = packaged_artifact_profile_summary_details();
192    let speed_policy = profile_summary.profile.speed_policy;
193    let summary = PackagedArtifactProductionProfileSummary {
194        profile_id: ARTIFACT_PROFILE_ID,
195        label: ARTIFACT_LABEL,
196        artifact_version: artifact.header.version,
197        time_range: artifact_time_range(artifact),
198        source_provenance: production_generation_source_summary_for_report(),
199        body_coverage: packaged_body_coverage_summary_details(),
200        artifact_profile: profile_summary.profile,
201        speed_policy,
202        generation_policy: PackagedArtifactGenerationPolicy::AdjacentSameBodyQuadraticWindows,
203        request_policy: packaged_request_policy_summary_details(),
204        lookup_epoch_policy: packaged_lookup_epoch_policy_summary_details().policy,
205        frame_treatment: packaged_frame_treatment_summary_details(),
206        storage_summary: packaged_artifact_storage_summary_details(),
207        target_thresholds: packaged_artifact_target_threshold_summary_details(),
208    };
209    debug_assert!(summary.validate().is_ok());
210    summary
211}
212
213/// Returns the current packaged-artifact production-profile draft summary.
214pub fn packaged_artifact_production_profile_summary() -> &'static str {
215    static SUMMARY: OnceLock<String> = OnceLock::new();
216    SUMMARY
217        .get_or_init(|| {
218            let summary = packaged_artifact_production_profile_summary_details();
219            match summary.validated_summary_line() {
220                Ok(rendered) => rendered,
221                Err(error) => {
222                    format!("Packaged artifact production profile draft: unavailable ({error})")
223                }
224            }
225        })
226        .as_str()
227}
228
229/// Structured generation parameters for the packaged artifact generator.
230#[derive(Clone, Debug, PartialEq)]
231pub struct PackagedArtifactGeneratorParameters {
232    /// Stable identifier for the generation profile.
233    pub profile_id: &'static str,
234    /// Human-readable generation label.
235    pub label: &'static str,
236    /// Version of the packaged artifact format.
237    pub artifact_version: u16,
238    /// Covered time range for the packaged artifact.
239    pub time_range: TimeRange,
240    /// Provenance summary for the checked-in production-generation corpus.
241    pub source_provenance: String,
242    /// Deterministic checksum of the checked-in packaged artifact.
243    pub checksum: u64,
244    /// Encoded size of the checked-in packaged artifact in bytes.
245    pub artifact_size_bytes: usize,
246    /// Bodies bundled into the packaged artifact.
247    pub body_coverage: PackagedBodyCoverageSummary,
248    /// Residual-bearing body coverage encoded by the packaged artifact.
249    pub residual_body_coverage: ArtifactResidualBodyCoverageSummary,
250    /// Capability profile encoded by the packaged artifact.
251    pub artifact_profile: ArtifactProfile,
252    /// Output speed policy encoded by the packaged artifact.
253    pub speed_policy: pleiades_compression::SpeedPolicy,
254    /// Generation policy used to turn reference snapshots into segments.
255    pub generation_policy: PackagedArtifactGenerationPolicy,
256    /// Request policy encoded by the packaged artifact.
257    pub request_policy: PackagedRequestPolicySummary,
258    /// Lookup-epoch policy encoded by the packaged artifact.
259    pub lookup_epoch_policy: PackagedLookupEpochPolicy,
260    /// Frame-treatment policy encoded by the packaged artifact.
261    pub frame_treatment: PackagedFrameTreatmentSummary,
262    /// Storage/reconstruction policy encoded by the packaged artifact.
263    pub storage_summary: PackagedArtifactStorageSummary,
264    /// Release-facing statement about the packaged-artifact target thresholds.
265    pub target_thresholds: PackagedArtifactTargetThresholdSummary,
266}
267
268impl PackagedArtifactGeneratorParameters {
269    /// Returns the generator parameters as a compact human-readable line.
270    pub fn summary_line(&self) -> String {
271        format!(
272            "Packaged artifact generator parameters: profile id={}; label={}; version={}; time range={}; source provenance={}; checksum=0x{:016x}; artifact size={} bytes; body coverage={}; residual bodies={}; artifact profile={}; output support={}; speed policy={}; generation policy={}; segment strategy={}; request policy={}; lookup epoch policy={}; frame treatment={}; storage/reconstruction={}; {}",
273            self.profile_id,
274            self.label,
275            self.artifact_version,
276            self.time_range,
277            self.source_provenance,
278            self.checksum,
279            self.artifact_size_bytes,
280            self.body_coverage,
281            self.residual_body_coverage.summary_line_with_body_count(),
282            self.artifact_profile,
283            self.artifact_profile.output_support_entries_summary_line(),
284            self.speed_policy,
285            self.generation_policy,
286            self.generation_policy.segment_strategy(),
287            self.request_policy,
288            self.lookup_epoch_policy.summary_line(),
289            self.frame_treatment,
290            self.storage_summary,
291            self.target_thresholds,
292        )
293    }
294
295    /// Returns `Ok(())` when the parameters still match the current packaged-artifact posture.
296    pub fn validate(&self) -> Result<(), pleiades_compression::CompressionError> {
297        let current = packaged_artifact_production_profile_summary_details();
298        let artifact = packaged_artifact();
299
300        if self.profile_id != current.profile_id {
301            return Err(pleiades_compression::CompressionError::new(
302                pleiades_compression::CompressionErrorKind::InvalidFormat,
303                "packaged artifact generator parameters profile id does not match the current production profile",
304            ));
305        }
306        if self.label != current.label {
307            return Err(pleiades_compression::CompressionError::new(
308                pleiades_compression::CompressionErrorKind::InvalidFormat,
309                "packaged artifact generator parameters label does not match the current production profile",
310            ));
311        }
312        if self.artifact_version != current.artifact_version {
313            return Err(pleiades_compression::CompressionError::new(
314                pleiades_compression::CompressionErrorKind::InvalidFormat,
315                "packaged artifact generator parameters version does not match the current production profile",
316            ));
317        }
318        if self.time_range != current.time_range {
319            return Err(pleiades_compression::CompressionError::new(
320                pleiades_compression::CompressionErrorKind::InvalidFormat,
321                "packaged artifact generator parameters time range does not match the current production profile",
322            ));
323        }
324        if self.source_provenance != current.source_provenance {
325            return Err(pleiades_compression::CompressionError::new(
326                pleiades_compression::CompressionErrorKind::InvalidFormat,
327                "packaged artifact generator parameters source provenance does not match the current production profile",
328            ));
329        }
330        if self.checksum != artifact.checksum {
331            return Err(pleiades_compression::CompressionError::new(
332                pleiades_compression::CompressionErrorKind::InvalidFormat,
333                "packaged artifact generator parameters checksum does not match the current packaged artifact",
334            ));
335        }
336        let expected_artifact_size_bytes = packaged_artifact_encoded_bytes(artifact);
337        if self.artifact_size_bytes != expected_artifact_size_bytes {
338            return Err(pleiades_compression::CompressionError::new(
339                pleiades_compression::CompressionErrorKind::InvalidFormat,
340                format!(
341                    "packaged artifact generator parameters artifact size {} bytes does not match the current packaged artifact size {} bytes",
342                    self.artifact_size_bytes,
343                    expected_artifact_size_bytes
344                ),
345            ));
346        }
347        if self.body_coverage != current.body_coverage {
348            return Err(pleiades_compression::CompressionError::new(
349                pleiades_compression::CompressionErrorKind::InvalidFormat,
350                "packaged artifact generator parameters body coverage does not match the current production profile",
351            ));
352        }
353        if self.residual_body_coverage != artifact.residual_body_coverage_summary() {
354            return Err(pleiades_compression::CompressionError::new(
355                pleiades_compression::CompressionErrorKind::InvalidFormat,
356                "packaged artifact generator parameters residual body coverage does not match the current packaged artifact",
357            ));
358        }
359        if self.artifact_profile != current.artifact_profile {
360            return Err(pleiades_compression::CompressionError::new(
361                pleiades_compression::CompressionErrorKind::InvalidFormat,
362                "packaged artifact generator parameters artifact profile does not match the current production profile",
363            ));
364        }
365        if self.speed_policy != current.speed_policy {
366            return Err(pleiades_compression::CompressionError::new(
367                pleiades_compression::CompressionErrorKind::InvalidFormat,
368                "packaged artifact generator parameters speed policy does not match the current production profile",
369            ));
370        }
371        if self.generation_policy != current.generation_policy {
372            return Err(pleiades_compression::CompressionError::new(
373                pleiades_compression::CompressionErrorKind::InvalidFormat,
374                "packaged artifact generator parameters generation policy does not match the current production profile",
375            ));
376        }
377        if self.request_policy != current.request_policy {
378            return Err(pleiades_compression::CompressionError::new(
379                pleiades_compression::CompressionErrorKind::InvalidFormat,
380                "packaged artifact generator parameters request policy does not match the current production profile",
381            ));
382        }
383        if self.lookup_epoch_policy != current.lookup_epoch_policy {
384            return Err(pleiades_compression::CompressionError::new(
385                pleiades_compression::CompressionErrorKind::InvalidFormat,
386                "packaged artifact generator parameters lookup epoch policy does not match the current production profile",
387            ));
388        }
389        if self.frame_treatment != current.frame_treatment {
390            return Err(pleiades_compression::CompressionError::new(
391                pleiades_compression::CompressionErrorKind::InvalidFormat,
392                "packaged artifact generator parameters frame-treatment policy does not match the current production profile",
393            ));
394        }
395        if self.storage_summary != current.storage_summary {
396            return Err(pleiades_compression::CompressionError::new(
397                pleiades_compression::CompressionErrorKind::InvalidFormat,
398                "packaged artifact generator parameters storage summary does not match the current production profile",
399            ));
400        }
401        self.target_thresholds
402            .state
403            .validate_production_ready()
404            .map_err(|_| {
405                pleiades_compression::CompressionError::new(
406                    pleiades_compression::CompressionErrorKind::InvalidFormat,
407                    "packaged artifact generator parameters target thresholds do not match the current production profile",
408                )
409            })?;
410        if self.target_thresholds != current.target_thresholds {
411            return Err(pleiades_compression::CompressionError::new(
412                pleiades_compression::CompressionErrorKind::InvalidFormat,
413                "packaged artifact generator parameters target thresholds do not match the current production profile",
414            ));
415        }
416
417        Ok(())
418    }
419
420    /// Returns the validated generator parameters summary line.
421    pub fn validated_summary_line(&self) -> Result<String, pleiades_compression::CompressionError> {
422        self.validate()?;
423        Ok(self.summary_line())
424    }
425}
426
427impl fmt::Display for PackagedArtifactGeneratorParameters {
428    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
429        f.write_str(&self.summary_line())
430    }
431}
432
433pub(crate) fn fnv1a64(bytes: &[u8]) -> u64 {
434    const OFFSET_BASIS: u64 = 0xcbf29ce484222325;
435    const PRIME: u64 = 0x100000001b3;
436
437    let mut hash = OFFSET_BASIS;
438    for byte in bytes {
439        hash ^= u64::from(*byte);
440        hash = hash.wrapping_mul(PRIME);
441    }
442    hash
443}
444
445fn packaged_artifact_generation_manifest_checksum(
446    parameters: &PackagedArtifactGeneratorParameters,
447    regeneration: &PackagedArtifactRegenerationSummary,
448) -> u64 {
449    let mut bytes = Vec::new();
450    bytes.extend_from_slice(parameters.summary_line().as_bytes());
451    bytes.push(b'\n');
452    bytes.extend_from_slice(regeneration.summary_line().as_bytes());
453    fnv1a64(&bytes)
454}
455
456/// Structured deterministic manifest for the packaged artifact generator.
457#[derive(Clone, Debug, PartialEq)]
458pub struct PackagedArtifactGenerationManifest {
459    /// Generator parameters used to produce the packaged artifact.
460    pub parameters: PackagedArtifactGeneratorParameters,
461    /// Regeneration provenance anchored to the checked-in artifact and source snapshot.
462    pub regeneration: PackagedArtifactRegenerationSummary,
463    /// Deterministic checksum of the rendered manifest content.
464    pub manifest_checksum: u64,
465}
466
467impl PackagedArtifactGenerationManifest {
468    /// Returns the deterministic manifest as a compact human-readable line.
469    pub fn summary_line(&self) -> String {
470        format!(
471            "Packaged artifact generation manifest: manifest checksum=0x{:016x}; {}; regeneration={}",
472            self.manifest_checksum, self.parameters, self.regeneration,
473        )
474    }
475
476    /// Returns `Ok(())` when the manifest still matches the current packaged-artifact posture.
477    pub fn validate(&self) -> Result<(), pleiades_compression::CompressionError> {
478        self.parameters.validate()?;
479        self.regeneration.validate()?;
480
481        let expected_checksum =
482            packaged_artifact_generation_manifest_checksum(&self.parameters, &self.regeneration);
483        if self.manifest_checksum != expected_checksum {
484            return Err(pleiades_compression::CompressionError::new(
485                pleiades_compression::CompressionErrorKind::InvalidFormat,
486                format!(
487                    "packaged artifact generation manifest checksum 0x{:016x} does not match the current packaged-artifact manifest checksum 0x{:016x}",
488                    self.manifest_checksum,
489                    expected_checksum
490                ),
491            ));
492        }
493
494        Ok(())
495    }
496
497    /// Returns the validated manifest summary line.
498    pub fn validated_summary_line(&self) -> Result<String, pleiades_compression::CompressionError> {
499        self.validate()?;
500        Ok(self.summary_line())
501    }
502}
503
504impl fmt::Display for PackagedArtifactGenerationManifest {
505    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
506        f.write_str(&self.summary_line())
507    }
508}
509
510/// Returns the current packaged-artifact generator parameters.
511pub fn packaged_artifact_generator_parameters_details() -> PackagedArtifactGeneratorParameters {
512    let summary = packaged_artifact_production_profile_summary_details();
513    let regeneration = packaged_artifact_regeneration_summary_details();
514    let parameters = PackagedArtifactGeneratorParameters {
515        profile_id: summary.profile_id,
516        label: summary.label,
517        artifact_version: summary.artifact_version,
518        time_range: summary.time_range,
519        source_provenance: summary.source_provenance,
520        checksum: regeneration.checksum,
521        artifact_size_bytes: regeneration.artifact_size_bytes,
522        body_coverage: summary.body_coverage,
523        residual_body_coverage: regeneration.residual_body_coverage_summary(),
524        artifact_profile: summary.artifact_profile,
525        speed_policy: summary.speed_policy,
526        generation_policy: summary.generation_policy,
527        request_policy: summary.request_policy,
528        lookup_epoch_policy: summary.lookup_epoch_policy,
529        frame_treatment: summary.frame_treatment,
530        storage_summary: summary.storage_summary,
531        target_thresholds: summary.target_thresholds,
532    };
533    debug_assert!(parameters.validate().is_ok());
534    parameters
535}
536
537/// Returns the current deterministic packaged-artifact generation manifest.
538pub fn packaged_artifact_generation_manifest_details() -> PackagedArtifactGenerationManifest {
539    let parameters = packaged_artifact_generator_parameters_details();
540    let regeneration = packaged_artifact_regeneration_summary_details();
541    let manifest = PackagedArtifactGenerationManifest {
542        manifest_checksum: packaged_artifact_generation_manifest_checksum(
543            &parameters,
544            &regeneration,
545        ),
546        parameters,
547        regeneration,
548    };
549    debug_assert!(manifest.validate().is_ok());
550    manifest
551}
552
553/// Returns the current deterministic packaged-artifact generation manifest.
554pub fn packaged_artifact_generation_manifest() -> &'static str {
555    static SUMMARY: OnceLock<String> = OnceLock::new();
556    SUMMARY
557        .get_or_init(|| {
558            let manifest = packaged_artifact_generation_manifest_details();
559            match manifest.validated_summary_line() {
560                Ok(rendered) => rendered,
561                Err(error) => {
562                    format!("Packaged artifact generation manifest: unavailable ({error})")
563                }
564            }
565        })
566        .as_str()
567}
568
569#[derive(Clone, Debug, PartialEq, Eq)]
570pub struct PackagedArtifactProfileSummary {
571    /// Number of bundled bodies that share the packaged artifact profile.
572    pub body_count: usize,
573    /// Bodies bundled under the packaged artifact profile.
574    pub bodies: Vec<CelestialBody>,
575    /// Byte-order policy encoded by the packaged artifact.
576    pub endian_policy: EndianPolicy,
577    /// Capability profile encoded by the packaged artifact.
578    pub profile: ArtifactProfile,
579}
580
581impl PackagedArtifactProfileSummary {
582    /// Returns the packaged artifact profile coverage as a typed summary.
583    pub fn profile_coverage_summary(&self) -> ArtifactProfileCoverageSummary {
584        ArtifactProfileCoverageSummary::new(self.profile.clone(), self.bodies.clone())
585    }
586
587    /// Validates that the packaged artifact profile summary is internally
588    /// consistent with its bundled body list, byte-order policy, and embedded capability profile.
589    pub fn validate(&self) -> Result<(), pleiades_compression::CompressionError> {
590        let artifact = packaged_artifact();
591        if self.endian_policy != artifact.header.endian_policy {
592            return Err(pleiades_compression::CompressionError::new(
593                pleiades_compression::CompressionErrorKind::InvalidFormat,
594                "packaged artifact profile byte-order policy does not match the checked-in packaged artifact header",
595            ));
596        }
597        if self.profile != artifact.profile_coverage_summary().profile {
598            return Err(pleiades_compression::CompressionError::new(
599                pleiades_compression::CompressionErrorKind::InvalidFormat,
600                "packaged artifact profile metadata does not match the checked-in packaged artifact profile",
601            ));
602        }
603
604        if self.body_count != self.bodies.len() {
605            return Err(pleiades_compression::CompressionError::new(
606                pleiades_compression::CompressionErrorKind::InvalidFormat,
607                "packaged artifact profile body count does not match bundled body list",
608            ));
609        }
610
611        if self.bodies.is_empty() {
612            let coverage = self.profile_coverage_summary();
613            coverage.validate()?;
614            return Ok(());
615        }
616
617        if self
618            .bodies
619            .iter()
620            .enumerate()
621            .any(|(index, body)| self.bodies[..index].contains(body))
622        {
623            let coverage = self.profile_coverage_summary();
624            coverage.validate()?;
625            return Ok(());
626        }
627
628        if self.bodies.as_slice() != packaged_bodies() {
629            return Err(pleiades_compression::CompressionError::new(
630                pleiades_compression::CompressionErrorKind::InvalidFormat,
631                format!(
632                    "packaged artifact profile bundled body list does not match the checked-in packaged body set: expected [{}]; got [{}]",
633                    join_display(packaged_bodies()),
634                    join_display(&self.bodies)
635                ),
636            ));
637        }
638
639        let coverage = self.profile_coverage_summary();
640        coverage.validate()?;
641
642        Ok(())
643    }
644
645    /// Returns the validated packaged artifact profile summary line.
646    pub fn validated_summary_line(&self) -> Result<String, pleiades_compression::CompressionError> {
647        self.validate()?;
648        Ok(self.summary_line())
649    }
650
651    /// Returns the validated packaged artifact profile summary line with bundled bodies.
652    pub fn validated_summary_line_with_bodies(
653        &self,
654    ) -> Result<String, pleiades_compression::CompressionError> {
655        self.validate()?;
656        Ok(self.summary_line_with_bodies())
657    }
658
659    /// Returns the validated packaged artifact profile summary line with output support.
660    pub fn validated_summary_line_with_output_support(
661        &self,
662    ) -> Result<String, pleiades_compression::CompressionError> {
663        self.validate()?;
664        Ok(format!(
665            "{}; output support: {}",
666            self.summary_line_with_bodies(),
667            self.profile.validated_output_support_summary_line()?
668        ))
669    }
670
671    /// Renders the packaged artifact profile into a release-facing summary line.
672    pub fn summary_line(&self) -> String {
673        let coverage = self.profile_coverage_summary();
674        format!(
675            "byte order: {}; {}",
676            self.endian_policy,
677            coverage.summary_line()
678        )
679    }
680
681    /// Returns the packaged artifact profile's output-support summary line.
682    pub fn output_support_summary_line(&self) -> String {
683        self.profile.output_support_summary_line()
684    }
685
686    /// Renders the packaged artifact profile with its bundled body list.
687    pub fn summary_line_with_bodies(&self) -> String {
688        let coverage = self.profile_coverage_summary();
689        format!(
690            "byte order: {}; {}",
691            self.endian_policy,
692            coverage.summary_line_with_bodies(),
693        )
694    }
695
696    /// Renders the packaged artifact profile together with the built-in output
697    /// support posture used by the current packaged artifact.
698    pub fn summary_line_with_output_support(&self) -> String {
699        format!(
700            "{}; output support: {}",
701            self.summary_line_with_bodies(),
702            self.output_support_summary_line()
703        )
704    }
705}
706
707impl fmt::Display for PackagedArtifactProfileSummary {
708    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
709        f.write_str(&self.summary_line())
710    }
711}
712
713/// Returns the current packaged-artifact profile summary record.
714pub fn packaged_artifact_profile_summary_details() -> PackagedArtifactProfileSummary {
715    let artifact = packaged_artifact();
716    let coverage = artifact.profile_coverage_summary();
717    let summary = PackagedArtifactProfileSummary {
718        body_count: coverage.body_count,
719        bodies: coverage.bodies,
720        endian_policy: artifact.header.endian_policy,
721        profile: coverage.profile,
722    };
723    debug_assert!(summary.validate().is_ok());
724    summary
725}
726
727/// Returns the current packaged-artifact profile coverage summary record.
728pub fn packaged_artifact_profile_coverage_summary_details() -> ArtifactProfileCoverageSummary {
729    packaged_artifact_profile_summary_details().profile_coverage_summary()
730}
731
732/// Returns the current packaged-artifact profile summary.
733///
734/// The summary is validated before it is rendered so release-facing callers
735/// see an explicit unavailable marker if the bundled profile metadata drifts.
736pub fn packaged_artifact_profile_summary() -> String {
737    render_packaged_artifact_profile_summary(&packaged_artifact_profile_summary_details(), false)
738}
739
740/// Returns the current packaged-artifact profile summary with bundled body coverage.
741///
742/// The summary is validated before it is rendered so release-facing callers
743/// see an explicit unavailable marker if the bundled profile metadata drifts.
744pub fn packaged_artifact_profile_summary_with_body_coverage() -> String {
745    render_packaged_artifact_profile_summary(&packaged_artifact_profile_summary_details(), true)
746}
747
748/// Returns the current packaged-artifact profile summary with the output-support posture.
749///
750/// The summary is validated before it is rendered so release-facing callers
751/// see an explicit unavailable marker if the bundled profile metadata drifts.
752pub fn packaged_artifact_profile_summary_with_output_support() -> String {
753    let summary = packaged_artifact_profile_summary_details();
754    match summary.validated_summary_line_with_output_support() {
755        Ok(line) => line,
756        Err(error) => {
757            format!("Packaged artifact profile with output support: unavailable ({error})")
758        }
759    }
760}
761
762/// Structured output-support semantics for the packaged artifact profile.
763#[derive(Clone, Debug, PartialEq, Eq)]
764pub struct PackagedArtifactOutputSupportSummary {
765    /// Capability profile encoded by the packaged artifact.
766    pub profile: ArtifactProfile,
767}
768
769fn validate_packaged_artifact_output_support_profile(
770    profile: &ArtifactProfile,
771) -> Result<(), pleiades_compression::CompressionError> {
772    let expected_states = [
773        (
774            ArtifactOutput::EclipticCoordinates,
775            ArtifactOutputSupport::Derived,
776        ),
777        (
778            ArtifactOutput::EquatorialCoordinates,
779            ArtifactOutputSupport::Derived,
780        ),
781        (
782            ArtifactOutput::ApparentCorrections,
783            ArtifactOutputSupport::Unsupported,
784        ),
785        (
786            ArtifactOutput::TopocentricCoordinates,
787            ArtifactOutputSupport::Unsupported,
788        ),
789        (
790            ArtifactOutput::SiderealCoordinates,
791            ArtifactOutputSupport::Unsupported,
792        ),
793        (ArtifactOutput::Motion, ArtifactOutputSupport::Derived),
794    ];
795
796    for (output, expected_support) in expected_states {
797        let actual_support = profile.output_support(output);
798        if actual_support != expected_support {
799            return Err(pleiades_compression::CompressionError::new(
800                pleiades_compression::CompressionErrorKind::InvalidFormat,
801                format!(
802                    "packaged artifact output support summary is out of sync with the bundled artifact profile field `output_support[{output}]`: expected {expected_support}, found {actual_support}"
803                ),
804            ));
805        }
806    }
807
808    Ok(())
809}
810
811impl PackagedArtifactOutputSupportSummary {
812    /// Validates that the embedded artifact profile is internally consistent
813    /// and still advertises the packaged artifact's current built-in output
814    /// support posture.
815    pub fn validate(&self) -> Result<(), pleiades_compression::CompressionError> {
816        self.profile.validate()?;
817        validate_packaged_artifact_output_support_profile(&self.profile)
818    }
819
820    /// Returns the validated output-support posture for the packaged artifact profile.
821    pub fn validated_summary_line(&self) -> Result<String, pleiades_compression::CompressionError> {
822        self.validate()?;
823        Ok(self.summary_line())
824    }
825
826    /// Renders the packaged artifact profile's output-support semantics.
827    pub fn summary_line(&self) -> String {
828        self.profile.output_support_summary_line()
829    }
830}
831
832impl fmt::Display for PackagedArtifactOutputSupportSummary {
833    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
834        f.write_str(&self.summary_line())
835    }
836}
837
838/// Returns the current packaged-artifact output-support summary record.
839pub fn packaged_artifact_output_support_summary_details() -> PackagedArtifactOutputSupportSummary {
840    let summary = PackagedArtifactOutputSupportSummary {
841        profile: packaged_artifact_profile_summary_details().profile,
842    };
843    debug_assert!(summary.validate().is_ok());
844    summary
845}
846
847/// Structured speed-policy semantics for the packaged artifact profile.
848#[derive(Clone, Copy, Debug, PartialEq, Eq)]
849pub struct PackagedArtifactSpeedPolicySummary {
850    /// Speed policy encoded by the packaged artifact.
851    pub policy: SpeedPolicy,
852}
853
854/// Validation error for the packaged-data speed-policy summary.
855#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
856pub enum PackagedArtifactSpeedPolicySummaryValidationError {
857    /// A summary field is out of sync with the current packaged-data posture.
858    FieldOutOfSync { field: &'static str },
859}
860
861impl fmt::Display for PackagedArtifactSpeedPolicySummaryValidationError {
862    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
863        match self {
864            Self::FieldOutOfSync { field } => write!(
865                f,
866                "the packaged artifact speed-policy summary field `{field}` is out of sync with the current posture"
867            ),
868        }
869    }
870}
871
872impl std::error::Error for PackagedArtifactSpeedPolicySummaryValidationError {}
873
874impl PackagedArtifactSpeedPolicySummary {
875    /// Returns the packaged artifact speed policy as a compact human-readable line.
876    pub fn summary_line(self) -> String {
877        format!(
878            "{}; motion output support={}",
879            self.policy,
880            self.policy.motion_output_support()
881        )
882    }
883
884    /// Returns `Ok(())` when the summary still matches the current packaged-data posture.
885    pub fn validate(&self) -> Result<(), PackagedArtifactSpeedPolicySummaryValidationError> {
886        let current_policy = packaged_artifact_profile_summary_details()
887            .profile
888            .speed_policy;
889        if self.policy != current_policy {
890            return Err(
891                PackagedArtifactSpeedPolicySummaryValidationError::FieldOutOfSync {
892                    field: "policy",
893                },
894            );
895        }
896        Ok(())
897    }
898
899    /// Returns the validated packaged artifact speed-policy summary line.
900    pub fn validated_summary_line(
901        &self,
902    ) -> Result<String, PackagedArtifactSpeedPolicySummaryValidationError> {
903        self.validate()?;
904        Ok(self.summary_line())
905    }
906}
907
908impl fmt::Display for PackagedArtifactSpeedPolicySummary {
909    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
910        f.write_str(&self.summary_line())
911    }
912}
913
914const PACKAGED_ARTIFACT_SPEED_POLICY_SUMMARY: PackagedArtifactSpeedPolicySummary =
915    PackagedArtifactSpeedPolicySummary {
916        policy: SpeedPolicy::FittedDerivative,
917    };
918
919/// Returns the current packaged-artifact speed-policy summary record.
920pub fn packaged_artifact_speed_policy_summary_details() -> PackagedArtifactSpeedPolicySummary {
921    let summary = PACKAGED_ARTIFACT_SPEED_POLICY_SUMMARY;
922    debug_assert!(summary.validate().is_ok());
923    summary
924}
925
926pub(crate) fn render_packaged_artifact_profile_summary(
927    summary: &PackagedArtifactProfileSummary,
928    with_bodies: bool,
929) -> String {
930    if with_bodies {
931        match summary.validated_summary_line_with_bodies() {
932            Ok(line) => line,
933            Err(error) => {
934                format!("Packaged artifact profile with bundled bodies: unavailable ({error})")
935            }
936        }
937    } else {
938        match summary.validated_summary_line() {
939            Ok(line) => line,
940            Err(error) => format!("Packaged artifact profile: unavailable ({error})"),
941        }
942    }
943}