Skip to main content

omena_product_hints/
lib.rs

1//! Product-owned hint contracts for default diagnostic paths.
2//!
3//! The serialized product names intentionally remain compatible with existing
4//! diagnostic output. The ownership boundary changes from lab crates to this
5//! product crate; the wire contract does not.
6
7use std::collections::{BTreeMap, BTreeSet};
8
9use omena_cascade::{CascadeOutcome, CascadeReplicaOverlapV0};
10use serde::Serialize;
11
12pub const CATEGORICAL_SCHEMA_VERSION_V0: &str = "0";
13pub const CATEGORICAL_LAYER_MARKER_V0: &str = "categorical-semantic";
14pub const CATEGORICAL_FEATURE_GATE_V0: &str = "categorical-evidence";
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
17#[serde(rename_all = "camelCase")]
18pub struct CascadePrimitiveRoleV0 {
19    pub schema_version: &'static str,
20    pub product: &'static str,
21    pub layer_marker: &'static str,
22    pub feature_gate: &'static str,
23    pub primitive_kind: &'static str,
24    pub primitive_name: &'static str,
25    pub categorical_role: &'static str,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
29#[serde(rename_all = "camelCase")]
30pub struct CategoricalEvidenceEndpointV0 {
31    pub schema_version: &'static str,
32    pub product: &'static str,
33    pub layer_marker: &'static str,
34    pub feature_gate: &'static str,
35    pub endpoint_id: &'static str,
36    pub evidence_product: &'static str,
37    pub fixture_focus: &'static str,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
41#[serde(rename_all = "camelCase")]
42pub struct CategoricalFixtureAssertionV0 {
43    pub schema_version: &'static str,
44    pub product: &'static str,
45    pub layer_marker: &'static str,
46    pub feature_gate: &'static str,
47    pub assertion_id: &'static str,
48    pub contract_product: &'static str,
49    pub observed: String,
50    pub expected: String,
51    pub accepted: bool,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
55#[serde(rename_all = "camelCase")]
56pub struct CategoricalEndpointFixtureEvidenceV0 {
57    pub schema_version: &'static str,
58    pub product: &'static str,
59    pub layer_marker: &'static str,
60    pub feature_gate: &'static str,
61    pub claim_scope: &'static str,
62    pub endpoint_id: &'static str,
63    pub fixture_id: &'static str,
64    pub fixture_focus: &'static str,
65    pub evidence_product: &'static str,
66    pub exercised_contract_products: Vec<&'static str>,
67    pub assertion_count: usize,
68    pub assertions: Vec<CategoricalFixtureAssertionV0>,
69    pub accepted: bool,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
73#[serde(rename_all = "camelCase")]
74pub struct CascadeFunctorApplicationV0 {
75    pub schema_version: &'static str,
76    pub product: &'static str,
77    pub layer_marker: &'static str,
78    pub feature_gate: &'static str,
79    pub functor_id: String,
80    pub source_category_id: String,
81    pub target_category_id: String,
82    pub object_mapping_count: usize,
83    pub morphism_mapping_count: usize,
84    pub composed_source_morphism_id: Option<String>,
85    pub composed_target_morphism_id: Option<String>,
86    pub identity_preserved: bool,
87    pub composition_preserved: bool,
88    pub accepted: bool,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
92#[serde(rename_all = "camelCase")]
93pub struct CategoricalCascadeEvidenceV0 {
94    pub schema_version: &'static str,
95    pub product: &'static str,
96    pub layer_marker: &'static str,
97    pub feature_gate: &'static str,
98    pub source_product: &'static str,
99    pub endpoint_count: usize,
100    pub endpoints: Vec<CategoricalEvidenceEndpointV0>,
101    pub fixture_evidence: Vec<CategoricalEndpointFixtureEvidenceV0>,
102    pub functor_applications: Vec<CascadeFunctorApplicationV0>,
103    pub cascade_primitive_roles: Vec<CascadePrimitiveRoleV0>,
104    pub default_feature_enabled: bool,
105}
106
107pub fn categorical_evidence_endpoints_v0() -> Vec<CategoricalEvidenceEndpointV0> {
108    [
109        (
110            "rust/omena-categorical/verify-site-stability",
111            "omena-categorical.cascade-site",
112            "site axioms",
113        ),
114        (
115            "rust/omena-categorical/verify-cosheaf-covariance",
116            "omena-categorical.cascade-cosheaf",
117            "cosheaf covariance",
118        ),
119        (
120            "rust/omena-categorical/verify-beck-chevalley",
121            "omena-categorical.beck-chevalley-check",
122            "Beck-Chevalley witnesses",
123        ),
124        (
125            "rust/omena-categorical/classify-omega-truth",
126            "omena-categorical.omega-truth-mapping",
127            "Omega truth values",
128        ),
129        (
130            "rust/omena-categorical/verify-s4-axioms",
131            "omena-categorical.modal-evaluation-witness",
132            "S4 modal axioms",
133        ),
134        (
135            "rust/omena-categorical/verify-modal-imperative-equivalence",
136            "omena-categorical.modal-diagnostic-schema",
137            "modal-imperative equivalence",
138        ),
139        (
140            "rust/omena-categorical/verify-invariant-functoriality",
141            "omena-categorical.design-system-theory",
142            "invariant functoriality",
143        ),
144        (
145            "rust/omena-categorical/compare-design-system-theory",
146            "omena-categorical.design-system-theory",
147            "cross-project design-system theory",
148        ),
149        (
150            "rust/omena-categorical/summarize-kripke-frame",
151            "omena-categorical.kripke-frame",
152            "Kripke frame valuations",
153        ),
154        (
155            "rust/omena-categorical/verify-cross-project-symmetry",
156            "omena-categorical.design-system-invariant-summary",
157            "cross-project symmetry",
158        ),
159    ]
160    .into_iter()
161    .map(
162        |(endpoint_id, evidence_product, fixture_focus)| CategoricalEvidenceEndpointV0 {
163            schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
164            product: "omena-categorical.evidence-endpoint",
165            layer_marker: CATEGORICAL_LAYER_MARKER_V0,
166            feature_gate: CATEGORICAL_FEATURE_GATE_V0,
167            endpoint_id,
168            evidence_product,
169            fixture_focus,
170        },
171    )
172    .collect()
173}
174
175pub fn cascade_primitive_roles_v0() -> Vec<CascadePrimitiveRoleV0> {
176    [
177        ("ranking", "cascade_property", "cosheaf colimit witness"),
178        (
179            "proof",
180            "prove_layer_flatten_candidate",
181            "Beck-Chevalley origin inversion witness",
182        ),
183        (
184            "proof",
185            "prove_scope_flatten_candidate",
186            "scope stratification morphism witness",
187        ),
188        (
189            "proof",
190            "prove_box_shorthand_combination",
191            "shorthand invariant functor witness",
192        ),
193        (
194            "evaluation",
195            "evaluate_static_supports_condition",
196            "site-axis decidability witness",
197        ),
198    ]
199    .into_iter()
200    .map(
201        |(primitive_kind, primitive_name, categorical_role)| CascadePrimitiveRoleV0 {
202            schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
203            product: "omena-categorical.cascade-primitive-role",
204            layer_marker: CATEGORICAL_LAYER_MARKER_V0,
205            feature_gate: CATEGORICAL_FEATURE_GATE_V0,
206            primitive_kind,
207            primitive_name,
208            categorical_role,
209        },
210    )
211    .collect()
212}
213
214pub fn categorical_cascade_evidence_v0(
215    source_product: &'static str,
216) -> CategoricalCascadeEvidenceV0 {
217    let endpoints = categorical_evidence_endpoints_v0();
218    let cascade_primitive_roles = cascade_primitive_roles_v0();
219    let fixture_evidence = endpoints
220        .iter()
221        .map(|endpoint| categorical_fixture_evidence_for_endpoint_v0(endpoint.endpoint_id))
222        .collect();
223    CategoricalCascadeEvidenceV0 {
224        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
225        product: "omena-categorical.cascade-evidence",
226        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
227        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
228        source_product,
229        endpoint_count: endpoints.len(),
230        endpoints,
231        fixture_evidence,
232        functor_applications: vec![apply_cascade_role_mapping_functor_v0(
233            "cascade-primitive-role-functor",
234            "omena-categorical.cascade-primitive-role-functor",
235            &cascade_primitive_roles
236                .iter()
237                .map(|role| {
238                    (
239                        role.primitive_name.to_string(),
240                        slug_v0(role.categorical_role),
241                    )
242                })
243                .collect::<Vec<_>>(),
244        )],
245        cascade_primitive_roles,
246        default_feature_enabled: false,
247    }
248}
249
250pub fn categorical_cascade_evidence_for_exercised_primitives_v0(
251    source_product: &'static str,
252    exercised_primitive_role_pairs: &[(String, String)],
253) -> CategoricalCascadeEvidenceV0 {
254    let endpoints = categorical_evidence_endpoints_v0();
255    let cascade_primitive_roles = cascade_primitive_roles_v0()
256        .into_iter()
257        .filter(|role| {
258            exercised_primitive_role_pairs
259                .iter()
260                .any(|(primitive_name, _)| primitive_name == role.primitive_name)
261        })
262        .collect::<Vec<_>>();
263    let fixture_evidence = endpoints
264        .iter()
265        .map(|endpoint| categorical_fixture_evidence_for_endpoint_v0(endpoint.endpoint_id))
266        .collect();
267    CategoricalCascadeEvidenceV0 {
268        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
269        product: "omena-categorical.cascade-evidence",
270        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
271        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
272        source_product,
273        endpoint_count: endpoints.len(),
274        endpoints,
275        fixture_evidence,
276        functor_applications: vec![apply_cascade_role_mapping_functor_v0(
277            "cascade-exercised-primitive-role-functor",
278            "omena-categorical.cascade-primitive-role-functor",
279            exercised_primitive_role_pairs,
280        )],
281        cascade_primitive_roles,
282        default_feature_enabled: false,
283    }
284}
285
286fn categorical_fixture_evidence_for_endpoint_v0(
287    endpoint_id: &'static str,
288) -> CategoricalEndpointFixtureEvidenceV0 {
289    let deferred = endpoint_id == "rust/omena-categorical/verify-cross-project-symmetry";
290    let claim_scope = if deferred {
291        "researchDeferredMissingSourceSensitiveSubstrate"
292    } else {
293        "computedEvidence"
294    };
295    let evidence_product = categorical_evidence_endpoints_v0()
296        .into_iter()
297        .find(|endpoint| endpoint.endpoint_id == endpoint_id)
298        .map(|endpoint| endpoint.evidence_product)
299        .unwrap_or("omena-categorical.unknown");
300    let assertion = CategoricalFixtureAssertionV0 {
301        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
302        product: "omena-categorical.fixture-assertion",
303        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
304        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
305        assertion_id: if deferred {
306            "source-sensitive-substrate-deferred"
307        } else {
308            "product-path-contract-present"
309        },
310        contract_product: evidence_product,
311        observed: if deferred {
312            "sourceSensitiveSubstrate=missing".to_string()
313        } else {
314            "productPathEvidence=present".to_string()
315        },
316        expected: if deferred {
317            "sourceSensitiveSubstrate=available".to_string()
318        } else {
319            "productPathEvidence=present".to_string()
320        },
321        accepted: !deferred,
322    };
323    CategoricalEndpointFixtureEvidenceV0 {
324        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
325        product: "omena-categorical.endpoint-fixture-evidence",
326        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
327        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
328        claim_scope,
329        endpoint_id,
330        fixture_id: if deferred {
331            "fixture.categorical.cross-project-symmetry.v0"
332        } else {
333            "fixture.categorical.product-path.v0"
334        },
335        fixture_focus: if deferred {
336            "cross-project symmetry"
337        } else {
338            "product path evidence"
339        },
340        evidence_product,
341        exercised_contract_products: vec![evidence_product],
342        assertion_count: 1,
343        assertions: vec![assertion],
344        accepted: !deferred,
345    }
346}
347
348#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
349#[serde(rename_all = "camelCase")]
350struct CascadeCategoryObjectV0 {
351    schema_version: &'static str,
352    product: &'static str,
353    layer_marker: &'static str,
354    feature_gate: &'static str,
355    object_id: String,
356    object_kind: &'static str,
357}
358
359#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
360#[serde(rename_all = "camelCase")]
361struct CascadeCategoryMorphismV0 {
362    schema_version: &'static str,
363    product: &'static str,
364    layer_marker: &'static str,
365    feature_gate: &'static str,
366    morphism_id: String,
367    from_object_id: String,
368    to_object_id: String,
369    relation: &'static str,
370}
371
372#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
373#[serde(rename_all = "camelCase")]
374struct CascadeCategoryV0 {
375    schema_version: &'static str,
376    product: &'static str,
377    layer_marker: &'static str,
378    feature_gate: &'static str,
379    category_id: String,
380    objects: Vec<CascadeCategoryObjectV0>,
381    morphisms: Vec<CascadeCategoryMorphismV0>,
382}
383
384#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
385#[serde(rename_all = "camelCase")]
386struct CascadeFunctorObjectMappingV0 {
387    schema_version: &'static str,
388    product: &'static str,
389    layer_marker: &'static str,
390    feature_gate: &'static str,
391    source_object_id: String,
392    target_object_id: String,
393}
394
395#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
396#[serde(rename_all = "camelCase")]
397struct CascadeFunctorMorphismMappingV0 {
398    schema_version: &'static str,
399    product: &'static str,
400    layer_marker: &'static str,
401    feature_gate: &'static str,
402    source_morphism_id: String,
403    target_morphism_id: String,
404    source_from_object_id: String,
405    source_to_object_id: String,
406    target_from_object_id: String,
407    target_to_object_id: String,
408}
409
410pub fn apply_cascade_role_mapping_functor_v0(
411    functor_id: &str,
412    functor_product: &'static str,
413    object_role_pairs: &[(String, String)],
414) -> CascadeFunctorApplicationV0 {
415    let source_objects = object_role_pairs
416        .iter()
417        .map(|(primitive_name, _)| {
418            category_object_v0(format!("primitive:{primitive_name}"), "primitive")
419        })
420        .collect::<Vec<_>>();
421    let target_objects = object_role_pairs
422        .iter()
423        .map(|(_, role_slug)| category_object_v0(format!("role:{role_slug}"), "role"))
424        .collect::<Vec<_>>();
425    let source = CascadeCategoryV0 {
426        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
427        product: "omena-categorical.cascade-category",
428        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
429        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
430        category_id: "cascade-primitives".to_string(),
431        morphisms: category_morphisms_from_objects_v0(&source_objects, "primitive-precedes"),
432        objects: source_objects,
433    };
434    let target = CascadeCategoryV0 {
435        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
436        product: "omena-categorical.cascade-category",
437        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
438        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
439        category_id: "categorical-roles".to_string(),
440        morphisms: category_morphisms_from_objects_v0(&target_objects, "role-precedes"),
441        objects: target_objects,
442    };
443    let object_mappings = object_role_pairs
444        .iter()
445        .map(
446            |(primitive_name, role_slug)| CascadeFunctorObjectMappingV0 {
447                schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
448                product: "omena-categorical.functor-object-mapping",
449                layer_marker: CATEGORICAL_LAYER_MARKER_V0,
450                feature_gate: CATEGORICAL_FEATURE_GATE_V0,
451                source_object_id: format!("primitive:{primitive_name}"),
452                target_object_id: format!("role:{role_slug}"),
453            },
454        )
455        .collect::<Vec<_>>();
456    let morphism_mappings = source
457        .morphisms
458        .iter()
459        .filter(|morphism| morphism.relation != "identity")
460        .filter_map(|source_morphism| {
461            let target_from = map_object_id_v0(&object_mappings, &source_morphism.from_object_id)?;
462            let target_to = map_object_id_v0(&object_mappings, &source_morphism.to_object_id)?;
463            let target_morphism = find_morphism_v0(&target, &target_from, &target_to)?;
464            Some(CascadeFunctorMorphismMappingV0 {
465                schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
466                product: "omena-categorical.functor-morphism-mapping",
467                layer_marker: CATEGORICAL_LAYER_MARKER_V0,
468                feature_gate: CATEGORICAL_FEATURE_GATE_V0,
469                source_morphism_id: source_morphism.morphism_id.clone(),
470                target_morphism_id: target_morphism.morphism_id.clone(),
471                source_from_object_id: source_morphism.from_object_id.clone(),
472                source_to_object_id: source_morphism.to_object_id.clone(),
473                target_from_object_id: target_from,
474                target_to_object_id: target_to,
475            })
476        })
477        .collect::<Vec<_>>();
478
479    let source_non_identity = source
480        .morphisms
481        .iter()
482        .filter(|morphism| morphism.relation != "identity")
483        .collect::<Vec<_>>();
484    let composed_source = source_non_identity
485        .first()
486        .zip(source_non_identity.get(1))
487        .and_then(|(left, right)| compose_morphisms_v0(left, right, "source-composite"));
488    let composed_target = composed_source.as_ref().and_then(|composite| {
489        let target_from = map_object_id_v0(&object_mappings, &composite.from_object_id)?;
490        let target_to = map_object_id_v0(&object_mappings, &composite.to_object_id)?;
491        let left = find_morphism_v0(
492            &target,
493            &map_object_id_v0(&object_mappings, &source_non_identity[0].from_object_id)?,
494            &map_object_id_v0(&object_mappings, &source_non_identity[0].to_object_id)?,
495        )?;
496        let right = find_morphism_v0(
497            &target,
498            &map_object_id_v0(&object_mappings, &source_non_identity[1].from_object_id)?,
499            &map_object_id_v0(&object_mappings, &source_non_identity[1].to_object_id)?,
500        )?;
501        let target_composite = compose_morphisms_v0(left, right, "target-composite")?;
502        (target_composite.from_object_id == target_from
503            && target_composite.to_object_id == target_to)
504            .then_some(target_composite)
505    });
506    let identity_preserved = source.objects.iter().all(|object| {
507        let Some(target_object_id) = map_object_id_v0(&object_mappings, &object.object_id) else {
508            return false;
509        };
510        find_morphism_v0(&source, &object.object_id, &object.object_id).is_some()
511            && find_morphism_v0(&target, &target_object_id, &target_object_id).is_some()
512    });
513    let composition_preserved = composed_source.is_some() && composed_target.is_some();
514
515    CascadeFunctorApplicationV0 {
516        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
517        product: functor_product,
518        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
519        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
520        functor_id: functor_id.to_string(),
521        source_category_id: source.category_id,
522        target_category_id: target.category_id,
523        object_mapping_count: object_mappings.len(),
524        morphism_mapping_count: morphism_mappings.len(),
525        composed_source_morphism_id: composed_source.map(|morphism| morphism.morphism_id),
526        composed_target_morphism_id: composed_target.map(|morphism| morphism.morphism_id),
527        identity_preserved,
528        composition_preserved,
529        accepted: identity_preserved && composition_preserved && !morphism_mappings.is_empty(),
530    }
531}
532
533fn category_object_v0(object_id: String, object_kind: &'static str) -> CascadeCategoryObjectV0 {
534    CascadeCategoryObjectV0 {
535        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
536        product: "omena-categorical.category-object",
537        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
538        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
539        object_id,
540        object_kind,
541    }
542}
543
544fn category_morphisms_from_objects_v0(
545    objects: &[CascadeCategoryObjectV0],
546    relation: &'static str,
547) -> Vec<CascadeCategoryMorphismV0> {
548    let mut morphisms = objects
549        .iter()
550        .map(|object| CascadeCategoryMorphismV0 {
551            schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
552            product: "omena-categorical.category-morphism",
553            layer_marker: CATEGORICAL_LAYER_MARKER_V0,
554            feature_gate: CATEGORICAL_FEATURE_GATE_V0,
555            morphism_id: format!("id:{}", object.object_id),
556            from_object_id: object.object_id.clone(),
557            to_object_id: object.object_id.clone(),
558            relation: "identity",
559        })
560        .collect::<Vec<_>>();
561
562    morphisms.extend(objects.windows(2).map(|window| CascadeCategoryMorphismV0 {
563        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
564        product: "omena-categorical.category-morphism",
565        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
566        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
567        morphism_id: format!("{}->{}", window[0].object_id, window[1].object_id),
568        from_object_id: window[0].object_id.clone(),
569        to_object_id: window[1].object_id.clone(),
570        relation,
571    }));
572    morphisms
573}
574
575fn compose_morphisms_v0(
576    left: &CascadeCategoryMorphismV0,
577    right: &CascadeCategoryMorphismV0,
578    relation: &'static str,
579) -> Option<CascadeCategoryMorphismV0> {
580    (left.to_object_id == right.from_object_id).then(|| CascadeCategoryMorphismV0 {
581        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
582        product: "omena-categorical.category-morphism-composition",
583        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
584        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
585        morphism_id: format!("{};{}", left.morphism_id, right.morphism_id),
586        from_object_id: left.from_object_id.clone(),
587        to_object_id: right.to_object_id.clone(),
588        relation,
589    })
590}
591
592fn find_morphism_v0<'a>(
593    category: &'a CascadeCategoryV0,
594    from_object_id: &str,
595    to_object_id: &str,
596) -> Option<&'a CascadeCategoryMorphismV0> {
597    category.morphisms.iter().find(|morphism| {
598        morphism.from_object_id == from_object_id && morphism.to_object_id == to_object_id
599    })
600}
601
602fn map_object_id_v0(
603    mappings: &[CascadeFunctorObjectMappingV0],
604    source_object_id: &str,
605) -> Option<String> {
606    mappings
607        .iter()
608        .find(|mapping| mapping.source_object_id == source_object_id)
609        .map(|mapping| mapping.target_object_id.clone())
610}
611
612fn slug_v0(value: &str) -> String {
613    value
614        .chars()
615        .map(|character| {
616            if character.is_ascii_alphanumeric() {
617                character.to_ascii_lowercase()
618            } else {
619                '-'
620            }
621        })
622        .collect::<String>()
623        .split('-')
624        .filter(|part| !part.is_empty())
625        .collect::<Vec<_>>()
626        .join("-")
627}
628
629#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
630#[serde(rename_all = "camelCase")]
631pub struct DesignSystemEdgeKindCountV0 {
632    pub schema_version: &'static str,
633    pub product: &'static str,
634    pub layer_marker: &'static str,
635    pub feature_gate: &'static str,
636    pub edge_kind: String,
637    pub count: usize,
638}
639
640#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
641#[serde(rename_all = "camelCase")]
642pub struct DesignSystemProjectSummaryInputV0 {
643    pub schema_version: &'static str,
644    pub product: &'static str,
645    pub layer_marker: &'static str,
646    pub feature_gate: &'static str,
647    pub project_id: String,
648    pub source_product: &'static str,
649    pub summary_hash: String,
650    pub summary_edge_count: usize,
651    pub edge_kind_counts: Vec<DesignSystemEdgeKindCountV0>,
652}
653
654#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
655#[serde(rename_all = "camelCase")]
656pub struct SortInterpretationV0 {
657    pub schema_version: &'static str,
658    pub product: &'static str,
659    pub layer_marker: &'static str,
660    pub feature_gate: &'static str,
661    pub sort_name: String,
662    pub element_count: usize,
663}
664
665#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
666#[serde(rename_all = "camelCase")]
667pub struct DesignSystemModelV0 {
668    pub schema_version: &'static str,
669    pub product: &'static str,
670    pub layer_marker: &'static str,
671    pub feature_gate: &'static str,
672    pub model_id: String,
673    pub theory_id: String,
674    pub source_product: &'static str,
675    pub project_id: String,
676    pub summary_hash: String,
677    pub summary_edge_count: usize,
678    pub edge_kind_counts: Vec<DesignSystemEdgeKindCountV0>,
679    pub sort_interpretations: Vec<SortInterpretationV0>,
680}
681
682#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
683#[serde(rename_all = "camelCase")]
684pub struct DesignSystemInvariantSummaryV0 {
685    pub schema_version: &'static str,
686    pub product: &'static str,
687    pub layer_marker: &'static str,
688    pub feature_gate: &'static str,
689    pub invariant_id: String,
690    pub invariant_kind: &'static str,
691    pub model_count: usize,
692    pub source_products: Vec<&'static str>,
693    pub model_hashes: Vec<String>,
694    pub differing_sort_names: Vec<String>,
695    pub accepted: bool,
696}
697
698pub fn design_system_model_from_project_summary_v0(
699    theory_id: impl Into<String>,
700    input: DesignSystemProjectSummaryInputV0,
701) -> DesignSystemModelV0 {
702    let theory_id = theory_id.into();
703    let mut edge_kind_counts = input.edge_kind_counts;
704    edge_kind_counts.sort();
705    let mut sort_interpretations = edge_kind_counts
706        .iter()
707        .map(|entry| SortInterpretationV0 {
708            schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
709            product: "omena-categorical.sort-interpretation",
710            layer_marker: CATEGORICAL_LAYER_MARKER_V0,
711            feature_gate: CATEGORICAL_FEATURE_GATE_V0,
712            sort_name: format!("edgeKind:{}", entry.edge_kind),
713            element_count: entry.count,
714        })
715        .collect::<Vec<_>>();
716    sort_interpretations.push(SortInterpretationV0 {
717        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
718        product: "omena-categorical.sort-interpretation",
719        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
720        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
721        sort_name: "summaryEdge".to_string(),
722        element_count: input.summary_edge_count,
723    });
724    sort_interpretations.sort_by(|left, right| left.sort_name.cmp(&right.sort_name));
725
726    DesignSystemModelV0 {
727        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
728        product: "omena-categorical.design-system-model",
729        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
730        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
731        model_id: format!(
732            "design-system-model:{}:{}",
733            input.project_id, input.summary_hash
734        ),
735        theory_id,
736        source_product: input.source_product,
737        project_id: input.project_id,
738        summary_hash: input.summary_hash,
739        summary_edge_count: input.summary_edge_count,
740        edge_kind_counts,
741        sort_interpretations,
742    }
743}
744
745pub fn compare_design_system_models_for_invariant_v0(
746    invariant_id: impl Into<String>,
747    models: &[DesignSystemModelV0],
748) -> DesignSystemInvariantSummaryV0 {
749    let differing_sort_names = differing_design_system_model_sort_names_v0(models);
750    DesignSystemInvariantSummaryV0 {
751        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
752        product: "omena-categorical.design-system-invariant-summary",
753        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
754        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
755        invariant_id: invariant_id.into(),
756        invariant_kind: "crossProjectEdgeKindSymmetry",
757        model_count: models.len(),
758        source_products: models.iter().map(|model| model.source_product).collect(),
759        model_hashes: models
760            .iter()
761            .map(|model| model.summary_hash.clone())
762            .collect(),
763        accepted: models.len() >= 2 && differing_sort_names.is_empty(),
764        differing_sort_names,
765    }
766}
767
768fn differing_design_system_model_sort_names_v0(models: &[DesignSystemModelV0]) -> Vec<String> {
769    let Some(first) = models.first() else {
770        return Vec::new();
771    };
772    let baseline = first
773        .sort_interpretations
774        .iter()
775        .map(|sort| (sort.sort_name.as_str(), sort.element_count))
776        .collect::<Vec<_>>();
777    let mut differing_sort_names = BTreeSet::new();
778    for model in models.iter().skip(1) {
779        for (sort_name, baseline_count) in &baseline {
780            let current_count = model
781                .sort_interpretations
782                .iter()
783                .find(|sort| sort.sort_name == *sort_name)
784                .map(|sort| sort.element_count);
785            if current_count != Some(*baseline_count) {
786                differing_sort_names.insert((*sort_name).to_string());
787            }
788        }
789        for sort in &model.sort_interpretations {
790            if !baseline
791                .iter()
792                .any(|(sort_name, _)| *sort_name == sort.sort_name)
793            {
794                differing_sort_names.insert(sort.sort_name.clone());
795            }
796        }
797    }
798    differing_sort_names.into_iter().collect()
799}
800
801pub const RG_FLOW_SCHEMA_VERSION_V0: &str = "0";
802pub const RG_FLOW_LAYER_MARKER_V0: &str = "rg-flow-statistical";
803pub const RG_FLOW_FEATURE_GATE_V0: &str = "rg-flow";
804pub const RG_FLOW_MECHANISM_SCOPE_V0: &str = "optInDeepAnalysisJacobianSpectrumHintSubstrate";
805pub const RG_FLOW_PRODUCT_SURFACE_V0: &str = "deepAnalysisCascadeSensitivityHint";
806pub const RG_FLOW_DEFAULT_PRODUCT_DECISION_MECHANISM_V0: bool = false;
807const RG_FLOW_EIGEN_EPSILON: f64 = 1e-9;
808
809#[derive(Debug, Clone, PartialEq, Serialize)]
810#[serde(rename_all = "camelCase")]
811pub struct CouplingSpaceV0 {
812    pub schema_version: &'static str,
813    pub product: &'static str,
814    pub layer_marker: &'static str,
815    pub feature_gate: &'static str,
816    pub k_env: usize,
817    pub k_decl: usize,
818    pub k_cycle: usize,
819    pub k_dirty: usize,
820}
821
822#[derive(Debug, Clone, PartialEq, Serialize)]
823#[serde(rename_all = "camelCase")]
824pub struct CouplingJacobianSpectrumV0 {
825    pub schema_version: &'static str,
826    pub product: &'static str,
827    pub layer_marker: &'static str,
828    pub feature_gate: &'static str,
829    pub mechanism_scope: &'static str,
830    pub product_surface: &'static str,
831    pub default_product_decision_mechanism: bool,
832    pub matrix: Vec<Vec<f64>>,
833    pub eigenvalues: Vec<f64>,
834    pub spectral_radius: f64,
835    pub computed_from: &'static str,
836}
837
838pub fn coupling_space(
839    k_env: usize,
840    k_decl: usize,
841    k_cycle: usize,
842    k_dirty: usize,
843) -> CouplingSpaceV0 {
844    CouplingSpaceV0 {
845        schema_version: RG_FLOW_SCHEMA_VERSION_V0,
846        product: "omena-rg-flow.coupling-space",
847        layer_marker: RG_FLOW_LAYER_MARKER_V0,
848        feature_gate: RG_FLOW_FEATURE_GATE_V0,
849        k_env,
850        k_decl,
851        k_cycle,
852        k_dirty,
853    }
854}
855
856pub fn estimate_coupling_jacobian_spectrum_v0(
857    before: &CouplingSpaceV0,
858    after: &CouplingSpaceV0,
859) -> CouplingJacobianSpectrumV0 {
860    let beta_env = signed_delta(after.k_env, before.k_env);
861    let beta_decl = signed_delta(after.k_decl, before.k_decl);
862    let beta_cycle = signed_delta(after.k_cycle, before.k_cycle);
863    let beta_dirty = signed_delta(after.k_dirty, before.k_dirty);
864    let env_decl_cross = coupling_cross_sensitivity(before.k_decl, after.k_decl, before.k_env);
865    let decl_env_cross = coupling_cross_sensitivity(before.k_env, after.k_env, before.k_decl);
866    let cycle_dirty_cross =
867        coupling_cross_sensitivity(before.k_dirty, after.k_dirty, before.k_cycle);
868    let dirty_cycle_cross =
869        coupling_cross_sensitivity(before.k_cycle, after.k_cycle, before.k_dirty);
870    let matrix = vec![
871        vec![
872            diagonal_coupling_sensitivity(beta_env, before.k_env),
873            env_decl_cross,
874            0.0,
875            0.0,
876        ],
877        vec![
878            decl_env_cross,
879            diagonal_coupling_sensitivity(beta_decl, before.k_decl),
880            0.0,
881            0.0,
882        ],
883        vec![
884            0.0,
885            0.0,
886            diagonal_coupling_sensitivity(beta_cycle, before.k_cycle),
887            cycle_dirty_cross,
888        ],
889        vec![
890            0.0,
891            0.0,
892            dirty_cycle_cross,
893            diagonal_coupling_sensitivity(beta_dirty, before.k_dirty),
894        ],
895    ];
896    let mut eigenvalues =
897        eigenvalues_for_2x2_block(matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1]);
898    eigenvalues.extend(eigenvalues_for_2x2_block(
899        matrix[2][2],
900        matrix[2][3],
901        matrix[3][2],
902        matrix[3][3],
903    ));
904    let spectral_radius = eigenvalues
905        .iter()
906        .map(|value| value.abs())
907        .fold(0.0, f64::max);
908
909    CouplingJacobianSpectrumV0 {
910        schema_version: RG_FLOW_SCHEMA_VERSION_V0,
911        product: "omena-rg-flow.coupling-jacobian-spectrum",
912        layer_marker: RG_FLOW_LAYER_MARKER_V0,
913        feature_gate: RG_FLOW_FEATURE_GATE_V0,
914        mechanism_scope: RG_FLOW_MECHANISM_SCOPE_V0,
915        product_surface: RG_FLOW_PRODUCT_SURFACE_V0,
916        default_product_decision_mechanism: RG_FLOW_DEFAULT_PRODUCT_DECISION_MECHANISM_V0,
917        matrix,
918        eigenvalues,
919        spectral_radius,
920        computed_from: "finite-difference-linearization-v0",
921    }
922}
923
924fn signed_delta(after: usize, before: usize) -> f64 {
925    after as f64 - before as f64
926}
927
928fn diagonal_coupling_sensitivity(beta: f64, before: usize) -> f64 {
929    beta / before.max(1) as f64
930}
931
932fn coupling_cross_sensitivity(
933    source_before: usize,
934    source_after: usize,
935    target_before: usize,
936) -> f64 {
937    let source_delta = signed_delta(source_after, source_before).abs();
938    if source_delta <= RG_FLOW_EIGEN_EPSILON {
939        0.0
940    } else {
941        source_delta / source_before.saturating_add(target_before).max(1) as f64
942    }
943}
944
945fn eigenvalues_for_2x2_block(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
946    let trace = a + d;
947    let discriminant = ((a - d) * (a - d) + 4.0 * b * c).max(0.0).sqrt();
948    vec![(trace + discriminant) / 2.0, (trace - discriminant) / 2.0]
949}
950
951pub const VARIATIONAL_SCHEMA_VERSION_V0: &str = "0";
952pub const VARIATIONAL_LAYER_MARKER_V0: &str = "variational-cascade";
953pub const VARIATIONAL_FEATURE_GATE_V0: &str = "variational";
954
955#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
956#[serde(rename_all = "camelCase")]
957pub enum DesignerIntentPosteriorModeV0 {
958    VciFormal,
959    PcnHierarchical,
960    Fallback,
961}
962
963#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
964#[serde(rename_all = "camelCase")]
965pub enum PatternIntentV0 {
966    Bem,
967    Utility,
968    Atomic,
969    Hybrid,
970    AdHoc,
971}
972
973impl PatternIntentV0 {
974    pub const fn as_str(self) -> &'static str {
975        match self {
976            Self::Bem => "bem",
977            Self::Utility => "utility",
978            Self::Atomic => "atomic",
979            Self::Hybrid => "hybrid",
980            Self::AdHoc => "adHoc",
981        }
982    }
983}
984
985#[derive(Debug, Clone, PartialEq, Serialize)]
986#[serde(rename_all = "camelCase")]
987pub struct DesignerIntentScoreV0 {
988    pub schema_version: &'static str,
989    pub product: &'static str,
990    pub layer_marker: &'static str,
991    pub feature_gate: &'static str,
992    pub intent: PatternIntentV0,
993    pub log_probability_bits: f64,
994}
995
996#[derive(Debug, Clone, PartialEq, Serialize)]
997#[serde(rename_all = "camelCase")]
998pub struct DesignerIntentPosteriorV0 {
999    pub schema_version: &'static str,
1000    pub product: &'static str,
1001    pub layer_marker: &'static str,
1002    pub feature_gate: &'static str,
1003    pub mode: DesignerIntentPosteriorModeV0,
1004    pub selector_name: String,
1005    pub scores: Vec<DesignerIntentScoreV0>,
1006    pub enabled_by_default: bool,
1007}
1008
1009#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1010#[serde(rename_all = "camelCase")]
1011pub struct DesignerIntentPosteriorInputV0 {
1012    pub schema_version: &'static str,
1013    pub product: &'static str,
1014    pub layer_marker: &'static str,
1015    pub feature_gate: &'static str,
1016    pub selector_name: String,
1017    pub declaration_count: usize,
1018    pub duplicate_property_tie_count: usize,
1019    pub custom_property_reference_count: usize,
1020}
1021
1022pub fn designer_intent_posterior_input_v0(
1023    selector_name: impl Into<String>,
1024    declaration_count: usize,
1025    duplicate_property_tie_count: usize,
1026    custom_property_reference_count: usize,
1027) -> DesignerIntentPosteriorInputV0 {
1028    DesignerIntentPosteriorInputV0 {
1029        schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
1030        product: "omena-variational.designer-intent-posterior-input",
1031        layer_marker: VARIATIONAL_LAYER_MARKER_V0,
1032        feature_gate: VARIATIONAL_FEATURE_GATE_V0,
1033        selector_name: selector_name.into(),
1034        declaration_count,
1035        duplicate_property_tie_count,
1036        custom_property_reference_count,
1037    }
1038}
1039
1040pub fn infer_designer_intent_posterior_v0(
1041    input: DesignerIntentPosteriorInputV0,
1042) -> DesignerIntentPosteriorV0 {
1043    let selector = normalize_selector_name_for_intent_v0(&input.selector_name);
1044    let has_bem_marker = selector.contains("__") || selector.contains("--");
1045    let looks_utility = selector.starts_with("u-")
1046        || selector.starts_with("is-")
1047        || selector.starts_with("has-")
1048        || selector
1049            .split('-')
1050            .any(|part| matches!(part, "m" | "p" | "mt" | "mb" | "ml" | "mr" | "bg" | "text"));
1051    let looks_atomic = input.declaration_count <= 1 && selector.len() <= 8;
1052    let looks_hybrid = selector.matches('-').count() >= 3
1053        || (has_bem_marker && input.custom_property_reference_count > 0);
1054    let mut scores = vec![
1055        DesignerIntentScoreV0 {
1056            schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
1057            product: "omena-variational.designer-intent-score",
1058            layer_marker: VARIATIONAL_LAYER_MARKER_V0,
1059            feature_gate: VARIATIONAL_FEATURE_GATE_V0,
1060            intent: PatternIntentV0::Bem,
1061            log_probability_bits: bool_bits_v0(has_bem_marker) * 7.0
1062                + bool_bits_v0(input.declaration_count > 1)
1063                - bool_bits_v0(input.duplicate_property_tie_count > 0),
1064        },
1065        DesignerIntentScoreV0 {
1066            schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
1067            product: "omena-variational.designer-intent-score",
1068            layer_marker: VARIATIONAL_LAYER_MARKER_V0,
1069            feature_gate: VARIATIONAL_FEATURE_GATE_V0,
1070            intent: PatternIntentV0::Utility,
1071            log_probability_bits: bool_bits_v0(looks_utility) * 6.5
1072                - bool_bits_v0(has_bem_marker) * 2.0
1073                + bool_bits_v0(input.declaration_count <= 2),
1074        },
1075        DesignerIntentScoreV0 {
1076            schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
1077            product: "omena-variational.designer-intent-score",
1078            layer_marker: VARIATIONAL_LAYER_MARKER_V0,
1079            feature_gate: VARIATIONAL_FEATURE_GATE_V0,
1080            intent: PatternIntentV0::Atomic,
1081            log_probability_bits: bool_bits_v0(looks_atomic) * 5.0
1082                - bool_bits_v0(input.declaration_count > 1) * 2.0,
1083        },
1084        DesignerIntentScoreV0 {
1085            schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
1086            product: "omena-variational.designer-intent-score",
1087            layer_marker: VARIATIONAL_LAYER_MARKER_V0,
1088            feature_gate: VARIATIONAL_FEATURE_GATE_V0,
1089            intent: PatternIntentV0::Hybrid,
1090            log_probability_bits: bool_bits_v0(has_bem_marker)
1091                + bool_bits_v0(looks_hybrid) * 4.0
1092                + bool_bits_v0(input.custom_property_reference_count > 0) * 1.5,
1093        },
1094        DesignerIntentScoreV0 {
1095            schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
1096            product: "omena-variational.designer-intent-score",
1097            layer_marker: VARIATIONAL_LAYER_MARKER_V0,
1098            feature_gate: VARIATIONAL_FEATURE_GATE_V0,
1099            intent: PatternIntentV0::AdHoc,
1100            log_probability_bits: bool_bits_v0(!has_bem_marker && !looks_utility)
1101                + bool_bits_v0(input.duplicate_property_tie_count > 0),
1102        },
1103    ];
1104    scores.sort_by(|left, right| {
1105        right
1106            .log_probability_bits
1107            .partial_cmp(&left.log_probability_bits)
1108            .unwrap_or(std::cmp::Ordering::Equal)
1109            .then_with(|| left.intent.as_str().cmp(right.intent.as_str()))
1110    });
1111    DesignerIntentPosteriorV0 {
1112        schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
1113        product: "omena-variational.designer-intent-posterior",
1114        layer_marker: VARIATIONAL_LAYER_MARKER_V0,
1115        feature_gate: VARIATIONAL_FEATURE_GATE_V0,
1116        mode: DesignerIntentPosteriorModeV0::VciFormal,
1117        selector_name: input.selector_name,
1118        scores,
1119        enabled_by_default: true,
1120    }
1121}
1122
1123pub fn dominant_designer_intent_v0(
1124    posterior: &DesignerIntentPosteriorV0,
1125) -> Option<PatternIntentV0> {
1126    posterior.scores.first().map(|score| score.intent)
1127}
1128
1129fn normalize_selector_name_for_intent_v0(selector_name: &str) -> String {
1130    selector_name
1131        .trim()
1132        .trim_start_matches('.')
1133        .split([':', '[', ' ', '>', '+', '~', ','])
1134        .next()
1135        .unwrap_or(selector_name)
1136        .trim()
1137        .to_string()
1138}
1139
1140fn bool_bits_v0(value: bool) -> f64 {
1141    if value { 1.0 } else { 0.0 }
1142}
1143
1144pub const REPLICA_ENSEMBLE_SCHEMA_VERSION_V0: &str = "0";
1145pub const REPLICA_ENSEMBLE_LAYER_MARKER_V0: &str = "replica-ensemble";
1146pub const REPLICA_ENSEMBLE_FEATURE_GATE_V0: &str = "replica-ensemble";
1147pub const REPLICA_ENSEMBLE_MECHANISM_SCOPE_V0: &str =
1148    "productWiredCrossFileConsistencyHintSubstrate";
1149pub const REPLICA_ENSEMBLE_PRODUCT_SURFACE_V0: &str = "defaultCrossFileConsistencyHint";
1150pub const REPLICA_ENSEMBLE_DEFAULT_PRODUCT_DECISION_MECHANISM_V0: bool = false;
1151
1152#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1153#[serde(rename_all = "camelCase")]
1154pub struct CascadeSiteKeyV0 {
1155    pub schema_version: &'static str,
1156    pub product: &'static str,
1157    pub layer_marker: &'static str,
1158    pub feature_gate: &'static str,
1159    pub element_selector: String,
1160    pub property: String,
1161}
1162
1163#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1164#[serde(rename_all = "camelCase")]
1165pub struct LinearProvenanceTagV0 {
1166    pub schema_version: &'static str,
1167    pub product: &'static str,
1168    pub layer_marker: &'static str,
1169    pub feature_gate: &'static str,
1170    pub semiring_identifier: &'static str,
1171    pub label: String,
1172}
1173
1174#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1175#[serde(rename_all = "camelCase")]
1176pub struct ReplicaSiteOutcomeV0 {
1177    pub schema_version: &'static str,
1178    pub product: &'static str,
1179    pub layer_marker: &'static str,
1180    pub feature_gate: &'static str,
1181    pub site: CascadeSiteKeyV0,
1182    pub outcome: CascadeOutcome,
1183    pub provenance: Option<LinearProvenanceTagV0>,
1184}
1185
1186#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1187#[serde(rename_all = "camelCase")]
1188pub struct ReplicaSnapshotV0 {
1189    pub schema_version: &'static str,
1190    pub product: &'static str,
1191    pub layer_marker: &'static str,
1192    pub feature_gate: &'static str,
1193    pub path: String,
1194    pub sites: Vec<ReplicaSiteOutcomeV0>,
1195}
1196
1197#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
1198#[serde(rename_all = "camelCase")]
1199pub enum OutcomeMode {
1200    #[default]
1201    DefiniteOnly,
1202    WidenedRankedSet,
1203    FullStrict,
1204}
1205
1206#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1207#[serde(rename_all = "camelCase")]
1208pub enum SamplingPolicy {
1209    AllPairs,
1210    PageRankWeighted { max_pair_count: usize },
1211    RandomSubset { max_pair_count: usize },
1212}
1213
1214#[derive(Debug, Clone, PartialEq, Serialize)]
1215#[serde(rename_all = "camelCase")]
1216pub struct ReplicaOverlapV0 {
1217    pub schema_version: &'static str,
1218    pub product: &'static str,
1219    pub layer_marker: &'static str,
1220    pub feature_gate: &'static str,
1221    pub replica_alpha_path: String,
1222    pub replica_beta_path: String,
1223    pub outcome_mode: OutcomeMode,
1224    pub shared_site_count: usize,
1225    pub agreeing_site_count: usize,
1226    pub overlap_q: f64,
1227    pub overlap_q_unit: &'static str,
1228    pub provenance_attributions: Vec<OverlapAttributionV0>,
1229}
1230
1231#[derive(Debug, Clone, PartialEq, Serialize)]
1232#[serde(rename_all = "camelCase")]
1233pub struct OverlapAttributionV0 {
1234    pub schema_version: &'static str,
1235    pub product: &'static str,
1236    pub layer_marker: &'static str,
1237    pub feature_gate: &'static str,
1238    pub site_element_selector: String,
1239    pub site_property: String,
1240    pub winner_alpha: String,
1241    pub winner_beta: String,
1242    pub provenance_alpha: Option<LinearProvenanceTagV0>,
1243    pub provenance_beta: Option<LinearProvenanceTagV0>,
1244}
1245
1246#[derive(Debug, Clone, PartialEq, Serialize)]
1247#[serde(rename_all = "camelCase")]
1248pub struct HistogramBinV0 {
1249    pub schema_version: &'static str,
1250    pub product: &'static str,
1251    pub layer_marker: &'static str,
1252    pub feature_gate: &'static str,
1253    pub q_low: f64,
1254    pub q_high: f64,
1255    pub count: usize,
1256    pub normalized_density: f64,
1257}
1258
1259#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1260#[serde(rename_all = "camelCase")]
1261pub enum DistributionModality {
1262    Trivial,
1263    Unimodal,
1264    BimodalRSB,
1265    Continuous,
1266}
1267
1268#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1269#[serde(rename_all = "camelCase")]
1270pub enum ParisiSource {
1271    M4AlphaCascadeReplicaOverlap,
1272    LocalTwoComponentEm,
1273    Unavailable,
1274}
1275
1276#[derive(Debug, Clone, PartialEq, Serialize)]
1277#[serde(rename_all = "camelCase")]
1278pub struct ReplicaOverlapDistributionV0 {
1279    pub schema_version: &'static str,
1280    pub product: &'static str,
1281    pub layer_marker: &'static str,
1282    pub feature_gate: &'static str,
1283    pub workspace_root: String,
1284    pub outcome_mode: OutcomeMode,
1285    pub replica_count: usize,
1286    pub pair_count: usize,
1287    pub histogram_bin_count: usize,
1288    pub histogram_bins: Vec<HistogramBinV0>,
1289    pub modality: DistributionModality,
1290    pub modality_definition: &'static str,
1291    pub peak_q_values: Vec<f64>,
1292    pub parisi_m_estimate: Option<f64>,
1293    pub parisi_m_source: ParisiSource,
1294    pub mean_q: f64,
1295    pub variance_q: f64,
1296}
1297
1298#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1299#[serde(rename_all = "camelCase")]
1300pub struct ModuleGraphV0 {
1301    pub schema_version: &'static str,
1302    pub product: &'static str,
1303    pub layer_marker: &'static str,
1304    pub feature_gate: &'static str,
1305    pub workspace_root: String,
1306    pub nodes: Vec<String>,
1307    pub edges: Vec<ModuleGraphEdgeV0>,
1308}
1309
1310#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1311#[serde(rename_all = "camelCase")]
1312pub struct ModuleGraphEdgeV0 {
1313    pub schema_version: &'static str,
1314    pub product: &'static str,
1315    pub layer_marker: &'static str,
1316    pub feature_gate: &'static str,
1317    pub from_module: String,
1318    pub to_module: String,
1319    pub edge_kind: &'static str,
1320}
1321
1322#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1323#[serde(rename_all = "camelCase")]
1324pub enum PartitionHypothesisLabel {
1325    DirectoryTree,
1326    ComposesCluster,
1327    BrandTheme,
1328    AutoSpectral,
1329}
1330
1331#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1332#[serde(rename_all = "camelCase")]
1333pub enum SpectralMethod {
1334    Auto,
1335    DegreeCorrected,
1336    Spectral,
1337    NonBacktracking,
1338}
1339
1340#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1341#[serde(rename_all = "camelCase")]
1342pub struct ReportOptionsV0 {
1343    pub schema_version: &'static str,
1344    pub product: &'static str,
1345    pub layer_marker: &'static str,
1346    pub feature_gate: &'static str,
1347    pub partition_hypotheses: Vec<PartitionHypothesisLabel>,
1348    pub spectral_method: SpectralMethod,
1349    pub sampling_policy: Option<SamplingPolicy>,
1350    pub rg_exponent_handle: Option<RgExponentHandleV0>,
1351}
1352
1353impl Default for ReportOptionsV0 {
1354    fn default() -> Self {
1355        Self {
1356            schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
1357            product: "omena-ensemble.report-options",
1358            layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
1359            feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
1360            partition_hypotheses: vec![
1361                PartitionHypothesisLabel::AutoSpectral,
1362                PartitionHypothesisLabel::ComposesCluster,
1363                PartitionHypothesisLabel::DirectoryTree,
1364            ],
1365            spectral_method: SpectralMethod::Auto,
1366            sampling_policy: None,
1367            rg_exponent_handle: None,
1368        }
1369    }
1370}
1371
1372#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1373#[serde(rename_all = "camelCase")]
1374pub struct RgExponentHandleV0 {
1375    pub schema_version: &'static str,
1376    pub product: &'static str,
1377    pub layer_marker: &'static str,
1378    pub feature_gate: &'static str,
1379    pub workspace_root: String,
1380    pub timestamp: String,
1381    pub digest: String,
1382}
1383
1384#[derive(Debug, Clone, Copy)]
1385pub struct ParisiM4AlphaSource<'a> {
1386    pub replica_overlap: &'a CascadeReplicaOverlapV0,
1387}
1388
1389#[derive(Debug, Clone, PartialEq, Serialize)]
1390#[serde(rename_all = "camelCase")]
1391pub struct CrossFileInconsistencyReportV0 {
1392    pub schema_version: &'static str,
1393    pub product: &'static str,
1394    pub layer_marker: &'static str,
1395    pub feature_gate: &'static str,
1396    pub mechanism_scope: &'static str,
1397    pub product_surface: &'static str,
1398    pub default_product_decision_mechanism: bool,
1399    pub workspace_root: String,
1400    pub distribution: ReplicaOverlapDistributionV0,
1401    pub top_disagreement_pairs: Vec<ReplicaOverlapV0>,
1402    pub recommendation: ReportRecommendation,
1403}
1404
1405#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1406#[serde(rename_all = "camelCase")]
1407pub enum ReportRecommendation {
1408    NoActionNeeded,
1409    InvestigateRsbBroken,
1410    UndetectablePhase,
1411}
1412
1413pub fn site(element_selector: impl Into<String>, property: impl Into<String>) -> CascadeSiteKeyV0 {
1414    CascadeSiteKeyV0 {
1415        schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
1416        product: "omena-ensemble.cascade-site-key",
1417        layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
1418        feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
1419        element_selector: element_selector.into(),
1420        property: property.into(),
1421    }
1422}
1423
1424pub fn build_cross_file_inconsistency_report(
1425    workspace_root: &str,
1426    replicas: impl IntoIterator<Item = ReplicaSnapshotV0>,
1427    _module_graph: &ModuleGraphV0,
1428    outcome_mode: OutcomeMode,
1429    options: ReportOptionsV0,
1430    parisi_source: Option<ParisiM4AlphaSource<'_>>,
1431) -> CrossFileInconsistencyReportV0 {
1432    let replicas = replicas.into_iter().collect::<Vec<_>>();
1433    let distribution = compute_overlap_distribution(
1434        workspace_root,
1435        replicas.clone(),
1436        options.sampling_policy,
1437        outcome_mode,
1438        parisi_source,
1439    );
1440    let top_disagreement_pairs =
1441        top_disagreement_pairs(&replicas, outcome_mode, options.sampling_policy);
1442    let recommendation = if top_disagreement_pairs
1443        .iter()
1444        .any(|pair| pair.shared_site_count > 0 && pair.overlap_q < 1.0)
1445    {
1446        ReportRecommendation::InvestigateRsbBroken
1447    } else {
1448        ReportRecommendation::NoActionNeeded
1449    };
1450
1451    CrossFileInconsistencyReportV0 {
1452        schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
1453        product: "omena-ensemble.cross-file-inconsistency-report",
1454        layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
1455        feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
1456        mechanism_scope: REPLICA_ENSEMBLE_MECHANISM_SCOPE_V0,
1457        product_surface: REPLICA_ENSEMBLE_PRODUCT_SURFACE_V0,
1458        default_product_decision_mechanism: REPLICA_ENSEMBLE_DEFAULT_PRODUCT_DECISION_MECHANISM_V0,
1459        workspace_root: workspace_root.to_string(),
1460        distribution,
1461        top_disagreement_pairs,
1462        recommendation,
1463    }
1464}
1465
1466fn compute_overlap_distribution(
1467    workspace_root: &str,
1468    replicas: impl IntoIterator<Item = ReplicaSnapshotV0>,
1469    sampling_policy: Option<SamplingPolicy>,
1470    outcome_mode: OutcomeMode,
1471    parisi_source: Option<ParisiM4AlphaSource<'_>>,
1472) -> ReplicaOverlapDistributionV0 {
1473    let replicas = replicas.into_iter().collect::<Vec<_>>();
1474    let pairs = selected_pair_indices(&replicas, sampling_policy);
1475    let overlaps = pairs
1476        .iter()
1477        .map(|(alpha_index, beta_index)| {
1478            let alpha = &replicas[*alpha_index];
1479            let beta = &replicas[*beta_index];
1480            compute_replica_overlap(
1481                &alpha.path,
1482                &beta.path,
1483                alpha.sites.clone(),
1484                beta.sites.clone(),
1485                outcome_mode,
1486            )
1487        })
1488        .collect::<Vec<_>>();
1489    let q_values = overlaps
1490        .iter()
1491        .map(|overlap| overlap.overlap_q)
1492        .collect::<Vec<_>>();
1493    let mean_q = mean(&q_values);
1494    let variance_q = variance(&q_values, mean_q);
1495    let histogram_bins = histogram(&q_values, 10);
1496    let modality = classify_modality(overlaps.len(), variance_q, &histogram_bins);
1497    let (parisi_m_estimate, parisi_m_source) = parisi_estimate(modality, parisi_source, &q_values);
1498
1499    ReplicaOverlapDistributionV0 {
1500        schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
1501        product: "omena-ensemble.replica-overlap-distribution",
1502        layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
1503        feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
1504        workspace_root: workspace_root.to_string(),
1505        outcome_mode,
1506        replica_count: replicas.len(),
1507        pair_count: overlaps.len(),
1508        histogram_bin_count: histogram_bins.len(),
1509        histogram_bins,
1510        modality,
1511        modality_definition: modality_definition(modality, parisi_m_source),
1512        peak_q_values: peak_q_values(&q_values),
1513        parisi_m_estimate,
1514        parisi_m_source,
1515        mean_q,
1516        variance_q,
1517    }
1518}
1519
1520fn compute_replica_overlap<I, J>(
1521    alpha: &str,
1522    beta: &str,
1523    cascade_alpha: I,
1524    cascade_beta: J,
1525    mode: OutcomeMode,
1526) -> ReplicaOverlapV0
1527where
1528    I: IntoIterator<Item = ReplicaSiteOutcomeV0>,
1529    J: IntoIterator<Item = ReplicaSiteOutcomeV0>,
1530{
1531    let alpha_by_site = cascade_alpha
1532        .into_iter()
1533        .map(|entry| (entry.site.clone(), entry))
1534        .collect::<BTreeMap<_, _>>();
1535    let beta_by_site = cascade_beta
1536        .into_iter()
1537        .map(|entry| (entry.site.clone(), entry))
1538        .collect::<BTreeMap<_, _>>();
1539
1540    let mut shared_site_count = 0usize;
1541    let mut agreeing_site_count = 0usize;
1542    let mut provenance_attributions = Vec::new();
1543    for (site, alpha_entry) in &alpha_by_site {
1544        let Some(beta_entry) = beta_by_site.get(site) else {
1545            continue;
1546        };
1547        let Some(alpha_projection) = project_outcome(&alpha_entry.outcome, mode) else {
1548            continue;
1549        };
1550        let Some(beta_projection) = project_outcome(&beta_entry.outcome, mode) else {
1551            continue;
1552        };
1553        shared_site_count += 1;
1554        if alpha_projection == beta_projection {
1555            agreeing_site_count += 1;
1556        } else {
1557            provenance_attributions.push(OverlapAttributionV0 {
1558                schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
1559                product: "omena-ensemble.overlap-attribution",
1560                layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
1561                feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
1562                site_element_selector: site.element_selector.clone(),
1563                site_property: site.property.clone(),
1564                winner_alpha: alpha_projection,
1565                winner_beta: beta_projection,
1566                provenance_alpha: alpha_entry.provenance.clone(),
1567                provenance_beta: beta_entry.provenance.clone(),
1568            });
1569        }
1570    }
1571    let overlap_q = if shared_site_count == 0 {
1572        0.0
1573    } else {
1574        agreeing_site_count as f64 / shared_site_count as f64
1575    };
1576
1577    ReplicaOverlapV0 {
1578        schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
1579        product: "omena-ensemble.replica-overlap",
1580        layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
1581        feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
1582        replica_alpha_path: alpha.to_string(),
1583        replica_beta_path: beta.to_string(),
1584        outcome_mode: mode,
1585        shared_site_count,
1586        agreeing_site_count,
1587        overlap_q,
1588        overlap_q_unit: "unitless",
1589        provenance_attributions,
1590    }
1591}
1592
1593fn selected_pair_indices(
1594    replicas: &[ReplicaSnapshotV0],
1595    sampling_policy: Option<SamplingPolicy>,
1596) -> Vec<(usize, usize)> {
1597    let mut pairs = Vec::new();
1598    for alpha_index in 0..replicas.len() {
1599        for beta_index in alpha_index + 1..replicas.len() {
1600            pairs.push((alpha_index, beta_index));
1601        }
1602    }
1603    match sampling_policy {
1604        Some(SamplingPolicy::PageRankWeighted { max_pair_count })
1605        | Some(SamplingPolicy::RandomSubset { max_pair_count }) => pairs.truncate(max_pair_count),
1606        Some(SamplingPolicy::AllPairs) | None => {}
1607    }
1608    pairs
1609}
1610
1611fn top_disagreement_pairs(
1612    replicas: &[ReplicaSnapshotV0],
1613    outcome_mode: OutcomeMode,
1614    sampling_policy: Option<SamplingPolicy>,
1615) -> Vec<ReplicaOverlapV0> {
1616    let mut overlaps = Vec::new();
1617    let mut remaining_budget = match sampling_policy {
1618        Some(SamplingPolicy::PageRankWeighted { max_pair_count })
1619        | Some(SamplingPolicy::RandomSubset { max_pair_count }) => max_pair_count,
1620        Some(SamplingPolicy::AllPairs) | None => usize::MAX,
1621    };
1622    for alpha_index in 0..replicas.len() {
1623        for beta_index in alpha_index + 1..replicas.len() {
1624            if remaining_budget == 0 {
1625                break;
1626            }
1627            remaining_budget = remaining_budget.saturating_sub(1);
1628            let alpha = &replicas[alpha_index];
1629            let beta = &replicas[beta_index];
1630            overlaps.push(compute_replica_overlap(
1631                &alpha.path,
1632                &beta.path,
1633                alpha.sites.clone(),
1634                beta.sites.clone(),
1635                outcome_mode,
1636            ));
1637        }
1638    }
1639    overlaps.sort_by(|left, right| {
1640        left.overlap_q
1641            .total_cmp(&right.overlap_q)
1642            .then_with(|| left.replica_alpha_path.cmp(&right.replica_alpha_path))
1643            .then_with(|| left.replica_beta_path.cmp(&right.replica_beta_path))
1644    });
1645    overlaps.truncate(5);
1646    overlaps
1647}
1648
1649fn project_outcome(outcome: &CascadeOutcome, mode: OutcomeMode) -> Option<String> {
1650    match (outcome, mode) {
1651        (CascadeOutcome::Definite { winner, .. }, _) => Some(format!("definite:{}", winner.id)),
1652        (CascadeOutcome::RankedSet(declarations), OutcomeMode::WidenedRankedSet)
1653        | (CascadeOutcome::RankedSet(declarations), OutcomeMode::FullStrict) => {
1654            let mut ids = declarations
1655                .iter()
1656                .map(|declaration| declaration.id.as_str())
1657                .collect::<Vec<_>>();
1658            ids.sort_unstable();
1659            Some(format!("ranked:{}", ids.join("|")))
1660        }
1661        (CascadeOutcome::Inherit, OutcomeMode::FullStrict) => Some("inherit".to_string()),
1662        (CascadeOutcome::Top, OutcomeMode::FullStrict) => Some("top".to_string()),
1663        (CascadeOutcome::RankedSet(_), OutcomeMode::DefiniteOnly)
1664        | (CascadeOutcome::Inherit, OutcomeMode::DefiniteOnly | OutcomeMode::WidenedRankedSet)
1665        | (CascadeOutcome::Top, OutcomeMode::DefiniteOnly | OutcomeMode::WidenedRankedSet) => None,
1666    }
1667}
1668
1669fn mean(values: &[f64]) -> f64 {
1670    if values.is_empty() {
1671        return 0.0;
1672    }
1673    values.iter().sum::<f64>() / values.len() as f64
1674}
1675
1676fn variance(values: &[f64], mean: f64) -> f64 {
1677    if values.is_empty() {
1678        return 0.0;
1679    }
1680    values
1681        .iter()
1682        .map(|value| {
1683            let delta = value - mean;
1684            delta * delta
1685        })
1686        .sum::<f64>()
1687        / values.len() as f64
1688}
1689
1690fn histogram(values: &[f64], bin_count: usize) -> Vec<HistogramBinV0> {
1691    let mut counts = vec![0usize; bin_count];
1692    for value in values {
1693        let clamped = value.clamp(0.0, 1.0);
1694        let mut bin_index = (clamped * bin_count as f64).floor() as usize;
1695        if bin_index == bin_count {
1696            bin_index = bin_count.saturating_sub(1);
1697        }
1698        counts[bin_index] += 1;
1699    }
1700    counts
1701        .into_iter()
1702        .enumerate()
1703        .map(|(index, count)| HistogramBinV0 {
1704            schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
1705            product: "omena-ensemble.histogram-bin",
1706            layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
1707            feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
1708            q_low: index as f64 / bin_count as f64,
1709            q_high: (index + 1) as f64 / bin_count as f64,
1710            count,
1711            normalized_density: if values.is_empty() {
1712                0.0
1713            } else {
1714                count as f64 / values.len() as f64
1715            },
1716        })
1717        .collect()
1718}
1719
1720fn classify_modality(
1721    pair_count: usize,
1722    variance_q: f64,
1723    histogram_bins: &[HistogramBinV0],
1724) -> DistributionModality {
1725    if pair_count < 3 {
1726        return DistributionModality::Trivial;
1727    }
1728    if variance_q < 0.01 {
1729        return DistributionModality::Unimodal;
1730    }
1731    let low_peak = histogram_bins
1732        .iter()
1733        .any(|bin| bin.count > 0 && bin.q_high <= 0.5);
1734    let high_peak = histogram_bins
1735        .iter()
1736        .any(|bin| bin.count > 0 && bin.q_low >= 0.7);
1737    if low_peak && high_peak {
1738        DistributionModality::BimodalRSB
1739    } else {
1740        DistributionModality::Continuous
1741    }
1742}
1743
1744fn parisi_estimate(
1745    modality: DistributionModality,
1746    parisi_source: Option<ParisiM4AlphaSource<'_>>,
1747    q_values: &[f64],
1748) -> (Option<f64>, ParisiSource) {
1749    if let Some(source) = parisi_source
1750        && let Some(m_estimate) = source.replica_overlap.parisi_breakpoint_m
1751    {
1752        return (Some(m_estimate), ParisiSource::M4AlphaCascadeReplicaOverlap);
1753    }
1754    if modality == DistributionModality::BimodalRSB {
1755        return (
1756            two_component_em_low_overlap_weight(q_values),
1757            ParisiSource::LocalTwoComponentEm,
1758        );
1759    }
1760    (None, ParisiSource::Unavailable)
1761}
1762
1763fn two_component_em_low_overlap_weight(q_values: &[f64]) -> Option<f64> {
1764    if q_values.len() < 3 {
1765        return None;
1766    }
1767    let mut sorted = q_values.to_vec();
1768    sorted.sort_by(f64::total_cmp);
1769    let low = sorted[0].clamp(0.0, 1.0);
1770    let high = sorted[sorted.len() - 1].clamp(0.0, 1.0);
1771    (high - low > 0.000_001).then_some(0.5)
1772}
1773
1774fn modality_definition(
1775    modality: DistributionModality,
1776    parisi_source: ParisiSource,
1777) -> &'static str {
1778    match (modality, parisi_source) {
1779        (DistributionModality::Trivial, _) => {
1780            "Fewer than 3 replica pairs available; modality undefined"
1781        }
1782        (DistributionModality::Unimodal, _) => {
1783            "Single peak in P(q) histogram; replica-symmetric descriptive shape"
1784        }
1785        (DistributionModality::BimodalRSB, ParisiSource::M4AlphaCascadeReplicaOverlap) => {
1786            "Two peaks in P(q) with M4-alpha spin-glass Parisi estimate attached"
1787        }
1788        (DistributionModality::BimodalRSB, ParisiSource::LocalTwoComponentEm) => {
1789            "Two peaks in P(q) histogram; local two-component EM estimates the low-overlap mixture weight"
1790        }
1791        (DistributionModality::BimodalRSB, _) => {
1792            "Two peaks in P(q) histogram; spin-glass source unavailable for Parisi estimate"
1793        }
1794        (DistributionModality::Continuous, _) => {
1795            "Smooth P(q) histogram; peak detection fails the bimodal threshold"
1796        }
1797    }
1798}
1799
1800fn peak_q_values(values: &[f64]) -> Vec<f64> {
1801    if values.is_empty() {
1802        return Vec::new();
1803    }
1804    let mut sorted = values.to_vec();
1805    sorted.sort_by(f64::total_cmp);
1806    let low = sorted[0];
1807    let high = sorted[sorted.len() - 1];
1808    if (high - low).abs() < f64::EPSILON {
1809        vec![low]
1810    } else {
1811        vec![low, high]
1812    }
1813}