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 after validating the structured posture.
214pub fn packaged_artifact_production_profile_summary_for_report() -> String {
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(line) => line,
221                Err(error) => {
222                    format!("Packaged artifact production profile draft: unavailable ({error})")
223                }
224            }
225        })
226        .clone()
227}
228
229/// Returns the current packaged-artifact production-profile draft summary.
230pub fn packaged_artifact_production_profile_summary() -> &'static str {
231    static SUMMARY: OnceLock<String> = OnceLock::new();
232    SUMMARY
233        .get_or_init(|| {
234            let summary = packaged_artifact_production_profile_summary_details();
235            match summary.validated_summary_line() {
236                Ok(rendered) => rendered,
237                Err(error) => {
238                    format!("Packaged artifact production profile draft: unavailable ({error})")
239                }
240            }
241        })
242        .as_str()
243}
244
245/// Structured generation parameters for the packaged artifact generator.
246#[derive(Clone, Debug, PartialEq)]
247pub struct PackagedArtifactGeneratorParameters {
248    /// Stable identifier for the generation profile.
249    pub profile_id: &'static str,
250    /// Human-readable generation label.
251    pub label: &'static str,
252    /// Version of the packaged artifact format.
253    pub artifact_version: u16,
254    /// Covered time range for the packaged artifact.
255    pub time_range: TimeRange,
256    /// Provenance summary for the checked-in production-generation corpus.
257    pub source_provenance: String,
258    /// Deterministic checksum of the checked-in packaged artifact.
259    pub checksum: u64,
260    /// Encoded size of the checked-in packaged artifact in bytes.
261    pub artifact_size_bytes: usize,
262    /// Bodies bundled into the packaged artifact.
263    pub body_coverage: PackagedBodyCoverageSummary,
264    /// Residual-bearing body coverage encoded by the packaged artifact.
265    pub residual_body_coverage: ArtifactResidualBodyCoverageSummary,
266    /// Capability profile encoded by the packaged artifact.
267    pub artifact_profile: ArtifactProfile,
268    /// Output speed policy encoded by the packaged artifact.
269    pub speed_policy: pleiades_compression::SpeedPolicy,
270    /// Generation policy used to turn reference snapshots into segments.
271    pub generation_policy: PackagedArtifactGenerationPolicy,
272    /// Request policy encoded by the packaged artifact.
273    pub request_policy: PackagedRequestPolicySummary,
274    /// Lookup-epoch policy encoded by the packaged artifact.
275    pub lookup_epoch_policy: PackagedLookupEpochPolicy,
276    /// Frame-treatment policy encoded by the packaged artifact.
277    pub frame_treatment: PackagedFrameTreatmentSummary,
278    /// Storage/reconstruction policy encoded by the packaged artifact.
279    pub storage_summary: PackagedArtifactStorageSummary,
280    /// Release-facing statement about the packaged-artifact target thresholds.
281    pub target_thresholds: PackagedArtifactTargetThresholdSummary,
282}
283
284impl PackagedArtifactGeneratorParameters {
285    /// Returns the generator parameters as a compact human-readable line.
286    pub fn summary_line(&self) -> String {
287        format!(
288            "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={}; {}",
289            self.profile_id,
290            self.label,
291            self.artifact_version,
292            self.time_range,
293            self.source_provenance,
294            self.checksum,
295            self.artifact_size_bytes,
296            self.body_coverage,
297            self.residual_body_coverage.summary_line_with_body_count(),
298            self.artifact_profile,
299            self.artifact_profile.output_support_entries_summary_line(),
300            self.speed_policy,
301            self.generation_policy,
302            self.generation_policy.segment_strategy(),
303            self.request_policy,
304            self.lookup_epoch_policy.summary_line(),
305            self.frame_treatment,
306            self.storage_summary,
307            self.target_thresholds,
308        )
309    }
310
311    /// Returns `Ok(())` when the parameters still match the current packaged-artifact posture.
312    pub fn validate(&self) -> Result<(), pleiades_compression::CompressionError> {
313        let current = packaged_artifact_production_profile_summary_details();
314        let artifact = packaged_artifact();
315
316        if self.profile_id != current.profile_id {
317            return Err(pleiades_compression::CompressionError::new(
318                pleiades_compression::CompressionErrorKind::InvalidFormat,
319                "packaged artifact generator parameters profile id does not match the current production profile",
320            ));
321        }
322        if self.label != current.label {
323            return Err(pleiades_compression::CompressionError::new(
324                pleiades_compression::CompressionErrorKind::InvalidFormat,
325                "packaged artifact generator parameters label does not match the current production profile",
326            ));
327        }
328        if self.artifact_version != current.artifact_version {
329            return Err(pleiades_compression::CompressionError::new(
330                pleiades_compression::CompressionErrorKind::InvalidFormat,
331                "packaged artifact generator parameters version does not match the current production profile",
332            ));
333        }
334        if self.time_range != current.time_range {
335            return Err(pleiades_compression::CompressionError::new(
336                pleiades_compression::CompressionErrorKind::InvalidFormat,
337                "packaged artifact generator parameters time range does not match the current production profile",
338            ));
339        }
340        if self.source_provenance != current.source_provenance {
341            return Err(pleiades_compression::CompressionError::new(
342                pleiades_compression::CompressionErrorKind::InvalidFormat,
343                "packaged artifact generator parameters source provenance does not match the current production profile",
344            ));
345        }
346        if self.checksum != artifact.checksum {
347            return Err(pleiades_compression::CompressionError::new(
348                pleiades_compression::CompressionErrorKind::InvalidFormat,
349                "packaged artifact generator parameters checksum does not match the current packaged artifact",
350            ));
351        }
352        let expected_artifact_size_bytes = packaged_artifact_encoded_bytes(artifact);
353        if self.artifact_size_bytes != expected_artifact_size_bytes {
354            return Err(pleiades_compression::CompressionError::new(
355                pleiades_compression::CompressionErrorKind::InvalidFormat,
356                format!(
357                    "packaged artifact generator parameters artifact size {} bytes does not match the current packaged artifact size {} bytes",
358                    self.artifact_size_bytes,
359                    expected_artifact_size_bytes
360                ),
361            ));
362        }
363        if self.body_coverage != current.body_coverage {
364            return Err(pleiades_compression::CompressionError::new(
365                pleiades_compression::CompressionErrorKind::InvalidFormat,
366                "packaged artifact generator parameters body coverage does not match the current production profile",
367            ));
368        }
369        if self.residual_body_coverage != artifact.residual_body_coverage_summary() {
370            return Err(pleiades_compression::CompressionError::new(
371                pleiades_compression::CompressionErrorKind::InvalidFormat,
372                "packaged artifact generator parameters residual body coverage does not match the current packaged artifact",
373            ));
374        }
375        if self.artifact_profile != current.artifact_profile {
376            return Err(pleiades_compression::CompressionError::new(
377                pleiades_compression::CompressionErrorKind::InvalidFormat,
378                "packaged artifact generator parameters artifact profile does not match the current production profile",
379            ));
380        }
381        if self.speed_policy != current.speed_policy {
382            return Err(pleiades_compression::CompressionError::new(
383                pleiades_compression::CompressionErrorKind::InvalidFormat,
384                "packaged artifact generator parameters speed policy does not match the current production profile",
385            ));
386        }
387        if self.generation_policy != current.generation_policy {
388            return Err(pleiades_compression::CompressionError::new(
389                pleiades_compression::CompressionErrorKind::InvalidFormat,
390                "packaged artifact generator parameters generation policy does not match the current production profile",
391            ));
392        }
393        if self.request_policy != current.request_policy {
394            return Err(pleiades_compression::CompressionError::new(
395                pleiades_compression::CompressionErrorKind::InvalidFormat,
396                "packaged artifact generator parameters request policy does not match the current production profile",
397            ));
398        }
399        if self.lookup_epoch_policy != current.lookup_epoch_policy {
400            return Err(pleiades_compression::CompressionError::new(
401                pleiades_compression::CompressionErrorKind::InvalidFormat,
402                "packaged artifact generator parameters lookup epoch policy does not match the current production profile",
403            ));
404        }
405        if self.frame_treatment != current.frame_treatment {
406            return Err(pleiades_compression::CompressionError::new(
407                pleiades_compression::CompressionErrorKind::InvalidFormat,
408                "packaged artifact generator parameters frame-treatment policy does not match the current production profile",
409            ));
410        }
411        if self.storage_summary != current.storage_summary {
412            return Err(pleiades_compression::CompressionError::new(
413                pleiades_compression::CompressionErrorKind::InvalidFormat,
414                "packaged artifact generator parameters storage summary does not match the current production profile",
415            ));
416        }
417        self.target_thresholds
418            .state
419            .validate_production_ready()
420            .map_err(|_| {
421                pleiades_compression::CompressionError::new(
422                    pleiades_compression::CompressionErrorKind::InvalidFormat,
423                    "packaged artifact generator parameters target thresholds do not match the current production profile",
424                )
425            })?;
426        if self.target_thresholds != current.target_thresholds {
427            return Err(pleiades_compression::CompressionError::new(
428                pleiades_compression::CompressionErrorKind::InvalidFormat,
429                "packaged artifact generator parameters target thresholds do not match the current production profile",
430            ));
431        }
432
433        Ok(())
434    }
435
436    /// Returns the validated generator parameters summary line.
437    pub fn validated_summary_line(&self) -> Result<String, pleiades_compression::CompressionError> {
438        self.validate()?;
439        Ok(self.summary_line())
440    }
441}
442
443impl fmt::Display for PackagedArtifactGeneratorParameters {
444    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
445        f.write_str(&self.summary_line())
446    }
447}
448
449pub(crate) fn fnv1a64(bytes: &[u8]) -> u64 {
450    const OFFSET_BASIS: u64 = 0xcbf29ce484222325;
451    const PRIME: u64 = 0x100000001b3;
452
453    let mut hash = OFFSET_BASIS;
454    for byte in bytes {
455        hash ^= u64::from(*byte);
456        hash = hash.wrapping_mul(PRIME);
457    }
458    hash
459}
460
461fn packaged_artifact_generation_manifest_checksum(
462    parameters: &PackagedArtifactGeneratorParameters,
463    regeneration: &PackagedArtifactRegenerationSummary,
464) -> u64 {
465    let mut bytes = Vec::new();
466    bytes.extend_from_slice(parameters.summary_line().as_bytes());
467    bytes.push(b'\n');
468    bytes.extend_from_slice(regeneration.summary_line().as_bytes());
469    fnv1a64(&bytes)
470}
471
472/// Structured deterministic manifest for the packaged artifact generator.
473#[derive(Clone, Debug, PartialEq)]
474pub struct PackagedArtifactGenerationManifest {
475    /// Generator parameters used to produce the packaged artifact.
476    pub parameters: PackagedArtifactGeneratorParameters,
477    /// Regeneration provenance anchored to the checked-in artifact and source snapshot.
478    pub regeneration: PackagedArtifactRegenerationSummary,
479    /// Deterministic checksum of the rendered manifest content.
480    pub manifest_checksum: u64,
481}
482
483impl PackagedArtifactGenerationManifest {
484    /// Returns the deterministic manifest as a compact human-readable line.
485    pub fn summary_line(&self) -> String {
486        format!(
487            "Packaged artifact generation manifest: manifest checksum=0x{:016x}; {}; regeneration={}",
488            self.manifest_checksum, self.parameters, self.regeneration,
489        )
490    }
491
492    /// Returns `Ok(())` when the manifest still matches the current packaged-artifact posture.
493    pub fn validate(&self) -> Result<(), pleiades_compression::CompressionError> {
494        self.parameters.validate()?;
495        self.regeneration.validate()?;
496
497        let expected_checksum =
498            packaged_artifact_generation_manifest_checksum(&self.parameters, &self.regeneration);
499        if self.manifest_checksum != expected_checksum {
500            return Err(pleiades_compression::CompressionError::new(
501                pleiades_compression::CompressionErrorKind::InvalidFormat,
502                format!(
503                    "packaged artifact generation manifest checksum 0x{:016x} does not match the current packaged-artifact manifest checksum 0x{:016x}",
504                    self.manifest_checksum,
505                    expected_checksum
506                ),
507            ));
508        }
509
510        Ok(())
511    }
512
513    /// Returns the validated manifest summary line.
514    pub fn validated_summary_line(&self) -> Result<String, pleiades_compression::CompressionError> {
515        self.validate()?;
516        Ok(self.summary_line())
517    }
518}
519
520impl fmt::Display for PackagedArtifactGenerationManifest {
521    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
522        f.write_str(&self.summary_line())
523    }
524}
525
526/// Returns the current packaged-artifact generator parameters.
527pub fn packaged_artifact_generator_parameters_details() -> PackagedArtifactGeneratorParameters {
528    let summary = packaged_artifact_production_profile_summary_details();
529    let regeneration = packaged_artifact_regeneration_summary_details();
530    let parameters = PackagedArtifactGeneratorParameters {
531        profile_id: summary.profile_id,
532        label: summary.label,
533        artifact_version: summary.artifact_version,
534        time_range: summary.time_range,
535        source_provenance: summary.source_provenance,
536        checksum: regeneration.checksum,
537        artifact_size_bytes: regeneration.artifact_size_bytes,
538        body_coverage: summary.body_coverage,
539        residual_body_coverage: regeneration.residual_body_coverage_summary(),
540        artifact_profile: summary.artifact_profile,
541        speed_policy: summary.speed_policy,
542        generation_policy: summary.generation_policy,
543        request_policy: summary.request_policy,
544        lookup_epoch_policy: summary.lookup_epoch_policy,
545        frame_treatment: summary.frame_treatment,
546        storage_summary: summary.storage_summary,
547        target_thresholds: summary.target_thresholds,
548    };
549    debug_assert!(parameters.validate().is_ok());
550    parameters
551}
552
553/// Returns the current deterministic packaged-artifact generation manifest.
554pub fn packaged_artifact_generation_manifest_details() -> PackagedArtifactGenerationManifest {
555    let parameters = packaged_artifact_generator_parameters_details();
556    let regeneration = packaged_artifact_regeneration_summary_details();
557    let manifest = PackagedArtifactGenerationManifest {
558        manifest_checksum: packaged_artifact_generation_manifest_checksum(
559            &parameters,
560            &regeneration,
561        ),
562        parameters,
563        regeneration,
564    };
565    debug_assert!(manifest.validate().is_ok());
566    manifest
567}
568
569/// Returns the current deterministic packaged-artifact generation manifest after validation.
570pub fn packaged_artifact_generation_manifest_for_report() -> String {
571    static SUMMARY: OnceLock<String> = OnceLock::new();
572    SUMMARY
573        .get_or_init(|| {
574            let manifest = packaged_artifact_generation_manifest_details();
575            match manifest.validated_summary_line() {
576                Ok(line) => line,
577                Err(error) => {
578                    format!("Packaged artifact generation manifest: unavailable ({error})")
579                }
580            }
581        })
582        .clone()
583}
584
585/// Returns the current deterministic packaged-artifact generation manifest checksum after validation.
586pub fn packaged_artifact_generation_manifest_checksum_for_report() -> String {
587    static SUMMARY: OnceLock<String> = OnceLock::new();
588    SUMMARY
589        .get_or_init(|| {
590            let manifest = packaged_artifact_generation_manifest_details();
591            match manifest.validate() {
592                Ok(()) => format!(
593                    "Packaged artifact generation manifest checksum: 0x{:016x}",
594                    manifest.manifest_checksum
595                ),
596                Err(error) => {
597                    format!("Packaged artifact generation manifest checksum: unavailable ({error})")
598                }
599            }
600        })
601        .clone()
602}
603
604/// Returns the current deterministic packaged-artifact generation manifest.
605pub fn packaged_artifact_generation_manifest() -> &'static str {
606    static SUMMARY: OnceLock<String> = OnceLock::new();
607    SUMMARY
608        .get_or_init(|| {
609            let manifest = packaged_artifact_generation_manifest_details();
610            match manifest.validated_summary_line() {
611                Ok(rendered) => rendered,
612                Err(error) => {
613                    format!("Packaged artifact generation manifest: unavailable ({error})")
614                }
615            }
616        })
617        .as_str()
618}
619
620#[derive(Clone, Debug, PartialEq, Eq)]
621pub struct PackagedArtifactProfileSummary {
622    /// Number of bundled bodies that share the packaged artifact profile.
623    pub body_count: usize,
624    /// Bodies bundled under the packaged artifact profile.
625    pub bodies: Vec<CelestialBody>,
626    /// Byte-order policy encoded by the packaged artifact.
627    pub endian_policy: EndianPolicy,
628    /// Capability profile encoded by the packaged artifact.
629    pub profile: ArtifactProfile,
630}
631
632impl PackagedArtifactProfileSummary {
633    /// Returns the packaged artifact profile coverage as a typed summary.
634    pub fn profile_coverage_summary(&self) -> ArtifactProfileCoverageSummary {
635        ArtifactProfileCoverageSummary::new(self.profile.clone(), self.bodies.clone())
636    }
637
638    /// Validates that the packaged artifact profile summary is internally
639    /// consistent with its bundled body list, byte-order policy, and embedded capability profile.
640    pub fn validate(&self) -> Result<(), pleiades_compression::CompressionError> {
641        let artifact = packaged_artifact();
642        if self.endian_policy != artifact.header.endian_policy {
643            return Err(pleiades_compression::CompressionError::new(
644                pleiades_compression::CompressionErrorKind::InvalidFormat,
645                "packaged artifact profile byte-order policy does not match the checked-in packaged artifact header",
646            ));
647        }
648        if self.profile != artifact.profile_coverage_summary().profile {
649            return Err(pleiades_compression::CompressionError::new(
650                pleiades_compression::CompressionErrorKind::InvalidFormat,
651                "packaged artifact profile metadata does not match the checked-in packaged artifact profile",
652            ));
653        }
654
655        if self.body_count != self.bodies.len() {
656            return Err(pleiades_compression::CompressionError::new(
657                pleiades_compression::CompressionErrorKind::InvalidFormat,
658                "packaged artifact profile body count does not match bundled body list",
659            ));
660        }
661
662        if self.bodies.is_empty() {
663            let coverage = self.profile_coverage_summary();
664            coverage.validate()?;
665            return Ok(());
666        }
667
668        if self
669            .bodies
670            .iter()
671            .enumerate()
672            .any(|(index, body)| self.bodies[..index].contains(body))
673        {
674            let coverage = self.profile_coverage_summary();
675            coverage.validate()?;
676            return Ok(());
677        }
678
679        if self.bodies.as_slice() != packaged_bodies() {
680            return Err(pleiades_compression::CompressionError::new(
681                pleiades_compression::CompressionErrorKind::InvalidFormat,
682                format!(
683                    "packaged artifact profile bundled body list does not match the checked-in packaged body set: expected [{}]; got [{}]",
684                    join_display(packaged_bodies()),
685                    join_display(&self.bodies)
686                ),
687            ));
688        }
689
690        let coverage = self.profile_coverage_summary();
691        coverage.validate()?;
692
693        Ok(())
694    }
695
696    /// Returns the validated packaged artifact profile summary line.
697    pub fn validated_summary_line(&self) -> Result<String, pleiades_compression::CompressionError> {
698        self.validate()?;
699        Ok(self.summary_line())
700    }
701
702    /// Returns the validated packaged artifact profile summary line with bundled bodies.
703    pub fn validated_summary_line_with_bodies(
704        &self,
705    ) -> Result<String, pleiades_compression::CompressionError> {
706        self.validate()?;
707        Ok(self.summary_line_with_bodies())
708    }
709
710    /// Returns the validated packaged artifact profile summary line with output support.
711    pub fn validated_summary_line_with_output_support(
712        &self,
713    ) -> Result<String, pleiades_compression::CompressionError> {
714        self.validate()?;
715        Ok(format!(
716            "{}; output support: {}",
717            self.summary_line_with_bodies(),
718            self.profile.validated_output_support_summary_line()?
719        ))
720    }
721
722    /// Renders the packaged artifact profile into a release-facing summary line.
723    pub fn summary_line(&self) -> String {
724        let coverage = self.profile_coverage_summary();
725        format!(
726            "byte order: {}; {}",
727            self.endian_policy,
728            coverage.summary_line()
729        )
730    }
731
732    /// Returns the packaged artifact profile's output-support summary line.
733    pub fn output_support_summary_line(&self) -> String {
734        self.profile.output_support_summary_line()
735    }
736
737    /// Renders the packaged artifact profile with its bundled body list.
738    pub fn summary_line_with_bodies(&self) -> String {
739        let coverage = self.profile_coverage_summary();
740        format!(
741            "byte order: {}; {}",
742            self.endian_policy,
743            coverage.summary_line_with_bodies(),
744        )
745    }
746
747    /// Renders the packaged artifact profile together with the built-in output
748    /// support posture used by the current packaged artifact.
749    pub fn summary_line_with_output_support(&self) -> String {
750        format!(
751            "{}; output support: {}",
752            self.summary_line_with_bodies(),
753            self.output_support_summary_line()
754        )
755    }
756}
757
758impl fmt::Display for PackagedArtifactProfileSummary {
759    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
760        f.write_str(&self.summary_line())
761    }
762}
763
764/// Returns the current packaged-artifact profile summary record.
765pub fn packaged_artifact_profile_summary_details() -> PackagedArtifactProfileSummary {
766    let artifact = packaged_artifact();
767    let coverage = artifact.profile_coverage_summary();
768    let summary = PackagedArtifactProfileSummary {
769        body_count: coverage.body_count,
770        bodies: coverage.bodies,
771        endian_policy: artifact.header.endian_policy,
772        profile: coverage.profile,
773    };
774    debug_assert!(summary.validate().is_ok());
775    summary
776}
777
778/// Returns the current packaged-artifact profile coverage summary record.
779pub fn packaged_artifact_profile_coverage_summary_details() -> ArtifactProfileCoverageSummary {
780    packaged_artifact_profile_summary_details().profile_coverage_summary()
781}
782
783/// Returns the current packaged-artifact profile coverage summary for reporting.
784pub fn packaged_artifact_profile_coverage_summary_for_report() -> String {
785    static SUMMARY: OnceLock<String> = OnceLock::new();
786    SUMMARY
787        .get_or_init(|| {
788            let summary = packaged_artifact_profile_summary_details();
789            match summary.validate() {
790                Ok(()) => match summary
791                    .profile_coverage_summary()
792                    .validated_summary_line_with_bodies()
793                {
794                    Ok(line) => line,
795                    Err(error) => format!("Artifact profile coverage: unavailable ({error})"),
796                },
797                Err(error) => format!("Artifact profile coverage: unavailable ({error})"),
798            }
799        })
800        .clone()
801}
802
803/// Returns the current packaged-artifact profile summary.
804///
805/// The summary is validated before it is rendered so release-facing callers
806/// see an explicit unavailable marker if the bundled profile metadata drifts.
807pub fn packaged_artifact_profile_summary() -> String {
808    render_packaged_artifact_profile_summary(&packaged_artifact_profile_summary_details(), false)
809}
810
811/// Returns the current packaged-artifact profile summary with bundled body coverage.
812///
813/// The summary is validated before it is rendered so release-facing callers
814/// see an explicit unavailable marker if the bundled profile metadata drifts.
815pub fn packaged_artifact_profile_summary_with_body_coverage() -> String {
816    render_packaged_artifact_profile_summary(&packaged_artifact_profile_summary_details(), true)
817}
818
819/// Returns the current packaged-artifact profile summary with the output-support posture.
820///
821/// The summary is validated before it is rendered so release-facing callers
822/// see an explicit unavailable marker if the bundled profile metadata drifts.
823pub fn packaged_artifact_profile_summary_with_output_support() -> String {
824    let summary = packaged_artifact_profile_summary_details();
825    match summary.validated_summary_line_with_output_support() {
826        Ok(line) => line,
827        Err(error) => {
828            format!("Packaged artifact profile with output support: unavailable ({error})")
829        }
830    }
831}
832
833/// Returns the current packaged-artifact profile summary with output support for reporting.
834pub fn packaged_artifact_profile_summary_with_output_support_for_report() -> String {
835    packaged_artifact_profile_summary_with_output_support()
836}
837
838/// Structured output-support semantics for the packaged artifact profile.
839#[derive(Clone, Debug, PartialEq, Eq)]
840pub struct PackagedArtifactOutputSupportSummary {
841    /// Capability profile encoded by the packaged artifact.
842    pub profile: ArtifactProfile,
843}
844
845fn validate_packaged_artifact_output_support_profile(
846    profile: &ArtifactProfile,
847) -> Result<(), pleiades_compression::CompressionError> {
848    let expected_states = [
849        (
850            ArtifactOutput::EclipticCoordinates,
851            ArtifactOutputSupport::Derived,
852        ),
853        (
854            ArtifactOutput::EquatorialCoordinates,
855            ArtifactOutputSupport::Derived,
856        ),
857        (
858            ArtifactOutput::ApparentCorrections,
859            ArtifactOutputSupport::Unsupported,
860        ),
861        (
862            ArtifactOutput::TopocentricCoordinates,
863            ArtifactOutputSupport::Unsupported,
864        ),
865        (
866            ArtifactOutput::SiderealCoordinates,
867            ArtifactOutputSupport::Unsupported,
868        ),
869        (ArtifactOutput::Motion, ArtifactOutputSupport::Derived),
870    ];
871
872    for (output, expected_support) in expected_states {
873        let actual_support = profile.output_support(output);
874        if actual_support != expected_support {
875            return Err(pleiades_compression::CompressionError::new(
876                pleiades_compression::CompressionErrorKind::InvalidFormat,
877                format!(
878                    "packaged artifact output support summary is out of sync with the bundled artifact profile field `output_support[{output}]`: expected {expected_support}, found {actual_support}"
879                ),
880            ));
881        }
882    }
883
884    Ok(())
885}
886
887impl PackagedArtifactOutputSupportSummary {
888    /// Validates that the embedded artifact profile is internally consistent
889    /// and still advertises the packaged artifact's current built-in output
890    /// support posture.
891    pub fn validate(&self) -> Result<(), pleiades_compression::CompressionError> {
892        self.profile.validate()?;
893        validate_packaged_artifact_output_support_profile(&self.profile)
894    }
895
896    /// Returns the validated output-support posture for the packaged artifact profile.
897    pub fn validated_summary_line(&self) -> Result<String, pleiades_compression::CompressionError> {
898        self.validate()?;
899        Ok(self.summary_line())
900    }
901
902    /// Renders the packaged artifact profile's output-support semantics.
903    pub fn summary_line(&self) -> String {
904        self.profile.output_support_summary_line()
905    }
906}
907
908impl fmt::Display for PackagedArtifactOutputSupportSummary {
909    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
910        f.write_str(&self.summary_line())
911    }
912}
913
914/// Returns the current packaged-artifact output-support summary record.
915pub fn packaged_artifact_output_support_summary_details() -> PackagedArtifactOutputSupportSummary {
916    let summary = PackagedArtifactOutputSupportSummary {
917        profile: packaged_artifact_profile_summary_details().profile,
918    };
919    debug_assert!(summary.validate().is_ok());
920    summary
921}
922
923/// Returns the output-support semantics of the packaged artifact profile for reporting.
924pub fn packaged_artifact_output_support_summary_for_report() -> String {
925    static SUMMARY: OnceLock<String> = OnceLock::new();
926    SUMMARY
927        .get_or_init(|| {
928            let summary = packaged_artifact_output_support_summary_details();
929            match summary.validated_summary_line() {
930                Ok(rendered) => rendered,
931                Err(error) => format!("unavailable ({error})"),
932            }
933        })
934        .clone()
935}
936
937/// Structured speed-policy semantics for the packaged artifact profile.
938#[derive(Clone, Copy, Debug, PartialEq, Eq)]
939pub struct PackagedArtifactSpeedPolicySummary {
940    /// Speed policy encoded by the packaged artifact.
941    pub policy: SpeedPolicy,
942}
943
944/// Validation error for the packaged-data speed-policy summary.
945#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
946pub enum PackagedArtifactSpeedPolicySummaryValidationError {
947    /// A summary field is out of sync with the current packaged-data posture.
948    FieldOutOfSync { field: &'static str },
949}
950
951impl fmt::Display for PackagedArtifactSpeedPolicySummaryValidationError {
952    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
953        match self {
954            Self::FieldOutOfSync { field } => write!(
955                f,
956                "the packaged artifact speed-policy summary field `{field}` is out of sync with the current posture"
957            ),
958        }
959    }
960}
961
962impl std::error::Error for PackagedArtifactSpeedPolicySummaryValidationError {}
963
964impl PackagedArtifactSpeedPolicySummary {
965    /// Returns the packaged artifact speed policy as a compact human-readable line.
966    pub fn summary_line(self) -> String {
967        format!(
968            "{}; motion output support={}",
969            self.policy,
970            self.policy.motion_output_support()
971        )
972    }
973
974    /// Returns `Ok(())` when the summary still matches the current packaged-data posture.
975    pub fn validate(&self) -> Result<(), PackagedArtifactSpeedPolicySummaryValidationError> {
976        let current_policy = packaged_artifact_profile_summary_details()
977            .profile
978            .speed_policy;
979        if self.policy != current_policy {
980            return Err(
981                PackagedArtifactSpeedPolicySummaryValidationError::FieldOutOfSync {
982                    field: "policy",
983                },
984            );
985        }
986        Ok(())
987    }
988
989    /// Returns the validated packaged artifact speed-policy summary line.
990    pub fn validated_summary_line(
991        &self,
992    ) -> Result<String, PackagedArtifactSpeedPolicySummaryValidationError> {
993        self.validate()?;
994        Ok(self.summary_line())
995    }
996}
997
998impl fmt::Display for PackagedArtifactSpeedPolicySummary {
999    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1000        f.write_str(&self.summary_line())
1001    }
1002}
1003
1004const PACKAGED_ARTIFACT_SPEED_POLICY_SUMMARY: PackagedArtifactSpeedPolicySummary =
1005    PackagedArtifactSpeedPolicySummary {
1006        policy: SpeedPolicy::FittedDerivative,
1007    };
1008
1009/// Returns the current packaged-artifact speed-policy summary record.
1010pub fn packaged_artifact_speed_policy_summary_details() -> PackagedArtifactSpeedPolicySummary {
1011    let summary = PACKAGED_ARTIFACT_SPEED_POLICY_SUMMARY;
1012    debug_assert!(summary.validate().is_ok());
1013    summary
1014}
1015
1016/// Returns the packaged-artifact speed-policy semantics for reporting.
1017pub fn packaged_artifact_speed_policy_summary_for_report() -> String {
1018    static SUMMARY: OnceLock<String> = OnceLock::new();
1019    SUMMARY
1020        .get_or_init(|| {
1021            let summary = packaged_artifact_speed_policy_summary_details();
1022            match summary.validate() {
1023                Ok(()) => summary.summary_line(),
1024                Err(error) => format!("unavailable ({error})"),
1025            }
1026        })
1027        .clone()
1028}
1029
1030pub(crate) fn render_packaged_artifact_profile_summary(
1031    summary: &PackagedArtifactProfileSummary,
1032    with_bodies: bool,
1033) -> String {
1034    if with_bodies {
1035        match summary.validated_summary_line_with_bodies() {
1036            Ok(line) => line,
1037            Err(error) => {
1038                format!("Packaged artifact profile with bundled bodies: unavailable ({error})")
1039            }
1040        }
1041    } else {
1042        match summary.validated_summary_line() {
1043            Ok(line) => line,
1044            Err(error) => format!("Packaged artifact profile: unavailable ({error})"),
1045        }
1046    }
1047}