Skip to main content

omena_query/
explain.rs

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