Skip to main content

omena_query/
explain.rs

1use omena_evidence_graph::{EvidenceNodeKeyV0, GuaranteeKindV0};
2use omena_parser::{ClosedWorldBundleV0, ParserPositionV0, ParserRangeV0};
3use omena_query_core::{FactPrecision, fact_precision_from_analysis_precision};
4use omena_query_transform_runner::{
5    TransformDecision, TransformExecutionContextV0, TransformSemanticGuaranteeTierV0,
6    TransformStrictPolicyEventV0, TransformStrictPolicySummaryV0,
7};
8use serde::Serialize;
9
10use crate::{
11    OmenaQueryCascadeAtPositionV0, OmenaQuerySourcePrecisionReferenceV0,
12    OmenaQueryStyleDiagnosticV0,
13};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
16#[serde(rename_all = "camelCase")]
17pub enum OmenaQueryExplainAvailabilityV0 {
18    Available,
19    NotYetAvailable,
20    NotFound,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
24#[serde(rename_all = "camelCase")]
25pub enum OmenaQueryExplainCapabilityV0 {
26    Diagnostic,
27    Transform,
28    TreeShake,
29    Precision,
30    Cascade,
31    Bundle,
32    HoverTrace,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
36#[serde(rename_all = "camelCase")]
37pub enum OmenaQueryExplainSymbolKindV0 {
38    Class,
39    Keyframes,
40    Value,
41    CustomProperty,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
45#[serde(
46    tag = "kind",
47    rename_all = "camelCase",
48    rename_all_fields = "camelCase"
49)]
50pub enum OmenaQueryExplainTargetV0 {
51    Diagnostic {
52        style_path: String,
53        code: String,
54        range: ParserRangeV0,
55    },
56    Transform {
57        pass_id: String,
58        decision_ordinal: usize,
59    },
60    TreeShake {
61        symbol_kind: OmenaQueryExplainSymbolKindV0,
62        symbol_name: String,
63    },
64    Precision {
65        source_path: String,
66        variable_name: String,
67        reference_byte_offset: usize,
68    },
69    Cascade {
70        style_path: String,
71        position: ParserPositionV0,
72    },
73    Bundle {
74        chunk_reference: String,
75    },
76    HoverTrace {
77        document_uri: String,
78        position: Option<ParserPositionV0>,
79    },
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
83#[serde(
84    tag = "kind",
85    rename_all = "camelCase",
86    rename_all_fields = "camelCase"
87)]
88pub enum OmenaQueryExplainFactReferenceV0 {
89    Diagnostic {
90        style_path: String,
91        code: String,
92        range: ParserRangeV0,
93        evidence_node_key: EvidenceNodeKeyV0,
94    },
95    TransformOutcome {
96        pass_id: String,
97        decision_ordinal: usize,
98        evidence_node_key: EvidenceNodeKeyV0,
99    },
100    ClosedWorldReachability {
101        closure_hash: String,
102        symbol_kind: OmenaQueryExplainSymbolKindV0,
103        symbol_name: String,
104        guarantee: GuaranteeKindV0,
105    },
106    PrecisionFact {
107        source_path: String,
108        variable_name: String,
109        reference_byte_offset: usize,
110    },
111    CascadeResolution {
112        style_path: String,
113        position: ParserPositionV0,
114        winner_range: Option<ParserRangeV0>,
115    },
116    CapabilityGate {
117        capability: OmenaQueryExplainCapabilityV0,
118    },
119    HoverResolution {
120        document_uri: String,
121        position: Option<ParserPositionV0>,
122        reason_code: String,
123    },
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
127#[serde(
128    tag = "kind",
129    rename_all = "camelCase",
130    rename_all_fields = "camelCase"
131)]
132pub enum OmenaQueryExplainFactValueV0 {
133    DiagnosticIdentity {
134        severity: String,
135    },
136    ProvenanceLabel {
137        label: String,
138    },
139    TransformDecision {
140        decision_kind: &'static str,
141        status: String,
142        mutation_count: usize,
143        provenance_preserved: bool,
144        #[serde(skip_serializing_if = "Option::is_none")]
145        semantic_guarantee_tier: Option<TransformSemanticGuaranteeTierV0>,
146        refused_count: usize,
147        rolled_back_count: usize,
148        refusal_reasons: Vec<TransformStrictPolicyEventV0>,
149        rollback_reasons: Vec<TransformStrictPolicyEventV0>,
150    },
151    ReachabilityMembership {
152        reachable: bool,
153    },
154    PrecisionClassification {
155        precision: FactPrecision,
156        resolved_tier: String,
157    },
158    CascadeResolution {
159        status: String,
160        candidate_count: usize,
161        winner_source_order: Option<usize>,
162    },
163    CapabilityAvailability {
164        availability: OmenaQueryExplainAvailabilityV0,
165    },
166    HoverResolution {
167        matched: bool,
168        candidate_count: usize,
169        definition_count: usize,
170    },
171}
172
173/// A fact-backed explanation component.
174///
175/// The fields are intentionally private so callers cannot construct a prose-only
176/// explanation without a typed reference.
177///
178/// ```compile_fail
179/// use omena_query::OmenaQueryExplainFactV0;
180///
181/// let _ = OmenaQueryExplainFactV0 {};
182/// ```
183#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
184#[serde(rename_all = "camelCase")]
185pub struct OmenaQueryExplainFactV0 {
186    reference: OmenaQueryExplainFactReferenceV0,
187    value: OmenaQueryExplainFactValueV0,
188}
189
190impl OmenaQueryExplainFactV0 {
191    fn new(
192        reference: OmenaQueryExplainFactReferenceV0,
193        value: OmenaQueryExplainFactValueV0,
194    ) -> Self {
195        Self { reference, value }
196    }
197
198    pub fn reference(&self) -> &OmenaQueryExplainFactReferenceV0 {
199        &self.reference
200    }
201
202    pub fn value(&self) -> &OmenaQueryExplainFactValueV0 {
203        &self.value
204    }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
208#[serde(rename_all = "camelCase")]
209pub struct OmenaQueryExplainSourceSpanV0 {
210    source_path: String,
211    range: ParserRangeV0,
212    fact_reference: OmenaQueryExplainFactReferenceV0,
213}
214
215impl OmenaQueryExplainSourceSpanV0 {
216    fn new(
217        source_path: impl Into<String>,
218        range: ParserRangeV0,
219        fact_reference: OmenaQueryExplainFactReferenceV0,
220    ) -> Self {
221        Self {
222            source_path: source_path.into(),
223            range,
224            fact_reference,
225        }
226    }
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
230#[serde(rename_all = "camelCase")]
231pub struct OmenaQueryExplainResponseV0 {
232    schema_version: &'static str,
233    product: &'static str,
234    target: OmenaQueryExplainTargetV0,
235    availability: OmenaQueryExplainAvailabilityV0,
236    primary_fact: OmenaQueryExplainFactV0,
237    supporting_facts: Vec<OmenaQueryExplainFactV0>,
238    related_spans: Vec<OmenaQueryExplainSourceSpanV0>,
239}
240
241impl OmenaQueryExplainResponseV0 {
242    fn new(
243        target: OmenaQueryExplainTargetV0,
244        availability: OmenaQueryExplainAvailabilityV0,
245        primary_fact: OmenaQueryExplainFactV0,
246        supporting_facts: Vec<OmenaQueryExplainFactV0>,
247        related_spans: Vec<OmenaQueryExplainSourceSpanV0>,
248    ) -> Self {
249        Self {
250            schema_version: "0",
251            product: "omena-query.explain",
252            target,
253            availability,
254            primary_fact,
255            supporting_facts,
256            related_spans,
257        }
258    }
259
260    pub fn target(&self) -> &OmenaQueryExplainTargetV0 {
261        &self.target
262    }
263
264    pub const fn availability(&self) -> OmenaQueryExplainAvailabilityV0 {
265        self.availability
266    }
267
268    pub fn primary_fact(&self) -> &OmenaQueryExplainFactV0 {
269        &self.primary_fact
270    }
271
272    pub fn supporting_facts(&self) -> &[OmenaQueryExplainFactV0] {
273        self.supporting_facts.as_slice()
274    }
275
276    pub fn related_spans(&self) -> &[OmenaQueryExplainSourceSpanV0] {
277        self.related_spans.as_slice()
278    }
279}
280
281pub enum OmenaQueryExplainInputV0<'a> {
282    Diagnostic {
283        style_path: &'a str,
284        diagnostic: &'a OmenaQueryStyleDiagnosticV0,
285    },
286    Transform {
287        decision: &'a TransformDecision,
288        decision_ordinal: usize,
289    },
290    TransformWithPolicy {
291        decision: &'a TransformDecision,
292        decision_ordinal: usize,
293        strict_policy: &'a TransformStrictPolicySummaryV0,
294    },
295    TreeShake {
296        bundle: &'a ClosedWorldBundleV0,
297        symbol_kind: OmenaQueryExplainSymbolKindV0,
298        symbol_name: &'a str,
299    },
300    Precision {
301        reference: &'a OmenaQuerySourcePrecisionReferenceV0,
302    },
303    Cascade {
304        result: &'a OmenaQueryCascadeAtPositionV0,
305    },
306    BundleUnavailable {
307        chunk_reference: &'a str,
308    },
309    HoverTrace {
310        document_uri: &'a str,
311        position: Option<ParserPositionV0>,
312        reason_code: &'a str,
313        matched: bool,
314        candidate_count: usize,
315        definition_count: usize,
316    },
317}
318
319pub fn explain_omena_query(input: OmenaQueryExplainInputV0<'_>) -> OmenaQueryExplainResponseV0 {
320    match input {
321        OmenaQueryExplainInputV0::Diagnostic {
322            style_path,
323            diagnostic,
324        } => explain_diagnostic(style_path, diagnostic),
325        OmenaQueryExplainInputV0::Transform {
326            decision,
327            decision_ordinal,
328        } => explain_transform(
329            decision,
330            decision_ordinal,
331            &TransformStrictPolicySummaryV0::default(),
332        ),
333        OmenaQueryExplainInputV0::TransformWithPolicy {
334            decision,
335            decision_ordinal,
336            strict_policy,
337        } => explain_transform(decision, decision_ordinal, strict_policy),
338        OmenaQueryExplainInputV0::TreeShake {
339            bundle,
340            symbol_kind,
341            symbol_name,
342        } => explain_tree_shake(bundle, symbol_kind, symbol_name),
343        OmenaQueryExplainInputV0::Precision { reference } => explain_precision(reference),
344        OmenaQueryExplainInputV0::Cascade { result } => explain_cascade(result),
345        OmenaQueryExplainInputV0::BundleUnavailable { chunk_reference } => {
346            explain_bundle_unavailable(chunk_reference)
347        }
348        OmenaQueryExplainInputV0::HoverTrace {
349            document_uri,
350            position,
351            reason_code,
352            matched,
353            candidate_count,
354            definition_count,
355        } => explain_hover_trace(
356            document_uri,
357            position,
358            reason_code,
359            matched,
360            candidate_count,
361            definition_count,
362        ),
363    }
364}
365
366pub fn explain_omena_query_tree_shake_for_style_source(
367    style_path: &str,
368    style_source: &str,
369    context: &TransformExecutionContextV0,
370    symbol_kind: OmenaQueryExplainSymbolKindV0,
371    symbol_name: &str,
372) -> Option<OmenaQueryExplainResponseV0> {
373    let requested_pass_id = match symbol_kind {
374        OmenaQueryExplainSymbolKindV0::Class => "tree-shake-class",
375        OmenaQueryExplainSymbolKindV0::Keyframes => "tree-shake-keyframes",
376        OmenaQueryExplainSymbolKindV0::Value => "tree-shake-value",
377        OmenaQueryExplainSymbolKindV0::CustomProperty => "tree-shake-custom-property",
378    };
379    let bundle = crate::style::build_closed_world_bundle_for_single_style_source_context(
380        style_path,
381        style_source,
382        &[requested_pass_id.to_string()],
383        context,
384    )?;
385    Some(explain_omena_query(OmenaQueryExplainInputV0::TreeShake {
386        bundle: &bundle,
387        symbol_kind,
388        symbol_name,
389    }))
390}
391
392fn explain_diagnostic(
393    style_path: &str,
394    diagnostic: &OmenaQueryStyleDiagnosticV0,
395) -> OmenaQueryExplainResponseV0 {
396    let evidence_node_key = EvidenceNodeKeyV0::new("diagnosticProvenance", diagnostic.code);
397    let reference = OmenaQueryExplainFactReferenceV0::Diagnostic {
398        style_path: style_path.to_string(),
399        code: diagnostic.code.to_string(),
400        range: diagnostic.range,
401        evidence_node_key,
402    };
403    let supporting_facts = diagnostic
404        .provenance
405        .iter()
406        .map(|label| {
407            OmenaQueryExplainFactV0::new(
408                reference.clone(),
409                OmenaQueryExplainFactValueV0::ProvenanceLabel {
410                    label: (*label).to_string(),
411                },
412            )
413        })
414        .collect();
415
416    OmenaQueryExplainResponseV0::new(
417        OmenaQueryExplainTargetV0::Diagnostic {
418            style_path: style_path.to_string(),
419            code: diagnostic.code.to_string(),
420            range: diagnostic.range,
421        },
422        OmenaQueryExplainAvailabilityV0::Available,
423        OmenaQueryExplainFactV0::new(
424            reference.clone(),
425            OmenaQueryExplainFactValueV0::DiagnosticIdentity {
426                severity: diagnostic.severity.to_string(),
427            },
428        ),
429        supporting_facts,
430        vec![OmenaQueryExplainSourceSpanV0::new(
431            style_path,
432            diagnostic.range,
433            reference,
434        )],
435    )
436}
437
438fn explain_transform(
439    decision: &TransformDecision,
440    decision_ordinal: usize,
441    strict_policy: &TransformStrictPolicySummaryV0,
442) -> OmenaQueryExplainResponseV0 {
443    let outcome = decision.compatibility_outcome();
444    let decision_kind = match decision {
445        TransformDecision::Applied { .. } => "applied",
446        TransformDecision::NoChange { .. } => "noChange",
447        TransformDecision::Blocked { .. } => "blocked",
448        TransformDecision::Rejected { .. } => "rejected",
449    };
450    let reference = OmenaQueryExplainFactReferenceV0::TransformOutcome {
451        pass_id: outcome.pass_id.to_string(),
452        decision_ordinal,
453        evidence_node_key: outcome.evidence_node_key(),
454    };
455    OmenaQueryExplainResponseV0::new(
456        OmenaQueryExplainTargetV0::Transform {
457            pass_id: outcome.pass_id.to_string(),
458            decision_ordinal,
459        },
460        OmenaQueryExplainAvailabilityV0::Available,
461        OmenaQueryExplainFactV0::new(
462            reference,
463            OmenaQueryExplainFactValueV0::TransformDecision {
464                decision_kind,
465                status: format!("{:?}", outcome.status),
466                mutation_count: outcome.mutation_count,
467                provenance_preserved: outcome.provenance_preserved,
468                semantic_guarantee_tier: decision.semantic_guarantee_tier().cloned(),
469                refused_count: strict_policy.refused_count,
470                rolled_back_count: strict_policy.rolled_back_count,
471                refusal_reasons: strict_policy.refusal_reasons.clone(),
472                rollback_reasons: strict_policy.rollback_reasons.clone(),
473            },
474        ),
475        Vec::new(),
476        Vec::new(),
477    )
478}
479
480fn explain_tree_shake(
481    bundle: &ClosedWorldBundleV0,
482    symbol_kind: OmenaQueryExplainSymbolKindV0,
483    symbol_name: &str,
484) -> OmenaQueryExplainResponseV0 {
485    let reachable = match symbol_kind {
486        OmenaQueryExplainSymbolKindV0::Class => bundle
487            .reachability()
488            .class_names()
489            .iter()
490            .any(|candidate| candidate == symbol_name),
491        OmenaQueryExplainSymbolKindV0::Keyframes => bundle
492            .reachability()
493            .keyframe_names()
494            .iter()
495            .any(|candidate| candidate == symbol_name),
496        OmenaQueryExplainSymbolKindV0::Value => bundle
497            .reachability()
498            .value_names()
499            .iter()
500            .any(|candidate| candidate == symbol_name),
501        OmenaQueryExplainSymbolKindV0::CustomProperty => bundle
502            .reachability()
503            .custom_property_names()
504            .iter()
505            .any(|candidate| candidate == symbol_name),
506    };
507    let reference = OmenaQueryExplainFactReferenceV0::ClosedWorldReachability {
508        closure_hash: bundle.closure_hash().to_string(),
509        symbol_kind,
510        symbol_name: symbol_name.to_string(),
511        guarantee: GuaranteeKindV0::NotClaimedExactTraversal,
512    };
513    OmenaQueryExplainResponseV0::new(
514        OmenaQueryExplainTargetV0::TreeShake {
515            symbol_kind,
516            symbol_name: symbol_name.to_string(),
517        },
518        OmenaQueryExplainAvailabilityV0::Available,
519        OmenaQueryExplainFactV0::new(
520            reference,
521            OmenaQueryExplainFactValueV0::ReachabilityMembership { reachable },
522        ),
523        Vec::new(),
524        Vec::new(),
525    )
526}
527
528pub fn explain_omena_query_tree_shake_unavailable(
529    symbol_kind: OmenaQueryExplainSymbolKindV0,
530    symbol_name: &str,
531) -> OmenaQueryExplainResponseV0 {
532    OmenaQueryExplainResponseV0::new(
533        OmenaQueryExplainTargetV0::TreeShake {
534            symbol_kind,
535            symbol_name: symbol_name.to_string(),
536        },
537        OmenaQueryExplainAvailabilityV0::NotYetAvailable,
538        OmenaQueryExplainFactV0::new(
539            OmenaQueryExplainFactReferenceV0::CapabilityGate {
540                capability: OmenaQueryExplainCapabilityV0::TreeShake,
541            },
542            OmenaQueryExplainFactValueV0::CapabilityAvailability {
543                availability: OmenaQueryExplainAvailabilityV0::NotYetAvailable,
544            },
545        ),
546        Vec::new(),
547        Vec::new(),
548    )
549}
550
551fn explain_precision(
552    precision_reference: &OmenaQuerySourcePrecisionReferenceV0,
553) -> OmenaQueryExplainResponseV0 {
554    let reference = OmenaQueryExplainFactReferenceV0::PrecisionFact {
555        source_path: precision_reference.source_path.clone(),
556        variable_name: precision_reference.variable_name.clone(),
557        reference_byte_offset: precision_reference.reference_byte_offset,
558    };
559    OmenaQueryExplainResponseV0::new(
560        OmenaQueryExplainTargetV0::Precision {
561            source_path: precision_reference.source_path.clone(),
562            variable_name: precision_reference.variable_name.clone(),
563            reference_byte_offset: precision_reference.reference_byte_offset,
564        },
565        OmenaQueryExplainAvailabilityV0::Available,
566        OmenaQueryExplainFactV0::new(
567            reference,
568            OmenaQueryExplainFactValueV0::PrecisionClassification {
569                precision: fact_precision_from_analysis_precision(&precision_reference.precision),
570                resolved_tier: precision_reference.resolved_tier.to_string(),
571            },
572        ),
573        Vec::new(),
574        Vec::new(),
575    )
576}
577
578fn explain_cascade(result: &OmenaQueryCascadeAtPositionV0) -> OmenaQueryExplainResponseV0 {
579    let reference = OmenaQueryExplainFactReferenceV0::CascadeResolution {
580        style_path: result.style_path.clone(),
581        position: result.query_position,
582        winner_range: result.winner_declaration_range,
583    };
584    let related_spans = result
585        .winner_declaration_range
586        .map(|range| {
587            vec![OmenaQueryExplainSourceSpanV0::new(
588                result
589                    .winner_declaration_file_path
590                    .as_deref()
591                    .unwrap_or(result.style_path.as_str()),
592                range,
593                reference.clone(),
594            )]
595        })
596        .unwrap_or_default();
597    OmenaQueryExplainResponseV0::new(
598        OmenaQueryExplainTargetV0::Cascade {
599            style_path: result.style_path.clone(),
600            position: result.query_position,
601        },
602        OmenaQueryExplainAvailabilityV0::Available,
603        OmenaQueryExplainFactV0::new(
604            reference,
605            OmenaQueryExplainFactValueV0::CascadeResolution {
606                status: result.status.to_string(),
607                candidate_count: result.candidate_declaration_count,
608                winner_source_order: result.winner_declaration_source_order,
609            },
610        ),
611        Vec::new(),
612        related_spans,
613    )
614}
615
616fn explain_bundle_unavailable(chunk_reference: &str) -> OmenaQueryExplainResponseV0 {
617    OmenaQueryExplainResponseV0::new(
618        OmenaQueryExplainTargetV0::Bundle {
619            chunk_reference: chunk_reference.to_string(),
620        },
621        OmenaQueryExplainAvailabilityV0::NotYetAvailable,
622        OmenaQueryExplainFactV0::new(
623            OmenaQueryExplainFactReferenceV0::CapabilityGate {
624                capability: OmenaQueryExplainCapabilityV0::Bundle,
625            },
626            OmenaQueryExplainFactValueV0::CapabilityAvailability {
627                availability: OmenaQueryExplainAvailabilityV0::NotYetAvailable,
628            },
629        ),
630        Vec::new(),
631        Vec::new(),
632    )
633}
634
635fn explain_hover_trace(
636    document_uri: &str,
637    position: Option<ParserPositionV0>,
638    reason_code: &str,
639    matched: bool,
640    candidate_count: usize,
641    definition_count: usize,
642) -> OmenaQueryExplainResponseV0 {
643    OmenaQueryExplainResponseV0::new(
644        OmenaQueryExplainTargetV0::HoverTrace {
645            document_uri: document_uri.to_string(),
646            position,
647        },
648        OmenaQueryExplainAvailabilityV0::Available,
649        OmenaQueryExplainFactV0::new(
650            OmenaQueryExplainFactReferenceV0::HoverResolution {
651                document_uri: document_uri.to_string(),
652                position,
653                reason_code: reason_code.to_string(),
654            },
655            OmenaQueryExplainFactValueV0::HoverResolution {
656                matched,
657                candidate_count,
658                definition_count,
659            },
660        ),
661        Vec::new(),
662        Vec::new(),
663    )
664}
665
666#[cfg(test)]
667mod tests {
668    use omena_query_transform_runner::TransformCascadeEnvironmentV0;
669
670    use super::*;
671
672    #[test]
673    fn transform_explanation_references_the_production_outcome_evidence_key() {
674        let execution = crate::execute_omena_query_transform_passes_from_source(
675            "fixture.css",
676            ".button {}",
677            &["print-css".to_string()],
678        );
679        let decision = &execution.execution.decisions[0];
680        let outcome = decision.compatibility_outcome();
681        let response = explain_omena_query(OmenaQueryExplainInputV0::Transform {
682            decision,
683            decision_ordinal: 0,
684        });
685
686        assert_eq!(
687            response.availability(),
688            OmenaQueryExplainAvailabilityV0::Available
689        );
690        assert!(matches!(
691            response.primary_fact().reference(),
692            OmenaQueryExplainFactReferenceV0::TransformOutcome {
693                evidence_node_key,
694                ..
695            } if evidence_node_key == &outcome.evidence_node_key()
696        ));
697    }
698
699    #[test]
700    fn transform_explanation_surfaces_strict_policy_counts_and_reasons() {
701        let execution =
702            crate::execute_omena_query_consumer_build_style_source_with_context_and_options(
703                "fixture.css",
704                ".card { color: red; } .card { background: blue; }",
705                &["rule-merging".to_string()],
706                &TransformExecutionContextV0::default(),
707                &crate::OmenaQueryConsumerBuildOptionsV0 {
708                    verification_profile: crate::OmenaQueryBuildVerificationProfileV0::Strict,
709                    ..crate::OmenaQueryConsumerBuildOptionsV0::default()
710                },
711            );
712        let decision = &execution.execution.decisions[0];
713        let response = explain_omena_query(OmenaQueryExplainInputV0::TransformWithPolicy {
714            decision,
715            decision_ordinal: 0,
716            strict_policy: &execution.execution.strict_policy,
717        });
718
719        assert!(matches!(
720            response.primary_fact().value(),
721            OmenaQueryExplainFactValueV0::TransformDecision {
722                refused_count: 1,
723                rolled_back_count: 0,
724                refusal_reasons,
725                rollback_reasons,
726                ..
727            } if refusal_reasons.len() == 1 && rollback_reasons.is_empty()
728        ));
729    }
730
731    #[test]
732    fn transform_explanation_carries_the_typed_semantic_trust_tier() {
733        let execution = crate::execute_omena_query_transform_passes_from_source_with_context(
734            "fixture.css",
735            ".card { color: red; } .card { background: blue; }",
736            &["rule-merging".to_string()],
737            &TransformExecutionContextV0 {
738                cascade_environment: Some(TransformCascadeEnvironmentV0::default()),
739                ..TransformExecutionContextV0::default()
740            },
741        );
742        let decision = &execution.execution.decisions[0];
743        let response = explain_omena_query(OmenaQueryExplainInputV0::Transform {
744            decision,
745            decision_ordinal: 0,
746        });
747
748        assert!(matches!(
749            response.primary_fact().value(),
750            OmenaQueryExplainFactValueV0::TransformDecision {
751                semantic_guarantee_tier: Some(
752                    TransformSemanticGuaranteeTierV0::WinnerEqualityObserved { axes }
753                ),
754                ..
755            } if !axes.is_empty()
756        ));
757    }
758
759    #[test]
760    fn transform_explanation_preserves_typed_trust_absence() {
761        let execution = crate::execute_omena_query_transform_passes_from_source(
762            "fixture.css",
763            "@scope (.root) { .unused {} }",
764            &["empty-rule-removal".to_string()],
765        );
766        let decision = &execution.execution.decisions[0];
767        let response = explain_omena_query(OmenaQueryExplainInputV0::Transform {
768            decision,
769            decision_ordinal: 0,
770        });
771
772        assert!(matches!(
773            response.primary_fact().value(),
774            OmenaQueryExplainFactValueV0::TransformDecision {
775                semantic_guarantee_tier: Some(TransformSemanticGuaranteeTierV0::Absent { reasons }),
776                ..
777            } if !reasons.is_empty()
778        ));
779    }
780
781    #[test]
782    fn tree_shake_explanation_inherits_the_non_exact_traversal_guarantee() -> Result<(), String> {
783        let context = TransformExecutionContextV0 {
784            reachable_class_names: vec!["button".to_string()],
785            ..TransformExecutionContextV0::default()
786        };
787        let response = explain_omena_query_tree_shake_for_style_source(
788            "src/App.module.css",
789            ".button { color: red; }",
790            &context,
791            OmenaQueryExplainSymbolKindV0::Class,
792            "button",
793        )
794        .ok_or_else(|| "linked closed-world fixture should be available".to_string())?;
795
796        assert!(matches!(
797            response.primary_fact().reference(),
798            OmenaQueryExplainFactReferenceV0::ClosedWorldReachability {
799                guarantee: GuaranteeKindV0::NotClaimedExactTraversal,
800                ..
801            }
802        ));
803        assert!(matches!(
804            response.primary_fact().value(),
805            OmenaQueryExplainFactValueV0::ReachabilityMembership { reachable: true }
806        ));
807        Ok(())
808    }
809
810    #[test]
811    fn unavailable_bundle_explanation_is_still_fact_backed() {
812        let response = explain_omena_query(OmenaQueryExplainInputV0::BundleUnavailable {
813            chunk_reference: "main",
814        });
815        assert_eq!(
816            response.availability(),
817            OmenaQueryExplainAvailabilityV0::NotYetAvailable
818        );
819        assert!(matches!(
820            response.primary_fact().reference(),
821            OmenaQueryExplainFactReferenceV0::CapabilityGate {
822                capability: OmenaQueryExplainCapabilityV0::Bundle
823            }
824        ));
825    }
826
827    #[test]
828    fn diagnostic_explanation_reuses_product_diagnostic_provenance() -> Result<(), String> {
829        let diagnostics = crate::summarize_omena_query_style_diagnostics_for_file(
830            "file:///fixture.scss",
831            "@import 'legacy';",
832            &[],
833        );
834        let diagnostic = diagnostics
835            .diagnostics
836            .first()
837            .ok_or_else(|| "fixture should produce a Sass import diagnostic".to_string())?;
838        let response = explain_omena_query(OmenaQueryExplainInputV0::Diagnostic {
839            style_path: "file:///fixture.scss",
840            diagnostic,
841        });
842
843        assert_eq!(
844            response.supporting_facts().len(),
845            diagnostic.provenance.len()
846        );
847        assert!(matches!(
848            response.primary_fact().reference(),
849            OmenaQueryExplainFactReferenceV0::Diagnostic {
850                code,
851                evidence_node_key,
852                ..
853            } if code == diagnostic.code
854                && evidence_node_key == &EvidenceNodeKeyV0::new("diagnosticProvenance", diagnostic.code)
855        ));
856        assert_eq!(response.related_spans().len(), 1);
857        Ok(())
858    }
859
860    #[test]
861    fn precision_explanation_uses_the_authoritative_precision_adapter() -> Result<(), String> {
862        let source = "const className = 'button';\nclassName;";
863        let reference_byte_offset = source
864            .rfind("className")
865            .ok_or_else(|| "fixture reference should exist".to_string())?;
866        let reference = crate::resolve_omena_query_source_precision_for_source(
867            "fixture.ts",
868            source,
869            Some("typescript"),
870            "className",
871            reference_byte_offset,
872        );
873        let response = explain_omena_query(OmenaQueryExplainInputV0::Precision {
874            reference: &reference,
875        });
876
877        assert!(matches!(
878            response.primary_fact().value(),
879            OmenaQueryExplainFactValueV0::PrecisionClassification {
880                precision: FactPrecision::Conservative,
881                ..
882            }
883        ));
884        Ok(())
885    }
886}