Skip to main content

pleiades_data/coverage/
target.rs

1use super::*;
2
3/// Release-state for the packaged-artifact target thresholds.
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub enum PackagedArtifactTargetThresholdState {
6    /// Calibrated fit envelope is recorded, but production thresholds are not yet release-ready.
7    Draft,
8    /// Production thresholds have been finalized for the current profile.
9    ProductionReady,
10}
11
12/// Validation error for a packaged-artifact target-threshold state that is not release-ready.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub enum PackagedArtifactTargetThresholdStateValidationError {
15    /// The target-threshold state is still draft.
16    Draft,
17}
18
19impl fmt::Display for PackagedArtifactTargetThresholdStateValidationError {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            Self::Draft => write!(
23                f,
24                "the packaged-artifact target-threshold state is draft; production thresholds are not yet release-ready"
25            ),
26        }
27    }
28}
29
30impl std::error::Error for PackagedArtifactTargetThresholdStateValidationError {}
31
32impl PackagedArtifactTargetThresholdState {
33    /// Returns the compact label used in release-facing summaries.
34    pub const fn label(self) -> &'static str {
35        match self {
36            Self::Draft => {
37                "calibrated fit envelope recorded; production thresholds not yet release-ready"
38            }
39            Self::ProductionReady => "production thresholds recorded",
40        }
41    }
42
43    /// Returns a compact human-readable line for the target-threshold state.
44    pub fn summary_line(self) -> String {
45        format!("target-threshold state: {}", self)
46    }
47
48    /// Returns whether the target thresholds are finalized for production release.
49    pub const fn is_production_ready(self) -> bool {
50        matches!(self, Self::ProductionReady)
51    }
52
53    /// Returns `Ok(())` when the state is release-ready.
54    pub fn validate_production_ready(
55        self,
56    ) -> Result<(), PackagedArtifactTargetThresholdStateValidationError> {
57        if self.is_production_ready() {
58            Ok(())
59        } else {
60            Err(PackagedArtifactTargetThresholdStateValidationError::Draft)
61        }
62    }
63
64    /// Returns the validated target-threshold state as a compact human-readable line.
65    pub fn validated_summary_line(
66        self,
67    ) -> Result<String, PackagedArtifactTargetThresholdStateValidationError> {
68        self.validate_production_ready()?;
69        Ok(self.summary_line())
70    }
71}
72
73impl fmt::Display for PackagedArtifactTargetThresholdState {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        f.write_str(self.label())
76    }
77}
78
79const PACKAGED_ARTIFACT_TARGET_THRESHOLD_STATE: PackagedArtifactTargetThresholdState =
80    PackagedArtifactTargetThresholdState::ProductionReady;
81pub(crate) const PACKAGED_ARTIFACT_TARGET_THRESHOLD_SCOPES: &[&str] = &[
82    "luminaries",
83    "major planets",
84    "pluto",
85    "lunar points",
86    "selected asteroids",
87    "custom bodies",
88];
89
90/// Phase-2 corpus evidence used to keep the packaged-artifact threshold policy aligned
91/// with the current reference, fixture-exactness, comparison, hold-out, selected-asteroid, boundary-overlay, and production-generation corpora.
92#[derive(Clone, Debug, PartialEq)]
93pub struct PackagedArtifactPhase2CorpusAlignmentSummary {
94    /// Source-material evidence from the checked-in reference snapshot.
95    pub reference_snapshot_source: pleiades_jpl::ReferenceSnapshotSourceSummary,
96    /// Body-class coverage evidence from the checked-in reference snapshot.
97    pub reference_snapshot: pleiades_jpl::ReferenceSnapshotBodyClassCoverageSummary,
98    /// Exact J2000 fixture-exactness evidence from the checked-in reference snapshot.
99    pub reference_snapshot_exact_j2000: pleiades_jpl::ReferenceSnapshotExactJ2000EvidenceSummary,
100    /// Source-material evidence from the checked-in comparison snapshot.
101    pub comparison_snapshot_source: pleiades_jpl::ComparisonSnapshotSourceSummary,
102    /// Body-class coverage evidence from the checked-in comparison snapshot.
103    pub comparison_snapshot: pleiades_jpl::ComparisonSnapshotBodyClassCoverageSummary,
104    /// Source-material evidence from the checked-in independent hold-out snapshot.
105    pub independent_holdout_source: pleiades_jpl::IndependentHoldoutSourceSummary,
106    /// Body-class coverage evidence from the checked-in independent hold-out snapshot.
107    pub independent_holdout: pleiades_jpl::IndependentHoldoutSnapshotBodyClassCoverageSummary,
108    /// Source-backed evidence for the selected-asteroid validation corpus.
109    pub selected_asteroid_source: pleiades_jpl::SelectedAsteroidSourceSummary,
110    /// Source-backed window evidence for the selected-asteroid validation corpus.
111    pub selected_asteroid_source_windows: pleiades_jpl::SelectedAsteroidSourceWindowSummary,
112    /// Checked-in request-corpus evidence for the selected-asteroid validation corpus in the ecliptic frame.
113    pub selected_asteroid_source_request_corpus:
114        pleiades_jpl::SelectedAsteroidSourceRequestCorpusSummary,
115    /// Checked-in request-corpus evidence for the selected-asteroid validation corpus in the equatorial frame.
116    pub selected_asteroid_source_request_corpus_equatorial:
117        pleiades_jpl::SelectedAsteroidSourceRequestCorpusSummary,
118    /// Source-material evidence for the checked-in production-generation boundary overlay.
119    pub production_generation_boundary_source: pleiades_jpl::IndependentHoldoutSourceSummary,
120    /// Body-class coverage evidence for the checked-in production-generation corpus.
121    pub production_generation_body_class_coverage:
122        pleiades_jpl::ProductionGenerationSnapshotBodyClassCoverageSummary,
123    /// Combined provenance for the checked-in production-generation corpus.
124    pub production_generation_source: ProductionGenerationSourceSummary,
125}
126
127/// Validation error for a phase-2 corpus alignment summary that drifted from the current posture.
128#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
129pub enum PackagedArtifactPhase2CorpusAlignmentSummaryValidationError {
130    /// A summary field is out of sync with the current packaged-artifact posture.
131    FieldOutOfSync { field: &'static str },
132}
133
134impl fmt::Display for PackagedArtifactPhase2CorpusAlignmentSummaryValidationError {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        match self {
137            Self::FieldOutOfSync { field } => write!(
138                f,
139                "the packaged artifact phase-2 corpus alignment summary field `{field}` is out of sync with the current posture"
140            ),
141        }
142    }
143}
144
145impl std::error::Error for PackagedArtifactPhase2CorpusAlignmentSummaryValidationError {}
146
147fn phase2_corpus_alignment_validation_field_path(field: &'static str) -> &'static str {
148    match field {
149        "reference_snapshot_source" => "phase2_corpus_alignment.reference_snapshot_source",
150        "reference_snapshot" => "phase2_corpus_alignment.reference_snapshot",
151        "reference_snapshot_exact_j2000" => {
152            "phase2_corpus_alignment.reference_snapshot_exact_j2000"
153        }
154        "comparison_snapshot_source" => "phase2_corpus_alignment.comparison_snapshot_source",
155        "comparison_snapshot" => "phase2_corpus_alignment.comparison_snapshot",
156        "independent_holdout_source" => "phase2_corpus_alignment.independent_holdout_source",
157        "independent_holdout" => "phase2_corpus_alignment.independent_holdout",
158        "selected_asteroid_source" => "phase2_corpus_alignment.selected_asteroid_source",
159        "selected_asteroid_source_windows" => {
160            "phase2_corpus_alignment.selected_asteroid_source_windows"
161        }
162        "selected_asteroid_source_request_corpus" => {
163            "phase2_corpus_alignment.selected_asteroid_source_request_corpus"
164        }
165        "selected_asteroid_source_request_corpus_equatorial" => {
166            "phase2_corpus_alignment.selected_asteroid_source_request_corpus_equatorial"
167        }
168        "production_generation_boundary_source" => {
169            "phase2_corpus_alignment.production_generation_boundary_source"
170        }
171        "production_generation_body_class_coverage" => {
172            "phase2_corpus_alignment.production_generation_body_class_coverage"
173        }
174        "production_generation_source" => "phase2_corpus_alignment.production_generation_source",
175        _ => "phase2_corpus_alignment",
176    }
177}
178
179impl PackagedArtifactPhase2CorpusAlignmentSummary {
180    /// Returns the phase-2 corpus alignment posture as a compact human-readable line.
181    pub fn summary_line(&self) -> String {
182        format!(
183            "reference source={}; reference snapshot={}; reference exact J2000 evidence={}; comparison source={}; comparison snapshot={}; independent hold-out source={}; independent hold-out={}; selected asteroid source evidence={}; selected asteroid source windows={}; selected asteroid source request corpus={}; selected asteroid source request corpus equatorial={}; production generation boundary source={}; production generation body-class coverage={}; production generation source={}",
184            self.reference_snapshot_source.summary_line(),
185            self.reference_snapshot.summary_line(),
186            self.reference_snapshot_exact_j2000.summary_line(),
187            self.comparison_snapshot_source.summary_line(),
188            self.comparison_snapshot.summary_line(),
189            self.independent_holdout_source.summary_line(),
190            self.independent_holdout.summary_line(),
191            self.selected_asteroid_source.summary_line(),
192            self.selected_asteroid_source_windows.summary_line(),
193            self.selected_asteroid_source_request_corpus.summary_line(),
194            self.selected_asteroid_source_request_corpus_equatorial.summary_line(),
195            pleiades_jpl::format_production_generation_boundary_source_summary(
196                &self.production_generation_boundary_source,
197            ),
198            self.production_generation_body_class_coverage.summary_line(),
199            self.production_generation_source.summary_line(),
200        )
201    }
202
203    /// Returns `Ok(())` when the phase-2 corpus evidence still matches the current packaged-artifact posture.
204    pub fn validate(
205        &self,
206    ) -> Result<(), PackagedArtifactPhase2CorpusAlignmentSummaryValidationError> {
207        let Some(expected) = packaged_artifact_phase2_corpus_alignment_summary_details() else {
208            return Err(
209                PackagedArtifactPhase2CorpusAlignmentSummaryValidationError::FieldOutOfSync {
210                    field: "phase2_corpus_alignment",
211                },
212            );
213        };
214
215        let field_out_of_sync = |field| {
216            PackagedArtifactPhase2CorpusAlignmentSummaryValidationError::FieldOutOfSync { field }
217        };
218
219        if self.reference_snapshot_source != expected.reference_snapshot_source {
220            return Err(field_out_of_sync("reference_snapshot_source"));
221        }
222        self.reference_snapshot_source
223            .validate()
224            .map_err(|_| field_out_of_sync("reference_snapshot_source"))?;
225
226        if self.reference_snapshot != expected.reference_snapshot {
227            return Err(field_out_of_sync("reference_snapshot"));
228        }
229        self.reference_snapshot
230            .validate()
231            .map_err(|_| field_out_of_sync("reference_snapshot"))?;
232
233        if self.reference_snapshot_exact_j2000 != expected.reference_snapshot_exact_j2000 {
234            return Err(field_out_of_sync("reference_snapshot_exact_j2000"));
235        }
236        self.reference_snapshot_exact_j2000
237            .validate()
238            .map_err(|_| field_out_of_sync("reference_snapshot_exact_j2000"))?;
239
240        if self.comparison_snapshot_source != expected.comparison_snapshot_source {
241            return Err(field_out_of_sync("comparison_snapshot_source"));
242        }
243        self.comparison_snapshot_source
244            .validate()
245            .map_err(|_| field_out_of_sync("comparison_snapshot_source"))?;
246
247        if self.comparison_snapshot != expected.comparison_snapshot {
248            return Err(field_out_of_sync("comparison_snapshot"));
249        }
250        self.comparison_snapshot
251            .validate()
252            .map_err(|_| field_out_of_sync("comparison_snapshot"))?;
253
254        if self.independent_holdout_source != expected.independent_holdout_source {
255            return Err(field_out_of_sync("independent_holdout_source"));
256        }
257        self.independent_holdout_source
258            .validate()
259            .map_err(|_| field_out_of_sync("independent_holdout_source"))?;
260
261        if self.independent_holdout != expected.independent_holdout {
262            return Err(field_out_of_sync("independent_holdout"));
263        }
264        self.independent_holdout
265            .validate()
266            .map_err(|_| field_out_of_sync("independent_holdout"))?;
267
268        if self.selected_asteroid_source != expected.selected_asteroid_source {
269            return Err(field_out_of_sync("selected_asteroid_source"));
270        }
271        self.selected_asteroid_source
272            .validate()
273            .map_err(|_| field_out_of_sync("selected_asteroid_source"))?;
274
275        if self.selected_asteroid_source_windows != expected.selected_asteroid_source_windows {
276            return Err(field_out_of_sync("selected_asteroid_source_windows"));
277        }
278        self.selected_asteroid_source_windows
279            .validate()
280            .map_err(|_| field_out_of_sync("selected_asteroid_source_windows"))?;
281
282        if self.selected_asteroid_source_request_corpus
283            != expected.selected_asteroid_source_request_corpus
284        {
285            return Err(field_out_of_sync("selected_asteroid_source_request_corpus"));
286        }
287        self.selected_asteroid_source_request_corpus
288            .validate()
289            .map_err(|_| field_out_of_sync("selected_asteroid_source_request_corpus"))?;
290
291        if self.selected_asteroid_source_request_corpus_equatorial
292            != expected.selected_asteroid_source_request_corpus_equatorial
293        {
294            return Err(field_out_of_sync(
295                "selected_asteroid_source_request_corpus_equatorial",
296            ));
297        }
298        self.selected_asteroid_source_request_corpus_equatorial
299            .validate()
300            .map_err(|_| field_out_of_sync("selected_asteroid_source_request_corpus_equatorial"))?;
301
302        if self.production_generation_boundary_source
303            != expected.production_generation_boundary_source
304        {
305            return Err(field_out_of_sync("production_generation_boundary_source"));
306        }
307        self.production_generation_boundary_source
308            .validate()
309            .map_err(|_| field_out_of_sync("production_generation_boundary_source"))?;
310
311        if self.production_generation_body_class_coverage
312            != expected.production_generation_body_class_coverage
313        {
314            return Err(field_out_of_sync(
315                "production_generation_body_class_coverage",
316            ));
317        }
318        self.production_generation_body_class_coverage
319            .validate()
320            .map_err(|_| field_out_of_sync("production_generation_body_class_coverage"))?;
321
322        if self.production_generation_source != expected.production_generation_source {
323            return Err(field_out_of_sync("production_generation_source"));
324        }
325        self.production_generation_source
326            .validate()
327            .map_err(|_| field_out_of_sync("production_generation_source"))?;
328
329        Ok(())
330    }
331
332    /// Returns the validated phase-2 corpus alignment posture as a compact human-readable line.
333    pub fn validated_summary_line(
334        &self,
335    ) -> Result<String, PackagedArtifactPhase2CorpusAlignmentSummaryValidationError> {
336        self.validate()?;
337        Ok(self.summary_line())
338    }
339}
340
341impl fmt::Display for PackagedArtifactPhase2CorpusAlignmentSummary {
342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343        f.write_str(&self.summary_line())
344    }
345}
346
347/// Returns the structured phase-2 corpus alignment posture used to keep the packaged-artifact threshold policy aligned with the current corpus.
348pub fn packaged_artifact_phase2_corpus_alignment_summary_details(
349) -> Option<PackagedArtifactPhase2CorpusAlignmentSummary> {
350    Some(PackagedArtifactPhase2CorpusAlignmentSummary {
351        reference_snapshot_source: pleiades_jpl::reference_snapshot_source_summary(),
352        reference_snapshot: pleiades_jpl::reference_snapshot_body_class_coverage_summary()?,
353        reference_snapshot_exact_j2000:
354            pleiades_jpl::reference_snapshot_exact_j2000_evidence_summary()?,
355        comparison_snapshot_source: pleiades_jpl::comparison_snapshot_source_summary(),
356        comparison_snapshot: comparison_snapshot_body_class_coverage_summary()?,
357        independent_holdout_source: pleiades_jpl::independent_holdout_source_summary(),
358        independent_holdout: independent_holdout_snapshot_body_class_coverage_summary()?,
359        selected_asteroid_source: pleiades_jpl::selected_asteroid_source_evidence_summary()?,
360        selected_asteroid_source_windows: pleiades_jpl::selected_asteroid_source_window_summary()?,
361        selected_asteroid_source_request_corpus: selected_asteroid_source_request_corpus_summary(
362            CoordinateFrame::Ecliptic,
363        )?,
364        selected_asteroid_source_request_corpus_equatorial:
365            selected_asteroid_source_request_corpus_summary(CoordinateFrame::Equatorial)?,
366        production_generation_boundary_source:
367            pleiades_jpl::production_generation_boundary_source_summary(),
368        production_generation_body_class_coverage:
369            pleiades_jpl::production_generation_snapshot_body_class_coverage_summary()?,
370        production_generation_source: production_generation_source_summary(),
371    })
372}
373
374/// Structured target-threshold posture for the packaged artifact generator.
375#[derive(Clone, Debug, PartialEq)]
376pub struct PackagedArtifactTargetThresholdSummary {
377    /// Stable identifier for the release profile that the thresholds apply to.
378    pub profile_id: &'static str,
379    /// Current release posture for the production thresholds.
380    pub state: PackagedArtifactTargetThresholdState,
381    /// Body-class scopes covered by the current threshold policy.
382    pub scopes: &'static [&'static str],
383    /// Measured fit envelope captured for the current packaged artifact posture.
384    pub fit_envelope: PackagedArtifactFitEnvelopeSummary,
385    /// Body-class-specific fit envelopes captured for the current packaged artifact posture.
386    pub scope_envelopes: PackagedArtifactTargetThresholdScopeEnvelopesSummary,
387    /// Phase-2 corpus evidence that keeps the threshold posture aligned with the current corpus.
388    pub phase2_corpus_alignment: PackagedArtifactPhase2CorpusAlignmentSummary,
389}
390
391/// Validation error for a packaged-artifact target-threshold summary that drifted from the current posture.
392#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
393pub enum PackagedArtifactTargetThresholdSummaryValidationError {
394    /// A summary field is out of sync with the current packaged-artifact posture.
395    FieldOutOfSync { field: &'static str },
396}
397
398impl fmt::Display for PackagedArtifactTargetThresholdSummaryValidationError {
399    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400        match self {
401            Self::FieldOutOfSync { field } => write!(
402                f,
403                "the packaged artifact target-threshold summary field `{field}` is out of sync with the current posture"
404            ),
405        }
406    }
407}
408
409impl std::error::Error for PackagedArtifactTargetThresholdSummaryValidationError {}
410
411impl PackagedArtifactTargetThresholdSummary {
412    /// Returns the target-threshold posture as a compact human-readable line.
413    pub fn summary_line(&self) -> String {
414        format!(
415            "profile id={}; target thresholds: {}; scopes={}; {}; scope envelopes={}; phase 2 corpus alignment={}",
416            self.profile_id,
417            self.state,
418            self.scopes.join(", "),
419            self.fit_envelope.summary_line(),
420            join_display(&self.scope_envelopes.scope_envelopes),
421            self.phase2_corpus_alignment.summary_line(),
422        )
423    }
424
425    /// Returns `Ok(())` when the summary still matches the current packaged-artifact posture.
426    pub fn validate(&self) -> Result<(), PackagedArtifactTargetThresholdSummaryValidationError> {
427        if self.profile_id != ARTIFACT_PROFILE_ID {
428            return Err(
429                PackagedArtifactTargetThresholdSummaryValidationError::FieldOutOfSync {
430                    field: "profile_id",
431                },
432            );
433        }
434        self.state.validate_production_ready().map_err(|_| {
435            PackagedArtifactTargetThresholdSummaryValidationError::FieldOutOfSync { field: "state" }
436        })?;
437        if self.state != PACKAGED_ARTIFACT_TARGET_THRESHOLD_STATE {
438            return Err(
439                PackagedArtifactTargetThresholdSummaryValidationError::FieldOutOfSync {
440                    field: "state",
441                },
442            );
443        }
444        if self.scopes != PACKAGED_ARTIFACT_TARGET_THRESHOLD_SCOPES {
445            return Err(
446                PackagedArtifactTargetThresholdSummaryValidationError::FieldOutOfSync {
447                    field: "scopes",
448                },
449            );
450        }
451
452        let expected_fit_envelope = packaged_artifact_fit_envelope_summary_details();
453        if self.fit_envelope != expected_fit_envelope {
454            return Err(
455                PackagedArtifactTargetThresholdSummaryValidationError::FieldOutOfSync {
456                    field: "fit_envelope",
457                },
458            );
459        }
460        if self.state.is_production_ready() {
461            let thresholds = packaged_artifact_fit_threshold_summary_details();
462            self.fit_envelope
463                .validate_against_thresholds(&thresholds)
464                .map_err(|_| {
465                    PackagedArtifactTargetThresholdSummaryValidationError::FieldOutOfSync {
466                        field: "fit_envelope",
467                    }
468                })?;
469        }
470        let expected_scope_envelopes =
471            packaged_artifact_target_threshold_scope_envelopes_summary_details();
472        if self.scope_envelopes != expected_scope_envelopes {
473            return Err(
474                PackagedArtifactTargetThresholdSummaryValidationError::FieldOutOfSync {
475                    field: "scope_envelopes",
476                },
477            );
478        }
479        self.scope_envelopes.validate().map_err(|_| {
480            PackagedArtifactTargetThresholdSummaryValidationError::FieldOutOfSync {
481                field: "scope_envelopes",
482            }
483        })?;
484
485        let expected_phase2_corpus_alignment =
486            packaged_artifact_phase2_corpus_alignment_summary_details().ok_or(
487                PackagedArtifactTargetThresholdSummaryValidationError::FieldOutOfSync {
488                    field: "phase2_corpus_alignment",
489                },
490            )?;
491        self.phase2_corpus_alignment
492            .validate()
493            .map_err(|error| match error {
494                PackagedArtifactPhase2CorpusAlignmentSummaryValidationError::FieldOutOfSync {
495                    field,
496                } => PackagedArtifactTargetThresholdSummaryValidationError::FieldOutOfSync {
497                    field: phase2_corpus_alignment_validation_field_path(field),
498                },
499            })?;
500        if self.phase2_corpus_alignment != expected_phase2_corpus_alignment {
501            return Err(
502                PackagedArtifactTargetThresholdSummaryValidationError::FieldOutOfSync {
503                    field: "phase2_corpus_alignment",
504                },
505            );
506        }
507
508        let thresholds = packaged_artifact_fit_threshold_summary_details();
509        for scope_envelope in &self.scope_envelopes.scope_envelopes {
510            scope_envelope
511                .fit_envelope
512                .validate_against_thresholds(&thresholds)
513                .map_err(|_| {
514                    PackagedArtifactTargetThresholdSummaryValidationError::FieldOutOfSync {
515                        field: "scope_envelopes",
516                    }
517                })?;
518        }
519
520        Ok(())
521    }
522
523    /// Returns the validated target-threshold posture as a compact human-readable line.
524    pub fn validated_summary_line(
525        &self,
526    ) -> Result<String, PackagedArtifactTargetThresholdSummaryValidationError> {
527        self.validate()?;
528        Ok(self.summary_line())
529    }
530}
531
532impl fmt::Display for PackagedArtifactTargetThresholdSummary {
533    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
534        f.write_str(&self.summary_line())
535    }
536}
537
538/// Returns the current packaged-artifact target-threshold summary record.
539pub fn packaged_artifact_target_threshold_summary_details() -> PackagedArtifactTargetThresholdSummary
540{
541    let summary = PackagedArtifactTargetThresholdSummary {
542        profile_id: ARTIFACT_PROFILE_ID,
543        state: PACKAGED_ARTIFACT_TARGET_THRESHOLD_STATE,
544        scopes: PACKAGED_ARTIFACT_TARGET_THRESHOLD_SCOPES,
545        fit_envelope: packaged_artifact_fit_envelope_summary_details(),
546        scope_envelopes: packaged_artifact_target_threshold_scope_envelopes_summary_details(),
547        phase2_corpus_alignment: packaged_artifact_phase2_corpus_alignment_summary_details()
548            .expect("phase-2 corpus evidence should be available"),
549    };
550    debug_assert!(summary.validate().is_ok());
551    summary
552}
553
554/// Structured sync summary for the packaged-artifact source-fit and hold-out checks.
555#[derive(Clone, Debug, PartialEq)]
556pub struct PackagedArtifactSourceFitHoldoutSyncSummary {
557    /// Calibrated fit thresholds used by the current packaged-artifact posture.
558    pub fit_thresholds: PackagedArtifactFitThresholdSummary,
559    /// Release-threshold posture that keeps the phase-2 corpus alignment synchronized.
560    pub target_thresholds: PackagedArtifactTargetThresholdSummary,
561    /// Phase-2 corpus evidence that anchors the threshold posture to current source coverage.
562    pub phase2_corpus_alignment: PackagedArtifactPhase2CorpusAlignmentSummary,
563}
564
565/// Validation error for a packaged-artifact source-fit and hold-out sync summary that drifted from the current posture.
566#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
567pub enum PackagedArtifactSourceFitHoldoutSyncSummaryValidationError {
568    /// A summary field is out of sync with the current packaged-artifact posture.
569    FieldOutOfSync { field: &'static str },
570}
571
572impl fmt::Display for PackagedArtifactSourceFitHoldoutSyncSummaryValidationError {
573    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
574        match self {
575            Self::FieldOutOfSync { field } => write!(
576                f,
577                "the packaged artifact source-fit and hold-out sync summary field `{field}` is out of sync with the current posture"
578            ),
579        }
580    }
581}
582
583impl std::error::Error for PackagedArtifactSourceFitHoldoutSyncSummaryValidationError {}
584
585impl PackagedArtifactSourceFitHoldoutSyncSummary {
586    /// Returns the source-fit and hold-out sync posture as a compact human-readable line.
587    pub fn summary_line(&self) -> String {
588        format!(
589            "source-fit and hold-out sync: fit thresholds={}; target thresholds={}; phase 2 corpus alignment={}",
590            self.fit_thresholds.summary_line(),
591            self.target_thresholds.summary_line(),
592            self.phase2_corpus_alignment.summary_line(),
593        )
594    }
595
596    /// Returns `Ok(())` when the sync summary still matches the current packaged-artifact posture.
597    pub fn validate(
598        &self,
599    ) -> Result<(), PackagedArtifactSourceFitHoldoutSyncSummaryValidationError> {
600        let expected_fit_thresholds = packaged_artifact_fit_threshold_summary_details();
601        if self.fit_thresholds != expected_fit_thresholds {
602            return Err(
603                PackagedArtifactSourceFitHoldoutSyncSummaryValidationError::FieldOutOfSync {
604                    field: "fit_thresholds",
605                },
606            );
607        }
608        self.fit_thresholds.validate().map_err(|_| {
609            PackagedArtifactSourceFitHoldoutSyncSummaryValidationError::FieldOutOfSync {
610                field: "fit_thresholds",
611            }
612        })?;
613
614        let expected_target_thresholds = packaged_artifact_target_threshold_summary_details();
615        if self.target_thresholds != expected_target_thresholds {
616            return Err(
617                PackagedArtifactSourceFitHoldoutSyncSummaryValidationError::FieldOutOfSync {
618                    field: "target_thresholds",
619                },
620            );
621        }
622        self.target_thresholds.validate().map_err(|_| {
623            PackagedArtifactSourceFitHoldoutSyncSummaryValidationError::FieldOutOfSync {
624                field: "target_thresholds",
625            }
626        })?;
627
628        let expected_phase2_corpus_alignment =
629            packaged_artifact_phase2_corpus_alignment_summary_details().ok_or(
630                PackagedArtifactSourceFitHoldoutSyncSummaryValidationError::FieldOutOfSync {
631                    field: "phase2_corpus_alignment",
632                },
633            )?;
634        self.phase2_corpus_alignment
635            .validate()
636            .map_err(|error| match error {
637                PackagedArtifactPhase2CorpusAlignmentSummaryValidationError::FieldOutOfSync {
638                    field,
639                } => PackagedArtifactSourceFitHoldoutSyncSummaryValidationError::FieldOutOfSync {
640                    field: phase2_corpus_alignment_validation_field_path(field),
641                },
642            })?;
643        if self.phase2_corpus_alignment != expected_phase2_corpus_alignment {
644            return Err(
645                PackagedArtifactSourceFitHoldoutSyncSummaryValidationError::FieldOutOfSync {
646                    field: "phase2_corpus_alignment",
647                },
648            );
649        }
650
651        Ok(())
652    }
653
654    /// Returns the validated source-fit and hold-out sync posture as a compact human-readable line.
655    pub fn validated_summary_line(
656        &self,
657    ) -> Result<String, PackagedArtifactSourceFitHoldoutSyncSummaryValidationError> {
658        self.validate()?;
659        Ok(self.summary_line())
660    }
661}
662
663impl fmt::Display for PackagedArtifactSourceFitHoldoutSyncSummary {
664    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
665        f.write_str(&self.summary_line())
666    }
667}
668
669/// Returns the current packaged-artifact source-fit and hold-out sync summary record.
670pub fn packaged_artifact_source_fit_holdout_sync_summary_details(
671) -> PackagedArtifactSourceFitHoldoutSyncSummary {
672    let summary = PackagedArtifactSourceFitHoldoutSyncSummary {
673        fit_thresholds: packaged_artifact_fit_threshold_summary_details(),
674        target_thresholds: packaged_artifact_target_threshold_summary_details(),
675        phase2_corpus_alignment: packaged_artifact_phase2_corpus_alignment_summary_details()
676            .expect("phase-2 corpus evidence should be available"),
677    };
678    debug_assert!(summary.validate().is_ok());
679    summary
680}