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#[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}