1pub 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}