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::cmp::Ordering;
8use std::collections::{BTreeMap, BTreeSet};
9
10use omena_cascade::{CascadeOutcome, CascadeReplicaOverlapV0};
11use omena_syntax::ident::AuthoredPropertyTextV0;
12use serde::Serialize;
13
14pub const CATEGORICAL_SCHEMA_VERSION_V0: &str = "0";
15pub const CATEGORICAL_LAYER_MARKER_V0: &str = "categorical-semantic";
16pub const CATEGORICAL_FEATURE_GATE_V0: &str = "categorical-evidence";
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct CascadePrimitiveRoleV0 {
21    pub schema_version: &'static str,
22    pub product: &'static str,
23    pub layer_marker: &'static str,
24    pub feature_gate: &'static str,
25    pub primitive_kind: &'static str,
26    pub primitive_name: &'static str,
27    pub categorical_role: &'static str,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
31#[serde(rename_all = "camelCase")]
32pub struct CategoricalEvidenceEndpointV0 {
33    pub schema_version: &'static str,
34    pub product: &'static str,
35    pub layer_marker: &'static str,
36    pub feature_gate: &'static str,
37    pub endpoint_id: &'static str,
38    pub evidence_product: &'static str,
39    pub fixture_focus: &'static str,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
43#[serde(rename_all = "camelCase")]
44pub struct CategoricalFixtureAssertionV0 {
45    pub schema_version: &'static str,
46    pub product: &'static str,
47    pub layer_marker: &'static str,
48    pub feature_gate: &'static str,
49    pub assertion_id: &'static str,
50    pub contract_product: &'static str,
51    pub observed: String,
52    pub expected: String,
53    pub accepted: bool,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
57#[serde(rename_all = "camelCase")]
58pub struct CategoricalEndpointFixtureEvidenceV0 {
59    pub schema_version: &'static str,
60    pub product: &'static str,
61    pub layer_marker: &'static str,
62    pub feature_gate: &'static str,
63    pub claim_scope: &'static str,
64    pub endpoint_id: &'static str,
65    pub fixture_id: &'static str,
66    pub fixture_focus: &'static str,
67    pub evidence_product: &'static str,
68    pub exercised_contract_products: Vec<&'static str>,
69    pub assertion_count: usize,
70    pub assertions: Vec<CategoricalFixtureAssertionV0>,
71    pub accepted: bool,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
75#[serde(rename_all = "camelCase")]
76pub struct CascadeFunctorApplicationV0 {
77    pub schema_version: &'static str,
78    pub product: &'static str,
79    pub layer_marker: &'static str,
80    pub feature_gate: &'static str,
81    pub functor_id: String,
82    pub source_category_id: String,
83    pub target_category_id: String,
84    pub object_mapping_count: usize,
85    pub morphism_mapping_count: usize,
86    pub composed_source_morphism_id: Option<String>,
87    pub composed_target_morphism_id: Option<String>,
88    pub identity_preserved: bool,
89    pub composition_preserved: bool,
90    pub accepted: bool,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
94#[serde(rename_all = "camelCase")]
95pub struct CategoricalCascadeEvidenceV0 {
96    pub schema_version: &'static str,
97    pub product: &'static str,
98    pub layer_marker: &'static str,
99    pub feature_gate: &'static str,
100    pub source_product: &'static str,
101    pub endpoint_count: usize,
102    pub endpoints: Vec<CategoricalEvidenceEndpointV0>,
103    pub fixture_evidence: Vec<CategoricalEndpointFixtureEvidenceV0>,
104    pub functor_applications: Vec<CascadeFunctorApplicationV0>,
105    pub cascade_primitive_roles: Vec<CascadePrimitiveRoleV0>,
106    pub default_feature_enabled: bool,
107}
108
109pub fn cascade_section_evidence_endpoints_v0() -> Vec<CategoricalEvidenceEndpointV0> {
110    [
111        (
112            "rust/omena-categorical/verify-cascade-section-aggregation-plan-stability",
113            "omena-categorical.cascade-section-aggregation-plan",
114            "cascade section aggregation plan stability",
115        ),
116        (
117            "rust/omena-categorical/verify-cascade-section-aggregation-covariance",
118            "omena-categorical.cascade-section-aggregation",
119            "cascade section aggregation covariance",
120        ),
121        (
122            "rust/omena-categorical/verify-beck-chevalley",
123            "omena-categorical.beck-chevalley-check",
124            "Beck-Chevalley witnesses",
125        ),
126        (
127            "rust/omena-categorical/classify-omega-truth",
128            "omena-categorical.omega-truth-mapping",
129            "Omega truth values",
130        ),
131        (
132            "rust/omena-categorical/verify-s4-axioms",
133            "omena-categorical.modal-evaluation-witness",
134            "S4 modal axioms",
135        ),
136        (
137            "rust/omena-categorical/verify-modal-imperative-equivalence",
138            "omena-categorical.modal-diagnostic-schema",
139            "modal-imperative equivalence",
140        ),
141        (
142            "rust/omena-categorical/verify-invariant-functoriality",
143            "omena-categorical.design-system-theory",
144            "invariant functoriality",
145        ),
146        (
147            "rust/omena-categorical/compare-design-system-theory",
148            "omena-categorical.design-system-theory",
149            "cross-project design-system theory",
150        ),
151        (
152            "rust/omena-categorical/summarize-kripke-frame",
153            "omena-categorical.kripke-frame",
154            "Kripke frame valuations",
155        ),
156        (
157            "rust/omena-categorical/verify-cross-project-symmetry",
158            "omena-categorical.design-system-invariant-summary",
159            "cross-project symmetry",
160        ),
161    ]
162    .into_iter()
163    .map(
164        |(endpoint_id, evidence_product, fixture_focus)| CategoricalEvidenceEndpointV0 {
165            schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
166            product: "omena-categorical.evidence-endpoint",
167            layer_marker: CATEGORICAL_LAYER_MARKER_V0,
168            feature_gate: CATEGORICAL_FEATURE_GATE_V0,
169            endpoint_id,
170            evidence_product,
171            fixture_focus,
172        },
173    )
174    .collect()
175}
176
177#[deprecated(
178    since = "0.4.0",
179    note = "use cascade_section_evidence_endpoints_v0; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
180)]
181pub fn categorical_evidence_endpoints_v0() -> Vec<CategoricalEvidenceEndpointV0> {
182    let mut endpoints = cascade_section_evidence_endpoints_v0();
183    if let Some(endpoint) = endpoints.get_mut(0) {
184        endpoint.endpoint_id = "rust/omena-categorical/verify-site-stability";
185        endpoint.evidence_product = "omena-categorical.cascade-site";
186        endpoint.fixture_focus = "site axioms";
187    }
188    if let Some(endpoint) = endpoints.get_mut(1) {
189        endpoint.endpoint_id = "rust/omena-categorical/verify-cosheaf-covariance";
190        endpoint.evidence_product = "omena-categorical.cascade-cosheaf";
191        endpoint.fixture_focus = "cosheaf covariance";
192    }
193    endpoints
194}
195
196pub fn cascade_implementation_roles_v0() -> Vec<CascadePrimitiveRoleV0> {
197    [
198        (
199            "ranking",
200            "cascade_property",
201            "cascade section aggregation witness",
202        ),
203        (
204            "proof",
205            "prove_layer_flatten_candidate",
206            "Beck-Chevalley origin inversion witness",
207        ),
208        (
209            "proof",
210            "prove_scope_flatten_candidate",
211            "scope stratification morphism witness",
212        ),
213        (
214            "proof",
215            "prove_box_shorthand_combination",
216            "shorthand invariant functor witness",
217        ),
218        (
219            "evaluation",
220            "evaluate_static_supports_condition",
221            "cascade-section-axis decidability witness",
222        ),
223    ]
224    .into_iter()
225    .map(
226        |(primitive_kind, primitive_name, categorical_role)| CascadePrimitiveRoleV0 {
227            schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
228            product: "omena-categorical.cascade-primitive-role",
229            layer_marker: CATEGORICAL_LAYER_MARKER_V0,
230            feature_gate: CATEGORICAL_FEATURE_GATE_V0,
231            primitive_kind,
232            primitive_name,
233            categorical_role,
234        },
235    )
236    .collect()
237}
238
239#[deprecated(
240    since = "0.4.0",
241    note = "use cascade_implementation_roles_v0; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
242)]
243pub fn cascade_primitive_roles_v0() -> Vec<CascadePrimitiveRoleV0> {
244    let mut roles = cascade_implementation_roles_v0();
245    if let Some(role) = roles
246        .iter_mut()
247        .find(|role| role.primitive_name == "cascade_property")
248    {
249        role.categorical_role = "cosheaf colimit witness";
250    }
251    if let Some(role) = roles
252        .iter_mut()
253        .find(|role| role.primitive_name == "evaluate_static_supports_condition")
254    {
255        role.categorical_role = "site-axis decidability witness";
256    }
257    roles
258}
259
260pub fn cascade_section_evidence_v0(source_product: &'static str) -> CategoricalCascadeEvidenceV0 {
261    let endpoints = cascade_section_evidence_endpoints_v0();
262    let cascade_primitive_roles = cascade_implementation_roles_v0();
263    let fixture_evidence = endpoints
264        .iter()
265        .map(|endpoint| {
266            cascade_section_fixture_evidence_for_endpoint_v0(endpoint.endpoint_id, &endpoints)
267        })
268        .collect();
269    CategoricalCascadeEvidenceV0 {
270        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
271        product: "omena-categorical.cascade-evidence",
272        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
273        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
274        source_product,
275        endpoint_count: endpoints.len(),
276        endpoints,
277        fixture_evidence,
278        functor_applications: vec![apply_cascade_role_mapping_functor_v0(
279            "cascade-primitive-role-functor",
280            "omena-categorical.cascade-primitive-role-functor",
281            &cascade_primitive_roles
282                .iter()
283                .map(|role| {
284                    (
285                        role.primitive_name.to_string(),
286                        slug_v0(role.categorical_role),
287                    )
288                })
289                .collect::<Vec<_>>(),
290        )],
291        cascade_primitive_roles,
292        default_feature_enabled: false,
293    }
294}
295
296#[allow(deprecated)]
297#[deprecated(
298    since = "0.4.0",
299    note = "use cascade_section_evidence_v0; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
300)]
301pub fn categorical_cascade_evidence_v0(
302    source_product: &'static str,
303) -> CategoricalCascadeEvidenceV0 {
304    let endpoints = categorical_evidence_endpoints_v0();
305    let cascade_primitive_roles = cascade_primitive_roles_v0();
306    let fixture_evidence = endpoints
307        .iter()
308        .map(|endpoint| {
309            cascade_section_fixture_evidence_for_endpoint_v0(endpoint.endpoint_id, &endpoints)
310        })
311        .collect();
312    CategoricalCascadeEvidenceV0 {
313        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
314        product: "omena-categorical.cascade-evidence",
315        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
316        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
317        source_product,
318        endpoint_count: endpoints.len(),
319        endpoints,
320        fixture_evidence,
321        functor_applications: vec![apply_cascade_role_mapping_functor_v0(
322            "cascade-primitive-role-functor",
323            "omena-categorical.cascade-primitive-role-functor",
324            &cascade_primitive_roles
325                .iter()
326                .map(|role| {
327                    (
328                        role.primitive_name.to_string(),
329                        slug_v0(role.categorical_role),
330                    )
331                })
332                .collect::<Vec<_>>(),
333        )],
334        cascade_primitive_roles,
335        default_feature_enabled: false,
336    }
337}
338
339pub fn cascade_section_evidence_for_exercised_primitives_v0(
340    source_product: &'static str,
341    exercised_primitive_role_pairs: &[(String, String)],
342) -> CategoricalCascadeEvidenceV0 {
343    let endpoints = cascade_section_evidence_endpoints_v0();
344    let cascade_primitive_roles = cascade_implementation_roles_v0()
345        .into_iter()
346        .filter(|role| {
347            exercised_primitive_role_pairs
348                .iter()
349                .any(|(primitive_name, _)| primitive_name == role.primitive_name)
350        })
351        .collect::<Vec<_>>();
352    let fixture_evidence = endpoints
353        .iter()
354        .map(|endpoint| {
355            cascade_section_fixture_evidence_for_endpoint_v0(endpoint.endpoint_id, &endpoints)
356        })
357        .collect();
358    CategoricalCascadeEvidenceV0 {
359        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
360        product: "omena-categorical.cascade-evidence",
361        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
362        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
363        source_product,
364        endpoint_count: endpoints.len(),
365        endpoints,
366        fixture_evidence,
367        functor_applications: vec![apply_cascade_role_mapping_functor_v0(
368            "cascade-exercised-primitive-role-functor",
369            "omena-categorical.cascade-primitive-role-functor",
370            exercised_primitive_role_pairs,
371        )],
372        cascade_primitive_roles,
373        default_feature_enabled: false,
374    }
375}
376
377#[allow(deprecated)]
378#[deprecated(
379    since = "0.4.0",
380    note = "use cascade_section_evidence_for_exercised_primitives_v0; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
381)]
382pub fn categorical_cascade_evidence_for_exercised_primitives_v0(
383    source_product: &'static str,
384    exercised_primitive_role_pairs: &[(String, String)],
385) -> CategoricalCascadeEvidenceV0 {
386    let endpoints = categorical_evidence_endpoints_v0();
387    let cascade_primitive_roles = cascade_primitive_roles_v0()
388        .into_iter()
389        .filter(|role| {
390            exercised_primitive_role_pairs
391                .iter()
392                .any(|(primitive_name, _)| primitive_name == role.primitive_name)
393        })
394        .collect::<Vec<_>>();
395    let fixture_evidence = endpoints
396        .iter()
397        .map(|endpoint| {
398            cascade_section_fixture_evidence_for_endpoint_v0(endpoint.endpoint_id, &endpoints)
399        })
400        .collect();
401    CategoricalCascadeEvidenceV0 {
402        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
403        product: "omena-categorical.cascade-evidence",
404        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
405        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
406        source_product,
407        endpoint_count: endpoints.len(),
408        endpoints,
409        fixture_evidence,
410        functor_applications: vec![apply_cascade_role_mapping_functor_v0(
411            "cascade-exercised-primitive-role-functor",
412            "omena-categorical.cascade-primitive-role-functor",
413            exercised_primitive_role_pairs,
414        )],
415        cascade_primitive_roles,
416        default_feature_enabled: false,
417    }
418}
419
420fn cascade_section_fixture_evidence_for_endpoint_v0(
421    endpoint_id: &'static str,
422    endpoints: &[CategoricalEvidenceEndpointV0],
423) -> CategoricalEndpointFixtureEvidenceV0 {
424    let deferred = endpoint_id == "rust/omena-categorical/verify-cross-project-symmetry";
425    let claim_scope = if deferred {
426        "researchDeferredMissingSourceSensitiveSubstrate"
427    } else {
428        "computedEvidence"
429    };
430    let evidence_product = endpoints
431        .iter()
432        .find(|endpoint| endpoint.endpoint_id == endpoint_id)
433        .map(|endpoint| endpoint.evidence_product)
434        .unwrap_or("omena-categorical.unknown");
435    let assertion = CategoricalFixtureAssertionV0 {
436        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
437        product: "omena-categorical.fixture-assertion",
438        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
439        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
440        assertion_id: if deferred {
441            "source-sensitive-substrate-deferred"
442        } else {
443            "product-path-contract-present"
444        },
445        contract_product: evidence_product,
446        observed: if deferred {
447            "sourceSensitiveSubstrate=missing".to_string()
448        } else {
449            "productPathEvidence=present".to_string()
450        },
451        expected: if deferred {
452            "sourceSensitiveSubstrate=available".to_string()
453        } else {
454            "productPathEvidence=present".to_string()
455        },
456        accepted: !deferred,
457    };
458    CategoricalEndpointFixtureEvidenceV0 {
459        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
460        product: "omena-categorical.endpoint-fixture-evidence",
461        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
462        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
463        claim_scope,
464        endpoint_id,
465        fixture_id: if deferred {
466            "fixture.categorical.cross-project-symmetry.v0"
467        } else {
468            "fixture.categorical.product-path.v0"
469        },
470        fixture_focus: if deferred {
471            "cross-project symmetry"
472        } else {
473            "product path evidence"
474        },
475        evidence_product,
476        exercised_contract_products: vec![evidence_product],
477        assertion_count: 1,
478        assertions: vec![assertion],
479        accepted: !deferred,
480    }
481}
482
483#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
484#[serde(rename_all = "camelCase")]
485struct CascadeCategoryObjectV0 {
486    schema_version: &'static str,
487    product: &'static str,
488    layer_marker: &'static str,
489    feature_gate: &'static str,
490    object_id: String,
491    object_kind: &'static str,
492}
493
494#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
495#[serde(rename_all = "camelCase")]
496struct CascadeCategoryMorphismV0 {
497    schema_version: &'static str,
498    product: &'static str,
499    layer_marker: &'static str,
500    feature_gate: &'static str,
501    morphism_id: String,
502    from_object_id: String,
503    to_object_id: String,
504    relation: &'static str,
505}
506
507#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
508#[serde(rename_all = "camelCase")]
509struct CascadeCategoryV0 {
510    schema_version: &'static str,
511    product: &'static str,
512    layer_marker: &'static str,
513    feature_gate: &'static str,
514    category_id: String,
515    objects: Vec<CascadeCategoryObjectV0>,
516    morphisms: Vec<CascadeCategoryMorphismV0>,
517}
518
519#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
520#[serde(rename_all = "camelCase")]
521struct CascadeFunctorObjectMappingV0 {
522    schema_version: &'static str,
523    product: &'static str,
524    layer_marker: &'static str,
525    feature_gate: &'static str,
526    source_object_id: String,
527    target_object_id: String,
528}
529
530#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
531#[serde(rename_all = "camelCase")]
532struct CascadeFunctorMorphismMappingV0 {
533    schema_version: &'static str,
534    product: &'static str,
535    layer_marker: &'static str,
536    feature_gate: &'static str,
537    source_morphism_id: String,
538    target_morphism_id: String,
539    source_from_object_id: String,
540    source_to_object_id: String,
541    target_from_object_id: String,
542    target_to_object_id: String,
543}
544
545pub fn apply_cascade_role_mapping_functor_v0(
546    functor_id: &str,
547    functor_product: &'static str,
548    object_role_pairs: &[(String, String)],
549) -> CascadeFunctorApplicationV0 {
550    let source_objects = object_role_pairs
551        .iter()
552        .map(|(primitive_name, _)| {
553            category_object_v0(format!("primitive:{primitive_name}"), "primitive")
554        })
555        .collect::<Vec<_>>();
556    let target_objects = object_role_pairs
557        .iter()
558        .map(|(_, role_slug)| category_object_v0(format!("role:{role_slug}"), "role"))
559        .collect::<Vec<_>>();
560    let source = CascadeCategoryV0 {
561        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
562        product: "omena-categorical.cascade-category",
563        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
564        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
565        category_id: "cascade-primitives".to_string(),
566        morphisms: category_morphisms_from_objects_v0(&source_objects, "primitive-precedes"),
567        objects: source_objects,
568    };
569    let target = CascadeCategoryV0 {
570        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
571        product: "omena-categorical.cascade-category",
572        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
573        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
574        category_id: "categorical-roles".to_string(),
575        morphisms: category_morphisms_from_objects_v0(&target_objects, "role-precedes"),
576        objects: target_objects,
577    };
578    let object_mappings = object_role_pairs
579        .iter()
580        .map(
581            |(primitive_name, role_slug)| CascadeFunctorObjectMappingV0 {
582                schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
583                product: "omena-categorical.functor-object-mapping",
584                layer_marker: CATEGORICAL_LAYER_MARKER_V0,
585                feature_gate: CATEGORICAL_FEATURE_GATE_V0,
586                source_object_id: format!("primitive:{primitive_name}"),
587                target_object_id: format!("role:{role_slug}"),
588            },
589        )
590        .collect::<Vec<_>>();
591    let morphism_mappings = source
592        .morphisms
593        .iter()
594        .filter(|morphism| morphism.relation != "identity")
595        .filter_map(|source_morphism| {
596            let target_from = map_object_id_v0(&object_mappings, &source_morphism.from_object_id)?;
597            let target_to = map_object_id_v0(&object_mappings, &source_morphism.to_object_id)?;
598            let target_morphism = find_morphism_v0(&target, &target_from, &target_to)?;
599            Some(CascadeFunctorMorphismMappingV0 {
600                schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
601                product: "omena-categorical.functor-morphism-mapping",
602                layer_marker: CATEGORICAL_LAYER_MARKER_V0,
603                feature_gate: CATEGORICAL_FEATURE_GATE_V0,
604                source_morphism_id: source_morphism.morphism_id.clone(),
605                target_morphism_id: target_morphism.morphism_id.clone(),
606                source_from_object_id: source_morphism.from_object_id.clone(),
607                source_to_object_id: source_morphism.to_object_id.clone(),
608                target_from_object_id: target_from,
609                target_to_object_id: target_to,
610            })
611        })
612        .collect::<Vec<_>>();
613
614    let source_non_identity = source
615        .morphisms
616        .iter()
617        .filter(|morphism| morphism.relation != "identity")
618        .collect::<Vec<_>>();
619    let composed_source = source_non_identity
620        .first()
621        .zip(source_non_identity.get(1))
622        .and_then(|(left, right)| compose_morphisms_v0(left, right, "source-composite"));
623    let composed_target = composed_source.as_ref().and_then(|composite| {
624        let target_from = map_object_id_v0(&object_mappings, &composite.from_object_id)?;
625        let target_to = map_object_id_v0(&object_mappings, &composite.to_object_id)?;
626        let left = find_morphism_v0(
627            &target,
628            &map_object_id_v0(&object_mappings, &source_non_identity[0].from_object_id)?,
629            &map_object_id_v0(&object_mappings, &source_non_identity[0].to_object_id)?,
630        )?;
631        let right = find_morphism_v0(
632            &target,
633            &map_object_id_v0(&object_mappings, &source_non_identity[1].from_object_id)?,
634            &map_object_id_v0(&object_mappings, &source_non_identity[1].to_object_id)?,
635        )?;
636        let target_composite = compose_morphisms_v0(left, right, "target-composite")?;
637        (target_composite.from_object_id == target_from
638            && target_composite.to_object_id == target_to)
639            .then_some(target_composite)
640    });
641    let identity_preserved = source.objects.iter().all(|object| {
642        let Some(target_object_id) = map_object_id_v0(&object_mappings, &object.object_id) else {
643            return false;
644        };
645        find_morphism_v0(&source, &object.object_id, &object.object_id).is_some()
646            && find_morphism_v0(&target, &target_object_id, &target_object_id).is_some()
647    });
648    let composition_preserved = composed_source.is_some() && composed_target.is_some();
649
650    CascadeFunctorApplicationV0 {
651        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
652        product: functor_product,
653        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
654        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
655        functor_id: functor_id.to_string(),
656        source_category_id: source.category_id,
657        target_category_id: target.category_id,
658        object_mapping_count: object_mappings.len(),
659        morphism_mapping_count: morphism_mappings.len(),
660        composed_source_morphism_id: composed_source.map(|morphism| morphism.morphism_id),
661        composed_target_morphism_id: composed_target.map(|morphism| morphism.morphism_id),
662        identity_preserved,
663        composition_preserved,
664        accepted: identity_preserved && composition_preserved && !morphism_mappings.is_empty(),
665    }
666}
667
668fn category_object_v0(object_id: String, object_kind: &'static str) -> CascadeCategoryObjectV0 {
669    CascadeCategoryObjectV0 {
670        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
671        product: "omena-categorical.category-object",
672        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
673        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
674        object_id,
675        object_kind,
676    }
677}
678
679fn category_morphisms_from_objects_v0(
680    objects: &[CascadeCategoryObjectV0],
681    relation: &'static str,
682) -> Vec<CascadeCategoryMorphismV0> {
683    let mut morphisms = objects
684        .iter()
685        .map(|object| CascadeCategoryMorphismV0 {
686            schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
687            product: "omena-categorical.category-morphism",
688            layer_marker: CATEGORICAL_LAYER_MARKER_V0,
689            feature_gate: CATEGORICAL_FEATURE_GATE_V0,
690            morphism_id: format!("id:{}", object.object_id),
691            from_object_id: object.object_id.clone(),
692            to_object_id: object.object_id.clone(),
693            relation: "identity",
694        })
695        .collect::<Vec<_>>();
696
697    morphisms.extend(objects.windows(2).map(|window| CascadeCategoryMorphismV0 {
698        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
699        product: "omena-categorical.category-morphism",
700        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
701        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
702        morphism_id: format!("{}->{}", window[0].object_id, window[1].object_id),
703        from_object_id: window[0].object_id.clone(),
704        to_object_id: window[1].object_id.clone(),
705        relation,
706    }));
707    morphisms
708}
709
710fn compose_morphisms_v0(
711    left: &CascadeCategoryMorphismV0,
712    right: &CascadeCategoryMorphismV0,
713    relation: &'static str,
714) -> Option<CascadeCategoryMorphismV0> {
715    (left.to_object_id == right.from_object_id).then(|| CascadeCategoryMorphismV0 {
716        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
717        product: "omena-categorical.category-morphism-composition",
718        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
719        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
720        morphism_id: format!("{};{}", left.morphism_id, right.morphism_id),
721        from_object_id: left.from_object_id.clone(),
722        to_object_id: right.to_object_id.clone(),
723        relation,
724    })
725}
726
727fn find_morphism_v0<'a>(
728    category: &'a CascadeCategoryV0,
729    from_object_id: &str,
730    to_object_id: &str,
731) -> Option<&'a CascadeCategoryMorphismV0> {
732    category.morphisms.iter().find(|morphism| {
733        morphism.from_object_id == from_object_id && morphism.to_object_id == to_object_id
734    })
735}
736
737fn map_object_id_v0(
738    mappings: &[CascadeFunctorObjectMappingV0],
739    source_object_id: &str,
740) -> Option<String> {
741    mappings
742        .iter()
743        .find(|mapping| mapping.source_object_id == source_object_id)
744        .map(|mapping| mapping.target_object_id.clone())
745}
746
747fn slug_v0(value: &str) -> String {
748    value
749        .chars()
750        .map(|character| {
751            if character.is_ascii_alphanumeric() {
752                character.to_ascii_lowercase()
753            } else {
754                '-'
755            }
756        })
757        .collect::<String>()
758        .split('-')
759        .filter(|part| !part.is_empty())
760        .collect::<Vec<_>>()
761        .join("-")
762}
763
764#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
765#[serde(rename_all = "camelCase")]
766pub struct DesignSystemEdgeKindCountV0 {
767    pub schema_version: &'static str,
768    pub product: &'static str,
769    pub layer_marker: &'static str,
770    pub feature_gate: &'static str,
771    pub edge_kind: String,
772    pub count: usize,
773}
774
775#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
776#[serde(rename_all = "camelCase")]
777pub struct DesignSystemProjectSummaryInputV0 {
778    pub schema_version: &'static str,
779    pub product: &'static str,
780    pub layer_marker: &'static str,
781    pub feature_gate: &'static str,
782    pub project_id: String,
783    pub source_product: &'static str,
784    pub summary_hash: String,
785    pub summary_edge_count: usize,
786    pub edge_kind_counts: Vec<DesignSystemEdgeKindCountV0>,
787}
788
789#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
790#[serde(rename_all = "camelCase")]
791pub struct SortInterpretationV0 {
792    pub schema_version: &'static str,
793    pub product: &'static str,
794    pub layer_marker: &'static str,
795    pub feature_gate: &'static str,
796    pub sort_name: String,
797    pub element_count: usize,
798}
799
800#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
801#[serde(rename_all = "camelCase")]
802pub struct DesignSystemModelV0 {
803    pub schema_version: &'static str,
804    pub product: &'static str,
805    pub layer_marker: &'static str,
806    pub feature_gate: &'static str,
807    pub model_id: String,
808    pub theory_id: String,
809    pub source_product: &'static str,
810    pub project_id: String,
811    pub summary_hash: String,
812    pub summary_edge_count: usize,
813    pub edge_kind_counts: Vec<DesignSystemEdgeKindCountV0>,
814    pub sort_interpretations: Vec<SortInterpretationV0>,
815}
816
817#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
818#[serde(rename_all = "camelCase")]
819pub struct DesignSystemInvariantSummaryV0 {
820    pub schema_version: &'static str,
821    pub product: &'static str,
822    pub layer_marker: &'static str,
823    pub feature_gate: &'static str,
824    pub invariant_id: String,
825    pub invariant_kind: &'static str,
826    pub model_count: usize,
827    pub source_products: Vec<&'static str>,
828    pub model_hashes: Vec<String>,
829    pub differing_sort_names: Vec<String>,
830    pub accepted: bool,
831}
832
833pub fn design_system_model_from_project_summary_v0(
834    theory_id: impl Into<String>,
835    input: DesignSystemProjectSummaryInputV0,
836) -> DesignSystemModelV0 {
837    let theory_id = theory_id.into();
838    let mut edge_kind_counts = input.edge_kind_counts;
839    edge_kind_counts.sort();
840    let mut sort_interpretations = edge_kind_counts
841        .iter()
842        .map(|entry| SortInterpretationV0 {
843            schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
844            product: "omena-categorical.sort-interpretation",
845            layer_marker: CATEGORICAL_LAYER_MARKER_V0,
846            feature_gate: CATEGORICAL_FEATURE_GATE_V0,
847            sort_name: format!("edgeKind:{}", entry.edge_kind),
848            element_count: entry.count,
849        })
850        .collect::<Vec<_>>();
851    sort_interpretations.push(SortInterpretationV0 {
852        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
853        product: "omena-categorical.sort-interpretation",
854        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
855        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
856        sort_name: "summaryEdge".to_string(),
857        element_count: input.summary_edge_count,
858    });
859    sort_interpretations.sort_by(|left, right| left.sort_name.cmp(&right.sort_name));
860
861    DesignSystemModelV0 {
862        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
863        product: "omena-categorical.design-system-model",
864        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
865        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
866        model_id: format!(
867            "design-system-model:{}:{}",
868            input.project_id, input.summary_hash
869        ),
870        theory_id,
871        source_product: input.source_product,
872        project_id: input.project_id,
873        summary_hash: input.summary_hash,
874        summary_edge_count: input.summary_edge_count,
875        edge_kind_counts,
876        sort_interpretations,
877    }
878}
879
880pub fn compare_design_system_models_for_invariant_v0(
881    invariant_id: impl Into<String>,
882    models: &[DesignSystemModelV0],
883) -> DesignSystemInvariantSummaryV0 {
884    let differing_sort_names = differing_design_system_model_sort_names_v0(models);
885    DesignSystemInvariantSummaryV0 {
886        schema_version: CATEGORICAL_SCHEMA_VERSION_V0,
887        product: "omena-categorical.design-system-invariant-summary",
888        layer_marker: CATEGORICAL_LAYER_MARKER_V0,
889        feature_gate: CATEGORICAL_FEATURE_GATE_V0,
890        invariant_id: invariant_id.into(),
891        invariant_kind: "crossProjectEdgeKindSymmetry",
892        model_count: models.len(),
893        source_products: models.iter().map(|model| model.source_product).collect(),
894        model_hashes: models
895            .iter()
896            .map(|model| model.summary_hash.clone())
897            .collect(),
898        accepted: models.len() >= 2 && differing_sort_names.is_empty(),
899        differing_sort_names,
900    }
901}
902
903fn differing_design_system_model_sort_names_v0(models: &[DesignSystemModelV0]) -> Vec<String> {
904    let Some(first) = models.first() else {
905        return Vec::new();
906    };
907    let baseline = first
908        .sort_interpretations
909        .iter()
910        .map(|sort| (sort.sort_name.as_str(), sort.element_count))
911        .collect::<Vec<_>>();
912    let mut differing_sort_names = BTreeSet::new();
913    for model in models.iter().skip(1) {
914        for (sort_name, baseline_count) in &baseline {
915            let current_count = model
916                .sort_interpretations
917                .iter()
918                .find(|sort| sort.sort_name == *sort_name)
919                .map(|sort| sort.element_count);
920            if current_count != Some(*baseline_count) {
921                differing_sort_names.insert((*sort_name).to_string());
922            }
923        }
924        for sort in &model.sort_interpretations {
925            if !baseline
926                .iter()
927                .any(|(sort_name, _)| *sort_name == sort.sort_name)
928            {
929                differing_sort_names.insert(sort.sort_name.clone());
930            }
931        }
932    }
933    differing_sort_names.into_iter().collect()
934}
935
936pub const MULTISCALE_COMPLEXITY_HEURISTIC_SCHEMA_VERSION_V0: &str = "0";
937#[deprecated(
938    since = "0.4.0",
939    note = "legacy layer byte owned by omena-product-hints maintainers; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
940)]
941const LEGACY_MULTISCALE_COMPLEXITY_LAYER_BYTES_V0: &str = "rg-flow-statistical";
942#[deprecated(
943    since = "0.4.0",
944    note = "legacy feature byte owned by omena-product-hints maintainers; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
945)]
946const LEGACY_MULTISCALE_COMPLEXITY_FEATURE_BYTES_V0: &str = "rg-flow";
947#[allow(deprecated)]
948const MULTISCALE_COMPLEXITY_HEURISTIC_COMPATIBILITY_LAYER_MARKER_V0: &str =
949    LEGACY_MULTISCALE_COMPLEXITY_LAYER_BYTES_V0;
950#[allow(deprecated)]
951const MULTISCALE_COMPLEXITY_HEURISTIC_COMPATIBILITY_FEATURE_GATE_V0: &str =
952    LEGACY_MULTISCALE_COMPLEXITY_FEATURE_BYTES_V0;
953pub const MULTISCALE_COMPLEXITY_HEURISTIC_LAYER_MARKER_V0: &str =
954    "multiscale-complexity-heuristic-statistical";
955pub const MULTISCALE_COMPLEXITY_HEURISTIC_FEATURE_GATE_V0: &str = "multiscale-complexity-heuristic";
956pub const MULTISCALE_COMPLEXITY_HEURISTIC_MECHANISM_SCOPE_V0: &str =
957    "optInDeepAnalysisJacobianSpectrumHintSubstrate";
958pub const MULTISCALE_COMPLEXITY_HEURISTIC_PRODUCT_SURFACE_V0: &str =
959    "deepAnalysisCascadeSensitivityHint";
960pub const MULTISCALE_COMPLEXITY_HEURISTIC_DEFAULT_PRODUCT_DECISION_MECHANISM_V0: bool = false;
961const MULTISCALE_COMPLEXITY_HEURISTIC_EIGEN_EPSILON: f64 = 1e-9;
962
963/// Pre-1.0 wire compatibility constants owned by `omena-product-hints`
964/// maintainers. Remove after downstream migration and zero audited non-compat uses.
965#[deprecated(
966    since = "0.4.0",
967    note = "use MULTISCALE_COMPLEXITY_HEURISTIC_*; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
968)]
969pub const RG_FLOW_SCHEMA_VERSION_V0: &str = "0";
970#[deprecated(
971    since = "0.4.0",
972    note = "use MULTISCALE_COMPLEXITY_HEURISTIC_*; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
973)]
974pub const RG_FLOW_LAYER_MARKER_V0: &str = "rg-flow-statistical";
975#[deprecated(
976    since = "0.4.0",
977    note = "use MULTISCALE_COMPLEXITY_HEURISTIC_*; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
978)]
979pub const RG_FLOW_FEATURE_GATE_V0: &str = "rg-flow";
980#[deprecated(
981    since = "0.4.0",
982    note = "use MULTISCALE_COMPLEXITY_HEURISTIC_*; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
983)]
984pub const RG_FLOW_MECHANISM_SCOPE_V0: &str = MULTISCALE_COMPLEXITY_HEURISTIC_MECHANISM_SCOPE_V0;
985#[deprecated(
986    since = "0.4.0",
987    note = "use MULTISCALE_COMPLEXITY_HEURISTIC_*; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
988)]
989pub const RG_FLOW_PRODUCT_SURFACE_V0: &str = MULTISCALE_COMPLEXITY_HEURISTIC_PRODUCT_SURFACE_V0;
990#[deprecated(
991    since = "0.4.0",
992    note = "use MULTISCALE_COMPLEXITY_HEURISTIC_*; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
993)]
994pub const RG_FLOW_DEFAULT_PRODUCT_DECISION_MECHANISM_V0: bool =
995    MULTISCALE_COMPLEXITY_HEURISTIC_DEFAULT_PRODUCT_DECISION_MECHANISM_V0;
996
997#[derive(Debug, Clone, PartialEq, Serialize)]
998#[serde(rename_all = "camelCase")]
999pub struct CouplingSpaceV0 {
1000    pub schema_version: &'static str,
1001    pub product: &'static str,
1002    pub layer_marker: &'static str,
1003    pub feature_gate: &'static str,
1004    pub k_env: usize,
1005    pub k_decl: usize,
1006    pub k_cycle: usize,
1007    pub k_dirty: usize,
1008}
1009
1010#[derive(Debug, Clone, PartialEq, Serialize)]
1011#[serde(rename_all = "camelCase")]
1012pub struct CouplingJacobianSpectrumV0 {
1013    pub schema_version: &'static str,
1014    pub product: &'static str,
1015    pub layer_marker: &'static str,
1016    pub feature_gate: &'static str,
1017    pub mechanism_scope: &'static str,
1018    pub product_surface: &'static str,
1019    pub default_product_decision_mechanism: bool,
1020    pub matrix: Vec<Vec<f64>>,
1021    pub eigenvalues: Vec<f64>,
1022    pub spectral_radius: f64,
1023    pub computed_from: &'static str,
1024}
1025
1026fn coupling_space_with_markers_v0(
1027    k_env: usize,
1028    k_decl: usize,
1029    k_cycle: usize,
1030    k_dirty: usize,
1031    layer_marker: &'static str,
1032    feature_gate: &'static str,
1033) -> CouplingSpaceV0 {
1034    CouplingSpaceV0 {
1035        schema_version: MULTISCALE_COMPLEXITY_HEURISTIC_SCHEMA_VERSION_V0,
1036        product: "omena-rg-flow.coupling-space",
1037        layer_marker,
1038        feature_gate,
1039        k_env,
1040        k_decl,
1041        k_cycle,
1042        k_dirty,
1043    }
1044}
1045
1046/// Pre-1.0 neutral-name compatibility constructor.
1047/// Owner: `omena-product-hints` maintainers. Removal condition: not before
1048/// 1.0, after downstream migration and zero audited non-compatibility uses.
1049#[allow(deprecated)]
1050#[deprecated(
1051    since = "0.4.0",
1052    note = "use multiscale_complexity_heuristic_coupling_space; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
1053)]
1054pub fn coupling_space(
1055    k_env: usize,
1056    k_decl: usize,
1057    k_cycle: usize,
1058    k_dirty: usize,
1059) -> CouplingSpaceV0 {
1060    coupling_space_with_markers_v0(
1061        k_env,
1062        k_decl,
1063        k_cycle,
1064        k_dirty,
1065        MULTISCALE_COMPLEXITY_HEURISTIC_COMPATIBILITY_LAYER_MARKER_V0,
1066        MULTISCALE_COMPLEXITY_HEURISTIC_COMPATIBILITY_FEATURE_GATE_V0,
1067    )
1068}
1069
1070/// Construct a coupling-space carrier with accurate diagnostic-layer bytes.
1071pub fn multiscale_complexity_heuristic_coupling_space(
1072    k_env: usize,
1073    k_decl: usize,
1074    k_cycle: usize,
1075    k_dirty: usize,
1076) -> CouplingSpaceV0 {
1077    coupling_space_with_markers_v0(
1078        k_env,
1079        k_decl,
1080        k_cycle,
1081        k_dirty,
1082        MULTISCALE_COMPLEXITY_HEURISTIC_LAYER_MARKER_V0,
1083        MULTISCALE_COMPLEXITY_HEURISTIC_FEATURE_GATE_V0,
1084    )
1085}
1086
1087fn coupling_jacobian_spectrum_with_markers_v0(
1088    before: &CouplingSpaceV0,
1089    after: &CouplingSpaceV0,
1090    layer_marker: &'static str,
1091    feature_gate: &'static str,
1092) -> CouplingJacobianSpectrumV0 {
1093    let beta_env = signed_delta(after.k_env, before.k_env);
1094    let beta_decl = signed_delta(after.k_decl, before.k_decl);
1095    let beta_cycle = signed_delta(after.k_cycle, before.k_cycle);
1096    let beta_dirty = signed_delta(after.k_dirty, before.k_dirty);
1097    let env_decl_cross = coupling_cross_sensitivity(before.k_decl, after.k_decl, before.k_env);
1098    let decl_env_cross = coupling_cross_sensitivity(before.k_env, after.k_env, before.k_decl);
1099    let cycle_dirty_cross =
1100        coupling_cross_sensitivity(before.k_dirty, after.k_dirty, before.k_cycle);
1101    let dirty_cycle_cross =
1102        coupling_cross_sensitivity(before.k_cycle, after.k_cycle, before.k_dirty);
1103    let matrix = vec![
1104        vec![
1105            diagonal_coupling_sensitivity(beta_env, before.k_env),
1106            env_decl_cross,
1107            0.0,
1108            0.0,
1109        ],
1110        vec![
1111            decl_env_cross,
1112            diagonal_coupling_sensitivity(beta_decl, before.k_decl),
1113            0.0,
1114            0.0,
1115        ],
1116        vec![
1117            0.0,
1118            0.0,
1119            diagonal_coupling_sensitivity(beta_cycle, before.k_cycle),
1120            cycle_dirty_cross,
1121        ],
1122        vec![
1123            0.0,
1124            0.0,
1125            dirty_cycle_cross,
1126            diagonal_coupling_sensitivity(beta_dirty, before.k_dirty),
1127        ],
1128    ];
1129    let mut eigenvalues =
1130        eigenvalues_for_2x2_block(matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1]);
1131    eigenvalues.extend(eigenvalues_for_2x2_block(
1132        matrix[2][2],
1133        matrix[2][3],
1134        matrix[3][2],
1135        matrix[3][3],
1136    ));
1137    let spectral_radius = eigenvalues
1138        .iter()
1139        .map(|value| value.abs())
1140        .fold(0.0, f64::max);
1141
1142    CouplingJacobianSpectrumV0 {
1143        schema_version: MULTISCALE_COMPLEXITY_HEURISTIC_SCHEMA_VERSION_V0,
1144        product: "omena-rg-flow.coupling-jacobian-spectrum",
1145        layer_marker,
1146        feature_gate,
1147        mechanism_scope: MULTISCALE_COMPLEXITY_HEURISTIC_MECHANISM_SCOPE_V0,
1148        product_surface: MULTISCALE_COMPLEXITY_HEURISTIC_PRODUCT_SURFACE_V0,
1149        default_product_decision_mechanism:
1150            MULTISCALE_COMPLEXITY_HEURISTIC_DEFAULT_PRODUCT_DECISION_MECHANISM_V0,
1151        matrix,
1152        eigenvalues,
1153        spectral_radius,
1154        computed_from: "finite-difference-linearization-v0",
1155    }
1156}
1157
1158/// Pre-1.0 neutral-name compatibility estimator.
1159/// Owner: `omena-product-hints` maintainers. Removal condition: not before
1160/// 1.0, after downstream migration and zero audited non-compatibility uses.
1161#[allow(deprecated)]
1162#[deprecated(
1163    since = "0.4.0",
1164    note = "use estimate_multiscale_complexity_heuristic_coupling_jacobian_spectrum_v0; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
1165)]
1166pub fn estimate_coupling_jacobian_spectrum_v0(
1167    before: &CouplingSpaceV0,
1168    after: &CouplingSpaceV0,
1169) -> CouplingJacobianSpectrumV0 {
1170    coupling_jacobian_spectrum_with_markers_v0(
1171        before,
1172        after,
1173        MULTISCALE_COMPLEXITY_HEURISTIC_COMPATIBILITY_LAYER_MARKER_V0,
1174        MULTISCALE_COMPLEXITY_HEURISTIC_COMPATIBILITY_FEATURE_GATE_V0,
1175    )
1176}
1177
1178/// Additive canonical projection with accurate diagnostic-layer bytes. The
1179/// pre-existing neutral estimator retains its V0 serialized layer and feature.
1180pub fn estimate_multiscale_complexity_heuristic_coupling_jacobian_spectrum_v0(
1181    before: &CouplingSpaceV0,
1182    after: &CouplingSpaceV0,
1183) -> CouplingJacobianSpectrumV0 {
1184    coupling_jacobian_spectrum_with_markers_v0(
1185        before,
1186        after,
1187        MULTISCALE_COMPLEXITY_HEURISTIC_LAYER_MARKER_V0,
1188        MULTISCALE_COMPLEXITY_HEURISTIC_FEATURE_GATE_V0,
1189    )
1190}
1191
1192fn signed_delta(after: usize, before: usize) -> f64 {
1193    after as f64 - before as f64
1194}
1195
1196fn diagonal_coupling_sensitivity(beta: f64, before: usize) -> f64 {
1197    beta / before.max(1) as f64
1198}
1199
1200fn coupling_cross_sensitivity(
1201    source_before: usize,
1202    source_after: usize,
1203    target_before: usize,
1204) -> f64 {
1205    let source_delta = signed_delta(source_after, source_before).abs();
1206    if source_delta <= MULTISCALE_COMPLEXITY_HEURISTIC_EIGEN_EPSILON {
1207        0.0
1208    } else {
1209        source_delta / source_before.saturating_add(target_before).max(1) as f64
1210    }
1211}
1212
1213fn eigenvalues_for_2x2_block(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
1214    let trace = a + d;
1215    let discriminant = ((a - d) * (a - d) + 4.0 * b * c).max(0.0).sqrt();
1216    vec![(trace + discriminant) / 2.0, (trace - discriminant) / 2.0]
1217}
1218
1219pub const VARIATIONAL_SCHEMA_VERSION_V0: &str = "0";
1220pub const VARIATIONAL_LAYER_MARKER_V0: &str = "variational-cascade";
1221pub const VARIATIONAL_FEATURE_GATE_V0: &str = "variational";
1222
1223#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1224#[serde(rename_all = "camelCase")]
1225pub enum DesignerIntentPosteriorModeV0 {
1226    VciFormal,
1227    PcnHierarchical,
1228    Fallback,
1229}
1230
1231#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1232#[serde(rename_all = "camelCase")]
1233pub enum PatternIntentV0 {
1234    Bem,
1235    Utility,
1236    Atomic,
1237    Hybrid,
1238    AdHoc,
1239}
1240
1241impl PatternIntentV0 {
1242    pub const fn as_str(self) -> &'static str {
1243        match self {
1244            Self::Bem => "bem",
1245            Self::Utility => "utility",
1246            Self::Atomic => "atomic",
1247            Self::Hybrid => "hybrid",
1248            Self::AdHoc => "adHoc",
1249        }
1250    }
1251}
1252
1253#[derive(Debug, Clone, PartialEq, Serialize)]
1254#[serde(rename_all = "camelCase")]
1255pub struct DesignerIntentScoreV0 {
1256    pub schema_version: &'static str,
1257    pub product: &'static str,
1258    pub layer_marker: &'static str,
1259    pub feature_gate: &'static str,
1260    pub intent: PatternIntentV0,
1261    pub log_probability_bits: f64,
1262}
1263
1264#[derive(Debug, Clone, PartialEq, Serialize)]
1265#[serde(rename_all = "camelCase")]
1266pub struct DesignerIntentPosteriorV0 {
1267    pub schema_version: &'static str,
1268    pub product: &'static str,
1269    pub layer_marker: &'static str,
1270    pub feature_gate: &'static str,
1271    pub mode: DesignerIntentPosteriorModeV0,
1272    pub selector_name: String,
1273    pub scores: Vec<DesignerIntentScoreV0>,
1274    pub enabled_by_default: bool,
1275}
1276
1277#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1278#[serde(rename_all = "camelCase")]
1279pub struct DesignerIntentPosteriorInputV0 {
1280    pub schema_version: &'static str,
1281    pub product: &'static str,
1282    pub layer_marker: &'static str,
1283    pub feature_gate: &'static str,
1284    pub selector_name: String,
1285    pub declaration_count: usize,
1286    pub duplicate_property_tie_count: usize,
1287    pub custom_property_reference_count: usize,
1288}
1289
1290pub fn designer_intent_posterior_input_v0(
1291    selector_name: impl Into<String>,
1292    declaration_count: usize,
1293    duplicate_property_tie_count: usize,
1294    custom_property_reference_count: usize,
1295) -> DesignerIntentPosteriorInputV0 {
1296    DesignerIntentPosteriorInputV0 {
1297        schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
1298        product: "omena-variational.designer-intent-posterior-input",
1299        layer_marker: VARIATIONAL_LAYER_MARKER_V0,
1300        feature_gate: VARIATIONAL_FEATURE_GATE_V0,
1301        selector_name: selector_name.into(),
1302        declaration_count,
1303        duplicate_property_tie_count,
1304        custom_property_reference_count,
1305    }
1306}
1307
1308pub fn infer_designer_intent_posterior_v0(
1309    input: DesignerIntentPosteriorInputV0,
1310) -> DesignerIntentPosteriorV0 {
1311    let selector = normalize_selector_name_for_intent_v0(&input.selector_name);
1312    let has_bem_marker = selector.contains("__") || selector.contains("--");
1313    let looks_utility = selector.starts_with("u-")
1314        || selector.starts_with("is-")
1315        || selector.starts_with("has-")
1316        || selector
1317            .split('-')
1318            .any(|part| matches!(part, "m" | "p" | "mt" | "mb" | "ml" | "mr" | "bg" | "text"));
1319    let looks_atomic = input.declaration_count <= 1 && selector.len() <= 8;
1320    let looks_hybrid = selector.matches('-').count() >= 3
1321        || (has_bem_marker && input.custom_property_reference_count > 0);
1322    let mut scores = vec![
1323        DesignerIntentScoreV0 {
1324            schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
1325            product: "omena-variational.designer-intent-score",
1326            layer_marker: VARIATIONAL_LAYER_MARKER_V0,
1327            feature_gate: VARIATIONAL_FEATURE_GATE_V0,
1328            intent: PatternIntentV0::Bem,
1329            log_probability_bits: bool_bits_v0(has_bem_marker) * 7.0
1330                + bool_bits_v0(input.declaration_count > 1)
1331                - bool_bits_v0(input.duplicate_property_tie_count > 0),
1332        },
1333        DesignerIntentScoreV0 {
1334            schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
1335            product: "omena-variational.designer-intent-score",
1336            layer_marker: VARIATIONAL_LAYER_MARKER_V0,
1337            feature_gate: VARIATIONAL_FEATURE_GATE_V0,
1338            intent: PatternIntentV0::Utility,
1339            log_probability_bits: bool_bits_v0(looks_utility) * 6.5
1340                - bool_bits_v0(has_bem_marker) * 2.0
1341                + bool_bits_v0(input.declaration_count <= 2),
1342        },
1343        DesignerIntentScoreV0 {
1344            schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
1345            product: "omena-variational.designer-intent-score",
1346            layer_marker: VARIATIONAL_LAYER_MARKER_V0,
1347            feature_gate: VARIATIONAL_FEATURE_GATE_V0,
1348            intent: PatternIntentV0::Atomic,
1349            log_probability_bits: bool_bits_v0(looks_atomic) * 5.0
1350                - bool_bits_v0(input.declaration_count > 1) * 2.0,
1351        },
1352        DesignerIntentScoreV0 {
1353            schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
1354            product: "omena-variational.designer-intent-score",
1355            layer_marker: VARIATIONAL_LAYER_MARKER_V0,
1356            feature_gate: VARIATIONAL_FEATURE_GATE_V0,
1357            intent: PatternIntentV0::Hybrid,
1358            log_probability_bits: bool_bits_v0(has_bem_marker)
1359                + bool_bits_v0(looks_hybrid) * 4.0
1360                + bool_bits_v0(input.custom_property_reference_count > 0) * 1.5,
1361        },
1362        DesignerIntentScoreV0 {
1363            schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
1364            product: "omena-variational.designer-intent-score",
1365            layer_marker: VARIATIONAL_LAYER_MARKER_V0,
1366            feature_gate: VARIATIONAL_FEATURE_GATE_V0,
1367            intent: PatternIntentV0::AdHoc,
1368            log_probability_bits: bool_bits_v0(!has_bem_marker && !looks_utility)
1369                + bool_bits_v0(input.duplicate_property_tie_count > 0),
1370        },
1371    ];
1372    scores.sort_by(|left, right| {
1373        right
1374            .log_probability_bits
1375            .partial_cmp(&left.log_probability_bits)
1376            .unwrap_or(std::cmp::Ordering::Equal)
1377            .then_with(|| left.intent.as_str().cmp(right.intent.as_str()))
1378    });
1379    DesignerIntentPosteriorV0 {
1380        schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
1381        product: "omena-variational.designer-intent-posterior",
1382        layer_marker: VARIATIONAL_LAYER_MARKER_V0,
1383        feature_gate: VARIATIONAL_FEATURE_GATE_V0,
1384        mode: DesignerIntentPosteriorModeV0::VciFormal,
1385        selector_name: input.selector_name,
1386        scores,
1387        enabled_by_default: true,
1388    }
1389}
1390
1391pub fn dominant_designer_intent_v0(
1392    posterior: &DesignerIntentPosteriorV0,
1393) -> Option<PatternIntentV0> {
1394    posterior.scores.first().map(|score| score.intent)
1395}
1396
1397fn normalize_selector_name_for_intent_v0(selector_name: &str) -> String {
1398    selector_name
1399        .trim()
1400        .trim_start_matches('.')
1401        .split([':', '[', ' ', '>', '+', '~', ','])
1402        .next()
1403        .unwrap_or(selector_name)
1404        .trim()
1405        .to_string()
1406}
1407
1408fn bool_bits_v0(value: bool) -> f64 {
1409    if value { 1.0 } else { 0.0 }
1410}
1411
1412pub const REPLICA_ENSEMBLE_SCHEMA_VERSION_V0: &str = "0";
1413pub const REPLICA_ENSEMBLE_LAYER_MARKER_V0: &str = "replica-ensemble";
1414pub const REPLICA_ENSEMBLE_FEATURE_GATE_V0: &str = "replica-ensemble";
1415pub const REPLICA_ENSEMBLE_MECHANISM_SCOPE_V0: &str =
1416    "productWiredCrossFileConsistencyHintSubstrate";
1417pub const REPLICA_ENSEMBLE_PRODUCT_SURFACE_V0: &str = "defaultCrossFileConsistencyHint";
1418pub const REPLICA_ENSEMBLE_DEFAULT_PRODUCT_DECISION_MECHANISM_V0: bool = false;
1419
1420/// Pre-1.0 nominal compatibility key.
1421///
1422/// Owner: `omena-product-hints` maintainers. Removal is not before 1.0 and
1423/// requires downstream migration plus zero audited in-repo non-compatibility uses.
1424#[deprecated(
1425    since = "0.4.0",
1426    note = "use CascadeSectionKeyV0; removal is not before 1.0 and requires downstream migration plus zero audited in-repo non-compatibility uses"
1427)]
1428#[derive(Debug, Clone, Serialize)]
1429#[serde(rename_all = "camelCase")]
1430pub struct CascadeSiteKeyV0 {
1431    pub schema_version: &'static str,
1432    pub product: &'static str,
1433    pub layer_marker: &'static str,
1434    pub feature_gate: &'static str,
1435    pub element_selector: String,
1436    pub property: AuthoredPropertyTextV0,
1437}
1438
1439#[derive(Debug, Clone, Serialize)]
1440#[serde(rename_all = "camelCase")]
1441pub struct CascadeSectionKeyV0 {
1442    pub schema_version: &'static str,
1443    pub product: &'static str,
1444    pub layer_marker: &'static str,
1445    pub feature_gate: &'static str,
1446    pub element_selector: String,
1447    pub property: AuthoredPropertyTextV0,
1448}
1449
1450macro_rules! impl_cascade_key_identity {
1451    ($type_name:ty) => {
1452        #[allow(deprecated)]
1453        impl PartialEq for $type_name {
1454            fn eq(&self, other: &Self) -> bool {
1455                self.schema_version == other.schema_version
1456                    && self.product == other.product
1457                    && self.layer_marker == other.layer_marker
1458                    && self.feature_gate == other.feature_gate
1459                    && self.element_selector == other.element_selector
1460                    && self.property.to_property_name().canonical_key()
1461                        == other.property.to_property_name().canonical_key()
1462            }
1463        }
1464
1465        #[allow(deprecated)]
1466        impl Eq for $type_name {}
1467
1468        #[allow(deprecated)]
1469        impl PartialOrd for $type_name {
1470            fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1471                Some(self.cmp(other))
1472            }
1473        }
1474
1475        #[allow(deprecated)]
1476        impl Ord for $type_name {
1477            fn cmp(&self, other: &Self) -> Ordering {
1478                (
1479                    self.schema_version,
1480                    self.product,
1481                    self.layer_marker,
1482                    self.feature_gate,
1483                    &self.element_selector,
1484                    self.property.to_property_name().canonical_key(),
1485                )
1486                    .cmp(&(
1487                        other.schema_version,
1488                        other.product,
1489                        other.layer_marker,
1490                        other.feature_gate,
1491                        &other.element_selector,
1492                        other.property.to_property_name().canonical_key(),
1493                    ))
1494            }
1495        }
1496    };
1497}
1498
1499impl_cascade_key_identity!(CascadeSiteKeyV0);
1500impl_cascade_key_identity!(CascadeSectionKeyV0);
1501
1502#[allow(deprecated)]
1503#[deprecated(
1504    since = "0.4.0",
1505    note = "compatibility conversion owned by omena-product-hints maintainers; removal is not before 1.0 and requires downstream migration plus zero audited in-repo non-compatibility uses"
1506)]
1507pub fn cascade_section_key_from_site_key_v0(key: CascadeSiteKeyV0) -> CascadeSectionKeyV0 {
1508    CascadeSectionKeyV0 {
1509        schema_version: key.schema_version,
1510        product: "omena-ensemble.cascade-section-key",
1511        layer_marker: key.layer_marker,
1512        feature_gate: key.feature_gate,
1513        element_selector: key.element_selector,
1514        property: key.property,
1515    }
1516}
1517
1518#[allow(deprecated)]
1519#[deprecated(
1520    since = "0.4.0",
1521    note = "compatibility conversion owned by omena-product-hints maintainers; removal is not before 1.0 and requires downstream migration plus zero audited in-repo non-compatibility uses"
1522)]
1523pub fn compatibility_key_from_cascade_section_key_v0(key: CascadeSectionKeyV0) -> CascadeSiteKeyV0 {
1524    CascadeSiteKeyV0 {
1525        schema_version: key.schema_version,
1526        product: "omena-ensemble.cascade-site-key",
1527        layer_marker: key.layer_marker,
1528        feature_gate: key.feature_gate,
1529        element_selector: key.element_selector,
1530        property: key.property,
1531    }
1532}
1533
1534#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1535#[serde(rename_all = "camelCase")]
1536pub struct LinearProvenanceTagV0 {
1537    pub schema_version: &'static str,
1538    pub product: &'static str,
1539    pub layer_marker: &'static str,
1540    pub feature_gate: &'static str,
1541    pub semiring_identifier: &'static str,
1542    pub label: String,
1543}
1544
1545#[allow(deprecated)]
1546#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1547#[serde(rename_all = "camelCase")]
1548pub struct ReplicaSiteOutcomeV0 {
1549    pub schema_version: &'static str,
1550    pub product: &'static str,
1551    pub layer_marker: &'static str,
1552    pub feature_gate: &'static str,
1553    #[deprecated(
1554        since = "0.4.0",
1555        note = "use a future cascade-section outcome carrier; removal is not before 1.0 and requires downstream migration plus zero audited in-repo non-compatibility uses"
1556    )]
1557    pub site: CascadeSiteKeyV0,
1558    pub outcome: CascadeOutcome,
1559    pub provenance: Option<LinearProvenanceTagV0>,
1560}
1561
1562#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1563#[serde(rename_all = "camelCase")]
1564pub struct ReplicaSnapshotV0 {
1565    pub schema_version: &'static str,
1566    pub product: &'static str,
1567    pub layer_marker: &'static str,
1568    pub feature_gate: &'static str,
1569    pub path: String,
1570    pub sites: Vec<ReplicaSiteOutcomeV0>,
1571}
1572
1573#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
1574#[serde(rename_all = "camelCase")]
1575pub enum OutcomeMode {
1576    #[default]
1577    DefiniteOnly,
1578    WidenedRankedSet,
1579    FullStrict,
1580}
1581
1582#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1583#[serde(rename_all = "camelCase")]
1584pub enum SamplingPolicy {
1585    AllPairs,
1586    PageRankWeighted { max_pair_count: usize },
1587    RandomSubset { max_pair_count: usize },
1588}
1589
1590#[derive(Debug, Clone, PartialEq, Serialize)]
1591#[serde(rename_all = "camelCase")]
1592pub struct ReplicaOverlapV0 {
1593    pub schema_version: &'static str,
1594    pub product: &'static str,
1595    pub layer_marker: &'static str,
1596    pub feature_gate: &'static str,
1597    pub replica_alpha_path: String,
1598    pub replica_beta_path: String,
1599    pub outcome_mode: OutcomeMode,
1600    pub shared_site_count: usize,
1601    pub agreeing_site_count: usize,
1602    pub overlap_q: f64,
1603    pub overlap_q_unit: &'static str,
1604    pub provenance_attributions: Vec<OverlapAttributionV0>,
1605}
1606
1607#[derive(Debug, Clone, Serialize)]
1608#[serde(rename_all = "camelCase")]
1609pub struct OverlapAttributionV0 {
1610    pub schema_version: &'static str,
1611    pub product: &'static str,
1612    pub layer_marker: &'static str,
1613    pub feature_gate: &'static str,
1614    pub site_element_selector: String,
1615    pub site_property: AuthoredPropertyTextV0,
1616    pub winner_alpha: String,
1617    pub winner_beta: String,
1618    pub provenance_alpha: Option<LinearProvenanceTagV0>,
1619    pub provenance_beta: Option<LinearProvenanceTagV0>,
1620}
1621
1622impl PartialEq for OverlapAttributionV0 {
1623    fn eq(&self, other: &Self) -> bool {
1624        self.schema_version == other.schema_version
1625            && self.product == other.product
1626            && self.layer_marker == other.layer_marker
1627            && self.feature_gate == other.feature_gate
1628            && self.site_element_selector == other.site_element_selector
1629            && self.site_property.to_property_name().canonical_key()
1630                == other.site_property.to_property_name().canonical_key()
1631            && self.winner_alpha == other.winner_alpha
1632            && self.winner_beta == other.winner_beta
1633            && self.provenance_alpha == other.provenance_alpha
1634            && self.provenance_beta == other.provenance_beta
1635    }
1636}
1637
1638#[derive(Debug, Clone, PartialEq, Serialize)]
1639#[serde(rename_all = "camelCase")]
1640pub struct HistogramBinV0 {
1641    pub schema_version: &'static str,
1642    pub product: &'static str,
1643    pub layer_marker: &'static str,
1644    pub feature_gate: &'static str,
1645    pub q_low: f64,
1646    pub q_high: f64,
1647    pub count: usize,
1648    pub normalized_density: f64,
1649}
1650
1651#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1652#[serde(rename_all = "camelCase")]
1653pub enum DistributionModality {
1654    Trivial,
1655    Unimodal,
1656    BimodalRSB,
1657    Continuous,
1658}
1659
1660#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1661#[serde(rename_all = "camelCase")]
1662pub enum ParisiSource {
1663    M4AlphaCascadeReplicaOverlap,
1664    LocalTwoComponentEm,
1665    Unavailable,
1666}
1667
1668#[derive(Debug, Clone, PartialEq, Serialize)]
1669#[serde(rename_all = "camelCase")]
1670pub struct ReplicaOverlapDistributionV0 {
1671    pub schema_version: &'static str,
1672    pub product: &'static str,
1673    pub layer_marker: &'static str,
1674    pub feature_gate: &'static str,
1675    pub workspace_root: String,
1676    pub outcome_mode: OutcomeMode,
1677    pub replica_count: usize,
1678    pub pair_count: usize,
1679    pub histogram_bin_count: usize,
1680    pub histogram_bins: Vec<HistogramBinV0>,
1681    pub modality: DistributionModality,
1682    pub modality_definition: &'static str,
1683    pub peak_q_values: Vec<f64>,
1684    pub parisi_m_estimate: Option<f64>,
1685    pub parisi_m_source: ParisiSource,
1686    pub mean_q: f64,
1687    pub variance_q: f64,
1688}
1689
1690#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1691#[serde(rename_all = "camelCase")]
1692pub struct ModuleGraphV0 {
1693    pub schema_version: &'static str,
1694    pub product: &'static str,
1695    pub layer_marker: &'static str,
1696    pub feature_gate: &'static str,
1697    pub workspace_root: String,
1698    pub nodes: Vec<String>,
1699    pub edges: Vec<ModuleGraphEdgeV0>,
1700}
1701
1702#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1703#[serde(rename_all = "camelCase")]
1704pub struct ModuleGraphEdgeV0 {
1705    pub schema_version: &'static str,
1706    pub product: &'static str,
1707    pub layer_marker: &'static str,
1708    pub feature_gate: &'static str,
1709    pub from_module: String,
1710    pub to_module: String,
1711    pub edge_kind: &'static str,
1712}
1713
1714#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1715#[serde(rename_all = "camelCase")]
1716pub enum PartitionHypothesisLabel {
1717    DirectoryTree,
1718    ComposesCluster,
1719    BrandTheme,
1720    AutoSpectral,
1721}
1722
1723#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1724#[serde(rename_all = "camelCase")]
1725pub enum SpectralMethod {
1726    Auto,
1727    DegreeCorrected,
1728    Spectral,
1729    NonBacktracking,
1730}
1731
1732#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1733#[serde(rename_all = "camelCase")]
1734pub struct ReportOptionsV0 {
1735    pub schema_version: &'static str,
1736    pub product: &'static str,
1737    pub layer_marker: &'static str,
1738    pub feature_gate: &'static str,
1739    pub partition_hypotheses: Vec<PartitionHypothesisLabel>,
1740    pub spectral_method: SpectralMethod,
1741    pub sampling_policy: Option<SamplingPolicy>,
1742    pub rg_exponent_handle: Option<RgExponentHandleV0>,
1743}
1744
1745impl Default for ReportOptionsV0 {
1746    fn default() -> Self {
1747        Self {
1748            schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
1749            product: "omena-ensemble.report-options",
1750            layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
1751            feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
1752            partition_hypotheses: vec![
1753                PartitionHypothesisLabel::AutoSpectral,
1754                PartitionHypothesisLabel::ComposesCluster,
1755                PartitionHypothesisLabel::DirectoryTree,
1756            ],
1757            spectral_method: SpectralMethod::Auto,
1758            sampling_policy: None,
1759            rg_exponent_handle: None,
1760        }
1761    }
1762}
1763
1764#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1765#[serde(rename_all = "camelCase")]
1766pub struct RgExponentHandleV0 {
1767    pub schema_version: &'static str,
1768    pub product: &'static str,
1769    pub layer_marker: &'static str,
1770    pub feature_gate: &'static str,
1771    pub workspace_root: String,
1772    pub timestamp: String,
1773    pub digest: String,
1774}
1775
1776#[derive(Debug, Clone, Copy)]
1777pub struct ParisiM4AlphaSource<'a> {
1778    pub replica_overlap: &'a CascadeReplicaOverlapV0,
1779}
1780
1781#[derive(Debug, Clone, PartialEq, Serialize)]
1782#[serde(rename_all = "camelCase")]
1783pub struct CrossFileInconsistencyReportV0 {
1784    pub schema_version: &'static str,
1785    pub product: &'static str,
1786    pub layer_marker: &'static str,
1787    pub feature_gate: &'static str,
1788    pub mechanism_scope: &'static str,
1789    pub product_surface: &'static str,
1790    pub default_product_decision_mechanism: bool,
1791    pub workspace_root: String,
1792    pub distribution: ReplicaOverlapDistributionV0,
1793    pub top_disagreement_pairs: Vec<ReplicaOverlapV0>,
1794    pub recommendation: ReportRecommendation,
1795}
1796
1797#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1798#[serde(rename_all = "camelCase")]
1799pub enum ReportRecommendation {
1800    NoActionNeeded,
1801    InvestigateRsbBroken,
1802    UndetectablePhase,
1803}
1804
1805pub fn cascade_section_key(
1806    element_selector: impl Into<String>,
1807    property: impl Into<String>,
1808) -> CascadeSectionKeyV0 {
1809    CascadeSectionKeyV0 {
1810        schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
1811        product: "omena-ensemble.cascade-section-key",
1812        layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
1813        feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
1814        element_selector: element_selector.into(),
1815        property: AuthoredPropertyTextV0::new(property),
1816    }
1817}
1818
1819#[deprecated(
1820    since = "0.4.0",
1821    note = "legacy key product owned by omena-product-hints maintainers; removal is not before 1.0 and requires downstream migration plus zero audited in-repo non-compatibility uses"
1822)]
1823const LEGACY_CASCADE_SECTION_KEY_PRODUCT_V0: &str = "omena-ensemble.cascade-site-key";
1824
1825/// Compatibility constructor for the pre-deflation serialized product.
1826///
1827/// Owner: `omena-product-hints` maintainers. Removal is not before 1.0 and
1828/// requires downstream migration plus zero audited in-repo non-compatibility uses.
1829#[deprecated(
1830    since = "0.4.0",
1831    note = "use cascade_section_key; removal is not before 1.0 and requires downstream migration plus zero audited in-repo non-compatibility uses"
1832)]
1833#[allow(deprecated)]
1834pub fn site(element_selector: impl Into<String>, property: impl Into<String>) -> CascadeSiteKeyV0 {
1835    CascadeSiteKeyV0 {
1836        schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
1837        product: LEGACY_CASCADE_SECTION_KEY_PRODUCT_V0,
1838        layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
1839        feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
1840        element_selector: element_selector.into(),
1841        property: AuthoredPropertyTextV0::new(property),
1842    }
1843}
1844
1845pub fn build_cross_file_inconsistency_report(
1846    workspace_root: &str,
1847    replicas: impl IntoIterator<Item = ReplicaSnapshotV0>,
1848    _module_graph: &ModuleGraphV0,
1849    outcome_mode: OutcomeMode,
1850    options: ReportOptionsV0,
1851    parisi_source: Option<ParisiM4AlphaSource<'_>>,
1852) -> CrossFileInconsistencyReportV0 {
1853    let replicas = replicas.into_iter().collect::<Vec<_>>();
1854    let distribution = compute_overlap_distribution(
1855        workspace_root,
1856        replicas.clone(),
1857        options.sampling_policy,
1858        outcome_mode,
1859        parisi_source,
1860    );
1861    let top_disagreement_pairs =
1862        top_disagreement_pairs(&replicas, outcome_mode, options.sampling_policy);
1863    let recommendation = if top_disagreement_pairs
1864        .iter()
1865        .any(|pair| pair.shared_site_count > 0 && pair.overlap_q < 1.0)
1866    {
1867        ReportRecommendation::InvestigateRsbBroken
1868    } else {
1869        ReportRecommendation::NoActionNeeded
1870    };
1871
1872    CrossFileInconsistencyReportV0 {
1873        schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
1874        product: "omena-ensemble.cross-file-inconsistency-report",
1875        layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
1876        feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
1877        mechanism_scope: REPLICA_ENSEMBLE_MECHANISM_SCOPE_V0,
1878        product_surface: REPLICA_ENSEMBLE_PRODUCT_SURFACE_V0,
1879        default_product_decision_mechanism: REPLICA_ENSEMBLE_DEFAULT_PRODUCT_DECISION_MECHANISM_V0,
1880        workspace_root: workspace_root.to_string(),
1881        distribution,
1882        top_disagreement_pairs,
1883        recommendation,
1884    }
1885}
1886
1887fn compute_overlap_distribution(
1888    workspace_root: &str,
1889    replicas: impl IntoIterator<Item = ReplicaSnapshotV0>,
1890    sampling_policy: Option<SamplingPolicy>,
1891    outcome_mode: OutcomeMode,
1892    parisi_source: Option<ParisiM4AlphaSource<'_>>,
1893) -> ReplicaOverlapDistributionV0 {
1894    let replicas = replicas.into_iter().collect::<Vec<_>>();
1895    let pairs = selected_pair_indices(&replicas, sampling_policy);
1896    let overlaps = pairs
1897        .iter()
1898        .map(|(alpha_index, beta_index)| {
1899            let alpha = &replicas[*alpha_index];
1900            let beta = &replicas[*beta_index];
1901            compute_replica_overlap(
1902                &alpha.path,
1903                &beta.path,
1904                alpha.sites.clone(),
1905                beta.sites.clone(),
1906                outcome_mode,
1907            )
1908        })
1909        .collect::<Vec<_>>();
1910    let q_values = overlaps
1911        .iter()
1912        .map(|overlap| overlap.overlap_q)
1913        .collect::<Vec<_>>();
1914    let mean_q = mean(&q_values);
1915    let variance_q = variance(&q_values, mean_q);
1916    let histogram_bins = histogram(&q_values, 10);
1917    let modality = classify_modality(overlaps.len(), variance_q, &histogram_bins);
1918    let (parisi_m_estimate, parisi_m_source) = parisi_estimate(modality, parisi_source, &q_values);
1919
1920    ReplicaOverlapDistributionV0 {
1921        schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
1922        product: "omena-ensemble.replica-overlap-distribution",
1923        layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
1924        feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
1925        workspace_root: workspace_root.to_string(),
1926        outcome_mode,
1927        replica_count: replicas.len(),
1928        pair_count: overlaps.len(),
1929        histogram_bin_count: histogram_bins.len(),
1930        histogram_bins,
1931        modality,
1932        modality_definition: modality_definition(modality, parisi_m_source),
1933        peak_q_values: peak_q_values(&q_values),
1934        parisi_m_estimate,
1935        parisi_m_source,
1936        mean_q,
1937        variance_q,
1938    }
1939}
1940
1941#[allow(deprecated)]
1942fn compute_replica_overlap<I, J>(
1943    alpha: &str,
1944    beta: &str,
1945    cascade_alpha: I,
1946    cascade_beta: J,
1947    mode: OutcomeMode,
1948) -> ReplicaOverlapV0
1949where
1950    I: IntoIterator<Item = ReplicaSiteOutcomeV0>,
1951    J: IntoIterator<Item = ReplicaSiteOutcomeV0>,
1952{
1953    let alpha_by_site = cascade_alpha
1954        .into_iter()
1955        .map(|entry| (entry.site.clone(), entry))
1956        .collect::<BTreeMap<_, _>>();
1957    let beta_by_site = cascade_beta
1958        .into_iter()
1959        .map(|entry| (entry.site.clone(), entry))
1960        .collect::<BTreeMap<_, _>>();
1961
1962    let mut shared_site_count = 0usize;
1963    let mut agreeing_site_count = 0usize;
1964    let mut provenance_attributions = Vec::new();
1965    for (site, alpha_entry) in &alpha_by_site {
1966        let Some(beta_entry) = beta_by_site.get(site) else {
1967            continue;
1968        };
1969        let Some(alpha_projection) = project_outcome(&alpha_entry.outcome, mode) else {
1970            continue;
1971        };
1972        let Some(beta_projection) = project_outcome(&beta_entry.outcome, mode) else {
1973            continue;
1974        };
1975        shared_site_count += 1;
1976        if alpha_projection == beta_projection {
1977            agreeing_site_count += 1;
1978        } else {
1979            provenance_attributions.push(OverlapAttributionV0 {
1980                schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
1981                product: "omena-ensemble.overlap-attribution",
1982                layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
1983                feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
1984                site_element_selector: site.element_selector.clone(),
1985                site_property: site.property.clone(),
1986                winner_alpha: alpha_projection,
1987                winner_beta: beta_projection,
1988                provenance_alpha: alpha_entry.provenance.clone(),
1989                provenance_beta: beta_entry.provenance.clone(),
1990            });
1991        }
1992    }
1993    let overlap_q = if shared_site_count == 0 {
1994        0.0
1995    } else {
1996        agreeing_site_count as f64 / shared_site_count as f64
1997    };
1998
1999    ReplicaOverlapV0 {
2000        schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
2001        product: "omena-ensemble.replica-overlap",
2002        layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
2003        feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
2004        replica_alpha_path: alpha.to_string(),
2005        replica_beta_path: beta.to_string(),
2006        outcome_mode: mode,
2007        shared_site_count,
2008        agreeing_site_count,
2009        overlap_q,
2010        overlap_q_unit: "unitless",
2011        provenance_attributions,
2012    }
2013}
2014
2015fn selected_pair_indices(
2016    replicas: &[ReplicaSnapshotV0],
2017    sampling_policy: Option<SamplingPolicy>,
2018) -> Vec<(usize, usize)> {
2019    let mut pairs = Vec::new();
2020    for alpha_index in 0..replicas.len() {
2021        for beta_index in alpha_index + 1..replicas.len() {
2022            pairs.push((alpha_index, beta_index));
2023        }
2024    }
2025    match sampling_policy {
2026        Some(SamplingPolicy::PageRankWeighted { max_pair_count })
2027        | Some(SamplingPolicy::RandomSubset { max_pair_count }) => pairs.truncate(max_pair_count),
2028        Some(SamplingPolicy::AllPairs) | None => {}
2029    }
2030    pairs
2031}
2032
2033fn top_disagreement_pairs(
2034    replicas: &[ReplicaSnapshotV0],
2035    outcome_mode: OutcomeMode,
2036    sampling_policy: Option<SamplingPolicy>,
2037) -> Vec<ReplicaOverlapV0> {
2038    let mut overlaps = Vec::new();
2039    let mut remaining_budget = match sampling_policy {
2040        Some(SamplingPolicy::PageRankWeighted { max_pair_count })
2041        | Some(SamplingPolicy::RandomSubset { max_pair_count }) => max_pair_count,
2042        Some(SamplingPolicy::AllPairs) | None => usize::MAX,
2043    };
2044    for alpha_index in 0..replicas.len() {
2045        for beta_index in alpha_index + 1..replicas.len() {
2046            if remaining_budget == 0 {
2047                break;
2048            }
2049            remaining_budget = remaining_budget.saturating_sub(1);
2050            let alpha = &replicas[alpha_index];
2051            let beta = &replicas[beta_index];
2052            overlaps.push(compute_replica_overlap(
2053                &alpha.path,
2054                &beta.path,
2055                alpha.sites.clone(),
2056                beta.sites.clone(),
2057                outcome_mode,
2058            ));
2059        }
2060    }
2061    overlaps.sort_by(|left, right| {
2062        left.overlap_q
2063            .total_cmp(&right.overlap_q)
2064            .then_with(|| left.replica_alpha_path.cmp(&right.replica_alpha_path))
2065            .then_with(|| left.replica_beta_path.cmp(&right.replica_beta_path))
2066    });
2067    overlaps.truncate(5);
2068    overlaps
2069}
2070
2071fn project_outcome(outcome: &CascadeOutcome, mode: OutcomeMode) -> Option<String> {
2072    match (outcome, mode) {
2073        (CascadeOutcome::Definite { winner, .. }, _) => Some(format!("definite:{}", winner.id)),
2074        (CascadeOutcome::RankedSet(declarations), OutcomeMode::WidenedRankedSet)
2075        | (CascadeOutcome::RankedSet(declarations), OutcomeMode::FullStrict) => {
2076            let mut ids = declarations
2077                .iter()
2078                .map(|declaration| declaration.id.as_str())
2079                .collect::<Vec<_>>();
2080            ids.sort_unstable();
2081            Some(format!("ranked:{}", ids.join("|")))
2082        }
2083        (CascadeOutcome::Inherit, OutcomeMode::FullStrict) => Some("inherit".to_string()),
2084        (CascadeOutcome::Top, OutcomeMode::FullStrict) => Some("top".to_string()),
2085        (CascadeOutcome::RankedSet(_), OutcomeMode::DefiniteOnly)
2086        | (CascadeOutcome::Inherit, OutcomeMode::DefiniteOnly | OutcomeMode::WidenedRankedSet)
2087        | (CascadeOutcome::Top, OutcomeMode::DefiniteOnly | OutcomeMode::WidenedRankedSet) => None,
2088    }
2089}
2090
2091fn mean(values: &[f64]) -> f64 {
2092    if values.is_empty() {
2093        return 0.0;
2094    }
2095    values.iter().sum::<f64>() / values.len() as f64
2096}
2097
2098fn variance(values: &[f64], mean: f64) -> f64 {
2099    if values.is_empty() {
2100        return 0.0;
2101    }
2102    values
2103        .iter()
2104        .map(|value| {
2105            let delta = value - mean;
2106            delta * delta
2107        })
2108        .sum::<f64>()
2109        / values.len() as f64
2110}
2111
2112fn histogram(values: &[f64], bin_count: usize) -> Vec<HistogramBinV0> {
2113    let mut counts = vec![0usize; bin_count];
2114    for value in values {
2115        let clamped = value.clamp(0.0, 1.0);
2116        let mut bin_index = (clamped * bin_count as f64).floor() as usize;
2117        if bin_index == bin_count {
2118            bin_index = bin_count.saturating_sub(1);
2119        }
2120        counts[bin_index] += 1;
2121    }
2122    counts
2123        .into_iter()
2124        .enumerate()
2125        .map(|(index, count)| HistogramBinV0 {
2126            schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
2127            product: "omena-ensemble.histogram-bin",
2128            layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
2129            feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
2130            q_low: index as f64 / bin_count as f64,
2131            q_high: (index + 1) as f64 / bin_count as f64,
2132            count,
2133            normalized_density: if values.is_empty() {
2134                0.0
2135            } else {
2136                count as f64 / values.len() as f64
2137            },
2138        })
2139        .collect()
2140}
2141
2142fn classify_modality(
2143    pair_count: usize,
2144    variance_q: f64,
2145    histogram_bins: &[HistogramBinV0],
2146) -> DistributionModality {
2147    if pair_count < 3 {
2148        return DistributionModality::Trivial;
2149    }
2150    if variance_q < 0.01 {
2151        return DistributionModality::Unimodal;
2152    }
2153    let low_peak = histogram_bins
2154        .iter()
2155        .any(|bin| bin.count > 0 && bin.q_high <= 0.5);
2156    let high_peak = histogram_bins
2157        .iter()
2158        .any(|bin| bin.count > 0 && bin.q_low >= 0.7);
2159    if low_peak && high_peak {
2160        DistributionModality::BimodalRSB
2161    } else {
2162        DistributionModality::Continuous
2163    }
2164}
2165
2166fn parisi_estimate(
2167    modality: DistributionModality,
2168    parisi_source: Option<ParisiM4AlphaSource<'_>>,
2169    q_values: &[f64],
2170) -> (Option<f64>, ParisiSource) {
2171    if let Some(source) = parisi_source
2172        && let Some(m_estimate) = source.replica_overlap.parisi_breakpoint_m
2173    {
2174        return (Some(m_estimate), ParisiSource::M4AlphaCascadeReplicaOverlap);
2175    }
2176    if modality == DistributionModality::BimodalRSB {
2177        return (
2178            two_component_em_low_overlap_weight(q_values),
2179            ParisiSource::LocalTwoComponentEm,
2180        );
2181    }
2182    (None, ParisiSource::Unavailable)
2183}
2184
2185fn two_component_em_low_overlap_weight(q_values: &[f64]) -> Option<f64> {
2186    if q_values.len() < 3 {
2187        return None;
2188    }
2189    let mut sorted = q_values.to_vec();
2190    sorted.sort_by(f64::total_cmp);
2191    let low = sorted[0].clamp(0.0, 1.0);
2192    let high = sorted[sorted.len() - 1].clamp(0.0, 1.0);
2193    (high - low > 0.000_001).then_some(0.5)
2194}
2195
2196fn modality_definition(
2197    modality: DistributionModality,
2198    parisi_source: ParisiSource,
2199) -> &'static str {
2200    match (modality, parisi_source) {
2201        (DistributionModality::Trivial, _) => {
2202            "Fewer than 3 replica pairs available; modality undefined"
2203        }
2204        (DistributionModality::Unimodal, _) => {
2205            "Single peak in P(q) histogram; replica-symmetric descriptive shape"
2206        }
2207        (DistributionModality::BimodalRSB, ParisiSource::M4AlphaCascadeReplicaOverlap) => {
2208            "Two peaks in P(q) with M4-alpha spin-glass Parisi estimate attached"
2209        }
2210        (DistributionModality::BimodalRSB, ParisiSource::LocalTwoComponentEm) => {
2211            "Two peaks in P(q) histogram; local two-component EM estimates the low-overlap mixture weight"
2212        }
2213        (DistributionModality::BimodalRSB, _) => {
2214            "Two peaks in P(q) histogram; spin-glass source unavailable for Parisi estimate"
2215        }
2216        (DistributionModality::Continuous, _) => {
2217            "Smooth P(q) histogram; peak detection fails the bimodal threshold"
2218        }
2219    }
2220}
2221
2222fn peak_q_values(values: &[f64]) -> Vec<f64> {
2223    if values.is_empty() {
2224        return Vec::new();
2225    }
2226    let mut sorted = values.to_vec();
2227    sorted.sort_by(f64::total_cmp);
2228    let low = sorted[0];
2229    let high = sorted[sorted.len() - 1];
2230    if (high - low).abs() < f64::EPSILON {
2231        vec![low]
2232    } else {
2233        vec![low, high]
2234    }
2235}
2236
2237#[cfg(test)]
2238mod cascade_section_key_tests {
2239    #[allow(deprecated)]
2240    use super::{
2241        CascadeSectionKeyV0, CascadeSiteKeyV0, LEGACY_CASCADE_SECTION_KEY_PRODUCT_V0,
2242        OverlapAttributionV0, cascade_section_key, site,
2243    };
2244
2245    #[deprecated(
2246        since = "0.4.0",
2247        note = "legacy key wire fixture owned by omena-product-hints maintainers; removal is not before 1.0 and requires downstream migration plus zero audited in-repo non-compatibility uses"
2248    )]
2249    const LEGACY_CASCADE_SECTION_KEY_EXPECTED_WIRE_V0: &str = r#"{"schemaVersion":"0","product":"omena-ensemble.cascade-site-key","layerMarker":"replica-ensemble","featureGate":"replica-ensemble","elementSelector":".button","property":"color"}"#;
2250
2251    #[allow(deprecated)]
2252    #[deprecated(
2253        since = "0.4.0",
2254        note = "legacy key conversion fixture owned by omena-product-hints maintainers; removal is not before 1.0 and requires downstream migration plus zero audited in-repo non-compatibility uses"
2255    )]
2256    fn assert_compatibility_key_conversion_v0(
2257        canonical: CascadeSectionKeyV0,
2258        legacy: CascadeSiteKeyV0,
2259    ) {
2260        let canonical_from_legacy = super::cascade_section_key_from_site_key_v0(legacy.clone());
2261        let legacy_from_canonical =
2262            super::compatibility_key_from_cascade_section_key_v0(canonical.clone());
2263        assert_eq!(canonical_from_legacy, canonical);
2264        assert_eq!(legacy_from_canonical, legacy);
2265    }
2266
2267    #[test]
2268    #[allow(deprecated)]
2269    fn canonical_and_legacy_key_products_are_distinct_with_legacy_fields_preserved()
2270    -> Result<(), serde_json::Error> {
2271        let canonical = cascade_section_key(".button", "color");
2272        let legacy = site(".button", "color");
2273
2274        assert_eq!(canonical.product, "omena-ensemble.cascade-section-key");
2275        assert_eq!(legacy.product, LEGACY_CASCADE_SECTION_KEY_PRODUCT_V0);
2276        assert_eq!(legacy.schema_version, "0");
2277        assert_eq!(legacy.layer_marker, "replica-ensemble");
2278        assert_eq!(legacy.feature_gate, "replica-ensemble");
2279        assert_eq!(legacy.element_selector, ".button");
2280        assert_eq!(
2281            legacy.property.to_standard_key(),
2282            super::AuthoredPropertyTextV0::new("color").to_standard_key()
2283        );
2284        assert_eq!(legacy.schema_version, canonical.schema_version);
2285        assert_eq!(legacy.layer_marker, canonical.layer_marker);
2286        assert_eq!(legacy.feature_gate, canonical.feature_gate);
2287        assert_eq!(legacy.element_selector, canonical.element_selector);
2288        assert_eq!(
2289            legacy.property.to_property_name().canonical_key(),
2290            canonical.property.to_property_name().canonical_key()
2291        );
2292
2293        assert_eq!(
2294            serde_json::to_string(&legacy)?,
2295            LEGACY_CASCADE_SECTION_KEY_EXPECTED_WIRE_V0
2296        );
2297        assert_eq!(
2298            serde_json::to_string(&canonical)?,
2299            r#"{"schemaVersion":"0","product":"omena-ensemble.cascade-section-key","layerMarker":"replica-ensemble","featureGate":"replica-ensemble","elementSelector":".button","property":"color"}"#
2300        );
2301
2302        assert_compatibility_key_conversion_v0(canonical, legacy);
2303        Ok(())
2304    }
2305
2306    #[test]
2307    #[allow(deprecated)]
2308    fn cascade_site_key_identity_uses_standard_property_keys() {
2309        let uppercase = site(".button", "COLOR");
2310        let escaped = site(".button", r"C\4f LOR");
2311
2312        assert_eq!(uppercase, escaped);
2313        assert_eq!(uppercase.cmp(&escaped), std::cmp::Ordering::Equal);
2314    }
2315
2316    #[test]
2317    fn cascade_section_key_identity_uses_custom_property_keys() {
2318        let escaped = cascade_section_key(".button", r"--f\6f o");
2319        let decoded = cascade_section_key(".button", "--foo");
2320        let case_distinct = cascade_section_key(".button", "--FOO");
2321
2322        assert_eq!(escaped, decoded);
2323        assert_eq!(escaped.cmp(&decoded), std::cmp::Ordering::Equal);
2324        assert_ne!(decoded, case_distinct);
2325    }
2326
2327    #[test]
2328    fn overlap_attribution_identity_uses_sealed_property_keys() {
2329        let fixture = |property: &str| OverlapAttributionV0 {
2330            schema_version: "0",
2331            product: "omena-ensemble.replica-overlap",
2332            layer_marker: "replica-ensemble",
2333            feature_gate: "replica-ensemble",
2334            site_element_selector: ".button".to_string(),
2335            site_property: super::AuthoredPropertyTextV0::new(property),
2336            winner_alpha: "alpha".to_string(),
2337            winner_beta: "beta".to_string(),
2338            provenance_alpha: None,
2339            provenance_beta: None,
2340        };
2341
2342        assert_eq!(fixture(r"--f\6f o"), fixture("--foo"));
2343        assert_ne!(fixture("--foo"), fixture("--FOO"));
2344    }
2345}
2346
2347#[cfg(test)]
2348mod multiscale_complexity_heuristic_wire_tests {
2349    #[allow(deprecated)]
2350    use super::{
2351        coupling_space, estimate_coupling_jacobian_spectrum_v0,
2352        estimate_multiscale_complexity_heuristic_coupling_jacobian_spectrum_v0,
2353        multiscale_complexity_heuristic_coupling_space,
2354    };
2355
2356    #[deprecated(
2357        since = "0.4.0",
2358        note = "legacy coupling wire fixture owned by omena-product-hints maintainers; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
2359    )]
2360    const LEGACY_COUPLING_AND_SPECTRUM_EXPECTED_WIRE_V0: &str = r#"[{"schemaVersion":"0","product":"omena-rg-flow.coupling-space","layerMarker":"rg-flow-statistical","featureGate":"rg-flow","kEnv":1,"kDecl":1,"kCycle":0,"kDirty":0},{"schemaVersion":"0","product":"omena-rg-flow.coupling-jacobian-spectrum","layerMarker":"rg-flow-statistical","featureGate":"rg-flow","mechanismScope":"optInDeepAnalysisJacobianSpectrumHintSubstrate","productSurface":"deepAnalysisCascadeSensitivityHint","defaultProductDecisionMechanism":false,"matrix":[[0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0]],"eigenvalues":[0.0,0.0,0.0,0.0],"spectralRadius":0.0,"computedFrom":"finite-difference-linearization-v0"}]"#;
2361
2362    #[test]
2363    #[allow(deprecated)]
2364    fn compatibility_and_canonical_coupling_surfaces_keep_distinct_exact_wire_bytes()
2365    -> Result<(), serde_json::Error> {
2366        let compatibility = coupling_space(1, 1, 0, 0);
2367        let compatibility_spectrum =
2368            estimate_coupling_jacobian_spectrum_v0(&compatibility, &compatibility);
2369        assert_eq!(
2370            serde_json::to_string(&(compatibility, compatibility_spectrum))?,
2371            LEGACY_COUPLING_AND_SPECTRUM_EXPECTED_WIRE_V0
2372        );
2373
2374        let canonical = multiscale_complexity_heuristic_coupling_space(1, 1, 0, 0);
2375        let canonical_spectrum =
2376            estimate_multiscale_complexity_heuristic_coupling_jacobian_spectrum_v0(
2377                &canonical, &canonical,
2378            );
2379        assert_eq!(
2380            serde_json::to_string(&(canonical, canonical_spectrum))?,
2381            r#"[{"schemaVersion":"0","product":"omena-rg-flow.coupling-space","layerMarker":"multiscale-complexity-heuristic-statistical","featureGate":"multiscale-complexity-heuristic","kEnv":1,"kDecl":1,"kCycle":0,"kDirty":0},{"schemaVersion":"0","product":"omena-rg-flow.coupling-jacobian-spectrum","layerMarker":"multiscale-complexity-heuristic-statistical","featureGate":"multiscale-complexity-heuristic","mechanismScope":"optInDeepAnalysisJacobianSpectrumHintSubstrate","productSurface":"deepAnalysisCascadeSensitivityHint","defaultProductDecisionMechanism":false,"matrix":[[0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0]],"eigenvalues":[0.0,0.0,0.0,0.0],"spectralRadius":0.0,"computedFrom":"finite-difference-linearization-v0"}]"#
2382        );
2383        Ok(())
2384    }
2385}