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