1use 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}