Skip to main content

omena_semantic/
evidence.rs

1//! Promotion evidence for parser facts entering the semantic layer.
2//!
3//! These summaries are used by gate checks to verify that parser output is not
4//! merely present, but is promoted into semantic contracts with the expected
5//! counts, certainty, and downstream readiness signals.
6
7use engine_input_producers::EngineInputV2;
8use serde::Serialize;
9
10use crate::{ParserBoundarySyntaxFactsV0, StyleSemanticFactsV0, summarize_source_input_evidence};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
13#[serde(rename_all = "camelCase")]
14pub struct SemanticPromotionEvidenceSummaryV0 {
15    pub schema_version: &'static str,
16    pub product: &'static str,
17    pub items: Vec<SemanticPromotionEvidenceItemV0>,
18    pub blocking_gaps: Vec<&'static str>,
19    pub next_priorities: Vec<&'static str>,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
23#[serde(rename_all = "camelCase")]
24pub struct SemanticPromotionEvidenceItemV0 {
25    pub evidence: &'static str,
26    pub status: &'static str,
27    pub provider: &'static str,
28    pub observed_count: usize,
29    pub reason: String,
30}
31
32pub fn summarize_semantic_promotion_evidence(
33    parser_facts: &ParserBoundarySyntaxFactsV0,
34    semantic_facts: &StyleSemanticFactsV0,
35) -> SemanticPromotionEvidenceSummaryV0 {
36    let source_span_ready = parser_facts.lossless_cst.all_token_spans_within_source
37        && parser_facts.lossless_cst.all_node_spans_within_source;
38    let unresolved_sass_count = semantic_facts
39        .sass
40        .selectors_with_unresolved_variable_refs_names
41        .len()
42        + semantic_facts
43            .sass
44            .selectors_with_unresolved_mixin_includes_names
45            .len();
46    let rewrite_blocker_count =
47        semantic_facts.selector_identity.nested_unsafe_names.len() + unresolved_sass_count;
48    let custom_property_seed_count = parser_facts.custom_properties.decl_names.len()
49        + parser_facts.custom_properties.ref_names.len();
50
51    let items = vec![
52        SemanticPromotionEvidenceItemV0 {
53            evidence: "selectorCanonicalId",
54            status: if semantic_facts.selector_identity.canonical_names.is_empty() {
55                "gap"
56            } else {
57                "ready"
58            },
59            provider: "StyleSelectorIdentityFactsV0.canonicalNames",
60            observed_count: semantic_facts.selector_identity.canonical_names.len(),
61            reason: format!(
62                "{} canonical selector ids are exposed",
63                semantic_facts.selector_identity.canonical_names.len()
64            ),
65        },
66        SemanticPromotionEvidenceItemV0 {
67            evidence: "sourceSpan",
68            status: if source_span_ready { "ready" } else { "gap" },
69            provider: "ParserLosslessCstFactsV0",
70            observed_count: parser_facts.lossless_cst.token_count,
71            reason: format!(
72                "token spans valid={} node spans valid={}",
73                parser_facts.lossless_cst.all_token_spans_within_source,
74                parser_facts.lossless_cst.all_node_spans_within_source
75            ),
76        },
77        SemanticPromotionEvidenceItemV0 {
78            evidence: "bindingOrigin",
79            status: "partial",
80            provider: "StyleSassSemanticFactsV0.selectorSymbolFacts",
81            observed_count: semantic_facts.sass.selector_symbol_facts.len(),
82            reason: format!(
83                "{} Sass selector symbol facts are exposed; cross-file origin still needs source bridge evidence",
84                semantic_facts.sass.selector_symbol_facts.len()
85            ),
86        },
87        SemanticPromotionEvidenceItemV0 {
88            evidence: "styleModuleEdge",
89            status: if parser_facts.sass.module_use_edges.is_empty() {
90                "partial"
91            } else {
92                "ready"
93            },
94            provider: "ParserSassSyntaxFactsV0.moduleUseEdges",
95            observed_count: parser_facts.sass.module_use_edges.len(),
96            reason: format!(
97                "{} Sass module use edges are exposed",
98                parser_facts.sass.module_use_edges.len()
99            ),
100        },
101        SemanticPromotionEvidenceItemV0 {
102            evidence: "valueDomainExplanation",
103            status: "partial",
104            provider: "ParserIndexValueFactsV0",
105            observed_count: parser_facts.values.ref_names.len(),
106            reason: format!(
107                "{} value refs are exposed; source-side expression-domain explanation remains external",
108                parser_facts.values.ref_names.len()
109            ),
110        },
111        SemanticPromotionEvidenceItemV0 {
112            evidence: "designTokenSeed",
113            status: if custom_property_seed_count == 0 {
114                "partial"
115            } else {
116                "ready"
117            },
118            provider: "ParserIndexCustomPropertyFactsV0",
119            observed_count: custom_property_seed_count,
120            reason: format!(
121                "{} CSS custom property declarations and {} var() references are exposed",
122                parser_facts.custom_properties.decl_names.len(),
123                parser_facts.custom_properties.ref_names.len()
124            ),
125        },
126        SemanticPromotionEvidenceItemV0 {
127            evidence: "rewriteSafetyBlocker",
128            status: if rewrite_blocker_count == 0 {
129                "ready"
130            } else {
131                "partial"
132            },
133            provider: "StyleSelectorIdentityFactsV0 + StyleSassSemanticFactsV0",
134            observed_count: rewrite_blocker_count,
135            reason: format!("{rewrite_blocker_count} selector or Sass blockers are exposed"),
136        },
137        SemanticPromotionEvidenceItemV0 {
138            evidence: "referenceSiteIdentity",
139            status: "gap",
140            provider: "EngineInputV2.selector-usage",
141            observed_count: 0,
142            reason: "reference sites are produced outside this parser-backed semantic boundary"
143                .to_string(),
144        },
145        SemanticPromotionEvidenceItemV0 {
146            evidence: "certaintyReason",
147            status: "gap",
148            provider: "EngineInputV2.type-facts",
149            observed_count: 0,
150            reason: "source-side certainty reasons are not yet carried into omena-semantic"
151                .to_string(),
152        },
153    ];
154
155    SemanticPromotionEvidenceSummaryV0 {
156        schema_version: "0",
157        product: "omena-semantic.promotion-evidence",
158        items,
159        blocking_gaps: vec!["referenceSiteIdentity", "certaintyReason"],
160        next_priorities: vec!["referenceSiteIdentity", "certaintyReason", "bindingOrigin"],
161    }
162}
163
164pub fn summarize_semantic_promotion_evidence_with_source_input(
165    parser_facts: &ParserBoundarySyntaxFactsV0,
166    semantic_facts: &StyleSemanticFactsV0,
167    input: &EngineInputV2,
168) -> SemanticPromotionEvidenceSummaryV0 {
169    let source_evidence = summarize_source_input_evidence(input);
170    let mut summary = summarize_semantic_promotion_evidence(parser_facts, semantic_facts);
171
172    for item in &mut summary.items {
173        match item.evidence {
174            "bindingOrigin" => {
175                item.status = source_evidence.binding_origin.status;
176                item.provider = "EngineInputV2.class-expressions";
177                item.observed_count = source_evidence.binding_origin.expression_count;
178                item.reason = format!(
179                    "{} source class expressions expose binding origins",
180                    source_evidence.binding_origin.expression_count
181                );
182            }
183            "styleModuleEdge" => {
184                item.status = source_evidence.style_module_edge.status;
185                item.provider = "EngineInputV2.source-style-edges";
186                item.observed_count = source_evidence.style_module_edge.source_style_edge_count;
187                item.reason = format!(
188                    "{} source-to-style module edges are linked",
189                    source_evidence.style_module_edge.source_style_edge_count
190                );
191            }
192            "valueDomainExplanation" => {
193                item.status = source_evidence.value_domain_explanation.status;
194                item.provider = "EngineInputV2.expression-semantics";
195                item.observed_count = source_evidence.value_domain_explanation.expression_count;
196                item.reason = format!(
197                    "{} source expressions expose value-domain explanations",
198                    source_evidence.value_domain_explanation.expression_count
199                );
200            }
201            "referenceSiteIdentity" => {
202                item.status = source_evidence.reference_site_identity.status;
203                item.provider = "EngineInputV2.selector-usage";
204                item.observed_count = source_evidence.reference_site_identity.reference_site_count;
205                item.reason = format!(
206                    "{} selector reference sites are identity-preserving",
207                    source_evidence.reference_site_identity.reference_site_count
208                );
209            }
210            "certaintyReason" => {
211                item.status = source_evidence.certainty_reason.status;
212                item.provider = "EngineInputV2.expression-semantics";
213                item.observed_count = source_evidence.certainty_reason.expression_count;
214                item.reason = format!(
215                    "{} source expressions expose selector certainty reasons",
216                    source_evidence.certainty_reason.expression_count
217                );
218            }
219            _ => {}
220        }
221    }
222
223    summary.blocking_gaps = summary
224        .items
225        .iter()
226        .filter(|item| item.status == "gap")
227        .map(|item| item.evidence)
228        .collect();
229    summary.next_priorities = source_evidence.blocking_gaps;
230    summary
231}