Skip to main content

pleiades_data/coverage/
regen.rs

1use super::*;
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4pub(crate) enum PackagedArtifactBodyCadence {
5    Luminaries,
6    InnerPlanets,
7    OuterPlanets,
8    Pluto,
9    LunarPoints,
10    SelectedAsteroids,
11    CustomBodies,
12}
13
14impl PackagedArtifactBodyCadence {
15    pub(crate) fn uses_dense_sampling(self) -> bool {
16        matches!(
17            self,
18            Self::Luminaries
19                | Self::Pluto
20                | Self::LunarPoints
21                | Self::SelectedAsteroids
22                | Self::CustomBodies
23        )
24    }
25
26    pub(crate) fn uses_dense_validation_sampling(self) -> bool {
27        matches!(
28            self,
29            Self::Luminaries
30                | Self::InnerPlanets
31                | Self::OuterPlanets
32                | Self::Pluto
33                | Self::LunarPoints
34                | Self::SelectedAsteroids
35                | Self::CustomBodies
36        )
37    }
38
39    pub(crate) fn uses_dense_residual_sample_lattice(self, kind: ChannelKind) -> bool {
40        match kind {
41            ChannelKind::Longitude | ChannelKind::Latitude => self.uses_dense_sampling(),
42            ChannelKind::DistanceAu => {
43                matches!(
44                    self,
45                    Self::InnerPlanets
46                        | Self::OuterPlanets
47                        | Self::Pluto
48                        | Self::LunarPoints
49                        | Self::SelectedAsteroids
50                        | Self::CustomBodies
51                )
52            }
53            _ => false,
54        }
55    }
56}
57
58pub(crate) fn packaged_artifact_body_cadence(body: &CelestialBody) -> PackagedArtifactBodyCadence {
59    match body {
60        CelestialBody::Sun | CelestialBody::Moon => PackagedArtifactBodyCadence::Luminaries,
61        CelestialBody::Mercury | CelestialBody::Venus | CelestialBody::Mars => {
62            PackagedArtifactBodyCadence::InnerPlanets
63        }
64        CelestialBody::Jupiter
65        | CelestialBody::Saturn
66        | CelestialBody::Uranus
67        | CelestialBody::Neptune => PackagedArtifactBodyCadence::OuterPlanets,
68        CelestialBody::Pluto => PackagedArtifactBodyCadence::Pluto,
69        CelestialBody::MeanNode
70        | CelestialBody::TrueNode
71        | CelestialBody::MeanApogee
72        | CelestialBody::TrueApogee
73        | CelestialBody::MeanPerigee
74        | CelestialBody::TruePerigee => PackagedArtifactBodyCadence::LunarPoints,
75        CelestialBody::Ceres
76        | CelestialBody::Pallas
77        | CelestialBody::Juno
78        | CelestialBody::Vesta => PackagedArtifactBodyCadence::SelectedAsteroids,
79        CelestialBody::Custom(custom) if custom.catalog.eq_ignore_ascii_case("asteroid") => {
80            PackagedArtifactBodyCadence::SelectedAsteroids
81        }
82        CelestialBody::Custom(_) => PackagedArtifactBodyCadence::CustomBodies,
83        _ => PackagedArtifactBodyCadence::CustomBodies,
84    }
85}
86
87pub(crate) fn packaged_artifact_target_threshold_scope_envelope_summary_details(
88    scope: &'static str,
89) -> PackagedArtifactTargetThresholdScopeSummary {
90    let artifact = packaged_artifact();
91    let bodies: Vec<CelestialBody> = artifact
92        .bodies
93        .iter()
94        .filter(|body| packaged_artifact_body_scope(&body.body) == scope)
95        .map(|body| body.body.clone())
96        .collect();
97    let body_count = bodies.len();
98    let samples = packaged_artifact_fit_samples_for_current_artifact()
99        .iter()
100        .filter(|sample| packaged_artifact_body_scope(&sample.body) == scope)
101        .cloned()
102        .collect::<Vec<_>>();
103    let expected_sample_count =
104        packaged_artifact_fit_expected_sample_count_with_filter(artifact, |body| {
105            packaged_artifact_body_scope(body) == scope
106        });
107    let fit_envelope =
108        packaged_artifact_fit_envelope_summary_from_samples(&samples, expected_sample_count);
109    PackagedArtifactTargetThresholdScopeSummary {
110        scope,
111        bodies,
112        body_count,
113        fit_envelope,
114    }
115}
116
117fn packaged_artifact_target_threshold_scope_envelope_summaries_details(
118) -> Vec<PackagedArtifactTargetThresholdScopeSummary> {
119    PACKAGED_ARTIFACT_TARGET_THRESHOLD_SCOPES
120        .iter()
121        .copied()
122        .map(packaged_artifact_target_threshold_scope_envelope_summary_details)
123        .collect()
124}
125
126/// Returns the current packaged-artifact body-class target-threshold scope envelopes after validating the structured posture.
127pub fn packaged_artifact_target_threshold_scope_envelopes_summary_details(
128) -> PackagedArtifactTargetThresholdScopeEnvelopesSummary {
129    PackagedArtifactTargetThresholdScopeEnvelopesSummary {
130        scope_envelopes: packaged_artifact_target_threshold_scope_envelope_summaries_details(),
131    }
132}
133
134pub(crate) fn format_scope_bodies(bodies: &[CelestialBody]) -> String {
135    match bodies {
136        [] => "0 (none)".to_string(),
137        [single] => format!("1 ({single})"),
138        _ => format!("{} ({})", bodies.len(), join_display(bodies)),
139    }
140}
141
142pub(crate) fn packaged_artifact_quantization_scales_line() -> String {
143    let artifact = packaged_artifact();
144    let stored = packaged_artifact_channel_quantization_scales(artifact, false);
145    let residual = packaged_artifact_channel_quantization_scales(artifact, true);
146
147    if residual.is_empty() {
148        format!("quantization scales: stored={stored}")
149    } else {
150        format!("quantization scales: stored={stored}; residual={residual}")
151    }
152}
153
154fn packaged_artifact_channel_quantization_scales(
155    artifact: &CompressedArtifact,
156    residual_channels: bool,
157) -> String {
158    let entries = [
159        ChannelKind::Longitude,
160        ChannelKind::Latitude,
161        ChannelKind::DistanceAu,
162    ]
163    .into_iter()
164    .filter_map(|kind| {
165        let mut exponents = artifact
166            .bodies
167            .iter()
168            .flat_map(|body| body.segments.iter())
169            .flat_map(|segment| {
170                let channels = if residual_channels {
171                    &segment.residual_channels
172                } else {
173                    &segment.channels
174                };
175
176                channels
177                    .iter()
178                    .filter(move |channel| channel.kind == kind)
179                    .map(|channel| channel.scale_exponent)
180                    .collect::<Vec<_>>()
181            })
182            .collect::<Vec<_>>();
183
184        if exponents.is_empty() {
185            None
186        } else {
187            exponents.sort_unstable();
188            exponents.dedup();
189            Some(format!("{kind}={}", join_display(&exponents)))
190        }
191    })
192    .collect::<Vec<_>>();
193
194    if entries.is_empty() {
195        "none".to_string()
196    } else {
197        entries.join(", ")
198    }
199}
200
201pub(crate) fn packaged_artifact_segment_span_bounds(artifact: &CompressedArtifact) -> (f64, f64) {
202    let mut min_span_days: f64 = f64::INFINITY;
203    let mut max_span_days: f64 = 0.0;
204
205    for body in &artifact.bodies {
206        for segment in &body.segments {
207            let span_days = segment.end.julian_day.days() - segment.start.julian_day.days();
208            min_span_days = min_span_days.min(span_days);
209            max_span_days = max_span_days.max(span_days);
210        }
211    }
212
213    if min_span_days.is_infinite() {
214        (0.0, 0.0)
215    } else {
216        (min_span_days, max_span_days)
217    }
218}
219
220pub(crate) fn packaged_artifact_channel_count(
221    artifact: &CompressedArtifact,
222    residual_channels: bool,
223) -> usize {
224    artifact
225        .bodies
226        .iter()
227        .flat_map(|body| body.segments.iter())
228        .map(|segment| {
229            if residual_channels {
230                segment.residual_channels.len()
231            } else {
232                segment.channels.len()
233            }
234        })
235        .sum()
236}
237
238pub(crate) fn packaged_artifact_encoded_bytes(artifact: &CompressedArtifact) -> usize {
239    artifact
240        .encode()
241        .expect("packaged artifact should be encodable")
242        .len()
243}
244
245/// Structured normalized-intermediate provenance for the packaged artifact.
246#[derive(Clone, Debug, PartialEq)]
247pub struct PackagedArtifactNormalizedIntermediateSummary {
248    /// Human-readable generation label.
249    pub label: &'static str,
250    /// Version of the packaged artifact format.
251    pub artifact_version: u16,
252    /// Human-readable provenance/source summary.
253    pub source: &'static str,
254    /// Canonical source-revision summary for the checked-in production-generation corpus.
255    pub source_revision: String,
256    /// Stable identifier for the packaged-artifact profile.
257    pub profile_id: &'static str,
258    /// Covered time range for the normalized intermediate layout.
259    pub time_range: TimeRange,
260    /// Generation policy used to turn reference snapshots into segments.
261    pub generation_policy: PackagedArtifactGenerationPolicy,
262    /// Per-channel quantization scales captured from the checked-in artifact.
263    pub quantization_scales: String,
264    /// Deterministic checksum of the rendered normalized-intermediate payload.
265    pub checksum: u64,
266    /// Bodies bundled into the packaged artifact.
267    pub body_count: usize,
268    /// Total segment count across all bundled bodies.
269    pub segment_count: usize,
270    /// Total count of segments carrying residual correction channels.
271    pub residual_segment_count: usize,
272    /// Total count of stored channels across all segments.
273    pub stored_channel_count: usize,
274    /// Total count of residual channels across all segments.
275    pub residual_channel_count: usize,
276    /// Smallest observed segment span in days.
277    pub min_segment_span_days: f64,
278    /// Largest observed segment span in days.
279    pub max_segment_span_days: f64,
280}
281
282impl PackagedArtifactNormalizedIntermediateSummary {
283    /// Returns the normalized-intermediate payload used for checksuming and rendering.
284    pub(crate) fn summary_payload_line(&self) -> String {
285        format!(
286            "label={}; profile id={}; version={}; time range={}; source={}; source revision={}; body count={}; segments={}; residual-bearing segments={}; stored channels={}; residual channels={}; segment span days={:.12}..{:.12}; segment strategy={}; {}",
287            self.label,
288            self.profile_id,
289            self.artifact_version,
290            self.time_range,
291            self.source,
292            self.source_revision,
293            self.body_count,
294            self.segment_count,
295            self.residual_segment_count,
296            self.stored_channel_count,
297            self.residual_channel_count,
298            self.min_segment_span_days,
299            self.max_segment_span_days,
300            self.generation_policy.segment_strategy(),
301            self.quantization_scales,
302        )
303    }
304
305    /// Returns the normalized intermediates as a compact human-readable line.
306    pub fn summary_fields_line(&self) -> String {
307        format!(
308            "{}; checksum=0x{:016x}",
309            self.summary_payload_line(),
310            self.checksum,
311        )
312    }
313
314    /// Returns the normalized intermediates as a compact human-readable line.
315    pub fn summary_line(&self) -> String {
316        format!(
317            "Packaged artifact normalized intermediates: {}",
318            self.summary_fields_line()
319        )
320    }
321
322    /// Returns `Ok(())` when the summary still matches the current packaged-artifact posture.
323    pub fn validate(&self) -> Result<(), pleiades_compression::CompressionError> {
324        let artifact = packaged_artifact();
325        if self.label != ARTIFACT_LABEL {
326            return Err(pleiades_compression::CompressionError::new(
327                pleiades_compression::CompressionErrorKind::InvalidFormat,
328                "packaged artifact normalized intermediate summary label does not match the checked-in artifact label",
329            ));
330        }
331        if self.artifact_version != artifact.header.version {
332            return Err(pleiades_compression::CompressionError::new(
333                pleiades_compression::CompressionErrorKind::InvalidFormat,
334                "packaged artifact normalized intermediate summary artifact version does not match the checked-in packaged artifact version",
335            ));
336        }
337        if self.source != packaged_artifact_source_text() {
338            return Err(pleiades_compression::CompressionError::new(
339                pleiades_compression::CompressionErrorKind::InvalidFormat,
340                "packaged artifact normalized intermediate summary source does not match the checked-in artifact source",
341            ));
342        }
343        if self.source_revision != production_generation_source_summary_for_report() {
344            return Err(pleiades_compression::CompressionError::new(
345                pleiades_compression::CompressionErrorKind::InvalidFormat,
346                "packaged artifact normalized intermediate summary source revision does not match the checked-in production-generation source summary",
347            ));
348        }
349        if self.profile_id != ARTIFACT_PROFILE_ID {
350            return Err(pleiades_compression::CompressionError::new(
351                pleiades_compression::CompressionErrorKind::InvalidFormat,
352                "packaged artifact normalized intermediate summary profile id does not match the checked-in artifact profile id",
353            ));
354        }
355        if self.time_range != artifact_time_range(artifact) {
356            return Err(pleiades_compression::CompressionError::new(
357                pleiades_compression::CompressionErrorKind::InvalidFormat,
358                "packaged artifact normalized intermediate summary time range does not match the checked-in packaged artifact",
359            ));
360        }
361        if self.generation_policy
362            != PackagedArtifactGenerationPolicy::AdjacentSameBodyQuadraticWindows
363        {
364            return Err(pleiades_compression::CompressionError::new(
365                pleiades_compression::CompressionErrorKind::InvalidFormat,
366                "packaged artifact normalized intermediate summary generation policy does not match the checked-in packaged artifact",
367            ));
368        }
369        if self.quantization_scales != packaged_artifact_quantization_scales_line() {
370            return Err(pleiades_compression::CompressionError::new(
371                pleiades_compression::CompressionErrorKind::InvalidFormat,
372                "packaged artifact normalized intermediate summary quantization scales do not match the checked-in packaged artifact",
373            ));
374        }
375        if self.body_count != artifact.bodies.len() {
376            return Err(pleiades_compression::CompressionError::new(
377                pleiades_compression::CompressionErrorKind::InvalidFormat,
378                "packaged artifact normalized intermediate summary body count does not match the checked-in packaged artifact",
379            ));
380        }
381        if self.segment_count != artifact.segment_count() {
382            return Err(pleiades_compression::CompressionError::new(
383                pleiades_compression::CompressionErrorKind::InvalidFormat,
384                "packaged artifact normalized intermediate summary segment count does not match the checked-in packaged artifact",
385            ));
386        }
387        if self.residual_segment_count != artifact.residual_segment_count() {
388            return Err(pleiades_compression::CompressionError::new(
389                pleiades_compression::CompressionErrorKind::InvalidFormat,
390                "packaged artifact normalized intermediate summary residual segment count does not match the checked-in packaged artifact",
391            ));
392        }
393        if self.stored_channel_count != packaged_artifact_channel_count(artifact, false) {
394            return Err(pleiades_compression::CompressionError::new(
395                pleiades_compression::CompressionErrorKind::InvalidFormat,
396                "packaged artifact normalized intermediate summary stored channel count does not match the checked-in packaged artifact",
397            ));
398        }
399        if self.residual_channel_count != packaged_artifact_channel_count(artifact, true) {
400            return Err(pleiades_compression::CompressionError::new(
401                pleiades_compression::CompressionErrorKind::InvalidFormat,
402                "packaged artifact normalized intermediate summary residual channel count does not match the checked-in packaged artifact",
403            ));
404        }
405        let (expected_min_segment_span_days, expected_max_segment_span_days) =
406            packaged_artifact_segment_span_bounds(artifact);
407        if self.min_segment_span_days != expected_min_segment_span_days {
408            return Err(pleiades_compression::CompressionError::new(
409                pleiades_compression::CompressionErrorKind::InvalidFormat,
410                "packaged artifact normalized intermediate summary minimum segment span does not match the checked-in packaged artifact",
411            ));
412        }
413        if self.max_segment_span_days != expected_max_segment_span_days {
414            return Err(pleiades_compression::CompressionError::new(
415                pleiades_compression::CompressionErrorKind::InvalidFormat,
416                "packaged artifact normalized intermediate summary maximum segment span does not match the checked-in packaged artifact",
417            ));
418        }
419
420        let expected_checksum = fnv1a64(self.summary_payload_line().as_bytes());
421        if self.checksum != expected_checksum {
422            return Err(pleiades_compression::CompressionError::new(
423                pleiades_compression::CompressionErrorKind::InvalidFormat,
424                format!(
425                    "packaged artifact normalized intermediate summary checksum 0x{:016x} does not match the current normalized-intermediate checksum 0x{:016x}",
426                    self.checksum,
427                    expected_checksum
428                ),
429            ));
430        }
431
432        Ok(())
433    }
434
435    /// Returns the validated normalized intermediates as a compact human-readable line.
436    pub fn validated_summary_line(&self) -> Result<String, pleiades_compression::CompressionError> {
437        self.validate()?;
438        Ok(self.summary_line())
439    }
440}
441
442impl fmt::Display for PackagedArtifactNormalizedIntermediateSummary {
443    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
444        f.write_str(&self.summary_line())
445    }
446}
447
448/// Structured regeneration provenance for the packaged artifact.
449#[derive(Clone, Debug, PartialEq)]
450pub struct PackagedArtifactRegenerationSummary {
451    /// Human-readable generation label.
452    pub label: &'static str,
453    /// Version of the packaged artifact format.
454    pub artifact_version: u16,
455    /// Human-readable provenance/source summary.
456    pub source: &'static str,
457    /// Canonical source-revision summary for the checked-in production-generation corpus.
458    pub source_revision: String,
459    /// Stable identifier for the packaged-artifact profile.
460    pub profile_id: &'static str,
461    /// Checksum of the checked-in packaged artifact.
462    pub checksum: u64,
463    /// Encoded size of the checked-in packaged artifact in bytes.
464    pub artifact_size_bytes: usize,
465    /// Generation policy used to turn reference snapshots into segments.
466    pub generation_policy: PackagedArtifactGenerationPolicy,
467    /// Per-channel quantization scales captured from the checked-in artifact.
468    pub quantization_scales: String,
469    /// Bodies that carry residual correction channels in the packaged artifact.
470    pub residual_bodies: Vec<CelestialBody>,
471    /// Bodies bundled into the packaged artifact.
472    pub bodies: Vec<CelestialBody>,
473    /// Normalized intermediate layout captured from the checked-in artifact.
474    pub normalized_intermediates: PackagedArtifactNormalizedIntermediateSummary,
475    /// Fit envelope measured against the generation source samples.
476    pub fit_envelope: PackagedArtifactFitEnvelopeSummary,
477    /// Coverage summary for the checked-in JPL reference snapshot used for regeneration.
478    pub reference_snapshot: Option<ReferenceSnapshotSummary>,
479}
480
481impl PackagedArtifactRegenerationSummary {
482    /// Returns the bundled bodies as a compact human-readable line.
483    pub fn body_coverage_line(&self) -> String {
484        format!(
485            "{} bundled bodies ({})",
486            self.bodies.len(),
487            join_display(&self.bodies)
488        )
489    }
490
491    /// Returns the checked-in JPL snapshot coverage as a compact human-readable line.
492    pub fn reference_snapshot_line(&self) -> String {
493        self.reference_snapshot
494            .map(|summary| format_reference_snapshot_summary(&summary))
495            .unwrap_or_else(|| "Reference snapshot coverage: unavailable".to_string())
496    }
497
498    /// Returns the normalized intermediate layout as a compact human-readable line.
499    pub fn normalized_intermediates_line(&self) -> String {
500        self.normalized_intermediates.summary_fields_line()
501    }
502
503    /// Returns the residual-correction body coverage as a compact structured summary.
504    pub fn residual_body_coverage_summary(&self) -> ArtifactResidualBodyCoverageSummary {
505        ArtifactResidualBodyCoverageSummary::new(self.residual_bodies.clone())
506    }
507
508    /// Returns the residual-correction body list as a compact human-readable line.
509    pub fn residual_body_line(&self) -> String {
510        self.residual_body_coverage_summary()
511            .summary_line_with_body_count()
512    }
513
514    /// Returns the generation policy as a compact human-readable line.
515    pub fn generation_policy_line(&self) -> String {
516        format!(
517            "generation policy: {}",
518            self.generation_policy.summary_line()
519        )
520    }
521
522    /// Validates that every residual body is actually part of the bundled body list.
523    pub(crate) fn validate_residual_body_subset(
524        &self,
525    ) -> Result<(), pleiades_compression::CompressionError> {
526        for body in &self.residual_bodies {
527            if !self.bodies.contains(body) {
528                return Err(pleiades_compression::CompressionError::new(
529                    pleiades_compression::CompressionErrorKind::InvalidFormat,
530                    format!(
531                        "packaged artifact regeneration summary residual body {body} is not covered by the bundled body list"
532                    ),
533                ));
534            }
535        }
536
537        Ok(())
538    }
539
540    /// Validates that the regeneration summary stays aligned with the bundled
541    /// body list, the current checked-in artifact metadata, and the checked-in
542    /// reference snapshot coverage.
543    pub fn validate(&self) -> Result<(), pleiades_compression::CompressionError> {
544        let artifact = packaged_artifact();
545
546        if self.label != ARTIFACT_LABEL {
547            return Err(pleiades_compression::CompressionError::new(
548                pleiades_compression::CompressionErrorKind::InvalidFormat,
549                "packaged artifact regeneration summary label does not match the checked-in artifact label",
550            ));
551        }
552        if self.source != packaged_artifact_source_text() {
553            return Err(pleiades_compression::CompressionError::new(
554                pleiades_compression::CompressionErrorKind::InvalidFormat,
555                "packaged artifact regeneration summary source does not match the checked-in artifact source",
556            ));
557        }
558        if self.source_revision != production_generation_source_summary_for_report() {
559            return Err(pleiades_compression::CompressionError::new(
560                pleiades_compression::CompressionErrorKind::InvalidFormat,
561                "packaged artifact regeneration summary source revision does not match the checked-in production-generation source summary",
562            ));
563        }
564        self.normalized_intermediates.validate().map_err(|error| {
565            pleiades_compression::CompressionError::new(
566                pleiades_compression::CompressionErrorKind::InvalidFormat,
567                format!(
568                    "packaged artifact regeneration summary normalized intermediates are invalid: {error}"
569                ),
570            )
571        })?;
572        if self.profile_id != ARTIFACT_PROFILE_ID {
573            return Err(pleiades_compression::CompressionError::new(
574                pleiades_compression::CompressionErrorKind::InvalidFormat,
575                "packaged artifact regeneration summary profile id does not match the checked-in artifact profile id",
576            ));
577        }
578        if self.artifact_version != artifact.header.version {
579            return Err(pleiades_compression::CompressionError::new(
580                pleiades_compression::CompressionErrorKind::InvalidFormat,
581                format!(
582                    "packaged artifact regeneration summary artifact version {} does not match the checked-in packaged artifact version {}",
583                    self.artifact_version,
584                    artifact.header.version
585                ),
586            ));
587        }
588        if self.checksum != artifact.checksum {
589            return Err(pleiades_compression::CompressionError::new(
590                pleiades_compression::CompressionErrorKind::InvalidFormat,
591                format!(
592                    "packaged artifact regeneration summary checksum 0x{:016x} does not match the checked-in packaged artifact checksum 0x{:016x}",
593                    self.checksum,
594                    artifact.checksum
595                ),
596            ));
597        }
598        let expected_artifact_size_bytes = packaged_artifact_encoded_bytes(artifact);
599        if self.artifact_size_bytes != expected_artifact_size_bytes {
600            return Err(pleiades_compression::CompressionError::new(
601                pleiades_compression::CompressionErrorKind::InvalidFormat,
602                format!(
603                    "packaged artifact regeneration summary artifact size {} bytes does not match the checked-in packaged artifact size {} bytes",
604                    self.artifact_size_bytes,
605                    expected_artifact_size_bytes
606                ),
607            ));
608        }
609        self.generation_policy.validate().map_err(|error| {
610            pleiades_compression::CompressionError::new(
611                pleiades_compression::CompressionErrorKind::InvalidFormat,
612                format!(
613                    "packaged artifact regeneration summary generation policy is invalid: {error}"
614                ),
615            )
616        })?;
617
618        if self.quantization_scales != packaged_artifact_quantization_scales_line() {
619            return Err(pleiades_compression::CompressionError::new(
620                pleiades_compression::CompressionErrorKind::InvalidFormat,
621                "packaged artifact regeneration summary quantization scales do not match the checked-in packaged artifact",
622            ));
623        }
624
625        self.residual_body_coverage_summary()
626            .validate(artifact)
627            .map_err(|error| {
628                pleiades_compression::CompressionError::new(
629                    pleiades_compression::CompressionErrorKind::InvalidFormat,
630                    format!(
631                        "packaged artifact regeneration summary residual body coverage is invalid: {error}"
632                    ),
633                )
634            })?;
635
636        if self.reference_snapshot.is_none() {
637            return Err(pleiades_compression::CompressionError::new(
638                pleiades_compression::CompressionErrorKind::InvalidFormat,
639                "packaged artifact regeneration summary is missing reference snapshot coverage",
640            ));
641        }
642
643        for (index, body) in self.bodies.iter().enumerate() {
644            if self.bodies[..index].iter().any(|other| other == body) {
645                return Err(pleiades_compression::CompressionError::new(
646                    pleiades_compression::CompressionErrorKind::InvalidFormat,
647                    format!("packaged artifact regeneration summary contains duplicate body entry {body}"),
648                ));
649            }
650        }
651
652        let expected_bodies = packaged_bodies();
653        if self.bodies.as_slice() != expected_bodies {
654            return Err(pleiades_compression::CompressionError::new(
655                pleiades_compression::CompressionErrorKind::InvalidFormat,
656                format!(
657                    "packaged artifact regeneration summary body list does not match the checked-in packaged body set: expected [{}]; got [{}]",
658                    join_display(expected_bodies),
659                    join_display(&self.bodies)
660                ),
661            ));
662        }
663
664        self.validate_residual_body_subset()?;
665
666        self.fit_envelope.validate().map_err(|error| {
667            pleiades_compression::CompressionError::new(
668                pleiades_compression::CompressionErrorKind::InvalidFormat,
669                format!("packaged artifact regeneration fit envelope is invalid: {error}"),
670            )
671        })?;
672
673        if let Some(reference_snapshot) = self.reference_snapshot {
674            reference_snapshot.validate().map_err(|error| {
675                pleiades_compression::CompressionError::new(
676                    pleiades_compression::CompressionErrorKind::InvalidFormat,
677                    format!(
678                        "packaged artifact regeneration reference snapshot is invalid: {error}"
679                    ),
680                )
681            })?;
682            for body in &self.bodies {
683                if !reference_snapshot.bodies.contains(body) {
684                    return Err(pleiades_compression::CompressionError::new(
685                        pleiades_compression::CompressionErrorKind::InvalidFormat,
686                        format!("packaged artifact regeneration body {body} is not covered by the reference snapshot"),
687                    ));
688                }
689            }
690            if self.reference_snapshot != reference_snapshot_summary() {
691                return Err(pleiades_compression::CompressionError::new(
692                    pleiades_compression::CompressionErrorKind::InvalidFormat,
693                    "packaged artifact regeneration summary reference snapshot does not match the checked-in reference snapshot summary",
694                ));
695            }
696        }
697
698        Ok(())
699    }
700
701    /// Returns the full packaged-artifact regeneration provenance summary.
702    pub fn summary_line(&self) -> String {
703        format!(
704            "Packaged artifact regeneration source: label={}; profile id={}; source={}; source revision={}; normalized intermediates: {}; checksum=0x{:016x}; artifact size={} bytes; {}; segment strategy={}; {}; {}; bundled bodies: {}; {}; fit envelope: {}; artifact version={}",
705            self.label,
706            self.profile_id,
707            self.source,
708            self.source_revision,
709            self.normalized_intermediates_line(),
710            self.checksum,
711            self.artifact_size_bytes,
712            self.generation_policy_line(),
713            self.generation_policy.segment_strategy(),
714            self.quantization_scales,
715            self.residual_body_line(),
716            self.body_coverage_line(),
717            self.reference_snapshot_line(),
718            self.fit_envelope.summary_line(),
719            self.artifact_version,
720        )
721    }
722
723    /// Returns the full packaged-artifact regeneration provenance summary after validating the structured posture.
724    pub fn validated_summary_line(&self) -> Result<String, pleiades_compression::CompressionError> {
725        self.validate()?;
726        Ok(self.summary_line())
727    }
728}
729
730impl fmt::Display for PackagedArtifactRegenerationSummary {
731    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
732        f.write_str(&self.summary_line())
733    }
734}
735
736/// Returns the structured packaged-artifact regeneration provenance.
737pub fn packaged_artifact_regeneration_summary_details() -> PackagedArtifactRegenerationSummary {
738    static SUMMARY: OnceLock<PackagedArtifactRegenerationSummary> = OnceLock::new();
739    SUMMARY
740        .get_or_init(|| {
741            let artifact = packaged_artifact();
742            let summary = PackagedArtifactRegenerationSummary {
743                label: ARTIFACT_LABEL,
744                artifact_version: artifact.header.version,
745                source: packaged_artifact_source_text(),
746                source_revision: production_generation_source_summary_for_report(),
747                profile_id: ARTIFACT_PROFILE_ID,
748                checksum: artifact.checksum,
749                artifact_size_bytes: packaged_artifact_encoded_bytes(artifact),
750                generation_policy:
751                    PackagedArtifactGenerationPolicy::AdjacentSameBodyQuadraticWindows,
752                quantization_scales: packaged_artifact_quantization_scales_line(),
753                residual_bodies: artifact.residual_bodies(),
754                bodies: packaged_bodies().to_vec(),
755                normalized_intermediates: packaged_artifact_normalized_intermediate_summary_details(
756                ),
757                fit_envelope: packaged_artifact_fit_envelope_summary_details(),
758                reference_snapshot: reference_snapshot_summary(),
759            };
760            debug_assert!(summary.validate().is_ok());
761            summary
762        })
763        .clone()
764}
765
766/// Returns the structured normalized-intermediate provenance.
767pub fn packaged_artifact_normalized_intermediate_summary_details(
768) -> PackagedArtifactNormalizedIntermediateSummary {
769    let artifact = packaged_artifact();
770    let payload_checksum = fnv1a64(
771        format!(
772            "label={}; profile id={}; version={}; time range={}; source={}; source revision={}; body count={}; segments={}; residual-bearing segments={}; stored channels={}; residual channels={}; segment span days={:.12}..{:.12}; segment strategy={}; {}",
773            ARTIFACT_LABEL,
774            ARTIFACT_PROFILE_ID,
775            artifact.header.version,
776            artifact_time_range(artifact),
777            packaged_artifact_source_text(),
778            production_generation_source_summary_for_report(),
779            artifact.bodies.len(),
780            artifact.segment_count(),
781            artifact.residual_segment_count(),
782            packaged_artifact_channel_count(artifact, false),
783            packaged_artifact_channel_count(artifact, true),
784            packaged_artifact_segment_span_bounds(artifact).0,
785            packaged_artifact_segment_span_bounds(artifact).1,
786            PackagedArtifactGenerationPolicy::AdjacentSameBodyQuadraticWindows.segment_strategy(),
787            packaged_artifact_quantization_scales_line(),
788        )
789        .as_bytes(),
790    );
791    let summary = PackagedArtifactNormalizedIntermediateSummary {
792        label: ARTIFACT_LABEL,
793        artifact_version: artifact.header.version,
794        source: packaged_artifact_source_text(),
795        source_revision: production_generation_source_summary_for_report(),
796        profile_id: ARTIFACT_PROFILE_ID,
797        time_range: artifact_time_range(artifact),
798        generation_policy: PackagedArtifactGenerationPolicy::AdjacentSameBodyQuadraticWindows,
799        quantization_scales: packaged_artifact_quantization_scales_line(),
800        checksum: payload_checksum,
801        body_count: artifact.bodies.len(),
802        segment_count: artifact.segment_count(),
803        residual_segment_count: artifact.residual_segment_count(),
804        stored_channel_count: packaged_artifact_channel_count(artifact, false),
805        residual_channel_count: packaged_artifact_channel_count(artifact, true),
806        min_segment_span_days: packaged_artifact_segment_span_bounds(artifact).0,
807        max_segment_span_days: packaged_artifact_segment_span_bounds(artifact).1,
808    };
809    debug_assert!(summary.validate().is_ok());
810    summary
811}