Skip to main content

omena_variational/
lib.rs

1//! Variational cascade inference contracts.
2//!
3//! Stochastic evidence is isolated in this crate and disabled by default. V0
4//! contracts serialize log quantities in bits; any nats arithmetic stays behind
5//! the public boundary.
6//!
7//! claim_level: product-wired posterior inference for checker evidence, not a
8//! corpus-calibrated probabilistic design model.
9
10pub mod hover;
11pub mod unit;
12
13use omena_abstract_value::AbstractClassValueProvenanceV0;
14use serde::Serialize;
15
16pub const VARIATIONAL_SCHEMA_VERSION_V0: &str = "0";
17pub const VARIATIONAL_LAYER_MARKER_V0: &str = "variational-cascade";
18pub const VARIATIONAL_FEATURE_GATE_V0: &str = "variational";
19const DESIGNER_INTENT_BP_MAX_ITERATIONS_V0: usize = 12;
20const DESIGNER_INTENT_BP_CONVERGENCE_EPSILON_BITS_V0: f64 = 0.005;
21const DESIGNER_INTENT_BP_DAMPING_V0: f64 = 0.35;
22const DESIGNER_INTENT_BP_FEEDBACK_WEIGHT_V0: f64 = 0.08;
23const DESIGNER_INTENT_BP_MAX_FEEDBACK_BITS_V0: f64 = 0.35;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
26#[serde(rename_all = "camelCase")]
27pub enum DesignerIntentPosteriorModeV0 {
28    VciFormal,
29    PcnHierarchical,
30    Fallback,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
34#[serde(rename_all = "camelCase")]
35pub enum PatternIntentV0 {
36    Bem,
37    Utility,
38    Atomic,
39    Hybrid,
40    AdHoc,
41}
42
43impl PatternIntentV0 {
44    pub const fn as_str(self) -> &'static str {
45        match self {
46            Self::Bem => "bem",
47            Self::Utility => "utility",
48            Self::Atomic => "atomic",
49            Self::Hybrid => "hybrid",
50            Self::AdHoc => "adHoc",
51        }
52    }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
56#[serde(rename_all = "camelCase")]
57pub enum PatternPriorKindV0 {
58    UniformDirichlet,
59    CorpusCalibrated,
60    Bespoke,
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize)]
64#[serde(rename_all = "camelCase")]
65pub struct DesignerIntentPosteriorV0 {
66    pub schema_version: &'static str,
67    pub product: &'static str,
68    pub layer_marker: &'static str,
69    pub feature_gate: &'static str,
70    pub mode: DesignerIntentPosteriorModeV0,
71    pub selector_name: String,
72    pub scores: Vec<DesignerIntentScoreV0>,
73    pub enabled_by_default: bool,
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize)]
77#[serde(rename_all = "camelCase")]
78pub struct DesignerIntentScoreV0 {
79    pub schema_version: &'static str,
80    pub product: &'static str,
81    pub layer_marker: &'static str,
82    pub feature_gate: &'static str,
83    pub intent: PatternIntentV0,
84    pub log_probability_bits: f64,
85}
86
87#[derive(Debug, Clone, PartialEq, Serialize)]
88#[serde(rename_all = "camelCase")]
89pub struct VariationalFreeEnergyV0 {
90    pub schema_version: &'static str,
91    pub product: &'static str,
92    pub layer_marker: &'static str,
93    pub feature_gate: &'static str,
94    pub complexity_bits: f64,
95    pub accuracy_bits: f64,
96    pub free_energy_bits: f64,
97    pub public_framing: &'static str,
98}
99
100#[derive(Debug, Clone, PartialEq, Serialize)]
101#[serde(rename_all = "camelCase")]
102pub struct PatternPriorV0 {
103    pub schema_version: &'static str,
104    pub product: &'static str,
105    pub layer_marker: &'static str,
106    pub feature_gate: &'static str,
107    pub kind: PatternPriorKindV0,
108    pub prior_kind: &'static str,
109    pub dirichlet_alpha: Vec<PatternPriorAlphaV0>,
110    pub concentration_bits: f64,
111    pub corpus_calibration: Option<PatternPriorCalibrationV0>,
112    pub rg_universality_class: Option<RgUniversalityClassRefV0>,
113}
114
115#[derive(Debug, Clone, PartialEq, Serialize)]
116#[serde(rename_all = "camelCase")]
117pub struct PatternPriorAlphaV0 {
118    pub schema_version: &'static str,
119    pub product: &'static str,
120    pub layer_marker: &'static str,
121    pub feature_gate: &'static str,
122    pub intent: PatternIntentV0,
123    pub alpha_bits: f64,
124}
125
126#[derive(Debug, Clone, PartialEq, Serialize)]
127#[serde(rename_all = "camelCase")]
128pub struct PatternPriorCalibrationV0 {
129    pub schema_version: &'static str,
130    pub product: &'static str,
131    pub layer_marker: &'static str,
132    pub feature_gate: &'static str,
133    pub corpus_fingerprint: String,
134    pub calibration_scope: &'static str,
135    pub axis_a_schema_version: &'static str,
136    pub fixture_count: usize,
137    pub generated_at_epoch: u64,
138    pub human_review_gate_passed: bool,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
142#[serde(rename_all = "camelCase")]
143pub struct RgUniversalityClassRefV0 {
144    pub schema_version: &'static str,
145    pub product: &'static str,
146    pub layer_marker: &'static str,
147    pub feature_gate: &'static str,
148    pub class_label: &'static str,
149}
150
151#[derive(Debug, Clone, PartialEq, Serialize)]
152#[serde(rename_all = "camelCase")]
153pub struct EmissionLikelihoodV0 {
154    pub schema_version: &'static str,
155    pub product: &'static str,
156    pub layer_marker: &'static str,
157    pub feature_gate: &'static str,
158    pub selector_name: String,
159    pub factor_count: usize,
160    pub factors: Vec<EmissionLikelihoodFactorV0>,
161    pub log_likelihood_bits: f64,
162}
163
164#[derive(Debug, Clone, PartialEq, Serialize)]
165#[serde(rename_all = "camelCase")]
166pub struct EmissionLikelihoodFactorV0 {
167    pub schema_version: &'static str,
168    pub product: &'static str,
169    pub layer_marker: &'static str,
170    pub feature_gate: &'static str,
171    pub source: &'static str,
172    pub factor_name: &'static str,
173    pub contribution_bits: f64,
174    pub log_likelihood_bits: f64,
175    pub reason: Option<&'static str>,
176}
177
178#[derive(Debug, Clone, PartialEq, Serialize)]
179#[serde(rename_all = "camelCase")]
180pub struct ProvenancePosteriorAnnotationV0 {
181    pub schema_version: &'static str,
182    pub product: &'static str,
183    pub layer_marker: &'static str,
184    pub feature_gate: &'static str,
185    pub node_count: usize,
186    pub annotations: Vec<ProvenancePosteriorNodeV0>,
187    pub provenance: Option<AbstractClassValueProvenanceV0>,
188    pub annotation_id: String,
189    pub mutates_existing_provenance_enum: bool,
190}
191
192#[derive(Debug, Clone, PartialEq, Serialize)]
193#[serde(rename_all = "camelCase")]
194pub struct ProvenancePosteriorNodeV0 {
195    pub schema_version: &'static str,
196    pub product: &'static str,
197    pub layer_marker: &'static str,
198    pub feature_gate: &'static str,
199    pub provenance: AbstractClassValueProvenanceV0,
200    pub posterior_logit_bits: f64,
201    pub likelihood_factor_bits: f64,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
205#[serde(rename_all = "camelCase")]
206pub struct DesignerIntentPosteriorInputV0 {
207    pub schema_version: &'static str,
208    pub product: &'static str,
209    pub layer_marker: &'static str,
210    pub feature_gate: &'static str,
211    pub selector_name: String,
212    pub declaration_count: usize,
213    pub duplicate_property_tie_count: usize,
214    pub custom_property_reference_count: usize,
215}
216
217pub fn designer_intent_posterior_input_v0(
218    selector_name: impl Into<String>,
219    declaration_count: usize,
220    duplicate_property_tie_count: usize,
221    custom_property_reference_count: usize,
222) -> DesignerIntentPosteriorInputV0 {
223    DesignerIntentPosteriorInputV0 {
224        schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
225        product: "omena-variational.designer-intent-posterior-input",
226        layer_marker: VARIATIONAL_LAYER_MARKER_V0,
227        feature_gate: VARIATIONAL_FEATURE_GATE_V0,
228        selector_name: selector_name.into(),
229        declaration_count,
230        duplicate_property_tie_count,
231        custom_property_reference_count,
232    }
233}
234
235#[derive(Debug, Clone, PartialEq, Serialize)]
236#[serde(rename_all = "camelCase")]
237pub struct DesignerIntentBeliefPropagationTraceV0 {
238    pub schema_version: &'static str,
239    pub product: &'static str,
240    pub layer_marker: &'static str,
241    pub feature_gate: &'static str,
242    pub selector_name: String,
243    pub factor_count: usize,
244    pub iteration_count: usize,
245    pub converged: bool,
246    pub max_delta_bits: f64,
247    pub free_energy_delta_bits: f64,
248    pub free_energy: VariationalFreeEnergyV0,
249    pub message_count: usize,
250    pub messages: Vec<DesignerIntentBeliefPropagationMessageV0>,
251    pub posterior_scores: Vec<DesignerIntentScoreV0>,
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
255#[serde(rename_all = "camelCase")]
256pub enum DesignerIntentMessageDirectionV0 {
257    IntentToFactor,
258    FactorToIntent,
259}
260
261#[derive(Debug, Clone, PartialEq, Serialize)]
262#[serde(rename_all = "camelCase")]
263pub struct DesignerIntentBeliefPropagationMessageV0 {
264    pub schema_version: &'static str,
265    pub product: &'static str,
266    pub layer_marker: &'static str,
267    pub feature_gate: &'static str,
268    pub iteration_index: usize,
269    pub direction: DesignerIntentMessageDirectionV0,
270    pub source_factor: &'static str,
271    pub target_intent: PatternIntentV0,
272    pub message_bits: f64,
273}
274
275pub fn summarize_variational_default_posterior_v0(
276    selector_name: impl Into<String>,
277) -> DesignerIntentPosteriorV0 {
278    DesignerIntentPosteriorV0 {
279        schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
280        product: "omena-variational.designer-intent-posterior",
281        layer_marker: VARIATIONAL_LAYER_MARKER_V0,
282        feature_gate: VARIATIONAL_FEATURE_GATE_V0,
283        mode: DesignerIntentPosteriorModeV0::Fallback,
284        selector_name: selector_name.into(),
285        scores: vec![DesignerIntentScoreV0 {
286            schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
287            product: "omena-variational.designer-intent-score",
288            layer_marker: VARIATIONAL_LAYER_MARKER_V0,
289            feature_gate: VARIATIONAL_FEATURE_GATE_V0,
290            intent: PatternIntentV0::AdHoc,
291            log_probability_bits: 0.0,
292        }],
293        enabled_by_default: false,
294    }
295}
296
297pub fn infer_designer_intent_posterior_v0(
298    input: DesignerIntentPosteriorInputV0,
299) -> DesignerIntentPosteriorV0 {
300    let trace = designer_intent_belief_propagation_trace_v0(&input);
301
302    DesignerIntentPosteriorV0 {
303        schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
304        product: "omena-variational.designer-intent-posterior",
305        layer_marker: VARIATIONAL_LAYER_MARKER_V0,
306        feature_gate: VARIATIONAL_FEATURE_GATE_V0,
307        mode: if trace.converged {
308            DesignerIntentPosteriorModeV0::VciFormal
309        } else {
310            DesignerIntentPosteriorModeV0::PcnHierarchical
311        },
312        selector_name: input.selector_name,
313        scores: trace.posterior_scores,
314        enabled_by_default: true,
315    }
316}
317
318pub fn designer_intent_belief_propagation_trace_v0(
319    input: &DesignerIntentPosteriorInputV0,
320) -> DesignerIntentBeliefPropagationTraceV0 {
321    let selector = normalize_selector_name_for_intent_v0(&input.selector_name);
322    let factors = designer_intent_evidence_factors_v0(&selector, input);
323    let intents = [
324        PatternIntentV0::Bem,
325        PatternIntentV0::Utility,
326        PatternIntentV0::Atomic,
327        PatternIntentV0::Hybrid,
328        PatternIntentV0::AdHoc,
329    ];
330    let prior_log_probability_bits = -(intents.len() as f64).log2();
331    let mut factor_to_intent_messages = vec![vec![0.0; intents.len()]; factors.len()];
332    let mut posterior_log_probability_bits = vec![prior_log_probability_bits; intents.len()];
333    let mut messages = Vec::new();
334    let mut iteration_count = 0;
335    let mut converged = false;
336    let mut max_delta_bits = f64::INFINITY;
337    let mut free_energy_delta_bits = f64::INFINITY;
338    let mut free_energy = variational_free_energy_from_beliefs_v0(
339        &posterior_log_probability_bits,
340        prior_log_probability_bits,
341        &factor_to_intent_messages,
342    );
343
344    for iteration_index in 0..DESIGNER_INTENT_BP_MAX_ITERATIONS_V0 {
345        iteration_count = iteration_index + 1;
346        let mut intent_to_factor_messages = vec![vec![0.0; intents.len()]; factors.len()];
347
348        for (factor_index, factor) in factors.iter().enumerate() {
349            let mut raw_intent_messages = Vec::with_capacity(intents.len());
350            for intent_index in 0..intents.len() {
351                let incoming_from_other_factors = factor_to_intent_messages
352                    .iter()
353                    .enumerate()
354                    .filter(|(candidate_index, _)| *candidate_index != factor_index)
355                    .map(|(_, factor_messages)| factor_messages[intent_index])
356                    .sum::<f64>();
357                raw_intent_messages.push(prior_log_probability_bits + incoming_from_other_factors);
358            }
359            let normalization_bits = log2_sum_exp_v0(&raw_intent_messages);
360            for (intent_index, intent) in intents.iter().enumerate() {
361                let message_bits = raw_intent_messages[intent_index] - normalization_bits;
362                intent_to_factor_messages[factor_index][intent_index] = message_bits;
363                messages.push(DesignerIntentBeliefPropagationMessageV0 {
364                    schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
365                    product: "omena-variational.designer-intent-bp-message",
366                    layer_marker: VARIATIONAL_LAYER_MARKER_V0,
367                    feature_gate: VARIATIONAL_FEATURE_GATE_V0,
368                    iteration_index,
369                    direction: DesignerIntentMessageDirectionV0::IntentToFactor,
370                    source_factor: factor.source_factor,
371                    target_intent: *intent,
372                    message_bits,
373                });
374            }
375        }
376
377        let mut next_factor_to_intent_messages = factor_to_intent_messages.clone();
378        for (factor_index, factor) in factors.iter().enumerate() {
379            for (intent_index, intent) in intents.iter().enumerate() {
380                let evidence_bits = factor.message_bits_for(*intent);
381                let feedback_bits = (intent_to_factor_messages[factor_index][intent_index]
382                    - prior_log_probability_bits)
383                    .clamp(
384                        -DESIGNER_INTENT_BP_MAX_FEEDBACK_BITS_V0,
385                        DESIGNER_INTENT_BP_MAX_FEEDBACK_BITS_V0,
386                    )
387                    * DESIGNER_INTENT_BP_FEEDBACK_WEIGHT_V0;
388                let target_message_bits = evidence_bits + feedback_bits;
389                let message_bits = DESIGNER_INTENT_BP_DAMPING_V0
390                    * factor_to_intent_messages[factor_index][intent_index]
391                    + (1.0 - DESIGNER_INTENT_BP_DAMPING_V0) * target_message_bits;
392                next_factor_to_intent_messages[factor_index][intent_index] = message_bits;
393                messages.push(DesignerIntentBeliefPropagationMessageV0 {
394                    schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
395                    product: "omena-variational.designer-intent-bp-message",
396                    layer_marker: VARIATIONAL_LAYER_MARKER_V0,
397                    feature_gate: VARIATIONAL_FEATURE_GATE_V0,
398                    iteration_index,
399                    direction: DesignerIntentMessageDirectionV0::FactorToIntent,
400                    source_factor: factor.source_factor,
401                    target_intent: *intent,
402                    message_bits,
403                });
404            }
405        }
406
407        let next_posterior_log_probability_bits = posterior_log_probability_bits_from_messages_v0(
408            prior_log_probability_bits,
409            &next_factor_to_intent_messages,
410        );
411        max_delta_bits = max_abs_delta_bits_v0(
412            &posterior_log_probability_bits,
413            &next_posterior_log_probability_bits,
414        );
415        let next_free_energy = variational_free_energy_from_beliefs_v0(
416            &next_posterior_log_probability_bits,
417            prior_log_probability_bits,
418            &next_factor_to_intent_messages,
419        );
420        free_energy_delta_bits =
421            (free_energy.free_energy_bits - next_free_energy.free_energy_bits).abs();
422
423        posterior_log_probability_bits = next_posterior_log_probability_bits;
424        factor_to_intent_messages = next_factor_to_intent_messages;
425        free_energy = next_free_energy;
426
427        if max_delta_bits <= DESIGNER_INTENT_BP_CONVERGENCE_EPSILON_BITS_V0
428            && free_energy_delta_bits <= DESIGNER_INTENT_BP_CONVERGENCE_EPSILON_BITS_V0
429        {
430            converged = true;
431            break;
432        }
433    }
434
435    let posterior_scores =
436        designer_intent_scores_from_log_probabilities_v0(&intents, &posterior_log_probability_bits);
437
438    DesignerIntentBeliefPropagationTraceV0 {
439        schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
440        product: "omena-variational.designer-intent-belief-propagation",
441        layer_marker: VARIATIONAL_LAYER_MARKER_V0,
442        feature_gate: VARIATIONAL_FEATURE_GATE_V0,
443        selector_name: input.selector_name.clone(),
444        factor_count: factors.len(),
445        iteration_count,
446        converged,
447        max_delta_bits,
448        free_energy_delta_bits,
449        free_energy,
450        message_count: messages.len(),
451        messages,
452        posterior_scores,
453    }
454}
455
456pub fn dominant_designer_intent_v0(
457    posterior: &DesignerIntentPosteriorV0,
458) -> Option<PatternIntentV0> {
459    posterior.scores.first().map(|score| score.intent)
460}
461
462pub fn uniform_pattern_prior_v0(corpus_fingerprint: impl Into<String>) -> PatternPriorV0 {
463    PatternPriorV0 {
464        schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
465        product: "omena-variational.pattern-prior",
466        layer_marker: VARIATIONAL_LAYER_MARKER_V0,
467        feature_gate: VARIATIONAL_FEATURE_GATE_V0,
468        kind: PatternPriorKindV0::UniformDirichlet,
469        prior_kind: "uniformDirichlet",
470        dirichlet_alpha: [
471            PatternIntentV0::Bem,
472            PatternIntentV0::Utility,
473            PatternIntentV0::Atomic,
474            PatternIntentV0::Hybrid,
475            PatternIntentV0::AdHoc,
476        ]
477        .into_iter()
478        .map(|intent| PatternPriorAlphaV0 {
479            schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
480            product: "omena-variational.pattern-prior-alpha",
481            layer_marker: VARIATIONAL_LAYER_MARKER_V0,
482            feature_gate: VARIATIONAL_FEATURE_GATE_V0,
483            intent,
484            alpha_bits: 1.0,
485        })
486        .collect(),
487        concentration_bits: 5.0,
488        corpus_calibration: Some(PatternPriorCalibrationV0 {
489            schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
490            product: "omena-variational.pattern-prior-calibration",
491            layer_marker: VARIATIONAL_LAYER_MARKER_V0,
492            feature_gate: VARIATIONAL_FEATURE_GATE_V0,
493            corpus_fingerprint: corpus_fingerprint.into(),
494            calibration_scope: "fixtureUniformNoCorpusCalibration",
495            axis_a_schema_version: "0",
496            fixture_count: 0,
497            generated_at_epoch: 0,
498            human_review_gate_passed: false,
499        }),
500        rg_universality_class: None,
501    }
502}
503
504fn normalize_selector_name_for_intent_v0(selector_name: &str) -> String {
505    selector_name
506        .trim()
507        .trim_start_matches('.')
508        .split([':', '[', ' ', '>', '+', '~', ','])
509        .next()
510        .unwrap_or(selector_name)
511        .trim()
512        .to_string()
513}
514
515fn bool_bits_v0(value: bool) -> f64 {
516    if value { 1.0 } else { 0.0 }
517}
518
519struct DesignerIntentEvidenceFactorV0 {
520    source_factor: &'static str,
521    contributions: Vec<(PatternIntentV0, f64)>,
522}
523
524impl DesignerIntentEvidenceFactorV0 {
525    fn message_bits_for(&self, intent: PatternIntentV0) -> f64 {
526        self.contributions
527            .iter()
528            .find_map(|(candidate, bits)| (*candidate == intent).then_some(*bits))
529            .unwrap_or(0.0)
530    }
531}
532
533fn designer_intent_evidence_factors_v0(
534    selector: &str,
535    input: &DesignerIntentPosteriorInputV0,
536) -> Vec<DesignerIntentEvidenceFactorV0> {
537    let has_bem_marker = selector.contains("__") || selector.contains("--");
538    let looks_utility = selector.starts_with("u-")
539        || selector.starts_with("is-")
540        || selector.starts_with("has-")
541        || selector
542            .split('-')
543            .any(|part| matches!(part, "m" | "p" | "mt" | "mb" | "ml" | "mr" | "bg" | "text"));
544    let looks_atomic = input.declaration_count <= 1 && selector.len() <= 8;
545    let looks_hybrid = selector.matches('-').count() >= 3
546        || (has_bem_marker && input.custom_property_reference_count > 0);
547
548    vec![
549        DesignerIntentEvidenceFactorV0 {
550            source_factor: "selector-bem-marker",
551            contributions: vec![
552                (PatternIntentV0::Bem, bool_bits_v0(has_bem_marker) * 7.0),
553                (
554                    PatternIntentV0::Utility,
555                    -bool_bits_v0(has_bem_marker) * 2.0,
556                ),
557                (PatternIntentV0::Hybrid, bool_bits_v0(has_bem_marker) * 1.0),
558                (
559                    PatternIntentV0::AdHoc,
560                    bool_bits_v0(!has_bem_marker && !looks_utility) * 1.0,
561                ),
562            ],
563        },
564        DesignerIntentEvidenceFactorV0 {
565            source_factor: "selector-utility-marker",
566            contributions: vec![
567                (PatternIntentV0::Utility, bool_bits_v0(looks_utility) * 6.5),
568                (
569                    PatternIntentV0::AdHoc,
570                    bool_bits_v0(!has_bem_marker && !looks_utility) * 1.0,
571                ),
572            ],
573        },
574        DesignerIntentEvidenceFactorV0 {
575            source_factor: "declaration-cardinality",
576            contributions: vec![
577                (
578                    PatternIntentV0::Bem,
579                    bool_bits_v0(input.declaration_count > 1) * 1.0,
580                ),
581                (
582                    PatternIntentV0::Utility,
583                    bool_bits_v0(input.declaration_count <= 2) * 1.0,
584                ),
585                (
586                    PatternIntentV0::Atomic,
587                    bool_bits_v0(looks_atomic) * 5.0
588                        - bool_bits_v0(input.declaration_count > 1) * 2.0,
589                ),
590            ],
591        },
592        DesignerIntentEvidenceFactorV0 {
593            source_factor: "source-order-tie",
594            contributions: vec![
595                (
596                    PatternIntentV0::Bem,
597                    -bool_bits_v0(input.duplicate_property_tie_count > 0) * 1.0,
598                ),
599                (
600                    PatternIntentV0::AdHoc,
601                    bool_bits_v0(input.duplicate_property_tie_count > 0) * 1.0,
602                ),
603            ],
604        },
605        DesignerIntentEvidenceFactorV0 {
606            source_factor: "custom-property-context",
607            contributions: vec![(
608                PatternIntentV0::Hybrid,
609                bool_bits_v0(looks_hybrid) * 4.0
610                    + bool_bits_v0(input.custom_property_reference_count > 0) * 1.5,
611            )],
612        },
613    ]
614}
615
616fn log2_sum_exp_v0(logits: &[f64]) -> f64 {
617    let max_logit = logits
618        .iter()
619        .copied()
620        .fold(f64::NEG_INFINITY, |left, right| left.max(right));
621    max_logit
622        + logits
623            .iter()
624            .map(|logit| 2_f64.powf(*logit - max_logit))
625            .sum::<f64>()
626            .log2()
627}
628
629fn posterior_log_probability_bits_from_messages_v0(
630    prior_log_probability_bits: f64,
631    factor_to_intent_messages: &[Vec<f64>],
632) -> Vec<f64> {
633    let intent_count = factor_to_intent_messages
634        .first()
635        .map(Vec::len)
636        .unwrap_or_default();
637    let logits = (0..intent_count)
638        .map(|intent_index| {
639            prior_log_probability_bits
640                + factor_to_intent_messages
641                    .iter()
642                    .map(|factor_messages| factor_messages[intent_index])
643                    .sum::<f64>()
644        })
645        .collect::<Vec<_>>();
646    let normalization_bits = log2_sum_exp_v0(&logits);
647    logits
648        .into_iter()
649        .map(|logit| logit - normalization_bits)
650        .collect()
651}
652
653fn designer_intent_scores_from_log_probabilities_v0(
654    intents: &[PatternIntentV0],
655    log_probability_bits: &[f64],
656) -> Vec<DesignerIntentScoreV0> {
657    let mut scores = intents
658        .iter()
659        .zip(log_probability_bits.iter())
660        .map(|(intent, log_probability_bits)| DesignerIntentScoreV0 {
661            schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
662            product: "omena-variational.designer-intent-score",
663            layer_marker: VARIATIONAL_LAYER_MARKER_V0,
664            feature_gate: VARIATIONAL_FEATURE_GATE_V0,
665            intent: *intent,
666            log_probability_bits: *log_probability_bits,
667        })
668        .collect::<Vec<_>>();
669    scores.sort_by(|left, right| {
670        right
671            .log_probability_bits
672            .partial_cmp(&left.log_probability_bits)
673            .unwrap_or(std::cmp::Ordering::Equal)
674            .then_with(|| left.intent.as_str().cmp(right.intent.as_str()))
675    });
676    scores
677}
678
679fn max_abs_delta_bits_v0(left: &[f64], right: &[f64]) -> f64 {
680    left.iter()
681        .zip(right.iter())
682        .map(|(left, right)| (left - right).abs())
683        .fold(0.0, f64::max)
684}
685
686fn variational_free_energy_from_beliefs_v0(
687    posterior_log_probability_bits: &[f64],
688    prior_log_probability_bits: f64,
689    factor_to_intent_messages: &[Vec<f64>],
690) -> VariationalFreeEnergyV0 {
691    let mut complexity_bits = 0.0;
692    let mut accuracy_bits = 0.0;
693    for (intent_index, posterior_log_probability_bits) in
694        posterior_log_probability_bits.iter().enumerate()
695    {
696        let probability = 2_f64.powf(*posterior_log_probability_bits);
697        complexity_bits +=
698            probability * (posterior_log_probability_bits - prior_log_probability_bits);
699        accuracy_bits += probability
700            * factor_to_intent_messages
701                .iter()
702                .map(|factor_messages| factor_messages[intent_index])
703                .sum::<f64>();
704    }
705
706    VariationalFreeEnergyV0 {
707        schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
708        product: "omena-variational.free-energy",
709        layer_marker: VARIATIONAL_LAYER_MARKER_V0,
710        feature_gate: VARIATIONAL_FEATURE_GATE_V0,
711        complexity_bits,
712        accuracy_bits,
713        free_energy_bits: complexity_bits - accuracy_bits,
714        public_framing: "V0 mean-field free-energy over fixture-uniform prior",
715    }
716}
717
718pub fn variational_free_energy_v0(
719    complexity_bits: f64,
720    accuracy_bits: f64,
721) -> VariationalFreeEnergyV0 {
722    VariationalFreeEnergyV0 {
723        schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
724        product: "omena-variational.free-energy",
725        layer_marker: VARIATIONAL_LAYER_MARKER_V0,
726        feature_gate: VARIATIONAL_FEATURE_GATE_V0,
727        complexity_bits,
728        accuracy_bits,
729        free_energy_bits: complexity_bits - accuracy_bits,
730        public_framing: "V0 mean-field free-energy over fixture-uniform prior",
731    }
732}
733
734pub fn emission_likelihood_v0(
735    selector_name: impl Into<String>,
736    factors: Vec<EmissionLikelihoodFactorV0>,
737) -> EmissionLikelihoodV0 {
738    let log_likelihood_bits = factors
739        .iter()
740        .map(|factor| factor.contribution_bits)
741        .sum::<f64>();
742    EmissionLikelihoodV0 {
743        schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
744        product: "omena-variational.emission-likelihood",
745        layer_marker: VARIATIONAL_LAYER_MARKER_V0,
746        feature_gate: VARIATIONAL_FEATURE_GATE_V0,
747        selector_name: selector_name.into(),
748        factor_count: factors.len(),
749        factors,
750        log_likelihood_bits,
751    }
752}
753
754pub fn emission_likelihood_factor_v0(
755    source: &'static str,
756    contribution_bits: f64,
757    reason: Option<&'static str>,
758) -> EmissionLikelihoodFactorV0 {
759    EmissionLikelihoodFactorV0 {
760        schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
761        product: "omena-variational.emission-likelihood-factor",
762        layer_marker: VARIATIONAL_LAYER_MARKER_V0,
763        feature_gate: VARIATIONAL_FEATURE_GATE_V0,
764        source,
765        factor_name: source,
766        contribution_bits,
767        log_likelihood_bits: contribution_bits,
768        reason,
769    }
770}
771
772pub fn provenance_posterior_annotation_v0(
773    annotation_id: impl Into<String>,
774    annotations: Vec<ProvenancePosteriorNodeV0>,
775) -> ProvenancePosteriorAnnotationV0 {
776    let provenance = annotations.first().map(|node| node.provenance);
777    ProvenancePosteriorAnnotationV0 {
778        schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
779        product: "omena-variational.provenance-posterior-annotation",
780        layer_marker: VARIATIONAL_LAYER_MARKER_V0,
781        feature_gate: VARIATIONAL_FEATURE_GATE_V0,
782        node_count: annotations.len(),
783        annotations,
784        provenance,
785        annotation_id: annotation_id.into(),
786        mutates_existing_provenance_enum: false,
787    }
788}
789
790pub fn provenance_posterior_node_v0(
791    provenance: AbstractClassValueProvenanceV0,
792    posterior_logit_bits: f64,
793    likelihood_factor_bits: f64,
794) -> ProvenancePosteriorNodeV0 {
795    ProvenancePosteriorNodeV0 {
796        schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
797        product: "omena-variational.provenance-posterior-node",
798        layer_marker: VARIATIONAL_LAYER_MARKER_V0,
799        feature_gate: VARIATIONAL_FEATURE_GATE_V0,
800        provenance,
801        posterior_logit_bits,
802        likelihood_factor_bits,
803    }
804}
805
806#[cfg(test)]
807mod tests {
808    use super::*;
809
810    #[test]
811    fn posterior_is_default_off_and_bits_only() {
812        let posterior = summarize_variational_default_posterior_v0(".button");
813        assert_eq!(posterior.schema_version, "0");
814        assert_eq!(posterior.layer_marker, "variational-cascade");
815        assert!(!posterior.enabled_by_default);
816        assert_eq!(unit::nats_to_bits(std::f64::consts::LN_2), 1.0);
817    }
818
819    #[test]
820    fn posterior_inference_uses_selector_and_cascade_features() {
821        let bem = infer_designer_intent_posterior_v0(designer_intent_posterior_input_v0(
822            ".button--primary",
823            2,
824            1,
825            0,
826        ));
827        let utility = infer_designer_intent_posterior_v0(designer_intent_posterior_input_v0(
828            ".u-color-red",
829            2,
830            1,
831            0,
832        ));
833
834        assert_eq!(bem.mode, DesignerIntentPosteriorModeV0::VciFormal);
835        assert!(bem.enabled_by_default);
836        assert_eq!(
837            dominant_designer_intent_v0(&bem),
838            Some(PatternIntentV0::Bem)
839        );
840        assert_eq!(
841            dominant_designer_intent_v0(&utility),
842            Some(PatternIntentV0::Utility)
843        );
844        assert_ne!(
845            bem.scores.first().map(|score| score.intent),
846            utility.scores.first().map(|score| score.intent)
847        );
848    }
849
850    #[test]
851    fn belief_propagation_trace_carries_non_tautological_factor_messages() {
852        let tied = designer_intent_posterior_input_v0(".button--primary", 2, 1, 0);
853        let explicit = DesignerIntentPosteriorInputV0 {
854            duplicate_property_tie_count: 0,
855            ..tied.clone()
856        };
857        let tied_trace = designer_intent_belief_propagation_trace_v0(&tied);
858        let explicit_trace = designer_intent_belief_propagation_trace_v0(&explicit);
859
860        assert_eq!(tied_trace.factor_count, 5);
861        assert!(tied_trace.iteration_count > 1);
862        assert!(tied_trace.converged);
863        assert!(
864            tied_trace.max_delta_bits <= DESIGNER_INTENT_BP_CONVERGENCE_EPSILON_BITS_V0,
865            "final iteration should satisfy posterior fixpoint tolerance"
866        );
867        assert!(
868            tied_trace.free_energy_delta_bits <= DESIGNER_INTENT_BP_CONVERGENCE_EPSILON_BITS_V0,
869            "free-energy objective should participate in convergence"
870        );
871        assert_eq!(
872            tied_trace.message_count,
873            tied_trace.messages.len(),
874            "trace message count must reflect retained iteration evidence"
875        );
876        assert!(
877            tied_trace.message_count > 25,
878            "iterative belief propagation should retain more than one factor-to-intent sweep"
879        );
880        assert!(tied_trace.messages.iter().any(|message| {
881            message.direction == DesignerIntentMessageDirectionV0::IntentToFactor
882        }));
883        assert!(tied_trace.messages.iter().any(|message| {
884            message.direction == DesignerIntentMessageDirectionV0::FactorToIntent
885        }));
886        assert!(tied_trace.messages.iter().any(|message| {
887            message.direction == DesignerIntentMessageDirectionV0::FactorToIntent
888                && message.source_factor == "selector-bem-marker"
889                && message.target_intent == PatternIntentV0::Bem
890                && message.message_bits > 0.0
891        }));
892        assert!(tied_trace.messages.iter().any(|message| {
893            message.direction == DesignerIntentMessageDirectionV0::FactorToIntent
894                && message.source_factor == "source-order-tie"
895                && message.target_intent == PatternIntentV0::Bem
896                && message.message_bits < 0.0
897        }));
898
899        let single_sweep_trace = designer_intent_single_sweep_trace_for_test_v0(&tied);
900        let single_sweep_bem_bits =
901            score_bits_for_intent(&single_sweep_trace, PatternIntentV0::Bem);
902        let tied_bem_bits = score_bits_for_intent(&tied_trace, PatternIntentV0::Bem);
903        assert_ne!(
904            tied_bem_bits, single_sweep_bem_bits,
905            "coupled iterative messages must change the posterior relative to the previous single-sweep mechanism"
906        );
907
908        let explicit_bem_bits = score_bits_for_intent(&explicit_trace, PatternIntentV0::Bem);
909        assert!(
910            tied_bem_bits < explicit_bem_bits,
911            "source-order tie factor must lower the BEM posterior instead of leaving the fixture tautological"
912        );
913    }
914
915    fn designer_intent_single_sweep_trace_for_test_v0(
916        input: &DesignerIntentPosteriorInputV0,
917    ) -> DesignerIntentBeliefPropagationTraceV0 {
918        let selector = normalize_selector_name_for_intent_v0(&input.selector_name);
919        let factors = designer_intent_evidence_factors_v0(&selector, input);
920        let intents = [
921            PatternIntentV0::Bem,
922            PatternIntentV0::Utility,
923            PatternIntentV0::Atomic,
924            PatternIntentV0::Hybrid,
925            PatternIntentV0::AdHoc,
926        ];
927        let prior_log_probability_bits = -(intents.len() as f64).log2();
928        let factor_to_intent_messages = factors
929            .iter()
930            .map(|factor| {
931                intents
932                    .iter()
933                    .map(|intent| factor.message_bits_for(*intent))
934                    .collect::<Vec<_>>()
935            })
936            .collect::<Vec<_>>();
937        let posterior_log_probability_bits = posterior_log_probability_bits_from_messages_v0(
938            prior_log_probability_bits,
939            &factor_to_intent_messages,
940        );
941        let posterior_scores = designer_intent_scores_from_log_probabilities_v0(
942            &intents,
943            &posterior_log_probability_bits,
944        );
945
946        DesignerIntentBeliefPropagationTraceV0 {
947            schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
948            product: "omena-variational.designer-intent-belief-propagation",
949            layer_marker: VARIATIONAL_LAYER_MARKER_V0,
950            feature_gate: VARIATIONAL_FEATURE_GATE_V0,
951            selector_name: input.selector_name.clone(),
952            factor_count: factors.len(),
953            iteration_count: 1,
954            converged: true,
955            max_delta_bits: 0.0,
956            free_energy_delta_bits: 0.0,
957            free_energy: variational_free_energy_from_beliefs_v0(
958                &posterior_log_probability_bits,
959                prior_log_probability_bits,
960                &factor_to_intent_messages,
961            ),
962            message_count: factors.len() * intents.len(),
963            messages: factors
964                .iter()
965                .flat_map(|factor| {
966                    intents
967                        .iter()
968                        .map(|intent| DesignerIntentBeliefPropagationMessageV0 {
969                            schema_version: VARIATIONAL_SCHEMA_VERSION_V0,
970                            product: "omena-variational.designer-intent-bp-message",
971                            layer_marker: VARIATIONAL_LAYER_MARKER_V0,
972                            feature_gate: VARIATIONAL_FEATURE_GATE_V0,
973                            iteration_index: 0,
974                            direction: DesignerIntentMessageDirectionV0::FactorToIntent,
975                            source_factor: factor.source_factor,
976                            target_intent: *intent,
977                            message_bits: factor.message_bits_for(*intent),
978                        })
979                })
980                .collect(),
981            posterior_scores,
982        }
983    }
984
985    #[test]
986    fn uniform_dirichlet_prior_covers_all_pattern_intents_in_bits() {
987        let prior = uniform_pattern_prior_v0("fixture-corpus-sha256");
988        assert_eq!(prior.schema_version, "0");
989        assert_eq!(prior.kind, PatternPriorKindV0::UniformDirichlet);
990        assert_eq!(
991            prior
992                .dirichlet_alpha
993                .iter()
994                .map(|alpha| alpha.intent.as_str())
995                .collect::<Vec<_>>(),
996            vec!["bem", "utility", "atomic", "hybrid", "adHoc"]
997        );
998        assert_eq!(prior.concentration_bits, 5.0);
999        let calibration = prior.corpus_calibration.as_ref();
1000        assert_eq!(
1001            calibration.map(|calibration| calibration.axis_a_schema_version),
1002            Some("0")
1003        );
1004        assert_eq!(
1005            calibration.map(|calibration| calibration.calibration_scope),
1006            Some("fixtureUniformNoCorpusCalibration")
1007        );
1008    }
1009
1010    #[test]
1011    fn likelihood_and_vfe_stay_at_bits_boundary() {
1012        let likelihood = emission_likelihood_v0(
1013            ".button",
1014            vec![
1015                emission_likelihood_factor_v0("cascadeProof", -1.0, Some("proof accepted")),
1016                emission_likelihood_factor_v0("specificityFit", -2.5, None),
1017            ],
1018        );
1019        let energy = variational_free_energy_v0(8.0, 3.5);
1020
1021        assert_eq!(likelihood.factor_count, 2);
1022        assert_eq!(likelihood.log_likelihood_bits, -3.5);
1023        assert_eq!(energy.free_energy_bits, 4.5);
1024        assert_eq!(unit::bits_to_nats(unit::nats_to_bits(2.0)), 2.0);
1025    }
1026
1027    #[test]
1028    fn posterior_annotation_is_sidecar_only() {
1029        let annotation = provenance_posterior_annotation_v0(
1030            "annotation",
1031            vec![provenance_posterior_node_v0(
1032                AbstractClassValueProvenanceV0::FiniteSetWideningChars,
1033                -0.25,
1034                -1.5,
1035            )],
1036        );
1037
1038        assert_eq!(annotation.node_count, 1);
1039        assert_eq!(
1040            annotation.provenance,
1041            Some(AbstractClassValueProvenanceV0::FiniteSetWideningChars)
1042        );
1043        assert!(!annotation.mutates_existing_provenance_enum);
1044    }
1045
1046    fn score_bits_for_intent(
1047        trace: &DesignerIntentBeliefPropagationTraceV0,
1048        intent: PatternIntentV0,
1049    ) -> f64 {
1050        trace
1051            .posterior_scores
1052            .iter()
1053            .find_map(|score| (score.intent == intent).then_some(score.log_probability_bits))
1054            .unwrap_or(f64::NEG_INFINITY)
1055    }
1056}